前言
什么是Third Person Starter Asset
自己看了几篇文章和视频,写了个人物移动脚本,有很多瑕疵。这个时候研究一下优秀的代码总是好的,Unity官方有Third Person Starter Asset可供研究,其官方商店页面是:Starter Assets - ThirdPerson | Updates in new CharacterController package
官方B站介绍视频是:Bilibili - [Unity教程]-Starter Assets 轻量的角色控制器
这个资产使用依赖于两个package,分别是Input System和Cinemachine。Input System有别于传统的输入系统,二者并不兼容。了解InputSystem参加官方文档:Unity Documention - Input System
二者类似的代码示例如下:
// 启用输入系统
InputSystem.EnableDevice<Keyboard>();
Keyboard keyboard = InputSystem.GetDevice<Keyboard>();
// 传统input
float h = Input.GetAxis ("Horizontal");
float v = Input.GetAxis ("Vertical");
若把项目设置成与旧输入系统共存,会造成非常严重的卡顿。故只使用Input System。
- Player Input组件挂在Player Armature下
- Behavior是Send Messages
- 动作接收脚本是StarterAssetsInputs.cs。
而CameraChine则可通过此文快速了解:微信文章 - 5分钟入门Cinemachine智能相机系统
代码开头部分
然后就是洋洋洒洒392行的ThirdPersonController.cs代码,开头长这样:
有如下知识点:
引入UnityEngine
命名空间确实包含了该命名空间中的所有内容,但是这并不包括UnityEngine
命名空间内部其他子命名空间的内容。每个子命名空间在Unity
中都被设计为相对独立,需要显式引入才能访问其中的类和功能。所以后面条件编译指令里又引入了UnityEngine.InputSystem
底下就是声明特性(Attribute)的一些代码:
// 要求将拥有这个特性的组件附加到同一 GameObject 上时
// 该 GameObject 必须包含 CharacterController 组件
[RequireComponent(typeof(CharacterController))]
然后再底下就是把变量暴露到编辑器中的操作。一个叫Player
的标题,其下有一堆属性可供设置,
[Tooltip("...")]
设置在 Unity 编辑器中将鼠标悬停在变量上时的提示信息。[Space(10)]
是在变量上面创建一个高度为 10 个像素的空白间隔,以提高可读性。[Range(x,y)]
则可以为变量创建一个范围在[x,y]
之间的滑动控制条
在编辑器中就这样:
显然,在这里具有如下的命名规则:私有变量都是下划线开头的。public
变量是大驼峰命名法,private
是小驼峰。
值得一提的是,这里是属性的声明:
private bool IsCurrentDeviceMouse
{get{
#if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKEDreturn _playerInput.currentControlScheme == "KeyboardMouse";
#elsereturn false;
#endif}
}
start()
然后再start()
函数里进行初始化,值得一提的是它把Animator
需要初始化的单独写在AssignAnimationIDs()
中了
// private int _animIDSpeed;
// ...
private void AssignAnimationIDs()
{_animIDSpeed = Animator.StringToHash("Speed");_animIDGrounded = Animator.StringToHash("Grounded");_animIDJump = Animator.StringToHash("Jump");_animIDFreeFall = Animator.StringToHash("FreeFall");_animIDMotionSpeed = Animator.StringToHash("MotionSpeed");
}
这里使用的是int
值。在 Unity 的动画系统中,动画参数(如速度、是否在地面上等)是通过哈希值来标识的,而不是直接使用字符串。使用整数值的好处是:
- 比较速度比字符串快
- 通常比字符串占的内存小
- 没拼写错误
状态机与动画系统
注意:如果在动画过渡中勾选了 “Has Exit Time” 并且只有一条过渡路径,那么在当前动画播放完成后,会自动触发过渡到下一个动画。
关于Cinemachine
基础配置参数解析:CSDN - Unity Cinemachine之第三人称摄像机CinemachineFreeLook属性详解
另一篇基础:CSDN - 【学习笔记】Unity基础(九)【cinemachine基础(body、aim参数详解)】(多fig动图示范)
一些进阶的镜头运用:CSDN - unity 的Cinemachine组件运用
关于Input System
CSDN - Unity Input System 新输入系统的功能及用法介绍
Update()和LateUpdate()
然后就是Update()
和LateUpdate()
,关于事件的执行顺序可以参见:
- Unity Documention - 事件函数
- Unity Documention - 事件函数的执行顺序
Update()
在每一帧更新之前被调用,用于处理对象的运动、输入检测以及其他与帧更新相关的任务。
LateUpdate()
在 Update()
方法之后被调用。如果脚本上有任何 Update()
方法,那么 LateUpdate()
将在所有 Update()
方法执行完毕后调用。
如果在 Update()
内让角色移动和转向,可以在 LateUpdate()
中执行所有摄像机移动和旋转计算。这样可以确保角色在摄像机跟踪其位置之前已完全移动。
private void Update()
{_hasAnimator = TryGetComponent(out _animator);JumpAndGravity(); GroundedCheck();Move();
}private void LateUpdate()
{CameraRotation();
}
先检查是不是在地面,然后设置垂直速度(但不设置位移,因为后面会在Move()
加上水平面的移动一块进行)
我这里唯一的问题是JumpAndGravity()
在GroundedCheck()
之前被调用,那么Grounded
的值实际上是上帧的结果,而且是上帧位移前的结果(在GroundedCheck()
之后调用的Move()
)
JumpAndGravity()
private void JumpAndGravity()
{if (Grounded){// reset the fall timeout timer_fallTimeoutDelta = FallTimeout;// update animator if using characterif (_hasAnimator){_animator.SetBool(_animIDJump, false);_animator.SetBool(_animIDFreeFall, false);}// stop our velocity dropping infinitely when groundedif (_verticalVelocity < 0.0f){_verticalVelocity = -2f;}// Jumpif (_input.jump && _jumpTimeoutDelta <= 0.0f){// the square root of H * -2 * G = how much velocity needed to reach desired height_verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);// update animator if using characterif (_hasAnimator){_animator.SetBool(_animIDJump, true);}}// jump timeoutif (_jumpTimeoutDelta >= 0.0f){_jumpTimeoutDelta -= Time.deltaTime;}}else{// reset the jump timeout timer_jumpTimeoutDelta = JumpTimeout;// fall timeoutif (_fallTimeoutDelta >= 0.0f){_fallTimeoutDelta -= Time.deltaTime;}else{// update animator if using characterif (_hasAnimator){_animator.SetBool(_animIDFreeFall, true);}}// if we are not grounded, do not jump_input.jump = false;}// apply gravity over time if under terminal (multiply by delta time twice to linearly speed up over time)if (_verticalVelocity < _terminalVelocity){_verticalVelocity += Gravity * Time.deltaTime;}
}
重力
JumpAndGravity()
分为两种情况(在地面和不在地面)处理角色跳跃和重力逻辑,显然地:
- 重力的作用是影响垂直速度(利用垂直速度模拟的下落)
- 重力在地上是不需要重力作用的(仅当角色不在地面时起作用)
- 一直下落不会导致垂直速度一直增加(空气是有阻力的),在这里是设为了
-2f
由跳跃高度反推初始速度
人物的跳跃参数设置的是跳跃高度而非跳跃初始速度,高度会更加直观
注意到代码有这样一行(这行代码实际上在其他类似的也能看见)
_verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);
这个式子写成公式就是
v y = − 2 h g v_y=\sqrt{-2hg} vy=−2hg
根据基础物理知识,当一个物体从静止状态开始下落时,它的垂直位移可以用以下公式来表示:
h = v 0 t + 1 2 g t 2 h = v_0t + \frac{1}{2}gt^2 h=v0t+21gt2
不管在地面上以何速度跳跃最高点高度 h h h,此时在最高点垂直速度必为0,因此我们可以通过倒放的情况(最高点跳下来)来判断起跳的垂直初速度
从地面以某个速度调到最高点(此时速度为0)和是倒放的过程,
h = 1 2 g t 2 ⇒ 反解 t = 2 h g h = \frac{1}{2}gt^2 \xRightarrow{反解}t = \sqrt{\frac{2h}{g}} h=21gt2反解t=g2h
此 t t t为物体从跳起到达最高点所需的时间
现在回到原始的问题,我们需要求出跳跃的垂直速度。在跳到最高点时,速度为0。由于 v = v 0 + g t v = v_0 + gt v=v0+gt且跳到最高点时速度 v = 0 v=0 v=0,把 t t t和 v v v带入得下式
0 = v 0 + g ⋅ 2 h g 0 = v_0 + g \cdot \sqrt{\frac{2h}{g}} 0=v0+g⋅g2h
反解得 v 0 = 2 g h v_0 = \sqrt{2gh} v0=2gh,由于Unity坐标系Y轴正方向垂直朝上且g是朝下的,所以有个负号,即跳跃的初始速度可以用 − 2 g h -\sqrt{2gh} −2gh 表示。
跳跃超时计时器和下落超时计时器
_jumpTimeoutDelta
用于跟踪跳跃动作之间的时间间隔。如果角色执行了跳跃动作,_jumpTimeoutDelta
将被设置为预先定义的跳跃超时值。然后,_jumpTimeoutDelta
将在每帧更新时递减,直到达到零或以下。一旦 _jumpTimeoutDelta
小于等于零,玩家就可以再次执行跳跃动作(当其小于0且按下了跳跃时,设置状态机变量Jump
为True
)。此举旨在防止玩家连续多次执行跳跃动作。
_fallTimeoutDelta
的值会在每帧更新时递减,直到达到零或以下。如果角色在空中,且 _fallTimeoutDelta
的值小于等于零,则会将角色视为处于自由落体状态(设置状态机变量FreeFall
为True
)
其他
整体逻辑:
- 首先检查角色是否在地面上(Grounded)。如果在地面上则:
- 重置下落超时计时器(
_fallTimeoutDelta = FallTimeout
)。 - 如果使用角色动画,更新动画状态。
- 防止垂直速度无限下降,如果垂直速度小于0,则将其设置为-2f(这里有点疑问,速度不是平滑变过去的,是不是有些生硬?)
- 如果输入中包含跳跃指令且跳跃超时时间小于等于0,则执行跳跃操作。跳跃操作会根据所需的跳跃高度计算所需的垂直速度,并更新动画状态。
- 重置下落超时计时器(
在地面状态下Jump
和FreeFall
均为false
这样会一直维持在IWR
状态下,不会变化
-
如果角色不在地面上,则:
- 重置跳跃超时计时器(_jumpTimeoutDelta = JumpTimeout)。
- 如果下落超时时间大于等于0,则递减下落超时时间,否则更新动画状态为自由下落。
- 如果角色不在地面上,则将跳跃指令设置为false,防止在空中跳跃。
-
最后,无论角色是否在地面上,都会根据重力和时间应用垂直速度。如果垂直速度小于终端速度(_terminalVelocity),则会根据重力和时间逐渐增加垂直速度。
GroundedCheck()
GroundedCheck()
的作用是检测角色是否在地面上,并将检测结果存储在 Grounded
变量中。
private void GroundedCheck()
{// set sphere position, with offsetVector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset,transform.position.z);Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers,QueryTriggerInteraction.Ignore);// update animator if using characterif (_hasAnimator){_animator.SetBool(_animIDGrounded, Grounded);}
}
虽然Grounded可能被设为True,但是由于如果角色没有经过在空中的阶段,即没到达过InAir
,那么始终是无法进入落地动画的(落地动画是在落地以后播放)
-
设置球体检测点位置:
- 计算一个球体检测点的位置
spherePosition
,该位置位于角色位置的下方,并考虑了一个垂直偏移(GroundedOffset
)。这个偏移通常用于调整地面检测点相对于角色的位置。
- 计算一个球体检测点的位置
-
进行地面检测:
- 使用
Physics.CheckSphere
方法检测位于spherePosition
处的球体,半径为GroundedRadius
,是否与地面碰撞。GroundedRadius
可能是一个表示地面检测球体的半径的值。 - 将检测结果保存到布尔变量
Grounded
中。如果球体与地面碰撞,则Grounded
为true
,否则为false
。 GroundLayers
参数可能用于指定哪些层是地面层,只有与这些层碰撞的情况才会被认为是在地面上。
- 使用
-
更新状态机的条件
- 没啥说的
关于球体检测:球体检测通常用于检测角色或物体是否与地面碰撞,特别是在处理不规则形状的地面时,使用球形检测可以有助于减轻或避免由于小凸起或不规则地形引起的频繁状态切换问题。当角色经过小凸起时,如果直接依赖于简单的 isGrounded
属性,可能会导致在角色行走过程中不断地从“着地状态”到“空中状态”的频繁切换,这会影响角色动画的流畅性和游戏体验。
而且它还具有可拓展性,它可以可以在不同的时间点使用不同大小的球体来模拟角色的跳跃或下落状态,以检测角色是否在跳跃过程中与地面接触。
此外球形检测器不限于仅检测角色底部是否与地面接触。您可以将球形检测器放置在角色周围的任意位置,从而实现更复杂的地面检测逻辑,例如检测角色的脚部、身体或头部是否与地面接触。
Move()
Move()
是处理角色移动的逻辑。实现了角色的平滑移动,包括加速度、减速度、动画混合以及根据输入方向旋转角色。
-
设置目标速度(
targetSpeed
):如果按下冲刺键,则使用SprintSpeed
作为目标速度;否则,使用MoveSpeed
。而且如果没按下移动那就再改为0。 -
加速和减速:每帧使用
Mathf.Lerp
将_speed
插值靠近target,然后直到打到目标速度±speedOffset之后变为目标速度 -
动画混合:
- 通过插值(
Lerp
)计算动画混合值_animationBlend
,用于在动画中平滑过渡速度的变化。
- 通过插值(
-
旋转角色:
- 根据输入方向旋转角色,使角色朝向移动方向。这个旋转会基于相机的方向,使得角色相对于相机的运动更自然。
-
计算移动方向:
- 根据输入方向计算移动方向。
_targetRotation
存储了角色应该朝向的目标旋转角度。
- 根据输入方向计算移动方向。
-
移动角色:
- 使用
CharacterController
的Move
方法来移动角色,考虑了水平速度和垂直速度。垂直速度通常用于处理跳跃和重力。
- 使用
-
更新动画参数:
- 如果使用了动画(
_hasAnimator
为true
),更新动画控制器中的速度和动作速度参数。这些参数通常用于控制角色的动画播放。
- 如果使用了动画(
speedOffset
speedOffset
是一个浮点数,它用于在加速或减速时引入一个小的偏移量。这个偏移量的作用是使速度变化更加平滑,以防止在速度接近目标速度时频繁地切换。具体来说,当当前水平速度与目标速度之间的差距小于 speedOffset 时,就不会再应用加速或减速。
inputMagnitude
适用于手柄的模拟移动的相关选项
向量与!=
!=
运算符会使用一种近似值的方式来进行比较,而不是直接比较向量的每个分量是否完全相等。这种近似值的比较方式可以更好地处理浮点数误差,并且比直接比较每个分量的值更为高效。因此,当我们需要检查一个 Vector2
对象是否非零时,使用 != 运算符会更为合适和高效,而不是先计算向量的长度(使用 magnitude
方法)然后再与零比较。
MotionSpeed
使用参数MotionSpeed
控制动画播放速度,这是一个细节。通过调整动画播放的速度,可以使动画与角色的移动行为更加匹配,从而提升游戏的视觉体验。例如,当玩家快速移动时,动画可以播放得更快以与移动速度相匹配;而当玩家缓慢移动时,动画可以播放得更慢,以更好地反映出角色的移动状态。
随后在Move
函数中设置了
float inputMagnitude = _input.analogMovement ? _input.move.magnitude : 1f;
// ...
_animator.SetFloat(_animIDMotionSpeed, inputMagnitude);
不过键鼠输入下inputMagnitude
的值显然始终为 1.0f
CameraRotation
首先检查输入是否存在且摄像机位置未固定。如果输入存在且摄像机位置未固定,则根据输入的值更新目标的偏航角(yaw)和俯仰角(pitch)。然后对旋转角度进行限制,确保值在360度范围内。最后,将更新后的目标旋转应用到Cinemachine相机目标上。
private void CameraRotation()
{// if there is an input and camera position is not fixedif (_input.look.sqrMagnitude >= _threshold && !LockCameraPosition){//Don't multiply mouse input by Time.deltaTime;float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1.0f : Time.deltaTime;_cinemachineTargetYaw += _input.look.x * deltaTimeMultiplier;_cinemachineTargetPitch += _input.look.y * deltaTimeMultiplier;}// clamp our rotations so our values are limited 360 degrees_cinemachineTargetYaw = ClampAngle(_cinemachineTargetYaw, float.MinValue, float.MaxValue);_cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp);// Cinemachine will follow this targetCinemachineCameraTarget.transform.rotation = Quaternion.Euler(_cinemachineTargetPitch + CameraAngleOverride,_cinemachineTargetYaw, 0.0f);
}
番外:匹配旧系统的移动代码
是这样的,我把它原本的代码改了,不用InputSystem了,并且删掉了一部分(适用于手柄输入的)功能,修改主要集中在以下地方:
- 移除了对UnityEngine.InputSystem的引用。
- 移除了与PlayerInput组件和相关引用相关的条件编译指令,并将其移除。
- 替换了原来使用Input.GetAxis和Input.GetButton方法的部分,改为使用传统输入方式,例如Input.GetAxis(“Horizontal”)和Input.GetAxis(“Vertical”)来获取水平和垂直输入,以及使用Input.GetKeyDown(KeyCode.Space)来检测跳跃输入。
具体修改的地方包括:
- 移除了头部的相关条件编译指令和对UnityEngine.InputSystem的引用。
- 移除了_PlayerInput_ 和 StarterAssetsInputs 的相关声明和初始化。
- 在_Update_ 方法中,替换了原来检测输入的方式,改为使用Input.GetAxis和Input.GetKeyDown方法。
- 在_CameraRotation_ 方法中,替换了获取鼠标输入的方式,改为使用Input.GetAxis(“Mouse X”)和Input.GetAxis(“Mouse Y”)。
- 在_Move_ 方法中,替换了原来检测输入的方式,改为使用Input.GetKey(KeyCode.LeftShift)来检测是否按下Shift键。
- 在_JumpAndGravity_ 方法中,替换了原来检测跳跃输入的方式,改为使用Input.GetKeyDown(KeyCode.Space)。
Cinemachine的相应的输入轴应该也是得改的。
using UnityEngine;
namespace StarterAssets
{[RequireComponent(typeof(CharacterController))]public class ThirdPersonController : MonoBehaviour{[Header("Player")][Tooltip("Move speed of the character in m/s")]public float MoveSpeed = 2.0f;[Tooltip("Sprint speed of the character in m/s")]public float SprintSpeed = 5.335f;[Tooltip("How fast the character turns to face movement direction")][Range(0.0f, 0.3f)]public float RotationSmoothTime = 0.12f;[Tooltip("Acceleration and deceleration")]public float SpeedChangeRate = 10.0f;public AudioClip LandingAudioClip;public AudioClip[] FootstepAudioClips;[Range(0, 1)] public float FootstepAudioVolume = 0.5f;[Space(10)][Tooltip("The height the player can jump")]public float JumpHeight = 1.2f;[Tooltip("The character uses its own gravity value. The engine default is -9.81f")]public float Gravity = -15.0f;[Space(10)][Tooltip("Time required to pass before being able to jump again. Set to 0f to instantly jump again")]public float JumpTimeout = 0.50f;[Tooltip("Time required to pass before entering the fall state. Useful for walking down stairs")]public float FallTimeout = 0.15f;[Header("Player Grounded")][Tooltip("If the character is grounded or not. Not part of the CharacterController built in grounded check")]public bool Grounded = true;[Tooltip("Useful for rough ground")]public float GroundedOffset = -0.14f;[Tooltip("The radius of the grounded check. Should match the radius of the CharacterController")]public float GroundedRadius = 0.28f;[Tooltip("What layers the character uses as ground")]public LayerMask GroundLayers;[Header("Cinemachine")][Tooltip("The follow target set in the Cinemachine Virtual Camera that the camera will follow")]public GameObject CinemachineCameraTarget;[Tooltip("How far in degrees can you move the camera up")]public float TopClamp = 70.0f;[Tooltip("How far in degrees can you move the camera down")]public float BottomClamp = -30.0f;[Tooltip("Additional degress to override the camera. Useful for fine tuning camera position when locked")]public float CameraAngleOverride = 0.0f;[Tooltip("For locking the camera position on all axis")]public bool LockCameraPosition = false;// cinemachineprivate float _cinemachineTargetYaw;private float _cinemachineTargetPitch;// playerprivate float _speed;private float _animationBlend;private float _targetRotation = 0.0f;private float _rotationVelocity;private float _verticalVelocity;private float _terminalVelocity = 53.0f;// timeout deltatimeprivate float _jumpTimeoutDelta;private float _fallTimeoutDelta;// animation IDsprivate int _animIDSpeed;private int _animIDGrounded;private int _animIDJump;private int _animIDFreeFall;private int _animIDMotionSpeed;private Animator _animator;private CharacterController _controller;private GameObject _mainCamera;private const float _threshold = 0.01f;private bool _hasAnimator;private void Awake(){// get a reference to our main cameraif (_mainCamera == null){_mainCamera = GameObject.FindGameObjectWithTag("MainCamera");}}private void Start(){_cinemachineTargetYaw = CinemachineCameraTarget.transform.rotation.eulerAngles.y;_hasAnimator = TryGetComponent(out _animator);_controller = GetComponent<CharacterController>();AssignAnimationIDs();// reset our timeouts on start_jumpTimeoutDelta = JumpTimeout;_fallTimeoutDelta = FallTimeout;}private void Update(){_hasAnimator = TryGetComponent(out _animator);JumpAndGravity();GroundedCheck();Move();}private void LateUpdate(){CameraRotation();}private void AssignAnimationIDs(){_animIDSpeed = Animator.StringToHash("Speed");_animIDGrounded = Animator.StringToHash("Grounded");_animIDJump = Animator.StringToHash("Jump");_animIDFreeFall = Animator.StringToHash("FreeFall");_animIDMotionSpeed = Animator.StringToHash("MotionSpeed");}private void GroundedCheck(){// set sphere position, with offsetVector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset,transform.position.z);Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers,QueryTriggerInteraction.Ignore);// update animator if using characterif (_hasAnimator){_animator.SetBool(_animIDGrounded, Grounded);}}private void CameraRotation(){// if there is an input and camera position is not fixedVector2 lookInput = new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y"));if (lookInput.sqrMagnitude >= _threshold && !LockCameraPosition){_cinemachineTargetYaw += lookInput.x * Time.deltaTime;_cinemachineTargetPitch += lookInput.y * Time.deltaTime;}// clamp our rotations so our values are limited 360 degrees_cinemachineTargetYaw = ClampAngle(_cinemachineTargetYaw, float.MinValue, float.MaxValue);_cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp);// Cinemachine will follow this targetCinemachineCameraTarget.transform.rotation = Quaternion.Euler(_cinemachineTargetPitch + CameraAngleOverride,_cinemachineTargetYaw, 0.0f);}private void Move(){// set target speed based on move speed, sprint speed and if sprint is pressedfloat targetSpeed = Input.GetKey(KeyCode.LeftShift) ? SprintSpeed : MoveSpeed;// a simplistic acceleration and deceleration designed to be easy to remove, replace, or iterate upon// note: Vector2's == operator uses approximation so is not floating point error prone, and is cheaper than magnitude// if there is no input, set the target speed to 0Vector2 moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));if (moveInput == Vector2.zero) targetSpeed = 0.0f;// a reference to the players current horizontal velocityfloat currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude;float speedOffset = 0.1f;// accelerate or decelerate to target speedif (currentHorizontalSpeed < targetSpeed - speedOffset ||currentHorizontalSpeed > targetSpeed + speedOffset){// creates curved result rather than a linear one giving a more organic speed change// note T in Lerp is clamped, so we don't need to clamp our speed_speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed, Time.deltaTime * SpeedChangeRate);// round speed to 3 decimal places_speed = Mathf.Round(_speed * 1000f) / 1000f;}else{_speed = targetSpeed;}_animationBlend = Mathf.Lerp(_animationBlend, targetSpeed, Time.deltaTime * SpeedChangeRate);if (_animationBlend < 0.01f) _animationBlend = 0f;// normalise input directionVector3 inputDirection = new Vector3(moveInput.x, 0.0f, moveInput.y).normalized;// note: Vector2's != operator uses approximation so is not floating point error prone, and is cheaper than magnitude// if there is a move input rotate player when the player is movingif (moveInput != Vector2.zero){_targetRotation = Mathf.Atan2(inputDirection.x, inputDirection.z) * Mathf.Rad2Deg +_mainCamera.transform.eulerAngles.y;float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity,RotationSmoothTime);// rotate to face input direction relative to camera positiontransform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);}Vector3 targetDirection = Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward;// move the player_controller.Move(targetDirection.normalized * (_speed * Time.deltaTime) +new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);// update animator if using characterif (_hasAnimator){_animator.SetFloat(_animIDSpeed, _animationBlend);_animator.SetFloat(_animIDMotionSpeed, 1);}}private void JumpAndGravity(){if (Grounded){// reset the fall timeout timer_fallTimeoutDelta = FallTimeout;// update animator if using characterif (_hasAnimator){_animator.SetBool(_animIDJump, false);_animator.SetBool(_animIDFreeFall, false);}// stop our velocity dropping infinitely when groundedif (_verticalVelocity < 0.0f){_verticalVelocity = -2f;}// Jumpif (Input.GetKeyDown(KeyCode.Space) && _jumpTimeoutDelta <= 0.0f){// the square root of H * -2 * G = how much velocity needed to reach desired height_verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);// update animator if using characterif (_hasAnimator){_animator.SetBool(_animIDJump, true);}}// jump timeoutif (_jumpTimeoutDelta >= 0.0f){_jumpTimeoutDelta -= Time.deltaTime;}}else{// reset the jump timeout timer_jumpTimeoutDelta = JumpTimeout;// fall timeoutif (_fallTimeoutDelta >= 0.0f){_fallTimeoutDelta -= Time.deltaTime;}else{// update animator if using characterif (_hasAnimator){_animator.SetBool(_animIDFreeFall, true);}}}// apply gravity over time if under terminal (multiply by delta time twice to linearly speed up over time)if (_verticalVelocity < _terminalVelocity){_verticalVelocity += Gravity * Time.deltaTime;}}private static float ClampAngle(float lfAngle, float lfMin, float lfMax){if (lfAngle < -360f) lfAngle += 360f;if (lfAngle > 360f) lfAngle -= 360f;return Mathf.Clamp(lfAngle, lfMin, lfMax);}private void OnDrawGizmosSelected(){Color transparentGreen = new Color(0.0f, 1.0f, 0.0f, 0.35f);Color transparentRed = new Color(1.0f, 0.0f, 0.0f, 0.35f);if (Grounded) Gizmos.color = transparentGreen;else Gizmos.color = transparentRed;// when selected, draw a gizmo in the position of, and matching radius of, the grounded colliderGizmos.DrawSphere(new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z),GroundedRadius);}private void OnFootstep(AnimationEvent animationEvent){if (animationEvent.animatorClipInfo.weight > 0.5f){if (FootstepAudioClips.Length > 0){var index = Random.Range(0, FootstepAudioClips.Length);AudioSource.PlayClipAtPoint(FootstepAudioClips[index], transform.TransformPoint(_controller.center), FootstepAudioVolume);}}}private void OnLand(AnimationEvent animationEvent){if (animationEvent.animatorClipInfo.weight > 0.5f){AudioSource.PlayClipAtPoint(LandingAudioClip, transform.TransformPoint(_controller.center), FootstepAudioVolume);}}}
}
其他
辅助调试函数OnDrawGizmosSelected
private void OnDrawGizmosSelected()
{Color transparentGreen = new Color(0.0f, 1.0f, 0.0f, 0.35f);Color transparentRed = new Color(1.0f, 0.0f, 0.0f, 0.35f);if (Grounded) Gizmos.color = transparentGreen;else Gizmos.color = transparentRed;// when selected, draw a gizmo in the position of, and matching radius of, the grounded colliderGizmos.DrawSphere(new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z),GroundedRadius);
}
这段代码绘制了一个球形,其位置位于角色的位置(transform.position)加上一个向下偏移量(GroundedOffset),以确保球形在角色的脚底下。球形的半径是 GroundedRadius,用来表示地面检测的范围。绘制的颜色是绿色(在地上)或者红色(空中)以区分两种状态。
动画事件
Unity Documentation - 使用动画事件
private void OnFootstep(AnimationEvent animationEvent)
{if (animationEvent.animatorClipInfo.weight > 0.5f){if (FootstepAudioClips.Length > 0){var index = Random.Range(0, FootstepAudioClips.Length);AudioSource.PlayClipAtPoint(FootstepAudioClips[index], transform.TransformPoint(_controller.center), FootstepAudioVolume);}}
}private void OnLand(AnimationEvent animationEvent)
{if (animationEvent.animatorClipInfo.weight > 0.5f){AudioSource.PlayClipAtPoint(LandingAudioClip, transform.TransformPoint(_controller.center), FootstepAudioVolume);}
}
动画事件会在播放到指定为止时调用一个函数,但是动画是动画,脚本是脚本,这就引出了一个关键问题:Unity去哪找这个函数。
我查了一下,应该是Animator播放动画,然后去Animator组件所在的Gameobject上的脚本里找定义(但是定义于所在Gameobject的子级Gameobject上的脚本是无效的)
这里代码就是播放声音(比如说踏出一步时播放脚步声)
其他参考
YouTube - Third Person Movement (With Animations) Unity Tutorial