Module 03 · Leçon 6 sur 6
ModuleScripts et programmation orientée objet
Comprends les ModuleScripts et require, reconnais un code bien organisé autour d’un module Config, lis une « classe » faite avec les métatables et découvre le typage optionnel de Luau.
- ⏱️ 50 min
- 📝 Quiz de 8 questions
- 🔄 Vérifié le 24 octobre 2026
Sur cette page
- Le ModuleScript : du code à partager
- Fil rouge : le module Config
- Utiliser Config depuis un autre script
- Où ranger ses modules ?
- La programmation orientée objet (POO)
- Les métatables et __index
- Le motif de classe
- . ou : ? La clé de self
- Utiliser la classe
- Aperçu : le typage optionnel de Luau
- Les annotations
- Les modes d’analyse et --!strict
- Lire ce code comme un relecteur
- Exercice 1 : que va afficher l’Output ?
- Exercice 2 : ce code est-il bien organisé ?
- Quiz de fin de leçon
🎯 Dans cette leçon, tu vas apprendre à…
- Lire un ModuleScript et le require qui le charge
- Reconnaître un code bien organisé, avec les réglages centralisés dans un module Config
- Comprendre une « classe » faite avec les métatables, __index, new et self
- Lire les annotations de type et le mode --!strict
Ton Obby des Nuages a maintenant plusieurs scripts : KillBricks, PiecesTournantes, Checkpoints… Chacun contient ses propres réglages (vitesse de rotation, hauteur de réapparition…) éparpillés un peu partout. Le jour où tu voudras les modifier, il faudra fouiller chaque script. Dans cette dernière leçon du module, tu vas apprendre à reconnaître un code bien rangé : des ModuleScripts pour partager réglages et fonctions, et la programmation orientée objet pour créer tes propres types d’objets. C’est une compétence clé pour diriger une IA : elle écrit vite, mais c’est à toi de vérifier qu’elle range au bon endroit, ne duplique pas les réglages et ne met rien de secret là où les joueurs peuvent le lire. Tu termineras par un aperçu du typage de Luau, que tu verras souvent dans le code généré.
Le ModuleScript : du code à partager
Un ModuleScript est un script qui ne s’exécute pas tout seul. Il renvoie une valeur (presque toujours une table) que d’autres scripts récupèrent avec require.
Les règles essentielles, d’après la documentation Roblox :
- un ModuleScript doit renvoyer exactement une valeur, avec
returnà la fin, au niveau principal du fichier (pas à l’intérieur d’une fonction), et cette valeur ne peut pas êtrenil; - son code s’exécute une seule fois par environnement : le premier
requireexécute le module, les suivants renvoient la même valeur (mise en cache) ; - le serveur et chaque client sont des environnements séparés : si un Script et un LocalScript font
requiredu même module, chacun reçoit sa propre copie.
Fil rouge : le module Config
On va centraliser tous les réglages de l’Obby des Nuages dans un seul module.
- Dans ReplicatedStorage, crée un Folder nommé
Modules. - Dans ce dossier, insère un ModuleScript nommé
Config. - Remplace son contenu par le code ci-dessous.
Dans ton projet Rojo, ces trois étapes se résument à un fichier : src/ReplicatedStorage/Modules/Config.luau (le dossier src/ReplicatedStorage/Modules devient le Folder ReplicatedStorage.Modules, et le suffixe .luau seul donne un ModuleScript). Tu peux le taper ou le faire créer par Claude Code (prompt dans l’encadré plus bas). Voici le module à obtenir :
-- ModuleScript « Config » dans ReplicatedStorage.Modules
-- Tous les réglages de l'Obby des Nuages, au même endroit.
local Config = {}
-- Pièces
Config.VITESSE_ROTATION_PIECES = 90 -- degrés par seconde
Config.VALEUR_PIECE = 1 -- pièces gagnées par ramassage
-- Checkpoints
Config.HAUTEUR_REAPPARITION = Vector3.new(0, 4, 0)
-- Niveaux : un tableau de dictionnaires (leçon 4)
Config.Niveaux = {
{nom = "Les premiers nuages", etapeDebut = 1, etapeFin = 5},
{nom = "La tempête", etapeDebut = 6, etapeFin = 10},
{nom = "Le sommet", etapeDebut = 11, etapeFin = 12},
}
-- Un module peut aussi contenir des fonctions
-- Renvoie le niveau qui contient l'étape donnée (ou nil)
function Config.obtenirNiveau(etape)
for _, niveau in Config.Niveaux do
if etape >= niveau.etapeDebut and etape <= niveau.etapeFin then
return niveau
end
end
return nil
end
return Config -- OBLIGATOIRE : c'est la valeur que recevra require
Utiliser Config depuis un autre script
Modifions le Script PiecesTournantes de la leçon 4 pour qu’il lise sa vitesse dans Config :
-- Script « PiecesTournantes » dans ServerScriptService
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")
-- require renvoie la table "Config" retournée par le module
local Config = require(ReplicatedStorage:WaitForChild("Modules"):WaitForChild("Config"))
local dossierPieces = Workspace:WaitForChild("Pieces")
while true do
local ecoule = task.wait()
local rotation = CFrame.Angles(0, math.rad(Config.VITESSE_ROTATION_PIECES * ecoule), 0)
for _, piece in dossierPieces:GetChildren() do
if piece:IsA("BasePart") then
piece.CFrame = piece.CFrame * rotation
end
end
end
Et dans n’importe quel autre script :
-- Script dans ServerScriptService : utiliser une fonction du module
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Config = require(ReplicatedStorage:WaitForChild("Modules"):WaitForChild("Config"))
local niveau = Config.obtenirNiveau(7)
if niveau then
print(`L'étape 7 fait partie du niveau « {niveau.nom} »`) --> La tempête
end
Désormais, pour accélérer les pièces, tu modifies une seule ligne dans Config. Fais de même dans ton script Checkpoints : ajoute en haut les deux lignes qui récupèrent ReplicatedStorage et font le require de Config (comme ci-dessus), supprime sa constante HAUTEUR_REAPPARITION et écris Config.HAUTEUR_REAPPARITION à la place, dans l’appel à PivotTo.
Où ranger ses modules ?
| Emplacement | Qui peut faire require |
Exemples |
|---|---|---|
ReplicatedStorage.Modules |
Serveur et clients | Config, Catalogue de la boutique, fonctions utilitaires |
ServerScriptService (ou ServerStorage) |
Serveur uniquement | Logique de sauvegarde, validation des achats |
La programmation orientée objet (POO)
Imagine que tu veuilles des pièces qui tournent chacune à leur vitesse, et qu’on puisse arrêter une par une. Tu pourrais gérer des tables de vitesses et d’états… ou créer un nouveau type d’objet, PieceTournante, qui regroupe ses données (la Part, la vitesse, l’état) et ses actions (tourner, s’arrêter). C’est l’idée de la programmation orientée objet : une classe est un modèle, et chaque objet (ou instance) créé à partir d’elle a ses propres données.
Luau n’a pas de mot-clé « classe » : on les fabrique avec les métatables.
Les métatables et __index
Une métatable est une table qui modifie le comportement d’une autre table. Le champ le plus utile s’appelle __index : quand on lit une clé absente d’une table, Luau va la chercher dans la table indiquée par __index.
local valeursParDefaut = {vitesse = 90, couleur = "Or"}
local maPiece = {vitesse = 180}
-- setmetatable(table, metatable) attache la métatable et renvoie la table
setmetatable(maPiece, {__index = valeursParDefaut})
print(maPiece.vitesse) --> 180 (la clé existe dans maPiece)
print(maPiece.couleur) --> Or (absente de maPiece : trouvée via __index)
Le motif de classe
Voici le motif classique utilisé partout sur Roblox, et que les IA produisent très souvent : apprends à le reconnaître. Pour l’essayer, crée un ModuleScript nommé PieceTournante dans ServerScriptService (fichier src/ServerScriptService/PieceTournante.luau dans ton projet Rojo) :
-- ModuleScript « PieceTournante » dans ServerScriptService
local PieceTournante = {}
-- Les objets chercheront leurs méthodes absentes dans PieceTournante
PieceTournante.__index = PieceTournante
-- Le constructeur : crée et renvoie un nouvel objet
function PieceTournante.new(part, vitesse)
local self = setmetatable({}, PieceTournante)
self.part = part -- données propres à CET objet
self.vitesse = vitesse -- degrés par seconde
self.active = true
return self
end
-- Une méthode (déclarée avec ":") : reçoit automatiquement l'objet dans "self"
function PieceTournante:mettreAJour(ecoule)
if not self.active then
return
end
self.part.CFrame = self.part.CFrame * CFrame.Angles(0, math.rad(self.vitesse * ecoule), 0)
end
function PieceTournante:arreter()
self.active = false
end
return PieceTournante
Décortiquons :
PieceTournante.__index = PieceTournante: la classe sert de métatable à ses objets, et ses méthodes sont trouvées grâce à__index;PieceTournante.new(...)est le constructeur : il crée une table vide, lui attache la classe comme métatable (setmetatablerenvoie la table), remplit ses données et la renvoie ;- les méthodes sont déclarées avec
:et utilisentself, qui désigne l’objet sur lequel on appelle la méthode.
. ou : ? La clé de self
Les deux-points sont un raccourci qui ajoute un paramètre self :
Écriture avec : |
Équivaut à |
|---|---|
function PieceTournante:arreter() |
function PieceTournante.arreter(self) |
maPiece:arreter() |
maPiece.arreter(maPiece) |
Tu utilises ce mécanisme depuis la leçon 1 : game:GetService("Players") est un appel de méthode sur l’objet game.
Utiliser la classe
-- Script dans ServerScriptService : crée un objet PieceTournante par pièce
local ServerScriptService = game:GetService("ServerScriptService")
local Workspace = game:GetService("Workspace")
local PieceTournante = require(ServerScriptService:WaitForChild("PieceTournante"))
local dossierPieces = Workspace:WaitForChild("Pieces")
local pieces = {} -- tableau d'objets PieceTournante
for _, part in dossierPieces:GetChildren() do
if part:IsA("BasePart") then
-- Vitesse au hasard entre 60 et 180 degrés par seconde
local objet = PieceTournante.new(part, math.random(60, 180))
table.insert(pieces, objet)
end
end
-- Au bout de 10 secondes, on arrête la première pièce (pour la démonstration)
task.delay(10, function()
if pieces[1] then
pieces[1]:arreter()
end
end)
while true do
local ecoule = task.wait()
for _, objet in pieces do
objet:mettreAJour(ecoule)
end
end
Chaque objet a sa propre vitesse et son propre état, mais toutes les pièces partagent les mêmes méthodes, écrites une seule fois.
Pour tester cette version, désactive ton Script PiecesTournantes (propriété Enabled décochée) : sinon, les deux scripts feraient tourner les mêmes pièces en même temps. Une fois tes essais terminés, fais l’inverse : désactive ce script de démonstration et réactive PiecesTournantes (la version branchée sur Config). C’est elle que les modules suivants reprennent, notamment au module 10 pour mesurer et alléger son coût.
Dans ton projet Rojo, le script de démonstration est lui aussi un fichier, par exemple src/ServerScriptService/DemoPieceTournante.server.luau. Pour mettre PiecesTournantes en pause sans risquer que Rojo rétablisse le script, le plus simple est d’entourer temporairement tout le contenu de PiecesTournantes.server.luau d’un commentaire long --[[ ... ]] (leçon 1). Une fois tes essais terminés, supprime les deux fichiers de démonstration (PieceTournante.luau et DemoPieceTournante.server.luau) et retire le commentaire : la suite de la formation ne les utilise pas.
Aperçu : le typage optionnel de Luau
Luau est un langage à typage graduel : tu peux, si tu le souhaites, annoter le type de tes variables et de tes fonctions. Ces annotations n’ont aucun effet sur l’exécution ; elles permettent à l’analyseur de Studio de repérer des erreurs pendant que tu écris (passer un texte à une fonction qui attend un nombre, oublier un cas nil…).
Les annotations
-- Variable annotée : deux-points puis le type
local nombreEtapes: number = 12
local nomDuJeu: string = "Obby des Nuages"
-- Paramètres et type de retour annotés ; "number?" veut dire "number ou nil"
local function calculerRecompense(etape: number, bonus: number?): number
return etape * 10 + (bonus or 0)
end
print(calculerRecompense(3)) --> 30
print(calculerRecompense(3, 5)) --> 35
-- Les types Roblox existent aussi : Part, Player, Vector3, Enum.Material...
local function colorer(part: BasePart, couleur: Color3)
part.Color = couleur
end
Les modes d’analyse et --!strict
Un commentaire spécial en première ligne du script choisit le niveau de vérification :
--!nocheck: pas de vérification des types ;--!nonstrict: vérifie surtout ce que tu as annoté ; ce qui n’est pas clair est traité commeany(« n’importe quoi »). C’est le mode par défaut indiqué par la documentation de Luau ;--!strict: vérifie tout, en devinant (inférant) les types que tu n’as pas écrits.
Pour choisir le mode par défaut de tous les scripts de ton jeu, la documentation Roblox renvoie à la propriété LuauTypeCheckMode du Workspace ; le commentaire en première ligne d’un script reste prioritaire.
Voici une version typée et stricte d’un module de niveaux. Le mot-clé type crée un alias de type, et export le rend utilisable dans les scripts qui font require du module :
--!strict
-- ModuleScript « Niveaux » dans ReplicatedStorage.Modules (version typée)
export type Niveau = {
nom: string,
etapeDebut: number,
etapeFin: number,
}
local Niveaux = {}
Niveaux.liste = {
{nom = "Les premiers nuages", etapeDebut = 1, etapeFin = 5},
{nom = "La tempête", etapeDebut = 6, etapeFin = 10},
} :: {Niveau} -- "::" précise le type d'une expression : un tableau de Niveau
-- Renvoie un Niveau, ou nil (d'où le "?")
function Niveaux.obtenir(etape: number): Niveau?
for _, niveau in Niveaux.liste do
if etape >= niveau.etapeDebut and etape <= niveau.etapeFin then
return niveau
end
end
return nil
end
return Niveaux
Avec --!strict, si tu écris par erreur niveau.etapeDebu ou Niveaux.obtenir("7"), Studio le souligne immédiatement, avant même de lancer le jeu.
Lire ce code comme un relecteur
Exercice 1 : que va afficher l’Output ?
local Compteur = {}
Compteur.__index = Compteur
function Compteur.new(depart)
local self = setmetatable({}, Compteur)
self.valeur = depart
return self
end
function Compteur:ajouter(n)
self.valeur += n
return self.valeur
end
local a = Compteur.new(10)
local b = Compteur.new(0)
a:ajouter(5)
print(a.valeur, b.valeur)
print(b:ajouter(2))
Et que se passerait-il si on écrivait a.ajouter(5), avec un point ?
Exercice 2 : ce code est-il bien organisé ?
Tu as demandé à une IA de brancher PiecesTournantes sur Config. Voici ce qu’elle propose :
-- Script « PiecesTournantes » dans ServerScriptService (version proposée par une IA)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
local Config = require(ReplicatedStorage:WaitForChild("Modules"):WaitForChild("Config"))
local VITESSE_ROTATION = 90 -- degrés par seconde
local dossierPieces = Workspace:WaitForChild("Pieces")
while true do
local ecoule = task.wait()
local rotation = CFrame.Angles(0, math.rad(VITESSE_ROTATION * ecoule), 0)
for _, piece in dossierPieces:GetChildren() do
if piece:IsA("BasePart") then
piece.CFrame = piece.CFrame * rotation
end
end
end
- Tu passes
Config.VITESSE_ROTATION_PIECESà 180. Que se passe-t-il ? - Quelles lignes sont inutiles ?
Ce qu’il faut retenir
- Un ModuleScript renvoie une seule valeur (souvent une table) récupérée avec
require; il s’exécute une fois par environnement et sa valeur est mise en cache. - Le module
ConfigdansReplicatedStorage.Modulescentralise les réglages de l’Obby des Nuages. - Une classe = une table utilisée comme métatable, avec
Classe.__index = Classe, un constructeurClasse.newqui appellesetmetatable, et des méthodes déclarées avec:. objet:methode()équivaut àobjet.methode(objet): c’est ce qui remplitself.- Les annotations de type (
: number,?,type,export type) et--!strictaident Studio à détecter les erreurs, sans changer l’exécution. - Un code bien organisé n’a qu’une source pour chaque réglage et ne met jamais de secret dans ReplicatedStorage : vérifie-le dans tout code généré.
Sources et pour aller plus loin
- Module scripts — documentation Roblox
- ModuleScript — référence API
- Metatables (__index, classes) — documentation Luau de Roblox
- Type checking — documentation Luau de Roblox
- Types et modes d’analyse — luau.org
- Workspace.LuauTypeCheckMode — référence API
- Tables (table.freeze, table.clone) — documentation Luau de Roblox
- Fil DevForum « How do I fix “Module code did not return exactly one value” »