← 모든 글

언리얼과 C++이용한 웨이브가 있는 게임 2 - 아이템 가챠와 점수 저장

웨이브 UI를 개선하고, 물음표 상자를 획득할 때 확률에 따라 코인·회복·지뢰 효과를 결정하도록 아이템 구조를 리팩터링한 과정을 정리했습니다.

Unreal Engine 5 C++Unreal Engine 5GameModeGameStateGameInstanceDataTableWeighted RandomUMGRefactoring

웨이브 게임에 확률형 아이템 흐름 추가하기

지난 글에서는 GameMode, GameState, Widget의 역할을 나누고 Spawn Volume과 Data Table로 필드 아이템을 생성했다. 아이템을 수집하면 목표 개수를 확인해 다음 웨이브나 다음 스테이지로 이동하는 기본 진행 구조도 만들었다.

지난 글: 언리얼과 C++이용한 웨이브가 있는 게임 1 - 웨이브 시스템

이번 작업에서는 그 아이템 시스템의 방향을 바꿨다. 필드에서 코인이나 회복 아이템의 정체를 바로 보여주는 대신 동일한 물음표 상자를 생성하고, 플레이어가 상자를 획득하는 순간 실제 결과를 추첨한다. 여기에 지뢰 효과, 결과 UI, 스테이지 사이의 점수 저장 구조도 추가했다.

이번 변경의 핵심은 필드에 놓이는 Actor와 획득 결과를 분리한 것이다. 이전에는 코인과 체력 회복 아이템이 각각 별도의 Actor 클래스로 자신의 효과를 처리했다. 현재는 필드에 AFieldItem 하나를 배치하고, 획득 시 Data Table에서 코인, 회복 아이템, 지뢰 중 하나를 확률에 따라 선택한다.

아이템별 Actor 구조를 데이터와 게임 규칙 중심으로 변경한 전후 비교

이번에 변경한 내용

물음표 상자를 획득할 때 결과 정하기

이전 구조에서는 필드에 스폰되는 시점에 코인이나 회복 아이템의 종류가 이미 결정됐다. 이번에는 필드에는 동일한 물음표 상자를 보여주고, 플레이어가 상자를 획득하는 순간 AFieldItem::Roll()을 호출해 결과를 결정하도록 바꿨다.

Data Table의 각 행은 다음 정보를 가진다.

추첨은 모든 행의 DropRate를 더한 뒤, 0부터 전체 가중치 사이의 난수를 만든다. 각 행의 구간을 누적하면서 난수가 포함된 행을 찾는 가중치 추첨 방식이다.

전체 가중치 = 모든 DropRate의 합
난수 = Random(0, 전체 가중치)

Coin 구간       0 ---------------- 50
HealthPack 구간 50 ----------- 80
Mine 구간       80 ----------- 100

Drop Rate가 50, 30, 20이라면 결과 확률은 각각 50%, 30%, 20%가 된다. 합계를 반드시 100으로 맞출 필요가 없고 상대적인 비율만 유지하면 된다는 장점이 있다.

DropRate 50, 30, 20을 누적 구간으로 만든 가중치 추첨 예시

선택된 행의 효과 수치는 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가 체력, 점수, 레벨 규칙을 모두 알지 않아도 되어 역할이 이전보다 분명해졌다.

물음표 상자를 획득한 뒤 Coin, HealthPack, Mine 효과를 적용하고 결과 UI를 표시하는 흐름

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 체력 감소

리팩터링하면서 달라진 점

가장 큰 변화는 아이템의 외형, 데이터, 효과 처리를 분리한 것이다.

기존의 아이템별 상속 구조는 종류가 늘어날수록 비슷한 Actor 클래스가 계속 생기는 형태였다. 현재 구조는 공통 Actor 하나와 데이터 중심의 분기로 바뀌어, 아이템 확률과 수치를 에디터에서 조정하기 쉬워졌다. 이번 단계에서는 아이템 획득 결과를 Data Table 기반으로 바꾸고, 효과 적용과 점수 저장의 책임을 분리하는 데 초점을 맞췄다. 다음 작업에서는 남은 연결 부분을 정리한 뒤 가챠 UI 애니메이션과 웨이브 완료 조건을 함께 완성할 예정이다.