While most roguelikes include basic attack and defense mechanics as a core player activity, the real challenges are introduced when gameplay moves beyond bump-combat and sees the player juggling a more limited amount of unique resources in the form of special abilities, magic, consumables, and other effect-producing items.
Just as they challenge the player, however, the architecture behind these systems often imposes greater challenges on the developer. How do you create a system able to serve up a wide variety of interesting situations for the player without it turning into an unmaintainable, unexpandable mess on the inside?
It's a common question among newer developers, and there are as many answers as there are roguelikes, worth talking about here because it's fundamental to creating those interesting interactions that make roguelikes so fun.
An increasing number of roguelikes are based on flexible entity component systems, though being born of a combination between my old 7DRL and an even older project built before I even knew what ECS was, Cogmind's abilities are each handled by one of two unrelated implementations: hard-coded routines and trigger-condition-effect scripts.
The first system is dead simple, the kind of thing that you get when starting a game as a 7DRL :P. But, as a result it's also the kind of thing unlikely to cause problems down the line as long as we don't ask too much of it. (That was also the point of starting with it, to make sure nothing would go wrong on the technical side of things in the seven days available to create the game.)
In short, there are a set number of hard-coded item abilities, and an item can choose at most a single type of ability, and also assign a single integer to its variable which usually serves to define the degree of that effect. There are 95 such abilities in all:
Hard-coded effect descriptions, as seen in source, where "effectData" is the integer referred to above. I removed a couple dozen abilities due to huge spoilers! (I rarely hard-code text like this, but here it was easier to build some strings that require formulas, e.g. many of those not shown.)
If you look closely you'll see that some are essentially identical to others, differing only by a single factor and thus not really providing additional unique behavior. That reveals one of the limitations of this kind of system: it doesn't well support slight variations on the same behavior without extra code, something that a more complex system could do quite easily.
But once an effect is coded, it is extremely easy to assign it to an item, making ability-based items quick to design and compare.
List of sample items with their effects (other stats cropped).
So these abilities/effects are each checked for wherever they're relevant in the game code, be it when an actor is attacked, attacks another actor, scans an enemy, is hit by a thermal weapon, is caught in stasis, or any number of other things happen. Some are simply direct modifiers to a stat, affecting it only while the given item is active (this was another 7DRL decision--values are always referenced by the function that calculates on the fly all factors which can affect them, rather than allowing changes to modify a value which is stored and referenced, since those changes may need to be undone later and that's... really annoying and complicated).
In terms of behavior it has maximum flexibility since we can code whatever we want :). Examples:
Applying thermal absorption effect.
So far I've only mentioned items, but there are a smaller number of non-item abilities handled in a similar manner. Instead of ability types they are sourced from what are called "traits," of which an object can have as many as necessary (but realistically most objects have none, some have one, and a very few have multiple traits).
Traits originally existed as part of the more involved second system I'll be describing below, but in some cases it was convenient to borrow them in circumventing the old one-effect-per-item rule instated for the 7DRL, and also giving hard-coded abilities to actors and terrain (to which item abilities cannot be applied, but traits can). Despite the name, these aren't always passive characteristics, and in terms of implementation they're not really any different from item abilities (defined by way of a type-and-variable pair), so I won't go into them here.
In addition to being simple to implement, the straightforward nature of this approach somewhat carries over to the player side as well--each ability-capable item can generally be expected to provide one unique benefit, a benefit that only differs from similar items by a single variable, making them easier to analyze and compare (fairly important in a game where you can equip up to 26 items at once :P)
Comparing the Improved and Advanced versions of the Weapon Cycler. Technically opening
the info window isn't even necessary to do the comparison, since their respective effect values
are also displayed in the parts list's info mode right on the HUD.
Scriptable Ability Objects
The other system is much more powerful, and while it's still rooted in hard-coded effects, once the necessary code is in place it can be used as the basis for a much wider variety of possibilities.
This system was actually inherited from X@COM, where it enabled me to turn the X-COM world and ruleset into a fantasy game complete with classes, unique special abilities, dozens of spells and special items, etc, all in a few weeks. And that was purely via scripting--no code at all! (Around that time, other non-coder types were also able to use it to create interesting behaviors for their own mods.)
So with that as a background, let's look at the underlying system that makes it possible...
"Abilities," or really anything that might affect something else, are a type of (C++) object that can be created and assigned to one of the game's primary object types (Entity, Item, Prop). The assignment occurs manually in external data files, and, more importantly for a dynamic system, at runtime by other abilities as the game is played.
Abilities themselves are defined in their own data file, by listing the trigger, conditions, and effects of each. Those individual components are all defined in source code, and work as follows:
An ability first specifies what causes it to take effect. Only one such "trigger" is allowed per ability, and at relevant points in the code the engine explicitly checks for applicable triggers. Example triggers include being hit by a projectile, moving next to a prop, a robot exploding, seeing another robot... dozens of them.
A single ability may have any number of effects, or multiple effects from which only one or some are chosen (the list can be weighted if desired). Example effects include dialogue, explosions, modify AI behavior, spawn an object, convert one object into another... again dozens of available possibilities.
The key to making the whole system dynamic is conditional application of triggers and effects. Those which only happen under certain conditions allow for much more interesting possibilities, as well as more complex relationships and behaviors. Thus both triggers and effects may be controlled by any number of conditions. Examples include a random roll, robot stat values, distance from player, what faction a robot belongs to, how long the ability itself has been in existence... (once again, many dozens :P).
Multiple conditions can be combined on the same element with the syntax A|B|C, and there is even syntax for more complex conditionals, like "A|[B|C]" is "A and either B or C". Effects with conditions can also use the "contingency" system, so that a certain effect might only take effect if an earlier effect from the same ability did not occur for whatever reason (one of its conditions failed), or all previous effects failed, or all succeeded, or basically whatever :)
To demonstrate all the primary components working together, I'll dissect a single "ability," in this case really an event, that can occur early in the game when you visit the mines:
This particular ability is assigned to an invisible prop used to facilitate location-based events. PROP_INTERVAL is a trigger checked once every turn for any prop that has one, but in this case there are conditions: it won't be triggered unless P_PLAYER_RANGE is within 10 cells, and its position is also visible to the player (the "(1)", as opposed to (0)). When the player approaches and successfully triggers it, as per the SPAWN_ENT effect it will create a new entity (robot), but only if the effect's conditions also pass, which in this case requires that the player not already have a Mining Laser in their possession. This effect, like many others, must specify a number of additional details describing the result, here the NUMBER and type of ENTITY to spawn, its FACTION, and optional values that tell it to FOLLOW the PLAYER and assign it another ability to define future behavior (Min_Helper_Helps1). When this entity appears it also says a line of DIALOG.
The same script in action.
All components (triggers/conditions/effects) might have various data parameters that help specify more about its behavior, such as AI details for spawning actors, the shape of an effect area, how to treat various special cases, and more. There are also generic data values applicable to most all components, allowing any of them to create log messages, particle effects, etc.
On that note, the system is fully hooked into the code-external sound effect and particle systems, so composite content can be created without touching the code at all.
It's also tied into a system of global world state variables, to control the plot and other situations stemming from that--useful for conditions! For example, there is one NPC encounter that only occurs if you destroy a certain group of robots, but you have to get them all, so each one destroyed adds to the global counter, and later when you reach the NPC's location one of its SPAWN_ENT conditions is that counter which must have reached the required number.
Effects may also mark objects with traits (mentioned earlier) that appear in conditional expressions, allowing even more complex relationships that can evolve over time. This application of traits would be more appropriately named "markers" given how they're most commonly used. For example, there are a few special places in the world where an entire section of wall opens up when the player simply approaches. When created, as per its prefab definition each of those positions is marked with a specific trait. Nearby on the ground is an invisible "ability" object that triggers when the player passes by, and the effect is to send out an invisible explosion which checks for that trait on any walls that it hits, which are then "opened" using a particle effect while the original walls themselves are destroyed/converted into floor. Hm, can't demo any of these here because they're all secret :P
Even more interesting are abilities that actuaflly assign abilities to other objects, like the Molecular Deconstructor, but details on that bad boy are classified.
If absolutely necessary, for extra special cases the ability system can also hook into unique hard-coded behavior via the SPECIAL effect. When developing the original system for X@COM, I avoided relying on this one since the goal was to allow ability scripts to be able to define just about anything the game was capable of without access to the source (to empower modders), but with Cogmind the more important goal has been to finish the game, while some of the special events have so many wide-reaching consequences that it was deemed too much effort to make the necessary abstractions to further expand the system. Better to hard code and move on. Still, only a tiny percentage of effects are hard-coded, averaging about one per map (compared to hundreds of text-scripted effects associated with objects).
At heart it's really a pretty simple system, but you can imagine the number of possible combinations here is massive, even with only a modest number of components of any one type. So with the right hooks into the code, and a script parser to make sense of a list of triggers, conditions, and effects, this can be used to create some cool stuff :D
A shorter version of this post previously appeared on /r/roguelikedev under the FAQ Friday by the same title, where you can also see how other roguelike developers approach this same topic.