본문 바로가기

Unity3D/C#

유니티 랜덤 Random.Range 정수 · 실수 차이와 자주 하는 실수 (int, float, InitState)

반응형

RANDOM Range

 

이 블로그의 예전 글 「유니티 범위 안에 있는 랜덤 값 뽑기 (INT, FLOAT)」 에서는 정수 쪽 Random.Range 에만 최댓값에 1 을 더해 두고, 정수와 실수가 왜 다르게 나오는지는 한 줄로 넘어갔습니다. 그 뒤로 제가 잰드케이로 낸 앱을 만들면서 랜덤 때문에 코드를 고친 일이 두 번 있었습니다. 기억력 게임 뇌깜빡할사이의 선긋기에서는 후보 목록이 빈 채로 Random.Range(0, 0) 이 돌려준 0 번을 읽다가 오류가 났고, 랜덤 결정 앱 RandomGO 에서는 System.Random 과 UnityEngine.Random 이 섞여 있어서 시드를 정해도 결과가 따라오지 않는 게임이 있었습니다. 유니티 랜덤은 정수(int)와 실수(float)를 같은 이름 Random.Range 로 부르지만, 정수는 최댓값이 나오지 않고 실수는 최댓값까지 나옵니다. 이번 예시에서는 주사위, 배열에서 하나 뽑기, 목록 섞기, 확률, 시드로 같은 스테이지 만들기를 스크립트 하나에 담고, 그 아래에 자주 하는 실수를 붙여 보겠습니다.

 

C# (UNITY 3D)

 

 

정수 버전 Random.Range(int, int) 는 최솟값은 포함하고 최댓값은 포함하지 않습니다. 공식 문서의 예시대로 Random.Range(0, 10) 은 0~9 중 하나입니다. 실수 버전 Random.Range(float, float) 는 양 끝을 모두 포함해서 Random.Range(0f, 10f) 는 10.0 도 나올 수 있습니다. 어느 버전이 불릴지는 넣은 값의 타입으로 정해지기 때문에, 정수 두 개를 넣으면 정수 버전, 하나라도 f 가 붙은 실수면 실수 버전이 됩니다.

 

입력 칸 두 개로 범위를 받아 랜덤 정수와 랜덤 실수를 화면에 함께 보여 주는 UI 예시는 예전 글 「유니티 범위 안에 있는 랜덤 값 뽑기」 에 있습니다. 이번 글은 UI 없이 Console 창으로 결과를 봅니다.

 

using System.Collections.Generic;
using UnityEngine;

//CodeFinder 코드파인더
//From https://codefinder.janndk.com/
public class RandomRangeExample : MonoBehaviour
{
    public string[] items = { "사과", "바나나", "포도", "딸기" };
    public int stage_seed = 1234;

    void Start()
    {
        Log_Range();

        Debug.Log("뽑은 항목 : " + Pick_Item(items));

        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        Shuffle_List(numbers);
        Debug.Log("섞은 목록 : " + string.Join(", ", numbers));

        Debug.Log("30% 당첨 : " + Check_Chance(30));

        int[] stage = Make_Stage(stage_seed, 8);
        Debug.Log("스테이지 : " + string.Join(", ", stage));
    }

    void Log_Range()
    {
        int dice = Random.Range(1, 7);          // 1~6 (정수는 7 이 나오지 않습니다)
        int zero = Random.Range(0, 1);          // 정수 버전이라 늘 0
        float ratio = Random.Range(0f, 1f);     // 0.0~1.0 (실수는 1.0 도 나옵니다)
        int reversed = Random.Range(10, 0);     // 1~10 (0~9 가 아닙니다)

        Debug.LogFormat("dice : {0}, zero : {1}, ratio : {2}, reversed : {3}", dice, zero, ratio, reversed);
    }

    public string Pick_Item(string[] list)
    {
        // 빈 배열이면 Random.Range(0, 0) 이 0 을 돌려주고 list[0] 에서 예외가 납니다
        if (list == null || list.Length == 0)
        {
            return null;
        }
        return list[Random.Range(0, list.Length)];   // Length 는 나오지 않으므로 -1 을 하지 않습니다
    }

