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

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

明思义 2025-9-28 18:00:26
新版输入系统——斜向移动变快问题解决

1.png

生成对应的input管理脚本
2.png

Day 01——角色移动基类

CharacterMovementControlBase
  1. using UnityEngine;
  2. namespace Spiderman.Movement
  3. {
  4.     [RequireComponent(typeof(CharacterController))]
  5.     public abstract class CharacterMovementControlBase : MonoBehaviour
  6.     {
  7.         // 角色控制器组件,用于处理角色移动相关的物理交互
  8.         private CharacterController _controller;
  9.         // 动画组件,用于控制角色动画播放
  10.         private Animator _animator;
  11.         // 地面检测相关变量
  12.         protected bool _characterIsOnGround;
  13.         [Header("地面检测相关变量")]
  14.         [SerializeField]protected float _groundDetectionPositionOffset; // 地面检测位置偏移量
  15.         [SerializeField]protected float _detectionRang;                 // 地面检测范围
  16.         [SerializeField]protected LayerMask _whatIsGround;              // 地面层掩码
  17.         // 重力相关变量
  18.         protected readonly float CharacterGravity = -9.8f;
  19.         protected float _characterVerticalVelocity;     // 角色垂直方向速度
  20.         protected float _fallOutDeltaTime;              // 下落 delta 时间,用于计算重力作用的时间积累
  21.         protected float _fallOutTime = 0.15f;           // 下落等待时间,控制跌落动画播放时机
  22.         protected readonly float _characterVerticalMaxVelocity = 54f; // 角色最大垂直速度,低于这个值应用重力
  23.         protected Vector3 _characterVerticalDirection;  // 角色Y轴移动方向,通过charactercontroller.move来实现y轴移动
  24.         // 初始化函数,在对象实例化后、Start 之前调用,获取必要组件
  25.         protected virtual void Awake()
  26.         {
  27.             _controller = GetComponent<CharacterController>();
  28.             _animator = GetComponent();
  29.         }
  30.         protected virtual void Start()
  31.         {
  32.             _fallOutDeltaTime = _fallOutTime;
  33.         }
  34.         private void Update()
  35.         {
  36.             SetCharacterGravity();
  37.             UpdateCharacterGravity();
  38.         }
  39.         /// <summary>
  40.         /// 地面检测方法
  41.         /// </summary>
  42.         /// <returns>返回角色是否在地面的布尔值</returns>
  43.         private bool GroundDetection()
  44.         {
  45.             // 构建检测位置:基于角色当前位置,调整 Y 轴偏移(用于地面检测的位置修正)
  46.             Vector3 detectionPosition = new Vector3(
  47.                 transform.position.x,
  48.                 transform.position.y - _groundDetectionPositionOffset,
  49.                 transform.position.z
  50.             );
  51.             // 球形检测:检查在指定位置、指定半径范围内,与 _whatIsGround 层的碰撞体是否存在相交
  52.             // 参数分别为:检测中心、检测半径、地面层掩码、忽略触发器交互
  53.             return Physics.CheckSphere(
  54.                 detectionPosition,
  55.                 _detectionRang,
  56.                 _whatIsGround,
  57.                 QueryTriggerInteraction.Ignore
  58.             );
  59.         }
  60.         /// <summary>
  61.         /// 根据是否在地面设置对应的角色重力逻辑
  62.         /// </summary>
  63.         private void SetCharacterGravity()
  64.         {
  65.             // 检测角色是否在地面
  66.             _characterIsOnGround = GroundDetection();
  67.             if (_characterIsOnGround)
  68.             {
  69.                 //1.在地面
  70.                 // 1.1 重置下落等待时间
  71.                 _fallOutDeltaTime = _fallOutTime;
  72.                 // 1.2 重置垂直速度(防止落地后持续累积速度)
  73.                 if (_characterVerticalVelocity < 0)
  74.                 {
  75.                     _characterVerticalVelocity = -2f;
  76.                 }
  77.             }
  78.             else
  79.             {
  80.                 //2.不在地面
  81.                 if (_fallOutDeltaTime > 0)
  82.                 {
  83.                     // 2.1 处理楼梯/小落差:等待 0.15 秒后再应用重力
  84.                     _fallOutDeltaTime -= Time.deltaTime;
  85.                 }
  86.                 else
  87.                 {
  88.                     // 2.2 倒计时结束还没有落地?那说明不是小落差,要开始应用重力
  89.                 }
  90.                 if (_characterVerticalVelocity < _characterVerticalMaxVelocity)
  91.                 {
  92.                     _characterVerticalVelocity += CharacterGravity * Time.deltaTime;
  93.                     // 重力公式累积垂直速度
  94.                 }
  95.             }
  96.         }
  97.         /// <summary>
  98.         /// 更新角色垂直方向移动(应用重力效果)
  99.         /// </summary>
  100.         private void UpdateCharacterGravity()
  101.         {
  102.             //这里只处理 y 轴重力
  103.             // x/z 由其他移动逻辑控制
  104.             Vector3 _characterVerticalDirection = new Vector3(0, _characterVerticalVelocity, 0);
  105.             // 通过 CharacterController 应用y轴移动
  106.             _controller.Move(_characterVerticalDirection * Time.deltaTime);
  107.         }
  108.         /// <summary>
  109.         /// 斜坡方向重置:检测角色是否在坡上移动,防止下坡速度过快导致异常
  110.         /// </summary>
  111.         /// <param name="moveDirection">原始移动方向</param>
  112.         /// <returns>适配斜坡后的移动方向</returns>
  113.         private Vector3 SlopResetDirection(Vector3 moveDirection)
  114.         {
  115.             // 射线检测参数配置
  116.             Vector3 rayOrigin = transform.position + transform.up * 0.5f;   // 射线起点
  117.             Vector3 rayDirection = Vector3.down;                            // 射线方向
  118.             float maxDistance = _controller.height * 0.85f;                 // 射线最大距离
  119.             LayerMask targetLayer = _whatIsGround;                          // 检测的目标地面层
  120.             QueryTriggerInteraction triggerInteraction = QueryTriggerInteraction.Ignore; // 忽略触发器
  121.             // 执行向下的射线检测
  122.             if (Physics.Raycast(rayOrigin, rayDirection, out RaycastHit hit, maxDistance, targetLayer, triggerInteraction))
  123.             {
  124.                 // 点积判断:检测地面法线是否与角色上方向垂直(点积接近0表示垂直,非0则说明有坡度)
  125.                 if (Vector3.Dot(transform.up, hit.normal) != 0)
  126.                 {
  127.                     // 将移动方向投影到斜坡平面
  128.                     moveDirection = Vector3.ProjectOnPlane(moveDirection, hit.normal);
  129.                 }
  130.             }
  131.             return moveDirection;
  132.         }
  133.         private void OnDrawGizmos()
  134.         {
  135.             // 设置gizmos颜色为红色,使其更容易看到
  136.             Gizmos.color = Color.red;
  137.   
  138.             Vector3 detectionPosition = new Vector3(
  139.                 transform.position.x,
  140.                 transform.position.y - _groundDetectionPositionOffset,
  141.                 transform.position.z
  142.             );
  143.             Gizmos.DrawWireSphere(detectionPosition, _detectionRang);
  144.         }
  145.     }
  146. }
复制代码
PlayerMovementControl
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. namespace Spiderman.Movement
  5. {
  6.     public class PlayerMovementControl : CharacterMovementControlBase
  7.     {
  8.     }
  9. }
复制代码
Day02 带碰撞体相机脚本

GameInputManager
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class GameInputManager : MonoBehaviour
  5. {
  6.     private GameInputAction _gameInputAction;
  7.     public Vector2 Movement => _gameInputAction.Player.Movement.ReadValue<Vector2>();
  8.     public Vector2 CameraLook => _gameInputAction.Player.CameraLook.ReadValue<Vector2>();
  9.     private void Awake()
  10.     {
  11.         _gameInputAction ??= new GameInputAction(); //是空的,则创建新的实例
  12.     }
  13.     private void OnEnable()
  14.     {
  15.         _gameInputAction.Enable();
  16.     }
  17.     private void OnDisable()
  18.     {
  19.         _gameInputAction.Disable();
  20.     }
  21. }
复制代码
3.png

4.png

5.png

TP_CameraControl
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class TP_CameraControl : MonoBehaviour
  5. {
  6.     private GameInputManager _gameInputManager;
  7.     [Header("相机参数配置")]
  8.     [SerializeField] private Transform _lookTarget;             //相机跟随目标
  9.     [SerializeField] private float _controlSpeed;               //相机移动速度
  10.     [SerializeField] private Vector2 _cameraVerticalMaxAngle;   //相机上下旋转角度限制
  11.     [SerializeField] private Vector2 _cameraHorizontalMaxAngle; //相机左右旋转角度限制
  12.     [SerializeField] private float _smoothSpeed;                //平滑速度
  13.     [SerializeField] private float _cameraDistance;             //相机到跟随目标的距离
  14.     [SerializeField] private float _cameraHeight;               //相机高度
  15.     [SerializeField] private float _DistancemoothTime;         //位置跟随平滑时间
  16.     private Vector3 smoothDampVelocity = Vector3.zero;          //旋转阻尼
  17.     private Vector2 _input;                                     // 输入值
  18.     private Vector3 _cameraRotation;                            // 相机旋转方向
  19.     private bool _cameraInputEnabled = true;                    // 相机输入是否启用
  20.     private void Awake()
  21.     {
  22.         // 获取游戏输入管理组件
  23.         _gameInputManager = GetComponent<GameInputManager>();
  24.         //隐藏光标
  25.         Cursor.lockState = CursorLockMode.Locked;
  26.         Cursor.visible = false;
  27.     }
  28.     private void Update()
  29.     {
  30.         // 检测到按下ESC键或鼠标左键点击窗口,则切换相机输入状态
  31.         HandleCameraInputToggle();
  32.         // 只有在相机输入启用时才处理输入
  33.         if (_cameraInputEnabled)
  34.         {
  35.             // 实时处理相机输入
  36.             CameraInput();
  37.         }
  38.     }
  39.     private void LateUpdate()
  40.     {
  41.         // 更新相机旋转
  42.         UpdateCameraRotation();
  43.         // 更新相机位置
  44.         UpdateCameraPosition();
  45.     }
  46.     /// <summary>
  47.     /// 处理相机输入,获取并处理上下查看等输入,限制垂直角度范围
  48.     /// </summary>
  49.     private void CameraInput()
  50.     {
  51.         // 获取相机xy轴输入
  52.         _input.y += _gameInputManager.CameraLook.x * _controlSpeed;
  53.         _input.x -= _gameInputManager.CameraLook.y * _controlSpeed;
  54.         // 限制相机垂直方向角度范围,垂直方向是绕 x 轴旋转,所以平滑的是x轴输入
  55.         _input.x = Mathf.Clamp(
  56.             _input.x,
  57.             _cameraVerticalMaxAngle.x,
  58.             _cameraVerticalMaxAngle.y
  59.         );
  60.         // 限制相机水平方向角度范围,水平方向是绕 y 轴旋转,所以限制的是y轴输入
  61.         _input.y = Mathf.Clamp(
  62.             _input.y,
  63.             _cameraHorizontalMaxAngle.x,
  64.             _cameraHorizontalMaxAngle.y
  65.         );
  66.     }
  67.     /// <summary>
  68.     /// 更新相机旋转
  69.     /// </summary>
  70.     private void UpdateCameraRotation()
  71.     {
  72.         var targetRotation = new Vector3(_input.x, _input.y, 0);
  73.         _cameraRotation = Vector3.SmoothDamp(
  74.             _cameraRotation,
  75.             targetRotation,
  76.             ref smoothDampVelocity,
  77.             _smoothSpeed
  78.         );
  79.         //更新相机欧拉角
  80.         transform.eulerAngles = _cameraRotation;
  81.     }
  82.     /// <summary>
  83.     /// 更新相机位置
  84.     /// </summary>
  85.     private void UpdateCameraPosition()
  86.     {
  87.         var newPos = _lookTarget.position
  88.             + Vector3.back * _cameraDistance
  89.             + Vector3.up * _cameraHeight;
  90.         // 平滑位置移动
  91.         transform.position = Vector3.Lerp(
  92.             transform.position,
  93.             newPos,
  94.             _DistancemoothTime
  95.         );
  96.     }
  97.     /// <summary>
  98.     /// 处理相机输入状态切换
  99.     /// </summary>
  100.     private void HandleCameraInputToggle()
  101.     {
  102.         // 检测ESC键切换相机输入状态
  103.         if (Input.GetKeyDown(KeyCode.Escape))
  104.         {
  105.             _cameraInputEnabled = false;
  106.             // 显示光标并解锁
  107.             Cursor.lockState = CursorLockMode.None;
  108.             Cursor.visible = true;
  109.         }
  110.         // 检测鼠标左键点击窗口来恢复相机控制
  111.         if (Input.GetMouseButtonDown(0) && !_cameraInputEnabled)
  112.         {
  113.             _cameraInputEnabled = true;
  114.             // 隐藏光标并锁定
  115.             Cursor.lockState = CursorLockMode.Locked;
  116.             Cursor.visible = false;
  117.         }
  118.     }
  119. }
