랜덤

시드 기반의 결정론적 난수 — 이름 있는 스트림과 스냅샷/복원으로 저장에 올바른 실행을 보장합니다.

재현 가능한 무작위성: 같은 시드면 같은 결과 — 모든 플랫폼에서, 저장과 로드를 가로질러.

인터페이스IRandomService
끄기 스위치NullRandomService
어셈블리CommonGameSystem.Core
시작부팅 시 자동 등록 — 별도 설정 불필요

하는 일

랜덤 서비스는 Unity의 단일 전역 Random 상태를 독립적이고 재현 가능한 난수 소스로 대체합니다. 여기서 결정론적이란: 고정된 시드를 주면 루트 테이블, 적 조우, 절차적 생성이 재플레이할 때마다 정확히 재현되고 — 그 시퀀스가 모든 플랫폼에서 동일하다는 뜻입니다. 버그 리포트("시드 12345, 세 번째 방")에 이상적이고, 저장/로드 정확성에는 필수적입니다.

각 게임 시스템은 자신만의 이름 있는 스트림"loot"이나 "vfx" 같은 문자열로 식별되는 독립적인 난수 시퀀스 — 에서 값을 뽑을 수 있습니다. 스트림은 서로 격리되어 있어, 한 시스템에 난수 호출을 하나 추가해도 다른 시스템의 시퀀스는 절대 밀리지 않습니다. 코스메틱한 화면 흔들림 롤이 어떤 루트가 떨어질지를 바꾸는 일은 있을 수 없습니다.

전체 상태는 스냅샷으로 캡처했다가 나중에 복원할 수 있습니다: 런 중간에 저장을 다시 불러와도 다음 롤은 정확히 멈춘 그 지점에서 이어집니다 — 더 좋은 드롭을 노리고 다시 불러오는 세이브 스커밍은 통하지 않습니다.

빠른 시작

프레임워크가 시작된 뒤에 실행되도록 [DefaultExecutionOrder(100)]이 붙은 MonoBehaviour에서 서비스를 resolve해 캐시하세요:

var rng = ServiceLocator.Resolve<IRandomService>();
// or the short alias: var rng = SL.Resolve<IRandomService>();

완전한 결정론적 실행이 필요하면, 명시적 시드로 만든 서비스로 교체하세요:

ServiceLocator.Replace<IRandomService>(new RandomService(new RandomOptions(seed: 12345)));

서비스 자체가 기본(마스터) 스트림입니다 — 모든 draw 메서드를 즉시 호출할 수 있습니다.

API 레퍼런스

핵심 draw 메서드

각 호출은 스트림 상태를 전진시킵니다.

  • ulong NextULong() — 원시 64비트 값.
  • uint NextUInt() — 원시 32비트 값.
  • int NextInt(int maxExclusive)[0, maxExclusive) 범위의 편향 없는 정수.
  • int NextInt(int minInclusive, int maxExclusive) — 주어진 범위의 편향 없는 정수.
  • float NextFloat()[0, 1) 범위의 균등 분포 값. 정확히 1.0을 반환하는 일은 없습니다.
  • float NextFloat(float min, float max) — 주어진 범위의 균등 분포 값.
  • double NextDouble()[0, 1) 범위의 균등 분포 값.
  • bool NextBool() — 공정한 동전 던지기.
  • bool Chance(float probability) — 주어진 확률로 true. 확률이 정확히 0이나 1이면 값을 뽑지 않고 즉시 반환합니다.

편의 메서드

  • int NextIndex(int count)[0, count) 범위의 균등 인덱스. count가 0 이하이면 -1을 반환합니다.
  • int NextWeightedIndex(IReadOnlyList<float> weights) — 가중치에 비례해 인덱스를 선택합니다. 퇴화 입력(빈 리스트, 모두 0인 가중치)에는 -1을 반환합니다.
  • T Pick<T>(IReadOnlyList<T> items) — 리스트에서 균등하게 선택한 요소.
  • void Shuffle<T>(IList<T> items) — 리스트를 제자리에서 섞습니다. 모든 순서가 동일한 확률로 나옵니다.

이름 있는 스트림 (격리)

  • IRandomStream GetStream(string name) — 독립 스트림을 얻거나 만듭니다. 같은 이름은 세션 내내 같은 스트림 인스턴스를 반환합니다. IRandomStream은 위에 나열한 것과 동일한 draw 메서드를 제공합니다.
  • ulong Seed { get; } — 현재 마스터 시드.
  • void Reseed(ulong seed) — 새 시드로 리셋합니다. 모든 이름 있는 스트림이 폐기됩니다.

저장과 복원

  • RandomSnapshot Capture() — 전체 상태를 직렬화합니다: 마스터 시드, 기본 스트림, 그리고 모든 이름 있는 스트림.
  • void Restore(RandomSnapshot snapshot) — 스냅샷에서 재개합니다. 예를 들어 런 중간에 저장을 불러올 때.

RandomSnapshot은 평범한 직렬화 가능 구조체이므로 저장 데이터 클래스에 그대로 넣으면 됩니다(아래 저장 연동 예제 참고). IsValid 프로퍼티는 불러온 스냅샷이 사용 가능한 페이로드를 담고 있는지 알려 줍니다.

