1. 加载流程概述
Lyra的Experience系统采用异步加载模式,确保游戏在加载过程中保持响应性。完整的加载流程包含以下状态转换:
Unloaded → Loading → LoadingGameFeatures → ExecutingActions → Loaded
2. 详细流程实现
2.1 设置当前经验
cpp
void ULyraExperienceManagerComponent::SetCurrentExperience(FPrimaryAssetId ExperienceId)
{
ULyraAssetManager& AssetManager = ULyraAssetManager::Get();
FSoftObjectPath AssetPath = AssetManager.GetPrimaryAssetPath(ExperienceId);
TSubclassOf<ULyraExperienceDefinition> AssetClass = Cast<UClass>(AssetPath.TryLoad());
check(AssetClass);
const ULyraExperienceDefinition* Experience = GetDefault<ULyraExperienceDefinition>(AssetClass);
check(Experience != nullptr);
check(CurrentExperience == nullptr);
CurrentExperience = Experience;
StartExperienceLoad();
}
关键实现点:
- 通过
ULyraAssetManager获取经验资源路径 - 尝试加载资源类并转换为
ULyraExperienceDefinition类型 - 验证经验有效性并设置为当前经验
- 调用
StartExperienceLoad()开始加载流程
2.2 开始经验加载
cpp
void ULyraExperienceManagerComponent::StartExperienceLoad()
{
check(CurrentExperience != nullptr);
check(LoadState == ELyraExperienceLoadState::Unloaded);
UE_LOG(LogLyraExperience, Log, TEXT("EXPERIENCE: StartExperienceLoad(CurrentExperience = %s, %s)"),
*CurrentExperience->GetPrimaryAssetId().ToString(),
*GetClientServerContextString(this));
LoadState = ELyraExperienceLoadState::Loading;
ULyraAssetManager& AssetManager = ULyraAssetManager::Get();
TSet<FPrimaryAssetId> BundleAssetList;
TSet<FSoftObjectPath> RawAssetList;
BundleAssetList.Add(CurrentExperience->GetPrimaryAssetId());
for (const TObjectPtr<ULyraExperienceActionSet>& ActionSet : CurrentExperience->ActionSets)
{
if (ActionSet != nullptr)
{
BundleAssetList.Add(ActionSet->GetPrimaryAssetId());
}
}
TArray<FName> BundlesToLoad;
BundlesToLoad.Add(FLyraBundles::Equipped);
const ENetMode OwnerNetMode = GetOwner()->GetNetMode();
const bool bLoadClient = GIsEditor || (OwnerNetMode != NM_DedicatedServer);
const bool bLoadServer = GIsEditor || (OwnerNetMode != NM_Client);
if (bLoadClient) BundlesToLoad.Add(UGameFeaturesSubsystemSettings::LoadStateClient);
if (bLoadServer) BundlesToLoad.Add(UGameFeaturesSubsystemSettings::LoadStateServer);
TSharedPtr<FStreamableHandle> BundleLoadHandle = nullptr;
if (BundleAssetList.Num() > 0)
{
BundleLoadHandle = AssetManager.ChangeBundleStateForPrimaryAssets(BundleAssetList.Array(), BundlesToLoad, {}, false, FStreamableDelegate(), FStreamableManager::AsyncLoadHighPriority);
}
TSharedPtr<FStreamableHandle> RawLoadHandle = nullptr;
if (RawAssetList.Num() > 0)
{
RawLoadHandle = AssetManager.LoadAssetList(RawAssetList.Array(), FStreamableDelegate(), FStreamableManager::AsyncLoadHighPriority, TEXT("StartExperienceLoad()"));
}
TSharedPtr<FStreamableHandle> Handle = nullptr;
if (BundleLoadHandle.IsValid() && RawLoadHandle.IsValid())
{
Handle = AssetManager.GetStreamableManager().CreateCombinedHandle({ BundleLoadHandle, RawLoadHandle });
}
else
{
Handle = BundleLoadHandle.IsValid() ? BundleLoadHandle : RawLoadHandle;
}
FStreamableDelegate OnAssetsLoadedDelegate = FStreamableDelegate::CreateUObject(this, &ThisClass::OnExperienceLoadComplete);
if (!Handle.IsValid() || Handle->HasLoadCompleted())
{
FStreamableHandle::ExecuteDelegate(OnAssetsLoadedDelegate);
}
else
{
Handle->BindCompleteDelegate(OnAssetsLoadedDelegate);
Handle->BindCancelDelegate(FStreamableDelegate::CreateLambda([OnAssetsLoadedDelegate]()
{
OnAssetsLoadedDelegate.ExecuteIfBound();
}));
}
TSet<FPrimaryAssetId> PreloadAssetList;
if (PreloadAssetList.Num() > 0)
{
AssetManager.ChangeBundleStateForPrimaryAssets(PreloadAssetList.Array(), BundlesToLoad, {});
}
}
关键实现点:
- 验证当前经验有效性和加载状态
- 记录加载日志,包含经验ID和客户端/服务器上下文
- 转换状态为
ELyraExperienceLoadState::Loading - 构建要加载的资源列表,包括经验定义和相关动作集
- 根据网络模式(客户端/服务器/编辑器)确定要加载的资源包
- 使用
FStreamableHandle进行异步资源加载 - 支持合并多个加载请求,提高效率
- 绑定资源加载完成回调
- 支持预加载额外资源(当前留空)
2.3 经验资源加载完成
cpp
void ULyraExperienceManagerComponent::OnExperienceLoadComplete()
{
check(LoadState == ELyraExperienceLoadState::Loading);
check(CurrentExperience != nullptr);
UE_LOG(LogLyraExperience, Log, TEXT("EXPERIENCE: OnExperienceLoadComplete(CurrentExperience = %s, %s)"),
*CurrentExperience->GetPrimaryAssetId().ToString(),
*GetClientServerContextString(this));
GameFeaturePluginURLs.Reset();
auto CollectGameFeaturePluginURLs = [This=this](const UPrimaryDataAsset* Context, const TArray<FString>& FeaturePluginList)
{
for (const FString& PluginName : FeaturePluginList)
{
FString PluginURL;
if (UGameFeaturesSubsystem::Get().GetPluginURLByName(PluginName, /*out*/ PluginURL))
{
This->GameFeaturePluginURLs.AddUnique(PluginURL);
}
else
{
ensureMsgf(false, TEXT("OnExperienceLoadComplete failed to find plugin URL from PluginName %s for experience %s - fix data, ignoring for this run"), *PluginName, *Context->GetPrimaryAssetId().ToString());
}
}
};
CollectGameFeaturePluginURLs(CurrentExperience, CurrentExperience->GameFeaturesToEnable);
for (const TObjectPtr<ULyraExperienceActionSet>& ActionSet : CurrentExperience->ActionSets)
{
if (ActionSet != nullptr)
{
CollectGameFeaturePluginURLs(ActionSet, ActionSet->GameFeaturesToEnable);
}
}
NumGameFeaturePluginsLoading = GameFeaturePluginURLs.Num();
if (NumGameFeaturePluginsLoading > 0)
{
LoadState = ELyraExperienceLoadState::LoadingGameFeatures;
for (const FString& PluginURL : GameFeaturePluginURLs)
{
ULyraExperienceManager::NotifyOfPluginActivation(PluginURL);
UGameFeaturesSubsystem::Get().LoadAndActivateGameFeaturePlugin(PluginURL, FGameFeaturePluginLoadComplete::CreateUObject(this, &ThisClass::OnGameFeaturePluginLoadComplete));
}
}
else
{
OnExperienceFullLoadCompleted();
}
}
关键实现点:
- 验证加载状态和当前经验有效性
- 记录资源加载完成日志
- 收集游戏特性插件URL的Lambda函数
- 从经验定义和动作集中收集需要加载的游戏特性插件
- 转换状态为
ELyraExperienceLoadState::LoadingGameFeatures - 异步加载并激活游戏特性插件
- 支持无插件情况下直接完成加载
2.4 游戏特性插件加载完成
cpp
void ULyraExperienceManagerComponent::OnGameFeaturePluginLoadComplete(const UE::GameFeatures::FResult& Result)
{
NumGameFeaturePluginsLoading--;
if (NumGameFeaturePluginsLoading == 0)
{
OnExperienceFullLoadCompleted();
}
}
关键实现点:
- 递减正在加载的插件计数
- 当所有插件加载完成后,调用最终完成函数
2.5 经验完全加载完成
cpp
void ULyraExperienceManagerComponent::OnExperienceFullLoadCompleted()
{
check(LoadState != ELyraExperienceLoadState::Loaded);
if (LoadState != ELyraExperienceLoadState::LoadingChaosTestingDelay)
{
const float DelaySecs = LyraConsoleVariables::GetExperienceLoadDelayDuration();
if (DelaySecs > 0.0f)
{
FTimerHandle DummyHandle;
LoadState = ELyraExperienceLoadState::LoadingChaosTestingDelay;
GetWorld()->GetTimerManager().SetTimer(DummyHandle, this, &ThisClass::OnExperienceFullLoadCompleted, DelaySecs, /*bLooping=*/ false);
return;
}
}
LoadState = ELyraExperienceLoadState::ExecutingActions;
FGameFeatureActivatingContext Context;
const FWorldContext* ExistingWorldContext = GEngine->GetWorldContextFromWorld(GetWorld());
if (ExistingWorldContext)
{
Context.SetRequiredWorldContextHandle(ExistingWorldContext->ContextHandle);
}
auto ActivateListOfActions = [&Context](const TArray<UGameFeatureAction*>& ActionList)
{
for (UGameFeatureAction* Action : ActionList)
{
if (Action != nullptr)
{
Action->OnGameFeatureRegistering();
Action->OnGameFeatureLoading();
Action->OnGameFeatureActivating(Context);
}
}
};
ActivateListOfActions(CurrentExperience->Actions);
for (const TObjectPtr<ULyraExperienceActionSet>& ActionSet : CurrentExperience->ActionSets)
{
if (ActionSet != nullptr)
{
ActivateListOfActions(ActionSet->Actions);
}
}
LoadState = ELyraExperienceLoadState::Loaded;
OnExperienceLoaded_HighPriority.Broadcast(CurrentExperience);
OnExperienceLoaded_HighPriority.Clear();
OnExperienceLoaded.Broadcast(CurrentExperience);
OnExperienceLoaded.Clear();
OnExperienceLoaded_LowPriority.Broadcast(CurrentExperience);
OnExperienceLoaded_LowPriority.Clear();
#if !UE_SERVER
ULyraSettingsLocal::Get()->OnExperienceLoaded();
#endif
}
关键实现点:
- 验证非已加载状态
- 支持混沌测试延迟(用于模拟加载延迟)
- 转换状态为
ELyraExperienceLoadState::ExecutingActions - 创建游戏特性激活上下文,包含世界上下文信息
- 激活动作列表的Lambda函数
- 执行经验定义和动作集中的所有动作(注册→加载→激活)
- 转换状态为
ELyraExperienceLoadState::Loaded - 按优先级广播加载完成事件:
- 高优先级(核心功能)
- 普通优先级
- 低优先级
- 在客户端调用本地设置的经验加载完成回调
3. 网络同步机制
Lyra的Experience系统支持网络同步,确保客户端和服务器使用相同的经验配置:
cpp
void ULyraExperienceManagerComponent::OnRep_CurrentExperience()
{
StartExperienceLoad();
}
void ULyraExperienceManagerComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ThisClass, CurrentExperience);
}
关键实现点:
- 当前经验通过
DOREPLIFETIME宏自动复制到客户端 - 客户端在收到复制的经验后,自动调用
StartExperienceLoad()开始加载 - 加载流程在客户端和服务器上独立执行,但最终结果一致
4. 加载状态管理
Experience系统使用ELyraExperienceLoadState枚举管理加载状态:
cpp
enum class ELyraExperienceLoadState
{
Unloaded, // 未加载状态
Loading, // 加载经验资源
LoadingGameFeatures, // 加载游戏特性插件
LoadingChaosTestingDelay, // 混沌测试延迟
ExecutingActions, // 执行经验动作
Loaded, // 完全加载完成
Deactivating // 停用经验
};
5. 加载流程的取消和清理
系统支持加载过程中的取消处理:
cpp
Handle->BindCancelDelegate(FStreamableDelegate::CreateLambda([OnAssetsLoadedDelegate]()
{
OnAssetsLoadedDelegate.ExecuteIfBound();
}));
在组件结束时,会尝试停用已激活的游戏特性插件:
cpp
void ULyraExperienceManagerComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
for (const FString& PluginURL : GameFeaturePluginURLs)
{
if (ULyraExperienceManager::RequestToDeactivatePlugin(PluginURL))
{
UGameFeaturesSubsystem::Get().DeactivateGameFeaturePlugin(PluginURL);
}
}
// 停用和清理动作
// ...
}
6. 加载状态查询
cpp
bool ULyraExperienceManagerComponent::IsExperienceLoaded() const
{
return (LoadState == ELyraExperienceLoadState::Loaded) && (CurrentExperience != nullptr);
}
const ULyraExperienceDefinition* ULyraExperienceManagerComponent::GetCurrentExperienceChecked() const
{
check(LoadState == ELyraExperienceLoadState::Loaded);
check(CurrentExperience != nullptr);
return CurrentExperience;
}
7. 总结
Lyra的Experience系统加载流程具有以下特点:
- 异步加载模式:确保游戏在加载过程中保持响应性
- 状态驱动:通过明确的状态转换管理加载过程
- 模块化设计:将资源加载、插件激活和动作执行分离
- 网络同步:确保客户端和服务器使用相同的经验配置
- 优先级事件系统:支持不同优先级的加载完成回调
- 可测试性:包含混沌测试延迟功能
- 可扩展性:支持通过动作集扩展经验功能
这种设计确保了Experience系统的灵活性和可维护性,同时提供了良好的用户体验。