Complete Roguelike Tutorial, using python+libtcod, part 6
This is part of a series of tutorials; the main page can be found here. |
Going Berserk!
The Fighter component
Finally, it's time to bash some orc helmets into little metal pancakes! Combat is a big turning point in the development of any game -- it separates the game from the tech demo. Although dungeon exploration is interesting, the simplest fight can be far more fun, and you may even find yourself playing your game more than you code it!
We have a big design decision ahead of us. Until now, the Object class was enough to hold the properties of both the player and the enemies. But as we develop further, many properties will make sense for one kind of Object, but not for another. How do we solve this?
We could just pretend this is not a problem. It's called the data-driven approach and is used by many Roguelikes, especially the oldest ones. It has the advantages of playing nicely with the rigid data structures of C (not our case!), and the properties of all objects can be edited in a big table or spreadsheet. But you either have to limit the number of properties, which can result in all objects seeming like minor variations of the same things, or have a truly huge number of (mostly redundant) properties per object.
The other popular alternative is inheritance. You define a hierarchy of parent (a.k.a. base) and child (a.k.a. derived) classes, and child classes (like Item or Monster), in addition to their own properties, receive the properties from their parent classes (like Object). This reduces redundancy, and there's a seemingly clean separation between different classes.
However, the separation is not that clean, since the properties of parent classes are "pasted" on the same space as the child's properties; their properties can conflict if they share names. And there's the temptation to define deep hierarchies of classes. As you develop further, your hierarchy will grow to extreme lengths (such as Object > Item > Equipment > Weapon > Melee weapon > Blunt weapon > Mace > Legendary mace of Deep Deep Hierarchies). Each level can add just a tiny bit of functionality over the last one.
The fact that a Mace can't be both a Weapon and a MagicItem due to the rigid hierarchy is a bummer. Shuffling classes and code around to achieve these simple tasks is common with inheritance. We wanna mix and match freely! Hence, inheritance's older, but often forgotten, cousin: composition. It has none of the disadvantages listed above.
It's dead simple: there's the Object class, and some component classes. A component class defines extra properties and methods for an Object that needs them. Then you just slap an instance of the component class as a property of the Object; it now "owns" the component. It doesn't require special functions or code! Let's see how it works.
Our first component will be the Fighter. Any object that can fight or be attacked must have it. It holds hit points, maximum hit points (for healing), defense and attack power.
class Fighter:
#combat-related properties and methods (monster, player, NPC).
def __init__(self, hp, defense, power):
self.max_hp = hp
self.hp = hp
self.defense = defense
self.power = power
It'll later be augmented with methods to attack and take damage. Then there's the BasicMonster component, which holds AI routines. You can create other AI components (say, for ranged combat) and use them for some monsters. We'll define a take_turn method; as long as a component defines this method, it's a valid alternative to BasicMonster. For now it just prints a debug message!
class BasicMonster:
#AI for a basic monster.
def take_turn(self):
print 'The ' + self.owner.name + ' growls!'
Don't mind the reference to self.owner -- it's just the Object instance that owns this component, and is initialized elsewhere. We'll get to that in a moment. Ok, so how do we associate components with an Object? It's simple: create a Fighter instance, and/or a BasicMonster instance, and pass them as parameters when initializing the Object:
def __init__(self, x, y, char, name, color, blocks=False, fighter=None, ai=None):
Notice that all components are optional; they can be None if you don't want them. Then they're stored as properties of the object, for example with self.fighter = fighter. Also, since a component will often want to deal with its owner Object, it has to "know" who it is (for example, to get its position, or its name -- as you noticed earlier, BasicMonster 's take_turn method needs to know the object's name to display a proper message). In addition to holding the component, the Object will set the component's owner property to itself. The if lines make sure this happens only if the component is actually defined.
self.fighter = fighter
if self.fighter: #let the fighter component know who owns it
self.fighter.owner = self
self.ai = ai
if self.ai: #let the AI component know who owns it
self.ai.owner = self
This may look a bit weird, but now we can follow these properties around to go from a component (self), to its owner object (self.owner), to a different one of its components (self.owner.ai), allowing us to do all sorts of funky stuff! Most other systems don't have this kind of flexibility for free. Actually, this is the most complicated code that composition needs; the rest will be pure game logic!
The whole code is available here.