System that only allows one menu in a table to be active at once?

In my game, I’m trying to make a bunch of menus for various functions. I am trying to create a system that only allows one menu open at a time so users don’t have to manually close every tab they have opened.
Currently, I am using a tweening function to open and close the menus

-- Function to open the GUI by tweening it upwards
local function openGUI()
    local targetPosition = UDim2.new(0.5, 0, 0.5, 0) -- Center of the screen
    local tween = TweenService:Create(Frame, getTweenInfo(), {Position = targetPosition})
	tween:Play()
end

-- Function to close the GUI by tweening it downwards
local function closeGUI()
    local targetPosition = UDim2.new(0.5, 0, 1.5, 0) -- Below the screen
    local tween = TweenService:Create(Frame, getTweenInfo(), {Position = targetPosition})
    tween:Play()
end

To my knowledge, it is not really possible for scripts to communicate with and call functions from one another (my guess is that they’re just shy :P). I’ve tried using a boolValue and then having the script listen for it:

local isOpen = script:FindFirstChild("Open")

local function toggleDoor()
	if isOpen == true then
		closeGUI()
		isOpen = false
		elseif isOpen == false then
		openGUI()
		isOpen = true
		else
		print("failure")
	end
	isOpen = not isOpen
end

isOpen:GetPropertyChangedSignal("Value"):Connect(toggleDoor)
ImageButton.MouseButton1Click:Connect(toggleDoor())

But somehow, when I click on the variable, it prints “failure”. Then when I click it again it functions, however, it is reversed. The variable is true when it should be false.

Is this the best solution, or is there a more efficient method?
Preferably, there’d be some kind of table where I can add more GUIs if necessary.

Thank you for your time!

1 Like

This part might be causing an error. You should try changing this to:

ImageButton.MouseButton1Click:Connect(toggleDoor) -- Without the ()

Hopefully this helps! :smiley:

2 Likes

Oh, that must’ve been an error from copy and pasting. I readded it and it is still stuck, but thank you for your help :slight_smile: !

1 Like

This is happening because isOpen is referring to the BoolValue and not its Value property, which means that the conditional statements that check if isOpen == true or if isOpen == false is comparing the object to true or false.

Updating it to check if isOpen.Value == true or false should resolve that issue:

Example Revision

local isOpen = script:FindFirstChild("Open")

local function toggleDoor()
	if isOpen.Value == true then
		closeGUI()
		isOpen.Value = false
    elseif isOpen.Value == false then
		openGUI()
		isOpen.Value = true
    else
		print("failure")
	end

	isOpen.Value = not isOpen.Value
end

isOpen:GetPropertyChangedSignal("Value"):Connect(toggleDoor)
ImageButton.MouseButton1Click:Connect(toggleDoor)

*And @Lauri9 was also correct about the function call, but that was only 1 of 2 issues that were present. With both of those changes, it should work as intended.

Oh gosh, I could have sworn I fixed that last night. It was almost midnight though… It must have not saved properly.

Ok, I revised my code (dont ask me why I called the function toggleDoor and not toggleGUI in the first place), but now the code automatically runs twice on game startup, and refuses to respond.

local isOpen = script:FindFirstChild("Open")  -- Initialize a boolean variable with a value of false

local function toggleGUI()
	print("Toggling")
	if isOpen.Value == true then
		closeGUI()
		print("Attempting to close...")
	elseif isOpen.Value == false then
		print("Attempting to open...")
		openGUI()
		else
		print("Attempt Failure")
	end
	isOpen = not isOpen
end

isOpen:GetPropertyChangedSignal("Value"):Connect(toggleGUI())

ImageButton.MouseButton1Click:Connect(toggleGUI())

image
image

(Immediately after joining)

Then it tells me that the boolvalue isnt there anymore?

Thank you so much for your help!

1 Like

As a suggestion if you want to try and call functions from in between scripts, I would suggest trying out ModuleScripts!

2 Likes

Ah, that’s probably because of this:

