2010
05.28

The cave has just went through a major refactoring of the item system. Items are the core of any roguelike and any hack&slash, so you’d better think twice before starting to code their foundation classes. This is the third item system I code and I hope it will last longer than the previous ones.

µstep method (tm)

Fortunately I’ve used advanced µstep method (tm) for this refactoring and it went perfectly smoothly. It took only two evenings and the game never stopped compiling  or even running. So far, there is no visible bug. What I call µstep method is the process of moving the code by tiny steps, having everything compiling and running between every step. Steps should not be bigger than a coding session. For this refactoring, I’ve tried the extreme way and used steps of a few dozens of lines. Of course, you go through some very ugly stage where some items use the new system while the others keep using the old one, but you can stop the refactoring at any moment and work on something else. The game runs, it’s still playable, even releasable even if a nuclear blast is occurring in its core classes. You could believe that it takes longer to do all those steps than blowing everything and rewriting directly some clean new code. Well, my experience showed me that in that case, you may have the first version of the new system sooner, but debugging it will be a real pain and in the end, you’ll have spent more time than with the µstep method.

Item systems

The Chronicles Of Doryen’s system

So what about those three systems ? The first one is from the old “The Chronicles Of Doryen” codebase. I was pretty ambitious then and put all the levers to the max. Of course there is a generic Item class. Each item is associated to an ItemType that describes the item behavior. Item types are organised as a tree, the “root” item type being the, well… root of the tree. Then you have a first level of abstract types of very high level : static, material, general, shields, armors, weapons. Each of these abstract types contains several levels of abstract types before you reach the leafs, actual items. For example, under general, you have food, lights, oils. Food contains ingredient, potions. Weapon contains blades, hammers, staffs, axes. Blades contains daggers, short swords, single handed long blades, two handed long blades.

Organizing the item types in such a hierarchy has several advantages :

  • the game code can easily check if an item belongs to a category. If you want to check if an item is some food, you can simply call item->isA(ItemType::food) instead of having a long list of tests (item->type == carot || item->type == tomato …).
  • abstract types (they could be called ‘categories’) are used to filter the inventory. With this system, you can easily implement an inventory that looks like Windows Explorer, with a hierarchy of directories (abstract types) and files (actual items). In TCOD, the item types config file allows me to define which abstract type is shown in the inventory. I have a few hardcoded filters (all, food, light, armor, shield weapon), but inside a filter, items are still organized as a tree as seen on this screenshot :
  • in the config file, abstract types are not only containers. You can define properties on them that will apply to all sub-types. Maintaining the (huge) config file is easier and there is no duplicated data. Example : a part of the armor configuration :
// ********** ARMORS **********
ItemType "armors" {
abstract
inventory
ascii='['
feature "wear" {}
feature "armor" { when "is_worn" {} }
 ItemType "light armor" {
  abstract
  inventory
  ItemType "leather armor" {
  abstract
    durability=4
    feature "armor" { bonus=10 }
    ItemType "leather leggings" { feature "wear" {body_part="legs"} cost=15 }
    ItemType "leather gloves" { feature "wear" {body_part="hands"} cost=10 }
    ItemType "a leather cuirass" { feature "wear" {body_part="torso"} cost=25 }
    ItemType "a leather helmet" { feature "wear" {body_part="head"} cost=15 }
    ItemType "a leather left bracer" { feature "wear" {body_part="left wrist"} cost=5 }
    ItemType "a leather right bracer" { feature "wear" {body_part="right wrist"} cost=5 }
    ItemType "leather boots" { feature "wear" {body_part="feet"} cost=10 }
  }
}
}

As you can see, the actual item types only use one line because most of the properties are defined on higher level abstract types.

There are no hardcoded item types (like Weapon, Food and so on). ItemType is also a generic class. Distinct behavior in the game come from another class : item features. The item type defines some standard properties of the item : what character is used to represent it, is it stackable and so on. But the most important properties are in the features. Exemple of features are whether the item can be worn or wielded, eaten, can it be de/activated (like a torch), does it deal damages when used, or does it protect it’s owner. Each item type contains a list of features and the game checks for known features to implement the correct behaviour.

You can see on the config sample above that armors have two features : “armor” and “wear”. Armor means that the owner’s armor bonus is increased. “wear” means that it can be worn by the player.

Now TCOD doesn’t stop there with a list of hardcoded features like Armor, Wear, Edible, … No, no. Ambitious I said… Features are also generic! Here is the third level of abstraction. The Feature class defines a generic feature with an id, a name and a list of parameters. That means I can add a new feature in the config file, let’s say “BlowIfHit” with some parameters (blowRadius, blowColor) and without changing a line of code (except the config file parser), the feature is available in the game. Of course you have to add some code to actually implement this feature (if item->hasFeature(“BlowIfHit”) then trigger explosion), but you don’t need to code the BlowIfHit class.

Now generic Feature is pretty complex to implements, because a feature has static parameters (defined on the item type) and dynamic parameters (defined on the item because they change over time). For example, a torch light radius is defined on the torch item type, but it has also to be stored on the torch item because it will decrease over time until the torch is burnt away.

