Skip to content

Timers and tasks

Lesson 13.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; you ask the game to call you again later.

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

Waiting inside a coroutine: cl.spawn and cl.sleep_ticks

The friendliest way to wait is a coroutine, which you met in Coroutines. cl.spawn(fn) starts fn as a coroutine, and inside it cl.sleep_ticks(n) pauses for n ticks while the game goes on:

cl.on("player.join", function(event)
  local player = event.player
  cl.spawn(function()
    cl.sleep_ticks(60)                 -- 3 seconds
    if player:is_valid() then
      player:send_message("Don't forget to read the rules!")
    end
  end)
end)

The code reads from top to bottom, as if it really waited. Underneath, the coroutine yields, and the game resumes it 60 ticks later.

Two things to keep in mind:

  • Copy what you need from the event first. The event is gone once the handler returns, and the coroutine runs on after that. Here local player = event.player copies the player before the wait.
  • Check that things still exist after waiting. The player may have left in those 3 seconds; is_valid() tells you.

cl.sleep_ticks only works inside cl.spawn. Called anywhere else, it raises cl.sleep_ticks must be called inside cl.spawn.

Step-by-step work

Waiting in a loop spreads work over time. A tower that grows one block every half second:

local function grow_tower(world, base, height)
  cl.spawn(function()
    for i = 1, height do
      world:set_block(base + cl.pos(0, i, 0), "minecraft:stone_bricks")
      cl.sleep_ticks(10)
    end
  end)
end

This is also the way to do big jobs without a lag spike: do a slice of the work, sleep one tick, do the next slice.

The scheduler: cl.sched

cl.sched runs a plain function later. It returns a task you can cancel.

-- once, in 5 seconds
local task = cl.sched.run_server(100, function()
  cl.entity.server_broadcast("Five seconds have passed.")
end)

-- every minute, starting now
local reminder = cl.sched.repeat_server(0, 1200, function()
  cl.entity.server_broadcast("Remember to drink water.")
end)

-- stop it again
reminder:cancel()
Function Runs
cl.sched.run_server(delay, fn) Once, after delay ticks, on the server.
cl.sched.repeat_server(delay, period, fn) First after delay ticks, then every period ticks, on the server.
cl.sched.run_client(delay, fn) Once, after delay ticks, in the game client.

task:cancel() stops a task, and task:is_pending() tells you whether it is still waiting.

The server functions only work while a world runs: in singleplayer, or on a server. A delay of 0 means “on the next tick”.

Which one to use

  • A sequence with waits in between (a countdown, an animation, a slow build): cl.spawn and cl.sleep_ticks.
  • Something that repeats forever (a reminder, an autosave): cl.sched.repeat_server.
  • Something every single tick: an event handler for tick.server_post. Keep it short, it runs 20 times a second.

Both cl.spawn and the scheduler cost nothing while nothing is waiting.

Pausing

In singleplayer the world stops while the pause menu is open, and so do server ticks. Your timers wait too, just like the game’s own.

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!”.

Show a solution
cl.on("server.command_register", function()
  cl.cmd.register(cl.cmd.literal("countdown"):executes(function(ctx)
    local player = ctx:player()
    if not player then
      ctx:failure("Players only.")
      return 0
    end
    cl.spawn(function()
      for n = 5, 1, -1 do
        if not player:is_valid() then return end
        player:send_action_bar(tostring(n))
        cl.sleep_ticks(20)
      end
      if player:is_valid() then player:send_action_bar("Go!") end
    end)
  end))
end)

The command returns right away; the coroutine keeps running for the next five seconds. player was copied out of ctx before the first wait, for the same reason as with events.

Recap: cl.spawn with cl.sleep_ticks waits inside a coroutine, cl.sched.run_server and repeat_server call a function later or repeatedly, and 20 ticks are one second.