praca_magisterska/games/unreal/tutorial/part-4-cpp-create-enemy.md
Copilot dd009a0029
Add C++ code-first tutorial for Unreal bullet-hell game (#2)
* Initial plan

* Add C++ code-first tutorial approach for Unreal game

Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>

* Add quick start guide for code-first approach

Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>

* Restructure C++ tutorial into part-by-part format and add migration guide

Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>

* Address PR feedback: improve part-1, add Expected Results, split parts 4-9 into separate files

Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>
2026-01-08 16:44:56 +01:00

15 KiB
Raw Blame History

Part 4 (C++): Create the Enemy

← Previous: Part 3 (C++) - Create the Bullet | Back to Index | Next: Part 5 (C++) - Create Enemy Spawner →


Overview

Create enemy ships in C++. Again, we'll copy-paste all 15 enemy variables instead of clicking UI 75+ times.

Time comparison:

  • Blueprint approach (Part 4): ~90 minutes (16 variables + complex Blueprint nodes)
  • C++ approach (this part): ~25 minutes (copy-paste code)

Step 4.1: Create STGEnemy C++ Class

In Unreal Editor:

  1. Go to Tools → New C++ Class (top menu bar)

  2. In the "Choose Parent Class" window:

    • Click "Actor" (enemies are independent game objects)
    • Click "Next"
  3. In the "Name Your New Actor" window:

    • Name: STGEnemy
    • Path: Should be Source/BulletHellGame/ (default)
    • Click "Create Class"

What Happens Next:

  1. Unreal generates STGEnemy.h and STGEnemy.cpp
  2. VS Code opens with the new files
  3. Project compiles automatically (~30-60 seconds)
  4. Unreal Editor refreshes

Step 4.2: Define Enemy Variables in STGEnemy.h

This is where the magic happens! Instead of clicking UI 75+ times (16 variables × ~5 clicks each), we'll copy-paste all variables at once.

Open STGEnemy.h in VS Code

Replace the entire file content with:

#pragma once

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

class UStaticMeshComponent;
class UBoxComponent;

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")
    UStaticMeshComponent* MeshComp;

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

    // ===== HEALTH & SCORE (copy-paste all 15 variables!) =====
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    int32 MaxHealth = 12;

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

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

    // ===== MOVEMENT =====
    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;

    // ===== FIRING =====
    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 = 0.0f;
    float WaveSeed = 0.0f;
    float ElapsedTime = 0.0f;
};

What Just Happened?

You just defined 15 gameplay variables + 2 components with default values in ~30 seconds!

In the Blueprint tutorial, this would require:

  • 16 variables × 5 clicks each = 80 clicks
  • ~20 minutes of manual work
  • High chance of typos or wrong types

Time saved: 19.5 minutes


Step 4.3: Implement STGEnemy Logic

Open STGEnemy.cpp in VS Code

Replace the file content with:

#include "STGEnemy.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"
#include "Kismet/GameplayStatics.h"
#include "STGProjectile.h"
#include "STGPawn.h"

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

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

    // Mesh component
    MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp"));
    MeshComp->SetupAttachment(RootComponent);
    MeshComp->SetCollisionProfileName("NoCollision");
    
    // Load cube mesh
    static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeMesh(TEXT("/Engine/BasicShapes/Cube"));
    if (CubeMesh.Succeeded())
    {
        MeshComp->SetStaticMesh(CubeMesh.Object);
        MeshComp->SetRelativeScale3D(FVector(0.6f, 0.6f, 0.1f));
    }

    // Collision component
    CollisionComp = CreateDefaultSubobject<UBoxComponent>(TEXT("CollisionComp"));
    CollisionComp->SetupAttachment(RootComponent);
    CollisionComp->SetBoxExtent(FVector(30.f, 30.f, 10.f));
    CollisionComp->SetCollisionProfileName("OverlapAllDynamic");
    CollisionComp->SetGenerateOverlapEvents(true);
    CollisionComp->OnComponentBeginOverlap.AddDynamic(this, &ASTGEnemy::OnOverlapBegin);
}

void ASTGEnemy::BeginPlay()
{
    Super::BeginPlay();
    
    CurrentHealth = MaxHealth;
    BaseX = GetActorLocation().X;
    WaveSeed = FMath::FRand() * 1000.0f;
    ElapsedTime = 0.0f;

    // Start firing timer
    GetWorldTimerManager().SetTimer(TimerHandle_Fire, this, &ASTGEnemy::Fire, 
                                    FireInterval, true, FireInterval);
}

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

    ElapsedTime += DeltaTime;

    // Sinusoidal movement (wave pattern)
    FVector NewLocation = GetActorLocation();
    
    // Move downward
    NewLocation.X -= VerticalSpeed * DeltaTime;
    
    // Horizontal sine wave
    float HorizontalOffset = HorizontalAmplitude * FMath::Sin(
        HorizontalFrequency * (ElapsedTime + WaveSeed)
    );
    NewLocation.Y = BaseX + HorizontalOffset;
    
    SetActorLocation(NewLocation);

    // Check if enemy should despawn (moved off screen)
    if (NewLocation.X < DespawnY)
    {
        Destroy();
    }
}

void ASTGEnemy::Fire()
{
    // Fire radial burst of bullets
    for (int32 i = 0; i < BulletsPerBurst; i++)
    {
        // Calculate angle for this bullet in the burst
        float AngleDeg = (BurstSpread / BulletsPerBurst) * i;
        float AngleRad = FMath::DegreesToRadians(AngleDeg);
        
        // Calculate direction vector
        FVector Direction = FVector(
            FMath::Cos(AngleRad),
            FMath::Sin(AngleRad),
            0.0f
        );
        
        // Spawn location slightly below enemy
        FVector SpawnLocation = GetActorLocation() + FVector(0.f, 0.f, -30.f);
        
        // Spawn bullet
        ASTGProjectile* Bullet = GetWorld()->SpawnActor<ASTGProjectile>(
            ASTGProjectile::StaticClass(),
            SpawnLocation,
            Direction.Rotation()
        );
        
        if (Bullet)
        {
            Bullet->bIsPlayerBullet = false;
            Bullet->SetSpeed(EnemyBulletSpeed);
            Bullet->SetBulletColor(FLinearColor::Red);
            Bullet->Lifetime = EnemyBulletLifetime;
        }
    }
}

void ASTGEnemy::HandleDamage(float DamageAmount)
{
    CurrentHealth -= DamageAmount;
    
    if (CurrentHealth <= 0)
    {
        // Award score to player
        ASTGPawn* Player = Cast<ASTGPawn>(UGameplayStatics::GetPlayerPawn(GetWorld(), 0));
        if (Player)
        {
            Player->AddScore(ScoreValue);
        }
        
        // Destroy enemy
        Destroy();
    }
}

void ASTGEnemy::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, 
                                UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, 
                                bool bFromSweep, const FHitResult& SweepResult)
{
    // Enemy collides with player - damage player
    if (OtherActor && OtherActor != this)
    {
        ASTGPawn* Player = Cast<ASTGPawn>(OtherActor);
        if (Player)
        {
            Player->TakeHit(1);
            Destroy(); // Enemy dies on collision with player
        }
    }
}

Step 4.4: Update Bullet Collision Logic

Now that enemies exist, we can complete the bullet collision logic.

Open STGProjectile.cpp

Find the OnOverlapBegin function and update the player bullet section:

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

Add include at top of STGProjectile.cpp:

#include "STGEnemy.h"  // Add this line

Step 4.5: Compile the Code

In Unreal Editor:

  1. Click "Compile" button (bottom right)
  2. Wait for compilation (~30-60 seconds)
  3. Check for errors in Output Log

OR

