Gameplay Mechanics Cookbook: Building the Experience

Games aren’t built on graphics; they’re built on “Game Feel.” That’s the satisfying weight of an object, the snap of a weapon firing, and the predictable logic of the world.

This cookbook isn’t just about code snippets. It’s about building Robust C++ Systems that handle the edge cases—what happens when a weapon hits a wall? How do you save the player’s progress without corrupting data? Let’s dive in.

Data-Driven Mechanics: The DataTable Advantage

Hard-coding your weapon’s damage or your player’s run speed is the fastest way to kill your project’s iteration speed. By using DataTables, you move the data out of the C++ compiler and into a format your designers can edit in real-time.

C++ Struct Implementation: By inheriting from FTableRowBase, we make our data structures compatible with Unreal’s reflection system, allowing for easy CSV import/export.

USTRUCT(BlueprintType)
struct FSettingsDataStruct : public FTableRowBase {
    GENERATED_USTRUCT_BODY()

public:
    FSettingsDataStruct()
        : ScreenResolution("1920x1080")
        , ViewDistance("Far")
        , AntiAliasing("Epic")
        , ShadowQuality("Ultra")
        , TextureQuality("Ultra")
        , VsyncQuality("on")
        , MasterVolume(1.0f)
        , MusicVolume(1.0f)
    {}

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings Data Stuct")
    FString ScreenResolution;
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings Data Stuct")
    FString ViewDistance;
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings Data Stuct")
    FString AntiAliasing;
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings Data Stuct")
    FString ShadowQuality;
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings Data Stuct")
    FString TextureQuality;
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings Data Stuct")
    FString VsyncQuality;
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings Data Stuct")
    float MasterVolume;
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings Data Stuct")
    float MusicVolume;
};

Gun

A professional weapon system is a dance between two actors: the Gun (the logic handler) and the Projectile (the physics handler).

The Trigger Logic

When you pull the trigger, the Gun actor doesn’t just subtract health. It performs a “Trace” to see what was hit, spawns visual effects (Muzzle Flash), and triggers sounds.

Gun.cpp Tip: Use LineTraceSingleByChannel. It’s the most efficient way to detect hits in a high-speed shooter.

Weapon System Implementation

Here is how you handle the actual trigger pull and weapon trace in C++.

Gun.h

public:
    void PullTrigger();
    UPROPERTY(EditAnywhere, Category = "Attack", meta = (AllowPrivateAccess = "true"))
    int DefaultAmmo;
    int Ammo;
    UPROPERTY(EditAnywhere, Category = "Attack", meta = (AllowPrivateAccess = "true"))
    float FireRate = 0.5f;
    UPROPERTY(EditAnywhere, Category = "Attack", meta = (AllowPrivateAccess = "true"))
    float FireRepeat = 1.0f;

private:
    UPROPERTY(VisibleAnywhere)
    USceneComponent* Root;
    UPROPERTY(VisibleAnywhere)
    USkeletalMeshComponent* Mesh;
    UPROPERTY(EditAnywhere, Category = "Effect", meta = (AllowPrivateAccess = "true"))
    UParticleSystem* MuzzleFlash;
    UPROPERTY(EditAnywhere, Category = "Effect", meta = (AllowPrivateAccess = "true"))
    UParticleSystem* ImpactEffect;
    UPROPERTY(EditAnywhere, Category = "Effect", meta = (AllowPrivateAccess = "true"))
    USoundBase* MuzzleSound;
    UPROPERTY(EditAnywhere, Category = "Effect", meta = (AllowPrivateAccess = "true"))
    USoundBase* ImpactSound;
    UPROPERTY(EditAnywhere, Category = "Attack", meta = (AllowPrivateAccess = "true"))
    float MaxRange = 1000;
    UPROPERTY(EditAnywhere)
    float Damage = 10;
    
    bool GunTrace(FHitResult &Hit, FVector &ShotDirection);
    AController* GetOwnerController() const;
    // ... Components (Mesh, MuzzleFlash, Sound) ...

Gun.cpp

// Sets default values
AGun::AGun()
{
    PrimaryActorTick.bCanEverTick = true;
    Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
    SetRootComponent(Root);
    Mesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mesh"));
    Mesh->SetupAttachment(Root);
}

