Skip to content

Timers and tasks

Lesson 8.2

“Do this in three seconds.” “Do this every minute.” “Build this tower one block at a time.” All of these need code that waits. In a game you never wait by stopping everything (no sleep and no busy loop); you ask the game to call you again later.

The game counts time in ticks: 20 ticks are one second.

Once, later: clight::after

clight::after(60, [] {
    clight::broadcast("Don't forget to read the rules!");
});   // in 3 seconds

clight::after(ticks, fn) calls fn once, after ticks ticks, on the server’s tick. 0 means “on the next tick”. It returns an Expected<clight::Task>; you can ignore it (fire and forget), or keep the task to cancel it.

fn is a lambda, and it may capture whatever it needs: a player, a position, a count. The wrapper keeps the lambda until it has run, then frees it; task.cancel() frees it too.

Now a countdown is short:

void countdown(clight::Entity player, int seconds_left) {
    if (!player.is_valid()) return;   // the player left while we waited
    if (seconds_left > 0) {
        player.send_action_bar(std::to_string(seconds_left));
        clight::after(20, [player, seconds_left] { countdown(player, seconds_left - 1); });   // again in one second
    } else {
        player.send_action_bar("Go!");
    }
}

// countdown(player, 5);

Two habits from this example:

  • Check that things still exist after waiting. The player may have left; is_valid() tells you.
  • Capture by value. The lambda runs a second later, long after countdown returned. player and seconds_left are copied into it, so they are still there. Capturing a local variable by reference ([&]) would point at something that is gone by then.

A function that schedules itself again, like countdown, is a neat way to run a sequence with waits in between.

Again and again: clight::every

class ReminderMod final : public clight::Mod {
public:
    void on_server_starting() override {
        // every minute, starting in one minute:
        if (auto task = clight::every(1200, [] { clight::broadcast("Remember to drink water."); })) {
            reminder_ = *task;
        }
    }

    void stop_reminder() { reminder_.cancel(); }

private:
    clight::Task reminder_;
};

clight::every(period, fn) calls fn every period ticks, the first time after one period. A third argument sets a different first delay: clight::every(1200, fn, 0) starts on the next tick. The period must be at least 1. Keep the clight::Task to stop it later with cancel().

A repeating lambda can also stop itself. Give it a clight::Task parameter and it gets its own task:

int left = 10;
clight::every(20, [left](clight::Task self) mutable {
    clight::broadcast(std::to_string(left));
    if (--left == 0) self.cancel();   // after ten calls
});

mutable lets the lambda change its own copy of left. after takes a clight::Task parameter too, if you need it.

Function Runs
clight::after(ticks, fn) Once, after ticks ticks, on the server.
clight::every(period, fn) Every period ticks, on the server (the first time after one period; a third argument sets the first delay).
clight::after_client(ticks, fn) Once, after ticks ticks, in the game client.
task.cancel() Stops a waiting or repeating task and frees its lambda.
task.is_pending() true while the task is still waiting.

The server functions only work while a world runs: in singleplayer, or on a server. That is why the reminder starts in on_server_starting. In singleplayer the world stops while the pause menu is open, and your timers wait too, just like the game’s own.

The C functions underneath are clight::sched::run_server, repeat_server and run_client. They take a plain function and a void* instead of a lambda; you only need them when you port C code.

Big jobs without lag

Doing a slice of the work per tick keeps the game smooth. A tower that grows one block every half second:

void build_tower(clight::Level level, cl_block_pos base, int built, int height) {
    if (built >= height) return;
    level.set_block({base.x, base.y + built + 1, base.z}, "minecraft:stone_bricks");
    clight::after(10, [=] { build_tower(level, base, built + 1, height); });
}

// build_tower(player.level(), block_under_player, 0, 20);

[=] captures every variable the lambda uses by value. This is also the answer to “my mod freezes the game”: a callback that places 100,000 blocks at once makes everyone wait. 1,000 per tick for 100 ticks doesn’t.

Which one to use

  • A sequence with waits in between (a countdown, an animation, a slow build): clight::after with a function that schedules itself again.
  • Something that repeats (a reminder, an autosave): clight::every.
  • Something every single tick: a handler for ev::TickServerPost. Keep it short, it runs 20 times a second.

Tasks cost nothing while they wait.

Try it

Write a /countdown command that counts down from 5 in the action bar of the player who typed it, one number per second, then shows “Go!”. Reuse countdown from above.

Show a solution
class CountdownMod final : public clight::Mod {
public:
    void on_construct() override {
        clight::on<clight::ev::ServerCommandRegister>([](cl_ev_command_register&) {
            clight::cmd::add("countdown", [](clight::CmdCtx ctx) -> int32_t {
                clight::Entity p = ctx.player();
                if (!p) {
                    ctx.send_failure("Players only.");
                    return 0;
                }
                countdown(p, 5);
                return 1;
            });
        }).detach();
    }
};

The command returns right away; the tasks keep running for the next five seconds. The player handle is copied into each lambda, so it is still there after the command’s context is gone.

Recap: clight::after(ticks, lambda) runs code once after a number of ticks, clight::every(period, lambda) again and again, and task.cancel() stops it. The lambdas may capture what they need; the wrapper frees them when they are done or cancelled.