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

写在前面:

真正实现这些细枝末节的东西的时候才能感受到这种技术力的恐怖。

——致敬顽皮狗工作室

插件安装

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

为角色添加组件

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

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

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

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

右手同理

状态机脚本编写

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

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.碰撞体触发器的交互机制

  1. 角色进入 “触发器区域” → OnTriggerEnter 触发(一次)
  2. 角色持续待在区域内 → 每帧触发 OnTriggerStay
  3. 角色离开区域 → 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;      } 

注意:

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

SearchStateOnTriggerEnter()中调用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的权重

1.对现有代码进行一些小改动,更符合真实世界的运作机制

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

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

1)先解决 ResetState -> SearchState

    // 持续时间计时器     float _elapsedTimer = 0.0f;     // 持续时间的阈值     float _resetDuration = 2.0f; 
    public override void EnterState(){         // 重置 持续时间计时器         _elapsedTimer = 0.0f;         // 重置 最近碰撞点 和 当前碰撞体         Context.ClosestPointOnColliderFromShoulder = Vector3.positiveInfinity;         Context.CurrentIntersectingCollider = null;         Debug.Log("ResetState EnterState");     } 
    public override void UpdateState() {         _elapsedTimer += Time.deltaTime;     } 
    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 的状态跳转

    // 接近碰撞点的距离阈值     public float _approachDistanceThreshold = 2.0f; 
    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

    // 接近状态的计时器     float _elapsedTimer = 0.0f;     // 过渡时间     float _lerpduration = 5.0f;     // 接近状态的目标权重     float _approachWeight = 0.5f; 
    public override void EnterState() {         Debug.Log("ApproachState OnTriggerEnter");         // 重置计时器         _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);     } 
    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

    // 交互点的Y轴偏移量,用来细调每个具体状态的交互点的高度     public float InteractionPoint_Y_Offset { get; set; } = 0.0f;     // 角色碰撞体的中心点的高度     public float CharacterColliderCenterY { get; set; } 

EnvironmentInteractionStateMachine 的 ConstructEnvironmentDetectionCollider()

        _context.CharacterColliderCenterY = characterController.center.y; 

ResetState

    // 平滑过渡的持续时间     float _lerpDuration = 10.0f; 
    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轴偏移

        // 5. 更新 IK 目标位置         Context.CurrentIkTargetTransform.position =              new Vector3(                 targettPosition.x,                 Context.InteractionPoint_Y_Offset,                 targettPosition.z); 
ApproachState状态需要手腕旋转到让手掌朝向地面,也就是Multi-Rotation Constraint组件需要权重过渡到一个目标值

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

    // 接近状态的IkConstraint目标权重     float _approachWeight = 0.5f;     // 接近状态的MultiRotationConstraint目标旋转权重     float _approachRotationWeight = 0.75f;     // 旋转速度     float _rotationSpeed = 500f; 
    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

    // 接近状态持续时间,超过就回到ResetState状态     float _approachDuration = 2.0f; 
    // 是否能切换到上升状态的距离阈值     float _riseDistanceThreshold = 0.5f; 
    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()

        // 更新权重:平滑重置当前的权重         //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)

        // 默认设置当前身体的侧边为无穷大         SetCurrentSide(Vector3.positiveInfinity); 

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

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

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

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

EnvironmentInteractionContext中记录初始position和rotation信息

    // 记录初始位置     private Vector3 _leftOriginalTransformPosition;     private Vector3 _rightOriginalTransformPosition; 

构造函数中

        _leftOriginalTransformPosition = _leftIkConstraint.data.target.transform.localPosition;         _rightOriginalTransformPosition = _rightIkConstraint.data.target.transform.localPosition;         OriginalTargetRotation = _leftIkConstraint.data.target.rotation;            // 初始的目标旋转(左右侧一样) 

公开属性

    public Vector3 CurrentOriginalTargetPosition { get; private set; }     public Quaternion OriginalTargetRotation { get; private set; } 

SetCurrentSide()中赋值

        //靠近哪边就赋值哪边的Rig相关参数到CurrentXXX参数         if (isLeftCloser)         {             Debug.Log("目标更靠近角色的左侧");             CurrentBodySide = EBodySide.LEFT;             CurrentIkConstraint = _leftIkConstraint;             CurrentMultiRotationConstraint = _leftMultiRotationConstraint;             CurrentOriginalTargetPosition = _leftOriginalTargetPosition;         }         else         {             Debug.Log("目标更靠近角色的右侧");             CurrentBodySide = EBodySide.RIGHT;             CurrentIkConstraint = _rightIkConstraint;             CurrentMultiRotationConstraint = _rightMultiRotationConstraint;             CurrentOriginalTargetPosition = _rightOriginalTargetPosition;         } 

ResetState中让ik目标控制器部件回到原来的position和rotation

    // 转向速度     float _rotationSpeed = 500f; 

UpdateState()

        // 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

    float _elapsedTimer = 0.0f;         // 已消耗时间,用于控制插值进度     float _lerpDuration = 5.0f;         // 插值总时长,决定状态过渡的“慢/快”     float _riseWeight = 1.0f;           // 权重目标值,用于IK和旋转约束的过渡  
    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

    Quaternion _targetHandRotation;   // 手部的目标旋转角度,用于让手部贴合交互物体表面     float _maxDistance = 0.5f;         // 射线检测的最大距离     protected LayerMask _interactableLayerMask = LayerMask.GetMask("Interactable");     float _rotationSpeed = 1000f;      // 旋转速度 
    /// <summary>     /// 计算期望的手部旋转角度,用于让手部贴合交互物体表面     /// </summary>     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()

        // 计算期望的手部旋转角度         CalculateExpectedHandRotation(); 
        // 4. 让 IK目标控制器 朝着 预期的手部旋转角度 平滑旋转         Context.CurrentIkTargetTransform.rotation = Quaternion.RotateTowards(             Context.CurrentIkTargetTransform.rotation,             _targetHandRotation,             _rotationSpeed * Time.deltaTime         ); 

6)RiseState 的状态切换 -> TouchState / ResetState (两种切换方式)

状态切换

如果继续接近碰撞点到一定距离阈值:RiseState -> TouchState

如果在RiseState状态持续时间超过一个阈值:RiseState-> ResetState

RiseState

    // 用于判断是否能够进入TouchState状态的阈值     float _touchDistanceThreshold = 0.05f;  // TouchState的距离阈值     float _touchTimeThreshold = 1f;         // TouchState的持续时间阈值 
    public override void EnterState()     {         // 重置计时器         _elapsedTimer = 0.0f;     } 
    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

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

    private float _movingAwayOffset = 0.005f;       // 远离目标的偏移值      bool _shouldReset;      // 标志位:是否能够进入ResetState 
    /// <summary>     /// 是否能够进入ResetState     /// </summary>     /// <returns>能够进入时返回 true,否则返回 false</returns>     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;     }  

触发机制的检测函数

    /// <summary>     /// Reset事件的触发机制1: ———— 玩家是否停止移动     /// </summary>     /// <returns></returns>     protected bool CheckIsStopped()     {         bool isPlayerStopped = GameInputManager.MainInstance.Movement == Vector2.zero;         return isPlayerStopped;     }     /// <summary>     /// Reset事件的触发机制2: ———— 玩家是否正在远离目标交互点     /// </summary>     /// <returns>玩家远离目标时返回 true,否则返回 false</returns>     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;     }     /// <summary>     /// Reset事件的触发机制3: ———— 当前交互的角度是否为“非法角度”     /// </summary>     /// <returns>如果是非法角度返回 true,否则返回 false</returns>     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;     }     /// <summary>     /// Reset事件的触发机制4: ———— 玩家是否正在跳跃     /// </summary>     /// <returns></returns>     protected bool CheckIsJumping()     {         bool isPlayerJumping = Mathf.Round(Context.CharacterController.velocity.y) >= 1;         return isPlayerJumping;     } 

在每个状态的状态切换函数GetNextState()中加入 切换到ResetState的触发条件

SearchState

        if (CheckShouldReset())         {             // 切换到Reset状态             return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Reset;         } 

ApproachState

        if (isOverStateLifeTime || CheckShouldReset())         {             // 切换到Reset状态             return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Reset;         } 

RiseState

        if (CheckShouldReset())         {             // 切换到Reset状态             return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Reset;         } 

TouchState

        if (_elapsedTime > _resetThreshold || CheckShouldReset())         {             // 切换到 ResetState             return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Reset;         } 

找到了之前从ResetState切换到SearchState一直响应慢的问题根源:

动画根运动驱动,需要用输入来判断是否在移动

ResetState的GetNextState()函数

        // 标志位:是否正在移动(是否有Movement输入)         bool isMoving = GameInputManager.MainInstance.Movement != Vector2.zero; 

最终效果如下:

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

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

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

发表评论

评论已关闭。

相关文章