Sunday, 13 January 2008

Unangband Dungeon Generation - Part Seven (Game-Spaces)

(You probably want to read parts one, two, three, four, five and six before starting this).

At the conclusion to part six, I touched briefly on the importance of taking a data-centric approach to game design in Unangband, and cryptically hinted at 'merging possible game spaces' as a means of improving the overall game design. And by merging the possible game spaces, I mean minimising the number of disjoint game elements: that is, the number of different components that make up the game play. A concrete example will help immensely, before I degenerate into a series of circular definitions.

In Angband, there are two ways that a player can be affected by monster attacks. Each monster blow has an effect, which for historical reasons starts with RBE_. Examples would be RBE_HURT, RBE_POISON, RBE_CONFUSE and so on. The monster blow effects dictate what type of damage the player suffers, what items protect the player from this damage and so on.

But there's also monster spells, which have their own separate set of effects, which for historical reasons start with GF_. Examples would be GF_FIRE, GF_POISON, GF_CONFUSE and so on. The spell effects dictate what type of damage the player suffers, what items protect the player from this damage and so on.

When implementing the feature code, I wanted to add the ability to define trap damage in the terrain.txt file. I could have equally created a new set of trap effects, which for historical reasons would start with AD_. Examples would be AD_HURT, AD_POISON, AD_CONFUSE and so on.

Or I could have adopted either the monster blow code, or the monster spell code to implement my trap effects. This would have allowed me to re-use the code that already existed for these two types in many instances, but I would have had to compromise slightly for game balance sake, as the vision I had for e.g. confusion traps, may not have matched either the monster blow or the monster spell code exactly. Unfortunately, there was no clear 'winner' out of the two existing implementations.

What I instead did, was merge the RBE_ and GF_ types together, so that I only had a single choice to make, and then adopted the merged effects code for the trap implementation. This meant not only could I refactor some existing code away, but that I could have traps that caused the player to hallucinate (a monster blow effect) or be hit by gravity (a monster spell effect).

This is the concrete example of what I mean by 'merging the game space'. I took two disparate game elements (monster blows and monster spells) and combined them to take advantage of synergies of scale. I was then able to apply these to traps, other terrain, and ultimately as it turns out, to coating weapons, casting spells, implementing object effects and many other areas that did not feature in the original implementation.

The above example may seem fairly self-evident to you. But consider how you'd implement traps. I discussed this briefly in the previous part of this article. As I mentioned, several variants have implemented traps using a separate data structure, instead of as a feature (or object).

But this means that they have to extend e.g. the monster path finding code to consider separate cases for moving through a fire trap vs a fiery terrain grid. By making traps a feature, I got this functionality for free. Similarly, the monster trap disarming code uses the majority of the same code path as the monster door opening code.

In general, you should extend your existing data structures where ever possible, instead of creating additional ones. I suspect that the vast majority of games would be satisfied with five core data structures: player, enemies, playing field, pick-ups and effects. You may even be able to collapse e.g. player and enemy together.

I would argue against further reduction of data types, because you end up with an object oriented design where everything gets reduced to an 'entity'. This level of abstraction means you'll end up worrying more about the data type design than actual implementation. It seems to crop up quite frequently as a question on and I've yet to see any concrete benefits in doing so.

These five data types then have a minimum of ten possible interactions between them as shown below. Actually, there's an implicit additional set of 5 interactions, because each data type could potentially interact with itself.

An interaction is not necessarily just between one instance of each type: it could equally be a 'has-a' relationship of some kind. For instance, in across the Angband variants where the enemy is a monster race, a player 'has-a' an implicit monster race consistenting of the shape the player currently is, as well as being able to ride a monster, and in Unangband, being escorted by particular monster types on battle field dungeons.

Generally in programming, you want to minimise the different total interactions, as these usually require a separate set of code paths (and test cases). But you want the interactions to be as expressive as possible, by maximising the number of permutations of these interactions.

In Unangband, there are a large number of effects, such as fire, water, acid, plasma and so on. Effects are associated with all of the above data types: players and monsters can create effects using spells, the playing field hits locations with effects and pickups (objects) create an effect by exploding, coating another item, and being used by the player.

So it's possible to use water to douse a fire, wash a bloody altar clean or create a wave. A water source could be the waves washing around a water elemental, a potion of fresh or salt water, a player casting a wall of water spell or water flooding from an adjacent grid location. This immediately creates a large number of possible interactions (the player could summon a water elemental to clean a bloody altar, or throw a potion of water at it, or cast a spell, or flood an adjacent location etc) for any particular situation, creating a large number of action choices and rewards the player for lateral thinking.

By merging the possible game-spaces, I've not had to write nearly as much code: instead of every possible permutation of water source and water effect, I've abstracted away as much as possible to the higher level 'playing field' and 'effect' interactions, and handled the majority of the work with data as opposed to code.

In part eight, I attempt some kind of summing up (Actually, it turns out I just continue, looking forward to summing up at in a later part).

1 comment:

takkaria said...

I think the RBE_ constant name is "Race Blow Effect".

I'm enjoying the series, BTW. :)