找回密码
 立即注册
首页 业界区 业界 仿神秘海域/美末环境交互的程序化动画学习 ...

仿神秘海域/美末环境交互的程序化动画学习

老僻贞 5 天前
写在前面:
真正实现这些细枝末节的东西的时候才能感受到这种技术力的恐怖。
——致敬顽皮狗工作室
插件安装


为角色添加组件





右手同理
状态机脚本编写


BaseState.cs
  1. using UnityEngine; using System; ///  /// 状态基类,定义了状态机中所有状态的基本行为规范 /// 泛型参数EEState限制为枚举类型,用于表示具体的状态类型 ///  /// 状态枚举类型,继承自Enum public abstract class BaseState 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
  1. using UnityEngine; using System; using System.Collections.Generic; ///  /// 状态管理器泛型抽象类 ///  /// 状态枚举类型,需继承自Enum public abstract class StateManager : MonoBehaviour where EState : Enum { // 存储所有状态的字典,键为状态枚举,值为对应的状态实例 protected Dictionary> States = new Dictionary>(); // 当前激活的状态 protected BaseState 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); } } ///  /// 状态切换方法,用于从当前状态切换到目标状态 ///  /// 目标状态的枚举标识 protected virtual void TransitionToState(EState stateKey) { IsTransitioningState = true; // 退出当前状态 CurrentState.ExitState(); // 进入目标状态 CurrentState = States[stateKey]; CurrentState.EnterState(); IsTransitioningState = false; } ///  /// 当碰撞体进入触发器时调用的方法,转发给当前状态处理 ///  /// 进入触发器的碰撞体 void OnTriggerEnter(Collider other) { CurrentState.OnTriggerEnter(other); } ///  /// 当碰撞体持续处于触发器中时调用的方法,转发给当前状态处理 ///  /// 处于触发器中的碰撞体 void OnTriggerStay(Collider other) { CurrentState.OnTriggerStay(other); } ///  /// 当碰撞体退出触发器时调用的方法,转发给当前状态处理 ///  /// 退出触发器的碰撞体 void OnTriggerExit(Collider other) { CurrentState.OnTriggerExit(other); } }
复制代码
Animation Rigging

Rig Builder组件要放在Animator的同级



Rig放置的位置


环境交互状态机的编写



