Tutorial 3: Graphics, graphics, graphics
This tutorial, we'll be looking at graphics of all descriptions. Our last project was some coloured rectangles, this time your game should look a little better at least :) For those of you new to these tutorials, try parts one and two before you move on - although they're not required. I'm going to start posting generic code snippets now as well as adding to the code from last tutorial, which will help you form a unique game. Let's hope my server is ready for another round, also, I use up 1-2 gigs per tutorial :P
Odds and ends from tutorial 2
I did omit some things from tutorial 2, but thanks to the forums I've found out some new things, and I'm going to run them through below:
- Multiple compilers
If you are using several compiler sets on one PC, you'll probably want to set your PATH variable on a compile-by-compile basis. For instance, storing the original path somewhere, adding to it, then restoring it when you're done. Some of you, for instance, might be calling Borland's make utility instead. Another solution is to call c:\devkitadv\bin\make directly.
- *nix/Mac users
I am only just starting to develop on *nix, and there are several options available. I suggest you hit #gp32 or the forums for help - gp32x's forum in particular has a direct link to a debian package that will set you up in no time (unstable).
- Linking in the math library
Open up the 'gp32.mk' file in c:\devkitadv. Find the line
GPLIBS=-lgpsdk -lgpgraphic -lgpmem -lgpos -lgpstdlib -lgpstdio -lgpsound -lgpfont -lgpg_ex0
and add -lm to the end. Save and the math library will be linked into every new gxb/fxe you produce.
- Make not working?
If you are getting the error make: /bin/sh.exe: Command not found you are probably using some broken version of Cygwin, as there is already a cygwin 'sh' binary in the \bin directory. However, replacing make with make --win32 in your batch file will solve the problem (thanks M^4 !)
- Keep typing C++ style comments?
I sure do. Unfortunately GCC now refuses to accept -ansi and -Wp,--lang-c++-comments so you'll have to use a sed script to do it:
sed "s?//\(.*\)?/*\1 */?" gpmain.c > gpmain2.c
Then compile gpmain2.c - this preserves your original gpmain.c with comments, and if anything messes up with the sed conversion, you still have the original.
Getting your images
A foolish programmer begins coding with no graphics.
A wise programmer begins coding with full graphics.
A master programmer begins coding with a bunch of boxes, squares and lines as placeholders for graphics until he gets someone else to do them for him.
We're going to take either the second or third approach, but you'll need some sort of graphics before you can see what the hell is going on. There are several options:
- Use a free sprite resource
- Rip graphics off a game
- Draw them
- Get someone else to draw then
Ripping is done two ways, either from screengrabs or from the game directly. A place like http://www.gsarchives.net/ can get you tons of links to video game sprites. Or use a tool like Paint Shop Pro's remote capture, play the game, take screenshots and extract the raw data. This is what I did with a Flash game for my game GPunk32:
Here are some of the 100+ tiles I grabbed. It was a long process - you had to clean every sprite - and I wouldn't recommend it. In some games (e.g. roms) the graphics are stored in their original format inside the rom file, and can be extracted/converted. http://www.zophar.net/ (Zophar's Domain) has a lot of tools related to that.
There are other places. Search for 'rm2k tilesets' and steal those. Or try http://www.windrider.freeservers.com/ or http://purezc.com/ for tiles (thanks jlebrech !)
Assuming you have all these graphics, what do you do with them?
- Make them computer-readable: It's easiest to put them in a large strip or grid, strips are probably easier. This will allow the code to look up graphics very fast with just one multiplication.
Clean them: That means make parts that should be transparent, transparent. Try a colour like pink.
- Reduce to 256-colours: All graphics should have the same 256-colour palette. Easiest way is to copy and paste all the images to one big file, then reduce that to 256-colours, save the palette and load that palette over all your files. This will create a global palette that works for all your images equally, preserving image quality. Also, you'll need to take a note of the index of the transparent colour (pink or whatever) for later on.
- Avoid transparency where possible: Backgrounds shouldn't need much transparency. Transparent images are slower to draw than solid ones, so where you can, use solid images. Note: you have to make an entire set solid, not just one image - it's not the images themselves, it's how they're drawn by the code. For instance you could have all your tiles solid and all your sprites (characters) with transparency.
- Save in bmp format: You'll find out why later.
Now let's modify some code to draw random tiles instead of random rectangles.
Adding sprite support
The magical function for sprite drawing is
int GpBitBlt (GPDRAWTAG * gptag, GPDRAWSURFACE * ptgpds, int dx, int dy, int width, int height, unsigned char * src, int sx, int sy, int imgw, int imgh);
This isn't too complicated, really. gptag is NULL always, for us. It's a clipping box, and defaults to the entire screen, which is what we want. ptgpds is the confusingly-named surface to draw on. For our code this will probably be &gpDraw[nflip]. dx and dy are the destination co-ordinates. So if we want to copy an image to (32, 34) then dx = 32, dy = 34. width and height are the dimensions of the copied image. src is the array of pixels to draw. When we convert your bmp images to pixel arrays, we'll reference them here.
sx and sy are the source co-ordinates, from that pixel array, while imgw and imgh are the dimensions of the whole source image. Just to hammer the point in further I've drawn a little diagram:
By the way, thanks to jlebrech for clearing up a bug I had ... and no thanks to all the "coders" who "checked" my tutorial but didn't tell me that my definition of GpBitBlt was completely wrong in one place. Biggest problem was, my definition works unless you're not blitting the full height of the source image... Enough talk, let's do this. First we need to:
Converting your sprites
Open the tools directory and find GP32Converter , or head over to http://www.ifrance.com/edorul/gp32/.
Load your image file, assuming it is properly-formatted as I said earlier. Type in an array variable name . Anything will do, but you may wish to prefix your name with sprite_ . For example, sprite_tileset . Also make sure generate palette is checked. Click convert in a new file... and type in some filename, like 'tileset.h'.
Let the conversion begin! Once it's finished, open your tileset .h file, and delete the C++ comments or replace them with C ones :) Else you won't compile anything... now note the two variables created. One should be your tileset, and the other should be your global image palette (since your palette is the same for all your images ... right?) called tileset_Pal or something. You'll probably want to rename it; I use npal in my code for (no logical reason whatsoever).
Now you need to link the .h file in, just add
#include "tileset.h"
to the top of your code. Now your sprites will be compiled into the gxb/fxe file itself, and ready for use in your programs. A while back I mentioned the src variable of GpBitBlt ? Well, that's where you put sprite_tileset or whatever. But first:
Initialising the global palette
In the original code we didn't do any palette work. The GP32 assumes its own standard palette by default; it's easy to change using GpPaletteSelect . Add the following code underneath the GpRectFill(NULL, &gpDraw[nflip], 0, 0, gpDraw[nflip].buf_w, gpDraw[nflip].buf_h, 0xff); line in Init() :
h_pal = GpPaletteCreate(256, (GP_PALETTEENTRY*)npal);
GpPaletteSelect(h_pal);
GpPaletteRealize();
GpPaletteDelete(h_pal);
h_pal = NULL;
You'll also have to add: GP_HPALETTE h_pal ; in your gpmain.h file.
So what does this do? Well firstly there are two types to palette - a simple list of colour values created by GP32Converter , and the GP32's special palette format. npal is the GP32Converter "format", h_pal is GP32. We take npal and create a GP32 palette from it, storing it into h_pal . We then set the GP32 to use this palette. Now the palette's in the GP32's internal memory, we don't need it anymore, so we get rid of it.
Blitting some sprites
You may recall last tutorial we drew rectangles over the screen using
GpRectFill(NULL, &gpDraw[nflip], i * TILESIZE - xpos, j * TILESIZE - ypos, TILESIZE, TILESIZE, map[xtile + i][ytile + j]);
Find that line in GameEngine() and replace it with something else... can you guess the exact syntax? Well let's see, gptag is still NULL, ptgpds would still be &gpDraw[nflip] , dx and dy would be taken from the above line ( i * TILESIZE - xpos , etc). width and height are both TILESIZE , based on your tilesize defined earlier in the program.
It gets a little more complex here depending on what you're trying to do. Let's say I have a strip of 20 tiles. Let's number them 0 to 19. My map array (defined earlier) holds numbers from 0 to 19, and those numbers relate to what tile I want to draw. The y co-ordinate of the tile on the strip will always be zero, but the x will change. It will in fact be tilenumber * TILESIZE . With tilesize 32, the first tile is at (0,0), the second at (32,0), the third at (64,0) and so on. The imgw and imgh values are supplied by the converter as #defines.
To find out what tile we need, we use the same code from the rectangles: map[xtile + i][ytile + j] . Just this time, map numbers relate to the tile to draw, not the colour. So let's (finally) replace the rectangle line:
tile = map[ytile + j][xtile + i];
GpBitBlt(NULL, &gpDraw[nflip], i * TILESIZE - xpos, j * TILESIZE - ypos, TILESIZE, TILESIZE,
(unsigned char*)sprite_tileset, tile * TILESIZE, 0, sprite_tileset_width, sprite_tileset_height);
The TILESIZE everywhere can get distracting, but hopefully you can understand it. Just compare it to the GpBitBlt definition above. The (unsigned char *) is important too, you must add it to every src array you pass to GpBitBlt.
The final touch is to change the map initialisation in Init() . Find the line ' map[i][j] = (unsigned char)(rand() % 255); ' and replace 255 with another number. If you have 20 tiles, put in 19. It's common sense to make this a #define so you can easily change it later.
Try doing all this and you should get a load of random tiles. So how exactly do we do this transformation from
to
?
Mappy, the freeware map editor
We're going to use Mappy to make some 1337 mappage. First off, this assumes that you have no 'zero' tile, for a background behind it or whatever. However this is easy to do: make a tile with chequers or something as the first tile, and call that your transparent one. It doesn't actually matter what's on it (see later for drawing tiles over a background).You can find Mappy at http://www.tilemap.co.uk/mappy.php. Grab it, run it and start a new map.
We're not using isometric complexity or Mappy-specific features, so hit No and use Easy. Ah ... the, er, easiness.
You'll have to specific the tile size and map dimensions. Also make sure you check paletted (8bit) . Note that you have two #defines in your code, MAPH and MAPW; these will have to be changed to whatever you put for map size here.
Now to import the graphics - click File > Import and choose your image. Click 'Yes'. Your tiles should appear in a lovely list to the right.
At this point, just select stuff and draw a map. Read up the documentation, particularly brushes, to find out how to use the program. It's easy to learn. When you're done (you can change stuff later) save the original image as an FMP file.
Now go to File > Export and choose 'txt file (for GBA)'. The GBA is similar to the GP32 for this at least.
I had problems with 'Skip block 0' so I ignored it and dealt with that stuff at code level. Clicking OK brings up another dialog; here choose the stuff as shown. Finally rename the .txt file to .h, and include it in gpmain.c as normal. Also check the .h file for stuff to remove, the mappy.h #include for instance.
Replacing the map loader
Now to replace the loader code. In Init() all we had was this:
for(i = 0; i < MAPW; i++)
{
for(j = 0; j < MAPH; j++)
{
map[i][j] = (unsigned char)(rand() % 19); /* or whatever */
}
}
Now let's replace the middle bit:
for (i = 0; i < MAPH; i++)
{
for (j = 0; j < MAPW; j++)
{
map[i][j] = ( unsigned char )(testmap[i][j]) - 1;
}
}
Where testmap is your map variable. We're doing a straight copy, but this way you can hold multiple maps in data and load them as needed. The -1 is a lazy hack, you won't need it if you have a fully transparent tile (see below). Now you should have a proper map!
Updating tilesets
If you update your graphics, reconvert the file using GP32Converter and change the C++ comments again. You could also add the graphic to the same file again, and delete the first one. Either method works. In Mappy, import the tileset again but this time, click No when it asks if you want new block structures. It's unwise to do this too much, though.
Drawing tiles over a background
You either have tiles as the background, or over it. If you have tiles over it, you'll need to make a few changes to my code as you now use transparency.
tile = map[ytile + j][xtile + i];
if (tile > 0) GpTransBlt(NULL, &gpDraw[nflip], i * TILESIZE - xpos, j * TILESIZE - ypos, TILESIZE, TILESIZE,
(unsigned char*)sprite_tileset, tile * TILESIZE, 0, sprite_tileset_width, sprite_tileset_height, transcolour);
First of all replace your blit code with the one above. Assuming tile 0 is the placeholder for nothing (i.e. totally transparent), here you can see that if the tile is not 0, draw it, else skip it (as it's fully transparent anyways). This saves some speed.
Also to handle transparency, find out the index colour of your transparent colour, like pink, and replace transcolour with that number. GpTransBlt works exactly the same as its cousin GpBitBlt except it doesn't draw tiles of colour transcolour . That's what you want, right? :D
I'll assume you know enough about coding for the GP32 now to draw a bit fat black rectangle as the background (do this before drawing the tiles, every frame). Or more logically you could find yourself a 320x240 image, convert it, include it and blit that :
GpBitBlt(NULL, &gpDraw[nflip], 0, 0, 320, 240, (unsigned char*)sprite_background, 0, 0, 320, 240);
Finally, get rid of the - 1 in the map loader above.