UE5.2 LyraDemo源码阅读笔记(二)
创建了关卡中的体验玩家Actor和7个体验玩法入口之后。
接下来操作关卡中的玩家与玩法入口交互,进入玩法入口,选择进入B_LyraFrontEnd_Experience玩法入口,也就是第3个入口。触发以下请求方法,切换到关卡L_LyraFrontEnd:
(为啥选择L_LyraFrontEnd入口?因为这个流程比前两个独立的直接关卡玩法流程更完善)
可以看到GetWorld()->ServerTravel(url)里url的值为:/Game/System/FrontEnd/Maps/L_LyraFrontEnd?Experience=B_LyraFrontEnd_Experience。
其中,L_LyraFrontEnd是要进入的关卡,?Experience=B_LyraFrontEnd_Experience则是自定义数据,明显这是一个蓝图类和蓝图实例名称。
这个自定义参数将会传到C++类LyraGameMode的OptionsString字段,然后在进入新关卡L_LyraFrontEnd时,会重新执行LyraGameMode::InitGame方法,后面再在ALyraGameMode::HandleMatchAssignmentIfNotExpectingOne()里加载对于的蓝图类,即B_LyraFrontEnd_Experience:
在这个方法里,在各种场景方式下,决定了进入新关卡时创建哪个蓝图类,包括在编辑器启动游戏时的默认蓝图角色B_LyraDefaultExperience。
ALyraGameMode.cpp:
void ALyraGameMode::HandleMatchAssignmentIfNotExpectingOne()
{...if (!ExperienceId.IsValid() && UGameplayStatics::HasOption(OptionsString, TEXT("Experience"))){const FString ExperienceFromOptions = UGameplayStatics::ParseOption(OptionsString, TEXT("Experience"));ExperienceId = FPrimaryAssetId(FPrimaryAssetType(ULyraExperienceDefinition::StaticClass()->GetFName()), FName(*ExperienceFromOptions));ExperienceIdSource = TEXT("OptionsString");}...// Final fallback to the default experienceif (!ExperienceId.IsValid()){if (TryDedicatedServerLogin()){// This will start to host as a dedicated serverreturn;}//@TODO: Pull this from a config setting or somethingExperienceId = FPrimaryAssetId(FPrimaryAssetType("LyraExperienceDefinition"), FName("B_LyraDefaultExperience"));ExperienceIdSource = TEXT("Default");}OnMatchAssignmentGiven(ExperienceId, ExperienceIdSource);
}
然后在下面的OnMatchAssignmentGiven(ExperienceId, ExperienceIdSource)方法里,获取GameState里的ExperienceComponent组件来设置其Experience。
void ALyraGameMode::OnMatchAssignmentGiven(FPrimaryAssetId ExperienceId, const FString& ExperienceIdSource)
{if (ExperienceId.IsValid()){UE_LOG(LogLyraExperience, Log, TEXT("Identified experience %s (Source: %s)"), *ExperienceId.ToString(), *ExperienceIdSource);ULyraExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass<ULyraExperienceManagerComponent>();check(ExperienceComponent);ExperienceComponent->SetCurrentExperience(ExperienceId);}...
}
为什么要设置到GameState里的ExperienceComponent组件呢?因为GameMode是控制整个关卡里的游戏流程,GameState则处理游戏状态,而游戏流程的数据则保存在Component里,而且,各个客户端的游戏GameState数据同步是通过Component的属性来同步(Replicated)的。可以打开ULyraExperienceManagerComponent.h查看CurrentExperience定义,带了ReplicatedUsing标签,所以它是会同步到其它客户端的。
ULyraExperienceManagerComponent.h:
class ULyraExperienceManagerComponent final : public UGameStateComponent, public ILoadingProcessInterface...
private:UPROPERTY(ReplicatedUsing=OnRep_CurrentExperience)TObjectPtr<const ULyraExperienceDefinition> CurrentExperience;...
根据ReplicatedUsing标签,当CurrentExperience被赋值时,则会触发OnRep_CurrentExperience()方法,从而开始加载资源。
Experience数据蓝图加载完成后,开始加载其中的Actions插件:
ULyraExperienceManagerComponent.cpp:
void ULyraExperienceManagerComponent::OnExperienceLoadComplete()
{...// Load and activate the features 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));}}...
}
等Experience插件加载完成后,开始激活:
ULyraExperienceManagerComponent.cpp
void ULyraExperienceManagerComponent::OnExperienceFullLoadCompleted()
{...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();// Apply any necessary scalability settings
#if !UE_SERVERULyraSettingsLocal::Get()->OnExperienceLoaded();
#endif
}
Component里插件加载完成激活后,回调到ALyraGameMode::OnExperienceLoaded()方法,如果数据资产里Default Pawn Data里设置有LyraPawnData,那么将会根据此Data在关卡中生成对应的Actor。明显,B_LyraFrontEnd_Experience只是一个入口UI。没有设置。
ALyraGameMode.cpp:
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);}}}
}
与此同时,B_LyraFrontEnd_Experience数据蓝图里定义的Actions被加载完成激活时,这个Action同时被触发。
这里主要关注AddComponents这个Action,这里往LyraGameState这个类里添加了2个状态组件。这个Action会找到GameMode里的GameState,如果没有则新实例化一个,然后添加组件。
在添加的B_LyraFrontendStateComponent里,主要显示了LoadingUI界面和游戏入口菜单。看组件代码ULyraFrontendStateComponent.cpp:
void ULyraFrontendStateComponent::OnExperienceLoaded(const ULyraExperienceDefinition* Experience)
{FControlFlow& Flow = FControlFlowStatics::Create(this, TEXT("FrontendFlow")).QueueStep(TEXT("Wait For User Initialization"), this, &ThisClass::FlowStep_WaitForUserInitialization).QueueStep(TEXT("Try Show Press Start Screen"), this, &ThisClass::FlowStep_TryShowPressStartScreen).QueueStep(TEXT("Try Join Requested Session"), this, &ThisClass::FlowStep_TryJoinRequestedSession).QueueStep(TEXT("Try Show Main Screen"), this, &ThisClass::FlowStep_TryShowMainScreen);Flow.ExecuteFlow();FrontEndFlow = Flow.AsShared();
}
组件里监听玩法数据资产加载完成后,开始根据玩家状态显示对应的UI,这里对游戏入口菜单进行显示:
void ULyraFrontendStateComponent::FlowStep_TryShowMainScreen(FControlFlowNodeRef SubFlow)
{if (UPrimaryGameLayout* RootLayout = UPrimaryGameLayout::GetPrimaryGameLayoutForPrimaryPlayer(this)){constexpr bool bSuspendInputUntilComplete = true;RootLayout->PushWidgetToLayerStackAsync<UCommonActivatableWidget>(FrontendTags::TAG_UI_LAYER_MENU, bSuspendInputUntilComplete, MainScreenClass,[this, SubFlow](EAsyncWidgetLayerState State, UCommonActivatableWidget* Screen) {switch (State){case EAsyncWidgetLayerState::AfterPush:bShouldShowLoadingScreen = false;SubFlow->ContinueFlow();return;case EAsyncWidgetLayerState::Canceled:bShouldShowLoadingScreen = false;SubFlow->ContinueFlow();return;}});}
}
这里显示游戏菜单。
至此:
但最后还有一个:
在这些之前还有一个LoadingUI和大厅背景加载,他们是在什么时候进行加载的呢?
来看到关卡L_LyraFrontEnd下的蓝图节点B_LoadRandomLobbyBackground,打开它的蓝图,这个蓝图功能比较容易看得出来了,这里对数据资产ShooterGameLobbyBG里的BackgroundLevel字段里的大厅关卡进行加载,并打开LoadingUI。
那么打开LoadingUI的Widget是哪里设置的?打开显示UI的蓝图节点查看:
其静态方法定义,这里创建了一个ULoadingProcessTask实例并返回给蓝图持有引用,用于关闭LoadingUI:
ULoadingProcessTask.cpp
/*static*/ ULoadingProcessTask* ULoadingProcessTask::CreateLoadingScreenProcessTask(UObject* WorldContextObject, const FString& ShowLoadingScreenReason)
{UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);UGameInstance* GameInstance = World ? World->GetGameInstance() : nullptr;ULoadingScreenManager* LoadingScreenManager = GameInstance ? GameInstance->GetSubsystem<ULoadingScreenManager>() : nullptr;if (LoadingScreenManager){ULoadingProcessTask* NewLoadingTask = NewObject<ULoadingProcessTask>(LoadingScreenManager);NewLoadingTask->SetShowLoadingScreenReason(ShowLoadingScreenReason);LoadingScreenManager->RegisterLoadingProcessor(NewLoadingTask); return NewLoadingTask;}return nullptr;
}
而这里的LoadingUI引用的Widget则在ULoadingScreenManagerl类里,跟进去看
,里面定义了LoadingScreenWidget:
ULoadingScreenManager.h
...
class COMMONLOADINGSCREEN_API ULoadingScreenManager : public UGameInstanceSubsystem, public FTickableGameObject
{...
private:.../** A reference to the loading screen widget we are displaying (if any) */TSharedPtr<SWidget> LoadingScreenWidget;...
};
LoadingScreenWidget被定义了private,所以它是在内部.cpp赋值:
ULoadingScreenManager.h
void ULoadingScreenManager::ShowLoadingScreen()
{...const UCommonLoadingScreenSettings* Settings = GetDefault<UCommonLoadingScreenSettings>();...// Create the loading screen widgetTSubclassOf<UUserWidget> LoadingScreenWidgetClass = Settings->LoadingScreenWidget.TryLoadClass<UUserWidget>();if (UUserWidget* UserWidget = UUserWidget::CreateWidgetInstance(*LocalGameInstance, LoadingScreenWidgetClass, NAME_None)){LoadingScreenWidget = UserWidget->TakeWidget();}...
}
所以它是去读取了UCommonLoadingScreenSettings里的设置。
这个设置在UE编辑器下的:编辑>项目设置>游戏/CommonLoadingScreen下
至此,整个游戏大厅加载环节流程完成。