Enemy Spawner와 난이도 조절
21강. Enemy Spawner와 난이도 조절
1. 이번 강의에서 해결할 문제
Spawner가 항상 같은 위치에 같은 적을 같은 속도로 생성합니다. 시간이 지나면 난도가 오르되 최대 활성 수와 게임 오버 상태를 지키는 생성 규칙을 만듭니다.
2. 학습 목표
3. 핵심 개념
Spawner는 적 행동을 소유하지 않고 “언제, 무엇을, 어디에” 만들지만 결정합니다. EnemyDataAsset은 능력치, EnemyBase는 행동, Spawner는 생성 규칙을 담당합니다.
난이도는 매 프레임 증가시킬 필요가 없습니다. Spawn 시점의 경과 시간으로 새 간격을 계산하면 충분합니다. 최대 활성 수는 성능과 플레이 가능성을 동시에 보호하는 안전장치입니다.
4. 단계별 실습
파일 경로: Source/TopDownSurvival/Systems/EnemySpawner.h · 설정
class AEnemyBase;
class UEnemyDataAsset;
UPROPERTY(EditDefaultsOnly, Category = "Spawning")
TArray<TObjectPtr<UEnemyDataAsset>> EnemyTypes;
UPROPERTY(EditDefaultsOnly, Category = "Spawning", meta = (ClampMin = "100.0", Units = "cm"))
float SpawnRadius = 1200.0f;
UPROPERTY(EditDefaultsOnly, Category = "Spawning", meta = (ClampMin = "0.1", Units = "s"))
float InitialInterval = 2.5f;
UPROPERTY(EditDefaultsOnly, Category = "Spawning", meta = (ClampMin = "0.1", Units = "s"))
float MinimumInterval = 0.6f;
UPROPERTY(EditDefaultsOnly, Category = "Spawning", meta = (ClampMin = "1"))
int32 MaxActiveEnemies = 40;
TArray<TWeakObjectPtr<AEnemyBase>> ActiveEnemies;
파일 경로: Source/TopDownSurvival/Systems/EnemySpawner.cpp · 생성 규칙
#include "Data/EnemyDataAsset.h"
#include "Enemies/EnemyBase.h"
#include "NavigationSystem.h"
#include "Systems/SurvivalGameSubsystem.h"
void AEnemySpawner::SpawnEnemy()
{
USurvivalGameSubsystem* Game =
GetGameInstance()->GetSubsystem<USurvivalGameSubsystem>();
if (!Game || Game->IsGameOver())
{
GetWorldTimerManager().ClearTimer(SpawnTimerHandle);
return;
}
ActiveEnemies.RemoveAll(
[](const TWeakObjectPtr<AEnemyBase>& Enemy) { return !Enemy.IsValid(); });
if (ActiveEnemies.Num() >= MaxActiveEnemies || EnemyTypes.IsEmpty())
{
return;
}
UEnemyDataAsset* Data =
EnemyTypes[FMath::RandRange(0, EnemyTypes.Num() - 1)];
if (!Data || !Data->EnemyClass)
{
return;
}
FNavLocation NavLocation;
UNavigationSystemV1* Navigation =
FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
if (!Navigation ||
!Navigation->GetRandomReachablePointInRadius(
GetActorLocation(), SpawnRadius, NavLocation))
{
return;
}
FActorSpawnParameters Params;
Params.SpawnCollisionHandlingOverride =
ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding;
AEnemyBase* Enemy = GetWorld()->SpawnActor<AEnemyBase>(
Data->EnemyClass, NavLocation.Location, FRotator::ZeroRotator, Params);
if (Enemy)
{
Enemy->InitializeFromData(Data);
ActiveEnemies.Add(Enemy);
}
const float Elapsed = GetWorld()->GetTimeSeconds();
const float NextInterval = FMath::Max(
MinimumInterval, InitialInterval - Elapsed * 0.015f);
if (!FMath::IsNearlyEqual(NextInterval, SpawnInterval, 0.05f))
{
RestartSpawnTimer(NextInterval);
}
}
Unreal Editor
BP_EnemySpawner의 EnemyTypes에 DA_EnemyBasic과 DA_EnemyFast를 추가합니다. Nav Mesh 안쪽 중앙에 배치하고 MaxActiveEnemies를 20으로 낮춰 첫 테스트를 진행합니다.
5. 코드가 동작하는 이유
약한 참조 목록은 적을 소유하지 않으며 제거된 Actor는 prune 단계에서 빠집니다. Navigation System은 이동 가능한 위치를 반환합니다. 경과 시간이 늘수록 간격이 줄지만 MinimumInterval 아래로 내려가지 않아 무한히 빨라지는 규칙을 막습니다.
6. 자주 하는 실수와 해결법
7. 직접 실습
8. 이해 점검 질문 3개
9. 핵심 요약
Enemy Spawner와 난이도 조절 미니 퀴즈
선택 즉시 정답과 해설을 확인할 수 있습니다. 결과는 이 브라우저에만 저장됩니다.
학습을 마쳤나요?
직접 실습과 점검 질문까지 확인한 뒤 완료로 표시하세요.