확률 미니게임 설계 – 확률 누적과 최종 판정 분리 구조

목차

1. 요구 사항

2. 설계 목표

3. 흐름도

4. 구현

        4.1. 확률 미니 게임 내부 구조

       4.2. 게임 자동 실행 구조

       4.3. 최종 확률 확정 및 결과 판정

       4.4. 랜덤 속성 부여 및 결과 적용

5. 개발 의도

1. 시스템 요구 사항

속성 부여 시스템의 네 번째 핵심은 확률 미니 게임의 결과가 단순한 연출에 그치지 않고, 최종 속성 부여 결과와 정확히 연결되도록 하는 것이었다.

확률 미니 게임은 속성 부여 버튼을 누른 직후 결과를 반환하는 방식이 아니라, 플레이어의 입력 과정을 거쳐 확률이 점진적으로 변화한 뒤 최종 성공 여부가 결정되는 구조다.

이 단계에서는 미니 게임 내부의 확률 변화, 최종 확률 확정 시점, 그리고 그 결과가 아이템 데이터에 적용되는 흐름이 명확히 분리되어야 했다.

따라서 이 단계에서는 확률 변화 과정과 최종 결과 적용을 명확히 분리하여, 확률 미니게임이 하나의 독립된 판단 단계로 동작하도록 설계하는 것을 목표로 했다.

2. 설계  목표

- 확률 계산과 속성 적용 로직을 분리할 것

- 미니게임 내부에서만 확률을 변화시킬 것

- 최종 확률 확정 시점은 하나로 제한할 것

- 결과 적용은 단일 진입점에서 처리할 것

- 자동 실행과 수동 입력이 완전히 동일한 로직 경로를 사용하도록 할 것

3. 흐름도

최종 확률 확정 단계에서는 값만 결정되고, 실제 성공·실패 판정은 그 이후 단 한 번만 수행된다.

4. 구현

4.1. 확률 미니 게임 내부 구조
public void OnButtonClick(int buttonIndex)
{
    if (Random.Range(0.0f, 100.0f) <= probability)
    {
        buttons[buttonIndex].GetComponent<Image>().color = new Color(0, 1, 0.933f);
        probability -= 5.0f;
    }
    else
    {
        buttons[buttonIndex].GetComponent<Image>().color = new Color(0, 0.33f, 0.4f);
        probability += 5.0f;
    }

    if (buttonIndex != buttons.Length - 1)
    {
        buttons[buttonIndex].interactable = false;
        ActivateButton(buttonIndex + 1);
        UpdateProbabilityTxt();
    }
    else
    {
        propertySystem.UpdateProbability(probability);
    }
}

OnButtonClick 함수는 확률 미니게임에서 특정 버튼이 눌렸을 때, 해당 버튼 입력 1회를 확률을 변화시키는 단일 연산 단계로 처리하는 역할을 한다.

함수가 호출되면 가장 먼저 Random.Range()를 통해 0 이상 100 미만의 실수 난수 하나를 생성한다.

이 난수 값은 현재 확률 값(probability)과 즉시 비교되며, 난수가 확률 값 이하인지에 따라 분기가 갈린다.

즉, Random.Range는 이번 속성 부여가 성공했는가를 판단하기 위해 사용된 것이 아니라, 플레이어의 선택에 따라 확률이 어떻게 변화하는지를 결정하기 위한 수단으로 사용된다.

Unity의 Random.Range는 프레임 독립적이며, 미니게임 도중 여러 번 호출되더라도 상태를 내부적으로 유지하지 않기 때문에 누적 확률 계산과 충돌하지 않는다.

Random.Range를 실수(float) 범위로 사용한 이유는, 확률 값을 정수 단위가 아닌 연속적인 퍼센트 값으로 다루기 위함이며, 이후 확률 변화 폭을 더 세밀하게 조정할 수 있는 여지를 남기기 위함이다.

이 비교 방식은 현재 확률이 30이라면, 0~100 중 30 이하의 값이 나올 확률은 30%라는 확률의 정의 자체를 코드로 그대로 옮긴 형태다.

즉, probability는 단순한 숫자가 아니라 이 입력이 성공 판정으로 처리될 확률(%)을 의미한다.

따라서 난수가 probability보다 작거나 같은 경우는 확률적으로 성공이 발생한 경우에 해당하며, 이 조건을 만족하지 못한 경우는 실패로 처리된다.

