> 文章列表 > 1.16 从0开始学习Unity游戏开发--人物控制

1.16 从0开始学习Unity游戏开发--人物控制

1.16 从0开始学习Unity游戏开发--人物控制

上一篇我们简单的做了一个玩家不动的情况下,如何控制准心来射击子弹,但是显然正常的游戏需要移动玩家本体,所以本篇我们需要补全这部分玩法所需的功能。

人物移动

在我们之前的篇章里面,讲解了如何通过物理引擎来实现物体的物理仿真移动,那么我们是不是可以通过物理引擎来实现人物的移动呢?

当然可以,在我们做之前,我们先给场景里面添加好一个地面和几面墙,我们都使用Cube来拉长实现,因为Cube自带了Collider组件,可以直接支持物理碰撞,实现我们的人物能站在上面不掉下去。之前那个Wall我们也加长一点:

然后我们再新建一个Cube来作为我们的玩家人物,因为我们的人物是需要受物理引擎改变位置的,所以不要忘记加入RigidBody组件:

我们把方块放在比较高的位置,如果跑起来的话可以看到这个Player会掉下来落到Ground上,但是不会掉下去。

ok,现在我们需要加入一个组件来实现对Player的控制效果,也就是我们使用wasd需要能给这个Player的刚体组件提供一个速度。

有请gpt帮我们快速写一个:

using UnityEngine;public class PlayerController : MonoBehaviour
{public float speed = 20f; // 控制速度的变量private Rigidbody rb; // 刚体组件void Start(){rb = GetComponent<Rigidbody>(); // 获取刚体组件}void FixedUpdate(){float moveHorizontal = Input.GetAxis("Horizontal"); // 获取水平方向的输入float moveVertical = Input.GetAxis("Vertical"); // 获取垂直方向的输入Vector3 movement = new Vector3(moveHorizontal, 0f, moveVertical); // 创建一个移动向量rb.AddForce(movement * speed); // 将移动向量乘以速度并添加到刚体上}
}

值得注意的是,gpt正确的意识到了物理引擎相关的每帧操作需要在FixedUpdate里面而非Update里面,这跟Unity内部各个系统执行的顺序有关,如果你用了错误的Update,那么可能就会出现你这帧设置的物理数据,没有及时的在本帧体现出来,而是延迟到下一帧才体现。

给Player这个方块加上这个PlayerController组件看看效果(注意需要在Scene窗口下观察,因为我们还没实现相机跟着人物的效果):

 

可以看到虽然确实是受我们的wasd输入影响了,但是感觉很奇怪,是这样几个问题:

  1. 正常人走路是不可能翻滚的
  2. 按下wasd会有一个加速的过程,并不是跟真实人走路一样立刻就以一个速度行走了,并且这个速度会随着按住时常加速到越来越快

对于问题1,主要原因是虽然我们只给予了水平方向的力(XZ轴),但是因为来自摩擦力或者碰撞带来的反作用力的计算精度问题,总会出现一些不那么水平的力,所以就会让物体无法保持稳定,就出现了翻滚,我们如果需要保持物体不翻滚,就可以锁定RigidBody组件里面的对应坐标轴:

这里很浅显易懂,Freeze就是让数据不要变动,而它支持我们单独的锁定位置信息或者旋转信息,现在我们需要锁定的是XYZ所有方向上的旋转,只希望我们的位置信息被物理引擎改变,那么勾选上Freeze Rotation的三个勾选框即可。

对于问题2,其实是因为我们人正常行走是来自脚蹬起来的,并不是单纯的一个小滑块进行水平加速形成的,这其中会涉及很多物理学上的东西,所以如果我们需要在游戏内模拟出真实的人类关节进行行走,那代价是挺大的,也没那么必要。所以我们可以通过直接修改刚体的速度而非施加力来近似这个效果(在Rigidbody.velocity的官方文档里面也提到了这些),但是需要注意的是,只有特定场景才需要这么直接修改速度,正常情况下应该都是施加力。

好,那我们再修改一下代码:

using UnityEngine;public class PlayerController : MonoBehaviour
{public float speed = 1f; // 控制速度的变量private Rigidbody rb; // 刚体组件void Start(){rb = GetComponent<Rigidbody>(); // 获取刚体组件}void FixedUpdate(){float moveHorizontal = Input.GetAxis("Horizontal"); // 获取水平方向的输入float moveVertical = Input.GetAxis("Vertical"); // 获取垂直方向的输入Vector3 movement = new Vector3(moveHorizontal, 0f, moveVertical) * speed; // 创建一个移动向量rb.velocity = new Vector3(movement.x, rb.velocity.y, movement.z);  // 直接修改速度}
}

这里我们直接修改了速度,但是需要注意的是,我们不是直接赋值整个vector3,因为还有Y轴上的速度分量我们并不希望置空,仍然希望它能受到重力影响而不至于漂浮在地面上。

