找回密码
 立即注册
首页 业界区 安全 【Unity】类蜘蛛侠+刺客信条暗杀动作系统开发日志 ...

【Unity】类蜘蛛侠+刺客信条暗杀动作系统开发日志

济曝喊 7 天前
新版输入系统——斜向移动变快问题解决


生成对应的input管理脚本

Day 01——角色移动基类

CharacterMovementControlBase
  1. using UnityEngine;namespace Spiderman.Movement{    [RequireComponent(typeof(CharacterController))]    public abstract class CharacterMovementControlBase : MonoBehaviour    {        // 角色控制器组件,用于处理角色移动相关的物理交互        private CharacterController _controller;        // 动画组件,用于控制角色动画播放        private Animator _animator;        // 地面检测相关变量        protected bool _characterIsOnGround;        [Header("地面检测相关变量")]        [SerializeField]protected float _groundDetectionPositionOffset; // 地面检测位置偏移量        [SerializeField]protected float _detectionRang;                 // 地面检测范围        [SerializeField]protected LayerMask _whatIsGround;              // 地面层掩码        // 重力相关变量        protected readonly float CharacterGravity = -9.8f;        protected float _characterVerticalVelocity;     // 角色垂直方向速度        protected float _fallOutDeltaTime;              // 下落 delta 时间,用于计算重力作用的时间积累        protected float _fallOutTime = 0.15f;           // 下落等待时间,控制跌落动画播放时机        protected readonly float _characterVerticalMaxVelocity = 54f; // 角色最大垂直速度,低于这个值应用重力        protected Vector3 _characterVerticalDirection;  // 角色Y轴移动方向,通过charactercontroller.move来实现y轴移动        // 初始化函数,在对象实例化后、Start 之前调用,获取必要组件        protected virtual void Awake()        {            _controller = GetComponent();            _animator = GetComponent();        }        protected virtual void Start()        {            _fallOutDeltaTime = _fallOutTime;        }        private void Update()        {            SetCharacterGravity();            UpdateCharacterGravity();        }        ///         /// 地面检测方法        ///         /// 返回角色是否在地面的布尔值        private bool GroundDetection()        {            // 构建检测位置:基于角色当前位置,调整 Y 轴偏移(用于地面检测的位置修正)            Vector3 detectionPosition = new Vector3(                transform.position.x,                transform.position.y - _groundDetectionPositionOffset,                transform.position.z            );            // 球形检测:检查在指定位置、指定半径范围内,与 _whatIsGround 层的碰撞体是否存在相交            // 参数分别为:检测中心、检测半径、地面层掩码、忽略触发器交互            return Physics.CheckSphere(                detectionPosition,                _detectionRang,                _whatIsGround,                QueryTriggerInteraction.Ignore            );        }        ///         /// 根据是否在地面设置对应的角色重力逻辑        ///         private void SetCharacterGravity()        {            // 检测角色是否在地面            _characterIsOnGround = GroundDetection();            if (_characterIsOnGround)            {                //1.在地面                // 1.1 重置下落等待时间                _fallOutDeltaTime = _fallOutTime;                // 1.2 重置垂直速度(防止落地后持续累积速度)                if (_characterVerticalVelocity < 0)                {                    _characterVerticalVelocity = -2f;                }            }            else            {                //2.不在地面                if (_fallOutDeltaTime > 0)                {                    // 2.1 处理楼梯/小落差:等待 0.15 秒后再应用重力                    _fallOutDeltaTime -= Time.deltaTime;                }                else                {                    // 2.2 倒计时结束还没有落地?那说明不是小落差,要开始应用重力                }                if (_characterVerticalVelocity < _characterVerticalMaxVelocity)                {                    _characterVerticalVelocity += CharacterGravity * Time.deltaTime;                    // 重力公式累积垂直速度                }            }        }        ///         /// 更新角色垂直方向移动(应用重力效果)        ///         private void UpdateCharacterGravity()        {            //这里只处理 y 轴重力            // x/z 由其他移动逻辑控制            Vector3 _characterVerticalDirection = new Vector3(0, _characterVerticalVelocity, 0);            // 通过 CharacterController 应用y轴移动            _controller.Move(_characterVerticalDirection * Time.deltaTime);        }        ///         /// 斜坡方向重置:检测角色是否在坡上移动,防止下坡速度过快导致异常        ///         /// 原始移动方向        /// 适配斜坡后的移动方向        private Vector3 SlopResetDirection(Vector3 moveDirection)        {            // 射线检测参数配置            Vector3 rayOrigin = transform.position + transform.up * 0.5f;   // 射线起点            Vector3 rayDirection = Vector3.down;                            // 射线方向            float maxDistance = _controller.height * 0.85f;                 // 射线最大距离            LayerMask targetLayer = _whatIsGround;                          // 检测的目标地面层            QueryTriggerInteraction triggerInteraction = QueryTriggerInteraction.Ignore; // 忽略触发器            // 执行向下的射线检测            if (Physics.Raycast(rayOrigin, rayDirection, out RaycastHit hit, maxDistance, targetLayer, triggerInteraction))            {                // 点积判断:检测地面法线是否与角色上方向垂直(点积接近0表示垂直,非0则说明有坡度)                if (Vector3.Dot(transform.up, hit.normal) != 0)                {                    // 将移动方向投影到斜坡平面                    moveDirection = Vector3.ProjectOnPlane(moveDirection, hit.normal);                }            }            return moveDirection;        }        private void OnDrawGizmos()        {            // 设置gizmos颜色为红色,使其更容易看到            Gizmos.color = Color.red;              Vector3 detectionPosition = new Vector3(                transform.position.x,                transform.position.y - _groundDetectionPositionOffset,                transform.position.z            );            Gizmos.DrawWireSphere(detectionPosition, _detectionRang);        }    }}
复制代码
PlayerMovementControl
  1. using System.Collections;using System.Collections.Generic;using UnityEngine;namespace Spiderman.Movement{    public class PlayerMovementControl : CharacterMovementControlBase    {    }}
复制代码
Day02 带碰撞体相机脚本

GameInputManager
  1. using System.Collections;using System.Collections.Generic;using UnityEngine;public class GameInputManager : MonoBehaviour{    private GameInputAction _gameInputAction;    public Vector2 Movement => _gameInputAction.Player.Movement.ReadValue();    public Vector2 CameraLook => _gameInputAction.Player.CameraLook.ReadValue();    private void Awake()    {        _gameInputAction ??= new GameInputAction(); //是空的,则创建新的实例    }    private void OnEnable()    {        _gameInputAction.Enable();    }    private void OnDisable()    {        _gameInputAction.Disable();    }}
复制代码



TP_CameraControl
  1. using System.Collections;using System.Collections.Generic;using UnityEngine;public class TP_CameraControl : MonoBehaviour{    private GameInputManager _gameInputManager;    [Header("相机参数配置")]    [SerializeField] private Transform _lookTarget;             //相机跟随目标    [SerializeField] private float _controlSpeed;               //相机移动速度    [SerializeField] private Vector2 _cameraVerticalMaxAngle;   //相机上下旋转角度限制    [SerializeField] private Vector2 _cameraHorizontalMaxAngle; //相机左右旋转角度限制    [SerializeField] private float _smoothSpeed;                //平滑速度    [SerializeField] private float _cameraDistance;             //相机到跟随目标的距离    [SerializeField] private float _cameraHeight;               //相机高度    [SerializeField] private float _DistancemoothTime;         //位置跟随平滑时间    private Vector3 smoothDampVelocity = Vector3.zero;          //旋转阻尼    private Vector2 _input;                                     // 输入值    private Vector3 _cameraRotation;                            // 相机旋转方向    private bool _cameraInputEnabled = true;                    // 相机输入是否启用    private void Awake()    {        // 获取游戏输入管理组件        _gameInputManager = GetComponent();        //隐藏光标        Cursor.lockState = CursorLockMode.Locked;        Cursor.visible = false;    }    private void Update()    {        // 检测到按下ESC键或鼠标左键点击窗口,则切换相机输入状态        HandleCameraInputToggle();        // 只有在相机输入启用时才处理输入        if (_cameraInputEnabled)        {            // 实时处理相机输入            CameraInput();        }    }    private void LateUpdate()    {        // 更新相机旋转        UpdateCameraRotation();        // 更新相机位置        UpdateCameraPosition();    }    ///     /// 处理相机输入,获取并处理上下查看等输入,限制垂直角度范围    ///     private void CameraInput()    {        // 获取相机xy轴输入        _input.y += _gameInputManager.CameraLook.x * _controlSpeed;        _input.x -= _gameInputManager.CameraLook.y * _controlSpeed;        // 限制相机垂直方向角度范围,垂直方向是绕 x 轴旋转,所以平滑的是x轴输入        _input.x = Mathf.Clamp(            _input.x,            _cameraVerticalMaxAngle.x,            _cameraVerticalMaxAngle.y        );        // 限制相机水平方向角度范围,水平方向是绕 y 轴旋转,所以限制的是y轴输入        _input.y = Mathf.Clamp(            _input.y,            _cameraHorizontalMaxAngle.x,            _cameraHorizontalMaxAngle.y        );    }    ///     /// 更新相机旋转    ///     private void UpdateCameraRotation()    {        var targetRotation = new Vector3(_input.x, _input.y, 0);        _cameraRotation = Vector3.SmoothDamp(            _cameraRotation,            targetRotation,            ref smoothDampVelocity,            _smoothSpeed        );        //更新相机欧拉角        transform.eulerAngles = _cameraRotation;    }    ///     /// 更新相机位置    ///     private void UpdateCameraPosition()    {        var newPos = _lookTarget.position             + Vector3.back * _cameraDistance             + Vector3.up * _cameraHeight;        // 平滑位置移动        transform.position = Vector3.Lerp(            transform.position,            newPos,            _DistancemoothTime        );    }    ///     /// 处理相机输入状态切换    ///     private void HandleCameraInputToggle()    {        // 检测ESC键切换相机输入状态        if (Input.GetKeyDown(KeyCode.Escape))        {            _cameraInputEnabled = false;            // 显示光标并解锁            Cursor.lockState = CursorLockMode.None;            Cursor.visible = true;        }        // 检测鼠标左键点击窗口来恢复相机控制        if (Input.GetMouseButtonDown(0) && !_cameraInputEnabled)        {            _cameraInputEnabled = true;            // 隐藏光标并锁定            Cursor.lockState = CursorLockMode.Locked;            Cursor.visible = false;        }    }}
复制代码
加入摄像机碰撞逻辑

GameInputManager继承于单例模式
  1. using System.Collections;using System.Collections.Generic;using UnityEngine;using GGG.Tool.Singleton;public class GameInputManager : Singleton{    private GameInputAction _gameInputAction;    public Vector2 Movement => _gameInputAction.Player.Movement.ReadValue();    public Vector2 CameraLook => _gameInputAction.Player.CameraLook.ReadValue();    private void Awake()    {        base.Awake();        _gameInputAction ??= new GameInputAction(); //是空的,则创建新的实例    }    private void OnEnable()    {        _gameInputAction.Enable();    }    private void OnDisable()    {        _gameInputAction.Disable();    }}
复制代码
TP_CameraControl
  1. using System.Collections;using System.Collections.Generic;using UnityEngine;using GGG.Tool;public class TP_CameraControl : MonoBehaviour{    [Header("相机参数配置")]    [SerializeField] private Transform _lookTarget;             //相机跟随目标    [SerializeField] private float _controlSpeed;               //相机移动速度    [SerializeField] private Vector2 _cameraVerticalMaxAngle;   //相机上下旋转角度限制    [SerializeField] private Vector2 _cameraHorizontalMaxAngle; //相机左右旋转角度限制    [SerializeField] private float _smoothSpeed;                //平滑速度    [SerializeField] private float _cameraDistance;             //相机到跟随目标的距离    [SerializeField] private float _cameraHeight;               //相机高度    [SerializeField] private float _distanceSmoothTime;         //位置跟随平滑时间    private Vector3 smoothDampVelocity = Vector3.zero;          //旋转阻尼    private Vector2 _input;                                     // 输入值    private Vector3 _cameraRotation;                            // 相机旋转方向    private bool _cameraInputEnabled = true;                    // 相机输入是否启用    private void Awake()    {        //隐藏光标        Cursor.lockState = CursorLockMode.Locked;        Cursor.visible = false;    }    private void Update()    {        // 检测到按下ESC键或鼠标左键点击窗口,则切换相机输入状态        HandleCameraInputToggle();        // 只有在相机输入启用时才处理输入        if (_cameraInputEnabled)        {            // 实时处理相机输入            CameraInput();        }    }    private void LateUpdate()    {        // 更新相机旋转        UpdateCameraRotation();        // 更新相机位置        UpdateCameraPosition();    }    ///     /// 处理相机输入,获取并处理上下查看等输入,限制垂直角度范围    ///     private void CameraInput()    {        // 获取相机xy轴输入        _input.y += GameInputManager.MainInstance.CameraLook.x * _controlSpeed;        _input.x -= GameInputManager.MainInstance.CameraLook.y * _controlSpeed;        // 限制相机垂直方向角度范围,垂直方向是绕 x 轴旋转,所以平滑的是x轴输入        _input.x = Mathf.Clamp(            _input.x,            _cameraVerticalMaxAngle.x,            _cameraVerticalMaxAngle.y        );        // 限制相机水平方向角度范围,水平方向是绕 y 轴旋转,所以限制的是y轴输入        _input.y = Mathf.Clamp(            _input.y,            _cameraHorizontalMaxAngle.x,            _cameraHorizontalMaxAngle.y        );    }    ///     /// 更新相机旋转    ///     private void UpdateCameraRotation()    {        var targetRotation = new Vector3(_input.x, _input.y, 0);        _cameraRotation = Vector3.SmoothDamp(            _cameraRotation,            targetRotation,            ref smoothDampVelocity,            _smoothSpeed        );        //更新相机欧拉角        transform.eulerAngles = _cameraRotation;    }    ///     /// 更新相机位置    ///     private void UpdateCameraPosition()    {        var newPos = _lookTarget.position             + Vector3.back * _cameraDistance             + Vector3.up * _cameraHeight;        // 平滑位置移动        transform.position = Vector3.Lerp(            transform.position,            newPos,            DevelopmentToos.UnTetheredLerp(_distanceSmoothTime)        );    }    ///     /// 处理相机输入状态切换    ///     private void HandleCameraInputToggle()    {        // 检测ESC键切换相机输入状态        if (Input.GetKeyDown(KeyCode.Escape))        {            _cameraInputEnabled = false;            // 显示光标并解锁            Cursor.lockState = CursorLockMode.None;            Cursor.visible = true;        }        // 检测鼠标左键点击窗口来恢复相机控制        if (Input.GetMouseButtonDown(0) && !_cameraInputEnabled)        {            _cameraInputEnabled = true;            // 隐藏光标并锁定            Cursor.lockState = CursorLockMode.Locked;            Cursor.visible = false;        }    }}
复制代码
Day03 Movement

动画部分





脚本

CharacterMovementControlBase
  1.         protected Vector3 _moveDirection; // 角色移动方向
复制代码
  1.         ///         /// 脚本控制animator的根运动        ///         protected virtual void OnAnimatorMove()        {            _animator.ApplyBuiltinRootMotion();            UpdateCharacterMoveDirection(_animator.deltaPosition);        }
复制代码
  1.         ///         /// 更新角色水平移动方向——绕y轴旋转        ///         protected void UpdateCharacterMoveDirection(Vector3 direction)        {            _moveDirection = SlopResetDirection(direction);            _controller.Move(_moveDirection * Time.deltaTime);        }
复制代码
GameInputManager
  1.     public bool Run => _gameInputAction.Player.Run.triggered;
复制代码
PlayerMovementControl
  1. using GGG.Tool;using System.Collections;using System.Collections.Generic;using UnityEngine;namespace Spiderman.Movement{    public class PlayerMovementControl : CharacterMovementControlBase    {        [SerializeField] float moveSpeed = 1.5f;        // 角色旋转角度(绕 Y 轴)        private float _rotationAngle;        // 旋转角速度        private float _angleVelocity = 0;        // 旋转平滑时间        [SerializeField] private float _rotationSmoothTime;        private Transform _mainCamera;        protected override void Awake()        {            base.Awake();            _mainCamera = Camera.main.transform;        }        private void LateUpdate()        {            UpdateAnimation();            CharacterRotationControl();        }        ///         /// 角色旋转控制        ///         private void CharacterRotationControl()        {            // 不在地面时直接返回,不处理旋转            if (!_characterIsOnGround)                return;            // 处理输入存在时的旋转角度计算            if (_animator.GetBool("HasInput"))            {                _rotationAngle =                    Mathf.Atan2(                        GameInputManager.MainInstance.Movement.x,                        GameInputManager.MainInstance.Movement.y                    ) * Mathf.Rad2Deg                    + _mainCamera.eulerAngles.y;          // 计算角色的旋转角度(弧度转角度)              }            // 满足HasInput==true且处于“Motion”动画标签时,平滑更新角色旋转            if (_animator.GetBool("HasInput") && _animator.AnimationAtTag("Motion"))            {                transform.eulerAngles = Vector3.up                                        * Mathf.SmoothDampAngle(                                            transform.eulerAngles.y,                                            _rotationAngle,                                            ref _angleVelocity,                                            _rotationSmoothTime                                        );            }        }        ///         /// 更新动画        ///         private void UpdateAnimation()        {            if (!_characterIsOnGround)                return;            _animator.SetBool("HasInput", GameInputManager.MainInstance.Movement != Vector2.zero);            if (_animator.GetBool("HasInput"))            {                if (GameInputManager.MainInstance.Run)                {                    //按下奔跑键                    _animator.SetBool("Run",true);                }                //有输入                //  Run被开启,那就Movement设置为2,否则设置为输入的两个轴的平方                var targetSpeed = _animator.GetBool("Run") ? 2f :GameInputManager.MainInstance.Movement.sqrMagnitude;                _animator.SetFloat(                    "Movement",                    targetSpeed / _animator.humanScale * moveSpeed,                    0.25f,                    Time.deltaTime                );            }            else            {                //无输入                _animator.SetFloat("Movement", 0f, 0.25f, Time.deltaTime);                if (_animator.GetFloat("Movement") < 0.2f)                {                    _animator.SetBool("Run", false);                }            }        }    }}
复制代码
Day04  事件管理器

GameEventManager
  1. using System.Collections;using System.Collections.Generic;using UnityEngine;using System;using GGG.Tool;using GGG.Tool.Singleton;public class GameEventManager : SingletonNonMono{    // 事件接口    private interface IEventHelp    {    }    // 事件类,实现 IEventHelp 接口,用于管理事件注册、调用等逻辑    private class EventHelp : IEventHelp    {        // 存储事件委托        private event Action _action;        // 构造函数,初始化时绑定初始事件逻辑        public EventHelp(Action action)        {            // 首次实例化时赋值,仅执行这一次初始绑定            _action = action;        }        // 增加事件注册的方法,将新的事件逻辑追加到委托中        public void AddCall(Action action)        {            _action += action;        }        // 调用事件的方法,若有绑定逻辑则执行        public void Call()        {            _action?.Invoke();        }        // 移除事件的方法,将指定事件逻辑从委托中移除        public void Remove(Action action)        {            _action -= action;        }    }    private class EventHelp : IEventHelp    {        // 存储事件委托        private event Action _action;        // 构造函数,初始化时绑定初始事件逻辑        public EventHelp(Action action)        {            // 首次实例化时赋值,仅执行这一次初始绑定            _action = action;        }        // 增加事件注册的方法,将新的事件逻辑追加到委托中        public void AddCall(Action action)        {            _action += action;        }        // 调用事件的方法,若有绑定逻辑则执行        public void Call(T value)        {            _action?.Invoke(value);        }        // 移除事件的方法,将指定事件逻辑从委托中移除        public void Remove(Action action)        {            _action -= action;        }    }    private class EventHelp : IEventHelp    {        // 存储事件委托        private event Action _action;        // 构造函数,初始化时绑定初始事件逻辑        public EventHelp(Action action)        {            // 首次实例化时赋值,仅执行这一次初始绑定            _action = action;        }        // 增加事件注册的方法,将新的事件逻辑追加到委托中        public void AddCall(Action action)        {            _action += action;        }        // 调用事件的方法,若有绑定逻辑则执行        public void Call(T1 value1, T2 value2)        {            _action?.Invoke(value1, value2);        }        // 移除事件的方法,将指定事件逻辑从委托中移除        public void Remove(Action action)        {            _action -= action;        }    }    ///     /// 事件中心,用于管理事件注册、调用    ///     private Dictionary _eventCenter = new Dictionary();    ///     /// 添加事件监听    ///     /// 事件名称    /// 回调函数    public void AddEventListening(string eventName, Action action)    {        if (_eventCenter.TryGetValue(eventName, out var eventHelp))        {            (eventHelp as EventHelp)?.AddCall(action);        }        else        {            // 如果事件中心不存在叫这个名字的事件,new一个然后添加            _eventCenter.Add(eventName, new EventHelp(action));        }    }    public void AddEventListening(string eventName, Action action)    {        if (_eventCenter.TryGetValue(eventName, out var eventHelp))        {            (eventHelp as EventHelp)?.AddCall(action);        }        else        {            // 如果事件中心不存在叫这个名字的事件,new一个然后添加            _eventCenter.Add(eventName, new EventHelp(action));        }    }    public void AddEventListening(string eventName, Action action)    {        if (_eventCenter.TryGetValue(eventName, out var eventHelp))        {            (eventHelp as EventHelp)?.AddCall(action);        }        else        {            // 如果事件中心不存在叫这个名字的事件,new一个然后添加            _eventCenter.Add(eventName, new EventHelp(action));        }    }    ///     /// 调用事件    ///     /// 事件名称    public void CallEvent(string eventName)    {        if (_eventCenter.TryGetValue(eventName, out var eventHelp))        {            (eventHelp as EventHelp)?.Call();        }        else        {            LogEventNotFound(eventName, "调用");        }    }    public void CallEvent(string eventName, T value)    {        if (_eventCenter.TryGetValue(eventName, out var eventHelp))        {            (eventHelp as EventHelp)?.Call(value);        }        else        {            LogEventNotFound(eventName, "调用");        }    }    public void CallEvent(string eventName, T1 value, T2 value1)    {        if (_eventCenter.TryGetValue(eventName, out var eventHelp))        {            (eventHelp as EventHelp)?.Call(value, value1);        }        else        {            LogEventNotFound(eventName, "调用");        }    }    ///     /// 移除事件监听    ///     /// 事件名称    /// 要移除的事件回调    public void RemoveEvent(string eventName, Action action)    {        if (_eventCenter.TryGetValue(eventName, out var eventHelp))        {            (eventHelp as EventHelp)?.Remove(action);        }        else        {            LogEventNotFound(eventName, "移除");        }    }    public void RemoveEvent(string eventName, Action action)    {        if (_eventCenter.TryGetValue(eventName, out var eventHelp))        {            (eventHelp as EventHelp)?.Remove(action);        }        else        {            LogEventNotFound(eventName, "移除");        }    }    public void RemoveEvent(string eventName, Action action)    {        if (_eventCenter.TryGetValue(eventName, out var eventHelp))        {            (eventHelp as EventHelp)?.Remove(action);        }        else        {            LogEventNotFound(eventName, "移除");        }    }    ///     /// 事件未找到时的统一日志输出    ///     /// 事件名称    /// 操作类型(移除、调用)    private void LogEventNotFound(string eventName, string operation)    {        DevelopmentTools.WTF($"当前未找到{eventName}的事件,无法{operation}");    }}
复制代码
Day05 AnimationStringToHash
  1. using System.Collections;using System.Collections.Generic;using UnityEngine;/// /// 动画参数哈希值管理类,用于统一存储Animator参数的哈希值,避免重复计算/// public class AnimationID{    // 角色移动相关动画参数哈希    public static readonly int MovementID = Animator.StringToHash("Movement");    public static readonly int LockID = Animator.StringToHash("Lock");    public static readonly int HorizontalID = Animator.StringToHash("Horizontal");    public static readonly int VerticalID = Animator.StringToHash("Vertical");    public static readonly int HasInputID = Animator.StringToHash("HasInput");    public static readonly int RunID = Animator.StringToHash("Run");}
复制代码
Day06  GameTimer
  1. using System;using System.Collections;using System.Collections.Generic;using UnityEngine;/// /// 计时器状态枚举,描述计时器不同工作阶段/// public enum TimerState{    NOTWORKERE, // 没有工作(初始或重置后状态)    WORKERING,  // 工作中(计时进行时)    DONE        // 工作完成(计时结束)}/// /// 游戏计时器类,用于管理计时逻辑,支持启动计时、更新计时、获取状态、重置等功能/// public class GameTimer{    // 计时时长(剩余计时时间)    private float _startTime;    // 计时结束后要执行的任务(Action 委托)    private Action _task;    // 是否停止当前计时器标记    private bool _isStopTimer;    // 当前计时器的状态    private TimerState _timerState;    ///     /// 构造函数,初始化时重置计时器    ///     public GameTimer()    {        ResetTimer();    }    ///     /// 1. 开始计时    ///     /// 要计时的时长    /// 计时结束后要执行的任务(Action 委托)    public void StartTimer(float time, Action task)    {        _startTime = time;        _task = task;        _isStopTimer = false;        _timerState = TimerState.WORKERING;    }    ///     /// 2. 更新计时器(通常在 MonoBehaviour 的 Update 里调用,驱动计时逻辑)    ///     public void UpdateTimer()    {        // 如果标记为停止,直接返回,不执行计时更新        if (_isStopTimer)            return;        // 递减计时时间        _startTime -= Time.deltaTime;        // 计时时间小于 0,说明计时结束        if (_startTime < 0)        {            // 安全调用任务(如果任务不为 null 才执行)            _task?.Invoke();            // 更新状态为已完成            _timerState = TimerState.DONE;            // 标记为停止,后续不再继续计时更新            _isStopTimer = true;        }    }    ///     /// 3. 获取当前计时器的状态    ///     /// 返回 TimerState 枚举值,代表当前计时器状态    public TimerState GetTimerState() => _timerState;    ///     /// 4. 重置计时器,恢复到初始状态    ///     public void ResetTimer()    {        _startTime = 0f;        _task = null;        _isStopTimer = true;        _timerState = TimerState.NOTWORKERE;    }}
复制代码
TimerManager
  1. using System;using System.Collections;using System.Collections.Generic;using GGG.Tool;using GGG.Tool.Singleton;using UnityEngine;using UnityEngine.UIElements;/// /// 计时器管理器,采用单例模式,负责管理空闲计时器队列和工作中计时器列表,/// 实现计时器的初始化、分配、回收及更新逻辑/// public class TimerManager : Singleton{    #region 私有字段    // 初始最大计时器数量,在 Inspector 中配置    [SerializeField] private int _initMaxTimerCount;    // 空闲计时器队列,存储可用的 GameTimer    private Queue _notWorkingTimer = new Queue();    // 工作中计时器列表,存储正在计时的 GameTimer    private List _workingTimer = new List();    #endregion    #region 生命周期与初始化    protected override void Awake()    {        base.Awake();        InitTimerManager();    }    ///     /// 初始化计时器管理器,创建初始数量的空闲计时器    ///     private void InitTimerManager()    {        for (int i = 0; i < _initMaxTimerCount; i++)        {            CreateTimerInternal();        }    }    ///     /// 内部创建计时器并加入空闲队列的方法    ///     private void CreateTimerInternal()    {        var timer = new GameTimer();        _notWorkingTimer.Enqueue(timer);    }    #endregion    #region 计时器分配与回收    ///     /// 尝试获取一个计时器,用于执行定时任务    ///     /// 计时时长    /// 计时结束后执行的任务    public void TryGetOneTimer(float time, Action task)    {        // 若空闲队列为空,额外创建一个计时器        if (_notWorkingTimer.Count == 0)        {            CreateTimerInternal();        }        var timer = _notWorkingTimer.Dequeue();        timer.StartTimer(time, task);        _workingTimer.Add(timer);    }    ///     /// 回收计时器(可在 GameTimer 完成任务时调用,这里逻辑已内联在更新里,也可扩展外部调用)    /// 注:当前通过 UpdateWorkingTimer 自动回收,此方法可留作扩展    ///     /// 要回收的计时器    private void RecycleTimer(GameTimer timer)    {        timer.ResetTimer();        _notWorkingTimer.Enqueue(timer);        _workingTimer.Remove(timer);    }    #endregion    #region 计时器更新逻辑    private void Update()    {        UpdateWorkingTimer();    }    ///     /// 更新工作中计时器的状态,处理计时推进和完成后的回收    ///     private void UpdateWorkingTimer()    {        // 遍历副本,避免列表修改时迭代出错        for (int i = _workingTimer.Count - 1; i >= 0; i--)        {            var timer = _workingTimer[i];            timer.UpdateTimer();            if (timer.GetTimerState() == TimerState.DONE)            {                RecycleTimer(timer);            }        }    }    #endregion}
复制代码
Day07 脚部拖尾特效的控制——奔跑时启用
  1. using UnityEngine;using System.Collections;public class ObjectVisibilityController : MonoBehaviour{    // 在 Inspector 中手动拖入需要控制的子物体    public GameObject targetChild;    public Animator playerAnimator;    // 存储当前目标状态,用于判断是否需要执行状态切换    private bool _currentTargetState;    // 标记是否正在等待延迟,避免重复启动协程    private bool _isWaiting = false;    private void Update()    {        // 获取动画状态的当前值        bool desiredState = playerAnimator.GetBool(AnimationID.RunID);        // 如果状态发生变化且不在等待状态,则启动延迟协程        if (desiredState != _currentTargetState && !_isWaiting)        {            StartCoroutine(ChangeStateAfterDelay(desiredState, 0.5f));        }    }    // 延迟改变状态的协程    private IEnumerator ChangeStateAfterDelay(bool newState, float delay)    {        _isWaiting = true; // 标记为正在等待        yield return new WaitForSeconds(delay); // 等待指定秒数        // 应用新状态        targetChild.SetActive(newState);        _currentTargetState = newState;        _isWaiting = false; // 重置等待标记    }}
复制代码

Day08 IKController——头部IK跟随相机(平滑控制)

IKController
  1. using System.Collections;using System.Collections.Generic;using UnityEngine;public class IKController : MonoBehaviour{    public Animator _animator;    //IK控制点    //四肢关节点    public Transform ik_LHand;    public Transform ik_RHand;    public Transform ik_LFoot;    public Transform ik_RFoot;    //头部控制点,可以根据主相机的位置,让玩家能够从侧视角下看到头部偏转。    public Transform Head_IKPoint;    [Header("IK 权重控制")]    [SerializeField] private float ikBlendSpeed = 5f; // IK权重变化速度    [SerializeField] private float headTurnSpeed = 5f; // 头部转向速度      [SerializeField] private float maxHeadAngle = 60f; // 头部最大转向角度    // IK相关私有变量    private float _currentHeadIKWeight = 0f; // 当前头部IK权重    private Vector3 _currentLookTarget; // 缓存"当前正在看的点"    private bool _hasInitializedLookTarget = false; // 是否已初始化看向目标    private void OnAnimatorIK(int layerIndex)    {        // 四肢IK控制        if (ik_LHand != null)            IKControl(AvatarIKGoal.LeftHand, ik_LHand);        if (ik_RHand != null)            IKControl(AvatarIKGoal.RightHand, ik_RHand);        if (ik_LFoot != null)            IKControl(AvatarIKGoal.LeftFoot, ik_LFoot);        if (ik_RFoot != null)            IKControl(AvatarIKGoal.RightFoot, ik_RFoot);        // 头部IK控制 - 使用平滑权重过渡        HandleHeadIK();    }    ///     /// 处理头部IK控制 - 解决生硬切换问题    ///     private void HandleHeadIK()    {        if (Head_IKPoint == null)            return;        // 判断是否应该启用头部IK        bool shouldUseHeadIK = _animator.GetFloat(AnimationID.MovementID) < 0.1f;        // 计算目标权重        float targetWeight = shouldUseHeadIK ? 1f : 0f;        // 平滑过渡权重 - 这是解决生硬切换的关键        _currentHeadIKWeight = Mathf.Lerp(_currentHeadIKWeight, targetWeight, ikBlendSpeed * Time.deltaTime);        // 如果权重大于0,执行头部IK控制        if (_currentHeadIKWeight > 0.01f)        {            IKHeadControl(Head_IKPoint, headTurnSpeed, maxHeadAngle);        }        // 使用平滑权重而不是固定的1f        _animator.SetLookAtWeight(_currentHeadIKWeight);        // 如果已初始化目标位置,设置看向位置        if (_hasInitializedLookTarget)        {            _animator.SetLookAtPosition(_currentLookTarget);        }    }    ///     /// 头部 IK 控制(平滑转向 + 角度限制)    ///     /// 要看的对象    /// 插值速度    /// 最大允许夹角(度数)    private void IKHeadControl(Transform target,                               float turnSpeed = 5f,                               float maxAngle = 60f)    {        // 初始化看向目标 - 防止第一次启用时的突然跳转        if (!_hasInitializedLookTarget)        {            _currentLookTarget = transform.position + transform.forward * 5f;            _hasInitializedLookTarget = true;        }        // 1. 计算最终想要看的点        Vector3 rawTargetPos;        Vector3 directionToCamera = target.position - transform.position;        bool isCameraInFront = Vector3.Dot(transform.forward, directionToCamera.normalized) > 0;        if (isCameraInFront)        {            // 相机在前面,看向相机            rawTargetPos = target.position;        }        else        {            // 相机在背后,看向相机视线向前延伸的点            rawTargetPos = target.position + target.forward * 10f;        }        // 2. 计算与正前方向的夹角        Vector3 dirToRawTarget = (rawTargetPos - transform.position).normalized;        float angle = Vector3.Angle(transform.forward, dirToRawTarget);        // 3. 如果角度在范围内,才允许平滑转向        if (angle  float.Epsilon || Mathf.Abs(x - z) > float.Epsilon || Mathf.Abs(y - z) > float.Epsilon) {            Debug.LogWarning("The xyz scales of the Spider are not equal. Please make sure they are. The scale of the spider is defaulted to be the Y scale and a lot of values depend on this scale.");        }        rb = GetComponent();        //Initialize the two Sphere Casts        downRayRadius = downRaySize * getColliderRadius();        float forwardRayRadius = forwardRaySize * getColliderRadius();        downRay = new SphereCast(transform.position, -transform.up, downRayLength * getColliderLength(), downRayRadius, transform, transform);        forwardRay = new SphereCast(transform.position, transform.forward, forwardRayLength * getColliderLength(), forwardRayRadius, transform, transform);        //Initialize the bodyupLocal as the spiders transform.up parented to the body. Initialize the breathePivot as the body position parented to the spider        bodyY = body.transform.InverseTransformDirection(transform.up);        bodyZ = body.transform.InverseTransformDirection(transform.forward);        bodyCentroid = body.transform.position + getScale() * bodyOffsetHeight * transform.up;        bodyDefaultCentroid = transform.InverseTransformPoint(bodyCentroid);    }    void FixedUpdate() {        //** Ground Check **//        grdInfo = GroundCheck();        //** Rotation to normal **//         float normalAdjustSpeed = (grdInfo.rayType == RayType.ForwardRay) ? forwardNormalAdjustSpeed : groundNormalAdjustSpeed;        Vector3 slerpNormal = Vector3.Slerp(transform.up, grdInfo.groundNormal, 0.02f * normalAdjustSpeed);        Quaternion goalrotation = getLookRotation(Vector3.ProjectOnPlane(transform.right, slerpNormal), slerpNormal);        // Save last Normal for access        lastNormal = transform.up;        //Apply the rotation to the spider        if (Quaternion.Angle(transform.rotation,goalrotation)>Mathf.Epsilon) transform.rotation = goalrotation;        // Dont apply gravity if close enough to ground        if (grdInfo.distanceToGround > getGravityOffDistance()) {            rb.AddForce(-grdInfo.groundNormal * gravityMultiplier * 0.0981f * getScale()); //Important using the groundnormal and not the lerping normal here!        }    }    void Update() {        //** Debug **//        if (showDebug) drawDebug();        Vector3 Y = body.TransformDirection(bodyY);        //Doesnt work the way i want it too! On sphere i go underground. I jiggle around when i go down my centroid moves down to.(Depends on errortolerance of IKSolver)        if (legCentroidAdjustment) bodyCentroid = Vector3.Lerp(bodyCentroid, getLegsCentroid(), Time.deltaTime * legCentroidSpeed);        else bodyCentroid = getDefaultCentroid();        body.transform.position = bodyCentroid;        if (legNormalAdjustment) {            Vector3 newNormal = GetLegsPlaneNormal();            //Use Global X for  pitch            Vector3 X = transform.right;            float angleX = Vector3.SignedAngle(Vector3.ProjectOnPlane(Y, X), Vector3.ProjectOnPlane(newNormal, X), X);            angleX = Mathf.LerpAngle(0, angleX, Time.deltaTime * legNormalSpeed);            body.transform.rotation = Quaternion.AngleAxis(angleX, X) * body.transform.rotation;            //Use Local Z for roll. With the above global X for pitch, this avoids any kind of yaw happening.            Vector3 Z = body.TransformDirection(bodyZ);            float angleZ = Vector3.SignedAngle(Y, Vector3.ProjectOnPlane(newNormal, Z), Z);            angleZ = Mathf.LerpAngle(0, angleZ, Time.deltaTime * legNormalSpeed);            body.transform.rotation = Quaternion.AngleAxis(angleZ, Z) * body.transform.rotation;        }        if (breathing) {            float t = (Time.time * 2 * Mathf.PI / breathePeriod) % (2 * Mathf.PI);            float amplitude = breatheMagnitude * getColliderRadius();            Vector3 direction = body.TransformDirection(bodyY);            body.transform.position = bodyCentroid + amplitude * (Mathf.Sin(t) + 1f) * direction;        }        // Update the moving status        if (transform.hasChanged) {            isMoving = true;            transform.hasChanged = false;        }        else isMoving = false;    }    //** Movement methods**//    private void move(Vector3 direction, float speed) {        // TODO: Make sure direction is on the XZ plane of spider! For this maybe refactor the logic from input from spidercontroller to this function.        //Only allow direction vector to have a length of 1 or lower        float magnitude = direction.magnitude;        if (magnitude > 1) {            direction = direction.normalized;            magnitude = 1f;        }        // Scale the magnitude and Clamp to not move more than down ray radius (Makes sure the ground is not lost due to moving too fast)        if (direction != Vector3.zero) {            float directionDamp = Mathf.Pow(Mathf.Clamp(Vector3.Dot(direction / magnitude, transform.forward), 0, 1), 2);            float distance = 0.0004f * speed * magnitude * directionDamp * getScale();            distance = Mathf.Clamp(distance, 0, 0.99f * downRayRadius);            direction = distance * (direction / magnitude);        }        //Slerp from old to new velocity using the acceleration        currentVelocity = Vector3.Slerp(currentVelocity, direction, 1f - walkDrag);        //Apply the resulting velocity        transform.position += currentVelocity;    }    public void turn(Vector3 goalForward) {        //Make sure goalForward is orthogonal to transform up        goalForward = Vector3.ProjectOnPlane(goalForward, transform.up).normalized;        if (goalForward == Vector3.zero || Vector3.Angle(goalForward, transform.forward) < Mathf.Epsilon) {            return;        }        goalForward = Vector3.ProjectOnPlane(goalForward, transform.up);        transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation(goalForward, transform.up), turnSpeed);    }    //** Movement methods for public access**//    // It is advised to call these on a fixed update basis.    public void walk(Vector3 direction) {        if (direction.magnitude < Mathf.Epsilon) return;        move(direction, walkSpeed);    }    public void run(Vector3 direction) {        if (direction.magnitude < Mathf.Epsilon) return;        move(direction, runSpeed);    }    //** Ground Check Method **//    private groundInfo GroundCheck() {        if (groundCheckOn) {            if (forwardRay.castRay(out hitInfo, walkableLayer)) {                return new groundInfo(true, hitInfo.normal.normalized, Vector3.Distance(transform.TransformPoint(capsuleCollider.center), hitInfo.point) - getColliderRadius(), RayType.ForwardRay);            }            if (downRay.castRay(out hitInfo, walkableLayer)) {                return new groundInfo(true, hitInfo.normal.normalized, Vector3.Distance(transform.TransformPoint(capsuleCollider.center), hitInfo.point) - getColliderRadius(), RayType.DownRay);            }        }        return new groundInfo(false, Vector3.up, float.PositiveInfinity, RayType.None);    }    //** Helper methods**//    /*    * Returns the rotation with specified right and up direction       * May have to make more error catches here. Whatif not orthogonal?    */    private Quaternion getLookRotation(Vector3 right, Vector3 up) {        if (up == Vector3.zero || right == Vector3.zero) return Quaternion.identity;        // If vectors are parallel return identity        float angle = Vector3.Angle(right, up);        if (angle == 0 || angle == 180) return Quaternion.identity;        Vector3 forward = Vector3.Cross(right, up);        return Quaternion.LookRotation(forward, up);    }    //** Torso adjust methods for more realistic movement **//    // Calculate the centroid (center of gravity) given by all end effector positions of the legs    private Vector3 getLegsCentroid() {        if (legs == null || legs.Length == 0) {            Debug.LogError("Cant calculate leg centroid, legs not assigned.");            return body.transform.position;        }        Vector3 defaultCentroid = getDefaultCentroid();        // Calculate the centroid of legs position        Vector3 newCentroid = Vector3.zero;        float k = 0;        for (int i = 0; i < legs.Length; i++) {            newCentroid += legs[i].getEndEffector().position;            k++;        }        newCentroid = newCentroid / k;        // Offset the calculated centroid        Vector3 offset = Vector3.Project(defaultCentroid - getColliderBottomPoint(), transform.up);        newCentroid += offset;        // Calculate the normal and tangential translation needed        Vector3 normalPart = Vector3.Project(newCentroid - defaultCentroid, transform.up);        Vector3 tangentPart = Vector3.ProjectOnPlane(newCentroid - defaultCentroid, transform.up);        return defaultCentroid + Vector3.Lerp(Vector3.zero, normalPart, legCentroidNormalWeight) + Vector3.Lerp(Vector3.zero, tangentPart, legCentroidTangentWeight);    }    // Calculate the normal of the plane defined by leg positions, so we know how to rotate the body    private Vector3 GetLegsPlaneNormal() {        if (legs == null) {            Debug.LogError("Cant calculate normal, legs not assigned.");            return transform.up;        }        if (legNormalWeight  2f)                return false;            // 3. 在敌人前方/在敌人背后但角度差超过 60° 时不允许(和敌人同向的时候,是很小的锐角)            float angle = Vector3.Angle(transform.forward, _currentEnemy.forward);            if (angle > 60f)                return false;            // 4. 正在播放暗杀动画时不允许(避免重复触发)            if (_animator.AnimationAtTag("Assassin"))                return false;            return true;        }        ///         /// 处理角色暗杀输入的响应逻辑        ///         private void CharacterAssassinInput()        {            // 不满足暗杀条件时直接返回            if (!CanAssassin())                return;            // 检测到 "取出武器/触发暗杀" 输入时执行逻辑            if (GameInputManager.Instance.FinishAttack)            {                // 1. 随机选取暗杀连招索引                _currentComboIndex = Random.Range(                    0,                    _assassinCombo.TryGetComboMaxCount()                );                // 2. 播放对应的暗杀动画                string animationState = _assassinCombo.TryGetOneComboAction(_currentComboIndex);                _animator.Play(animationState, 0, 0f);                // 3. 获取暗杀动画的命中名称,用于事件传递                string hitName = _assassinCombo.TryGetOneHitName(_currentComboIndex, 0);                // 4. 调用事件中心,触发敌人的处决/暗杀事件                GameEventManager.MainInstance.CallEvent(                    "触发处决",                    hitName,                    transform,                    _currentEnemy                );                // 5. 重置连招状态,防止索引越界                ResetComboInfo();            }        }        #endregion
复制代码
修改之前的正面处决条件,加上角度限制
  1.         ///         /// 是否允许执行处决攻击        ///         private bool CanSpecialAttack()        {            // 处于 "Finish" 标签动画时,不允许            if (_animator.AnimationAtTag("Finish"))                return false;            // 没有当前敌人时,不允许            if (_currentEnemy == null)                return false;            // 当前连招总数小于2时,不允许            if(_currentComboCount < 2)                return false;            // 在敌人后方或侧面时,不允许(和敌人同向的时候,是很小的锐角,慢慢放大这个角就是侧面,所以小于某个超过90度的角即可,这里取120度)            float angle = Vector3.Angle(transform.forward, _currentEnemy.forward);            if (angle < 120f)                return false;            return true;        }
复制代码
Bug修复——让处决的Combo索引单独控制
  1.         // 处决执行状态变量        private int _finishComboIndex;  // 处决连招动作索引
复制代码
  1.         ///         /// 处理角色暗杀输入的响应逻辑        ///         private void CharacterAssassinInput()        {            // 不满足暗杀条件时直接返回            if (!CanAssassin())                return;            // 检测到 "取出武器/触发暗杀" 输入时执行逻辑            if (GameInputManager.Instance.FinishAttack)            {                // 1. 随机选取暗杀连招索引                _finishComboIndex = Random.Range(                    0,                    _assassinCombo.TryGetComboMaxCount()                );                // 2. 播放对应的暗杀动画                string animationState = _assassinCombo.TryGetOneComboAction(_finishComboIndex);                _animator.Play(animationState, 0, 0f);                // 3. 获取暗杀动画的命中名称,用于事件传递                string hitName = _assassinCombo.TryGetOneHitName(_finishComboIndex, 0);                // 4. 调用事件中心,触发敌人的处决/暗杀事件                GameEventManager.MainInstance.CallEvent(                    "触发处决",                    hitName,                    transform,                    _currentEnemy                );                // 5. 重置连招状态,防止索引越界                ResetComboInfo();            }        }
复制代码
region 触发伤害(普通攻击和处决攻击)
  1.     private void TriggerDamage()
复制代码
  1.             else            {               // 同一处决动画期间会触发多次伤害                //处决攻击                // 从处决数据中获取连招伤害相关参数                float damageValue = _finishCombo.TryGetComboDamage(_finishComboIndex);                // 调用触发处决伤害事件                GameEventManager.MainInstance.CallEvent("触发处决伤害", damageValue,_currentEnemy);                Debug.Log("触发处决伤害");            }            #endregion
复制代码
  1.         #region 位置同步        ///         /// 处决期间玩家位置同步        ///         private void MatchPosition()        {            if (_currentEnemy == null)                return;            if (!_animator)                return;            if (_animator.AnimationAtTag("Finish"))//当前在处决动画            {                transform.rotation = Quaternion.LookRotation(-_currentEnemy.forward);   // 面对敌人                RunningMatch(_finishCombo,_finishComboIndex);            }            else if (_animator.AnimationAtTag("Assassin"))//当前在普通攻击动画            {                transform.rotation = Quaternion.LookRotation(_currentEnemy.forward);    // 背对敌人                RunningMatch(_assassinCombo, _finishComboIndex);            }        }        private void RunningMatch(CharacterComboSO combo,int comboIndex, float startTime = 0f, float endTime = 0.01f)        {            if (!_animator.isMatchingTarget && !_animator.IsInTransition(0))//当前不在匹配,同时不处于过渡状态            {                _animator.MatchTarget(                    _currentEnemy.position + (-transform.forward * combo.TryGetComboPositionOffset(comboIndex)),                    Quaternion.identity,                    AvatarTarget.Body,                    new MatchTargetWeightMask(Vector3.one, 0f),                    startTime,                    endTime                );            }        }        #endregion
复制代码
  1.         ///         /// 重置连招状态(索引、冷却时间)        ///         private void ResetComboInfo()        {            _currentComboIndex = 0;            _maxColdTime = 0f;            _hitIndex = 0;            _finishComboIndex = 0;        }
复制代码
  1.         ///         /// 处理角色处决攻击的输入响应逻辑        ///         private void CharacterFinishAttackInput()        {            // 不满足处决攻击条件时,直接返回            if (!CanSpecialAttack())                return;            // 检测到处决输入时,执行处决流程            if (GameInputManager.Instance.FinishAttack)            {                // 1. 随机选取处决连招索引                _finishComboIndex = Random.Range(0, _finishCombo.TryGetComboMaxCount());                // 2. 播放对应的处决动画                string finishAnim = _finishCombo.TryGetOneComboAction(_finishComboIndex);                _animator.Play(finishAnim);                // 3. 调用事件中心,触发敌人的处决事件                string hitName = _finishCombo.TryGetOneHitName(_finishComboIndex, 0);                GameEventManager.MainInstance.CallEvent(                    "触发处决",                    hitName,                    transform,                    _currentEnemy                );                // 4. 调用定时器事件:更新连招状态信息,防止索引越界                TimerManager.Instance.TryGetOneTimer(                    _finishCombo.TryGetColdTime(_finishComboIndex),    //这里原先写的是固定的0.5f                    UpdateComboInfo);                // 5. 重置连招状态,防止索引越界                ResetComboInfo();            }        }
复制代码
敌人AI

这个还没看明白,时间不太够,后面做
Foot IK

方案:实际走的是斜面(不渲染),footik识别的是阶梯本身(渲染)
优点:相机平滑,角色走路平滑,只对脚部进行ik



随机待机动画系统
  1. using UnityEngine;[RequireComponent(typeof(Animator))]public class RandomIdleAnimation : MonoBehaviour{    [SerializeField] private int IdleNum = 9;    private Animator animator;    private float idleTimeCounter = 0f;    private bool isInIdleState = false;    private const float idleThreshold = 5f;    private const string idleStateName = "Idle";    private const string blendTreeParameter = "IdleType";    void Start()    {        animator = GetComponent();    }    void Update()    {        // 检查当前是否处于Idle状态        bool isCurrentStateIdle = IsInIdleState();        if (isCurrentStateIdle)        {            idleTimeCounter += Time.deltaTime;            isInIdleState = true;            if (idleTimeCounter >= idleThreshold)            {                RandomizeIdleAnimation();                idleTimeCounter = 0f;            }        }        else        {            // 离开Idle状态时重置            idleTimeCounter = 0f;            isInIdleState = false;        }    }    // 检查是否处于Idle状态    private bool IsInIdleState()    {        if (animator.layerCount == 0)            return false;        AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);        // 使用IsName方法检查状态名称,这比哈希值比较更可靠        return stateInfo.IsName(idleStateName) && !animator.IsInTransition(0);    }    private void RandomizeIdleAnimation()    {        int randomInt = Random.Range(0, IdleNum);        animator.SetFloat(blendTreeParameter, randomInt);    }}
复制代码
演示效果(倍速处理)

开启物理碰撞交互
  1. using System.Collections;using System.Collections.Generic;using UnityEngine;public class ColliderInteraction : MonoBehaviour{    private void OnControllerColliderHit(ControllerColliderHit hit)    {        if(hit.transform.TryGetComponent(out Rigidbody rigidbody))        {            // 只要接触到的碰撞体是rigidbody,就给它施加一个向前的力            rigidbody.AddForce(transform.forward * 20f,ForceMode.Force);        }    }}
复制代码

加上手部扶墙的程序动画刚好完美推门(不过要记得的删去非法角度的判断)
注意,开启物理碰撞之后,之前的扶墙物体需要把Kinematic打开,让他固定住不受外力,不然墙倒了

不过推门还是尽量做单独的animator动画比较好,用扶墙的程序动画看着还行,但还是用固定的动画不容易出戏


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册