Building a Circular Tile Map World

Hi peeps,

Today I want to talk about an alternative world view, which I have kind of fallen in love with. Have you ever played games like God Finger (sadly no longer available), and the recently launched Reus? They feature a nice little round world that you can rotate and place various objects etc. It makes for a very cute little environment.

You can play an example of what we’ll be creating at Round World:
ImageThe code can be downloaded from here.

For this tutorial we’ll be focusing on the level code, but you can find the rest at the above link if you want to sneak a peek at the player.js and camera.js files. You can also refer to my post lerping camera for a description of the camera implementation.

Just a small note, this might not be the most efficient or elegant implementation. At the moment I’m prototyping the idea, so there may be better ways to go about it. However, there is a lack of information or tutorials on this kind of game view so I thought, why not share my dev story. 😉

This is what we’ll be covering:

  • Create a rotating world.
  • Add the ability to build and adjust the terrain (slightly).

Of course we have a player and a camera as well, but this post is going to be massive, so if you have time, have a dig in the files. Once you know what the level class is doing, then you should be able to work out the rest (if not send me a query via the comments and I’ll try to answer it for you).

I’m using Javascript and Canvas again (because I love them dearly), and we’ll be relying on the context.arc() draw method to create a little rotating world. A small note, you could use bezier curves which would be preferable because then you can really tweak the geometry of the world in more interesting ways. However, it’s a bit complicated for this tutorial, but keep it in mind for later enhancements.

Setting up

The main code that handles the world is the level object. Here we update the rotation of the world based on player input, draw the world itself etc.  The level object is defined as follows:

 
function Level(Game){
    // Get a reference to our Game singleton
    this.game = Game;

    // Set the world position on the screen
    this.x = (this.game.canvas.width * 0.5);
    this.y = (this.game.canvas.height);

    // Store the state of the level. Build is the default
    this.state = {
        _current: 0,
        BUILD: 0,
        TERRAFORM: 1
    }

    // Event Handlers
    // Sets the state to Build mode.
    $('#build').click(function(e){
        Game.level.state._current = Game.level.state.BUILD;
    });

    // Sets the state to Terraform mode.
    $('#terraform').click(function(e){
        Game.level.state._current = Game.level.state.TERRAFORM;
    });

};

The first method is an Init() function to set up all our variables:

Level.prototype.Init = function(width, height){
    // Arbitrary sizes, tweak as necessary
    this.width = 32;
    this.height = 64;

    // Store the full rotation value – small optimisation
    this.threesixty = Math.PI * 2;

    // Representation of our world.
    this.world_array = [0,0,0,0,0,0,0,0,0,0];

    // individual heights of each segment
    this.heightmap = [40,-10,-20,20,10,5,0,10,20,10];

    // An array to store building objects.
    this.level_objects = [];

    // The size of our world
    this.radius = (this.world_array.length) * this.width;

    // How big each tile should be
    this.segment = (this.threesixty/this.world_array.length);

    // An offset to compensate for the canvas context drawing order.
    this.offset = (Math.PI*3/2);

    // Amount of rotation (obviously)
    this.rotation = 0;

    // Set the coodinates of the world.
    this.x = (this.game.canvas.width * 0.5);
    this.y = (this.game.canvas.height);

    // An index in to the world_array
    this.tileID = 0; 

    // Add a texture
    this.texture = new Image();
    this.texture.src = "img/dirt2.jpg";
};

Our world is represented by a one dimensional array. This defines the size of the world and I’m also going to use it to mark tiles as occupied for buildings created by the player.

   this.world_array = [0,0,0,0,0,0,0,0,0,0];

Next we have a heightmap used to control the height of each segment, I’ve stuck some random values in to illustrate, but this could be done with Math.random() adding a spot of procedural level generation.

    this.heightmap = [40,-10,-20,20,10,5,0,10,20,10];

To calculate the size of our world we simply multiply the length of the world_array by a width value. I chose 32px just for kicks, for a fairly good sized world.

    this.radius = this.world_array.length * this.width;

Next up is a bit more math. First, you know a full circle can be represented by Math.PI * 2 right? And a three quarter circle can be represented by Math.PI * 3/2? Cool. Why do I ask? Well for some reason the context.arc() method draws the circle starting from the right and moving clockwise (as a default). So our world_array will start from 90 degrees on the right as I’ve shown in red (I only went up to 7 for the image so assume the world is 7 in length for this example).
world

This causes a slight problem because for simplicity I would like to have the beginning of the world_array represented by the top of the circle (12 o’clock) where the player will be standing. And so we want the world_array 0 position to be placed at the top of the world as the starting point (labelled in blue). That’s what the offset variable is for and we’ll see it again a bit later when we handle mouse input.

    
    this.segment = this.threesixty / this.world_array.length;
    this.offset = Math.PI * 3/2;

Next the rotation variable will store the amount the world has been rotated by the player based on their keypress (this is in the player.js file) .

    this.rotation = 0;

Then we set the location of the world to the middle of the screen along the x-axis and the centre of the y-axis below the bottom of the screen, and finally initialise the tileID attribute to 0. We also define a texture that can be used later to make the world look a bit more world like.

    this.x = this.game.canvas.width * 0.5;
    this.y = this.game.canvas.height;

    this.tileID = 0;

    this.texture = new Image();
    this.texture.src = "img/dirt.jpg";

Updating the world

So now we have set up our world, lets do stuff to it. First we’ll detect if the player has pressed either left or right and update the rotation with the velocity along the x-axis, which we multiply by delta time (dt) to keep things running at the same rate across platforms.

This snippet is contained in the player.js file.

Player.prototype.Move = function(dt){
    if(this.left){
        this.rotation += this.vx * dt;
    }
    if(this.right){
        this.rotation -= this.vx * dt;
    }
};

There is a little more than that involved for the player in terms of position and rendering, but as I mentioned, let’s keep bashing out the level stuff.

To update the world, we want to check for a few things:

  1. Check the rotation of the player.
  2. Check for player input for placing buildings and terraforming.
  3. Calculate the tile location to place the building.

First, we check the rotation of the player with a checkRotation() function which resets the rotation of the world to 0 degrees once we’ve travelled the full circumference of the circle. This keeps the range of rotation to between 0 and Math.PI * 2 (or 360 degrees):

Level.prototype.checkRotation = function(angle){
    if(angle < -this.threesixty){
        angle = this.threesixty;
    }
    if(angle > this.threesixty){
        angle = 0;
    }
    return angle;
};

The update function handles the input from the player by converting it to a tile position, and handling the state of the level, i.e. whether mouse clicks should be interpreted as a build or a terraform action.  The parameters xScroll, yScroll, and zoom come from the games camera (check out the camera.js file).

Level.prototype.update = function(dt, xScroll, yScroll, zoom){
    this.rotation = this.checkRotation(this.game.player.rotation); 
    if(this.game.mouse.clicked){
        this.game.mouse.clicked = false;

        if(this.state._current == this.state.BUILD){
            this.Build(this.tileID);
        }

        if(this.state._current == this.state.TERRAFORM){
            this.Terraform(this.tileID);
        }
    }
    this.tileID = this.getMouseAngle(this.game.mouse, xScroll, yScroll, zoom);
};

The getMouseAngle() function returns an index into the world_array and needs to offset the mouse position so that we get the correct tile ID, where the top of the circle represents world_array[0]. I found this bit the trickiest (see my Game Dev Stackoverflow post), and it’s the nuts and bolts of the whole game as far as interacting with the world is concerned. I’ve commented the code rather than split it up as I’ve done above.

Level.prototype.getMouseAngle = function(mouse, xScroll, yScroll, zoom){
    // Calculate the arctan between the mouse and the level x and y at the world center.
    var dx = mouse.x - (this.x - xScroll);
    var dy = mouse.y - ((this.y - yScroll) * zoom);
    var arctan = Math.atan2(dy, dx); 

    // Reset angle to 0 on full rotation
    if(dy < 0){
        angle = (Math.PI * 2) + arctan;
    } else { 
        angle = arctan;
    }

    // Deduct the world rotation
    angle -= this.rotation;

    // Get original tile position offset tile position and calculate final tile.
    // N.B segment = 360/worldsize.

    var orig_tile = angle/this.segment;
    var offset_tile = this.offset/this.segment;
    var final_tile = Math.floor(offset_tile - orig_tile);

    // Adjust tile ID according to the offset.
    var tile = final_tile;
    if (tile < 0){
        tile = (tile + this.world_array.length);
    } 
    if(tile > 0){
        tile = Math.abs((this.world_array.length-1) - tile);
    }
    if(this.world_array.length + final_tile == this.world_array.length){
        tile = this.world_array.length - 1;
    }

// Return the index to the world_array
    return tile; 
};

Phew, that was a long one. Once we know the tile ID the world is our oyster!! Let’s do some building, here I’ve just commented the code again to save on waffle:

Level.prototype.Build = function(tileID){    
    var x, y, angle, building;

    // Check if this tile is available
    if(this.world_array[tileID] == 0){
        // Calculate the angle of the building for drawing.
        angle = tileID * this.segment + (this.segment * 0.5);
        x = (tileID * this.segment);
        // Set the initial position of the building to the base height of the level
        y = this.radius;

        // Create the building object and pass it the necessary parameters
        building = new Building(tileID, x, y, angle, 'red');

        // Add the building to the world_array
        this.world_array[tileID] = building;
    }
};

And some very boring terraforming:

Level.prototype.Terraform = function(tileID){
    // Reduce the height of the segment
    this.heightmap[tileID] -= 10;

    // Reduce the height of the building if there is one
    if(this.world_array[tileID] != 0){
        this.world_array[tileID].y -= 10; 
    }
};

Terra-forming can be vastly improved, here we’re just reducing the height of the segment to demonstrate it works.

Drawing the world

