Better handling of behaviours in ImpactJS

For the JIPPI project we made a game with a fairly standard setup with a menu that links to a couple of levels. We started off by adding functionality to the main class and then switching between update loops based on which level you're at. This will quickly become difficult to maintain.

Instead, we opted for a more Unity'ish approach (where you use GameObjects to add behaviour to the scene. And these GameObjects are placed in the scene. A bit like working with ActionsScript 1 and 2 -if anyone still remembers, where you put some AS-code in a MovieClip and put that in the scene). We created behaviour entities that we placed in the levels. These were never rendered, but provided an update loop that kept the level running, and the main file clean.

There are some benefits of using an approach like this:

  • game.loadLevel() will kill all your entities when you switch levels. That way you don't have to create any dealloc functions to clean up any mess.
  • It's easy to see when editing the level which behaviour this level has. And it's equally easy to change the level's behaviour.
  • The code is separated from the main class and easily maintainable.

So, how do you setup a system like this? Let me show you.

We need two base classes that all behaviour and entity classes extend from.

BaseBehaviour

ig.module(
    'game.base.base-behaviour'
)
.requires(
    'impact.entity'
)
.defines(function(){
    ig.EntityBaseBehaviour = ig.Entity.extend({
        _wmScalable: false,
        _wmDrawBox: true,
        _wmBoxColor: 'rgba(196, 255, 0, 0.7)',

        size: {x:64, y:64},

        game: null,
        game_data: null,

        _ready: function(args) {
            if (args !== undefined) {
                if (args.game) this.game = args.game;
            }
            if (this.game.director.data !== null) {
                this.game_data = ig.copy(this.game.director.data);
                this.game.director.data = null;
            }
            this.ready();
        }
    });
});

BaseEntity

ig.module(
    'game.base.base-entity'
)
.requires(
    'impact.entity'
)
.defines(function(){
    ig.EntityBaseEntity = ig.Entity.extend({

        behaviour: null,
        game: null,

        _ready: function(args) {
            if (args !== undefined) {
                if (args.behaviour) this.behaviour = args.behaviour;
                if (args.game) this.game = args.game;
            }
            this.ready();
        }
    });
});

Main

loadLevel: function( data ) {
    this.screen = {x: 0, y: 0};
    this.entities = [];
    this.namedEntities = {};
    for( var i = 0; i < data.entities.length; i++ ) {
        var ent = data.entities[i],
        this.spawnEntity( ent.type, ent.x, ent.y, ent.settings );
    }
    this.sortEntities();

    // Call post-init ready function on all entities
    var behaviour = this.getEntitiesByType(ig.EntityBaseBehaviour)[0],
        entits = this.getEntitiesByType(ig.EntityBaseEntity);

    if (typeof behaviour !== 'undefined' && behaviour !== null) {
        // Pass reference to game class to behaviour
        behaviour._ready({ game: this });
    }
    else {
        console.error('Your level is missing the main behaviour');
    }

    // Pass reference to behaviour class to all entities
    for( var i = 0; i < entits.length; i++ ) {
        entits[i]._ready({
            behaviour: behaviour,
            game: this
        });
    }
}

Director

goTo: function(nick, options) {
    if (typeof options !== 'undefined') {
        this.data = options;
    }
    this.jumpTo(nick);
}

What happens here is the following:

Switching between levels is done using a Director class. With this class you can register levels with a keyword. That way you don't have to load a level by making a direct reference to the level class. The Director plugin's goTo function accepts two parameters: nick name for the level to load, and an optional options object for any data that you might want to pass from a level to another.

When you call director.goTo(), the Director will save any optional data in its own data property, and then trigger loadLevel (this is done in jumpTo(), where the Director checks to see if it has a level with the desired nick name).

LoadLevel will traverse all entities in the levels file and look for behaviours and entities. Once a behaviour has been found, all entities that extend from BaseEntity will get a reference to the behaviour so that they can access it via this.behaviour. Both the entities and the behaviour will get a reference to the main game class so that they don't have to use ig.game (which doesn't work well with Weltmeister).

Once the passing of references to the behaviour and the game class has been done, ready() is called on all entities. That's where an entity can access the data object via this.behaviour.game_data.

Caveats

This implementation supports only one behaviour per level. I have some ideas for how to implement support for multiple behaviours per level. That would most definitely come in handy.

Latest articles