An intro. In about two hours you'll write roughly a hundred and twenty lines of C++ and end up with a real, playable game — coins that spin, a countdown, a score, and a win screen. No Blueprints. Not one node.
Play it first. Twenty coins, thirty seconds, one very keen orange square. Then we'll go and write the whole thing in C++ inside Unreal — same rules, same numbers, better graphics.
int32 — a whole number that goes up. Section 03.float counting down every frame. Section 03.for loop. Section 06.TArray. Section 07.if statement asking two questions. Section 05.class — one design, twenty objects. Section 08.That little game is running on about eighty lines of code. The version you're about to build in Unreal is about a hundred and twenty, does the same six things, and has a spinning three-dimensional coin in it instead of a yellow circle.
Unreal gives you two ways to make things happen. You'll spend most of this course in the other one — so it's worth being straight about why we're starting here.
Visual. You drag nodes onto a graph and join them with wires. Fast to build with, hard to break, brilliant for designers, and genuinely how a lot of shipped games are made. This is where you'll be living from next term.
Typed. You write instructions as text and the computer {{compile|compiles}} them into the actual program. Slower to write, faster to run, and — the bit that matters — every single Blueprint node you will ever use is a piece of C++ that somebody wrote.
Blueprints hide the ideas. You can wire up a working game without ever quite knowing what a variable is, and then one day something doesn't work and you have no way to reason about why. Spend two hours writing the ideas out in text, where nothing is hidden, and every Blueprint you touch afterwards makes more sense. That's the entire pitch. You are not going to be a C++ programmer by teatime, and you don't need to be.
Every program ever written — Unreal, Fortnite, the browser you're reading this in — is built from these six things arranged in different orders. Learn them once and they transfer to every language you'll ever meet.
Named boxes that hold a value. The score. The time left. Whether the game is over.
A named chunk of instructions you can run whenever you like. "Add to the score." "Spawn the coins."
Asking a question and acting on the answer. "Has the timer run out? Then stop the game."
Doing something over and over without writing it out twenty times.
One name holding many things. All twenty coins in a single list.
A blueprint — small b — for a kind of thing. Write "coin" once, make twenty of them.
Sections 02 to 08 are the ideas, each with a small example and a "guess what this prints" challenge. Sections 09 to 12 are you writing the real game, file by file, with every line explained. Section 13 is what to do when it won't compile — and it won't, at some point, for everybody. That's not a sign you're bad at this. It's just Tuesday.
Unreal can only run C++ if it has something to compile it with. On Windows that's Visual Studio — not Visual Studio Code, the other one — and it needs a specific chunk installed. Get this bit wrong and nothing else in this guide works, so do it properly.
Open the Windows Start menu and look for Visual Studio 2022. If it's there, open Visual Studio Installer → Modify, and make sure "Game development with C++" is ticked. If Visual Studio isn't there at all, that's an IT request and it takes time — tell your tutor today rather than discovering it in the session.
CoinRush and put it in C:\UnrealProjects The name matters — it turns up inside the code later as COINRUSH_API. If you name it something else, that's fine, just expect your version to say something different there.Have a look in C:\UnrealProjects\CoinRush. The folder that matters is Source — that's where your code lives, and it's the one place Unreal will look for it.
CoinRush.uproject // double-click this to open the project Config/ // project settings Content/ // art, levels, sounds — the stuff you can see Source/ // YOUR CODE LIVES HERE CoinRush/ CoinRush.Build.cs // which bits of the engine this project uses CoinRushCharacter.h // the template's player character CoinRushCharacter.cpp Binaries/ // the compiled result. Never edit. Never commit. Intermediate/ // compiler scratch paper. Also never commit.
C++ splits every class across two files. It looks like pointless duplication for about a week, and then it clicks. Here's the version that makes it click faster:
Coin.h — the promiseThe {{header|header}}. A short list of what this thing has and what it can do. Names only, no instructions. Like a menu: it tells you lasagne is available; it does not tell you how to make lasagne.
Coin.cpp — the deliveryThe {{source|source file}}. The actual instructions for everything the header promised. The recipe. Long, detailed, and the only place anything really happens.
So other files can read the short version. When your GameRules.cpp wants to talk to a coin, it includes Coin.h — twenty lines telling it what a coin can do — instead of wading through the whole recipe. It's the difference between reading a menu and reading a cookbook.
The trap: promise something in the header and forget to write it in the .cpp, and the compiler will let you get all the way to the end before complaining. That's the LNK2019 error, and it's in section 13, because you will meet it.
You will change code and wonder why nothing happened. Ninety per cent of the time it's this:
Leave the editor open and press Ctrl + Alt + F11. That's {{live|Live Coding}}. It recompiles in a few seconds and your change is live without restarting anything. Magic, when it works.
Close the Unreal editor completely, then in Visual Studio press Ctrl + Shift + B to build, then reopen the project. Live Coding cannot add new things while the editor is running, and if you make it try, it will half-work in ways that waste twenty minutes.
In Unreal: Window → Output Log. Dock it somewhere you can see it. That's where UE_LOG messages land, and it's how you find out what your program is actually doing rather than what you assumed it was doing. Type LogTemp into its filter box to hide the engine's own chatter and see only your messages.
That's it. That's a variable. You give it a name, you say what kind of thing goes in it, and from then on you can read it, change it, and ask questions about it. Your score is a variable. The time left is a variable. Whether the game has ended is a variable.
int32the type — what kind of value is allowed in the boxScorethe name — what you'll call it from now on=put this in it0the starting value;end of instruction. Never optional.Read out loud, that line says: "make a whole number called Score, and start it at zero." From then on, Score means whatever is currently in that box.
int32 Score = 0; // whole number. No decimal point, ever. float TimeRemaining = 30.0f; // number with a decimal point. Note the f. bool bGameOver = false; // true or false. Nothing else. FString PlayerName = TEXT("Aoife"); // text. Unreal's own string type. FVector SpawnPoint = FVector(0, 0, 90);// a position: X, Y, Z. FRotator Spin = FRotator(0, 90, 0); // a rotation: pitch, yaw, roll.
| Type | Holds | Examples | Watch out for |
|---|---|---|---|
int32 | Whole numbers | 0, 20, -7 | Unreal uses int32, not plain int. Same thing, but match the house style. |
float | Decimals | 30.0f, 1.5f | The f on the end matters. Leave it off and you get warnings. |
bool | True or false | true, false | Unreal names these with a lower-case b first: bGameOver. |
FString | Text | TEXT("Hello") | Always wrap the text in TEXT(...) in Unreal. |
FVector | A 3D position | FVector(0, 0, 50) | Z is up in Unreal. Not Y. This catches everyone once. |
Score = 0; // put 0 in it Score = Score + 1; // read it, add one, put the answer back Score += 1; // exactly the same thing, less typing Score++; // also the same thing. Programmers are lazy. TimeRemaining -= 0.016f;// take a bit off bGameOver = true; // flip the switch
Score = Score + 1; is not a claim that a number equals itself plus one. = does not mean "equals" — it means "put the thing on the right into the box on the left". Read it right-to-left: work out Score + 1, then put the answer in Score. When you do want to ask whether two things are equal, that's ==, with two signs, and it's in section 05.
You cannot debug what you cannot see. There are two ways to print a value in Unreal and you want both.
// 1. To the Output Log (Window -> Output Log). Keeps a history. UE_LOG(LogTemp, Warning, TEXT("Score is now %d"), Score); // 2. Big text on the screen while you play. Instant, disappears. if (GEngine) { GEngine->AddOnScreenDebugMessage(1, 2.0f, FColor::Yellow, FString::Printf(TEXT("Score: %d"), Score)); }
The %d is a hole that the value gets poured into. Use %d for whole numbers, %f for decimals, and %s for text — and text needs a * in front of the variable, because Unreal.
UPROPERTYPut one line above a variable and it appears in the editor, in the Details panel, where you can change it without recompiling anything. This is genuinely the best thing about writing gameplay code in Unreal, and you'll use it constantly in section 12.
A {{func|function}} is a chunk of instructions you've given a name to, so you can run it whenever you like by writing that name. Instead of copying the same eight lines everywhere you need them, you write them once, call it AddScore, and from then on you just say AddScore(1);.
voidwhat it gives back. void means nothing.AddScorethe name — how you'll run it(int32 Points)the parameters — what you hand it{ ... }the body — the actual instructionsvoid AGameRules::AddScore(int32 Points)
{
Score = Score + Points;
UE_LOG(LogTemp, Warning, TEXT("Score is now %d"), Score);
}
// Somewhere else, you run it by writing its name:
AddScore(1); // Score goes up by 1
AddScore(5); // same function, different answer. That's the point.
Some functions do a job. Others work something out and hand it back to you — that's the {{ret|return value}}, and the type at the front says what kind of thing to expect.
// Promises to hand back a bool. Must actually do it. bool AGameRules::HasPlayerWon() { return Score >= CoinsToSpawn; } // Now you can ask the question anywhere: if (HasPlayerWon()) { EndGame(); }
Functions that do something get a verb: AddScore, SpawnCoins, EndGame. Functions that answer something get a question: HasPlayerWon, IsGameOver. Get this habit now and your code stays readable when it gets long — which it will, faster than you think.
Remember the promise and the delivery. Every function you write appears twice, and the two have to match exactly.
void AddScore(int32 Points);
void AGameRules::AddScore(int32 Points)
{
Score = Score + Points;
}
Three differences to spot, because each one is an error waiting to happen:
AGameRules:: in front of the name. That's saying "this is the AGameRules version of AddScore". Forget it and the compiler thinks you've invented a brand new, unrelated function.int32 in one and float in the other is two different functions as far as C++ is concerned.error LNK2019: unresolved external symbol — the single most common error a beginner meets, and it is almost always the same thing: you declared a function in the header and never wrote the body in the .cpp. The compiler was fine with the promise. The linker went looking for the actual code and found nothing. Section 13.
Everything a game decides — has the timer run out, did you win, is that a wall or a coin — is an if statement. You ask something that can only be true or false, and the code inside the braces only runs when the answer is true.
if (TimeRemaining <= 0.0f)
{
EndGame(); // only runs when the timer has run out
}
else if (Score >= CoinsToSpawn)
{
UE_LOG(LogTemp, Warning, TEXT("Got them all!"));
}
else
{
ShowStatus(); // runs when neither of the above was true
}
It reads exactly like English: if the time is at or below zero, end the game; otherwise if the score has reached the target, say so; otherwise just show the status. Only one of the three blocks ever runs.
| Written | Means | Example that's true |
|---|---|---|
== | is equal to | Score == 20 when the score is 20 |
!= | is not equal to | Score != 0 when you've collected anything |
> < | greater / less than | TimeRemaining < 5.0f when it's getting tense |
>= <= | greater / less than or equal to | Score >= 20 when you've won |
&& | and — both must be true | Score > 0 && TimeRemaining > 0 |
|| | or — either will do | bGameOver || TimeRemaining <= 0 |
! | not — flips it | !bGameOver when the game is still running |
if (Score = 20) uses one equals sign, so it doesn't ask a question — it shoves 20 into Score and then treats that as "true". Your score silently becomes 20 and the branch always runs. One = assigns, two == compares. If something is behaving impossibly, count your equals signs.
You'll see this constantly in the game code, so it's worth recognising now. Rather than wrapping everything in a giant if, check for the reasons to stop and get out immediately:
void AGameRules::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (bGameOver)
{
return; // stop right here. Nothing below runs.
}
// If we got this far, the game is definitely still going.
TimeRemaining = TimeRemaining - DeltaTime;
}
return; in a void function means "I'm finished, leave now". It's how you avoid crashes by bailing out before you touch a {{null|nullptr}}. It keeps your code flat and readable instead of a pyramid of nested braces, and it's how you avoid crashes by checking for nullptr before using something.
You need twenty coins in the level. You could write the spawn line twenty times. Or you could write it once and tell the computer to do it twenty times — and then change twenty to two hundred by editing one character. That's a loop, and it's where programming starts feeling like a superpower.
for loopint32 i = 0start — make a counter, begin at 0i < 20keep going while this is truei++after each go — add one to the counterfor (int32 i = 0; i < 20; i++)
{
UE_LOG(LogTemp, Warning, TEXT("Spawning coin number %d"), i);
}
That prints coin number 0 all the way to coin number 19. Twenty lines of output from three lines of code — and note it starts at 0, not 1. Programmers count from zero, everywhere, forever. It feels wrong for about a fortnight and then it feels normal.
Because the condition is i < 20, not i <= 20. When i reaches 20 the condition is false and the loop stops. So the loop body runs exactly twenty times, with i being 0 to 19. Change it to <= and you get twenty-one coins — which is exactly the kind of bug that takes an hour to find and a second to fix.
// while — keep going until something changes. Careful: if the // condition never becomes false, Unreal freezes. Forever. while (TimeRemaining > 0.0f) { TimeRemaining -= 1.0f; } // ranged-for — "for every coin in the list". The nicest loop // there is, and the one you'll use most. Section 07. for (ACoin* Coin : SpawnedCoins) { Coin->Destroy(); }
Write a while whose condition never becomes false and Unreal will hang solid — no crash, no message, just a frozen editor and a fan going like a jet. If it happens: Ctrl+Alt+Delete, Task Manager, end Unreal, and lose whatever you hadn't saved. Everyone does it once. Check that the thing in your condition actually changes inside the loop.
You've got twenty coins. You are not going to make twenty variables called Coin1, Coin2, Coin3. You make one list and put all twenty in it. In Unreal that list is a TArray, and it's the container you'll use more than any other.
// A list that can hold pointers to coins. Starts empty. TArray<ACoin*> SpawnedCoins; SpawnedCoins.Add(NewCoin); // put one on the end SpawnedCoins.Num(); // how many are in it? SpawnedCoins[0]; // the first one. ZERO, not one. SpawnedCoins.Empty(); // bin the lot // Lists of anything, not just actors: TArray<int32> HighScores; TArray<FString> PlayerNames;
The bit in the angle brackets says what kind of thing the {{array|list}} holds. A TArray<int32> holds whole numbers and will refuse to hold anything else — which sounds annoying and is actually the compiler catching your mistakes before the game runs.
A list of twenty coins has positions 0 to 19. There is no position 20. Ask for SpawnedCoins[20] and Unreal crashes instantly with an "array index out of bounds" message — which is genuinely the polite option, because in some languages it would quietly hand you rubbish and let you carry on.
// "For every coin in SpawnedCoins, call it Coin, and do this:" for (ACoin* Coin : SpawnedCoins) { if (IsValid(Coin)) // has it already been collected? { Coin->Destroy(); } }
IsValid check mattersWhen the player collects a coin it destroys itself — but its slot in the list is still there, now pointing at nothing. Reach through a {{ptr|pointer}} to something that no longer exists and Unreal crashes on the spot. IsValid(Coin) asks "is this still a real thing?" before you touch it. Get into the habit now: check pointers before you use them, every time. It's the difference between a game and a crash report.
A class is a description of a kind of thing — what it has, and what it can do. An object is one actual thing made from that description. You write the class ACoin once; the game makes twenty objects from it, and each one has its own position, its own points value, its own everything.
The design. Has a mesh, a spin speed, a points value. Can spin and be collected. Exists in your code.
Twenty real things in the level, each at its own position, each with its own copy of every variable.
The cutter and the biscuits. One cutter, many biscuits, each biscuit separate — bite one and the others are unaffected. Collect one coin and the other nineteen carry on spinning.
UCLASS()
class COINRUSH_API ACoin : public AActor
{
GENERATED_BODY()
public:
ACoin(); // the constructor — runs when one is made
int32 PointsValue = 1; // member variable — every coin has its own
float SpinSpeed = 90.0f;
virtual void Tick(float DeltaTime) override; // member function
};
| The bit | What it's for |
|---|---|
UCLASS() | Tells Unreal "this is one of yours" — it can now spawn it, save it, and show it in the editor. Leave it off and Unreal ignores the class completely. |
COINRUSH_API | Your project's name in capitals. Unreal writes this for you when you create the class. Don't type it by hand. |
: public AActor | {{inherit|Inheritance}}. "A coin is a kind of Actor." It instantly gets a position, rotation, the ability to be placed in a level — thousands of lines you didn't write. |
GENERATED_BODY() | Unreal's code generator fills this in. No semicolon, no arguments, always the first line inside the braces. Nobody types this from memory. |
ACoin() | The constructor — a function with the same name as the class that runs once, automatically, whenever a coin is created. Set things up here. |
public: | "Anyone can touch these." private: means only this class can. Today, keep it simple and make things public. |
The A at the front of ACoin means Actor — anything that can exist in a level. By saying : public AActor you inherit every single thing an Actor can do: having a position, being spawned, being destroyed, ticking every frame. You then add the two per cent that makes it a coin. That's not laziness, that's the entire design of the engine.
void ACoin::BeginPlay()
{
Super::BeginPlay(); // let AActor do its version first. Never skip this.
// Runs ONCE, when the game starts or this thing is spawned.
}
void ACoin::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// Runs EVERY FRAME. Maybe 60 times a second, maybe 144.
// DeltaTime is how long since the last frame, in seconds.
}
Spin the coin by 2.0f every frame and it spins twice as fast on a good PC as on a bad one — your game literally plays differently depending on the machine. Multiply by DeltaTime and you're saying "90 degrees per second" instead of "per frame", and it's identical everywhere. This is the single most useful habit in gameplay code, in any engine.
Six ideas: variables, functions, conditionals, loops, arrays, classes. Everything from here is those six things arranged into a game. Go and build it.
This is your first real class. It spins, it bobs up and down, and when the player walks into it, it tells the scorekeeper and deletes itself. Forty lines, and every one of them is something from the last six sections.
In Unreal: Tools → New C++ Class → Actor → Next, name it Coin, Create Class. Unreal writes both files, puts them in the right folder, and — importantly — tells the build system they exist. Then do it again for GameRules. Make both classes before you paste any code, because the coin needs to know the rules exist.
Replace everything in the file Unreal made with this.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Coin.generated.h"
UCLASS()
class COINRUSH_API ACoin : public AActor
{
GENERATED_BODY()
public:
ACoin();
// Everything with UPROPERTY(EditAnywhere) shows up in the
// Details panel, so you can tune it without recompiling.
// How many points this coin is worth.
UPROPERTY(EditAnywhere, Category = "Coin")
int32 PointsValue = 1;
// Degrees per second it spins.
UPROPERTY(EditAnywhere, Category = "Coin")
float SpinSpeed = 180.0f;
// How far it floats up and down, in centimetres.
UPROPERTY(EditAnywhere, Category = "Coin")
float BobHeight = 20.0f;
// How fast it bobs.
UPROPERTY(EditAnywhere, Category = "Coin")
float BobSpeed = 3.0f;
virtual void Tick(float DeltaTime) override;
protected:
virtual void BeginPlay() override;
private:
// The visible 3D shape.
UPROPERTY(VisibleAnywhere)
class UStaticMeshComponent* Mesh;
// Where this coin started, so it can bob around that point.
FVector StartLocation;
// Seconds since this coin appeared. Drives the bobbing.
float RunningTime = 0.0f;
// Runs automatically when something touches this coin.
UFUNCTION()
void OnCoinOverlap(AActor* OverlappedActor, AActor* OtherActor);
};
#include "Coin.generated.h" must be the last include. Not second last. Last. Unreal's code generator demands it and the error message you get otherwise is famously unhelpful.
GENERATED_BODY() goes on the first line inside the braces, with no semicolon after it. Both of these are things you copy rather than remember, forever, including the people who wrote the engine.
#include "Coin.h" #include "GameRules.h" #include "Components/StaticMeshComponent.h" #include "GameFramework/Character.h" #include "Kismet/GameplayStatics.h" #include "UObject/ConstructorHelpers.h" #include "Engine/StaticMesh.h" // The constructor. Runs once, the moment a coin is created. ACoin::ACoin() { // Without this, Tick() never runs and the coin never moves. PrimaryActorTick.bCanEverTick = true; // Make the visible shape and make it the root of this actor. Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh")); RootComponent = Mesh; // Borrow a cylinder that ships with the engine. static ConstructorHelpers::FObjectFinder<UStaticMesh> CylinderAsset( TEXT("/Engine/BasicShapes/Cylinder.Cylinder")); if (CylinderAsset.Succeeded()) { Mesh->SetStaticMesh(CylinderAsset.Object); } // Squash it flat and stand it on its edge, so it looks like a coin. Mesh->SetRelativeScale3D(FVector(0.6f, 0.6f, 0.08f)); Mesh->SetRelativeRotation(FRotator(90.0f, 0.0f, 0.0f)); // Overlap, don't block — the player walks THROUGH it. Mesh->SetCollisionProfileName(TEXT("OverlapAllDynamic")); Mesh->SetGenerateOverlapEvents(true); } void ACoin::BeginPlay() { Super::BeginPlay(); // Remember where we started so we can bob around it. StartLocation = GetActorLocation(); // "When anything overlaps me, run OnCoinOverlap." OnActorBeginOverlap.AddDynamic(this, &ACoin::OnCoinOverlap); } void ACoin::Tick(float DeltaTime) { Super::Tick(DeltaTime); // Count up in seconds. DeltaTime is the gap since the last frame. RunningTime = RunningTime + DeltaTime; // Spin. Times DeltaTime, so it's the same on every PC. AddActorLocalRotation(FRotator(0.0f, SpinSpeed * DeltaTime, 0.0f)); // Bob. Sin() slides smoothly between -1 and 1 forever. FVector NewLocation = StartLocation; NewLocation.Z = NewLocation.Z + FMath::Sin(RunningTime * BobSpeed) * BobHeight; SetActorLocation(NewLocation); } void ACoin::OnCoinOverlap(AActor* OverlappedActor, AActor* OtherActor) { // Was it the player, or a stray bit of scenery? Cast asks // "is this thing a Character?" and hands back nullptr if not. ACharacter* Player = Cast<ACharacter>(OtherActor); if (Player == nullptr) { return; // not the player. Ignore it. } // Go and find the scorekeeper somewhere in the level. AActor* Found = UGameplayStatics::GetActorOfClass(GetWorld(), AGameRules::StaticClass()); AGameRules* Rules = Cast<AGameRules>(Found); // ALWAYS check a pointer before using it. Always. if (Rules != nullptr) { Rules->AddScore(PointsValue); } UE_LOG(LogTemp, Warning, TEXT("Coin collected. Worth %d"), PointsValue); // Delete myself. Goodbye. Destroy(); }
Cast is doingCast<ACharacter>(OtherActor) means "if this thing is a Character, hand it to me as one; if it isn't, hand me nullptr." It's the safe way to ask what something is. The if (Player == nullptr) return; straight after is not optional politeness — without it, a falling rock could collect your coins.
Sin is doingYou do not need the trigonometry. FMath::Sin takes an ever-increasing number and hands back a value that slides smoothly from -1 up to 1 and back, forever. Multiply by BobHeight and you've got a coin gently floating. This one line is how almost every hover, pulse and wobble in every game works.
One actor that you drop into the level, which runs the whole game. It spawns the coins with a loop, keeps them in an array, counts the timer down every frame, and decides when you've won. All six ideas, in one file.
#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "GameRules.generated.h" // "A class called ACoin exists." Enough for the header; the // full #include lives in the .cpp. class ACoin; UCLASS() class COINRUSH_API AGameRules : public AActor { GENERATED_BODY() public: AGameRules(); // ---- The knobs. All editable in the Details panel. ---- UPROPERTY(EditAnywhere, Category = "Rules") int32 CoinsToSpawn = 20; UPROPERTY(EditAnywhere, Category = "Rules") float TimeLimit = 30.0f; // How far from this actor coins can appear, in centimetres. UPROPERTY(EditAnywhere, Category = "Rules") float SpawnRadius = 1200.0f; // How high off the ground they float. UPROPERTY(EditAnywhere, Category = "Rules") float SpawnHeight = 60.0f; // Which kind of coin to spawn. Set for you in the constructor. UPROPERTY(EditAnywhere, Category = "Rules") TSubclassOf<ACoin> CoinClass; // Coins call this when they're collected. void AddScore(int32 Points); virtual void Tick(float DeltaTime) override; protected: virtual void BeginPlay() override; private: // ---- The state of the game right now. ---- int32 Score = 0; float TimeRemaining = 0.0f; bool bGameOver = false; // Every coin we made, in one list. UPROPERTY() TArray<ACoin*> SpawnedCoins; void SpawnCoins(); void ShowStatus(); void EndGame(); };
#include "GameRules.h"
#include "Coin.h"
#include "Engine/World.h"
#include "Engine/Engine.h"
AGameRules::AGameRules()
{
PrimaryActorTick.bCanEverTick = true;
// Default to spawning our own ACoin class.
CoinClass = ACoin::StaticClass();
}
void AGameRules::BeginPlay()
{
Super::BeginPlay();
// Reset everything, then fill the level with coins.
Score = 0;
TimeRemaining = TimeLimit;
bGameOver = false;
SpawnCoins();
UE_LOG(LogTemp, Warning, TEXT("Go! %d coins, %.0f seconds."), CoinsToSpawn, TimeLimit);
}
// ---------- A LOOP, AN ARRAY, AND SOME RANDOM NUMBERS ----------
void AGameRules::SpawnCoins()
{
// Spawn even if something's slightly in the way.
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride =
ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
for (int32 i = 0; i < CoinsToSpawn; i++)
{
// A random spot within SpawnRadius of this actor.
float X = FMath::FRandRange(-SpawnRadius, SpawnRadius);
float Y = FMath::FRandRange(-SpawnRadius, SpawnRadius);
FVector SpawnLocation = GetActorLocation() + FVector(X, Y, SpawnHeight);
ACoin* NewCoin = GetWorld()->SpawnActor<ACoin>(
CoinClass, SpawnLocation, FRotator::ZeroRotator, SpawnParams);
if (NewCoin != nullptr)
{
SpawnedCoins.Add(NewCoin); // remember it
}
}
UE_LOG(LogTemp, Warning, TEXT("Spawned %d coins."), SpawnedCoins.Num());
}
// ---------- RUNS EVERY SINGLE FRAME ----------
void AGameRules::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (bGameOver)
{
return; // nothing to do any more
}
TimeRemaining = TimeRemaining - DeltaTime;
if (TimeRemaining <= 0.0f)
{
TimeRemaining = 0.0f;
EndGame();
return;
}
ShowStatus();
}
// ---------- CALLED BY EVERY COIN THE PLAYER TOUCHES ----------
void AGameRules::AddScore(int32 Points)
{
Score = Score + Points;
if (Score >= CoinsToSpawn)
{
EndGame(); // got the lot, early finish
}
}
void AGameRules::ShowStatus()
{
if (GEngine == nullptr)
{
return;
}
// %d is a hole for a whole number, %.1f for one decimal place.
FString Status = FString::Printf(
TEXT("SCORE %d / %d TIME %.1f"),
Score, CoinsToSpawn, TimeRemaining);
// Key 1 means "replace the last message with this one".
GEngine->AddOnScreenDebugMessage(1, 0.1f, FColor::Yellow, Status);
}
void AGameRules::EndGame()
{
bGameOver = true;
// Tidy up any coins the player didn't get.
for (ACoin* Coin : SpawnedCoins)
{
if (IsValid(Coin))
{
Coin->Destroy();
}
}
// Pick a verdict. An if / else if / else, doing real work.
FString Verdict;
if (Score >= CoinsToSpawn)
{
Verdict = TEXT("EVERY LAST ONE. SHOWOFF.");
}
else if (Score >= CoinsToSpawn / 2)
{
Verdict = TEXT("NOT BAD.");
}
else
{
Verdict = TEXT("TRY AGAIN.");
}
// The * in front of a FString is needed for %s. Unreal thing.
FString Message = FString::Printf(TEXT("%s FINAL SCORE: %d"), *Verdict, Score);
if (GEngine != nullptr)
{
GEngine->AddOnScreenDebugMessage(2, 10.0f, FColor::Green, Message);
}
UE_LOG(LogTemp, Warning, TEXT("Game over. Final score %d"), Score);
}
Variables: Score, TimeRemaining, bGameOver. Functions: SpawnCoins, AddScore, EndGame, ShowStatus. Conditionals: every if, including the three-way verdict. Loops: the for that spawns and the ranged-for that cleans up. Arrays: SpawnedCoins. Classes: the whole thing is one, and it talks to another.
That's a complete game loop in about a hundred lines. Everything else you ever build is this, with more of it.
The code is written. Now the bit where it becomes a game. Follow this exactly — the order matters more than you'd think.
Build: 1 succeeded. If you get errors, section 13 — and read the first error, not the last one.CoinRush.uproject.Coin and GameRules are in there.GameRules into the level Drop it in the middle of the open floor. Then set its Z position to about 100 in the Details panel, so it's just above the ground — coins spawn relative to this actor, so where you put it is where the game happens.Rules category, with Coins To Spawn, Time Limit, Spawn Radius and Spawn Height. Those are the UPROPERTY lines you wrote, now knobs you can turn. That's the payoff.Spinning coins scattered around you, yellow SCORE 0 / 20 TIME 30.0 in the top-left corner counting down, and a satisfying jump in the score every time you run through one. Get all twenty and it ends early and calls you a showoff. Run out of time and it tells you to try again. Check the Output Log afterwards — there's a line for every coin you collected.
These are game problems, not code problems. Compiler errors are in section 13.
Is the GameRules actor actually in the level? Check the Outliner. If it is, select it and look at Coin Class in the Details panel — if it says None, set it to Coin by hand.
They're at the wrong height. Change Spawn Height on the GameRules actor to around 60–90 and play again. No recompiling needed — that's what UPROPERTY bought you.
Turn Spawn Radius down to 800 and move the GameRules actor to the middle of the open area. Making them land properly on any surface is challenge 6 in the next section.
Time Limit is 0 on the placed actor. See the box below — this catches everybody exactly once.
When you drag an actor into a level, Unreal saves a copy of its settings into the level. Change a default in the header afterwards — say TimeLimit = 30.0f to 60.0f — and the actor already sitting in your level keeps the old number. It is not ignoring you; it's remembering what it was told.
Two ways out: change the value in the Details panel instead (which is the point of UPROPERTY), or delete the actor from the level and drag a fresh one in.
You have a working game and a set of dials. This is the best twenty minutes of the session: change a number, press play, see what happens, decide whether it's better. That loop — change, run, judge — is what game development actually is.
Select the GameRules actor, change these in the Details panel, press Play. Instant.
| Turn this | To this | And you get |
|---|---|---|
Time Limit | 10 | Genuine panic. Ten seconds is nowhere near enough, which is exactly why it's fun. |
Coins To Spawn | 200 | Absurd. Do it anyway — it's the loop proving it doesn't care how many times it runs. |
Spawn Radius | 300 | All the coins in a tight pile. Trivially easy, and it shows you why level design is a job. |
Spawn Height | 400 | Coins floating out of reach. Now you need a jump — and the template character can already jump. Try it. |
Spin Speed (on a coin, mid-game) | 2000 | Ridiculous. Also a good demonstration that DeltaTime is keeping it smooth. |
In Tick, print something when the clock drops below five seconds. You'll need an if and a comparison.
if (TimeRemaining < 5.0f)
{
GEngine->AddOnScreenDebugMessage(3, 0.1f, FColor::Red, TEXT("HURRY UP"));
}Right now every coin spins at exactly the same speed, which looks mechanical. Give each one its own in BeginPlay. One line.
SpinSpeed = FMath::FRandRange(90.0f, 400.0f);
In SpawnCoins, after the coin is spawned, give roughly one in six a PointsValue of 5 and a bigger scale so players can tell.
if (FMath::RandRange(1, 6) == 1)
{
NewCoin->PointsValue = 5;
NewCoin->SetActorScale3D(FVector(1.6f, 1.6f, 1.6f));
}Then find the bug you just made. The win check is Score >= CoinsToSpawn — with bonus coins you can hit 20 points having collected only twelve coins, and the game ends with coins still lying around. That's a real design bug, and finding it yourself is worth more than the feature. Fix it by counting coins collected separately from points scored.
Genuinely funny to play against. In the coin's Tick, find the player and, if they're close, move the other way.
APawn* Player = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
if (Player != nullptr)
{
float Distance = FVector::Dist(GetActorLocation(), Player->GetActorLocation());
if (Distance < 400.0f)
{
FVector Away = GetActorLocation() - Player->GetActorLocation();
Away.Normalize();
StartLocation = StartLocation + Away * 300.0f * DeltaTime;
}
}In the Content Browser, right-click → Material, name it M_Gold, open it, set Base Colour to yellow and Metallic to 1. Then load it in the coin's constructor the same way you loaded the cylinder, and apply it with Mesh->SetMaterial(0, GoldMaterial);. The path will be something like /Game/M_Gold.M_Gold.
Random X and Y means coins end up inside walls and over cliffs. The professional fix is a line trace — fire an invisible ray straight down from high above the random spot, find where it hits the floor, and spawn the coin there. Look up LineTraceSingleByChannel. This is genuinely a step up in difficulty and it is exactly how real games place things.
One change at a time, then play it. Change six things at once and something breaks, you've no idea which one did it, and you'll spend longer unpicking it than you saved. This is also the entire argument for version control — commit before each experiment and you can always get back.
Next term you'll build all of this again in Blueprints, and it'll feel like cheating — because you'll already know what a variable is, why the loop stops at 19, and what "cast failed" means. That's what today was for.
If you want to keep the game: put it under version control before you change anything else. There's a whole guide: Save Point →
Everyone's code fails to compile. Professionals' code fails to compile several times an hour. The difference between someone who's been doing this a year and someone on their first day isn't that one of them makes fewer mistakes — it's that one of them reads the error message.
1. Read the FIRST error, not the last. One missing semicolon can produce forty errors. The first one is the real one; the other thirty-nine are the compiler getting increasingly confused. Scroll up.
2. The problem is often the line ABOVE the one it names. Especially with missing semicolons. The compiler only notices something's wrong when it gets to the next line.
3. Google the error code. C2065, LNK2019 — those codes are the same for everyone on earth. Someone has had your exact problem and written it up. That's not cheating, that's the job.
Warm up on these. Click the line you think is wrong.
Ask, and bring three things: the first error message, copied as text, the file and line number it names, and what you changed just before it started. That's a question anyone can answer in thirty seconds. "It's broken" is a question nobody can answer at all.
All six ideas, plus the three Unreal-specific things that catch people out. If you can do these, you understood today.
Programming has a lot of vocabulary and most of it is worse than it needs to be. Here's all of it, without pretending any of it is obvious.
Ask in class, or email JBell@belfastmet.ac.uk — with the error message copied as text, please, not a photo of a monitor.