Features also have conditions. For example an armor has “armor” and “wear” features, but both features are linked with a condition (when “is_worn”). That means the armor bonus is only active if the item is worn.

This whole stack of generic classes really works pretty well, but that’s a lot of code with high level of abstraction. Leave it a few months and you don’t understand it anymore. Read it for the first time and you’re completely lost.

Pyromancer ! system

Pyromancer ! is a 7DRL so I took the exact opposite direction : straightforward approach with everything hardcoded. A class for each item. For small size projects, you can’t make simpler. Of course, you probably still need an Item base class to avoid duplicating all the code.

The cave’s items

The cave is based on pyromancer! source code, so it started with the same design. But it reached the size where this design cannot stand the requirements anymore, hence the refactoring. I didn’t reimplement the whole TCOD stuff though. I still use Item, ItemType and ItemFeature classes, but now features are hardcoded, with a class for each feature. I also dropped the item type hierarchy is favor of something simpler and more powerful : tags.

The main inconvient with TCOD’s hierarchy is that an item belongs only to one branch of the tree. For example, an item can be an ingredient (food branch) or a material, but not both. Tags are more versatile :

  • You can apply any number of tags to each non abstract item type.
  • Tags are used in the config file like preprocessor macros in C. All of the tag’s properties are copied into any item type containing the tag.
  • Tags are not necessarily linked to each other. Getting the properties of a blade does not mean that you have to get the properties of a weapon.
  • But you can still organize tags in hierarchy. In that case, you get the same behavior as TCOD’s type hierarchy.
  • In the game engine, tags are reduced to a bitfield used to classify the items with a TCOD-like isA() function, but you can now have overlapping hierarchies instead of a single one.

With tags, TCOD armor config file would look like this :

// ********** ARMORS **********

ItemTag "armor" {

    inventory

    ascii='['

    feature "wear" {}

    feature "armor" { when "is_worn" {} }

	ItemTag "light armor" {

        inventory

		ItemTag "leather armor" {

		    durability=4

		    feature "armor" { bonus=10 }
		}
	}
}

ItemType "leather leggings" { tags=["leather armor"] feature "wear" {body_part="legs"} cost=15 }
Or like that :
// ********** ARMORS **********

ItemTag "armor" {

    inventory

    ascii='['

    feature "wear" {}

    feature "armor" { when "is_worn" {} }
}

ItemTag "light armor" {

      inventory
}

ItemTag "leather armor" {

    durability=4

    feature "armor" { bonus=10 }
}

ItemType "leather leggings" { tags=["armor","light armor","leather armor"] feature "wear" {body_part="legs"} cost=15 }
I find this system more powerful, and it has a simpler data structure.

If you’re still reading, you earn a free phoenix flying mount usable in the first release of TCOD, to be released Q3 2046. 😀

5 comments so far

Add Your Comment
  1. Woo, when do I collect the mount? 😀

  2. Very nice. I was thinking that tree structure limits the game. Personally I “when i release my roguelike” will use “complex item system”:
    archetypes witch require features (shovel, sharp, pointy) they will give actions (dig, slash, poke) and actions efficiency will be determined by how good item matches archetype. E.g shovel with long handle will be better for digging then with short, but because of short weapon archetype short handled shovel will be better at close combat. And all that will depend on item part quality, materials and etc…
    P.s. I will remember to ask for my pheonix mount! 😉

  3. Nice explanations and diagrams there. I may be the only one, but I found that quite enlightening. I haven’t yet coded any sort of item system for my current roguelike project Roam, but this may help when I do. More lengthy blog posts please (and that is _not_ sarcasm)!

    As for refactoring: I used to work on the Tstep method: refactor absolutely everything in one go (spread across a number of coding sessions). Then the project would completely fail to compile or work, and I’d ditch the whole idea in favour of a different project. Then I moved on to the Gstep method, where I’d still refactor everything in one go, but I would keep a backup of before I started, so that when the whole thing broke, I’d just be able to go back to before and start again. I’ve been progressing towards something like µstep, but it’s good to have the concept detailed like this.

    Oh, and thanks for the offer of a free phoenix flying mount. I assume that this 2046 release of TCOD will be compatible with “Windows 135 for Workgroups, 128kbit edition”, and that it will use the USB v20.0 Telepathy Headset from Logitech. Actually, someone should set out the libtcod framework for that input module soon.

  4. Yeah, you better start thinking about 3D rendering because in a few years graphics cards won’t support anything less than that! 😛

    I also enjoy this sort of post immensely! If you find the time to write more of them, know that they’re very appreciated 🙂 My items system is not too different from The Cave’s, as the characteristics of each item are described by its collection of components, defined in a config file, but the components themselves are all hard-coded. (They’re way too low-level to be configurable.) Some of them have lots of properties and behaviors, but others are completely empty and are only meant to be detected by code elsewhere, to “classify” this item (like your tags). I do like the idea of tags serving as prototypes, the item definitions I have can become quite lengthy 🙂

  5. This is the system I was thinking of using, a giant tree of abstract subclasses with the only the leaves actually being implemented, so glad I’m on the right track before I start it:P 🙂

    Btw, have you used this same method with your tiles/terrain? that’s one thing I have implemented, though it’s easier than items.