Make sure to change that to isOpen.Value = not isOpen.Value, so that it updates the Value property. Otherwise, it’ll update the variable itself to false instead of referring to the BoolValue object (hopefully I explained that properly).


Edit 1: Also make sure to update the last line of code with the change that @Lauri9 suggested:

ImageButton.MouseButton1Click:Connect(toggleGUI)

Edit 2: And then lastly, consider the suggestion of @FBIagent9903 if you want to create systems that can communicate between scripts. There’s resources on the Roblox Creator Documentation site regarding Modules if you want to learn more.

1 Like

Awesome! It’s working now. I should really be proofreading these better for such simple mistakes.

Thank you so much!

P.S. this error comes up after using it, is this an important error or is it negligable?
image
It’s making a loop, isnt it

1 Like

Yup, the :GetPropertyChangedSignal() listener is calling the function over and over again because isOpen.Value is being changed every time the function is called.

Now that I think about it, I don’t think isOpen.Value = not isOpen.Value would be necessary to include in the function at all, since the necessary changes that need to happen are only related to opening or closing the GUI after you’ve detected the value being changed (so you could remove that from the function and then that error will no longer occur).

1 Like

That makes sense, however, the button no longer works because it cant change the bool value. Naturally, I changed

ImageButton.MouseButton1Click:Connect(toggleGUI())

to

ImageButton.MouseButton1Click:Connect(function()
	if isOpen.Value == true then
		isOpen.Value = false
	elseif isOpen.Value == false then
		isOpen.Value = true
	end
end)

But now I’m running into the same problem where, when the game starts, the code somehow thinks that the variable is true?
image

When I had the output open and the bool value selected, I clicked the button and it set the bool value to true, but the output says “Attempting to close…”

1 Like

Oh yeah, I forgot that it’s not being updated from a single place somewhere else in the game.

Before trying to explain a possible solution, I think it’d be good to have some more clarification about how the menus are set up, first.

  • Are all of the menus within the same ScreenGui?

  • Were you planning on having these functions included in separate scripts for each menu, or would everything be in a single script?

  • Would this singular BoolValue be referenced by each of the menus?


And sorry about taking quite a while to help ya figure out a viable solution. It’s past 6 AM here and I should have gone to sleep a long time ago… so after this I’m going to actually get some rest haha

1 Like
  1. Right now, all the menus are seperate screenGUIs. “Main” is the toolbar with the buttons on it.
    image
    Maybe it would be better to consolidate it under one Screen gui
    image

  2. The button controls the frames right now, which may not be a great idea for efficiency and organization.
    image

  3. The buttons would reference the bool values of all the other menus (except for the toolbar and main start menu).

1 Like

Thanks for clarifying! With all of that in mind, here’s one possible way of going about that should be able to be scaled fairly easily *(without using BoolValues, though), even if you create more menus.

  1. Create a ModuleScript in the ReplicatedStorage that will act as the centralized place for handling the opening / closing of GUIs and keeping track of the menu that is currently open.

  2. Whenever one of the buttons that is meant to open or close the Frame is pressed, have it call the relevant function in the ModuleScript so it can handle everything without needing to copy and paste the functions into scripts for each menu.

If you have any questions, feel free to ask! Although I may not respond for several hours because it is now 7 AM and I am going to sleep lol.

Example ModuleScript

local MenuModule = {}

MenuModule.activeMenu = nil

local function openGUI(Frame, tweenInfo) -- Function based on what was included in the original post
    if not tweenInfo then
        -- Consider creating a backup tweenInfo in case one wasn't sent through
    end

    local targetPosition = UDim2.new(0.5, 0, 0.5, 0) -- Center of the screen
    local tween = TweenService:Create(Frame, tweenInfo, {Position = targetPosition})
	tween:Play()
end

local function closeGUI(Frame, tweenInfo) -- Function based on what was included in the original post
    if not tweenInfo then
        -- Consider creating a backup tweenInfo in case one wasn't sent through
    end

    local targetPosition = UDim2.new(0.5, 0, 1.5, 0) -- Below the screen
    local tween = TweenService:Create(Frame, tweenInfo, {Position = targetPosition})
    tween:Play()
end

function MenuModule.updateCurrentMenu(Frame, desiredFrameVisibility, tweenInfo)
    if Frame ~= nil then
        if desiredFrameVisibility == true then

            local activeMenuCheck = MenuModule.activeMenu
            if activeMenuCheck ~= nil then
                closeGUI(activeMenuCheck)
            end
--[[
Consider creating a backup tweenInfo somewhere (preferably in this Module,
for ease of access), so that in the case where the currently active menu needs,
to be closed as a result of another menu being opened, the currently visible Frame
will have appropriate settings given the desired style of the closing animation.
--]]

            MenuModule.activeMenu = Frame
            openGUI(Frame, tweenInfo)
            
        elseif desiredFrameVisibility == false then
            closeGUI(Frame, tweenInfo)
        end
    end
end

Example of a LocalScript communicating with the ModuleScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MenuModule = require(ReplicatedStorage.MenuModule)

local ImageButton -- Reference to ImageButton here
local Frame -- Reference to relevant Frame here

local function getTweenInfo()
--[[ If this needs to be specific for each menu, then you can retrieve it
in the same way you are already, but then have it sent to the ModuleScript.
Otherwise, a universal TweenInfo could be stored in the ModuleScript so these
LocalScripts don't have to communicate it every time a menu needs to be
opened or closed --]]

-- If the function below calls this function, have it return the TweenInfo
end

ImageButton.Activated:Connect(function()
    local currentFrameVisibility = Frame.Visible
    local desiredFrameVisibility = not currentFrameVisibility
--[[ If the Frame is currently invisible, the menu is closed. Since the player
pressed the button, they want to open it, so we can just reverse the value of
Frame.Visible and send that to the ModuleScript so it knows what value it
should be updated to --]]

    local tweenInfo = getTweenInfo()

    MenuModule.updateCurrentMenu(Frame, desiredFrameVisibility, tweenInfo)
end)
2 Likes

Thank you. Your help is greatly appreciated. Sleep well!

1 Like

Okay, after a while of trial and error, I ended up with these two scripts.
They work well, no errors yet. But the GUIs are still overlapping. Can you help me start on the activeMenu check?

Module

local MenuModule = {}

MenuModule.activeMenu = nil
local TweenService = game:GetService("TweenService")

local function openGUI(Frame, tweenInfo) -- Function based on what was included in the original post
	if not tweenInfo then
		return TweenInfo.new(
			0.5, -- Time
			Enum.EasingStyle.Quad, -- Easing style
			Enum.EasingDirection.Out, -- Easing direction
			0, -- Repeat count (0 = no repeat)
			false, -- Reverses (true = reverse)
			0 -- Delay time   
		)
	end
	local targetPosition = UDim2.new(0.5, 0, 0.5, 0) -- Center of the screen
	local tween = TweenService:Create(Frame, tweenInfo, {Position = targetPosition})
	tween:Play()
end

local function closeGUI(Frame, tweenInfo) -- Function based on what was included in the original post
	if not tweenInfo then
		return TweenInfo.new(
			0.5, -- Time
			Enum.EasingStyle.Quad, -- Easing style
			Enum.EasingDirection.Out, -- Easing direction
			0, -- Repeat count (0 = no repeat)
			false, -- Reverses (true = reverse)
			0 -- Delay time
		)
	end
	
	local targetPosition = UDim2.new(0.5, 0, 1.5, 0) -- Below the screen 
	local tween = TweenService:Create(Frame, tweenInfo, {Position = targetPosition})
	tween:Play()
	
end

function MenuModule.updateCurrentMenu(Frame, desiredFrameVisibility, tweenInfo)
	print(MenuModule.activeMenu)
	if Frame ~= nil then
		if desiredFrameVisibility == true then

			local activeMenuCheck = MenuModule.activeMenu
			if activeMenuCheck ~= nil then
				closeGUI(activeMenuCheck)
			end
			
			MenuModule.activeMenu = Frame
			openGUI(Frame, tweenInfo)

		elseif desiredFrameVisibility == false then
			closeGUI(Frame, tweenInfo)
		end

	end
