UE 入坑系列(四):UE 的 GamePlay
前言
本文将会持续总结 ue 的 GamePlay 框架相关内容。
GameModes
ue 中有两个主要类负责处理进行中游戏的相关信息:Game Mode 和 Game State。
即使最开放的游戏也拥有基础规则,而这些规则构成了 Game Mode。在最基础的层面上,这些规则包括:
-
出现的玩家和观众数量,以及允许的玩家和观众最大数量。
-
玩家进入游戏的方式,可包含选择生成地点和其他 生成/重生 行为的规则。
-
游戏是否可以暂停,以及如何处理游戏暂停。
-
关卡之间的过渡,包括游戏是否以动画模式开场。
基于规则的事件在游戏中发生,需要进行追踪并和所有玩家共享时,信息将通过 Game State 进行存储和同步。这些信息包括:
-
游戏已运行的时间(包括本地玩家加入前的运行时间)。
-
每个个体玩家加入游戏的时间和玩家的当前状态。
-
当前 Game Mode 的基类。
-
游戏是否已开始。
Game Modes 的任务是定义和实现规则。Game Modes 常用的基类有两个:AGameModeBase
与 AGameMode
。
AGameModeBase
所有 Game Mode 均为 AGameModeBase
的子类。而 AGameModeBase
包含大量可重写的基础功能。部分常见函数包括:
函数/事件 | 目的 |
---|---|
InitGame | 在任何其他脚本(包括 PreInitializeComponents)之前调用的 InitGame 事件,用于 AGameModeBase 的初始化。 |
PreLogin | 接受或拒绝试图加入服务器的玩家。如果将 ErrorMessage 设置为非空字符串,会导致登录失败。PreLogin 在登录之前调用,尤其是如果加入的玩家需要下载游戏内容,则可能会经过较长时间。 |
PostLogin | 在成功登录后调用。这是第一个可以安全调用 PlayerController 上的复制函数的地方。OnPostLogin 可以在蓝图中实现以添加额外的逻辑。 |
HandleStartingNewPlayer | 在 PostLogin 或无缝旅行之后调用,可以在蓝图中覆盖以更改新玩家的操作。默认情况下,它将为玩家创建一个 Pawn。 |
RestartPlayer | 用于开始生成玩家的 Pawn。还提供了 RestartPlayerAtPlayerStart 和 RestartPlayerAtTransform 函数,如果要指定 Pawn 将生成的位置。可以在此函数完成后在蓝图中实现 OnRestartPlayer 以添加逻辑。 |
SpawnDefaultPawnAtTransform | 实际生成玩家的 Pawn,可以在蓝图中覆盖。 |
Logout | 当玩家离开游戏或被销毁时调用。OnLogout 可以在蓝图中实现以执行蓝图逻辑。 |
可针对游戏提供的每个比赛模式、任务类型或特殊区域创建 AGameModeBase
的子类。一款游戏可拥有任意数量的 Game Mode,因此也可拥有任意数量的 AGameModeBase
类子类;然而,给定时间上只能使用一个 Game Mode。每次关卡进行游戏实例化时 Game Mode Actor 将通过 UGameEngine::LoadMap()
函数进行实例化。
Game Mode 不会复制到加入多人游戏的远程客户端;它只存在于服务器上。因此本地客户端只能看到之前使用过的留存 Game Mode 类(或蓝图),但无法访问实际的实例并检查其变量,确定游戏进程中已发生哪些变化。
如玩家确实需要更新与当前 Game Mode 相关的信息,可将信息保存在一个 AGameStateBase
Actor 上以保持同步。AGameStateBase
Actor 随 Game Mode 而创建,之后被复制到所有远程客户端。
AGameMode
AGameMode 是 AGameModeBase 的子类,具有额外的功能以支持多人游戏匹配和传统行为。所有新创建的项目默认使用 AGameModeBase,但如果需要额外的功能,可以切换到继承自 AGameMode。如果继承自 AGameMode,还应该从 AGameState 继承游戏状态,因为它也支持 MatchState 状态机。
AGameMode 包含一个状态机,用于跟踪匹配或一般游戏流程的状态。要查询当前状态,你可以使用 GetMatchState
或像 HasMatchStarted
、IsMatchInProgress
和 HasMatchEnded
这样的包装函数。以下是可能的 MatchState:
-
EnteringMap
是初始状态。角色尚未进行 Tick,世界尚未完全初始化。当一切都加载完毕时,它将转换到下一个状态。 -
WaitingToStart
是下一个状态,进入时会调用HandleMatchIsWaitingToStart
。角色进行 Tick,但玩家尚未生成。如果ReadyToStartMatch
返回 true,或者调用StartMatch
,它将转换到下一个状态。 -
InProgress
是游戏的主要部分发生的状态。进入时会调用HandleMatchHasStarted
,然后调用所有角色的BeginPlay
。此时,正常的游戏正在进行。当ReadyToEndMatch
返回 true 或调用EndMatch
时,匹配将转换到下一个状态。 -
WaitingPostMatch
是倒数第二个状态,进入时会调用HandleMatchHasEnded
。角色仍在 Tick,但新玩家无法加入。当地图转移开始时,它将转换到下一个状态。 -
LeavingMap
是正常流程的最后一个状态,进入时调用HandleLeavingMap
。匹配在传输到新地图时保持在此状态,然后会转换回EnteringMap
。 -
Aborted
是失败状态,可以通过调用AbortMatch
来启动。当出现不可恢复的错误时,将设置此状态。
以下为 AGameMode.h
中的定义:
1 | /** Possible state of the current match, where a match is all the gameplay that happens on a single map */ |
匹配状态几乎总是 InProgress
,因为这是调用 BeginPlay
并开始角色更新的状态。然而,个别游戏可以覆盖这些状态的行为,以构建具有更复杂规则的多人游戏,例如允许玩家在等待其他玩家加入多人射击游戏时自由飞行。
GameState
Game State 负责启用客户端监控游戏状态。从概念上而言,Game State 应该管理所有已连接客户端已知的信息(特定于 Game Mode 但不特定于任何个体玩家)。它能够追踪游戏层面的属性,如已连接玩家的列表、夺旗游戏中的团队得分、开放世界游戏中已完成的任务,等等。
Game State 并非追踪玩家特有内容(如夺旗比赛中特定玩家为团队获得的分数)的最佳之处,因为它们由 Player State 更清晰地处理。整体而言,GameState 应该追踪游戏进程中变化的属性。这些属性与所有人皆相关,且所有人可见。Game mode 只存在于服务器上,而 Game State 存在于服务器上且会被复制到所有客户端,保持所有已连接机器的游戏进程更新。
GamePlay 相关类的网络同步
在 UE 中,与 GamePlay 相关的 Pawn、PlayerController、PlayerState、GameMode、GameState 类各自有不同的网络同步设置。
PlayerController 与 PlayerState
Server 端拥有所有玩家的 PlayerController,客户端仅拥有自己控制的角色的 Controller;而 PlayerState 存在于所有的 Client 与 Server 中。
因此,对于那些 “只想让操控者(Owner)的客户端看到的效果”,如拾取武器后 UI 的显示等,应当在 PlayerController 中执行。无论是使用 Replicate 还是 RPC,都只会同步到这一个客户端;同样道理,玩家血量等属性放在 PlayerState 中比较合适,可以同步到所有的客户端以用于显示血量等状态。
GameMode 与 GameState
与前文所述一致,GameMode 仅存在于 服务器端,GameState 存在于所有客户端与服务器端。
因此,GameMode 适合完成 玩家的重生、胜利的判定等对游戏至关重要的功能(GameMode 本就是为了这些功能而设计的);GameState 可以储存当前游戏的一些状态信息如游戏时间与维护当前所有玩家的列表等。
Pawn
Pawn 与 PlayerState 的同步状态相同,同时存在于所有的客户端与服务器上。
最基本的现实层面的功能应该放在此处完成,如某个客户端开枪,需要同步在其他客户端与服务器上显示开枪的效果,一种合理的实现方法是使用 RPC,在客户端开枪时调用 ServerRPC ,Server 再调用一个 NetMulticast RPC,使得所有的客户端与服务器均展示这个效果。详见 UE5-MPTPS-5。