언리얼과 C++이용한 웨이브가 있는 게임 2 - 아이템 가챠와 점수 저장
웨이브 UI를 개선하고, 물음표 상자를 획득할 때 확률에 따라 코인·회복·지뢰 효과를 결정하도록 아이템 구조를 리팩터링한 과정을 정리했습니다.
웨이브 게임에 확률형 아이템 흐름 추가하기
지난 글에서는 GameMode, GameState, Widget의 역할을 나누고 Spawn Volume과 Data Table로 필드 아이템을 생성했다. 아이템을 수집하면 목표 개수를 확인해 다음 웨이브나 다음 스테이지로 이동하는 기본 진행 구조도 만들었다.
지난 글: 언리얼과 C++이용한 웨이브가 있는 게임 1 - 웨이브 시스템
이번 작업에서는 그 아이템 시스템의 방향을 바꿨다. 필드에서 코인이나 회복 아이템의 정체를 바로 보여주는 대신 동일한 물음표 상자를 생성하고, 플레이어가 상자를 획득하는 순간 실제 결과를 추첨한다. 여기에 지뢰 효과, 결과 UI, 스테이지 사이의 점수 저장 구조도 추가했다.
이번 변경의 핵심은 필드에 놓이는 Actor와 획득 결과를 분리한 것이다. 이전에는 코인과 체력 회복 아이템이 각각 별도의 Actor 클래스로 자신의 효과를 처리했다. 현재는 필드에 AFieldItem 하나를 배치하고, 획득 시 Data Table에서 코인, 회복 아이템, 지뢰 중 하나를 확률에 따라 선택한다.
이번에 변경한 내용
- 지난 글에서 만든 웨이브 HUD와 획득 흐름을 새 아이템 구조에 맞게 조정했다.
- 필드 아이템의 외형을 물음표 상자로 통일했다.
- 상자를 획득하는 순간 아이템 결과를 추첨하도록 변경했다.
- 코인, 체력 회복, 지뢰를
EItemType으로 구분했다. - 아이템별 Actor 클래스를 제거하고 효과 처리를 GameMode에 모았다.
- 레벨이 바뀌어도 점수를 유지하도록 GameInstance에 점수를 저장했다.
- 가챠 결과를 보여주기 위한 Widget 클래스를 추가했다.
물음표 상자를 획득할 때 결과 정하기
이전 구조에서는 필드에 스폰되는 시점에 코인이나 회복 아이템의 종류가 이미 결정됐다. 이번에는 필드에는 동일한 물음표 상자를 보여주고, 플레이어가 상자를 획득하는 순간 AFieldItem::Roll()을 호출해 결과를 결정하도록 바꿨다.
Data Table의 각 행은 다음 정보를 가진다.
ItemType: Coin, HealthPack, Mine 중 실제 효과AmountMin,AmountMax: 효과 수치의 최소·최대 범위DropRate: 해당 결과가 선택될 가중치
추첨은 모든 행의 DropRate를 더한 뒤, 0부터 전체 가중치 사이의 난수를 만든다. 각 행의 구간을 누적하면서 난수가 포함된 행을 찾는 가중치 추첨 방식이다.
전체 가중치 = 모든 DropRate의 합
난수 = Random(0, 전체 가중치)
Coin 구간 0 ---------------- 50
HealthPack 구간 50 ----------- 80
Mine 구간 80 ----------- 100
Drop Rate가 50, 30, 20이라면 결과 확률은 각각 50%, 30%, 20%가 된다. 합계를 반드시 100으로 맞출 필요가 없고 상대적인 비율만 유지하면 된다는 장점이 있다.
선택된 행의 효과 수치는 RollAmount()로 다시 결정한다. 같은 코인이 나와도 Data Table에 설정한 범위 안에서 획득량이 달라질 수 있다.
아이템 종류를 Enum으로 통합하기
아이템 결과는 EItemType으로 구분한다.
UENUM(BlueprintType)
enum class EItemType : uint8
{
Coin,
HealthPack,
Mine
};
기존에는 AFieldCoin, AFieldHealthPack처럼 효과마다 클래스를 만들고 OnPicked()를 오버라이드했다. 현재는 이 클래스들을 제거하고, 하나의 AFieldItem과 Data Table이 결과 데이터를 제공하도록 단순화했다.
왜 이렇게 했냐면, 아이템을 습득할 때 주사위를 돌리기 위해서이다. 따라서 이제는 아이템별로 필드 아이템을 만들 필요가 없어졌다.
GameMode에서 아이템 효과 적용하기
플레이어가 AFieldItem과 Overlap하면 현재 GameMode에 획득 처리를 요청한다. GameMode는 추첨된 ItemType에 따라 실제 게임 규칙을 적용한다.
switch (Row->ItemType)
{
case EItemType::Mine:
// 플레이어 체력 감소
break;
case EItemType::Coin:
// GameInstance의 점수 증가
break;
case EItemType::HealthPack:
// 최대 체력을 넘지 않도록 회복
break;
}
Mine은 플레이어의 UStatComponent에서 현재 체력을 가져와 감소시킨다. HealthPack은 현재 체력을 증가시키되 MaxHealth를 넘지 않도록 제한한다. Coin은 레벨 전환 뒤에도 유지되어야 하므로 GameMode가 아니라 GameInstance의 점수를 증가시킨다.
필드 Actor는 무엇이 나올 수 있는지를 제공하고, GameMode는 결과가 게임에 어떤 영향을 주는지를 결정한다. Actor가 체력, 점수, 레벨 규칙을 모두 알지 않아도 되어 역할이 이전보다 분명해졌다.
GameInstance에 점수 저장하기
GameMode와 GameState는 레벨이 바뀌면 새로 생성된다. 여러 스테이지를 이동하는 동안 누적 점수를 유지하려면 수명이 더 긴 GameInstance가 필요하다.
void UIngameGameInstance::AddScore(int InAmount)
{
Score += InAmount;
}
void UIngameGameInstance::ClearScore()
{
Score = 0;
}
int UIngameGameInstance::GetScore()
{
return Score;
}
코인을 획득하면 AddScore()를 호출하고, 새로운 게임을 시작할 때만 ClearScore()를 호출할 수 있다. 이 방식으로 Stage 1에서 얻은 점수를 Stage 2와 Stage 3에서도 이어서 사용할 수 있다.
다만 GameInstance는 실행 중인 게임 세션 동안 값을 유지하는 용도다. 프로그램을 종료했다가 다시 실행해도 점수를 남겨야 한다면 이후에는 SaveGame 시스템을 추가해야 한다.
변경된 전체 흐름
웨이브 시작
↓
Spawn Volume이 물음표 상자 생성
↓
플레이어와 AFieldItem Overlap
↓
Data Table의 DropRate로 결과 추첨
↓
Coin / HealthPack / Mine 결정
↓
GameMode가 플레이어와 게임 상태에 효과 적용
├─ Coin → GameInstance 점수 증가
├─ HealthPack → StatComponent 체력 회복
└─ Mine → StatComponent 체력 감소
리팩터링하면서 달라진 점
가장 큰 변화는 아이템의 외형, 데이터, 효과 처리를 분리한 것이다.
AFieldItem은 필드에 존재하는 상자와 추첨을 담당한다.FFieldItemSpawnRow는 결과 종류, 수치 범위, 확률을 담는다.AIngameGameMode는 결과에 따른 게임 규칙을 적용한다.UStatComponent는 플레이어 체력을 관리한다. (추가적으로 다른 것도 관리할 수 있다.)UIngameGameInstance는 스테이지 사이의 누적 점수를 보관한다.UGatchaWidget은 플레이어에게 추첨 결과를 보여준다.
기존의 아이템별 상속 구조는 종류가 늘어날수록 비슷한 Actor 클래스가 계속 생기는 형태였다. 현재 구조는 공통 Actor 하나와 데이터 중심의 분기로 바뀌어, 아이템 확률과 수치를 에디터에서 조정하기 쉬워졌다. 이번 단계에서는 아이템 획득 결과를 Data Table 기반으로 바꾸고, 효과 적용과 점수 저장의 책임을 분리하는 데 초점을 맞췄다. 다음 작업에서는 남은 연결 부분을 정리한 뒤 가챠 UI 애니메이션과 웨이브 완료 조건을 함께 완성할 예정이다.