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
countdownreturned.playerandseconds_leftare 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::afterwith 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.