Aller au contenu
Forma Roblox

Rechercher dans la formation

Module 03 · Leçon 5 sur 6

Réagir aux événements

Comprends les événements de Roblox (contacts, joueurs, personnages), le debounce et les attributs, repère les connexions en trop dans du code généré, et mets en place les checkpoints de ton obby.

  • ⏱️ 45 min
  • 📝 Quiz de 7 questions
  • 🔄 Vérifié le 24 octobre 2026
Sur cette page

🎯 Dans cette leçon, tu vas apprendre à…

  • Lire une connexion à un événement et savoir quand elle se déclenche
  • Comprendre l’arrivée des joueurs et l’apparition de leurs personnages
  • Repérer un debounce manquant ou une connexion créée en trop
  • Stocker des informations sur des objets avec les attributs

Pour l’instant, ton code s’exécute au lancement du jeu puis s’arrête, sauf la fonction surContact de la kill brick, qui se déclenche « toute seule ». C’est grâce à un événement. Presque tout ce qui rend un jeu vivant passe par des événements : un joueur arrive, un personnage réapparaît, une Part est touchée, une valeur change… C’est aussi là que le code généré par une IA cache ses bugs les plus sournois : un contact compté dix fois, une connexion créée en trop à chaque saut, un joueur « oublié » au démarrage. Dans cette leçon, tu apprends à lire et à maîtriser les événements, puis tu mets en place les checkpoints de l’Obby des Nuages.

Qu’est-ce qu’un événement ?

Un événement (event) est un signal qu’un objet envoie quand quelque chose se produit. Tu peux y connecter une fonction : Roblox l’appellera automatiquement à chaque fois que le signal est envoyé, en lui passant des informations utiles (les arguments de l’événement).

-- Script placé DANS une Part du Workspace
local part = script.Parent

-- Touched envoie en argument la Part qui a touché
local function surContact(autrePart)
	print(part.Name .. " a été touchée par " .. autrePart.Name)
end

part.Touched:Connect(surContact) -- ATTENTION : pas de () après surContact

On écrit surContact sans parenthèses : on donne la fonction elle-même à Roblox, qui l’appellera plus tard. Avec surContact(), tu l’appellerais tout de suite, une seule fois.

Couper une connexion : Disconnect et Once

:Connect renvoie un objet connexion (RBXScriptConnection). Garde-le dans une variable si tu veux arrêter d’écouter l’événement plus tard.

-- Script placé DANS une Part : réagit seulement aux 3 premiers contacts
local part = script.Parent
local nombreContacts = 0
local connexion

connexion = part.Touched:Connect(function(autrePart)
	nombreContacts += 1
	print("Contact n°" .. nombreContacts .. " avec " .. autrePart.Name)
	if nombreContacts >= 3 then
		connexion:Disconnect() -- la fonction ne sera plus jamais appelée
	end
end)

-- Once : connexion qui se coupe automatiquement après le premier déclenchement
part.Touched:Once(function(autrePart)
	print("Premier contact de la partie :", autrePart.Name)
end)

Bon à savoir : d’après la documentation Roblox, quand un objet est détruit (:Destroy()), les connexions à ses événements sont en général coupées automatiquement. Couper toi-même une connexion devenue inutile reste une bonne pratique.

Attendre un événement : :Wait()

:Wait() met le script en pause jusqu’au prochain déclenchement de l’événement, puis renvoie ses arguments. L’exemple le plus courant récupère le personnage d’un joueur, qu’il soit déjà apparu ou pas encore :

-- Script dans ServerScriptService
local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(player)
	-- Si le personnage existe déjà, on le prend ; sinon on attend qu'il apparaisse
	local personnage = player.Character or player.CharacterAdded:Wait()
	print("Personnage prêt :", personnage.Name)
end)

Tu reconnais l’astuce du or de la leçon 2 : si player.Character vaut nil, on attend le prochain CharacterAdded.

L’événement Touched en détail

Touched est l’événement des obbys : kill bricks, checkpoints, pièces, plateformes piégées… Ce qu’en dit la documentation Roblox :

  • il se déclenche à cause d’un mouvement physique : déplacer une Part en changeant son CFrame pour qu’elle chevauche une autre ne le déclenche pas ;
  • au moins une des deux Parts ne doit pas être ancrée (Anchored). Ça tombe bien : le personnage du joueur ne l’est pas ;
  • la propriété CanTouch doit être activée (elle l’est par défaut) ;
  • il peut se déclencher plusieurs fois de suite pour un seul contact, à cause de petites collisions successives (et chaque membre du personnage qui touche la Part compte).

Pour savoir quel joueur a touché, on utilise Players:GetPlayerFromCharacter(autrePart.Parent), qui renvoie le joueur à qui appartient ce personnage, ou nil si ce n’est pas un personnage de joueur.

Les joueurs et leurs personnages

Deux niveaux à bien distinguer :

  • le joueur (Player), qui existe de son arrivée à son départ, dans le service Players ;
  • son personnage (Character), le Model 3D dans le Workspace, détruit et recréé à chaque réapparition.
Événement Quand il se déclenche
Players.PlayerAdded Une fois, quand un joueur rejoint la partie
Players.PlayerRemoving Une fois, quand un joueur quitte la partie
player.CharacterAdded À chaque apparition et réapparition du personnage
player.CharacterRemoving Juste avant que le personnage soit retiré (souvent après sa mort)
-- Script dans ServerScriptService
local Players = game:GetService("Players")

local function surPersonnageAjoute(personnage)
	print(personnage.Name .. " vient d'apparaître")
end

local function surJoueurAjoute(player)
	print(player.Name .. " a rejoint l'Obby des Nuages")
	player.CharacterAdded:Connect(surPersonnageAjoute)
end

Players.PlayerAdded:Connect(surJoueurAjoute)

-- Les joueurs déjà présents avant la connexion ne déclenchent pas PlayerAdded :
-- on les traite à la main pour ne rater personne
for _, player in Players:GetPlayers() do
	-- task.spawn appelle surJoueurAjoute(player) sans bloquer la boucle (détaillé plus bas)
	task.spawn(surJoueurAjoute, player)
end

WaitForChild ou FindFirstChild ?

  • FindFirstChild("Nom") cherche un enfant tout de suite et renvoie nil s’il n’existe pas. À utiliser quand l’objet peut légitimement être absent, avec un test if objet then.
  • WaitForChild("Nom") attend que l’enfant existe, puis le renvoie. À utiliser quand l’objet doit exister mais n’est peut-être pas encore chargé (très fréquent côté client, voir module 4).

Si l’objet n’arrive jamais, WaitForChild bloque le script ; après 5 secondes, l’Output affiche un avertissement « Infinite yield possible on… ». Tu peux donner un délai maximal : WaitForChild("Nom", 10) renvoie nil après 10 secondes.

task.spawn et task.delay

Certaines fonctions mettent le script en pause (task.wait, :Wait(), WaitForChild…). Pour lancer du code sans bloquer la suite, la bibliothèque task propose :

  • task.spawn(fonction, arguments...) exécute la fonction immédiatement, mais dans un fil d’exécution séparé : si elle se met en pause, le reste du script continue ;
  • task.delay(secondes, fonction, arguments...) exécute la fonction plus tard, après le délai, sans bloquer le script.
-- Script dans ServerScriptService
task.delay(5, function()
	print("5 secondes après le lancement")
end)

task.spawn(function()
	while true do
		task.wait(60)
		print("Une minute de plus !")
	end
end)

print("Ce message s'affiche en premier : rien ne bloque")

Rappel : spawn et delay (sans task.) sont dépréciées, comme wait.

Le debounce : éviter les déclenchements en rafale