void AGun::PullTrigger()
{
    // Effect Spawn
    UGameplayStatics::SpawnEmitterAttached(MuzzleFlash, Mesh, TEXT("MuzzleFlashSocket"));
    UGameplayStatics::SpawnSoundAttached(MuzzleSound, Mesh, TEXT("MuzzleFlashSocket"));
    
    FHitResult Hit;
    FVector ShotDirection;
    bool bSuccess = GunTrace(Hit, ShotDirection);
    
    // if distance possible create particle and LineTraceSingle at location
    if (bSuccess)
    {
        UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactEffect, Hit.Location, ShotDirection.Rotation());
        UGameplayStatics::SpawnSoundAtLocation(GetWorld(), ImpactSound, Hit.Location, ShotDirection.Rotation());
        
        AActor* HitActor = Hit.GetActor();
        if (HitActor != nullptr)
        {
            FPointDamageEvent DamageEvent(Damage, Hit, ShotDirection, nullptr);
            AController* OwnerController = GetOwnerController();
            HitActor->TakeDamage(Damage, DamageEvent, OwnerController, this);
        }
    }
}

bool AGun::GunTrace(FHitResult& Hit, FVector& ShotDirection)
{
    AController* OwnerController = GetOwnerController();
    if (!OwnerController) { return false; }
    
    FVector Location;
    FRotator Rotation;
    OwnerController->GetPlayerViewPoint(Location, Rotation);
    ShotDirection = -Rotation.Vector();
    
    // We are calculating PlayerViewPoint between Wall distance
    FVector End = Location + Rotation.Vector() * MaxRange;
    
    // We are ignoring own character
    FCollisionQueryParams Params;
    Params.AddIgnoredActor(this);
    Params.AddIgnoredActor(GetOwner());
    
    return GetWorld()->LineTraceSingleByChannel(
        Hit,
        Location,
        End,
        ECollisionChannel::ECC_GameTraceChannel1,
        Params);
}

AController* AGun::GetOwnerController() const
{
    APawn* OwnerPawn = Cast<APawn>(GetOwner());
    if (OwnerPawn == nullptr) { return nullptr; }
    return OwnerPawn->GetController();
}

Projectile

ProjectileBase.h

class UProjectileMovementComponent;

private:
    // COMPONENTS
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
    UProjectileMovementComponent* ProjectileMovement = nullptr;
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Move", meta = (AllowPrivateAccess = "true"))
    float MovementSpeed = 1300.0f;
    
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
    UStaticMeshComponent* ProjectileMesh = nullptr;
    
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
    UParticleSystemComponent* ParticleTrail = nullptr;

    // VARIABLES
    UPROPERTY(EditDefaultsOnly, Category = "Damage")
    TSubclassOf<UDamageType> DamageType;
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Damage", meta = (AllowPrivateAccess = "true"))
    float Damage = 50.0f;
    
    // Projecttile effect(behind smoke)
    UPROPERTY(EditAnywhere, Category= "Effect")
    UParticleSystem* HitParticle = nullptr;
    
    // Audio
    UPROPERTY(EditAnywhere, Category = "Effect")
    USoundBase* HitSound;
    UPROPERTY(EditAnywhere, Category = "Effect")
    USoundBase* LaunchSound;
    
    UPROPERTY(EditAnywhere, Category = "Effects")
    TSubclassOf<UCameraShake> HitShake;

    // FUNCTION
    UFUNCTION()
    void OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit);

ProjectileBase.cpp

AProjectileBase::AProjectileBase()
{
    PrimaryActorTick.bCanEverTick = false;
    ProjectileMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Projectile Mesh"));
    ProjectileMesh->OnComponentHit.AddDynamic(this, &AProjectileBase::OnHit);
    RootComponent = ProjectileMesh;
    
    ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("Projectile Movement"));
    ProjectileMovement->InitialSpeed = MovementSpeed;
    ProjectileMovement->MaxSpeed = MovementSpeed;
    
    ParticleTrail = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("Particle Trail"));
    ParticleTrail->SetupAttachment(RootComponent);
    InitialLifeSpan = 3.0f;
}

void AProjectileBase::BeginPlay()
{
    Super::BeginPlay();
    UGameplayStatics::PlaySoundAtLocation(this, LaunchSound, GetActorLocation());
}

