Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

랜덤 함수

많은 게임은 핵심 게임 메커니즘을 구현하기 위해 무작위성에 의존합니다. 이 페이지는 일반적인 유형의 무작위성과 이를 Godot에서 구현하는 방법을 안내합니다.

난수를 생성하는 유용한 함수에 대한 간략한 개요를 제공한 후 배열, 사전에서 임의의 요소를 얻는 방법과 GDScript에서 노이즈 생성기를 사용하는 방법을 배우게 됩니다. 마지막으로 암호화된 보안 난수 생성에 대해 살펴보고 이것이 일반적인 난수 생성과 어떻게 다른지 살펴보겠습니다.

참고

컴퓨터는 "진짜" 난수를 생성할 수 없습니다. 대신 `의사 난수 생성기 <https://en.wikipedia.org/wiki/Pseudorandom_number_generator>`__(PRNG)에 의존합니다.

Godot는 내부적으로 의사 난수 생성기의 `PCG Family <https://www.pcg-random.org/>`__를 사용합니다.

전역 범위와 RandomNumberGenerator 클래스 비교

Godot는 난수를 생성하는 두 가지 방법을 공개합니다: 전역 범위 방법을 통하거나 RandomNumberGenerator 클래스를 사용하는 것입니다.

전역 범위 방법은 설정하기가 더 쉽지만 많은 제어 기능을 제공하지 않습니다.

RandomNumberGenerator를 사용하려면 더 많은 코드가 필요하지만 각각 고유한 시드와 상태를 가진 여러 인스턴스를 생성할 수 있습니다.

이 자습서에서는 메서드가 RandomNumberGenerator 클래스에만 존재하는 경우를 제외하고 전역 범위 메서드를 사용합니다.

무작위화() 메서드

참고

Godot 4.0부터, 무작위 시드는 프로젝트가 시작될 때 자동으로 무작위 값으로 설정됩니다. 즉, 프로젝트 실행 전반에 걸쳐 결과가 무작위인지 확인하기 위해 더 이상 ``_ready()``에서 ``randomize()``를 호출할 필요가 없습니다. 그러나 특정 시드 번호를 사용하거나 다른 방법을 사용하여 생성하려는 경우에는 여전히 ``randomize()``를 사용할 수 있습니다.

전역 범위에서는 randomize() 메서드를 찾을 수 있습니다. 이 메서드는 프로젝트가 무작위 시드 초기화를 시작할 때 한 번만 호출해야 합니다. 여러 번 호출하는 것은 불필요하며 성능에 부정적인 영향을 미칠 수 있습니다.

기본 씬 스크립트의 _ready() 방법에 넣는 것이 좋은 선택입니다.

func _ready():
    randomize()

:ref:`seed() <class_@GlobalScope_method_seed>`을 사용하는 대신 고정된 무작위 시드를 설정할 수도 있습니다. 이렇게 하면 실행 전반에 걸쳐 결정적 결과를 얻을 수 있습니다.

func _ready():
    seed(12345)
    # To use a string as a seed, you can hash it to a number.
    seed("Hello world".hash())

RandomNumberGenerator 클래스를 사용하는 경우 자체 시드가 있으므로 인스턴스에서 ``randomize()``를 호출해야 합니다.

var random = RandomNumberGenerator.new()
random.randomize()

메인 씬 설정하기

Godot에서 난수를 생성하기 위해 가장 일반적으로 사용되는 함수와 방법을 살펴보겠습니다.

randi() 함수는 0``와 ``2^32 - 1 사이의 난수를 반환합니다. 최대값이 크기 때문에 모듈로 연산자(%)를 사용하여 0과 분모 사이의 결과를 제한하는 것이 좋습니다.

# Prints a random integer between 0 and 49.
print(randi() % 50)

# Prints a random integer between 10 and 60.
print(randi() % 51 + 10)

randf() <class_@GlobalScope_method_randf>`는 0과 1 사이의 임의의 부동 소수점 숫자를 반환합니다. 이는 특히 :ref:`doc_random_number_generation_weighted_random_probability 시스템을 구현하는 데 유용합니다.

:ref:`randfn() <class_@GlobalScope_method_randfn>`은 `정규 분포 <https://en.wikipedia.org/wiki/Normal_distribution>`__에 따라 임의의 부동 소수점 숫자를 반환합니다. 이는 반환된 값이 편차(기본적으로 1.0)에 따라 달라지며 평균(기본적으로 0.0) 근처에 있을 가능성이 더 높다는 것을 의미합니다.

# Prints a random floating-point number from a normal distribution with a mean 0.0 and deviation 1.0.
print(randfn(0.0, 1.0))

randf_range() <class_@GlobalScope_method_randf_range>`는 두 개의 인수 ``from`to``를 사용하고 ``fromto 사이의 임의의 부동 소수점 숫자를 반환합니다.

# Prints a random floating-point number between -4 and 6.5.
print(randf_range(-4, 6.5))

randi_range() <class_@GlobalScope_method_randi_range>`는 두 개의 인수 ``from`to``를 사용하고 ``fromto 사이의 임의의 정수를 반환합니다.