이 조건이 참일 경우, 해당 버튼의 색상으로 변경하여, 입력이 성공적인 결과였음을 즉시 시각적으로 표현한다.

그 다음 probability -= 5.0f를 수행하여 다음 버튼 입력에서 사용할 확률 값을 의도적으로 낮춘다.

이 확률 감소는 이번 입력에서 운이 좋았으나, 다음 입력은 더 불리해진다는 리스크 증가 구조를 명시적으로 코드에 반영한 부분이다.

즉, 연속 성공이 점점 어려워지도록 설계된 구조다.

반대로 난수가 probability보다 큰 경우에는 이번 입력이 실패 판정으로 처리된다.

이 경우 버튼 색상을 실패 색상으로 변경하고, probability += 5.0f를 통해 다음 버튼 입력에서 사용할 확률 값을 증가시킨다.

이는 이번 입력에서 실패했으나, 다음 입력에서는 보정이 들어간다는 확률 완화 메커니즘을 구현한 것이다.

이로 인해 플레이어는 연속 실패 시에도 다음 시도에서 점점 유리해진다는 피드백을 받게 된다.

이렇게 if–else 블록 전체는 이번 버튼 입력 하나를 ‘확률 조정 이벤트’로 처리하는 역할을 수행한다.

여기서 중요한 점은, 이 단계의 성공·실패는 속성 부여 결과와는 아무런 직접적인 연관이 없다는 것이다.

이 판정은 오직 확률 값을 어떻게 변화시킬지를 결정하기 위한 중간 판단이다.

이후 코드에서는 현재 눌린 버튼이 마지막 버튼인지 여부를 검사한다.

마지막 버튼이 아니라면, 현재 버튼의 interactable을 false로 설정하여 같은 버튼이 다시 눌리지 않도록 차단하고, ActivateButton(buttonIndex + 1)을 호출해 다음 버튼 하나만 활성화한다.

동시에 UpdateProbabilityTxt 함수를 호출하여 방금 갱신된 확률 값을 UI에 즉시 반영한다.

반대로 현재 버튼이 마지막 버튼이라면, 더 이상 버튼 입력을 받지 않고, UpdateProbability(probability)를 호출해 지금까지 누적된 확률 값을 그대로 최종 판정 단계로 전달한다.

이 함수는 확률을 계산하거나 최종 결과를 판정하는 역할을 가지지 않으며, 오직 버튼 입력에 따른 확률 변화와 진행 상태를 단계적으로 관리하는 책임만을 가진다.

4.2. 게임 자동 실행 구조
public void StartAutoPlay()
{
    if (isAutoPlaying) return;
    isAutoPlaying = true;
    StartCoroutine(AutoPlayRoutine());
}

private IEnumerator AutoPlayRoutine()
{
    int startIndex = 0;

    for (int i = 0; i < buttons.Length; i++)
    {
        if (buttons[i].interactable)
        {
            startIndex = i;
            break;
        }
    }

    for (int i = startIndex; i < buttons.Length; i++)
    {
        if (!gameObject.activeInHierarchy)
            break;

        OnButtonClick(i);

        if (i < buttons.Length - 1)
            yield return new WaitForSeconds(0.3f);
    }

    isAutoPlaying = false;
}

자동 실행 기능은 확률 미니게임을 반복적으로 사용해야 하는 상황을 고려하여 추가된 기능이다.

자동 실행은 현재 활성화된 버튼부터 마지막 버튼까지 동일한 클릭 흐름을 그대로 재현한다.

자동 실행 기능은 확률 미니게임에서 플레이어가 수동으로 버튼을 누르는 과정을 그대로 자동으로 반복 실행하는 기능이다.

StartAutoPlay 함수는 확률 미니게임의 버튼 입력 과정을 자동으로 실행하기 위한 진입점 역할을 한다.

이 함수가 호출되면 가장 먼저 isAutoPlaying 플래그를 검사하여, 이미 자동 실행이 진행 중인 상태인지 여부를 확인한다.

자동 실행 중인 상태에서 다시 실행될 경우 동일한 버튼 입력 로직이 중첩 호출되며 확률 계산이 중복 적용될 수 있기 때문에, 이러한 상황을 구조적으로 차단하기 위해 이미 자동 실행 중이면 즉시 return으로 함수를 종료한다.

