The Fancy 2.5D Tutorial V1.1 - Tips & Code for 2.5D
Please report any URL or other errors.
Here I'll talk about 2.5D rendering and spacial issues, and some tips on collision in a basic 2.5D space. Alright, I'll give you the tip (no s, sorry, I lied) right now: Use doubles for positions of your sprites, that way your sorting routine has a far less chance of collision if your movement is velocity based, so you can use quick sort rather than merge sort if you want.
The most basic idea of 2.5D are 2D image layers with sorting.
Expanded beyond that is 2D objects with y-sorting and a depth property. This isn't even close to real 3D. In fact, there's no 3D math or decided camera angle involved. Depth really just becomes how large the top and bottom face of a rectangular prism is, for example. Most sprites are going to be rectangular prisms, so that's what I'm going to deal with.
The final one I'll discuss considers height. Normally, 2D games will use layers instead of height to decide where things are drawn. You have your sprite layer, your background layer, and your top layer. More or less, that's the basic idea. This is a lot closer to 3D. In fact, you will sort by two height, and then sort by Y. While this is close enough to 3D for you to consider using something such as OpenGL for your rendering, it lacks 3D depth-related math (so no true 3D sorting), so it's simple enough to be done in just FBgfx. This can be considered a very dynamic form of layering, if you will. This one's very uncommon (most games just use layers), but it's very awesome.
One I won't discuss is isometrics. Static Isometrics are easy, especially when the rotation is at 45 degrees, but Iso moves a lot closer into the 3D realm with dynamic Isometric Angles. It's closer to hacking 3D to become 2D than hacking 2D to become 3D (in my opinion). In fact, most Iso engines would benefit from being fully 3D in OpenGL. There's really nothing gained except the coolness of doing something in 2D from keeping the math and engine setup more "purely" 2D.
Basic 2.5D (2D image layers with sorting) is very easy. Here, there isn't a true angle for the observant view. Everything is still exactly 2D except for two things - Sorting, and at times, the art style for sprites. There's no sprite depth. We just decide that one axis (x or y) is going to be distance, and minus or plus is going to tell us how far. The most popular version of this uses the 'overhead' sorting method, where the camera's say, a 45 or 60 degree up from the ground. Higher up on the screen is farther away distance-wise.
Other examples will include a lot of action games, RPGs, and erm, well, solitaire I think. Solitaire's card stack shows how y-sorting can be used in the opposite manner, with the cards being "closer" to you the higher up they are, rather than being farther away.
Let's build a basic sprite first:
And then let's draw some:
First thing we did was create an array of sprites. Then we set them randomly on the screen. After that, we drew them all, unsorted. Pretty simple...
Then let's add a y sorting routine to all of this:
We used crt's qsorting procedure here, which allows us to decide our own sorting. Reverse the comparisons which decide to return -1 and 1, and the sorting would be reversed as well. Higher up would be in front. Lower would be in back. This might work well for a stack of cards...
Layers are just arrays of images. So instead of mySprite(99) that could be myLayer(99), where 99 is the number of sprites - 1. If you want layers, just make a new array of sprites and draw it before another array. It's that simple. I myself have made a layer class before, but that's really only useful when you get into the more complex stuff like transitioning between layers, and having more specific layer properties.
Adding depth to a 2D sprite does something which 2D games in general don't have builtin to their space: Collision on the Y Axis, but not collision based on the top of the sprite. A collision box standing from the bottom of the sprite to however much depth towards the Y it has. No fancy 3D math is added here, so it's truly still 2D. This just expands the idea of taking advantage of already-existent properties to a 2D sprite, and giving it a new one.
We're going to use rectangular prisms to demonstrate depth, as it's the most common, and easiest shape to work with in this case. The rendering doesn't do any math based on the camera angle. Saying how much depth you have is just saying where on the Y access our rectangular prism's going to be expanded on, and by how much.
There's an issue you'd see here with multiple sprites: That is, now that we have depth, we should have collision, or else things will look very weird. Much more weird than normal 2D sprites drawing over each other. We're only going to use 2 sprites, because randomly placing a ton of sprites and making sure none collide, then giving you room to move might take a good amount of extra, unnecessary code for this discussion. You'll find that collision with depth is much like normal bounding box collision. Just one more variable to worry about.
Alright, let's modify our sprite class to have depth:
First thing we do is draw the front face, then the top face over that, then the outline over that. Mini-layers? :P Note that I'm using z as the depth of the object, even though z would be an axis position. You can use something like d instead if you want. Guess that makes more sense, but I'm using z for the convenience right now.
Let's add an 'is collided' function, and 'handle collision', to check and react to collision between two sprites:
Pay close attention to this code. We're careful to take the new collision boxes (our depth) into account when checking collision. Our collision's pretty lame, but it keeps us out of stuff.
Now let's create two sprites. One is going to be our obstacle, and the other, us:
Move around with the arrow keys and run into'em. Now it's time to add sorting:
You might want to note how now that we have depth, since the sprites can be different sizes the sorting routine now checks for (ourPosition + ourHeight), rather than the top of us. You can solve all that junk I added about qsort if you have a rendering list. The rendering list is sorted, but your array of characters remains the same.
Adding Jump - Height and Tallness in 2.5D:
The first thing we want to know about is how the dimensions are judged in this space. Your sprite's height is from now on the X axis. Sounds weird, right? Well, I decided on this as the sorting is being done on the Y axis, and thus, the Y axis has turned into what is normally our Z axis.
You may do things differently. You may sit down and tell me to shove a pineapple into my behind for this, but it seems to make sense. I put the 2D environment into the 3D realm, and rotated the axis to properly match how we were sorting our objects. Z is positioned where our screen's y coordinated are. Iso goes a step beyond this and makes Z/Y your Z, or Z/X. Something like that. Anything beyond what we're doing right now, you should definitely consider going 3D.
Using the X Axis and a Camera, we *could* theoretically get rid of Layering completely.
The first type of layering, transitional layering, happens in 2D all the time. You're on one floor, and you walk upstairs to the next. Since the second floor's drawn over the first floor, your current layer has to be changed. This can be gotten rid of simply by setting an X value for the floor, X value for the second floor, and decrease your X value as you climb up the stairs.
The second type of layering, which is the hardcoded, truly different plane, really deserves to remain on its own plane. However, we *could* switch over to a purely dimensional-based system by setting unrealistically high values for the layers which are rendered above us. They would continue to be drawn above us, unless the camera decided that they were too high up in the air, and were not to be drawn.
The jump height in our pic that causes the shadow is on the X axis. The distance from the left is the Y. The distance from the top of the screen is our Z. Just remember that.
Here we run into a funny problem which will be solved. We have height, so now we can have depth in the ground and in a lot of different places. Really 2.5D stuff. This is not done through layering, but through our sorting. The ground has a different height than the hole, and we can fall into the hole, or jump out of it. How do we sort our sprites?
Well, consider the comparison I made earlier about this being more like dynamic layering. Just like layers, if the X bounds of our environment are completely above us, they will be drawn above us. If the X bound is completely below us, they will be drawn below us. Note that I use completely. So the lower bound of the X is above our upper bound, and they will be drawn above us. The upper bound of the X (tallest point) is below our lower bound? We're drawn above them. Else, z sorting.
Now we need to consider what might happen in this code. Jotting down some solutions or not as I scribble the details:
- You might jump, say onto a platform above you. Jump can be done cheaply, but if we want to make collision better, as well as making movement more fluent, we might want to try a velocity vector and time-based movement.
- You might jump extremely high, and the camera should follow you rather than letting you go offscreen. The Z positions (Y on the screen, if you still remember) of the objects should be affected by this.
- You might bump into a platform which has height, but is not higher than you are.
- You might fall in a hole in the ground, where you'll be below the ground, but still in front of the ground in back of you. We need to decide how things are going to be sorted based on both X and Z positions then. Lower X in this case (jumping higher up off the ground - our y axis) is in front, while higher up on the screen (Z axis) is our distance on that plane.
- To fully demonstrate all this, we probably want a rendering list so I can avoid that qsort workaround with swapping and everything. It gets even more CPU intensive now that we want multiple sprites involved - Finding the mainsprite would take a while, and every frame? No way. Rendering lists it is! Don't worry. I can add to this list, remove from it, and sort it.
So now we're going to redesign our sprite class to accept our new dimensional setup, as well as having velocity:
I think the rendering of our sprite *first* was very important here. I like the idea of you fooling around with the previous sprites yourself, but this gets a bit more complex. Just a bit. So I decided to give an example. Next we're going to get into time-based movement, where we move based on the amount of time passed. 640 pixels in a second, for instance.
We're going to add a timer here so we can move around freely, then we're going to get into collision again and make a better form of collision where we can slide off of objects rather than get teleported. We need movement first, because how much we move in a frame helps us know where we came from. Otherwise, we don't know how much we were moved, and can't decide which side of the object we're sliding off of.
A simple timer object is created, and a pointer to one is added to our sprite:
We also added friction and gravity to slow down the characters. Once collision is added, we might just make the world its own sprite which is underneath you. Instead of checking if you're at 0, you'll land if you collide on the bottom with something, and collision will keep us in bounds in regards to that instead.
It's great having velocity now, because we know how much we move per frame. Using this information, we can undo this movement temporarily for collision, so we know where and whether to set us back on a certain side of another sprite. Another option is to simply subtract the amount you moved that frame, but when you're moving at high speeds, this can be a real bummer. It gets even worse when you're in a crowded space. Bumping you back the other way by your velocity could mean you avoiding one collision, but going into another! We assume that if you go through an object, you have collided with it, and thus, are on it. Not in it and teleporting backwards.
So let's add 2 objects here. A world underneath us which we will walk on. This means we'll be removing that position check for our spd.x, and instead letting collision land us. Another object will be one at our height, which we'll test our collision with. It won't be sorted just yet, however, as these objects are hard-coded for simplicity (no render list yet).
Another thing we're going to add is the shadow changing its base height to wherever the current ground is. This way when we add jumping up to a higher or lower platform, our shadow will follow us up there instead of sticking way down to the ground at 0 height:
Everything appears to be working okay at the moment. But we won't know for sure until further tests. We need the render list to show whether we can go under objects or not, and if we are doing so properly. I added a #define debug at the top so you can see the regions which we divide the prisms and check collision with them. It was added mainly due to this having gotten harder for me when writing the tutorial.
You see where the collision code might seem a little sketchy. Inconsistencies between using the visual represenatation to check if we're in another object, vs checking the other two axis. We should be checking the other two axis, but I first coded the collision using the visual representation and I'm not removing anything until bugs show up. But a true bounding box with two axis each side of the prism you're checking is the ideal collision.
I changed the system I originally had in order to make collision with jumping easier - The X positition is now at the top of your prism, rather than the bottom, so your bottom will now be at 0 if your altitude matches your height. This made sense, and it also made the collision math a lot easier. You'll see that when it comes to collision on the Z axis, things get more complicated - This is due to our viewing angle, and the math being done in 2D rather than 3D. This is the kind of thing you should expect to run into when adding additional, fake axis to any given plane.
One more thing - the render list is going to have a camera as well. The perspective's really complex at the moment, wouldn't you agree? A camera moving when we jump and when we move would help make things easier to understand.
Now that we have sorting, we can see that the collision appears to be working. The camera also makes it a lot easier to see what's going on. I got rid of the shadows because we do not have proper shadow rendering - Ideally, the renderlist would handle these so shadows don't appear on the same layer as objects lower than whatever caused the shadow. Our shadows were very cheaply made, anyways, and had no well-defined purpose to their coordinates.
And finally, just to demonstrate all of our concepts... A small editor where you can add to the world:
Controls (lots of them, sorry!):
r,g,b - increase the color values of the current block by 5
wasdqe - increase/decrease the size values of the current block by 1
arrow keys, rshift, enter - change the position of the current block
p - set the current block to the player
1, 2 - 1 = run the simulation... 2 = go back to editor
m - make a new block
BackSpace - save in a format which currently can't be loaded back... sorry
ikjl - move the camera offset for easy editing. Really makes things easier...
© 2008 by Pritchard - do not modify or redistribute without his permission