Isometric Heaven

Back again? Great, thanks for following our progress,… first time? Well, we’ll take you by the hand and lead you around our development/thought processes for Wormonger-Ultimate(!!!).

Last time we focused on rebuilding the worm from scratch and to find a good balance between size and speed.  This time round, we wanted to think about the game world. Our jam entry was based on the traditional top-down view, but could it work in isometric? We did some research and found this nice little game on the NES:

The snake movement is very smooth and free in Snake Rattle ‘n’ Roll, so clearly it’s been done well before. So, with that in mind off we went and coded up a prototype to test the feel. Here are the results (no collision code yet):

Demo: Wormonger V0.2

Nice! Feels kind of good to move around, especially along the diagonals, very satisfying, oooh yeah!  Right, so let’s take a general tour of how it was implemented.

Love Isometric <B

So along the way there were a few bugs and hiccups and what the $%@#!? moments,  but we’ve come away with a new grasp of the isometric projection system.

In terms of the code the only thing we needed to change from the previous version was the rendering of the tilemap, and the player. Looking at the tilemap code first, we’re using this little test tile for the demo (feel free to use – it’s so basic anyway):

groundWe generated the tile using Blender3D by referring to this nice short tutorial Isometric Tiles in Blender.

Coding it up (tilemap and player)

On to the coding bit… We first specify the isometric tile dimensions and a horizontal offset in order to position the tilemap in the middle of the screen.

function TilemapISO (width, height) {
    this.tile = {width: 64, height: 32};
    this.tilemap = [[]];
    this.tileset = new Image();
    this.tileset.src = './img/ground.png';
    this.offsetX = (canvas.width * 0.5);
    this.offsetY = 0;
};

We also have a little helper function to create a map for us, pretty basic, but could be augmented later i.e. for procedurally generating maps.

TilemapISO.prototype.getNew = function(width, height) {
    var row, col;
    for (row = 0; row < height; row++){
        for (var col = 0; col < width; col++){
             tilemap[row][col] = 1;
        }
    }
    return tilemap;
};

Now on to drawing. This is where all the iso-magic happens. To get the isometric coordinates we need the following formula:

isoX = (x - y)
isoY = (x + y)  /  2

The y coordinate is divided by 2 since the height (32px) of the tile is half the tile width (64px). You can also multiply by 0.5, since division is a tad slower than multiplication, so I tend to prefer multiplication as a simple, though somewhat early, optimisation.

TilemapISO.prototype.draw = function(dt, context) {
    var row, col, tileX, tileY;
    for (row = 0; row < this.tilemap_data.length; row ++) {
        for (col = 0; col < this.tilemap_data[0].length; col ++) {
            if(this.tilemap_data[row] && this.tilemap_data[row][col]) {
                tileX = (row - col) * this.tile.height;
                tileX += this.offsetX - (this.tile.width * 0.5);

                tileY = (row + col) * (this.tile.height * 0.5);
                tileY -= this.offsetY + (this.tile.height * 0.5);

                context.drawImage(
                    this.tileset_img,
                    Math.round(tileX), 
                    Math.round(tileY)
                );
            }
        }
    }
};

As you can see, we calculate the isometric view first, then offset the tile to keep the map in the middle of the screen.

Wormy wormy wormy (player)

We also need to amend the player update function, the segments behave as before (using inverse kinematics to calculate each segments position relative to each other – see our previous post). However, we need to update our movement code to figure out which direction we’ll be going in.  The worm in Wormonger moves in eight directions, achieved by setting the direction x and direction y variables based on each player direction flag, and then multiplying the result with the thrust value.

Player.prototype.update = function(dt, segment) {
    var dx = 0, dy = 0;
    if(this.up && this.right) {
        dy = -1;
    } else
    if(this.up && this.left) {
        dx = -1;
    } else
    if(this.down && this.right) {
        dx = 1;
    } else
    if(this.down && this.left) {
        dy = 1;
    } else
    if(this.left) { 
        dx = -1;
        dy = 1;
    } else
    if(this.right) {
        dx = 1;
        dy = -1;
    } else
    if(this.down) {
        dx = 1;
        dy = 1;
    } else
    if(this.up) { 
        dx = -1;
        dy = -1;
    }

    this.x += dx * this.thrust;
    this.y += dy * this.thrust;

    // update the head segment
    this.drag(this.segments[0], this.x, this.y);
    var i, segment;   
    for (i = 1; i < this.segments.length; i ++) {
            // update each segment of the body sans head
            segment = this.segments[i];
            this.drag(
                segment, 
                this.segments[i-1].x, 
                this.segments[i-1].y
            );
        }
    };

As you see there wasn’t too much of a difference from this update function and the one in the previous post. Next we project the segments from orthographic to isometric with the help of this little function I made earlier:

function toIso(object) {
    // convert object x and y to isometric
    return {
        x: parseInt(object.x - object.y), 
        y: parseInt((object.x + object.y) * 0.5)
    }
};

This function is run over each segment, and the isometric coordinates are passed to whatever it is we are drawing, in this case a simple circle.

Player.prototype.draw = function(dt, context) {
    var i, segment, iso;
    for (i = this.segments.length - 1; i >= 0; i--) {
        segment = this.segments[i];
        iso = toIso(segment);

        context.fillStyle = segment.color;
        context.beginPath();

        context.arc(
            iso.x, 
            iso.y, 
            segment.radius, 
            0, 
            2 * Math.PI
        );
        context.fill();
    }
};

And…. voila! That’s pretty much it, job done!  Couldn’t believe how straight- forward this would be (of course we’re just drawing, no interaction with the tile map or any sort of NPC, so our sunny disposition could soon change, we’ll see).

Wow, two blog posts in one day, afraid that’s it until next time, I’m pooped, …and I should be working!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s