Catégories
Jeu vidéo ROBLOX ROBLOX-NIVEAU02

Créer une Zone Antigravité (Zone d’Apesanteur)

Construire en trois phases une zone magique où le joueur flotte comme dans l’espace :

  1. Phase 1 : donner au joueur une force vers le haut pour annuler la gravité quand il touche une Part.
  2. Phase 2 : supprimer cette force quand le joueur sort de la Part (et qu’il est retombé au sol).
  3. 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

  1. Dans Workspace, crée un dossier GravityFolder.
  2. Ajoute une Part nommée Part : 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 (sinon Touched, TouchEnded et GetTouchingParts ne fonctionnent pas !).
  3. Crée un Script dans ServerScriptService et 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 Touched se déclenche à chaque contact (rappel : des dizaines de fois par seconde !).
  • On cherche si une VectorForce existe 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 :

  1. rootPart.AssemblyMass : la masse totale du personnage (corps, habits, accessoires…). C’est pour ça qu’on utilise AssemblyMass et pas Mass (qui ne donnerait que la masse d’une seule Part !).
  2. 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.)
  3. force.Force = Vector3.new(0, GRAVITY * rootPart.AssemblyMass, 0) : la force est recalculée à chaque Touched, 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 VectorForce et un Attachment dans le HumanoidRootPart.
  • Si tu sautes, tu ne montes presque plus (JumpHeight = 1).

QCM :

  1. 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.
  1. Pourquoi utilise-t-on rootPart.AssemblyMass plutôt que rootPart.Mass ?
    • a) Pour avoir la masse totale du personnage entier, pas seulement celle du HumanoidRootPart.
    • b) C’est plus rapide.
    • c) Mass n’existe pas.
  1. 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 VectorForce seraient créées (un par contact Touched !) et le joueur partirait en fusée.
    • b) Rien.
    • c) Le jeu plante immédiatement.
  1. 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 TouchEnded se 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 avec humanoid:GetState() :
    • Enum.HumanoidStateType.Jumping : il saute → on attend.
    • Enum.HumanoidStateType.Freefall : il tombe → on attend.
  • Dernier piège : GetTouchingParts() est appelé après un task.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 VectorForce et l’Attachment ont disparu de l’Explorer.

QCM :

  1. Pourquoi vérifier isStillTouching(character) avant de supprimer la force ?
    • a) Parce que TouchEnded se 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 TouchEnded ne fonctionne pas.
  1. 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.
  1. Pourquoi ignorer l’événement si l’état du joueur est Jumping ou Freefall ?
    • 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.
  1. 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.

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 Humanoid passe par des états : Running (il court), Jumping (il saute), Freefall (il tombe), Landed (il vient d’atterrir !)…
  • L’événement humanoid.StateChanged se déclenche à chaque changement d’état, avec l’ancien et le nouvel état en paramètres.
  • Notre règle : si le nouvel état est Landed ET 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 !) :

  1. Players.PlayerAdded → nouveau joueur.
  2. player.CharacterAdded → chaque apparition/réapparition du personnage.
  3. 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 :

  1. newState ~= Enum.HumanoidStateType.Landed : on filtre tout ce qui n’est pas un atterrissage (courir, sauter, nager…). Seul Landed nous intéresse.
  2. Le duo PlayerAdded + boucle sur GetPlayers() : rappelle-toi le tutoriel des checkpoints ! Si le script démarre après qu’un joueur soit déjà là, PlayerAdded ne se déclenchera pas pour lui → la boucle finale rattrape tout le monde.
  3. task.spawn(...) : lance la fonction dans un thread séparé pour ne pas bloquer le script pendant les WaitForChild.

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 VectorForce a été détruit par la Phase 3 (et non la Phase 2 !).
  • Tue ton personnage (Reset Character dans 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 par CharacterAdded !).

QCM :

  1. À 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.
  1. 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.
  1. 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.
  1. Pourquoi une boucle for sur Players:GetPlayers() à la fin du script ?
    • a) Pour rattraper les joueurs déjà connectés avant le démarrage du script (le PlayerAdded ne se déclenche pas pour eux).
    • b) Pour les déconnecter.
    • c) Pour afficher leurs noms.
  1. 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

ConceptDescriptionExemple
Poids = masse × gravitéLa formule physique pour annuler la gravité.GRAVITY * rootPart.AssemblyMass
VectorForceApplique une force continue sur une Part.force.Force = Vector3.new(0, p, 0)
AttachmentPoint d’ancrage invisible pour la force.force.Attachment0 = attachment
AssemblyMassMasse totale du personnage.rootPart.AssemblyMass
TouchEndedDé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 / CharacterAddedSuivre les joueurs et leurs respawns.Phase 3
FactorisationDes 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 augmente JumpHeight à 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 !) avec EmissionDirection = 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 BodyAngularVelocity ou applique rootPart.AssemblyAngularVelocity = Vector3.new(0, 2, 0) à l’entrée dans la zone.

Défi final :

Crée un niveau spatial complet :

  1. Trois zones : une d’apesanteur (le script du jour), une « lune » (gravité à 50 %), une « geyser » (force à 150 %).
  2. Des plateformes flottantes entre les zones (réutilise le tutoriel TweenService !).
  3. Un checkpoint au sommet (réutilise le tutoriel des checkpoints !) avec une bulle de chat « Bien joué ! » verte à l’arrivée.