# Code-First Approach: Building the Bullet Hell Game in C++ [Back to Index](README.md) --- ## 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`: ```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`: ```cpp #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`: ```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(TEXT("Root")); // Ship mesh - cone shape pointing upward ShipMesh = CreateDefaultSubobject(TEXT("ShipMesh")); ShipMesh->SetupAttachment(RootComponent); ShipMesh->SetCollisionProfileName("NoCollision"); // Load cone mesh static ConstructorHelpers::FObjectFinder 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(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(TEXT("HitboxIndicator")); HitboxIndicator->SetupAttachment(RootComponent); HitboxIndicator->SetCollisionEnabled(ECollisionEnabled::NoCollision); static ConstructorHelpers::FObjectFinder 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(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(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::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 FoundEnemies; UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASTGEnemy::StaticClass(), FoundEnemies); for (AActor* Enemy : FoundEnemies) { Enemy->Destroy(); } TArray FoundBullets; UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASTGProjectile::StaticClass(), FoundBullets); for (AActor* Bullet : FoundBullets) { ASTGProjectile* Projectile = Cast(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(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`: ```cpp #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`: ```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(TEXT("SphereComp")); CollisionComp->InitSphereRadius(5.0f); CollisionComp->SetCollisionProfileName("OverlapAllDynamic"); CollisionComp->OnComponentBeginOverlap.AddDynamic(this, &ASTGProjectile::OnOverlapBegin); RootComponent = CollisionComp; // Visual mesh MeshComp = CreateDefaultSubobject(TEXT("MeshComp")); MeshComp->SetupAttachment(CollisionComp); MeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision); static ConstructorHelpers::FObjectFinder 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(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(OtherActor); if (Enemy) { Enemy->HandleDamage(Damage); Destroy(); } } else { // Enemy bullet hits player ASTGPawn* Player = Cast(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`: ```cpp #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++)](code-first-part5-spawning.md) - [Part 6: Game Mode and Rules (C++)](code-first-part6-gamemode.md) - [Part 7: HUD and UI (C++)](code-first-part7-hud.md) - [Complete C++ Reference](appendix-d-cpp-reference.md) --- [Back to Index](README.md)