자동 실행이 시작 가능한 상태인 경우에만 isAutoPlaying을 true로 설정하여 현재 자동 실행 상태임을 명시하고, 실제 자동 입력 처리를 담당하는 AutoPlayRoutine 코루틴을 시작한다.

이 시점부터 자동 실행은 프레임 단위가 아닌 시간 흐름을 가진 비동기 로직으로 동작하게 된다.

AutoPlayRoutine 코루틴은 자동 실행의 실제 동작을 담당하는 코루틴으로, 먼저 현재 미니게임이 어느 단계까지 진행된 상태인지를 파악하는 것부터 시작한다.

이를 위해 버튼 배열을 처음부터 순회하며 interactable 상태가 true인 버튼을 찾고, 그 버튼의 인덱스를 시작 지점으로 저장한다.

이 과정은 미니게임이 항상 첫 번째 버튼부터 시작된다는 가정을 두지 않기 위해 존재하며, 자동 실행이 중간 단계에서 시작되더라도 현재 활성화된 버튼부터 자연스럽게 이어서 진행되도록 하기 위한 장치다.

즉, 자동 실행은 미니게임의 현재 상태를 그대로 이어받아 동작하도록 설계되어 있다.

시작 인덱스가 결정되면, 해당 버튼부터 마지막 버튼까지 순차적으로 반복문을 실행한다.

반복문 내부에서는 먼저 미니게임 오브젝트가 현재 활성 상태인지 확인한다.

만약 미니게임이 비활성화된 상태라면, 이는 이미 결과 화면으로 전환되었거나 외부 로직에 의해 미니게임이 종료된 상황이므로, 자동 실행을 즉시 중단한다.

이를 통해 자동 실행이 결과 처리 단계와 충돌하거나, 이미 종료된 게임 오브젝트에 대해 버튼 입력을 시도하는 상황을 방지한다.

미니게임이 활성 상태인 경우에는 현재 버튼 인덱스를 인자로 하여 OnButtonClick(i)를 호출한다.

이 호출은 플레이어가 실제로 버튼을 클릭했을 때와 완전히 동일한 코드 경로를 사용하며, 확률 조정 로직, 버튼 비활성화, 다음 버튼 활성화, 확률 UI 갱신까지 모두 동일하게 처리된다.

자동 실행이 별도의 확률 계산 로직을 가지지 않고 수동 입력과 동일한 함수만을 호출하도록 설계한 이유는, 자동 실행과 수동 입력 간에 결과 차이가 발생할 가능성을 원천적으로 제거하기 위함이다.

즉, 자동 실행은 자동으로 눌러주는 행위만 담당하고, 게임 로직 자체에는 어떠한 분기도 추가하지 않는다.

각 버튼 입력 이후에는 다음 버튼이 존재하는 경우에만 WaitForSeconds()를 통해 0.3초씩 짧은 대기 시간을 둔다.

이는 모든 버튼 입력이 한 프레임에 연속 실행되는 것을 방지하고, 사람이 버튼을 누르는 것과 유사한 시간 흐름을 만들어 주기 위함이다.

또한 이 대기 시간 덕분에 버튼 색상 변화나 확률 UI 갱신과 같은 시각적 피드백이 자연스럽게 노출된다.

모든 버튼 입력 처리가 끝나면 자동 실행이 완료되었음을 의미하므로 isAutoPlaying을 false로 되돌려, 이후 다시 자동 실행을 시작할 수 있는 상태로 복구한다.

이 구조를 통해 자동 실행은 단 한 번의 명확한 실행 구간을 가지며, 실행 중과 종료 이후의 상태가 명확히 구분된다.

* 결과

처음부터 자동 클릭
플레이어가 클릭하다가 중간부터 자동 클릭

4.3. 최종 확률 확정 및 결과 판정
public void UpdateProbability(float newProbability)
{
    result.SetActive(true);
    miniGame.SetActive(false);

    bool isUpgradeSuccessful =
        Random.Range(0.0f, 100.0f) <= newProbability;

    if (isUpgradeSuccessful && currentItem != null)
    {
        ApplyRandomPropertyToCurrentItem();
    }
    else
    {
        resultTxt.text = "추가 실패!";
    }
}

