UE5.2 LyraDemo源码阅读笔记(二)

UE5.2 LyraDemo源码阅读笔记(二)

创建了关卡中的体验玩家Actor和7个体验玩法入口之后。
接下来操作关卡中的玩家与玩法入口交互,进入玩法入口,选择进入B_LyraFrontEnd_Experience玩法入口,也就是第3个入口。触发以下请求方法,切换到关卡L_LyraFrontEnd:
(为啥选择L_LyraFrontEnd入口?因为这个流程比前两个独立的直接关卡玩法流程更完善)
UE5.2 LyraDemo源码阅读笔记(二)_第1张图片
可以看到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,如果没有则新实例化一个,然后添加组件。
UE5.2 LyraDemo源码阅读笔记(二)_第2张图片

在添加的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;
			}
		});
	}
}

这里显示游戏菜单。
至此:
UE5.2 LyraDemo源码阅读笔记(二)_第3张图片
但最后还有一个:
在这些之前还有一个LoadingUI和大厅背景加载,他们是在什么时候进行加载的呢?
来看到关卡L_LyraFrontEnd下的蓝图节点B_LoadRandomLobbyBackground,打开它的蓝图,这个蓝图功能比较容易看得出来了,这里对数据资产ShooterGameLobbyBG里的BackgroundLevel字段里的大厅关卡进行加载,并打开LoadingUI。
那么打开LoadingUI的Widget是哪里设置的?打开显示UI的蓝图节点查看:
UE5.2 LyraDemo源码阅读笔记(二)_第4张图片
其静态方法定义,这里创建了一个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下
UE5.2 LyraDemo源码阅读笔记(二)_第5张图片
至此,整个游戏大厅加载环节流程完成。

你可能感兴趣的:(ue5,笔记,前端)