Dans ce tutoriel, tu vas apprendre à programmer des ennemis très simples dans Roblox. Les ennemis sont représentés par des blocs. Les ennemies détectent un joueur proche, avancent vers lui, et lui infligent des dégâts lorsqu’ils le touchent.
Tu vas construire ton script pour gérer plusieurs ennemis qui pourront avoir des comportements différents : distance proche différence, vitesse de déplacement différente, niveau de dommage différent.
Les ennemis s’approchent
Dans un premier temps construis l’arborescence suivante avec un Folder qui va regrouper tous tes ennemis, un script, des Part qui vont représenter tes ennemis :

Renomme les éléments de ton arborescence :
- Folder : Enemies
- Script : EnemiesScript
- Part : Enemy
Puis saisie le code suivant dans ton script pour que tous les blocs ennemis s’approchent du joueur à partir d’une certaine distance entre le joueur et le bloc :
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local Debris = game:GetService("Debris")
-- Configuration
local DETECTION_DISTANCE = 30 -- distance de détection du joueur (studs)
local SPEED = 10 -- vitesse de déplacement (studs/s)
local STOP_DISTANCE = 3 -- distance à laquelle la part s'arrête
local enemies = script.Parent
-- Cache des données par platform (évite GetAttribute à chaque frame)
local enemiesData = {}
-- Initialisation
for _, enemy in enemies:GetChildren() do
if not enemy:IsA("BasePart") then continue end
enemy.Anchored = true
-- Stocke les données en mémoire
enemiesData[enemy] = {
detectionDistance = enemy:GetAttribute("DetectionDistance") or DETECTION_DISTANCE,
speed = enemy:GetAttribute("Speed") or SPEED,
stopDistance = enemy:GetAttribute("StopDistance") or STOP_DISTANCE,
}
end
RunService.Heartbeat:Connect(function(deltaTime)
-- Récupère le joueur local (ou le premier joueur connecté côté serveur)
local player = Players:GetPlayers()[1]
if not player then return end
local character = player.Character
if not character then return end
-- Récupère la position du joueur
local root = character:FindFirstChild("HumanoidRootPart")
if not root then return end
for enemy, data in pairs(enemiesData) do
-- Vérifie que la plateforme existe encore
if not enemy or not enemy.Parent then
enemyData[enemy] = nil
continue
end
-- Vérifie que le joueur est à portée
local distance = (root.Position - enemy.Position).Magnitude
-- Trop loin : la part attend
if distance > data.detectionDistance then continue end
-- Assez proche : arrêt
if data.stopDistance > 0 and distance <= data.stopDistance then
enemy.AssemblyLinearVelocity = Vector3.zero
continue
end
-- Calcul de la direction vers le joueur
local direction = (root.Position - enemy.Position).Unit
local newPosition = enemy.Position + direction * SPEED * deltaTime
-- Mise à jour de la force à appliquer en fonction de la vitesse
enemy.AssemblyLinearVelocity = direction * data.speed
-- Mise à jour de la position de la part et son orientation
enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position) + direction * data.speed * deltaTime
end
end)
Lance le jeu : les blocs s’approchent du joueur mais restent à une certaine distance du joueur.
Les ennemis infligent des dommages au joueur
Dans le script suivant les blocs s’approchent à toucher le joueur, et lui infligent des dommages :
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local Debris = game:GetService("Debris")
-- Configuration
local DETECTION_DISTANCE = 30 -- distance de détection du joueur (studs)
local SPEED = 10 -- vitesse de déplacement (studs/s)
local STOP_DISTANCE = 0 -- distance à laquelle la part s'arrête
local DAMAGE = 5 -- dégâts infligés aux joueurs
local RESET_DELAY = 0.5 -- délai avant de pouvoir toucher à nouveau (secondes)
local enemies = script.Parent
-- Cache des données par platform (évite GetAttribute à chaque frame)
local enemiesData = {}
-- Initialisation
for _, enemy in enemies:GetChildren() do
if not enemy:IsA("BasePart") then continue end
enemy.Anchored = true
enemy.CanCollide = true
-- Stocke les données en mémoire
enemiesData[enemy] = {
detectionDistance = enemy:GetAttribute("DetectionDistance") or DETECTION_DISTANCE,
speed = enemy:GetAttribute("Speed") or SPEED,
stopDistance = enemy:GetAttribute("StopDistance") or STOP_DISTANCE,
damage = enemy:GetAttribute("Damage") or DAMAGE,
resetDelay = enemy:GetAttribute("ResetDelay") or RESET_DELAY,
}
enemy.Touched:Connect(function(otherPart)
-- Vérifie que l'objet touché est un personnage
local character = otherPart.Parent
if not Players:GetPlayerFromCharacter(character) then return end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid or humanoid.Health <= 0 then return end
-- Vérifie que la part n'a pas déjà été touchée
if enemy:FindFirstChild("Touched") then return end
-- Applique les dégâts et empêche la répétition
humanoid:TakeDamage((enemiesData[enemy].damage))
enemy.AssemblyLinearVelocity = Vector3.zero
-- Marque la part comme touchée
local tag = Instance.new("Folder")
tag.Name = "Touched"
tag.Parent = enemy
Debris:AddItem(tag, enemiesData[enemy].resetDelay)
end)
end
RunService.Heartbeat:Connect(function(deltaTime)
-- Récupère le joueur local (ou le premier joueur connecté côté serveur)
local player = Players:GetPlayers()[1]
if not player then return end
local character = player.Character
if not character then return end
-- Récupère la position du joueur
local root = character:FindFirstChild("HumanoidRootPart")
if not root then return end
for enemy, data in pairs(enemiesData) do
-- Vérifie que la plateforme existe encore
if not enemy or not enemy.Parent then
enemyData[enemy] = nil
continue
end
-- Si la part est déja touchée, on ne fait rien
if enemy:FindFirstChild("Touched") then return end
-- Vérifie que le joueur est à portée
local distance = (root.Position - enemy.Position).Magnitude
-- Trop loin : la part attend
if distance > data.detectionDistance then return end
-- Assez proche : arrêt
if data.stopDistance > 0 and distance <= data.stopDistance then
enemy.AssemblyLinearVelocity = Vector3.zero
continue
end
-- Calcul de la direction vers le joueur
local direction = (root.Position - enemy.Position).Unit
-- Mise à jour de la force à appliquer en fonction de la vitesse
enemy.AssemblyLinearVelocity = direction * data.speed
-- Mise à jour de la position de la part et son orientation
enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position) + direction * data.speed * deltaTime
end
end)
Paramétrer chaque ennemis
Ajoute aux propriétés de chaque ennemi les attributs suivants avec des valeurs différentes :
- DetectionDistance : distance de détection du joueur (studs)
- Speed : vitesse de déplacement (studs/s)
- StopDistance : distance à laquelle la part s’arrête si 0 l’ennemi vient toucher le joueur
- Damage : dégâts infligés aux joueurs
- ResetDelay : délai avant de pouvoir toucher à nouveau (secondes)