end

return MenuModule

Local Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MenuModule = require(ReplicatedStorage.MenuModule)
print(MenuModule)

local associatedButtonName = script.Parent.Name

local ImageButton = script.Parent.Parent.Parent:WaitForChild("Toolbar"):WaitForChild(associatedButtonName)
local Frame = script.Parent

local function getTweenInfo()
	    return TweenInfo.new(
        0.5, -- Time
        Enum.EasingStyle.Quad, -- Easing style
        Enum.EasingDirection.Out, -- Easing direction
        0, -- Repeat count (0 = no repeat)
        false, -- Reverses (true = reverse)
        0 -- Delay time
    )

end
local currentFrameVisibility = false
ImageButton.MouseButton1Click:Connect(function()

	local desiredFrameVisibility = not currentFrameVisibility

	local tweenInfo = getTweenInfo()
	
	MenuModule.updateCurrentMenu(Frame, desiredFrameVisibility, tweenInfo)
	
	currentFrameVisibility = not currentFrameVisibility
	
end)```
1 Like

Sure! I think I know which section of code is causing that to happen, but there are a few things I’d like to mention:

For the example LocalScript code, the way that currentFrameVisibility and desiredFrameVisibility are defined and updated could cause some issues. For example, if any of the Menus happen to start with their Visible property set to true and the player tried to close it directly, it would require them to press it twice because the script would begin by setting desiredFrameVisibility to true and sending a request to the module to make it visible.

In addition to that, because currentFrameVisibility is only updated upon the activation of that ImageButton, this would very quickly create discrepancies when another menu is opened, since it would cause this frame to be automatically closed by the closeGUI function in the module without updating the currentFrameVisibility variable in the LocalScript (since the player needed to press the ImageButton to do so).

As a result, it’d be more reliable to move currentFrameVisibility into the function and to directly reference the Frame.Visible property for currentFrameVisibility, as that would ensure that the function will always be referring to the current visibility of the Frame whenever the player decides to press the button.


Within the openGUI and closeGUI functions, you wouldn’t need to return the TweenInfo since it would be used in those functions (and returning is typically meant for sending something back to the line of code where the function was called from). In this case, you would just need to override the tweenInfo variable with that newly created TweenInfo. And since both of the backup TweenInfos happen to have the same settings, it may be better to keep that stored in a single place (such as within the module’s table) for ease of access and organization.

As far as I understand, this is what was causing the menus to continue overlapping, since the activeMenu did not have a tweenInfo sent with it to the closeGUI function. As a result, it would have to create a new TweenInfo, but when it reaches that part of the function and it returns it, it stops the rest of the function from running, meaning that the frame is not moved off of the screen.

There’s also one more change I’d make to the ModuleScript that I didn’t consider earlier, which would be to set activeMenu to nil if the updateCurrentMenu function is called with the desiredFrameVisibility being set to false, so that the next time a menu is opened, it won’t try to set that menu’s visibility to false again since it was already manually closed by the player. It would have still worked properly without making this change, but this creates more consistency.


Here’s an example revision of both of those scripts:

Revised ModuleScript Example

local MenuModule = {}

MenuModule.activeMenu = nil
MenuModule.defaultTweenInfo = TweenInfo.new(
    0.5, -- Time
    Enum.EasingStyle.Quad, -- Easing style
    Enum.EasingDirection.Out, -- Easing direction
    0, -- Repeat count (0 = no repeat)
    false, -- Reverses (true = reverse)
    0 -- Delay time   
)

local TweenService = game:GetService("TweenService")

local function openGUI(Frame, tweenInfo) -- Function based on what was included in the original post
	if not tweenInfo then
		tweenInfo = MenuModule.defaultTweenInfo
	end

	local targetPosition = UDim2.new(0.5, 0, 0.5, 0) -- Center of the screen
	local tween = TweenService:Create(Frame, tweenInfo, {Position = targetPosition})
	tween:Play()
end

local function closeGUI(Frame, tweenInfo) -- Function based on what was included in the original post
	if not tweenInfo then
		tweenInfo = MenuModule.defaultTweenInfo
	end
	
	local targetPosition = UDim2.new(0.5, 0, 1.5, 0) -- Below the screen 
	local tween = TweenService:Create(Frame, tweenInfo, {Position = targetPosition})
	tween:Play()
end

function MenuModule.updateCurrentMenu(Frame, desiredFrameVisibility, tweenInfo)
	print(MenuModule.activeMenu)
	if Frame ~= nil then
		if desiredFrameVisibility == true then

			local activeMenuCheck = MenuModule.activeMenu
			if activeMenuCheck ~= nil then
				closeGUI(activeMenuCheck)
			end
			
			MenuModule.activeMenu = Frame
			openGUI(Frame, tweenInfo)

		elseif desiredFrameVisibility == false then
            if Frame == MenuModule.activeMenu then
                MenuModule.activeMenu = nil
            end

			closeGUI(Frame, tweenInfo)
		end

	end
end

return MenuModule

Revised LocalScript Example

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MenuModule = require(ReplicatedStorage.MenuModule)
print(MenuModule)


local Frame = script.Parent
local associatedButtonName = Frame.Name

local ToolBar = Frame.Parent.Parent:WaitForChild("Toolbar")
--[[
I'd recommend defining the other objects (e.g. Frame.Parent and then
the Parent of that Parent) so that if you ever change its hierarchy, you
know what object it was supposed to be referring to at each step
--]]
local ImageButton = Toolbar:WaitForChild(associatedButtonName)


local function getTweenInfo()
    return TweenInfo.new(
        0.5, -- Time
        Enum.EasingStyle.Quad, -- Easing style
        Enum.EasingDirection.Out, -- Easing direction
        0, -- Repeat count (0 = no repeat)
        false, -- Reverses (true = reverse)
        0 -- Delay time
    )
end

ImageButton.MouseButton1Click:Connect(function()
    local currentFrameVisibility = Frame.Visible
	local desiredFrameVisibility = not currentFrameVisibility

	local tweenInfo = getTweenInfo()
	
	MenuModule.updateCurrentMenu(Frame, desiredFrameVisibility, tweenInfo)
end)

Is there a way to do this without making it invisible? I want the tween to be visible.
Secondly, when I didn’t put the tweenInfo in again, it gave me the error “Argument 2 missing or nil.” when opening a second time. (Start > Open menu > Close menu > Open menu (ERROR))

And lastly, how can I set up a close button within the frame?

Again, thanks for your help!

1 Like

The Tween would not be invisible; the currentFrameVisibility variable is just referring to the current Visibility value of the Frame, and when desiredFrameVisibility is set to not currentFrameVisibility, it’s only storing a value in that variable which is the opposite of currentFrameVisibility. It doesn’t actually change the visibility of the frame, it stores the value in the variable so it can be sent to the updateCurrentMenu function within the module.

Which function is that error coming from? *And, so we’re on the same page, what do both of the scripts look like now?

Ah, I had structured the LocalScript in a way where if the player pressed the same ImageButton that they pressed to open the menu, it would close it. If you wanted a completely separate button to close it (with the option to also click the original button that had opened it), you could create a new function within the LocalScript which calls the updateCurrentMenu function exclusively with a second value of false. Here’s an example:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MenuModule = require(ReplicatedStorage.MenuModule)
print(MenuModule)


local Frame = script.Parent
local associatedButtonName = Frame.Name
local closeButton -- Reference to the newly created "close menu" button here

local ToolBar = Frame.Parent.Parent:WaitForChild("Toolbar")
--[[
I'd recommend defining the other objects (e.g. Frame.Parent and then
the Parent of that Parent) so that if you ever change its hierarchy, you
know what object it was supposed to be referring to at each step
--]]
local ImageButton = Toolbar:WaitForChild(associatedButtonName)


local function getTweenInfo()
    return TweenInfo.new(
        0.5, -- Time
        Enum.EasingStyle.Quad, -- Easing style
        Enum.EasingDirection.Out, -- Easing direction
        0, -- Repeat count (0 = no repeat)
        false, -- Reverses (true = reverse)
        0 -- Delay time
    )
end

ImageButton.MouseButton1Click:Connect(function()
    local currentFrameVisibility = Frame.Visible
	local desiredFrameVisibility = not currentFrameVisibility

	local tweenInfo = getTweenInfo()
	
	MenuModule.updateCurrentMenu(Frame, desiredFrameVisibility, tweenInfo)
end)

closeButton.MouseButton1Click:Connect(function()
    local tweenInfo = getTweenInfo()
    MenuModule.updateCurrentMenu(Frame, false, tweenInfo)
end)

The Tween would not be invisible; the currentFrameVisibility variable is just referring to the current Visibility value of the Frame,

But when Frame.Visible is set to false, the Frame wont be rendered. I also don’t see anything that would switch that value, wouldn’t it stay at true forever? If it wasn’t clear, I am not changing the value of the Frame.Visible property, I’m just hiding it off screen. Would you explain more?

Which function is that error coming from? *And, so we’re on the same page, what do both of the scripts look like now?

I’m pretty sure it was coming from the Module, but I cant check now because Im not at my computer. I deduced that it was talking about openGUI(Frame, tweenInfo) so I put the tweenInfo in again.
Both scripts right now look like the ones I sent here

you could create a new function within the LocalScript which calls the updateCurrentMenu function exclusively with a second value of false . Here’s an example:

Thanks, that makes sense!

1 Like

Ahhh for some reason I had been stuck thinking that the Frame’s visibility was also being set to false after it went off-screen. Thanks for pointing that out because I kept missing that every time lol.

In that case, I have an idea which would simultaneously allow for the Tweens within the open and close GUI to appear as intended while also making the usage of referring to Frame.Visible possible so that the currentFrameVisibility variable would not be susceptible to returning inaccurate values if the menu is closed from the opening of a different menu.

Ideas

  • When closing a menu, listen for the completion of the Tween with Tween.Completed, and then update the Frame’s visibility to false.
local function closeGUI(Frame, tweenInfo) -- Function based on what was included in the original post
	if not tweenInfo then
		tweenInfo = MenuModule.defaultTweenInfo
	end
	
	local targetPosition = UDim2.new(0.5, 0, 1.5, 0) -- Below the screen 
	local tween = TweenService:Create(Frame, tweenInfo, {Position = targetPosition})
	tween:Play()
    tween.Completed:Wait()

    Frame.Visible = false
end

  • When opening a menu, immediately set the Frame’s visibility to true and then play the Tween.
local function openGUI(Frame, tweenInfo) -- Function based on what was included in the original post
	if not tweenInfo then
		tweenInfo = MenuModule.defaultTweenInfo
	end

    Frame.Visible = true

	local targetPosition = UDim2.new(0.5, 0, 0.5, 0) -- Center of the screen
	local tween = TweenService:Create(Frame, tweenInfo, {Position = targetPosition})
	tween:Play()
end

This would ensure that the Frame is not unnecessarily set to visible when it’s meant to be hidden off-screen while avoiding any interference with the visualization of the Tweens (making sure that it doesn’t suddenly appear or disappear while the Tween is still playing), all while allowing the function in the LocalScript to be able to refer to the current “state” (open or closed) of the menu, through its Visible property.


Oh; the ModuleScript code in that post still has the return TweenInfo in both the openGUI and closeGUI functions, which would be stopping the function from running when there is no TweenInfo provided.

I explained an example solution for that and a possible revision in the post right after you sent that one, if you want to give that a try (although I don’t think that is what would be causing the error to appear, because it would stop the function before it had a chance to error, if I understand correctly).

*If you decide to also go with the idea for updating the Frame’s visibility before / after the Tweens, then the example LocalScript code I provided in that same post would be compatible with that (since it’s checking the value of Frame.Visible when the player presses the ImageButton).


Whenever you have the time to check it, please let me know (no rush)!