Catégories
Jeu vidéo ROBLOX

Ennemies qui avancent !!

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)

Des ennemis qui ne traversent pas les murs (Raycast) :

-- recherche des services
local RunService = game:GetService("RunService")
local Players    = game:GetService("Players")
local Debris     = game:GetService("Debris")

-- Configuration
local DETECTION_DISTANCE = 60   -- distance de détection du joueur (studs)
local SPEED              = 12   -- vitesse de déplacement (studs/s)
local STOP_DISTANCE      = 10    -- distance à laquelle la part s'arrête

-- recherche des ennemis dans le workspace sous le folder "Monstre"
local enemies          = workspace.GAME:WaitForChild("Monstre")

-- Paramètres dynamiques par ennemi
local enemiesData = {}

-- Initialisation de tous les ennemis
for _, enemy in enemies:GetChildren() do

	-- contrôle si c'est une part qui représente un ennemi
	if not enemy:IsA("BasePart") then continue end

	enemy.Anchored = true

	-- Stocke les données en mémoire pour chaque ennemi
	enemiesData[enemy] = {
		detectionDistance = enemy:GetAttribute("DetectionDistance") or DETECTION_DISTANCE,
		speed = enemy:GetAttribute("Speed") or SPEED,
		stopDistance = enemy:GetAttribute("StopDistance") or STOP_DISTANCE,
		cooldown = false,
	}

	-- dégats sur le joueur si il touche l'ennemi'
	enemy.Touched:Connect(function(hit)
		local character = hit.Parent
		local humanoid  = character and character:FindFirstChildOfClass("Humanoid")
		if not humanoid or humanoid.Health <= 0 then return end
		if enemiesData[enemy].cooldown then return end
		enemiesData[enemy].cooldown = true
		humanoid:TakeDamage(10)
		task.delay(0.5, function() enemiesData[enemy].cooldown = false end)
	end)
end

--[[ Configuration du Raycast (exclure les ennemis eux-mêmes et le personnage)
     Pour éviter que les parts traversent les murs, il faut faire un Raycast entre la part et le joueur. 
     Si le rayon touche un mur avant le joueur on stoppe la part.
]]

local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Exclude

RunService.Heartbeat:Connect(function(deltaTime)
	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

	-- Met à jour les exclusions à chaque frame (Player + tous les ennemis exclus)
	local excluded = { character }
	for enemy in pairs(enemiesData) do
		if enemy and enemy.Parent then
			table.insert(excluded, enemy)
		end
	end
	raycastParams.FilterDescendantsInstances = excluded

	for enemy, data in pairs(enemiesData) do
		if not enemy or not enemy.Parent then
			enemiesData[enemy] = nil
			continue
		end
		
		-- Calcul de la direction et de la distance entre l'ennemi et le joueur
		local toPlayer    = root.Position - enemy.Position
		local distance    = toPlayer.Magnitude
		local direction   = toPlayer.Unit

		-- Trop loin : la part attend
		if distance > data.detectionDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end

		-- Assez proche : arrêt
		if data.stopDistance > 0 and distance <= data.stopDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end

		-- Raycast entre l'ennemi et le joueur
		local rayResult = workspace:Raycast(enemy.Position, direction * distance, raycastParams)

		if rayResult then
			-- Un mur est détecté avant le joueur : on stoppe
			enemy.AssemblyLinearVelocity = Vector3.zero
			-- Optionnel : orienter quand même vers le joueur
			enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position)
		else
			-- Chemin libre : déplacement normal
			enemy.AssemblyLinearVelocity = direction * data.speed
			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 :

-- recherche des services
local RunService = game:GetService("RunService")
local Players    = game:GetService("Players")
local Debris     = game:GetService("Debris")

-- Configuration
local DETECTION_DISTANCE = 60   -- distance de détection du joueur (studs)
local SPEED              = 12   -- vitesse de déplacement (studs/s)
local STOP_DISTANCE      = 10    -- distance à laquelle la part s'arrête

-- recherche des ennemis dans le workspace sous le folder "Monstre"
local enemies          = workspace.GAME:WaitForChild("Monstre")

-- Paramètres dynamiques par ennemi
local enemiesData = {}

-- Initialisation de tous les ennemis
for _, enemy in enemies:GetChildren() do

	-- contrôle si c'est une part qui représente un ennemi
	if not enemy:IsA("BasePart") then continue end

	enemy.Anchored = true

	-- Stocke les données en mémoire pour chaque ennemi
	enemiesData[enemy] = {
		detectionDistance = enemy:GetAttribute("DetectionDistance") or DETECTION_DISTANCE,
		speed = enemy:GetAttribute("Speed") or SPEED,
		stopDistance = enemy:GetAttribute("StopDistance") or STOP_DISTANCE,
		cooldown = false,
	}

	-- dégats sur le joueur si il touche l'ennemi'
	enemy.Touched:Connect(function(hit)
		local character = hit.Parent
		local humanoid  = character and character:FindFirstChildOfClass("Humanoid")
		if not humanoid or humanoid.Health <= 0 then return end
		if enemiesData[enemy].cooldown then return end
		enemiesData[enemy].cooldown = true
		character:PivotTo(CFrame.new(-10, 1.5, -69))
		task.delay(0.5, function() enemiesData[enemy].cooldown = false end)
	end)
end

