Skip to content

Examples

Twelve small programs, each showing one part of the SDK. Every one builds for the desktop and for the VGC Zero, and the tutorials explain them. The source is in docs/examples.

Building them

cmake -B build.examples -S docs/examples
cmake --build build.examples
./build.examples/chili-catch/chili-catch

See building the examples for console builds.

The older and larger 32blit examples also build with this SDK. They were written for the 32blit's 320×240 screen, so most don't fit the VGC Zero's 128×128 one, but they are a good source of techniques. Some of them run in your browser.

Hello World

Hello World

The smallest complete game: text and a colour cycle.

Explained in: the tutorial

hello-world.cpp
// Hello World: the smallest complete game.
//
// Every game provides three functions: init() runs once at start-up,
// update() runs 100 times a second to move the game on, and render()
// runs once per frame to draw it.
#include "32blit.hpp"

using namespace blit;

void init() {
  // 128x128 pixels, the full VGC Zero screen
  set_screen_mode(ScreenMode::hires);
}

void update(uint32_t time) {
  // nothing to update yet
}

void render(uint32_t time) {
  // fade the background through the colour wheel, one turn every 10 seconds
  float hue = (time % 10000) / 10000.0f;
  screen.pen = hsv_to_rgba(hue, 0.6f, 0.4f);
  screen.clear();

  screen.pen = Pen(255, 255, 255);
  screen.text("Hello, VGC Zero!", minimal_font, Point(64, 60), true, TextAlign::center_center);

  screen.pen = Pen(200, 200, 200);
  screen.text(std::to_string(time / 1000) + " s", minimal_font, Point(64, 72), true, TextAlign::center_center);
}

Shapes

Shapes

Every drawing primitive: rectangles, circles, triangles, lines, polygons, pixels and transparency.

Explained in: the tutorial

shapes.cpp
// Shapes: every drawing primitive on one screen.
#include "32blit.hpp"

using namespace blit;

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

void update(uint32_t time) {
}

void render(uint32_t time) {
  screen.pen = Pen(20, 20, 30);
  screen.clear();

  // filled rectangle
  screen.pen = Pen(214, 40, 40);
  screen.rectangle(Rect(8, 8, 32, 20));

  // circle, given a centre and a radius
  screen.pen = Pen(70, 170, 60);
  screen.circle(Point(64, 18), 11);

  // triangle
  screen.pen = Pen(70, 120, 220);
  screen.triangle(Point(92, 28), Point(104, 6), Point(118, 28));

  // lines, and fast horizontal and vertical spans
  screen.pen = Pen(250, 210, 60);
  screen.line(Point(8, 40), Point(120, 56));
  screen.h_span(Point(8, 62), 112);
  screen.v_span(Point(124, 36), 30);

  // polygon from a list of points
  screen.pen = Pen(230, 130, 40);
  screen.polygon({Point(10, 76), Point(40, 70), Point(50, 96), Point(24, 104), Point(6, 92)});

  // single pixels: a small starfield that drifts left
  screen.pen = Pen(255, 255, 255);
  for(int i = 0; i < 24; i++) {
    int x = (i * 37 + 128 - (time / 40) % 128) % 128;
    screen.pixel(Point(x, 108 + (i * 7) % 18));
  }

  // alpha: a translucent rectangle over the shapes underneath
  screen.pen = Pen(255, 255, 255, 96);
  screen.rectangle(Rect(60, 70, 60, 34));

  // global alpha fades everything drawn after it
  screen.alpha = 128;
  screen.pen = Pen(250, 170, 180);
  screen.circle(Point(90, 87), 14);
  screen.alpha = 255;
}

Input

Input

Move a square with the d-pad, react to presses and releases, and list the held buttons.

Explained in: the tutorial

input.cpp
// Input: move a square with the d-pad and show which buttons are held.
#include "32blit.hpp"

using namespace blit;

Vec2 position(60, 60);
Pen colour(214, 40, 40);
int presses = 0;

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

void update(uint32_t time) {
  // update() runs every 10 ms, so 0.5 pixels per update is 50 pixels a second
  const float speed = 0.5f;

  // held buttons: test the bits of `buttons`
  if(buttons & Button::DPAD_LEFT)  position.x -= speed;
  if(buttons & Button::DPAD_RIGHT) position.x += speed;
  if(buttons & Button::DPAD_UP)    position.y -= speed;
  if(buttons & Button::DPAD_DOWN)  position.y += speed;

  // keep the square on screen
  position.x = std::max(0.0f, std::min(position.x, 120.0f));
  position.y = std::max(12.0f, std::min(position.y, 120.0f));

  // buttons.pressed only holds buttons that went down since the last update,
  // so this runs once per press however long the button is held
  if(buttons.pressed & Button::A) {
    colour = hsv_to_rgba((blit::random() % 360) / 360.0f, 0.8f, 1.0f);
    presses++;
  }

  // buttons.released is the same for buttons that were let go
  if(buttons.released & Button::B)
    position = Vec2(60, 60);
}

void render(uint32_t time) {
  screen.pen = Pen(20, 20, 30);
  screen.clear();

  screen.pen = colour;
  screen.rectangle(Rect(Point(position), Size(8, 8)));

  // list the held buttons along the top
  static const struct { Button button; const char *name; } names[] = {
    {Button::A, "A"}, {Button::B, "B"}, {Button::X, "X"}, {Button::Y, "Y"},
    {Button::DPAD_UP, "U"}, {Button::DPAD_DOWN, "D"}, {Button::DPAD_LEFT, "L"}, {Button::DPAD_RIGHT, "R"},
    {Button::MENU, "MENU"},
  };

  std::string held;
  for(auto &n : names) {
    if(pressed(n.button))
      held += std::string(n.name) + " ";
  }

  screen.pen = Pen(255, 255, 255);
  screen.text("Held: " + held, minimal_font, Point(2, 2));
  screen.text("A presses: " + std::to_string(presses), minimal_font, Point(2, 118));
}

Sprites

Sprites

Draw sprites by index, cell or block; animate them; flip, rotate and scale them.

Explained in: the tutorial

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);
}

Text

Text

The built-in fonts, fixed width, alignment, measuring and wrapping.

Explained in: the tutorial

text.cpp
// Text: built-in fonts, alignment, measuring and wrapping.
#include "32blit.hpp"

using namespace blit;

const std::string story =
  "The chili pepper is the fruit of plants in the genus Capsicum. "
  "This text is wrapped to fit the box below, breaking between words.";

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

void update(uint32_t time) {
}

void render(uint32_t time) {
  screen.pen = Pen(20, 20, 30);
  screen.clear();

  // the three built-in fonts
  screen.pen = Pen(255, 255, 255);
  screen.text("minimal_font", minimal_font, Point(2, 2));
  screen.text("fat_font", fat_font, Point(2, 12));
  screen.text("outline_font", outline_font, Point(2, 22));

  // variable = false uses the fixed character width, like a terminal
  screen.pen = Pen(250, 210, 60);
  screen.text("fixed", minimal_font, Point(126, 2), false, TextAlign::top_right);

  // alignment inside a rectangle
  Rect box(2, 34, 124, 30);
  screen.pen = Pen(60, 60, 80);
  screen.rectangle(box);
  screen.pen = Pen(255, 255, 255);
  screen.text("top left", minimal_font, box, true, TextAlign::top_left);
  screen.text("centre", minimal_font, box, true, TextAlign::center_center);
  screen.text("bottom right", minimal_font, box, true, TextAlign::bottom_right);

  // measure text to draw a background exactly behind it
  std::string label = "measured";
  Size size = screen.measure_text(label, minimal_font);
  screen.pen = Pen(214, 40, 40);
  screen.rectangle(Rect(Point(80, 18), size));
  screen.pen = Pen(255, 255, 255);
  screen.text(label, minimal_font, Point(80, 18));

  // wrap long text to a width, then draw it as several lines
  Rect para(2, 68, 124, 58);
  std::string wrapped = screen.wrap_text(story, para.w, minimal_font);
  screen.pen = Pen(200, 220, 255);
  screen.text(wrapped, minimal_font, para);
}

Audio

Audio

Four synth channels with different waveforms and envelopes, played from the buttons.

Explained in: the tutorial

audio.cpp
// Audio: play notes on the synth channels.
//
// A, B, X and Y each play a note with a different waveform. Up and down
// change the octave.
#include "32blit.hpp"

using namespace blit;

const char *wave_names[] = {"Square", "Triangle", "Saw", "Sine"};
int octave = 0;
int last_channel = -1;

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

  // channel 0: a short square-wave beep
  channels[0].waveforms = Waveform::SQUARE;
  channels[0].attack_ms = 5;
  channels[0].decay_ms = 100;
  channels[0].sustain = 0;   // no sustain, so the note ends by itself
  channels[0].release_ms = 0;

  // channel 1: a soft triangle that holds while the button is held
  channels[1].waveforms = Waveform::TRIANGLE;
  channels[1].attack_ms = 30;
  channels[1].decay_ms = 200;
  channels[1].sustain = 0xafff;
  channels[1].release_ms = 300;

  // channel 2: a buzzy saw
  channels[2].waveforms = Waveform::SAW;
  channels[2].attack_ms = 10;
  channels[2].decay_ms = 300;
  channels[2].sustain = 0x7fff;
  channels[2].release_ms = 100;

  // channel 3: a sine with a long release, like a bell
  channels[3].waveforms = Waveform::SINE;
  channels[3].attack_ms = 2;
  channels[3].decay_ms = 600;
  channels[3].sustain = 0;
  channels[3].release_ms = 600;
}

void play(int channel, uint16_t frequency) {
  channels[channel].frequency = frequency << octave;
  channels[channel].trigger_attack();
  last_channel = channel;
}

void update(uint32_t time) {
  const Button keys[] = {Button::A, Button::B, Button::X, Button::Y};
  const uint16_t notes[] = {262, 330, 392, 523}; // C4, E4, G4, C5

  for(int i = 0; i < 4; i++) {
    if(buttons.pressed & keys[i])
      play(i, notes[i]);

    // letting go moves a held note on to its release
    if(buttons.released & keys[i])
      channels[i].trigger_release();
  }

  if(buttons.pressed & Button::DPAD_UP)   octave = std::min(octave + 1, 2);
  if(buttons.pressed & Button::DPAD_DOWN) octave = std::max(octave - 1, 0);
}

void render(uint32_t time) {
  screen.pen = Pen(20, 20, 30);
  screen.clear();

  screen.pen = Pen(255, 255, 255);
  screen.text("A B X Y: play notes", minimal_font, Point(4, 4));
  screen.text("Up/Down: octave " + std::to_string(octave), minimal_font, Point(4, 14));

  // one bar per channel showing its envelope level
  for(int i = 0; i < 4; i++) {
    int y = 34 + i * 22;
    // adsr is the envelope level, 24-bit
    int level = channels[i].adsr >> 18; // 0..63
    if(channels[i].adsr_phase == ADSRPhase::OFF)
      level = 0;

    screen.pen = Pen(60, 60, 80);
    screen.rectangle(Rect(50, y, 70, 12));
    screen.pen = hsv_to_rgba(i / 4.0f, 0.7f, 1.0f);
    screen.rectangle(Rect(50, y, level + 6 * (level > 0), 12));

    screen.pen = i == last_channel ? Pen(255, 255, 255) : Pen(150, 150, 150);
    screen.text(wave_names[i], minimal_font, Point(4, y + 2));
  }
}

Saves

Saves

A five-second button-mashing game that keeps your best score with read_save, write_save and on_quit.

Explained in: the tutorial

saves.cpp
// Saves: keep a best score between sessions.
//
// Press A as many times as you can in 5 seconds. The best score is saved
// with write_save() and loaded again with read_save() the next time the
// game starts.
#include "32blit.hpp"

using namespace blit;

// Everything that is saved goes in one plain struct. Add a version number
// so a later build of the game can recognise (and upgrade) old saves.
struct SaveData {
  uint32_t version = 1;
  uint32_t best = 0;
  uint32_t games_played = 0;
};

SaveData save;

enum class State { ready, playing, done } state = State::ready;
uint32_t round_start = 0;
uint32_t score = 0;
bool new_best = false;

void save_now() {
  write_save(save);
}

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

  // read_save() returns false when there is no save yet (or it is the wrong size)
  if(!read_save(save) || save.version != 1)
    save = SaveData();

  // called when the player quits to the home screen (or closes the window on desktop)
  on_quit = save_now;
}

void update(uint32_t time) {
  switch(state) {
    case State::ready:
      if(buttons.pressed & Button::A) {
        state = State::playing;
        round_start = time;
        score = 0;
      }
      break;

    case State::playing:
      if(buttons.pressed & Button::A)
        score++;

      if(time - round_start >= 5000) {
        state = State::done;
        save.games_played++;
        new_best = score > save.best;
        if(new_best)
          save.best = score;

        // save at the end of every round too: switching the console off can't be caught
        save_now();
      }
      break;

    case State::done:
      if(buttons.pressed & Button::A)
        state = State::ready;
      break;
  }

  // Y clears the save file
  if(buttons.pressed & Button::Y) {
    remove_save();
    save = SaveData();
  }
}

