Skip to content

3. Sprites and assets

Sprites

Most games draw with sprites: small images cut from a larger spritesheet. This lesson packs a sheet into the game, then draws, animates, flips, rotates and scales sprites from it.

The spritesheet

The examples share this sheet, docs/examples/shared/sprites.png, shown here at 4× size:

The examples' spritesheet

It's an ordinary PNG with transparency, 128×8 pixels, holding sixteen 8×8 sprites. Sprites are numbered from the top-left, along each row: the chili is sprite 0, the player is 1 and 2, the heart 3, and so on.

The SDK works in 8×8 cells. Bigger sprites are blocks of cells, like a 16×16 sprite made of 2×2 cells. Keep sheets 128 pixels wide (16 cells per row) so they also work with tile maps.

Packing assets into the game

Images aren't loaded from files at run time. The 32blit tool converts them into C++ arrays at build time, which end up in flash with your code. List them in assets.yml:

assets.yml
assets.cpp:
  assets/sprites.png:
    name: asset_sprites

and add the file to your build in CMakeLists.txt:

blit_assets_yaml(${PROJECT_NAME} assets.yml)

The build creates assets.cpp and assets.hpp. Include the header to use the data:

#include "assets.hpp"   // declares asset_sprites and asset_sprites_length

The asset pipeline guide covers palettes, transparency colours, maps, fonts and raw data.

Drawing sprites

sprites.cpp
// Sprites: draw from a spritesheet, animate, flip, rotate and scale.
//
// The sheet is assets/sprites.png, a grid of 8x8 sprites packed into the
// game by assets.yml. Sprite 0 is the top-left one, counting along each row.
#include "32blit.hpp"
#include "assets.hpp"

using namespace blit;

// sprite indices in the sheet
enum Sprite : uint16_t {
  CHILI = 0, PLAYER_WALK_1 = 1, PLAYER_WALK_2 = 2, HEART = 3, COIN = 4, BOMB = 5, STAR = 6
};

void init() {
  set_screen_mode(ScreenMode::hires);

  // load the packed image into a surface and make it the active spritesheet
  screen.sprites = Surface::load(asset_sprites);
}

void update(uint32_t time) {
}

void render(uint32_t time) {
  screen.pen = Pen(40, 60, 90);
  screen.clear();

  screen.pen = Pen(255, 255, 255);
  screen.text("Index, Point, Rect", minimal_font, Point(4, 2));

  // three ways to pick a sprite: by index...
  screen.sprite(CHILI, Point(8, 14));
  // ...by its column and row in 8x8 units...
  screen.sprite(Point(3, 0), Point(24, 14));
  // ...or a block of several sprites as one (here 2x1 sprites wide)
  screen.sprite(Rect(1, 0, 2, 1), Point(40, 14));

  screen.text("Animation", minimal_font, Point(4, 28));
  // swap between two frames every 200 ms
  uint16_t frame = (time / 200) % 2 ? PLAYER_WALK_1 : PLAYER_WALK_2;
  screen.sprite(frame, Point(8, 40));

  // walk across the screen, flipping to face the direction of travel
  int t = (time / 16) % 200;
  int x = t < 100 ? t : 200 - t;
  screen.sprite(frame, Point(20 + x, 40), t < 100 ? SpriteTransform::NONE : SpriteTransform::HORIZONTAL);

  screen.text("Transforms", minimal_font, Point(4, 54));
  const SpriteTransform transforms[] = {
    SpriteTransform::NONE, SpriteTransform::HORIZONTAL, SpriteTransform::VERTICAL,
    SpriteTransform::R90, SpriteTransform::R180, SpriteTransform::R270
  };
  for(int i = 0; i < 6; i++)
    screen.sprite(CHILI, Point(8 + i * 14, 66), transforms[i]);

  screen.text("Scaling", minimal_font, Point(4, 80));
  // scale around the sprite's centre (origin 4,4) so it grows in place
  float scale = 1.0f + 1.5f * (0.5f + 0.5f * sinf(time / 300.0f));
  screen.sprite(COIN, Point(20, 106), Point(4, 4), scale);
  screen.sprite(HEART, Point(52, 106), Point(4, 4), 3.0f);
  screen.sprite(BOMB, Point(84, 106), Point(4, 4), Vec2(4.0f, 2.0f));
  screen.sprite(STAR, Point(112, 106), Point(4, 4), 2.0f);
}

Loading the sheet

screen.sprites = Surface::load(asset_sprites);

Surface::load unpacks the image into a new surface in RAM. Setting screen.sprites makes it the sheet that screen.sprite() draws from. Load once, in init(): loading allocates memory each time.

If your game has several sheets, keep each Surface * and switch screen.sprites between them as you draw.

Choosing a sprite

screen.sprite(which, position) draws a sprite with its top-left corner at position. There are three ways to say which sprite:

which Means
5 sprite number 5, counting along the rows
Point(5, 0) the cell at column 5, row 0
Rect(1, 0, 2, 2) a block of cells, 2 wide and 2 tall, starting at column 1, row 0: a 16×16 sprite

Animation

Animation is choosing a different sprite as time passes:

uint16_t frame = (time / 200) % 2 ? PLAYER_WALK_1 : PLAYER_WALK_2;

For longer animations, keep the frames in an array:

const uint16_t walk[] = {16, 17, 18, 17};
uint16_t frame = walk[(time / 120) % 4];

Flipping and rotating

The last argument is a SpriteTransform: HORIZONTAL and VERTICAL flips, and R90, R180, R270 rotations. Flip a character horizontally to make them face the other way, so you only need to draw them facing one way.

Scaling

With an origin and a scale, the sprite is scaled around the origin, measured in pixels from the sprite's top-left corner. Use the middle of the sprite (Point(4, 4) for an 8×8) to grow it in place; the position is then where the middle goes.

screen.sprite(COIN, Point(20, 106), Point(4, 4), 2.0f);           // twice the size
screen.sprite(BOMB, Point(84, 106), Point(4, 4), Vec2(4.0f, 2.0f)); // stretched

Scaling looks best with whole-number factors on pixel art.

Blitting whole images

screen.blit() copies any rectangle of one surface onto another, without the 8×8 grid. It's how you draw a background picture or a title image:

Surface *title = Surface::load(asset_title);
...
screen.blit(title, Rect(0, 0, 128, 64), Point(0, 20));

stretch_blit() does the same while resizing to a destination rectangle. See Surface.

Memory

A loaded sheet is uncompressed: an 8-bit paletted image takes one byte per pixel, plus its palette. A 128×128 sheet is 16 KB. That matters on the console, where a game has 448 KB of RAM in total. If a sheet never changes, Surface::load_read_only() points straight at the data in flash instead of copying it (for unpacked images only; set packed: no on the asset).

Try it

  • Draw a row of hearts for lives, as a game's HUD would.
  • Make the walking player in the example move up and down too, and use R90/R270 so they face their direction.
  • Draw your own 8×8 sprite in an image editor, add it to the sheet, and show it.

Next: text.