当前位置: 首页 > news >正文

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

写在前面:

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

——致敬顽皮狗工作室

插件安装

1755773270538

为角色添加组件

1755773337095

1755773371949

1755773411325

1755773431970

右手同理

状态机脚本编写

1755775744433

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的同级

1755784900189

1755784962449

1755785011835

Rig放置的位置

1755784884750

1755785030047

环境交互状态机的编写

1755785081711

1755789073630

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));//设置初始状态为ResetCurrentState = States[EEnvironmentInteractionState.Reset];}

1755793245712

状态机运行正常

环境检测

1755793230388

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;}

1755800944612

1755800951964

2.碰撞体触发器的交互机制

  1. 角色进入 “触发器区域” → OnTriggerEnter 触发(一次)
  2. 角色持续待在区域内 → 每帧触发 OnTriggerStay
  3. 角色离开区域 → OnTriggerExit 触发(一次)

1755794086675

1755794176628

1755794192951

3.找到离角色更近的一侧,用来决定后面开启哪边的IK

EnvironmentInteractionContext加入:判断碰撞相交位置更靠近哪一侧

1755801127469

    // 身体两侧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() { // 下一个状态为 SearchStatereturn EnvironmentInteractionStateMachine.EEnvironmentInteractionState.Search;//return StateKey; }

注意:

1755802950451

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

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

测试一下功能是否正常:

1755803008162

1755807104145

效果倒是正常,不过这是我调试好久发现的问题,只有挂载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);}}

1755809630756

新的问题出现了:

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

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));}

问题解决

1755810520182

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;}}

效果:

1755810786266

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,效果是这样的:

1755812110107

当然,我们还得根据具体的状态写Ik权重的控制脚本

每个具体状态的Ik控制逻辑的脚本编写

也就是根据状态决定是否/怎样更新手部Two Bone IK Constraint的权重

http://www.sczhlp.com/news/26656/

相关文章:

  • 龙华营销型网站建设公司手机登录百度pc端入口
  • 做网站哪里的好合肥网站优化平台
  • 网站设计与制作优点网站建设的流程是什么
  • 导航网站 wordpress内蒙古最新消息
  • bootstrap网页模板关键词优化推广排名多少钱
  • 可以做网站的语言营销方案100例
  • 网站商城维护怎么做临沂seo公司稳健火星
  • 做网站为什么要做备案接入外贸营销网站怎么建站
  • 网站风格苏州关键词排名系统
  • 湖南养老院中企动力网站建设百度网址大全官方下载
  • 连云建网站公司制作网站
  • 简单的个人网页代码佛山旺道seo优化
  • 做临时网站北京软件培训机构前十名
  • 公司网页制作网站网络促销策略
  • 企业网站seo价格网站专业术语中seo意思是
  • b2b模式平台seo网站课程
  • 安卓手机app制作公司企业seo网络营销
  • 做论坛网站的元素荆州网站seo
  • 建立的英语seo关键词优化排名
  • wordpress海报生成器seo网站推广培训
  • 做淘客必须有自己内部网站吗抖音代运营收费详细价格
  • 深圳做网站比较好考研培训班集训营
  • 商城网站备案爱站工具seo综合查询
  • 2018网站建设涉及今日热搜新闻头条
  • 做网站搜索推广点击率太低怎么办黑龙江最新疫情通报
  • co域名网站新冠咳嗽一般要咳多少天
  • 网站的可视化设计宁波网站推广方案
  • 个人与企业签订网站开发合同百度app官方下载
  • 用c语言怎么做网站搜关键词网站
  • 广告设计专业学什么福州百度快速优化排名