写在前面:
真正实现这些细枝末节的东西的时候才能感受到这种技术力的恐怖。
——致敬顽皮狗工作室
插件安装
为角色添加组件
右手同理
状态机脚本编写
BaseState.cs- using UnityEngine;
- using System;
- /// <summary>
- /// 状态基类,定义了状态机中所有状态的基本行为规范
- /// 泛型参数EEState限制为枚举类型,用于表示具体的状态类型
- /// </summary>
- /// <typeparam name="EState">状态枚举类型,继承自Enum</typeparam>
- public abstract class BaseState<EState> where EState : Enum
- {
- //构造函数
- public BaseState(EState key)
- {
- StateKey = key;
- }
- public EState StateKey { get; private set; }
- public abstract void EnterState();
- public abstract void ExitState();
- public abstract void UpdateState();
- public abstract EState GetNextState();
- public abstract void OnTriggerEnter(Collider other);
- public abstract void OnTriggerStay(Collider other);
- public abstract void OnTriggerExit(Collider other);
- }
复制代码 NewBaseState.cs- using UnityEngine;
- using System;
- using System.Collections.Generic;
- /// <summary>
- /// 状态管理器泛型抽象类
- /// </summary>
- /// <typeparam name="EState">状态枚举类型,需继承自Enum</typeparam>
- public abstract class StateManager<EState> : MonoBehaviour where EState : Enum
- {
- // 存储所有状态的字典,键为状态枚举,值为对应的状态实例
- protected Dictionary<EState, BaseState<EState>> States = new Dictionary<EState, BaseState<EState>>();
- // 当前激活的状态
- protected BaseState<EState> CurrentState;
- // 标志位:是否处于状态切换中
- protected bool IsTransitioningState = false;
- void Start()
- {
- CurrentState.EnterState();
- }
- void Update()
- {
- EState nextStateKey = CurrentState.GetNextState();
- if (!IsTransitioningState && nextStateKey.Equals(CurrentState.StateKey))
- {
- // 如果当前状态和下一状态相同,则更新当前状态
- CurrentState.UpdateState();
- }
- else if(!IsTransitioningState)
- {
- // 不同,则切换到下一状态
- TransitionToState(nextStateKey);
- }
- }
- /// <summary>
- /// 状态切换方法,用于从当前状态切换到目标状态
- /// </summary>
- /// <param name="stateKey">目标状态的枚举标识</param>
- protected virtual void TransitionToState(EState stateKey)
- {
- IsTransitioningState = true;
- // 退出当前状态
- CurrentState.ExitState();
- // 进入目标状态
- CurrentState = States[stateKey];
- CurrentState.EnterState();
- IsTransitioningState = false;
- }
- /// <summary>
- /// 当碰撞体进入触发器时调用的方法,转发给当前状态处理
- /// </summary>
- /// <param name="other">进入触发器的碰撞体</param>
- void OnTriggerEnter(Collider other)
- {
- CurrentState.OnTriggerEnter(other);
- }
- /// <summary>
- /// 当碰撞体持续处于触发器中时调用的方法,转发给当前状态处理
- /// </summary>
- /// <param name="other">处于触发器中的碰撞体</param>
- void OnTriggerStay(Collider other)
- {
- CurrentState.OnTriggerStay(other);
- }
- /// <summary>
- /// 当碰撞体退出触发器时调用的方法,转发给当前状态处理
- /// </summary>
- /// <param name="other">退出触发器的碰撞体</param>
- void OnTriggerExit(Collider other)
- {
- CurrentState.OnTriggerExit(other);
- }
- }
复制代码 Animation Rigging
Rig Builder组件要放在Animator的同级
Rig放置的位置
环境交互状态机的编写
EnvironmentInteractionStateMachine- using UnityEngine;
- using UnityEngine.Animations.Rigging;
- using UnityEngine.Assertions; //调试用
- public class EnvironmentInteractionStateMachine : StateManager<EnvironmentInteractionStateMachine.EEnvironmentInteractionState>
- {
- // 环境交互状态
- public enum EEnvironmentInteractionState
- {
- Search, // 搜索状态
- Approach, // 接近状态
- Rise, // 起身状态
- Touch, // 触碰状态
- Reset // 重置状态
- }
- private EnvironmentInteractionContext _context;
- // 约束、组件等引用
- [SerializeField] private TwoBoneIKConstraint _leftIkConstraint;
- [SerializeField] private TwoBoneIKConstraint _rightIkConstraint;
- [SerializeField] private MultiRotationConstraint _leftMultiRotationConstraint;
- [SerializeField] private MultiRotationConstraint _rightMultiRotationConstraint;
- [SerializeField] private CharacterController characterController;
- void Awake()
- {
- ValidateConstraints();
- _context = new EnvironmentInteractionContext(_leftIkConstraint, _rightIkConstraint, _leftMultiRotationConstraint, _rightMultiRotationConstraint, characterController);
- }
- // 校验各类约束、组件是否正确赋值
- private void ValidateConstraints()
- {
- Assert.IsNotNull(_leftIkConstraint, "Left IK constraint 没有赋值");
- Assert.IsNotNull(_rightIkConstraint, "Right IK constraint 没有赋值");
- Assert.IsNotNull(_leftMultiRotationConstraint, "Left multi-rotation constraint 没有赋值");
- Assert.IsNotNull(_rightMultiRotationConstraint, "Right multi-rotation constraint 没有赋值");
- Assert.IsNotNull(characterController, "characterController used to control character 没有赋值");
- }
- }
复制代码 EnvironmentInteractionContext用来管理各种属性- using UnityEngine;
- using UnityEngine.Animations.Rigging;
- public class EnvironmentInteractionContext
- {
- private TwoBoneIKConstraint _leftIkConstraint;
- private TwoBoneIKConstraint _rightIkConstraint;
- private MultiRotationConstraint _leftMultiRotationConstraint;
- private MultiRotationConstraint _rightMultiRotationConstraint;
- private CharacterController _characterController;
- public EnvironmentInteractionContext(
- TwoBoneIKConstraint leftIkConstraint,
- TwoBoneIKConstraint rightIkConstraint,
- MultiRotationConstraint leftMultiRotationConstraint,
- MultiRotationConstraint rightMultiRotationConstraint,
- CharacterController characterController)
- {
- _leftIkConstraint = leftIkConstraint;
- _rightIkConstraint = rightIkConstraint;
- _leftMultiRotationConstraint = leftMultiRotationConstraint;
- _rightMultiRotationConstraint = rightMultiRotationConstraint;
- _characterController = characterController;
- }
- // 外部可以访问的属性
- public TwoBoneIKConstraint LeftIkConstraint => _leftIkConstraint;
- public TwoBoneIKConstraint RightIkConstraint => _rightIkConstraint;
- public MultiRotationConstraint LeftMultiRotationConstraint => _leftMultiRotationConstraint;
- public MultiRotationConstraint RightMultiRotationConstraint => _rightMultiRotationConstraint;
- public CharacterController CharacterController => _characterController;
- }
复制代码 从ResetState开始- using UnityEngine;
- public class ResetState : EnvironmentInteractionState
- {
- // 构造函数
- public ResetState(EnvironmentInteractionContext context, EnvironmentInteractionStateMachine.EEnvironmentInteractionState estate) : base(context, estate)
- {
- EnvironmentInteractionContext Context = context;
- }
- public override void EnterState(){}
- public override void ExitState() { }
- public override void UpdateState() { }
- public override EnvironmentInteractionStateMachine.EEnvironmentInteractionState GetNextState()
- {
- return StateKey;
- }
- public override void OnTriggerEnter(Collider other) { }
- public override void OnTriggerStay(Collider other) { }
- public override void OnTriggerExit(Collider other) { }
- }
复制代码 EnvironmentInteractionStateMachine中加入初始化函数- void Awake()
- {
- //原来的代码
- InitalizeStates();
- }
复制代码- /// <summary>
- /// 初始化状态机
- /// </summary>
- private void InitalizeStates()
- {
- //添加状态
- States.Add(EEnvironmentInteractionState.Reset, new ResetState(_context, EEnvironmentInteractionState.Reset));
- States.Add(EEnvironmentInteractionState.Search, new SearchState(_context, EEnvironmentInteractionState.Search));
- States.Add(EEnvironmentInteractionState.Approach, new ApproachState(_context, EEnvironmentInteractionState.Approach));
- States.Add(EEnvironmentInteractionState.Rise, new RiseState(_context, EEnvironmentInteractionState.Rise));
- States.Add(EEnvironmentInteractionState.Touch, new TouchState(_context, EEnvironmentInteractionState.Touch));
- //设置初始状态为Reset
- CurrentState = States[EEnvironmentInteractionState.Reset];
- }
复制代码
状态机运行正常
环境检测
1.在角色身上创建一个稍大于臂展的碰撞盒
EnvironmentInteractionStateMachine- void Awake()
- {
- ///原来的代码
- ConstructEnvironmentDetectionCollider();
- }
复制代码- /// <summary>
- /// 创建一个环境检测用的碰撞体
- /// </summary>
- private void ConstructEnvironmentDetectionCollider()
- {
- // 碰撞体大小的基准值
- float wingspan = characterController.height;
- // 给当前游戏对象添加盒型碰撞体组件
- BoxCollider boxCollider = gameObject.AddComponent<BoxCollider>();
- // 设置碰撞体大小为立方体,各边长度等于翼展
- boxCollider.size = new Vector3(wingspan, wingspan, wingspan);
- // 设置碰撞体中心位置
- // 基于角色控制器的中心位置进行偏移:
- // Y轴方向上移翼展的25%,Z轴方向前移翼展的50%
- boxCollider.center = new Vector3(
- characterController.center.x,
- characterController.center.y + (.25f * wingspan),
- characterController.center.z + (.5f * wingspan)
- );
- // 将碰撞体设置为触发器模式(用于检测碰撞而非物理碰撞响应)
- boxCollider.isTrigger = true;
- }
复制代码
2.碰撞体触发器的交互机制
- 角色进入 “触发器区域” → OnTriggerEnter 触发(一次)
- 角色持续待在区域内 → 每帧触发 OnTriggerStay
- 角色离开区域 → OnTriggerExit 触发(一次)
3.找到离角色更近的一侧,用来决定后面开启哪边的IK
在EnvironmentInteractionContext加入:判断碰撞相交位置更靠近哪一侧
- // 身体两侧
- public enum EBodySide
- {
- RIGHT,
- LEFT
- }
复制代码- // 当前IK约束
- public TwoBoneIKConstraint CurrentIkConstraint { get; private set; }
- // 当前多旋转约束
- public MultiRotationConstraint CurrentMultiRotationConstraint { get; private set; }
- // 当前IK控制的目标位置
- public Transform CurrentIkTargetTransform { get; private set; }
- // 当前肩部骨骼
- public Transform CurrentShoulderTransform { get; private set; }
- // 当前身体的侧边(左或右)
- public EBodySide CurrentBodySide { get; private set; }
- /// <summary>
- /// 根据传入位置,判断目标更靠近左侧还是右侧肩部,设置当前身体的侧边
- /// </summary>
- /// <param name="positionToCheck">需要检测的目标位置</param>
- public void SetCurrentSide(Vector3 positionToCheck)
- {
- // 左肩部骨骼
- Vector3 leftShoulder = _leftIkConstraint.data.root.transform.position;
- // 右肩部骨骼
- Vector3 rightShoulder = _rightIkConstraint.data.root.transform.position;
- // 标志位:目标位置是否更靠近左侧
- bool isLeftCloser = Vector3.Distance(positionToCheck, leftShoulder) <
- Vector3.Distance(positionToCheck, rightShoulder);
- if (isLeftCloser)
- {
- CurrentBodySide = EBodySide.LEFT;
- CurrentIkConstraint = _leftIkConstraint;
- CurrentMultiRotationConstraint = _leftMultiRotationConstraint;
- }
- else
- {
- CurrentBodySide = EBodySide.RIGHT;
- CurrentIkConstraint = _rightIkConstraint;
- CurrentMultiRotationConstraint = _rightMultiRotationConstraint;
- }
- // 记录当前肩部骨骼 和 IK控制的目标位置
- CurrentShoulderTransform = CurrentIkConstraint.data.root.transform;
- CurrentIkTargetTransform = CurrentIkConstraint.data.target.transform;
- }
复制代码 EnvironmentInteractionState- /// <summary>
- /// 启动 IK 目标位置追踪
- /// </summary>
- /// <param name="intersectingCollider">相交的碰撞体,作为追踪关联对象</param>
- protected void StartIkTargetPositionTracking(Collider intersectingCollider)
- {
- //只有碰撞体的层级为Interactable时才进行IK目标位置追踪
- if (intersectingCollider.gameObject.layer == LayerMask.NameToLayer("Interactable"))
- {
- // 最近的碰撞点
- Vector3 closestPointFromRoot = GetClosestPointOnCollider(intersectingCollider, Context.RootTransform.position);
- // 设置当前更靠近的侧面(根据最近的碰撞点)
- Context.SetCurrentSide(closestPointFromRoot);
- }
- }
- /// <summary>
- /// 更新 IK 目标位置
- /// </summary>
- /// <param name="intersectingCollider">相交的碰撞体,依据其状态更新目标位置</param>
- protected void UpdateIkTargetPosition(Collider intersectingCollider)
- {
- }
- /// <summary>
- /// 重置 IK 目标位置追踪
- /// </summary>
- /// <param name="intersectingCollider">相交的碰撞体,针对其执行追踪重置</param>
- protected void ResetIkTargetPositionTracking(Collider intersectingCollider)
- {
- }
复制代码 这里要用到一个新的变量RootTransform用来在GetClosestPointOnCollider()方法中传入参数positionToCheck
EnvironmentInteractionContext- // 根对象
- private Transform _rootTransform;
复制代码 构造函数要加入这个变量- public EnvironmentInteractionContext(
- TwoBoneIKConstraint leftIkConstraint,
- TwoBoneIKConstraint rightIkConstraint,
- MultiRotationConstraint leftMultiRotationConstraint,
- MultiRotationConstraint rightMultiRotationConstraint,
- CharacterController characterController,
- Transform rootTransform)
- {
- _leftIkConstraint = leftIkConstraint;
- _rightIkConstraint = rightIkConstraint;
- _leftMultiRotationConstraint = leftMultiRotationConstraint;
- _rightMultiRotationConstraint = rightMultiRotationConstraint;
- _characterController = characterController;
- _rootTransform = rootTransform;
- }
复制代码- public Transform RootTransform => _rootTransform;
复制代码 当然,在EnvironmentInteractionStateMachine中也要传入这个变量
Awake()- _context = new EnvironmentInteractionContext(_leftIkConstraint, _rightIkConstraint, _leftMultiRotationConstraint, _rightMultiRotationConstraint, characterController,transform.root);
复制代码 写一下ResetState的GetNextState()的下一状态切换逻辑- public override EnvironmentInteractionStateMachine.EEnvironmentInteractionState GetNextState()
- {
- // 下一个状态为 SearchState
- return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Search;
- //return StateKey;
- }
复制代码注意:
在SearchState的OnTriggerEnter()中调用StartIkTargetPositionTracking()启动 IK 目标位置追踪- public override void OnTriggerEnter(Collider other) {
- // 进入搜索状态时,开始跟踪目标位置
- StartIkTargetPositionTracking(other);
- }
复制代码 测试一下功能是否正常:
效果倒是正常,不过这是我调试好久发现的问题,只有挂载rigidbody的物体才会触发Trigger回调函数,正常来说只要一方有rigidbody就能触发,不知道为什么这里会出现这个问题,角色身上的这个触发器肯定是rigidbody,那已经满足条件了,为什么还要其他物体也要挂载rigidbody,想不明白。。。
不过实现了就好,后面再排查问题吧,先完成最要紧
4.解决一下在狭窄通道走过的时候,左右频繁触发的问题
EnvironmentInteractionContext- // 当前交互的碰撞体
- public Collider CurrentIntersectingCollider { get; set; }
复制代码 EnvironmentInteractionState- /// <summary>
- /// 启动 IK 目标位置追踪
- /// </summary>
- /// <param name="intersectingCollider">相交的碰撞体,作为追踪关联对象</param>
- protected void StartIkTargetPositionTracking(Collider intersectingCollider)
- {
- //只有碰撞体的层级为Interactable && 当前没有可交互的碰撞体 时才进行IK目标位置追踪
- // 防止频繁触发
- if (intersectingCollider.gameObject.layer == LayerMask.NameToLayer("Interactable") && Context.CurrentIntersectingCollider == null)
- {
- // 记录当前碰撞体
- Context.CurrentIntersectingCollider = intersectingCollider;
- // 最近的碰撞点
- Vector3 closestPointFromRoot = GetClosestPointOnCollider(intersectingCollider, Context.RootTransform.position);
- // 设置当前更靠近的侧面(根据最近的碰撞点)
- Context.SetCurrentSide(closestPointFromRoot);
- }
- }
复制代码- /// <summary>
- /// 重置 IK 目标位置追踪
- /// </summary>
- /// <param name="intersectingCollider">相交的碰撞体,针对其执行追踪重置</param>
- protected void ResetIkTargetPositionTracking(Collider intersectingCollider)
- {
- if(intersectingCollider == Context.CurrentIntersectingCollider)
- {
- Context.CurrentIntersectingCollider = null;
- }
- }
复制代码 SearchState- public override void OnTriggerEnter(Collider other) {
- Debug.Log("Trigger:Enter");
- // 进入搜索状态,开始跟踪目标位置
- StartIkTargetPositionTracking(other);
- }
- public override void OnTriggerStay(Collider other) { }
- public override void OnTriggerExit(Collider other) {
- Debug.Log("Trigger:Exit");
- // 退出搜索状态,停止跟踪目标位置
- ResetIkTargetPositionTracking(other);
- }
复制代码 5.设置IK的目标位置
EnvironmentInteractionContext- // 相交碰撞体的最近点——默认值设为无穷大
- public Vector3 ClosestPointOnColliderFromShoulder { get; set; } = Vector3.positiveInfinity;
复制代码 EnvironmentInteractionState- /// <summary>
- /// 设置 IK 目标位置
- /// </summary>
- /// <param name="targetPosition"></param>
- private void SetIkTargetPosition()
- {
- // 最近的碰撞点
- Context.ClosestPointOnColliderFromShoulder = GetClosestPointOnCollider(Context.CurrentIntersectingCollider, Context.CurrentShoulderTransform.position);
- }
复制代码- /// <summary>
- /// 启动 IK 目标位置追踪
- /// </summary>
- /// <param name="intersectingCollider">相交的碰撞体,作为追踪关联对象</param>
- protected void StartIkTargetPositionTracking(Collider intersectingCollider)
- {
- //只有碰撞体的层级为Interactable && 当前没有可交互的碰撞体 时才进行IK目标位置追踪
- // 防止频繁触发
- if (intersectingCollider.gameObject.layer == LayerMask.NameToLayer("Interactable") && Context.CurrentIntersectingCollider == null)
- {
- // 原来的代码不变
- //设置IK目标位置
- SetIkTargetPosition();
- }
- }
复制代码- /// <summary>
- /// 更新 IK 目标位置
- /// </summary>
- /// <param name="intersectingCollider">相交的碰撞体,依据其状态更新目标位置</param>
- protected void UpdateIkTargetPosition(Collider intersectingCollider)
- {
- // 在接触过程中,一直更新IK目标位置
- if (Context.CurrentIntersectingCollider == intersectingCollider)
- {
- SetIkTargetPosition();
- }
- }
复制代码 SearchState- public override void OnTriggerStay(Collider other) {
- // 跟踪目标位置
- UpdateIkTargetPosition(other);
- }
复制代码 然后在EnvironmentInteractionStateMachine中加入可视化- /// <summary>
- /// 当物体被选中时调用Gizmos绘制
- /// </summary>
- private void OnDrawGizmosSelected()
- {
- Gizmos.color = Color.red;
- // 在最近碰撞点处绘制一个红色的球
- if (_context != null && _context.ClosestPointOnColliderFromShoulder != null)
- {
- Gizmos.DrawSphere(_context.ClosestPointOnColliderFromShoulder, 0.03f);
- }
- }
复制代码
新的问题出现了:
当角色行走的时候,由于身体会浮动,这个最近的碰撞点也在上下浮动,后面加上动画会出现手一直在墙上 上下乱摸。。。
6.解决最近碰撞点上下浮动问题
其实加一个变量记录一下角色的肩高就行,设定ik位置的时候传入该参数,这个点的高度就保持不变了
EnvironmentInteractionContext的构造函数加入一个角色的肩部高度变量- public EnvironmentInteractionContext(
- TwoBoneIKConstraint leftIkConstraint,
- TwoBoneIKConstraint rightIkConstraint,
- MultiRotationConstraint leftMultiRotationConstraint,
- MultiRotationConstraint rightMultiRotationConstraint,
- CharacterController characterController,
- Transform rootTransform)
- {
- _leftIkConstraint = leftIkConstraint;
- _rightIkConstraint = rightIkConstraint;
- _leftMultiRotationConstraint = leftMultiRotationConstraint;
- _rightMultiRotationConstraint = rightMultiRotationConstraint;
- _characterController = characterController;
- _rootTransform = rootTransform;
- CharacterShoulderHeight = leftIkConstraint.data.root.transform.position.y;
- }
复制代码- // 角色的肩部高度,用来约束Ik的高度
- public float CharacterShoulderHeight { get; private set; }
复制代码 EnvironmentInteractionState传入目标位置的参数的y轴改成角色肩高CharacterShoulderHeight- /// <summary>
- /// 设置 IK 目标位置
- /// </summary>
- /// <param name="targetPosition"></param>
- private void SetIkTargetPosition()
- {
- // 最近的碰撞点
- Context.ClosestPointOnColliderFromShoulder = GetClosestPointOnCollider(Context.CurrentIntersectingCollider,
- // 目标位置:上半身的xz位置 角色肩高的y位置(高度位置)
- new Vector3(Context.RootTransform.position.x, Context.CharacterShoulderHeight, Context.RootTransform.position.z));
- }
复制代码问题解决
7.在离开当前碰撞体后,重置Ik的目标位置为无穷大
EnvironmentInteractionState- /// <summary>
- /// 重置 IK 目标位置追踪
- /// </summary>
- /// <param name="intersectingCollider">相交的碰撞体,针对其执行追踪重置</param>
- protected void ResetIkTargetPositionTracking(Collider intersectingCollider)
- {
- if(intersectingCollider == Context.CurrentIntersectingCollider)
- {
- // 重置当前碰撞体为空
- Context.CurrentIntersectingCollider = null;
- // 重置IK目标位置为无穷大
- Context.ClosestPointOnColliderFromShoulder = Vector3.positiveInfinity;
- }
- }
复制代码 效果:
8.开始对手部的IK组件目标位置进行更新
注意:需要为ik的目标位置加一个法向的偏移,防止手部穿模(因为手是有厚度的,不是纸片人)
EnvironmentInteractionState- /// <summary>
- /// 设置 IK 目标位置
- /// </summary>
- /// <param name="targetPosition"></param>
- private void SetIkTargetPosition()
- {
- // 最近的碰撞点
- Context.ClosestPointOnColliderFromShoulder = GetClosestPointOnCollider(Context.CurrentIntersectingCollider,
- // 目标位置:上半身的xz位置 角色肩高的y位置(高度位置)
- new Vector3(Context.RootTransform.position.x, Context.CharacterShoulderHeight, Context.RootTransform.position.z));
- #region 让手部的IK目标移动到这个最近碰撞点
- // 1. 射线方向:从“最近碰撞点”指向“当前肩部位置”的向量
- Vector3 rayDirection = Context.CurrentShoulderTransform.position
- - Context.ClosestPointOnColliderFromShoulder;
- // Unity 中向量的运算:Vector3 终点 - Vector3 起点
- // 2. 归一化,得到单位向量
- Vector3 normalizedRayDirection = rayDirection.normalized;
- // 3. 偏移距离,防止手部穿模
- float offsetDistance = 0.05f;
- // 4. 最终要到达的位置:在“最近碰撞点”基础上,加上 沿rayDirection射线方向偏移 offsetDistance 距离
- Vector3 targettPosition = Context.ClosestPointOnColliderFromShoulder
- + normalizedRayDirection * offsetDistance;
- // 5. 更新 IK 目标位置
- Context.CurrentIkTargetTransform.position = targettPosition;
- #endregion
- }
复制代码 如果把权重一开始就拉到1,效果是这样的:
当然,我们还得根据具体的状态写Ik权重的控制脚本
每个具体状态的Ik控制逻辑的脚本编写
也就是根据状态决定是否/怎样更新手部Two Bone IK Constraint的权重
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |