Game Actor Class

Game actor

How to make an actor class for your game that features:

  • Init, active, killed and destroyed lifecycle;
  • Allows a _beforeDestroy method to be defined in order to show the kill animation and the data is only destroyed when the _afterDestroy method is called.
  • Automatically subtract life to the other actor on collision;
  • Has a player owner in order to know if has to subtract the life of the other actor (except when friendly damage is on);
  • Automatically kill the actor when the life is below 0 (except if destroy when killed is off);
  • Automatically increases the experience points after damage done and kills;
  • Automatically increases the money based on kills;
  • Allow for modular plugins to be added in order to enable certain features;
  • Keeps track of: total damage done, total kills, total experience points earned and total money earned;

Read more about classes and plugins in javascript.

Actor.js

Real world actor class used in Game03.

"use strict";

/**
 * Rationale:
 *
 *
 * Because of flexibility reasons we don't impose on you (so your code is cleaner and lighter):
 *     - position     : you might need multiple positions, maybe you don't need a position at all;
 *     - visual entity: you don't need to specify any visual entity (VE), is all up to you to create and
 *                      destroy them at will. Because of this you can have multiple VE and name them
 *                      correctly.
 *
 * But you have some bundled features:
 *     - When 2 actors collide their life is subtracted automatically and will kill anyone without
 *       life.
 *
 *     - Kills, damage done, experience points earned all are stored and increased on hit and on kill.
 *       You might be interested on how the points and damage are calculated, so you can check the
 *       following methods: notifyPostSolveColision, getExpRewardForDamage, getExpRewardForKill;
 *
 *     -
 *
 * Actor life-cycle:
 *
 *     - instanced : the first state an actor is found after instancing; If o.autoInit==true
 *                   and world and scene is provided, then it will automatically go to the next state;
 *     - init      : at this state the actor is shown and in the world;
 *
 *     - tick      : before doing anything call _beforeTick and only continue if is true;
 *
 *     - killed    : at this state the actor is dead, but not yet destroyed. This means that the actor
 *                   is not alive. This is used in order to keep a pre-destroy state which is needed for
 *                   pre-destroy animation; (maybe other stuff)
 *
 *     - destroyed : you usually don't want to have a reference to it when is in this state as it cannot
 *                   do anything. In this state there is no visible thing, no box2d entity nor any data.
 *
 * Other things you might need to know:
 *
 *     - the __postSolveCollisions can destroy bodies (when their life is 0) and is called within the
 *       world.step. In the case of being called once and this actor is killed and destroyed it's state
 *       will be destroyed. After that the body is not yet destroyed, is only scheduled to be destroyed
 *       and will be destroyed in the next world step. It can happen that another collision will happen
 *       and will find this actor in an invalid state (destroyed, not init). In order to avoid this we
 *       keep all the collisions in an array and will be calculated on the next actor tick. (in the
 *       _beforeTick method)
 *
 * How to use:
 * ===========
 *
 * When sub-classing:
 * ------------------
 *
 *     Create the following methods:
 *
 *         - constructor: first extend your options, then call the super constructor,
 *                        Box2DPixi.Actor.prototype.constructor.call(this, o); and then call the
 *                        _afterConstructor method when you end your constructor.
 *
 *         - init: here you should call on the first line "_beforeInit" and when you end initializing
 *                 the actor call "_afterInit"; Those methods are necessary in order to keep the Actor's
 *                 life-cycle.
 *
 *         - destroy: if you create other objects and you want to destroy them when the actor is
 *                    destroyed (you should have every object created destroyed when the actor is
 *                    in the destroyed state, if not you will leak memory) you should override the
 *                    "destroy" method and call the "_beforeDestroy" on the first line and the
 *                    "_afterDestroy" when you ended destroying everything you need.
 *
 *         - tick: this method is called on every frame. The first argument is the ms of the Box2D step.
 *
 *
 *     Other features:
 *
 *         - you want to show an animation on death: turn the "destroyWhenKilled" off by setting it to
 *           "false". Then listen to actor.onKilled.then(function(){**Start here your animation**}), when
 *           you end your animation and you are sure you don't need the actor anymore just call the
 *           actor.destroy() method.
 *
 *         - You want to give spawn protection: set the life to Infinity, put a timer of 1000ms and
 *           restore it's life when the timer ends.
 *
 *         - You want a ghost actor: just use the _isGhost variable. The framework just disable
 *           taking life from collisions.
 *
 * When used from exterior environment:
 * ------------------------------------
 *
 *     - When you no longer need an actor call the actor.destroy() method.
 *
 *     - If you want to kill an actor you should call actor.kill();
 *       Don't kill on the impact. The impact force is calculated in the actor and automatically subtract
 *       the actor's life. When the actor has no more life automatically the "kill" method is called
 *       and will turn the actor in "not alive" and therefore the "KILLED" status.
 *
 *     - If you want to know when this actor is killed: actor.onKilled.then(function(actor){...})
 *
 *     - Instance a new actor: new Actor({world: world, scene: scene});
 *       By default will also init the actor, because you supplied the world and scene; If you want to
 *       init later (in the case you don't have world or scene) you can do: new Actor() or
 *       new Actor({world: world, scene: scene, autoInit: false}); Then you call actor.init() to init.
 */
var Actor = function(options){
    var o = $.extend(true, {
        id                 : Actor.lastId++,
        // player:
        //     null            : neutral
        //     not null        : not neutral
        player             : null,

        world              : null,
        scene              : null,
        name               : 'Actor: ',
        startLife          : 1,

        // if left undefined will be the same as startLife
        maxLife            : undefined,
        damageMultiplier   : 1,
        moneyRewardForKill : 0,
        expRewardForDamage : 0,
        expRewardForKill   : 0,
        autoInit           : true,
        friendlyDamage     : false,
        isGhost            : false,
        destroyWhenKilled  : true,
    }, options);
    this.__id                 = o.id;
    this.__player             = o.player;
    this.__world              = o.world;
    this.__scene              = o.scene;
    this.__name               = o.name;
    this.__life               = o.startLife;
    this.__maxLife            = typeof o.maxLife === 'undefined' ? o.startLife : o.maxLife;
    this.__damageMultiplier   = o.damageMultiplier;
    this.__moneyRewardForKill = o.moneyRewardForKill;
    this.__expRewardForDamage = o.expRewardForDamage;
    this.__expRewardForKill   = o.expRewardForKill;
    this.__autoInit           = o.autoInit;
    this.__friendlyDamage     = o.friendlyDamage;
    this._isGhost             = o.isGhost;
    this._destroyWhenKilled   = o.destroyWhenKilled;

    this.__state               = Actor.STATE.INSTANCED;
    this.__totalDamageDone     = 0;
    this.__totalKills          = 0;
    this.__experiencePoints    = 0;
    this.__earnedMoney         = 0;
    this.__postSolveCollisions = [];
    this.__msSinceStart        = 0;

    Actor.add(this.__id, this);

    // events
    this.onInit    = new Future();
    this.onKilled  = new Future();
    this.onDestroy = new Future();
}



Actor.lastId = 0;
Actor.STATE = {
    INSTANCED : 0,
    INIT      : 1,
    KILLED    : 2,
    DESTROYED : 3,
}


window.instancedActors = Actor.instanced =[];



Actor.getById = function(id){
    return Actor.instanced[id];
}



Actor.add = function(id, instance){
    return Actor.instanced[id] = instance;
}



Actor.getAll = function(){
    // return a copy
    return Actor.instanced.slice();
}



Actor.remove = function(id){
    Actor.instanced[id] = null;
}



/**
 * A plugin is an instance of a class that is then attached to the prototype.
 */
Actor.addPlugin = function(Plugin, TargetActorClass, args){
    if(!TargetActorClass.plugins){
        var plugins = [];
        TargetActorClass.plugins = plugins;
        TargetActorClass.prototype.__plugins = plugins;
    }
    TargetActorClass.plugins.push(Plugin);
    $.each(Plugin.attachMethods, function(index, value){
        if(TargetActorClass.prototype[index]){
            throw new Error('The target of the plugin already has a method named: ' + value);
        }
        TargetActorClass.prototype[index] = value;
    })
},






_.extend(Actor.prototype, Backbone.Events, {
    _afterConstructor: function(){
        this.__initPlugins();
        if(this.__autoInit && this.__world && this.__scene){
            this.init();
        }
    },



    init: function(){
        this._beforeInit();
        this._afterInit();
    },



    getPlugins: function(){
        return this.__plugins;
    },



    __initPlugins: function(){
        this._callMethodOnEachPlugin('init');
    },



    // args as array
    _callMethodOnEachPlugin: function(methodName, args){
        var context = this;
        _.each(this.getPlugins(), function(Plugin){
            if(Plugin.pluginMethods[methodName]){
                Plugin.pluginMethods[methodName].apply(context, args);
            }
        })
    },



    _beforeInit: function(){
        if(this.__state !== Actor.STATE.INSTANCED)
            throw new Error('Invalid state.')

        this.__state = Actor.STATE.INIT;
    },



    _afterInit: function(){
        this.onInit.done(this);
    },


    getState: function(){
        return this.__state;
    },



    getTotalDamageDone: function(){
        return this.__totalDamageDone;
    },



    getTotalKills: function(){
        return this.__totalKills;
    },



    getExpRewardForDamage: function(){
        return this.__expRewardForDamage;
    },



    getExpRewardForKill: function(){
        return this.__expRewardForKill;
    },



    getMoneyRewardForKill: function(){
        return this.__moneyRewardForKill;
    },



    // experience points are based on total damage done + 10 times the kills
    getExperiencePoints: function(){
        return this.__experiencePoints;
    },



    addExperiencePoints: function(experiencePoints){
        this.__experiencePoints += experiencePoints;
        this.trigger('changeExperiencePoints', {
            actor: this,
            currentExperiencePoints: this.__experiencePoints,
            deltaExperiencePoints: experiencePoints,
        });
    },



    getEarnedMoney: function(){
        return this.__earnedMoney;
    },



    addMoney: function(money){
        this.__earnedMoney += money;
        this.trigger('changeMoney', {
            actor: this,
            totalMoney: this.__earnedMoney,
            deltaMoney: money,
        })
    },



    getPlayer: function(){
        return this.__player;
    },



    getId: function(){
        return this.__id;
    },



    kill: function(){
        if([Actor.STATE.INSTANCED, Actor.STATE.INIT].indexOf(this.__state) === -1)
            throw new Error('Invalid state');

        this.__state   = Actor.STATE.KILLED;
        this.onKilled.done(this);
        if(this._destroyWhenKilled){
            this.destroy();
        }
    },



    /**
     * Destroying an actor means that all of it's data is deleted. Don't call this
     * method unless you still need it. But remember to call when you don't need an
     * actor anymore as it will release the memory.
     */
    destroy: function(){
        this._beforeDestroy();
        this._afterDestroy();
    },



    _beforeDestroy: function(){
        if([Actor.STATE.INSTANCED, Actor.STATE.INIT, Actor.STATE.KILLED].indexOf(this.__state) === -1)
            throw new Error('Invalid state');

        this.__state  = Actor.STATE.DESTROYED;
        this._callMethodOnEachPlugin('_beforeDestroy', arguments);
    },



    _afterDestroy: function(){
        this._callMethodOnEachPlugin('_afterDestroy', arguments);
        this.__name                            = null;
        this.__player                          = null;
        this.__world                           = null;
        this.__scene                           = null;
        this.__postSolveCollisions.length      = 0;
        this.__postSolveCollisions             = null;
        this.__energyGathererArgstotalGathered = null;
        this.__experiencePoints                = null;
        this.__life                            = null;
        this.__maxLife                         = null;
        this.__totalDamageDone                 = null;
        this.__msSinceStart                    = null;
        this.onInit                            = null;
        this.onKilled                          = null;
        this.onDestroy                         = null;

        // backbone events
        this.off();

        Actor.remove(this.__id);
        this.__id = null;
    },



    isDestroyed: function(){
        return this.__state === Actor.STATE.DESTROYED;
    },



    isAlive: function(){
        return this.__state === Actor.STATE.INIT;
    },



    isSelected: function(){
        return this.__isSelected;
    },



    setSelected: function(isSelected){
        this.__isSelected = isSelected;
        if(isSelected){
            this.trigger('selected');
        }else{
            this.trigger('deselected');
        }
    },



    isHovered: function(){
        return this.__isHovered;
    },



    setHovered: function(isHovered){
        this.__isHovered = isHovered;
    },



    getLife: function(){
        return this.__life;
    },



    subtractLife: function(lifeUnits){
        if(!this.isAlive()){return;}
        this.__life -= lifeUnits;
        if(this.__life <= 0){
            this.kill();
        }
    },



    addLife: function(lifeUnits){
        if(!this.isAlive()){console.warn('You are giving life to a killed actor. Are you sure? It will remain killed.'); return;}
        this.__life = lifeUnits;
        if(this.__life > this.__maxLife){
            this.__life = this.__maxLife;
        }
    },



    // used for sensors mainly because sensors doesn't have preSolve
    notifyBeginContactColision: function(c){
        this._callMethodOnEachPlugin('notifyBeginContactColision', arguments);
    },



    notifyPreSolveColision: function(c){
        // extend
    },



    notifyPostSolveColision: function(c){
        if(this.__state !== Actor.STATE.INIT) console.warn('Should be init.');
        this.__postSolveCollisions.push(c);
        // this is called twice for the same contact
    },



    // used for sensors mainly because sensors doesn't have preSolve
    notifyEndContactColision: function(c){
        this._callMethodOnEachPlugin('notifyEndContactColision', arguments);
    },



    tick: function(box2DDeltaMs, realDeltaMs){
        this._beforeTick(box2DDeltaMs);
    },



    _beforeTick: function(box2DDeltaMs){
        if(this.isDestroyed()) return false;
        this.__msSinceStart += box2DDeltaMs;

        if(!this._isGhost){
            _.each(this.__postSolveCollisions, function(c){
                // Is required to test if the actor is killed because it could be killed in a previous
                // collision, so we don't make more damage if is killed.
                // But is not required to check if this actor is killed because it can have been killed
                // by the same collision but by the other actor. Therefore if we would filter out the
                // A actor (this) when is killed we would only apply the damage from actor B to A and
                // not the damage from A to B.

                // we know that A is ours actor and B is the other
                var otherActor = c.actorB;
                var isActor    = otherActor;
                if(!isActor) return;
                if(!otherActor.isAlive()) return;

                var isSamePlayer = otherActor.getPlayer() === this.__player;

                if(isSamePlayer && !this.__friendlyDamage){
                    return;
                }

                // calculate the damage, if the other is killed, money and exp rewards
                var hitForce               = c.impulse.normalImpulses[0];
                var damageDone             = hitForce * this.__damageMultiplier;
                this.__totalDamageDone    += damageDone;
                var moneyEarned            = 0;
                var experiencePointsEarned = damageDone * otherActor.getExpRewardForDamage();

                otherActor.subtractLife(damageDone);
                var killed = false;

                if(!otherActor.isAlive()){
                    this.__totalKills++;
                    killed = true;
                    experiencePointsEarned += otherActor.getExpRewardForKill();
                    moneyEarned = otherActor.getMoneyRewardForKill();
                }

                this.addMoney(moneyEarned);
                this.addExperiencePoints(experiencePointsEarned)
                this.trigger('hit', {
                    hitActor               : otherActor,
                    damageDone             : damageDone,
                    experiencePointsEarned : experiencePointsEarned,
                    moneyEarned            : moneyEarned,
                    killed                 : killed,
                    actorSource : this,
                })
            }.bind(this));
            this.__postSolveCollisions = [];
        }
        this._callMethodOnEachPlugin('tick', arguments);
        return true;
    },



    set: function(o){
        $.extend(true, this, o);
    },
})

return Actor;

Subscribe to receive new interesting posts about programming

comments powered by Disqus