- Published on
【中英双语】Tom Looman 的 Unreal Engine 入门教程
- Authors
- Name
- Zihan Li
Getting started with Unreal Engine C++ can be a bit of a struggle. The resources online have no clear path to follow or fail to explain the Unrealisms you’ll encounter. In this article, I’ll attempt to give you an overview of many unique aspects of Unreal’s C++ and briefly go over some of the native C++ features and how they are used in the context of Unreal Engine. It’s a compilation of the many different concepts that you will face when working in C++ and Unreal Engine specifically.
虚幻引擎 C++ 入门可能有点困难。在线资源没有明确的路径可以遵循,或者无法解释您将遇到的 Unrealisms。在本文中,我将尝试向您概述 Unreal C++ 的许多独特方面,并简要介绍一些原生 C++ 功能以及它们在 Unreal Engine 环境中的使用方式。它是你在 C++ 和 Unreal Engine 中工作时将面临的许多不同概念的汇编。
This article can be used as a reference guide in your Unreal Engine C++ journey and as a companion to the documentation and video tutorials/courses out there.
本文可以用作 Unreal Engine C++ 之旅中的参考指南,也可以作为文档和视频教程/课程的配套指南。
Disclaimer: this guide is not exhaustive in teaching you programming from the ground up. This guide should help you understand the specifics of C++ within Unreal Engine. To have a starting point and reference guide while diving into the hands-on tutorials that demonstrate the practical use of C++ for your game.
免责声明:本指南并未详尽地从头开始教您编程。本指南应能帮助你了解虚幻引擎中 C++ 的细节。在深入研究演示 C++ 在游戏中的实际应用的动手教程时,获得起点和参考指南。
This guide is long, don’t forget to bookmark it!
本指南很长,别忘了收藏它!
Ready to become an Unreal Engine C++ master? Don’t miss this limited-time offer to join my comprehensive course and accelerate your learning journey!
准备好成为 Unreal Engine C++ 大师了吗?不要错过这个限时优惠,加入我的综合课程并加速您的学习之旅!
C++ vs. Blueprints C++ 与蓝图
Before we begin, a quick word on C++ vs. Blueprint. It’s the most common discussion in the community. I love C++ and Blueprint and heavily use both. Building a solid foundation in C++ (your framework) and creating small game-specific ‘scripts’ on top using Blueprint is an extremely powerful combination.
在我们开始之前,简单说一下 C++ 与 Blueprint 的对比。这是社区中最常见的讨论。我喜欢 C++ 和 Blueprint,并且大量使用两者。在 C++(您的框架)中构建坚实的基础并使用蓝图创建特定于游戏的小型“脚本”是一个非常强大的组合。
While Blueprint in Unreal Engine is a powerful scripting tool for anyone looking to build games, learning C++ unlocks the full potential of the engine. Not every feature is exposed to Blueprint, for certain things you still need C++. Certain game features may just be easier to build and maintain in C++ in the first place. Not to mention the potential performance gain of using code over Blueprint for the core systems of your game.
虽然虚幻引擎中的蓝图对于任何想要构建游戏的人来说都是一个强大的脚本工具,但学习 C++ 可以释放引擎的全部潜力。并非每个功能都向 Blueprint 公开,对于某些功能,您仍然需要 C++。某些游戏功能可能首先在 C++ 中更容易构建和维护。更不用说对游戏的核心系统使用代码而不是蓝图的潜在性能提升。
“In the early days, I went deep into C++ and tried to do pretty much everything with it, disregarding the power of Blueprint. In hindsight, this made my code more rigid than it needed to be and removed some flexibility for others to make adjustments without C++ knowledge. I later focused more on a healthy balance to great effect.”
“在早期,我深入研究了 C++,并试图用它做几乎所有事情,而忽略了 Blueprint 的强大功能。事后看来,这使得我的代码比需要的更僵化,并消除了其他人在没有 C++ 知识的情况下进行调整的一些灵活性。后来我更专注于健康的平衡,效果很好。
Building the foundational systems (eg. inventory system, world interaction, etc.) in C++ and using these systems in Blueprint to tie it all together. This is now a large focus of my course, where we build the foundational game framework and ability system to allow flexible and small Blueprints to be created on top for individual features/abilities, etc.
在 C++ 中构建基础系统(例如库存系统、世界交互等),并在蓝图中使用这些系统将它们联系在一起。这现在是我课程的一大重点,我们在这里构建了基础游戏框架和技能系统,允许在其上为各个特性/技能等创建灵活而小巧的蓝图。
Alex Forsythe has a great video explaining how C++ and Blueprint fit together and why you should use both instead of evangelizing one and dismissing the other.
Alex Forsythe 有一个很棒的视频,解释了 C++ 和 Blueprint 如何协同工作,以及为什么你应该同时使用两者,而不是宣传一个而忽视另一个。
C++ Syntax & Symbols C++语法和符号
Throughout the article, I’ll be using code snippets as concrete examples. You can find the reference game example project over on GitHub. You can freely browse this repository to see more examples of how C++ is used with Unreal Engine.
在整篇文章中,我将使用代码片段作为具体示例。您可以在 GitHub 上找到参考游戏示例项目。你可以自由浏览此存储库,查看如何将 C++ 与 Unreal Engine 结合使用的更多示例。
While looking at C++ tutorials, you may be wondering about a few common symbols. I will explain their meaning and use cases without going too deep into their technical details. I’ll explain how they are most commonly used within Unreal Engine gameplay programming, not C++ programming in general.
在查看 C++ 教程时,您可能想知道一些常见的符号。我将解释它们的含义和用例,但不会太深入地介绍它们的技术细节。我将解释它们在 Unreal Engine 游戏编程中最常用的方式,而不是一般的 C++ 编程。
Asterisk ‘*’ (Pointers) 星号 '*' (指针)
Commonly known as “pointers”, they may sound scarier than they actually are within Unreal Engine, as most memory management is being taken care of while we’re dealing with gameplay programming. Most commonly used to access objects like Actors in your level and references to assets in your content folders such as sound effects or particle systems.
它们通常被称为“指针”,听起来可能比虚幻引擎中的实际情况更可怕,因为在我们处理游戏编程时,大多数内存管理都是在处理的。最常用于访问关卡中的 Actor 等对象,以及内容文件夹中的资源(如音效或粒子系统)的引用。
Pointers to Objects 指向对象的指针
The first way you’ll be using pointers is to access and track instances of your objects. In order to access your player, you’ll keep a pointer to the player class. For example, AMyCharacter* MyPlayer;
使用指针的第一种方式是访问和跟踪对象的实例。为了访问您的播放器,您将保留一个指向 player 类的指针。例如,AMyCharacter* MyPlayer;
// Get pointer to player controller, points to somewhere in memory containing all data about the object.
APlayerController* PC = GetWorld()->GetPlayerController();
After running this code, the ‘PC’ variable is now pointing to the same place in memory as the player controller we retrieved from World. We didn’t duplicate anything or create anything new, we just looked up where to find the object we need, and can now use it to do stuff for us such as calling functions on it or accessing its variables.
运行此代码后,'PC' 变量现在指向内存中的同一位置,与我们从 World 检索的玩家控制器相同。我们没有复制任何东西或创建任何新的东西,我们只是查找了在哪里找到我们需要的对象,现在可以使用它来为我们做一些事情,例如在它上面调用函数或访问它的变量。
// Example function that tries to get the Actor underneath the player crosshair if there is any
AActor* FocusedActor = GetFocusedInteractionActor();
if (FocusedActor != nullptr)
{
FocusedActor->Interact();
}
// alternative shorthand to check if pointer is valid is simply
if (FocusedActor)
{
FocusedActor->Interact();
}
It’s important to check if pointers are not “null” (also written as “nullptr” in code, meaning not pointing to anything) before attempting to call functions or change its variables, or the engine will crash when executing that piece of code. So you will use the above if-statement often.
在尝试调用函数或更改其变量之前,请务必检查指针是否不是 “null” (在代码中也写为 “nullptr”,意味着不指向任何内容),否则引擎将在执行该段代码时崩溃。所以你会经常使用上面的 if 语句。
Perhaps even more important than knowing when to check for nullptr’s, is when NOT to include nullptr checks.
也许比知道何时检查 nullptr 更重要的是何时不包含 nullptr 检查。
You should generally only check for nullptr if it’s likely and acceptable that a pointer is in fact null and continue execution of the rest of the game regardless. In the above code example, the FocusedActor* is going to be null in many cases, whenever there is no interactable Actor under the player’s crosshair.
通常,您应该仅在指针实际上可能且可接受的情况下检查 nullptr,并继续执行游戏的其余部分。在上面的代码示例中,FocusedActor* 在许多情况下都会为 null,只要玩家的十字准线下没有可交互的 Actor。
Now imagine in the example below we return a nullptr from GetPlayerController() and (quietly) skip the if-statement where we would otherwise add an item to inventory. Further down the line, you will scratch your head wondering why you didn’t receive this item. Having no player controller is unlikely enough in most cases that when it does happen, you may be better off failing entirely as the state of the game is already broken.
现在想象一下,在下面的示例中,我们从 GetPlayerController() 返回一个 nullptr,并(悄悄地)跳过 if 语句,否则我们会将项目添加到库存中。再往下走,你会挠头想知道为什么你没有收到这个项目。在大多数情况下,没有玩家控制器的可能性很小,当它真的发生时,你最好完全失败,因为游戏的状态已经被打破了。
APlayerController* PC = GetWorld()->GetPlayerController();
if (PC)
{
PC->AddToInventory(NewItem);
}
For more info on this concept, I recommend Ari Arnbjörnsson’s talk (at 22:48).
有关此概念的更多信息,我推荐 Ari Arnbjörnsson 的演讲(22:48)。
When creating components to be used in your Actor classes we use similar syntax. In the header file, we define a pointer to a component, this will be a nullptr until we assign it an instance of the component. Here is an example from the header of SCharacter.h where we define a CameraComponent
. (See “ObjectPtr” further down in this article which will replace using raw pointers in headers in future releases of UE5)
在创建要在 Actor 类中使用的组件时,我们使用类似的语法。在头文件中,我们定义了一个指向组件的指针,这将是一个 nullptr,直到我们为其分配组件的实例。下面是 SCharacter.h 标头中的一个示例,我们在其中定义了一个 CameraComponent。(请参阅本文后面的“ObjectPtr”,它将在 UE5 的未来版本中取代在标头中使用原始指针)
UPROPERTY(VisibleAnywhere)
UCameraComponent* CameraComp;
Now in the SCharacter.cpp constructor (called during spawning/instantiation of the Character class), we create an instance of the CameraComponent.
现在,在 SCharacter.cpp 构造函数(在 Character 类的生成/实例化期间调用)中,我们创建 CameraComponent 的实例。
// This function is only used within constructors to create new instances of our components. Outside of the constructor we use NewObject<T>();
CameraComp = CreateDefaultSubobject<UCameraComponent>("CameraComp");
// We can now safely call functions on the component
CameraComp->SetupAttachment(SpringArmComp);
We have now created and assigned an instance to the CameraComp
variable.
现在,我们已经创建了一个实例并将其分配给 CameraComp 变量。
If you want to create a new object outside the constructor, you instead use NewObject(), and for creating and spawning Actors use GetWorld()->SpawnActor<T>()
where T is the class you want to spawn such as ASCharacter
.
如果您想在构造函数之外创建一个新对象,您可以改用 NewObject(),并且要创建和生成 Actor,请使用 GetWorld()->SpawnActor<T>()
,其中 T 是您想要生成的类,例如 ASCharacter。
TObjectPtr
In Unreal Engine 5 a new concept was introduced called TObjectPtr
to replace raw pointers (eg. UCameraComponent*
) in header files with UProperties. This benefits the new systems such as virtualized assets among other things which is why it’s the new standard moving forward. The example above will now look as follows.
在虚幻引擎 5 中,引入了一个名为 TObjectPtr 的新概念来替换原始指针(例如。UCameraComponent*) 的 UPrapies。这有利于新系统,例如虚拟化资产等,这就是为什么它是向前发展的新标准。上面的示例现在如下所示。
UPROPERTY(VisibleAnywhere)
TObjectPtr<UCameraComponent> CameraComp;
These benefits are for the editor only and in shipped builds it will function identically to raw pointers. You may continue to use raw pointers, but it’s advised by Epic to move over to using TObjectPtr whenever possible.
这些好处仅适用于编辑器,在发布的版本中,它的功能与原始指针相同。您可以继续使用原始指针,但 Epic 建议尽可能改用 TObjectPtr。
TObjectPtr is only for the member properties in the headers, your C++ code in .cpp files continues to use raw pointers as there is no benefit to using TObjectPtr in functions and short-lived scope.
TObjectPtr 仅适用于标头中的成员属性,.cpp文件中的 C++ 代码将继续使用原始指针,因为在函数中使用 TObjectPtr 没有任何好处,并且生存期较短。
Pointers to Assets 指向资产的指针
The other common way to use pointers is to reference assets. These don’t represent instances in your world/level, but instead point to loaded content in memory such as textures, sound effects, meshes, etc. (it’s still pointing to an object, which in this case is the class representing a piece of content or an “in-memory representation of an asset on disk”).
使用指针的另一种常见方法是引用资产。这些不表示世界/关卡中的实例,而是指向内存中加载的内容,例如纹理、音效、网格等(它仍然指向一个对象,在本例中是表示一段内容的类或“磁盘上资源的内存中表示”)。
Much like the previous example of the Camera Component, in Unreal Engine 5 you will use TObjectPtr instead of UParticleSystem* (raw pointer) to reference assets. The raw pointers continue to work and shipped builds will effectively use raw pointers again automatically.
与前面的摄像机组件示例非常相似,在虚幻引擎 5 中,您将使用 TObjectPtr 而不是 UParticleSystem*(原始指针)来引用资产。原始指针将继续工作,并且提供的版本将自动再次有效地使用原始指针。
We can take a projectile attack ability as an example that references a particle system. The header defines the ParticleSystem
pointer:
我们可以以一个 projectile attack ability 为例,它引用了一个粒子系统。标头定义 ParticleSystem 指针:
/* Particle System played during attack animation */
UPROPERTY(EditAnywhere, Category = "Attack")
UParticleSystem* CastingEffect;
// Can point to an asset in our content folder, will be assigned something via the editor, not in the constructor as we did with components
Note that this pointer is going to be empty (nullptr
) unless we assigned it to a specific ParticleSystem via the Editor. That’s why we add UPROPERTY(EditAnywhere)
to expose the variable to be assigned in the editor.
请注意,此指针将为空 (nullptr),除非我们通过 Editor 将其分配给特定的 ParticleSystem。这就是我们添加 UPROPERTY(EditAnywhere) 来公开要在编辑器中分配的变量的原因。
Now in the class file of the projectile attack (line 28), we can use this asset pointer to spawn the specified ParticleSystem:
现在,在射弹攻击的类文件(第 28 行)中,我们可以使用此资源指针来生成指定的 ParticleSystem:
UGameplayStatics::SpawnEmitterAttached(CastingEffect, Character->GetMesh(), HandSocketName, FVector::ZeroVector, FRotator::ZeroRotator, EAttachLocation::SnapToTarget);
Note: In this example, we didn’t check whether CastingEffect is a nullptr before attempting to use it, the SpawnEmitterAttached function already does that and won’t crash if it wasn’t assigned a valid particle system.
注意:在此示例中,在尝试使用 CastingEffect 之前,我们没有检查 CastingEffect 是否为 nullptr,SpawnEmitterAttached 函数已经执行了该操作,如果未为其分配有效的粒子系统,则不会崩溃。
Period ‘.’ and Arrow operator ‘->’ (Accessing Variables/Functions) 句点 '.' 和箭头运算符 '->'(访问变量/函数)
Used to access Variables or call Functions of objects. You can type in the period ‘.’ and it automatically converts to ‘->’ in source editors like Visual Studio when used on a pointer. While they are similar in use, the ‘.’ is used on Value-types such as structs (like FVector, FRotator, and FHitResult) and ‘->’ is generally used on classes that you access using Pointers, like Actor, GameMode, ParticleSystem, etc.
用于访问对象的 Variables 或调用 Function。您可以键入句点 '.',当用于指针时,它会在 Visual Studio 等源代码编辑器中自动转换为 '->'。虽然它们的使用方式相似,但 '.' 用于结构体(如 FVector、FRotator 和 FHitResult)等值类型,而 '->' 通常用于使用指针访问的类,如 Actor、GameMode、ParticleSystem 等。
Examples:
例子:
// pointer to Actor class called AMyCar ('A' prefix explained later)
AMyCar* MyCar = SpawnActor<AMyCar>(...);
// Calling function on class instance (pointer)
MyCar->StartEngine();
// Getting variable from class instance (pointer)
float Variable = MyCar->EngineTorque;
// struct containing line trace info
FHitResult HitResult;
// FHitResult is a struct, meaning we use it as a value type and not a class instance.
FVector HitLocation = HitResult.ImpactLocation;
Note: You can use pointers with value types like struct, float, etc. You often don’t use pointers on these types in game code, hence why I used this as the differentiator.
注意:您可以使用具有 struct、float 等值类型的指针。在游戏代码中,您通常不会对这些类型使用指针,因此我将其用作区分器。
Double Colon ‘::’ 双冒号 '::'
Used to access ‘static functions’ (and variables) on classes. A good example is UGameplayStatics, which only consists of static functions, eg. to spawn particles and sounds. Generally, you’ll have very few static variables, so its main use is for easy-to-access functions. Static functions cannot be called on a class instance and only on the class type itself (see below).
用于访问类上的“静态函数”(和变量)。一个很好的例子是 UGameplayStatics,它只由静态函数组成,例如。生成粒子和声音。通常,您的静态变量非常少,因此它的主要用途是易于访问的函数。静态函数不能在 class 实例上调用,只能在 class type 本身上调用(见下文)。
Example of calling a static function on a class:
对类调用静态函数的示例:
UGameplayStatics::PlaySoundAtLocation(this, SoundOnStagger, GetActorLocation());
Since these functions are static, they don’t belong to a specific ‘UWorld
‘. UWorld
is generally the level/world you play in, but within the editor, it could be many other things (the static mesh editor has its own UWorld
for example). Many things need UWorld
, and so you will often see the first parameter of static functions look like this:
由于这些函数是静态的,因此它们不属于特定的 'UWorld'。UWorld 通常是您玩的关卡/世界,但在编辑器中,它可以是许多其他内容(例如,静态网格编辑器有自己的 UWorld)。很多东西都需要 UWorld,所以你经常会看到 static 函数的第一个参数是这样的:
static void PlaySoundAtLocation(const UObject* WorldContextObject, USoundBase* Sound, FVector Location, ...)
UObject* WorldContextObject
can be anything that lives in the relevant world, such as the character that calls this function. And so most of the time you can pass ‘this
‘ keyword as the first parameter. The ‘const’ keyword in front of the parameter means you cannot make changes to that WorldContextObject
within the context of the function.
UObject* WorldContextObject 可以是位于相关世界中的任何内容,例如调用此函数的角色。所以大多数时候你可以将 'this' 关键字作为第一个参数传递。参数前面的 'const' 关键字意味着您无法在函数的上下文中更改该 WorldContextObject。
You will also see a double colon when declaring the body of a function itself (regardless of it being ‘static’ or not)
在声明函数本身的主体时,你也会看到一个双冒号(无论它是否是 'static' 的)
void ASAICharacter::Stagger(UAnimMontage* AnimMontage, FName SectionName /* = NAME_None*/)
{
// ... code in the function (this is in the .cpp file)
}
Ampersand ‘&’ (References & Address operator) && (引用和地址运算符)
Also known as the reference symbol and address operator. I find that I don’t use this as often as the others within gameplay code specifically, but important to know how to use it nonetheless as you will need it to pass around functions when setting timers or binding input.
也称为引用符号和地址运算符。我发现我不像在游戏代码中那样经常使用它,但知道如何使用它很重要,因为在设置计时器或绑定输入时,您将需要它来传递函数。
Pass by Reference 按引用传递
A common concept is to ‘pass by reference‘ a value type like a struct, or a big Array filled with thousands of objects. If you were to pass these variables into a function, without the reference symbol, two things happen:
一个常见的概念是 “通过引用传递” 一个值类型,比如一个 struct,或者一个充满数千个对象的大 Array。如果要将这些变量传递到函数中,而不使用引用符号,则会发生两种情况:
- The code creates a copy of the parameter value, in the case of a big array this can be costly and unnecessary.
该代码会创建一个参数值的副本,如果数组很大,这可能成本高昂且不必要。 - More importantly, because a copy is created, you can’t simply change that variable and have it change in the ‘original’ variable too, you basically cloned it and left the original variable unchanged. If you want to change the original variable inside the function, you need to pass it in as a reference (this is specific to value types like float, bool, structs such as FVector, etc.) Let me give you an example.
更重要的是,因为创建了一个副本,所以你不能简单地改变那个变量,然后让它也在 'original' 变量中改变,你基本上是克隆它并保持原始变量不变。如果你想改变函数内部的原始变量,你需要把它作为引用传入(这特定于 float、bool 等值类型,FVector 等结构体)。让我给你举个例子。
void ChangeTime(float TimeToUpdate)
{
// add 1 second to the total time
TimeToUpdate += 1.0f;
}
Now calling this function as seen in the example below will print out 0.0f at the end since the original TimeVar was never actually changed.
现在调用这个函数,如下例所示,将在最后打印出 0.0f,因为原始 TimeVar 实际上从未更改过。
float TimeVar = 0.0f;
ChangeTime(TimeVar);
print(TimeVar); // This would print out: 0.0f - because we cloned the original variable, and didn't pass in the original into the function. So any change made to that value inside the function is lost.
Now we change the function to:
现在我们将函数更改为:
void ChangeTime(float& TimeToUpdate)
{
// add 1 second to the total time
TimeToUpdate += 1.0f;
}
Now if we use the same code as before, we get a different result: The printed value would now be 1.0f.
现在,如果我们使用与以前相同的代码,我们会得到不同的结果:打印的值现在为 1.0f。
float TimeVar = 0.0f;
ChangeTime(TimeVar);
print(TimeVar); // This would print out: 1.0f - because we passed in the original value by reference, let the function add 1.0f and so it updated TimeVar instead of a copy.
Address Operator 地址运算符
Another important use is the address operator, which even lets us pass functions as parameters into other functions. This is very useful for binding user input and setting timers to trigger specific functions.
另一个重要的用途是 address 运算符,它甚至允许我们将函数作为参数传递给其他函数。这对于绑定用户输入和设置计时器以触发特定功能非常有用。
The BindAxis function in the example below needs to know which function to call when the mapped input is triggered. We pass in the function and use the address operator (&).
以下示例中的 BindAxis 函数需要知道在触发映射输入时要调用哪个函数。我们传入函数并使用地址运算符 (&)。
// Called to bind functionality to input
void ASCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
PlayerInputComponent->BindAxis("MoveForward", this, &ASCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &ASCharacter::MoveRight);
}
Another common use case is to pass a function into timers. The third parameter is again the function we pass in to be called when the timer elapses.
另一个常见的用例是将函数传递给计时器。第三个参数也是我们传入的函数,以便在计时器结束时调用。
// Activate the fuze to explode the bomb after several seconds
GetWorldTimerManager().SetTimer(FuzeTimerHandle, this, &ASBombActor::Explode, MaxFuzeTime, false);
Public, Protected, Private 公共、受保护、私有
These keywords can mark variables and functions in the header file to give ‘access rights’ for other classes and class instances.
这些关键字可以在头文件中标记变量和函数,以授予其他类和类实例的“访问权限”。
Private variables can only be accessed inside that class and not other classes or even derived classes.
私有变量只能在该类内部访问,而不能访问其他类甚至派生类。
Protected means it cannot be accessed from other classes but can be accessed in the derived class.
Protected 表示它不能从其他类访问,但可以在派生类中访问。
Public means other classes have open access to the variable or function.
Public 表示其他类对变量或函数具有开放访问权限。
Generally, you only want to expose what can be safely called/changed from the outside (other classes). You don’t want to make your variables public if they should trigger an event whenever they are changed. Instead, you mark the variable protected or even private and create a public function instead which sets the variable and calls the desired event.
通常,您只想公开可以从外部安全调用/更改的内容(其他类)。如果变量在更改时应触发事件,则您不希望将变量设为公共变量。相反,您将变量标记为 protected 甚至 private,并创建一个 public 函数来设置变量并调用所需的事件。
private:
int32 MyInt;
public:
void SetMyInt(int32 NewInt);
Forward Declaring Classes 前向声明类
Forward declaring C++ classes is done in header files and is done instead of including the full files via #include
. The purpose of forward declaring is to reduce compile times and dependencies between classes compared to including the .h file.
前向声明C++类是在头文件中完成的,而不是通过 #include 包含完整文件。与包含 .h 文件相比,前向声明的目的是减少编译时间和类之间的依赖关系。
Let’s say we wish to use UParticleSystem
class in another header named MyCharacter.h
. The header file (and compiler) doesn’t need to know everything about UParticleSystem
, just that the word is used as a class.
假设我们希望在另一个名为 MyCharacter.h 的标头中使用 UParticleSystem 类。头文件(和编译器)不需要了解有关 UParticleSystem 的所有信息,只需将单词用作类即可。
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SCharacter.generated.h"
//#include "ParticleSystem.h" // << We don't need to include the entire file
class UParticleSystem; // << We can instead just 'forward declare' the type.
UCLASS()
class ACTIONROGUELIKE_API ASCharacter : public ACharacter
{
GENERATED_BODY()
UParticleSystem* CastingEffect;
// ...
The class
keyword provides the minimum the compiler requires to understand that word is in fact a class. If we included the .h file for the class instead this could negatively impact our compile times. Any changes to the included header (eg. including your MyCharacter.h elsewhere in your code) will cause the classes which include said header to re-compile too.
class 关键字提供了编译器理解 word 实际上是一个类所需的最低要求。如果我们包含类的 .h 文件,这可能会对我们的编译时间产生负面影响。对包含的标头的任何更改(例如,在代码中的其他位置包含 MyCharacter.h)都将导致包含所述标头的类也重新编译。
Here is the character class example that forward declares all the Components used in the header instead of including their .h files.
下面是字符类示例,它 forward 声明 header 中使用的所有组件,而不是包含它们的 .h 文件。
Forward Declaration is mentioned in Epic’s Coding Standards as well. “If you can use forward declarations instead of including a header, do so.”
Forward Declaration 在 Epic 的编码标准中也有提及。“如果可以使用 forward declarations 而不是包含 header,请这样做。”
Casting (Cast)
Casting to specific classes is something you’ll use all the time. Casting pointers in Unreal Engine is a bit different from ‘raw C++’ in that it’s safe to cast to types that might not be valid, your code won’t crash and instead just returns a nullptr (null pointer).
强制转换到特定类是您将一直使用的东西。在虚幻引擎中转换指针与“原始 C++”略有不同,因为可以安全地转换到可能无效的类型,你的代码不会崩溃,而只会返回一个 nullptr(空指针)。
As an example, you might want to Cast your APawn* (pointer) to your own character class (eg. ASCharacter) as casting is required to access the variables and functions declared in that specific class.
例如,您可能希望将 APawn*(指针)转换为您自己的字符类(例如。ASCharacter),因为需要强制转换才能访问该特定类中声明的变量和函数。
APawn* MyPawn = GetPawn();
ASCharacter* MyCharacter = Cast<ASCharacter>(MyPawn);
if (MyCharacter) // verify the cast succeeded before calling functions
{
// Respawn() is defined in ASCharacter, and doesn't exist in the base class APawn. Therefore we must first Cast to the appropriate class.
MyCharacter->Respawn();
}
It’s not always preferable to cast to specific classes, especially in Blueprint as this can have a negative impact on how much data needs to be loaded into memory. Any time you add a Cast to a certain Blueprint class on your EventGraph that object will be loaded into memory immediately (not when the Cast-node is hit at runtime, but as soon as the Blueprint itself gets loaded/created), causing a cascade of loaded objects. Especially when Blueprints reference a lot of assets (meshes, particles, textures) this has a large impact on your project’s (load/memory) performance.
强制转换为特定类并不总是可取的,尤其是在 Blueprint 中,因为这可能会对需要加载到内存中的数据量产生负面影响。每当将 Cast 添加到 EventGraph 上的某个 Blueprint 类时,该对象都会立即加载到内存中(不是在运行时命中 Cast-node 时,而是在加载/创建蓝图本身时),从而导致加载对象的级联。特别是当蓝图引用大量资产(网格、粒子、纹理)时,这会对项目的(负载/内存)性能产生很大影响。
Blueprint Example: BlueprintA has a cast-to node in its EventGraph that casts to BlueprintB. Now as soon as BlueprintA is used/loaded in-game, BlueprintB is loaded at the same time. They will now both remain in memory even if you don’t actually have any instances of BlueprintB in your Level.
蓝图示例:BlueprintA 的 EventGraph 中有一个 cast-to 节点,该节点转换为 BlueprintB。现在,只要在游戏中使用/加载 BlueprintA,BlueprintB 就会同时加载。现在,即使您的关卡中实际上没有任何 BlueprintB 实例,它们也将保留在内存中。
This often becomes a problem when developers put all their code in the Character Blueprint. Everything you Cast-to on its EventGraph will be loaded including all their textures, models, and particles.
当开发人员将所有代码放入 Character Blueprint 中时,这通常会成为一个问题。你在其 EventGraph 上投射到的所有内容都将被加载,包括它们的所有纹理、模型和粒子。
Since all C++ classes will be loaded into memory at startup regardless, the main reason to cast to base classes is compilation time. It will avoid having to recompile classes that reference (#include) your class headers whenever you make a change. This can have a cascading effect of recompiling classes that depend on each other.
由于所有 C++ 类都会在启动时加载到内存中,因此强制转换为基类的主要原因是编译时间。这将避免在你进行更改时重新编译引用 (#include) 类头的类。这可能会产生重新编译相互依赖的类的级联效应。
C++ Example: You only cast to AMyCharacter if your function or variable required is first declared in that class. If you instead need something already declared in APawn, you should simply cast to APawn instead.
C++ 示例:仅当函数或变量 required 首次在该类中声明时,才强制转换为 AMyCharacter。如果你需要已经在 APawn 中声明的内容,你应该简单地转换为 APawn。
One way to reduce class dependencies is through interfaces…so that’s what we will talk about next.
减少类依赖的一种方法是通过接口......这就是我们接下来要讨论的内容。
Interfaces 接口
Interfaces are a great way to add functions to multiple classes without specifying any actual functionality yet (implementation). Your player might be able to interact with a large variety of different Actors in the level, each with a different reaction/implementation. A lever might animate, a door could open or a key gets picked up and added to the inventory.
接口是向多个类添加函数的好方法,而无需指定任何实际功能(实现)。您的玩家可能能够与关卡中的各种不同 Actor 交互,每个 Actor 都有不同的反应/实现。拉杆可能会动画化,门可以打开,或者钥匙被拾取并添加到库存中。
Interfaces in Unreal are a bit different from normal programming interfaces in that in UE you are not required to implement the function, it’s optional.
Unreal 中的接口与普通的编程接口略有不同,因为在 UE 中,您不需要实现该函数,它是可选的。
An alternative to interfaces is to create a single base class (as mentioned earlier) that contains a Interact()
function that child classes can override to implement their own behavior. Having a single base class is not always ideal or even possible depending on your class hierarchy, and that’s where interfaces might solve your problem.
接口的替代方法是创建一个包含子类可以覆盖的 Interact() 函数的单个基类(如前所述)。拥有单个基类并不总是理想的,甚至不可能,具体取决于您的类层次结构,这就是接口可能解决您的问题的地方。
Interfaces are a little odd at first in C++ as they require two classes with different prefix letters. They are both used for different reasons but first, let’s look at the header.
在 C++ 中,接口起初有点奇怪,因为它们需要两个具有不同前缀字母的类。它们都出于不同的原因使用,但首先,让我们看看标题。
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class USGameplayInterface : public UInterface
{
GENERATED_BODY()
};
/**
*
*/
class ACTIONROGUELIKE_API ISGameplayInterface
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
void Interact(APawn* InstigatorPawn);
};
With the interface class defined you can ‘inherit’ from it in other C++ classes and implement actual behavior. For this, you use the “I” prefixed class name. Next to public AActor
we add , public ISGameplayInterface
to specify we want to inherit the functions from the interface.
定义接口类后,您可以在其他 C++ 类中“继承”它并实现实际行为。为此,请使用前缀为 “I” 的类名。在 public AActor 旁边,我们添加了 public ISGameplayInterface 来指定我们要从接口继承函数。
UCLASS()
class ACTIONROGUELIKE_API ASItemChest : public AActor, public ISGameplayInterface // 'inherit' from interface
{
GENERATED_BODY()
// declared as _Implementation since we defined the function in interface as BlueprintNativeEvent
void Interact_Implementation(APawn* InstigatorPawn);
}
BlueprintNativeEvent is useful to allow C++ to provide a base implementation, Blueprint child classes can then override or extend this function. In C++ the function implementation will have an _Implementation suffix added. This is from code generated by Unreal.
BlueprintNativeEvent 对于允许 C++ 提供基本实现非常有用,然后蓝图子类可以覆盖或扩展此函数。在 C++ 中,函数实现将添加 _Implementation 后缀。这是来自 Unreal 生成的代码。
In order to check whether a specific class implements (inherits from) the interface you can use Implements<T>()
. For this, you use the “U” prefixed class name.
为了检查特定类是否实现(继承自)接口,您可以使用 Implements<T>()
。为此,请使用以 “U” 为前缀的类名。
if (MyActor->Implements<USGameplayInterface>())
{
}
Calling interface functions is again unconventional. The signature looks as follows: IMyInterface::Execute_YourFunctionName(ObjectToCallOn, Params);
This is another case where you use the “I” prefixed class.
调用接口函数也是非常规的。签名如下所示: IMyInterface::Execute_YourFunctionName(ObjectToCallOn, Params);
这是使用“I”前缀类的另一种情况。
ISGameplayInterface::Execute_Interact(MyActor, MyParam1);
Important: There are other ways to call this function, such as casting your Actor to the interface type and calling the function directly. However, this fails entirely when interfaces are added/inherited to your class in Blueprint instead of in C++, so it’s recommended to just avoid that altogether.
重要提示: 还有其他方法可以调用此函数,例如将 Actor 强制转换为接口类型并直接调用该函数。但是,当接口在 Blueprint 而不是 C++ 中添加/继承到您的类时,这完全会失败,因此建议完全避免这种情况。
However, if you want to share functionality between Actors but don’t want to use a base class then you could use an ActorComponent.
但是,如果您想在 Actor 之间共享功能,但不想使用基类,则可以使用 ActorComponent。
Steve Streeting has more details on using Interfaces which I recommend checking out. There is a code example in the Action Roguelike project as well using SGameplayInterface used by InteractionComponent to call Interact()
on any Actor implementing the interface.
Steve Streeting 提供了有关使用 Interfaces 的更多详细信息,我建议查看。Action Roguelike 项目中也有一个代码示例,使用 InteractionComponent 使用的 SGameplayInterface 在实现该接口的任何 Actor 上调用 Interact()。
Delegates (Events) 委托 (事件)
Delegates (also known as Events) allow code to call one or multiple bound functions when triggered. Sometimes you’ll see this referred to as Callbacks. For example, It can be incredibly helpful to bind/listen to a delegate and be notified when a value (such as character health) changes. This can be a lot more efficient than polling whether something changes during Tick()
.
委托(也称为 Events)允许代码在触发时调用一个或多个绑定函数。有时,您会看到这称为 Callbacks。例如,绑定/侦听代理并在值(例如角色生命值)发生变化时收到通知,这可能非常有用。这比轮询 Tick() 期间是否有变化要高效得多。
There are several types of these delegates/events. I’ll explain the most commonly used ones for game code using practical examples rather than low-level language details. I’m also not covering all the different ways of binding (only focusing on the more practical ways instead) or niche use cases, you can find more details on the official documentation for those.
这些委托/事件有几种类型。我将使用实际示例而不是低级语言细节来解释游戏代码中最常用的 Cookies。我也没有涵盖所有不同的绑定方式(只关注更实用的方法)或小众用例,你可以在官方文档中找到更多详细信息。
Declaring and Using Delegates 声明和使用委托
You start by declaring the delegate with a MACRO. There are variants available to allow passing in parameters, these have the following suffix. _OneParam, _TwoParams, _ThreeParams, etc. You define these in the header file, ideally, above the class where you want to call them.
首先使用 MACRO.有一些变体可用于允许传入参数,这些变体具有以下后缀。_OneParam、_TwoParams、_ThreeParams等。您可以在头文件中定义这些值,理想情况下,在要调用它们的类的上方。
// These macros will sit at the top of your header files.
DECLARE_DYNAMIC_MULTICAST_DELEGATE()
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams()
We’ll start by showing the process of declaring and using delegates in detail with a commonly used type, and then explain the other types more briefly as they share the same concepts.
首先,我们将详细展示使用常用类型声明和使用委托的过程,然后更简要地解释其他类型,因为它们具有相同的概念。
Multicast Dynamic 组播动态
One of the most used types of delegate in your game code as they can be exposed to Blueprint to bind and receive callbacks.
游戏代码中最常用的代理类型之一,因为它们可以公开给蓝图以绑定和接收回调。
Note: Dynamic Multicast Delegates are also known as Event Dispatchers in Blueprint.
注意:动态多播代理在 Blueprint 中也称为 Event Dispatcher。
The macros take at least one parameter, which defines their name. eg. FOnAttributeChanged could be a name we use as our Delegate to execute whenever an attribute such as Health changes.
宏至少采用一个参数,该参数定义其名称。例如。FOnAttributeChanged 可以是我们用作代理的名称,以便在 Health 等属性发生变化时执行。
DECLARE_DYNAMIC_MULTICAST_DELEGATE(<typename>)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(<typename>, <paramtype1>, <paramvarname1>, <paramtype2>,<paramvarname2>)
Here is one example of a delegate with four parameters to notify code about a change to an attribute. The type and variable names are split by commas, unlike normal functions.
下面是一个具有四个参数的委托示例,用于通知代码对属性的更改。与普通函数不同,类型和变量名称由逗号分隔。
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOnAttributeChanged, AActor, InstigatorActor, USAttributeComponent, OwningComp, float, NewValue, float, Delta);
You now add the delegate in your class header, which may look as follows:
现在,您在类标头中添加委托,它可能如下所示:
UPROPERTY(BlueprintAssignable, Category = "Attributes")
FOnAttributeChanged OnHealthChanged;
You may have noticed BlueprintAssignable, this is a powerful feature of the Dynamic delegates which can be exposed to Blueprint and used on the EventGraph.
您可能已经注意到 BlueprintAssignable,这是 Dynamic 委托的一个强大功能,可以公开给蓝图并在 EventGraph 上使用。
Executing Delegates 执行委托
Finally, to actually trigger the callback we call OnHealthChanged*.Broadcast()* and pass in the expected parameters.
最后,为了实际触发回调,我们调用 OnHealthChanged*。Broadcast()* 并传入预期的参数。
OnHealthChanged.Broadcast(InstigatorActor, this, NewHealth, Delta);
Binding to Delegates 绑定到委托
Binding in C++ C++ 中的绑定
You should *never* bind your delegates in the constructor and choose either AActor::PostInitializeComponents()
or BeginPlay()
to avoid issues where delegates get serialized into the Blueprint and will still be called even when you later remove the delegate binding in C++.
您永远不要在构造函数中绑定代理,并选择 AActor::PostInitializeComponents()
或 BeginPlay() 以避免代理序列化到蓝图中,并且即使您稍后在 C++ 中删除代理绑定,仍将被调用的问题。
Since delegates are weakly referenced you often don’t need to unbind delegates when destroying objects/actors unless you want to manually stop listening/reacting to specific events.
由于代理是弱引用的,因此在销毁对象/Actor 时,通常不需要取消绑定代理,除非您想手动停止侦听/响应特定事件。
You can bind to a delegate calling .AddDynamic()
. The first parameter takes a UObject
for which we can pass ‘this
‘. The second parameter types the address of the function (YourClass::YourFunction
) which is why we pass the function with the ampersand (&
) symbol which is the address operator.
您可以绑定到调用 .AddDynamic() 的 API 中。第一个参数接受一个 UObject,我们可以为其传递 'this'。第二个参数键入函数的地址 (YourClass::YourFunction),这就是为什么我们用 & 符号(地址运算符)传递函数的原因。
void ASAICharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
AttributeComp->OnHealthChanged.AddDynamic(this, &ASAICharacter::OnHealthChanged);
}
The above OnHealthChanged function is declared with UFUNCTION()
in the header.
上述 OnHealthChanged 函数在标头中使用 UFUNCTION() 声明。
UFUNCTION()
void OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth, float Delta);
Binding in Blueprint 蓝图中的绑定
You can easily bind your dynamic delegates in Blueprint. When implemented on an ActorComponent as in the example below you can select the Component in the outliner and click the “+” symbol in its details panel. This creates the Delegate on the EventGraph and is already bound for us.
您可以在 Blueprint 中轻松绑定动态代理。在 ActorComponent 上实现时,如下例所示,您可以在大纲视图中选择该组件,然后单击其详细信息面板中的“+”符号。这将在 EventGraph 上创建 Delegate,并且已经为我们绑定了代理。
You can also manually bind the delegates via the EventGraph (eg. binding to another Actor’s delegates.
您还可以通过 EventGraph 手动绑定代理(例如,绑定到另一个 Actor 的代理)。
Note: Dynamic delegates are less performant than non-dynamic (seen below) variants. It’s therefore advisable to only use this type when you want to expose it to Blueprint.
注意:动态委托的性能低于非动态(见下文)变体。因此,建议仅在你想将其公开给 Blueprint 时才使用此类型。
C++ Delegates C++ 委托
Macro: DECLARE_DELEGATE
, DECLARE_DELEGATE_OneParam
宏:DECLARE_DELEGATE、DECLARE_DELEGATE_OneParam
When used only in C++ we can define delegates with an unspecified amount of parameters. In the following example, we’ll use a more complex use case which is asynchronously loading game assets.
当仅在 C++ 中使用时,我们可以定义具有未指定数量的参数的委托。在以下示例中,我们将使用一个更复杂的用例,即异步加载游戏资产。
The StreamableManager of Unreal defines a FStreamableDelegate
.
Unreal 的 StreamableManager 定义了一个 FStreamableDelegate。
DECLARE_DELEGATE(FStreamableDelegate);
This doesn’t specify any parameters yet and lets us define what we wish to pass along in our own game code.
这还没有指定任何参数,让我们定义我们希望在自己的游戏代码中传递的内容。
The following is taken from SGameModeBase
in the ActionRoguelike project (link to code). We asynchronously load the data of an enemy Blueprint to spawn them once the load has finished.
以下内容摘自 ActionRoguelike 项目中的 SGameModeBase(代码链接)。我们异步加载敌人蓝图的数据,以便在加载完成后生成它们。
if (UAssetManager* Manager = UAssetManager::GetIfValid())
{
// Primary Id is part of AssetManager, we grab one from a DataTable
FPrimaryAssetId MonsterId = SelectedMonsterRow->MonsterId;
TArray<FName> Bundles;
// A very different syntax, we create a delegate via CreateUObject and pass in the parameters we want to use once loading has completed several frames or seconds later. (In this case the MonsterId is the asset we are loading via LoadPrimaryAsset and Locations[0] is the desired spawn location once loaded)
FStreamableDelegate Delegate = FStreamableDelegate::CreateUObject(this, &ASGameModeBase::OnMonsterLoaded, MonsterId, Locations[0]);
// Requests the load in Asset Manager on the MonsterId (first param) and passes in the Delegate we just created
Manager->LoadPrimaryAsset(MonsterId, Bundles, Delegate);
}
In the example above we create a new Delegate variable and fill it with variables, in this case MonsterId
and the first vector location from an array (Locations[0]
). Once the LoadPrimaryAsset function from Unreal has finished, it will call the delegate OnMonsterLoaded
with the provided parameters we passed into the CreateUObject function previously.
在上面的示例中,我们创建一个新的 Delegate 变量并用变量填充它,在本例中为 MonsterId 和数组中的第一个向量位置 (Locations[0])。Unreal 的 LoadPrimaryAsset 函数完成后,它将使用我们之前传递给 CreateUObject 函数的提供的参数调用委托 OnMonsterLoaded。
void ASGameModeBase::OnMonsterLoaded(FPrimaryAssetId LoadedId, FVector SpawnLocation)
Another example of using delegates/callbacks is with Timers. We don’t need to specify our own delegate first and can directly pass in the function address so long as it has no parameters. It’s possible to use timers with parameters as well. To learn more you can check out my blog post on Using C++ Timers.
使用 delegates/callbacks 的另一个示例是使用 Timers。我们不需要先指定自己的 delegate,只要没有参数就可以直接传入函数地址。也可以使用带有参数的计时器。要了解更多信息,您可以查看我关于使用 C++ 计时器的博客文章。
There is a lot more to talk about, but this should provide a core understanding from which to build. There are many more variants to the macros and different ways to bind…which could be a whole article on its own.
还有很多内容要讨论,但这应该提供一个核心理解,并以此为基础进行构建。宏还有更多变体和不同的绑定方式......这本身可以是一整篇文章。
To read more about delegates I recommend BenUI’s Intro to Delegates and Advanced Delegates in C++.
要了解更多关于委托的信息,我推荐 BenUI 的 C++ 委托介绍和高级委托。
Public/Private Folders 公共/私人文件夹
Public and private folders define which files are available to use in other modules. Generally, your header files are placed in the Public folder so other modules can gain access and the cpp files are in the Private folder. Headers that are not meant to be used directly by other modules can go into the Private folder as well.
Public 和 private 文件夹定义哪些文件可在其他模块中使用。通常,您的头文件放在 Public 文件夹中,以便其他模块可以访问,而 cpp 文件位于 Private 文件夹中。不打算被其他模块直接使用的 Headers 也可以进入 Private 文件夹。
Your primary game module doesn’t need this public/private structure if you don’t intend to have other modules depend on it.
如果您不打算让其他模块依赖它,则您的主游戏模块不需要此 public/private 结构。
I recommend checking out Ari’s talk on modules for more information on Modules and how to use them.
我建议查看 Ari 关于模块的演讲,以获取有关模块以及如何使用它们的更多信息。
Class Prefixes (F, A, U, E, G, T, …) 类前缀 (F, A, U, E, G, T, ...)
Classes in Unreal have a prefix, for example, the class ‘Actor’ is named ‘AActor’ when seen in C++. These are helpful in telling you more about the type of object. Here are a few important examples.
Unreal 中的类有一个前缀,例如,在 C++ 中可以看到类 'Actor' 时命名为 'AActor'。这些有助于您了解有关对象类型的更多信息。以下是一些重要示例。
A. Actor derived classes (including Actor itself) have A as prefix, eg. APawn, AGameMode, AYourActorClass
A. Actor 派生类(包括 Actor 本身)的前缀为 A,例如。APawn、AGameMode、AYourActorClass
U. UObject derived classes, including UBlueprintFunctionLibrary
, UActorComponent
and UGameplayStatics
. Yes, AActor
derives from UObject
, but it overrides it with its own A prefix.
U. UObject 派生类,包括 UBlueprintFunctionLibrary、UActorComponent 和 UGameplayStatics。是的,AActor 派生自 UObject,但它会用自己的 A 前缀覆盖它。
F. Structs, like FHitResult, FVector, FRotator, and your own structs should start with F.
F. 结构(如 FHitResult、FVector、FRotator)和您自己的结构应以 F 开头。
E. The convention for enum types. (EEnvQueryStatus
, EConstraintType
, …)
E.枚举类型的约定。(EEnvQueryStatus、EConstraintType、...)
G. “globals” for example, GEngine->AddOnscreenDebugMessage()
where GEngine
is global and can be accessed anywhere. Not very common in your use within gameplay programming itself though.
例如,G. “globals”, GEngine->AddOnscreenDebugMessage()
其中 GEngine 是 global 的,可以在任何地方访问。不过,在游戏程序本身中的使用中并不常见。
T. Template classes, like TSubclassOf<T>
(class derived from T, which can be almost anything), TArray<T>
(lists), TMap<T>
(dictionaries) etc. classes that can accept multiple classes. Examples:
模板类,如 TSubclassOf<T>
(派生自 T 的类,几乎可以是任何内容)、TArray<T>
(列表)、TMap<T>
(字典)等可以接受多个类的类。例子:
// A list of strings.
TArray<FString> MyStrings;
// A list of actors
TArray<AActor*> MyActors;
// Can be assigned with a CLASS (not an instance of an actor) that is either a GameMode class or derived from GameMode.
TSubclassOf<AGameMode> SubclassOfActor;
Mike Fricker (Lead Technical Director) explained the origins of “F” Prefix:
Mike Fricker(首席技术总监)解释了“F”前缀的由来:
“The ‘F’ prefix actually stands for “Float” (as in Floating Point.)“
“'F' 前缀实际上代表 'Float'(如 Floating Point)。”
“Tim Sweeney wrote the original “FVector” class along with many of the original math classes, and the ‘F’ prefix was useful to distinguish from math constructs that would support either integers or doubles, even before such classes were written. Much of the engine code dealt with floating-point values, so the pattern spread quickly to other new engine classes at the time, then eventually became standard everywhere.”
“Tim Sweeney 编写了原始的 ”FVector“ 类以及许多原始的数学类,'F' 前缀可用于区分支持整数或双精度的数学结构,甚至在编写此类类之前。许多引擎代码都处理浮点值,因此该模式在当时迅速传播到其他新的引擎类,然后最终成为各地的标准。
“This was in the mid-nineties sometime. Even though most of Unreal Engine has been rewritten a few times over since then, some of the original math classes still resemble their Unreal 1 counterparts, and certain idioms remain part of Epic’s coding standard today.”
“那是在 90 年代中期的某个时候。尽管从那时起,虚幻引擎的大部分内容已经被重写了几次,但一些原始的数学类仍然类似于 Unreal 1 中的对应项,并且某些习语今天仍然是 Epic 编码标准的一部分。
Project Prefixes 项目前缀
Projects in Unreal should use their own (unique) prefix to signify their origin. For example, all classes in Unreal Tournament use “UT” (AUTActor
, UUTAbility
), and Fortnite uses “Fort” prefix (AFortActor
, UFortAbility
, etc).
Unreal 中的项目应使用自己的(唯一)前缀来表示其来源。例如,虚幻竞技场中的所有类都使用 “UT” (AUTActor, UUTAbility),而 Fortnite 使用 “Fort” 前缀 (AFortActor, UFortAbility 等)。
In the many code examples in this guide, I used “S” as the prefix. These examples are from the Action Roguelike project. (Note: Since Unreal’s Widgets/Slate already uses “S” as a prefix one could argue I should have used “SU” or some other more unique prefix – in all these years I’ve never had any issue with this – so it’s been more of a cosmetic issue).
在本指南的许多代码示例中,我使用了 “S” 作为前缀。这些示例来自 Action Roguelike 项目。(注意:由于 Unreal 的 Widgets/Slate 已经使用“S”作为前缀,因此有人可能会争辩说我应该使用“SU”或其他更独特的前缀 - 这些年来,我从来没有遇到过这个问题 - 所以这更像是一个表面问题)。
Common Engine Types 常见发动机类型
Besides the standard types like float
, int32
, bool
, which I won’t cover as there is nothing too special to them within Unreal Engine – Unreal has built-in classes to handle very common logic that you will use a lot throughout your programming. Here are a few of the most commonly seen types from Unreal that you will use. Luckily the official documentation has some information on these types, so I will be referring to that a lot.
除了 float、int32、bool 等标准类型之外,我不会介绍这些类型,因为在 Unreal Engine 中它们没有什么特别之处——Unreal 有内置的类来处理你在整个编程过程中会经常使用的非常常见的逻辑。以下是您将使用的一些最常见的 Unreal 类型。幸运的是,官方文档有一些关于这些类型的信息,所以我会经常提到。
Ints are special in that you are not supposed to use “int” in serialized UProperties as the size of int can change per platform. That’s why Unreal uses its own sized int16, int32, uint16, etc. – Source
Int 很特殊,因为您不应该在序列化的 UProperties 中使用 “int”,因为 int 的大小可能会因平台而异。这就是为什么 Unreal 使用自己大小的 int16、int32、uint16 等。– 来源
FString, FName, FTex
There are three types of ‘strings’ in Unreal Engine that are used for distinctly different things. It’s important to select the right type for the job or you’ll suffer later. The most common problem is using FString for UI text instead of FText
, this will be a huge headache later if you plan to do any sort of localization.
Unreal Engine 中有三种类型的“字符串”,它们用于截然不同的事物。为工作选择正确的类型很重要,否则您以后会受苦。最常见的问题是对 UI 文本使用 FString 而不是 FText,如果您打算进行任何类型的本地化,这将是一个巨大的麻烦。
FString, The base representation for strings in Unreal Engine. Used often when debugging and logging information or passing raw string information between systems (such as REST APIs). Can be easily manipulated.
FString,Unreal Engine 中字符串的基本表示形式。通常在调试和记录信息或在系统之间传递原始字符串信息时使用(例如 REST API)。可以轻松操作。
FName, Essentially hashed strings that allow to much faster comparisons between two FNames. (FNames don’t change once created) and are used often for look-ups such as SocketNames on a Skeletal Mesh and GameplayTags.
FName,本质上是哈希字符串,允许在两个 FNames 之间更快地进行比较。(FNames 在创建后不会更改),并且通常用于查找,例如骨架网格体上的 SocketNames 和 GameplayTags。
FText, Front-end text to display to the user. Can be localized easily. All your front-facing text should always be FText instead of FNames or FString.
FText,要向用户显示的前端文本。可以轻松定位。所有正面文本应始终为 FText 而不是 FNames 或 FString。
Here is a piece of Documentation on String handling including how to convert between the different types.
这是一段关于 String 处理的文档,包括如何在不同类型的之间进行转换。
FVector, FRotator, FTransform (FQuat)
Used to specify the location, rotation, and scale of things in the World. A line trace for example needs two FVectors (Locations) to specify the start and end of the line. Every Actor has an FTransform that contains Location, Rotation, and Scale to give it a place in the world.
用于指定 World 中事物的位置、旋转和缩放。例如,线条追踪需要两个 FVector (Locations) 来指定线条的起点和终点。每个 Actor 都有一个 FTransform,其中包含 Location(位置)、Rotation (旋转) 和 Scale (缩放),以便它在世界中占有一席之地。
FVector, 3-axis as XYZ where Z is up. specifies either a Location or a direction much like common Vector-math.
FVector,3 轴作为 XYZ,其中 Z 向上。指定 Location 或 direction 与常见的 Vector-math 非常相似。
FRotator, 3 params Pitch, Yaw and Roll to give it a rotation value.
FRotator,3 个参数 Pitch、Yaw 和 Roll 为其提供旋转值。
FTransform, consists of FVector (Location), FRotator (Rotation) and FVector (Scale in 3-axis).
FTransform 由 FVector (位置)、FRotator (旋转) 和 FVector (3 轴缩放) 组成。
FQuat, another variable that can specify a rotation also known by its full name as Quaternion, you will mostly use FRotator in game-code however, FQuat is less used outside the engine modules although it can prevent Gimbal lock. (It’s also not exposed to Blueprint)
FQuat,另一个可以指定旋转的变量,其全名也称为 Quaternion,您将主要在游戏代码中使用 FRotator,但是,FQuat 在引擎模块之外较少使用,尽管它可以防止万向节锁定。(它也不会暴露在 Blueprint 中)
TArray, TMap, TSet
Basically variations of lists of objects/values. Array is a simple list that you can add/remove items to and from. TMaps are dictionaries, meaning they have Keys and Values (where the Key must always be unique) eg. TMap<int32, Actor>
where a bunch of Actors are mapped to unique integers. And finally, TSet which is an optimized (hashed) version of TArray, requires items in the list to be unique. Can be great for certain performance scenarios, but typically you would use TArray, unless you find you need to squeeze performance out of a specific piece of code.
基本上是对象/值列表的变体。数组 是一个简单的列表,您可以在其中添加/删除项目。TMap 是字典,这意味着它们有 Keys 和 Values(其中 Key 必须始终是唯一的),例如。TMap,其中一组 Actor 被映射到唯一的整数。最后,TSet 是 TArray 的优化(哈希)版本,它要求列表中的项目是唯一的。对于某些性能方案可能非常有用,但通常您会使用 TArray,除非您发现需要从特定代码段中挤出性能。
- Arrays in Unreal Engine.
Unreal Engine 中的数组。 - TMaps (aka Dictionaries)
TMaps(又名词典) - TSet
塞特
TSubclassOf
Very useful for assigning classes that derive from a certain type. For example, you may expose this variable to Blueprint where a designer can assign which projectile class must be spawned.
对于分配从特定类型派生的类非常有用。例如,您可以将此变量公开给 Blueprint,设计师可以在其中分配必须生成的射弹类。
UPROPERTY(EditAnywhere) // Expose to Blueprint
TSubclassOf<AProjectileActor> ProjectileClass; // The class to assign in Blueprint, eg. BP_MyMagicProjectile.
Now the designer will get a list of classes to assign that derive from ProjectileActor, making the code very dynamic and easy to change from Blueprint.
现在,设计人员将获得一个要从 ProjectileActor 派生的要分配的类列表,使代码非常动态,并且很容易从 Blueprint 中更改。
Here we use the TSubclassOf variable ProjectileClass to spawn a new instance: (link to code)
在这里,我们使用 TSubclassOf 变量 ProjectileClass 来生成一个新实例:(链接到代码)
FTransform SpawnTM = FTransform(ProjRotation, HandLocation);
GetWorld()->SpawnActor<AActor>(ProjectileClass, SpawnTM, SpawnParams);
C++ MACROS (& Unreal Property System) C++宏(&虚幻属性系统)
The ALL CAPS preprocessor directives are used by the compiler to ‘unfold’ into (large) pieces of code. In Unreal Engine, it’s most often used by the Unreal Property System and to add boilerplate code to our class headers. These examples are all macros, but Macros can be used for a lot more than shown below.
编译器使用 ALL CAPS 预处理器指令将 ALL CAPS 预处理器指令“展开”为(大)代码段。在 Unreal Engine 中,它最常用于 Unreal Property System,并将样板代码添加到我们的类标头中。这些示例都是宏,但宏的用途远不止下面显示的。
UFUNCTION()
Allows extra markup on functions, and exposes it to the Property System (Reflection) of Unreal. Commonly used to expose functions to Blueprint. Sometimes required by the engine to bind functions to delegates (eg. binding a timer to call a function).
允许在函数上进行额外标记,并将其公开给 Unreal 的 Property System (Reflection) (属性系统(反射))。通常用于向 Blueprint 公开函数。有时引擎需要将函数绑定到委托(例如,绑定计时器以调用函数)。
Here is additional information in a blog post on the available keywords within UFUNCTION()
and how to use them. There are a lot of function specifiers worth checking out, and BenUI does a great job of detailing what’s available.
以下是博客文章中有关 UFUNCTION() 中可用关键字及其使用方法的其他信息。有很多函数说明符值得一试,BenUI 在详细说明可用内容方面做得很好。
// Can be called by Blueprint
UFUNCTION(BlueprintCallable, Category = "Action")
bool IsRunning() const;
// Can be overriden by Blueprint to override/extend behavior but cannot be called by Blueprint (only C++)
UFUNCTION(BlueprintNativeEvent, Category = "Action")
void StartAction(AActor* Instigator);
UPROPERTY()
Allows marking-up variables, and exposing them to the Property System (Reflection) of Unreal. Commonly used to expose your C++ to Blueprint but it can do a lot more using this large list of property specifiers. Again, it’s worth checking out BenUI’s article on UPROPERTY specifiers.
允许标记变量,并将它们公开给 Unreal 的 Property System (Reflection) (属性系统(反射))。通常用于将 C++ 公开给蓝图,但使用这个庞大的属性说明符列表,它可以做更多的事情。同样,值得一看 BenUI 关于 UPROPERTY 说明符的文章。
// Expose to Blueprint and allow editing of its defaults and only grant read-only access in the node graphs.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
TSoftObjectPtr<UTexture2D> Icon;
// Mark 'replicated' to be synchronized between client and server in multiplayer.
UPROPERTY(Replicated)
USActionComponent* ActionComp;
GENERATED_BODY()
At the top of classes and structs and used by Unreal to add boilerplate code required by the engine.
位于类和结构体的顶部,并被 Unreal 用于添加引擎所需的样板代码。
GENERATED_BODY()
USTRUCT, UCLASS, UENUM
These macros are required when defining new classes, structs, and enums in Unreal Engine. When you create your new class, this is already added for you in the Header. By default, they will be empty like UCLASS()
but can be used to add additional markup to an object for example
在 Unreal Engine 中定义新类、结构和枚举时,需要这些宏。当您创建新类时,它已添加到 Header 中。默认情况下,它们将像 UCLASS() 一样为空,但可用于向对象添加其他标记,例如
USTRUCT(BlueprintType)
struct FMyStruct
{
}
UE_LOG (日志记录)
UE_LOG (Logging)Macro to easily log information including a category (eg. LogAI, LogGame, LogEngine) and a severity (eg. Log, Warning, Error, or Verbose) and can be an incredibly valuable tool to verify your code by printing out some data while playing your game much like PrintString in Blueprint.
Macro 轻松记录信息,包括类别(例如。LogAI、LogGame、LogEngine)和严重性(例如。Log、Warning、Error 或 Verbose),并且可以是一个非常有价值的工具,通过在玩游戏时打印一些数据来验证您的代码,就像蓝图中的 PrintString 一样。
// The simple logging without additional info about the context
UE_LOG(LogAI, Log, TEXT("Just a simple log print"));
// Putting actual data and numbers here is a lot more useful though!
UE_LOG(LogAI, Warning, TEXT("X went wrong in Actor %s"), *GetName());
The above syntax may look a bit scary. The third parameter is a string we can fill with useful data, in the above case we print the name of the object so we know in which instance this happened. The asterisk (*) before GetName()
is used to convert the return value to the correct type (from FString returned by the function to Char[] for the macro). The Unreal Wiki has a lot more detailed explanation on logging.
上面的语法可能看起来有点吓人。第三个参数是一个字符串,我们可以填充有用的数据,在上面的情况下,我们打印对象的名称,以便我们知道这是在哪个实例中发生的。GetName() 前面的星号 (*) 用于将返回值转换为正确的类型(从函数返回的 FString 转换为宏的 Char[])。Unreal Wiki 对日志记录有更详细的解释。
##Modules 模块
Unreal Engine consists of a large number (1000+) of individual modules. Your game code is contained in one or multiple modules. You can place your game-specific logic in one module, and your more generic framework logic for multiple games in another to keep a separation of dependencies.
Unreal Engine 由大量 (1000+) 个单独的模块组成。您的游戏代码包含在一个或多个模块中。您可以将特定于游戏的逻辑放在一个模块中,将多个游戏的更通用的框架逻辑放在另一个模块中,以保持依赖项的分离。
You can find examples of these code modules in your engine installation folder (eg. Epic Games\UE_5.0\Engine\Source\Runtime\AIModule) where each module has its own [YourModuleName].build.cs
file to configure itself and its dependencies.
您可以在引擎安装文件夹中找到这些代码模块的示例(例如。Epic Games\UE_5.0\Engine\Source\Runtime\AIModule),其中每个模块都有自己的 [YourModuleName].build.cs 文件来配置自身及其依赖项。
Not every module is loaded by default. When programming in C++ you sometimes need to include additional modules to access their code. One such example is AIModule
that you must add to the module’s *.build.cs file in which you wish to use it before being able to access any of the AI classes it contains.
默认情况下,并非每个模块都加载。使用 C++ 编程时,有时需要包含其他模块才能访问其代码。一个这样的例子是 AIModule,你必须将其添加到你希望使用它的模块的 *.build.cs 文件中,然后才能访问它包含的任何 AI 类。
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "AIModule", "GameplayTasks", "UMG", "GameplayTags", "OnlineSubsystem", "DeveloperSettings" });
The above is one example from ActionRoguelike.build.cs where AIModule
(among several others) has been added.
以上是 ActionRoguelike.build.cs 中的一个示例,其中添加了 AIModule(以及其他几个示例)。
You can include additional modules through the .uproject as well instead of the build file. This is where the editor will automatically add modules under AdditionalDependencies
when required (such as the moment of creating a new C++ class that derives from a missing module).
您也可以通过 .uproject 而不是构建文件包含其他模块。这是编辑器在需要时(例如创建从缺失模块派生的新 C++ 类)在 AdditionalDependencies 下自动添加模块的位置。
Ari from Epic Games has a great talk on Modules that I recommend checking out and is linked below. I’ve added a few takeaways from his talk.
来自 Epic Games 的 Ari 关于模块的精彩演讲,我建议你查看,链接如下。我从他的演讲中添加了一些要点。
为什么使用模块?
Why use modules?- Better code practices/encapsulation of functionality
更好的代码实践/功能封装 - Re-use code easily between projects
在项目之间轻松重用代码 - Only ship modules you use (eg. trim out Editor-only functionality and unused Unreal features)
仅提供你使用的模块(例如,删减仅限编辑器的功能和未使用的 Unreal 功能) - Faster compilation and linking times
更快的编译和链接时间 - Better control of what gets loaded and when.
更好地控制加载的内容和时间。
Garbage Collection (Memory Management) 垃圾回收 (内存管理)
Unreal Engine has a built-in garbage collection that greatly reduces our need to manually manage object lifetime. You’ll still need to take some steps to ensure this goes smoothly, but it’s easier than you’d think. Garbage collection occurs every 60 seconds by default and will clean up all unreferenced objects.
Unreal Engine 具有内置的垃圾回收功能,可大大减少我们手动管理对象生命周期的需要。您仍然需要采取一些步骤来确保这一切顺利进行,但这比您想象的要容易。默认情况下,垃圾回收每 60 秒进行一次,并将清理所有未引用的对象。
When calling MyActor->DestroyActor()
, the Actor will be removed from the world and prepared to be cleared from memory. To properly manage ‘reference counting’ and memory you should add UPROPERTY()
to pointers in your C++. I’ll discuss that more in the section below.
调用 MyActor->DestroyActor() 时,Actor 将从世界中删除,并准备从内存中清除。要正确管理“引用计数”和内存,您应该将 UPROPERTY() 添加到 C++ 中的指针。我将在下面的部分中详细讨论这一点。
It may take some time before GC kicks in and actually deletes the memory/object. You may run into this when using UMG and GetAllWidgetsOfClass
. When removing a Widget from the Viewport, it will remain in memory and is still returned by that function until GC kicks in and has verified all references are cleared.
GC 可能需要一些时间才能启动并实际删除内存/对象。在使用 UMG 和 GetAllWidgetsOfClass 时,您可能会遇到这种情况。从视窗中删除 Widget 时,它将保留在内存中,并且仍由该函数返回,直到 GC 启动并验证所有引用均已清除。
It’s important to be mindful of how many objects you are creating and deleting at runtime as Garbage Collection can easily eat up a large chunk of your frame time and cause stuttering during gameplay. There are concepts such as Object Pooling to consider.
请务必注意在运行时创建和删除的对象数量,因为垃圾回收很容易占用大量帧时间,并导致游戏过程中卡顿。需要考虑对象池等概念。
Automatic Updating of References (Actors & ActorComponents) 自动更新引用(Actor & ActorComponents)
References to Actors (and ActorComponents) can be automatically nulled after they get destroyed. For this to work you must mark the pointer with UPROPERTY()
so it can be tracked properly.
对 Actor(和 ActorComponents)的引用在销毁后可以自动为空。为此,您必须使用 UPROPERTY() 标记指针,以便可以正确跟踪它。
// SInteractionComponent.h
UPROPERTY()
TObjectPtr<AActor> FocusedActor;
“Destroyed actors don’t have references to them nulled until they’re actually garbage collected. That’s what IsValid() is used for checking.” – Ari Arnbjörnsson
“被销毁的 actor 在实际被垃圾回收之前,不会对它们的引用为空。这就是 IsValid() 用于检查的内容。
You can read more about automatic updating of references on the official docs. The thing to keep in mind is that it only works for Actor and ActorComponent derived classes.
你可以在官方文档中阅读更多关于自动更新参考文献的信息。要记住的是,它仅适用于 Actor 和 ActorComponent 派生类。
In UE5 the behavior for automatically clearing RawPtrs / ObjectPtrs will change.
在 UE5 中,自动清除 RawPtrs / ObjectPtrs 的行为将发生变化。
“This will be changing a bit in UE5. The GC will no longer clear UPROPERTY + RawPtr/TObjectPtr references (even for Actors) but instead mark them as garbage (MarkAsGarbage()) and not GC them. The only way to clear the memory will be to null the reference or use weak pointers.” – Ari Arnbjörnsson
“这在 UE5 中会有一些变化。GC 将不再清除 UPROPERTY + RawPtr/TObjectPtr 引用(即使对于 Actor),而是将它们标记为垃圾 (MarkAsGarbage()),而不是 GC 它们。清除内存的唯一方法是将引用 null 或使用弱指针。
I will update this post once the new behavior has been enabled by default.
默认情况下启用新行为后,我将更新此帖子。
TWeakObjPtr (UObjects)
Weak Object Pointer. This is similar to pointers like UObject*
, except that we tell the engine that we don’t want to hold onto the memory or object if we are the last piece of code referencing it. UObjects are automatically destroyed and garbage collected when no code is holding a (hard) reference to it. Use weak object pointers carefully to ensure objects are GC’ed when needed.
弱对象指针。这与 UObject* 等指针类似,不同之处在于,如果我们是引用内存或对象的最后一段代码,我们会告诉引擎,我们不想保留内存或对象。当没有代码持有对它的(硬)引用时,UObjects 会自动销毁并进行垃圾回收。请谨慎使用弱对象指针,以确保在需要时对对象进行 GC。
// UGameAbility derived from UObject
TWeakObjectPtr<UGameAbility> MyReferencedAbility;
Now we don’t try to hold onto the object explicitly and it can be garbage collected safely. Before accessing the object, we must call .Get()
which will attempt to retrieve the object from the internal object array and makes sure it’s valid. If it’s no longer a valid object, a nullptr is returned instead.
现在我们不再尝试显式保留对象,它可以安全地进行垃圾回收。在访问对象之前,我们必须调用 .Get() 的 Git 函数,它将尝试从内部对象数组中检索对象并确保其有效。如果它不再是有效对象,则返回 nullptr。
UGameAbility* Ability = MyReferencedAbility.Get();
if (Ability)
{
}
- Documentation on WeakObjPtr
WeakObjPtr 上的文档 - Soft & Weak Object Pointers (Ari Arnbjörnsson)
软弱物体指针 (Ari Arnbjörnsson) - Even more technical, on Smart Pointers in Unreal
更技术性的是,在 Unreal 中的智能指针上
Class Default Object 类默认对象
Class Default Object is the default instance of a class in Unreal Engine. This instance is automatically created and used to quickly instantiate new instances. You can use this CDO in other ways too to avoid having to manually create and maintain an instance.
Class Default Object (类默认对象) 是 Unreal Engine 中类的默认实例。此实例是自动创建的,用于快速实例化新实例。您也可以以其他方式使用此 CDO,以避免手动创建和维护实例。
You can easily get the CDO in C++ via GetDefault. You should take care to not accidentally make changes to the CDO as this will bleed over into any new instance created for that class.
您可以通过 GetDefault 轻松获取 C++ 中的 CDO。您应该注意不要意外地对 CDO 进行更改,因为这会渗透到为该类创建的任何新实例中。
Below is one example from SaveGameSubsystem using the ‘class default object’ to access DeveloperSettings (Project & Editor Settings) without creating a new instance.
下面是SaveGameSubsystem的一个示例,它使用'类默认对象'来访问DeveloperSettings(项目和编辑器设置),而无需创建新实例。
// Example from: SSaveGameSubsystem.cpp (in Initialize())
const USSaveGameSettings* Settings = GetDefault<USSaveGameSettings>();
// Access default value from class
CurrentSlotName = Settings->SaveSlotName;
Asserts (Debugging) 断言 (调试)
If you really need to be sure if something is not Null or a specific (if-)statement is true and want the code to tell you if it isn’t, then you can use Asserts. Asserts are great as additional checks in code where if it were to silently fail, code later down the line may fail too (which may then take a while to debug and find the origin).
如果你真的需要确定某项内容是否为 Null 或特定 (if-) 语句是否为 true,并希望代码告诉你它是否为 true,那么你可以使用 Asserts。断言非常适合作为代码中的附加检查,如果它静默失败,稍后的代码也可能失败(这可能需要一段时间来调试和找到来源)。
Two main assertion types are check and ensure.
两种主要的断言类型是 check 和 ensure。
check(MyValue == 1); // treated as fatal error if statement is 'false'
check(MyActorPointer);
// convenient to break here when the pointer is nullptr we should investigate immediately
if (ensure(MyActorPointer)) // non-fatal, execution is allowed to continue, useful to encapsulate in if-statements
{
}
Ensure is great for non-fatal errors and is only triggered once per session. You can use ensureAlways
to allow the assert to trigger multiple times per session. But make sure the assert isn’t in a high-frequency code path for your own sake or you’ll be flooded with error reports.
Ensure 非常适合非致命错误,并且每个会话仅触发一次。您可以使用 ensureAlways 允许 assert 在每个会话中触发多次。但是,为了您自己,请确保 assert 不在高频代码路径中,否则您将被错误报告淹没。
It’s good to know that Asserts are compiled out of shipping builds by default and so it won’t negatively affect runtime performance for your end-user.
很高兴知道 Asserts 默认是从发布的版本中编译出来的,因此它不会对最终用户的运行时性能产生负面影响。
By adding these asserts you are immediately notified of the (coding) error. One tip I would give here is to only use it for potential coder mistakes and perhaps don’t use it when a piece of content isn’t assigned by a designer (having them run into asserts isn’t as useful as to them it will look like a crash (unless they have an IDE attached) or stall the editor for a bit (as a minidump is created) and not provide a valuable piece of information). For them might be better of using logs and prints on the screen to tell them what they did not set up properly. I sometimes still add in asserts for content mistakes as this is very useful in solo or small team projects.
通过添加这些断言,您会立即收到 (编码) 错误的通知。我在这里给出的一个提示是,仅将其用于潜在的编码人员错误,并且当设计人员未分配一段内容时,可能不要使用它(让它们遇到断言并不像它们有用,它看起来像崩溃(除非它们附加了 IDE)或使编辑器停滞一段时间(因为创建了小型转储)并且不会提供有价值的信息)。对他们来说,最好在屏幕上使用日志和打印来告诉他们他们没有正确设置什么。我有时仍然会为内容错误添加断言,因为这在个人或小型团队项目中非常有用。
Core Redirects 核心重定向
Core Redirects are a refactoring tool. They let you redirect pretty much any class, function, name, etc. after your C++ has changed via the configuration files (.ini). This can be incredibly helpful in reducing the massive headache of updating your Blueprints after a C++ change.
Core Redirects 是一种重构工具。它们允许你在通过配置文件 (.ini) 更改 C++ 后重定向几乎任何类、函数、名称等。这对于减少在 C++ 更改后更新蓝图的巨大麻烦非常有帮助。
The official documentation (above) does a pretty good job of explaining how to set this up. It’s one of those things that’s good to know before you need it. Modern IDEs with proper Unreal Engine support such as JetBrains Rider even have support for creating redirects when you refactor your Blueprint exposed code.
官方文档(上面)很好地解释了如何设置。这是在您需要之前了解它的好处之一。具有适当 Unreal Engine 支持的现代 IDE(例如 JetBrains Rider)甚至支持在重构蓝图公开代码时创建重定向。
Closing 关闭
I hope this article provided you with some new insight into C++ and how it’s used in Unreal Engine.
我希望本文能为您提供一些关于 C++ 及其在 Unreal Engine 中的使用的新见解。
Please comment below if you’d like to see anything else you struggled with when starting with C++ and gameplay programming in Unreal Engine so I can add it to the list! This article is mainly focused on the uncommon aspects that are unique to Unreal Engine and how they apply within that context rather than C++ or programming in general.
如果您想查看在虚幻引擎中开始使用 C++ 和游戏编程时遇到的其他困难,请在下面发表评论,以便我将其添加到列表中!本文主要关注虚幻引擎独有的不常见方面,以及它们如何应用于该上下文中,而不是一般的 C++ 或编程中。
As always, don’t forget to follow me on Twitter for more Unreal Engine tutorials!
与往常一样,别忘了在 Twitter 上关注我,获取更多虚幻引擎教程!
On The Horizon… 在地平线上...
Things that didn’t quite make it in yet or require a more detailed explanation in the current sections. Leave your suggestions in the comments!
尚未完全包含在其中或需要在当前部分中更详细解释的内容。在评论中留下您的建议!
- Unreal Header Tool / Unreal Build Tool (”Unreal Build System”)
Unreal Header Tool / Unreal Build Tool (“Unreal Build System”) - Project Structure (Game, Engine, build.cs, Target, binaries, .uproject)
项目结构(游戏、引擎、build.cs、目标、二进制文件、.uproject) - Including other classes (and how to find their path)
包括其他类(以及如何找到它们的路径) - Hot Reloading & Live Coding in UE5.0
UE5.0中的热重载和实时编码 - IDE recommendations and setup
IDE 建议和设置 - Timers, Async actions (Latent), Multi-threading
计时器、异步操作 (Latent)、多线程 - Game Class Hierarchy and most commonly used classes (primer).
Game Class Hierarchy 和最常用的类 (Primer)。 - virtual/override keywords. (”Virtual Functions and Polymorphism”)
virtual/override 关键字。(“虚函数和多态性”) - ‘const’ keyword & const correctness
'const' 关键字 & const 正确性 - Operator Overloading (examples of where Unreal has done so, eg. with FString when used with logging)
运算符重载(Unreal 执行此操作的示例,例如,与日志记录一起使用时使用 FString)
References & Further Reading 参考资料和进一步阅读
- Laura’s C++ Speedrun
Laura 的 C++ 速通 - Why C++ In Unreal Engine Isn’t That Scary?
为什么虚幻引擎中的 C++ 不是很可怕吗? - Gameplay Framework Primer
- Introduction to Unreal C++ Programming (External)
Unreal C++ 编程简介(外部) - Gameplay Framework Documentation (External)
Gameplay Framework 文档(外部) - Gameplay Programming Documentation (External)
Gameplay 编程文档(外部)