Arduino Pac-Man part 3 – animating bitmaps

In the first entry of this series, I discussed what led me to experimenting with video output on an Arduino UNO. In the second entry, I describe getting my first simple sketch showing on the TV, including bouncing some balls around the screen and moving a player. Today I continue sharing my progress as I started playing with bitmap graphics.

Another feature of the TVout library is the support of bitmapped graphics. Since the display is black and white, the graphics are 1-bit images, where each bit of a byte represents 8 pixels on the screen. “00000000” would be all pixels off, and “11111111” is all pixels on. The TVout library has a simple structure for these bitmaps, which is just an array of bytes. The first two bytes are the width (in pixels) and height (in bytes) of the bitmap. A simple 8×8 square might look like this:

PROGMEM const unsigned char square[] = {
  8, 8,
  0xFF,
  0xFF,
  0xFF,
  0xFF,
  0xFF,
  0xFF,
  0xFF
}

Digression: Hexidecimal, binary and other weird numbers.

If you are already familiar with hexadecimal, binary and such, skip this section.

0xFF in hexidecimal is 255, which is 11111111 in binary (all bits on). If you have never used hex before, here’s a quick explanation. Our “normal” numbers are base ten. We count 0 to 9 (ten) and then increment the digit to the left. We can count 0 to 9, and then we add a 1 to the beginning and start over going 10 to 19, then we add 1 to the first digit and go 20 to 29. When the first digit reaches 9 (at 99), we continue with 100 and so on.

Hexideximal is base 16. We do exactly the same thing, but counting to 16. Since our digits only cover 0-9 (ten), hex continues after the 9 with the letters A through F. So, with hex, you count 0-9, followed by A-F, then you increment the first digit and it becomes 10 through 1F, and then 20 through 2F and so on.

I wish someone would have explained it to me like this back in 1982. I found it far more confusing then.

Anyway, in a hex number (represented in C by 0x at the start), each digit represents four pixels. 0xF0 would be “11110000” and 0x0F would be “00001111”. In binary (base 2), the numbering counts to 2 and then adds the one, so you get 0, 1, 10, 11, 100, 101, 110, 111, and so on. At this point, it gets more confusing.

My point is, drawing a square in hex may be easy (all pixels on is F), but trying to create anything else quickly gets confusing. You may know binary enough to know that you can figure out the bit values (0001=1, 0010=2, 0100=4, 1000=8), but trying to draw something using 3C EF AE 31 is not the way I want to spend my time.  Instead, we cheat.

End of digression.

Creating the bitmap in hex is cumbersome, but there is a non-standard way to represent binary in the compiler that the Arduino IDE uses. This is non-standard and generally you should not write code using things that are non-standard because that code may not work in other places. But, since we are cheating, and will only be running the code on an Arduino, here is how it works:

In C, the prefix “0x” makes a number hexadecimal. In non-standard Arduino C, you can use “0b” to make the number binary. It looks like this:

x = 255;        // 255 (11111111) in decimal
x = 0xFF;       // 255 (11111111) in hexidecimal
x = 0b11111111; // 255 (11111111) in binary

This would make creating 1-bit graphics much easier in source code:

PROGMEM const unsigned char square[] = {
  8, 8,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111
}

Don’t those 1s look more like a solid square now? How about a non-solid square with an “X” across it?

PROGMEM const unsigned char square[] = {
  8, 8,
  0b11111111,
  0b11000011,
  0b10100101,
  0b10011001,
  0b10011001,
  0b10100101,
  0b11000011,
  0b11111111
}

If you squint just right, you can see it. And this is how I decided to change my filled circle to a Pac-Man shape. Here are three variations of Pac-Man facing the right (mouth closed, mouth partially open, and mouth fully open). To get things to work out evenly, the character size was 8×7.

PROGMEM const unsigned char pacmanClosed[] = {
  PLAYERW,PLAYERH,  // width, height
  0b00111100,
  0b01111110,
  0b11111111,
  0b11111111,
  0b11111111,
  0b01111110,
  0b00111100
};

PROGMEM const unsigned char pacmanOpenSmall[] = {
  PLAYERW,PLAYERH,  // width, height
  0b00111100,
  0b01111110,
  0b11111100,
  0b11110000,
  0b11111100,
  0b01111110,
  0b00111100
};

PROGMEM const unsigned char pacmanOpenBig[] = {
  PLAYERW,PLAYERH,  // width, height
  0b00111100,
  0b01111100,
  0b11111000,
  0b11110000,
  0b11111000,
  0b01111100,
  0b00111100
};

Any one of these bitmaps could be displayed by doing “TV.bitmap(x, y, pacmanClosed);” Initially I just drew an open-mouthed Pac-Man and moved him around instead of the filled circle. Here is the complete code, with some extra stuff added, like a border around the screen (and adjusting the X/Y edge detection to know about that).

#include <TVout.h>

TVout TV;

#define BALLS      10 // Number of balls to bounce.
#define BALLSIZE   4  // Size of balls.
#define PLAYERSIZE 6  // Size of player.

#define ANALOGXPIN 0  // Pin 0 is X on iTead joystick
#define ANALOGYPIN 1  // Pin 1 is Y on iTead joystick

#define PLAYERW    8
#define PLAYERH    7

#define BORDERSIZE 2  // 1 pixel border around the screen

PROGMEM const unsigned char pacmanOpenBig[] = {
  PLAYERW, PLAYERH,  // width, height
  0b00111100,
  0b01111100,
  0b11111000,
  0b11110000,
  0b11111000,
  0b01111100,
  0b00111100
};

void setup()
{
  uint8_t i;

  TV.begin(NTSC, 120, 96);
  Serial.begin(9600);

  TV.clear_screen();

  for (i=0; i<BORDERSIZE; i++)
  {
    TV.draw_rect(i, i, TV.hres()-i*2-1, TV.vres()-i*2-1, WHITE);
  }
}

void loop()
{
  uint8_t  x[BALLS], y[BALLS];    // X and Y position of ball
  int8_t   xm[BALLS], ym[BALLS];  // X and Y movement of ball
  uint8_t  i;       // counter

  uint8_t  px, py;                // X and Y position of player

  // Initialize balls.
  for (i=0; i<BALLS; i++)
  {
    // Random position
    x[i] = random(BALLSIZE+BORDERSIZE, TV.hres()-BALLSIZE-BORDERSIZE-1);
    y[i] = random(BALLSIZE+BORDERSIZE, TV.vres()-BALLSIZE-BORDERSIZE-1);

    // Random direction
    xm[i] = random(2)*2 - 1;
    ym[i] = random(2)*2 - 1;
  }

  // Initialize player.
  px = TV.hres()/2;
  py = TV.vres()/2;

  // We will do our own control loop here.
  while(1)
  {
    // Wait for end of screen to be drawn.
    TV.delay_frame(1);

    for (i=0; i<BALLS; i++)
    {
      // Erase balls.
      TV.draw_circle(x[i], y[i], BALLSIZE, BLACK);

      x[i] = x[i] + xm[i];
      if (x[i]<=BALLSIZE+BORDERSIZE || x[i]>=TV.hres()-BALLSIZE-BORDERSIZE-1)
      {
        xm[i] = -xm[i];
        x[i] = x[i] + xm[i];
      }
      y[i] = y[i] + ym[i];
      if (y[i]<=BALLSIZE+BORDERSIZE || y[i]>=TV.vres()-BALLSIZE-BORDERSIZE-1)
      {
        ym[i] = -ym[i];
        y[i] = y[i] + ym[i];
      }

      TV.draw_circle(x[i], y[i], BALLSIZE, WHITE);
    }

    // Erase player
    TV.draw_rect(px, py, PLAYERW, PLAYERH, BLACK, BLACK);

    // Read joystick (0-1023) and convert to screen resolution.
    px = analogRead(ANALOGXPIN)/(1024/TV.hres());
    if (px<BORDERSIZE)
    {
      px = BORDERSIZE;
    } else if (px>=TV.hres()-PLAYERW-BORDERSIZE-1)
    {
      px = TV.hres()-PLAYERW-BORDERSIZE-1;
    }

    py = analogRead(ANALOGYPIN)/(1024/TV.vres());
    if (py<BORDERSIZE)
    {
      py = BORDERSIZE;
    } else if (py>=TV.vres()-PLAYERH-BORDERSIZE-1)
    {
      py = TV.vres()-PLAYERH-BORDERSIZE-1;
    }

    // Draw player.
    TV.bitmap(px, py, pacmanOpenBig);
  }
}

