Create: Computing AgesDoc Brass
Piloter le monde

Évènements

Comment un programme attend le monde : la file d'évènements, os.pull_event, les minuteries, et la boucle principale qui réagit à la fois aux boutons, aux touches et aux messages.

Un programme qui pilote une machine passe l'essentiel de son temps à attendre : un bouton, une touche, un message d'un autre ordinateur, la fin d'un délai. En Brass, tout cela arrive de la même façon, sous forme d'évènements.

Un évènement est une petite table qui décrit ce qui vient de se passer. Il a toujours un nom, name, et certains portent d'autres champs :

Brass
local levier = {name = "redstone"}
local touche = {name = "key", key = "enter"}
local ordre = {name = "message", sender = 12, channel = "door", data = "open", via = "cable", distance = 8.5}

Cette page explique d'où viennent les évènements, comment les attendre sans en perdre, et comment écrire la boucle principale à laquelle aboutit tout programme un peu sérieux.

La file d'évènements

Chaque ordinateur tient une file d'évènements. Quand quelque chose arrive (une entrée redstone change, une touche est pressée, un message arrive, une minuterie sonne), l'ordinateur ajoute un évènement au bout de la file. os.pull_event retire de la file l'évènement le plus ancien et le renvoie. Quand la file est vide, le programme attend dans os.pull_event qu'un évènement arrive. Cette attente ne coûte rien : l'ordinateur n'exécute aucune instruction pendant ce temps.

Brass
os.queue_event("porte", "ouverte")
os.queue_event("lampe", 15)
local premier = os.pull_event()
local second = os.pull_event()
print(premier.name, premier.data)
print(second.name, second.data)
Écran
porte   ouverte
lampe   15

os.queue_event ajoute à la file un évènement de votre cru (son second argument devient e.data). Les évènements sortent dans l'ordre où ils sont entrés.

Quelques règles sur la file :

  • Elle contient 256 évènements au plus. Quand elle est pleine, le plus ancien est supprimé pour faire de la place au nouveau. Un programme qui cesse longtemps de lire ses évènements perd d'abord les plus vieux.
  • Les évènements continuent d'arriver pendant que le programme calcule ou dort dans sleep : ils attendent dans la file (voir sleep et les évènements).
  • La file est vidée quand l'ordinateur redémarre ou s'éteint. Un ordinateur éteint ne reçoit rien.
  • À l'invite du shell, quand aucun programme ne tourne, les évènements autres que la frappe sont jetés.

Attendre un seul type d'évènement

os.pull_event accepte un nom facultatif. Avec lui, l'appel ne renvoie qu'un évènement de ce nom :

Brass
os.pull_event("redstone")   -- attend qu'une entrée redstone change
print("on a appuyé sur le bouton (ou on l'a relâché)")

C'est la façon la plus simple d'attendre une chose précise. Mais ce filtre a un prix : tout évènement qui ne correspond pas est jeté, pas mis de côté.

Brass
os.queue_event("click")
os.queue_event("redstone")
os.queue_event("message")
local e = os.pull_event("redstone")
print("reçu " .. e.name)
os.queue_event("controle")
print("puis " .. os.pull_event().name)
Écran
reçu redstone
puis message

Le click placé avant l'évènement redstone a disparu. Le message, arrivé après, est toujours dans la file. Un programme qui attend en boucle avec os.pull_event("redstone") perd donc tous les clics sur son écran et tous les messages qui arrivent entre-temps.

Attention

net.receive est un os.pull_event("message") déguisé : lui aussi jette tous les autres évènements. Un programme qui doit réagir aux messages et à autre chose a besoin de la boucle principale ci-dessous.

Le filtre est le bon outil quand le programme fait vraiment une seule chose à la fois : une porte qui attend son bouton, s'ouvre, puis attend de nouveau. Dès que deux choses peuvent arriver, passez-vous du filtre.

Les évènements du jeu

nomquandchamps
redstoneune entrée redstone change sur l'une des six facesaucun : lisez les faces avec rs.get
timerune minuterie lancée par os.start_timer arrive à son termeid
keyune touche spéciale est pressée dans le terminalkey : "enter", "backspace", "delete", "up", "down", "left", "right", "home", "end", "tab"
charun caractère est tapé dans le terminal (lettres, chiffres, espace...)char : le caractère
pasteCtrl+V dans le terminaltext : le texte collé (4096 caractères au plus)
clickun clic sur l'écran du terminal (pas sur le papier du Calculateur à tubes), ou un clic droit sur un moniteurx, y (caractère), px, py (pixel gfx), button, source
dragla souris bouge avec un bouton enfoncé, dans le terminalles mêmes que click
messageun message d'un autre ordinateur (net)sender, channel, data, via, distance
linkla puissance reçue sur une fréquence de Liaison de redstone utilisée par le programme change (link)a, b, power
diskun support de stockage est inséré ou éjectéinserted : true ou false
n'importe quel nomos.queue_event(nom, donnée)data

La page Évènements détaille chaque champ. Quelques points à retenir dès maintenant :

  • L'évènement redstone dit que quelque chose a changé, pas quoi. Lisez les faces qui vous intéressent avec rs.get quand il arrive (voir Redstone et liaisons de Create).
  • Les clics se comptent à partir de 1, 1 en haut à gauche. button vaut 1 pour le bouton gauche, 2 pour le droit, 3 pour celui du milieu ; un appui sur un moniteur a toujours button égal à 1. source vaut "terminal" ou "monitor".
  • Un clic droit sur un moniteur n'atteint le programme qu'une fois que celui-ci a attendu des clics : avec os.pull_event() (sans filtre) ou os.pull_event("click"). Sinon le clic droit ouvre le terminal, comme d'habitude. Accroupi, on ouvre toujours le terminal.
  • Ctrl+T (arrêt) et Ctrl+R (redémarrage) ne sont pas des évènements : un programme ne peut pas les intercepter.

La boucle principale

Presque tous les programmes qui pilotent quelque chose ont le même cœur : un seul os.pull_event sans filtre, dans une boucle sans fin, suivi d'un test sur e.name :

Brass
while true do
  local e = os.pull_event()
  if e.name == "redstone" then
    -- un bouton, un levier, un détecteur
  elseif e.name == "click" then
    -- un appui sur l'écran
  elseif e.name == "message" then
    -- un autre ordinateur nous parle
  elseif e.name == "timer" then
    -- l'heure du travail périodique
  end
end

Rien ne se perd, puisque chaque évènement sort de la file et passe par les tests. Ceux qui n'intéressent pas le programme ne déclenchent simplement aucune branche.

Gardez chaque branche courte : pendant qu'une branche s'exécute, les autres évènements attendent. Rangez l'état du programme (la porte est-elle ouverte, quelle ligne est sélectionnée) dans des variables en haut du fichier, pour que chaque branche puisse le lire et le modifier.

Quand les types d'évènements se multiplient, une table de gestionnaires se lit mieux qu'un long if. Chaque gestionnaire est une fonction rangée sous le nom de l'évènement :

Brass
local gestionnaires = {}
local caisses = 0

function gestionnaires.caisse(e)
  caisses = caisses + e.data
end

function gestionnaires.bilan(e)
  print("caisses jusqu'ici : " .. caisses)
end

os.queue_event("caisse", 3)
os.queue_event("bruit")
os.queue_event("caisse", 4)
os.queue_event("bilan")
for i = 1, 4 do
  local e = os.pull_event()
  local gestionnaire = gestionnaires[e.name]
  if gestionnaire then
    gestionnaire(e)
  end
end
Écran
caisses jusqu'ici : 7

L'évènement bruit n'a pas de gestionnaire : il est ignoré, sans erreur. (Un vrai programme utilise while true do à la place de la boucle for, qui sert seulement à arrêter cet exemple après quatre évènements.)

Les minuteries : du travail périodique sans rater d'évènement

sleep(5) arrête le programme pendant 5 secondes : aucun évènement n'est traité pendant ce temps. Pour faire quelque chose toutes les quelques secondes et rester réactif, lancez plutôt une minuterie. os.start_timer(secondes) rend la main tout de suite avec un nombre, l'identifiant de la minuterie ; quand le temps est écoulé, un évènement timer portant cet id rejoint la file.

Une minuterie ne sonne qu'une fois. Pour un travail périodique, relancez-en une chaque fois que la précédente sonne :