void render(uint32_t time) {
  screen.pen = Pen(20, 20, 30);
  screen.clear();

  screen.pen = Pen(255, 255, 255);
  screen.text("Best: " + std::to_string(save.best), fat_font, Point(4, 4));
  screen.text("Played: " + std::to_string(save.games_played), minimal_font, Point(4, 16));

  std::string message;
  switch(state) {
    case State::ready:
      message = "Press A to start";
      break;
    case State::playing:
      message = std::to_string(score) + "\n" + std::to_string(5 - (time - round_start) / 1000) + " s left";
      break;
    case State::done:
      message = "Score " + std::to_string(score) + (new_best ? "\nNew best!" : "") + "\nA: again";
      break;
  }
  screen.text(message, fat_font, Rect(0, 30, 128, 70), true, TextAlign::center_center);

  screen.pen = Pen(150, 150, 150);
  screen.text("Y: delete save", minimal_font, Point(4, 118));
}

Timers and Tweens

Timers and Tweens

Repeating and counted timers, and tweens that slide and bob.

Explained in: the tutorial

timers-tweens.cpp
// Timers and tweens.
//
// A Timer calls a function after a delay, optionally repeating. A Tween
// moves a value from one number to another over time along a curve. Both
// are updated by the engine, you only start them.
#include "32blit.hpp"

using namespace blit;

Timer blink_timer;
Timer spawn_timer;
Tween slide;
Tween bounce;

bool blink_on = true;
int spawned = 0;

void blink(Timer &timer) {
  blink_on = !blink_on;
}

void spawn(Timer &timer) {
  spawned++;
  // timer.loop_count counts how many times it has fired
}

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

  // fire every 500 ms, forever (loops = -1)
  blink_timer.init(blink, 500, -1);
  blink_timer.start();

  // fire every second, five times, then stop
  spawn_timer.init(spawn, 1000, 5);
  spawn_timer.start();

  // 0 to 100 over 2 seconds, repeating
  slide.init(tween_ease_in_out_quad, 0.0f, 100.0f, 2000, -1);
  slide.start();

  // a sine wave between 20 and 60, one cycle per 1.5 seconds
  bounce.init(tween_sine, 20.0f, 60.0f, 1500, -1);
  bounce.start();
}

void update(uint32_t time) {
  // A pauses and resumes the slide
  if(buttons.pressed & Button::A) {
    if(slide.is_running())
      slide.pause();
    else
      slide.start();
  }

  // B restarts the spawn timer
  if(buttons.pressed & Button::B) {
    spawned = 0;
    spawn_timer.stop();
    spawn_timer.start();
  }
}

void render(uint32_t time) {
  screen.pen = Pen(20, 20, 30);
  screen.clear();

  screen.pen = Pen(255, 255, 255);
  screen.text("Tween (A: pause)", minimal_font, Point(4, 4));
  screen.pen = Pen(214, 40, 40);
  screen.circle(Point(14 + int(slide.value), 22), 6);

  screen.pen = Pen(70, 170, 60);
  screen.circle(Point(100, 30 + int(bounce.value)), 6);

  screen.pen = Pen(255, 255, 255);
  screen.text("Timers (B: restart)", minimal_font, Point(4, 40));
  if(blink_on) {
    screen.pen = Pen(250, 210, 60);
    screen.rectangle(Rect(4, 52, 10, 10));
  }

  screen.pen = Pen(70, 120, 220);
  for(int i = 0; i < spawned; i++)
    screen.rectangle(Rect(4 + i * 14, 70, 10, 10));

  screen.pen = Pen(200, 200, 200);
  screen.text(spawn_timer.is_finished() ? "spawn timer finished" : "spawning...", minimal_font, Point(4, 86));
}

Particles

Particles

A water fountain made with ParticleGenerator, with wind on A.

Explained in: the tutorial

particles.cpp
// Particles: a fountain made with ParticleGenerator.
//
// A generator owns a list of particles. It creates new ones with your
// generate function, moves them by their velocity plus a constant force,
// and removes them when they are older than their lifetime. It aims to keep
// `count` particles alive, spread evenly over the lifetime.
#include "32blit.hpp"

using namespace blit;

Particle *new_drop() {
  // start at the nozzle with a random upward velocity (pixels per second)
  float spread = (float(blit::random() % 200) - 100.0f) / 100.0f;
  float speed = 90.0f + float(blit::random() % 40);
  return new Particle(Vec2(64, 110), Vec2(spread * 25.0f, -speed));
}

