Saturday 31 January 2009

Depth sorting = easy when you know how

Just finished adding quick and dirty load and save routines. Just means I can place some objects, save them and it will automatically load them back in when the program starts. Anyway, the main thing I wanna talk about in this post is depth sorting. My ideal solution would have been to have each object calculate a ZOrder based on it's current position and then sort the object list and draw. Like I said, this would have been the ideal solution. Instead I've got a solution that works but is not as cut and dry as the previous method. Before I start I want to explain why this has been such a problem. Most isometric programs are tile based which means all objects occupy the same amount of space and any object that is larger than a standard tile is divided into two or more objects. What I needed for my engine was something much more versatile. Something that would accept objects of any size and any location, not just stuff that was for example 32x32x32 and ultimately only ever moved across one tile at a time. After countless experiments attempting to obtain a unique ZOrder based on the X,Y and Z and after many more tests the Width,Height and Depth also. None of this worked and despite many tutorials giving off a supposed correct formula it still didn't work in all situations so I plugged away experimenting until the solution struck me. I have no idea how many people have used this same method but it is not one that i've come across yet, however I have a feeling the ArmyOfTrolls game EdgeRetroHouse had a similar way of doing things even if the actual methods were never published. Once I'd got the method it seemed so obvious, just wish I'd thought of it sooner.

The Method - Requires that no two objects ever intersect.
If (Object1.X>=Object2.X) and (Object1.X>Object2.X+Object2.W) Then Object1InFront=True
Else
If (Object1.Y>=Object2.Y) and (Object1.Y>Object2.Y+Object2.H) Then Object1InFront=True
Else
If (Object1.Z>=Object2.Z) and (Object1.Z>Object2.Z+Object2.D) Then Object1InFront=True

Note: I have already set up a DrawList which is populated by objects that are on screen. This ensures anything offscreen is not part of the sort process. All visible objects are added to the DrawList but are not in order. The DrawList is then sorted.

Object2 is always after Object1 in the DrawList. If Object1InFront=True after our checks then we can swap their position in our DrawList. So next time round "Object1" will actually be Object 2 and vice versa and the same test will result in Object1InFront=False. I will be happy to provide some source code if there is sufficient demand.

Woe is me. I found a bug.

While attempting to fix the blue cube sorting bug I discovered a more serious problem with my collision detection which I should have realised would happen when I wrote the code. Since collision detection occurs when the corner of an object intersects another object it would seem that all we have to do is check all corners of two objects to check whether they are colliding or not. This works perfectly for bounding boxes in 2D but when it comes to 3D it's not so easy. If one object sufficiently large in the X and Y planes but relatively thin in the Z plane and the other object is large in the Z plane but smaller in the X or Y plane then the program will allow the two objects to intersect without noticing a collision since no corners are within the other object. Here's a wireframe diagram of what I mean.
See what I mean? Even though the objects are clearly colliding there isn't a single corner on either object that is within the other object and so a collision is not detected. Will look into some collision detection stuff for 3D at some point. Coming next is my depth sorting.

Friday 30 January 2009

New version

Posted an updated version of my prototype today with mouse scrolling which you can get here. Just hold down left mouse button and move it to scroll. Make sure you press D first to see the draw list fill up and empty out as objects come into view. Pretty neat. There's a read me with all the controls inside. Remember this is only a test program so the controls are not particularly well selected. Here's a screenshot with some objects stacked on top of each other and still being drawn in the correct order, which is something I'll go into with my next post. BTW, the blue cube and the two chests have incorrect dimensions so may result in depth sorting issues. This is not a code bug, but a bug in the object which I'll get round to.
Run IsoTest.Exe

Controls

Hold left mouse button and move mouse to scroll map

Up = Move object north
Down = Move object south

Left = Move object east
Right = Move object west

Q = Move object up
A = Move object down
There is a known bug sometimes when an object is on top of another object which causes the objects to flicker.
It is known, and I know why it's happening so I fix it when I get round to it.

M = Select next object

