在之前的逻辑中,Lyra通过在初始地图中放置了B_ExperienceList3D蓝图实例来创建了不同的游戏入口也就是B_TeleportToUserFacingExperience蓝图实例。该蓝图作为子游戏的入口,我们继续来看该蓝图的内部逻辑。
完整的蓝图事件图表如下:
该蓝图共有三条逻辑分支。
-
分支一
该分支在控件被初始化时,生命周期函数BeginPlay调用时执行。它按顺序执行了两条逻辑,第一条逻辑为:
1.获取该蓝图的子控件并转换为W_ExperienceTile类型
2.调用W_ExperienceTile的SetUserFacingExperiecne函数
并将UserFacingExperienceToLoad变量的值传递进去,该变量的值是在上一篇博客中通过AssetManager加载并传递的。SetUserFacingExperiecne函数内部实现如下:
主要内容就是设置在场景中显示的世界UI上的文本内容和图标。 -
分支二
该分支处理了触发器触发事件逻辑,主要逻辑如下:
1.当有Actor进入触发范围时,将该Actor转换为Pawn类型
同时获取控制器。
2.验证获取的Pawn是否有效,如果有效则进入条件判断节点。
3.判断当前获取的控制器是否是本地玩家控制器,如果是则将获取的Pawn转换为PlayerController作为参数传递给一个自定事件,从而触发分支三的逻辑。 -
分之三
1.验证UserFacingExperienceToLoad变量是否有效
2.调用PushContentToLayerForPlayer函数,该函数主要用来向指定玩家显示对应的UI界面,这里是
3.调用UserFacingExperienceToLoad的CreateHostingRequest函数创建一个用于主机会话的请求对象,该函数实现在LyraUserFacingExperienceDefinition.cpp文件中,具体逻辑如下:
//创建一个用于主机会话的请求对象 UCommonSession_HostSessionRequest,
//并对该请求对象的各种属性进行初始化设置
UCommonSession_HostSessionRequest* ULyraUserFacingExperienceDefinition::CreateHostingRequest(const UObject* WorldContextObject) const
{
const FString ExperienceName = ExperienceID.PrimaryAssetName.ToString();//将 ExperienceID 的主要资产名称转换为字符串
const FString UserFacingExperienceName = GetPrimaryAssetId().PrimaryAssetName.ToString();//将当前对象的主要资产 ID 的主要资产名称转换为字符串。
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull);//获取世界对象,如果获取失败则返回 nullptr
UGameInstance* GameInstance = World ? World->GetGameInstance() : nullptr;//获取游戏实例
UCommonSession_HostSessionRequest* Result = nullptr;//存储最终的主机会话请求对象指针
if (UCommonSessionSubsystem* Subsystem = GameInstance ? GameInstance->GetSubsystem<UCommonSessionSubsystem>() : nullptr)
{
Result = Subsystem->CreateOnlineHostSessionRequest();//通过子系统创建主机会话请求
}
//如果子系统创建失败,则手动创建请求对象
if (!Result)
{
Result = NewObject<UCommonSession_HostSessionRequest>();//手动创建一个 UCommonSession_HostSessionRequest 对象
Result->OnlineMode = ECommonSessionOnlineMode::Online;//设置会话的在线模
Result->bUseLobbies = true;//设置是否使用大厅
Result->bUseLobbiesVoiceChat = false;//是否使用大厅语音聊天
//设置是否启用会话的存在状态,根据是否运行专用服务器来决定,非专用服务器则启用
Result->bUsePresence = !IsRunningDedicatedServer();
}
Result->MapID = MapID;//设置地图 ID
Result->ModeNameForAdvertisement = UserFacingExperienceName;//设置用于广告宣传的模式名称
Result->ExtraArgs = ExtraArgs;//设置额外参数
Result->ExtraArgs.Add(TEXT("Experience"), ExperienceName);//添加一个键值对 "Experience" 和 ExperienceName
Result->MaxPlayerCount = MaxPlayerCount;//设置最大玩家数量
//根据平台支持情况添加录像参数
if (ULyraReplaySubsystem::DoesPlatformSupportReplays())
{
if (bRecordReplay)
{
Result->ExtraArgs.Add(TEXT("DemoRec"), FString());//添加一个键值对 "DemoRec" 和空字符串,表示要记录录像
}
}
return Result;
}
4.设置在线模式为Offline,然后调用UCommonSessionSubsystem的HostSession函数。
以上就是全部蓝图逻辑。剩下的就是代码成面的逻辑了。
UCommonSessionSubsystem是一个用于管理多人游戏会话的核心子系统,负责处理会话创建、加入、参数传递以及与在线服务(如Steam、EOS等)的交互。
UCommonSessionSubsystem的HostSession函数的具体实现如下:
void UCommonSessionSubsystem::HostSession(APlayerController* HostingPlayer, UCommonSession_HostSessionRequest* Request)
{
if (Request == nullptr)
{
SetCreateSessionError(NSLOCTEXT("NetworkErrors", "InvalidRequest", "HostSession passed an invalid request."));
OnCreateSessionComplete(NAME_None, false);
return;
}
ULocalPlayer* LocalPlayer = (HostingPlayer != nullptr) ? HostingPlayer->GetLocalPlayer() : nullptr;
if (LocalPlayer == nullptr && !bIsDedicatedServer)
{
SetCreateSessionError(NSLOCTEXT("NetworkErrors", "InvalidHostingPlayer", "HostingPlayer is invalid."));
OnCreateSessionComplete(NAME_None, false);
return;
}
FText OutError;
if (!Request->ValidateAndLogErrors(OutError))
{
SetCreateSessionError(OutError);
OnCreateSessionComplete(NAME_None, false);
return;
}
if (Request->OnlineMode == ECommonSessionOnlineMode::Offline)
{
if (GetWorld()->GetNetMode() == NM_Client)
{
SetCreateSessionError(NSLOCTEXT("NetworkErrors", "CannotHostAsClient", "Cannot host offline game as client."));
OnCreateSessionComplete(NAME_None, false);
return;
}
else
{
// Offline so travel to the specified match URL immediately
GetWorld()->ServerTravel(Request->ConstructTravelURL());
}
}
else
{
CreateOnlineSessionInternal(LocalPlayer, Request);
}
NotifySessionInformationUpdated(ECommonSessionInformationState::InGame, Request->ModeNameForAdvertisement, Request->GetMapName());
}
由于我们之前参数传递中使用的是离线模式,所以他会执行到这个分支下的逻辑:
调用UWorld的ServerTravel函数在服务器端触发地图跳转。
bool UWorld::ServerTravel(const FString& FURL, bool bAbsolute, bool bShouldSkipGameNotify)
{
AGameModeBase* GameMode = GetAuthGameMode();
if (GameMode != nullptr && !GameMode->CanServerTravel(FURL, bAbsolute))
{
return false;
}
//设置跳转类型
NextTravelType = bAbsolute ? TRAVEL_Absolute : TRAVEL_Relative;
// if we're not already in a level change, start one now
// If the bShouldSkipGameNotify is there, then don't worry about seamless travel recursion
// and accept that we really want to travel
if (NextURL.IsEmpty() && (!IsInSeamlessTravel() || bShouldSkipGameNotify))
{
NextURL = FURL;//设置目标URL
if (GameMode != NULL)
{
// Skip notifying clients if requested
if (!bShouldSkipGameNotify)
{
GameMode->ProcessServerTravel(FURL, bAbsolute);//通知客户端
}
}
else
{
NextSwitchCountdown = 0;//无GameMode 触发硬切换
}
}
return true;
}
继续进入ProcessServerTravel函数查看:
void AGameModeBase::ProcessServerTravel(const FString& URL, bool bAbsolute)
{
#if WITH_SERVER_CODE
StartToLeaveMap();//触发地图离开前的逻辑
UE_LOG(LogGameMode, Log, TEXT("ProcessServerTravel: %s"), *URL);
//环境初始化
UWorld* World = GetWorld();
check(World);
FWorldContext& WorldContext = GEngine->GetWorldContextFromWorldChecked(World);
//无缝切换条件判断
// Use game mode setting but default to full load screen if the server has been up for a long time so that TimeSeconds doesn't overflow and break everything
bool bSeamless = (bUseSeamlessTravel && GetWorld()->TimeSeconds < 172800.0f); // 172800 seconds == 48 hours
// Compute the next URL, and pull the map out of it. This handles short->long package name conversion
FURL NextURL = FURL(&WorldContext.LastURL, *URL, bAbsolute ? TRAVEL_Absolute : TRAVEL_Relative);
// Override based on URL parameters
//支持通过URL参数?SeamlessTravel或?NoSeamlessTravel动态覆盖默认设置
if (NextURL.HasOption(TEXT("SeamlessTravel")))
{
bSeamless = true;
}
else if (NextURL.HasOption(TEXT("NoSeamlessTravel")))
{
bSeamless = false;
}
// There are some issues with seamless travel in PIE, so fall back to hard travel unless it is supported
//PIE环境特殊处理
if (World->WorldType == EWorldType::PIE && bSeamless && !FParse::Param(FCommandLine::Get(), TEXT("MultiprocessOSS")))
{
if (!UE::GameModeBase::Private::bAllowPIESeamlessTravel)
{
UE_LOG(LogGameMode, Warning, TEXT("ProcessServerTravel: Seamless travel is disabled in PIE, set net.AllowPIESeamlessTravel=1 to enable."));
bSeamless = false;
}
}
//通知客户端
// Notify clients we're switching level and give them time to receive.
FString URLMod = NextURL.ToString();
APlayerController* LocalPlayer = ProcessClientTravel(URLMod, bSeamless, bAbsolute);
World->NextURL = URLMod;//设置下一地图的URL路径
ENetMode NetMode = GetNetMode();
//执行切换逻辑
if (bSeamless)
{
World->SeamlessTravel(World->NextURL, bAbsolute);
World->NextURL = TEXT("");
}
else
{
// Switch immediately if not networking.
if (NetMode != NM_DedicatedServer && NetMode != NM_ListenServer)
{
World->NextSwitchCountdown = 0.0f;
}
GEngine->IncrementGlobalNetTravelCount();
GEngine->SaveConfig();
}
#endif // WITH_SERVER_CODE
}
以上基本上就是进入子游戏的全部逻辑了,具体如何通知到客户端,感兴趣的朋友可以继续依次点进对应的函数去看,不在此处啰嗦了。