Let there be drawing!! This is a fairly important function. We need to transform the canvas context to make everything relative to the camera scroll coordinates and the zoom level, as well as handling the centering of world on the screen (canvas normally renders everything in relation to the top-left corner of the screen).

Level.prototype.draw = function(dt, context, xScroll, yScroll, zoom){
    context.save();
    context.setTransform(
        zoom, // ScaleX
 of the camera zoom
        0, // SkewX
        0, // SkewY
        zoom, // ScaleY
 of the camera zoom
        this.x - xScroll, // TranslateX: center of the screen along the x-axis
        (this.y - yScroll) * zoom // TranslateY: bottom of the screen along the y-axis
    );

    // Rotate everything from this point onwards according to the current world rotation set by the player.
    context.rotate(this.rotation);

    // Wrapper function to draw the buildings and world
    this.drawBuildings(dt, context, xScroll, yScroll, zoom);
    this.drawWorld(dt, context, xScroll, yScroll, zoom);

    context.restore();
};

I split the drawing out to make it more readable. Let’s draw the buildings first so that the world obscures the fact that the building bases are not curved (if you’re not sure what I mean, move the drawBuildings() function after the drawWorld() function:

Level.prototype.drawBuildings = function(dt, context, xScroll, yScroll, zoom){
    // Iterate through the world_array and call the draw function for each building.
    for (var i = 0; i < this.world_array.length; i++) {
        if(this.world_array[i]){
            this.world_array[i].draw(dt, context, xScroll, yScroll, zoom);
        }
    }
};

The context.arc() method is used to do the drawing of the tiles in pretty much the same way as a 2D tilemap, it’s just we’re drawing sections of a circle rather than square tiles.  The parameters for the .arc() method are as follows:

    context.arc(x, y, radius, startAngle, endAngle, anticlockwise)

We iterate through the world_array and draw each entry in the array as a slice of the world according to its start and end angles, and it’s size (radius), again making sure to offset the angle.  We’re using the stroke() method to draw a thick green line to represent grass, and use a dirt texture as the fillStyle.

Level.prototype.drawWorld = function(dt, context, xScroll, yScroll, zoom){
    // Draw world segments
    var tile;

    // Some basic grass
    context.strokeStyle = "green";
    context.lineWidth = 7;

    // Draw the segments
    for(tile = 0; tile < this.world_array.length; tile++){
        context.beginPath();
        // Remember we set the x and y position earlier with setTransform()
        //arc(x, y, radius, startAngle, endAngle, anticlockwise)

        context.arc(
            0, // x-axis
            0, // y-axis
            this.radius + this.heightmap[tile], // the height of the tile 
            this.offset + (tile * this.segment), // Angle From beginning of this segment
            this.offset + ((tile + 1) * this.segment), // Angle To the beginning of next segment
            false // Draw clockwise
        );
        context.stroke();
        context.lineTo(0, 0);

        // Fill the arcs with a dirt texture
        context.fillStyle = context.createPattern(this.texture, 'repeat');
        context.fill();
        context.closePath();
    }
};

Adding buildings

So far you’ve been pretty patient, since I haven’t actually given you the building object code yet, even though we mentioned it way up at the top there. Here you go:

var Building = function(tileID, x, y, rotation, color){
    // Record it's tile ID
    this.tileID = tileID || 0;
    this.color = color || "black";

    // Set x and y positions
    this.x = x || 0;
    this.y = y || 0;

    // Get random height and width
    this.height = Math.floor(64 + Math.random() * 256);
    this.width = Math.floor(64 + Math.random() * 96);
    // Set it's rotation
    this.rotation = rotation || 0;

};

Building.prototype.update = function(dt){
   // Nothing here, but you could add some mouse collision to allow the player 
   // to select the building to show an inventory or something.
};

Building.prototype.draw = function(dt, context, xScroll, yScroll){
    context.save();
    context.fillStyle = this.color
    // Rotate the building to the correct orientation
    context.rotate(this.rotation);
    context.fillRect(
        -this.width * 0.5, // center it in the middle of the segment 
        (-this.y - this.height) + 16, // Sink the building down a bit to cover the base 
        this.width, 
        this.height
    );
    context.restore();
};

And there you have it. The level code as promised.  If you read this far then as I always say, kudos and grazie!

Wrapping up

So that is pretty much everything you need to know about updating and drawing a round world. A few applications I can think of are God games, strategies, survival, space shooters etc., but it’s really just up to your imagination.

Some ways it can be made more interesting include:

  1. Draw segments as bezier curves to allow finer control of the landscape.
  2. Make it multiplayer, I’ll visit your world if you visit mine (alla Chefsville).
  3. Add some trees and animals (oooo so cute).
  4. Have a proper weather system (unlike the naff clouds I added).
  5. Add day and night cycle (Done, but a bit of a sudden transition – needs more work).
  6. I’m actually out of ideas, but I’m sure more will follow hopefully.

Until next time, thanks for reading! And good luck with your creations!

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