void AProjectileBase::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit)
{
    AActor* MyOwner = GetOwner();
    if (!MyOwner) { return; }
    
    if (OtherActor && OtherActor != this && OtherActor != MyOwner)
    {
        UGameplayStatics::ApplyDamage(
            OtherActor,
            Damage,
            MyOwner->GetInstigatorController(),
            this,
            DamageType
        );
        
        // When hitted object, It will create hitparticle and audio.
        UGameplayStatics::SpawnEmitterAtLocation(this, HitParticle, GetActorLocation());
        UGameplayStatics::PlaySoundAtLocation(this, HitSound, GetActorLocation());
        GetWorld()->GetFirstPlayerController()->ClientPlayCameraShake(HitShake);
        Destroy();
    }
}

Spawner

A basic timed spawner is one of the most reusable gameplay building blocks: set a repeating timer in BeginPlay, compute a randomized spawn transform, then call SpawnActor.

Spawner.h

private:
    void Spawn();
    void CalculateSpawn();

    FTimerHandle SpawnTimer;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawn Settings", meta = (AllowPrivateAccess = "true"))
    FVector SpawnerLocation;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawn Settings", meta = (AllowPrivateAccess = "true"))
    FRotator SpawnerRotation;

    // Spawn Object
    UPROPERTY(EditAnywhere, Category = "Spawner Settings")
    TSubclassOf<AActor> ActorToSpawn;

    // Spawner Settings
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawner Settings", meta = (AllowPrivateAccess = "true"))
    float SpawnSpeed = 0.5f;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawner Settings", meta = (AllowPrivateAccess = "true"))
    float SpawnRepeat = 1.0f;

    // Spawn Location and Rotation
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawner Settings", meta = (AllowPrivateAccess = "true"))
    float SpawnMinZ = -30.0f;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawner Settings", meta = (AllowPrivateAccess = "true"))
    float SpawnMaxZ = 300.0f;

Spawner.cpp

#include "Engine/World.h"
#include "TimerManager.h"

void ASpawner::BeginPlay()
{
    Super::BeginPlay();
    GetWorldTimerManager().SetTimer(SpawnTimer, this, &ASpawner::Spawn, SpawnRepeat, true, SpawnSpeed);
}

void ASpawner::Spawn()
{
    CalculateSpawn();
    GetWorld()->SpawnActor<AActor>(ActorToSpawn, SpawnerLocation, SpawnerRotation);
}

void ASpawner::CalculateSpawn()
{
    SpawnerLocation = GetActorLocation();
    SpawnerLocation.Z = GetActorLocation().Z - FMath::RandRange(SpawnMinZ, SpawnMaxZ);
    SpawnerRotation = GetActorRotation();
}

Spawner V2

Our Pattern: We use a SpawnSettings struct. This allows us to define “Spawn Direction” as an Enum (X, Y, or Z), giving designers a clear dropdown menu in the editor instead of making them type raw vectors.

Creating a spawner that can define direction, randomness, and attached attributes (like logic for a “FireObstacle”) requires a structured approach.

SpawnerV2.h (Structs & Enums)

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Engine/TriggerVolume.h"
#include "FireObstacleSpawner.generated.h"
// Direction Choice
UENUM(BlueprintType)
enum ESpawnDirection
{
    SpawnDirectionX UMETA(Display = "X direction"),
    SpawnDirectionY UMETA(Display = "Y direction"),
    SpawnDirectionZ UMETA(Display = "Z direction")
};
// Choice for every array elements.
USTRUCT(BlueprintType)
struct CONTRA_API FSpawnSettings
{
    GENERATED_BODY()
public:
    UPROPERTY(EditAnywhere)
    TEnumAsByte<ESpawnDirection> SpawnDirection;
    // Min-Max value for Spawn Location and Rotation
    UPROPERTY(EditAnywhere)
    float MinSpawnLocation;
    UPROPERTY(EditAnywhere)
    float MaxSpawnLocation;
    // - or + Roll value
    UPROPERTY(EditAnywhere)
    float SpawnRotation;
};
class AContraCharacter;

