# Unreal Engine的Gameplay框架的初级读物

此文为Unreal Engine Gameplay Framework Primer for C++ (opens new window)的原创翻译,本文内容版权归原文所有,仅供学习,如需转载望注本文地址,翻译不易,谢谢理解。

Unreal Engine 4的游戏框架提供了一个强大的类的集合来帮你创建游戏,你的游戏可能是个射击游戏,农场模拟,或者深度的RPG,这些都不重要,这个框架很灵活,做了一些繁重的工作并且设定了一些标准。它和UE4引擎有深度的整合以致笔者直接建议要坚持用这些类而不是像在Unity3d中试着创建自己的游戏框架。理解这个框架对于成功有效地创建自己的工程是至关重要的。

# 这是为谁准备的

任何对使用UE4创建游戏有兴趣的人,尤其那些使用C++并且想学习更多关于Unreal游戏框架的人。这篇博客覆盖了游戏框架中的核心类和它们的应用场景,它们是如何被引擎初始化,如何从你的其他部分游戏代码来访问这些类。大部分提供的信息同样也适用于蓝图。

# Gameplay框架类

在使用UE4创建游戏你将会发现很多样板文件已经为你准备好了。在使用C++或蓝图制作游戏过程中你将频繁使用几个类。我将带你熟悉这些类和它们优雅的特性以及如何从你代码的其他部分引用它们。这里的大部分信息同样也适用于蓝图,尽管我使用没有暴露给蓝图的C++代码片段和一些函数,因此这部分只和C++用户有关。

# Actor

这可能是你在游戏中使用最多的类。Actor是在关卡中任何物体的基类,包括玩家,AI敌人,门,窗和Gameplay的对象。Actors通过使用ActorComponents被组合起来,比如StaticMeshComponent,CharacterMovementComponent,ParticleComponent还有很多其他的。甚至像GameMode一样的类也是Actors,即便GameMode并没有真的位置在场景世界中。下面再来说一些关于Actors你需要知道的事情。

Actor在多人游戏中是可在网络中多重复制的类,通过在构造器中设置SetReplicates(true)来实现。Actor还有很多有效处理网络的特性,这里就不一一列举了。

Actor支持从外部承受伤害的概念。可使用MyActor->TakeDamage(...)或通过UGameplayStatics::ApplyDamage(...)直接将伤害应用到Actor上,注意有很多PointDamage(点射武器)和RadialDamage(爆炸,范围伤害)的可用变种。

你可以容易地在代码中使用GetWorld()->SpawnActor<T>(...)来生成一个新的Actor实例,T是要生成Actor的类型,比如你自己的一种AActor像AGadgetActor,AGameplayProp等。

下面是一段Actor在运行时生成的代码:

FTransform SpawnTM;
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SpawnParams.Owner = GetOwner();
 /* Attempt to assign an instigator (used for damage application) */
SpawnParams.Instigator = Cast<APawn>(GetOwner());

ASEquippableActor* NewItem = GetWorld()->SpawnActor<ASEquippableActor>(NewItemClass, SpawnTM, SpawnParams);
1
2
3
4
5
6
7
8

还有很多方法去使用Actors,通常对于你感兴趣的Actor你会有一个指针或引用。同上面例子一样,我们可以持有Actor的指针来生成新的变量和操作已有的Actor实例。

一个在写游戏原型时特别有用的函数是UGameplayStatics::GetAllActorsOfClass(...),它使你得到一个所有你传入的Actor类类型的对象数组,包括派生类,可能有些并不是你想要的类,这个函数通常是不推荐使用的,因为它不是你和环境交互的有效方法,但有时候它却是你唯一能使用的工具。

Actors并不拥有平移,旋转或缩放的属性,要设置或获取这些属性得通过RootComponent,比如在SceneComponents层级中的顶层组件,普遍使用的函数像MyActor->GetActorLocation()通常会到RootComponent中返回它的世界位置。

