É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 :
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.
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)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 :
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é.
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)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.
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
| nom | quand | champs |
|---|---|---|
redstone | une entrée redstone change sur l'une des six faces | aucun : lisez les faces avec rs.get |
timer | une minuterie lancée par os.start_timer arrive à son terme | id |
key | une touche spéciale est pressée dans le terminal | key : "enter", "backspace", "delete", "up", "down", "left", "right", "home", "end", "tab" |
char | un caractère est tapé dans le terminal (lettres, chiffres, espace...) | char : le caractère |
paste | Ctrl+V dans le terminal | text : le texte collé (4096 caractères au plus) |
click | un clic sur l'écran du terminal (pas sur le papier du Calculateur à tubes), ou un clic droit sur un moniteur | x, y (caractère), px, py (pixel gfx), button, source |
drag | la souris bouge avec un bouton enfoncé, dans le terminal | les mêmes que click |
message | un message d'un autre ordinateur (net) | sender, channel, data, via, distance |
link | la puissance reçue sur une fréquence de Liaison de redstone utilisée par le programme change (link) | a, b, power |
disk | un support de stockage est inséré ou éjecté | inserted : true ou false |
| n'importe quel nom | os.queue_event(nom, donnée) | data |
La page Évènements détaille chaque champ. Quelques points à retenir dès maintenant :
- L'évènement
redstonedit que quelque chose a changé, pas quoi. Lisez les faces qui vous intéressent avecrs.getquand il arrive (voirRedstone et liaisons de Create). - Les clics se comptent à partir de 1, 1 en haut à gauche.
buttonvaut 1 pour le bouton gauche, 2 pour le droit, 3 pour celui du milieu ; un appui sur un moniteur a toujoursbuttonégal à 1.sourcevaut"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) ouos.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 :
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
endRien 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 :
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
endcaisses 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 :
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
endComparez 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.
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")
endpersonne 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 :
os.start_timer(0.5)
sleep(2)
local e = os.pull_event()
print(e.name .. " a sonné pendant la pause")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.
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 iciQuand 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.
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")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.
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
endTrois 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").
Exemple : un menu piloté au clavier
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.
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)
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.