The term sprite refers to a two-dimensional bitmap image, designed to be part of a larger scene (the “background”). Originally, when processors were not yet powerful enough to handle them, sprites were composited with the background using dedicated hardware. Background and sprites remained completely separated, and the latter could be moved at will on the screen without needing to draw anything with the CPU. This advantage was offset by the limitations of the graphics chips of the time that required sprites to be rather small and limited in number.
With the increase in computing power, the term sprite began to be used to refer to any bitmap used as part of a graphics display, even if drawn into a frame buffer instead of being composited on-the-fly at display time.
In the absence of dedicated hardware, other techniques for compositing images in the frame buffer such as bit blitting were developed. In bit blitting, two or more images are combined bit-wise using boolean operations. A classic use for blitting is to render a transparent sprite onto a background using a mask.
Unlike hardware sprite, every time a bitmap is drawn in the frame buffer, the underlying background content is overwritten. It’s up to the software to restore the background before drawing the sprite in its new location.
How we will handle sprites
Our #DIYConsole does not have hardware that can handle sprites, so we’ll have to directly manipulate the screen buffer. The easiest way to draw a game screens is as follows:
- Draw the background
- Compute the position of the sprites
- Draw sprites
- Restart from step 1
For these operations we will develop two functions to draw bitmaps on the screen: a faster one for images without transparency (especially useful for the background) and one that allows you to draw sprites with transparency. For sprites with transparency we will not use bit blitting, which is a flexible but rather slow technique, but we will simply use a color index to indicate where the sprite is transparent.
There are various ways to optimize the drawing of a game screen. For example, in many cases it is not necessary to redraw the entire background but it’s enough to redraw only the parts where there are sprites. To make this easier, we will develop a function that copies a background rectangle that can be used later to restore it when it’s overwritten by a sprite. Using this function, the procedure for drawing the game screens becomes:
- Draw the background
- Jump to step 4
- Restore the overwritten background parts
- Compute the position of the sprites
- Save the background in the sprites locations
- Draw sprites
- Restart from step 3
Bitmap representation
We will store a bitmap in memory simply as an array of pixels, where each pixel is a byte used to index the palette. The image is stored in the array as a sequence of rows.
Bitmap functions
Add this declarations to the GFX class:
void drawBitmap(uint8_t* bitmap, int16_t x, int16_t y, uint16_t width, uint16_t height);
void drawTransparentBitmap(uint8_t* bitmap, int16_t x, int16_t y, uint16_t width, uint16_t height, uint8_t transparentColor);
void copyScreenBufferRect(uint8_t* buffer, int16_t x, int16_t y, uint16_t width, uint16_t height);
and this code to GFX.cpp:
void GFX::drawBitmap(uint8_t* bitmap, int16_t x, int16_t y, uint16_t width, uint16_t height) {
// Check if bitmap is outside the screen
if(x >= 320) return;
if(y >= 480) return;
if(x + width - 1 < 0) return;
if(y + height - 1 < 0) return;
// Offset to get the bitmap pixels
int16_t uOffset = -x;
int16_t vOffset = -y;
// Calculate the visible part of the bitmap
uint16_t bitmapWidth = width;
cropToViewSize(&x, &width, 320);
int16_t yEnd = cropToViewSize(&y, &height, 480);
// Copy the bitmap to screen buffer line by line
uint16_t u = x + uOffset;
uint16_t v = y + vOffset;
for( ; y <= yEnd ; y++, v++) {
int bitmapOffset = bitmapWidth * v + u;
if(SCREENBUFFER_SECTOR(y)) {
int ysb = y - 256;
int screenBufferOffset = 320 * ysb + x;
memcpy(screenBuffer[1] + screenBufferOffset, bitmap + bitmapOffset, width);
} else {
int screenBufferOffset = 320 * y + x;
memcpy(screenBuffer[0] + screenBufferOffset, bitmap + bitmapOffset, width);
}
}
}
void GFX::drawTransparentBitmap(uint8_t* bitmap, int16_t x, int16_t y, uint16_t width, uint16_t height, uint8_t transparentColor) {
// Check if bitmap is outside the screen
if(x >= 320) return;
if(y >= 480) return;
if(x + width - 1 < 0) return;
if(y + height - 1 < 0) return;
// Offset to get the bitmap pixels
int16_t uOffset = -x;
int16_t vOffset = -y;
// Calculate the visible part of the bitmap
uint16_t bitmapWidth = width;
int16_t xEnd = cropToViewSize(&x, &width, 320);
int16_t yEnd = cropToViewSize(&y, &height, 480);
// Copy the bitmap to screen buffer
uint16_t uStart = x + uOffset;
for(uint16_t v = y + vOffset; y <= yEnd; y++, v++) {
uint16_t u = uStart;
for(uint16_t xp = x; xp <= xEnd; xp++, u++) {
int bitmapOffset = bitmapWidth * v + u;
if(bitmap[bitmapOffset] != transparentColor) {
if(SCREENBUFFER_SECTOR(y)) {
int ysb = y - 256;
int screenBufferOffset = 320 * ysb + xp;
screenBuffer[1][screenBufferOffset] = bitmap[bitmapOffset];
} else {
int screenBufferOffset = 320 * y + xp;
screenBuffer[0][screenBufferOffset] = bitmap[bitmapOffset];
}
}
}
}
}
void GFX::copyScreenBufferRect(uint8_t* buffer, int16_t x, int16_t y, uint16_t width, uint16_t height) {
// Check if rect is outside the screen
if(x >= 320) return;
if(y >= 480) return;
if(x + width - 1 < 0) return;
if(y + height - 1 < 0) return;
// Offset to get the rect pixels
int16_t uOffset = -x;
int16_t vOffset = -y;
// Calculate the visible part of the bitmap
uint16_t rectWidth = width;
cropToViewSize(&x, &width, 320);
int16_t yEnd = cropToViewSize(&y, &height, 480);
// Copy the screen buffer rect to the buffer line by line
uint16_t u = x + uOffset;
uint16_t v = y + vOffset;
for( ; y <= yEnd ; y++, v++) {
int rectOffset = rectWidth * v + u;
if(SCREENBUFFER_SECTOR(y)) {
int ysb = y - 256;
int screenBufferOffset = 320 * ysb + x;
memcpy(buffer + rectOffset, screenBuffer[1] + screenBufferOffset, width);
} else {
int screenBufferOffset = 320 * y + x;
memcpy(buffer + rectOffset, screenBuffer[0] + screenBufferOffset, width);
}
}
}
Scaling and rotating bitmaps
The geometry of a bitmap can be manipulated by applying transformations. The two most common transformations are scaling and rotation, so we will develop a function that can apply a combination of these two.
There are several methods to scale and rotate a bitmap. The algorithm we will use consists in applying a scaling/rotation matrix to each point of the destination bitmap. For each point, the matrix produces a corresponding point in the source bitmap, from which we will retrieve the color to be written to the destination point.
Add this declaration to the GFX class:
void scaleAndRotateBitmap(uint8_t* destination, uint16_t* destinationWidth, uint16_t* destinationHeight, uint8_t* source, uint16_t sourceWidth, uint16_t sourceHeight, float scaleX, float scaleY, float rotation, uint_fast16_t backgroundColor = 0);
and this code to GFX.cpp:
void GFX::scaleAndRotateBitmap(uint8_t* destination, uint16_t* destinationWidth, uint16_t* destinationHeight, uint8_t* source, uint16_t sourceWidth, uint16_t sourceHeight, float scaleX, float scaleY, float rotation, uint_fast16_t backgroundColor) {
float sinTheta = sin(0.0174532925199 * rotation);
float cosTheta = cos(0.0174532925199 * rotation);
// Calculate width and height of resulting bitmap
int w1 = scaleX * sourceWidth * cosTheta + scaleY * sourceHeight * sinTheta;
int w2 = scaleX * sourceWidth * cosTheta - scaleY * sourceHeight * sinTheta;
*destinationWidth = max(abs(w1), abs(w2));
int h1 = scaleX * sourceWidth * sinTheta + scaleY * sourceHeight * cosTheta;
int h2 = scaleX * sourceWidth * sinTheta - scaleY * sourceHeight * cosTheta;
*destinationHeight = max(abs(h1), abs(h2));
// Map every pixel of destination bitmap in source bitmap
float sourceCenterX = 0.5 * (sourceWidth - 1);
float sourceCenterY = 0.5 * (sourceHeight - 1);
float destinationCenterX = 0.5 * (*destinationWidth - 1);
float destinationCenterY = 0.5 * (*destinationHeight - 1);
for(float v = -destinationCenterY; v <= destinationCenterY; v = v + 1) {
for(float u = -destinationCenterX; u <= destinationCenterX; u = u + 1) {
int sx = sourceCenterX + (cosTheta * u + sinTheta * v) / scaleX;
int sy = sourceCenterY + (cosTheta * v - sinTheta * u) / scaleY;
int destinationOffset = *destinationWidth * (v + destinationCenterY) + u + destinationCenterX;
if(sx >= 0 && sx < sourceWidth && sy >= 0 && sy < sourceHeight) {
int sourceOffset = sourceWidth * sy + sx;
destination[destinationOffset] = source[sourceOffset];
} else {
destination[destinationOffset] = backgroundColor;
}
}
}
}
Testing bitmap functions
To test bitmap functions, we will write a small program that implements the drawing procedure previously seen.
We will create a starry background and draw a starship that moves over it. For each frame we will save the background rectangle that we will overwrite with the starship, then restore it in the next frame, and so on.
First, create a new file, bitmaps.h, that will contain the starship bitmap data:
/* bitmaps.h */
#ifndef _BITMAPS_H
#define _BITMAPS_H
#include <stdint.h>
// Starship 32x32 pixels
uint8_t starship[1024] = {
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 12, 0, 0, 12, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 12, 0, 0, 12, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 12, 0, 0, 12, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 15, 15, 12, 0, 0, 12, 15, 15, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 15, 12, 0, 0, 12, 15, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 8, 15, 15, 15, 15, 15, 15, 15, 15, 12, 0, 0, 12, 0, 0, 12, 0, 0, 12, 15, 15, 15, 15, 15, 15, 15, 15, 8, 15, 15,
15, 8, 8, 15, 15, 15, 15, 15, 15, 15, 15, 15, 12, 0, 12, 0, 0, 12, 0, 12, 15, 15, 15, 15, 15, 15, 15, 15, 15, 8, 8, 15,
15, 8, 13, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 12, 12, 0, 0, 12, 12, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 8, 15,
15, 13, 13, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 12, 0, 0, 12, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 15,
15, 13, 13, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 12, 0, 0, 12, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 15,
15, 13, 13, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 12, 0, 12, 12, 0, 12, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 15,
15, 13, 13, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 7, 7, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 13, 13, 15,
15, 13, 13, 0, 0, 0, 0, 15, 15, 15, 15, 15, 3, 0, 12, 7, 7, 12, 0, 3, 15, 15, 15, 15, 15, 0, 0, 0, 0, 13, 13, 15,
15, 14, 13, 12, 12, 0, 0, 0, 0, 15, 15, 3, 0, 0, 7, 7, 7, 7, 0, 0, 3, 15, 15, 0, 0, 0, 0, 12, 12, 13, 14, 15,
15, 14, 14, 15, 15, 12, 12, 0, 0, 15, 15, 3, 0, 0, 7, 7, 7, 7, 0, 0, 3, 15, 15, 0, 0, 12, 12, 15, 15, 14, 14, 15,
15, 15, 14, 15, 15, 15, 15, 12, 12, 0, 0, 3, 0, 0, 6, 7, 7, 6, 0, 0, 3, 0, 0, 12, 12, 15, 15, 15, 15, 14, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 12, 0, 3, 0, 0, 0, 6, 6, 0, 0, 0, 3, 0, 12, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 12, 3, 0, 0, 0, 0, 0, 0, 0, 0, 3, 12, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 0, 0, 12, 15, 15, 3, 12, 12, 0, 0, 12, 12, 3, 15, 15, 12, 0, 0, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 0, 0, 12, 15, 15, 15, 15, 15, 15, 12, 12, 15, 15, 15, 15, 15, 15, 12, 0, 0, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 0, 0, 12, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 12, 0, 0, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 12, 12, 15, 15, 15, 15, 15, 15, 15, 15, 2, 2, 15, 15, 15, 15, 15, 15, 15, 15, 12, 12, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 3, 2, 2, 3, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 3, 2, 2, 3, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 3, 3, 3, 3, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 3, 3, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15
};
#endif
Then copy this code in your Arduino sketch or main.cpp file:
#include <Arduino.h>
#include "GFX.h"
#include "bitmaps.h"
GFX gfx;
uint8_t backgroundCopy[4624];
uint8_t scaledAndRotatedBitmap[4624];
float x, y, theta, xTarget, yTarget;
uint16_t width, height;
unsigned long lastUpdate;
void setup() {
// Start graphics library
gfx.begin();
// Draw stars background
for(int i=0; i<60; i++)
gfx.drawPixel(random(0,320), random(0,480), 14);
for(int i=0; i<30; i++)
gfx.drawPixel(random(0,320), random(0,480), 1);
for(int i=0; i<40; i++)
gfx.drawPixel(random(0,320), random(0,480), 2);
for(int i=0; i<40; i++)
gfx.drawPixel(random(0,320), random(0,480), 7);
// Prepare starship bitmap
x = 160;
y = 240;
theta = 0;
gfx.scaleAndRotateBitmap(scaledAndRotatedBitmap, &width, &height, starship, 32, 32, 1.5, 1.5, theta, 15);
// Copy background
gfx.copyScreenBufferRect(backgroundCopy, x-width/2, y-height/2, width, height);
// Draw starship
gfx.drawTransparentBitmap(scaledAndRotatedBitmap, x-width/2, y-height/2, width, height, 15);
// Set initial target
xTarget = random(0, 320);
yTarget = random(0, 480);
// Start time
lastUpdate = micros();
// Draw first frame
gfx.update();
}
void loop() {
// Calculate frame delta time
unsigned long now = micros();
float deltaTime = (float)(now - lastUpdate) / 1000000;
lastUpdate = now;
// Restore background
gfx.drawBitmap(backgroundCopy, x-width/2, y-height/2, width, height);
// Update starship rotation
float thetaTarget = atan2(yTarget - y, xTarget - x) * 57.2957795 + 90;
// Turn the starship to the target in the direction of the smallest angle
if(fabs(thetaTarget - theta) > 180) {
if(theta < 0)
theta += 360;
else
theta -= 360;
}
theta += max(min(3 * (thetaTarget - theta), 180), -180) * deltaTime;
// Update starship position
float xSpeed = 0.5 * (xTarget - x) * (1 / (1 + 0.1 * fabs(thetaTarget - theta)));
float ySpeed = 0.5 * (yTarget - y) * (1 / (1 + 0.1 * fabs(thetaTarget - theta)));
x = x + xSpeed * deltaTime;
y = y + ySpeed * deltaTime;
// If near the target, choose another random target
if((fabs(x - xTarget) < 40) && (fabs(y - yTarget) < 40)) {
xTarget = random(0, 320);
yTarget = random(0, 480);
}
// Prepare starship bitmap
gfx.scaleAndRotateBitmap(scaledAndRotatedBitmap, &width, &height, starship, 32, 32, 1.5, 1.5, theta, 15);
// Copy background
gfx.copyScreenBufferRect(backgroundCopy, x-width/2, y-height/2, width, height);
// Draw starship
gfx.drawTransparentBitmap(scaledAndRotatedBitmap, x-width/2, y-height/2, width, height, 15);
// Update screen
gfx.update();
}
If everything works properly, you should see the starship rotate and move across the screen.
You can find the complete code of this post here.
In the next post…
In the next post we will deal with fonts and writing strings on screen.