I am doing AI for my npc killer using State machines, but i ran into the classic issue where it stutters behind the player
What solutions have you tried so far? Did you look for solutions on the Creator Hub?
Yes, every single post related to this, But none of the solutions worked, i tried
- getting the Target rootpart from the client in a renderstepped
- Setting the second argument of MoveTo()
- Setting networkship owner to nil (both one time, and in a loop, and even setting all the baseparts)
- setting networkship owner to the player (both one time, and in a loop)
- doing MoveTo() from the client, by sending a remoteevent to the client, so the client can move it
- using both workspace:GetServerTimeNow() and player:GetNetworkPing() to predict the pos as an offset (didn’t work)
I haven’t implemented pathfinding, this is just simple moveto, but i see it doesn’t work
My Chase state :
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RobloxStateMachine = require(ReplicatedStorage.RobloxStateMachine)
local PlayerPos = require("@game/ReplicatedStorage/PlayerPos")
local Chase = RobloxStateMachine.State.new("Chase")
local function hasLineOfSight(fromPos, toPos, ignoreList)
local direction = toPos - fromPos
local params = RaycastParams.new()
params.FilterDescendantsInstances = ignoreList
params.FilterType = Enum.RaycastFilterType.Exclude
return workspace:Raycast(fromPos, direction, params) == nil
end
local function getPlayerHRP(player)
local char = player and player.Character
if not char then return nil end
return char:FindFirstChild("HumanoidRootPart")
end
function Chase:OnEnter(data)
data.humanoid.WalkSpeed = data.chaseSpeed
data.moveTimer = 0
data.hasLoS = true
data.inRange = true
data.graceTimer = data.graceTime
--data.rootPart:SetNetworkOwner(data.targetPlayer)
print("[KillerAI] Chase -> targeting", data.targetPlayer and data.targetPlayer.Name)
end
function Chase:OnHeartbeat(data, dt)
local player = data.targetPlayer
if not player then
data.hasLoS = false
data.inRange = false
return
end
local hrp = getPlayerHRP(player)
if not hrp then
data.hasLoS = false
data.inRange = false
return
end
local rootPos = data.rootPart.Position
local playerPos = hrp.Position
local dist = (playerPos - rootPos).Magnitude
data.inRange = dist <= data.detectionRange
local ignoreList = { data.killer, player.Character }
if hasLineOfSight(rootPos, playerPos, ignoreList) then
data.hasLoS = true
data.lastKnownPos = playerPos
data.graceTimer = data.graceTime
else
data.hasLoS = false
-- Out of range AND no LoS
local drainRate = data.inRange and 1 or 3
data.graceTimer = math.max(0, data.graceTimer - dt * drainRate)
end
local playerPos = PlayerPos.GetPos(player or data.targetPlayer)
-- Keep walking toward last known position
data.moveTimer -= dt
if data.moveTimer <= 0 then
data.moveTimer = data.moveUpdateRate
if data.lastKnownPos then
if data.hasLoS and data.targetPlayer then
-- local hrpPos = getPlayerHRP(data.targetPlayer) and getPlayerHRP(data.targetPlayer).Position
local hrpPos = playerPos
if hrpPos then
local dist = (hrpPos - data.rootPart.Position).Magnitude
if dist > data.stopDistance then
local dir = (hrpPos - data.rootPart.Position).Unit
local targetPos = hrpPos - dir * data.stopDistance
data.humanoid:MoveTo(targetPos)
else
data.humanoid:MoveTo(data.rootPart.Position)
end
end
else
data.humanoid:MoveTo(data.lastKnownPos)
end
end
end
end
function Chase:OnLeave(data)
-- nothing
end
Chase.Transitions = {
require(script.Parent.Parent.Transitions.ToSearch)
}
return Chase
Last solution i tried was getting the Position of the target rootpart from the client, results in a weird/inaccurate position
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local sendEvent = ReplicatedStorage:WaitForChild("SendRootPart")
local Players = game:GetService("Players")
local plr = Players.LocalPlayer
local character = plr.Character or plr.CharacterAdded:Wait()
RunService:BindToRenderStep("SendRootPart", Enum.RenderPriority.Camera.Value + 5, function(dt)
local character = plr.Character or character
if character then
local rootpart = character.PrimaryPart
local cframe = character:GetPivot()
sendEvent:FireServer(cframe.Position)
end
end)
PlayerPos Module :
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local playerPos = {}
local sendEvent = ReplicatedStorage:FindFirstChild("SendRootPart")
sendEvent.OnServerEvent:Connect(function(player, pos)
local currentpos = pos
playerPos[player] = currentpos
end)
Players.PlayerRemoving:Connect(function(p)
playerPos[p] = nil
end)
local player_service = {}
function player_service.GetPos(player)
return playerPos[player] or nil
end
return player_service