47
\$\begingroup\$

So, I'm making a HTML5 RPG just for fun. The map is a <canvas> (512px width, 352px height | 16 tiles across, 11 tiles top to bottom). I want to know if there's a more efficient way to paint the <canvas>.

Here's how I have it right now.

How tiles are loaded and painted on map

The map is being painted by tiles (32x32) using the Image() piece. The image files are loaded through a simple for loop and put into an array called tiles[] to be PAINTED on using drawImage().

First, we load the tiles...

enter image description here

and here's how it's being done:

// SET UP THE & DRAW THE MAP TILES
tiles = [];
var loadedImagesCount = 0;
for (x = 0; x <= NUM_OF_TILES; x++) {
  var imageObj = new Image(); // new instance for each image
  imageObj.src = "js/tiles/t" + x + ".png";
  imageObj.onload = function () {
    console.log("Added tile ... " + loadedImagesCount);
    loadedImagesCount++;
    if (loadedImagesCount == NUM_OF_TILES) {
      // Onces all tiles are loaded ...
      // We paint the map
      for (y = 0; y <= 15; y++) {
        for (x = 0; x <= 10; x++) {
          theX = x * 32;
          theY = y * 32;
          context.drawImage(tiles[5], theY, theX, 32, 32);
        }
      }
    }
  };
  tiles.push(imageObj);
}

Naturally, when a player starts a game it loads the map they last left off. But for here, it an all-grass map.

Right now, the maps use 2D arrays. Here's an example map.

[[4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 1, 1, 1, 1], 
[1, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 1, 1, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 13, 13, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 13, 13, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 13, 13, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 1, 1, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 13, 13, 13, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 13, 11, 11, 11, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 1, 1, 1, 1, 1, 1, 1, 13, 13, 13, 13, 13, 1], 
[1, 1, 1, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 1, 1, 1]];

I get different maps using a simple if structure. Once the 2d array above is return, the corresponding number in each array will be painted according to Image() stored inside tile[]. Then drawImage() will occur and paint according to the x and y and times it by 32 to paint on the correct x-y coordinate.

How multiple map switching occurs

With my game, maps have five things to keep track of: currentID, leftID, rightID, upID, and bottomID.

  • currentID: The current ID of the map you are on.
  • leftID: What ID of currentID to load when you exit on the left of current map.
  • rightID: What ID of currentID to load when you exit on the right of current map.
  • downID: What ID of currentID to load when you exit on the bottom of current map.
  • upID: What ID of currentID to load when you exit on the top of current map.

Something to note: If either leftID, rightID, upID, or bottomID are NOT specific, that means they are a 0. That means they cannot leave that side of the map. It is merely an invisible blockade.

So, once a person exits a side of the map, depending on where they exited... for example if they exited on the bottom, bottomID will the number of the map to load and thus be painted on the map.

Here's a representational .GIF to help you better visualize:

enter image description here

As you can see, sooner or later, with many maps I will be dealing with many IDs. And that can possibly get a little confusing and hectic.

The obvious pros is that it load 176 tiles at a time, refresh a small 512x352 canvas, and handles one map at time. The con is that the MAP ids, when dealing with many maps, may get confusing at times.

My question

  • Is this an efficient way to store maps (given the usage of tiles), or is there a better way to handle maps?

I was thinking along the lines of a giant map. The map-size is big and it's all one 2D array. The viewport, however, is still 512x352 pixels.

Here's another .gif I made (for this question) to help visualize:

enter image description here

Sorry if you cannot understand my English. Please ask anything you have trouble understanding. Hopefully, I made it clear. Thanks.

\$\endgroup\$
1
  • 16
    \$\begingroup\$ The effort put into this question with the graphics and all deserves an upvote. :) \$\endgroup\$
    – Tapio
    Commented Jul 10, 2012 at 6:08

7 Answers 7

5
+150
\$\begingroup\$

Edit: Just saw that my answer was based on your code but didn't actually answer your question. I kept the old answer in case you can use that information.

Edit 2: I have fixed 2 issues with the original code: - The +1 addition in the calculation of end x and y was by mistake inside the brackets, but it needs to be added after the division. - I forgot to validate the x and y values.

You could simply have the current map in memory and load the surrounding maps. Imagine a world that consists of a 2d array of single levels of the size 5x5. The player starts in field 1. As the top and the left bounds of the level are on the edge of the world, they do not need to be loaded. So in that case level 1/1 is active and level 1/2 and 2/1 are loaded... If the player now moves to the right, all levels (besides the one your moving to) are unloaded and the new surroundings are loaded. Means level 2/1 is now active, 1/1, 2/2 and 3/1 are now loaded.

I hope this gives you an idea how it could be done. But this approach won't work very good, when every level has to be simulated during the game. But if you can freeze unused levels this should work fine.

Old Answer:

What I do when rendering the level (also tile based) is to calculate which items from the tile array intersect with the viewport and then I only render those tiles. Like that you can have big maps but only need to render the portion on screen.

//Mock values
//camera position x
//viewport.x = 233f;
//camera position y
//viewport.y = 100f;
//viewport.w = 640
//viewport.h = 480
//levelWidth = 10
//levelHeight = 15
//tilesize = 32;

//startX defines the starting index for the x axis.
// 7 = 233 / 32
int startX = viewport.x / tilesize;

//startY defines the starting index
// 3 = 100 / 32
int startY = viewport.y / tilesize;

//End index y
// 28 = (233 + 640) / 32 + 1
int endX = ((viewport.x + viewport.w) / tilesize) + 1;

//End index y
// 19 = (100 + 480) / 32 + 1
int endX = ((viewport.x + viewport.w) / tilesize) + 1;

//Validation
if(startX < 0) startX = 0;
if(startY < 0) startY = 0;
//endX is set to 9 here
if(endX >= levelWidth) endX = levelWidth - 1;
//endX is set to 14 here
if(endY >= levelHeight) endY = levelHeight - 1;

for(int y = startY; y < yEnd; y++)
    for(int x = startX; x < xEnd; x++)
          [...]

Note: The code above is not tested but it should give an idea what to do.

Following a basic example for a viewport representation:

public class Viewport{
    public float x;
    public float y;
    public float w;
    public float h;
}

A javascript representation would look like

<script type="text/javascript">
//Declaration
//Constructor
var Viewport = function(xVal, yVal, wVal, hVal){
    this.x = xVal;
    this.y = yVal;
    this.w = wVal;
    this.h = hVal;
}
//Prototypes
Viewport.prototype.x = null;
Viewport.prototype.y = null;
Viewport.prototype.w = null;
Viewport.prototype.h = null;
Viewport.prototype.toString = function(){
    return ["Position: (", this.x, "/" + this.y + "), Size: (", this.w, "/", this.h, ")"].join("");
}

//Usage
var viewport = new Viewport(23, 111, 640, 480);
//Alerts "Position: (23/111), Size: (640/480)
alert(viewport.toString());
</script>

If you take my code example, you simple have to replace the int and float declarations with var. Also make sure that you use Math.floor on those calculations, that are assigned to the int based values, to get the same behavior.

One thing I would consider (though I'm not sure if this matters in javascript) is to put all tiles (or as many as possible) in one big texture instead of using many single files.

Here is a link explaining how to do it: http://thiscouldbebetter.wordpress.com/2012/02/25/slicing-an-image-into-tiles-in-javascript/

\$\endgroup\$
9
  • \$\begingroup\$ Just to clarify... viewport.x would be predined as "531" and viewport.y would be "352" and tilesize = "32". And such..? \$\endgroup\$
    – test
    Commented Jul 10, 2012 at 21:06
  • \$\begingroup\$ If you mean that the position of the camera is 531 then yes. I added some comments to the code above. \$\endgroup\$ Commented Jul 10, 2012 at 23:11
  • \$\begingroup\$ I'm using JavaScript... I am Java is near identical to this. \$\endgroup\$
    – test
    Commented Jul 11, 2012 at 3:24
  • \$\begingroup\$ Yeah, you just have to make a js based viewport class (or you could just create 4 variables x, y, w and h). Also you have to make sure to floor (Math.floor) those calculations, that are assigned to the int values. I added a code example, how the viewport class would look like. Just make sure, that you put the declaration before the usage. \$\endgroup\$ Commented Jul 11, 2012 at 7:53
  • \$\begingroup\$ Just saying that you put 640, 480 .. but it can work with 512, 352 right? \$\endgroup\$
    – test
    Commented Jul 11, 2012 at 12:27
3
\$\begingroup\$

Storing maps as tiles is useful for allowing you to re-use assets and re-designing the map. Realistically though it doesn't really offer much benefit to the player. Plus, you're then re-drawing every single tile each time. Here's what I would do:

Store the whole map as one big array. There's no point having maps and submaps, as well as potentially sub-sub-maps (if you're entering buildings, for instance). You're loading it all anyway and this keeps everything nice and simple.

Render the map all at once. Have an off-screen canvas that you render the map to and then use this in your game. This page shows you how to do it with a simple javascript function:

var renderToCanvas = function (width, height, renderFunction) {
  var buffer = document.createElement('canvas');
  buffer.width = width;
  buffer.height = height;
  renderFunction(buffer.getContext('2d'));
  return buffer;
};

