8. 문제 발생 및 수정하기
A. 아이템 생성 위치가 (0, 0)에 너무 가까울 수도 있다.

가끔씩 캐릭터의 시작 위치인 (0, 0)과 너무 가까운 위치에 아이템이 생성되는 현상이 생긴다. 아직 발생한 적은 없지만, 시작하자마자 아이템이 캐릭터와 겹치게 생성될 우려가 있다.
//무작위 위치 (x, y 모두 캐릭터와 붙여서 생성되면 안됨)
float x, y;
do
{
x = FMath::RandRange(-2000.0f, 2000.0f);
y = FMath::RandRange(-2000.0f, 2000.0f);
} while (FMath::Abs(x) < 100.0f && FMath::Abs(y) < 100.0f);
그래서 x와 y 모두 절댓값이 100 미만이면 다시 값을 설정하게 만들어 겹치지 않게 만들었다.
B. 순회 참조 에러
이 문제에 비하면 위 문제는 사소한 문제다. 위 문제는 사소한 버그 수준인데 이 문제는 코드 구조 자체가 문제라서 컴파일에 실패하기 때문이다.

아이템 액터 코드의 기능에 아이템 생성 클래스를 사용하도록 수정할 생각이다. 그래서 아이템 액터의 헤더 상단부에 아이템 생성의 헤더를 삽입했다.

하지만, 이미 아이템 생성 클래스에서 아이템 액터 클래스가 필요하기 때문에 여기서 아이템 액터 헤더를 삽입한 상태다.

그 결과 컴파일을 시도하면 순회 참조(Circular dependency) 에러가 발생하여 실패한다.
이 문제를 해결하기 위해 자료를 찾아본 결과, 어느 한 클래스가 다른 클래스를 사용할 경우 아래와 같은 방식으로 코드를 만드는 것이 좋다고 한다.

반드시 필요한 경우가 아니라면 헤더말고 cpp파일에 추가해야 한다. 그래서 필요한 헤더를 뺀 나머지는 cpp에 옮겨서 추가했다.


그리고 헤더에는 필요한 클래스를 선언만 한다. 이렇게 하고 내용을 cpp 파일에서 직접 구현하거나 참조할 헤더를 추가하면 문제없이 사용할 수 있다.
위의 내용은 아이템 생성 액터 글래스 상단에 아이템 액터 클래스 선언만 하는 경우, 그리고 USphereComponent 클래스를 선언과 동시에 변수 타입으로 지정하는 경우다.

9. 아이템 액터의 기능을 아이템 생성 액터로 옮기기
A. 싱글톤 패턴 (이라고 불러도 되나)
문제를 해결했으니 원래 하려고 한 것을 진행한다.
내가 아이템 생성 액터를 ‘ItemMaker’나 ‘ItemSpawner’같은 것이 아닌 ‘ItemManager’로 명명한 이유가 있는데, 단순히 아이템 생성에서 그치는 것이 아니라 아이템 관련 기능을 이 액터가 전담하도록 하는 것이 내 설계 의도다. 아이템 액터를 만들 때는 임시로 구현한 것이라서 이제 이 기능을 옮기려고 한다.
앞으로 내가 만든 ItemManager 액터를 ‘아이템 매니저’라고 부른다.
//ItemManager.h
public:
static AItemManager* instance;
static AItemManager* GetInstance();
AItemManager();
protected:
virtual void BeginPlay() override;
virtual void PostInitializeComponents() override;
public:
virtual void Tick(float DeltaTime) override;
virtual void BeginDestroy() override;
//ItemManager.cpp
AItemManager* AItemManager::instance = nullptr;
AItemManager* AItemManager::GetInstance()
{
return instance;
}
void AItemManager::PostInitializeComponents()
{
Super::PostInitializeComponents();
instance = this;
}
void AItemManager::BeginDestroy()
{
Super::BeginDestroy();
instance = nullptr;
}
아이템 매니저를 쉽게 접근할 수 있도록 싱글톤 패턴을 구현한다. 게임 진행 중 오로지 1개만 존재할 액터라서 이게 적합하다고 생각한다.
다만 이게 언리얼 엔진임을 생각해서 일반적인 형태에서 살짝 틀어보려고 한다. 원래 일반적인 싱글톤 패턴이라면 GetInstance함수에서 instance가 null일때 새 객체를 생성하지만 아이템 매니저는 Main 맵 전용이자 항상 맵 내에서 1개만 존재하는 액터다. 그래서 GetInstance함수에 새 객체를 생성하는 부분을 넣지 않았다. 혹시나 다른 맵을 만들었을 경우 그 맵에서 아이템 매니저를 호출하면 안되니까 이렇게 만들었다.
instance는 PostInitializeComponents함수에서 할당하도록 만들었으며 파괴되기 시작할 때 (맵에서 벗어나거나 게임이 종료될 때) 호출되는 함수인 BeginDestroy에서 null로 만든다.
원래의 싱글톤 패턴에서 벗어나서 이걸 싱글톤 패턴이라고 불러도 될지는 모르겠다. 그냥 ‘스태틱 인스턴스’라고 불러야 겠다.
B. 아이템 액터의 코드를 아이템 매니저 코드로 옮기기
public:
//아이템 효과 적용
void InvokeItemEffect(int itemID);
먼저 아이템 액터의 코드를 옮길 곳이 필요하다. 아이템 매니저에 함수를 하나 만들었다. 이 함수는 아이템 매니저 입장에서는 외부 클래스인 아이템 액터에서 접근해야 하니 public에서 선언한다. 이 함수는 아이템 기능을 실행할 함수가 될 것이니 InvokeItemEffect라고 명명했다.
//ItemManager.cpp
void AItemManager::InvokeItemEffect(int itemID)
{
switch (itemID)
{
case 1:
UE_LOG(LogTemp, Warning, TEXT("Coin"));
break;
case 2:
UE_LOG(LogTemp, Warning, TEXT("Skull"));
break;
case 3:
UE_LOG(LogTemp, Warning, TEXT("First Aid"));
break;
default:
break;
}
}
//ItemActor.cpp
void AItemActor::OnCharacterOverlap(UPrimitiveComponent* overlappedComp, AActor* otherActor,
UPrimitiveComponent* otherComp, int otherBodyIndex, bool fromSweep, const FHitResult& sweepResult)
{
AItemManager::GetInstance()->InvokeItemEffect(itemID);
Destroy();
}
그 다음 아이템 액터에서 캐릭터와 겹칠 때 실행했던 로그 출력 기능을 아이템 매니저의 해당 함수로 옮긴다. 아이템 액터에서는 아이템 매니저에서 그 함수를 호출하는 코드로 변경한다.

10. 반복하기
이 과제의 내용은 ‘게임 루프 및 UI 재설계’이다. 여기서 ‘게임 루프’를 구현할 때가 됐다.
멀티 웨이브 기능을 구현한다. 생성된 아이템을 모두 먹을 때마다 다시 아이템을 생성한다. 그리고 이 웨이브를 기록하는 것도 필요하다.
그런데 이 멀티 웨이브 기능은 아이템의 효과가 아니라 게임의 룰이다. 슬슬 게임 모드를 만져야 할 때가 왔다.

프로젝트 생성시 삼인칭을 고른 덕분에 미리 만들어진 게임 모드가 있다. 여기서 멀티 웨이브 기능을 구현하면 될 것 같다.
A. 일단 생성 아이템을 모두 획득했는지 확인하는 기능을 만들자
그 전에 아이템 매니저에서 할 일이 있는데, 생성한 아이템을 모두 얻었는지 확인하는 기능이 필요하다.
private:
//생성한 동전 아이템 개수
int spawnCoinCount;
아이템 매니저의 헤더에 생성한 코인 아이템 개수를 저장하는 변수를 만든다.
for (int iNum = 0; iNum < spawnCount; ++iNum)
{
//무작위 위치 (x, y 모두 캐릭터와 붙여서 생성되면 안됨)
float x, y;
do
{
x = FMath::RandRange(-2000.0f, 2000.0f);
y = FMath::RandRange(-2000.0f, 2000.0f);
} while (FMath::Abs(x) < 100.0f && FMath::Abs(y) < 100.0f);
FVector spawnPoint = FVector(x, y, 100.0f);
//해당 위치에 아이템 생성
GetWorld()->SpawnActor<AItemActor>(coinItem, spawnPoint, FRotator::ZeroRotator);
++spawnCoinCount;
}
그 다음 아이템 생성 함수에서 아이템 1개를 만들 때마다 이 변수의 값을 1 증가시킨다. 지금은 동전 아이템만 만드니 그냥 SpawnActor밑에 적었다.
switch (itemID)
{
case 1:
UE_LOG(LogTemp, Warning, TEXT("Coin"));
if (--spawnCoinCount == 0)
{
UE_LOG(LogTemp, Warning, TEXT("Complete"));
}
break;
case 2:
UE_LOG(LogTemp, Warning, TEXT("Skull"));
break;
case 3:
UE_LOG(LogTemp, Warning, TEXT("First Aid"));
break;
default:
break;
}
마지막으로 아이템 기능 실행 함수에서 코인 아이템에 대하여 이 변수 값을 1 감소시킴과 동시에 0이 되면 로그를 추가로 출력하도록 만들었다.
public:
//아이템 생성
void SpawnItems(int spawnCount);
이왕 아이템 매니저를 수정하는 김에 아이템 생성 함수를 public으로 옮겼다. 게임 모드를 건드리는 작업을 하기 전 사전 작업이다.

B. 멀티 웨이브 구현
이제 게임 모드를 건드려보자

만들어진 게임 모드를 보면 기본 캐릭터를 지정한 코드가 있는데 당연히 건드릴 이유가 없다. 이 코드를 피해서 만들거 만들면 된다.
여담으로 이 코드를 잘 보면 알겠지만 기본 캐릭터는 콘텐츠 브라우저 경로를 직접 적어서 참조하는 방식을 쓰고 있다. 이 방식은 지금도 먹히기는 하지만 경로를 직접 확인해야 하는 등 불편한 점이 있기 때문에 자주 사용되는 방식은 아니다.
protected:
virtual void BeginPlay() override;
public:
//다음 웨이브 진행
void NextWave();
private:
//시작시 생성하는 동전 아이템 개수
const int START_COIN_COUNT = 10;
//현재 웨이브
int currentWave = 0;
일단 헤더에서 만들 것은 다음과 같다.
- 미리 만들어진 게임 모드 코드에는 BeginPlay 함수가 없으니 만든다.
- 다음 웨이브로 넘어가는 함수를 선언한다.
- 시작시 동전 아이템을 몇 개 만들지 정한다. 이것은 const 함수로 만든다.
- 현재 웨이브를 기록하는 변수를 선언한다. 기본값은 0이다.
void ANBC_Task8GameMode::BeginPlay()
{
Super::BeginPlay();
currentWave = 0;
NextWave();
}
void ANBC_Task8GameMode::NextWave()
{
++currentWave;
UE_LOG(LogProcess, Warning, TEXT("Wave %d Start"), currentWave);
AItemManager::GetInstance()->SpawnItems(START_COIN_COUNT);
}
그 다음 선언한 함수를 구현했다.
- BeginPlay에서 현재 웨이브를 0으로 초기화를 한 다음 바로 다음 웨이브로 넘어가는 함수를 호출한다.
- 다음 웨이브로 넘어가는 함수에는 웨이브 값을 1 증가시킨후 아이템 생성 함수를 호출한다.
- 다음 웨이브로 넘어갈 때, 즉 다음 웨이브의 시작시 로그를 출력한다.
void AItemManager::BeginPlay()
{
Super::BeginPlay();
}
게임 모드에서 아이템 생성 함수를 호출하는 것으로 만들었으니 기존 아이템 매니저의 BetginPlay에 있는 아이템 생성 함수의 호출을 제거한다.
switch (itemID)
{
case 1:
UE_LOG(LogTemp, Warning, TEXT("Coin"));
if (--spawnCoinCount == 0)
{
ANBC_Task8GameMode* gameMode = Cast<ANBC_Task8GameMode>(GetWorld()->GetAuthGameMode());
if (gameMode != nullptr)
{
gameMode->NextWave();
}
}
break;
case 2:
UE_LOG(LogTemp, Warning, TEXT("Skull"));
break;
case 3:
UE_LOG(LogTemp, Warning, TEXT("First Aid"));
break;
default:
break;
}
그리고 동전 아이템을 모두 먹을 때 로그를 출력한 부분을 제거하고 게임 모드의 다음 웨이브로 넘어가는 함수를 호출한다.
게임 모드는 미리 만들어진 만큼 월드에도 미리 등록되어 있다. 덕분에 월드를 통하여 호출이 가능하기 때문에 게임 모드에는 굳이 스태틱 인스턴스를 만들 필요가 없다. 다만 캐스팅이 필요할 뿐이다.