Comme Touched se déclenche plusieurs fois par contact, une lave qui retire 20 PV (points de vie) par contact en retirerait 60 ou 80 d’un coup. La solution s’appelle un debounce : un verrou qui ignore les nouveaux déclenchements pendant un court délai.

Le debounce le plus simple est une variable true/false, mais il bloque tous les joueurs pendant le délai. Dans un jeu multijoueur, on préfère un verrou par joueur, rangé dans une table :

-- Script placé DANS une Part « Lave » (retire 20 PV, au plus une fois par seconde et par joueur)
local Players = game:GetService("Players")

local lave = script.Parent
local DEGATS = 20
local DELAI = 1
local joueursEnAttente = {} -- clé = joueur, valeur = true pendant le délai

lave.Touched:Connect(function(autrePart)
	local personnage = autrePart.Parent
	local player = Players:GetPlayerFromCharacter(personnage)
	if not player or joueursEnAttente[player] then
		return -- pas un joueur, ou joueur déjà « verrouillé » : on ignore
	end

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

	joueursEnAttente[player] = true -- on verrouille AVANT d'agir
	humanoid:TakeDamage(DEGATS)

	task.delay(DELAI, function()
		joueursEnAttente[player] = nil -- on déverrouille après le délai
	end)
end)

Les attributs

Les attributs sont des propriétés personnalisées que tu ajoutes toi-même à n’importe quel objet : un nombre, un texte, un booléen, une couleur, un Vector3… Tu peux les créer dans Studio (en bas de la fenêtre Properties, section Attributes) ou par script :

-- Script dans ServerScriptService
local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(player)
	-- SetAttribute crée l'attribut s'il n'existe pas, ou le modifie
	player:SetAttribute("Etape", 0)

	-- GetAttributeChangedSignal : un événement déclenché quand CET attribut change
	player:GetAttributeChangedSignal("Etape"):Connect(function()
		-- la fonction ne reçoit rien : on relit la valeur avec GetAttribute
		print(player.Name .. " est maintenant à l'étape " .. player:GetAttribute("Etape"))
	end)
end)

À savoir :

  • GetAttribute("Nom") renvoie nil si l’attribut n’existe pas ; SetAttribute("Nom", nil) le supprime ;
  • les noms d’attributs n’utilisent que des lettres, des chiffres et _ - . /, sans espaces, 100 caractères maximum, et ne commencent pas par RBX ;
  • les changements faits par le serveur sont répliqués (transmis) aux clients : un LocalScript peut donc lire un attribut du joueur pour l’afficher (au module 5, l’interface de la boutique s’en servira pour savoir quelles traînées le joueur possède) ;
  • pendant un test, sélectionne l’objet dans l’Explorer : ses attributs s’affichent en direct dans Properties, idéal pour déboguer.

Fil rouge : les checkpoints de l’Obby des Nuages

Objectif : quand un joueur touche le checkpoint suivant, on retient son étape ; quand il meurt (kill brick !), il réapparaît sur son dernier checkpoint.

Préparer la scène

  1. Dans le Workspace, crée un Folder nommé Checkpoints.
  2. Place des Parts plates et bien visibles sur ton parcours, ancrées, et nomme-les 1, 2, 3… dans l’ordre du parcours (le nom est le numéro d’étape).
  3. Range-les dans Checkpoints.

Le code

Ce script est plus long que les précédents : c’est typiquement le genre de code que tu feras écrire par Claude Code (prompt dans l’encadré plus bas), dans le fichier src/ServerScriptService/Checkpoints.server.luau de ton projet Rojo. Ton rôle est de le lire : repère chaque événement connecté, et demande-toi à chaque fois « quand se déclenche-t-il, et combien de fois ? ».

-- Script « Checkpoints » dans ServerScriptService
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")

local dossierCheckpoints = Workspace:WaitForChild("Checkpoints")
local HAUTEUR_REAPPARITION = Vector3.new(0, 4, 0) -- pour apparaître AU-DESSUS du checkpoint

