Continuing on with the previous post, today we’re going to be taking those wheel sections and putting them together in a scene. We’ll also create functions for spinning the wheel and getting the result it “lands on” (I put it in quotes because in the code it’s actually the other way around, but more on that later).
The scene
Let’s make a new 2D scene and name it “Wheel.” As the first child of the root node, make another 2D node. I’m going to name this one “Sections,” as it will be the container for holding all of the WheelSection scenes we instantiate. That’s enough for us to get started on the scene script, so let’s make one and attach it to the root.
This is another script that I’d like to be able to watch update in the editor when I change its properties, so I’m going to make it a tool
script. Additionally I’m going to define some exported variables so we can make edits to the script in the inspector.
tool
extends Node2D
# This export isn't strictly necessary, since we could use a preload() function with
# the predefined path to the WheelSection scene, but I'll do it this way for example.
export(PackedScene) var wheel_section
export(Array) var wheel_sections
export(float, .1, 2) var stopping_time := 1.0
export(float, .1, 2) var startup_time := 1.0
Hopefully these variables are pretty self-explanatory with what they relate to, so I’ll continue on. Because this is a tool
script, I’d like the code we set here get run in the editor, specifically when editing the wheel_sections
variable (which will be an array of WheelSectionData
). So I’m going to make a setget
for that variable and define a setter function.
export(Array) var wheel_sections setget _set_wheel_sections
func _set_wheel_sections(sections):
wheel_sections = sections
_draw_wheel()
The _draw_wheel()
function is the key function here — that’s the code I want to have run during edit-time, so let’s go over that.
Drawing the wheel
We need to first check that the wheel_section
variable has been assigned. If not, we can’t instantiate the scene and so we’ll return early. We also want to make sure that our current node (i.e. the root) is within the scene tree. If it isn’t, then Godot will give us errors when we try to add child nodes. This part is only needed since this is a tool
script and will be running in the editor.
func _draw_wheel():
if not wheel_section or not is_inside_tree():
return
Next, we want to erase/remove all children of the “Sections” node because we want to be working with a clean slate when we start instantiating.
# _draw_wheel()
for child in $Sections.get_children():
child.queue_free()
Now we will get a reference to the edited_scene_root
(which should be the same as the node we’re currently attached to, but for completeness’s sake we’ll treat it as though it was separate). We need this reference so that once we instantiate the child scene we can set its owner
property to the root. What this does is allow the instantiated scene to appear in the scene tree in the Scene panel to the left of the viewport area. Without setting owner
, the scene would still appear in the viewport, but not in the hierarchy.
We also want to keep track of what rotation we need to start these wheel sections at, so we’ll set a current_offset
variable to start at 0
.
# _draw_wheel()
var root = get_tree().edited_scene_root
var current_offset := 0
Now we can loop through all of the wheel_sections
and instantiate them.
# _draw_wheel()
for section_data in wheel_sections:
# Instantiate the PackedScene
var scene = wheel_section.instance()
# Attach the instantiated scene as a child to the $Sections node
$Sections.add_child(scene)
# Set the owner property of the scene to the edited_scene_root
scene.owner = root
# Assign the wheel_section_data
# Because the WheelSection scene was also a tool script, the wheel_section_data
# setget method will also be run within this tool script.
scene.wheel_section_data = section_data
# Rotate the scene current_offset degrees
scene.rotate(deg2rad(current_offset))
# Increment the current_offset by the size of the wheel_section_data
current_offset += scene.wheel_section_data.percent_of_wheel * 360.0
And that’s it! Our _draw_wheel()
function has been created! Now you should be able to assign an array of WheelSectionData
in the scene’s editor and watch the wheel be drawn as you update it! (Don’t forget to also assign the wheel_section
scene to the scene we created last time)
The final code for the _draw_wheel()
function looks like this.
func _draw_wheel():
if not wheel_section or not is_inside_tree():
return
for child in $Sections.get_children():
child.queue_free()
var root = get_tree().edited_scene_root
var current_offset := 0
for section_data in wheel_sections:
var scene = wheel_section.instance()
$Sections.add_child(scene)
scene.owner = root
scene.wheel_section_data = section_data
scene.rotate(deg2rad(current_offset))
current_offset += scene.wheel_section_data.percent_of_wheel * 360.0