// up to 300 particles, each living 2 seconds
ParticleGenerator fountain(300, 2000, new_drop);

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

  // gravity, in pixels per second per second
  fountain.force = Vec2(0, 90);
}

void update(uint32_t time) {
  // pass the current time: the generator works out the time since its last update
  fountain.update(time);

  // A blows the spray sideways while held
  fountain.force.x = (buttons & Button::A) ? 60.0f : 0.0f;
}

void render(uint32_t time) {
  screen.pen = Pen(10, 15, 30);
  screen.clear();

  for(auto p : fountain.particles) {
    // p->age goes from 0 (new) to 1 (about to be removed)
    screen.pen = Pen(120, 180, 255, int(255 * (1.0f - p->age)));
    screen.pixel(Point(p->pos));
  }

  screen.pen = Pen(150, 150, 150);
  screen.rectangle(Rect(58, 110, 12, 18));

  screen.pen = Pen(255, 255, 255);
  screen.text(std::to_string(fountain.particles.size()) + " particles", minimal_font, Point(2, 2));
  screen.text("Hold A for wind", minimal_font, Point(2, 12));
}

Tile Map

Tile Map

A scrolling level built from text, with coins you can collect.

Explained in: the tutorial

tilemap.cpp
// Tile map: build a level from a spritesheet and scroll around it.
//
// A TileMap is a grid of sprite indices. Its width and height (in tiles)
// must be powers of two, and its spritesheet must be 128 pixels wide
// (16 sprites per row). Levels drawn in Tiled can be packed with
// assets.yml and loaded with TileMap::load_tmx() instead.
#include <cstring>

#include "32blit.hpp"
#include "assets.hpp"

using namespace blit;

// sprite indices in examples/shared/sprites.png
enum Tile : uint8_t {
  COIN = 4, GRASS = 8, DIRT = 9, BRICK = 10, WATER = 11, SKY = 12, FLOWER = 13, CLOUD = 15
};

constexpr int map_w = 32, map_h = 16; // in tiles, both powers of two

// The level as text, one character per tile. Easier to edit than numbers.
const char *level[map_h] = {
  "................................",
  "................................",
  "....c...........c.........c.....",
  "...........c....................",
  "................................",
  "..............$$$...............",
  ".............bbbbb.......$$.....",
  "........................bbbb....",
  ".....$$.........................",
  "....bbbb..............f.........",
  ".........f.....f....ggggggg.....",
  "gggggggggggg..ggggggddddddd.gggg",
  "ddddddddddddwwddddddddddddddwddd",
  "ddddddddddddwwddddddddddddddwddd",
  "dddddddddddddddddddddddddddddddd",
  "dddddddddddddddddddddddddddddddd",
};

uint8_t tiles[map_w * map_h];
TileMap *map = nullptr;
Surface *sheet = nullptr;

Vec2 camera(0, 0);

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

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

  // turn the text into tile indices
  for(int y = 0; y < map_h; y++) {
    for(int x = 0; x < map_w; x++) {
      uint8_t t = SKY;
      switch(level[y][x]) {
        case 'g': t = GRASS; break;
        case 'd': t = DIRT; break;
        case 'b': t = BRICK; break;
        case 'w': t = WATER; break;
        case 'f': t = FLOWER; break;
        case 'c': t = CLOUD; break;
        case '$': t = COIN; break;
      }
      tiles[x + y * map_w] = t;
    }
  }

  // no per-tile transforms (nullptr), map size in tiles, and the spritesheet
  map = new TileMap(tiles, nullptr, Size(map_w, map_h), sheet);

  // SKY tiles are skipped when drawing, so the background shows through
  map->empty_tile_id = SKY;

  // wrap around horizontally when the camera goes past the edge
  map->repeat_mode = TileMap::REPEAT;
}

void update(uint32_t time) {
  if(buttons & Button::DPAD_LEFT)  camera.x -= 1.0f;
  if(buttons & Button::DPAD_RIGHT) camera.x += 1.0f;
  if(buttons & Button::DPAD_UP)    camera.y -= 1.0f;
  if(buttons & Button::DPAD_DOWN)  camera.y += 1.0f;

  camera.y = std::max(-32.0f, std::min(camera.y, float(map_h * 8 - 128)));

  // A collects the coin under the centre of the screen, if there is one
  if(buttons.pressed & Button::A) {
    Point centre = Point(camera + Vec2(64, 64)) / 8; // pixels to tiles
    int32_t index = map->offset(centre);
    if(index >= 0 && tiles[index] == COIN)
      tiles[index] = SKY;
  }
}

