처음에는 기존 맵 이름이 BasicLevel이었는데, 과제 진행 중 맵 이름을 Level1로 변경하였다.
하지만 게임 시작 버튼을 눌러도 다음 레벨로 이동하지 않고, 로그에 Invalid URL 관련 오류가 발생하였다.
원인을 확인해보니 StartGame() 함수와 레벨 이동 배열 쪽에서 아직 이전 맵 이름을 참조하고 있었다.
// SpartaPlayerController
UGameplayStatics::OpenLevel(GetWorld(), FName("BasicLevel"));
// 위 코드에서 맵 이름을 Level1로 변경했으니 코드에서도 동일하게 수정해주었다.
UGameplayStatics::OpenLevel(GetWorld(), FName("Level1"));
// 그리고 GameState에서 사용하는 LevelMapNames 배열도 에디터에서 Level1, Level2, Level3로 다시 확인해주었다.
이 문제를 통해 맵 이름을 변경할 때는 에디터의 맵 파일명뿐 아니라,
코드와 블루프린트에서 참조하는 이름도 같이 확인해야 한다는 것을 알게 되었다.
처음에는 MineItem에서도 부모 클래스인 BaseItem의 ActivateItem()을 그대로 사용하려고 했다.
하지만 BaseItem::ActivateItem() 안에서는 파티클과 사운드를 재생한 뒤 바로 DestroyItem()을 호출하고 있었다.
// BaseItem.cpp
void ABaseItem::ActivateItem(AActor* Activator)
{
// 파티클, 사운드 처리
DestroyItem();
}
이 구조를 지뢰 아이템에 그대로 사용하니, 지뢰가 폭발 타이머를 기다리기 전에 액터가 먼저 제거되는 문제가 생겼다.
그래서 MineItem에서는 Super::ActivateItem()을 호출하지 않고, 지뢰 전용 로직을 따로 구현하였다.
// MineItem.cpp
void AMineItem::ActivateItem(AActor* Activator)
{
if (bHasExploded) return;
bHasExploded = true;
if (Collision)
{
Collision->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
GetWorld()->GetTimerManager().SetTimer(
ExplosionTimerHandle,
this,
&AMineItem::Explode,
ExplosionDelay,
false
);
}
이렇게 수정하니 지뢰가 플레이어와 충돌하면 바로 사라지지 않고, 일정 시간이 지난 뒤 정상적으로 폭발하도록 되었다.
아이템 획득이나 지뢰 폭발 시 파티클을 생성했는데, 웨이브가 끝나거나 레벨이 바뀌어도 파티클이 남아있는 문제가 있었다.
처음에는 각 아이템에서 타이머로 파티클을 제거하려고 했지만,
웨이브나 레벨 전환 시점과 파티클 제거 타이밍이 겹치면서 에디터가 강제로 종료되면서 오류가 발생하며 관리가 복잡해졌다.
그래서 생성된 파티클을 GameState에 등록해두고, 웨이브나 레벨이 끝날 때 한 번에 정리하는 방식으로 수정하였다.
// SpartaGameState.cpp
void ASpartaGameState::RegisterParticle(UParticleSystemComponent* Particle)
{
if (Particle)
{
ActiveParticles.Add(Particle);
}
}
void ASpartaGameState::ClearActiveParticles()
{
for (TWeakObjectPtr<UParticleSystemComponent>& WeakParticle : ActiveParticles)
{
if (WeakParticle.IsValid())
{
WeakParticle->DeactivateSystem();
WeakParticle->DestroyComponent();
}
}
ActiveParticles.Empty();
}
그리고 EndWave()와 EndLevel()에서 ClearActiveParticles()를 호출하여 남아있는 파티클을 정리하였다.
// SpartaGameState.cpp
void ASpartaGameState::EndWave()
{
ClearActiveParticles();
GetWorldTimerManager().ClearTimer(LevelTimerHandle);
}
이렇게 수정하니 웨이브 전환 시 화면에 남아있던 파티클들이 깔끔하게 제거되었다.
이번 과제에서는 단순히 아이템을 스폰하고 획득하는 기능을 넘어서,
게임 전체 흐름을 GameState에서 관리하는 구조를 만들어보았다.
레벨과 웨이브 진행, 제한 시간, 코인 획득 조건, 다음 레벨 이동, 게임오버 처리까지 하나의 흐름으로 연결하면서 게임 루프가 어떻게 구성되는지 더 이해할 수 있었다.
또한 SpawnVolume과 데이터테이블을 활용해 아이템 스폰 확률을 관리하고, 레벨이 올라갈수록 지뢰와 회복 아이템의 난이도가 달라지도록 구현하면서 데이터 기반 설계의 편리함도 느낄 수 있었다.
UI 부분에서는 처음에는 GameState에서 직접 HUD 텍스트를 수정했지만,
점점 코드가 복잡해져서 SpartaHUDWidget과 SpartaMainMenuWidget으로 분리하였다.
덕분에 게임 진행 로직과 UI 표시 로직을 나누어 관리할 수 있었고, 코드 구조도 더 깔끔해졌다.
작업 중 맵 이름 변경으로 인한 레벨 이동 오류, 지뢰 아이템이 바로 사라지는 문제, 웨이브 전환 시 파티클이 남아 충돌하는 문제 등이 있었지만, 하나씩 원인을 찾아 수정하면서 언리얼에서 액터의 생명주기, 타이머, 위젯 관리가 얼마나 중요한지 알게 되었다.
이번 과제를 통해 GameState, PlayerController, Widget, DataTable, Timer가 서로 어떻게 연결되는지 직접 경험할 수 있었고,
이전보다 게임 구조를 조금 더 이해할 수 있게 된 것 같다.
| Unreal C++ 싱글플레이 FPS 슈터 게임 팀 프로젝트 적 AI - 2 (0) | 2026.05.20 |
|---|---|
| Unreal C++ 싱글플레이 FPS 슈터 게임 팀 프로젝트 적 AI - 1 (0) | 2026.05.19 |
| Unreal C++ 게임 루프 및 UI 설계 후 코인 획득 게임 구현 (0) | 2026.04.29 |
| 타이머 실행중에 레벨이 바뀌면 에디터가 종료되는 오류 (0) | 2026.04.21 |
| Unreal C++ 기초 8. 아이템 스폰 및 레벨 데이터 구현 (0) | 2026.04.17 |