新版输入系统——斜向移动变快问题解决
生成对应的input管理脚本
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();
- }
- protected virtual void Start()
- {
- _fallOutDeltaTime = _fallOutTime;
- }
- private void Update()
- {
- SetCharacterGravity();
- UpdateCharacterGravity();
- }
- /// <summary>
- /// 地面检测方法
- /// </summary>
- /// <returns>返回角色是否在地面的布尔值</returns>
- private bool GroundDetection()
- {
- // 构建检测位置:基于角色当前位置,调整 Y 轴偏移(用于地面检测的位置修正)
- Vector3 detectionPosition = new Vector3(
- transform.position.x,
- transform.position.y - _groundDetectionPositionOffset,
- transform.position.z
- );
- // 球形检测:检查在指定位置、指定半径范围内,与 _whatIsGround 层的碰撞体是否存在相交
- // 参数分别为:检测中心、检测半径、地面层掩码、忽略触发器交互
- return Physics.CheckSphere(
- detectionPosition,
- _detectionRang,
- _whatIsGround,
- QueryTriggerInteraction.Ignore
- );
- }
- /// <summary>
- /// 根据是否在地面设置对应的角色重力逻辑
- /// </summary>
- private void SetCharacterGravity()
- {
- // 检测角色是否在地面
- _characterIsOnGround = GroundDetection();
- if (_characterIsOnGround)
- {
- //1.在地面
- // 1.1 重置下落等待时间
- _fallOutDeltaTime = _fallOutTime;
- // 1.2 重置垂直速度(防止落地后持续累积速度)
- if (_characterVerticalVelocity < 0)
- {
- _characterVerticalVelocity = -2f;
- }
- }
- else
- {
- //2.不在地面
- if (_fallOutDeltaTime > 0)
- {
- // 2.1 处理楼梯/小落差:等待 0.15 秒后再应用重力
- _fallOutDeltaTime -= Time.deltaTime;
- }
- else
- {
- // 2.2 倒计时结束还没有落地?那说明不是小落差,要开始应用重力
- }
- if (_characterVerticalVelocity < _characterVerticalMaxVelocity)
- {
- _characterVerticalVelocity += CharacterGravity * Time.deltaTime;
- // 重力公式累积垂直速度
- }
- }
- }
- /// <summary>
- /// 更新角色垂直方向移动(应用重力效果)
- /// </summary>
- private void UpdateCharacterGravity()
- {
- //这里只处理 y 轴重力
- // x/z 由其他移动逻辑控制
- Vector3 _characterVerticalDirection = new Vector3(0, _characterVerticalVelocity, 0);
- // 通过 CharacterController 应用y轴移动
- _controller.Move(_characterVerticalDirection * Time.deltaTime);
- }
- /// <summary>
- /// 斜坡方向重置:检测角色是否在坡上移动,防止下坡速度过快导致异常
- /// </summary>
- /// <param name="moveDirection">原始移动方向</param>
- /// <returns>适配斜坡后的移动方向</returns>
- private Vector3 SlopResetDirection(Vector3 moveDirection)
- {
- // 射线检测参数配置
- Vector3 rayOrigin = transform.position + transform.up * 0.5f; // 射线起点
- Vector3 rayDirection = Vector3.down; // 射线方向
- float maxDistance = _controller.height * 0.85f; // 射线最大距离
- LayerMask targetLayer = _whatIsGround; // 检测的目标地面层
- QueryTriggerInteraction triggerInteraction = QueryTriggerInteraction.Ignore; // 忽略触发器
- // 执行向下的射线检测
- if (Physics.Raycast(rayOrigin, rayDirection, out RaycastHit hit, maxDistance, targetLayer, triggerInteraction))
- {
- // 点积判断:检测地面法线是否与角色上方向垂直(点积接近0表示垂直,非0则说明有坡度)
- if (Vector3.Dot(transform.up, hit.normal) != 0)
- {
- // 将移动方向投影到斜坡平面
- moveDirection = Vector3.ProjectOnPlane(moveDirection, hit.normal);
- }
- }
- return moveDirection;
- }
- private void OnDrawGizmos()
- {
- // 设置gizmos颜色为红色,使其更容易看到
- Gizmos.color = Color.red;
-
- Vector3 detectionPosition = new Vector3(
- transform.position.x,
- transform.position.y - _groundDetectionPositionOffset,
- transform.position.z
- );
- Gizmos.DrawWireSphere(detectionPosition, _detectionRang);
- }
- }
- }
复制代码 PlayerMovementControl- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- namespace Spiderman.Movement
- {
- public class PlayerMovementControl : CharacterMovementControlBase
- {
- }
- }
复制代码 Day02 带碰撞体相机脚本
GameInputManager- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- public class GameInputManager : MonoBehaviour
- {
- private GameInputAction _gameInputAction;
- public Vector2 Movement => _gameInputAction.Player.Movement.ReadValue<Vector2>();
- public Vector2 CameraLook => _gameInputAction.Player.CameraLook.ReadValue<Vector2>();
- private void Awake()
- {
- _gameInputAction ??= new GameInputAction(); //是空的,则创建新的实例
- }
- private void OnEnable()
- {
- _gameInputAction.Enable();
- }
- private void OnDisable()
- {
- _gameInputAction.Disable();
- }
- }
复制代码
TP_CameraControl- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- public class TP_CameraControl : MonoBehaviour
- {
- private GameInputManager _gameInputManager;
- [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
动画部分
脚本
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;
- }
- }
复制代码 TimerManagerDay07 脚部拖尾特效的控制——奔跑时启用
- using UnityEngine;
- using System.Collections;
- public class ObjectVisibilityController : MonoBehaviour
- {
- // 在 Inspector 中手动拖入需要控制的子物体
- public GameObject targetChild;
- public Animator playerAnimator;
- // 存储当前目标状态,用于判断是否需要执行状态切换
- private bool _currentTargetState;
- // 标记是否正在等待延迟,避免重复启动协程
- private bool _isWaiting = false;
- private void Update()
- {
- // 获取动画状态的当前值
- bool desiredState = playerAnimator.GetBool(AnimationID.RunID);
- // 如果状态发生变化且不在等待状态,则启动延迟协程
- if (desiredState != _currentTargetState && !_isWaiting)
- {
- StartCoroutine(ChangeStateAfterDelay(desiredState, 0.5f));
- }
- }
- // 延迟改变状态的协程
- private IEnumerator ChangeStateAfterDelay(bool newState, float delay)
- {
- _isWaiting = true; // 标记为正在等待
- yield return new WaitForSeconds(delay); // 等待指定秒数
- // 应用新状态
- targetChild.SetActive(newState);
- _currentTargetState = newState;
- _isWaiting = false; // 重置等待标记
- }
- }
复制代码
Day08 IKController——头部IK跟随相机(平滑控制)
IKController- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- public class IKController : MonoBehaviour
- {
- public Animator _animator;
- //IK控制点
- //四肢关节点
- public Transform ik_LHand;
- public Transform ik_RHand;
- public Transform ik_LFoot;
- public Transform ik_RFoot;
- //头部控制点,可以根据主相机的位置,让玩家能够从侧视角下看到头部偏转。
- public Transform Head_IKPoint;
- [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);
- }
- }
复制代码 修改之前的正面处决条件,加上角度限制- 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
- }
复制代码 Bug修复——让处决的Combo索引单独控制
- 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;
- }
- }
复制代码- 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);
- }
- }
复制代码 region 触发伤害(普通攻击和处决攻击)
- 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()
- {
- }
- }
复制代码- 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();
- }
- /// <summary>
- /// 音效对象从对象池取出时的逻辑
- /// </summary>
- public override void Spawn()
- {
- //PlaySound(_soundType);
- }
- private void PlaySound(SoundType _soundType)
- {
- }
- }
复制代码- 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>();
- }
- }
复制代码 敌人AI
这个还没看明白,时间不太够,后面做
Foot IK
方案:实际走的是斜面(不渲染),footik识别的是阶梯本身(渲染)
优点:相机平滑,角色走路平滑,只对脚部进行ik
随机待机动画系统
- /// <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;
- }
复制代码 演示效果(倍速处理)
开启物理碰撞交互
- 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();
- }
- /// <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);
- }
- }
复制代码
加上手部扶墙的程序动画刚好完美推门(不过要记得的删去非法角度的判断)
注意,开启物理碰撞之后,之前的扶墙物体需要把Kinematic打开,让他固定住不受外力,不然墙倒了
不过推门还是尽量做单独的animator动画比较好,用扶墙的程序动画看着还行,但还是用固定的动画不容易出戏
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |