praca_magisterska/games/unreal/tutorial/code-first-approach.md
copilot-swe-agent[bot] 3a97e8dbfa Add C++ code-first tutorial approach for Unreal game
Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>
2026-01-07 21:38:18 +00:00

24 KiB
Raw Blame History

Code-First Approach: Building the Bullet Hell Game in C++

Back to Index


Why Use the Code-First Approach?

The Blueprint-heavy tutorial requires extensive manual UI work - clicking through dozens of variable properties, typing values one by one, and repeating the process for every component. This is:

  • Time-consuming: Each variable requires 5-10 clicks to configure
  • Error-prone: Easy to mistype values or select wrong types
  • Hard to replicate: Difficult to copy settings between projects
  • Poor for version control: Blueprint files are binary and hard to diff/merge

The code-first approach solves these problems by defining all game logic, variables, and configurations in C++ header and source files. You can:

Copy-paste entire variable blocks instead of typing one-by-one
Version control with readable diffs using standard Git tools
Refactor with IDE tools (rename, find references, etc.)
Compile-time type checking prevents common mistakes
Create Blueprints that inherit from C++ classes for visual-only assets


Overview: Implementation Strategy

We will create C++ base classes for all game logic:

  1. ASTGPawn - Player ship with movement, firing, lives
  2. ASTGProjectile - Bullets (player and enemy)
  3. ASTGEnemy - Enemy ships with AI and firing patterns
  4. ASTGGameMode - Game rules, spawning, timer
  5. ASTGHUD - UI display for score, lives, timer

Then create minimal Blueprint classes that inherit from these C++ classes, used only for:

  • Assigning visual meshes/sprites
  • Setting material colors
  • Configuring asset references (sound effects, particle effects)

Part 1: Project Setup

Step 1.1: Create C++ Project in Unreal

  1. Open Unreal Engine 5
  2. Create New Project
  3. Select Games → Blank template
  4. Project Defaults:
    • Project Type: C++ (NOT Blueprint!)
    • Target Platform: Desktop
    • Quality Preset: Scalable
    • Starter Content: No Starter Content
  5. Name: BulletHellGame
  6. Click Create

Unreal will:

  • Generate C++ project structure
  • Open Visual Studio / VS Code / Rider
  • Compile initial project code (~2-5 minutes)

Step 1.2: Verify Project Structure

Your project folder should contain:

BulletHellGame/
├── BulletHellGame.uproject       # Project file
├── Source/                       # C++ source code
│   └── BulletHellGame/
│       ├── BulletHellGame.h
│       ├── BulletHellGame.cpp
│       ├── BulletHellGame.Build.cs
│       └── BulletHellGameGameMode.h/cpp  # Auto-generated
├── Content/                      # Assets (minimal Blueprints here)
├── Config/                       # Project settings
└── Intermediate/                 # Build artifacts (gitignore this)

Step 1.3: Configure .gitignore

Add to .gitignore:

# Unreal Engine
Binaries/
DerivedDataCache/
Intermediate/
Saved/
*.sln
*.suo
*.opensdf
*.sdf
*.VC.db
*.VC.opendb

Part 2: Create the Player (C++ Implementation)

Step 2.1: Create STGPawn Header

Create Source/BulletHellGame/STGPawn.h:

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "STGPawn.generated.h"

class UCameraComponent;
class USpringArmComponent;
class UStaticMeshComponent;
class UBoxComponent;

UCLASS()
class BULLETHELLGAME_API ASTGPawn : public APawn
{
    GENERATED_BODY()

public:
    ASTGPawn();

protected:
    virtual void BeginPlay() override;

public:    
    virtual void Tick(float DeltaTime) override;
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

    // ===== COMPONENTS =====
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
    UStaticMeshComponent* ShipMesh;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
    USpringArmComponent* SpringArm;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
    UCameraComponent* Camera;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
    UBoxComponent* Hitbox;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
    UStaticMeshComponent* HitboxIndicator;

    // ===== GAMEPLAY VARIABLES (All in one place - easy copy/paste!) =====
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float MoveSpeed = 750.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    FVector2D BoundsMin = FVector2D(-850.0f, -450.0f);

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    FVector2D BoundsMax = FVector2D(850.0f, 450.0f);

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float FireInterval = 0.08f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float BulletSpeed = 2200.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    int32 MaxLives = 3;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Stats")
    int32 CurrentLives = 3;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    int32 VolleySize = 3;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float VolleySpread = 12.0f;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Stats")
    bool bSpecialUsed = false;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Stats")
    int32 Score = 0;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float FireRate = 0.08f;

