And here we are at the final of the wheel posts. In this one I’ll walk through how I created a scene to hold both wheels and instantiate the scene when a battle happens.
Scene Setup
As with so many of these posts, we’ll start by creating a new scene (I’ve named it “WheelBattle”). We’ll pick a CanvasLayer
for the root node type, since we want this scene to be an overlay. It will be positioned in canvas space rather than in world space which allows it to be appropriately scaled depending on the screen size of the window the game is running in.
Directly under the root node, add a CanvasModulate
node. That node will tint each of its children a specified color, which we can use to tint the alpha channel of all child nodes at the same time. This will come in handy for fading in/out the scene as a transition.
Then there are 4 nodes we’ll add as children to the CanvasModulate
node: one for the background color, one for the screen separator (optional), and one for the attacking/defending wheels respectively. Because I want to have a fun dynamic intro animation whenever the scene pops up, I’m going to give each of these items their own Tween
child. This will allow me to tween their properties independently of one another.
Finally, the last thing I want to add is a representation of which unit is attacking and which unit is defending. The easiest way to do that is to just use their sprite, so I’ll create a spot for those sprites to sit as well.
The final scene tree will look something like this:
Animating
I’ll try not to spend too long on this section since it’s not as important to this post. I won’t be providing the exact code within this post, but I’ll cover the overview of what I imagine will happen.
Firstly, I imagine that there will be a background that fades in on top of the game. It will remain a little transparent so you can still see the game through the overlay.
Then the screen separator will appear. I think a mask reveal from left to right would be fun, so I’ll write a simple shader to allow me to specify the amount of the image that will be visible from 0-1. My separator will go from the top left to the bottom right — the attacking unit & wheel will be on the left side of the separator, with the defending unit & wheel on the right.
Then the two wheels will animate in at the same time from offscreen towards the center. The attack wheel will come from the left side and the defense wheel from the right.
Finally the two unit sprites will fade in and the battle scene will be fully visible.
Here’s an example of what my intro animation looks like:
I encourage you to think up how you’d animate these items! Part of the reason I didn’t provide code for this part was because I’d like you to think about how you’d go about doing this. We’ve learned about animating with Tween
s in the previous post, and shaders in the post before that. This scene will use both!
The exit animation will simply be a fade out, which we can easily do by tweening the CanvasModulate
‘s color
property’s alpha.
Battle Manager
And we’re back again with another manager script! Like the others, we want to be able to access this manager script from anywhere in the code, so I’m going to make it an Autoloaded script (you can refresh yourself on what that is in this post).
There are a few variables we want to keep track of within this script. Firstly, we want a reference to the scene we just created so that we can instantiate it whenever we need to.
var _wheel_battle_scene = preload("res://scenes/WheelBattle.tscn")
We should also probably define some signals so that other processes can listen for the results of the battle, in case some game effect needs to happen.
# I recently discovered you can tell Godot how many arguments to expect from
# a signal. It doesn't do anything, but it is nice for reference.
signal battle_started(attacker, defender)
signal battle_ended(attack_result, defend_result)
We’ll want a reference to the attacking and defending unit, and also the result of the battle.
var _attacker: Unit
var _defender: Unit
var _attack_result: WheelSectionData
var _defend_result: WheelSectionData
And lastly, I don’t know if we will need these or not, but I added them in anyway because I’m bad at following best coding practices and love to over-engineer things — two arrays that will contain hooks (callbacks) we can call before and after the battle. This may be unnecessary since we have the signals defined, but the advantage that these hooks can provide is that we can ensure all of them get run before the battle code continues. Signals can’t ensure that will be the case since they’re asynchronous.
var _before_spin_hooks := []
var _after_spin_hooks := []
Script functionality
We need some way to initialize our variables, so let’s create an initializer function.
func init_battle(attacker: Unit, defender: Unit):
_attacker = attacker
_defender = defender
_attack_result = null
_defend_result = null
emit_signal("battle_started", _attacker, _defender)
You’ll notice that we’re emitting the battle_started
signal in the initializer function. I decided to do it this way so that our actual battle execution function will only have to focus on the battle.
Let’s also create a cleanup function that we can call after the battle results are calculated.
func cleanup_battle():
emit_signal("battle_ended", _attack_result, _defend_result)
_before_spin_hooks.clear()
_after_spin_hooks.clear()
Now let’s define the primary function that we will call. This call should take care of instantiating the battle scene, spinning the wheels, getting the results, and removing the battle scene.
func _battle_flow():
var battle = _wheel_battle_scene.instance()
# Add instantiated scene to current scene
get_tree().current_scene.add_child(battle)
battle.set_attacker(_attacker, _attacker.get_attack_wheel_sections())
battle.set_defender(_defender, _defender.get_defense_wheel_sections())
# Animate the scene in
yield(battle.animate_in(1.25), "completed")
# Wait a moment after animation completes
yield(get_tree().create_timer(.5), "timeout")
# Spin the wheels for X seconds and get the results [atk_result, def_result]
var wheel_results = yield(battle.spin_wheels_for_duration(.75), "completed")
_attack_result = wheel_results[0]
_defend_result = wheel_results[1]
# Wait a moment after spin completes
yield(get_tree().create_timer(.75), "timeout")
# Fade the scene out
yield(battle.fade_out(), "completed")
# Remove battle scene from current scene
get_tree().current_scene.remove_child(battle)
I added some pauses during the flow so that the player(s) will be able to register what’s happening, instead of just running through the steps one by one; starting and finishing the battle before even realizing that something happened. Adding pauses also adds to the suspense of the result (though not too many pauses or else it just feels drawn out). Feel free to adjust these timings to your liking.
You may have noticed that I made this function private. That’s because I want to call those hook functions before and after this function gets run, so I’ll expose a different method that should be called which does all of that and returns the battle results.
func do_battle():
yield(_run_before_spin_hooks(), "completed")
yield(_battle_flow(), "completed")
yield(_run_after_spin_hooks(), "completed")
return [_attack_result, _defend_result]
Calling the hooks
The hook function arrays could be empty, so we can’t just assume the _run_before_spin_hooks()
and _run_after_spin_hooks()
are going to be yield
able unless we account for that case. We’ll add the following code to the start of both of those hook-calling methods.
if <hook_functions_array>.empty():
yield(get_tree(), "idle_frame")
return
Otherwise we can call the functions one by one.
# _run_before_spin_hooks()
for cb in _before_spin_hooks:
yield(Utils.yield_for_result(cb.call_funcv([_attacker, _defender])), "completed")
# _run_after_spin_hooks()
for cb in _after_spin_hooks:
var updated_results = yield(
Utils.yield_for_result(cb.call_funcv([_attack_result, _defend_result])), "completed"
)
# We will allow these hooks to modify the result of the wheel spins
_attack_result = updated_results[0]
_defend_result = updated_results[1]
The Utils.yield_for_result()
is a helper function I created in my Utils
singleton script. All it does is ensure that the function result is a GDScriptFunctionState
object. If not, it yields for one frame before returning, ensuring that any function wrapped within it can be yielded.
# utils.gd
func yield_for_result(result):
if result is GDScriptFunctionState:
result = yield(result, "completed")
else:
yield(get_tree(), "idle_frame")
return result
Finally, one last piece to add to this manager script is the ability to register the hooks.
func register_before_spin_callback(cb: FuncRef):
_before_spin_hooks.append(cb)
func register_after_spin_callback(cb: FuncRef):
_after_spin_hooks.append(cb)
Using the Battle Manager
Now that we’ve completed all of the work of building the battle system, all that remains to do is plug it into the existing code! Fortunately this is a very simple matter, since our attack action code has already been set up previously. All we need to do is replace the logic of how the damage is calculated with the following:
# unit_actions.gd -> do_attack_action()
BattleManager.init_battle(unit, target_unit)
var results = yield(BattleManager.do_battle(), "completed")
BattleManager.cleanup_battle()
if results[0].miss:
print("Attack missed!")
else:
var dmg = results[0].attack_points - results[1].defense_points
if dmg > 0:
dmg = target_unit.take_damage(dmg)
print(target_unit.name + " took " + str(dmg) + " damage from " + unit.name)
else:
print(target_unit.name + " took no damage")
Now let’s see this all in action!
I apologize if this post seemed somewhat rushed, I’ve been eager to get on with finishing up the project and this whole Project Delve series. There are other things I’d like to write about in the future too!
As with the previous two posts, the code for this section can be found here.