In the above code, the right and bottom edge detection is still off by one pixel, but I was just rushing from experiment to experiment and wasn’t taking the time to fix things.

So now we have a Pac-Man that can be moved around the screen… But Pac-Man animates. I had already created the different frames of Pac-Man, but rather than use code to choose which one to display, I decided I would put them all in an array, and let the program cycle through them. Instead of just having one array of bitmap bytes, I would use a multidimensional array so I could hold each array of bitmap frames in it. I duplicated one of the animation frames so I could just cycle through them (1 closed, 2 slightly open, 3 fully open, 4 slightly open, 1 closed, 2 slightly open, 3 fully , 4 closed) instead of having to control the sequence myself.

I created the array like this:

PROGMEM const unsigned char pacman[PFRAMES][PLAYERH+2] = {
  { // Closed
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111110,
    0b11111111,
    0b11111111,
    0b11111111,
    0b01111110,
    0b00111100
  }
  ,
  { // Open Small
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111110,
    0b11111100,
    0b11110000,
    0b11111100,
    0b01111110,
    0b00111100
  }
  ,
  { // Open Big
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111100,
    0b11111000,
    0b11110000,
    0b11111000,
    0b01111100,
    0b00111100
  }
  ,
  { // Open Small
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111110,
    0b11111100,
    0b11110000,
    0b11111100,
    0b01111110,
    0b00111100
  }
};

I needed a new #define for the number of frames (four frames, an array of four), and then had to specify the size of each array. If the bitmap is 7 bytes tall, the array is 7 plus the extra two bytes at the start that tell the width and height.

Now to display a frame, I would use “TV.bitmap(px, py, pacman[frame]);” where frame is 0-3 (four frames). I added a new frame counter that would cycle from 0 to 3 over and over, but it was way too fast. I added a frame delay value so it would only increment the frame every type that count was reached. The code that does that looks like this:

#include <TVout.h>

TVout TV;

#define BALLS      10 // Number of balls to bounce.
#define BALLSIZE   4  // Size of balls.
#define PLAYERSIZE 6  // Size of player.

#define ANALOGXPIN 0  // Pin 0 is X on iTead joystick
#define ANALOGYPIN 1  // Pin 1 is Y on iTead joystick

#define PLAYERW    8  // Width of player.
#define PLAYERH    7  // Height of player.
#define PFRAMES    4  // Frames for the player.
#define PFRATE     5  // Count to 5, display next frame.

#define BORDERSIZE 2  // 1 pixel border around the screen

PROGMEM const unsigned char pacman[PFRAMES][PLAYERH+2] = {
  { // Closed
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111110,
    0b11111111,
    0b11111111,
    0b11111111,
    0b01111110,
    0b00111100
  }
  ,
  { // Open Small
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111110,
    0b11111100,
    0b11110000,
    0b11111100,
    0b01111110,
    0b00111100
  }
  ,
  { // Open Big
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111100,
    0b11111000,
    0b11110000,
    0b11111000,
    0b01111100,
    0b00111100
  }
  ,
  { // Open Small
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111110,
    0b11111100,
    0b11110000,
    0b11111100,
    0b01111110,
    0b00111100
  }
};

