- What does the code do and what are you not satisfied with?
This module deals with handling of npc characters in my game. It uses pathfinding service to create paths to move to locations (almost always to players) and has a check sight function to detect direct sight. The main problem with it is that the pathfinding is dog water.
NPC Module
local PFS = game:GetService("PathfindingService")
local MAX_RETRIES = 5
local RETRY_TIME = 1 --in sec
local RAYDIST = 1000
local NPC = {}
NPC.__index = NPC
type self = {
NPC:Instance,
HRP:BasePart,
Humanoid: Humanoid,
Path:Path,
IsPathing:boolean,
CanAttack:boolean,
Damage:number,
Range:number,
Cooldown:number,
Friendlies:Instance,
Animations:{AnimationTrack}
}
export type NPC = typeof(setmetatable({} :: self, NPC))
function NPC.new(npc:Instance, pathArgs:{}): NPC
local self = setmetatable({} :: self, NPC)
self.NPC = npc
self.HRP = npc:FindFirstChild("HumanoidRootPart")
self.Humanoid = npc:FindFirstChildOfClass("Humanoid")
self.Path = PFS:CreatePath(pathArgs)
self.IsPathing = false
self.CanAttack = true
self.Damage = 20
self.Range = 5
self.Cooldown = 1
self.Friendlies = nil
self.Animations = {}
for i, v in ipairs(npc:GetChildren()) do
if v:IsA("Animation") then
if v.AnimationId == nil then continue; end
local track = self.Humanoid:FindFirstChildOfClass("Animator"):LoadAnimation(v)
self.Animations[v.Name] = track
elseif v:IsA("BasePart") then
v:SetNetworkOwner(nil)
end
end
self.Humanoid.Died:Connect(function()
task.wait(4)
self.NPC:Destroy()
self = nil
end)
return self
end
function NPC.WalkTo(self:NPC, obj:BasePart): ()
local function unstuck()
self.Humanoid:MoveTo(self.HRP.Position + Vector3.new(math.random(-1, 1), 0, math.random(-1, 1)))
self.Humanoid.Jump = true
task.wait(1)
end
local success, errMsg
local retries = 0
repeat
retries += 1
task.wait(RETRY_TIME)
success, errMsg = pcall(function()
self.Path:ComputeAsync(self.HRP.Position, obj.Position)
end)
until success and self.Path.Status == Enum.PathStatus.Success or retries >= MAX_RETRIES
if success and self.Path.Status == Enum.PathStatus.Success then
self.IsPathing = true
local waypoints = self.Path:GetWaypoints()
for i, waypoint:PathWaypoint in ipairs(waypoints) do
if i == 1 then continue; end
self.Humanoid:MoveTo(waypoint.Position)
if waypoint.Action == Enum.PathWaypointAction.Jump then
self.Humanoid.Jump = true
end
local reached = self.Humanoid.MoveToFinished:Wait(2)
if reached == false then
--print("cancelled1", self.NPC:GetFullName())
unstuck()
break
end
if self:CheckSight(obj.Position, obj) and math.abs(math.abs(obj.Position.Y) - math.abs(self.HRP.Position.Y)) < 3 then
--print("cancelled2", self.NPC:GetFullName())
break
end
if (obj.Position - waypoints[#waypoints].Position).Magnitude > 30 then
--print("cancelled3", self.NPC:GetFullName())
break
end
end
self.IsPathing = false
else
print("Path failed", self.NPC:GetFullName(), obj:GetFullName())
if not success then
print(errMsg)
end
end
end
function NPC.CheckSight(self:NPC, pos:Vector3, obj:BasePart): ()
local rayParams = RaycastParams.new() do
rayParams.FilterType = Enum.RaycastFilterType.Exclude
rayParams.FilterDescendantsInstances = {self.NPC, self.Friendlies}
end
local origin = self.HRP.Position
local raycastResult = workspace:Raycast(origin, (pos - origin) * RAYDIST, rayParams)
return (raycastResult ~= nil and raycastResult.Instance:IsDescendantOf(obj.Parent))
end
function NPC.Attack(self:NPC, target:Instance): ()
if self.CanAttack then
self.CanAttack = false
self:PlayAnimation("AttackAnim") --if self.Animations["AttackAnim"] ~= nil then self.Animations["AttackAnim"]:Play(); end
task.wait(0.25)
local raycast = workspace:Raycast(self.HRP.Position, self.HRP.CFrame.LookVector * self.Range)
if raycast and raycast.Instance.Parent == target then
target:FindFirstChildOfClass("Humanoid"):TakeDamage(self.Damage * (_G.difficulty/100))
print("hit")
end
task.delay(self.Cooldown, function() self.CanAttack = true end)
end
end
function NPC.PlayAnimation(self:NPC, anim:string): ()
if self.Animations[anim] ~= nil then
self.Animations[anim]:Play()
else
warn("missing animation")
end
end
function NPC.FindTargets(self:NPC): Instance
local potentialTargets = {}
local dist = math.huge
local target = nil
for i, v in ipairs(workspace.Friendlies:GetChildren()) do
local hum = v:FindFirstChildOfClass("Humanoid")
local torso = hum.RootPart --v:FindFirstChild("HumanoidRootPart")
if torso and hum and hum.Health > 0 then
table.insert(potentialTargets, v)
end
end
for i, v in ipairs(potentialTargets) do
local targetDist = (v.HumanoidRootPart.Position - self.HRP.Position).Magnitude
if targetDist < dist then
dist = targetDist
target = v
end
end
return target
end
return NPC
typical AI implementation
local NPC = require(game:GetService("ServerScriptService").NPC)
local dummy = NPC.new(script.Parent, {
AgentRadius = 1.5,
AgentHeight = 4.5,
AgentCanJump = true,
AgentCanClimb = true
})
dummy.Friendlies = workspace.Enemies
dummy.Damage = 12.5
while task.wait(0.1) do
if dummy.Humanoid.Health <= 0 then break; end
local target = dummy:FindTargets()
if target ~= nil then
if dummy:CheckSight(target.HumanoidRootPart.Position, target.HumanoidRootPart) and math.abs(math.abs(target.HumanoidRootPart.Position.Y) - math.abs(dummy.HRP.Position.Y)) < 3 then
dummy.Humanoid:MoveTo(target.HumanoidRootPart.Position)
else
if dummy.IsPathing == false then
task.spawn(dummy.WalkTo, dummy, target.HumanoidRootPart)
end
end
if (target.HumanoidRootPart.Position - dummy.HRP.Position).Magnitude < dummy.Range then
dummy:Attack(target)
end
end
end
-
What potential improvements have you considered?
Adding a check straight path function to check if there is a straight path possible is feasible. apart from that I really have no clue what to do. -
How (specifically) do you want to improve the code?
I want to fix and improve the pathfinding. It jitters and cannot often decide where it wants to go.
dr_help.rbxl (73.3 KB)
should contain one map, a npc chasing you, set up script and module