Sunken
A third-person dungeon crawler featuring an infinite dungeon that is procedurally generated each time the player enters it. This was a solo project where I created and coded all game elements in 10 weeks with the exception of animations (which were bought online and retargeted onto the character skeleton). Due to the time constraint, this version of the game lacks a lot of polishment but core systems such as the PCG dungeon and player combat are both being demonstrated.
The Dungeon
Procedural Generation:
In Sunken, the goal of the player is to stay alive and explore further and further into the unpredictable dungeon. To make the gameplay experience truly fresh and save time on manual level creation, Sunken uses a series of maze generation algorithms to generate a fresh new map layout every time the player enters the dungeon.
Rooms:
There are 2 types of rooms in Sunken, I refer to them as RandomShapedRoom and SetShapedRoom. Once we have inputted how large the overall map should be, a simple trial-and-error placement algorithm randomly tries to place as many rooms as it could within a given amount of iterations.
a bunch of RandomShapedRooms with their identification number on them
SetShapedRooms are as the name suggests, rooms with pre-determined sizes in both X and Y axis, allowing me to insert pre-designed rooms into the dungeon.
RandomShapedRooms on the other hand are generated by first generating a square of random size, and then repeatedly generating smaller and smaller squares around its edge until the room is at the desired size. Their purpose is to introduce an element of true randomness into each layout to make it less predictable.
Paths:
Once all the rooms are in place, we now need to figure out how to connect them in a way that not only makes sure that every single room is accessible to the player but also not be so straightforward as to take the shortest path possible to each other(it is a maze after all). Here I am basically generating a maze that fills every single square that is not occupied by a room, by picking a random empty point on the grid and allowing it to branch around itself until it has no more tiles to expand upon. This means I end up with a maze where any single tile in it is connected with any other tile.
The maze pathway that fills up every single empty square not occupied by a room.
Creating doors and cleaning the path up:
At this point, we are confident that if we just create a door anywhere along the outer wall of a room, we will connect to a pathway. Even better, we are sure that all the doors created this way will be connected to each other somehow as we know from the previous step any point in the maze connects to any other point. Once we have all of our doors created, it’s time to clean up the excess pathways that we do not want to make the maze less confusing for the player.
We can achieve this by first finding all the tiles that have 3 walls on them, meaning that they are dead-ends. Once we find all of them, we check to make sure that this doesn’t happen to be a dead-end with a door on it and remove it from being part of the path. This effectively creates a new set of dead-ends as a path tile that used to connect to a dead-end tile is now a dead-end itself. So we repeat this process over multiple iterations, effectively shrinking the paths that lead to dead-ends until they either stop on a tile with a door or be reduced drastically in length. We could technically keep killing them until there is not a single dead-end left in the maze, but for increased difficulty in the navigation of the map I wanted to keep a few of them around so the players aren’t always guaranteed to hit another room once they exit from one.
The map after the clean-up. Doors are marked with emissive green balls.
Walls:
Now it’s time to generate actual mesh for the dungeon floor. An actor called the TileHandler is created on each path tile, and its job is to calculate where and how many walls it should have based on the state of its neighbors.
Walls have been created for all the path tiles, while empty tiles are generated as a black cube.
Dressing up the level:
It was at this point I felt that I needed slightly better models for the floor and walls of the path tiles, and I experimented with multiple different designs for the wall. I wanted something that had a rocky and uneven surface, but not too visually distracting for the player.
different iterations of wall designs tested in different lighting conditions
I have eventually settled with the last design as I find it to strike a balance between the other designs. Along with the addition of a new torch that I have modeled in an hour, I have managed to make the hallways of the dungeon look much more complete.
Creating SetShapeRooms:
Now with the hallways looking almost decent, I turned to make a modular kit for the SetShapeRooms. Every tile in my generation is exactly 1 meter long, which made it very easy to create tileable floor tiles/wall tiles that can be simply tossed in the generation code and have them fit perfectly with each other.
The first SetShapedRoom I have made. There are still some visible seams where two wall tiles connect due to the way the brick pattern is, so I have created some pillar pieces to hide them where I could. As for the floor, I found creating a large patch of uneven dirt tiles that are larger than a normal tile, and randomizing their scale and rotation worked well to break the repetitiveness in the floor tiles.
Characters
Player Character
I modeled the player character in Maya and thought this would be a good opportunity to expand my knowledge of character animation. So I learned A.R.T. from an animator friend of mine and rigged the main character in Maya which took a lot longer than I had expected.
I also made sure to skin and rig the braid part of her hair, as I modeled them with the intention to physic-sim them in Unreal Engine.
Attack has 4 hits in its combo, and a special spin thrust to mix in
After failing to find animations that satisfy me on Mixamo, I decided to purchase a motion-captured animation pack of a woman doing sword-fighting moves. I was able to successfully transfer the animations over in a relatively reasonable amount of time using skeletal retargeting, and with the help of the Knight model I so often use from Mixamo I managed to quickly create a dummy enemy to test my animations on.
The finished character had 4 different dodge animations that changes based on the movement direction input before the dodge action is performed. I had created two sets of independent state machines for her animation since her attack animation did not blend nicely into the idle/run animation I had found for her. So as the solution the character would transition from her non-combat state machine into her combat state machine whenever she would perform an attack of any sort, and transition back after not attacking for a little while. Creating the code for her attack sequence was also tricky but I eventually ended up with a system where I specify different action point along an animation using animation notifiers, and the system will take care of turning on/off her weapon active frame, or allowing her to interrupt the previous animation with certain new ones.
Enemies
Enemies are the next in line as I now have a large but empty dungeon filled with nothing for the player to fight. I modeled an archer as the first enemy type. The design is inspired by archer armor in medieval times, as I particularly like the design of some helmets that almost covered their entire face.
Archer model in engine, with his crossbow-gun
Me trying to deal with multiple archers as once
Fortunately, I was able to find some basic soldier shooting animations on Mixamo and was able to modify them enough for the archer to use. However, I had to write some code in order to not only have the archer turn his head to look at players when they notice them, but also adjust their arm rotation so they are actually aiming their crossbow at her.
It was at this point that I realized that I did not have enough time left to create a new character model for another type of enemy if I want to finish all the other features I have planned, so I decided to instead just polish up the AI of the dummy fighter I had created earlier, and use him as the grunt type enemy that is most often encountered in the dungeon.
Game Play Loop
I now have all the key components that I need, but its time to create a simple gameplay loop so the game would have some short/long term goals for the player.
Elevaters
The player needed a way to enter the floor of a level, and be able to exit into the next one. For this, I have modeled an elevator within which the player would enter and exit a floor.
Entering a new floor inside an elevator
The entry elevator basically traps the player inside of itself until the floor is done generating(usually takes around 10 seconds), and “arrives“ at its destination once the floor is ready. I have created a room to hold the elevator during these waiting times, but to make it seem like the elevator is traveling I have coded the walls of the room keeps looping upwards instead. Once the level is ready, the elevator then teleports to the top of the elevator shaft in the level, and only then actually moves downwards to bring the player into the entrance of the new level.
Entering the exit elevator of a level to proceed to the floor below
The exit elevator works much the same way, except this time it is spawned into the level with its doors open, and once the player enters it, it will close its doors and starts moving downwards.
Now with both elevator rooms(entry and exit) always spawned into the map, the player would have a long-term goal in the dungeon: to search and enter the elevator leading to the next floor of the dungeon.
Currency & Respawnning
In terms of short to mid term goals, I wanted to address the problem that the player would need to start all the way from the top if they ever died during their exploration.
Spending gold to purchase a respawn point
The respawn shrine was my answer to that problem. As the player defeats enemies in the level they will collect coins dropped by the enemy. They can then use these coins to purchase respawn points at different locations of the map. This will allow them to respawn back at the totem if they ever died on this floor, but once they travel to a new floor they would need to purchase a new respawn point if they want to be able to respawn after death.
Dying and using the last respawn point to revive
Environmental Destruction
Destructible environment was something that I had in mind from the very beginning when making this project. As the result, I had made sure all of my environmental assets are modeled with full distractibility in mind. The algorithms I have chosen to generate the levels also ensure that there are a lot of hallways/rooms that are only a single wall away from each other, which gives the bomb utility not only in combat but also in creating shortcuts between different areas. In order to not tank my frame rate by spawning the level with each brick being an individual entity, I came up with a cheaper system:
Player tosses a capsule-shaped bomb, which detonates after a short delay
an object that is about to destroy any environmental asset (bombs in this case), will technically cast their field of destruction twice. The first time I will collect all the walls this destruction would affect, and swap those assets out with the much more expensive version of it that has each brick being an individual entity. Then comes the second cast, which happens one frame after the first, would then identify which bricks need to start to simulate physics and be affected by the force of the explosion. In testing this worked surprisingly well as most walls in my level were able to stay in one piece, and only those that were actually blown had to spawn additional entities for the parts that were destroyed.
Path Markers
The last thing I implemented in this prototype build was a marker system, where the player is able to create different markers on the wall, which will help them navigate the dark and confusing halls of the dungeon. I had plans for the player to be able to obtain different designs, possibly with different uses and quarks such as a design that makes a distinct sound that can be heard through walls, thus helping the player mark the location to bomb in order to reach a certain area they had reached before.
In the end, I only had time to create a single design which is an arrow. The player is able to use it to make basic marks on the walls of the dungeon, which had proven to be surprisingly helpful in navigating what would otherwise be rather featureless hallways.