컴파일 후 테스트 해보니 의도한 대로 잘 작동한다. 동전 10개를 먹을 때마다 다음 웨이브가 시작된다. 이로써 게임을 계속해서 반복할 수 있다.
C. 문제가 또 발생했다.
이전에 수정한 문제점 중 하나가 캐릭터와 너무 가까이 생성되는 아이템 문제였다. 그런데 게임 루프를 구현하고 보니 캐릭터의 위치가 매 웨이브 시작마다 다르다는 점을 간과했다. 캐릭터가 (0, 0)에 있을 것 만을 생각해서 수정했기 때문에 발생한 문제점이라서 캐릭터의 위치를 확인하고 그 주변을 생성하지 않도록 변경해야 한다.

플레이어 캐릭터 코드 또한 미리 만들어졌다. 하지만 지금은 이 코드를 수정할 필요는 없다. 왜냐하면 이 코드 또한 월드에 미리 등록됐기 때문이다. (애초에 게임 모드에 미리 적힌 코드가 캐릭터를 월드에 등록하는 코드다.)
private:
//캐릭터 액터
UPROPERTY()
AActor* playerCharacter = nullptr;
아이템 매니저의 헤더에 액터 변수 하나 선언한다. 이 액터는 플레이어 캐릭터를 할당할 것이다. 게임 시작 후 할당이 가능하기 때문에 UPROPERTY에 아무런 내용을 적지 않았다.
void AItemManager::BeginPlay()
{
Super::BeginPlay();
playerCharacter = GetWorld()->GetFirstPlayerController()->GetPawn();
}
그리고 BeginPlay에서 월드를 통해 할당한다. 월드를 통해 플레이어 컨트롤러에 접근한 다음에 이 컨트롤러가 조종하는 캐릭터를 가져오면 된다.
void AItemManager::SpawnItems(int spawnCount)
{
//1개 이상 생성
if (spawnCount <= 0)
return;
//현재 캐릭터 위치
FVector curCharPos = playerCharacter->GetActorLocation();
for (int iNum = 0; iNum < spawnCount; ++iNum)
{
//무작위 위치 (캐릭터와 너무 가까운 곳에서 생성하면 안됨)
FVector spawnPoint;
do
{
float x = FMath::RandRange(-MAP_SIZE_HALF, MAP_SIZE_HALF);
float y = FMath::RandRange(-MAP_SIZE_HALF, MAP_SIZE_HALF);
spawnPoint = FVector(x, y, ITEM_FROM_FLOOR);
} while (FVector::DistSquared(spawnPoint, curCharPos) < ITEM_FROM_PLAYER);
//해당 위치에 아이템 생성
GetWorld()->SpawnActor<AItemActor>(coinItem, spawnPoint, FRotator::ZeroRotator);
++spawnCoinCount;
}
}
아이템 생성 함수는 이 캐릭터 위치를 확인하여 캐릭터 근처에 생성되지 않도록 만든다. 참고로 FVector의 Distance함수보다 DistSquared함수가 더 빠르기 때문에 이 함수를 채택했다. (벡터의 각 값의 차를 제곱한 값을 모두 더한 값이 후자 함수의 반환 값이고 이 값의 제곱근이 전자의 반환 값이다.)
private:
//맵 크기 (한 변의 절반)
const float MAP_SIZE_HALF = 2000.0f;
//바닥과 아이템 사이의 거리
const float ITEM_FROM_FLOOR = 100.0f;
//아이템 생성시 아이템과 캐릭터의 최소 거리 (제곱 합)
const float ITEM_FROM_PLAYER = 1000.0f;
그런데 아이템 생성 함수를 잘 보니 그냥 숫자를 적은 부분이 많은데 문제를 수정하는 김에 이 부분도 수정하면 되겠다. 헤더에 적절한 const 변수를 선언해주고 이에 맞게 아이템 생성 함수도 수정했다.
'언리얼엔진 학습 및 과제' 카테고리의 다른 글
| 게임 루프 및 UI 재설계 과제 (7) (0) | 2025.10.16 |
|---|---|
| 게임 루프 및 UI 재설계 과제 (6) (0) | 2025.10.15 |
| 게임 루프 및 UI 재설계 과제 (4) (0) | 2025.10.02 |
| 게임 루프 및 UI 재설계 과제 (3) (0) | 2025.10.01 |
| 게임 루프 및 UI 재설계 과제 (2) (0) | 2025.09.30 |