5.1 Unreal Core & the Actor Model

Phase 5 · Unreal Engine (alternative) · Study time: 30–50 h

How Unreal structures a game with Actors, Components and the Gameplay Framework, and its reflection / UObject system.

Every chapter so far used Unity and C#. This chapter switches engines. Unreal Engine is a different game engine made by Epic Games, and it thinks about a running game differently from Unity. The good news: you already know what a game engine is — a program that gives you a 3D world, a renderer, physics, input, and an editor, then calls your code every frame. What changes is the vocabulary and the building blocks. This chapter covers Unreal's core building block, the Actor, the reflection system that makes Unreal's editor and Blueprint tools work, and the small set of framework classes (GameMode, Pawn, Character, PlayerController) that every Unreal project is built from.

We will keep contrasting with Unity the whole way, because that contrast is the fastest way to learn a second engine: you are not learning "what is a game engine" from zero, you are learning "how does this engine's version of the thing I already know work."

1. What Is Unreal Engine, And Who Uses It

Unreal Engine is a general-purpose game engine, in active development since 1998, currently on its fifth major version (UE5). Like Unity, it ships an editor, a renderer, physics, animation tools, audio, networking, and a way to package a build for PC, console, or mobile. Two things separate it from Unity in practice:

Unreal is known for high-end visual fidelity and is the default choice for a lot of AAA console and PC development. Studios and games built on it include Epic Games' own Fortnite and the Gears of War series, Final Fantasy VII Remake/Rebirth (Square Enix), the Star Wars Jedi series (Respawn), Tekken 8 (Bandai Namco), Black Myth: Wukong (Game Science), and CD Projekt Red's upcoming Witcher games, which moved from their own engine to UE5. Plenty of mobile and live-service studios — including some that ship on Unity, like the earlier chapters' reference point HoYoverse — mix in Unreal for specific titles or evaluate it for projects that need Unreal's rendering quality. Knowing both engines is a normal, valuable thing on a game programmer's resume; they solve the same problems with different opinions about how code should be organized.

Tip Unreal's source code is available to read (free, with an Epic account) on GitHub. When you are unsure what a function actually does, you can open the engine source itself — something Unity's closed engine does not offer in the same way.

2. Actors And Components vs Unity's GameObjects

In Unity, the object you place in a scene is a GameObject — an empty container. It does nothing by itself; every capability, including the Transform (position/rotation/scale) itself, comes from attached Components, and a MonoBehaviour script is just one more component type.

In Unreal, the object you place in a level is an Actor (its C++ base class is AActor). An Actor is "a thing that can exist in the world" — it already comes with a position/rotation/scale and a set of lifecycle calls (which we cover in section 7) built into the base class itself. An Actor can also own Components (its C++ base class is UActorComponent), which are objects attached to the Actor that add a specific capability: a mesh to render, a camera, a collision shape, a movement rule. This is close in spirit to Unity's GameObject + Component model, but the split is different:

Unity object "Player" GameObject | +-- Transform (component) +-- MeshRenderer (component) +-- CapsuleCollider (component) +-- PlayerScript : MonoBehaviour (component; your gameplay code) Unreal object "AMyCharacter" AMyCharacter (an AActor subclass -- your gameplay code can live directly here, in C++ methods, not only in components) | +-- RootComponent (a USceneComponent; holds the transform) +-- CapsuleComponent (collision) +-- SkeletalMeshComponent (the mesh) +-- CharacterMovementComponent (movement rules)

Two differences worth naming clearly:

Tip Both engines let you go either way if you want to — Unity supports composition-heavy ECS-style code, and Unreal supports building lots of small ActorComponents. What matters for this chapter is what each engine's defaults and built-in classes nudge you toward.

3. UObject: The Base Everything Sits On

Almost every engine class you touch in Unreal — AActor, UActorComponent, even non-Actor helper objects — ultimately derives from one root class: UObject. AActor is a UObject (through a chain of base classes), which is the entire reason Actors get three things for free, none of which exist in plain C++:

UObject | +-- AActor | | | +-- APawn | | | | | +-- ACharacter | | | +-- AGameModeBase | +-- APlayerController | +-- (your own actors: AHealthActor, APickupActor, ...) | +-- UActorComponent | +-- USceneComponent (adds a transform) | +-- UStaticMeshComponent +-- UCameraComponent +-- USpringArmComponent