UpdateProbability 함수는 확률 미니게임 전체 흐름에서 단 한 번만 호출되는 최종 판정 함수로, 실제 속성 부여 성공·실패를 결정하는 유일한 진입점이다.

이 함수가 호출되는 시점은 모든 버튼 입력이 끝나고, 미니게임 내부에서 확률 변화가 더 이상 발생하지 않는 상태다.

즉, 이 시점의 newProbability 값은 플레이어의 모든 입력 결과가 누적된 최종 확률을 의미한다.

함수가 실행되면 가장 먼저 미니게임 UI를 비활성화하고 결과 UI를 활성화하여, 플레이어의 시선을 즉시 결과 영역으로 전환한다.

이 UI 전환은 단순한 연출이 아니라, 더 이상 확률을 조작할 수 없는 상태로 게임의 단계가 넘어갔음을 명확히 선언하는 역할을 한다.

이후 Random.Range(0.0f, 100.0f)를 사용해 0 이상 100 미만의 실수 난수 하나를 생성하고, 이 값을 최종 확률 값인 newProbability와 비교하여 실제 속성 부여가 성공인지 실패인지를 판정한다.

이 비교 방식은 확률의 정의를 그대로 코드로 옮긴 구조로, 예를 들어 최종 확률이 40이라면 0~100 사이의 난수 중 40 이하의 값이 나올 확률은 40%가 된다.

따라서 난수가 newProbability 이하인 경우를 성공으로 간주하는 조건은 확률 개념을 직관적으로 구현한 형태다.

앞선 미니게임 단계에서 사용되던 난수 판정이 확률 값을 조정하기 위한 중간 판단이었다면, 이 단계의 난수 판정은 실제 아이템 데이터에 영향을 주는 유일한 결과 판정이라는 점에서 의미가 완전히 다르다.

판정 결과가 성공이고 현재 아이템이 존재하는 경우에만 실제 속성 변경을 담당하는 ApplyRandomPropertyToCurrentItem 함수가 호출된다.

이로써 아이템 데이터에 대한 변경은 오직 이 단일 지점에서만 발생하게 되며, 미니게임 도중이나 확률 변화 단계에서는 어떤 경우에도 아이템 데이터가 수정되지 않는다.

반대로 실패로 판정된 경우에는 아이템 데이터에는 아무런 변경도 가하지 않고, 결과 텍스트만 갱신하여 실패 사실을 전달한다.

이 구조를 통해 확률 계산 과정과 결과 적용 과정이 명확히 분리되며, 확률 판정이 버튼 입력 도중 여러 번 수행되거나 UI에 표시된 확률과 실제 결과가 어긋나는 상황을 구조적으로 차단할 수 있다.

초기 구현에서 하나의 함수에 섞여 있던 확률 판정, 속성 선택, 누적 처리 로직을 분리함으로써, 이 함수는 최종 확률을 받아 결과를 확정하는 단일 책임만을 갖게 되었다.

이후 확률 시스템이나 미니게임 연출이 변경되더라도 결과 적용 로직에는 영향을 주지 않는 구조를 확보하게 되었다.

4.4. 랜덤 속성 부여 및 결과 적용
private void ApplyRandomPropertyToCurrentItem()
{
    resultTxt.text = "강화 성공! " + upgradelist[randomIndex];

    string statToUpdate = "";
    float statValueToAdd = 0f;

    switch (randomIndex)
    {
        case 0: statToUpdate = "체력(%)";         statValueToAdd = 5;    break;
        case 1: statToUpdate = "공격속도";        statValueToAdd = 0.1f; break;
        case 2: statToUpdate = "사거리";          statValueToAdd = 5;    break;
        case 3: statToUpdate = "이동속도";        statValueToAdd = 5;    break;
        case 4: statToUpdate = "경험치 획득량";   statValueToAdd = 8;    break;
    }

    bool statFound = false;

    for (int i = 0; i < currentItem.additionalStat.Length; i++)
    {
        if (string.IsNullOrEmpty(currentItem.additionalStat[i]))
            continue;

        string[] splitStat = currentItem.additionalStat[i].Split(',');
        if (splitStat[0] == statToUpdate)
        {
            float currentStatValue = float.Parse(splitStat[1]);
            float newValue = currentStatValue + statValueToAdd;

            currentItem.additionalStat[i] = statToUpdate + "," + newValue.ToString();
            statFound = true;

            currentItem.isPropertyMax = true;
            break;
        }
    }

    if (!statFound)
    {
        List<string> additionalStatsList = new List<string>(currentItem.additionalStat);
        additionalStatsList.Add(statToUpdate + "," + statValueToAdd.ToString());
        currentItem.additionalStat = additionalStatsList.ToArray();
    }

    UpdateCrystalUI();
    UpdateUpgradeCostUI();
}

ApplyRandomPropertyToCurrentItem 함수는 확률 미니게임의 최종 판정이 성공으로 확정된 이후, 실제로 아이템 데이터에 속성을 반영하는 역할을 담당하는 함수다.

이 함수가 호출되는 시점에는 이미 아이템 선택 여부, 재화 차감, 확률 판정까지 모두 완료된 상태이며, 더 이상 실패 가능성이나 조건 검사는 존재하지 않는다.

함수의 책임은 오직 이번 시도에서 선택된 속성을 현재 아이템에 어떻게 적용할 것인가에만 집중되어 있다.

함수가 시작되면 먼저 결과 텍스트를 갱신하여, 이번 강화가 성공했으며 어떤 속성이 선택되었는지를 플레이어에게 명확히 전달한다.

이때 표시되는 속성 이름은 미니게임 진입 시점에 이미 결정된 randomIndex를 기준으로 upgradelist에서 가져오며, 확률 미니게임 단계에서는 이 인덱스가 변경되지 않는다.

즉, 이 함수는 속성을 다시 랜덤으로 뽑지 않고, 이미 확정된 결과를 데이터에 반영하는 단계다.

이후 statToUpdate와 statValueToAdd라는 두 변수를 초기화한 뒤, randomIndex에 따라 실제로 적용할 속성의 이름과 증가 수치를 결정한다.

이 switch 문은 이번 강화에서 어떤 능력치를, 얼마만큼 증가시킬 것인가를 명시적으로 매핑하는 역할을 하며, 속성 종류가 늘어날 경우 이 구간만 확장하면 되도록 설계되어 있다.

이 시점에서 statToUpdate는 문자열 형태의 속성 키가 되고, statValueToAdd는 해당 속성에 더해질 실제 수치 값이 된다.

속성 정보가 결정되면, 현재 아이템이 이미 동일한 속성을 가지고 있는지 여부를 확인하기 위해 additionalStat 배열을 처음부터 순회한다.

additionalStat은 "사거리,5" 와 같은 "속성이름,수치" 형식의 문자열 배열이기 때문에, 먼저 null이나 빈 문자열이 아닌지 검사하여 불필요한 파싱을 건너뛴다.

이후 Split(',')을 사용해 문자열을 속성 키와 수치 값으로 분리하고, 앞쪽 키가 이번에 적용하려는 statToUpdate와 동일한지 비교한다.

이 비교를 통해 이미 같은 속성이 존재하는지를 판단하며, 이는 중복 속성 처리 규칙을 구현하기 위한 핵심 단계다.

만약 동일한 속성이 발견되면, 해당 속성의 기존 수치를 float로 파싱한 뒤 statValueToAdd만큼 증가시킨 새로운 값을 계산한다.

그리고 이 값을 다시 "속성이름,새수치" 형태의 문자열로 조합해 additionalStat 배열의 해당 인덱스에 덮어쓴다.

이 경우 statFound를 true로 설정하고 반복문을 종료하는데, 이는 동일 속성이 하나만 존재한다는 전제를 코드로 명확히 표현한 것이다.

동시에 isPropertyMax 플래그를 true로 설정하여, 동일 속성 중복 누적이 발생한 경우에는 더 이상 새로운 속성 슬롯이 열리지 않도록 논리적 최대 상태를 기록한다.

반대로 배열을 끝까지 순회했음에도 동일한 속성이 발견되지 않은 경우에는, 이번 강화가 완전히 새로운 속성을 추가하는 케이스로 처리된다.

이때 string[] 배열의 고정 길이 한계를 직접 다루지 않기 위해, 기존 배열을 List<string>으로 변환한 뒤 새로운 "속성이름,수치" 항목을 Add()로 추가한다.

List를 사용하는 이유는 런타임 중 안전하게 원소를 추가할 수 있도록 하기 위함이며, 이후 ToArray()를 통해 다시 string[] 형태로 되돌림으로써 시스템 전반에서 사용하는 데이터 포맷과의 정합성을 유지한다.

