games / lua
Coroutines
Explication
Un multitâche différent de ce à quoi on pense habituellement
Ce que vous allez apprendre
- Créer une coroutine et comprendre le cycle
create/resume/yield - Distinguer
coroutine.create(contrôle fin des erreurs) decoroutine.wrap(syntaxe concise) - Construire un générateur paresseux qui produit des valeurs à la demande
- Utiliser les coroutines pour représenter une séquence d'actions étalée dans le temps (dialogue, chargement)
- Vérifier qu'on est bien dans une coroutine avant d'appeler
yield(coroutine.isyieldable)
Dans quel contexte ?
Un scénario de quête dans un jeu doit afficher plusieurs répliques d'un PNJ, chacune séparée par une attente du joueur ou par un délai, sans bloquer le reste du jeu pendant cette attente et sans avoir à réécrire toute la logique en callbacks imbriqués. Les coroutines permettent d'écrire cette séquence comme une simple suite d'instructions linéaires, alors qu'en coulisses, elle est mise en pause et reprise plusieurs fois.
Quand on parle de "faire plusieurs choses en même temps" en programmation, on pense souvent aux threads, où le système d'exploitation décide arbitrairement quand interrompre chaque tâche. Les coroutines Lua fonctionnent différemment : c'est du multitâche COOPÉRATIF, où une seule coroutine s'exécute à la fois, et c'est ELLE-MÊME qui décide, via yield, du moment précis où elle cède volontairement la main. Aucune interruption surprise, aucun problème de concurrence classique — c'est prévisible et facile à raisonner.
L'image à retenir : mettre en pause et reprendre exactement où on était
Une coroutine, c'est comme un signet dans un livre : yield marque la page où l'on s'arrête, et resume reprend la lecture exactement à cet endroit, avec toutes les variables locales intactes, comme si le temps s'était juste arrêté puis redémarré. C'est ce qui rend les coroutines idéales pour représenter des séquences d'actions étalées dans le temps — un dialogue de jeu qui attend une réponse du joueur, un chargement de ressources progressif, une machine à états complexe.
Deux façons de créer une coroutine, deux philosophies
coroutine.create suivi de resume donne un contrôle fin sur les erreurs : resume retourne un booléen de succès plutôt que de faire planter le programme. coroutine.wrap est plus concis à écrire mais propage les erreurs de façon brutale, comme une fonction normale qui lève une exception — un compromis entre lisibilité et robustesse à choisir selon le contexte.
| Fonction | Erreurs | Type de retour |
|---|---|---|
coroutine.create | Capturées, resume renvoie false + message | objet thread |
coroutine.wrap | Propagées comme une erreur classique | fonction appelable |
Astuce
Utilise coroutine.create dès que tu veux gérer proprement un échec possible (par exemple une séquence de quête qui peut être interrompue), et réserve coroutine.wrap aux générateurs simples où une erreur signifie de toute façon un bug à corriger.
Un usage très naturel : les générateurs paresseux
Combiner coroutine.wrap avec une boucle interne qui yield une valeur à chaque itération permet de construire un itérateur qui ne calcule chaque élément qu'au moment où on le demande réellement — utile pour parcourir de grandes séquences sans tout calculer et stocker en mémoire d'un coup.
Commandes & code
Coroutines
Les coroutines permettent de suspendre et reprendre l'exécution d'une fonction, offrant une forme de multitâche coopératif (pas de parallélisme réel, un seul thread OS).
-- Création et exécution basique d'une coroutine
local co = coroutine.create(function(a, b)
print("début", a, b)
local c = coroutine.yield(a + b) -- suspend, renvoie a+b au "resume" appelant
print("reprise avec c =", c)
return "terminé"
end)
print(coroutine.resume(co, 1, 2)) -- true 3 (exécute jusqu'au yield)
print(coroutine.status(co)) -- "suspended"
print(coroutine.resume(co, 100)) -- true "terminé" (reprend après yield, c=100)
print(coroutine.status(co)) -- "dead"-- Générateur / itérateur paresseux via coroutine : produit des valeurs à la demande sans tout calculer d'avance
local function range(n)
return coroutine.wrap(function() -- coroutine.wrap retourne directement une fonction appelable
for i = 1, n do
coroutine.yield(i)
end
end)
end
for value in range(5) do
print(value) -- 1 2 3 4 5, chaque valeur générée à la demande
end-- Différence coroutine.create vs coroutine.wrap
-- create : retourne un objet "thread", nécessite coroutine.resume() explicite, gère les erreurs proprement
-- wrap : retourne une fonction directement appelable, propage les erreurs par un "error()" classique (moins de contrôle)
local co = coroutine.create(function() error("boom") end)
local ok, err = coroutine.resume(co)
print(ok, err) -- false "chemin:ligne: boom" (l'erreur est capturée, pas de crash du programme)-- Cas d'usage réel : simulation d'une séquence d'actions asynchrones dans une boucle de jeu
-- (pattern très courant dans les moteurs de script de jeux vidéo)
local function questSequence()
print("Le PNJ dit: Bonjour aventurier !")
coroutine.yield() -- suspend, attend la frame/tick suivant du jeu
print("Le PNJ dit: As-tu trouvé l'artefact ?")
coroutine.yield()
print("Le PNJ dit: Merci, voici ta récompense !")
end
local questCo = coroutine.create(questSequence)
-- Boucle de jeu simplifiée : une étape de la coroutine par "frame"
for frame = 1, 3 do
print("--- frame " .. frame .. " ---")
coroutine.resume(questCo)
end-- Producteur/consommateur avec coroutines : découpler la génération de données de leur traitement
local function producer()
return coroutine.wrap(function()
for i = 1, 5 do
coroutine.yield("item_" .. i)
end
end)
end
local function consumer(gen)
for item in gen do
print("traitement de " .. item)
end
end
consumer(producer())-- Pattern avancé : coroutines pour une machine à états (state machine) lisible sans callbacks imbriqués
local function loadingSequence()
print("Chargement des assets...")
coroutine.yield("loading_assets")
print("Connexion au serveur...")
coroutine.yield("connecting")
print("Prêt à jouer !")
coroutine.yield("ready")
end
local state = coroutine.wrap(loadingSequence)
print(state()) -- "loading_assets" (avec les prints intercalés)
print(state()) -- "connecting"
print(state()) -- "ready"-- Vérifier qu'on est bien à l'intérieur d'une coroutine avant d'appeler yield (sinon erreur)
local function safeYield(value)
if coroutine.isyieldable() then
return coroutine.yield(value)
end
return value -- fallback si appelé hors coroutine
endRésumé
- Les coroutines sont un multitâche COOPÉRATIF : une seule s'exécute à la fois, elle cède la main volontairement via
yield. coroutine.create+resumegère les erreurs proprement ;coroutine.wrapest plus concis mais propage les erreurs brutalement.- Un générateur/itérateur paresseux se construit naturellement avec
coroutine.wrapet une boucleyield. - Les coroutines simplifient les séquences d'actions étalées dans le temps (dialogues, chargements, machines à états).
Exercices pratiques
Mission : la quête qui saute une réplique
Objectif : Analyser précisément le cycle resume/yield d'une séquence de dialogue de quête et choisir la bonne stratégie de gestion d'erreur.
Contexte
Une séquence de dialogue de quête utilise une coroutine :
local function questSequence()
print("PNJ: Bonjour aventurier !")
local reponse = coroutine.yield("attente_reponse_1")
print("PNJ: Tu as dit " .. reponse)
coroutine.yield("attente_reponse_2")
print("PNJ: Merci, voici ta récompense !")
return "quete_terminee"
end
local co = coroutine.create(questSequence)
print(coroutine.resume(co)) -- ok, "attente_reponse_1"
print(coroutine.resume(co, "bonjour")) -- ok, "attente_reponse_2"
print(coroutine.status(co))Un testeur signale qu'un joueur qui répond très vite provoque parfois une erreur "cannot resume dead coroutine" en jeu.