Since we’ve been working on Project Zomboid, we’ve been for the first time introduced to the wonderful world of isometric game making.
This has been somewhat of a nightmare for Binky, seeing as he has to draw isometric tiles and worse, animate isometric characters in 5 directions.
It’s also been a bit of a coding challenge, as I’ve never touched an iso engine before and in fact had only vaguely considered the principals of it prior to making the game.
Since back at this point I was still considering flash for the game, I looked around at various isometric based engines and found a few, but they were all pretty simple. Few had multi-floor support, a must for our game, and generally they all seemed more simplistic and suited to games like Farmville more than an X-COM style engine.
As usual, I dove in quickly to get a feel of how the engine would work. My first bash at it revealed that to draw the isometric map in a way that drew all the tiles correctly, I had to draw the tiles like this:
In the first prototype of the engine, I did this for each z floor of the map, which seemed to work great. Each grid square could have one or more IsoObjects on it. A floor tile, for example, would be added first, and thus drawn first. Secondly, a wall segment if present, on either the west or north walls (tilting your head to the right)
Walls are never present on the south or east walls since they are north or west walls of the adjacent tiles further down the X, Y axis.
After a wall a piece of furniture on the tile could then be placed. This ensured the everything drew correctly and was in its proper place.
Next came adding support for moving objects to the engine. The tricky thing about doing this is that, usually, you maintain an object list for all updating entities in your game, which you iterate to update and draw them. But clearly for an iso engine, where the characters need to be drawn at a precise time, I would need something else.
So to solve this I introduced a list of moving objects for each GridSquare, in addition to the global movingobject list. Subsequent to an object’s update, I removed the object from it’s ‘previous’ grid-square, and added it to its ‘current’ grid-square, updating current and previous after this. So when it comes time to draw the gridsquare, in the correct order, it will then draw all characters who are standing on that square.
This in itself had a few issues, in that if a character was to stray too close to the bottom left / right of an isometric floor tile, their feet would be clipped by the tile in front, which is getting drawn after the character.
Also it highlighted another issue, once stairs were added in. When climbing stairs, the player, or zombies, would get drawn over by the floor above. Since I was drawing entire floors at once, the floor tiles behind the stairs would overwrite the player’s head as he climbed them.
Forgive the shitty graphics, but you get my point.
So obviously the rendering of an entire floor at a time was no good. It was at this stage I started the engine again in LWJGL, and this time I altered it slightly to render vertical slices. That is drawing in the same order, but instead drawing up to the maximum height of the map for each tile, like so…
The above assuming there were only two z levels to the map, of course…
This worked perfectly. Now when the characters were stood on the stairs, it would have already drawn the tiles behind in the above example when it came to draw the character, and so he would draw correctly and not be cut off.
Except not quite. See, we are trying to make Project Zomboid run on as many machines as possible, and thus it needs to be fast as hell. We’ve already got some quite costly line of sight stuff going on (with some crafty cheats in to speed it up) and plenty of pathfinding and whatnot, so it is imperative that the graphics render as fast as possible. We were finding it was struggling on lower end laptops and after profiling it became clear what the cause was.
In OpenGL, which LWJGL uses, in order to draw a texture to the screen you first need to ‘bind’ it. This is a relatively costly action, and it seemed to be a massive bottleneck in rendering the map.
Binding’s purpose is to set up OpenGL to use that texture’s ID, and thus all calls to vertex colouring, setting UV coordinates, and any other quad/tri drawing functions, will operate using that bound texture, and thus when you render quads you end up with them mapped using that texture.
I already had code inside my ‘bind’ function that checks if the texture was the last one that was bound, and if so ignores the call. However it was rarely using this optimization due to the fact that since the isometric draw order required us to draw ‘floor’, ‘wall’, ‘chair’, ‘zombie’, ‘floor’, ‘wall’, it was constantly having to bind, hundreds of time per frame, even when only drawing from a handful of iso sprite sheets.
Obviously this was a big problem, and cue solution #1
To the uninitiated, a z-buffer, or depth buffer, is a texture that accompanies the screen, which is used primarily in 3D games. When you draw triangles to the screen, they will have a z distance from the camera for each pixel drawn to the screen, depending on the 3D orientation of the triangle relative to the camera. So the left of the triangle may be further away than the right, and so on. This depth information is stored per pixel in the z-buffer, which means that when subsequent triangles are drawn, their pixels will only be slapped on the screen if the z value in the buffer for that pixel is further away than the one they are placing.
Hence it allows you to draw triangles in any order you like, and yet they will appear to have been drawn back to front.
Of course this is mainly intended for 3D games, but I figured it’d work just as well for a 2D game. And indeed it did! Now I could just fake a z value for each gridsquare based on it’s x+y+z value, and batch up all the tile draw calls, and draw them sorted by the sprite sheet they were on.
Instead of having to bind the texture sometimes 3-4 times per gridsquare, I now only had to do it 3-4 times to draw the entire screen of tiles. Result!
Of course there is a big issue when using z-buffers, and that be alpha.
Transparency is a pain in the arse when it comes to z-buffers. The reason for this is the transparent texture may be drawn before stuff that’s supposed to be behind it.
Yup, what a pain in the tits.
So basically, in the left example, where things are sorted properly and the garden is drawn first, then Mr. T, then the window, all is good and proper.
In the second example, the garden is drawn first, then the window, and then Mr. T. So we get the garden properly ‘transparentised’ by the transparent window (technical term) and then when it comes to draw Mr. T after, it draws him fine in the gap where the window is open (since the values in the z-buffer are far away garden values) but he fails to draw at all in the upper part of the window since those pixels of the depth buffer have a value closer than Mr. T (i.e. the window) and so all the pixels of Mr. T are discarded.
So why was this a problem for Project Zomboid?
Well, since we have a isometric viewpoint, we need to be able to transparentise the walls to make the player visible through them if he wanders too close. Otherwise navigating tight interiors would be a nightmare, you’d get munched by unseen zombies. It would suck.
SO, we need transparent walls. What’s the problem? Well we’re drawing ALL walls at the same time, so that means that things we want to be visible through the transparent walls we need to draw first, before the walls. This means effectively walls need to be drawn last, because anything could feasibly be visible through a transparent wall.
But then we have zombies, and line of sight.
When something comes into view it appears. To make it suddenly pop on at full opacity would look horrible, so we have them fading on nicely. But then… uh oh. Transparency.
That means that we need to have them drawn after anything we could feasibly want to show through them while transparent. But… what happens if a zombie fades in when stood in front of a wall? We have to draw characters before walls otherwise they will be invisible when viewed through a transparent wall, which defeats the whole purpose of doing it.
And if the zombies are drawn before walls, then if the zombie fades in while in front of a wall, we won’t see the wall at all through the transparent zombie, but whatever is behind the wall. The floor, or perhaps grass. Resulting in a grassy silhouette of a zombie instead of a transparent one. GARGH!
So yeah. Screw the z-buffer. We needed another solution.
This one has a happier ending, since we were being grossly wasteful with our textures. Basically we had the walls / floors / doors etc on separate sprite sheets, and the sheets were 64×128 grids, a lot of which were wasted space. As so:
Sorry I’ve blurred these since they are out of the game and all.
Anyway clearly this is ridiculously wasteful, and we could massively reduce the amount of binds necessary by compressing as much as possible onto a single 1024 square texture.
So we worked on a sprite packing tool, which took a collection of sprite pages, or separate sprites, and packed them into as small a space as possible, creating a text file which contained the name of the sprite (based on the filename and index on the sheet) as well as the offset on the packed texture, as well as offsets required to simulate the transparent space of the original texture (since this was trimmed out in the sprite packer code)
We ended up with this:
Much much better. We’ve gotten every tile used in the map, including particle effects for tiles, blood splats. Everything. All on one 1024 square texture. We can now draw the entire map only doing a single bind! Of course, by the time the demo is released the chances that we’d be able to fit all map tiles onto a single texture is zero, but still.
Of course characters on tiles would still force a bind (and therefore 2, since it’d need to rebind the map texture) since they are stored on a separate packed texture. But you can’t have everything. We’re currently working on ways to mitigate this further. Even so it now runs on my mother’s graphic card challenged laptop pretty much perfectly, whereas before we were getting 5-10 FPS at best. Funky!
Now we’re working on a way to get the tile editor to work out quadrants of the map and generate packed textures for these map regions. This means that even if there are lots of textures used in a specific map cell, the chances are you’re only going to be drawing from one or two of them.
So there you go. Not sure how useful any of this is, but just thought I’d write about how it all went down. Perhaps there’s a much better way and I’m being a thicko. As I said, I’ve never done an iso engine before now.