I mentioned in the first post that I'd rewritten most of the procedural level generation from
Eldritch, and since I just spent the morning changing it even further, I thought it might be interesting to talk about why and how that happened.
For
Eldritch's Lovecraftian worlds, messy and chaotic levels were somewhat desirable. Or frankly, I used the theme as a crutch to avoid solving some hard problems. Brick walls would join to caverns or ancient ruins with little regard for spatial or functional coherency. (If you're curious, you can watch me nervously talk about level generation in
Eldritch here:
http://www.gdcvault.com/play/1022110/Level-Design-in-a-Day.)
In
Slayer Shock, I'm trying to model more familiar spaces. For example, here's an early WIP shot of the suburbs level:
It's not the most complicated thing, but it's coherent: streets intersect in a reasonable way, and houses are oriented to face out onto an adjacent street.
Levels in both games are built from axis-aligned tiles, where each edge of a tile may either be a terminating edge (i.e., a wall) or a connection to another tile (a portal).
The first step toward more coherent levels was to tag the portals with some metadata that defines not only that they *can* connect, but *how* they connect.
For example, this 3-way junction has purple walls on three sides, indicating that the tile must connect to a street on that edge. On the fourth side, the yellow wall marks a curb that should connect to a housing lot.
Another important addition to the level generator was prioritization of tiles and portals. Unconstrained, the generator could theoretically place a street, then a house beside that street, then another parallel street on the opposite side of the house, and so on in that fashion, producing a series of short streets that never intersected:
I'm good at drawing. So anyway, that sort of street plan is obviously silly and undesirable.
My solution was to introduce a priority system that would let me specify certain tiles or portals to always take precedence if expansion through them is available. In this case, I make the generator build a network of interconnected streets first, by prioritizing the street pieces before the housing lots; then after the street expansion has run as far as it is allowed, the remaining holes in the level are filled in with houses wherever they fit. Finally, a high wall is placed beyond the lots to enclose the whole space.
An unfortunate side effect of this approach is that it is possible for the generator to produce invalid worlds. After laying out the street network, it could happen that there are no house tiles that fit the holes in the map. In that case, the generator throws up its hands, discards the level completely, and starts over. It's an acceptable solution, but not ideal. In the worst case, the game will spin at the loading screen for 2-3 seconds while the generator churns through hundreds of abortive levels to find one that works.
I've given some thought to better ways of handling failure cases, but nothing has stuck yet. The idea I keep coming back to is to unwind the last steps of generation and try something else, effectively depth-first-searching the state space in random order. It's a neat idea that might have other value, but it would also be a heavy change to the generator and could actually perform much worse than a discard/restart in the worst cases.
* * *
Edit:
I forgot to mention it, but another *huge* improvement to the level editor and generator is that tiles can be any size now. That 3-way junction above is what I'd call a 1x1 tile, the smallest piece that fits on the level's footprint grid. But tiles can be any multiple of that footprint. For example, this house piece is a 1x2 tile, twice as long as it is wide:
By comparison, in
Eldritch, large features had to be painstakingly assembled from a bunch of 1x1 tiles. It was a mess.