local ArrowPart = game.ReplicatedStorage.Decoration.Arrow
local RunService = game:GetService("RunService")
local CollectionService = game:GetService("CollectionService")
local function GetRadius(Adornee)
if not Adornee then return 0 end
if Adornee.Parent:IsA("Model") then
local Size = Adornee.Parent:GetExtentsSize()
return math.max(Size.X, Size.Y, Size.Z)
elseif Adornee:IsA("BasePart") then
local Size = Adornee.Size
return math.max(Size.X, Size.Y, Size.Z)
end
warn("No basepart or model")
return 0
end
local function UpdateArrow(ArrowPart, Adornee, Point, Radius, Relative)
if not ArrowPart or not ArrowPart.Parent then return end
-- If Relative is true, Point is an offset from Adornee, otherwise it's a world position
local targetPoint = Relative and (Adornee.Position + Point) or Point
local direction = (targetPoint - Adornee.Position).Unit
-- Position the arrow at adornee's position, offset by radius in the direction of point
local arrowPosition = Adornee.Position + (direction * Radius)
-- Instead of pointing AT targetPoint, create a CFrame that points in the direction
-- by using a lookVector. We create a point far along the direction vector.
local lookAtPoint = arrowPosition + (direction * 100) -- 100 studs away in that direction
ArrowPart.CFrame = CFrame.new(arrowPosition, lookAtPoint) * CFrame.Angles(math.rad(-90), 0, 0)
end
------------------------
local Arrow = {}
Arrow.ArrowTable = {}
-- Single RenderStepped connection for ALL arrows
local updateConnection = nil
local function StartUpdateLoop()
if updateConnection then return end -- Already running
updateConnection = RunService.RenderStepped:Connect(function()
-- Update all arrows in one loop
for arrowPart, data in pairs(Arrow.ArrowTable) do
if arrowPart and arrowPart.Parent and data._Adornee and data._Adornee.Parent then
UpdateArrow(arrowPart, data._Adornee, data._Point, data._Radius, data._Relative)
else
-- Clean up invalid arrows
Arrow.Remove(arrowPart)
end
end
-- Stop the loop if no arrows remain
if next(Arrow.ArrowTable) == nil then
if updateConnection then
updateConnection:Disconnect()
updateConnection = nil
end
end
end)
end
Arrow.Spawn = function(Adornee, Point, Relative)
if not Adornee or not Point then error("Missing Adornee or Point") end
Relative = Relative or false -- Default to false
local NewArrowPart = ArrowPart:Clone()
NewArrowPart.Parent = workspace
NewArrowPart.Anchored = true
NewArrowPart.CanCollide = false
NewArrowPart.CanQuery = false
NewArrowPart.CanTouch = false
local Radius = GetRadius(Adornee) + 1.5
-- Store arrow data
Arrow.ArrowTable[NewArrowPart] = {
_Adornee = Adornee,
_Point = Point,
_Radius = Radius,
_Relative = Relative
}
-- Start the single update loop if not already running
StartUpdateLoop()
return NewArrowPart
end
Arrow.Remove = function(ArrowPart)
if Arrow.ArrowTable[ArrowPart] then
-- Remove from table
Arrow.ArrowTable[ArrowPart] = nil
-- Destroy the part
if ArrowPart then
ArrowPart:Destroy()
end
end
end
Arrow.RemoveAll = function()
for arrowPart, _ in pairs(Arrow.ArrowTable) do
-- Destroy the part
if arrowPart then
arrowPart:Destroy()
end
end
-- Clear the table
Arrow.ArrowTable = {}
-- Disconnect the update loop
if updateConnection then
updateConnection:Disconnect()
updateConnection = nil
end
end
return Arrow
Simple working module script may not be performant. Comments made by claude. rest of the script by me.