Optimising a Gui elements creation script

Hello, I’ve been working on a script to make it easier to add effects to gui objects and to simplify it. It works fine, all of the events. Ive had a really hard and somewhat frustrating time figuring out how it lags. I tried using RunService.Heartbeat and while loops but I don’t know if there’s a better answer.

When the player join,s the code searches all descendants of the playerGui(not including stuff like chat, mobile controls, etc.) for object values determining what effect it gives. Then when the player hovers over or leaves or clicks the GuiObject in the tables where the parts are located with its data for tweens, properties, etc. It does it.

EDIT: I’m not sure if I was clear on this so here. All objects that ave a value are put inside the table tableEffects to prevent searching through all of playerGui to get updates, with extra data for the parts in extraTableInfo. Currently I search through that table of objects for events

Now for the issue:
Ive had a hard time with some of the events causing lag when the player hovers over some objects, leaves, and clicks. I added denounces to make that event happen only once per updateTime, but it isn’t enough. Its come a long way from basically not playable, to its current state. Im not 100% on this, but I’m pretty sure it causes some crashes on mobile atm. I want to get help on this because my end goal is to publish this script to help other devs who struggle with making gui elements to make their lives easier, but I don’t want to publish it if its just going to lag their game. Any suggestions on how I can make it less laggy are appreciated, even on how I can make the instructions more clear. Ive spent >2 days trying to optimize it and recently I’ve been struggling/burnt out so I’m coming here for some help.

--[[
		-- Created by grif_0
			-- Gui Elements V1.0

	========== LABELS ==========
	
	When inserting an element to a label, insert an ObjectValue into the label, and 

	_buttonMove - Button grows when hovered over and left
	_labelHover - Label will hover slightly up and down 
	_labelInfo - Displays a textlabel at the mouse with the info inside of a string value inside the value
	_buttonClickBL - doesnt play a click when the button is clicked, and doButtonClick = true. ONLY works inside of a TextButton or ImageButton. 
	_buttonHoverBL - doesnt play a click when the button is clicked, and doButtonHover = true. ONLY works inside of a TextButton or ImageButton.		

	doButtonClick - determines whether you want buttons to make a click sound when clicked. 
	doButtonHover - determines whether buttons make a sound when you enter and make a sound. 																							
	
	for _labelHover, you can chose how high it hovers up and down by putting a Module named '_Config' inside the ObjectValue, with a table inside named:
	
	module.Config = {
	
		["Position"] = UDim2.new(); amount ADDED to the current position
		
		["EasingDirection"] = EasingDirection;
		["EasingStyle"] = EasingStyle;
		["Time"] = Time}
		
	The default values are as following:
		
		["Position"] = UDim2.new(0,0,guiPart.Parent.AbsoluteSize.Y * .05 / guiPart.Parent.AbsoluteSize.Y,0)
		["EasingDirection"] = Enum.EasingDirection.InOut;
		["EasingStyle"] = Enum.EasingStyle.Circular;
		["Time"] = 5
		["Override"] = true -- will always be true	
	
	
			
	for _buttonMove, you are able to determine how bix exactily the buttons grow, by putting a module named '_Config' inside of the ObjectValue, with a table inside named:
	
	module.Config = {
	
		["Position"] = UDim2.new(); -- amount ADDED to the current position
		["EasingStyle"] = EasingStyle;
		["EasingDirection"] = EasingDirection;
		["Time"] = Time} }
	
	the default values are as following:
		
		["Position"] = UDim2.new(guiPart.Size.X.Scale/4,0,0,0)
		["EasingDirection"] = Enum.EasingStyle.Sine;
		["EasingStyle"] = Enum.EasingDirection.Out;
		["Time"] = .5
		
		
	for _labelInfo, the textbox scales automatically to the size of the text. To change the style of the text label, then put a module named '_Config' inside of the Object value, with a table named 'Config'. If no module is found then it will set the style
	found in 'labelInfoTable'. You can change the style buy adding the Property name inside the index of the table, and the value to the value of the table. The defaults are as shown:
	
		["BackgroundTransparency"] = .5;
		["BorderSizePixel"] = 0;
		["BackgroundColor3"] = Color3.fromRGB(125,125,125);
		
	]]

local updateTime = .05
	
local doButtonClick = true -- determines if clicking buttons makes a sound 
local doButtonHover = true -- determines if hovering over buttons makes a sound
local labelInfoTable = {
	["BackgroundTransparency"] = .75;
	["BorderSizePixel"] = 0;
}

