I'm creating a JavaScript/WebGL based game and would like you all to take a look at the current entity component system which I created for it. I've used it once before on another project, and while I was very happy with the results, there were parts of it I did not feel 100% good about.
In particular, sometimes a component needs to call the attachedEntities
version of a function it has overridden. For example handleInput
might call the attachedEntities
handleInput
functions and then with the new movementVector
multiply them by negative 1 so now all input while this component is active is reversed.
When I intercept the properties, I currently wrap intercepted function calls into a closure (see basecomponent.intercept()
) so that the this
object can refer to the component itself. Without that the this
object was still being referred to as the attachedEntity
(from within the components version of that function), so to get the components properties I had to run a query.
Things I've used as reference:
- http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/
- http://gameprogrammingpatterns.com/component.html
- http://blog.bengarney.com/2010/06/12/composition-vs-inheritance/
Full source code can be found here.
Base Component
(function() {
ChuClone.namespace("ChuClone.components");
ChuClone.components.BaseComponent = function() {
this.interceptedProperties = {};
return this;
};
ChuClone.components.BaseComponent.prototype = {
/**
* Array of properties intercepted, this is used when detaching the component
* @type {Array}
*/
interceptedProperties : null,
/**
* @type {ChuClone.GameEntity}
*/
attachedEntity : null,
/**
* @type {Number}
*/
detachTimeout : 0,
/**
* Unique name for this component
* @type {String}
*/
displayName : "BaseComponent",
/**
* If a component can stack, then it doesn't matter if it's already attached.
* If it cannot stack, it is not applied if it's currently active.
* For example, you can not be frozen after being frozen.
* However you can be sped up multiple times.
* @type {Boolean}
*/
canStack : false,
/**
* Attach the component to the host object
* @param {ChuClone.GameEntity} anEntity
*/
attach: function(anEntity) {
this.attachedEntity = anEntity;
},
/**
* Execute the component
* For example if you needed to cause an animation to start when a character is 'unfrozen', this is when you would do it
*/
execute: function() {
},
/**
* Detaches a component from an 'attachedEntity' and restores the properties
*/
detach: function() {
clearTimeout(this.detachTimeout);
this.restore();
this.interceptedProperties = null;
this.attachedEntity = null;
},
/**
* Detach after N milliseconds, for example freeze component might call this to unfreeze
* @param {Number} aDelay
*/
detachAfterDelay: function(aDelay) {
var that = this;
this.detachTimeout = setTimeout(function() {
that.attachedEntity.removeComponentWithName(that.displayName);
}, aDelay);
},
/**
* Intercept properties from the entity we are attached to.
* For example, if we intercept handleInput, then our own 'handleInput' function gets called.
* We can reset all the properties by calling, this.restore();
* @param {Array} arrayOfProperties
*/
intercept: function(arrayOfProperties) {
var len = arrayOfProperties.length;
var that = this;
while (len--) {
var aKey = arrayOfProperties[len];
this.interceptedProperties[aKey] = this.attachedEntity[aKey];
// Wrap function calls in closure so that the 'this' object refers to the component, if not just overwrite
if(this.attachedEntity[aKey] instanceof Function) {
this.attachedEntity[aKey] = function(){
that[aKey].apply(that, arguments);
}
} else {
this.attachedEntity[aKey] = this[aKey];
}
}
},
/**
* Restores poperties that were intercepted that were intercepted.
* Be sure to call this when removing the component!
*/
restore: function() {
for (var key in this.interceptedProperties) {
if (this.interceptedProperties.hasOwnProperty(key)) {
this.attachedEntity[key] = this.interceptedProperties[key];
}
}
}
}
})();
JumpComponent
(function(){
ChuClone.namespace("ChuClone.components");
ChuClone.components.JumpPadComponent = function() {
ChuClone.components.JumpPadComponent.superclass.constructor.call(this);
console.log(this)
};
ChuClone.components.JumpPadComponent.prototype = {
displayName : "JumpPadComponent", // Unique string name for this Trait
_textureSource : "assets/images/game/jumppad.png",
_restitution : 2,
_previousMaterial : null,
_previousRestitution : 0,
/**
* @inheritDoc
*/
attach: function(anEntity) {
ChuClone.components.JumpPadComponent.superclass.attach.call(this, anEntity);
var view = anEntity.getView();
var body = anEntity.getBody();
// Swap materials
this._previousMaterial = view.materials[0];
view.materials[0] = new THREE.MeshLambertMaterial( {
color: 0xFFFFFF, shading: THREE.SmoothShading,
map : THREE.ImageUtils.loadTexture( this._textureSource )
});
view.materials[0] = new THREE.MeshBasicMaterial( { color: 0x608090, opacity: 0.5, wireframe: true } );
// Swap restitution
this.swapRestitution( body );
// Listen for body change
this.intercept(['setBody', 'height']);
},
/**
* Sets the restitution level of the provided body's fixtures to make it a jumppad
* @param {Box2D.Dynamics.b2Body} aBody
*/
swapRestitution: function( aBody ) {
var node = aBody.GetFixtureList();
while(node) {
var fixture = node;
node = fixture.GetNext();
this._previousRestitution = fixture.GetRestitution();
fixture.SetRestitution( this._restitution );
}
},
/**
* Set the body
* @param {Box2D.Dynamics.b2Body} aBody
*/
setBody: function( aBody ) {
this.interceptedProperties.setBody.call(this.attachedEntity, aBody );
if(aBody) // Sometimes setBody is called with null
this.swapRestitution( aBody )
},
/**
* Restore material and restitution
*/
detach: function() {
this.attachedEntity.getView().materials[0] = this._previousMaterial;
var node = this.attachedEntity.getBody().GetFixtureList();
while(node) {
var fixture = node;
node = fixture.GetNext();
fixture.SetRestitution(this._previousRestitution);
}
ChuClone.components.JumpPadComponent.superclass.detach.call(this);
}
};
ChuClone.extend( ChuClone.components.JumpPadComponent, ChuClone.components.BaseComponent );
})();
Entity support for components
/**
* Adds and attaches a component, to this entity
* @param {ChuClone.components.BaseComponent} aComponent
* @return {ChuClone.components.BaseComponent}
*/
addComponent: function(aComponent) {
// Check if we already have this component, if we do - make sure the component allows stacking
var existingVersionOfComponent = this.getComponentWithName(aComponent.displayName);
if (existingVersionOfComponent && !existingVersionOfComponent.canStack) {
return false;
}
// Remove existing version
if (existingVersionOfComponent) {
this.removeComponentWithName(aComponent.displayName);
}
this.components.push(aComponent);
aComponent.attach(this);
return aComponent;
},
/**
* Convenience method that calls ChuClone.GameEntity.addComponent then calls execute on the newly created component
* @param {ChuClone.components.BaseComponent} aComponent
* @return {ChuClone.components.BaseComponent}
*/
addComponentAndExecute: function(aComponent) {
var wasAdded = this.addComponent(aComponent);
if (wasAdded) {
aComponent.execute();
return aComponent;
}
return null;
},
/**
* Returns a component with a matching .displayName property
* @param aComponentName
*/
getComponentWithName: function(aComponentName) {
var len = this.components.length;
var component = null;
for (var i = 0; i < len; ++i) {
if (this.components[i].displayName === aComponentName) {
component = this.components[i];
break;
}
}
return component;
},
/**
* Removes a component with a matching .displayName property
* @param {String} aComponentName
*/
removeComponentWithName: function(aComponentName) {
var len = this.components.length;
var removedComponents = [];
for (var i = 0; i < len; ++i) {
if (this.components[i].displayName === aComponentName) {
removedComponents.push(this.components.splice(i, 1));
break;
}
}
// Detach removed components
if (removedComponents) {
i = removedComponents.length;
while (i--) {
removedComponents[i].detach();
}
}
},
/**
* Removes all components contained in this entity
*/
removeAllComponents: function() {
var i = this.components.length;
while (i--) {
this.components[i].detach();
}
this.components = [];
}
Usage
var jumpPadComponent = new ChuClone.components.JumpPadComponent();
entity.addComponentAndExecute( jumpPadComponent );
//... Some time later
entity.removeComponentWithName( ChuClone.components.JumpPadComponent.prototype.displayName );
I'm curious about others' thoughts on this implementation of such a component-based system for a JavaScript game.
JumpPadComponent
attached to a couple of standard entities:
JumpPad component toggle
-
2\$\begingroup\$ "i did not feel 100% about. <code dump>" Would you like to give a high level overview of which parts might need addressing \$\endgroup\$Raynos– Raynos2011年07月04日 20:35:29 +00:00Commented Jul 4, 2011 at 20:35
-
2\$\begingroup\$ A rough review of the code makes me think it's far too verbose and over-engineered. KISS. \$\endgroup\$Raynos– Raynos2011年07月04日 20:46:06 +00:00Commented Jul 4, 2011 at 20:46
-
\$\begingroup\$ Well that isn't much help, in fact you had already posted that before I finished posting the entire code dump - specifically the management of components within the entity. Which was one of the more important parts ot it. \$\endgroup\$onedayitwillmake– onedayitwillmake2011年07月04日 21:13:47 +00:00Commented Jul 4, 2011 at 21:13
1 Answer 1
Your code requires a ton of domain knowledge to properly review. I went to your Github repository to try and be more familiar with what you have.
Entity support for components
You asked specifically for this one, and I did find some things to ponder upon.
addComponent
existingVersionOfComponent
-> a better name could beexistingInstanceofComponent
or even simplerinstance
, as there is no versioning.// Remove existing version
, for me it is confusing that the component canstack
, but you still only allow 1 instance of it. Maybe you should find a better name/descriptor thanstack
.
addComponentAndExecute
- You return
null
here , not bad in itself, except that you returnfalse
inaddComponent
. You might want to consider reviewing your code and standardizing onfalse
ornull
.
removeComponentWithName
This code confuses me. It seems built with multiple instances of the same component in mind, but then it
breaks
out thefor
loop. It seems you could write this code with 1 loop.You can merge the 2 loops here in to 1 loop:
removeComponentWithName: function(aComponentName) { var len = this.components.length; var removedComponents = []; for (var i = 0; i < len; ++i) { if (this.components[i].displayName === aComponentName) { removedComponents.push(this.components.splice(i, 1)); break; } } // Detach removed components if (removedComponents) { i = removedComponents.length; while (i--) { removedComponents[i].detach(); } } },
can be
removeComponentWithName: function(componentName) { var len = this.components.length, removedComponent; for (var i = 0; i < len; ++i) { if (this.components[i].displayName === componentName) { removedComponent = this.components.splice(i, 1).pop(); removedComponent.detach(); break; } } },
-
1\$\begingroup\$ Great feedback. The get component with name, uses the string, displayName property of a component - because it looks for instances of that type, instead of a specific component. \$\endgroup\$onedayitwillmake– onedayitwillmake2014年01月28日 01:27:19 +00:00Commented Jan 28, 2014 at 1:27
-
\$\begingroup\$ Right, it can be called by other components with no specific instance in mind. For example a slowdown component, will just get a component called 'speedup' by name and disable it. Not sure what your question is then, obviously \$\endgroup\$onedayitwillmake– onedayitwillmake2014年01月28日 02:05:46 +00:00Commented Jan 28, 2014 at 2:05
-
\$\begingroup\$ Removed that part. \$\endgroup\$konijn– konijn2014年01月28日 13:09:28 +00:00Commented Jan 28, 2014 at 13:09
Explore related questions
See similar questions with these tags.