Lyra Development 模块深度解析——把开发效率焊死在你的工作流里
写在前面:PIE一小时,调配置两分钟?
不知道你有没有过这种体验——想在编辑器里跑一下多人对战,结果手动开十几个PIE窗口、一个个输控制台命令加Bot、调到一半发现用的是线上Experience而不是调试用的那个、切回来重新来……等一切就绪,午饭都凉了。
这不是你的问题。大型项目的编辑器工作流本身就是个"隐形的时间黑洞"。每次你手动做一件重复的事,看起来只花了两分钟,但一天重复二十次,那就是半小时以上——更可怕的是,这些时间会打断你的心流,让你从"写代码"的状态里被反复拽出来。
Lyra 的 Development 模块就是为解决这问题而生的。它不是一个"大模块"——总共就六个源文件、三个核心类——但它像一把瑞士军刀,把开发过程中那些"每次都要手动搞一遍"的脏活累活全自动化了。Bot管理、平台模拟、Experience切换、自动作弊命令……这三四个小工具凑在一起,省下来的时间够你多写不少逻辑。
1. 先看清地图:这个模块到底干了什么
在深入每个类的实现之前,咱们先站在高处把整个模块的布局看清楚。Development 模块的三个核心类,各自管一块独立的事情,但又通过 LyraBotCreationComponent 和编辑器生命周期悄悄地联系在一起。
1.1 三大组件速览
| 类名 | 文件 | 一句话概括 |
|---|---|---|
ULyraBotCheats | [LyraBotCheats.h](file:///e:/UEProject/LyraStarterGame/Source/LyraGame/Development/LyraBotCheats.h) / .cpp | 控制台敲 AddPlayerBot 就能加机器人,不用切窗口 |
ULyraDeveloperSettings | [LyraDeveloperSettings.h](file:///e:/UEProject/LyraStarterGame/Source/LyraGame/Development/LyraDeveloperSettings.h) / .cpp | 编辑器项目设置面板,PIE时自动切Experience、调Bot数量、跑预设作弊命令 |
ULyraPlatformEmulationSettings | [LyraPlatformEmulationSettings.h](file:///e:/UEProject/LyraStarterGame/Source/LyraGame/Development/LyraPlatformEmulationSettings.h) / .cpp | 在编辑器里假装自己是PS5/Xbox/Switch,不用实机就能测多平台兼容性 |
1.2 架构全景图
┌────────────────────────────────────────────────────────────────────────┐
│ 编辑器 (Editor) │
│ ┌──────────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ ULyraDeveloperSettings │ │ ULyraPlatformEmulationSettings │ │
│ │ (项目设置 → Lyra 分类) │ │ (项目设置 → Lyra 分类) │ │
│ │ │ │ │ │
│ │ • ExperienceOverride │ │ • PretendPlatform │ │
│ │ • Bot数量 / 行为开关 │ │ • 平台特征标签控制 │ │
│ │ • PIE流程控制 │ │ • 设备配置文件模拟 │ │
│ │ • 自动作弊命令列表 │ │ • 帧率/性能选项 │ │
│ │ • 常用地图快捷入口 │ │ │ │
│ └──────────────┬───────────────┘ └──────────────┬──────────────────┘ │
│ │ │ │
│ │ OnPlayInEditorStarted() │ OnPlayInEditorStarted()
│ │ (显示Toast提醒) │ (显示Toast提醒) │
└─────────────────┼─────────────────────────────────┼────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 运行时 (Runtime) │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ ULyraBotCheats │ │
│ │ (CheatManagerExtension — 自动注册) │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌──────────────────┐ │ │
│ │ │ AddPlayerBot() │ │ RemovePlayerBot() │ │ │
│ │ │ (Exec命令) │ │ (Exec命令) │ │ │
│ │ └────────┬────────┘ └────────┬─────────┘ │ │
│ │ │ │ │ │
│ └───────────┼──────────────────────┼────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ LyraBotCreationComponent (GameModes模块) │ │
│ │ • Cheat_AddBot() │ │
│ │ • Cheat_RemoveBot() │ │
│ │ • 读取 ULyraDeveloperSettings 的Bot数量配置 │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ UPlatformSettingsManager / UCommonUIVisibilitySubsystem │ │
│ │ (接收平台模拟设置,影响全局行为) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
你看,这个架构其实一点都不复杂。三个设置类通过编辑器生命周期联动,一个运行时类通过 CheatManager 扩展机制自动注入。各自职责边界清清楚楚,谁也别越界。
1.3 模块依赖关系
Development 模块
│
├── 继承链
│ ├── ULyraBotCheats → UCheatManagerExtension
│ ├── ULyraDeveloperSettings → UDeveloperSettingsBackedByCVars
│ └── ULyraPlatformEmulationSettings → UDeveloperSettingsBackedByCVars
│
├── 内部依赖
│ └── LyraBotCreationComponent (GameModes模块) ← 真正干活的Bot工厂
│
├── 引擎依赖
│ ├── UPlatformSettingsManager ← 模拟平台切换
│ ├── UDeviceProfileManager ← 设备配置选择
│ ├── FSlateNotificationManager ← 编辑器Toast提醒
│ └── UCommonUIVisibilitySubsystem ← 平台特征控制UI可见性
│
└── 编译控制
├── WITH_SERVER_CODE
├── UE_WITH_CHEAT_MANAGER
└── WITH_EDITOR / WITH_EDITORONLY_DATA
你可能注意到了,ULyraDeveloperSettings 和 ULyraPlatformEmulationSettings 都继承自 UDeveloperSettingsBackedByCVars。这是什么意思?就是说这些配置项不仅能在编辑器的 Project Settings 面板里改,还能通过控制台变量(CVar)在运行时动态覆盖。面板改一下立刻生效,不用重启编辑器——这对调试来说简直太香了。
2. Bot作弊系统:一行命令就能拉满战场
2.1 你为什么要关心一个"作弊"类?
先澄清一个概念——UE里的 CheatManager 并不是"外挂作弊器"的意思。它更像一个开发调试的快捷通道。正式打包的时候这些命令根本不会编译进去。你完全可以把它理解为"给开发者留的后门"。
那 Lyra 的 ULyraBotCheats 做了什么?两个命令、不到60行代码,就把"加Bot"这件每次PIE必做的事自动化了。
2.2 类的骨架
// LyraBotCheats.h(精简展示)
UCLASS(NotBlueprintable)
class ULyraBotCheats final : public UCheatManagerExtension
{
GENERATED_BODY()
public:
ULyraBotCheats();
UFUNCTION(Exec, BlueprintAuthorityOnly)
void AddPlayerBot();
UFUNCTION(Exec, BlueprintAuthorityOnly)
void RemovePlayerBot();
private:
ULyraBotCreationComponent* GetBotComponent() const;
};
几个关键标记你得看懂:
| 标记 | 作用 |
|---|---|
NotBlueprintable | 不允许蓝图继承,纯C++工具类,不需要暴露给策划 |
final | 禁止再被继承,防止架构膨胀。一个简单的工具类别搞出继承树 |
Exec | 让这个函数可以当控制台命令执行,你敲 AddPlayerBot 就直接进这个函数 |
BlueprintAuthorityOnly | 只在服务器端有效。Bot是服务器创建的,客户端没权限 |
2.3 自动注册机制——最精彩的部分
这个类最妙的地方不在它的业务逻辑,而在它是怎么把自己装到 CheatManager 里去的。来看构造函数:
ULyraBotCheats::ULyraBotCheats()
{
#if WITH_SERVER_CODE && UE_WITH_CHEAT_MANAGER
if (HasAnyFlags(RF_ClassDefaultObject))
{
UCheatManager::RegisterForOnCheatManagerCreated(
FOnCheatManagerCreated::FDelegate::CreateLambda(
[](UCheatManager* CheatManager)
{
CheatManager->AddCheatManagerExtension(
NewObject<ThisClass>(CheatManager));
}));
}
#endif
}
这个构造函数干了四件非常聪明的事:
第一,条件编译三重保护。
#if WITH_SERVER_CODE && UE_WITH_CHEAT_MANAGER
两层把关——既要求当前编译目标包含服务器代码(客户端不需要Bot管理),又要求引擎的 CheatManager 功能被启用。Release包里这段代码直接消失,零开销。
第二,只在CDO构造时注册一次。
if (HasAnyFlags(RF_ClassDefaultObject))
UE 里每个 UClass 都有一个唯一的"类默认对象"(Class Default Object, CDO),它在引擎启动时构造一次,之后所有实例都从它拷贝属性。用 RF_ClassDefaultObject 标志确保这个注册逻辑只在 CDO 构造时跑,不会每创建一个实例就重复注册一遍。
第三,委托 + Lambda 解耦注册时机。
RegisterForOnCheatManagerCreated 注册的回调会在每次有新的 CheatManager 被创建时触发——比如PIE启动、关卡切换、网络连接建立。Lambda 里做的事很简单:拿到这个 CheatManager,往上面挂一个新的 ULyraBotCheats 实例作为扩展。
第四,NewObject<ThisClass> 的 Outer 链。
注意 NewObject<ThisClass>(CheatManager) 里的 CheatManager——它把这个新创建的 ULyraBotCheats 对象 Outer 到了 CheatManager 上。这样当 CheatManager 被销毁时,这个扩展对象也会被自动GC回收。不用手动管理生命周期,靠UE的UObject体系帮你兜底。
2.4 核心逻辑:从控制台到真实Bot的路径
void ULyraBotCheats::AddPlayerBot()
{
#if WITH_SERVER_CODE && UE_WITH_CHEAT_MANAGER
if (ULyraBotCreationComponent* BotComponent = GetBotComponent())
{
BotComponent->Cheat_AddBot();
}
#endif
}
逻辑本身极其简单——拿到 BotComponent,调它的作弊方法。那 GetBotComponent() 是怎么找到这个组件的?
ULyraBotCreationComponent* ULyraBotCheats::GetBotComponent() const
{
if (UWorld* World = GetWorld())
{
if (AGameStateBase* GameState = World->GetGameState())
{
return GameState->FindComponentByClass<ULyraBotCreationComponent>();
}
}
return nullptr;
}
这是一条经典的 UE 对象链式查找路径:
CheatManager → GetWorld() → World → GetGameState() → GameState
→ FindComponentByClass<ULyraBotCreationComponent>()
→ BotComponent (或 nullptr)
FindComponentByClass 是 UE 提供的模板方法,会在 GameState 的所有 ActorComponent 中按类型查找。这样设计的好处是——ULyraBotCheats 不需要知道 BotCreationComponent 在哪,它只需要知道"这玩意儿一定挂在 GameState 上"。即使以后 Bot 的创建逻辑移到了别的地方,只要保证 GameState 上能找到一个 LyraBotCreationComponent,这边的代码一行不用改。
2.5 实战场景
假设你现在正在调一个3v3的团战逻辑,需要快速凑够6个人。不用这个工具的话,你可能要这样:
① 启动PIE
② 打开另一个PIE窗口(客户端)
③ 再开一个……
④ 每个窗口手动输控制台命令加Bot
有了 ULyraBotCheats,你只需在服务器窗口输入:
AddPlayerBot
AddPlayerBot
AddPlayerBot
AddPlayerBot
AddPlayerBot
五条命令,五个Bot,三秒搞定。
3. 开发者设置:你的编辑器专属"驾驶舱"
3.1 不只是个配置面板
ULyraDeveloperSettings 可能是三个类里"看起来最简单、实际最有用"的那个。它的核心价值在于——把你每次PIE前需要手动调整的设置集中到一个面板,并且在你启动PIE时主动提醒你当前开了哪些非默认配置。
3.2 配置清单
| 配置项 | 类型 | 默认值 | 干什么用的 |
|---|---|---|---|
ExperienceOverride | FPrimaryAssetId | 空 | 覆盖PIE时用的Experience。比如你正在调Boss战,不想每次从头走一遍大厅流程——直接切到Boss战的Experience |
bOverrideBotCount | bool | false | 要不要覆盖默认Bot数量 |
OverrideNumPlayerBotsToSpawn | int32 | 0 | 覆盖几个Bot。配合上面那个开关用 |
bAllowPlayerBotsToAttack | bool | true | Bot能不能攻击。调非战斗逻辑的时候把它关了,省得被Bot打死 |
bTestFullGameFlowInPIE | bool | false | PIE要不要走完整游戏流程。平时关掉跳过等待大厅,联调时再打开 |
bSkipLoadingCosmeticBackgroundsInPIE | bool | false | 跳过外观背景加载。迭代期关掉,加载快一两秒 |
CheatsToRun | TArray<FLyraCheatToRun> | 空 | PIE启动时自动执行的作弊命令列表 |
LogGameplayMessages | bool | false | 是否记录 Gameplay Message 子系统日志 |
CommonEditorMaps | TArray<FSoftObjectPath> | 空 | 编辑器工具栏快速切换的常用地图 |
3.3 自动作弊命令——“进门就全自动”
这个功能我觉得是 DeveloperSettings 里最有想象力的设计。看它的数据结构:
UENUM()
enum class ECheatExecutionTime
{
OnCheatManagerCreated, // CheatManager 创建时执行
OnPlayerPawnPossession // 玩家控制 Pawn 时执行
};
USTRUCT()
struct FLyraCheatToRun
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
ECheatExecutionTime Phase = ECheatExecutionTime::OnPlayerPawnPossession;
UPROPERTY(EditAnywhere)
FString Cheat;
};
你在编辑器的 Project Settings 里可以配一个作弊命令列表,每个命令指定它在什么时机自动执行:
OnCheatManagerCreated:适合那些不依赖玩家Pawn的命令,比如加载资源、修改全局参数OnPlayerPawnPossession(默认):适合那些需要玩家角色已经存在的命令,比如God(无敌)、Fly(飞行)、GiveItem 某武器
举个例子——你每次PIE测试战斗都需要开无敌、加满弹药、飞到特定坐标。以前你每次都要手动输三行命令,现在直接在面板里配好:
CheatsToRun:
[0] Phase=OnPlayerPawnPossession, Cheat="God"
[1] Phase=OnPlayerPawnPossession, Cheat="GiveAmmo 999"
[2] Phase=OnPlayerPawnPossession, Cheat="TeleportTo BattleArena"
PIE启动,角色生成,命令自动跑完——你直接开始测试。
3.4 “别忘了你开了覆盖”——PIE Toast提醒
这又是一个特别贴心的细节。当你开了 ExperienceOverride,启动PIE时编辑器右上角会弹一个Toast:
Developer Settings Override
Experience BossFight_Experience
这个通知2秒后自动消失,不会挡你操作,但足够让你意识到"哦对,我现在用的是Boss战的Experience而不是默认的"。这个设计的意义在于——减少"我明明改了配置怎么没反应"的排查时间。
实现代码:
void ULyraDeveloperSettings::OnPlayInEditorStarted() const
{
if (ExperienceOverride.IsValid())
{
FNotificationInfo Info(FText::Format(
LOCTEXT("ExperienceOverrideActive",
"Developer Settings Override\nExperience {0}"),
FText::FromName(ExperienceOverride.PrimaryAssetName)
));
Info.ExpireDuration = 2.0f;
FSlateNotificationManager::Get().AddNotification(Info);
}
}
这里值得注意的细节:
FSlateNotificationManager是编辑器通知系统,只在编辑器下有效,打包后不存在LOCTEXT宏支持本地化,多语言团队友好ExpireDuration = 2.0f设置自动消失时间,不会像某些弹窗一样赖着不走PrimaryAssetName只显示资源名,不显示完整路径,信息简洁
3.5 设置如何生效——三个时机
void ULyraDeveloperSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
ApplySettings();
}
void ULyraDeveloperSettings::PostReloadConfig(FProperty* PropertyThatWasLoaded)
{
Super::PostReloadConfig(PropertyThatWasLoaded);
ApplySettings();
}
void ULyraDeveloperSettings::PostInitProperties()
{
Super::PostInitProperties();
ApplySettings();
}
三个入口,同一个出口 ApplySettings():
| 入口 | 触发时机 |
|---|---|
PostEditChangeProperty | 你在编辑器面板里改了一个值,立刻触发 |
PostReloadConfig | 配置文件被重新加载(比如切分支、改ini文件) |
PostInitProperties | 对象属性初始化完成(引擎启动时) |
这三个时机覆盖了"修改→重载→启动"的完整生命周期。ApplySettings() 目前是空实现(预留扩展点),但它上面的 ExperienceOverride 之类的配置项实际上是通过 UDeveloperSettingsBackedByCVars 基类的机制自动映射到控制台变量的,不需要额外代码。
4. 平台模拟:一台电脑测遍全平台
4.1 痛点场景
你做了一整套移动端UI适配,想验证一下在Switch上效果怎么样。没有实机的话,你通常只能:
- 把项目拷到Switch开发机上跑(来回半小时)
- 截图发给QA(沟通成本高)
- 靠想象力(不靠谱)
ULyraPlatformEmulationSettings 的思路是:你不需要实机,你在编辑器里就能"假装"自己是任何平台。它会切换平台配置文件、设备配置、平台特征标签——引擎的底层行为会根据这些设置改变,你就直接在编辑器里看到接近真机的效果。
4.2 配置清单
| 配置项 | 类型 | 默认值 | 干什么用的 |
|---|---|---|---|
PretendPlatform | FName | NAME_None | 模拟的平台名(如 IOS、Android、Switch) |
PretendBaseDeviceProfile | FName | NAME_None | 模拟的设备配置文件(如 iPhone13) |
AdditionalPlatformTraitsToEnable | FGameplayTagContainer | 空 | 额外启用的平台特征标签 |
AdditionalPlatformTraitsToSuppress | FGameplayTagContainer | 空 | 额外抑制的平台特征标签 |
bApplyFrameRateSettingsInPIE | bool | false | PIE中是否应用帧率限制(模拟低端机帧率) |
bApplyFrontEndPerformanceOptionsInPIE | bool | false | PIE中是否应用前端性能选项 |
bApplyDeviceProfilesInPIE | bool | false | PIE中是否应用设备配置文件 |
4.3 核心机制:平台特征标签系统
这里有个你可能不太熟悉的概念——GameplayTag 驱动的平台特征系统。Lyra 用 GameplayTag 来描述"这个平台有哪些特征",比如:
Platform.Trait.Mobile—— 这是个移动平台Platform.Trait.SupportsMouseAndKeyboard—— 支持键鼠Input.Touch—— 有触屏输入
UI系统、输入系统、性能系统都可以查询这些标签来决定自己的行为。ULyraPlatformEmulationSettings 做的事就是在编辑器里篡改这些标签:
void ULyraPlatformEmulationSettings::ApplySettings()
{
UCommonUIVisibilitySubsystem::SetDebugVisibilityConditions(
AdditionalPlatformTraitsToEnable,
AdditionalPlatformTraitsToSuppress);
if (GIsEditor && PretendPlatform != LastAppliedPretendPlatform)
{
ChangeActivePretendPlatform(PretendPlatform);
}
PickReasonableBaseDeviceProfile();
}
三步走:
- 设置UI可见性条件——告诉 CommonUI 子系统哪些平台特征标签应该假装存在/不存在。比如你启用了
Platform.Trait.Mobile,那么所有依赖这个标签来控制显隐的UI元素都会按移动端规则走 - 切换模拟平台——如果 PretendPlatform 变了,调用引擎 API 切换
- 自动选设备配置——如果没手动选设备配置文件,帮你挑一个合理的
4.4 切换平台的底层实现
void ULyraPlatformEmulationSettings::ChangeActivePretendPlatform(FName NewPlatformName)
{
LastAppliedPretendPlatform = NewPlatformName;
PretendPlatform = NewPlatformName;
UPlatformSettingsManager::SetEditorSimulatedPlatform(PretendPlatform);
}
UPlatformSettingsManager::SetEditorSimulatedPlatform() 是引擎提供的核心API。调用它之后,所有查询"当前是什么平台"的代码都会返回你模拟的那个平台。这包括:
- 平台特定的
.ini配置加载 - 平台特定的材质质量等级
- 平台特定的 Scalability 设置
LastAppliedPretendPlatform 的作用是去重——避免每次 ApplySettings 都重新设置同一个平台。
4.5 智能设备配置文件选择算法
void ULyraPlatformEmulationSettings::PickReasonableBaseDeviceProfile()
{
UDeviceProfileManager& Manager = UDeviceProfileManager::Get();
// 第一步:检查当前选的是否兼容
if (UDeviceProfile* ProfilePtr = Manager.FindProfile(
PretendBaseDeviceProfile.ToString(), false))
{
const bool bIsCompatible =
(PretendPlatform == NAME_None) ||
(ProfilePtr->DeviceType == PretendPlatform.ToString());
if (!bIsCompatible)
{
PretendBaseDeviceProfile = NAME_None;
}
}
// 第二步:没有兼容配置?自动选一个
if ((PretendPlatform != NAME_None) && (PretendBaseDeviceProfile == NAME_None))
{
FName ShortestMatchingProfileName;
const FString PretendPlatformStr = PretendPlatform.ToString();
for (const TObjectPtr<UDeviceProfile>& Profile : Manager.Profiles)
{
if (Profile->DeviceType == PretendPlatformStr)
{
const FName TestName = Profile->GetFName();
if ((ShortestMatchingProfileName == NAME_None) ||
(TestName.GetStringLength() <
ShortestMatchingProfileName.GetStringLength()))
{
ShortestMatchingProfileName = TestName;
}
}
}
PretendBaseDeviceProfile = ShortestMatchingProfileName;
}
}
这个算法用了两个聪明但不过度设计的策略:
策略一:兼容性检查。 如果你选了 PretendPlatform = Android 但 PretendBaseDeviceProfile = iPhone13,那肯定不兼容,算法会清掉你的设备配置选择,进入第二步重新选。
策略二:名称最短启发式。 当需要自动选择时,它遍历所有 DeviceType 匹配的设备配置,选名称最短的那个。为什么选最短?因为设备配置文件通常有一个命名规律——iPhone13 是基础配置,iPhone13_HighQuality 是高质量变体,iPhone13_LowMemory 是低内存变体。名称最短的往往就是最基础、最通用的那个。
这个算法不是百分百精确,但它在99%的情况下能给出合理结果。而且它只是一个默认选择,你随时可以在面板里手动覆盖。
4.6 编辑器通知——三条提醒
OnPlayInEditorStarted() 会检查三个可能的状态并分别弹出提醒:
① Platform Trait Override: Enabling (Platform.Trait.Mobile)
② Platform Trait Override: Suppressing (Platform.Trait.SupportsMouseAndKeyboard)
③ Platform Override Active: Pretending to be Android
三条提醒各自3秒后消失。这个设计和 ULyraDeveloperSettings 里的 Experience 提醒异曲同工——防止你忘了自己在模拟环境下测试,看到奇怪的行为还以为出了Bug。
5. 三个类的联动——它们怎么配合
虽然三个类各自独立,但它们在 Lyra 的实际使用中是联动的。来看一个典型场景:
场景:你要测试"4个Bot + BossExperience + 假装是Switch"的效果
-
打开 Project Settings → Lyra:
ExperienceOverride→BossFight_ExperiencebOverrideBotCount→trueOverrideNumPlayerBotsToSpawn→4CheatsToRun→ 添加God、GiveAllWeapons
-
打开 Project Settings → Lyra(Platform Emulation):
PretendPlatform→SwitchbApplyFrameRateSettingsInPIE→true
-
点击 Play In Editor
此时发生的事情:
PIE启动
│
├── ULyraDeveloperSettings::OnPlayInEditorStarted()
│ └── 弹出Toast:"Experience Override: BossFight_Experience"
│
├── ULyraPlatformEmulationSettings::OnPlayInEditorStarted()
│ ├── 弹出Toast:"Platform Override: Pretending to be Switch"
│ └── 弹出Toast:"Platform Trait Override: Enabling (...)"
│
├── LyraBotCreationComponent 读取 DeveloperSettings
│ ├── bOverrideBotCount = true → 创建4个Bot
│ └── bAllowPlayerBotsToAttack = true → Bot可攻击
│
├── 体验加载 → 使用 BossFight_Experience 而非默认
│
├── 平台设置生效 → 引擎按Switch配置运行
│ ├── Switch的Scalability设置
│ ├── Switch的设备配置文件
│ └── 帧率限制(如果配置了)
│
└── 玩家Pawn生成 → CheatManager自动执行预设命令
├── God → 无敌
└── GiveAllWeapons → 满武器
等你反应过来的时候,已经在Switch模拟环境下的Boss战场里,带着4个Bot队友,无敌+全武器,直接开测。整个过程你只做了三件事——配置面板、点PIE按钮。
6. 设计上的几个巧思
绕了一圈,我们回头看看这个模块在代码架构上的几个值得学习的地方。
6.1 条件编译的克制使用
整个模块到处是条件编译:
#if WITH_SERVER_CODE && UE_WITH_CHEAT_MANAGER
#if WITH_EDITOR
#if WITH_EDITORONLY_DATA
但它的条件编译不是随便加、到处加的,而是有明确的分层:
WITH_SERVER_CODE && UE_WITH_CHEAT_MANAGER→ 只保护"运行时作弊命令"。因为Release客户端不应该有加Bot的能力WITH_EDITOR→ 保护编辑器专属功能。PostEditChangeProperty、ApplySettings这些WITH_EDITORONLY_DATA→ 保护纯编辑器数据。比如CommonEditorMaps数组
这形成了一个清晰的编译裁剪层次:
Release Client: 无作弊命令,无编辑器功能,无编辑器数据
Release Server: 有作弊命令,无编辑器功能,无编辑器数据
Editor: 全都有
6.2 设置类的"自动生效"模式
ULyraDeveloperSettings 和 ULyraPlatformEmulationSettings 都遵循同一个模式:
PostEditChangeProperty → ApplySettings()
PostReloadConfig → ApplySettings()
PostInitProperties → ApplySettings()
这个模式的好处是——不管设置从哪来、什么时机来,都走同一个入口。新增配置项只需要在 ApplySettings 里加逻辑,不需要去每个入口各写一遍。
6.3 "提醒而非阻止"的设计哲学
两处 OnPlayInEditorStarted 都只是弹Toast提醒,不阻止你启动PIE。这个设计选择背后有个理念:
工具应该辅助开发者,而不是替开发者做决定。
你开了Experience覆盖,工具提醒你一声但不拦截你。你知道自己在干什么。如果要改成"弹出确认对话框",表面上更安全,实际上会增加一个"每次都要多点一下"的摩擦——而这种小摩擦积累多了,开发者的心流就断了。
7. 可以改进的地方
没有完美的代码,Development 模块也有一些可以继续打磨的点。
7.1 ApplySettings 空壳
[LyraDeveloperSettings.cpp](file:///e:/UEProject/LyraStarterGame/Source/LyraGame/Development/LyraDeveloperSettings.cpp#L43-L45) 里的 ApplySettings() 目前是空的:
void ULyraDeveloperSettings::ApplySettings()
{
}
虽然很多配置通过 UDeveloperSettingsBackedByCVars 自动生效了,但这仍然是一个"看起来很像是被遗忘的TODO"。至少加个注释说明为什么不需要实现,或者把当前依赖CVar自动同步的配置项在注释里列一下。
7.2 RemovePlayerBot 的随机移除策略
RemovePlayerBot 会随机移除一个Bot,但你没办法控制移除哪一个。如果要调试"特定队友挂了之后的情况",这就没法用了。可以考虑加一个 RemovePlayerBotByName(FString BotName) 的变体。
7.3 设备配置选择算法的可配置性
PickReasonableBaseDeviceProfile 的"名称最短启发式"在绝大多数情况下有效,但如果是自定义的设备配置命名不规范,就可能选错。可以考虑加一个 DefaultDeviceProfileForPlatform 的映射表,让项目组自己配置每个平台的默认设备配置。
7.4 缺少设置冲突检测
比如你同时设置了 bOverrideBotCount = false 又配置了 OverrideNumPlayerBotsToSpawn = 5——这两个设置之间有语义冲突但不会报错。加一个 PostEditChangeProperty 里的交叉验证会更好。
写在最后
写完这篇东西,我最大的感受是:Lyra 的 Development 模块虽然代码量小,但每行代码都在解决一个真实的开发效率痛点。它没有炫技式的设计模式堆叠,没有过度抽象,三个类各司其职、清清楚楚。
几个我觉得特别值得拿走的思路:
- CheatManager 扩展的自动注册模式——用一个
RegisterForOnCheatManagerCreated在构造函数里搞定,不需要在别的地方手动调初始化。你的项目里如果有类似的"需要在某个系统创建时自动注入"的需求,直接抄这个模式 - 设置类的"三入口一出口"模式——
PostEditChangeProperty/PostReloadConfig/PostInitProperties全部指向同一个ApplySettings(),干净利落 - Toast 提醒而不是弹窗拦截——工具应该轻量、不打断心流。每次PIE弹一个2秒消失的小提示,比一个需要手动点的对话框友好得多
- 平台特征标签系统是宝藏——如果你的项目还没用 GameplayTag 来驱动平台差异化逻辑,强烈建议看看这个模块怎么用的,思路非常值得借鉴
Development 模块不是什么高深莫测的东西,但它是一个极好的教材——如何用最少的代码,让团队每天的开发体验好上一大截。希望这篇分析能给你带来一些灵感。

340

被折叠的 条评论
为什么被折叠?



