Radial Popup Menu
Problem
You want a radial menu - a ring of buttons that pops up for you to choose an option.
Solution
Radial menus are used in a variety of games to allow access to a selection of buttons. For example, clicking on an NPC in a game allows you to choose what action to take: talk, inspect, attack, etc.
The specific look-and-feel of your menu should match with your game’s esthetic. For this demo, we’ll focus on the mechanics of making the menu work, and leave the styling choices to you.
Here’s the node setup:
We’re using a TextureButton
as our root node. This is the button you’ll click to open/close the menu.
The “Buttons” Control
node is the container where you’ll place any number of items that you want. For this example, we’re using some buttons from our Cooldown Button recipe.
Finally, we have a Tween
to handle animating the menu opening/closing.
Now, let’s look at the script for the button:
extends TextureButton
export var radius = 120
export var speed = 0.25
var num
var active = false
Here are our variables. radius
represents the “size” of the menu: the radius of the circle. speed
is for the animation - smaller numbers are faster.
num
keeps track of how many buttons there are, while active
is a flag to track whether the menu is open or closed.
func _ready():
$Buttons.hide()
num = $Buttons.get_child_count()
for b in $Buttons.get_children():
b.rect_position = rect_position
In _ready()
we start setting things up: hiding the menu buttons by default, and setting their positions to that of the main button.
Now connect the pressed
signal of the main button, and the tween_all_completed
signal of the Tween
.
func _on_StartButton_pressed():
disabled = true
if active:
hide_menu()
else:
show_menu()
Clicking the button show/hides the menu. We also need to disable the button, or else clicking again while the tween is playing would restart it.
func _on_Tween_tween_all_completed():
disabled = false
active = not active
if not active:
$Buttons.hide()
When the tween animation finishes, toggle the active state and enable the button again.
Let’s look at the show_menu()
function:
func show_menu():
var spacing = TAU / num
for b in $Buttons.get_children():
# Subtract PI/2 to align the first button to the top
var a = spacing * b.get_position_in_parent() - PI / 2
var dest = b.rect_position + Vector2(radius, 0).rotated(a)
$Tween.interpolate_property(b, "rect_position",
b.rect_position, dest, speed,
Tween.TRANS_BACK, Tween.EASE_OUT)
$Tween.interpolate_property(b, "rect_scale",
Vector2(0.5, 0.5), Vector2.ONE, speed,
Tween.TRANS_LINEAR)
$Buttons.show()
$Tween.start()
In this function we calculate the angle we need between each item (spacing
). We then loop through each button and find its destination (dest
) based on this angle and the chosen radius
. For each button, we’re tweening two properties, rect_position
and rect_scale
, to get our desired effect.
hide_menu()
performs the exact opposite function:
func hide_menu():
for b in $Buttons.get_children():
$Tween.interpolate_property(b, "rect_position", b.rect_position,
rect_position, speed, Tween.TRANS_BACK, Tween.EASE_IN)
$Tween.interpolate_property(b, "rect_scale", null,
Vector2(0.5, 0.5), speed, Tween.TRANS_LINEAR)
$Tween.start()
Here’s the end result: