Catégories
Jeu vidéo ROBLOX ROBLOX-NIVEAU03

Comment déplacer des parts

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.

Explications

1. Qu’est-ce qu’un espace vectoriel ? (Rappel maths)

En mathématiques, un espace vectoriel est un ensemble où on peut :

  • Additionner des vecteurs (ex: (x1, y1, z1) + (x2, y2, z2) = (x1+x2, y1+y2, z1+z2)).
  • Multiplier un vecteur par un nombre (ex: 2 * (x, y, z) = (2x, 2y, 2z)).
  • Représenter des points dans l’espace avec des coordonnées (x, y, z).

Exemple concret en 2D :
Si tu as un point à (2, 3) et que tu le déplaces de (1, -1), il arrive à (3, 2).

2. Roblox comme espace vectoriel

Dans Roblox, tout est un vecteur :

  • La position d’un objet est un vecteur (X, Y, Z) (en studs, l’unité de mesure de Roblox).
  • Le déplacement d’un objet utilise des vecteurs (ex: part.Position = part.Position + Vector3.new(1, 0, 0) déplace l’objet de 1 stud vers la droite).
  • Les directions sont aussi des vecteurs (ex: Vector3.new(0, 1, 0) = vers le haut).

Comparaison maths ↔ Roblox :

Concept en mathsÉquivalent dans RobloxExemple
Vecteur (x, y, z)Vector3.new(x, y, z)Vector3.new(1, 0, 0) = 1 stud vers la droite
Addition de vecteursvector1 + vector2Vector3.new(1, 0, 0) + Vector3.new(0, 1, 0) = Vector3.new(1, 1, 0)
Position d’un pointpart.Positionpart.Position = Vector3.new(5, 2, 0)
Déplacementpart.Position += vectorpart.Position += Vector3.new(0, 1, 0) (monte de 1 stud)

3. Objectif du jeu

Tu vas créer un jeu où :

  1. Le joueur entre dans une zone de jeu (hitbox).
  2. Une interface apparaît avec des boutons pour déplacer un bloc dans les 3 dimensions (haut/bas, gauche/droite, avant/arrière).
  3. Le bloc doit toucher des clés de couleurs spécifiques dans l’ordre pour ouvrir une porte.
  4. Quand toutes les clés sont collectées, la porte s’ouvre et le joueur gagne !

Pourquoi c’est intéressant ?

  • Tu vas visualiser les vecteurs en 3D.
  • Tu vas manipuler des coordonnées comme en maths, mais de manière interactive.
  • Tu vas comprendre comment les jeux vidéo utilisent les maths pour les déplacements.

QCM / Questions

1. Qu’est-ce qu’un vecteur en maths ?

  • a) Un nombre.
  • b) Une flèche avec une direction et une longueur (ex: (x, y, z)).
  • c) Une couleur.

2. Dans Roblox, comment représente-t-on un vecteur en 3D ?

  • a) Vector2.new(x, y)
  • b) Position.new(x, y, z)
  • c) Vector3.new(x, y, z)

3. Si un objet est à la position (2, 3, 0) et qu’on ajoute Vector3.new(1, -1, 0), où sera-t-il ?

  • a) (3, 2, 0)
  • b) (1, 2, 0)
  • c) (2, 4, 0)

Documentation Roblox Lua :


Étape 1 : Comprendre le jeu avant de coder

But : Visualiser le jeu et comprendre son fonctionnement.
Concepts clés : Logique de jeu, objets 3D, vecteurs.

Explications

Scénario du jeu :

  1. Le joueur entre dans une zone de jeu (hitbox).
  2. Une interface avec 6 boutons apparaît :
    • Haut / Bas : Déplace le bloc sur l’axe Y (vertical).
    • Gauche / Droite : Déplace le bloc sur l’axe Z (profondeur).
    • Avant / Arrière : Déplace le bloc sur l’axe X (horizontal).
  3. Le bloc doit toucher des clés colorées dans un ordre précis (ex: rouge → bleu → vert).
  4. Quand toutes les clés sont touchées, la porte s’ouvre !

Objets nécessaires :

ObjetRôlePosition exemple
HitBoxZone où le joueur peut interagirZone invisible
Part (bloc mobile)Bloc à déplacer pour toucher les clésPart fluo
WallPorte à ouvrirMur de séparation
Key1, Key2, Key3Clés à collecter (couleurs : rouge, bleu, vert)Parts de couleurs à mettre sur le mur
ScreenGuiInterface avec les boutonsDans StarterGui

À toi de jouer !

  1. Dans Roblox Studio, crée la structure suivante :
    • Un dossier VectorSpace dans Workspace.
    • Dans ce dossier, ajoute :
      • Une Part nommée HitBox (taille : 10, 1, 10, ancrée, transparence : 0.5).
      • Une Part nommée Part (le bloc mobile, taille : 2, 2, 2, couleur : rouge, ancrée).
      • Une Part nommée Wall (la porte, taille : 10, 5, 1, ancrée, couleur : gris).
      • 3 Parts nommées Key1, Key2, Key3 (tailles : 1, 1, 1, couleurs : rouge, bleu, vert, ancrées).
  2. Positionne les objets comme dans le tableau ci-dessus.
  3. Teste : Déplace-toi dans la HitBox et imagine que tu dois déplacer le bloc rouge pour toucher les clés.

QCM / Questions

1. Quel axe utilise-t-on pour monter/descendre ?

  • a) X
  • b) Y
  • c) Z

2. Quel axe utilise-t-on pour aller à gauche/droite ?

  • a) X
  • b) Y
  • c) Z

3. Quel axe utilise-t-on pour avancer/reculer ?

  • a) X
  • b) Y
  • c) Z

Étape 2 : Créer la structure des objets dans l’Explorateur

But : Organiser les objets du jeu dans l’Explorateur pour faciliter la programmation.
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 (hitbox, bloc mobile, clés, mur).
  • ServerScriptService : Contient les scripts côté serveur (pour la logique du jeu).
  • StarterGui : Contient les interfaces utilisateur (boutons pour déplacer le bloc).
  • ReplicatedStorage : Contient les modèles réutilisables (non utilisé ici, mais utile pour cloner des objets).

Structure recommandée :

Workspace
├── VectorSpace (Folder)
│   ├── HitBox (Part)
│   ├── Part (Part, bloc mobile)
│   ├── Wall (Part, porte)
│   ├── Key1 (Part, clé rouge)
│   ├── Key2 (Part, clé bleue)
│   └── Key3 (Part, clé verte)
StarterGui
└── ScreenGui (Folder)
    └── VectorSpace (Frame)
        ├── UpButton (TextButton)
        ├── DownButton (TextButton)
        ├── LeftButton (TextButton)
        ├── RightButton (TextButton)
        ├── FrontButton (TextButton)
        └── BackButton (TextButton)
ServerScriptService
└── VectorSpaceScript (Script)

À toi de jouer !

  1. Crée la structure ci-dessus dans Roblox Studio.
  2. Pour l’interface :
    • Dans StarterGui, ajoute un ScreenGui.
    • 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.
  3. 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 :

  1. On vérifie que c’est bien un personnage de joueur (et pas un autre objet).
  2. On active l’interface pour ce joueur.
  3. 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

LigneExplication
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 !

  1. Copie le code dans le Script VectorSpaceScript.
  2. Teste :
    • Entre dans la hitbox avec ton personnage.
    • Ouvre la fenêtre de sortie (Affichage > Sortie) pour voir les messages.
  3. 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).
  • Joue un son quand le joueur entre dans la zone.

Documentation Roblox Lua :


É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 PlayerGui cô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

  1. 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 !

  1. Modifie le script serveur dans ServerScriptService.
  2. Teste :
    • Lance le jeu : l’interface ne doit pas être visible.
  3. 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).
  • Ajoute un son quand un bouton est cliqué.

Documentation Roblox Lua :


Étape 6 : Lier l’interface au Script serveur et déplacer le bloc

But : Déplacer le bloc avec les boutons et vérifier les collisions possibles.
Concepts clés :

Explications

Pour déplacer le bloc, on utilise des vecteurs :

  • Haut/Bas : Vector3.new(0, MOVE_STEP, 0) (axe Y).
  • Gauche/Droite : Vector3.new(0, 0, MOVE_STEP) (axe Z).
  • Avant/Arrière : Vector3.new(MOVE_STEP, 0, 0) (axe X).

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 :

LigneExplication
moveMovablePart(axisVector, directionSign)Déplace le bloc dans la direction spécifiée, si aucun obstacle ne bloque le chemin.
RaycastVé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 !

  1. Copie le code dans le Script serveur
  2. 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.
  3. 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.

Documentation Roblox Lua :


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

  1. Utiliser workspace:GetPartsInPart(movablePart, overlapParams) pour récupérer toutes les parties qui touchent le bloc mobile.
  2. Parcourir les parties retournées et vérifier si l’une d’elles est une clé.
  3. Si c’est une clé, vérifier sa couleur et la comparer à la prochaine couleur attendue (KEY_COLORS[1]).
  4. 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

LigneExplication
KEY_COLORS = {Color3.fromRGB(255, 0, 0), ...}Définit l’ordre des couleurs des clés à collecter.
workspace:GetPartsInPart(movablePart, overlapParams)Récupère toutes les parties qui touchent le bloc mobile.
touchingPart.Name == "Key"Vérifie si la partie touchée est une clé.
touchingPart.Color == KEY_COLORS[1]Vérifie si la couleur de la clé correspond à la prochaine couleur attendue.
table.remove(KEY_COLORS, 1)Retire la première couleur de la liste (car la clé a été collectée).
movablePart.Color = KEY_COLORS[1]Change la couleur du bloc pour indiquer la prochaine clé à trouver.
openWall()Détruit le mur et le bloc mobile (victoire).

À toi de jouer !

  1. Copie le code dans le Script serveur.
  2. Teste :
    • Déplace le bloc pour toucher la première clé (rouge) : elle doit disparaître, et le bloc doit devenir bleu.
    • Touche la clé bleue, puis la verte : le mur doit s’ouvrir !
  3. Modifie :
    • Change l’ordre des couleurs dans KEY_COLORS (ex: {Color3.fromRGB(0, 255, 0), Color3.fromRGB(255, 0, 0)}).
    • Ajoute une 4ème clé avec une nouvelle couleur (ex: jaune).

QCM / Questions

1. Pourquoi utilise-t-on workspace:GetPartsInPart ?

  • a) Parce que .Touched ne fonctionne pas entre deux Parts ancrées.
  • b) Parce que c’est plus rapide.
  • c) Parce que c’est plus facile à coder.

2. Que fait table.remove(KEY_COLORS, 1) ?

  • a) Ajoute une couleur à la liste.
  • b) Retire la première couleur de la liste.
  • c) Change la couleur du bloc.

3. Pourquoi utilise-t-on OverlapParams ?

  • a) Pour filtrer les parties à détecter (ex: exclure la hitbox).
  • b) Pour changer la taille du bloc.
  • c) Pour détruire le bloc.

Pour aller plus loin

  • Ajoute un son quand une clé est collectée.
  • Affiche un message à l’écran quand une clé est collectée (utilise un TextLabel dans le ScreenGui).

Documentation Roblox Lua :

table.remove

GetPartsInPart

OverlapParams

Color3.fromRGB


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

  1. Déconnecter les connexions quand un joueur quitte la zone ou le jeu.
  2. Nettoyer les références aux objets détruits (ex: movablePart = nil après sa destruction).
  3. 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

OptimisationExplication
playerButtonConnections[player] = nilNettoie 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 joueursAméliore l’expérience utilisateur.

À toi de jouer !

  1. Copie le script final dans VectorSpaceScript.
  2. 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.
  3. Modifie :
    • Ajoute une 4ème clé avec une nouvelle couleur (ex: jaune).
    • Change MOVE_STEP à 0.5 pour des déplacements plus précis.

QCM / Questions

1. Pourquoi utilise-t-on playerButtonConnections[player] = nil ?

  • a) Pour éviter les fuites mémoire.
  • b) Pour changer le nom du joueur.
  • c) Pour détruire le joueur.

Pour aller plus loin

  • Ajoute un chronomètre : Limite le temps pour collecter toutes les clés.
  • Ajoute un système de score : Calcul le temps mis par le joueur pour ouvrir la porte.
  • Ajoute des obstacles : Des murs que le bloc ne peut pas traverser.

Catégories
Jeu vidéo ROBLOX ROBLOX-NIVEAU03

Détruis une pyramide de briques

Dans ce tutoriel, tu vas créer un jeu où le joueur doit détruire une pyramide de briques en lançant une balle.
Voici ce que tu vas apprendre :

  • Construire une pyramide dynamiquement avec des tailles aléatoires.
  • Détecter les interactions (quand le joueur entre dans une zone, appuie sur un bouton, etc.).
  • Lancer une balle et vérifier si la pyramide est détruite.
  • Gérer des effets comme le recul du joueur ou la caméra en première personne.

Étape 1 : Comprendre le jeu avant de coder

But : Visualiser le jeu et comprendre son fonctionnement.
Concepts clés : Logique de jeu, objets 3D.

Explications

Avant de coder, il faut imaginer le jeu :

  1. Une pyramide de briques est générée automatiquement au début.
  2. Le joueur entre dans une zone de lancer (hitbox).
  3. En appuyant sur un bouton (ProximityPrompt), il lance une balle.
  4. Si la balle détruit toutes les briques de la pyramide, le joueur gagne des points et est éjecté de la zone.
  5. Sinon, il perd un point et peut réessayer.

Objets nécessaires :

  • Une hitbox (zone invisible pour détecter le joueur).
  • Un modèle de balle (à cloner pour chaque lancer).
  • Un modèle de brique (pour construire la pyramide).
  • Un ProximityPrompt (bouton pour lancer la balle).
  • Un détecteur de pyramide (zone pour vérifier si des briques restent debout).

À toi de jouer !

  1. Dans Roblox Studio, ajoute :
    • Une Part (pour la hitbox) dans Workspace.
    • Un Modèle de balle (Part avec une forme de sphère) dans Workspace.
    • Un Modèle de brique (Part cubique) dans Workspace.
    • Un ProximityPrompt (attache-le à la hitbox).
  2. Teste : Déplace ton personnage dans la hitbox et imagine le gameplay.

QCM / Questions

Quels objets sont nécessaires pour construire la pyramide ?

  • a) Une hitbox, une balle, un ProximityPrompt.
  • b) Un modèle de brique, un détecteur de pyramide.
  • c) Un script, une image, un son.

Documentation Roblox Lua :


É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 !

  1. Crée un dossier BreakPyramid dans Workspace.
  2. 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).
  3. 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

LigneExplication
for i = 1, PYRAMID_PARTS_COUNT doBoucle 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 = trueEmpêche la brique de bouger pendant la construction.
task.wait(0.1)Attend 0.1 seconde pour que tout soit bien positionné.
part.Anchored = falsePermet à la brique de bouger (physique activée).

À toi de jouer !

  1. Copie le code dans un Script dans ServerScriptService.
  2. Teste :
    • Lance le jeu : une pyramide de 8 briques doit apparaître.
    • Observe les tailles et les rotations aléatoires.
  3. 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.

Documentation Roblox Lua :


É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

LigneExplication
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 !

  1. Copie le code dans le Script existant.
  2. Teste :
    • Entre dans la hitbox avec ton personnage.
    • Ouvre la fenêtre de sortie (Affichage > Sortie) pour voir les messages.
  3. 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).
  • Joue un son quand le joueur entre dans la zone.

Documentation Roblox Lua :


É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

Explications du code

LigneExplication
player.CameraMode = Enum.CameraMode.LockFirstPersonForce la vue à la première personne.
player.CameraMinZoomDistance = 0.5Définit la distance minimale de zoom.
player.CameraMaxZoomDistance = 0.5Définit la distance maximale de zoom.
task.wait(0.1)Attend 0.1 seconde pour éviter les bugs de caméra.
resetCamera(player)Réinitialise la caméra en vue classique.

À toi de jouer !

  1. Copie le code dans le Script.
  2. Teste :
    • Entre dans la hitbox : la vue doit passer en première personne.
    • Quitte la hitbox : la vue doit revenir en classique.
  3. Modifie :
    • Change CameraMinZoomDistance à 1.0 et observe la différence.
    • Essaie de désactiver LockFirstPerson et utilise Enum.CameraMode.FirstPerson pour voir 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 (verrouillée).
  • c) Désactive la caméra.

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.

Documentation Roblox Lua :


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

  1. Cloner la balle depuis ReplicatedStorage.
  2. Positionner la balle devant le joueur.
  3. 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

LigneExplication
ballTemplate.Parent = ServerStorageDé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 !

  1. 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
  2. Copie le code dans le Script.
  3. Teste :
    • Entre dans la hitbox.
    • Appuie sur E (ou la touche définie pour le ProximityPrompt) : une balle doit être lancée !
  4. 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.

Pour aller plus loin

  • Ajoute un son quand la balle est lancée.

Documentation Roblox Lua :


Étape 7 : Détecter que la pyramide est détruite

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 :

  1. Compter le nombre de briques encore debout dans la zone de détection (ControlPyramid).
  2. 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

LigneExplication
workspace:GetPartsInPart(pyramidDetector)Récupère toutes les parties qui touchent ControlPyramid.
for _, part in ipairs(touchingParts) doParcourt toutes les parties en contact avec ControlPyramid.
if part.Name == "pyramid" thenVé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 !

  1. Copie le code dans le Script.
  2. Teste :
    • Entre dans la hitbox et lance des balles jusqu’à détruire la pyramide.
    • Observe le recul du joueur quand il réussit.
  3. 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.

Documentation Roblox Lua :

Catégories
Jeu vidéo ROBLOX ROBLOX-NIVEAU03

Jouer au basket

Roblox Studio est un outil qui permet de créer des jeux vidéo en utilisant le langage Lua. Aujourd’hui, nous allons créer un mini-jeu de basket où :

  • Le joueur entre dans une zone de lancer.
  • Il appuie sur la touche F pour lancer un ballon.
  • Si le ballon entre dans le panier, il marque des points.

Avant de coder, il faut imaginer le jeu :

  • Le joueur entre dans une zone de lancer (une hitbox).
  • En appuyant sur F, il lance une balle.
  • Si la balle touche le panier, il gagne des points.