UCLASS()
class CONTRA_API AFireObstacleSpawner : public APawn
{
    GENERATED_BODY()
public:
    // Sets default values for this actor's properties
    AFireObstacleSpawner();
    virtual void Tick(float DeltaSeconds) override;
protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;
    virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
private:
    // Function
    void ObstacleSpawn(); // creating actor
    void SetSpawnTimer(); // calling timer
    void SpawnChoice(); // defines values.
    void BacktoNormal(); // Slow motion
    bool IsDead() const;
    // Spawner Main Settings
    UPROPERTY(EditDefaultsOnly)
    UStaticMeshComponent* SpawnerMesh = nullptr;
    // Cast Main Player
    AContraCharacter* ContraCharacter = nullptr;
    // Spawn Object
    UPROPERTY(EditAnywhere, Category = "Main Settings")
    TSubclassOf<AActor> ActorToSpawn;
    FTimerHandle SpawnTimer;
    // Info from struct
    UPROPERTY(EditAnywhere, meta = (TitleProperty = "Spawner Settings"))
    TArray<struct FSpawnSettings> SpawnSetting;
    // Spawner temp value for set the actor.
    FVector SpawnerLocation;
    FRotator SpawnerRotation;
    // Trigger Volume for the ActorMovement
    UPROPERTY(EditAnywhere, Category = "Spawner Settings")
    ATriggerVolume* Trigger = nullptr;
    // Spawner Settings
    UPROPERTY(EditAnywhere, Category = "Spawner Settings")
    float SpawnSpeed = 0.5f;
    UPROPERTY(EditAnywhere, Category = "Spawner Settings")
    float SpawnRepeat = 1.0f;
    UPROPERTY(EditAnywhere, Category = "Spawner Settings")
    float SpawnedMovementSpeed = 300.0f;
    UPROPERTY(EditAnywhere, Category = "Spawner Settings")
    float SpawnedDeadTime = 10.0f; // Spawned obstacle destroy time
    int i = 0; // array start value
    bool IsWork = true;
    // Slow Motion
    bool SlowLogic = false;
    FTimerHandle BacktoNormalTimer;
    float TimerValue = 0.01;
    // Health
    bool ReturnGameModeDead = true;
    UPROPERTY(EditDefaultsOnly)
    float MaxHealth = 100.0f;
    UPROPERTY(VisibleAnywhere)
    float Health;
    // Effect Choice
    UPROPERTY(EditAnywhere, Category = "Effects", meta = (AllowPrivateAccess = "true"))
    UParticleSystem* DeathParticle = nullptr;
    UPROPERTY(EditAnywhere, Category = "Effects")
    USoundBase* DeathSound;
    // for detect attached actor or static meshes
    TArray<AActor*> Attached;
    UPROPERTY(EditAnywhere)
    bool DestroyWAttachment = false;
};

Spawner Logic (Cpp)

// ... Constructor and BeginPlay ...

void AFireObstacleSpawner::Tick(float DeltaSeconds)
{
    // if player goes out of the trigger, timer will stop (spawnner).
    if (Trigger && Trigger->IsOverlappingActor(ContraCharacter) && IsWork)
    {
        SetSpawnTimer();
        IsWork = false;
    }
    else if (Trigger && !Trigger->IsOverlappingActor(ContraCharacter))
    {
        GetWorldTimerManager().PauseTimer(SpawnTimer);
        IsWork = true;
    }
}

void AFireObstacleSpawner::ObstacleSpawn() {
    // Repeating array 0 to max and restarting.
    if (i < SpawnSetting.Num()) {
        SpawnChoice();
        i++;
    } else {
        i = 0;
        SpawnChoice();
    }

    AFireObstacle* SpawnedActor = GetWorld()->SpawnActor<AFireObstacle>(ActorToSpawn, SpawnerLocation, SpawnerRotation);
    
    // Pass logic to child
    if (SpawnedActor && SpawnedActor->ActorMovement && Trigger) {
        SpawnedActor->DeadDelay = SpawnnedDeadTime;
        SpawnedActor->ActorMovement->Direction = DirectionX;
        SpawnedActor->ActorMovement->PressPlate = Trigger;
        SpawnedActor->ActorMovement->TransporterForwardSpeed = SpawnnedMovementSpeed;
        SpawnnedActor->FireObstacleSpawner = this;
    }
}

