Notes from Professional Game Development in C++ and Unreal Engine

These are my notes for Tom Looman’s course Professional Game Development in C++ and Unreal Engine. These notes mainly consist of things i did not know or understand at the time, and things i did not want to forget. This is mainly for myself.

  1. About Unreal Engine
  2. Naming and Conventions
  3. Editor Tips
  4. C++
  5. Blueprint Tips
  6. Development Tips
  7. Math Tips
  8. Interfaces
  9. Multiplayer
  10. Collision
  11. UMG
  12. Materials
  13. Sound
  14. Anim Blueprints
  15. AI
  16. EQS
  17. Curves
  18. Commands and Cheats
  19. Savegame System
  20. Data Tables
  21. Data Assets
  22. Soft and Hard References
  23. Profiling
  24. Cooking/Packaging
  25. Performance Tips

About Unreal Engine

Coordinate System

X is Forward
Y is Right
Z is Up

Transforms

A transform holds Location, Rotation & Scale

Control Rotation

Control rotation refers to the rotation of the player controller. The controller’s rotation has priority over the pawn it is controlling. Setting the pawn rotation will do nothing if it is being controlled by a controller. You will need to set the controller rotation instead. So if your pawn is having problems rotating, it might be that the controller is overwriting it.

Components and transforms

Components that derive from USceneComponent have a transform (relative to its parent). So, remember to attach them to their correct parent.

Components that derive from UActorComponent don’t have a transform.

Naming and Conventions

Unreal Engine Prefixes

Unreal uses prefixes when naming their classes, these are the most common ones.

Prefix Meaning
U Derives from UObject
A Derives from Actor
F Structs
E Enums
I Interfaces

Source Files

It is customary to add a prefix to all your source code files. This makes files easily distinguishable from engine files and other projects.

For example we could choose the prefix ‘RPG’ for a project and our source files could be “RPGMyCharacter.h/cpp”.

Editor Tips

Resetting variables to their default state

In the editor, there’s a little arrow next to variables, clicking reset’s the value to it’s default state.

Slomo Command

The “slomo” command will slow or accelerates simulation time. 1 is the default value. Between 0 and 1, it becomes slower. 1+ is faster.

Make warnings more prominent

It’s a good idea to make warnings easier to see and in your face. Turn on “Promote Output Log Warnings During PIE”.

Asset Searching Shortcut

Ctrl+P opens a window to search for assets. It’s really fast since as soon as you press the shortcut you can start typing.

Move around in PIE without stopping the simulation

Press F8 while playing in editor to be able to move around without stopping the simulation. Press F8 again to go back to your camera view.

Something should be working but its not?

The unreal editor is good but not perfect. Sometimes weird things happen that make no sense. Maybe a file is not appearing on a dropdown or something that you think should be there/working is not.

Try restarting the editor, recompiling, re-saving the filess, hell maybe even restart your computer.

C++

Whenever possible, forward declare

Try to not include headers in C++ class definitions. It is better to forward declare them for faster compilation times. Including headers will result in huge translation units.

Order your code

It’s a good idea to order your functions in the order they might get executed. For example, the constructor of a class should be near the top of the file. BeginPlay should also be near the top of the file. Mantaining a consistency like this will make finding functions faster and easier.

Stepping into engine code

To use the debugger with Unreal’s engine code, you need to download the editor symbols for debugging. This can be done from Epic Game’s Launcher.

Structs

When declaring structs in unreal, remember to prefix them with ‘F’ and add the required macro’s for unreal’s header tool.

Example:

1
2
3
4
5
6
7
8
USTRUCT()
struct FMyStruct
{
    GENERATED_BODY()

    UPROPERTY()
    int MyVar;
};

Casting

Using regular C-style casting is not recommended. Unreal has it’s own way of casting and it’s done using an existing template.

1
ClassToCastTo *MyPtr = Cast<ClassToCastTo>(Pointer);

Static Class

Sometimes you need a variable that does not need to point to a instance of a class, but to a class type. In order to get the class type we can call a method called StaticClass().

1
ClassName::StaticClass();

Asserts

ensure” will assert but continue the game after. By default it will only halt execution once.

ensureAlways” is another flavor of ensure, but this one will always halt execution.

check” will halt execution when it’s evaluation is false.

ensureMsgf” not only triggers the debugger when failing, but it also logs a message. This message appears in a red color inside the editor.

AActor::PostInitializeComponents

This function runs before BeginPlay. It’s a good place to bind to component events, like: “OnComponentHit”.

Const UFUNCTION

Setting a UFUNCTION to const will make it appear as a green node with no execution pin inside blueprints.

Performance

Don’t call “GetAllActorsOfClass”. It iterates through every actor on the world to find them.

Iterating through all actors in a world

