【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”的关卡。在角色蓝图中实现以下功能:

image-20250704221601626

打包项目,并将其拷贝到客机。客机需要和主机连接到同一局域网。

在主机按下“1”后,主机会加载关卡“Lobby”,Options中的“listen”表明要以监听服务器身份运行,它会打开 socket 接口等待其他玩家连接。随后在客机按下”2“,游戏会执行命令“Open 121.48.198.53”,客户端尝试通过 socket 连接到该地址运行的 listen server。如果目标IP是监听服务器,它会接受连接,并将客户端加载到正确的地图中。

完成上面的操作后,就可以看到两台电脑的角色在一起活蹦乱跳了。

image-20250704222653264

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);
}
}

逻辑和上一小节基本上是一样的,只不过多了一种用于连接主机的方法。用相同的方式进行测试。

image-20250704222414649

联机成功。

image-20250704222825472

目前我们实现了最基础的局域网联机。使用 UE 提供的 ServerTravelClientTravel 方法,我们可以让一台设备以 listen server 的身份运行,并允许其他局域网内的设备通过 IP 地址连接进来。虽然方式简单直接,但依赖于网络环境,难以在公网环境中稳定使用。为了解决这一问题,我们将在下一节引入 UE 的 OnlineSubsystem模块,并通过 Steam 实现跨网段联机。

2 测试通过Steam进行联机

在线子系统(Online Subsystem) 及其接口提供一种可访问SteamXbox LiveFacebook等在线服务功能的常用方法。开发一款在多平台上发行或支持多在线服务的游戏时,在线子系统可确保开发者唯一需要做的变更就是对所有支持的服务进行配置调整。UE引擎提供了一系列针对不同平台的Online Subsystem插件,而本项目中使用的就是针对Steam平台的插件,它提供了底层 SteamNetDriverSteamSessionSteamAPI调用封装等模块。

Online Subsystem in Unreal Engine | 虚幻引擎 5.5 文档 | Epic Developer Community

虚幻引擎Steam在线子系统接口 | 虚幻引擎 5.5 文档 | Epic Developer Community

详细的使用方法可以参照官方文档。

2.1 配置

创建新的项目后,我们需要开启这个插件:

image-20250704223838193

并且在配置文件(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

; If using Sessions
; bInitServerOnClient=true

[/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
//MenuSystemCharacter.h
IOnlineSessionPtr OnlineSessionInterface;

//MenuSystemCharacter.cpp
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 子系统。

image-20250704230838975

2.3 创建会话

获得了会话接口后,就可以用其提供的各种方法进行对话的创建、寻找、加入、销毁等操作,从而实现联机。首先从创建会话开始。

创建会话的流程包含三个步骤:

  • 绑定创建会话委托(用于接收异步回调)

  • 构造会话设置对象(设置连接数、匹配类型等)

  • 调用在线子系统接口创建会话(异步操作)

我声明了一个创建会话的接口函数和一个回调函数,以及一个委托。简单来说,委托可以绑定一个(或多个,根据委托的类型来决定)回调函数,我们可以对委托进行(手动或自动)广播,这样所有和这个委托绑定的回调函数都会被调用。委托是实现异步通信的一个常用方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected:
UFUNCTION(BlueprintCallable)
void CreateGameSession();

/**
* Delegate fired when a session create request has completed
*
* @param SessionName the name of the session this callback is for
* @param bWasSuccessful true if the async action completed without error, false if there was an error
*/
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()
{
//按“1”调用函数

//检查指针是否有效
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; //是否允许通过 Steam 好友加入
SessionSettings->bShouldAdvertise = true; //是否将房间暴露出去
SessionSettings->bUsesPresence = true; //是否使用“在线状态系统”
SessionSettings->bUseLobbiesIfAvailable = true; //是否使用 Steam Lobby 功能
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(); //获得会话ID
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)
);
}

//检查比赛类型是否是FreeForAll
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 配置

创建一个空白插件。

image-20250706224649885

我们需要声明这个插件需要依赖的插件。本插件依赖于官方插件OnlineSubsystemOnlineSubsystemSteam,因此需要在插件的.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"
// ... add other public dependencies that you statically link with here ...
}
);

这是告诉 Unreal Build Tool(UBT)本插件在编译时需要链接 OnlineSubsystem 和 Steam 的 C++ 模块,否则在代码中引用 IOnlineSessionFOnlineSessionSettings 等类会编译失败。

3.2 创建自己的类

这里我画了一个图,用于方便看懂插件中的四大部分之间是如何交互的。之后的所有内容都遵循这个流程,因为具体功能实现方式和上面其实差不多,所以只做简单讲解。

image-20250709232702573

