À quoi sert ce script ?
Ce script permet de créer un parcours de course :
- Des checkpoints invisibles qui font respawn le joueur au bon endroit s’il tombe ou meurt
- Un compteur de tours : combien de fois le joueur a terminé le parcours
- Un meilleur temps (record) qui s’affiche et se met à jour automatiquement
Comment l’utiliser dans votre jeu
Étape 1 : Créer les parts de checkpoint
- Créez un dossier (Folder) nommé
PartSpawnNivdans le Workspace. - Créez un dossier
GAMEdans le Workspace qui contiendra ce dossierPartSpawnNiv. - Dans
PartSpawnNiv, créez une Part pour chaque checkpoint de votre parcours (une au départ, une à chaque étape importante, une à l’arrivée). - Nommez-les avec des numéros dans l’ordre :
"1","2","3", etc. ; c’est très important, le script trie les checkpoints selon ces numéros !
Workspace
└── GAME
└── PartSpawnNiv
├── 1 (départ)
├── 2
├── 3
└── 4 (arrivée)

Le script rend ces parts automatiquement invisibles et traversables : pas besoin de le faire vous-même !
Étape 2 : Créer l’interface (GUI)

Dans StarterGui, créez un ScreenGui contenant :
- Un
FramenomméCounteravec unTextLabelnomméCounterLabel(nombre de tours) - Un
FramenomméBestavec unTextLabelnomméBestLabel(meilleur temps)
Étape 3 : Placer le script
Placez ce script en tant que Script (serveur, pas LocalScript) directement dans ServerScriptService.
Comment fonctionne le script (en 6 points)
- Au démarrage, le script range tous les checkpoints du dossier
PartSpawnNivdans l’ordre de leurs numéros, puis les rend invisibles et traversables. - Quand un joueur touche un checkpoint, le script vérifie qui il est, puis retient dans un tableau
playersProgression: la part actuelle, le nombre de tours, l’heure de départ, et le meilleur temps. - Si le joueur touche le checkpoint n°1 juste après avoir touché le DERNIER checkpoint, le script comprend qu’il a terminé un tour complet : il incrémente le compteur et compare le temps réalisé au record.
- Si le joueur meurt, l’événement
CharacterAddedse déclenche : le script téléporte automatiquement le joueur au-dessus du dernier checkpoint touché (et pas au tout début du parcours !). - Le tableau
playersProgressionretient les informations de chaque joueur séparément, grâce à sonUserId(un peu comme une fiche personnelle par joueur). - Quand un joueur quitte le jeu, ses informations sont automatiquement supprimées du tableau pour ne pas garder des données inutiles en mémoire.
Pourquoi utiliser un tableau, et que contient playersProgression ?
Pourquoi a-t-on besoin d’un tableau ici ?
Imagine que plusieurs joueurs jouent en même temps sur ton parcours. Le script doit se souvenir, pour chaque joueur individuellement :
- à quel checkpoint il en est
- depuis quand il a commencé son tour actuel
- combien de tours il a déjà faits
- son meilleur temps
Si le script utilisait une seule variable simple (par exemple local spawnActuel = ...), cette variable serait partagée par tous les joueurs — dès qu’un 2ème joueur touche un checkpoint, ça écraserait la progression du 1er joueur ! Ce serait comme si tout le monde partageait le même carnet de notes : la dernière personne qui écrit efface ce que l’autre avait noté.
Un tableau permet de créer une « fiche » séparée pour chaque joueur, un peu comme un casier avec un tiroir par élève dans une salle de classe.
Qu’est-ce qu’un tableau d’informations (une table Lua) ?
En Lua, une table peut fonctionner comme un dictionnaire : au lieu d’accéder à une information par un numéro de position (comme dans une liste), on y accède par une clé (un identifiant unique), et chaque clé pointe vers une valeur.
playersProgression = {
[12345] = { ... }, -- clé = UserId du joueur 1
[67890] = { ... }, -- clé = UserId du joueur 2
}
C’est comme un répertoire téléphonique : la clé, c’est le nom de la personne ; la valeur, c’est son numéro. Ici, la clé est le UserId (identifiant unique de chaque joueur sur Roblox), et la valeur est une autre table qui contient toutes les infos de progression de ce joueur précis.
Pourquoi utiliser le UserId comme clé ?
Parce que chaque joueur a un UserId unique et permanent sur Roblox — contrairement à son pseudo qui pourrait théoriquement changer, ou à sa position dans une liste qui peut varier si un joueur quitte. Utiliser le UserId garantit qu’on retrouve toujours la bonne fiche pour le bon joueur, même avec plusieurs joueurs connectés simultanément.
Le contenu détaillé de playersProgression[UserId]
Pour un joueur donné, la table stockée ressemble à ceci :
playersProgression[12345] = {
spawn = <référence à la Part checkpoint>,
course = 2, -- nombre de tours réalisés
start = 1735689042.5, -- temps de démarrage du tour
best = 45.32, -- temps du meilleur parcours
}
| Clé | Signification |
|---|---|
spawn | Le dernier checkpoint touché par ce joueur — utilisé pour savoir où faire respawn s’il meurt |
course | Le nombre de tours complets déjà réalisés par ce joueur |
start | L’heure exacte (tick()) à laquelle le joueur a commencé son tour actuel — sert à calculer son temps une fois arrivé |
best | Le meilleur temps (le record personnel) réalisé par ce joueur sur un tour complet |
QCM pour vérifier la compréhension
1. Pourquoi faut-il nommer les checkpoints « 1 », « 2 », « 3 »… et pas « Depart », « Milieu », « Fin » ?
A. Pour que ce soit plus joli dans l’Explorer
B. Parce que le script trie les checkpoints selon ces numéros pour connaître leur ordre
C. Ça n’a aucune importance, on peut les nommer comme on veut
D. Parce que Roblox l’exige pour toutes les Parts
2. Que se passe-t-il quand un joueur meurt en plein milieu du parcours ?
A. Il recommence tout le parcours depuis le début
B. Il respawn au dernier checkpoint qu’il a touché
C. Le jeu se ferme
D. Son compteur de tours est remis à zéro
3. Comment le script sait-il qu’un joueur a terminé un tour complet ?
A. Quand il touche n’importe quel checkpoint
B. Quand il touche le checkpoint n°1 juste après avoir touché le tout dernier checkpoint
C. Quand il appuie sur une touche spéciale
D. Automatiquement après 60 secondes
4. Pourquoi les checkpoints sont-ils rendus invisibles et traversables (CanCollide = false) ?
A. Pour que le joueur ne les voie pas et ne soit pas bloqué en les traversant
B. Pour que le jeu soit plus rapide
C. Pour économiser de la mémoire
D. Ce n’est pas obligatoire, c’est juste un choix esthétique
5. À quoi sert le UserId du joueur dans le tableau playersProgression ?
A. À afficher le nom du joueur à l’écran
B. À retenir séparément la progression de CHAQUE joueur individuellement
C. À bannir les joueurs trichent
D. À compter le nombre total de joueurs connectés
6. Que se passe-t-il si deux checkpoints ont accidentellement le même numéro (ex: deux parts nommées « 3 ») ?
A. Rien, le script gère ça automatiquement
B. Le parcours pourrait ne pas fonctionner correctement, car l’ordre ne serait plus fiable
C. Le jeu affiche une erreur et s’arrête
D. Roblox choisit au hasard laquelle utiliser
7. Pourquoi utilise-t-on un Script (serveur) et pas un LocalScript pour ce système ?
A. Parce que le compteur de tours et le meilleur temps doivent être valables pour tout le monde, gérés par un seul endroit fiable (le serveur)
B. Parce que les LocalScripts ne fonctionnent pas avec les Parts
C. Ça n’a pas d’importance, les deux marcheraient pareil
D. Parce que c’est plus rapide à écrire
-- ═══════════════════════════════════════
-- SERVICES ET RÉFÉRENCES
-- ═══════════════════════════════════════
local Players = game:GetService("Players")
local Game = workspace.GAME
local SpawnFolder = Game:WaitForChild("PartSpawnNiv", true)
local baseplate = workspace:FindFirstChild("Baseplate")
if baseplate then
baseplate:Destroy()
end
-- Tableau des joueurs en cours de partie avec l'étape mémorisée
local playersProgression = {}
-- Tableau de toutes les étapes du jeu, triées par nom pour garantir l'ordre
local spawnsList = SpawnFolder:GetChildren()
table.sort(spawnsList, function(a, b)
return tonumber(a.Name) < tonumber(b.Name)
end)
local lastSpawnIndex = #spawnsList
local function formatTime(t)
local minutes = math.floor(t / 60)
local seconds = math.floor(t % 60)
local centiemes = math.floor((t * 100) % 100)
return string.format("%02d:%02d.%02d", minutes, seconds, centiemes)
end
-- ═══════════════════════════════════════
-- RESPAWN AU DERNIER CHECKPOINT
-- ═══════════════════════════════════════
local function respawnAuCheckpoint(player)
local progression = playersProgression[player.UserId]
if not progression or not progression.spawn then return end
local character = player.Character
if not character then return end
local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
if not humanoidRootPart then return end
local spawn = progression.spawn
local targetPosition = spawn.Position + Vector3.new(0, (spawn.Size.Y / 2) + 2, 0)
humanoidRootPart.CFrame = CFrame.new(targetPosition)
end
-- ═══════════════════════════════════════
-- GESTION DU CHARGEMENT DES JOUEURS
-- ═══════════════════════════════════════
local function initPlayer(player)
player.CharacterAdded:Connect(function(character)
character:WaitForChild("HumanoidRootPart")
respawnAuCheckpoint(player)
end)
end
Players.PlayerAdded:Connect(initPlayer)
Players.PlayerRemoving:Connect(function(player)
playersProgression[player.UserId] = nil
end)
-- ═══════════════════════════════════════
-- CHARGEMENT DES ÉTAPES ET DÉTECTION DES CHECKPOINTS
-- ═══════════════════════════════════════
for index, spawn in ipairs(spawnsList) do
if spawn:IsA("BasePart") then
spawn.Transparency = 1
spawn.CanCollide = false
spawn.Anchored = true
spawn.Touched:Connect(function(hit)
local player = Players:GetPlayerFromCharacter(hit.Parent)
if not player then return end
local progression = playersProgression[player.UserId]
-- Ignore si le joueur est déjà sur ce checkpoint
if progression and progression.spawn == spawn then return end
if not progression then
playersProgression[player.UserId] = {
spawn = spawn,
course = 0,
start = tick(),
best = 0,
counterLabel = player.PlayerGui.ScreenGui.Counter.CounterLabel,
bestLabel = player.PlayerGui.ScreenGui.Best.BestLabel,
}
return
end
-- Tour complet détecté : dernier checkpoint -> premier checkpoint
if index == 1 and progression.spawn == spawnsList[lastSpawnIndex] then
progression.course += 1
progression.counterLabel.Text = string.format("%02d", progression.course)
local finishTime = tick() - progression.start
if progression.best == 0 or finishTime < progression.best then
progression.best = finishTime
end
progression.bestLabel.Text = formatTime(progression.best)
end
progression.spawn = spawn
progression.start = tick()
end)
end
end
