本文為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 則 玩家人數消息 會被 進入或退出游戲的玩家昵稱消息 覆蓋。