跑起来看看:

 

这样看起来正常多了,但是目前为止,我们还是只实现了在Scene窗口里面看到我们的操作,实际上我们操作人物移动的时候,游戏视角应该会跟着变化,因此接下来我们需要进一步的控制我们的相机。

相机控制

在我们之前的文章中其实已经简单讲解过画面是如何通过相机的参数来确定如何渲染,以及我们如何移动相机来调整我们的视角。

简单来说,根据游戏玩法的不同,相机所代表的意义也不一样。

  • 在第一人称的游戏中,相机其实就是玩家的眼睛
  • 在第三人称或者俯视角的游戏中,相机其实跟我们在场景窗口里面漫游整个场景类似,是一个独立的不存在于真实世界逻辑中的视角。但是这样的视角能让玩家更加直观的操控和感知游戏世界,所以就存在了这样一种相机使用的方法。

对于第一人称来说比较好理解,直接把相机绑定在人物的固定位置(比如眼睛位置)上,就可以实现,所以我们本篇会直接讲解第三人称这种形式下,相机应该是如何处理的。

在制作之前,找一下各种第三人称的游戏视频先参考参考,可以发现,相机其实是一直跟在人物背后,人物移动的前进方向其实是视角的方向,而非当前人物面朝的方向,这一点可以尝试让角色面向你,然后操作向前跑,人物会立即转身朝着视线方向跑。

另外一点就是,相机都是会距离人物一定的距离,不至于让人物挡住了全部屏幕,也不至于让人物在屏幕中占比太小。

想明白这些规则之后,我们尝试做一下相机的处理:首先我们需要让相机跟人物离一个固定距离,这样我们需要在每帧计算人物所在的位置,然后倒推出相机所在的位置。

但是在3D世界里面,距离一个点固定长度的位置有无数多个,这些位置组合成了一个球形,所以相机可以在这个球面上的任意一个点上,如果我们再加上相机的Y轴坐标,就能限定相机到一个2D圆环上,最后如果再限定当前相机的朝向,则可以唯一确定一个坐标点,而这个点就是相机的位置。

如果画图的话大概是这样:

黑色表示距离人物位置的定长的所有点组成的球体,蓝色表示如果限定相机Y轴坐标能确定的一个圆面,红色表示如果限定相机当前看向哪里,则可以在圆面上找到唯一一个点(虽然可以有两个,但是另外一个点无法让人物在相机镜头内,显然不能用)。

那么我们来简单推导一下数学公式:

假设相机始终看向玩家的位置是(x,y,z)

蓝色圆心是: (x, y+h, z)

假设相机朝向是direction

那么蓝色圆上的任意一个点是 (x,y+h,z) + (direction.x, 0, direction.z) * r

圆形半径r^2 = distance^2 - h^2,distance是相机距离看向位置的距离

可以看到上面这些量全是已知,除了相机水平方向上的朝向

那么我们可以简单的让相机初始状态的朝向就是人物正前方朝向来计算这个逻辑

好了,有了这个数学基础,我们需要用代码实现一下:

using UnityEngine;public class ThirdPersonCameraController : MonoBehaviour
{public Transform target; // 相机要跟随的目标public float distance = 10f; // 相机与目标的距离public float height = 5f; // 相机与目标的高度private void LateUpdate(){// 相机于目标的距离不能小于高度差,不然无论如何都无法维持if (Mathf.Abs(distance) < Mathf.Abs(height)){return;}// 先计算圆心的位置Vector3 lookAtPosition = target.transform.position;Vector3 circleCenter = lookAtPosition + new Vector3(0, height, 0);// 计算圆的半径float circleRadius = Mathf.Sqrt(distance * distance - height * height);// 简单处理让相机的水平朝向就是人物朝向Vector3 cameraDirection = target.transform.forward;// 记得干掉Y轴的数据,因为我们是在水平面的圆形上计算位置信息cameraDirection.y = 0;// 最后拿到相机的位置Vector3 cameraPos = circleCenter + (cameraDirection.normalized * circleRadius);// 设置相机的位置transform.position = cameraPos;// 让相机朝向目标transform.LookAt(target);}
}

把组件加到MainCamera上,然后将target赋值为Player,跑起来看看?

1.16 从0开始学习Unity游戏开发--人物控制-1

默认给的参数值不太合理,可以在运行时动态调整到我们满意的效果,但是记得这个调整在退出后会重置回去,所以需要记一下等退出运行后再改一下。

值得注意的是,我们用了LateUpdate而不是Update,可以查阅官方文档知道LateUpdate发生在Update和FixedUpdate之后,其实意思就是说我们需要等物体移动完毕后再修改相机位置,否则如果我们先修改相机位置,而这一帧物体又动了,这样我们的渲染结果可能就不是很准确。

