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:

  1. Draw the background
  2. Compute the position of the sprites
  3. Draw sprites
  4. 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:

  1. Draw the background
  2. Jump to step 4
  3. Restore the overwritten background parts
  4. Compute the position of the sprites
  5. Save the background in the sprites locations
  6. Draw sprites
  7. 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.


0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

By continuing to browse this Website, you consent to the use of cookies. More information

This Website uses:

By continuing to browse this Website without changing the cookies settings of your browser or by clicking "Accept" below, you consent to the use of cookies.

Close