下面是一些你在Actor上下文中会使用到的一些有用函数:

  • BeginPlay 在Actor生成和完全初始化的时候第一个被调用的函数,它只被调用一次。

  • Tick 在每一帧被调用,对大部分Actors会因为性能原因将该特性关闭,但它默认是被开启的。可以快速建立动态逻辑然后在每一帧检查,但最后优化时,你会移动大部分代码到事件驱动逻辑部分或降低检查的频率。

  • EndPlay 当Actor被场景世界移除时调用,包含一个"EEndPlayReason"来指出为什么它被调用了。

  • GetComponentByClass 找到一个指定类的组件实例,在你不Actor的确切类型但知道该Actor有确定的某个组件类型时非常有用,它会返回找到的全部组件实例。

  • GetActorLocation 该函数还有很多变种,比如*Rotation,*Scale包含SetActorLocation等等。

  • NotifyActorBeginOverlap 灵活地检查它的任意一个组件触发的重叠,可以用这个方法快速地建立gameplay的触发器。

  • GetOverlappingActors 容易地找出正在重叠当前Actor的其他Actor,一个组件变种是GetOverlappingComponents。

Actor包含很多函数和变量,它是gameplay框架的基石,一个很好的方法去深入探索了解这个类是打开Actor.h头文件去看它的实现。

# ActorComponent

Actors里面的组件,通用组件包含StaticMeshComponent,CharacterMovementComponent,CameraComponent,和SphereComponent。每个组件负责一项具体的任务,比如移动,物理交互(一个只检测重叠Actors的碰撞体积),或者视觉上在场景世界中显示一些东西比如一个player的网格。

这个组件的一个子类是SceneComponent,它是所有拥有Transform(Position,Rotation,Scale)和支持attachment的基类。比如你可以将你的CameraComponent依附在SpringArmComponent来完成一个第三人称摄像机,使用Transform和attachment都需要合理的管理相对位置。

组件通常在Actor的构造器中创建,你可以在运行时创建和摧毁组件。首先来看一个Actor的构造器:

ASEquippableActor::ASEquippableActor()
{
 PrimaryActorTick.bCanEverTick = true;

 MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp"));
 MeshComp->SetCollisionObjectType(ECC_WorldDynamic); // Just setting a property on our component
 RootComponent = MeshComp; // Set our root component for other SceneComponents to attach to

ItemComp = CreateDefaultSubobject<UItemComponent>(TEXT("ItemComp")); // not attached to anything (not a SceneComponent with a transform)

}
1
2
3
4
5
6
7
8
9
10
11

上面中USkeletalMeshComponent由CreateDefaultSubobject<T>创建,这是Actor的一个方法,它需要指定一个名字(你可能会在Blueprint的组件列表中看到这个名字),如果你常用C++创建游戏的话你会经常使用这个方法,但是这个方法只能在构造器的上下文中使用。

你可能注意到我们设置MeshComp成为新的RootComponent。现在任何场景组件必须都被依附到这个mesh上,我们可通过下面实现:

WidgetComp = CreateDefaultSubobject<UWidgetComponent>(TEXT("InteractWidgetComp"));
WidgetComp->SetupAttachment(MeshComp);
1
2

上面的SetupAttachment将为我们处理依附的初始阶段,它可被除了RootComponent之外的所有场景组件在构造器中调用。你可能好奇为什么ItemComponent并没有调用SetupAttachment函数,这仅仅因为这个组件是一个ActorComponent而不是一个SceneComponent,它没有Transform,因此也没有必要被添加到组件层级中。无论怎样,这个组件将会在Actor中注册,即便它是与组件层级分离的,像MyActor->GetComponentByClass这样的函数仍会返回任何的ActorComponents和SceneComponents。

与Actor一起,这些组件在用C++和Blueprint构建游戏时都是很重要的。他们是真正构建游戏的组件,你能容易地创建自己定制的组件然后让他们在游戏中处理一些具体的东西,比如HealthComponent会有受击点(hitpoints),它接收宿主Actor受到的伤害。

在gameplay中你可以使用下面的代码生成你自己的组件。它和CreateDefaultSubobject是不同的,因为CreateDefaultSubobject仅能在构造器中使用。

 UActorComponent* SpawnedComponent = NewObject<UActorComponent>(this, UStaticMeshComponent::StaticClass(), TEXT("DynamicSpawnedMeshCompoent"));
 if (SpawnedComponent)
 {
 SpawnedComponent->RegisterComponent();
 }
1
2
3
4
5

一些ActorComponents内置有用的函数包含:

  • TickComponent() 就像Actor的Tick()在每一帧中做高频率检查逻辑。
  • bool bIsActive和一些函数像Activate,Deactivate...用来完全地激活/禁用组件(包含TickComponent),这个过程不用摧毁组件或将它从Actor上移除。

要激活ActorComponent的网络复制,需要调用SetIsReplicated(true),它在名称上和Actor的函数稍稍有些不同。它仅适用这种场景,当你尝试去复制针对某个组件的一块具体的逻辑,比如函数调用的变量,不用所有的组件在这Actor上被复制。

# PlayerController

代表player的主要类,接受用户的输入。PlayerController本身并不在环境中可视地显现出来,它会控制Pawn实例来定义玩家在场景世界中视觉和物理特性的展现。在一个gameplay里一个player可能控制多个不同的Pawns(比如一辆车,在重新生成时会有一份全新的拷贝)尽管PlayerController实例在关卡中一直保持不变。这很重要因为有些时候PlayerController可能并没有任何pawn实例,这意味着有些事情比如菜单的打开应该被添加到PlayerController里而不是Pawn类中。

在多人游戏中PlayerController值存在自己的客户端和服务器端,这意味着在一个四人的游戏中,服务器端有四个PlayerController,然后每个客户端有一个自己的PlayerController,这有助于理解这种情况,当所有players需要一个player的变量被复制时,它绝不应该在PlayerController中而是在Pawn甚至是在PlayerState中。

# 得到PlayerController

GetWorld()->GetPlayerControllerIterator()
GetWorld在任何Actor实例中都可用

PlayerState->GetOwner()
PlayerState的拥有者是PlayerController类型,你自己需要将它cast转换成PlayerController

Pawn->GetController()
只有当Pawn被PlayerController当前控制时,才能得到PlayerController。
1
2
3
4
5
6
7
8

这个类包含PlayerCameraManager,它用来处理视角目标和camera transform包括camera抖动。另外一个PlayerController处理的重要类是HUD,它用来Canvas渲染,自从UMG出现后就用的不多了,但是当它用来管理一些你想传给UMG接口的数据时还是很有用的。

在任何一个新的player加入GameMode的时候,一个新的PlayerController通过GameModeBase类中的Login()为这个player创建。

# AIController

同PlayerController类的概念相似,但它仅针对于AI代理而不是players。它像PlayerController一样会控制Pawn,是AI代理的"大脑"。Pawn是AI代理的视觉代表而AIController是AI代理的决策者。

行为树通过这个控制器运行,所以它能处理感知数据(AI可以看或听)然后按决策让Pawn行动。

# 得到AIController

同PlayerController一样,可以从一个Pawn获取AIController,C++中通过GetController,蓝图中可通过GetAIController函数,这个节点的输入希望是被AI控制的Pawn。

# Spawning

如果想让AIController自动生成可通过在Pawn类中设置变量"Auto Possess AI"。如果你期望类自动产生确保你在Pawn中设置了"AI Controller Class"。

# Pawn