void setup()
{
  uint8_t i;

  TV.begin(NTSC, 120, 96);
  Serial.begin(9600);

  TV.clear_screen();

  for (i=0; i<BORDERSIZE; i++)
  {
    TV.draw_rect(i, i, TV.hres()-i*2-1, TV.vres()-i*2-1, WHITE);
  }
}

void loop()
{
  uint8_t  x[BALLS], y[BALLS];    // X and Y position of ball
  int8_t   xm[BALLS], ym[BALLS];  // X and Y movement of ball
  uint8_t  i;       // counter

  uint8_t  px, py;                // X and Y position of player
  uint8_t  playerFrame;           // Player frame to display
  uint8_t  playerRate;            // Frame speed counter

  // Initialize balls.
  for (i=0; i<BALLS; i++)
  {
    // Random position
    x[i] = random(BALLSIZE+BORDERSIZE, TV.hres()-BALLSIZE-BORDERSIZE-1);
    y[i] = random(BALLSIZE+BORDERSIZE, TV.vres()-BALLSIZE-BORDERSIZE-1);

    // Random direction
    xm[i] = random(2)*2 - 1;
    ym[i] = random(2)*2 - 1;
  }

  // Initialize player.
  px = TV.hres()/2;
  py = TV.vres()/2;

  playerFrame = 0;
  playerRate = 0;

  // We will do our own control loop here.
  while(1)
  {
    // Wait for end of screen to be drawn.
    TV.delay_frame(1);

    for (i=0; i<BALLS; i++)
    {
      // Erase balls.
      TV.draw_circle(x[i], y[i], BALLSIZE, BLACK);

      x[i] = x[i] + xm[i];
      if (x[i]<=BALLSIZE+BORDERSIZE || x[i]>=TV.hres()-BALLSIZE-BORDERSIZE-1)
      {
        xm[i] = -xm[i];
        x[i] = x[i] + xm[i];
      }
      y[i] = y[i] + ym[i];
      if (y[i]<=BALLSIZE+BORDERSIZE || y[i]>=TV.vres()-BALLSIZE-BORDERSIZE-1)
      {
        ym[i] = -ym[i];
        y[i] = y[i] + ym[i];
      }

      TV.draw_circle(x[i], y[i], BALLSIZE, WHITE);
    }

    // Erase player
    TV.draw_rect(px, py, PLAYERW, PLAYERH, BLACK, BLACK);

    // Read joystick (0-1023) and convert to screen resolution.
    px = analogRead(ANALOGXPIN)/(1024/TV.hres());
    if (px<BORDERSIZE)
    {
      px = BORDERSIZE;
    }
    else if (px>=TV.hres()-PLAYERW-BORDERSIZE-1)
    {
      px = TV.hres()-PLAYERW-BORDERSIZE-1;
    }

    py = analogRead(ANALOGYPIN)/(1024/TV.vres());
    if (py<BORDERSIZE)
    {
      py = BORDERSIZE;
    }
    else if (py>=TV.vres()-PLAYERH-BORDERSIZE-1)
    {
      py = TV.vres()-PLAYERH-BORDERSIZE-1;
    }

    // Draw player.
    TV.bitmap(px, py, pacman[playerFrame]);

    playerRate++;
    if (playerRate>=PFRAMES)
    {
      playerRate = 0;
      playerFrame++;
      if (playerFrame>=PFRAMES)
      {
        playerFrame = 0;
      }
    }
  }
}

And now I had an animated Pac-Man I could move around the screen… At this point, I think I had decided I would try to draw a Pac-Man maze and see if I could move the Pac-Man around in it.

In the next update, we will look at how I recreated the Pac-Man maze.

3 thoughts on “Arduino Pac-Man part 3 – animating bitmaps

  1. Pingback: Arduino Pac-Man part 4 – maze | Sub-Etha Software

  2. Pingback: Arduino Pac-Man part 5 – dot dilemma | Sub-Etha Software

  3. Pingback: Ardunio Pac-Man part 8 – dot dilemma 2 | Sub-Etha Software

Leave a Reply