Catégories
Jeu vidéo ROBLOX

Points de respawns, comptage des tours, meilleur temps

À 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

  1. Créez un dossier (Folder) nommé PartSpawnNiv dans le Workspace.
  2. Créez un dossier GAME dans le Workspace qui contiendra ce dossier PartSpawnNiv.
  3. Dans PartSpawnNiv, créez une Part pour chaque checkpoint de votre parcours (une au départ, une à chaque étape importante, une à l’arrivée).
  4. 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 Frame nommé Counter avec un TextLabel nommé CounterLabel (nombre de tours)
  • Un Frame nommé Best avec un TextLabel nommé BestLabel (meilleur temps)

Étape 3 : Placer le script

Placez ce script en tant que Script (serveur, pas LocalScript) directement dans ServerScriptService.

Comment fonctionne le script (en 6 points)

  1. Au démarrage, le script range tous les checkpoints du dossier PartSpawnNiv dans l’ordre de leurs numéros, puis les rend invisibles et traversables.
  2. 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.
  3. 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.
  4. Si le joueur meurt, l’événement CharacterAdded se déclenche : le script téléporte automatiquement le joueur au-dessus du dernier checkpoint touché (et pas au tout début du parcours !).
  5. Le tableau playersProgression retient les informations de chaque joueur séparément, grâce à son UserId (un peu comme une fiche personnelle par joueur).
  6. 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
spawnLe dernier checkpoint touché par ce joueur — utilisé pour savoir où faire respawn s’il meurt
courseLe nombre de tours complets déjà réalisés par ce joueur
startL’heure exacte (tick()) à laquelle le joueur a commencé son tour actuel — sert à calculer son temps une fois arrivé
bestLe 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