I am creating a knockback script for a Roblox tower defense game that is meant to teleport an enemy backwards toward another part by 5 studs. The knockback function works for this purpose, but after being pushed back, the enemy skips the waypoint it is moving towards. Enemies in this game move along the track by moving from waypoint blocks to waypoint blocks. When the knockback occurs, the enemy moves diagonally off the track toward the next waypoint instead of the one it is supposed to move to.
So far, I have attempted to use a BindableEvent to set the enemy's waypoint target back by one, but it is having no effect on its corner-cutting movement. Here is the code for my knockback script:
task.wait(10)
local target = game:GetService("Workspace")["Live Enemies"]["Handcuff Arrest Officer"]
local waypoint: Part = nil
if target.EnemyInfo.Waypoint.Value == 0 then
waypoint = game:GetService("Workspace").SpawnPoint
else
waypoint = game:GetService("Workspace").Waypoints[target.EnemyInfo.Waypoint.Value]
end
local studsToMove = 5
-- Make sure the model has a PrimaryPart set
if target.PrimaryPart then
-- Calculate direction from target's PrimaryPart to waypoint
local direction = (waypoint.Position - target.PrimaryPart.Position).unit
-- Calculate the new position, moving the PrimaryPart 5 studs in the direction of the waypoint
local newPosition = target.PrimaryPart.Position + direction * studsToMove
-- Offset the entire model by the same movement, preserving its orientation
target.Knockback:Fire()
target:SetPrimaryPartCFrame(target.PrimaryPart.CFrame + (direction * studsToMove))
end
Additionally, here is the code for the enemy movement script. It uses a no-timeout version of humanoid:MoveTo()
from https://create.roblox.com/docs/reference/engine/classes/Humanoid.
-- In case the enemy dies before this script can run, pcall blocks errors.
pcall(function(...)
-- Constants
local humanoid = script.Parent.Humanoid
local info = script.Parent.EnemyInfo
local waypoints = game:GetService("Workspace").Waypoints
local waypoints_hit = 0 -- Not constant, waypoint counter
local knockback = script.Parent.Knockback
-- MoveTo() without timeout
local function moveTo(humanoid, targetPoint)
local targetReached = false
local connection
connection = humanoid.MoveToFinished:Connect(function(reached)
targetReached = true
print("Reached target "..tostring(targetPoint))
connection:Disconnect()
connection = nil
end)
humanoid:MoveTo(targetPoint)
while not targetReached do
if not (humanoid and humanoid.Parent) then
break
end
if humanoid.WalkToPoint ~= targetPoint then
break
end
humanoid:MoveTo(targetPoint)
task.wait(0.1)
end
if connection then
connection:Disconnect()
connection = nil
end
end
knockback.Event:Connect(function(...: any)
print("Fired")
waypoints_hit -= 1
script.Parent.EnemyInfo.Waypoint.Value = tostring(waypoints_hit)
if waypoints_hit == 0 then
moveTo(humanoid, game:GetService("Workspace").SpawnPoint.Position)
else
moveTo(humanoid, game:GetService("Workspace").Waypoints[waypoints_hit].Position)
end
end)
-- Enemy configurations
local damage: number = info.Damage.Value -- Damage to base
local hp: number = info.HP.Value -- Hit points
local speed: number = info.Speed.Value -- Speed
-- Set HP and speed
humanoid.MaxHealth = hp
humanoid.Health = hp
humanoid.WalkSpeed = speed
-- Move
for i,v in pairs(waypoints:GetChildren()) do
moveTo(humanoid, v.Position)
waypoints_hit += 1
script.Parent.EnemyInfo.Waypoint.Value = tostring(waypoints_hit)
if waypoints_hit >= 8 then
game:GetService("Workspace")["Match Config"].BaseHP.Value -= damage
script.Parent:Destroy()
end
end
end)
I found out that humanoid:MoveTo()
stops when the root part's CFrame is changed, which is what I am doing to facilitate knockback. However, I'm not sure how to get around this limitation, which I need to do for the knockback feature. Any help would be appreciated!
Your code does a good job setting up a list of waypoints for your enemy to walk along, and you already have the foundation for changing targets. But the logic that decides when to switch waypoints just needs to be a bit smarter.
So let's walk through the way a knockback along a path should work :
So one way to represent this process is with simple distance calculations. You subtract the distance from the enemy to their last waypoint until the distance to the next waypoint is greater than the remaining knockback, then just place the enemy along the points again. If you make it back to the beginning, just escape and set the enemy back at the spawn point
So your knockback logic needs to know the following information :
Let's make a ModuleScript
to handle your Enemy logic. The updated knockback logic will live in the Enemy:OnKnockback()
function, which you can trigger however you like later.
local Enemy = {}
Enemy.__index = Enemy
function Enemy.new(waypoints : { BasePart }, damage : number, speed : number, health : number, modelTemplate : Model)
assert(#waypoints >= 2, "List of waypoints must be >= 2")
local self = {
waypoints = waypoints,
currentWaypoint = 2, -- assume that 1 is the spawn, so the first target is 2
damage = damage,
speed = speed,
health = health,
modelTemplate = modelTemplate,
moveToConnection = nil, -- for now
modelRef = nil, -- for now
}
setmetatable(self, Enemy)
return self
end
function Enemy:Spawn()
local newModel = self.modelTemplate:Clone()
newModel.Humanoid.Health = self.health
newModel.Humanoid.MaxHealth = self.health
newModel.Humanoid.WalkSpeed = self.speed
newModel:PivotTo(self.waypoints[1].CFrame)
newModel.Parent = game.Workspace
self.modelRef = newModel
end
function Enemy:Walk()
if (self.moveToConnection) then
self.moveToConnection:Disconnect()
end
local waypointTarget = self.waypoints[self.currentWaypoint].Position
local humanoid : Humanoid = self.modelRef.Humanoid
self.moveToConnection = humanoid.MoveToFinished:Connect(function(didReach : boolean)
if not didReach then
warn("Failed to reach waypoint for some reason, figure out why!")
end
self.currentWaypoint += 1
-- if there are no more waypoints, we've made it to the end!
if self.currentWaypoint > #self.waypoints then
print(string.format("Enemy should deal %d damage!", self.damage))
return
end
-- walk towards the next waypoint
self:Walk()
end)
self.modelRef.Humanoid:MoveTo(waypointTarget)
end
function Enemy:SetPosition(newPosition : Vector3)
local waypointTarget = self.waypoints[self.currentWaypoint].Position
local newPivot = CFrame.new(newPosition, waypointTarget)
self.modelRef:PivotTo(newPivot)
-- tell the enemy to start moving towards its next waypoint again.
self:Walk()
end
function Enemy:OnKnockback(knockbackAmount : number)
-- take snapshots of values while we calculate
local waypoints = self.waypoints
local currentWaypoint = self.currentWaypoint
local model : Model = self.modelRef
local position : Vector3 = model:GetPivot().Position
-- loop as many times as needed, but escape if we're back at the start
while (currentWaypoint > 1) do
-- calculate how far it is from the enemy to their previous waypoint
local distToLastWaypoint : Vector3 = (position - waypoints[currentWaypoint - 1].Position)
local progress = distToLastWaypoint.Magnitude
-- if the distance is greater than the knockback, translate the enemy back towards their previous waypoint
if (progress >= knockbackAmount) then
-- reassign values
self.currentWaypoint = currentWaypoint
-- move to the new position
local newPosition = position - (distToLastWaypoint.Unit * knockbackAmount)
self:SetPosition(newPosition)
-- we're done here, escape the loop
return
end
-- subtract their progress from the total knockback amount
knockbackAmount -= progress
currentWaypoint -= 1
-- adjust the position to the last waypoint, and loop as many times as necessary
position = self.waypoints[currentWaypoint].Position
end
-- if we're here, we got pushed all the way back to the beginning
-- set the position, and start the moveTo again
self.currentWaypoint = 2
local newPosition = waypoints[1].Position
self:SetPosition(newPosition)
end
return Enemy
Then, in your game logic Script
you can use it like :
-- services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace") -- not really needed
-- modules
local Enemy = require(ReplicatedStorage.Enemy) -- or wherever you put it
-- fetch the officer model from ReplicatedStorage, and configure some values
local enemyModelTemplate = ReplicatedStorage.Enemies["Handcuff Arrest Officer"]
local health = 10
local speed = 14 -- max studs / second
local damage = 10
local wps = Workspace.Waypoints
local waypoints = { wps["1"], wps["2"], wps["3"] } -- add as many as needed
-- create the enemy
local officerEnemy = Enemy.new(waypoints, damage, speed, health, enemyModelTemplate)
officerEnemy:Spawn()
officerEnemy:Walk()
-- test out the knockback
wait(10.0)
local studsToMove = 5
officerEnemy:OnKnockback(studsToMove)