插件的实现主要依靠实现一个自己的类:MultiplayerSessionsSubsystem类,它继承自游戏实例子系统类(UGameInstanceSubsystem)。UGameInstanceSubsystem 是 Unreal Engine 提供的一种全局生命周期子系统,它在游戏启动后就存在,并贯穿整个游戏生命周期,不会因为关卡切换而销毁MultiplayerSessionsSubsystem类是插件的核心,主要提供了一些对于会话的操作的接口以及相应操作的委托,只要使用这些接口和委托就可以快速实现联机。

首先需要获取在线会话接口并进行初始化:

1
2
3
4
5
6
7
8
//MultiplayerSessionsSubsystem.h
OnlineSessionPtr SessionInterface;

//MultiplayerSessionsSubsystem.cpp
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();

//
// 外部接口:供 UI 或其他模块调用的多人联机会话控制函数
//
void CreateSession(int32 NumPublicConnections, FString MatchType);
void FindSessions(int MaxSearchResults);
void JoinSessions(const FOnlineSessionSearchResult& SessionResult);
void DestroySession();
void StartSession();

protected:
//
// 回调函数:会被 OnlineSubsystem 在异步操作完成后自动调用
//
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;

//
// 委托声明+绑定:将回调函数挂载到OnlineSubsystem的事件通知中
// 每个委托都有对应的DelegateHandle用于解绑(防止重复绑定)
//
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);
//SetFocus();
bIsFocusable = true; //按理说这一步在新版本的UE中被弃用了,应该被替换成上面的SetFocus函数,并且我最开始也是这么做的,但是后面莫名其妙又可以用这个方法了,并且还是有效的

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);
}
}
}

菜单的创建属于基础内容,直接快进。

image-20250707220712885

在菜单类中对这两个按钮进行绑定,方法是使用元数据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
//MenuSetup函数
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
//MultiplayerSessionsSubsystem.h
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; //是否允许通过 Steam 好友加入
LastSessionSettings->bShouldAdvertise = true; //是否将房间暴露出去
LastSessionSettings->bUsesPresence = true; //是否使用“在线状态系统”
LastSessionSettings->bUseLobbiesIfAvailable = true; //是否使用 Steam Lobby 功能(新版UE需要启用这个功能才能创建会话)
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按钮后,我们要做的就是调用MultiplayerSessionsSubsystemCreateSession接口,将上面声明的变量传递过去用于创建会话。

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
//MultiplayerSessionsSubsystem.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);

public:
//
// 我们自己自定义的委托,菜单类的回调函数可以绑定这个委托
//
FMultiplayerOnCreateSessionComplete MultiplayerOnCreateSessionComplete;
FMultiplayerOnFindSessionsComplete MultiplayerOnFindSessionsComplete;
FMultiplayerOnJoinSessionComplete MultiplayerOnJoinSessionComplete;
FMultiplayerOnDestroySessionComplete MultiplayerOnDestroySessionComplete;
FMultiplayerOnStartSessionComplete MultiplayerOnStartSessionComplete;

//Menu.h
protected:
//
// 绑定到MultiplayerSessionsSubsystem类的自定义委托的回调函数
//
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 声明,并且参数类型必须是反射系统可识别的类型(UCLASSUSTRUCT、基础类型等)。优点是可序列化、能与蓝图交互;缺点是运行时开销略大(使用 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函数中传入truefalse来控制按钮的可用性。

当按下创建会话按钮后需要将按钮设为不可用,直到回调函数中返回“创建会话失败”再设为可用,表示可以重新尝试创建会话了。

按下加入会话按钮后同样将按钮设为不可用,当回调函数中返回“加入会话失败”再设为可用,表示可以重新尝试加入会话。

当修补了这个小瑕疵后,插件的功能就完成的差不多了。当安装插件后,可以直接通过菜单的两个按钮实现联机,也可以通过调用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
//MenuSystemCharacter.h
#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()

/** Camera boom positioning the camera behind the character */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
USpringArmComponent* CameraBoom;

/** Follow camera */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
UCameraComponent* FollowCamera;

/** MappingContext */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputMappingContext* DefaultMappingContext;

/** Jump Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* JumpAction;

/** Move Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* MoveAction;

/** Look Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* LookAction;

public:
AMenuSystemCharacter();


protected:

/** Called for movement input */
void Move(const FInputActionValue& Value);

/** Called for looking input */
void Look(const FInputActionValue& Value);


protected:

virtual void NotifyControllerChanged() override;

virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

public:
/** Returns CameraBoom subobject **/
FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
/** Returns FollowCamera subobject **/
FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }

public:
//在线会话接口的指针
IOnlineSessionPtr OnlineSessionInterface;

protected:
UFUNCTION(BlueprintCallable)
void CreateGameSession();