void render(uint32_t time) {
  // sky gradient behind the map
  for(int y = 0; y < 128; y++) {
    screen.pen = Pen(90 + y / 2, 150 + y / 3, 230);
    screen.h_span(Point(0, y), 128);
  }

  // the map's transform turns screen positions into map positions (in pixels),
  // so translating by the camera position scrolls the map
  map->transform = Mat3::translation(camera);
  map->draw(&screen, Rect(0, 0, 128, 128));

  // crosshair in the centre
  screen.pen = Pen(255, 255, 255);
  screen.h_span(Point(60, 64), 9);
  screen.v_span(Point(64, 60), 9);

  Point centre = Point(camera + Vec2(64, 64)) / 8;
  screen.pen = Pen(0, 0, 0, 160);
  screen.rectangle(Rect(0, 0, 128, 10));
  screen.pen = Pen(255, 255, 255);
  screen.text("tile " + std::to_string(centre.x) + "," + std::to_string(centre.y) +
              " = " + std::to_string(map->tile_at(centre)) + "  A: take coin", minimal_font, Point(2, 1));
}

System Info

System Info

Battery, USB, the player's settings and firmware details from the system API.

Explained in: System API

system-info.cpp
// System info: battery, USB, the player's settings and firmware details.
//
// These calls come from the console's firmware (the loader). On desktop
// builds, and on older firmware, they report "not available" instead.
#include <cstdio>

#include "32blit.hpp"

using namespace blit;

std::string kb(uint32_t bytes) {
  return std::to_string(bytes / 1024) + " KB";
}

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

void update(uint32_t time) {
}

void render(uint32_t time) {
  screen.pen = Pen(20, 20, 30);
  screen.clear();

  std::string lines;

  // this game's own metadata.yml, and the SDK it was built with
  auto meta = get_metadata();
  lines += std::string(meta.title) + " " + meta.version + "\n";
  lines += std::string("SDK ") + get_version_string() + "\n\n";

  // battery: -1 means the device can't measure it
  int level = get_battery_level();
  if(level >= 0) {
    char buf[32];
    snprintf(buf, sizeof(buf), "Battery %d%% (%.2f V)", level, get_battery_voltage() / 1000.0);
    lines += std::string(buf) + "\n";
  } else {
    lines += "Battery: n/a\n";
  }
  lines += std::string("USB: ") + (is_usb_connected() ? "connected" : "not connected") + "\n";

  // brightness, volume and sleep are applied by the firmware; games can read them
  SystemSettings settings;
  if(get_system_settings(settings)) {
    lines += "Brightness " + std::to_string(settings.brightness) + "/15\n";
    lines += "Volume " + std::to_string(settings.volume) + "%\n";
    lines += "Sleep " + (settings.sleep_timeout ? std::to_string(settings.sleep_timeout / 60) + " min" : std::string("off")) + "\n";
  } else {
    lines += "Settings: n/a\n";
  }

  SystemInfo info;
  if(get_system_info(info)) {
    lines += std::string("FW ") + info.firmware_version + "\n";
    lines += "Games " + kb(info.app_flash_used) + "/" + kb(info.app_flash_total) + "\n";
    lines += "Drive free " + kb(info.storage_free) + "\n";
  } else {
    lines += "System info: n/a\n";
  }

  screen.pen = Pen(255, 255, 255);
  screen.text(lines, minimal_font, Point(4, 4));
}

Chili Catch

Chili Catch

A complete game: catch chilis, dodge bombs, beat your best score.

Explained in: the tutorial

chili-catch.cpp
// Chili Catch: a complete small game.
//
// Move with the d-pad and catch the falling chilis. Coins are worth more,
// hearts give a life back, and bombs take one away. The game speeds up as
// the score grows, and the best score is saved.
//
// It brings together the pieces from the tutorials: the game loop, input,
// sprites, sound, saves, tweens and simple particles.
#include <vector>

#include "32blit.hpp"
#include "assets.hpp"

using namespace blit;

// ---------------------------------------------------------------------------
// constants

constexpr int screen_w = 128, screen_h = 128;
constexpr int ground_y = 112;      // top of the grass
constexpr float player_speed = 1.2f; // pixels per update (update runs 100 times a second)
constexpr int start_lives = 3, max_lives = 5;

// sprite indices in examples/shared/sprites.png
enum Sprite : uint16_t {
  CHILI = 0, PLAYER_1 = 1, PLAYER_2 = 2, HEART = 3, COIN = 4, BOMB = 5, STAR = 6, BASKET = 7, GRASS = 8, DIRT = 9
};