    public void Shuffle_List(List<int> list)
    {
        for (int i = list.Count - 1; i >= 1; i--)
        {
            int k = Random.Range(0, i + 1);   // 0~i 중 하나 (i 자신도 나오게 +1)
            int temp = list[i];
            list[i] = list[k];
            list[k] = temp;
        }
    }

    public bool Check_Chance(int percent)
    {
        return Random.Range(0, 100) < percent;   // 0~99 중 percent 개가 당첨
    }

    public int[] Make_Stage(int seed, int count)
    {
        Random.State state_before = Random.state;   // 지금까지의 랜덤 흐름을 저장
        Random.InitState(seed);                     // 같은 seed 면 매번 같은 스테이지

        int[] stage = new int[count];
        for (int i = 0; i < count; i++)
        {
            stage[i] = Random.Range(0, 4);
        }

        Random.state = state_before;                // 나머지 랜덤은 원래 흐름으로
        return stage;
    }
}

 

빈 오브젝트에 붙이고 Play 를 누르면 Console 창에 다섯 줄이 찍힙니다. 랜덤이라 숫자는 누를 때마다 달라지고, 스테이지 줄만 stage_seed 가 같으면 늘 같게 나옵니다.

 

함수(function)
Log_Range : 정수 · 실수 버전과 최솟값이 더 큰 경우를 한 번씩 뽑아 Console 에 찍습니다.
Pick_Item : 배열에서 하나를 뽑습니다. 비어 있으면 null 을 돌려줍니다.
Shuffle_List : 뒤에서부터 앞쪽의 아무 자리와 바꿔 목록을 섞습니다(피셔-예이츠 섞기).
Check_Chance : percent(0~100) 확률로 true 를 돌려줍니다.
Make_Stage : seed 로 정해진 스테이지 배열을 만들고, 랜덤 상태는 원래대로 돌려놓습니다.

 

변수(variable)
items(string[]) : 뽑기에 쓸 항목입니다. 인스펙터에서 바꿀 수 있습니다.
stage_seed(int) : 스테이지를 만들 때 쓸 시드입니다.
state_before(Random.State) : InitState 를 부르기 전의 랜덤 상태입니다.

 

자주 하는 실수

 

1. 주사위를 Random.Range(1, 6) 으로 굴리기
정수 버전은 최댓값이 나오지 않아서 6 이 영영 안 나옵니다. 1~6 이 필요하면 Random.Range(1, 7) 처럼 원하는 최댓값에 1 을 더해 넣습니다.

 

2. 0~1 사이 값을 Random.Range(0, 1) 로 받기
정수 두 개를 넣었으니 정수 버전이 불리고, 0 이상 1 미만의 정수는 0 하나뿐이라 늘 0 이 나옵니다. 0~1 사이 실수가 필요하면 Random.Range(0f, 1f) 나 Random.value 를 씁니다. 반대로 Random.Range(0, 1f) 처럼 한쪽만 f 를 붙여도 실수 버전이 불립니다.

 

3. 배열 인덱스에서 1 을 빼기
배열에서 하나를 뽑을 때는 Random.Range(0, list.Length) 가 맞습니다. 마지막 인덱스가 Length - 1 이라고 Random.Range(0, list.Length - 1) 로 쓰면 마지막 항목이 절대 뽑히지 않습니다. 배열이 비어 있을 때도 조심해야 합니다. 최솟값과 최댓값이 같으면 문서대로 최솟값을 돌려주기 때문에 Random.Range(0, 0) 은 0 이 되고, 빈 목록의 0 번을 읽다가 예외가 납니다. 뇌깜빡할사이 선긋기가 이 경우였습니다. 모서리 점에 닿았을 때 갈 수 있는 점을 이미 모두 지나 후보 목록이 비면 이 예외가 났고, 선이 긴 8~10단계에서만 가끔 일어났습니다. 1.0.7 에서 후보가 비면 선을 처음부터 다시 뽑게 고쳤습니다.

 

