Skip to content

7. Timers, tweens and particles

Games are full of things that happen over time: a power-up that runs out, a menu that slides in, sparks that fly and fade. The SDK has three helpers for these.

Timers

Timers and tweens

A Timer calls a function after a delay, once, a set number of times, or forever:

Timer blink_timer;

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

void init() {
  blink_timer.init(blink, 500, -1);   // every 500 ms, forever
  blink_timer.start();
}

The arguments to init() are the callback, the interval in milliseconds, and how many times to fire (-1 for forever). The engine checks running timers before each frame and calls the callback when one is due; you don't need to update them yourself.

Call Effect
start() start, or resume after pause()
pause() stop counting; start() carries on from where it was
stop() stop; start() begins again from zero
is_running(), is_paused(), is_stopped(), is_finished() check the state

timer.loop_count counts how many times it has fired. A timer with a set number of loops ends up is_finished().

Timers must outlive their use

The engine keeps a pointer to each started timer, so make timers global variables or members of objects that live as long as they run. A timer destroyed while running is removed safely, but a local Timer in a function stops as soon as the function returns.

Tweens

A Tween moves a number from one value to another over a duration, following a curve. Read tween.value whenever you need it:

Tween slide;

void init() {
  slide.init(tween_ease_in_out_quad, 0.0f, 100.0f, 2000, -1);
  slide.start();
}

void render(uint32_t time) {
  screen.circle(Point(14 + int(slide.value), 22), 6);
}

The arguments are the curve, the start and end values, the duration in milliseconds, and the number of loops (-1 repeats forever, 1 runs once). Tweens have the same start(), pause(), stop() and is_...() calls as timers.

Curve Shape
tween_linear constant speed
tween_ease_in_quad starts fast and slows down to the end value
tween_ease_out_quad starts slowly and speeds up
tween_ease_in_out_quad slow at both ends, fast in the middle
tween_sine starts at the end value, swings to the start value and back once per loop: good for bobbing and pulsing

(The two one-sided quad curves are named the opposite way round from the usual easing names. The table describes what they actually do.)

You can write your own curve: any function float f(uint32_t t, float from, float to, uint32_t duration) that returns from at t = 0 and to at t = duration.

Here is the example that shows timers and tweens together:

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 ParticleGenerator manages a stream of particles: it creates them with your function, moves them, applies a constant force such as gravity, and removes them when they're too old.

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));
}
  • The constructor takes how many particles to keep alive, how long each lives in milliseconds, and a function that returns a new Particle(position, velocity).
  • force is added to every particle's velocity each second; set y for gravity.
  • Call update(time) with the current time every update. It works out how much time has passed since the last call itself.
  • Each particle has pos, vel, and age (0 when new, 1 when about to be removed), which is handy for fading.

The generator spreads new particles evenly over the lifetime, so you get a steady stream, which suits fountains, smoke and engine trails. For a one-off burst, such as sparks when something explodes, a small std::vector of your own structs is simpler: see how Chili Catch does it.

One generator at a time

ParticleGenerator::update remembers the previous time in one variable shared by every generator. With two generators updated in the same step, the second sees no time pass and its particles freeze. Use a single generator, or your own particle list for the others. See Particles.

Try it

  • Use a one-shot tween (loops 1) to slide a "LEVEL 2" banner in from the left.
  • Make the particle fountain change colour with hsv_to_rgba() as particles age.
  • Use a timer with 3 loops for a "3, 2, 1, GO" countdown.

Next: tile maps.