lua3drobloxluauroblox-studio

Roblox Knockback Skipping Waypoint


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!


Solution

  • 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 :

    Description Diagram
    Consider the path ABC:
    - A is the spawn point, and C is the end point,
    - the distance from A to B is 10 studs,
    - the distance from B to C is 15.
    A simple diagram with three points labelled "A", "B", and "C". There are lines connecting "A" to "B", and "B" to "C".
    Let's imagine we have an enemy marching from B to C, and they are 3 studs past B... The same diagram as before, now there is a red dot representing an enemy. It rests along the line between "B" and "C"
    When the enemy is knocked back 5 studs...
    - they should be sent back the 3 studs to B,
    - then the remaining 2 studs should send them towards A.
    The enemy dot has been moved onto the line between A and B

    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)