L’UNIHIKER K10 (ci-après dénommée K10) est une carte de développement et d’apprentissage conçue pour une prise en main rapide de l’intelligence artificielle. Elle répond aux besoins d’apprentissage en programmation, IoT et IA. La carte intègre un écran LCD couleur, le Wi-Fi, le Bluetooth, une caméra, un microphone, un haut-parleur, un indicateur RGB, de multiples capteurs et de nombreuses interfaces d’extension. Grâce à ses ressources embarquées hautement intégrées, le contrôle des capteurs, les applications IoT et les projets d’intelligence artificielle tels que la reconnaissance faciale, la reconnaissance vocale et la synthèse vocale peuvent être facilement mis en œuvre sans avoir besoin de connecter d’appareils supplémentaires pendant l’apprentissage.
Fonctionnalité
Caméra intégrée et algorithmes intégrés pour la détection d’images hors ligne
Microphone intégré et algorithme intégré pour la reconnaissance vocale hors ligne
Haut-parleur intégré et algorithme intégré pour la synthèse vocale hors ligne
Écran couleur de 2,8 pouces pour une présentation des données plus claire
Interface riche, compatible avec de nombreux logiciels de programmation, bonne évolutivité
Couleur d’arrière-plan de l’affichage
Définition de la couleur de fond de l’écran K10
Connecte ton UNIHIKER K10 :
Demande le téléchargement de ton programme :
Lorsque le téléchargement est effectué, teste ton programme.
Puis réalise un test
Album photo électronique
Formatez la carte TF en FAT32 et insérez-la dans le K10. Appuyez sur le bouton A du K10 pour prendre une photo et sur le bouton B pour l’afficher.
Puis réalise un test
Afficher du texte sur l’image
Lors de l’affichage de texte par-dessus une image, désactivez l’effacement automatique et utilisez une actualisation constante de l’image pour écraser l’ancien contenu.
Tu vas apprendre à récupérer un objet et à utiliser ses propriétés.
Concepts clés
Variable
WaitForChild()
Explications
Une variable est comme une boîte portant une étiquette.
Nous allons créer une boîte appelée :
folderTest
et y ranger notre dossier Roblox.
Code Lua
Crée un Script dans ServerScriptService.
-- Récupère le dossier FolderKilyan dans Workspace
local folderTest = workspace:WaitForChild("FolderTest")
-- Affiche le nom et le type de l'objet
print("Folder test :", folderTest.Name, folderTest.ClassName)
Que fait le programme ?
Cette ligne :
local folderTest = workspace:WaitForChild("FolderTest")
signifie :
« Cherche FolderTest dans Workspace et place-le dans la variable folderTest. »
Puis :
folderTest.Name
récupère son nom.
Et :
folderTest.ClassName
récupère son type.
À toi de jouer !
Lance le jeu.
Regarde la fenêtre Output (Sortie)
Tu devrais voir quelque chose comme :
Folder test : FolderTest Folder
Modification
Remplace :
print("Folder test :", folderTest.Name, folderTest.ClassName)
par :
print("Mon dossier s'appelle :", folderTest.Name)
Observe le résultat.
Questions
1. Que contient la variable folderTest ?
a) Une Part
b) Un folder nommé FolderTest
c) Le joueur
2. À quoi sert local ?
a) À créer une variable b) À créer une Part c) À détecter un clic
Jusqu’à maintenant, nous savons récupérer un dossier (Folder).
Maintenant, nous voulons parcourir tous les objets qu’il contient.
Concepts clés
Boucle for
GetChildren()
Explications
Imagine que le programme ouvre la boîte FolderTest et regarde chaque objet l’un après l’autre.
FolderTest
↓
Part01
↓
Part02
↓
Part03
C’est exactement le rôle de la boucle for.
Code Lua
local folderTest = workspace:WaitForChild("FolderTest")
-- Parcourt tous les objets directement contenus dans le Folder
for i, part in ipairs(folderTest:GetChildren()) do
-- Affiche le nom et le type de chaque objet
print("Part :", part.Name, part.ClassName)
end
Explication
Cette partie :
folderTest:GetChildren()
demande :
« Donne-moi tous les enfants de FolderTest. »
La boucle :
for i, part in ipairs(...)
prend ensuite chaque objet un par un.
part représente l’objet actuellement étudié.
À toi de jouer !
Lance le jeu.
Tu dois obtenir :
Part : Part01 Part
Part : Part02 Part
Part : Part03 Part
Expérience
Ajoute une quatrième Part :
Part04
Sans modifier le script.
Relance le jeu.
Que se passe-t-il ?
Le programme doit automatiquement détecter Part04.
C’est la puissance des boucles !
Questions
1. Combien de fois la boucle s’exécute-t-elle avec 3 Parts ?
a) 1
b) 2
c) 3
2. Que contient la variable part pendant la boucle ?
Tous les objets d’un Folder ne sont pas forcément des Parts.
Nous allons apprendre à vérifier le type d’un objet avant de le modifier.
Concept clé
Condition if
Code Lua
local folderTest = workspace:WaitForChild("FolderTest")
for _, part in ipairs(folderTest:GetChildren()) do
-- Vérifie si l'objet est bien une pièce physique
if part:IsA("BasePart") then
print(part.Name, part.ClassName)
-- Change la couleur de la Part en rouge
part.Color = Color3.fromRGB(255, 0, 0)
end
end
Explications
La condition :
if part:IsA("BasePart") then
signifie :
« Est-ce que cet objet est une Part physique ? »
Si la réponse est oui, le programme exécute le code entre :
then
et
end
À toi de jouer !
Ajoute un objet différent dans le Folder, par exemple un Model.
Lance le jeu.
Le programme ne devrait pas essayer de lui appliquer une couleur.
Étape 6 : Donner une couleur différente à chaque Part
But
Tu vas maintenant utiliser plusieurs conditions.
Concepts clés
if
elseif
else
Code Lua
local folderTest = workspace:WaitForChild("FolderTest")
for _, part in ipairs(folderTest:GetChildren()) do
-- Vérifie que l'objet est bien une Part
if part:IsA("BasePart") then
-- Part01 devient rouge
if part.Name == "Part01" then
part.Color = Color3.fromRGB(255, 0, 0)
-- Part02 devient verte
elseif part.Name == "Part02" then
part.Color = Color3.fromRGB(0, 255, 0)
-- Toutes les autres Parts deviennent bleues
else
part.Color = Color3.fromRGB(0, 0, 255)
end
end
end
Explications
Le programme pose plusieurs questions :
Est-ce Part01 ?
↓
oui → rouge
non
↓
Est-ce Part02 ?
↓
oui → vert
non
↓
bleu
Le else signifie :
« Si aucune des conditions précédentes n’est vraie. »
À toi de jouer !
Lance le jeu.
Tu dois obtenir :
Part01 → ROUGE
Part02 → VERT
Part03 → BLEU
Expérience
Ajoute :
Part04
Part05
Elles doivent automatiquement devenir bleues.
Puis crée une condition pour Part04.
Quelle couleur veux-tu lui donner ?
Questions
Si part.Name vaut "Part01", quelle condition est vraie ?
Jusqu’à maintenant, le programme s’exécute automatiquement.
Nous allons maintenant attendre une action du joueur.
Concepts clés
Événement
Fonction
Explications
Un événement signifie :
« Quelque chose vient de se produire. »
Par exemple :
Le joueur clique
↓
événement MouseClick
↓
le programme réagit
Nous allons utiliser le ClickDetector.
Rajoute un ClickDetector à chaque part
Code Lua
local folderTest = workspace:WaitForChild("FolderKTest")
for _, part in ipairs(folderTest:GetChildren()) do
if part:IsA("BasePart") then
-- Recherche le ClickDetector placé dans la Part
local clickDetector = part:FindFirstChildOfClass("ClickDetector")
if clickDetector then
-- Attend que le joueur clique sur la Part
clickDetector.MouseClick:Connect(function(player)
-- Affiche le nom du joueur et de la Part
print(player.Name .. " a cliqué sur " .. part.Name)
end)
end
end
end
Explication de function
Cette partie :
function(player)
crée une petite fonction.
Elle sera exécutée plus tard, lorsque le joueur cliquera.
C’est comme dire :
« Quand le clic arrivera, fais ce qui se trouve dans cette fonction. »
À toi de jouer !
Lance le jeu.
Clique sur les Parts.
Dans Output, tu dois voir :
Patrick a cliqué sur Part01
Patrick a cliqué sur Part02
Tu vas maintenant faire agir la Part lorsqu’elle est cliquée.
C’est ici que nous faisons le lien avec les mathématiques.
Concepts clés
Vector3
Vitesse
Explications
Un Vector3 contient trois valeurs :
X
Y
Z
Par exemple :
Vector3.new(50, 0, 0)
signifie :
X = 50
Y = 0
Z = 0
On demande donc à la Part de se déplacer selon l’axe X.
Code Lua
local folderTest = workspace:WaitForChild("FolderTest")
for _, part in ipairs(folderTest:GetChildren()) do
if part:IsA("BasePart") then
-- Recherche le ClickDetector
local clickDetector = part:FindFirstChildOfClass("ClickDetector")
if clickDetector then
-- Action réalisée lorsque le joueur clique
clickDetector.MouseClick:Connect(function(player)
print(player.Name .. " a cliqué sur " .. part.Name)
-- Autorise la Part à être déplacée par la physique
part.Anchored = false
-- Crée un vecteur de vitesse
local velocityVector = Vector3.new(50, 0, 0)
-- Applique le vecteur à la Part
part.AssemblyLinearVelocity = velocityVector
end)
end
end
end
Que représente le vecteur ?
Vector3.new(50, 0, 0)
On peut le représenter mathématiquement :
La Part part dans la direction positive de X.
À toi de jouer !
Clique sur une Part.
Elle doit partir vers la droite.
Expériences
Essaie successivement :
Vector3.new(-50, 0, 0)
Puis :
Vector3.new(0, 50, 0)
Puis :
Vector3.new(0, 0, 50)
Observe les directions.
Puis essaie :
Vector3.new(30, 0, 40)
Que se passe-t-il ?
Tu viens de créer un déplacement oblique.
Question
Que signifie :
Vector3.new(0, 50, 0)
a) Aller vers X
b) Aller vers Y
c) Aller vers Z
Pour aller plus loin
Demande-toi :
Pourquoi Vector3 est-il intéressant pour représenter un vecteur mathématique ?
Tu peux maintenant commencer à introduire :
Vecteur mathématique
↓
Direction
Sens
Longueur
↓
Vector3 Roblox
(X, Y, Z)
Jusqu’à maintenant, la Part était lancée avec une vitesse.
Maintenant, nous allons créer une animation contrôlée.
Concepts clés
Service
Tween
Explications
TweenService permet de modifier progressivement une propriété.
Par exemple :
Position initiale
↓
↓
↓
Position finale
Au lieu de déplacer brutalement la Part, Roblox crée une animation.
Code Lua
local folderTest = workspace:WaitForChild("FolderKilyan")
-- Récupère le service qui permet de créer des animations
local tweenService = game:GetService("TweenService")
-- Définit la durée et le type de l'animation
local tweenInfo = TweenInfo.new(
1,
Enum.EasingStyle.Linear,
Enum.EasingDirection.Out,
0,
false,
0
)
for _, part in ipairs(folderTest:GetChildren()) do
if part:IsA("BasePart") then
-- Recherche le ClickDetector
local clickDetector = part:FindFirstChildOfClass("ClickDetector")
if clickDetector then
clickDetector.MouseClick:Connect(function(player)
print(player.Name .. " a cliqué sur " .. part.Name)
-- Vecteur de déplacement
local moveVector = CFrame.new(0, 10, 0)
-- Création de l'animation
local tween = tweenService:Create(
part,
tweenInfo,
{
CFrame = part.CFrame * moveVector
}
)
-- Lance l'animation
tween:Play()
end)
end
end
end
Que signifie le déplacement ?
CFrame.new(0, 10, 0)
correspond à :
X = 0
Y = 10
Z = 0
La Part monte donc de 10 studs.
À toi de jouer !
Clique sur une Part.
Elle doit monter progressivement.
Modification 1
Essaie :
CFrame.new(10, 0, 0)
La Part se déplace horizontalement.
Modification 2
Essaie :
CFrame.new(10, 10, 0)
La Part se déplace à la fois :
sur X ;
sur Y.
Tu viens de construire un déplacement composé de deux directions.
Question
Quelle instruction démarre réellement l’animation ?
Nous avons maintenant suffisamment de programmation pour créer une vraie règle.
But
Tu vas combiner plusieurs concepts.
Règle proposée
Chaque couleur correspond à une action :
ROUGE → se déplace à droite
VERT → monte
BLEU → se déplace en diagonale
Code
local folderTest = workspace:WaitForChild("FolderKilyan")
for _, part in ipairs(folderTest:GetChildren()) do
if part:IsA("BasePart") then
-- Détermine la couleur de la Part
if part.Name == "Part01" then
part.Color = Color3.fromRGB(255, 0, 0)
elseif part.Name == "Part02" then
part.Color = Color3.fromRGB(0, 255, 0)
else
part.Color = Color3.fromRGB(0, 0, 255)
end
-- Recherche le ClickDetector
local clickDetector = part:FindFirstChildOfClass("ClickDetector")
if clickDetector then
clickDetector.MouseClick:Connect(function(player)
print(player.Name .. " a cliqué sur " .. part.Name)
-- Part rouge
if part.Name == "Part01" then
part.Anchored = false
part.AssemblyLinearVelocity = Vector3.new(50, 0, 0)
-- Part verte
elseif part.Name == "Part02" then
part.Anchored = false
part.AssemblyLinearVelocity = Vector3.new(0, 50, 0)
-- Part bleue
else
part.Anchored = false
part.AssemblyLinearVelocity = Vector3.new(30, 0, 40)
end
end)
end
end
end
Le jeu possède maintenant une règle
JOUEUR
|
clique
↓
Quelle Part ?
/ | \
ROUGE VERT BLEU
↓ ↓ ↓
→ X ↑ Y ↗ X+Z
Étape 11 : Défi mathématique — prédire le déplacement
Maintenant, c’est à ton tour de réfléchir avant d’exécuter le programme.
Le code indique :
local moveVector = Vector3.new(20, 0, 30)
Demande-toi :
Dans quelle direction la Part va-t-elle partir ?
Dessine le vecteur sur une feuille.
Puis teste dans Roblox.
Défi 1
Quel vecteur faut-il utiliser pour aller :
10 à droite et 20 vers l’avant ?
Réponse :
Vector3.new(10, 0, 20)
Défi 2
Quel vecteur faut-il utiliser pour aller :
20 à gauche ?
Vector3.new(-20, 0, 0)
Défi 3
Quel vecteur permet de monter de 15 studs ?
Vector3.new(0, 15, 0)
Défi 4
Construis un déplacement :
X = -20
Y = 10
Z = 30
Solution :
Vector3.new(-20, 10, 30)
Tu viens de manipuler un vecteur dans l’espace.
Ce que tu as appris
À la fin du tutoriel, tu as utilisé de nombreuses notions fondamentales :
Notion
Exemple dans le jeu
Variable
folderTest
Objet
part
Propriété
part.Color
Condition
if part.Name == ...
else
autre Part
Boucle
for _, part
Fonction
function(player)
Événement
MouseClick
Service
GetService()
Vector3
Vector3.new(50,0,0)
Physique
AssemblyLinearVelocity
Animation
TweenService
Organisation
Folder
Et surtout, tu as découvert une idée importante :
PROGRAMME
↓
OBSERVER
↓
DÉCIDER
↓
AGIR
C’est cette structure qui permet de transformer progressivement quelques lignes de Lua en un véritable jeu.
Pour aller beaucoup plus loin
Maintenant, mets-toi à la place du joueur.
Au début, tu ne connais pas le jeu :
« Je clique sur une Part. Que va-t-il se passer ? »
Puis tu deviens joueur expérimenté :
« La Part rouge part dans la direction X. »
Puis programmeur :
« Si je remplace Vector3.new(50,0,0) par Vector3.new(20,0,40), je change la direction du déplacement. »
Puis concepteur de jeu :
« Je vais créer une zone d’arrivée et donner des points lorsque la Part atteint sa cible. »
Tu peux alors faire évoluer le projet avec :
un score ;
des vies ;
plusieurs niveaux ;
des obstacles ;
des zones interdites ;
des portes ;
des téléporteurs ;
des vecteurs à calculer ;
des angles à déterminer ;
un véritable défi de géométrie dans l’espace.
Cela permettrait de faire évoluer le même projet de la sixième à la troisième, en introduisant progressivement programmation, coordonnées, déplacements, vecteurs, angles, distances et géométrie 3D.
Présenter Roblox Studio, l’objectif du jeu, et faire le lien entre les espaces vectoriels en maths et les objets 3D dans Roblox. Concepts clés : Espace vectoriel, coordonnées 3D, vecteurs.
Dans le ScreenGui, ajoute un Frame nommé VectorSpace (taille : 300, 200).
Dans le Frame, ajoute 6 TextButtons (noms : UpButton, DownButton, etc.).
Positionne les boutons comme ceci :
Haut : En haut au centre.
Bas : En bas au centre.
Gauche : À gauche au centre.
Droite : À droite au centre.
Avant : En bas à gauche.
Arrière : En bas à droite.
Teste : Vérifie que tous les objets sont bien placés.
QCM / Questions
Où doit-on placer les boutons de déplacement ?
a) Dans Workspace.
b) Dans StarterGui.
c) Dans ServerScriptService.
Quel objet contient le bloc mobile (Part) ?
a) Workspace > VectorSpace
b) StarterGui
c) ServerScriptService
Étape 3 : Détecter l’arrivée du joueur dans la hitbox
But : Détecter quand un joueur entre dans la zone de jeu et activer l’interface. Concepts clés : Événements, fonctions, détection de collision.
Explications
Pour détecter quand un joueur entre dans la HitBox, on utilise l’événement .Touched. Quand un joueur touche la hitbox :
On vérifie que c’est bien un personnage de joueur (et pas un autre objet).
On active l’interface pour ce joueur.
On stocke l’heure d’entrée pour éviter les bugs (ex: si le joueur entre/sort rapidement).
Pourquoi utiliser .Touched ?
C’est l’événement standard pour détecter les collisions dans Roblox.
Il se déclenche quand une partie du personnage (bras, jambes, tête) touche la hitbox.
Code Lua
Crée un Script nommé VectorSpaceScript dans ServerScriptService et copie ce code :
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
-- ==================== CONFIGURATION ====================
local EXIT_CONFIRM_DELAY = 0.5 -- Délai pour éviter les faux positifs à la sortie
-- ==================== RÉFÉRENCES WORKSPACE ====================
local vectorSpaceGameFolder = Workspace:WaitForChild("VectorSpace")
local hitbox = vectorSpaceGameFolder:WaitForChild("HitBox")
local movablePart = vectorSpaceGameFolder:WaitForChild("Part")
-- ==================== ÉTAT DU JEU ====================
local playerEntryTimestamps = {} -- [Player] = tick() au moment de l'entrée dans la zone
-- ==================== FONCTIONS UTILITAIRES ====================
-- Récupère le joueur à partir d'une partie touchée
local function getPlayerFromTouchedPart(hit)
local character = hit.Parent
if not character then return nil end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return nil end
return Players:GetPlayerFromCharacter(character)
end
-- ==================== ÉVÉNEMENTS DE COLLISION ====================
hitbox.Touched:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
-- Anti-spam : ignore si le joueur est déjà dans la zone
if playerEntryTimestamps[player] then return end
playerEntryTimestamps[player] = tick()
print(player.Name .. " est entré dans la zone !")
end)
hitbox.TouchEnded:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
-- Vérifie que le joueur est bien dans la zone
if not playerEntryTimestamps[player] then return end
-- Attend un délai pour éviter les faux positifs (ex: si le joueur touche encore la hitbox avec une autre partie du corps)
task.wait(EXIT_CONFIRM_DELAY)
-- Vérifie qu'aucune partie du personnage ne touche encore la hitbox
local character = player.Character
local stillTouching = false
if character then
for _, part in pairs(hitbox:GetTouchingParts()) do
if part.Parent == character then
stillTouching = true
break
end
end
end
if stillTouching then return end
if not playerEntryTimestamps[player] then return end
-- Le joueur a vraiment quitté la zone
playerEntryTimestamps[player] = nil
print(player.Name .. " a quitté la zone.")
end)
Explications du code
Ligne
Explication
Players = game:GetService("Players")
Récupère le service Players pour accéder aux joueurs.
hitbox.Touched:Connect(...)
Écoute l’événement Touched sur la hitbox.
getPlayerFromTouchedPart(hit)
Vérifie si l’objet qui a touché est un personnage de joueur.
playerEntryTimestamps[player] = tick()
Stocke l’heure d’entrée du joueur dans la zone.
hitbox.TouchEnded:Connect(...)
Écoute l’événement TouchEnded pour détecter quand le joueur quitte la hitbox.
hitbox:GetTouchingParts()
Récupère toutes les parties qui touchent encore la hitbox.
task.wait(EXIT_CONFIRM_DELAY)
Attend 0.5 seconde pour éviter les faux positifs.
À toi de jouer !
Copie le code dans le ScriptVectorSpaceScript.
Teste :
Entre dans la hitbox avec ton personnage.
Ouvre la fenêtre de sortie (Affichage > Sortie) pour voir les messages.
Modifie :
Change EXIT_CONFIRM_DELAY à 1 pour voir l’effet.
Ajoute un print pour afficher le nombre de joueurs dans la zone.
QCM / Questions
1. À quoi sert hitbox.Touched:Connect(...) ? a) À détruire la hitbox. b) À exécuter du code quand un objet touche la hitbox. c) À changer la couleur de la hitbox. Réponse : b)
2. Pourquoi utilise-t-on playerEntryTimestamps[player] ? a) Pour stocker l’heure d’entrée du joueur dans la zone. b) Pour changer le nom du joueur. c) Pour supprimer le joueur. Réponse : a)
3. Que fait hitbox:GetTouchingParts() ? a) Récupère toutes les parties qui touchent la hitbox. b) Détruit la hitbox. c) Change la taille de la hitbox. Réponse : a)
Pour aller plus loin
Affiche un message à l’écran quand le joueur entre dans la zone (utilise un ScreenGui et un TextLabel).
Étape 4 : Créer une interface utilisateur pour déplacer le bloc
But : Afficher une interface avec des boutons pour déplacer le bloc dans les 3 dimensions. Concepts clés : ScreenGui, TextButton.
Explications
Pour créer une interface utilisateur (GUI), on utilise :
Un ScreenGui : Conteneur pour les éléments d’interface.
Un Frame : Cadre pour regrouper les boutons.
Des TextButtons : Boutons cliquables.
Un PlayerGuit : Dossier propre à chaque joueur qui contient les interfaces graphiques
Pourquoi un LocalScript ?
Les interfaces utilisateur sont spécifiques à chaque joueur (chacun voit la sienne).
Récupération du dossier PlayerGuicôté serveur (pour un joueur spécifique), évitant d’utiliser un LocalScript qui s’exécute côté client. Cela permet notamment de centraliser la gestion de l’interface et de décider quand elle apparaît.
Code Lua
Modifie le code du script serveur : :
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
-- ==================== CONFIGURATION ====================
local EXIT_CONFIRM_DELAY = 0.5 -- Délai pour éviter les faux positifs à la sortie
-- ==================== RÉFÉRENCES WORKSPACE ====================
local vectorSpaceGameFolder = Workspace:WaitForChild("VectorSpace")
local hitbox = vectorSpaceGameFolder:WaitForChild("HitBox")
local movablePart = vectorSpaceGameFolder:WaitForChild("Part")
-- ==================== ÉTAT DU JEU ====================
local playerEntryTimestamps = {} -- [Player] = tick() au moment de l'entrée dans la zone
-- ==================== FONCTIONS UTILITAIRES ====================
-- Récupère le joueur à partir d'une partie touchée
local function getPlayerFromTouchedPart(hit)
local character = hit.Parent
if not character then return nil end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return nil end
return Players:GetPlayerFromCharacter(character)
end
--- Récupère les GUI nécessaires au déplacement pour un joueur donné.
local function getPlayerGuiElements(player)
local playerGui = player:WaitForChild("PlayerGui")
local screenGui = playerGui:WaitForChild("ScreenGui")
local vectorSpaceFrame = screenGui:WaitForChild("VectorSpace")
return vectorSpaceFrame
end
-- ==================== ÉVÉNEMENTS DE COLLISION ====================
hitbox.Touched:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
-- Anti-spam : ignore si le joueur est déjà dans la zone
if playerEntryTimestamps[player] then return end
playerEntryTimestamps[player] = tick()
local vectorSpaceFrame = getPlayerGuiElements(player)
if not vectorSpaceFrame then return end
vectorSpaceFrame.Visible = true
end)
hitbox.TouchEnded:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
if not playerEntryTimestamps[player] then return end
-- Attend un délai pour éviter les faux positifs (ex: si le joueur touche encore la hitbox avec une autre partie du corps)
task.wait(EXIT_CONFIRM_DELAY)
-- Vérifie qu'aucune partie du personnage ne touche encore la hitbox
local character = player.Character
local stillTouching = false
if character then
for _, part in pairs(hitbox:GetTouchingParts()) do
if part.Parent == character then
stillTouching = true
break
end
end
end
if stillTouching then return end
if not playerEntryTimestamps[player] then return end
-- Le joueur a vraiment quitté la zone
playerEntryTimestamps[player] = nil
local vectorSpaceFrame = getPlayerGuiElements(player)
if not vectorSpaceFrame then return end
vectorSpaceFrame.Visible = false
end)
-- Interface invisible au lancement du joueur
Players.PlayerAdded:Connect(function(player)
-- Aucune action nécessaire pour les joueurs déjà connectés
local vectorSpaceFrame = getPlayerGuiElements(player)
if not vectorSpaceFrame then return end
vectorSpaceFrame.Visible = false
end)
Pour l’instant, l’interface est masquée par défaut. On va la lier au Script serveur dans l’étape suivante.
À toi de jouer !
Modifie le script serveur dans ServerScriptService.
Teste :
Lance le jeu : l’interface ne doit pas être visible.
Personnalise l’interface :
Change la couleur de fond du Frame (BackgroundColor3).
Change le texte des boutons (Text).
Change la taille des boutons (Size).
Pour aller plus loin
Ajoute des icônes aux boutons (utilise ImageLabel).
Pour détecter les collisions, puisque les blocs sont Anchored et ROBLOX ne gère pas les collisions entre les blocs Anchored :
Raycast
Blockcast
Code Lua
1. Modifie le Script serveur (VectorSpaceScript)
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
-- ==================== CONFIGURATION ====================
local EXIT_CONFIRM_DELAY = 0.5 -- Délai pour éviter les faux positifs à la sortie
local MOVE_STEP = 1 -- Distance parcourue par clic (en studs)
-- ==================== RÉFÉRENCES WORKSPACE ====================
local vectorSpaceGameFolder = Workspace:WaitForChild("VectorSpace")
local hitbox = vectorSpaceGameFolder:WaitForChild("HitBox")
local movablePart = vectorSpaceGameFolder:WaitForChild("Part")
local wall = vectorSpaceGameFolder:WaitForChild("Wall")
-- ==================== ÉTAT DU JEU ====================
local playerEntryTimestamps = {} -- [Player] = tick() au moment de l'entrée dans la zone
local playerButtonConnections = {} -- [Player] = liste des connexions de boutons
-- ==================== RAYCAST (empêche de traverser les murs) ====================
local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
raycastParams.FilterDescendantsInstances = {hitbox}-- ==================== DÉTECTION DE COLLISION ====================
-- Touched ne se déclenche pas entre deux parts Anchored : on vérifie donc manuellement les chevauchements
local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Exclude
overlapParams.FilterDescendantsInstances = {hitbox, movablePart}
-- ==================== FONCTIONS UTILITAIRES ====================
-- Récupère le joueur à partir d'une partie touchée
local function getPlayerFromTouchedPart(hit)
local character = hit.Parent
if not character then return nil end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return nil end
return Players:GetPlayerFromCharacter(character)
end
--- Récupère les GUI nécessaires au déplacement pour un joueur donné.
local function getPlayerGuiElements(player)
local playerGui = player:WaitForChild("PlayerGui")
local screenGui = playerGui:WaitForChild("ScreenGui")
local vectorSpaceFrame = screenGui:WaitForChild("VectorSpace")
return vectorSpaceFrame
end
-- Déplace la Part si le raycast ne détecte aucun obstacle sur le trajet
local function moveMovablePart(movablePart, axisVector, directionSign)
if not movablePart then return end
local moveOffset = axisVector * directionSign
local rayOrigin = movablePart.Position + ((movablePart.Size / 2) * directionSign)
local result = workspace:Raycast(rayOrigin, moveOffset, raycastParams)
if result then return end -- Un obstacle bloque le trajet
movablePart.Position = movablePart.Position + moveOffset
print("Bloc déplacé à :", movablePart.Position)
end
-- Nettoie les connexions de boutons d'un joueur
local function disconnectPlayerButtons(player)
local connections = playerButtonConnections[player]
if not connections then return end
for _, connection in ipairs(connections) do
connection:Disconnect()
end
playerButtonConnections[player] = nil
end
--- Récupère les GUI nécessaires au déplacement pour un joueur donné.
local function getPlayerGuiButtons(vectorSpaceFrame)
local upButton = vectorSpaceFrame:WaitForChild("UpButton")
local downButton = vectorSpaceFrame:WaitForChild("DownButton")
local leftButton = vectorSpaceFrame:WaitForChild("LeftButton")
local rightButton = vectorSpaceFrame:WaitForChild("RightButton")
local backButton = vectorSpaceFrame:WaitForChild("BackButton")
local frontButton = vectorSpaceFrame:WaitForChild("FrontButton")
return upButton, downButton, leftButton, rightButton, backButton, frontButton
end
-- Met en place les contrôles de déplacement pour un joueur
local function setupPlayerMovementControls(player, movablePart, vectorSpaceFrame)
if not player then return end
if not vectorSpaceFrame then return end
-- On récupère les boutons du joueur
local upButton, downButton, leftButton, rightButton, backButton, frontButton = getPlayerGuiButtons(vectorSpaceFrame)
-- On nettoie d'éventuelles anciennes connexions avant d'en recréer
disconnectPlayerButtons(player)
local connections = {}
table.insert(connections, downButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(0, MOVE_STEP, 0), -1)
end))
table.insert(connections, upButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(0, MOVE_STEP, 0), 1)
end))
table.insert(connections, leftButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(0, 0, MOVE_STEP), -1)
end))
table.insert(connections, rightButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(0, 0, MOVE_STEP), 1)
end))
table.insert(connections, backButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(MOVE_STEP, 0, 0), 1)
end))
table.insert(connections, frontButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(MOVE_STEP, 0, 0), -1)
end))
playerButtonConnections[player] = connections
end
-- ==================== ÉVÉNEMENTS DE COLLISION ====================
hitbox.Touched:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
-- Anti-spam : ignore si le joueur est déjà dans la zone
if playerEntryTimestamps[player] then return end
playerEntryTimestamps[player] = tick()
local vectorSpaceFrame = getPlayerGuiElements(player)
if not vectorSpaceFrame then return end
vectorSpaceFrame.Visible = true
setupPlayerMovementControls(player, movablePart, vectorSpaceFrame)
end)
hitbox.TouchEnded:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
if not playerEntryTimestamps[player] then return end
-- Attend un délai pour éviter les faux positifs (ex: si le joueur touche encore la hitbox avec une autre partie du corps)
task.wait(EXIT_CONFIRM_DELAY)
-- Vérifie qu'aucune partie du personnage ne touche encore la hitbox
local character = player.Character
local stillTouching = false
if character then
for _, part in pairs(hitbox:GetTouchingParts()) do
if part.Parent == character then
stillTouching = true
break
end
end
end
if stillTouching then return end
if not playerEntryTimestamps[player] then return end
-- Le joueur a vraiment quitté la zone
playerEntryTimestamps[player] = nil
local vectorSpaceFrame = getPlayerGuiElements(player)
if not vectorSpaceFrame then return end
vectorSpaceFrame.Visible = false
end)
-- Interface invisible au lancement du joueur
Players.PlayerAdded:Connect(function(player)
-- Aucune action nécessaire pour les joueurs déjà connectés
local vectorSpaceFrame = getPlayerGuiElements(player)
if not vectorSpaceFrame then return end
vectorSpaceFrame.Visible = false
end)
Explications du code
Script serveur :
Ligne
Explication
moveMovablePart(axisVector, directionSign)
Déplace le bloc dans la direction spécifiée, si aucun obstacle ne bloque le chemin.
Raycast
Vérifie s’il y a un obstacle entre la position actuelle et la nouvelle position.
moveEvent.OnServerEvent:Connect(...)
Écoute les demandes de déplacement envoyées par le client.
À toi de jouer !
Copie le code dans le Script serveur
Teste :
Entre dans la hitbox : l’interface doit apparaître.
Clique sur les boutons : le bloc doit se déplacer.
Quitte la hitbox : l’interface doit disparaître.
Modifie :
Change MOVE_STEP à 2 pour que le bloc se déplace plus vite.
Ajoute un print dans la fonction moveMovablePart pour afficher la nouvelle position.
QCM / Questions
1. Pourquoi utilise-t-on Raycast dans moveMovablePart ?
a) Pour vérifier s’il y a un obstacle entre la position actuelle et la nouvelle position.
b) Pour changer la couleur du bloc.
c) Pour détruire le bloc.
Pour aller plus loin
Ajoute un effet visuel quand le bloc se déplace (ex: une traînée de particules).
Limite les déplacements pour que le bloc ne sorte pas de la hitbox.
Étape 7 : Détecter les collisions avec les clés (Raycast et OverlapParams)
But : Détecter quand le bloc mobile touche une clé, et gérer la logique de collecte. Concepts clés : OverlapParams, GetPartsInPart, conditions, tableaux.
Explications
Problème :
L’événement .Touched ne fonctionne pas entre deux Parts ancrées (comme notre bloc mobile et les clés).
On doit donc vérifier manuellement si le bloc touche une clé après chaque déplacement.
Solution :
Utiliser workspace:GetPartsInPart(movablePart, overlapParams) pour récupérer toutes les parties qui touchent le bloc mobile.
Parcourir les parties retournées et vérifier si l’une d’elles est une clé.
Si c’est une clé, vérifier sa couleur et la comparer à la prochaine couleur attendue (KEY_COLORS[1]).
Si la couleur correspond, supprimer la clé et passer à la suivante.
Pourquoi GetPartsInPart ?
Cette fonction retourne toutes les parties qui chevauchent (overlap) une partie donnée.
On peut filtrer les résultats avec OverlapParams pour exclure certaines parties (ex: la hitbox).
Code Lua
Ajoute ce code au Script serveur (VectorSpaceScript) :
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
-- ==================== CONFIGURATION ====================
local EXIT_CONFIRM_DELAY = 0.5 -- Délai pour éviter les faux positifs à la sortie
local MOVE_STEP = 1 -- Distance parcourue par clic (en studs)
-- Couleurs des clés à collecter, dans l'ordre où elles doivent être ramassées
local KEY_COLORS = {
Color3.fromRGB(255, 0, 0), -- Rouge
Color3.fromRGB(0, 0, 255), -- Bleu
Color3.fromRGB(0, 255, 0), -- Vert
}
-- ==================== RÉFÉRENCES WORKSPACE ====================
local vectorSpaceGameFolder = Workspace:WaitForChild("VectorSpace")
local hitbox = vectorSpaceGameFolder:WaitForChild("HitBox")
local movablePart = vectorSpaceGameFolder:WaitForChild("Part")
local wall = vectorSpaceGameFolder:WaitForChild("Wall")
-- ==================== ÉTAT DU JEU ====================
local playerEntryTimestamps = {} -- [Player] = tick() au moment de l'entrée dans la zone
local playerButtonConnections = {} -- [Player] = liste des connexions de boutons
-- ==================== RAYCAST (empêche de traverser les murs) ====================
local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
raycastParams.FilterDescendantsInstances = {hitbox}
-- ==================== DÉTECTION DE COLLISION ====================
-- Touched ne se déclenche pas entre deux parts Anchored : on vérifie donc manuellement les chevauchements
local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Exclude
overlapParams.FilterDescendantsInstances = {hitbox}
-- ==================== FONCTIONS UTILITAIRES ====================
-- Récupère le joueur à partir d'une partie touchée
local function getPlayerFromTouchedPart(hit)
local character = hit.Parent
if not character then return nil end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return nil end
return Players:GetPlayerFromCharacter(character)
end
--- Récupère les GUI nécessaires au déplacement pour un joueur donné.
local function getPlayerGuiElements(player)
local playerGui = player:WaitForChild("PlayerGui")
local screenGui = playerGui:WaitForChild("ScreenGui")
local vectorSpaceFrame = screenGui:WaitForChild("VectorSpace")
return vectorSpaceFrame
end
-- Ouvre la porte
local function openWall()
if wall then
wall:Destroy()
movablePart:Destroy()
movablePart = nil
print("La porte s'ouvre !")
end
end
-- Vérifie si le bloc mobile touche une clé, et déclenche la logique de collecte
local function checkMovablePartCollisions()
if not movablePart then return end
print("test de collision")
local touchingParts = Workspace:GetPartsInPart(movablePart, overlapParams)
print(touchingParts)
for _, touchingPart in ipairs(touchingParts) do
print(touchingPart.Name)
-- On ne réagit qu'à la clé actuellement attendue (la première de la liste)
if touchingPart.Name == "Key" and touchingPart.Color == KEY_COLORS[1] then
touchingPart:Destroy() -- Supprime la clé
table.remove(KEY_COLORS, 1) -- Retire la couleur de la liste
if #KEY_COLORS > 0 then
-- Il reste des clés à collecter : on indique la prochaine couleur attendue
movablePart.Color = KEY_COLORS[1]
print("Prochaine clé :", KEY_COLORS[1])
else
-- Toutes les clés sont collectées : victoire !
print("Toutes les clés ont été collectées !")
openWall()
end
break -- On sort de la boucle car on a trouvé une clé
end
end
end
-- Déplace la Part si le raycast ne détecte aucun obstacle sur le trajet
local function moveMovablePart(movablePart, axisVector, directionSign)
if not movablePart then return end
local moveOffset = axisVector * directionSign
local rayOrigin = movablePart.Position + ((movablePart.Size / 2) * directionSign)
local result = workspace:Raycast(rayOrigin, moveOffset, raycastParams)
if result then return end -- Un obstacle bloque le trajet
movablePart.Position = movablePart.Position + moveOffset
checkMovablePartCollisions()
end
-- Nettoie les connexions de boutons d'un joueur
local function disconnectPlayerButtons(player)
local connections = playerButtonConnections[player]
if not connections then return end
for _, connection in ipairs(connections) do
connection:Disconnect()
end
playerButtonConnections[player] = nil
end
--- Récupère les GUI nécessaires au déplacement pour un joueur donné.
local function getPlayerGuiButtons(vectorSpaceFrame)
local upButton = vectorSpaceFrame:WaitForChild("UpButton")
local downButton = vectorSpaceFrame:WaitForChild("DownButton")
local leftButton = vectorSpaceFrame:WaitForChild("LeftButton")
local rightButton = vectorSpaceFrame:WaitForChild("RightButton")
local backButton = vectorSpaceFrame:WaitForChild("BackButton")
local frontButton = vectorSpaceFrame:WaitForChild("FrontButton")
return upButton, downButton, leftButton, rightButton, backButton, frontButton
end
-- Met en place les contrôles de déplacement pour un joueur
local function setupPlayerMovementControls(player, movablePart, vectorSpaceFrame)
if not player then return end
if not vectorSpaceFrame then return end
-- On récupère les boutons du joueur
local upButton, downButton, leftButton, rightButton, backButton, frontButton = getPlayerGuiButtons(vectorSpaceFrame)
-- On nettoie d'éventuelles anciennes connexions avant d'en recréer
disconnectPlayerButtons(player)
local connections = {}
table.insert(connections, downButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(0, MOVE_STEP, 0), -1)
end))
table.insert(connections, upButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(0, MOVE_STEP, 0), 1)
end))
table.insert(connections, leftButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(0, 0, MOVE_STEP), -1)
end))
table.insert(connections, rightButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(0, 0, MOVE_STEP), 1)
end))
table.insert(connections, backButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(MOVE_STEP, 0, 0), 1)
end))
table.insert(connections, frontButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(MOVE_STEP, 0, 0), -1)
end))
playerButtonConnections[player] = connections
end
-- ==================== ÉVÉNEMENTS DE COLLISION ====================
hitbox.Touched:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
-- Anti-spam : ignore si le joueur est déjà dans la zone
if playerEntryTimestamps[player] then return end
playerEntryTimestamps[player] = tick()
local vectorSpaceFrame = getPlayerGuiElements(player)
if not vectorSpaceFrame then return end
vectorSpaceFrame.Visible = true
setupPlayerMovementControls(player, movablePart, vectorSpaceFrame)
end)
hitbox.TouchEnded:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
if not playerEntryTimestamps[player] then return end
-- Attend un délai pour éviter les faux positifs (ex: si le joueur touche encore la hitbox avec une autre partie du corps)
task.wait(EXIT_CONFIRM_DELAY)
-- Vérifie qu'aucune partie du personnage ne touche encore la hitbox
local character = player.Character
local stillTouching = false
if character then
for _, part in pairs(hitbox:GetTouchingParts()) do
if part.Parent == character then
stillTouching = true
break
end
end
end
if stillTouching then return end
if not playerEntryTimestamps[player] then return end
-- Le joueur a vraiment quitté la zone
playerEntryTimestamps[player] = nil
local vectorSpaceFrame = getPlayerGuiElements(player)
if not vectorSpaceFrame then return end
vectorSpaceFrame.Visible = false
end)
-- Interface invisible au lancement du joueur
Players.PlayerAdded:Connect(function(player)
-- Aucune action nécessaire pour les joueurs déjà connectés
local vectorSpaceFrame = getPlayerGuiElements(player)
if not vectorSpaceFrame then return end
vectorSpaceFrame.Visible = false
end)
Explications du code
Ligne
Explication
KEY_COLORS = {Color3.fromRGB(255, 0, 0), ...}
Définit l’ordre des couleurs des clés à collecter.
Étape 8 : Finaliser le jeu (nettoyage et optimisations)
But : Finaliser le jeu en ajoutant des optimisations et en nettoyant les connexions. Concepts clés : Gestion de la mémoire, nettoyage des connexions, optimisation.
Explications
Pour éviter les fuites mémoire (problèmes de performance), il faut :
Déconnecter les connexions quand un joueur quitte la zone ou le jeu.
Nettoyer les références aux objets détruits (ex: movablePart = nil après sa destruction).
Gérer les cas d’erreur (ex: si un joueur quitte pendant qu’il est dans la zone).
Code Lua (Script serveur final)
Voici le script complet avec toutes les optimisations :
luaCopier
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
-- ==================== CONFIGURATION ====================
local EXIT_CONFIRM_DELAY = 0.5 -- Délai pour éviter les faux positifs à la sortie
local MOVE_STEP = 1 -- Distance parcourue par clic (en studs)
-- Couleurs des clés à collecter, dans l'ordre où elles doivent être ramassées
local KEY_COLORS = {
Color3.fromRGB(255, 0, 0), -- Rouge
Color3.fromRGB(0, 0, 255), -- Bleu
Color3.fromRGB(0, 255, 0), -- Vert
}
-- ==================== RÉFÉRENCES WORKSPACE ====================
local vectorSpaceGameFolder = Workspace:WaitForChild("VectorSpace")
local hitbox = vectorSpaceGameFolder:WaitForChild("HitBox")
local movablePart = vectorSpaceGameFolder:WaitForChild("Part")
local wall = vectorSpaceGameFolder:WaitForChild("Wall")
-- ==================== ÉTAT DU JEU ====================
local playerEntryTimestamps = {} -- [Player] = tick() au moment de l'entrée dans la zone
local playerButtonConnections = {} -- [Player] = liste des connexions de boutons
-- ==================== RAYCAST (empêche de traverser les murs) ====================
local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
raycastParams.FilterDescendantsInstances = {hitbox}
-- ==================== DÉTECTION DE COLLISION ====================
-- Touched ne se déclenche pas entre deux parts Anchored : on vérifie donc manuellement les chevauchements
local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Exclude
overlapParams.FilterDescendantsInstances = {hitbox}
-- ==================== FONCTIONS UTILITAIRES ====================
-- Récupère le joueur à partir d'une partie touchée
local function getPlayerFromTouchedPart(hit)
local character = hit.Parent
if not character then return nil end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return nil end
return Players:GetPlayerFromCharacter(character)
end
--- Récupère les GUI nécessaires au déplacement pour un joueur donné.
local function getPlayerGuiElements(player)
local playerGui = player:WaitForChild("PlayerGui")
local screenGui = playerGui:WaitForChild("ScreenGui")
local vectorSpaceFrame = screenGui:WaitForChild("VectorSpace")
return vectorSpaceFrame
end
-- Cache l'interface pour tous les joueurs
local function setInterfaceVisibility(player, etat)
if not player then return end
local vectorSpaceFrame = getPlayerGuiElements(player)
vectorSpaceFrame.Visible = etat
return vectorSpaceFrame
end
-- Ouvre la porte
local function openWall()
if wall then
wall:Destroy()
end
if movablePart then
movablePart:Destroy()
movablePart = nil
end
print("La porte s'ouvre !")
-- Cache l'interface pour tous les joueurs
for player, _ in pairs(playerEntryTimestamps) do
setInterfaceVisibility(player, false)
end
end
-- Vérifie si le bloc mobile touche une clé, et déclenche la logique de collecte
local function checkMovablePartCollisions()
if not movablePart then return end
local touchingParts = Workspace:GetPartsInPart(movablePart, overlapParams)
for _, touchingPart in ipairs(touchingParts) do
-- On ne réagit qu'à la clé actuellement attendue (la première de la liste)
if touchingPart.Name == "Key" and touchingPart.Color == KEY_COLORS[1] then
touchingPart:Destroy() -- Supprime la clé
table.remove(KEY_COLORS, 1) -- Retire la couleur de la liste
if #KEY_COLORS > 0 then
-- Il reste des clés à collecter : on indique la prochaine couleur attendue
movablePart.Color = KEY_COLORS[1]
else
-- Toutes les clés sont collectées : victoire !
openWall()
end
break -- On sort de la boucle car on a trouvé une clé
end
end
end
-- Déplace la Part si le raycast ne détecte aucun obstacle sur le trajet
local function moveMovablePart(movablePart, axisVector, directionSign)
if not movablePart then return end
local moveOffset = axisVector * directionSign
local rayOrigin = movablePart.Position + ((movablePart.Size / 2) * directionSign)
local result = workspace:Raycast(rayOrigin, moveOffset, raycastParams)
if result then return end -- Un obstacle bloque le trajet
movablePart.Position = movablePart.Position + moveOffset
checkMovablePartCollisions()
end
-- Nettoie les connexions de boutons d'un joueur
local function disconnectPlayerButtons(player)
local connections = playerButtonConnections[player]
if not connections then return end
for _, connection in ipairs(connections) do
connection:Disconnect()
end
playerButtonConnections[player] = nil
end
--- Récupère les GUI nécessaires au déplacement pour un joueur donné.
local function getPlayerGuiButtons(vectorSpaceFrame)
local upButton = vectorSpaceFrame:WaitForChild("UpButton")
local downButton = vectorSpaceFrame:WaitForChild("DownButton")
local leftButton = vectorSpaceFrame:WaitForChild("LeftButton")
local rightButton = vectorSpaceFrame:WaitForChild("RightButton")
local backButton = vectorSpaceFrame:WaitForChild("BackButton")
local frontButton = vectorSpaceFrame:WaitForChild("FrontButton")
return upButton, downButton, leftButton, rightButton, backButton, frontButton
end
-- Met en place les contrôles de déplacement pour un joueur
local function setupPlayerMovementControls(player, movablePart, vectorSpaceFrame)
if not player then return end
if not vectorSpaceFrame then return end
-- On récupère les boutons du joueur
local upButton, downButton, leftButton, rightButton, backButton, frontButton = getPlayerGuiButtons(vectorSpaceFrame)
-- On nettoie d'éventuelles anciennes connexions avant d'en recréer
disconnectPlayerButtons(player)
local connections = {}
table.insert(connections, downButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(0, MOVE_STEP, 0), -1)
end))
table.insert(connections, upButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(0, MOVE_STEP, 0), 1)
end))
table.insert(connections, leftButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(0, 0, MOVE_STEP), -1)
end))
table.insert(connections, rightButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(0, 0, MOVE_STEP), 1)
end))
table.insert(connections, backButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(MOVE_STEP, 0, 0), 1)
end))
table.insert(connections, frontButton.MouseButton1Click:Connect(function()
moveMovablePart(movablePart, Vector3.new(MOVE_STEP, 0, 0), -1)
end))
playerButtonConnections[player] = connections
end
-- ==================== ÉVÉNEMENTS DE COLLISION ====================
hitbox.Touched:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
-- Anti-spam : ignore si le joueur est déjà dans la zone
if playerEntryTimestamps[player] then return end
playerEntryTimestamps[player] = tick()
local vectorSpaceFrame = setInterfaceVisibility(player, true)
setupPlayerMovementControls(player, movablePart, vectorSpaceFrame)
end)
hitbox.TouchEnded:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
if not playerEntryTimestamps[player] then return end
-- Attend un délai pour éviter les faux positifs (ex: si le joueur touche encore la hitbox avec une autre partie du corps)
task.wait(EXIT_CONFIRM_DELAY)
-- Vérifie qu'aucune partie du personnage ne touche encore la hitbox
local character = player.Character
local stillTouching = false
if character then
for _, part in pairs(hitbox:GetTouchingParts()) do
if part.Parent == character then
stillTouching = true
break
end
end
end
if stillTouching then return end
if not playerEntryTimestamps[player] then return end
-- Le joueur a vraiment quitté la zone
playerEntryTimestamps[player] = nil
setInterfaceVisibility(player, false)
disconnectPlayerButtons(player)
end)
-- Interface invisible au lancement du joueur
Players.PlayerAdded:Connect(function(player)
-- Aucune action nécessaire pour les joueurs déjà connectés
setInterfaceVisibility(player, false)
end)
-- Nettoyage quand un joueur quitte
Players.PlayerRemoving:Connect(function(player)
playerEntryTimestamps[player] = nil
disconnectPlayerButtons(player)
setInterfaceVisibility(player, false)
end)
Explications des optimisations
Optimisation
Explication
playerButtonConnections[player] = nil
Nettoie les connexions pour éviter les fuites mémoire.
if not movablePart then return end
Évite les erreurs si le bloc a été détruit.
setInterfaceVisibility(player, false)
Optimise la visibilité de l’écran du joueur
openWall() cache l’interface pour tous les joueurs
Améliore l’expérience utilisateur.
À toi de jouer !
Copie le script final dans VectorSpaceScript.
Teste le jeu complet :
Entre dans la hitbox.
Déplace le bloc pour toucher les clés dans l’ordre.
Vérifie que la porte s’ouvre et que l’interface disparaît.
Modifie :
Ajoute une 4ème clé avec une nouvelle couleur (ex: jaune).
Change MOVE_STEP à 0.5 pour des déplacements plus précis.
Étape 2 : Créer la structure des objets dans l’Explorateur
But : Organiser les objets du jeu dans l’Explorateur. Concepts clés : Hiérarchie des objets, organisation.
Explications
Dans Roblox, les objets sont organisés comme un arbre :
Workspace : Contient tous les objets visibles dans le jeu (pyramide, hitbox, balle, etc.).
ServerScriptService : Contient les scripts côté serveur (pour la logique du jeu).
ReplicatedStorage : Contient les modèles réutilisables (comme la balle ou la brique).
StarterPack : Contient des objets donnés au joueur au démarrage.
Structure recommandée :
Workspace
├── BreakPyramid (Folder)
│ ├── HitBox (Part)
│ │ └── Attachment (avec ProximityPrompt)
│ ├── Ball (Part)
│ ├── Part (Part, modèle de brique)
│ └── ControlPyramid (Part, détecteur de pyramide)
ServerScriptService
└── Script (pour la logique du jeu)
À toi de jouer !
Crée un dossierBreakPyramid dans Workspace.
Ajoute les objets suivants dans BreakPyramid :
Une Part nommée HitBox.
Une Part nommée Ball (forme de sphère).
Une Part nommée Part (modèle de brique).
Une Part nommée ControlPyramid (pour détecter les briques restantes).
Ajoute un Attachment à HitBox et un ProximityPrompt à l’Attachment.
Étape 3 : Construire dynamiquement une pyramide de briques
But : Créer une fonction qui génère une pyramide avec des briques de tailles aléatoires. Concepts clés : Fonctions, boucles for, nombres aléatoires, clonage d’objets, CFrame.
Explications
Pour construire une pyramide dynamiquement (sans tout faire à la main), on utilise :
Une boucle for pour répéter la création de briques.
math.random() pour générer des tailles aléatoires.
partTemplate:Clone() pour créer des copies de la brique de base.
CFrame pour positionner et orienter les briques.
Exemple concret :
La première brique est grande.
Chaque brique suivante est plus petite (pour former une pyramide).
Les briques sont empilées verticalement.
Code Lua
Crée un Script dans ServerScriptService et copie ce code :
-- ==================== SERVICES ====================
local Workspace = game:GetService("Workspace")
local ServerStorage = game:GetService("ServerStorage")
-- ==================== CONFIGURATION ====================
local PYRAMID_PARTS_COUNT = 8 -- Nombre de briques dans la pyramide
local PYRAMID_MIN_SIZE_STEP = 0.2 -- Réduction minimale de la taille à chaque brique
local PYRAMID_MAX_SIZE_STEP = 0.5 -- Réduction maximale de la taille à chaque brique
-- ==================== RÉFÉRENCES SCÈNE ====================
local breakPyramidFolder = Workspace:WaitForChild("BreakPyramid")
local pyramidPartTemplate = breakPyramidFolder:WaitForChild("Part")
local ballTemplate = breakPyramidFolder:WaitForChild("Ball")
-- on retire le modèle du workspace, il ne sert que de gabarit à cloner
ballTemplate.Parent = ServerStorage
-- ==================== FONCTION : CONSTRUIRE LA PYRAMIDE ====================
-- (Re)construit la pyramide de briques à partir du gabarit
local function rebuildPyramid(partTemplate)
local baseHeight = partTemplate.Position.Y + partTemplate.Size.Y / 2
local parentFolder = partTemplate.Parent
-- Supprime l'ancienne pyramide si elle existe
local existingFolder = parentFolder:FindFirstChild("PyramidParts")
if existingFolder then
existingFolder:Destroy()
end
-- Crée un dossier pour les briques de la pyramide
local folder = Instance.new("Folder")
folder.Name = "PyramidParts"
folder.Parent = parentFolder
-- Taille initiale de la brique (entre minSize et maxSize)
local minSize = partTemplate.Size.X / 2
local maxSize = partTemplate.Size.X
local currentHeight = baseHeight
-- 1) Construction : toutes les briques sont créées ANCRÉES et SANS collision
for i = 1, PYRAMID_PARTS_COUNT do
local part = partTemplate:Clone()
local partHeight = math.random(1, 3) -- Hauteur aléatoire entre 1 et 3
currentHeight = currentHeight + partHeight / 2
part.Name = "pyramid" -- Nom pour identifier les briques de la pyramide
part.Anchored = true -- Empêche la brique de bouger pendant la construction
part.CanCollide = false -- Désactive les collisions pendant la construction
part.CanTouch = false -- Désactive la détection de contact
part.Transparency = 0 -- Rend la brique visible
part.Size = Vector3.new(
math.random(minSize, maxSize), -- Largeur aléatoire
partHeight, -- Hauteur aléatoire
math.random(minSize, maxSize) -- Profondeur aléatoire
)
-- Positionne la brique au centre de la pyramide, avec une rotation aléatoire
part.CFrame = CFrame.new(
partTemplate.Position.X,
currentHeight,
partTemplate.Position.Z
) * CFrame.Angles(0, math.rad(math.random(0, 360)), 0) -- Rotation aléatoire autour de l'axe Y
-- Copie les propriétés visuelles du modèle
part.BrickColor = partTemplate.BrickColor
part.Material = partTemplate.Material
-- Ajoute la brique au dossier
part.Parent = folder
-- Réduit la taille pour les prochaines briques
minSize = minSize - PYRAMID_MIN_SIZE_STEP
maxSize = maxSize - PYRAMID_MAX_SIZE_STEP
currentHeight = currentHeight + partHeight / 2
end
-- 2) Activation : une fois la pyramide construite, on active la physique
task.wait(0.1) -- Petit délai pour s'assurer que tout est bien positionné
for _, part in ipairs(folder:GetChildren()) do
part.CanCollide = true -- Active les collisions
part.CanTouch = true -- Active la détection de contact
part.Anchored = false -- Permet à la brique de bouger (physique)
end
end
-- ==================== INITIALISATION ====================
rebuildPyramid(pyramidPartTemplate) -- Construit la pyramide au démarrage
Explications du code
Ligne
Explication
for i = 1, PYRAMID_PARTS_COUNT do
Boucle qui répète la création de briques 8 fois.
math.random(1, 3)
Génère un nombre aléatoire entre 1 et 3 (hauteur de la brique).
partTemplate:Clone()
Crée une copie du modèle de brique.
part.Size = Vector3.new(...)
Définit la taille de la brique (largeur, hauteur, profondeur).
CFrame.new(x, y, z)
Positionne la brique à un endroit précis.
CFrame.Angles(...)
Applique une rotation aléatoire à la brique.
part.Anchored = true
Empêche la brique de bouger pendant la construction.
task.wait(0.1)
Attend 0.1 seconde pour que tout soit bien positionné.
part.Anchored = false
Permet à la brique de bouger (physique activée).
À toi de jouer !
Copie le code dans un Script dans ServerScriptService.
Teste :
Lance le jeu : une pyramide de 8 briques doit apparaître.
Observe les tailles et les rotations aléatoires.
Modifie :
Change PYRAMID_PARTS_COUNT à 5 pour avoir une pyramide plus petite.
Change PYRAMID_MIN_SIZE_STEP et PYRAMID_MAX_SIZE_STEP à 0.3 pour que les briques rétrécissent plus vite.
QCM / Questions
À quoi sert la boucle for i = 1, PYRAMID_PARTS_COUNT do ?
a) À créer une seule brique.
b) À répéter la création de briques PYRAMID_PARTS_COUNT fois.
c) À détruire la pyramide.
Que fait partTemplate:Clone() ?
a) Supprime le modèle de brique.
b) Crée une copie du modèle de brique.
c) Change la couleur de la brique.
Pourquoi utilise-t-on task.wait(0.1) avant d’activer la physique ?
a) Pour attendre que toutes les briques soient bien positionnées.
b) Pour ralentir le jeu.
c) Pour afficher un message.
Pour aller plus loin
Ajoute des couleurs aléatoires aux briques avec part.BrickColor = BrickColor.new(math.random()).
Fais en sorte que les briques soient empilées en spirale en modifiant leur position.
Étape 4 : Gérer une hitbox pour détecter l’arrivée du joueur
But : Détecter quand un joueur entre dans la zone de lancer. Concepts clés : Événements, fonctions, détection de collision, ProximityPrompt.
Explications
Une hitbox est une zone invisible qui détecte quand un joueur entre ou sort. En Lua, on utilise :
L’événement .Touched pour savoir quand un objet touche la hitbox.
L’événement .TouchEnded pour savoir quand un objet quitte la hitbox.
ProximityPrompt pour afficher un bouton quand le joueur est dans la zone.
Exemple concret :
Quand le joueur entre dans la hitbox, on peut changer sa caméra ou activer un bouton.
Code Lua
Ajoute ce code au Script précédent :
-- ==================== SERVICES ====================
local Workspace = game:GetService("Workspace")
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
-- ==================== CONFIGURATION ====================
local PYRAMID_PARTS_COUNT = 8 -- Nombre de briques dans la pyramide
local PYRAMID_MIN_SIZE_STEP = 0.2 -- Réduction minimale de la taille à chaque brique
local PYRAMID_MAX_SIZE_STEP = 0.5 -- Réduction maximale de la taille à chaque brique
-- ==================== RÉFÉRENCES SCÈNE ====================
local breakPyramidFolder = Workspace:WaitForChild("BreakPyramid")
local pyramidPartTemplate = breakPyramidFolder:WaitForChild("Part")
local ballTemplate = breakPyramidFolder:WaitForChild("Ball")
local hitbox = breakPyramidFolder:WaitForChild("HitBox")
local prompt = hitbox.Attachment:WaitForChild("ProximityPrompt")-- ==================== ÉTAT DU JEU ====================
-- playersData[player] = { inZone = bool, nbrPoints = number }
local playersData = {}
-- on retire le modèle du workspace, il ne sert que de gabarit à cloner
ballTemplate.Parent = ServerStorage
-- ==================== FONCTION : CONSTRUIRE LA PYRAMIDE ====================
-- (Re)construit la pyramide de briques à partir du gabarit
local function rebuildPyramid(partTemplate)
local baseHeight = partTemplate.Position.Y + partTemplate.Size.Y / 2
local parentFolder = partTemplate.Parent
-- Supprime l'ancienne pyramide si elle existe
local existingFolder = parentFolder:FindFirstChild("PyramidParts")
if existingFolder then
existingFolder:Destroy()
end
-- Crée un dossier pour les briques de la pyramide
local folder = Instance.new("Folder")
folder.Name = "PyramidParts"
folder.Parent = parentFolder
-- Taille initiale de la brique (entre minSize et maxSize)
local minSize = partTemplate.Size.X / 2
local maxSize = partTemplate.Size.X
local currentHeight = baseHeight
-- 1) Construction : toutes les briques sont créées ANCRÉES et SANS collision
for i = 1, PYRAMID_PARTS_COUNT do
local part = partTemplate:Clone()
local partHeight = math.random(1, 3) -- Hauteur aléatoire entre 1 et 3
currentHeight = currentHeight + partHeight / 2
part.Name = "pyramid" -- Nom pour identifier les briques de la pyramide
part.Anchored = true -- Empêche la brique de bouger pendant la construction
part.CanCollide = false -- Désactive les collisions pendant la construction
part.CanTouch = false -- Désactive la détection de contact
part.Transparency = 0 -- Rend la brique visible
part.Size = Vector3.new(
math.random(minSize, maxSize), -- Largeur aléatoire
partHeight, -- Hauteur aléatoire
math.random(minSize, maxSize) -- Profondeur aléatoire
)
-- Positionne la brique au centre de la pyramide, avec une rotation aléatoire
part.CFrame = CFrame.new(
partTemplate.Position.X,
currentHeight,
partTemplate.Position.Z
) * CFrame.Angles(0, math.rad(math.random(0, 360)), 0) -- Rotation aléatoire autour de l'axe Y
-- Copie les propriétés visuelles du modèle
part.BrickColor = partTemplate.BrickColor
part.Material = partTemplate.Material
-- Ajoute la brique au dossier
part.Parent = folder
-- Réduit la taille pour les prochaines briques
minSize = minSize - PYRAMID_MIN_SIZE_STEP
maxSize = maxSize - PYRAMID_MAX_SIZE_STEP
currentHeight = currentHeight + partHeight / 2
end
-- 2) Activation : une fois la pyramide construite, on active la physique
task.wait(0.1) -- Petit délai pour s'assurer que tout est bien positionné
for _, part in ipairs(folder:GetChildren()) do
part.CanCollide = true -- Active les collisions
part.CanTouch = true -- Active la détection de contact
part.Anchored = false -- Permet à la brique de bouger (physique)
end
end
-- ==================== UTILITAIRES ====================
-- Récupère le joueur à partir d'une partie touchée
local function getPlayerFromTouchedPart(hit)
local character = hit.Parent
if not character then return nil end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return nil end
return Players:GetPlayerFromCharacter(character)
end
-- ==================== ÉVÉNEMENTS : ZONE DE JEU ====================
-- Quand un joueur touche la hitbox
hitbox.Touched:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
-- Anti-spam : ignore si le joueur est déjà dans la zone
if playersData[player] and playersData[player].inZone then return end
playersData[player] = { inZone = true, nbrPoints = 5 } -- 5 points de départ
print(player.Name .. " est entré dans la zone de lancer !")
end)
-- Quand un joueur quitte la hitbox
hitbox.TouchEnded:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
if playersData[player] and playersData[player].inZone==false then return end
-- Attend 0.5 seconde pour éviter les faux positifs (si le joueur touche encore la hitbox avec une autre partie du corps)
task.wait(0.5)
-- Vérifie qu'aucune partie du personnage ne touche encore la hitbox
local character = player.Character
local stillTouching = false
if character then
for _, part in pairs(hitbox:GetTouchingParts()) do
if part.Parent == character then
stillTouching = true
break
end
end
end
if stillTouching then return end
if playersData[player] and playersData[player].inZone==false then return end
-- Le joueur a vraiment quitté la zone
playersData[player].inZone = false
print(player.Name .. " a quitté la zone de lancer.")
end)
-- ==================== INITIALISATION ====================
rebuildPyramid(pyramidPartTemplate) -- Construit la pyramide au démarrage
Explications du code
Ligne
Explication
hitbox.Touched:Connect(...)
Écoute l’événement Touched sur la hitbox.
getPlayerFromTouchedPart(hit)
Vérifie si l’objet qui a touché est un personnage de joueur.
playersData[player]
Stocke des informations sur le joueur (ex: s’il est dans la zone, son nombre de points).
hitbox.TouchEnded:Connect(...)
Écoute l’événement TouchEnded pour détecter quand le joueur quitte la hitbox.
hitbox:GetTouchingParts()
Récupère toutes les parties qui touchent la hitbox.
task.wait(0.5)
Attend 0.5 seconde pour éviter les faux positifs (ex: si le joueur bouge rapidement).
À toi de jouer !
Copie le code dans le Script existant.
Teste :
Entre dans la hitbox avec ton personnage.
Ouvre la fenêtre de sortie (Affichage > Sortie) pour voir les messages.
Modifie :
Change le nombre de points de départ (nbrPoints = 10).
Ajoute un print pour afficher le nombre de points du joueur quand il entre dans la zone.
QCM / Questions
À quoi sert hitbox.Touched:Connect(...) ?
a) À détruire la hitbox.
b) À exécuter du code quand un objet touche la hitbox.
c) À changer la couleur de la hitbox.
Pourquoi utilise-t-on task.wait(0.5) dans TouchEnded ?
a) Pour attendre que le joueur ait vraiment quitté la zone.
b) Pour ralentir le jeu.
c) Pour afficher un message.
Que fait playersData[player] = { inZone = true, nbrPoints = 5 } ?
a) Supprime les données du joueur.
b) Stocke des informations sur le joueur (s’il est dans la zone et son nombre de points).
c) Change le nom du joueur.
Pour aller plus loin
Affiche un message à l’écran quand le joueur entre dans la zone (utilise un ScreenGui et un TextLabel).
Étape 5 : Changer la vision du joueur en première personne
But : Passer en vue première personne quand le joueur entre dans la zone de lancer. Concepts clés : Propriétés des objets, fonctions utilitaires, CameraMode.
Explications
En Roblox, on peut modifier la caméra du joueur pour :
LockFirstPerson : Vue à la première personne (comme dans un FPS).
Classic : Vue classique (derrière le personnage).
Exemple concret :
Quand le joueur entre dans la zone, on passe en première personne pour mieux viser.
Quand il quitte la zone, on revient en vue classique.
Code Lua
Ajoute ces fonctions au Script :
-- ==================== UTILITAIRES ====================
-- Applique une caméra first person (vue première personne verrouillée et zoomée)
local function lockFirstPersonCamera(player)
player.CameraMode = Enum.CameraMode.LockFirstPerson
player.CameraMinZoomDistance = 0.5
player.CameraMaxZoomDistance = 0.5
end
-- Restaure la caméra classique du joueur
local function resetCamera(player)
player.CameraMode = Enum.CameraMode.Classic
player.CameraMaxZoomDistance = 400
task.wait(0.1) -- Attend 0.1 seconde pour que la caméra se réinitialise correctement
player.CameraMinZoomDistance = 10
end
Puis modifie les événements Touched et TouchEnded :
-- ==================== SERVICES ====================
local Workspace = game:GetService("Workspace")
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
-- ==================== CONFIGURATION ====================
local PYRAMID_PARTS_COUNT = 8 -- Nombre de briques dans la pyramide
local PYRAMID_MIN_SIZE_STEP = 0.2 -- Réduction minimale de la taille à chaque brique
local PYRAMID_MAX_SIZE_STEP = 0.5 -- Réduction maximale de la taille à chaque brique
-- ==================== RÉFÉRENCES SCÈNE ====================
local breakPyramidFolder = Workspace:WaitForChild("BreakPyramid")
local pyramidPartTemplate = breakPyramidFolder:WaitForChild("Part")
local ballTemplate = breakPyramidFolder:WaitForChild("Ball")
local hitbox = breakPyramidFolder:WaitForChild("HitBox")
local prompt = hitbox.Attachment:WaitForChild("ProximityPrompt")
-- ==================== ÉTAT DU JEU ====================
-- playersData[player] = { inZone = bool, nbrPoints = number }
local playersData = {}
-- on retire le modèle du workspace, il ne sert que de gabarit à cloner
ballTemplate.Parent = ServerStorage
-- ==================== FONCTION : CONSTRUIRE LA PYRAMIDE ====================
-- (Re)construit la pyramide de briques à partir du gabarit
local function rebuildPyramid(partTemplate)
local baseHeight = partTemplate.Position.Y + partTemplate.Size.Y / 2
local parentFolder = partTemplate.Parent
-- Supprime l'ancienne pyramide si elle existe
local existingFolder = parentFolder:FindFirstChild("PyramidParts")
if existingFolder then
existingFolder:Destroy()
end
-- Crée un dossier pour les briques de la pyramide
local folder = Instance.new("Folder")
folder.Name = "PyramidParts"
folder.Parent = parentFolder
-- Taille initiale de la brique (entre minSize et maxSize)
local minSize = partTemplate.Size.X / 2
local maxSize = partTemplate.Size.X
local currentHeight = baseHeight
-- 1) Construction : toutes les briques sont créées ANCRÉES et SANS collision
for i = 1, PYRAMID_PARTS_COUNT do
local part = partTemplate:Clone()
local partHeight = math.random(1, 3) -- Hauteur aléatoire entre 1 et 3
currentHeight = currentHeight + partHeight / 2
part.Name = "pyramid" -- Nom pour identifier les briques de la pyramide
part.Anchored = true -- Empêche la brique de bouger pendant la construction
part.CanCollide = false -- Désactive les collisions pendant la construction
part.CanTouch = false -- Désactive la détection de contact
part.Transparency = 0 -- Rend la brique visible
part.Size = Vector3.new(
math.random(minSize, maxSize), -- Largeur aléatoire
partHeight, -- Hauteur aléatoire
math.random(minSize, maxSize) -- Profondeur aléatoire
)
-- Positionne la brique au centre de la pyramide, avec une rotation aléatoire
part.CFrame = CFrame.new(
partTemplate.Position.X,
currentHeight,
partTemplate.Position.Z
) * CFrame.Angles(0, math.rad(math.random(0, 360)), 0) -- Rotation aléatoire autour de l'axe Y
-- Copie les propriétés visuelles du modèle
part.BrickColor = partTemplate.BrickColor
part.Material = partTemplate.Material
-- Ajoute la brique au dossier
part.Parent = folder
-- Réduit la taille pour les prochaines briques
minSize = minSize - PYRAMID_MIN_SIZE_STEP
maxSize = maxSize - PYRAMID_MAX_SIZE_STEP
currentHeight = currentHeight + partHeight / 2
end
-- 2) Activation : une fois la pyramide construite, on active la physique
task.wait(0.1) -- Petit délai pour s'assurer que tout est bien positionné
for _, part in ipairs(folder:GetChildren()) do
part.CanCollide = true -- Active les collisions
part.CanTouch = true -- Active la détection de contact
part.Anchored = false -- Permet à la brique de bouger (physique)
end
end
-- ==================== UTILITAIRES ====================
-- Applique une caméra first person (vue première personne verrouillée et zoomée)
local function lockFirstPersonCamera(player)
player.CameraMode = Enum.CameraMode.LockFirstPerson
player.CameraMinZoomDistance = 0.5
player.CameraMaxZoomDistance = 0.5
end
-- Restaure la caméra classique du joueur
local function resetCamera(player)
player.CameraMode = Enum.CameraMode.Classic
player.CameraMaxZoomDistance = 400
task.wait(0.1) -- Attend 0.1 seconde pour que la caméra se réinitialise correctement
player.CameraMinZoomDistance = 10
end
-- Récupère le joueur à partir d'une partie touchée
local function getPlayerFromTouchedPart(hit)
local character = hit.Parent
if not character then return nil end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return nil end
return Players:GetPlayerFromCharacter(character)
end
-- ==================== ÉVÉNEMENTS : ZONE DE JEU ====================
-- Quand un joueur touche la hitbox
hitbox.Touched:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
-- Anti-spam : ignore si le joueur est déjà dans la zone
if playersData[player] and playersData[player].inZone then return end
playersData[player] = { inZone = true, nbrPoints = 5 } -- 5 points de départ
print(player.Name .. " est entré dans la zone de lancer !")
lockFirstPersonCamera(player)
end)
-- Quand un joueur quitte la hitbox
hitbox.TouchEnded:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
if playersData[player] and playersData[player].inZone==false then return end
-- Attend 0.5 seconde pour éviter les faux positifs (si le joueur touche encore la hitbox avec une autre partie du corps)
task.wait(0.5)
-- Vérifie qu'aucune partie du personnage ne touche encore la hitbox
local character = player.Character
local stillTouching = false
if character then
for _, part in pairs(hitbox:GetTouchingParts()) do
if part.Parent == character then
stillTouching = true
break
end
end
end
if stillTouching then return end
if playersData[player] and playersData[player].inZone==false then return end
-- Le joueur a vraiment quitté la zone
playersData[player].inZone = false
print(player.Name .. " a quitté la zone de lancer.")
resetCamera(player)
end)
-- ==================== INITIALISATION ====================
rebuildPyramid(pyramidPartTemplate) -- Construit la pyramide au démarrage
Étape 6 : Proposer le lancement d’une balle avec un ProximityPrompt
But : Permettre au joueur de lancer une balle en appuyant sur un bouton. Concepts clés : ProximityPrompt, clonage d’objets, physique, RemoteEvent (optionnel).
Explications
Un ProximityPrompt est un bouton qui apparaît quand le joueur est proche d’un objet. Quand le joueur appuie sur le bouton (par défaut, la touche E), on peut :
Cloner la balle depuis ReplicatedStorage.
Positionner la balle devant le joueur.
Appliquer une force pour la lancer.
Exemple concret :
Le joueur voit un bouton « Lancer la balle » quand il est dans la hitbox.
En appuyant sur E, une balle est lancée devant lui.
Code Lua
Ajoute ce code au Script :
-- ==================== UTILITAIRES ====================
-- Fait apparaître une balle devant le joueur et lui applique une impulsion
local function fireBall(player)
local character = player.Character or player.CharacterAdded:Wait()
local hrp = character:WaitForChild("HumanoidRootPart")
local ball = ballTemplate:Clone()
ball.Parent = workspace
ball.CFrame = hrp.CFrame * CFrame.new(-2, 2, 0) -- Position devant le joueur
-- Applique une vitesse initiale vers l'avant + vers le haut
ball.AssemblyLinearVelocity = hrp.CFrame.LookVector * 60 + Vector3.new(0, 60, 0)
-- Détruit la balle après 2 secondes
game:GetService("Debris"):AddItem(ball, 2)
end
-- ==================== ÉVÉNEMENT : PROXIMITY PROMPT ====================
prompt.Triggered:Connect(function(player)
local data = playersData[player]
if not data or not data.inZone then return end -- Vérifie que le joueur est bien dans la zone
fireBall(player)
print(player.Name .. " a lancé une balle !")
end)
Exemple du code modifié :
-- ==================== SERVICES ====================
local Workspace = game:GetService("Workspace")
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
-- ==================== CONFIGURATION ====================
local PYRAMID_PARTS_COUNT = 8 -- Nombre de briques dans la pyramide
local PYRAMID_MIN_SIZE_STEP = 0.2 -- Réduction minimale de la taille à chaque brique
local PYRAMID_MAX_SIZE_STEP = 0.5 -- Réduction maximale de la taille à chaque brique
-- ==================== RÉFÉRENCES SCÈNE ====================
local breakPyramidFolder = Workspace:WaitForChild("BreakPyramid")
local pyramidPartTemplate = breakPyramidFolder:WaitForChild("Part")
local ballTemplate = breakPyramidFolder:WaitForChild("Ball")
local hitbox = breakPyramidFolder:WaitForChild("HitBox")
local prompt = hitbox.Attachment:WaitForChild("ProximityPrompt")
-- ==================== ÉTAT DU JEU ====================
-- playersData[player] = { inZone = bool, nbrPoints = number }
local playersData = {}
-- on retire le modèle du workspace, il ne sert que de gabarit à cloner
ballTemplate.Parent = ServerStorage
-- ==================== FONCTION : CONSTRUIRE LA PYRAMIDE ====================
-- (Re)construit la pyramide de briques à partir du gabarit
local function rebuildPyramid(partTemplate)
local baseHeight = partTemplate.Position.Y + partTemplate.Size.Y / 2
local parentFolder = partTemplate.Parent
-- Supprime l'ancienne pyramide si elle existe
local existingFolder = parentFolder:FindFirstChild("PyramidParts")
if existingFolder then
existingFolder:Destroy()
end
-- Crée un dossier pour les briques de la pyramide
local folder = Instance.new("Folder")
folder.Name = "PyramidParts"
folder.Parent = parentFolder
-- Taille initiale de la brique (entre minSize et maxSize)
local minSize = partTemplate.Size.X / 2
local maxSize = partTemplate.Size.X
local currentHeight = baseHeight
-- 1) Construction : toutes les briques sont créées ANCRÉES et SANS collision
for i = 1, PYRAMID_PARTS_COUNT do
local part = partTemplate:Clone()
local partHeight = math.random(1, 3) -- Hauteur aléatoire entre 1 et 3
currentHeight = currentHeight + partHeight / 2
part.Name = "pyramid" -- Nom pour identifier les briques de la pyramide
part.Anchored = true -- Empêche la brique de bouger pendant la construction
part.CanCollide = false -- Désactive les collisions pendant la construction
part.CanTouch = false -- Désactive la détection de contact
part.Transparency = 0 -- Rend la brique visible
part.Size = Vector3.new(
math.random(minSize, maxSize), -- Largeur aléatoire
partHeight, -- Hauteur aléatoire
math.random(minSize, maxSize) -- Profondeur aléatoire
)
-- Positionne la brique au centre de la pyramide, avec une rotation aléatoire
part.CFrame = CFrame.new(
partTemplate.Position.X,
currentHeight,
partTemplate.Position.Z
) * CFrame.Angles(0, math.rad(math.random(0, 360)), 0) -- Rotation aléatoire autour de l'axe Y
-- Copie les propriétés visuelles du modèle
part.BrickColor = partTemplate.BrickColor
part.Material = partTemplate.Material
-- Ajoute la brique au dossier
part.Parent = folder
-- Réduit la taille pour les prochaines briques
minSize = minSize - PYRAMID_MIN_SIZE_STEP
maxSize = maxSize - PYRAMID_MAX_SIZE_STEP
currentHeight = currentHeight + partHeight / 2
end
-- 2) Activation : une fois la pyramide construite, on active la physique
task.wait(0.1) -- Petit délai pour s'assurer que tout est bien positionné
for _, part in ipairs(folder:GetChildren()) do
part.CanCollide = true -- Active les collisions
part.CanTouch = true -- Active la détection de contact
part.Anchored = false -- Permet à la brique de bouger (physique)
end
end
-- ==================== UTILITAIRES ====================
-- Fait apparaître une balle devant le joueur et lui applique une impulsion
local function fireBall(player)
local character = player.Character or player.CharacterAdded:Wait()
local hrp = character:WaitForChild("HumanoidRootPart")
local ball = ballTemplate:Clone()
ball.Parent = workspace
ball.CFrame = hrp.CFrame * CFrame.new(-2, 2, 0) -- Position devant le joueur
-- Applique une vitesse initiale vers l'avant + vers le haut
ball.AssemblyLinearVelocity = hrp.CFrame.LookVector * 60 + Vector3.new(0, 60, 0)
-- Détruit la balle après 2 secondes
game:GetService("Debris"):AddItem(ball, 2)
end
-- Applique une caméra first person (vue première personne verrouillée et zoomée)
local function lockFirstPersonCamera(player)
player.CameraMode = Enum.CameraMode.LockFirstPerson
player.CameraMinZoomDistance = 0.5
player.CameraMaxZoomDistance = 0.5
end
-- Restaure la caméra classique du joueur
local function resetCamera(player)
player.CameraMode = Enum.CameraMode.Classic
player.CameraMaxZoomDistance = 400
task.wait(0.1) -- Attend 0.1 seconde pour que la caméra se réinitialise correctement
player.CameraMinZoomDistance = 10
end
-- Récupère le joueur à partir d'une partie touchée
local function getPlayerFromTouchedPart(hit)
local character = hit.Parent
if not character then return nil end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return nil end
return Players:GetPlayerFromCharacter(character)
end
-- ==================== ÉVÉNEMENTS : ZONE DE JEU ====================
-- Quand un joueur touche la hitbox
hitbox.Touched:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
-- Anti-spam : ignore si le joueur est déjà dans la zone
if playersData[player] and playersData[player].inZone then return end
playersData[player] = { inZone = true, nbrPoints = 5 } -- 5 points de départ
print(player.Name .. " est entré dans la zone de lancer !")
lockFirstPersonCamera(player)
prompt.Enabled = true
end)
-- Quand un joueur quitte la hitbox
hitbox.TouchEnded:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
if playersData[player] and playersData[player].inZone==false then return end
-- Attend 0.5 seconde pour éviter les faux positifs (si le joueur touche encore la hitbox avec une autre partie du corps)
task.wait(0.5)
-- Vérifie qu'aucune partie du personnage ne touche encore la hitbox
local character = player.Character
local stillTouching = false
if character then
for _, part in pairs(hitbox:GetTouchingParts()) do
if part.Parent == character then
stillTouching = true
break
end
end
end
if stillTouching then return end
if playersData[player] and playersData[player].inZone==false then return end
-- Le joueur a vraiment quitté la zone
playersData[player].inZone = false
print(player.Name .. " a quitté la zone de lancer.")
resetCamera(player)
prompt.Enabled = false
end)
-- ==================== ÉVÉNEMENT : PROXIMITY PROMPT ====================
prompt.Triggered:Connect(function(player)
local data = playersData[player]
if not data or not data.inZone then return end -- Vérifie que le joueur est bien dans la zone
fireBall(player)
print(player.Name .. " a lancé une balle !")
end)
-- ==================== INITIALISATION ====================
rebuildPyramid(pyramidPartTemplate) -- Construit la pyramide au démarrage
prompt.Enabled = false -- Masque le prompt au démarrage
Explications du code
Ligne
Explication
ballTemplate.Parent = ServerStorage
Déplace le modèle de balle dans ServerStorage pour le cloner plus tard.
prompt.Triggered:Connect(...)
Écoute l’événement quand le joueur appuie sur le bouton.
ballTemplate:Clone()
Crée une copie de la balle.
ball.CFrame = hrp.CFrame * CFrame.new(-2, 2, 0)
Positionne la balle devant le joueur (2 unités devant, 2 unités au-dessus).
ball.AssemblyLinearVelocity = ...
Applique une vitesse à la balle (60 unités vers l’avant + 60 unités vers le haut).
Debris:AddItem(ball, 2)
Détruit la balle après 2 secondes.
À toi de jouer !
Déplace le modèle de balle dans ServerStorage (si ce n’est pas déjà fait).
local ServerStorage = game:GetService(« ServerStorage »)
local ballTemplate = breakPyramidFolder:WaitForChild(« Ball »)r
ballTemplate.Parent = ServerStorage
Copie le code dans le Script.
Teste :
Entre dans la hitbox.
Appuie sur E (ou la touche définie pour le ProximityPrompt) : une balle doit être lancée !
Modifie :
Change la position de la balle (CFrame.new(-2, 2, 0) en CFrame.new(-3, 1, 0)).
Change la vitesse de la balle (60 en 80 pour un lancer plus fort).
QCM / Questions
À quoi sert prompt.Triggered:Connect(...) ?
a) À détecter quand le joueur appuie sur le bouton du ProximityPrompt.
b) À détruire le ProximityPrompt.
c) À changer la couleur du bouton.
Que fait ball.AssemblyLinearVelocity = hrp.CFrame.LookVector * 60 + Vector3.new(0, 60, 0) ?
a) Change la taille de la balle.
b) Détruit la balle.
c) Applique une vitesse à la balle vers l’avant et vers le haut.
But : Vérifier si toutes les briques de la pyramide ont été détruites. Concepts clés : Détection de collision, boucles for, conditions if, compteur.
Explications
Pour vérifier si la pyramide est détruite, on peut :
Compter le nombre de briques encore debout dans la zone de détection (ControlPyramid).
Comparer ce nombre à 0 : si aucune brique ne reste, la pyramide est détruite.
Exemple concret :
Après chaque lancer, on vérifie si des briques touchent encore ControlPyramid.
Si aucune brique ne touche ControlPyramid, le joueur a réussi !
Code Lua
Ajoute ces fonctions au Script :
-- ==================== UTILITAIRES ====================
-- Compte le nombre de briques de la pyramide encore présentes dans la zone de détection
local function countStandingPyramidParts()
local touchingParts = workspace:GetPartsInPart(pyramidDetector)
local total = 0
for _, part in ipairs(touchingParts) do
if part.Name == "pyramid" then
total += 1
end
end
return total
end
-- Applique un recul physique au joueur (utilisé après un strike réussi)
local function knockbackPlayer(player, force)
local character = player.Character
if not character then return end
local hrp = character:FindFirstChild("HumanoidRootPart")
if not hrp then return end
hrp.AssemblyLinearVelocity = hrp.CFrame.LookVector * -force
end
-- Fait sortir proprement un joueur de la zone : reset caméra + pyramide + état
local function exitZone(player)
local data = playersData[player]
if not data or not data.inZone then return end
data.inZone = false
prompt.Enabled = true -- Réactive le ProximityPrompt
resetCamera(player)
rebuildPyramid(pyramidPartTemplate) -- Reconstruit la pyramide
end
Exemple du code modifié :
-- ==================== SERVICES ====================
local Workspace = game:GetService("Workspace")
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
-- ==================== CONFIGURATION ====================
local PYRAMID_PARTS_COUNT = 8 -- Nombre de briques dans la pyramide
local PYRAMID_MIN_SIZE_STEP = 0.2 -- Réduction minimale de la taille à chaque brique
local PYRAMID_MAX_SIZE_STEP = 0.5 -- Réduction maximale de la taille à chaque brique
-- ==================== RÉFÉRENCES SCÈNE ====================
local breakPyramidFolder = Workspace:WaitForChild("BreakPyramid")
local pyramidPartTemplate = breakPyramidFolder:WaitForChild("Part")
local ballTemplate = breakPyramidFolder:WaitForChild("Ball")
local hitbox = breakPyramidFolder:WaitForChild("HitBox")
local prompt = hitbox.Attachment:WaitForChild("ProximityPrompt")
local pyramidDetector = breakPyramidFolder:WaitForChild("ControlPyramid") -- zone qui détecte les briques encore debout
-- ==================== ÉTAT DU JEU ====================
-- playersData[player] = { inZone = bool, nbrPoints = number }
local playersData = {}
-- on retire le modèle du workspace, il ne sert que de gabarit à cloner
ballTemplate.Parent = ServerStorage
-- ==================== FONCTION : CONSTRUIRE LA PYRAMIDE ====================
-- (Re)construit la pyramide de briques à partir du gabarit
local function rebuildPyramid(partTemplate)
local baseHeight = partTemplate.Position.Y + partTemplate.Size.Y / 2
local parentFolder = partTemplate.Parent
-- Supprime l'ancienne pyramide si elle existe
local existingFolder = parentFolder:FindFirstChild("PyramidParts")
if existingFolder then
existingFolder:Destroy()
end
-- Crée un dossier pour les briques de la pyramide
local folder = Instance.new("Folder")
folder.Name = "PyramidParts"
folder.Parent = parentFolder
-- Taille initiale de la brique (entre minSize et maxSize)
local minSize = partTemplate.Size.X / 2
local maxSize = partTemplate.Size.X
local currentHeight = baseHeight
-- 1) Construction : toutes les briques sont créées ANCRÉES et SANS collision
for i = 1, PYRAMID_PARTS_COUNT do
local part = partTemplate:Clone()
local partHeight = math.random(1, 3) -- Hauteur aléatoire entre 1 et 3
currentHeight = currentHeight + partHeight / 2
part.Name = "pyramid" -- Nom pour identifier les briques de la pyramide
part.Anchored = true -- Empêche la brique de bouger pendant la construction
part.CanCollide = false -- Désactive les collisions pendant la construction
part.CanTouch = false -- Désactive la détection de contact
part.Transparency = 0 -- Rend la brique visible
part.Size = Vector3.new(
math.random(minSize, maxSize), -- Largeur aléatoire
partHeight, -- Hauteur aléatoire
math.random(minSize, maxSize) -- Profondeur aléatoire
)
-- Positionne la brique au centre de la pyramide, avec une rotation aléatoire
part.CFrame = CFrame.new(
partTemplate.Position.X,
currentHeight,
partTemplate.Position.Z
) * CFrame.Angles(0, math.rad(math.random(0, 360)), 0) -- Rotation aléatoire autour de l'axe Y
-- Copie les propriétés visuelles du modèle
part.BrickColor = partTemplate.BrickColor
part.Material = partTemplate.Material
-- Ajoute la brique au dossier
part.Parent = folder
-- Réduit la taille pour les prochaines briques
minSize = minSize - PYRAMID_MIN_SIZE_STEP
maxSize = maxSize - PYRAMID_MAX_SIZE_STEP
currentHeight = currentHeight + partHeight / 2
end
-- 2) Activation : une fois la pyramide construite, on active la physique
task.wait(0.1) -- Petit délai pour s'assurer que tout est bien positionné
for _, part in ipairs(folder:GetChildren()) do
part.CanCollide = true -- Active les collisions
part.CanTouch = true -- Active la détection de contact
part.Anchored = false -- Permet à la brique de bouger (physique)
end
end
-- ==================== UTILITAIRES ====================
-- Fait apparaître une balle devant le joueur et lui applique une impulsion
local function fireBall(player)
local character = player.Character or player.CharacterAdded:Wait()
local hrp = character:WaitForChild("HumanoidRootPart")
local ball = ballTemplate:Clone()
ball.Parent = workspace
ball.CFrame = hrp.CFrame * CFrame.new(-2, 2, 0) -- Position devant le joueur
-- Applique une vitesse initiale vers l'avant + vers le haut
ball.AssemblyLinearVelocity = hrp.CFrame.LookVector * 60 + Vector3.new(0, 60, 0)
-- Détruit la balle après 2 secondes
game:GetService("Debris"):AddItem(ball, 2)
end
-- Applique une caméra first person (vue première personne verrouillée et zoomée)
local function lockFirstPersonCamera(player)
player.CameraMode = Enum.CameraMode.LockFirstPerson
player.CameraMinZoomDistance = 0.5
player.CameraMaxZoomDistance = 0.5
end
-- Restaure la caméra classique du joueur
local function resetCamera(player)
player.CameraMode = Enum.CameraMode.Classic
player.CameraMaxZoomDistance = 400
task.wait(0.1) -- Attend 0.1 seconde pour que la caméra se réinitialise correctement
player.CameraMinZoomDistance = 10
end
-- Récupère le joueur à partir d'une partie touchée
local function getPlayerFromTouchedPart(hit)
local character = hit.Parent
if not character then return nil end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return nil end
return Players:GetPlayerFromCharacter(character)
end
-- Compte le nombre de briques de la pyramide encore présentes dans la zone de détection
local function countStandingPyramidParts()
local touchingParts = workspace:GetPartsInPart(pyramidDetector)
local total = 0
for _, part in ipairs(touchingParts) do
if part.Name == "pyramid" then
total += 1
end
end
return total
end
-- Applique un recul physique au joueur (utilisé après un strike réussi)
local function knockbackPlayer(player, force)
local character = player.Character
if not character then return end
local hrp = character:FindFirstChild("HumanoidRootPart")
if not hrp then return end
hrp.AssemblyLinearVelocity = hrp.CFrame.LookVector * -force
end
-- Fait sortir proprement un joueur de la zone : reset caméra + pyramide + état
local function exitZone(player)
local data = playersData[player]
if not data or not data.inZone then return end
data.inZone = false
prompt.Enabled = true -- Réactive le ProximityPrompt
resetCamera(player)
end
-- ==================== ÉVÉNEMENTS : ZONE DE JEU ====================
-- Quand un joueur touche la hitbox
hitbox.Touched:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
-- Anti-spam : ignore si le joueur est déjà dans la zone
if playersData[player] and playersData[player].inZone then return end
playersData[player] = { inZone = true, nbrPoints = 5 } -- 5 points de départ
print(player.Name .. " est entré dans la zone de lancer !")
lockFirstPersonCamera(player)
prompt.Enabled = true
end)
-- Quand un joueur quitte la hitbox
hitbox.TouchEnded:Connect(function(hit)
local player = getPlayerFromTouchedPart(hit)
if not player then return end
if playersData[player] and playersData[player].inZone==false then return end
-- Attend 0.5 seconde pour éviter les faux positifs (si le joueur touche encore la hitbox avec une autre partie du corps)
task.wait(0.5)
-- Vérifie qu'aucune partie du personnage ne touche encore la hitbox
local character = player.Character
local stillTouching = false
if character then
for _, part in pairs(hitbox:GetTouchingParts()) do
if part.Parent == character then
stillTouching = true
break
end
end
end
if stillTouching then return end
if playersData[player] and playersData[player].inZone==false then return end
-- Le joueur a vraiment quitté la zone
playersData[player].inZone = false
print(player.Name .. " a quitté la zone de lancer.")
resetCamera(player)
prompt.Enabled = false
end)
-- ==================== ÉVÉNEMENT : PROXIMITY PROMPT ====================
prompt.Triggered:Connect(function(player)
local data = playersData[player]
if not data or not data.inZone then return end -- Vérifie que le joueur est bien dans la zone
fireBall(player)
-- Attend 2 secondes pour laisser le temps à la balle de toucher les briques
prompt.Enabled = false
task.wait(1)
prompt.Enabled = true
if countStandingPyramidParts() > 0 then
-- Raté : il reste des briques debout
if data.nbrPoints > 0 then
data.nbrPoints -= 1
print(player.Name .. " a raté ! Il lui reste " .. data.nbrPoints .. " points.")
end
prompt.Enabled = true -- Réactive le bouton
else
-- Strike : plus aucune brique debout
print(player.Name .. " a détruit la pyramide !")
knockbackPlayer(player, 150) -- Applique un recul au joueur
exitZone(player) -- Fait sortir le joueur de la zone
task.wait(2)
rebuildPyramid(pyramidPartTemplate) -- Reconstruit la pyramide
end
end)
-- ==================== INITIALISATION ====================
rebuildPyramid(pyramidPartTemplate) -- Construit la pyramide au démarrage
prompt.Enabled = false -- Masque le prompt au démarrage
Explications du code
Ligne
Explication
workspace:GetPartsInPart(pyramidDetector)
Récupère toutes les parties qui touchent ControlPyramid.
for _, part in ipairs(touchingParts) do
Parcourt toutes les parties en contact avec ControlPyramid.
if part.Name == "pyramid" then
Vérifie si la partie est une brique de la pyramide.
countStandingPyramidParts()
Compte le nombre de briques encore debout.
knockbackPlayer(player, 150)
Applique un recul au joueur (force de 150).
exitZone(player)
Fait sortir le joueur de la zone et reconstruit la pyramide.
À toi de jouer !
Copie le code dans le Script.
Teste :
Entre dans la hitbox et lance des balles jusqu’à détruire la pyramide.
Observe le recul du joueur quand il réussit.
Modifie :
Change la force du recul (150 en 200).
Change le nombre de points perdus en cas d’échec (data.nbrPoints -= 2).
QCM / Questions
À quoi sert workspace:GetPartsInPart(pyramidDetector) ?
a) À détruire ControlPyramid.
b) À récupérer toutes les parties qui touchent ControlPyramid.
c) À changer la couleur de ControlPyramid.
Que fait knockbackPlayer(player, 150) ?
a) Applique un recul au joueur avec une force de 150.
b) Détruit le joueur.
c) Affiche un message.
Pourquoi utilise-t-on task.wait(2) après fireBall(player) ?
a) Pour attendre que la balle ait le temps de toucher les briques.
b) Pour ralentir le jeu.
c) Pour afficher un message.
Pour aller plus loin
Ajoute un système de score : Utilise ScoreManager pour ajouter des points au joueur quand il réussit.
Affiche un message de victoire à l’écran quand le joueur détruit la pyramide.
Étape 3 : Démarrer le script avec une gestion de hitbox
But : Détecter quand un joueur entre dans la zone de lancer. Concepts clés : Événements, fonctions, détection de collision.
Explications
Une hitbox est une zone invisible qui détecte quand un joueur entre ou sort. En Lua, on utilise l’événement .Touched pour savoir quand un objet touche la hitbox.
Exemple :
-- ==================== RÉFÉRENCES SCÈNE ====================
local basketFolder = workspace:WaitForChild("Basket")
local hitbox = basketFolder:WaitForChild("HitBox")
hitbox.Touched:Connect(function(hit)
print("Quelque chose a touché la hitbox !")
end)
Touched : Événement déclenché quand un objet entre en contact avec la hitbox.
hit : L’objet qui a touché la hitbox (ici, une partie du personnage du joueur).
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
-- ==================== RÉFÉRENCES SCÈNE ====================
local basketFolder = workspace:WaitForChild("Basket")
local hitbox = basketFolder:WaitForChild("HitBox")
-- ==================== ÉVÉNEMENTS : ZONE DE JEU
-- Quand un joueur touche la hitbox
hitbox.Touched:Connect(function(hit)
-- Vérifie si l'objet qui a touché est un personnage de joueur
local character = hit.Parent
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
local player = Players:GetPlayerFromCharacter(character)
if player then
print(player.Name .. " est entré dans la zone de lancer !")
end
end
end)
Explications du code
Ligne
Explication
local Players = game:GetService("Players")
Récupère le service Players pour accéder aux joueurs.
hitbox.Touched:Connect(...)
Écoute l’événement Touched sur la hitbox.
hit.Parent
Récupère le parent de l’objet qui a touché (ici, le personnage du joueur).
FindFirstChildOfClass("Humanoid")
Vérifie si le parent a un Humanoid (donc c’est un personnage joueur).
Players:GetPlayerFromCharacter(character)
Récupère le joueur associé au personnage.
Copie ce code dans un Script (dans ServerScriptService).
Teste :
Entre dans la hitbox avec ton personnage.
Ouvre la fenêtre de sortie (Affichage > Sortie) pour voir le message.
QCM / Questions
À quoi sert hitbox.Touched:Connect(...) ?
a) À détruire la hitbox.
b) À exécuter du code quand un objet touche la hitbox.
c) À changer la couleur de la hitbox.
Que fait Players:GetPlayerFromCharacter(character) ?
a) Crée un nouveau personnage.
b) Récupère le joueur associé à un personnage.
c) Supprime le personnage.
Étape 4 : Changer la vision du joueur en première personne
But : Passer en vue première personne quand le joueur entre dans la zone. Concepts clés : Propriétés des objets, fonctions utilitaires.
Explications
En Roblox, on peut modifier la caméra du joueur pour :
LockFirstPerson : Vue à la première personne (comme dans un FPS).
-- Applique une caméra first person (vue première personne verrouillée et zoomée)
local function lockFirstPersonCamera(player)
player.CameraMode = Enum.CameraMode.LockFirstPerson
player.CameraMinZoomDistance = 0.5
player.CameraMaxZoomDistance = 0.5
end
-- Restaure la caméra classique du joueur
local function resetCamera(player)
player.CameraMode = Enum.CameraMode.Classic
player.CameraMaxZoomDistance = 400
task.wait(0.1) -- Attend 0.1 seconde pour que la caméra se réinitialise
player.CameraMinZoomDistance = 10
end
Puis modifie l’événement Touched :
hitbox.Touched:Connect(function(hit)
local character = hit.Parent
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
local player = Players:GetPlayerFromCharacter(character)
if player then
print(player.Name .. " est entré dans la zone de lancer !")
lockFirstPersonCamera(player) -- Passe en première personne
end
end
end)
Entre dans la hitbox : la vue doit passer en première personne.
Modifie :
Change CameraMinZoomDistance à 1.0 et observe la différence.
QCM / Questions
Que fait player.CameraMode = Enum.CameraMode.LockFirstPerson ? a) Passe en vue à la troisième personne. b) Passe en vue à la première personne. c) Désactive la caméra.
Rajoute dans ton code pour modifier la caméra en FirstPerson lorsque le joueur rentre dans la zone de tir, puis modifie la caméra en normal lorsque le joueur quitte la zone de tir :
-- ==================== CONFIGURATION ====================
local EXIT_CONFIRM_DELAY = 0.5 -- Délai avant de confirmer la sortie de zone (anti faux-positif)
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
-- ==================== RÉFÉRENCES SCÈNE ====================
local basketFolder = workspace:WaitForChild("Basket")
local hitbox = basketFolder:WaitForChild("HitBox")
-- ==================== ÉTAT DU JEU ====================
-- playersData[player] = { inZone = bool, nbrPoints = number }
local playersData = {}
-- Applique une caméra first person (vue première personne verrouillée et zoomée)
local function lockFirstPersonCamera(player)
player.CameraMode = Enum.CameraMode.LockFirstPerson
player.CameraMinZoomDistance = 0.5
player.CameraMaxZoomDistance = 0.5
end
-- Restaure la caméra classique du joueur
local function resetCamera(player)
player.CameraMode = Enum.CameraMode.Classic
player.CameraMaxZoomDistance = 400
task.wait(0.1) -- Attend 0.1 seconde pour que la caméra se réinitialise
player.CameraMinZoomDistance = 10
end
-- Fait sortir proprement un joueur de la zone : reset caméra + pyramide + état
local function exitZone(player)
local data = playersData[player]
if not data or not data.inZone then return end
data.inZone = false
resetCamera(player)
end
-- ==================== ÉVÉNEMENTS : ZONE DE JEU
-- Quand un joueur touche la hitbox
hitbox.Touched:Connect(function(hit)
-- Vérifie si l'objet qui a touché est un personnage de joueur
local character = hit.Parent
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
local player = Players:GetPlayerFromCharacter(character)
if player then
-- Anti-spam : on ignore si le joueur est déjà comptabilisé dans la zone
local data = playersData[player]
if data and data.inZone then return end
playersData[player] = { inZone = true, fire = 0 }
print(player.Name .. " est entré dans la zone de lancer !")
lockFirstPersonCamera(player) -- Passe en première personne
end
end
end)
-- Quand une partie du personnage quitte la hitbox, on vérifie si c'est un joueur
hitbox.TouchEnded:Connect(function(hit)
-- Vérifie si l'objet qui a touché est un personnage de joueur
local character = hit.Parent
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
local player = Players:GetPlayerFromCharacter(character)
if player then
task.wait(EXIT_CONFIRM_DELAY)
-- On vérifie qu'aucune partie du personnage ne touche encore la hitbox
-- (utile car Touched/TouchEnded se déclenchent par membre du corps)
local character = player.Character
local stillTouching = false
if character then
for _, part in pairs(hitbox:GetTouchingParts()) do
if part.Parent == character then
stillTouching = true
break
end
end
end
if stillTouching then return end
exitZone(player)
end
end
end)
Pourquoi utilise-t-on task.wait(0.1) dans resetCamera ? a) Pour attendre que la caméra se réinitialise correctement. b) Pour ralentir le jeu. c) Pour afficher un message.
Étape 5 : Écrire une interface utilisateur pour indiquer d’utiliser la touche F
But : Afficher un message à l’écran pour guider le joueur. Concepts clés : LocalScript, ** interfaces utilisateur (GUI)**, TextLabel.
Explications
Pour afficher un message à l’écran, on utilise :
Un ScreenGui (interface à l’écran).
Un TextLabel (texte affiché).
Un LocalScript (script côté client, car l’interface est visible uniquement par le joueur).
Exemple :
local player = game.Players.LocalPlayer
local gui = Instance.new("ScreenGui")
local label = Instance.new("TextLabel")
label.Text = "Appuie sur F pour lancer !"
label.Parent = gui
gui.Parent = player:WaitForChild("PlayerGui")
Code Lua
Crée un LocalScript dans StarterPlayerScripts.
Copie ce code :
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- ==================== RÉFÉRENCES ====================
local player = Players.LocalPlayer
local readyEvent = ReplicatedStorage:WaitForChild("ReadyEvent")
-- ==================== INTERFACE ====================
local gui = Instance.new("ScreenGui")
local label = Instance.new("TextLabel")
-- Configure le TextLabel
label.Text = "Appuie sur F pour lancer !"
label.Size = UDim2.new(0, 200, 0, 50) -- Largeur: 200, Hauteur: 50
label.Position = UDim2.new(0.5, -100, 0.9, -25) -- Centre en bas
label.BackgroundColor3 = Color3.fromRGB(0, 0, 0) -- Fond noir
label.TextColor3 = Color3.fromRGB(255, 255, 255) -- Texte blanc
label.TextScaled = true -- Le texte s'adapte à la taille
label.Parent = gui
gui.Parent = player:WaitForChild("PlayerGui")
-- Désactive l'affichage au début
gui.Enabled = false
-- Écoute l'événement ReadyEvent pour activer/désactiver l'interface
readyEvent.OnClientEvent:Connect(function(isReady)
gui.Enabled = isReady
end)
Modifie le script sur le serveur pour envoyer l’évènement readyevent avec l’instruction readyEvent:FireClient(player, true) :
-- ==================== CONFIGURATION ====================
local EXIT_CONFIRM_DELAY = 0.5 -- Délai avant de confirmer la sortie de zone (anti faux-positif)
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local fireInputEvent = ReplicatedStorage:WaitForChild("FireInputEvent")
local readyEvent = ReplicatedStorage:WaitForChild("ReadyEvent")
-- ==================== RÉFÉRENCES SCÈNE ====================
local basketFolder = workspace:WaitForChild("Basket")
local hitbox = basketFolder:WaitForChild("HitBox")
-- ==================== ÉTAT DU JEU ====================
-- playersData[player] = { inZone = bool, nbrPoints = number }
local playersData = {}
-- Applique une caméra first person (vue première personne verrouillée et zoomée)
local function lockFirstPersonCamera(player)
player.CameraMode = Enum.CameraMode.LockFirstPerson
player.CameraMinZoomDistance = 0.5
player.CameraMaxZoomDistance = 0.5
end
-- Restaure la caméra classique du joueur
local function resetCamera(player)
player.CameraMode = Enum.CameraMode.Classic
player.CameraMaxZoomDistance = 400
task.wait(0.1) -- Attend 0.1 seconde pour que la caméra se réinitialise
player.CameraMinZoomDistance = 10
end
-- Fait sortir proprement un joueur de la zone : reset caméra + pyramide + état
local function exitZone(player)
local data = playersData[player]
if not data or not data.inZone then return end
data.inZone = false
resetCamera(player)
end
-- ==================== ÉVÉNEMENTS : ZONE DE JEU
-- Quand un joueur touche la hitbox
hitbox.Touched:Connect(function(hit)
-- Vérifie si l'objet qui a touché est un personnage de joueur
local character = hit.Parent
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
local player = Players:GetPlayerFromCharacter(character)
if player then
-- Anti-spam : on ignore si le joueur est déjà comptabilisé dans la zone
local data = playersData[player]
if data and data.inZone then return end
playersData[player] = { inZone = true, fire = 0 }
print(player.Name .. " est entré dans la zone de lancer !")
lockFirstPersonCamera(player) -- Passe en première personne
-- Envoie d'un évènement au client pour lui dire qu'il peut lancer le ballon
readyEvent:FireClient(player, true) -- prêt à lancer le ballon
end
end
end)
-- Quand une partie du personnage quitte la hitbox, on vérifie si c'est un joueur
hitbox.TouchEnded:Connect(function(hit)
-- Vérifie si l'objet qui a touché est un personnage de joueur
local character = hit.Parent
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
local player = Players:GetPlayerFromCharacter(character)
if player then
task.wait(EXIT_CONFIRM_DELAY)
-- On vérifie qu'aucune partie du personnage ne touche encore la hitbox
-- (utile car Touched/TouchEnded se déclenchent par membre du corps)
local character = player.Character
local stillTouching = false
if character then
for _, part in pairs(hitbox:GetTouchingParts()) do
if part.Parent == character then
stillTouching = true
break
end
end
end
if stillTouching then return end
exitZone(player)
-- Envoie d'un évènement au client pour lui dire qu'il ne peut plus lancer le ballon
readyEvent:FireClient(player, false)
end
end
end)
Explications du code
Ligne
Explication
local player = Players.LocalPlayer
Récupère le joueur local (celui qui exécute le script).
Instance.new("ScreenGui")
Crée une interface à l’écran.
label.Text = "Appuie sur F pour lancer !"
Définit le texte affiché.
readyEvent.OnClientEvent:Connect(...)
Écoute un événement distant pour activer/désactiver l’interface.
À toi de jouer !
Crée un RemoteEvent nommé ReadyEvent dans ReplicatedStorage.
Copie le code du LocalScript dans StarterPlayerScripts.
Teste :
Entre dans la hitbox : le message doit s’afficher.
Modifie :
Change le texte en "Prêt ? Appuie sur F !".
Change la couleur du fond en rouge (Color3.fromRGB(255, 0, 0)).
QCM / Questions
À quoi sert un LocalScript ?
a) À exécuter du code côté serveur.
b) À créer des objets dans le jeu.
c) À exécuter du code côté client (pour un joueur spécifique).
Que fait gui.Enabled = false ?
a) Supprime l’interface.
b) Désactive l’interface (elle n’est plus visible).
Étape 6 : Coder un LocalScript pour détecter l’appui sur la touche F
But : Détecter quand le joueur appuie sur F et envoyer un signal au serveur. Concepts clés : InputBegan, RemoteEvent, UserInputService.
Explications
Pour détecter une touche, on utilise :
UserInputService : Service qui gère les entrées clavier/souris.
InputBegan : Événement déclenché quand une touche est pressée.
RemoteEvent : Pour envoyer un signal du client au serveur.
Exemple :
luaCopier
local UserInputService = game:GetService("UserInputService")
UserInputService.InputBegan:Connect(function(input, gameProcessed)
if input.KeyCode == Enum.KeyCode.F then
print("F a été pressée !")
end
end)
Code Lua
Ajoute ce code au LocalScript de l’étape 5 :
luaCopier
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- ==================== RÉFÉRENCES ====================
local player = Players.LocalPlayer
local readyEvent = ReplicatedStorage:WaitForChild("ReadyEvent")
local fireInputEvent = ReplicatedStorage:WaitForChild("FireInputEvent")
-- ==================== INTERFACE ====================
local gui = Instance.new("ScreenGui")
local label = Instance.new("TextLabel")
-- Configure le TextLabel
label.Text = "Appuie sur F pour lancer !"
label.Size = UDim2.new(0, 200, 0, 50) -- Largeur: 200, Hauteur: 50
label.Position = UDim2.new(0.5, -100, 0.9, -25) -- Centre en bas
label.BackgroundColor3 = Color3.fromRGB(0, 0, 0) -- Fond noir
label.TextColor3 = Color3.fromRGB(255, 255, 255) -- Texte blanc
label.TextScaled = true -- Le texte s'adapte à la taille
label.Parent = gui
gui.Parent = player:WaitForChild("PlayerGui")
-- Désactive l'affichage au début
gui.Enabled = false
-- Écoute l'événement ReadyEvent pour activer/désactiver l'interface
readyEvent.OnClientEvent:Connect(function(isReady)
gui.Enabled = isReady
end)
-- ==================== DÉTECTION TOUCHE F ====================
UserInputService.InputBegan:Connect(function(input, gameProcessed)
if input.KeyCode == Enum.KeyCode.F then
-- Envoie un événement au serveur avec la direction du regard du joueur
local character = player.Character or player.CharacterAdded:Wait()
local humanoidRootPart = character:WaitForChild("HumanoidRootPart")
local lookDirection = humanoidRootPart.CFrame.LookVector
fireInputEvent:FireServer(lookDirection)
end
end)
Puis modifie le script serveur pour recevoir l’évènement de la demande de TIR :
Rajoute EventRemote : fireInputEvent :
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local fireInputEvent = ReplicatedStorage:WaitForChild("FireInputEvent")
local readyEvent = ReplicatedStorage:WaitForChild("ReadyEvent")
Puis la réception de l’évènement dans hitbox.Touched :
-- ==================== ÉVÉNEMENTS : ZONE DE JEU
-- Quand un joueur touche la hitbox
hitbox.Touched:Connect(function(hit)
-- Vérifie si l'objet qui a touché est un personnage de joueur
local character = hit.Parent
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
local player = Players:GetPlayerFromCharacter(character)
if player then
-- Anti-spam : on ignore si le joueur est déjà comptabilisé dans la zone
local data = playersData[player]
if data and data.inZone then return end
playersData[player] = { inZone = true, fire = 0 }
print(player.Name .. " est entré dans la zone de lancer !")
lockFirstPersonCamera(player) -- Passe en première personne
-- Envoie d'un évènement au client pour lui dire qu'il peut lancer le ballon
readyEvent:FireClient(player, true) -- prêt à lancer le ballon
-- Réception véènment pour le lancer du ballon
fireInputEvent.OnServerEvent:Connect(function(player, lookDirection)
print("Lancement du ballon par " .. player.Name)
end)
end
end
end)
Explications du code
Ligne
Explication
UserInputService.InputBegan:Connect(...)
Écoute quand une touche est pressée.
input.KeyCode == Enum.KeyCode.F
Vérifie si la touche F a été pressée.
humanoidRootPart.CFrame.LookVector
Récupère la direction dans laquelle le joueur regarde.
fireInputEvent:FireServer(lookDirection)
Envoie la direction au serveur via un RemoteEvent.
À toi de jouer !
Crée un RemoteEvent nommé FireInputEvent dans ReplicatedStorage.
Copie le code dans le LocalScript.
Teste :
Entre dans la hitbox.
Appuie sur F : rien ne se passe encore (le serveur ne gère pas encore l’événement).
Modifie :
Change la touche en E (Enum.KeyCode.E).
QCM / Questions
À quoi sert UserInputService.InputBegan ?
a) À détecter quand une touche est relâchée.
b) À détecter quand une touche est pressée.
c) À changer la touche par défaut.
Que fait fireInputEvent:FireServer(lookDirection) ?
a) Affiche un message à l’écran.
b) Envoie la direction du regard au serveur.
c) Détruit le ballon.
Pour aller plus loin
Ajoute un cooldown (délai entre deux lancers) pour éviter le spam.
Étape 7 : Gérer la réception du RemoteEvent et lancer la balle
But : Recevoir le signal du client et lancer une balle. Concepts clés : RemoteEvent.OnServerEvent, clonage d’objets, physique.
Explications
Quand le serveur reçoit un RemoteEvent, il peut :
Vérifier que le joueur a le droit de lancer.
Cloner la balle depuis ReplicatedStorage.
Appliquer une force pour la lancer.
Modifie le Script (côté serveur) :
-- ==================== CONFIGURATION ====================
local EXIT_CONFIRM_DELAY = 0.5 -- Délai avant de confirmer la sortie de zone (anti faux-positif)
local PUSH_FORCE = 150 -- Force de lancement en fonction de l'angle
local BALL_LIFETIME = 2 -- Durée de vie de la balle avant destruction (secondes)
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local fireInputEvent = ReplicatedStorage:WaitForChild("FireInputEvent")
local readyEvent = ReplicatedStorage:WaitForChild("ReadyEvent")
local ServerStorage = game:GetService("ServerStorage")
local Debris = game:GetService("Debris")
-- ==================== RÉFÉRENCES SCÈNE ====================
local basketFolder = workspace:WaitForChild("Basket")
local hitbox = basketFolder:WaitForChild("HitBox")
local ballTemplate = basketFolder:WaitForChild("Ball")
ballTemplate.Parent = ServerStorage -- Déplace le modèle dans ServerStorage
-- ==================== ÉTAT DU JEU ====================
-- playersData[player] = { inZone = bool, nbrPoints = number }
local playersData = {}
-- Applique une caméra first person (vue première personne verrouillée et zoomée)
local function lockFirstPersonCamera(player)
player.CameraMode = Enum.CameraMode.LockFirstPerson
player.CameraMinZoomDistance = 0.5
player.CameraMaxZoomDistance = 0.5
end
-- Restaure la caméra classique du joueur
local function resetCamera(player)
player.CameraMode = Enum.CameraMode.Classic
player.CameraMaxZoomDistance = 400
task.wait(0.1) -- Attend 0.1 seconde pour que la caméra se réinitialise
player.CameraMinZoomDistance = 10
end
-- Fait sortir proprement un joueur de la zone : reset caméra + pyramide + état
local function exitZone(player)
local data = playersData[player]
if not data or not data.inZone then return end
data.inZone = false
resetCamera(player)
end
-- Fait apparaître une balle devant le joueur et lui appliquer une impulsion
local function fireBall(player, force)
local character = player.Character or player.CharacterAdded:Wait()
local hrp = character:WaitForChild("HumanoidRootPart")
local ball = ballTemplate:Clone()
ball.CustomPhysicalProperties = PhysicalProperties.new(
1, -- Density
0, -- Friction
0.1, -- Elasticity
0, -- FrictionWeight
0.1 -- ElasticityWeight
)
ball.Parent = workspace
ball.CFrame = hrp.CFrame * CFrame.new(-2, 2, 0) -- Position devant le joueur
ball.Name = player.Name
-- Applique une vitesse initiale vers l'avant + vers le haut
ball.AssemblyLinearVelocity = hrp.CFrame.LookVector * (force - math.random(38, 42)) + Vector3.new(0, force + math.random(28, 32), 0)
-- Détruit la balle après 2 secondes
Debris:AddItem(ball, BALL_LIFETIME)
end
local function throwBall(player, lookDirection)
local data = playersData[player]
if tick() - data.fire < 2 then return end -- Cooldown de 2 secondes
readyEvent:FireClient(player, false)
data.fire = tick()
local force = ((lookDirection.Y + 1) / 2) * PUSH_FORCE -- Force de lancement en fonction de l'angle
fireBall(player, force)
task.wait(2)
readyEvent:FireClient(player, true)
end
-- ==================== ÉVÉNEMENTS : ZONE DE JEU
-- Quand un joueur touche la hitbox
hitbox.Touched:Connect(function(hit)
-- Vérifie si l'objet qui a touché est un personnage de joueur
local character = hit.Parent
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
local player = Players:GetPlayerFromCharacter(character)
if player then
-- Anti-spam : on ignore si le joueur est déjà comptabilisé dans la zone
local data = playersData[player]
if data and data.inZone then return end
playersData[player] = { inZone = true, fire = 0 }
print(player.Name .. " est entré dans la zone de lancer !")
lockFirstPersonCamera(player) -- Passe en première personne
-- Envoie d'un évènement au client pour lui dire qu'il peut lancer le ballon
readyEvent:FireClient(player, true) -- prêt à lancer le ballon
-- Réception véènment pour le lancer du ballon
fireInputEvent.OnServerEvent:Connect(function(player, lookDirection)
throwBall(player, lookDirection)
end)
end
end
end)
-- Quand une partie du personnage quitte la hitbox, on vérifie si c'est un joueur
hitbox.TouchEnded:Connect(function(hit)
-- Vérifie si l'objet qui a touché est un personnage de joueur
local character = hit.Parent
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
local player = Players:GetPlayerFromCharacter(character)
if player then
task.wait(EXIT_CONFIRM_DELAY)
-- On vérifie qu'aucune partie du personnage ne touche encore la hitbox
-- (utile car Touched/TouchEnded se déclenchent par membre du corps)
local character = player.Character
local stillTouching = false
if character then
for _, part in pairs(hitbox:GetTouchingParts()) do
if part.Parent == character then
stillTouching = true
break
end
end
end
if stillTouching then return end
exitZone(player)
-- Envoie d'un évènement au client pour lui dire qu'il ne peut plus lancer le ballon
readyEvent:FireClient(player, false)
end
end
end)
Explications du code
Ligne
Explication
fireInputEvent.OnServerEvent:Connect(...)
Écoute l’événement envoyé par le client.
ballTemplate:Clone()
Crée une copie de la balle.
ball.CFrame = hrp.CFrame * CFrame.new(-2, 2, 0)
Positionne la balle devant le joueur.
ball.AssemblyLinearVelocity = ...
Applique une vitesse à la balle (physique).
Debris:AddItem(ball, 2)
Détruit la balle après 2 secondes.
tick() - data.fire < 2
Vérifie si 2 secondes se sont écoulées depuis le dernier lancer.
À toi de jouer !
Copie le code dans le Script côté serveur.
Teste :
Entre dans la hitbox.
Appuie sur F : une balle doit être lancée !
Modifie :
Change BALL_LIFETIME à 5 pour que la balle reste plus longtemps.
Change la force du lancer en modifiant local PUSH_FORCE = 150 — Force de lancement en fonction de l’angle force = ((lookDirection.Y + 1) / 2) * PUSH_FORCE .
QCM / Questions
À quoi sert ballTemplate:Clone() ?
a) À supprimer la balle.
b) À créer une copie de la balle.
c) À changer la couleur de la balle.
Que fait Debris:AddItem(ball, 2) ?
a) Détruit la balle après 2 secondes.
b) Affiche la balle pendant 2 secondes.
c) Fait rebondir la balle 2 fois.
Pour aller plus loin
Ajoute un effet de particule quand la balle est lancée.
Étape 8 : Détecter quand la balle touche le panier
But : Marquer des points quand la balle entre dans le panier. Concepts clés : Détection de collision, gestion de score, sons.
Pour détecter quand la balle touche le panier :
Ajoute une Part (nommée ControlBall) dans le panier.
Utilise .Touched pour détecter la collision.
Ajoute des points au joueur si la balle est la sienne.
Exemple :
ballDetector.Touched:Connect(function(hit)
local player = Players:FindFirstChild(hit.Name)
if player then
ScoreManager.addPoints(player, 3) -- Ajoute 3 points
end
end)
Voici le script final avec toutes les fonctionnalités intégrées :
-- ==================== CONFIGURATION ====================
local EXIT_CONFIRM_DELAY = 0.5 -- Délai avant de confirmer la sortie de zone (anti faux-positif)
local PUSH_FORCE = 150 -- Force de lancement en fonction de l'angle
local BALL_LIFETIME = 2 -- Durée de vie de la balle avant destruction (secondes)
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local fireInputEvent = ReplicatedStorage:WaitForChild("FireInputEvent")
local readyEvent = ReplicatedStorage:WaitForChild("ReadyEvent")
local ServerStorage = game:GetService("ServerStorage")
local Debris = game:GetService("Debris")
-- ==================== RÉFÉRENCES SCÈNE ====================
local basketFolder = workspace:WaitForChild("Basket")
local hitbox = basketFolder:WaitForChild("HitBox")
local ballTemplate = basketFolder:WaitForChild("Ball")
local ballDetector = basketFolder:WaitForChild("ControlBall") -- zone qui détecte les balles dans le panier
ballTemplate.Parent = ServerStorage -- Déplace le modèle dans ServerStorage
local clapSound = basketFolder:WaitForChild("claps")
-- ==================== ÉTAT DU JEU ====================
-- playersData[player] = { inZone = bool, nbrPoints = number }
local playersData = {}
-- Joue un son
local function playSound(sound)
if not sound then return end
pcall(function() sound:Play() end)
end
-- Applique une caméra first person (vue première personne verrouillée et zoomée)
local function lockFirstPersonCamera(player)
player.CameraMode = Enum.CameraMode.LockFirstPerson
player.CameraMinZoomDistance = 0.5
player.CameraMaxZoomDistance = 0.5
end
-- Restaure la caméra classique du joueur
local function resetCamera(player)
player.CameraMode = Enum.CameraMode.Classic
player.CameraMaxZoomDistance = 400
task.wait(0.1) -- Attend 0.1 seconde pour que la caméra se réinitialise
player.CameraMinZoomDistance = 10
end
-- Fait sortir proprement un joueur de la zone : reset caméra + pyramide + état
local function exitZone(player)
local data = playersData[player]
if not data or not data.inZone then return end
data.inZone = false
resetCamera(player)
end
-- Fait apparaître une balle devant le joueur et lui appliquer une impulsion
local function fireBall(player, force)
local character = player.Character or player.CharacterAdded:Wait()
local hrp = character:WaitForChild("HumanoidRootPart")
local ball = ballTemplate:Clone()
ball.CustomPhysicalProperties = PhysicalProperties.new(
1, -- Density
0, -- Friction
0.1, -- Elasticity
0, -- FrictionWeight
0.1 -- ElasticityWeight
)
ball.Parent = workspace
ball.CFrame = hrp.CFrame * CFrame.new(-2, 2, 0) -- Position devant le joueur
ball.Name = player.Name
-- Applique une vitesse initiale vers l'avant + vers le haut
ball.AssemblyLinearVelocity = hrp.CFrame.LookVector * (force - math.random(38, 42)) + Vector3.new(0, force + math.random(28, 32), 0)
-- Détruit la balle après 2 secondes
Debris:AddItem(ball, BALL_LIFETIME)
end
local function throwBall(player, lookDirection)
local data = playersData[player]
if tick() - data.fire < 2 then return end -- Cooldown de 2 secondes
readyEvent:FireClient(player, false)
data.fire = tick()
local force = ((lookDirection.Y + 1) / 2) * PUSH_FORCE -- Force de lancement en fonction de l'angle
fireBall(player, force)
task.wait(2)
readyEvent:FireClient(player, true)
end
-- ==================== ÉVÉNEMENTS : ZONE DE JEU
-- Quand un joueur touche la hitbox
hitbox.Touched:Connect(function(hit)
-- Vérifie si l'objet qui a touché est un personnage de joueur
local character = hit.Parent
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
local player = Players:GetPlayerFromCharacter(character)
if player then
-- Anti-spam : on ignore si le joueur est déjà comptabilisé dans la zone
local data = playersData[player]
if data and data.inZone then return end
playersData[player] = { inZone = true, fire = 0 }
print(player.Name .. " est entré dans la zone de lancer !")
lockFirstPersonCamera(player) -- Passe en première personne
-- Envoie d'un évènement au client pour lui dire qu'il peut lancer le ballon
readyEvent:FireClient(player, true) -- prêt à lancer le ballon
-- Réception véènment pour le lancer du ballon
fireInputEvent.OnServerEvent:Connect(function(player, lookDirection)
print("Lancement du ballon par " .. player.Name)
throwBall(player, lookDirection)
end)
end
end
end)
-- Quand une partie du personnage quitte la hitbox, on vérifie si c'est un joueur
hitbox.TouchEnded:Connect(function(hit)
-- Vérifie si l'objet qui a touché est un personnage de joueur
local character = hit.Parent
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
local player = Players:GetPlayerFromCharacter(character)
if player then
task.wait(EXIT_CONFIRM_DELAY)
-- On vérifie qu'aucune partie du personnage ne touche encore la hitbox
-- (utile car Touched/TouchEnded se déclenchent par membre du corps)
local character = player.Character
local stillTouching = false
if character then
for _, part in pairs(hitbox:GetTouchingParts()) do
if part.Parent == character then
stillTouching = true
break
end
end
end
if stillTouching then return end
exitZone(player)
-- Envoie d'un évènement au client pour lui dire qu'il ne peut plus lancer le ballon
readyEvent:FireClient(player, false)
end
end
end)
-- Détection du ballon dans le panier
ballDetector.Touched:Connect(function(hit)
local player = Players:FindFirstChild(hit.Name)
if player then
playSound(clapSound)
print(player.Name .. " a marqué 3 points !")
end
end)
-- Nettoyage quand un joueur quitte
Players.PlayerRemoving:Connect(function(player)
playersData[player] = nil
end)
Explications du code
Ligne
Explication
ballDetector.Touched:Connect(...)
Écoute quand un objet touche le détecteur du panier.
Players:FindFirstChild(hit.Name)
Vérifie si l’objet qui a touché est une balle lancée par un joueur.
playSound(clapSound)
Joue un son de applaudissements.
print(player.Name .. " a marqué 3 points !")
Affiche un message dans la sortie.
À toi de jouer !
Ajoute une Part nommée ControlBall dans le panier (dans Basket).
Ajoute un son nommé claps dans Basket (tu peux importer un son depuis Roblox Library).
Copie le code dans le Script.
Teste :
Lance une balle dans le panier : tu seras applaudi.
QCM / Questions
À quoi sert ballDetector.Touched:Connect(...) ?
a) À détruire le panier.
b) À changer la couleur du panier.
c) À détecter quand une balle touche le panier.
Que fait Players:FindFirstChild(hit.Name) ?
a) Crée un nouveau joueur.
b) Trouve un joueur dont le nom correspond à celui de l’objet qui a touché.
c) Supprime le joueur.
Pour aller plus loin
Ajoute un système de score avec un Leaderboard (tableau des scores).
Ici, tu vas apprendre à coder des boosts pour rendre ton personnage plus petit, transparent, plus rapide ou capable de sauter plus haut. Tu vas découvrir comment modifier ton personnage avec des scripts simples en Lua, le langage de Roblox.
Créer une base avec 4 pièces (parts) dans Roblox. Chaque pièce active ou désactive un boost (ex. : transparence, taille réduite, saut plus haut, course plus rapide) quand le joueur la traverse.
Ce que tu vas apprendre :
Lire la structure hiérarchique sous workspace :
Utiliser une boucle for pour parcourir toutes les pièces dans le jeu.
Reconnaître chaque part grâce à son nom (ex. : MoreJump, MoreSpeed, etc.).
Gérer l’événement de collision (Touched) :
Détecter quand un joueur touche une part avec part.Touched:Connect().
Vérifier si l’objet qui touche est bien un joueur (en cherchant un Humanoid).
Appliquer ou retirer le boost :
Utiliser des conditions (if/else) pour savoir quel boost activer en fonction du nom de la part.
local existingFolder = humanoid:FindFirstChild(« Speed ») if existingFolder then
Modifier les propriétés du joueur (ex. : Humanoid.WalkSpeed pour la vitesse, Humanoid.JumpPower pour le saut).
Gérer un cooldown :
Ajouter un délai (task.delay(COOLDOWN_DURATION)) pour éviter que le joueur active le boost trop souvent.
Utiliser une table (activeCooldowns) pour suivre les joueurs en cooldown.
Résultat : Un système où chaque part active/désactive un boost différent, avec une gestion propre des collisions et des délais
Ce script exemple permet de créer des zones spéciales dans Roblox qui, lorsqu’un joueur les touche, augmentent temporairement sa hauteur de saut (par exemple, pour sauter plus haut). Après avoir touché la zone, le joueur doit attendre un certain temps (COOLDOWN_DURATION) avant de pouvoir réutiliser l’effet.
Objets à créer dans le Workspace de Roblox
Pour que le script fonctionne, il faut créer ces éléments dans Roblox Studio :
Un dossier nommé ZonesBoost dans le Workspace :
C’est ici que seront placées toutes les zones de boost.
Comment faire : Dans l’onglet Explorer, cliquez droit sur Workspace → Insert Object → Folder → Renommez-le en ZonesBoost.
Des pièces (Parts) nommées MoreJump dans le dossier ZonesBoost :
Ce sont les zones que le joueur pourra toucher pour activer le boost.
Comment faire :
Insérez une Part (cliquez droit sur ZonesBoost → Insert Object → Part).
Renommez-la en MoreJump.
Ajustez sa taille et sa position pour qu’elle soit visible et accessible dans le jeu.
Répétez pour ajouter plusieurs zones si besoin.
Exemple de script pour sauter plus haut
local ZONES_FOLDER = workspace:WaitForChild("ZonesBoost")
local JUMP_MAX = 80
local COOLDOWN_DURATION = 1 -- en secondes
local activeCooldowns = {} -- [character] = true si en cooldown
-- Parcourir uniquement les enfants directs du dossier (pas les descendants)
for _, part in ipairs(ZONES_FOLDER:GetDescendants()) do
if not (part:IsA("BasePart") and part.Name == "MoreJump") then continue end
part.Touched:Connect(function(hit)
-- Vérifier que l'objet qui a touché est bien un personnage
local character = hit.Parent
if not character then return end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return end
-- Vérifier le cooldown
if activeCooldowns[character] then return end
activeCooldowns[character] = true
-- Sauvegarder ou restaurer la hauteur de saut
local jumpValue = humanoid:FindFirstChild("Jump")
if jumpValue then
humanoid.JumpHeight = jumpValue.Value
jumpValue:Destroy()
else
jumpValue = Instance.new("IntValue")
jumpValue.Name = "Jump"
jumpValue.Value = humanoid.JumpHeight
jumpValue.Parent = humanoid
humanoid.JumpHeight = JUMP_MAX
end
-- Réinitialiser le cooldown après le délai
task.delay(COOLDOWN_DURATION, function()
activeCooldowns[character] = nil
end)
end)
end
Déclaration des variables globales
local ZONES_FOLDER = workspace.ZonesBoost local JUMP_MAX = 80 local COOLDOWN_DURATION = 1 -- secondes local activeCooldowns = {} -- [character] = true pendant le cooldown
ZONES_FOLDER : Référence au dossier ZonesBoost dans le Workspace. C’est ici que le script va chercher les zones de boost.
JUMP_MAX : La hauteur de saut maximale (80) appliquée quand le joueur touche une zone.
COOLDOWN_DURATION : Temps d’attente (1 seconde) avant que le joueur puisse réutiliser le boost.
activeCooldowns : Une table (comme un dictionnaire) qui stocke si un joueur est en cooldown ou non.
Exemple : activeCooldowns[character] = true signifie que le joueur ne peut pas encore réutiliser le boost.
Récupérer les objets du Workspace
for _, part in ipairs(folder:GetDescendants()) do
ZONES_FOLDER:GetDescendants() : Récupère tous les objets (y compris les sous-objets) dans le dossier ZonesBoost.
ipairs : Parcourt la liste des objets un par un.
for _, part in ... :
_ est une variable non utilisée (on s’intéresse seulement à part).
part représente chaque objet dans le dossier.
Filtrer les objets valides
if not (part:IsA("BasePart") and part.Name=="MoreJump") then continue end
part:IsA("BasePart") : Vérifie si l’objet est une Part (un bloc 3D dans Roblox).
part.Name=="MoreJump" : Vérifie si la Part s’appelle MoreJump.
continue : Si la condition n’est pas remplie, on passe à l’objet suivant (on ignore les autres objets).
Détecter la collision avec Touched
part.Touched:Connect(function(plr)
Touched : Événement déclenché quand un objet entre en collision avec la Part.
plr : Représente l’objet qui a touché la Part (ici, ce sera le personnage du joueur).
Récupérer le joueur et son Humanoid
local character = plr.Parent local humanoid = character and character:FindFirstChild("Humanoid") if not humanoid then return end
plr.Parent : Le parent de plr est le modèle du personnage (le joueur).
character:FindFirstChild("Humanoid") : Cherche l’Humanoid dans le personnage.
L’Humanoid est un composant Roblox qui gère les propriétés du personnage (comme la hauteur de saut).
if not humanoid then return end : Si le personnage n’a pas d’Humanoid, on arrête la fonction (cas rare, mais possible).
Gérer le COOLDOWN
if activeCooldowns[character] then return end activeCooldowns[character] = true
activeCooldowns[character] : Vérifie si le personnage est déjà en cooldown.
Si oui (true), on arrête la fonction avec return.
Si non, on active le cooldown pour ce personnage : activeCooldowns[character] = true.
Sauvegarder et restaurer la hauteur de saut
local jumpValue = humanoid:FindFirstChild("Jump")
if jumpValue then humanoid.JumpHeight = jumpValue.Value existingFolder:Destroy() else jumpValue = Instance.new("IntValue") jumpValue.Name = "Jump" jumpValue.Parent = humanoid jumpValue.Value = humanoid.JumpHeight humanoid.JumpHeight = JUMPMAX end
jumpValue : Cherche si l’Humanoid a déjà un enfant nommé Jump (un IntValue qui stocke la hauteur de saut d’origine).
Si Jump existe :
On restaure la hauteur de saut d’origine (jumpVallue.Value).
On supprime le IntValue car il n’est plus utile.
Si Jump n’existe pas :
On crée un IntValue nommé Jump.
On y stocke la hauteur de saut actuelle (humanoid.JumpHeight).
On applique la nouvelle hauteur de saut (JUMPMAX = 80).
task.delay(COOLDOWN_DURATION) : Attend 1 seconde (la valeur de COOLDOWN).
activeCooldowns[character] = nil : Supprime le cooldown pour ce personnage, qui peut maintenant réutiliser le boost.
Pourquoi le COOLDOWN ?
Eviter les nombreux événements de collision : Sans cooldown, le joueur spamme le boost en touchant rapidement la zone plusieurs fois.
Éviter les bugs : Sans cooldown, le script pourrait s’exécuter trop souvent et causer des problèmes.
Résumé des concepts clés
Concept
Explication
Workspace
L’espace 3D où se trouvent tous les objets du jeu.
BasePart
Une pièce de base dans Roblox (ex. : un cube, une sphère).
Touched
Événement déclenché quand un objet entre en collision avec un autre.
Humanoid
Composant qui contrôle les propriétés d’un personnage (vie, saut, etc.).
IntValue
Un objet Roblox qui stocke un nombre entier (ici, la hauteur de saut d’origine).
activeCooldowns
Table qui suit quels joueurs sont en cooldown.
task.delay()
Fonction qui pause le script pendant un certain temps.
Exemple d’un script pour gérer plusieurs Boosts
-- ==================== SERVICES ====================
local Players = game:GetService("Players")
-- ==================== CONFIGURATION ====================
local ZONES_FOLDER = workspace:WaitForChild("ZonesBoost")
local COOLDOWN_DURATION = 1 -- secondes avant de pouvoir re-déclencher une zone
-- Suivi des cooldowns actifs par personnage (évite le spam de déclenchement)
local activeCooldowns = {} -- [character] = true pendant le cooldown
-- ==================== EFFETS APPLIQUÉS AU PERSONNAGE ====================
-- Rend transparent les accessoires et MeshPart du personnage
local function setPlayerTransparency(character, transparency)
if not character then return end
for _, child in ipairs(character:GetChildren()) do
if child:IsA("Accessory") then
-- Rend transparent les BasePart/Decal de l'accessoire
for _, descendant in ipairs(child:GetDescendants()) do
if descendant:IsA("BasePart") or descendant:IsA("Decal") then
descendant.Transparency = transparency
end
end
elseif child:IsA("MeshPart") then
-- Le MeshPart lui-même est la part à rendre transparente
child.Transparency = transparency
end
end
end
-- Modifie la taille (échelle) du personnage
local function setPlayerScale(character, scale)
if not character then return end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return end
print("scale: ", scale)
humanoid.BodyHeightScale.Value = scale
humanoid.BodyWidthScale.Value = scale
humanoid.BodyDepthScale.Value = scale
humanoid.HeadScale.Value = scale
end
-- Modifie la vitesse de marche du personnage
local function setPlayerSpeed(character, speed)
if not character then return end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return end
humanoid.WalkSpeed = speed
end
-- Modifie la hauteur de saut du personnage
local function setPlayerJump(character, jumpHeight)
if not character then return end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return end
humanoid.JumpHeight = jumpHeight
end
-- ==================== VALEURS PAR DÉFAUT (pour restaurer l'état d'origine) ====================
local function getDefaultTransparency(character)
if not character then return nil end
return 0
end
local function getDefaultScale(character)
if not character then return nil end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return nil end
return humanoid.BodyHeightScale.Value
end
local function getDefaultSpeed(character)
if not character then return nil end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return nil end
return humanoid.WalkSpeed
end
local function getDefaultJump(character)
if not character then return nil end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return nil end
return humanoid.JumpHeight
end
-- ==================== CONFIGURATION DES BOOSTS ====================
-- Chaque boost définit : sa valeur max, comment récupérer la valeur par défaut,
-- le nom de la valeur stockée sur le personnage, et l'action à appliquer
local BOOSTS = {
MoreSpeed = {
max = 50,
default = getDefaultSpeed,
valueName = "Speed",
action = setPlayerSpeed,
},
MoreJump = {
max = 80,
default = getDefaultJump,
valueName = "Jump",
action = setPlayerJump,
},
LessScale = {
max = 0.3,
default = getDefaultScale,
valueName = "Scale",
action = setPlayerScale,
},
Invisibility = {
max = 0.9,
default = getDefaultTransparency,
valueName = "Transparency",
action = setPlayerTransparency,
},
}
-- ==================== LOGIQUE DE ZONE ====================
-- Applique ou retire un boost selon l'état actuel du personnage (toggle)
local function handleBoostTouch(character, boost)
local existingValue = character:FindFirstChild(boost.valueName)
if existingValue then
-- Le boost est déjà actif : on restaure la valeur d'origine et on nettoie
boost.action(character, existingValue.Value)
existingValue:Destroy()
else
-- Le boost n'est pas actif : on sauvegarde la valeur par défaut, puis on applique le boost max
local defaultValue = boost.default(character)
if not defaultValue then return end -- sécurité : impossible de sauvegarder, on annule
local savedValue = Instance.new("IntValue")
savedValue.Name = boost.valueName
savedValue.Value = defaultValue
savedValue.Parent = character
boost.action(character, boost.max)
end
end
for _, part in ipairs(ZONES_FOLDER:GetDescendants()) do
if not part:IsA("BasePart") then continue end
local boost = BOOSTS[part.Name]
if not boost then continue end
print(boost)
part.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
-- Vérifie que c'est bien un personnage joueur (pas un NPC ou une part aléatoire)
local player = Players:GetPlayerFromCharacter(character)
if not player then return end
-- Vérifie le cooldown pour éviter le spam de déclenchement
if activeCooldowns[character] then return end
activeCooldowns[character] = true
handleBoostTouch(character, boost)
-- Réinitialise le cooldown après le délai
task.delay(COOLDOWN_DURATION, function()
activeCooldowns[character] = nil
end)
end)
end
-- ==================== NETTOYAGE ====================
Players.PlayerRemoving:Connect(function(player)
-- Nettoyage complet pour éviter les fuites mémoire quand un joueur quitte le jeu
local character = player.Character
if not character then return end
activeCooldowns[character] = nil
-- Supprimer tous les IntValue liés aux boosts
for _, boost in pairs(BOOSTS) do
local value = character:FindFirstChild(boost.valueName)
if value then
value:Destroy()
end
end
end)
Ce script permet de créer 4 types de zones de boost dans un seul script :
MoreSpeed : Augmente la vitesse de marche (WalkSpeed).
MoreJump : Augmente la hauteur de saut (JumpHeight).
MoreScale : Réduit la taille du personnage (échelle).
Transparency : Rend le personnage transparent.
Chaque zone est une BasePart dans le dossier ZonesBoost. Quand un joueur touche une zone, le boost correspondant est activé ou désactivé (toggle). Un cooldown empêche le spam.
Structure de la table BOOSTS
La table BOOSTS est le cœur du script. Elle définit les propriétés et comportements de chaque boost sous forme de sous-tables.
Définition de BOOSTS
local BOOSTS = { MoreSpeed = { max = 50, -- Valeur maximale du boost (ex: WalkSpeed = 50) default = getDefaultSpeed, -- Fonction pour récupérer la valeur par défaut valueName = "Speed", -- Nom de la valeur stockée sur le personnage (ex: IntValue "Speed") action = setPlayerSpeed, -- Fonction à appeler pour appliquer le boost }, MoreJump = { max = 80, default = getDefaultJump, valueName = "Jump", action = setPlayerJump, }, MoreScale = { max = 1.5, default = getDefaultScale, valueName = "Scale", action = setPlayerScale, }, Transparency = { max = 0.9, default = getDefaultTransparency, valueName = "Transparency", action = setPlayerTransparency, }, }
Explication des champs
Champ
Type
Rôle
Exemple pour MoreSpeed
max
number
Valeur maximale du boost (ex: WalkSpeed = 50).
50
default
function
Fonction qui récupère la valeur par défaut du personnage (ex: WalkSpeed initiale).
getDefaultSpeed (retourne humanoid.WalkSpeed)
valueName
string
Nom de l’IntValue stocké sur le personnage pour sauvegarder la valeur d’origine.
"Speed"
action
function
Fonction qui applique le boost (ex: modifie WalkSpeed).
setPlayerSpeed(character, 50)
Pourquoi cette structure ?
Modularité : Chaque boost est indépendant et peut être modifié sans toucher aux autres.
Réutilisabilité : Les fonctions (default, action) sont partagées entre tous les boosts du même type.
Extensibilité : Pour ajouter un nouveau boost, il suffit d’ajouter une entrée dans BOOSTS.
3. Fonctionnement des boosts (Toggle)
Le script utilise un mécanisme de toggle :
1ère collision : Active le boost (ex: WalkSpeed = 50).
2ème collision : Désactive le boost et restaure la valeur d’origine.
Fonction handleBoostTouch
local function handleBoostTouch(character, boost) local existingValue = character:FindFirstChild(boost.valueName)
if existingValue then -- Boost déjà actif : on restaure la valeur d'origine boost.action(character, existingValue.Value) existingValue:Destroy() else -- Boost inactif : on sauvegarde la valeur par défaut, puis on applique le boost local defaultValue = boost.default(character) if not defaultValue then return end
local savedValue = Instance.new("IntValue") savedValue.Name = boost.valueName savedValue.Value = defaultValue savedValue.Parent = character
boost.action(character, boost.max) end end
Explication du toggle
Vérification de existingValue :
Le script cherche un IntValue dans le character avec le nom boost.valueName (ex: "Speed").
Si existingValue existe → Le boost est déjà actif :
On restaure la valeur d’origine (existingValue.Value) en appelant boost.action.
On supprime l’IntValue pour indiquer que le boost est désactivé.
Si existingValue n’existe pas → Le boost est inactif :
On récupère la valeur par défaut avec boost.default(character) (ex: getDefaultSpeed).
On crée un IntValue pour stocker cette valeur dans le character.
On applique le boost avec boost.action(character, boost.max).
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.
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 !
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 son Transparency à 0.5 et désactive CanCollide.
Dans StarterGui, insère un ScreenGui, puis à l’intérieur deux Frame nommés NumberGenerator et Answers.
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
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
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
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 hitbox en hitboxx à 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 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 !
Ajoute la fonction randomPair dans ton script.
Remplace l’appel math.random(2, maxValue) de l’étape 4.2 par randomPair(2, maxValue).
Lance Play plusieurs fois et vérifie dans l’Output que le nombre affiché est toujours pair.
É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
É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() et IsA("TextLabel") pour mettre à jour le texte de tous les TextLabel du dossier NumberGenerator en une seule boucle.
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
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 NumberGameScript par 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)
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.
Tu vas construire, étape par étape, un mini-jeu appelé « Trouve la bonne couleur ».
Voici la règle du jeu : au démarrage, le jeu tire une couleur au hasard parmi rouge, vert et bleu, et l’affiche sur un bloc de référence. Sur un plateau composé de plusieurs blocs, un seul de ces blocs a exactement cette couleur, les autres ont des couleurs différentes. Le joueur doit se déplacer et toucher le bloc qui possède la bonne couleur.
Si le joueur touche le bon bloc, il regagne toute sa vie. Si le joueur touche un mauvais bloc, il perd 10 points de vie. Une fois le bon bloc trouvé, une nouvelle manche démarre automatiquement après quelques secondes, avec une nouvelle couleur à retrouver.
Ce tutoriel est découpé en huit étapes. Chaque étape t’apprend un ou plusieurs concepts de programmation que tu retrouveras dans énormément d’autres jeux et logiciels : les variables, les conditions, les boucles, les fonctions et les événements. À la fin, tu auras écrit et compris l’intégralité du script du jeu, et tu pourras l’améliorer toi-même.
Une variable, en programmation, c’est comme une boîte dans laquelle on range une information (un nombre, un texte, une couleur…) et à laquelle on donne un nom pour la retrouver facilement. C’est une idée que tu vas beaucoup utiliser dans ce tutoriel.
N’aie pas peur de te tromper. Une erreur dans un script n’est pas grave : elle t’indique simplement qu’il faut chercher et corriger, exactement comme un jeu de piste. C’est comme ça que tous les programmeurs apprennent.
Étape 1 : Comprendre le jeu avant de coder
But : Avant d’écrire la moindre ligne de code, tu vas apprendre à analyser un jeu et à le découper en petites règles simples. C’est une étape indispensable pour tout programmeur, avant même d’ouvrir un éditeur de code.
Concepts clés : analyse d’un problème, découpage en étapes logiques.
Explications :
Un programmeur ne commence jamais à écrire du code au hasard. Il commence par se poser des questions simples : que doit faire le jeu, dans quel ordre, et que doit-il se passer dans chaque situation.
Pour notre jeu, on peut résumer les règles ainsi :
Le jeu choisit une couleur au hasard.
Cette couleur est affichée sur un bloc de référence.
Un bloc du plateau reçoit exactement cette couleur, les autres blocs reçoivent d’autres couleurs.
Quand le joueur touche un bloc, le script compare sa couleur à la couleur de référence.
Si les couleurs sont identiques, le joueur regagne toute sa vie.
Si les couleurs sont différentes, le joueur perd 10 points de vie.
Une nouvelle manche démarre après un court délai.
Chacune de ces phrases va correspondre, un peu plus tard, à un morceau de code.
À toi de jouer !
Ensuite, essaie d’imaginer un autre mini-jeu avec la même idée (retrouver un élément parmi plusieurs) et écris ses règles de la même façon. Par exemple, un jeu où il faudrait retrouver la bonne forme plutôt que la bonne couleur.
Étape 2 : Modifier les propriétés d’un objet Roblox
But : Apprendre à accéder à un objet du jeu (une Part) et à modifier une de ses propriétés, ici sa couleur.
Concepts clés : variables, propriétés d’un objet, Color3.
Explications :
Dans Roblox, tout ce que tu vois dans le monde du jeu (un bloc, un personnage, une porte) est un objet, appelé instance. Chaque objet possède des propriétés que l’on peut lire ou modifier depuis un script, par exemple sa position, sa taille ou sa couleur.
Pour récupérer un objet du jeu dans le script, on utilise WaitForChild, qui va chercher un objet par son nom et attend qu’il existe avant de continuer. On range le résultat dans une variable, notre « boîte » qui va contenir l’objet.
Crée dans le workspace par Explorer la structure suivante un folder « ColorsGame », un script « ColorsGameScript, une part ReferenceColor :
Pour changer la couleur d’un objet, on utilise sa propriété Color, à laquelle on donne une valeur créée avec Color3.fromRGB(rouge, vert, bleu). Chacun des trois nombres va de 0 à 255 et représente la quantité de rouge, de vert ou de bleu dans la couleur finale.
Code Lua :
-- On récupère le dossier principal du jeu dans le Workspace
local colorsGameFolder = workspace:WaitForChild("ColorsGame")
-- On récupère la partie qui affiche la couleur de référence (celle à retrouver)
local referenceColorPart = colorsGameFolder:WaitForChild("ReferenceColor")
-- On modifie la propriété Color de cet objet pour lui donner une couleur rouge vif
referenceColorPart.Color = Color3.fromRGB(255, 0, 0)
À toi de jouer !
Recopie ce code dans le ColorsGameScript placé dans le Workspace de Roblox Studio, en remplaçant « Part » par le nom exact ReferenceColor d’un bloc que tu as toi-même placé dans ton lieu. Lance le jeu en mode test et observe la couleur du bloc.
Ensuite, modifie les trois nombres de Color3.fromRGB pour obtenir une couleur verte, puis une couleur bleue, puis une couleur de ton choix.
QCM :
Que représente Color3.fromRGB(0, 255, 0) ?
a) Une couleur rouge.
b) Une couleur verte.
c) Une couleur bleue.
Que fait workspace:WaitForChild("MyPart") ?
a) Il crée un nouveau bloc nommé « MyPart ».
b) Il récupère l’objet nommé « MyPart » et attend qu’il existe avant de continuer.
c) Il supprime l’objet « MyPart ».
Étape 3 : Débugger et tester chaque étape
But : Apprendre à utiliser l’instruction print pour afficher des informations dans la console de sortie, afin de vérifier que le script fait bien ce que l’on attend de lui.
Concepts clés : print, console de sortie, débogage.
Explications :
Quand un script ne fonctionne pas comme prévu, il est très utile de pouvoir « regarder à l’intérieur » pour comprendre ce qui se passe réellement. C’est le rôle de l’instruction print : elle affiche du texte ou la valeur d’une variable dans la fenêtre « Output » (sortie) de Roblox Studio.
Cette technique s’appelle le débogage, ou debug en anglais. C’est une des compétences les plus importantes en programmation : un programmeur passe une grande partie de son temps à vérifier pourquoi son code ne fait pas exactement ce qu’il souhaite.
Code Lua :
-- On récupère le dossier principal du jeu dans le Workspace
local colorsGameFolder = workspace:WaitForChild("ColorsGame")
-- On récupère la partie qui affiche la couleur de référence (celle à retrouver)
local referenceColorPart = colorsGameFolder:WaitForChild("ReferenceColor")
-- On modifie la propriété Color de cet objet pour lui donner une couleur rouge vif
referenceColorPart.Color = Color3.fromRGB(0, 255, 0)
-- On affiche à nouveau la couleur pour vérifier qu'elle a bien changé
print("Nouvelle couleur de MyPart :", referenceColorPart.Color)
À toi de jouer !
Ajoute ces lignes print à ton script de l’étape 2, lance le jeu en mode test, puis ouvre la fenêtre « Output » dans Roblox Studio (menu View, puis Output) pour lire les messages affichés.
Essaie d’ajouter un print supplémentaire de ton choix, par exemple pour afficher le nom de l’objet avec myPart.Name.
QCM :
À quoi sert l’instruction print ?
a) À afficher un message ou la valeur d’une variable dans la console, pour vérifier le fonctionnement du script.
b) À supprimer un objet du jeu.
c) À faire gagner le joueur.
Pourquoi le débogage est-il important en programmation ?
a) Il ne sert à rien, on peut s’en passer.
b) Il permet de comprendre ce qui se passe réellement dans le script et de corriger les erreurs.
c) Il remplace le besoin d’écrire du code.
Étape 4 : Donner de l’imprévisibilité au jeu
But : Apprendre à générer un nombre au hasard avec math.random, puis à utiliser une condition if / elseif pour choisir une couleur différente selon ce nombre.
Concepts clés : math.random, conditions if / elseif.
Explications :
Un jeu comme « Trouve la bonne couleur » doit surprendre le joueur à chaque manche : la couleur à retrouver ne doit jamais être toujours la même. Pour cela, on utilise le hasard, avec la fonction math.random(min, max), qui renvoie un nombre entier choisi au hasard entre min et max.
Une fois ce nombre obtenu, on utilise une condition if / elseif pour décider quoi faire selon sa valeur. Une condition permet à un script de prendre des décisions différentes selon la situation, un peu comme lorsque tu choisis un chemin différent selon le panneau indicateur que tu croises.
Code Lua :
-- On récupère le dossier principal du jeu dans le Workspace
local colorsGameFolder = workspace:WaitForChild("ColorsGame")
-- On récupère la partie qui affiche la couleur de référence (celle à retrouver)
local referenceColorPart = colorsGameFolder:WaitForChild("ReferenceColor")
-- On modifie la propriété Color de cet objet pour lui donner une couleur rouge vif
referenceColorPart.Color = Color3.fromRGB(0, 255, 0)
-- On tire un nombre entier au hasard entre 1 et 3
local randomChoice = math.random(1, 3)
print("Nombre tiré :", randomChoice)local referenceColor = nil
-- Selon le nombre tiré, on choisit une couleur dominante différente
if randomChoice == 1 then
referenceColor = Color3.fromRGB(math.random(150, 255), 0, 0) -- couleur rouge
elseif randomChoice == 2 then
referenceColor = Color3.fromRGB(0, math.random(150, 255), 0) -- couleur verte
elseif randomChoice == 3 then
referenceColor = Color3.fromRGB(0, 0, math.random(150, 255)) -- couleur bleue
end
referenceColorPart.Color = referenceColor
-- On affiche à nouveau la couleur pour vérifier qu'elle a bien changé
print("Nouvelle couleur de MyPart :", referenceColorPart.Color)
Le fait d’utiliser math.random(150, 255) plutôt que math.random(0, 255) garantit une couleur assez claire et vive, jamais trop sombre.
À toi de jouer !
Ajoute ce code à ton script et lance le jeu plusieurs fois de suite en mode test. Observe que la couleur change à chaque lancement.
Modifie ensuite l’intervalle math.random(150, 255) en math.random(0, 100) et observe la différence sur les couleurs obtenues.
QCM :
Que fait math.random(1, 3) ?
a) Il renvoie toujours le nombre 3.
b) Il renvoie un nombre entier choisi au hasard entre 1 et 3.
c) Il renvoie un nombre décimal entre 1 et 3.
À quoi sert une condition if / elseif dans un script ?
a) À répéter plusieurs fois la même action.
b) À faire prendre au script une décision différente selon une situation.
c) À afficher un message dans la console.
Étape 5 : Organiser son code
But : Apprendre à créer une fonction pour regrouper un ensemble d’instructions que l’on pourra réutiliser plusieurs fois.
Concepts clés : fonctions, paramètres, valeur de retour.
Explications :
Une fonction est un bloc de code auquel on donne un nom, et que l’on peut exécuter (« appeler ») autant de fois que nécessaire, sans avoir à recopier le code à chaque fois. Une fonction peut recevoir des informations en entrée, appelées paramètres, et peut renvoyer un résultat avec return.
Dans notre jeu, on va regrouper tout le code qui choisit une nouvelle couleur dans une fonction nommée chooseColor. Elle prendra en paramètre le bloc de référence et le plateau de blocs, et renverra la couleur choisie.
Code Lua :
-- On récupère le dossier principal du jeu dans le Workspace
local colorsGameFolder = workspace:WaitForChild("ColorsGame")
-- On récupère la partie qui affiche la couleur de référence (celle à retrouver)
local referenceColorPart = colorsGameFolder:WaitForChild("ReferenceColor")
-- On modifie la propriété Color de cet objet pour lui donner une couleur rouge vif
referenceColorPart.Color = Color3.fromRGB(0, 255, 0)
local function chooseColor(referenceColorPart)
-- On tire un nombre entier au hasard entre 1 et 3
local randomChoice = math.random(1, 3)
print("Nombre tiré :", randomChoice)
local referenceColor = nil
-- Selon le nombre tiré, on choisit une couleur dominante différente
if randomChoice == 1 then
referenceColor = Color3.fromRGB(math.random(150, 255), 0, 0) -- couleur rouge
elseif randomChoice == 2 then
referenceColor = Color3.fromRGB(0, math.random(150, 255), 0) -- couleur verte
elseif randomChoice == 3 then
referenceColor = Color3.fromRGB(0, 0, math.random(150, 255)) -- couleur bleue
end
referenceColorPart.Color = referenceColor
return referenceColorend
local referenceColor = chooseColor(referenceColorPart)
-- On affiche à nouveau la couleur pour vérifier qu'elle a bien changé
print("Nouvelle couleur de MyPart :", referenceColorPart.Color)
À toi de jouer !
Ajoute ce code à ton script. Appelle la fonction chooseColor deux fois de suite dans deux variables différentes, et affiche les deux résultats avec print pour vérifier qu’ils sont bien différents.
QCM :
Pourquoi utilise-t-on une fonction plutôt que de recopier plusieurs fois le même code ?
a) Parce que Lua interdit de recopier du code.
b) Pour éviter les répétitions et pouvoir réutiliser facilement le même bloc de code.
c) Parce que cela rend le jeu plus rapide.
Que fait l’instruction return dans une fonction ?
a) Elle arrête complètement le jeu.
b) Elle renvoie un résultat à l’endroit où la fonction a été appelée.
c) Elle affiche un message dans la console.
Étape 6 : Optimiser son code
But : Apprendre à parcourir automatiquement tous les objets contenus dans un dossier grâce à une boucle, plutôt que de traiter chaque bloc un par un.
Concepts clés : boucles for, GetChildren, IsA.
Explications :
Notre plateau de jeu, ColorBoard, ne contient pas un seul bloc mais plusieurs. Écrire manuellement le code pour chacun des blocs serait très long, et il faudrait tout réécrire si on ajoute un bloc supplémentaire. À la place, on utilise une boucle for, qui répète automatiquement une même action pour chaque objet d’une liste.
La méthode GetChildren() renvoie la liste de tous les objets contenus dans un dossier. On la combine avec ipairs(...) pour parcourir cette liste un par un, en récupérant à chaque tour de boucle un index (1, 2, 3…) et l’objet correspondant.
Comme un dossier peut contenir des objets qui ne sont pas des blocs (un script, par exemple), on vérifie le type de chaque objet avec part:IsA("BasePart") avant de le modifier.
Enfin, pour tirer au sort l’index du bloc gagnant parmi tous les blocs du plateau, on utilise #colorBoard:GetChildren(), qui compte le nombre maxi d’objets présents dans la liste.
Code Lua :
-- On récupère le dossier principal du jeu dans le Workspace
local colorsGameFolder = workspace:WaitForChild("ColorsGame")
-- On récupère la partie qui affiche la couleur de référence (celle à retrouver)
local referenceColorPart = colorsGameFolder:WaitForChild("ReferenceColor")
-- On modifie la propriété Color de cet objet pour lui donner une couleur rouge vif
referenceColorPart.Color = Color3.fromRGB(0, 255, 0)
local function chooseColor(referenceColorPart)
-- On tire un nombre entier au hasard entre 1 et 3
local randomChoice = math.random(1, 3)
print("Nombre tiré :", randomChoice)
local referenceColor = nil
-- Selon le nombre tiré, on choisit une couleur dominante différente
if randomChoice == 1 then
referenceColor = Color3.fromRGB(math.random(150, 255), 0, 0) -- couleur rouge
elseif randomChoice == 2 then
referenceColor = Color3.fromRGB(0, math.random(150, 255), 0) -- couleur verte
elseif randomChoice == 3 then
referenceColor = Color3.fromRGB(0, 0, math.random(150, 255)) -- couleur bleue
end
referenceColorPart.Color = referenceColor
return referenceColor
end
local referenceColor = chooseColor(referenceColorPart)
-- On affiche à nouveau la couleur pour vérifier qu'elle a bien changé
print("Nouvelle couleur de MyPart :", referenceColorPart.Color)
local colorBoard = colorsGameFolder:WaitForChild("ColorBoard")
-- On compte le nombre de blocs, puis on tire au sort l'index du bloc gagnant
local correctPartIndex = math.random(1, #colorBoard:GetChildren())
-- On parcourt tous les objets contenus dans ColorBoard
for index, part in ipairs(colorBoard:GetChildren()) do
-- On ne traite que les objets qui sont bien des blocs
if part:IsA("BasePart") then
print(index, correctPartIndex)
if index == correctPartIndex then
-- C'est le bloc tiré au sort : il reçoit la couleur de référence
part.Color = referenceColor
else
-- Les autres blocs reçoivent une couleur aléatoire différente
part.Color = Color3.fromRGB(math.random(0, 255), math.random(0, 255), math.random(0, 255))
end
end
end
À toi de jouer !
Place plusieurs blocs dans un dossier nommé « ColorBoard » dans le Workspace, puis ajoute ce code à ton script. Lance le jeu en mode test et observe qu’un seul bloc a exactement la couleur de référence.
Ajoute ou supprime un bloc dans le dossier « ColorBoard », relance le jeu, et vérifie que le code fonctionne toujours sans rien modifier.
QCM :
Que renvoie colorBoard:GetChildren() ?
a) Le nom du dossier ColorBoard.
b) La liste de tous les objets contenus dans le dossier ColorBoard.
c) La couleur du dossier ColorBoard.
Pourquoi utilise-t-on part:IsA("BasePart") dans la boucle ?
a) Pour vérifier que l’objet est bien un bloc avant de modifier sa couleur.
b) Pour compter le nombre d’objets.
c) Pour supprimer les objets qui ne sont pas des blocs.
Étape 7 : Construire l’architecture du jeu
But : Apprendre à créer une constante pour les valeurs qui ne changent jamais et assembler l’ensemble des concepts vus précédemment (variables, fonctions, boucles, conditions) avec un nouvel élément essentiel : les événements, pour obtenir le script complet et jouable du jeu.
Une constante est une variable dont la valeur ne change jamais pendant toute la partie, par exemple le nombre de points de vie perdus en cas d’erreur. On lui donne généralement un nom entièrement en majuscules, pour bien la distinguer des autres variables, par exemple LOSS_HEALTH.
Un événement est une notification envoyée par Roblox lorsqu’une action précise se produit dans le jeu, par exemple un contact entre deux objets. Chaque Part possède un événement Touched, auquel on peut « s’abonner » avec :Connect(function(hit) ... end) pour exécuter du code à chaque fois qu’un contact a lieu.
Dans la fonction connectée à Touched, hit désigne la partie qui a touché le bloc, généralement une main ou un pied. Son Parent est alors le personnage entier du joueur. Ce personnage possède un objet Humanoid, que l’on récupère avec FindFirstChildOfClass("Humanoid"), et qui gère notamment sa vie via la propriété Health.
En assemblant tout, on obtient l’architecture complète du script : on récupère les objets du jeu, on définit la fonction chooseColor, on l’appelle une première fois pour démarrer la partie, puis on parcourt tous les blocs du plateau pour préparer leurs propriétés et connecter l’événement Touched sur chacun d’eux.
Enfin, task.spawn permet d’exécuter du code « en parallèle » du reste du script, et task.wait(secondes) permet de faire patienter ce code sans bloquer le reste du jeu, ce qui est utilisé pour relancer une nouvelle manche quelques secondes après une bonne réponse.
Code Lua :
-- =========================================================
-- JEU : Trouve la bonne couleur !
-- Le jeu choisit une couleur au hasard (rouge, vert ou bleu).
-- Un des blocs de la ColorBoard aura exactement cette couleur.
-- Si le joueur touche le bon bloc, il regagne toute sa vie.
-- Si le joueur touche un mauvais bloc, il perd 10 points de vie.
-- =========================================================
local LOSS_HEALTH = 10 -- perte de santé pour un mauvais choix
local GAME_TIMEOUT = 5 -- en secondes, pour un time out avent de rejouer la partie = 5
-- On récupère le dossier principal du jeu dans le Workspace
local colorsGameFolder = workspace:WaitForChild("ColorsGame")
-- On récupère la partie qui affiche la couleur de référence (celle à retrouver)
local referenceColorPart = colorsGameFolder:WaitForChild("ReferenceColor")
-- On récupère le plateau qui contient tous les blocs de couleur
local colorBoard = colorsGameFolder:WaitForChild("ColorBoard")
local function chooseColor(referenceColorPart, colorBoard)
-- Cette variable va contenir la couleur de référence à retrouver
local referenceColor = nil
-- On tire un nombre au hasard entre 1 et 3 pour choisir quelle couleur dominante on va utiliser
local randomChoice = math.random(1, 3)
-- Selon le nombre tiré, on crée une couleur avec un fort rouge, vert ou bleu
-- math.random(150, 255) donne une couleur assez claire/vive, pas trop sombre
if randomChoice == 1 then
referenceColor = Color3.fromRGB(math.random(150, 255), 0, 0) -- couleur rouge
elseif randomChoice == 2 then
referenceColor = Color3.fromRGB(0, math.random(150, 255), 0) -- couleur verte
elseif randomChoice == 3 then
referenceColor = Color3.fromRGB(0, 0, math.random(150, 255)) -- couleur bleue
end
-- On applique cette couleur à la partie "ReferenceColor" pour que le joueur puisse la voir
referenceColorPart.Color = referenceColor
-- On choisit au hasard l'INDEX du bloc qui aura la bonne couleur (le bloc "gagnant")
local correctPartIndex = math.random(1, #colorBoard:GetChildren())
-- On parcourt tous les blocs présents dans le plateau de couleurs
for index, part in ipairs(colorBoard:GetChildren()) do
-- On vérifie que l'objet est bien un "BasePart" (un bloc, pas un script ou autre chose)
if part:IsA("BasePart") then
print(index, correctPartIndex)
-- Si c'est le bloc tiré au sort, on lui donne la couleur de référence (le bon bloc)
if index == correctPartIndex then
part.Color = referenceColor
else
-- Sinon, on lui donne une couleur totalement aléatoire (un bloc piège)
part.Color = Color3.fromRGB(math.random(0, 255), math.random(0, 255), math.random(0, 255))
end
end
end
return referenceColor
end
local referenceColor = chooseColor(referenceColorPart, colorBoard)
-- On parcourt tous les blocs présents dans le plateau de couleurs
for index, part in pairs(colorBoard:GetChildren()) do
-- On vérifie que l'objet est bien un "BasePart" (un bloc, pas un script ou autre chose)
if part:IsA("BasePart") then
part.Material = Enum.Material.Plastic
part.Anchored = true
part.CanCollide = true
-- On surveille si un joueur touche ce bloc
part.Touched:Connect(function(hit)
-- "hit" est la partie du joueur qui a touché le bloc (souvent une main ou un pied)
-- Son "Parent" est le personnage (le modèle) du joueur
local character = hit.Parent
if not character then return end -- si on ne trouve pas de personnage, on arrête ici
-- On cherche l'Humanoid du personnage, c'est lui qui gère la vie (Health)
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return end -- si pas d'Humanoid, on arrête ici (ce n'est pas un joueur)
if part.Color == Color3.fromRGB(255, 255, 255) then return end
-- On compare la couleur du bloc touché avec la couleur de référence
if part.Color == referenceColor then
-- Bonne couleur : le joueur récupère toute sa vie
humanoid.Health = 100
part.Color = Color3.fromRGB(255, 255, 255)
part.Material = Enum.Material.Neon
task.spawn(function()
task.wait(GAME_TIMEOUT)
referenceColor = chooseColor(referenceColorPart, colorBoard)
part.Material = Enum.Material.Plastic
end)
else
-- Mauvaise couleur : le joueur perd 10 points de vie
humanoid.Health -= LOSS_HEALTH
end
end)
end
end
À toi de jouer !
Recrée dans le Workspace un dossier nommé « ColorsGame » contenant une Part nommée « ReferenceColor » et un dossier nommé « ColorBoard » rempli de plusieurs blocs. Copie ce script complet, lance le jeu en mode test, et essaie de retrouver le bon bloc avec ton personnage.
Modifie la valeur de LOSS_HEALTH pour la mettre à 25, relance le jeu, et observe en combien de mauvais choix ton personnage perd toute sa vie.
QCM :
À quoi sert l’événement Touched ?
a) À déplacer un bloc.
b) À exécuter du code lorsqu’un contact se produit avec la Part.
c) À changer la couleur du plateau automatiquement.
Pourquoi utilise-t-on task.spawn avant task.wait(GAME_TIMEOUT) ?
a) Pour arrêter complètement le jeu pendant l’attente.
b) Pour exécuter cette attente en parallèle, sans bloquer le reste du script.
c) Pour supprimer le bloc touché.
Étape 8 : Améliorations pour aller plus loin
But : Utiliser tous les concepts appris pour faire évoluer le jeu par toi-même, en ajoutant de nouvelles règles.
Concepts clés : réinvestissement des concepts précédents, créativité.
Explications :
Maintenant que tu comprends l’ensemble du script, tu es capable de le modifier pour créer tes propres règles. Voici quelques pistes, de la plus simple à la plus avancée :
Ajouter un score : crée une variable playerScore initialisée à 0, augmente-la de 1 à chaque bonne réponse, et affiche-la avec print ou dans une interface.
Ajouter un chronomètre : utilise RunService.Heartbeat pour calculer le temps du jeu afin de contrôler le temps de chaque manche.
local RunService = game:GetService("RunService")
local temps = 0 -- Temps écoulé en secondes
RunService.Heartbeat:Connect(function(dt)
temps += dt
local tempsEntier = math.floor(temps)
local minutes = math.floor(tempsEntier / 60)
local secondes = tempsEntier % 60
print(string.format("%02d:%02d", minutes, secondes))
end)
Ajouter une condition de victoire : écris une fonction checkWinCondition() qui vérifie si playerScore a atteint une valeur suffisante, et qui affiche un message de victoire si c’est le cas.
Ajouter des obstacles : place des blocs supplémentaires qui ne font pas partie du jeu de couleurs, mais qui font perdre de la vie au joueur s’il les touche, en réutilisant l’événement Touched.
Choisis au moins une de ces améliorations et essaie de l’ajouter à ton script. N’hésite pas à utiliser print pour vérifier que tes nouvelles variables évoluent bien comme tu le souhaites.
QCM :
Que faudrait-il faire pour ajouter un système de score au jeu ?
a) Créer une variable comme playerScore et l’augmenter à chaque bonne réponse.
b) Modifier uniquement la couleur des blocs.
c) Supprimer la fonction chooseColor.
Pourquoi est-il utile de réutiliser des concepts déjà appris (variables, conditions, boucles, événements) pour ajouter de nouvelles règles ?
a) Parce que ce sont les seuls outils disponibles en Lua.
b) Parce que ces mêmes concepts permettent de construire des règles de jeu très différentes.
c) Parce que Roblox Studio l’exige.
-- =========================================================
-- JEU : Trouve la bonne couleur !
-- Le jeu choisit une couleur au hasard (rouge, vert ou bleu).
-- Un des blocs de la ColorBoard aura exactement cette couleur.
-- Si le joueur touche le bon bloc, il regagne toute sa vie.
-- Si le joueur touche un mauvais bloc, il perd 10 points de vie.
-- =========================================================
local LOSS_HEALTH = 10 -- perte de santé pour un mauvais choix
local GAME_TIMEOUT = 5 -- en secondes, pour un time out avent de rejouer la partie = 5
-- On récupère le dossier principal du jeu dans le Workspace
local colorsGameFolder = workspace:WaitForChild("ColorsGame")
-- On récupère la partie qui affiche la couleur de référence (celle à retrouver)
local referenceColorPart = colorsGameFolder:WaitForChild("ReferenceColor")
-- On récupère le plateau qui contient tous les blocs de couleur
local colorBoard = colorsGameFolder:WaitForChild("ColorBoard")
local winSound = colorsGameFolder.winner
local alarmSound = colorsGameFolder.alarm
-- Créer une fonction pour charger et tester un son
local function playSound(sound)
if not sound then return end
pcall(function() sound:Play() end)
end
local function chooseColor(referenceColorPart, colorBoard)
-- Cette variable va contenir la couleur de référence à retrouver
local referenceColor = nil
-- On tire un nombre au hasard entre 1 et 3 pour choisir quelle couleur dominante on va utiliser
local randomChoice = math.random(1, 3)
-- Selon le nombre tiré, on crée une couleur avec un fort rouge, vert ou bleu
-- math.random(150, 255) donne une couleur assez claire/vive, pas trop sombre
if randomChoice == 1 then
referenceColor = Color3.fromRGB(math.random(150, 255), 0, 0) -- couleur rouge
elseif randomChoice == 2 then
referenceColor = Color3.fromRGB(0, math.random(150, 255), 0) -- couleur verte
elseif randomChoice == 3 then
referenceColor = Color3.fromRGB(0, 0, math.random(150, 255)) -- couleur bleue
end
-- On applique cette couleur à la partie "ReferenceColor" pour que le joueur puisse la voir
referenceColorPart.Color = referenceColor
-- On choisit au hasard l'INDEX du bloc qui aura la bonne couleur (le bloc "gagnant")
local correctPartIndex = math.random(1, #colorBoard:GetChildren())
-- On parcourt tous les blocs présents dans le plateau de couleurs
for index, part in ipairs(colorBoard:GetChildren()) do
-- On vérifie que l'objet est bien un "BasePart" (un bloc, pas un script ou autre chose)
if part:IsA("BasePart") then
print(index, correctPartIndex)
-- Si c'est le bloc tiré au sort, on lui donne la couleur de référence (le bon bloc)
if index == correctPartIndex then
part.Color = referenceColor
else
-- Sinon, on lui donne une couleur totalement aléatoire (un bloc piège)
part.Color = Color3.fromRGB(math.random(0, 255), math.random(0, 255), math.random(0, 255))
end
end
end
return referenceColor
end
local referenceColor = chooseColor(referenceColorPart, colorBoard)
-- On parcourt tous les blocs présents dans le plateau de couleurs
for index, part in pairs(colorBoard:GetChildren()) do
-- On vérifie que l'objet est bien un "BasePart" (un bloc, pas un script ou autre chose)
if part:IsA("BasePart") then
part.Material = Enum.Material.Plastic
part.Anchored = true
part.CanCollide = true
-- On surveille si un joueur touche ce bloc
part.Touched:Connect(function(hit)
-- "hit" est la partie du joueur qui a touché le bloc (souvent une main ou un pied)
-- Son "Parent" est le personnage (le modèle) du joueur
local character = hit.Parent
if not character then return end -- si on ne trouve pas de personnage, on arrête ici
-- On cherche l'Humanoid du personnage, c'est lui qui gère la vie (Health)
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return end -- si pas d'Humanoid, on arrête ici (ce n'est pas un joueur)
if part.Color == Color3.fromRGB(255, 255, 255) then return end
-- On compare la couleur du bloc touché avec la couleur de référence
if part.Color == referenceColor then
-- Bonne couleur : le joueur récupère toute sa vie
humanoid.Health = 100
part.Color = Color3.fromRGB(255, 255, 255)
part.Material = Enum.Material.Neon
task.spawn(function()
playSound(winSound)
task.wait(GAME_TIMEOUT)
referenceColor = chooseColor(referenceColorPart, colorBoard)
part.Material = Enum.Material.Plastic end)
else
-- Mauvaise couleur : le joueur perd 10 points de vie
humanoid.Health -= LOSS_HEALTH
task.spawn(function()
playSound(alarmSound)
end)
end
end)
end
end
Conclusion
Mets-toi maintenant à la place d’un joueur qui découvre ton jeu pour la première fois : est-il facile à comprendre ? La règle est-elle claire dès les premières secondes ? Puis imagine ce même joueur devenir peu à peu un expert, capable de battre le jeu très rapidement : le jeu reste-t-il intéressant, ou devient-il trop simple ?
Ces deux points de vue, celui du débutant et celui de l’expert, sont essentiels pour améliorer un jeu vidéo : un bon jeu doit rester accessible au début, tout en offrant un défi qui évolue avec le joueur.
Tu peux maintenant partager ton jeu avec tes camarades et observer comment ils y jouent, ou continuer à le faire évoluer, par exemple en ajoutant un système de vies limitées, plusieurs niveaux de difficulté, ou un mode à plusieurs joueurs. Chaque nouvelle règle que tu ajouteras réutilisera les mêmes bases que tu viens d’apprendre : des variables pour stocker des informations, des conditions pour prendre des décisions, des boucles pour répéter des actions, des fonctions pour organiser le code, et des événements pour réagir à ce que fait le joueur.
Ce script rend un bloc poussable par le joueur en utilisant directement le moteur physique de Roblox, sans avoir besoin de code complexe pour détecter les contacts. part.Anchored = false est la ligne essentielle : elle « détache » la Part du monde et lui permet de bouger sous l’effet de forces physiques (comme la poussée du joueur). part.CanCollide = true garantit que le joueur ne traverse pas le bloc mais rentre réellement dedans, ce qui déclenche la poussée physique naturelle.
PhysicalProperties.new(...) personnalise le comportement physique du bloc : ici, une densité basse (0.2) rend l’objet léger et facile à pousser, une friction basse (0.2) le fait glisser facilement, et une élasticité (0.5) lui donne un léger effet de rebond.
part.CustomPhysicalProperties = physicalProperties applique ces réglages personnalisés à la Part, plutôt que d’utiliser les valeurs par défaut de Roblox (qui donneraient un objet plus lourd et moins glissant).
local part = script.Parent
local physicalProperties = PhysicalProperties.new(
0.2, -- Density (densité, plus c'est bas plus c'est léger à pousser)
0.2, -- Friction
0.5, -- Elasticity
1, -- FrictionWeight
1 -- ElasticityWeight
)
part.Anchored = false
part.CanCollide = true
part.CustomPhysicalProperties = physicalProperties
Pousser un bloc avec une impulsion
Ce script combine deux mécanismes pour rendre un bloc poussable :
Configuration physique de base : le bloc devient mobile (Anchored = false) et reçoit des propriétés physiques personnalisées (densité, friction, élasticité) qui définissent son comportement naturel (léger/lourd, glissant/collant, rebondissant ou non).
Poussée scriptée additionnelle : en plus de la physique naturelle, le script détecte quand un joueur touche le bloc et lui applique une impulsion (un « coup de pousse ») dans la direction où le joueur regarde/avance, grâce à ApplyImpulse.
Détail du fonctionnement
humanoidRootPart.CFrame.LookVector récupère un vecteur représentant la direction vers laquelle le joueur est orienté — c’est cette direction qui détermine où le bloc va être poussé.
part:ApplyImpulse(direction * PUSH_FORCE * part:GetMass()) calcule la force de poussée en tenant compte de la masse du bloc (GetMass()) : plus le bloc est lourd, plus il faut une impulsion importante pour le déplacer de la même façon (comme dans la vraie vie).
part.Touched:Connect(onTouched) signifie que cette poussée se déclenche à chaque fois que quelque chose touche le bloc — donc potentiellement plusieurs fois par seconde si le joueur reste collé dessus.
Pourquoi combiner les deux mécanismes ?
La physique seule (Anchored = false + CanCollide = true) suffit déjà à rendre le bloc poussable naturellement. Mais ajouter une impulsion scriptée permet de renforcer ou contrôler cet effet — par exemple pour que la poussée soit plus franche et immédiate, même si le joueur touche le bloc à faible vitesse.
QCM pour mieux comprendre
1. Que se passe-t-il si on met part.Anchored = true dans ce script ? A. Le bloc devient plus facile à pousser B. Le bloc ne pourra plus bouger du tout, malgré l’impulsion appliquée C. Le bloc disparaît D. Rien ne change
2. Que représente humanoidRootPart.CFrame.LookVector ? A. La vitesse du joueur B. La direction vers laquelle le joueur regarde/avance C. La position exacte du joueur D. Le nom du joueur
3. Pourquoi multiplie-t-on la force par part:GetMass() ? A. Pour que la poussée soit toujours la même, peu importe le poids du bloc B. Pour adapter la force de poussée au poids réel du bloc (plus lourd = plus de force nécessaire) C. Pour calculer la vitesse du joueur D. Pour rendre le bloc invisible
4. Que fait part:ApplyImpulse(...) ? A. Il déplace instantanément le bloc à une position précise B. Il applique une force ponctuelle qui pousse le bloc dans une direction donnée C. Il détruit le bloc D. Il change la couleur du bloc
5. Quand la fonction onTouched se déclenche-t-elle ? A. Une seule fois au démarrage du jeu B. À chaque fois qu’un objet (joueur ou autre) touche le bloc C. Uniquement quand le joueur saute sur le bloc D. Toutes les 5 secondes automatiquement
6. Si on augmente la valeur de PUSH_FORCE, que se passe-t-il ? A. Le bloc devient plus lourd visuellement B. Le bloc sera poussé plus fort/plus loin à chaque contact C. Le joueur devient plus rapide D. Rien, cette variable n’est pas utilisée dans le script
7. Pourquoi le script vérifie-t-il if not humanoid or not humanoidRootPart then return end ? A. Pour vérifier que c’est bien un joueur (ou personnage) qui touche le bloc, et pas un autre objet B. Pour supprimer le joueur s’il n’a pas de vie C. Pour ralentir le jeu volontairement D. Ce n’est pas nécessaire, cette ligne ne sert à rien
local part = script.Parent
local physicalProperties = PhysicalProperties.new(
0.5, -- Density (densité, plus c'est bas plus c'est léger à pousser)
0.5, -- Friction
0.5, -- Elasticity
1, -- FrictionWeight
1 -- ElasticityWeight
)
part.Anchored = false
part.CanCollide = true
part.CustomPhysicalProperties = physicalProperties
local PUSH_FORCE = 2 -- Intensité de la poussée
local function onTouched(hit)
local character = hit.Parent
local humanoid = character and character:FindFirstChild("Humanoid")
local humanoidRootPart = character and character:FindFirstChild("HumanoidRootPart")
if not humanoid or not humanoidRootPart then return end
-- Calcule la direction dans laquelle le joueur se déplace
local direction = humanoidRootPart.CFrame.LookVector
-- Applique une impulsion dans cette direction
part:ApplyImpulse(direction * PUSH_FORCE * part:GetMass())
end
part.Touched:Connect(onTouched)
Pousser plusieurs BLOCs
local partFolder = script.Parent
local physicalProperties = PhysicalProperties.new(
0.5, -- Density (densité, plus c'est bas plus c'est léger à pousser)
0.5, -- Friction
0.5, -- Elasticity
1, -- FrictionWeight
1 -- ElasticityWeight
)
local PUSH_FORCE = 20 -- Intensité de la poussée
local function onTouched(hit, part)
local character = hit.Parent
local humanoid = character and character:FindFirstChild("Humanoid")
local humanoidRootPart = character and character:FindFirstChild("HumanoidRootPart")
if not humanoid or not humanoidRootPart then return end
-- Calcule la direction dans laquelle le joueur se déplace
local direction = humanoidRootPart.CFrame.LookVector
-- Applique une impulsion dans cette direction
part:ApplyImpulse(direction * PUSH_FORCE * part:GetMass())
end
for _, part in partFolder:GetChildren() do
if part:IsA("BasePart") then
part.Anchored = false
part.CanCollide = true
part.CustomPhysicalProperties = physicalProperties
part.Touched:Connect(onTouched, part)
end
end
Le script rend ces parts automatiquement invisibles et traversables : pas besoin de le faire vous-même !
Étape 2 : Créer l’interface (GUI)
Dans StarterGui, créez un ScreenGui contenant :
Un Frame nommé Counter avec un TextLabel nommé CounterLabel (nombre de tours)
Un Frame nommé Best avec un TextLabel nommé BestLabel (meilleur temps)
Étape 3 : Placer le script
Placez ce script en tant que Script (serveur, pas LocalScript) directement dans ServerScriptService.
Comment fonctionne le script (en 6 points)
Au démarrage, le script range tous les checkpoints du dossier PartSpawnNivdans l’ordre de leurs numéros, puis les rend invisibles et traversables.
Quand un joueur touche un checkpoint, le script vérifie qui il est, puis retient dans un tableau playersProgression : la part actuelle, le nombre de tours, l’heure de départ, et le meilleur temps.
Si le joueur touche le checkpoint n°1 juste après avoir touché le DERNIER checkpoint, le script comprend qu’il a terminé un tour complet : il incrémente le compteur et compare le temps réalisé au record.
Si le joueur meurt, l’événement CharacterAdded se déclenche : le script téléporte automatiquement le joueur au-dessus du dernier checkpoint touché (et pas au tout début du parcours !).
Le tableau playersProgression retient les informations de chaque joueur séparément, grâce à son UserId (un peu comme une fiche personnelle par joueur).
Quand un joueur quitte le jeu, ses informations sont automatiquement supprimées du tableau pour ne pas garder des données inutiles en mémoire.
Pourquoi utiliser un tableau, et que contient playersProgression ?
Pourquoi a-t-on besoin d’un tableau ici ?
Imagine que plusieurs joueurs jouent en même temps sur ton parcours. Le script doit se souvenir, pour chaque joueur individuellement :
à quel checkpoint il en est
depuis quand il a commencé son tour actuel
combien de tours il a déjà faits
son meilleur temps
Si le script utilisait une seule variable simple (par exemple local spawnActuel = ...), cette variable serait partagée par tous les joueurs — dès qu’un 2ème joueur touche un checkpoint, ça écraserait la progression du 1er joueur ! Ce serait comme si tout le monde partageait le même carnet de notes : la dernière personne qui écrit efface ce que l’autre avait noté.
Un tableau permet de créer une « fiche » séparée pour chaque joueur, un peu comme un casier avec un tiroir par élève dans une salle de classe.
En Lua, une table peut fonctionner comme un dictionnaire : au lieu d’accéder à une information par un numéro de position (comme dans une liste), on y accède par une clé (un identifiant unique), et chaque clé pointe vers une valeur.
playersProgression = {
[12345] = { ... }, -- clé = UserId du joueur 1
[67890] = { ... }, -- clé = UserId du joueur 2
}
C’est comme un répertoire téléphonique : la clé, c’est le nom de la personne ; la valeur, c’est son numéro. Ici, la clé est le UserId (identifiant unique de chaque joueur sur Roblox), et la valeur est une autre table qui contient toutes les infos de progression de ce joueur précis.
Pourquoi utiliser le UserId comme clé ?
Parce que chaque joueur a un UserId unique et permanent sur Roblox — contrairement à son pseudo qui pourrait théoriquement changer, ou à sa position dans une liste qui peut varier si un joueur quitte. Utiliser le UserId garantit qu’on retrouve toujours la bonne fiche pour le bon joueur, même avec plusieurs joueurs connectés simultanément.
Le contenu détaillé de playersProgression[UserId]
Pour un joueur donné, la table stockée ressemble à ceci :
playersProgression[12345] = {
spawn = <référence à la Part checkpoint>,
course = 2, -- nombre de tours réalisés
start = 1735689042.5, -- temps de démarrage du tour
best = 45.32, -- temps du meilleur parcours
}
Clé
Signification
spawn
Le dernier checkpoint touché par ce joueur — utilisé pour savoir où faire respawn s’il meurt
course
Le nombre de tours complets déjà réalisés par ce joueur
start
L’heure exacte (tick()) à laquelle le joueur a commencé son tour actuel — sert à calculer son temps une fois arrivé
best
Le meilleur temps (le record personnel) réalisé par ce joueur sur un tour complet
QCM pour vérifier la compréhension
1. Pourquoi faut-il nommer les checkpoints « 1 », « 2 », « 3 »… et pas « Depart », « Milieu », « Fin » ? A. Pour que ce soit plus joli dans l’Explorer B. Parce que le script trie les checkpoints selon ces numéros pour connaître leur ordre C. Ça n’a aucune importance, on peut les nommer comme on veut D. Parce que Roblox l’exige pour toutes les Parts
2. Que se passe-t-il quand un joueur meurt en plein milieu du parcours ? A. Il recommence tout le parcours depuis le début B. Il respawn au dernier checkpoint qu’il a touché C. Le jeu se ferme D. Son compteur de tours est remis à zéro
3. Comment le script sait-il qu’un joueur a terminé un tour complet ? A. Quand il touche n’importe quel checkpoint B. Quand il touche le checkpoint n°1 juste après avoir touché le tout dernier checkpoint C. Quand il appuie sur une touche spéciale D. Automatiquement après 60 secondes
4. Pourquoi les checkpoints sont-ils rendus invisibles et traversables (CanCollide = false) ? A. Pour que le joueur ne les voie pas et ne soit pas bloqué en les traversant B. Pour que le jeu soit plus rapide C. Pour économiser de la mémoire D. Ce n’est pas obligatoire, c’est juste un choix esthétique
5. À quoi sert le UserId du joueur dans le tableau playersProgression ? A. À afficher le nom du joueur à l’écran B. À retenir séparément la progression de CHAQUE joueur individuellement C. À bannir les joueurs trichent D. À compter le nombre total de joueurs connectés
6. Que se passe-t-il si deux checkpoints ont accidentellement le même numéro (ex: deux parts nommées « 3 ») ? A. Rien, le script gère ça automatiquement B. Le parcours pourrait ne pas fonctionner correctement, car l’ordre ne serait plus fiable C. Le jeu affiche une erreur et s’arrête D. Roblox choisit au hasard laquelle utiliser
7. Pourquoi utilise-t-on un Script (serveur) et pas un LocalScript pour ce système ? A. Parce que le compteur de tours et le meilleur temps doivent être valables pour tout le monde, gérés par un seul endroit fiable (le serveur) B. Parce que les LocalScripts ne fonctionnent pas avec les Parts C. Ça n’a pas d’importance, les deux marcheraient pareil D. Parce que c’est plus rapide à écrire
-- ═══════════════════════════════════════
-- SERVICES ET RÉFÉRENCES
-- ═══════════════════════════════════════
local Players = game:GetService("Players")
local Game = workspace.GAME
local SpawnFolder = Game:WaitForChild("PartSpawnNiv", true)
local baseplate = workspace:FindFirstChild("Baseplate")
if baseplate then
baseplate:Destroy()
end
-- Tableau des joueurs en cours de partie avec l'étape mémorisée
local playersProgression = {}
-- Tableau de toutes les étapes du jeu, triées par nom pour garantir l'ordre
local spawnsList = SpawnFolder:GetChildren()
table.sort(spawnsList, function(a, b)
return tonumber(a.Name) < tonumber(b.Name)
end)
local lastSpawnIndex = #spawnsList
local function formatTime(t)
local minutes = math.floor(t / 60)
local seconds = math.floor(t % 60)
local centiemes = math.floor((t * 100) % 100)
return string.format("%02d:%02d.%02d", minutes, seconds, centiemes)
end
-- ═══════════════════════════════════════
-- RESPAWN AU DERNIER CHECKPOINT
-- ═══════════════════════════════════════
local function respawnAuCheckpoint(player)
local progression = playersProgression[player.UserId]
if not progression or not progression.spawn then return end
local character = player.Character
if not character then return end
local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
if not humanoidRootPart then return end
local spawn = progression.spawn
local targetPosition = spawn.Position + Vector3.new(0, (spawn.Size.Y / 2) + 2, 0)
humanoidRootPart.CFrame = CFrame.new(targetPosition)
end
-- ═══════════════════════════════════════
-- GESTION DU CHARGEMENT DES JOUEURS
-- ═══════════════════════════════════════
local function initPlayer(player)
player.CharacterAdded:Connect(function(character)
character:WaitForChild("HumanoidRootPart")
respawnAuCheckpoint(player)
end)
end
Players.PlayerAdded:Connect(initPlayer)
Players.PlayerRemoving:Connect(function(player)
playersProgression[player.UserId] = nil
end)
-- ═══════════════════════════════════════
-- CHARGEMENT DES ÉTAPES ET DÉTECTION DES CHECKPOINTS
-- ═══════════════════════════════════════
for index, spawn in ipairs(spawnsList) do
if spawn:IsA("BasePart") then
spawn.Transparency = 1
spawn.CanCollide = false
spawn.Anchored = true
spawn.Touched:Connect(function(hit)
local player = Players:GetPlayerFromCharacter(hit.Parent)
if not player then return end
local progression = playersProgression[player.UserId]
-- Ignore si le joueur est déjà sur ce checkpoint
if progression and progression.spawn == spawn then return end
if not progression then
playersProgression[player.UserId] = {
spawn = spawn,
course = 0,
start = tick(),
best = 0,
counterLabel = player.PlayerGui.ScreenGui.Counter.CounterLabel,
bestLabel = player.PlayerGui.ScreenGui.Best.BestLabel,
}
return
end
-- Tour complet détecté : dernier checkpoint -> premier checkpoint
if index == 1 and progression.spawn == spawnsList[lastSpawnIndex] then
progression.course += 1
progression.counterLabel.Text = string.format("%02d", progression.course)
local finishTime = tick() - progression.start
if progression.best == 0 or finishTime < progression.best then
progression.best = finishTime
end
progression.bestLabel.Text = formatTime(progression.best)
end
progression.spawn = spawn
progression.start = tick()
end)
end
end