Implementation

Drawing

The display of the game console can differentiate between four grey scales. Each pixel on the display is defined by two bits (00 ≙ white pixel, 01 ≙ light grey pixel, 10 ≙ dark grey pixel, 11 ≙ black pixel).

To convert PNG images into code we wrote a Python script that takes a PNG image as input and converts it into a C array. The entries of the array are 8-bit entries that represent the different pages (page ≙ four vertical pixels) of the image.

Instead of using the page() function we wrote the following function, in which we use the window functionality of the display. The details are explained in the data sheet.

void drawsprite(uint8_t x, uint8_t y, uint8_t width, uint8_t height, const uint8_t* sprite)
{
    enable_window(x, y, width, height);
    for (uint16_t i = 0; i < width * height; ++i)
        sendbyte(pgm_read_byte_near(sprite + i), 1);
    disable_window();
}

This is faster than using the page() function, because fewer commands are sent to the display.

In order to draw pixel-by-pixel and not only pagewise in the vertical direction, we wrote the drawsprite_px() function:

void drawsprite_px(uint8_t x, uint8_t y, uint8_t width, uint8_t height, const uint8_t* sprite)
{
    uint8_t offset = 2 * (y % 4);
    if (offset == 0)
    {
        drawsprite(x, y / 4, width, height / 4, sprite);
    }
    else
    {
        enable_window(x, y / 4, width, height / 4 + 1);
        uint16_t i = 0;
        for (; i < width; ++i)
            sendbyte(pgm_read_byte_near(sprite + i) << offset, 1);
        for (; i < height / 4 * width; ++i)
            sendbyte(pgm_read_byte_near(sprite + i) << offset |
                     pgm_read_byte_near(sprite + i - width) >> (8 - offset), 1);
        for (; i < (height / 4 + 1) * width; ++i)
            sendbyte(pgm_read_byte_near(sprite + i - width) >> (8 - offset), 1);
        disable_window();
    }
}

Problem: Flash Size

We currently use about 95% of the available flash storage.

We currently use about 95% of the available flash storage.

Since the Atmega 328 only has 32 KiB of flash memory, we had to come up with creative ways to reduce the space used. First, before storing the sprites as arrays in the PROGMEM, we wrote the calls to the page() function by hand for every sprite. Of course, it is much more efficient to read the pages from an array. Second, large images tend to consist of a lot of unnecessary white pixels if the form of the object displayed is not rectangular. Therefore, we split some sprites up into smaller chunks:

Splash screen split up in three

Level Setup

Every level consists of a number of rooms which is a random number between one and five. Furthermore the monster(s) and the positions of the platforms and water are chosen randomly for every new room and the floor and ceiling sprite is chosen randomly for every new level.

To make sure that a level you have already been to looks the same as before, we choose the seed of the random number generator depending on the level and on the room the protagonist is in.

srandom(level_seed + level_pos);
platforms_13 = random();
platforms_19 = random();
platforms_24 = random();
nofloor = random();

Movement

We use a single C struct for monsters, projectiles and the protagonist. This enables us to reuse the functions responsible for movement for all of them, which saves us quite some flash space.

struct Character
{
    uint8_t x;
    uint8_t y;
    enum {LOOK_MONSTER_MEMU, LOOK_PROTAGONIST, LOOK_FIREBALL, ...} look;
    uint8_t lookstate; // to e.g. store whether the wings are turned upwards or downwards
    uint32_t lastlookstatechg;
    uint8_t width;// in pixels
    uint8_t height; // in pixels
    enum {DIRECTION_LEFT, DIRECTION_RIGHT} direction;
    enum {DIRECTION_UP, DIRECTION_DOWN} verticaldirection;
    int8_t jumpstate;
    uint8_t initial_health;
    int8_t health;
    uint8_t damage;
    uint8_t jumpheight;
    enum {FOLLOW_PROTAGONIST, BACK_AND_FORTH, ...} movement;
    uint8_t x_pace;
    uint8_t y_pace;
};

First, there are the functions moveleft(), moveright(), moveup() and movedown(). These take care of

Then, there is also a function move() which automatically decides in which direction to move. For example, a monster with movement==FOLLOW_PROTAGONIST will automatically move towards the protagonist whenever a pointer to that very monster is passed to the function.

Course of the Game

To react to user input and automatically move monsters etc., we use multiple if statements in an infinite loop which check whether the respective timer has expired and whether all preconditions are satisfied.

For example, when the user presses the B_RIGHT button, the protagonist is moved one pixel to the right and it is ensured that he will not move for another 50 ms.

Also, if the B_A button is pressed and the protagonist still has a rocket to shoot that is not yet moving, the rocket is drawn to the screen and the protagonist loses one of his rockets. To make sure that he still has the same number of rockets when the game is resumed after turning the console off, the number is also stored in the EEPROM.

while (1)
{
    if (nextmoveevent < getMsTimer())
    {
        if (B_RIGHT)
        {
            moveright(protagonist);
            nextmoveevent = getMsTimer() + 50;
        }
        ...
    }
    if (projectile->movement == HIDDEN
        && num_rockets > 0
        && nextshootevent < getMsTimer()
        && B_A)
    {
        projectile->movement = PROJECTILE;
        draw(projectile);
        num_rockets--;
        eeprom_write_byte(&num_rockets_stored, num_rockets);
        nextshootevent = getMsTimer() + 500;
    }
    if (monster->movement != HIDDEN && collision(protagonist, monster))
    {
        takingdamage(monster->damage);
    }
    ...
}

This is just a small excerpt of the while loop.