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).
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()
_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
# _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
Spinning 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 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
- Wheel continue spin (controlled by
- Wheel end spin (controlled by
_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.
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") ...
_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
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
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
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
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!
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)