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

这篇具有很好参考价值的文章主要介绍了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代码,开头长这样:
starter assets - thirdperson | updates in new charactercontroller package,Unity,学习,unity
有如下知识点:

引入UnityEngine命名空间确实包含了该命名空间中的所有内容,但是这并不包括UnityEngine命名空间内部其他子命名空间的内容。每个子命名空间在Unity中都被设计为相对独立,需要显式引入才能访问其中的类和功能。所以后面条件编译指令里又引入了UnityEngine.InputSystem

底下就是声明特性(Attribute)的一些代码:

// 要求将拥有这个特性的组件附加到同一 GameObject 上时
// 该 GameObject 必须包含 CharacterController 组件
[RequireComponent(typeof(CharacterController))]

然后再底下就是把变量暴露到编辑器中的操作。一个叫Player的标题,其下有一堆属性可供设置,

  • [Tooltip("...")]设置在 Unity 编辑器中将鼠标悬停在变量上时的提示信息。
  • [Space(10)]是在变量上面创建一个高度为 10 个像素的空白间隔,以提高可读性。
  • [Range(x,y)]则可以为变量创建一个范围在[x,y]之间的滑动控制条

在编辑器中就这样:
starter assets - thirdperson | updates in new charactercontroller package,Unity,学习,unity
显然,在这里具有如下的命名规则:私有变量都是下划线开头的。public变量是大驼峰命名法,private是小驼峰。

值得一提的是,这里是属性的声明

