系列导航

主题
架构哲学总览
Experience 系统
Character 组件体系(本文)
GAS 集成层
Equipment & Inventory
Input 系统
网络同步框架
UI 系统

一句话

ALyraCharacter 不是一个”角色”,而是一个”插槽”——它只做三件事:创建核心 Component、转发接口调用、初始化死亡流程。所有真正的游戏逻辑全部下沉到独立的 PawnComponent 中,由 GameplayTag 驱动的 InitState 状态机协调初始化顺序。


一,继承链:从 Engine 到 Lyra

1
2
3
ACharacter (Engine)
└─ AModularCharacter (ModularGameplayActors 插件)
└─ ALyraCharacter (LyraGame)

AModularCharacter 极其简单——它只重写了三个函数:

1
2
3
4
5
6
7
8
9
10
11
12
void PreInitializeComponents() {
// 将此 Actor 注册为 GameFrameworkComponentManager 的 Receiver
// 这是 Actor 级别的唯一注册点
UGameFrameworkComponentManager::AddReceiver(this);
}
void BeginPlay() {
Super::BeginPlay();
// 发送信号:Actor 已就绪,GameFeatureAction 可以开始注入
UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(
this, NAME_GameActorReady);
}
void EndPlay() { /* 反注册 */ }

三行逻辑,但这是 Lyra 最关键的”锚点”——它把 Actor 接入到 GameFrameworkComponentManager 的全局管理体系中,从此任何 GameFeatureAction 都可以通过 AddExtensionHandler(ActorClass, Delegate) 在检测到此类 Actor 生成时动态注入 Component 和 Ability。


二,ALyraCharacter 的五层解耦

2.1 薄 Actor:构造函数就是全部”主动行为”

1
2
3
4
5
6
7
8
9
ALyraCharacter::ALyraCharacter() {
PawnExtComp = CreateDefaultSubobject<ULyraPawnExtensionComponent>("PawnExtension");
HealthComp = CreateDefaultSubobject<ULyraHealthComponent>("HealthComponent");
CameraComp = CreateDefaultSubobject<ULyraCameraComponent>("CameraComponent");

// 替换默认 MovementComponent 为 Lyra 定制版
ObjectInitializer.SetDefaultSubobjectClass<ULyraCharacterMovementComponent>(
ACharacter::CharacterMovementComponentName);
}

仅 3 个 Component。注意什么不在构造函数中:

  • ❌ 没有 ULyraHeroComponent(输入绑定 + 相机代理)——在 BP 或 GameFeature 中添加
  • ❌ 没有 UAbilitySystemComponent(GAS 核心)——放在 ULyraPawnExtensionComponent 中,被动创建
  • ❌ 没有武器/装备 Component —— 通过 Equipment 系统动态挂载

2.2 接口转发:Actor 自己不做实现

ALyraCharacter 实现了 4 个接口,但全部是转发

1
2
3
4
5
6
7
8
9
10
11
12
// IAbilitySystemInterface
UAbilitySystemComponent* GetAbilitySystemComponent() const {
return PawnExtComp->GetLyraAbilitySystemComponent(); // 转发到 PawnExtension
}

// IGameplayTagAssetInterface
void GetOwnedGameplayTags(FGameplayTagContainer& TagContainer) const {
GetAbilitySystemComponent()->GetOwnedGameplayTags(TagContainer); // 转发到 ASC
}

// ILyraTeamAgentInterface(队伍归属)
// MyTeamID 成员变量 + ConditionalBroadcastTeamChanged 广播

2.3 InitState 状态机:不再依赖 BeginPlay 时序

在传统 UE 项目中,BeginPlay 的调用顺序不可靠——你无法保证 Component A 的 BeginPlay 在 Component B 之前执行。Lyra 用显式状态机替代隐式时序。

2.4 ExtensionHandler 动态注入

GameFeatureAction 在运行时动态注入 Component + AbilitySet——ALyraCharacter 无需知道是哪个插件注入了什么。

2.5 PawnData 数据驱动

所有配置外化到 ULyraPawnData DataAsset:AbilitySetsInputConfigCameraModeTagRelationshipMapping。设计师不写代码即可切换 Pawn 功能。


三,InitState 状态机详解

3.1 为什么用 GameplayTag 而不是枚举?

因为 GameFeature 插件可以定义自己的中间状态。例如,DLC 插件可以定义 InitState_DLC_AssetsLoaded,要求所有 Feature 在继续之前等待 DLC 资产加载——整个过程不需要修改引擎代码或核心框架。

3.2 四阶段状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
InitState_Spawned           ← Actor 已存在,Component 已注册
│ 触发:BeginPlay
│ 条件:无(总是通过)

InitState_DataAvailable ← 所需配置数据已就绪
│ 触发:SetPawnData() + Controller 已 Possessed
│ 条件:PawnData 非空;Authority/Local 需已 Possessed

InitState_DataInitialized ← 所有 Feature 的数据都已就绪
│ 触发:HaveAllFeaturesReachedInitState(DataAvailable)
│ 条件:HeroComponent 的 DataAvailable 也通过
│ (Hero 需要 PlayerState + Controller + LocalPlayer)

InitState_GameplayReady ← 所有 Component 初始化完成,开始游戏逻辑
条件:无条件通过(实际工作由各 Component 回调完成)

3.3 HeroComponent 的更严格前置