// ---------------------------------------------------------------------------
// game state

enum class State { title, playing, game_over };

struct Item {
  Sprite sprite;
  Vec2 pos;
  float speed;
};

struct Spark {
  Vec2 pos, vel;
  uint32_t born;
};

struct SaveData {
  uint32_t version = 1;
  uint32_t best = 0;
};

State state = State::title;
SaveData save;

float player_x = 60;
bool facing_left = false;
bool moving = false;

std::vector<Item> items;
std::vector<Spark> sparks;
uint32_t score = 0;
int lives = start_lives;
uint32_t next_spawn = 0;
uint32_t hurt_until = 0;   // flash the player after a bomb
bool new_best = false;

Tween title_bob;

// ---------------------------------------------------------------------------
// sound

void setup_sound() {
  // channel 0: pickup blip
  channels[0].waveforms = Waveform::SQUARE;
  channels[0].attack_ms = 2;
  channels[0].decay_ms = 80;
  channels[0].sustain = 0;
  channels[0].volume = 0x6000;

  // channel 1: bomb explosion
  channels[1].waveforms = Waveform::NOISE;
  channels[1].attack_ms = 5;
  channels[1].decay_ms = 400;
  channels[1].sustain = 0;

  // channel 2: game over / extra life tune
  channels[2].waveforms = Waveform::TRIANGLE;
  channels[2].attack_ms = 10;
  channels[2].decay_ms = 300;
  channels[2].sustain = 0;
}

void play(int channel, uint16_t frequency) {
  channels[channel].frequency = frequency;
  channels[channel].trigger_attack();
}

// ---------------------------------------------------------------------------
// helpers

void save_best() {
  write_save(save);
}

void burst(Vec2 pos, int count) {
  for(int i = 0; i < count; i++) {
    float angle = (blit::random() % 628) / 100.0f;
    float speed = 0.3f + (blit::random() % 100) / 100.0f;
    sparks.push_back({pos, Vec2(cosf(angle) * speed, sinf(angle) * speed - 0.5f), now()});
  }
}

void start_game(uint32_t time) {
  state = State::playing;
  items.clear();
  sparks.clear();
  score = 0;
  lives = start_lives;
  player_x = 60;
  next_spawn = time + 500;
  new_best = false;
}

void end_game() {
  state = State::game_over;
  new_best = score > save.best;
  if(new_best) {
    save.best = score;
    save_best();
  }
  play(2, 196);
}

void spawn_item(uint32_t time) {
  // pick what falls: mostly chilis, some coins and bombs, the odd heart
  int roll = blit::random() % 100;
  Sprite sprite = CHILI;
  if(roll >= 97) sprite = HEART;
  else if(roll >= 75) sprite = BOMB;
  else if(roll >= 63) sprite = COIN;

  // everything falls faster as the score goes up
  float speed = 0.35f + std::min<uint32_t>(score, 200) / 250.0f + (blit::random() % 30) / 100.0f;

  items.push_back({sprite, Vec2(float(4 + blit::random() % 116), -8.0f), speed});

  // and arrives more often
  uint32_t gap = 900 - std::min<uint32_t>(score * 4, 600);
  next_spawn = time + gap / 2 + blit::random() % gap;
}

// ---------------------------------------------------------------------------
// init / update / render

void init() {
  set_screen_mode(ScreenMode::hires);
  screen.sprites = Surface::load(asset_sprites);

  if(!read_save(save) || save.version != 1)
    save = SaveData();
  on_quit = save_best;

  setup_sound();

  title_bob.init(tween_sine, -3.0f, 3.0f, 1200, -1);
  title_bob.start();
}

