24 KiB
Code-First Approach: Building the Bullet Hell Game in C++
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:
- ASTGPawn - Player ship with movement, firing, lives
- ASTGProjectile - Bullets (player and enemy)
- ASTGEnemy - Enemy ships with AI and firing patterns
- ASTGGameMode - Game rules, spawning, timer
- 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
- Open Unreal Engine 5
- Create New Project
- Select Games → Blank template
- Project Defaults:
- Project Type: C++ (NOT Blueprint!)
- Target Platform: Desktop
- Quality Preset: Scalable
- Starter Content: No Starter Content
- Name:
BulletHellGame - 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:
- All 12 player variables defined in ~12 lines (vs 60+ clicks in Blueprint UI)
- Default values inline (
= 750.0f) - no need to set after compile - Copy-paste friendly - duplicate variables easily
- Version control friendly - see exact changes in Git diff
- 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):
- Edit → Project Settings
- Engine → Input
- Axis Mappings:
MoveForward: W = 1.0, S = -1.0MoveRight: D = 1.0, A = -1.0
- Action Mappings:
Fire: Space, Left Mouse ButtonSpecial: X, Right Mouse Button
This is a one-time setup - all other configuration is in code.
Step 2.4: Compile and Test
- In Unreal Editor: Click Compile button (or close editor)
- Build in IDE: Build Solution / Compile Project
- Expected compile time: ~30-60 seconds (vs instant in Blueprint, but you save hours on UI work)
- Open Unreal Editor (if you closed it)
- 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
Hybrid Approach (Recommended):
🎯 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:
- Part 5: Enemy Spawning System (C++)
- Part 6: Game Mode and Rules (C++)
- Part 7: HUD and UI (C++)
- Complete C++ Reference