Fact Sheet / Summary
– Genre: Point-and-click Turn-based Board Game
– Available Platforms: Windows, WebGL, Android
– Engine: Unity 2017 - 2020
– Asset Creation: Blender 2.8x & 2.9x
– Key Programmatic Features: Hierarchal State machines, Recursive Path Traversal, MiniMax AI with Alpha-Beta Pruning
– Available Platforms: Windows, WebGL, Android
– Engine: Unity 2017 - 2020
– Asset Creation: Blender 2.8x & 2.9x
– Key Programmatic Features: Hierarchal State machines, Recursive Path Traversal, MiniMax AI with Alpha-Beta Pruning
WebGL version of the game on Simmer.io
Other (download) versions:
Windows:
Android:
|
|
Background
This game originated as the final project in my advanced game programming class before I graduated with my degree in Game Development at Golden West College. Although the purpose of this project was advanced programming concepts, after graduation I created custom assets for the game (the “Puffguins”) in order to personalize the project and to demonstrate that I have skills in “right-brained” asset creation, as well as “left-brained” coding and scripting.
Gameplay
Based on the children’s board game, “Hey! That's My Fish!”, the gameplay is simple. Each player has a number of “Puffguins” that stand on the “ice floes” (hexagonal tiles) that make up the game board. Each tile has 1 – 3 fish, but each “Puffguin” must start on a tile with only one. The object of the game is to win the most fish. On each turn, the player moves one of their “Puffguins” by clicking on the one they wish to move and then clicking on the destination tile. As it leaves a tile, the “Puffguin” acquires the fish that were on that tile and they are added to the player’s score, and the vacated tile sinks, so that it is no longer accessible. If a “Puffguin” becomes isolated on an “island” of tiles that no other player can reach, that “Puffguin” is removed from the game and the total remaining fish on that “island” are automatically won by that player. The game continues until only one player has any “Puffguins” left.
Designing the Assets
Most of the scene assets in this game, i.e. the “Puffguins” and tiles, were modeled, textured, rigged, and animated (in Blender) by myself. Only the fish and “robot penguins” came from existing assets. With the original penguin-based board game in mind, and inspired by a funny song about puffins by the brilliant Malinda Kathleen Reese, I conceived of these creatures for my game, which are halfway between the two.
To add to the mood of the game, I also added a background asset of animated ocean waves, as well as ambient sounds of splashing water and sea birds.
Animating Gameplay
The animation in this game is pretty straightforward – There is an idle animation for Puffguins not currently moving, and a “waddle” as a Puffguin moves to a new tile, along with an animation that “sinks” the vacated tile. Raycasting is employed to enable clicking on the Puffguin to move and the destination tile. Lerp() is used to ease the Puffguins’ locomotion in and out. There’s also a few extra details like a particle burst when fish are won, and rotation of the “wind up key” on the “robot” penguin as the AI calculates a move for itself.
The UI
Unity’s UI system is utilized for three main canvases in the game, as shown above (Images): The start screen where player(s) choose what color Puffguins they want to play as (and an optional color assigned to the AI), the main game scene, where the current scores and current turn are displayed, and the end screen, displaying final scores, and offering the user the option to play another game.
Game Management
The overall flow of the game is handled by a GameManager class that maintains a State machine of the current game conditions. There are four main game states (shown in my preliminary diagram/notes above): Start Screen (Where players choose their colors and set the AI), Puffguin Setup (Where players randomly place their Puffguin on their initial tiles), Main Game Play, and Game Over (where final scores are displayed). Game Play also has several sub-states defining whose turn it is. A Player class defines an internal object for each player, and polymorphism is used on each turn to hand control to either the current human player (to prompt user for a Puffguin to move and its destination tile) or to the AI (to compute its next move and move its Puffguin automatically).
Internal Board Representation
Physically, in the game scene, the board is an array of tessellated hexagonal tiles, as shown in my original sketch above – Puffguins move from tile to tile, and only one can occupy any given tile at a time. But for simplification, the board is treated internally like a square grid, as on a chess or checker board, and each tile has a name defining its location in the grid, using the same convention as chess: Rows labeled 1 to 8 and columns labeled a to h. So for example, “e4” is the tile on column 5 (“e” is 5th letter) and row 4. But the situation is complicated by the tessellation of the hexagonal tiles, so that the number of tiles per row alternates between seven and eight, and because they’re hexagons, there are potentially six directions each Puffguin can move on a turn, instead of only four, as it would be the case if they moved like chess rooks or bishops over square tiles. Therefore an intelligent algorithm was required to determine each Puffguins’ set of legal moves each turn. To this end, there is a TileManager class that maintains the current state of the board during the game, and which also provides methods to, given an input tile, mathematically determine the adjacent tiles in the six available directions, designated as “west”, “east”, “northeast”, and so on. Each tile also has an ITileTraverse iteration interface, that allows the use of a foreach loop to find all valid tiles that can be reached in one move, from the input tile. ITileTraverse is implemented by the respective classes for the physical tiles in the scene, and the “virtual tiles” in the board’s internal representation. Each tile also has an internal state to determine if it is “active,” i.e. it can be occupied by a Puffguin, or if it has “sunk” and is no longer in the game (in which case the physical tile is removed from the game scene).
The AI
“Puffguins and Fish” implements the MiniMax algorithm with Alpha-Beta pruning to allow the user to play against the computer (represented on the board by the “wind-up robot penguins”). This MiniMax implementation maintains its own copy of the game board and a stack of moves (using a Command Pattern) in order to traverse its tree of candidate moves. The utility function determines the best move based on what potential subsequent moves will lead to acquiring the most fish in the least amount of moves.
Because of the time it takes for the MiniMax algorithm to run, especially with more than two players, the current implementation only seeks one level for a move in the early moves of the game, until only two players still have Puffguins on the board, and the number of remaining tiles falls below 40. When these two conditions are met, then the full MiniMax traversal kicks in. Without this admittedly regrettable limitation, the game animation would “freeze” for a perceptible length of time while waiting for the algorithm to complete. People have asked me why I didn’t just use a coroutine, and it’s because of the MiniMax algorithm’s inherent recursive nature, and coroutines are really not designed for such a purpose. In a future version, I plan to solve this issue, and allow MiniMax to always execute in the background without impacting the game display, with either multi-threading or the Unity Jobs system.
Because of the time it takes for the MiniMax algorithm to run, especially with more than two players, the current implementation only seeks one level for a move in the early moves of the game, until only two players still have Puffguins on the board, and the number of remaining tiles falls below 40. When these two conditions are met, then the full MiniMax traversal kicks in. Without this admittedly regrettable limitation, the game animation would “freeze” for a perceptible length of time while waiting for the algorithm to complete. People have asked me why I didn’t just use a coroutine, and it’s because of the MiniMax algorithm’s inherent recursive nature, and coroutines are really not designed for such a purpose. In a future version, I plan to solve this issue, and allow MiniMax to always execute in the background without impacting the game display, with either multi-threading or the Unity Jobs system.