현재 프로젝트에서는 Wave가 시간 흐름에 따라 변경되고 있다.
Wave 진행의 핵심은 AEnemySpawner 클래스에 있다. AEnemySpawner::Tick()에서 매 프레임 UpdateSpawning(DeltaTime)을 호출하고, 이 함수 안에서 현재 Wave의 경과 시간, 적 스폰 간격, 총 스폰 수, 다음 Wave 대기 시간을 관리한다.
특히 CurrentWaveElapsedTime += DeltaTime으로 현재 Wave의 시간이 누적되고, SpawnInterval마다 적을 스폰한다. 이후 TotalSpawnCount만큼 적을 스폰했고 Wave 완료 조건을 만족하면 CompleteCurrentWave()가 호출된다.
Wave가 완료되면 바로 다음 Wave로 넘어가는 것이 아니라, DelayAfterWave만큼 기다린 뒤 StartWave(CurrentWaveIndex + 1)을 호출한다. 따라서 이 프로젝트의 Wave는 시간과 스폰 상태에 따라 실제로 변화하고 있다.
Wave가 실제로 바뀌는 시점은 AEnemySpawner::StartWave() 안이다.
void AEnemySpawner::StartWave(int32 NewWaveIndex)
{
if (!WaveData || NewWaveIndex >= WaveData->GetRuntimeWaveCount())
{
StopSpawning();
OnAllWavesCompleted.Broadcast();
return;
}
CurrentWaveIndex = NewWaveIndex;
SpawnedCountInCurrentWave = 0;
CurrentWaveElapsedTime = 0.0f;
TimeSinceLastSpawn = BIG_NUMBER;
bWaitingForNextWave = false;
ActiveWaveEnemies.Reset();
if (const FBulletHellWaveDefinition* WaveDefinition = GetCurrentWaveDefinition())
{
OnWaveStarted.Broadcast(CurrentWaveIndex + 1, WaveDefinition->WaveName);
}
}
여기서 중요한 부분은 OnWaveStarted.Broadcast(CurrentWaveIndex + 1, WaveDefinition->WaveName);이다.
CurrentWaveIndex는 내부적으로 0부터 시작하지만, HUD에 보여줄 Wave 번호는 1부터 보여주는 것이 자연스럽기 때문에 CurrentWaveIndex + 1을 전달하고 있다.
AEnemySpawner에는 이미 Wave 변경을 알릴 수 있는 Delegate가 선언되어 있다.
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
FBulletHellWaveChangedSignature,
int32, WaveIndex,
FName, WaveName
);
그리고 AEnemySpawner 안에 다음 Delegate들이 있다.
UPROPERTY(BlueprintAssignable, Category = "Enemy Spawner|Wave")
FBulletHellWaveChangedSignature OnWaveStarted;
UPROPERTY(BlueprintAssignable, Category = "Enemy Spawner|Wave")
FBulletHellWaveChangedSignature OnWaveCompleted;
UPROPERTY(BlueprintAssignable, Category = "Enemy Spawner|Wave")
FBulletHellAllWavesCompletedSignature OnAllWavesCompleted;
HUD에 현재 Wave 번호를 보여주려면 새 Delegate를 만들기보다 OnWaveStarted에 바인딩하는 것이 가장 적절하다.
현재 HUD 구조에서는 UBulletHellHUDPresenter가 HUD Widget과 게임 데이터를 연결하는 역할을 하고 있다.
체력 변경 Delegate와 인벤토리 변경 Delegate도 UBulletHellHUDPresenter::Initialize()에서 바인딩하고 있으므로, Wave 변경 Delegate도 같은 위치에 바인딩하는 것이 구조적으로 자연스럽다.
void UBulletHellHUDPresenter::Initialize(
APlayerFighterjet* InPlayer,
UBulletHellPlayerHUDWidget* InView)
{
Player = InPlayer;
View = InView;
if (!Player || !View)
{
return;
}
if (UBulletHellHealthComponent* HealthComponent = Player->GetHealthComponent())
{
HealthComponent->OnHealthChanged.AddDynamic(
this,
&UBulletHellHUDPresenter::HandleHealthChanged);
}
if (UBulletHellInventoryComponent* InventoryComponent = Player->GetInventoryComponent())
{
InventoryComponent->OnInventoryChanged.AddDynamic(
this,
&UBulletHellHUDPresenter::HandleInventoryChanged);
}
for (TActorIterator<AEnemySpawner> It(Player->GetWorld()); It; ++It)
{
EnemySpawner = *It;
EnemySpawner->OnWaveStarted.AddUniqueDynamic(
this,
&UBulletHellHUDPresenter::HandleWaveChanged);
break;
}
RefreshAll();
}
이 코드를 사용하려면 BulletHellHUDPresenter.cpp 상단에 다음 include가 필요하다.
#include "EngineUtils.h"
#include "Spawner/EnemySpawner.h"
AddDynamic(this, &UBulletHellHUDPresenter::HandleWaveChanged)에서 에러가 발생한 이유는 Delegate 시그니처와 연결하려는 함수의 시그니처가 맞지 않았기 때문이다.
OnWaveStarted는 int32 WaveIndex, FName WaveName 두 개의 인자를 전달한다.
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
FBulletHellWaveChangedSignature,
int32, WaveIndex,
FName, WaveName
);
따라서 바인딩되는 함수도 동일하게 두 개의 인자를 받아야 한다.
잘못된 형태는 다음과 같다.
UFUNCTION()
void HandleWaveChanged();
올바른 형태는 다음과 같다.
UFUNCTION()
void HandleWaveChanged(int32 WaveIndex, FName WaveName);
HUD Widget에는 이미 OnWaveChanged(int CurrentWave) 함수가 준비되어 있다.
따라서 Presenter의 HandleWaveChanged에서는 전달받은 WaveIndex를 View에 넘겨주면 된다.
void UBulletHellHUDPresenter::HandleWaveChanged(int32 WaveIndex, FName WaveName)
{
if (!View)
{
return;
}
View->OnWaveChanged(WaveIndex);
}
현재는 Wave 이름을 HUD에 표시하지 않기 때문에 WaveName은 사용하지 않아도 된다. 다만 Delegate 시그니처를 맞추기 위해 인자로는 받아야 한다.
UBulletHellPlayerHUDWidget에는 이미 Wave 번호를 표시하는 함수가 구현되어 있다.
void UBulletHellPlayerHUDWidget::OnWaveChanged_Implementation(int CurrentWave)
{
if (Text_CurrentWave)
{
const FText WaveText = FText::Format(
NSLOCTEXT("BulletHellHUD", "WaveFormat", "Wave {0}"),
FText::AsNumber(CurrentWave)
);
Text_CurrentWave->SetText(WaveText);
}
}
따라서 Text_CurrentWave가 Widget Blueprint에서 정상적으로 바인딩되어 있다면, Wave가 시작될 때마다 HUD에 Wave 1, Wave 2, Wave 3 형태로 표시된다.
현재 프로젝트의 Wave는 AEnemySpawner에서 시간과 스폰 상태에 따라 변경된다.
HUD에 현재 Wave를 표시하기 위해 새 Delegate를 만들 필요는 없다. 이미 존재하는 AEnemySpawner::OnWaveStarted에 HUD Presenter를 바인딩하면 된다.
Delegate를 다는 위치는 UBulletHellHUDPresenter::Initialize()가 가장 적절하다. 이 클래스가 이미 체력, 인벤토리 같은 게임 상태를 HUD Widget에 전달하는 중간 계층 역할을 하고 있기 때문이다.
AddDynamic 에러는 HandleWaveChanged 함수가 Delegate와 같은 인자 목록을 갖지 않아서 발생한 것이다. void HandleWaveChanged(int32 WaveIndex, FName WaveName) 형태로 수정하면 해결된다.