Building a Better Wave -- DQ2 Progress Report for March 2022

Designing levels was taking too long, so we designed some tools to speed up the process and we've had great results.

Building a Better Wave -- DQ2 Progress Report for March 2022

So, last month I revealed that we had hired a full time level designer. Our (anonymous for now) new staff member has been incredibly helpful and I'm proud to say that the last month has been one of the most productive ones yet, involving a lot of revamping and rebalancing of the game's base formulas, with seven levels converted from "Lars' embarrassing placeholder" status to "actually interesting and fun."

Key to this has been implementing new tooling and getting a second pair of eyes and hands on the project. We've spent a lot of time ramping up and getting a feel for the engine but it feels like we're hitting our stride together now.

Speeding up the iterative loop

The main challenge with designing DQ2 levels has been that it takes too long. This was for two reasons: 1) the in-house tools were a bit clunky, and 2) the underlying math of balancing enemy waves can be a bit unintuitive.

This is compounded by the fact that testing battles is difficult as well. Once you've beaten a level you gain rewards from it that increase your characters' levels and stats, so as a level designer you can't just go back to one of those battles and play through it again and see if it feels too hard or too easy. Instead you need to make sure you're at the right power level for that mission and then test it.

Formerly I'd been relying on a combination of a cheat code that plays through the game automatically up to a defined point, and exporting save file snapshots at various points of progression. That's clunky, but works. But it really falls apart when you're tearing up a lot of levels at once, invalidating all your old save files. In any case, the difference between downtime of 5 seconds and even 1 minute when making level design changes is astronomical, and it's frankly embarrassing I haven't fixed this any sooner.

But now it's fixed! At our level designer's request, I have now implemented a cheat function that he can input in the debug version of the game that will speedily calculate how much experience a straight playthrough of the game would have generated up to the current mission, and hard-set the player's character levels to what they ought to be naturally. Then he can playtest any level in the game quickly and easily without having to auto-play from scratch or importing any save files.

Designing better levels

The other bit of trouble we've discovered in designing levels is that the numbers are particularly tricky to work with. The amount of juice you get from killing enemies is tied to the enemies' levels, and obviously scales with the number of units (more enemies killed = more juice earned). So if you up the amount and/or level of enemies in the next wave, it's not immediately clear if you're actually making the level any easier or harder relative to the player's expected amount of in-game power at that point.

To this end our designer created an internal tool called "wavegen" that lets you express the designer's intent more directly and then let the level derive what the actual bottom level stats of each wave should be in response.

This is a lot like the method described in Kyle Pittman's famous "Building a Better Jump" GDC talk, but applied to tower defense wave design instead of jumping in platformers:

We start by establishing a baseline for where we want the level to start and where we want it to end, in terms of variables that the designer actively thinks about:

This lets us encode our assumption as designers about what kind of resources should be available in this level and what we are expecting the player to do.

Note the "stress" and "wasted_rate" variables in particular. These are our primary tuning mechanisms. "Stress" is an emergent phenomenon we care about as designers that isn't implemented as a single variable in the actual game, but instead results from the interplay of how many enemies you're facing, how strong they are, how densely packed are they, and how powerful you are in relation to them (just for starters).

We then establish some more assumptions about the topology of the map, and how many defenders we expect to be available to cover various lanes:

Of course the player can subvert these expectations and play in a way totally different than we expected. That's not a bad thing, in fact it's a good thing. But it serves to have at least one intentional, well-defined view of what the level is trying to do, and give ourselves an easy way to make sure we're providing the right balance of enemies and resources to both challenge and reward them. The above metadata encodes those intentions and the script will make sure the level balances around them.

Now we've defined a few things:

  • Starting conditions, especially:
    • Stress level
    • Starting juice
  • Ending conditions, especially:
    • Stress level
    • Margin for error
  • How the player is expected to divide their forces

This lets us establish a baseline curve that we can build the rest of the level's challenge and reward progression around. Next we work out intentions for the individual waves. We still define waves in terms of what kind of enemy they are, where they spawn from, and how fast they spawn, but we omit the number and level of the enemies.

Instead, we specify a "stress" rate on each wave. If we don't define it, the stress rate for that wave is 1.0, or equal to the baseline curve. If it's higher than 1.0, then the difficulty you are experiencing relative to your expected power level at that point in the battle will be higher than normal. The wavegen tool then figures out programmatically what the amount and level of enemies in the wave should be based on the baseline established by the default curves.

Our designer's done a bunch of other things under the hood too, like redesign the parasite and machine classes, and fix some fairly embarrassing numerical bugs under the hood and generally tighten up the whole ship. As an actual deliverable, we've filled out levels 16-22.

Also, I've taken a bit of an axe to the "towns" in the game because we're moving more to having an always-available onboard "shop" that you can access anywhere, and which just gets new items in it over time. We'll still leave some blue pearls on the map to represent non-combat areas where story events will happen, however.

Finally, I've added a new attack type – 'artillery' attacks, though there's no unit that uses it yet. This is basically a variant on an unguided projectile and functions like a big cannonball. The defender takes a bead on an enemy, and then fires at the tile they were standing on. The projectile visually arcs through the air (much like knocked back enemies), and then when it lands it does only splash damage (since it has no main enemy it is targeting). Also, it can optionally leave a "patch" on the tile it landed on, such as a patch of tar (which slows down all enemies that try to pass over it). I coded this up at the request of our level designer to give him more tools to play with.

Another thing I'm considering is better telegraphing of what kinds of enemies will be coming from what waves, and what paths they will take. Many other tower defense games do this, and I feel the need for it when I can't tell right away whether a bunch of attackers will be coming from the left or the right side of the screen, and I don't want to have to play the whole level just to find out.

A final note that the new levels are currently a bit on the hard side and need a final balance pass, but they're far more interesting and though-provoking than what I had put into the game so far, and I'm liking the trajectory we're on. A few more months of this and we should have a solid catalog of DQ2 levels to start painting up, but more on that in the next month's report.

And... that's it! See you next month :)