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

插件安装

为角色添加组件

右手同理

状态机脚本编写

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的目标权重

相关推荐
西岸行者4 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意4 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码4 天前
嵌入式学习路线
学习
科技前瞻观察4 天前
腾讯控股下的销售易,如何重塑中国CRM格局?
microsoft
毛小茛4 天前
计算机系统概论——校验码
学习
babe小鑫4 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms4 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下4 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。4 天前
2026.2.25监控学习
学习
im_AMBER4 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode