Unreal Engine is like a high-performance sports car, and C++ is its engine. While you can drive it with Blueprints (visual scripting), knowing C++ gives you access to the engine room where you can fine-tune every aspect of performance and create systems that aren't possible with Blueprints alone.
Before diving into code, let's set up your development environment properly. Think of this as preparing your workshop before building a masterpiece.
// Windows: Visual Studio 2019/2022
// Required Components:
// - Game development with C++
// - .NET Framework
// - Windows 10 SDK
// macOS: Xcode
// - Latest version from App Store
// - Command Line Tools
// Linux:
// - clang 13.0.1+
// - Specific toolchain from Epic
Unreal Engine extends C++ with its own macro system and conventions. It's like C++ with superpowers designed specifically for game development.
// MyActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h" // Always last include!
UCLASS() // Macro that makes this visible to Unreal
class MYPROJECT_API AMyActor : public AActor
{
GENERATED_BODY() // Required boilerplate
public:
AMyActor();
// Called every frame
virtual void Tick(float DeltaTime) override;
protected:
// Called when the game starts
virtual void BeginPlay() override;
// Properties visible in editor
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "My Variables")
float Speed = 100.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "My Variables")
int32 Health = 100;
// Function callable from Blueprints
UFUNCTION(BlueprintCallable, Category = "My Functions")
void TakeDamage(int32 DamageAmount);
private:
float TimeLived = 0.0f;
};
// MyActor.cpp
#include "MyActor.h"
AMyActor::AMyActor()
{
// Set this actor to call Tick() every frame
PrimaryActorTick.bCanEverTick = true;
}
void AMyActor::BeginPlay()
{
Super::BeginPlay();
UE_LOG(LogTemp, Warning, TEXT("Actor spawned with health: %d"), Health);
}
void AMyActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
TimeLived += DeltaTime;
// Move the actor
FVector NewLocation = GetActorLocation();
NewLocation.X += Speed * DeltaTime;
SetActorLocation(NewLocation);
}
void AMyActor::TakeDamage(int32 DamageAmount)
{
Health -= DamageAmount;
if (Health <= 0)
{
UE_LOG(LogTemp, Error, TEXT("Actor died!"));
Destroy();
}
}
Unreal Engine provides a rich hierarchy of classes. Understanding these is like knowing the different LEGO blocks available for building your game.
// UObject - Base class for all Unreal objects
// Provides reflection, serialization, and garbage collection
// AActor - Can be placed in the world
class MYPROJECT_API AMyGameActor : public AActor
{
GENERATED_BODY()
public:
// Components
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* MeshComponent;
UPROPERTY(VisibleAnywhere)
class USphereComponent* CollisionComponent;
AMyGameActor();
};
// APawn - Actor that can be possessed by a controller
class MYPROJECT_API AMyPawn : public APawn
{
GENERATED_BODY()
public:
virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override;
void MoveForward(float Value);
void MoveRight(float Value);
};
// ACharacter - Pawn with built-in movement
class MYPROJECT_API AMyCharacter : public ACharacter
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "Combat")
float MaxHealth = 100.0f;
UFUNCTION(BlueprintImplementableEvent, Category = "Combat")
void OnDeath(); // Implemented in Blueprint
UFUNCTION(BlueprintNativeEvent, Category = "Combat")
void TakeDamage(float Damage); // Can be overridden in Blueprint
virtual void TakeDamage_Implementation(float Damage);
};
Components are like LEGO pieces you attach to actors. They provide specific functionality like rendering, physics, or audio.
// HealthComponent.h
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class MYPROJECT_API UHealthComponent : public UActorComponent
{
GENERATED_BODY()
public:
UHealthComponent();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Health")
float MaxHealth = 100.0f;
UPROPERTY(BlueprintReadOnly, Category = "Health")
float CurrentHealth;
UFUNCTION(BlueprintCallable, Category = "Health")
void TakeDamage(float DamageAmount);
UFUNCTION(BlueprintCallable, Category = "Health")
void Heal(float HealAmount);
// Events
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float, Health);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDeath);
UPROPERTY(BlueprintAssignable, Category = "Health")
FOnHealthChanged OnHealthChanged;
UPROPERTY(BlueprintAssignable, Category = "Health")
FOnDeath OnDeath;
protected:
virtual void BeginPlay() override;
};
// HealthComponent.cpp
UHealthComponent::UHealthComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UHealthComponent::BeginPlay()
{
Super::BeginPlay();
CurrentHealth = MaxHealth;
}
void UHealthComponent::TakeDamage(float DamageAmount)
{
CurrentHealth = FMath::Clamp(CurrentHealth - DamageAmount, 0.0f, MaxHealth);
OnHealthChanged.Broadcast(CurrentHealth);
if (CurrentHealth <= 0.0f)
{
OnDeath.Broadcast();
}
}
void UHealthComponent::Heal(float HealAmount)
{
CurrentHealth = FMath::Clamp(CurrentHealth + HealAmount, 0.0f, MaxHealth);
OnHealthChanged.Broadcast(CurrentHealth);
}
Unreal provides a complete gameplay framework. Think of it as the rules and structure of a sports game - you have players, rules, and a playing field.
// MyGameMode.h
UCLASS()
class MYPROJECT_API AMyGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
AMyGameMode();
virtual void StartPlay() override;
virtual void HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer) override;
UFUNCTION(BlueprintCallable, Category = "Game")
void RespawnPlayer(AController* Controller);
protected:
UPROPERTY(EditDefaultsOnly, Category = "Game")
TSubclassOf DefaultPawnClass;
UPROPERTY(EditDefaultsOnly, Category = "Game")
float RespawnDelay = 3.0f;
UPROPERTY()
TArray PlayerStarts;
private:
void FindPlayerStarts();
APlayerStart* GetBestPlayerStart(AController* Controller);
};
// MyGameMode.cpp
AMyGameMode::AMyGameMode()
{
// Set default classes
DefaultPawnClass = AMyCharacter::StaticClass();
PlayerControllerClass = AMyPlayerController::StaticClass();
HUDClass = AMyHUD::StaticClass();
GameStateClass = AMyGameState::StaticClass();
PlayerStateClass = AMyPlayerState::StaticClass();
}
void AMyGameMode::StartPlay()
{
Super::StartPlay();
FindPlayerStarts();
}
void AMyGameMode::RespawnPlayer(AController* Controller)
{
if (Controller && Controller->GetPawn())
{
Controller->GetPawn()->Destroy();
}
// Delay respawn
FTimerHandle RespawnTimerHandle;
FTimerDelegate RespawnDelegate;
RespawnDelegate.BindLambda([this, Controller]()
{
if (Controller)
{
APlayerStart* SpawnPoint = GetBestPlayerStart(Controller);
if (SpawnPoint)
{
FVector SpawnLocation = SpawnPoint->GetActorLocation();
FRotator SpawnRotation = SpawnPoint->GetActorRotation();
APawn* NewPawn = GetWorld()->SpawnActor(
DefaultPawnClass, SpawnLocation, SpawnRotation
);
if (NewPawn)
{
Controller->Possess(NewPawn);
}
}
}
});
GetWorldTimerManager().SetTimer(
RespawnTimerHandle, RespawnDelegate, RespawnDelay, false
);
}
Handling input in Unreal is like setting up a control panel - you map physical inputs to game actions.
// MyPlayerController.h
UCLASS()
class MYPROJECT_API AMyPlayerController : public APlayerController
{
GENERATED_BODY()
public:
virtual void SetupInputComponent() override;
protected:
// Movement
void MoveForward(float Value);
void MoveRight(float Value);
void Turn(float Value);
void LookUp(float Value);
// Actions
void Jump();
void StopJumping();
void Fire();
void Reload();
// Enhanced Input System (UE5)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
class UInputMappingContext* DefaultMappingContext;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
class UInputAction* MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
class UInputAction* LookAction;
};
// MyPlayerController.cpp
void AMyPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
// Traditional Input (UE4 style)
InputComponent->BindAxis("MoveForward", this, &AMyPlayerController::MoveForward);
InputComponent->BindAxis("MoveRight", this, &AMyPlayerController::MoveRight);
InputComponent->BindAxis("Turn", this, &AMyPlayerController::Turn);
InputComponent->BindAxis("LookUp", this, &AMyPlayerController::LookUp);
InputComponent->BindAction("Jump", IE_Pressed, this, &AMyPlayerController::Jump);
InputComponent->BindAction("Jump", IE_Released, this, &AMyPlayerController::StopJumping);
InputComponent->BindAction("Fire", IE_Pressed, this, &AMyPlayerController::Fire);
// Enhanced Input (UE5 style)
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem(GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultMappingContext, 0);
}
}
void AMyPlayerController::MoveForward(float Value)
{
if (APawn* ControlledPawn = GetPawn())
{
const FRotator YawRotation(0, ControlRotation.Yaw, 0);
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
ControlledPawn->AddMovementInput(Direction, Value);
}
}
Collision in Unreal is like setting up invisible force fields around objects that determine how they interact.
// Projectile.h
UCLASS()
class MYPROJECT_API AProjectile : public AActor
{
GENERATED_BODY()
public:
AProjectile();
UPROPERTY(VisibleAnywhere, Category = "Components")
class USphereComponent* CollisionComponent;
UPROPERTY(VisibleAnywhere, Category = "Components")
class UProjectileMovementComponent* ProjectileMovement;
UPROPERTY(VisibleAnywhere, Category = "Components")
UStaticMeshComponent* MeshComponent;
UPROPERTY(EditDefaultsOnly, Category = "Damage")
float Damage = 20.0f;
UFUNCTION()
void OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComponent, FVector NormalImpulse,
const FHitResult& Hit);
UFUNCTION()
void OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult);
};
// Projectile.cpp
AProjectile::AProjectile()
{
PrimaryActorTick.bCanEverTick = false;
// Create collision component
CollisionComponent = CreateDefaultSubobject(TEXT("SphereComponent"));
CollisionComponent->SetSphereRadius(15.0f);
CollisionComponent->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
CollisionComponent->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
CollisionComponent->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
CollisionComponent->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Block);
CollisionComponent->SetCollisionResponseToChannel(ECollisionChannel::ECC_WorldStatic, ECollisionResponse::ECR_Block);
RootComponent = CollisionComponent;
// Create mesh
MeshComponent = CreateDefaultSubobject(TEXT("MeshComponent"));
MeshComponent->SetupAttachment(RootComponent);
MeshComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// Create projectile movement
ProjectileMovement = CreateDefaultSubobject(TEXT("ProjectileMovement"));
ProjectileMovement->UpdatedComponent = CollisionComponent;
ProjectileMovement->InitialSpeed = 3000.0f;
ProjectileMovement->MaxSpeed = 3000.0f;
ProjectileMovement->bRotationFollowsVelocity = true;
ProjectileMovement->bShouldBounce = false;
ProjectileMovement->ProjectileGravityScale = 0.0f;
// Bind collision events
CollisionComponent->OnComponentHit.AddDynamic(this, &AProjectile::OnHit);
CollisionComponent->OnComponentBeginOverlap.AddDynamic(this, &AProjectile::OnBeginOverlap);
// Die after 3 seconds
InitialLifeSpan = 3.0f;
}
void AProjectile::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComponent, FVector NormalImpulse,
const FHitResult& Hit)
{
// Apply damage
if (OtherActor && OtherActor != GetOwner())
{
UGameplayStatics::ApplyPointDamage(
OtherActor,
Damage,
GetActorLocation(),
Hit,
nullptr,
this,
UDamageType::StaticClass()
);
}
// Spawn impact effect
if (ImpactEffect)
{
UGameplayStatics::SpawnEmitterAtLocation(
GetWorld(),
ImpactEffect,
Hit.Location,
Hit.Normal.Rotation()
);
}
// Destroy projectile
Destroy();
}
Unreal's networking is like a synchronized dance - the server leads, and clients follow, with the engine handling most of the complex synchronization.
// NetworkedCharacter.h
UCLASS()
class MYPROJECT_API ANetworkedCharacter : public ACharacter
{
GENERATED_BODY()
public:
ANetworkedCharacter();
// Replicated properties
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Stats")
float Health = 100.0f;
UPROPERTY(ReplicatedUsing = OnRep_Armor, BlueprintReadOnly, Category = "Stats")
float Armor = 50.0f;
// RPC Functions (Remote Procedure Calls)
UFUNCTION(Server, Reliable, WithValidation)
void ServerTakeDamage(float DamageAmount, AController* InstigatedBy);
UFUNCTION(NetMulticast, Reliable)
void MulticastPlayHitEffect();
UFUNCTION(Client, Reliable)
void ClientNotifyKill(const FString& KillerName);
protected:
virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override;
UFUNCTION()
void OnRep_Armor();
virtual void BeginPlay() override;
};
// NetworkedCharacter.cpp
ANetworkedCharacter::ANetworkedCharacter()
{
// Enable replication
bReplicates = true;
SetReplicateMovement(true);
}
void ANetworkedCharacter::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// Replicate to everyone
DOREPLIFETIME(ANetworkedCharacter, Health);
// Replicate with condition
DOREPLIFETIME_CONDITION(ANetworkedCharacter, Armor, COND_OwnerOnly);
}
void ANetworkedCharacter::ServerTakeDamage_Implementation(float DamageAmount, AController* InstigatedBy)
{
// Server-only logic
if (!HasAuthority()) return;
float ActualDamage = DamageAmount;
// Apply armor reduction
if (Armor > 0)
{
float ArmorAbsorbed = FMath::Min(Armor, DamageAmount * 0.5f);
Armor -= ArmorAbsorbed;
ActualDamage -= ArmorAbsorbed;
}
Health -= ActualDamage;
// Play effect on all clients
MulticastPlayHitEffect();
if (Health <= 0)
{
// Notify killer
if (APlayerController* KillerPC = Cast(InstigatedBy))
{
if (APlayerState* VictimPS = GetPlayerState())
{
KillerPC->ClientNotifyKill(VictimPS->GetPlayerName());
}
}
}
}
bool ANetworkedCharacter::ServerTakeDamage_Validate(float DamageAmount, AController* InstigatedBy)
{
// Validate the RPC - prevent cheating
return DamageAmount > 0 && DamageAmount <= 1000.0f;
}
void ANetworkedCharacter::MulticastPlayHitEffect_Implementation()
{
// Play effect on all clients
// This runs on server and all clients
}
void ANetworkedCharacter::OnRep_Armor()
{
// Called on clients when Armor changes
// Update UI or play effects
}
Writing efficient Unreal C++ code is like tuning a race car - every optimization counts when you're pushing for 60+ FPS.
// Object Pooling Example
UCLASS()
class MYPROJECT_API AProjectilePool : public AActor
{
GENERATED_BODY()
private:
UPROPERTY()
TArray PooledProjectiles;
UPROPERTY(EditDefaultsOnly, Category = "Pool")
TSubclassOf ProjectileClass;
UPROPERTY(EditDefaultsOnly, Category = "Pool")
int32 PoolSize = 50;
public:
virtual void BeginPlay() override;
AProjectile* GetPooledProjectile();
void ReturnProjectileToPool(AProjectile* Projectile);
};
void AProjectilePool::BeginPlay()
{
Super::BeginPlay();
// Pre-spawn projectiles
for (int32 i = 0; i < PoolSize; i++)
{
AProjectile* NewProjectile = GetWorld()->SpawnActor(
ProjectileClass, FVector::ZeroVector, FRotator::ZeroRotator
);
if (NewProjectile)
{
NewProjectile->SetActorHiddenInGame(true);
NewProjectile->SetActorEnableCollision(false);
NewProjectile->SetActorTickEnabled(false);
PooledProjectiles.Add(NewProjectile);
}
}
}
// Async Loading Example
void AMyGameMode::LoadGameAssets()
{
// Async load multiple assets
TArray AssetsToLoad;
AssetsToLoad.Add(FSoftObjectPath("/Game/Weapons/Rifle.Rifle"));
AssetsToLoad.Add(FSoftObjectPath("/Game/Characters/Enemy.Enemy"));
FStreamableManager& StreamableManager = UAssetManager::GetStreamableManager();
StreamableManager.RequestAsyncLoad(
AssetsToLoad,
FStreamableDelegate::CreateUObject(this, &AMyGameMode::OnAssetsLoaded)
);
}
Debugging in Unreal is like being a detective with superpowers - you have visual debugging, logging, and profiling tools at your disposal.
// Debug Drawing
void AMyCharacter::DrawDebugInfo()
{
// Draw sphere at character location
DrawDebugSphere(
GetWorld(),
GetActorLocation(),
100.0f, // Radius
12, // Segments
FColor::Red, // Color
false, // Persistent
2.0f // Lifetime
);
// Draw line to target
if (CurrentTarget)
{
DrawDebugLine(
GetWorld(),
GetActorLocation(),
CurrentTarget->GetActorLocation(),
FColor::Green,
false,
2.0f,
0,
5.0f // Thickness
);
}
// Draw debug string
DrawDebugString(
GetWorld(),
GetActorLocation() + FVector(0, 0, 100),
FString::Printf(TEXT("Health: %.1f"), Health),
nullptr,
FColor::White,
2.0f,
true // Draw shadow
);
}
// Console Commands
static FAutoConsoleCommand DebugHealthCommand(
TEXT("Debug.ShowHealth"),
TEXT("Shows health values above all characters"),
FConsoleCommandDelegate::CreateLambda([]()
{
for (TActorIterator It(GWorld); It; ++It)
{
(*It)->bShowHealthDebug = !(*It)->bShowHealthDebug;
}
})
);
// Logging
UE_LOG(LogTemp, Display, TEXT("Character spawned at %s"), *GetActorLocation().ToString());
UE_LOG(LogTemp, Warning, TEXT("Low health: %f"), Health);
UE_LOG(LogTemp, Error, TEXT("Failed to find weapon class!"));
// Screen Messages
GEngine->AddOnScreenDebugMessage(
-1, // Key (-1 = auto)
5.0f, // Duration
FColor::Yellow, // Color
FString::Printf(TEXT("Damage Dealt: %.1f"), Damage)
);
// Profiling
DECLARE_CYCLE_STAT(TEXT("MyCharacter Tick"), STAT_MyCharacterTick, STATGROUP_Game);
void AMyCharacter::Tick(float DeltaTime)
{
SCOPE_CYCLE_COUNTER(STAT_MyCharacterTick);
Super::Tick(DeltaTime);
// Your tick code here
}
Create a complete weapon system with the following features:
// Challenge: Implement this weapon system
class MYPROJECT_API AWeapon : public AActor
{
// TODO: Add components (mesh, audio)
// TODO: Add firing mechanism
// TODO: Add reload system
// TODO: Add weapon switching
// TODO: Add network replication
};
Hints:
Your journey with Unreal Engine C++ has just begun! Here are paths to continue growing:
Remember: Unreal Engine C++ is a journey, not a destination. Each project teaches new techniques and patterns. Start small, experiment often, and don't forget to have fun creating amazing games!