Lactose Wars: Programming


 

The areas of development that posed the biggest risk to Lactose Wars lay entirely in the grid system. Having only ever previously worked on one grid based title my experience working with tile based gameplay was very limited; especially since my role on that team was purely focused on game design and only rarely required me to dive into the code. With this being a solo project however, diving into the code was inevitable so I needed to ensure I would be able to overcome or work around all associated development risks. Knowing I was a bit in over my head I turned to the internet for assistance and stumbles across Quill18Creates’s video series on “Dungeon Tile Movement & Pathfinding” which gave a quick introduction to both generating a grid and implementing pathfinding. Confident enough I would be able to apply this information to my own project I set out to try my hand at something I would have otherwise thought too difficult.

The Board:

The simplest solution to creating a grid would have been to use square tiles to build the board ahead of time within the editor where each tile snapped to Unity’s grid. However because I wanted the naval units to feel more fluid and life-like as they mover around the board, I had decided to use pointed top hex tiles to build the play space. This would make it impossible to build out a grid within the editor where each tile nicely snapped to whole units. In addition to this, because the straw player is able to use their turn to drink the milk in the glass, the stage shape and size must be able to change dynamically during play. This meant I would have to figure out a way to dynamically generate the game board.

As the milk level (pink) drops, the grid will need to conform to the walls of the glass (teal)

Fully generated grid


Grid Generation 1.0:

The fact that the stage was circular posed an interesting problem when it comes to generating a grid as I would not be able to simply create a nice mathematical shape to perfectly fit its dimensions. This was especially true if I planned to create multiple uniquely shaped stages where a slight change in the slope of the stage would prevent certain tiles from being valid as they then overlapped with the stage.

In order to generate my hex grid, I treated it much like generating a 2D square tile grid, and generated my board via quadrants (see quadrant 1 below). I iterated through a for loop and sequentially instantiated a row of tiles one unit apart along the X axis until I reached a specified number of tiles. I then iterated through a second for loop, incrementing the number along the Y axis and began instantiating tiles on the new row; this created a grid of evenly spaced tiles (see quadrant 4 below). The problem however, was that there were gaps between my hex tiles along the X axis, as the hex tiles were slightly less than one unit in width. In addition to this, each row along the Y axis was perfectly in line with the row below it and the pointed tops of the hexes were touching. In order to create the zipper-like effect present within hex grids, each row on the Y axis would need to be slightly less than one unit above the row below it and every other row on the Y axis would have to be slightly offset on the X axis as well (see quadrant 2 below). At this point all four quadrants were generating properly, however they extended through the stage. I then implemented a check when each tile is instantiated where I call a sphere cast at the point where the next tile would spawn and check for collisions against any objects tagged as a wall. If the sphere cast finds nothing, a tile is created. If a wall is found however, the tile is not created and I move on to generate the next row (see quadrant 3 below).

 

Quadrant 1: Visualization of the coordinates each tile is assigned.

Quadrant 2: Visualization of how each row is offset upon generation.

Quadrant 3: Visualization of when each row is terminated along the X axis.

Quadrant 4: Visualization of the order of operations each quadrant is generated using. Quadrant 1 generates left to right along the X axis and then bottom to top along the Y axis. Quadrant 2 generates right to left along the X axis and then bottom to top along the Y axis. Quadrant 3 generates right to left along the X axis and then top to bottom along the Y axis. Quadrant 4 generates left to right along the X axis and then top to bottom along the Y axis.

 

With my grid now generating better than anticipated, I was ready to move onto integrating it with pathfinding. Most of the game logic would be handled through a node system in the back end as my hex tiles were meant for purely visual and user interface purposes. For the sake of clarity and troubleshooting, I had been assigning tiles and nodes coordinates respective to the quadrants in the above example. This however, also prevented me from being able to properly generate a node graph for quadrants 2 - 4 as I was unable to use the same system to generate my nodes as I had used to generate the graph visuals due to issues with iterating through negatives within a for loop.


