2011
09.09

Items class hierarchies

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.

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. šŸ˜€

No Comment.

Add Your Comment