Lyra项目浅析(新手向)-1
接口访问级别干啥的获取 Controller 对应的 PawnData下一帧重启玩家/Bot统一的重启判断玩家/Bot 初始化完成事件ProtectedExperience 加载完成回调Protected检查 Experience 状态Protected那个7层优先级解析函数Protected设置 Experience 到组件Protected专用服务器登录Protected专用服务器创建会话Pr
🔥 先赞后看,养成习惯! 这篇文章带你从0到1彻底搞懂 Lyra 项目中 GameMode 是怎么跑起来的、干了哪些活、哪些接口最关键。建议收藏,开发的时候随时翻阅~
📌 一句话概括
ALyraGameMode 和咱们以前写的传统 UE GameMode 完全不是一个思路。它不再硬编码玩法逻辑,而是变成了一个 “玩法调度中心”——活都交给 Experience 数据资产去干,GameMode 自己就负责协调调度。
说白了:GameMode 不干活,它只负责让对的资产干对的活。
🏗️ 一、先搞清楚继承链
AActor
└── AInfo
└── AGameModeBase
└── AModularGameModeBase ← ModularGameplayActors 插件
└── ALyraGameMode ← Lyra 项目
这里面 AModularGameModeBase 挺省事的,就是把默认的 GameState、PlayerController 之类的类全换成 Modular 版本,本身没加什么逻辑。所以核心都在 ALyraGameMode 里。
🎯 二、GameMode 到底负责干啥?
很多小伙伴刚接触 Lyra 的时候会懵:GameMode 怎么这么"空"?其实它负责的事情一点都不少,只不过干活方式变了:
| 职责 | 一句话说明 |
|---|---|
| 🧩 Experience 选择与启动 | 地图加载后,按优先级解析该加载哪个 Experience |
| 📡 Experience 加载监听 | 注册回调,加载完了我得知道 |
| 🧍 玩家 Pawn 生成 | Pawn 类型和数据都由 Experience 的 PawnData 决定 |
| 📍 玩家出生点选择 | 不自己选,甩锅给 PlayerSpawningManagerComponent |
| 🔄 玩家重启/重生 | 支持延迟重启,失败了还会自动重试 |
| 🖥️ 专用服务器登录 | Dedicated Server 的在线登录和会话托管 |
| 📢 玩家初始化广播 | 通过委托告诉其他系统"这个玩家准备好了" |
💡 划重点:你会发现 GameMode 自己几乎不实现业务逻辑,全是通过组件和委托来完成的。这就是 Lyra 的设计哲学——数据驱动 + 组件化。
🔄 三、完整运行流程——从地图加载到玩法运行
3.1 全局生命周期一览
先看一张大图,心里有个数:
3.2 构造函数——先把队伍拉起来
ALyraGameMode::ALyraGameMode(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
GameStateClass = ALyraGameState::StaticClass();
GameSessionClass = ALyraGameSession::StaticClass();
PlayerControllerClass = ALyraPlayerController::StaticClass();
ReplaySpectatorPlayerControllerClass = ALyraReplayPlayerController::StaticClass();
PlayerStateClass = ALyraPlayerState::StaticClass();
DefaultPawnClass = ALyraCharacter::StaticClass();
HUDClass = ALyraHUD::StaticClass();
}
构造函数干的事很简单——把 Lyra 自己的那套班子绑上去。GameState、GameSession、PlayerController、PlayerState、Pawn、HUD 全部换成 Lyra 版本。
⚠️ 注意:这里绑定的只是默认类,后面 Experience 加载的时候,PawnData 还会进一步覆盖和补充配置,所以别以为改了这里就完事了。
3.3 InitGame——关键的"下一帧"设计
void ALyraGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
Super::InitGame(MapName, Options, ErrorMessage);
// 重点来了:不是立刻解析,而是下一帧!
GetWorld()->GetTimerManager().SetTimerForNextTick(this, &ThisClass::HandleMatchAssignmentIfNotExpectingOne);
}
🤔 为什么要下一帧? 这是个好问题!
因为 InitGame 被调用的时候,很多配置还没初始化好呢——PIE 设置、URL 参数、命令行参数啥的可能还没来得及生效。延迟一帧就给这些东西留出了初始化的时间窗口。这个设计挺巧妙的,记住了,后面经常用到这个"延迟一帧"的套路。
3.4 Experience 选择——7层优先级,一个都不能少
HandleMatchAssignmentIfNotExpectingOne() 这个函数名字虽然长,但逻辑很清晰,就是按优先级一层层往下找 ExperienceId:
| 优先级 | 来源 | 怎么用 | 啥场景 |
|---|---|---|---|
| 1 | Matchmaking 分配 | 预留接口,目前还没实现 | 在线匹配 |
| 2 | URL 选项 | 地图名后面加 ?Experience=xxx |
快速测试、Seamless Travel 传参 |
| 3 | 开发者设置 | ULyraDeveloperSettings::ExperienceOverride |
仅 PIE 模式生效 |
| 4 | 命令行 | 启动参数加 -Experience=xxx |
命令行启动专用 |
| 5 | WorldSettings | 在地图的 WorldSettings 里配 | 给地图配默认 Experience |
| 6 | Dedicated Server | TryDedicatedServerLogin() |
专用服务器自动登录 |
| 7 | 默认回退 | B_LyraDefaultExperience |
以上全都没有,就它了 |
💡 实战小技巧:开发调试的时候,最方便的就是方法2和方法3——在PIE里直接override,或者URL里加参数,不用改代码就能切换玩法模式。
3.5 OnMatchAssignmentGiven——找到了,把它交给专业的人
void ALyraGameMode::OnMatchAssignmentGiven(FPrimaryAssetId ExperienceId, const FString& ExperienceIdSource)
{
ULyraExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass<ULyraExperienceManagerComponent>();
check(ExperienceComponent);
ExperienceComponent->SetCurrentExperience(ExperienceId);
}
拿到 ExperienceId 之后,GameMode 什么都不干,直接甩给 ULyraExperienceManagerComponent。这个组件才是真正负责加载的——异步加载资产、激活 GameFeature 插件、执行 Action,全是它的事。
🎮 打个比方:GameMode 是项目经理,ExperienceManagerComponent 是程序员。项目经理说"做这个需求",程序员就去干活了。
3.6 Experience 加载——一个完整的状态机
ULyraExperienceManagerComponent 内部是个状态机,状态流转如下:
整个加载过程拆解一下就是:
- SetCurrentExperience → 让 AssetManager 去加载 Experience 数据资产
- StartExperienceLoad → 异步加载 Experience 及 ActionSet 的 Bundle 资产(会区分 Client/Server)
- OnExperienceLoadComplete → 把需要加载的 GameFeature 插件 URL 收集起来
- LoadAndActivateGameFeaturePlugin → 一个一个加载并激活 GameFeature 插件
- OnExperienceFullLoadCompleted → 执行所有 Experience Actions 和 ActionSet 里的 Actions
- 广播 OnExperienceLoaded → 按高/中/低三个优先级通知所有监听者
⚠️ 踩坑提醒:GameFeature 插件加载是异步的,而且可能不止一个。如果你在 Experience 加载完成前就去访问某些插件里的东西,大概率会崩。一定要在 OnExperienceLoaded 回调之后再去操作!
3.7 OnExperienceLoaded——加载完了,赶紧把等着的玩家安排上
void ALyraGameMode::OnExperienceLoaded(const ULyraExperienceDefinition* CurrentExperience)
{
for (FConstPlayerControllerIterator Iterator = GetWorld()->GetPlayerControllerIterator(); Iterator; ++Iterator)
{
APlayerController* PC = Cast<APlayerController>(*Iterator);
if ((PC != nullptr) && (PC->GetPawn() == nullptr))
{
if (PlayerCanRestart(PC))
{
RestartPlayer(PC);
}
}
}
}
这段逻辑很直白:遍历所有已经连上但还没 Pawn 的玩家,赶紧给他们生成 Pawn。
为什么要这么做?因为玩家可能在 Experience 还没加载完的时候就连进来了,那时候 Pawn 类都还没确定呢,只能让他们先等着。现在 Experience 加载完了,该安排的都安排上。
3.8 InitGameState——先把回调注册好
void ALyraGameMode::InitGameState()
{
Super::InitGameState();
ULyraExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass<ULyraExperienceManagerComponent>();
check(ExperienceComponent);
ExperienceComponent->CallOrRegister_OnExperienceLoaded(
FOnLyraExperienceLoaded::FDelegate::CreateUObject(this, &ThisClass::OnExperienceLoaded));
}
CallOrRegister_OnExperienceLoaded 这个函数名就说明了一切——如果 Experience 已经加载完了,直接调用;如果还没完,先注册回调等着。这个设计很实用,不管谁先谁后都能处理。
🔧 四、核心接口详解——一个一个掰扯清楚
4.1 重写的 AGameModeBase 接口
📦 InitGame
virtual void InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) override;
- 啥时候调:地图加载后、任何 Actor 初始化之前
- Lyra 干了啥:调完
Super后,延迟一帧调度 Experience 解析 - 为啥这么干:前面说了,给配置初始化留时间
📡 InitGameState
virtual void InitGameState() override;
- 啥时候调:GameState 创建并初始化之后
- Lyra 干了啥:找到
ExperienceManagerComponent,注册OnExperienceLoaded回调 - 为啥这么干:不注册回调的话,GameMode 怎么知道 Experience 加载完了?到时候玩家就只能干等着了
🧍 GetDefaultPawnClassForController_Implementation
virtual UClass* GetDefaultPawnClassForController_Implementation(AController* InController) override;
- 啥时候调:要给 Controller 确定用哪个 Pawn 类的时候
- Lyra 干了啥:走
GetPawnDataForController拿 PawnData,从里面取PawnClass - 为啥这么干:Pawn 类不再写死在 GameMode 里了,而是由 Experience 的 PawnData 动态决定。想换个角色?改 PawnData 就行,代码都不用动
🎭 SpawnDefaultPawnAtTransform_Implementation
virtual APawn* SpawnDefaultPawnAtTransform_Implementation(AController* NewPlayer, const FTransform& SpawnTransform) override;
- 啥时候调:要在指定位置生成玩家 Pawn 的时候
- Lyra 干了啥:
- 拿到 Pawn 类
bDeferConstruction = true先不完成构造就 Spawn 出来- 找到
ULyraPawnExtensionComponent,调用SetPawnData注入 PawnData FinishSpawning完成整个 Pawn 创建
- 为啥这么干:💡 这是 Lyra 模块化 Pawn 的灵魂! PawnData 里包含输入配置、能力集(AbilitySet)、相机模式等。如果不先注入 PawnData 就完成构造,Pawn 里面那些依赖数据的初始化就会出问题
⚠️ 踩坑:如果你自己写代码生成 Pawn,一定要记得先调 SetPawnData 再 FinishSpawning,不然 Pawn 初始化会丢数据!
👋 HandleStartingNewPlayer_Implementation
virtual void HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer) override;
- 啥时候调:新玩家加入游戏
- Lyra 干了啥:只有 Experience 加载完了才继续处理,否则啥也不干
- 为啥这么干:Experience 没加载完的话,连 Pawn 类是啥都不知道,怎么给玩家生成 Pawn?不如先放着,等
OnExperienceLoaded的时候统一处理
📍 ChoosePlayerStart_Implementation
virtual AActor* ChoosePlayerStart_Implementation(AController* Player) override;
- Lyra 干了啥:直接交给
ULyraPlayerSpawningManagerComponent::ChoosePlayerStart - 为啥这么干:出生点选择逻辑解耦了!不同玩法可以换不同的 SpawningManager,GameMode 完全不用改
🔄 FinishRestartPlayer
virtual void FinishRestartPlayer(AController* NewPlayer, const FRotator& StartRotation) override;
- Lyra 干了啥:先通知
PlayerSpawningManagerComponent,再调Super - 为啥这么干:让 SpawningManager 在重启后也能做点自己的事情
✅ PlayerCanRestart_Implementation / ControllerCanRestart
virtual bool PlayerCanRestart_Implementation(APlayerController* Player) override;
virtual bool ControllerCanRestart(AController* Controller);
- Lyra 干了啥:统一走
ControllerCanRestart,里面分情况:- PlayerController → 先走引擎基础检查
- Bot Controller → 做简单有效性检查
- 最后都去问
PlayerSpawningManagerComponent::ControllerCanRestart
- 为啥这么干:玩家和 Bot 的重启逻辑统一管理,不用写两套
🚫 ShouldSpawnAtStartSpot
virtual bool ShouldSpawnAtStartSpot(AController* Player) override;
- Lyra 干了啥:永远返回
false - 为啥这么干:Lyra 完全不用引擎默认的 StartSpot 生成逻辑,出生点全由
PlayerSpawningManagerComponent管。这个返回 false 就是在告诉引擎:“别管了,我自己来”
📢 GenericPlayerInitialization
virtual void GenericPlayerInitialization(AController* NewPlayer) override;
- Lyra 干了啥:调完
Super之后,广播OnGameModePlayerInitialized委托 - 为啥这么干:提供统一的"玩家已就绪"事件,玩家和 Bot 都会触发,其他系统一听就知道有人准备好了
🔄 UpdatePlayerStartSpot
virtual bool UpdatePlayerStartSpot(AController* Player, const FString& Portal, FString& OutErrorMessage) override;
- Lyra 干了啥:直接返回
true,啥也不干 - 为啥这么干:这时候团队分配啥的还没完成呢,更新出生点没意义。等真正生成的时候再决定就行
⚡ FailedToRestartPlayer
virtual void FailedToRestartPlayer(AController* NewPlayer) override;
- Lyra 干了啥:如果 Pawn 类存在,就通过
RequestPlayerRestartNextFrame下一帧再试 - 为啥这么干:🛡️ 自动重试机制! 不然玩家可能因为某些临时条件不满足就永远无法重生了。这设计很贴心,避免了"卡死"的情况
4.2 Lyra 自己加的接口
🔍 GetPawnDataForController
UFUNCTION(BlueprintCallable, Category = "Lyra|Pawn")
const ULyraPawnData* GetPawnDataForController(const AController* InController) const;
这个函数的查找链值得仔细看:
- 先从
ALyraPlayerState::GetPawnData()找 → 可能被其他系统设置过了 - 找不到?从当前 Experience 的
DefaultPawnData找 - Experience 还没加载?返回
nullptr - Experience 加载了但没配 DefaultPawnData?返回全局默认的
ULyraAssetManager::GetDefaultPawnData()
💡 这个查找链的设计很灵活:支持按玩家定制(第1步)、按 Experience 定制(第2步)、还有兜底方案(第4步)。
⏳ RequestPlayerRestartNextFrame
UFUNCTION(BlueprintCallable)
void RequestPlayerRestartNextFrame(AController* Controller, bool bForceReset = false);
行为拆解:
bForceReset = true→ 立刻Controller->Reset()(把当前 Pawn 扔了)- PlayerController → 下一帧调
ServerRestartPlayer_Implementation - BotController → 下一帧调
ServerRestartController
💡 又是"下一帧"的套路!为什么要这样?因为在当前帧的调用栈里直接重启,很容易出递归或状态不一致的问题。延迟一帧就安全了。
📢 OnGameModePlayerInitialized
FOnLyraGameModePlayerInitialized OnGameModePlayerInitialized;
// 声明:DECLARE_MULTICAST_DELEGATE_TwoParams(FOnLyraGameModePlayerInitialized, AGameModeBase*, AController*)
玩家和 Bot 初始化完成后都会触发。外部系统想监听"有人准备好了",绑这个就对了。
4.3 Protected 接口(内部用的)
| 接口 | 一句话说明 |
|---|---|
OnExperienceLoaded |
Experience 加载完了,赶紧给等着的人生成 Pawn |
IsExperienceLoaded |
检查 Experience 加载没 |
HandleMatchAssignmentIfNotExpectingOne |
那个按7层优先级找 Experience 的函数 |
OnMatchAssignmentGiven |
找到 Experience 了,设置到组件里 |
TryDedicatedServerLogin |
专用服务器登录,用 CommonUserSubsystem |
HostDedicatedServerMatch |
解析命令行创建会话,触发地图旅行 |
OnUserInitializedForDedicatedServer |
专用服务器用户登录回调,不管成功失败都去创建会话 |
🤝 五、GameMode 和它的朋友们——协作关系全景图
5.1 先看架构图
5.2 ULyraExperienceManagerComponent——GameMode 最铁的兄弟
- 住在哪:
ALyraGameState上面的组件 - 干啥的:管理 Experience 的整个生命周期——加载、GameFeature 插件激活/反激活、Action 执行
- 和 GameMode 怎么交互:
- GameMode 调
SetCurrentExperience→ 让它去加载 - GameMode 调
CallOrRegister_OnExperienceLoaded→ 注册回调等通知 - GameMode 调
IsExperienceLoaded()→ 查状态 - GameMode 调
GetCurrentExperienceChecked()→ 拿加载完的 Experience(用来读 PawnData)
- GameMode 调
5.3 ULyraPlayerSpawningManagerComponent——出生点管家
- 住在哪:也是
ALyraGameState上的组件 - 干啥的:管玩家在哪出生、能不能重启
- GameMode 代理调用关系:
ChoosePlayerStart→ 甩给它选出生点ControllerCanRestart→ 甩给它判断能不能重启FinishRestartPlayer→ 甩给它做重启后处理
💡 设计亮点:出生点策略从 GameMode 解耦了!不同玩法想要不同的生成策略?换一个 SpawningManager 就行,GameMode 完全不用改。
5.4 ULyraBotCreationComponent——Bot 制造机
- 住在哪:还是
ALyraGameState上的组件 - 干啥的:Experience 加载完后自动创建 Bot
- 和 GameMode 怎么交互:
- 监听
CallOrRegister_OnExperienceLoaded_LowPriority(注意是低优先级!确保真人玩家先生成) - 调
GameMode->GenericPlayerInitialization和GameMode->RestartPlayer完成 Bot 初始化
- 监听
⚠️ 注意:Bot 用的是低优先级回调,这样保证真人玩家在 Bot 之前生成。如果你的自定义逻辑也依赖 Experience 加载完成,注意选对优先级!
5.5 ALyraWorldSettings——地图的默认配置
- 干啥的:给地图配默认 Experience
- 和 GameMode 怎么交互:GameMode 在找 ExperienceId 的时候,如果前面6种方式都没找到,就来这里通过
GetDefaultGameplayExperience()取
5.6 ALyraGameSession——简单的代理
- 干啥的:覆盖了
ProcessAutoLogin直接返回true - 为啥这么干:因为登录逻辑已经由 GameMode 的
TryDedicatedServerLogin处理了,GameSession 不用再管
🧬 六、玩家生成全流程——从连接到拿到 Pawn
这个流程太重要了,我单独拿出来讲。很多小伙伴搞不清楚"为什么玩家进来了但没有Pawn",看完这个就明白了:
💡 这就是为什么有时候玩家进来了但没有 Pawn——Experience 还没加载完呢!遇到这种情况不要慌,检查一下 Experience 加载是否正常。
🖥️ 七、专用服务器启动流程
如果你的项目要上专用服务器,这段流程必须搞明白:
⚠️ 注意:OnUserInitializedForDedicatedServer 里不管登录成功失败都会调 HostDedicatedServerMatch。这是因为即使在线登录失败,也可以用离线模式继续运行。
💎 八、关键设计模式——Epic 的工程师是怎么想的?
8.1 🧩 数据驱动取代继承
传统做法:想加个新玩法?继承 GameMode 写个新的。
Lyra 做法:配一个新的 Experience 数据资产就行。
传统:AGameMode_DM → AGameMode_TDM → AGameMode_CTF (继承链越来越长)
Lyra:B_ShooterGame_Experience / B_TopDown_Experience (配置不同,代码共享)
这才是正确的打开方式!🎉
8.2 ⏳ 延迟初始化
GameMode 到处都是"下一帧再干":
InitGame→ 下一帧才解析 ExperienceHandleStartingNewPlayer→ 等 Experience 加载完才处理RequestPlayerRestartNextFrame→ 下一帧才重启
这不是偷懒,这是智慧——避免在调用栈中产生递归和状态不一致的问题。
8.3 🔌 组件化委托
GameMode 把核心逻辑都甩给了 GameState 上的组件:
| 组件 | 负责啥 |
|---|---|
ExperienceManagerComponent |
Experience 生命周期管理 |
PlayerSpawningManagerComponent |
生成逻辑管理 |
BotCreationComponent |
Bot 创建管理 |
好处很明显:想换策略就换组件,GameMode 不用动。这就是开闭原则的实践!
8.4 📢 事件广播
OnGameModePlayerInitialized→ 玩家/Bot 初始化完了OnExperienceLoaded→ Experience 加载完了(还有三个优先级)
系统之间通过事件通信,谁也不用认识谁,解耦得干干净净。
📋 九、接口速查表——开发时随时翻
AGameModeBase 重写接口
| 接口 | 干啥的 | Lyra 的特殊处理 |
|---|---|---|
InitGame |
游戏初始化 | 下一帧才解析 Experience |
InitGameState |
GameState 初始化 | 注册 OnExperienceLoaded 回调 |
GetDefaultPawnClassForController |
获取 Pawn 类 | 从 PawnData 动态拿 |
SpawnDefaultPawnAtTransform |
生成 Pawn | 先注入 PawnData 再完成构造 |
HandleStartingNewPlayer |
处理新玩家 | 等 Experience 加载完才处理 |
ChoosePlayerStart |
选出生点 | 甩给 PlayerSpawningManagerComponent |
FinishRestartPlayer |
完成重启 | 通知 PlayerSpawningManagerComponent |
PlayerCanRestart |
能否重启 | 统一走 ControllerCanRestart |
ShouldSpawnAtStartSpot |
在 StartSpot 生成? | 永远返回 false |
GenericPlayerInitialization |
通用初始化 | 广播 OnGameModePlayerInitialized |
UpdatePlayerStartSpot |
更新出生点 | 直接返回 true |
FailedToRestartPlayer |
重启失败 | 自动下一帧重试 |
Lyra 自定义接口
| 接口 | 访问级别 | 干啥的 |
|---|---|---|
GetPawnDataForController |
Public + BP | 获取 Controller 对应的 PawnData |
RequestPlayerRestartNextFrame |
Public + BP | 下一帧重启玩家/Bot |
ControllerCanRestart |
Public, Virtual | 统一的重启判断 |
OnGameModePlayerInitialized |
Public Delegate | 玩家/Bot 初始化完成事件 |
OnExperienceLoaded |
Protected | Experience 加载完成回调 |
IsExperienceLoaded |
Protected | 检查 Experience 状态 |
HandleMatchAssignmentIfNotExpectingOne |
Protected | 那个7层优先级解析函数 |
OnMatchAssignmentGiven |
Protected | 设置 Experience 到组件 |
TryDedicatedServerLogin |
Protected | 专用服务器登录 |
HostDedicatedServerMatch |
Protected | 专用服务器创建会话 |
OnUserInitializedForDedicatedServer |
Protected | 专用服务器登录回调 |
📂 十、源文件索引
| 文件 | 说明 |
|---|---|
| LyraGameMode.h | GameMode 头文件 |
| LyraGameMode.cpp | GameMode 实现 |
| LyraGameState.h | GameState 头文件 |
| LyraExperienceManagerComponent.h | Experience 管理组件头文件 |
| LyraExperienceManagerComponent.cpp | Experience 管理组件实现 |
| LyraExperienceDefinition.h | Experience 数据定义 |
| LyraWorldSettings.h | WorldSettings(默认 Experience) |
| LyraPlayerSpawningManagerComponent.h | 玩家生成管理组件 |
| LyraBotCreationComponent.h | Bot 创建组件 |
| LyraUserFacingExperienceDefinition.h | UI 侧 Experience 定义 |
| LyraGameSession.h | GameSession |
| ModularGameMode.h | Modular GameMode 基类 |
🎯 总结一句话:Lyra 的 GameMode 不是传统意义上的"游戏规则制定者",而是一个"玩法调度中心"。它通过 Experience 数据资产驱动玩法加载,通过组件委托处理具体逻辑,通过事件广播通知外部系统。理解了这个定位,你就理解了 Lyra 的设计精髓!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)