复制代码
加入摄像机碰撞逻辑

GameInputManager继承于单例模式
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using GGG.Tool.Singleton;
  5. public class GameInputManager : Singleton<GameInputManager>
  6. {
  7.     private GameInputAction _gameInputAction;
  8.     public Vector2 Movement => _gameInputAction.Player.Movement.ReadValue<Vector2>();
  9.     public Vector2 CameraLook => _gameInputAction.Player.CameraLook.ReadValue<Vector2>();
  10.     private void Awake()
  11.     {
  12.         base.Awake();
  13.         _gameInputAction ??= new GameInputAction(); //是空的,则创建新的实例
  14.     }
  15.     private void OnEnable()
  16.     {
  17.         _gameInputAction.Enable();
  18.     }
  19.     private void OnDisable()
  20.     {
  21.         _gameInputAction.Disable();
  22.     }
  23. }
复制代码
TP_CameraControl
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using GGG.Tool;
  5. public class TP_CameraControl : MonoBehaviour
  6. {
  7.     [Header("相机参数配置")]
  8.     [SerializeField] private Transform _lookTarget;             //相机跟随目标
  9.     [SerializeField] private float _controlSpeed;               //相机移动速度
  10.     [SerializeField] private Vector2 _cameraVerticalMaxAngle;   //相机上下旋转角度限制
  11.     [SerializeField] private Vector2 _cameraHorizontalMaxAngle; //相机左右旋转角度限制
  12.     [SerializeField] private float _smoothSpeed;                //平滑速度
  13.     [SerializeField] private float _cameraDistance;             //相机到跟随目标的距离
  14.     [SerializeField] private float _cameraHeight;               //相机高度
  15.     [SerializeField] private float _distanceSmoothTime;         //位置跟随平滑时间
  16.     private Vector3 smoothDampVelocity = Vector3.zero;          //旋转阻尼
  17.     private Vector2 _input;                                     // 输入值
  18.     private Vector3 _cameraRotation;                            // 相机旋转方向
  19.     private bool _cameraInputEnabled = true;                    // 相机输入是否启用
  20.     private void Awake()
  21.     {
  22.         //隐藏光标
  23.         Cursor.lockState = CursorLockMode.Locked;
  24.         Cursor.visible = false;
  25.     }
  26.     private void Update()
  27.     {
  28.         // 检测到按下ESC键或鼠标左键点击窗口,则切换相机输入状态
  29.         HandleCameraInputToggle();
  30.         // 只有在相机输入启用时才处理输入
  31.         if (_cameraInputEnabled)
  32.         {
  33.             // 实时处理相机输入
  34.             CameraInput();
  35.         }
  36.     }
  37.     private void LateUpdate()
  38.     {
  39.         // 更新相机旋转
  40.         UpdateCameraRotation();
  41.         // 更新相机位置
  42.         UpdateCameraPosition();
  43.     }
  44.     /// <summary>
  45.     /// 处理相机输入,获取并处理上下查看等输入,限制垂直角度范围
  46.     /// </summary>
  47.     private void CameraInput()
  48.     {
  49.         // 获取相机xy轴输入
  50.         _input.y += GameInputManager.MainInstance.CameraLook.x * _controlSpeed;
  51.         _input.x -= GameInputManager.MainInstance.CameraLook.y * _controlSpeed;
  52.         // 限制相机垂直方向角度范围,垂直方向是绕 x 轴旋转,所以平滑的是x轴输入
  53.         _input.x = Mathf.Clamp(
  54.             _input.x,
  55.             _cameraVerticalMaxAngle.x,
  56.             _cameraVerticalMaxAngle.y
  57.         );
  58.         // 限制相机水平方向角度范围,水平方向是绕 y 轴旋转,所以限制的是y轴输入
  59.         _input.y = Mathf.Clamp(
  60.             _input.y,
  61.             _cameraHorizontalMaxAngle.x,
  62.             _cameraHorizontalMaxAngle.y
  63.         );
  64.     }
  65.     /// <summary>
  66.     /// 更新相机旋转
  67.     /// </summary>
  68.     private void UpdateCameraRotation()
  69.     {
  70.         var targetRotation = new Vector3(_input.x, _input.y, 0);
  71.         _cameraRotation = Vector3.SmoothDamp(
  72.             _cameraRotation,
  73.             targetRotation,
  74.             ref smoothDampVelocity,
  75.             _smoothSpeed
  76.         );
  77.         //更新相机欧拉角
  78.         transform.eulerAngles = _cameraRotation;
  79.     }
  80.     /// <summary>
  81.     /// 更新相机位置
  82.     /// </summary>
  83.     private void UpdateCameraPosition()
  84.     {
  85.         var newPos = _lookTarget.position
  86.             + Vector3.back * _cameraDistance
  87.             + Vector3.up * _cameraHeight;
  88.         // 平滑位置移动
  89.         transform.position = Vector3.Lerp(
  90.             transform.position,
  91.             newPos,
  92.             DevelopmentToos.UnTetheredLerp(_distanceSmoothTime)
  93.         );
  94.     }
  95.     /// <summary>
  96.     /// 处理相机输入状态切换
  97.     /// </summary>
  98.     private void HandleCameraInputToggle()
  99.     {
  100.         // 检测ESC键切换相机输入状态
  101.         if (Input.GetKeyDown(KeyCode.Escape))
  102.         {
  103.             _cameraInputEnabled = false;
  104.             // 显示光标并解锁
  105.             Cursor.lockState = CursorLockMode.None;
  106.             Cursor.visible = true;
  107.         }
  108.         // 检测鼠标左键点击窗口来恢复相机控制
  109.         if (Input.GetMouseButtonDown(0) && !_cameraInputEnabled)
  110.         {
  111.             _cameraInputEnabled = true;
  112.             // 隐藏光标并锁定
  113.             Cursor.lockState = CursorLockMode.Locked;
  114.             Cursor.visible = false;
  115.         }
  116.     }
  117. }
复制代码
Day03 Movement

动画部分

6.png

7.png

8.png

9.png
10.png

脚本

CharacterMovementControlBase
  1.         protected Vector3 _moveDirection; // 角色移动方向
复制代码
  1.         /// <summary>
  2.         /// 脚本控制animator的根运动
  3.         /// </summary>
  4.         protected virtual void OnAnimatorMove()
  5.         {
  6.             _animator.ApplyBuiltinRootMotion();
  7.             UpdateCharacterMoveDirection(_animator.deltaPosition);
  8.         }
复制代码
  1.         /// <summary>
  2.         /// 更新角色水平移动方向——绕y轴旋转
  3.         /// </summary>
  4.         protected void UpdateCharacterMoveDirection(Vector3 direction)
  5.         {
  6.             _moveDirection = SlopResetDirection(direction);
  7.             _controller.Move(_moveDirection * Time.deltaTime);
  8.         }
复制代码
GameInputManager
  1.     public bool Run => _gameInputAction.Player.Run.triggered;
复制代码
PlayerMovementControl
  1. using GGG.Tool;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. namespace Spiderman.Movement
  6. {
  7.     public class PlayerMovementControl : CharacterMovementControlBase
  8.     {
  9.         [SerializeField] float moveSpeed = 1.5f;
  10.         // 角色旋转角度(绕 Y 轴)
  11.         private float _rotationAngle;
  12.         // 旋转角速度
  13.         private float _angleVelocity = 0;
  14.         // 旋转平滑时间
  15.         [SerializeField] private float _rotationSmoothTime;
  16.         private Transform _mainCamera;
  17.         protected override void Awake()
  18.         {
  19.             base.Awake();
  20.             _mainCamera = Camera.main.transform;
  21.         }
  22.         private void LateUpdate()
  23.         {
  24.             UpdateAnimation();
  25.             CharacterRotationControl();
  26.         }
  27.         /// <summary>
  28.         /// 角色旋转控制
  29.         /// </summary>
  30.         private void CharacterRotationControl()
  31.         {
  32.             // 不在地面时直接返回,不处理旋转
  33.             if (!_characterIsOnGround)
  34.                 return;
  35.             // 处理输入存在时的旋转角度计算
  36.             if (_animator.GetBool("HasInput"))
  37.             {
  38.                 _rotationAngle =
  39.                     Mathf.Atan2(
  40.                         GameInputManager.MainInstance.Movement.x,
  41.                         GameInputManager.MainInstance.Movement.y
  42.                     ) * Mathf.Rad2Deg
  43.                     + _mainCamera.eulerAngles.y;          // 计算角色的旋转角度(弧度转角度)
  44.   
  45.             }
  46.             // 满足HasInput==true且处于“Motion”动画标签时,平滑更新角色旋转
  47.             if (_animator.GetBool("HasInput") && _animator.AnimationAtTag("Motion"))
  48.             {
  49.                 transform.eulerAngles = Vector3.up
  50.                                         * Mathf.SmoothDampAngle(
  51.                                             transform.eulerAngles.y,
  52.                                             _rotationAngle,
  53.                                             ref _angleVelocity,
  54.                                             _rotationSmoothTime
  55.                                         );
  56.             }
  57.         }
  58.         /// <summary>
  59.         /// 更新动画
  60.         /// </summary>
  61.         private void UpdateAnimation()
  62.         {
  63.             if (!_characterIsOnGround)
  64.                 return;
  65.             _animator.SetBool("HasInput", GameInputManager.MainInstance.Movement != Vector2.zero);
  66.             if (_animator.GetBool("HasInput"))
  67.             {
  68.                 if (GameInputManager.MainInstance.Run)
  69.                 {
  70.                     //按下奔跑键
  71.                     _animator.SetBool("Run",true);
  72.                 }
  73.                 //有输入
  74.                 //  Run被开启,那就Movement设置为2,否则设置为输入的两个轴的平方
  75.                 var targetSpeed = _animator.GetBool("Run") ? 2f :GameInputManager.MainInstance.Movement.sqrMagnitude;
  76.                 _animator.SetFloat(
  77.                     "Movement",
  78.                     targetSpeed / _animator.humanScale * moveSpeed,
  79.                     0.25f,
  80.                     Time.deltaTime
  81.                 );
  82.             }
  83.             else
  84.             {
  85.                 //无输入
  86.                 _animator.SetFloat("Movement", 0f, 0.25f, Time.deltaTime);
  87.                 if (_animator.GetFloat("Movement") < 0.2f)
  88.                 {
  89.                     _animator.SetBool("Run", false);
  90.                 }
  91.             }
  92.         }
  93.     }
  94. }
