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!

Leave a Reply

Up ↑