Making a Procedural 2D World in Unity Part II: Structural Generation
Hi, it’s a me! Again! This is the second part of a little piece I wrote a few weeks ago in which I talk about the procedural elements and the approach we took in Twin Flames. If you haven’t read it, I highly recommend that you do before reading this one. Go on, I’ll wait.
So, quick recap. Twin Flames’ level generation is a two step process: first we have a structural generation and then we have a physical generation. In this article I’ll focus on the first step.
This is based on two main elements: the structural level and the structural room. We define the level as the combination of all the rooms, with all the rules that go with the total level (size, probability of an alternate branch, number of necessary rooms, etc). We define the room as the element that combines the room rules (size, type, base position) and can connect to other rooms like itself via exits.
-Structural Level – It controls the level creation. It spawns the first room and from there creates a series of adjacent rooms. It also stores all the properties that a level should have. In order to achieve this, in Twin Flames we created the script StructuralLevel, which looks like this in the Unity Inspector:
–Unity Inspector – For now, the Inspector allows us to perform internal testing. We can define if we want to create a new seed or use a predefined one, we can change the level size, number of rooms and even the size of the boss room! 😀
–Code – The main engine for the level generation is in the Generate() method. Its function is to create an initial room and use the method GeneratePath() to assign to each room the creation of their adjacent rooms.
The logic in the creation of the exits in the room is in the logic of the structural room.
-Structural Room – It defines specific properties that each room should have (size, position, type, exits) how each type is created and how it connects to other rooms. It has three main steps: Instantiate, exit creation and adjacent room creation.
–Useful Functions – Before all this, we think it’s necessary to run a few functions that help us calculate a few useful things. These are: GetValidRoomBuildDirection(), GetValidSpaces(), GetRight(), GetLeft(), GetUp() and GetDown().
GetValidRoomBuildDirection() – Is used to find a valid direction in which we can build. It picks a random direction and finds out if the space in said direction is available or not. It has a maximum number of tries defined, and when it does not find an available direction, it returns DIRECTIONS.None.
GetValidSpaces() – It tells us the number of spaces in the available direction found. It evaluates one by one if a space in certain direction is available until there is none. It has a counter, so you can get the exact number of spaces in the direction specified.
GetLeft(), GetRight(), GetUp(), GetDown() – They’re all very similar. They just tell if a certain position is occupied by an existing room or not.
–Instantiate – Instantiation is when the creation of a room without its exits occurs. In Twin Flames we have various types of rooms, some of them have sizes and properties well defined, and others are more random in nature. For instance, the Init room (the first room of a level) is a 2×1 room with an exit to the right, while a normal room is quite variable in size and possible exits.
Init Room – The beginning room. This is one of such rooms that is previously defined and never changes. To create a room, we have two new variables (horizontalDirection and verticalDirection) , which are used to indicate a base position and in which direction the room will go. Another important note is that this room now takes two spaces: its base position (where its starts) and one position to the right (because the room goes that way). Storing which positions are taken is very important because in the future you’ll be able to tell where you can put new rooms and where you cannot. The exit generation step is simplified in this case because the init Room always has only one exit to the right, and the next room always is a normal type.
Normal Type Rooms – The creation of this room has two main parts: first comes the direction and size of the rooms, then it fills those spaces in the main map.
To calculate the direction and size, you first have to calculate the main direction, this is the starting point for the generation of the room. Once this is defined, it picks a second direction and a valid distance (again, checking which spaces are available and which ones are not). Once this happens, these variables are used to fill the spaces taken by the room and create their size and direction.
–Exits Creation – This is the step in which we add a coherent exit to a created room. The Init Room or other predefined rooms do not need this step because you always know where the exits will be, but the other rooms need this calculations to find an exit. It’s worth noting that, we call them “exits”, but they also function as entrances, so a room (other than the first room, last room and dead end rooms) should at least contain two exits.
The exit creation logic is very similar to the room creation logic. In any given room, first you calculate a valid coordinate, and then a valid direction for the exit. Then it selects the next room type to said exit.
In Twin Flames we cheated a little bit. Creating a 100% procedural level would take a lot of time, so we defined a few rules and limitations for these exits. For instance, 2×2 rooms limit the coordinates in which they can spawn exits, so we reduce the number of possible rooms that must be created next. These values can vary depending on the project and type of game you’re creating, of course. We chose to limit ourself in order to polish other aspects and have a little more control.
–Adjacent Room Generation – This is the part where the cycle of room creation begins. Each room creates a new room next to it, and that room create the next, and so on.
This algorithm creates a path in each room and creates new rooms according to the exits previously created. Each time a new room is created, it calls the GeneratePath() method, so the cycle can go on. The only way for this cycle to end is that it generates a room with no exits available (and we can define when this exitless room will appear, effectively controlling the size of the whole level).
Phew! This was kind of a heavy article, but stick with me, we’re getting to the really fun part.
What do you think? Does it makes sense so far? In the next article I’ll talk about how to physically create the rooms, so don’t miss it!
If you have any questions or suggestions, write them in the comments and I’ll reply and answer you. Hope to hear from you soon! See you next time!
If you like the game, please stay tuned on our social channels:
- Facebook: facebook.com/FatPandaGames
- Twitter: twitter.com/FatPandaGames
- IndieDB: indiedb.com/games/twin-flames
- MadeWithUnity: madewith.unity.com/en/games/twin-flames