Topics

► Games

► Sound & Music

► Watches & Clocks

► GPS

► Power Supplies

► Computers

► Graphics

► Thermometers

► Wearables

► Test Equipment

► Tutorials

► Libraries

► PCB-Based Projects

By processor

AVR ATtiny

► ATtiny10

► ATtiny2313

► ATtiny84

► ATtiny841

► ATtiny85

► ATtiny861

► ATtiny88

AVR ATmega

► ATmega328

► ATmega1284

AVR 0-series and 1-series

► ATmega4809

► ATtiny1604

► ATtiny1614

► ATtiny3216

► ATtiny3227

► ATtiny402

► ATtiny404

► ATtiny414

► ATtiny814

AVR DA/DB-series

► AVR128DA28

► AVR128DA32

► AVR128DA48

► AVR128DB28

ARM

► ATSAMD21

► RP2040

► RA4M1

About me

  • About me
  • Twitter
  • Mastodon

Feeds

RSS feed

Simple Sprite Routines for the Wio Terminal

17th August 2020

This article describes four routines designed to make it easy to write games in C that use sprites on the Seeed Studio Wio Terminal. They allow you to plot erasable sprites, check sprites for collision, and move sprites on the screen while avoiding flicker.

These routines allow you to write simple games in just a few lines of C, so they're ideal for beginners to learn programming. To demonstrate I've included a simple maze game in which you roll a ball through a random maze by tilting the Wio Terminal, taking advantage of the board's accelerometer:

 Maze.jpg

Rolling ball maze game on a Seeed Studio Wio Terminal, demonstrating how
 to use these routines to move a sprite and do collision detection.

Introduction

My earlier post Simple Sprite Routines for the PyGamer/PyBadge described sprite routines for the Adafruit PyGamer or PyBadge, game pads incorporating a 1.8" 160x128 colour TFT display. Another interesting platform for games is the Seeed Studio Wio Terminal [1]. Like the Adafruit boards it’s based on the ATSAMD51 ARM Cortex M4 microcontroller running at 120 MHz, with 512 KB flash and 192 KB RAM, and an LIS3DHTR accelerometer. However, it includes larger a 2.4” 320x240 colour TFT display, and also features a microphone, buzzer, microSD card slot, light sensor, and infrared emitter.

The Wio Terminal Arduino core already provides a routine to read the colour value of a point on the display, so making a version of my sprite routines for the Wio Terminal was relatively simpler.

This article provides an xorPixel routine that plots a point using exclusive-OR plotting, xorSprite that plots an 8 x 8 sprite using exclusive-OR plotting, hitSprite that checks whether a sprite has hit a pixel of specified colour on the display, and moveSprite that moves a sprite in a specified direction, avoiding flicker by plotting only the pixels that have changed.

Together these routines let you write simple games based on graphics drawn on the screen, without needing any work behind the scenes. As an example I've provided a rolling-ball maze game in which you have to navigate a ball to the goal by tilting the Wio terminal, using the built-in accelerometer to detect the tilt. The behaviour of the game is almost complely defined by the graphics drawn on the screen, so to change it all you have to do is draw a different maze.

Here's a description of the four new routines:

xorPixel()

Plots a pixel at the specified X, Y coordinates by exclusive-ORing the specified colour with the colour that's already there:

xorPixel (uint16_t x, uint16_t y, uint16_t colour)

On a black display this has the same effect as drawPixel(). On a white display this will draw in the inverse colour; for example, blue will appear as yellow, because 0x001F (blue) ^ 0xFFFF (white) = 0xFFE0 (yellow).

On any background, plotting a second time with the same colour will undo the effect of the first plot, restoring the background.

xorSprite()

Plots an 8x8 sprite with its top left pixel at the specified x, y coordinates:

xorSprite (uint16_t x0, uint16_t y0, uint64_t sprite, uint16_t color)

Sprite.png

The colour should be a 16-bit number in the same format as other colours in the GFX library, with 5-6-5 bits specifying the red, green, and blue respectively. Bits that are '1' in the sprite will be plotted by exclusive-ORing the specified colour with the colour that's already there. Bits that are '0' will remain in the background colour.