Grid Generation 2.0:

I needed both my grid visuals and my nodes to utilize the same generation methods because my play space needed to be able to change dynamically; meaning I would never be able to predict which tiles would or would not be valid at any given time. This meant I would need to refactor my grid generation in order to create an array of nodes that contained all generated tiles. Fortunately my node generation was working within the positive quadrant (quadrant 1) so, with a few tweaks I altered the grid generation to essentially spawn as one giant quadrant.

 

Any tile located along the red arrows are void and never created.

Any tile located along the yellow arrows is created but subsequently destroyed after detecting the stage walls.

Any tile located along the green arrows are valid and successfully instantiated.

 

Using the length of the X and Y axis, I offset the tiles’ spawn point to center the quadrant over the stage to ensure the play area will remain symmetrical. I then utilize a largely similar tile generation method as shown above in quadrant 1; however now including an additional check. As I iterate through the for loop, I now also send a raycast below the position where the next tile is to be generated. If the raycast hits the stage, the tile is generated. If the raycast does not detect the stage, no tile is generated, but the for loop continues as usual.

For code samples relating to how I generate the grid see below:

 

Initialization for grid and node generation

Hex grid generation including instantiation checks

 

Pathfinding:

Node Graph:

Once generation of the grid’s visuals was complete, the next step was integrating it with a pathfinding algorithm. Even before pathfinding however, I needed to create a data structure that would track each tile and all of its neighbors so I would have information to which I could send to the pathfinding algorithm; I refer to this as a node graph. The important distinction here is thinking of the tile graph and the node graph as two distinct elements, despite them being intertwined; the tile graph represents the visuals and what the player will be interacting with where the node graph simply relays what the player is doing to the code base (pathfinding, attacking, unit selection, etc..) and passes along any pertinent information.

The element that is distinct with the node graph from the tile graph is that despite not every tile being visualized within the scene, I create the node data regardless. This means that while I may not be using it, I have access to the node in the top right corner of the quadrant as well as all of its neighboring nodes. This allows for a more simplified system of dynamically generating the tiles whenever the straw player takes the drink action. By preserving the node and unit positional information, I can simply destroy the tile visuals, shift the stage upwards and re-generate the visuals with no apparent change.

Below are code samples relating to generating the node information:

Custom class to keep track of the node graph and neighbor information

Populating the node graph according to the tile visual information

Calculating the neighbors for every node in the node graph, stopping at the nodes bordering the quadrant X and Y

A small element to note is that in refactoring the grid generation, I also removed the check that incremented the current row along the Y axis in order to simplify the creation of our path finding graph as nodes are only created where a valid tile could be. This means if I kept the aforementioned check, the tiles along the rightmost edge of the grid would not have their neighboring nodes recorded, which could cause unforeseen errors if the current grid size is shaped peculiarly.

Because any invalid tiles do not have a visual, the player is unable to click on that location and move their unit there, despite the presence of a node. However when pathfinding around the edge of the stage, there are a few instances where a unit may travel outside a valid node and temporarily leave the stage. To combat this, I created a simple method that toggles a node to a “walkable” or “unwalkable” state and included a check when pathfinding to calculate if the unit is able to move onto a given node. When generating the node graph, for any tile that does not have a visual, I set its respective node to “unwalkable”.


Dijkstra’s Pathfinding Algorithm:

Having never toyed with pathfinding before I was tempted to utilize a third party package that handled pathfinding for me. However, I knew that would force me to conform my code and worst case, my design, to the specifications of the package. Additionally, what better way to learn new skills than to try for yourself? Pathfinding has always interested me and has always been something I had hoped to learn, so I set out to try my hand at a simple pathfinding system. With the continued assistance from Quill18Creates’ video series, I was able to translate Dijkstra’s pathfinding algorithm pesudocode to work within the confines of my project., as seen below.

 

Part 1

Part 2