From inheritance to traits
When I started this project I was quite fond of inheritance. Inheritance is great for a lot of things, but if you are inheriting more than 3 levels you’re either doing something particular or you are on you way to inheritance hell.
Beginning writing the game engine I started out with a time keeper that I call the “Engine” and a place where all objects are kept that I named the “World”. The engine decides how much time has passed between iterations and feeds it to the world which in turn feeds that to each object.
There are a lot of different objects and my basic one was simply named “Object”. It had a position, the 3d model, a timeshift() function and a collide() function. Once I added Megaman to the game I added a Character object which inherits from the basic object, and then the Megaman object in turn inherits the character object.
So far so good. But now I needed a solid object. Something you can’t walk through and that supports you so that you can stand on it and jump off of it.
Simple and dandy. But what if I need a character that you can stand on. I want to use the same code as the solid object, but on my a character. This can be solved by multiple inheritance. In JavaScript that kind of thing is often called mixin because you mix in methods from one class into another.
Now we have an inheritance tree of some sort mimicking the illustration to the left.
So far this kinda works, but as you start adding more things to the game it will crumble under it’s own complexity.
You will want to have an AI on an object, but what if you want Megaman to use the AI of Heatman or you want a version of Crashman which you climb on like a ladder. While it is possible to do with inheritance, after a while you will have a complete mess or a monolithic object that can do everything.
Creating Megaman would not require any of these contrived examples to work, but the reason I’m doing this project isn’t so that I can play Megaman in a couple of years, but to learn how to create a flexible engine that is easy to expand.
Introducing traits
A trait in the world of my engine is a self contained set of methods and properties that can be applied to any object.
This means that I can create a solid that has the AI of Dr.Wily but that is also a ladder you can climb on. Or a ladder that shoots fire. The former example would require you to write something like this.
var shooting_ladder = new Engine.Object();
shooting_ladder.applyTrait(new Engine.traits.Ladder());
shooting_ladder.applyTrait(new Engine.traits.Weapon());
Let’s get technical
A trait is a class containing “magic” methods, and a set of private and public methods and members. Traits can be applied to any object. That object becomes their “host” object. Traits can then mutate their host in any way they see fit and also gets callbacks to their special magic methods.
Public methods and members have normal words like engage(), duration and are meant to be exposed to be manipulated
Private methods and members are just prepended with underscore. They are not really private in the strictest sense of the word, but a cosmetic detail to convey it will have unknown consequences if tampered with.
Magic methods means that they have special meaning and are prepended by double underscore. My first implementation of traits uses seven magic methods.
- __attach() Called when Trait is applied to “host” object.
- __collides() Called when host enters the collision zone of an object.
- __detach() Called when Trait is removed from object.
- __obstruct() Called when host is pushed by another object.
- __require() Used to find a trait on host, in case we depend on another trait.
- __uncollides() Called when host leaves an objects collision zone.
- __timeshift() Called when host receives a timeshift request.
Let’s take the invincibility trait as example for how a trait is used. The first thing that happens after the trait has been instantiated, meaning new Engine.traits.Invincibility() has been called, is that it gets applied to an object and therefore the magic __attach function is called. The attach function gets the object that it is applied to as a variable called host.
Since it makes no sense to attain invincibility without having health, the first thing the invincibility trait does is to call __require(host, Engine.traits.Health). This iterates through all available traits, finds the health trait, and returns it’s instance. We can therefore export the existing health trait and store that on the invincibility trait for easy reference.
Then we call the parent Trait’s __attach method, which I will explain soon.
Lastly, we bind the Invincibility traits engage() method to the EVENT_DAMAGED event on our host.
We call the parent Trait object’s __attach method to automatically bind our magic methods to the hosts events and bind the host to this._host. If we didn’t need anything special to happen for our trait in the __attach stage we could leave that method out.
From now on, whenever our host takes damage, the engage() method will get a callback. In this method we do the following:
- Ensure duration property is not zero. Being invincible for zero seconds effectively turns the trait off.
- Set the host’s health to be infinite by setting the health traits infinite flag to true. We could also express that using this._host.health.infinite = true.
- Reset _elapsed time to zero. This is the counter for how long we have been invincible.
- Set _engaged flag to true. This is how we know we have modified our object and should be counting time.
This means that the host no longer can take any damage and that we will need to handle this in our magic __timeshift method which will now begin to have significance.
On every loop we do the following:
- Check that the _engaged flag is true.
- Switch the visible flag of the hosts model. This is what makes Megaman flicker.
- Check if elapsed time is longer than duration and disengage() if it is.
- Add deltaTime to elapsed time.
When duration has been reached we call disengage which
- Turns off infinite on the host’s health.
- Sets the visible flag of the host’s model to true.
- Turns off our timeshift loop by setting engaged to false.
Instead of having the _engaged flag I could bind the __timeshift method to the host when engage() is run and unbind it when disengage() is run.
So far all my previous functionality has been easy to port into traits and I’m super excited for the simplifications this will have when continuing to build Megaman 2. I’m already thinking about all the fun traits that needs to be written for Megaman 3, which for example introduced “sliding” to Megaman.
And with that, I say thank you for reading and hope I did a good job explaining why traits make a difference here.