games / lua
Fonctions, portée et closures
Explication
Les fonctions comme valeurs, pas juste comme instructions
En Lua, une fonction n'est pas un simple bloc d'instructions nommé : c'est une VALEUR comme un nombre ou une chaîne, qu'on peut stocker dans une variable, passer en paramètre à une autre fonction, ou retourner comme résultat. Cette idée, appelée "fonction de première classe", est ce qui rend possible tout le reste de la leçon.
Ce que vous allez apprendre
- Déclarer une fonction et la manipuler comme une valeur (l'assigner, la passer en argument)
- Utiliser les varargs (
...) pour accepter un nombre variable d'arguments - Exploiter les valeurs de retour multiples, une particularité forte de Lua
- Construire une closure et comprendre ce qu'elle "capture" réellement
- Écrire une fonction récursive locale sans tomber dans le piège du nom non résolu
Dans quel contexte ?
Un système d'effets de statut dans un jeu (poison, brûlure, ralentissement) doit générer une fonction différente pour chaque type d'effet, chacune se souvenant de sa propre intensité et de sa propre durée, sans jamais mélanger les états de deux effets actifs en même temps sur des joueurs différents. C'est exactement le problème que les closures résolvent naturellement, et que ce cours retrouvera constamment sous forme de callbacks dans les leçons suivantes.
| Concept | Ce qu'il permet | Exemple |
|---|---|---|
| Fonction de 1ère classe | Stocker/passer une fonction comme une valeur | local f = print |
Varargs ... | Accepter un nombre variable d'arguments | function sum(...) |
| Retours multiples | Renvoyer plusieurs valeurs distinctes | return min, max |
| Closure | Se souvenir de son environnement de création | makeCounter() |
Bonne pratique
Utilise toujours local function nom(...) plutôt que local nom = function(...) end dès qu'une fonction doit s'appeler elle-même (récursion) : au moment de la définition, local function rend le nom immédiatement visible dans son propre corps, contrairement à l'autre syntaxe.
Des fonctions plus flexibles qu'il n'y paraît
Une fonction Lua peut accepter un nombre variable d'arguments (..., les varargs) et surtout peut RETOURNER plusieurs valeurs en une seule fois — pas un tuple ou une liste comme dans d'autres langages, mais littéralement plusieurs valeurs distinctes que l'appelant peut capturer séparément. C'est une particularité du langage qui simplifie beaucoup de code une fois assimilée.
Comprendre les closures avec une image simple
Une closure, c'est une fonction qui "se souvient" de l'environnement dans lequel elle a été créée, même après que cet environnement a normalement cessé d'exister. Imaginez une fonction qui crée un compteur : chaque appel à cette fonction créatrice produit un NOUVEAU compteur indépendant, avec sa propre mémoire privée, parce que chaque closure capture sa propre copie de la variable locale au moment de sa création.
Un piège classique... qui n'existe pas en Lua
Dans certains langages, une boucle for qui crée des closures capture accidentellement la MÊME variable partagée entre toutes les itérations, causant un bug notoire. En Lua, chaque itération d'une boucle for numérique crée une NOUVELLE variable locale distincte — ce piège classique n'existe donc simplement pas, un détail rassurant à connaître.
La récursion, un cas particulier à ne pas rater
Pour qu'une fonction puisse s'appeler elle-même par son nom de façon fiable, il faut la déclarer avec local function nom(...) plutôt que local nom = function(...) end — sinon, au moment où la fonction est définie, son propre nom n'existe pas encore dans la portée locale, et l'appel récursif échouerait ou référencerait la mauvaise variable.
Commandes & code
Fonctions, portée et closures
-- Déclaration de fonction : les fonctions sont des valeurs de première classe (first-class citizens)
local function add(a, b)
return a + b
end
local multiply = function(a, b) -- forme équivalente, fonction anonyme assignée à une variable
return a * b
end
print(add(3, 4), multiply(3, 4)) -- 7 12-- Nombre variable d'arguments : varargs "..."
local function sum(...)
local total = 0
for _, value in ipairs({...}) do -- {...} capture tous les arguments dans une table
total = total + value
end
return total
end
print(sum(1, 2, 3, 4, 5)) -- 15
-- select() pour inspecter les varargs sans les empaqueter dans une table
local function countArgs(...)
return select("#", ...) -- nombre total d'arguments, y compris les nil explicites
end
print(countArgs(1, nil, 3)) -- 3-- Valeurs de retour multiples, une particularité forte de Lua
local function minMax(t)
local min, max = t[1], t[1]
for _, v in ipairs(t) do
if v < min then min = v end
if v > max then max = v end
end
return min, max -- deux valeurs retournées
end
local lo, hi = minMax({5, 2, 9, 1, 7})
print(lo, hi) -- 1 9-- Closure : une fonction interne "capture" les variables locales de son environnement englobant
local function makeCounter()
local count = 0 -- variable capturée par la closure ci-dessous
return function()
count = count + 1 -- chaque appel modifie la MÊME variable capturée
return count
end
end
local counter1 = makeCounter()
local counter2 = makeCounter() -- environnement indépendant, sa propre variable "count"
print(counter1()) -- 1
print(counter1()) -- 2
print(counter2()) -- 1 (indépendant de counter1)-- Cas d'usage réel : générateur d'event handlers paramétrés, pattern très courant en gamedev
local function createDamageHandler(damageAmount)
return function(entity)
entity.hp = entity.hp - damageAmount
print(entity.name .. " subit " .. damageAmount .. " dégâts, HP restant: " .. entity.hp)
end
end
local fireballHandler = createDamageHandler(50)
local slapHandler = createDamageHandler(5)
local player = { name = "Alice", hp = 100 }
fireballHandler(player) -- "Alice subit 50 dégâts, HP restant: 50"
slapHandler(player) -- "Alice subit 5 dégâts, HP restant: 45"-- Piège classique des closures dans une boucle "for" avec des fonctions accumulées
local handlers = {}
for i = 1, 3 do
handlers[i] = function() print(i) end -- en Lua, "i" est une NOUVELLE variable locale à chaque itération
end
handlers[1]() -- 1
handlers[2]() -- 2
handlers[3]() -- 3
-- (contrairement à certains langages où toutes les closures partageraient la même variable finale)-- Fonctions récursives locales : nécessite "local function" (pas juste "local x = function")
-- pour que la fonction puisse s'appeler elle-même par son nom
local function factorial(n)
if n <= 1 then return 1 end
return n * factorial(n - 1) -- "factorial" doit déjà être visible dans son propre corps
end
print(factorial(6)) -- 720Résumé
- Les fonctions Lua acceptent des varargs (
...) et retournent nativement plusieurs valeurs. - Une closure capture les variables locales de son environnement de création, indépendamment entre instances.
- Dans une boucle
fornumérique, chaque itération crée une nouvelle variable locale (pas de piège de partage). local function nom(...)(et nonlocal nom = function) est requis pour une récursion propre par nom.
Exercices pratiques
Mission : des effets de poison qui se mélangent entre joueurs
Objectif : Corriger un système d'effets de statut où tous les joueurs empoisonnés semblent partager la même intensité, et fiabiliser une fonction récursive.
Contexte
Un système d'effets de statut RP génère un gestionnaire d'effet par type, mais un développeur a écrit ce code qui pose problème :
local activeIntensity = 0 -- variable globale au fichier, PAS locale à chaque effet
local function makePoisonEffect(intensity)
activeIntensity = intensity
return function(entity)
entity.hp = entity.hp - activeIntensity
end
end
local poisonWeak = makePoisonEffect(5)
local poisonStrong = makePoisonEffect(20)
poisonWeak(playerA) -- inflige 20 dégâts au lieu de 5 !Le développeur pensait, comme dans la leçon sur les closures, que chaque appel à makePoisonEffect créerait un environnement indépendant. Ce n'est visiblement pas le cas ici.