I’ve come up with some new design decisions in my rework of the engine:
- Everything inside a level is an entity
- Logic is reactionary
- Mutability is not a complete evil so long as it is contained
Entities are this concept that things are better through composition. Now this is not a new idea, object oriented programmers recognize the idea of preferring composition over inheritance.
So in the beginning was inheritance
class DungeonMonster { public void Move(Direction direction)... public void Attack(Direction direction)... } class Goblin: DungeonMonster { ... } |
This seems pretty straightforward, monsters can move and attack. But there’s only one monster, the goblin.
Now let’s add some unique monsters, like some evil sentient plant. Not too different from those trees in the Wizard of Oz that throw apples at Dorothy.
class EvilAppleThrowingTree: DungeonMonster { ... } |
Now we’re in trouble: not only are there evil trees, but they don’t work well with the base class DungeonMonster because trees can’t move. So, next step is to rework those classes:
class DungeonMonster { public virtual void Move(Direction direction)... //Default is now able to move. public void Attack(Direction direction)... } class Goblin: DungeonMonster { //Don't override Move. } class EvilAppleThrowingTree: DungeonMonster { public override void Move(Direction direction)... //Does not allow move } |
Boom! That wasn’t so bad. We can just override whatever behavior whenever we want.
Let’s add a few more monsters to make this game really unique:
class PoisonIvy: DungeonMonster { public override void Move(Direction direction)... //Does not allow move } class Mouse: DungeonMonster { //Shouldn't be able to attack. Just moves around and stuff. } |
Now this is getting to be a problem. PoisonIvy also can’t move. So I have the option of either copy-pasting code from EvilAppleThrowingTree’s Move function into PoisonIvy’s Move function which is stupid, or I could add another layer to the heirarchy.
class ImmobileDungeonMonster: DungeonMonster { public override void Move(Direction direction)... //unable to move. } class PoisonIvy: ImmobileDungeonMonster { } class EvilAppleThrowingTree: ImmobileDungeonMonster { } |
But wait, didn’t I add a mouse too?
class Mouse: DungeonMonster { //Shouldn't be able to attack. Just moves around and stuff. } |
I guess the Attack function needs the same reworking that the Move function did. This is all the code so far
class DungeonMonster { public virtual void Move(Direction direction)... //Default is now able to move. public virtual void Attack(Direction direction)... } class ImmobileDungeonMonster: DungeonMonster { public override void Move(Direction direction)... //unable to move. } class Goblin: DungeonMonster { //Don't override Move. //Don't override Attack. } class Mouse: DungeonMonster { public override void Attack(Direction direction) ... //Shouldn't be able to attack. } class PoisonIvy: ImmobileDungeonMonster { } class EvilAppleThrowingTree: ImmobileDungeonMonster { } |
I could keep going on and adding more monsters, but I’m trying to make the point that a dungeon monster can have many attributes. And a multitude of monsters means these attributes will be combined in many different ways. For example: I might just add a LightMold. A LightMold is a sentient fungus that cannot move, but it can cast spells.
class LightMold: ImmobileDungeonMonster { public override void Attack(Direction direction) ... //Shouldn't be able to attack. } |
We also need to add the spell-casting attribute to DungeonMonster.
class DungeonMonster { public virtual void Move(Direction direction)... //Default is now able to move. public virtual void Attack(Direction direction)... public virtual void CastSpell(Spell spell)... //Most monsters can't cast spells. So virtual } |
Guess what? An egregious sin has happened for now I have to go back and override all the other monster’s CastSpell function to do nothing. I could have the base class DungeonMonster do nothing with spells, but then the problem of copy pasting spell casting logic to all classes that can cast spells appears (we haven’t even added evil wizards yet). Not to mention the fact that I just copy pasted the Mouse’s Attack function override.
In short, I’ve made a mess of everything.
Let’s rework this using composition. A dungeon monster is made up of behaviors. A behavior is how something acts.
interface IMove { void Behave(Direction direction) } interface IAttack { void Behave(Direction direction) } class DungeonMonster { public IMove MoveBehavior {get; set; } public IAttack AttackBehavior {get; set; } public void Move(Direction direction) { MoveBehavior.Behave(direction); } public void Attack(Direction direction) { AttackBehavior.Behave(direction); } } |
I don’t care what those behaviors do so long as they behave. They’re just an interface.
class Mobile: IMove { public void Behave(Direction direction) ... //Move } class Immobile: IMove { public void Behave(Direction direction) ... //Don't Move } class Defensive: IAttack { public void Behave(Direction direction) ... //Attack } class Defenseless: IAttack { public void Behave(Direction direction) ... //Don't Attack } |
I have any kind of monster I could ever wish for now by combining these behaviors
class Goblin: DungeonMonster { public Goblin() { MoveBehavior = new Mobile(); AttackBehavior = new Defensive(); } } |
Adding an attribute to DungeonMonster got a lot easier too. No more complex inheritance or copy-pasting logic. All logic is sitting in those behaviors. Combine the behaviors to make the logic how you wish.
interface ICastSpell { void Behave(Spell spell); } class DungeonMonster { public IMove MoveBehavior {get; set; } public IAttack AttackBehavior {get; set; } public ICastSpell SpellBehavior {get; set } public void Move(Direction direction) { MoveBehavior.Behave(direction); } public void Attack(Direction direction) { AttackBehavior.Behave(direction); } public void CastSpell(Spell spell) { SpellBehavior.Behave(spell); } } class LightMold: DungeonMonster { public LightMold() { MoveBehavior = new Immobile(); AttackBehavior = new Defenseless(); SpellBehavior = new EvilSpellCast(); //LightMolds cast evil spells apparently. } } class Goblin: DungeonMonster { public Goblin() { MoveBehavior = new Mobile(); AttackBehavior = new Defensive(); SpellBehavior = new NoSpells(); //Goblins have no magical aptitude } } |
This is not what I’m actually doing, but it is a great step forward. I’ve laid out the code above because that is actually how I arrived at my current solution. So join me next time when I explain my understanding of entities.