Home

  • Project Delve Part 9: Objectives

    Project Delve Part 9: Objectives

    What’s a game without an objective? Probably something, for sure, but my poorly-conceived question has a point (in my head)! Without any sort of goal, a game will lose its draw over time. Even if they’re self-given goals in a sandbox game, you as the player are always trying to accomplish something.

    With that philosophical exercise out of the way, let’s get on with the blog post.


    Quests

    In Descent, each encounter has an objective for both the heroes and the overlord to accomplish. Whichever side accomplishes their goal first is the winner. Pretty standard stuff. We will need a way of defining these goals and then tracking the relevant game actions that will lead to accomplishing that goal in order to determine who wins the game (and when the game should end, for that matter).

    For this post I’m going to keep things pretty simple and not try and overcomplicate quests, like I am wont to do with programming things. There are many actions that could define quest goals but we’ll just focus on a simple one today: kill count. How many monsters/heroes are you required to slay before you win? So let’s bust out a new script.

    I’m going to name this script “quest.gd” and have it extend Resource so that we can create and save them within the editor and potentially use them for more than just one scenario.

    # quest.gd
    extends Resource
    
    # Define a signal that we can emit when the quest has been completed
    signal completed
    
    class_name Quest
    
    # Only define one quest type for now
    enum QuestType { kill }
    
    # Define the quest type
    export(QuestType) var type := QuestType.kill
    
    # How many of the quest type does the player need to do?
    export(int) var amount := 0
    
    # Which groups will the targets be a part of?
    export(Array) var objective_groups := []
    
    # Keep track of how many [kills] the player has made so far
    var _current_count := 0
    Now that we’ve created the Resource script, it should show up in the Resource Creation Dialog.

    So we’ve defined the outward-facing variables of the Quest script, but as it stands they’re just numbers. They don’t do anything in and of themselves — that’s where scripting comes in.

    For our kill quest type, we want to be notified whenever a unit dies (we’re not going to worry about how it died or who killed it). When that happens we need to check if the unit is within one of the groups defined by objective_groups, and if so, to increase the _current_count and check for quest completion.

    func _on_unit_killed(unit: Unit):
    	print("unit was killed: " + unit.name)
    	if type != QuestType.kill:
    		return
    
    	var valid := false
    	for group in objective_groups:
    		if unit.is_in_group(group):
    			valid = true
    			break
    
    	if valid:
    		_update_count(1)
    
    
    func _update_count(increase_by: int):
    	_current_count += increase_by
    	_check_objective_fulfilled()
    
    
    func _check_objective_fulfilled():
    	if _current_count >= amount:
    		emit_signal("completed", self)

    And badda-bing badda-boom, there we have it! A simple logic to update the Quest resource and check for completion.

    Hooking it up

    Of course, we still aren’t using the script anywhere so even if we create a Quest resource and define the fields, the game behavior isn’t going to change. Let’s fix that!

    There are a couple of ways we could go about this, but in keeping with the simple-code strategy let’s just make a middle man script that can act as a sort of event emitter hub. We also need to define a generic Game Manager script to keep track of various game states, so I think it makes sense to combine those two for now.

    # game_manager.gd
    extends Node
    
    var _hero_quest: Quest
    var _overlord_quest: Quest
    
    func set_hero_quest(quest: Quest):
    	_hero_quest = quest
    	Utils.connect_signal(_hero_quest, "completed", self, "_end_game", [true], CONNECT_ONESHOT)
    
    
    func set_overlord_quest(quest: Quest):
    	_overlord_quest = quest
    	Utils.connect_signal(_overlord_quest, "completed", self, "_end_game", [false], CONNECT_ONESHOT)
    
    
    func _end_game(quest, was_hero_quest: bool):
    	var who = "hero" if was_hero_quest else "overlord"
    	print("Game over, " + who + " player wins!")

    We will want this script to be available for all other scripts so we’re going to turn to the tried-and-true AutoLoad feature again! Making this script an AutoLoaded script allows us to access its members and functions within any other script.

    So we’ve completed half of this script’s purpose (holding game state), now let’s add some signals to act as a central event relay-er.

    The reason we need an event relay is because when scripts want to listen for a certain signal, but that signal is only emitted by dynamic scripts that can be created and destroyed at any time, the would-be listener is unable to determine (easily) when to start and when to stop listening to script signals. By using a middle man event relay we add a level of abstraction to the process, allowing both the emitter and the listener to only care about the single static instance that will always be available.

    It might make a little more sense in practice, so let’s do that. First let’s define a signal type in “game_manager.gd” that we know our quest wants to listen for.

    signal unit_died(unit)

    Then we can hook up both sides in their respective scripts.

    # unit.gd
    
    func _update_hp(newValue):
    	# ... existing code ...
    	if hp <= 0:
    		GameManager.emit_signal("unit_died", self)
    # quest.gd
    
    func begin_tracking():
    	_current_count = 0
    	Utils.connect_signal(GameManager, "unit_died", self, "_on_unit_killed", [])

    And just like that, whenever a unit’s HP drops to 0 or below, any active quests will be notified and will run their _on_unit_killed() function. Lastly we need to make sure that the quest’s begin_tracking() function gets called, so we can do that in the GameManager when setting the quests.

    func set_hero_quest(quest: Quest):
    	_hero_quest = quest
    	Utils.connect_signal(_hero_quest, "completed", self, "_end_game", [true], CONNECT_ONESHOT)
    
    	# ADD THIS LINE!
    	_hero_quest.begin_tracking()
    
    
    func set_overlord_quest(quest: Quest):
    	_overlord_quest = quest
    	Utils.connect_signal(_overlord_quest, "completed", self, "_end_game", [false], CONNECT_ONESHOT)
    
    	# ADD THIS LINE!
    	_overlord_quest.begin_tracking()

    Game script

    One last thing needs to happen before everything is set up and ready to play, and that is calling the set_hero_quest() and set_overlord_quest() functions! Where will those be called?

    Up until now my main demo scene has been using a script called “demo_scene.gd.” I’m going to replace that with a new script I’ll just call “game.gd” since I anticipate this one to be more permanent. In that script I’ll create two exported variables for the hero quest and overlord quest respectively. Then in the _ready() lifecycle hook I’ll call the GameManager functions to set the quests. To get the game loop started, I’ll kick it off by calling the PhaseManager’s start_hero_turn().

    extends Node
    
    export(Resource) var hero_quest
    export(Resource) var overlord_quest
    
    onready var _phase_manager = $PhaseManager
    
    func _ready():
    	GameManager.set_hero_quest(hero_quest)
    	GameManager.set_overlord_quest(overlord_quest)
    	_phase_manager.call_deferred("start_hero_turn")
    

    This has been a fairly simple example showing how one might go about defining and tracking quest objectives, feel free to expand on the idea! Some things to think about:

    • Maybe the heroes & overlord can have more than 1 quest to accomplish each; how would you allow for that?
      • What if the quests should be sequential (one has to be completed before another)?
    • How would you modify the hero quest to kill a full monster group instead of a single monster?
    • Maybe there are optional side quests for each encounter; how would you acquire and accomplish those quests, and what reward would you get?
    • Besides kill quests, what other quest types could you add?

    The code for this post can be found here.

  • Project Delve Part 8.2: Wheel Battle

    Project Delve Part 8.2: Wheel Battle

    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 Tweens 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 yieldable 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.

  • Project Delve Part 8.1: Assembling the Wheel

    Project Delve Part 8.1: Assembling the Wheel

    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
    finalized wheel drawn
    How your wheel might look when the _draw_wheel() function has been run

    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 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.

    1. Wheel start spin (controlled by Tween node)
    2. Wheel continue spin (controlled by _process() function)
    3. 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_wheels.

    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!

    demonstration of wheel spinning and stopping
    I also added a small 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)

  • Project Delve Part 8: Battle Scene – Wheel Section

    Project Delve Part 8: Battle Scene – Wheel Section

    Hiya folks, I’m back.

    As I mentioned in my previous post, I was starting to feel drained with the work I was putting into this project and was starting to lose motivation. Turns out that’s what we call “burnout” in the biz! But after a few months hiatus I’m feeling rejuvenated and ready to finish this bad boy.


    Where was I?

    Naturally after opening back up the project for the first time in months I had forgotten what I was working on exactly. Fortunately I keep a Trello board and a markdown file with my to-do list items and thoughts I have had along the way, so that was the first thing I checked. I also revisited my previous post and the initial Project Vision post to see where I was at and remembered that I was in the process of adding skills to the game.

    After reading through my last post I feel reasonably comfortable with how I described the process of creating a new skill (at least as far as “action” skills go — I’ll be writing a post about “interrupt” and “passive” skills later), so today we’ll be looking at putting together a more engaging battle scene!


    Making a Randomizer

    Because Descent is a board game, it uses the simplest randomizer for outcomes that can have a defined number of results: dice. But since this game we’re making uses code we don’t need any physical apparatus, we can have the computer provide a random result for us! So because we’re not tied to “dice,” I don’t want to use any sort of dice in our game (for no reason other than I don’t want to). Thus we need to come up with a different way to visually achieve random results.

    One idea I’m fairly fond of is a wheel — think of the big “clicky” wheels at carnivals that eventually land on a certain prize spot. I think it’d be fun to have a spinning wheel as the attack/defense randomizer! Plus it also comes with a few added benefits:

    1. We’re not limited to exactly 6 outcomes. There can be fewer or there can be more!
    2. We’re not limited to a 1/6 chance for every outcome. If you use dice you’re stuck with that. Unless the dice are loaded. In which case… I hope you’re only playing with friends with whom you don’t care about burning bridges.

    My first thought was to create a “wheel segment” sprite that could be stitched together with other segments to create a full wheel. This had the drawback of not being able to resize the sections at will if we needed to do some balancing later on, or if we want to allow the players to customize their wheels and allow them to select the size of each section.

    My second thought was to create a single wheel and use some sort of image mask to cover up the leftover part of the wheel (the inverse of the section size, thinking in terms of degrees). While researching this I came across people discussing how you could use a texture as a mask for another texture through some fancy sorcery called shaders. I’ve heard of shaders before and in my mind they’ve always been a ominous entity. When I think of shaders I think of incredible visual effects with gorgeous lighting and high-detailed scenery.

    An example of a shader built for Minecraft

    As it turns out though, a shader is simply a snippet of code that the GPU runs for every pixel on the screen. After reading the Godot documentation on shaders and watching a few YouTube videos (this one was especially helpful), I felt ready to create my own.

    Creating the scene

    I started out by taking a simple PNG of a circle and adding a few effects to it in Photoshop (though the process will work fine with just a regular circle). I also added a pure Green rgb(0, 255, 0) border around the circle so that it could be selected and colored separately from the rest of the texture.

    I then created a new scene which I named WheelSection with a root node of Sprite, and I assigned my circle texture to the Sprite node.

    Next I created a simple Control node as the single child of the root and named it “Pivot,” since it would act as the anchor around which the icons would rotate. Everything else in the scene will be a descendant of the Pivot node so that their transforms will inherit the rotation of Pivot. I set the initial rotation of Pivot to be 180 degrees and will update it with a script depending on the percent of the wheel that should be visible.

    I next created a BoxContainer node as a child of Pivot that I named “Icons,” which is where we will dynamically add our icons to represent the value of the wheel section.

    Finally I created a script to attach to the root which I named wheel_section.gd. This script will take care of dynamically updating our wheel section based on the data provided.

    # wheel_section.gd
    class_name WheelSection
    
    onready var _pivot = $Pivot
    onready var _icon_containers = $Pivot/Icons
    
    export(Resource) var wheel_section_data setget _set_data
    
    export(Texture) var sword_icon
    export(Texture) var shield_icon
    export(Texture) var lightning_icon
    export(Texture) var heart_icon
    export(Texture) var miss_icon
    
    # This function ensures that when the wheel_section_data gets changed (described in the next section)
    # we automatically update the scene values.
    func _set_data(section_data):
    	if wheel_section_data:
    		wheel_section_data.disconnect("changed", self, "_update_wheel")
    	wheel_section_data = section_data
    	if wheel_section_data:
    		wheel_section_data.connect("changed", self, "_update_wheel")
    		if is_inside_tree():
    			_update_wheel()
    
    # We will break up the update into 3 steps, as shown here
    func _update_wheel():
    	if not wheel_section_data or not is_inside_tree():
    		return
    
    	_update_icons()
    	_update_positioning()
    	_update_visibility()

    Icons

    Updating the icons will be the most complex part, relatively speaking, so let’s start there.

    As an explanation for what we want to do, when we read the wheel_section_data (explained in the next section) there will be different values for different types (attack, defense, special, etc.). I’d like to display each of these icons in a grid pattern, grouped with similar icons. Thus, the top-level _update_icons() function will look like this.

    func _update_icons():
    	if not _icon_containers:
    		_icon_containers = $Pivot/Icons
    
    	# For simplicity's sake, we will delete any existing icon groups and recreate them
    	for child in _icon_containers.get_children():
    		child.queue_free()
    
    	if wheel_section_data.miss:
    		_add_icon_grid(1, miss_icon, "Miss")
    	else:
    		_add_icon_grid(wheel_section_data.heal_points, heart_icon, "Heal")
    		_add_icon_grid(wheel_section_data.attack_points, sword_icon, "Attack")
    		_add_icon_grid(wheel_section_data.special_points, lightning_icon, "Special")
    		_add_icon_grid(wheel_section_data.defense_points, shield_icon, "Defense")

    For the _add_icon_grid() function, we will be creating a new GridContainer node and adjusting the amount of columns it has based on the value that gets passed in.

    func _add_icon_grid(amount, texture, name):
    	if amount <= 0:
    		# We skip creating any element if there are no icons to add
    		return
    
    	var max_columns = 4
    	var icon_grid := GridContainer.new()
    	_icon_containers.add_child(icon_grid)
    	icon_grid.name = name
    	icon_grid.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
    
    	if amount < max_columns:
    		icon_grid.columns = amount
    	else:
    		if amount / 2 < max_columns:
    			icon_grid.columns = amount / 2
    		else:
    			icon_grid.columns = max_columns
    
    	# This ensures we will see the updated scene elements in the editor's scene inspector
    	icon_grid.owner = get_tree().edited_scene_root
    	_add_icons(icon_grid, amount, texture, name)

    Finally, the _add_icons() function takes care of creating the TextureRects and setting their properties to the provided values.

    func _add_icons(container, amount, texture, node_prefix = "TexRect"):
    	for i in range(0, amount):
    		var tex_rect := TextureRect.new()
    		tex_rect.texture = texture
    		tex_rect.name = node_prefix + "_" + str(i + 1)
    		tex_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
    		container.add_child(tex_rect)
    		
    		# Like before, this updates the editor's scene inspector
    		tex_rect.owner = get_tree().edited_scene_root

    Positioning

    Next, in order to position the icons in an appropriate spot, we will update the rotation of the Pivot node.

    func _update_positioning():
    	if not _pivot:
    		_pivot = $Pivot
    
    	var default_rotation = PI  # 180 degrees
    	_pivot.set_rotation(default_rotation * wheel_section_data.percent_of_wheel)

    Visibility

    Lastly, we will set a shader parameter that will set how much of the circle image is visible.

    func _update_visibility():
    	material.set_shader_param("visible_percent", wheel_section_data.percent_of_wheel)

    Creating the data

    Because we want to be able to have each instance of this scene have different properties, and because we want these properties to be defined purely as data, we will be making a new Resource script. I’ll call it wheel_section_data.gd.

    # wheel_section_data.gd
    extends Resource
    
    class_name WheelSectionData
    
    export(float, 0, 1) var percent_of_wheel := 1.0
    export(int) var attack_points := 0
    export(int) var defense_points := 0
    export(int) var special_points := 0
    export(int) var heal_points := 0
    export(int, 0, 6) var range_points := 0
    export(bool) var miss := false

    This script will work just fine for holding the data we care about, but to make it a little easier for development, let’s make this a tool script (meaning it will run in the Godot editor) and add some setget functions to notify when a value of the resource changes.

    tool
    extends Resource
    
    class_name WheelSectionData
    
    export(float, 0, 1) var percent_of_wheel := 1.0 setget _set_percent
    export(int) var attack_points := 0 setget _set_atk
    export(int) var defense_points := 0 setget _set_def
    export(int) var special_points := 0 setget _set_spec
    export(int) var heal_points := 0 setget _set_heal
    export(int, 0, 6) var range_points := 0 setget _set_range
    export(bool) var miss := false setget _set_miss
    
    
    func _set_percent(val):
    	percent_of_wheel = val
    	emit_changed()
    
    
    func _set_atk(val):
    	attack_points = val
    	emit_changed()
    
    
    func _set_def(val):
    	defense_points = val
    	emit_changed()
    
    
    func _set_spec(val):
    	special_points = val
    	emit_changed()
    
    
    func _set_heal(val):
    	heal_points = val
    	emit_changed()
    
    
    func _set_range(val):
    	range_points = val
    	emit_changed()
    
    
    func _set_miss(val):
    	miss = val
    	emit_changed()
    

    Now when a value on the resource is changed, the wheel_section.gd script will register that it needs to call its _update_wheel() function because of the signal we connected to it! Once you provide sprites for each of the wheel icons you can test this out for yourself (though you’ll probably get an error about the material not having a shader parameter “visible_percent”). So let’s fix that now.

    demonstrating icons dynamically being added

    Creating the shader

    Start by clicking the root of the scene (which should be the Sprite node) and finding the “Material” dropdown under the “CanvasItem” section in the Inspector. Expand that section, click on the value dropdown, and select “New ShaderMaterial.” Then there will be a new field that shows up directly below “Material” called “Shader.” Open up its value dropdown and select “New Shader.” This will open up a shader script editor section in Godot where you can code the shader.

    As described in the docs and video I linked to earlier, each shader script must begin with the shader_type keyword with a certain type following it. Because our ShaderMaterial is part of a CanvasItem node, the shader_type will be canvas_item.

    shader_type canvas_item;

    As for the things we want to be able to control outside of the shader (like our export variables in the gdscripts) we need to define some uniform variables.

    const float PI = 3.1415926535;
    
    uniform vec4 wheel_color : hint_color = vec4(.33, .61, .97, 1);
    uniform vec4 border_color : hint_color = vec4(1, 1, 1, 1);
    uniform float visible_percent : hint_range(0, 1) = 1.0;

    Because we want to modify the texture’s color value, we need to define the fragment() function. Within this function we’re provided with a few “global” variables: TEXTURE, UV, and COLOR.

    • TEXTURE refers to the full texture attached to the material (i.e. our circle sprite)
    • UV refers to the coordinates of the current pixel the fragment() function is operating on
    • COLOR refers to the color value of the current pixel the fragment() function is operating on

    Modifying COLOR is how we will update the color value of the pixel. The texture() function is a global function that takes a texture and coordinates and returns the color at that coordinate. The following fragment function will assign the color of the current pixel to the color of the texture at the current pixel’s coordinates. (In other words, does nothing to modify the result)

    void fragment() {
    	COLOR = texture(TEXTURE, UV);
    }

    Try experimenting with modifying COLOR in this function and watch how the sprite changes in the editor! For example, this would set the blue channel of the pixel to be 1 (scale is 0-1) for each pixel on the texture.

    COLOR.b = 1;

    With this knowledge alone we can do some pretty fun stuff, but let’s stick to just hiding the pixels we don’t want to see so we end up with a slice of the circle. For that we will simply set COLOR.a (the alpha channel) to 0 on any pixel we don’t want shown, so it will be fully transparent.

    …but how can we tell which pixels we need to do that for versus the ones that should be shown? Let’s create a function to call that will tell us whether we should draw the pixel or not.

    bool should_draw(vec2 uv) {
    	if (visible_percent == 1.0) return true;
    	if (visible_percent == 0.0) return false;
    }
    
    void fragment() {
    	if (!should_draw(UV)) {
    		COLOR.a = 0.0;
    		return;
    	}
    	
    	COLOR = texture(TEXTURE, UV);
    }

    These two use cases are easy to determine — if 100% of the wheel is visible, we can always return true. Likewise, if 0% of the wheel is visible we can always return false. But what about in the other instances? For this we’ll need to do a bit of trigonometry, since we want to hide the circle radially instead of linearly.

    First let’s determine which angles define the “start” and “end” of the area we want to be shown. (We won’t actually use the start angle in the calculations since it’ll be 0, but I’m including it here for… reasons.)

    float start_angle = 0.0;
    float end_angle = visible_percent * PI;

    Easy enough.

    Now let’s ensure that anything outside of the radius of 1 will be hidden (this ensures the resulting sprite will always be cropped to a circular shape).

    float radius = 1.0;
    vec2 local_uv = uv - vec2(0.5, 0.5);
    if (distance(vec2(0.0, 0.0), local_uv) > radius) {
    	return false;
    }

    We’re subtracting the vec2(0.5, 0.5) from the provided uv coordinates since we want to check from the center of the texture instead of the top left (which is the default).

    Finally let’s whip out those dusty old trig textbooks and find the chapter(s) on calculating the angle of a point.

    What we want to discover is whether a point within a circle lies between two different angles. We’ve already tested that it is within the radius of the circle, so now we just need to calculate the angle of the point and determine whether that’s greater than our start_angle and less than our end_angle. If it is, we can draw it!

    To find the angle of our point we will use the atan() function, which takes an x and y coordinate and returns the angle of the point.

    float point_ang = atan(local_uv.x, local_uv.y);

    We’re using the result local_uv since we want the origin at the center.

    float cutoff_ang = atan(cos(end_angle), sin(end_angle)) * 2.0;

    To determine the angle of the cutoff point, we need to take the arctangent of the x and y value of our end_angle, and then multiply that by 2, since… that’s what works. (I spent a long time experimenting with different ways to get the correct angle, and even longer trying to understand why that worked, but I ultimately don’t know and if anyone can explain to me why this works I’d love to hear it!)

    code doesn't work meme

    Then we can round off the function by comparing the point_ang to the cutoff_ang.

    return point_ang >= cutoff_ang;

    All together the shader script looks like this.

    shader_type canvas_item;
    
    const float PI = 3.1415926535;
    uniform vec4 wheel_color : hint_color = vec4(.33, .61, .97, 1);
    uniform vec4 border_color : hint_color = vec4(1, 1, 1, 1);
    uniform float visible_percent : hint_range(0, 1) = 1.0;
    
    bool is_border(vec4 color) {
    	if (color.r != 0.0 || color.b != 0.0) return false;
    	return color.g >= 0.98 && color.a == 1.0;
    }
    
    bool should_draw(vec2 uv) {
    	if (visible_percent == 1.0) return true;
    	if (visible_percent == 0.0) return false;
    	
    	float start_angle = 0.0;
    	float end_angle = visible_percent * PI;
    	
    	// center of circle at (0.5, 0.5)
    	vec2 local_uv = uv - vec2(0.5, 0.5);
    	if (distance(vec2(0.0, 0.0), local_uv) > 1.0) {
    		return false;
    	}
    	
    	float cutoff_ang = atan(cos(end_angle), sin(end_angle)) * 2.0;
    	float point_ang = atan(local_uv.x, local_uv.y);
    	
    	return point_ang >= cutoff_ang;
    }
    
    void fragment() {
    	if (!should_draw(UV)) {
    		COLOR.a = 0.0;
    		return;
    	}
    	
    	COLOR = texture(TEXTURE, UV);
    	if (is_border(COLOR)) {
    		COLOR = border_color;
    	} else if (COLOR.a == 1.0) {
    		COLOR *= vec4(wheel_color.r, wheel_color.g, wheel_color.b, 1.0);
    	} else {
    		COLOR = vec4(border_color.r, border_color.g, border_color.b, COLOR.a);
    	}
    }
    visualizing the wheel shader

    Wheel Section

    So there we have it! A working shader for our wheel section object! This post was a lot longer than I originally anticipated, so the next post will be how we can fit this into a full wheel and from there, make a full Battle Scene. It may be one more post, it may be two. We’ll just have to see!

    full demo

    The code up to this point in the project can be found here.

  • Project Delve Part 7: Skills

    Project Delve Part 7: Skills

    Sorry it’s been a minute, I’ve been pretty busy the last few weeks and haven’t spent much time on the project. Also to be fully transparent, I’ve started to lose that initial drive I had for the project. That’s not to say I don’t want to complete it — I do! But I want to be completely honest about my process here so I thought that it’d be a good idea to point that out. This is also the primary reason I started this blog, so that when I start to get into a productivity slump or lose motivation I have a sort of “external incentive” to persevere. So with that out of the way, let’s dive into what we’re going to cover today: Skills!


    The Process

    So, I’m a pretty big fan of Object Oriented Programming and inheritance. Thus my initial plan to create skills was to create some sort of super class that would act as an interface for individual skills to inherit from.

    However, after pondering how to best structure the super class for nearly a week (without actually coding or diagraming anything) I realized the best strategy for architecting the skills classes would be from the bottom-up instead of from the top-down. That is, code a skill or two and determine from the concrete code the parts that could be abstracted, thus helping me to template future skill implementations.

    The skill

    Oath of Honor skill card

    I picked a basic skill from the Knight class called Oath of Honor. It’s one of the skills a knight character will start with so I thought it made as much sense as anything to begin there. After reading the skill description again to refresh my memory I determined that there are two parts to this particular skill:

    1. Determining if the skill can be used
    2. Using the skill

    I think that this can probably be extrapolated to most other active skills (as opposed to passive skills, which I’ll cover later), so already we’ve found something that can be abstracted! Let’s create the super class now with those two methods and just plan on making more modifications as we go along.

    # skill.gd
    extends Node
    
    class_name Skill
    
    func can_use() -> bool:
        pass
    
    func use():
        pass

    Now that we have a rough template to work from, let’s create the Oath of Honor skill script.

    # sk_oath_of_honor.gd
    extends Skill
    
    func can_use() -> bool:
        pass
    
    func use():
        pass

    Defining can_use()

    Nearly all skills will have a stamina cost associated with them, and if the unit does not have the requisite amount of stamina they cannot use the skill. So let’s define a basic function to check for that.

    # skill.gd
    func _has_enough_stamina():
        return hero.stamina >= stamina_cost

    …Oh, but wait. We don’t have a reference to the hero that’s using the skill. Let’s plop that on the Skill class real quick. We can worry about assigning it later.

    var hero: Unit

    We also need a reference to how much the skill will cost, so let’s add that too.

    var stamina_cost: int

    Now in the sk_oath_of_honor.gd script let’s call that function inside the can_use() function.

    # sk_oath_of_honor.gd
    func can_use() -> bool:
        if not _has_enough_stamina():
            return false
        # Do additional checks here...
        return true

    The additional checks

    Reading the skill description for Oath of Honor, the prerequisites for using this skill (besides its mana cost) are:

    1. An ally hero is within 3 spaces of the current hero
    2. That ally has a monster adjacent to them
    3. There is an empty space adjacent to that monster

    In order to find these things we’re going to need an easy way to query the dungeon grid, which brings up the next problem to figure out. I can foresee there being many uses for querying unit positions and dungeon proximities, so I think we should make a new singleton script that has the sole purpose of taking these kind of queries.


    DungeonManager

    Because I’ve been fond of making so many managers lately, what’s one more? Let’s create a new script called dungeon_manager.gd and add it to the list of AutoLoaded scripts so that it’s globally available. This script will need a reference to the current dungeon object, so let’s add a variable to hold that and a function to set it.

    # dungeon_manager.gd
    extends Node
    
    # Not setting the type of this because it would
    # create a circular dependency... strike 4,001 against GDScript
    var _active_dungeon
    
    func set_active_dungeon(dungeon):
        _active_dungeon = dungeon

    We’ll be adding functions to this class as we find need for them. For the Oath of Honor skill we need to know those three prerequisites I outlined earlier, so let’s make functions that can help us find what we need. I want to try and keep the script generalized so that it can be used for more things than just unit position querying, so I’m going to name the functions to reflect that.

    # Returns the nodes within groups specified by node_groups array
    # that are within tile_radius number of tiles of the world_point
    func get_nodes_within_grid_radius(world_point: Vector2, tile_radius: int, node_groups: PoolStringArray):
        pass
    
    # Will get a list of all potentially empty spaces adjacent to the
    # tile and then remove any space occupied by a node in obstacle_groups
    func get_empty_grid_spaces_adjacent_to(world_point: Vector2, obstacle_groups: PoolStringArray = []):
        pass
    
    # Just a proxy function for the dungeon's method
    func get_grid_coordinates_of_node(node: Node):
        return _active_dungeon.get_grid_position(node.position)
    
    # Yes, these functions need to be renamed. It's confusing
    # to have to remember which function to use on what object
    # especially when GDScript has such poor intellisense support.
    func grid_to_world_position(grid_coordinate: Vector2, center_of_tile := false):
        return _active_dungeon.map_to_world_point(grid_coordinate, center_of_tile)

    Before filling out the implementation of these functions, let’s return to the Oath of Honor script and use these methods as though they work flawlessly (obvi).


    Implementing the Rest of the Skill

    We’re going to need a list of valid tiles that the hero can move to when they use this skill. We’re also going to need to calculate those same spaces for determining whether the skill can be used, so it makes sense to only run the calculations for finding those spaces once and store the result. For that let’s make a function to do those calculations.

    # sk_oath_of_honor.gd
    var _valid_grid_spaces := []
    
    func _get_valid_grid_spaces():
        # Get a list of all heroes in range of the skill
        var allies_in_range = DungeonManager.get_nodes_within_grid_radius(hero.position, 3, ["heroes"])
        for ally in allies_in_range:
            # Get a list of the monsters next to the hero in range
            var monsters_next_to_ally = DungeonManager.get_nodes_within_grid_radius(ally.position, 1, ["monsters"])
            if monsters_next_to_ally.size() > 0:
                # Get a list of empty spaces next to ally
                var ally_spaces = DungeonManager.get_empty_grid_spaces_adjacent_to(ally.position, ["units"])
    
                # Get a list of empty spaces next to monster
                var monster_spaces := []
                for monster in monsters_next_to_ally:
                    monster_spaces.append_array(
                        DungeonManager.get_empty_grid_spaces_adjacent_to(monster.position, ["units"])
                    )
    
                # Take intersection of the two lists
                var intersection := []
                for grid_space in ally_spaces:
                    if monster_spaces.has(grid_space):
                        intersection.append(grid_space)
    
                # Add result of the intersection to _valid_grid_spaces
                _valid_grid_spaces.append_array(intersection)

    Now in the can_use() function we just have to run this function and return whether the size of the _valid_grid_spaces array is greater than 0, indicating that there’s a valid space to move to.

    func can_use() -> bool:
        if not _has_enough_stamina():
            return false
        return _ensure_valid_grid_spaces()
    
    func _ensure_valid_grid_spaces():
         if _valid_grid_spaces.size() > 0:
              return true
    
         _get_valid_grid_spaces()
         return _valid_grid_spaces.size() > 0

    Defining use()

    Now the second part comes for creating the skill: using it! We still don’t have all systems properly in place to fully finish the skill, but we have enough at this point where we can throw together something that works.

    func use():
        # Calling this function mainly ensures that the
        # _valid_grid_spaces array has been populated
        if not _ensure_valid_grid_spaces()
            return
    	
        # TODO: Select one of the valid spaces
        # Just use first in list for now
        var selected_grid_space = _valid_grid_spaces[0]
    
        # Subtract stamina
        hero.stamina -= stamina_cost
    
        # Move unit to selected space
        hero.position = DungeonManager.grid_to_world_position(selected_grid_space, true)
    
        # Attack monster adjacent to ally
        # TODO: perform attack...
        print("Attack...")
    
        return

    Finishing DungeonManager functionality

    Alright, like I promised let’s go back to the DungeonManager and fill out the function stubs we added. The logic for getting nodes within a tile radius is fairly straightforward. First we’ll get the list of nodes in the scene we care about and then check whether the position of the node is within the given radius.

    func get_nodes_within_grid_radius(world_point: Vector2, tile_radius: int, node_groups: PoolStringArray):
        var within_radius := []
        for node_group in node_groups:
            var nodes = get_tree().get_nodes_in_group(node_group)
            for node in nodes:
                if (
                    not within_radius.has(node)
                    and _active_dungeon.tile_distance_to(world_point, node.position) <= tile_radius
                ):
                    within_radius.append(node)
        return within_radius

    The steps for getting the empty grid spaces adjacent to a grid tile are a little more complex. First we’ll add a new function to the Dungeon class called walkable_tile_exists_at(), which will simply check for whether a tile exists at the given point and no grid obstacle exists at that point.

    # dungeon.gd
    func walkable_tile_exists_at(world_point: Vector2):
        var grid_pos = get_grid_position(world_point)
        return floors.get_cellv(grid_pos) != TileMap.INVALID_CELL and obstacles.get_cellv(grid_pos) == TileMap.INVALID_CELL

    Now we just need to run this check for each adjacent tile space and add the results that return true to an array.

    func get_empty_grid_spaces_adjacent_to(world_point: Vector2, obstacle_groups: PoolStringArray = []):
        var empty_spaces := []
        # The _get_neighbors() function simply returns the 8 points that surround
        # the world_point, adding the cell_size so we can get the grid coordinate
        # at the neighbor point
        for neighbor_point in _get_neighbors(world_point, _active_dungeon.floors.cell_size):
            if _active_dungeon.walkable_tile_exists_at(neighbor_point):
                empty_spaces.append(_active_dungeon.get_grid_position(neighbor_point))

    The next thing we need to do in the function is remove spaces where an obstacle exists (like units or interactables).

        for group in obstacle_groups:
            var obstacles = get_grid_coordinates_of_group(group)
            for obstacle in obstacles.keys():
                empty_tiles.erase(obstacle)
    
        return empty_tiles
    
    func get_grid_coordinates_of_group(group: String) -> Dictionary:
        var coordinates := {}
        var nodes = get_tree().get_nodes_in_group(group)
    
        for node in nodes:
            coordinates[_active_dungeon.get_grid_position(node.position)] = node
    
        return coordinates

    Testing it out

    Now the fun part! Will it work? Let’s find out!

    Initial Oath of Honor test gif
    Note that I had to jerry-rig a few things together before testing it out here, such as adding the skill to the list of available skills for the knight. Also, you can’t tell very well due to my bad unit HUD implementation, but the stamina bar for the knight goes down by 1.

    SelectionManager Update

    The skill card says that the Knight is supposed to move to the “closest available empty space” next to the monster, but I would prefer if the player got to choose which space to jump to. So that’s what I’ll do! I love making the rules!

    Let’s add a new function in the SelectionManager to listen for a grid tile to be clicked.

    # selection_manager.gd
    
    # Create a new signal to emit when grid tile is clicked
    signal grid_tile_selected
    
    func select_grid_tile(valid_grid_coordinates = null):
        Utils.connect_signal(_active_dungeon, "grid_tile_clicked", self, "_clicked_tile", [valid_grid_coordinates])
    
    # This function needs the first three arguments because that's what
    # the grid_tile_clicked signal emits with in the dungeon script, even
    # though we don't need all of them here.
    func _clicked_tile(event, loc, pathfinder, valid_grid_coordinates):
        var grid_coordinate = _active_dungeon.get_grid_position(loc)
        if valid_grid_coordinates and valid_grid_coordinates.size() > 0 and not valid_grid_coordinates.has(grid_coordinate):
            # If the selected tile was not valid, we simply return and
            # wait for a valid tile to be selected
            return
    
        Utils.disconnect_signal(_active_dungeon, "grid_tile_clicked", self, "_clicked_tile")
        emit_signal("grid_tile_selected", grid_coordinate)

    Then in the Oath of Honor skill we can swap the auto-selecting of the first valid grid space with the user-selected space.

    # Select one of the valid spaces
    # var selected_grid_space = _valid_grid_spaces[0]
    SelectionManager.select_grid_tile(_valid_grid_spaces)
    var selected_grid_space = yield(SelectionManager, "grid_tile_selected")

    Testing the selection logic

    Now let’s run the code again and see if it works as expected!

    Oath of Honor with space selection gif
    I created a tile highlighting effect for better demonstrating, but in hindsight I think it’d be better to highlight all available tiles when the skill is selected instead of only when you hover over a tile.

    Last Step (For Now)

    Okay, so I’ve been a little coy with showing you demonstrations of the code running without telling you exactly how I finished the setup. I just wanted to cover this topic last since I didn’t originally think of this architecture pattern when first creating the skill, so in a way this post sort of follows my own journey of discovery as I found better ways to do certain things as I worked on it.

    Because skills are tied to classes and not to characters (in the way Descent is designed, anyway) it made the most sense to me to have the skills be their own Resources, just like how units are their own Resources. (Remember, a Resource class in Godot is basically just a data class, akin to Unity’s ScriptableObject)

    The problem, though, is that skills are a combination of both data and functionality. I want to be able to separate the two so that we can store a reference to the skill’s data and access its functionality later during run-time. Separating things this way also helps with decoupling the classes and interdependencies on one another, and we won’t have to define a new resource type for each different skill we create. I also like this idea because it allows for swapping functionality during run-time if we want to do that somewhere down the line. I also have this idea in my head of defining all of the data for the game in an XML/JSON file format that can be loaded in dynamically, which would only be capable of storing data, not functionality. But it could store a reference to a script class!

    …It’s hard to articulate thoughts sometimes, ya know?

    Anyway, I got this idea from the game Rimworld, which I made a few mods for. They split their classes this way as well, allowing you to define your data in one place and implement it in another.

    The data that units will store will not be Skills, they’ll be SkillDefs. Each SkillDef will have all the skill information like name, description, stamina cost, experience cost, etc. But it will also contain a path reference to the script file that will implement it, which will be used dynamically during the game.

    Here’s how I defined the SkillDef resource class.

    extends Resource
    
    class_name SkillDef
    
    export(String) var skill_script_path := "res://scripts/skills/skill.gd"
    export(String) var skill_name
    export(String) var skill_description
    export(bool) var is_action
    export(bool) var is_interrupt
    export(int) var stamina_cost
    export(int) var experience_cost
    
    # Because the node is created on the fly, we also
    # need to make sure to free it when we're done with it.
    # Otherwise they will start to take up game memory.
    func get_skill(hero_ref) -> Node2D:
        var skill_node := Node2D.new()
        skill_node.name = skill_name
    
        # This is where we dynamically load the skill's functionality
        skill_node.script = load(skill_script_path)
        skill_node.hero = hero_ref
        skill_node.skill_def = self
        return skill_node
    Screenshot of Oath of Honor resource

    Then I’ll add the skill resource onto the Nite character’s skills array (which you’ll need to define) and suddenly it’s available to be used!


    And I think that just about covers it! Again, there’s some new stuff I added that I didn’t write about (things like code refactoring and clean-up) but the main purpose of this post was to start to understand how to build Skills for units to use. Hopefully with a little more work we’ll be able to crank these out like hotcakes!

    The code for this lesson and project up to this point can be found here.

  • Project Delve Part 6.2: Overlord and Monster Phases

    Project Delve Part 6.2: Overlord and Monster Phases

    Hiya again, I’m back with the next steps for making the Turn System! (Well, I suppose the actual turn system itself is already completed with the functioning state machine, but I want to fully flesh out the selecting and performing of each of the unit’s turns)

    Today the focus is going to be on the Overlord and monster phases. The code for the MonsterActionPhase will look almost identical to the code for the HeroActionPhase, since monsters are people too.

    …Okay, maybe not. But the code will still remain very similar. So similar in fact that I wonder whether I should combine the HeroActionPhase and the MonsterActionPhase into one base class. That would probably be the wise thing to do, but since when have I been wise? I’m going to not worry about that particular good-practice right now and just continue on, since I’m eager to keep up my momentum.


    MonsterActionPhase

    I’ll start by copying all of the contents of the hero_action_phase.gd script and pasting it into the monster_action_phase.gd script. Then I’ll rename a few variables and change the class_name value back to MonsterActionPhase so that the top of the script now looks like this.

    extends State
    
    class_name MonsterActionPhase
    
    var monster: Unit
    var action_points: int
    var leftover_movement: int
    
    func _init(sm: StateMachine, unit: Unit).(sm, "MonsterActionPhase"):
        monster = unit
        action_points = 2
        leftover_movement = 0

    Now, the monsters don’t get as many action choices as the heroes do. Specifically, they aren’t able to do the Stand Up action or the Revive action, since when a monster is defeated they’re removed from the game. They also aren’t able to do the Rest action since they don’t have stamina points.

    The other major difference between a hero’s turn and a monster’s turn is that monsters are only allowed to spend one action point on an attack during their turn, whereas heroes are allowed to use both action points on attacking, if they so choose.

    So with all of that in mind, let’s modify the _get_available_actions() function to remove the unavailable action types, as well as make an additional check when determining whether to allow the Attack action.

    var has_attacked: bool
    
    func _init(sm: StateMachine, unit: Unit).(sm, "MonsterActionPhase"):
        # ...
        has_attacked = false
    
    # ...
    
    func _get_available_actions():
        var available = [UnitActions.Actions.end_turn]
        if action_points <= 0:
            if leftover_movement > 0 and UnitActions.can_do_move_action(monster):
                available.append(UnitActions.Actions.move_extra)
        else:
            if not has_attacked and UnitActions.can_do_attack_action(monster, null):
                available.append(UnitActions.Actions.attack)
            if UnitActions.can_do_move_action(monster):
                available.append(UnitActions.Actions.move)
            if UnitActions.can_do_interact_action(monster):
                available.append(UnitActions.Actions.interact)
            if UnitActions.can_do_skill_action(monster):
                available.append(UnitActions.Actions.skill)

    As a side note, I moved the Actions enum to the UnitActions singleton since both the heroes and monsters will be using it.

    Then all we need to do is modify the _action_selected() function to remove the actions that cannot be selected! We can also set the has_attacked variable in the UnitActions.Actions.attack block.

    func _action_selected(action):
        # ...
        match action:
            # ...
            UnitActions.Actions.Attack:
                has_attacked = yield(UnitActions.do_attack_action(monster, "heroes"), "completed")

    And that pretty much covers the modifications we need to make to the MonsterActionPhase!


    GUI

    In the last post we made a GUIManager with a HeroTurnGUI scene. Because the monster group phase will behave very similarly to the hero group phase I think we can repurpose that GUI to serve both the heroes and monsters! The only differences will be which actions are available, which we already covered in the last section.

    Firstly, since this will now be used by both the heroes and the monsters I think it makes sense to rename it to something less hero-centric. I’ve opted to rename it to UnitTurnGUI, and I’ll accordingly change the function name in GUIManager to be get_unit_turn_gui() instead of get_hero_turn_gui(), as well as remove the “gui_type” metadata and instead just assign the scene to a group “unit_gui”.

    Displaying UnitTurnGUI scene view

    The other change I thought to make for the UnitTurnGUI is to change the column count of the button row to match the number of visible buttons. That way we won’t get overflowing buttons like we saw in the demo last week.

    Replaying part of gif from previous post

    The function now looks like this:

    func enable_buttons(action_list: Array, hide_disabled_buttons = false):
        var count = 0
        for action in _btn_map.keys():
            var btn = _btn_map[action]
            if action_list.has(action):
                btn.disabled = false
                btn.visible = true
            else:
                btn.disabled = true
                btn.visible = (
                    false
                    if (
                        hide_disabled_buttons
                        or action == UnitActions.Actions.stand
                        or action == UnitActions.Actions.move_extra
                    )
                    else true
                )
            if btn.visible:
                count += 1
        _button_grid.columns = count

    Unit activation order

    Another key thing we’ll need to be able to do is select which units we want to activate when. Currently we’re just activating the units in the order they’re in within the scene tree, but part of the strategy of the game will be determining the order of operations for your units’ activations.

    I think that selecting the unit’s avatar image might work, so let’s try that out.

    Let’s create a new script and name it something like avatar_selection_gui.gd. This GUI will hold the list of CharacterAvatar scenes previously held by the UnitTurnGUI, so we can remove the reference to that from UnitTurnGUI and add it to the AvatarSelectionGUI. There are two ways to go about doing this, one would be to sub-class the UnitTurnGUI to the AvatarSelectionGUI. The other would be to create a new scene with the AvatarSelectionGUI as the scene root node. I like the second approach more since it keeps the UI responsibilities separate, so that’s what I’ll do.

    We’re going to add three new functions to the AvatarSelectionGUI, one to enable selection, one to disable selection, and one to set which avatars should be shown in grayscale (indicating they’ve already been activated).

    Let’s start with the enable_avatar_selection() and disable_avatar_selection() functions.

    func enable_avatar_selection(disabled_options = []):
        for i in range(_avatar_list.avatars.size()):
            if disabled_options.has(_unit_list[i]):
                # If the avatar is disabled we don't want
                # to connect the following callbacks.
                continue
    
            var avatar = _avatar_list.avatars[i]
    
            # The "clicked" signal is a custom signal on character_avatar.gd
            # The mouse_entered and mouse_exited are signals provided by the Control node
            avatar.connect("clicked", self, "_on_clicked_avatar", [avatar])
            avatar.connect("mouse_entered", self, "_on_hover_over_avatar", [avatar, true])
            avatar.connect("mouse_exited", self, "_on_hover_over_avatar", [avatar, false])
    
    func disable_avatar_selection():
        for avatar in _avatar_list.avatars:
            avatar.disconnect("clicked", self, "_on_clicked_avatar")
            avatar.disconnect("mouse_entered", self,  "_on_hover_over_avatar")
            avatar.disconnect("mouse_exited", self, "_on_hover_over_avatar")

    The _on_clicked_avatar() function will emit a custom defined signal, avatar_clicked, along with the unit the avatar represents. How do we know which unit the avatar represents? I’m glad you asked! In the set_avatar_list() function that we brought over from the UnitTurnGUI we’re given a list of units. From those units we instantiate all the avatar scenes we need. All we need to do is add an extra line of code in that function to link the avatar with the unit it represents.

    avatar.set_meta("linked_unit", unit)

    Then in the _on_clicked_avatar() function, we just need to put the following.

    emit_signal("avatar_clicked", avatar.get_meta("linked_unit"))

    The other function about hover enter and exit will set the border outline to white or black, depending on whether it’s entering or exiting.

    func _on_hover_over_avatar(avatar, entering):
        if entering:
            avatar.border_color = Color.white
        else:
            avatar.border_color = Color.black

    As for the grayscaling, I just added another option to the CharacterAvatar shader indicating whether it should be grayscaled or not, and the grayscale_avatars() function will set this option on the desired avatars.

    And that pretty much covers it!

    Testing it out

    The last thing to do is replace the current _select_next_hero()/_select_next_monster() functions with using this functionality instead of using the next unit in the list. This is as simple as connecting to the avatar_clicked function on the AvatarSelectionGUI and starting the selected unit’s turn.

    # For simplicity I'm using the term "unit" rather than "hero"
    #  or "monster" but the function will behave the same for both.
    func _select_next_unit():
        var gui = GUIManager.get_avatar_selection_gui()
        gui.set_avatar_list(units)
        gui.set_header_text("Select unit...")
        gui.grayscale_avatars(_have_finished_turn)
        gui.connect("avatar_clicked", self, "_start_unit_turn",  [], CONNECT_ONESHOT)
        gui.enable_avatar_selection(_have_finished_turn)
    
    func _start_unit_turn(unit):
        GUIManager.get_avatar_selection_gui().disable_avatar_selection()
        # Change state to unit turn start phase ...
    Demonstrating updates to monster turn
    Want to play “see how many bugs you can spot?”

    We’re now able to select which unit we want to activate, and you can see the columns get resized when there’s an extra action available. The attack action is also only available once per turn for monsters. Sweet! We’ve verified our code works!


    Monster Groups

    Now, if all we had to do were select between any monster on the screen then our work would be done! (Minus one or twelve bug fixes) However, the Overlord’s monster phase works by selecting a monster group, and then activating all monsters within that group before moving on to the next group. That means that we need yet another layer of selection logic.

    Fortunately, because we separated the AvatarSelectionGUI from the UnitTurnGUI we only need to worry about the first in the OverlordMonsterPhase. However, we’ll need to tweak the AvatarSelectionGUI slightly in order to accommodate non-unit nodes (since the OverlordMonsterPhase will be concerned with nodes that have units as their children, rather than the units themselves).

    Fortunately this won’t be too much of a hassle. We’ll just add a second parameter option to the set_avatar_list() function, is_unit_group. This will affect the function in the following way.

    func set_avatar_list(units, is_unit_group = false):
        _unit_list = units
    
        var avatars = []
        for unit in units:
            if not unit or (is_unit_group and unit.get_children().size() == 0):
                continue
    
            var avatar = _avatar_scene.instance()
            var meta_key
            if is_unit_group:
                # We're assuming that all children of the unit group node
                # are of type "Unit"
                avatar.character_sprite = unit.get_children()[0].unit_data.sprite
                meta_key = "linked_units"
            else:
                avatar.character_sprite = unit.unit_data.sprite
                meta_key = "linked_unit"
    
            avatar.set_meta(meta_key, unit)
            avatars.append(avatar)
        _avatar_list.set_avatar_list(avatars, true)

    Then when in the callbacks like the _on_clicked_avatar() or _on_hover_over_avatar() we just need to check for which meta key is defined before using the appropriate one.

    if avatar.has_meta("linked_units"): ...

    All Done!

    And that’s it! Obviously I didn’t provide every new line of code I wrote, but I hope that I explained my process well enough for people to follow along and fill in the spaces themselves. Here’s a quick demo of checking that the unit selection process is working seamlessly with the state machine.

    Demonstrating a full round with selection logic

    The code for all the posts up to this point can be found here.

  • Project Delve Part 6.1: Hero Action Phase

    Project Delve Part 6.1: Hero Action Phase

    Alrighty, there’s a lot to cover in this one so let’s jump right into this!

    Breaking Up the UnitActions Singleton

    Up until this point I’ve been throwing a lot of functionality into the unit_actions.gd script, but the time has come to delegate responsibility. From now on, the UnitActions singleton will only be in charge of

    1. Checking whether an action can be performed
    2. Performing that action

    Almost all of the code that was in that class will be split into two additional classes: DrawManager and SelectionManager. I’ll get to those in a minute, but first let’s stub the functions the UnitActions class will implement. The way I’ve done this is by defining a can_do_<ActionName>_action() and do_<ActionName>_action() method for each possible action a hero can perform. The “can_do” functions will make checks (outlined in the blue boxes in the flowchart a few sections down) and return a bool value indicating whether the action is valid. The “do” functions will actually perform the actions and return when the full action has been completed, thus they will need to be coroutine functions that utilize the yield keyword.

    As an example, I’ll demonstrate a simple implementation of the Rest action.

    func can_do_rest_action(unit) -> bool:
        return true
    
    func do_rest_action(unit):
        unit.rest()
        yield(get_tree(), "idle_frame")

    For now, feel free to just stub out the rest of the action methods and we’ll return to them later.


    DrawManager

    The DrawManager class will be responsible for making common custom draw calls on the screen. For the functionality we’ve made so far, this includes drawing the path a unit will take when it moves, and drawing a target and line of sight when making an attack. Like the UnitActions class, we’ll add this to the list of auto-loaded scripts so that it can be accessed from anywhere.

    The purpose of these manager classes is to expose the abstraction and hide the implementation. All that outside classes need to know about the DrawManager is that it can make draw calls in the way they need it to. So let’s start by defining the public-facing functions in the DrawManager.

    # draw_manager.gd
    
    func enable_path_drawing():
        pass
    
    func disable_path_drawing():
        pass
    
    func enable_target_drawing():
        pass
    
    func disable_target_drawing():
        pass

    Let’s cut and paste the drawing functionality from the unit_actions.gd script into the draw_manager.gd script. These will primarily be the functions _highlight_path() and _highlight_target_point(), which are callbacks for the grid_tile_hovered signal emitted by the Dungeon. That means we’re going to need a reference to the current dungeon in the DrawManager class, so let’s add one more public method to the script.

    func set_active_dungeon(dungeon):
        _active_dungeon = dungeon

    I’ll walk you through what I did for the enable_target_drawing() method and let you figure out the enable_path_drawing() on your own using the same principles I do here.

    The things the drawing function needs in order to properly draw are:

    1. The position of the origin point
    2. The target group (null if targeting an empty space is allowed)
    3. Whether line of sight is required

    If the target group is null then we just need to connect to the grid_tile_hovered signal on the _active_dungeon. Otherwise we’ll get all the nodes in the target group and connect to their entered signal, assuming the group is composed of MouseListener derived objects.

    func enable_target_drawing(origin_position: Vector2, target_group = null, needs_LoS = true):
        if not target_group:
            _active_dungeon.connect("grid_tile_hovered", self, "_draw_target_to_point", [origin_position, needs_LoS])
        else:
            var group = get_tree().get_nodes_in_group(target_group)
            for node in group:
                node.connect("entered", self, "_draw_target_to_group_member", [origin_position, needs_LoS])

    For the disable function we basically just need to do the same thing, but use disconnect() instead of connect().

    func disable_target_drawing(target_group = null):
        if _active_dungeon.is_connected("grid_tile_hovered", self, "_draw_target_to_point"):
            _active_dungeon.disconnect("grid_tile_hovered", self, "_draw_target_to_point")
    
        if target_group:
            var group = get_tree().get_nodes_in_group(target_group)
            for node in group:
                if node.is_connected("entered", self, "_draw_target_to_group_member"):
    
                    node.disconnect("entered", self, "_draw_target_to_group_member")
    
        _active_dungeon.clear_drawings()

    (the functions I’m connecting to are derived from the functions we brought over from the UnitActions class)

    Now see if you can implement the other two public functions on your own! (If you get stuck or want to “check your work” you can compare it to my code, which I link to at the end of this post. Not saying that my code is the best per-se, but it works!)


    SelectionManager

    The SelectionManager class will only expose one function for now, which will be the _wait_until_unit_selected() function from UnitActions. Since there are many things we’ll want to select within the game, though, I’m going to make it more generic. I also want to be able to control which things should be selectable, so I’m going to add an additional parameter target_group like I did in the DrawManager.

    # selection_manager.gd
    
    signal group_member_selected
    
    # target_group can be either a string, or an array
    # of nodes. If it's a string, the group is the result
    # of calling get_nodes_in_group() with the string. If
    # the target_group is null, it defaults to "interactable".
    # If the target_group is empty, null will be returned.
    func wait_until_group_member_selected(target_group, highlight_color = null, fade = false, fade_frequency = 0):
        var group = _get_group(target_group)
        if group.empty():
            return null
    
        for node in group:
            node.connect("clicked", self, "_select_group_member", [node, target_group])
            node.connect("entered", self, "_toggle_highlightable", [node, true, highlight_color, fade, fade_frequency])
            node.connect("exited", self, "_toggle_highlightable", [node, false])
    
        var member = yield(self, "group_member_selected")
        return member
    
    func _toggle_highlightable(event, highlightable, highlighted, color = null, fade = false, fade_frequency = 0):
        highlightable.toggle_highlight(highlighted, color, fade, fade_frequency)
    
    func _select_group_member(event, member, target_group):
        # This function just disconnects the signals like we did in the draw manager
        _disconnect_group_signals(target_group)
        emit_signal("group_member_selected", member)

    I also moved the toggle_highlight() function from the Unit class to a superclass I called Highlightable, which will allow any node that uses or extends that class to show the same outline effect as the units.


    Hero Action Phase

    Phew! Now that we’ve got all of that behind us, we can finally start work on the HeroActionPhase! (Sorry if my explanations were a little patchy, there was a lot to cover and if I pasted all of the code I wrote along with explanations, it would make this post way too long.)

    Just like how in my previous post I created a flowchart diagram for the flow of the game, I thought it’d be a good idea to do the same thing for the HeroActionPhase. I find that there’s something very helpful about being able to visualize a workflow. Building it helps me organize my thoughts and think about how to handle certain states. Here’s what I came up with for the HeroActionPhase.

    Flowchart of the hero action phase

    Dissecting this a bit, this diagram is a high-level outline of what a hero’s turn will look like. We will start out by checking whether the hero is knocked down. If so, there’s only one action they can perform, which is to stand up. Otherwise we will have a list of actions for the hero to choose from (displayed in the orange boxes).

    Almost all of the actions have a prerequisite that must be met to determine whether they can be performed, which I’ve outlined on the left in the A-E boxes. The actual checks will likely turn out to be much more nuanced than I’ve written here in the diagram, but they’ll serve as a good starting point for us to build on as we find what those nuanced things are.

    The small text in the top left grouped by the curly braces is just a copy-paste of the rules describing the actions a hero can take on their turn, found on page 7 of the Descent Rulebook.


    Setting up the phase flow

    Let’s go to the hero_action_phase.gd script now. There will be three main variables we need to keep track of during this phase:

    1. The current hero
    2. The number of remaining action points
    3. Any leftover movement

    We can initialize these in the constructor, with the hero being passed in as an argument.

    # hero_action_phase.gd
    
    var hero: Unit
    var action_points: int
    var leftover_movement: int
    
    func _init(sm: StateMachine, unit: Unit).(sm, "HeroActionPhase"):
        hero = unit
        action_points = 2
        leftover_movement = 0

    When we enter this state we’ll want to present the user with the list of actions they can take, so that means we’ll need to use those checks we created in the UnitActions singleton. We can worry about how the user will select the actions in a little bit, so let’s start with what we know.

    # This enum is for convenience so we can keep track of which
    # actions we can/do use
    enum Actions {
        move,
        rest,
        skill,
        attack,
        interact,
        revive,
        stand,
        move_extra,
        end_turn
    }
    
    func enter_state():
        .enter_state()
        _select_action()
    
    func _select_action():
        # This function will call _get_available_actions() to
        # tell the player which actions they can pick, and
        # wait for them to pick one.
        pass
    
    func _action_selected(action):
        # This function will be called when one of the actions are
        # selected and will call the UnitActions.do... function
        # matching the selected action.
        pass
    
    func _get_available_actions():
        # This function will call all of the UnitActions.can_do...
        # functions and store a list of the actions that are available.
        pass

    Get available actions

    Let’s work on the _get_available_actions() method first. As I said in the comment, this method will be called by the _select_action() function to get a list of which Actions can be taken. The functionality for these checks has been put in the UnitActions singleton so all we need to worry about is calling them from this script, which keeps it simple and clean!

    func _get_available_actions():
        # The end turn action will always be available
        # if for some reason the player wants to end their
        # turn early.
        var available = [Actions.end_turn]
    
        # First we'll check whether the hero needs to
        # stand up, like outlined in the flowchart.
        if UnitActions.can_do_stand_up_action(hero):
            available = [Actions.stand]
    
        # If they can't perform the stand up action, then we
        # need to check the remaining action points.
        elif action_points <= 0:
            # Check for any leftover movement
            if leftover_movement > 0 and UnitActions.can_do_move_action(hero):
                # I decided to make the leftover movement
                # its own action so that we can easily
                # differentiate it from the regular move
                # action when we're performing the action.
                available.append(Actions.move_extra)
    
        # If the hero still has remaining action points then
        # we will check all of the other action availability.
        else:
            if UnitActions.can_do_attack_action(hero):
                available.append(Actions.attack)
            if UnitActions.can_do_move_action(hero):
                available.append(Actions.move)
            if UnitActions.can_do_rest_action(hero):
                available.append(Actions.rest)
            if UnitActions.can_do_interact_action(hero):
                available.append(Actions.interact)
            if UnitActions.can_do_skill_action(hero):
                available.append(Actions.skill)
            if UnitActions.can_do_revive_action(hero):
                available.append(Actions.revive)
    
        return available

    Action selected

    Now let’s take a look at the _action_selected() function. This function will call on the UnitActions singleton to perform the action, wait for it to finish, and then call _select_action() again to start the cycle over until the player chooses the Actions.end_turn action.

    func _action_selected(action):
        # We'll want to keep track of how much ability points
        # are spent during the hero's action, since some actions
        # (like standing up) will take all actions and if the player
        # cancels an action we don't want to count it as an action taken.
        var ap_used := 1
    
        match action:
            Actions.end_turn:
                # If it's the end turn action we can just update
                # the state and return early
                _change_state(HeroEndPhase.new(_parent))
                return
            Actions.stand:
                yield(UnitActions.do_stand_up_action(hero), "completed")
                ap_used = 2
            Actions.move:
                # The do_move_action() function will return -1 if the
                # action was cancelled
                var cost = yield(UnitActions.do_move_action(hero, true), "completed")
                if cost == -1:
                    ap_used = 0
                else:
                    leftover_movement = hero.unit_data.speed - cost
            Actions.move_extra:
                var cost = yield(UnitActions.do_move_action(hero, false, leftover_movement), "completed")
                if cost == -1:
                    ap_used = 0
                else:
                    leftover_movement -= cost
            Actions.rest:
                # Performing a rest action will always take the
                # remaining action points
                yield(UnitActions.do_rest_action(hero), "completed")
                ap_used = 2
            Actions.skill:
                # Eventually we'll come back to this, where null
                # will be replaced by the chosen skill
                yield(UnitActions.do_skill_action(hero, null), "completed")
            Actions.attack:
                # "monsters" is the target group we want to affect
                yield(UnitActions.do_attack_action(hero, "monsters"), "completed")
            Actions.interact:
                yield(UnitActions.do_interact_action(hero), "completed")
            Actions.revive:
                yield(UnitActions.do_revive_action(hero), "completed")
    
        action_points -= ap_used
        _select_action()

    Select action

    Finally let’s figure out how we want the player to select their action. For now I think a simple button-based UI will suffice, but because I always prefer working with things that look good over things that look mangy I wanted to do a mock-up first.

    mock of the hero gui
    I like the idea of having the hero avatars in the top corner, with the current one being highlighted in some way.

    Fortunately, making a simple layout is pretty easy in Godot. After messing around with a few options, here’s what I ended up settling on.

    the (semi) finalized hero gui

    Hero turn GUI

    I attached a script to the base node and named it hero_turn_gui.gd. In the script I get a reference to all of the buttons in the scene and create a map from the HeroActionPhase.Actions enum to their corresponding button. I also define a button_pressed signal in the script that we’ll be able to connect to in the HeroActionPhase, and a function to enable/disable buttons based on the result of _get_available_actions(). Here’s what all that looks like in the script.

    # hero_turn_gui.gd
    
    extends CanvasLayer
    
    class_name HeroTurnGUI
    
    signal button_pressed
    
    onready var _btn_move = $Backdrop/ButtonGrid/Move
    onready var _btn_move_extra = $Backdrop/ButtonGrid/MoveExtra
    onready var _btn_attack = $Backdrop/ButtonGrid/Attack
    onready var _btn_skill = $Backdrop/ButtonGrid/Skill
    onready var _btn_interact = $Backdrop/ButtonGrid/Interact
    onready var _btn_rest = $Backdrop/ButtonGrid/Rest
    onready var _btn_revive = $Backdrop/ButtonGrid/Revive
    onready var _btn_stand = $Backdrop/ButtonGrid/StandUp
    onready var _btn_end = $Backdrop/ButtonGrid/EndTurn
    onready var _hero_name = $HeroName
    var _btn_map: Dictionary
    
    func _ready():
        # The purpose of this line will be
        # explained in the next section
        set_meta("gui_type", "hero")
    
        _btn_map = {
            HeroActionPhase.Actions.move: _btn_move,
            HeroActionPhase.Actions.move_extra: _btn_move_extra,
            HeroActionPhase.Actions.attack: _btn_attack,
            HeroActionPhase.Actions.skill: _btn_skill,
            HeroActionPhase.Actions.interact: _btn_interact,
            HeroActionPhase.Actions.rest: _btn_rest,
            HeroActionPhase.Actions.revive: _btn_revive,
            HeroActionPhase.Actions.stand: _btn_stand,
            HeroActionPhase.Actions.end_turn: _btn_end,
        }
    
    
        # This is just a simple way to forward on the button
        # pressed signal to listeners of this script
        for key in _btn_map.keys():
            _btn_map[key].connect("pressed", self, "emit_signal", ["button_pressed", key])
    
    func enable_buttons(action_list: Array):
        for action in _btn_map.keys():
            var btn = _btn_map[action]
            if action_list.has(action):
                btn.disabled = false
            else:
                btn.disabled = true

    GUI Manager

    Now in order to access this script we’ll need a reference to it somehow. Let’s create another auto-loaded script and call it gui_manager.gd. This script will only have one function (for now) that will return the HeroTurnGUI if it’s found in the scene.

    # gui_manager.gd
    
    extends Node
    
    # Gets the Hero GUI in the current scene. If not
    # present, this returns null.
    func get_hero_gui():
        var guis = _get_gui_type("hero")
        if guis.empty():
            return null
        return guis[0]
    
    
    func _get_gui_type(type: String) -> Array:
        var guis = []
    
        # I put the HeroTurnGUI scene in a group called "gui"
        var nodes = get_tree().get_nodes_in_group("gui")
        for node in nodes:
            # Because I specified the gui_type metadata in
            # the HeroTurnGUI, this will be able to get the
            # exact script we're looking for.
            if node.get_meta("gui_type") == type:
                guis.append(node)
        return guis

    Select action (…again)

    Home stretch guys! Now that everything else is in place we can fill out the _select_action() function back in hero_action_phase.gd. We just need to get a reference to the HeroTurnGUI in the scene, enable the buttons that should be enabled, and listen for the button_pressed signal.

    func _select_action():
        var hero_gui = GUIManager.get_hero_gui()
        hero_gui.enable_buttons(_get_available_actions())
        hero_gui.connect("button_pressed", self, "action_selected", [], CONNECT_ONESHOT)

    Bringing It All Together

    And finally here we are at the end of this long post! I’ll note that even though I didn’t mention it during this post, I did a lot of trial and error testing while writing this code to make sure it all worked as expected, and I highly recommend you do the same as you (if you) follow along with me. That way you don’t have to go back and track down why a certain aspect isn’t working as expected after you’ve finished everything and instead know exactly what changed since your last test, which helps immensely in debugging.

    So here’s a little demo of how the code looks so far! (I added a little bit of animation of the GUI so it felt a little more alive)

    gif of a full hero turn

    The code for this post can be found here.

  • Project Delve Part 6: Turn System

    Project Delve Part 6: Turn System

    Alright, now we’re getting into the real juicy stuff — making systems for our game to use! If you’re unfamiliar with the term systems in this context, a short explanation is this: Systems are pieces of code that are used for higher-level functionality in the program. They’re responsible for tying various related parts of the program together. Think of an office building: these generally have electricity, air conditioning, heating, plumbing, security, etc. Each of these things can be considered a system within the overall building, each affecting parts of the building in their own way.

    The system we’ll be focusing on in all of the Part 6 posts is the Turn System (yes, this is going to be a multi-post topic). How will the game know whose turn it is, what they can do, what happens after that, and what triggers it? Let’s dive in.


    Game Flow

    The first thing to establish when figuring out a game’s flow are its states, and what moves the game through the states (transitions). This is known as the State Machine pattern. The State Machine pattern is a very popular game programming pattern because games are full of states and transitions. Actions the user can and can’t take depend on the state of the game, and states change when a transition happens.

    As an example, take a generic FPS game. For the sake of the example I’ll keep it simple. The player may have two states:

    1. Player has ammo
    2. Player is out of ammo

    While the player is in the first state (has ammunition), they can fire their weapon. If they are in the second state (out of ammunition), they cannot fire their weapon. The transition between S1 and S2 would be triggered by the amount of ammo a player has left: if the player’s ammunition is equal to zero, transition to S2. The transition between S2 and S1 would be triggered by an action: if the reload action is taken while in S2, transition to S1.


    Diagraming the states and transitions

    Using an online flowcharting tool, I created the following diagram based on the Descent rulebook.

    Flowchart of the game states

    The game always starts with the heroes. After the heroes all take their turn, it’s the overlord’s turn. The overlord starts their turn by “drawing a card” (something that I may or may not keep in this game, we’ll see), and then activates each monster group. When a monster group is activated, each monster in that group takes its turn. After all the monster groups have activated, the overlord’s turn is over and it’s back to the heroes turn.

    One thing that I didn’t diagram correctly is that the check for whether any of the objectives have been completed only happens after the overlord’s turn, when in reality it should be happening at the end of each individual hero turn and each individual monster turn, but that’s the only caveat.


    Coding The State Machine

    I’ll start by saying that while I have been enjoying using Godot, GDScript is not a contributor to that enjoyment. It’s true that Godot technically supports C# scripting (which I much prefer) but I tried it briefly and it turned out to be more trouble than it was worth to use. I like that GDScript is optimized directly for the game engine, but overall it hasn’t been my favorite language to work with.

    Rant aside though, one of the reasons I dislike GDScript is due to its poor object-oriented structure. It’s very much a scripting type language (as the name would suggest) and as such it doesn’t make it easy to create and use regular generic objects or create a standalone system architecture. So I struggled for a while to figure out how to make a simple state machine that fit with the GDScript paradigm instead of fighting against it, and I think I found a workable solution.

    Let’s start by creating the StateMachine class.

    # state_machine.gd
    
    extends Node
    
    class_name StateMachine
    
    # Passes with parameters: new state name
    signal changed_state
    
    # The current state
    var current
    
    func change_state(new_state):
        current = new_state
        emit_signal("changed_state", new_state.name)

    This works, but doesn’t do much at the moment. It only holds a reference to a variable that could be anything, and the state itself doesn’t know whether or not it’s active. This is important to know because we likely will want to alter the game in some way when the state gets changed. We could of course have the states connect to the changed_state signal and check if the new current state is itself, but that would get very messy very quickly. Instead, let’s create a State class that has an enter_state() and exit_state() method. Then when change_state() is called on the StateMachine, we can just call exit_state() on the current state and enter_state() on the new state.

    # state.gd
    
    extends Node
    
    class_name State
    
    signal state_entered
    signal state_exited
    
    func enter_state():
        emit_signal("state_entered")
    
    func exit_state():
        emit_signal("state_exited")
    
        # After exiting the state we can remove it from the scene tree.
        # Otherwise we'll have memory leaks as unused objects stack up.
        queue_free()
    # state_machine.gd
    
    func change_state(new_state):
        if current != null:
            current.exit_state()
    
        current = new_state
    
        if current != null:
            current.enter_state()
            emit_signal("changed_state", current.name)
        else:
            emit_signal("changed_state", null)

    And just like that we have a simple state machine with states that know when they’ve been entered and exited!


    Expanding the functionality

    Because some of our states need to keep track of their own sub-states, we need to give them their own state machines to use. It would also be convenient to have a reference to the StateMachine owning the state from within the state itself too. That way when a state knows it should update, it can call the change_state() function on its parent state machine to move to the next state. Let’s add these options to the State class. (Have I said “state” enough?)

    var _parent: StateMachine
    var _state_machine: StateMachine
    
    func _init(owning_state_machine: StateMachine):
        _parent = owning_state_machine
        _state_machine = StateMachine.new()
    
        # We want to add the state machine as a child to this Node so that
        # when it's freed, the state machine will be freed as well.
        add_child(state_machine)
    
    func _change_state(next_state: State):
        _parent.change_state(next_state)

    To test that this is working, let’s create a new scene and add a script onto the root Node that extends StateMachine, as well as two inner classes that extend the State class (these don’t need to be attached to any nodes since they’ll be created and destroyed dynamically). Add a method to the StateMachine-derived class that calls change_state() with a new instance of one of the state classes we just created. From that state class, inside the enter_state() method, we’re going to call _change_state() with a new instance of the second state class. In each of the enter_state() and exit_state() methods, add a print() statement indicating we just entered or exited the state.

    In code it’ll all look something like this.

    # my_state_machine.gd
    
    extends StateMachine
    
    func test_state_machine():
        print("Starting state machine")
        change_state(State1.new(self))
    
    class State1:
        extends State
    
        func enter_state():
            # Putting a period before the function name will call the
            # super-class's method
            .enter_state()
            print("Entered state 1")
    
            # Passing a reference to the parent state machine to the next state
            _change_state(State2.new(_parent))
    
        func exit_state():
            .exit_state()
            print("Exited state 1")
    
    class State2:
        extends State
    
    func enter_state():
        .enter_state()
        print("Entered state 2")

    Now all that’s left to do is add a button to the scene and connect its pressed signal to the test_state_machine() method! Before running the code, let’s first think about what we expect the output to look like. First, the print() method in the test_state_machine() method will run. Then we would expect to see “Entered state 1” as the enter_state() method is called on the state. We would then expect to see “Exited state 1” and then “Entered state 2.” Let’s test it out and see!

    demonstrating state machine functionality

    Implementing The Game Flow

    Now that we can see that our state machine works as expected, let’s scaffold up the game flow based on the diagram at the start of the post. Due to the number of state classes I’m not going to post the contents of every script, but I will point out a few key things.


    Purple state blocks

    First of all, we have the purple state blocks. These states will be the ones that make use of their own state machines to create sub-states. These “super” states will want to monitor when their sub-states change so that when the EndPhase state is entered, we can make the appropriate check for whether we should initiate another round or move on to the next phase. I’ll demonstrate this with the HeroGroupPhase state.

    # hero_group_phase.gd
    
    extends State
    
    class_name HeroGroupPhase
    
    # The syntax here of _init().(): is just calling the super method of _init
    # (like calling the super constructor)
    func _init(sm: StateMachine).(sm, "HeroGroupPhase"):
        _state_machine.connect("changed_state", self, "_on_sub_state_machine_change_state")
    
    func enter_state():
        .enter_state()
        print("Hero group turn started!")
        # Make the parent state machine of the sub-state this class's
        # state machine
        _state_machine.change_state(HeroStartPhase.new(_state_machine))
    
    func _on_sub_state_machine_change_state(new_state_name: String):
        if new_state_name == "HeroEndPhase":
            print("Have all heroes been activated?")
            # !! Check if any heroes still need to take their turn !!
            _change_state(OverlordPhase.new(_parent))

    When the HeroGroupPhase state is entered, it starts the sub-state machine of the hero turn. When the state machine changes states, the HeroGroupPhase state checks the new state. If it’s the HeroEndPhase state we make our check and then proceed according to the flowchart. (In practice these sub-states will receive which hero’s turn they’re acting on, among other things, but since I don’t have that coded out just yet I’m leaving it be for now.)


    Orange state blocks

    The orange state blocks represent states that will immediately change themselves to the next state. They all look almost identical to this.

    extends State
    
    func _init(sm: StateMachine).(sm, "OrangeStateType"):
        pass
    
    func enter_state():
        .enter_state()
        _change_state(NextState.new(_parent))

    “But why make a state that is only transitory and has no self-contained functionality?” I hear you ask. Well the reason for these “empty” states is so that any actions that trigger “at the beginning of the hero’s turn” or “at the end of the hero’s turn” can listen for these states to be entered. We may not end up needing them because I may find a better way to do the same thing, but it works for now!


    Green state blocks

    Green state blocks are where the actual gameplay will happen. These states will not auto-advance to the next state until certain conditions are met. Because they are much more complex, I’ll be explaining these in more depth in the following post.


    The Result

    After implementing each of the states in my diagram, this is how the flow of the state machine looks now.

    the output of the full state machine

    So that’s all for today. In the next post, like I mentioned in the above section, we’ll be diving deeper into what will happen in the “functional” states. Primarily we’ll be focused on the HeroActionPhase.

    The code for this lesson can be found here.

  • Project Delve Part 5: Line of Sight

    Project Delve Part 5: Line of Sight

    In Descent and many other games, units often must have what’s known as “line of sight” to their targets when performing various actions such as attacking, buffing an ally, teleporting, and more. Sometimes games differentiate between “line of sight” and “line of targeting,” the difference being between what a unit can see versus what a unit can target. However, for our game these are going to be the same for simplicity’s sake.

    At the end of this post we’ll have a functional line-of-sight calculator, as well as a visual representation of what a unit can see and what it cannot.


    The Script

    We’ll start by adding a new Node2D to the Dungeon scene called “LineOfSight” and create a new script to attach to it. I named it line_of_sight.gd (super original, right?). Our script is going to have only a single public method: can_see(). This follows the single-responsibility principle, which states that functions, modules, and classes (or in our case scripts) should be responsible for only a single part of the larger program. I’m not always the best at following all of the principles I learned in school in every piece of code I write, but the goal is to always be striving to improve.

    The basic idea of the function is this: take in two points and try to draw an (invisible) uninterrupted line between both of them (also known as a raycast). If the line is not blocked by anything we define to be a vision-blocking item, the target point is considered to be in the line of sight of the origin. Let’s start by defining the function signature.

    func can_see(world_point_origin: Vector2, world_point_target: Vector2):
        pass

    The simplest way to do the check is to use Godot’s raycaster.

    get_world_2d().direct_space_state.intersect_ray(pointA, pointB)

    The intersect_ray() function returns a dictionary object containing the information of the first intersection the ray makes from pointA to pointB. If there’s no obstruction, the dictionary will be empty. This is the state we’ll want to check for to make sure there’s line of sight from pointA to pointB.

    Our simple function should look like this now.

    func can_see(world_point_origin: Vector2, world_point_target: Vector2):
        var result = get_world_2d().direct_space_state.intersect_ray(world_point_origin, world_point_target)
        return result.empty()

    The only caveat with using a raycaster is that it interacts with the physics system, which our game does not use. In order for the ray to collide with something, that “something” needs to have a collider attached, so we’ll need to set up colliders on our tileset. Those instructions can be found on the Godot docs.

    Once you’ve set that up, select the Obstacles node in the Dungeon scene and enable the “Show collision” property in the inspector. You should see something like this if you’ve set it all up properly.

    how the grid should look when obstacles has "show collisions" checked

    Now if we write some quick code in the dungeon.gd script we can check that our can_see() function is working like it should. If the two points are within line of sight of each other, the line color will be green. If not, it will be red.

    demonstrating direct line of sight

    Refining The Function

    So now that we have a basic function to calculate line of sight, let’s look at the rules for how Descent determines line of sight.

    In order for a figure to have line of sight to a space, a player must be able to trace an uninterrupted, straight line from any corner of that figure’s space to any corner of the target space.

    Rulebook, pg. 12, “Line of Sight”

    This approach to line of sight is known as the corner-to-corner method. The other most commonly used method (when dealing with a grid system) is the center-to-center approach. I like the corner-to-corner method because it allows for a much larger field of vision than center-to-center.

    With this in mind, let’s update the function. We now want to do the same raycast from each of the origin point’s corners to each of the target point’s corners. If any of the raycasts are successful (meaning they didn’t collide with anything) then we can exit the function early without needing to do any more checks.

    First of all, we need to ensure that the world point we pass to the can_see() function is the top-left corner of the tile that was clicked on. This is fairly simple since the origin point of our tiles are set to the top left, so we just need to get that point and then add Vector2.RIGHT * tile_size, Vector2.DOWN * tile_size, and Vector2.ONE * tile_size to it and we’ll have the four corners of the tile.

    Let’s start by getting the position of the top left corner of the tile in world coordinates. Because the LineOfSight node is on the Dungeon, I think it makes sense for the dungeon.gd script to have a method that exposes the can_see() function, just so we don’t have to rely on the structure of the Dungeon scene tree to get the line_of_sight.gd script.

    # dungeon.gd
    
    func has_line_of_sight_to(from_world_point: Vector2, to_world_point: Vector2):
        # First we need the map coordinates for the given world coordinates.
        var origin_map_point = floors.world_to_map(from_world_point)
        var target_map_point = floors.world_to_map(to_world_point)
    
        # Then we need to convert those map coordinates back to
        # world coordinates. This will give us the top-left corner
        # of the tiles in world coordinates.
        var origin_world_point = floors.map_to_world(origin_map_point)
        var target_world_point = floors.map_to_world(target_map_point)
    
        # Then we just return the result of the can_see() function
        return $LineOfSight.can_see(origin_world_point, target_world_point)

    Now let’s fix the can_see() function to check each corner of the origin point and target point.

    # line_of_sight.gd
    
    func can_see(world_point_origin: Vector2, world_point_target: Vector2):
        for from_world_point in _get_tile_corners(world_point_origin):
            for to_world_point in _get_tile_corners(world_point_target):
                if from_world_point == to_world_point or _has_LoS(from_world_point, to_world_point):
                    return true
    
        return false
    
    func _has_LoS(from_world_point: Vector2, to_world_point: Vector2):
        var result = get_world_2d().direct_space_state.intersect_ray(from_world_point, to_world_point)
        return result.empty()
    
    func _get_tile_corners(point):
        return PoolVector2Array(
            [
                point,                              # top left
                point + Vector2.RIGHT * _tile_size, # top right
                point + Vector2.DOWN * _tile_size,  # bottom left
                point + Vector2.ONE * _tile_size    # bottom right
            ]
        )

    The last variable we need is the tile_size, which the tilemap can give us in the dungeon.gd script. So when I said we’d only be surfacing a single function in the line_of_sight.gd script… I lied. Oops! But because this function will supplement the can_see() function I think it still follows the single-responsibility principle, so we’re in the clear.

    # line_of_sight.gd
    
    var _tile_size: Vector2
    
    # Make sure to call this function from the dungeon.gd
    # script at some point before calling can_see()
    func set_tile_size(size: Vector2):
        _tile_size = size

    Here’s a visual representation of how the algorithm behaves now.

    demonstrating corner-to-corner line of sight

    What’s Left?

    So it seems like we’re pretty much done, right? Well, almost. Our function currently only considers the tilemap obstacles for blocking line of sight (because that’s the only thing we’ve set up colliders for so far). But in the game, there are more things that can block line of sight than a static obstacle — units, for instance. Units of different types (meaning hero or monster) block line of sight for the opposite type, so we still need to account for that in our calculations.

    As I said at the start of the post, I’d also like to have a visual indicator of what a unit can or cannot see. I originally thought it’d be cool to have all the tiles that are within line of sight of a unit highlighted, but running this function for every possible combination of tiles on the grid could get expensive, especially when you consider that the grid will most likely be much larger than our little demo dungeon here. I think instead I’ll settle for calculating line of sight only when needed, and only on hovered tiles (kind of like what’s happening when displaying the movement of a unit).

    demonstrating targeting
    This is a general idea of how targeting could work. Ultimately I’ll want to update the artwork, but this will suffice for now

    And of course lastly there’s bound to be bugs or inconsistencies that will come to light with further use. I actually discovered one bug while recording the above gif! It seems like when the line of sight would pass on the diagonal edge of an obstacle tile (but not through it) it counts it as being blocked. This is a case that’s specifically called out in the rulebook as being allowed, and I think it should be as well, so I’ll need to figure out the best way to handle that use case.

    The code for this post can be found here.

  • Project Delve Part 4.3: Various Enhancements

    Project Delve Part 4.3: Various Enhancements

    So for this post, unlike the posts I’ve made before, I’m not going to be walking you through step-by-step how I created one thing or another (though I will explain the general concept). This is because I’d like to move on to Part 5 in the series posts but there were some things I felt needed to be addressed first, both incomplete things and quality of life things. There were quite a few changes I made that would take a lot of time explaining each one in detail when they felt more or less unrelated to the overall structure of the project. So with that said, let’s jump in!


    Pathfinding Misunderstanding

    One thing I do want to explain fully, though, is a “bug” I thought I had found while testing out the pathfinding script. (See the issue I posted on the Godot Github repo here.) In short, I thought that the AStar.get_x_path() functions were incorrectly calculating the overall weight of the path, and would not always return the path with the shortest weight.

    Because Godot is open-source, I was able to look through the engine’s source code and found the implementation of their pathfinding algorithm. I realized I had incorrectly assumed the algorithm was based on a grid-like structure, but when I looked at the source code I saw that the distance between points was calculated based on Euclidean distance, and not by grid-tile neighbors.

    This is important because, if you remember from your geometry class, the Pythagorean Theorem states that the third side of a right triangle is equal to the square root of the sum of the square of the other two sides. Or, in other words, a2 + b2 = c2.

    So while the distance between two neighboring orthogonal points on our grid is 1, the distance between to neighboring diagonal points on the grid is 1.41. Because of this false assumption I was working under that the distance between any neighboring points would be 1 * <weight of the point>, there was a problem when the diagonal point had a weight greater than 1 (e.g. a water tile).

    A gif of the issue

    Of course this makes sense when you think about all the various applications of AStar that needed to be accounted for, but I wasn’t sure how to solve the issue. Fortunately a helpful person answered my issue with the solution: extend the AStar class and override the _compute_cost() function. After doing that the pathfinding worked as expected!

    # pathfinder.gd
    
    onready var _a_star = GridAStar.new()
    
    class GridAStar:
        extends AStar2D
    
        func _compute_cost(from_id, to_id):
            var pointA = get_point_position(from_id)
            var pointB = get_point_position(to_id)
            return floor(pointA.distance_to(pointB))

    Caching Paths

    Also having to do with the pathfinder, I made an optimization on the CharacterController class that caches all calculated paths, based on the destination point, until the character moves. This is going to be helpful because we won’t need to ask the Pathfinder to repeatedly calculate the path to the same spot from the same spot if we’ve already done it before. Additionally I’m also caching the cost associated with each calculated path for the same reason. I made a simple Dictionary object in the CharacterController for these caches, and a simple _key() function for the cache key.

    func _key(loc: Vector2, pathfinder: Pathfinder) -> String:
        return str(pathfinder.convert_to_map_point(loc))

    Stamina

    I factored in stamina to a unit’s movement. A unit is able to move up to its speed without using stamina, and then one stamina for each additional movement point. A unit only has a finite amount of stamina which they can only recover through resting or other special actions.

    showing stamina-based movement

    You may also notice the yellow/orange bars underneath the unit’s health bar. I thought it would be a good way to represent the remaining stamina points a unit has. The fun part about making the stamina bar was that I did it through a shader script! I do plan on devoting a full post to the usage of shaders sometime in the future, but the short explanation of what a shader is, for those who may be unfamiliar (like I was before doing this!), is that a shader is able to manipulate pixels in a texture in various ways.

    There’s a really helpful and interesting video on the topic of shaders in Godot on YouTube that I think covers the topic very well for beginners like me. You can find that video here.


    Path Highlighting

    Something that I find very helpful in tactical games is the indication of a route a unit will take when they move. I wanted something like that in this game, so I made it! Currently it’s very rudimentary and unpolished, but it gets the job done.

    Godot makes it very easy to draw on the screen through code. (See the docs for more info.) All I needed to do was calculate the path to the final destination through the Pathfinder and then draw a line segment from point to point until reaching the end. Simple! The tricky part was displaying an arrow to cap the line when it reached the destination. I made a simple arrow head image using the online pixel art tool Pixil, then imported it into the project. I made it white so that I could change its color to be whatever I wanted in the code, and I also made the image size much bigger than 16×16 so that I could scale it down and not have it be so hard-edged on the diagonal parts.

    I added a new node onto the Dungeon scene called PathDrawer and attached a new script to it by the same name, then surfaced two public functions: draw_path() and erase_path(), one that sets the internal _path array and one that clears it. Then I overrode the _draw() function to draw a line from point to point like I explained above, but to draw the arrow I needed to do some transformations to the image before drawing it.

    Because the default origin point of an image is the top left corner, I needed to offset the image by the full height of the texture and half of the width (since I drew the arrow from the bottom middle pointing up). I also needed to rotate it based on the direction of the final two line points and scale it by the appropriate factor so that it would be sized correctly relative to the line width and tiles. Ultimately that code came out looking like this:

    var scale = 8.0
    var scale_size = Vector2(1.0 / scale, 1.0 / scale)
    var arrow_size = arrow_point.get_size()
    
    var final_loc = _path[-1]
    
    var offset = last_dir * arrow_size
    offset += last_dir.rotated(-PI / 2.0) * arrow_size / 2.0
    offset -= last_dir * arrow_size / 2.0
    
    draw_set_transform(final_loc + offset * scale_size, last_dir.angle() + PI / 2.0, scale_size)
    
    draw_texture(arrow_point, Vector2.ZERO, path_color)

    And this was the end result. Much more clarity to movement!


    Camera

    Another thing that has been bugging me for a while is how zoomed out the scene seemed when I would play it. (You can’t tell from the gifs I post because I crop them, but the grid size was very small compared to the window size.)

    grid size compared to window size

    I definitely don’t want that to be the size of the screen when this is all playable! So I created a Camera2D node as a child to the Dungeon scene. Unfortunately the out-of-the-box camera node for Godot doesn’t come with a lot of helper functions that the camera in Unity comes with, so I had to implement the one I needed myself.

    When using a camera, you have to do a few calculations to get the point in world-space relative to the point on your screen (screen-space). Unless the camera is positioned at the origin of the world, clicking somewhere on your screen won’t translate to its point in the world.

    After some trial and error and reading up on transformation matrices, I realized all I needed was a simple calculation to convert the point in screen-space to the correct point in world-space. Fortunately we’re working with a 2D orthogonal camera, which makes the transformation much simpler. I wrote a script that extends Camera2D and added the calculation function to it. In the end, this was the whole script.

    extends Camera2D
    
    class_name DungeonCamera
    
    func screen_to_world_point(point: Vector2) -> Vector2:
        return point * zoom + position
    

    Now whenever I need to get the world-coordinates of a point on the screen I can just run that point through this function and be on my merry way!


    Unit Selection Indication

    The final thing I implemented was a way to tell if you were hovering over a unit, or if they were being targeted in some way. For this I felt like showing an outline around the character sprite would be a good solution, and figured the best way to do this would be to use another shader. However, I wasn’t sure where to start in creating a shader to do that so I did a quick search on the ol’ Googs and found this shader that did exactly what I needed! So giving credit where credit is due, thank you Juulpower for your contribution to a good cause.

    After applying this shader to unit sprites I made a few modifications to it so that I could enable and disable it at will, as well as make it flash for effect. It added the clarity I felt the game needed when indicating selection!


    Results

    Putting all of these things together, I feel like the state of our game is much more clear on what is happening! It’s still not perfect of course, but it’s progress in the right direction!

    Here’s a quick render of the state of the game as it currently stands.

    gif of the results put together

    Because I didn’t give a detailed rundown of all the individual changes I made, I recommend taking a look at the code up to this point. That can be found here. I’ll be back soon with the next part of the project: determining line of sight!

Up ↑