Unity笔记:第三人称Starter Asset代码学习

前言

什么是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+gg2h

反解得 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且按下了跳跃时,设置状态机变量JumpTrue)。此举旨在防止玩家连续多次执行跳跃动作。

_fallTimeoutDelta 的值会在每帧更新时递减,直到达到零或以下。如果角色在空中,且 _fallTimeoutDelta 的值小于等于零,则会将角色视为处于自由落体状态(设置状态机变量FreeFallTrue

其他

整体逻辑:

  1. 首先检查角色是否在地面上(Grounded)。如果在地面上则:
    • 重置下落超时计时器(_fallTimeoutDelta = FallTimeout)。
    • 如果使用角色动画,更新动画状态。
    • 防止垂直速度无限下降,如果垂直速度小于0,则将其设置为-2f(这里有点疑问,速度不是平滑变过去的,是不是有些生硬?)
    • 如果输入中包含跳跃指令且跳跃超时时间小于等于0,则执行跳跃操作。跳跃操作会根据所需的跳跃高度计算所需的垂直速度,并更新动画状态。

在这里插入图片描述
在地面状态下JumpFreeFall均为false这样会一直维持在IWR状态下,不会变化

  1. 如果角色不在地面上,则:

    • 重置跳跃超时计时器(_jumpTimeoutDelta = JumpTimeout)。
    • 如果下落超时时间大于等于0,则递减下落超时时间,否则更新动画状态为自由下落。
    • 如果角色不在地面上,则将跳跃指令设置为false,防止在空中跳跃。
  2. 最后,无论角色是否在地面上,都会根据重力和时间应用垂直速度。如果垂直速度小于终端速度(_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,那么始终是无法进入落地动画的(落地动画是在落地以后播放)

  1. 设置球体检测点位置:

    • 计算一个球体检测点的位置 spherePosition,该位置位于角色位置的下方,并考虑了一个垂直偏移(GroundedOffset)。这个偏移通常用于调整地面检测点相对于角色的位置。
  2. 进行地面检测:

    • 使用 Physics.CheckSphere 方法检测位于 spherePosition 处的球体,半径为 GroundedRadius,是否与地面碰撞。GroundedRadius 可能是一个表示地面检测球体的半径的值。
    • 将检测结果保存到布尔变量 Grounded 中。如果球体与地面碰撞,则 Groundedtrue,否则为 false
    • GroundLayers 参数可能用于指定哪些层是地面层,只有与这些层碰撞的情况才会被认为是在地面上。
  3. 更新状态机的条件

    • 没啥说的

关于球体检测:球体检测通常用于检测角色或物体是否与地面碰撞,特别是在处理不规则形状的地面时,使用球形检测可以有助于减轻或避免由于小凸起或不规则地形引起的频繁状态切换问题。当角色经过小凸起时,如果直接依赖于简单的 isGrounded 属性,可能会导致在角色行走过程中不断地从“着地状态”到“空中状态”的频繁切换,这会影响角色动画的流畅性和游戏体验。

而且它还具有可拓展性,它可以可以在不同的时间点使用不同大小的球体来模拟角色的跳跃或下落状态,以检测角色是否在跳跃过程中与地面接触。

此外球形检测器不限于仅检测角色底部是否与地面接触。您可以将球形检测器放置在角色周围的任意位置,从而实现更复杂的地面检测逻辑,例如检测角色的脚部、身体或头部是否与地面接触。

Move()

Move()是处理角色移动的逻辑。实现了角色的平滑移动,包括加速度、减速度、动画混合以及根据输入方向旋转角色。

  1. 设置目标速度(targetSpeed:如果按下冲刺键,则使用 SprintSpeed 作为目标速度;否则,使用 MoveSpeed。而且如果没按下移动那就再改为0。

  2. 加速和减速:每帧使用Mathf.Lerp_speed插值靠近target,然后直到打到目标速度±speedOffset之后变为目标速度

  3. 动画混合:

    • 通过插值(Lerp)计算动画混合值 _animationBlend,用于在动画中平滑过渡速度的变化。
  4. 旋转角色:

    • 根据输入方向旋转角色,使角色朝向移动方向。这个旋转会基于相机的方向,使得角色相对于相机的运动更自然。
  5. 计算移动方向:

    • 根据输入方向计算移动方向。_targetRotation 存储了角色应该朝向的目标旋转角度。
  6. 移动角色:

    • 使用 CharacterControllerMove 方法来移动角色,考虑了水平速度和垂直速度。垂直速度通常用于处理跳跃和重力。
  7. 更新动画参数:

    • 如果使用了动画(_hasAnimatortrue),更新动画控制器中的速度和动作速度参数。这些参数通常用于控制角色的动画播放。

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了,并且删掉了一部分(适用于手柄输入的)功能,修改主要集中在以下地方:

  1. 移除了对UnityEngine.InputSystem的引用。
  2. 移除了与PlayerInput组件和相关引用相关的条件编译指令,并将其移除。
  3. 替换了原来使用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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/471789.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot 笔记 020 redis集成

1.1 安装redis Windows 下 Redis 安装与配置 教程_redis windows-CSDN博客 2.1 引入redis坐标 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency> 2.2 配置…

482. License Key Formatting(密钥格式化)

问题描述 给定一个许可密钥字符串 s&#xff0c;仅由字母、数字字符和破折号组成。字符串由 n 个破折号分成 n 1 组。你也会得到一个整数 k 。 我们想要重新格式化字符串 s&#xff0c;使每一组包含 k 个字符&#xff0c;除了第一组&#xff0c;它可以比 k 短&#xff0c;但…

opencv鼠标响应与操作

这节讲得好,建议仔细揣摩 Point sp(-1, -1);//初始位置 Point ep(-1, -1);//结束位置 Mat temp; static void on_draw(int event, int x, int y, int flags, void * userdata) {Mat image *((Mat*)userdata);//先将void类型转为Mat类型//((Mat*)userdata)是Mat类型指针 前面加…

B端系统升级方案模板:针对美观性和体验性升级(总体方案)

大家好&#xff0c;我是大美B端工场&#xff0c;专注于前端开发和UI设计&#xff0c;有需求可以私信。本篇从全局分享如何升级B端系统&#xff0c;搞B端系统升级的有个整体思维&#xff0c;不是说美化几个图标&#xff0c;修改几个页面就能解决的&#xff0c;这个方案模板&…

JavaScript_00001_00000

contents 简介变量与数据类型自动类型转换强制类型转换 简介 变量与数据类型 根据变量定义的范围不同&#xff0c;变量有全局变量和局部变量之分。直接定义的变量是全局变量&#xff0c;全局变量可以被所有的脚本访问&#xff1b;在函数里定义的变量称为局部变量&#xff0c;…

自动化测试定位不到元素怎么办?

1.动态id定位不到元素 分析原因&#xff1a;每次打开页面&#xff0c;ID都会变化。用ID去找元素&#xff0c;每次刷新页面ID都会发生变化。 解决方案&#xff1a;推荐使用xpath的相对路径方法或者cssSelector查找到该元素。 2.iframe原因定位不到元素 分析原因&#xff1a;…

Android---DslTabLayout实现底部导航栏

1. 在 Android 项目中引用 JitPack 库 AGP 8. 根目录的 settings.gradle dependencyResolutionManagement {...repositories {...maven { url https://jitpack.io }} } AGP 8. 根目录如果是 settings.gradle.kts 文件 dependencyResolutionManagement {...repositories {...…

【Windows】MacOS制作纯净版Windows10安装U盘

方法一、在window系统中更新win10&#xff08;不更新引导程序&#xff09; cp -rp /Volumes/Windows10专业版\ 64位/* /Volumes/WIN10/https://baijiahao.baidu.com/s?id1760695844372493842&wfrspider&forpc 方法二、在window系统中更新win10&#xff08;更新引导程…

threejs之使用shader实现雷达扫描

varying vec2 vUv; uniform vec3 uColor; uniform float uTime;mat2 rotate2d(float _angle){return mat2(cos(_angle),-sin(_angle),sin(_angle),cos(_angle)); }void main(){vec2 newUv rotate2d(uTime*6.18)*(vUv-0.5);float angle atan(newUv.x,newUv.y);// 根据uv坐标获…

STM32——OLED菜单(二级菜单)

文章目录 一.补充二. 二级菜单代码 简介&#xff1a;首先在我的51 I2C里面有OLED详细讲解&#xff0c;本期代码从51OLED基础上移植过来的&#xff0c;可以先看完那篇文章&#xff0c;在看这个&#xff0c;然后按键我是用的定时器扫描不会堵塞程序,可以翻开我的文章有单独的定时…

代码随想录算法训练营DAY18 | 二叉树 (5)

一、LeetCode 513 找树左下角的值 题目链接&#xff1a;513.找树左下角的值https://leetcode.cn/problems/find-bottom-left-tree-value/ 思路一&#xff1a;递归回溯全局变量比深度。 class Solution {int Max_depth 0;int result 0;public int findBottomLeftValue(TreeNo…

前端工程化面试题 | 10.精选前端工程化高频面试题

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…