Overview
Expect this Unit to take about 2 hours. To demonstrate what you should have at the end of this Unit, check out the Demo for Unit 5. Don't forget to use the Chrome DevTools to look at the console and check out the source code for my tests!
Components is a concept where we divide up functionality for objects into small, re-usable peices that can be mixed and matched and assigned to an object as needed. The component-entity paradigm is very popular in game engines, in fact you've probably seen it if you've used other game engines out there, and while we can easily get away with just sticking to only using inheritence in this engine I think it's a valuable lesson to get comfortable with building a component system.
Before we talk too much about what components are and what they might do, we'll start with the GameObject, who has the responsibility of managing an arbitrary list of components.
Then we'll get into Components, how they manage being added and removed from a GameObject.
gameObjects.js
gameObject.js
Finally! It's time for the GameObject class! This will be our base class in which all game objects in the game derive from. For now this class will just be a storage unit for components that are attached to it.
A constructor function will create an array to store all the active Components.
We will need three functions to manage components: addComponent, hasComponent, deleteComponent.
We are also going to inherit the destroy() function from DrawNode. We want to make sure that any components that are attached to this GameObject know when it is destroyed, and we'll want to delete all the components as well.
Source Code:
//*****************************************************************************
// GameObject class
//
// Required Systems:
// Engine
// Engine.DrawNode
//
// Required libraries:
// None
//
// Features:
// Manages having components, and has other utility functions.
// This class extends the Engine.DrawNode class, so:
// * it can bind to events and trigger events
// * it can be rendered as part of a render tree
//
//*****************************************************************************
// Base GameObject class, added to the Engine namespace
Engine.GameObject = Engine.DrawNode.extend({
// class Name, for Debugging
typename: "GameObject",
//=========================================================================
// Constructor: add storage for components
init: function(props) {
this._super(props);
this.activeComponents = [];
}
//=========================================================================
// Checks if this GameObject has a component type
hasComponent: function(componentName) {
// @TODO
},
//=========================================================================
// Add a component by name to this GameObject, passing along properties.
// componentName parameter is optional, but if used it allows for more
// than one component of the same type/class.
addComponent: function(componentType, properties, componentName) {
// @TODO
},
//=========================================================================
// Remove a component by name
deleteComponent: function(componentName) {
// @TODO
},
//=========================================================================
// Called whenever the object is no longer alive
destroy: function() {
// @TODO
}
});
addComponent()
Adding a component onto a GameObject should be as simple as giving it the component type name (what it was registered as) along with any constructor properties that need to get passed along.
We take an additional, optional parameter of a name to give the component. This is useful in case you want to add more than one component of the same type to a given GameObject. Say we had multiple sprite components attached to a single GameObject: a character, a hat, a weapon - each would be added under different names (although that's just one way to approach a solution).
We need to find the Component class that has the passed in name, and then create a new instance of it and store it in the list of .activeComponents
As a shortcut to getting access to the component, we store it by name as a property of the GameObject directly.
Once a component is added, we fire off an event in case any objects are listening.
Source Code:
addComponent: function(componentType, properties, componentName) {
componentName = componentName || componentType;
// First make sure we don't already have a component stored by this name
if( this.hasComponent(componentName) ) {
return;
}
// Find the Component definition by type
var compClass = Engine.ComponentList[componentType];
if( compClass) {
var newComp = new compClass(this, properties);
// add the component to the GameObject's list of active components
this.activeComponents.push(newComp);
// add the component to the GameObject as a property under its name
this[componentName] = newComp;
// trigger an event signaling a new component was added
this.triggerEvent('componentAdded', newComp);
}
return this;
}
hasComponent()
Now that we see how the storage of components looks, it's very easy to tell if a game object has a specific component or not!
Source Code:
hasComponent: function(componentName) {
return this[componentName] ? true : false;
}
deleteComponent()
Deleting a component is just the mirror opposite of adding one. We iterate over all of the active components
Source Code:
deleteComponent: function(componentName) {
// Looks up to see if this GameObject has the given component, and destroys it if so
if(componentName && this.hasComponent(componentName)) {
// trigger an event signaling a component was destroyed
this.triggerEvent('deleteComponent',this[componentName]);
this[componentName].destroy();
}
}
destroy()
When a GameObject is destroyed, it destroys all of it's components. Since it is also a DrawNode, calling this._super() makes sure it destroys all of it's child nodes too, and going up the inheritance tree we know that since DrawNodes derive from BaseObject, it debinds listeners attached to it. OOP can be fun!
We have a .destroyed flag just in case someone keeps a handle to the object even after calling destroyed. It can be helpful for debugging, as well as makes sure we don't try destroying an object more than once.
Source Code:
destroy: function() {
// prevent duplicate destruction
if(this.destroyed) {
return;
}
this._super();
// trigger an event on this object signaling that it has been destroyed
this.triggerEvent('removed');
// destroy any attached component
if( this.activeComponents ) {
for(var i=0,len=this.activeComponents.length;i<len;i++) {
var comp = this.activeComponents[i];
// trigger an event signaling a component was destroyed
this.triggerEvent('deleteComponent',comp.name);
comp.destroy();
}
this.activeComponents.length = 0;
}
// mark this object as destroyed
this.destroyed = true;
}
components.js
Since components are added to a game object by type, we need a good way to register and store the available component types. You already saw in the GameObject's addComponent that we use a list stored on the global Engine namespace, so let's explain how that works.
Component Registration with the Engine
Whenever you want to create a new Component type, by deriving from the Component class, it'd be great if it automatically registered itself with the Engine.
We are going to write a wrapper function to do just that whenever a user (you!) tries to use Component.Extend(), and change it up so that it expects an extra parameter for what name to register the component under.
Source Code:
//=========================================================================
// New component classes are stored here
//
Engine.ComponentList = {};
//=========================================================================
// When creating a new component class, automatically register it
// with the list above.
//
var _componentExtend = Engine.Component.extend;
Engine.Component.extend = function(name, props) {
if( typeof name === "object" ) {
name = props.name;
}
if( typeof name !== "string" ) {
console.error( "Cannot add unnamed component" );
return;
}
if( Engine.ComponentList[name] ) {
console.error( "Engine registering a component that already registered." );
} else {
// Call the original Component.extend() to create the new class type
var childClass = _componentExtend(props);
Engine.ComponentList[name] = childClass;
}
return childClass;
};
components.js
The base component class really only needs to handle being added to and removed from an object. Let's have a look at the API first: we are going to want init() and destroy() functions to handle when the component is created and destroy.
Source Code:
//*****************************************************************************
// Component Class
//
// Required Systems:
// * Engine
// * Engine.BaseObject
//
// Required libraries:
// * Underscore
//
// Features:
// Handles being added and removed to a game object.
// The extend attribute can store any properties to be added to the entity directly.
//
//*****************************************************************************
// Base Component class, added to the Engine namespace
Engine.Component = Engine.BaseObject.extend({
typename: "Component",
defaults: {
},
//=========================================================================
// Constructor function
init: function(entity, props) {
// @TODO
},
//=========================================================================
// Destructor function
destroy: function() {
// @TODO
}
});
init()
We already saw how the components are created when added to a game object, so you should notice now that the init() function is taking an extra parameter which is the object it is being attached to. We store a reference to that and that's it!
Source Code:
init: function(entity, props) {
this._super(props);
// set a property so that the component can refer back to the entity
this.entity = entity;
}
destroy()
When a GameObject is destroyed, all of its components are destroyed as well. If a single component is destroyed on its own, then it needs to make sure to remove itself from the GameObject.
Make sure to call the base class destructor, and also cleanup the GameObject's named reference to this component.
Source Code:
destroy: function() {
// Call the base class destructor
this._super();
// destroy the property from the entity
// (unlike C++ where this would look like we're deleting a pointer, here we are just deleting an entry from a JS object)
delete this.entity[this.ComponentName];
// remove the component from the entity's list of active components
var idx = this.entity.activeComponents.indexOf(this.ComponentName);
if(idx != -1) {
this.entity.activeComponents.splice(idx,1);
}
}
Test code
For an example, let's create a Health Component, give it to a Game Object, and see how it might work.
Source Code:
// Notice the difference in parameters for .extend with Components
var HealthComponent = Engine.Component.extend("health", {
defaults: {
health: 100,
maxHealth: 100
},
init: function(props) {
this._super(props);
this.entity.bindEvent("hurt", this, "subtractHealth" );
this.entity.bindEvent("heal", this, "addHealth" );
},
subtractHealth: function(dmgAmount) {
this.health -= dmgAmount;
if( this.health <= 0 ) {
console.log( "No health remaining! Destroying the game object" );
this.entity.destroy(); // or this could be an event!
return;
}
console.log( "damage taken. current health:", this.health );
},
addHealth: function(healthAmount) {
this.health += healthAmount;
if( this.health > this.maxHealth ) {
this.health = this.maxHealth;
}
console.log( "health added. current health:", this.health );
}
});
// Adding the health component is *super* easy!
var player1 = new Engine.GameObject();
player1.addComponent( "health" );
// Since the component is wired up to listen to events from it's parent game object, all we have to do
// is fire off some of those events!
player1.triggerEvent( "heal", 5 );
player1.triggerEvent( "hurt", 5 );
Homework
Part 1. Create and add the two files to your project: gameObject.js, components.js
- Create the files and put them all in your /js_engine sub-directory.
- Add all three files to the list of engine files to load through modernizr in main.js
Part 2. Fill out gameObject.js. and components.js with the source code from the class lecture.
Part 3. Test the new features of the Engine
- Make sure you can get the test code in the class lecture working.
- Try creating your own derived Component type. Some suggestions might be something like an AI Component or a Position Component.
- Create a new derived type of GameObject. Make sure you are comfortable with assigning it new default properties, inheriting functions, and creating new function types.