【UE5】多人联机TPS游戏开发(一) —— 制作联机插件
前言:
本来准备继续深入学习GAS系统的,但是被学院弄去实训一个月后对前面学的东西有点陌生了,而且到7月还没找到实习,准备直接秋招了,所以想先把网络相关的内容学起来。
网络部分是我在之前的学习中完全没有接触过的内容,当看到两个角色出现在一张地图中并且可以进行互动的时候感觉还是挺有意思的。当然这个项目中使用的方法比较有局限性,但是从中学习到的一些理念和框架也足够我初步了解并动手实现网络联机了。
又是一段艰难的旅程,但是也没办法,谁让我就是头铁要走这条路呢~
1.联机基础 联机游戏的本质,是在多台设备上运行的多个游戏实例之间建立状态同步 。为了保证所有玩家在同一个“游戏世界”中实时互动,这些实例必须通过网络通信同步位置、输入、事件等关键数据。联机的方式大概可以分为两类:Peer-to-Peer(P2P)
和Client-Server(CS)
。
在P2P
架构中,所有玩家都需要时刻向其他所有玩家发送自己的信息,而发送信息的数量会随着玩家数量的增加而快速增长,因此这是一种简单但局限性极大 的方式。
Client-Server
架构则是存在一个服务器,所有玩家只需要和服务器进行通信,服务器会接收所有玩家的信息,并向所有玩家同步这些信息。CS
架构又可以分为两类:Listen-Server
(监听服务器) ,一台玩家主机既运行游戏客户端,也承担服务器角色。主机上的逻辑会被当作权威状态,服务器会把必要的状态(位置、事件等)同步到连接的客户端。这种方法使用于网络传输需求较小的游戏。个人猜测Warframe
就是使用了这种方式;Dedicated-Server
(专用服务器) ,有一个专门的服务器负责接收请求并发送数据,通常用于对网络要求较高的游戏,如大型MMO、战术射击游戏等。
本项目使用的是收听服务器的方式进行联机。在实现联机功能时,一个主要挑战是如何对接各个平台(Steam、Epic、Xbox、PS 等)的底层 API 。每个平台的网络服务机制不同,实现方式也不统一。为了解决这个问题,Unreal 提供了一个跨平台的抽象层 —— OnlineSubsystem
(简称 OSS) 。OSS 为开发者屏蔽了平台差异,使我们可以使用统一的接口来实现会话创建、玩家身份识别、好友系统等功能。
本项目使用的是 OSS 中的 OnlineSubsystemSteam
插件 ,通过 Steam 提供的 API 实现跨局域网的联机功能。
1.1 测试局域网联机 首先进行联机功能的第一步,也是最简单的一步:实现局域网联机。
创建一个第三人称项目,并创建一个名为“Lobby”的关卡。在角色蓝图中实现以下功能:
打包项目,并将其拷贝到客机。客机需要和主机连接到同一局域网。
在主机按下“1”后,主机会加载关卡“Lobby”,Options中的“listen”表明要以监听服务器 身份运行,它会打开 socket 接口等待其他玩家连接。随后在客机按下”2“,游戏会执行命令“Open 121.48.198.53”,客户端尝试通过 socket 连接到该地址运行的 listen server。如果目标IP是监听服务器,它会接受连接,并将客户端加载到正确的地图中。
完成上面的操作后,就可以看到两台电脑的角色在一起活蹦乱跳了。
1.2 使用代码实现局域网联机 逻辑很简单,直接贴代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void AMPTestingCharacter::OpenLobby () { UWorld* world = GetWorld (); if (world) { world->ServerTravel ("/Game/ThirdPerson/Maps/Lobby?Listen" ); } } void AMPTestingCharacter::CallOpenLevel (const FString& Address) { UGameplayStatics::OpenLevel (this , *Address); } void AMPTestingCharacter::CallClientTravel (const FString& Address) { APlayerController* playerController = GetGameInstance ()->GetFirstLocalPlayerController (); if (playerController) { playerController->ClientTravel (Address, ETravelType::TRAVEL_Absolute); } }
逻辑和上一小节基本上是一样的,只不过多了一种用于连接主机的方法。用相同的方式进行测试。
联机成功。
目前我们实现了最基础的局域网联机。使用 UE 提供的 ServerTravel
和 ClientTravel
方法,我们可以让一台设备以 listen server 的身份运行,并允许其他局域网内的设备通过 IP 地址连接进来。虽然方式简单直接,但依赖于网络环境,难以在公网环境中稳定使用。为了解决这一问题,我们将在下一节引入 UE 的 OnlineSubsystem
模块,并通过 Steam 实现跨网段联机。
2 测试通过Steam进行联机 在线子系统(Online Subsystem) 及其接口提供一种可访问Steam
、Xbox Live
、Facebook
等在线服务功能的常用方法。开发一款在多平台上发行或支持多在线服务的游戏时,在线子系统可确保开发者唯一需要做的变更就是对所有支持的服务进行配置调整。UE引擎提供了一系列针对不同平台的Online Subsystem插件,而本项目中使用的就是针对Steam平台的插件,它提供了底层 SteamNetDriver
、SteamSession
、SteamAPI
调用封装等模块。
Online Subsystem in Unreal Engine | 虚幻引擎 5.5 文档 | Epic Developer Community
虚幻引擎Steam在线子系统接口 | 虚幻引擎 5.5 文档 | Epic Developer Community
详细的使用方法可以参照官方文档。
2.1 配置 创建新的项目后,我们需要开启这个插件:
并且在配置文件(Config/DefaultEngine.INI)中添加以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [/Script/Engine.GameEngine] +NetDriverDefinitions =(DefName="GameNetDriver" ,DriverClassName="OnlineSubsystemSteam.SteamNetDriver" ,DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver" ) [OnlineSubsystem] DefaultPlatformService =Steam[OnlineSubsystemSteam] bEnabled =true SteamDevAppId =480 [/Script/OnlineSubsystemSteam.SteamNetDriver] NetConnectionClassName ="OnlineSubsystemSteam.SteamNetConnection"
这么做是为了告诉 UE:”默认的联机服务平台是 Steam。“SteamDevAppId=480
是 Valve 提供的开发专用测试 ID(SpaceWar) ,允许不通过 Steam 发布就进行测试联机。bEnabled=true
是显式启用插件。
我们还需要手动在构建文件MenuSystem.Build.cs
中添加模块依赖:
1 2 3 4 5 6 7 8 9 public class MenuSystem : ModuleRules{ public MenuSystem (ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange (new string[] { ...(原有模块), "OnlineSubsystemSteam" , "OnlineSubsystem" }); } }
UE 构建系统依赖这些模块标记来将插件静态链接进你的项目。OSS 是跨模块插件,不声明无法访问接口。
最后重新构建项目,就完成了配置。
2.2 获取会话接口 因为现在要做的是测试Steam的联机功能,所以就不新建一个类了,而是直接在角色类中实现。
要通过在线子系统实现联机,我们首先要做的就是访问在线子系统并获取Session接口 。这是后续实现 创建/查找/加入会话 (Session)功能的基础。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 IOnlineSessionPtr OnlineSessionInterface; AMenuSystemCharacter::AMenuSystemCharacter () : IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get (); if (OnlineSubsystem) { OnlineSessionInterface = OnlineSubsystem->GetSessionInterface (); if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Blue, FString::Printf (TEXT ("Found subsystem %s" ), *OnlineSubsystem->GetSubsystemName ().ToString ()) ); } } }
我们首先声明了一个IOnlineSessionPtr
成员变量,这个变量本质上是一个IOnlineSessiion
类的共享指针,并保证了线程安全。在构造函数中,我们通过IOnlineSubsystem::Get()
方法获取到了当前激活的在线子系统。这里的 Get()
是一个 静态方法 ,它是通过名称条件,查找已经进行注册/创建的实例。各个平台注册自己的子系统,然后让用户通过 Get()
全局访问。
获取成功后,我们就可以通过在线子系统为OnlineSessionInterface
进行赋值,从而获得了会话模块的接口(下文简称会话接口 )。通过会话接口,我们就可以进行接下来的创建、查找、加入会话等工作。
获得接口后,我们可以将其名称打印在屏幕上,来观察是否成功获取了Steam子系统。
注意,这里要打包项目后运行打包好的项目才能观察到正确的结果。因为编辑器里不会启用 Steam OSS,打包后才会真正加载 Steam 子系统。
2.3 创建会话 获得了会话接口后,就可以用其提供的各种方法进行对话的创建、寻找、加入、销毁等操作,从而实现联机。首先从创建会话开始。
创建会话的流程包含三个步骤:
绑定创建会话委托(用于接收异步回调)
构造会话设置对象(设置连接数、匹配类型等)
调用在线子系统接口创建会话(异步操作)
我声明了一个创建会话的接口函数和一个回调函数 ,以及一个委托 。简单来说,委托可以绑定一个(或多个,根据委托的类型来决定)回调函数,我们可以对委托进行(手动或自动)广播,这样所有和这个委托绑定的回调函数都会被调用。委托是实现异步通信的一个常用方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 protected : UFUNCTION (BlueprintCallable) void CreateGameSession () ; void OnCreateSessionComplete (FName SessionName, bool bWasSuccessful) ; private : FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate;
UE 的 OSS 就是一个异步系统 ,创建 Session 不会立刻完成,必须用回调函数处理结果。FOnCreateSessionCompleteDelegate
是一个专门用于监听创建结果的事件类型。
我们可以直接在构造函数的初始化列表中将委托与回调函数进行绑定:
1 CreateSessionCompleteDelegate (FOnCreateSessionCompleteDelegate::CreateUObject (this , &ThisClass::OnCreateSessionComplete))
接下来就是创建会话了。在CreateGameSession
函数实现以下逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void AMenuSystemCharacter::CreateGameSession () { if (!OnlineSessionInterface.IsValid ()) { return ; } auto ExistingSession = OnlineSessionInterface->GetNamedSession (NAME_GameSession); if (ExistingSession) { OnlineSessionInterface->DestroySession (NAME_GameSession); } OnlineSessionInterface->AddOnCreateSessionCompleteDelegate_Handle (CreateSessionCompleteDelegate); TSharedPtr<FOnlineSessionSettings> SessionSettings = MakeShareable (new FOnlineSessionSettings ()); SessionSettings->bIsLANMatch = false ; SessionSettings->NumPublicConnections = 4 ; SessionSettings->bAllowJoinInProgress = true ; SessionSettings->bAllowJoinViaPresence = true ; SessionSettings->bShouldAdvertise = true ; SessionSettings->bUsesPresence = true ; SessionSettings->bUseLobbiesIfAvailable = true ; SessionSettings->Set (FName ("MatchType" ), FString ("FreeForAll" ), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing); const ULocalPlayer* localPlayer = GetWorld ()->GetFirstLocalPlayerFromController (); OnlineSessionInterface->CreateSession (*localPlayer->GetPreferredUniqueNetId (), NAME_GameSession, *SessionSettings); }
如果当前已经存在会话,我们需要将其销毁。然后将我们声明的委托添加到列表中。这是OSS提供的方法,当使用OSS的接口创建会话后,它就会对列表中的所有委托进行广播,进而调用与委托绑定的函数。下面通过共享指针定义了一个会话设置,里面定义了一些要创建的会话的信息。在我使用的版本(UE5.5)需要设置bUseLobbiesIfAvailable = true
才能进行正常联机。最后就是创建会话了。UE 的 Session 创建需要提供唯一身份 ID,localPlayer->GetPreferredUniqueNetId()
就是当前 Steam 登录用户的标识,需要通过获得当前的玩家去获得身份ID。
为什么在创建会话设置的对象时,要在堆上创建并使用共享指针进行管理,而不是创建一个临时变量?
Unreal
的会话接口(如 CreateSession
)是异步操作 ,也就是说函数返回后,实际的会话创建过程仍在后台执行,直到系统触发对应的回调函数。如果我们使用的是函数内的局部变量,那么在函数结束时,该对象会自动析构。而此时后台异步操作还未完成,它若尝试访问这个已销毁的对象,就会产生未定义行为 ,导致程序崩溃。如果改为值传递,虽然避免了悬空指针的问题,但这会引入一次不必要的深拷贝开销 。因此,更推荐的做法是使用 TSharedPtr
,通过 MakeShareable(new ...)
将 SessionSettings
分配到堆上,主动管理生命周期,即使函数返回,内部的引用计数仍保持为 1(或以上),直到异步流程完成后系统再销毁该对象。这样既避免了复制,也保证了稳定性。
之后有很多功能都会用到这个操作,思想大概都是这样。
回调函数的逻辑很简单,当创建会话完成后触发CreateSessionCompleteDelegate
委托的广播,自动调用创建会话的回调函数。如果传入的值为true
,说明创建成功,传送到大厅并设为监听服务器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void AMenuSystemCharacter::OnCreateSessionComplete (FName SessionName, bool bWasSussful) { if (bWasSussful) { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Blue, FString::Printf (TEXT ("Created session: %s" ),*SessionName.ToString ()) ); } UWorld* world = GetWorld (); if (world) { world->ServerTravel (FString ("/Game/ThirdPerson/Maps/Lobby?listen" )); } } else { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Red, FString (TEXT ("Failed to create session!" )) ); } } }
2.4 加入会话 实现创建会话后,顺理成章的就要开始实现加入会话了。和创建会话时相同,声明加入会话的函数和两个回调函数,以及两个相应的委托,分别用于发现会话和加入会话,并将委托和回调函数进行绑定。还声明了一个成员变量,用来存储搜索的条件和结果等(存疑)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //MenuSystemCharacter.h UFUNCTION(BlueprintCallable) void JoinGameSession(); void OnFindSessionsComplete(bool bWasSuccessful); void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result); FOnFindSessionsCompleteDelegate FindSessionsCompleteDelegate; FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate; TSharedPtr<FOnlineSessionSearch> SessionSearch; //MenuSystemCharacter.cpp //构造函数的初始化列表 FindSessionsCompleteDelegate(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete)), JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete))
将发现会话的委托添加到委托列表,创建一个寻找会话的条件,就可以开始寻找会话了。之后使用本地玩家的NetID
向 OSS 发起搜索请求。此过程也是异步的 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void AMenuSystemCharacter::JoinGameSession () { if (!OnlineSessionInterface.IsValid ()) { return ; } OnlineSessionInterface->AddOnFindSessionsCompleteDelegate_Handle (FindSessionsCompleteDelegate); SessionSearch = MakeShareable (new FOnlineSessionSearch ()); SessionSearch->MaxSearchResults = 10000 ; SessionSearch->bIsLanQuery = false ; SessionSearch->QuerySettings.Set (SEARCH_PRESENCE, true , EOnlineComparisonOp::Equals); const ULocalPlayer* LocalPlayer = GetWorld ()->GetFirstLocalPlayerFromController (); OnlineSessionInterface->FindSessions (*LocalPlayer->GetPreferredUniqueNetId (), SessionSearch.ToSharedRef ()); }
寻找会话完成后,会调用寻找会话委托绑定的回调函数。在回调函数中,我们需要对寻找到的结果进行一些筛选。当找到符合要求的会话后,就需要加入这个会话了。将加入会话的委托加入委托列表,就可以通过NetID加入会话了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 void AMenuSystemCharacter::OnFindSessionsComplete (bool bWasSuccessful) { if (!OnlineSessionInterface.IsValid ()) { return ; } for (auto result : SessionSearch->SearchResults) { FString Id = result.GetSessionIdStr (); FString User = result.Session.OwningUserName; FString MatchType; result.Session.SessionSettings.Get (FName ("MatchType" ), MatchType); if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Cyan, FString::Printf (TEXT ("Id: %s, User: %s" ), *Id, *User) ); } if (MatchType == FString ("FreeForAll" )) { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Cyan, FString::Printf (TEXT ("Joing Match Type: %s" ), *MatchType) ); } OnlineSessionInterface->AddOnJoinSessionCompleteDelegate_Handle (JoinSessionCompleteDelegate); const ULocalPlayer* localPlayer = GetWorld ()->GetFirstLocalPlayerFromController (); OnlineSessionInterface->JoinSession (*localPlayer->GetPreferredUniqueNetId (), NAME_GameSession, Result); } } }
加入会话后,会调用加入会话的回调函数。因为加入会话并不会将客户端自动传送至主机地图,在这里要使用ClientTravel
方法将玩家传送到主机所在的地图。其中的地址是通过会话接口解析会话得到的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 void AMenuSystemCharacter::OnJoinSessionComplete (FName SessionName, EOnJoinSessionCompleteResult::Type Result) { if (!OnlineSessionInterface.IsValid ()) { return ; } FString Address; if (OnlineSessionInterface->GetResolvedConnectString (NAME_GameSession, Address)) { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Yellow, FString::Printf (TEXT ("Connext string: %s" ), *Address) ); } APlayerController* PlayerController = GetGameInstance ()->GetFirstLocalPlayerController (); if (PlayerController) { PlayerController->ClientTravel (Address, ETravelType::TRAVEL_Absolute); } } else { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Yellow, FString::Printf (TEXT ("Address Not Find" )) ); } }
一个简单的联机逻辑就这样实现了。里面有很多逻辑不够严密的地方,并且选项都是写死的,但是用作测试已经足够了。
在进行测试时,需要两台电脑,分别登录两个不同的Steam账号,并且下载地点设为同一个地方。启动游戏,一遍按1
调用创建会话,另一边按2
加入会话。成功!
3.插件制作 终于到了这篇文章的重点。我想制作一个插件,使得无论何时我想制作一个多人联机项目,只要安装制作好的插件就可以通过调用它的一些接口,轻松实现联机功能。
3.1 配置 创建一个空白插件。
我们需要声明这个插件需要依赖的插件。本插件依赖于官方插件OnlineSubsystem
和OnlineSubsystemSteam
,因此需要在插件的.uplugin
文件中加上下面的内容:
1 2 3 4 5 6 7 8 9 10 "Plugins": [ { "Name": "OnlineSubsystem", "Enabled": true }, { "Name": "OnlineSubsystemSteam", "Enabled": true } ]
它告诉 UE 编译器 在启用本插件时自动启用这两个插件 ,避免了手动逐一勾选的麻烦。
并且在插件的构建文件中加上:
1 2 3 4 5 6 7 8 9 PublicDependencyModuleNames.AddRange( new string [] { "Core" , "OnlineSubsystem" , "OnlineSubsystemSteam" } );
这是告诉 Unreal Build Tool(UBT)
:本插件在编译时需要链接 OnlineSubsystem
和 Steam 的 C++ 模块 ,否则在代码中引用 IOnlineSession
、FOnlineSessionSettings
等类会编译失败。
3.2 创建自己的类 这里我画了一个图,用于方便看懂插件中的四大部分之间是如何交互的 。之后的所有内容都遵循这个流程,因为具体功能实现方式和上面其实差不多,所以只做简单讲解。
插件的实现主要依靠实现一个自己的类:MultiplayerSessionsSubsystem
类,它继承自游戏实例子系统类 (UGameInstanceSubsystem
)。UGameInstanceSubsystem
是 Unreal Engine 提供的一种全局生命周期子系统 ,它在游戏启动后就存在,并贯穿整个游戏生命周期,不会因为关卡切换而销毁 。MultiplayerSessionsSubsystem
类是插件的核心,主要提供了一些对于会话的操作的接口以及相应操作的委托,只要使用这些接口和委托就可以快速实现联机。
首先需要获取在线会话接口并进行初始化:
1 2 3 4 5 6 7 8 OnlineSessionPtr SessionInterface; IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get (); if (Subsystem) { SessionInterface = Subsystem->GetSessionInterface (); }
我们的类中将会提供创建、寻找、加入、销毁、开始会话的五个接口,而这些接口实现其功能的方式则主要依赖于这个在线会话接口。现在来声明这些接口,并声明这些功能对应的委托及回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public : UMultiplayerSessionsSubsystem (); void CreateSession (int32 NumPublicConnections, FString MatchType) ; void FindSessions (int MaxSearchResults) ; void JoinSessions (const FOnlineSessionSearchResult& SessionResult) ; void DestroySession () ; void StartSession () ; protected : void OnCreateSessionComplete (FName SessionName, bool bWasSuccessful) ; void OnFindSessionsComplete (bool bWasSuccessful) ; void OnJoinSessionComplete (FName SessionName, EOnJoinSessionCompleteResult::Type Result) ; void OnDestroySessionComplete (FName SessionName, bool bWasSuccessful) ; void OnStartSessionComplete (FName SessionName, bool bWasSuccessful) ; private : IOnlineSessionPtr SessionInterface; FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate; FDelegateHandle CreateSessionCompleteDelegateHandle; FOnFindSessionsCompleteDelegate FindSessionsCompleteDelegate; FDelegateHandle FindSessionsCompleteDelegateHandle; FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate; FDelegateHandle JoinSessionCompleteDelegateHandle; FOnDestroySessionCompleteDelegate DestroySessionCompleteDelegate; FDelegateHandle DestroySessionCompleteDelegateHandle; FOnStartSessionCompleteDelegate StartSessionCompleteDelegate; FDelegateHandle StartSessionCompleteDelegateHandle;
将委托和回调函数进行绑定:
1 2 3 4 5 CreateSessionCompleteDelegate (FOnCreateSessionCompleteDelegate::CreateUObject (this ,&ThisClass::OnCreateSessionComplete)),FindSessionsCompleteDelegate (FOnFindSessionsCompleteDelegate::CreateUObject (this , &ThisClass::OnFindSessionsComplete)),JoinSessionCompleteDelegate (FOnJoinSessionCompleteDelegate::CreateUObject (this , &ThisClass::OnJoinSessionComplete)),DestroySessionCompleteDelegate (FOnDestroySessionCompleteDelegate::CreateUObject (this , &ThisClass::OnDestroySessionComplete)),StartSessionCompleteDelegate (FOnStartSessionCompleteDelegate::CreateUObject (this , &ThisClass::OnStartSessionComplete))
这里在声明委托的同时也声明了其对应的句柄 ,这是为了在需要时能“解绑”委托回调函数,避免重复绑定或内存泄漏。当我们将委托添加到对应的委托列表后,它是不会自动移除的。如果重复多次进行这一操作,同一个函数会被绑定多次,将会在回调时被多次调用 (重复执行)。因此,为了后续解绑,需要保留绑定操作返回的句柄 FDelegateHandle
。
我们还将创建一个菜单类,这个类继承自UserWidget
类,会通过调用MultiplayerSessionsSubsystem
类的接口将对会话的操作简化成最简单的两个步骤:按下Host
按钮和按下Join
按钮。这样一来,使用插件时既可以在安装插件后直接使用最简单的联机功能,也可以通过提供的接口实现更多功能。
菜单类的构建文件中需要添加以下内容:
1 2 3 "UMG" ,"Slate" ,"SlateCore"
在菜单类中,先定义一个初始化菜单的函数,将菜单添加到视图上,并将输入模式设为仅UI,将光标显示出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void UMenu::MenuSetup () { AddToViewport (); SetVisibility (ESlateVisibility::Visible); bIsFocusable = true ; UWorld* World = GetWorld (); if (World) { APlayerController* PlayerController = World->GetFirstPlayerController (); if (PlayerController) { FInputModeUIOnly InputModeData; InputModeData.SetWidgetToFocus (TakeWidget ()); InputModeData.SetLockMouseToViewportBehavior (EMouseLockMode::DoNotLock); PlayerController->SetInputMode (InputModeData); PlayerController->SetShowMouseCursor (true ); } } }
菜单的创建属于基础内容,直接快进。
在菜单类中对这两个按钮进行绑定,方法是使用元数据meta=(BindWidget)
。声明这两个按钮的回调函数,并声明一个多人会话子系统的指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 UPROPERTY (meta = (BindWidget))class UButton * HostButton;UPROPERTY (meta = (BindWidget))UButton* JoinButton; UFUNCTION ()void HostButtonClicked () ;UFUNCTION ()void JoinButtonClicked () ;class UMultiplayerSessionsSubsystem * MultiplayerSessionsSubsystem;
在MenuSetup
函数中初始化子系统,并在UI初始化时将按钮点击的委托和回调函数进行绑定。注意,绑定的逻辑要放在Initialize
函数中,而不是构造函数中,因为在构造函数调用时还无法访问蓝图中的控件,而Initialize
函数会在控件全部加载完成后调用 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 UGameInstance* GameInstance = GetGameInstance (); if (GameInstance) { MultiplayerSessionsSubsystem = GameInstance->GetSubsystem <UMultiplayerSessionsSubsystem>(); } bool UMenu::Initialize () { if (!Super::Initialize ()) { return false ; } if (HostButton) { HostButton->OnClicked.AddDynamic (this , &ThisClass::HostButtonClicked); } if (JoinButton) { JoinButton->OnClicked.AddDynamic (this , &ThisClass::JoinButtonClicked); } return true ;; } void UMenu::HostButtonClicked () { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Yellow, FString (TEXT ("Host Button Clicked" )) ); } if (MultiplayerSessionsSubsystem) { MultiplayerSessionsSubsystem->CreateSession (4 , FString ("FreeForAll" )); } } void UMenu::JoinButtonClicked () { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Yellow, FString (TEXT ("Join Button Clicked" )) ); } }
3.3 创建会话 回到MultiplayerSessionsSubsystem
类,现在要实现创建会话的功能了。在创建会话的函数中,将创建会话的委托加入委托列表,并保存句柄方便后续的解绑。然后创建一个在线会话的设置,使用这个设置来通过接口创建会话。如果创建失败,需要将委托解绑。
在创建会话时,如果有已经存在的会话需要将其销毁,但是这个过程需要时间,如果在调用销毁函数后离开创建会话,很容易因为当前会话还未销毁完毕而创建失败。解决方法是创建了三个变量,表示当前有没有正在销毁的会话,以及要创建的会话的信息,然后在销毁会话完成后的回调函数中进行创建。
1 2 3 4 bool bCreateSessionOnDestroy = false ;int32 LastNumPublicConnections; FString LastMatchType;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 void UMultiplayerSessionsSubsystem::CreateSession (int32 NumPublicConnections, FString MatchType) { if (!SessionInterface.IsValid ()) { return ; } auto ExistingSession = SessionInterface->GetNamedSession (NAME_GameSession); if (ExistingSession) { bCreateSessionOnDestroy = true ; LastNumPublicConnections = NumPublicConnections; LastMatchType = MatchType; DestroySession (); } CreateSessionCompleteDelegateHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle (CreateSessionCompleteDelegate); LastSessionSettings = MakeShareable (new FOnlineSessionSettings ()); LastSessionSettings->bIsLANMatch = IOnlineSubsystem::Get ()->GetSubsystemName () == "NULL" ; LastSessionSettings->NumPublicConnections = NumPublicConnections; LastSessionSettings->bAllowJoinInProgress = true ; LastSessionSettings->bAllowJoinViaPresence = true ; LastSessionSettings->bShouldAdvertise = true ; LastSessionSettings->bUsesPresence = true ; LastSessionSettings->bUseLobbiesIfAvailable = true ; LastSessionSettings->Set (FName ("MatchType" ), MatchType, EOnlineDataAdvertisementType::ViaOnlineServiceAndPing); LastSessionSettings->BuildUniqueId = 1 ; const ULocalPlayer* LocalPlayer = GetWorld ()->GetFirstLocalPlayerFromController (); if (!SessionInterface->CreateSession (*LocalPlayer->GetPreferredUniqueNetId (), NAME_GameSession, *LastSessionSettings)) { SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle (CreateSessionCompleteDelegateHandle); MultiplayerOnCreateSessionComplete.Broadcast (false ); } }
在菜单类中声明三个变量,用来指定创建会话时的一些信息。在MenuSetup函数中接收这三个参数,为创建会话做好准备。
1 2 3 4 5 6 7 8 public : void MenuSetup (int32 NumberOfPublicConnections = 4 , FString TypeOfMatch = FString(TEXT("FreeForAll" )), FString LobbyPath = FString(TEXT("/Game/ThirdPerson/Maps/Lobby" ))) ; private : int32 NumPublicConnections{ 4 }; FString MatchType{ TEXT ("FreeForAll" ) }; FString PathToLobby{ TEXT ("" ) };
按下Host
按钮后,我们要做的就是调用MultiplayerSessionsSubsystem
的CreateSession
接口,将上面声明的变量传递过去用于创建会话。
1 2 3 if (MultiplayerSessionsSubsystem) { MultiplayerSessionsSubsystem->CreateSession (NumPublicConnections, MatchType); }
目前已经实现了Menu
类对MultiplayerSessionsSubsystem
类的单向通信(点击Host
按钮后通过MultiplayerSessionsSubsystem
类创建会话),但是还没有实现MultiplayerSessionsSubsystem
类对Menu
类的通信。这里实现双向通信的方式仍然是委托。我们在MultiplayerSessionsSubsystem
类中声明一组委托,用于绑定Menu
类的回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam (FMultiplayerOnCreateSessionComplete, bool , bWasSuccessful);DECLARE_MULTICAST_DELEGATE_TwoParams (FMultiplayerOnFindSessionsComplete, const TArray <FOnlineSessionSearchResult>& SessionResults, bool bWasSuccessful);DECLARE_MULTICAST_DELEGATE_OneParam (FMultiplayerOnJoinSessionComplete, EOnJoinSessionCompleteResult::Type Result);DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam (FMultiplayerOnDestroySessionComplete, bool , bWasSuccessful);DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam (FMultiplayerOnStartSessionComplete, bool , bWasSuccessful);public : FMultiplayerOnCreateSessionComplete MultiplayerOnCreateSessionComplete; FMultiplayerOnFindSessionsComplete MultiplayerOnFindSessionsComplete; FMultiplayerOnJoinSessionComplete MultiplayerOnJoinSessionComplete; FMultiplayerOnDestroySessionComplete MultiplayerOnDestroySessionComplete; FMultiplayerOnStartSessionComplete MultiplayerOnStartSessionComplete; protected : UFUNCTION () void OnCreateSession (bool bWasSuccessful) ; void OnFindSessions (const TArray <FOnlineSessionSearchResult>& SessionResults, bool bWasSuccessful) ; void OnJoinSession (EOnJoinSessionCompleteResult::Type Result) ; UFUNCTION () void OnDestroySession (bool bWasSuccessful) ; UFUNCTION () void OnStartSession (bool bWasSuccessful) ;
可以看到,这些回调函数中有的声明了UFUNCTION
,有的没有。这是因为在声明委托时,有的声明为了动态多播委托,而动态多播委托必须绑定UFUNCTION
,参数中的类也必须是UCLASS
或者USTRUCT
。然而在发现会话的委托和加入会话的委托中,我们想要传递的参数并不是UCLASS
或者USTRUCT
,所以无法使用动态多播,只能声明为多播,因此绑定的函数也不能声明为UFUNCTION
。
DECLARE_DYNAMIC_MULTICAST_DELEGATE
(动态多播委托)与 DECLARE_MULTICAST_DELEGATE
(普通多播)之间的主要差别在于是否依赖反射系统(Reflection)并可被蓝图/序列化访问 :
动态(Dynamic)多播委托 :可以在蓝图中绑定/触发(可标为 UPROPERTY
),需要绑定的回调函数用 UFUNCTION
声明,并且参数类型必须是反射系统可识别的类型(UCLASS
、USTRUCT
、基础类型等)。优点是可序列化、能与蓝图交互;缺点是运行时开销略大(使用 FScriptDelegate
),不能承载任意 C++ 类型参数。
普通(非动态)多播委托 :纯 C++,允许任意 C++ 参数类型(例如 FOnlineSessionSearchResult
),性能更好,但不能直接在蓝图绑定或序列化。
如果委托需要暴露给 UMG / Blueprint(比如在蓝图里直接绑定),使用动态多播;如果是纯 C++ 的高频回调或参数类型不被反射支持,推荐使用普通多播。
在菜单建立的函数中,将回调函数和MultiplayerSessionsSubsystem
类中自定义的委托进行绑定。根据委托是否是动态的,绑定方式也有所区别。
1 2 3 4 5 6 7 8 if (MultiplayerSessionsSubsystem) { MultiplayerSessionsSubsystem->MultiplayerOnCreateSessionComplete.AddDynamic (this , &ThisClass::OnCreateSession); MultiplayerSessionsSubsystem->MultiplayerOnFindSessionsComplete.AddUObject (this , &ThisClass::OnFindSessions); MultiplayerSessionsSubsystem->MultiplayerOnJoinSessionComplete.AddUObject (this , &ThisClass::OnJoinSession); MultiplayerSessionsSubsystem->MultiplayerOnDestroySessionComplete.AddDynamic (this , &ThisClass::OnDestroySession); MultiplayerSessionsSubsystem->MultiplayerOnStartSessionComplete.AddDynamic (this , &ThisClass::OnStartSession); }
这样一来,MultiplayerSessionsSubsystem
类就可以与Menu
类进行通话了。在MultiplayerSessionsSubsystem
类中创建会话时,如果创建失败,需要广播自定义委托并传入参数false
,这样菜单就知道:创建会话失败了。
1 2 3 4 5 6 if (!SessionInterface->CreateSession (*LocalPlayer->GetPreferredUniqueNetId (), NAME_GameSession, *LastSessionSettings)) { SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle (CreateSessionCompleteDelegateHandle); MultiplayerOnCreateSessionComplete.Broadcast (false ); }
如果没有失败,MultiplayerSessionsSubsystem
类需要在创建会话的回调函数中广播委托,告诉菜单会话有没有创建成功。在这之前还要先解绑创建会话的委托 。
1 2 3 4 5 6 7 8 void UMultiplayerSessionsSubsystem::OnCreateSessionComplete (FName SessionName, bool bWasSuccessful) { if (SessionInterface) { SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle (CreateSessionCompleteDelegateHandle); } MultiplayerOnCreateSessionComplete.Broadcast (bWasSuccessful); }
在Menu
类中的发现会话的函数中,我们接收到了MultiplayerSessionsSubsystem
类传来的信息。如果创建成功了,就传送到大厅并设为监听服务器。传送完成后,还需要清除菜单并使玩家能够控制菜单。这部分逻辑放在MenuTearDown
函数中实现,这个函数在NativeDestruct
函数中被调用,而NativaDestrcut
函数会在离开地图时被自动调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 void UMenu::OnCreateSession (bool bWasSuccessful) { if (bWasSuccessful) { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Yellow, FString (TEXT ("Session created successfully!" )) ); } UWorld* World = GetWorld (); if (World) { World->ServerTravel (PathToLobby); } } else { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Red, FString (TEXT ("Failed to create session!" )) ); } } } void UMenu::NativeDestruct () { MenuTearDown (); Super::NativeDestruct (); } void UMenu::MenuTearDown () { RemoveFromParent (); UWorld* World = GetWorld (); if (World) { APlayerController* PlayerController = World->GetFirstPlayerController (); if (PlayerController) { FInputModeGameOnly InputModeData; PlayerController->SetInputMode (InputModeData); PlayerController->SetShowMouseCursor (false ); } } }
3.4 加入会话 创建会话后,需要实现加入会话的功能。当Join
按钮被按下后,回调函数中会执行:
1 2 3 if (MultiplayerSessionsSubsystem) { MultiplayerSessionsSubsystem->FindSessions (10000 ); }
发现会话的实现如下,和之前的逻辑差不多,直接放代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void UMultiplayerSessionsSubsystem::FindSessions (int MaxSearchResults) { if (!SessionInterface.IsValid ()) { return ; } FindSessionsCompleteDelegateHandle = SessionInterface->AddOnFindSessionsCompleteDelegate_Handle (FindSessionsCompleteDelegate); LastSessionSearch = MakeShareable (new FOnlineSessionSearch ()); LastSessionSearch->MaxSearchResults = MaxSearchResults; LastSessionSearch->bIsLanQuery = IOnlineSubsystem::Get ()->GetSubsystemName () == "NULL" ; LastSessionSearch->QuerySettings.Set (SEARCH_PRESENCE, true , EOnlineComparisonOp::Equals); const ULocalPlayer* LocalPlayer = GetWorld ()->GetFirstLocalPlayerFromController (); if (!SessionInterface->FindSessions (*LocalPlayer->GetPreferredUniqueNetId (), LastSessionSearch.ToSharedRef ())) { SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle (FindSessionsCompleteDelegateHandle); MultiplayerOnFindSessionsComplete.Broadcast (TArray <FOnlineSessionSearchResult>(), false ); } }
如果没有失败,会执行发现会话的回调函数,如果没有发现会话,直接返回,否则将搜索结果作为参数进行广播,调用菜单类的发现会话的回调函数。注意新版UE5在这里要对搜索结果手动将bUseLobbiesIfAvailable
设为true,因为不知道什么时候会自动变成false。如果这里不改回来,将无法加入会话。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void UMultiplayerSessionsSubsystem::OnFindSessionsComplete (bool bWasSuccessful) { if (SessionInterface) { SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle (FindSessionsCompleteDelegateHandle); } if (LastSessionSearch->SearchResults.Num () <= 0 ) { MultiplayerOnFindSessionsComplete.Broadcast (TArray <FOnlineSessionSearchResult>(), false ); if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 1.f , FColor::Red, FString (TEXT ("Session not found!" )) ); } return ; } for (auto & Result : LastSessionSearch->SearchResults) { Result.Session.SessionSettings.bUseLobbiesIfAvailable = true ; } MultiplayerOnFindSessionsComplete.Broadcast (LastSessionSearch->SearchResults, bWasSuccessful); }
菜单类的回调函数,如果找到匹配的会话,直接通过接口加入会话:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void UMenu::OnFindSessions (const TArray<FOnlineSessionSearchResult>& SessionResults, bool bWasSuccessful) { if (!MultiplayerSessionsSubsystem) { return ; } for (const auto & Result : SessionResults) { FString SettingsValue; Result.Session.SessionSettings.Get (FName ("MatchType" ), SettingsValue); if (SettingsValue == MatchType) { MultiplayerSessionsSubsystem->JoinSessions (Result); return ; } } }
加入会话的函数实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void UMultiplayerSessionsSubsystem::JoinSessions (const FOnlineSessionSearchResult& SessionResult) { if (!SessionInterface.IsValid ()) { MultiplayerOnJoinSessionComplete.Broadcast (EOnJoinSessionCompleteResult::UnknownError); return ; } JoinSessionCompleteDelegateHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle (JoinSessionCompleteDelegate); const ULocalPlayer* LocalPlayer = GetWorld ()->GetFirstLocalPlayerFromController (); if (!SessionInterface->JoinSession (*LocalPlayer->GetPreferredUniqueNetId (), NAME_GameSession, SessionResult)) { SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle (JoinSessionCompleteDelegateHandle); MultiplayerOnJoinSessionComplete.Broadcast (EOnJoinSessionCompleteResult::UnknownError); } }
在回调函数中向菜单类广播结果。
1 2 3 4 5 6 7 8 void UMultiplayerSessionsSubsystem::OnJoinSessionComplete (FName SessionName, EOnJoinSessionCompleteResult::Type Result) { if (!SessionInterface) { SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle (JoinSessionCompleteDelegateHandle); } MultiplayerOnJoinSessionComplete.Broadcast (Result); }
在菜单类的回调函数中调用ClientTravel
将客户端传送到主机。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void UMenu::OnJoinSession (EOnJoinSessionCompleteResult::Type Result) { IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get (); if (Subsystem) { IOnlineSessionPtr SessionInterface = Subsystem->GetSessionInterface (); if (SessionInterface.IsValid ()) { FString Address; SessionInterface->GetResolvedConnectString (NAME_GameSession, Address); if (Address == FString ()) { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Red, FString (TEXT ("Address not found!" )) ); } } APlayerController* PlayerController = GetGameInstance ()->GetFirstLocalPlayerController (); if (PlayerController) { PlayerController->ClientTravel (Address, ETravelType::TRAVEL_Absolute); } } } }
这样从创建会话到加入会话的功能就全部完成了。
3.5 销毁会话 解绑委托后直接使用在线会话接口销毁会话即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void UMultiplayerSessionsSubsystem::DestroySession () { if (!SessionInterface.IsValid ()) { MultiplayerOnDestroySessionComplete.Broadcast (false ); return ; } DestroySessionCompleteDelegateHandle = SessionInterface->AddOnDestroySessionCompleteDelegate_Handle (DestroySessionCompleteDelegate); if (!SessionInterface->DestroySession (NAME_GameSession)) { SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle (DestroySessionCompleteDelegateHandle); MultiplayerOnDestroySessionComplete.Broadcast (false ); } }
在回调函数中将bCreateSessionOnDestroy
设为false,表示当前没有要销毁的会话,然后通过成员变量保存的信息创建会话。
1 2 3 4 5 6 7 8 9 10 11 void UMultiplayerSessionsSubsystem::OnDestroySessionComplete (FName SessionName, bool bWasSuccessful) { if (SessionInterface) { SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle (DestroySessionCompleteDelegateHandle); } if (bWasSuccessful && bCreateSessionOnDestroy) { bCreateSessionOnDestroy = false ; CreateSession (LastNumPublicConnections, LastMatchType); } MultiplayerOnDestroySessionComplete.Broadcast (bWasSuccessful); }
3.6 收尾 菜单中有一个瑕疵:Host
按钮和Join
按钮可以被反复按下,而在加入会话和搜寻会话的过程中,我们是不希望按钮被按下的,这是我们要限制按钮的功能。主要是通过向按钮的SetIsEnabled
函数中传入true
和false
来控制按钮的可用性。
当按下创建会话按钮后需要将按钮设为不可用,直到回调函数中返回“创建会话失败”再设为可用,表示可以重新尝试创建会话了。
按下加入会话按钮后同样将按钮设为不可用,当回调函数中返回“加入会话失败”再设为可用,表示可以重新尝试加入会话。
当修补了这个小瑕疵后,插件的功能就完成的差不多了。当安装插件后,可以直接通过菜单的两个按钮实现联机,也可以通过调用MultiplayerSessionsSubsystem
来实现更多功能。
4.完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 #pragma once #include "CoreMinimal.h" #include "GameFramework/Character.h" #include "Logging/LogMacros.h" #include "Interfaces/OnlineSessionInterface.h" #include "MenuSystemCharacter.generated.h" class USpringArmComponent ;class UCameraComponent ;class UInputMappingContext ;class UInputAction ;struct FInputActionValue ;DECLARE_LOG_CATEGORY_EXTERN (LogTemplateCharacter, Log, All);UCLASS (config=Game)class AMenuSystemCharacter : public ACharacter{ GENERATED_BODY () UPROPERTY (VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true" )) USpringArmComponent* CameraBoom; UPROPERTY (VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true" )) UCameraComponent* FollowCamera; UPROPERTY (EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true" )) UInputMappingContext* DefaultMappingContext; UPROPERTY (EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true" )) UInputAction* JumpAction; UPROPERTY (EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true" )) UInputAction* MoveAction; UPROPERTY (EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true" )) UInputAction* LookAction; public : AMenuSystemCharacter (); protected : void Move (const FInputActionValue& Value) ; void Look (const FInputActionValue& Value) ; protected : virtual void NotifyControllerChanged () override ; virtual void SetupPlayerInputComponent (class UInputComponent* PlayerInputComponent) override ; public : FORCEINLINE class USpringArmComponent* GetCameraBoom () const { return CameraBoom; } FORCEINLINE class UCameraComponent* GetFollowCamera () const { return FollowCamera; } public : IOnlineSessionPtr OnlineSessionInterface; protected : UFUNCTION (BlueprintCallable) void CreateGameSession () ; UFUNCTION (BlueprintCallable) void JoinGameSession () ; void OnCreateSessionComplete (FName SessionName, bool bWasSussful) ; void OnFindSessionsComplete (bool bWasSuccessful) ; void OnJoinSessionComplete (FName SessionName, EOnJoinSessionCompleteResult::Type Result) ; private : FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate; FOnFindSessionsCompleteDelegate FindSessionsCompleteDelegate; TSharedPtr<FOnlineSessionSearch> SessionSearch; FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 #include "MenuSystemCharacter.h" #include "Engine/LocalPlayer.h" #include "Camera/CameraComponent.h" #include "Components/CapsuleComponent.h" #include "GameFramework/CharacterMovementComponent.h" #include "GameFramework/SpringArmComponent.h" #include "GameFramework/Controller.h" #include "EnhancedInputComponent.h" #include "EnhancedInputSubsystems.h" #include "InputActionValue.h" #include "OnlineSubsystem.h" #include "OnlineSessionSettings.h" #include "Online/OnlineSessionNames.h" DEFINE_LOG_CATEGORY (LogTemplateCharacter);AMenuSystemCharacter::AMenuSystemCharacter () : CreateSessionCompleteDelegate (FOnCreateSessionCompleteDelegate::CreateUObject (this , &ThisClass::OnCreateSessionComplete)), FindSessionsCompleteDelegate (FOnFindSessionsCompleteDelegate::CreateUObject (this , &ThisClass::OnFindSessionsComplete)), JoinSessionCompleteDelegate (FOnJoinSessionCompleteDelegate::CreateUObject (this , &ThisClass::OnJoinSessionComplete)) { GetCapsuleComponent ()->InitCapsuleSize (42.f , 96.0f ); bUseControllerRotationPitch = false ; bUseControllerRotationYaw = false ; bUseControllerRotationRoll = false ; GetCharacterMovement ()->bOrientRotationToMovement = true ; GetCharacterMovement ()->RotationRate = FRotator (0.0f , 500.0f , 0.0f ); GetCharacterMovement ()->JumpZVelocity = 700.f ; GetCharacterMovement ()->AirControl = 0.35f ; GetCharacterMovement ()->MaxWalkSpeed = 500.f ; GetCharacterMovement ()->MinAnalogWalkSpeed = 20.f ; GetCharacterMovement ()->BrakingDecelerationWalking = 2000.f ; GetCharacterMovement ()->BrakingDecelerationFalling = 1500.0f ; CameraBoom = CreateDefaultSubobject <USpringArmComponent>(TEXT ("CameraBoom" )); CameraBoom->SetupAttachment (RootComponent); CameraBoom->TargetArmLength = 400.0f ; CameraBoom->bUsePawnControlRotation = true ; FollowCamera = CreateDefaultSubobject <UCameraComponent>(TEXT ("FollowCamera" )); FollowCamera->SetupAttachment (CameraBoom, USpringArmComponent::SocketName); FollowCamera->bUsePawnControlRotation = false ; IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get (); if (OnlineSubsystem) { OnlineSessionInterface = OnlineSubsystem->GetSessionInterface (); } } void AMenuSystemCharacter::NotifyControllerChanged () { Super::NotifyControllerChanged (); if (APlayerController* PlayerController = Cast <APlayerController>(Controller)) { if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem <UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer ())) { Subsystem->AddMappingContext (DefaultMappingContext, 0 ); } } } void AMenuSystemCharacter::SetupPlayerInputComponent (UInputComponent* PlayerInputComponent) { if (UEnhancedInputComponent* EnhancedInputComponent = Cast <UEnhancedInputComponent>(PlayerInputComponent)) { EnhancedInputComponent->BindAction (JumpAction, ETriggerEvent::Started, this , &ACharacter::Jump); EnhancedInputComponent->BindAction (JumpAction, ETriggerEvent::Completed, this , &ACharacter::StopJumping); EnhancedInputComponent->BindAction (MoveAction, ETriggerEvent::Triggered, this , &AMenuSystemCharacter::Move); EnhancedInputComponent->BindAction (LookAction, ETriggerEvent::Triggered, this , &AMenuSystemCharacter::Look); } else { UE_LOG (LogTemplateCharacter, Error, TEXT ("'%s' Failed to find an Enhanced Input component! This template is built to use the Enhanced Input system. If you intend to use the legacy system, then you will need to update this C++ file." ), *GetNameSafe (this )); } } void AMenuSystemCharacter::CreateGameSession () { if (!OnlineSessionInterface.IsValid ()) { return ; } auto ExistingSession = OnlineSessionInterface->GetNamedSession (NAME_GameSession); if (ExistingSession) { OnlineSessionInterface->DestroySession (NAME_GameSession); } OnlineSessionInterface->AddOnCreateSessionCompleteDelegate_Handle (CreateSessionCompleteDelegate); TSharedPtr<FOnlineSessionSettings> SessionSettings = MakeShareable (new FOnlineSessionSettings ()); SessionSettings->bIsLANMatch = false ; SessionSettings->NumPublicConnections = 4 ; SessionSettings->bAllowJoinInProgress = true ; SessionSettings->bAllowJoinViaPresence = true ; SessionSettings->bShouldAdvertise = true ; SessionSettings->bUsesPresence = true ; SessionSettings->bUseLobbiesIfAvailable = true ; SessionSettings->Set (FName ("MatchType" ), FString ("FreeForAll" ), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing); const ULocalPlayer* localPlayer = GetWorld ()->GetFirstLocalPlayerFromController (); OnlineSessionInterface->CreateSession (*localPlayer->GetPreferredUniqueNetId (), NAME_GameSession, *SessionSettings); } void AMenuSystemCharacter::JoinGameSession () { if (!OnlineSessionInterface.IsValid ()) { return ; } OnlineSessionInterface->AddOnFindSessionsCompleteDelegate_Handle (FindSessionsCompleteDelegate); SessionSearch = MakeShareable (new FOnlineSessionSearch ()); SessionSearch->MaxSearchResults = 10000 ; SessionSearch->bIsLanQuery = false ; SessionSearch->QuerySettings.Set (SEARCH_PRESENCE, true , EOnlineComparisonOp::Equals); const ULocalPlayer* LocalPlayer = GetWorld ()->GetFirstLocalPlayerFromController (); OnlineSessionInterface->FindSessions (*LocalPlayer->GetPreferredUniqueNetId (), SessionSearch.ToSharedRef ()); } void AMenuSystemCharacter::OnCreateSessionComplete (FName SessionName, bool bWasSussful) { if (bWasSussful) { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Blue, FString::Printf (TEXT ("Created session: %s" ),*SessionName.ToString ()) ); } UWorld* world = GetWorld (); if (world) { world->ServerTravel (FString ("/Game/ThirdPerson/Maps/Lobby?listen" )); } } else { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Red, FString (TEXT ("Failed to create session!" )) ); } } } void AMenuSystemCharacter::OnFindSessionsComplete (bool bWasSuccessful) { if (!OnlineSessionInterface.IsValid ()) { return ; } for (auto Result : SessionSearch->SearchResults) { FString Id = Result.GetSessionIdStr (); FString User = Result.Session.OwningUserName; FString MatchType; Result.Session.SessionSettings.Get (FName ("MatchType" ), MatchType); if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Cyan, FString::Printf (TEXT ("Id: %s, User: %s" ), *Id, *User) ); } if (MatchType == FString ("FreeForAll" )) { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Cyan, FString::Printf (TEXT ("Joing Match Type: %s" ), *MatchType) ); } OnlineSessionInterface->AddOnJoinSessionCompleteDelegate_Handle (JoinSessionCompleteDelegate); const ULocalPlayer* localPlayer = GetWorld ()->GetFirstLocalPlayerFromController (); OnlineSessionInterface->JoinSession (*localPlayer->GetPreferredUniqueNetId (), NAME_GameSession, Result); } } } void AMenuSystemCharacter::OnJoinSessionComplete (FName SessionName, EOnJoinSessionCompleteResult::Type Result) { if (!OnlineSessionInterface.IsValid ()) { return ; } FString Address; if (OnlineSessionInterface->GetResolvedConnectString (NAME_GameSession, Address)) { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Yellow, FString::Printf (TEXT ("Connext string: %s" ), *Address) ); } APlayerController* PlayerController = GetGameInstance ()->GetFirstLocalPlayerController (); if (PlayerController) { PlayerController->ClientTravel (Address, ETravelType::TRAVEL_Absolute); } } else { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Yellow, FString::Printf (TEXT ("Address Not Find" )) ); } } void AMenuSystemCharacter::Move (const FInputActionValue& Value) { FVector2D MovementVector = Value.Get <FVector2D>(); if (Controller != nullptr ) { const FRotator Rotation = Controller->GetControlRotation (); const FRotator YawRotation (0 , Rotation.Yaw, 0 ) ; const FVector ForwardDirection = FRotationMatrix (YawRotation).GetUnitAxis (EAxis::X); const FVector RightDirection = FRotationMatrix (YawRotation).GetUnitAxis (EAxis::Y); AddMovementInput (ForwardDirection, MovementVector.Y); AddMovementInput (RightDirection, MovementVector.X); } } void AMenuSystemCharacter::Look (const FInputActionValue& Value) { FVector2D LookAxisVector = Value.Get <FVector2D>(); if (Controller != nullptr ) { AddControllerYawInput (LookAxisVector.X); AddControllerPitchInput (LookAxisVector.Y); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 #pragma once #include "CoreMinimal.h" #include "Subsystems/GameInstanceSubsystem.h" #include "Interfaces/OnlineSessionInterface.h" #include "MultiplayerSessionsSubsystem.generated.h" DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam (FMultiplayerOnCreateSessionComplete, bool , bWasSuccessful);DECLARE_MULTICAST_DELEGATE_TwoParams (FMultiplayerOnFindSessionsComplete, const TArray <FOnlineSessionSearchResult>& SessionResults, bool bWasSuccessful);DECLARE_MULTICAST_DELEGATE_OneParam (FMultiplayerOnJoinSessionComplete, EOnJoinSessionCompleteResult::Type Result);DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam (FMultiplayerOnDestroySessionComplete, bool , bWasSuccessful);DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam (FMultiplayerOnStartSessionComplete, bool , bWasSuccessful);UCLASS ()class MULTIPLAYERSESSIONS_API UMultiplayerSessionsSubsystem : public UGameInstanceSubsystem{ GENERATED_BODY () public : UMultiplayerSessionsSubsystem (); void CreateSession (int32 NumPublicConnections, FString MatchType) ; void FindSessions (int MaxSearchResults) ; void JoinSessions (const FOnlineSessionSearchResult& SessionResult) ; void DestroySession () ; void StartSession () ; FMultiplayerOnCreateSessionComplete MultiplayerOnCreateSessionComplete; FMultiplayerOnFindSessionsComplete MultiplayerOnFindSessionsComplete; FMultiplayerOnJoinSessionComplete MultiplayerOnJoinSessionComplete; FMultiplayerOnDestroySessionComplete MultiplayerOnDestroySessionComplete; FMultiplayerOnStartSessionComplete MultiplayerOnStartSessionComplete; protected : void OnCreateSessionComplete (FName SessionName, bool bWasSuccessful) ; void OnFindSessionsComplete (bool bWasSuccessful) ; void OnJoinSessionComplete (FName SessionName, EOnJoinSessionCompleteResult::Type Result) ; void OnDestroySessionComplete (FName SessionName, bool bWasSuccessful) ; void OnStartSessionComplete (FName SessionName, bool bWasSuccessful) ; private : IOnlineSessionPtr SessionInterface; TSharedPtr<FOnlineSessionSettings> LastSessionSettings; TSharedPtr<FOnlineSessionSearch> LastSessionSearch; FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate; FDelegateHandle CreateSessionCompleteDelegateHandle; FOnFindSessionsCompleteDelegate FindSessionsCompleteDelegate; FDelegateHandle FindSessionsCompleteDelegateHandle; FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate; FDelegateHandle JoinSessionCompleteDelegateHandle; FOnDestroySessionCompleteDelegate DestroySessionCompleteDelegate; FDelegateHandle DestroySessionCompleteDelegateHandle; FOnStartSessionCompleteDelegate StartSessionCompleteDelegate; FDelegateHandle StartSessionCompleteDelegateHandle; bool bCreateSessionOnDestroy = false ; int32 LastNumPublicConnections; FString LastMatchType; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 #include "MultiplayerSessionsSubsystem.h" #include "OnlineSubsystem.h" #include "OnlineSessionSettings.h" #include "Online/OnlineSessionNames.h" UMultiplayerSessionsSubsystem::UMultiplayerSessionsSubsystem (): CreateSessionCompleteDelegate (FOnCreateSessionCompleteDelegate::CreateUObject (this ,&ThisClass::OnCreateSessionComplete)), FindSessionsCompleteDelegate (FOnFindSessionsCompleteDelegate::CreateUObject (this , &ThisClass::OnFindSessionsComplete)), JoinSessionCompleteDelegate (FOnJoinSessionCompleteDelegate::CreateUObject (this , &ThisClass::OnJoinSessionComplete)), DestroySessionCompleteDelegate (FOnDestroySessionCompleteDelegate::CreateUObject (this , &ThisClass::OnDestroySessionComplete)), StartSessionCompleteDelegate (FOnStartSessionCompleteDelegate::CreateUObject (this , &ThisClass::OnStartSessionComplete)) { IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get (); if (Subsystem) { SessionInterface = Subsystem->GetSessionInterface (); } } void UMultiplayerSessionsSubsystem::CreateSession (int32 NumPublicConnections, FString MatchType) { if (!SessionInterface.IsValid ()) { return ; } auto ExistingSession = SessionInterface->GetNamedSession (NAME_GameSession); if (ExistingSession) { bCreateSessionOnDestroy = true ; LastNumPublicConnections = NumPublicConnections; LastMatchType = MatchType; DestroySession (); } CreateSessionCompleteDelegateHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle (CreateSessionCompleteDelegate); LastSessionSettings = MakeShareable (new FOnlineSessionSettings ()); LastSessionSettings->bIsLANMatch = IOnlineSubsystem::Get ()->GetSubsystemName () == "NULL" ; LastSessionSettings->NumPublicConnections = NumPublicConnections; LastSessionSettings->bAllowJoinInProgress = true ; LastSessionSettings->bAllowJoinViaPresence = true ; LastSessionSettings->bShouldAdvertise = true ; LastSessionSettings->bUsesPresence = true ; LastSessionSettings->bUseLobbiesIfAvailable = true ; LastSessionSettings->Set (FName ("MatchType" ), MatchType, EOnlineDataAdvertisementType::ViaOnlineServiceAndPing); LastSessionSettings->BuildUniqueId = 1 ; const ULocalPlayer* LocalPlayer = GetWorld ()->GetFirstLocalPlayerFromController (); if (!SessionInterface->CreateSession (*LocalPlayer->GetPreferredUniqueNetId (), NAME_GameSession, *LastSessionSettings)) { SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle (CreateSessionCompleteDelegateHandle); MultiplayerOnCreateSessionComplete.Broadcast (false ); } } void UMultiplayerSessionsSubsystem::FindSessions (int MaxSearchResults) { if (!SessionInterface.IsValid ()) { return ; } FindSessionsCompleteDelegateHandle = SessionInterface->AddOnFindSessionsCompleteDelegate_Handle (FindSessionsCompleteDelegate); LastSessionSearch = MakeShareable (new FOnlineSessionSearch ()); LastSessionSearch->MaxSearchResults = MaxSearchResults; LastSessionSearch->bIsLanQuery = IOnlineSubsystem::Get ()->GetSubsystemName () == "NULL" ; LastSessionSearch->QuerySettings.Set (SEARCH_PRESENCE, true , EOnlineComparisonOp::Equals); const ULocalPlayer* LocalPlayer = GetWorld ()->GetFirstLocalPlayerFromController (); if (!SessionInterface->FindSessions (*LocalPlayer->GetPreferredUniqueNetId (), LastSessionSearch.ToSharedRef ())) { SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle (FindSessionsCompleteDelegateHandle); MultiplayerOnFindSessionsComplete.Broadcast (TArray <FOnlineSessionSearchResult>(), false ); } } void UMultiplayerSessionsSubsystem::JoinSessions (const FOnlineSessionSearchResult& SessionResult) { if (!SessionInterface.IsValid ()) { MultiplayerOnJoinSessionComplete.Broadcast (EOnJoinSessionCompleteResult::UnknownError); return ; } JoinSessionCompleteDelegateHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle (JoinSessionCompleteDelegate); const ULocalPlayer* LocalPlayer = GetWorld ()->GetFirstLocalPlayerFromController (); if (!SessionInterface->JoinSession (*LocalPlayer->GetPreferredUniqueNetId (), NAME_GameSession, SessionResult)) { SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle (JoinSessionCompleteDelegateHandle); MultiplayerOnJoinSessionComplete.Broadcast (EOnJoinSessionCompleteResult::UnknownError); } } void UMultiplayerSessionsSubsystem::DestroySession () { if (!SessionInterface.IsValid ()) { MultiplayerOnDestroySessionComplete.Broadcast (false ); return ; } DestroySessionCompleteDelegateHandle = SessionInterface->AddOnDestroySessionCompleteDelegate_Handle (DestroySessionCompleteDelegate); if (!SessionInterface->DestroySession (NAME_GameSession)) { SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle (DestroySessionCompleteDelegateHandle); MultiplayerOnDestroySessionComplete.Broadcast (false ); } } void UMultiplayerSessionsSubsystem::StartSession () {} void UMultiplayerSessionsSubsystem::OnCreateSessionComplete (FName SessionName, bool bWasSuccessful) { if (SessionInterface) { SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle (CreateSessionCompleteDelegateHandle); } MultiplayerOnCreateSessionComplete.Broadcast (bWasSuccessful); } void UMultiplayerSessionsSubsystem::OnFindSessionsComplete (bool bWasSuccessful) { if (SessionInterface) { SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle (FindSessionsCompleteDelegateHandle); } if (LastSessionSearch->SearchResults.Num () <= 0 ) { MultiplayerOnFindSessionsComplete.Broadcast (TArray <FOnlineSessionSearchResult>(), false ); if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 1.f , FColor::Red, FString (TEXT ("Session not found!" )) ); } return ; } for (auto & Result : LastSessionSearch->SearchResults) { Result.Session.SessionSettings.bUseLobbiesIfAvailable = true ; } MultiplayerOnFindSessionsComplete.Broadcast (LastSessionSearch->SearchResults, bWasSuccessful); } void UMultiplayerSessionsSubsystem::OnJoinSessionComplete (FName SessionName, EOnJoinSessionCompleteResult::Type Result) { if (!SessionInterface) { SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle (JoinSessionCompleteDelegateHandle); } MultiplayerOnJoinSessionComplete.Broadcast (Result); } void UMultiplayerSessionsSubsystem::OnDestroySessionComplete (FName SessionName, bool bWasSuccessful) { if (SessionInterface) { SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle (DestroySessionCompleteDelegateHandle); } if (bWasSuccessful && bCreateSessionOnDestroy) { bCreateSessionOnDestroy = false ; CreateSession (LastNumPublicConnections, LastMatchType); } MultiplayerOnDestroySessionComplete.Broadcast (bWasSuccessful); } void UMultiplayerSessionsSubsystem::OnStartSessionComplete (FName SessionName, bool bWasSuccessful) {}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 #pragma once #include "CoreMinimal.h" #include "Blueprint/UserWidget.h" #include "Interfaces/OnlineSessionInterface.h" #include "Menu.generated.h" UCLASS ()class MULTIPLAYERSESSIONS_API UMenu : public UUserWidget{ GENERATED_BODY () public : UFUNCTION (BlueprintCallable) void MenuSetup (int32 NumberOfPublicConnections = 4 , FString TypeOfMatch = FString(TEXT("FreeForAll" )), FString LobbyPath = FString(TEXT("/Game/ThirdPerson/Maps/Lobby" ))) ; protected : virtual bool Initialize () override ; virtual void NativeDestruct () override ; UFUNCTION () void OnCreateSession (bool bWasSuccessful) ; void OnFindSessions (const TArray <FOnlineSessionSearchResult>& SessionResults, bool bWasSuccessful) ; void OnJoinSession (EOnJoinSessionCompleteResult::Type Result) ; UFUNCTION () void OnDestroySession (bool bWasSuccessful) ; UFUNCTION () void OnStartSession (bool bWasSuccessful) ; private : UPROPERTY (meta = (BindWidget)) class UButton * HostButton; UPROPERTY (meta = (BindWidget)) UButton* JoinButton; UFUNCTION () void HostButtonClicked () ; UFUNCTION () void JoinButtonClicked () ; void MenuTearDown () ; class UMultiplayerSessionsSubsystem * MultiplayerSessionsSubsystem; int32 NumPublicConnections{ 4 }; FString MatchType{ TEXT ("FreeForAll" ) }; FString PathToLobby{ TEXT ("" ) }; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 #include "Menu.h" #include "Components/Button.h" #include "MultiplayerSessionsSubsystem.h" #include "OnlineSessionSettings.h" #include "OnlineSubsystem.h" void UMenu::MenuSetup (int32 NumberOfPublicConnections, FString TypeOfMatch, FString LobbyPath) { PathToLobby = FString::Printf (TEXT ("%s?listen" ), *LobbyPath); NumPublicConnections = NumberOfPublicConnections; MatchType = TypeOfMatch; AddToViewport (); SetVisibility (ESlateVisibility::Visible); bIsFocusable = true ; UWorld* World = GetWorld (); if (World) { APlayerController* PlayerController = World->GetFirstPlayerController (); if (PlayerController) { FInputModeUIOnly InputModeData; InputModeData.SetWidgetToFocus (TakeWidget ()); InputModeData.SetLockMouseToViewportBehavior (EMouseLockMode::DoNotLock); PlayerController->SetInputMode (InputModeData); PlayerController->SetShowMouseCursor (true ); } } UGameInstance* GameInstance = GetGameInstance (); if (GameInstance) { MultiplayerSessionsSubsystem = GameInstance->GetSubsystem <UMultiplayerSessionsSubsystem>(); } if (MultiplayerSessionsSubsystem) { MultiplayerSessionsSubsystem->MultiplayerOnCreateSessionComplete.AddDynamic (this , &ThisClass::OnCreateSession); MultiplayerSessionsSubsystem->MultiplayerOnFindSessionsComplete.AddUObject (this , &ThisClass::OnFindSessions); MultiplayerSessionsSubsystem->MultiplayerOnJoinSessionComplete.AddUObject (this , &ThisClass::OnJoinSession); MultiplayerSessionsSubsystem->MultiplayerOnDestroySessionComplete.AddDynamic (this , &ThisClass::OnDestroySession); MultiplayerSessionsSubsystem->MultiplayerOnStartSessionComplete.AddDynamic (this , &ThisClass::OnStartSession); } } bool UMenu::Initialize () { if (!Super::Initialize ()) { return false ; } if (HostButton) { HostButton->OnClicked.AddDynamic (this , &ThisClass::HostButtonClicked); } if (JoinButton) { JoinButton->OnClicked.AddDynamic (this , &ThisClass::JoinButtonClicked); } return true ;; } void UMenu::NativeDestruct () { MenuTearDown (); Super::NativeDestruct (); } void UMenu::OnCreateSession (bool bWasSuccessful) { if (bWasSuccessful) { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Yellow, FString (TEXT ("Session created successfully!" )) ); } UWorld* World = GetWorld (); if (World) { World->ServerTravel (PathToLobby); } } else { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Red, FString (TEXT ("Failed to create session!" )) ); } HostButton->SetIsEnabled (true ); } } void UMenu::OnFindSessions (const TArray<FOnlineSessionSearchResult>& SessionResults, bool bWasSuccessful) { if (!MultiplayerSessionsSubsystem) { return ; } for (const auto & Result : SessionResults) { FString SettingsValue; Result.Session.SessionSettings.Get (FName ("MatchType" ), SettingsValue); if (SettingsValue == MatchType) { MultiplayerSessionsSubsystem->JoinSessions (Result); return ; } } if (!bWasSuccessful || SessionResults.Num () == 0 ) { JoinButton->SetIsEnabled (true ); } } void UMenu::OnJoinSession (EOnJoinSessionCompleteResult::Type Result) { IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get (); if (Subsystem) { IOnlineSessionPtr SessionInterface = Subsystem->GetSessionInterface (); if (SessionInterface.IsValid ()) { FString Address; SessionInterface->GetResolvedConnectString (NAME_GameSession, Address); if (Address == FString ()) { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Red, FString (TEXT ("Address not found!" )) ); } } APlayerController* PlayerController = GetGameInstance ()->GetFirstLocalPlayerController (); if (PlayerController) { PlayerController->ClientTravel (Address, ETravelType::TRAVEL_Absolute); } } } if (Result != EOnJoinSessionCompleteResult::Success) { JoinButton->SetIsEnabled (true ); } } void UMenu::OnDestroySession (bool bWasSuccessful) {} void UMenu::OnStartSession (bool bWasSuccessful) {} void UMenu::HostButtonClicked () { HostButton->SetIsEnabled (false ); if (MultiplayerSessionsSubsystem) { MultiplayerSessionsSubsystem->CreateSession (NumPublicConnections, MatchType); } } void UMenu::JoinButtonClicked () { JoinButton->SetIsEnabled (false ); if (MultiplayerSessionsSubsystem) { MultiplayerSessionsSubsystem->FindSessions (10000 ); } else { if (GEngine) { GEngine->AddOnScreenDebugMessage ( -1 , 15.f , FColor::Yellow, FString (TEXT ("MultiplayerSessionsSubsystem not found!" )) ); } } } void UMenu::MenuTearDown () { RemoveFromParent (); UWorld* World = GetWorld (); if (World) { APlayerController* PlayerController = World->GetFirstPlayerController (); if (PlayerController) { FInputModeGameOnly InputModeData; PlayerController->SetInputMode (InputModeData); PlayerController->SetShowMouseCursor (false ); } } }