    // ===== INPUT FUNCTIONS =====
    void MoveForward(float Value);
    void MoveRight(float Value);
    void StartFire();
    void StopFire();
    void UseSpecial();

    // ===== GAME LOGIC =====
    void FireShot();
    void TakeHit(int32 Damage);
    void HandleDeath();
    void AddScore(int32 Points);

private:
    FTimerHandle TimerHandle_Fire;
    bool bIsFiring = false;
    FVector MovementInput;
    float FireTimer = 0.0f;
};

Key advantages of this approach:

  1. All 12 player variables defined in ~12 lines (vs 60+ clicks in Blueprint UI)
  2. Default values inline (= 750.0f) - no need to set after compile
  3. Copy-paste friendly - duplicate variables easily
  4. Version control friendly - see exact changes in Git diff
  5. Type-safe - compiler catches type mismatches

Step 2.2: Create STGPawn Implementation

Create Source/BulletHellGame/STGPawn.cpp:

#include "STGPawn.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"
#include "Kismet/GameplayStatics.h"
#include "STGProjectile.h"

ASTGPawn::ASTGPawn()
{
    PrimaryActorTick.bCanEverTick = true;

    // Root component
    RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));

    // Ship mesh - cone shape pointing upward
    ShipMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ShipMesh"));
    ShipMesh->SetupAttachment(RootComponent);
    ShipMesh->SetCollisionProfileName("NoCollision");
    
    // Load cone mesh
    static ConstructorHelpers::FObjectFinder<UStaticMesh> ConeMesh(TEXT("/Engine/BasicShapes/Cone"));
    if (ConeMesh.Succeeded())
    {
        ShipMesh->SetStaticMesh(ConeMesh.Object);
        ShipMesh->SetRelativeScale3D(FVector(0.5f, 0.5f, 0.7f));
        ShipMesh->SetRelativeRotation(FRotator(90.f, 0.f, 0.f));
    }

    // Hitbox - tiny for bullet-hell precision
    Hitbox = CreateDefaultSubobject<UBoxComponent>(TEXT("Hitbox"));
    Hitbox->SetupAttachment(RootComponent);
    Hitbox->SetBoxExtent(FVector(25.f, 25.f, 10.f));
    Hitbox->SetCollisionProfileName("OverlapAllDynamic");
    Hitbox->SetGenerateOverlapEvents(true);

    // Visual hitbox indicator
    HitboxIndicator = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("HitboxIndicator"));
    HitboxIndicator->SetupAttachment(RootComponent);
    HitboxIndicator->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    
    static ConstructorHelpers::FObjectFinder<UStaticMesh> SphereMesh(TEXT("/Engine/BasicShapes/Sphere"));
    if (SphereMesh.Succeeded())
    {
        HitboxIndicator->SetStaticMesh(SphereMesh.Object);
        HitboxIndicator->SetRelativeScale3D(FVector(0.05f, 0.05f, 0.05f));
    }

    // Camera setup
    SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
    SpringArm->SetupAttachment(RootComponent);
    SpringArm->SetRelativeRotation(FRotator(-90.f, 0.f, 0.f)); // Top-down view
    SpringArm->TargetArmLength = 1200.f;
    SpringArm->bDoCollisionTest = false;
    SpringArm->bInheritPitch = false;
    SpringArm->bInheritRoll = false;
    SpringArm->bInheritYaw = false;

    Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
    Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName);
}

void ASTGPawn::BeginPlay()
{
    Super::BeginPlay();
    CurrentLives = MaxLives;
}

void ASTGPawn::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    // Movement
    if (!MovementInput.IsZero())
    {
        FVector NewLocation = GetActorLocation();
        FVector Normalized = MovementInput.GetSafeNormal();
        NewLocation += Normalized * MoveSpeed * DeltaTime;

        // Clamp to bounds
        NewLocation.X = FMath::Clamp(NewLocation.X, BoundsMin.X, BoundsMax.X);
        NewLocation.Y = FMath::Clamp(NewLocation.Y, BoundsMin.Y, BoundsMax.Y);

        SetActorLocation(NewLocation);
    }

    // Auto-fire
    if (bIsFiring)
    {
        FireTimer -= DeltaTime;
        if (FireTimer <= 0.0f)
        {
            FireShot();
            FireTimer = FireInterval;
        }
    }
}

void ASTGPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);

    // Bind movement axes
    PlayerInputComponent->BindAxis("MoveForward", this, &ASTGPawn::MoveForward);
    PlayerInputComponent->BindAxis("MoveRight", this, &ASTGPawn::MoveRight);

    // Bind actions
    PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &ASTGPawn::StartFire);
    PlayerInputComponent->BindAction("Fire", IE_Released, this, &ASTGPawn::StopFire);
    PlayerInputComponent->BindAction("Special", IE_Pressed, this, &ASTGPawn::UseSpecial);
}

void ASTGPawn::MoveForward(float Value)
{
    MovementInput.X = Value;
}

void ASTGPawn::MoveRight(float Value)
{
    MovementInput.Y = Value;
}

void ASTGPawn::StartFire()
{
    bIsFiring = true;
    FireTimer = 0.0f; // Fire immediately
}

void ASTGPawn::StopFire()
{
    bIsFiring = false;
}

void ASTGPawn::FireShot()
{
    // Spawn volley of bullets
    for (int32 i = 0; i < VolleySize; i++)
    {
        FVector SpawnLocation = GetActorLocation() + FVector(0.f, 0.f, 50.f);
        FRotator SpawnRotation = FRotator(90.f, 0.f, 0.f);

        // Spread calculation
        float Angle = VolleySpread * (i - (VolleySize - 1) / 2.0f);
        SpawnRotation.Yaw += Angle;

        // Spawn bullet (will create this class next)
        UWorld* World = GetWorld();
        if (World)
        {
            ASTGProjectile* Bullet = World->SpawnActor<ASTGProjectile>(ASTGProjectile::StaticClass(), SpawnLocation, SpawnRotation);
            if (Bullet)
            {
                Bullet->bIsPlayerBullet = true;
                Bullet->SetSpeed(BulletSpeed);
                Bullet->SetBulletColor(FLinearColor::Green);
            }
        }
    }
}

void ASTGPawn::UseSpecial()
{
    if (!bSpecialUsed)
    {
        bSpecialUsed = true;
        
        // Destroy all enemies and bullets on screen
        TArray<AActor*> FoundEnemies;
        UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASTGEnemy::StaticClass(), FoundEnemies);
        for (AActor* Enemy : FoundEnemies)
        {
            Enemy->Destroy();
        }

        TArray<AActor*> FoundBullets;
        UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASTGProjectile::StaticClass(), FoundBullets);
        for (AActor* Bullet : FoundBullets)
        {
            ASTGProjectile* Projectile = Cast<ASTGProjectile>(Bullet);
            if (Projectile && !Projectile->bIsPlayerBullet)
            {
                Projectile->Destroy();
            }
        }
    }
}

void ASTGPawn::TakeHit(int32 Damage)
{
    CurrentLives = FMath::Clamp(CurrentLives - Damage, 0, MaxLives);
    
    if (CurrentLives <= 0)
    {
        HandleDeath();
    }
}

void ASTGPawn::HandleDeath()
{
    SetActorHiddenInGame(true);
    
    // Notify GameMode
    ASTGGameMode* GameMode = Cast<ASTGGameMode>(UGameplayStatics::GetGameMode(GetWorld()));
    if (GameMode)
    {
        GameMode->OnPlayerDied();
    }
}

void ASTGPawn::AddScore(int32 Points)
{
    Score += Points;
}

Notice how much easier this is:

  • Movement logic in ~10 lines vs 30+ Blueprint nodes
  • Fire logic with volley calculation - would be complex in Blueprints
  • Easy to copy patterns between functions
  • IDE autocomplete helps prevent typos

Step 2.3: Configure Input Mappings

