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

写在前面:

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

------致敬顽皮狗工作室

插件安装

为角色添加组件

右手同理

状态机脚本编写

BaseState.cs

csharp 复制代码
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

csharp 复制代码
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

csharp 复制代码
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用来管理各种属性

csharp 复制代码
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开始

csharp 复制代码
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中加入初始化函数

csharp 复制代码
    void Awake()
    {
	//原来的代码

        InitalizeStates();
    }
csharp 复制代码
    /// <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

csharp 复制代码
    void Awake()
    {
        ///原来的代码

        ConstructEnvironmentDetectionCollider();
    }
csharp 复制代码
    /// <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加入:判断碰撞相交位置更靠近哪一侧

csharp 复制代码
    // 身体两侧
    public enum EBodySide
    {
        RIGHT,
        LEFT
    }
csharp 复制代码
    // 当前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

csharp 复制代码
    /// <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

csharp 复制代码
    // 根对象
    private Transform _rootTransform;

构造函数要加入这个变量

csharp 复制代码
    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;
    }
csharp 复制代码
    public Transform RootTransform => _rootTransform;

当然,在EnvironmentInteractionStateMachine中也要传入这个变量

Awake()

csharp 复制代码
        _context = new EnvironmentInteractionContext(_leftIkConstraint, _rightIkConstraint, _leftMultiRotationConstraint, _rightMultiRotationConstraint, characterController,transform.root);

写一下ResetState的GetNextState()的下一状态切换逻辑

csharp 复制代码
    public override EnvironmentInteractionStateMachine.EEnvironmentInteractionState GetNextState() 
    { 
        // 下一个状态为 SearchState
        return EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Search;
        //return StateKey; 
    }

注意:

SearchStateOnTriggerEnter() 中调用**StartIkTargetPositionTracking()**启动 IK 目标位置追踪

csharp 复制代码
    public override void OnTriggerEnter(Collider other) {
        // 进入搜索状态时,开始跟踪目标位置
        StartIkTargetPositionTracking(other);
    }

测试一下功能是否正常:

效果倒是正常,不过这是我调试好久发现的问题,只有挂载rigidbody的物体才会触发Trigger回调函数,正常来说只要一方有rigidbody就能触发,不知道为什么这里会出现这个问题,角色身上的这个触发器肯定是rigidbody,那已经满足条件了,为什么还要其他物体也要挂载rigidbody,想不明白。。。

不过实现了就好,后面再排查问题吧,先完成最要紧

4.解决一下在狭窄通道走过的时候,左右频繁触发的问题

EnvironmentInteractionContext

csharp 复制代码
    // 当前交互的碰撞体
    public Collider CurrentIntersectingCollider { get; set; }

EnvironmentInteractionState

csharp 复制代码
    /// <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);
        }
    }
csharp 复制代码
    /// <summary>
    /// 重置 IK 目标位置追踪
    /// </summary>
    /// <param name="intersectingCollider">相交的碰撞体,针对其执行追踪重置</param>
    protected void ResetIkTargetPositionTracking(Collider intersectingCollider)
    {
        if(intersectingCollider == Context.CurrentIntersectingCollider)
        {
            Context.CurrentIntersectingCollider = null;
        }
    }

SearchState

csharp 复制代码
    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

csharp 复制代码
    // 相交碰撞体的最近点------默认值设为无穷大
    public Vector3 ClosestPointOnColliderFromShoulder { get; set; } = Vector3.positiveInfinity;

EnvironmentInteractionState

csharp 复制代码
    /// <summary>
    /// 设置 IK 目标位置
    /// </summary>
    /// <param name="targetPosition"></param>
    private void SetIkTargetPosition()
    {
        // 最近的碰撞点
        Context.ClosestPointOnColliderFromShoulder = GetClosestPointOnCollider(Context.CurrentIntersectingCollider, Context.CurrentShoulderTransform.position);
    }
csharp 复制代码
    /// <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();
        }
    }
csharp 复制代码
    /// <summary>
    /// 更新 IK 目标位置
    /// </summary>
    /// <param name="intersectingCollider">相交的碰撞体,依据其状态更新目标位置</param>
    protected void UpdateIkTargetPosition(Collider intersectingCollider)
    {
        // 在接触过程中,一直更新IK目标位置
        if (Context.CurrentIntersectingCollider == intersectingCollider)
        {
            SetIkTargetPosition();
        }
    }

SearchState

csharp 复制代码
    public override void OnTriggerStay(Collider other) {
        // 跟踪目标位置
        UpdateIkTargetPosition(other);
    }

然后在EnvironmentInteractionStateMachine中加入可视化

csharp 复制代码
    /// <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的构造函数加入一个角色的肩部高度变量

csharp 复制代码
    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;
    }
csharp 复制代码
    // 角色的肩部高度,用来约束Ik的高度
    public float CharacterShoulderHeight { get; private set; }

EnvironmentInteractionState 传入目标位置的参数的y轴改成角色肩高CharacterShoulderHeight

csharp 复制代码
    /// <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

csharp 复制代码
    /// <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

csharp 复制代码
    /// <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的权重

相关推荐
岁忧8 小时前
(nice!!!)(LeetCode 每日一题) 679. 24 点游戏 (深度优先搜索)
java·c++·leetcode·游戏·go·深度优先
妮妮学代码11 小时前
c#:TCP服务端管理类
java·tcp/ip·c#
SchuylerEX13 小时前
第六章 JavaScript 互操(2).NET调用JS
前端·c#·.net·blazor·ui框架
★YUI★13 小时前
学习游戏制作记录(制作系统与物品掉落系统)8.16
学习·游戏·ui·unity·c#
lingzhilab20 小时前
零知开源——基于STM32F103RBT6与ADXL362三轴加速度计的体感迷宫游戏设计与实现
游戏
mit6.8241 天前
Linux下C#项目构建
开发语言·c#
代码改变世界100861 天前
像素风球球大作战 HTML 游戏
前端·游戏·html
Nita.1 天前
.NET 中的延迟初始化:Lazy<T> 与LazyInitializer
c#·.net
好望角雾眠1 天前
第一阶段C#基础-10:集合(Arraylist,list,Dictionary等)
笔记·学习·c#