-- Quand un joueur touche un checkpoint
local function surCheckpointTouche(checkpoint, autrePart)
	local player = Players:GetPlayerFromCharacter(autrePart.Parent)
	if not player then
		return -- ce n'est pas un joueur
	end

	local numero = tonumber(checkpoint.Name) -- "3" devient 3 (nil si le nom n'est pas un nombre)
	local etapeActuelle = player:GetAttribute("Etape") or 0

	-- Un checkpoint n'est accepté que s'il est le SUIVANT (étape actuelle + 1) :
	-- impossible de sauter des étapes ou de revenir en arrière.
	-- Cette condition sert aussi de debounce naturel : retoucher le même checkpoint ne fait rien.
	if numero and numero == etapeActuelle + 1 then
		player:SetAttribute("Etape", numero)
		print(`{player.Name} a atteint l'étape {numero}`)
	end
end

-- Quand le personnage (ré)apparaît : on le replace sur son checkpoint
local function placerAuCheckpoint(player, personnage)
	local etape = player:GetAttribute("Etape") or 0
	local checkpoint = dossierCheckpoints:FindFirstChild(tostring(etape))
	if not (checkpoint and checkpoint:IsA("BasePart")) then
		return -- étape 0 ou checkpoint introuvable : la SpawnLocation fait son travail
	end

	-- CharacterAdded se déclenche AVANT que le personnage soit placé dans le Workspace :
	-- on attend qu'il y soit, puis une image de plus pour laisser Roblox finir de le positionner
	if not personnage:IsDescendantOf(Workspace) then
		personnage.AncestryChanged:Wait()
	end
	task.wait()
	if not personnage:IsDescendantOf(Workspace) then
		return -- le personnage a déjà disparu pendant l'attente (joueur parti, nouvelle réapparition...)
	end

	personnage:PivotTo(checkpoint.CFrame + HAUTEUR_REAPPARITION)
end

local function surJoueurAjoute(player)
	player:SetAttribute("Etape", 0)
	player.CharacterAdded:Connect(function(personnage)
		placerAuCheckpoint(player, personnage)
	end)
end

-- 1) Connecter chaque checkpoint
for _, checkpoint in dossierCheckpoints:GetChildren() do
	if checkpoint:IsA("BasePart") then
		checkpoint.Touched:Connect(function(autrePart)
			surCheckpointTouche(checkpoint, autrePart)
		end)
	end
end

-- 2) Gérer les joueurs (y compris ceux déjà présents)
Players.PlayerAdded:Connect(surJoueurAjoute)
for _, player in Players:GetPlayers() do
	task.spawn(surJoueurAjoute, player)
end

Ce qu’il faut comprendre :

  • l’étape de chaque joueur est rangée dans un attribut Etape sur son objet Player : c’est le numéro du dernier checkpoint validé, 0 au départ ;
  • un checkpoint N n’est accepté que si N == etape + 1 : le joueur doit les valider dans l’ordre ;
  • tonumber(checkpoint.Name) transforme le nom de la Part en nombre : c’est pour ça que les checkpoints s’appellent 1, 2, 3… ;
  • on utilise une fonction anonyme dans Touched:Connect pour transmettre aussi le checkpoint concerné à surCheckpointTouche ;
  • PivotTo déplace tout le Model du personnage d’un coup, vers une position et une orientation (un CFrame) ; on ajoute 4 studs de hauteur pour ne pas apparaître dans la Part.

Lire ce code comme un relecteur

Exercice 1 : dans quel ordre l’Output affiche-t-il les messages ?

-- Script dans ServerScriptService
print("A")

task.delay(1, function()
	print("B")
end)

task.spawn(function()
	print("C")
	task.wait(2)
	print("D")
end)

print("E")

Exercice 2 : que se passe-t-il si… ?

Une IA a proposé ce début de script. Ton dossier s’appelle Checkpoints (avec un « s »).

