I’m creating a javascript/webgl based game and would like you guys to take a look at the current entity component system which I created for it, and perhaps weigh in some opinions.

On caveat I don’t like is that, sometimes a component needs to call the attachedEntities version of a function it has overridden. For example handleInput, it 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, currently I 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.

So I’m curious what others think of this implementation.

Things I 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/

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 to get other’s 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

Full source code can be found here:

https://github.com/onedayitwillmake/ChuClone