本文为B站系列教学视频 《UE5_C++多人TPS完整教程》 —— 《P23 记录加入的玩家(Couting Incoming Players)》 的学习笔记,该系列教学视频为 Udemy 课程 《Unreal Engine 5 C++ Multiplayer Shooter》 的中文字幕翻译版,UP主(也是译者)为 游戏引擎能吃么。
文章目录
- P23 记录加入的玩家
- 23.1 游戏模式和游戏状态
- 23.2 追踪加入或离开游戏的玩家
- 23.3 使用 Lobby 游戏模式
- 23.4 进行测试
- 23.5 Summary
P23 记录加入的玩家
本节课将创建一个游戏模式,以便追踪(Track)加入游戏会话的玩家、打印游戏人数,之后我们就可以利用统计的人数来决定是否要从关卡 “Lobby
” 过渡(Transition)到实际的匹配中。
23.1 游戏模式和游戏状态
- 在多人游戏中,两个重要的类分别是游戏模式类和游戏状态类:
- 游戏模式规定了游戏规则,这涉及了很多方面,如何时将玩家移动至关卡中、选择出生位等。游戏模式有几个继承的虚拟函数,可以在玩家加入或离开会话时进行追踪,例如每当玩家加入游戏时都会调用 “
PostLogin()
” 访问玩家的控制器;当玩家离开时调用 “Logout()
” 函数。 - 客户端可以监控游戏状态,游戏状态将保存游戏的状态信息,而非特定的单个玩家的信息。游戏状态包含玩家状态数组,玩家状态类中保存了特定的单个玩家的信息,比如得分和胜利次数等。
- 游戏模式类可以访问游戏状态类的玩家状态数组,通过查看该数组的大小可以得出玩家的个数。
两个主要类负责处理进行中游戏的相关信息:Game Mode 和 Game State。
即使最开放的游戏也拥有基础规则,而这些规则构成了 Game Mode。在最基础的层面上,这些规则包括:- 出现的玩家和观众数量,以及允许的玩家和观众最大数量。
- 玩家进入游戏的方式,可包含选择生成地点和其他生成/重生成行为的规则。
- 游戏是否可以暂停,以及如何处理游戏暂停。
- 关卡之间的过渡,包括游戏是否以动画模式开场。
基于规则的事件在游戏中发生,需要进行追踪并和所有玩家共享时,信息将通过 Game State 进行存储和同步。这些信息包括:
- 游戏已运行的时间(包括本地玩家加入前的运行时间)。
- 每个个体玩家加入游戏的时间和玩家的当前状态。
- 当前 Game Mode 的基类。
- 游戏是否已开始。
Game Modes
特定的基础(如进行游戏所需要的玩家数量,或玩家加入游戏的方法)在多种类型的游戏中具有共通性。可根据开发的特定游戏进行无穷无尽的规则变化。无论规则如何,Game Modes 的任务都是定义和实现规则。Game Modes 当前常用的基类有两个。
4.14 版本中加入了AGameModeBase
,这是所有 Game Mode 的基类,是经典的AGameMode
简化版本。AGameMode
是 4.14 版本之前的基类,仍然保留,功能 如旧,但现在是AGameModeBase
的子类。由于其比赛状态概念的实现,AGameMode
更适用于标准游戏类型(如多人射击游戏)。AGameModeBase
简洁高效,是新代码项目中包含的全新默认游戏模式。
Game State
Game State 负责启用客户端监控游戏状态。从概念上而言,Game State 应该管理所有已连接客户端已知的信息(特定于 Game Mode 但不特定于任何个体玩家)。它能够追踪游戏层面的属性,如已连接玩家的列表、夺旗游戏中的团队得分、开放世界游戏中已完成的任务,等等。
Game State 并非追踪玩家特有内容(如夺旗比赛中特定玩家为团队获得的分数)的最佳之处,因为它们由 Player State 更清晰地处理。整体而言,Game State 应该追踪游戏进程中变化的属性。这些属性与所有人皆相关,且所有人可见。Game Mode 只存在于服务器上,而 Game State 存在于服务器上且会被复制到所有客户端,保持所有已连接机器的游戏进程更新。
—— 虚幻引擎官方文档《Game Mode 和 Game State》
- 打开虚幻引擎,在内容浏览器中展开 “
C++类/MenuSystem
”,添加游戏模式基础(Game mode base)C++ 类,命名为 “LobbyGameMode
”,选择模块为我们上节课新建的插件 “MenuSystem (Runtime)
”。
- 点击 “创建类” 按钮,VS 中出现弹窗,选择 “全部重新加载(A)”。
23.2 追踪加入或离开游戏的玩家
-
在 “
LobbyGameMode.h
” 中添加头文件 “GameFramework/GameStateBase.h
” 和 “GameFramework/PlayerState.h
”,避免出现错误 “使用了未定义类型AGameStateBase
” 和 “使用了未定义类型APlayerState
”。接着,声明重写 “virtual void PostLogin()
” 和 “virtual void Logout()
”。// Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h" #include "GameFramework/GameModeBase.h"/* P23 记录加入的玩家(Couting Incoming Players)*/ #include "GameFramework/GameStateBase.h" #include "GameFramework/PlayerState.h" /* P23 记录加入的玩家(Couting Incoming Players)*/#include "LobbyGameMode.generated.h"/*** */ UCLASS() class MENUSYSTEM_API ALobbyGameMode : public AGameModeBase {GENERATED_BODY()/* P23 记录加入的玩家(Couting Incoming Players)*/ public:// 成功登录后调用。这是首个在 PlayerController 上安全调用复制函数之处。// OnPostLogin 可在蓝图中实现,以添加额外的逻辑。virtual void PostLogin(APlayerController* NewPlayer) override; // 重写 virtual void PostLogin()// 玩家离开游戏或被摧毁时调用。可实现 OnLogout 执行蓝图逻辑。virtual void Logout(AController* Exiting) override; // virtual void Logout() /* P23 记录加入的玩家(Couting Incoming Players)*/ };
-
在 “
LobbyGameMode.cpp
” 中实现“virtual void PostLogin()
” 和 “virtual void Logout()
” 的覆写。如果 “GameState
” 和 “PlayerState
” 标红且错误提示为 “不允许指针指向不完整的类类型AGameStateBase
” 和 “不允许指针指向不完整的类类型APlayerState
”,则添加头文件 “GameFramework/GameStateBase.h
” 和 “GameFramework/PlayerState.h
”。// Fill out your copyright notice in the Description page of Project Settings.#include "LobbyGameMode.h"/* P23 记录加入的玩家(Couting Incoming Players)*/ void ALobbyGameMode::PostLogin(APlayerController* NewPlayer) {Super::PostLogin(NewPlayer);if (GameState) {int32 NumberOfPlayers = GameState.Get()->PlayerArray.Num();if (GEngine) {GEngine->AddOnScreenDebugMessage( // 添加调试信息到屏幕上1, // Key 不是 -1 时,则更新现有消息60.f, // 调试信息的显示时间FColor::Red, // 字体颜色:黄色FString::Printf(TEXT("Players in game: %d!"), NumberOfPlayers) // 打印玩家人数);APlayerState* PlayerState = NewPlayer->GetPlayerState<APlayerState>();if (PlayerState) {FString PlayerName = PlayerState->GetPlayerName();GEngine->AddOnScreenDebugMessage( // 添加调试信息到屏幕上2, // Key 不是 -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, // Key 不是 -1 时,则更新现有消息60.f, // 调试信息的显示时间FColor::Red, // 字体颜色:红色FString::Printf(TEXT("Players in game: %d!"), NumberOfPlayers - 1) // 打印玩家人数,// 此时 PlayerArray.Num() 还未更新,这里进行减 1 操作只是为了方便测试时显示正确的人数,在实际项目中不会如此操作);FString PlayerName = PlayerState->GetPlayerName();GEngine->AddOnScreenDebugMessage( // 添加调试信息到屏幕上2, // Key 不是 -1 时,则更新现有消息60.f, // 调试信息的显示时间FColor::Cyan, // 字体颜色:蓝绿色FString::Printf(TEXT("%s has exited the game!"), *PlayerName) // 打印进入游戏的玩家昵称);}} /* P23 记录加入的玩家(Couting Incoming Players)*/
这里可以复习一下函数 “
AddOnScreenDebugMessage()
” 第一个入参 “int32 Key
” 取不同值的含义。这里 打印玩家人数 和 打印进入或退出游戏的玩家昵称 设置了不同的 “Key
” 是为了都能在屏幕上显示,如果设置为 -1 则 玩家人数消息 会被 进入或退出游戏的玩家昵称消息 覆盖。调用全局变量
GEngine
指针调用函数AddOnScreenDebugMessage
节点,进行屏幕输出。void AddOnScreenDebugMessage {int32 Key,float TimeToDisplay,FColor Di splayColor,const FString & DebugMessage,bool bNewerOnTop,const FVector2D & TextScale }
Key = -1
时,则添加新的消息,不会覆盖旧有消息(当Key = -1
时,bNewerOnTop
有效,直接添加到队列最上层)Key != -1
时,则更新现有消息,效率更高。
—— 《虚幻引擎基础入门(C++) — 【日志输出篇 03】》
-
在 “
MultiplayerSessionsSubsystem.cpp
” 的 “CreateSession()
” 函数中加入一条会话设置的代码行,用来设置唯一构建标识,防止不同的构建在搜索过程中可以互相搜索到。void UMultiplayerSessionsSubsystem::CreateSession(int32 NumpublicConnections, FString MatchType) {...// FOnlineSessionSettings 在头文件 "OnlineSessionSettings.h" 中LastSessionSettings = MakeShareable(new FOnlineSessionSettings()); // 创建会话设置,利用函数 MakeShareable 初始化// 会话设置成员变量参阅及含义:https://docs.unrealengine.com/5.0/en-US/API/Plugins/OnlineSubsystem/FOnlineSessionSettings/LastSessionSettings->bIsLANMatch = IOnlineSubsystem::Get()->GetSubsystemName() == "NULL" ? true : false; // 会话设置:如果找到的子系统名称为 “NULL”,则使用 LAN 连接,否则不使用LastSessionSettings->NumPublicConnections = NumpublicConnections; // 会话设置:设置最大公共连接数为函数输入变量 NumpublicConnections.../* P23 记录加入的玩家(Couting Incoming Players)*/LastSessionSettings->BuildUniqueId = 1; // 会话设置:设置唯一构建标识,防止不同的构建在搜索过程中可以互相搜索到。/* P23 记录加入的玩家(Couting Incoming Players)*/...}
-
在 “
MenuSystem\Config
” 目录下打开 “DefaultGame.ini
”,设置最大玩家数为 100。创建会话中设置最大公共连接数 “NumPublicConnections
”区别开,设置最大公共连接数是指定加入会话的最大连接数,而设置最大玩家数是指定连接到游戏项目最大人数。[/Script/EngineSettings.GeneralProjectSettings] ProjectID=6A5F83AB4DEB75FB9BB586AC8DE40CDA ProjectName=Third Person Game Template[StartupActions] bAddPacks=True InsertPack=(PackSource="StarterContent.upack",PackName="StarterContent")[/Script/Engine.GameSession] MaxPlayers=100
-
(选做)注释掉 “
MenuSystemCharacter.cpp
” 获取在线子系统并打印在线子系统名称到屏幕上的代码,并删除 “BP_ThirdPersonCharacter
” 角色蓝图中按下数字键 “1” 和 “2” 对应的事件。// AMenuSystemCharacterAMenuSystemCharacter::AMenuSystemCharacter() : // 为委托绑定回调函数CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete)),FindSessionsCompleteDelegate(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete)),JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete)) {.../* P23 记录加入的玩家(Couting Incoming Players)*///IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get(); // 获取当前的在线子系统指针//if (OnlineSubsystem) { // 如果当前在线子系统有效// OnlineSessionInterface = OnlineSubsystem->GetSessionInterface(); // 获取会话接口智能指针// if (GEngine) {// GEngine->AddOnScreenDebugMessage( // 添加调试信息到屏幕上// -1, // 使用 -1 不会覆盖前面的调试信息// 15.f, // 调试信息的显示时间// FColor::Blue, // 字体颜色// FString::Printf(TEXT("Found subsystem %s!"),// *OnlineSubsystem->GetSubsystemName().ToString()) // 打印在线子系统的名称// );// }//}/* P23 记录加入的玩家(Couting Incoming Players)*/}
23.3 使用 Lobby 游戏模式
- 打开虚幻引擎,在内容浏览器中目录 “
/内容/ThirdPerson/Blueprints/
” 下新建 “LobbyGameMode
” 蓝图类,命名为 “BP_LobbyGameMode
”。
- 双击 “
BP_LobbyGameMode
”,进入蓝图编辑器,在右侧 “细节” 面板 “类” 选项卡下设置 “默认 pawn 类” 为 “BP_ThirdPersonCharacter
”,编译、保存。
- 在目录 “
/内容/ThirdPerson/Maps/
” 下打开地图 “Lobby
”,在右下方 “世界场景设置” 面板设置 “游戏模式” 为“BP_LobbyGameMode
”,保存。
如果没有看到右下方的 “世界场景设置” 面板,可以点击工具栏右侧 “项目和编辑器设置” 按钮(虚幻引擎窗口右上方),在下拉菜单栏中选中 “世界场景设置”。
23.4 进行测试
-
将项目打包之后发送到另一台设备上。在设备 1 上运行游戏(保证 Steam 已经运行),点击 “
Host
” 按钮,当前关卡跳转至 “Lobby
”,且左上角显示会话创建成功消息。
-
在设备 2 上运行游戏(保证 Steam 已经运行且登录的账户与设备1 上登录的账号不同),点击 “
Join
” 按钮,当前关卡跳转至 “Lobby
”,并且可以看到有两个玩家存在,说明设备 2 成功找到并加入到了设备 1 创建的会话中,但是设备 2 屏幕左上角没有显示玩家人数和加入游戏信息,而创建会话的设备 1 上有显示。
-
在设备 2 上退出游戏,可以看到设备 1 屏幕左上角显示了设备 2 上的玩家离开游戏的消息,玩家人数也发生了变化。
23.5 Summary
本节课了解了游戏模式和游戏状态的基本概念,并在虚幻引擎创建了游戏模式基础(Game mode base)C++ 类 “LobbyGameMode
”。接着我们在 “LobbyGameMode.cpp
” 中实现“virtual void PostLogin()
” 和 “virtual void Logout()
” 的覆写,以便能够打印在线玩家人数以及玩家加入或退出游戏的消息。为了能够使用这个游戏模式,我们以 “LobbyGameMode
” 为父类新建了蓝图类 “BP_LobbyGameMode
”,设置蓝图类的 “默认 pawn 类” 为 “BP_ThirdPersonCharacter
”,并将地图 “Lobby
” 的 “游戏模式” 设置为“BP_LobbyGameMode
”。最后,我们在两台设备上以玩家加入和退出游戏的方式进行来了测试,玩家人数以及玩家加入和退出游戏的消息都能正常显示。
在 23.2 追踪加入或离开游戏的玩家 的 步骤 2 中使用函数 AddOnScreenDebugMessage()
进行屏幕消息输出时,若函数第一个入参 “int32 Key
” 为 -1 ,则添加新的消息,不会覆盖旧有消息(当 Key
为 -1 时,bNewerOnTop
有效,直接添加到队列最上层),若 Key
不为 -1 ,则更新现有消息。这里打印 玩家人数 和 打印进入或退出游戏的玩家昵称 设置了不同的 “Key
” 是为了都能在屏幕上显示,如果设置为 -1 则 玩家人数消息 会被 进入或退出游戏的玩家昵称消息 覆盖。