I’m creating a pathfinder module and I need some performance optimizations. While it may work, my code could still be improved. Also, this module uses PartCache, for all your quick part-creation needs for toggling the visible waypoints. Please tell me anything you find. Here’s the module:
--!nonstrict
------------------------
-- Services: -----------
local TweenService : TweenService = game:GetService("TweenService")
local PathfindingService : PathfindingService = game:GetService("PathfindingService")
script:WaitForChild("PartCache")
local PartCache = require(script.PartCache)
------------------------
-- Errors: -------------
local argument_Error = "Argument %i missing or nil"
local instance_Error = "%s is not a valid member of %s %q"
local index_nil_Error = "attempt to index nil with '%s'"
local unsupported_value_Error = "type '%s' is unsupported for %s %q"
local character_Error = "%q rigged incorrectly | currentRootPart %q | currentPrimaryPart %q"
local read_only_Error = "can't set value"
------------------------
-- Types: --------------
export type ReferencePoint = Vector3 | Model | BasePart | Humanoid
export type Waypoints = (...{Position : Vector3, Action : Enum.PathWaypointAction}) -> (...PathWaypoint)
export type AgentParameters = {
AgentRadius : number,
AgentHeight : number,
AgentCanJump : boolean
}
export type PathfinderObject = {
Name : string,
Target : ReferencePoint,
Current : ReferencePoint,
AgentParameters : AgentParameters,
SetIndicatorGroup : any,
Waypoints : Waypoints,
Completion : number,
Visible : boolean,
Path : Path,
StartDistance : number,
CurrentWaypointIndex : number,
WaypointsCache : string,
Active : boolean,
Start : any,
Pause : any,
Resume : any,
Stop : any,
Update : any,
WalkToWaypoint : any,
PausedAction : any,
Action : any,
Destroy : any
}
------------------------
-- Private: ------------
-- Functions
local function GetAgentParameters(agentParameters : AgentParameters, character : Model, humanoid : Humanoid) : AgentParameters
if character then
if (humanoid.RigType == Enum.HumanoidRigType.R6) then
local head,hrp = character:FindFirstChild("Head"),character:FindFirstChild("HumanoidRootPart")
local la,ra,leg = character:FindFirstChild("Left Arm"),character:FindFirstChild("Right Arm"),(character:FindFirstChild("Left Leg") or character:FindFirstChild("Right Leg"))
if (head and hrp) then
if leg then
agentParameters.AgentHeight = (head.Size.Y + hrp.Size.Y + leg.Size.Y)
else
agentParameters.AgentHeight = (head.Size.Y + hrp.Size.Y)
end
if (la and ra) then
agentParameters.AgentRadius = 0.5 * (hrp.Size.X + la.Size.X + ra.Size.X)
elseif la then
agentParameters.AgentRadius = 0.5 * (hrp.Size.X + la.Size.X)
elseif ra then
agentParameters.AgentRadius = 0.5 * (hrp.Size.X + ra.Size.X)
end
end
end
elseif (humanoid.RigType == Enum.HumanoidRigType.R15) and humanoid then
local head,hrp,hip = character:FindFirstChild("Head"),character:FindFirstChild("HumanoidRootPart"),(humanoid and humanoid.HipHeight)
local arm,leg = (character:FindFirstChild("LeftUpperArm") or character:FindFirstChild("RightUpperArm")),(character:FindFirstChild("LeftUpperLeg") or character:FindFirstChild("RightUpperLeg"))
if (head and hrp and hip) then
agentParameters.AgentHeight = (head.Size.Y + hrp.Size.Y + hip)
if arm then
agentParameters.AgentRadius = 0.5 * ((2 * arm.Size.X) + hrp.Size.X)
elseif leg then
agentParameters.AgentRadius = 0.5 * ((2 * leg.Size.X) + hrp.Size.X)
end
elseif hip then
local height = humanoid:FindFirstChild("BodyHeightScale")
local width = humanoid:FindFirstChild("BodyWidthScale")
if height then
agentParameters.AgentHeight = (height.Value * 5)
end
if width then
agentParameters.AgentRadius = (width.Value * 5)
end
end
end
if humanoid then
if humanoid.UseJumpPower then
agentParameters.AgentCanJump = (humanoid.JumpPower > 0)
else
agentParameters.AgentCanJump = (humanoid.JumpHeight > 0)
end
end
return agentParameters
end
local function UseAgentParameters(current : ReferencePoint) : boolean
if typeof(current) == "Instance" then
if current:IsA("Model") then
return true
elseif current:IsA("Part") then
return true
end
return current:IsA("Humanoid")
else
return false
end
end
local function getPosition(value : ReferencePoint) : Vector3
if typeof(value) == "Instance" then
if value:IsA("Model") then
return value.PrimaryPart.Position
elseif value:IsA("Part") then
return value.Position
elseif value:IsA("Humanoid") then
return value.RootPart.Position
end
end
return (value :: Vector3)
end
local CloneTable
CloneTable = function(t) : {}
local copy = {}
for key,value in pairs(t) do
if typeof(value) == "table" then
copy[key] = CloneTable(value)
else
copy[key] = value
end
end
return copy
end
local function len(Dictionary) : number
local length : number = 0
for _,__ in pairs(Dictionary) do
length += 1
end
return length
end
local function assertwarn(requirement: boolean, warnMessage: string)
if not requirement then
warn(warnMessage)
end
end
local function emptyFunction()
end
-- Default Properties
local default_AgentParameters : AgentParameters = {AgentRadius = 2, AgentHeight = 5, AgentCanJump = true}
local empty_AgentParameters : AgentParameters = {AgentRadius = 0, AgentHeight = 0, AgentCanJump = true}
local default_Proxy : {} = {}
local default_MetaTable : {__index : any, __newindex : any, __tostring : any} = {
__index = function(self : PathfinderObject, key : string) : any
local rawValue = self[default_Proxy][key]
assert(typeof(rawValue) ~= "nil", instance_Error:format(key,self[default_Proxy]["Name"],script:GetFullName()))
return rawValue
end,
__newindex = function(self : PathfinderObject, key : string, value : any)
local rawValues = self[default_Proxy]
assert(typeof(rawValues[key]) ~= "nil", instance_Error:format(key,rawValues["Name"],script:GetFullName()))
self:Update(key, value)
end,
__tostring = function(self : PathfinderObject) : string
return self[default_Proxy]["Name"]
end
}
-- Set Values
local set_types : {string} = {"Name","Current","Target","Visible","AgentParameters","Waypoints"}
-- Waypoints Cache and Folders
local PartWaypoint = script:WaitForChild("Default Waypoint")
local WaypointsCacheFolder : Folder = (workspace:FindFirstChild("Waypoints Cache") or Instance.new("Folder"))
local WaypointsCache : {[string]: PartCache.PartCache} = {Default = PartCache.new(PartWaypoint, 400)}
------------------------
-- Setup: --------------
-- PathfinderModule
local PathfinderModule = {}
PathfinderModule.__index = default_MetaTable.__index
PathfinderModule.__newindex = default_MetaTable.__newindex
PathfinderModule.Name = "Pathfinder"
PathfinderModule.Current = Vector3.new(0, 0, 0)
PathfinderModule.Target = Vector3.new(0, 0, 0)
PathfinderModule.AgentParameters = default_AgentParameters
PathfinderModule.Waypoints = {}
PathfinderModule.Completion = 0
PathfinderModule.Visible = false
PathfinderModule.CurrentWaypointIndex = 0
PathfinderModule.WaypointsCache = "Default"
PathfinderModule.PausedAction = emptyFunction
PathfinderModule.Action = emptyFunction
PathfinderModule.Active = false
PathfinderModule.Path = false
-- Waypoints Cache
WaypointsCacheFolder.Name = "Waypoints Cache"
WaypointsCacheFolder.Parent = workspace
WaypointsCacheFolder = WaypointsCacheFolder:FindFirstChild("Default Waypoints") or Instance.new("Folder", WaypointsCacheFolder)
WaypointsCacheFolder.Name = "Default Waypoints"
WaypointsCache["Default"]:SetCacheParent(WaypointsCacheFolder)
WaypointsCacheFolder = nil
PartWaypoint.CFrame = CFrame.new(0, 10e8, 0)
PartWaypoint = nil
-- Other Stuff
if script:FindFirstChild("API") then script:FindFirstChild("API"):Destroy() end -- Destroy the API file while in-game, as it's unneeded
------------------------
-- Constructor: --------
local Class = {}
function Class.new(current : ReferencePoint, target : ReferencePoint, agentParameters : AgentParameters) : PathfinderObject
-- Pathfinder Object
local object : PathfinderObject = {
Name = nil,
Target = nil,
Current = nil,
AgentParameters = nil,
SetIndicatorGroup = nil,
Waypoints = nil,
Completion = nil,
Visible = nil,
Path = nil,
StartDistance = nil,
CurrentWaypointIndex = nil,
WaypointsCache = nil,
Active = nil,
Start = nil,
Pause = nil,
Resume = nil,
Stop = nil,
Update = nil,
WalkToWaypoint = nil,
PausedAction = nil,
Action = nil,
Destroy = nil
}
-- Initalize Pathfinder Object
object[default_Proxy] = CloneTable(PathfinderModule)
object[default_Proxy].__index = nil
object[default_Proxy].__newindex = nil
object[default_Proxy].AgentParameters = agentParameters or default_AgentParameters
object[default_Proxy].Path = PathfindingService:CreatePath(object[default_Proxy]["AgentParameters"])
-- Sets Metatable
setmetatable(object, default_MetaTable)
-- Calls Update
object:Update("Current", current)
object:Update("Target", target)
object:Update("AgentParameters", agentParameters)
return object
end
function Class:CreateIndicatorGroup(Name : string, Indicator : BasePart)
assert(Name, argument_Error:format(1))
assert(Indicator, argument_Error:format(2))
local Parent : Instance? = WaypointsCache[PathfinderModule.WaypointsCache].CurrentCacheParent.Parent
WaypointsCacheFolder = Parent:FindFirstChild(Name .. " Waypoints") or Instance.new("Folder")
WaypointsCacheFolder.Name = (Name .. " Waypoints")
WaypointsCacheFolder.Parent = Parent
WaypointsCache[Name] = PartCache.new(Indicator, 400, Parent)
Indicator.CFrame = CFrame.new(0, 10e8, 0)
end
function Class:RemoveIndicatorGroup(Name : string)
assert(Name, argument_Error:format(1))
WaypointsCache[Name]:Dispose()
WaypointsCache[Name] = nil
end
------------------------
-- Functions: ----------
function PathfinderModule:SetIndicatorGroup(GroupName : string)
assert(getmetatable(self) == default_MetaTable, index_nil_Error:format("WalkToWaypoint"))
self[default_Proxy]["WaypointsCache"] = GroupName
end
function PathfinderModule:WalkToWaypoint()
assert(getmetatable(self) == default_MetaTable, index_nil_Error:format("WalkToWaypoint"))
self.Current:MoveTo(self.Waypoints[self.CurrentWaypointIndex])
self.Current.MoveToFinished:Wait()
self.CurrentWaypointIndex += 1
end
function PathfinderModule:Start(waypoints : Waypoints)
assert(getmetatable(self) == default_MetaTable, index_nil_Error:format("Start"))
self.Completion = (self.CurrentWaypointIndex / #self.Waypoints)
if self[default_Proxy]["Current"] and self[default_Proxy]["Target"] then
print("Started")
end
end
function PathfinderModule:Pause(pauseTime : number)
assert(getmetatable(self) == default_MetaTable, index_nil_Error:format("Pause"))
self.Active = false
self.PausedAction = self.Action
self.Action = emptyFunction
if (pauseTime > 0) or self.Active then
delay(pauseTime, function()
self:Resume()
end)
end
end
function PathfinderModule:Resume()
assert(getmetatable(self) == default_MetaTable, index_nil_Error:format("Resume"))
self.Active = true
self.Action = self.PausedAction
end
function PathfinderModule:Stop()
assert(getmetatable(self) == default_MetaTable, index_nil_Error:format("Stop"))
if (#self.Waypoints > 0) or self.Active then
self.Active = false
self.CurrentWaypointIndex = 1
end
end
function PathfinderModule:Destroy()
assert(getmetatable(self) == default_MetaTable, index_nil_Error:format("Destroy"))
for key,_ in pairs(self) do
self[key] = nil
end
end
function PathfinderModule:Update(key : string, value : any)
assert(getmetatable(self) == default_MetaTable, index_nil_Error:format("Update"))
local Current = self.Current -- The current PVInstance or the current Vector3 position
local Target = self.Target -- The current PVInstance or the current Vector3 position
local Path : Path = self.Path -- The path from Current to Target
-- Sets value to the correct value
if (not table.find(set_types, key)) then
error(read_only_Error)
elseif typeof(value) == "Instance" then
if value:IsA("Model") then
local humanoid : Humanoid? = value:FindFirstChildOfClass("Humanoid")
if humanoid then
assertwarn(humanoid.RootPart ~= value.PrimaryPart, character_Error:format(value:GetFullName(),humanoid.RootPart:GetFullName(),value.PrimaryPart:GetFullName()))
self[default_Proxy][key] = humanoid
else
self[default_Proxy][key] = (value :: Model)
end
elseif value:IsA("Part") then
local checkParent : Instance
repeat
checkParent = value.Parent
print(checkParent)
until (checkParent.Parent == workspace)
local humanoid : Humanoid? = checkParent:FindFirstChildOfClass("Humanoid")
if humanoid then
assert(humanoid.RootPart ~= value:GetRootPart(), character_Error:format(value:GetFullName(),humanoid.RootPart:GetFullName(),value.PrimaryPart:GetFullName()))
self[default_Proxy][key] = humanoid
else
self[default_Proxy][key] = (value:GetRootPart() :: BasePart)
end
end
elseif typeof(value) == "Vector3" or typeof(value) == "boolean" or typeof(value) == "string" then
self[default_Proxy][key] = (value :: Vector3 | boolean | string)
elseif (key == "AgentParameters") then
self[default_Proxy][key] = (value :: AgentParameters)
else
error(unsupported_value_Error:format(typeof(value),self.Name,self.Name .. "." .. key))
end
-- Re-Defines Current and Target
Current = getPosition(self.Current)
Target = getPosition(self.Target)
-- Configures the self.Path and self.Waypoints
if (Current ~= Target) then
-- Sets self.AgentParameters to the correct value
if typeof(self.Current) == "Vector3" then
self[default_Proxy]["AgentParameters"] = empty_AgentParameters
end
-- Sets self.Waypoints to the correct values
spawn(function()
Path:ComputeAsync(Current, Target)
self[default_Proxy]["Waypoints"] = Path:GetWaypoints()
local Cache : PartCache.PartCache = WaypointsCache[self[default_Proxy]["WaypointsCache"]]
local Waypoints = self[default_Proxy]["Waypoints"]
local VisualWaypoints = Cache.InUse
for i=((#VisualWaypoints > #Waypoints) and #VisualWaypoints) or #Waypoints,1,-1 do
if Waypoints[i] then
local part = VisualWaypoints[i] or Cache:GetPart()
part.CFrame = CFrame.lookAt(Waypoints[i]["Position"],(Waypoints[i+1] or Waypoints[1])["Position"])
else
Cache:ReturnPart(VisualWaypoints[i])
end
end
repeat
local left : number = (#Waypoints - #Cache.InUse)
print(left)
for i=(#Cache.InUse+1),#Waypoints do
if Waypoints[i] then
local part = VisualWaypoints[i] or Cache:GetPart()
part.CFrame = CFrame.new(Waypoints[i]["Position"])
else
Cache:ReturnPart(VisualWaypoints[i])
end
end
until (#Cache.InUse >= #Waypoints)
end)
end
end
------------------------
-- Return: -------------
return Class