Brass
local rafraichir = os.start_timer(2)
while true do
  local e = os.pull_event()
  if e.name == "timer" and e.id == rafraichir then
    -- toutes les 2 secondes : lire les capteurs, redessiner l'écran...
    rafraichir = os.start_timer(2)
  elseif e.name == "click" then
    -- les clics sont traités tout de suite, même entre deux rafraîchissements
  end
end

Comparez toujours e.id : dès qu'un programme utilise deux minuteries, un évènement timer seul ne dit pas laquelle a sonné. Une minuterie ne s'annule pas : quand vous n'en voulez plus, oubliez son identifiant, et son évènement ne déclenchera aucune branche.

Comme os.pull_event n'a pas de délai maximum, une minuterie sert aussi de délai d'attente : attendre quelque chose, mais pas éternellement.

Brass
local function attendre(nom, secondes)
  local limite = os.start_timer(secondes)
  while true do
    local e = os.pull_event()
    if e.name == nom then
      return e
    elseif e.name == "timer" and e.id == limite then
      return nil
    end
  end
end

local e = attendre("redstone", 1)
if e == nil then
  print("personne n'a appuyé sur le bouton")
end
Écran
personne n'a appuyé sur le bouton

Comme un filtre, cette fonction jette les autres évènements qu'elle lit pendant son attente. Réservez-la aux attentes courtes, ou appelez depuis elle les gestionnaires de votre boucle principale.

Le guide Temps et minuteries revient en détail sur les minuteries, sleep et les horloges de l'ordinateur.

sleep et les évènements

sleep ne retire aucun évènement de la file. Tout ce qui arrive pendant que le programme dort attend dans la file, et le prochain os.pull_event le renvoie :

Brass
os.start_timer(0.5)
sleep(2)
local e = os.pull_event()
print(e.name .. " a sonné pendant la pause")
Écran
timer a sonné pendant la pause

Rien n'est perdu (tant que moins de 256 évènements s'accumulent), mais tout arrive en retard : un bouton pressé au début d'un sleep(10) n'est vu que 10 secondes plus tard. Dans un programme qui doit réagir, remplacez les longues pauses par des minuteries.

read() et les évènements

read() attend que le joueur tape une ligne dans le terminal. Il ne consomme que les évènements key, char et paste : les autres (redstone, minuteries, messages, clics) restent dans la file, dans leur ordre, et le programme les retrouve avec os.pull_event une fois que read() a rendu la main.

Brass
local limite = os.start_timer(30)
write("Mot de passe : ")
local mot_de_passe = read("*")   -- les étoiles cachent la saisie
-- un changement de redstone, un clic ou la minuterie pendant la saisie sont encore dans la file ici

Quand un programme attend dans os.pull_event, les touches tapées dans le terminal arrivent au contraire sous forme d'évènements key et char : rien ne s'affiche à l'écran, c'est le programme qui décide quoi faire de chaque touche.

La vitesse de réaction

Un ordinateur s'exécute une fois par tick (20 fois par seconde) tant qu'il tourne. Un évènement placé dans la file pendant un tick est traité au plus tard au tick suivant de l'ordinateur : un programme en attente réagit en moins d'un vingtième de seconde.

Quand beaucoup d'évènements arrivent ensemble, le programme peut revenir d'os.pull_event (ou de toute autre attente) 16 fois par tick au plus, le tout dans le budget d'instructions de ce tick. Une rafale de 40 évènements se traite en trois ticks : 16 au premier, 16 au deuxième, les 8 derniers au troisième.

Brass
for i = 1, 40 do
  os.queue_event("caisse", i)
end
local debut = os.time()
for i = 1, 40 do
  os.pull_event("caisse")
end
print("fini " .. os.time() - debut .. " ticks après le début")
Écran
fini 2 ticks après le début

Un ordinateur qui ne tourne pas (pas de rotation, ou un réseau en surcharge) est figé : ses évènements attendent dans la file, et le programme les traite dès que la rotation revient. Le guide Vitesse, mémoire et limites explique le budget.

Exemple : une porte avec un bouton, une minuterie et le réseau