Puis saisie le Nom de l’attribut ainsi que son type number :

Puis donne une valeur pour chaque attribut :

L’ennemi est une boule

Ajoute dans le code pour un nouveau comportement pour faire rouler le boule :
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local Debris = game:GetService("Debris")
-- Configuration
local DETECTION_DISTANCE = 30 -- distance de détection du joueur (studs)
local SPEED = 10 -- vitesse de déplacement (studs/s)
local STOP_DISTANCE = 0 -- distance à laquelle la part s'arrête
local DAMAGE = 5 -- dégâts infligés aux joueurs
local RESET_DELAY = 0.5 -- délai avant de pouvoir toucher à nouveau (secondes)
local enemies = script.Parent
-- Cache des données par platform (évite GetAttribute à chaque frame)
local enemiesData = {}
-- Initialisation
for _, enemy in enemies:GetChildren() do
if not enemy:IsA("BasePart") then continue end
enemy.Anchored = true
enemy.CanCollide = true
-- Stocke les données en mémoire
enemiesData[enemy] = {
detectionDistance = enemy:GetAttribute("DetectionDistance") or DETECTION_DISTANCE,
speed = enemy:GetAttribute("Speed") or SPEED,
stopDistance = enemy:GetAttribute("StopDistance") or STOP_DISTANCE,
damage = enemy:GetAttribute("Damage") or DAMAGE,
resetDelay = enemy:GetAttribute("ResetDelay") or RESET_DELAY,
}
enemy.Touched:Connect(function(otherPart)
-- Vérifie que l'objet touché est un personnage
local character = otherPart.Parent
if not Players:GetPlayerFromCharacter(character) then return end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid or humanoid.Health <= 0 then return end
-- Vérifie que la part n'a pas déjà été touchée
if enemy:FindFirstChild("Touched") then return end
-- Applique les dégâts et empêche la répétition
enemy.AssemblyLinearVelocity = Vector3.zero
enemy.AssemblyAngularVelocity = Vector3.zero
humanoid:TakeDamage((enemiesData[enemy].damage))
-- Marque la part comme touchée
local tag = Instance.new("Folder")
tag.Name = "Touched"
tag.Parent = enemy
Debris:AddItem(tag, enemiesData[enemy].resetDelay)
end)
end
RunService.Heartbeat:Connect(function(deltaTime)
-- Récupère le joueur local (ou le premier joueur connecté côté serveur)
local player = Players:GetPlayers()[1]
if not player then return end
local character = player.Character
if not character then return end
-- Récupère la position du joueur
local root = character:FindFirstChild("HumanoidRootPart")
if not root then return end
for enemy, data in pairs(enemiesData) do
-- Vérifie que la plateforme existe encore
if not enemy or not enemy.Parent then
--enemyData[enemy] = nil
continue
end
-- Si la part est déja touchée, on ne fait rien
if enemy:FindFirstChild("Touched") then continue end
-- Vérifie que le joueur est à portée
local distance = (root.Position - enemy.Position).Magnitude
-- Trop loin : la part attend
if distance > data.detectionDistance then continue end
-- Assez proche : arrêt
if data.stopDistance > 0 and distance <= data.stopDistance then
enemy.AssemblyLinearVelocity = Vector3.zero
continue
end
-- Calcul de la direction vers le joueur
if enemy.Shape == Enum.PartType.Ball then
enemy.Anchored = false
-- Direction vers le joueur (ignorant Y pour rester au sol)
local direction = Vector3.new(
root.Position.X - enemy.Position.X,
0,
root.Position.Z - enemy.Position.Z
).Unit
-- Force appliquée vers le joueur
local mass = enemy.AssemblyMass
local targetVelocity = direction * data.speed
local correction = targetVelocity - Vector3.new(
enemy.AssemblyLinearVelocity.X,
0,
enemy.AssemblyLinearVelocity.Z
)
enemy:ApplyImpulse(correction * mass)
-- Rotation de roulement
local rollAxis = Vector3.new(-direction.Y, 0, -direction.X)
local rollSpeed = data.speed / (enemy.Size.Y / 2)
if enemy.Size.X > enemy.Size.Y then
rollSpeed = data.speed / (enemy.Size.X / 2)
end
enemy.AssemblyAngularVelocity = rollAxis * rollSpeed
else
local direction = (root.Position - enemy.Position).Unit
-- Mise à jour de la force à appliquer en fonction de la vitesse
enemy.AssemblyLinearVelocity = direction * data.speed
-- Mise à jour de la position de la part et son orientation
enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position) + direction * data.speed * deltaTime
end
end
end)
Les ennemies sont des RIG
Ajoute des rig sous une structure suivante :

Pour ajouter un RIG choisis :

Ajoute sous chaque Rig un script que tu renommes Animate, ce script anime le Rig pour qu’il marche ou qu’il attende en fonction de la situation :

local humanoid = script.Parent:WaitForChild("Humanoid")
local animator = humanoid:WaitForChild("Animator")
-- IDs des animations R6 par défaut de Roblox
local ANIMATIONS = {
idle = "rbxassetid://180435571",
walk = "rbxassetid://180426354",
run = "rbxassetid://180426354",
jump = "rbxassetid://125750702",
fall = "rbxassetid://180436148",
}
-- Charge les animations
local tracks = {}
for name, id in pairs(ANIMATIONS) do
local anim = Instance.new("Animation")
anim.AnimationId = id
tracks[name] = animator:LoadAnimation(anim)
end
-- Priorités
tracks.idle.Priority = Enum.AnimationPriority.Idle
tracks.walk.Priority = Enum.AnimationPriority.Movement
-- Joue l'idle au démarrage
tracks.idle:Play()
local currentAnim = "idle"
-- Vérification de vélocité pour gérer l'animation
local VELOCITY_THRESHOLD = 0.5 -- en dessous = considéré à l'arrêt
game:GetService("RunService").Heartbeat:Connect(function()
local velocity = humanoid.RootPart and
humanoid.RootPart.AssemblyLinearVelocity or Vector3.zero
local isMoving = Vector3.new(velocity.X, 0, velocity.Z).Magnitude > VELOCITY_THRESHOLD
if isMoving then
if currentAnim ~= "walk" then
tracks.idle:Stop()
tracks.walk:Play()
currentAnim = "walk"
end
else
if currentAnim == "walk" then
tracks.walk:Stop()
tracks.idle:Play()
currentAnim = "idle"
end
end
end)
Sous ton folder principal, créer un script et ajoute ce code :