--[[
Dont edit anything below unless you know what your doing
--------------------------------------------------------------------------------------------------------------------------------------------------]]

local player = game.Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local mouse = player:GetMouse()

local soundService = game:GetService("SoundService")

local tableEffects = {
	["_buttonMove"] = {};
	["_labelHover"] = {};
	["_labelInfo"] = {};
	["_buttonClickBL"] = {};
	["_buttonHoverBL"] = {};
	["_buttonSounds"] = {}
}
local extraTableInfo = {
	["_buttonMove"] = {};
	["_labelHover"] = {};
	["_labelInfo"] = {};
	["_buttonHover"] = {};
}

local labelInfoDefault = {
	["Text"] = " ";
	["BackgroundColor3"] = Color3.fromRGB(125,125,125);
	["BackgroundTransparency"] = .5;
	["BorderColor3"] = Color3.fromRGB(0,0,0);
	["BorderMode"] = Enum.BorderMode.Outline;
	["BorderSizePixel"] = labelInfoTable["BorderSizePixlel"];
	["Font"] = Enum.Font.Arial;
	["TextColor3"] = Color3.fromRGB(0,0,0);
	["TextStrokeColor3"] = Color3.fromRGB(0,0,0);
	["TextStrokeTransparency"] = 1;
	["TextTransparency"] = 0;
	["TextXAlignment"] = Enum.TextXAlignment.Left;
	["TextYAlignment"] = Enum.TextYAlignment.Bottom;
	["TextSize"] = 10
}


local debounce1 = false
local debounce2 = false
local debounce3 = false
local debounce4 = false
local finished = false
local doLabelInfo = false

