2D Line Path - From one frame to another

I tinkered a bit with coding, and made this rudimentary but somewhat good script for creating a straight direct line to one place to another for GUI! It updates seamlessly, and always goes UNDER the main Frame, to keep it from ending sharp and unconnected. (Doesn’t work on SurfaceGUI or BillboardGUI. I’ve tried.)

This script is super easy to use, as I’ve made it a simple table for use of it! All you need to do is just add a pair of Frames to use at the top of the code, like so-

{"Location1", "Location2"},
{"Location2", "Location3"},

etc.. etc…
and it works like a charm!

Included with the script is comments on what is what, and a basic modified feature that changes the color of the line.

Here is the full script! Feel free to use wherever, with or without credit! <3

local RunService = game:GetService("RunService")

local map = script.Parent

-- Table of location pairs to connect as lines, with line type (1=white, 2=blue, 3=green)
-- its CASE SENSITIVE!!! -lumi
local locationPairs = {
    {"Location1", "Location2", 1},
    {"Location2", "Location3", 2},
    {"Location3", "Location4", 3},
    {"Location4", "Location5", 1},
    {"Location5", "Location6", 2},
    {"Location6", "Location7", 3}
}

-- Helper to get center of a frame (AbsolutePosition + 0.5 * AbsoluteSize)
local function getCenter(frame)
    return frame.AbsolutePosition + Vector2.new(frame.AbsoluteSize.X * 0.5, frame.AbsoluteSize.Y * 0.5)
end

local function getRelativeToMap(pos)
    return pos - map.AbsolutePosition
end

-- Helper to get color for line type
local function getLineColor(lineType)
    if lineType == 1 then
        return Color3.fromRGB(255, 255, 255) -- White
    elseif lineType == 2 then
        return Color3.fromRGB(0, 162, 255) -- Blue
    elseif lineType == 3 then
        return Color3.fromRGB(0, 255, 0) -- Green
    else
        return Color3.fromRGB(255, 255, 255) -- Default to white for others
    end
end

-- Helper to create or get a line frame
local function getOrCreateLine(idx, lineType)
    local lineName = "Path2DLine_" .. idx
    local line = map:FindFirstChild(lineName)
    if not line then
        line = Instance.new("Frame")
        line.Name = lineName
        line.BackgroundTransparency = 0
        line.BorderSizePixel = 0
        line.AnchorPoint = Vector2.new(0.5, 0.5)
        line.Parent = map
    end
    line.BackgroundColor3 = getLineColor(lineType)
    line.AnchorPoint = Vector2.new(0.5, 0.5)
    line.ZIndex = 1 -- Set ZIndex lower than locations
    return line
end

-- Remove unused line frames if number of pairs changes
local function cleanupLines(numLines)
    for _, child in map:GetChildren() do
        if child:IsA("Frame") and child.Name:match("^Path2DLine_%d+$") then
            local idx = tonumber(child.Name:match("^Path2DLine_(%d+)$"))
            if not idx or idx > numLines then
                child:Destroy()
            end
        end
    end
end

-- Helper to recursively set ZIndex for all descendants
local function setDescendantsZIndex(instance, zindex)
    for _, child in instance:GetChildren() do
        if child:IsA("GuiObject") then
            child.ZIndex = zindex
        end
        setDescendantsZIndex(child, zindex)
    end
end

local function updateLines()
    local numLines = #locationPairs
    cleanupLines(numLines)
    for i = 1, numLines do
        local pair = locationPairs[i]
        local nameA, nameB, lineType = pair[1], pair[2], pair[3]
        local locA = map:FindFirstChild(nameA)
        local locB = map:FindFirstChild(nameB)
        if locA and locB then
            local line = getOrCreateLine(i, lineType)
            local p1 = getCenter(locA)
            local p2 = getCenter(locB)
            local rp1 = getRelativeToMap(p1)
            local rp2 = getRelativeToMap(p2)
            local delta = rp2 - rp1
            local length = delta.Magnitude
            local angle = math.atan2(delta.Y, delta.X)
            local midpoint = (rp1 + rp2) * 0.5

            line.Size = UDim2.new(0, length, 0, 6)
            line.Position = UDim2.new(0, midpoint.X, 0, midpoint.Y)
            line.Rotation = math.deg(angle)
            line.ZIndex = 1 -- Ensure lines are under locations
            line.Visible = true
        end
    end
    -- Set ZIndex of location frames and all their descendants to be higher than lines
    for _, child in map:GetChildren() do
        if child:IsA("Frame") and not child.Name:match("^Path2DLine_%d+$") then
            child.ZIndex = 10
            setDescendantsZIndex(child, 10)
        end
    end
end

RunService.RenderStepped:Connect(updateLines)
updateLines()

this is my first post, so dont judge me if i missed something i was supposed to mention :[

4 Likes

Wouldn’t it be far easier to just use a Path2D instance instead of doing all this?

5 Likes

I wanted to try having a frame as the line for more customizability and such! Good point though.

1 Like