Aller au contenu
Forma Roblox

Rechercher dans la formation

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

🎯 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 être nil ;
  • son code s’exécute une seule fois par environnement : le premier require exé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 require du 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.

  1. Dans ReplicatedStorage, crée un Folder nommé Modules.
  2. Dans ce dossier, insère un ModuleScript nommé Config.
  3. 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 (setmetatable renvoie la table), remplit ses données et la renvoie ;
  • les méthodes sont déclarées avec : et utilisent self, 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é comme any (« 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
  1. Tu passes Config.VITESSE_ROTATION_PIECES à 180. Que se passe-t-il ?
  2. 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 Config dans ReplicatedStorage.Modules centralise les réglages de l’Obby des Nuages.
  • Une classe = une table utilisée comme métatable, avec Classe.__index = Classe, un constructeur Classe.new qui appelle setmetatable, et des méthodes déclarées avec :.
  • objet:methode() équivaut à objet.methode(objet) : c’est ce qui remplit self.
  • Les annotations de type (: number, ?, type, export type) et --!strict aident 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

Quiz : vérifie tes acquis

8 questions · réussite à partir de 70 % · valide chaque réponse pour voir l’explication

  1. Question 1Vrai ou fauxSi trois Scripts serveur font require du même ModuleScript, le code du module s’exécute trois fois.
  2. Question 2QCMTu écris function Piece:arreter() self.active = false end. Quelle écriture est équivalente ?
  3. Question 3QCMDans le motif de classe, à quoi sert la ligne Classe.__index = Classe ?
  4. Question 4QCMOù ranger le ModuleScript Config de l’Obby des Nuages pour que serveur ET clients puissent le lire ?
  5. Question 5Trouve le bugCette classe compte des pièces. L’appel compteur:ajouter(5) provoque l’erreur « attempt to index nil with 'valeur' ». Où est le bug ?
    local Compteur = {}
    Compteur.__index = Compteur
    
    function Compteur.new()
    	local self = setmetatable({}, Compteur)
    	self.valeur = 0
    	return self
    end
    
    function Compteur.ajouter(montant)
    	self.valeur += montant
    end
    
    return Compteur
  6. Question 6Vrai ou fauxÉcrire --!strict en première ligne d’un script active une vérification des types plus poussée par l’analyseur de Studio, sans changer ce que fait le code.
  7. Question 7Trouve le bugTrouve la faille dans ce code généré par IA. Ce module fonctionne, mais une ligne met le jeu en danger. Laquelle ?
    -- ModuleScript « Config » dans ReplicatedStorage.Modules
    local Config = {}
    
    Config.VITESSE_ROTATION_PIECES = 90
    Config.VALEUR_PIECE = 1
    Config.HAUTEUR_REAPPARITION = Vector3.new(0, 4, 0)
    Config.CODE_ADMIN = "nuage-7731" -- code secret des commandes admin
    
    function Config.estAdmin(code)
    	return code == Config.CODE_ADMIN
    end
    
    return Config
  8. Question 8Améliore ce promptTu veux que Claude Code centralise tes réglages. Quelle version du prompt donnera le meilleur résultat ?
    Le prompt de départ
    range mes réglages dans un module

    Choisis la meilleure version de ce prompt.