praca_magisterska/games/unreal/tutorial/part-4-cpp-create-enemy.md

551 lines
15 KiB
Markdown
Raw Permalink 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.

# Part 4 (C++): Create the Enemy
[← Previous: Part 3 (C++) - Create the Bullet](part-3-cpp-create-bullet.md) | [Back to Index](README.md) | [Next: Part 5 (C++) - Create Enemy Spawner →](part-5-cpp-create-spawner.md)
---
## 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:
> **⚠️ IMPORTANT:** Replace `YOURPROJECTNAME_API` with your actual project's API macro (e.g., `BULLETHELLCPP_API`).
```cpp
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "STGEnemy.generated.h"
class UStaticMeshComponent;
class UBoxComponent;
UCLASS()
class YOURPROJECTNAME_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:
```cpp
#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:
```cpp
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:
```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:
```cpp
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:
```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](part-3-cpp-create-bullet.md) | [Back to Index](README.md) | [Next: Part 5 (C++) - Create Enemy Spawner →](part-5-cpp-create-spawner.md)