Catégories
Jeu vidéo ROBLOX

Crée ton propre Quiz de calcul

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) :

  1. Un joueur touche une zone au sol (la Hitbox).
  2. Une question de calcul apparaît : un nombre à trouver, et 4 boutons de réponse possibles.
  3. Un seul bouton contient la bonne décomposition (par exemple si le nombre cible est 14, la bonne réponse pourrait être « 9 + 5 »).
  4. Si le joueur clique sur la bonne réponse : une nouvelle question apparaît, un peu plus difficile.
  5. Si le joueur clique sur une mauvaise réponse : il perd des points de vie.
  6. Quand le joueur sort de la zone, le quiz disparaît.
  7. 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ée HitBox, transparente, non collisionnable (CanCollide = false), qui représente la zone au sol

Dans StarterGui (l’interface qui sera copiée pour chaque joueur) :

  • Un ScreenGui
    • Un cadre nommé NumberGenerator contenant un ou plusieurs TextLabel (pour afficher le nombre à décomposer)
    • Un cadre nommé Answers contenant 4 TextButton (les boutons de réponse)

À toi de jouer !

  1. Dans l’Explorer, clique droit sur Workspace → Insert Object → Folder, renomme-le NumberGame.
  2. À l’intérieur, insère une Part, renomme-la HitBox, réduis son Transparency à 0.5 et désactive CanCollide.
  3. Dans StarterGui, insère un ScreenGui, puis à l’intérieur deux Frame nommés NumberGenerator et Answers.
  4. Dans NumberGenerator, ajoute un TextLabel. Dans Answers, ajoute 4 TextButton.

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 !

  1. Dans l’Explorer, clique droit sur ServerScriptService → Insert Object → Script.
  2. Renomme-le NumberGameScript.
  3. 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é !")
  1. Clique sur Play (▶) en haut de Roblox Studio.
  2. 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 !

  1. Ajoute volontairement une faute dans ton script (par exemple, renomme hitbox en hitboxx à un seul endroit) et lance Play.
  2. Lis le message d’erreur dans l’Output : à quelle ligne pointe-t-il ?
  3. Corrige l’erreur, puis ajoute un print() dans la fonction generateSimpleAddition pour afficher targetNumber, part1 et part2 à 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 !

  1. Ajoute la fonction randomPair dans ton script.
  2. Remplace l’appel math.random(2, maxValue) de l’étape 4.2 par randomPair(2, maxValue).
  3. Lance Play plusieurs fois et vérifie dans l’Output que le nombre affiché est toujours pair.
  4. 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 :

  1. 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.
  2. Ensuite les services Roblox utilisés (Players, etc.).
  3. Puis les fonctions utilitaires, chacune avec un rôle précis et un nom qui décrit ce qu’elle fait (calculateAnswer, randomPair, generateQuestion…).
  4. 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 !

  1. Utilise GetDescendants() et IsA("TextLabel") pour mettre à jour le texte de tous les TextLabel du dossier NumberGenerator en une seule boucle.
  2. Ajoute un 2ème TextLabel dans 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 !

  1. Remplace le contenu de ton NumberGameScript par ce script complet.
  2. Teste le jeu en solo avec Play, en te déplaçant vers la Hitbox.
  3. 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 TextLabel en 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) :