Skip to content

Latest commit

 

History

History
1446 lines (1036 loc) · 71.5 KB

File metadata and controls

1446 lines (1036 loc) · 71.5 KB

十八、多人游戏中的游戏框架类

概观

在这一章中,你将学习多人游戏中游戏框架类的实例。您还将学习如何使用游戏状态和玩家状态类,以及游戏模式中的一些新概念,包括匹配状态。我们还将介绍一些有用的内置功能,可以在不同类型的游戏中使用。

到本章结束时,您将能够使用游戏状态和玩家状态类来存储关于游戏和特定玩家的信息,任何客户端都可以访问这些信息。您还将知道如何充分利用游戏模式类和其他相关功能。

简介

在前一章中,我们介绍了远程过程调用,它允许服务器和客户端在彼此上执行远程功能。我们还介绍了枚举和双向循环数组索引

在这一章中,我们将看看最常见的游戏框架类,看看它们在多人环境中的实例。理解这一点很重要,这样您就知道在特定的游戏实例中可以访问哪些实例。这方面的一个例子是,只有服务器应该能够访问游戏模式实例,所以如果你在玩堡垒之夜,玩家不应该能够访问它并修改游戏规则。

在本章中,我们还将介绍游戏状态和玩家状态类。顾名思义,这些存储关于游戏状态和每个玩游戏的玩家的信息。最后,在本书的最后,我们将介绍游戏模式中的一些新概念,以及一些有用的内置功能。

我们将从多人游戏中游戏框架类如何工作开始。

多人游戏中的游戏框架类

虚幻引擎 4 附带了一个游戏框架,这是一组允许你更容易地创建游戏的类。游戏框架通过提供大多数游戏中存在的内置通用功能来做到这一点,例如定义游戏规则(游戏模式)的方式,以及控制角色(玩家控制器和棋子/角色类)的方式。当在多人游戏环境中创建一个游戏框架类的实例时,它可以存在于服务器、客户端和拥有者客户端上,拥有者客户端的玩家控制器是该实例的所有者。这意味着游戏框架类的实例总是属于以下类别之一:

  • 仅服务器:类的实例将只存在于服务器上。
  • 服务器和客户端:类的实例将存在于服务器和客户端上。
  • 服务器和拥有客户端:类的实例将存在于服务器和拥有客户端上。
  • 仅拥有客户端:类的实例将只存在于拥有客户端上。

请看下图,它显示了游戏框架中最常见的类的类别和用途:

Figure 18.1: The most common gameplay framework classes divided into categories

图 18.1:最常见的游戏框架分类

让我们更详细地了解一下上图中的每个类:

  • 游戏模式(仅限服务器):游戏模式类定义了游戏的规则,其实例只能被服务器访问。如果客户端试图访问它,实例将总是无效的,以防止客户端更改游戏规则。
  • 游戏状态(服务器和客户端):游戏状态类存储游戏的状态,服务器和客户端都可以访问它的实例。游戏状态将在未来的主题中更深入地讨论。
  • 玩家状态(服务器和客户端):玩家状态类存储玩家的状态,其实例可以被服务器和客户端访问。玩家状态将在未来的主题中有更深入的讨论。
  • 棋子(服务器和客户端):棋子类是玩家的可视化表示,其实例可以被服务器和客户端访问。
  • PlayerController(服务器和拥有客户端):玩家控制器类代表玩家的意图,这个意图被中继到当前拥有的棋子上,它的实例只能在服务器和拥有客户端上访问。出于安全原因,客户端无法访问其他客户端的播放器控制器,因此它们应该使用服务器进行通信。如果客户端使用除0以外的索引调用UGameplayStatics::GetPlayerController函数(这将返回其播放器控制器),则返回的实例将始终无效。这意味着服务器是唯一可以访问所有播放器控制器的地方。您可以通过调用AController::IsLocalController函数来确定一个玩家控制器实例是否在它自己的客户端中。
  • HUD(仅限拥有客户端):HUD 类作为即时模式在屏幕上绘制基本形状和文字。因为它用于用户界面,所以它的实例只在拥有它的客户机上可用,因为服务器和其他客户机不需要知道它。
  • UMG 小部件(仅拥有客户端):UMG 小部件类用于在屏幕上显示复杂的 UI。因为它用于用户界面,所以它的实例只在拥有它的客户机上可用,因为服务器和其他客户机不需要知道它。

为了帮助你理解这些概念,我们可以用 Dota 2 作为例子。游戏模式定义了游戏有不同的阶段(赛前为英雄挑选,实际游戏,赛后与胜者),最终目标是摧毁对方队伍的远古。因为这是一个对游戏性至关重要的类,所以不能允许客户端访问它:

  • 游戏状态存储经过的时间,无论是白天还是晚上,每个团队的得分等等,所以服务器和客户端需要能够访问它。
  • 玩家状态存储了玩家的名字、选择的英雄和击杀/死亡/辅助比率,所以服务器和客户端需要能够访问它。
  • 棋子将是英雄、信使、幻象等等,由玩家控制,因此服务器和客户端需要能够访问它。
  • 玩家控制器将输入信息传递给被控制的棋子,因此只有服务器和拥有它的客户端才需要能够访问它。
  • 用户界面类(HUDUser小部件)将显示拥有客户端的所有信息,因此只需要在那里访问。

在下一个练习中,您将显示最常见的游戏框架类的实例值。

练习 18.01:显示游戏框架实例值

在本练习中,我们将创建一个使用第三人称模板的新 C++ 项目,并添加以下内容:

  • 在拥有的客户端上,播放器控制器创建一个简单的 UMG 小部件并将其添加到视口中,该小部件显示菜单实例的名称。

  • On the tick function, the character displays the value of its own instance (as a pawn) as well as whether it has a valid instance for the game mode, game state, player state, player controller, and HUD.

    注意

    如果需要,您可以参考第 1 章虚幻引擎介绍,来回顾一下勾选功能。

以下步骤将帮助您完成练习:

  1. 使用名为GFInstances ( )的C++ 创建一个新的Third Person模板项目,并将其保存在您选择的位置。一旦创建了项目,它就应该打开编辑器和 Visual Studio 解决方案。

  2. 在编辑器中,创建一个名为GFInstancePlayerController的新C++ 类,该类源自PlayerController。等待编译结束,关闭编辑器,然后返回 Visual Studio。

  3. 打开GFInstancesCharacter.h文件,声明Tick功能的保护覆盖:

    virtual void Tick(float DeltaSeconds) override;
  4. 打开GFInstancesCharacter.cpp文件,包括DrawDebugHelpers.hPlayerController.h :

    #include "DrawDebugHelpers.h"
    #include "GameFramework/PlayerController.h"
  5. 实现Tick功能:

    void AGFInstancesCharacter::Tick(float DeltaSeconds)
    {
      Super::Tick(DeltaSeconds);
    }
  6. Get the instances for the game mode, game state, player controller, and HUD:

    AGameModeBase* GameMode = GetWorld()->GetAuthGameMode();
    AGameStateBase* GameState = GetWorld()->GetGameState();
    APlayerController* PlayerController =   Cast<APlayerController>(GetController());
    AHUD* HUD = PlayerController != nullptr ? PlayerController-  >GetHUD() : nullptr;

    在前面的代码片段中,我们将游戏模式、游戏状态、玩家控制器和 HUD 的实例存储在单独的变量中,这样我们就可以检查它们是否有效。

  7. Create a string for each gameplay framework class:

    const FString GameModeString = GameMode != nullptr ?   TEXT("Valid") : TEXT("Invalid");
    const FString GameStateString = GameState != nullptr ?   TEXT("Valid") : TEXT("Invalid");
    const FString PlayerStateString = GetPlayerState() != nullptr ?   TEXT("Valid") : TEXT("Invalid");
    const FString PawnString = GetName();
    const FString PlayerControllerString = PlayerController !=   nullptr ? TEXT("Valid") : TEXT("Invalid");
    const FString HUDString = HUD != nullptr ? TEXT("Valid") :   TEXT("Invalid");

    在这里,我们创建字符串来存储棋子的名称以及其他游戏框架实例是否有效。

  8. Display each string on the screen:

    const FString String = FString::Printf(TEXT("Game Mode = %s\nGame   State = %s\nPlayerState = %s\nPawn = %s\nPlayer Controller =   %s\nHUD = %s"), *GameModeString, *GameStateString,   *PlayerStateString, *PawnString, *PlayerControllerString,   *HUDString);
    DrawDebugString(GetWorld(), GetActorLocation(), String, nullptr,   FColor::White, 0.0f, true);

    在这个代码片段中,我们打印了前面代码中创建的字符串,这些字符串指示棋子的名称以及其他游戏框架实例是否有效。

  9. Before we can move on to the AGFInstancesPlayerController class, we need to tell Unreal Engine that we want to use UMG functionality in order to be able to use the UUserWidget class. To do this, we need to open GFInstances.Build.cs and add UMG to the PublicDependencyModuleNames string array, like so:

    PublicDependencyModuleNames.AddRange(new string[] { "Core",   "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay",   "UMG" });

    如果您试图编译并从添加新模块中获得错误,那么请清理并重新编译您的项目。如果这不起作用,请尝试重新启动您的 IDE。

  10. 打开GFInstancesPlayerController.h并添加受保护的变量来创建 UMG 小部件:

```cpp
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GF   Instance Player Controller")
TSubclassOf<UUserWidget> MenuClass;
UPROPERTY()
UUserWidget* Menu;
```
  1. 宣布BeginPlay功能的保护覆盖:
```cpp
virtual void BeginPlay() override;
```
  1. 打开GFInstancesPlayerController.cpp并包含UserWidget.h :
```cpp
#include "Blueprint/UserWidget.h"
```
  1. 实现BeginPlay功能:
```cpp
void AGFInstancePlayerController::BeginPlay()
{
  Super::BeginPlay();
}
```
  1. 如果不是拥有的客户端或者菜单类无效,中止该功能:
```cpp
if (!IsLocalController() || MenuClass == nullptr)
{
  return;
}
```
  1. 创建小部件并将其添加到视口:
```cpp
Menu = CreateWidget<UUserWidget>(this, MenuClass);
if (Menu != nullptr)
{
  Menu->AddToViewport(0);
}
```
  1. 编译并运行代码。
  2. Content Browser中,转到Content文件夹,新建一个名为UI的文件夹,并将其打开。
  3. 创建一个名为UI_Menu的新小部件蓝图并打开它。
  4. 将名为tbTextText Block添加到根画布面板,并通过单击“详细信息”面板顶部其名称旁边的复选框Is Variable将其设置为变量。
  5. tbText设置为Size To Contenttrue
  6. Go to the Graph section and, in Event Graph, implement the Event Construct in the following manner:
![Figure 18.2: The Event Construct that displays the name of the UI_Menu instance ](img/B16183_18_02.jpg)

图 18.2:显示用户界面菜单实例名称的事件构造

注意