ULyraHeroComponentDataAvailablePawnExtensionComponent 更严格:

  • PlayerState 必须存在
  • Controller->PlayerState->GetOwner() == Controller(确保绑定完成)
  • 本地控制且非 Bot:InputComponent + LyraPC + LocalPlayer 都必须存在

这就是 Lyra 对”输入绑定必须在所有玩家信息就绪后才能执行”这个隐式约束的显式化。

3.4 HaveAllFeaturesReachedInitState 的工作原理

UGameFrameworkComponentManager 维护每个 Actor 上所有已注册 Feature 的当前状态。当任何 Feature 的状态发生变化时:

  1. Manager 调用所有注册了 BindOnActorInitStateChanged 的回调
  2. ULyraPawnExtensionComponent 在回调中调用 CheckDefaultInitialization()
  3. 检查”所有 Feature 是否都达到了目标状态” → 如果是,推进当前 Feature 的状态链

这是事件驱动而非轮询——状态推进只在状态变化时发生,无 CPU 浪费。


四,完整生命周期:9 阶段时间线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
阶段 0-1: Experience 加载
└─ GameMode::OnExperienceLoaded → RestartPlayer

阶段 2: Pawn 生成与 PawnData 注入
└─ SpawnActor(bDeferConstruction) → SetPawnData → FinishSpawning → BeginPlay
★ PawnData 在 BeginPlay 之前注入——组件 BeginPlay 时 PawnData 已可用

阶段 3: InitState 启动
└─ PawnExtensionComponent::BeginPlay → TryToChangeInitState(Spawned) ✓
→ CheckDefaultInitialization → DataAvailable ✓

阶段 4: PlayerState 设置
└─ LyraPlayerState::SetPawnData() → GiveAbilitySet(PawnData->AbilitySets)
→ NAME_LyraAbilityReady 事件 → GameFeatureAction 注入额外 AbilitySet

阶段 5: DataInitialized → 全局初始化
└─ HeroComponent: InitializeAbilitySystem(ASC, LyraPS)
→ ASC 广播 OnAbilitySystemInitialized
→ HealthComponent: InitializeWithAbilitySystem(ASC) → 绑定 HealthSet 委托
→ HeroComponent: InitializePlayerInput → Enhanced Input 全链路绑定
→ CameraComponent: DetermineCameraModeDelegate 绑定

阶段 6: GameplayReady
└─ NAME_BindInputsNow 事件 → GameFeatureAction 绑定额外输入
→ GameplayReady ✓ → 玩家可正常游戏

阶段 7: 游戏运行中
└─ ASC 处理 Ability、HealthComponent 监听属性、CameraComponent 混合 CameraMode

阶段 8: 角色死亡
└─ HandleOutOfHealth → StartDeath → DisableMovementAndCollision
→ FinishDeath → DestroyDueToDeath
★ ASC 在 PlayerState 上继续存活,不随 Pawn 销毁

阶段 9: Respawn
└─ GameMode 生成新 Pawn → InitializeAbilitySystem(复用旧 ASC)
→ 新 Pawn 成为 ASC 的 Avatar

关键洞察:ASC 生命周期 > Pawn 生命周期

ASC 放在 ALyraPlayerState 上而不是 ALyraCharacter 上。这意味着 Pawn 销毁时 ASC 存活,新 Pawn 直接复用旧 ASC。Respawn 不需要重新授予 AbilitySet、不需要重建 AttributeSet——所有 GAS 状态跨生命保持。


五,ExtensionHandler:GameFeature 如何动态注入

GameFeatureAction 通过 AddExtensionHandler 实现完全解耦的动态注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
1. GameFeatureAction 注册 Handler:
Manager->AddExtensionHandler(ALyraCharacter::StaticClass(),
FGameFrameworkComponentExtensionDelegate::CreateLambda([](AActor* Actor) {
// 2. 动态创建 Component
UActorComponent* NewComp = NewObject<UMyFeatureComponent>(Actor);
NewComp->RegisterComponent();

// 3. 授予 AbilitySet
MyAbilitySet->GiveToAbilitySystem(ASC, &GrantedHandles, Actor);
}));

4. 当任何 ALyraCharacter 生成时 → Manager 自动调用 Lambda
5. ALyraCharacter 从头到尾不知道是哪个插件给它加了什么

六,ARPG 落地要点

对于 TowerChallenge:

  1. 为 Character 创建 C++ 子类在 Lyra 体系中通常是反模式——这是框架的核心约束。我们有过教训:430 行 ATowerCharacter 全量返工,最终 ~90 行 Component 解决了同样的问题。
  2. ASC 放 PlayerState 上——这是 Lyra 的选择,不是标准 GAS 做法。优点:Respawn 不丢失 GAS 状态。缺点:GetASC() 需要经过 PlayerState 间接访问。
  3. 自定义 InitState 中间状态——如果某个 Feature 需要等待 DLC 资产加载,定义 InitState_DLC_AssetsLoaded Tag,在 CanChangeInitState 中检查即可。
  4. PawnData 的三级覆盖:Experience.DefaultPawnData → ActionSet.PawnData → 运行时覆盖。设计师通过切换 DataAsset 实现不同爬塔阶段的角色配置。

下一篇GAS 集成层 — 拆解 Lyra 在 UE 原生 GAS 之上构建的五大封装层:AbilitySet 打包、InputTag 桥接、ActivationGroup 互斥、GlobalAbility 系统、Experience 注入管道。