UFUNCTION(BlueprintCallable)
void JoinGameSession();

/**
* Delegate fired when a session create request has completed
*
* @param SessionName the name of the session this callback is for
* @param bWasSuccessful true if the async action completed without error, false if there was an error
*/
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
//MenuSystemCharacter.cpp
#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::AMenuSystemCharacter() :
CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete)),
FindSessionsCompleteDelegate(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete)),
JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete))
{
// Set size for collision capsule
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);

// Don't rotate when the controller rotates. Let that just affect the camera.
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;

// Configure character movement
GetCharacterMovement()->bOrientRotationToMovement = true; // Character moves in the direction of input...
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f); // ...at this rotation rate

// Note: For faster iteration times these variables, and many more, can be tweaked in the Character Blueprint
// instead of recompiling to adjust them
GetCharacterMovement()->JumpZVelocity = 700.f;
GetCharacterMovement()->AirControl = 0.35f;
GetCharacterMovement()->MaxWalkSpeed = 500.f;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f;

// Create a camera boom (pulls in towards the player if there is a collision)
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 400.0f; // The camera follows at this distance behind the character
CameraBoom->bUsePawnControlRotation = true; // Rotate the arm based on the controller

// Create a follow camera
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // Attach the camera to the end of the boom and let the boom adjust to match the controller orientation
FollowCamera->bUsePawnControlRotation = false; // Camera does not rotate relative to arm

// Note: The skeletal mesh and anim blueprint references on the Mesh component (inherited from Character)
// are set in the derived blueprint asset named ThirdPersonCharacter (to avoid direct content references in C++)

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())
);
}*/
}
}

//////////////////////////////////////////////////////////////////////////
// Input

void AMenuSystemCharacter::NotifyControllerChanged()
{
Super::NotifyControllerChanged();

// Add Input Mapping Context
if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultMappingContext, 0);
}
}
}

void AMenuSystemCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
// Set up action bindings
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {

// Jumping
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);

// Moving
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMenuSystemCharacter::Move);

// Looking
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()
{
//按“1”调用函数

//检查指针是否有效
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; //是否允许通过 Steam 好友加入
SessionSettings->bShouldAdvertise = true; //是否将房间暴露出去
SessionSettings->bUsesPresence = true; //是否使用“在线状态系统”
SessionSettings->bUseLobbiesIfAvailable = true; //是否使用 Steam Lobby 功能
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(); //获得会话ID
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)
);
}

//检查比赛类型是否是FreeForAll
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)
{
// input is a Vector2D
FVector2D MovementVector = Value.Get<FVector2D>();

if (Controller != nullptr)
{
// find out which way is forward
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);

// get forward vector
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);

// get right vector
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

// add movement
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
}

void AMenuSystemCharacter::Look(const FInputActionValue& Value)
{
// input is a Vector2D
FVector2D LookAxisVector = Value.Get<FVector2D>();

if (Controller != nullptr)
{
// add yaw and pitch input to controller
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
//MultiplayerSessionsSubsystem.h
#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();

//
// 外部接口:供 UI 或其他模块调用的多人联机会话控制函数
//
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:
//
// 回调函数:会被 OnlineSubsystem 在异步操作完成后自动调用
//
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;

//
// 委托声明+绑定:将回调函数挂载到OnlineSubsystem的事件通知中
// 每个委托都有对应的DelegateHandle用于解绑(防止重复绑定)
//
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
//MultiplayerSessionsSubsystem.cpp
#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; //是否允许通过 Steam 好友加入
LastSessionSettings->bShouldAdvertise = true; //是否将房间暴露出去
LastSessionSettings->bUsesPresence = true; //是否使用“在线状态系统”
LastSessionSettings->bUseLobbiesIfAvailable = true; //是否使用 Steam Lobby 功能(新版UE需要启用这个功能才能创建会话)
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
//Menu.h
#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;

//
// 绑定到MultiplayerSessionsSubsystem类的自定义委托的回调函数
//
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
//Menu.cpp
#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);
//SetFocus();
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()
{
/*if (GEngine) {
GEngine->AddOnScreenDebugMessage(
-1,
15.f,
FColor::Yellow,
FString(TEXT("Host Button Clicked"))
);
}*/

HostButton->SetIsEnabled(false);
if (MultiplayerSessionsSubsystem) {
MultiplayerSessionsSubsystem->CreateSession(NumPublicConnections, MatchType);
}
}

void UMenu::JoinButtonClicked()
{
/*if (GEngine) {
GEngine->AddOnScreenDebugMessage(
-1,
15.f,
FColor::Yellow,
FString(TEXT("Join Button Clicked"))
);
}*/

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);
}
}
}