결정론적 시드 ( with 난수 알고리즘)

박지예·2026년 7월 28일

공부2026

목록 보기
12/20

파이어앰블램 좋아하시는 분 ?
파앰에선 ( 그 외 다른 게임 다수) 난수 시스템으로 특정 행동을 하면 다음 행동을 강제화 하는데, 난수 시스템을 구현하기 위해 필요한 결정론적 시드, 시드 개념에 대해 알아보자.

컴퓨터의 난수 개발 방식

일단 컴퓨터가 난수를 만드는 방식에 대해 먼저 알아보자.
크게 의사난수(PRNG) 와 진짜 난수(TRNG)로 나뉜다.
진짜 난수는 자연계의 물리법칙 처럼 정말 예측 못하는 난수고, 오늘은 컴퓨터가 계산하는 의사난수에 대해 알아볼 것 이다.

의사난수 (Pseudo-Random Number Generator)

사실 CPU는 정해진 명령을 수행하는 기계라서 스스로 "무작위"를 만들 수 가 없다.
그래서 내부 상태를 "수식"으로 변형하면서 결과가 무작위로 보이는 숫자 열을 뽑아낸다.

시드(초기값) → 상태 → [수식 적용] → 다음 상태 → 숫자 출력 → [수식 적용] → ...

상태가 수식으로만 결정되니까, 시드가 같으면 결과가 항상 같다.

C# 에서는 system.Random(seed) 형식으로 seed 를 넣어서 항상 같은 값을 뽑아낼 수 있다. 이것이 "결정론적 시드" 이다.

작업에서

savefile 마스터 시드 + SplitMix64 믹싱으로 시드를 만든다.

// RandomManager.cs:42
var state = (ulong)_saveSeed;        // ← SaveFile 고유 시드 (hashcode 아님)
SplitMix64.Next(ref state);
state ^= (ulong)(long)tag;           // ← 태그를 XOR로 섞음
SplitMix64.Next(ref state);
foreach (var d in discriminators)    // ← 카운터 등을 섞음
{
    state ^= (ulong)d;
    SplitMix64.Next(ref state);
}
var s0 = SplitMix64.Next(ref state);
var s1 = SplitMix64.Next(ref state);
return new Xoroshiro128PlusPlus(s0, s1);

여기서 시드의 재료는 3개

  • _saveSeed - 저장 파일마다 고정된 마스터시드
  • tag - 어떤 기능(or ㄷㅂㅈ) 인지 구분하는 enum 값
  • discriminators - 도전 횟수 같은 카운터 (같은 기능 안에서 n번 시도했는지)

XOR

중간에 xor로 시드를 섞는 이유는 다음과 같다.

(xor 은 두 비트를 비교해서 다르면 1, 같으면 0을 내는 연산이다.)

  1. 되돌릴 수 있음 (자기 역원)
    같은 값을 2번 XOR하면 원래대로 돌아온다.

  2. 비트가 골고루 섞인다.
    한쪽 값이 무작위라면 xor 의 각 비트가 0일 확률과 1일 확률이 정확히 반반이다.

  3. 모든 비트 자리에 영향
    + 는 자리 올림(carry) 때문에 낮은 자리가 높은 자리에 영향을 주지만 방향이 한쪽이다. 각 비트가 독립적으로 뒤집혀서, 상위 하위 비트 어디든 균등하게 반영된다.

앞서 말한 시드 생성에서 기존에 섞여 있던 상태에서 새 상태를 주입시킬때 xor 를 사용하면

  1. 재료를 합칠 때 정보를 안 잃는다.
    xor 로 다시 되돌릴 수 있기 때문

2.균등하게 섞인다.
xor 은 각 비트가 반반 확률로 뒤집혀서 치우침이 없다.
시드는 골고루 퍼져야 좋은 난수값이 나온다.

  1. tag 가 다르거나 discriminators 의 카운트가 1 늘어나면 xor로 주입되는 비트가 달라지고 바로 뒤에 SplitMix64 가 그 차이를 온전히 비트로 퍼트린다.

SplitMix64