复制代码
Day04  事件管理器

GameEventManager
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using System;
  5. using GGG.Tool;
  6. using GGG.Tool.Singleton;
  7. public class GameEventManager : SingletonNonMono<GameEventManager>
  8. {
  9.     // 事件接口
  10.     private interface IEventHelp
  11.     {
  12.     }
  13.     // 事件类,实现 IEventHelp 接口,用于管理事件注册、调用等逻辑
  14.     private class EventHelp : IEventHelp
  15.     {
  16.         // 存储事件委托
  17.         private event Action _action;
  18.         // 构造函数,初始化时绑定初始事件逻辑
  19.         public EventHelp(Action action)
  20.         {
  21.             // 首次实例化时赋值,仅执行这一次初始绑定
  22.             _action = action;
  23.         }
  24.         // 增加事件注册的方法,将新的事件逻辑追加到委托中
  25.         public void AddCall(Action action)
  26.         {
  27.             _action += action;
  28.         }
  29.         // 调用事件的方法,若有绑定逻辑则执行
  30.         public void Call()
  31.         {
  32.             _action?.Invoke();
  33.         }
  34.         // 移除事件的方法,将指定事件逻辑从委托中移除
  35.         public void Remove(Action action)
  36.         {
  37.             _action -= action;
  38.         }
  39.     }
  40.     private class EventHelp<T> : IEventHelp
  41.     {
  42.         // 存储事件委托
  43.         private event Action<T> _action;
  44.         // 构造函数,初始化时绑定初始事件逻辑
  45.         public EventHelp(Action<T> action)
  46.         {
  47.             // 首次实例化时赋值,仅执行这一次初始绑定
  48.             _action = action;
  49.         }
  50.         // 增加事件注册的方法,将新的事件逻辑追加到委托中
  51.         public void AddCall(Action<T> action)
  52.         {
  53.             _action += action;
  54.         }
  55.         // 调用事件的方法,若有绑定逻辑则执行
  56.         public void Call(T value)
  57.         {
  58.             _action?.Invoke(value);
  59.         }
  60.         // 移除事件的方法,将指定事件逻辑从委托中移除
  61.         public void Remove(Action<T> action)
  62.         {
  63.             _action -= action;
  64.         }
  65.     }
  66.     private class EventHelp<T1, T2> : IEventHelp
  67.     {
  68.         // 存储事件委托
  69.         private event Action<T1, T2> _action;
  70.         // 构造函数,初始化时绑定初始事件逻辑
  71.         public EventHelp(Action<T1, T2> action)
  72.         {
  73.             // 首次实例化时赋值,仅执行这一次初始绑定
  74.             _action = action;
  75.         }
  76.         // 增加事件注册的方法,将新的事件逻辑追加到委托中
  77.         public void AddCall(Action<T1, T2> action)
  78.         {
  79.             _action += action;
  80.         }
  81.         // 调用事件的方法,若有绑定逻辑则执行
  82.         public void Call(T1 value1, T2 value2)
  83.         {
  84.             _action?.Invoke(value1, value2);
  85.         }
  86.         // 移除事件的方法,将指定事件逻辑从委托中移除
  87.         public void Remove(Action<T1, T2> action)
  88.         {
  89.             _action -= action;
  90.         }
  91.     }
  92.     /// <summary>
  93.     /// 事件中心,用于管理事件注册、调用
  94.     /// </summary>
  95.     private Dictionary<string, IEventHelp> _eventCenter = new Dictionary<string, IEventHelp>();
  96.     /// <summary>
  97.     /// 添加事件监听
  98.     /// </summary>
  99.     /// <param name="eventName">事件名称</param>
  100.     /// <param name="action">回调函数</param>
  101.     public void AddEventListening(string eventName, Action action)
  102.     {
  103.         if (_eventCenter.TryGetValue(eventName, out var eventHelp))
  104.         {
  105.             (eventHelp as EventHelp)?.AddCall(action);
  106.         }
  107.         else
  108.         {
  109.             // 如果事件中心不存在叫这个名字的事件,new一个然后添加
  110.             _eventCenter.Add(eventName, new EventHelp(action));
  111.         }
  112.     }
  113.     public void AddEventListening<T>(string eventName, Action<T> action)
  114.     {
  115.         if (_eventCenter.TryGetValue(eventName, out var eventHelp))
  116.         {
  117.             (eventHelp as EventHelp<T>)?.AddCall(action);
  118.         }
  119.         else
  120.         {
  121.             // 如果事件中心不存在叫这个名字的事件,new一个然后添加
  122.             _eventCenter.Add(eventName, new EventHelp<T>(action));
  123.         }
  124.     }
  125.     public void AddEventListening<T1, T2>(string eventName, Action<T1, T2> action)
  126.     {
  127.         if (_eventCenter.TryGetValue(eventName, out var eventHelp))
  128.         {
  129.             (eventHelp as EventHelp<T1, T2>)?.AddCall(action);
  130.         }
  131.         else
  132.         {
  133.             // 如果事件中心不存在叫这个名字的事件,new一个然后添加
  134.             _eventCenter.Add(eventName, new EventHelp<T1, T2>(action));
  135.         }
  136.     }
  137.     /// <summary>
  138.     /// 调用事件
  139.     /// </summary>
  140.     /// <param name="eventName">事件名称</param>
  141.     public void CallEvent(string eventName)
  142.     {
  143.         if (_eventCenter.TryGetValue(eventName, out var eventHelp))
  144.         {
  145.             (eventHelp as EventHelp)?.Call();
  146.         }
  147.         else
  148.         {
  149.             LogEventNotFound(eventName, "调用");
  150.         }
  151.     }
  152.     public void CallEvent<T>(string eventName, T value)
  153.     {
  154.         if (_eventCenter.TryGetValue(eventName, out var eventHelp))
  155.         {
  156.             (eventHelp as EventHelp<T>)?.Call(value);
  157.         }
  158.         else
  159.         {
  160.             LogEventNotFound(eventName, "调用");
  161.         }
  162.     }
  163.     public void CallEvent<T1, T2>(string eventName, T1 value, T2 value1)
  164.     {
  165.         if (_eventCenter.TryGetValue(eventName, out var eventHelp))
  166.         {
  167.             (eventHelp as EventHelp<T1, T2>)?.Call(value, value1);
  168.         }
  169.         else
  170.         {
  171.             LogEventNotFound(eventName, "调用");
  172.         }
  173.     }
  174.     /// <summary>
  175.     /// 移除事件监听
  176.     /// </summary>
  177.     /// <param name="eventName">事件名称</param>
  178.     /// <param name="action">要移除的事件回调</param>
  179.     public void RemoveEvent(string eventName, Action action)
  180.     {
  181.         if (_eventCenter.TryGetValue(eventName, out var eventHelp))
  182.         {
  183.             (eventHelp as EventHelp)?.Remove(action);
  184.         }
  185.         else
  186.         {
  187.             LogEventNotFound(eventName, "移除");
  188.         }
  189.     }
  190.     public void RemoveEvent<T>(string eventName, Action<T> action)
  191.     {
  192.         if (_eventCenter.TryGetValue(eventName, out var eventHelp))
  193.         {
  194.             (eventHelp as EventHelp<T>)?.Remove(action);
  195.         }
  196.         else
  197.         {
  198.             LogEventNotFound(eventName, "移除");
  199.         }
  200.     }
  201.     public void RemoveEvent<T1, T2>(string eventName, Action<T1, T2> action)
  202.     {
  203.         if (_eventCenter.TryGetValue(eventName, out var eventHelp))
  204.         {
  205.             (eventHelp as EventHelp<T1, T2>)?.Remove(action);
  206.         }
  207.         else
  208.         {
  209.             LogEventNotFound(eventName, "移除");
  210.         }
  211.     }
  212.     /// <summary>
  213.     /// 事件未找到时的统一日志输出
  214.     /// </summary>
  215.     /// <param name="eventName">事件名称</param>
  216.     /// <param name="operation">操作类型(移除、调用)</param>
  217.     private void LogEventNotFound(string eventName, string operation)
  218.     {
  219.         DevelopmentTools.WTF($"当前未找到{eventName}的事件,无法{operation}");
  220.     }
  221. }
复制代码
Day05 AnimationStringToHash
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. /// <summary>
  5. /// 动画参数哈希值管理类,用于统一存储Animator参数的哈希值,避免重复计算
  6. /// </summary>
  7. public class AnimationID
  8. {
  9.     // 角色移动相关动画参数哈希
  10.     public static readonly int MovementID = Animator.StringToHash("Movement");
  11.     public static readonly int LockID = Animator.StringToHash("Lock");
  12.     public static readonly int HorizontalID = Animator.StringToHash("Horizontal");
  13.     public static readonly int VerticalID = Animator.StringToHash("Vertical");
  14.     public static readonly int HasInputID = Animator.StringToHash("HasInput");
  15.     public static readonly int RunID = Animator.StringToHash("Run");
  16. }
复制代码
Day06  GameTimer
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. /// <summary>
  6. /// 计时器状态枚举,描述计时器不同工作阶段
  7. /// </summary>
  8. public enum TimerState
  9. {
  10.     NOTWORKERE, // 没有工作(初始或重置后状态)
  11.     WORKERING,  // 工作中(计时进行时)
  12.     DONE        // 工作完成(计时结束)
  13. }
  14. /// <summary>
  15. /// 游戏计时器类,用于管理计时逻辑,支持启动计时、更新计时、获取状态、重置等功能
  16. /// </summary>
  17. public class GameTimer
  18. {
  19.     // 计时时长(剩余计时时间)
  20.     private float _startTime;
  21.     // 计时结束后要执行的任务(Action 委托)
  22.     private Action _task;
  23.     // 是否停止当前计时器标记
  24.     private bool _isStopTimer;
  25.     // 当前计时器的状态
  26.     private TimerState _timerState;
  27.     /// <summary>
  28.     /// 构造函数,初始化时重置计时器
  29.     /// </summary>
  30.     public GameTimer()
  31.     {
  32.         ResetTimer();
  33.     }
  34.     /// <summary>
  35.     /// 1. 开始计时
  36.     /// </summary>
  37.     /// <param name="time">要计时的时长</param>
  38.     /// <param name="task">计时结束后要执行的任务(Action 委托)</param>
  39.     public void StartTimer(float time, Action task)
  40.     {
  41.         _startTime = time;
  42.         _task = task;
  43.         _isStopTimer = false;
  44.         _timerState = TimerState.WORKERING;
  45.     }
  46.     /// <summary>
  47.     /// 2. 更新计时器(通常在 MonoBehaviour 的 Update 里调用,驱动计时逻辑)
  48.     /// </summary>
  49.     public void UpdateTimer()
  50.     {
  51.         // 如果标记为停止,直接返回,不执行计时更新
  52.         if (_isStopTimer)
  53.             return;
  54.         // 递减计时时间
  55.         _startTime -= Time.deltaTime;
  56.         // 计时时间小于 0,说明计时结束
  57.         if (_startTime < 0)
  58.         {
  59.             // 安全调用任务(如果任务不为 null 才执行)
  60.             _task?.Invoke();
  61.             // 更新状态为已完成
  62.             _timerState = TimerState.DONE;
  63.             // 标记为停止,后续不再继续计时更新
  64.             _isStopTimer = true;
  65.         }
  66.     }
  67.     /// <summary>
  68.     /// 3. 获取当前计时器的状态
  69.     /// </summary>
  70.     /// <returns>返回 TimerState 枚举值,代表当前计时器状态</returns>
  71.     public TimerState GetTimerState() => _timerState;
  72.     /// <summary>
  73.     /// 4. 重置计时器,恢复到初始状态
  74.     /// </summary>
  75.     public void ResetTimer()
  76.     {
  77.         _startTime = 0f;
  78.         _task = null;
  79.         _isStopTimer = true;
  80.         _timerState = TimerState.NOTWORKERE;
  81.     }
  82. }