EnvironmentInteractionStateMachine
  1. using UnityEngine; using UnityEngine.Animations.Rigging; using UnityEngine.Assertions; //调试用 public class EnvironmentInteractionStateMachine : StateManager { // 环境交互状态 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用来管理各种属性
  1. 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开始
  1. 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中加入初始化函数
  1. void Awake() { //原来的代码 InitalizeStates(); }
复制代码
  1. ///  /// 初始化状态机 ///  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
  1. void Awake() { ///原来的代码 ConstructEnvironmentDetectionCollider(); }
复制代码
  1. ///  /// 创建一个环境检测用的碰撞体 ///  private void ConstructEnvironmentDetectionCollider() { // 碰撞体大小的基准值 float wingspan = characterController.height; // 给当前游戏对象添加盒型碰撞体组件 BoxCollider boxCollider = gameObject.AddComponent(); // 设置碰撞体大小为立方体,各边长度等于翼展 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加入:判断碰撞相交位置更靠近哪一侧
  1. // 身体两侧 public enum EBodySide { RIGHT, LEFT }
复制代码
  1. // 当前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; } ///  /// 根据传入位置,判断目标更靠近左侧还是右侧肩部,设置当前身体的侧边 ///  /// 需要检测的目标位置 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
  1. ///  /// 启动 IK 目标位置追踪 ///  /// 相交的碰撞体,作为追踪关联对象 protected void StartIkTargetPositionTracking(Collider intersectingCollider) { //只有碰撞体的层级为Interactable时才进行IK目标位置追踪 if (intersectingCollider.gameObject.layer == LayerMask.NameToLayer(&quot;Interactable&quot;)) { // 最近的碰撞点 Vector3 closestPointFromRoot = GetClosestPointOnCollider(intersectingCollider, Context.RootTransform.position); // 设置当前更靠近的侧面(根据最近的碰撞点) Context.SetCurrentSide(closestPointFromRoot); } } ///  /// 更新 IK 目标位置 ///  /// 相交的碰撞体,依据其状态更新目标位置 protected void UpdateIkTargetPosition(Collider intersectingCollider) { } ///  /// 重置 IK 目标位置追踪 ///  /// 相交的碰撞体,针对其执行追踪重置 protected void ResetIkTargetPositionTracking(Collider intersectingCollider) { }
复制代码
这里要用到一个新的变量RootTransform用来在GetClosestPointOnCollider()方法中传入参数positionToCheck
EnvironmentInteractionContext
  1. // 根对象 private Transform _rootTransform;
复制代码
构造函数要加入这个变量
  1. 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; }
复制代码
  1. public Transform RootTransform => _rootTransform;
复制代码
当然,在EnvironmentInteractionStateMachine中也要传入这个变量
Awake()
  1. _context = new EnvironmentInteractionContext(_leftIkConstraint, _rightIkConstraint, _leftMultiRotationConstraint, _rightMultiRotationConstraint, characterController,transform.root);
复制代码
写一下ResetState的GetNextState()的下一状态切换逻辑
  1. public override EnvironmentInteractionStateMachine.EEnvironmentInteractionState GetNextState() { // 下一个状态为 SearchState return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Search; //return StateKey; }
复制代码
注意:
SearchStateOnTriggerEnter()中调用StartIkTargetPositionTracking()启动 IK 目标位置追踪
  1. public override void OnTriggerEnter(Collider other) { // 进入搜索状态时,开始跟踪目标位置 StartIkTargetPositionTracking(other); }
复制代码
测试一下功能是否正常:


效果倒是正常,不过这是我调试好久发现的问题,只有挂载rigidbody的物体才会触发Trigger回调函数,正常来说只要一方有rigidbody就能触发,不知道为什么这里会出现这个问题,角色身上的这个触发器肯定是rigidbody,那已经满足条件了,为什么还要其他物体也要挂载rigidbody,想不明白。。。
不过实现了就好,后面再排查问题吧,先完成最要紧
4.解决一下在狭窄通道走过的时候,左右频繁触发的问题

EnvironmentInteractionContext
  1. // 当前交互的碰撞体 public Collider CurrentIntersectingCollider { get; set; }
复制代码
EnvironmentInteractionState
  1. ///  /// 启动 IK 目标位置追踪 ///  /// 相交的碰撞体,作为追踪关联对象 protected void StartIkTargetPositionTracking(Collider intersectingCollider) { //只有碰撞体的层级为Interactable && 当前没有可交互的碰撞体 时才进行IK目标位置追踪 // 防止频繁触发 if (intersectingCollider.gameObject.layer == LayerMask.NameToLayer(&quot;Interactable&quot;) && Context.CurrentIntersectingCollider == null) { // 记录当前碰撞体 Context.CurrentIntersectingCollider = intersectingCollider; // 最近的碰撞点 Vector3 closestPointFromRoot = GetClosestPointOnCollider(intersectingCollider, Context.RootTransform.position); // 设置当前更靠近的侧面(根据最近的碰撞点) Context.SetCurrentSide(closestPointFromRoot); } }
复制代码
  1. ///  /// 重置 IK 目标位置追踪 ///  /// 相交的碰撞体,针对其执行追踪重置 protected void ResetIkTargetPositionTracking(Collider intersectingCollider) { if(intersectingCollider == Context.CurrentIntersectingCollider) { Context.CurrentIntersectingCollider = null; } }
复制代码
SearchState
  1. public override void OnTriggerEnter(Collider other) { Debug.Log(&quot;Trigger:Enter&quot;); // 进入搜索状态,开始跟踪目标位置 StartIkTargetPositionTracking(other); } public override void OnTriggerStay(Collider other) { } public override void OnTriggerExit(Collider other) { Debug.Log(&quot;Trigger:Exit&quot;); // 退出搜索状态,停止跟踪目标位置 ResetIkTargetPositionTracking(other); }
复制代码
5.设置IK的目标位置

EnvironmentInteractionContext
  1. // 相交碰撞体的最近点——默认值设为无穷大 public Vector3 ClosestPointOnColliderFromShoulder { get; set; } = Vector3.positiveInfinity;
复制代码
EnvironmentInteractionState
  1. ///  /// 设置 IK 目标位置 ///  ///  private void SetIkTargetPosition() { // 最近的碰撞点 Context.ClosestPointOnColliderFromShoulder = GetClosestPointOnCollider(Context.CurrentIntersectingCollider, Context.CurrentShoulderTransform.position); }
复制代码
  1. ///  /// 启动 IK 目标位置追踪 ///  /// 相交的碰撞体,作为追踪关联对象 protected void StartIkTargetPositionTracking(Collider intersectingCollider) { //只有碰撞体的层级为Interactable && 当前没有可交互的碰撞体 时才进行IK目标位置追踪 // 防止频繁触发 if (intersectingCollider.gameObject.layer == LayerMask.NameToLayer(&quot;Interactable&quot;) && Context.CurrentIntersectingCollider == null) { // 原来的代码不变 //设置IK目标位置 SetIkTargetPosition(); } }
复制代码
  1. ///  /// 更新 IK 目标位置 ///  /// 相交的碰撞体,依据其状态更新目标位置 protected void UpdateIkTargetPosition(Collider intersectingCollider) { // 在接触过程中,一直更新IK目标位置 if (Context.CurrentIntersectingCollider == intersectingCollider) { SetIkTargetPosition(); } }
复制代码
SearchState
  1. public override void OnTriggerStay(Collider other) { // 跟踪目标位置 UpdateIkTargetPosition(other); }
复制代码
然后在EnvironmentInteractionStateMachine中加入可视化
  1. ///  /// 当物体被选中时调用Gizmos绘制 ///  private void OnDrawGizmosSelected() { Gizmos.color = Color.red; // 在最近碰撞点处绘制一个红色的球 if (_context != null && _context.ClosestPointOnColliderFromShoulder != null) { Gizmos.DrawSphere(_context.ClosestPointOnColliderFromShoulder, 0.03f); } }
复制代码

新的问题出现了:
当角色行走的时候,由于身体会浮动,这个最近的碰撞点也在上下浮动,后面加上动画会出现手一直在墙上 上下乱摸。。。
6.解决最近碰撞点上下浮动问题

其实加一个变量记录一下角色的肩高就行,设定ik位置的时候传入该参数,这个点的高度就保持不变了
EnvironmentInteractionContext的构造函数加入一个角色的肩部高度变量
  1. 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; }
复制代码
  1. // 角色的肩部高度,用来约束Ik的高度 public float CharacterShoulderHeight { get; private set; }
复制代码
EnvironmentInteractionState传入目标位置的参数的y轴改成角色肩高CharacterShoulderHeight
  1. ///  /// 设置 IK 目标位置 ///  ///  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
  1. ///  /// 重置 IK 目标位置追踪 ///  /// 相交的碰撞体,针对其执行追踪重置 protected void ResetIkTargetPositionTracking(Collider intersectingCollider) { if(intersectingCollider == Context.CurrentIntersectingCollider) { // 重置当前碰撞体为空 Context.CurrentIntersectingCollider = null; // 重置IK目标位置为无穷大 Context.ClosestPointOnColliderFromShoulder = Vector3.positiveInfinity; } }
复制代码
效果:

8.开始对手部的IK组件目标位置进行更新

注意:需要为ik的目标位置加一个法向的偏移,防止手部穿模(因为手是有厚度的,不是纸片人)
EnvironmentInteractionState
  1. ///  /// 设置 IK 目标位置 ///  ///  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的权重
1.对现有代码进行一些小改动,更符合真实世界的运作机制

ResetState <-> SearchState:这个切换不应该是瞬时发生的,应该要加入一个延迟

1)先解决 ResetState -> SearchState
  1. // 持续时间计时器 float _elapsedTimer = 0.0f; // 持续时间的阈值 float _resetDuration = 2.0f;
复制代码
  1. public override void EnterState(){ // 重置 持续时间计时器 _elapsedTimer = 0.0f; // 重置 最近碰撞点 和 当前碰撞体 Context.ClosestPointOnColliderFromShoulder = Vector3.positiveInfinity; Context.CurrentIntersectingCollider = null; Debug.Log(&quot;ResetState EnterState&quot;); }
复制代码
  1. public override void UpdateState() { _elapsedTimer += Time.deltaTime; }
复制代码
  1. public override EnvironmentInteractionStateMachine.EEnvironmentInteractionState GetNextState() { bool isMoving = Context.CharacterController.velocity != Vector3.zero; //只有当持续时间超过阈值,且角色正在移动时,才会切换到 SearchState if(_elapsedTimer > _resetDuration && isMoving) { // 下一个状态为 SearchState return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Search; } return StateKey; }
复制代码
2)解决 SearchState 的状态跳转
  1. // 接近碰撞点的距离阈值 public float _approachDistanceThreshold = 2.0f;
复制代码
  1. public override EnvironmentInteractionStateMachine.EEnvironmentInteractionState GetNextState() { // 标志位:是否接近目标 bool isCloseToTarget = Vector3.Distance(Context.ClosestPointOnColliderFromShoulder, Context.RootTransform.position) < _approachDistanceThreshold; // 标志位:是否是最近碰撞点(只要不是无穷大,就是最近碰撞点) bool isClosestPointOnColliderValid = Context.ClosestPointOnColliderFromShoulder != Vector3.positiveInfinity; // 状态转移到接近状态ApproachState if (isCloseToTarget && isClosestPointOnColliderValid) { return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Approach; } return StateKey; }
复制代码
3)ApproachState
  1. // 接近状态的计时器 float _elapsedTimer = 0.0f; // 过渡时间 float _lerpduration = 5.0f; // 接近状态的目标权重 float _approachWeight = 0.5f;
复制代码
  1. public override void EnterState() { Debug.Log(&quot;ApproachState OnTriggerEnter&quot;); // 重置计时器 _elapsedTimer = 0.0f; } public override void ExitState() { } public override void UpdateState() { _elapsedTimer += Time.deltaTime; // 从当前的权重过渡到接近状态的权重 Context.CurrentIkConstraint.weight = Mathf.Lerp(Context.CurrentIkConstraint.weight, _approachWeight, _elapsedTimer / _lerpduration); }
复制代码
  1. public override void OnTriggerEnter(Collider other) { StartIkTargetPositionTracking(other); } public override void OnTriggerStay(Collider other) { UpdateIkTargetPosition(other); } public override void OnTriggerExit(Collider other) { ResetIkTargetPositionTracking(other); }
复制代码
现在能够在进入ApproachState状态时,随时间从当前的权重平滑过渡到Approach的目标权重
ApproachState状态需要让手部ik在更低的位置


以左手为例,面板做如下调整:

开始编写脚本,让进入Approach时手部ik目标高度在角色腰部,也就是角色碰撞体的中心的y轴坐标
EnvironmentInteractionContext
  1. // 交互点的Y轴偏移量,用来细调每个具体状态的交互点的高度 public float InteractionPoint_Y_Offset { get; set; } = 0.0f; // 角色碰撞体的中心点的高度 public float CharacterColliderCenterY { get; set; }
复制代码
EnvironmentInteractionStateMachine 的 ConstructEnvironmentDetectionCollider()
  1. _context.CharacterColliderCenterY = characterController.center.y;
复制代码
ResetState
  1. // 平滑过渡的持续时间 float _lerpDuration = 10.0f;
复制代码
  1. public override void UpdateState() { _elapsedTimer += Time.deltaTime; // 碰撞点的 Y 轴偏移,平滑过渡到角色碰撞体中心的高度 Context.InteractionPoint_Y_Offset = Mathf.Lerp(Context.InteractionPoint_Y_Offset, Context.CharacterColliderCenterY, _elapsedTimer / _lerpDuration); }
复制代码
EnvironmentInteractionState 的 SetIkTargetPosition(),y轴方向换成碰撞点的y轴偏移
  1. // 5. 更新 IK 目标位置 Context.CurrentIkTargetTransform.position = new Vector3( targettPosition.x, Context.InteractionPoint_Y_Offset, targettPosition.z);
复制代码
ApproachState状态需要手腕旋转到让手掌朝向地面,也就是Multi-Rotation Constraint组件需要权重过渡到一个目标值

  1. // 接近状态的IkConstraint目标权重 float _approachWeight = 0.5f; // 接近状态的MultiRotationConstraint目标旋转权重 float _approachRotationWeight = 0.75f; // 旋转速度 float _rotationSpeed = 500f;
复制代码
  1. public override void UpdateState() { //目标朝向:让手掌朝向地面,forwad=向下,up=角色的朝向 Quaternion targetGroundRotation = Quaternion.LookRotation(-Vector3.up, Context.RootTransform.forward); _elapsedTimer += Time.deltaTime; // 控制手腕旋转ik的控制器朝向 旋转到 目标朝向 Context.CurrentIkTargetTransform.rotation = Quaternion.RotateTowards( Context.CurrentIkTargetTransform.rotation, targetGroundRotation, _rotationSpeed * Time.deltaTime); // 更新权重:从当前的权重过渡到接近状态的对应权重 //MultiRotationConstraint: Context.CurrentMultiRotationConstraint.weight = Mathf.Lerp( Context.CurrentMultiRotationConstraint.weight, _approachRotationWeight, _elapsedTimer / _lerpduration); //IkConstraint: Context.CurrentIkConstraint.weight = Mathf.Lerp( Context.CurrentIkConstraint.weight, _approachWeight, _elapsedTimer / _lerpduration); }
复制代码

4) ApproachState的切换 -> RiseState / ResetState (有两种切换方式)

状态切换

如果继续接近碰撞点到一定距离阈值:ApproachState -> RiseState
如果在ApproachState状态持续时间超过一个阈值:ApproachState -> ResetState
ApproachState
  1. // 接近状态持续时间,超过就回到ResetState状态 float _approachDuration = 2.0f;
复制代码
  1. // 是否能切换到上升状态的距离阈值 float _riseDistanceThreshold = 0.5f;
复制代码
  1. public override EnvironmentInteractionStateMachine.EEnvironmentInteractionState GetNextState() { // 是否超过Approach状态的持续时间 bool isOverStateLifeTime = _elapsedTimer > _approachDuration; if (isOverStateLifeTime) { // 切换到Reset状态 return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Reset; } // 是否在手臂伸手范围内 bool isWithArmsReach = Vector3.Distance(Context.ClosestPointOnColliderFromShoulder, Context.CurrentShoulderTransform.position) < _riseDistanceThreshold; bool isClosestPointOnColliderValid = Context.ClosestPointOnColliderFromShoulder != Vector3.positiveInfinity; if (isWithArmsReach && isClosestPointOnColliderValid) { // 切换到上升状态 return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Rise; } return StateKey; }
复制代码
ResetState中重置权重

UpdateState()
  1. // 更新权重:平滑重置当前的权重 //MultiRotationConstraint: Context.CurrentMultiRotationConstraint.weight = Mathf.Lerp( Context.CurrentMultiRotationConstraint.weight, 0, _elapsedTimer / _lerpDuration); //IkConstraint: Context.CurrentIkConstraint.weight = Mathf.Lerp( Context.CurrentIkConstraint.weight, 0, _elapsedTimer / _lerpDuration);
复制代码
EnvironmentInteractionContext的构造函数加入身体侧边的默认设置,也就是把一侧Rig相关参数传入CurrentXXX参数(CurrentIkConstraint、CurrentMultiRotationConstraint)
  1. // 默认设置当前身体的侧边为无穷大 SetCurrentSide(Vector3.positiveInfinity);
复制代码


回到Reset之后,需要让ik控制器部件也回到原来的position和rotation


EnvironmentInteractionContext中记录初始position和rotation信息
  1. // 记录初始位置 private Vector3 _leftOriginalTransformPosition; private Vector3 _rightOriginalTransformPosition;
复制代码
构造函数中
  1. _leftOriginalTransformPosition = _leftIkConstraint.data.target.transform.localPosition; _rightOriginalTransformPosition = _rightIkConstraint.data.target.transform.localPosition; OriginalTargetRotation = _leftIkConstraint.data.target.rotation; // 初始的目标旋转(左右侧一样)
复制代码
公开属性
  1. public Vector3 CurrentOriginalTargetPosition { get; private set; } public Quaternion OriginalTargetRotation { get; private set; }
复制代码
SetCurrentSide()中赋值
  1. //靠近哪边就赋值哪边的Rig相关参数到CurrentXXX参数 if (isLeftCloser) { Debug.Log(&quot;目标更靠近角色的左侧&quot;); CurrentBodySide = EBodySide.LEFT; CurrentIkConstraint = _leftIkConstraint; CurrentMultiRotationConstraint = _leftMultiRotationConstraint; CurrentOriginalTargetPosition = _leftOriginalTargetPosition; } else { Debug.Log(&quot;目标更靠近角色的右侧&quot;); CurrentBodySide = EBodySide.RIGHT; CurrentIkConstraint = _rightIkConstraint; CurrentMultiRotationConstraint = _rightMultiRotationConstraint; CurrentOriginalTargetPosition = _rightOriginalTargetPosition; }
复制代码
ResetState中让ik目标控制器部件回到原来的position和rotation
  1. // 转向速度 float _rotationSpeed = 500f;
复制代码
UpdateState()
  1. // ik目标控制器部件也回到原来的position和rotation Context.CurrentIkTargetTransform.localPosition = Vector3.Lerp( Context.CurrentIkTargetTransform.localPosition, Context.CurrentOriginalTargetPosition, _elapsedTimer / _lerpDuration ); Context.CurrentIkTargetTransform.rotation = Quaternion.RotateTowards( Context.CurrentIkTargetTransform.rotation, Context.OriginalTargetRotation, _rotationSpeed * Time.deltaTime );
复制代码

5)RiseState


先更新ik目标控制器的y轴高度:
RiseState
  1. float _elapsedTimer = 0.0f; // 已消耗时间,用于控制插值进度 float _lerpDuration = 5.0f; // 插值总时长,决定状态过渡的“慢/快” float _riseWeight = 1.0f; // 权重目标值,用于IK和旋转约束的过渡
复制代码
  1. public override void UpdateState() { // 1. 碰撞点的y轴高度偏移 平滑更新到 最近碰撞点的Y坐标 Context.InteractionPoint_Y_Offset = Mathf.Lerp( Context.InteractionPoint_Y_Offset, Context.ClosestPointOnColliderFromShoulder.y, _elapsedTimer / _lerpDuration ); // 2. 更新IK约束CurrentIkConstraint的权重:从当前权重到目标权重_riseWeight Context.CurrentIkConstraint.weight = Mathf.Lerp( Context.CurrentIkConstraint.weight, _riseWeight, _elapsedTimer / _lerpDuration ); // 3. 更新多旋转约束CurrentMultiRotationConstraint的权重:从当前权重到目标权重_riseWeight Context.CurrentMultiRotationConstraint.weight = Mathf.Lerp( Context.CurrentMultiRotationConstraint.weight, _riseWeight, _elapsedTimer / _lerpDuration ); _elapsedTimer += Time.deltaTime; }
复制代码
再更新手掌的朝向:
RiseState
  1. Quaternion _targetHandRotation; // 手部的目标旋转角度,用于让手部贴合交互物体表面 float _maxDistance = 0.5f; // 射线检测的最大距离 protected LayerMask _interactableLayerMask = LayerMask.GetMask(&quot;Interactable&quot;); float _rotationSpeed = 1000f; // 旋转速度
复制代码
  1. ///  /// 计算期望的手部旋转角度,用于让手部贴合交互物体表面 ///  private void CalculateExpectedHandRotation() { // 1. 获取起始点(肩部位置)和终点(最近碰撞点) Vector3 startPos = Context.CurrentShoulderTransform.position; Vector3 endPos = Context.ClosestPointOnColliderFromShoulder; // 2. 射线方向:肩部指向碰撞点的归一化方向向量 Vector3 direction = (endPos - startPos).normalized; // 3. 发射射线 if (Physics.Raycast(startPos, direction, out RaycastHit hit, _maxDistance, _interactableLayerMask)) { // 碰撞点的表面法线 Vector3 surfaceNormal = hit.normal; // 目标朝向:与表面法线相反(让手部朝向碰撞点的表面法线的反方向) Vector3 targetForward = -surfaceNormal; // 手部的目标旋转方向:与目标朝向相同,但绕着Y轴旋转90度 _targetHandRotation = Quaternion.LookRotation(targetForward, Vector3.up); } }
复制代码
UpdateState()
  1. // 计算期望的手部旋转角度 CalculateExpectedHandRotation();
复制代码
  1. // 4. 让 IK目标控制器 朝着 预期的手部旋转角度 平滑旋转 Context.CurrentIkTargetTransform.rotation = Quaternion.RotateTowards( Context.CurrentIkTargetTransform.rotation, _targetHandRotation, _rotationSpeed * Time.deltaTime );
复制代码
6)RiseState 的状态切换 -> TouchState / ResetState (两种切换方式)

状态切换

如果继续接近碰撞点到一定距离阈值:RiseState -> TouchState
如果在RiseState状态持续时间超过一个阈值:RiseState-> ResetState
RiseState
  1. // 用于判断是否能够进入TouchState状态的阈值 float _touchDistanceThreshold = 0.05f; // TouchState的距离阈值 float _touchTimeThreshold = 1f; // TouchState的持续时间阈值
复制代码
  1. public override void EnterState() { // 重置计时器 _elapsedTimer = 0.0f; }
复制代码
  1. public override EnvironmentInteractionStateMachine.EEnvironmentInteractionState GetNextState() { // 标志位: 是否达到能够Touch的距离阈值 bool isCloseToTouch = Vector3.Distance( Context.CurrentIkTargetTransform.position, Context.ClosestPointOnColliderFromShoulder ) < _touchDistanceThreshold; // 标志位: 是否达到能够Touch的持续时间阈值 bool isTouchTimeOver = _elapsedTimer >= _touchTimeThreshold; if (isCloseToTouch && isTouchTimeOver) { // 切换到Touch状态 return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Touch; } return StateKey; }
复制代码
7)TouchState -> ResetState

切换条件只有时间阈值,超过就切换到ResetState
  1. using UnityEngine; public class TouchState : EnvironmentInteractionState { public float _elapsedTime = 0.0f; public float _resetThreshold = 0.5f; // 重置阈值:超过该时长就切换到 Reset 状态 public TouchState(EnvironmentInteractionContext context,EnvironmentInteractionStateMachine.EEnvironmentInteractionState estate): base(context, estate) { EnvironmentInteractionContext Context = context; } public override void EnterState() { // 重置计时器 _elapsedTime = 0.0f; } public override void ExitState() { } public override void UpdateState() { _elapsedTime += Time.deltaTime; } public override EnvironmentInteractionStateMachine.EEnvironmentInteractionState GetNextState() { if (_elapsedTime > _resetThreshold) { // 切换到 ResetState return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Reset; } return StateKey; } public override void OnTriggerEnter(Collider other) { StartIkTargetPositionTracking(other); } public override void OnTriggerStay(Collider other) { UpdateIkTargetPosition(other); } public override void OnTriggerExit(Collider other) { ResetIkTargetPositionTracking(other); } }
复制代码

Reset事件的几个触发机制


另加一个可能的情况:角色跳的时候也触发Reset(虽然现在没给角色加入跳跃)
EnvironmentInteractionState
  1. private float _movingAwayOffset = 0.005f; // 远离目标的偏移值 bool _shouldReset; // 标志位:是否能够进入ResetState
复制代码
  1. ///  /// 是否能够进入ResetState ///  /// 能够进入时返回 true,否则返回 false protected bool CheckShouldReset() { if (_shouldReset) { // 重置「最近距离」为无穷大 Context.LowestDistance = Mathf.Infinity; // 重置标志位 _shouldReset = false; return true; } // 标志位:是否停止移动 bool isPlayerStopped = CheckIsStopped(); // 标志位:是否正在远离目标交互点 bool isMovingAway = CheckIsMovingAway(); // 标志位:是否是非法角度 bool isInvalidAngle = CheckIsInvalidAngle(); // 标志位:是否正在跳跃 bool isPlayerJumping = CheckIsJumping(); if(isPlayerStopped || isMovingAway || isInvalidAngle || isPlayerJumping) { // 重置「最近距离」为无穷大 Context.LowestDistance = Mathf.Infinity; return true; } return false; }
复制代码
触发机制的检测函数
  1. ///  /// Reset事件的触发机制1: ———— 玩家是否停止移动 ///  ///  protected bool CheckIsStopped() { bool isPlayerStopped = GameInputManager.MainInstance.Movement == Vector2.zero; return isPlayerStopped; } ///  /// Reset事件的触发机制2: ———— 玩家是否正在远离目标交互点 ///  /// 玩家远离目标时返回 true,否则返回 false protected bool CheckIsMovingAway() { // 1. 角色根节点到目标碰撞点的当前距离 float currentDistanceToTarget = Vector3.Distance( Context.RootTransform.position, Context.ClosestPointOnColliderFromShoulder ); // 标志位:是否正在搜索新的交互点 bool isSearchingForNewInteraction = Context.CurrentIntersectingCollider == null; if (isSearchingForNewInteraction) { return false; } // 标志位:是否在靠近目标 bool isGettingCloserToTarget = currentDistanceToTarget <= Context.LowestDistance; if (isGettingCloserToTarget) { // 更新最近距离 Context.LowestDistance = currentDistanceToTarget; // 未远离 return false; } // 标志位:是否已远离目标(当前距离超过「最近距离 + 偏移值」) bool isMovingAwayFromTarget = currentDistanceToTarget > Context.LowestDistance + _movingAwayOffset; if (isMovingAwayFromTarget) { // 标记为远离,重置「最近距离」(下次重新开始计算) Context.LowestDistance = Mathf.Infinity; // 远离 return true; } return false; } ///  /// Reset事件的触发机制3: ———— 当前交互的角度是否为“非法角度” ///  /// 如果是非法角度返回 true,否则返回 false protected bool CheckIsInvalidAngle() { // 如果当前交互的碰撞体为空,直接判定不是不良角度 if (Context.CurrentIntersectingCollider == null) { return false; } // 计算从肩部指向碰撞点的方向向量 Vector3 targetDirection = Context.ClosestPointOnColliderFromShoulder - Context.CurrentShoulderTransform.position; // 根据身体侧别(左/右)确定肩部的参考方向 Vector3 shoulderDirection = (Context.CurrentBodySide == EnvironmentInteractionContext.EBodySide.RIGHT) ? Context.RootTransform.right : -Context.RootTransform.right; // 计算肩部参考方向与目标方向的点积(用于判断夹角方向) float dotProduct = Vector3.Dot(shoulderDirection, targetDirection.normalized); // 非法角度 = 点积小于 0 (目标方向与肩部参考方向夹角大于 90 度) bool isInvalidAngle = dotProduct < 0; return isInvalidAngle; } ///  /// Reset事件的触发机制4: ———— 玩家是否正在跳跃 ///  ///  protected bool CheckIsJumping() { bool isPlayerJumping = Mathf.Round(Context.CharacterController.velocity.y) >= 1; return isPlayerJumping; }
复制代码
在每个状态的状态切换函数GetNextState()中加入 切换到ResetState的触发条件
SearchState
  1. if (CheckShouldReset()) { // 切换到Reset状态 return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Reset; }
复制代码
ApproachState
  1. if (isOverStateLifeTime || CheckShouldReset()) { // 切换到Reset状态 return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Reset; }
复制代码
RiseState
  1. if (CheckShouldReset()) { // 切换到Reset状态 return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Reset; }
复制代码
TouchState
  1. if (_elapsedTime > _resetThreshold || CheckShouldReset()) { // 切换到 ResetState return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Reset; }
复制代码
找到了之前从ResetState切换到SearchState一直响应慢的问题根源:
动画根运动驱动,需要用输入来判断是否在移动
ResetState的GetNextState()函数
  1. // 标志位:是否正在移动(是否有Movement输入) bool isMoving = GameInputManager.MainInstance.Movement != Vector2.zero;
复制代码
最终效果如下:



我的评价是很丝滑很自然,这是我做过细节最多最复杂的动作拆解系统

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

相关推荐

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