虚幻引擎4(简称UE4)的GamePlay框架提供了一套强大的类来构建游戏。你的游戏可以是一个射击游戏,农场模拟器游戏,一个深度的RPG游戏,GamePlay框架可以帮助你实现这些复杂的工作。理解这个框架对于成功和高效是至关重要的。
对UE4感兴趣,特别是那些正在学习UE4C++,并想了解更多关于虚幻的游戏框架的开发者。这篇文章介绍了您将在游戏框架中使用的核心类,并解释了它们的用法、引擎如何实例化它们以及如何从游戏代码的其他部分访问这些类。提供的大多数信息也适用于蓝图。
目录
Actor
ActorComponet
PlayerController
AIController
Pawn
GameModeBase
HUD
World
GameInstance
PlayerState
GameStateBase
UObject
GamePlayStatics
推荐阅读
在虚幻引擎4中构建游戏时,你会发现很多模板已经为你准备好了。在C++中,你会用到大量的类来制作游戏。我将介绍每一个类,它们拥有的一些简洁的特性,以及如何从代码中的其他地方引用它们。本指南中的大多数信息都适用于蓝图,尽管我使用C++代码段,但某些功能可能不暴露于蓝图,因此只与C++开发者有关。
生命周期,网络复制,碰撞事件,伤害。
这可能是你在游戏开发中最常见的类,Actor是游戏中Level中所有对象的基类,包括玩家,敌人,门,墙和其他游戏对象。Actor是使用ActorComponent(见下一节)构建的,比如StaticMeshComponent、CharacterMovementComponent、ParticleComponent等等。甚至像GameMode这样的类(见下文)也是(即使GameMode在世界上没有“真实”的位置)。下面我们讲解关于Actor的一些内容。
在多人游戏中,Actor是可以通过网络复制的类,通过在Actor类对象的构造函数中调用SetReplicates(true)实现复制。Actor可以接受各种伤害,伤害可以通过MyActor->TakeDamage(...)或者UGamePlayStatics::ApplyDamage(...)直接将伤害作用到Actor上,对于不用类型的伤害有不同的调用方法,例如武器点伤害和范围爆炸类伤害。在虚幻的官方网站上有一个关于UE4伤害的详细介绍。
你可以使用GetWorld()->SpawnActor
代码如下:
生成 FTransfrom SpawnTM;
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandingOverride = ESpawnActorCollisionHandingMethod::AlwaySpawn;
SpawnParams.Owner = GetOwner();
/* Attempt to assign an instigator (used for damage application) */
SpawnParams.Instigator = Cast(GetOwner());
ASEquippableActor* NewItem = GetWorld()->SpawnActor(NewItemClass, SpawnTM, SpawnParams);
有很多方法访问Actor对象,通常你可以使用一个指针或者引用来访问Actor对象的成员。上面的示例代码中,可以用通过NewItemClass(ASEquipableActor类型的指针)来对NewItemClass的成员进行操作。
一个特别有用的函数UGamePlayStatics:GetAllActorsOfClass(...), 它可以让你获取传入参数类型的所有派生类型对象的数组列表,如果你传入Actor,几乎可以获取整个Level中所有的对象。这个函数也许对你来说并不重要,因为确实对游戏用处不大,但是它可以作为你调试Level的工具函数,让你更加清楚整个Level都存在哪些Actor。
Actor自己并不具备变换特性(平移、缩放、旋转)。这些变换特性是通过Actor中的RootComponent实现的,例如SceneComponents层次结构中的顶级组件(有关SceneComponents的更多信息,请参见下文)。非常费用的函数如:MyActor->GetActorLocation()获取Actor中RootComponent组件的世界空间位置信息包括(平移、缩放、旋转)。
这里有一些很有用的函数,也是Actor使用最频繁的函数。
Actor包含了大量的成员变量和函数,它是虚幻引擎GamePlay框架的基石,所以不必惊讶。为了更好地了解Actor类,一种的办法是在Visual Studio编辑器中打开Actor类的头文件(.h),并查看可用的功能。这将涉及到更多的知识,所以我们移动到下一个类。
推荐阅读官方文档:
组件存在于Actor的内部,常见的组件包括StaticMeshComponent(静态网格组件), CharacterMovementComponent(人物移动组件), CameraComponent(摄像机组件)和SphereComponent(球形组件)。 这些组件各自处理特定的任务,例如移动,物理交互(例如一个碰撞体积,只负责检测重叠碰撞)或者虚拟演示一些像角色网格的事物。
ActorComponet的一个常见的子类是SceneComponent, 同时ActorComponet还是所有具有变换特性(平移,缩放,旋转)和支持附件的类对象的基类。例如,你可以将你的(CameraComponent)摄像机组件设置为SpringArmComponent(摄像机臂组件,类似于自拍杆)的附件来实现第三人称摄像机,同时拥有变换特性和附件特性要求开发者妥善管理相关的位置信息。
组件绝大多数情况在Actor的构造函数中被创建,你也可以在运行时创建和销毁组件。先让我看一个Actor对象的构造函数。
ASEquippableActor::ASEquippableActor()
{
PrimaryActorTick.bCanEverTick = true;
MeshComp = CreateDefaultSubobject(TEXT("MeshComp"));
MeshComp->SetCollisionObjectType(ECC_WorldDynamic); // Just setting a property on our component
// Set our root component for other SceneComponents to attach to
RootComponent = MeshComp;
// not attached to anything (not a SceneComponent with a transform)
ItemComp = CreateDefaultSubobject(TEXT("ItemComp"));
}
USkeletalMeshComponent是使用Actor类中的函数CreateDefualtSubject
你也许会注意到上面的代码中我们设置了MeshComp做为新的RootComponent,现在所有的SceneComponent都必须依附于这个MeshCoonent。利用下面两行代码将WidgetComp组件设置为根组件MeshComponent的附件:
WidgetComp = CreateDefaultSuboject(TEXT("interactWidgetComp"));
WidgetComp->SetupAttachment(MeshComp)
SetupAttachComponent函数用来初始化场景中所有组件的依赖关系,它将会被除了RootComponent(根组件)之外的所有场景组件调用。换句话说,UE4已经提供了一个根组件,我们只需要将整个根组件作为树根,将其他场景组件添加到根组件上,呈现一个树形结构,这些都将在构造函数里完成。你也许想知道为什么这里的ItemComponent 没有调用SetupAttachment函数, 这仅仅是因为该组件是ActorComponent类型,并不是一个SceneComponent类型的组件,它没有Transfrom变换特性(平移,旋转和缩放),因此不需要添加到场景的层级结构树上。不管怎样,该组件将失踪被注册为Actor类型的对象,所以MyActor->GetComponentByClass函数返回的是所有ActorComponent和SceneComponent对象。
这些组件和Actor是使用c++和蓝图开发游戏的主要部分,构成了游戏的基石。你可以很容易在游戏中创建你自己的自定义组件,来处理一些特定的事情,例如一个HealthComponent组件保存了角色的生命值,并且与之关联的Actor对象进行伤害交互。
在GamePlay期间,你可以使用如下代码生成你自己的组件,这与CreateDefualtSuboject只能在构造函数中生成对象不同
UActorComponent* SpawnedComponent = NewObject(this, UStaticMeshComponent::StaticClass(), TEXT("DynamicSpawnedMeshComponent"));
if (SpawnedComponent)
{
SpawnedComponent->RegisterComponent();
}
在ActorComponent中有一些很有用的成员包括:
TickComponent() // 这和Actor类的Tick函数很相似,每帧调用,用来处理一下高频率更新的逻辑。
bool bIsActive 和其相关函数像Activate, Deactivate用来启用或者禁用ActorComponent组件,而不是将该组件从Actor对象销毁或者移除,仅仅是禁用或者启用。
为了启用ActorComponent的网络复制功能,你必须调用SetIsReplicated(true), 这和Actor中的相应的网络复制有点不同,如果你只是想复制Actor中一段特定的组件的逻辑(像复制一个变量),而不是复制Actor上的所有组件, 此时ActorComponent的SetIsReplicate方法是很必要的。
角色对象最主要的一个类,接收玩家的输入信息。PlayerController类它自己一般不会显示在游戏场景里,但是它存在于场景,换句话说它是在场景中是一个虚拟的存在,没有任何的视觉表现。在场景中他是一个虚拟的具有物理特性的角色对象。在整个游戏进程中,一个玩家Player可以拥有多个Pawns(例如一辆汽车,一个Pawn复制的副本),而PlayerController作为这些Pawns的拥有者在整个Level中保持不变,这一点很重要,也有可能某个时候PlayerController根本不拥有任何的Pawns,这就像打开的菜单里没有任何的条目,你需要向PlayerController中添加一些Pawns对象。
在多人游戏人中,PlayerController仅仅存在于拥有玩家角色的客户端和服务器上,这意味着一个5人游戏,服务器上拥有5个PlayerController对象,而客户端只有属于自己的那1个PlayerController对象。这对于理解将某些变量放在何处非常重要,因为当所有玩家都要求复制一个玩家的变量时,它不应该存在于PlayerController中,而应该存在于Pawn甚至PlayerState(稍后讨论)中。
此类包含了一个PlayerCameraManager对象,该对象用来处理视口目标和摄像机变换,包括摄像机的震动。PlayerController处理的另一个重要的类是HUD(稍后讨论),HUD被用于渲染画布(现在用得不是很多了,现在用的多的是UMG)和很好的管理一些你想传递给UMG接口的数据。
当一个新玩家加入游戏GameMode,一个新的PlayerController将通过GameModeBase中的OnLogin()函数被创建。
这和PlayerController的概率相似,区别是它只是控制AI而不是玩家。它就像一个大脑可以拥有并控制一个Pawn对象,这点很像PlayerController,AIController是Pawn控制者的代理对象,可以决定Pawn的行为。
行为树和任何AI能看见或者听见的事物都可以通过访问这个控制器,并将决策推送给Pawn并让其执行。
AIController在Pawn对象中获取和PlayerController一样通过GetController获取。在蓝图中通过任何地方都可以调用的GetAIController函数来获取, 但是传入的参数必须是被AIController控制的Pawn对象。
AIController对象可以通过设置Pawn对象的成员变量Auto Possess AI自动生成。当使用这样的方式生成时,确保你设置了Pawn对象中的变量"AI Controller Class"
玩家(或人工智能)控制的物理和视觉表现,他是一个具有移动功能的Actor,它依然是不可见的,就像是一个游戏中的透明人,有和其他可见玩家一样的数据,只是没有视觉表现。 可以是汽车、战士、炮台等其他代表你游戏人物角色的对象。Pawn一个常用的子类Character、该类实现了网格SkeletalMesh和更重要的移动CharacterMovementComponent,并提供了很多选项用来微调玩家在游戏场景中各种属性数据。
在多人游戏中,每个Pawn实例将被复制到其他的客户端,这意味着5人游戏中,服务器和每一个客户端都拥有5个Pawn实例。 当玩家在游戏中死亡时,销毁一个Pawn对象实例,待玩家重生时,生产一个新的实例对象,这样的处理方式并不少见。记住这一点,以便在玩家的单个生命周期之外存储您想要保留的数据(或者不要完全使用此模式,而是让Pawn实例保持活着状态)
GameModeBase 生成Pawn通过SpawnDefaultPawnAtTransfrom, GameModeBase类还需要指定要生成的Pawn名字。
指定使用哪个类(PlayerController、 Pawn、 HUD、GameState、PlayerState)的主类,通常用于指定游戏规则,比如说处理其他关键属性,例如生成玩家。
GameMode是GameModeBase的基类,包含了很多被用于Unreal Tournament的(比如MatchState和其他射击类游戏)原生特性。
在多人游戏中, GameMode存在于服务器上, 这意为着所有客户端没有实例对象,这对于单人游戏没有影响。为了复制函数和储存你需要储存的GameMode数据,你需要考虑使用GameState,GameState存在于所有的客户端,它是专门为此目的而设计的类。
用户UI界面类,拥有很多绘制Canvas代码,在UMG出现之前——如今基本被UMG取代了
HUD存在于客户端,不适合复制,被PlayerController拥有控制
PlayerController->GetHUD() //可在你本地的PlayerController中使用
在拥有HUD的PlayerController中通过SpawnDefaultHUD生成, 然后被GamwModeBase通过InitializeHUDForPlayer重写
现在UMG已经基本取代了HUD,UMG几乎实现了HUD所有的功能,并有了更加强大的功能。记住在多人游戏中你需要生成Widget组件,首先要确保PlayerController是IsLocalController为true。
UWorld 是最顶层的Level对象,是Actor和各种组件存在和被渲染的地方的一张地图。包含了不间断的关卡Level和许多其他对象,比如GameState,GameMode和当前在地图上的Pawns和Controllers的列表。
光线追踪和其所有变体都是通过World中的函数(比如World->LineTraceSingleByChannel)实现的。
在任何一个Actor中通过GetWorld很容易获取到
当在静态方法中获取World你需要传入一个参数"WorldContextObject"。然后我们用这个参数来调用GetWorld(),请看下面的例子:
静态方法
static APlayerController* GetFirstLocalPlayerController(UObject* WorldContextObject);
使用:
UObject* Context;
GetFirstLocalPlayerController(Context)
Context->GetWolrd();
在整个GamwPlay进程中,至始至终,只有一个GameInstance实例,从地图到菜单始终都是维护此对象实例。该类可以用于提供网络消息,加载游戏数据和一些与指定关卡lLevel无关的事件处理。
通常在游戏开始早期不宜使用太多,除非你对UE(游戏会话,demo回放或者保存一个关卡之间的数据)理解很深刻。
此类包含了一些变量,这些变量可以在客户端和服务器之间进行复制, 数据并不会运行它自己的逻辑。当客户端上的玩家死亡后,Pawn通常就会被销毁,PlayerState就是储存玩家死亡之后的的游戏数据的好地方。
Pawn拥有自己的成员函数Pawn->PlayerState,同时你也可以用Controller->PlayerState。在Pawn里,只有设置了possess占有的Controller。PlayerState才会被赋值,否则它将是一个NULL
获取PlayerSate上的所有的玩家,可以使用GameState->PlayerArray
PlayerState在GameMode中创建并赋值,通过AController::InitPlayerState()
类似于PlayerState,能为客户端提供GameMode信息。 由于在客户端没有GameMode实例,所以GameStateBase成为了一个有用的容器,用来复制像匹配时间和团队积分等的内容。
拥有两种变体,GameState和GameStateBase,其中GameState通过GameMode处理额外的变量(这与GameModeBase相反)
优先使用GameStateBase,除非你的GameMode是从GameMode派生而来,而不是GameModeBase。换句话说,如果MyGameMode从GameMode继承而来,则使用GameState,从GameModeBase派生而来,就使用GameStateBase。
引擎中所有类的基类。Actor和其他核心类(比如GameInstance)都是派生自UObject。此类不会被渲染,但它非常有用,当在C++中声明的成员的类型不太适合你的要求的时候,用UObject就能解决问题,因为是它是所有类的基类,兼容所有类型。
UObject的生成和Actor不同,它使用NewObject
TSubclassOf ClassToCreate;
UObject* NewDesc = NewObject(this, ClassToCreate);
一般情况,不要直接从UObject类派生出新的类实现一些自定义的系统,除非你对UE引擎了解够深。例如使用它来储存一个基础数据列表。
可以被用于网络,但是需要在Object类中设置一些额外的参数,并Object对象需要被储存于Actor内部。
静态类。用来管理游戏中公共的功能模块,例如播放音效、生成粒子特效、生成Actor、施加伤害到Actor、获取Pawn、获取Controller等等。总之,此类对各种游戏对象的访问非常有用,因为它的所有函数都是静态函数,不需要你用指向游戏实例对象的指针来调用,任何地方,直接调用,往下看。
如果GamePlayStatics 是一个UBlueprintFuntionLibrary,你可以在代码(或蓝图)中的任何地方获取它:
//静态方法,在任意地方可以调用,但一定要添加包含头文件"Kismet/GameplayStatics.h"
UGameplayStatics::WhateverFunction();
GamePlayStatics包含了很多有用的接口函数,做任何游戏都必须了解的类,强烈推荐 官方教程,也许你能找到有用的东西。