Retour au cours

games / lua

Métatables et programmation orientée objet

Leçon 61 exercice

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 setmetatable et comprendre son rôle
  • Construire un pattern "classe" avec __index pointant vers une table de méthodes
  • Comprendre que objet:methode() n'est qu'un raccourci pour objet.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éthodeDéclenchée parUsage typique
__indexLecture d'une clé absenteHéritage, méthodes de "classe"
__newindexÉcriture d'une clé absenteValidation, table en lecture seule
__addOpérateur +Objets mathématiques (vecteurs)
__tostringtostring(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.

lua
-- 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"
lua
-- __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)
lua
-- 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
lua
-- 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"
lua
-- 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
lua
-- __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
lua
-- 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 implicitement self en premier argument.
  • L'héritage se fait en chaînant les __index d'une "classe" à l'autre.
  • Les métaméthodes arithmétiques/comparaison (__add, __eq, __tostring...) permettent de surcharger les opérateurs.

Exercices pratiques

1 disponible
1

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 :

lua
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.

Résoudre l’exercice →