复制代码
TimerManager
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using GGG.Tool;
  5. using GGG.Tool.Singleton;
  6. using UnityEngine;
  7. using UnityEngine.UIElements;
  8. /// <summary>
  9. /// 计时器管理器,采用单例模式,负责管理空闲计时器队列和工作中计时器列表,
  10. /// 实现计时器的初始化、分配、回收及更新逻辑
  11. /// </summary>
  12. public class TimerManager : Singleton<TimerManager>
  13. {
  14.     #region 私有字段
  15.     // 初始最大计时器数量,在 Inspector 中配置
  16.     [SerializeField] private int _initMaxTimerCount;
  17.     // 空闲计时器队列,存储可用的 GameTimer
  18.     private Queue<GameTimer> _notWorkingTimer = new Queue<GameTimer>();
  19.     // 工作中计时器列表,存储正在计时的 GameTimer
  20.     private List<GameTimer> _workingTimer = new List<GameTimer>();
  21.     #endregion
  22.     #region 生命周期与初始化
  23.     protected override void Awake()
  24.     {
  25.         base.Awake();
  26.         InitTimerManager();
  27.     }
  28.     /// <summary>
  29.     /// 初始化计时器管理器,创建初始数量的空闲计时器
  30.     /// </summary>
  31.     private void InitTimerManager()
  32.     {
  33.         for (int i = 0; i < _initMaxTimerCount; i++)
  34.         {
  35.             CreateTimerInternal();
  36.         }
  37.     }
  38.     /// <summary>
  39.     /// 内部创建计时器并加入空闲队列的方法
  40.     /// </summary>
  41.     private void CreateTimerInternal()
  42.     {
  43.         var timer = new GameTimer();
  44.         _notWorkingTimer.Enqueue(timer);
  45.     }
  46.     #endregion
  47.     #region 计时器分配与回收
  48.     /// <summary>
  49.     /// 尝试获取一个计时器,用于执行定时任务
  50.     /// </summary>
  51.     /// <param name="time">计时时长</param>
  52.     /// <param name="task">计时结束后执行的任务</param>
  53.     public void TryGetOneTimer(float time, Action task)
  54.     {
  55.         // 若空闲队列为空,额外创建一个计时器
  56.         if (_notWorkingTimer.Count == 0)
  57.         {
  58.             CreateTimerInternal();
  59.         }
  60.         var timer = _notWorkingTimer.Dequeue();
  61.         timer.StartTimer(time, task);
  62.         _workingTimer.Add(timer);
  63.     }
  64.     /// <summary>
  65.     /// 回收计时器(可在 GameTimer 完成任务时调用,这里逻辑已内联在更新里,也可扩展外部调用)
  66.     /// 注:当前通过 UpdateWorkingTimer 自动回收,此方法可留作扩展
  67.     /// </summary>
  68.     /// <param name="timer">要回收的计时器</param>
  69.     private void RecycleTimer(GameTimer timer)
  70.     {
  71.         timer.ResetTimer();
  72.         _notWorkingTimer.Enqueue(timer);
  73.         _workingTimer.Remove(timer);
  74.     }
  75.     #endregion
  76.     #region 计时器更新逻辑
  77.     private void Update()
  78.     {
  79.         UpdateWorkingTimer();
  80.     }
  81.     /// <summary>
  82.     /// 更新工作中计时器的状态,处理计时推进和完成后的回收
  83.     /// </summary>
  84.     private void UpdateWorkingTimer()
  85.     {
  86.         // 遍历副本,避免列表修改时迭代出错
  87.         for (int i = _workingTimer.Count - 1; i >= 0; i--)
  88.         {
  89.             var timer = _workingTimer[i];
  90.             timer.UpdateTimer();
  91.             if (timer.GetTimerState() == TimerState.DONE)
  92.             {
  93.                 RecycleTimer(timer);
  94.             }
  95.         }
  96.     }
  97.     #endregion
  98. }
复制代码
Day07 脚部拖尾特效的控制——奔跑时启用
  1. using UnityEngine;
  2. using System.Collections;
  3. public class ObjectVisibilityController : MonoBehaviour
  4. {
  5.     // 在 Inspector 中手动拖入需要控制的子物体
  6.     public GameObject targetChild;
  7.     public Animator playerAnimator;
  8.     // 存储当前目标状态,用于判断是否需要执行状态切换
  9.     private bool _currentTargetState;
  10.     // 标记是否正在等待延迟,避免重复启动协程
  11.     private bool _isWaiting = false;
  12.     private void Update()
  13.     {
  14.         // 获取动画状态的当前值
  15.         bool desiredState = playerAnimator.GetBool(AnimationID.RunID);
  16.         // 如果状态发生变化且不在等待状态,则启动延迟协程
  17.         if (desiredState != _currentTargetState && !_isWaiting)
  18.         {
  19.             StartCoroutine(ChangeStateAfterDelay(desiredState, 0.5f));
  20.         }
  21.     }
  22.     // 延迟改变状态的协程
  23.     private IEnumerator ChangeStateAfterDelay(bool newState, float delay)
  24.     {
  25.         _isWaiting = true; // 标记为正在等待
  26.         yield return new WaitForSeconds(delay); // 等待指定秒数
  27.         // 应用新状态
  28.         targetChild.SetActive(newState);
  29.         _currentTargetState = newState;
  30.         _isWaiting = false; // 重置等待标记
  31.     }
  32. }
复制代码
11.gif

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

IKController
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class IKController : MonoBehaviour
  5. {
  6.     public Animator _animator;
  7.     //IK控制点
  8.     //四肢关节点
  9.     public Transform ik_LHand;
  10.     public Transform ik_RHand;
  11.     public Transform ik_LFoot;
  12.     public Transform ik_RFoot;
  13.     //头部控制点,可以根据主相机的位置,让玩家能够从侧视角下看到头部偏转。
  14.     public Transform Head_IKPoint;
  15.     [Header("IK 权重控制")]
  16.     [SerializeField] private float ikBlendSpeed = 5f; // IK权重变化速度
  17.     [SerializeField] private float headTurnSpeed = 5f; // 头部转向速度  
  18.     [SerializeField] private float maxHeadAngle = 60f; // 头部最大转向角度
  19.     // IK相关私有变量
  20.     private float _currentHeadIKWeight = 0f; // 当前头部IK权重
  21.     private Vector3 _currentLookTarget; // 缓存"当前正在看的点"
  22.     private bool _hasInitializedLookTarget = false; // 是否已初始化看向目标
  23.     private void OnAnimatorIK(int layerIndex)
  24.     {
  25.         // 四肢IK控制
  26.         if (ik_LHand != null)
  27.             IKControl(AvatarIKGoal.LeftHand, ik_LHand);
  28.         if (ik_RHand != null)
  29.             IKControl(AvatarIKGoal.RightHand, ik_RHand);
  30.         if (ik_LFoot != null)
  31.             IKControl(AvatarIKGoal.LeftFoot, ik_LFoot);
  32.         if (ik_RFoot != null)
  33.             IKControl(AvatarIKGoal.RightFoot, ik_RFoot);
  34.         // 头部IK控制 - 使用平滑权重过渡
  35.         HandleHeadIK();
  36.     }
  37.     /// <summary>
  38.     /// 处理头部IK控制 - 解决生硬切换问题
  39.     /// </summary>
  40.     private void HandleHeadIK()
  41.     {
  42.         if (Head_IKPoint == null)
  43.             return;
  44.         // 判断是否应该启用头部IK
  45.         bool shouldUseHeadIK = _animator.GetFloat(AnimationID.MovementID) < 0.1f;
  46.         // 计算目标权重
  47.         float targetWeight = shouldUseHeadIK ? 1f : 0f;
  48.         // 平滑过渡权重 - 这是解决生硬切换的关键
  49.         _currentHeadIKWeight = Mathf.Lerp(_currentHeadIKWeight, targetWeight, ikBlendSpeed * Time.deltaTime);
  50.         // 如果权重大于0,执行头部IK控制
  51.         if (_currentHeadIKWeight > 0.01f)
  52.         {
  53.             IKHeadControl(Head_IKPoint, headTurnSpeed, maxHeadAngle);
  54.         }
  55.         // 使用平滑权重而不是固定的1f
  56.         _animator.SetLookAtWeight(_currentHeadIKWeight);
  57.         // 如果已初始化目标位置,设置看向位置
  58.         if (_hasInitializedLookTarget)
  59.         {
  60.             _animator.SetLookAtPosition(_currentLookTarget);
  61.         }
  62.     }
  63.     /// <summary>
  64.     /// 头部 IK 控制(平滑转向 + 角度限制)
  65.     /// </summary>
  66.     /// <param name="target">要看的对象</param>
  67.     /// <param name="turnSpeed">插值速度</param>
  68.     /// <param name="maxAngle">最大允许夹角(度数)</param>
  69.     private void IKHeadControl(Transform target,
  70.                                float turnSpeed = 5f,
  71.                                float maxAngle = 60f)
  72.     {
  73.         // 初始化看向目标 - 防止第一次启用时的突然跳转
  74.         if (!_hasInitializedLookTarget)
  75.         {
  76.             _currentLookTarget = transform.position + transform.forward * 5f;
  77.             _hasInitializedLookTarget = true;
  78.         }
  79.         // 1. 计算最终想要看的点
  80.         Vector3 rawTargetPos;
  81.         Vector3 directionToCamera = target.position - transform.position;
  82.         bool isCameraInFront = Vector3.Dot(transform.forward, directionToCamera.normalized) > 0;
  83.         if (isCameraInFront)
  84.         {
  85.             // 相机在前面,看向相机
  86.             rawTargetPos = target.position;
  87.         }
  88.         else
  89.         {
  90.             // 相机在背后,看向相机视线向前延伸的点
  91.             rawTargetPos = target.position + target.forward * 10f;
  92.         }
  93.         // 2. 计算与正前方向的夹角
  94.         Vector3 dirToRawTarget = (rawTargetPos - transform.position).normalized;
  95.         float angle = Vector3.Angle(transform.forward, dirToRawTarget);
  96.         // 3. 如果角度在范围内,才允许平滑转向
  97.         if (angle <= maxAngle)
  98.         {
  99.             _currentLookTarget = Vector3.Lerp(_currentLookTarget, rawTargetPos,
  100.                                               turnSpeed * Time.deltaTime);
  101.         }
  102.         // 否则保持上一帧的 _currentLookTarget 不变(即不更新)
  103.         // 4. Debug绘制
  104.         Debug.DrawLine(transform.position, _currentLookTarget, Color.red);
  105.         Debug.DrawRay(target.position, target.forward * 10f, Color.blue);
  106.         // 注意:移除了这里的SetLookAtWeight和SetLookAtPosition调用
  107.         // 因为现在在HandleHeadIK()中统一处理
  108.     }
  109.     /// <summary>
  110.     /// 四肢IK控制
  111.     /// </summary>
  112.     /// <param name="ControlPosition"></param>
  113.     /// <param name="target"></param>
  114.     public void IKControl(AvatarIKGoal ControlPosition, Transform target)
  115.     {
  116.         _animator.SetIKPositionWeight(ControlPosition, 1);
  117.         _animator.SetIKPosition(ControlPosition, target.position);
  118.         _animator.SetIKRotationWeight(ControlPosition, 1);
  119.         _animator.SetIKRotation(ControlPosition, target.rotation);
  120.     }
  121. }
