本文用来记录学习虚幻4时,查看的一篇很有用的外文文章。翻译是机翻+个人理解。(英语不太好)
虚化4游戏框架提供了一些列强大的类来构建我们的系统。你的游戏可以是射击游戏,农场模拟游戏,深度RPG游戏,这些都不重要,框架非常灵活,可以做一些繁重的工作并设定一些标准。它与引擎有着相当深的集成,所以我的直接建议是坚持使用这些类,而不是像使用Unity3D引擎那样尝试“推出自己的”游戏框架。理解这个框架对于成功和高效率地构建你的项目至关重要。
Who is this for?
任何对用UE4构建游戏感兴趣,特别是想用C++构建的和想要学习更多关于虚幻的游戏框架的人。本文将介绍您将从Gameplay框架中使用的核心类,并解释它们的用途、如何由引擎实例化以及如何从游戏代码的其他部分访问这些类。所提供的大多数信息也适用于蓝图。
Gameplay Framework Classes
当你开始构建游戏的时候,你会发现已经有一些模板提供给你了。在用C++或蓝图制作游戏时,你会用到很多类。我将介绍每个类,它们所包含的一些整洁的特性,以及如何从代码中的其他地方引用它们。本项目中大部分C++代码都可以应用到蓝图,但部分C++代码无法应用。
Actor
这或许是你在游戏中使用的最多的类。Actor是一切对象的基类,包括:players,AI,门,墙和游戏对象。Actors 集成使用了一些Actor组件(ActorCompoents)(看下文)例如:静态网络组件(StaticMeshComponent),角色位移组件(CharacterMoveComponents),粒子组件(ParticleComponent)等等。甚至像GameMode(游戏状态)之类的类也是继承的Actors(尽管一个GameMode类在world里甚至没有一个真正的坐标)。我会向你讲述一些你需要了解的Actor的基础知识。
Actor是一个可以在网络中复用的类(多人游戏),可以在构造函数中调用SetReplicates(true)来轻松实现。当然如何在网络中复用Actor有很多篇幅,本次博客将不会详细讲解。
Actor支持接受超出box的伤害。伤害能通过MyActor->TakeDameage(...)或UGameplayStatics::ApplyDamage(...)标识Actor上有效的伤害点和对周围物体的辐射伤害,就像爆炸。在官方的网站上有更好的关于UE4中伤害的教程。
同时也可以很简单地通过GetWorld()->SpawnActor<T>(...)来生成一个新的Actor实例;T是类中返回的,例如AGadgetActor、AGameplayProp等这种的类型之一的actor。
下面是一段运行时Actor产生的代码:
FTransform SpawnTM; FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SpawnParams.Owner = GetOwner(); /* 试图分配一个instigator 【发起者】(用于销毁application) */
SpawnParams.Instigator = Cast(GetOwner());
ASEquippableActor* NewItem = GetWorld()->SpawnActor(NewItemClass, SpawnTM, SpawnParams);
有很多方法可以访问Actors,通常您会有一个指向您感兴趣的特定Actor的指针/引用。在上面的示例中,我们可以通过NewItem变量保持指向EquippableActor的指针,并通过它开始操作Actor实例。
这里介绍一个特别有用的方法:UGameplayStatics::GetAllActorsOfClass(…) 。当你想在引擎中获取一个包含所有Actor的数组的时候,这个方法可以配上用场。(数组中包含Actor的所有派生类)。在实际开发中这个方法是尽量避免使用的,因为它不是一个与World交互中非常有效率的方法。当然有时这又是唯一的方法。
Actors实际上本身是没有移动、旋转或升降等操作。这些都是通过RootComponent来设置的,例如在SceneComponents层次结构中最顶层的组件,通常使用像MyActor->GetActorLocation()方法通过RootComponent返回它的world坐标。
这是在一个Actor上下文中使用的额外的一些方法:
- BeginPlay #Actor生成和初始化时调用的第一个方法。在这个方法中适合设置一些基本的逻辑、定时器或改变某些属性,因为此时已经完全实例化并请求到环境。
- Tick #Actors可以绘制每一帧,但出于性能的考虑,我们通常不会这么做。但在默认的情况下这是启用的。非常适合快速建立动态逻辑和按帧检查内容。最终,您会发现自己将更多的代码移动到基于事件的逻辑,或者运行计时器以较低的频率执行逻辑。
- EndPlay #Actor从world中移除的时候调用,包括制定调用原因的EEndPlayReason
- GetComponentByClass #在我们查找特定类的单个组件实例,同时我们不知道Actor的确切类型,但知道它必会包含的某个组件类型的时候非常有用。还有GetComponentsByClass返回的是类的所有实例,而不仅仅是找到的第一个实例。(也就是返回的是一个数组)
- GetActorLocation #所有的变化,Rotation(旋转),Scale (刻度),包括SetActorLocation等。
- NotifyActorBeginOverlap #用于检查由其任何组件引起的重叠。以这种方式快速设置游戏触发器。
- GedOverlappingActors #很容易找出其他的Actors当前有哪些与我们的Actors重叠,同时提供了一个组件变量:GetOverlappingComponents
Actors含有一些方法和变量,毫无疑问它是虚幻中游戏框架的基石。未来我们研究它的最好的方法就是用VS打开Actor.h头文件,去看那些方法是有效的。这里还有更多的内容,让我们转到我们列表上的下一节课。
记得读Actor的生命周期:Actor Lifecycle(这个很重要)
ActorComponent
Actors含有很多的公共组件,包括StaticMeshComponent(静态网络组件), CharacterMovementComponent(角色移动组件), CameraComponent(相机组件)和SphereComponent(球形组件)。这些组件每个都有特殊的任务比如:位移,物理碰撞或可视化地显示世界上的东西如玩家网络。
组件的子类是场景组件,同时它是任何变换的记录(坐标,旋转,攀登)并支持附加。例如例如,您可以将CameraComponent附加到SpringArmComponent以设置第三人称相机,这两个组件都具有转换,并且需要附加才能正确管理相对位置。
组件大多数在Actor的构造函数中创建,你也可以在运行时创建和销毁组件。首先来看一个Actor构造函数。
ASEquippableActor::ASEquippableActor() {
PrimaryActorTick.bCanEverTick = true;
MeshComp = CreateDefaultSubobject(TEXT("MeshComp"));
MeshComp->SetCollisionObjectType(ECC_WorldDynamic); // 只是在我们的组件上设置一个属性
RootComponent = MeshComp; // 设置要附加到其他scenecomponent的根组件
ItemComp = CreateDefaultSubobject(TEXT("ItemComp")); // 不附加到任何内容(不是带有转移的SceneComponent)
}
使用CreateDefaultSubobject<T>(Actor函数)创建USkeletalMeshComponent,并且需要指定名称(在蓝图的组件列表中可以看到这个名称)。如果是在C++中创建游戏代码,你会使用多次这个函数,但仅在构造函数的上下文里。
或许你注意到我们将MeshComp设置为新的RootComponent。现在任何场景组件都必须附加到这个网格上,如下所示:
WidgetComp = CreateDefaultSubobject(TEXT("InteractWidgetComp"));
WidgetComp->SetupAttachment(MeshComp);
SetupAttachment将为我们处理初始附加,它可以在构造函数中为除RootComponent本身之外的所有场景组件调用。您可能想知道为什么我的ItemComponent不调用这个SetupAttachment函数。这很简单,因为该组件是ActorComponent,而不是SceneComponent,同时它还没有转换(位置、旋转、缩放),因此不需要添加到层次结构中。不管怎么样组件都会注册到Actor,像这样的独立于层次结构的函数像MyActor->GetComponentByClass将返回任何的ActorComponents 和SceneComponents。
这些Actor组件对构建游戏十分重要,不管是C++还是蓝图。他们就是你的游戏的基石。你可以轻松地创建自己的自定义组件,并让它们处理游戏中的特定内容,例如一个HealthComponent,它可以保存要点(hitpoints)并对所属参与者收到的损害作出反应。
在游戏过程中,您可以使用下面的代码生成自己的组件。这与只用于构造函数的createDefaultSubject不同。
UActorComponent* SpawnedComponent = NewObject(this, UStaticMeshComponent::StaticClass(), TEXT("DynamicSpawnedMeshCompoent"));
if (SpawnedComponent) {
SpawnedComponent->RegisterComponent();
}
ActorComponents的一些内置有用功能包括:
- TickComponent()//就像Actors的Tick()运行每一帧来执行高频逻辑一样。
- bool bIsActive 和相关联的函数,例如:Activate, Deactivate等,能够完全启用或禁用组件(包括TickComponent)而不破坏组件或将它从组件移除。
为了能在ActorComponent中复用,你必须使用 SetIsReplicated(true),这个名字和Actor的方法有所不同的方法。只有当你尝试复用一个组件特殊逻辑像函数申明的变量,因此并不用复用你所需复用的Actor的所有组件。
PlayerController
player的主要类,接收用户的输入。PlayerController本身并没有在环境中可视化地显示,相反,它将“拥有”一个典当实例,该实例定义了该玩家在世界上的视觉和物理表示。在游戏过程中,玩家可以拥有多个不同的player(例如,一辆车,或者当重生时player的新副本),而PlayerController实例在整个关卡期间保持不变。这一点很重要,因为有时playController根本不拥有任何player。这意味着菜单的打开应该添加到PlayerController,而不是替换类。
在多人游戏中,playController只存在于拥有的客户端和服务器上。这意味着在一个有4个玩家的游戏中,服务器有4个playController,而每个客户端只有1个。这对于理解将某些变量放在哪里是非常重要的,因为当所有玩家都需要玩家的变量被复用时,它不应该存在于游戏控制器中,而是在player甚至PlayerState (稍后覆盖)中。
Accessing PlayerControllers
- GetWorld()->GetPlayerControllerIterator() // GetWorld 在任何Actor实例中都可获取
- PlayerState->GetOwner() // playerstate的所有者是PlayerController类型,您需要自己将其投射到PlayerController。
- Pawn->GetController() // 只有当前player被玩家控制时才设置
AIController
在概念上类似于PlayerController类,但纯粹用于人工智能代理而不是玩家。将拥有一个Pawn(见下文)非常像玩家控制器,是代理人的“大脑”。当Pawn是代理人的视觉表现时,控制者是决策者。行为树通过这个控制器运行,任何对感知数据的处理(人工智能所能看到和听到的)也应该通过这个控制器运行,并将决策推到Pawn中去执行。
Accessing AIController
AI Controller可以像playercontroller一样从Pawn内部(通过 “Get Controller”)访问,在蓝图中有一个“GetAIController”函数可以在任何地方使用,输入应该是由AI控制的Pawn。
Spawning(生成)
如果需要,可以通过在Pawn类中设置变量“Auto Possess AI”自动生成AIController。使用时,请确保将Pawn中的“AI Controller Class”变量设置为所需的类。
Pawn
player(或AI)控制的物理和视觉表现。这可以是一辆车,一个战士,一个炮塔,或者任何能展示你游戏角色的东西。Pawn的一个公共子类是Character,它实现一个SkeletalMesh,更重要的是CharacterMovementComponent,它有许多选项用于微调玩家如何使用公共射手类型的移动来遍历环境。
在多人游戏中,每个Pawn实例被复制到其他客户端。这意味着在4人游戏中,服务器和每个客户端都有4个Pawn实例。当玩家死亡时“杀死”一个Pawn实例,并在玩家重生时产生一个新实例并不少见。记住这一点,以便存储您希望保存在玩家单次生命周期之外的数据(或者不要完全使用此模式,而是保持Pawn实例的生命周期)
Accessing Pawns
- PlayerController->GetPawn() // 仅当当前PC正确持有一个pawn时
- GetWorld()->GetPawnIterator() // GetWorld在任何Actor实例中都是可用的,它返回所有的pawns,包括你可能拥有的AI。
Spawned by
GameModeBase通过SpawnDefaultPawnAtTransform生成Pawn,GameModeBase类还指定要生成哪个Pawn类。
GameModeBase
类似于PlayerState,但对于客户端来说是关于GameMode的信息。由于GameMode实例不存在于客户端,而是仅在服务器上,这是一个有用的容器来复制匹配时间、团队分数等信息。
有两种子类,GameState和GameStateBase,GameState处理GameMode 所需的额外变量(与gamesmodebase相反)
Accessing GameStateBase
- World->GetGameState<T>() // 其中T是要转换的类,例如GetGameState<AGameState>()
- MyGameMode->GetGameState() // 存储在gamemode实例中并可用(仅与拥有GameMode唯一实例的服务器相关),客户端应改用上述调用。
Personal Remarks(个人备注)
在GameState上使用GameStateBase,除非你的 gamemode 派生自GameMode 而不是GameModeBase。
UObject
引擎中几乎任何东西的基本对象,Actors都是从UObject派生的,其他核心类如GameInstance也是如此。这永远不会用于呈现某些内容,但在结构可能不适合您的特定需求时,它对于存储某些数据和函数非常有用。
Spawning UObjects
UObjects不是像Actors那样申明的,而是使用NewObject创建的,例如:
TSubclassOf ClassToCreate; UObject* NewDesc = NewObject(this, ClassToCreate);
个人备注
除非您对引擎很熟悉,并且正在深入到更定制的系统中,否则您不太可能直接从UObject派生类。例如,我使用它来存储数据库中的信息列表。
可以与网络一起使用,但需要在对象类中进行一些额外的设置,并且对象需要存储在Actor中。
GameplayStatics
静态类处理许多常见的与游戏相关的功能,如播放声音和生成粒子效果、生成角色、对角色应用伤害、获取playerpawn、playerController等。底线是,这个类对于各种游戏访问非常有用。所有函数都是静态的,这意味着您不需要拥有指向此类的任何实例的指针,并且可以从下面所示的任何位置直接调用函数。
Accessing GameplayStatics
因为GameplayStatics 是一个UBlueprintFunctionLibrary,所以您可以从代码中的任何位置(或蓝图)访问它。
UGameplayStatics::WhateverFunction(); // static functions are easily accessed anywhere, just include #include "Kismet/GameplayStatics.h"
个人备注
充满了有用的功能,并且在做任何游戏时都是一个必须知道的类。绝对推荐浏览课程,看看有什么可用的。