
Mastering Gameplay Systems: From Mechanics to UI in Unreal Engine
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.

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;
}
}