I am creating a tower defense game and can detect which enemy is closest to a tower, but I want the tower to attack the enemy closest to the end (or the first enemy).
Spawn time will not work as some enemies are faster than others, and overtake them.
What would be a good system/feature to achieve this? I was thinking an array/table could work.
Idea:
You got the path from the start to the end right? So if you calculate the total distance the enemy will do (basically add up each part of the path’s size) you get the total distance, and then you can make each enemy have an IntValue (or NumberValue) and it’s value will be equal to the distance they already did.
Then, your tower should check which enemy has the highest value.
I made a system where it works the other way around: instead of having the mobs move along the path and then calculating how far along it they’ve moved at any given point, it keeps track of their “path offset” and calculates the actual position based on that. That way it’s trivially easy to compare two mobs to see which one is further along the path.
Here’s the relevant code, although it’s very unfinished. Might want to return an object from setupPathFollower and put them all in a table.
--PathFollowerManager
local TagS = game:GetService("CollectionService")
local RunS = game:GetService("RunService")
local InputS = game:GetService("UserInputService")
function getPathNodes(path)
local nodes = {}
local nodeDists = {}
local nextNode = path.FirstNode.Value
local cumulativeDist = 0
while nextNode do
table.insert(nodes, nextNode)
nodeDists[nextNode] = cumulativeDist
local prevNode = nextNode
nextNode = nextNode.Next.Value
if nextNode then
cumulativeDist += (nextNode.Position - prevNode.Position).Magnitude
end
end
return nodes, nodeDists, cumulativeDist
end
function getPathOffsetPoint(nodes, nodeDists, pathLength, offset): Vector3
if #nodes == 1 then
return nodes[1].Position
end
if offset >= pathLength then
return getPathOffsetPoint(nodeDists, pathLength, offset % pathLength)
end
local prevNodeI, nextNodeI = 1, 2
while offset > nodeDists[nodes[nextNodeI]] do
prevNodeI += 1
nextNodeI += 1
end
local segmentDiff = nodes[nextNodeI].Position - nodes[prevNodeI].Position
return nodes[prevNodeI].Position + segmentDiff.Unit * (offset - nodeDists[nodes[prevNodeI]])
end
function getPathUnitOffsetPoint(nodes, nodeDists, pathLength, offset): Vector3
error("Not yet implemented!")
end
function findGroundPointUnderPoint(point: Vector3): Vector3
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Whitelist
params.FilterDescendantsInstances = {game.Workspace.Baseplate}
local result = game.Workspace:Raycast(
point,
Vector3.FromNormalId(Enum.NormalId.Bottom) * 5000,
params
)
if result then
return result.Position
end
return point
end
function setupPathFollower(pathFollower)
local path
local pathNodes
local pathNodeDistances
local pathLength
local offset = 0
RunS.Heartbeat:Connect(function(dt)
if path ~= pathFollower.Path.Value then
path = pathFollower.Path.Value
pathNodes, pathNodeDistances, pathLength = getPathNodes(path)
end
if #pathNodes > 0 then
offset += 10 * dt
offset = offset % pathLength
local offsetPoint = getPathOffsetPoint(pathNodes, pathNodeDistances, pathLength, offset)
local groundPoint = findGroundPointUnderPoint(offsetPoint)
pathFollower:SetPrimaryPartCFrame(CFrame.new() + groundPoint)
end
end)
end
table.foreachi(TagS:GetTagged("PathFollower"), function(_, v) setupPathFollower(v) end)
TagS:GetInstanceAddedSignal("PathFollower"):Connect(setupPathFollower)
Hi, I am not the best scripter but I am trying hard to get better. Is this basically just getting the length between path nodes and adding them up depending on the mobs position? And would this allow me to find the 1st mob in the tower’s range?
If you want a really easy way to determine this, you can put parts along the path and trigger whenever the first enemy touches it and update the towers current target. It would probably look something like this
local updateTargets = game:GetService("ServerStorage"):WaitForChild("UpdateTargets") -- bindable event to update the towers targets
local detectionParts = workspace:WaitForChild("DetectionParts"):GetChildren() -- a group or folder containing the detection parts
function onDetection (model)
updateTargets:Fire(model)
end
function addEvent ()
for i,v in pairs(detectionParts) do
v.Touched:Connect(function(hit)
if hit.Parent:FindFirstChild("Humanoid") --[[ you should probably change this to something only the enemies exclusively have ]] and hit.Parent:FindFirstChild("alreadyTouched") == false --[[ bool value to ensure that the part hasn't been touched anymore ]] not game.Players:GetPlayerFromCharacter(hit.Parent) and not game.Players:GetPlayerFromCharacter(hit.Parent.Parent) then -- ensure it's an enemy and not a player, you should probably edit this
onDetection(hit.Parent) -- send the model over
hit.Parent.alreadyTouched.Value = true
end
end)
end
end
addEvent()
-- then in your enemy script
local updateTargets = game:GetService("ServerStorage"):WaitForChild("UpdateTargets")
local currentTarget
updateTargets.Event:Connect(function(model)
currentTarget = model -- update target
end)
if you don’t feel comfortable using .Touched events, you can loop raycasts, although that’s going to be more costly on performance than you would probably like.
I’d do a for loop that scans each enemy. first, check it’s distance from the tower. if the distance is lower than what you want, you can then see if it is the farthest.
If you’re using the .Touched event it won’t cause much lag at all, so you can place the parts close to each other or far apart depending on how fast you want towers to react to the first enemy. Although I recommend adding debounce to the .Touched event to prevent the event firing multiple times.
Maybe a 0.1 second debounce, considering towers wont be attacking at such a fast speed. Again I am not the best of scripters but I will attempt to refine the code and implement it into my game. This is actually the best solution I have heard to this problem yet.