复制代码
修改之前的正面处决条件,加上角度限制
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using Raycasting;
  5. /*
  6. * This class represents the actual spider. It is responsible for "glueing" it to the surfaces around it. This is accomplished by
  7. * creating a fake gravitational force in the direction of the surface normal it is standing on. The surface normal is determined
  8. * by spherical raycasting downwards, as well as forwards for wall-climbing.
  9. *
  10. * The torso of the spider will move and rotate depending on the height of the referenced legs to mimic "spinal movement".
  11. *
  12. * The spider does not move on its own. Therefore a controller should call the provided functions walk() and turn() for
  13. * the desired control.
  14. */
  15. [DefaultExecutionOrder(0)] // Any controller of this spider should have default execution -1
  16. public class Spider : MonoBehaviour {
  17.     private Rigidbody rb;
  18.     [Header("Debug")]
  19.     public bool showDebug;
  20.     [Header("Movement")]
  21.     [Range(1, 10)]
  22.     public float walkSpeed;
  23.     [Range(1, 10)]
  24.     public float runSpeed;
  25.     [Range(1, 5)]
  26.     public float turnSpeed;
  27.     [Range(0.001f, 1)]
  28.     public float walkDrag;
  29.     [Header("Grounding")]
  30.     public CapsuleCollider capsuleCollider;
  31.     [Range(1, 10)]
  32.     public float gravityMultiplier;
  33.     [Range(1, 10)]
  34.     public float groundNormalAdjustSpeed;
  35.     [Range(1, 10)]
  36.     public float forwardNormalAdjustSpeed;
  37.     public LayerMask walkableLayer;
  38.     [Range(0, 1)]
  39.     public float gravityOffDistance;
  40.     [Header("IK Legs")]
  41.     public Transform body;
  42.     public IKChain[] legs;
  43.     [Header("Body Offset Height")]
  44.     public float bodyOffsetHeight;
  45.     [Header("Leg Centroid")]
  46.     public bool legCentroidAdjustment;
  47.     [Range(0, 100)]
  48.     public float legCentroidSpeed;
  49.     [Range(0, 1)]
  50.     public float legCentroidNormalWeight;
  51.     [Range(0, 1)]
  52.     public float legCentroidTangentWeight;
  53.     [Header("Leg Normal")]
  54.     public bool legNormalAdjustment;
  55.     [Range(0, 100)]
  56.     public float legNormalSpeed;
  57.     [Range(0, 1)]
  58.     public float legNormalWeight;
  59.     private Vector3 bodyY;
  60.     private Vector3 bodyZ;
  61.     [Header("Breathing")]
  62.     public bool breathing;
  63.     [Range(0.01f, 20)]
  64.     public float breathePeriod;
  65.     [Range(0, 1)]
  66.     public float breatheMagnitude;
  67.     [Header("Ray Adjustments")]
  68.     [Range(0.0f, 1.0f)]
  69.     public float forwardRayLength;
  70.     [Range(0.0f, 1.0f)]
  71.     public float downRayLength;
  72.     [Range(0.1f, 1.0f)]
  73.     public float forwardRaySize = 0.66f;
  74.     [Range(0.1f, 1.0f)]
  75.     public float downRaySize = 0.9f;
  76.     private float downRayRadius;
  77.     private Vector3 currentVelocity;
  78.     private bool isMoving = true;
  79.     private bool groundCheckOn = true;
  80.     private Vector3 lastNormal;
  81.     private Vector3 bodyDefaultCentroid;
  82.     private Vector3 bodyCentroid;
  83.     private SphereCast downRay, forwardRay;
  84.     private RaycastHit hitInfo;
  85.     private enum RayType { None, ForwardRay, DownRay };
  86.     private struct groundInfo {
  87.         public bool isGrounded;
  88.         public Vector3 groundNormal;
  89.         public float distanceToGround;
  90.         public RayType rayType;
  91.         public groundInfo(bool isGrd, Vector3 normal, float dist, RayType m_rayType) {
  92.             isGrounded = isGrd;
  93.             groundNormal = normal;
  94.             distanceToGround = dist;
  95.             rayType = m_rayType;
  96.         }
  97.     }
  98.     private groundInfo grdInfo;
  99.     private void Awake() {
  100.         //Make sure the scale is uniform, since otherwise lossy scale will not be accurate.
  101.         float x = transform.localScale.x; float y = transform.localScale.y; float z = transform.localScale.z;
  102.         if (Mathf.Abs(x - y) > float.Epsilon || Mathf.Abs(x - z) > float.Epsilon || Mathf.Abs(y - z) > float.Epsilon) {
  103.             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.");
  104.         }
  105.         rb = GetComponent<Rigidbody>();
  106.         //Initialize the two Sphere Casts
  107.         downRayRadius = downRaySize * getColliderRadius();
  108.         float forwardRayRadius = forwardRaySize * getColliderRadius();
  109.         downRay = new SphereCast(transform.position, -transform.up, downRayLength * getColliderLength(), downRayRadius, transform, transform);
  110.         forwardRay = new SphereCast(transform.position, transform.forward, forwardRayLength * getColliderLength(), forwardRayRadius, transform, transform);
  111.         //Initialize the bodyupLocal as the spiders transform.up parented to the body. Initialize the breathePivot as the body position parented to the spider
  112.         bodyY = body.transform.InverseTransformDirection(transform.up);
  113.         bodyZ = body.transform.InverseTransformDirection(transform.forward);
  114.         bodyCentroid = body.transform.position + getScale() * bodyOffsetHeight * transform.up;
  115.         bodyDefaultCentroid = transform.InverseTransformPoint(bodyCentroid);
  116.     }
  117.     void FixedUpdate() {
  118.         //** Ground Check **//
  119.         grdInfo = GroundCheck();
  120.         //** Rotation to normal **//
  121.         float normalAdjustSpeed = (grdInfo.rayType == RayType.ForwardRay) ? forwardNormalAdjustSpeed : groundNormalAdjustSpeed;
  122.         Vector3 slerpNormal = Vector3.Slerp(transform.up, grdInfo.groundNormal, 0.02f * normalAdjustSpeed);
  123.         Quaternion goalrotation = getLookRotation(Vector3.ProjectOnPlane(transform.right, slerpNormal), slerpNormal);
  124.         // Save last Normal for access
  125.         lastNormal = transform.up;
  126.         //Apply the rotation to the spider
  127.         if (Quaternion.Angle(transform.rotation,goalrotation)>Mathf.Epsilon) transform.rotation = goalrotation;
  128.         // Dont apply gravity if close enough to ground
  129.         if (grdInfo.distanceToGround > getGravityOffDistance()) {
  130.             rb.AddForce(-grdInfo.groundNormal * gravityMultiplier * 0.0981f * getScale()); //Important using the groundnormal and not the lerping normal here!
  131.         }
  132.     }
  133.     void Update() {
  134.         //** Debug **//
  135.         if (showDebug) drawDebug();
  136.         Vector3 Y = body.TransformDirection(bodyY);
  137.         //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)
  138.         if (legCentroidAdjustment) bodyCentroid = Vector3.Lerp(bodyCentroid, getLegsCentroid(), Time.deltaTime * legCentroidSpeed);
  139.         else bodyCentroid = getDefaultCentroid();
  140.         body.transform.position = bodyCentroid;
  141.         if (legNormalAdjustment) {
  142.             Vector3 newNormal = GetLegsPlaneNormal();
  143.             //Use Global X for  pitch
  144.             Vector3 X = transform.right;
  145.             float angleX = Vector3.SignedAngle(Vector3.ProjectOnPlane(Y, X), Vector3.ProjectOnPlane(newNormal, X), X);
  146.             angleX = Mathf.LerpAngle(0, angleX, Time.deltaTime * legNormalSpeed);
  147.             body.transform.rotation = Quaternion.AngleAxis(angleX, X) * body.transform.rotation;
  148.             //Use Local Z for roll. With the above global X for pitch, this avoids any kind of yaw happening.
  149.             Vector3 Z = body.TransformDirection(bodyZ);
  150.             float angleZ = Vector3.SignedAngle(Y, Vector3.ProjectOnPlane(newNormal, Z), Z);
  151.             angleZ = Mathf.LerpAngle(0, angleZ, Time.deltaTime * legNormalSpeed);
  152.             body.transform.rotation = Quaternion.AngleAxis(angleZ, Z) * body.transform.rotation;
  153.         }
  154.         if (breathing) {
  155.             float t = (Time.time * 2 * Mathf.PI / breathePeriod) % (2 * Mathf.PI);
  156.             float amplitude = breatheMagnitude * getColliderRadius();
  157.             Vector3 direction = body.TransformDirection(bodyY);
  158.             body.transform.position = bodyCentroid + amplitude * (Mathf.Sin(t) + 1f) * direction;
  159.         }
  160.         // Update the moving status
  161.         if (transform.hasChanged) {
  162.             isMoving = true;
  163.             transform.hasChanged = false;
  164.         }
  165.         else isMoving = false;
  166.     }
  167.     //** Movement methods**//
  168.     private void move(Vector3 direction, float speed) {
  169.         // TODO: Make sure direction is on the XZ plane of spider! For this maybe refactor the logic from input from spidercontroller to this function.
  170.         //Only allow direction vector to have a length of 1 or lower
  171.         float magnitude = direction.magnitude;
  172.         if (magnitude > 1) {
  173.             direction = direction.normalized;
  174.             magnitude = 1f;
  175.         }
  176.         // 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)
  177.         if (direction != Vector3.zero) {
  178.             float directionDamp = Mathf.Pow(Mathf.Clamp(Vector3.Dot(direction / magnitude, transform.forward), 0, 1), 2);
  179.             float distance = 0.0004f * speed * magnitude * directionDamp * getScale();
  180.             distance = Mathf.Clamp(distance, 0, 0.99f * downRayRadius);
  181.             direction = distance * (direction / magnitude);
  182.         }
  183.         //Slerp from old to new velocity using the acceleration
  184.         currentVelocity = Vector3.Slerp(currentVelocity, direction, 1f - walkDrag);
  185.         //Apply the resulting velocity
  186.         transform.position += currentVelocity;
  187.     }
  188.     public void turn(Vector3 goalForward) {
  189.         //Make sure goalForward is orthogonal to transform up
  190.         goalForward = Vector3.ProjectOnPlane(goalForward, transform.up).normalized;
  191.         if (goalForward == Vector3.zero || Vector3.Angle(goalForward, transform.forward) < Mathf.Epsilon) {
  192.             return;
  193.         }
  194.         goalForward = Vector3.ProjectOnPlane(goalForward, transform.up);
  195.         transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation(goalForward, transform.up), turnSpeed);
  196.     }
  197.     //** Movement methods for public access**//
  198.     // It is advised to call these on a fixed update basis.
  199.     public void walk(Vector3 direction) {
  200.         if (direction.magnitude < Mathf.Epsilon) return;
  201.         move(direction, walkSpeed);
  202.     }
  203.     public void run(Vector3 direction) {
  204.         if (direction.magnitude < Mathf.Epsilon) return;
  205.         move(direction, runSpeed);
  206.     }
  207.     //** Ground Check Method **//
  208.     private groundInfo GroundCheck() {
  209.         if (groundCheckOn) {
  210.             if (forwardRay.castRay(out hitInfo, walkableLayer)) {
  211.                 return new groundInfo(true, hitInfo.normal.normalized, Vector3.Distance(transform.TransformPoint(capsuleCollider.center), hitInfo.point) - getColliderRadius(), RayType.ForwardRay);
  212.             }
  213.             if (downRay.castRay(out hitInfo, walkableLayer)) {
  214.                 return new groundInfo(true, hitInfo.normal.normalized, Vector3.Distance(transform.TransformPoint(capsuleCollider.center), hitInfo.point) - getColliderRadius(), RayType.DownRay);
  215.             }
  216.         }
  217.         return new groundInfo(false, Vector3.up, float.PositiveInfinity, RayType.None);
  218.     }
  219.     //** Helper methods**//
  220.     /*
  221.     * Returns the rotation with specified right and up direction   
  222.     * May have to make more error catches here. Whatif not orthogonal?
  223.     */
  224.     private Quaternion getLookRotation(Vector3 right, Vector3 up) {
  225.         if (up == Vector3.zero || right == Vector3.zero) return Quaternion.identity;
  226.         // If vectors are parallel return identity
  227.         float angle = Vector3.Angle(right, up);
  228.         if (angle == 0 || angle == 180) return Quaternion.identity;
  229.         Vector3 forward = Vector3.Cross(right, up);
  230.         return Quaternion.LookRotation(forward, up);
  231.     }
  232.     //** Torso adjust methods for more realistic movement **//
  233.     // Calculate the centroid (center of gravity) given by all end effector positions of the legs
  234.     private Vector3 getLegsCentroid() {
  235.         if (legs == null || legs.Length == 0) {
  236.             Debug.LogError("Cant calculate leg centroid, legs not assigned.");
  237.             return body.transform.position;
  238.         }
  239.         Vector3 defaultCentroid = getDefaultCentroid();
  240.         // Calculate the centroid of legs position
  241.         Vector3 newCentroid = Vector3.zero;
  242.         float k = 0;
  243.         for (int i = 0; i < legs.Length; i++) {
  244.             newCentroid += legs[i].getEndEffector().position;
  245.             k++;
  246.         }
  247.         newCentroid = newCentroid / k;
  248.         // Offset the calculated centroid
  249.         Vector3 offset = Vector3.Project(defaultCentroid - getColliderBottomPoint(), transform.up);
  250.         newCentroid += offset;
  251.         // Calculate the normal and tangential translation needed
  252.         Vector3 normalPart = Vector3.Project(newCentroid - defaultCentroid, transform.up);
  253.         Vector3 tangentPart = Vector3.ProjectOnPlane(newCentroid - defaultCentroid, transform.up);
  254.         return defaultCentroid + Vector3.Lerp(Vector3.zero, normalPart, legCentroidNormalWeight) + Vector3.Lerp(Vector3.zero, tangentPart, legCentroidTangentWeight);
  255.     }
  256.     // Calculate the normal of the plane defined by leg positions, so we know how to rotate the body
  257.     private Vector3 GetLegsPlaneNormal() {
  258.         if (legs == null) {
  259.             Debug.LogError("Cant calculate normal, legs not assigned.");
  260.             return transform.up;
  261.         }
  262.         if (legNormalWeight <= 0f) return transform.up;
  263.         Vector3 newNormal = transform.up;
  264.         Vector3 toEnd;
  265.         Vector3 currentTangent;
  266.         for (int i = 0; i < legs.Length; i++) {
  267.             //normal += legWeight * legs[i].getTarget().normal;
  268.             toEnd = legs[i].getEndEffector().position - transform.position;
  269.             currentTangent = Vector3.ProjectOnPlane(toEnd, transform.up);
  270.             if (currentTangent == Vector3.zero) continue; // Actually here we would have a 90degree rotation but there is no choice of a tangent.
  271.             newNormal = Quaternion.Lerp(Quaternion.identity, Quaternion.FromToRotation(currentTangent, toEnd), legNormalWeight) * newNormal;
  272.         }
  273.         return newNormal;
  274.     }
  275.     //** Getters **//
  276.     public float getScale() {
  277.         return transform.lossyScale.y;
  278.     }
  279.     public bool getIsMoving() {
  280.         return isMoving;
  281.     }
  282.     public Vector3 getCurrentVelocityPerSecond() {
  283.         return currentVelocity / Time.fixedDeltaTime;
  284.     }
  285.     public Vector3 getCurrentVelocityPerFixedFrame() {
  286.         return currentVelocity;
  287.     }
  288.     public Vector3 getGroundNormal() {
  289.         return grdInfo.groundNormal;
  290.     }
  291.     public Vector3 getLastNormal() {
  292.         return lastNormal;
  293.     }
  294.     public float getColliderRadius() {
  295.         return getScale() * capsuleCollider.radius;
  296.     }
  297.     public float getNonScaledColliderRadius() {
  298.         return capsuleCollider.radius;
  299.     }
  300.     public float getColliderLength() {
  301.         return getScale() * capsuleCollider.height;
  302.     }
  303.     public Vector3 getColliderCenter() {
  304.         return transform.TransformPoint(capsuleCollider.center);
  305.     }
  306.     public Vector3 getColliderBottomPoint() {
  307.         return transform.TransformPoint(capsuleCollider.center - capsuleCollider.radius * new Vector3(0, 1, 0));
  308.     }
  309.     public Vector3 getDefaultCentroid() {
  310.         return transform.TransformPoint(bodyDefaultCentroid);
  311.     }
  312.     public float getGravityOffDistance() {
  313.         return gravityOffDistance * getColliderRadius();
  314.     }
  315.     //** Setters **//
  316.     public void setGroundcheck(bool b) {
  317.         groundCheckOn = b;
  318.     }
  319.     //** Debug Methods **//
  320.     private void drawDebug() {
  321.         //Draw the two Sphere Rays
  322.         downRay.draw(Color.green);
  323.         forwardRay.draw(Color.blue);
  324.         //Draw the Gravity off distance
  325.         Vector3 borderpoint = getColliderBottomPoint();
  326.         Debug.DrawLine(borderpoint, borderpoint + getGravityOffDistance() * -transform.up, Color.magenta);
  327.         //Draw the current transform.up and the bodys current Y orientation
  328.         Debug.DrawLine(transform.position, transform.position + 2f * getColliderRadius() * transform.up, new Color(1, 0.5f, 0, 1));
  329.         Debug.DrawLine(transform.position, transform.position + 2f * getColliderRadius() * body.TransformDirection(bodyY), Color.blue);
  330.         //Draw the Centroids
  331.         DebugShapes.DrawPoint(getDefaultCentroid(), Color.magenta, 0.1f);
  332.         DebugShapes.DrawPoint(getLegsCentroid(), Color.red, 0.1f);
  333.         DebugShapes.DrawPoint(getColliderBottomPoint(), Color.cyan, 0.1f);
  334.     }
  335. #if UNITY_EDITOR
  336.     void OnDrawGizmosSelected() {
  337.         if (!showDebug) return;
  338.         if (UnityEditor.EditorApplication.isPlaying) return;
  339.         if (!UnityEditor.Selection.Contains(transform.gameObject)) return;
  340.         Awake();
  341.         drawDebug();
  342.     }
  343. #endif
  344. }
