This is an ongoing personal project that was started in September 2021. What was originally an experiment in procedural level generation has turned into developing a full roguelike. The project has helped me practice and develop several pipelines commonly used across games. On the asset side, I've learnt how to produce and optimise models that use trimsheets to reduce draw calls and create modular elements that allow me to iterate quickly on spaces.
The game has been built entirely with Unreal Engine 5 and Blueprint.
The game has been built entirely with Unreal Engine 5 and Blueprint.
SKILLS DEVELOPED
- Procedural Level Generation
- 3D Animation Pipeline
- UE5 Blackboards and AI
- Enemy Management and Spawning Systems
- 3D Modelling and Texturing
- Project Management
- Optimising dynamic lighting for procedurally generated and placed levels.
PROCEDURAL LEVEL DESIGN
Level Flow:
The generator used in DEMONS VOID generates a tower as the player finishes encounters. With this generator I wanted to achieve a sense of progression and challenge seen in games like Inscryption and Hades but still allow the player to move back through the level to return to various NPCs and vendors like in Enter the Gungeon or Binding of Isaac. The main process is as follows:
- The generator selects a spawn room from the data table, and places it at a designer specified world location. The player is then spawned in.
- After the intro sequence ends, an easy tier encounter (combat room) is randomly selected from the data table, and placed above the player.
- The player can then move up to the next room and begin the combat encounter. Once the encounter ends and all waves of enemies have been defeated, the player is given a choice of three rooms.
- Each option has a small chance to become an interstitial, which simply uses a weighted boolean check, and if it succeeds it will select a random interstitial from the database. These can be either a vendor or challenge NPC room. If the check fails, it will increment the chance by a designer specified amount (built in as a scalability option for different levels) and select a random encounter at a random difficulty in its place.
- This process will repeat a specified number of times until we reach that, at which point it spawns a boss room.
Optimisation:
As with any procedural generator, dynamically generating environments can get expensive quickly. To make this system streamlined in the logic, I had two main principles:
- The system must only use one data table to store information for the rooms. This means that I can scale it easily and make bulk edits quickly in external tools such as Excel.
- The generator itself must have no ongoing logic at any time in the game. Any animation related to spawning the room or progressing the level must be done in the room actors (which draw logic from a parent actor) and send calls up to the generator only when absolutely necessary through blueprint interfaces.
Modular Assets:
Setting up the assets has required a great deal of experimentation and iteration, causing me to walk a fine line between style and performance. In terms of visuals, I decided to try and emulate a slightly surreal brutalist look. In the UK, we have many great examples of brutalist architecture, and while a lot of people find it ugly, there is a certain oppressive nature to the style of architecture that meshes well with the narrative context of the project.
From a technical standpoint, one of the earliest drafts of the level generator using these assets had the game running over 2500 draw calls per frame with only 7 or 8 rooms spawned at a time. I have since been able to reduce this drastically to around 800~ draw calls per frame with 10-20 rooms on screen at a time through the use of trimsheets, culling volumes, and merging the base room geometry into one mesh. These also use:
From a technical standpoint, one of the earliest drafts of the level generator using these assets had the game running over 2500 draw calls per frame with only 7 or 8 rooms spawned at a time. I have since been able to reduce this drastically to around 800~ draw calls per frame with 10-20 rooms on screen at a time through the use of trimsheets, culling volumes, and merging the base room geometry into one mesh. These also use:
- 8x8m metrics for a cleaner grid based layout.
- Trimsheets for base geometry to reduce the amount of textures in memory. As I go on to add decals and further the look, these will become less and less noticable.
- Culling for smaller decorative assets. This is hidden through the fog effect I have in the main level.
- Trimsheets for base geometry to reduce the amount of textures in memory. As I go on to add decals and further the look, these will become less and less noticable.
- Culling for smaller decorative assets. This is hidden through the fog effect I have in the main level.
Enemy Management
Spawning and Movement:
Enemies are spawned when rooms have finished their placement animation. This is done through the use of scene components that are attached to the rooms on a case by case basis. This allows me to move the spawn points and specify an enemy tier to choose from in the data table.
For example, in one room I might want the main floor to spawn only area denial enemies like those found in tier 3, and then on the upper levels I might want a shooting gallery of sorts, so I will specify the spawnpoints there to spawn only turret based enemies from tier 2. This means that while the player may come across the same room over multiple runs, the enemies will likely change each time.
The navmesh is built dynamically as the game goes on. This is necessary as each room actor is classified as a dynamic object. Currently there is one large navmesh volume that encompasses an area tall enough to fit twenty rooms. I have also looked into adding navigation cost volumes to each room, and will likely be implementing these alongside traps, to stop enemies from walking over them all the time.
Animation Implementation:
Animation in DEMONS VOID generally tends to be done slightly differently for each enemy as they have different needs. For example, enemies will use a mixture of behaviour trees, animation montages and animation blueprints. The main decision making process will have the behaviour tree decide what state (enumerator) to put the enemy in and play the relevant animation or montage. Some enemies might not use a skeletal mesh, instead relying on a simple static mesh with a material that uses a parameter to animate something.