이 방식은 데이터 저장 구조와 연산용 자료구조를 분리한 설계로, 코드 가독성과 안정성을 동시에 확보하기 위한 선택이다.

속성 데이터가 실제로 변경된 이후에는, UpdateCrystalUI 함수와 UpdateUpgradeCostUI 함수를 호출하여 강화로 인해 변화한 상태가 즉시 UI에 반영되도록 한다.

이를 통해 플레이어는 속성 부여 이후 남은 재료 수량, 다음 강화 비용, 최대 상태 여부 등을 지연 없이 확인할 수 있으며, 데이터와 UI 간의 불일치가 발생하지 않는다.

이 함수는 확률 계산이나 조건 판단을 전혀 포함하지 않으며, 오직 확정된 강화 결과를 아이템 데이터에 반영하고, 그 결과를 UI에 동기화한다는 단일 책임만을 수행한다.

속성 부여 성공 시 랜덤으로 선택된 속성이 현재 아이템에 적용된다.

이미 동일한 속성이 존재할 경우에는 기존 수치에 누적되도록 처리하였고, 새로운 속성일 경우에는 속성 리스트에 추가하도록 구성했다.

이를 통해 속성 중복 시의 처리 규칙을 명확히 하고, 예측 가능한 성장 구조를 유지할 수 있었다.

이 구조는 이후 속성 종류가 늘어나더라도 기존 저장 방식과 로직을 유지한 채 확장이 가능하도록 고려한 설계다.

지금 반지(아케인 스톤)의 속성 데이터는 Additional Stat 이 문자열 배열 형태로 저장되어 있고, 실제 값은 "사거리,5" 처럼 “키,수치” 포맷으로 들어가 있다.

이 구조를 문자열 기반으로 유지한 가장 큰 이유는 Unity 인스펙터와 직렬화 관점에서 관리 비용이 낮기 때문이다.

문자열 배열은 인스펙터에서 즉시 확인 ·수정이 가능하고, 프리팹 / 스크립터블오브젝트 / 세이브 데이터에서도 구조가 단순해 저장과 로딩이 편하다.

특히 속성 시스템은 밸런싱 대상이기 때문에, 개발 중에 속성 이름을 바꾸거나 수치를 조정하는 작업이 자주 발생한다.

이때 별도의 커스텀 타입(StatData 구조체, enum 기반 딕셔너리)을 만들면 타입 안정성은 좋아지지만, 데이터 정의 / 직렬화 / 에디터 표시 / 마이그레이션 비용이 같이 따라온다.

반면 "이름,수치" 문자열 방식은 규칙만 정해두면 데이터 형태가 바뀌어도 에디터에서 바로 조정 가능하고, 디버깅 시에도 인스펙터에서 한눈에 읽힌다는 장점이 있다.

그래서 이 포트폴리오 단계에서는 확장·유지보수의 실용성(툴링/밸런싱)을 우선하여 문자열 기반을 채택했다.

문자열 기반으로 저장하면 실제 로직에서 값 연산이 필요할 때 파싱이 필요하다.

그래서 "사거리,5" 같은 문자열은 Split(',')로 분해해서 앞쪽은 스탯 키(사거리), 뒤쪽은 수치(5)로 분리하고, 기존 Additional Stat 배열을 순회하면서 동일한 키가 존재하는지 찾는다.

문자열을 Split(',')로 분해하면 데이터 구조와 무관하게 속성 키 기준 비교가 가능해진다.

이 방식은 향후 데이터 구조가 변경되더라도 ‘동일 속성 탐색 → 누적’이라는 로직 책임을 그대로 유지할 수 있다.

동일 키가 이미 있으면 수치만 누적하는데, 이렇게 하면 같은 속성 재획득 시 누적이라는 룰을 데이터 구조와 무관하게 일관되게 적용할 수 있다.

반대로 동일 키가 없으면 새 엔트리를 추가해야 하는데, 여기서 string[] 배열의 한계가 나온다.

배열은 길이가 고정이기 때문에 런타임에 원소를 하나 더 넣으려면 새 배열을 만들고 기존 데이터를 복사한 뒤 마지막에 추가해야 한다.