Un contrôleur de porte sur un Mini-ordinateur (ou plus récent, pour le réseau). Un bouton de pierre est sur la face gauche, une porte en fer se dresse sur l'ordinateur. La porte s'ouvre quand on appuie sur le bouton, ou quand un autre ordinateur envoie "open" sur le canal "door", et se referme toute seule 5 secondes plus tard. Un message "close" la ferme immédiatement.

startup
local PORTE = "top"
local minuterie_fermeture = nil   -- identifiant de la minuterie en cours, nil quand la porte est fermée

local function ouvrir()
  rs.set(PORTE, true)
  minuterie_fermeture = os.start_timer(5)   -- une nouvelle minuterie : l'ancienne sera ignorée
end

local function fermer()
  rs.set(PORTE, false)
  minuterie_fermeture = nil
end

while true do
  local e = os.pull_event()
  if e.name == "redstone" then
    if rs.get("left") > 0 then   -- seulement l'appui, pas le relâchement
      ouvrir()
    end
  elseif e.name == "message" and e.channel == "door" then
    if e.data == "open" then
      ouvrir()
    elseif e.data == "close" then
      fermer()
    end
  elseif e.name == "timer" and e.id == minuterie_fermeture then
    fermer()
  end
end

Trois sources d'évènements, une seule boucle, rien de raté. Appuyer de nouveau sur le bouton pendant que la porte est ouverte lance une nouvelle minuterie : l'évènement de l'ancienne ne correspond plus à minuterie_fermeture, donc la porte reste ouverte 5 secondes après le dernier appui. Un autre ordinateur l'ouvre avec net.send(id, "open", "door").

Un menu pour une usine : les flèches déplacent la sélection, Entrée exécute la ligne choisie, les touches numériques choisissent une ligne directement, et un clic (dans le terminal ou sur un moniteur) marche aussi. Un Embrayage de Create est sur la face gauche (un embrayage alimenté arrête la chaîne), une lampe sur le dessus.

Brass
local lignes = {"Démarrer la chaîne de presses", "Arrêter la chaîne de presses", "Allumer/éteindre la lampe", "Quitter"}
local choisie = 1

local function dessiner()
  term.set_bg(term.colors.black)
  term.clear()
  term.set_cursor(3, 2)
  term.set_fg(term.colors.yellow)
  term.write("COMMANDE DE LA CHAÎNE DE PRESSES")
  for i, texte in ipairs(lignes) do
    term.set_cursor(3, 3 + i)
    if i == choisie then
      term.set_bg(term.colors.blue)
      term.set_fg(term.colors.white)
    else
      term.set_bg(term.colors.black)
      term.set_fg(term.colors.light_gray)
    end
    term.write(" " .. i .. ". " .. texte .. " ")
  end
  term.set_bg(term.colors.black)
  term.set_fg(term.colors.gray)
  term.set_cursor(3, 10)
  term.write("Flèches + Entrée, un chiffre, ou un clic")
end

local function executer(i)
  if i == 1 then
    rs.set("left", false)   -- embrayage relâché : la chaîne tourne
  elseif i == 2 then
    rs.set("left", true)    -- embrayage alimenté : la chaîne s'arrête
  elseif i == 3 then
    rs.set("top", rs.get_output("top") == 0)
  end
end

dessiner()
while true do
  local e = os.pull_event()
  local choix = nil
  if e.name == "key" then
    if e.key == "up" and choisie > 1 then
      choisie = choisie - 1
    elseif e.key == "down" and choisie < #lignes then
      choisie = choisie + 1
    elseif e.key == "enter" then
      choix = choisie
    end
  elseif e.name == "char" then
    local n = tonumber(e.char)
    if n ~= nil and n >= 1 and n <= #lignes then
      choix = n
    end
  elseif e.name == "click" and e.y >= 4 and e.y < 4 + #lignes then
    choix = e.y - 3
  end
  if choix ~= nil then
    choisie = choix
    if choix == #lignes then
      break
    end
    executer(choix)
  end
  dessiner()
end
term.clear()
term.set_cursor(1, 1)
Écran
Écran

La boucle lit chaque évènement sans filtre : le même menu marche au clavier, à la souris et sur un moniteur. Notez le test n ~= nil : tonumber renvoie nil pour une lettre. Le guide Écrans et moniteurs montre comment dessiner des boutons et des interfaces plus grandes, et buttons est un modèle prêt à l'emploi.