您可以在以下链接找到前面的全分辨率截图,以便更好地查看:[https://packt.live/38wvSr5](https://packt.live/38wvSr5)。
  1. 保存并关闭UI_Menu
  2. 转到Content文件夹,创建一个名为BP_PlayerController的蓝图,该蓝图源自GFInstancesPlayerController
  3. 打开BP_PlayerController,设置Menu Class使用UI_Menu
  4. 保存并关闭BP_PlayerController
  5. 转到Content文件夹,创建一个名为BP_GameMode的蓝图,该蓝图源自GFInstancesGameMode
  6. 打开BP_GameMode,设置Player Controller Class使用BP_PlayerController
  7. 保存并关闭BP_GameMode
  8. 转到Project Settings,从左侧面板中选择Maps & Modes,在Project类别中。
  9. Default GameMode设置为使用BP_GameMode
  10. Close Project Settings.
最后,您可以测试项目。
  1. 运行代码,等待编辑器完全加载。
  2. 转到Multiplayer Options,将客户端数量设置为2
  3. 将窗口大小设置为800x600
  4. New Editor Window (PIE)中播放。

完成本练习后,您将能够在每个客户端上进行游戏。您会注意到角色正在显示游戏模式、游戏状态、玩家状态、玩家控制器和抬头显示器的实例是否有效。它还显示棋子实例的名称。

现在,让我们分析一下ServerClient 1窗口中显示的值。让我们先从Server窗口开始。

服务器窗口

Server窗口,你有Server Character的值,在后台,你有Client 1 Character的值。左上角应该可以看到Server CharacterClient 1 CharacterUI_Menu UMG 小部件。UMG 小部件实例只为Server Character的玩家控制器创建,因为它是该窗口中唯一一个实际控制角色的玩家控制器。

我们先来分析一下Server Character的数值。

服务器 er 字符

这是监听服务器控制的角色,该服务器也集成了一个可以玩游戏的客户端。该字符上显示的值如下:

  • 游戏模式=有效因为游戏模式实例只存在于服务器中,也就是当前的游戏实例。
  • 游戏状态=有效因为游戏状态实例存在于客户端和服务器端,也就是当前的游戏实例。
  • 玩家状态=有效因为玩家状态实例存在于客户端和服务器端,也就是当前的游戏实例。
  • 棋子= ThirdPersonCharacter_2 因为棋子实例存在于客户端和服务器端,也就是当前的游戏实例。
  • 玩家控制器=有效因为玩家控制器实例存在于拥有的客户端和服务器上,这是当前的游戏实例。
  • 抬头显示器=有效因为抬头显示器实例只存在于拥有的客户端上,情况就是这样。

接下来,我们将在同一个窗口中查看Client 1 Character

Clie nt 1 字符

这就是Client 1所控制的人物。该字符上显示的值如下:

  • 游戏模式=有效因为游戏模式实例只存在于服务器中,也就是当前的游戏实例。
  • 游戏状态=有效因为游戏状态实例存在于客户端和服务器端,也就是当前的游戏实例。
  • 玩家状态=有效因为玩家状态实例存在于客户端和服务器端,也就是当前的游戏实例。
  • 棋子= ThirdPersonCharacter_0 因为棋子实例存在于客户端和服务器端,也就是当前的游戏实例。
  • 玩家控制器=有效因为玩家控制器实例存在于拥有的客户端和服务器上,这是当前的游戏实例。
  • HUD =无效因为 HUD 实例只存在于拥有的客户端上,而事实并非如此。

客户端 1 窗口

Client 1窗口中,你有Client 1 Character的值,在后台,你有Server Character的值。你应该会看到Client 1 Character, Server Character,以及左上角的UI_Menu UMG 小部件。UMG 小部件实例只为Client 1 Character的玩家控制器创建,因为它是该窗口中唯一一个实际控制角色的玩家控制器。

我们先来分析一下Client 1 Character的数值。

客户端 1 字符

这就是Client 1所控制的人物。该字符上显示的值如下:

  • 游戏模式=无效因为游戏模式实例只存在于服务器中,并不是当前的游戏实例。
  • 游戏状态=有效因为游戏状态实例存在于服务器和客户端,是当前的游戏实例。
  • 玩家状态=有效因为玩家状态实例存在于服务器和客户端,也就是当前的游戏实例。
  • 棋子= ThirdPersonCharacter_0 因为棋子实例存在于服务器和客户端,也就是当前的游戏实例。
  • 玩家控制器=有效因为玩家控制器实例存在于服务器和所属客户端,也就是当前的游戏实例。
  • 抬头显示器=有效因为抬头显示器实例只存在于拥有的客户端上,情况就是这样。

接下来,我们将在同一个窗口中查看Server Character

服务器角色

这是监听服务器正在控制的角色。该字符上显示的值如下:

  • 游戏模式=无效因为游戏模式实例只存在于服务器中,并不是当前的游戏实例。
  • 游戏状态=有效因为游戏状态实例存在于服务器和客户端,是当前的游戏实例。
  • 玩家状态=有效因为玩家状态实例存在于服务器和客户端,也就是当前的游戏实例。
  • 棋子= ThirdPersonCharacter_2 因为棋子实例存在于服务器和客户端,也就是当前的游戏实例。
  • 玩家控制器=无效因为玩家控制器实例存在于服务器和所属客户端,而不是当前的游戏实例。
  • HUD =无效因为 HUD 实例只存在于拥有的客户端上,而事实并非如此。

通过完成这个练习,您应该更好地理解游戏框架类的每个实例在哪里存在,在哪里不存在。接下来,我们将介绍玩家状态和游戏状态类,以及一些关于游戏模式和有用的内置功能的附加概念。

游戏模式、玩家状态和游戏状态

到目前为止,我们已经涵盖了游戏框架中的大多数重要类,包括游戏模式、玩家控制器和棋子。在本章中,我们将介绍玩家状态、游戏状态和游戏模式的一些附加概念,以及一些有用的内置功能。

游戏模式

我们已经讨论了游戏模式及其工作原理,但还有几个概念尚未涉及。

建造师

要设置默认的类值,可以使用如下构造函数:

ATestGameMode::ATestGameMode()
{
  DefaultPawnClass = AMyCharacter::StaticClass();
  PlayerControllerClass = AMyPlayerController::StaticClass();
  PlayerStateClass = AMyPlayerState::StaticClass();
  GameStateClass = AMyGameState::StaticClass();
}

前面的代码让你指定当我们使用这种游戏模式时产生棋子、玩家控制器、玩家状态和游戏状态时使用哪些类。

获取游戏模式实例

如果要访问游戏模式实例,需要使用以下代码从GetWorld函数获取:

AGameModeBase* GameMode = GetWorld()->GetAuthGameMode();

前面的代码允许您访问当前的游戏模式实例,这样您就可以运行函数并查阅某些变量的值。您必须确保只在服务器上调用它,因为出于安全原因,这在客户端上是无效的。

匹配状态

到目前为止,我们只使用了AGameModeBase类,这是框架中最基本的游戏模式类,虽然对于某些类型的游戏来说已经足够了,但是有些情况下您需要更多一点的功能。这方面的一个例子是,如果我们想做一个大厅系统,只有当所有球员都标记他们准备好了,比赛才会开始。这个例子不可能用AGameModeBase类来做。对于这些情况,最好使用AGameMode类,它是AGameModeBase的子类,通过使用匹配状态来增加对多人游戏的支持。匹配状态的工作方式是使用在给定时间只能处于以下状态之一的状态机:

  • EnteringMap:这是世界还在加载,演员还没滴答走的开始状态。一旦世界完成装载,它将转换到WaitingToStart状态。
  • WaitingToStart:这个状态是在世界加载完毕,演员在滴答作响的时候设定的,虽然因为游戏还没开始,玩家的棋子不会产生。当状态机进入该状态时,会调用HandleMatchIsWaitingToStart函数。如果ReadyToStartMatch函数返回true或者如果在代码中的某处调用了StartMatch函数,状态机将转换到InProgress状态。
  • InProgress:这个状态就是实际游戏发生的地方。当状态机进入这种状态时,会为玩家催生棋子,调用世界上所有演员上的BeginPlay,调用HandleMatchHasStarted函数。如果ReadyToEndMatch功能返回true或者如果在代码中的某处调用了EndMatch功能,状态机将转换到WaitingPostMatch状态。
  • WaitingPostMatch:比赛结束时设置此状态。当状态机进入该状态时,会调用HandleMatchHasEnded函数。在这种状态下,演员仍然打勾,但新玩家不能加入。当它开始卸载世界时,它将过渡到LeavingMap状态。
  • LeavingMap:这个状态是在卸载世界的时候设置的。当状态机进入该状态时,会调用HandleLeavingMap函数。当状态机开始加载新级别时,它将转换到EnteringMap状态。
  • Aborted:这是一种失败状态,只能通过调用AbortMatch函数来设置,该函数用于标记出现了阻止比赛发生的错误。

为了帮助您更好地理解这些概念,我们可以再次以 Dota 2 为例:

  • EnteringMap:加载地图时状态机会处于这种状态。
  • WaitingToStart:一旦地图加载完毕,玩家在挑选自己的英雄,状态机就会处于这种状态。ReadyToStartMatch功能会检查是否所有玩家都选择了自己的英雄;如果他们有,那么比赛就可以开始了。
  • InProgress:游戏实际进行的时候状态机会处于这种状态。玩家控制他们的英雄去农场和其他玩家战斗。ReadyToEndMatch功能会不断检查每个远古的健康状况,看是否有一个被破坏;如果是的话,比赛就结束了。
  • WaitingPostMatch:当游戏结束,你正在看到被摧毁的远古,并显示每个玩家的最终分数时,状态机将处于这种状态。
  • LeavingMap:状态机卸载地图时会处于这种状态。
  • Aborted:如果其中一个选手在初始阶段连接失败,状态机就会处于这种状态,从而中止整场比赛。

让玩家重生

当玩家死亡,你想重生时,你通常有两个选择。第一个选项是重用同一个棋子实例,手动将其状态重置回默认值,并将其传送到重生位置。第二种选择是摧毁棋子并产生一个新的棋子,这个棋子已经重置了状态。如果您更喜欢后一个选项,那么AGameModeBase::RestartPlayer功能会为您处理为某个玩家控制器生成新棋子实例的逻辑,并将其放在玩家开始处。

需要考虑的重要一点是,该函数只有在玩家控制器还没有拥有棋子的情况下才会产生一个新的棋子实例,所以在调用RestartPlayer之前一定要销毁被控制的棋子。

看看下面的例子:

void ATestGameMode::OnDeath(APlayerController* VictimController)
{
  if(VictimController == nullptr)
  {
    return;
  }

  APawn* Pawn = VictimController->GetPawn();
  if(Pawn != nullptr)
  {
    Pawn->Destroy();
  }

  RestartPlayer(VicitimController);
}

在前面的代码中,我们有OnDeath函数,该函数获取死亡玩家的玩家控制器,销毁其控制的棋子,并调用RestartPlayer函数在玩家开始时产生一个新的实例。默认情况下,所使用的玩家启动演员将始终与第一次衍生的玩家相同。如果你想让该功能在随机玩家启动时产生,那么你需要覆盖AGameModeBase::ShouldSpawnAtStartSpot功能并强制其为return false,如下所示:

bool ATestGameMode::ShouldSpawnAtStartSpot(AController* Player)
{
  return false;
}

前面的代码将使游戏模式使用随机玩家开始,而不是总是使用相同的。

注意

有关游戏模式的更多信息,请访问https://docs . unrealengine . com/en-US/game play/Framework/game mode/# game modeshttps://docs . unrealengine . com/en-US/API/Runtime/Engine/game Framework/AGameMode/index . html

玩家状态

玩家状态类存储玩家的状态,例如当前得分、死亡/死亡和捡到的硬币。在多人模式下,它主要用于存储其他客户端需要了解的玩家信息,因为他们无法访问其玩家控制器。最广泛使用的内置变量是PlayerNameScorePing,它们分别给你玩家的名字、分数和 ping。

多人射击游戏的记分牌条目是如何使用玩家状态的一个很好的例子,因为每个客户端都需要知道所有玩家的名字、死亡和 pings。玩家状态实例可以通过以下方式访问:

控制器::播放器状态

该变量具有与控制器相关联的播放器状态,并且它只能由服务器和拥有它的客户端访问。以下示例将演示如何使用变量:

APlayerState* PlayerState = Controller->PlayerState;

控制器::get layerstate()

该函数返回与控制器相关联的播放器状态,并且它只能由服务器和拥有它的客户端访问。这个函数还有一个模板版本,所以你可以把它转换成你自己的自定义玩家状态类。以下示例将演示如何使用该函数的默认版本和模板版本:

// Default version
APlayerState* PlayerState = Controller->GetPlayerState();
// Template version
ATestPlayerState* MyPlayerState = Controller->GetPlayerState<ATestPlayerState>();

apawn::get layerstate()

该函数返回与拥有棋子的控制器相关的玩家状态,服务器和客户端可以访问该状态。这个函数还有一个模板版本,所以你可以把它转换成你自己的自定义玩家状态类。以下示例将演示如何使用该函数的默认版本和模板版本:

// Default version
APlayerState* PlayerState = Pawn->GetPlayerState();
// Template version
ATestPlayerState* MyPlayerState = Pawn-  >GetPlayerState<ATestPlayerState>();

上面的代码演示了两种使用GetPlayerState函数的方法。您可以使用默认的APlayerState版本或为您自动转换的模板版本。

蹲下::播放器阵列

该变量存储每个玩家的玩家状态实例,可以在服务器和客户端上访问。以下示例将演示如何使用该变量:

TArray<APlayerState*> PlayerStates = GameState->PlayerArray;

为了帮助您更好地理解这些概念,我们可以再次以 Dota 2 为例。玩家状态至少有以下变量:

姓名:玩家姓名

英雄:选中的英雄

生命值:英雄的生命值

法力:英雄的法力

属性:英雄属性

等级:英雄当前所处的等级

击杀/死亡/辅助:玩家的击杀/死亡/辅助比例

注意

关于玩家状态的更多信息,请访问https://docs . unrealengine . com/en-US/API/Runtime/Engine/game framework/aplayer state/index . html

游戏状态

游戏状态类存储游戏的状态,包括比赛经过的时间和赢得游戏所需的分数。它主要用于多人模式下存储其他客户端需要了解的游戏信息,因为他们无法访问游戏模式。最广泛使用的变量是PlayerArray,这是一个数组,里面有每个连接的客户端的玩家状态。多人射击游戏上的记分牌是如何使用游戏状态的一个很好的例子,因为每个客户端都需要知道需要多少次击杀才能获胜,以及每个玩家的名字和 ping。

可以通过以下方式访问游戏状态实例:

超世界::GetGameState()

这个函数返回与世界相关的游戏状态,可以在服务器和客户端访问。这个函数还有一个模板版本,所以你可以把它转换成你自己的自定义游戏状态类。以下示例将演示如何使用该函数的默认版本和模板版本:

// Default version
AGameStateBase* GameState = GetWorld()->GetGameState();
// Template version
AMyGameState* MyGameState = GetWorld()->GetGameState<AMyGameState>();

阿加梅辩论::游戏状态

此变量具有与游戏模式相关联的游戏状态,并且只能在服务器上访问。以下示例将演示如何使用变量:

AGameStateBase* GameState = GameMode->GameState;

琼浆玉液:GetGameState()

该函数返回与游戏模式相关的游戏状态,并且只能在服务器上访问。这个函数还有一个模板版本,所以你可以把它转换成你自己的自定义游戏状态类。以下示例将演示如何使用该函数的默认版本和模板版本:

// Default version
AGameStateBase* GameState = GameMode->GetGameState<AGameStateBase>();
// Template version
AMyGameState* MyGameState = GameMode->GetGameState<AMyGameState>();

为了帮助您更好地理解这些概念,我们可以再次以 Dota 2 为例。游戏状态会有以下变量:

经过时间:比赛进行了多久

辐射击杀:辐射队击杀了多少可怕的英雄

可怕的杀戮:可怕的队伍杀死了多少光芒四射的英雄

昼/夜计时器:用于判断是白天还是黑夜

注意

有关游戏状态的更多信息,请访问https://docs . unrealengine . com/en-US/game play/Framework/game mode/# game statehttps://docs . unrealengine . com/en-US/API/Runtime/Engine/game Framework/AGameState/index . html

有用的内置功能

虚幻引擎 4 内置了很多有用的功能。以下是一些在开发游戏时有用的功能和组件的示例:

无效因素:结束播放(常量:结束播放原因:类型:结束播放原因)

这个函数在演员已经停止播放时调用,与BeginPlay函数相反。你有EndPlayReason参数,它告诉你为什么演员停止播放(如果它被破坏了,如果你停止了 PIE,等等)。看看下面的例子,它在屏幕上打印出演员已经停止演奏的事实:

void ATestActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
  Super::EndPlay(EndPlayReason);
  const FString String = FString::Printf(TEXT(«The actor %s has just     stopped playing"), *GetName());
  GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, String);
}

无效攻击::着陆(持续攻击结果&命中)

当玩家在空中后降落在一个表面时,这个函数被调用。看看下面的例子,当玩家在表面上着陆时,它会发出声音:

void ATestCharacter::Landed(const FHitResult& Hit)
{
  Super::Landed(Hit);
  UGameplayStatics::PlaySound2D(GetWorld(), LandSound);
}

bool uwworld::server travel(const fstab ring&furl,bool bAbsolute,bool bshouldskipgameotify)

这个函数将使服务器加载一个新的地图,并把所有连接的客户端一起带来。这不同于使用其他加载地图的方法,比如UGameplayStatics::OpenLevel函数,因为它不会把客户端带来;它只是将地图加载到服务器上,然后断开客户端。

需要考虑的一件重要事情是,server travel 只能在打包版本中正常工作,因此在编辑器中播放时不会将客户端带在身边。看看下面的例子,它获取当前的地图名称,并使用 server travel 来重新加载它并带来连接的客户端:

void ATestGameModeBase::RestartMap()
{
  const FString URL = GetWorld()->GetName();
  GetWorld()->ServerTravel(URL, false, false);
}

void TArray::Sort(const 谓语 _CLASS &谓语)

TArray数据结构附带了Sort函数,该函数允许您使用lambda函数对数组的值进行排序,该函数返回值A是否应该先排序,然后是值B。看看下面的例子,它从最小值到最大值对整数数组进行排序:

void ATestActor::SortValues()
{
  TArray<int32> SortTest;
  SortTest.Add(43);
  SortTest.Add(1);
  SortTest.Add(23);
  SortTest.Add(8);
  SortTest.Sort([](const int32& A, const int32& B) { return A < B; });
}

前面的代码将按值[43,1,23,8]从最小到最大[1,8,23,43]对SortTest数组进行排序。

void aactor::felloutofworld(const udmagetype&dmgtype)

在虚幻引擎 4 中,有一个概念叫做Kill Z,它是Z中某个值上的一个平面(设置在World Settings面板中),如果一个演员低于那个Z值,它会调用FellOutOfWorld函数,默认情况下,这个函数会破坏这个演员。看看下面的例子,它在屏幕上打印出演员从世界上消失的事实:

void AFPSCharacter::FellOutOfWorld(const UDamageType& DmgType)
{
  Super::FellOutOfWorld(DmgType);
  const FString String = FString::Printf(TEXT("The actor %s has fell     out of the world"), *GetName());
  GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, String);
}

尿静化运动组件

该组件按照在RotationRate变量中定义的每个轴上的特定速率,沿时间旋转拥有的参与者。要使用它,您需要包含以下标题:

#include "GameFramework/RotatingMovementComponent.h"

声明组件变量:

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Test Actor")
URotatingMovementComponent* RotatingMovement;

最后,在 actor 构造函数中初始化它,如下所示:

RotatingMovement = CreateDefaultSubobject   <URotatingMovementComponent>("Rotating Movement");
RotatingMovement->RotationRate = FRotator(0.0, 90.0f, 0);

在前面的代码中,RotationRate被设置为在Yaw轴上每秒旋转90度。

练习 18.02:制作简单的多人皮卡游戏

在本练习中,我们将创建一个使用第三人称模板的新 C++ 项目,并添加以下内容:

  • 在拥有的客户端上,玩家控制器创建一个 UMG 小部件并将其添加到视口中,为每个玩家显示分数,从最高到最低排序,以及它收集了多少个拾取。
  • 创建一个简单的拾取演员类,给拾取它的玩家 10 分。拾波器也将在Yaw轴上每秒旋转 90 度。
  • Kill Z设置为-500,使玩家重生,每次从世界上掉下来,丢 10 分。
  • 当没有更多的皮卡可用时,游戏将结束。一旦游戏结束,所有角色都将被摧毁,5 秒钟后,服务器将进行一次服务器旅行调用,以重新加载相同的地图,并带来连接的客户端。

以下步骤将帮助您完成练习:

  1. 使用名为PickupsC++ 创建一个新的Third Person模板项目,并将其保存到您选择的位置。

  2. Once the project has been created, it should open the editor as well as the Visual Studio solution.

    现在,让我们创建将要使用的新 C++ 类:

  3. 创建从Actor派生的Pickup类。

  4. 创建从GameState派生的PickupsGameState类。

  5. 创建从PlayerState派生的PickupsPlayerState类。

  6. 创建从PlayerController派生的PickupsPlayerController类。

  7. Close the editor and open Visual Studio.

    接下来,我们来学习Pickup课。

  8. 打开Pickup.h并清除所有现有功能。

  9. 声明受保护的Static Mesh 组件名为Mesh :

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   "Pickup")
    UStaticMeshComponent* Mesh;
  10. 声明被保护的旋转运动部件,称为RotatingMovement :

```cpp
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   "Pickup")
class URotatingMovementComponent* RotatingMovement;
```
  1. 声明受保护的PickupSound变量:
```cpp
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category =   "Pickup")
USoundBase* PickupSound;
```
  1. 声明受保护的构造函数并BeginPlay覆盖:
```cpp
APickup();
virtual void BeginPlay() override;
```
  1. 声明受保护的OnBeginOverlap功能:
```cpp
UFUNCTION()
void OnBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor*   OtherActor, UPrimitiveComponent* OtherComp, int32   OtherBodyIndex, bool bFromSweep, const FHitResult& Hit);
```
  1. 打开Pickup.cpp,包括PickupsCharacter.hPickupsGameState.hStaticMeshComponent.h、【T4:
```cpp
#include "PickupsCharacter.h"
#include "PickupsGameState.h"
#include "Components/StaticMeshComponent.h"
#include "GameFramework/RotatingMovementComponent.h"
```
  1. 在构造函数中,初始化Static Mesh组件,使其与所有组件重叠,重叠时调用OnBeginOverlap函数:
```cpp
Mesh = CreateDefaultSubobject<UStaticMeshComponent>("Mesh");
Mesh->SetCollisionProfileName("OverlapAll");
RootComponent = Mesh;
```
  1. 仍然在构造器中,初始化旋转运动组件以在Yaw轴上每秒旋转90度:
```cpp
RotatingMovement = CreateDefaultSubobject   <URotatingMovementComponent>("Rotating Movement");
RotatingMovement->RotationRate = FRotator(0.0, 90.0f, 0);
```
  1. 要最终确定构造函数,请启用复制并禁用Tick功能:
```cpp
bReplicates = true;
PrimaryActorTick.bCanEverTick = false;
```
  1. 实现BeginPlay功能,将开始重叠事件绑定到OnBeginOverlap功能:
```cpp
void APickup::BeginPlay()
{
  Super::BeginPlay();
  Mesh->OnComponentBeginOverlap.AddDynamic(this,     &APickup::OnBeginOverlap);
}
```
  1. Implement the OnBeginOverlap function, which checks whether the character is valid and has authority, removes the pickup on the game state, plays the pickup sound on the owning client, adds 10 points and the pickup to the character. Once all of that is done, the pickup destroys itself.
```cpp
void APickup::OnBeginOverlap(UPrimitiveComponent* OverlappedComp,   AActor* OtherActor, UPrimitiveComponent* OtherComp, int32   OtherBodyIndex, bool bFromSweep, const FHitResult& Hit)
{
  APickupsCharacter* Character =     Cast<APickupsCharacter>(OtherActor);
  if (Character == nullptr || !HasAuthority())
  {
    return;
  }
  APickupsGameState* GameState =     Cast<APickupsGameState>(GetWorld()->GetGameState());
  if (GameState != nullptr)
  {
    GameState->RemovePickup();
  }
  Character->ClientPlaySound2D(PickupSound);
  Character->AddScore(10);
  Character->AddPickup();
  Destroy();
}
```

接下来,我们将学习`PickupsGameState`课。
  1. 打开PickupsGameState.h并声明受保护的复制整数变量PickupsRemaining,该变量告诉所有客户端还有多少皮卡在该级别:
```cpp
UPROPERTY(Replicated, BlueprintReadOnly)
int32 PickupsRemaining;
```
  1. 宣布BeginPlay功能的保护覆盖:
```cpp
virtual void BeginPlay() override;
```
  1. 声明受保护的GetPlayerStatesOrderedByScore功能:
```cpp
UFUNCTION(BlueprintCallable)
TArray<APlayerState*> GetPlayerStatesOrderedByScore() const;
```
  1. 实现公共RemovePickup功能,从PickupsRemaining变量
```cpp
void RemovePickup() { PickupsRemaining--; }
```

中删除一个皮卡
  1. 执行公共HasPickups功能,返回是否还有皮卡剩余:
```cpp
bool HasPickups() const { return PickupsRemaining > 0; }
```
  1. 打开PickupsGameState.cpp,包括Pickup.hGameplayStatics.hUnrealNetwork.h、【T4:
```cpp
#include "Pickup.h"
#include "Kismet/GameplayStatics.h"
#include "Net/UnrealNetwork.h"
#include "GameFramework/PlayerState.h"
```
  1. 实现GetLifetimeReplicatedProps功能,使PickupRemaining变量复制到所有客户端:
```cpp
void APickupsGameState::GetLifetimeReplicatedProps(TArray<   FLifetimeProperty >& OutLifetimeProps) const
{
  Super::GetLifetimeReplicatedProps(OutLifetimeProps);
  DOREPLIFETIME(APickupsGameState, PickupsRemaining);
}
```
  1. 通过获取世界上所有的皮卡:
```cpp
void APickupsGameState::BeginPlay()
{
  Super::BeginPlay();
  TArray<AActor*> Pickups;
  UGameplayStatics::GetAllActorsOfClass(this,     APickup::StaticClass(), Pickups);
  PickupsRemaining = Pickups.Num();
}
```

,实现`BeginPlay`覆盖功能并设置`PickupsRemaining`的值
  1. Implement the GetPlayerStatesOrderedByScore function, which duplicates the PlayerArray variable and sorts it so that the players with the highest scores show up first:
```cpp
TArray<APlayerState*> APickupsGameState::GetPlayerStatesOrderedByScore() const
{
  TArray<APlayerState*> PlayerStates(PlayerArray);
  PlayerStates.Sort([](const APlayerState& A, const APlayerState&     B) { return A.Score > B.Score; });
  return PlayerStates;
}
```

接下来,我们来学习`PickupsPlayerState`课。
  1. 打开PickupsPlayerState.h,声明受保护的复制整型变量Pickups,表示一个玩家收集了多少个皮卡:
```cpp
UPROPERTY(Replicated, BlueprintReadOnly)
int32 Pickups;
```
  1. 实现公共AddPickup功能,为Pickups变量
```cpp
void AddPickup() { Pickups++ ; }
```

增加一个拾音器
  1. 打开PickupsPlayerState.cpp并包含UnrealNetwork.h :
```cpp
#include "Net/UnrealNetwork.h"
```
  1. Implement the GetLifetimeReplicatedProps function and make the Pickups variable replicate to all clients:
```cpp
void APickupsPlayerState::GetLifetimeReplicatedProps(TArray<   FLifetimeProperty >& OutLifetimeProps) const
{
  Super::GetLifetimeReplicatedProps(OutLifetimeProps);
  DOREPLIFETIME(APickupsPlayerState, Pickups);
}
```

接下来,我们来学习`PickupsPlayerController`课。
  1. 打开PickupsPlayerController.h并声明受保护的ScoreboardMenuClass变量,这使得我们想要用于记分板的 UMG 小部件能够被选择:
```cpp
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Pickup   Player Controller")
TSubclassOf<class UUserWidget> ScoreboardMenuClass;
```
  1. 声明受保护的ScoreboardMenu变量,该变量存储我们在BeginPlay函数变量上创建的记分板 UMG 小部件实例:
```cpp
UPROPERTY()
class UUserWidget* ScoreboardMenu;
```
  1. 宣布BeginPlay功能的保护覆盖:
```cpp
virtual void BeginPlay() override;
```
  1. 打开PickupsPlayerController.cpp并包含UserWidget.h :
```cpp
#include "Blueprint/UserWidget.h"
```
  1. Implement the BeginPlay override function, which, for the owning client, creates and adds the scoreboard UMG widget to the viewport:
```cpp
void APickupsPlayerController::BeginPlay()
{
  Super::BeginPlay();
  if (!IsLocalController() || ScoreboardMenuClass == nullptr)
  {
    return;
  }
  ScoreboardMenu = CreateWidget<UUserWidget>(this,     ScoreboardMenuClass);
  if (ScoreboardMenu != nullptr)
  {
    ScoreboardMenu->AddToViewport(0);
  }
}
```

现在,让我们编辑`PickupsGameMode`类:
  1. 打开PickupsGameMode.h,将GameModeBase.hinclude替换为GameMode.h :
```cpp
#include "GameFramework/GameMode.h"
```
  1. 使类派生自AGameMode而不是AGameModeBase :
```cpp
class APickupsGameMode : public AGameMode
```
  1. 声明受保护的游戏状态变量MyGameState,它将实例保存到APickupsGameState类:
```cpp
UPROPERTY()
class APickupsGameState* MyGameState;
```
  1. 将构造函数移动到受保护区域。
  2. 宣布BeginPlay功能的保护覆盖:
```cpp
virtual void BeginPlay() override;
```
  1. 宣布ShouldSpawnAtStartSpot功能的保护覆盖:
```cpp
virtual bool ShouldSpawnAtStartSpot(AController* Player)   override;
```
  1. 声明游戏模式匹配状态功能的受保护覆盖:
```cpp
virtual void HandleMatchHasStarted() override;
virtual void HandleMatchHasEnded() override;
virtual bool ReadyToStartMatch_Implementation() override;
virtual bool ReadyToEndMatch_Implementation() override;
```
  1. 声明受保护的RestartMap功能:
```cpp
void RestartMap();
```
  1. 打开PickupsGameMode.cpp,包括GameplayStatics.hPickupGameState.hEngine/World.hTimerManager.hEngine.h :
```cpp
#include "Kismet/GameplayStatics.h"
#include "PickupsGameState.h"
#include "Engine/World.h"
#include "Engine/Public/TimerManager.h"
#include "Engine/Engine.h"
```
  1. 实现BeginPlay覆盖功能,存储APickupGameState实例:
```cpp
void APickupsGameMode::BeginPlay()
{
  Super::BeginPlay();
  MyGameState = GetGameState<APickupsGameState>();
}
```
  1. 实现ShouldSpawnAtStartSpot覆盖功能,这表明我们希望玩家在随机玩家开始时重生,而不是总是在同一个玩家身上重生:
```cpp
bool APickupsGameMode::ShouldSpawnAtStartSpot   (AController* Player)
{
  return false;
}
```
  1. 实现HandleMatchHasStarted超控功能,打印到屏幕上,通知玩家游戏已经开始:
```cpp
void APickupsGameMode::HandleMatchHasStarted()
{
  Super::HandleMatchHasStarted();
  GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, "The     game has started!");
}
```
  1. 实现HandleMatchHasEnded覆盖功能,打印到屏幕上,通知玩家游戏已经结束,销毁所有角色,并安排计时器重启地图:
```cpp
void APickupsGameMode::HandleMatchHasEnded()
{
  Super::HandleMatchHasEnded();
  GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, "The     game has ended!");
  TArray<AActor*> Characters;
    UGameplayStatics::GetAllActorsOfClass(this,     APickupsCharacter::StaticClass(), Characters);
  for (AActor* Character : Characters)
  {
    Character->Destroy();
  }
  FTimerHandle TimerHandle;
  GetWorldTimerManager().SetTimer(TimerHandle, this,     &APickupsGameMode::RestartMap, 5.0f);
}
```
  1. 执行ReadyToStartMatch_Implementation超越功能,表示比赛可以直接开始:
```cpp
bool APickupsGameMode::ReadyToStartMatch_Implementation()
{
  return true;
}
```
  1. 执行ReadyToEndMatch_Implementation超控功能,当游戏状态没有剩余拾取时,表示比赛结束:
```cpp
bool APickupsGameMode::ReadyToEndMatch_Implementation()
{
  return MyGameState != nullptr && !MyGameState->HasPickups();
}
```
  1. Implement the RestartMap function, which indicates that the server travels to the same level and brings all clients along (only in the packaged version):
```cpp
void APickupsGameMode::RestartMap()
{
  GetWorld()->ServerTravel(GetWorld()->GetName(), false, false);
}
```

现在,让我们编辑`PickupsCharacter`类。
  1. 打开PickupsCharacter.h并声明下降着陆保护声音变量:
```cpp
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category =   "Pickups Character")
USoundBase* FallSound;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category =   "Pickups Character")
USoundBase* LandSound;
```
  1. 声明受保护的override功能:
```cpp
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason)   override;
virtual void Landed(const FHitResult& Hit) override;
virtual void FellOutOfWorld(const UDamageType& DmgType) override;
```
  1. 声明给玩家状态增加分数和拾取的公共功能:
```cpp
void AddScore(const float Score);
void AddPickup();
```
  1. 声明在所属客户端播放声音的公共客户端 RPC:
```cpp
UFUNCTION(Client, Unreliable)
void ClientPlaySound2D(USoundBase* Sound);
```
  1. 打开PickupsCharacter.cpp,包括PickupsPlayerState.hGameMode.hGameplayStatics.h :
```cpp
#include "PickupsPlayerState.h"
#include "GameFramework/GameMode.h"
#include "Kismet/GameplayStatics.h"
```
  1. 执行EndPlay覆盖功能,如果角色被破坏,播放坠落声:
```cpp
void APickupsCharacter::EndPlay(const EEndPlayReason::Type   EndPlayReason)
{
  Super::EndPlay(EndPlayReason);
  if (EndPlayReason == EEndPlayReason::Destroyed)
  {
    UGameplayStatics::PlaySound2D(GetWorld(), FallSound);
  }
}
```
  1. 执行Landed超越功能,播放落地声音:
```cpp
void APickupsCharacter::Landed(const FHitResult& Hit)
{
  Super::Landed(Hit);
  UGameplayStatics::PlaySound2D(GetWorld(), LandSound);
}
```
  1. 实现FellOutOfWorld覆盖功能,存储控制器,从分数中移除10点,破坏角色(使控制器无效),并告知游戏模式使用之前的控制器重启玩家:
```cpp
void APickupsCharacter::FellOutOfWorld(const UDamageType&   DmgType)
{
  AController* PreviousController = Controller;
  AddScore(-10);
  Destroy();
  AGameMode* GameMode = GetWorld()->GetAuthGameMode<AGameMode>();
  if (GameMode != nullptr)
  {
    GameMode->RestartPlayer(PreviousController);
  }
}
```
  1. 实现AddScore功能,在玩家状态
```cpp
void APickupsCharacter::AddScore(const float Score)
{
  APlayerState* MyPlayerState = GetPlayerState();
  if (MyPlayerState != nullptr)
  {
    MyPlayerState->Score += Score;
  }
}
```

下给`Score`变量加一个分数
  1. 实现AddPickup功能,在我们的自定义玩家状态
```cpp
void APickupsCharacter::AddPickup()
{
  APickupsPlayerState* MyPlayerState =     GetPlayerState<APickupsPlayerState>();
  if (MyPlayerState != nullptr)
  {
    MyPlayerState->AddPickup();
  }
}
```

中为`Pickup`变量添加一个拾取
  1. 实现ClientPlaySound2D_Implementation功能,在所属客户端播放声音:
```cpp
void APickupsCharacter::ClientPlaySound2D_Implementation(USoundBase*   Sound)
{
  UGameplayStatics::PlaySound2D(GetWorld(), Sound);
}
```
  1. Open Pickups.Build.cs and add the UMG module to PublicDependencyModuleNames, like so:
```cpp
PublicDependencyModuleNames.AddRange(new string[] { "Core",   "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay",   "UMG" });
```

如果您试图编译并从添加新模块中获得错误,那么请清理并重新编译您的项目。如果这不起作用,请尝试重新启动您的 IDE。
  1. Compile and run the code until the editor loads.
首先,让我们导入声音文件。
  1. Content Browser中,创建并转到Content\Sounds文件夹。
  2. Exercise18.02\Assets文件夹导入 Pickup.wavFootstep.wavJump.wavLand.wavFall.wav
  3. Save the new files.
接下来,让我们将`Play Sound`动画通知添加到角色的一些动画中。
  1. 打开位于Content\Mannequin\AnimationsThirdPersonJump_Start animation,使用Jump声音在0框添加一个Play Sound动画通知。
  2. 保存并关闭ThirdPersonJump_Start
  3. 打开位于Content\Mannequin\AnimationsThirdPersonRun动画,在时间 0.24 秒和 0.56 秒添加两个Play Sound动画通知。
  4. 保存并关闭ThirdPersonRun
  5. 打开位于Content\Mannequin\AnimationsThirdPersonWalk动画,在时间 0.24 秒和 0.79 秒添加两个Play Sound动画通知。
  6. Save and close ThirdPersonWalk.
现在,让我们为角色蓝图设置声音。
  1. 打开位于Content\ThirdPersonCPP\BlueprintsThirdPersonCharacter蓝图,设置Fall SoundLand Sound分别使用声音FallLand
  2. Save and close ThirdPersonCharacter.
现在,让我们创建皮卡的蓝图。
  1. 创建并打开Content\Blueprints文件夹。
  2. 创建一个从Pickup类派生的名为BP_Pickup的新蓝图,并将其打开。
  3. Configure the Static Mesh component in the following way:
```cpp
Scale = 0.5, 0.5, 0.5	
Static Mesh = Engine\BasicShapes\Cube
Material Element 0 = CubeMaterial
```

注意

要显示引擎内容,您需要转到静态网格下拉菜单右下角的“查看选项”,并确保“显示引擎内容”标志设置为真。
  1. Pickup Sound变量设置为使用Pickup声音。
  2. Save and close BP_Pickup.
接下来,让我们创建记分板 UMG 小部件。
  1. 创建并转到Content\UI文件夹。
  2. 创建名为UI_Scoreboard_Header的新小部件蓝图: * 在根画布面板上添加名为tbName的文本块,其中Is Variable设置为trueSize To Content设置为trueText设置为Player NameColor and Opacity设置为使用颜色green。 * 在根画布面板上添加名为tbScore的文本块,其中Is Variable设置为truePosition X = 500Alignment = 1.0, 0.0Size To Content设置为trueText设置为Score,而Color and Opacity设置为使用颜色green。 * 在根画布面板上添加名为tbPickups的文本块,其中Is Variable设置为truePosition X = 650Alignment = 1.0, 0.0Size To Content设置为trueText设置为Pickups,而Color and Opacity设置为使用颜色green
  3. Hierarchy面板中,选择三个新的文本块并复制它们。
  4. 保存并关闭UI_Scoreboard_Header
  5. 回到Content\UI,新建一个名为UI_Scoreboard_Entry的 UMG 小部件,打开它。
  6. 将复制的文本块粘贴在根画布面板上,改为white而不是green,并使它们都是变量。
  7. Go to the Graph section and create the Player State variable with the following configuration:
![Figure 18.3: Creating the Player State variable ](img/B16183_18_03.jpg)

图 18.3:创建玩家状态变量
  1. Go back to the Designer section and create a bind for tbName that does the following:
![Figure 18.4: Displaying the player name ](img/B16183_18_04.jpg)

图 18.4:显示玩家姓名

注意

您可以在以下链接找到前面的全分辨率截图,以便更好地查看:[https://packt.live/3pCk9Nt](https://packt.live/3pCk9Nt)。
  1. Create a bind for tbScore that does the following:
![Figure 18.5: Displaying the player score ](img/B16183_18_05.jpg)

图 18.5:显示玩家得分

注意

您可以在以下链接找到前面的全分辨率截图,以便更好地查看:[https://packt.live/3nuckYv](https://packt.live/3nuckYv)。
  1. Create a bind for tbPickups that does the following:
![Figure 18.6: Displaying the pickups count ](img/B16183_18_06.jpg)

图 18.6:显示提货数量

注意

您可以在以下链接找到前面的全分辨率截图,以便更好地查看:[https://packt.live/36pEGMz](https://packt.live/36pEGMz)。
  1. Create a pure function called Get Typeface that does the following:
![Figure 18.7: Determining whether the entry should be displayed in bold or regular ](img/B16183_18_07.jpg)

图 18.7:确定条目应该以粗体还是常规显示

注意

您可以在以下链接找到前面的全分辨率截图,以便更好地查看:[https://packt.live/2JW9Zam](https://packt.live/2JW9Zam)。

在前面的代码中,我们使用了一个选择节点,它可以通过从返回值中拖动一根线并将其释放到空白空间来创建,并在过滤器上键入“select”。从那里,我们从列表中选择选择节点。在这个特定的函数中,我们使用选择节点来选择我们将要使用的字体的名称,所以如果玩家状态的棋子与拥有小部件的棋子不同,它应该返回`Regular`,如果是,则返回`Bold`。我们这样做是为了用粗体突出显示玩家状态条目,以便玩家知道他们的条目是什么。
  1. Implement Event Construct in the following way:
![Figure 18.8: The Event Graph that sets the text for the name, score, and pickups count ](img/B16183_18_08.jpg)

图 18.8:为名称、分数和提货数量设置文本的事件图

注意

您可以在以下链接找到前面的全分辨率截图,以便更好地查看:[https://packt.live/2JOdP58](https://packt.live/2JOdP58)。

在前面的代码中,我们设置了`tbName`、`tbScore`和`tbPickups`的字体来使用`Bold`字体来突出显示哪个记分板条目相对于当前客户端的玩家。对于其余的玩家,使用`Regular`字样。
  1. 保存并关闭UI_Scoreboard_Entry
  2. 回到Content\UI然后创建一个名为UI_Scoreboard的新 UMG 小部件并打开它。
  3. 在根画布面板上添加一个名为vbScoreboard的垂直框,并启用Size To Content
  4. 向名为tbGameInfovbScoreboard添加一个文本块,其Text值默认为Game Info
  5. 转到Graph部分,创建一个名为Pickups Game State类型的新变量Game State
  6. Implement Event Construct in the following way:
![Figure 18.9: The Event Construct that sets a timer to update  the scoreboard every 0.5 seconds ](img/B16183_18_09.jpg)

图 18.9:设置计时器每 0.5 秒更新一次记分板的事件构造

注意

您可以在以下链接找到前面的全分辨率截图,以便更好地查看:[https://packt.live/3kemyu0](https://packt.live/3kemyu0)。

在前面的代码中,我们获取了游戏状态实例,更新了记分板,并安排了一个计时器每 0.5 秒自动更新一次记分板。
  1. Go back to the designer section and make the following bind for vbScoreboard:
![Figure 18.10: Displaying the number of pickups remaining in the world ](img/B16183_18_10.jpg)

图 18.10:显示世界上剩余的皮卡数量

注意

您可以在以下链接找到前面的全分辨率截图,以便更好地查看:[https://packt.live/38xUDTE](https://packt.live/38xUDTE)。
  1. Add a vertical box to vbScoreboard called vbPlayerStates with Is Variable set to true and a top padding of 50, so you should have the following:
![Figure 18.11: The UI_Scoreboard widget hierarchy ](img/B16183_18_11.jpg)

图 18.11:用户界面记分板小部件层次结构
  1. Go back to the Graph section and implement the Update Scoreboard event in the following way:
![Figure 18.12: The update scoreboard function, which clears and recreates the entry widgets ](img/B16183_18_12.jpg)

图 18.12:更新记分板功能,清除并重新创建条目小部件

注意

您可以在以下链接找到前面的全分辨率截图,以便更好地查看:[https://packt.live/3pf8EeN](https://packt.live/3pf8EeN)。

在前面的代码中,我们执行了以下操作:

*   清除`vbPlayerStates`中所有先前的条目。
*   创建记分板标题条目并将其添加到`vbPlayerStates`。
*   循环遍历所有按分数排序的玩家状态,为每个状态创建一个条目,并将其添加到`vbPlayerStates`中。
  1. Save and close UI_Scoreboard.
现在,让我们为玩家控制器创建蓝图。
  1. 转到Content\Blueprints并创建一个名为BP_PlayerController的新蓝图,该蓝图源自PickupPlayerController类。
  2. 打开新蓝图,设置Scoreboard Menu Class使用UI_Scoreboard
  3. Save and close BP_PlayerController.
接下来,让我们为游戏模式创建蓝图。
  1. Go to Content\Blueprints and create a new blueprint called BP_GameMode that is derived from the PickupGameMode class, open it, and change the following variables:
```cpp
Game State Class = PickupsGameState
Player Controller Class = BP_PlayerController
Player State Class = PickupsPlayerState
```

接下来,让我们配置`Project Settings`使用新的游戏模式。
  1. 转到Project Settings,从左侧面板中选择Maps & Modes,在Project类别中。
  2. 设置Default GameMode使用BP_GameMode
  3. Close Project Settings.
现在,让我们修改主级别。
  1. 确保你已经打开了ThirdPersonExampleMap,位于Content\ThirdPersonCPP\Maps
  2. 添加一些立方体演员作为平台,并确保他们之间有间隙,以迫使玩家跳到他们身上,并可能从水平下降。
  3. 在地图的不同部分添加几个玩家开始的演员。
  4. 添加至少 50 个BP_Pickup实例,并将它们分布在整个地图上。
  5. Here is an example of a possible way of configuring the map:
![Figure 18.13: An example of the map configuration ](img/B16183_18_13.jpg)

图 18.13:地图配置示例
  1. 运行代码,等待编辑器完全加载。
  2. 转到Multiplayer Options,将客户端数量设置为2
  3. 将窗口大小设置为800x600
  4. New Editor Window (PIE)中播放:

Figure 18.14: The listen Server and Client 1 picking up cubes in the world

图 18.14:监听服务器和客户端 1 拾取世界上的立方体

一旦你完成了这个练习,你将能够在每个客户端上玩,你会注意到角色可以收集拾音器,并通过与它们重叠来获得10点。如果一个角色从关卡中掉落,它将在随机玩家开始时重生,并失去10点。

一旦所有皮卡都被收集完毕,游戏将结束,在5秒后,它将执行一次服务器旅行来重新加载相同的等级,并将所有客户端都带上(仅在打包版本中)。您还可以看到,用户界面显示了关卡中还有多少拾取器,以及记分板,其中包含每个玩家的姓名、分数和拾取器的信息。

活动 18.01:在多人 FPS 游戏中增加死亡、重生、记分牌、击杀限制和拾取

在本活动中,您将为角色添加死亡、重生的概念以及使用拾音器的能力。我们还将添加一种检查记分板的方法和游戏的致命限制,以便它有一个最终目标。

以下步骤将帮助您完成本活动:

  1. 活动 17.01打开MultiplayerFPS项目,为多人 FPS 游戏添加武器和弹药。编译代码并运行编辑器。
  2. 接下来,您将创建我们需要的 C++ 类。创建一个名为FPSGameState的 C++ 类,该类从GameState类派生而来,有一个 kill limit 变量和一个返回 kill 排序的玩家状态的函数。
  3. 创建一个名为FPSPlayerState的 C++ 类,这个类是从PlayerState类派生出来的,存储一个玩家的击杀次数和死亡次数。
  4. 创建一个名为PlayerMenu的 C++ 类,这个类是从UserWidget类派生出来的,有一些BlueprintImplementableEvent功能可以切换记分板可见性,设置记分板可见性,当有玩家被杀时通知。
  5. 创建一个名为FPSPlayerController的 C++ 类,它从APlayerController派生而来,在拥有的客户端上创建PlayerMenu UMG 小部件实例。
  6. 创建一个名为Pickup的 C++ 类,它是从Actor类派生出来的,有一个在Yaw轴上每秒旋转 90 度的静态网格,玩家可以在重叠处拾取。一旦被拾取,它将播放拾取声音,并禁用碰撞和可见性。一定时间后,它会变得可见,并能够再次碰撞。
  7. 创建一个名为AmmoPickup的 C++ 类,这个类是从Pickup类派生出来的,给玩家增加一定量的一种弹药类型。
  8. 创建一个名为ArmorPickup的 C++ 类,由Pickup类派生而来,给玩家增加一定的护甲。
  9. 创建一个名为HealthPickup的 C++ 类,由Pickup类派生而来,为玩家增加一定的生命值。
  10. 创建一个名为WeaponPickup的 C++ 类,由Pickup类派生而来,给玩家增加一定的武器类型。如果玩家已经拥有武器,它会增加一定数量的弹药。
  11. 编辑FPSCharacter类,使其执行以下操作: * 角色受损后,会检查是否死亡。如果它死了,它会记录杀手角色的杀戮和角色的死亡,以及玩家的重生。如果角色没有死,它会在拥有它的客户身上播放痛苦的声音。 * 当角色死亡并执行EndPlay功能时,应该会摧毁其所有武器实例。 * 如果角色从世界上掉落,它会记录玩家的死亡并重生。 * 如果玩家按下标签键,将切换记分板菜单的可见性。
  12. 编辑MultiplayerFPSGameModeBase类,使其执行以下操作: * 存储赢得游戏所需的击杀次数。 * 使用新的玩家控制器、玩家状态和游戏状态类。 * 使其实现匹配状态功能,以便匹配立即开始,如果有玩家拥有所需的击杀次数,则匹配结束。 * 当比赛结束时,它将在 5 秒钟后执行服务器到同一级别的旅行。 * 通过将杀死(当被另一个玩家杀死时)和死亡添加到各自的玩家状态中来处理玩家死亡的情况,以及在玩家随机开始时对该玩家进行重生。
  13. Activity18.01\Assets进口AmmoPickup.wavContent\Pickups\Ammo
  14. AAmmoPickup创建BP_PistolBullets_Pickup,将其放入Content\Pickups\Ammo,并用以下值配置: * 比例尺:(X=0.5, Y=0.5, Z=0.5) * 静态网格:Engine\BasicShapes\Cube * 材料:Content\Weapon\Pistol\M_Pistol * 弹药类型:Pistol Bullets,弹药数量:25 * 拾音:Content\Pickup\Ammo\AmmoPickup
  15. AAmmoPickup创建BP_MachineGunBullets_Pickup,将其放入Content\Pickups\Ammo,并用以下值配置: * 比例尺:(X=0.5, Y=0.5, Z=0.5) * 静态网格:Engine\BasicShapes\Cube * 材料:Content\Weapon\MachineGun\M_MachineGun * 弹药类型:Machine Gun Bullets,弹药数量:50 * 拾音:Content\Pickup\Ammo\AmmoPickup
  16. AAmmoPickup创建BP_Slugs_Pickup,将其放入Content\Pickups\Ammo,并用以下值配置: * 比例尺:(X=0.5, Y=0.5, Z=0.5) * 静态网格:Engine\BasicShapes\Cube * 材料:Content\Weapon\Railgun\M_Railgun * 弹药类型:Slugs,弹药数量:5 * 拾音:Content\Pickup\Ammo\AmmoPickup
  17. Activity18.01\Assets进口ArmorPickup.wavContent\Pickups\Armor
  18. Content\Pickups\Armor中创建素材M_Armor,将Base Color设置为blue,将Metallic设置为1
  19. AArmorPickup创建BP_Armor_Pickup,将其放入Content\Pickups\Armor,并用以下值配置: * 比例尺:(X=1.0, Y=1.5, Z=1.0) * 静态网格:Engine\BasicShapes\Cube * 材料:Content\Pickup\Armor\M_Armor * 护甲数量:50 * 拾音:Content\Pickup\Armor\ArmorPickup
  20. Activity18.01\Assets进口HealthPickup.wavContent\Pickups\Health
  21. Content\Pickups\Health中创建素材M_Health,将Base Color设置为blue,将Metallic / Roughness设置为0.5
  22. AHealthPickup创建BP_Health_Pickup,将其放入Content\Pickups\Health,并用以下值配置: * 静态网格:Engine\BasicShapes\Sphere * 材料:Content\Pickup\Health\M_Health * 健康金额:50 * 拾音:Content\Pickup\Health\HealthPickup
  23. Activity18.01\Assets进口WeaponPickup.wavContent\Pickups\Weapon
  24. AWeaponPickup创建BP_Pistol_Pickup,将其放入Content\Pickups\Weapon,并用以下值配置: * 静态网格:Content\Pickup\Weapon\SM_Weapon * 材料:Content\Weapon\Pistol\M_Pistol * 武器类型:Pistol,弹药量:25 * 拾音:Content\Pickup\Weapon\WeaponPickup
  25. AWeaponPickup创建BP_MachineGun_Pickup,将其放入Content\Pickups\Weapon,并用以下值配置: * 静态网格:Content\Pickup\Weapon\SM_Weapon * 材料:Content\Weapon\MachineGun\M_MachineGun * 武器类型:Machine Gun,弹药量:50 * 拾音:Content\Pickup\Weapon\WeaponPickup
  26. AWeaponPickup创建BP_Pistol_Pickup,将其放入Content\Pickups\Weapon,并用以下值配置: * 静态网格:Content\Pickup\Weapon\SM_Weapon * 材料:Content\Weapon\Railgun\M_Railgun * 武器类型:Railgun,弹药量:5 * 拾音:Content\Pickup\Weapon\WeaponPickup
  27. Activity18.01\Assets进口Land.wavPain.wavContent\Player\Sounds
  28. 编辑BP_Player使其使用PainLand声音,并删除所有在Begin Play事件中创建UI_HUD实例并将其添加到视口的节点。
  29. Content\UI中创建一个名为UI_Scoreboard_Entry的 UMG 部件,显示AFPSPlayerState的名称、死亡人数和等级。
  30. 创建一个名为UI_Scoreboard_Header的 UMG 小部件,显示名称、死亡数和 ping 的标题。
  31. 创建一个名为UI_Scoreboard的 UMG 小部件,显示游戏状态下的击杀限制,一个以UI_Scoreboard_Header为第一个条目的垂直框,然后在游戏状态实例中为每个AFPSPlayerState添加一个UI_Scoreboard_Entry。垂直框将通过计时器每 0.5 秒更新一次,方法是清除其子框并再次添加它们。
  32. 编辑UI_HUD使其添加一个名为tbKilled的新文本块,该文本块以设置为HiddenVisibility开始。当玩家杀人时,会使文本块可见,显示被杀玩家的名字,1 秒后隐藏。
  33. UPlayerMenu创建一个名为UI_PlayerMenu的新蓝图,并将其放置在Content\UI中。使用带有索引0中的UI_HUD实例和索引1中的UI_Scoreboard实例的小部件切换器。在事件图中,确保覆盖在 C++ 中被设置为BlueprintImplementableEventToggle ScoreboardSet Scoreboard VisibilityNotify Kill事件。Toggle Scoreboard事件在01之间切换小部件切换器的活动索引,Set Scoreboard Visibility事件将小部件切换器的活动索引设置为01Notify Kill事件告诉UI_HUD实例设置文本并淡出动画。
  34. AFPSPlayerController创建BP_PlayerController,放入Content文件夹,设置PlayerMenuClass变量使用UI_PlayerMenu
  35. 编辑BP_GameMode并设置Player Controller Class使用BP_PlayerController
  36. Project SettingsInput部分,使用TAB键创建一个名为Scoreboard的动作映射。
  37. Edit the DM-Test level so that you have at least three new player starts placed in different locations, Kill Z to -500 in World Settings, and an instance placed of every different pickup.
预期产出:

![Figure 18.15: The expected output of the activity ](img/B16183_18_15.jpg)

图 18.15:活动的预期输出

结果应该是一个项目,每个客户的角色可以拿起,使用和切换三种不同的武器。如果一个角色杀死了另一个角色,它应该记录杀死和死亡,并对随机玩家开始时死亡的角色进行重生。你应该有一个记分牌,显示每个玩家的名字、死亡人数、死亡人数和等级。一个角色可以从关卡中掉落,只能算作死亡,并在随机玩家开始时重生。角色还应该能够在关卡中拾取不同的拾取物来获得弹药、护甲、生命值和武器。通过显示记分牌和服务器在 5 秒钟后移动到同一水平,当达到杀死限制时,游戏应该结束。

注意

这个活动的解决方案可以在:https://packt.live/338jEBx找到。

总结

在本章中,您了解到游戏框架类的实例存在于某些游戏实例中,但不存在于其他游戏实例中。拥有这些知识将有助于您了解在特定的游戏实例中可以访问哪些实例。您还学习了游戏状态和玩家状态类的目的,以及学习游戏模式的新概念和一些有用的内置功能。

在这一章的最后,你已经做了一个基本但实用的多人射击游戏,可以作为一个基础来建立。你可以添加新的武器,弹药类型,射击模式,皮卡,等等,让它更加功能齐全和有趣。

看完这本书,你现在应该对如何使用虚幻引擎 4 让你自己的游戏变得真实有了更好的了解。我们在这本书里讨论了很多话题,从简单的到更高级的。您首先学习了如何使用不同的模板创建项目,以及如何使用蓝图来创建参与者和组件。然后,您看到了如何通过导入和设置所需的资产,设置动画蓝图和混合空间,创建自己的游戏模式和角色,以及定义和处理输入,从头开始创建一个功能完整的第三人模板。

然后你继续你的第一个项目;一个简单的隐形游戏,使用游戏物理和碰撞,投射物运动组件,演员组件,界面,蓝图函数库,UMG,声音和粒子效果。接下来,您学习了如何使用人工智能、动画蒙太奇和可破坏网格创建一个简单的侧滚游戏。最后,您发现了如何使用网络框架附带的服务器-客户端体系结构、变量复制和 RPC 来创建第一人称多人射击游戏,并了解了玩家状态、游戏状态和游戏模式类的工作原理。

通过从事使用引擎不同部分的各种项目,您现在对虚幻引擎 4 的工作原理有了很强的理解,虽然这是本书的结尾,但这只是您使用虚幻引擎 4 进入游戏开发世界之旅的开始。