I’ve been trying my hand at enemy AI recently with FSMs and I have a decent result. It gets the job done but I have some issues and things I would like to improve that my pea brain can’t figure out. Here is a demo video.
As you can see, there are some things I can improve. The movement is choppy, I handle cooldowns really poorly in my spaghetti code and the fight just feels too unpredictable.
Please give me advice, coding techniques, resources, anything that you use to perfect your enemy AI. Any help is appreciated since I’m still not a great programmer haha
Here is the code.
local RunService = game:GetService("RunService")
local RS = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local damageEvent = RS:WaitForChild("Events"):FindFirstChild("DamagePlayer")
local cannonProj = RS:WaitForChild("Projectiles"):FindFirstChild("CannonArmPart")
local humanoid = script.Parent
local root = humanoid.Parent.PrimaryPart
local targetDistance = script:GetAttribute("TargetDistance")
local stopDistance = script:GetAttribute("StopDistance")
local damage = script:GetAttribute("Damage")
local attackDistance = script:GetAttribute("AttackDistance")
local attackDistance2 = script:GetAttribute("AttackDistance2")
local attackWait = script:GetAttribute("AttackWait")
local attackCooldown = script:GetAttribute("AttackCooldown")
local lastAttack = tick()
local originalSpeed = humanoid.WalkSpeed
local tickSpeed = 0.1
local state = {}
local currentState = nil
local attacking = false
blade = humanoid.Parent.Cutlass.Blade
local attackSound = blade.SwingSound
local t = {}
blade.Touched:Connect(function(hit)
if attacking then
local player = game.Players:GetPlayerFromCharacter(hit.Parent)
if player and player.Character then
if hit == player.Character.HumanoidRootPart and table.find(t, hit.Parent) == nil then
damageEvent:Fire(player.Character.Humanoid, damage)
table.insert(t, hit.Parent)
end
end
end
end)
function performSwordAttack()
attacking = true
local anim = script.Parent.Parent.Swing
humanoid = script.Parent
local track = humanoid:LoadAnimation(anim)
attackSound = blade.SwingSound
attackSound:Play()
track:Play()
track.Stopped:Wait()
attacking = false
humanoid:Move(Vector3.new())
end
function performCannonAttack()
humanoid.WalkSpeed = 0
local anim = script.Parent.Parent.Cannon
humanoid = script.Parent
local track = humanoid:LoadAnimation(anim)
track:Play()
delay (.2, function()
attackSound = blade.CannonSound
attackSound:Play()
local projectile = cannonProj:Clone()
local spawnPart = humanoid.Parent["Left Arm"].SpawnPart
local nearestPlayer, distance, direction = findNearestPlayer()
projectile.Parent = game.Workspace
projectile.Position = spawnPart.CFrame.Position + spawnPart.CFrame.LookVector * 1
print(nearestPlayer)
projectile.CFrame = CFrame.lookAt(projectile.Position, nearestPlayer.Character:FindFirstChild("HumanoidRootPart").Position)
RunService.Heartbeat:Connect(function()
projectile.Position = projectile.Position + projectile.CFrame.LookVector * 1
end)
end)
end
function state.Chasing()
local nearestPlayer, distance, direction = findNearestPlayer()
if nearestPlayer then
if distance <= targetDistance and distance >= attackDistance then
humanoid:Move(direction)
elseif distance >= targetDistance then
currentState = state.Wandering
humanoid:Move(Vector3.new())
elseif distance <= attackDistance and tick() - lastAttack >= attackCooldown then
currentState = state.Attacking
elseif distance <= attackDistance and not ((tick() - lastAttack) >= attackCooldown) then
humanoid:Move(direction)
end
end
function state.Attacking()
local chosenAttack = nil
local nearestPlayer, distance, direction = findNearestPlayer()
if distance <= attackDistance then
if distance <= attackDistance2 then
chosenAttack = math.random(2,10)
else
chosenAttack = math.random(1,4)
end
lastAttack = tick()
local humRootPart = script.Parent.Parent.HumanoidRootPart
local charPos = nearestPlayer.Character.HumanoidRootPart.Position
local pos = CFrame.lookAt(humRootPart.Position, Vector3.new(charPos.X, humRootPart.Position.Y, charPos.Z))
humRootPart.CFrame = pos
if chosenAttack <= 4 and chosenAttack > 1 then
performCannonAttack()
elseif chosenAttack == 1 then
currentState = state.Chasing
return
else
performSwordAttack()
end
wait(attackWait)
table.clear(t)
humanoid.WalkSpeed = originalSpeed
else
currentState = state.Chasing
end
end
end
function state.Wandering()
local nearestPlayer, distance, direction = findNearestPlayer()
if nearestPlayer then
if distance <= targetDistance then
currentState = state.Chasing
end
end
end
function findNearestPlayer()
local playerList = Players:GetPlayers()
local nearestPlayer = nil
local distance = nil
local direction = nil
for _, player in pairs(playerList) do
local character = player.Character
if character then
local distanceVector = (player.Character.HumanoidRootPart.Position - root.Position)
if not nearestPlayer then
nearestPlayer = player
distance = distanceVector.Magnitude
direction = distanceVector.Unit
elseif distanceVector.Magnitude < distance then
nearestPlayer = player
distance = distanceVector.Magnitude
direction = distanceVector.Unit
end
end
end
return nearestPlayer, distance, direction
end
currentState = state.Wandering
while true do
currentState()
task.wait(tickSpeed)
end