复制代码
Bug修复——让处决的Combo索引单独控制
  1. using UnityEngine;
  2. using System.Collections;
  3. using Raycasting;
  4. /*
  5. * This class needs a reference to the Spider class and calls the walk and turn functions depending on player input.
  6. * So in essence, this class translates player input to spider movement. The input direction is relative to a camera and so a
  7. * reference to one is needed.
  8. */
  9. [DefaultExecutionOrder(-1)] // Make sure the players input movement is applied before the spider itself will do a ground check and possibly add gravity
  10. public class SpiderController : MonoBehaviour {
  11.     public Spider spider;
  12.     [Header("Camera")]
  13.     public SmoothCamera smoothCam;
  14.     void FixedUpdate() {
  15.         //** Movement **//
  16.         Vector3 input = getInput();
  17.         if (Input.GetKey(KeyCode.LeftShift)) spider.run(input);
  18.         else spider.walk(input);
  19.         Quaternion tempCamTargetRotation = smoothCam.getCamTargetRotation();
  20.         Vector3 tempCamTargetPosition = smoothCam.getCamTargetPosition();
  21.         spider.turn(input);
  22.         smoothCam.setTargetRotation(tempCamTargetRotation);
  23.         smoothCam.setTargetPosition(tempCamTargetPosition);
  24.     }
  25.     void Update() {
  26.         //Hold down Space to deactivate ground checking. The spider will fall while space is hold.
  27.         spider.setGroundcheck(!Input.GetKey(KeyCode.Space));
  28.     }
  29.     private Vector3 getInput() {
  30.         Vector3 up = spider.transform.up;
  31.         Vector3 right = spider.transform.right;
  32.         Vector3 input = Vector3.ProjectOnPlane(smoothCam.getCameraTarget().forward, up).normalized * Input.GetAxis("Vertical") + (Vector3.ProjectOnPlane(smoothCam.getCameraTarget().right, up).normalized * Input.GetAxis("Horizontal"));
  33.         Quaternion fromTo = Quaternion.AngleAxis(Vector3.SignedAngle(up, spider.getGroundNormal(), right), right);
  34.         input = fromTo * input;
  35.         float magnitude = input.magnitude;
  36.         return (magnitude <= 1) ? input : input /= magnitude;
  37.     }
  38. }
