Creating a Radial Menu
Creating a Radial Menu
A radial menu is a style of menu often found in console games due to its ease of use with analog thumbstick controllers. In a radial menu, the selectable options are arranged in a ring. When the user pushes the analog stick, the option in the direction the user pushed will be selected.
This tutorial will cover how to implement a radial menu, with an emphasis on gamepads, although the same approach can be used for mouse/keyboard or mobile games.
Setting up Menu
Menus can be created manually or through code. This tutorial uses code so it can easily be put into any game:
Radial menus typically have two states: opened and closed. This code first defines positions and sizes for the main frame of the menu. With the current values, the open state of the menu will be centered on the screen and take up half of the screen’s width and height. You can adjust
menuOpenSize to adjust how much of the screen the menu takes up and where it is centered.
Next, the script creates a template for the menu items. In this case, a
TextButton is used, as it is simple. You should change this to use a more complicated element that fits the style of your game and interface. Keep in mind the code will animate the menu opening by changing its size, so
TextButton/TextScaled makes sure the text dynamically changes size with the button.
AutoButtonColoris disabled as this tutorial is focused on working with a gamepad. If adapting this code for use with a mouse, you should enable this property.
Finally, the code creates a
Frame. The Frame’s
GuiObject/Position are set to
menuClosedPosition respectively, as the menu should be hidden initially.
After the menu screen and frame are set up, the script then automatically creates buttons arranged in a ring. There are several constants you can use to configure how many buttons appear in the ring and how they are oriented.
newMenuItem function is used to create new menu items and to add these items to a table that will be used later when determining which item to select. The function takes three arguments:
name indicates the name of the option (and is also used to label it in the GUI),
angle sets where on the ring the option is centered, and
range sets how wide the input arc is.
The function first creates a clone of the
itemTemplate that was created earlier. It also sets the position of the button based on
angle. The function also stores a 2D vector made from the input
angle. This will be used later to compare with user input to figure out which item is selected.
for i = 1, NUM_OPTIONS do local angle = (360 / NUM_OPTIONS) * (i - 1) local name = "Option" .. i newMenuItem(name, angle, 360 / NUM_OPTIONS) end
newMenuItem function is declared, the code then executes a loop to create the menu items. The constant
NUM_OPTIONS determines how many menu options to create. The angle for each item is determined by dividing 360 (the number of degrees in a circle) by
NUM_OPTIONS and then multiplying by the current option. The range is also determined by dividing 360 by
NUM_OPTIONS. These values are all passed into
newMenuItem to create the button.
Opening the Menu
In this tutorial the menu is toggled open and closed with the left bumper.
ContextActionService can be used to bind this button to a custom function that opens and closes the menu.
-- The menu uses left bumper which the backpack also uses. Disabling backpack UI to prevent conflict game.StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, false) local function toggleMenu(actionName, inputState) if inputState == Enum.UserInputState.Begin then openMenu() elseif inputState == Enum.UserInputState.End then closeMenu() end end ContextActionService:BindAction("ToggleMenu", toggleMenu, false, Enum.KeyCode.ButtonL1)
The default Roblox control script binds the bumpers to toggle between items in the backpack. In order to rebind this input to the radial menu, the backpack UI has to be disabled using
StarterGui/SetCoreGuiEnabled. If you use a different input than the bumpers for your radial menu, you should be mindful of the Roblox defaults to prevent any conflicts.
toggleMenu function is bound to the left bumper using
inputState to see what state the bumper is in. If it is in the Begin state, then the user just started pressing the bumper and the menu needs to be opened. Otherwise, if it is in the End state, the menu needs to be closed.
local function openMenu() -- Store GuiNavigationEnabled and AutoSelectGuiEnabled and then set both to false wasGuiNavigationEnabled = GuiService.GuiNavigationEnabled wasAutoSelectGuiEnabled = GuiService.AutoSelectGuiEnabled GuiService.GuiNavigationEnabled = false GuiService.AutoSelectGuiEnabled = false -- Bind onThumbstickMoved function ContextActionService:BindAction("RadialMenu", onThumbstickMoved, false, Enum.KeyCode.Thumbstick1) -- Make sure frame is visible and play opening animation menuFrame.Visible = true menuFrame:TweenSizeAndPosition(menuOpenSize, menuOpenPosition, Enum.EasingDirection.Out, Enum.EasingStyle.Quart, .5, true) menuOpen = true end
openMenu function needs to do several things. First, it needs to disable
GuiService/AutoSelectGuiEnabled and store the values so that they can be restored later. These settings work well for other menu types, but are not well suited for radial menus.
Vector2.new(math.cos(angle), math.sin(angle)). A unit vector has a magnitude of 1, so it can be safely omitted from the divisor.
The angle between the vectors is then compared to half the item’s range. If it is less than half the item’s range, that means the thumbstick vector is somewhere in the arc defined for that item. If this is the case,
getButtonFromVector returns the item’s button.
getButtonFromVector returns a button,
onThumbstickMoved sets the
GuiService/SelectedObject to that button. This highlights the button so the user knows it has been selected.
At the end of
onThumbstickMoved, if the thumbstick was not pushed past the deadzone, the SelectedObject is set to nil to clear the selection.
Closing the Menu
When the menu closes, the code needs to call a function based on the option that was selected (if any).
-- Function to call when item is selected. This is where you should put your custom code -- to implement your menu local function onMenuSelect(option) print(option, "selected") end local function closeMenu() -- Restore GuiNavigationEnabled and AutoSelectGuiEnabled GuiService.GuiNavigationEnabled = wasGuiNavigationEnabled GuiService.AutoSelectGuiEnabled = wasAutoSelectGuiEnabled -- Unbind onThumbstickMoved function ContextActionService:UnbindAction("RadialMenu") if GuiService.SelectedObject then -- If there is a selection when the menu closed, this is the option the user wanted onMenuSelect(GuiService.SelectedObject) end -- Clear selected object and play closing animation GuiService.SelectedObject = nil menuFrame:TweenSizeAndPosition(menuClosedSize, menuClosedPosition, Enum.EasingDirection.Out, Enum.EasingStyle.Quart, .4, true, function() -- Callback function at end of the animation. If the user hasn't reopened the menu hide it if not menuOpen then menuFrame.Visible = false end end) menuOpen = false end
closeMenu function first restores GuiNavigationEnabled and AutoSelectGuiEnabled in case the rest of the game relies on those settings. It also unbinds
onThumbstickMoved so that the thumbstick can again be used for other purposes. Then, if there is a SelectedObject, the function calls
onMenuSelect, passing in the SelectedObject as an argument.
onMenuSelect is where you will want to add any custom code. Based on the button that was passed in, you can write code for whichever action you need to happen when the button is selected.
onMenuSelect is called,
closeMenu then clears the current selection and plays an animation to hide the menu.
Below is all of the code to implement a radial menu as seen above. The code should be in a
LocalScript and placed either in