In VS Code:

  1. Open terminal in VS Code (Ctrl+ `)
  2. Run build command (project-specific)
  3. Wait for build to finish

Expected Result:

  • Compilation succeeds
  • "C++ Classes" folder in Content Browser shows STGEnemy
  • No errors in Output Log

Step 4.6: Create Blueprint Child

Create a minimal Blueprint that inherits from STGEnemy. This Blueprint is ONLY for visual assets!

  1. In Content Browser, navigate to Content → Blueprints
  2. Right-click → Blueprint Class
  3. In "Pick Parent Class" window:
    • Click "All Classes" dropdown
    • Search for STGEnemy
    • Select it
    • Click "Select"
  4. Name it: BP_Enemy
  5. Double-click to open

In the Blueprint Editor:

Components (Left Panel):

  • You'll see MeshComp and CollisionComp from C++
  • NO NEED to add them manually!

Variables (My Blueprint Panel):

  • You'll see all 15 variables from C++ (MaxHealth, VerticalSpeed, etc.)
  • NO NEED to create them!
  • All default values are already set from C++!

What to do:

  • NOTHING for now! All logic is in C++
  • Compile and Save

Step 4.7: Test Enemy Spawning

  1. Drag BP_Enemy from Content Browser into the level
  2. Position it above the player (Y = 0, X = 500 or so)
  3. Press Play (Alt+P)

Expected Result

In Play Mode Window:

Enemy behavior:

  • Enemy appears above player
  • Enemy moves downward smoothly
  • Enemy moves in sine wave pattern (left-right oscillation)
  • Enemy fires red bullet bursts in circular pattern
  • Bullets spread out in all directions (360-degree burst)

Player vs Enemy:

  • Player bullets (green) destroy enemy when they hit
  • Enemy bullets (red) damage player when they hit
  • Player takes damage and loses life when hit
  • Touching enemy directly damages player and destroys enemy

Visual confirmation:

  • Enemy is a cube/box shape (red-ish by default)
  • Enemy fires 20 red bullets in a circular burst every 0.35 seconds
  • Player can shoot enemy with green bullets
  • Score increases when enemy is destroyed (check Output Log or will see in UI later)

To test collision:

  • Fly player into enemy - both should take damage
  • Let enemy bullets hit player - player should take damage
  • Shoot enemy with player bullets - enemy health should decrease

Despawning:

  • After ~10 seconds, enemy moves off bottom of screen and disappears (auto-destroy)

Step 4.8: Update Player Special Ability

Now that enemies exist, we can complete the special ability to destroy all enemies on screen.

Open STGPawn.cpp

Find the UseSpecial() function and replace it with:

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

        // Destroy all enemy bullets (not player bullets)
        TArray<AActor*> FoundBullets;
        UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASTGProjectile::StaticClass(), FoundBullets);
        for (AActor* Bullet : FoundBullets)
        {
            ASTGProjectile* Projectile = Cast<ASTGProjectile>(Bullet);
            if (Projectile && !Projectile->bIsPlayerBullet)
            {
                Projectile->Destroy();
            }
        }
        
        UE_LOG(LogTemp, Warning, TEXT("SPECIAL ABILITY - Screen Cleared!"));
    }
}

Add include at top of STGPawn.cpp:

#include "STGEnemy.h"  // Add this line

Compile again!


Step 4.9: Test Special Ability

  1. Place several BP_Enemy instances in the level (or duplicate the existing one)
  2. Press Play
  3. Let enemies spawn and fire bullets
  4. Press X to activate special ability

Expected Result in Play Mode:

  • All enemies disappear instantly when X is pressed
  • All red enemy bullets disappear instantly
  • Player's green bullets remain (not destroyed)
  • Message "SPECIAL ABILITY - Screen Cleared!" appears in Output Log
  • Pressing X again does nothing (ability can only be used once per game)

Comparison Summary

Blueprint Approach (Part 4):

  1. Create BP_Enemy Blueprint
  2. Add 2 components manually
  3. Add 16 variables one-by-one = ~20 minutes of clicking
  4. Create Blueprint nodes for movement (~40 nodes with sine wave math)
  5. Create Blueprint nodes for firing (~30 nodes for radial burst)
  6. Create Blueprint nodes for collision detection
  7. Create Blueprint nodes for health/damage system
  8. Update BP_Player special ability nodes
  9. Total: ~90 minutes

C++ Approach (This Part):

  1. Create STGEnemy C++ class
  2. Copy-paste header file with 15 variables 30 seconds
  3. Copy-paste implementation file with all logic
  4. Update bullet collision (5 lines)
  5. Update player special ability (10 lines)
  6. Compile
  7. Create BP_Enemy (inherits everything from C++)
  8. Total: ~25 minutes

Time saved: 65 minutes


What's Next?

In Part 5, we'll create the enemy spawner in C++. You'll see how easy it is to manage spawn rates and difficulty curves with code!


← Previous: Part 3 (C++) - Create the Bullet | Back to Index | Next: Part 5 (C++) - Create Enemy Spawner →