赞
踩
目录
3D游戏制作的大作业,先放要求:
游戏成果截图:
下面将按功能讲解具体实现
游戏的整体框架沿用之前的架构,用工厂单实例来产生箭射出。整体结构如下:
工欲善其事,必先利其器。首先要导入资源包,包括天空盒资源、树草资源、靶子资源和十字弩资源。这些都是免费资源~
游戏需要让玩家在地图中以第一人称视角在地图上游走,包括前后左右移动、跳跃和移动视角。首先要创建玩家对象,这里创建的是一个胶囊体capsule,去除掉原来的胶囊碰撞体,添加Character Controller作为新的碰撞体。
为了实现第一人称的效果,把相机调整到人物的上半部分,并挂载到人物上作为人物的子对象。射击用的十字弩也调整到合适位置并,挂载到相机下作为相机的子对象,这样摄像机移动时弓也能同步移动。为了检测玩家到地面的距离(防止一直跳上天的bug),还要添加一个检测对象在人物底部。这样对玩家对象的创建就结束了。
(个人想法)胶囊体什么都没有感觉有点突兀,所以直接在mesh render中去掉材料了,按一个减号就行,这样就能让人物变得透明同时还有碰撞体积。
用代码来控制人的移动、跳跃和相机视角的转动,下面两个代码分别挂载到人物和相机上。这两段代码是在网上搜的,但是现在找不到来源了……
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
-
- public class PlayerController : MonoBehaviour
- {
- //获得player的CharacterController组件
- private CharacterController cc;
-
- public float moveSpeed = 80.0f;//移动速度
-
- public float jumpSpeed = 50.0f;//跳跃速度
-
- //定义获得按键值的两个变量
- private float horizontalMove, verticalMove;
-
- //定义三维变量dir控制方向
- private Vector3 dir;
-
- //重力
- private float gravity = 9.8f;
-
- private Vector3 velocity;//用来控制Y轴速度
-
- //我们只需要检测player是否在地上就可以了,这里我们可以使用Physics中的CheckSphere方法,如果定义的球体和物体发生碰撞,返回真
- //为了使用这个方法,我们需要定义几个变量
- public Transform groundCheck;//检测点的中心位置
- public float checkRedius;//检测点的半径
- public LayerMask groundLayer;//需要检测的图层
- //布尔值来存储CheckSphere的返回值
- public bool isGround;
-
-
- private void Start()
- {
- //获取player的CharacterController组件
- cc = GetComponent<CharacterController>();
- //锁定鼠标后再解锁,鼠标将自动回到屏幕中心
- Cursor.lockState = CursorLockMode.Locked;
- // Cursor.lockState = CursorLockMode.None;
- //隐藏鼠标
- Cursor.visible = false;
- }
- private void Update()
- {
- isGround = Physics.CheckSphere(groundCheck.position,checkRedius,groundLayer);
- if(isGround && velocity.y < 0)
- {
- velocity.y = -2f;
- }
-
- horizontalMove = Input.GetAxis("Horizontal") *2 * moveSpeed;
- verticalMove = Input.GetAxis("Vertical") * 2 * moveSpeed;
-
- dir = transform.forward * verticalMove + transform.right * horizontalMove;
- cc.Move(dir * Time.deltaTime);
-
-
- //在一瞬间有一个向上的速度,在过程中也会随着重力慢慢下降,如果想要让它只跳跃一次的话,加上isGround就行了
- if(Input.GetKeyDown(KeyCode.Space) && isGround)
- {
- velocity.y = jumpSpeed*1.4f;
- }
-
- velocity.y -= gravity * Time.deltaTime;
- //再用CharacterController的Move方法来移动y轴
- cc.Move(velocity * Time.deltaTime);
- }
- }
控制相机跟随:
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
-
-
- public class CameraController : MonoBehaviour
- {
- //我们通过控制Player的旋转方法,来控制相机视角的左右移动,所以我们需要一个Player的Tranform
- public Transform player;
-
-
- //定义两个float变量,来获取鼠标移动的值
- private float mouseX, mouseY;
- //我们可以给鼠标增加一个灵敏度
- public float mouseSensitivity = 800f;
-
- //mouseY中的GetAxis方法会返回-1到1之间的浮点数,在鼠标移动的时候,数值会随着方向的变化而变化,在鼠标不动时,数值会回弹到0,所以我们就会遇到鼠标上下移动时回弹的问题
- public float xRotation;
-
- private void Update()
- {
- //在Update方法中,我们使用输入系统中的GetAxis方法来获取鼠标移动的值,乘以鼠标灵敏度再乘以Time.deltatime,鼠标移动的值就这样得到了
- //Input.GetAxis:它会在鼠标移动相应对应轴的过程中返回 -1 到 1 的值
- mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime;
- mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;
-
- xRotation -= mouseY;
-
- //使用数学函数Clamp限制
- xRotation = Mathf.Clamp(xRotation,-50f,60f);
-
- //Vector3.up是向上的一个三维变量,和一个0,1,0的三维变量是一样的
- //我们需要控制player的y轴旋转才能让它左右旋转
- player.Rotate(Vector3.up * mouseX);
- //接下来我们要选转相机了,我们使用tranform.localRotation方法,让相机上下旋转,使用localRotation就可以不被父对象旋转影响,造成一些奇怪的问题
- //因为localRotation是属性,我们还要给他赋值
- transform.localRotation = Quaternion.Euler(xRotation, 0, 0);
- }
- }
自行创建地形terrain,用自带的工具自行发挥创作。为了让上面的groundcheck能检测到东西,需要给地形的Layer设置成Ground,这个是需要自己创建并添加的。
种的树和草用的都是导入资源包里的预制。初始的预制是没有碰撞体积的,要想有碰撞体积,我采取的方法是编辑预制,根据树的形状手动加碰撞体,然后用让人物变透明的一样的方法让拟合的物体变透明。
天空盒资源用的是导入的资源包Fantasy Skybox FREE。作业要求是天空盒变化就好,但为了视觉效果更真实,让光照也跟着天空盒一起变化,光照的方向尽可能和贴图中太阳的方向一样。为了让场景的视觉效果更好,还调整了一系列参数,参考的教程是这个:
Unity中调整光照特效的7个技巧_unity怎么让场景变亮-CSDN博客
下面是控制天空盒变化的代码,里面的一些数字是根据场景手动调整的。
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
-
- public class ChangeSkyBox : MonoBehaviour
- {
- public Material[] mats;
- private int index=0;
- // 获取Directional Light组件
- private Light directionalLight;
- public Gradient lightColorGradient;
- public AnimationCurve lightIntensityCurve;
-
- // Start is called before the first frame update
- void Start()
- {
- mats = Resources.LoadAll<Material>("Materials/skyboxs");
- RenderSettings.skybox = mats[0];
- index ++;
- directionalLight = GameObject.Find("Directional Light").GetComponent<Light>();
- }
-
- // Update is called once per frame
- void Update()
- {
- if(Input.GetKeyDown(KeyCode.LeftShift)){
- ChangeBox();
- }
- ChangeLight();
- }
- public void ChangeBox()
- {
- RenderSettings.skybox = mats[index];
- index++;
- index %= mats.Length;
- }
- public void ChangeLight(){
- float currentTime = index*6f;
- // 计算当前的光强度
- float intensity = lightIntensityCurve.Evaluate(currentTime);
- // 计算当前的光颜色
- Color color = lightColorGradient.Evaluate(currentTime / 24f);
- // 设置光源的方向(模拟太阳的移动)
- float rotationAngle = (currentTime - 5f) * 15f; // 每小时15度
- directionalLight.transform.rotation = Quaternion.Euler(rotationAngle, -85f, 0f);
-
- // 设置光源的强度和颜色
- // directionalLight.intensity = intensity;
- directionalLight.color = color;
- }
- }
下面分别是早晨(6:00),中午(12:00),傍晚(18:00)和夜晚(24:00)的效果:
靶子的资源用的是Military target(这个模型不知道为什么很小,放大50倍感觉才正常点)。要求射中不同区域得不同的分数,所以需要修改一下。这里把靶子分成3个有效击中区域,给每个区域制作一个用来检测碰撞的射击环(其实是个圆盘)。环的大小和厚度都是手动调整的,调成透明的方法也和前面一样。
手动添加的碰撞体积用的都是mesh colider,其中除了background,都勾选上isTrigger选项,这样代码才能通过OnTriggerEnter函数检测到碰撞(勾选这个选项后实际上没有碰撞效果,箭会穿过去,但是后面有background兜着,所以不会穿过去)。
计分就由挂载到环上的代码来实现,当发生碰撞后,就把箭停下,然后设成靶子的子对象,并且使用运动学控制,这样就能实现箭插在靶子上的效果。碰撞发生后,用专门的计分器ScoreController计分。
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
-
- public class RingController : MonoBehaviour
- {
- //当前环的分值
- public int RingScore ;
- public ISceneController scene;
- public ScoreRecorder sc_recorder;
- // Start is called before the first frame update
- void Start()
- {
- scene = Director.getInstance().currentSceneController as FirstController;
- sc_recorder = Singleton<ScoreRecorder>.Instance;
- }
-
- // Update is called once per frame
- void Update()
- {
-
- }
- //碰撞检测,如果箭击中该环,就响应。
-
- void OnTriggerEnter(Collider collision){
- Debug.Log("trigger");
- Transform arrow = collision.gameObject.transform;
- Debug.Log(arrow);
- if (arrow == null)
- {
- return;
- }
- if (arrow.tag == "arrow")
- {
- //将箭的速度设为0
- arrow.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
- // Debug.Log("击中"+RingScore);
- //使用运动学运动控制
- arrow.GetComponent<Rigidbody>().isKinematic = true;
- arrow.transform.parent = this.transform.parent; // 将箭和靶子绑定
- //计分
- sc_recorder.Record(RingScore);
-
- }
- }
- }
- using System.Collections;
- using System.Collections.Generic;
- using System.Runtime.CompilerServices;
- using UnityEngine;
-
- public class ScoreRecorder : MonoBehaviour
- {
- int score;
- public FirstController firstController;
- public UserGUI userGUI;
- // Start is called before the first frame update
- void Start()
- {
- firstController = (FirstController)Director.getInstance().currentSceneController;
- firstController.scoreController = this;
- userGUI = this.gameObject.GetComponent<UserGUI>();
- }
-
- public void Record(int ringscore) {
- score += ringscore;
- userGUI.score = score;
- }
- }
把靶子加载到固定位置即可。我在这里受平时玩的一个fps游戏启发,设计了一个可交互的固定靶,可以自行调整一个固定靶的位置。
实现逻辑很简单,获取游戏对象,改变位置即可。为了方便(其实好像也不方便),这里在每个块上面都挂了代码,这里展示4m的:
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
-
- public class Interact : MonoBehaviour
- {
- GameObject target; //需要移动的游戏对象
- Vector3 origin_position;
- Transform origin_transform;
- // Start is called before the first frame update
- void Start()
- {
- origin_position = new Vector3(75f,8f,55f);
- target = GameObject.Find("move-fixed");
- // Debug.Log(origin_transform.position);
- }
-
- // Update is called once per frame
- void Update()
- {
-
- }
-
- void OnTriggerEnter(Collider collision)
- {
- // 获取被碰撞的物体
- GameObject collidedObject = collision.gameObject;
- if(collidedObject.tag == "arrow"){
- // Debug.Log(origin_transform.position);
- target.transform.position = origin_position + new Vector3(0,0,8f);
- }
- }
- }
给游戏对象自行制作相应的动画animation,上课都说过,这里不再介绍。
射击位也是通过碰撞实现。把射击区域设计成偏平的立方体,刚好能检测到碰撞的那种,这样看起来就像是地毯了。挂载代码到射击区域上,玩家进入区域时才通知firstcontroller可以射击。
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
-
- public class ShootArea : MonoBehaviour
- {
- //是否可以射箭
- private bool canShoot;
- public FirstController firstController;
-
- void Start(){
- firstController = (FirstController)Director.getInstance().currentSceneController;
- }
-
- public void OnTriggerEnter(Collider collider)
- {
- if (collider.gameObject.tag == "Player")
- {
- canShoot = true;
- firstController.AreaCallBack(canShoot);
- }
- }
-
-
- private void OnTriggerExit(Collider collider)
- {
- if (collider.gameObject.tag == "Player")
- {
- canShoot = false;
- firstController.AreaCallBack(canShoot);
- }
- }
- }
根据导入资源的动作自行制作一个状态机,和上课讲的状态机一样,其中empty-pull和hold都是混合树,通过这两个混合树来实现蓄力拉弓的动作。
射箭的逻辑由ShootControl控制,左键点击射箭,左键长按蓄力,长按时点击右键hold,hold后再次点击左键射出。通过一系列if else判断来检测鼠标状态,其中长按的检测用到了简单的协程;通过设置参数来控制动画,参数设置用setTrigger函数和setFloat实现。
还要从firstController中获取是否能射箭和剩余箭数量的信息,只有在设计区域并且有箭时才可以射箭。
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
-
- public class ShootControl : MonoBehaviour
- {
- public Animator animator;
- public FirstController firstController;
- public float power = 0.4f;
- private bool isHolding = false;
- private bool isMouseDown;
- private bool isMouseLongPressed;
-
- private float longPressDuration = 0.2f; // 定义长按的持续时间
-
- void Start(){
- animator = GetComponent<Animator>();
- firstController = (FirstController)Director.getInstance().currentSceneController;
- }
-
- private void Update()
- {
- // 长按左键不断增加力量
- if (isMouseLongPressed && !isHolding)
- {
- power = Mathf.Min(power + Time.deltaTime, 1f);
- }
- // Debug.Log(power);
- animator.SetFloat("power", power);
- if(firstController.GetArea() && firstController.arrowNum>0){
- ClickCheck();
- }
-
- }
-
- public void ClickCheck(){
- // 按下左键
- if (Input.GetMouseButtonDown(0))
- {
- // Debug.Log("单击左键");
- if(!isHolding){
- isMouseDown = true;
- isMouseLongPressed = false;
-
- // 开始协程检测长按
- StartCoroutine(CheckLongPress());
- // 触发start
- animator.SetFloat("power", power);
- animator.SetTrigger("start");
- }
- else{
- ShootAnimator();
- }
- }
- else if(isMouseLongPressed && Input.GetMouseButtonDown(1)){//right key down
- // Debug.Log("右键按下");
- isHolding = true;
- // 停止协程
- StopCoroutine(CheckLongPress());
- isMouseLongPressed = false;
-
- animator.SetFloat("hold power", power);
- animator.SetTrigger("hold");
- }
- // 松开左键
- else if (isMouseDown && Input.GetMouseButtonUp(0))
- {
- isMouseDown = false;
- if(!isHolding){
- // Debug.Log("松开左键");
- isMouseLongPressed = false;
-
- // 停止协程
- StopCoroutine(CheckLongPress());
- //触发hold
- animator.SetFloat("hold power", power);
- // Debug.Log("hold power:"+power+".");
- animator.SetTrigger("hold");
- // 触发shoot
- ShootAnimator();
- }
- }
- }
-
- private IEnumerator CheckLongPress()
- {
- yield return new WaitForSeconds(longPressDuration);
- // 如果鼠标处于按下状态,则表示长按
- if (isMouseDown)
- {
- // Debug.Log("长按左键");
- isMouseLongPressed = true;
- }
-
- }
-
- private void ShootAnimator(){
- animator.SetTrigger("shoot");
- firstController.ShootCallback(true, power);
- isHolding = false;
- power = 0.4f;
- }
- }
以上就是得分点的实现代码,但是只有这些是不完整的,还有其他比较重要的代码。比如射箭的动作控制:
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
-
- public class CCActionManager : SSActionManager, IActionCallback
- {
- public FirstController sceneController;
- public FlyAction action;
- public ArrowFactory factory;
-
- public GameObject arrow;
- public GameObject crossbow;
- public float forceRate=0.33f;
-
- // Start is called before the first frame update
- protected new void Start()
- {
- sceneController = (FirstController)Director.getInstance().currentSceneController;
- sceneController.actionManager = this;
- factory = Singleton<ArrowFactory>.Instance;
- crossbow = GameObject.Find("Crossbow");
- }
-
- public void SSActionEvent(SSAction source,
- SSActionEventType events = SSActionEventType.Completed,
- int intParam = 0,
- string strParam = null,
- Object objectParam = null) {
- factory.FreeArrow(source.transform.gameObject);
- }
-
- public void ShootArrow(float power){
- Quaternion bowRotation = crossbow.transform.rotation;
-
- arrow = factory.GetArrow();
- Vector3 shootDirection = bowRotation * Vector3.up;
- // Debug.Log(shootDirection);
- arrow.GetComponent<Rigidbody>().AddForce(shootDirection * power * forceRate, ForceMode.Impulse);
- arrow.transform.parent = null;
- action = FlyAction.GetSSAction();
- this.RunAction(arrow, action, this);
- }
- }
还有一个就是前面经常提到的firstController,它是代码的主体控制器,要把它挂载到玩家对象上来实现一系列功能的。
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
-
- public class FirstController : MonoBehaviour, ISceneController, IUserAction
- {
- public CCActionManager actionManager;
- public ArrowFactory factory;
- public UserGUI userGUI;
- public GameObject bow;
- public ScoreRecorder scoreController;
- float shotpower =0.4f;
- bool shot = false;
- public int arrowNum = 10;
- bool inArea = false;
-
- void Awake() {
- Director director = Director.getInstance();
- bow = GameObject.Find("Crossbow");
-
- director.currentSceneController = this;
- director.currentSceneController.LoadSource();
-
- gameObject.AddComponent<UserGUI>();
- gameObject.AddComponent<CCActionManager>();
- gameObject.AddComponent<ScoreRecorder>();
- gameObject.AddComponent<ArrowFactory>();
-
- factory = Singleton<ArrowFactory>.Instance;
- userGUI = gameObject.GetComponent<UserGUI>();
- }
- // Start is called before the first frame update
- void Start()
- {
-
- }
-
- // Update is called once per frame
- void Update()
- {
- if(inArea && arrowNum>0 ){
- Shoot(shotpower);
- }
- userGUI.arrowNum = arrowNum;
- }
-
- public void LoadSource(){
-
- }
-
- public void Shoot(float shootpower){
- if(shot){
- actionManager.ShootArrow(shootpower);
- shot = false;
- arrowNum --;
- }
- }
- public void gameOver(){
-
- }
-
- public void ShootCallback(bool isShot, float power)
- {
- shotpower = power;
- shot = isShot;
- }
-
- public void AreaCallBack(bool inArea){
- this.inArea = inArea;
- if(inArea)
- arrowNum = 10;
- }
-
- public bool GetArea(){
- return inArea;
- }
- }
上面就是游戏的主体功能的实现了。紧赶慢赶,前前后后用了一周的课余时间才做完的。可以看出游戏比较简陋,代码比较繁琐,而且不能动态地去加载所有的代码和资源;而且准心的实现也很简陋,只是在屏幕上简单的画了一个点等等,还有许多改进的地方。
作业的完成参考了许多别人的博客,这里感谢所有愿意写博客分享技术的大佬们~
游戏的效果演示:
Unity游戏制作——射箭游戏
后面应该会整理一下上传到GitHub
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。