辜酗徇 发表于 2025-8-18 17:24:44

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

新版输入系统——斜向移动变快问题解决


生成对应的input管理脚本

Day 01——角色移动基类

CharacterMovementControlBase
using UnityEngine;

namespace Spiderman.Movement
{
   
    public abstract class CharacterMovementControlBase : MonoBehaviour
    {
      // 角色控制器组件,用于处理角色移动相关的物理交互
      private CharacterController _controller;
      // 动画组件,用于控制角色动画播放
      private Animator _animator;

      // 地面检测相关变量
      protected bool _characterIsOnGround;
      
      protected float _groundDetectionPositionOffset; // 地面检测位置偏移量
      protected float _detectionRang;               // 地面检测范围
      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<CharacterController>();
            _animator = GetComponent();
      }

      protected virtual void Start()
      {
            _fallOutDeltaTime = _fallOutTime;
      }

      private void Update()
      {
            SetCharacterGravity();
            UpdateCharacterGravity();
      }

      /// <summary>
      /// 地面检测方法
      /// </summary>
      /// <returns>返回角色是否在地面的布尔值</returns>
      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
            );
      }

      /// <summary>
      /// 根据是否在地面设置对应的角色重力逻辑
      /// </summary>
      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;
                  // 重力公式累积垂直速度
                }
            }
      }

      /// <summary>
      /// 更新角色垂直方向移动(应用重力效果)
      /// </summary>
      private void UpdateCharacterGravity()
      {
            //这里只处理 y 轴重力
            // x/z 由其他移动逻辑控制
            Vector3 _characterVerticalDirection = new Vector3(0, _characterVerticalVelocity, 0);

            // 通过 CharacterController 应用y轴移动
            _controller.Move(_characterVerticalDirection * Time.deltaTime);
      }

      /// <summary>
      /// 斜坡方向重置:检测角色是否在坡上移动,防止下坡速度过快导致异常
      /// </summary>
      /// <param name="moveDirection">原始移动方向</param>
      /// <returns>适配斜坡后的移动方向</returns>
      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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Spiderman.Movement
{
    public class PlayerMovementControl : CharacterMovementControlBase
    {

    }
}Day02 带碰撞体相机脚本

GameInputManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameInputManager : MonoBehaviour
{
    private GameInputAction _gameInputAction;

    public Vector2 Movement => _gameInputAction.Player.Movement.ReadValue<Vector2>();
    public Vector2 CameraLook => _gameInputAction.Player.CameraLook.ReadValue<Vector2>();


    private void Awake()
    {
      _gameInputAction ??= new GameInputAction(); //是空的,则创建新的实例
    }

    private void OnEnable()
    {
      _gameInputAction.Enable();
    }
    private void OnDisable()
    {
      _gameInputAction.Disable();
    }
}


TP_CameraControl
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TP_CameraControl : MonoBehaviour
{
    private GameInputManager _gameInputManager;

   
    private Transform _lookTarget;             //相机跟随目标
    private float _controlSpeed;               //相机移动速度
    private Vector2 _cameraVerticalMaxAngle;   //相机上下旋转角度限制
    private Vector2 _cameraHorizontalMaxAngle; //相机左右旋转角度限制
    private float _smoothSpeed;                //平滑速度
    private float _cameraDistance;             //相机到跟随目标的距离
    private float _cameraHeight;               //相机高度
    private float _DistancemoothTime;         //位置跟随平滑时间

    private Vector3 smoothDampVelocity = Vector3.zero;          //旋转阻尼

    private Vector2 _input;                                     // 输入值
    private Vector3 _cameraRotation;                            // 相机旋转方向
    private bool _cameraInputEnabled = true;                  // 相机输入是否启用

    private void Awake()
    {
      // 获取游戏输入管理组件
      _gameInputManager = GetComponent<GameInputManager>();
      //隐藏光标
      Cursor.lockState = CursorLockMode.Locked;
      Cursor.visible = false;
    }

    private void Update()
    {
      // 检测到按下ESC键或鼠标左键点击窗口,则切换相机输入状态
      HandleCameraInputToggle();

      // 只有在相机输入启用时才处理输入
      if (_cameraInputEnabled)
      {
            // 实时处理相机输入
            CameraInput();
      }
    }


    private void LateUpdate()
    {
      // 更新相机旋转
      UpdateCameraRotation();
      // 更新相机位置
      UpdateCameraPosition();
    }

    /// <summary>
    /// 处理相机输入,获取并处理上下查看等输入,限制垂直角度范围
    /// </summary>
    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
      );

    }

    /// <summary>
    /// 更新相机旋转
    /// </summary>
    private void UpdateCameraRotation()
    {
      var targetRotation = new Vector3(_input.x, _input.y, 0);
      _cameraRotation = Vector3.SmoothDamp(
            _cameraRotation,
            targetRotation,
            ref smoothDampVelocity,
            _smoothSpeed
      );

      //更新相机欧拉角
      transform.eulerAngles = _cameraRotation;

    }

    /// <summary>
    /// 更新相机位置
    /// </summary>
    private void UpdateCameraPosition()
    {
      var newPos = _lookTarget.position
            + Vector3.back * _cameraDistance
            + Vector3.up * _cameraHeight;
      // 平滑位置移动
      transform.position = Vector3.Lerp(
            transform.position,
            newPos,
            _DistancemoothTime
      );
    }

    /// <summary>
    /// 处理相机输入状态切换
    /// </summary>
    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继承于单例模式
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using GGG.Tool.Singleton;

