games / lua
Modules et require
Explication
Ce que vous allez apprendre
- Créer un module Lua en construisant une table publique retournée avec
return - Importer un module avec
requireet comprendre son chemin de résolution - Expliquer pourquoi
requiren'exécute un fichier qu'une seule fois par processus - Cacher un état privé dans un module grâce à des variables
localnon exportées - Combiner modules et métatables pour exporter une "classe" complète
Dans quel contexte ?
Un serveur de jeu grossit et son fichier unique de 2000 lignes devient impossible à maintenir : la gestion des joueurs, les mathématiques utilitaires et l'inventaire sont tous mélangés dans le même espace de noms global. Découper ce fichier en modules indépendants (player.lua, math_utils.lua, inventory.lua), chacun import able via require, est l'étape presque obligatoire dès qu'un projet Lua dépasse quelques centaines de lignes.
| Élément | Rôle |
|---|---|
return M en fin de fichier | Expose l'API publique du module |
require("nom") | Charge le fichier et récupère la table exportée |
Variable local non exportée | État privé, invisible depuis l'extérieur du module |
package.loaded | Cache interne : un module n'est exécuté qu'une seule fois |
Bonne pratique
Regroupe tous les require() d'un fichier en haut du script, dans le même ordre que leurs dépendances réelles. Cela documente implicitement ce dont ton fichier a besoin pour fonctionner, et évite les surprises d'ordre de chargement.
Le problème : tout mettre dans un seul fichier ne tient pas longtemps
Un programme Lua qui grossit finit toujours par devoir être découpé en plusieurs fichiers logiques — une partie pour la gestion des joueurs, une autre pour les mathématiques utilitaires, une autre pour l'inventaire. Sans mécanisme dédié pour organiser ce découpage, on retombe vite sur des variables globales qui se marchent dessus entre fichiers. Les modules Lua répondent exactement à ce besoin d'organisation.
L'idée centrale : un fichier qui retourne une table
Contrairement à d'autres langages qui ont une syntaxe spéciale pour déclarer un module, Lua reste minimaliste : un module est simplement un fichier normal qui construit une table contenant tout ce qu'il veut rendre public, puis la retourne à la fin avec return. require("nom_fichier") charge ce fichier et récupère cette table — c'est tout le mécanisme, sans magie supplémentaire.
Pourquoi require ne recharge jamais deux fois le même fichier
Une propriété importante à comprendre : appeler require plusieurs fois sur le même module ne réexécute pas le fichier à chaque fois. Lua garde en mémoire (dans package.loaded) le résultat de la première exécution, et le renvoie tel quel aux appels suivants. C'est ce qui permet à un module de maintenir un état partagé cohérent, même s'il est utilisé depuis plusieurs endroits différents du programme.
L'encapsulation : cacher ce qui ne doit pas être touché de l'extérieur
Une variable déclarée local dans un fichier de module n'existe que dans ce fichier — elle n'apparaît jamais dans la table retournée, sauf si on l'y ajoute explicitement. Cette propriété simple permet de construire une vraie encapsulation : un état interne (comme une liste d'objets en inventaire) reste invisible et non modifiable directement depuis l'extérieur, seule l'API explicitement exposée (les fonctions ajoutées à la table M) permet d'interagir avec cet état.
Le lien avec la programmation orientée objet
Cette même idée de "table exportée" se combine naturellement avec les métatables vues précédemment dans ce cours : un module peut exporter une classe entière, prête à être instanciée ailleurs avec require("vector").new(...).
Commandes & code
Modules et require
-- math_utils.lua : un module Lua est simplement un fichier qui RETOURNE une table
local M = {}
function M.clamp(value, min, max)
if value < min then return min end
if value > max then return max end
return value
end
function M.lerp(a, b, t)
return a + (b - a) * t
end
return M -- l'export du module se fait via ce "return" en fin de fichier-- main.lua : import du module avec require (chemin sans extension .lua, séparateur "." pour les dossiers)
local mathUtils = require("math_utils") -- fichier math_utils.lua dans le chemin de recherche
-- local geo = require("utils.geometry") -- chargerait utils/geometry.lua
print(mathUtils.clamp(150, 0, 100)) -- 100
print(mathUtils.lerp(0, 10, 0.5)) -- 5.0-- require met en cache le résultat : un module n'est exécuté qu'UNE SEULE FOIS même si require()
-- est appelé plusieurs fois à différents endroits du programme
local a = require("math_utils")
local b = require("math_utils")
print(a == b) -- true, même table retournée, le fichier n'a été exécuté qu'une fois
-- package.loaded contient le cache : on peut forcer un rechargement en le vidant (utile en développement)
package.loaded["math_utils"] = nil
local reloaded = require("math_utils") -- ré-exécute le fichier cette fois-- Pattern module avec état privé (encapsulation) : variables locales invisibles depuis l'extérieur
-- inventory_manager.lua
local M = {}
local items = {} -- état privé, inaccessible directement en dehors du module
function M.addItem(name, quantity)
items[name] = (items[name] or 0) + quantity
end
function M.getQuantity(name)
return items[name] or 0
end
function M.getAllItems()
local copy = {}
for k, v in pairs(items) do copy[k] = v end -- copie défensive, empêche la modification externe directe
return copy
end
return M
-- Le consommateur ne peut JAMAIS accéder ni modifier "items" directement, uniquement via l'API exposée-- Modules orientés objet : combiner module + métatable pour exporter une "classe" complète
-- vector.lua
local Vector2 = {}
Vector2.__index = Vector2
function Vector2.new(x, y)
return setmetatable({ x = x or 0, y = y or 0 }, Vector2)
end
function Vector2:length()
return math.sqrt(self.x ^ 2 + self.y ^ 2)
end
return Vector2
-- utilisation ailleurs :
-- local Vector2 = require("vector")
-- local v = Vector2.new(3, 4)
-- print(v:length()) -- 5.0-- package.path : contrôle où Lua cherche les fichiers de module (utile pour des structures de projet custom)
print(package.path)
-- Ajouter un dossier custom au chemin de recherche
package.path = package.path .. ";./src/?.lua;./libs/?/init.lua"-- Bonnes pratiques de structuration de modules
1. Un module = une responsabilité claire (éviter les fichiers "utils.lua" fourre-tout géants)
2. Toujours "return M" en fin de fichier, jamais de pollution de l'espace global
3. Exposer une API minimale, garder l'état interne privé via des locales non exportées
4. Documenter les dépendances explicites en haut de fichier (tous les require() groupés)Résumé
- Un module Lua est un fichier qui construit puis
returnune table exposant son API publique. requiremet en cache le résultat : le fichier ne s'exécute qu'une seule fois par processus.- L'état privé s'obtient avec des variables locales au fichier, jamais placées dans la table exportée.
package.pathcontrôle la résolution des chemins derequirepour des structures de projet custom.
Exercices pratiques
Mission : l'inventaire partagé qui ne se réinitialise jamais
Objectif : Comprendre pourquoi un module d'inventaire garde un état partagé entre deux scripts distincts, et corriger l'encapsulation de son état interne.
Contexte
inventory_manager.lua expose un module de gestion d'inventaire :
local M = {}
M.items = {} -- état exposé directement dans la table publique, pas caché
function M.addItem(name, quantity)
M.items[name] = (M.items[name] or 0) + quantity
end
return MDeux scripts différents du serveur font chacun local inv = require("inventory_manager"). Un développeur pensait obtenir deux inventaires séparés, un par script, mais découvre que les objets ajoutés dans le script A apparaissent aussi côté script B. Pire, un troisième script accède directement à inv.items["or"] = 999999 pour "tester", sans passer par addItem.