复制代码
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. /*
  5. * This class holds references to each IKStepper of the legs and manages the stepping of them.
  6. * So instead of each leg managing its stepping on its own, this class acts as the brain and decides when each leg should step.
  7. * 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
  8. * the step function in the IKStepper when the time is right to step.
  9. */
  10. [DefaultExecutionOrder(+1)] // Make sure all the stepping logic is called after the IK was solved in each IKChain
  11. public class IKStepManager : MonoBehaviour {
  12.     public bool printDebugLogs;
  13.     public Spider spider;
  14.     public enum StepMode { AlternatingTetrapodGait, QueueWait, QueueNoWait }
  15.     /*
  16.      * Note the following about the stepping modes:
  17.      *
  18.      * Alternating Tetrapod Gait:   This mode is inspired by a real life spider walk.
  19.      *                              The legs are assigned one of two groups, A or B.
  20.      *                              Then a timer switches between these groups on the timeinterval "stepTime".
  21.      *                              Every group only has a specific frame at which stepping is allowed in each interval
  22.      *                              With this, legs in the same group will always step at the same time if they need to step,
  23.      *                              and will never step while the other group is.
  24.      *                              If dynamic step time is selected, the average of each legs dyanamic step time is used.
  25.      *                              This mode does not use the asynchronicity specified in each legs, since the asyncronicty is already given
  26.      *                              by the groups.
  27.      *            
  28.      * Queue Wait:  This mode stores the legs that want to step in a queue and performs the stepping in the order of the queue.
  29.      *              This mode will always prioritize the next leg in the queue and will wait until it is able to step.
  30.      *              This however can and will inhibit the other legs from stepping if the waiting period is too long.
  31.      *              Unlike the above mode, this mode uses the asyncronicity defined in each leg to determine whether a leg is
  32.      *              allowed to step or not. Each leg will be inhibited to step as long as these async legs are stepping.
  33.      *  
  34.      * Queue No Wait:   This mode is analog to the above with the exception of not waiting for each next leg in the queue.
  35.      *                  The legs will still be iterated through in queue order but if a leg is not able to step,
  36.      *                  we still continue iterating and perform steps for the following legs if they are able to.
  37.      *                  So to be more specific, this is not a queue in the usual sense. It is a list of legs that need stepping,
  38.      *                  which will be iterated through in order and if the k-th leg is allowed to step, it will step
  39.      *                  and the k-th element of this list will be removed.
  40.      */
  41.     [Header("Step Mode")]
  42.     public StepMode stepMode;
  43.     //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
  44.     [Header("Legs for Queue Modes")]
  45.     public List<IKStepper> ikSteppers;
  46.     private List<IKStepper> stepQueue;
  47.     private Dictionary<int, bool> waitingForStep;
  48.     [Header("Legs for Gait Mode")]
  49.     public List<IKStepper> gaitGroupA;
  50.     public List<IKStepper> gaitGroupB;
  51.     private List<IKStepper> currentGaitGroup;
  52.     private float nextSwitchTime;
  53.     [Header("Steptime")]
  54.     public bool dynamicStepTime = true;
  55.     public float stepTimePerVelocity;
  56.     [Range(0, 1.0f)]
  57.     public float maxStepTime;
  58.     public enum GaitStepForcing { NoForcing, ForceIfOneLegSteps, ForceAlways }
  59.     [Header("Debug")]
  60.     public GaitStepForcing gaitStepForcing;
  61.     private void Awake() {
  62.         /* Queue Mode Initialization */
  63.         stepQueue = new List<IKStepper>();
  64.         // Remove all inactive IKSteppers
  65.         int k = 0;
  66.         foreach (var ikStepper in ikSteppers.ToArray()) {
  67.             if (!ikStepper.allowedTargetManipulationAccess()) ikSteppers.RemoveAt(k);
  68.             else k++;
  69.         }
  70.         // Initialize the hash map for step waiting with false
  71.         waitingForStep = new Dictionary<int, bool>();
  72.         foreach (var ikStepper in ikSteppers) {
  73.             waitingForStep.Add(ikStepper.GetInstanceID(), false);
  74.         }
  75.         /* Alternating Tetrapod Gait Initialization */
  76.         // Remove all inactive IKSteppers from the Groups
  77.         k = 0;
  78.         foreach (var ikStepper in gaitGroupA.ToArray()) {
  79.             if (!ikStepper.allowedTargetManipulationAccess()) gaitGroupA.RemoveAt(k);
  80.             else k++;
  81.         }
  82.         k = 0;
  83.         foreach (var ikStepper in gaitGroupB.ToArray()) {
  84.             if (!ikStepper.allowedTargetManipulationAccess()) gaitGroupB.RemoveAt(k);
  85.             else k++;
  86.         }
  87.         // Start with Group A and set switch time to step time
  88.         currentGaitGroup = gaitGroupA;
  89.         nextSwitchTime = maxStepTime;
  90.     }
  91.     private void LateUpdate() {
  92.         if (stepMode == StepMode.AlternatingTetrapodGait) AlternatingTetrapodGait();
  93.         else QueueStepMode();
  94.     }
  95.     private void QueueStepMode() {
  96.         /* Perform the step checks for all legs not already waiting to step.
  97.          * If a step is needed, enqueue them.
  98.          */
  99.         foreach (var ikStepper in ikSteppers) {
  100.             // Check if Leg isnt already waiting for step.
  101.             if (waitingForStep[ikStepper.GetInstanceID()] == true) continue;
  102.             //Now perform check if a step is needed and if so enqueue the element
  103.             if (ikStepper.stepCheck()) {
  104.                 stepQueue.Add(ikStepper);
  105.                 waitingForStep[ikStepper.GetInstanceID()] = true;
  106.                 if (printDebugLogs) Debug.Log(ikStepper.name + " is enqueued to step at queue position " + stepQueue.Count);
  107.             }
  108.         }
  109.         if (printDebugLogs) printQueue();
  110.         /* Iterate through the step queue in order and check if legs are eligible to step.
  111.          * If legs are able to step, let them step.
  112.          * If not, we have two cases:   If the current mode selected is the QueueWait mode, then stop the iteration.
  113.          *                              If the current mode selected is the QueueNoWait mode, simply continue with the iteration.
  114.          */
  115.         int k = 0;
  116.         foreach (var ikStepper in stepQueue.ToArray()) {
  117.             if (ikStepper.allowedToStep()) {
  118.                 ikStepper.getIKChain().unpauseSolving();
  119.                 ikStepper.step(calculateStepTime(ikStepper));
  120.                 // Remove the stepping leg from the list:
  121.                 waitingForStep[ikStepper.GetInstanceID()] = false;
  122.                 stepQueue.RemoveAt(k);
  123.                 if (printDebugLogs) Debug.Log(ikStepper.name + " was allowed to step and is thus removed.");
  124.             }
  125.             else {
  126.                 if (printDebugLogs) Debug.Log(ikStepper.name + " is not allowed to step.");
  127.                 // Stop iteration here if Queue Wait mode is selected
  128.                 if (stepMode == StepMode.QueueWait) {
  129.                     if (printDebugLogs) Debug.Log("Wait selected, thus stepping ends for this frame.");
  130.                     break;
  131.                 }
  132.                 k++; // Increment k by one here since i did not remove the current element from the list.
  133.             }
  134.         }
  135.         /* Iterate through all the legs that are still in queue, and therefore werent allowed to step.
  136.          * For them pause the IK solving while they are waiting.
  137.          */
  138.         foreach (var ikStepper in stepQueue) {
  139.             ikStepper.getIKChain().pauseSolving();
  140.         }
  141.     }
  142.     private void AlternatingTetrapodGait() {
  143.         // If the next switch time isnt reached yet, do nothing.
  144.         if (Time.time < nextSwitchTime) return;
  145.         /* Since switch time is reached, switch groups and set new switch time.
  146.          * Note that in the case of dynamic step time, it would not make sense to have each leg assigned its own step time
  147.          * since i want the stepping to be completed at the same time in order to switch to next group again.
  148.          * Thus, i simply calculate the average step time of the current group and use it for all legs.
  149.          * 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
  150.          */
  151.         currentGaitGroup = (currentGaitGroup == gaitGroupA) ? gaitGroupB : gaitGroupA;
  152.         float stepTime = calculateAverageStepTime(currentGaitGroup);
  153.         nextSwitchTime = Time.time + stepTime;
  154.         if (printDebugLogs) {
  155.             string text = ((currentGaitGroup == gaitGroupA) ? "Group: A" : "Group B") + " StepTime: " + stepTime;
  156.             Debug.Log(text);
  157.         }
  158.         /* Now perform the stepping for the current gait group.
  159.          * A leg in the gait group will only step if a step is needed.
  160.          * However, for debug purposes depending on which force mode is selected the other legs can be forced to step anyway.
  161.          */
  162.         if (gaitStepForcing == GaitStepForcing.ForceAlways) {
  163.             foreach (var ikStepper in currentGaitGroup) ikStepper.step(stepTime);
  164.         }
  165.         else if (gaitStepForcing == GaitStepForcing.ForceIfOneLegSteps) {
  166.             bool b = false;
  167.             foreach (var ikStepper in currentGaitGroup) {
  168.                 b = b || ikStepper.stepCheck();
  169.                 if (b == true) break;
  170.             }
  171.             if (b == true) foreach (var ikStepper in currentGaitGroup) ikStepper.step(stepTime);
  172.         }
  173.         else {
  174.             foreach (var ikStepper in currentGaitGroup) {
  175.                 if (ikStepper.stepCheck()) ikStepper.step(stepTime);
  176.             }
  177.         }
  178.     }
  179.     private float calculateStepTime(IKStepper ikStepper) {
  180.         if (dynamicStepTime) {
  181.             float k = stepTimePerVelocity * spider.getScale(); // At velocity=1, this is the steptime
  182.             float velocityMagnitude = ikStepper.getIKChain().getEndeffectorVelocityPerSecond().magnitude;
  183.             return (velocityMagnitude == 0) ? maxStepTime : Mathf.Clamp(k / velocityMagnitude, 0, maxStepTime);
  184.         }
  185.         else return maxStepTime;
  186.     }
  187.     private float calculateAverageStepTime(List<IKStepper> ikSteppers) {
  188.         if (dynamicStepTime) {
  189.             float stepTime = 0;
  190.             foreach (var ikStepper in ikSteppers) {
  191.                 stepTime += calculateStepTime(ikStepper);
  192.             }
  193.             return stepTime / ikSteppers.Count;
  194.         }
  195.         else return maxStepTime;
  196.     }
  197.     private void printQueue() {
  198.         if (stepQueue == null) return;
  199.         string queueText = "[";
  200.         if (stepQueue.Count != 0) {
  201.             foreach (var ikStepper in stepQueue) {
  202.                 queueText += ikStepper.name + ", ";
  203.             }
  204.             queueText = queueText.Substring(0, queueText.Length - 2);
  205.         }
  206.         queueText += "]";
  207.         Debug.Log("Queue: " + queueText);
  208.     }
  209. }
复制代码
region 触发伤害(普通攻击和处决攻击)
  1. using UnityEngine;
  2. using System.Collections;
  3. public class CharacterSwitcher : MonoBehaviour
  4. {
  5.     [Header("角色设置")]
  6.     public GameObject character1;
  7.     public GameObject character2;
  8.     [Header("切换按键")]
  9.     public KeyCode switchKey = KeyCode.Tab;
  10.     [Header("当前状态")]
  11.     public bool isCharacter1Active = true;
  12.     [Header("角色2专用相机")]
  13.     public Camera camera2;
  14.     [Header("切换延迟")]
  15.     public float switchDelay = 0.5f;   // 等待时间
  16.     private bool isSwitching = false;    // 正在等待切换
  17.     private void Start()
  18.     {
  19.         if (character1 == null || character2 == null)
  20.         {
  21.             Debug.LogError("请在Inspector中指定两个角色的GameObject!");
  22.             return;
  23.         }
  24.         character1.SetActive(isCharacter1Active);
  25.         character2.SetActive(!isCharacter1Active);
  26.         if (camera2 != null)
  27.             camera2.gameObject.SetActive(!isCharacter1Active);
  28.     }
  29.     private void Update()
  30.     {
  31.         if (Input.GetKeyDown(switchKey) && !isSwitching)
  32.             SwitchCharacter();
  33.     }
  34.     /* 供外部脚本调用的接口同样延迟 */
  35.     public void SwitchCharacter()
  36.     {
  37.         if (character1 == null || character2 == null || isSwitching)
  38.             return;
  39.         isSwitching = true;
  40.         /* 立即冻结当前角色,防止继续移动 */
  41.         FreezeMovement(GetActiveCharacter());
  42.         /* 延迟真正切换 */
  43.         StartCoroutine(DelayedSwitch());
  44.     }
  45.     public void SwitchToSpecificCharacter(bool switchToCharacter1)
  46.     {
  47.         if (isCharacter1Active == switchToCharacter1 || isSwitching)
  48.             return;
  49.         isSwitching = true;
  50.         FreezeMovement(GetActiveCharacter());
  51.         StartCoroutine(DelayedSwitch(switchToCharacter1));
  52.     }
  53.     /* 0.5 秒后真正切换 */
  54.     private IEnumerator DelayedSwitch(bool? targetState = null)
  55.     {
  56.         yield return new WaitForSeconds(switchDelay);
  57.         bool nextState = targetState ?? !isCharacter1Active;
  58.         isCharacter1Active = nextState;
  59.         character1.SetActive(isCharacter1Active);
  60.         character2.SetActive(!isCharacter1Active);
  61.         if (camera2 != null)
  62.             camera2.gameObject.SetActive(!isCharacter1Active);
  63.         Debug.Log($"切换到: {(isCharacter1Active ? "角色1" : "角色2")}");
  64.         isSwitching = false;
  65.     }
  66.     /* 简单冻结:把 Rigidbody 设为 Kinematic,关闭 CharacterController */
  67.     private void FreezeMovement(GameObject go)
  68.     {
  69.         if (go.TryGetComponent(out Rigidbody rb))
  70.         {
  71.             rb.velocity = Vector3.zero;
  72.             rb.angularVelocity = Vector3.zero;
  73.             rb.isKinematic = true;
  74.         }
  75.         if (go.TryGetComponent(out CharacterController cc))
  76.             cc.enabled = false;
  77.     }
  78.     public GameObject GetActiveCharacter()
  79.     {
  80.         return isCharacter1Active ? character1 : character2;
  81.     }
  82. }