On any background, plotting a sprite a second time with the same colour will undo the effect of the first plot, restoring the background. You could therefore use xorSprite() to move a sprite across the screen, but using moveSprite() described below is preferable because it will achieve the same effect without flicker.

The sprite should be an 8x8 bitmap which you can define in C like this:

const uint64_t Sprite = 0b\
00111100\
01000010\
10000001\
10100101\
10000001\
10011001\
01000010\
00111100;

For example, this is the definition of the Smiley shown above. The '\' at the end of each line lets you insert linebreaks so you can visualise the sprite.

You can create larger sprites by tiling two or more sprites.

Although xorSprite() only plots in a single colour, you can create multicolour sprites by overlaying different colour bitmaps at the same x, y coordinates. In fact you can create a three-colour sprite with two calls to xorSprite(), a bit like overprinting with two different coloured inks.

hitSprite()

Performs collision detection between an 8x8 sprite with its top left pixel at the specified x, y coordinates with anything on the display in the specified colour:

bool hitSprite (uint16_t x0, uint16_t y0, uint64_t sprite, uint16_t color)

It returns true if a hit has been detected, or false (zero) if not. Only bits that are '1' in the sprite are checked.

If you want to have invisible barriers draw them in a colour that's only one different from the background, so the difference is imperceptible on the display. For example, if your background is black, 0x0000, draw your barriers in the colour 0x0001.

moveSprite()

Moves an 8x8 sprite from the specified x, y coordinates by one pixel in any direction, specified by dx and dy:

moveSprite(uint16_t x0, uint16_t y0, uint64_t sprite, int dx, int dy, uint16_t color)

The moveSprite() routine avoids flicker by plotting only the pixels that have changed between the sprite's two positions. The plotting is done by exclusive-ORing the specified colour with the colour that's already there, ensuring that any background is unaffected.

Rolling ball maze game

The following example program draws a random maze which you try to solve by rolling a ball through the maze, tilting the Wio Terminal to move it. When you reach the goal, indicated by a yellow star, the Wio Terminal's speaker beeps. You can then press the leftmost button above the display to draw a new random maze and start again.

Initialising the colours and sprite

The program first defines the colours used in the game, and sprites for the ball and star:

const uint16_t goalCol = TFT_YELLOW;
const uint16_t mazeCol = 0xF80F;      // Red + some blue
const uint16_t ballCol = TFT_WHITE;
const uint16_t boardCol = TFT_BLACK;

const uint64_t Sprite = 0b\
00111100\
01111110\
11111111\
11111111\
11111111\
11111111\
01111110\
00111100;

const uint64_t Star = 0b\
00011000\
01011010\
00111100\
11111111\
11111111\
00111100\
01011010\
00011000;

The approach

There are many alternative methods for drawing a random maze; the one I chose is called recursive backtracking because it's one of the simplest, and produces good mazes.

The maze is drawn on a 22 x 16 grid, with its origin at (mx,my). The walls are 2 pixels thick, and the cells 12 pixels square. These parameters are defined by constants so they should be easy to change:

const int tftWidth=320, tftHeight=240;
const int mx = 5, my = 7;             
const int mWidth = 22, mHeight = 16;
const int cellSize=12, gap=14;

The usual way to construct a maze would be to use a two-dimensional array to represent the maze, with each element of the array containing a four-bit value to specify which of the four walls are present. A cell that hasn't already been visited would have a zero value.

However I chose a simpler approach, which doesn't need a separate array to represent the maze. Starting from a solid rectangle we simply draw the paths of the maze on the display as we construct it; to see if a cell has already been visited we just read the colour of a pixel in the cell.

First we define arrays dx[] and dy[] to help calculate the coordinates of a cell in each of the four compass directions:

// Directions N  E  W  S
int dx[4] = { 0, 1,-1, 0 };
int dy[4] = {-1, 0, 0, 1 };

The directions are arranged so that we can easily calculate the opposite of a direction:

int opposite (int dir) { return 3 - dir; }

Initialising the maze

First we initialise the maze by clearing the display, and then drawing a rectangle filled with the maze colour (red). Initially the top left cell, corresponding to maze coordinates (0,0), is cleared to the board colour (black):

void initMaze() {
  tft.fillScreen(TFT_BLACK);
  tft.fillRect(mx, my, gap*mWidth+2, gap*mHeight+2, mazeCol);
  tft.fillRect(mx+2, my+2, cellSize, cellSize, boardCol);
}