--[[ Configuration du Raycast (exclure les ennemis eux-mêmes et le personnage)
     Pour éviter que les parts traversent les murs, il faut faire un Raycast entre la part et le joueur. 
     Si le rayon touche un mur avant le joueur on stoppe la part.
]]

local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Exclude

RunService.Heartbeat:Connect(function(deltaTime)
	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

	-- Met à jour les exclusions à chaque frame (Player + tous les ennemis exclus)
	local excluded = { character }
	for enemy in pairs(enemiesData) do
		if enemy and enemy.Parent then
			table.insert(excluded, enemy)
		end
	end
	raycastParams.FilterDescendantsInstances = excluded

	for enemy, data in pairs(enemiesData) do
		if not enemy or not enemy.Parent then
			enemiesData[enemy] = nil
			continue
		end
		
		-- Calcul de la direction et de la distance entre l'ennemi et le joueur
		local toPlayer    = root.Position - enemy.Position
		local distance    = toPlayer.Magnitude
		local direction   = toPlayer.Unit

		-- Trop loin : la part attend
		if distance > data.detectionDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end

		-- Assez proche : arrêt
		if data.stopDistance > 0 and distance <= data.stopDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end

		-- Raycast entre l'ennemi et le joueur
		local rayResult = workspace:Raycast(enemy.Position, direction * distance, raycastParams)

		if rayResult then
			-- Un mur est détecté avant le joueur : on stoppe
			enemy.AssemblyLinearVelocity = Vector3.zero
			-- Optionnel : orienter quand même vers le joueur
			enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position)
		else
			-- Chemin libre : déplacement normal
			enemy.AssemblyLinearVelocity = direction * data.speed
			enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position) 
				+ direction * data.speed * deltaTime
		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

Exemple d’un code avec des ennemis qui ne peuvent pas traverser un mur, utilisation de Raycast :

-- recherche des services
local RunService = game:GetService("RunService")
local Players    = game:GetService("Players")
local Debris     = game:GetService("Debris")

-- Configuration
local DETECTION_DISTANCE = 60   -- distance de détection du joueur (studs)
local SPEED              = 3   -- vitesse de déplacement (studs/s)
local STOP_DISTANCE      = 2    -- distance à laquelle la part s'arrête

-- recherche des ennemis dans le workspace sous le folder "Monstre"
local enemies          = workspace.GAME:WaitForChild("Monstre")

-- Paramètres dynamiques par ennemi
local enemiesData = {}

-- Initialisation de tous les ennemis
for _, enemy in enemies:GetChildren() do

	-- contrôle si c'est une part qui représente un ennemi
	if not enemy:IsA("BasePart") then continue end

	enemy.Anchored = true

	-- Stocke les données en mémoire pour chaque ennemi
	enemiesData[enemy] = {
		detectionDistance = enemy:GetAttribute("DetectionDistance") or DETECTION_DISTANCE,
		speed = enemy:GetAttribute("Speed") or SPEED,
		stopDistance = enemy:GetAttribute("StopDistance") or STOP_DISTANCE,
		cooldown = false,
	}

	-- dégats sur le joueur si il touche l'ennemi'
	enemy.Touched:Connect(function(hit)
		local character = hit.Parent
		local humanoid  = character and character:FindFirstChildOfClass("Humanoid")
		if not humanoid or humanoid.Health <= 0 then return end
		if enemiesData[enemy].cooldown then return end
		enemiesData[enemy].cooldown = true
		humanoid:TakeDamage(10)
		task.delay(0.5, function() enemiesData[enemy].cooldown = false end)
	end)
end

--[[ Configuration du Raycast (exclure les ennemis eux-mêmes et le personnage)
     Pour éviter que les parts traversent les murs, il faut faire un Raycast entre la part et le joueur. 
     Si le rayon touche un mur avant le joueur on stoppe la part.
]]

local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Exclude

RunService.Heartbeat:Connect(function(deltaTime)
	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

	-- Met à jour les exclusions à chaque frame (Player + tous les ennemis exclus)
	local excluded = { character }
	for enemy in pairs(enemiesData) do
		if enemy and enemy.Parent then
			table.insert(excluded, enemy)
		end
	end
	raycastParams.FilterDescendantsInstances = excluded

	for enemy, data in pairs(enemiesData) do
		if not enemy or not enemy.Parent then
			enemiesData[enemy] = nil
			continue
		end
		
		-- Calcul de la direction et de la distance entre l'ennemi et le joueur
		local toPlayer    = root.Position - enemy.Position
		local distance    = toPlayer.Magnitude
		local direction   = toPlayer.Unit

		-- Trop loin : la part attend
		if distance > data.detectionDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end

		-- Assez proche : arrêt
		if data.stopDistance > 0 and distance <= data.stopDistance then
			enemy.AssemblyLinearVelocity = Vector3.zero
			continue
		end

		-- Raycast entre l'ennemi et le joueur
		local rayResult = workspace:Raycast(enemy.Position, direction * distance, raycastParams)

		if rayResult then
			-- Un mur est détecté avant le joueur : on stoppe
			enemy.AssemblyLinearVelocity = Vector3.zero
			-- Optionnel : orienter quand même vers le joueur
			enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position)
		else
			-- Chemin libre : déplacement normal
			enemy.AssemblyLinearVelocity = direction * data.speed
			enemy.CFrame = CFrame.lookAt(enemy.Position, root.Position) 
				+ direction * data.speed * deltaTime
		end
	end
end)