本文为B站系列教学视频 《UE5_C++多人TPS完整教程》 —— 《P25 完善菜单子系统(Polishing The Menu Subsystem)》 的学习笔记,该系列教学视频为 Udemy 课程 《Unreal Engine 5 C++ Multiplayer Shooter》 的中文字幕翻译版,UP主(也是译者)为 游戏引擎能吃么。
文章目录
- P25 完善菜单子系统
- 25.1 保证创建会话随时可用
- 25.2 添加退出游戏按钮
- 25.3 添加按钮禁用功能
- 25.4 Summary
P25 完善菜单子系统
本节课是该系列课程关于制作多人游戏插件部分的最后一节课,我们将继续完善菜单子系统,保证创建会话功能一直可用,为菜单添加一个退出游戏按钮,然后实现按钮被点击一次之后就禁用的功能,以防短时间多次重复创建或查找会话,并考虑在一些情况下重新启用按钮。
25.1 保证创建会话随时可用
-
我们在进行测试的时候可能会发现一个问题:首先在设备 1 创建会话。
在设备 2 加入会话,
此时在设备 1 上退出游戏,原先会话被销毁,设备 2 上的玩家被踢出,回到菜单界面。
设备 2 在回到菜单界面后 立刻 点击 “Host
” 按钮创建会话,可以发现屏幕左上角出现创建会话失败消息。但过了几秒钟后再次创建会话,可又以发现创建会话成功了。
-
这是因为在 “
MultiplayerSessionsSubsystem.cpp
” 的 “CreateSession()
” 中,我们在创建会话前检查了先前存在会话,如果存在我们将调用 “SessionInterface->DestroySession()
” 销毁先前的会话,但信息同步至原来创建会话的设备上需要时间,所以当执行到 “SessionInterface->CreateSession()
” 函数时,会话可能还没有被销毁,这种情况下在线接口无法创建会话。void UMultiplayerSessionsSubsystem::CreateSession(int32 NumpublicConnections, FString MatchType) { // 检查会话接口是否有效 if (!SessionInterface.IsValid()) { return; } // 检查是否先前存在会话 auto ExistingSession = SessionInterface->GetNamedSession(NAME_GameSession); if (ExistingSession != nullptr) { // 如果先前存在会话 SessionInterface->DestroySession(NAME_GameSession); // 销毁会话 } ... if (!SessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *LastSessionSettings)) { // 如果创建会话失败,将委托移出委托列表 SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle); // 广播会话创建失败消息到自定义的子系统委托 SubsystemOnCreateSessionCompleteDelegate.Broadcast(false); } }
解决这个问题的方法就是利用先前定义的有关销毁会话的子系统委托,实现与其绑定的回调函数,在保证会话销毁完成之后再去执行 “
SessionInterface->CreateSession()
” 函数。 -
在 “
MultiplayerSessionsSubsystem.h
” 中定义布尔变量 “bCreateSessionOnDestroy()
”,用来标识上次创建的会话是否需要被销毁,定义变量 “LastNumPublicConnections
” 和 “LastMatchType
” 分别保存上次会话公共连接数和匹配类型。UCLASS() class MULTIPLAYERSESSIONS_API UMultiplayerSessionsSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() ... private: ... /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ bool bCreateSessionOnDestroy{ false }; // 上次调用 CreateSession() 时先前会话是否存在且需要被销毁 int32 LastNumPublicConnections; // 上次会话的公共连接数 FString LastMatchType; // 上次会话的匹配类型 /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ };
-
在 “
MultiplayerSessionsSubsystem.cpp
” 中修改 “CreateSession()
” 函数检查先前会话是否存在的代码,如果检查到先前存在代码,设置 “bCreateSessionOnDestroy
” 为 “true
”,表示先前创建的会话(上次会话)需要被销毁,保存公共连接数和匹配类型,执行 “DestroySession()
” 函数销毁会话,最后可以加上 “return;
” 防止后面的会话设置和广播会话创建消息的代码被执行。void UMultiplayerSessionsSubsystem::CreateSession(int32 NumpublicConnections, FString MatchType) { // 检查会话接口是否有效 if (!SessionInterface.IsValid()) { return; } // 检查是否先前存在会话 auto ExistingSession = SessionInterface->GetNamedSession(NAME_GameSession); if (ExistingSession != nullptr) { // 如果先前存在会话 /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ bCreateSessionOnDestroy = true; // 本次调用 CreateSession() 需要销毁先前会话 LastNumPublicConnections = NumpublicConnections; // 保存上次会话公共连接数 LastMatchType = MatchType; // 保存上次会话匹配类型 DestroySession(); // 销毁会话 return; // 防止执行后面的代码(未提及) /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ } ... }
-
在 “
MultiplayerSessionsSubsystem.cpp
” 中实现 “DestroySession()
” 函数以及回调函数 “OnDestroySessionComplete()
”,在保证会话销毁完成之后再去执行 “SessionInterface->CreateSession()
” 函数。... void UMultiplayerSessionsSubsystem::DestroySession() { /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ // 检查会话接口是否有效 if (!SessionInterface.IsValid()) { // 如果会话接口无效 SubsystemOnDestroySessionCompleteDelegate.Broadcast(false); // 广播会话销毁失败消息到自定义的子系统委托 return; } // 保存委托句柄,以便此后移出委托列表 DestroySessionCompleteDelegateHandle = SessionInterface->AddOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegate); // 添加委托到会话接口的委托列表 // 销毁会话 if (!SessionInterface->DestroySession(NAME_GameSession)) { // 如果销毁会话失败,将委托移出委托列表 SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegateHandle); // 广播会话销毁失败消息到自定义的子系统委托 SubsystemOnDestroySessionCompleteDelegate.Broadcast(false); } /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ } ... void UMultiplayerSessionsSubsystem::OnDestroySessionComplete(FName SessionName, bool bWasSuccessful) { /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ // 如果销毁会话成功,将委托移出委托列表 if (SessionInterface) { SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegateHandle); } // 如果上次调用 CreateSession() 时先前的会话需要被销毁且该会话已经销毁成功 if (bCreateSessionOnDestroy && bWasSuccessful) { bCreateSessionOnDestroy = false; // 恢复初始值 CreateSession(LastNumPublicConnections, LastMatchType); // 创建新会话 } // 广播销毁会话的结果到自定义的子系统委托 SubsystemOnDestroySessionCompleteDelegate.Broadcast(bWasSuccessful); /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ } ...
-
重新进行测试,可以看到在设备 2 在被设备 1 踢出后能立刻点击 “
Host
” 按钮,出现创建会话失败提示消息,因为先前的会话没有被销毁,在销毁先前的会话后出现创建会话成功的提示消息。
25.2 添加退出游戏按钮
-
在虚幻引擎的内容浏览器中双击双击 “
WBP_Menu
” 图标,进入用户控件设计器窗口。确定在左下 “层级” 面板中已经选中 “画布画板” 后,在 “控制板” 面板中将 “通用” 选项卡下的 “按钮” 组件拖拽到设计器中,添加按钮 “QuitButton
”;接着在右侧 “细节” 面板中设置 “瞄点” 在 “画布画板” 的右上方,设置 “QuitButton
” 的 “位置 X” 为 -450、“位置 Y” 为 -100、“尺寸 X” 为 350、“尺寸 Y” 为 100。
-
在 “控制板” 面板中将 “通用” 中的 “文本”(Text)组件拖拽到 “
QuitButton
” 上,为按钮添加文本。
-
这里我们简单地使用蓝图编写点击 “
QuitButton
” 按钮后退出游戏事件。在左下 “层级”(Components)面板中选择 “QuitButton
” 组件, 然后在 “细节” 面板中单击 “点击时”(On Clicked)后面的 “+” 按钮,此时 “事件图表” 面板会出现一个 “点击时 (QuitButton)” 事件节点。
-
从 “点击时 (QuitButton)” 事件节点的输出引脚处拖拽出一条线连接 “退出游戏” 节点。
-
进行测试,运行游戏,点击 “
Quit
” 按钮后可以退出游戏。
25.3 添加按钮禁用功能
-
在 “
Menu.cpp
” 的 “HostButtonClicked()
” 和 “JoinButtonClicked()
” 函数添加按钮禁用的代码,这样按钮只能被点击一次,而不能被点击多次,以避免重复创建和查找会话。void UMenu::HostButtonClicked() // 回调函数:响应鼠标单击 HostButton 事件 { /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ HostButton->SetIsEnabled(false); // 按钮被第一次点击后禁用 /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ if (MultiplayerSessionsSubsystem) { MultiplayerSessionsSubsystem->CreateSession(NumPublicConnections, MatchType); // 创建游戏会话 } } void UMenu::JoinButtonClicked() // 回调函数:响应鼠标单击 HostButton 事件 { /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ JoinButton->SetIsEnabled(false); // 按钮被第一次点击后禁用 /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ if (MultiplayerSessionsSubsystem) { MultiplayerSessionsSubsystem->FindSessions(10000); } }
-
在 “
Menu.cpp
” 的 “OnCreateSession
” 函数中添加如果会话创建失败重新启用 “HostButton
” 按钮的代码void UMenu::OnCreateSession(bool bWasSuccessful) { if (bWasSuccessful) { ... } else { if (GEngine) { GEngine->AddOnScreenDebugMessage( // 添加调试信息到屏幕上 -1, // 使用 -1 不会覆盖前面的调试信息 15.f, // 调试信息的显示时间 FColor::Yellow, // 字体颜色:黄色 FString::Printf(TEXT("Failed to create session!")) // 打印会话创建成功消息 ); } /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ HostButton->SetIsEnabled(true); // 如果会话创建失败,则重新启用 HostButton 按钮 /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ } }
-
在 “
Menu.cpp
” 的 “OnFindSessions()
” 函数中添加如果搜索会话失败或者搜索结果为 0 则重新启用 “JoinButton
” 按钮的代码,然后在“OnJoinSessions()
” 函数中添加如果搜索会话成功但加入会话失败则重新启用 “JoinButton
” 按钮的代码。void UMenu::OnFindSessions(const TArray<FOnlineSessionSearchResult>& SearchResults, bool bWasSuccessful) { if (MultiplayerSessionsSubsystem == nullptr) { return; } // 遍历搜索结果并加入第一个匹配类型相同的会话(以后可以进行改进) for (auto Result : SearchResults) { FString SettingsValue; // 保存会话匹配类型 Result.Session.SessionSettings.Get(FName("MatchType"), SettingsValue); // 获取会话匹配类型 if (SettingsValue == MatchType) { // 如果匹配类型相同 MultiplayerSessionsSubsystem->JoinSession(Result); // 调用子系统的加入会话函数 return; } } /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ // 如果搜索会话失败或者搜索结果为 0 if (!bWasSuccessful || SearchResults.Num() == 0) { JoinButton->SetIsEnabled(true); // 重新启用 JoinButton 按钮 } /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ } void UMenu::OnJoinSession(EOnJoinSessionCompleteResult::Type Result) { // 加入会话,并传送至关卡 “Lobby” IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get(); // 获取当前的在线子系统指针 if (OnlineSubsystem) { // 如果当前在线子系统有效 IOnlineSessionPtr SessionInterface = OnlineSubsystem->GetSessionInterface(); // 获取会话接口智能指针 if (SessionInterface.IsValid()) { // 如果获取会话接口成功 FString Address; // 保存会话创建源地址 SessionInterface->GetResolvedConnectString(NAME_GameSession, Address); // 获取会话创建源地址 APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController(); // 获取玩家控制器 if (PlayerController) { PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute); // 客户端传送至关卡 “Lobby” } } } /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ // 如果搜索会话成功但加入会话失败 if (Result != EOnJoinSessionCompleteResult::Success) { JoinButton->SetIsEnabled(true); // 重新启用 JoinButton 按钮 } /* P25 完善菜单子系统(Polishing The Menu Subsystem)*/ }
-
进行测试。运行游戏后,点击 “
Host
” 按钮,可以看到在会话创建成功之前,按钮的颜色会一直保持较暗的亮度。
25.4 Summary
本节课对我们的菜单类进行了完善:修复了创建会话前检查上次会话是否存在的代码,解决了加入会话的设备被会话创建者踢出后无法立即创建会话的问题,保证创建会话功能一直可用;接着为菜单添加一个退出游戏按钮,并使用蓝图编程实现退出游戏逻辑;然后实现按钮被点击一次之后就禁用的功能,以防短时间多次重复地创建或查找会话,并在会话创建失败后重新启用创建会话按钮,在搜索会话失败或搜索结果为 0 或加入会话失败后重新启用加入会话按钮。
至此,该系列课程关于多人游戏插件制作这部分已经结束,我们也已经拥有了一个健壮(Robust)的多人游戏插件,将被用于之后的游戏项目当中。