상자 열 때마다 같은 무기만 나온다면? 랜덤 획득 시스템 설계하기

개발 노트 — 루팅 시스템 설계

단순 균등 랜덤의 함정

Enter the Room에서는 방을 클리어할 때마다 보물 상자가 생성되고, 상자를 열면 무기 4종(권총, 기관단총, 소총, 샷건) 중 하나를 랜덤으로 획득합니다. 기능 자체는 간단해 보이지만, 막상 구현하고 플레이해보면 “단순 균등 랜덤”만으로는 플레이 경험이 어색해지는 순간들이 있습니다.

가장 기본적인 구현은 이렇게 짤 수 있습니다.

public class WeaponChest : MonoBehaviour
{
    public WeaponType[] possibleWeapons;

    public WeaponType Open()
    {
        int index = Random.Range(0, possibleWeapons.Length);
        return possibleWeapons[index];
    }
}

Random.Range()로 무기 배열에서 하나를 뽑는 방식입니다. 코드는 짧지만, 실제로 플레이해보면 두 가지 문제가 드러납니다.

문제 1: 이미 들고 있는 무기가 또 나온다

플레이어가 이미 소총을 들고 있는 상태에서 상자를 열었는데 또 소총이 나오면, 이 상자는 사실상 “낭비”가 됩니다. 균등 랜덤은 이런 맥락을 전혀 고려하지 않습니다.

해결책: 현재 보유 중인 무기를 후보에서 제외하거나, 가중치를 낮춥니다.

public WeaponType Open(WeaponType currentWeapon)
{
    var candidates = possibleWeapons
        .Where(w => w != currentWeapon)
        .ToList();

    // 만약 모든 무기가 제외되는 예외 상황이면(무기가 1종류뿐인 경우 등) 원래 목록 사용
    if (candidates.Count == 0)
    {
        candidates = possibleWeapons.ToList();
    }

    return candidates[Random.Range(0, candidates.Count)];
}

이렇게 하면 상자를 열 때마다 최소한 “지금 안 갖고 있는 무기 중 하나”가 보장되어, 매번 의미 있는 선택이 됩니다.

문제 2: 특정 무기만 계속 몰아서 나온다

균등 랜덤은 확률적으로 “동일한 무기가 연속으로 나오는” 상황을 배제하지 않습니다. 드물게 발생하는 일이지만, 플레이어 입장에서는 “이거 확률 조작 아니야?”라는 불신으로 이어지기 쉽습니다. 실제로는 순수 확률적으로 정상적인 현상이라도, 체감 공정성은 다른 문제입니다.

해결책: 최근 획득한 무기의 가중치를 일시적으로 낮추는 방식을 씁니다.

public class WeaponLootTable
{
    private Dictionary<WeaponType, float> _weights;
    private WeaponType? _lastDropped;

    public WeaponType RollWeapon()
    {
        var adjustedWeights = new Dictionary<WeaponType, float>(_weights);

        // 방금 나온 무기는 확률을 절반으로 낮춤 (완전 배제는 아님)
        if (_lastDropped.HasValue)
        {
            adjustedWeights[_lastDropped.Value] *= 0.5f;
        }

        WeaponType result = WeightedRandomPick(adjustedWeights);
        _lastDropped = result;
        return result;
    }

    private WeaponType WeightedRandomPick(Dictionary<WeaponType, float> weights)
    {
        float total = weights.Values.Sum();
        float roll = Random.Range(0f, total);

        float cumulative = 0f;
        foreach (var pair in weights)
        {
            cumulative += pair.Value;
            if (roll <= cumulative)
            {
                return pair.Key;
            }
        }

        return weights.Keys.Last(); // fallback
    }
}

완전히 배제하지 않고 가중치만 낮추는 이유는, 너무 강하게 배제하면 오히려 “다음엔 무조건 다른 무기”라는 규칙성이 생겨서 진짜 랜덤처럼 느껴지지 않기 때문입니다. 확률을 낮추되 가능성은 남겨두는 정도가 자연스러운 체감을 만듭니다.

가중치 테이블로 확장하기

무기 종류가 늘어나거나, 특정 무기를 더 희귀하게 만들고 싶다면 처음부터 가중치 테이블로 설계하는 게 좋습니다.

[System.Serializable]
public class WeaponDropEntry
{
    public WeaponType weapon;
    public float weight;
}

public class WeaponLootTable : MonoBehaviour
{
    public List<WeaponDropEntry> dropTable;

    public WeaponType Roll()
    {
        float total = dropTable.Sum(e => e.weight);
        float roll = Random.Range(0f, total);

        float cumulative = 0f;
        foreach (var entry in dropTable)
        {
            cumulative += entry.weight;
            if (roll <= cumulative)
            {
                return entry.weapon;
            }
        }

        return dropTable.Last().weapon;
    }
}

예를 들어 권총(기본 무기)은 가중치를 낮게, 샷건처럼 화끈한 무기는 조금 더 높게 잡는 식으로 인스펙터에서 바로 조정할 수 있습니다. 이 구조는 지난 글에서 다뤘던 ScriptableObject 기반 데이터 관리와도 잘 어울립니다 — 드롭 테이블 자체를 에셋으로 분리해두면, 코드 수정 없이 기획 값만 바꿔서 드롭률을 튜닝할 수 있습니다.

정리: 루팅 시스템에서 고려할 세 가지

  1. 맥락을 반영하기: 플레이어의 현재 상태(이미 보유한 아이템 등)를 고려하지 않은 순수 랜덤은 낭비되는 보상을 만듭니다.
  2. 체감 공정성을 신경 쓰기: 확률적으로는 정상이어도 “몰림 현상”은 불신을 만듭니다. 직전 결과에 가중치 페널티를 주는 정도로 완화할 수 있습니다.
  3. 가중치는 데이터로 분리하기: 하드코딩된 균등 확률보다, 조정 가능한 가중치 테이블로 설계해두면 나중에 밸런싱하기 훨씬 편합니다.

랜덤 보상 시스템은 “그냥 Random.Range() 하나면 끝”처럼 보이지만, 실제로 플레이해보면 위와 같은 디테일들이 체감 만족도를 크게 좌우합니다. 아이템 드롭, 가챠, 랜덤 보상 시스템을 설계하고 계시다면, 순수 확률보다 “플레이어가 어떻게 느낄지”를 기준으로 조정해보시길 추천합니다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