You still need to configure input in Project Settings (this can't be done in code):

  1. Edit → Project Settings
  2. Engine → Input
  3. Axis Mappings:
    • MoveForward: W = 1.0, S = -1.0
    • MoveRight: D = 1.0, A = -1.0
  4. Action Mappings:
    • Fire: Space, Left Mouse Button
    • Special: X, Right Mouse Button

This is a one-time setup - all other configuration is in code.

Step 2.4: Compile and Test

  1. In Unreal Editor: Click Compile button (or close editor)
  2. Build in IDE: Build Solution / Compile Project
  3. Expected compile time: ~30-60 seconds (vs instant in Blueprint, but you save hours on UI work)
  4. Open Unreal Editor (if you closed it)
  5. Content Browser → C++ Classes → BulletHellGame - you should see STGPawn

Part 3: Create the Projectile (C++ Implementation)

Step 3.1: Create STGProjectile Header

Create Source/BulletHellGame/STGProjectile.h:

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "STGProjectile.generated.h"

class USphereComponent;
class UProjectileMovementComponent;
class UStaticMeshComponent;

UCLASS()
class BULLETHELLGAME_API ASTGProjectile : public AActor
{
    GENERATED_BODY()
    
public:    
    ASTGProjectile();

protected:
    virtual void BeginPlay() override;

public:    
    virtual void Tick(float DeltaTime) override;

    // Components
    UPROPERTY(VisibleDefaultsOnly, Category=Projectile)
    USphereComponent* CollisionComp;

    UPROPERTY(VisibleDefaultsOnly, Category=Projectile)
    UStaticMeshComponent* MeshComp;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Movement)
    UProjectileMovementComponent* ProjectileMovement;

    // Variables (again, all in one place!)
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gameplay")
    bool bIsPlayerBullet = false;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gameplay")
    float Damage = 1.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gameplay")
    FLinearColor BulletColor = FLinearColor::Green;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gameplay")
    float Lifetime = 4.0f;

    // Functions
    UFUNCTION()
    void OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

    void SetBulletColor(FLinearColor InColor);
    void SetSpeed(float InSpeed);

private:
    UMaterialInstanceDynamic* DynamicMaterial;
};

Step 3.2: Create STGProjectile Implementation

Create Source/BulletHellGame/STGProjectile.cpp:

#include "STGProjectile.h"
#include "Components/SphereComponent.h"
#include "Components/StaticMeshComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "STGEnemy.h"
#include "STGPawn.h"

ASTGProjectile::ASTGProjectile()
{
    PrimaryActorTick.bCanEverTick = true;

    // Collision sphere
    CollisionComp = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComp"));
    CollisionComp->InitSphereRadius(5.0f);
    CollisionComp->SetCollisionProfileName("OverlapAllDynamic");
    CollisionComp->OnComponentBeginOverlap.AddDynamic(this, &ASTGProjectile::OnOverlapBegin);
    RootComponent = CollisionComp;

    // Visual mesh
    MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp"));
    MeshComp->SetupAttachment(CollisionComp);
    MeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    
    static ConstructorHelpers::FObjectFinder<UStaticMesh> SphereMesh(TEXT("/Engine/BasicShapes/Sphere"));
    if (SphereMesh.Succeeded())
    {
        MeshComp->SetStaticMesh(SphereMesh.Object);
        MeshComp->SetRelativeScale3D(FVector(0.1f, 0.1f, 0.1f));
    }

    // Projectile movement
    ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileComp"));
    ProjectileMovement->UpdatedComponent = CollisionComp;
    ProjectileMovement->InitialSpeed = 1200.f;
    ProjectileMovement->MaxSpeed = 1200.f;
    ProjectileMovement->bRotationFollowsVelocity = true;
    ProjectileMovement->bShouldBounce = false;

    // Auto-destroy after lifetime
    InitialLifeSpan = Lifetime;
}

void ASTGProjectile::BeginPlay()
{
    Super::BeginPlay();

    // Create dynamic material
    if (MeshComp)
    {
        DynamicMaterial = MeshComp->CreateAndSetMaterialInstanceDynamic(0);
        if (DynamicMaterial)
        {
            DynamicMaterial->SetVectorParameterValue(TEXT("BaseColor"), BulletColor);
            DynamicMaterial->SetVectorParameterValue(TEXT("EmissiveColor"), BulletColor * 3.0f);
        }
    }
}

void ASTGProjectile::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

void ASTGProjectile::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor && OtherActor != this)
    {
        if (bIsPlayerBullet)
        {
            // Player bullet hits enemy
            ASTGEnemy* Enemy = Cast<ASTGEnemy>(OtherActor);
            if (Enemy)
            {
                Enemy->HandleDamage(Damage);
                Destroy();
            }
        }
        else
        {
            // Enemy bullet hits player
            ASTGPawn* Player = Cast<ASTGPawn>(OtherActor);
            if (Player)
            {
                Player->TakeHit(1);
                Destroy();
            }
        }
    }
}