复制代码
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using GGG.Tool.Singleton;
  5. using GGG.Tool;
  6. public class GamePoolManager : Singleton<GamePoolManager>
  7. {
  8.     // 1. 缓存配置项类
  9.     [System.Serializable]
  10.     private class PoolItem
  11.     {
  12.         public string ItemName;      // 对象名称,用于标识
  13.         public GameObject Item;      // 要缓存的游戏对象
  14.         public int InitMaxCount;     // 初始最大缓存数量
  15.     }
  16.     // 2. 缓存配置列表
  17.     [SerializeField]
  18.     private List<PoolItem> _configPoolItem = new List<PoolItem>();
  19.     private Dictionary<string, Queue<GameObject>> _poolCenter = new Dictionary<string, Queue<GameObject>>();
  20.     //对象池父对象
  21.     private GameObject _poolItemParent;
  22.     private void Start()
  23.     {
  24.         _poolItemParent = new GameObject("PoolItemParent");
  25.         //放到GamePoolManager的子级,统一管理
  26.         _poolItemParent.transform.SetParent(this.transform);
  27.         InitPool();
  28.     }
  29.     private void InitPool()
  30.     {
  31.         // 1. 我们判断外部配置是不是空的。
  32.         if (_configPoolItem.Count == 0)
  33.             return;
  34.         for (var i = 0; i < _configPoolItem.Count; i++)
  35.         {
  36.             for (int j = 0; j < _configPoolItem[i].InitMaxCount; j++)
  37.             {
  38.                 var item = Instantiate(_configPoolItem[i].Item);
  39.                 // 将对象设置为不可见
  40.                 item.SetActive(false);
  41.                 // 设置为PoolItemParent的子物体
  42.                 item.transform.SetParent(_poolItemParent.transform);
  43.                 // 判断池子中有没有存在这个对象的
  44.                 if (!_poolCenter.ContainsKey(_configPoolItem[i].ItemName))
  45.                 {
  46.                     // 如果当前对象池中没有对应名称的池子,那么我们需要创建一个
  47.                     _poolCenter.Add(
  48.                         _configPoolItem[i].ItemName,
  49.                         new Queue<GameObject>()
  50.                     );
  51.                     _poolCenter[_configPoolItem[i].ItemName].Enqueue(item);
  52.                 }
  53.                 else
  54.                 {
  55.                     _poolCenter[_configPoolItem[i].ItemName].Enqueue(item);
  56.                 }
  57.             }
  58.         }
  59.         Debug.Log(_poolCenter.Count);
  60.         Debug.Log(_poolCenter["ATKSound"].Count);
  61.     }
  62.     /// <summary>
  63.     /// 从对象池中尝试获取指定名称的对象,并设置其位置和旋转信息
  64.     /// </summary>
  65.     /// <param name="name">要获取的对象池名称(用于标识特定类型的对象)</param>
  66.     /// <param name="position">对象激活后的世界坐标位置</param>
  67.     /// <param name="rotation">对象激活后的世界空间旋转角度</param>
  68.     public void TryGetPoolItem(string name, Vector3 position, Quaternion rotation)
  69.     {
  70.         // 检查对象池容器中是否存在指定名称的对象池
  71.         if (_poolCenter.ContainsKey(name))
  72.         {
  73.             // 从对应名称的对象池队列中取出队首的对象(出队操作)
  74.             var item = _poolCenter[name].Dequeue();
  75.             // 设置对象的位置信息
  76.             item.transform.position = position;
  77.             // 设置对象的旋转信息
  78.             item.transform.rotation = rotation;
  79.             // 激活对象
  80.             item.SetActive(true);
  81.             // 将使用后的对象重新放回队列尾部(实现对象复用,避免频繁创建销毁)
  82.             _poolCenter[name].Enqueue(item);
  83.         }
  84.         else
  85.         {
  86.             // 当请求的对象池不存在时
  87.             Debug.Log(message: $"当前请求的对象池{name}不存在");
  88.         }
  89.     }
  90.     /// <summary>
  91.     /// 从对象池中尝试获取指定名称的对象(重载方法,不指定位置和旋转)
  92.     /// </summary>
  93.     /// <param name="name">要获取的对象池名称</param>
  94.     /// <returns>获取到的游戏对象,若对象池不存在则返回null</returns>
  95.     public GameObject TryGetPoolItem(string name)
  96.     {
  97.         // 检查对象池容器中是否存在指定名称的对象池
  98.         if (_poolCenter.ContainsKey(name))
  99.         {
  100.             // 从对应名称的对象池队列中取出队首的对象
  101.             var item = _poolCenter[name].Dequeue();
  102.             // 激活对象
  103.             item.SetActive(true);
  104.             // 将使用后的对象重新放回队列尾部
  105.             _poolCenter[name].Enqueue(item);
  106.             return item;
  107.         }
  108.         // 当请求的对象池不存在时
  109.         Debug.Log(message: $"当前请求的对象池{name}不存在");
  110.         return null;
  111.     }
  112. }
复制代码
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. /// <summary>
  5. /// 对象池物品接口
  6. /// </summary>
  7. public interface IPoolItem
  8. {
  9.     void Spawn();   // 当对象从对象池取出、激活时执行的逻辑,比如初始化状态、显示特效等
  10.     void Recycle(); // 当对象回收到对象池时执行的逻辑,比如重置状态、隐藏对象等
  11. }
  12. /// <summary>
  13. /// 对象池物品基类,继承自MonoBehaviour并实现IPoolItem接口
  14. /// 作为具体对象池物品(如子弹、道具等)的抽象父类,封装通用逻辑
  15. /// </summary>
  16. public abstract class PoolItemBase : MonoBehaviour, IPoolItem
  17. {
  18.     private void OnEnable()
  19.     {
  20.         Spawn();
  21.     }
  22.     private void OnDisable()
  23.     {
  24.         Recycle();
  25.     }
  26.     public virtual void Spawn()
  27.     {
  28.     }
  29.     public virtual void Recycle()
  30.     {
  31.     }
  32. }
复制代码
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. /// <summary>
  5. /// 声音类型枚举
  6. /// </summary>
  7. public enum SoundType
  8. {
  9.     ATK,    // 攻击
  10.     HIT,    // 受击
  11.     BLOCK,  // 格挡
  12.     FOOT    // 脚步
  13. }
  14. /// <summary>
  15. /// 声音对象池物品类
  16. /// 用于管理音效播放对象的激活、回收,复用AudioSource
  17. /// </summary>
  18. public class PoolItemSound : PoolItemBase
  19. {
  20.     // 音频源
  21.     private AudioSource _audioSource;
  22.     [SerializeField] SoundType _soundType;
  23.     private void Awake()
  24.     {
  25.         _audioSource = GetComponent();
  26.     }
  27.     /// <summary>
  28.     /// 音效对象从对象池取出时的逻辑
  29.     /// </summary>
  30.     public override void Spawn()
  31.     {
  32.         //PlaySound(_soundType);
  33.     }
  34.     private void PlaySound(SoundType _soundType)
  35.     {
  36.     }
  37. }
复制代码
  1. using System.Collections.Generic;
  2. using UnityEngine;
  3. namespace Spiderman.Assets
  4. {
  5.     // 自定义创建Asset的菜单,方便在Unity编辑器右键创建该资源
  6.     [CreateAssetMenu(fileName = "Sound", menuName = "CreateActions/Assets/Sound", order = 0)]
  7.     public class AssetsSoundSO : ScriptableObject
  8.     {
  9.         // 序列化的内部类,用于配置声音类型和对应的音频片段数组
  10.         [System.Serializable]
  11.         private class SoundConfig
  12.         {
  13.             public SoundType SoundType;     // 声音类型,需有对应的枚举定义(代码里未展示,需确保存在)
  14.             public AudioClip[] AudioClips;  // 该类型声音对应的音频片段数组
  15.         }
  16.         // 声音配置列表,可在Inspector中配置不同类型声音及其音频片段
  17.         [SerializeField]
  18.         private List<SoundConfig> _configSound = new List<SoundConfig>();
  19.     }
  20. }
复制代码
敌人AI

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

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

13.png

14.png

随机待机动画系统
  1.         /// <summary>
  2.         /// 根据声音类型获取对应的音频片段
  3.         /// </summary>
  4.         /// <param name="_soundType"></param>
  5.         /// <returns></returns>
  6.         public AudioClip GetAudioClip(SoundType _soundType)
  7.         {
  8.             if(_configSound == null || _configSound.Count == 0)
  9.                 return null;
  10.             switch (_soundType)
  11.             {
  12.                 //随机返回对应类型的音频片段
  13.                 case SoundType.ATK:
  14.                     return _configSound[0].AudioClips[Random.Range(0, _configSound[0].AudioClips.Length)];
  15.                 case SoundType.HIT:
  16.                     return _configSound[1].AudioClips[Random.Range(0, _configSound[1].AudioClips.Length)];
  17.                 case SoundType.BLOCK:
  18.                     return _configSound[2].AudioClips[Random.Range(0, _configSound[2].AudioClips.Length)];
  19.                 case SoundType.FOOT:
  20.                     return _configSound[3].AudioClips[Random.Range(0, _configSound[3].AudioClips.Length)];
  21.             }
  22.             return null;
  23.         }
复制代码
演示效果(倍速处理)
15.gif

开启物理碰撞交互
  1. using Spiderman.Assets;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. /// <summary>
  6. /// 声音类型枚举
  7. /// </summary>
  8. public enum SoundType
  9. {
  10.     ATK,    // 攻击
  11.     HIT,    // 受击
  12.     BLOCK,  // 格挡
  13.     FOOT    // 脚步
  14. }
  15. /// <summary>
  16. /// 声音对象池物品类
  17. /// 用于管理音效播放对象的激活、回收,复用AudioSource
  18. /// </summary>
  19. public class PoolItemSound : PoolItemBase
  20. {
  21.     // 音频源
  22.     private AudioSource _audioSource;
  23.     [SerializeField] SoundType _soundType;
  24.     [SerializeField] AssetsSoundSO _soundAssets;
  25.     private void Awake()
  26.     {
  27.         _audioSource = GetComponent();
  28.     }
  29.     /// <summary>
  30.     /// 音效对象从对象池取出
  31.     /// </summary>
  32.     public override void Spawn()
  33.     {
  34.         //被激活的时候播放音效
  35.         PlaySound();
  36.     }
  37.     /// <summary>
  38.     /// 播放音效
  39.     /// </summary>
  40.     private void PlaySound()
  41.     {
  42.         _audioSource.clip = _soundAssets.GetAudioClip(_soundType);
  43.         _audioSource.Play();
  44.         // 回收音效对象
  45.         StartRecycle();
  46.     }
  47.     /// <summary>
  48.     /// 音效对象回收
  49.     /// </summary>
  50.     private void StartRecycle()
  51.     {
  52.         // 延迟0.3秒后停止播放
  53.         TimerManager.MainInstance.TryGetOneTimer(0.3f, DisableSelf);
  54.     }
  55.     /// <summary>
  56.     /// 定时任务:停止播放
  57.     /// </summary>
  58.     private void DisableSelf()
  59.     {
  60.         _audioSource.Stop();
  61.         gameObject.SetActive(false);
  62.     }
  63. }
复制代码
16.png

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

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


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

相关推荐

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