private bool IsCurrentDeviceMouse
{
    get
    {
#if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED
        return _playerInput.currentControlScheme == "KeyboardMouse";
#else
		return 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 的动画系统中,动画参数(如速度、是否在地面上等)是通过哈希值来标识的,而不是直接使用字符串。使用整数值的好处是:

  • 比较速度比字符串快
  • 通常比字符串占的内存小
  • 没拼写错误

状态机与动画系统

starter assets - thirdperson | updates in new charactercontroller package,Unity,学习,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 character
        if (_hasAnimator)
        {
            _animator.SetBool(_animIDJump, false);
            _animator.SetBool(_animIDFreeFall, false);
        }

        // stop our velocity dropping infinitely when grounded
        if (_verticalVelocity < 0.0f)
        {
            _verticalVelocity = -2f;
        }

        // Jump
        if (_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 character
            if (_hasAnimator)
            {
                _animator.SetBool(_animIDJump, true);
            }
        }

        // jump timeout
        if (_jumpTimeoutDelta >= 0.0f)
        {
            _jumpTimeoutDelta -= Time.deltaTime;
        }
    }
    else
    {
        // reset the jump timeout timer
        _jumpTimeoutDelta = JumpTimeout;

        // fall timeout
        if (_fallTimeoutDelta >= 0.0f)
        {
            _fallTimeoutDelta -= Time.deltaTime;
        }
        else
        {
            // update animator if using character
            if (_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,则执行跳跃操作。跳跃操作会根据所需的跳跃高度计算所需的垂直速度,并更新动画状态。

starter assets - thirdperson | updates in new charactercontroller package,Unity,学习,unity
在地面状态下JumpFreeFall均为false这样会一直维持在IWR状态下,不会变化

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

    • 重置跳跃超时计时器(_jumpTimeoutDelta = JumpTimeout)。
    • 如果下落超时时间大于等于0,则递减下落超时时间,否则更新动画状态为自由下落。
    • 如果角色不在地面上,则将跳跃指令设置为false,防止在空中跳跃。
  2. 最后,无论角色是否在地面上,都会根据重力和时间应用垂直速度。如果垂直速度小于终端速度(_terminalVelocity),则会根据重力和时间逐渐增加垂直速度。

GroundedCheck()

GroundedCheck()的作用是检测角色是否在地面上,并将检测结果存储在 Grounded 变量中。

private void GroundedCheck()
{
    // set sphere position, with offset
    Vector3 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 character
    if (_hasAnimator)
    {
        _animator.SetBool(_animIDGrounded, Grounded);
    }
}

starter assets - thirdperson | updates in new charactercontroller package,Unity,学习,unity

虽然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

starter assets - thirdperson | updates in new charactercontroller package,Unity,学习,unity
使用参数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 fixed
    if (_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 target
    CinemachineCameraTarget.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;

        // cinemachine
        private float _cinemachineTargetYaw;
        private float _cinemachineTargetPitch;

        // player
        private float _speed;
        private float _animationBlend;
        private float _targetRotation = 0.0f;
        private float _rotationVelocity;
        private float _verticalVelocity;
        private float _terminalVelocity = 53.0f;

        // timeout deltatime
        private float _jumpTimeoutDelta;
        private float _fallTimeoutDelta;

        // animation IDs
        private 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 camera
            if (_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 offset
            Vector3 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 character
            if (_hasAnimator)
            {
                _animator.SetBool(_animIDGrounded, Grounded);
            }
        }

        private void CameraRotation()
        {
            // if there is an input and camera position is not fixed
            Vector2 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 target
            CinemachineCameraTarget.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 pressed
            float 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 0
            Vector2 moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
            if (moveInput == Vector2.zero) targetSpeed = 0.0f;

            // a reference to the players current horizontal velocity
            float currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude;

            float speedOffset = 0.1f;


            // accelerate or decelerate to target speed
            if (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 direction
            Vector3 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 moving
            if (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 position
                transform.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 character
            if (_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 character
                if (_hasAnimator)
                {
                    _animator.SetBool(_animIDJump, false);
                    _animator.SetBool(_animIDFreeFall, false);
                }

                // stop our velocity dropping infinitely when grounded
                if (_verticalVelocity < 0.0f)
                {
                    _verticalVelocity = -2f;
                }

                // Jump
                if (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 character
                    if (_hasAnimator)
                    {
                        _animator.SetBool(_animIDJump, true);
                    }
                }

                // jump timeout
                if (_jumpTimeoutDelta >= 0.0f)
                {
                    _jumpTimeoutDelta -= Time.deltaTime;
                }
            }
            else
            {
                // reset the jump timeout timer
                _jumpTimeoutDelta = JumpTimeout;

                // fall timeout
                if (_fallTimeoutDelta >= 0.0f)
                {
                    _fallTimeoutDelta -= Time.deltaTime;
                }
                else
                {
                    // update animator if using character
                    if (_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 collider
            Gizmos.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 collider
    Gizmos.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);
    }
}

starter assets - thirdperson | updates in new charactercontroller package,Unity,学习,unity

动画事件会在播放到指定为止时调用一个函数,但是动画是动画,脚本是脚本,这就引出了一个关键问题:Unity去哪找这个函数。

我查了一下,应该是Animator播放动画,然后去Animator组件所在的Gameobject上的脚本里找定义(但是定义于所在Gameobject的子级Gameobject上的脚本是无效的)

这里代码就是播放声音(比如说踏出一步时播放脚步声)

其他参考

YouTube - Third Person Movement (With Animations) Unity Tutorial文章来源地址https://www.toymoban.com/news/detail-836789.html

到了这里,关于Unity笔记:第三人称Starter Asset代码学习的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • unity-第三人称摄像机简单脚本(包括跟随、视角旋转、滚轮缩放)

    本菜鸟为了实现第三人称视角摄像机的主要功能,踩了很多意料之外的坑,终于搞出一份自觉完美的脚本,分享一下。 功能:摄像机跟随、鼠标滚轮缩放视野、长按鼠标右键左右旋转视野、长按鼠标中键上下旋转视野。 此脚本挂接在摄像机上,有详细注释,可直接运行。

    2024年02月08日
    浏览(66)
  • unity3d 实现第三人称移动与摄像机调整

    首先展示效果 unity3d 关于人物移动,这里推荐使用Character Conrroller组件,优点就是可以不用处理刚体,不受重力的影响,自带物理碰撞检测。  人物移动代码包括转向 接下来就是摄影机的调整,这里使用unity自带的插件cinemachine,非常的推荐,无需使用冗杂的代码即可设计较好

    2024年02月11日
    浏览(50)
  • Unity解决:3D开发模式第三人称视角 WASD控制角色移动旋转 使用InputSystem

    Unity版本:2019.2.3f1 目录  安装InputSystem 1:创建InputHander.cs脚本 挂载到Player物体上 获取键盘输入WADS 2.创建PlayerLocomotion.cs挂载到Player物体上,控制物体移动转向   菜单栏/Window/Package Manager/Input System  工程面板内 右键--创建Input Actions  选中New Controls改名为PlayerControls 然后属

    2024年02月06日
    浏览(53)
  • ue4第三人称第一人称切换。

    1.默认第三人称。将远点的相机设置auto active为true。近点相机设置为false。   在做好的actor中设置camera的active,和人物是否隐藏。    通过v键来切换第一人称和第二人称。

    2024年02月12日
    浏览(54)
  • UE4第三人称多人联机游戏开发01

    单人游戏 中一台电脑只有一个游戏会话(Session),我们可以通过在同一电脑中分屏并用不同的输入设备,单人游戏不需要使用互联网。 多人游戏 中,游戏实例在独立的两台电脑中运行,而由于玩家在不断地输入指令,因此我们需要将一个游戏实例中的信息通过互联网传输到

    2024年02月07日
    浏览(90)
  • VR工地安全虚拟现实体验:多种事故模拟,第三人称回看

    建筑工地五大伤害是指:高处坠落、坍塌、物体打击、机械伤害、触电。利用VR(虚拟现实)技术体验建筑工地五大伤害,可以为建筑工人提供更真实、更安全的工作环境,同时也可以帮助他们更好地了解和掌握工作技能。 以下是 VR工地安全虚拟现实体验软件 的价值: 首先,通

    2024年02月12日
    浏览(49)
  • UE4/5 实现多相机模式切换(第一人称切第三人称等)

    对于多相机模式的切换,网上普遍使用SetViewTargetWithBlend这个节点,但这个节点很多相机参数不能设置,也不适合运用于游戏中多相机模式切换的场合。 UE中World Settings里的玩家控制器中,默认的控制器行为会对当前开启的Camera组件进行激活处理: 因此,默认绑在Pawn上的相机

    2024年02月11日
    浏览(46)
  • Unity | HDRP高清渲染管线学习笔记:HDRP配置文件(HDRP Asset)

    目录 一、Frame Settings(帧设置) 二、Volume 三、HDRP配置文件、帧设置和Volume之间的关系 四、HDRP配置文件 1.Rendering (1)Color Buffer Format(颜色缓存格式) (2)Lit Shader Mode(Lit着色器模式) (3)Motion Vectors(运动矢量) 2.Post-processing Quality Settings(后处理质量设置) 五、针对

    2024年02月16日
    浏览(46)
  • unity3d:asset store上C#代码热重载插件,不需要重运行,重新加载更新后函数

    https://assetstore.unity.com/packages/tools/utilities/hot-reload-edit-code-without-compiling-250972?clickref=1011lwHg8abvutm_source=partnerizeutm_medium=affiliateutm_campaign=unity_affiliate#description ●方法1:通过导航到Unity菜单栏中的“窗口”打开热重装窗口,然后选择“热重装”。 ●方法2:使用快捷键组合“Alt+Shift

    2024年02月02日
    浏览(52)
  • 【UnityShader入门精要学习笔记】第三章(1)Unity Shader介绍

    本系列为作者学习UnityShader入门精要而作的笔记,内容将包括: 书本中句子照抄 + 个人批注 项目源码 一堆新手会犯的错误 潜在的太监断更,有始无终 总之适用于同样开始学习Shader的同学们进行有取舍的参考。 从本章节开始我们要学习Shader相关的知识了,诸位看客可能有的

    2024年02月02日
    浏览(71)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包