Pixel-Perfect Collision Detection Tutorial
Bounding boxes and spheres are nice and quick for doing simple collision
detection. They are probably most appropriate for testing collisions
with square-ish objects, or in situations where sprites do not rotate (like
Space Invaders), or where speed is more important than accuracy.
Basically, if something enters the area bound by the box or sphere, a collision
has taken place and your trousers fall down, or whatever.
But what about when you want to use rotating sprites? Or what about if you want accurate collision detection? What kind of routine is good for testing collision with sprites at all sorts of strange angles? You could try a bounding sphere, but to me this seems a bit urgh - you lose out on accuracy, you have to tradeoff between the speed and accuracy of the circle (and even then you either encompass regions outside the sprite, or not encompass some regions within it, so it won't be too accurate anyway), and reading on the newsgroups, in some cases the bounding sphere test is meant to fail. So it's probably not worth the effort. (i've not even tried coding a bounding sphere routine because of this, so I could be wrong, but if anybody reckons they're ace and they're the saviour of all mankind, then let me know.)
The other alternative is to copy some collidable objects (like treasure, or enemy sprites) into a buffer, then test your collider object (e.g. your player, or the bullets you fire) pixel-by-pixel with whats been copied into the buffer. If there is already a pixel in some position in the buffer, then a collision has occured, and something should probably happen. This tutorial shows you how to go about doing it for yourself.
This technique is accurate, but is nowhere near as fast as bounding boxes. Nevertheless, if you want an easy way of doing collision detection on rotated/scaled sprites, this is probably your best bet. The routines I will give you could probably be optimised something rotten. For extra efficiency, I am combining bounding boxes with pixel-perfect collision, so that I only have to test for collision in an area surrounding the collider sprite.
This tutorial is based on an old sprite engine I coded a while back in assembler on the PC (in mode 13h). If you like looking at messy x86 asm code, then email me and we'll sort something out.
To do pixel-perfect collision, you will need:
As well as having a collision buffer in main RAM, you will need
smaller buffers to hold the objects that are to collide with the things
in the buffer. As I said before, we can't access VRAM directly.
So what we can do, after the VRAM buffer has been copied to main RAM, is
to StoreImage() some of the sprites to main RAM as well, converting to
16-bit mode if need be. An example of doing this can be seen in the
example code.
The picture below shows an enlarged view of a 16*8 pixel sprite
(A), together with three possible masks for the sprite (B,C and D).
As you can see, the mask of a sprite is just the shape of the sprite, but in one colour. The mask determines which areas of your sprite can be tested for collision. Where the mask is 0 (i.e. the PlayStation transparency colour in 16-bit mode), the collision detection routine will ignore those areas of the sprite. Where it is some other value other than 0, collision detection will be carried out. Because the comparison is done for each pixel of the sprite mask (at least until collision is detected), this makes the routine pixel-perfect.
So, what does this mean? Well, for one thing, we can be extremely accurate with collision detection, down to a single pixel. Notice masks B and C: Mask B has the windows in the UFO set to 0, while mask C is opaque throughout the UFO. If we had a single-pixel sprite, no collisions would be detected if the pixel occupied the position of a window of the UFO, but with Mask C, ALL collisions within the UFO would be detected.
Another thing we can do with masks is make them larger or smaller than the sprite it represents (like with Mask D). Why would we want to do that? Well, we can make the baddies easier or harder to hit by varying the mask size, so we can vary the difficulty of the game a bit.
Yet another nice feature about using masks is that we can change their colour. This is useful because it produces diferent values depending on how you detect the pixels. You can use this to your advantage and have different collidables use different coloured masks, so you can tell the difference between something that kills you and something which you can collect.
You don't even have to use masks if you just want pixel-perfect collision detection and are not bothered about telling collisions apart: you can just copy the image of your sprite to your VRAM buffer (with MoveImage() etc...) All you need is something that tells you what is 0 and what isn't, and the sprite data will do that anyway.
Whichever way you decide to use masks, anything in your VRAM buffer should be 16-bit. You can either convert the 4 or 8-bit mask data to its 16-bit representation at runtime and dump it somewhere in VRAM where you can move it into your area with MoveImage(), or you could convert your masks seperately with TIMutil and load them in seperately with everything else.
Or you could wait on for a bit and find out a clever way of putting things into offscreen VRAM area, which it turns out we need to do anyway for maximum flexibility....
Well if that algorithm is easy enough to understand, you might have
had a shot at rolling your own by now. And after feeling all smug
for a bit when you get it working, you will have discovered on a slight
problem:
But using the GsSortXXX() functions to draw off the screen is not quite as straightforward as it sounds. For one thing, the sprites are usually clipped by the hardware to inside your work and display buffers. If you specify coordinates outside these boundaries, then the sprite won't get drawn. This is usually a good thing because it prevents VRAM getting messed up by wandering sprites, but in this case it just gets in the way.
The GsDRAWENV variable tells the GPU all sorts of stuff about drawing operations (see the green book for more info). The most important thing to realise is that it is responsible for defining where the sprites get clipped to in VRAM. Look at the clip variable, and ofs[2]. Set the clip rectangle to your area in VRAM. The array ofs[] contains two values. These are the number of pixels to offset drawing by (X then Y). So when you set your sprite to be at (15,5) for example, the GPU adds on whatever ofs is for the X and Y position to put it in the right place in VRAM.
We also need another ordering table for our offscreen area. To be honest, I don't know why this should be, and why it doesn't work when you try and use your main ordering tables to draw offscreen (the sprites don't get displayed in the offscreen VRAM area, I know because i'm copying the collision area to the visible screen to see whats going on!). I set it up exactly as I did with my main ordering table, but gave it a different name, and it works fine (i.e. it doesn't crash).
So, when your inside your collision routine:
Great! You've now got zoomable/rotatable sprites to collide
with! But we have forgotten the collider sprite! That is not
being zoomed and rotated, is it?
Okay, this is one i'll leave for you to deal with, but its easy to do, if you put into practice what is going on above. Basically you just do the same thing with the colliders as you did with the collidables: have an offscreen VRAM buffer, draw zoomed/rotated colliders in, copy buffer to main RAM every time collision is called, and proceed from there.
Of course this means having an extra StoreImage() to contend with each time collision is detected, so thats another drain on the old CPU cycles. And for zooming collider sprites, if you want to detect all of the zoomed sprite you must ensure that the buffer can grow and shrink along with the zoom. If you're not too bothered about that, the sprites will just be clipped to their VRAM areas.
Right, view the source and have a go, the code goes as far as "theres one tiny problem...", so your on your own from there, it'll give you something to play about with and understand. It uses a few of my wrapper functions to set up sprites etc, just use your own and things should still work fine. Plus it was coded using CodeWarrior, but there should be no anti-GNU happenings going on. You can still look at the .PXE to see whats going on though.
If this info is useful to you, please put my name in your credits
list of your next game! Cheers!