-- Script dans ServerScriptService (proposé par une IA)
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")

local dossierCheckpoints = Workspace:WaitForChild("Checkpoint")

Players.PlayerAdded:Connect(function(player)
	player:SetAttribute("Etape", 0)
	print(player.Name .. " commence à l'étape 0")
end)

print(#dossierCheckpoints:GetChildren(), "checkpoints")
  1. Que se passe-t-il au lancement ?
  2. Une fois le nom corrigé, il arrive en Play Solo que le message de la ligne 9 ne s’affiche jamais. Pourquoi ?

Ce qu’il faut retenir

  • evenement:Connect(fonction) appelle la fonction à chaque déclenchement et renvoie une connexion (:Disconnect()) ; :Once ne réagit qu’une fois ; :Wait() met en pause jusqu’au déclenchement.
  • Touched se déclenche plusieurs fois par contact : protège-le avec un debounce, idéalement par joueur.
  • PlayerAdded = arrivée du joueur (une fois) ; CharacterAdded = chaque apparition du personnage. Traite aussi les joueurs déjà présents.
  • WaitForChild attend un objet, FindFirstChild le cherche tout de suite (et peut renvoyer nil).
  • task.spawn et task.delay lancent du code sans bloquer ; les attributs stockent tes propres données sur les objets.
  • En relisant, demande-toi pour chaque :Connect : quand se déclenche-t-il, combien de fois, et la connexion n’est-elle créée qu’une fois ?

Sources et pour aller plus loin

Quiz : vérifie tes acquis

7 questions · réussite à partir de 70 % · valide chaque réponse pour voir l’explication

  1. Question 1QCMQue renvoie part.Touched:Connect(maFonction) ?
  2. Question 2Vrai ou fauxL’événement Touched peut se déclencher plusieurs fois de suite pour un seul saut d’un joueur sur une Part.
  3. Question 3QCMQuel événement utiliser pour exécuter du code à chaque fois qu’un joueur réapparaît après être mort ?
  4. Question 4QCMQue se passe-t-il si dossier:WaitForChild("Checkpoints") attend un objet qui n’arrive jamais (sans délai maximal) ?
  5. Question 5Trouve le bugCe Script de lave doit retirer 20 PV (points de vie) au joueur, au plus une fois par seconde. En pratique, le joueur perd souvent plus de 20 PV d’un coup. Où est le bug ?
    local lave = script.Parent
    local enAttente = false
    
    lave.Touched:Connect(function(autrePart)
    	local humanoid = autrePart.Parent:FindFirstChildOfClass("Humanoid")
    	if humanoid and not enAttente then
    		humanoid:TakeDamage(20)
    		task.wait(1)
    		enAttente = false
    	end
    end)
  6. Question 6Trouve le bugTrouve la faille dans ce code généré par IA. Chaque joueur reçoit l’attribut Etape à 0 en arrivant (dans un autre script). Après quelques sauts sur le checkpoint, chaque changement d’étape affiche des dizaines de fois le même message. Où est le problème ?
    local Players = game:GetService("Players")
    local Workspace = game:GetService("Workspace")
    local checkpoint = Workspace:WaitForChild("Checkpoints"):WaitForChild("1")
    
    checkpoint.Touched:Connect(function(autrePart)
    	local player = Players:GetPlayerFromCharacter(autrePart.Parent)
    	if not player then return end
    	player:GetAttributeChangedSignal("Etape"):Connect(function()
    		print(player.Name .. " passe à l'étape " .. player:GetAttribute("Etape"))
    	end)
    	if player:GetAttribute("Etape") == 0 then
    		player:SetAttribute("Etape", 1)
    	end
    end)
  7. Question 7Améliore ce promptTu veux que Claude Code crée les checkpoints de la leçon. Quelle version du prompt donnera le meilleur résultat ?
    Le prompt de départ
    fais des checkpoints pour mon obby

    Choisis la meilleure version de ce prompt.