local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
-- Configuration
local DETECTION_DISTANCE = 60
local STOP_DISTANCE = 3
local SPEED = 10
local UPDATE_INTERVAL = 0.5 -- réduit pour un mouvement plus fluide
local DAMAGE = 5
local RESET_DELAY = 0.5
local rigsFolder = script.Parent
local enemieRigData = {}
-- Crée une hitbox soudée sur le HumanoidRootPart
local function createHitbox(rig)
local root = rig:FindFirstChild("HumanoidRootPart")
if not root then return nil end
local hitbox = Instance.new("Part")
hitbox.Name = "Hitbox_" .. rig.Name
hitbox.Shape = Enum.PartType.Ball
hitbox.Size = Vector3.new(5, 5, 5)
hitbox.Transparency = 0.8
hitbox.CanCollide = false
hitbox.CanTouch = true
hitbox.Anchored = false
hitbox.CastShadow = false
hitbox.Massless = true
hitbox.Color = Color3.fromRGB(255, 0, 0)
hitbox.Material = Enum.Material.Neon
hitbox.CFrame = root.CFrame * CFrame.new(0, 1, -0.5)
hitbox.Parent = workspace
local weld = Instance.new("WeldConstraint")
weld.Part0 = root
weld.Part1 = hitbox
weld.Parent = hitbox
-- Nettoyage automatique si le rig est détruit
rig.AncestryChanged:Connect(function()
if not rig.Parent then
hitbox:Destroy()
enemieRigData[rig] = nil
end
end)
return hitbox
end
-- Arrête le rig
local function stopRig(data)
if not data.isMoving then return end
data.humanoid.WalkSpeed = 0
data.isMoving = false
end
-- Déplace le rig vers une cible
local function chaseTarget(data, targetPosition)
local now = time()
if now - data.lastUpdate < data.updateInterval then return end
data.lastUpdate = now
data.humanoid.WalkSpeed = data.speed
data.humanoid:MoveTo(targetPosition)
data.isMoving = true
end
-- Initialisation des rigs
for _, rig in ipairs(rigsFolder:GetChildren()) do
if not rig:IsA("Model") then continue end
local humanoid = rig:FindFirstChildOfClass("Humanoid")
local rootPart = rig:FindFirstChild("HumanoidRootPart")
if not humanoid or not rootPart then continue end
local hitbox = createHitbox(rig)
if not hitbox then continue end
enemieRigData[rig] = {
detectionDistance = rig:GetAttribute("DetectionDistance") or DETECTION_DISTANCE,
speed = rig:GetAttribute("Speed") or SPEED,
stopDistance = rig:GetAttribute("StopDistance") or STOP_DISTANCE,
updateInterval = rig:GetAttribute("UpdateInterval") or UPDATE_INTERVAL,
damage = rig:GetAttribute("Damage") or DAMAGE,
hitbox = hitbox,
humanoid = humanoid,
rootPart = rootPart,
lastUpdate = 0,
isMoving = false,
lastHit = 0,
}
-- Détection de collision avec le joueur
hitbox.Touched:Connect(function(hit)
local data = enemieRigData[rig]
if not data then return end
-- Vérifie que c'est bien le joueur (pas une autre hitbox)
local character = hit:FindFirstAncestorOfClass("Model")
if not character then return end
local humanoidTarget = character:FindFirstChildOfClass("Humanoid")
local player = Players:GetPlayerFromCharacter(character)
if not humanoidTarget or not player then return end
-- Cooldown pour éviter les dégâts multiples
local now = time()
if now - data.lastHit < RESET_DELAY then return end
data.lastHit = now
humanoidTarget:TakeDamage(data.damage)
print(rig.Name, "inflige", data.damage, "dégâts à", player.Name)
end)
end
-- Heartbeat : déplacement de tous les rigs
RunService.Heartbeat:Connect(function()
local player = Players:GetPlayers()[1]
if not player then return end
local character = player.Character
if not character then return end
local root = character:FindFirstChild("HumanoidRootPart")
if not root then return end
for rig, data in pairs(enemieRigData) do
if not rig.Parent then continue end
local distance = (root.Position - data.rootPart.Position).Magnitude
if distance > data.detectionDistance or distance <= data.stopDistance then
stopRig(data)
continue
end
chaseTarget(data, root.Position)
end
end)
Les Rig disposent d’une Hitbox pour gérer la collision avec le joueur, pour rendre complétement transparente cette Hitbox, modifie sa Tranparency :
-- Crée une hitbox soudée sur le HumanoidRootPart
local function createHitbox(rig)
local root = rig:FindFirstChild("HumanoidRootPart")
if not root then return nil end
local hitbox = Instance.new("Part")
hitbox.Name = "Hitbox_" .. rig.Name
hitbox.Shape = Enum.PartType.Ball
hitbox.Size = Vector3.new(5, 5, 5)
hitbox.Transparency = 1