Sebastiano Vigna가 공개한(원 설계는 Steele, Lea, Flood의 Java 8 SplittableRandom 논문) 64비트 PRNG

public struct SplitMix64
{
    ulong _state;

    public SplitMix64(ulong seed) => _state = seed;

    public ulong Next()
    {
        unchecked
        {
            ulong z = (_state += 0x9E3779B97F4A7C15UL);
            z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9UL;
            z = (z ^ (z >> 27)) * 0x94D049BB133111EBUL;
            return z ^ (z >> 31);
        }
    }
}

xorshrio (엑소로시로)

이름 자체도 나타났듯이
xor, rotate, shift, rotate 이 4개로 상태 갱신을 한다


rotate

밀려나서 버려질 비트를 다시 반대편 끝으로 되돌려 넣는 시프트

private static ulong RotateLeft(ulong x, int k) => (x << k) | (x >> (64 - k));

이해하기 쉽게 8비트를 예시로 들고
x = 1011_0010, k = 3 라고 가정해보자

1단계)
x 를 왼쪽으로 3(k) 번 밀기

 ←  밀어냄
[101] 1 0 0 1 0 . . .      ← 왼쪽으로 삐져나간 101은 버려짐
    = 1 0 0 1 0 0 0 0      ← 빈 오른쪽은 0으로 채워짐

2단계)
x >> (8-3) = x >> 5 : 그 사라진 놈을 따로 건져내기

    1 0 1 1 0 0 1 0
                    →  오른쪽으로 5칸
    0 0 0 0 0 1 0 1

3단계)
or 로 합치기

  1 0 0 1 0 0 0 0     (<< 3 결과)
| 0 0 0 0 0 1 0 1     (>> 5 결과)
─────────────────
  1 0 0 1 0 1 0 1

여담)
C#에서는

using System.Numerics;
ulong r = BitOperations.RotateLeft(x, 24);

이걸로 rotate 하는게 이미 있단다 wow

이렇게 rotate 를 해주는 이유는 단순한 시프트 연산은 정보를 한 방향으로 흘리고 밀려난 비트는 버려진다. 이런 생성기는 "zeroland 탈출" 이 느리다.
상태에 우연히 0이 많이 몰린 구간에 들어가면 0이 거의 흩어지지 않은 출력이 연달아 나오게 된다.

rotate 는 버리는 비트 없이 64비트 전부를 다른 자리로 옮기기에 위와 같은 문제가 발생하지 않는다.


그럼 다시 돌아가서

var s0 = _s0;
var s1 = _s1;
var result = RotateLeft(s0 + s1, 17) + s0;   // ① 출력 계산

s1 ^= s0;                                     // ② 상태 갱신
_s0 = RotateLeft(s0, 49) ^ s1 ^ (s1 << 21);
_s1 = RotateLeft(s1, 28);

return result;
  1. 출력 만들기 - 현재 상태 s0, s1 로 이번에 반환할 숫자를 계산한다.
  2. 상태 갱신 - 다음 호출을 위해 _s0, _s1 을 다음 상태로 굴린다.
  • XOR — s1 ^= s0, ... ^ s1 ^ ...
  • Rotate — RotateLeft(...) (비트를 회전)
  • Shift — s1 << 21
  • Rotate — 또 회전
    즉 XOR + 회전 + 시프트만으로 상태를 휘젓는다. 곱셈·나눗셈이 없어서 아주 빠르다.

++ xoroshrio 는 상태가 전부 0이면 0만 뱉는 치명적인 약점이 존재한다. 방금 말한 동작들로는 0에서 벗어날 수 가 없기 때문.
위 코드에는 이를 방어하는 방어코드가 존재한다.

if (s0 == 0 && s1 == 0) s0 = 1;

정리

지금 보고 있는 결정론적 난수 생성기에서는 앞서 말한 splitmix64(시드 준비) 와 xoroshrio(난수 스트림 생성) 2가지의 알고리즘을 적절하게 사용하고 있다.

같은 savefile 시드 + 같은 tag + 같은 카운터면 언제 어디서 돌려도 똑같은 뽑기 결과가 나오게 된다!

profile
게임 클라이언트 개발자

0개의 댓글