它是player或AI控制的东西的物理和视觉方面的代表。它可能是一个车辆,战士,炮塔或者任何代表你游戏角色的东西。Pawn一个常见的子类是Character,它拥有SkeletalMesh和CharacterMovementComponent,后者拥有很多微调选项,并且决定了player以常见的射击类型游戏中的移动方式在环境中移动。

在多人游戏里每个Pawn实例会被复制到其他客户端。这意味着在一个4人游戏中,每个客户端和服务器都拥有4个Pawn实例。当player死的时候杀死一个Pawn实例,当player重生的时候生成一个新的pawn实例,这并不稀奇,所以请记住你要保存的数据是超出player一次的生命周期的,如果避免这种模式,那就让pawn实例一直存活,只是在数据上记录生死。

# 获取Pawns

PlayerController->GetPawn()
只有当PlayerController控制着一个pawn时

GetWorld()->GetPawnIterator()
GetWorld()在任何一个Actor实例中都有效,返回所有的pawns,包括AI拥有的。
1
2
3
4
5

# 生成

GameModeBase通过SpawnDefaultPawnAtTransform生成Pawn,它指定Pawn的类来生成相应的Pawn实例。

# GameModeBase

指定要使用哪些PlayerController, Pawn, HUD, GameState, PlayerState的主要类,它通常是用来指定某个模式的游戏规则,比如"夺旗"模式里,它会处理旗子的逻辑,在"按波生成敌人"的模式里,处理基于每波的射击逻辑。当然也会处理其他重要特性,比如生成player。

GameMode是GameModeBase的子类,包含一些被Unreal Tournament原生使用的特性,比如MathState和其他射击类型的特性。

在多人游戏里GameMode类只存在于服务器!这意味着没有客户端有它的实例。对于单人游戏这没有啥影响,如果要复制函数或者为GameMode储存数据,你应该考虑使用GameState,它存在于所有的客户端而且也是为这个目的而实现的。

# 获取GameMode

GetWorld()->GetAuthGameMode()
GetWorld在任何一个Actor实例中都有效

GetGameState()
返回gamestate,可用于复制函数或变量

InitGame()
初始化一些游戏规则包含URL提供的一些信息,比如“MyMap?MaxPlayersPerTeam=2,它在游戏加载关卡的时候传入。
1
2
3
4
5
6
7
8

# HUD

定义用户界面的类,有很多关于'Canvas'的代码,Canvas是在UMG之前渲染用户界面的代码,而现在主要渲染用户界面的是UMG。

这个类只存在于客户端,并不适合网络复制,它被PlayerController拥有

# 获取HUD

PlayerController->GetHUD()
只在你的本地PlayerController中有效
1
2

# 宿主

在拥有HUD的PlayerController里面通过SpawnDefaultHUD(生成一般的AHUD)生成,然后被GameModeBase通过InitializeHUDForPlayer用GameModeBase中指定的HUD类重写。

# HUD备注

相对于前UE4时代笔者越来越不怎么使用这个类了,UMG也可以通过PlayerController处理。记住在多人游戏里在生成任何物体之前你要使用IsLocalController()确保PlayerController是本地控制器。

# World

UWorld是代表地图的最顶层对象,它决定地图中的Actors和Components如何存在和渲染。包含持久层和很多其他对象比如GameState,GameMode和地图中像Pawns和Controller这样的一系列东西。

射线追踪和所有它的变体都是通过World的函数来实现,比如World->LineTraceSingleByChannel等等。

# 获取World

GetWorld()
在Actors里面很容易通过调用它来得到

当尝试在静态函数中获取World的实例时,你需要在能使用->GetWorld()的地方将"WorldContextObject"作为参数传入静态函数。比如

static APlayerController* GetFirstLocalPlayerController(UObject* WorldContextObject);
1
2
3
4
5
6

# GameInstance

GameInstance在游戏的整个生命周期中只有一个实例,在切换地图和菜单时只维护这一个实例,这个类被用来提供事件钩子来处理网络错误,加载用户的数据(游戏设置等信息)和其他常见的不仅针对一个关卡的信息。

