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: cl_sched_run_server
static void say_rules(void* user) {
(void)user;
cl_easy_broadcast("Don't forget to read the rules!");
}
// somewhere in server code:
cl_sched_run_server(60, say_rules, NULL, NULL); // in 3 seconds
cl_sched_run_server(delay, fn, user, out) calls fn(user) once, after delay ticks, on the server’s tick. A delay of 0 means “on the next tick”. out receives a task handle if you want to cancel it; pass NULL if you don’t.
Again and again: cl_sched_repeat_server
static cl_task g_reminder;
static void remind(void* user) {
(void)user;
cl_easy_broadcast("Remember to drink water.");
}
// every minute, starting in one minute:
cl_sched_repeat_server(1200, 1200, remind, NULL, &g_reminder);
// stop it again:
cl_task_cancel(g_reminder);
The first number is the delay before the first call, the second the period between calls. The period must be at least 1.
| Function | Runs |
|---|---|
cl_sched_run_server(delay, fn, user, &task) |
Once, after delay ticks, on the server. |
cl_sched_repeat_server(delay, period, fn, user, &task) |
First after delay ticks, then every period ticks, on the server. |
cl_sched_run_client(delay, fn, user, &task) |
Once, after delay ticks, in the game client. |
cl_task_cancel(task) |
Stops a waiting or repeating task. |
cl_task_is_pending(task) |
true while the task is still waiting. |
The server functions only work while a world runs: in singleplayer, or on a server. In singleplayer the world stops while the pause menu is open, and your timers wait too, just like the game’s own.
Taking things along: the user pointer
A task only gets a void* user. To take more than one value along, put them in a struct. The struct must still exist when the task runs, so it can’t be a local variable of the function that schedules it. Allocate it, and free it in the task:
typedef struct {
cl_entity player;
int seconds_left;
} countdown;
static void countdown_step(void* user) {
countdown* c = (countdown*)user;
if (!cl_entity_is_valid(c->player)) { // the player left
free(c);
return;
}
if (c->seconds_left > 0) {
char text[8];
snprintf(text, sizeof text, "%d", c->seconds_left);
cl_easy_send_action_bar(c->player, text);
c->seconds_left -= 1;
cl_sched_run_server(20, countdown_step, c, NULL); // again in one second
} else {
cl_easy_send_action_bar(c->player, "Go!");
free(c);
}
}
static void start_countdown(cl_entity player) {
countdown* c = malloc(sizeof *c);
if (!c) return;
c->player = player;
c->seconds_left = 5;
countdown_step(c);
}
Two habits from this example:
- Check that things still exist after waiting. The player may have left;
cl_entity_is_validtells you. - Every
mallocgets exactly onefree, on every path out of the task. W3Schools explains both in Allocate memory and Deallocate memory.
A task that schedules itself again, like countdown_step, is a neat way to run a sequence with waits in between.
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:
typedef struct {
cl_level level;
cl_block_pos base;
int built, height;
} tower;
static void tower_step(void* user) {
tower* t = (tower*)user;
t->built += 1;
cl_block_pos p = {t->base.x, t->base.y + t->built, t->base.z};
cl_easy_set_block(t->level, p, "minecraft:stone_bricks");
if (t->built < t->height) cl_sched_run_server(10, tower_step, t, NULL);
else free(t);
}
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): a task that reschedules itself.
- Something that repeats forever (a reminder, an autosave):
cl_sched_repeat_server. - Something every single tick: a handler for
CL_EV_TICK_SERVER_POST. 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 start_countdown from above.
Show a solution
static int32_t cmd_countdown(cl_cmd_ctx ctx, void* user) {
(void)user;
cl_entity p = cl_cmd_ctx_player(ctx);
if (CL_IS_NULL(p)) {
cl_text t = cl_text_literal(CL_STR("Players only."));
cl_cmd_send_failure(ctx, t);
cl_text_release(t);
return 0;
}
start_countdown(p);
return 1;
}
static void on_command_register(void* payload, void* user) {
(void)payload;
(void)user;
cl_cmd_desc d = CL_CMD_DESC_INIT;
d.name = CL_STR("countdown");
d.execute = cmd_countdown;
cl_cmd_register(&d);
}
The command returns right away; the task keeps running for the next five seconds. The player handle is copied into the struct, so it is still there after the command’s context is gone.
Recap: cl_sched_run_server calls a function once after a number of ticks, cl_sched_repeat_server calls it again and again, cl_task_cancel stops it, and values travel along in a malloced struct that the task frees.