void update_playing(uint32_t time) {
  moving = false;
  if(buttons & Button::DPAD_LEFT) {
    player_x -= player_speed;
    facing_left = true;
    moving = true;
  }
  if(buttons & Button::DPAD_RIGHT) {
    player_x += player_speed;
    facing_left = false;
    moving = true;
  }
  player_x = std::max(0.0f, std::min(player_x, float(screen_w - 16)));

  if(time >= next_spawn)
    spawn_item(time);

  // the basket is the 16 pixels in front of the player's hands
  Rect basket(int(player_x), ground_y - 14, 16, 6);

  for(auto it = items.begin(); it != items.end();) {
    it->pos.y += it->speed;

    Rect item_rect(Point(it->pos), Size(8, 8));
    bool caught = item_rect.intersects(basket);
    bool missed = it->pos.y > ground_y;

    if(caught) {
      switch(it->sprite) {
        case CHILI: score += 1; play(0, 880); break;
        case COIN:  score += 5; play(0, 1320); break;
        case HEART:
          lives = std::min(lives + 1, max_lives);
          play(2, 660);
          break;
        case BOMB:
          lives--;
          hurt_until = time + 600;
          play(1, 200);
          break;
        default: break;
      }
      burst(it->pos + Vec2(4, 4), it->sprite == BOMB ? 20 : 8);
    } else if(missed && it->sprite == CHILI) {
      // a dropped chili costs nothing, but gives a little puff
      burst(Vec2(it->pos.x + 4, ground_y), 3);
    }

    if(caught || missed)
      it = items.erase(it);
    else
      ++it;
  }

  if(lives <= 0)
    end_game();
}

void update(uint32_t time) {
  switch(state) {
    case State::title:
      if(buttons.pressed & Button::A)
        start_game(time);
      break;

    case State::playing:
      update_playing(time);
      break;

    case State::game_over:
      if(buttons.pressed & Button::A)
        start_game(time);
      break;
  }

  // sparks fly outwards, fall, and fade after 600 ms
  for(auto &s : sparks) {
    s.pos += s.vel;
    s.vel.y += 0.03f;
  }
  sparks.erase(std::remove_if(sparks.begin(), sparks.end(),
    [time](const Spark &s) { return time - s.born > 600; }), sparks.end());
}

void draw_background() {
  for(int y = 0; y < ground_y; y++) {
    screen.pen = Pen(100 + y / 2, 160 + y / 4, 235);
    screen.h_span(Point(0, y), screen_w);
  }
  for(int x = 0; x < screen_w; x += 8) {
    screen.sprite(GRASS, Point(x, ground_y));
    screen.sprite(DIRT, Point(x, ground_y + 8));
  }
}

void draw_hud() {
  screen.pen = Pen(0, 0, 0, 120);
  screen.rectangle(Rect(0, 0, screen_w, 11));

  for(int i = 0; i < lives; i++)
    screen.sprite(HEART, Point(2 + i * 9, 2));

  screen.pen = Pen(255, 255, 255);
  screen.text(std::to_string(score), minimal_font, Point(screen_w - 3, 2), true, TextAlign::top_right);
}

void draw_player(uint32_t time) {
  // flash while hurt
  if(time < hurt_until && (time / 80) % 2)
    return;

  uint16_t frame = moving && (time / 150) % 2 ? PLAYER_2 : PLAYER_1;
  uint8_t flip = facing_left ? SpriteTransform::HORIZONTAL : SpriteTransform::NONE;
  int x = int(player_x);

  // the player and the basket they hold out in front
  screen.sprite(frame, Point(facing_left ? x + 8 : x, ground_y - 8), flip);
  screen.sprite(BASKET, Point(facing_left ? x : x + 8, ground_y - 13));
}

void render(uint32_t time) {
  draw_background();

  for(auto &item : items)
    screen.sprite(item.sprite, Point(item.pos));

  if(state != State::title)
    draw_player(time);

  for(auto &s : sparks) {
    int age = time - s.born;
    screen.pen = Pen(255, 220, 80, 255 - age * 255 / 600);
    screen.pixel(Point(s.pos));
  }

  if(state == State::playing) {
    draw_hud();
    return;
  }

  // title and game over panels
  screen.pen = Pen(0, 0, 0, 150);
  screen.rectangle(Rect(10, 24, 108, 64));
  screen.pen = Pen(255, 255, 255);

  int bob = int(title_bob.value);
  if(state == State::title) {
    screen.sprite(CHILI, Point(56, 30 + bob), Point(4, 4), 2.0f);
    screen.text("CHILI CATCH", fat_font, Point(64, 48), true, TextAlign::center_center);
    screen.text("Best: " + std::to_string(save.best), minimal_font, Point(64, 62), true, TextAlign::center_center);
  } else {
    screen.text("GAME OVER", fat_font, Point(64, 36 + bob), true, TextAlign::center_center);
    screen.text("Score: " + std::to_string(score), minimal_font, Point(64, 54), true, TextAlign::center_center);
    screen.text(new_best ? "New best!" : "Best: " + std::to_string(save.best), minimal_font, Point(64, 64), true, TextAlign::center_center);
  }

  if((time / 500) % 2)
    screen.text("Press A", minimal_font, Point(64, 78), true, TextAlign::center_center);
}