Reflection is not magic — it is generated code. Unreal's build step runs a tool called the Unreal Header Tool (UHT) before the real C++ compiler does. UHT scans your header files for a specific set of macros (covered next), and for every class it finds, it writes an extra C++ file full of boilerplate — the class's .generated.h file — that plugs your class into the reflection system. This is why every Unreal class header ends with an include like #include "MyActor.generated.h", and why that include must always be the last include in the file: UHT needs to have seen everything above it first.

Common mistake Putting #include "MyActor.generated.h" anywhere but last, or forgetting it entirely. Both are compile errors in a real Unreal project. The generated file always comes last because it depends on everything declared above it in the same header.

4. Reflection Macros: UCLASS, UPROPERTY, UFUNCTION

Unity gets reflection "for free" from the .NET runtime — a C# field marked with the attribute [SerializeField] is discoverable at runtime through .NET's built-in System.Reflection, no extra build step required. C++ has no such runtime reflection, so Unreal opts individual classes, properties, and functions into its own hand-built reflection system using macros that UHT reads. There are three you will use constantly:

Here is a small, complete Actor that uses all three. This is the C++ equivalent of a Unity script with [SerializeField] private float health = 100f; and a public method.

// HealthActor.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "HealthActor.generated.h"     // must be the LAST include

UCLASS()
class MYGAME_API AHealthActor : public AActor
{
    GENERATED_BODY()   // must be the first line in the class body

public:
    AHealthActor();

    // Shows up as an editable slider in the Details panel, grouped
    // under "Health". Blueprint graphs can read AND write it too.
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Health")
    float Health = 100.0f;

    // Blueprint graphs can call this like a node. C++ callers can
    // call it directly, same as any normal member function.
    UFUNCTION(BlueprintCallable, Category = "Health")
    void TakeDamage(float Amount);

protected:
    virtual void BeginPlay() override;
};
// HealthActor.cpp
#include "HealthActor.h"

AHealthActor::AHealthActor()
{
    PrimaryActorTick.bCanEverTick = false;  // this actor never needs per-frame work
}

void AHealthActor::BeginPlay()
{
    Super::BeginPlay();
    UE_LOG(LogTemp, Warning, TEXT("HealthActor spawned with %f HP"), Health);
}

void AHealthActor::TakeDamage(float Amount)
{
    Health -= Amount;
    UE_LOG(LogTemp, Warning, TEXT("Took %f damage, Health is now %f"), Amount, Health);
}

UE_LOG is Unreal's version of std::cout or Unity's Debug.Log — it writes a line to the editor's Output Log window. TEXT("...") wraps a string literal so it matches Unreal's string type on every platform; you will see it around almost every string literal in Unreal C++. If you drop this Actor into a level and press Play, the Output Log shows this trace:

LogTemp: Warning: HealthActor spawned with 100.000000 HP

Then call TakeDamage(25.0f) from anywhere with a pointer to this actor (another Actor's code, or a Blueprint node), and the log gains a second line:

LogTemp: Warning: Took 25.000000 damage, Health is now 75.000000

And because Health is EditAnywhere, selecting this actor in the level opens a Details panel that looks roughly like this — no extra code needed to get an editable slider:

Details panel (Unreal Editor) -- AHealthActor selected in the level +----------------------------------------+ | Health | | Health [ 100.0 ] | +----------------------------------------+

To put the three benefits side by side: UPROPERTY's EditAnywhere is editor exposure (a designer can tune Health per-instance without touching code), the reflection data behind every UPROPERTY is what makes serialization work (that 100.0 gets saved into the level file, and can be saved into a save-game slot), and BlueprintReadWrite/BlueprintCallable are what give you Blueprint access (a designer's Blueprint graph can read Health or call TakeDamage without seeing a line of C++). One pair of macros, three payoffs.

5. AActor: A Thing You Place In The World

Now that reflection makes sense, back to the Actor itself. AActor is the base class for anything that can exist in a level: a character, a light, a pickup, an invisible trigger volume, even the camera. Every Actor carries, from the base class alone, before you add anything:

Notice the class name starts with A. Unreal uses a small set of one-letter prefixes so you can tell a type's category from its name alone: A for Actor subclasses, U for other UObject-derived classes (including all Components), F for plain C++ structs (like FVector, FString), E for enums, and I for interfaces. Unity has no equivalent convention — a Unity MonoBehaviour and a plain C# class both look like ordinary PascalCase names with no prefix.

Here is the smallest complete Actor you can write — no components, no properties, just the shell:

// MyActor.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"

UCLASS()
class MYGAME_API AMyActor : public AActor
{
    GENERATED_BODY()

public:
    AMyActor();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;
};
// MyActor.cpp
#include "MyActor.h"

AMyActor::AMyActor()
{
    PrimaryActorTick.bCanEverTick = true;   // allow Tick() to run (see section 7)
}

void AMyActor::BeginPlay()
{
    Super::BeginPlay();
}

void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

Drop this into a level and press Play: nothing visible happens, because it has no mesh and does no work yet, but it does exist — it has a location, it received BeginPlay once, and it is receiving Tick every frame. That "empty but alive" object is the equivalent of an empty Unity GameObject that already has a script attached with an empty Start() and Update().

6. Components: Attaching Data And Behavior To An Actor

An Actor on its own has a transform and lifecycle calls, but nothing to render and no collision. You add those capabilities by attaching Components — objects (deriving from UActorComponent) that plug into the Actor and do one job each. The three you will meet immediately:

In Unity you usually attach components in the editor by dragging them onto a GameObject, or at runtime with gameObject.AddComponent<T>(). In Unreal C++, the standard place to create an Actor's default components is inside its constructor, using CreateDefaultSubobject<T>(TEXT("Name")). This runs once when the class is first set up (technically, when its Class Default Object is built) and produces the components every instance of this Actor starts with — closer to configuring a Unity prefab than to calling AddComponent at runtime.

// PickupActor.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PickupActor.generated.h"

UCLASS()
class MYGAME_API APickupActor : public AActor
{
    GENERATED_BODY()

public:
    APickupActor();

    UPROPERTY(VisibleAnywhere, Category = "Components")
    USceneComponent* Root;

    UPROPERTY(VisibleAnywhere, Category = "Components")
    UStaticMeshComponent* Mesh;
};
// PickupActor.cpp
#include "PickupActor.h"

APickupActor::APickupActor()
{
    PrimaryActorTick.bCanEverTick = false;

    // Create a root so the actor has a transform to attach things to.
    Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
    SetRootComponent(Root);

    // Create the mesh and attach it under the root.
    Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
    Mesh->SetupAttachment(Root);
}

The component tree this builds looks like this once the object exists in the world:

APickupActor instance | +-- Root (USceneComponent) <- the RootComponent, holds the transform | +-- Mesh (UStaticMeshComponent) <- attached under Root, renders the mesh

Every UPROPERTY component pointer here is also VisibleAnywhere, so selecting a placed APickupActor shows both components listed in the Details panel — you (or a designer) can then assign an actual mesh asset to Mesh without writing more code.

Tip You do not have to write C++ to add components at all. In the Unreal Editor you can open a Blueprint class and add components visually, the same way you would drag components onto a Unity prefab. C++ is for components every instance should start with, or logic too heavy or too performance-sensitive for Blueprint; Blueprint is for per-level tweaks and designer-owned setup on top of that C++ base.

7. The Actor Lifecycle: Constructor, BeginPlay, Tick, EndPlay

Every Actor goes through the same sequence of calls. If you already know Unity's Awake/Start/Update/OnDestroy, this will feel familiar with different names and one important trap.

AMyActor() constructor: runs when the object is FIRST | constructed -- including in the EDITOR, not | only when the game is playing. Create | components here. Avoid gameplay logic here. v BeginPlay() runs ONCE, only when the actor exists in a | RUNNING world (editor Play, or a real game). | Safe to look up other actors, start timers, | the world is guaranteed to be ready. v Tick(DeltaTime) runs every frame, only if ticking is enabled | (repeats) (PrimaryActorTick.bCanEverTick = true). | DeltaTime is the seconds since last frame -- | same idea as Unity's Time.deltaTime. v EndPlay(Reason) runs once when the actor is removed from a running world: level unload, Destroy() call, or the game/editor session ending.

A worked example that touches three of these together, tracking how long the actor has been alive:

// TimedActor.h  (declarations only -- assume the usual .generated.h setup)
UCLASS()
class MYGAME_API ATimedActor : public AActor
{
    GENERATED_BODY()
public:
    ATimedActor();
protected:
    virtual void BeginPlay() override;
public:
    virtual void Tick(float DeltaTime) override;
    virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

    UPROPERTY(VisibleAnywhere, Category = "Timer")
    float TimeAlive = 0.0f;
};
// TimedActor.cpp
#include "TimedActor.h"

ATimedActor::ATimedActor()
{
    PrimaryActorTick.bCanEverTick = true;
}

void ATimedActor::BeginPlay()
{
    Super::BeginPlay();
    UE_LOG(LogTemp, Warning, TEXT("TimedActor: BeginPlay, TimeAlive = %f"), TimeAlive);
}

void ATimedActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    TimeAlive += DeltaTime;
    if (TimeAlive > 5.0f)
    {
        UE_LOG(LogTemp, Warning, TEXT("TimedActor: been alive over 5 seconds"));
    }
}

void ATimedActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    UE_LOG(LogTemp, Warning, TEXT("TimedActor: EndPlay, TimeAlive = %f"), TimeAlive);
    Super::EndPlay(EndPlayReason);
}

Trace it: at frame 0, BeginPlay fires once, printing TimeAlive = 0.000000. Every following frame, Tick adds that frame's DeltaTime (a fraction of a second) to TimeAlive. Once the running total crosses 5.0, the "been alive over 5 seconds" line starts printing every single frame from then on (this code has no flag to print it only once — that is left for exercise 1). If you stop Play in the editor, EndPlay fires once with whatever TimeAlive reached.

Common mistake Putting gameplay logic — spawning other actors, reading player state, calling GetWorld() — in the constructor instead of BeginPlay(). The constructor can run for editor-only preview objects where there is no running world at all, so world-dependent code there can crash or silently do nothing. Constructor: build components and set defaults. BeginPlay: everything that needs a live game world.
Tip Always call Super::BeginPlay(), Super::Tick(DeltaTime), and Super::EndPlay(...) inside your overrides, the same reflex as calling base.Start() in a Unity script that overrides a base class method. The base class's own logic often matters — skip it and things break in ways that are hard to trace.

8. The Gameplay Framework: How The Pieces Fit Together

Unity gives you a blank scene and no opinion about how a "player," a "match," or "input" should be structured — you build a GameManager singleton and a player controller script yourself, from scratch, on every project. Unreal ships an opinionated starter structure for exactly that, called the Gameplay Framework: a small set of built-in Actor classes whose jobs are already divided up, and which already know how to talk to each other. This chapter focuses on four of them:

Here is how a single-player session wires them together the moment Play starts:

+--------------+ | GameMode | decides: which Pawn class, which +--------------+ PlayerController class to use | | spawns spawns | | v v +-----------------+ +----------------+ |PlayerController | | Pawn/Character | +-----------------+ +----------------+ | ^ | possesses | +--------------------+ | owns (has many) | v +----------------+ | Components | | Mesh, Camera, | | Capsule, etc. | +----------------+

Read the diagram as a sequence: the GameMode spawns a PlayerController and a Pawn (using whichever classes it was told to use), the PlayerController then possesses the Pawn (meaning: input from this point on is routed into that Pawn), and the Pawn, like any Actor, owns whatever Components it needs to look and behave like something. In bigger, especially multiplayer, projects you will also meet AGameState (shared match data visible to everyone, like the score) and APlayerState (per-player data, like one player's score) — they sit alongside this diagram but are out of scope for this chapter.

9. GameMode: The Rules Of The Match

Unity has no built-in equivalent of GameMode — every Unity project reinvents its own "who spawns first, what class is the player" logic, usually as a hand-rolled singleton. Unreal formalizes that one job into a class: AGameModeBase exists specifically to answer "what are the rules of this session," and the most common rule it answers is which classes to spawn.

Set per-level (or as a project-wide default), a GameMode subclass typically just fills in a few class references in its constructor:

// MyGameMode.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "MyGameMode.generated.h"

UCLASS()
class MYGAME_API AMyGameMode : public AGameModeBase
{
    GENERATED_BODY()
public:
    AMyGameMode();
};
// MyGameMode.cpp
#include "MyGameMode.h"
#include "MyCharacter.h"
#include "MyPlayerController.h"

AMyGameMode::AMyGameMode()
{
    DefaultPawnClass = AMyCharacter::StaticClass();
    PlayerControllerClass = AMyPlayerController::StaticClass();
}

That is the entire class: two assignments. StaticClass() returns a handle to the class itself (its reflection data), not an instance — Unreal needs the class here because it is going to SpawnActor from it later, once per player. When Play starts, the engine looks at which GameMode the current level uses, reads DefaultPawnClass and PlayerControllerClass off it, and spawns one of each — that is the top box in section 8's diagram doing its job.

If you want a designer to be able to pick the Pawn class from the editor instead of hard-coding it in C++, expose it as a UPROPERTY using TSubclassOf — a type-safe "pointer to a class" (not an instance) that the Details panel can show as a dropdown of matching classes:

UPROPERTY(EditDefaultsOnly, Category = "Classes")
TSubclassOf<APawn> DefaultPawnClassOverride;

TSubclassOf<APawn> reads as "a class that is APawn or a subclass of it" — the compiler and the editor both enforce that, so you cannot accidentally assign a class that has nothing to do with Pawns.

10. Pawn And Character: What Gets Controlled

APawn is the base class for anything in the world that a Controller — human or AI — can possess and drive. Not every Actor is a Pawn: a door, a static prop, a trigger volume are Actors but never Pawns, because nothing is meant to "drive" them. A Pawn is specifically the kind of Actor built to receive control input.

ACharacter is a subclass of APawn, purpose-built for a walking, bipedal character. It adds two things out of the box that a plain APawn does not have:

A minimal playable Character, including a camera on a spring arm (a component that keeps the camera at a fixed distance behind the character and pushes it closer if something blocks the view — Unreal's equivalent of writing your own third-person camera-collision code in Unity):

// MyCharacter.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MyCharacter.generated.h"

class USpringArmComponent;
class UCameraComponent;
class UInputComponent;

UCLASS()
class MYGAME_API AMyCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    AMyCharacter();

protected:
    virtual void BeginPlay() override;
    virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;

    void MoveForward(float Value);
    void MoveRight(float Value);

    UPROPERTY(VisibleAnywhere, Category = "Camera")
    USpringArmComponent* SpringArm;

    UPROPERTY(VisibleAnywhere, Category = "Camera")
    UCameraComponent* Camera;
};
// MyCharacter.cpp
#include "MyCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/InputComponent.h"

AMyCharacter::AMyCharacter()
{
    // RootComponent, CapsuleComponent, and the movement component
    // already exist -- ACharacter's own constructor made them.
    SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
    SpringArm->SetupAttachment(RootComponent);
    SpringArm->TargetArmLength = 400.0f;

    Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
    Camera->SetupAttachment(SpringArm);
}

void AMyCharacter::BeginPlay()
{
    Super::BeginPlay();
}

void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);
    PlayerInputComponent->BindAxis("MoveForward", this, &AMyCharacter::MoveForward);
    PlayerInputComponent->BindAxis("MoveRight", this, &AMyCharacter::MoveRight);
}

void AMyCharacter::MoveForward(float Value)
{
    if (Controller && Value != 0.0f)
    {
        AddMovementInput(GetActorForwardVector(), Value);
    }
}

void AMyCharacter::MoveRight(float Value)
{
    if (Controller && Value != 0.0f)
    {
        AddMovementInput(GetActorRightVector(), Value);
    }
}

SetupPlayerInputComponent is where a Pawn/Character binds raw input axes to its own functions — this specific example uses the classic Axis Mapping system (set up in Project Settings), the simplest way to see the idea; newer Unreal versions favor a more flexible system called Enhanced Input, which is worth learning once these basics are solid. Notice the guard if (Controller && ...)Controller is a pointer this Pawn only has while something possesses it, covered next.

Common mistake Forgetting Super::SetupPlayerInputComponent(PlayerInputComponent); at the top of an override. If your class ever derives further, or the base class binds its own input, skipping the Super call silently drops that behavior — same trap as forgetting Super::BeginPlay().

11. PlayerController: The Bridge Between Input And Pawn

In Unity there is usually no separate object for "the player's connection" — you read Input.GetAxis(...) directly inside whatever script sits on the player GameObject, and if that GameObject is destroyed, your input-handling code goes with it. Unreal splits this on purpose: APlayerController represents one human player's control connection, and it is not a physical thing in the level — no mesh, usually no visible presence at all. Its whole job is: read hardware input, and forward the resulting commands into whichever Pawn it currently possesses.

