* 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>
15 KiB
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:
-
Go to Tools → New C++ Class (top menu bar)
-
In the "Choose Parent Class" window:
- Click "Actor" (enemies are independent game objects)
- Click "Next"
-
In the "Name Your New Actor" window:
- Name:
STGEnemy - Path: Should be
Source/BulletHellGame/(default) - Click "Create Class"
- Name:
What Happens Next:
- Unreal generates
STGEnemy.handSTGEnemy.cpp - VS Code opens with the new files
- Project compiles automatically (~30-60 seconds)
- 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:
- Click "Compile" button (bottom right)
- Wait for compilation (~30-60 seconds)
- Check for errors in Output Log
OR
In VS Code:
- Open terminal in VS Code (
Ctrl+`) - Run build command (project-specific)
- 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!
- In Content Browser, navigate to
Content → Blueprints - Right-click → Blueprint Class
- In "Pick Parent Class" window:
- Click "All Classes" dropdown
- Search for
STGEnemy - Select it
- Click "Select"
- Name it:
BP_Enemy - Double-click to open
In the Blueprint Editor:
Components (Left Panel):
- You'll see
MeshCompandCollisionCompfrom 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
- Drag
BP_Enemyfrom Content Browser into the level - Position it above the player (Y = 0, X = 500 or so)
- 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
- Place several
BP_Enemyinstances in the level (or duplicate the existing one) - Press Play
- Let enemies spawn and fire bullets
- 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):
- Create BP_Enemy Blueprint ✅
- Add 2 components manually
- Add 16 variables one-by-one = ~20 minutes of clicking
- Create Blueprint nodes for movement (~40 nodes with sine wave math)
- Create Blueprint nodes for firing (~30 nodes for radial burst)
- Create Blueprint nodes for collision detection
- Create Blueprint nodes for health/damage system
- Update BP_Player special ability nodes
- Total: ~90 minutes
C++ Approach (This Part):
- Create STGEnemy C++ class ✅
- Copy-paste header file with 15 variables 30 seconds
- Copy-paste implementation file with all logic
- Update bullet collision (5 lines)
- Update player special ability (10 lines)
- Compile
- Create BP_Enemy (inherits everything from C++)
- 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 →