Introduction
Bienvenue dans ce tutoriel ! Tu vas apprendre à programmer en Lua avec Roblox Studio en construisant un vrai mini-jeu : le Quiz de calcul.
Le principe est simple : un joueur marche jusqu’à une zone(on appelle ça une Hitbox). Dès qu’il entre dans cette zone, un QCM de calcul apparaît à l’écran avec 4 boutons de réponse. S’il clique sur la bonne réponse, le jeu devient un peu plus difficile (des nombres plus grands, puis une nouvelle opération : addition, soustraction, multiplication, division). S’il se trompe, il perd de la vie.
C’est un jeu qui combine deux choses : de la logique de programmation (variables, boucles, fonctions, conditions, événements) et des maths (le calcul mental). Tu vas construire ce jeu étape par étape, en testant à chaque fois ce que tu écris.
Ce dont tu as besoin :
- Roblox Studio installé et ouvert
- Un nouveau projet vide (« Baseplate » par exemple)
- De la curiosité, et l’envie de te tromper pour apprendre !
Une variable, c’est comme une boîte étiquetée dans laquelle on range une information (un nombre, un texte…). Une fonction, c’est comme une recette de cuisine : une suite d’instructions qu’on peut réutiliser autant de fois qu’on veut. Garde ces images en tête, on va s’en servir tout le long du tutoriel.
Étape 1 : Comprendre le jeu avant de coder
But : avant d’écrire la moindre ligne de code, il faut savoir exactement ce que le jeu doit faire. C’est une étape que beaucoup de débutants sautent, et c’est une erreur : coder sans plan, c’est comme construire une maison sans plan de l’architecte.
Concepts clés : analyse d’un problème, cahier des charges.
Explications
Voici les règles de notre jeu, écrites en langage humain (pas encore en code) :
- Un joueur touche une zone au sol (la Hitbox).
- Une question de calcul apparaît : un nombre à trouver, et 4 boutons de réponse possibles.
- Un seul bouton contient la bonne décomposition (par exemple si le nombre cible est 14, la bonne réponse pourrait être « 9 + 5 »).
- Si le joueur clique sur la bonne réponse : une nouvelle question apparaît, un peu plus difficile.
- Si le joueur clique sur une mauvaise réponse : il perd des points de vie.
- Quand le joueur sort de la zone, le quiz disparaît.
- Au fil des bonnes réponses, le jeu change d’opération : d’abord des additions, puis des soustractions, puis des multiplications, puis des divisions.
À toi de jouer !
Avant de coder quoi que ce soit, prends une feuille et réponds à ces questions :
- Quelles informations le jeu doit-il « retenir » en permanence ? (Indice : la vie du joueur, la difficulté actuelle…)
- Quels sont les événements qui déclenchent une action ? (Indice : « toucher » une zone, « cliquer » sur un bouton…)
QCM
1. Pourquoi analyse-t-on le jeu avant de coder ?
- a) Parce que c’est obligatoire dans Roblox Studio
- b) Pour savoir précisément ce qu’on doit programmer et éviter de se perdre
- c) Pour que le code soit plus court
2. Dans notre jeu, quel événement déclenche l’apparition du quiz ?
- a) Le joueur appuie sur une touche du clavier
- b) Le joueur entre en contact avec la Hitbox
- c) Le joueur atteint un score de 10
Pour aller plus loin
Imagine une variante du jeu (un quiz de vocabulaire, un quiz de géographie…) et écris ses règles sur ta feuille, de la même façon que ci-dessus.
Étape 2 : Créer la structure des objets dans l’Explorer

But : construire dans Roblox Studio les objets (les « briques ») sur lesquels ton script va s’appuyer, avant même d’écrire du code.
Concepts clés : hiérarchie d’objets, Explorer, Workspace, ScreenGui.
Explications
Dans Roblox, tout ce qui existe dans le jeu (une partie de décor, une zone invisible, une interface) est un objet, rangé dans une arborescence visible dans la fenêtre Explorer. C’est comme les dossiers et sous-dossiers sur un ordinateur.
Pour notre jeu, on a besoin de :
Dans Workspace :
- Un dossier nommé
NumberGame- Une
Part(bloc) nomméeHitBox, transparente, non collisionnable (CanCollide = false), qui représente la zone au sol
- Une
Dans StarterGui (l’interface qui sera copiée pour chaque joueur) :
- Un
ScreenGui- Un cadre nommé
NumberGeneratorcontenant un ou plusieursTextLabel(pour afficher le nombre à décomposer) - Un cadre nommé
Answerscontenant 4TextButton(les boutons de réponse)
- Un cadre nommé
À toi de jouer !
- Dans l’Explorer, clique droit sur Workspace → Insert Object → Folder, renomme-le
NumberGame. - À l’intérieur, insère une Part, renomme-la
HitBox, réduis sonTransparencyà 0.5 et désactiveCanCollide. - Dans StarterGui, insère un ScreenGui, puis à l’intérieur deux Frame nommés
NumberGeneratoretAnswers. - Dans
NumberGenerator, ajoute unTextLabel. DansAnswers, ajoute 4TextButton.
Teste : tu dois voir dans l’Explorer une arborescence qui ressemble à un plan bien rangé, sans avoir encore écrit une ligne de code.
QCM
1. Où doit-on placer la Hitbox du jeu ?
- a) Dans StarterGui
- b) Dans Workspace
- c) Dans ServerScriptService
2. À quoi sert un ScreenGui ?
- a) À afficher des éléments d’interface à l’écran du joueur
- b) À déplacer le personnage
- c) À créer une zone de collision
Pour aller plus loin
Documentation Roblox sur la hiérarchie des objets
Étape 3 : Créer le premier script — LocalScript ou Script ?
But : comprendre la différence entre un script qui tourne côté serveur et un script qui tourne côté joueur, puis choisir le bon pour notre jeu.
Concepts clés : Script (serveur), LocalScript (client), sécurité, autorité serveur.
Explications
Dans Roblox, il existe deux grandes familles de scripts :
- Le Script s’exécute sur le serveur. Il ne tourne qu’une seule fois pour tout le monde, et voit tous les joueurs en même temps. C’est le serveur qui a toujours raison (« l’autorité »), un joueur ne peut pas le tricher.
- Le LocalScript s’exécute sur l’ordinateur du joueur (le « client »). Il est rapide pour l’affichage, mais un joueur malveillant peut le modifier ou le désactiver pour tricher.
Notre choix : un Script placé dans ServerScriptService.
Pourquoi ? Parce que notre jeu doit :
- Détecter tous les joueurs qui touchent la même Hitbox partagée dans le monde (pas propre à un seul joueur) ;
- Faire perdre de la vie de façon fiable : si c’était géré uniquement côté joueur, un tricheur pourrait modifier son propre script pour ne jamais perdre de vie ;
- Le serveur peut quand même modifier l’affichage (les
TextLabel,TextButton) dans l’interface d’un joueur précis : les changements de propriétés faits par le serveur sont automatiquement envoyés (« répliqués ») à l’écran du joueur.
Pour un jeu plus avancé, on séparerait souvent la logique (serveur) de l’affichage (client) avec des RemoteEvent, mais pour ce tutoriel, un seul Script serveur suffit et reste plus simple à comprendre.
À toi de jouer !
- Dans l’Explorer, clique droit sur ServerScriptService → Insert Object → Script.
- Renomme-le
NumberGameScript. - Ouvre-le et écris juste ceci pour vérifier que tout fonctionne :
-- Ce print doit apparaître dans la console (fenêtre Output) au lancement du jeu
print("Le script du Number Game a bien démarré !")
- Clique sur Play (▶) en haut de Roblox Studio.
- Ouvre la fenêtre Output (View → Output) : tu dois voir ton message s’afficher.
QCM
1. Où s’exécute un Script (et non un LocalScript) ?
- a) Sur l’ordinateur du joueur
- b) Sur le serveur
- c) Dans le navigateur internet
2. Pourquoi ne pas gérer la perte de vie uniquement avec un LocalScript ?
- a) Parce que les LocalScript n’existent pas dans Roblox
- b) Parce qu’un joueur pourrait modifier son propre script pour tricher
- c) Parce que c’est plus lent
Pour aller plus loin
Documentation Roblox — Script vs LocalScript
Étape 4 : Écrire le code, petit à petit
But : construire la logique du jeu en plusieurs petites briques, du plus simple au plus complexe, au lieu d’écrire tout le script d’un coup.
Concepts clés : variables, fonctions, conditions if/else, boucles for, événements.
On ne va pas écrire tout le script NumberGameScript d’un coup : ce serait trop pour bien comprendre. On va le construire en 4 sous-étapes.
4.1 — Les variables et les constantes
Explications : une variable stocke une information qui peut changer (par exemple la vie perdue à chaque erreur). Une constante est une variable qu’on ne modifiera jamais dans le code, mais qui est facile à régler en un seul endroit (par exemple le nombre de points de vie perdus).
-- ==================== CONSTANTES ====================
local HEALTH_LOSS = 10 -- Points de vie perdus pour une mauvaise réponse
local MIN_DIFFICULTY = 1 -- Difficulté minimale (nombre de chiffres)
local MAX_DIFFICULTY = 3 -- Difficulté maximale (nombre de chiffres)
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
-- ==================== RÉFÉRENCES WORKSPACE ====================
local numberGameFolder = workspace:WaitForChild("NumberGame")
local hitbox = numberGameFolder:WaitForChild("HitBox")
WaitForChild demande à Roblox d’attendre que l’objet existe avant de continuer : c’est important car le jeu peut mettre quelques instants à tout charger.
4.2 — Une fonction pour générer une question d’addition
Explications : une fonction permet de regrouper des instructions sous un nom, pour les réutiliser facilement. Ici, on écrit une version très simplifiée qui ne gère qu’une addition, avant d’ajouter la soustraction, la multiplication et la division plus tard.
local function generateSimpleAddition(maxValue)
-- On choisit un nombre cible au hasard
local targetNumber = math.random(2, maxValue)
-- On choisit un premier morceau, le deuxième complète jusqu'au nombre cible
local part1 = math.random(1, targetNumber - 1)
local part2 = targetNumber - part1
return targetNumber, part1, part2
end
-- Test rapide
local target, a, b = generateSimpleAddition(20)
print(target, "=", a, "+", b)
4.3 — L’événement Touched : détecter le joueur
Explications : un événement, c’est quelque chose qui se produit dans le jeu (un contact, un clic…) auquel on peut « réagir » avec une fonction. Touched se déclenche quand un objet touche la HitBox.
hitbox.Touched:Connect(function(hit)
local character = hit.Parent
if not character then return end -- si "hit" n'a pas de parent, on arrête
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return end -- ce n'est pas un personnage de joueur
local player = Players:GetPlayerFromCharacter(character)
if not player then return end
print(player.Name .. " a touché la Hitbox !")
end)
Remarque le if ... then return end : c’est une condition qui arrête la fonction tout de suite si quelque chose ne va pas. C’est une façon très courante d’écrire des vérifications de sécurité en Lua.
4.4 — Les 4 boutons de réponse
Explications : on utilise une boucle for avec ipairs pour parcourir chaque bouton de la liste, et on connecte un événement de clic (MouseButton1Click) sur chacun.
local function getPlayerGuiElements(player)
local playerGui = player:WaitForChild("PlayerGui")
local screenGui = playerGui:WaitForChild("ScreenGui")
local numberDisplay = screenGui:WaitForChild("NumberGenerator")
local answerButtons = screenGui:WaitForChild("Answers")
return numberDisplay, answerButtons
end
local numberDisplay, answerButtons = getPlayerGuiElements(somePlayer)
for index, part in ipairs(answerButtons:GetDescendants()) do
if part:IsA("TextButton") then
part.MouseButton1Click:Connect(function()
print("Bouton numéro " .. index .. " cliqué")
end)
end
end
À toi de jouer !
Recopie chaque sous-étape (4.1 à 4.4) l’une après l’autre dans ton NumberGameScript, en testant à chaque fois avec Play. Ne passe pas à la sous-étape suivante tant que la précédente n’affiche pas ce que tu attends dans l’Output.
QCM
1. Que fait WaitForChild ? a) Il supprime un objet b) Il attend que l’objet existe avant de continuer le script c) Il crée un nouvel objet
2. Que se passe-t-il quand hit.Parent n’a pas de Humanoid ? a) Le jeu plante b) La fonction s’arrête grâce au return, sans erreur c) Le joueur perd de la vie
Pour aller plus loin
Documentation Roblox — événement Touched
Documentation Roblox — boucles for et ipairs
Étape 5 : Débugger et tester avec print
But : apprendre à utiliser print() pour « voir » ce qui se passe à l’intérieur du script, et corriger les erreurs efficacement.
Concepts clés : débogage, console Output, lecture des messages d’erreur.
Explications
Un script ne fait jamais ce qu’on veut qu’il fasse — il fait ce qu’on lui a écrit. Quand le résultat n’est pas celui attendu, on utilise print() pour afficher la valeur des variables à des endroits stratégiques et comprendre où ça coince.
local function generateSimpleAddition(maxValue)
local targetNumber = math.random(2, maxValue)
local part1 = math.random(1, targetNumber - 1)
local part2 = targetNumber - part1
-- Ligne de débogage : on vérifie que le calcul est correct
print("DEBUG - cible:", targetNumber, "part1:", part1, "part2:", part2)
return targetNumber, part1, part2
end
Quand une erreur rouge apparaît dans l’Output, elle indique en général le numéro de ligne et le type d’erreur (par exemple attempt to index nil, ce qui veut souvent dire qu’un objet n’a pas été trouvé avec WaitForChild). Lis toujours ce message avant de chercher au hasard.
À toi de jouer !
- Ajoute volontairement une faute dans ton script (par exemple, renomme
hitboxenhitboxxà un seul endroit) et lance Play. - Lis le message d’erreur dans l’Output : à quelle ligne pointe-t-il ?
- Corrige l’erreur, puis ajoute un
print()dans la fonctiongenerateSimpleAdditionpour affichertargetNumber,part1etpart2à chaque appel.
QCM
1. À quoi sert la fenêtre Output ? a) À afficher les décors du jeu b) À afficher les messages print() et les erreurs du script c) À modifier les objets de l’Explorer
2. Quand on voit une erreur attempt to index nil, cela signifie souvent que… a) Le jeu a trop de joueurs b) Un objet attendu (avec WaitForChild) n’a pas été trouvé c) Une variable contient trop de texte
Pour aller plus loin
Une fois le jeu terminé, tu pourras retirer ou commenter (mettre -- devant) les lignes print() de débogage pour « nettoyer » la console, sans les supprimer définitivement — elles peuvent resservir plus tard.
Étape 6 : Donner de l’imprévisibilité au jeu
But : comprendre math.random et pourquoi on doit parfois l’ajuster pour obtenir exactement ce qu’on veut (ici, uniquement des nombres pairs).
Concepts clés : génération de nombres aléatoires, math.random, math.ceil, math.floor.
Explications
math.random(min, max) renvoie un nombre entier au hasard entre min et max inclus. C’est ce qui rend chaque partie différente de la précédente.
Dans notre jeu, on veut que le nombre cible soit toujours pair, pour que les décompositions restent simples à calculer mentalement. Il ne suffit pas d’appeler math.random normalement, car il renverrait aussi des nombres impairs.
local function randomPair(min, max)
-- On se place dans "l'espace des nombres pairs" en divisant par 2
min = math.ceil(min / 2) -- arrondi vers le haut pour ne jamais descendre sous min
max = math.floor(max / 2) -- arrondi vers le bas pour ne jamais dépasser max
return math.random(min, max) * 2
end
print(randomPair(1, 20)) -- toujours un nombre pair entre 2 et 20
math.ceil arrondit toujours vers le haut, math.floor toujours vers le bas. On les utilise ici pour être certain que le résultat final reste bien compris entre min et max, même si ceux-ci sont impairs.
À toi de jouer !
- Ajoute la fonction
randomPairdans ton script. - Remplace l’appel
math.random(2, maxValue)de l’étape 4.2 parrandomPair(2, maxValue). - Lance Play plusieurs fois et vérifie dans l’Output que le nombre affiché est toujours pair.
- Essaie d’appeler
randomPair(1, 9): quels nombres peux-tu obtenir ?
QCM
1. Que renvoie math.random(1, 6) ?
- a) Toujours 6
- b) Un nombre entier au hasard entre 1 et 6
- c) Un nombre décimal entre 0 et 1
2. Pourquoi utilise-t-on math.ceil pour la borne minimale dans randomPair ?
- a) Pour ne jamais descendre en dessous de la borne demandée
- b) Parce que c’est plus rapide à calculer
- c) Pour obtenir un nombre impair
Pour aller plus loin
Documentation Roblox — math.random
Étape 7 : Organiser son code — constantes et fonctions
But : apprendre à ranger son script pour qu’il reste lisible et facile à modifier, même quand il grandit.
Concepts clés : organisation du code, constantes en majuscules, fonctions séparées par responsabilité.
Explications
Un script bien organisé se lit « de haut en bas » comme une histoire claire :
- D’abord les constantes (tout en MAJUSCULES par convention), regroupées en haut, pour pouvoir régler le jeu en un coup d’œil sans chercher dans tout le code.
- Ensuite les services Roblox utilisés (
Players, etc.). - Puis les fonctions utilitaires, chacune avec un rôle précis et un nom qui décrit ce qu’elle fait (
calculateAnswer,randomPair,generateQuestion…). - Enfin les événements qui utilisent toutes ces fonctions.
-- ==================== CONSTANTES ====================
local HEALTH_LOSS = 10
local MIN_DIFFICULTY = 1
local MAX_DIFFICULTY = 3
local OPERATORS = {"+", "-", "*", "/"}
-- ==================== FONCTIONS UTILITAIRES ====================
-- Vérifie si une réponse (part1 OPERATOR part2) donne bien le résultat attendu
local function calculateAnswer(operator, part1, part2, result)
if operator == "+" then
return (part1 + part2) == result
elseif operator == "-" then
return (part1 - part2) == result
elseif operator == "*" then
return (part1 * part2) == result
elseif operator == "/" then
return (part1 / part2) == result
end
end
Séparer le code en petites fonctions, chacune avec un seul but, rend chaque morceau plus facile à tester, à comprendre, et à corriger si une erreur apparaît.
À toi de jouer !
Range ton script actuel dans cet ordre : constantes, services, références workspace, fonctions utilitaires, puis événements. Ajoute des lignes de commentaires -- ==================== comme des titres de section pour t’y retrouver facilement.
QCM
1. Pourquoi écrit-on les constantes en MAJUSCULES ?
- a) C’est obligatoire en Lua, sinon le script plante
- b) C’est une convention qui permet de les repérer facilement dans le code
- c) Ça rend le script plus rapide
2. Quel est l’avantage de séparer le code en plusieurs petites fonctions ?
- a) Le jeu se lance plus vite
- b) Chaque fonction est plus facile à comprendre, tester et corriger
- c) Cela évite d’avoir à utiliser des variables
Pour aller plus loin
Documentation Roblox — bonnes pratiques de style de code Luau
Étape 8 : Optimiser son code — lire tous les objets d’un dossier
But : éviter de répéter le même code pour chaque TextLabel ou TextButton, en parcourant automatiquement tous les objets d’un dossier.
Concepts clés : GetDescendants, boucle for ... in, IsA.
Explications
Plutôt que d’écrire une ligne de code différente pour chaque bouton (Answers.Button1, Answers.Button2…), on demande à Roblox la liste de tous les objets contenus dans un dossier avec GetDescendants(), puis on filtre ceux qui nous intéressent avec IsA("TextButton").
for index, part in ipairs(answerButtons:GetDescendants()) do
if part:IsA("TextButton") then
part.Text = "Réponse " .. index
end
end
L’avantage : si tu ajoutes un 5ème bouton de réponse plus tard dans l’Explorer, ce code fonctionnera sans aucune modification. C’est ce qu’on appelle écrire du code « générique », qui s’adapte automatiquement au contenu du jeu.
À toi de jouer !
- Utilise
GetDescendants()etIsA("TextLabel")pour mettre à jour le texte de tous lesTextLabeldu dossierNumberGeneratoren une seule boucle. - Ajoute un 2ème
TextLabeldans l’Explorer sans toucher au script : vérifie qu’il se met bien à jour lui aussi automatiquement.
QCM
1. Que fait GetDescendants() ?
- a) Il renvoie la liste de tous les objets contenus dans un objet (et ses sous-dossiers)
- b) Il supprime tous les objets d’un dossier
- c) Il crée un nouveau dossier
2. À quoi sert IsA("TextButton") ?
- a) À vérifier le nom exact de l’objet
- b) À vérifier le type de l’objet, pour ne garder que les boutons par exemple
- c) À changer la couleur du bouton
Pour aller plus loin
Documentation Roblox — GetDescendants
Étape 9 : Construire l’architecture du jeu — assembler le script complet
But : relier toutes les briques construites dans les étapes précédentes pour obtenir le script final et fonctionnel.
Concepts clés : assemblage, événements Touched/TouchEnded, gestion multi-joueurs, nettoyage des connexions.
Explications
Un jeu multijoueur doit gérer plusieurs joueurs en même temps, sans qu’ils se marchent dessus. On utilise pour cela des tables qui associent chaque joueur à son propre état :
local playersInZone = {} -- [Player] = true tant qu'il est dans la zone
local playerConnections = {} -- [Player] = liste des connexions de boutons à déconnecter
On doit aussi penser à « nettoyer » les connexions d’événements quand un joueur sort de la zone ou quitte le jeu, sinon elles restent actives en mémoire inutilement (une fuite de mémoire) et peuvent déclencher plusieurs fois le même clic.
Voici le script complet, qui assemble toutes les briques précédentes :
--[[
Script : Number Game - Quiz de calcul (addition)
Description : Quand un joueur entre dans la Hitbox, un QCM de calcul s'affiche.
Une bonne réponse augmente la difficulté, une mauvaise réponse fait perdre de la vie.
]]
-- ==================== CONSTANTES ====================
local HEALTH_LOSS = 10
local MIN_DIFFICULTY = 1
local MAX_DIFFICULTY = 3
local ENTER_DELAY = 1
local EXIT_DELAY = 1
local OPERATORS = {"+", "-", "*", "/"}
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
-- ==================== RÉFÉRENCES WORKSPACE ====================
local numberGameFolder = workspace:WaitForChild("NumberGame")
local hitbox = numberGameFolder:WaitForChild("HitBox")
-- ==================== ÉTAT DU JEU ====================
local playersInZone = {}
local playerConnections = {}
-- ==================== FONCTIONS UTILITAIRES ====================
local function calculateAnswer(operator, part1, part2, result)
if operator == "+" then
return (part1 + part2) == result
elseif operator == "-" then
return (part1 - part2) == result
elseif operator == "*" then
return (part1 * part2) == result
elseif operator == "/" then
return (part1 / part2) == result
end
end
local function randomPair(min, max)
min = math.ceil(min / 2)
max = math.floor(max / 2)
return math.random(min, max) * 2
end
local function randomIncorrectNumber(maxValue, targetNumber, operator)
local wrong1 = randomPair(1, maxValue // 2)
local wrong2 = randomPair(1, maxValue // 2)
if operator == "*" or operator == "/" then
wrong2 = randomPair(1, 10)
end
while calculateAnswer(operator, wrong1, wrong2, targetNumber) do
wrong2 = randomPair(1, maxValue // 2)
end
return wrong1, wrong2
end
local function generateQuestion(numberDisplay, answerButtons, difficulty, operator)
if not numberDisplay or not answerButtons then return nil end
operator = operator or "+"
numberDisplay.Visible = true
answerButtons.Visible = true
difficulty = math.clamp(difficulty, MIN_DIFFICULTY, MAX_DIFFICULTY)
local maxValue = (10 ^ difficulty) - 1
local targetNumber = randomPair(1, maxValue)
local part1, part2 = 0, 0
if operator == "+" then
part1 = math.random(1, targetNumber - 1)
part2 = targetNumber - part1
elseif operator == "-" then
part2 = math.random(1, targetNumber // 2)
part1 = targetNumber + part2
elseif operator == "*" then
part2 = randomPair(1, 10)
part1 = randomPair(1, maxValue)
targetNumber = part1 * part2
elseif operator == "/" then
part2 = randomPair(1, 10)
part1 = targetNumber * part2
end
for _, part in ipairs(numberDisplay:GetDescendants()) do
if part:IsA("TextLabel") then
part.Text = string.format("%0" .. difficulty .. "d", targetNumber)
end
end
local correctAnswerText = string.format("%0" .. difficulty .. "d", part1)
.. " " .. operator .. " " .. string.format("%0" .. difficulty .. "d", part2)
local correctIndex = math.random(1, 4)
for index, part in ipairs(answerButtons:GetDescendants()) do
if part:IsA("TextButton") then
if index == correctIndex then
part.Text = correctAnswerText
else
local wrong1, wrong2 = randomIncorrectNumber(maxValue, targetNumber, operator)
part.Text = string.format("%0" .. difficulty .. "d", wrong1) .. " "
.. operator .. " " .. string.format("%0" .. difficulty .. "d", wrong2)
end
end
end
return correctIndex
end
local function getPlayerGuiElements(player)
local playerGui = player:WaitForChild("PlayerGui")
local screenGui = playerGui:WaitForChild("ScreenGui")
local numberDisplay = screenGui:WaitForChild("NumberGenerator")
local answerButtons = screenGui:WaitForChild("Answers")
return numberDisplay, answerButtons
end
local function hideQuiz(numberDisplay, answerButtons)
if numberDisplay then numberDisplay.Visible = false end
if answerButtons then answerButtons.Visible = false end
end
local function disconnectPlayerButtons(player)
local connections = playerConnections[player]
if not connections then return end
for _, connection in ipairs(connections) do
connection:Disconnect()
end
playerConnections[player] = nil
end
local function getNextOperator(currentIndex)
currentIndex += 1
if currentIndex > #OPERATORS then
currentIndex = #OPERATORS
end
return OPERATORS[currentIndex], currentIndex
end
local function startQuizForPlayer(player, humanoid)
local numberDisplay, answerButtons = getPlayerGuiElements(player)
local currentDifficulty = MIN_DIFFICULTY
local currentOperator, currentIndexOperator = getNextOperator(0)
local currentAnswerIndex = generateQuestion(numberDisplay, answerButtons, currentDifficulty, currentOperator)
disconnectPlayerButtons(player)
local connections = {}
for index, part in ipairs(answerButtons:GetDescendants()) do
if part:IsA("TextButton") then
local connection = part.MouseButton1Click:Connect(function()
if humanoid.Health <= 0 then return end
if index ~= currentAnswerIndex then
humanoid.Health = math.max(humanoid.Health - HEALTH_LOSS, 0)
else
if currentDifficulty == MAX_DIFFICULTY then
currentOperator, currentIndexOperator = getNextOperator(currentIndexOperator)
currentDifficulty = MIN_DIFFICULTY
end
currentAnswerIndex = generateQuestion(numberDisplay, answerButtons, currentDifficulty, currentOperator)
currentDifficulty = math.min(currentDifficulty + 1, MAX_DIFFICULTY)
end
end)
table.insert(connections, connection)
end
end
playerConnections[player] = connections
end
-- ==================== ÉVÉNEMENTS DE COLLISION ====================
hitbox.Touched:Connect(function(hit)
local character = hit.Parent
if not character then return end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return end
local player = Players:GetPlayerFromCharacter(character)
if not player then return end
if playersInZone[player] then return end
playersInZone[player] = true
task.wait(ENTER_DELAY)
if not playersInZone[player] or humanoid.Health <= 0 then return end
startQuizForPlayer(player, humanoid)
end)
hitbox.TouchEnded:Connect(function(hit)
local character = hit.Parent
if not character then return end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return end
local player = Players:GetPlayerFromCharacter(character)
if not player then return end
task.wait(EXIT_DELAY)
local stillTouching = false
for _, part in pairs(hitbox:GetTouchingParts()) do
if part.Parent == character then
stillTouching = true
break
end
end
if stillTouching or not playersInZone[player] then return end
playersInZone[player] = nil
disconnectPlayerButtons(player)
local numberDisplay, answerButtons = getPlayerGuiElements(player)
hideQuiz(numberDisplay, answerButtons)
end)
-- ==================== GESTION DES JOUEURS ====================
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function()
local numberDisplay, answerButtons = getPlayerGuiElements(player)
hideQuiz(numberDisplay, answerButtons)
end)
end)
Players.PlayerRemoving:Connect(function(player)
playersInZone[player] = nil
disconnectPlayerButtons(player)
end)
À toi de jouer !
- Remplace le contenu de ton
NumberGameScriptpar ce script complet. - Teste le jeu en solo avec Play, en te déplaçant vers la Hitbox.
- Teste ensuite avec Play plusieurs joueurs (Test → Clients : 2) pour vérifier que deux joueurs peuvent jouer en même temps sans se gêner.
QCM
1. Pourquoi range-t-on l’état de chaque joueur dans des tables comme playersInZone, plutôt que dans une seule variable ?
- a) Pour que le jeu soit plus joli
- b) Pour gérer plusieurs joueurs en même temps, chacun avec son propre état
- c) Ce n’est pas nécessaire, une seule variable suffirait
2. Pourquoi appelle-t-on disconnectPlayerButtons avant de recréer les connexions des boutons ?
- a) Pour éviter que les anciens clics déclenchent encore une action (fuite mémoire, doublons)
- b) Pour changer la couleur des boutons
- c) Ce n’est pas utile, on peut s’en passer
Pour aller plus loin
Documentation Roblox — RBXScript Connection et Disconnect
Étape 10 : Pour aller plus loin — améliore ton jeu
But : utiliser ce que tu as appris pour transformer ce jeu en quelque chose qui te ressemble.
Concepts clés : réutilisation, créativité, itération.
Explications
Tu maîtrises maintenant les briques essentielles de la programmation en Lua : variables, constantes, fonctions, conditions, boucles, événements, et un peu de hasard maîtrisé. Un vrai développeur de jeu ne s’arrête jamais à la première version : il l’améliore petit à petit, en testant à chaque changement.
À toi de jouer !
Voici quelques idées de règles ou d’éléments à ajouter, du plus simple au plus ambitieux :
- Ajoute un chronomètre : le joueur doit répondre en moins de 10 secondes, sinon il perd de la vie automatiquement.
- Ajoute un score : chaque bonne réponse rapporte des points, affichés dans un
TextLabelen haut de l’écran. - Ajoute un message de victoire quand la difficulté maximale et le dernier opérateur (division) sont atteints.
- Ajoute des obstacles dans le décor autour de la Hitbox pour rendre le déplacement plus difficile.
- Ajoute un son différent pour une bonne réponse et pour une mauvaise réponse.
Conclusion
Mets-toi maintenant à la place d’un joueur qui découvre ton jeu pour la première fois : est-ce clair ? Est-ce amusant ? Puis imagine un joueur devenu expert après plusieurs parties : le jeu reste-t-il intéressant, ou devient-il trop facile ?
Une fois satisfait de ton jeu, partage-le avec tes camarades de classe et demande-leur ce qu’ils en pensent. Leurs retours sont précieux : ce sont eux qui te diront si une règle est trop dure, trop facile, ou pas claire — exactement comme le font les vrais studios de jeux vidéo avec leurs joueurs.
Pour aller plus loin (documentation)
Documentation Roblox — RemoteEvent (pour séparer proprement client et serveur plus tard) :




























