One of the more notable things I made during this project was the procedural generation system. For the project we had to pick one programming challenge and one design challenge. Since I’ve been wanting to implement a procedural generation system for a while I pushed for that programming challenge which the team also thought was cool (the design challenge we picked was no HUD). I spent the first two weeks on the procedural generation system and the level module editor (see below). Since we had chosen to do a 2.5d side scroller I decided together with the designers that it would be easiest to create level modules (that are manually crafted) which are parts of the bigger level.
The level modules are all blueprint actor classes deriving from a main LevelModule cpp class, you can then add ModuleWall’s which is a subclass of UStaticMeshComponent. And you also add ModuleDoor’s where you want the player/exit door to spawn, ModuleEnemy’s where you want enemies to spawn, ModuleItem’s where you want items, coins or lives to spawn. All of these are billboard components that the procedural generator will use to randomly choose what will spawn at that location based on a list of weights (or pick a random billboard component in the case of the ModuleDoor after picking which LevelModule to spawn the player/door in). See below for the templated method I used for determining which class to use based on the weights.
When the level modules are done, they can be assigned in a list in the procedural generator together with a DataAsset which specify the size of the level (in level modules), size of the level module (in tiles), size of each tile in the level module (in units) as well as all the weights for items, enemies and obstacles (we didn’t have random obstacles). This is what the data asset interface looked like:
The level modules will be picked at random as long as the corridors between the level modules fit together. For each level module that is not the first one I am creating a list of level modules that are valid and picking randomly from that list. To determine which level module is valid I have an array of int’s in each level module (this either has to be manually specified or if using the level module editor it will be automatically filled in). The int’s in that array are defined as which index on each side the corridor has, index 0 is always at the left/top depending on which side we’re checking. If the corridor is multiple tiles wide, the lowest tile index is used (note that the entire level has to have the same corridor width since it’s defined in the data asset.
Since there is a chance that there might be no valid level modules to pick from if the corridors don’t line up, I have decided to handle that by not spawning anything at that location. But this will mean that the level might look super weird if the designers haven’t made level modules that will cover all of those edge cases. So therefore I am starting the level generation from the center and building out in all directions sequentially. So if there are level modules that can’t generate, the holes will only be in the corners which will make the level look quite good regardless if there are holes. This is how the generation looks:
When the whole level has been generated I patch up any corridors that lead nowhere using the same level module walls. Then I determine all the items, enemies and where the player/exit should spawn. The player spawn will preferably be in either the lower left or lower right corner module while the exit door will be in either the top left or top right corner module. If these two preferred modules doesn’t exist, a random level module will be picked instead.