To loop over all the classes in the world, we can use the class “TActorIterator”.

It’s what “GetAllActorsOfClass” uses behind the curtain.

1
2
3
4
5
for(TActorIterator<SMyClass>It;It(GetWorld(); It++))
{
    SMyClass *Ptr = *It;
	// Do stuff
}

Destroy Actor in X Seconds

The following function will set an actor to be destroyed in X seconds

1
Actor->SetLifeSpan(Seconds);

Enable Ragdoll

To enable ragdoll behaviour in C++, call:

1
2
GetMesh()->SetAllBodiesSimulatePhysics(true);
GetMesh()->SetCollisionProfileName("Ragdoll");

Creating a reusable function library

To create a reusable function library, create a class that inherits from “UBlueprintFunctionLibrary”. Make all the functions static, and expose them to Blueprints. An example of this is “UGameplayStatics”.

Static member functions

Static member functions are accessed by the class’s name as namespace.

1
MyClass::MyStaticFunction();

Timers with parameters

You can create a timer that executes a function with parameters.

1
2
3
4
5
6
FTimerHandle Handle;
FTimerDelegate Delegate;

Delegate.BindUFunction(this, "FunctionName", FirstParameter);

GetWorldTimerManager().SetTimer(Handle, Delegate, Time, false);

This will run FunctionName with parameters first Parameter in Time seconds

Unposses a Pawn/Character

1
MyController->Unposses();

Creating CVars (console variables)

To create a Cvar, go the .cpp file and at the top of the file declare the CVar.

1
2
3
4
static TAutoConsoleVariable<bool> CVarVariableName(TEXT("My.Variable"), defaultvalue(true), TEXT("Help text that says what this variable is for"), ECVF_Cheat)

// To get the value
bool Result = CVarVariableName.GetValueOnGameThread();

Instantiating new objects

The function NewObject(…) is used to instantiate new objecs. The paramater “Outer” is who owns this object.

Creating blueprints from UObject derived classes

Whenever you need to create a blueprint from a UObject derived class, you need to mark the class as Blueprintable

1
UCLASS(Blueprintable)

C++ Functions marked as BlueprintNativeEvent

When marking UFUNCTIONS as BlueprintNativeEvent, the C++ implementation name must append “_Implementation(…)” to it’s definition.

1
2
3
4
5
6
7
8
9
// Header
UFUNCTION(BlueprintNativeEvent)
void MyFunction();

// Implementation
void MyClass::MyFunction_Implementation()
{
    // Code
}

Accessing UWorld from blueprint classes derived from UObject

Classes that are derived from UObject don’t have access to UWorld when used in blueprints. This severely limits what can be donde in blueprints. To fix this, override the GetWorld function.

1
UWorld *GetWorld() const override;

GameplayTags in C++

We cannot create GameplayTags in C++. They must be added using the unreal editor.

But we can request and access gameplay tags from C++.

1
2
// Request a GameplayTag to the engine
FGameplayTag MyTag = FGameplayTag::RequestGameplayTag("Tag.MyTag");

Functions not appearing in blueprints

If a function is not showing up in the blueprint function list, it’s probably missing a UFUNCTION specifier. Try adding “BlueprintCallable”.

1
2
UFUNCTION(BlueprintCallable)
MyFunction();

ECollisionChannel Variables in C++

Creating a variable of type “ECollisionChannel” does not compile. You need to do it in the following way:

1
TEnumAsByte<ECollisionChannel> Channel;

GameplayTags Container in C++

When we need a container of gameplaytags, it’s a good idea to use FGameplayTagContainer. It’s an array fo gameplay tags with a bunch of helper functions implemented.

1
FGameplayTagContainer MyContainer;

Blueprint Tips

How to know what class/namespace does a node belong to

A good way to know what class/namespace a blueprint node belongs to is to mouse over it and looking at it’s target.

Variables or Functions not appearing in the editor

Sometimes variables or functions won’t appear in the blueprint editor, most likely you forgot to add a property specifier. Check your C++ code.

Get Default Values of a Class

To get a class default variable values and settings, you can use the node “GetClassDefaults”.

Blueprint Performance Tips

In project settings, you can disable tick in blueprints by default. Setting this makes using the tick function a conscious decision.

“Project Settings -> Blueprints -> Can blueprints tick by default”.

Development Tips

Use Debug Shapes

Sometimes drawing things in-game can be useful for debugging. Unreal has a bunch of easy to use functions to draw things such as boxed, spheres, lines…

1
2
3
4
5
6
7
8
9
DrawDebugPoint(...);
DrawDebugSphere(...);
DrawDebugCircle(...);
DrawDebugCircle(...);
DrawDebugSolidBox(...);
DrawDebugBox(...);
DrawDebugLine(...);
DrawDebugDirectionalArrow(...);
DrawDebugCrosshairs(...);

Get Component By Class

Use GetComponentByClass to find a specific component. This is faster than casting to player and accessing the component from player. This also has less error of invalid pointer, casting might fail.

Teleporting Actors

When you need to teleport an actor somewhere, it’s a good idea to use the built-in function “TeleportTo”. It handles a lot of edge cases when teleporting something. Such as checking for collision, valid location and more edge cases.

1
Actor->TeleportTo(...);

Math Tips

Finding a point in space where player is looking

In order to find a point in space where the player is looking, you need to get it’s rotator transform it to a vector, add the actor’s location and scale the rotation vector.

1
2
// GetRotation.Vector is the same thing as GetDirection(), Actor.GetDirection * 1000.0f;
FVector Point = Actor.GetLocation() + (Actor.GetRotation().Vector * 1000.0f);

Interfaces

Interfaces are useful to create a standardized API that different actors or object might implement. For example, you might want to create an interface that implements two different member functions, such as “SaveActor” or “LoadActor”. Making you are actors inherit this interface will make it easier to implement saving and loading of actors.

Actors can also be searched by interface. For example, you might do a search for all actors that implement the “Saving/Loading” interface.

When an Interface function is marked as BlueprintNativeEvent, the function definition must append “_Implementation” to it’s name.

1
2
3
4
5
6
7
8
9
// Header
UFUNCTION(BlueprintNativeEvent)
MyInterfaceFunction()

// Definition
MyInterfaceFunction_Implementation()
{
    ....
}

When an interface function is marked as BlueprintNativeEvent or BlueprintImplementable, to call it from C++ you need to prepend “Execute_” to the function name.

1
2
3
4
5
6
// Header
UFUNCTION(BlueprintNativeEvent)
MyInterfaceFunction();

// The function must be called like this
Execute_MyInterfaceFunction();

Collision

Collision Settings

  • Try to disable overlaps until required. “bGenerateOverlaps = false”.
  • Make sure “Collision Profiles” only react to necessary channels or even better, disable collision entirely.
  • Set “Collision Enabled” settings to “Query Only”, unless physics simulation is required.
  • Avoid Hard References wherever possible.

Object Channels

We can create new collision channels such as WorldStatic or WorldDynamic.

Go to “ProjectSettings->ObjectChannels->New”.

Show Collisions

The command “showcollision” enables a visualization of the collision primitives in game.

Multiplayer

Multiplayer Gameplay Classes

Class Where does it exist?
PlayerController Exists on the server and on the local client.
PlayerState Exists on the server and on all the clients
GameMode Only exists on the server
GameStateBase Exists on the server and on all the clients. This class is usually used to share GameMode data to all the clients and server
Pawn Every Pawn exists on the server and on all the clients also.
AIController Exists only on the server

How to know if a variable is being replicated in blueprints

If a variable is being replicated, the node will have an icon that looks like two cotton balls.

How to check if the code is running on the server

The function “HasAuthority” returns true if we are the server

1
bool bServer = HasAuthority();

Checking what type of client/server we are

There’s a function call IsNetMode that returns what type of client/server we are. It has more return types that just client or server.

1
2
3
4
5
6
7
// Examples Modes
// - NM_Client
// - NM_DedicatedServer
// - NM_ListenServer
// - NM_MAX
// - NM_Standalone
World->IsNetMode(ENetMode Mode);

Updating/Replicating Actors

Actors are updated in 2 ways:

  1. Property Updates (Automatically send updated variables from server to clients)
  2. RPC’s (Execute functions on the other machin)

Variables are synced from server to client, always!

Marking an actor to replicate in C++

In order to mark an actor for replication, set it’s variable “bReplicates” to true in it’s constructor. If you need to dynamically mark an actor as replicated you can use the “SetReplicates” function.

// In Constructor, prefer this over calling SetReplicates directly
bReplicates = true;

// To set replicate at runtime
SetReplicates(true);

Replicating Actor Components

To set an ActorComponent to replicate, call “SetIsReplicatedByDefault(true)” in it’s constructor.

1
2
// Constructor
SetIsReplicatedByDefault(true);

How to setup variables for replication

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Header File
// Mark a variable with the Replicated Property
UPROPERTY(Replicated, Reliable)
int MyReplicatedVariable;

// Implementation File
// Constructor
bReplicates = true; // Remember to set this in the constructor

// In the same class, implement the following function and place DOREPLIFETIME macro's with replication configuration for each variable
void AActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty) &OutLifetimeProps) const
{
    DOREPLIFETIME(MyClass, MyReplicatedVariable);
}

Now, the member variable MyReplicatedVariable will be synchronized to all connected clients for every copy of this actor type.