The separation matters for one concrete reason: the Controller persists across Pawn changes. If your Character dies and a new one gets spawned to respawn the player, the same PlayerController can simply possess the new Pawn — the player's connection, camera preferences, and any UI state tied to the controller survive, only the Pawn (the physical body in the world) is replaced.

// MyPlayerController.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MyPlayerController.generated.h"

UCLASS()
class MYGAME_API AMyPlayerController : public APlayerController
{
    GENERATED_BODY()
protected:
    virtual void BeginPlay() override;
    virtual void OnPossess(APawn* InPawn) override;
};
// MyPlayerController.cpp
#include "MyPlayerController.h"
#include "GameFramework/Pawn.h"

void AMyPlayerController::BeginPlay()
{
    Super::BeginPlay();
    UE_LOG(LogTemp, Warning, TEXT("PlayerController: BeginPlay"));
}

void AMyPlayerController::OnPossess(APawn* InPawn)
{
    Super::OnPossess(InPawn);
    UE_LOG(LogTemp, Warning, TEXT("PlayerController: now possessing %s"), *InPawn->GetName());
}

OnPossess fires on the Controller the moment it takes control of a Pawn — this is the "possesses" arrow from section 8's diagram, made concrete. *InPawn->GetName() dereferences the FString that GetName() returns so %s can print it. If InPawn is your AMyCharacter from section 10, placed as the level's default player start, the trace reads:

LogTemp: Warning: PlayerController: BeginPlay
LogTemp: Warning: PlayerController: now possessing MyCharacter_C_0

12. Tracing A Play Session Start To Finish

Section 8 showed the static diagram; here is the same picture as an event trace, so you can see the order things actually happen in when you press Play in the editor. Assume: AMyGameMode from section 9, using AMyCharacter from section 10 and AMyPlayerController from section 11, and every class logs its own BeginPlay.

1. Level loads. Engine picks the level's GameMode: AMyGameMode. 2. GameMode reads DefaultPawnClass / PlayerControllerClass off itself. 3. GameMode spawns an AMyPlayerController. 4. GameMode spawns an AMyCharacter at a PlayerStart in the level. 5. The PlayerController possesses the Character -- OnPossess() fires. 6. BeginPlay() fires on every actor now in the world, GameMode included. 7. Tick() begins running every frame on every actor that opted in.

Put into the Output Log, one plausible ordering looks like this (the exact order between different actors' BeginPlay calls is not something you should hard-code logic around — only the possession-before-BeginPlay-of-the-pawn relationship and "GameMode exists first" are guaranteed):

LogTemp: Warning: PlayerController: now possessing MyCharacter_C_0
LogTemp: Warning: PlayerController: BeginPlay
LogTemp: Warning: HealthActor spawned with 100.000000 HP

The practical takeaway: by the time any actor's BeginPlay runs, the GameMode has already decided the rules and the PlayerController has already been created — so it is always safe, from inside BeginPlay, to ask "what GameMode is running" or "who is the player's controller." It is not safe to assume that from inside a constructor, which is exactly the pitfall flagged back in section 7.

13. Unreal vs Unity, Side By Side

One reference table, gathering everything this chapter touched on:

Concept | Unity | Unreal ------------------------+----------------------------+---------------------------- Main language | C# | C++ (plus Blueprint, visual) World object base class | GameObject | AActor Holds the transform | Transform component | RootComponent (built in) Where behavior lives | MonoBehaviour component | Actor subclass itself, | | or an ActorComponent Per-frame update | Update() | Tick(float DeltaTime) First-frame setup | Start() | BeginPlay() Cleanup on removal | OnDestroy() | EndPlay(Reason) Editor-exposed field | [SerializeField] private | UPROPERTY(EditAnywhere) Reflection source | .NET runtime (built in) | Unreal Header Tool (UHT), | | driven by macros Player input owner | usually the player's script| APlayerController Match rules / spawning | your own GameManager | AGameModeBase (built in) Instantiate / destroy | Instantiate() / Destroy() | SpawnActor<T>() / Destroy() Naming convention | PascalCase, no prefix | A/U/F/E/I prefix by category

The theme underneath the table: Unity gives you fewer built-in opinions and expects you to build your own conventions (your own GameManager, your own player script pattern) on every project. Unreal gives you more built-in structure (GameMode, Pawn/Character split, PlayerController) and a heavier, macro-driven way of exposing things to its editor and to Blueprint, in exchange for that structure. Neither is strictly better — they are different trade-offs between "flexible, build it yourself" and "opinionated, plug into the framework" — and knowing both means you can read a job posting for either engine and already recognize the shape of the code before you open the project.

14. Glossary

15. Exercises

Exercise 1 Look back at the ATimedActor code in section 7. Its Tick currently logs "been alive over 5 seconds" on every frame once TimeAlive passes 5.0, instead of just once. Add a member variable and a small change to Tick so the message prints exactly once. State the type and UPROPERTY (or plain member, your choice) you added, and show the changed Tick function.
Show answer

Add a boolean flag that starts false and gets set once the message has fired:

UPROPERTY(VisibleAnywhere, Category = "Timer")
bool bHasLoggedFiveSeconds = false;
void ATimedActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    TimeAlive += DeltaTime;
    if (TimeAlive > 5.0f && !bHasLoggedFiveSeconds)
    {
        UE_LOG(LogTemp, Warning, TEXT("TimedActor: been alive over 5 seconds"));
        bHasLoggedFiveSeconds = true;
    }
}

