前言
自述:我本来想写文档,或者分析每个类和函数,但我觉得没有必要,我只需要知道核心逻辑就够了,这样即使换引擎,搭建更简单的版本,又或者是更换效果都是不变的,虽然技术在快速发展,但我很认同引擎的架构和设计思路是几乎不变的,我相信这是一个很好的角色控制器架构,因此我尝试用自己的语言概括的描述整个框架
看到评论区有人提到:IoC实际上就是一个每帧主动从一个中心的消息bus中拉去自己感兴趣的消息 进行处理 这里太过死板 首先消息永远是固定频率拉去 很多情况下没必要 而且这里的中心消息bus(你所谓的黑板)并没有主动路由分发消息的功能 只是一个被动的暂存而已 本质上的解耦实际上就是避免直接调用 最彻底的 就是一切通过消息 我希望别人做什么是给别人发消息 别人自己处理 别人希望我做什么也只能通过给我发消息 至于怎么理解和处理消息 是我的逻辑负责 实际上你需要的是Actor模式
那其实这个项目实现IOC(Inverse of control)控制反转的方式是DI(dependency Injection)依赖注入,指容器通过构造函数、属性、方法等方式,将对象依赖的实例 “注入” 到业务类中,业务类无需自己创建依赖,通过标准抽象接口实现。而一种更好的方式是Actor架构,是将来可以考虑的改进方向,但是思想是相同的,都是模块解耦
概述
PlayerInputReader将InputAction中已经设置好的Input Action二次绑定(所以只要把这个界面做个映射就是键位绑定了)读取二次绑定变量从InputAction中接收的键位数据,将不经过任何处理的数据写进(InputData中 – 脚本名)的结构体Rawdata,Rawdata中和同一种Action也包含两种数据:

rawData.Number2Held = number2Action != null && number2Action.action.IsPressed();
rawData.Number2JustPressed = number2Action != null && number2Action.action.WasPressedThisFrame();
其次ToggleActions函数会遍历所有的绑定,如果面板上有为空即不需要键位映射的Action会在OnEnable时自动被禁用
BBBCharacterController的update中
private void Update()
{
if (!_booted) return;
_lastState = StateMachine.CurrentState as PlayerBaseState;
ArbiterPipeline.ProcessUpdateArbiters();
InputPipeline.Update();
MainProcessorPipeline.UpdateIntentProcessors();
InventoryController.Update();
MainProcessorPipeline.UpdateParameterProcessors();
StateMachine.CurrentState.LogicUpdate();
UpperBodyCtrl.Update();
FacialController.Update();
ActionController.Update();
AudioController.Update();
}
}
}
控制器的单帧工作流(Sog

每帧调用InputPipline的update函数:
1.将该帧的数据赋值给上一帧的Data_inputData.lastFrameData = _inputData.currentFrameData;
缓存历史数据在栈上(读取快,而且只有上一帧的数据,数据量不大,并且频繁销毁,所以声明为栈)是为了计算动作缓存倒计时用,下一帧更新计时器的时候需要知道上一帧计算到什么时候了
public struct FrameInputData
{
public ulong FrameIndex;
public RawInputData Raw;
public ProcessedInputData Processed;
}
public class InputData
{
public FrameInputData currentFrameData;
public FrameInputData lastFrameData;
}
2.其负责触发抽象函数FetchRawInput,这正是实现AI移动的一个关键部分,即包装rawdata的获取函数,该抽象函数必定能从实现IInputSource接口(其只定义了FetchRawInput函数)的对象实现,其定义了所有能作为rawdata来源的对象,如PlayerInputReader中和目前还没用到的AICombatInputAdapter,我觉得只要把从键位绑定读取改为程序生成就能实现AI了
而IIputSource上层还定义了一个抽象类InputSourceBase,首先当然是继续声明抽象函数FetchRawInput继续由具体实现类重写,除此之外定义了两个在下所述防抖处理(移动轴的防抖缓存时间(秒),用于抖动抑制)和指令缓存时间的设定值,float类型变量InputFlickerBuffer和ActionBufferTime。PlayerInputReader派生自InputSourceBase进而实现接口
3.调用本类的ProcessRawInput函数将rawData转换为ProcessData,其包括对_rawData中MoveAxis的防抖处理,对于持续按压触发的rawdata如XXHeld直接赋值给ProcessData
currentFrame.Processed.JumpHeld = _rawData.JumpHeld;
但对于当前帧是否按下的XXPressed,填加了动作指令缓存时间(按下后该按键在此时间内被视为已按下):
一旦硬件触发 JustPressed 给对应的Timer充能,值为上所说的固定值。定义UpdateBuffer函数(这里我其实有个疑问,为什么要在update的函数内定义函数?),根据缓存的上一帧的剩余时间,在每一帧固定减去dt= Time.deltaTime;的时间,Inputdata(只是脚本名)中读取的 bool Pressed 是依赖此 Timer 的计算属性public bool OPressed => OBufferTimer > 0f;,只要倒计时没有归0,那就一直是大于0的值,即外界会读取到true(大于1都视为true),以此实现动作指令缓存时间,但是为什么要加这个机制呢
float dt = Time.deltaTime;
var lastProc = _inputData.lastFrameData.Processed;
float UpdateBuffer(float lastTimer, bool justPressed)
{
float newTimer = Mathf.Max(0f, lastTimer - dt);
if (justPressed) newTimer = _actionBufferTime;
return newTimer;
}
currentFrame.Processed.JumpBufferTimer = UpdateBuffer(lastProc.JumpBufferTimer, _rawData.JumpJustPressed);
——————————————————————————————————————————————
上面为数据模块,说实在确实是我见过最复杂的,为了实现AI,实现了输入来源接口,为了区分原始数据和处理数据,每个由分为持续触发和立即触发,增加了代码复杂度,同时也调用了Animacer的API实现了瞬时数据和持续数据,对于瞬时数据加了抗扰动和缓存时间的机制,下面介绍如何将所有处理好在InputData(只是脚本名)的数据转化为意图,为什么要加这一层?
Input Pipeline中的ConsumeJumpPressed函数将对应的Timer 瞬间归零,在输入转换为意图后调用后由XXIntentProcessor中调用,如果识别到Inputdata中XXpressed的处理数据为真,其对应的XXIntentProcessor会将RunTImeData中的WantsToPlayXX设置为真,同时调用ConsumeJumpPressed函数
public void Update(in ProcessedInputData input)
{
if (input.OPressed)
{
_data.WantsToPlayOAnim = true;
_inputPipeline.ConsumeOPressed();
}
}
该Update函数被集成在MainProcessorPipeline中,由BBBCharacterController调用
Official document:Animancer – Home
作者描述
对于一个优秀的控制器而言,需要支持增加各种新状态与交互模式的快速迭代,这就需要规定各个系统的指责边界来保持代码规范
Unity自带的画控制器Mecanim动存在许多设计缺陷:
- 没有对时序的控制权
- 无法重用
- 当状态和切换逻辑的数量很多时,不容易修改
- 多人在git同时修改连线,git合并会有问题
这些问题都可以解决
使用动画根运动控制角色移动还是使用代码驱动
PS:其实如果是大公司能够自产游戏动作资源的情况下,我觉得他们很可能会采用基于根运动移动的框架
- 由根运动驱动
- 本质是让美术的动画文件接管了物理位置,如果要改位移数值,需要美术重做动画
- 联机需求下,基于动画进度的位移会让网络同步变成一场灾难
- 他的运动跳过物理引擎,修复他又需要很多代码
- 代码控制
- 拥有完整的时序控制权
但是对于自带旋转等一些特殊动画,很难用代码对齐物理旋转和动画的表现
- 可以选择对这类特殊动画可以读取动画本身的速度和位移
// 读取动画根运动速度与位移
Vector3 rootVelocity = animator.deltaPosition / Time.deltaTime;
Vector3 rootPosition = animator.rootPosition;
// 应用到角色控制器
characterController.Move(rootVelocity * Time.deltaTime);
新问题出现了,游戏运行时,实时读取这些庞大的动画数据,会让控制器依赖底层的动画管线
- 因此定义一个动画结构体类型

然后通过运动数据提取器(Tools – BBBNexus – RootMotionBaker)离线烘焙动画位移数据,原理是在虚拟环境预先播放动画,将每一帧的位移离线烘焙成轻量级的数据文件

这样做还有另外的好处
- 可以直观的观察并调节位移数据
- 如果NPC的动画被降级停更运动,系统仍然可以烘焙数据正常工作(没懂)
Motion Driver类 – 为了统一管理驱动位移的逻辑
/// <summary>
/// 角色运动的核心驱动器 负责将输入、动画曲线、物理参数
/// 转换为实际的 CharacterController.Move() 调用 驱动角色在场景中的实际位移
/// </summary>
/// 3 个引用
public class MotionDriver
原始动画->离线数据提取器->烘焙位移数据->Motion Driver类

正是依赖这种数据提纯架构,才能实现如《最后生还者》等游戏中,通过**纯代码驱动的真实匹配跟线**完全接近运动帧频,最终达到工业级 3A 项目的苛刻标准
运行黑板RuntimeData与数据处理管线
- 如果输入系统个状态机直接关联,比如下面的代码,不仅仅是两者耦合的问题,还可能会导致时序问题(也可通过Actor架构的消息中枢实现)
- 时序问题 :状态切换发生在输入处理过程中,如果没有“先统一采样、再统一翻译、再统一切状态”的流程,就会出现:有的系统看到旧状态,有的系统看到新状态,有的系统看到旧输入,有的系统看到新输入
- 同一帧里谁先读输入?假设没有
InputPipeline这种统一快照,因为一帧里不是“所有脚本同时执行”,而是按顺序一个个执行。前面的系统看到的是旧值,后面的系统可能已经看到新值了。同一个按键事件,不同地方读到的时间点可能不一样。比如某一帧:- A 脚本先读到了
WasPressedThisFrame() == true - B 脚本晚一点读的时候,逻辑条件已经变了
- C 脚本在状态切换后再读,又是另一套结果
- A 脚本先读到了
- 因此新增一种数据类型用于表示角色当前帧的真实行为意图,定义RunTimeData类承载各类运行时数据
/// <summary>本帧是否想跑</summary>
public bool WantToRun;
/// <summary>本帧是否想闪避</summary>
public bool WantsToDodge;
/// <summary>本帧是否想翻滚</summary>
public bool WantsToRoll;
- 接着编写一批处理类XXIntentProcessor,按固定顺序将输入数据解析为行为意图,并将结果写入Runtime Data类

- 由于XX processor逐渐增多,设计main processor pipeline类统一调度和管理所有处理器
// 核心后处理管线
// 将后处理的输入数据转为角色意图
// 从平级的InputPipeline提取栈上后处理输入数据快照指针 -> 翻译为意图 -> 写入堆内黑板
// 3 个引用
public class MainProcessorPipeline

- 在这个例子中RunTime Data就是这块黑板
上半身层的设计
在上半身开了一个子状态机
// 上半身分层控制器
// 管理上半身的独立状态机 中央点是处理装备 瞄准 与攻击等上半身行为
// 使用遮罩确保只影响特定骨骼 与主状态机并行运行 互不干扰
public class UpperBodyController
{
上半身与物体交互有几种实现方式
- 上半身状态机识别物体类型来决定进入哪个逻辑分支
- 但这样做上半身状态机和物品之间就耦合了,导致状态机中会加入一大堆的switch
- 状态机不需要知道物品是什么(把物品当黑箱,知道能实现,但不知道也不负责具体如何实现)而是通过物品实现接口,状态机只负责判断持有 / 切换状态,在物品中实现逻辑与表现动画,这种上下解耦的思想就是依赖反转
- 架构设计原则,核心为依赖倒置。将对象依赖的创建与生命周期管控,从调用方转移至抽象容器, 调用方仅依赖标准抽象接口, 彻底解除与具体实现类的强耦合。
- 其中标准抽象接口,根据物体的类别可以分别定义枪械实现,近战实现,抛掷物实现,道具实现

表现层系统和仲裁管线
物品栏系统会影响角色的上半身状态机,也是每帧需要更新的模块,该模块和上半身状态机、状态机、MotionPlayer一样,也每帧读取RunTimeData,判断需不需要触发装备相关的操作
表现层包括音频、表情、Ik系统,都可以通过每帧响应黑板上的事件或读取其数据响应主要逻辑层和玩家的输入,因此都各自声明Action Audio Facial IK PlayerInventory UpperBody Controller来管理(存放在Expression文件夹中),状态机只负责移动跳跃攻击的核心逻辑,通过RunTimeData实现逻辑层和表现层的分离,这样做的好处首先是解耦和,其次可以避免表现层干扰核心逻辑,允许并行开发


这些模块因为RunTImeData的存在因此可以和状态机解耦,但是有些模块也需要获取状态信息,比如你不希望死亡后依然能做动作等等…对于模块对状态获取有需求的情况,在RunTImeData声明一组Bool变量用来决定是否要启用对应的控制器,称为仲裁标记,可以视为另一组意图数据,所以类似的,声明另外的仲裁管线ArbiterPipline,输入端是状态机的状态而非输入数据,这样就能动态的根据状态阻断或恢复某个表现层模块
public struct ArbitrationFlags
{
public bool BlockInput;
public bool BlockUpperBody;
public bool BlockFacial;
public bool BlockIK;
public bool BlockInventory;
public bool BlockAudio;
}

输入源的抽象


如果要让AI控制角色,只需要新建一个输入源,实现接口,然后创建一个行为树生成驱动数据的逻辑

所有和外部系统通信的关系都可以改为依赖倒置的架构, 3段式的设计:消费层,协议层,实现层
- 协议层:先定义接口和桥接抽象基类
- 再写不同场景的具体实现层
- 消费层:只写逻辑,而不关心具体如何实现

首先我有几个疑问,也许会伴随我学习的过程解答
- 能否单独关闭某些模块,比如装备切换
- 他怎么想出来这个架构的,AI还是视频?
- 框架作者的评论:playable播放一个动画要创建管理graph不说 实现一个平滑过度还得手搓一个update或者协程用数学去算两个端口的weight 人家animancer直接帮我封装好了方法 文档也比官方齐 更别说安全的生命周期托管和调试上的便利了 总之它对得起90美金的价格
- Idle目前我只看到有一个选项,怎么改为循环Idal动画,因为我有很多个Idle动画
- walk动画的三种模式是什么意思
- 3个IK组件各个参数的含义,其中一个导致我每次上平台脚都会下去,debug很久,之前见到有人说debug就是不断地修改每个参数,找到能解决问题的那个,这次我体会到了

我似乎没有烘焙,这样动作会生效吗
架构分析
第 1 层:输入层
由 InputSourceBase / IInputSource 提供输入,再交给 InputPipeline 做采样、缓存、防抖和清洗。InputPipeline 在源码里的定位非常明确:它是第一道关卡、唯一的数据生产者,负责把输入源的原始输入清洗后写进 InputData
InputSource:采集手柄/键鼠/AI输入 InputPipeline:把“生的输入”处理成“本帧可用输入快照
第 2 层:意图与参数层
MainProcessorPipeline 就是这个翻译中心。它把输入快照交给一组 Intent Processor 和 Parameter Processor,例如:
LocomotionIntentProcessorAimIntentProcessorJumpOrVaultIntentProcessorActionIntentProcessorMovementParameterProcessorViewRotationProcessor
输入不是直接驱动动画,而是先变成“行为意图”和“动画参数”
为什么要加这一层?明明已经有输入的抽象层了
第 3 层:状态机 / 打断 / 仲裁层
分层状态体系,决定现在最终允许执行哪个动作?
FullBody:移动、跳跃、翻滚、死亡这类全身动作UpperBody:持枪、拿东西、空手这类上半身动作-
Override:优先级最高的强制动作 - 还预留了表情层扩展
还有两套“限制系统”
- 拦截器 / 打断器:判断某状态能不能进、谁能打断谁
- 仲裁器:当多个请求冲突时,最后谁赢
第 4 层:表现层
既然决定要执行了,那怎么播出来?
AnimationFacadeBase / AnimancerFacade:统一动画播放入口-
MotionDriver:处理动画和位移同步 -
EquipmentDriver:装备挂接、切换 AudioDriver:音效触发IKController / IKSource:瞄准、手脚贴合等
状态机不会自己直接去操作 Animator,而是把“我要播哪个动作”交给外观层。
这正是它可迁移、可替换的关键:以后你就算不用 Animancer,理论上换一个别的动画后端,只要 facade 接口不变,上层逻辑还能继续用
第 5 层:最终呈现层
最后一层是玩家真正看到的效果:
- 角色姿态变化
- 根运动/位移同步
- 上下半身分层混合
- 武器显示
- IK 修正
- 音效/表情等附加表现
所以 LateUpdate 阶段的关键词是:把已经决定好的东西,真正同步到位移、IK 和最终表现上。
配置资源(很清晰,不用删)
PlayerSO:总配置
这是角色的总装配表。源码里它是 CreateAssetMenu("BBBNexus/Player/PlayerConfig (Main)"),字段里挂着:
BrainCoreLocomotionAnimsJumpAndLandingAimingVaultingDodgingRollingActionAudioEmj
以后你给不同角色做迁移,最核心的就是替换这份总配置和其引用的模块资源。
PlayerBrainSO:状态脑配置
这是“动作逻辑开关表”,不是动画资源表。
它的创建菜单是 BBBNexus/Player/Modules/Player Brain,里面主要配:
AvailableStates:这个角色启用哪些全身状态,列表第 0 个是启动状态GlobalInterceptors:全局打断器,按顺序决定优先级UpperBodyStates:启用哪些上半身状态,列表首位是启动状态UpperBodyInterceptors:上半身专属打断器
你可以理解成:
PlayerBrainSO 决定“角色会什么动作、动作切换规则是什么”。
各模块 SO:动作参数与动画数据
从 PlayerSO 的字段可以看出,作者把不同能力拆成单独模块:
- Core:基础参数,含上半身/表情 mask 等
- Locomotion:待机、走跑等基础移动动画
- Jump:跳跃/落地
- Aiming:瞄准相关
- Vaulting / Dodging / Rolling / Action:特殊动作
- Audio / Emj:表现扩展
你现在不必把它想成“一堆散乱资源”,而是把它想成:
每个动作能力单独一包资源,最后统一挂回 PlayerSO。
从“输入”到“最终动画呈现”的完整流程
第一步:玩家输入
玩家按键、摇杆、鼠标视角等先被 InputSourceBase 读出来。
如果你以后换成 AI,也只是换输入源,不需要重写整个动作系统。
第二步:输入管线清洗
InputPipeline 把原始输入做缓冲、防抖、合法化,产出当前帧的 InputData 快照。
第三步:翻译成意图
MainProcessorPipeline 把输入快照交给一组 IntentProcessor,例如:
- 我要移动
- 我要瞄准
- 我要跳
- 我要切武器
- 我要触发某个 Action
第四步:计算参数
同一个管线里,ParameterProcessor 继续算出:
- 速度
- 朝向
- 视角差
- 动画混合参数
第五步:状态机决定当前动作
全身层、上半身层、Override 层会根据这些意图、拦截器和仲裁结果,确定“这一帧谁真正生效”。
第六步:交给 AnimancerFacade 播放
状态本身不直接操纵 Animator,而是经由动画外观层把动画状态播放到 Animancer。
而 Animancer 的 Transition 本身就是专门用来存储“动画片段 + 淡入参数 + 相关播放信息”的可序列化数据,这非常适合做成资源挂在 SO 里。
第七步:分层混合与遮罩
Animancer 层系统会根据 Layer 和 Avatar Mask 实现:
- 全身底层动作
- 上半身叠加动作
- 面部/表情层叠加
第八步:位移 / IK / 音效一起落地
最后由 MotionDriver、IKController、AudioDriver 等,把动画时间、角色位移、IK权重、音效事件统一落到最终画面
文档
PlayerInputReader
PlayerInputReader 是玩家输入系统的最前端采样器。
它负责从 Unity Input System 读取按键、摇杆和鼠标输入,并把这些硬件输入写入 RawInputData,供后面的 InputPipeline 做防抖、缓冲和意图翻译
添加物品状态
ItemDefinitionSO
└─ EquippableItemSO
└─ RangedWeaponSO
└─ AKSO
ItemDefinitionSO
这是所有物品的最基础定义。它只管最通用的信息,比如:
ItemIDDisplayNameIconDescriptionMaxStack
也就是说,只要是“物品”,不管是枪、手雷、药包、材料,都可以先继承这一层。
EquippableItemSO((必须包含实现了 IHoldableItem 的脚本))
还是一个能拿在手上的东西。
在第一层“物品”的基础上,再加上:
Prefab- 手持偏移
- 装备动画
- 收起动画
- 持有待机动画
RangedWeaponSO
这不仅能拿在手上,还是一个远程武器。
在“可装备物品”基础上,再加:
- 瞄准动画
- 弹药量
AKSO
在远程武器的通用能力上,再加 AK 自己特有的参数:
- 装备动画结束时间
- IK 启停时间
- 子弹预制体
- 枪口特效
- 音效
- 后坐力参数
- 随机抖动参数
AK46Behaviour(IHoldableItem, IPoolable)
- 前面那 4 个:本质上都是 “数据配置资产”。
- 是一个 运行时行为脚本,挂在武器 prefab 上,
AKSO提供数据,AK46Behaviour读取并执行这些数据。负责回答的是:“这把枪被拿起来以后,具体怎么装备、怎么瞄准、怎么开火、怎么播特效、怎么加后坐力。”
如何修正武器生成位置
- 将全身IK组件 Full Body Biped IK禁用
- 在transform中调整transform和rotation的值直到正确显示位置
- 然后将该值复制到XXXSO中物理表现的Offset中

调整人物IK
- 选中武器,找到下面的L子物体,调整其位置
- 然后复制正确的transform数据应用到预制体上

实战出真知
添加一个全身动作
首先Player Action Map 里新增一个 Action,做映射,然后创建脚本
- OActionTestSO.cs,创建并配置资源文件
- 存放动画资源、阈值、参数、状态名单、模块引用等数据资产
- 这些数据在XXState中用上,运行该状态具体如何执行
- PlayerOActionState
- 这个状态所需要处理的所有逻辑
- OIntentProcessor
- 将输入转换为黑板意图即 PlayerRuntimeData中的WantsToXXX(Bool)设置为true
- 从
ProcessedInputData里看绑定键位是否被按下,如果是则把它翻译成黑板意图,
- OActionInterceptorSO创建并配置资源文件
- 将意图转换为状态
- 如果这一帧黑板里出现了
WantsToPlayOAnim
并且当前不在PlayerOActionState
那就把状态切到PlayerOActionState
修改以下脚本:
- PlayerSO
- 所有配置文件(scriptable Object)的集合
- 声明OActionTestSO的引用变量,然后在Inspector面板拖入OActionTestSO资源文件
- public OActionTestSO OActionTest;
- InputData
- 其结构体RawInputData
- 声明public bool OHeld;
- 声明public bool OJustPressed;
- 其结构体ProcessedInputData
- public bool OHeld;
- public float OBufferTimer;
- public bool OPressed => OBufferTimer > 0f;
- 其结构体RawInputData
- PlayerInputReader
- 输入最前端的读取器
- 绑定各个状态的InputAction
- 声明public InputActionReference oAction;,并在面板拖入Input Action的键位
- FetchRaInput函数中
rawData.OHeld = oAction != null && oAction.action.IsPressed();
rawData.OJustPressed = oAction != null && oAction.action.WasPressedThisFrame(); - 在ToggleActions的all数组中加入oAction
- InputPipeline
- 输入从 Raw 变成 Processed 的地方
- ProcessRawInput()
currentFrame.Processed.WalkHeld = _rawData.WalkHeld;
currentFrame.Processed.OHeld = _rawData.OHeld;
currentFrame.Processed.OBufferTimer = UpdateBuffer(lastProc.OBufferTimer, _rawData.OJustPressed);
public void ConsumeOPressed(){
var f = _inputData.currentFrameData;
f.Processed.OBufferTimer = 0f;
_inputData.currentFrameData = f;
}
- PlayerRuntimeData
- public bool WantsToPlayOAnim;
- 在ResetIntetnt()中加入WantsToPlayOAnim = false;
- 新增
OIntentProcessor
using UnityEngine;
namespace BBBNexus
{
public class OIntentProcessor
{
private readonly PlayerRuntimeData _data;
private readonly InputPipeline _inputPipeline;
public OIntentProcessor(PlayerRuntimeData data, InputPipeline inputPipeline)
{
_data = data;
_inputPipeline = inputPipeline;
}
public void Update(in ProcessedInputData input)
{
if (input.OPressed)
{
_data.WantsToPlayOAnim = true;
_inputPipeline.ConsumeOPressed();
}
}
}
}
- 把
OIntentProcessor接到MainProcessorPipeline- 增加字段:private readonly OIntentProcessor _oIntentProcessor;
- 构造函数里 new:_oIntentProcessor = new OIntentProcessor(_runtimeData, _inputPipeline);
- 在
UpdateIntentProcessors()里调用:_oIntentProcessor.Update(in inputSnapshot);
- 新建全身状态
PlayerOActionState
添加一个行为树,让AI去操作一个人物
添加SwordSO
- 如何判断进入下一个动作的节点
- XXBehaviour的函数是怎么被纳入BBB CharacterController的Update函数
似乎没有下坠动画和状态?有wantsToFall的意图,但没有任何用一个controller调用
找到原因了因为默认工程写了FallInterceptorSO但是没有创建对应的数据文件
创建后将其拖入到PlayerBrain的Global Interceptor中,优先顺序是从上到下,所以放到第四个位置
然后在Locomotion中的下落时间设定为2,直言普通的跳跃不会触发