function checkDescendants(v)
	if v:IsA("GuiButton") then
		if not v:IsDescendantOf(playerGui:WaitForChild("BubbleChat")) and not v:IsDescendantOf(playerGui:WaitForChild("Chat")) and not v:IsDescendantOf(playerGui:WaitForChild("Freecam")) then
			if playerGui:FindFirstChild("TouchGui") then
				if not v:IsDescendantOf(playerGui:WaitForChild("TouchGui")) then
					tableEffects["_buttonSounds"][#tableEffects["_buttonSounds"]+1] = v
					extraTableInfo["_buttonHover"][v] = {["Deb"] = false}
				end
			else
				tableEffects["_buttonSounds"][#tableEffects["_buttonSounds"]+1] = v
				extraTableInfo["_buttonHover"][v] = {["Deb"] = false}
			end
		end
	end
	if v:FindFirstChild("_buttonMove") then
		tableEffects["_buttonMove"][#tableEffects["_buttonMove"]+1] = v
		local size = require(v:FindFirstChild("_buttonMove"):FindFirstChild("_Config"))
		extraTableInfo["_buttonMove"][v] = {["Deb"] = false;["Close"] = v.Size;["Open"] = (size.Config["Position"] or UDim2.new(v.Size.X.Scale/4,0,0,0)) + v.Size;["EasingStyle"] = size.Config["EasingStyle"] or Enum.EasingStyle.Sine;["EasingDirection"] = size.Config["EasingDirection"] or Enum.EasingDirection.Out;["Time"] = size.Config["Time"] or .5}
		v:FindFirstChild("_buttonMove"):Destroy()
	end
	if v:FindFirstChild("_labelHover") then
		tableEffects["_labelHover"][#tableEffects["_labelHover"]+1] = v
		local size = require(v:FindFirstChild("_labelHover"):FindFirstChild("_Config"))
		extraTableInfo["_labelHover"][v] = {["Position"] = v.Position + (size.Config["Position"] or UDim2.new(0,0,v.Parent.AbsoluteSize.Y * .05 / v.Parent.AbsoluteSize.Y,0));["EasingStyle"] = size.Config["EasingStyle"] or Enum.EasingStyle.Circular;["EasingDirection"] = size.Config["EasingDirection"] or Enum.EasingDirection.InOut;["Time"] = size.Config["Time"] or 5}
		v:FindFirstChild("_labelHover"):Destroy()
	end
	if v:FindFirstChild("_labelInfo") then
		tableEffects["_labelInfo"][#tableEffects["_labelInfo"]+1] = v
		local module = require(v:FindFirstChild("_labelInfo"):FindFirstChild("_Config"))
		extraTableInfo["_labelInfo"][v] = {["Text"] = " ";["BackgroundColor3"] = " ";["BackgroundTransparency"] = " ";["BorderColor3"] = " ";["BorderMode"] = " ";["BorderSizePixel"] = " ";["Font"] = " ";["TextColor3"] = " ";["TextStrokeColor3"] = " ";["TextStrokeTransparency"] = " ";["TextTransparency"] = " ";["TextXAlignment"] = " ";["TextYAlignment"] = " ";["TextSize"] = " ";}
		for i,_ in pairs(extraTableInfo["_labelInfo"][v]) do
			if module.Config[i] ~= nil then
				extraTableInfo["_labelInfo"][v][i] = module.Config[i]
			elseif labelInfoTable[i] ~= nil then
				extraTableInfo["_labelInfo"][v][i] = labelInfoTable[i]
			else
				extraTableInfo["_labelInfo"][v][i] = labelInfoDefault[i]
			end
		end
		v:FindFirstChild("_labelInfo"):Destroy()
	end
	if v:FindFirstChild("_buttonClickBL") then
		tableEffects["_buttonClickBL"][#tableEffects["_buttonClickBL"]+1] = v
		v:FindFirstChild("_buttonClickBL"):Destroy()
	end
	if v:FindFirstChild("_buttonHoverBL") then
		tableEffects["_buttonHoverBL"][#tableEffects["_buttonHoverBL"]+1] = v
		v:FindFirstChild("_buttonHoverBL"):Destroy()
	end	
end

for i,v in pairs(playerGui:GetDescendants()) do
	checkDescendants(v)
end
finished = true

function mouseHoverSound()
		
end

function buttonMove(guiPart,direction)
	debounce3 = true
	if direction == true then
		extraTableInfo["_buttonMove"][guiPart]["Deb"] = true
		local size = extraTableInfo["_buttonMove"][guiPart]
		guiPart:TweenSize(size["Open"],size["EasingDirection"],size["EasingStyle"],size["Time"],true)
	elseif direction == false then
		extraTableInfo["_buttonMove"][guiPart]["Deb"] = false
		local size = extraTableInfo["_buttonMove"][guiPart]
		guiPart:TweenSize(size["Close"],size["EasingDirection"],size["EasingStyle"],size["Time"],true)
	end
	debounce3 = false
end

function labelInfo(guiPart,visible)
	if visible == true then
		if playerGui:FindFirstChild("_labelInfo") then
			playerGui:FindFirstChild("_labelInfo"):Destroy()
		end
		doLabelInfo = true
		local gui = Instance.new("ScreenGui")
		gui.Parent = playerGui
		gui.Name = "_labelInfo"
		gui.ResetOnSpawn = false
		local textLabel = Instance.new("TextLabel")
		textLabel.Size = UDim2.new(.1,0,.25,0)
		textLabel.Parent = gui
		textLabel.AnchorPoint = Vector2.new(0,1)
		textLabel.TextWrapped = true
		textLabel.Position = UDim2.new(0,mouse.X + 5,0,mouse.Y - 5)
		for i,v in pairs(extraTableInfo["_labelInfo"][guiPart]) do
			textLabel[i] = v
		end
		local size = game:GetService("TextService"):GetTextSize(extraTableInfo["_labelInfo"][guiPart]["Text"],extraTableInfo["_labelInfo"][guiPart]["TextSize"],extraTableInfo["_labelInfo"][guiPart]["Font"],textLabel.AbsoluteSize)
		textLabel.Size = UDim2.new(0,size.X,0,size.Y)
	elseif visible == false then
		doLabelInfo = false
		if playerGui:FindFirstChild("_labelInfo") then
			playerGui:FindFirstChild("_labelInfo"):Destroy()
		end
	end
end

for i,v in pairs(tableEffects["_labelHover"]) do
	local size = extraTableInfo["_labelHover"][v]
	game:GetService("TweenService"):Create(v,TweenInfo.new(size["Time"],size["EasingStyle"],size["EasingDirection"],math.huge,true,0),{Position = extraTableInfo["_labelHover"][v]["Position"]}):Play()
end

while wait(updateTime) do
	if finished == true then
		local deb1 = false
		local deb1 = false
		local deb2 = false
		local deb3 = false
		local deb4 = false
		for i,tab in pairs(tableEffects) do
			for i,v in pairs(tab) do
				if v:IsA("GuiObject") then
					v.MouseLeave:Connect(function()
						if table.find(tableEffects["_buttonSounds"],v) and extraTableInfo["_buttonHover"][v]["Deb"] == true then
							extraTableInfo["_buttonHover"][v]["Deb"] = false
						end
						if table.find(tableEffects["_buttonMove"],v) and debounce3 == false and extraTableInfo["_buttonMove"][v]["Deb"] == true and deb3 == false then
							deb3 = true
							buttonMove(v,false)
						end
						if table.find(tableEffects["_labelInfo"],v) and debounce4 == true and deb4 == false and playerGui:FindFirstChild("_labelInfo") then
							debounce4 = false
							deb4 = true
							labelInfo(v,false)
						end
					end)
					v.MouseEnter:Connect(function()
						if table.find(tableEffects["_buttonSounds"],v) and not table.find(tableEffects["_buttonHoverBL"],v) and extraTableInfo["_buttonHover"][v]["Deb"] == false and doButtonHover == true and debounce1 == false and deb1 == false then
							extraTableInfo["_buttonHover"][v]["Deb"] = true
							deb1 = true
							debounce1 = true
							soundService.ui_hover1:Play()
							debounce1 = false
						end 
						if table.find(tableEffects["_buttonMove"],v) and debounce3 == false and extraTableInfo["_buttonMove"][v]["Deb"] == false and deb3 == false then
							deb3 = true
							buttonMove(v,true)
						end	
						if table.find(tableEffects["_labelInfo"],v) and debounce4 == false and deb4 == false and not playerGui:FindFirstChild("_labelInfo") then
							debounce4 = true
							deb4 = true
							labelInfo(v,true)
						end
					end)
				end
				if v:IsA("GuiButton") then
					v.MouseButton1Click:Connect(function()
						if table.find(tableEffects["_buttonSounds"],v) and not table.find(tableEffects["_buttonClickBL"],v) and doButtonClick == true and debounce2 == false then
							debounce2 = true
							soundService.ui_button1:Play()
							debounce2 = false
						end
					end)
				end
			end
		end
		playerGui.DescendantAdded:Connect(function(part)
			for i,v in pairs(part:GetDescendants()) do
				checkDescendants(v)
			end
		end)
		if doLabelInfo == true then
			local textFrame = playerGui:WaitForChild("_labelInfo"):WaitForChild("TextLabel")
			textFrame.Position = UDim2.new(0,mouse.X + 5,0,mouse.Y - 5)
		end
	end
end
2 Likes

So, the purpose of this script still seems cloudy to me, despite reading through both your description and the documentation provided at the top.

My current interpretation of it is that it automatically styles elements at runtime and initializes event listeners. However the listeners themselves are obfuscated, the debounce implementation isn’t scalable at all and really confusing, and you keep reconnecting events every twentieth of a second or so, which probably explains the crippling lag.

I’d typically beautify the code snippet and offer commentary in posts like these under #help-and-feedback:code-review but since a lot of it to me is too obscure even after I processed it, I’m going to give a few ideas for you to ponder and implement; if you refactor the script in a more readable way, I could give more comprehensive advise.

The Design

This looks like it was designed for general use, given the “don’t edit anything below” disclaimer. If this truly is the case, the way this is designed to listen and look for elements to initialize is not intuitive and pretty ill-explained. Perhaps you could try to make a version that uses CollectionService? This would let either yourself or others tag GUI elements to “play click sound” or some other functionality from this script and not deal with the ObjectValue implementation you’ve posted, then having to traverse every instance in the PlayerGui.

Events

Don’t connect a listener dynamically every twentieth of a second. It’s just… not good; there’s no way around it. Just connect an event listener once to an event.

Naming

Your variable names are pretty vague, their function isn’t immediately clear. Perhaps you could go through and name variables appropriately?

Take-away:

Attempt to build an implementation of this script with this triage of sorts in mind, and repost it at a later date here? If you do that, I believe the community as a whole could give you greater, more fine-tuned feedback.

3 Likes

is there another way i can loop or something else through the table with all the gui objects for the events rather than updating it every 1/20th a second? I cant really think of another way to do that.

Certainly! Just use events:

I’ve also been playing around in Studio for a hot second, and came up with an implementation to demonstrate how you could handle certain events and tag UI elements through CollectionService like I had previously suggested:

Guify

A LocalScript with its parent set to StarterGui, the manager of sorts.

local CollectionService = game:GetService("CollectionService");

local applyRedBackgroundOnHover = require(script.ApplyRedBackgroundOnHover);
local Util = require(script.Util);

--[[
    Whether or not debug messages should be displayed.
]]
local DEBUG = true;
--[[
    The Tag Editor for use with UI elements is actually quite the headache, and I kept accidentally
    adding the wrong things to certain tags. When this is set to true, it will prune tagged
    instances that don't make sense in context; when set to false, it will error if a non-GuiObject
    instance is attached to a tag.
]]
local SHOULD_PRUNE = true;

--[[
    Applys the given effect to all GuiObjects with the given `tag`. Additionally, if `prune` is set
    to `true`, removes any instances improperly tagged.
    @param {string} tag
    @param {(GuiObject) -> nil} effect
    @param {boolean} [prune = false]
]]
local function applyEffectToTaggedGuiObjectInstances(tag, effect, prune)
	prune = prune ~= nil and prune or false;

    -- Don't need pretty diagnostics. Maintainers and contributors are the only
    -- end-user for this function.
	assert(type(tag) == "string")
	assert(type(effect) == "function")
	assert(type(prune) == "boolean")

	local taggedInstances = CollectionService:GetTagged(tag)
	local isUsedTag = #taggedInstances ~= 0;

	if DEBUG and not isUsedTag then
		local message = string.format(
			"The tag %q is not currently in use.",
			tag
		);
		warn(message);
	end

	for _, instance in ipairs(taggedInstances) do
		if not Util.isGuiObject(instance) and prune then
			local message = string.format(
				"%q is assumed to not belong with the tag %q; removing",
				instance.Name,
				tag
			);
			warn(message);
			CollectionService:RemoveTag(instance, tag);
			continue;
		end

		local ok, errorMessage = pcall(effect, instance);

		if not ok then
			local message = string.format(
				"An error occured while applying the %q effect to %q: %q",
				tag,
				instance.Name,
				errorMessage
			);
			error(message, 2);
		end
	end
end

applyEffectToTaggedGuiObjectInstances(
	"RedBackgroundOnHover",
	applyRedBackgroundOnHover,
	SHOULD_PRUNE
);
Guify.ApplyRedBackgroundOnHover

A ModuleScript that exports a single function that connects event listeners to a GuiObject. In practice, there would be multiple ModuleScripts under the manager that each conenct different effects.

local Util = require(script.Parent.Util)

local RED_COLOR = Color3.fromRGB(220, 0, 78);

--[[
    Given a GuiObject, `guiObject`, attaches an event listener that turns said
    object red when the player's mouse hovers over it.
    @param {GuiObject} guiObject
    @returns {nil}
]]
local function applyRedBackgroundOnHover(guiObject)
    Util.validateGuiObject(guiObject, 2);

    -- A variable that stores what the frame's original background was. We hoist
    -- it so both listeners have access to it.
    local originalBackground;

    local function makeBackgroundRed()
        originalBackground = guiObject.BackgroundColor3;
        guiObject.BackgroundColor3 = RED_COLOR;
    end

    local function revertBackground()
        guiObject.BackgroundColor3 = originalBackground;
    end

    guiObject.MouseEnter:Connect(makeBackgroundRed);
    guiObject.MouseLeave:Connect(revertBackground);
end

return applyRedBackgroundOnHover;
Guify.Util

A ModuleScript that exports some utility functions.

local Util = {};

--[[
    Validates that the given object is a GuiObject.
    @param {unknown} value - The value to validate.
    @param {number} [level = 1] - The level to emit the possible error at.
    @returns {GuiObject}
]]
function Util.validateGuiObject(value, level)
    level = level ~= nil and level or 1;

	local isInstance = typeof(value) == "Instance";
    local isValid = isInstance and value:IsA("GuiObject");

	if not isValid then
		local parameterAsString = isInstance and value.ClassName or typeof(value);
		local message = string.format(
            "Invalid parameter! Expected a GuiObject, got %q",
            parameterAsString
        );
        error(message, level + 1);
    end

	return value;
end

--[[
    Returns if the given value is a GuiObject.
    @param {unknown} value - The value to check.
    @returns {boolean}
]]
function Util.isGuiObject(value)
    local isGuiObject = pcall(Util.validateGuiObject, value);
    return isGuiObject;
end

return Util;

What’s really nice is that it’s easy to edit what UI effects are effected through tags as opposed to a lot of verbose yet obscure ObjectValue instances.

2 Likes

I had no idea CollectionService existed, thanks a ton!

1 Like

Also notice that per each instance I connect one event listener to MouseEnter and one to MouseLeave, not continuously creating and connecting the listeners again and again.

If you can make a version of your script using these systems, feel free to post it here to get more feedback from the community!

2 Likes