Upgrading to melonJS 1.1

September 23, 2014 André Schmitz 0 Comments


With the release of melonJS 1.1, is time to conduct a upgrade to this new version of the engine, packed with really nice features, as:

- New blazing-fast inheritance pattern (Jay Inheritance)
- New SAT (Separate Axis Theorem) based collision algorithm
- Super fast collision detection across a spatial partitioning algorithm (QuadTree)
- Added collision filtering support
- Entity object redesign with a full polygon shape based physic body implementation
- Several objects renamed  for a cleaner  and standardized API
- Fixed various bugs and issues

Check the melonJS Blog for a official announcement and get the lastest version in melonJS Site.

The recommended is to migrate the current version used in the game to one immediately more recent facilitating the upgrade process (some melonJS versions have API breaks). For example, if your game uses melonJS 0.9.11, you need to update first to 1.0.2 version (check the post Upgrading to melonJS 1.0) and after to 1.1.0 version.

Below we will check some of the necessary changes to upgrade from melonJS 1.0.2 (previous version) to melonJS 1.1.0 (current version when writing this post). For a complete list of changes visit the melonJS Upgrade Guide.


Before the Upgrade


The melonJS 1.1 is a major release and include several breaking changes which forces you to make several adjustments in your game code.

Before starting the migration, check the Upgrade Guide and the Changelog of engine and decide if the amount of work for the upgrade compensates the incredible new features of this melonJS version!

Don't forget to always have a backup of the last game version before starting the upgrade process!


Piece of Cake


Start renaming the functions, objects and variables changed in this version, which can be easily performed via a Search and Replace (or Refactor) available in your preferred Code Editor (or IDE):

// Old Code -> New Code
me.ObjectEntity -> me.Entity
me.SpriteObject -> me.Sprite
me.ObjectContainer -> me.Container
me.video.getWidth -> me.video.renderer.getWidth
me.video.getHeight -> me.video.renderer.getHeight
me.video.getSystemCanvas -> me.video.renderer.getCanvas
me.video.getSystemContext -> me.video.renderer.getContext
me.video.getScreenCanvas -> me.video.renderer.getScreenCanvas
me.video.getScreenContext -> me.video.renderer.getScreenContext
me.video.applyRGBFilter -> me.video.renderer.applyRGBFilter
me.video.getContext2d -> me.CanvasRenderer.getContext2d
If you used the onDestroyEvent callback from me.Renderable, the same should be renamed to onDeactivateEvent. Note that the function onDestroyEvent remains only to me.ScreenObject.

In preparation to WebGL support (probably available in version 1.2.0), the call to me.video.init has a new parameter informing the type of renderer (currently accepts only me.video.CANVAS):

// Before melonJS 1.1
me.video.init("game", 400, 300, true, "auto");

// After melonJS 1.1
me.video.init("game", me.video.CANVAS, 400, 300, true, "auto");

Let's Rock


The inheritance pattern has changed significantly, moving from John Resig Simple Inheritance to the new and fast Jay Inheritance, forcing various adjustments in the code. In the melonJS 1.0 and previous, you have something like:

// Before melonJS 1.1
var sprite = me.SpriteObject.extend({
  // Property
  active: yes,
  
  // Constructor
  init : function(x, y, sprite) {
    this.parent(x, y, sprite);
  },
 
  // Draw
  draw: function(context) {
    // Draw only if is active
    if (this.active)
       this.parent(context);
  }
});
To make performance improvements and enforce better rules you need to invoke a _super method (instead of parent), passing the base class itself, a string representing the method name, and an array of the arguments:

// After melonJS 1.1
var sprite = me.Sprite.extend({
  // Constructor
  init : function(x, y, sprite) {
    this._super(me.Sprite, "init", [x, y, sprite]);

    // Properties must be set in the init method only!
    this.active = yes;
  },
 
  // Draw
  draw: function(renderer) {
    // Draw only if is active
    if (this.active)
       this._super(me.Sprite, "draw", [renderer]);
  }
});
At first this change can scare, but the benefits with increased performance and reduced memory consumption compensate. For a more detailed explanation of the great new inheritance pattern, see the post about Jay Inheritance.

For better performance, the me.Renderable, me.Rect, me.PolyShape and me.Ellipse now requires (x,y) integer values over a me.Vector2d instance:

// Before melonJS 1.1
var myRenderable = new me.Renderable(new me.Vector2d(20, 50), 200, 300);
var myRect = new me.Rect(new me.Vector2d(30, 60), 150, 50);

// After melonJS 1.1
var myRenderable = new me.Renderable(20, 50, 200, 300);
var myRect = new me.Rect(30, 60, 150, 50);
The me.AnimationSheet now requires x, y and a settings hash to its constructor:

// Before melonJS 1.1
var animation = new me.AnimationSheet(10, 20, me.loader.getImage("sprite"), 64, 32);

// After melonJS 1.1
var animation = new me.AnimationSheet(10, 20, {
    image: me.loader.getImage("sprite"),
    spritewidth: 64,
    spriteheight: 32
});

Come Get Some


For a faster execution time, the me.Entity (me.ObjectEntity in the previous version) has been redesigned using a composition approach (lowering the amount of object properties), instead of inheritance based design used in the previous version. In this way, the me.Entity has a new me.Body child object that will hold all physic and collision properties and functions.

// Before melonJS 1.1
var entity = me.ObjectEntity.extend({
  // Constructor
  init: function(x, y, settings) {
    this.parent(x, y, settings);

    // Set the gravity
    this.gravity = 2;

    // Set the velocity
    this.setVelocity(5, 8);
  },

  // Update logic
  update: function(dt) {
      // Apply gravity
      this.vel.y += this.gravity;      

      // Update entity Position
      this.updateMovement();

      this.parent(dt);
      return true;
  }
});

// After melonJS 1.1
var entity = me.Entity.extend({
  // Constructor
  init: function(x, y, settings) {
    this._super(me.Entity, "init", [x, y, settings]);

    // Set the gravity
    this.body.gravity = 2;

    // Set the velocity
    this.body.setVelocity(5, 8);
  },

  // Update logic
  update: function(dt) {
      // Apply gravity
      this.body.vel.y += this.body.gravity;     

      // Update entity Position
      this.body.update();

      this._super(me.Entity, "update", [dt])
      return true;;
  }
});
Thus, the following functions have been renamed or assigned through the me.Body child object:

// Physics Old Code -> Physics New Code
this.accel -> this.body.accel
this.canBreakTile -> this.body.canBreakTile
this.disableTopLadderCollision -> this.body.disableTopLadderCollision
this.falling -> this.body.falling
this.gravity -> this.body.gravity
this.jumping -> this.body.jumping
this.maxVel -> this.body.maxVel
this.onladder -> this.body.onladder
this.onslope -> this.body.onslope
this.shapes -> this.body.shapes
this.vel -> this.body.vel
this.addShape -> this.body.addShape
this.getShape -> this.body.getShape
this.setShape -> this.body.setShape
this.setFriction -> this.body.setFriction
this.setMaxVelocity -> this.body.setMaxVelocity
this.setVelocity -> this.body.setVelocity
this.collisionMap -> this.body.collisionMap

// Collision Old Code -> Collision New Code
this.updateMovement() -> this.body.update()
me.game.world.collide(this) -> me.collision.check(this, true, this.collideHandler.bind(this), true)
The melonJS 1.1 has now a full SAT collision algorithm, for polygon collision detection and more accurate collision response. The previous collision check function has therefore been deprecated and replaced by a new me.collision.check function. Refined collision filtering is now possible through the use of me.body.setCollisionMask function (that defines what should collide with what).

// Before melonJS 1.1
var entity = me.ObjectEntity.extend({ 
    init: function(x, y, settings) {
        // Call the constructor
        this.parent(x, y , settings);

        // Default velocity
        this.setVelocity(3, 15);
    },
 
    // Update logic
    update: function(dt) {
        // Change velocity
        this.vel.x -= this.accel.x * me.timer.tick;
    
        // Check for collision with environment
        this.updateMovement();
  
        // Check for collision with enemies or objects
        var res = me.game.world.collide(this);

        if (res) {
            // Change velocity
            this.vel.y -= this.maxVel.y * me.timer.tick;

            // Change position
            this.pos.x -= 20;
        }
  
        this.parent(dt);
        return true;
    },
 
    // Collision callback
    onCollision: function() {
        // Disable collision
        this.collidable = false;
  
        // Remove from game
        me.game.world.removeChild(this);
    }
});

// After melonJS 1.1
var entity = me.Entity.extend({   
    init: function(x, y, settings) {
        // Call the constructor
        this._super(me.Entity, 'init', [x, y , settings]);
       
        // Default velocity
        this.body.setVelocity(3, 15);  
       
        // Set the collision callback function
        this.body.onCollision = this.onCollision.bind(this);          
    },
   
    // Update logic
    update: function(dt) {
        // Change velocity      
        this.body.vel.x -= this.body.accel.x * me.timer.tick;
            
        // Check for collision with environment
        this.body.update();
       
        // Check for collision with enemies or objects
        me.collision.check(this, true, this.collideHandler.bind(this), true);
           
        this._super(me.Entity, "update", [dt]);
        return true;       
    },
   
    // Collision handler
    collideHandler: function(response) { 
        // Change velocity    
        this.body.vel.y -= this.body.maxVel.y * me.timer.tick;                 

        // Change position
        this.pos.x -= 20;

        // Update the entity bounds since we manually changed the position
        this.updateBounds();
    },
   
    // Collision callback
    onCollision: function(res, obj) {
        // Disable collision
        this.body.setCollisionMask(me.collision.types.NO_OBJECT);
       
        // Remove from game
        me.game.world.removeChild(this);
    }
});
In preparation to support WebGL, the me.video is now agnostic of rendering target, by adding the me.CanvasRenderer object,  passing it to the draw calls of all the objects added to the world container and other subsequent containers.

// Before melonJS 1.1
draw: function(context) {
    // Set the context color
    context.fillStyle = "#FFFFFF";

    // Draw a simple rectangle
    context.fillRect(this.pos.x, this.pos.y, this.width, this.height);
}

// After melonJS 1.1 - Getting directly the context
draw: function(renderer) {
    // Get the renderer context
    var context = renderer.getContext();
   
    // Set the context color
    context.fillStyle = "#FFFFFF";
   
    // Draw a simple rectangle
    context.fillRect(this.pos.x, this.pos.y, this.width, this.height);
}

// After melonJS 1.1 - Using me.CanvasRenderer methods (recommended)
draw: function(renderer) {
    // Draw a simple rectangle, using the integrated function
    renderer.fillRect(this.pos.x, this.pos.y, this.width, this.height, "#FFFFFF");
}

Work Done


After the completion of the steps above, you must run the game performing various tests to detect possible adjustments or fixes to be made. For possible questions, use the melonJS Forum or analyze directly the source code on GitHub Repo.

Even with the extra work, the upgrade compensates for the awesome collision detection, fast inheritance, bug fixes and performance enhancements, in addition to being better prepared for the next melonJS version (in development), with the replacement of the current limited tile collision layer by a shape based collision layer and the probable addition of WebGL support! And you can enjoy to review some "obscure" or "hackish" code, using the best practices you acquired in recent times or to implement new features in your game.

0 comentários :