public class GameInputManager : Singleton<GameInputManager>
{
    private GameInputAction _gameInputAction;

    public Vector2 Movement => _gameInputAction.Player.Movement.ReadValue<Vector2>();
    public Vector2 CameraLook => _gameInputAction.Player.CameraLook.ReadValue<Vector2>();


    private void Awake()
    {
      base.Awake();
      _gameInputAction ??= new GameInputAction(); //是空的,则创建新的实例
    }

    private void OnEnable()
    {
      _gameInputAction.Enable();
    }
    private void OnDisable()
    {
      _gameInputAction.Disable();
    }
}TP_CameraControl
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using GGG.Tool;

public class TP_CameraControl : MonoBehaviour
{

   
    private Transform _lookTarget;             //相机跟随目标
    private float _controlSpeed;               //相机移动速度
    private Vector2 _cameraVerticalMaxAngle;   //相机上下旋转角度限制
    private Vector2 _cameraHorizontalMaxAngle; //相机左右旋转角度限制
    private float _smoothSpeed;                //平滑速度
    private float _cameraDistance;             //相机到跟随目标的距离
    private float _cameraHeight;               //相机高度
    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();
    }

    /// <summary>
    /// 处理相机输入,获取并处理上下查看等输入,限制垂直角度范围
    /// </summary>
    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
      );

    }

    /// <summary>
    /// 更新相机旋转
    /// </summary>
    private void UpdateCameraRotation()
    {
      var targetRotation = new Vector3(_input.x, _input.y, 0);
      _cameraRotation = Vector3.SmoothDamp(
            _cameraRotation,
            targetRotation,
            ref smoothDampVelocity,
            _smoothSpeed
      );

      //更新相机欧拉角
      transform.eulerAngles = _cameraRotation;

    }

    /// <summary>
    /// 更新相机位置
    /// </summary>
    private void UpdateCameraPosition()
    {
      var newPos = _lookTarget.position
            + Vector3.back * _cameraDistance
            + Vector3.up * _cameraHeight;
      // 平滑位置移动
      transform.position = Vector3.Lerp(
            transform.position,
            newPos,
            DevelopmentToos.UnTetheredLerp(_distanceSmoothTime)
      );
    }

    /// <summary>
    /// 处理相机输入状态切换
    /// </summary>
    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
      protected Vector3 _moveDirection; // 角色移动方向      /// <summary>
      /// 脚本控制animator的根运动
      /// </summary>
      protected virtual void OnAnimatorMove()
      {
            _animator.ApplyBuiltinRootMotion();
            UpdateCharacterMoveDirection(_animator.deltaPosition);
      }      /// <summary>
      /// 更新角色水平移动方向——绕y轴旋转
      /// </summary>
      protected void UpdateCharacterMoveDirection(Vector3 direction)
      {
            _moveDirection = SlopResetDirection(direction);
            _controller.Move(_moveDirection * Time.deltaTime);
      }GameInputManager
    public bool Run => _gameInputAction.Player.Run.triggered;PlayerMovementControl
using GGG.Tool;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Spiderman.Movement
{
    public class PlayerMovementControl : CharacterMovementControlBase
    {
       float moveSpeed = 1.5f;
      // 角色旋转角度(绕 Y 轴)
      private float _rotationAngle;
      // 旋转角速度
      private float _angleVelocity = 0;
      // 旋转平滑时间
       private float _rotationSmoothTime;

      private Transform _mainCamera;

      protected override void Awake()
      {
            base.Awake();
            _mainCamera = Camera.main.transform;
      }

      private void LateUpdate()
      {
            UpdateAnimation();
            CharacterRotationControl();
      }

      /// <summary>
      /// 角色旋转控制
      /// </summary>
      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
                                        );
            }
      }

      /// <summary>
      /// 更新动画
      /// </summary>
      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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using GGG.Tool;
using GGG.Tool.Singleton;

public class GameEventManager : SingletonNonMono<GameEventManager>
{
    // 事件接口
    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<T> : IEventHelp
    {
      // 存储事件委托
      private event Action<T> _action;

      // 构造函数,初始化时绑定初始事件逻辑
      public EventHelp(Action<T> action)
      {
            // 首次实例化时赋值,仅执行这一次初始绑定
            _action = action;
      }

      // 增加事件注册的方法,将新的事件逻辑追加到委托中
      public void AddCall(Action<T> action)
      {
            _action += action;
      }

      // 调用事件的方法,若有绑定逻辑则执行
      public void Call(T value)
      {
            _action?.Invoke(value);
      }

      // 移除事件的方法,将指定事件逻辑从委托中移除
      public void Remove(Action<T> action)
      {
            _action -= action;
      }
    }

    private class EventHelp<T1, T2> : IEventHelp
    {
      // 存储事件委托
      private event Action<T1, T2> _action;

      // 构造函数,初始化时绑定初始事件逻辑
      public EventHelp(Action<T1, T2> action)
      {
            // 首次实例化时赋值,仅执行这一次初始绑定
            _action = action;
      }

      // 增加事件注册的方法,将新的事件逻辑追加到委托中
      public void AddCall(Action<T1, T2> action)
      {
            _action += action;
      }

      // 调用事件的方法,若有绑定逻辑则执行
      public void Call(T1 value1, T2 value2)
      {
            _action?.Invoke(value1, value2);
      }

      // 移除事件的方法,将指定事件逻辑从委托中移除
      public void Remove(Action<T1, T2> action)
      {
            _action -= action;
      }
    }

    /// <summary>
    /// 事件中心,用于管理事件注册、调用
    /// </summary>
    private Dictionary<string, IEventHelp> _eventCenter = new Dictionary<string, IEventHelp>();

    /// <summary>
    /// 添加事件监听
    /// </summary>
    /// <param name="eventName">事件名称</param>
    /// <param name="action">回调函数</param>
    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<T>(string eventName, Action<T> action)
    {
      if (_eventCenter.TryGetValue(eventName, out var eventHelp))
      {
            (eventHelp as EventHelp<T>)?.AddCall(action);
      }
      else
      {
            // 如果事件中心不存在叫这个名字的事件,new一个然后添加
            _eventCenter.Add(eventName, new EventHelp<T>(action));
      }
    }
    public void AddEventListening<T1, T2>(string eventName, Action<T1, T2> action)
    {
      if (_eventCenter.TryGetValue(eventName, out var eventHelp))
      {
            (eventHelp as EventHelp<T1, T2>)?.AddCall(action);
      }
      else
      {
            // 如果事件中心不存在叫这个名字的事件,new一个然后添加
            _eventCenter.Add(eventName, new EventHelp<T1, T2>(action));
      }
    }

    /// <summary>
    /// 调用事件
    /// </summary>
    /// <param name="eventName">事件名称</param>
    public void CallEvent(string eventName)
    {
      if (_eventCenter.TryGetValue(eventName, out var eventHelp))
      {
            (eventHelp as EventHelp)?.Call();
      }
      else
      {
            LogEventNotFound(eventName, "调用");
      }
    }

    public void CallEvent<T>(string eventName, T value)
    {
      if (_eventCenter.TryGetValue(eventName, out var eventHelp))
      {
            (eventHelp as EventHelp<T>)?.Call(value);
      }
      else
      {
            LogEventNotFound(eventName, "调用");
      }
    }

    public void CallEvent<T1, T2>(string eventName, T1 value, T2 value1)
    {
      if (_eventCenter.TryGetValue(eventName, out var eventHelp))
      {
            (eventHelp as EventHelp<T1, T2>)?.Call(value, value1);
      }
      else
      {
            LogEventNotFound(eventName, "调用");
      }
    }


    /// <summary>
    /// 移除事件监听
    /// </summary>
    /// <param name="eventName">事件名称</param>
    /// <param name="action">要移除的事件回调</param>
    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<T>(string eventName, Action<T> action)
    {
      if (_eventCenter.TryGetValue(eventName, out var eventHelp))
      {
            (eventHelp as EventHelp<T>)?.Remove(action);
      }
      else
      {
            LogEventNotFound(eventName, "移除");
      }
    }
    public void RemoveEvent<T1, T2>(string eventName, Action<T1, T2> action)
    {
      if (_eventCenter.TryGetValue(eventName, out var eventHelp))
      {
            (eventHelp as EventHelp<T1, T2>)?.Remove(action);
      }
      else
      {
            LogEventNotFound(eventName, "移除");
      }
    }

    /// <summary>
    /// 事件未找到时的统一日志输出
    /// </summary>
    /// <param name="eventName">事件名称</param>
    /// <param name="operation">操作类型(移除、调用)</param>
    private void LogEventNotFound(string eventName, string operation)
    {
      DevelopmentTools.WTF($"当前未找到{eventName}的事件,无法{operation}");
    }

}Day05 AnimationStringToHash

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 动画参数哈希值管理类,用于统一存储Animator参数的哈希值,避免重复计算
/// </summary>
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");
}Day06GameTimer

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 计时器状态枚举,描述计时器不同工作阶段
/// </summary>
public enum TimerState
{
    NOTWORKERE, // 没有工作(初始或重置后状态)
    WORKERING,// 工作中(计时进行时)
    DONE      // 工作完成(计时结束)
}

/// <summary>
/// 游戏计时器类,用于管理计时逻辑,支持启动计时、更新计时、获取状态、重置等功能
/// </summary>
public class GameTimer
{
    // 计时时长(剩余计时时间)
    private float _startTime;
    // 计时结束后要执行的任务(Action 委托)
    private Action _task;
    // 是否停止当前计时器标记
    private bool _isStopTimer;
    // 当前计时器的状态
    private TimerState _timerState;

    /// <summary>
    /// 构造函数,初始化时重置计时器
    /// </summary>
    public GameTimer()
    {
      ResetTimer();
    }

    /// <summary>
    /// 1. 开始计时
    /// </summary>
    /// <param name="time">要计时的时长</param>
    /// <param name="task">计时结束后要执行的任务(Action 委托)</param>
    public void StartTimer(float time, Action task)
    {
      _startTime = time;
      _task = task;
      _isStopTimer = false;
      _timerState = TimerState.WORKERING;
    }

    /// <summary>
    /// 2. 更新计时器(通常在 MonoBehaviour 的 Update 里调用,驱动计时逻辑)
    /// </summary>
    public void UpdateTimer()
    {
      // 如果标记为停止,直接返回,不执行计时更新
      if (_isStopTimer)
            return;

      // 递减计时时间
      _startTime -= Time.deltaTime;
      // 计时时间小于 0,说明计时结束
      if (_startTime < 0)
      {
            // 安全调用任务(如果任务不为 null 才执行)
            _task?.Invoke();
            // 更新状态为已完成
            _timerState = TimerState.DONE;
            // 标记为停止,后续不再继续计时更新
            _isStopTimer = true;
      }
    }

    /// <summary>
    /// 3. 获取当前计时器的状态
    /// </summary>
    /// <returns>返回 TimerState 枚举值,代表当前计时器状态</returns>
    public TimerState GetTimerState() => _timerState;

    /// <summary>
    /// 4. 重置计时器,恢复到初始状态
    /// </summary>
    public void ResetTimer()
    {
      _startTime = 0f;
      _task = null;
      _isStopTimer = true;
      _timerState = TimerState.NOTWORKERE;
    }
}TimerManager
using System;
using System.Collections;
using System.Collections.Generic;
using GGG.Tool;
using GGG.Tool.Singleton;
using UnityEngine;
using UnityEngine.UIElements;

/// <summary>
/// 计时器管理器,采用单例模式,负责管理空闲计时器队列和工作中计时器列表,
/// 实现计时器的初始化、分配、回收及更新逻辑
/// </summary>
public class TimerManager : Singleton<TimerManager>
{
    #region 私有字段
    // 初始最大计时器数量,在 Inspector 中配置
    private int _initMaxTimerCount;

    // 空闲计时器队列,存储可用的 GameTimer
    private Queue<GameTimer> _notWorkingTimer = new Queue<GameTimer>();
    // 工作中计时器列表,存储正在计时的 GameTimer
    private List<GameTimer> _workingTimer = new List<GameTimer>();
    #endregion

    #region 生命周期与初始化
    protected override void Awake()
    {
      base.Awake();
      InitTimerManager();
    }

    /// <summary>
    /// 初始化计时器管理器,创建初始数量的空闲计时器
    /// </summary>
    private void InitTimerManager()
    {
      for (int i = 0; i < _initMaxTimerCount; i++)
      {
            CreateTimerInternal();
      }
    }

    /// <summary>
    /// 内部创建计时器并加入空闲队列的方法
    /// </summary>
    private void CreateTimerInternal()
    {
      var timer = new GameTimer();
      _notWorkingTimer.Enqueue(timer);
    }
    #endregion

    #region 计时器分配与回收
    /// <summary>
    /// 尝试获取一个计时器,用于执行定时任务
    /// </summary>
    /// <param name="time">计时时长</param>
    /// <param name="task">计时结束后执行的任务</param>
    public void TryGetOneTimer(float time, Action task)
    {
      // 若空闲队列为空,额外创建一个计时器
      if (_notWorkingTimer.Count == 0)
      {
            CreateTimerInternal();
      }

      var timer = _notWorkingTimer.Dequeue();
      timer.StartTimer(time, task);
      _workingTimer.Add(timer);
    }

    /// <summary>
    /// 回收计时器(可在 GameTimer 完成任务时调用,这里逻辑已内联在更新里,也可扩展外部调用)
    /// 注:当前通过 UpdateWorkingTimer 自动回收,此方法可留作扩展
    /// </summary>
    /// <param name="timer">要回收的计时器</param>
    private void RecycleTimer(GameTimer timer)
    {
      timer.ResetTimer();
      _notWorkingTimer.Enqueue(timer);
      _workingTimer.Remove(timer);
    }
    #endregion

    #region 计时器更新逻辑
    private void Update()
    {
      UpdateWorkingTimer();
    }

    /// <summary>
    /// 更新工作中计时器的状态,处理计时推进和完成后的回收
    /// </summary>
    private void UpdateWorkingTimer()
    {
      // 遍历副本,避免列表修改时迭代出错
      for (int i = _workingTimer.Count - 1; i >= 0; i--)
      {
            var timer = _workingTimer;
            timer.UpdateTimer();

            if (timer.GetTimerState() == TimerState.DONE)
            {
                RecycleTimer(timer);
            }
      }
    }
    #endregion
}Day07 脚部拖尾特效的控制——奔跑时启用

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
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;      private void OnAnimatorIK(int layerIndex)    {      //四肢      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);      //头部      if (Head_IKPoint != null)            IKHeadControl(Head_IKPoint);    }    ///   /// 头部 IK 控制(平滑转向 + 角度限制)    ///   /// 要看的对象    /// 插值速度    /// 最大允许夹角(度数)    private void IKHeadControl(Transform target,                               float turnSpeed = 3f,                               float maxAngle = 60f)    {      // 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 (anglefloat.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 forpitch            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.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
页: [1]
查看完整版本: 类蜘蛛侠+刺客信条暗杀动作系统开发日志