There is no need to declare GetLifetimeReplicatedProps in the header file. This is because once we mark a variable as replicated, the unreal header tool will automatically generate the definition for us.

Conditional replication of a variable

There’s another macro you can use in “GetLifetimeReplicatedProps” function to configure replication. This is the DOREPLIFETIME_CONDITION() macro.

This is mostly used to save bandwith and cpu time. The less data sent, the better. There are a lot of different conditions so be sure to check the official documentation.

1
2
// Be sure to check the different COND_ macros for different conditions
DOREPLIFETIME_CONDITION(Class, Variable, COND_...);

Running a function everytime a variable is replicated, OnRep

Replicated variables can also run a function everytime they change. For this we need to mark the variable with “ReplicatedUsing” property.

In Blueprints, This is called “RepNotify

1
2
3
4
5
UPROPERTY(ReplicatedUsing="OnRep_LidOpened", BlueprintReadOnly)
bool bLidOpened;

UFUNCTION()
void OnRep_LidOpened;

The convention is to prepend the function name with “OnRep_”

OnRep and RepNotify functions are only called automatically on clients. They are not called on the server. If you need this function to be called on the server you must call it manually.

Types of Remote Procedure Calls (RPC's)

There are three types of RPC’s

  • Server RPC's
  • Client RPC's
  • NetMulticast

Server RPC's

Functions marked as Server RPC are functions that are designed to be called on the server.

When called from a client, it requests the server to execute a function.

When called from the server, only the server runs the function.

1
2
UFUNCTION(Server, Reliable)
MyFunction();

Client RPC's

Client RPC’s are called by the server to run on a specific client.

NetMulticast

These are called by the server to run a function on all clients.

These are mostly used for transient things such as a barrel explosion. They might not be good for replicating state.

A new player that enters the server after the multicast was executed, will not receive this explosion event.

RPC's Example Declaration and Implementation

1
2
3
4
5
6
7
8
9
// Header
UFUNCTION(Server, Reliable, WithValidation) // Create a reliable server RPC
void MyFunctyion(int AParamenter);

// Implementation File
void AExampleClass::MyFunction_Implementation(int AParameter)
{
    // Stuff
}

Be aware that the implementation of the function appends “_Implementation” to the function name.

Remember to replicate as little data as possible, this includes RPC function parameters!

Creating RPC's in Blueprints

  • Create a new custom event
  • Click on it, and select the RPC type on the details panel
  • Select Reliable/Unreliable

Reliable / Unreliable

You can add a UPROPERTY/UFUNCTION specifier to functions and variables.

Reliable - This will make the variable guaranteed to update on all clients. If a network packet is lost, it will be resent until acknowledgement is received. Be aware that if a reliable packet fails to arrive, the engine will not process new packets until this one has arrived. The player might experience a lag spike.

Unreliable - Not guaranteed, packet can get lost and it won’t be resent.

Use Unrealiable as much as you can!

Where is my code being executed?

Add a ton of logging with a variable that shows whether it is running on the client or server.

Where should my code be executed?

Changing Gamestate? Execute on server.

Cosmetic? Execute on client.

They are often mixed to predict game state and reduce latency for clients.

Checking if a PlayerController is being controlled locally

There are two functions to check if a controller is being controlled locally.

1
2
APlayerController->IsLocalController(); // Also works for AI
AController->IsLocalPlayerController(); // Only works on players

IsLocallyControlled()

This function returns true if the pawn is controlled locally. It’s useful for running code only locally.

1
Pawn->IsLocallyControlled();

Blueprint Print Node

Printing a string in blueprints while on a multiplayer session will prepend where that node is running on a client or server. Printing “Hello World” will show up as “Client/Server: Hello World”

PlayerState

Every player has a “player state”. It’s a class which is automatically replicated to all clients and server. This is a good place to store variables that need to persist such as NumberOfKills.

GameStateBase

This class is usually used to share GameMode data to all the clients and server.

Using several replicated variables at the same time

If you need more than one replicated variable at the same time, you can wrap these in a struct and replicate the struct.

Network Relevancy

Unreal has a concept in networking called “Relevancy”. It’s mainly used to select which clients receive what data. For example, two players in fortnite really far away from each other do not need to be updated of each other since they are not near enough for this data to be relevant.

Sadly, this was not covered in the course.

Replicating UObjects

UObjects don’t have any built-in replication code. So in order to replicate UObjects you need to override the function “IsSupportedForNetworking” and just return true from it.

Also override the function “ReplicateSubObjects” and inside it call “ReplicateSubObject”.

1
Channel->ReplicatedSubObject(MyObject, *Bunch, *RepFlags);

Gotcha's

  1. Be careful when destroying actors, if the actor sent an RPC it might get destroyed before it ever sends it.

  2. Be careful when modifying local variables marked as RepNotifies. If we locally modify a variable, the next time the server updates the value, our OnRep function might not run. Unreal checks for differences in value to choose wether the function runs or not.

UMG

Creating a widget in C++

To create a C++ Widget class, inherit from “UserWidget”.

Instancing a widget in C++

1
2
MyWidget = CreateWidget<MyWidgetClass>(Owner, Class);
MyWidget->AddToViewport();

Removing a widget

To remove a widget, call the function:

1
Widget->RemoveFromParent();

Accesing Widget variables such as textboxes from C++

To access a UMG widget in C++, do the following:

  • Create a C++ class that inherits from “UserWidget”
  • Add a class member variable that represents the variable/widget
  • Mark it with UPROPERTY(meta=BindWidget)
  • In the editor create a widget of the same type and name
  • You can now access it from C++
1
2
3
4
5
6
7
// Remember to create the same variable with the same name and type in the UMG Blueprint

UPROPERTY(meta=BindWidget)
UTextBlock *MyTextBlock;

UPROPERTY(meta=BindWidget)
UImage *MyImage;

Tick function in widget

The tick function in widget’s is called “NativeTick”, override this function when needed.

Add widget to another widget

You can add other widget to a widget. For example you could have a HUD widget, and health could be another widget. This allows us to separate things and make blueprints more modular.

Add boxed as parent to align them

You can add Boxes and set the box as parent of other widget to make them auto aligned.

Bind

You can click on bind button to add a function that runs every frame and updates the value of something. This is not recommended since running everyfame is very expensive. Prefer using delegates to update values on change/update.

Get player&rsquo;s character from UMG

use GetOwningPlayerPawn to get the player’s character

1
GetOwningPlayerPawn();

Construct/BeginPlay

Construct is the BeginPlay of UMG objects. This is a good place to bind to the needed delegates!

Don't call widget functions from outside

Don’t create functions inside widgets and call them from actors. Make the UI separate from game code to avoid mantainance nightmares. Never call widgets functions from outside.

Use PreConstruct to set values

Set things such as colors and values in the pre construct event.

Make a widget have input parameters on creation

Checking “Expose on spawn” to a variable makes that variable be an input to the CreateWidget node. This way we can set variables when a widget is being created.

World Position to Screen Position

The node “ProjectWorldToScreen” transforms a position in world coordinates to screen coordinates.

If the screen coordinate seems weirdly off, try dividing ScreenPosition/GetViewPortScale.

Buttons do not need canvas

Creating a widget blueprint that is just a button does not need a canvas panel. Setting it to “Desired On Screen” instead of “FillScreen” is the way to go for buttons.

Edit multiple things at the same time

You can select multiple things and edit their appereance at the same time.

Instantiate Widgets by adding them to another widget

We can instantiate widgets by adding them to another widget instead of the viewport.

Let’s suppose we have a vertical box inside a widget and we want to add a widget inside this vertical box.

We can call VerticalBox->AddChild and remove it with VerticalBox->ClearChildren.

Materials

Materials are just shaders that are programmed using unreal’s nodes instead of glsl/hlsl. Materials are the blueprint/classes and material instances are the instances of materials.

Such as with glsl uniform variables, you can set input and parameters to a material.

Material Instances

Material instances cannot add/edit behaviours. Only variables exposed by a material are editable.

Material Functions

You can create material functions. These functions are able to be reutilized inside other materials.

Material functions can have Input, search for the input node.

When creating a material function, be sure to check the “Expose to library” checkbox. If you don’t do this, the function won’t appear in the material function list.

Material Functions can have multiple outputs!

Builtin Functions

There are a lot of complex functions already implemented for you in the material editor. I recommend having a look at them before implementing something inside a material, there’s a chance that what you want is already implemented and the solution might be to add a single node.

Debugging/Visualizing Scalar Values

One way of visualizing scalar values is to use the node “DebugScalarValues”. It prints a scalar value onto the mesh active in the material. It’s a good way of debugging values inside a shader/material.

Using Materials in UMG

In order to use a material in UMG/User Interfaces, we need to set the Material Domain to “User Interface”

Sound

Sound Cues

Sound Cues are the programmable audio assets. As with all thing in unreal you can program the sound using it’s specific nodes. Maybe add a reverb or something else…

Anim Blueprints

The event graph run every frame! To be more specific, the “Event Blueprint Update Animation” runs every frame.

The event graph inside an animation blueprint is able to set variables which are to be used inside the animation graphs to set the correct pose.

AI

Debugging Tips

Press the apostrophe key (’) to open the in-game AI Debugger. There’s a lot of information there. Press the key while looking at an AI to show the info about it’s ai instance

Debugging Tip -> We can use the “visual logger” to record gameplay and then see what was happening at different points of the playthrough. It can record a lot of events, suchs as EQS, BT’s, Pathfinding and more. It’s also good to note that the visual logger has a lot more features than AI debugging tools.

Behaviour Trees

To get the behaviour tree from C++, call the function:

1
AIController->GetBrainComponent();

Navmesh: The navmesh provides data on where the AI is allowed to move.

The navmesh might create valid sections inside big meshes. This might generate bugs, such as EQS generating points inside meshes. To fix this, create “Nav Modifier Volumes” and place them to delete the invalid navmesh sections.

AIController

When creating an AIController in C++ it still needs a blueprint that inherits from it.

Blackboard Component

You can get the blackboard component in AIController with the function GetBlackboardComponent

To change a value in a blackboard component, use the function:

1
BlackboardComponent->SetValueAs...(...)

BTServices

To create BTServices in C++ you need to inherit from “BTService”, if you are creating them from Blueprints, inherit from “BTService_BlueprintBase”.

In order to not hardcode blackboard keys as test in C++, you can create a variable like the following and set the key on the editor.

1
2
UPROPERTY(EditAnywhere)
FBlackboardKeySelector MyKey;

To run a Service each frame, add the frame to the rootnode of the Behavior Tree.

BTTasks

To create a BTTask node in C++, inherit ftrom BTTaskNode. To create one in blueprints, inherit from BTTask_BlueprintBase.

Marking values with UPROPERTY specifiers, make these variables editable inside the behaviour tree when selecting the corresponding node.

Decorators

Decorators have a vew flow control capabilities inside the details window of a decorator. Let’s say we have a decorator that depends on a bool. To instantly stop execution on that subtree when this bool changes, set NotifyObserver to: “OnResultChange” and ObserverAborts to: Self.

If you click on a decorator, unreal engine will apply color to different nodes. For example, unreal will color light blue those nodes that are aborted by low priority mode. Green those that are aborted by mode Self. Unreal always colors the nodes that are affected from what we have currently selected.

EQS

Running an EQS Query from C++

To run an EQS Query from C++, call:

1
UEnvQueryManager::RunEQSQuery(...);

EQS Queries might take multiple frames to complete. Remember to bind to “GetOnQueryFinishedEvent” to handle the query results.

EQS From BT's

When running an EQS from a BT, remember to set the key for the output value. For example, if running a query that will return a location, remember to set the output location key.

EQS Scoring Factor

Setting the scoring factor to a negative number makes the test weigh more in score.

EQS Querier

The “Querier” is the class that is running the query. If we run it from a BT, the querier is the AI running that BT. Queries can also be called from other classes such as game mode.

Running a Query from a non AI actor

We can run the query from another “center”, meaning a non existant EnvQueryContext. To do this we need to create a QueryContesxt. It’s a blueprint or C++ class that inherits from EnvQueryContext_BlueprintBase or the C++ one. Then, override one of the four functions and provide the possible actors/locations from which the query can be made.

Debugging Tips

You can create a special actor to visualize EQS queries in the editor. First create a blueprint that inherits from “EQSTestingPawn”, drop it in the world. Set the EQS query you want to visualize and, move the actor around to try and “refresh” it. If it all went well you should see the differentes EQS spheres and their results.

In the AI Visual debugger (’) key, press numpad 3 to enable EQS visualization for the selected AI. Blue means it failed, green succeeded. Each sphere is a queried location/point.

EQS Gotcha's

If the navmesh is not carefully created and cleaned, EQS might generate points on invalid areas, such as inside big meshes.

Senses

AI’s can have what unreal calls “senses”, such as seeing, listening for sounds and more. In the course we mainly used the “PawnSensingComponent” but there’s a new API called “AI Perception”. This new API should be used.

In the details pane of the “PawnSensingComponent” you can edit some values for the senses. Such as peripheral vision angle, which is a cone that represents the AI vision.

Curves

Unreal has an easy way to create curves for various purposes. To create a curve open the right click menu, and under “Misc” select and create a Curve. Set a few points to create the desired curve. You can access the curve from C++ by creating a variable:

1
UCurveFloat *MyCurve;

Commands and Cheats

Exec

Marking a function as Exec allows it to be called as a command in the game console.

This will work automatically as long as the function is in:

  • PlayerController
  • Character you are playing with
  • GameMode
  • CheatManager

Cheat Manager

Derive a class from UCheatManager and you can add the cheats there.

Stat Commands

There a lot of stat commands, type “stat” in the console to see a list of them.

Some useful stat commands:

  • stat unit
  • stat graph
  • stat uobjects (Shows number of instances of objects. Useful to find memory leaks / objects that are not being destroyed).

You can also create your own stat commands.

Tom Looman has a nice article explaining how to do custom stat traces & commands.

Savegame System

Location of saved game files

Save files are stored in: “ProjectName/Saved/SaveGames”

Overview of SaveGame implemented in the course

In the course we started by creating a new class that inherited from SaveGame.

Then we created a new variable and marked it with UPROPERTY so unreal can find it later.

We created two functions inside GameMode

  1. WriteSaveGame();
  2. LoadSaveGame();
Loading SaveGame

In GameMode we called our LoadGame function inside StartPlay.

SaveGame UObject

It’s a container to store variables like “Earned Credits”, unlocked abilities, player level, etc…

Saving on C++

This will be an example of saving and loading a single variable called “Credits”

  1. Create SaveGame UObject Instance
  2. Copy Creditos from PlayerState into SaveGame
  3. Call UGameplayStatics::SaveGameToSlot();
  4. Done!

Loading on C++

  1. Check if SaveGame file exists
  2. UGameplayStatics::LoadGameToSlot();
  3. Copy Loaded credits of SaveGame into PlayerState
  4. Done!

SaveGame Functions in GameplayStatics

1
2
3
4
SaveGameToSlot();
LoadGameFromSlot();
DoesSaveGameExist();
CreateSaveGameObject();

Saving/Loading Actor State

  1. Mark desired variables to save with the SaveGame UPROPERTY
Saving
  1. Iterate through all actors in the world.
  2. Find relevant actors based on Interface, Class, Tags, etc..
  3. Actor->Serialize(EmptyArchive), this will convert all variables marked with SaveGame into an FArchive(Binary)
  4. Add resulting binary data containing serialized Actor Data into a SaveGame UObject
Loading
  1. Get binary data from SaveGame object.
  2. Iterate Actors in world.
  3. Find matching actors by name from available SaveGame Data.
  4. Actor->Serialize(FilledArchive) This will convert binary data into actor’s variables.

Saving/Loading moved actors

Saving
  1. Iterate over actors in the world.
  2. Find relevant actors, maybe by if they implement an interface, the type of class, if they have some tags. This is your choice.
  3. Create a struct to hold the actor’s Name and Transform. If the actors were moved we need to save their new location.
  4. Add struct into SaveGame Object
Loading
  1. Load Actor data (the array with names and transforms).
  2. Iterate actors in the world.
  3. Find matching Actors by Name in our SaveGame data.
  4. Move each Actor to it’s saved Location. Actor->SetActorTransform(LoadedTransform).

Serialize Save Data

In order to serialize and store an actor’s variables marked as SaveGame. We need to create an array of bytes in SaveGame where the binary data will be stored.

Serializing and storing API is not particularly clear, you need to do something that looks like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Pass the array to fill
FMemoryWriter(ActorData.ByteArray);

FObjectAndNameAsStringProxyArchive Archive(MemWriter, true);

// Find only variables with UPROPERTY(SaveGame)
Archive.ArIsSaveGame = true;

// Converts actor savegame properties into Binary Array
Actor->Serialize(Archive);

Load Serialized Actor Data

It’s similar to save serialized data, but we use a memory reader instead of a writer.

1
2
3
4
5
6
FMemoryReader MemReader(ActorData.ByteArray);

// This will load saved variables onto Actor
FObjectAndNameAsStringProxyArchive Ar(MemReader, true);
Ar.ArIsSaveGame = true;
Actor->Serialize(Ar);

After loading variables be sure to run the procedures that put all the remaining state back.

Example: If the actor has a bIsOpen variable, remember to update it’s state. Set the door open.

It might be a good idea to create a Save/Load interface for actors. Call OnSave when saving, OnLoad when loading.

Saving Player Settings

Player settings should not be saved in SaveGame class, there is a separate class for this!

Data Tables

Data tables are basically a spreadsheet with data. It can be created and filled inside the editor or it can be populated with a CSV import.

They cannot be modified at runtime

The can be accesed in C++ by UDataTable*

Datatables in C++

In order to use data tables in C++, we also need to create a struct that represents a single data table row with all of its variables.

This struct must inherit from FTableRowBase.

Later, we can create a datatable in the editor and make it of the type struct we defined.

Data Assets

Data assets and data tables are a good to store separate data from code.

We can create a data asset for each monster and hold all of it’s variables there, like health and anything else.

Later, when we want to edit one of those values, it’s all contained there and we won’t need to go through code to change things.

Data Asset in C++

Create a C++ class that inherits from PrimaryDataAsset. Inheriting from PrimaryDataAsset will let us use the AssetManager with this data asset.

Soft and Hard References

Hard References

Hard references are always loaded synchronously when they are referenced by another object. This means that when trying to load a single “stone” actor we might load everything instead.

This can quickly make the game slow to load, and make the development experience also slow.

Examples of hard references:

  • TSubClassOf
  • Pointers to assets
  • Casting to certain classes are also hard references

Soft References

Soft references are not loaded automatically, they need to be loaded manually.

  • They require management of loading in code.
  • They load in the background and avoid game freezes.
  • They reduce load screens and memory use by loading data on the fly.

Tools to see reference status in your project

Size Map - This tool shows all the hard references of an object/actor. It will let you quickly see if that stone is in fact loading everything because of hard references.

Reference Viewer - View asset references, oftean reveal unnecessary Cast or references to other content.

Soft references in C++

To turn a UTexture2D hard reference into a soft ptr, do:

1
2
3
4
5
// Replace
UTexture2D *Img;

// With this
TSoftObjectPtr<UTexture2D> Img;

In UE5+, when converting a C++ hard reference to soft. When we compile, the engine will add a new node where the hard reference was used. This new node automatically returns null if the object is not already loaded in memory. It will not load it. We still have to manually load it. Be sure to check and correct everything when changin from hard to soft.

Remember, you will need to manually load this asset in code.

Loading soft references in blueprints

Use the node “Async Load Asset”.

Soft references tips

Don’t make everything a soft reference. It will become hell to manage.

Make smart and tactical decisions of which objects should be soft pointers.

Data Assets and Data Tables are a good example.

They are just data containers, but they are also hard referencing things. What looks like a small data asset might really be a lot of Megabytes when loaded into memory.

Soft pointers in C++

1
2
3
4
5
// Used for references of content such as UTexture2D, UParticleSystem, etc..
TSoftObjectPtr<T> MyObjectPtr;

// Used to hold classes
TSoftClassPtr<T> MyClassPtr;

Async Loading of individual assets

FStreamableManager (C++) or Blueprint nodes such as AsyncLoadAsset. These two load base content such as textures, particle systems, etc…

Asset Manager

The Asset manager is mainly used to load Data Assets. In order to load those data assets you need to provide an FPrimaryAssetId. It will use this Id to identify what to load.

The asset manager needs to be configured in the project settings. Inside the “Game->AssetManager” we need to add a new “Primary Asset Types to Scan”.

Example: Primary Asset Type: “Monsters”. Asset Base Class: “SMonsterData” (This a data asset type). Directories: Add directory where to search

Loads PrimaryDataAsset asynchronously by providing FPrimaryAssetID of an asset.

Loads a DataAsset that can contain a bunch of different content to load all at once.It’s a wrapper around FStreamableManager, it’s specific to loading/unloading DataAssets.

To find a Data Asset with the asset manager, we need to override a function in our data asset C++ implementation.

Example:

1
2
3
4
FPrimaryAssetId GetPrimaryAssetId() const override
{
    return FPrimaryAssetId("Monsters", GetName());
}

Now, instead of a pointer to a data asset, we change it to a FPrimaryId, and use this id to load it.

1
2
3
4
5
// Change this
USMonsterData *MyDataAsset;

// To this
FPrimaryAssetId MyDataAssetId;
Accessing the asset manager from C++
1
UAssetManager *Manager = UAssetManager::GetIfInitialized();
Loading an asset
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
UAssetManager *Manager = UAssetManager::GetIfInitialized();

// The delegate and the function it triggers can have parameters if you need them
FStreamableDelegate Delegate = FStreamableDelegate::CreateUObject(This, MyFunction);

TArray<FName> Bundles;

Manager->LoadPrimaryDataAsset(AssetId, Bundles, Delegate);

// The Delegate function will be triggered once the action has been loaded.
Getting a reference to the actual data

Once the asset has finished loading and the delegate function is triggered, we can now get a pointer to the data once again using the asset manager.

1
UMyData *MyData = Cast<UMyData>(Manager->GetPrimaryAssetObject(MyId));
Asset Manager Bundles

Bundles are a way to select what to load inside a data asset. If we only need one section of the data asset, we can load that part by using bundles.

FPrimaryAssetId

The id is basically an FName and we can set variables to be of this type.

Profiling

Always cook before profiling. Packaging and cooking may resutl in better performance.

Unreal Insights

You can add “Bookmarks to your code and make the section of the code you marked, appear in insights by name”.

Cooking/Packaging

Add levels to be packaged

In order to package the project correctly, we need to add the specific levels we want to package. By adding these levels everything reference by them will be automatically cooked and packaged.

Go to Packaging settings and add the desired levels to: “List of maps to include in a packaged build”.

Something not working in a packaged build?

Maybe some things related to the asset manager might not be working in the packaged build. Try settings the cooking rule for that type of asset to “Always Cook”.

“Game->AssetManager->Primary Type Asset To Scan->Rules->CookRule”.

Performance Tips

Avoid Ticking of Actors and Components

Most logic can be done through events or timers with lower frequency than ticks.

comments powered by Disqus