4. 실수 랜덤에 길이를 곱해 인덱스로 쓰기
(int)(Random.Range(0f, 1f) * list.Length) 같은 식은 거의 맞게 돌다가, 1.0 이 나오는 순간 인덱스가 list.Length 가 되어 범위를 벗어납니다. 문서에 따르면 실수 버전에서 어떤 값 하나가 나올 확률은 평균 1천만 번에 한 번쯤이라, 테스트에서는 거의 안 보이고 나중에 드물게 터집니다. 인덱스는 처음부터 정수 버전으로 뽑습니다.

 

5. 최솟값과 최댓값 자리를 바꿔 넣기
Random.Range(10, 0) 은 Random.Range(0, 10) 과 같지 않습니다. 두 값의 자리는 바뀌지만 포함 · 미포함은 원래 자리를 따라가서, 10 은 포함되고 0 은 빠진 1~10 이 나옵니다. 실수 버전은 양 끝이 다 포함이라 자리만 바뀝니다.

 

6. System.Random 과 섞어 쓰기
스크립트 위에 using System; 과 using UnityEngine; 이 함께 있으면 Random 이 어느 쪽인지 몰라 CS0104 컴파일 오류가 납니다. 이때는 아래처럼 별칭 한 줄을 더하거나 UnityEngine.Random.Range 처럼 전체 이름을 씁니다.

 

using System;
using UnityEngine;
using Random = UnityEngine.Random;   // 이 파일의 Random 은 UnityEngine.Random

 

이름 말고 동작도 다릅니다. Random.InitState 는 UnityEngine.Random 의 상태만 정하기 때문에, new System.Random() 으로 뽑는 코드는 시드를 줘도 따라오지 않습니다. RandomGO 의 스토어 화면을 찍는 도구가 InitState 로 룰렛 결과를 고르는데, System.Random 으로 섞던 럭키볼은 결과를 정할 수 없어서 원하는 결과가 나올 때까지 다시 누르게 해 두었습니다. 개발 중인 다음 판에서는 섞기를 모두 UnityEngine.Random 으로 모았습니다. 반대로 스레드나 Job 안에서는 UnityEngine.Random 을 쓰면 오류가 나므로, 문서대로 그쪽은 System.Random 인스턴스를 따로 만들어 씁니다.

 

7. 시드를 고정해 두고 잊기
UnityEngine.Random 은 앱이 시작될 때 운영체제에서 받은 값으로 한 번 시드가 정해지므로, 평소에는 InitState 를 부를 필요가 없습니다. 테스트하려고 Start 에 Random.InitState(1234) 를 넣어 두고 지우지 않으면 게임 전체가 매번 같은 순서로 나옵니다. 스테이지처럼 일부만 같게 만들고 싶을 때는 예시의 Make_Stage 처럼 Random.state 를 저장해 두었다가 끝나고 되돌립니다. 시드 값은 나중에 다시 꺼낼 수 없으니, 다시 만들 일이 있으면 시드를 따로 저장합니다.

 

위 예시 코드는 Unity 6000.3.14f1 빈 씬에 붙여 컴파일 · 실행해 확인했습니다. 오류 · 경고 없이 Console 에 이렇게 찍힙니다(값은 실행할 때마다 다릅니다).

 

dice : 1, zero : 0, ratio : 0.7428772, reversed : 7
뽑은 항목 : 포도
섞은 목록 : 4, 5, 2, 3, 1
30% 당첨 : True
스테이지 : 2, 0, 1, 3, 2, 0, 2, 3

 

프로젝트에 직접 만든 Debug 클래스가 있으면 「'Debug' does not contain a definition for 'LogFormat'」 오류가 날 수 있습니다. 그럴 때는 UnityEngine.Debug.LogFormat 처럼 이름을 다 써 주면 됩니다.

 

Random.Range 의 범위와 동작은 Unity 공식 Scripting API 문서(Random.Range) 와 Random · InitState 문서를 기준으로 적었습니다.

 

3번의 빈 목록 오류는 제가 잰드케이로 낸 기억력 게임 뇌깜빡할사이 1.0.7 에서 실제로 고친 오류입니다(스토어 개발사 표시는 Jakadela Games).

 

 

반응형

Google Play 및 Google Play 로고는 Google LLC의 상표입니다.