Home

  • 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!

  • Project Delve Part 4.2: Unit Health

    Project Delve Part 4.2: Unit Health

    For this part we’re going to implement a unit’s current health, as well as a way to lower and raise it and display it. This will act as our basic health system going forward for when units fight each other.


    Unit Health

    First of all, we know we want our units each to have their own health because we want them to be able to take damage and be defeated. We currently have a health variable defined in our UnitData, but that shouldn’t act as the unit’s current health because we’re treating it as an immutable resource (at run-time). Instead we’re going to have to create an instance variable on our Unit class that will keep track of their current health, and use UnitData.health as the “max health” instead.

    In our unit.gd script let’s create a new variable health, and initialize it in the _ready() function, and also create a take_damage() function that will reduce the health by the given amount when called, and then let’s also create a heal() function while we’re at it that does the opposite.

    var hp: int
    
    func _ready():
        self.hp = unit_data.health
    
    func take_damage(amount: int):
        self.hp -= amount
    
    func heal(amount: int):
        # We don't want to overheal!
        self.hp = [hp + amount, unit_data.health].min()

    Easy peasy! Now we just need a way to indicate what a unit’s current health is. The most common way to do that is by using a health bar, so that’s what we’ll do.


    Health Bar

    Open up the Unit scene and create a new child Node2D and name it “Health.” I’m going to position it at (-10, 6) so that the health bar will appear under the unit, but feel free to position it anywhere you want. The point that you put it at will be the top left corner of the health bar, so keep that in mind.

    Now as a child of the Health node create a ColorRect node and set the Rect.size to (20, 3). (You can also set the Control.margin for the same effect.) This is going to be the “background” of the health bar, so I’m going to make it a neutral gray color #414141 (rgb(65, 65, 65)).

    Then as a child of the ColorRect node we just created, create another ColorRect and give it the same with as the parent, but one less point vertically, so it will be (20, 2). This ColorRect will be the color of our health bar, so I’m going to set it to a green color for right now.

    When you’re all finished you should have something that looks similar to this.

    health bar example

    Coding the bar

    In order to update this health bar when the unit’s current health changes we’ll need to make a simple script to control the bar color and bar width. Let’s create a new script called hp_bar.gd and assign it to the Health node, then open it in your editor of choice.

    The first things we’ll want a reference to are the two ColorRects we attached as children to the Health node, so let’s get those references with the onready keyword.

    # I named the background bar "HpBar" and the color bar "HpColor"
    # for easy reference
    onready var _hp_bar = $HpBar
    onready var _hp_color = $HpBar/HpColor

    Personally I like the health bar to have 3 distinct colors, one for when the unit is near or at full health, one for around 50% health, and another for when the unit is close to that deep dark sleep from whence no traveler returns… So I’m going to create three exported Color variables in my script that will represent these states respectively. (These colors were ones I liked when tweaking the color wheel)

    export(Color) var healthy := Color("#20ea40")
    export(Color) var wounded := Color("#eae220")
    export(Color) var critical := Color("#e74322")

    The last thing we’ll need to know is how full we should display the health bar. For that we’ll need the ratio of what the current health value is over what the maximum health value is. Once we calculate that all we need to do is set the X scale of the background bar to that value.

    So let’s do that in the code! Make a function in the script and name it something like _update_hp_bar(). This is the function that we’ll run whenever the current_hp or max_hp changes. In order to make the code more readable I’m also going to create two more functions that will handle the change to both color and scale separately: _calculate_color() and _calculate_scale().

    Calculating the color

    Because we’ve identified three separate colors we want the health bar’s color to swap between, we’re going to need to set cutoff points for where those colors should be activated. As far as the actual color that’s displayed is concerned, we can go about it in two ways:

    1. Lerp (linearly-interpolate) between the colors depending on the current percent health
    2. Just set the color at the corresponding cutoff point

    Method #2 is much more straightforward but I like the look of the health bar gradually changing color instead of abruptly changing color, so we’re going to implement the first method.

    # The cutoff values can be whatever value you decide between 0 and 1,
    # mine are .95, .5, and .2
    func _calculate_color():
        var percent := _get_hp_percent()
        var cutoff_diff: float
    
        if percent >= HEALTHY_CUTOFF:
            _hp_color.color = healthy
        elif percent < CRITICAL_CUTOFF:
            _hp_color.color = critical
        elif percent >= WOUNDED_CUTOFF:
            cutoff_diff = HEALTHY_CUTOFF - WOUNDED_CUTOFF
    
            # This line means: "depending on the value given, return a color
            # between 'wounded' and 'healthy' where 1.0 == 'healthy' and 0.0 == 'wounded'"
            _hp_color.color = wounded.linear_interpolate(healthy, (percent - WOUNDED_CUTOFF) / cutoff_diff)
        else:
            cutoff_diff = WOUNDED_CUTOFF - CRITICAL_CUTOFF
            _hp_color.color = critical.linear_interpolate(wounded, (percent - CRITICAL_CUTOFF) / cutoff_diff)

    The _calculate_scale() function is much more straightforward — all we have to do is set the scale to the result of _get_hp_percent()!

    func _calculate_scale():
        _hp_bar.rect_scale.x = _get_hp_percent()
    
    func _get_hp_percent():
        return float(current_hp) / float(max_hp)

    Verifying

    In order to test that our script behaves how we want without needing to run the scene, guess what we’re going to do?? That’s right, say it with me now! Make a tool script! Well, we’ll actually be updating our hp_bar.gd script to a tool script.

    Once we’ve done that, let’s also add a setter method using setget for all of our exported variables so that we can see updates in real-time when changing these values. Here are what all of those functions will look like:

    func _set_max_hp(newVal):
        max_hp = newVal
        if max_hp < current_hp:
            current_hp = max_hp
        _update_hp_bar()
    
    func _set_hp(newVal):
        if newVal > max_hp:
            newVal = max_hp
        current_hp = newVal
        _update_hp_bar()
    
    func _set_healthy_color(newVal):
        healthy = newVal
        _calculate_color()
    
    func _set_wounded_color(newVal):
        wounded = newVal
        _calculate_color()
    
    func _set_critical_color(newVal):
        critical = newVal
        _calculate_color()

    Also, because we set our _hp_bar and _hp_color variables as onready variables, we need to check in our _calculate_color() function whether or not it has been set, and if not, to set it.

    func _calculate_color():
        # I ran into some weird errors when running this code but this
        # check seemed to fix it
        if not is_inside_tree():
            return
        if not _hp_color:
            _hp_color = $HpBar/HpColor
    
    # ...
    
    func _calculate_scale():
        if not is_inside_tree():
            return
        if not _hp_bar:
            _hp_bar = $HpBar

    And that’s it! When you return to the editor you should be able to see your changes happen in real-time!


    Syncing With The Unit

    Now that we have a working health bar, let’s make it work in tandem with the unit.

    The simple way to do this would be to get a reference to the Health node from our unit.gd script and just update the Health.current_hp whenever the unit’s HP changes, but that creates redundancy (having two variables representing the same thing) and coupling (adding an explicit dependency to Unit means the unit.gd script can’t run in isolation without modification). Instead we’re going to follow the Godot pattern of utilizing signals.

    First we’ll get rid of the current_hp and max_hp variables inside the hp_bar.gd script. That script will not store any outward-facing state besides what it has direct control over (the colors). Instead we’ll create a listener method in the script called _on_hp_changed(), which we will connect to a signal from the Unit it’s attached to, as well as create a private variable _percent to keep reference of the percent to show.

    # hp_bar.gd
    
    var _percent := 1.0
    
    func _on_hp_changed(newHp, maxHp):
        if maxHp == null:
            _percent = 1.0
        else:
            _percent = float(newHp) / float(maxHp)
        _update_hp_bar()
    
    # Also update all references in this script from the old _get_hp_percent()
    # to reference _percent instead

    Inside the unit.gd script let’s define a custom signal called hp_changed.

    # Emits with parameters: newHp, maxHp?
    signal hp_changed

    And let’s also make a setter function that emits hp_changed whenever the unit’s hp… changes.

    var hp: int setget _update_hp
    
    # ...
    
    func _update_hp(newValue):
        if newValue == null or newValue == hp:
            return
        hp = newValue
        if unit_data:
            emit_signal("hp_changed", newValue, unit_data.health)
        else:
            emit_signal("hp_changed", newValue, null)

    Connecting the signal

    Now that all of the code is set up we can go back to the editor and connect the hp_changed signal to the _on_hp_changed() function on our Health node.

    To connect a signal in the editor, click on the node that emits the signal you want to listen for, click on the “Node” tab (to the right of the “Inspector” tab), find the signal you’re looking for in the list of signals displayed there, and double-click on it. This will bring up a dialog that looks like the following image. Select the Health node and change the “Receiver Method” to “_on_hp_changed” and click “Connect.”


    Back in the MovementDemo scene we can verify if all of our code is put together correctly. Playing the scene you should see that pressing the “Attack” button and then clicking on a unit will reduce its health by however much you specified in the unit_actions.gd script.

    The second unit takes only 1 damage at a time because its defense is 1, whereas the first unit has a defense of 0 and takes 2 damage.

    And there we go! Two actions down! I probably won’t be writing a post or two for each separate action option from here on out, but I will give a summary of what I did.

    As promised, the code for this post (and the previous two posts) can be found here.

  • Project Delve Part 4.1: Unit Selection

    Project Delve Part 4.1: Unit Selection

    In this post I’m going to walk through a simple solution I made for being able to select units, as well as more application of the UnitActions script we started last time.

    Node2D does not have a signal emitted when it is clicked. This is the primary problem that we want to solve for our units, since our UnitActions singleton will want to know when a unit has been selected. This would be easy enough if we only ever wanted units to emit a “clicked” signal, since we could just check the _input() method inside our Unit class. But since we’re going to have a lot of interactable things in our game, I thought it best to find a solution that could be reused for multiple different objects.

    The thing I think will work best for this particular problem is making a parent class that any script we choose can extend. In other words, we’re going to use inheritance.


    Making The Listener

    Go ahead and create a new script and have it inherit from Node2D. You can name it something that describes its intention, which in our case is going to be detecting mouse input within a certain area (I’m naming mine mouse_listener.gd). In order to set the size of the relevant area, we’re going to need an export statement with the width and height. We could achieve this by using two separate variables, width and height, but a Vector2 will work just as well for us, so let’s use that.

    export(Vector2) var bounds := Vector2.ZERO

    This next part is optional, but I think it could come in handy at some point in the future so I’m going to include it. Say we want to only detect a mouse click when the right or middle mouse button is clicked. How can we control that? The solution most (if not all) game engines use is what’s known as a bit mask, and Godot provides a simple way to create one from the editor via an exported variable.

    We can declare a bitmask variable by adding the FLAGS hint to the export keyword. (“Hints” are what Godot calls declaring a type on the export, as a broad definition.)

    export(int, FLAGS, "left", "middle", "right") var button_mask := 1

    When you save the script and look in the editor, you’ll see the following.

    Detecting the input

    Now that we have the bounds and button_mask set, let’s use them to determine if a click event happened on our listener object. For this we can override the _unhandled_input() function (we’re using _unhandled_input() instead of _input() because we don’t want this to override any input that may have already been handled).

    func _unhandled_input(event):
        # Using a match statement here because we'll be listening for
        # more than just the mouse button event eventually
        match event.get_class():
            "InputEventMouseButton":
                pass

    Now the things we want to know are whether the click event just happened (we don’t want to trigger this event for every frame that the button is held down) which buttons were pressed, and where it occurred. For this we can use the event.is_action_pressed() function, the event.button_mask variable, and the event.position variable.

    To check if the button pressed is one of the buttons allowed by our button_mask variable, we just need to check whether the AND bitwise operation between the two is greater than 0.

    var is_valid_button = event.button_mask & button_mask > 0

    To check if the button was just barely pressed, we’ll need to create a new action in the Input Map of our project. To do this go to Project > Project settings… and click on the Input Map tab. From there, add a new action and assign three buttons to it: Left mouse button, Middle mouse button, and Right mouse button. When finished it should look similar to this.

    And in the _unhandled_input() function we can add this line.

    var just_pressed = event.is_action_pressed("mouse_click")

    Finally we need to check whether the event happened within the bounds that we defined. To keep things simple I decided that we would center the bounds on the Node itself and not deal with offsets (other than the offset to center the bounds). So to make those calculations we can do the following.

    func _get_offset_bounds():
        # I renamed bounds to size here since it makes
        # more sense in this context
        var size = bounds
        return Rect2(position - size / 2, size)
    
    func _within_bounds(position: Vector2):
        return _get_offset_bounds().has_point(position)

    After doing all this we’ll know whether we should emit an event on a mouse click. Our script should look something like this when we put it all together.

    extends Node2D
    
    class_name MouseListener
    
    signal clicked
    
    export(Vector2) var bounds := Vector2.ZERO
    export(int, FLAGS, "left", "middle", "right") var button_mask := 1
    
    func _unhandled_input(event):
        match event.get_class():
            "InputEventMouseButton":
               var just_pressed = event.is_action_pressed("mouse_click")
               var is_valid_button = event.button_mask & button_mask > 0
               if just_pressed and is_valid_button and _within_bounds(event.position):
                   emit_signal("clicked", event)
    
    func _get_offset_bounds():
        var size = bounds
        return Rect2(position - size / 2, size)
    
    func _within_bounds(position: Vector2):
        return _get_offset_bounds().has_point(position)

    Visualizing the bounds

    This is all well and good, but how can we tell where the bounds extend to? Fortunately Godot makes it easy to make our scripts runnable in the editor via the tool keyword (which we did in an earlier post).

    Once we add tool to the top of the script, let’s also create a setter function for our bounds variable so that whenever it changes, our setter function will run. In our setter function we’ll call update() which tells Godot that this node needs to be redrawn.

    export(Vector2) var bounds := Vector2.ZERO setget _set_bounds
    
    func _set_bounds(newVal):
        bounds = newVal
        update()

    Then we can override the _draw() function to easily draw to the screen.

    func _draw():
        # This tells Godot to only run this part in the editor view
        if Engine.editor_hint:
            var draw_bounds = _get_offset_bounds()
            var color = Color("#e74322")
            draw_rect(Rect2(-draw_bounds.size / 2, draw_bounds.size), color, false, 1.5)

    Now when you adjust the bounds of the script, you’ll see the area update in real time!


    Using The Listener

    Now that we’ve got the basic functionality down for our MouseListener, let’s change our unit.gd script to extend MouseListener instead of Node2D like it was before. Also, if we want to visualize the bounds of the unit we’ll need to add the tool keyword to this script too.

    Now back in the UnitActions singleton we worked on last time, we’re going to create that function I said not to worry about at the time: _wait_until_unit_selected().

    The first thing we want to do in the function is get a list of all the units in the scene and connect their clicked signal to a function. We can get all the units in the current scene the same way we got the list of our debug buttons by adding the units to their own group. Once the function has been connected to all of the units we’ll yield for a custom signal we define called unit_selected. When the unit has been selected we’ll disconnect all the units from listening for the clicked signal, and emit the unit_selected signal with the selected unit. That signal will resume our code at the yield statement and we’ll finally return the unit. (Whew!)

    Here’s what that will all look like.

    signal unit_selected
    
    func _wait_until_unit_selected():
        print("Waiting for unit selection...")
    
        var units = _get_all_units()
        for unit in units:
            unit.connect("clicked", self, "_select_unit", [unit])
    
        var unit = yield(self, "unit_selected")
        return unit
    
    func _select_unit(event, unit):
        var units = _get_all_units()
        for unit in units:
            unit.disconnect("clicked", self, "_select_unit")
    
        print("Unit selected: " + unit.name)
        emit_signal("unit_selected", unit)
    
    func _get_all_units():
        # All units in the scene should belong to the "units" group
        return get_tree().get_nodes_in_group("units")

    In the next post I’ll be guiding us through adding basic functionality to the “Attack” action. Like last time I don’t have a link to the code for this partial section, but I will provide it in the next post.

  • Project Delve Part 4: Define Actions

    Project Delve Part 4: Define Actions

    This part will likely be broken up into a few posts, since there’s a lot to cover here. We’ll also be revisiting the code we write during this part fairly often in the future as we continue to expand the game functionality.


    Setting Up The Interface

    The actions we’ll be defining in this post are ones that can be performed by both Heroes and Monsters and are the most basic actions of the game. We’ve already covered one of these exclusively, so we’ll be focusing on another a little more in depth today: attacking. Now, the basic implementation that we come up with today will not be the final version of how combat will work, but it’ll be enough to solidify the concept.

    Up to this point we haven’t bothered with any UI elements but we’re going to set up some quick and dirty buttons for development purposes.

    Start by opening up the MovementDemo scene that we’ve been working with and create a new Button node as a child of the scene. Set the button text to “Move” and position it somewhere in the scene (preferably not over our little game map). Now we’re going to add more buttons by duplicating the first (ctrl + d on Windows), each of them corresponding to an action a Hero can take. The different action types can be found on Page 7 of the Official Rulebook for Descent, though I’m grouping a few of the actions together under the generic label of “Interact.” When you’re finished you should have a group of buttons that look something like this.

    You can disable all of the buttons that aren’t either the “Move” or “Attack” button through the inspector since we won’t be working with them at this point.


    Action Handler

    Let’s create a script to have these buttons do something when they’re clicked. Unlike all the other scripts we’ve created up to this point though, this script won’t be attached to a node in the scene (though it still needs to extend Node). Instead we’re going to make this an AutoLoad script, which is Godot’s name for singletons. We want this script to be a singleton because it’s going to need to know about a lot of the different systems we’ll have in our game in order to properly handle actions. I anticipate this script is going to change a lot over the course of development so there’s no need to spend time polishing it just yet.

    After you create the script (I called mine unit_actions.gd) open the Project Settings and click on the “AutoLoad” tab. From there you can open the script you just created and click “Add,” then make sure it’s set to the “Enabled” status.

    Once you’ve added your script, as long as it’s enabled, you’ll be able to access it from any other script as though it were a singleton instance through the name that you gave it (in the Name column).

    We’re going to want a reference to the current Dungeon in our singleton script for various things we want to do, so let’s add the following to the script:

    # We're not setting the type in the variable declaration because
    # it will create a circular dependency when we reference it 
    # within the Dungeon script
    var _active_dungeon
    
    func set_active_dungeon(dungeon):
        _active_dungeon = dungeon

    Then in the dungeon.gd script we’re going to have the Dungeon register itself when it’s ready.

    func _ready():
        # ... Existing code ...
        UnitActions.set_active_dungeon(self)

    That way the singleton script doesn’t need to worry about finding the dungeon within the scene and can just expect the variable to be initialized by the time it’s needed.

    Back in the unit_actions.gd script let’s create an enum to keep track of all the potential actions a unit can take. (Hint, it’s going to be the same as the buttons we made earlier!)

    enum Action {
        move,
        attack,
        rest,
        stand_up,
        interact,
        special,
    }

    Now in order to hook the buttons in our scene up to the singleton we can’t use the editor. This is because the AutoLoad scripts don’t appear in the scene tree view and are only added when the scene runs. Instead we’re going to poll for all of our buttons inside the _ready() method of the singleton class, which will run on scene start. To make it easy to find these buttons, we can use the Godot feature of Groups.

    I like to think of a Group as a Tag. Every Node in the scene can belong to any number of Groups, and Groups can have any number of Nodes within them. When you add a Node to a Group you’re basically tagging the element for easy lookup later. If we assign all of the buttons that we created to the same Group, then all we need to do when we want to get a button in that group is call get_nodes_in_group() on the SceneTree.

    So let’s add a _ready() method to our singleton script, poll for the buttons we created, and connect them to a method we’ll create soon.

    func _ready():
        # I named the group that I assigned all of the buttons to "debug_buttons"
        var debug_buttons = get_tree().get_nodes_in_group("debug_buttons")
        for btn in debug_buttons:
            btn.connect("pressed", self, "_on_debug_button_clicked", [btn.text.to_lower()])

    Now the function we’re connecting to will look like this:

    func _on_debug_button_clicked(action_txt):
        match action_txt:
            "move":
                print("Selected move action")
            "attack":
                print("Selected attack action")
            "rest":
                print("Selected rest action")
            "stand up":
                print("Selected stand up action")
            "interact":
                print("Selected interact action")
            "special":
                print("Selected special action")
            _:
                return

    Now when you run the scene you’ll see the message related to which button you pressed pop up in the console! Bingo bango bongo, buttons connected!


    Basic Actions

    We’re going to take the CharacterController.move_to() command from the Dungeon class and put it into the AutoLoaded script. When the “Move” button is clicked, now the UnitActions will handle it. But now that we could potentially be dealing with multiple units at once we can’t be sure of which unit should be the one to move, so we need to select the unit before selecting the destination tile. Of course once we get further along in the project we probably won’t have to resort to clicking on a unit and then clicking on the destination, but for now it’s what we’ll do.

    Let’s update the dungeon.gd‘s _input() method to emit a signal when a tile is clicked. That way we can listen for it within the UnitActions script and respond appropriately.

    signal grid_tile_clicked
    
    func _input(event):
        if event.is_class("InputEventMouseButton") and (event as InputEventMouseButton).is_pressed():
            # We're sending the event along with the dungeon's pathfinder
            # so that listening methods will be able to use that information
            emit_signal("grid_tile_clicked", event, _pathfinder)

    And back in the unit_actions.gd script let’s create that “Move” action function.

    func do_move_action():
        # Don't worry about the specifics of this line right now.
        # You can hard-code in the unit if you want to test it out
        # right away.
        var unit = yield(_wait_until_unit_selected(), "completed")
        print("Waiting for destination selection...")
    
        var completed := false
        while not completed:
            # This listens for the grid_tile_clicked signal we just defined
            var signal_args = yield(_active_dungeon, "grid_tile_clicked")
            var event = signal_args[0]
            var pathfinder = signal_args[1]
    
            if unit.can_move_to(event.position, pathfinder):
                print("Moving to: " + str(event.position))
                yield(unit.move_to(event.position, pathfinder), "completed")
                completed = true
            else:
                print("Not a valid tile selection!")

    And while we’re at it let’s also create a simple “Attack” action function.

    func do_attack_action():
        # Again, ignore the _wait_until_unit_selected() function for now
        var unit = yield(_wait_until_unit_selected(), "completed")
        var dmg = unit.take_damage(2)
        print(unit.name + " took " + str(dmg) + " damage")

    We’re not through with part 4 quite yet, but I think we’ve made some good progress. I don’t have a link to the code we specifically created in this post since I’m breaking it all up and some of the code might look strange without context, but I do have a teaser gif of what the results will look like after my next post!

  • Project Delve Part 3: Unit Stats

    Project Delve Part 3: Unit Stats

    Now that we have some pathfinding down, the next order of business is to limit movement even more. (Aren’t rules the best?!) Except in rare instances, we want our characters to be constrained by the number of tiles they’re allowed to move on their turn. Right now units have an infinite movement range (so long as a path to the target exists), so we’re going to change that.

    The simplest way to add a movement restriction to a unit would be to create another exported variable and use that. However, this is another opportunity to explore more features of Godot and introduce another equally viable solution (and in some cases even more desirable) to this problem. Before going on, let me back up a bit…


    Super Relevant Background

    Every meaningful character in any RPG ever made has one or more “stats.” Stats are things like: the amount of damage a thing can take before being defeated, how fast something moves, how strong something is, etc. Every character will probably have slightly different stats from every other character, which introduces an opportunity for strategy. Some characters will be inherently better at doing some things than others, and choosing a team/party of characters that balance out one another’s weaknesses while complimenting their strengths is vital to any game with strategic elements.

    If we abstract the concept of strengths and weaknesses we can easily quantify them. Games have been doing this for years, and we’re jumping on the bandwagon!


    Adding Stats

    Now back to movement restriction. One of the stats we want our units to have (both friendly and not) is a speed stat. This will determine how far a unit will be able to move for a single action, in terms of tiles (diagonal included). But like I mentioned before, there’s a better way to handle this than using a script variable.

    In Unity there’s a concept of Scriptable Objects. These are essentially just chunks of custom data that can be used anywhere you like within your scripts. They’re meant to be easily modifiable and reproduceable and thus are excellent solutions for when you want to inject data into a script without needing to code anything (after the initial setup, anyway).

    In Godot, this idea is known as Resources, and we’re going to create a resource script to contain the stat data of every character in the game! …Not right now of course, but eventually that’s the goal.

    For now we’re going to start with a single piece of custom data: speed.

    In the Godot FileSystem tab, right click and select “New Script…” and name it something like unit_stats.gd. Have it inherit from Resource and open it up. We’re going to add only one line to the file and it’s going to look a lot like what we’ve done before, so no new ground here.

    extends Resource
    
    # Okay, I lied. You can also optionally add this line.
    class_name UnitStats
    
    export(int) var speed

    We’ll also need to add an _init() function. This function is necessary for Resource scripts to initialize their data with default values.

    func _init(default_speed = 1):
        speed = default_speed

    Edit: I just learned that the _init() function isn’t strictly necessary if you can give your exported values initial constants inline. See the “Bonus Content” section at the bottom.

    Now let’s go back to the FileSystem tab in the editor and right click again, but this time select “New Resource…” You can scroll through the long list of possible resources, or you can use the search bar at the top to search for your new unit_stats.gd script. Because we marked it as extending Resource, it will show up! Click “Create” and you’ll see the resource file pop up in the FileSystem tab. When you double-click on it, it will be brought up in the inspector and you should see the following.

    Our speed variable shows up just like it would on a regular Node script! Feel free to edit the value to whatever you’d like (a number greater than zero, preferably).

    Now how do we let our CharacterController know about this new value? Well we can just make an exported variable at the top of the character_controller.gd script for it to be assigned in the editor.

    export(Resource) var unit_stats

    and we can access it like so

    func a_function():
        go_fast(unit_stats.speed)

    Now, I can hear you saying, “why did we go through that extra step if we were just going to end up adding an exported variable to the CharacterController anyway?” Well that’s an excellent question. If all we planned on doing was adding a single variable then I would agree with you — this method does seem like overkill. However, this new UnitStats class will become the key to managing all the individual stats of any and every character we plan on adding to the game. If we decided not to use a Resource file for that kind of stuff, we would need to make a new Node for every character we wanted to add, as well as modify the variables on the specific Node the character_controller.gd script was attached to, which would quickly become cumbersome. This way we can decouple the functionality from the data, and be free to use either without a reliance on the other.

    When all’s said and done of course, some might argue that it’s still not be the best solution for this problem. To that person I would say, “you make a valid point,” and then continue to do things my way because this is my tutorial and I say we’re using it!


    Making Use Of It

    Now that we have reference to a speed stat for our unit, we need to be able to know if a move from tile A to tile B is legal, based on the unit’s speed. For this, let’s add a new function in the Pathfinder class called path_cost(). It will take a start position and end position and return an integer representing the total movement cost of moving between the two points.

    Unfortunately the AStar class doesn’t provide that functionality out of the box, so we’re going to need to do it ourselves.

    We’ll start the function in the same way we started the find_path() method.

    func path_cost(start: Vector2, end: Vector2):
        # We need to transform the variables to be in tile-coordinate space
        start = _tilemap.world_to_map(start)
        end = _tilemap.world_to_map(end)

    Because AStar references points by ID (which we called “index” in a previous post), we also need to get the ID of the start and end point.

        var start_point_index = _get_point_index(start)
        var end_point_index = _get_point_index(end)

    The other two bits of information we need to know about AStar is that it has two methods we haven’t used yet: get_id_path() and get_point_weight_scale(). The first method behaves exactly like the get_point_path() method in our find_path() function, except that it returns the IDs of the path points instead of the coordinates of the path points. With these IDs, all we need to do is sum up the total value of calling get_point_weight_scale() for each ID and return that.

        var id_path = a_star.get_id_path(start_point_index, end_point_index)
    
        var cost := 0
        for point_id in id_path:
            cost += a_star.get_point_weight_scale(point_id)
    
        return cost

    Now we can check whether the move is valid when moving the unit based on the unit’s speed! We just need to call pathfinder.path_cost(start, end) and compare it with unit_stats.speed. If the cost of the path is greater than the unit’s speed then we won’t move.

    This code begs for optimization since we’re essentially calculating the same path twice if the move is within the unit’s speed. However, this works for demonstration purposes and I may go back at some point to tidy it up a bit.

    Feel free to add any extra stats you want to keep track of on a unit-to-unit basis in the unit_stats.gd script. I’m also going to be adding health, attack, and defense. (I’m expecting attack and defense to be something a little more complex in a few lessons but for now an int value will be fine)


    Bonus Content: Unit Types

    I went back and forth on whether to make this section its own post or not, but ultimately I think it fits best along with this post.

    In Descent there are several different categories of units. When I refer to a unit type (at least in this section) I’m not referring to any specific monster or hero class. Unit types are going to be defined by us as a group of units that activate together. What do I mean when I say they activate together? Well, we’ll get more into detail further along but if you haven’t played Descent before or read the rules (or even D&D), here’s the gist:

    Rounds are turn-based and broken into two phases. Each hero takes their turn individually, in any order. After all of the heroes have had their turn (known as the hero phase), the monsters are activated (the monster phase). However, monsters can only activate on a group-by-group basis (known as a monster group). When a monster group activates, each monster of the same “species” performs their turns in any order, just like how the heroes did. When all monsters in the same group have finished their turns, it moves on to the next monster group (again, in any order). Once all monster groups have activated, the round ends and the process starts again.

    In this lesson we’re going to be defining these unit types and structuring the code in a way that makes it easy for us to add new heroes and monsters quickly in the future!


    The Good Stuff

    First off, I want to rename the unit_stats.gd script. Stats are not the only unique thing about units; each unit will also need other data to be fully complete like sprites and special abilities. These other traits don’t really fit well under the idea of “stats,” which I think of as exclusively numbers. The resource files we create need to be more encompassing than just a unit’s stats. So let’s change the name of the file to unit_data.gd and update the class_name to UnitData.

    Now we’re going to add quite a few more attributes to this resource file, so feel free to copy/paste this part.

    export(Texture) var sprite = null
    export(Vector2) var size := Vector2.ONE
    
    export(int) var health := 0
    export(int) var speed := 0
    export(int) var defense := 0  # will change type eventually
    export(int) var attack := 0  # will change type eventually
    
    export(int, -1, 5) var strength := -1
    export(int, -1, 5) var insight := -1
    export(int, -1, 5) var perception := -1
    export(int, -1, 5) var knowledge := -1
    export(int) var stamina := -1

    Another thing I want to do is make this file a tool file. What that means is Godot will recognize that this is a script that should be run within the editor itself. The reason I want to do this is twofold: first, I wanted to try it out. And second, I want whenever I set the sprite in the UnitData resource for it to update the sprite in the scene it’s attached to.

    To make this script a tool script, all we need to do is add that keyword as the first line of the script.

    tool
    extends Resource
    
    ...

    Now we need to add the setget keyword after we declare the sprite export.

    export(Texture) var sprite = null setget _set_sprite

    Now we need to define the _set_sprite() function elsewhere in the script. I’ll add it just below the wall of exported variables.

    func _set_sprite(new_sprite):
        sprite = new_sprite
        var scene = get_local_scene()
        if scene != null and scene.has_node("Sprite"):
            var sprite_node = scene.get_node("Sprite")
            sprite_node.texture = new_sprite

    In order for the get_local_scene() function to work properly, you’ll need to check the “Local to scene” property in the editor on the resource. If you’ve got it all set up right, you should see something like the following when you change the sprite on the UnitData resource.

    Lastly we’re going to create one more script and call it monster_group_data.gd, and have it extend Resource as well. Inside this script we’re going to export two variables.

    export(Resource) var elite_data
    export(Resource) var minion_data

    In each monster group there will normally be one elite monster who is stronger, and multiple minion monsters that make up the bulk of the group and are weaker. For each monster group we create, we want to have the data to create both the elites and the minions together in the same place.

    The code for this post can be found here.

Up ↑