一、Creating a Multiplayer Plugin
1. Multiplayer Concepts
unreal engine multiplayer: authoritative client - server 模式:
可以根据配置来选择用哪种模式(监听服务器还是专用服务器模式)
2. Testing Multiplayer
2.1 Testing in editor
play按钮右侧的三个点中,可以改变number of player
Net Mode:
play standalone 单机模式
play as listen server,编辑器作为监听服务器
play as client 会创建一个专用服务器
2.2 setting up lan connection局域网
创建一个新的level:lobby(大厅),让其他玩家通过局域网连接进入(file->new level, save current level)
进入Third Person Character蓝图,创建蓝图,按1键打开新level,option填listen,作为监听服务器,2键执行command命令,输入Open 172.22.26.21
获取自己电脑的ip,打开cmd,输入ipconfig得到ipv4地址:IPv4 地址 . . . . . . . . . . . . : 172.22.26.21
打包出package project,放到新建的Build文件夹中
使用两台机器测试,连接同一个网络
3. LAN Connection
3.1 C++创建局域网连接
Source -> MultiplyShooter -> MultiplyShooter.h(打错了,不过没关系,这个项目是用来测试多人玩家的)创建几个函数
1 2 3 4 5 6 7
//可以在蓝图中创建节点 UFUNCTION(BlueprintCallable) void OpenLobby(); UFUNCTION(BlueprintCallable) void CallOpenLevel(const FString& Address); UFUNCTION(BlueprintCallable) void CallClientTravel(const FString& Address);
在cpp文件实现
在官方文档查openlevel函数,加入官方文档中提到的头文件
#include "Kismet/GameplayStatics.h"
这里面的CallOpenLevel()和CallClientTravel()函数的功能是一样的
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 AMutiplyShooterCharacter::OpenLobby() { UWorld* World = GetWorld(); if (World) { //右键level->get file path : E:/Ue5/MutiplyShooter/Content/ThirdPerson/Maps/Lobby.umap World->ServerTravel("/Game/ThirdPerson/Maps/Lobby?listen"); } } void AMutiplyShooterCharacter::CallOpenLevel(const FString& Address) { //Address是FString类型,加*就成了C风格的字符串,这个可以隐式地创建FName对象 UGameplayStatics::OpenLevel(this, *Address); } //让角色传送到ip地址为address的房间去 void AMutiplyShooterCharacter::CallClientTravel(const FString& Address) { //获取本地角色控制器 APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController(); if (PlayerController) { PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute); } }
记得build后再回到编辑器(在vs里如果编译不成功,比如报错Unable to build while Live Coding is active. Exit the editor and game, or press Ctrl+Alt+F11 if iterating on code in the editor or game MutiplyShooter ,就在编辑器里按CTRL+ALT+F11来手动编译,可以编译成功)(或者在Edit -> editor preference中搜索live coding,关掉,就能在vs编译了,而且这样就能打包了,不然用live coding没发打包)
进入角色控制器蓝图
打包测试(测试失败可能是ip地址错误的问题)
4. Online Subsystem
为了不输入对方的IP地址就同世界各地的人们一起游戏,需要使用一个服务器服务,比如steam,又为了避免使用不同服务器代码库不同带来的影响,虚幻引擎抽象了一层Online Subsystem,以至于只用写一遍代码,打包成一个插件就可以在配置后连接各种类型服务器的服务,因为虚幻引擎的在线子系统处理了其中的细节。
5. Online Sessions
5.1 online subsystem在线子系统
在线子系统提供了一种访问在线平台服务功能的方式。在线平台,是指像Steam和Xbox Lives等这样的东西。这些平台中的每一个都有自己的一套服务支持,如朋友、成就。设置匹配会话,等等。在线子系统包含一组接口,旨在处理每个平台的这些不同服务。因此无论我们选择哪种服务,都可以用在线子系统来处理我们对这些接口的使用。我们所要做的就是为一个特定的平台配置我们的项目。
5.2 session interface会话接口
会话接口处理创建、管理和销毁游戏会话。它还处理搜索会话和其他匹配功能。一个会话可以被认为是游戏的一个实例,在服务器上运行,有一系列的属性,并且一个会话可以被公布,以便其他玩家可以找到这个会话并加入进来或者是私人的,所以只有被邀请的人才能加入游戏。
一个典型的游戏会话的基本寿命是这样的。
首先,你用一组所需的设置来创建会话。
然后你等待其他玩家的加入,并在他们进来的时候注册每个人。
一旦有足够的玩家加入,你就开始会话。
然后每个玩家都在同一个会话中玩游戏。
一旦比赛结束,你就可以结束会话并取消玩家的注册。
然后你可以更新会话,改变比赛的设置。
会话接口函数:CreateSession(), FindSession(), JoinSession(), StartSession(), DestroySession()
5.3 game plan
我们的目标是能够在我们游戏的菜单上点击一个按钮。现在,我们先假设我们有两个菜单按钮,Host和Join。
当点击host时,我们的代码将配置会话设置,然后调用会话接口函数创建会话。一旦完成这些,我们就可以打开大厅关卡,等待其他玩家加入。
然后当别人开始游戏并点击加入,我们将配置一些搜索设置。组属性将有助于过滤掉我们没有兴趣加入的任何游戏会话。然后我们将调用接口函数查找会话。这将返回一些搜索结果,我们将遍历这些结果并挑选一个有效的会话。一旦我们完成了这一工作,我们就能得到适当的P地址,我们可以用ClientTravel()函数来使用。好了,然后用这个函数去旅行到其他玩家那里,收听服务器,并与他们一起加入大厅关卡。
现在要把所有这些功能放到它自己的整洁的小类中,用来处理这些会话相关的功能。但我们还不想因为创建所有这些新的类而被淹没。现在,我们将简单地从角色类中访问在线子系统。并在那里调用这些函数,只是为了看看一切是如何运作的。一旦找们对如何便用在线子系统以及它的会话接口功能有了了解。然后创建我们自己的类来处理这些事情,我们将把它设计成可以用于任何我们希望使用它的游戏中。
6. Configure For Steam
6.1 creating a new project
创建Third Person项目(C++,starter content),MenuSystem
配置steam
edit -> plugins, 搜索online subsystem steam,勾选enable,然后restart
实际启用steam模块,进入vs,Source -> MenuSystem -> MenuSystem.Build.cs,在
PublicDependencyModuleNames
中添加OnlineSubsystemSteam
和OnlineSubsystem
1
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "OnlineSubsystemSteam", "OnlineSubsystem" });
6.2 configure the project for steam
打开项目文件夹->config->defalutEngine.ini,添加内容
去文档中找到 Online Subsystem Steam,里面有需要粘贴的内容
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"
关闭vs和项目,到项目文件夹,删除Saved、Intermediate和Binaries文件夹
右键MenuSystem.uproject,generate visual studio project files
双击MenuSystem.uproject,会提示丢失文件,点击yes就会重新生成
接下来回到项目即可
7. Accessing the Online Subsystem
进入MenuSystemCharacter.h,在末尾添加关于会话的public部分
1 2 3 4 5
public: // 在线对话接口的指针 // IOnlineSessionPtr OnlineSessionInterface; // IOnlineSessionPtr是TSharedPtr<class IOnlineSession, ESPMode::ThreadSafe>的别名 TSharedPtr<class IOnlineSession, ESPMode::ThreadSafe> OnlineSessionInterface;
进入MenuSystemCharacter.cpp,到构造函数的底部,添加代码访问在线子系统
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get(); if (OnlineSubsystem) { //获取在线会话的接口 OnlineSessionInterface = OnlineSubsystem->GetSessionInterface(); //检测是否找到在线子系统 if (GEngine) { GEngine->AddOnScreenDebugMessage( -1,//不会清除掉之前打印的内容 15.f,//持续15s FColor::Blue, FString::Printf(TEXT("Found subsystem %s"), *OnlineSubsystem->GetSubsystemName().ToString())//GetSubsystemName()得到FName,ToString得到FString,再加个*得到c风格字符串 ); } }
前往文档中的IOnlineSubsystem,去找到需要添加的头文件,并加在cpp文件中
#include "OnlineSubsystem.h"
到文档中搜索 IOnlineSession,将需要的头文件添加在cpp文件中
#include "Interfaces/OnlineSessionInterface.h"
如果收到报错Unable to delete hot-reload file(但我直接编译成功了,没报错),需要关闭vs和引擎,
删除Saved、Intermediate和Binaries文件夹
右键MenuSystem.uproject,generate visual studio project files
双击MenuSystem.uproject,会提示丢失文件,点击yes就会重新生成
登录steam
在编辑器运行会连接不上,打包后运行就可以连接上
8. Creating a Session
8.1 Delegate and callback
在MenuSystemCharacter.h中添加函数(protected)
1 2 3
protected: UFUNCTION(BlueprintCallable) void CreateGameSession();
在MenuSystemCharacter.h中创建一个委托变量(private)
1 2
private: FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate;
此时会报错:“CreateSessionCompleteDelegate”: 未知重写说明符 MenuSystem
因为FOnCreateSessionCompleteDelegate是一个typedef的名字,这时可以像之前IOnlineSessionPtr一样写它的原本名字,也可以直接把cpp文件中的
#include "Interfaces/OnlineSessionInterface.h"
剪切到头文件去。要注意:这个头文件要放在#include "MenuSystemCharacter.generated.h"
的上方,这时也可以把TSharedPtr<class IOnlineSession, ESPMode::ThreadSafe>
换回IOnlineSessionPtr
了在MenuSystemCharacter.h的protected下创建回调函数来和刚才创建的委托变量绑定
1
void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
薛定谔的编译,刚才编译报错,只是往
#include "Interfaces/OnlineSessionInterface.h"
头文件下加了一个回车,就编译成功了。
8.2 Bind the callback
- 在构造函数处为委托变量初始化
AMenuSystemCharacter::AMenuSystemCharacter(): CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete))
其中,ThisClass就是这个类名的typedef
8.3 Create a game session
去cpp文件中构建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
void AMenuSystemCharacter::CreateGameSession() { //当按1时执行回调 if (!OnlineSessionInterface.IsValid()) { return; } //检测当前是否有对话 auto ExitingSession = OnlineSessionInterface->GetNamedSession(NAME_GameSession); //如果已经有会话 if (ExitingSession != nullptr) { OnlineSessionInterface->DestroySession(NAME_GameSession); } //将委托加入委托列表 OnlineSessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate); //使用MakeShareable将指针创建为Tshared类型的指针 TSharedPtr<FOnlineSessionSettings> SessionSettings = MakeShareable(new FOnlineSessionSettings()); //设置会话设置 SessionSettings->bIsLANMatch = false;//不是lan连接 SessionSettings->NumPublicConnections = 4;//可以连接的最大人数 SessionSettings->bAllowJoinInProgress = true;//允许中途加入 SessionSettings->bAllowJoinViaPresence = true;//允许不同地区的人加入 SessionSettings->bShouldAdvertise = true;//steam会显示房间供玩家加入 SessionSettings->bUsesPresence = true;//显示地区 //获取角色控制器的指针 const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); OnlineSessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *SessionSettings); }
去文档查找FOnlineSessionSettings函数的头文件,并加入cpp文件中。
8.4 Print the session name
去构建回调函数OnCreateSessionComplete()
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 AMenuSystemCharacter::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful) { if (bWasSuccessful)//如果成功创建会话 { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1,//不清除之前的debug 15.f, FColor::Blue, FString::Printf(TEXT("Created ssesion : %s"), *SessionName.ToString()) ); } } else//如果没有成功创建会话 { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1,//不清除之前的debug 15.f, FColor::Red, FString::Printf(TEXT("Failed to create session!")) ); } } }
前往角色蓝图,按1键执行create game session节点
falied create原因:UE5.0.2有些许不同,需要把配置文件中的部分改一下:
1 2 3 4 5 6
[OnlineSubsystemSteam] bEnabled=true SteamDevAppId=480 ; If using Sessions ; bInitServerOnClient=true
改为:
1 2 3 4
[OnlineSubsystemSteam] bEnabled=true SteamDevAppId=480 bInitServerOnClient=true
9. Setup for Joining Game Sessions
9.1 JoinGameSession()
在MenuSystem.h文件中protected下创建一个蓝图节点函数
1 2
UFUNCTION(BlueprintCallable) void JoinGameSession();
去cpp文件中实现函数(会在之后实现)
9.2 Delegate and callback
接下来会像创建会话时一样创建委托和回调,在MenuSystem.h文件中的private下创建join会话的委托变量。
1
FOnFindSessionsCompleteDelegate FindSessionsCompleteDelagate;
在protected下创建回调函数
1
void OnFindSessionsComplete(bool bWasSuccessful);
去cpp文件实现回调
9.3 Binding the callback
在cpp的构造函数处为委托绑定回调
FindSessionsCompleteDelegate(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete))
9.4 Session search settings
实现JoinGameSession函数,(其中SessionSearch在头文件中的private下创建变量
TSharedPtr<FOnlineSessionSearch> SessionSearch
,因为要在回调函数中使用这个变量来获取会话结果数组)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
void AMenuSystemCharacter::JoinGameSession() { //寻找会话 //先判断在线会话接口的有效性 if (!OnlineSessionInterface.IsValid()) { return; } //将委托变量加入委托列表 OnlineSessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegate); //创建会话设置的智能指针,将类的指针makesharable SessionSearch = MakeShareable(new FOnlineSessionSearch()); SessionSearch->MaxSearchResults = 1000;//最大搜索个数 SessionSearch->bIsLanQuery = false;//禁止局域网的搜索 //获取角色控制器 const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); //执行在线会话接口的寻找会话的函数 OnlineSessionInterface->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), SessionSearch.ToSharedRef());//将ptr转为ref }
实现回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
void AMenuSystemCharacter::OnFindSessionsComplete(bool bWasSuccessful) { //遍历寻找到的会话数组 for (auto Result : SessionSearch->SearchResults) { FString Id = Result.GetSessionIdStr();//会话Id FString User = Result.Session.OwningUserName;//会话创建者的名字 //打印出来 if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Cyan, FString::Printf(TEXT("Id : %s, User : %s"), *Id, *User) ); } } }
到JoinGameSession函数中,在找之前添加一个SessionSearch的设置
1 2
//确保搜索的 会话询问 设置为 现在存在 SessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);
去蓝图中添加节点,按2时执行JoinGameSession
由于5.0.2版本问题导致出错,需要在CreateSession函数中添加一条SessionSettings:
SessionSettings->bUseLobbiesIfAvailable = true;
而且要regenerate(可能就是没有重新生成,所以失败了)打包,用两台机器测试
在joingamesession的函数里,
MakeShareable(new FOnlineSessionSearch());
这处代码的new FOnlineSessionSearch的后面一开始没有加(),也没报错,不知道是不是这里的原因导致找不到会话
10. Steam Regions
之前一直搜索不到人,一直以为是代码哪里出问题了,原来不是代码出问题了,是steam区域的问题,这个搜索只能搜索到同一区域的人,要到steamm的设置中去更改
11. Joining the Session
11.1 Create a lobby level
在Maps文件夹中file->new level,将地板scale放大点,保存为Lobby
在OnCreateSessionComplete函数中添加跳转代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
if (bWasSuccessful)//如果成功创建会话 { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1,//不清除之前的debug 15.f, FColor::Blue, FString::Printf(TEXT("Created ssesion : %s"), *SessionName.ToString()) ); } UWorld* World = GetWorld(); if (World) { //E:/Ue5/MenuSystem/Content/ThirdPerson/Maps/Lobby.umap World->ServerTravel(FString("/Game/ThirdPerson/Maps/Lobby?listen"));//作为监听服务器 } }
11.2 Specify a “match type”
在CreateGameSession()函数中添加一条设置
1
2
//设置游戏模式
SessionSettings->Set(FName("MatchType"), FString("FreeForAll"), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);
11.3 Check the match type
在OnFindSessionsComplete函数的遍历中,获取matchtype,并检查
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
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)
);
}
if (MatchType == FString("FreeForAll"))
{
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(
-1,
15.f,
FColor::Cyan,
FString::Printf(TEXT("Joing Match Type : %s"), *MatchType)
);
}
}
}
11.4 get the ip address
在头文件的private下,创建加入会话完成的委托
FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate;//加入会话完成的委托
在头文件的protected下,加入会话完成创建回调函数
void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);//加入会话完成的回调函数
绑定委托
JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete))
修改OnFindSessionsComplete函数,现在开头加上
1 2 3 4
if (!OnlineSessionInterface.IsValid()) { return; }
在模式对的情况下,为接口加入委托,并执行join
1 2 3 4 5 6
//加到委托列表中去 OnlineSessionInterface->AddOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegate); //获取角色控制器 const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); //加入会话 OnlineSessionInterface->JoinSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, Result);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
5. 实现join的回调函数
```cpp
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("Connect String : %s"), *Address)
);
}
}
}
11.5 join the session
在join的回调函数中添加跳转场景
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 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("Connect String : %s"), *Address)
);
}
//获得角色控制器指针
APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController();
//跳转场景
if (PlayerController)
{
PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute);
}
}
}
12. Creating a Plugin
plugins插件和modules模块
我们的项目文件uproject其实就是一个module
依赖关系,下层只能依赖同层和上层,比game module能依赖engine module,因为先有engine module才有game module
12.1 Create our plugin
Edit->Plugins,点击add,选择blank命名MultiplayerSessions,填写descriptor data:A plugin for handling online mutiplayer sessions,最后创建插件
如果不显示插件,可以点content drawer的settings勾选显示插件
编译文件
12.2 Add dependency
找到刚创建的MultiplayerSessions.uplugin文件,添加依赖:
1 2 3 4 5 6 7 8 9 10
"Plugins": [ { "Name": "OnlineSubsystem", "Enabled": true }, { "Name": "OnlineSubsystemSteam", "Enabled": true } ]
打开MultiplayerSession.Build.cs,添加public依赖模块:
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 ... } );
编译文件
13. Creating our Own Subsystem
13.1 Create our own subsystem
创建一个新的c++类,选择ugameinstancesubsystem,选择multiplayerSessions 模块,命名MultiplayerSessionsSubsystem
如果有红色波浪线报错的话,就重新generate一下uproject,要记得将plugins中MultiPlayerSessions文件夹中的二进制文件夹和intermediate文件夹删掉
在MultiplayerSubsystem.h中添加一些头文件和构造函数声明、变量声明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#pragma once #include "CoreMinimal.h" #include "Subsystems/GameInstanceSubsystem.h" #include "Interfaces/OnlineSessionInterface.h" #include "MultiplayerSubsystem.generated.h" /** * */ UCLASS() class MULTIPLAYERSESSIONS_API UMultiplayerSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() public: UMutiplayerSubsystem(); protected: private: IOnlineSessionPtr SessionInterface; };
在MultiplayerSubsystem.cpp中实现构造函数
1 2 3 4 5 6 7 8 9 10
#include "MultiplayerSubsystem.h" #include "OnlineSubsystem.h" UMultiplayerSubsystem::UMultiplayerSubsystem() { IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get(); if (Subsystem) { SessionInterface = Subsystem->GetSessionInterface(); } }
14. Session Interface Delegates
14.1 Functions
在头文件声明函数
1 2 3 4 5 6 7 8 9 10
public: UMultiplayerSubsystem(); // //处理会话功能,菜单类将会调用 // void CreateSession(int32 NumPublicConnections, FString MatchType); void FindSessions(int32 MaxSearchResults); void JoinSession(const FOnlineSessionSearchResult& SessionResult); void DestroySession(); void StartSession();
14.2 Delegates
在头文件添加委托变量
1 2 3 4 5 6 7 8 9 10 11 12
private: IOnlineSessionPtr SessionInterface; // //要添加在线会话接口的委托列表中的委托 //将绑定MultiplayerSubsystem内部回调函数到这些委托 // FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate; FOnFindSessionsCompleteDelegate FindSessionsCompleteDelegate; FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate; FOnDestroySessionCompleteDelegate DestroySessionCompleteDelegate; FOnStartSessionCompleteDelegate StartSessionCompleteDelegate;
14.3 Callbacks
在头文件中声明回调函数(protected)
1 2 3 4 5 6 7 8 9 10
protected: // //将要添加在 在线会话接口委托列表中委托的 内部回调函数 //因为不需要在这个类外调用,所以写在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); ssful);
14.4 Bind the callbacks
在cpp文件的构造函数下初始化,绑定委托和回调函数
1 2 3 4 5 6
UMultiplayerSubsystem::UMultiplayerSubsystem(): 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))
14.5 Dekegate handles
在头文件中为每一个委托创建delegate handle
1 2 3 4 5 6 7 8 9 10
FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate; FDelegateHandle CreateSessionCompleteDelegateHandle; FOnFindSessionsCompleteDelegate FindSessionsCompleteDelegate; FDelegateHandle FindSessionsCompleteDelegateHandle; FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate; FDelegateHandle JoinSessionCompleteDelegateHandle; FOnDestroySessionCompleteDelegate DestroySessionCompleteDelegate; FDelegateHandle DestroySessionCompleteDelegateHandle; FOnStartSessionCompleteDelegate StartSessionCompleteDelegate; FDelegateHandle StartSessionCompleteDelegateHandle;
编译文件
15. The Menu Class
15.1 Create a menu class
在MultiplayerSessionsC++Class->MultiplySessions->Public文件夹中创建新的c++类,搜索uuserwidget,选择UserWidget,下一步选择MultiplayerSessions,命名Menu
此时重新加载后,编译无法通过,尝试重新generate(千万不要!!!会打不开项目)
打开MultiplayerSessions.Build.cs文件,在PublicDependencyModuleNames.AddRange中添加引用依赖,解决了报错,再编译一下,就能打开项目了
1 2 3 4 5 6 7 8 9 10 11 12
PublicDependencyModuleNames.AddRange( new string[] { "Core", "OnlineSubsystem", "OnlineSubsystemSteam", "UMG", "Slate", "SlateCore" // ... add other public dependencies that you statically link with here ... } );
在Menu .h添加函数声明
1 2 3
public: UFUNCTION(BlueprintCallable) void MenuSetup();
在cpp中实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
void UMenu::MenuSetup() { AddToViewport();//添加到窗口 SetVisibility(ESlateVisibility::Visible);//设置为可见 bIsFocusable = true; UWorld* World = GetWorld(); if (World) { APlayerController* PlayerController = World->GetFirstPlayerController(); if (PlayerController) { //设置input的模式为UI模式,并设置其中的属性 FInputModeUIOnly InputModeData; InputModeData.SetWidgetToFocus(TakeWidget());//聚焦这个界面 InputModeData.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);//设置鼠标不锁定模式 PlayerController->SetInputMode(InputModeData);//设置输入模式 PlayerController->SetShowMouseCursor(true);//鼠标光标可见 } } }
15.2 Create a menu widget
在MultiplayerSessions Content文件夹中创建user interface->widget,命名UBP_Menu
进入UBP_Menu的设计模式,创建两个按钮,一个HostButton,一个JoinButton,将锚点设置成底部中心,调整按钮位置和大小
进入蓝图模式,点击class settings,将parent class设置为Menu
进入level blueprint,创建beginPlay节点,指向create widget节点,选择UBP_Menu,指向MenuSetup节点,return value连接target(如果没有menu set节点的话就尝试重启项目)
16. Accessing our Subsystem
16.1 Button callbacks
到Menu.h文件中为按钮创建变量
1 2 3 4 5 6 7 8 9 10 11 12 13
private: UPROPERTY(meta = (BindWidget))//要和界面的按钮控件绑定,就要加这个,而且变量名要和界面中创建的一致 class UButton* HostButton; UPROPERTY(meta = (BindWidget)) UButton* JoinButton; UFUNCTION()//因为要使用引擎的click事件或者委托,所以加上这个 void HostButtonClicked(); UFUNCTION() void JoinButtonClicked();
实现按钮事件(其实就是debug一下先)
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::HostButtonClicked() { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Yellow, FString(TEXT("Host Button clicked")) ); } } void UMenu::JoinButtonClicked() { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Yellow, FString(TEXT("Join Button clicked")) ); } }
接下来将事件绑定到按钮上,要先在头文件重写一下初始化函数
1 2 3
protected: virtual bool Initialize() override;
在cpp文件中添加头文件
#include "Components/Button.h"
实现initialize函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
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; }
编译并测试(如果没反应,可以尝试下重启项目,再不行就重新generate一下)
16.2 Access our subsystem
在menu.h的private中声明变量,我的类名叫UMultiplayerSubsyste,是因为创建类的时候少打了session,删除类也有点麻烦,所以也就没改名了
1 2
//管理所有在线会话的功能 class UMultiplayerSubsystem* MultiplayerSessionsSubsystem;
在cpp文件中,丰富menusetup函数,并添加MutiplayerSubsystem的头文件
1 2 3 4 5 6 7
UGameInstance* GameInstance = GetGameInstance(); if (GameInstance) { //为之前创建的变量赋值 MultiplayerSessionsSubsystem = GameInstance->GetSubsystem<UMultiplayerSubsystem>(); }
修改button的响应函数(具体实现在下一节)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
void UMenu::HostButtonClicked() { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Yellow, FString(TEXT("Host Button clicked")) ); } if (MultiplayerSessionsSubsystem) { MultiplayerSessionsSubsystem->CreateSession(4, FString("FreeForAll")); } }
17. Create Session
17.1 Implement CreateSession()
实现MultiplayerSubsystem.cpp的CreateSession函数
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
void UMultiplayerSubsystem::CreateSession(int32 NumPublicConnections, FString MatchType) { if (!SessionInterface.IsValid()) { return; } //判断是否存在NAME_GameSession的会话 auto ExistingSession = SessionInterface->GetNamedSession(NAME_GameSession); if (ExistingSession != nullptr) { SessionInterface->DestroySession(NAME_GameSession); } //将委托加入委托列表,并保存至handle中以便之后消除 CreateSessionCompleteDelegateHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate); LastSessionSettings = MakeShareable(new FOnlineSessionSettings()); //如果没连接steam那就是局域网连接,否则不是 LastSessionSettings->bIsLANMatch = IOnlineSubsystem::Get()->GetSubsystemName() == "NULL" ? true : false; LastSessionSettings->NumPublicConnections = NumPublicConnections; LastSessionSettings->bAllowJoinInProgress = true; LastSessionSettings->bAllowJoinViaPresence = true; LastSessionSettings->bShouldAdvertise = true; LastSessionSettings->bUsesPresence = true; LastSessionSettings->Set(FName("MatchType"), MatchType, EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);//这个应该是指显示出在线服务和ping值 const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); //如果创建会话失败,那么就消除委托列表中的创建委托 if (!SessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *LastSessionSettings)) { SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle); } }
在头文件创建私有变量:
TSharedPtr<FOnlineSessionSettings> LastSessionSettings;
在cpp文件包含头文件:
#include "OnlineSessionSettings.h"
17.2 Travel to the lobby
- 在menu.cpp文件中丰富HostButtonClicked()
1
2
3
4
5
6
7
8
9
if (MultiplayerSessionsSubsystem)
{
MultiplayerSessionsSubsystem->CreateSession(4, FString("FreeForAll"));
UWorld* World = GetWorld();
if (World)
{
World->ServerTravel("/Game/ThirdPerson/Maps/Lobby?listen");
}
}
编译并测试,不需要打包的测试方法:右键uproject->lauch game
测试失败,没有跳转场景,查了下,发现是ServerTravel里路径参数写错了,还得是去copy path最保险
在menu.h中声明一个私有函数MenuTearDown,用来解除inputmodeUI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
void UMenu::MenuTearDown() { RemoveFromParent(); UWorld* World = GetWorld(); if (World) { APlayerController* PlayerController = World->GetFirstPlayerController(); if (PlayerController) { FInputModeGameOnly InputModeData; PlayerController->SetInputMode(InputModeData); PlayerController->SetShowMouseCursor(false); } } }
重写Menu类的虚函数OnLevelRemovedFromWorld() (protected)
1 2 3 4 5 6
void UMenu::OnLevelRemovedFromWorld(ULevel* InLevel, UWorld* InWorld) { MenuTearDown(); //调用父级的函数 Super::OnLevelRemovedFromWorld(InLevel, InWorld); }
17.3 Add Input to MenuSetup
在头文件中添加私有变量并初始化
1 2
int32 NumPublicConnection{ 4 }; FString MatchType{ TEXT("FreeForAll") };
修改menuSetup函数(参数也要修改,头文件中要设置默认值),主要就是给变量赋初值
void MenuSetup(int32 NumberOfPublicConnections = 4 , FString TypeOfMatch = FString(TEXT("FreeForAll")));
1 2 3 4
void UMenu::MenuSetup(int32 NumberOfPublicConnections , FString TypeOfMatch ) { NumPublicConnection = NumberOfPublicConnections; MatchType = TypeOfMatch;
修改HostButtonClicked()函数中,createSession的参数,改成变量
MultiplayerSessionsSubsystem->CreateSession(NumPublicConnection, MatchType);
此时打开level bliprint可能会看到menu setup节点没有添加变量,重启下项目即可
18. Callbacks to our Subsystem Functions
使用自定义的委托,在执行完会话创建的委托回调函数后,创建自定义委托来执行menu类的回调函数,以此来使multiplayer session subsystem可以与menu类实现互通
18.1 Declare new delegate
在MultiplayerSubsystem.h中声明自定义委托(动态多播委托:多个类的回调函数都可以绑定这个委托,DYNAMIC意味着被序列化,而且可以在蓝图中加载和保存),并创建public变量,声明在类外(可能会报错有红色波浪线,没关系,做完这一节,最后重启项目和vs再编译一遍即可)
1
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMultiplayerOnCreateSessionComplete, bool, bWasSuccessful);
1 2 3 4
// //自定义委托变量(用于和menu类的回调函数绑定) // FMultiplayerOnCreateSessionComplete MultiplayerOnCreateSessionComplete;
18.2 Create callbacks on the menu
在menu.h中声明回调函数(protected)
1 2 3 4
// //回调函数(用于MultiplayerSystem的自定义委托) // void OnCreateSession(bool bWasSuccessful);
18.3 Bind the callbacks
在MenuSetup函数的底部添加代码
1 2 3 4
if (MultiplayerSessionsSubsystem) { MultiplayerSessionsSubsystem->MultiplayerOnCreateSessionComplete.AddDynamic(this, &ThisClass::OnCreateSession); }
在MultiplayerSubsystem.cpp中的createsession函数中,如果创建函数失败了,那么就广播这个自定义委托(false)
1 2 3 4
if (MultiplayerSessionsSubsystem) { MultiplayerSessionsSubsystem->MultiplayerOnCreateSessionComplete.AddDynamic(this, &ThisClass::OnCreateSession); }
在执行CreateSessionCompleteDelegate委托的回调函数时,自定义委托广播true
1 2 3 4 5 6 7 8 9
void UMultiplayerSubsystem::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful) { if (SessionInterface) { SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle); } MultiplayerOnCreateSessionComplete.Broadcast(bWasSuccessful); }
去实现自定义委托的回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
void UMenu::OnCreateSession(bool bWasSuccessful) { if (bWasSuccessful) { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Yellow, FString(TEXT("Session created successfully!")) ); } } }
此时运行可能没反应,是因为要将menu中的那个回调函数设置成UFUNCTION,才能绑定成功(因为用的时dynamic委托):
1 2 3 4 5
// //回调函数(用于MultiplayerSystem的自定义委托) // UFUNCTION() void OnCreateSession(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
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) { //E:/Ue5/MenuSystem/Content/ThirdPerson/Maps/Lobby.umap World->ServerTravel("/Game/ThirdPerson/Maps/Lobby?listen"); } } else { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Red, FString(TEXT("Failed to create a session!")) ); } } }
编译测试
19. More Subsystem Delegates
19.1 More delegates
MultiplayerSubsystem.h文件中声明委托,其中有dynamic的意味着可以使用蓝图节点,没有的就不能使用(更深层次的原理不太清除)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMultiplayerOnCreateSessionComplete, bool, bWasSuccessful); DECLARE_MULTICAST_DELEGATE_TwoParam(FMultiplayerOnFindSessionsComplete, const TArray<FOnlineSessionSearchResult>& SessionResult, 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);
创建委托变量
1 2 3 4 5 6 7 8
// //自定义委托变量(用于和menu类的回调函数绑定) // FMultiplayerOnCreateSessionComplete MultiplayerOnCreateSessionComplete; FMultiplayerOnFindSessionsComplete MultiplayerOnFindSessionsComplete; FMultiplayerOnJoinSessionComplete MultiplayerOnJoinSessionComplete; FMultiplayerOnDestroySessionComplete MultiplayerOnDestroySessionComplete; FMultiplayerOnStartSessionComplete MultiplayerOnStartSessionComplete;
19.2 Menu callbacks
在menu.h声明回调函数
1 2 3 4 5 6 7 8 9 10 11
// //回调函数(用于MultiplayerSystem的自定义委托) // UFUNCTION() void OnCreateSession(bool bWasSuccessful); void OnFindSessions(const TArray<FOnlineSessionSearchResult>& SessionResult, bool bWasSuccessful); void OnJoinSession(EOnJoinSessionCompleteResult::Type Result); UFUNCTION() void OnDestroySession(bool bWasSuccessful); UFUNCTION() void OnStartSession(bool bWasSuccessful);
在menu.cpp中menuSetup函数底部绑定回调
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); }
OnJoinSession函数会报错,是因为缺少头文件,在头文件文件中添加
#include "Interfaces/OnlineSessionInterface.h""
在cpp文件添加头文件
#include "OnlineSessionSettings.h"
之后如果突然出现了一些不明不白的红色波浪线,那么重启下项目和vs即可解决
20. Join sessions from the menu
20.1 Join session
修改menu.cpp的joinButtonClicked
1 2 3 4 5 6 7 8
void UMenu::JoinButtonClicked() { if(MultiplayerSessionsSubsystem) { MultiplayerSessionsSubsystem->FindSessions(10000); } }
实现multiplayerSubsystem的findsession函数,在头文件创建私有变量``
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
void UMultiplayerSubsystem::FindSessions(int32 MaxSearchResults) { if (!SessionInterface.IsValid()) { return; } //将委托变量加入委托列表 FindSessionsCompleteDelegateHandle = SessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegate); //创建会话设置的智能指针,将类的指针makesharable LastSessionSearch = MakeShareable(new FOnlineSessionSearch()); LastSessionSearch->MaxSearchResults = MaxSearchResults;//最大搜索个数 LastSessionSearch->bIsLanQuery = IOnlineSubsystem::Get()->GetSubsystemName() == "NULL" ? true : false; //确保搜索的 会话询问 设置为 现在存在的 LastSessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals); //获取角色控制器 const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); //执行在线会话接口的寻找会话的函数 if (!SessionInterface->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), LastSessionSearch.ToSharedRef()))//将ptr转为ref { SessionInterface->ClearOnCancelFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegateHandle); //搜索失败,那么广播时传的参数就是空的TArray MultiplayerOnFindSessionsComplete.Broadcast(TArray<FOnlineSessionSearchResult>(), false)'' } }
实现回调
OnFindSessionsComplete(bool bWasSuccessful)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
void UMultiplayerSubsystem::OnFindSessionsComplete(bool bWasSuccessful) { if (SessionInterface) { SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(FindSessionsCompleteDelegateHandle); } if (LastSessionSearch->SearchResults.Num() <= 0)//没有找到会话,也广播false给menu { MultiplayerOnFindSessionsComplete.Broadcast(TArray<FOnlineSessionSearchResult>(), false); return; } MultiplayerOnFindSessionsComplete.Broadcast(LastSessionSearch->SearchResults , bWasSuccessful); }
20.2 Joing a session
实现menu的回调函数OnJoinSession
1 2 3 4 5 6 7 8 9 10 11 12 13 14
void UMenu::OnFindSessions(const TArray<FOnlineSessionSearchResult>& SessionResult, bool bWasSuccessful) { //遍历搜索到的所有会话 for (auto Result : SessionResult) { FString SettingsValue; Result.Session.SessionSettings.Get(FName("MatchType"), SettingsValue);//将会话中设置的比赛类型存到SettingsVale中 if (SettingsValue == MatchType)//如果搜索到的,和我menu设置的menuType一致 { MultiplayerSessionsSubsystem->JoinSession(Result); return; } } }
去实现MultiplayerSubsystem的JoinSession函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
void UMultiplayerSubsystem::JoinSession(const FOnlineSessionSearchResult& SessionResult) { if (!SessionInterface.IsValid()) { MultiplayerOnCreateSessionComplete.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 UMultiplayerSubsystem::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result) { if (SessionInterface) { SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegateHandle); } MultiplayerOnJoinSessionComplete.Broadcast(Result); }
在menu.cpp实现join的回调,并添加头文件
#include "OnlineSubsystem.h"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
void UMenu::OnJoinSession(EOnJoinSessionCompleteResult::Type Result) { IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get(); if (Subsystem) { IOnlineSessionPtr SessionInterface = Subsystem->GetSessionInterface(); if (SessionInterface.IsValid()) { FString Address;//保存要join的地址 SessionInterface->GetResolvedConnectString(NAME_GameSession, Address); //获得角色控制器指针 APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController(); //跳转场景 if (PlayerController) { PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute); } } } }
由于找不到Session,应该是在createSession函数里忘了加
LastSessionSettings->bUseLobbiesIfAvailable = true;
这个了
21. Tracking Incoming Players
游戏模式:设定规则,处理角色进入游戏、离开游戏。
游戏状态:每一个客户端都会监视游戏状态信息,游戏状态是一个类,保存着每个玩家的数据数组
21.1 Create a game mode
- 把game mode创建在MenuSystem文件夹,而不是放在插件里,新建C++类,选择GAME MODE BASE,命名LobbyGameMode
21.2 Track incoming players
在头文件的public下重写函数声明
1 2 3
public: virtual void PostLogin(APlayerController* NewPlayer) override; virtual void Logout(ACpntroller* Exiting) override;
实现两个函数,添加头文件
#include "GameFrameWork/GameStateBase.h"
不然会报错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
void ALobbyGameMode::PostLogin(APlayerController* NewPlayer) { //执行父类的函数 Super::PostLogin(NewPlayer); if (GameState) { int32 NumberOfPlayers = GameState.Get()->PlayerArray.Num(); if (GEngine) { //打印玩家数量 GEngine->AddOnScreenDebugMessage( 1, 60.f, FColor::Yellow, FString::Printf(TEXT("Players in game: %d"), NumberOfPlayers) ); //打印进入游戏的玩家名字 APlayerState* PlayerState = NewPlayer->GetPlayerState<APlayerState>(); if (PlayerState) { FString PlayerName = PlayerState->GetPlayerName();//保存玩家名字 GEngine->AddOnScreenDebugMessage( -1, 60.f, FColor::Cyan, FString::Printf(TEXT("%s has joined the game!"), *PlayerName) ); } } } } void ALobbyGameMode::Logout(AController* Exiting) { Super::Logout(Exiting); //打印进入游戏的玩家名字 APlayerState* PlayerState = Exiting->GetPlayerState<APlayerState>(); if (PlayerState) { int32 NumberOfPlayers = GameState.Get()->PlayerArray.Num(); //打印玩家数量 GEngine->AddOnScreenDebugMessage( 1, 60.f, FColor::Yellow, FString::Printf(TEXT("Players in game: %d"), NumberOfPlayers - 1) ); //打印退出的玩家名字 FString PlayerName = PlayerState->GetPlayerName();//保存玩家名字 GEngine->AddOnScreenDebugMessage( -1, 60.f, FColor::Cyan, FString::Printf(TEXT("%s has exited the game!"), *PlayerName) ); } }
在multiplayerSubsystem类的createSession()中添加一行设置
1
LastSessionSettings->BuildUniqueId = 1;//如果不设置的话,就搜索不到其他人创建的房间了
设置项目最大人数,打开config文件夹中的DefaultGame.ini,添加:
1 2
[/Script/Engine.GameSession] MaxPlayers=100
在ThirdPerson文件夹创建一个蓝图,搜索LobbyGameMode,命名BP_LobbyGameMode
打开蓝图,将Default Pawn Class改为BP_ThirdPersonCharacter,编译并保存
进入Lobby场景,将World Settings的GameMode Override改为BP_LobbyGameMode,Default Pawn Class改为BP_ThirdPersonCharacter,只有这样玩家在场景中移动才能传到服务器
22. Start Session
23. Path to Lobby
在menu头文件创建私有变量
FString PathToLpbby{ TEXT("") };
给setup函数添加一个形参
void MenuSetup(int32 NumberOfPublicConnections = 4 , FString TypeOfMatch = FString(TEXT("FreeForAll")), FString LobbyPath = FString(TEXT("/Game/ThirdPerson/Maps/Lobby")));
在setup函数顶部添加代码:
//给PathToLobby赋值 PathToLobby = FString::Printf(TEXT("%s?listen"), *LobbyPath);
修改createsession函数
1
World->ServerTravel(PathToLobby);
进入初始房间level blueprint,可以修改节点的参数了
24. Polishing the Menu Subsystem
24.1 Create Session on destroy
当加入他人会话的玩家退回到主界面时,点击host会显示创建失败,但再次点击就会成功,是因为第一次点击destroy了上一个会话就立马create,但实际上还没来得及destroy完,所以需要重新设计下这个删除功能
在MultiplayerSubsystem.h中声明一些私有变量
1 2 3
bool bCreateSessionOnDestroy{ false }; int32 LastNumPublicConnection; FString LastMatchType;e;
修改createSession中销毁会话部分代码
1 2 3 4 5 6 7 8 9
//如果有会话存在,那么保存此次创建会话的信息,并在之后创建 if (ExistingSession != nullptr) { bCreateSessionOnDestroy = true; LastNumPublicConnection = NumPublicConnections; LastMatchType = MatchType; DestroySession(); }
实现Destroy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
void UMultiplayerSubsystem::DestroySession() { if (!SessionInterface.IsValid()) { //给menu的回调发送false MultiplayerOnDestroySessionComplete.Broadcast(false); return; } DestroySessionCompleteDelegateHandle = SessionInterface->AddOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegate); if (!SessionInterface->DestroySession(NAME_GameSession))//如果销毁失败 { SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegateHandle);//清除句柄 MultiplayerOnDestroySessionComplete.Broadcast(false); } }
实现destroy的回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13
void UMultiplayerSubsystem::OnDestroySessionComplete(FName SessionName, bool bWasSuccessful) { if (SessionInterface) { SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegateHandle);//清除句柄 } if (bWasSuccessful && bCreateSessionOnDestroy)//如果接口的destroy成功且是通过删除再创建新会话 { bCreateSessionOnDestroy = false;//重置标记 CreateSession(LastNumPublicConnection, LastMatchType);//创建会话,这样其实会多创一次,但这样保证了一定会创建成功一个 } MultiplayerOnDestroySessionComplete.Broadcast(bWasSuccessful);//广播道菜单去 }
24.2 Quit Game button
在右上角创建QuitButton即可,直接给按钮添加点击事件退出游戏即可
24.3 Disable menu button
修改menu.cpp的按钮事件即可,加上这行:
HostButton->SetIsEnabled(false);
如果创建加入会话失败,那么只需要在回调函数中添加
HostButton->SetIsEnabled(true);
在find回调中添加:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
if (SessionResult.Num() == 0 || !bWasSuccessful) { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Red, FString(TEXT("Sessions Num : 0! or find error!")) ); } JoinButton->SetIsEnabled(true); return; }
在join回调底部,如果join失败也
JoinButton->SetIsEnabled(true);