Construire en trois phases une zone magique où le joueur flotte comme dans l’espace :
- Phase 1 : donner au joueur une force vers le haut pour annuler la gravité quand il touche une
Part. - Phase 2 : supprimer cette force quand le joueur sort de la Part (et qu’il est retombé au sol).
- Phase 3 : supprimer cette force quand le joueur touche le sol (atterrissage).
Ce tutoriel introduit des concepts physiques réels (masse, gravité, poids) et des outils Roblox puissants : VectorForce, Attachment, les états de l’Humanoid (StateChanged) et GetTouchingParts().
Introduction : La physique de la gravité dans Roblox
Pourquoi le joueur tombe-t-il ?
Dans Roblox (comme dans la vraie vie !), chaque objet subit son poids : une force qui le tire vers le bas.
Poids = Masse × Gravité
- La gravité de Roblox vaut par défaut 196.2 studs/seconde² (accessible via
workspace.Gravity). - La masse du personnage est accessible via
rootPart.AssemblyMass. - Donc le poids d’un personnage =
196.2 × masse, dirigé vers le bas (Vector3.new(0, -poids, 0)).
L’astuce du script : annuler le poids !
Si on applique sur le joueur une force exactement égale à son poids, mais vers le HAUT, les deux forces s’annulent :
Gravité (vers le bas) + Notre force (vers le haut) = 0 → le joueur FLOTE !
C’est exactement ce que ressentent les astronautes en orbite : ils sont en apesanteur !
L’outil : VectorForce
Un VectorForce est un objet qui applique une force en continue sur une Part. Il a besoin :
- d’un
Attachment(un point d’ancrage invisible sur la Part), - d’une direction relative au monde (
Enum.ActuatorRelativeTo.World), - d’une force exprimée en
Vector3.
Préparation dans Roblox Studio
- Dans
Workspace, crée un dossierGravityFolder. - Ajoute une
PartnomméePart: c’est la zone d’apesanteur.- Forme libre : une grande zone transparente (
Transparency = 0.8,Anchored = true,CanCollide = false) donne le meilleur effet « champ de force » ! - Important : laisse
CanTouch = true(sinonTouched,TouchEndedetGetTouchingPartsne fonctionnent pas !).
- Forme libre : une grande zone transparente (
- Crée un
ScriptdansServerScriptServiceet colle le code complet (utilitaires + Phase 1 + Phase 2 + Phase 3).

Le script complet, corrigé et commenté en français
Voici le script commenté en français :
-- Récupérer le service des joueurs
local Players = game:GetService("Players")
-- Récupérer la zone anti-gravité
local folder = workspace:WaitForChild("GravityFolder")
local antiGravityPart = folder:WaitForChild("Part")
-- Constantes du script
local GRAVITY = workspace.Gravity * 0.99 -- 99% de la gravité (petite marge de confort)
local DEFAULT_JUMP_HEIGHT = 7.2 -- Hauteur de saut normale de Roblox
-- Fonction utilitaire : retrouve le personnage, son rootPart et son humanoid
-- à partir de l'objet qui a touché la zone (hit)
local function getCharacterFromHit(hit)
local character = hit:FindFirstAncestorOfClass("Model")
if not character then return nil end
local rootPart = character:FindFirstChild("HumanoidRootPart")
if not rootPart then return nil end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return nil end
return character, rootPart, humanoid -- On retourne les trois !
end
-- Fonction utilitaire : crée (ou retrouve) la force anti-gravité du joueur
local function getOrCreateAntiGravityForce(rootPart)
-- Chercher une force déjà existante (pour ne pas en créer deux !)
local force = rootPart:FindFirstChildOfClass("VectorForce")
if not force then
-- Créer le point d'ancrage invisible sur le joueur
local attachment = Instance.new("Attachment")
attachment.Parent = rootPart
-- Créer la force vectorielle
force = Instance.new("VectorForce")
force.Attachment0 = attachment -- où la force s'applique
force.RelativeTo = Enum.ActuatorRelativeTo.World -- relative au MONDE
force.ApplyAtCenterOfMass = true -- au centre de masse
force.Parent = rootPart
end
return force
end
-- Fonction utilitaire : supprime la force anti-gravité du joueur
local function removeAntiGravityForce(rootPart, humanoid)
local force = rootPart:FindFirstChildOfClass("VectorForce")
if force then
force.Attachment0:Destroy() -- supprimer le point d'ancrage
force:Destroy() -- supprimer la force
end
humanoid.JumpHeight = DEFAULT_JUMP_HEIGHT -- restaurer le saut normal
end
-- Fonction utilitaire : vérifie si le personnage touche encore la zone
local function isStillTouching(character)
for _, touchingPart in pairs(antiGravityPart:GetTouchingParts()) do
if touchingPart.Parent == character then
return true -- une partie du corps touche encore la zone !
end
end
return false -- plus aucun contact
end
PHASE 1 : Donner une force vers le haut quand le joueur touche la Part
But : dès que le joueur entre dans la zone, il flotte.
Explication :
- L’événement
Touchedse déclenche à chaque contact (rappel : des dizaines de fois par seconde !). - On cherche si une
VectorForceexiste déjà sur le joueur (sinon on en crée une). C’est notre réflexe « cooldown » des tutoriels précédents : ne jamais créer deux fois le même objet ! - On applique la formule physique : Force = Masse × Gravité, vers le haut.
- On réduit
JumpHeightà 1 : en apesanteur, un grand saut propulserait le joueur au plafond !
-- PHASE 1 : le joueur touche la zone → il flotte
antiGravityPart.Touched:Connect(function(hit)
-- Retrouver personnage, rootPart et humanoid (fonction utilitaire)
local character, rootPart, humanoid = getCharacterFromHit(hit)
if not character then return end -- pas un joueur : on ignore
-- Créer la force (ou retrouver celle qui existe déjà)
local force = getOrCreateAntiGravityForce(rootPart)
-- LA PHYSIQUE : force qui annule exactement le poids, vers le haut
-- Force = gravité × masse, dirigée vers +Y
force.Force = Vector3.new(0, GRAVITY * rootPart.AssemblyMass, 0)
-- En apesanteur, on limite le saut pour ne pas partir dans le décor !
humanoid.JumpHeight = 1
end)
Points clés :
rootPart.AssemblyMass: la masse totale du personnage (corps, habits, accessoires…). C’est pour ça qu’on utiliseAssemblyMasset pasMass(qui ne donnerait que la masse d’une seule Part !).GRAVITY = workspace.Gravity * 0.99: pourquoi 99 % et pas 100 % ? Avec exactement 100 %, le joueur resterait figé en l’air comme une statue. Avec 99 %, il descend très lentement : il peut quitter la zone en marchant, c’est plus jouable ! (Tu peux essayer 1.0 pour voir la différence.)force.Force = Vector3.new(0, GRAVITY * rootPart.AssemblyMass, 0): la force est recalculée à chaqueTouched, donc si le joueur met un chapeau (masse change), la force s’adapte !
Teste ! Place une Part dans un dossier GravityFolder, lance le jeu et marche dedans.
Contrôle :
- Ton personnage ** monte lentement / flotte ** dans la zone.
- Dans l’Explorer, tu vois apparaître un
VectorForceet unAttachmentdans leHumanoidRootPart. - Si tu sautes, tu ne montes presque plus (
JumpHeight = 1).
QCM :
- Que vaut la force appliquée au joueur ?
- a) La masse du personnage multipliée par la gravité, dirigée vers le haut.
- b) Toujours 100, vers le bas.
- c) La taille du personnage.
- Pourquoi utilise-t-on
rootPart.AssemblyMassplutôt querootPart.Mass?- a) Pour avoir la masse totale du personnage entier, pas seulement celle du HumanoidRootPart.
- b) C’est plus rapide.
- c)
Massn’existe pas.
- Que se passe-t-il si on ne vérifie pas si la force existe déjà (
if not force then)?- a) Des dizaines de
VectorForceseraient créées (un par contactTouched!) et le joueur partirait en fusée. - b) Rien.
- c) Le jeu plante immédiatement.
- a) Des dizaines de
- Pourquoi mettre
humanoid.JumpHeight = 1?- a) Pour empêcher le joueur de sauter très haut dans la zone et s’échapper au plafond.
- b) Pour l’empêcher de marcher.
- c) Pour changer sa couleur.
PHASE 2 : Supprimer la force quand le joueur SORT de la Part
But : quand le joueur n’a plus aucun contact avec la zone, il retrouve une gravité normale.
Explication — le problème est plus subtil qu’il n’y paraît :
- L’événement
TouchEndedse déclenche quand une Part précise (ex: le pied gauche) quitte le contact. Mais le joueur a encore peut-être un autre contact (le pied droit !). - Il faut donc vérifier avec
GetTouchingParts(): la liste de toutes les Parts qui touchent actuellement la zone. Si une seule appartient au personnage → il est encore dedans → on ne fait rien ! - Autre piège : si le joueur saute ou est en chute libre au moment du
TouchEnded, il est en l’air et va retoucher la zone dans un instant. On regarde donc son état avechumanoid:GetState():Enum.HumanoidStateType.Jumping: il saute → on attend.Enum.HumanoidStateType.Freefall: il tombe → on attend.
- Dernier piège :
GetTouchingParts()est appelé après untask.wait(1)pour laisser le temps au moteur physique de « réaliser » le nouveau contact (ex: le joueur qui retombe dans la zone juste après le saut).
-- PHASE 2 : le joueur sort de la zone → on enlève la force
antiGravityPart.TouchEnded:Connect(function(hit)
-- Retrouver le personnage
local character, rootPart, humanoid = getCharacterFromHit(hit)
if not character then return end
-- S'il saute ou tombe, il va peut-être retoucher la zone : on ne fait rien !
local state = humanoid:GetState()
if state == Enum.HumanoidStateType.Jumping then return end
if state == Enum.HumanoidStateType.Freefall then return end
-- Attendre 1 seconde que le contact se stabilise
task.wait(1)
-- Le joueur touche-t-il ENCORE la zone (avec une autre partie du corps) ?
if isStillTouching(character) then return end -- oui : il est toujours dedans !
-- Non : il est bien sorti → supprimer la force
removeAntiGravityForce(rootPart, humanoid)
end)
Teste ! Entre dans la zone, flotte, puis sors complètement.
Contrôle :
- Tant qu’une seule partie de ton corps touche la zone, tu continues de flotter (demande à un camarade de regarder depuis l’extérieur !).
- Dès que tu es totalement sorti, tu retombes : gravité normale, saut restauré (
JumpHeight = 7.2). - Le
VectorForceet l’Attachmentont disparu de l’Explorer.
QCM :
- Pourquoi vérifier
isStillTouching(character)avant de supprimer la force ?- a) Parce que
TouchEndedse déclenche pour UNE Part (ex: un pied), mais le joueur peut encore toucher la zone avec une autre (ex: l’autre pied). - b) Pour faire durer le jeu.
- c) Parce que
TouchEndedne fonctionne pas.
- a) Parce que
- Que retourne
part:GetTouchingParts()?- a) La liste de toutes les Parts actuellement en contact avec la zone.
- b) Le nom de la zone.
- c) La liste des joueurs du serveur.
- Pourquoi ignorer l’événement si l’état du joueur est
JumpingouFreefall?- a) Parce qu’en sautant ou en tombant, il va probablement retoucher la zone juste après : ce serait bête de couper la force pour rien.
- b) Parce que les sauts sont interdits.
- c) Parce que l’état Jumping détruit la force tout seul.
- Que fait
removeAntiGravityForce(rootPart, humanoid)?- a) Détruit l’Attachment et le VectorForce, et restaure
JumpHeightà 7.2. - b) Détruit le personnage.
- c) Téléporte le joueur.
- a) Détruit l’Attachment et le VectorForce, et restaure
PHASE 3 : Supprimer la force quand le joueur TOUCCHE LE SOL
But : si le joueur saute hors de la zone et atterrit au sol (sans jamais déclencher le TouchEnded complet de la Phase 2), il faut quand même couper la force — sinon il flotterait en permanence !
Explication — un nouvel outil puissant : StateChanged !
- Un
Humanoidpasse par des états :Running(il court),Jumping(il saute),Freefall(il tombe),Landed(il vient d’atterrir !)… - L’événement
humanoid.StateChangedse déclenche à chaque changement d’état, avec l’ancien et le nouvel état en paramètres. - Notre règle : si le nouvel état est
LandedET que le joueur ne touche plus la zone → on supprime la force.
Le problème du respawn : le VectorForce est attaché au personnage. Si on connectait StateChanged n’importe quand, on raterait les joueurs déjà connectés ou ceux qui respawnent. Solution en 3 étages (déjà vue dans le tutoriel des checkpoints !) :
Players.PlayerAdded→ nouveau joueur.player.CharacterAdded→ chaque apparition/réapparition du personnage.humanoid.StateChanged→ chaque changement d’état.
-- PHASE 3 : le joueur atterrit au sol → on enlève la force
-- Fonction appelée à chaque changement d'état du Humanoid
local function onStateChanged(character, humanoid, oldState, newState)
-- On ne s'intéresse qu'à l'atterrissage
if newState ~= Enum.HumanoidStateType.Landed then return end
local rootPart = character:FindFirstChild("HumanoidRootPart")
if not rootPart then return end
-- S'il touche encore la zone, il est dedans : on garde la force !
if isStillTouching(character) then return end
-- Il a atterri ailleurs : supprimer la force
removeAntiGravityForce(rootPart, humanoid)
end
-- Fonction appelée à chaque apparition du personnage
local function onCharacterAdded(player, character)
-- Attendre que le Humanoid existe
local humanoid = character:WaitForChild("Humanoid")
-- Connecter le suivi des changements d'état
humanoid.StateChanged:Connect(function(oldState, newState)
onStateChanged(character, humanoid, oldState, newState)
end)
end
-- Fonction appelée à chaque connexion d'un joueur
local function onPlayerAdded(player)
-- Cas 1 : le personnage existe DÉJÀ (joueur connecté avant le script)
if player.Character then
task.spawn(onCharacterAdded, player, player.Character)
end
-- Cas 2 : chaque apparition/réapparition future (mort, respawn...)
player.CharacterAdded:Connect(function(character)
onCharacterAdded(player, character)
end)
end
-- Nouveaux joueurs
Players.PlayerAdded:Connect(onPlayerAdded)
-- Joueurs déjà connectés AVANT l'exécution du script
for _, player in ipairs(Players:GetPlayers()) do
task.spawn(onPlayerAdded, player)
end
Points clés :
newState ~= Enum.HumanoidStateType.Landed: on filtre tout ce qui n’est pas un atterrissage (courir, sauter, nager…). SeulLandednous intéresse.- Le duo
PlayerAdded+ boucle surGetPlayers(): rappelle-toi le tutoriel des checkpoints ! Si le script démarre après qu’un joueur soit déjà là,PlayerAddedne se déclenchera pas pour lui → la boucle finale rattrape tout le monde. task.spawn(...): lance la fonction dans un thread séparé pour ne pas bloquer le script pendant lesWaitForChild.
Teste ! Entre dans la zone, saute au-dessus des bords et atterris loin de la zone.
Contrôle :
- À l’atterrissage hors zone, tu retombes immédiatement sous la gravité normale : le
VectorForcea été détruit par la Phase 3 (et non la Phase 2 !). - Tue ton personnage (
Reset Characterdans le menu) : il respawn… sans force résiduelle (le VectorForce était attaché à l’ancien personnage, détruit avec lui — et le nouveau personnage est suivi parCharacterAdded!).
QCM :
- À quoi sert
humanoid.StateChanged?- a) À exécuter une fonction à chaque changement d’état du personnage (saut, chute, atterrissage…).
- b) À détecter les collisions.
- c) À changer la couleur du personnage.
- Quel état nous intéresse dans la Phase 3 ?
- a)
Enum.HumanoidStateType.Landed(le joueur vient d’atterrir). - b)
Enum.HumanoidStateType.Running. - c)
Enum.HumanoidStateType.Dead.
- a)
- Pourquoi le script se connecte-t-il aussi à
player.CharacterAdded?- a) Pour suivre chaque nouveau personnage (mort, respawn) et reconnecter
StateChangedà chaque fois. - b) Pour créer un nouveau joueur.
- c) Pour rien.
- a) Pour suivre chaque nouveau personnage (mort, respawn) et reconnecter
- Pourquoi une boucle
forsurPlayers:GetPlayers()à la fin du script ?- a) Pour rattraper les joueurs déjà connectés avant le démarrage du script (le
PlayerAddedne se déclenche pas pour eux). - b) Pour les déconnecter.
- c) Pour afficher leurs noms.
- a) Pour rattraper les joueurs déjà connectés avant le démarrage du script (le
- Pourquoi vérifier
isStillTouching(character)même dans la Phase 3 ?- a) Parce qu’un joueur peut atterrir SUR la zone elle-même (ou juste à côté, en la touchant encore) : il doit continuer à flotter !
- b) Pour compter les joueurs.
- c) C’est inutile.
Résumé des concepts clés
| Concept | Description | Exemple |
|---|---|---|
| Poids = masse × gravité | La formule physique pour annuler la gravité. | GRAVITY * rootPart.AssemblyMass |
VectorForce | Applique une force continue sur une Part. | force.Force = Vector3.new(0, p, 0) |
Attachment | Point d’ancrage invisible pour la force. | force.Attachment0 = attachment |
AssemblyMass | Masse totale du personnage. | rootPart.AssemblyMass |
TouchEnded | Déclenché quand UN contact se termine (attention aux autres contacts !). | Phase 2 |
GetTouchingParts() | Liste toutes les Parts en contact actuel. | isStillTouching(character) |
humanoid:GetState() | État actuel du personnage (Jumping, Freefall, Landed…). | Phase 2 |
StateChanged | Événement à chaque changement d’état. | Phase 3 |
PlayerAdded / CharacterAdded | Suivre les joueurs et leurs respawns. | Phase 3 |
| Factorisation | Des fonctions utilitaires au lieu de dupliquer le code. | removeAntiGravityForce(...) |
Idées pour aller plus loin
1. Une zone qui donne une force supérieure au poids
- Objectif : le joueur monte lentement comme dans un ascenseur de geysers.
- Comment faire ? Multiplie la force par un coefficient :
Vector3.new(0, GRAVITY * rootPart.AssemblyMass * 1.5, 0)— le joueur s’élève à 50 % de la gravité nette !
2. Zone de lune (gravité réduite)
- Objectif : le joueur fait des bonds géants comme sur la Lune.
- Comment faire ? Force à 50 % du poids :
Vector3.new(0, GRAVITY * rootPart.AssemblyMass * 0.5, 0)et augmenteJumpHeightà 25 !
3. Ajouter des particules de lévitation
- Objectif : rendre la zone visuellement magique.
- Comment faire ? Ajoute un
ParticleEmitterà la zone (rappel du tutoriel des particules !) avecEmissionDirection = Enum.NormalId.Top.
4. Un effet de rotation pendant l’apesanteur
- Objectif : le joueur tourne lentement sur lui-même comme un astronaute.
- Comment faire ? Ajoute un
BodyAngularVelocityou appliquerootPart.AssemblyAngularVelocity = Vector3.new(0, 2, 0)à l’entrée dans la zone.
Défi final :
Crée un niveau spatial complet :
- Trois zones : une d’apesanteur (le script du jour), une « lune » (gravité à 50 %), une « geyser » (force à 150 %).
- Des plateformes flottantes entre les zones (réutilise le tutoriel TweenService !).
- Un checkpoint au sommet (réutilise le tutoriel des checkpoints !) avec une bulle de chat « Bien joué ! » verte à l’arrivée.