This is assuming your map isn't going to change much. If you wanted to render sub-maps in-place (for example, the inside of a building) you just render that to a canvas too and then draw it on top. Here's an example of how you can use this to render your map:

var map = renderToCanvas( map_width*32, map_height*32, renderMap );

var MapSizeX = 512;
var MapSizeY = 352;

var draw = function(canvas, mapCentreX, mapCentreY) {
  var context = canvas.getContext('2d');

  var minX = playerX - MapSizeX/2;
  var minY = playerY - MapSizeY/2;

  context.drawImage(map,minX,minY,MapSizeX,MapSizeY,0,0,MapSizeX,MapSizeY);
}

This will draw a small portion of the map centred around mapCentreX, mapCentreY. This will offer you smooth scrolling around the entire of the map, as well as sub-tile movement - you can store the player position as 1/32ths of a tile, so you can get smooth movement across the map (Clearly if you want to move tile-by-tile as a stylistic choice, you can just increment in chunks of 32).

You can still chunk up your map if you like, or if your world is huge and wouldn't fit in memory on your target systems (bear in mind that even for 1 byte per pixel, a 512x352 map is 176 KB) but by pre-rendering your map like this you'll see a large performance gain in most browsers.

What this gives you is the flexibility of reusing tiles across the world without the headache of editing one big image, but also allowing you to run this quickly and easily and only consider one 'map' (or as many 'maps' as you like).

\$\endgroup\$
2
\$\begingroup\$

In all the implementations I have seen tile maps are often generated as one big image and then "chunked" into smaller pieces.

https://gamedev.stackexchange.com/a/25035/10055

These chunks are usually in powers of 2, so they fit in memory optimally.

\$\endgroup\$
1
\$\begingroup\$

An easy way to keep track of the IDs would be to simply use another 2d array with them in: by only maintaining x,y on that "meta array", you can easily look up the other IDs.

That being said, here's Google's take on 2D tiled canvas gaming (well, actually HTML5 multiplayer, but tile engine is also covered) from their recent I/O 2012: http://www.youtube.com/watch?v=Prkyd5n0P7k It also has source code available.

\$\endgroup\$
1
\$\begingroup\$

Your idea to load the entire map into an array first, is an efficient way, but it will be easy to get JavaScript Injection'd, anyone will be able to know the map and there goes the adventure.

I'm working on a RPG game web-based too, the way I load my map is via Ajax from a PHP page, that loads the map from the database, where it says the X, the Y, the Z position and the vlaue of the tile, draw it, and finally the player move.

I'm still working on it to make it more efficient, but the map loads fast to enough to keep it this way by now.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Can I see a demo? \$\endgroup\$
    – test
    Commented Jul 12, 2012 at 23:21
1
\$\begingroup\$

No, a array is the most efficient way of storing a tile map.

There might be a few structures which require a little less memory but only at the expense of much higher costs for loading and accessing the data.


What needs some consideration however is when you are going to load the next map. Given that the maps are not especially big, the thing that actually will take the longest is waiting for the server to deliver the map. The actual loading will only take a few milliseconds. But this waiting can be done asynchronous, it doesn't have to block the flow of the game. So the best thing is not loading the data when needed but before. The player is in one map, now load all the adjacent maps. Player moves in the next map, load all adjacent maps again, etc., etc. The player won't even notice that your game is loading in the background.

This way you can also create a illusion of a border less world by also drawing the adjacent maps too, in that case you need to load not only the adjacent maps but also those adjacent of those, though.

\$\endgroup\$
0
\$\begingroup\$

I'm not sure why you think: As you can see, sooner or later, with many maps I will be dealing with many IDs. And that can possibly get a little confusing and hectic.

LeftID will always be -1 of the currentID, the rightID will always be +1 of the currentID.

UpID will always be -(total map Width) of the current ID and downID will always be +(total map width) of the current ID with the exception of zero meaning you've hit the edge.

A single map can be broken into many maps, and are saved sequentially with mapIDXXXX where XXXX is the id. That way, there isn't anything to be confused about. I've been to your site and it seems, I could be wrong, that the problem is down to your map editor imposing a size limitation which is hindering your automation to break down a large map into multiple small ones on save and this limitation is probably restricting a technical solution.

I've written a Javascript map editor which scrolls in all direction, 1000 x 1000 (I wouldn't recommend that size) tiles and it still moves as good as 50 x 50 map and that is with 3 layers including parallax scrolling. It saves and read in json format. I'll send you a copy if you say you don't mind an email approximately 2meg. It is meant to be on github but I haven't got any decent (legal) tileset which is why I haven't bothered to put it on there yet.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .