Building the Board: A Technical Overview (so far)


In keeping with my last post, here's a kind of "backlog post" about some of the technical aspects of this project and how I've been going about making this game using Godot. So be warned this might be a bit more technical, but I hope to also pepper in some design-y stuff as well.

To kick things off I have to give a huge shout out to Rodrigo Maureira Contreras, the maker of the Godot Tactical RPG demo. Looking at this project, exploring the code, and seeing how he went about creating the gameboard and implementing utility handlers was FUNDAMENTAL for me tackling this project idea in Godot 4. I'm super thankful for his openness and willingness to share his project, code, and demo.

So my initial hurdle was trying to figure out the updates and changes to the Nav2D/3D nodes in gdscript 2.0. I found that the initial documentation for the changes to these nodes, and in particular how they handle the A* algorithm, to be a bit above my current dev experience. Although I went through the very well documented GDQuest tutorial on making a Fire Emblem-esque 2D tactical game, I felt like the workflow was clunky and my limited knowledge of the new tiling system in Godot 4.0 was also dragging me down. So I opted for a more direct, if maybe less technically sophisticated approach.

I decided to adopt Rodrigo's approach to board-creation and make thing in 3D. This involves creating 3D models where the top faces have been ripped from the base, separated from each other, and turned into separate "tile" objects that can be "batch processed" in Godot. By this, I mean that you take the 3D model and go through each individual tile objects and assign it important data and characteristics that then get "recompiled" back into a singular board parent object. Without going into too much detail, this process of doing this challenged a lot of my gdscript coding experience because I had to figure out how I wanted to adopt Rodrigo's method while also adding additional tile information not present in his demo (things like obstacle tiles, "pass" tiles, and other data based on player input).

The main aspect of what the recompiling script performs is going through all the tiles in a parent object and assigning each object a trimesh-collider that then can be "read" by the player and enemy raycasters to highlight tiles to move and perform actions. Then the code sets that collider as the parent node with the mesh itself acting as the child. Afterward, the script goes through each parent group stored within the entire board (tiles, obstacles, pass, and win tiles) and assigns them materials, behaviors, and other bits of gameplay data.

Although a utility script handles all of this, the Board node (which basically stores everything in the game besides the UI) calls the utility function to do this and then handles most of the other input and game-state data. I tried to design this to be as agnostic as possible, and have implemented using a lot of static scripts (scripts not attached to nodes) and groups (Godot's tagging system) in order to get nodes, apply functions, and set game behaviors.

In practical terms, this is how this looks:


A scene has a root node that is basically left untouched, the Board handles most of the operations of the game state including player input, assigning tile materials and texture based on groups, and (generally) sending signals to the UI to allow for player movement. Thought this has been functionally very fluid and I've been trying to keep thorough notations of what's happening where within the board, I am starting to get worried about code bloat within the board handling all of the events in the scene. It doesn't do EVERYTHING, but it does do all of the input and game-state handling. I think going forward I'd like a separate script to be running game states so that I could do a bit more versatile debugging (and not have to sift through a mountain of code to find the exact spot where a state is being initiated) for now this seems OK.

The player is loaded onto the board with an array of rays attached as grouped children. These arrays check the tile collision on the board and determine any "moveable tiles" around the player. When tile is selected, the lil' pawn mesh I have as a placeholder moves to the tile's position, and the player move sequence is ended after "ending your turn." Then if an enemy is present on the board, it get's to take it's turn. Though tile selection, highlighting, and movement took a bit of time to program, it was nothing in comparison to coding the enemy behavior.

In order to understand how the enemy works, I think it's important to look at the different "phases" the enemy has:


Before doing anything, the enemy notifies the player that it is thinking using the narrative modules built to indicate board "data." This entails loading the narrative module, adding the appropriate node to the tree, and printing the information using a static function stored in the script attached to a custom narrative node (again, I might revisit this to make this a stand-alone resource, but this works fine). Immediately following this, the enemy checks to see what "moveable tiles" are available (identical process of using raycasting colliders like the player). 

After storying what tiles are available, the enemy performs a hidden coin flip to determine if the move they want to make is "intentional" or "random." When designing the enemy, I had initially made all of the movement random, but I found that what usually happened during play testing is that the enemy either boxed themselves in or ended up moving in a very limited loop (and didn't really pose much of a threat). I wanted the enemy to follow the players movement and respond to their position on the board. I designed this, however, so that it wouldn't always have to follow the player exclusively, but instead could move toward a specific tile on the board (again with the intention of trying to allow for flexible enemy types down the line). The enemy calculates the distance from all moveable tiles position to the players position and when moving "intentionally" selects the tile closest to the player (the tile with the shortest calculated distance).

Then after completing the movement the enemy performs a hidden dice roll to see if they perform an "attack" or skip this phase and "miss." Using a similar method as the movement raycasters, a separate group of "attacking" raycasters determine if there are any tiles to sabotage and if they pass a skill check they turn a random tile from a group of four available options into an obstacle (fictionally setting fire to that tile and preventing the player from passing through). I think that this method poses interesting challenges for the player, but I want to build out the different "classes" for each player (see below) so that the patter of attack and the outcome of attacks can be varied going forward. That will be mapped out at a later date, but for these initial boards this standard enemy type poses enough of a threat. That being said, I still have yet to program an end-game state if the enemy ever catches you or if they attack a tile that you are on. This is a bit of a high priority at the moment, and it's right behind implementing sabotage abilities for the player (more on that in a separate post probably).

After finishing the attack phase the enemy pauses and gives control back over to the player, thus starting the turn-loop over again. From a design perspective, there will be more variables at play to give the player some competitive edge and/or strategic tools to either trap an enemy or allow the player to generate "pass" tiles in order to allow for safer passage across the board.  

The last thing that I'll mention is that I recently had a minor technical revelation and discovered that implementing custom resources to handle narrative text blocks and controlling scene management would allow for a lot of flexibility in the development process. I created a custom resource that stores an array of text that can be iterated on using a simple narrative sequencer. The board then stores/loads this resource as an exported variable, so I can create new resources as necessary to determine story beats and scene sequencing. I think duplicating this method for enemy types would have similar benefits, since I feel that I'll want to create more custom behaviors for enemies going forward. For now the simple enemy AI that I designed is just fine.

I've probably left out some really important technical and coding aspects of the project that I feel very proud of at this stage, but I'm gonig to wrap things up here. Looking forward to sharing more details about the technical, narrative, and design aspects of the project in the coming weeks and thanks so much for taking the time to read!

Get Sewilla's March

Leave a comment

Log in with itch.io to leave a comment.