Hi there!
I’ve been trying to make some sort of an enemy AI that dodges at random intervals, making it harder to fight against.
However, I’ve recently ran into a problem where the enemy doesn’t move, but no error occurs.
Code:
local dist = 20
local refreshRate = 0.25
local tracks = {}
local debounce = false
local dodgeDebounce = false
local particleDebounce = false
local smokeDebounce = false -- for specific function
local soundDebounce = false
local currentBehaviour = "Idle"
local oldBehaviour = ""
local BaseDamage = 20
local DamageAmplifier = 1
local debounceTimerAmplifier = 1
local ForcefieldResistance = 2
local cooldownDuration = 0.2315
local dodgePower = 50000
local dodgePower2 = dodgePower / 7
local dodgeChance = 3
local SlashNumber = 1
local lastActionTime = 0
local resetTimeThreshold = 5
local introThread = nil
local idleThread = nil
local chaseThread = nil
local slashThread = nil
local isSlashing = false
local isDodging = false
local canSwitchAnims = false
local canDodge = true
local Chat = game:GetService("Chat")
local Players = game:GetService("Players")
local Debris = game:GetService("Debris")
local RunService = game:GetService("RunService")
local Enemy = script.Parent.Parent.Parent
local Humanoid = Enemy.Humanoid
local Torso = Enemy.Torso
local Animations = Enemy.Animations
local Sounds = Enemy.Sounds
local Particles = Enemy.Particles
local function playAnimation(animationName, humanoid, speed, loop)
local animation = Animations:FindFirstChild(animationName)
if animation then
if not humanoid or not animation then
return
end
task.synchronize()
local animationTrack = humanoid:LoadAnimation(animation)
if animationTrack then
animationTrack.Priority = Enum.AnimationPriority.Action
animationTrack:AdjustSpeed(speed)
animationTrack.Looped = loop
animationTrack:Play()
task.desynchronize()
table.insert(tracks, animationTrack)
end
end
end
local function stopAllAnimations()
coroutine.wrap(function()
for i = #tracks, 1, -1 do
local track = tracks[i]
if track.IsPlaying then
task.synchronize()
track:Stop()
task.desynchronize()
end
table.remove(tracks, i)
end
end)()
end
local function playParticle(particleName, handle, rate, lifetime)
if particleDebounce then return end
particleDebounce = true
local particle = Particles:FindFirstChild(particleName)
if particle then
task.synchronize()
local particleClone = particle:Clone()
particleClone.Parent = handle
particleClone:Emit(rate)
Debris:AddItem(particleClone, lifetime)
task.desynchronize()
end
task.delay(lifetime / 2, function()
particleDebounce = false
end)
end
local function playSound(soundName, handle)
if soundDebounce then return end
local sound = Sounds:FindFirstChild(soundName)
if sound and not soundDebounce then
task.synchronize()
soundDebounce = not soundDebounce
local soundClone = sound:Clone()
soundClone.Parent = handle
soundClone:Play()
Debris:AddItem(soundClone, soundClone.TimeLength)
task.wait(soundClone.TimeLength / 2)
soundDebounce = not soundDebounce
task.desynchronize()
end
end
local function createHitbox(enemy, waiter, handle, size, dmg)
task.delay(waiter, function()
task.synchronize()
local HitboxRegion = Region3.new(handle.Position - (size / 2), handle.Position + (size / 2))
local partsInHitbox = workspace:FindPartsInRegion3(HitboxRegion, nil, math.huge)
task.desynchronize()
local npcs = {}
for _, part in pairs(partsInHitbox) do
local hum = part.Parent:FindFirstChildOfClass("Humanoid")
task.synchronize()
if hum and part.Parent.Name ~= Enemy.Name and part.Parent.Name == Players:GetPlayerFromCharacter(part.Parent).Name and hum.Health > 0 and Humanoid.Health > 0 and part.Parent ~= nil then
table.insert(npcs, hum.Parent)
local hasForceField = hum.Parent:FindFirstChild("ForceField") ~= nil
local constantDamage = math.floor(dmg)
if hasForceField then
constantDamage = constantDamage / ForcefieldResistance
end
hum:TakeDamage(constantDamage)
playParticle("BloodHit", hum.Parent:FindFirstChild("Torso") or hum.Parent:FindFirstChild("UpperTorso"), 12.5, 1)
local hitSound = "Hit" .. SlashNumber
playSound(hitSound, Enemy.Sword.Handle)
task.synchronize()
if hasForceField then
hum.WalkSpeed = hum.WalkSpeed / (3 - (1.5 * (ForcefieldResistance - 0.25)))
else
hum.WalkSpeed = hum.WalkSpeed / (3 - 1.5)
end
hum.JumpPower = 0
task.wait(0.5)
if hasForceField then
hum.WalkSpeed = hum.WalkSpeed * (3 - (1.5 * (ForcefieldResistance - 0.25)))
else
hum.WalkSpeed = hum.WalkSpeed * (3 - 1.5)
end
hum.JumpPower = 50
break
end
end
task.desynchronize()
for i = #npcs, 1, -1 do
table.remove(npcs, i)
end
end)
end
local function findClosestPlayer()
local closestPlayer = nil
local closestDistance = math.huge
for _, player in pairs(Players:GetPlayers()) do
local character = player.Character
if character and character:FindFirstChild("HumanoidRootPart") and character:FindFirstChild("Humanoid") then
local distance = (character.HumanoidRootPart.Position - Enemy.HumanoidRootPart.Position).Magnitude
if distance < closestDistance then
closestDistance = distance
closestPlayer = player
end
end
end
return closestPlayer, closestDistance
end
local function ManipulateJump(Hum: Humanoid)
coroutine.wrap(function()
task.synchronize()
Hum.JumpPower = Hum.JumpPower / 4
Hum.Jump = true
task.wait(0.1)
Hum.JumpPower = Hum.JumpPower * 4
task.desynchronize()
end)()
end
local function CreateSmoke(part: BasePart)
if smokeDebounce then return end
coroutine.wrap(function()
task.synchronize()
smokeDebounce = not smokeDebounce
local Smoke = Instance.new("Smoke")
Smoke.Parent = part
Smoke.Enabled = true
task.wait(5/10)
Smoke.Enabled = false
Debris:AddItem(Smoke, 5)
task.wait(2.5)
smokeDebounce = not smokeDebounce
task.desynchronize()
end)()
end
local function Dodge()
if dodgeDebounce == true or isSlashing == true or isDodging == true or Humanoid.Health == 0 or canDodge == false then return end
if Humanoid and Torso and not dodgeDebounce then
coroutine.wrap(function()
task.synchronize()
isDodging = true
dodgeDebounce = true
local moveDirection = Humanoid.MoveDirection
local targetPosition
if moveDirection.Magnitude > 0 then
targetPosition = Torso.Position + (moveDirection * dodgePower) + Vector3.new(0, 35 * (dodgePower / 5000), 0)
else
local lookVector = Torso.CFrame.LookVector
targetPosition = Torso.Position + (lookVector * dodgePower) + Vector3.new(0, 35 * (dodgePower / 5000), 0)
end
ManipulateJump(Humanoid)
local BodyPosition = Instance.new("BodyPosition")
BodyPosition.Parent = Torso
BodyPosition.Position = targetPosition
BodyPosition.MaxForce = Vector3.new(4000 * (dodgePower2 / 5000), 4000 * (dodgePower2 / 5000), 4000 * (dodgePower2 / 5000))
BodyPosition.P = 100000 * (dodgePower / 5000)
coroutine.wrap(function()
playSound("Dodge", Torso)
playParticle("DodgeParticle", Torso, 100, 1.5)
CreateSmoke(Torso)
end)()
Debris:AddItem(BodyPosition, 0.1)
task.wait(2.5)
dodgeDebounce = false
isDodging = false
task.desynchronize()
end)()
end
end
local function Backflip()
if dodgeDebounce == true or isSlashing == true or isDodging == true or Humanoid.Health == 0 or canDodge == false then return end
if Humanoid and Torso and not dodgeDebounce then
coroutine.wrap(function()
task.synchronize()
isDodging = false
dodgeDebounce = false
local moveDirection = -Humanoid.MoveDirection
local targetPosition
if moveDirection.Magnitude > 0 then
targetPosition = Torso.Position + (moveDirection * dodgePower) + Vector3.new(0, 35 * (dodgePower / 5000), 0)
else
local lookVector = -Torso.CFrame.LookVector
targetPosition = Torso.Position + (lookVector * dodgePower) + Vector3.new(0, 35 * (dodgePower / 5000), 0)
end
ManipulateJump(Humanoid)
local BodyPosition = Instance.new("BodyPosition")
BodyPosition.Parent = Torso
BodyPosition.Position = targetPosition
BodyPosition.MaxForce = Vector3.new(4000 * (dodgePower2 / 5000), 4000 * (dodgePower2 / 5000), 4000 * (dodgePower2 / 5000))
BodyPosition.P = 100000 * (dodgePower / 5000)
coroutine.wrap(function()
playSound("Dodge", Torso)
playParticle("DodgeParticle", Torso, 100, 1.5)
CreateSmoke(Torso)
end)()
Debris:AddItem(BodyPosition, 0.1)
task.wait(2.5)
dodgeDebounce = false
isDodging = false
task.desynchronize()
end)()
end
end
local function AnimationHandler(behaviour: string)
if behaviour == "Idle" or behaviour == "Intro" then
stopAllAnimations()
playAnimation("Idle", Humanoid, 0.5, true)
elseif behaviour == "Chase" then
stopAllAnimations()
playAnimation("Walk", Humanoid, 1, true)
elseif behaviour:match("^Slash%d$") then
stopAllAnimations()
playAnimation(behaviour, Humanoid, 1, false)
elseif behaviour == "Freefall" then
stopAllAnimations()
playAnimation("Freefall", Humanoid, 1, true)
else
stopAllAnimations()
end
end
local function lookAtGyro(part1: BasePart, part2: BasePart)
coroutine.wrap(function()
local bodyGyro = Instance.new("BodyGyro")
bodyGyro.Parent = part1
bodyGyro.D = 450
bodyGyro.P = 120000
bodyGyro.MaxTorque = Vector3.one * 400000
local targetPosition = Vector3.new(part2.Position.X, part1.Position.Y, part2.Position.Z)
local thread = coroutine.create(function()
RunService.Heartbeat:ConnectParallel(function()
if Enemy.Humanoid.Health > 0 then
bodyGyro.CFrame = CFrame.new(part1.Position, targetPosition)
end
end)
end)
if Enemy.Humanoid.Health > 0 then
task.wait(0.25)
coroutine.close(thread)
bodyGyro:Destroy()
else
coroutine.close(thread)
bodyGyro:Destroy()
end
end)()
end
local function resetSlashNumber()
SlashNumber = 1
lastActionTime = tick()
end
local function AIHandler()
idleThread = coroutine.create(function()
Humanoid:MoveTo(Enemy.Torso.Position)
while true do
if currentBehaviour == "Idle" then
local closestPlayer, closestDistance = findClosestPlayer()
if closestPlayer and closestDistance <= dist and closestPlayer.Character.Humanoid.Health > 0 then
currentBehaviour = "Intro"
canDodge = true
end
end
task.wait(refreshRate)
end
end)
introThread = coroutine.create(function()
while true do
if currentBehaviour == "Intro" then
Chat:Chat(Enemy.Head, "die.")
currentBehaviour = "Chase"
end
task.wait(refreshRate)
end
end)
chaseThread = coroutine.create(function()
while true do
if currentBehaviour == "Chase" then
local closestPlayer, closestDistance = findClosestPlayer()
if closestPlayer then
local character = closestPlayer.Character
if character and character:FindFirstChild("Humanoid") and character.Humanoid.Health > 0 then
Humanoid:MoveTo(character.HumanoidRootPart.Position)
canDodge = true
else
currentBehaviour = "Idle"
canDodge = true
end
if closestDistance > dist * 1.5 then
Dodge()
canDodge = true
elseif closestDistance > dist * 4 then
currentBehaviour = "Idle"
canDodge = false
elseif closestDistance < dist / 3.45 then
canDodge = false
local Chance = math.random(1, dodgeChance*2)
if Chance == dodgeChance then
coroutine.wrap(function()
canDodge = true
Backflip()
task.wait(0.5)
canDodge = false
end)()
else
currentBehaviour = "Slash" .. SlashNumber
end
end
else
currentBehaviour = "Idle"
canDodge = true
end
end
task.wait(0.25)
end
end)
slashThread = coroutine.create(function()
if isDodging or Humanoid.Health == 0 then return end
while true do
if currentBehaviour:match("^Slash%d$") and Enemy.Sword.Parent ~= nil and Humanoid.Health > 0 then
local closestPlayer, closestDistance = findClosestPlayer()
if closestPlayer then
local character = closestPlayer.Character
if character and character:FindFirstChild("Humanoid") and character.Humanoid.Health > 0 then
local Mag = (character.HumanoidRootPart.Position - Enemy.HumanoidRootPart.Position).Magnitude
local targetPosition = Vector3.new(closestPlayer.Character.Torso.Position.X, Enemy.HumanoidRootPart.Position.Y, closestPlayer.Character.Torso.Position.Z)
if debounce then return end
task.synchronize()
Humanoid:MoveTo(character.HumanoidRootPart.Position)
Enemy.HumanoidRootPart.CFrame = CFrame.new(Enemy.HumanoidRootPart.Position, targetPosition)
task.desynchronize()
local currentTime = tick()
if currentTime - lastActionTime < cooldownDuration then
return
end
debounce = true
lastActionTime = currentTime
isSlashing = true
stopAllAnimations()
if SlashNumber == 1 then
DamageAmplifier = 0.75
debounceTimerAmplifier = 1
elseif SlashNumber == 2 then
DamageAmplifier = 1
debounceTimerAmplifier = 1
elseif SlashNumber == 3 then
DamageAmplifier = 1.25
debounceTimerAmplifier = 1.5
end
playAnimation(tostring("Slash" .. SlashNumber), Humanoid, 0.5, false)
playSound(tostring("Swing" .. SlashNumber), Enemy.Sword.Handle)
createHitbox(Humanoid, 0.3, Enemy.Sword.Handle, Enemy.Sword.Handle.Size * 2.5, BaseDamage * DamageAmplifier)
task.wait(cooldownDuration * debounceTimerAmplifier)
debounce = false
isSlashing = false
SlashNumber = SlashNumber % 3 + 1
if Mag > dist * 4 or character.Humanoid.Health <= 0 then
currentBehaviour = "Idle"
else
currentBehaviour = "Chase"
end
else
currentBehaviour = "Idle"
end
else
currentBehaviour = "Idle"
end
end
task.wait(refreshRate)
end
end)
coroutine.resume(introThread)
coroutine.resume(idleThread)
coroutine.resume(chaseThread)
coroutine.resume(slashThread)
end
local function AnimationHandlerSystem()
coroutine.wrap(function()
while not canSwitchAnims do
if oldBehaviour ~= currentBehaviour then
canSwitchAnims = not canSwitchAnims
oldBehaviour = currentBehaviour
end
if canSwitchAnims then
AnimationHandler(currentBehaviour)
canSwitchAnims = not canSwitchAnims
end
task.wait(refreshRate)
end
end)()
end
local function EnemySystem()
while Humanoid.Health > 0 do
AIHandler()
AnimationHandlerSystem()
task.wait(refreshRate)
if Humanoid.Health <= 0 then
currentBehaviour = "" -- Dead state
AnimationHandlerSystem()
end
end
if currentBehaviour == "" then
task.synchronize()
Enemy.Sword:Destroy()
task.desynchronize()
coroutine.close(idleThread)
coroutine.close(chaseThread)
coroutine.close(slashThread)
end
end
task.desynchronize()
EnemySystem()
Of course, it uses parallel lua for performance reasons.
The SignalBehaviour is also deferred (tho will be default because of the 3d skybox).
I’ve tried tracking it down, but to no avail.
I don’t know if it has something to do with the way I manage states or if it’s just how it is, but I don’t want a hacky way of solving this (if it isn’t absolutely necessary).
Feel free to improve the code also, as this is the first time I’ve used parallel lua for this.
Thank you!