# Prints a random integer between -10 and 10.
print(randi_range(-10, 10))

무작위 배열 요소 가져오기

무작위 정수 생성을 사용하여 배열에서 임의의 요소를 가져오거나 Array.pick_random 메서드를 사용하여 이를 수행할 수 있습니다.

var _fruits = ["apple", "orange", "pear", "banana"]

func _ready():
    for i in range(100):
        # Pick 100 fruits randomly.
        print(get_fruit())

    for i in range(100):
        # Pick 100 fruits randomly, this time using the `Array.pick_random()`
        # helper method. This has the same behavior as `get_fruit()`.
        print(_fruits.pick_random())

func get_fruit():
    var random_fruit = _fruits[randi() % _fruits.size()]
    # Returns "apple", "orange", "pear", or "banana" every time the code runs.
    # We may get the same fruit multiple times in a row.
    return random_fruit

동일한 과일을 연속해서 두 번 이상 따는 것을 방지하기 위해 위 메서드에 더 많은 논리를 추가할 수 있습니다. 이 경우에는 반복을 방지할 방법이 없기 때문에 :ref:`Array.pick_random<class_Array_method_pick_random>`를 사용할 수 없습니다.

var _fruits = ["apple", "orange", "pear", "banana"]
var _last_fruit = ""


func _ready():
    # Pick 100 fruits randomly.
    for i in range(100):
        print(get_fruit())


func get_fruit():
    var random_fruit = _fruits[randi() % _fruits.size()]
    while random_fruit == _last_fruit:
        # The last fruit was picked. Try again until we get a different fruit.
        random_fruit = _fruits[randi() % _fruits.size()]

    # Note: if the random element to pick is passed by reference,
    # such as an array or dictionary,
    # use `_last_fruit = random_fruit.duplicate()` instead.
    _last_fruit = random_fruit

    # Returns "apple", "orange", "pear", or "banana" every time the code runs.
    # The function will never return the same fruit more than once in a row.
    return random_fruit

이 접근 방식은 난수 생성이 덜 반복적으로 느껴지도록 하는 데 유용할 수 있습니다. 하지만 제한된 값 집합 사이에서 결과가 "핑퐁"되는 것을 막지는 못합니다. 이를 방지하려면 shuffle bag 패턴을 대신 사용하세요.

임의의 사전 값 가져오기

배열에서 사전에도 비슷한 논리를 적용할 수 있습니다.

var _metals = {
    "copper": {"quantity": 50, "price": 50},
    "silver": {"quantity": 20, "price": 150},
    "gold": {"quantity": 3, "price": 500},
}


func _ready():
    for i in range(20):
        print(get_metal())


func get_metal():
    var random_metal = _metals.values()[randi() % metals.size()]
    # Returns a random metal value dictionary every time the code runs.
    # The same metal may be selected multiple times in succession.
    return random_metal

가중 무작위 확률

randf() 메서드는 0.0과 1.0 사이의 부동 소수점 숫자를 반환합니다. 이를 사용하여 서로 다른 결과가 서로 다른 가능성을 갖는 "가중" 확률을 생성할 수 있습니다.

func _ready():
    for i in range(100):
        print(get_item_rarity())


func get_item_rarity():
    var random_float = randf()

    if random_float < 0.8:
        # 80% chance of being returned.
        return "Common"
    elif random_float < 0.95:
        # 15% chance of being returned.
        return "Uncommon"
    else:
        # 5% chance of being returned.
        return "Rare"

RandomNumberGenerator 인스턴스에서 rand_weighted() 메서드를 사용하여 가중치가 적용된 무작위 인덱스*를 얻을 수도 있습니다. 0과 매개변수로 전달된 배열 크기 사이의 임의의 정수를 반환합니다. 배열의 각 값은 인덱스로 반환될 *상대적 가능성을 나타내는 부동 소수점 숫자입니다. 값이 높을수록 값이 인덱스로 반환될 가능성이 높음을 의미하고, 0 값은 인덱스로 반환되지 않음을 의미합니다.

예를 들어, [0.5, 1, 1, 2]``가 매개변수로 전달되면 메소드는 인덱스 ``1``와 비교하여 ``3``(값 ``2``의 인덱스)를 반환할 가능성이 배, ``0``(``0.5 값의 인덱스)를 반환할 가능성이 두 배 더 높습니다. 2.

반환된 값은 배열의 크기와 일치하므로 다음과 같이 다른 배열에서 값을 가져오는 인덱스로 사용할 수 있습니다.

# Prints a random element using the weighted index that is returned by `rand_weighted()`.
# Here, "apple" will be returned twice as rarely as "orange" and "pear".
# "banana" is twice as common as "orange" and "pear", and four times as common as "apple".
var fruits = ["apple", "orange", "pear", "banana"]
var probabilities = [0.5, 1, 1, 2];

var random = RandomNumberGenerator.new()
print(fruits[random.rand_weighted(probabilities)])

셔플백을 사용한 "더 나은" 무작위성