Ok,现在我们初步完成了一个三人称相机的功能,虽然可以相机可以跟着Player走,但是我们还没有支持视角调整,也就是说我们需要支持鼠标移动来调整相机朝向,我们可以接收鼠标的移动输入信息来修改当前相机的朝向。

那我们又需要处理Input的鼠标移动的信息,如果鼠标水平移动,则是希望让相机的朝向发生改变,如果鼠标的竖直方向上移动,则是希望让相机相对于物体的高度差发生改变,如果鼠标滚动滑轮,我们认为是希望修改相机距离玩家人物的距离。

好,我们修改一下相机控制脚本来处理这几个输入信息:

对于高度和距离,我们可以简单的通过加减来实现,但是对于相机的朝向,我们需要通过旋转来实现,为了降低理解成本,我们换一种方法计算相机的朝向。

上面我们也说了,相机朝向本质上是用来确定相机在那个圆上的位置,相机和圆心的连线是有夹角的,我们可以通过改变夹角来旋转相机:

蓝色部分就是我们俯视这个圆形,那么相机在圆上的位置和圆心的连线跟初始状态(就是中间那根竖直的黑线)有一个夹角,夹角就是红色那个角,半径已知的情况下,我们可以使用sin和cos来计算出半径和相机距离圆心距离在XZ两个轴上的比值。我们只需要决定我们的相机0度角的时候是用X轴的方向还是Z轴的方向来做初始朝向,这里我们用X轴,那么sin对应的是z轴距离/r,cos对应的是x轴距离/r。

所以我们的相机控制脚本修改如下:

using UnityEngine;public class ThirdPersonCameraController : MonoBehaviour
{public Transform target; // 相机要跟随的目标public float distance = 10f; // 相机与目标的距离public float height = 5f; // 相机与目标的高度public float directionChangeSpeed = 1.0f;public float heightChangeSpeed = 1.0f;public float distanceChangeSpeed = 1.0f;private float currentDegree;private float currentHeight;private float currentDistance;private void Start(){// 初始情况下对齐玩家的正向朝向currentDegree = 0;currentHeight = height;currentDistance = distance;}private void LateUpdate(){float directionChange = Input.GetAxis("Mouse X") * directionChangeSpeed;float heightChange = Input.GetAxis("Mouse Y") * heightChangeSpeed;float distanceChange = Input.GetAxis("Mouse ScrollWheel") * distanceChangeSpeed;currentDegree += directionChange;currentHeight += heightChange;currentDistance += distanceChange;// 相机于目标的距离不能小于高度差,不然无论如何都无法维持if (Mathf.Abs(currentDistance) < Mathf.Abs(currentHeight)){return;}// 先计算圆心的位置Vector3 lookAtPosition = target.transform.position;Vector3 circleCenter = lookAtPosition + new Vector3(0, currentHeight, 0);// 计算圆的半径float circleRadius = Mathf.Sqrt(currentDistance * currentDistance - currentHeight * currentHeight);// 使用夹角来计算相机相对圆心的XZ偏移float rad = Mathf.Deg2Rad * currentDegree;float xDistance = Mathf.Cos(rad) * circleRadius;float zDistance = Mathf.Sin(rad) * circleRadius;// 最后拿到相机的位置Vector3 cameraPos = circleCenter + new Vector3(xDistance, 0, zDistance);// 设置相机的位置transform.position = cameraPos;// 让相机朝向目标transform.LookAt(target);}
}

可以看到我们用currentDegree,currentHeight,currentDistance三个成员来存储当前的状态,每次接收鼠标输入后都进行修改,然后重新计算相机位置。

跑起来之后我们就可以得到一个可以移动鼠标调整视角的三人称相机,可以自己调节speed达到一个操作比较舒服的程度。

1.16 从0开始学习Unity游戏开发--人物控制-2

思考题

在完成了初步的人物控制之后,如果有自己尝试实现更多的场景的话,其实会发现一些问题:

  1. 如果有台阶的情况下,方块上不去,那是否可以换成其他的碰撞体形状?如果是的话,用哪个内置形状最好?
  2. 如果要实现角色跳跃如何处理?
  3. 下楼梯的情况下会像抛物线一样飞出去,这个如何处理?
  4. 既然我们的视角变成三人成了,我们肯定也不能从相机来发射子弹,那是不是要改成从人物那里发射,这个情况下,准心又是如何瞄准的呢?

下一章

在一个场景里面玩了太久了,我们如果希望切换场景要怎么做?如果切换场景的话,我们当前场景的逻辑都是基于场景内的GameObject来驱动的,这些在新的场景里面又何去何从?下一章我们会讲解场景的切换和切换带来的逻辑与资源的管理需求如何处理。