In any 2D game, you have to know what order to draw your sprites in. You usually draw from the back of the scene to the front, with the earlier objects being covered by the later ones. This is the standard Painter's Algorithm used to represent depth on a canvas (digital or otherwise).
The most straightforward way to keep track of this is by putting all your objects into one big array, sorted by their depth. So in the above scene, your array would look something like: [Mountain, Ground, Tree1, Tree2, Tree3, ... ]
The Problem With a Single Big Array
The problem with one central display list is that there's no easy way to find out which objects are on screen at any given moment. To do that, you need to loop through the entire array and check every single object. This mostly becomes an issue if you have a large game world where many objects exist outside the screen and shouldn't be rendered or updated.
A spatial hash is a way of storing your objects to avoid this problem. The neat thing about using a hash is it always takes a constant time to figure out which objects are on screen, regardless of how huge the game world might be!
Now most game engines won't actually let you play around with how they structure their objects internally, but if you're programming in an environment where you're in control of the draw calls (such as your own OpenGL engine, a pure JavaScript game, or more open frameworks such as LÖVE), this could be something worth implementing.
The Alternative: Spatial Hashes
A spatial hash is just a hash table where each key is a 2D coordinate and the value is a list of game objects in that area.
Imagine that your world is split into a grid such that each object belongs to at least one cell. Here's how you would look up something at position (420,600) if you had this implemented in JavaScript:
var X,Y = 420,600; //Snap X and Y to the grid X = Math.round(X / CELL_SIZE) * CELL_SIZE; Y = Math.round(Y / CELL_SIZE) * CELL_SIZE; //Now check what elements are in that position spatialHash[X+","+Y] //this is a list of objects in that cell
It's that easy! You can immediately know what's in that position. The key is a string concatenation of the X and Y coordinates, but it doesn't have to be a string, nor does it need a comma in the middle; it just has to be unique for each pair of X and Y.
To see why this is so convenient, consider how you would get the objects at this position using one big array:
var X = 420; var Y = 600; for(var i=0;i<gameObjects.length;i++){ var dx = Math.abs(gameObjects[i].x - X); var dy = Math.abs(gameObjects[i].y - Y); if(dx < CELL_SIZE && dy < CELL_SIZE){ //Object is in the area! } }
We're checking every single object, even if most of them are very far away to begin with! This can greatly cripple your performance if you're doing many lookups like this and your gameObjects
array is huge.
A Concrete Example
If you're not yet convinced of how useful this is can be, here's a demo written in pure JavaScript where I try to render a million objects in the game world. In both cases, only the objects on screen are actually rendered.
Click to see the live demo running!
And the live spatial hash version.
The single array version is painfully slow. Even if you save which elements are on screen so you don't have to check every frame, you still have to check the whole array again when the camera moves, leading to severe choppiness.
Just changing the way we store our game objects can make all the difference between a smooth experience and an unplayable game.
Implementing a Spatial Hash
A spatial hash should be really easy to implement in any language (in fact, going from the first example to the second above only took an extra 30 lines of code!)
There are four steps to implementing this as your rendering system:
- Set up the hash table.
- Add and remove objects in the hash.
- Collect objects in any given area.
- Sort objects by depth before rendering them.
You can see a functional implementation in JavaScript on GitHub as a reference.
1. Set Up the Hash Table
Most languages have some sort of built-in hash table/map. Our spatial hash is just a standard hash table. In JavaScript you can just declare one with:
var spatialHash = {}; var CELL_SIZE = 60;
The only other thing to mention here is that you have some leeway with picking the cell size. In general, having your cells be twice as big as your average object seems to work well. If your cells are too big then you'll be pulling in too many objects with every lookup. If they're too small then you'll have to check more cells to cover the area you want.
2. Add and Remove Objects in the Hash
Adding an object to the hash is just a matter snapping it to a cell, creating the cell array if it doesn't exist, and adding it to that array. Here's my JavaScript version:
spatialHash.add = function(obj){ var X = Math.round(obj.x / CELL_SIZE) * CELL_SIZE; var Y = Math.round(obj.y / CELL_SIZE) * CELL_SIZE; var key = X + "," + Y; if(spatialHash[key] == undefined) spatialHash[key] = [] spatialHash[key].push(obj) }
There's a caveat, though: What if your object spans multiple cells, or is too big to fit in one cell?
The solution is just to add it to all the cells that it touches. This guarantees that if any part of the object is in view then it will be rendered. (Of course, then you also need to make sure you're not rendering these objects multiple times.)
I haven't implemented a remove function in my example, but removing an object is just a matter of taking it out of the array(s) it's a part of. To make this simpler, you can have each object keep a reference to which cells it belongs to.
3. Collect Objects in Any Given Area
Now here's the core of this technique: given an area on screen, you want to be able to get all the objects in there.
All you need to do here is start going through all the cells based on where your camera is in the game world, and collect all the sub-lists together into one array to render. Here's the relevant JavaScript snippet:
var padding = 100; //Padding to grab extra cells around the edges so the player doesn't see objects "pop" into existence. var startX = -camX - padding; var startY = -camY - padding; var endX = -camX + canvas.width + padding; var endY = -camY + canvas.height + padding; var onScreen = [] for(var X = startX; X < endX; X += CELL_SIZE){ for(var Y = startY; Y < endY; Y += CELL_SIZE){ var sublist = spatialHash.getList(X,Y) if(sublist != undefined) { onScreen = onScreen.concat(sublist) } } }
4. Sort Objects by Depth Before Rendering Them
You might have realized by now that giving up on the big display list idea also means you give up on the convenient depth sorting. We grab objects from our grid based on their location, but the array we get is not sorted in any way.
As a final step before rendering, we have to sort our array based on some key. I gave each object a depth value, and so I can do:
onScreen.sort(function(a,b){ return a.depth > b.depth })
Before finally rendering everything:
for(var i=0;i<onScreen.length;i++){ var obj = onScreen[i]; obj.update(); obj.draw(); }
This is one of the drawbacks to this method, that you have to sort what's on screen every frame. You can always speed this up by making sure all your sub-lists are sorted, so you can merge them while you concatenate to maintain the order.
That's it! You should now have a (hopefully much faster) working rendering system!
Other Uses & Tips
This can be a really useful technique, but as I said in the introduction, you can only do this in a game engine or framework that gives you control over the draw calls. Still, there are things you can use spatial hashes for besides rendering. In fact, they're more commonly used to speed up collision detection (you can skip any collision checks for objects you know are far away or are not in the same cell).
Another technique that's similar to spatial hashes, but a little more complicated, is using a quadtree. Whereas a spatial hash is just a flat grid, a quadtree is more of a hierarchical structure, so you don't have to worry about cell size, and you can more quickly get all the objects in a given area without having to check every little cell.
In general, you should keep in mind that a spatial structure is not always going to be the best solution. It's ideal for a game that has:
- a large world with many objects
- relatively few objects on screen compared to the world size
- mostly static objects
If all of your objects are moving around all the time, you'll have to keep removing and adding them to different cells, which might incur a significant performance penalty. It was an ideal rendering system for a game like Move or Die (almost doubling the fps) since levels were made up of a lot of static objects, and the characters were the only things that moved.
Hopefully this tutorial has given you an idea of how structuring data spatially can be an easy way to boost performance, and how your rendering system does not always have to be a single linear list!