소스 검색

Add a random number generation tutorial

This closes #2436.
Hugo Locurcio 4 년 전
부모
커밋
7d9ffa77a7
2개의 변경된 파일297개의 추가작업 그리고 0개의 파일을 삭제
  1. 1 0
      tutorials/math/index.rst
  2. 296 0
      tutorials/math/random_number_generation.rst

+ 1 - 0
tutorials/math/index.rst

@@ -10,3 +10,4 @@ Math
    matrices_and_transforms
    interpolation
    beziers_and_curves
+   random_number_generation

+ 296 - 0
tutorials/math/random_number_generation.rst

@@ -0,0 +1,296 @@
+.. _doc_random_number_generation:
+
+Random number generation
+========================
+
+Many games rely on randomness to implement core game mechanics. This tutorial
+guides you through common types of randomness and how to implement them in
+Godot.
+
+.. note::
+
+    Computers cannot generate "true" random numbers. Instead, they rely on
+    `psuedorandom number generators <https://en.wikipedia.org/wiki/Pseudorandom_number_generator>`__
+    (PRNGs).
+
+Global scope versus RandomNumberGenerator class
+-----------------------------------------------
+
+Godot exposes two ways to generate random numbers: via *global scope* methods
+or using the :ref:`class_RandomNumberGenerator` class.
+
+Global scope methods are easier to set up, but they don't offer as much control.
+
+RandomNumberGenerator requires more code to use, but exposes many methods not
+found in global scope such as
+:ref:`randi_range() <class_RandomNumberGenerator_method_randi_range>` and
+:ref:`randfn() <class_RandomNumberGenerator_method_randfn>`. On top of that,
+it allows creating multiple instances each with their own seed.
+
+This tutorial uses global scope methods, except when the method is only found in
+the RandomNumberGenerator class.
+
+The randomize() method
+----------------------
+
+In global scope, you can find a :ref:`randomize() <class_@GDScript_method_randomize>`
+method.
+**This method should be called only once when your project starts to initialize
+the random seed.** Calling it multiple times is unnecessary and may impact
+performance negatively.
+
+Putting it in your main scene script's ``_ready()`` method is a good choice::
+
+    func _ready():
+        randomize()
+
+You can also set a fixed random seed instead using
+:ref:`seed() <class_@GDScript_method_seed>`. This will give you *deterministic*
+results across runs::
+
+    func _ready():
+        seed(12345)
+        # To use a string as a seed, you can hash it to a number.
+        seed("Hello world".hash())
+
+When using the RandomNumberGenerator class, you should call ``randomize()``
+on the instance since it has its own seed::
+
+    var rng = RandomNumberGenerator.new()
+    rng.randomize()
+
+Get a random number
+-------------------
+
+Godot provides several methods to get random numbers.
+
+:ref:`randi() <class_@GDScript_method_randi>` returns a random number between 0
+and 2^32-1. Since the maximum value is really high, you most likely want to use
+the modulo operator (``%``) to bound the result between 0 and the denominator::
+
+    # Prints a random integer between 0 and 49.
+    print(randi() % 50)
+
+    # Prints a random integer between 10 and 60.
+    print(randi() % 51 + 10)
+
+:ref:`randf() <class_@GDScript_method_randf>` returns a random floating-point number
+between 0 and 1. This is useful to implement a
+:ref:`doc_random_number_generation_weighted_random_probability` system, among
+other things.
+
+:ref:`randfn() <class_RandomNumberGenerator_method_randfn>` returns a random floating-point
+number between 0 and 1. Unlike :ref:`randf() <class_@GDScript_method_randf>` which follows
+an uniform distribution, the returned number follows a
+`normal distribution <https://en.wikipedia.org/wiki/Normal_distribution>`__.
+This means the returned value is more likely to be around 0.5 compared to the
+extreme bounds (0 and 1)::
+
+    # Prints a normally distributed floating-point number between 0.0 and 1.0.
+    var rng = RandomNumberGenerator.new()
+    rng.randomize()
+    print(rng.randfn())
+
+:ref:`rand_range() <class_@GDScript_method_rand_range>` takes two arguments ``from`` and
+``to``, and returns a random floating-point number between ``from`` and ``to``::
+
+    # Prints a random floating-point number between -4 and 6.5.
+    print(rand_range(-4, 6.5))
+
+:ref:`RandomNumberGenerator.randi_range() <class_RandomNumberGenerator_method_randi_range>`
+takes two arguments ``from`` and ``to``, and returns a random integer between
+``from`` and ``to``::
+
+    # Prints a random floating-point number between -10 and 10.
+    var rng = RandomNumberGenerator.new()
+    rng.randomize()
+    print(rng.randi_range(-10, 10)
+
+Get a random array element
+--------------------------
+
+We can use random integer generation to get a random element from an array::
+
+    var fruits = ["apple", "orange", "pear", "banana"]
+
+
+    func _ready():
+        randomize()
+
+        for i in 100:
+            # Pick 100 fruits randomly.
+            # (``for i in 100`` is a faster shorthand for ``for i in range(100)``.)
+            print(get_fruit())
+
+
+    func get_fruit():
+        var random_fruit = fruits[randi() % fruits.size()]
+        # Returns "apple", "orange", "pear", or "banana" every time the code is run.
+        # The same fruit may be selected multiple times in succession.
+        return random_fruit
+
+To prevent the same fruit from being picked more than once in a row, we can add more
+logic to this method::
+
+    var fruits = ["apple", "orange", "pear", "banana"]
+    var last_fruit = ""
+
+
+    func _ready():
+        randomize()
+
+        for i in 100:
+            # Pick 100 fruits randomly.
+            # (``for i in 100`` is a faster shorthand for ``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 is run.
+        # The same fruit will never be returned more than once in a row.
+        return random_fruit
+
+This approach can be useful to make random number generation feel less
+repetitive, but it doesn't prevent results from "ping-ponging" between a limited
+set of values. To prevent this, use the
+:ref:`shuffle bag <doc_random_number_generation_shuffle_bags>` pattern instead.
+
+Get a random dictionary value
+-----------------------------
+
+We can apply similar logic from arrays to dictionaries as well::
+
+    var metals = {
+        "copper": {"quantity": 50, "price": 50},
+        "silver": {"quantity": 20, "price": 150},
+        "gold": {"quantity": 3, "price": 500},
+    }
+
+
+    func _ready():
+        randomize()
+
+        for i in 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 is run.
+        # The same metal may be selected multiple times in succession.
+        return random_metal
+
+
+.. _doc_random_number_generation_weighted_random_probability:
+
+Weighted random probability
+---------------------------
+
+The :ref:`randf() <class_@GDScript_method_randf>` method returns a floating-point number
+between 0.0 and 1.0. We can use this to create a "weighted" probability where
+different outcomes have different likelihoods::
+
+    func _ready():
+        randomize()
+
+        for i in 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"
+
+.. _doc_random_number_generation_shuffle_bags:
+
+"Better" randomness using shuffle bags
+--------------------------------------
+
+Taking the same exemple as above, we would like to pick fruits at random.
+However, relying on random number generation every time a fruit is selected can
+lead to a less *uniform* distribution. If the player is lucky (or unlucky), they
+could get the same fruit 3 or more times in a row.
+
+This can be accomplished by using the *shuffle bag* pattern. It works by
+removing the element from the array once it has been chosen. If this is done
+multiple times, the array might end up being empty. In this case, its value is
+reinitialized to its default state where it's full::
+
+    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():
+        randomize()
+        fruits_full = fruits.duplicate()
+        fruits.shuffle()
+
+        for i in 100:
+            print(get_fruit())
+
+
+    func get_fruit():
+        if fruits.empty():
+            # Fill the fruits array again and shuffle it.
+            fruits = fruits_full.duplicate()
+            fruits.shuffle()
+
+        # Get a random fruit (since the array has been suffled)
+        # and remove it from the `fruits` array.
+        var random_fruit = fruits.pop_front()
+        # Prints "apple", "orange", "pear", or "banana" every time the code is run.
+        return random_fruit
+
+When running the above code, the same fruit will *never* be picked more than
+twice in a row. This is because once a fruit has been picked, it will no longer
+be a possible return value unless the array is now empty. When the array is
+empty, we reset it back to its full state, which makes it possible to have the
+same fruit again (but only once).
+
+Random noise
+------------
+
+The random number generation shown above can show its limits when you need a
+value that *slowly* changes depending on the input. (The input can be a
+position, time, or anything else.)
+
+To achieve this, you can use random *noise* functions. Noise functions are
+especially poopular in producedural generation to generate realistic-looking
+terrain. Godot provides :ref:`class_opensimplexnoise` for this, which supports
+1D, 2D, 3D, and 4D noise. Here's an example with 1D noise::
+
+    var noise = OpenSimplexNoise.new()
+
+
+    func _ready():
+        randomize()
+        # Configure the OpenSimplexNoise instance.
+        noise.seed = randi()
+        noise.octaves = 4
+        noise.period = 20.0
+        noise.persistence = 0.8
+
+        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))