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 :