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 experience
if (!ExperienceId.IsValid())
{
if (TryDedicatedServerLogin())
{
// This will start to host as a dedicated server
return;
}
//@TODO: Pull this from a config setting or something
ExperienceId = 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_SERVER
ULyraSettingsLocal::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 widget
TSubclassOf<UUserWidget> LoadingScreenWidgetClass = Settings->LoadingScreenWidget.TryLoadClass<UUserWidget>();
if (UUserWidget* UserWidget = UUserWidget::CreateWidgetInstance(*LocalGameInstance, LoadingScreenWidgetClass, NAME_None))
{
LoadingScreenWidget = UserWidget->TakeWidget();
}
...
}
所以它是去读取了UCommonLoadingScreenSettings里的设置。
这个设置在UE编辑器下的:编辑>项目设置>游戏/CommonLoadingScreen下
至此,整个游戏大厅加载环节流程完成。