How would I scale/move billboard/surface GUI's easier?

So I’m having a problem which is very annoying. I have to enter a specific number to scale and move the billboard/surface GUI which is pain. Nobody has really mentioned this and I haven’t found anything.

You can try creating the Gui in StarterGui then putting it in SurfaceGui.
If, on the other hand, you have to change the values from SurfaceGui then you have to do it manually.

I know that already but it’s not that effective if you don’t know your sizes for the part to SurfaceGui.

I know but you can try it like I did here:


in the first box I set the Gui to full screen, in the second one I resized it making it more central and small and the result pretty much coincides

(obviously also depends on the size of the part)

Okay, I’ll look into it. If anyone else has any new suggestions please reply to this post.

Would it be beneficial to you to have a plug-in that automatically synchronized changes between the GUI in StarterGui and the one in the SurfaceGui?

Exactly, someone has to make a plugin that does this because scaling is a waste of time.

1 Like

Alright, here’s the first version of the plugin. I’m not too familiar with GUI editing so it may have some quirks, let me know in a DM when they occur. Save your work before you use this, I think I got all the bugs but if I missed one there’s a chance it might delete part of your GUI.

For now, just paste this into the console when you’re ready to use it. It will stop running when you close the game.

To use it, just select a BillboardGui or SurfaceGui and the editor will pop up on your screen. Let me know what works and what doesn’t and I’ll see if I can fix it before I release it as a full plugin.

--<< GLOBAL VARS >>--
local flat_directory        --| {int = {Instance, Instance}} | flat directory of all files, allows us to get dopplegangers easily without recursion.
local world_gui             --|                     Instance | the world gui object.
local editor_gui            --|                     Instance | the editor gui object.
local events                --|        {RbxScriptConnection} | table containing connected events to assist with cleanup.

--<< FUNCTION VARS >>--
local load_directory        --|       void | load_directory       |   SurfaceGui/BillboardGui dir
local connect_events        --|       void | connect_events       |   Instance obj
local obj_is_selected       --|    boolean | obj_is_selected      |   Instance obj
local is_world_gui          --|    boolean | is_world_gui         |   Instance obj
local is_editor_gui         --|    boolean | is_editor_gui        |   Instance obj
local get_doppleganger      --|   Instance | get_doppleganger     |   Instance obj
local change_property       --|       void | change_property      |   Instance obj, string property, any value
local is_editor_selected    --|    boolean | is_editor_selected   |   Instance obj
local is_world_selected     --|    boolean | is_world_selected    |   Instance obj

-- TODO: Errors on drag and drop children

