Wading into Godot - Episode 2

Wading into Godot - Episode 2

To recap - I am taking some time in-between gigs to learn about Godot, a 2D/3D game engine. My eventual intention is to use Godot to create some gamification of some coaching and operational practices to help people learn how to execute and improve planning engagement and collaboration. This is my second article.

Here is a link to the Godot home page.

Here is a link to Episode 1.

So far I have managed to create the basic structure of the 'Agileopardy' board (seen above) in a Godot components. Now it is time to handle player interactions and figure out how to manage the game state. This is what I am going to try to accomplish in this episode:

  • Build some click interactivity into the board so we can select questions. One of the good things about the TileMap component is it has support for translating mouse coordinates into tile coordinates, as we will see.

  • Build a state machine for the overall state of the game. This is a common need in games, so I assume there will be good support for this. We will use this state to keep track of the board and the score and the steps the player is taking in the game.

How do we get input to our component?

Instead of creating a hook, or some other sort of injection, Godot components expect their implementors to override some function to interact with the world. If you were to look in the documentation of TileMap, you may not see some of the functions that you need. To see them, we must look at the parent class chain. TileMap's ancestry looks like this:

Node2D < CanvasItem < Node < Object

This means that TileMap is a more specific implementation of several more generic classes. TileMap has the 'is-a' relationship with all of these parent types - this means it is a Node2D object, a CanvasItem object, etc., and can use the functions implemented on those classes.

TileMap's immediate parent class is Node2D. This class represents all Godot node components that are used in a 2D space. Each of these has its own individual space in a hierarchical 2D scene. We have to go up the inheritance chain just a bit to get to the input function.

The _input() function we have to override lives in the Node class, which is the class of all things that can live in a node hierarchy. It must abide be certain rules that are enforced by the class code. For example, nodes can have only one parent node. Any other arrangement would break the strict pattern that this hierarchy relies on for its behavior to be correct.

The parent of the Node class is the Object class, the root node (we can say) of all class hierarchies in Godot from which all classes ultimately inherit from. It contains very basic code to handle properties, signals and notifications. It also has the function to_string(), which provides a way for all Godot objects to identify themselves with a string representation.

Writing the input code

This code goes in our extension of the TileMap class. We override the _input ( InputEvent event ) virtual function to get a crack at processing any of the events that the TileMap handles. The argument to the method is an InputEvent, which means it could be an object of any class that inherits from InputEvent. The event we need to look for is of type InputEventMouseButton, which has InputEvent in its inheritance hierarchy. We ignore all events that are not of this type. The position property lives on the InputEventMouse superclass and tells us where the click occurred on the screen:

Getting a left mouse button click event and its location

We use some other methods if TileMap to figure out which map cell was clicked. The Node2D parent class has this method to convert the click coordinates to coordinates local to the game screen:

Translate click coordinates to local

The TileMap class that we are inheriting from provides a method for converting local coordinates to a particular tile in the map by returning a vector of two integers:

Get map position of click as a vector

We will get a 2D vector with the position if we add this code:

Acquiring a vector that identifies the clicked tile

And the result gives us the vector location of the tile in the map that we clicked on. We don't have to do anything to give control back to the application once we are done processing the event. We also can totally ignore everything in the first row (the 'category' cards) since we want players to click only on the question tiles. The code for the entire input handler looks like this:

Initial implementation of the input handler

This accomplishes one of the two things I wanted to achieve. In the next episode we can work on flipping the tiles that we click on and displaying the questions.

State machines in Godot applications

I started here at the official documentation for Godot finite state machines. I was a bit surprised that state handling was not supported by a base class that I could easily inherit from. I guess that without Generics it would not be very easy to create an adaptable base class. I shall persevere by going back to basics and looking at the design pattern documentation for a 'state' and re-visiting the basics of implementing a finite-state machine.

A simple finite-state machine is an object that maintains a deterministic state, controls how states are allowed to change, and provides notification when it does. For our purposes, we can keep the states themselves as members of an enum and not get into complex states with nested information.

Here are the specifications for the state machine I want to build:

  • States will be: START, PLAY, QUESTION, SCORE and END (for now). START will signal that the game has been reset, PLAY that the game is running. The QUESTION state will occur when a question is selected and SCORE state will mean the score has been updated (i.e., a question has been answered) and the END state will signal an end of the game and the final state for the machine. They will be in an enum called GameStates.

  • The machine will have a set_state(GameState state) function that will allow a state change to any other state. We can deal with building a directed graph later if we need it.

  • The machine will signal state_changed(GameState state) when set_state(...) is called to report the state change to connected listeners.

Here is the code I created for the GameState class and the enum that represents the discrete states that the machine can be in:

We declare the user-define signal that this Node-based object will fire and the parameters that will come with the notification. I chose to include the previous state and the new state as parameters to the signal. Here is also the variable that will contain the current state of the machine:

The last bit of the state machine is the management of the state property. The method that 'gets' the value and returns it to the caller is called an 'accessor' and the method that updates the value is the 'mutator'. The accessor is given a hint that the type being expected is on of the state enum values in GameStates.

The mutator takes a bit more code. First we check if the incoming state is the same as the current state. If so, it is a 'no-op' and we exit the function. If the new state is different, we save it to the property variable and emit the signal that the state has changed.

Connecting back to the game

Since we created the GameState to be a child node of the game board (encapsulated in the TileMap), there is already a relationship between the board and the state machine.

We can acquire and hold on to a reference to the GameState by traversing the child nodes of the TileMap. We can override the _ready() function to capture the reference as the node hierarchy is established:

Admittedly, this may not be the best way to do this. I would rather not have to hardcode the node name to get the state machine.

The setup_board() method is called in the _ready() function to create the entries in the TileMap that build out the board. Each column is the same, with the 'category' header at the top and the prize values extending below.

Tying it all together

We can arrange a test to see that our new state machine is behaving correctly. We can use the Godot UI to directly connect the TileMap to the GameState node to listen to our custom state_changed signal. When we do this, the UI will generate a function and insert it into the code that will listen for the signal and its parameters.

The generated method looks like this when we add code to print out the parameters:

I will use the mechanism that we have been using to capture mouse clicks and leverage that. This code will flip the GameState from GameState.START to GameState.END and print out the new state.

When we run our game and click a few times on the board, we can see the state toggle four times in the printed messages:

Wrapping up

I am going to accomplish these next tasks for our next episode:

  • Creating a bank of questions and answers for each value in some human-readable format. MVP for this will just be a single question for each cell.

  • Add the current score and breadcrumbs to the UI. This will allow us to not just show the current score, but also maintain a list of the previously answered questions in the round.

  • Add the logic that presents the questions (maybe with some animation) and checks the answers.

See you for episode three!

To view or add a comment, sign in

Others also viewed

Explore topics