위와 동일한 예를 사용하여 무작위로 과일을 선택하고 싶습니다. 그러나 과일을 선택할 때마다 난수 생성에 의존하면 덜 균일한 분포가 발생할 수 있습니다. 플레이어가 운이 좋으면(또는 불운하면) 같은 과일을 연속으로 세 번 이상 얻을 수 있습니다.

shuffle bag 패턴을 사용하여 이를 수행할 수 있습니다. 요소를 선택한 후 배열에서 요소를 제거하여 작동합니다. 여러 번 선택하면 배열이 비어 있게 됩니다. 그런 일이 발생하면 기본값으로 다시 초기화합니다.

var _fruits = ["apple", "orange", "pear", "banana"]
# A copy of the fruits array so we can restore the original value into `fruits`.
var _fruits_full = []


func _ready():
    _fruits_full = _fruits.duplicate()
    _fruits.shuffle()

    for i in 100:
        print(get_fruit())


func get_fruit():
    if _fruits.is_empty():
        # Fill the fruits array again and shuffle it.
        _fruits = _fruits_full.duplicate()
        _fruits.shuffle()

    # Get a random fruit, since we shuffled the array,
    # and remove it from the `_fruits` array.
    var random_fruit = _fruits.pop_front()
    # Returns "apple", "orange", "pear", or "banana" every time the code runs, removing it from the array.
    # When all fruit are removed, it refills the array.
    return random_fruit

위 코드를 실행하면 같은 과일이 연속으로 두 번 나올 확률이 있습니다. 과일을 선택하면 배열이 비어 있지 않으면 더 이상 반환 값이 될 수 없습니다. 배열이 비어 있으면 이를 기본값으로 재설정하여 동일한 과일을 다시 가질 수 있지만 단 한 번만 가능합니다.

랜덤성(Randomness)

위에 표시된 난수 생성은 입력에 따라 천천히 변경되는 값이 필요할 때 한계를 보여줄 수 있습니다. 입력은 위치, 시간 등이 될 수 있습니다.

이를 달성하기 위해 임의의 노이즈 기능을 사용할 수 있습니다. 노이즈 기능은 사실적으로 보이는 지형을 생성하기 위한 절차적 생성에서 특히 많이 사용됩니다. Godot는 이를 위해 1D, 2D 및 3D 노이즈를 지원하는 :ref:`class_fastnoiselite`를 제공합니다. 1D 노이즈의 예는 다음과 같습니다.

var _noise = FastNoiseLite.new()

func _ready():
    # Configure the FastNoiseLite instance.
    _noise.noise_type = FastNoiseLite.NoiseType.TYPE_SIMPLEX_SMOOTH
    _noise.seed = randi()
    _noise.fractal_octaves = 4
    _noise.frequency = 1.0 / 20.0

    for i in 100:
        # Prints a slowly-changing series of floating-point numbers
        # between -1.0 and 1.0.
        print(_noise.get_noise_1d(i))

암호화 방식으로 안전한 의사 난수 생성

지금까지 위에서 언급한 접근 방식은 암호학적으로 안전한 의사 난수 생성(CSPRNG)에 적합하지 않습니다. 이는 게임에는 적합하지만 암호화, 인증 또는 서명이 관련된 시나리오에는 충분하지 않습니다.

Godot는 이를 위해 Crypto 클래스를 제공합니다. 이 클래스는 비대칭 키 암호화/암호 해독, 서명/검증을 수행하는 동시에 암호화된 보안 임의 바이트, RSA 키, HMAC 다이제스트 및 자체 서명된 X509Certificates를 생성할 수도 있습니다.

:abbr:`CSPRNG(암호적으로 안전한 의사 난수 생성)`의 단점은 표준 의사 난수 생성보다 훨씬 느리다는 것입니다. API는 사용하기도 덜 편리합니다. 따라서 게임플레이 요소에서는 :abbr:`CSPRNG(암호적으로 안전한 의사 난수 생성)`을 피해야 합니다.

Crypto 클래스를 사용하여 ``0``와 ``2^32 - 1``(포함) 사이에 2개의 임의의 정수를 생성하는 예:

var crypto := Crypto.new()
# Request as many bytes as you need, but try to minimize the amount
# of separate requests to improve performance.
# Each 32-bit integer requires 4 bytes, so we request 8 bytes.
var byte_array := crypto.generate_random_bytes(8)

# Use the ``decode_u32()`` method from PackedByteArray to decode a 32-bit unsigned integer
# from the beginning of `byte_array`. This method doesn't modify `byte_array`.
var random_int_1 := byte_array.decode_u32(0)
# Do the same as above, but with an offset of 4 bytes since we've already decoded
# the first 4 bytes previously.
var random_int_2 := byte_array.decode_u32(4)

prints("Random integers:", random_int_1, random_int_2)

더 보기

생성된 바이트를 정수 또는 부동 소수점과 같은 다양한 유형의 데이터로 디코딩하는 데 사용할 수 있는 다른 방법에 대해서는 :ref:`class_PackedByteArray`의 설명서를 참조하세요.