void AFireObstacleSpawner::SpawnChoice()
{
    // It's giving random value to each array elements and repeats every time we call this function.
    switch (SpawnSetting[i].SpawnDirection)
    {
        case SpawnDirectionZ:
            SpawnerLocation = GetActorLocation();
            SpawnerLocation.Z = GetActorLocation().Z - FMath::RandRange(SpawnSetting[i].MinSpawnLocation, SpawnSetting[i].MaxSpawnLocation);
            SpawnerRotation = FRotator(0, 0, SpawnSetting[i].SpawnRotation);
            break;
        case SpawnDirectionY:
            SpawnerLocation = GetActorLocation();
            SpawnerLocation.Y = GetActorLocation().Y - FMath::RandRange(SpawnSetting[i].MinSpawnLocation, SpawnSetting[i].MaxSpawnLocation);
            SpawnerRotation = FRotator(0, 0, SpawnSetting[i].SpawnRotation);
            break;
        case SpawnDirectionX:
            SpawnerLocation = GetActorLocation();
            SpawnerLocation.X = GetActorLocation().X - FMath::RandRange(SpawnSetting[i].MinSpawnLocation, SpawnSetting[i].MaxSpawnLocation);
            SpawnerRotation = FRotator(0, 0, SpawnSetting[i].SpawnRotation);
            break;
    }
}

float AFireObstacleSpawner::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
    float DamageToApply = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
    DamageToApply = FMath::Min(Health, DamageToApply);
    Health -= DamageToApply;

    // when health was less than 10, it will set start slow motion value and call function.
    if (Health < 10)
    {
        SlowLogic = true;
        UGameplayStatics::SetGlobalTimeDilation(GetWorld(), TimerValue);
        BacktoNormal();
    }
    
    if (IsDead())
    {
        // Death logic (Effects, Score, Destroy)
        UGameplayStatics::SpawnEmitterAtLocation(this, DeathParticle, GetActorLocation());
        UGameplayStatics::SpawnSoundAtLocation(this, DeathSound, GetActorLocation());
        Destroy();
    }
    return DamageToApply;
}

void AFireObstacleSpawner::BacktoNormal()
{
    GetWorldTimerManager().SetTimer(BacktoNormalTimer, this, &AFireObstacleSpawner::BacktoNormal, 2, true, 0.1f);
    if (SlowLogic)
    {
        if (TimerValue >= 0.5f && Health <= 0)
        {
            TimerValue = 1.0f;
            UGameplayStatics::SetGlobalTimeDilation(this, TimerValue);
            GetWorldTimerManager().ClearTimer(BacktoNormalTimer);
        }
        else
        {
            TimerValue += 0.1;
            UGameplayStatics::SetGlobalTimeDilation(this, TimerValue);
        }
    }
}

Persistent Progress: Save and Load

Unreal’s USaveGame class is the foundation for persistence. By creating a custom SaveGame class and using UGameplayStatics::SaveGameToSlot, you can store any serializable variable (integers, strings, structs) to a binary file on the user’s device. This ensures that when the player returns, their progress stays right where they left it.

YourSaveGameClass.h

#pragma once
#include "CoreMinimal.h"
#include "SettingsDataStruct.h"
#include "GameFramework/SaveGame.h"
#include "YourSaveGame.generated.h"

UCLASS()
class YOURGAME_API UYourSaveGame : public USaveGame
{
    GENERATED_BODY()
    
public:
    UYourSaveGame();

    UPROPERTY(VisibleAnywhere, Category = "Save Game Data")
    FSettingsDataStruct* SettingsData;
};

YourSettingsWidgetClass.cpp

void MyClass::LoadSettings()
{
    if (SettingsDataTable)
    {
        SaveGameData = Cast<UYourSaveGame>(UGameplayStatics::LoadGameFromSlot("YourGameSlot", 0));

        if (SaveGameData != nullptr)
        {
            // retrieve configuration data from data table
            FString ContextString;
            SaveGameData->SettingsData = SettingsDataTable->FindRow<FSettingsDataStruct>("ConfigData", ContextString);
        }
        else
        {
            // couldn't load save game
            SaveGameData = Cast<UYourSaveGame>(UGameplayStatics::CreateSaveGameObject(UYourSaveGame::StaticClass()));

            FString ContextString;
            SaveGameData->SettingsData = SettingsDataTable->FindRow<FSettingsDataStruct>("ConfigData", ContextString);
        }
    }
}

void MyClass::SaveSettings()
{
    if (SaveGameData)
    {
        // Example: updating Vsync
        SaveGameData->SettingsData->VsyncQuality = "on";
        UGameplayStatics::SaveGameToSlot(SaveGameData, "YourGameSlot", 0);
    }
}

Movable Platform

A reusable ActorMovement component can turn any Static Mesh into a moving platform, door, or trap.

