LyraStarterGame_5.6 Experience系统加载流程详细实现

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系统加载流程具有以下特点:

  1. 异步加载模式:确保游戏在加载过程中保持响应性
  2. 状态驱动:通过明确的状态转换管理加载过程
  3. 模块化设计:将资源加载、插件激活和动作执行分离
  4. 网络同步:确保客户端和服务器使用相同的经验配置
  5. 优先级事件系统:支持不同优先级的加载完成回调
  6. 可测试性:包含混沌测试延迟功能
  7. 可扩展性:支持通过动作集扩展经验功能

这种设计确保了Experience系统的灵活性和可维护性,同时提供了良好的用户体验。

相关推荐
添砖java‘’2 小时前
常见的进程间通信方式详解
linux·c++·操作系统·信息与通信·进程通信
浦东新村轱天乐2 小时前
2025.12.08-2025.12.14:课题分离,不要在意外在评价。
笔记·职场发展
秋深枫叶红2 小时前
嵌入式第三十四篇——linux系统编程——进程
linux·服务器·数据库·学习
一韦以航.2 小时前
C【指针】详解(上)
c语言·数据结构·c++·算法
martian6653 小时前
深入解析C++驱动开发实战:优化高效稳定的驱动应用
开发语言·c++·驱动开发
d111111111d3 小时前
STM32得中断服务函数,为什么不能有返回值
笔记·stm32·单片机·嵌入式硬件·学习
阿蒙Amon3 小时前
JavaScript学习笔记:12.类
javascript·笔记·学习
光影少年3 小时前
PostgreSQL数据库学习路线
数据库·学习·postgresql
FMRbpm3 小时前
用队列实现栈
数据结构·c++·新手入门