Making Double Dodge, Part 3: Constructing an Endless Level
Double Dodge is a procedurally generated dodge 'em up which challenges players through multi-tasking objectives. The player controls multiple characters as they avoid obstacles to reach the innermost depths of an ever-changing dungeon. I have been working on this game for over a month and have just released my first playable demo. This multi-part devlog covers my design process to date.
In Part 1 I discussed my history of bouncing between projects and how being more intentional in the design process has helped me stay focused and be more effective.
In Part 2 I discuss how I went from a game jam prompt to designing a core game loop and its mechanics.
In this section, I cover how I designed the procedurally generated level as well as the game’s current content.++
High-Level Level Design
Level design is not a topic I’ve studied in the past, so my approach here has been based on intuition and trying things rather than any theory or practical advice. When I first sat down, I had completed my game loop (or at least an early version of it). Before I could really get into any detailed design work, I need to answer a few high-level questions.
The first question was whether I wanted to do procedural level design or hand-crafted. This was an easy decision for me, as procedural more clearly aligned with my core experience (making the game dynamic). At that point I had never built a procedural game, so I wasn’t sure if this would add too much scope to the project (spoiler, it added quite a bit). After a month I can say, I’ve spent most of my time getting the procedural generation “right”, but I’m now at a point where I feel like I can rapidly add content within the framework I created. Basically, it was a fairly heavy upfront investment that has paid off by making future content generation require less work. Maybe not an ideal tradeoff for a one month project, but hey, it worked out!
The next question was how I wanted to generate the “dungeon”. The simplicity of my game loop more-or-less necessitated that my game either have a fixed length (and simply “end”) or be designed as an endless mode. In terms of the actual structure of the “dungeon”, I was between a room based map (i.e. Binding of Isaac) or some form of endless runner (i.e. Temple Run). This was a tough decision for me, given my time constraints. It felt like whichever choice I made, I was stuck with it even if it didn't come out well. Picking one or the other would fundamentally change how I designed the remaining content and changing my decision would require a ton of additional content rework. I felt (and still feel) that a dungeon format would have more closely aligned to my core experience in terms of making the game feel more dynamic. However, I opted for the safer pick because I knew that I might be overscoping my project if I tried to tackle dungeon design. Oh well. I just need to remind myself that I’ll always make less games than I imagine in my head, and to not cling to the ideas that never come to fruition.
Defining Sections and Chunks
With the higher level decisions in place, I could focus on breaking things down into manageable design chunks. I created an abstraction for a room that was always a fixed M x N tile grid (10 x 40 in the demo). A chunk would therefore be a series of Z sections laid out in a series (currently 6 plus a rest section), with each section’s “exit” connecting to the next section’s “entrance”. With split screen, this would result in two hallways, side by side, like so:
The on-screen distance between the two hallways makes tracking obstacles on both sides pretty tough, but not impossible. When a player enters a section (i), the section after it (i+1) is generated, and the section two before (i-2) is destroyed to ensure machine memory is always freed up. This required me to build a camera constraint that the player could never move backward more than a certain distance from their forward-most position, otherwise they could walk back to a destroyed section and break the game. This camera function also had the benefit of encouraging forward motion without removing flexibility for dodging backwards.
Finally, each section will generate its own obstacles. While an obstacle can move between sections, it is always originated in the currently ‘active’ section, i.e. the one the player has stepped into. In the diagram above, when the player steps on the blue “trigger”, the yellow section will be generated, but the green section will be “activated”. All obstacles are designed in such a way that their individual state machine is “idling” until activated.
Obstacle Design
In order to build towards the core experience, it was imperative that the obstacle design closely relate to a sense of mastery, that each individual section be dynamic (i.e. varied) and that the core challenge must come from having to multitask. Since the sections themselves were fixed, this meant that all the dynamism of the game had to come from how the obstacles were generated. This led me in the direction of randomly spawning a mix of different obstacle types. Each section would select which obstacle types it would generate and how many of each. Every obstacle would be designed with a unique behavioral pattern that clearly challenged the player along the core mechanics of avoiding enemies: identifying obstacles, predicting motion, planning a path and moving forward. For instance, the first obstacle I created in the whole game is the “rock”. It blends into the background a little bit, so it takes a small amount of observation. It doesn’t move though, so no prediction is required and they are easy to plan around. Also, they don’t do damage but impede your path, so running into one is mostly just annoying. That is, until paired with another obstacle type that you do have to avoid. While individually harmless, if you aren’t paying attention to the rocks, your planned path will be disrupted and the obstacle you are avoiding might hurt you.To date, I’ve only built out a handful of obstacle types, but they all follow similar patterns. The centipede prevents you from rushing forward because it blocks off the entire road during portions of its movement pattern. The darts require you to plan ahead and move into a specific lateral position while avoiding other obstacles. The “shooters”- the lava monsters that shoot the purple bullets -prevent you from standing still in their range. Like the rocks, they don’t provide much challenge individually. When coupled with the centipede or darts though, the player can easily find themselves stuck in the shooter's line of fire.
I hope this pattern of designing simple obstacles that, when combined, create tricky scenarios will lead to emergent gameplay over time. My inspiration here is Chess. The rules governing each individual Chess piece are dead simple. When combined, the decision space is so large that it takes people a lifetime to master the game. Even with only 5 obstacle types, I’ve already noticed how they’ve started to combine in ways I did not intend as a designer.
Layer Based Obstacle Generation
So how do I actually pick the obstacle types and place them on the board? This has taken some iteration, but the abstraction that I’ve landed on is something I’m calling a “layer-based approach”. Rather than picking obstacle types from a bag, my AI director picks layers from a bag. The difference is that the layer governs how a set of obstacles will be placed. To date, each layer I’ve created only places multiples of a single obstacle type, but it’s flexible enough that that doesn’t have to be the case in the future.
Each layer (so far) follows one of two placement strategies: partitioned or recurring. A partitioned layer always places obstacles using a fixed grid pattern, with some “jitter” or random displacement. Here’s a diagram of how the shooters are placed:
The shooters all spawn along the left hand side of the path, in the lava. I first break the section vertically into fixed height partitions. Each partition comprises a rectangular grouping of tiles. Then I place one shooter in each partition, but at a randomly selected tile within it. This randomness, or “jitter”, allows for some variations in the pacing and intensity of the section, while ensuring a fixed average separation between units so that it's never too clustered. In this example, the shooters labelled “A” are uniformly distributed, creating an even pacing for that portion of the section. The “B’s” on the hand are denser, therefore producing more intense gameplay. This is balanced by having larger pockets of lower intensity before and after.
The second placement type I’ve used to date are “recurring layers”. These spawn enemies based on a timer. Their placement is always relative to the player, rather than the section itself. The darts layer is a good example. Every two seconds a batch of darts are spawned just offscreen and given an initial velocity to shoot down towards the player. When the batch is spawned, the layer randomly selects which “lanes”- which tile along the x axis - they will occupy. Recurring layers begin spawning when their section is “activated”, which is right as the player enters the section. They are “deactivated” as the player leaves. All layers of a shared type, for instance all dart layers, share a global timer. This ensures that if the same layer type is selected in consecutive sections the transition will be fluid.
All of these different layers are mixed and matched with each other. I’ll cover the exact selection mechanic in the next section.
In addition to this layer system, there is a complimentary “pacing system” which is effectively a layer that always has to be selected and is limited to a small subset of static obstacles. This is what generates the rocks and lava pits along the path throughout the game. The goal of this additional layer is to ensure that there is a minimum density of obstacles throughout the game. Initially, I had treated rocks and pits as standard layers that could be selected. I realized pretty quickly there were too many pockets which were totally empty and non-threatening. When rocks or pits were chosen as a layer, it totally changed the feel of the game. The pacing was just off. Once I made the switch, it was like playing a different game. Underneath the hood, this layer is partitioned just like the shooter, except over the walkable portions of the section. It randomly drops either a rock or pit in each partition. The probability of each is dependent on the overall difficulty scale at the time (more on this later). In the beginning of the game there is a 100% chance of selecting a rock and that gradually shifts towards being 100% pits. As I add more content, I’ll balance these probabilities and add other types of obstacles into the mix.
The AI Director
The last part of the level design to cover is the AI director. The director serves as the game’s difficulty progression system, is responsible for selecting the layers and chooses which side of the screen to place each entity.
The progression system is pretty simple. Each time the player finishes a chunk, the global difficulty increases by one. The first section of each chunk has a difficulty equal to the global difficulty. Each time the player enters a new section within that chunk, the difficulty is bumped up a bit. This creates a cycle of rising and released tension, like a steadily increasing sawtooth wave function. In my self-testing, I found this to create a nice flow, though more experimentation will be needed if I ever get to test this with other players.
The layers for each section are selected using a point system. The director allocates a number of points using the formula 2 * local_difficulty, which expands to 2 * (global_difficulty + floor(section_num_within_chunk / 2)). Separately, I assign point values to each layer based on their perceived difficulty. Points are not linear, meaning two points are not twice as hard as one point. I don’t have enough layers or gameplay data to devise an empirical process for allocating points. In a future version I can imagine a heuristic model that adjusts points based on how often players lose to this layer relative to other layers. Some day! For now I make my best guess and tweak. More often than not, I end up tweaking the layer to match a desired difficulty rather than the reverse. Regardless, the director will randomly select layers until all points have been allocated. There are some additional constraints, but this is a good general overview.
The last of the director’s functions is to choose which screen to spawn any generated obstacle on. When the layers generate obstacles, they do so on a “virtual grid”. This grid mimics the exact proportions of a section. Once the grid is initialized, the director iterates through each obstacle and effectively flips a coin to pick a side. The first iteration of the game had the director assigning full layers to each side. I found this to cause a lot of clustering. Too often the player’s full attention was on one screen or the other. This approach is more balanced and even feels more fluid and dynamic.
Conclusion
Despite only working on this game for a month, I feel like I’ve been able to build a really robust core for generating content. To be fair, this has come at the cost of actually generating content to include in the demo. There is less variety in the demo than I had initially hoped for. Still, there is enough that I feel confident that the system works.
Generating more content will be my most immediate next step. I want to create a few more “pacing obstacles” like rocks and increase my layer type count from three to ten. My hope is this is enough content to learn what works and what doesn’t. I’ll use that information to inform how to add “depth” to the game by expanding the mid and long-term game loop systems. At that point, I'll have a real game!
If you’ve made it this far, thanks for reading! I hope this gives you some inspiration for your own work. I’ll do my best to post design updates as I progress, maybe one a month. See you next time!
Leave a comment
Log in with itch.io to leave a comment.