π Lesson 1.5: Your First C++ Class & the Live Coding Loop
Everything so far has been groundwork. Now you'll create a real C++ actor, make it do something visible, place it in a level, and use Live Coding to change its behavior without ever closing the editor. This is the loop you'll run thousands of times.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Create a C++ actor class with the editor's class wizard
- Read and understand every line of a generated actor's
.hand.cpp - Add a
UPROPERTY, log to the Output Log, and make the actor rotate each frame - Use Live Coding to compile changes while the editor stays open
- Know when Live Coding is enough and when you need a full rebuild
Estimated Time: 60 minutes
Engine Version: Unreal Engine 5.8
In This Lesson
Creating the Class
Unreal generates the boilerplate for you. In the editor:
- Open Tools β New C++ Classβ¦
- Choose Actor as the parent class, click Next
- Name it
SpinningCube, confirm the module, and click Create Class
Unreal creates SpinningCube.h and SpinningCube.cpp in your Source/ tree, adds them to the build, and compiles. The class name becomes ASpinningCube β the A prefix marks it as an actor (a convention enforced by the header tool; more on prefixes in Lesson 2.1).
Anatomy of the Generated Files
Here's what the wizard produces, annotated line by line. First the header:
// SpinningCube.h
#pragma once // include guard β appears once per header
#include "CoreMinimal.h" // the lightweight base include for most headers
#include "GameFramework/Actor.h" // we derive from AActor, so we need its header
#include "SpinningCube.generated.h" // MUST be last β generated reflection code
UCLASS() // reflection macro: register this class
class MYPROJECT_API ASpinningCube : public AActor
{
GENERATED_BODY() // expands to engine boilerplate for this class
public:
ASpinningCube(); // constructor: set defaults, create components
protected:
virtual void BeginPlay() override; // called once when play starts
public:
virtual void Tick(float DeltaTime) override; // called every frame
};
π MYPROJECT_API?
That macro controls DLL export so other modules can use your class. The header tool defines it per module (it'll match your project's name). Leave it as generated β it matters when other modules link against yours.
Now the source file:
// SpinningCube.cpp
#include "SpinningCube.h"
// Constructor: runs when the object is created (in editor AND at runtime).
ASpinningCube::ASpinningCube()
{
// Tell the engine this actor wants Tick() called every frame.
// (It's off by default for performance β you opt in.)
PrimaryActorTick.bCanEverTick = true;
}
// Called once, when the game starts or the actor is spawned into a running level.
void ASpinningCube::BeginPlay()
{
Super::BeginPlay(); // ALWAYS call the base version first
}
// Called every frame; DeltaTime is seconds since the last frame.
void ASpinningCube::Tick(float DeltaTime)
{
Super::Tick(DeltaTime); // ALWAYS call the base version
}
β οΈ Never forget Super::
When you override an engine lifecycle function (BeginPlay, Tick, etc.), call Super:: β it runs the base class's implementation, which does real engine work. Forgetting it causes subtle, hard-to-diagnose bugs.
Making It Do Something
Let's give the cube a visible mesh, an exposed rotation-speed property, a startup log, and per-frame rotation. This touches everything from Module 1 at once. The header first:
// SpinningCube.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SpinningCube.generated.h"
class UStaticMeshComponent; // forward declaration β we only hold a pointer here
UCLASS()
class MYPROJECT_API ASpinningCube : public AActor
{
GENERATED_BODY()
public:
ASpinningCube();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
// The visible cube. VisibleAnywhere: shown but not reassignable in Details.
UPROPERTY(VisibleAnywhere, Category = "Spinning Cube")
UStaticMeshComponent* Mesh;
// Degrees per second. EditAnywhere + BlueprintReadWrite so designers and
// Blueprint subclasses can tune it. This appears in the Details panel.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spinning Cube")
float RotationSpeed = 90.0f;
};
Now the implementation:
// SpinningCube.cpp
#include "SpinningCube.h"
#include "Components/StaticMeshComponent.h" // full header β we call its members
ASpinningCube::ASpinningCube()
{
PrimaryActorTick.bCanEverTick = true;
// Create the mesh component and make it this actor's root.
// CreateDefaultSubobject is the constructor-only way to build components.
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
RootComponent = Mesh;
}
void ASpinningCube::BeginPlay()
{
Super::BeginPlay();
// Our first real log: %s prints an FString; the leading * converts
// GetName()'s FString to the raw characters the format expects.
UE_LOG(LogTemp, Warning, TEXT("%s is ready to spin at %.1f deg/s"),
*GetName(), RotationSpeed);
}
void ASpinningCube::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// Frame-rate independent rotation: multiplying by DeltaTime means the cube
// spins RotationSpeed degrees PER SECOND regardless of frame rate.
AddActorLocalRotation(FRotator(0.0f, RotationSpeed * DeltaTime, 0.0f));
}
β
Why * DeltaTime matters
Without it, the cube would rotate RotationSpeed degrees per frame β spinning faster on fast machines. Multiplying by DeltaTime (seconds since last frame) makes motion depend on time, not frame rate. This is a universal game-programming habit; make it reflex.
π CreateDefaultSubobject
This is the only correct way to create a component from a constructor. It registers the component with the engine's object system. Using plain new here would break serialization and garbage collection β a rule we'll justify fully in Module 2 and Module 3.
Place, Compile, Play
With the code written:
- Compile β press the Compile button in the editor toolbar (or build from your IDE). This triggers Live Coding (next section).
- Assign a mesh β drag
ASpinningCubefrom C++ Classes into the level, select it, and in the Details panel set the mesh component's Static Mesh to the engine'sCube(or make a Blueprint subclass with the mesh pre-set β the Module 6 way). - Press Play β the cube spins, and the Output Log shows your
"β¦is ready to spin at 90.0 deg/s"line. - Tune it live β while stopped, change
RotationSpeedin Details to360and play again. No recompile needed, because the value is data, not code.
π‘ Data vs code: changing aUPROPERTYvalue in Details is instant β it's data on an instance. Changing the logic in.cpprequires a compile. Knowing which kind of change you're making tells you whether you need Live Coding.
The Live Coding Loop
Live Coding lets you recompile C++ and patch it into the running editor β no restart. It's the feature that makes C++ iteration bearable. The loop:
(or Compile button)"] B --> C["Live Coding recompiles"] C --> D["Editor patches the code in place"] D --> E["Test immediately"] E --> A
What Live Coding handles well
- Changes to function bodies β new logic inside
Tick,BeginPlay, your own methods - Adjusting calculations, adding logs, fixing behavior
What needs a full rebuild (close editor, build, reopen)
- Adding or removing member variables or UPROPERTY declarations (changes the object's memory layout)
- Adding or removing classes, changing a class's parent, or editing headers structurally
- Changing reflection macros (new
UFUNCTION, changed specifiers)
β οΈ The rule of thumb
Edits inside .cpp function bodies β Live Coding. Edits to .h structure (new members, new classes, changed inheritance) β close the editor and do a full build. Trying to Live-Code a header layout change is the most common source of "why didn't my change take?" β and occasionally of instability. When in doubt, full rebuild.
β Pro Tip
Live Coding is enabled by default in 5.8. You'll see its status in the bottom-right of the editor. If a Live Coding patch ever leaves the editor in a weird state, a full close-and-rebuild always resets you to solid ground.
Hands-on Exercise & Quiz
ποΈ Exercise: Extend the cube
Objective: Practice the write β Live Code β test loop and feel the Live-Coding boundary.
- Add a bobbing motion: in
Tick, also move the cube up and down over time. (Hint: accumulate time and useFMath::Sin, thenAddActorLocalOffset.) - Compile with Ctrl+Alt+F11 and confirm the change appears without restarting the editor.
- Now add a new
UPROPERTY float BobHeight. Try Live Coding β note that this header change needs a full rebuild. Do the rebuild and confirmBobHeightappears in Details.
π‘ Hint β the bob in Tick
// Add a member: float RunningTime = 0.0f; (in the header β needs a rebuild)
RunningTime += DeltaTime;
const float Offset = FMath::Sin(RunningTime) * BobHeight * DeltaTime;
AddActorLocalOffset(FVector(0.0f, 0.0f, Offset));
β What you should observe
Step 1's logic change (if you hardcode values) Live-Codes fine. The moment you add the RunningTime or BobHeight members in the header, Live Coding can't apply it and you need a full rebuild β exactly the boundary from the rule of thumb.
π― Quick Quiz
Question 1: Why multiply rotation by DeltaTime?
Question 2: Which change can Live Coding apply without an editor restart?
Question 3: What's the correct way to create a component in a constructor?
Summary
π Key Takeaways
- The New C++ Class wizard generates a compile-ready actor; the
Aprefix marks actors. - Always call
Super::when overriding lifecycle functions likeBeginPlayandTick. - Create components in the constructor with
CreateDefaultSubobject<T>β never plainnew. - Multiply per-frame motion by
DeltaTimefor frame-rate independence; expose tunables withUPROPERTY. - Live Coding hot-patches
.cppbody edits into the running editor; structural.hchanges need a full rebuild.
π Additional Resources
π What's Next?
You've shipped your first working C++ actor β congratulations, that's the hardest first step. Module 2 goes under the hood: the reflection system that made UCLASS and UPROPERTY work, Unreal's own types and containers, and the memory model that explains why we never use plain new.
π Module 1 complete!
You can create, compile, and iterate C++ in Unreal. Now let's understand what's really happening.