The &&!bHasLoggedFiveSeconds check means the body only ever runs the first time TimeAlive crosses 5.0 — after that, bHasLoggedFiveSeconds is true, so the condition is false on every later frame no matter how large TimeAlive gets. This is the same "print once" pattern you would use in Unity with a bool field checked inside Update().

Exercise 2 A teammate writes this constructor and asks why the game crashes the moment the level loads, before Play is even pressed:
AMyActor::AMyActor()
{
    PrimaryActorTick.bCanEverTick = false;
    AActor* Other = GetWorld()->SpawnActor<AActor>(AActor::StaticClass());
}
Explain what is wrong and rewrite it correctly.
Show answer

The constructor runs even when there is no running game world — for example when the editor builds this Actor's Class Default Object to show it in menus and previews. At that point GetWorld() can return a null pointer (or a world that is not set up for spawning), so calling SpawnActor on it crashes or misbehaves. Section 7's rule applies exactly here: world-dependent code belongs in BeginPlay(), not the constructor.

// MyActor.h  (just the two overrides that matter)
protected:
    virtual void BeginPlay() override;

// MyActor.cpp
AMyActor::AMyActor()
{
    PrimaryActorTick.bCanEverTick = false;
    // no world-dependent calls here
}

void AMyActor::BeginPlay()
{
    Super::BeginPlay();
    AActor* Other = GetWorld()->SpawnActor<AActor>(AActor::StaticClass());
}

By the time BeginPlay runs, the actor is guaranteed to be part of a live, running world, so GetWorld() is always safe to use there.

Exercise 3 Using the framework diagram from section 8 and the traced example from section 12, put these four events in the correct order, and for each one say which class (GameMode, PlayerController, or Character/Pawn) is responsible for making it happen:
  • (a) The Character's BeginPlay() runs.
  • (b) The PlayerController possesses the Character.
  • (c) The GameMode reads DefaultPawnClass and spawns the Character.
  • (d) The GameMode spawns the PlayerController.
Show answer

Correct order: (d), (c), (b), (a).

  • (d) happens first — the GameMode is responsible; it needs a PlayerController to exist before there is anyone to possess a Pawn.
  • (c) happens next — the GameMode is responsible again; it reads its own DefaultPawnClass and spawns the Character into the level, typically at a PlayerStart.
  • (b) happens next — the PlayerController is responsible; once both it and the Character exist, it possesses the Character, and OnPossess fires on the controller.
  • (a) happens last of these four — the Character itself is responsible for its own BeginPlay, which fires once it is part of the running world (this can actually happen close together with possession, but logically the Character must exist, i.e. step (c), before it can either be possessed or receive BeginPlay).

The pattern worth remembering: GameMode only ever creates things (it is the rulebook, not a physical presence), PlayerController is the one that connects input to a Pawn, and every actor's own BeginPlay is purely about that actor's own setup, independent of why it was spawned.

That is the Actor model and the Gameplay Framework in outline. You now know what an Actor and a Component are and how they compare to Unity's GameObject/Component pair, why UCLASS/UPROPERTY/UFUNCTION exist and what each one buys you, the shape of an Actor's lifecycle, and how GameMode, Pawn/Character, and PlayerController divide up the job of starting a play session. The next chapters build directly on this: more of the Gameplay Framework, then Blueprint/C++ interaction in more depth.

← Back to all chapters