예제

일상적인 draw

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class LootDropper : MonoBehaviour
{
    private IRandomService _rng;

    void Awake()
    {
        _rng = ServiceLocator.Resolve<IRandomService>();
    }

    public void RollDrop()
    {
        // Deterministic damage roll: the same seed gives the same sequence on all platforms.
        int dmg = _rng.NextInt(8, 13);
        Debug.Log($"Damage: {dmg}");

        // A named stream keeps cosmetic effects isolated from gameplay rolls.
        var vfx = _rng.GetStream("vfx");
        float shake = vfx.NextFloat(0f, 0.5f);
        Debug.Log($"Screen shake: {shake}");

        // Weighted loot table.
        float[] weights = { 30f, 50f, 20f }; // common, rare, epic
        int rarity = _rng.NextWeightedIndex(weights);
        Debug.Log($"Rarity: {rarity}");
    }
}

Save 서비스와 함께 쓰는 스냅샷 / 복원

서비스는 스냅샷을 어디에도 직접 쓰지 않습니다 — 저장 / 불러오기 서비스를 통해 나머지 저장 데이터와 함께 라우팅하는 것은 여러분의 몫입니다. RandomSnapshot을 저장 클래스의 필드로 넣으세요:

using CommonGameSystem.Core;
using UnityEngine;

[System.Serializable]
public class RunSaveData
{
    public int stage;
    public RandomSnapshot rng; // the full random state rides inside your save data
}

[DefaultExecutionOrder(100)]
public class RunSaveManager : MonoBehaviour
{
    private IRandomService _rng;
    private ISaveService _save;

    void Awake()
    {
        _rng = ServiceLocator.Resolve<IRandomService>();
        _save = ServiceLocator.Resolve<ISaveService>();
    }

    public void SaveRun(int currentStage)
    {
        var data = new RunSaveData
        {
            stage = currentStage,
            rng = _rng.Capture() // freeze the dice exactly where they are
        };
        _save.Save("run-autosave", data);
    }

    public void LoadRun()
    {
        var result = _save.Load<RunSaveData>("run-autosave");
        if (result.Status != SaveStatus.Ok)
            return; // no save yet, or the file is unreadable — start fresh

        if (result.Value.rng.IsValid)
            _rng.Restore(result.Value.rng); // the next roll continues the saved sequence

        // ... restore the rest of your run state from result.Value ...
    }
}

Restore 후의 바로 다음 NextInt는(마스터 스트림이든 어떤 이름 있는 스트림이든) 게임을 종료하지 않았더라면 반환했을 값을 정확히 그대로 반환합니다. 저장을 다시 불러와도 결과를 다시 굴릴 수 없습니다.

끄는 방법

ServiceLocator.Replace<IRandomService>(new NullRandomService());

이렇게 하면 랜덤 서비스가 무음 처리됩니다. 모든 draw는 난수 대신 고정된 안전한 기본값(0, 0f, false, -1, 또는 default(T))을 반환합니다. 테스트나 결정론적 샌드박스 모드에서 무작위성을 억제할 때 유용합니다. 프로그래밍 오류는 여전히 throw합니다: null 스트림 이름이나 Dispose 이후의 호출은 실제 서비스와 똑같이 예외를 일으킵니다.

흔한 함정

  • 메인 스레드 전용입니다. 모든 draw와 스트림 접근은 메인 스레드에서 이뤄져야 합니다. 워커 스레드에 난수가 필요하면 메인 스레드에서 먼저 값을 뽑거나 스냅샷을 캡처한 뒤 결과를 워커에 넘기세요.
  • Reseed()를 넘어 이름 있는 스트림을 들고 있지 마세요. Reseed()는 서비스에서 모든 이름 있는 스트림을 폐기합니다. 리시드 전에 캐시해 둔 스트림은 자체적으로는 계속 동작하지만, 서비스는 더 이상 추적하지 않으며 — 스냅샷에도 더 이상 나타나지 않습니다. 리시드 후에는 GetStream()을 다시 호출해 새 시드에서 파생된 스트림을 받으세요.
  • Chance(0f)Chance(1f)는 draw를 건너뜁니다. 이 두 경우는 즉시 반환하며 스트림 상태를 전진시키지 않습니다. 결정론을 위해 정확한 draw 횟수에 의존하는 코드라면 이 점을 감안하세요.
  • 스냅샷 저장은 여러분의 일입니다. Capture()는 평범한 값을 반환할 뿐, 서비스가 어딘가에 쓰지 않습니다. 위에서 보여 준 대로 저장 / 불러오기 서비스를 통해 나머지 저장 데이터와 함께 라우팅하고, 불러올 때 Restore를 호출하세요.
  • 잘못된 입력은 throw 대신 안전한 값을 반환합니다. 빈 리스트, 폭이 0인 범위, 모두 0인 가중치, 음수 count는 0, -1, 또는 default(T)를 반환하며, 경우에 따라 경고를 로그합니다(RandomOptions.WarnOnDegenerateInput으로 제어). 서비스는 퇴화 데이터로 절대 크래시하지 않습니다.

관련 페이지