_draw_wheel()
function has been runSpinning the wheel
Now that we have the wheel assembly taken care of, we need to make it functional. There are two main things we want to be able to do with the wheel from other scripts: spin and stop. Additionally we want to receive the WheelSectionData
of whatever segment of the wheel we landed on. So let’s start by adding a few things to our scene and script.
First, add a new Tween
node to the scene (anywhere in the scene works — I just added it as a child of the root). This handy node lets you do tweening animations fairly easily.
Next we want to have a few script variables to help us keep track of where the wheel is in its process of spinning, so let’s define those.
var _wheel_started := false
var _wheel_spinning := false
var _startup_final_rot := 360
var _prev_deg := 0
var _deg_delta
I think the only two variables here that may need some explanation are the _startup_final_rot
and _deg_delta
. The _startup_final_rot
variable is basically just the final rotation of where the wheel should be at the end of the “startup” phase of wheel spinning. The wheel spin animation will be broken up into three phases.
- Wheel start spin (controlled by
Tween
node) - Wheel continue spin (controlled by
_process()
function) - Wheel end spin (controlled by
Tween
node)
The _deg_delta
variable will keep the wheel spinning at the same rate as it was at the end of the first phase.
We also want to define a signal so that any script can listen for when the wheel stops and get the result.
signal wheel_stopped
Now let’s create the spin_wheel()
function. This will be the method we can call from outside scripts to begin the spin process.
func spin_wheel():
if _wheel_started:
# If we've already started spinning the wheel, we don't want to
# re-do the startup animation
return
_wheel_started = true
# Because the rotation of a node isn't bound between 0 and 360, the
# actual_rot variable will hold the node's full rotation, while current_rot
# will hold the rotation between 0 and 360
var actual_rot = $Sections.rotation_degrees
var current_rot = int(actual_rot) % 360 + (actual_rot - int(actual_rot))
# We will set the final rotation to be 1 full rotation past the current rotation
# in the counter-clockwise direction (for a clockwise spin switch - to +)
_startup_final_rot = current_rot - 360
Now that we’ve initialized the variables, we can start the tweening process to startup the wheel.
$Tween.interpolate_property(
$Sections,
"rotation_degrees",
current_rot,
_startup_final_rot,
startup_time,
Tween.TRANS_QUINT,
Tween.EASE_IN
)
$Tween.start()
yield(get_tree().create_timer(startup_time), "timeout")
_wheel_spinning = true
If you now were to run this function, you’d see that the wheel will do one full rotation in the given startup_time
, but will abruptly stop after that time. That is when the _process()
function will take over.
Continuing to spin
func _process(delta):
# These checks make sure that we can spin the wheel. The Engine.editor_hint
# prevents this function running while in the editor (_process() runs every
# frame so that wouldn't be good).
if Engine.editor_hint or not is_inside_tree() or not _wheel_spinning:
return
# Otherwise, every frame rotate the wheel another _deg_delta
$Sections.rotate(deg2rad(_deg_delta))
The observant among you may have realized that we haven’t ever set _deg_delta
, and may be wondering how we do that. First off, way to think through it! Let’s address that now.
Back in the start_wheel()
function when we use the Tween
node to begin the wheel spinning, we can set the delta. Because “delta” just means “the amount of change,” we want to be able to measure the amount of rotational change that happens between two frames. Lucky for us, the Tween
node emits a signal at each frame with some information about the interpolation currently happening called “tween_step”. We will hook into this function and calculate the amount of change that happens between each frame until the tween ends.
# spin_wheel()
...
# Before $Tween.start() is called, make sure to connect the signal to our function
$Tween.connect("tween_step", self, "_set_delta")
...
# After the yield statement of waiting for the timeout, disconnect the function
$Tween.disconnect("tween_step", self, "_set_delta")
...
The _set_delta()
function will take 4 arguments (the number emitted from the “tween_step” signal), but the only one we really care about is the 4th, which is the value at the current frame. All we need to do is set the _deg_delta
to the difference between the current value, and the value at the previous frame. Then we update the “previous” value to be the current value in anticipation of the next _set_delta()
call.
func _set_delta(obj, key, elapsed, current_deg):
# If current_deg is the _startup_final_rot, we are on the last frame of
# the tween, and do not want to update the _deg_delta
if current_deg == _startup_final_rot:
return
_deg_delta = current_deg - _prev_deg
_prev_deg = current_deg
Stopping the wheel
Now that we have the wheel set up to start its spin and continue spinning at the same speed, we need a way to stop the wheel and determine which section it landed on. However, as I eluded to in the intro to this post, we’re actually going to do it an easier way. Instead of worrying about how the final angle lines up and doing math to figure out which section that lies in based on the angle of our “wheel section pointer” and the angle of the wheel (trust me, doing it this way is a nightmare — I’ve done it before on a project I did in the past), we’re going to first randomly select a value between 0 and 1 and find out which section that would fall under. Then we just make the wheel stop on the angle associated with the random value! Let’s see it in practice.
func stop_wheel():
if not _wheel_spinning:
# Like before, if we've already stopped the wheel, we don't want to
# restart the stop animation
return
_wheel_spinning = false
var actual_rot = $Sections.rotation_degrees
var rand_value = rand_range(0, 1.0)
# Get current rotation based off 0 degrees
var ending_rot = actual_rot - int(actual_rot) % 360
# Set end value to within 360 degrees. (Again, if a clockwise spin is desired,
# change the -= to += on this and the next line)
ending_rot -= rand_value * 360
# Spin 2 extra times before stopping
ending_rot -= 360 * 2
$Tween.interpolate_property(
_sections_container,
"rotation_degrees",
actual_rot,
ending_rot,
stopping_time,
Tween.TRANS_SINE,
Tween.EASE_OUT
)
$Tween.start()
yield(get_tree().create_timer(stopping_time), "timeout")
_wheel_started = false
Now that the wheel animation has stopped, we can emit the “wheel_stopped” signal with the WheelSectionData
at the rand_value
.
emit_signal("wheel_stopped", get_section_at(rand_value))
The get_section_at()
function is pretty simple, we just return the first section we come across that has the rand_value
less than the current percent_of_wheel
summed with the previous percent_of_wheel
s.
func get_section_at(percent):
var offset := 0.0
for section in wheel_sections:
if percent <= section.percent_of_wheel + offset:
return section
offset += section.percent_of_wheel
And just like that, the script is complete!
…Well, nearly complete. We would like the rand_range()
to give us less predictable random values, so we need to call the randomize()
function in the script’s _ready()
function.
We also want to make sure that the wheel gets drawn when it first loads up, so we’ll add the _draw_wheel()
function to the _ready()
hook as well.
func _ready():
randomize()
_draw_wheel()
Testing it out!
Now that we’ve got the script fully fleshed out, let’s try loading up this bad boy and seeing if it works!

ColorRect
node at the top of the wheel to indicate which section the wheel needs to land under.There will be one more post in this section before we move on, which is putting these battle wheels into a battle system, and using it when our units attack one another. Until then, au reviour!
The code up to this point in the project can be found here. (Same as previous post)
Leave a Reply