games / lua
Métatables et programmation orientée objet
Explication
Lua n'a pas de "classes" — et pourtant on peut faire de la POO
Contrairement à des langages comme Java ou Python qui ont un mot-clé class intégré, Lua ne fournit aucun mécanisme d'orienté objet natif. Ce que cette leçon montre, c'est comment construire ce concept À PARTIR des tables et d'un mécanisme plus fondamental : les métatables. Comprendre cela change la façon de voir la programmation orientée objet : ce n'est pas une fonctionnalité magique du langage, mais un PATTERN qu'on peut reconstruire soi-même une fois qu'on comprend les briques de base.
Ce que vous allez apprendre
- Attacher une métatable à une table avec
setmetatableet comprendre son rôle - Construire un pattern "classe" avec
__indexpointant vers une table de méthodes - Comprendre que
objet:methode()n'est qu'un raccourci pourobjet.methode(objet) - Mettre en place un héritage simple en chaînant des
__index - Surcharger des opérateurs (
+,==, conversion en chaîne) avec des métaméthodes
Dans quel contexte ?
Un jeu doit représenter plusieurs types d'entités (un joueur, un PNJ, un véhicule) qui partagent un comportement commun (avoir une position, subir des dégâts) tout en ayant chacune leurs particularités propres. Sans classes natives, Lua résout ce besoin avec des tables et des métatables : c'est exactement le genre de structure qu'on retrouve dans la quasi-totalité des scripts de jeu un peu ambitieux, y compris les frameworks RP les plus utilisés.
| Métaméthode | Déclenchée par | Usage typique |
|---|---|---|
__index | Lecture d'une clé absente | Héritage, méthodes de "classe" |
__newindex | Écriture d'une clé absente | Validation, table en lecture seule |
__add | Opérateur + | Objets mathématiques (vecteurs) |
__tostring | tostring(obj) / print(obj) | Affichage lisible d'un objet |
Prérequis
Cette leçon suppose que tu es à l'aise avec les tables (leçons 3 et 4) et les fonctions (leçon 5) : les métatables s'appuient directement sur ces deux notions.
Le mécanisme clé : __index comme "table de secours"
Quand on essaie de lire un champ inexistant dans une table qui a une métatable avec un champ __index, Lua va chercher automatiquement dans cette table de secours avant de renvoyer nil. C'est exactement ce mécanisme qui permet de simuler l'héritage : une "instance" qui n'a pas sa propre méthode va la chercher automatiquement dans sa "classe" via __index, et une classe peut elle-même chercher dans une classe parente en chaînant les __index les uns aux autres.
Le sucre syntaxique objet:methode()
L'écriture objet:methode() n'est qu'un raccourci pour objet.methode(objet) — le langage passe implicitement l'objet lui-même comme premier argument, habituellement nommé self dans la définition de la fonction. Comprendre que ce n'est QUE du sucre syntaxique démystifie beaucoup de code Lua orienté objet qui semble complexe au premier abord.
Les métaméthodes, ou comment redéfinir le comportement des opérateurs
Au-delà de __index, les métatables permettent de définir ce que signifie +, ==, ou même l'affichage d'un objet (tostring) pour un type personnalisé, via des métaméthodes comme __add, __eq, __tostring. C'est ce qui permet à un objet Vector3 de s'additionner avec v1 + v2 au lieu d'exiger un appel de méthode explicite — la prochaine leçon approfondit encore ces mécanismes de métaprogrammation.
Commandes & code
Métatables et programmation orientée objet
Lua n'a pas de classes natives : la POO se construit avec des tables et des métatables.
-- Métatable minimale : intercepte les opérations sur une table via des "métaméthodes"
local t = setmetatable({}, {
__index = function(table, key)
return "clé inconnue: " .. key -- appelé quand une clé absente est lue
end,
})
print(t.foo) -- "clé inconnue: foo"-- __index avec une TABLE (pas une fonction) : pattern le plus courant pour l'héritage/POO
local Animal = {}
Animal.__index = Animal -- Animal sert de "classe" ET de métatable pour ses instances
function Animal.new(name, sound)
local self = setmetatable({}, Animal)
self.name = name
self.sound = sound
return self
end
function Animal:makeSound() -- "self" implicite via la syntaxe ":"
print(self.name .. " fait " .. self.sound)
end
local dog = Animal.new("Rex", "Wouf")
dog:makeSound() -- "Rex fait Wouf"
-- Fonctionnement : dog n'a pas de méthode makeSound, Lua cherche dans sa métatable (__index = Animal)-- Sucre syntaxique ":" équivalent à passer "self" explicitement en premier argument
function Animal:makeSound() ... end
-- est strictement équivalent à :
function Animal.makeSound(self) ... end
dog:makeSound() -- équivalent à :
Animal.makeSound(dog) -- appel explicite avec self passé manuellement-- Héritage : chaîner les métatables (Dog hérite de Animal)
local Dog = setmetatable({}, { __index = Animal }) -- Dog délègue à Animal si non trouvé
Dog.__index = Dog
function Dog.new(name)
local self = Animal.new(name, "Wouf") -- réutilise le constructeur parent
return setmetatable(self, Dog) -- réassigne la métatable à Dog (pas Animal)
end
function Dog:fetch() -- méthode spécifique à Dog, absente de Animal
print(self.name .. " rapporte la balle")
end
local rex = Dog.new("Rex")
rex:makeSound() -- hérité d'Animal : "Rex fait Wouf"
rex:fetch() -- propre à Dog : "Rex rapporte la balle"-- Métaméthodes arithmétiques : surcharge d'opérateurs pour des types custom (ex: vecteurs 2D)
local Vector2 = {}
Vector2.__index = Vector2
function Vector2.new(x, y)
return setmetatable({ x = x, y = y }, Vector2)
end
Vector2.__add = function(a, b)
return Vector2.new(a.x + b.x, a.y + b.y)
end
Vector2.__tostring = function(v)
return string.format("Vector2(%g, %g)", v.x, v.y)
end
Vector2.__eq = function(a, b)
return a.x == b.x and a.y == b.y
end
local v1 = Vector2.new(1, 2)
local v2 = Vector2.new(3, 4)
local v3 = v1 + v2 -- appelle __add
print(tostring(v3)) -- appelle __tostring : "Vector2(4, 6)"
print(v1 == Vector2.new(1, 2)) -- appelle __eq : true-- __newindex : intercepte l'ÉCRITURE sur une clé absente (utile pour des tables en lecture seule ou validées)
local function readOnlyTable(t)
return setmetatable({}, {
__index = t,
__newindex = function(table, key, value)
error("Tentative de modification d'une table en lecture seule: " .. tostring(key), 2)
end,
})
end
local CONFIG = readOnlyTable({ maxPlayers = 4, difficulty = "hard" })
print(CONFIG.maxPlayers) -- 4, lecture OK via __index
-- CONFIG.maxPlayers = 8 -- lèverait une erreur via __newindex-- Tableau récapitulatif des métaméthodes les plus utilisées
-- __index : lecture d'une clé absente
-- __newindex : écriture d'une clé absente
-- __add/__sub/__mul/__div : opérateurs arithmétiques
-- __eq/__lt/__le : comparaisons
-- __tostring : conversion en chaîne (utilisée par print/tostring)
-- __call : rend une table "appelable" comme une fonction
-- __len : personnalise le résultat de l'opérateur "#"Résumé
setmetatable+__index(table) est le socle de la POO et de l'héritage en Lua.- La syntaxe
objet:methode()passe implicitementselfen premier argument. - L'héritage se fait en chaînant les
__indexd'une "classe" à l'autre. - Les métaméthodes arithmétiques/comparaison (
__add,__eq,__tostring...) permettent de surcharger les opérateurs.
Exercices pratiques
Mission : un PNJ qui n'aboie jamais
Objectif : Diagnostiquer une chaîne d'héritage cassée entre entités de jeu et implémenter une surcharge d'opérateur manquante.
Contexte
Un système d'entités de jeu définit une hiérarchie Animal -> Dog, mais quelque chose cloche :
local Animal = {}
Animal.__index = Animal
function Animal.new(name, sound)
local self = setmetatable({}, Animal)
self.name = name
self.sound = sound
return self
end
function Animal:makeSound()
print(self.name .. " fait " .. self.sound)
end
local Dog = {} -- oubli : pas de setmetatable(Dog, { __index = Animal })
function Dog.new(name)
local self = Animal.new(name, "Wouf")
return setmetatable(self, Dog)
end
local rex = Dog.new("Rex")
rex:makeSound() -- erreur: attempt to call a nil value (method 'makeSound')L'équipe veut aussi pouvoir additionner deux entités Vector2 (positions) avec +, mais l'opérateur n'est pas encore défini pour ce type.