Search code examples
scenegodotgdscript

Godot : How to instantiate a scene from a list of possible scenes


I am trying to create a game in which procedural generation will be like The Binding of Isaac one : successive rooms selected from a list. I think I will be able to link them together and all, but I have a problem : How do I choose a room from a list ? My first thought is to create folders containing scenes, something like

  • zone_1
    • basic_rooms
      • room_1.tscn
      • room_2.tscn
    • special_rooms
      • ...
  • zone_2
    • ...

and to select a random scene from the folder I need, for example a random basic room from the first zone would be a random scene from zone_1/basic_rooms.

The problem is that I have no idea if this a good solution as it will create lots of scenes, and that I don't know how to do this properly. Do I simply use a string containing the folder path, or are there better ways ? Then I suppose I get all the files in the folder, choose one randomly, load it and instanciate it, but again, I'm not sure.

I think I got a little lost in my explainations, but to summarize, I am searching for a way to select a room layout from a list, and don't know how to do.


Solution

  • What you suggest would work.

    You can instance scene by this pattern:

    var room_scene = load("res://zone/room_type/room_1.tscn")
    var room_instance = room_scene.instance()
    parent.add_child(room_instance)
    

    I'll also remind you to give a position to the room_instance.

    So, as you said, you can build the string you pass to load.

    I'll suggest to put hat logic in an autoload and call it where you need it.


    However, the above code will stop the game while it is loading the scene. Instead do Background Loading with ResourceLoader.

    First you need to call load_interactive which will give you a ResourceInteractiveLoader object:

    loader = ResourceLoader.load_interactive(path)
    

    Then you need to call poll on the loader. Until it returns ERR_FILE_EOF. In which case you can get the scene with get_resource:

    if loader.poll() == ERR_FILE_EOF:
        scene = loader.get_resource()
    

    Otherwise, it means that call to poll wasn't enough to finish loading.

    The idea is to spread the calls to poll across multiple frames (e.g. by calling it from _process).

    You can call get_stage_count to get the number of times you need to call poll, and get_stage will tell you how many you have called it so far.

    Thus, you can use them to compute the progress:

    var progress = float(loader.get_stage()) / loader.get_stage_count()
    

    That gives you a value from 0 to 1. Where 0 is not loaded at all, and 1 is done. Multiply by 100 to get a percentage to display. You may also use it for a progress bar.


    The problem is that I have no idea if this a good solution as it will create lots of scenes

    This is not a problem.

    Do I simply use a string containing the folder path

    Yes.

    Then I suppose I get all the files in the folder, choose one randomly

    Not necessarily.

    You can make sure that all the scenes in the folder have the same name, except for a number, then you only need to know how many scenes are in the folder, and pick a number.

    However, you may not want full randomness. Depending on your approach to generate the rooms, you may want to:

    • Pick the room based on the connections it has. To make sure it connects to adjacent rooms.
    • Have weighs for how common or rare a room should be.

    Thus, it would be useful to have a file with that information (e.g. a json or a csv file). Then your autoload code responsible for loading scenes would load that file into a data structure (e.g. a dictionary or an array), from where it can pick what scene to load, considering any weighs or constraints specified there.


    I will assume that your rooms exist on a grid, and can have doors for NORTH, SOUTH, EAST, WEST. I will also assume that the player can backtrack, so the layout must be persistent.

    I don't know how far ahead you will generate. You can choose to generate all the map at once, or generate rooms as the player attempt to enter, or generate a few rooms ahead.

    If you are going to generate as the player attempts to enter, you will want an room transition animation where you can hide the scene loading (with the Background Loading approach).

    However, you should not generate a room that has already been generated. Thus, keep a literal grid (an array) where you can store if a room has been generated. You would first check the grid (the array), and if it has been generated, there is nothing to do. But if it hasn't, then you need to pick a room at random.

    But wait! If you are entering - for example - from the south, the room you pick must have a south door to go back. If you organize the rooms by the doors they have, then you can pick from the rooms that have south doors - in this example.

    In fact, you need to consider the doors of any neighbor rooms you have already generated. Thus, store in the grid (the array) what doors the room that was generated has. So you can later read from the array to see what doors the new room needs. If there is no room, decide at random if you want a door there. Then pick a room at random, from the sets that have the those doors.

    Your sets of rooms would be, the combinations of NORTH, SOUTH, EAST, WEST. A way to generate the list, is to give each direction a power of two. For example:

    • NORTH = 1
    • SOUTH = 2
    • EAST = 4
    • WEST = 8

    Then to figure out the sets, you can count, and the binary representation gives the doors. For example 10 = 8 + 2 -> WEST and SOUTH.

    Those are your sets of rooms. To reiterate, look at the already generated neighbors for doors going into the room you are going to generate. If there is no room, decide at random if you want a door there. That should tell you from what set of rooms you need to pick to generate.

    This is similar to the approach auto-tile solution use. You may want to read how that works.


    Now assuming the rooms in the set have weights (so some rooms are more common and others are rarer), and you need to pick at random.

    This is the general algorithm:

    1. Sum the weights.
    2. Normalize the weights (Divide the weights by the sum, so they add up to 1).
    3. Accumulate the normalized weights.
    4. Generate a random number from 0 to 1, and what is the last accumulated normalized weight that is greater than the random number we got.

    Since, presumably, you will be picking rooms from the same set multiple times, you can calculate and store the accumulated normalized weights (let us call them final weights), so you don't compute them every time.

    You can compute them like this:

        var total_weight:float = 0.0
        for option in options:
            total_weight = total_weight + option.weight
    
        var final_weight:float = 0.0
        var final_weights:Array = []
        for option in options:
            var normalized_weight = option.weight / total_weight
            final_weight = final_weight + normalized_weight
            final_weights.append(final_weight)
    

    Then you can pick like this:

        var randomic:float = randf()
        for index in final_weights.size():
            if final_weights[index] > randomic:
                return options[index]
    

    Once you have picked what room to generate, you can load it (e.g. with the Background Loading approach), instance it, and add it to the scene tree. Remember to give a position in the world.

    Also remember to update the grid (the array) information. You picked a room from a set that have certain doors. You want to store that to take into account to generate the adjacent rooms.

    And, by the way, if you need large scale path-finding (for something going from a room to another), you can use that grid too.