Drawing the maze

The maze is actually constructed by the recursive routine drawMaze(). It is called with a starting cell (ax,ay), and checks the four cells in each direction from this, N, E, W, and S. The order in which they are checked is randomised to ensure that each maze is different.

The second cell is (bx,by). If this a valid cell, and contains the maze colour, then it hasn't already been visited. This cell is then filled with the board colour, and the wall between (ax,ay) and (bx,by) is removed by drawing a black square over it.

Then drawMaze() is called recursively with the coordinates of the second cell (bx,by).

Here's the whole definition of drawMaze():

void drawMaze (int ax, int ay, int depth) {
  if (depth > maxDepth) { gx = ax; gy = ay; maxDepth = depth; }
  int r = random(4);
  for (int d=0; d<4; d++) {
    int dir = (d+r) % 4;
    int bx = ax + dx[dir], by = ay + dy[dir];
    int x = mx+bx*gap+2, y = my+by*gap+2;
    if (bx>=0 && bx<mWidth && by>=0 && by<mHeight && tft.getPixel(x,y)==mazeCol) {
      tft.fillRect(x, y, cellSize, cellSize, boardCol);
      x = mx+(ax+bx)*gap/2+2, y = my+(ay+by)*gap/2+2;
      tft.fillRect(x, y, cellSize, cellSize, boardCol);
      drawMaze(bx, by, depth++);
    }
  }
}

One additional feature of the routine is that it sets (gx,gy) to the coordinates of the cell at the maximum depth of recursion. This is a simple way of finding a good goal for solving the maze.

If you want to see the maze being drawn recursively, add a delay in the drawMaze() routine by adding a line:

delay(100);

before the line:

drawMaze(bx, by, depth++);

Setup routine

The setup() routine initialises the display, LIS3DH accelerometer, and push button:

void setup() {
  tft.init();
  tft.setRotation(3);
  Wire1.begin();
  lis3dhRate(2);
  pinMode(Button1, INPUT_PULLUP);
}

Rolling the ball

The main loop() function calls initMaze() and drawMaze() to draw a random maze, and then moves the ball according to the inputs from the accelerometer.

First a new maze is drawn:

  maxDepth = 0;
  initMaze();
  drawMaze(0,0,0);
  tft.xorSprite(mx+gx*gap+4, my+gy*gap+4, Star, goalCol);
  int x = mx+4, y = my+4, dx, dy, won = false;
  tft.xorSprite(x, y, Sprite, ballCol);

The main loop then runs repeatedly unless the push button is pressed. First the X and Y values of the accelerometer are read:

 while(digitalRead(Button1)) {
    // Move ball?
    int sx = -lis3dhXYZ(1) / Sens;
    int sy = lis3dhXYZ(0) / Sens;

The variables dx and dy are each set to -1, 0, or 1 depending on whether the board is tilted in the corresponding direction:

    // Set dx and dy to -1, 0, or +1
    dx = (sx > 0) - (sx < 0);
    dy = (sy > 0) - (sy < 0);

If moving in the X or Y direction causes the ball to hit the maze, the corresponding movement is cancelled by setting it to zero:

    if (tft.hitSprite(x+dx, y, Sprite, mazeCol)) dx = 0;
    if (tft.hitSprite(x, y+dy, Sprite, mazeCol)) dy = 0;

If the ball is over the star, three beeps are played to indicate that you've reached the goal:

    if (tft.hitSprite(x, y, Sprite, goalCol^ballCol) & !won) {
      for (int n=0; n<3; n++) { tone(Speaker, 880, 100); delay(200); }
      noTone(Speaker);
      won = true;
    }

The collision detection checks for the colour goalCol^ballCol, because the ball is plotted over the star.

Finally, the ball sprite is moved in the appropriate direction, and its x,y position is updated:

    tft.moveSprite(x, y, Sprite, dx, dy, ballCol);
    x = x + dx; y = y + dy;
    delay(10);

Here are the Wio Terminal sprite routines together with the maze demo: Wio Terminal Sprite Routines.


  1. ^ Wio Terminal on Seeed Studio.

blog comments powered by Disqus