+2 votes

Hello,

I'm working on a new game and in it I know I want to have a pie menu. The game is for a touch screen, so you'll hit a button and then more options will appear equally spaced around your finger, kind of like this:
https://www.youtube.com/watch?v=ImvuYqMW4q8

I'm not into code yet as I'm still working on other things, but I am starting to think about what the best approach would be. Ideas? I had two thoughts,

I could refresh my sine/cosine math, and use them to calculate the position of the sub menu objects based on a radius from the main object. That's probably the way to go?

Or I could have the main object that has child containers that are the width of the radius. The pivot point of the containers to the far left of the container, and aligned with the center of the main object. Then the submenu object would be anchored to the far right of the container. Then I could rotate each container x degrees. The problem with this approach would be that then the submenu object would also be rotated (I think) and I'd like the label to still be orientated normal. Then maybe if I just set the rotation to *-1 of the rotation of it's parent all will be well?

Advice is welcome.

in Engine by (24 points)

2 Answers

0 votes

I would definately go with the sinus/cosinus approach. Especially since you want circular buttons (so no need to actually rotate the buttons themselves).

You can brush up on how to do it or just grab another tutorial online. Not Godot specific but something like this will help you out:
https://www.robloxdev.com/articles/Creating-a-Radial-Menu

But there are also a lot of similiar tutorials online.

by (14 points)
+4 votes

I know the question is a bit old, but thought I would contribute some code to anyone else looking for radial menus like I was. Be warned mine is very rough and could use a lot of improvements, but it works and I think is a good starting place at least.

Usage
First create a container node, the base one that doesn't actually position anything. Next add a script to it and copy what I have below:

Warning: I made some tweaks to this (removed extra unused code, added comments) without testing so it may not run immediately.

extends Container

#Creates a radial container node

export var button_radius = 100 #in godot position units
export var radial_width = 50 #in godot position units

# Called when the node enters the scene tree for the first time.
func _ready():
    place_buttons()

#Repositions the buttons
func place_buttons():
    var buttons = get_children()

    #Stop before we cause problems when no buttons are available
    if buttons.size() == 0:
        return

    #Amount to change the angle for each button
    var angle_offset = (2*PI)/buttons.size() #in degrees

    var angle = 0 #in radians
    for btn in buttons: 
        #calculate the x and y positions for the button at that angle
        var x = cos(angle)*button_radius
        var y = sin(angle)*button_radius

        #Note: A bit confused but somehow godot corrects the sign on it's own so that isn't needed.

        #set button's position
        #>we want to center the element on the circle. 
        #>to do this we need to offset the calculated x and y respectively by half the height and width
        var corner_pos = Vector2(x, -y)-(btn.get_size()/2) #Screen coordinates so calculated y must be negated
        btn.set_position(corner_pos)

        #Advance to next angle position
        angle += angle_offset

#utility function for adding buttons and recalculating their positions
#TODO: Should probably just use a signal to run place_button on any tree change
func add_button(btn):
    add_child(btn)
    place_buttons()

Result
The result isn't viewable in the editor, but if you run the project it will position any buttons in the container so they are centered and spread evenly on an invisible line at the configured radius. Screenshot below:

enter image description here

by (28 points)

No need to mess around with sin/cos. Vectors can do the job for you. All this:

    var x = cos(angle)*button_radius
    var y = sin(angle)*button_radius
    var corner_pos = Vector2(x, -y)-(btn.get_size()/2) #Screen coordinates so calculated y must be negated
    btn.set_position(corner_pos)
    angle += angle_offset

Can be replaced with this:

    btn.rect_position = Vector2(button_radius, 0).rotated(angle)
    angle += angle_offset

Note that you can also make this a tool script if you want to see the result in the editor.

Ah thanks, that's good to know. Took 20-30 minutes dredging up trigonometry memory to solve that.

Though your code is not completely correct for what I was wanting. I think for a radial menu you would want the buttons centered on the circle from the given radius. Basically I just replaced my trig calcs with the vector then used that for the centering formula

for btn in buttons: 
    #calculates the buttons location on the circle
    var circle_pos = Vector2(button_radius, 0).rotated(angle)

    #set button's position
    #>we want to center the element on the circle. 
    #>to do this we need to offset the calculated x and y respectively by half the height and width
    btn.rect_position = circle_pos-(btn.get_size()/2)

    #Advance to next angle position
    angle += angle_offset

I haven't gotten to a tutorial for making tools yet. Once I do that and solve another issue I'll update my answer. Of course there are other improvements that could be made, but that should make a nice basic version of a radial container.

Thanks for the Feedback!

Issue with the button-based pie menus is that when the text grows, the alignment goes all wobbly like so:

enter image description here

So I tried to fix it. The following is me trying to get that to work (and failing, kinda)

I wanted to avoid manually repositioning them so I tried setting the button grow direction:

for btn in buttons:
    btn.rect_position = Vector2(button_radius, 0).rotated(angle)
    if angle == PI*1.5 or angle == PI*0.5 or angle == PI*-0.5:
        btn.grow_horizontal = Control.GROW_DIRECTION_BOTH
    elif angle >= PI*0.5 and angle < PI*1.5:
        btn.grow_horizontal = Control.GROW_DIRECTION_BEGIN
    else:
        btn.grow_horizontal = Control.GROW_DIRECTION_END
    angle += angle_offset

enter image description here

As you can see, this works... with a caveat. For some reason, the next time you call it the .grow_horizontal property always acts as if it's back to GROW_DIRECTION_END, so you're back to how it was before. I'm not sure if this is a bug with buttons or not... In my code, I was calling queue_free() the buttons and making new ones as children. Yet, the buttons would somehow keep some sort of state between this...

I inspected the buttons of pie menu before and after: the difference was that margin changed. I don't know enough about how margin gets set yet to figure this out! Does anyone have any insight?

Here's what actually does work... on the second time it's called:

for btn in buttons:
    btn.grow_horizontal = Control.GROW_DIRECTION_END
    btn.rect_position = Vector2(button_radius, 0).rotated(angle)
    if angle == PI*1.5 or angle == PI*0.5 or angle == PI*-0.5:
        btn.rect_position.x -= btn.rect_size.x / 2
    elif angle > PI*0.5 and angle < PI*1.5:
        btn.rect_position.x -= btn.rect_size.x
    angle += angle_offset
Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to webmaster@godotengine.org with your username.