do --<< FUNCTION CREATION >>--
    function load_directory(dir)
        -- Load a directory. This function expects dir 
        -- to be a valid SurfaceGui or BillboardGui.
        -- It will load all children of dir into the
        -- flat_directory table and connect the events.
        -- It will create the new editor GUI.

        -- Initialize variables.
        world_gui = dir
        events = {}
        flat_directory = {}
        -- Initialize the editor GUI.
        if editor_gui then editor_gui:Destroy() end
        editor_gui = Instance.new("ScreenGui", game.StarterGui)
        editor_gui.Name = "EditorGui"
        local main = Instance.new("Frame", editor_gui)
        main.Name = "Main"
        main.Size = UDim2.new(0, dir.AbsoluteSize.X, 0, dir.AbsoluteSize.Y)
        if dir:IsA("SurfaceGui") and dir.Parent and dir.Parent:IsA("BasePart") then
            main.BackgroundColor3 = dir.Parent.Color
            main.BorderSizePixel = 0
            main.BackgroundTransparency = dir.Parent.Transparency
        else
            main.BackgroundTransparency = 0.9
            main.BorderSizePixel = 3
        end
        -- Create the flat directory id system.
        for i, v in dir:GetDescendants() do
            v:SetAttribute("EditorId", i)
            flat_directory[i] = {v}
        end
        -- Copy the directory to the editor GUI.
        for i, v in dir:GetChildren() do
            v:Clone().Parent = main
        end
        -- Add the editor GUI to the flat directory.
        for i, v in main:GetDescendants() do
            table.insert(flat_directory[v:GetAttribute("EditorId")], v)
        end
        -- Connect events.
        connect_events(main)
        for i, v in flat_directory do
            connect_events(v[1])
            connect_events(v[2])
        end
    end

    function connect_events(obj)
        -- Connect the events for the object.

        -- Check if the object is a valid target and save a local variable
        -- based on whether this is part of the editor GUI or the world GUI.
        local is_editor
        if is_editor_gui(obj) then
            is_editor = true
        elseif is_world_gui(obj) then
            is_editor = false
        else
            return
        end

        if obj ~= editor_gui.Main then
            events[#events+1] = obj.Changed:Connect(function(prop)
                -- If the object is selected and the doppleganger is not, change
                -- the doppleganger.
                local doppleganger = get_doppleganger(obj)
                if not obj_is_selected(obj) then return end
                if not doppleganger then return end
                if obj_is_selected(doppleganger) then return end
                -- Use pcall for edge cases with read-only properties
                -- such as AbsoluteSize
                pcall(change_property, doppleganger, prop, obj[prop])
            end)
        else
            events[#events+1] = obj.Changed:Connect(function(prop)
                if world_gui:IsA("SurfaceGui") and world_gui.Parent and world_gui.Parent:IsA("BasePart") then
                    if prop == "BackgroundColor3" then
                        world_gui.Parent.Color = obj[prop]
                    elseif prop == "BackgroundTransparency" then
                        world_gui.Parent.Transparency = obj[prop]
                    end
                end
            end)
        end

        events[#events+1] = obj.ChildAdded:Connect(function(child)
            -- If the main GUI directory is selected, create the doppleganger.
            task.wait()
            if (is_editor and is_world_selected())
            or (not is_editor and is_editor_selected()) then
                return
            end
            -- Abort if this already has an existing entry in the flat directory.
            local attr = child:GetAttribute("EditorId")
            if attr and flat_directory[attr] and
            (flat_directory[attr][1] == child
            or flat_directory[attr][2] == child) then
                -- warn("Could not sync changes. Manually double check your changes"..
                -- " and contact the moron who wrote this plugin. ID 102")
                return
            end
            -- Create the doppleganger and add both to the flat directory.
            attr = #flat_directory + 1
            child:SetAttribute("EditorId", attr)
            local clone = child:Clone()
            if is_editor then
                flat_directory[attr] = {clone, child}
            else
                flat_directory[attr] = {child, clone}
            end
            clone.Parent = get_doppleganger(obj)
            -- Connect the events.
            connect_events(child)
            connect_events(clone)
        end)

        events[#events+1] = obj.ChildRemoved:Connect(function(child)
            -- Clean up references to the child and the doppleganger, if any.
            local attr = child:GetAttribute("EditorId")
            if not attr then return end
            if not flat_directory[attr] then return end
            if flat_directory[attr][1] ~= child and flat_directory[attr][2] ~= child then return end
            local doppleganger = get_doppleganger(child)
            table.remove(flat_directory, attr)
            doppleganger:Destroy()
            game.Selection:Set({obj})
        end)
    end

    function obj_is_selected(obj)
        -- Check if the object is contained in the selection.
        return table.find(game.Selection:Get(), obj) ~= nil
    end

    function is_world_gui(obj)
        -- Check if the object is part of the world GUI.
        -- These functions only exist for the sake of self-documenting code.
        return obj:IsDescendantOf(world_gui) or obj == world_gui
    end

    function is_editor_gui(obj)
        -- Check if the object is a descendant of the editor GUI.
        return obj:IsDescendantOf(editor_gui) or obj == editor_gui
    end

    function get_doppleganger(obj)
        -- Get the doppleganger of the object.
        local attr = obj:GetAttribute("EditorId")
        if attr and flat_directory[attr] then
            if flat_directory[attr][1] == obj then
                return flat_directory[attr][2]
            elseif flat_directory[attr][2] == obj then
                return flat_directory[attr][1]
            end
        end
        return nil
    end

    function change_property(obj, prop, value)
        -- This function exists only to make pcall look cleaner.
        obj[prop] = value
    end

    function is_editor_selected()
        -- Check if the editor gui or a descendant of it is selected.
        for i, v in game.Selection:Get() do
            if not is_editor_gui(v) and v ~= editor_gui then
                return false
            end
        end
    end

    function is_world_selected()
        -- Check if the world gui or a descendant of it is selected.
        for i, v in game.Selection:Get() do
            if not is_world_gui(v) and v ~= world_gui then
                return false
            end
        end
    end
end

do --<< EVENT CREATION >>--
    game.Selection.SelectionChanged:Connect(function()
        task.wait()
        local selection = game.Selection:Get()
        if #selection > 1 then return end
        local new = true
        if world_gui then
            for i, v in selection do
                if is_world_gui(v) or is_editor_gui(v) then
                    new = false
                    break
                end
            end
        end
        if new then
            world_gui = nil
            if events then
                for i, v in events do
                    v:Disconnect()
                end
                events = {}
            end
            if editor_gui then
                editor_gui:Destroy()
                editor_gui = nil
            end
            if #selection == 0 then return end
            local world = selection[1]
            if world:IsA("BillboardGui") or world:IsA("SurfaceGui") then
                load_directory(world)
                game.Selection:Set({editor_gui.Main})
            else
                world = world:FindFirstAncestorWhichIsA("BillboardGui")
                        or world:FindFirstAncestorWhichIsA("SurfaceGui")
                if world then
                    load_directory(world)
                    local doppleganger = get_doppleganger(selection[1])
                    if doppleganger then
                        game.Selection:Set({doppleganger})
                    else
                        game.Selection:Set({editor_gui.Main})
                    end
                end
            end
        elseif #selection == 1 then
            if is_world_gui(selection[1]) then
                local doppleganger = get_doppleganger(selection[1])
                if doppleganger then
                    game.Selection:Set({doppleganger})
                end
            end
        end
    end)
end
2 Likes

This is really cool, I’ll definitely be using this in the future!

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.