Create: Computing AgesDoc Brass
Piloter le monde

Temps et minuteries

Les ticks, sleep et les minuteries, les horloges de l'ordinateur et l'heure du monde : attendre, mesurer, répéter, et agir à la tombée de la nuit.

Le temps est partout dans une machine : une lampe qui clignote, un écran rafraîchi chaque seconde, une chaîne de presses qui tourne 30 secondes, des réverbères qui s'allument à la tombée de la nuit. Brass a quelques outils pour cela : sleep pour attendre, les minuteries pour attendre tout en faisant autre chose, os.clock et os.time pour mesurer, et os.day_time pour lire l'heure du monde Minecraft.

Cette page explique comment ils comptent, lequel choisir, et ce que devient le temps quand l'ordinateur cesse de tourner ou que son chunk se décharge.

Les ticks

Minecraft avance par ticks : 20 par seconde quand le serveur suit, soit 0,05 seconde par tick. Un ordinateur s'exécute une fois par tick, et tous les outils de cette page comptent en ticks : sleep, les minuteries, os.time et os.clock.

Les durées se donnent en secondes (sleep(0.5), os.start_timer(2)) et se convertissent en ticks en arrondissant vers le haut. Un vingtième de seconde est le pas le plus fin : rien en Brass n'attend moins d'un tick.

Quand le serveur rame et fait moins de 20 ticks par seconde, les programmes ralentissent avec le reste du monde : un sleep(1) attend toujours 20 ticks, qui durent alors plus d'une vraie seconde. Les machines de Create ralentissent de la même façon, si bien qu'un programme reste au pas de la machine qu'il pilote.

sleep

sleep(secondes) met le programme en pause. C'est la façon la plus simple d'attendre, et la bonne pour les programmes qui font une chose après l'autre : une lampe qui clignote, une suite de pistons, le cycle d'un feu tricolore.

Brass
for _, voulu in ipairs({0, 0.01, 0.06, 0.5, 1}) do
  local debut = os.time()
  sleep(voulu)
  print("sleep(" .. voulu .. ") : " .. os.time() - debut .. " tick(s)")
end
Écran
sleep(0) : 1 tick(s)
sleep(0.01) : 1 tick(s)
sleep(0.06) : 2 tick(s)
sleep(0.5) : 10 tick(s)
sleep(1) : 20 tick(s)

sleep(0) et sleep(0.01) attendent tous deux un tick : c'est la pause la plus courte, pratique pour laisser le monde appliquer une sortie avant la suivante (voir Redstone et liaisons de Create). sleep(0.06) attend deux ticks, jamais moins que demandé.

Pendant qu'il dort, le programme n'exécute aucune instruction et ne traite aucun évènement : ceux qui arrivent attendent dans la file jusqu'à ce que le programme les lise (Évènements).

Les horloges de l'ordinateur : os.time et os.clock

os.time() est le nombre de ticks depuis le démarrage de l'ordinateur. os.clock() donne la même chose en secondes (les ticks divisés par 20) : il avance donc par pas de 0,05.

Elles servent à mesurer des durées : lisez l'horloge deux fois et faites la différence.

Brass
local debut = os.clock()
sleep(1.5)
print("attendu " .. os.clock() - debut .. " s")
Écran
attendu 1.5 s

Trois choses à savoir sur ces horloges :

  • Elles repartent de 0 à chaque démarrage : quand l'ordinateur est allumé, redémarré, ou chargé de nouveau avec son chunk. Leur valeur seule ne veut rien dire, seule une différence compte. Deux ordinateurs affichent des valeurs différentes au même instant.
  • Elles comptent les ticks où l'ordinateur a vraiment tourné. Quand il s'arrête de tourner, ses horloges s'arrêtent aussi (voir Temps et minuteries).
  • Elles n'ont rien à voir avec l'heure du monde : pour celle-ci, utilisez os.day_time.

Mesurer une durée

Un Observateur intelligent surveille le tapis à la sortie d'une Presse mécanique : il s'allume chaque fois qu'un objet pressé passe, et son signal arrive sur la face gauche de l'ordinateur. Ce programme affiche le temps entre deux objets, la vraie cadence de la chaîne :

