Aller au contenu
Forma Roblox

Rechercher dans la formation

Module 04 · Leçon 3 sur 5

RemoteFunctions et BindableEvents

Pose une question au serveur et attends sa réponse avec une RemoteFunction, évite les pièges d’InvokeClient et fais dialoguer des scripts d’un même côté.

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

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

  • Faire une requête avec réponse grâce à InvokeServer et OnServerInvoke
  • Connaître les risques d’InvokeClient et les éviter
  • Faire communiquer des scripts d’un même côté avec BindableEvent et BindableFunction
  • Choisir le bon outil de communication selon la situation

Avec un RemoteEvent, on envoie un message et on continue sa vie sans attendre de réponse. Mais parfois, le client a besoin d’une réponse : « quelles traînées est-ce que je possède ? ». C’est le travail de la RemoteFunction. Dans cette leçon, tu vas aussi découvrir pourquoi l’inverse (le serveur qui questionne un client) est risqué, et comment des scripts du même côté peuvent se parler avec les Bindables.

La RemoteFunction : une question, une réponse

Une RemoteFunction se range au même endroit que les RemoteEvents, dans ReplicatedStorage.Remotes. Elle fonctionne comme un appel téléphonique :

  • le client appelle InvokeServer(...) et attend (son script est mis en pause) ;
  • le serveur exécute la fonction qu’il a assignée à OnServerInvoke (ce genre de fonction s’appelle un callback, ou « fonction de rappel ») ;
  • ce que cette fonction renvoie (avec return) revient au client, et son script reprend.

Comme pour OnServerEvent, le serveur reçoit le joueur en premier paramètre, puis les arguments du client. Et les mêmes limites s’appliquent aux arguments et aux valeurs renvoyées : tables copiées, fonctions transformées en nil, tables mixtes à éviter, etc.

Exemple : demander son inventaire

Dans l’Obby des Nuages, l’interface de la boutique doit savoir quelles traînées le joueur possède déjà. Crée une RemoteFunction nommée ObtenirInventaire dans ReplicatedStorage.Remotes.

-- Script placé dans ServerScriptService (côté serveur)
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local remotes = ReplicatedStorage:WaitForChild("Remotes")
local obtenirInventaire = remotes:WaitForChild("ObtenirInventaire") :: RemoteFunction

-- L'inventaire vit sur le serveur (source de vérité). Au module 5, il sera sauvegardé.
local inventaires: { [Player]: { string } } = {}

Players.PlayerAdded:Connect(function(player: Player)
	inventaires[player] = {} -- nouvelle partie : aucun objet
end)

Players.PlayerRemoving:Connect(function(player: Player)
	inventaires[player] = nil
end)

-- Callback : on lui AFFECTE une fonction avec =, on ne le « connecte » pas
obtenirInventaire.OnServerInvoke = function(player: Player)
	local inventaire = inventaires[player] or {}
	-- On renvoie une liste pure de textes : elle arrive au client sous forme de copie
	return table.clone(inventaire)
end
-- LocalScript placé dans StarterPlayer > StarterPlayerScripts (côté client)
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local remotes = ReplicatedStorage:WaitForChild("Remotes")
local obtenirInventaire = remotes:WaitForChild("ObtenirInventaire") :: RemoteFunction

-- pcall : si l'appel échoue, notre script ne s'arrête pas net
local ok, resultat = pcall(function()
	return obtenirInventaire:InvokeServer()
end)