void ASTGProjectile::SetBulletColor(FLinearColor InColor)
{
    BulletColor = InColor;
    if (DynamicMaterial)
    {
        DynamicMaterial->SetVectorParameterValue(TEXT("BaseColor"), BulletColor);
        DynamicMaterial->SetVectorParameterValue(TEXT("EmissiveColor"), BulletColor * 3.0f);
    }
}

void ASTGProjectile::SetSpeed(float InSpeed)
{
    if (ProjectileMovement)
    {
        ProjectileMovement->InitialSpeed = InSpeed;
        ProjectileMovement->MaxSpeed = InSpeed;
    }
}

Part 4: Create the Enemy (C++ Implementation)

I'll provide the complete implementation - notice how defining 15+ enemy variables takes ~15 lines of code instead of 75+ clicks:

Step 4.1: Create STGEnemy Header

Create Source/BulletHellGame/STGEnemy.h:

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "STGEnemy.generated.h"

UCLASS()
class BULLETHELLGAME_API ASTGEnemy : public AActor
{
    GENERATED_BODY()
    
public:    
    ASTGEnemy();

protected:
    virtual void BeginPlay() override;

public:    
    virtual void Tick(float DeltaTime) override;

    // Components
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
    class UStaticMeshComponent* MeshComp;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
    class UBoxComponent* CollisionComp;

    // Variables - all configurable in one place!
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    int32 MaxHealth = 12;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Stats")
    int32 CurrentHealth = 12;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    int32 ScoreValue = 50;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float VerticalSpeed = 220.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float HorizontalAmplitude = 250.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float HorizontalFrequency = 1.8f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float DespawnY = -750.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float FireInterval = 0.35f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    int32 BulletsPerBurst = 20;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float BurstSpread = 360.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float EnemyBulletSpeed = 1000.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float EnemyBulletLifetime = 6.0f;

    // Functions
    void Fire();
    void HandleDamage(float DamageAmount);

    UFUNCTION()
    void OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

private:
    FTimerHandle TimerHandle_Fire;
    float BaseX;
    float WaveSeed;
    UMaterialInstanceDynamic* DynamicMaterial;
};

See the difference? All 15 enemy stat variables defined in ~15 lines with default values. In the Blueprint approach, this would require:

  • 15 × (Click +, type name, select type, click compile, set value) = 75+ clicks
  • Easy to make mistakes
  • Hard to duplicate for enemy variants

Comparison: Time Investment

Blueprint-Heavy Approach (Original Tutorial)

Part 2: Create Player

  • Step 2.3: Create 12 variables manually = ~15 minutes
    • For each variable: Click +, name it, select type from dropdown, compile, set default value
    • Easy to mistype or select wrong type
  • Step 2.4: Set up Enhanced Input = ~20 minutes
    • Create 3 Input Action assets manually
    • Create Input Mapping Context
    • Add 8 key bindings with modifiers
  • Step 2.5: Create firing logic with Blueprint nodes = ~25 minutes
    • Drag ~20 nodes, connect white/blue wires
    • Easy to miss connections
  • Total for Player: ~60 minutes

Full Game Blueprint Tutorial: ~6-8 hours

Code-First Approach

Part 2: Create Player

  • Step 2.1: Copy-paste header file = ~2 minutes
    • All 12 variables defined inline with defaults
  • Step 2.2: Copy-paste implementation = ~3 minutes
    • Movement, firing, all logic in readable code
  • Step 2.3: Configure input mappings (one-time) = ~5 minutes
  • Step 2.4: Compile = ~1 minute
  • Total for Player: ~11 minutes (5.5x faster!)

Full Game C++ Implementation: ~2-3 hours (3x faster!)


When to Use Each Approach

Use Code-First (C++) When:

You have many variables to configure
You want version control with readable diffs
You need complex logic (math, loops, algorithms)
You're working in a team (better for code review)
You want refactoring tools (rename, find references)
You need compile-time safety (type checking)

Use Blueprints When:

Setting visual assets (meshes, materials, sprites)
Rapid prototyping of simple mechanics
Non-programmers need to modify behavior
Creating animation blueprints (visual timeline editing)
Level design and placement

🎯 C++ base classes → Define all logic and variables
🎯 Blueprint child classes → Inherit from C++, set only visual assets
🎯 Best of both worlds → Code maintainability + visual asset management


Next Steps

Continue with the complete C++ implementations:


Back to Index