UE官方示例项目Lyra Game拆解(二)

在之前的逻辑中,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
}

以上基本上就是进入子游戏的全部逻辑了,具体如何通知到客户端,感兴趣的朋友可以继续依次点进对应的函数去看,不在此处啰嗦了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值