if ok and typeof(resultat) == "table" then
	print("Je possède", #resultat, "objet(s) :", table.concat(resultat, ", "))
else
	warn("Impossible de récupérer l'inventaire :", resultat)
end

Remarque le typeof(resultat) == "table" côté client : on vérifie ce qu’on reçoit avant de l’utiliser. Bonne habitude, même si c’est le serveur qui répond. Le pcall protège contre une erreur levée pendant l’appel. Il ne fixe pas de limite de temps : si le serveur ne répond jamais, le script attend.

Trois règles pour des RemoteFunctions saines

  1. Un seul callback. OnServerInvoke n’accepte qu’une fonction à la fois : si tu en affectes plusieurs, seule la dernière s’exécute.
  2. Répondre vite. Le client attend pendant tout le temps que met ton callback. Évite les longues attentes (task.wait) dans OnServerInvoke.
  3. Pas de réponse utile ? Pas de RemoteFunction. La documentation recommande un RemoteEvent quand on n’a pas besoin du résultat : c’est asynchrone et personne n’attend.

InvokeClient : la fausse bonne idée

La RemoteFunction marche aussi dans l’autre sens : le serveur appelle InvokeClient(player, ...) et le client répond via OnClientInvoke. La documentation déconseille fortement cette direction et liste trois risques :

  • si le client lève une erreur (son code plante), le serveur lève aussi l’erreur ;
  • si le client se déconnecte pendant l’appel, InvokeClient lève une erreur ;
  • si le client ne répond jamais (son callback ne se termine pas, ou il n’en a défini aucun), le serveur attend pour toujours.

Or un tricheur contrôle entièrement le code de son client. Il peut donc bloquer volontairement ton script serveur. De plus, il n’existe pas de paramètre de délai maximum (timeout) dans InvokeClient : rien ne vient débloquer l’attente à ta place.

La bonne alternative : deux RemoteEvents

Dans la grande majorité des cas, le serveur n’a pas besoin d’attendre. La documentation fait remarquer que le client a rarement une information que le serveur n’a pas. Si le serveur veut quand même une réponse, utilise deux messages à sens unique :

  1. le serveur envoie une question avec FireClient (sans attendre) ;
  2. le client répond plus tard avec FireServer ;
  3. le serveur traite la réponse s’il la reçoit, et la valide comme n’importe quel message client.

Si tu dois vraiment utiliser InvokeClient

Isole l’appel dans un fil d’exécution séparé (task.spawn), protège-le avec pcall, et fixe ta propre limite de temps :

-- Extrait d'un Script serveur (ServerScriptService)
-- Pose une question à un client sans risquer de bloquer le script appelant pour toujours
local function invoquerAvecDelai(remote: RemoteFunction, player: Player, delaiMax: number, question: string): (boolean, any)
	local termine = false
	local reussi = false
	local reponse: any = nil

	-- L'appel risqué tourne dans un fil séparé, protégé par pcall
	task.spawn(function()
		local ok, valeur = pcall(function()
			return remote:InvokeClient(player, question)
		end)
		reussi = ok
		reponse = valeur
		termine = true
	end)

	-- On attend au maximum delaiMax secondes
	local debut = os.clock()
	while not termine and os.clock() - debut < delaiMax do
		task.wait(0.1)
	end

	if not termine then
		return false, "Délai dépassé"
	end
	return reussi, reponse -- la réponse vient du client : à valider avant usage !
end

Même ainsi, si le client ne répond jamais, le fil séparé reste en attente indéfiniment. C’est une raison de plus de préférer la solution avec deux RemoteEvents.

Les Bindables : parler à un script du même côté

Parfois, deux scripts du même côté doivent communiquer : par exemple, le script des checkpoints veut prévenir un autre script serveur (récompenses, statistiques…) qu’un joueur a atteint une étape. Les RemoteEvents ne servent pas à ça. On utilise les Bindables :

Objet Rôle Envoyer Recevoir
BindableEvent Message à sens unique Fire(...) .Event:Connect(...)
BindableFunction Question avec réponse Invoke(...) OnInvoke = function

La documentation recommande de les placer dans ServerScriptService pour la communication entre scripts serveur, et dans ReplicatedStorage pour la communication entre scripts client.

Exemple : l’événement « étape atteinte »

Crée un BindableEvent nommé EtapeAtteinte dans ServerScriptService.

-- Extrait du Script des checkpoints (ServerScriptService) : on annonce l'étape aux autres scripts serveur
local ServerScriptService = game:GetService("ServerScriptService")

local etapeAtteinte = ServerScriptService:WaitForChild("EtapeAtteinte") :: BindableEvent

-- À appeler quand une étape est validée (par exemple dans le script Progression
-- de la leçon 5, juste après etape.Value = numero)
local function quandEtapeValidee(player: Player, numeroEtape: number)
	-- Fire ne met pas le script en pause : les scripts abonnés réagissent de leur côté
	etapeAtteinte:Fire(player, numeroEtape)
end
-- Script « Recompenses » placé dans ServerScriptService (côté serveur)
local ServerScriptService = game:GetService("ServerScriptService")

local etapeAtteinte = ServerScriptService:WaitForChild("EtapeAtteinte") :: BindableEvent

etapeAtteinte.Event:Connect(function(player: Player, numeroEtape: number)
	print(player.Name .. " a atteint l'étape " .. numeroEtape)
	-- Ici : bonus de pièces toutes les 5 étapes, statistiques (module 9)…
end)

L’avantage : le script des checkpoints n’a pas besoin de connaître tous les scripts qui s’intéressent aux étapes. Chacun s’abonne de son côté.

Ce qu’il faut savoir sur les Bindables

  • Les arguments subissent les mêmes limites que les remotes : clés non textuelles converties en textes, tables mixtes à éviter, métatables perdues, tables copiées.
  • Si plusieurs fonctions sont connectées au même BindableEvent, leur ordre d’exécution n’est pas garanti.
  • BindableFunction:Invoke() attend la réponse. Si aucun OnInvoke n’a jamais été défini, le script qui appelle ne reprend jamais. Et comme pour les RemoteFunctions, seul le dernier OnInvoke affecté compte.
  • La documentation rappelle que les ModuleScripts sont souvent une meilleure solution pour partager des données ou des fonctions entre scripts. Garde les Bindables pour les vrais « événements ».

Quel outil choisir ?

Besoin Outil
Le client demande une action au serveur RemoteEvent (FireServer)
Le serveur prévient un joueur ou tout le monde RemoteEvent (FireClient / FireAllClients)
Effet visuel éphémère, donnée continue UnreliableRemoteEvent
Le client a besoin d’une réponse du serveur RemoteFunction (InvokeServer)
Le serveur a besoin d’une réponse du client Deux RemoteEvent (éviter InvokeClient)
Deux scripts du même côté BindableEvent, BindableFunction ou un ModuleScript

Ce qu’il faut retenir

  • Une RemoteFunction fait une requête avec réponse : InvokeServer côté client, OnServerInvoke = function(player, ...) côté serveur.
  • L’appelant attend la réponse ; le callback serveur doit être rapide et valider ses arguments.
  • OnServerInvoke est un callback : on l’affecte avec =, un seul à la fois.
  • Évite InvokeClient : erreur relayée, déconnexion, attente infinie. Préfère FireClient puis FireServer.
  • BindableEvent / BindableFunction : communication entre scripts du même côté ; mêmes limites d’arguments que les remotes.

Sources et pour aller plus loin

Quiz : vérifie tes acquis

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

  1. Question 1QCMQue fait un LocalScript qui appelle InvokeServer ?
  2. Question 2QCMPourquoi la documentation déconseille-t-elle InvokeClient ? (plusieurs réponses possibles)

    Plusieurs réponses possibles : coche toutes les bonnes.

  3. Question 3Vrai ou fauxUn BindableEvent permet à un Script serveur d’envoyer un message à un LocalScript.
  4. Question 4Vrai ou fauxSi deux scripts assignent chacun une fonction à OnServerInvoke sur la même RemoteFunction, seule la dernière assignée sera utilisée.
  5. Question 5Trouve le bugCe Script serveur doit répondre à la RemoteFunction ObtenirInventaire. Il provoque une erreur au démarrage. Où est le bug ?
    local ReplicatedStorage = game:GetService("ReplicatedStorage")
    
    local remotes = ReplicatedStorage:WaitForChild("Remotes")
    local obtenirInventaire = remotes:WaitForChild("ObtenirInventaire")
    
    obtenirInventaire.OnServerInvoke:Connect(function(player)
    	return { "TrailNuage" }
    end)
  6. Question 6Améliore ce promptTu veux que Claude Code crée la RemoteFunction qui donne au client la liste de ses traînées. Quelle version du prompt donnera le meilleur résultat ?
    Le prompt de départ
    fais une remotefunction pour l'inventaire

    Choisis la meilleure version de ce prompt.