ActorMovement.h

#include "Components/ActorComponent.h"
#include "Engine/TriggerVolume.h"

UENUM()
enum EPlatformDirection
{
    DirectionX UMETA(Display = "X direction"),
    DirectionY UMETA(Display = "Y direction"),
    DirectionZ UMETA(Display = "Z direction")
};

UCLASS()
class CONTRA_API UActorMovement : public UActorComponent {
    GENERATED_BODY()
private:
    UPROPERTY(EditAnywhere)
    ATriggerVolume* PressPlate = nullptr;
    UPROPERTY(EditAnywhere)
    UAudioComponent* AudioComponent = nullptr;
    UPROPERTY(EditAnywhere)
    AActor* ActorThatOpen;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category= "Direction", meta = (AllowPrivateAccess = "true"))
    TEnumAsByte<EPlatformDirection> Direction;
    float DirectionValue;

    FVector Backward;
    FVector Forward;
    float TransporterForwardLast = 0.0f;

    UPROPERTY(EditAnywhere)
    float TransporterForwardSpeed = 200.0f;
    UPROPERTY(EditAnywhere)
    float TransportDelay = 5.0f;
    float TransporterInitial;
    float TransporterCurrent;
    UPROPERTY(EditAnywhere)
    float TransporterTarget = 3000;

    bool ForwardSound = false;
    bool BackwardSound = true;
    
public:
    UActorMovement();
    virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
    void ForwardTransporter(float DeltaTime);
    void BackwardTransporter(float DeltaTime);
    void TransporterLogic(float DeltaTime);
    void DirectionChoice();
    void FindPressPlate();
    void FindAudioComponent();
};

ActorMovement.cpp

void UActorMovement::BeginPlay()
{
    Super::BeginPlay();
    DirectionChoice();
    TransporterInitial = DirectionValue;
    TransporterCurrent = TransporterInitial;
    TransporterTarget = TransporterInitial + TransporterTarget;
    
    FindPressPlate();
    FindAudioComponent();
    ActorThatOpen = GetWorld()->GetFirstPlayerController()->GetPawn();
}

void UActorMovement::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
    TransporterLogic(DeltaTime);
}

void UActorMovement::DirectionChoice()
{
    switch (Direction)
    {
        case DirectionX:
            DirectionValue = GetOwner()->GetActorLocation().X;
            Forward = GetOwner()->GetActorLocation(); Forward.X = TransporterCurrent;
            Backward = GetOwner()->GetActorLocation(); Backward.X = TransporterCurrent;
            break;
        case DirectionY:
            DirectionValue = GetOwner()->GetActorLocation().Y;
            Forward = GetOwner()->GetActorLocation(); Forward.Y = TransporterCurrent;
            Backward = GetOwner()->GetActorLocation(); Backward.Y = TransporterCurrent;
            break;
        case DirectionZ:
            DirectionValue = GetOwner()->GetActorLocation().Z;
            Forward = GetOwner()->GetActorLocation(); Forward.Z = TransporterCurrent;
            Backward = GetOwner()->GetActorLocation(); Backward.Z = TransporterCurrent;
            break;
        default:
             DirectionValue = GetOwner()->GetActorLocation().Z;
             break;
    }
}

void UActorMovement::TransporterLogic(float DeltaTime) {
    if (PressPlate && PressPlate->IsOverlappingActor(ActorThatOpen)) {
        if (TransporterCurrent != TransporterTarget) {
            ForwardTransporter(DeltaTime);
            TransporterForwardLast = GetWorld()->GetTimeSeconds();
        }
    }
    else if (PressPlate && !PressPlate->IsOverlappingActor(ActorThatOpen)) {
        if (GetWorld()->GetTimeSeconds() - TransporterForwardLast > TransportDelay) {
            BackwardTransporter(DeltaTime);
        }
    }
}

void UActorMovement::ForwardTransporter(float DeltaTime) {
    TransporterCurrent = FMath::FInterpConstantTo(TransporterCurrent, TransporterTarget, DeltaTime, TransporterForwardSpeed);
    DirectionChoice();
    GetOwner()->SetActorLocation(Forward);
    
    BackwardSound = false;
    if (!AudioComponent) { return; }
    if (!ForwardSound) {
        AudioComponent->Play();
        ForwardSound = true;
    }
}