O = Change object type (If object is changed and ends up becoming intersected with another object as a result
(i.e object suddenly gets bigger and is "inside" another object) the object will not move until it is no longer
inside the other object(i.e is changed to a smaller object to when it go stuck).
Best used when object is away from surrounding objects.

1 = Increase width of object
2 = Decrease width of object

3 = Increase height of object
4 = Decrease height of object

5 = Increase depth of object
6 = Decrease depth of object

7 = Increase anchor X position
8 = Decrease anchor X position
Anchor is the drawing offset of the object's bitmap, it should be the pixel that corresponds to the rear bottom left
corner of the object. Best not to touch this as it will bugger up the drawing position of the object.

9 = Increase anchor Y position
0 = Decrease anchor Y position

D = Enable debug mode
This shows the draw order of all objects(Constanly changing)

All graphics are mine so please don't steal my poor programmer art.

Thursday 29 January 2009

Obtaining screen coordinates

In order to convert an X,Y,Z coordinate into X,Y screen coordinates I needed to decide which axis was which in terms of isometric direction. Using the cartesian system the X axis runs horizontally across the screen and the Y axis runs vertically up and down the screen so something that worked logically with that in mind seems the best method to use. The following diagrams illustrate the decision I had to make regarding the 3 axes of an isometric or more importantly the Y and Z axes.
The X axis is the same in both directions however the Y and Z axes are reversed in Method 2. Method 1 is the standard rotation used in 3D graphics with the Z axis going "into" the screen and as such would seem the obvious choice for my purposes. As a break from tradition I decided to go with Method 2 for the purposes of my isometric engine since when it comes to the perspective I'll be using for the project it makes more sense to imagine the objects or tiles to be viewed from above for the purposes of converting from 3D to 2D. I'm having trouble explaining why that is but it makes a lot more sense to work in X and Y rather than X and Z when it comes to simply moving objects around on a flat surface. Hope I made this clear. Anyways, the maths behind converting these coordinates is as follows:

Screen.X = X-Y

Screen.Y = ((X+Y) Div 2)-Z The Div is a simple divide but it also rounds down the result into an integer value. Very handy. Divide by Zero errors still apply.

This results with slightly skewed but usable screen coordinates. The only values I need from this calculation even values anyway so the fact certain values are displaced is irrelevant. Early on I was moving objects 1 isometric unit at a time but because of the staggered manner of an isometric plane when converted to screen coordinates it meant objects would jump slightly once per 2 unit movement and I spent a lot of time trying to get the perfect conversion formula until I realised that none of this mattered and it actually looked a lot better if all coordinates were first rounded down to multiples of 2.

X = (X Div 2)*2

It actually doesn't matter so much if the Z coordinate is rounded since any change to Z move the object up or down the screen in a straight line so the movement is smooth anyway but if I didn't move it in multiples of 2 it would move a half as slow as X or Y movements. Now that I've got the calculations for 3D isometric to cartesian coordinates the only problem is the origin of all these values is 0,0 on the game window which effectively means 0,0,0 in isometric is in the top right corner of the game window and worse 0,10,0 is actually offscreen so with a slight addition to my formula we have a working screen coordinate generator.

Screen.X = Offset.X+(X-Y)
Screen.Y = Offset.Y+ ((X+Y) Div 2)-Z

Offset is a TPoint (X,Y: Integer) so I initially set this to 400,200 and our object at 0,0,0 appears in the middle of the window. It also means I can increment the offset values and get a scrolling effect and provided I have a clipper so only objects that are within the game window get drawn I have the groundwork for an effective draw routine.

Wednesday 28 January 2009

And so it was written...

Well an obvious starting point for an isometric engine would be an object class for use on all isometric objects. I've decided to call it TGameObject and looks a little something like this.

GUID: Integer This is a globally unique identifier. It retrieves it's value from a simple GUID object that generates unique indices for each object. Handy for locating objects and comparing objects.
Name: String The name of the object of course. This is not unique and many objects may share the same name.
Position: TVertex
This is the location (X,Y,Z: Integer) of the object in the game space not the screen coordinates.
Dimensions: TVertex The width, height and depth of the object.
Image: TImage For the time being this is a simple pointer to our TImage object which basically stores the entire bitmap of this particular object. Sure beats having an image stored with every object as that would result in a huge memory footprint pretty damn fast.
Direction: Byte No more than 8 directions but probably only 4 for most objects
Animation: Byte No more than 256 different animation for any object type
FrameIndex: Byte No more than 256 frames per animation
Not implemented yet but eventually this will mean the program automatically select the correct frames based on three things: Direction, Animation and FrameIndex.
Anchor: TPoint (X,Y: Integer) This is the image offset that defines where the image is drawn in relation to the screen coordinates when the image is draw i.e The rear bottom left corner of the object.At the moment I'm only using static single frame objects so the Anchor can be stored directly with the actual object. However, once I am using animated objects the anchor position may change from frame to frame so that the animation is smooth and ultimately the Anchor point will be stored with the image and animation information.

This is my basic structure for all isometric objects. I will make additions as I progress but for now this is enough to move and draw objects in order and at the correct positions relative to their calculated screen coordinates. Next time I'll go into calculating screen coordinates and then onto depth sorting.

Saturday 24 January 2009

About the prototype

My current prototype is a very simple program using the Windows GDI to draw stuff to screen. Once I've got the isometric engine running I intend to use my 2D OpenGL wrapper to make the most of 3D accelerated graphics cards that most PC's are using. When it comes to drawing stuff, the GDI is extremely slow compared to OpenGL or DirectX and doesn't allow all the cool features that graphics cards are capable of (alpha blending mainly) but for the time being it allows me to experiment without worrying too much about initialising windows, loading textures into video memory and all the other boring stuff that comes with using either of the main APIs. I just want to get the basic core of it up and in time I can port it to OpenGL and maybe make a really cool game.
Here's some pics of where I'm at or you can download the actual program using the link on the right.

Development goals

I have attempted various isometric projects in the 12 years since I began programming but there was always a rather large stumbling block for me when trying to approach the problem; depth sorting. After spending the last month working on my latest isometric engine with several clearly defined goals I have decided to publish my experiences in this blog in the hope that someone (maybe even you!) will learn something from it and I can feel all nice inside when they (that's you) send me a nice email thanking me for enlightening them on a subject that in my experience has very little in the way of tutorials or reference material. My goals, for your dilectation are as follows:

  • Objects can be anywhere within the play space (X,Y and Z)
  • Objects can come in any size (Width, Height and Depth)
  • Objects cannot intersect
  • Objects will collide using bounding boxes
  • All objects will be ordered correctly depending on position and size
  • Tiling is not required though it is possible
I am writing the engine in Delphi (Pascal) but for the most part I will try to convey techniques using pseudo code and diagrams rather than platform specific code. I am already partway through the development process and as such a lot of the following techniques I have already coded and tested so the following posts will be used to illustrate my methods so far.