【技术探索】Lyra 框架拆解(三):Character 组件体系与 InitState 状态机
系列导航
| 篇 | 主题 |
|---|---|
| 一 | 架构哲学总览 |
| 二 | Experience 系统 |
| 三 | Character 组件体系(本文) |
| 四 | GAS 集成层 |
| 五 | Equipment & Inventory |
| 六 | Input 系统 |
| 七 | 网络同步框架 |
| 八 | UI 系统 |
一句话
ALyraCharacter 不是一个”角色”,而是一个”插槽”——它只做三件事:创建核心 Component、转发接口调用、初始化死亡流程。所有真正的游戏逻辑全部下沉到独立的 PawnComponent 中,由 GameplayTag 驱动的 InitState 状态机协调初始化顺序。
一,继承链:从 Engine 到 Lyra
1 | ACharacter (Engine) |
AModularCharacter 极其简单——它只重写了三个函数:
1 | void PreInitializeComponents() { |
三行逻辑,但这是 Lyra 最关键的”锚点”——它把 Actor 接入到 GameFrameworkComponentManager 的全局管理体系中,从此任何 GameFeatureAction 都可以通过 AddExtensionHandler(ActorClass, Delegate) 在检测到此类 Actor 生成时动态注入 Component 和 Ability。
二,ALyraCharacter 的五层解耦
2.1 薄 Actor:构造函数就是全部”主动行为”
1 | ALyraCharacter::ALyraCharacter() { |
仅 3 个 Component。注意什么不在构造函数中:
- ❌ 没有
ULyraHeroComponent(输入绑定 + 相机代理)——在 BP 或 GameFeature 中添加 - ❌ 没有
UAbilitySystemComponent(GAS 核心)——放在ULyraPawnExtensionComponent中,被动创建 - ❌ 没有武器/装备 Component —— 通过 Equipment 系统动态挂载
2.2 接口转发:Actor 自己不做实现
ALyraCharacter 实现了 4 个接口,但全部是转发:
1 | // IAbilitySystemInterface |
2.3 InitState 状态机:不再依赖 BeginPlay 时序
在传统 UE 项目中,BeginPlay 的调用顺序不可靠——你无法保证 Component A 的 BeginPlay 在 Component B 之前执行。Lyra 用显式状态机替代隐式时序。
2.4 ExtensionHandler 动态注入
GameFeatureAction 在运行时动态注入 Component + AbilitySet——ALyraCharacter 无需知道是哪个插件注入了什么。
2.5 PawnData 数据驱动
所有配置外化到 ULyraPawnData DataAsset:AbilitySets、InputConfig、CameraMode、TagRelationshipMapping。设计师不写代码即可切换 Pawn 功能。
三,InitState 状态机详解
3.1 为什么用 GameplayTag 而不是枚举?
因为 GameFeature 插件可以定义自己的中间状态。例如,DLC 插件可以定义 InitState_DLC_AssetsLoaded,要求所有 Feature 在继续之前等待 DLC 资产加载——整个过程不需要修改引擎代码或核心框架。
3.2 四阶段状态
1 | InitState_Spawned ← Actor 已存在,Component 已注册 |
3.3 HeroComponent 的更严格前置
ULyraHeroComponent 的 DataAvailable 比 PawnExtensionComponent 更严格:
- PlayerState 必须存在
Controller->PlayerState->GetOwner() == Controller(确保绑定完成)- 本地控制且非 Bot:InputComponent + LyraPC + LocalPlayer 都必须存在
这就是 Lyra 对”输入绑定必须在所有玩家信息就绪后才能执行”这个隐式约束的显式化。
3.4 HaveAllFeaturesReachedInitState 的工作原理
UGameFrameworkComponentManager 维护每个 Actor 上所有已注册 Feature 的当前状态。当任何 Feature 的状态发生变化时:
- Manager 调用所有注册了
BindOnActorInitStateChanged的回调 ULyraPawnExtensionComponent在回调中调用CheckDefaultInitialization()- 检查”所有 Feature 是否都达到了目标状态” → 如果是,推进当前 Feature 的状态链
这是事件驱动而非轮询——状态推进只在状态变化时发生,无 CPU 浪费。
四,完整生命周期:9 阶段时间线
1 | 阶段 0-1: Experience 加载 |
关键洞察:ASC 生命周期 > Pawn 生命周期
ASC 放在 ALyraPlayerState 上而不是 ALyraCharacter 上。这意味着 Pawn 销毁时 ASC 存活,新 Pawn 直接复用旧 ASC。Respawn 不需要重新授予 AbilitySet、不需要重建 AttributeSet——所有 GAS 状态跨生命保持。
五,ExtensionHandler:GameFeature 如何动态注入
GameFeatureAction 通过 AddExtensionHandler 实现完全解耦的动态注入:
1 | 1. GameFeatureAction 注册 Handler: |
六,ARPG 落地要点
对于 TowerChallenge:
- 为 Character 创建 C++ 子类在 Lyra 体系中通常是反模式——这是框架的核心约束。我们有过教训:430 行
ATowerCharacter全量返工,最终 ~90 行 Component 解决了同样的问题。 - ASC 放 PlayerState 上——这是 Lyra 的选择,不是标准 GAS 做法。优点:Respawn 不丢失 GAS 状态。缺点:
GetASC()需要经过PlayerState间接访问。 - 自定义 InitState 中间状态——如果某个 Feature 需要等待 DLC 资产加载,定义
InitState_DLC_AssetsLoadedTag,在CanChangeInitState中检查即可。 - PawnData 的三级覆盖:Experience.DefaultPawnData → ActionSet.PawnData → 运行时覆盖。设计师通过切换 DataAsset 实现不同爬塔阶段的角色配置。
下一篇:GAS 集成层 — 拆解 Lyra 在 UE 原生 GAS 之上构建的五大封装层:AbilitySet 打包、InputTag 桥接、ActivationGroup 互斥、GlobalAbility 系统、Experience 注入管道。