Sprites / SpritesB Library

From Arduboy Wiki
Revision as of 09:59, 11 August 2024 by Filmote (talk | contribs)
Jump to navigation Jump to search

What are Sprites?

Sprites are a computer graphic which can be rendered and moved on screen as a single unit.  In older systems such as the Commodore 64 and Atari, the sprite was rendered by hardware as an overlay to the normal screen image.  As the sprite is an overlay, it can be moved around without it affecting the background image.

The Arduboy library has support for sprites but due to the lack of a powerful graphics processor handles them differently.  When rendering a sprite, the image is mapped into a single display buffer that may already have a background image drawn on it.  If you move the sprite you need to regenerate the background from its old position.

Sprites Library

The standard Arduboy library includes a secondary library named Sprites which is implicitly available when you declare the Arduboy2 library within a sketch.

Using the Sprites library is easy and a sample is shown in the code below.

000  #include <Arduboy2.h> 
001
002  Arduboy2 arduboy;
003
004  const byte PROGMEM ball[] = {
005    16, 16, 
006    0xE0, 0xF8, 0xFC, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3E, 0x3E, 0xFC, 0xF8, 0xE0,
007    0x07, 0x1F, 0x3F, 0x7F, 0x7F, 0xFF, 0xFF, 0xDF, 0xDF, 0xCF, 0xE7, 0x63, 0x7B, 0x3F, 0x1F, 0x07,
008  };
009
010  void setup() {
011    arduboy.begin();
012  }
013 
014  void loop() {
015    arduboy.clear();
016    Sprites::drawOverwrite(10, 20, ball, 0);
017    arduboy.display();
018  }

The important lines to note are lines 004 - 008 which define our sprite graphic and line 016 which displays the graphic on the screen. In this example, its a ball that is 16 x 16 pixels in size. The first two numbers of the array indicate the size, the rest of the data is the graphic itself encoded using one of the graphics tools detailed below. Line 016 uses the Sprites library to render the ball graphic at postion 10,0 on the screen. It is using a predefined function, drawOverwrite(), to render the graphic over the top of any graphics already rendered. As all sprites are rectangular yet our ball is (obviously) round, it will erase the graphics in the 'corners'. The Sprites library provides additional functions to mask the round image thus allowing the ball to be rendered properly without the 'corners' erasing the corners. These functions are describe below.

SpritesB Library

The SpritesB library provides the same methods ass the Sprites library however these libraries have been optimised for speed. To invoke a SpritesB method, simply substitute the library designation with that of the SpritesB library. For example, Sprites::drawOverwrite(10, 20, ball, 0); would become SpritesB::drawOverwrite(10, 20, ball, 0);

Creating Your Own Sprites

Creating a Sprite

Sprites can be created in any graphical tool that can produce black and white PNG files - which is pretty much any tool! When developing sprites, it is important to note that they must be a multiple of 8 pixels high.

Pharap has created an excellent tool that can be downloaded here and Press Play on Tape have created one that can be used on the Arduboy itself here

Completed graphics can be converted by dragging and dropping the images onto the tool hosted here This will produce a snippet of code with the graphical data encoded into an array ready for you to paste into your application.

Dragging and dropping the following ball graphic produces:

const byte PROGMEM ball[] = {
  16, 16, 
  0xE0, 0xF8, 0xFC, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3E, 0x3E, 0xFC, 0xF8, 0xE0,
  0x07, 0x1F, 0x3F, 0x7F, 0x7F, 0xFF, 0xFF, 0xDF, 0xDF, 0xCF, 0xE7, 0x63, 0x7B, 0x3F, 0x1F, 0x07,
};

The data above has 34 bytes due to the size of the image. The first two indicate the width and height of the graphic - in this case 16 x 16 pixels. The remainder of the data - 32 bytes - describes the actual data to be displayed. Each element in the array describes 8 vertical pixels and as our image is 16 pixels in width the first row of 16 values describes the the upper 16x8 pixels of the image. The second row of values describes the lower 16x8 pixels of our image.

Creating a Sprite Mask

As eluded to earlier, the Sprites Library allows you to render sprites using a mask to allow them to be positioned over background graphics while controlling what areas are drawn and what areas are not. Consider the following graphic and mask:


When using a sprite and mask, each pixel in the graphic is considered with its corresponding pixel in the mask to determine what will be rendered. Pixels set white in the mask indicate that the pixel will be set to the value of the corresponding image pixel.  If a pixel in the image is white and the corresponding pixel in the mask is also white, then the resultant pixel to be rendered the screen will be white. Likewise, if the pixel in the image is black and the corresponding pixel in the mask is white, then the resultant pixel to be rendered the screen will be black.

Pixels set to black in the mask will be left unchanged - whatever colour the pixel is in the graphic will be rendered to the screen.

Masks can be created and converted using the same tools above. However, when copying the data to your program, remove the dimensions of the mask as they are not needed. Converting the grpahic and mask above results in the data below:

const byte PROGMEM ball[] = {
  16, 16, 
  0xE0, 0xF8, 0xFC, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3E, 0x3E, 0xFC, 0xF8, 0xE0,
  0x07, 0x1F, 0x3F, 0x7F, 0x7F, 0xFF, 0xFF, 0xDF, 0xDF, 0xCF, 0xE7, 0x63, 0x7B, 0x3F, 0x1F, 0x07,
};

const byte PROGMEM ball_Mask[] = {
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
};

As you can see the ball and mask are described in two separate arrays. This format is appropriate for the drawExternalMask() detailed below.

When using the drawExternalMask() function, both image and mask are passed to the function: Sprites::drawExternalMask(10, 20, ball, ball_Mask, 0, 0);

The drawPlusMask() function uses is a variation of the drawExternalMask()function which uses a single array of data instead of separate graphic and mask arrays. If you study the data in the array below, you will notice that the first byte is derived from the first byte of the graphics data and is followed by the first byte of the mask data. The next byte is the second byte of the graphics data followed by the second byte of the mask data and so on. Arranging the data this way allows the code to retrieve the data in the most optimal way and improves performance. The down side is that the mask cannot be shared across multiple, similar graphic images.

const byte PROGMEM ball_plusMask[] = {
  16, 16, 
  0xE0, 0xFF, 0xF8, 0xFF, 0xFC, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
  0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x3E, 0x00, 0x3E, 0x00, 0xFC, 0x00, 0xF8, 0x00, 0xE0, 0x00, 
  0x07, 0x00, 0x1F, 0x00, 0x3F, 0x00, 0x7F, 0x00, 0x7F, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xDF, 0x00, 
  0xDF, 0xFF, 0xCF, 0xFF, 0xE7, 0xFF, 0x63, 0xFF, 0x7B, 0xFF, 0x3F, 0xFF, 0x1F, 0xFF, 0x07, 0xFF, 

};

When using the drawPlusMask() function, the combined image and mask data are passed as a single parameter to the function: Sprites::drawPlusMask(10, 20, ball_plusMask, 0);

Using Sprites Masks

The Arduboy / Sprites library also provides some nice masking utilities that allow you to render a sprite over a background and have it take that background into account.  The drawing functions include:

  • drawOverwrite()
  • drawErase()
  • drawExternalMask()
  • drawPlusMask()
  • drawSefMasked()

But what do they each do?  Consider the following image and mask:


Using these images with the various draw functions produces different results:


drawOverwrite() -  The sprite is drawn by simply overwriting what was already there. A bit set to 1 in the frame will set the pixel to 1 in the buffer, and a 0 in the array will set a 0 in the buffer.  In the example below, the black corners of the ball are visible as the ball passes into the white area.



drawErase() -  Erases the sprite.  When "erasing" a sprite, bits set to 1 in the sprite will set the corresponding pixel in the background to 0.  Sprite bits set to 0 will not affect the background.  In the example below, the ball is not visible on the left hand side of the screen as the white portions of the image has set the background to black.  The right hand shows that the white sections of the image have ‘erased’ the white background.



drawExternalMask() and drawPlusMask() -  draw a sprite using a mask.  As the name implies, the drawExternalMask() function allows the image and mask to be nominated separately whereas drawPlusMask() uses a single image with the mask embedded within it.

When rendering a sprite, bits set to 1 in the mask indicate that the pixel will be set to the value of the corresponding image bit.  Bits set to 0 in the mask will be left unchanged.  This can be seen clearly as the ball moves into the right hand side of the background.  The top-left and bottom-right corners of the image are rendered as black as the mask is set to 1 in these areas which in turn ensures that the images pixels (both zeroes and ones) are rendered on the background.



drawSelfMasked() - Draw a sprite using only the bits set to 1.  Bits set to 1 in the frame will be used to draw the sprite by setting the corresponding pixel in the background to 1. Bits set to 0 in the sprite will remain unchanged in the background.

As you can see, when the ball moves onto the white and striped backgrounds the highlights on the ball are not visible on the solid white as the background is rendered (also white!) whereas the highlights are visible on the striped background.  This is simply due to the position of the ball that fortuitously placed the highlights over the black area between stripes on the background.

To ensure that the ball’s highlight was always visible, you could create a mask that was the exact shape and size of the ball and was solid – without a highlight – and use either of the drawExternalMask()  and drawPlusMask() functions.



Using the above image and mask, we get a nicely masked image that does not remove any background unnecessarily:



A sample application that demonstrates the various draw commands can be found here