Tuesday, October 8, 2013

Tutorial : blitting graphics

(this is the second part of the tutorial about making a Fire game for the bitbox)

If you need basic information about writing software for the bitbox console, please read this introductory tutorial 


Displaying sprites and background

Our game will need to display its state on the screen. We have to define a callback (game_line) , which will blit the current line number "line" on a buffer. A pixel is a 16bit RGB (0x0bgr encoding for the rev1 of the HW).

The bitbox console hardware does not have a frame buffer, it has a line buffer. So you'll have to "race the beam" and blit one line at a time in the game_line callback.

For our simple game, since we don't have the size to put a single frame in memory (!), we're going to compress the pixels and decompress them on the fly. So we're going to store sprites as a RLE encoded list of blits : number of repeats, pixel color used, if pixel_color high bit is not set, don't blit it.

We could have put any other encoding/compression since we're going to write the blit function, but we don't need anything fancier there. experiment in your own : avoid overdraw, encode patterns, gradients ...

Blit will be a 2 array of uint16 :
   
    typedef uint16_t Sprite[][2]; // each blit is a repeat () / a color
// disable high bit of the color part to skip blitting (ie set transparent)

We need also to define sprite width and height to know when to go to next line or stop blitting.

This is an example of a manually defined sprite
    /* this will blit a triangle of 4 lines of white pixels 
       (* is white, . is transparent)

       **** 
       ***.
       **..
       *... 
    */
    uint16_t Blit sprite_4lines[][2] = {
        {4,0x1fff},
        {3,0x1fff},{1,0},
        {2,0x1fff},{2,0},
        {1,0x1fff},{3,0}
    }
    sprite_4lines_w = 4;
    sprite_4lines_h = 4;

    We're going to use the same for the backgrounds, with simple enough background (ie. with large horizontal lines). We could have used tiles, but for the sake of simplicity we're going to have only one encoding for graphics.

Generating sprites

Of course we wont be entering our sprites in a text editor, we'll use a program for that.
I programmed a simple python program to read image png files and output lines as a C file.
Here it is in its entirety :

    from itertools import groupby
    import sys

    from PIL import Image # using the PIL library, maybe you'll need to install it, it's named python-imaging in ubuntu

    print '#include "RLE_sprites.h"'

    totsize = 0 # keep statistics
    for fname in sys.argv[1:] : # for each input file
        name = fname.rsplit('.',1)[0]
        src = Image.open(name+'.png').convert('RGB')
        data = tuple(src.getdata()) # keep image in RAM as RVB tuples.
        w,h=src.size
        size=0
        print 'const int %s_w = %d;'%(name,w)
        print 'const int %s_h = %d;'%(name,h)
        print 'Sprite %s_sprite = { '%name
        for i,y in enumerate(xrange(h)) :
            print '   ',
            for c,g in groupby(data[y*w:(y+1)*w]) :
                t = tuple(x>>4 for x in reversed(c)) # currently stored as BGR ... duh
                n = len(tuple(g))
                print "{ 0x%x%x%x"%t,',',n,'},',
                size += 4
            print
        print '}; // %d bytes, reduction by %.1f'%(size,float(w)*h/size)
        print
        totsize += size
    print '// total size : %d bytes'%totsize



We'll be adding a rule to the Makefile to rebuild our sprites automatically.

    sprites.c: fireman.png background.png
        python mk_sprites.py $^ > $@

Blitting the sprites


Blitting the scene will be done with the game_line callback.
This will be repeated each line. So we must take care of where we are in the blitting of each sprite. We'll track it with an extra value for each *instance* of the sprite : the index of the blit (where are we now).

We'll add it for each elements : the background, the firemen and each falling guy.

    int firemen_index, bg_index;
    typedef struct Guy {
        int frame, x,y,vx,vy, index; // set frame to -1 to disable this guy
    } guy;

for each frame, we will "rewind" the sprite by setting its index to zero.

blitting a sprite line will then be :

    extern int line;
    extern uint16_t draw_buffer[];

    int blit (Sprite sprite, int w, int h, int x, int y, int blitindex)
    {
        // shoud we blit ?
        if (line>y && (line-y)>h )
        {
            for (int dx=0; dx<w ;dx += sprite[blit_index++][0])
                { // find next blit, break loop if we reached right
                    uint16_t color = sprite[blit_index][1];
                    if (color != TRANSPARENT_COLOR)
                    {
                        // fill line, note that we must be sure that x is >0 !
                        memset(draw_buffer[x+dx],color, sprite[blit_index][0] );
                    }
            }
        }

        return blitindex; // return the new index in the sprite
    }

blitting a whole scene line will be :

    bg_index = blit(bg_sprite, bg_w, bg_h,bg_x,bg_y, bg_index);
    for (int i=0;i<MAX_GUYS;i++)
    {
        if (falling_guys[i].frame != -1)
        {
            falling_guy[i].index = blit(   
                guy_sprite, guy_w, guy_h,
                falling_guy[i].x,
                falling_guy[i].y,
                falling_guy[i].index
            );
        }
    }
    firemen_index =blit(firemen_sprite, firemen_w, firemen_h, firemen_x, firemen_y, firemen_index);

Of course, this is very crude. We're hardcoding the scene into the code, RLE can be improved, there is no overdraw omitting logic (ie we're always writing ALL the background + ALL sprites , even if another sprite is in front of it we will always draw all pixels ...)

   
We're now going to add some graphics and let the game roll ! you can see the new listing here, still less than 200 loc.



No comments:

Post a Comment