# 获取GameInstance

GetWorld()->GetGameInstance<T>();
将T换为UGameInstance,比如GetWorld()->GetGameInstance<UGameInstance>()

Actor->GetGameInstance()
1
2
3
4

# GameInstance备注

一般在项目初期不会大量使用,除非你需要深入开发中,比如Game session,demo playback或持久化关卡之间的数据。

# PlayerState

储存和某个Player相关变量的容器,这些变量会在客户端服务器端之间复制。对于多人游戏,它并不是用来运行逻辑的而只是数据容器因为PlayerController不是在所有的客户端之间有效,而且Pawn经常在player死的时候被摧毁并不适合存储超出生命周期的数据。

# 获取PlayerState

Pawn->PlayerState
Controller->PlayerState
Pawn中的PlayerState只在Controller控制Pawn的时候被分配,否则它是nullptr

GameState->PlayerArray
获取比赛中的所有的player的PlayerState实例
1
2
3
4
5
6

# PlayerState宿主

它的宿主PlayerState类在GameMode中,被AController::InitPlayerState()生成。

# PlayerState备注

这个类只在多人游戏中有用

# GameStateBase

和PlayerState相似,但它针对的是GameModeBase中的信息。因为GameModeBase实例只存在于服务器端,那么GameStateBase作为储存一些在客户端服务器端之间常复制的信息(比赛时间,团队分数)的容器非常有用。

它还有个变体GameState,用来处理GameMode需要的额外变量。UE4.14引入了AGameModeBase,它是所有GameMode的基类,是AGameMode的一个更简化和更有效率的版本。AGameMode在4.14之前是GameMode的基类,在4.14之后仍然存在但是AGameModeBase的派生类。AGameMode因为比赛匹配的概念实现更适合标准的游戏类型比如多人射击,AGameModeBase因为其简洁和效率而作为新的默认GameMode。

# 获取GameStateBase

World->GetGameState<T>()
T是GameState实例,比如GetGameState<AGameState>()

MyGameMode->GetGameState()
存在于GameMode实例中,GameMode只存在于服务器上,客户端应该都使用上面的调用。
1
2
3
4
5

# GameStateBase备注

建议使用GameStateBase而不是GameState除非你的GameMode是从GameMode继承而不是GameModeBase。

# UObject

引擎里几乎所有对象的基类,Actors派生于UObject,其他核心类也是如此,比如GameInstance。不会用它来渲染任何东西,但是可以在结构体并不适用的场景里用它来存储数据和函数来满足你的具体要求。

# 获得UObject

UObject的生成不像Actos,可以使用NewObject<T>():

TSubclassOf<UObject> ClassToCreate;
UObject* NewDesc = NewObject<UObject>(this, ClassToCreate);
1
2

# UObject备注

你不太可能直接从UObject派生类除非你很熟悉引擎并想深入定制一些系统。比如我想使用它来存储从数据库中检索的一系列信息。

可以在网络游戏中使用,但是需要在对象类中进行额外的处理,这些对象需要被存储在Actor中。

# GameplayStatics

它是一个静态类,可以用来处理很多常见的游戏相关的功能比如播放声音,生成粒子效果,生成Actors,施加伤害给Actors,得到player pawn,player controller等等,总之,这个类对所有gameplay的获取都很有用。另外这个类的所有方法都是静态的,这意味着你需要指针指向该类的任何实例,可以从你看到的任何地方直接调用函数。

# 获取GameplayStatics

因为GameplayStatics属于UBlueprintFunctionLibrary,你可从你代码的任何地方获取它。

#include "Kismet/GameplayStatics.h"
UGameplayStatics::WhateverFunction();
1
2

# GameplayStatics备注

不管你做任何类型的游戏你都必须要知道的一个类,它包含很多有用的函数,建议看下它的文档 (opens new window)