Brass
local precedent = nil
while true do
  os.pull_event("redstone")
  if rs.get("left") > 0 then   -- le début d'une impulsion
    local maintenant = os.clock()
    if precedent ~= nil then
      print(string.format("%.2f s depuis l'objet précédent", maintenant - precedent))
    end
    precedent = maintenant
  end
end

La précision est d'un tick (0,05 s). Pour chronométrer quelque chose de plus court, répétez-le et divisez : mesurez 20 objets, pas un seul.

L'heure du monde : os.day_time

os.day_time() est l'heure du monde, de 0 à 23999. Une journée Minecraft dure 24000 ticks, soit 20 minutes, et 0 n'est pas minuit mais le lever du soleil :

os.day_time()heuredans le monde
006:00lever du soleil
100007:00plein jour
600012:00midi
1200018:00début du coucher du soleil
1300019:00nuit : les monstres peuvent apparaître dehors
1800000:00minuit
2300005:00aube

Une heure du monde vaut 1000 ticks (50 vraies secondes) : l'heure est donc t // 1000 en partant de 6 heures. Pour afficher l'heure comme une horloge :

Brass
local function texte_heure(t)
  local heures = (t // 1000 + 6) % 24
  local minutes = (t % 1000) * 60 // 1000
  return string.format("%02d:%02d", heures, minutes)
end
print(texte_heure(0))
print(texte_heure(6000))
print(texte_heure(13000))
print(texte_heure(18000))
print(texte_heure(23500))
Écran
06:00
12:00
19:00
00:00
05:30

os.day_time ne donne que l'heure du jour en cours, pas la date : le nombre de jours écoulés depuis la création du monde n'est pas disponible. Tenez votre propre compteur (dans un fichier, voir plus bas) si vous en avez besoin.

Attention

L'heure du monde peut sauter : quand les joueurs dorment pour passer la nuit, ou après une commande /time set. Avec la règle de jeu doDaylightCycle désactivée, elle ne bouge plus du tout. N'attendez jamais une valeur exacte (os.day_time() == 13000 n'est presque jamais vrai, puisque le programme ne lit l'heure que de temps en temps) : testez un intervalle, ou repérez qu'un moment est passé depuis la dernière lecture, comme le fait la routine quotidienne plus bas.

Minuterie ou sleep ?

os.start_timer(secondes) rend la main tout de suite avec un identifiant, et un évènement timer portant cet id rejoint la file quand le temps est écoulé. Son délai s'arrondit au tick supérieur comme sleep, avec un tick au minimum.

sleep(secondes)os.start_timer(secondes)
le programmes'arrête jusqu'au boutcontinue tout de suite
les évènements pendant ce tempsattendent dans la filesont traités au fil de l'eau
la fin de l'attentesleep rend la mainun évènement timer avec id
idéal pourles séquences : clignoter, impulsions, cyclesles programmes qui réagissent : boutons, écrans, messages

Un programme peut avoir 256 minuteries en attente au plus ; une de plus l'arrête avec too many timers. Une minuterie ne s'annule pas : oubliez son identifiant et ignorez son évènement. La section Évènements montre les usages courants : une minuterie périodique dans la boucle principale, et un délai d'attente.

Planifier

Toutes les 5 minutes. Dans une boucle principale, une minuterie relancée chaque fois qu'elle sonne :

Brass
local SAUVER_TOUTES = 5 * 60
local minuterie = os.start_timer(SAUVER_TOUTES)
while true do
  local e = os.pull_event()
  if e.name == "timer" and e.id == minuterie then
    -- écrire les compteurs dans un fichier, envoyer un rapport...
    minuterie = os.start_timer(SAUVER_TOUTES)
  end
end

Quand le travail lui-même prend du temps, ce planning dérive un peu : la minuterie suivante démarre après le travail. Quand plusieurs tâches tournent à des rythmes différents, une échéance pour chacune les garde séparées et ne dérive jamais :

Brass
local prochain_rapport = os.clock() + 300   -- toutes les 5 minutes
local prochain_releve = os.clock() + 2      -- toutes les 2 secondes
while true do
  local maintenant = os.clock()
  if maintenant >= prochain_releve then
    -- lire le coffre renforcé, mettre l'écran à jour
    prochain_releve = prochain_releve + 2
  end
  if maintenant >= prochain_rapport then
    -- envoyer les chiffres à la salle de contrôle
    prochain_rapport = prochain_rapport + 300
  end
  sleep(0.5)
end

À la tombée de la nuit. Lisez l'heure toutes les quelques secondes et agissez quand le jour bascule en nuit, pas pendant toute la nuit :

Brass
local function fait_nuit()
  local t = os.day_time()
  return t >= 13000 and t < 23000
end

local nuit = nil   -- inconnu au départ : la première lecture agit toujours
while true do
  local nuit_maintenant = fait_nuit()
  if nuit_maintenant ~= nuit then
    nuit = nuit_maintenant
    rs.set("top", nuit)   -- les réverbères du village
  end
  sleep(5)
end

La recette Éclairage jour et nuit construit un éclairage complet sur cette idée.

Exemple : une horloge à l'écran

L'heure du monde en grands chiffres, rafraîchie chaque seconde, sur un ciel bleu le jour et sombre la nuit. L'écran ci-dessous est celui d'un ordinateur à midi.

Brass
local function texte_heure(t)
  local heures = (t // 1000 + 6) % 24
  local minutes = (t % 1000) * 60 // 1000
  return string.format("%02d:%02d", heures, minutes)
end

-- les lettres de gfx font 3 pixels de large plus 1 d'espace, fois l'échelle
local function centre(y, texte, couleur, echelle)
  local largeur = #texte * 4 * echelle - echelle
  gfx.text((gfx.size().w - largeur) // 2 + 1, y, texte, couleur, echelle)
end

while true do
  local t = os.day_time()
  local nuit = t >= 13000 and t < 23000
  gfx.clear(nuit and "blue" or "light_blue")
  centre(50, texte_heure(t), "white", 12)
  centre(130, nuit and "NUIT" or "JOUR", nuit and "yellow" or "white", 3)
  sleep(1)
end
Écran
Écran

Pour afficher aussi l'heure sur un Panneau d'affichage ou un Tube Nixie de Create, ajoutez display.set({"Heure", texte_heure(t)}) dans la boucle et pointez une Liaison d'affichage sur l'ordinateur (display).

Exemple : une routine quotidienne

Un village Create qui vit au rythme de l'horloge : le moulin démarre au lever du soleil et s'arrête au coucher, une cloche sonne à midi, les réverbères s'allument la nuit. Chaque ligne de la routine est une heure et une sortie. Le moulin tourne à travers un Embrayage sur la face gauche : un embrayage alimenté l'arrête, donc le moulin démarre quand la sortie s'éteint.

startup
local routine = {
  {heure = 0,     face = "left",  allume = false},   -- 06:00 embrayage relâché : le moulin démarre
  {heure = 6000,  face = "right", allume = true},    -- 12:00 la cloche sonne
  {heure = 6100,  face = "right", allume = false},   --       et s'arrête
  {heure = 12000, face = "left",  allume = true},    -- 18:00 embrayage alimenté : le moulin s'arrête
  {heure = 13000, face = "top",   allume = true},    -- 19:00 réverbères allumés
  {heure = 23000, face = "top",   allume = false},   -- 05:00 réverbères éteints
}

-- le moment est-il passé entre deux lectures ? (la journée repasse de 23999 à 0)
local function passe(moment, de, vers)
  if de <= vers then
    return moment > de and moment <= vers
  end
  return moment > de or moment <= vers
end

local derniere = os.day_time()
while true do
  sleep(1)
  local maintenant = os.day_time()
  for _, etape in ipairs(routine) do
    if passe(etape.heure, derniere, maintenant) then
      rs.set(etape.face, etape.allume)
    end
  end
  derniere = maintenant
end

Comme elle regarde ce qui est passé depuis la dernière lecture, la routine résiste aux sauts : quand les joueurs dorment toute la nuit, chaque étape entre le soir et le matin s'exécute à la lecture suivante, et les sorties se retrouvent comme elles doivent être le matin.

Un ordinateur démarre avec toutes ses sorties éteintes. Pour remettre les choses d'aplomb après un redémarrage en pleine journée, réglez chaque sortie une fois au début d'après l'heure actuelle, avant la boucle.

Exemple : un chronomètre

Un chronomètre pour un circuit : Entrée (ou une plaque de pression reliée à la face gauche) le lance et l'arrête, r le remet à zéro. Une minuterie redessine l'écran dix fois par seconde, pendant que les touches et la plaque sont traitées tout de suite.

Brass
local en_marche = false
local parti_a = 0   -- os.clock() au départ de la course en cours
local acquis = 0    -- secondes comptées par les courses précédentes

local function total()
  if en_marche then
    return acquis + os.clock() - parti_a
  end
  return acquis
end

local function dessiner()
  local dixiemes = math.floor(total() * 10)
  local texte = string.format("%02d:%04.1f", dixiemes // 600, (dixiemes % 600) / 10)
  gfx.clear("black")
  gfx.text(59, 50, texte, en_marche and "lime" or "white", 7)
  gfx.text(88, 130, "ENTRÉE : MARCHE/ARRÊT    R : ZÉRO", "gray", 1)
end

local function marche_arret()
  if en_marche then
    acquis = total()
    en_marche = false
  else
    parti_a = os.clock()
    en_marche = true
  end
end

dessiner()
local redessin = os.start_timer(0.1)
while true do
  local e = os.pull_event()
  if e.name == "timer" and e.id == redessin then
    dessiner()
    redessin = os.start_timer(0.1)
  elseif e.name == "key" and e.key == "enter" then
    marche_arret()
  elseif e.name == "redstone" and rs.get("left") > 0 then
    marche_arret()
  elseif e.name == "char" and e.char == "r" and not en_marche then
    acquis = 0
    dessiner()
  end
end
Écran
Écran

Le temps vient d'os.clock, pas du nombre de rafraîchissements : si l'ordinateur ralentit, l'écran se met à jour moins souvent mais le temps reste juste.

Quand la rotation s'arrête

Un ordinateur fonctionne à la rotation. Sa vitesse ne change que le nombre d'instructions qu'il exécute par tick, pas l'écoulement du temps pour lui :

  • Une rotation plus lente ralentit le programme, pas les horloges. À 32 tr/min, un Micro-ordinateur exécute un huitième de ses 24 000 instructions par seconde, mais sleep(1) attend toujours 20 ticks et une minuterie sonne toujours à l'heure. Ce qui s'allonge, c'est le code entre deux attentes. Voir Vitesse, mémoire et limites.
  • Plus de rotation du tout (ou un réseau en surcharge) fige l'ordinateur : le programme s'arrête là où il est, os.clock et os.time s'arrêtent, les pauses et les minuteries cessent de compter, les sorties redstone gardent leur valeur. Les évènements continuent d'entrer dans la file (256 au plus). Quand la rotation revient, tout reprend où c'en était.

Au-delà de 256 tr/min, l'ordinateur n'est pas plus rapide : 256 tr/min, c'est la pleine vitesse.

Quand le chunk se décharge

Quand plus personne n'est à proximité et que son chunk se décharge (ou que le serveur s'arrête), l'ordinateur s'éteint : le programme se termine, la mémoire est perdue, les sorties redstone et les signaux des Liaisons de redstone sont relâchés. Quand le chunk se recharge, l'ordinateur démarre : ses horloges repartent de 0, et il exécute son fichier startup s'il en a un.

Un programme qui doit continuer d'une visite à l'autre doit donc :

  • vivre dans le fichier nommé startup, pour repartir tout seul ;
  • enregistrer ce dont il doit se souvenir (compteurs, état d'une machine, dernier jour vu) dans un fichier avec fs.write(), et le relire au début avec fs.read() ;
  • régler ses sorties au début, puisqu'elles sont toutes éteintes après un démarrage.
startup
-- un compteur de lingots pressés qui survit aux redémarrages et aux déchargements
local compte = tonumber(fs.read("compte.txt") or "0")
while true do
  os.pull_event("redstone")
  if rs.get("left") > 0 then
    compte = compte + 1
    fs.write("compte.txt", tostring(compte))
  end
end

Les minuteries ne survivent pas non plus : une minuterie de 10 minutes lancée juste avant le déchargement ne sonne jamais. Pour les longues échéances, écrivez l'échéance dans un fichier, ou servez-vous d'os.day_time, qui appartient au monde et continue d'avancer même quand l'ordinateur est absent.