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

819 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<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`:
```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<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`:
```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)