Objets nécessaires :

  • Une hitbox (zone invisible pour détecter le joueur).
  • Un ballon (modèle 3D).
  • Un panier (pour marquer des points).
  • Un script (pour gérer les règles du jeu).

À toi de jouer !

  1. Dans Roblox Studio, ajoute :
    • Une Part (pour la hitbox) dans Workspace.
    • Un Ball (modèle de balle) dans ReplicatedStorage.
    • Un Basket (panier) dans Workspace.
  2. Teste : Déplace ton personnage dans la hitbox et imagine le gameplay.

Étape 2 : Créer la structure des objets dans l’Explorateur

But : Organiser les objets du jeu dans l’Explorateur pour faciliter la programmation.
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 (balles, paniers, etc.).
  • ServerScriptService : Contient les scripts côté serveur (pour la logique du jeu).
  • ReplicatedStorage : Contient les modèles réutilisables (comme la balle).
  • StarterGui : Contient les interfaces utilisateur (pour afficher des messages).

Exemple :

textCopier

Workspace
├── Basket (Folder)
│   ├── HitBox (Part)
│   ├── Ball (Model)
│   └── ControlBall (Part)
ReplicatedStorage
└── FireInputEvent (RemoteEvent)

É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

LigneExplication
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.ParentRé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).
  • Classic : Vue classique (derrière le personnage).

Exemple :

player.CameraMode = Enum.CameraMode.LockFirstPerson

Code Lua

Ajoute ces fonctions au script précédent :

luaCopier

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

Explications du code

LigneExplication
player.CameraMode = Enum.CameraMode.LockFirstPersonForce la vue à la première personne.
player.CameraMinZoomDistance = 0.5Définit la distance minimale de zoom.
task.wait(0.1)Attend 0.1 seconde pour éviter les bugs.

À toi de jouer !

  1. Copie le code dans ton script.
  2. Teste :
    • Entre dans la hitbox : la vue doit passer en première personne.
  3. 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.

Documentation Roblox Lua :


É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

  1. Crée un LocalScript dans StarterPlayerScripts.
  2. 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

LigneExplication
local player = Players.LocalPlayerRé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 !

  1. Crée un RemoteEvent nommé ReadyEvent dans ReplicatedStorage.
  2. Copie le code du LocalScript dans StarterPlayerScripts.
  3. Teste :
    • Entre dans la hitbox : le message doit s’afficher.
  4. 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).
  • c) Change la taille de l’interface.

Documentation Roblox Lua :


É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

LigneExplication
UserInputService.InputBegan:Connect(...)Écoute quand une touche est pressée.
input.KeyCode == Enum.KeyCode.FVérifie si la touche F a été pressée.
humanoidRootPart.CFrame.LookVectorRécupère la direction dans laquelle le joueur regarde.
fireInputEvent:FireServer(lookDirection)Envoie la direction au serveur via un RemoteEvent.

À toi de jouer !

  1. Crée un RemoteEvent nommé FireInputEvent dans ReplicatedStorage.
  2. Copie le code dans le LocalScript.
  3. Teste :
    • Entre dans la hitbox.
    • Appuie sur F : rien ne se passe encore (le serveur ne gère pas encore l’événement).
  4. 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.

Documentation Roblox Lua :


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

  1. Vérifier que le joueur a le droit de lancer.
  2. Cloner la balle depuis ReplicatedStorage.
  3. 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

LigneExplication
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 < 2Vérifie si 2 secondes se sont écoulées depuis le dernier lancer.

À toi de jouer !

  1. Copie le code dans le Script côté serveur.
  2. Teste :
    • Entre dans la hitbox.
    • Appuie sur F : une balle doit être lancée !
  3. 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.

Documentation Roblox Lua :


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

  1. Ajoute une Part (nommée ControlBall) dans le panier.
  2. Utilise .Touched pour détecter la collision.
  3. 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

LigneExplication
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 !

  1. Ajoute une Part nommée ControlBall dans le panier (dans Basket).
  2. Ajoute un son nommé claps dans Basket (tu peux importer un son depuis Roblox Library).
  3. Copie le code dans le Script.
  4. 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).
  • Affiche les points à l’écran avec un TextLabel.

Documentation Roblox Lua :

Catégories
Jeu vidéo ROBLOX ROBLOX-NIVEAU03

Crée ton propre Quiz de calcul

Introduction

Bienvenue dans ce tutoriel ! Tu vas apprendre à programmer en Lua avec Roblox Studio en construisant un vrai mini-jeu : le Quiz de calcul.

Le principe est simple : un joueur marche jusqu’à une zone(on appelle ça une Hitbox). Dès qu’il entre dans cette zone, un QCM de calcul apparaît à l’écran avec 4 boutons de réponse. S’il clique sur la bonne réponse, le jeu devient un peu plus difficile (des nombres plus grands, puis une nouvelle opération : addition, soustraction, multiplication, division). S’il se trompe, il perd de la vie.

C’est un jeu qui combine deux choses : de la logique de programmation (variables, boucles, fonctions, conditions, événements) et des maths (le calcul mental). Tu vas construire ce jeu étape par étape, en testant à chaque fois ce que tu écris.

Ce dont tu as besoin :

  • Roblox Studio installé et ouvert
  • Un nouveau projet vide (« Baseplate » par exemple)
  • De la curiosité, et l’envie de te tromper pour apprendre !

Une variable, c’est comme une boîte étiquetée dans laquelle on range une information (un nombre, un texte…). Une fonction, c’est comme une recette de cuisine : une suite d’instructions qu’on peut réutiliser autant de fois qu’on veut. Garde ces images en tête, on va s’en servir tout le long du tutoriel.

Étape 1 : Comprendre le jeu avant de coder

But : avant d’écrire la moindre ligne de code, il faut savoir exactement ce que le jeu doit faire. C’est une étape que beaucoup de débutants sautent, et c’est une erreur : coder sans plan, c’est comme construire une maison sans plan de l’architecte.

Concepts clés : analyse d’un problème, cahier des charges.

Explications

Voici les règles de notre jeu, écrites en langage humain (pas encore en code) :

  1. Un joueur touche une zone au sol (la Hitbox).
  2. Une question de calcul apparaît : un nombre à trouver, et 4 boutons de réponse possibles.
  3. Un seul bouton contient la bonne décomposition (par exemple si le nombre cible est 14, la bonne réponse pourrait être « 9 + 5 »).
  4. Si le joueur clique sur la bonne réponse : une nouvelle question apparaît, un peu plus difficile.
  5. Si le joueur clique sur une mauvaise réponse : il perd des points de vie.
  6. Quand le joueur sort de la zone, le quiz disparaît.
  7. Au fil des bonnes réponses, le jeu change d’opération : d’abord des additions, puis des soustractions, puis des multiplications, puis des divisions.

À toi de jouer !

Avant de coder quoi que ce soit, prends une feuille et réponds à ces questions :

  • Quelles informations le jeu doit-il « retenir » en permanence ? (Indice : la vie du joueur, la difficulté actuelle…)
  • Quels sont les événements qui déclenchent une action ? (Indice : « toucher » une zone, « cliquer » sur un bouton…)

QCM

1. Pourquoi analyse-t-on le jeu avant de coder ?

  • a) Parce que c’est obligatoire dans Roblox Studio
  • b) Pour savoir précisément ce qu’on doit programmer et éviter de se perdre
  • c) Pour que le code soit plus court

2. Dans notre jeu, quel événement déclenche l’apparition du quiz ?

  • a) Le joueur appuie sur une touche du clavier
  • b) Le joueur entre en contact avec la Hitbox
  • c) Le joueur atteint un score de 10

Pour aller plus loin

Imagine une variante du jeu (un quiz de vocabulaire, un quiz de géographie…) et écris ses règles sur ta feuille, de la même façon que ci-dessus.

Étape 2 : Créer la structure des objets dans l’Explorer

But : construire dans Roblox Studio les objets (les « briques ») sur lesquels ton script va s’appuyer, avant même d’écrire du code.

Concepts clés : hiérarchie d’objets, Explorer, Workspace, ScreenGui.

Explications

Dans Roblox, tout ce qui existe dans le jeu (une partie de décor, une zone invisible, une interface) est un objet, rangé dans une arborescence visible dans la fenêtre Explorer. C’est comme les dossiers et sous-dossiers sur un ordinateur.

Pour notre jeu, on a besoin de :

Dans Workspace :

  • Un dossier nommé NumberGame
    • Une Part (bloc) nommée HitBox, transparente, non collisionnable (CanCollide = false), qui représente la zone au sol

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

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

À toi de jouer !

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

Teste : tu dois voir dans l’Explorer une arborescence qui ressemble à un plan bien rangé, sans avoir encore écrit une ligne de code.

QCM

1. Où doit-on placer la Hitbox du jeu ?

  • a) Dans StarterGui
  • b) Dans Workspace
  • c) Dans ServerScriptService

2. À quoi sert un ScreenGui ?

  • a) À afficher des éléments d’interface à l’écran du joueur
  • b) À déplacer le personnage
  • c) À créer une zone de collision

Pour aller plus loin

Documentation Roblox sur la hiérarchie des objets

Étape 3 : Créer le premier script — LocalScript ou Script ?

But : comprendre la différence entre un script qui tourne côté serveur et un script qui tourne côté joueur, puis choisir le bon pour notre jeu.

Concepts clés : Script (serveur), LocalScript (client), sécurité, autorité serveur.

Explications

Dans Roblox, il existe deux grandes familles de scripts :

  • Le Script s’exécute sur le serveur. Il ne tourne qu’une seule fois pour tout le monde, et voit tous les joueurs en même temps. C’est le serveur qui a toujours raison (« l’autorité »), un joueur ne peut pas le tricher.
  • Le LocalScript s’exécute sur l’ordinateur du joueur (le « client »). Il est rapide pour l’affichage, mais un joueur malveillant peut le modifier ou le désactiver pour tricher.

Notre choix : un Script placé dans ServerScriptService.

Pourquoi ? Parce que notre jeu doit :

  • Détecter tous les joueurs qui touchent la même Hitbox partagée dans le monde (pas propre à un seul joueur) ;
  • Faire perdre de la vie de façon fiable : si c’était géré uniquement côté joueur, un tricheur pourrait modifier son propre script pour ne jamais perdre de vie ;
  • Le serveur peut quand même modifier l’affichage (les TextLabel, TextButton) dans l’interface d’un joueur précis : les changements de propriétés faits par le serveur sont automatiquement envoyés (« répliqués ») à l’écran du joueur.

Pour un jeu plus avancé, on séparerait souvent la logique (serveur) de l’affichage (client) avec des RemoteEvent, mais pour ce tutoriel, un seul Script serveur suffit et reste plus simple à comprendre.

À toi de jouer !

  1. Dans l’Explorer, clique droit sur ServerScriptService → Insert Object → Script.
  2. Renomme-le NumberGameScript.
  3. Ouvre-le et écris juste ceci pour vérifier que tout fonctionne :
-- Ce print doit apparaître dans la console (fenêtre Output) au lancement du jeu
print("Le script du Number Game a bien démarré !")
  1. Clique sur Play (▶) en haut de Roblox Studio.
  2. Ouvre la fenêtre Output (View → Output) : tu dois voir ton message s’afficher.

QCM

1. Où s’exécute un Script (et non un LocalScript) ?

  • a) Sur l’ordinateur du joueur
  • b) Sur le serveur
  • c) Dans le navigateur internet

2. Pourquoi ne pas gérer la perte de vie uniquement avec un LocalScript ?

  • a) Parce que les LocalScript n’existent pas dans Roblox
  • b) Parce qu’un joueur pourrait modifier son propre script pour tricher
  • c) Parce que c’est plus lent

Pour aller plus loin

Documentation Roblox — Script vs LocalScript

Étape 4 : Écrire le code, petit à petit

But : construire la logique du jeu en plusieurs petites briques, du plus simple au plus complexe, au lieu d’écrire tout le script d’un coup.

Concepts clés : variables, fonctions, conditions if/else, boucles for, événements.

On ne va pas écrire tout le script NumberGameScript d’un coup : ce serait trop pour bien comprendre. On va le construire en 4 sous-étapes.

4.1 — Les variables et les constantes

Explications : une variable stocke une information qui peut changer (par exemple la vie perdue à chaque erreur). Une constante est une variable qu’on ne modifiera jamais dans le code, mais qui est facile à régler en un seul endroit (par exemple le nombre de points de vie perdus).

-- ==================== CONSTANTES ====================
local HEALTH_LOSS = 10        -- Points de vie perdus pour une mauvaise réponse
local MIN_DIFFICULTY = 1      -- Difficulté minimale (nombre de chiffres)
local MAX_DIFFICULTY = 3      -- Difficulté maximale (nombre de chiffres)

-- ==================== SERVICES ====================
local Players = game:GetService("Players")

-- ==================== RÉFÉRENCES WORKSPACE ====================
local numberGameFolder = workspace:WaitForChild("NumberGame")
local hitbox = numberGameFolder:WaitForChild("HitBox")

WaitForChild demande à Roblox d’attendre que l’objet existe avant de continuer : c’est important car le jeu peut mettre quelques instants à tout charger.

4.2 — Une fonction pour générer une question d’addition

Explications : une fonction permet de regrouper des instructions sous un nom, pour les réutiliser facilement. Ici, on écrit une version très simplifiée qui ne gère qu’une addition, avant d’ajouter la soustraction, la multiplication et la division plus tard.

local function generateSimpleAddition(maxValue)
    -- On choisit un nombre cible au hasard
    local targetNumber = math.random(2, maxValue)
    -- On choisit un premier morceau, le deuxième complète jusqu'au nombre cible
    local part1 = math.random(1, targetNumber - 1)
    local part2 = targetNumber - part1
    return targetNumber, part1, part2
end

-- Test rapide
local target, a, b = generateSimpleAddition(20)
print(target, "=", a, "+", b)

4.3 — L’événement Touched : détecter le joueur

Explications : un événement, c’est quelque chose qui se produit dans le jeu (un contact, un clic…) auquel on peut « réagir » avec une fonction. Touched se déclenche quand un objet touche la HitBox.

hitbox.Touched:Connect(function(hit)
    local character = hit.Parent
    if not character then return end -- si "hit" n'a pas de parent, on arrête

    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if not humanoid then return end -- ce n'est pas un personnage de joueur

    local player = Players:GetPlayerFromCharacter(character)
    if not player then return end

    print(player.Name .. " a touché la Hitbox !")
end)

Remarque le if ... then return end : c’est une condition qui arrête la fonction tout de suite si quelque chose ne va pas. C’est une façon très courante d’écrire des vérifications de sécurité en Lua.

4.4 — Les 4 boutons de réponse

Explications : on utilise une boucle for avec ipairs pour parcourir chaque bouton de la liste, et on connecte un événement de clic (MouseButton1Click) sur chacun.

local function getPlayerGuiElements(player)
    local playerGui = player:WaitForChild("PlayerGui")
    local screenGui = playerGui:WaitForChild("ScreenGui")
    local numberDisplay = screenGui:WaitForChild("NumberGenerator")
    local answerButtons = screenGui:WaitForChild("Answers")
    return numberDisplay, answerButtons
end

local numberDisplay, answerButtons = getPlayerGuiElements(somePlayer)

for index, part in ipairs(answerButtons:GetDescendants()) do
    if part:IsA("TextButton") then
        part.MouseButton1Click:Connect(function()
            print("Bouton numéro " .. index .. " cliqué")
        end)
    end
end

À toi de jouer !

Recopie chaque sous-étape (4.1 à 4.4) l’une après l’autre dans ton NumberGameScript, en testant à chaque fois avec Play. Ne passe pas à la sous-étape suivante tant que la précédente n’affiche pas ce que tu attends dans l’Output.

QCM

1. Que fait WaitForChild ? a) Il supprime un objet b) Il attend que l’objet existe avant de continuer le script c) Il crée un nouvel objet

2. Que se passe-t-il quand hit.Parent n’a pas de Humanoid ? a) Le jeu plante b) La fonction s’arrête grâce au return, sans erreur c) Le joueur perd de la vie

Pour aller plus loin

Documentation Roblox — événement Touched

Documentation Roblox — boucles for et ipairs

Étape 5 : Débugger et tester avec print

But : apprendre à utiliser print() pour « voir » ce qui se passe à l’intérieur du script, et corriger les erreurs efficacement.

Concepts clés : débogage, console Output, lecture des messages d’erreur.

Explications

Un script ne fait jamais ce qu’on veut qu’il fasse — il fait ce qu’on lui a écrit. Quand le résultat n’est pas celui attendu, on utilise print() pour afficher la valeur des variables à des endroits stratégiques et comprendre où ça coince.

local function generateSimpleAddition(maxValue)
    local targetNumber = math.random(2, maxValue)
    local part1 = math.random(1, targetNumber - 1)
    local part2 = targetNumber - part1

    -- Ligne de débogage : on vérifie que le calcul est correct
    print("DEBUG - cible:", targetNumber, "part1:", part1, "part2:", part2)

    return targetNumber, part1, part2
end

Quand une erreur rouge apparaît dans l’Output, elle indique en général le numéro de ligne et le type d’erreur (par exemple attempt to index nil, ce qui veut souvent dire qu’un objet n’a pas été trouvé avec WaitForChild). Lis toujours ce message avant de chercher au hasard.

À toi de jouer !

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

QCM

1. À quoi sert la fenêtre Output ? a) À afficher les décors du jeu b) À afficher les messages print() et les erreurs du script c) À modifier les objets de l’Explorer

2. Quand on voit une erreur attempt to index nil, cela signifie souvent que… a) Le jeu a trop de joueurs b) Un objet attendu (avec WaitForChild) n’a pas été trouvé c) Une variable contient trop de texte

Pour aller plus loin

Une fois le jeu terminé, tu pourras retirer ou commenter (mettre -- devant) les lignes print() de débogage pour « nettoyer » la console, sans les supprimer définitivement — elles peuvent resservir plus tard.

Étape 6 : Donner de l’imprévisibilité au jeu

But : comprendre math.random et pourquoi on doit parfois l’ajuster pour obtenir exactement ce qu’on veut (ici, uniquement des nombres pairs).

Concepts clés : génération de nombres aléatoires, math.random, math.ceil, math.floor.

Explications

math.random(min, max) renvoie un nombre entier au hasard entre min et max inclus. C’est ce qui rend chaque partie différente de la précédente.

Dans notre jeu, on veut que le nombre cible soit toujours pair, pour que les décompositions restent simples à calculer mentalement. Il ne suffit pas d’appeler math.random normalement, car il renverrait aussi des nombres impairs.

local function randomPair(min, max)
    -- On se place dans "l'espace des nombres pairs" en divisant par 2
    min = math.ceil(min / 2)   -- arrondi vers le haut pour ne jamais descendre sous min
    max = math.floor(max / 2)  -- arrondi vers le bas pour ne jamais dépasser max
    return math.random(min, max) * 2
end

print(randomPair(1, 20)) -- toujours un nombre pair entre 2 et 20

math.ceil arrondit toujours vers le haut, math.floor toujours vers le bas. On les utilise ici pour être certain que le résultat final reste bien compris entre min et max, même si ceux-ci sont impairs.

À toi de jouer !

  1. Ajoute la fonction randomPair dans ton script.
  2. Remplace l’appel math.random(2, maxValue) de l’étape 4.2 par randomPair(2, maxValue).
  3. Lance Play plusieurs fois et vérifie dans l’Output que le nombre affiché est toujours pair.
  4. Essaie d’appeler randomPair(1, 9) : quels nombres peux-tu obtenir ?

QCM

1. Que renvoie math.random(1, 6) ?

  • a) Toujours 6
  • b) Un nombre entier au hasard entre 1 et 6
  • c) Un nombre décimal entre 0 et 1

2. Pourquoi utilise-t-on math.ceil pour la borne minimale dans randomPair ?

  • a) Pour ne jamais descendre en dessous de la borne demandée
  • b) Parce que c’est plus rapide à calculer
  • c) Pour obtenir un nombre impair

Pour aller plus loin

Documentation Roblox — math.random

Étape 7 : Organiser son code — constantes et fonctions

But : apprendre à ranger son script pour qu’il reste lisible et facile à modifier, même quand il grandit.

Concepts clés : organisation du code, constantes en majuscules, fonctions séparées par responsabilité.

Explications

Un script bien organisé se lit « de haut en bas » comme une histoire claire :

  1. D’abord les constantes (tout en MAJUSCULES par convention), regroupées en haut, pour pouvoir régler le jeu en un coup d’œil sans chercher dans tout le code.
  2. Ensuite les services Roblox utilisés (Players, etc.).
  3. Puis les fonctions utilitaires, chacune avec un rôle précis et un nom qui décrit ce qu’elle fait (calculateAnswer, randomPair, generateQuestion…).
  4. Enfin les événements qui utilisent toutes ces fonctions.
-- ==================== CONSTANTES ====================
local HEALTH_LOSS = 10
local MIN_DIFFICULTY = 1
local MAX_DIFFICULTY = 3
local OPERATORS = {"+", "-", "*", "/"}

-- ==================== FONCTIONS UTILITAIRES ====================

-- Vérifie si une réponse (part1 OPERATOR part2) donne bien le résultat attendu
local function calculateAnswer(operator, part1, part2, result)
    if operator == "+" then
        return (part1 + part2) == result
    elseif operator == "-" then
        return (part1 - part2) == result
    elseif operator == "*" then
        return (part1 * part2) == result
    elseif operator == "/" then
        return (part1 / part2) == result
    end
end

Séparer le code en petites fonctions, chacune avec un seul but, rend chaque morceau plus facile à tester, à comprendre, et à corriger si une erreur apparaît.

À toi de jouer !

Range ton script actuel dans cet ordre : constantes, services, références workspace, fonctions utilitaires, puis événements. Ajoute des lignes de commentaires -- ==================== comme des titres de section pour t’y retrouver facilement.

QCM

1. Pourquoi écrit-on les constantes en MAJUSCULES ?

  • a) C’est obligatoire en Lua, sinon le script plante
  • b) C’est une convention qui permet de les repérer facilement dans le code
  • c) Ça rend le script plus rapide

2. Quel est l’avantage de séparer le code en plusieurs petites fonctions ?

  • a) Le jeu se lance plus vite
  • b) Chaque fonction est plus facile à comprendre, tester et corriger
  • c) Cela évite d’avoir à utiliser des variables

Pour aller plus loin

Documentation Roblox — bonnes pratiques de style de code Luau

Étape 8 : Optimiser son code — lire tous les objets d’un dossier

But : éviter de répéter le même code pour chaque TextLabel ou TextButton, en parcourant automatiquement tous les objets d’un dossier.

Concepts clés : GetDescendants, boucle for ... in, IsA.

Explications

Plutôt que d’écrire une ligne de code différente pour chaque bouton (Answers.Button1, Answers.Button2…), on demande à Roblox la liste de tous les objets contenus dans un dossier avec GetDescendants(), puis on filtre ceux qui nous intéressent avec IsA("TextButton").

for index, part in ipairs(answerButtons:GetDescendants()) do
    if part:IsA("TextButton") then
        part.Text = "Réponse " .. index
    end
end

L’avantage : si tu ajoutes un 5ème bouton de réponse plus tard dans l’Explorer, ce code fonctionnera sans aucune modification. C’est ce qu’on appelle écrire du code « générique », qui s’adapte automatiquement au contenu du jeu.

À toi de jouer !

  1. Utilise GetDescendants() et IsA("TextLabel") pour mettre à jour le texte de tous les TextLabel du dossier NumberGenerator en une seule boucle.
  2. Ajoute un 2ème TextLabel dans l’Explorer sans toucher au script : vérifie qu’il se met bien à jour lui aussi automatiquement.

QCM

1. Que fait GetDescendants() ?

  • a) Il renvoie la liste de tous les objets contenus dans un objet (et ses sous-dossiers)
  • b) Il supprime tous les objets d’un dossier
  • c) Il crée un nouveau dossier

2. À quoi sert IsA("TextButton") ?

  • a) À vérifier le nom exact de l’objet
  • b) À vérifier le type de l’objet, pour ne garder que les boutons par exemple
  • c) À changer la couleur du bouton

Pour aller plus loin

Documentation Roblox — GetDescendants

Étape 9 : Construire l’architecture du jeu — assembler le script complet

But : relier toutes les briques construites dans les étapes précédentes pour obtenir le script final et fonctionnel.

Concepts clés : assemblage, événements Touched/TouchEnded, gestion multi-joueurs, nettoyage des connexions.

Explications

Un jeu multijoueur doit gérer plusieurs joueurs en même temps, sans qu’ils se marchent dessus. On utilise pour cela des tables qui associent chaque joueur à son propre état :

local playersInZone = {}       -- [Player] = true tant qu'il est dans la zone
local playerConnections = {}   -- [Player] = liste des connexions de boutons à déconnecter

On doit aussi penser à « nettoyer » les connexions d’événements quand un joueur sort de la zone ou quitte le jeu, sinon elles restent actives en mémoire inutilement (une fuite de mémoire) et peuvent déclencher plusieurs fois le même clic.

Voici le script complet, qui assemble toutes les briques précédentes :

--[[
    Script : Number Game - Quiz de calcul (addition)
    Description : Quand un joueur entre dans la Hitbox, un QCM de calcul s'affiche.
    Une bonne réponse augmente la difficulté, une mauvaise réponse fait perdre de la vie.
]]

-- ==================== CONSTANTES ====================
local HEALTH_LOSS = 10
local MIN_DIFFICULTY = 1
local MAX_DIFFICULTY = 3
local ENTER_DELAY = 1
local EXIT_DELAY = 1
local OPERATORS = {"+", "-", "*", "/"}

-- ==================== SERVICES ====================
local Players = game:GetService("Players")

-- ==================== RÉFÉRENCES WORKSPACE ====================
local numberGameFolder = workspace:WaitForChild("NumberGame")
local hitbox = numberGameFolder:WaitForChild("HitBox")

-- ==================== ÉTAT DU JEU ====================
local playersInZone = {}
local playerConnections = {}

-- ==================== FONCTIONS UTILITAIRES ====================

local function calculateAnswer(operator, part1, part2, result)
    if operator == "+" then
        return (part1 + part2) == result
    elseif operator == "-" then
        return (part1 - part2) == result
    elseif operator == "*" then
        return (part1 * part2) == result
    elseif operator == "/" then
        return (part1 / part2) == result
    end
end

local function randomPair(min, max)
    min = math.ceil(min / 2)
    max = math.floor(max / 2)
    return math.random(min, max) * 2
end

local function randomIncorrectNumber(maxValue, targetNumber, operator)
    local wrong1 = randomPair(1, maxValue // 2)
    local wrong2 = randomPair(1, maxValue // 2)
    if operator == "*" or operator == "/" then
        wrong2 = randomPair(1, 10)
    end
    while calculateAnswer(operator, wrong1, wrong2, targetNumber) do
        wrong2 = randomPair(1, maxValue // 2)
    end
    return wrong1, wrong2
end

local function generateQuestion(numberDisplay, answerButtons, difficulty, operator)
    if not numberDisplay or not answerButtons then return nil end
    operator = operator or "+"

    numberDisplay.Visible = true
    answerButtons.Visible = true

    difficulty = math.clamp(difficulty, MIN_DIFFICULTY, MAX_DIFFICULTY)
    local maxValue = (10 ^ difficulty) - 1
    local targetNumber = randomPair(1, maxValue)
    local part1, part2 = 0, 0

    if operator == "+" then
        part1 = math.random(1, targetNumber - 1)
        part2 = targetNumber - part1
    elseif operator == "-" then
        part2 = math.random(1, targetNumber // 2)
        part1 = targetNumber + part2
    elseif operator == "*" then
        part2 = randomPair(1, 10)
        part1 = randomPair(1, maxValue)
        targetNumber = part1 * part2
    elseif operator == "/" then
        part2 = randomPair(1, 10)
        part1 = targetNumber * part2
    end

    for _, part in ipairs(numberDisplay:GetDescendants()) do
        if part:IsA("TextLabel") then
            part.Text = string.format("%0" .. difficulty .. "d", targetNumber)
        end
    end

    local correctAnswerText = string.format("%0" .. difficulty .. "d", part1)
        .. " " .. operator .. " " .. string.format("%0" .. difficulty .. "d", part2)
    local correctIndex = math.random(1, 4)

    for index, part in ipairs(answerButtons:GetDescendants()) do
        if part:IsA("TextButton") then
            if index == correctIndex then
                part.Text = correctAnswerText
            else
                local wrong1, wrong2 = randomIncorrectNumber(maxValue, targetNumber, operator)
                part.Text = string.format("%0" .. difficulty .. "d", wrong1) .. " "
                    .. operator .. " " .. string.format("%0" .. difficulty .. "d", wrong2)
            end
        end
    end

    return correctIndex
end

local function getPlayerGuiElements(player)
    local playerGui = player:WaitForChild("PlayerGui")
    local screenGui = playerGui:WaitForChild("ScreenGui")
    local numberDisplay = screenGui:WaitForChild("NumberGenerator")
    local answerButtons = screenGui:WaitForChild("Answers")
    return numberDisplay, answerButtons
end

local function hideQuiz(numberDisplay, answerButtons)
    if numberDisplay then numberDisplay.Visible = false end
    if answerButtons then answerButtons.Visible = false end
end

local function disconnectPlayerButtons(player)
    local connections = playerConnections[player]
    if not connections then return end
    for _, connection in ipairs(connections) do
        connection:Disconnect()
    end
    playerConnections[player] = nil
end

local function getNextOperator(currentIndex)
    currentIndex += 1
    if currentIndex > #OPERATORS then
        currentIndex = #OPERATORS
    end
    return OPERATORS[currentIndex], currentIndex
end

local function startQuizForPlayer(player, humanoid)
    local numberDisplay, answerButtons = getPlayerGuiElements(player)

    local currentDifficulty = MIN_DIFFICULTY
    local currentOperator, currentIndexOperator = getNextOperator(0)
    local currentAnswerIndex = generateQuestion(numberDisplay, answerButtons, currentDifficulty, currentOperator)

    disconnectPlayerButtons(player)
    local connections = {}

    for index, part in ipairs(answerButtons:GetDescendants()) do
        if part:IsA("TextButton") then
            local connection = part.MouseButton1Click:Connect(function()
                if humanoid.Health <= 0 then return end

                if index ~= currentAnswerIndex then
                    humanoid.Health = math.max(humanoid.Health - HEALTH_LOSS, 0)
                else
                    if currentDifficulty == MAX_DIFFICULTY then
                        currentOperator, currentIndexOperator = getNextOperator(currentIndexOperator)
                        currentDifficulty = MIN_DIFFICULTY
                    end
                    currentAnswerIndex = generateQuestion(numberDisplay, answerButtons, currentDifficulty, currentOperator)
                    currentDifficulty = math.min(currentDifficulty + 1, MAX_DIFFICULTY)
                end
            end)
            table.insert(connections, connection)
        end
    end

    playerConnections[player] = connections
end

-- ==================== ÉVÉNEMENTS DE COLLISION ====================

hitbox.Touched:Connect(function(hit)
    local character = hit.Parent
    if not character then return end

    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if not humanoid then return end

    local player = Players:GetPlayerFromCharacter(character)
    if not player then return end

    if playersInZone[player] then return end
    playersInZone[player] = true

    task.wait(ENTER_DELAY)
    if not playersInZone[player] or humanoid.Health <= 0 then return end

    startQuizForPlayer(player, humanoid)
end)

hitbox.TouchEnded:Connect(function(hit)
    local character = hit.Parent
    if not character then return end

    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if not humanoid then return end

    local player = Players:GetPlayerFromCharacter(character)
    if not player then return end

    task.wait(EXIT_DELAY)

    local stillTouching = false
    for _, part in pairs(hitbox:GetTouchingParts()) do
        if part.Parent == character then
            stillTouching = true
            break
        end
    end

    if stillTouching or not playersInZone[player] then return end

    playersInZone[player] = nil
    disconnectPlayerButtons(player)

    local numberDisplay, answerButtons = getPlayerGuiElements(player)
    hideQuiz(numberDisplay, answerButtons)
end)

-- ==================== GESTION DES JOUEURS ====================

Players.PlayerAdded:Connect(function(player)
    player.CharacterAdded:Connect(function()
        local numberDisplay, answerButtons = getPlayerGuiElements(player)
        hideQuiz(numberDisplay, answerButtons)
    end)
end)

Players.PlayerRemoving:Connect(function(player)
    playersInZone[player] = nil
    disconnectPlayerButtons(player)
end)

À toi de jouer !

  1. Remplace le contenu de ton NumberGameScript par ce script complet.
  2. Teste le jeu en solo avec Play, en te déplaçant vers la Hitbox.
  3. Teste ensuite avec Play plusieurs joueurs (Test → Clients : 2) pour vérifier que deux joueurs peuvent jouer en même temps sans se gêner.

QCM

1. Pourquoi range-t-on l’état de chaque joueur dans des tables comme playersInZone, plutôt que dans une seule variable ?

  • a) Pour que le jeu soit plus joli
  • b) Pour gérer plusieurs joueurs en même temps, chacun avec son propre état
  • c) Ce n’est pas nécessaire, une seule variable suffirait

2. Pourquoi appelle-t-on disconnectPlayerButtons avant de recréer les connexions des boutons ?

  • a) Pour éviter que les anciens clics déclenchent encore une action (fuite mémoire, doublons)
  • b) Pour changer la couleur des boutons
  • c) Ce n’est pas utile, on peut s’en passer

Pour aller plus loin

Documentation Roblox — RBXScript Connection et Disconnect

Étape 10 : Pour aller plus loin — améliore ton jeu

But : utiliser ce que tu as appris pour transformer ce jeu en quelque chose qui te ressemble.

Concepts clés : réutilisation, créativité, itération.

Explications

Tu maîtrises maintenant les briques essentielles de la programmation en Lua : variables, constantes, fonctions, conditions, boucles, événements, et un peu de hasard maîtrisé. Un vrai développeur de jeu ne s’arrête jamais à la première version : il l’améliore petit à petit, en testant à chaque changement.

À toi de jouer !

Voici quelques idées de règles ou d’éléments à ajouter, du plus simple au plus ambitieux :

  • Ajoute un chronomètre : le joueur doit répondre en moins de 10 secondes, sinon il perd de la vie automatiquement.
  • Ajoute un score : chaque bonne réponse rapporte des points, affichés dans un TextLabel en haut de l’écran.
  • Ajoute un message de victoire quand la difficulté maximale et le dernier opérateur (division) sont atteints.
  • Ajoute des obstacles dans le décor autour de la Hitbox pour rendre le déplacement plus difficile.
  • Ajoute un son différent pour une bonne réponse et pour une mauvaise réponse.

Conclusion

Mets-toi maintenant à la place d’un joueur qui découvre ton jeu pour la première fois : est-ce clair ? Est-ce amusant ? Puis imagine un joueur devenu expert après plusieurs parties : le jeu reste-t-il intéressant, ou devient-il trop facile ?

Une fois satisfait de ton jeu, partage-le avec tes camarades de classe et demande-leur ce qu’ils en pensent. Leurs retours sont précieux : ce sont eux qui te diront si une règle est trop dure, trop facile, ou pas claire — exactement comme le font les vrais studios de jeux vidéo avec leurs joueurs.

Pour aller plus loin (documentation)

Documentation Roblox — RemoteEvent (pour séparer proprement client et serveur plus tard) :

Catégories
Jeu vidéo ROBLOX ROBLOX-NIVEAU03

Trouve la bonne couleur !

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 :

  1. Que représente Color3.fromRGB(0, 255, 0) ?
    • a) Une couleur rouge.
    • b) Une couleur verte.
    • c) Une couleur bleue.
  2. 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 :

  1. À 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.
  2. 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 :

  1. 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.
  2. À 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 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)

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

  1. 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.
  2. 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 :

  1. 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.
  2. 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.

Concepts clés : constantes, événements, Touched, Humanoid, Health, task.spawn, task.wait.

Explications :

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 :

  1. À 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.
  2. 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.

À toi de jouer !

Tu peux t’inspirer de ce tuto : https://123codage.com/points-de-respawns-comptage-des-tours-meilleur-temps/

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 :

  1. 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.
  2. 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.

Catégories
Jeu vidéo ROBLOX ROBLOX-NIVEAU03

Faire rouler en boucle une boule sur une pente

Dans ce tutoriel, tu vas réaliser une animation où une boule descend une pente, disparaît lorsqu’elle arrive en bas, puis réapparaît automatiquement en haut pour recommencer son parcours.

Cette technique est très utilisée dans les jeux pour créer des décors animés ou des parcours d’obstacles.

Tu vas apprendre à :

  • construire une boule avec un Part sphérique ;
  • créer une pente avec un Part Coin ;
  • détecter l’arrivée de la boule grâce à une boîte de collision ;
  • utiliser un script serveur pour replacer la boule ;
  • organiser correctement les objets dans l’Explorateur ;
  • utiliser ReplicatedStorage pour conserver un modèle de référence.

Faire rouler une boule sur une pente

Crée la structure suivante sous workspace :

Pourquoi utiliser un Folder ?

Un Folder permet de ranger les objets qui appartiennent au même système.

Ici, le dossier Boules contient :

  • la pente (inclinedPlane) ;
  • la boule (Ball) ;
  • la zone de détection (HitBox).

Les avantages sont nombreux :

  • retrouver rapidement les objets ;
  • éviter d’encombrer le Workspace ;
  • simplifier les scripts ;
  • rendre le projet plus facile à modifier.

Dans un gros jeu, il peut y avoir plusieurs centaines d’objets. Une bonne organisation devient alors indispensable.

Construire la pente

Crée une Part rectangulaire.

Ensuite :

  1. agrandis-la ;
  2. ancre-la (Anchored = true).

La pente servira de support à la boule.

Construire la boule

Ajoute une Part.

La gravité pourra ainsi faire rouler la boule naturellement.

Place-la en haut de la pente.

Comment la boule est-elle positionnée dans l’espace ?

Chaque objet Roblox possède une propriété :

Position

Cette position contient trois coordonnées :

X = gauche / droite
Y = hauteur
Z = avant / arrière

Exemple :

Vector3.new(34, 15, -77)

Cela signifie :

  • 34 studs vers la droite ;
  • 15 studs de hauteur ;
  • 77 studs vers l’arrière.

Lorsque tu places la boule en haut de la pente dans Roblox Studio, Roblox lui attribue automatiquement une position.

Créer la zone de disparition

Ajoute une Part en bas de la pente.

Renomme-la :

ZoneArrivee

Paramètres :

Transparency = 1
CanCollide = false
Anchored = true

Cette Part est invisible.

Elle servira uniquement à détecter l’arrivée de la boule.

On l’appelle souvent une HitBox ou boîte de collision.

Pourquoi utiliser une boîte de collision ?

Au lieu de vérifier en permanence la position de la boule, Roblox peut détecter automatiquement lorsqu’un objet touche une zone.

Les avantages :

  • moins de calculs ;
  • code plus simple ;
  • meilleure précision.

Faire disparaître et réapparaître la boule

Où placer le script ?

Deux solutions sont possibles.

Solution 1 : ServerScriptService

ServerScriptService
└─ GestionBoule

C’est la méthode recommandée.

Les avantages :

  • tous les scripts sont regroupés ;
  • plus facile à maintenir ;
  • meilleure sécurité.

Solution 2 : Sous le Folder

Workspace
└─ Boules
└─ Script

Cela fonctionne également.

Cette solution est parfois utilisée lorsque le script ne gère qu’un seul système.

Pour un petit projet comme celui-ci, cela reste acceptable, crée un script sous folder :

Saisie le code suivant :

-- récupération des ressources graphiques
local folder = script.Parent
local hitBox = folder.HitBox
local ball = folder.Ball

-- récupération du folder replicatedStorage
local replicatedStorage = game:GetService("ReplicatedStorage")

-- récupération de la position initiale de la balle
local positionInitial = ball.Position

-- modification des propriété de la ball
ball.Anchored = false
ball.CanCollide = true
ball.Name = "Ball"
-- clonage de la balle
local newBall = ball:Clone()
newBall.Parent = folder
-- rangement de la balle en modèle replicatedStorage
ball.Parent = replicatedStorage

-- détection de la collision de la balle
hitBox.Touched:Connect(function(hit)
	if hit.Name ~= "Ball" then return end

	newBall:Destroy()	
	task.wait(1)
	
	newBall = ball:Clone()	
	newBall.Parent = folder
	newBall.Anchored = false

end)

Pourquoi utiliser un Script serveur ?

Nous allons utiliser un Script et non un LocalScript.

Le serveur est responsable :

  • des objets du Workspace ;
  • de la physique ;
  • des collisions ;
  • des déplacements des Parts.

Comme la boule existe pour tous les joueurs, c’est le serveur qui doit la gérer.

Mémoriser la position de départ

Le script va enregistrer la position initiale de la boule :

local positionInitial = ball.Position

Cette variable contient les coordonnées exactes du point de départ.

Plus tard, le script pourra replacer la boule exactement au même endroit.

Sauvegarder un modèle dans ReplicatedStorage

Crée une copie parfaite de la boule.

Place cette copie dans :

-- récupération du folder replicatedStorage
local replicatedStorage = game:GetService("ReplicatedStorage")
ball.Parent = replicatedStorage

Cette boule ne sera jamais utilisée directement dans le jeu.

Elle sert de modèle de référence.

Pourquoi utiliser ReplicatedStorage ?

ReplicatedStorage permet de stocker des objets qui pourront être utilisés plus tard.

Les avantages :

  • conserver un modèle intact ;
  • recréer facilement un objet détruit ;
  • éviter de reconstruire l’objet à la main ;
  • faciliter les systèmes complexes.

Pourquoi cloner la boule ?

Lorsque la boule disparaît, il est parfois plus simple de :

  1. détruire l’ancienne ;
  2. recréer une nouvelle boule.

Pour cela :

local newBall = ball:Clone()

Le clonage produit une copie identique :

  • même taille ;
  • même couleur ;
  • mêmes propriétés ;
  • mêmes scripts éventuels.

Cette méthode est très utilisée dans les jeux Roblox.


Créer un flash lumineux

Ce script sert à créer un effet de flash lumineux à un endroit précis dans le jeu. Par exemple, il peut être utilisé lorsqu’un joueur clique, lorsqu’une explosion se produit ou lorsqu’un objet apparaît ou disparaît.

Le flash est créé sous la forme d’une petite sphère lumineuse qui grandit rapidement avant de disparaître.

Quand la fonction est appelée :

  1. une petite boule lumineuse est créée ;
  2. elle apparaît à la position demandée ;
  3. elle grossit rapidement ;
  4. elle devient transparente ;
  5. elle disparaît ;
  6. Roblox la supprime automatiquement.

Le résultat ressemble à une petite explosion lumineuse ou à un effet magique très utilisé dans les jeux Roblox.

Rajoute ce code dans ton script :

local Debris = game:GetService("Debris")
local TweenService = game:GetService("TweenService")

-- 
-- CONFIGURATION
-- 
local CONFIG = {
	-- Flash
	flashActif      = true,
	sizeFlash       = Vector3.new(20, 20, 20),
	colorFlash      = Color3.fromRGB(255, 0, 0),
}

-- 
-- EFFETS  Flash
-- 

local function creerFlash(position)
	local flash = Instance.new("Part")
	flash.Shape = Enum.PartType.Ball
	flash.Size = Vector3.new(0.1, 0.1, 0.1)
	flash.Position = position
	flash.Anchored = true
	flash.CanCollide = false
	flash.CastShadow = false
	flash.Material = Enum.Material.Neon
	flash.Color = CONFIG.colorFlash
	flash.Parent = workspace

	local tween = TweenService:Create(flash,
		TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
		{ Size = CONFIG.sizeFlash, Transparency = 1 }
	)
	tween:Play()
	Debris:AddItem(flash, 0.2)
end

-- 
-- EXPLOSION PRINCIPALE
-- 
local function exploser(ball)
	local position = ball.Position

	ball.CanCollide = false
	ball.Transparency = 1

	if CONFIG.flashActif   then creerFlash(position)      end

	ball:Destroy()
end


Puis modifie ton script pour appeler le flash lorsque la balle touche la HitBox :


-- détection de la collision de la balle
hitBox.Touched:Connect(function(hit)
	if hit.Name ~= "Ball" then return end
	exploser(newBall)
		
	task.wait(1)
	
	newBall = ball:Clone()	
	newBall.Parent = folder
	newBall.Anchored = false

end)

Crée une onde de choc

Ce script crée une onde de choc visuelle qui se propage autour d’un point précis.

Tu peux imaginer ce qui se passe lorsqu’une pierre tombe dans l’eau : une vague se forme puis s’agrandit progressivement. Ici, c’est le même principe, mais sous la forme d’une sphère lumineuse qui grandit avant de disparaître.

Ce type d’effet est souvent utilisé pour :

  • une explosion ;
  • un sort magique ;
  • un impact puissant ;
  • l’arrivée d’un boss ;
  • une compétence spéciale.
-- Script dans la boule (Part) ou un Script serveur

local Debris = game:GetService("Debris")
local TweenService = game:GetService("TweenService")

-- 
-- CONFIGURATION
-- 
local CONFIG = {
	-- Onde de choc
	ondeActive      = true,
	ondeCouleur     = Color3.fromRGB(255, 150, 0),
	-- Dégâts (rayon)
	rayonDegats     = 40,
	forceDegats     = 80,
	-- Flash
	flashActif      = true,
	sizeFlash       = Vector3.new(20, 20, 20),
	colorFlash      = Color3.fromRGB(255, 0, 0),

}

-- 
-- EFFETS ONDE DE CHOC
-- 

local function creerOndeChoc(position)
	local onde = Instance.new("Part")
	onde.Shape = Enum.PartType.Ball
	onde.Size = Vector3.new(1, 1, 1)
	onde.Position = position
	onde.Anchored = true
	onde.CanCollide = false
	onde.CastShadow = false
	onde.Material = Enum.Material.Neon
	onde.Color = CONFIG.ondeCouleur
	onde.Transparency = 0.3
	onde.Parent = workspace

	local tween = TweenService:Create(onde,
		TweenInfo.new(0.4, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
		{ Size = Vector3.new(CONFIG.rayonDegats * 2, CONFIG.rayonDegats * 2, CONFIG.rayonDegats * 2), Transparency = 1 }
	)
	tween:Play()
	Debris:AddItem(onde, 0.5)
end

local function appliquerForce(position)
	for _, part in ipairs(workspace:GetPartBoundsInRadius(position, CONFIG.rayonDegats)) do
		if part.Anchored then continue end
		local direction = (part.Position - position)
		local distance = direction.Magnitude
		if distance < 0.1 then continue end
		local force = (1 - distance / CONFIG.rayonDegats) * CONFIG.forceDegats
		part:ApplyImpulse(direction.Unit * force * part.AssemblyMass)
	end
end


-- 
-- EXPLOSION PRINCIPALE
-- 
local function exploser(ball)
	local position = ball.Position

	ball.CanCollide = false
	ball.Transparency = 1

	if CONFIG.flashActif   then creerFlash(position)      end
	if CONFIG.ondeActive   then creerOndeChoc(position)   end

	appliquerForce(position)
	ball:Destroy()

end

Lorsque la fonction creerOndeChoc() est appelée :

  1. une petite sphère lumineuse apparaît ;
  2. elle est placée au centre de l’explosion ;
  3. elle s’agrandit jusqu’au rayon défini dans la configuration ;
  4. elle devient progressivement transparente ;
  5. elle disparaît ;
  6. Roblox la supprime automatiquement.

Cette technique permet de créer facilement des effets spectaculaires pour des explosions, des sorts magiques ou des impacts puissants dans un jeu Roblox.


Créer de la fumée

Ce script permet de créer un effet de fumée à un endroit précis dans le jeu.

Cet effet peut être utilisé pour représenter :

  • une explosion ;
  • un moteur ;
  • un incendie ;
  • une cheminée ;
  • l’impact d’un projectile.

La fumée apparaît pendant un court instant, puis disparaît automatiquement.

-- ═══════════════════════════════════════
-- CONFIGURATION
-- ═══════════════════════════════════════
local CONFIG = {
	-- Fumée
	fumeeActive     = true,
	-- Onde de choc
	ondeActive      = true,
	ondeCouleur     = Color3.fromRGB(255, 150, 0),
	-- Dégâts (rayon)
	rayonDegats     = 40,
	forceDegats     = 80,
	-- Flash
	flashActif      = true,
	sizeFlash       = Vector3.new(20, 20, 20),
	colorFlash      = Color3.fromRGB(255, 0, 0),

}

-- ═══════════════════════════════════════
-- EFFETS FUMEE
-- ═══════════════════════════════════════

l
local function creerFumee(position)
	local anchor = Instance.new("Part")
	anchor.Size = Vector3.new(1, 1, 1)
	anchor.Position = position
	anchor.Anchored = true
	anchor.CanCollide = false
	anchor.Transparency = 1
	anchor.Parent = workspace

	local fumee = Instance.new("Smoke")
	fumee.Color = Color3.fromRGB(80, 80, 80)
	fumee.Opacity = 0.5
	fumee.RiseVelocity = 10
	fumee.Size = 12
	fumee.Parent = anchor

	-- Couper la fumée puis supprimer
	task.delay(0.3, function()
		fumee.Enabled = false
	end)
	Debris:AddItem(anchor, 3)
end

-- ═══════════════════════════════════════
-- EXPLOSION PRINCIPALE
-- ═══════════════════════════════════════
local function exploser(ball)
	local position = ball.Position

	ball.CanCollide = false
	ball.Transparency = 1

	if CONFIG.flashActif   then creerFlash(position)      end
	if CONFIG.ondeActive   then creerOndeChoc(position)   end
	if CONFIG.fumeeActive  then creerFumee(position)      end

	appliquerForce(position)
	ball:Destroy()

end

L’objet Smoke ne peut pas exister seul.

Il doit être attaché à un objet Roblox.

La Part invisible sert donc de :

  • support ;
  • point d’émission ;
  • repère dans l’espace.

Le joueur ne la voit jamais, mais elle permet à la fumée de fonctionner correctement.

Lorsque la fonction creerFumee() est appelée :

  • un support invisible est créé ;
  • un effet Smoke est ajouté dessus ;
  • la fumée grise est émise ;
  • elle monte rapidement ;
  • l’émission s’arrête après 0,3 seconde ;
  • Roblox supprime automatiquement l’ensemble après 3 secondes.

Cette technique permet d’ajouter facilement des effets visuels réalistes à tes explosions et animations dans Roblox.

Création d’étincelles

Ce script permet de créer un effet d’étincelles semblable à celui d’une explosion, d’un feu d’artifice ou d’un impact métallique.

Lorsqu’il est exécuté, plusieurs petites sphères lumineuses sont créées au même endroit. Elles partent ensuite dans différentes directions à des vitesses variées avant de disparaître progressivement.


Création de plusieurs étincelles

La fonction utilise une boucle qui se répète autant de fois que la valeur définie dans :

CONFIG.nbEtincelles

Si cette valeur est égale à 20, alors 20 étincelles seront créées.

Plus cette valeur est grande, plus l’effet sera spectaculaire, mais aussi plus il demandera de calculs à Roblox.


-- Script dans la boule (Part) ou un Script serveur
local Debris = game:GetService("Debris")
local TweenService = game:GetService("TweenService")

-- ═══════════════════════════════════════
-- CONFIGURATION
-- ═══════════════════════════════════════
local CONFIG = {
	-- Particules
	nbEtincelles    = 50,
	-- Fumée
	fumeeActive     = true,
	-- Onde de choc
	ondeActive      = true,
	ondeCouleur     = Color3.fromRGB(255, 150, 0),
	-- Dégâts (rayon)
	rayonDegats     = 40,
	forceDegats     = 80,
	-- Flash
	flashActif      = true,
	sizeFlash       = Vector3.new(20, 20, 20),
	colorFlash      = Color3.fromRGB(255, 0, 0),

}

-- ═══════════════════════════════════════
-- EFFETS ETINCELLES
-- ═══════════════════════════════════════

local function creerEtincelles(position)
	for i = 1, CONFIG.nbEtincelles do
		local etincelle = Instance.new("Part")
		etincelle.Shape = Enum.PartType.Ball
		-- taille des etincelles 
		etincelle.Size = Vector3.new(0.2, 0.2, 0.2)
		etincelle.Position = position
		etincelle.CanCollide = false
		etincelle.CastShadow = false
		etincelle.Material = Enum.Material.Neon
		-- Couleur aléatoire entre orange et jaune
		etincelle.Color = Color3.fromRGB(255, math.random(100, 255), 0)
		etincelle.Parent = workspace

		-- Direction aléatoire
		local direction = Vector3.new(
			math.random(-100, 100) / 100,
			math.random(20, 100) / 100,
			math.random(-100, 100) / 100
		).Unit
		-- vitesse aléatoire de propagation des étincelles
		local vitesse = math.random(20, 200)
		local vb = Instance.new("BodyVelocity")
		vb.Velocity = direction * vitesse
                -- Autorise une poussée très forte sur les axes X, Y et Z pour propulser l'étincelle 1e4 = 10000
		vb.MaxForce = Vector3.new(1e4, 1e4, 1e4)
		vb.Parent = etincelle

		-- Disparition progressive
		local duree = math.random(5, 12) / 10
		local tween = TweenService:Create(etincelle,
			TweenInfo.new(duree, Enum.EasingStyle.Quad, Enum.EasingDirection.In),
			{ Transparency = 1, Size = Vector3.new(0.05, 0.05, 0.05) }
		)
		tween:Play()
		Debris:AddItem(etincelle, duree)
	end
end

-- ═══════════════════════════════════════
-- EXPLOSION PRINCIPALE
-- ═══════════════════════════════════════
local function exploser(ball)
	local position = ball.Position

	ball.CanCollide = false
	ball.Transparency = 1

	if CONFIG.flashActif   then creerFlash(position)      end
	if CONFIG.ondeActive   then creerOndeChoc(position)   end


	if CONFIG.fumeeActive  then creerFumee(position)      end
	
	creerEtincelles(position)

	appliquerForce(position)
	ball:Destroy()

end

Apparence des étincelles

Chaque étincelle est représentée par une très petite sphère lumineuse.

Le matériau Neon lui donne un aspect brillant qui ressemble à une particule incandescente.

Pour rendre l’effet plus réaliste, la couleur n’est pas toujours la même. Le rouge reste au maximum tandis que le vert varie aléatoirement. Cela produit différentes nuances allant :

  • du jaune vif ;
  • à l’orange ;
  • jusqu’à l’orange foncé.

Comme dans une véritable explosion, toutes les étincelles n’ont donc pas exactement la même couleur.

Une direction différente pour chaque étincelle

Après leur création, les étincelles doivent se disperser.

Le script génère donc une direction aléatoire pour chacune d’elles.

Certaines partent :

  • vers la gauche ;
  • vers la droite ;
  • vers l’avant ;
  • vers l’arrière ;
  • vers le haut.

Cette dispersion donne l’impression d’une véritable projection de particules.

Si toutes les étincelles allaient dans la même direction, l’effet serait beaucoup moins naturel.

Une vitesse différente pour chaque étincelle

Le script choisit également une vitesse aléatoire.

Certaines étincelles sont éjectées très rapidement tandis que d’autres se déplacent plus lentement.

C’est ce mélange de vitesses qui rend l’explosion crédible.

Dans la réalité, les fragments d’une explosion ne se déplacent jamais tous à la même vitesse.

Utilisation de BodyVelocity

Pour propulser les étincelles, le script utilise un objet appelé BodyVelocity.

On peut le comparer à une petite poussée invisible qui agit sur chaque étincelle. Cette poussée envoie immédiatement la particule dans la direction choisie avec la vitesse calculée. Le résultat est un mouvement rapide qui donne l’impression que les étincelles jaillissent du centre de l’explosion.

Disparition progressive

Une étincelle ne reste pas visible indéfiniment. Le script lui attribue une durée de vie aléatoire. Certaines disparaissent rapidement alors que d’autres restent visibles un peu plus longtemps.

Pendant cette durée :

  • elles deviennent progressivement transparentes ;
  • leur taille diminue petit à petit.

L’effet obtenu ressemble à une braise qui refroidit et s’éteint.

Animation avec TweenService

La disparition est réalisée grâce à TweenService.

Au lieu de rendre l’étincelle invisible d’un seul coup, Roblox anime progressivement :

  • la transparence ;
  • la taille.

L’étincelle semble donc se consumer naturellement avant de disparaître.

Nettoyage automatique

Une fois son animation terminée, chaque étincelle est supprimée grâce au service Debris. Cela évite d’accumuler des centaines de petits objets invisibles dans le jeu. C’est une étape importante pour conserver de bonnes performances.

Ce que voit le joueur

Lorsqu’une explosion se produit :

  1. plusieurs petites boules lumineuses apparaissent ;
  2. elles partent dans toutes les directions ;
  3. certaines vont plus vite que d’autres ;
  4. elles deviennent progressivement transparentes ;
  5. elles rétrécissent ;
  6. elles disparaissent complètement.

L’ensemble crée un effet très proche des étincelles d’un feu d’artifice, d’une explosion ou d’un choc métallique.


Créer un effet de projection de débris

Ce script sert à simuler une explosion de petits morceaux de matière, comme des cailloux, des fragments de mur ou des débris projetés au sol.

L’idée est simple : au lieu d’avoir un seul effet, on génère plusieurs petits objets qui partent dans toutes les directions avec des tailles et des vitesses différentes.


-- ═══════════════════════════════════════
-- CONFIGURATION
-- ═══════════════════════════════════════
local CONFIG = {
	-- Particules
	nbEtincelles    = 50,
	nbDebris        = 10,
	-- Fumée
	fumeeActive     = true,
	-- Onde de choc
	ondeActive      = true,
	ondeCouleur     = Color3.fromRGB(255, 150, 0),
	-- Dégâts (rayon)
	rayonDegats     = 40,
	forceDegats     = 80,
	-- Flash
	flashActif      = true,
	sizeFlash       = Vector3.new(20, 20, 20),
	colorFlash      = Color3.fromRGB(255, 0, 0),

}

-- ═══════════════════════════════════════
-- EFFETS DEBRIS
-- ═══════════════════════════════════════


local function creerDebris(position)
	for i = 1, CONFIG.nbDebris do
		local morceau = Instance.new("Part")
		morceau.Shape = Enum.PartType.CornerWedge
		morceau.Size = Vector3.new(
			math.random(1, 4) / 10,
			math.random(1, 4) / 10,
			math.random(1, 4) / 10
		)
		morceau.Position = position + Vector3.new(
			math.random(-2, 2),
			math.random(0, 2),
			math.random(-2, 2)
		)
		morceau.Material = Enum.Material.SmoothPlastic
		morceau.Color = Color3.fromRGB(80, 80, 80)
		morceau.Parent = workspace

		local direction = Vector3.new(
			math.random(-100, 100) / 100,
			math.random(30, 100) / 100,
			math.random(-100, 100) / 100
		).Unit

		local vb = Instance.new("BodyVelocity")
		vb.Velocity = direction * math.random(10, 30)
		vb.MaxForce = Vector3.new(1e4, 1e4, 1e4)
		vb.Parent = morceau

		Debris:AddItem(morceau, math.random(2, 4))
	end
end

-- ═══════════════════════════════════════
-- EXPLOSION PRINCIPALE
-- ═══════════════════════════════════════
local function exploser(ball)
	local position = ball.Position

	ball.CanCollide = false
	ball.Transparency = 1

	if CONFIG.flashActif   then creerFlash(position)      end
	if CONFIG.ondeActive   then creerOndeChoc(position)   end


	if CONFIG.fumeeActive  then creerFumee(position)      end
	
	creerEtincelles(position)
	creerDebris(position)

	appliquerForce(position)
	ball:Destroy()

end

Création de plusieurs morceaux

La fonction utilise une boucle qui se répète un nombre de fois défini par :

CONFIG.nbDebris

Plus cette valeur est grande, plus l’explosion est impressionnante. Chaque boucle crée donc un nouveau morceau de débris.

Apparence des débris

Chaque fragment est une petite Part avec :

  • une taille différente à chaque fois (petits ou un peu plus gros morceaux) ;
  • une couleur gris foncé pour simuler de la pierre ou du béton ;
  • un matériau simple pour rester léger et réaliste.

L’objectif est d’éviter un rendu trop uniforme : dans une vraie explosion, tous les morceaux ne sont jamais identiques.

Position initiale aléatoire

Les débris ne sont pas tous créés exactement au même point.

Ils apparaissent légèrement autour de la position d’origine, avec un petit décalage :

  • certains un peu à gauche ;
  • d’autres un peu à droite ;
  • certains légèrement plus haut.

Cela donne l’impression que l’explosion a un vrai volume, et pas juste un point fixe.

Direction de projection

Chaque débris reçoit une direction aléatoire dans l’espace. Cette direction est calculée pour permettre :

  • une dispersion horizontale (gauche / droite / avant / arrière) ;
  • une poussée vers le haut pour simuler l’explosion.

Ensuite, cette direction est normalisée, ce qui signifie qu’on garde uniquement le sens, sans influencer la vitesse.

Mise en mouvement

Pour propulser les morceaux, le script utilise un BodyVelocity.

Ce système agit comme une poussée instantanée appliquée à chaque débris. La vitesse est également aléatoire :

  • certains morceaux partent doucement ;
  • d’autres sont projetés rapidement.

Cela rend l’effet beaucoup plus naturel et dynamique.

Durée de vie limitée

Chaque débris n’est pas permanent. Le service Debris s’occupe de supprimer automatiquement chaque morceau après un temps aléatoire.

Cela évite :

  • d’encombrer le jeu avec trop d’objets ;
  • de ralentir les performances ;
  • de laisser des débris inutiles dans le monde.

Résultat visuel

Quand la fonction est appelée, on observe :

  1. plusieurs petits morceaux apparaissent au point d’impact ;
  2. ils sont légèrement dispersés autour du centre ;
  3. ils sont projetés dans toutes les directions ;
  4. certains montent haut, d’autres restent proches du sol ;
  5. ils disparaissent progressivement.

L’ensemble donne un effet très proche :

  • d’une explosion de mur ;
  • d’un impact de balle lourde ;
  • ou d’un objet qui se casse violemment.

Rajouter un son d’explosion

Trouve et insère le dans l’arborescence :

Puis ajoute à ton script la gestion du son le son :

local soundExplosion = folder.Explosion

local function playSound(sound)
	if not sound then return end
	pcall(function() sound:Play() end)
end

Puis modifie la fonction Exploser de ton script pour ajouter le son de l’explosion :

-- 
-- EXPLOSION PRINCIPALE
-- 
local function exploser(ball)
	local position = ball.Position

	ball.CanCollide = false
	ball.Transparency = 1
	
	playSound(soundExplosion)

	if CONFIG.flashActif   then creerFlash(position)      end
	if CONFIG.ondeActive   then creerOndeChoc(position)   end

	if CONFIG.fumeeActive  then creerFumee(position)      end
	
	creerEtincelles(position)
	creerDebris(position)

	appliquerForce(position)
	ball:Destroy()
end
Catégories
Jeu vidéo ROBLOX ROBLOX-NIVEAU03

Adventure interactive rooms

Créer un jeu sur Roblox, inventer une expérience interactive pour les joueurs. tu vas construire un véritable espace de salles de cinéma interactives dans Roblox Studio.

Le joueur peut se déplacer librement d’une salle à l’autre, découvrir différents écrans et interagir directement avec eux grâce à la souris. Chaque salle pourra proposer une ambiance différente, des boutons interactifs ou des animations qui réagissent aux actions du joueur.

Par script, tu iras encore plus loin en découvrant comment modifier la caméra du joueur pour rendre l’expérience plus immersive. Selon les situations, la caméra permettra :

  • de passer automatiquement en vue First Person lorsque le joueur entre dans une salle ;
  • ou de rapprocher la caméra du personnage pour donner un effet de cinéma ou de jeu narratif ;
  • et même de changer la caméra avec une touche du clavier.

À travers ce projet, tu utilisera plusieurs outils importants de Roblox Studio :

  • la création d’interfaces et d’écrans interactifs sur des parts;
  • les scripts en langage Lua ;
  • la gestion des déplacements du joueur ;
  • les événements déclenchés par le clavier ou les collisions ;
  • et le contrôle de la caméra pour améliorer l’immersion du jeu.

A toi de réfléchir à partir de ces scripts pour programmer un jeu comme de vrais créateurs de jeux vidéo : comment guider le joueur, rendre une salle intéressante et créer une ambiance unique grâce à la caméra et aux interactions.

Comment créer une interactive room

Crée la structure de la salle de cinéma avec une boite de collision hit box et un écran sur le lequel tu positionnes un surfaceGui. Sur le surfaceGui crée un textButton que l’on fera bouger :

Le joueur interagit sur le bouton.

Crée et modifie le script sous surfaceGui pour déplacer le bouton et interagir avec la souris :

-- récupération du frame
local frame = script.Parent.Frame
-- récupération du service de tween
local tweenService = game:GetService("TweenService")
-- récupération du bouton
local button = frame.TextButton
-- variable de bascule entre pause et reprendre le déplacement
local pause = false
-- couleur de base du bouton
local colorBase = button.BackgroundColor3

-- déplacement horizontal du bouton avec un tweenService
local tween = tweenService:Create(button, 
	TweenInfo.new(10, 
		Enum.EasingStyle.Linear, 
		Enum.EasingDirection.InOut, -1, false), 
	{Position = button.Position + UDim2.new(1, 0, 0, 0)})

tween:Play()

-- action sur le click de souris
button.MouseButton1Click:Connect(function()
	--click de la souris pour mettre en pause ou reprendre le tween
	pause = not pause
	if pause then
		tween:Pause()
		button.BackgroundColor3 = Color3.fromRGB(0, 255, 0)
	else
		tween:Play()
		button.BackgroundColor3 = colorBase
	end


end)

Sous ReplicatedStorage crée un remoteEvent, ce remoteEvent est utilisé pour prévenir un scriptLocal du joueur afin de changer son comportement :

La hitbox est une Part qui occupe tout l’intérieur de la salle. Elle est transparente et sans CanCollide. Elle sert à gérer la collision lorsque le joueur pénètre dans la salle.

Crée et modifie ce script pour gérer la collision avec le joueur quand celui ci rentre ou sort de la salle pour envoyer un signal au script local du joueur :

local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- récupération du remoteEvent
local kioskEvent = ReplicatedStorage:WaitForChild("KioskEvent")
-- récupération des services Debris
local Debris            = game:GetService("Debris")
-- récupération de la hitbox
local hitbox = script.Parent
-- nom de l'action
local actionName = "KioskLocked"
--- durée du cooldown en secondes
local COOLDOWN_DURATION = 2

-- locker du kiosk
local function KioskLocked(character)
	
	-- si le joueur est déjà dans la salle, ne rien faire
	if character:FindFirstChild(actionName) then return false end
	-- création d'un tag temporaire pour indiquer que le joueur est dans la salle		
	local tag = Instance.new("Folder")
	tag.Name  = actionName
	tag.Parent = character
	Debris:AddItem(tag, COOLDOWN_DURATION)
	
	return true
end

-- détection du joueur qui entre dans la salle 
hitbox.Touched:Connect(function(hit)
	local player = game.Players:GetPlayerFromCharacter(hit.Parent)
	if player then
		local character = player.Character
		if not character then return end
		-- Envoyer au client concerné uniquement
		if KioskLocked(character) then
			kioskEvent:FireClient(player, true)  
		end	
	end
end)

-- détection du joueur qui sort de la salle
hitbox.TouchEnded:Connect(function(hit)
	local player = game.Players:GetPlayerFromCharacter(hit.Parent)
	if player then
		local character = player.Character
		if not character then return end
		-- Envoyer au client concerné uniquement
		if KioskLocked(character) then
			kioskEvent:FireClient(player, false)  
		end		
	end
end)

Crée un localScript sous StarterPlayer pour gérer une touche pour le changement de caméra sur le joueur ou pour gérer le remoteEvent venant du serveur :

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local kioskEvent = ReplicatedStorage:WaitForChild("KioskEvent")
local CinemaEvent = ReplicatedStorage:WaitForChild("CinemaEvent")

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local player = Players.LocalPlayer
local char = player.Character or player.CharacterAdded:Wait()
local humanoid = char:FindFirstChildOfClass("Humanoid")
local head = char:WaitForChild("Head")
local hrp = char:WaitForChild("HumanoidRootPart")
local cam = workspace.CurrentCamera

local screen = player.PlayerGui.ScreenGui
local cinemaButton = screen.Frame.CinemaButton

local angleY = 0
local angleX = 0
local DISTANCE = -0.5
local HEIGHT = 0

local KEY_ROTATION_SPEED = 2

local isLocked = false
local connection = nil  -- stocker la connexion pour pouvoir la couper

local function enableLockedCam(firstPerson, autorotate, viewAngle, distance, height)
	-- Paramètres par défaut
	if firstPerson == nil then
		firstPerson = true
	end
	
	if autorotate == nil then
		autorotate = true
	end
	
	if not autorotate then
		humanoid.WalkSpeed = 0
		humanoid.JumpPower = 0 
	end

	viewAngle = viewAngle or {0,0}
	distance    = distance    or DISTANCE
	height = height or HEIGHT

	cam.CameraType = Enum.CameraType.Custom
	
	if firstPerson then
		UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
	else
		UserInputService.MouseBehavior = Enum.MouseBehavior.Default
	end
	-- Initialiser l'angle sur la rotation actuelle du personnage
	-- Correction : ToEulerAnglesYXZ retourne (X, Y, Z), l'angle horizontal est le 2ème
	local currentAngleX, currentAngleY, _ = hrp.CFrame:ToEulerAnglesYXZ()
	angleY = math.deg(currentAngleY)
	angleX = math.deg(currentAngleX)
	
	local initangleY = angleY
	local initangleX = angleX
	
	connection = RunService.RenderStepped:Connect(function()
		
		local delta = UserInputService:GetMouseDelta()
		
		-- Rotation clavier (flèches ou Q/D)
		local keyDeltaX = 0
		if UserInputService:IsKeyDown(Enum.KeyCode.Left) or UserInputService:IsKeyDown(Enum.KeyCode.Q) then
			keyDeltaX = -KEY_ROTATION_SPEED
		elseif UserInputService:IsKeyDown(Enum.KeyCode.Right) or UserInputService:IsKeyDown(Enum.KeyCode.D) then
			keyDeltaX = KEY_ROTATION_SPEED
		end

		local keyDeltaY = 0
		if UserInputService:IsKeyDown(Enum.KeyCode.Up) then
			keyDeltaY = KEY_ROTATION_SPEED
		elseif UserInputService:IsKeyDown(Enum.KeyCode.Down) then
			keyDeltaY = -KEY_ROTATION_SPEED
		end
		
		-- Fusionner souris + clavier
		local totalDeltaX = delta.X + keyDeltaX
		local totalDeltaY = delta.Y 	

		
		if viewAngle[1] > 0 then
			angleY = math.clamp(angleY - totalDeltaX, -viewAngle[1]+initangleY, viewAngle[1]+initangleY)
		elseif viewAngle[1] < 0 then
			angleY = 0
		else
			angleY = angleY - totalDeltaX * 0.3
		end
		
		if viewAngle[2] > 0 then
			angleX = math.clamp(angleX - totalDeltaY, -viewAngle[2]+initangleX, viewAngle[2]+initangleX)
		elseif viewAngle[2] < 0 then
			angleX = 0
		else
			angleX = angleX - totalDeltaY* 0.3
		end
		
		-- Caméra placée dans la tête, regardant vers l'avant
		-- local rotation = CFrame.Angles(math.rad(angleX), math.rad(angleY), 0)
		-- Reculer la caméra derrière la tête (ajuste la valeur selon le rendu)
		local camCFrame = CFrame.new(head.Position) * CFrame.Angles(0, math.rad(angleY), 0) * CFrame.Angles(math.rad(angleX), 0, 0) * CFrame.new(0,  height, distance)  
		cam.CFrame = camCFrame
		
		-- Tourner le personnage avec la caméra
		hrp.CFrame = CFrame.new(hrp.Position) * CFrame.Angles(0, math.rad(angleY), 0)


	end)
end

local function disableLockedCam()
	-- Couper le RenderStepped
	if connection then
		task.spawn(function()
			RunService.RenderStepped:Wait()
			connection:Disconnect()
			connection = nil
		end)

	end
	-- Restaurer le comportement normal de Roblox
	local cam = workspace.CurrentCamera
	cam.CameraType = Enum.CameraType.Custom
	
	-- 2. Reassigner le sujet (crucial !)
	if humanoid then
		cam.CameraSubject = humanoid
		humanoid.WalkSpeed = 16
		humanoid.JumpPower = 50
	end
	-- 3. Laisser Roblox reprendre le contrôle au prochain frame
	RunService.RenderStepped:Wait()
	UserInputService.MouseBehavior = Enum.MouseBehavior.Default
	
end

-- Toggle avec F
UserInputService.InputBegan:Connect(function(input, gameProcessed)
	if gameProcessed then return end
	if input.KeyCode == Enum.KeyCode.F then
		print("F",isLocked)
		isLocked = not isLocked
		if isLocked then
			enableLockedCam(false, true, {180,60},-2, 1)
		else
			disableLockedCam()
		end
	end
end)

screen.Enabled = false

-- Écouter l'événement envoyé par le serveur
kioskEvent.OnClientEvent:Connect(function(entered)
	print("KioskEvent reçu :", entered)

	if entered then
		screen.Enabled = true
		if not isLocked then 
			enableLockedCam(false, true, {180,0},-2, 1)
		end
	else
		screen.Enabled = false
		isLocked = false
		disableLockedCam()
	end
end)

local cinemaetat = false
cinemaButton.MouseButton1Click:Connect(function()
	cinemaetat = not cinemaetat
	CinemaEvent:FireServer(cinemaetat)
end)

Vue derrière le joueur :

enableLockedCam(false, false, {120,10},5, 2)
firstperson ou pas, player peut bouger ou pas, {angle de vision horizontal, angle de vision vertical, 5 studs en arrière, 2 au dessus de la tête

Vue devant le joueur :

enableLockedCam(false, {120,0},-1, 0)
firstperson ou pas, player peut bouger ou pas, {angle de vision horizontal, angle de vision vertical, 1 stud en avant de la tête, 0 au dessus de la tête

Vue firstperson :

enableLockedCam(true)
Catégories
Jeu vidéo ROBLOX ROBLOX-NIVEAU03

Ennemies qui avancent !!

Dans ce tutoriel, tu vas apprendre à programmer des ennemis très simples dans Roblox. Les ennemis sont représentés par des blocs. Les ennemies détectent un joueur proche, avancent vers lui, et lui infligent des dégâts lorsqu’ils le touchent.
Tu vas construire ton script pour gérer plusieurs ennemis qui pourront avoir des comportements différents : distance proche différence, vitesse de déplacement différente, niveau de dommage différent.

Les ennemis s’approchent

Dans un premier temps construis l’arborescence suivante avec un Folder qui va regrouper tous tes ennemis, un script, des Part qui vont représenter tes ennemis :

Renomme les éléments de ton arborescence :

  • Folder : Enemies
  • Script : EnemiesScript
  • Part : Enemy

Puis saisie le code suivant dans ton script pour que tous les blocs ennemis s’approchent du joueur à partir d’une certaine distance entre le joueur et le bloc :

local RunService = game:GetService("RunService")
local Players    = game:GetService("Players")
local Debris            = game:GetService("Debris")

-- Configuration
local DETECTION_DISTANCE = 30   -- distance de détection du joueur (studs)
local SPEED              = 10   -- vitesse de déplacement (studs/s)
local STOP_DISTANCE      = 3    -- distance à laquelle la part s'arrête

local enemies          = script.Parent

-- Cache des données par platform (évite GetAttribute à chaque frame)
local enemiesData = {}

-- Initialisation
for _, enemy in enemies:GetChildren() do

	if not enemy:IsA("BasePart") then continue end

	enemy.Anchored = true

	-- Stocke les données en mémoire
	enemiesData[enemy] = {
		detectionDistance = enemy:GetAttribute("DetectionDistance") or DETECTION_DISTANCE,
		speed = enemy:GetAttribute("Speed") or SPEED,
		stopDistance = enemy:GetAttribute("StopDistance") or STOP_DISTANCE,
	}

end

RunService.Heartbeat:Connect(function(deltaTime)
	-- Récupère le joueur local (ou le premier joueur connecté côté serveur)
	local player = Players:GetPlayers()[1]
	if not player then return end

	local character = player.Character
	if not character then return end
	-- Récupère la position du joueur
	local root = character:FindFirstChild("HumanoidRootPart")
	if not root then return end

	for enemy, data in pairs(enemiesData) do

		-- Vérifie que la plateforme existe encore
		if not enemy or not enemy.Parent then
			enemyData[enemy] = nil
			continue
		end	
		-- Vérifie que le joueur est à portée
		local distance = (root.Position - enemy.Position).Magnitude
		-- Trop loin : la part attend
		if distance > data.detectionDistance then continue end
		-- Assez proche : arrêt
		if data.stopDistance > 0 and distance <= data.stopDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end
		-- Calcul de la direction vers le joueur
		local direction = (root.Position - enemy.Position).Unit
		local newPosition = enemy.Position + direction * SPEED * deltaTime
		-- Mise à jour de la force à appliquer en fonction de la vitesse
		enemy.AssemblyLinearVelocity = direction * data.speed
		-- Mise à jour de la position de la part et son orientation	
		enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position) + direction * data.speed * deltaTime
	end		
end)

Lance le jeu : les blocs s’approchent du joueur mais restent à une certaine distance du joueur.

Les ennemis infligent des dommages au joueur

Dans le script suivant les blocs s’approchent à toucher le joueur, et lui infligent des dommages :

local RunService = game:GetService("RunService")
local Players    = game:GetService("Players")
local Debris            = game:GetService("Debris")

-- Configuration
local DETECTION_DISTANCE = 30   -- distance de détection du joueur (studs)
local SPEED              = 10   -- vitesse de déplacement (studs/s)
local STOP_DISTANCE      = 0    -- distance à laquelle la part s'arrête
local DAMAGE             = 5    -- dégâts infligés aux joueurs
local RESET_DELAY        = 0.5  -- délai avant de pouvoir toucher à nouveau (secondes)

local enemies          = script.Parent

-- Cache des données par platform (évite GetAttribute à chaque frame)
local enemiesData = {}

-- Initialisation
for _, enemy in enemies:GetChildren() do

	if not enemy:IsA("BasePart") then continue end
	
	enemy.Anchored = true
	enemy.CanCollide = true

	-- Stocke les données en mémoire
	enemiesData[enemy] = {
		detectionDistance = enemy:GetAttribute("DetectionDistance") or DETECTION_DISTANCE,
		speed = enemy:GetAttribute("Speed") or SPEED,
		stopDistance = enemy:GetAttribute("StopDistance") or STOP_DISTANCE,
		damage	= enemy:GetAttribute("Damage")	or DAMAGE,
		resetDelay = enemy:GetAttribute("ResetDelay") or RESET_DELAY,
	}
	
	enemy.Touched:Connect(function(otherPart)
		-- Vérifie que l'objet touché est un personnage
		local character = otherPart.Parent
		if not Players:GetPlayerFromCharacter(character) then return end
		local humanoid = character:FindFirstChildOfClass("Humanoid")
		if not humanoid or humanoid.Health <= 0 then return end
		-- Vérifie que la part n'a pas déjà été touchée
		if enemy:FindFirstChild("Touched") then return end
		-- Applique les dégâts et empêche la répétition
		humanoid:TakeDamage((enemiesData[enemy].damage))
		enemy.AssemblyLinearVelocity = Vector3.zero
		-- Marque la part comme touchée
		local tag = Instance.new("Folder")
		tag.Name  = "Touched"
		tag.Parent = enemy
		Debris:AddItem(tag, enemiesData[enemy].resetDelay)
	end)
end

RunService.Heartbeat:Connect(function(deltaTime)
	-- Récupère le joueur local (ou le premier joueur connecté côté serveur)
	local player = Players:GetPlayers()[1]
	if not player then return end

	local character = player.Character
	if not character then return end
	-- Récupère la position du joueur
	local root = character:FindFirstChild("HumanoidRootPart")
	if not root then return end
	
	for enemy, data in pairs(enemiesData) do
	
		-- Vérifie que la plateforme existe encore
		if not enemy or not enemy.Parent then
			enemyData[enemy] = nil
			continue
		end	
		-- Si la part est déja touchée, on ne fait rien
		if enemy:FindFirstChild("Touched") then return end
		-- Vérifie que le joueur est à portée
		local distance = (root.Position - enemy.Position).Magnitude
		-- Trop loin : la part attend
		if distance > data.detectionDistance then return end
		-- Assez proche : arrêt
		if data.stopDistance > 0 and distance <= data.stopDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end
		-- Calcul de la direction vers le joueur
		local direction = (root.Position - enemy.Position).Unit
		
		-- Mise à jour de la force à appliquer en fonction de la vitesse
		enemy.AssemblyLinearVelocity = direction * data.speed
		-- Mise à jour de la position de la part et son orientation	
		enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position) + direction * data.speed * deltaTime
	end		
end)

Paramétrer chaque ennemis

Ajoute aux propriétés de chaque ennemi les attributs suivants avec des valeurs différentes :

  • DetectionDistance : distance de détection du joueur (studs)
  • Speed : vitesse de déplacement (studs/s)
  • StopDistance : distance à laquelle la part s’arrête si 0 l’ennemi vient toucher le joueur
  • Damage : dégâts infligés aux joueurs
  • ResetDelay : délai avant de pouvoir toucher à nouveau (secondes)

Puis saisie le Nom de l’attribut ainsi que son type number :

Puis donne une valeur pour chaque attribut :

L’ennemi est une boule

Ajoute dans le code pour un nouveau comportement pour faire rouler le boule :

local RunService = game:GetService("RunService")
local Players    = game:GetService("Players")
local Debris            = game:GetService("Debris")

-- Configuration
local DETECTION_DISTANCE = 30   -- distance de détection du joueur (studs)
local SPEED              = 10   -- vitesse de déplacement (studs/s)
local STOP_DISTANCE      = 0    -- distance à laquelle la part s'arrête
local DAMAGE             = 5    -- dégâts infligés aux joueurs
local RESET_DELAY        = 0.5  -- délai avant de pouvoir toucher à nouveau (secondes)

local enemies          = script.Parent

-- Cache des données par platform (évite GetAttribute à chaque frame)
local enemiesData = {}

-- Initialisation
for _, enemy in enemies:GetChildren() do

	if not enemy:IsA("BasePart") then continue end

	enemy.Anchored = true
	enemy.CanCollide = true

	-- Stocke les données en mémoire
	enemiesData[enemy] = {
		detectionDistance = enemy:GetAttribute("DetectionDistance") or DETECTION_DISTANCE,
		speed         	  = enemy:GetAttribute("Speed")             or SPEED,
		stopDistance 	  = enemy:GetAttribute("StopDistance")      or STOP_DISTANCE,
		damage	          = enemy:GetAttribute("Damage")	    or DAMAGE,
		resetDelay        = enemy:GetAttribute("ResetDelay")        or RESET_DELAY,
	}
	
	enemy.Touched:Connect(function(otherPart)
		-- Vérifie que l'objet touché est un personnage
		local character = otherPart.Parent
		if not Players:GetPlayerFromCharacter(character) then return end
		local humanoid = character:FindFirstChildOfClass("Humanoid")
		if not humanoid or humanoid.Health <= 0 then return end
		-- Vérifie que la part n'a pas déjà été touchée
		if enemy:FindFirstChild("Touched") then return end
		-- Applique les dégâts et empêche la répétition
		enemy.AssemblyLinearVelocity = Vector3.zero
		enemy.AssemblyAngularVelocity = Vector3.zero
		humanoid:TakeDamage((enemiesData[enemy].damage))
		-- Marque la part comme touchée
		local tag = Instance.new("Folder")
		tag.Name  = "Touched"
		tag.Parent = enemy
		Debris:AddItem(tag, enemiesData[enemy].resetDelay)
	end)
end

RunService.Heartbeat:Connect(function(deltaTime)
	-- Récupère le joueur local (ou le premier joueur connecté côté serveur)
	local player = Players:GetPlayers()[1]
	if not player then return end

	local character = player.Character
	if not character then return end
	-- Récupère la position du joueur
	local root = character:FindFirstChild("HumanoidRootPart")
	if not root then return end

	for enemy, data in pairs(enemiesData) do

		-- Vérifie que la plateforme existe encore
		if not enemy or not enemy.Parent then
			--enemyData[enemy] = nil
			continue
		end	
		-- Si la part est déja touchée, on ne fait rien
		if enemy:FindFirstChild("Touched") then continue end
		-- Vérifie que le joueur est à portée
		local distance = (root.Position - enemy.Position).Magnitude
		-- Trop loin : la part attend
		if distance > data.detectionDistance then continue end
		-- Assez proche : arrêt
		if data.stopDistance > 0 and distance <= data.stopDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end
		-- Calcul de la direction vers le joueur
		
		if enemy.Shape == Enum.PartType.Ball then
			enemy.Anchored = false
			-- Direction vers le joueur (ignorant Y pour rester au sol)
			local direction = Vector3.new(
				root.Position.X - enemy.Position.X,
				0,
				root.Position.Z - enemy.Position.Z
			).Unit

			-- Force appliquée vers le joueur
			local mass      = enemy.AssemblyMass
			local targetVelocity = direction * data.speed
			local correction     = targetVelocity - Vector3.new(
				enemy.AssemblyLinearVelocity.X,
				0,
				enemy.AssemblyLinearVelocity.Z
			)
			enemy:ApplyImpulse(correction * mass)

			-- Rotation de roulement
			local rollAxis  = Vector3.new(-direction.Y, 0, -direction.X)
			local rollSpeed = data.speed / (enemy.Size.Y / 2)
			if enemy.Size.X > enemy.Size.Y then
				rollSpeed = data.speed / (enemy.Size.X / 2)
			end
			enemy.AssemblyAngularVelocity = rollAxis * rollSpeed
		else
			local direction = (root.Position - enemy.Position).Unit

			-- Mise à jour de la force à appliquer en fonction de la vitesse
			enemy.AssemblyLinearVelocity = direction * data.speed
			-- Mise à jour de la position de la part et son orientation	
			enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position) + direction * data.speed * deltaTime
		end

	end		
end)

Des ennemis qui ne traversent pas les murs (Raycast) :

-- recherche des services
local RunService = game:GetService("RunService")
local Players    = game:GetService("Players")
local Debris     = game:GetService("Debris")

-- Configuration
local DETECTION_DISTANCE = 60   -- distance de détection du joueur (studs)
local SPEED              = 12   -- vitesse de déplacement (studs/s)
local STOP_DISTANCE      = 10    -- distance à laquelle la part s'arrête

-- recherche des ennemis dans le workspace sous le folder "Monstre"
local enemies          = workspace.GAME:WaitForChild("Monstre")

-- Paramètres dynamiques par ennemi
local enemiesData = {}

-- Initialisation de tous les ennemis
for _, enemy in enemies:GetChildren() do

	-- contrôle si c'est une part qui représente un ennemi
	if not enemy:IsA("BasePart") then continue end

	enemy.Anchored = true

	-- Stocke les données en mémoire pour chaque ennemi
	enemiesData[enemy] = {
		detectionDistance = enemy:GetAttribute("DetectionDistance") or DETECTION_DISTANCE,
		speed = enemy:GetAttribute("Speed") or SPEED,
		stopDistance = enemy:GetAttribute("StopDistance") or STOP_DISTANCE,
		cooldown = false,
	}

	-- dégats sur le joueur si il touche l'ennemi'
	enemy.Touched:Connect(function(hit)
		local character = hit.Parent
		local humanoid  = character and character:FindFirstChildOfClass("Humanoid")
		if not humanoid or humanoid.Health <= 0 then return end
		if enemiesData[enemy].cooldown then return end
		enemiesData[enemy].cooldown = true
		humanoid:TakeDamage(10)
		task.delay(0.5, function() enemiesData[enemy].cooldown = false end)
	end)
end

--[[ Configuration du Raycast (exclure les ennemis eux-mêmes et le personnage)
     Pour éviter que les parts traversent les murs, il faut faire un Raycast entre la part et le joueur. 
     Si le rayon touche un mur avant le joueur on stoppe la part.
]]

local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Exclude

RunService.Heartbeat:Connect(function(deltaTime)
	local player = Players:GetPlayers()[1]
	if not player then return end
	local character = player.Character
	if not character then return end
	local root = character:FindFirstChild("HumanoidRootPart")
	if not root then return end

	-- Met à jour les exclusions à chaque frame (Player + tous les ennemis exclus)
	local excluded = { character }
	for enemy in pairs(enemiesData) do
		if enemy and enemy.Parent then
			table.insert(excluded, enemy)
		end
	end
	raycastParams.FilterDescendantsInstances = excluded

	for enemy, data in pairs(enemiesData) do
		if not enemy or not enemy.Parent then
			enemiesData[enemy] = nil
			continue
		end
		
		-- Calcul de la direction et de la distance entre l'ennemi et le joueur
		local toPlayer    = root.Position - enemy.Position
		local distance    = toPlayer.Magnitude
		local direction   = toPlayer.Unit

		-- Trop loin : la part attend
		if distance > data.detectionDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end

		-- Assez proche : arrêt
		if data.stopDistance > 0 and distance <= data.stopDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end

		-- Raycast entre l'ennemi et le joueur
		local rayResult = workspace:Raycast(enemy.Position, direction * distance, raycastParams)

		if rayResult then
			-- Un mur est détecté avant le joueur : on stoppe
			enemy.AssemblyLinearVelocity = Vector3.zero
			-- Optionnel : orienter quand même vers le joueur
			enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position)
		else
			-- Chemin libre : déplacement normal
			enemy.AssemblyLinearVelocity = direction * data.speed
			enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position) 
				+ direction * data.speed * deltaTime
		end
	end
end)

Les ennemies sont des RIG

Ajoute des rig sous une structure suivante :

Pour ajouter un RIG choisis :

Ajoute sous chaque Rig un script que tu renommes Animate, ce script anime le Rig pour qu’il marche ou qu’il attende en fonction de la situation :

-- recherche des services
local RunService = game:GetService("RunService")
local Players    = game:GetService("Players")
local Debris     = game:GetService("Debris")

-- Configuration
local DETECTION_DISTANCE = 60   -- distance de détection du joueur (studs)
local SPEED              = 12   -- vitesse de déplacement (studs/s)
local STOP_DISTANCE      = 10    -- distance à laquelle la part s'arrête

-- recherche des ennemis dans le workspace sous le folder "Monstre"
local enemies          = workspace.GAME:WaitForChild("Monstre")

-- Paramètres dynamiques par ennemi
local enemiesData = {}

-- Initialisation de tous les ennemis
for _, enemy in enemies:GetChildren() do

	-- contrôle si c'est une part qui représente un ennemi
	if not enemy:IsA("BasePart") then continue end

	enemy.Anchored = true

	-- Stocke les données en mémoire pour chaque ennemi
	enemiesData[enemy] = {
		detectionDistance = enemy:GetAttribute("DetectionDistance") or DETECTION_DISTANCE,
		speed = enemy:GetAttribute("Speed") or SPEED,
		stopDistance = enemy:GetAttribute("StopDistance") or STOP_DISTANCE,
		cooldown = false,
	}

	-- dégats sur le joueur si il touche l'ennemi'
	enemy.Touched:Connect(function(hit)
		local character = hit.Parent
		local humanoid  = character and character:FindFirstChildOfClass("Humanoid")
		if not humanoid or humanoid.Health <= 0 then return end
		if enemiesData[enemy].cooldown then return end
		enemiesData[enemy].cooldown = true
		character:PivotTo(CFrame.new(-10, 1.5, -69))
		task.delay(0.5, function() enemiesData[enemy].cooldown = false end)
	end)
end

--[[ Configuration du Raycast (exclure les ennemis eux-mêmes et le personnage)
     Pour éviter que les parts traversent les murs, il faut faire un Raycast entre la part et le joueur. 
     Si le rayon touche un mur avant le joueur on stoppe la part.
]]

local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Exclude

RunService.Heartbeat:Connect(function(deltaTime)
	local player = Players:GetPlayers()[1]
	if not player then return end
	local character = player.Character
	if not character then return end
	local root = character:FindFirstChild("HumanoidRootPart")
	if not root then return end

	-- Met à jour les exclusions à chaque frame (Player + tous les ennemis exclus)
	local excluded = { character }
	for enemy in pairs(enemiesData) do
		if enemy and enemy.Parent then
			table.insert(excluded, enemy)
		end
	end
	raycastParams.FilterDescendantsInstances = excluded

	for enemy, data in pairs(enemiesData) do
		if not enemy or not enemy.Parent then
			enemiesData[enemy] = nil
			continue
		end
		
		-- Calcul de la direction et de la distance entre l'ennemi et le joueur
		local toPlayer    = root.Position - enemy.Position
		local distance    = toPlayer.Magnitude
		local direction   = toPlayer.Unit

		-- Trop loin : la part attend
		if distance > data.detectionDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end

		-- Assez proche : arrêt
		if data.stopDistance > 0 and distance <= data.stopDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end

		-- Raycast entre l'ennemi et le joueur
		local rayResult = workspace:Raycast(enemy.Position, direction * distance, raycastParams)

		if rayResult then
			-- Un mur est détecté avant le joueur : on stoppe
			enemy.AssemblyLinearVelocity = Vector3.zero
			-- Optionnel : orienter quand même vers le joueur
			enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position)
		else
			-- Chemin libre : déplacement normal
			enemy.AssemblyLinearVelocity = direction * data.speed
			enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position) 
				+ direction * data.speed * deltaTime
		end
	end
end)

Sous ton folder principal, créer un script et ajoute ce code :

local RunService = game:GetService("RunService")
local Players    = game:GetService("Players")

-- Configuration
local DETECTION_DISTANCE = 60
local STOP_DISTANCE      = 3
local SPEED              = 10
local UPDATE_INTERVAL    = 0.5  -- réduit pour un mouvement plus fluide
local DAMAGE             = 5
local RESET_DELAY        = 0.5

local rigsFolder    = script.Parent
local enemieRigData = {}

-- Crée une hitbox soudée sur le HumanoidRootPart
local function createHitbox(rig)
	local root = rig:FindFirstChild("HumanoidRootPart")
	if not root then return nil end

	local hitbox             = Instance.new("Part")
	hitbox.Name              = "Hitbox_" .. rig.Name
	hitbox.Shape             = Enum.PartType.Ball
	hitbox.Size              = Vector3.new(5, 5, 5)
	hitbox.Transparency      = 0.8
	hitbox.CanCollide        = false
	hitbox.CanTouch          = true
	hitbox.Anchored          = false
	hitbox.CastShadow        = false
	hitbox.Massless          = true
	hitbox.Color             = Color3.fromRGB(255, 0, 0)
	hitbox.Material          = Enum.Material.Neon
	hitbox.CFrame            = root.CFrame * CFrame.new(0, 1, -0.5)
	hitbox.Parent            = workspace

	local weld       = Instance.new("WeldConstraint")
	weld.Part0       = root
	weld.Part1       = hitbox
	weld.Parent      = hitbox

	-- Nettoyage automatique si le rig est détruit
	rig.AncestryChanged:Connect(function()
		if not rig.Parent then
			hitbox:Destroy()
			enemieRigData[rig] = nil
		end
	end)

	return hitbox
end

-- Arrête le rig
local function stopRig(data)
	if not data.isMoving then return end
	data.humanoid.WalkSpeed = 0
	data.isMoving           = false
end

-- Déplace le rig vers une cible
local function chaseTarget(data, targetPosition)
	local now = time()
	if now - data.lastUpdate < data.updateInterval then return end
	data.lastUpdate = now

	data.humanoid.WalkSpeed = data.speed
	data.humanoid:MoveTo(targetPosition)
	data.isMoving = true
end

-- Initialisation des rigs
for _, rig in ipairs(rigsFolder:GetChildren()) do
	if not rig:IsA("Model") then continue end

	local humanoid = rig:FindFirstChildOfClass("Humanoid")
	local rootPart = rig:FindFirstChild("HumanoidRootPart")
	if not humanoid or not rootPart then continue end

	local hitbox = createHitbox(rig)
	if not hitbox then continue end

	enemieRigData[rig] = {
		detectionDistance = rig:GetAttribute("DetectionDistance") or DETECTION_DISTANCE,
		speed             = rig:GetAttribute("Speed")             or SPEED,
		stopDistance      = rig:GetAttribute("StopDistance")      or STOP_DISTANCE,
		updateInterval    = rig:GetAttribute("UpdateInterval")    or UPDATE_INTERVAL,
		damage            = rig:GetAttribute("Damage")            or DAMAGE,
		hitbox            = hitbox,
		humanoid          = humanoid,
		rootPart          = rootPart,
		lastUpdate        = 0,
		isMoving          = false,
		lastHit           = 0,
	}

	-- Détection de collision avec le joueur
	hitbox.Touched:Connect(function(hit)
		local data = enemieRigData[rig]
		if not data then return end

		-- Vérifie que c'est bien le joueur (pas une autre hitbox)
		local character = hit:FindFirstAncestorOfClass("Model")
		if not character then return end

		local humanoidTarget = character:FindFirstChildOfClass("Humanoid")
		local player         = Players:GetPlayerFromCharacter(character)
		if not humanoidTarget or not player then return end

		-- Cooldown pour éviter les dégâts multiples
		local now = time()
		if now - data.lastHit < RESET_DELAY then return end
		data.lastHit = now

		humanoidTarget:TakeDamage(data.damage)
		print(rig.Name, "inflige", data.damage, "dégâts à", player.Name)
	end)
end

-- Heartbeat : déplacement de tous les rigs
RunService.Heartbeat:Connect(function()
	local player = Players:GetPlayers()[1]
	if not player then return end

	local character = player.Character
	if not character then return end

	local root = character:FindFirstChild("HumanoidRootPart")
	if not root then return end

	for rig, data in pairs(enemieRigData) do
		if not rig.Parent then continue end

		local distance = (root.Position - data.rootPart.Position).Magnitude

		if distance > data.detectionDistance or distance <= data.stopDistance then
			stopRig(data)
			continue  
		end

		chaseTarget(data, root.Position)
	end
end)

Les Rig disposent d’une Hitbox pour gérer la collision avec le joueur, pour rendre complétement transparente cette Hitbox, modifie sa Tranparency :

-- Crée une hitbox soudée sur le HumanoidRootPart
local function createHitbox(rig)
	local root = rig:FindFirstChild("HumanoidRootPart")
	if not root then return nil end

	local hitbox             = Instance.new("Part")
	hitbox.Name              = "Hitbox_" .. rig.Name
	hitbox.Shape             = Enum.PartType.Ball
	hitbox.Size              = Vector3.new(5, 5, 5)
	hitbox.Transparency      = 1

Exemple d’un code avec des ennemis qui ne peuvent pas traverser un mur, utilisation de Raycast :

-- recherche des services
local RunService = game:GetService("RunService")
local Players    = game:GetService("Players")
local Debris     = game:GetService("Debris")

-- Configuration
local DETECTION_DISTANCE = 60   -- distance de détection du joueur (studs)
local SPEED              = 3   -- vitesse de déplacement (studs/s)
local STOP_DISTANCE      = 2    -- distance à laquelle la part s'arrête

-- recherche des ennemis dans le workspace sous le folder "Monstre"
local enemies          = workspace.GAME:WaitForChild("Monstre")

-- Paramètres dynamiques par ennemi
local enemiesData = {}

-- Initialisation de tous les ennemis
for _, enemy in enemies:GetChildren() do

	-- contrôle si c'est une part qui représente un ennemi
	if not enemy:IsA("BasePart") then continue end

	enemy.Anchored = true

	-- Stocke les données en mémoire pour chaque ennemi
	enemiesData[enemy] = {
		detectionDistance = enemy:GetAttribute("DetectionDistance") or DETECTION_DISTANCE,
		speed = enemy:GetAttribute("Speed") or SPEED,
		stopDistance = enemy:GetAttribute("StopDistance") or STOP_DISTANCE,
		cooldown = false,
	}

	-- dégats sur le joueur si il touche l'ennemi'
	enemy.Touched:Connect(function(hit)
		local character = hit.Parent
		local humanoid  = character and character:FindFirstChildOfClass("Humanoid")
		if not humanoid or humanoid.Health <= 0 then return end
		if enemiesData[enemy].cooldown then return end
		enemiesData[enemy].cooldown = true
		humanoid:TakeDamage(10)
		task.delay(0.5, function() enemiesData[enemy].cooldown = false end)
	end)
end

--[[ Configuration du Raycast (exclure les ennemis eux-mêmes et le personnage)
     Pour éviter que les parts traversent les murs, il faut faire un Raycast entre la part et le joueur. 
     Si le rayon touche un mur avant le joueur on stoppe la part.
]]

local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Exclude

RunService.Heartbeat:Connect(function(deltaTime)
	local player = Players:GetPlayers()[1]
	if not player then return end
	local character = player.Character
	if not character then return end
	local root = character:FindFirstChild("HumanoidRootPart")
	if not root then return end

	-- Met à jour les exclusions à chaque frame (Player + tous les ennemis exclus)
	local excluded = { character }
	for enemy in pairs(enemiesData) do
		if enemy and enemy.Parent then
			table.insert(excluded, enemy)
		end
	end
	raycastParams.FilterDescendantsInstances = excluded

	for enemy, data in pairs(enemiesData) do
		if not enemy or not enemy.Parent then
			enemiesData[enemy] = nil
			continue
		end
		
		-- Calcul de la direction et de la distance entre l'ennemi et le joueur
		local toPlayer    = root.Position - enemy.Position
		local distance    = toPlayer.Magnitude
		local direction   = toPlayer.Unit

		-- Trop loin : la part attend
		if distance > data.detectionDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end

		-- Assez proche : arrêt
		if data.stopDistance > 0 and distance <= data.stopDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end

		-- Raycast entre l'ennemi et le joueur
		local rayResult = workspace:Raycast(enemy.Position, direction * distance, raycastParams)

		if rayResult then
			-- Un mur est détecté avant le joueur : on stoppe
			enemy.AssemblyLinearVelocity = Vector3.zero
			-- Optionnel : orienter quand même vers le joueur
			enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position)
		else
			-- Chemin libre : déplacement normal
			enemy.AssemblyLinearVelocity = direction * data.speed
			enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position) 
				+ direction * data.speed * deltaTime
		end
	end
end)
Catégories
Jeu vidéo ROBLOX ROBLOX-NIVEAU03

ROBLOX Minecraft

Dans ce tutoriel, tu vas apprendre à programmer un système de construction comme dans Minecraft avec Roblox et le langage Lua.
Le joueur pourra créer des blocs pour construire des structures de blocs et détruire des blocs.
Tu découvriras comment utiliser la souris, le clavier et les scripts pour interagir avec les objets dans Roblox Studio.
Ce projet permet d’apprendre les bases importantes de la programmation de jeux vidéo : événements, conditions, coordonnées et objets 3D, les scripts en local et les scripts sur le serveur.
À la fin du tutoriel, tu auras créé un véritable système de construction interactif dans ton propre jeu Roblox.

Dans un premier temps, tu vas récupérer les actions du joueur comme les clics de souris et les touches du clavier dans LocalScript.
Dans un deuxième temps, ces informations seront ensuite envoyées par un système ROBLOX de RemoteEvent au serveur afin de vérifier et traiter les demandes du joueur.
Dans un troisième temps, tu créeras un Script sur le serveur pour créer ou détruir les blocs dans le monde du jeu pour que tous les joueurs puissent voir les modifications en même temps.

Tu découvriras ainsi comment fonctionne la communication entre le joueur et le serveur dans un vrai jeu multijoueur Roblox.

Récupération des événements souris et clavier

Sous StarterPlayer puis StarterCharacterScripts crée un Folder que tu renommes Minecraft, puis un LocalScript que tu renommes MCInput :

Le joueur interagit avec le monde du jeu comme dans Minecraft grâce à la souris et au clavier. Un clic sur le bouton gauche de la souris permet de construire un nouveau bloc dans le jeu. Un clic sur le bouton droit de la souris permet de détruire un bloc existant. Cependant, pour laisser la possibilité d’utiliser la souris pour les déplacements du joueur et de la caméra, la construction ou la destruction ne sera possible uniquement si le joueur maintient en même temps la touche Majuscule gauche (Left Shift) du clavier.

Dans MCInput, saisie cette programmation pour récupérer les événements clavier et souris :

local UserInputService = game:GetService("UserInputService")
local RunService       = game:GetService("RunService")

RunService.RenderStepped:Connect(function()
	-- Appuyer sur la touche la touche majuscule pour que la souris soit prise en compteif
	if not UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) then return end
	-- Touche gauche de la souris pour construire
	if UserInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1) then
		print("Mouse left pressed")
	-- Touche droite de la souris pour détruire
	elseif UserInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton2) then
		print("Mouse right pressed")
	end
end)

Lance le jeu et vérifie la prise en compte des boutons gauche et droit de la souris :

Comment remonter les actions du local vers le serveur

Dans Roblox, les actions de la souris sont détectées dans le LocalScript, car elles appartiennent uniquement au joueur qui utilise le jeu. Pour que le serveur puisse créer ou détruire les blocs, nous devons lui envoyer les informations des clics de souris. Nous allons utiliser des RemoteEvent, un système de communication entre le joueur et le serveur dans Roblox. Le LocalScript enverra par exemple le clic gauche et l’action demandée (construire). Le serveur recevra ensuite ces événements et appliquera les modifications dans le monde du jeu pour que tous les joueurs puissent les voir.

Deux événements sont remontés construire et détruire.

Crée deux Folder sous ReplicatedStorage Minecraft puis Remotes.

Sous le folder Remotes crée deux RemoteEvent MCBuild et MCDestroy :

Puis sous ServerScriptService crée le Folder Maincraft puis crée un Script MCSystem afin d’exécuter les ordres reçues par le LocalScript du joueur via les RemoteEvent :

Saisie ce code dans MCSystem :

local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- Références
local minecraft = ReplicatedStorage:WaitForChild("Minecraft")
local remotes = minecraft:WaitForChild("Remotes")


-- Remote : poser un bloc
remotes.MCBuild.OnServerEvent:Connect(function(player, data)
	print("Action Build")
end)

-- Remote : détruire un bloc
remotes.MCDestroy.OnServerEvent:Connect(function(player, data)
  	print("Action Destroy")
end)

Modifie ton LocalScript MCInput pour envoyer les actions :

local UserInputService = game:GetService("UserInputService")
local RunService       = game:GetService("RunService")

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local minecraft = ReplicatedStorage:WaitForChild("Minecraft")
local remotes = minecraft:WaitForChild("Remotes")

RunService.RenderStepped:Connect(function()
	-- Appuyer sur la touche la touche majuscule pour que la souris soit prise en compte
	if not UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) then return end
	-- Touche gauche de la souris pour construire
	if UserInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1) then
		print("Mouse left pressed")
		remotes.MCBuild:FireServer()
	-- Touche droite de la souris pour détruire
	elseif UserInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton2) then
		print("Mouse right pressed")
		remotes.MCDestroy:FireServer()
	end
end)

Lance ton jeu et vérifie que le serveur reçoit bien les deux actions :

Lorsque le joueur maintient un bouton de la souris appuyé, Roblox envoie les événements plusieurs fois de suite très rapidement.
Sans protection, le serveur pourrait alors créer ou détruire un très grand nombre de blocs en quelques secondes. Cela peut provoquer des bugs, ralentir le jeu ou permettre à un joueur de construire trop vite.
Nous allons donc mettre en place un mécanisme de limitation, appelé souvent “cooldown” ou temporisation, pour attendre un petit délai entre deux actions.
Ainsi, le serveur traitera les demandes de manière plus propre et le jeu restera fluide pour tous les joueurs.

Modifie le script serveur MCSystem :

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Debris            = game:GetService("Debris")

-- Références
local minecraft = ReplicatedStorage:WaitForChild("Minecraft")
local remotes = minecraft:WaitForChild("Remotes")

-- Système anti rebond via un Folder temporaire dans le Character du player
local function withCooldown(character, actionName, callback)
	if character:FindFirstChild(actionName) then return end

	local tag = Instance.new("Folder")
	tag.Name  = actionName
	tag.Parent = character
	Debris:AddItem(tag, COOLDOWN_DURATION)

	callback()
end

-- Remote : poser un bloc
remotes.MCBuild.OnServerEvent:Connect(function(player, data)
	local character = player.Character
	withCooldown(character, "Build", function()
		print("Action Build")
	end)

end)

-- Remote : détruire un bloc
remotes.MCDestroy.OnServerEvent:Connect(function(player, data)
	local character = player.Character
	withCooldown(character, "Destroying", function()
		print("Action Destroy")
	end)
end)

Tu remarques que dans la sortie, tu as une seule action pour plusieurs événements souris :

Récupération de la position du bloc à créer

Maintenant, nous devons récupérer la position de la souris dans le monde du jeu et non sa position sur l’écran de l’ordinateur.
En effet, pour construire ou détruire des blocs, le jeu doit savoir exactement quel endroit du monde 3D le joueur vise avec sa souris.
Le LocalScript va donc utiliser un système appelé “Raycast” pour projeter un rayon invisible depuis la caméra vers la scène du jeu.
Grâce à cela, nous pourrons obtenir les coordonnées précises du point visé dans l’espace ainsi que l’objet touché par la souris, par exemple un bloc ou la baseplate.
Ces informations seront ensuite envoyées au serveur pour créer ou supprimer les blocs au bon endroit.

Modifie le script serveur MCSystem :

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Debris            = game:GetService("Debris")

-- Références
local minecraft = ReplicatedStorage:WaitForChild("Minecraft")
local remotes = minecraft:WaitForChild("Remotes")

-- Système anti rebond via un Folder temporaire dans le Character du player
local function withCooldown(character, actionName, callback)
	if character:FindFirstChild(actionName) then return end

	local tag = Instance.new("Folder")
	tag.Name  = actionName
	tag.Parent = character
	Debris:AddItem(tag, COOLDOWN_DURATION)

	callback()
end

-- Valide les données communes aux deux remotes
local function parseData(player, data)
	local character = player.Character
	if not character then return nil end
	if not character:FindFirstChildOfClass("Humanoid") then return nil end

	local position3D  = data[1]
	local direction   = data[2]
	local partTouchee = data[3]
	if not partTouchee then return nil end
	print("Position3D:", position3D, "Direction:", direction, "Part touchée:", partTouchee.Name)
	return character, position3D, direction, partTouchee
end

-- Remote : poser un bloc
remotes.MCBuild.OnServerEvent:Connect(function(player, data)
	local character, position3D, direction = parseData(player, data)
	if not character then return end
	withCooldown(character, "Build", function()
		print("Action Build")
	end)

end)

-- Remote : détruire un bloc
remotes.MCDestroy.OnServerEvent:Connect(function(player, data)
	local character, _, _, partTouchee = parseData(player, data)
	if not character then return end
	withCooldown(character, "Destroying", function()
		print("Action Destroy")
	end)
end)

Lance ton jeu et vérifie que le serveur reçoit la position pointée par la souris, le direction et la part touchée.

Création d’un bloc Minecraft

Pour créer un nouveau bloc dans le jeu, nous allons utiliser un modèle de bloc préparé dans le ReplicatedStorage. Le serveur va créer une copie de ce bloc, appelée un “clone”, puis le placer dans un dossier Minecraft créé dans le Workspace pour ranger proprement tous les blocs Minecraft.
Le bloc doit être ensuite aligné automatiquement sur une grille afin que toutes les constructions restent bien organisées comme dans Minecraft.
Après sa création, le bloc descend jusqu’à toucher un autre bloc ou la Baseplate. Dès qu’il entre en contact avec un support, nous activerons la propriété Anchored pour le bloquer définitivement et éviter qu’il continue à bouger.

Crée un part qui sera ta brique de base Minecraft :

Fais en sorte que le part soit cubique :

Déplace ton bloc de base sous ReplicatedStorage et renomme le MCBlock :

Puis continue à modifier le script serveur MCSystem pour créer les blocs :

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Debris            = game:GetService("Debris")

-- Références
local minecraft = ReplicatedStorage:WaitForChild("Minecraft")
local remotes = minecraft:WaitForChild("Remotes")
local blockTemplate = minecraft:WaitForChild("MCBlock")

-- Configuration
local GRID_SIZE      = blockTemplate.Size.X
local ANCHOR_DELAY   = 0.5  -- délai avant tentative d'ancrage
local VELOCITY_THRESHOLD = 0.1  -- seuil de vitesse pour ancrage
local COOLDOWN_DURATION  = 0.5  -- durée du cooldown build/destroy
local FOLDERMC = "Minecraft"

-- Système anti rebond via un Folder temporaire dans le Character du player
local function withCooldown(character, actionName, callback)
	if character:FindFirstChild(actionName) then return end

	local tag = Instance.new("Folder")
	tag.Name  = actionName
	tag.Parent = character
	Debris:AddItem(tag, COOLDOWN_DURATION)

	callback()
end

-- Valide les données communes aux deux remotes
local function parseData(player, data)
	local character = player.Character
	if not character then return nil end
	if not character:FindFirstChildOfClass("Humanoid") then return nil end

	local position3D  = data[1]
	local direction   = data[2]
	local partTouchee = data[3]
	if not partTouchee then return nil end
	print("Position3D:", position3D, "Direction:", direction, "Part touchée:", partTouchee.Name)
	return character, position3D, direction, partTouchee
end

-- Aligne une position sur la grille en fonction du cube de référence
local function snapToGrid(position)
	local function snap(v) return math.floor(v / GRID_SIZE + 0.5) * GRID_SIZE end
	return Vector3.new(snap(position.X), snap(position.Y), snap(position.Z))
end

-- Ancrage progressif d'un bloc
local function anchorBlock(block)
	if block.Anchored then return end

	task.wait(ANCHOR_DELAY)

	repeat task.wait(0.1) until
	not block or
		not block.Parent or
		block.AssemblyLinearVelocity.Magnitude <= VELOCITY_THRESHOLD

	if not block or not block.Parent then return end

	block.Anchored               = true
	block.AssemblyLinearVelocity  = Vector3.zero
	block.AssemblyAngularVelocity = Vector3.zero
end

-- Récupère ou crée le folder Minecraft dans le workspace
local function getOrCreateFolderMC()
	local folder = workspace:FindFirstChild(FOLDERMC)
	if not folder then
		folder = Instance.new("Folder")
		folder.Name   = FOLDERMC
		folder.Parent = workspace
	end
	return folder
end

local folderMinecraft = getOrCreateFolderMC()
-- Remote : poser un bloc
remotes.MCBuild.OnServerEvent:Connect(function(player, data)
	local character, position3D, direction = parseData(player, data)
	if not character then return end
	withCooldown(character, "Build", function()
		local newPosition = snapToGrid(position3D + direction * GRID_SIZE)

		local newBlock = blockTemplate:Clone()
		newBlock.Position = newPosition
		newBlock.Anchored = false
		newBlock.Parent   = folderMinecraft

		local connection
		connection = newBlock.Touched:Connect(function(otherPart)
			if not otherPart.Anchored and otherPart.Name ~= "Baseplate" then return end
			connection:Disconnect()  -- un seul ancrage
			anchorBlock(newBlock)
		end)
	end)

end)

-- Remote : détruire un bloc
remotes.MCDestroy.OnServerEvent:Connect(function(player, data)
	local character, _, _, partTouchee = parseData(player, data)
	if not character then return end
	withCooldown(character, "Destroying", function()
		print("Action Destroy")
	end)
end)

Destruction d’un bloc

Dans le script serveur MCSystem modifie juste l’action MCDestroy :


-- Remote : détruire un bloc
remotes.MCDestroy.OnServerEvent:Connect(function(player, data)
	local character, _, _, partTouchee = parseData(player, data)
	if not character then return end
	withCooldown(character, "Destroying", function()
		if partTouchee:IsA("BasePart") and partTouchee.Name == blockTemplate.Name then
			partTouchee:Destroy()
		end
	end)
end)

Si tout fonctionne correctement, tu peux supprimer les « print » de ton code ou les mettre en commentaire :

-- Valide les données communes aux deux remotes
local function parseData(player, data)
	local character = player.Character
	if not character then return nil end
	if not character:FindFirstChildOfClass("Humanoid") then return nil end

	local position3D  = data[1]
	local direction   = data[2]
	local partTouchee = data[3]
	if not partTouchee then return nil end
	--print("Position3D:", position3D, "Direction:", direction, "Part touchée:", partTouchee.Name)
	return character, position3D, direction, partTouchee
end
Catégories
Jeu vidéo ROBLOX ROBLOX-NIVEAU03

Système d’ouverture de portes

Construis un système de portes que tu pourras répliquer partout dans ton jeu. Le joueur pourra ouvrir des portes et découvrir d’autres pièces ou d’autres mondes. Tu pourras choisir entre une fermeture manuelle de la porte ou automatique après un laps de temps. Plusieurs boutons synchronisés permettent de piloter une même porte. Le bouton peut-être installé à distance de la porte.

Construis cette arborescence dans ton workspace :

Sous un folder, insére un son pour l’ouverture de ta porte, un script, un part représentant ta porte, sous la porte un part pour représenter le bouton d’ouverture, puis un proxyprompt sous le bouton. Renomme chaque élément pour plus de clarté :

Les noms donnés n’ont pas d’importance par contre la structure de l’arborescence du folder doit être absolument respectée.

Saisie le code suivant dans le script :

-- Récupération du joueur
local Players = game:GetService("Players")

-- Récupération du dossier des portes
local doorsFolder = script.Parent

-- Initialisation de chaque porte
-- Le dossier doit comporte un part pour la porte
-- puis un part pour chaque bouton
-- puis proxymity prompt pour chaque bouton
for _, door in doorsFolder:GetChildren() do
	if not door:IsA("Part") then continue end
	door.Anchored     = true
	door.Transparency = 0
	door.CanCollide   = true

	for _, button in door:GetChildren() do
		if not button:IsA("Part") then continue end
		button.Anchored     = true
		button.Transparency = 0
		button.CanCollide   = true		
		local proxi = button:FindFirstChildOfClass("ProximityPrompt")
		if not proxi then continue end
		proxi.ActionText = LABEL_OPEN
		proxi.KeyboardKeyCode=Enum.KeyCode.E
		local delay = button:GetAttribute("TimeoutClose") or TIMEOUT_CLOSE

		proxi.Triggered:Connect(function(player)
			-- Ouverture ou fermeture de la porte
			
			print("ouverture porte")
		end)
	end
end

Lance ton jeu, quand tu actives le proximitry prompt, le message suivant s’affiche dans la console de sortie mais la porte ne s’ouvre pas encore :

Modifie ton code pour ouvrir la porte :

-- Récupération du joueur
local Players = game:GetService("Players")

-- Configuration
local TIMEOUT_CLOSE = 0
local LABEL_CLOSE   = "Fermer"
local LABEL_OPEN    = "Ouvrir"

-- Récupération du dossier des portes
local doorsFolder = script.Parent
local sound       = doorsFolder:FindFirstChildOfClass("Sound")

-- Lecture du son d'ouverture de la porte
local function playSound()
	if not sound then return end
	pcall(function() sound:Play() end)
end

-- Ouverture ou fermeture d'une porte
local function setDoor(door, isOpen)
	door.Transparency = isOpen and 1 or 0
	door.CanCollide   = not isOpen
end

-- Mise à jour du texte des proximity prompt des boutons
local function setActionText(door)
	-- Pas de fermeture automatique
	for _, button in door:GetChildren() do
		if button:IsA("Part") then
			local proxi = button:FindFirstChildOfClass("ProximityPrompt")
			if proxi then
				proxi.ActionText = door.CanCollide and LABEL_OPEN or LABEL_CLOSE
			end
		end
	end

end

-- Activation ou désactivation des proximity prompts des boutons
local function setEnabledProxi(door, isOpen)
	-- Pas de fermeture automatique
	for _, button in door:GetChildren() do
		if button:IsA("Part") then
			local proxi = button:FindFirstChildOfClass("ProximityPrompt")
			proxi.Enabled = isOpen			
		end
	end	
end

-- Initialisation de chaque porte
-- Le dossier doit comporte un part pour la porte
-- puis un part pour chaque bouton
-- puis proxymity prompt pour chaque bouton
for _, door in doorsFolder:GetChildren() do
	if not door:IsA("Part") then continue end
	door.Anchored     = true
	door.Transparency = 0
	door.CanCollide   = true

	for _, button in door:GetChildren() do
		if not button:IsA("Part") then continue end
		button.Anchored     = true
		button.Transparency = 0
		button.CanCollide   = true		
		local proxi = button:FindFirstChildOfClass("ProximityPrompt")
		if not proxi then continue end
		proxi.ActionText = LABEL_OPEN
		proxi.KeyboardKeyCode=Enum.KeyCode.E
		local delay = button:GetAttribute("TimeoutClose") or TIMEOUT_CLOSE

		proxi.Triggered:Connect(function(player)
			-- Ouverture ou fermeture de la porte
			playSound()
			setDoor(door, door.CanCollide)

			if delay > 0 then
				-- Décompte pour fermeture automatique
				setEnabledProxi(door, false)
				task.delay(delay, function()
					setDoor(door, false)
					setEnabledProxi(door, true)
					playSound()
				end)
			else
				-- Pas de fermeture automatique
				setActionText(door)
			end
		end)
	end
end

Lance ton jeu et regarde l’ouverture de ta porte.

Modifie dans ton script la valeur de cette constante :

local TIMEOUT_CLOSE = 3

Lance ton jeu et regarde le comportement de ta porte.

Duplique les boutons d’ouverture :

Lance ton jeu, et essaye tous les boutons d’ouverture.

Construis un décor autour de ta porte :

Crée une arborescence sous la forme d’un folder sous ta porte pour ranger la structure qui entoure celle-ci :

Tu peux dupliquer ta porte et ainsi avoir tout un système de portes dans ton jeu :

Puis différencier les portes :

Modifie ton code pour prendre en compte un ClickDetector sur un bouton :

Modifie ton script pour tester si le bouton a un promixityPrompt ou un clickDetector :

-- Récupération du joueur
local Players = game:GetService("Players")

-- Configuration
local TIMEOUT_CLOSE = 0
local LABEL_CLOSE   = "Fermer"
local LABEL_OPEN    = "Ouvrir"

-- Récupération du dossier des portes
local doorsFolder = script.Parent
local sound       = doorsFolder:FindFirstChildOfClass("Sound")

-- Lecture du son d'ouverture de la porte
local function playSound()
	if not sound then return end
	pcall(function() sound:Play() end)
end

-- Ouverture ou fermeture d'une porte
local function setDoor(door, isOpen)
	door.Transparency = isOpen and 1 or 0
	door.CanCollide   = not isOpen
end

-- Mise à jour du texte des proximity prompt des boutons
local function setActionText(door)
	-- Pas de fermeture automatique
	for _, button in door:GetChildren() do
		if button:IsA("Part") then
			local proxi = button:FindFirstChildOfClass("ProximityPrompt")
			if proxi then
				proxi.ActionText = door.CanCollide and LABEL_OPEN or LABEL_CLOSE
			end
		end
	end

end

-- Activation ou désactivation des proximity prompts des boutons
local function setEnabledProxi(door, isOpen)
	-- Pas de fermeture automatique
	for _, button in door:GetChildren() do
		if button:IsA("Part") then
			local proxi = button:FindFirstChildOfClass("ProximityPrompt")
			if proxi then 
				proxi.Enabled = isOpen
			end
		end
	end	
end

-- Logique de déclenchement commune
local function connectTrigger(door, button, onTriggered)
	local delay = button:GetAttribute("TimeoutClose") or TIMEOUT_CLOSE

	onTriggered(function()
		-- Ouverture ou fermeture de la porte
		playSound()
		setDoor(door, door.CanCollide)

		if delay > 0 then
			-- Décompte pour fermeture automatique
			setEnabledProxi(door, false)
			task.delay(delay, function()
				setDoor(door, false)
				setEnabledProxi(door, true)
				playSound()
			end)
		else
			-- Pas de fermeture automatique
			setActionText(door)
		end
	end)

end



-- Initialisation de chaque porte
-- Le dossier doit comporte un part pour la porte
-- puis un part pour chaque bouton
-- puis proxymity prompt pour chaque bouton
for _, door in doorsFolder:GetChildren() do
	if not door:IsA("Part") then continue end
	door.Anchored     = true
	door.Transparency = 0
	door.CanCollide   = true

	for _, button in door:GetChildren() do
		if not button:IsA("Part") then continue end
		button.Anchored     = true
		button.Transparency = 0
		button.CanCollide   = true	
		--test si le bouton a un proximity prompt ou un click detector
		local proxi = button:FindFirstChildOfClass("ProximityPrompt")
		local clicker = button:FindFirstChildOfClass("ClickDetector")

		if proxi then
			proxi.ActionText = LABEL_OPEN
			proxi.KeyboardKeyCode=Enum.KeyCode.E
			connectTrigger(door, button, function(callback)
				proxi.Triggered:Connect(function(player)
					callback()
				end)
			end)

		elseif clicker then
			connectTrigger(door, button, function(callback)
				clicker.MouseClick:Connect(function(player)
					callback()
				end)
			end)

		else
			warn("[DoorManager] Aucun ProximityPrompt ni ClickDetector sur :", button:GetFullName())
		end
		
	end
end