void UActorMovement::BackwardTransporter(float DeltaTime) {
    TransporterCurrent = FMath::FInterpConstantTo(TransporterCurrent, TransporterInitial, DeltaTime, TransporterForwardSpeed);
    DirectionChoice();
    GetOwner()->SetActorLocation(Backward);
    
    ForwardSound = false;
    if (!AudioComponent) { return; }
    if (!BackwardSound) {
        AudioComponent->Play();
        BackwardSound = true;
    }
}

Open Next Level

A clean Blueprint pattern for level progression is to keep a levels string array and advance an index after a win condition, trigger overlap, or mission completion.

Blueprint: level array + OpenLevel flow for advancing to the next map


Draw Debug Line

When you’re using traces (line traces, sphere traces), debug lines are the fastest way to verify your math.

#include "DrawDebugHelpers.h"

DrawDebugLine(
    GetWorld(),
    StartLocation,
    EndLocation,
    FColor(0, 255, 0),
    false,
    0.0f, // lifetime (0 = single frame)
    0,    // depth priority
    5.0f  // thickness
);

Rotate Turret (2 Methods)

Method 1: Fixed Axis

void APawnBase::RotateTurret(FVector LookAtTarget)
{
    // Only can rotate left or right.
    FVector LookAtTargetCleaned = FVector(LookAtTarget.X, LookAtTarget.Y, TurretMesh->GetComponentLocation().Z);
    FVector StartLocation = TurretMesh->GetComponentLocation();
    
    FRotator TurretRotation = FVector(LookAtTargetCleaned - StartLocation).Rotation();
    TurretMesh->SetWorldRotation(TurretRotation);
}

Method 2: Interpolated Rotation

void APawnBase::RotateTurret(FVector LookAtTarget)
{
    FRotator RotationCalculate = UKismetMathLibrary::FindLookAtRotation(TurretMesh->GetComponentLocation(), LookAtTarget);
    FRotator TurretRotation = FMath::RInterpTo(TurretMesh->GetComponentRotation(), RotationCalculate, GetWorld()->GetDeltaSeconds(), RotateSpeed);
    TurretMesh->SetWorldRotation(TurretRotation);
}

Health Box

A classic pickup actor that handles collision, particle effects, and gameplay logic (healing).

HealthBox.h

// Fill out your copyright notice in the Description page of Project Settings.
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "HealthBox.generated.h"

UCLASS()
class CONTRA_API AHealthBox : public AActor
{
    GENERATED_BODY()
public:
    AHealthBox();

protected:
    virtual void BeginPlay() override;

    UFUNCTION()
    void OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit);

private:
    void FindPointer();
    
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category= "Components",meta = (AllowPrivateAccess = true))
    UStaticMeshComponent* HealthBoxMesh;
    
    // Effect System
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category= "Effects", meta = (AllowPrivateAccess = true))
    UParticleSystem* GiveHealthParticle = nullptr;
    
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category= "Effects", meta = (AllowPrivateAccess = true))
    USoundBase* GiveHealthSound = nullptr;
    
    // Healthbox giving this amounth health percent.
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category= "Components",meta = (AllowPrivateAccess = true))
    float GiveHealthValue = 10.0f;
};

HealthBox.cpp

AHealthBox::AHealthBox() {
    PrimaryActorTick.bCanEverTick = false;
    HealthBoxMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("HealthBox Mesh"));
    HealthBoxMesh->OnComponentHit.AddDynamic(this, &AHealthBox::OnHit);
    RootComponent = HealthBoxMesh;
}

void AHealthBox::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, ...) {
    AContraCharacter* ContraCharacter = Cast<AContraCharacter>(OtherActor);
    if (!ContraCharacter) { return; }

    if(ContraCharacter && OtherActor != this) {
        ContraCharacter->Health += GiveHealthValue;
        FindPointer();
        UGameplayStatics::SpawnEmitterAtLocation(this, GiveHealthParticle, GetActorLocation());
        UGameplayStatics::SpawnSoundAtLocation(this, GiveHealthSound, GetActorLocation());
        Destroy();
    }
}
void AHealthBox::FindPointer()
{
    if (!GiveHealthSound) { return; }
    if (!GiveHealthParticle) { return; }
}

Grab

Implementing physics-based object manipulation similar to Half-Life or Portal.

Grabber.h

#include "CoreMinimal.h"
#include "Components/SceneComponent.h"
#include "PhysicsEngine/PhysicsHandleComponent.h"
#include "Grabber.generated.h"

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class CONTRA_API UGrabber : public USceneComponent
{
    GENERATED_BODY()

public:	
    UGrabber();
    virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
    
    void Grab();
    void Release();

protected:
    virtual void BeginPlay() override;
    
    UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
    void NotifyQuestActor(AActor* Actor);

private:
    void Grabbed();
    FVector GetMaxGrabLocation() const;
    FVector GetHoldLocation() const;
    
    UFUNCTION(BlueprintCallable, BlueprintPure)
    UPhysicsHandleComponent* GetPhysicsComponent() const;
    
    FHitResult GetFirstPhysicsBodyInReach() const;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
    float MaxGrabDistance = 100;
    
    UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
    float HoldDistance = 100;
    
    class AFirstPersonCharacter* FirstPersonCharacter = nullptr;
};

Grabber.cpp

void UGrabber::Grab() {
    FHitResult HitResult = GetFirstPhysicsBodyInReach();
    UPrimitiveComponent* ComponentToGrab = HitResult.GetComponent();
    AActor* ActorHit = HitResult.GetActor();

    if (ActorHit && ComponentToGrab) {
        if (!GetPhysicsComponent()) return;
        
        GetPhysicsComponent()->GrabComponentAtLocationWithRotation(
            ComponentToGrab,
            NAME_None,
            ComponentToGrab->GetCenterOfMass(),
            FRotator()
        );
        NotifyQuestActor(ActorHit);
    }
}

void UGrabber::TickComponent(float DeltaTime, ...) {
    Super::TickComponent(DeltaTime, ...);
    Grabbed();
}

void UGrabber::Grabbed()
{
    if (!GetPhysicsComponent()) { return; }
    if (GetPhysicsComponent()->GrabbedComponent)
    {
        GetPhysicsComponent()->SetTargetLocation(GetMaxGrabLocation());
    }
}

Obstacle

An actor that deals damage when touched, commonly used in platformers.

Obstacle.h

#include "CoreMinimal.h"
#include "FireObstacleSpawner.h"
#include "GameFramework/Actor.h"
#include "NiagaraComponent.h"
#include "FireObstacle.generated.h"

class UActorMovement;
class AContraCharacter;

UCLASS()
class CONTRA_API AFireObstacle : public AActor
{
    GENERATED_BODY()
public:
    AFireObstacle();
    virtual void Tick(float DeltaSeconds) override;

    UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "Components")
    AFireObstacleSpawner* FireObstacleSpawner = nullptr;
    
    UPROPERTY(EditAnywhere)
    UActorMovement* ActorMovement = nullptr;
    
    UPROPERTY(EditAnywhere)
    float DeadDelay = 10.0f;

private:
    void ObstacleHit(); 
    void EffectHitStart(); 
    void ObstacleDestroy();

    USceneComponent* SceneComponent = nullptr;
    
    UPROPERTY(EditDefaultsOnly, Category= "Effect")
    UStaticMeshComponent* EffectStart = nullptr;
    
    UPROPERTY(EditDefaultsOnly, Category= "Effect")
    UStaticMeshComponent* EffectEnd = nullptr;
    
    UPROPERTY(EditDefaultsOnly, Category= "Effect")
    UNiagaraComponent* FireEffect = nullptr;
    
    FName EffectName = "User.BeamEnd";
    
    AContraCharacter* ContraCharacter = nullptr;
    FTimerHandle DeadTimer;
protected:
    virtual void BeginPlay() override;
};

Obstacle.cpp

// ... Constructor and BeginPlay ...
void AFireObstacle::EffectHitStart() {
    FireEffect->SetVectorParameter(EffectName, EffectStart->GetComponentLocation());

    FHitResult Hit;
    FCollisionQueryParams TraceParams;
    
    GetWorld()->LineTraceSingleByObjectType(
        OUT Hit,
        EffectStart->GetComponentLocation(),
        EffectEnd->GetComponentLocation(),
        ECC_Pawn,
        TraceParams
    );

    if (Hit.GetActor() && Hit.GetActor()->ActorHasTag("Player")) {
        ObstacleHit();
    }
}

void AFireObstacle::ObstacleHit() {
    if (ContraCharacter) {
        ContraCharacter->Health -= 0.5f; 
    }
}