이 과정을 매번 직접 구현하면 코드가 길어지고, 인덱스 실수나 누락 같은 버그 가능성이 커진다.

그래서 새 속성 추가 케이스에서는 List<string>을 사용했다.

List<T>는 C#의 동적 배열 컨테이너로, 내부적으로 크기를 자동 관리하면서 원소를 늘릴 수 있다.

즉, 배열처럼 고정 길이가 아니라서 Add() 한 줄로 안전하게 항목을 추가할 수 있다.

Add()는 리스트의 끝에 새로운 원소를 붙이는 함수이며, 필요한 경우 내부 배열의 용량(capacity)을 확장하고 기존 데이터를 복사하는 일을 List<T>가 알아서 처리한다.

개발자가 직접 '새 배열 생성 + 복사' 로직을 반복해서 작성하지 않아도 되기 때문에 코드가 간결해지고 유지보수성이 올라간다.

추가 속성 개수 제한(MaxPropertyCount)이 바뀌거나, 향후 속성 종류가 늘어나더라도 추가 로직 자체는 흔들리지 않는다는 점이 이 구조의 실용적인 강점이다.

다만 시스템 전체는 currentItem.additionalStat을 배열(string[])로 들고 있게 설계되어 있다.

인스펙터에서 Additional Stat 필드가 배열로 보이는 것도 이 때문이고, 다른 함수들(UI 갱신, 최대 상태 판단 등)도 배열을 전제로 작성되어 있다.

그래서 새 속성을 추가할 때만 임시로 List<string>로 변환해 확장 작업을 처리한 뒤, 최종 저장 형태는 다시 배열로 되돌려야 한다.

이때 ToArray()를 사용한다.

ToArray()는 리스트에 들어 있는 원소들을 그대로 복사해서 새로운 배열을 만들어주는 함수다.

즉, 동적 확장은 List로 편하게 처리하고, 시스템의 표준 저장 포맷은 string[]로 유지한다는 역할 분담을 만들어준다.

결과적으로 추가 처리에서는 List<string>.Add()로 안전성과 간결성을 확보하고, 저장 및 시스템 연동 단계에서는 ToArray()로 기존 데이터 구조와의 정합성을 유지하는 구조가 된다.

정리하면, 문자열 기반은 Unity 인스펙터 / 밸런싱 / 디버깅 / 직렬화의 실용성을 우선한 선택이고, List<string> + Add() + ToArray() 흐름은 배열 고정 길이 한계를 안전하게 우회하면서도 기존 시스템 저장 형태는 유지하기 위한 구현 전략이다.

이 방식은 구현 속도와 유지보수성을 동시에 잡을 수 있고, 추후에 필요해지면 문자열 대신 구조체 / enum 기반 데이터로 교체할 때도 로직 책임(탐색/누적/추가)을 그대로 유지한 채 데이터 표현만 바꾸는 식으로 점진적으로 개선할 수 있다.

물론 문자열 기반 구조는 타입 안정성이나 컴파일 타임 검증 측면에서는 한계가 있다.

다만 이 프로젝트에서는 구현 속도, 인스펙터 기반 밸런싱, 디버깅 가시성을 우선하여 해당 방식을 선택했고, 이후 필요해질 경우 구조체나 enum 기반 데이터 구조로 점진적으로 전환할 수 있도록 로직 책임을 분리해두었다.

배열을 직접 확장하지 않고 List를 경유하는 방식은, 런타임 안전성과 코드 가독성을 동시에 확보하기 위한 선택이다. 이는 데이터 저장 포맷과 연산용 자료구조를 분리한 설계다.

5. 개발 의도

확률 미니게임은 단순한 연출 요소가 아니라, 속성 부여 결과를 결정하는 핵심 로직의 일부다.

그래서 확률 변화, 결과 판정, 속성 적용을 서로 다른 단계로 분리하여 각 단계가 명확한 책임을 갖도록 설계하였다.

이 구조를 통해 수동 입력, 자동 실행, 추후 확률 조정이나 연출 변경이 이루어지더라도 결과 처리 로직은 영향을 받지 않는다.

확률 미니게임은 속성 부여 시스템 전체 흐름에서 마지막 결정 지점 역할을 하며, 앞선 게시글에서 정리한 입력 처리, 상태 동기화, 시도 검증 구조 위에서 안정적으로 동작하도록 구성하였다.