<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      【Unity】類蜘蛛俠+刺客信條暗殺動作系統開發日志

      新版輸入系統——斜向移動變快問題解決

      1754941696648

      生成對應的input管理腳本

      1754941881506

      Day 01——角色移動基類

      CharacterMovementControlBase

      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<CharacterController>();
                  _animator = GetComponent<Animator>();
              }
      
              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();
          }
      }
      
      

      1754944324179

      1754944336983

      1754944344944

      TP_CameraControl

      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<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
      {
      
          [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();
          }
      
          /// <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

      動畫部分

      1754948921177

      1754948949774

      1754948992938

      17549490189541754949029653

      腳本

      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
          {
              [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();
              }
      
              /// <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");
      }
      

      Day06 GameTimer

      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 中配置
          [SerializeField] 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[i];
                  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; // 重置等待標記
          }
      }
      
      

      1755513462447

      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;
      
          [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();
          }
      
          /// <summary>
          /// 處理頭部IK控制 - 解決生硬切換問題
          /// </summary>
          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);
              }
          }
      
      
          /// <summary>
          /// 頭部 IK 控制(平滑轉向 + 角度限制)
          /// </summary>
          /// <param name="target">要看的對象</param>
          /// <param name="turnSpeed">插值速度</param>
          /// <param name="maxAngle">最大允許夾角(度數)</param>
          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 <= maxAngle)
              {
                  _currentLookTarget = Vector3.Lerp(_currentLookTarget, rawTargetPos,
                                                    turnSpeed * Time.deltaTime);
              }
              // 否則保持上一幀的 _currentLookTarget 不變(即不更新)
      
              // 4. Debug繪制
              Debug.DrawLine(transform.position, _currentLookTarget, Color.red);
              Debug.DrawRay(target.position, target.forward * 10f, Color.blue);
      
              // 注意:移除了這里的SetLookAtWeight和SetLookAtPosition調用
              // 因為現在在HandleHeadIK()中統一處理
          }
          /// <summary>
          /// 四肢IK控制
          /// </summary>
          /// <param name="ControlPosition"></param>
          /// <param name="target"></param>
          public void IKControl(AvatarIKGoal ControlPosition, Transform target)
          {
              _animator.SetIKPositionWeight(ControlPosition, 1);
              _animator.SetIKPosition(ControlPosition, target.position);
              _animator.SetIKRotationWeight(ControlPosition, 1);
              _animator.SetIKRotation(ControlPosition, target.rotation);
          }
      }
      
      

      1755513646553

      Day09 角色切換——Spiderman To Spider

      蜘蛛控制腳本——Rigging Animation

      Spider

      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      using Raycasting;
      
      /*
       * This class represents the actual spider. It is responsible for "glueing" it to the surfaces around it. This is accomplished by
       * creating a fake gravitational force in the direction of the surface normal it is standing on. The surface normal is determined
       * by spherical raycasting downwards, as well as forwards for wall-climbing.
       * 
       * The torso of the spider will move and rotate depending on the height of the referenced legs to mimic "spinal movement".
       * 
       * The spider does not move on its own. Therefore a controller should call the provided functions walk() and turn() for
       * the desired control.
       */
      
      [DefaultExecutionOrder(0)] // Any controller of this spider should have default execution -1
      public class Spider : MonoBehaviour {
      
          private Rigidbody rb;
      
          [Header("Debug")]
          public bool showDebug;
      
          [Header("Movement")]
          [Range(1, 10)]
          public float walkSpeed;
          [Range(1, 10)]
          public float runSpeed;
          [Range(1, 5)]
          public float turnSpeed;
          [Range(0.001f, 1)]
          public float walkDrag;
      
          [Header("Grounding")]
          public CapsuleCollider capsuleCollider;
          [Range(1, 10)]
          public float gravityMultiplier;
          [Range(1, 10)]
          public float groundNormalAdjustSpeed;
          [Range(1, 10)]
          public float forwardNormalAdjustSpeed;
          public LayerMask walkableLayer;
          [Range(0, 1)]
          public float gravityOffDistance;
      
          [Header("IK Legs")]
          public Transform body;
          public IKChain[] legs;
      
          [Header("Body Offset Height")]
          public float bodyOffsetHeight;
      
          [Header("Leg Centroid")]
          public bool legCentroidAdjustment;
          [Range(0, 100)]
          public float legCentroidSpeed;
          [Range(0, 1)]
          public float legCentroidNormalWeight;
          [Range(0, 1)]
          public float legCentroidTangentWeight;
      
          [Header("Leg Normal")]
          public bool legNormalAdjustment;
          [Range(0, 100)]
          public float legNormalSpeed;
          [Range(0, 1)]
          public float legNormalWeight;
      
          private Vector3 bodyY;
          private Vector3 bodyZ;
      
          [Header("Breathing")]
          public bool breathing;
          [Range(0.01f, 20)]
          public float breathePeriod;
          [Range(0, 1)]
          public float breatheMagnitude;
      
          [Header("Ray Adjustments")]
          [Range(0.0f, 1.0f)]
          public float forwardRayLength;
          [Range(0.0f, 1.0f)]
          public float downRayLength;
          [Range(0.1f, 1.0f)]
          public float forwardRaySize = 0.66f;
          [Range(0.1f, 1.0f)]
          public float downRaySize = 0.9f;
          private float downRayRadius;
      
          private Vector3 currentVelocity;
          private bool isMoving = true;
          private bool groundCheckOn = true;
      
          private Vector3 lastNormal;
          private Vector3 bodyDefaultCentroid;
          private Vector3 bodyCentroid;
      
          private SphereCast downRay, forwardRay;
          private RaycastHit hitInfo;
      
          private enum RayType { None, ForwardRay, DownRay };
          private struct groundInfo {
              public bool isGrounded;
              public Vector3 groundNormal;
              public float distanceToGround;
              public RayType rayType;
      
              public groundInfo(bool isGrd, Vector3 normal, float dist, RayType m_rayType) {
                  isGrounded = isGrd;
                  groundNormal = normal;
                  distanceToGround = dist;
                  rayType = m_rayType;
              }
          }
      
          private groundInfo grdInfo;
      
          private void Awake() {
      
              //Make sure the scale is uniform, since otherwise lossy scale will not be accurate.
              float x = transform.localScale.x; float y = transform.localScale.y; float z = transform.localScale.z;
              if (Mathf.Abs(x - y) > 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<Rigidbody>();
      
              //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 <= 0f) return transform.up;
      
              Vector3 newNormal = transform.up;
              Vector3 toEnd;
              Vector3 currentTangent;
      
              for (int i = 0; i < legs.Length; i++) {
                  //normal += legWeight * legs[i].getTarget().normal;
                  toEnd = legs[i].getEndEffector().position - transform.position;
                  currentTangent = Vector3.ProjectOnPlane(toEnd, transform.up);
      
                  if (currentTangent == Vector3.zero) continue; // Actually here we would have a 90degree rotation but there is no choice of a tangent.
      
                  newNormal = Quaternion.Lerp(Quaternion.identity, Quaternion.FromToRotation(currentTangent, toEnd), legNormalWeight) * newNormal;
              }
              return newNormal;
          }
      
      
          //** Getters **//
          public float getScale() {
              return transform.lossyScale.y;
          }
      
          public bool getIsMoving() {
              return isMoving;
          }
      
          public Vector3 getCurrentVelocityPerSecond() {
              return currentVelocity / Time.fixedDeltaTime;
          }
      
          public Vector3 getCurrentVelocityPerFixedFrame() {
              return currentVelocity;
          }
          public Vector3 getGroundNormal() {
              return grdInfo.groundNormal;
          }
      
          public Vector3 getLastNormal() {
              return lastNormal;
          }
      
          public float getColliderRadius() {
              return getScale() * capsuleCollider.radius;
          }
      
          public float getNonScaledColliderRadius() {
              return capsuleCollider.radius;
          }
      
          public float getColliderLength() {
              return getScale() * capsuleCollider.height;
          }
      
          public Vector3 getColliderCenter() {
              return transform.TransformPoint(capsuleCollider.center);
          }
      
          public Vector3 getColliderBottomPoint() {
              return transform.TransformPoint(capsuleCollider.center - capsuleCollider.radius * new Vector3(0, 1, 0));
          }
      
          public Vector3 getDefaultCentroid() {
              return transform.TransformPoint(bodyDefaultCentroid);
          }
      
          public float getGravityOffDistance() {
              return gravityOffDistance * getColliderRadius();
          }
      
          //** Setters **//
          public void setGroundcheck(bool b) {
              groundCheckOn = b;
          }
      
          //** Debug Methods **//
          private void drawDebug() {
              //Draw the two Sphere Rays
              downRay.draw(Color.green);
              forwardRay.draw(Color.blue);
      
              //Draw the Gravity off distance
              Vector3 borderpoint = getColliderBottomPoint();
              Debug.DrawLine(borderpoint, borderpoint + getGravityOffDistance() * -transform.up, Color.magenta);
      
              //Draw the current transform.up and the bodys current Y orientation
              Debug.DrawLine(transform.position, transform.position + 2f * getColliderRadius() * transform.up, new Color(1, 0.5f, 0, 1));
              Debug.DrawLine(transform.position, transform.position + 2f * getColliderRadius() * body.TransformDirection(bodyY), Color.blue);
      
              //Draw the Centroids 
              DebugShapes.DrawPoint(getDefaultCentroid(), Color.magenta, 0.1f);
              DebugShapes.DrawPoint(getLegsCentroid(), Color.red, 0.1f);
              DebugShapes.DrawPoint(getColliderBottomPoint(), Color.cyan, 0.1f);
          }
      
      #if UNITY_EDITOR
          void OnDrawGizmosSelected() {
      
              if (!showDebug) return;
              if (UnityEditor.EditorApplication.isPlaying) return;
              if (!UnityEditor.Selection.Contains(transform.gameObject)) return;
      
              Awake();
              drawDebug();
          }
      #endif
      
      }
      
      

      SpiderController

      using UnityEngine;
      using System.Collections;
      using Raycasting;
      
      /*
       * This class needs a reference to the Spider class and calls the walk and turn functions depending on player input.
       * So in essence, this class translates player input to spider movement. The input direction is relative to a camera and so a 
       * reference to one is needed.
       */
      
      [DefaultExecutionOrder(-1)] // Make sure the players input movement is applied before the spider itself will do a ground check and possibly add gravity
      public class SpiderController : MonoBehaviour {
      
          public Spider spider;
      
          [Header("Camera")]
          public SmoothCamera smoothCam;
      
          void FixedUpdate() {
              //** Movement **//
              Vector3 input = getInput();
      
              if (Input.GetKey(KeyCode.LeftShift)) spider.run(input);
              else spider.walk(input);
      
              Quaternion tempCamTargetRotation = smoothCam.getCamTargetRotation();
              Vector3 tempCamTargetPosition = smoothCam.getCamTargetPosition();
              spider.turn(input);
              smoothCam.setTargetRotation(tempCamTargetRotation);
              smoothCam.setTargetPosition(tempCamTargetPosition);
          }
      
          void Update() {
              //Hold down Space to deactivate ground checking. The spider will fall while space is hold.
              spider.setGroundcheck(!Input.GetKey(KeyCode.Space));
          }
      
          private Vector3 getInput() {
              Vector3 up = spider.transform.up;
              Vector3 right = spider.transform.right;
              Vector3 input = Vector3.ProjectOnPlane(smoothCam.getCameraTarget().forward, up).normalized * Input.GetAxis("Vertical") + (Vector3.ProjectOnPlane(smoothCam.getCameraTarget().right, up).normalized * Input.GetAxis("Horizontal"));
              Quaternion fromTo = Quaternion.AngleAxis(Vector3.SignedAngle(up, spider.getGroundNormal(), right), right);
              input = fromTo * input;
              float magnitude = input.magnitude;
              return (magnitude <= 1) ? input : input /= magnitude;
          }
      }
      

      IKStepManager

      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      
      /*
       * This class holds references to each IKStepper of the legs and manages the stepping of them.
       * So instead of each leg managing its stepping on its own, this class acts as the brain and decides when each leg should step.
       * It uses the step checking function in the IKStepper to determine if a step is wanted for a leg, and then handles it by calling
       * the step function in the IKStepper when the time is right to step.
       */
      
      [DefaultExecutionOrder(+1)] // Make sure all the stepping logic is called after the IK was solved in each IKChain
      public class IKStepManager : MonoBehaviour {
          public bool printDebugLogs;
      
          public Spider spider;
      
          public enum StepMode { AlternatingTetrapodGait, QueueWait, QueueNoWait }
          /*
           * Note the following about the stepping modes:
           * 
           * Alternating Tetrapod Gait:   This mode is inspired by a real life spider walk.
           *                              The legs are assigned one of two groups, A or B.
           *                              Then a timer switches between these groups on the timeinterval "stepTime".
           *                              Every group only has a specific frame at which stepping is allowed in each interval
           *                              With this, legs in the same group will always step at the same time if they need to step,
           *                              and will never step while the other group is.
           *                              If dynamic step time is selected, the average of each legs dyanamic step time is used.
           *                              This mode does not use the asynchronicity specified in each legs, since the asyncronicty is already given
           *                              by the groups.
           *            
           * Queue Wait:  This mode stores the legs that want to step in a queue and performs the stepping in the order of the queue.
           *              This mode will always prioritize the next leg in the queue and will wait until it is able to step.
           *              This however can and will inhibit the other legs from stepping if the waiting period is too long.
           *              Unlike the above mode, this mode uses the asyncronicity defined in each leg to determine whether a leg is 
           *              allowed to step or not. Each leg will be inhibited to step as long as these async legs are stepping.
           *  
           * Queue No Wait:   This mode is analog to the above with the exception of not waiting for each next leg in the queue.
           *                  The legs will still be iterated through in queue order but if a leg is not able to step,
           *                  we still continue iterating and perform steps for the following legs if they are able to.
           *                  So to be more specific, this is not a queue in the usual sense. It is a list of legs that need stepping,
           *                  which will be iterated through in order and if the k-th leg is allowed to step, it will step
           *                  and the k-th element of this list will be removed.
           */
      
          [Header("Step Mode")]
          public StepMode stepMode;
      
          //Order is important here as this is the order stepCheck is performed, giving the first elements more priority in case of a same frame step desire
          [Header("Legs for Queue Modes")]
          public List<IKStepper> ikSteppers;
          private List<IKStepper> stepQueue;
          private Dictionary<int, bool> waitingForStep;
      
          [Header("Legs for Gait Mode")]
          public List<IKStepper> gaitGroupA;
          public List<IKStepper> gaitGroupB;
          private List<IKStepper> currentGaitGroup;
          private float nextSwitchTime;
      
          [Header("Steptime")]
          public bool dynamicStepTime = true;
          public float stepTimePerVelocity;
          [Range(0, 1.0f)]
          public float maxStepTime;
      
          public enum GaitStepForcing { NoForcing, ForceIfOneLegSteps, ForceAlways }
          [Header("Debug")]
          public GaitStepForcing gaitStepForcing;
      
          private void Awake() {
      
              /* Queue Mode Initialization */
      
              stepQueue = new List<IKStepper>();
      
              // Remove all inactive IKSteppers
              int k = 0;
              foreach (var ikStepper in ikSteppers.ToArray()) {
                  if (!ikStepper.allowedTargetManipulationAccess()) ikSteppers.RemoveAt(k);
                  else k++;
              }
      
              // Initialize the hash map for step waiting with false
              waitingForStep = new Dictionary<int, bool>();
              foreach (var ikStepper in ikSteppers) {
                  waitingForStep.Add(ikStepper.GetInstanceID(), false);
              }
      
              /* Alternating Tetrapod Gait Initialization */
      
              // Remove all inactive IKSteppers from the Groups
              k = 0;
              foreach (var ikStepper in gaitGroupA.ToArray()) {
                  if (!ikStepper.allowedTargetManipulationAccess()) gaitGroupA.RemoveAt(k);
                  else k++;
              }
              k = 0;
              foreach (var ikStepper in gaitGroupB.ToArray()) {
                  if (!ikStepper.allowedTargetManipulationAccess()) gaitGroupB.RemoveAt(k);
                  else k++;
              }
      
              // Start with Group A and set switch time to step time
              currentGaitGroup = gaitGroupA;
              nextSwitchTime = maxStepTime;
          }
      
          private void LateUpdate() {
              if (stepMode == StepMode.AlternatingTetrapodGait) AlternatingTetrapodGait();
              else QueueStepMode();
          }
      
          private void QueueStepMode() {
      
              /* Perform the step checks for all legs not already waiting to step.
               * If a step is needed, enqueue them.
               */
              foreach (var ikStepper in ikSteppers) {
      
                  // Check if Leg isnt already waiting for step.
                  if (waitingForStep[ikStepper.GetInstanceID()] == true) continue;
      
                  //Now perform check if a step is needed and if so enqueue the element
                  if (ikStepper.stepCheck()) {
                      stepQueue.Add(ikStepper);
                      waitingForStep[ikStepper.GetInstanceID()] = true;
                      if (printDebugLogs) Debug.Log(ikStepper.name + " is enqueued to step at queue position " + stepQueue.Count);
                  }
              }
      
              if (printDebugLogs) printQueue();
      
              /* Iterate through the step queue in order and check if legs are eligible to step.
               * If legs are able to step, let them step.
               * If not, we have two cases:   If the current mode selected is the QueueWait mode, then stop the iteration.
               *                              If the current mode selected is the QueueNoWait mode, simply continue with the iteration.
               */
              int k = 0;
              foreach (var ikStepper in stepQueue.ToArray()) {
                  if (ikStepper.allowedToStep()) {
                      ikStepper.getIKChain().unpauseSolving();
                      ikStepper.step(calculateStepTime(ikStepper));
                      // Remove the stepping leg from the list:
                      waitingForStep[ikStepper.GetInstanceID()] = false;
                      stepQueue.RemoveAt(k);
                      if (printDebugLogs) Debug.Log(ikStepper.name + " was allowed to step and is thus removed.");
                  }
                  else {
                      if (printDebugLogs) Debug.Log(ikStepper.name + " is not allowed to step.");
      
                      // Stop iteration here if Queue Wait mode is selected
                      if (stepMode == StepMode.QueueWait) {
                          if (printDebugLogs) Debug.Log("Wait selected, thus stepping ends for this frame.");
                          break;
                      }
                      k++; // Increment k by one here since i did not remove the current element from the list.
                  }
              }
      
              /* Iterate through all the legs that are still in queue, and therefore werent allowed to step.
               * For them pause the IK solving while they are waiting.
               */
              foreach (var ikStepper in stepQueue) {
                  ikStepper.getIKChain().pauseSolving();
              }
          }
      
          private void AlternatingTetrapodGait() {
      
              // If the next switch time isnt reached yet, do nothing.
              if (Time.time < nextSwitchTime) return;
      
      
              /* Since switch time is reached, switch groups and set new switch time.
               * Note that in the case of dynamic step time, it would not make sense to have each leg assigned its own step time
               * since i want the stepping to be completed at the same time in order to switch to next group again.
               * Thus, i simply calculate the average step time of the current group and use it for all legs.
               * TODO: Add a random offset to the steptime of each leg to imitate nature more closely and use the max value as the next switch time
               */
              currentGaitGroup = (currentGaitGroup == gaitGroupA) ? gaitGroupB : gaitGroupA;
              float stepTime = calculateAverageStepTime(currentGaitGroup);
              nextSwitchTime = Time.time + stepTime;
      
              if (printDebugLogs) {
                  string text = ((currentGaitGroup == gaitGroupA) ? "Group: A" : "Group B") + " StepTime: " + stepTime;
                  Debug.Log(text);
              }
      
              /* Now perform the stepping for the current gait group.
               * A leg in the gait group will only step if a step is needed.
               * However, for debug purposes depending on which force mode is selected the other legs can be forced to step anyway.
               */
              if (gaitStepForcing == GaitStepForcing.ForceAlways) {
                  foreach (var ikStepper in currentGaitGroup) ikStepper.step(stepTime);
              }
              else if (gaitStepForcing == GaitStepForcing.ForceIfOneLegSteps) {
                  bool b = false;
                  foreach (var ikStepper in currentGaitGroup) {
                      b = b || ikStepper.stepCheck();
                      if (b == true) break;
                  }
                  if (b == true) foreach (var ikStepper in currentGaitGroup) ikStepper.step(stepTime);
              }
              else {
                  foreach (var ikStepper in currentGaitGroup) {
                      if (ikStepper.stepCheck()) ikStepper.step(stepTime);
                  }
              }
          }
      
          private float calculateStepTime(IKStepper ikStepper) {
              if (dynamicStepTime) {
                  float k = stepTimePerVelocity * spider.getScale(); // At velocity=1, this is the steptime
                  float velocityMagnitude = ikStepper.getIKChain().getEndeffectorVelocityPerSecond().magnitude;
                  return (velocityMagnitude == 0) ? maxStepTime : Mathf.Clamp(k / velocityMagnitude, 0, maxStepTime);
              }
              else return maxStepTime;
          }
      
          private float calculateAverageStepTime(List<IKStepper> ikSteppers) {
              if (dynamicStepTime) {
                  float stepTime = 0;
                  foreach (var ikStepper in ikSteppers) {
                      stepTime += calculateStepTime(ikStepper);
                  }
                  return stepTime / ikSteppers.Count;
              }
              else return maxStepTime;
          }
      
          private void printQueue() {
              if (stepQueue == null) return;
              string queueText = "[";
              if (stepQueue.Count != 0) {
                  foreach (var ikStepper in stepQueue) {
                      queueText += ikStepper.name + ", ";
                  }
                  queueText = queueText.Substring(0, queueText.Length - 2);
              }
              queueText += "]";
              Debug.Log("Queue: " + queueText);
          }
      }
      
      
      

      1755513101242

      切換角色

      CharacterSwitcher

      using UnityEngine;
      using System.Collections;
      
      public class CharacterSwitcher : MonoBehaviour
      {
          [Header("角色設置")]
          public GameObject character1;
          public GameObject character2;
      
          [Header("切換按鍵")]
          public KeyCode switchKey = KeyCode.Tab;
      
          [Header("當前狀態")]
          public bool isCharacter1Active = true;
      
          [Header("角色2專用相機")]
          public Camera camera2;
      
          [Header("切換延遲")]
          public float switchDelay = 0.5f;   // 等待時間
      
          private bool isSwitching = false;    // 正在等待切換
      
          private void Start()
          {
              if (character1 == null || character2 == null)
              {
                  Debug.LogError("請在Inspector中指定兩個角色的GameObject!");
                  return;
              }
      
              character1.SetActive(isCharacter1Active);
              character2.SetActive(!isCharacter1Active);
      
              if (camera2 != null)
                  camera2.gameObject.SetActive(!isCharacter1Active);
          }
      
          private void Update()
          {
              if (Input.GetKeyDown(switchKey) && !isSwitching)
                  SwitchCharacter();
          }
      
          /* 供外部腳本調用的接口同樣延遲 */
          public void SwitchCharacter()
          {
              if (character1 == null || character2 == null || isSwitching)
                  return;
      
              isSwitching = true;
      
              /* 立即凍結當前角色,防止繼續移動 */
              FreezeMovement(GetActiveCharacter());
      
              /* 延遲真正切換 */
              StartCoroutine(DelayedSwitch());
          }
      
          public void SwitchToSpecificCharacter(bool switchToCharacter1)
          {
              if (isCharacter1Active == switchToCharacter1 || isSwitching)
                  return;
      
              isSwitching = true;
              FreezeMovement(GetActiveCharacter());
              StartCoroutine(DelayedSwitch(switchToCharacter1));
          }
      
          /* 0.5 秒后真正切換 */
          private IEnumerator DelayedSwitch(bool? targetState = null)
          {
              yield return new WaitForSeconds(switchDelay);
      
              bool nextState = targetState ?? !isCharacter1Active;
      
              isCharacter1Active = nextState;
              character1.SetActive(isCharacter1Active);
              character2.SetActive(!isCharacter1Active);
      
              if (camera2 != null)
                  camera2.gameObject.SetActive(!isCharacter1Active);
      
              Debug.Log($"切換到: {(isCharacter1Active ? "角色1" : "角色2")}");
      
              isSwitching = false;
          }
      
          /* 簡單凍結:把 Rigidbody 設為 Kinematic,關閉 CharacterController */
          private void FreezeMovement(GameObject go)
          {
              if (go.TryGetComponent(out Rigidbody rb))
              {
                  rb.velocity = Vector3.zero;
                  rb.angularVelocity = Vector3.zero;
                  rb.isKinematic = true;
              }
      
              if (go.TryGetComponent(out CharacterController cc))
                  cc.enabled = false;
          }
      
          public GameObject GetActiveCharacter()
          {
              return isCharacter1Active ? character1 : character2;
          }
      }
      

      1755513004357

      1755513442129

      Day10 對象池管理——音頻管理

      對象池管理

      GamePoolManager

      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      using GGG.Tool.Singleton;
      using GGG.Tool;
      
      public class GamePoolManager : Singleton<GamePoolManager>
      {
          // 1. 緩存配置項類
          [System.Serializable]
          private class PoolItem
          {
              public string ItemName;      // 對象名稱,用于標識
              public GameObject Item;      // 要緩存的游戲對象
              public int InitMaxCount;     // 初始最大緩存數量
          }
      
          // 2. 緩存配置列表
          [SerializeField]
          private List<PoolItem> _configPoolItem = new List<PoolItem>();
      
          private Dictionary<string, Queue<GameObject>> _poolCenter = new Dictionary<string, Queue<GameObject>>();
          //對象池父對象
          private GameObject _poolItemParent;
      
          private void Start()
          {
              _poolItemParent = new GameObject("PoolItemParent");
              //放到GamePoolManager的子級,統一管理
              _poolItemParent.transform.SetParent(this.transform);
              InitPool();
          }
      
          private void InitPool()
          {
              // 1. 我們判斷外部配置是不是空的。
              if (_configPoolItem.Count == 0)
                  return;
      
              for (var i = 0; i < _configPoolItem.Count; i++)
              {
                  for (int j = 0; j < _configPoolItem[i].InitMaxCount; j++)
                  {
                      var item = Instantiate(_configPoolItem[i].Item);
                      // 將對象設置為不可見
                      item.SetActive(false);
                      // 設置為PoolItemParent的子物體
                      item.transform.SetParent(_poolItemParent.transform);
                      // 判斷池子中有沒有存在這個對象的
                      if (!_poolCenter.ContainsKey(_configPoolItem[i].ItemName))
                      {
                          // 如果當前對象池中沒有對應名稱的池子,那么我們需要創建一個
                          _poolCenter.Add(
                              _configPoolItem[i].ItemName,
                              new Queue<GameObject>()
                          );
                          _poolCenter[_configPoolItem[i].ItemName].Enqueue(item);
                      }
                      else
                      {
                          _poolCenter[_configPoolItem[i].ItemName].Enqueue(item);
                      }
                  }
              }
              Debug.Log(_poolCenter.Count);
              Debug.Log(_poolCenter["ATKSound"].Count);
          }
      
          /// <summary>
          /// 從對象池中嘗試獲取指定名稱的對象,并設置其位置和旋轉信息
          /// </summary>
          /// <param name="name">要獲取的對象池名稱(用于標識特定類型的對象)</param>
          /// <param name="position">對象激活后的世界坐標位置</param>
          /// <param name="rotation">對象激活后的世界空間旋轉角度</param>
          public void TryGetPoolItem(string name, Vector3 position, Quaternion rotation)
          {
              // 檢查對象池容器中是否存在指定名稱的對象池
              if (_poolCenter.ContainsKey(name))
              {
                  // 從對應名稱的對象池隊列中取出隊首的對象(出隊操作)
                  var item = _poolCenter[name].Dequeue();
      
                  // 設置對象的位置信息
                  item.transform.position = position;
      
                  // 設置對象的旋轉信息
                  item.transform.rotation = rotation;
      
                  // 激活對象
                  item.SetActive(true);
      
                  // 將使用后的對象重新放回隊列尾部(實現對象復用,避免頻繁創建銷毀)
                  _poolCenter[name].Enqueue(item);
              }
              else
              {
                  // 當請求的對象池不存在時
                  Debug.Log(message: $"當前請求的對象池{name}不存在");
              }
          }
      
          /// <summary>
          /// 從對象池中嘗試獲取指定名稱的對象(重載方法,不指定位置和旋轉)
          /// </summary>
          /// <param name="name">要獲取的對象池名稱</param>
          /// <returns>獲取到的游戲對象,若對象池不存在則返回null</returns>
          public GameObject TryGetPoolItem(string name)
          {
              // 檢查對象池容器中是否存在指定名稱的對象池
              if (_poolCenter.ContainsKey(name))
              {
                  // 從對應名稱的對象池隊列中取出隊首的對象
                  var item = _poolCenter[name].Dequeue();
      
                  // 激活對象
                  item.SetActive(true);
      
                  // 將使用后的對象重新放回隊列尾部
                  _poolCenter[name].Enqueue(item);
      
                  return item;
              }
      
              // 當請求的對象池不存在時
              Debug.Log(message: $"當前請求的對象池{name}不存在");
              return null;
          }
      }
      
      

      1755501412795

      新建一個音頻預制體,作為對象池的物品

      1755501420801

      1755501470171

      1755501448240

      對象池物品基類

      PoolItemBase

      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      
      
      /// <summary>
      /// 對象池物品接口
      /// </summary>
      public interface IPoolItem
      {
          void Spawn();   // 當對象從對象池取出、激活時執行的邏輯,比如初始化狀態、顯示特效等
          void Recycle(); // 當對象回收到對象池時執行的邏輯,比如重置狀態、隱藏對象等
      }
      
      /// <summary>
      /// 對象池物品基類,繼承自MonoBehaviour并實現IPoolItem接口
      /// 作為具體對象池物品(如子彈、道具等)的抽象父類,封裝通用邏輯
      /// </summary>
      public abstract class PoolItemBase : MonoBehaviour, IPoolItem
      {
      
          private void OnEnable()
          {
              Spawn();
          }
      
          private void OnDisable()
          {
              Recycle();
          }
      
          public virtual void Spawn()
          {
      
          }
      
          public virtual void Recycle()
          {
      
          }
      }
      
      

      對象池中的物品——音頻

      PoolItemSound

      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      
      /// <summary>
      /// 聲音類型枚舉
      /// </summary>
      public enum SoundType
      {
          ATK,    // 攻擊
          HIT,    // 受擊
          BLOCK,  // 格擋
          FOOT    // 腳步
      }
      
      /// <summary>
      /// 聲音對象池物品類
      /// 用于管理音效播放對象的激活、回收,復用AudioSource
      /// </summary>
      public class PoolItemSound : PoolItemBase
      {
          // 音頻源
          private AudioSource _audioSource;
          [SerializeField] SoundType _soundType;
      
          private void Awake()
          {
              _audioSource = GetComponent<AudioSource>();
          }
      
          /// <summary>
          /// 音效對象從對象池取出時的邏輯
          /// </summary>
          public override void Spawn()
          {
              //PlaySound(_soundType); 
          }
      
      
          private void PlaySound(SoundType _soundType)
          {
      
          }
      
      
      }
      

      音頻ScriptableObject

      AssetsSoundSO

      using System.Collections.Generic;
      using UnityEngine;
      
      namespace Spiderman.Assets
      {
          // 自定義創建Asset的菜單,方便在Unity編輯器右鍵創建該資源
          [CreateAssetMenu(fileName = "Sound", menuName = "CreateActions/Assets/Sound", order = 0)]
          public class AssetsSoundSO : ScriptableObject
          {
              // 序列化的內部類,用于配置聲音類型和對應的音頻片段數組
              [System.Serializable]
              private class SoundConfig
              {
                  public SoundType SoundType;     // 聲音類型,需有對應的枚舉定義(代碼里未展示,需確保存在)
                  public AudioClip[] AudioClips;  // 該類型聲音對應的音頻片段數組
              }
      
              // 聲音配置列表,可在Inspector中配置不同類型聲音及其音頻片段
              [SerializeField]
              private List<SoundConfig> _configSound = new List<SoundConfig>();
          }
      }
      

      1755507932833

      然后在AssetsSoundSO中加入函數:根據聲音類型獲取對應的音頻片段

              /// <summary>
              /// 根據聲音類型獲取對應的音頻片段
              /// </summary>
              /// <param name="_soundType"></param>
              /// <returns></returns>
              public AudioClip GetAudioClip(SoundType _soundType)
              {
                  if(_configSound == null || _configSound.Count == 0)
                      return null;
      
                  switch (_soundType)
                  {
                      //隨機返回對應類型的音頻片段
                      case SoundType.ATK:
                          return _configSound[0].AudioClips[Random.Range(0, _configSound[0].AudioClips.Length)];
                      case SoundType.HIT:
                          return _configSound[1].AudioClips[Random.Range(0, _configSound[1].AudioClips.Length)];
                      case SoundType.BLOCK:
                          return _configSound[2].AudioClips[Random.Range(0, _configSound[2].AudioClips.Length)];
                      case SoundType.FOOT:
                          return _configSound[3].AudioClips[Random.Range(0, _configSound[3].AudioClips.Length)];
                  }
      
                  return null;
              }
      
      

      音頻預制體的播放邏輯

      在PoolItemSound中加入音效播放及其回收邏輯

      using Spiderman.Assets;
      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      
      /// <summary>
      /// 聲音類型枚舉
      /// </summary>
      public enum SoundType
      {
          ATK,    // 攻擊
          HIT,    // 受擊
          BLOCK,  // 格擋
          FOOT    // 腳步
      }
      
      /// <summary>
      /// 聲音對象池物品類
      /// 用于管理音效播放對象的激活、回收,復用AudioSource
      /// </summary>
      public class PoolItemSound : PoolItemBase
      {
          // 音頻源
          private AudioSource _audioSource;
          [SerializeField] SoundType _soundType;
          [SerializeField] AssetsSoundSO _soundAssets;
      
          private void Awake()
          {
              _audioSource = GetComponent<AudioSource>();
          }
      
          /// <summary>
          /// 音效對象從對象池取出
          /// </summary>
          public override void Spawn()
          {
              //被激活的時候播放音效
              PlaySound(); 
          }
      
          /// <summary>
          /// 播放音效
          /// </summary>
          private void PlaySound()
          {
              _audioSource.clip = _soundAssets.GetAudioClip(_soundType);
              _audioSource.Play();
              // 回收音效對象
              StartRecycle();
          }
      
          /// <summary>
          /// 音效對象回收
          /// </summary>
          private void StartRecycle()
          {
              // 延遲0.3秒后停止播放
              TimerManager.MainInstance.TryGetOneTimer(0.3f, DisableSelf);
          }
      
          /// <summary>
          /// 定時任務:停止播放
          /// </summary>
          private void DisableSelf()
          {
              _audioSource.Stop();
              gameObject.SetActive(false);
          }
      
      
      }
      

      然后在聲音預制體中拖入該腳本PoolItemSound

      1755509095062

      1755509108582

      注意勾選Assets和對應的Type

      Day11 Animation Event動畫事件

      音頻播放與Animation聯動起來

      腳本AnimationEvent掛在角色身上

      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      
      namespace Spiderman.Event
      {
          public class AnimationEvent : MonoBehaviour
          {
              private void PlaySound(string _soundName)
              {
                  //選取對象池中的音效對象
                  GamePoolManager.MainInstance.TryGetPoolItem(_soundName,transform.position,Quaternion.identity);
              }
          }
      
      }
      
      

      在關鍵幀加入事件調用PlaySound函數:

      1755511849327

      目前進度:

      1755512949571

      Day12 踩在地面上的腳步聲音

      一開始我試過用動畫事件,但是混合樹會讓動畫事件也混合,聲音播放會重疊,因此選擇用腳本控制,但這種方法聲音和動畫的匹配度不夠精準,暫時想不到更好的,慢慢調節參數也能實現基本的腳步聲,后面會去學習更好的方法

      PlayerMovementControl

              // 腳步聲的播放定時器
              private float _nextFootTime;
              [SerializeField] private float _slowFootTime;
              [SerializeField] private float _fastFootTime;
      
              /// <summary>
              /// 更新動畫
              /// </summary>
              private void UpdateAnimation()
              {
                  if (!_characterIsOnGround)
                      return;
      
                  _animator.SetBool(AnimationID.HasInputID, GameInputManager.MainInstance.Movement != Vector2.zero);
      
                  if (_animator.GetBool(AnimationID.HasInputID))
                  {
                      if (GameInputManager.MainInstance.Run)
                      {
                          //按下奔跑鍵
                          _animator.SetBool(AnimationID.RunID, true);
                      }
                      //有輸入
                      //  Run被開啟,那就Movement設置為2,否則設置為輸入的兩個軸的平方
                      var targetSpeed = _animator.GetBool(AnimationID.RunID) ? 2f :GameInputManager.MainInstance.Movement.sqrMagnitude;
                      // 平滑更新animator中的移動參數Movement
                      _animator.SetFloat(
                          AnimationID.MovementID,
                          targetSpeed,
                          0.25f,
                          Time.deltaTime
                      );
                      // 腳步聲的設置
                      SetCharacterFootSound();
      
              /// <summary>
              /// 角色腳步聲的設置
              /// </summary>
              private void SetCharacterFootSound()
              {
                  // 三個條件同時滿足:
                  // 1. 角色當前在地面上(_characterIsOnGround為true)
                  // 2. 動畫控制器中的移動參數大于0.5(有明顯移動)
                  // 3. 當前播放的動畫包含"Motion"標簽(移動類動畫)
                  if (_characterIsOnGround &&
                      _animator.GetFloat(AnimationID.MovementID) > 0.5f &&
                      _animator.AnimationAtTag("Motion"))
                  {
                      // 所有條件滿足時,定時播放腳步聲
                      _nextFootTime -= Time.deltaTime;
                      if (_nextFootTime <= 0f)
                      {
                          PlayFootSound();
                      }
                  }
                  else
                  {
                      _nextFootTime = 0f;
                  }
              }
      
              /// <summary>
              /// 播放腳步聲
              /// </summary>
              private void PlayFootSound()
              {
                  // 從對象池管理器GamePoolManager中獲取腳步聲音的實例
                  // 參數說明:
                  // "FootSound":對象池中的資源標識
                  // transform.position:在角色當前位置播放聲音
                  // Quaternion.identity:使用默認旋轉(聲音通常不需要旋轉)
                  GamePoolManager.MainInstance.TryGetPoolItem(
                      "FootSound",
                      transform.position,
                      Quaternion.identity);
      
                  // 根據當前移動速度決定下次腳步聲的間隔時間
                  _nextFootTime = (_animator.GetFloat(AnimationID.MovementID) > 1.1f)
                      ? _fastFootTime
                      : _slowFootTime;
              }
      
      

      Day13 TurnBackRun折返跑

      Animator

      1755665573846

      1755665591588

      動畫跳轉

      1755665597779

      1755665679957

      1755665715055

      腳本部分

      PlayerMovementControl

              // 角色期望朝向
              private Vector3 _characterTargetDirection;
      
              /// <summary>
              /// 角色旋轉控制
              /// </summary>
              private void CharacterRotationControl(){
      }
      
                  // 滿足HasInput==true且處于“Motion”動畫標簽時,平滑更新角色旋轉
                  if (_animator.GetBool(AnimationID.HasInputID) && _animator.AnimationAtTag("Motion"))
                  {
                      //更新角色朝向
                      transform.eulerAngles = Vector3.up
                                              * Mathf.SmoothDampAngle(
                                                  transform.eulerAngles.y,
                                                  _rotationAngle,
                                                  ref _angleVelocity,
                                                  _rotationSmoothTime
                                              );
                      //角色期望朝向(水平方向,繞 Y 軸的四元數 * 向前方向)
                      _characterTargetDirection = Quaternion.Euler(0, _rotationAngle, 0) * Vector3.forward;
                  }
                  // 計算角色的增量角
                  var deltaAngle = DevelopmentTools.GetDeltaAngle(transform, _characterTargetDirection);
                  // 傳入DeltaAngle參數
                  _animator.SetFloat(AnimationID.DeltaAngleID, deltaAngle);
              }
      

      這里會用到一個封裝好的工具函數GetDeltaAngle"獲取增量角":

              /// <summary>
              /// 獲取增量角
              /// </summary>
              /// <param name="currentTransform">當前角色的Transform</param>
              /// <param name="targetDirection">目標移動方向</param>
              /// <returns></returns>
              public static float GetDeltaAngle(Transform currentTransform, Vector3 targetDirection)
              {
                  //當前角色朝向的角度
                  float angleCurrent = Mathf.Atan2(currentTransform.forward.x, currentTransform.forward.z) * Mathf.Rad2Deg;
                  //目標方向的角度也就是希望角色轉過去的那個方向的角度
                  float targetAngle = Mathf.Atan2(targetDirection.x, targetDirection.z) * Mathf.Rad2Deg;
      
                  return Mathf.DeltaAngle(angleCurrent, targetAngle);
              }
      
      

      AnimationID.DeltaAngleID 別忘了設置

      1755665762354

      注意:

      _rotationSmoothTime這里需要改成0.5以上,太小的值會過快更新當前朝向,導致在計算增量角的時候異常,但是_rotationSmoothTime過大會犧牲轉向的靈活度,會帶來的操作粘滯感,需要后面考慮更好的折返跑邏輯,或角色轉向通過混合樹來控制

      Day14 移動重置連招 和 加入變招

      重置連招

      PlayerCombatControl

              private void Update()
              {
                  CharacterBaseAttackInput();
                  OnEndCombo();
              }
      
              #region 重置連招狀態
              /// <summary>
              /// 重置連招狀態(索引、冷卻時間)
              /// </summary>
              private void ResetComboInfo()
              {
                  _currentComboIndex = 0;
                  _maxColdTime = 0f;
                  _hitIndex = 0;
              }
      
              /// <summary>
              /// 移動的時候重置Combo索引
              /// </summary>
              private void OnEndCombo()
              {
                  if(_animator.AnimationAtTag("Motion") && _canAttackInput)
                  {
                      ResetComboInfo();
                  }
              }
              #endregion
      

      切手技——輕重攻擊混合

      PlayerCombatControl

              // 角色連招配置(Inspector 可配置)
              [Header("角色輕擊連招配置")]
              [SerializeField] private CharacterComboSO _lightCombo;      // 輕擊連招
              [Header("角色重擊連招配置")]
              [SerializeField] private CharacterComboSO _heavyCombo;      // 重擊連招
      
              private int _currentComboCount;  // 當前連招動作總數
      
              private void Update()
              {
                  CharacterBaseAttackInput();
                  OnEndCombo();
              }
      

      在CharacterBaseAttackInput()中加入切手技

              #region 基礎攻擊輸入
              /// <summary>
              /// 角色基礎攻擊輸入處理
              /// </summary>
              private void CharacterBaseAttackInput()
              {
                  if (!CanBaseAttackInput())
                      return;
      
                  if (GameInputManager.MainInstance.LAttack)      // 發起基礎攻擊——輕擊
                  {
                      // 切換/重置基礎連招
                      if (_currentCombo == null || _currentCombo != _lightCombo)
                      {
                          ChangeComboState(_lightCombo);
                      }
                      // 執行連招動作
                      ExecuteComboAction();
                  }
                  else if (GameInputManager.MainInstance.RAttack) // 發起變招攻擊——重擊
                  {
                      // 切換到重擊連招
                      ChangeComboState(_heavyCombo);
                      // 在重擊的Combo連招表中選取不同的動作,用于變招
                      // case幾就是輕擊多少下
                      switch (_currentComboCount)
                      {
                          case 0:
                              // R
                          case 1:
                              // LR
                              _currentComboIndex = 0;
                              break;
                          case 2:
                              // LLR
                              _currentComboIndex = 1;
                              break;
                          case 3:
                              // LLLR
                              _currentComboIndex = 2;
                              break;
                          case 4:
                              // LLLLR
                              _currentComboIndex = 3;
                              break;
                      }
                      // 執行連招動作
                      ExecuteComboAction();
                      // 重擊會重置連招總數
                      _currentComboCount = 0;
                  }
              }
      
              #region 切換連招狀態
              /// <summary>
              /// 切換連招狀態——輕擊||重擊
              /// </summary>
              /// <param name="newCombo"></param>
              private void ChangeComboState(CharacterComboSO newCombo)
              {
                  if(newCombo != _currentCombo)
                  {
                      _currentCombo = newCombo;
                      //每次切換都要重置Combo索引
                      ResetComboInfo();
                  }
              }
              #endregion
      

      面板:

      1755758421587

      1755758434725

      1755758444985

      1755758478000

      1755758485209

      Day15 受擊

      1755892699498

      1755892717502

      1755892728480

      數值管控

      CharacterHealthBase

      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      
      namespace Spiderman.Health
      {
          public abstract class CharacterHealthBase : MonoBehaviour
          {
              // 共同邏輯說明:
              // - 都有受傷、被處決、格擋等函數
              // - 都維護生命值信息及當前攻擊者等狀態
      
              protected Animator _animator;
      
              // 當前的攻擊者
              protected Transform _currentAttacker;
      
              private void Awake()
              {
                  _animator = GetComponent<Animator>();
              }
      
              protected virtual void OnEnable()
              {
                  GameEventManager.MainInstance.AddEventListening<float, string, string, Transform, Transform>("觸發傷害", OnCharacterHitEventHandler);
              }
      
              protected virtual void OnDisable()
              {
                  GameEventManager.MainInstance.RemoveEvent<float, string, string, Transform, Transform>("觸發傷害", OnCharacterHitEventHandler);
              }
      
              #region 受擊相關行為
              /// <summary>
              /// 角色受擊行為處理
              /// </summary>
              /// <param name="damage"> 受到的傷害值 </param>
              /// <param name="hitName">攻擊名稱(可用于區分不同攻擊類型表現)</param>
              /// <param name="parryName">格擋相關名稱(若有對應格擋邏輯可依據此處理)</param>
              protected virtual void CharacterHitAction(float damage, string hitName, string parryName)
              {
              
              }
      
              /// <summary>
              /// 處理角色受到傷害的邏輯,需在子類中完善扣除生命值等具體實現
              /// </summary>
              /// <param name="damage">受到的傷害值</param>
              protected virtual void TakeDamage(float damage)
              {
                  // TODO: 去扣除生命值 
              
              }
              #endregion
      
              #region 攻擊者設置邏輯
              /// <summary>
              /// 設置當前的攻擊者
              /// </summary>
              /// <param name="attacker">攻擊者的 Transform</param>
              private void SetAttacker(Transform attacker)
              {
                  if (_currentAttacker == null || _currentAttacker != attacker)
                  {
                      // 標記當前攻擊者
                      _currentAttacker = attacker;
                  }
              }
              #endregion
      
              #region 事件處理邏輯
              /// <summary>
              /// 角色受擊事件處理器,用于響應外部傳來的受擊事件
              /// 做初步校驗后,調用相關方法處理受擊流程
              /// </summary>
              /// <param name="damage">受到的傷害值</param>
              /// <param name="hitName">攻擊名稱</param>
              /// <param name="parryName">格擋相關名稱</param>
              /// <param name="attack">攻擊者的 Transform</param>
              /// <param name="self">自身角色的 Transform(用于校驗是否是自身受擊)</param>
              private void OnCharacterHitEventHandler(float damage, string hitName, string parryName, Transform attack, Transform self)
              {
                  // 如果傳來的self不是當前對象,說明不是自身在受擊
                  if (self != transform)
                  {
                      return;
                  }
      
                  // 否則打的就是自己
                  #region 處理受擊邏輯
                  SetAttacker(attack); // 標記當前攻擊者
                  CharacterHitAction(damage, hitName, parryName); // 處理受擊行為表現
                  TakeDamage(damage); // 處理傷害扣除邏輯
                  #endregion
              }
              #endregion
      
          }
      }
      
      

      EnemyHealthControl

      using Spiderman.Health;
      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      
      namespace Spiderman.Health
      {
          public class EnemyHealthControl : CharacterHealthBase
          {
              protected override void CharacterHitAction(float damage, string hitName, string parrName)
              {
                  // 1.先判斷角色的耐力值是否大于0,大于0就應該是格擋而不是直接受傷
      
                  if (damage <= 30f)
                  {
                      // 不是破防動作:進行格擋或者閃避。
                  }
                  else
                  {
                      // 是破防動作:播放受傷動畫
                      _animator.Play(hitName, layer: 0, normalizedTime: 0f);
      
                      // 播放音效
                      GamePoolManager.MainInstance.TryGetPoolItem(
                          name: "HitSound",
                          transform.position,
                          Quaternion.identity
                      );
                  }
              }
      
          }
      
      }
      

      攻擊事件

              #region 攻擊事件
              /// <summary>
              /// 攻擊事件
              /// </summary>
              private void ATKEvent()
              {
                  // 傷害觸發
                  // 攻擊音效觸發(從對象池中取用)
              }
              #endregion
      

      攻擊相關檢測

      PlayerCombatControl

              private Transform _mainCamera;
              private void Awake()
              {
                  _animator = GetComponent<Animator>();
                  _mainCamera = Camera.main.transform;
              }
      
              // 檢測的方向
              private Vector3 _detectDirection;
              [Header("攻擊檢測")]
              [SerializeField] private float _detectionRange; // 攻擊檢測范圍
              [SerializeField] private float _detectionDistance; // 攻擊檢測距離
              private Transform _currentEnemy;    // 當前檢測到的敵人
      

      Update()

                  // 更新檢測方向
                  UpdateDetectDirection();
      

      FixedUpdate()

              private void FixedUpdate()
              {
                  DetectionTarget();
              }
      
              #region 攻擊檢測
              /// <summary>
              /// 檢測目標敵人
              /// </summary>
              private void DetectionTarget()
              {
                  if (Physics.SphereCast(
                      GetDetectionOrigin(),
                      _detectionRange,
                      _detectDirection,
                      out RaycastHit hit,
                      _detectionDistance,
                      layerMask: 1 << 13,
                      QueryTriggerInteraction.Ignore))
                  {
                      _currentEnemy = hit.collider.transform;
                  }
              }
      
              /// <summary>
              /// 調試用:繪制檢測范圍
              /// </summary>
              private void OnDrawGizmos()
              {
                  // 檢測中心在檢測原點 + 檢測方向 * 檢測距離
                  Vector3 sphereCenter = GetDetectionOrigin() + _detectDirection * _detectionDistance;
                  // 半徑為檢測范圍
                  Gizmos.DrawWireSphere(sphereCenter, _detectionRange);
              }
      
              /// <summary>
              /// 獲取檢測原點
              /// </summary>
              private Vector3 GetDetectionOrigin()
              {
                  // 檢測原點:角色位置 + 向上偏移 0.7f
                  return transform.position + transform.up * 0.7f;
              }
      
              /// <summary>
              /// 更新檢測方向
              /// </summary>
              private void UpdateDetectDirection()
              {
                  // 檢測方向:相機水平方向的輸入移動量
                  _detectDirection =
                      _mainCamera.forward * GameInputManager.MainInstance.Movement.y +
                      _mainCamera.right * GameInputManager.MainInstance.Movement.x;
      
                  // 檢測方向不含豎直方向,清零
                  // (如果后面要上擊和下擊,可以再加上豎直方向)
                  _detectDirection.Set(_detectDirection.x, 0f, _detectDirection.z);
      
                  // 歸一化檢測方向
                  _detectDirection = _detectDirection.normalized;
              }
              #endregion
      

      觸發傷害

      PlayerCombatControl

      Update()

              private void Update()
              {
                  // 更新檢測方向
                  UpdateDetectDirection();
      
                  CharacterBaseAttackInput();
      
                  OnEndCombo();
      
                  // 觸發傷害
                  TriggerDamage();
      
              }
      
              #region 觸發傷害
              private void TriggerDamage()
              {
                  // 1. 要確保有目標。
                  // 2. 要確保敵人處于我們可觸發傷害的距離和角度
                  // 3. 去呼叫事件中心,幫我調用觸發傷害這個函數
      
                  #region 無法觸發傷害的情況
                  // 無目標敵人
                  if (_currentEnemy == null)
                      return;
      
                  // 目標敵人不在有效角度內
                  // 角色朝向 和 角色到當前敵人向量的點積 :是否在有效角度內(閾值 0.85,越接近 1 越好)
                  if (Vector3.Dot(transform.forward, DirectionForTarget(transform, _currentEnemy)) < 0.85f)
                      return;
      
                  // 距離超過閾值
                  if (DistanceForTarget(_currentEnemy, transform) > 1.3f)
                      return;
                  #endregion
      
                  //條件都滿足,才可以觸發傷害
                  #region 可以觸發傷害的情況
                  if (_animator.AnimationAtTag("Attack"))
                  {
                      //基礎攻擊
                      // 從連擊數據中獲取傷害相關參數
                      float damageValue = _currentCombo.TryGetComboDamage(_currentComboIndex);
                      string hitName = _currentCombo.TryGetOneHitName(_currentComboIndex, _hitIndex);
                      string parryName = _currentCombo.TryGetOneParryName(_currentComboIndex, _hitIndex);
      
                      // 調用事件中心觸發傷害事件
                      GameEventManager.MainInstance.CallEvent(
                          "觸發傷害",
                          damageValue,          // value: 傷害值
                          hitName,              // value1: 受傷動畫名
                          parryName,            // value2: 格擋動畫名
                          transform,            // value3: 攻擊者(自己)
                          _currentEnemy         // value4: 當前被攻擊者(敵人)
                      );
      
                      // 備注:這里傳的受傷動畫是單個動畫片段
                  }
                  else
                  {
                      //處決攻擊
                      // 處決是一個完整的被處決動作,同一動畫期間會觸發多次傷害
                  }
                  #endregion
              }
      
              /// <summary>
              /// 自身到目標的單位向量
              /// </summary>
              /// <param name="target">目標</param>
              /// <param name="self">自身</param>
              /// <returns></returns>
              public Vector3 DirectionForTarget(Transform target, Transform self)
              {
                  return (self.position - target.position).normalized;
              }
      
              /// <summary>
              /// 自身到目標之間的距離
              /// </summary>
              /// <param name="target"></param>
              /// <param name="self"></param>
              /// <returns></returns>
              public float DistanceForTarget(Transform target, Transform self)
              {
                  return Vector3.Distance(self.position, target.position);
              }
              #endregion
      
      

      敵人必須在玩家的正前方,才能觸發傷害

              private void Update()
              {
                  // 更新檢測方向
                  UpdateDetectDirection();
      
                  CharacterBaseAttackInput();
      
                  OnEndCombo();
      
                  // 觸發傷害
                  TriggerDamage();
                  // 角色朝向目標敵人
                  LookTargetOnAttack();
              }
      
              #region 讓玩家朝向目標敵人的位置
              /// <summary>
              /// 讓玩家朝向目標敵人
              /// </summary>
              private void LookTargetOnAttack()
              {
                  if(_currentEnemy == null)
                  {
                      return;
                  }
      
                  // 獲取Layer 0層的當前動畫狀態信息
                  AnimatorStateInfo currentState = _animator.GetCurrentAnimatorStateInfo(0);
      
                  // 當前動畫狀態是攻擊動畫,且動畫未播放到后半段
                  if (_animator.AnimationAtTag("Attack") && currentState.normalizedTime < 0.5f)
                  {
                      // 讓當前對象平滑朝向目標敵人的位置
                      transform.Look(_currentEnemy.position, 50f);
                  }
              }
      
              #region 工具函數
              /// <summary>
              /// 看向目標
              /// </summary>
              /// <param name="transform"></param>
              /// <param name="target"></param>
              /// <param name="timer">平滑時間(如果是單擊某個按鍵觸發那么值最好設置100以上。)</param>
              public void Look(Transform transform, Vector3 target, float timer)
              {
                  var direction = (target - transform.position).normalized;
                  direction.y = 0f;
                  Quaternion lookRotation = Quaternion.LookRotation(direction);
                  transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, UnTetheredLerp(timer));
              }
      
              /// <summary>
              /// 不受幀數影響的Lerp
              /// </summary>
              /// <param name="time">平滑時間(盡量設置為大于10的值)</param>
              public float UnTetheredLerp(float time = 10f)
              {
                  return 1 - Mathf.Exp(-time * Time.deltaTime);
              }
              #endregion
      
              #endregion
      

      修改一下攻擊事件函數,并在動畫面板中修改AnimationEvent,音效觸發直接調用這個攻擊事件函數即可觸發相應的傷害和音效,而不只是單單觸發音效

              #region 攻擊事件
              /// <summary>
              /// 攻擊事件
              /// </summary>
              private void ATKEvent()
              {
                  // 傷害觸發
                  TriggerDamage();
                  // 命中索引更新
                  UpdateHitIndex();
                  // 攻擊音效觸發(從對象池中取用)
                  GamePoolManager.MainInstance.TryGetPoolItem(
                      "ATKSound", 
                      transform.position, 
                      Quaternion.identity);
              }
              #endregion
      
              #region 更新命中索引
              /// <summary>
              /// 更新命中索引
              /// </summary>
              private void UpdateHitIndex()
              {
                  _hitIndex++;
      
                  int maxCount = _currentCombo.TryGetHitOrParryMaxCount(_currentComboIndex);
                  if (_hitIndex == maxCount)
                  {
                      _hitIndex = 0;
                  }
              }
              #endregion
      
      

      Player

      1755949478018

      直接在Animation Event中調用這個事件即可

      1755949519119

      配置受擊Combo

      1755949567447

      對比一下之前的播放攻擊音效方式

      1755941102972

      讓人物被打的時候面朝攻擊者

      CharacterHealthBase

              protected virtual void Update()
              {
                  OnHitLookAttacker(); // 角色受擊時面向攻擊者
              }
      
              #region 角色受擊時面向攻擊者
              /// <summary>
              /// 角色受擊時面向攻擊者
              /// </summary>
              private void OnHitLookAttacker()
              {
                  // 沒有當前攻擊者,直接返回
                  if (_currentAttacker == null)
                      return;
      
                  // 獲取當前動畫狀態信息(Layer 0)
                  AnimatorStateInfo currentState = _animator.GetCurrentAnimatorStateInfo(0);
      
                  // 條件:處于受擊(Hit)或格擋(Parry)動畫階段,且動畫標準化時間小于 0.5
                  bool isHitOrParryState = _animator.AnimationAtTag("Hit")
                                           || (_animator.AnimationAtTag("Parry")
                                               && currentState.normalizedTime < 0.5f);
      
                  if (isHitOrParryState)
                  {
                      // 讓當前對象朝向攻擊者位置,平滑時間參數 50f
                      transform.Look(_currentAttacker.position, 50f);
                  }
              }
              #endregion
      
      

      和敵人距離太遠,角色不朝向敵人

      PlayerCombatControl.cs

      LookTargetOnAttack()

                  // 和敵人距離超過閾值,不進行朝向
                  if (DistanceForTarget(_currentEnemy, transform) > 5.0f)
                      return;
      

      Day16 正面處決

      添加處決相關事件:觸發處決、觸發處決傷害

      CharacterHealthBase

              protected virtual void OnEnable()
              {
                  GameEventManager.MainInstance.AddEventListening<float, string, string, Transform, Transform>
                      ("觸發傷害", OnCharacterHitEventHandler);
      
                  GameEventManager.MainInstance.AddEventListening<string, Transform, Transform>
                      ("觸發處決", OnCharacterFinishAttackEventHandler);
      
                  GameEventManager.MainInstance.AddEventListening<float>
                      ("觸發處決傷害", TriggerDamageEventHandler);
              }
      
              protected virtual void OnDisable()
              {
                  GameEventManager.MainInstance.RemoveEvent<float, string, string, Transform, Transform>
                      ("觸發傷害", OnCharacterHitEventHandler);
      
                  GameEventManager.MainInstance.RemoveEvent<string, Transform, Transform>
                      ("觸發處決", OnCharacterFinishAttackEventHandler);
      
                  GameEventManager.MainInstance.RemoveEvent<float>
                      ("觸發處決傷害", TriggerDamageEventHandler);
              }
      

      觸發處決傷害事件 和 處決事件

      CharacterHealthBase

              #region FinishAttack
              /// <summary>
              /// 角色處決事件
              /// </summary>
              /// <param name="hitName"></param>
              /// <param name="attacker"></param>
              /// <param name="self"></param>
              private void OnCharacterFinishAttackEventHandler(string hitName, Transform attacker, Transform self)
              {
                  // 如果傳來的self不是當前對象,說明不是自身在受擊
                  if (self != transform)
                  {
                      return;
                  }
      
                  // 否則打的就是自己
                  #region 處理受擊邏輯
                  SetAttacker(attacker); // 標記當前攻擊者
      
                  // 處理傷害扣除邏輯
                  #endregion
              }
      
              /// <summary>
              /// 處決動畫的觸發傷害事件
              /// </summary>
              /// <param name="damage"></param>
              private void TriggerDamageEventHandler(float damage)
              {
                  TakeDamage(damage);
              }
              #endregion
      
      

      處決的邏輯

      PlayerCombatControl.cs

              [Header("角色處決連招配置")]
              [SerializeField] private CharacterComboSO _finishCombo;      // 處決
      

      CanBaseAttackInput()處決時不允許基礎攻擊

      PlayerCombatControl.cs

              /// <summary>
              /// 判斷是否允許發起基礎攻擊
              /// </summary>
              /// <returns>允許攻擊返回 true,否則 false</returns>
              private bool CanBaseAttackInput()
              {
                  // 不能攻擊輸入時不允許攻擊
                  if (!_canAttackInput)
                      return false;
                  // 正在受擊(Hit 標簽動畫)不允許攻擊
                  if (_animator.AnimationAtTag("Hit"))
                      return false;
                  // 正在格擋(Parry 標簽動畫)不允許攻擊
                  if (_animator.AnimationAtTag("Parry"))
                      return false;
                  // 正在處決(Finish 標簽動畫)不允許攻擊
                  if(_animator.AnimationAtTag("Finish"))
                      return false;
      
                  return true;
              }
              #endregion
      

      是否執行處決、處決的輸入邏輯

      PlayerCombatControl.cs

              #region 處決
              /// <summary>
              /// 是否允許執行處決攻擊
              /// </summary>
              private bool CanSpecialAttack()
              {
                  // 處于 "Finish" 標簽動畫時,不允許
                  if (_animator.AnimationAtTag("Finish"))
                      return false;
      
                  // 沒有當前敵人時,不允許
                  if (_currentEnemy == null)
                      return false;
      
                  return true;
              }
      
              /// <summary>
              /// 處理角色處決攻擊的輸入響應邏輯
              /// </summary>
              private void CharacterFinishAttackInput()
              {
                  // 不滿足處決攻擊條件時,直接返回
                  if (!CanSpecialAttack())
                      return;
      
                  // 檢測到處決輸入時,執行處決流程
                  if (GameInputManager.Instance.FinishAttack)
                  {
                      // 1. 隨機選取處決連招索引
                      _currentComboIndex = Random.Range(0, _finishCombo.TryGetComboMaxCount());
      
                      // 2. 播放對應的處決動畫
                      string finishAnim = _finishCombo.TryGetOneComboAction(_currentComboIndex);
                      _animator.Play(finishAnim);
      
                      // 3. 調用事件中心,觸發敵人的處決事件
                      string hitName = _finishCombo.TryGetOneHitName(_currentComboIndex, 0);
                      GameEventManager.MainInstance.CallEvent(
                          "觸發處決",
                          hitName,
                          transform,
                          _currentEnemy
                      // 4. 調用定時器事件:更新連招狀態信息,防止索引越界
                      TimerManager.Instance.TryGetOneTimer(
                          _finishCombo.TryGetColdTime(_currentComboIndex),    //這里原先寫的是固定的0.5f
                          UpdateComboInfo);
                      );
                  }
              }
              #endregion
      
      

      觸發傷害(普通攻擊和處決攻擊)

      PlayerCombatControl.cs

      TriggerDamage()

      region 可以觸發傷害的情況

                  else
                  {               // 同一處決動畫期間會觸發多次傷害
                      //處決攻擊
                      // 從處決數據中獲取連招傷害相關參數
                      float damageValue = _finishCombo.TryGetComboDamage(_currentComboIndex);
                      // 調用觸發處決傷害事件
                      GameEventManager.MainInstance.CallEvent("觸發處決傷害", damageValue);
                  }
      

      把處決輸入放進update

              private void Update()
              {
                  // 更新檢測方向
                  UpdateDetectDirection();
      
                  // 基礎攻擊輸入
                  CharacterBaseAttackInput();
      
                  // 移動的時候結束連招(重置Combo索引)
                  OnEndCombo();
      
                  // 角色朝向目標敵人
                  LookTargetOnAttack();
      
                  // 處決攻擊輸入
                  CharacterFinishAttackInput();
              }
      

      處決期間同步角色位置

      PlayerCombatControl.cs

              #region 位置同步
              /// <summary>
              /// 處決期間玩家位置同步
              /// </summary>
              private void MatchPosition()
              {
                  // 無當前敵人時直接返回
                  if (_currentEnemy == null)
                      return;
      
                  // Animator 未初始化時跳過
                  if (_animator == null)
                      return;
      
                  // 處于 "Finish" 標簽動畫時,執行旋轉與匹配流程
                  if (_animator.AnimationAtTag("Finish"))
                  {
                      // 讓角色朝向敵人的反方向(根據需求確認邏輯是否正確)
                      transform.rotation = Quaternion.LookRotation(-_currentEnemy.forward);
      
                      // 執行連招匹配邏輯,使用默認時間參數
                      RunningMatch(_finishCombo);
                  }
              }
      
              /// <summary>
              /// 執行動畫目標匹配流程,處理位置與權重配置
              /// </summary>
              /// <param name="combo">連招配置數據</param>
              /// <param name="startTime">匹配起始標準化時間</param>
              /// <param name="endTime">匹配結束標準化時間</param>
              private void RunningMatch(CharacterComboSO combo, float startTime = 0f, float endTime = 0.01f)
              {
                  // 當前不在動畫匹配狀態 且 動畫未處于過渡 時,觸發 MatchTarget
                  if (!_animator.isMatchingTarget && !_animator.IsInTransition(0))
                  {
                      // 計算目標位置偏移(根據當前連招索引)
                      float positionOffset = combo.TryGetComboPositionOffset(_currentComboIndex);
                      Vector3 targetPosition = _currentEnemy.position - transform.forward * positionOffset;
      
                      // 構建 MatchTarget 參數
                      Quaternion targetRotation = Quaternion.identity;
                      AvatarTarget avatarTarget = AvatarTarget.Body;
                      MatchTargetWeightMask weightMask = new MatchTargetWeightMask(Vector3.one, 0f);
      
                      // 執行動畫位置匹配
                      _animator.MatchTarget(
                          targetPosition,
                          targetRotation,
                          avatarTarget,
                          weightMask,
                          startTime,
                          endTime
                      );
                  }
              }
              #endregion
      
      

      放到update()

              private void Update()
              {
                  // 更新檢測方向
                  UpdateDetectDirection();
      
                  // 基礎攻擊輸入
                  CharacterBaseAttackInput();
      
                  // 移動的時候結束連招(重置Combo索引)
                  OnEndCombo();
      
                  // 角色朝向目標敵人
                  LookTargetOnAttack();
      
                  // 處決期間玩家位置同步
                  MatchPosition();
      
                  // 處決攻擊輸入
                  CharacterFinishAttackInput();
              }
      

      處決動畫

      玩家

      1755968243920

      1755968256271

      1755972751927

      1755972725828

      敵人

      1755968354767

      1755968363466

      1755973192394

      1755973208464

      創建處決技能SO

      技能

      1755969028648

      技能表

      1755969042295

      動畫行為腳本——處決時忽略碰撞體

      IgnoreCollider

      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      
      public class IgnoreCollider : StateMachineBehaviour
      {
      
          [SerializeField]
          private int _selfLayer;  // 自身所在的層
      
          [SerializeField]
          private int[] _targetLayers;  // 要處理碰撞忽略的目標層數組
      
          /// <summary>
          /// 當進入動畫狀態時調用,處理層碰撞忽略
          /// </summary>
          /// <param name="animator">動畫組件</param>
          /// <param name="stateInfo">動畫狀態信息</param>
          /// <param name="layerIndex">層索引</param>
          override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
          {
              // 遍歷目標層數組,設置忽略自身層與目標層的碰撞
              foreach (int targetLayer in _targetLayers)
              {
                  Physics.IgnoreLayerCollision(_selfLayer, targetLayer, true);
              }
          }
      
          /// <summary>
          /// 當退出動畫狀態時調用,恢復層碰撞(取消忽略)
          /// </summary>
          /// <param name="animator">動畫組件</param>
          /// <param name="stateInfo">動畫狀態信息</param>
          /// <param name="layerIndex">層索引</param>
          override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
          {
              // 遍歷目標層數組,恢復自身層與目標層的碰撞(設置為不忽略)
              foreach (int targetLayer in _targetLayers)
              {
                  Physics.IgnoreLayerCollision(_selfLayer, targetLayer, false);
              }
          }
      }
      
      

      1755969692145

      1755969720328

      需要根據實際的Layer去填寫

      修改bug——處決時其他敵人也受到傷害的問題

      CharacterHealthBase

                  GameEventManager.MainInstance.AddEventListening<float,Transform>
                      ("觸發處決傷害", TriggerDamageEventHandler);
      
                  GameEventManager.MainInstance.RemoveEvent<float,Transform>
                      ("觸發處決傷害", TriggerDamageEventHandler);
      
              /// <summary>
              /// 處決動畫的觸發傷害事件
              /// </summary>
              /// <param name="damage"></param>
              private void TriggerDamageEventHandler(float damage, Transform self)
              {
                  // 如果傳來的self不是當前對象,說明不是自身在受擊
                  if(self != transform)
                      return;
                  // 處理傷害扣除邏輯
                  TakeDamage(damage);
                  // 播放受擊音效
                  GamePoolManager.Instance.TryGetPoolItem("HitSound", transform.position, Quaternion.identity);
              }
      

      PlayerCombatControl

      region 觸發傷害(普通攻擊和處決攻擊)

          private void TriggerDamage()
      
                  else
                  {               // 同一處決動畫期間會觸發多次傷害
                      //處決攻擊
                      // 從處決數據中獲取連招傷害相關參數
                      float damageValue = _finishCombo.TryGetComboDamage(_currentComboIndex);
                      // 調用觸發處決傷害事件
                      GameEventManager.MainInstance.CallEvent("觸發處決傷害", damageValue, _currentEnemy);
                      Debug.Log("觸發處決傷害");
                  }
      

      Day17 背后處決

      玩家

      1756000026718

      1756000015998

      1756000053751

      1756000310031

      敵人

      1756000197030

      1756000207510

      1756000280959

      1756000297412

      加一個動畫參數

      1756000349460

      技能表

      1756000585531

      1756000593387

      背后處決邏輯

      PlayerCombatControl

              [Header("角色背后處決連招配置")]
              [SerializeField] private CharacterComboSO _assassinCombo;       // 背后處決連招
      

      處決期間位置同步MatchPosition():加上背后的目標位置匹配

                  else if (_animator.AnimationAtTag("Assassin"))//當前在普通攻擊動畫
                  {
                      transform.rotation = Quaternion.LookRotation(_currentEnemy.forward);    // 背對敵人
                      RunningMatch(_assassinCombo);
                  }
      
              #region 暗殺
              /// <summary>
              /// 是否允許執行暗殺邏輯的條件判斷
              /// </summary>
              private bool CanAssassin()
              {
                  // 1. 無目標時不允許
                  if (_currentEnemy == null)
                      return false;
      
                  // 2. 距離超過 2f 時不允許
                  float distance = DevelopmentTools.DistanceForTarget(_currentEnemy, transform);
                  if (distance > 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;
              }
      
              /// <summary>
              /// 處理角色暗殺輸入的響應邏輯
              /// </summary>
              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
      

      修改之前的正面處決條件,加上角度限制

              /// <summary>
              /// 是否允許執行處決攻擊
              /// </summary>
              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索引單獨控制

              // 處決執行狀態變量
              private int _finishComboIndex;  // 處決連招動作索引
      
              /// <summary>
              /// 處理角色暗殺輸入的響應邏輯
              /// </summary>
              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 觸發傷害(普通攻擊和處決攻擊)

          private void TriggerDamage()
      
                  else
                  {               // 同一處決動畫期間會觸發多次傷害
                      //處決攻擊
                      // 從處決數據中獲取連招傷害相關參數
                      float damageValue = _finishCombo.TryGetComboDamage(_finishComboIndex);
                      // 調用觸發處決傷害事件
                      GameEventManager.MainInstance.CallEvent("觸發處決傷害", damageValue,_currentEnemy);
                      Debug.Log("觸發處決傷害");
                  }
                  #endregion
      
              #region 位置同步
              /// <summary>
              /// 處決期間玩家位置同步
              /// </summary>
              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
      
              /// <summary>
              /// 重置連招狀態(索引、冷卻時間)
              /// </summary>
              private void ResetComboInfo()
              {
                  _currentComboIndex = 0;
                  _maxColdTime = 0f;
                  _hitIndex = 0;
                  _finishComboIndex = 0;
              }
      
              /// <summary>
              /// 處理角色處決攻擊的輸入響應邏輯
              /// </summary>
              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

      1755982328187

      1755982526804

      1755982402802

      隨機待機動畫系統

      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<Animator>();
          }
      
          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);
          }
      }
      
      

      演示效果(倍速處理)

      1755995639305

      開啟物理碰撞交互

      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);
              }
          }
      }
      
      

      1756014981848

      加上手部扶墻的程序動畫剛好完美推門(不過要記得的刪去非法角度的判斷)

      注意,開啟物理碰撞之后,之前的扶墻物體需要把Kinematic打開,讓他固定住不受外力,不然墻倒了

      1756015456530

      不過推門還是盡量做單獨的animator動畫比較好,用扶墻的程序動畫看著還行,但還是用固定的動畫不容易出戲

      1756016306243

      posted @ 2025-08-24 16:18  EanoJiang  閱讀(78)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 亚洲精品日韩久久精品| 国产精品久久久久久久专区| 国产精品一区二区三区黄| 大伊香蕉在线精品视频75| 日韩精品卡1卡2日韩在线| 国产乱码一区二区三区| 美日韩精品综合一区二区| 亚洲高清成人av在线| 韩国无码AV片午夜福利| 国产精品高清一区二区三区| 国产一区二区高潮视频| 国产农村老熟女国产老熟女 | 中文字幕日韩有码av| 国产国语一级毛片| 怡春院欧美一区二区三区免费 | 欧美人与zoxxxx另类| av无码精品一区二区三区宅噜噜| 粉嫩蜜臀av一区二区三区| 中文字幕日韩有码国产| 亚洲欧美自偷自拍视频图片| 无码av中文字幕久久专区| 久久夜夜免费视频| 亚洲最大成人av在线天堂网| 大香伊蕉在人线国产av | 无套内谢少妇高清毛片| 无码国模国产在线观看免费| 久久久无码精品亚洲日韩蜜臀浪潮| 欧美gv在线| 在线高清免费不卡全码| 精品无码国产一区二区三区51安| 亚洲国产av永久精品成人| 九九热在线观看视频免费| 国产精品爽黄69天堂A| 久久亚洲私人国产精品| 97久久久亚洲综合久久| 99久9在线视频 | 传媒| 少妇粗大进出白浆嘿嘿视频| 视频一区视频二区视频三区| 国产又色又爽又黄的视频在线 | 国产免费人成网站在线播放| 国产成人亚洲无码淙合青草|