-
I want to make an enemy pathfind to a player following a specific track using pathfinding for a tower defense game. The paths are curved (like in kingdom rush) and the player is at the end of the path. When the player dies it’s game over.
-
The problem I have is that the enemy does not pathfind along the track. Instead it finds the shortes way to the player and I don’t know how to make it follow the track.
- I’ve tried using the enemies floor material and Region3 to determine whether it is on the track or not but I’m not sure what to do after that. Should I also try raycasting the enemies position too?
The track is made using EgoMoose’s draw class to create the path
The code I’m using to controll the enemies:
-- This uses collection service to gather all the enemies
local CollectionService = game:GetService("CollectionService")
-- Table used to keep track of all alive enemies
local enemies = {}
local rs = game:GetService("ReplicatedStorage")
-- Raycast hitbox used for enemy attacks
local RaycastHitbox = require(rs.Modules:WaitForChild("RaycastHitbox"))
-- Find the player in game.Players
function findPlayer(target)
for index, plr in pairs(game.Players:GetPlayers()) do
if plr.Name == target.Parent.Name then
return plr
end
end
end
-- Calculating The players damage based on their armor defense
function CalculatePlayerDamage(player, min, max)
local defense = 0
if game.ReplicatedStorage.Armor:FindFirstChild(player.Stats.CurrentArmor.Value) then
defense = game.ReplicatedStorage.Armor[player.Stats.CurrentArmor.Value].Defense.Value
end
return math.clamp(math.random(min, max)-defense, 1, math.huge)
end
-- Squirts blood effects at a specific location
function squirtBlood(location)
for i = 1, math.random(1,5) do
local b = Instance.new("Part")
b.CanCollide = false
b.Shape = Enum.PartType.Ball
b.Size = Vector3.new(math.random(5)/10,0,0)
local bloodColors = {"Bright red","Really red","Crimson","Maroon"}
b.BrickColor = BrickColor.new(bloodColors[math.random(#bloodColors)])
b.Velocity = Vector3.new(math.random(-30,30),math.random(20,30),math.random(-30,30))
b.CFrame = CFrame.new(location)
b.Parent = game.Workspace.Debris
game:GetService("Debris"):AddItem(b,1)
end
end
--Used to create new threads easily while avoid spawn()
function spawner(func,...)
local co = coroutine.wrap(func)
co(...)
end
--------------------------
----Enemy AI Handling----
--------------------------
--Simple function for getting the distance between 2 points
function checkDist(part1,part2)
if typeof(part1) ~= Vector3 then part1 = part1.Position end
if typeof(part2) ~= Vector3 then part2 = part2.Position end
return (part1 - part2).Magnitude
end
--Loops through the human tag to find the closest valid target
function updateTarget()
local humans = CollectionService:GetTagged("Human")
for _,v in pairs(enemies) do
local target = nil
local dist = v.Settings.ChaseRange
for _,human in pairs(humans) do
local root = human.RootPart
if root and human.Health > 0 and checkDist(root,v.root) < dist and human.Parent.Name ~= v.char.Name then
dist = checkDist(root,v.root)
target = root
end
end
v.target = target
end
end
--Target updating
spawner(function()
while wait(1) do
updateTarget()
end
end)
--Called to have the enemy path towards it's current target
function pathToTarget(enemy)
local path = game:GetService("PathfindingService"):CreatePath()
path:ComputeAsync(enemy.root.Position,enemy.target.Position)
local waypoints = path:GetWaypoints()
local currentTarget = enemy.target
for i,v in pairs(waypoints) do
if v.Action == Enum.PathWaypointAction.Jump then
enemy.human.Jump = true
else
enemy.human:MoveTo(v.Position)
spawner(function()
wait(0.5)
if enemy.human.WalkToPoint.Y > enemy.root.Position.Y then
enemy.human.Jump = true
end
end)
enemy.human.MoveToFinished:Wait()
if not enemy.target then
break
elseif checkDist(currentTarget,waypoints[#waypoints]) > 10 or currentTarget ~= enemy.target then
pathToTarget(enemy)
break
end
end
end
end
--Simple loop to handle the pathfinding function
function movementHandler(enemy)
while wait(1) do
if enemy.human.Health <= 0 then
break
end
if enemy.target then
pathToTarget(enemy)
end
end
end
-- Enemy attack function to be optimized some day
function attack(enemy)
local human = enemy.target.Parent.Humanoid
if human.Health <= 0 then return end
local plr = findPlayer(enemy.target)
local Hitbox = RaycastHitbox:Initialize(enemy.char, {enemy.char, enemy.root, enemy.Human})
local Damage = CalculatePlayerDamage(plr, enemy.Settings.MinimumDamage, enemy.Settings.MaximumDamage)
enemy.attackAnim:Play()
enemy.AttackSound:Play()
squirtBlood(enemy.char.RightHand:WaitForChild('DmgPoint').WorldPosition)
squirtBlood(enemy.char.LeftHand:WaitForChild('DmgPoint').WorldPosition)
Hitbox:HitStart(Damage,'EnemyHumanoid')
wait(enemy.Settings.AttackCooldown)
Hitbox:HitStop()
end
-- Check if enemy is close enough to attack
spawner(function()
while wait(0.5) do
for _,v in pairs(enemies) do
if v.target then
if checkDist(v.target,v.root) < 8 then
attack(v)
end
end
end
end
end)
-------------------------------
----Enemy Table Management----
-------------------------------
--Simple function to check instances for humanoid then tag them if found
function tagHuman(instance)
local human = instance:FindFirstChildWhichIsA("Humanoid")
if human then
CollectionService:AddTag(human,"Human")
end
end
--Respawning that is tied the to the .Died event
function removeEnemy(enemy)
local index = table.find(enemies,enemy)
table.remove(enemies,index)
end
--Adds Enemies to our enemies table, sets up respawning,
--and spawns a pathing thread for each enemy.
function addEnemy(enemyHumanoid)
-- Enemy Values
table.insert(enemies,{
char = enemyHumanoid.Parent,
root = enemyHumanoid.RootPart,
human = enemyHumanoid,
target = nil,
attackAnim = enemyHumanoid:LoadAnimation(enemyHumanoid.Parent.attackAnim),
AttackSound = enemyHumanoid.Parent.Head.Attack,
Settings = require(game.ReplicatedStorage.EnemyModules[enemyHumanoid.Parent.Name])
})
for _,enemy in pairs(enemies) do
if enemy.human == enemyHumanoid then
enemy.human.Died:Connect(function() removeEnemy(enemy) end)
for i,v in pairs(enemy.char:GetDescendants()) do
if v:IsA("BasePart") and v:CanSetNetworkOwnership() then
v:SetNetworkOwner(nil)
end
end
spawner(movementHandler,enemy)
break
end
end
end
--Checking each object in the workspace for a humanoid as it enters/respawns
workspace.ChildAdded:Connect(tagHuman)
--Whenever something is tagged as a enemy then we add it to our table of alive enemies
CollectionService:GetInstanceAddedSignal("Enemy"):Connect(function(enemyHumanoid)
addEnemy(enemyHumanoid)
end)
--Ran one time to add all current enemies in the workspace on run
function intialize()
for _,v in pairs(CollectionService:GetTagged("Enemy")) do
local found = false
for _,x in pairs(enemies) do
if x.human == v then
found = true
end
end
if not found then
addEnemy(v)
end
end
for i,v in pairs(workspace:GetChildren()) do
tagHuman(v)
end
end
intialize()
If I made any mistakes in the code or if its not optimized then do tell me. Any example scripts would be greatly appreciated and thanks for the help in advance.