CatFishing
Overview
Although I didn't have a hand in the initial development of the game, I was later put on the project as the primary/lead developer. As the primary developer, not only was I developing features for the game, but I also organized the overall architecture of the game's codebase, and kept an eye on the game and its codebase as a whole throughout development.
I would also teach and lead junior developers on the team, work with our producer and help determine what features need to get done when, and give and receive feedback with everyone on the team.
I also ended up doing a good amount of game design for the project, with one of the first things I did for the project being consolidating all the scattered ideas and features, using them to write the game's design document. While writing the document, I designed several systems and features myself to fill in some of the missing gaps.
Most of the game's code was written in-house, but a handful of plugins were used to help with development. Of note are:
- Odin Inspector - improved formatting and tools in the inspector
- Rewired - better input handling and management
- DOTween - tweening utility
- Curvy Splines - spline math
The Character Controller
When development started, the character controller and surrounding systems were copied and modified over from a previous project. The problem is, that project was a first-person shooter, and this is a simple top-down adventure game. The character controller was filled with tons of unnecessary features, like equipment, damage, knockback, and more.
And so, when I was moved onto the project, I had the task of trying to get something functional and maintainable out of this mess. A lot of the features were heavily intertwined with each other and thus required to use, so I ended up having to pull some awkward shenanigans to get things to work, like making the only piece of equipment in the game an "empty hand" that was always equipped.
Ultimately, what ended up happening was a pretty significant refactoring of the character controller, which wasn't too difficult to do since we were so early in the project. A handful of the features were ripped out, with the state machine updated to have the now-missing features, while some features, such as input and animation handling, were left intact.
The rest of the unused features were pretty harmless and lightweight, like the health system, and were deemed not worth the effort and potential headache to remove, so they were tucked away into the "unused but we're not deleting it for safety/compiler reasons" closet and left untouched.
Note: ControlledObject
is a custom class that contains general functionality for things like physics and movement. This is the class that the player controller derives from.
So, what were we left with after everything got sorted out? Well, the character's state machine, the main part of the character controller, works a little something like this:
- Each state is an instance of a "State"
ScriptableObject
- Each state has its own version of
Enable
,Disable
, and all theUpdate
loops, which can be given custom logic by creating a new state that inherits from theState
script.- These functions are called at their corresponding timings by the state machine
- The state machine is used to handle transitions between different states, which can be called from inside of a state or from an external source
Here's a (heavily) simplified example of what some of the state scripts might look like:
{
public override void OnFixedUpdate(ControlledObject controlledObject)
{
base.OnFixedUpdate(controlledObject);
if(controlledObject.notOnStableGround)
{
// Locomotion is an enum, which the state machine uses to access common states quickly
controlledObject.StateMachine.SetState(Locomotion.Fall);
}
else if(controlledObject.HasMovementInput())
{
controlledObject.StateMachine.SetState(Locomotion.Move);
}
else if(controlledObject.GetButtonPressed(InputAction.Jump))
{
// InputAction is another enum, used for handling player inputs
controlledObject.StateMachine.SetState(Locomotion.Jump);
}
}
}
{
// Define state-specific variables up here
public override void OnEnter(ControlledObject controlledObject)
{
base.OnEnter(controlledObject);
controlledObject.isRunning = false;
controlledObject.moveSpeed = controlledObject.walkSpeed;
}
public override void OnExit(ControlledObject controlledObject)
{
base.OnExit(controlledObject);
controlledObject.isRunning = false;
controlledObject.moveSpeed = controlledObject.walkSpeed;
}
public override void OnFixedUpdate(ControlledObject controlledObject)
{
base.OnFixedUpdate(controlledObject);
// Basic Movement
controlledObject.InputDirection = controlledObject.GetInputVector(InputAction.Move);
controlledObject.isRunning = controlledObject.GetButton(InputAction.Run);
controlledObject.moveSpeed = controlledObject.isRunning
? controlledObject.runSpeed : controlledObject.walkSpeed;
controlledObject.SetAnimationBool(AnimationRunParameter, controlledObject.isRunning);
// Other movement code would go here
}
}
As the game's development continued, the design of the game evolved with it, resulting in some needed changes to various systems. One of the biggest of those changes was a complete rewrite of the character controller's movement and physics system, as it was not originally built with much platforming and vertically in mind, which was being added to add some more depth to the game's exploration systems.
While the ground movement was largely okay, a few adjustments were made, namely moving and exposing some variables to make things more accessible to designers. The biggest changes needed had to do with the jump and air momentum, which ended up getting rewritten entirely.
Go Fishing!
Alongside exploration and the game's story, the primary game mechanic is fishing, which was designed and implemented by me. A rough overview of how the fishing system flows is shown in the diagram, but in detail:
- When the player is in a valid state (idle, walk, etc.), they can interact with water to begin fishing
- This sets the player's state machine into the start of series of dedicated "Fishing" states. This plays the sitting down animation, before letting the player cast their rod, handled inside one of the fishing states.
- Once the player casts their rod, they wait for a fish to bite (fish AI and biting handled separately, led by another developer)
- After a fish bites, the player enters the fishing minigame. The player's state sends input to the minigame script, which handles all the minigame logic (see below), and once the minigame ends the script sends the player's state machine information on whether they won or lost
- Different animations/timelines play depending on a win or loss, before resetting to the casting state
- During the casting state, the player can exit, which plays an animation before returning them to the idle state
As for the fishing minigame itself, the end product works like this:
- After the starting animation, the minigame begins. A "catch meter" fills up, and starts at 50% full.
- When this meter hits 0%, the fish escapes and the player loses. When it hits 100%, the player wins and catches the fishing
- The player takes control of a reticle on the screen, and must chase a swimming fish icon across the play area
- When the player's reticle is hovering over the fish, the catch meter fills. Otherwise, the meter depletes.
- The depletion gets faster as the minigame goes on, ramping up the difficulty if the player takes too long.
- Each type of fish can have their own difficulty settings, controlling how randomly and how fast the fish icon moves
- The player has a "stun attack" they can use. If the player's reticle is over the fish, this attack will stun it, causing it to slow down drastically. This attack has a cooldown between uses
In terms of implementation, all logic for the fishing minigame is handled in its own scripts, separate from the player's state machine. The only interactions between the two systems are when the state machine tells the minigame to start, the state machine sending the player's raw inputs to the minigame (which the minigame then processes on its own), and when the minigame tells the state machine when the minigame is over and if the player won or lost.
The fishing minigame went through many iterations based on testing and feedback. The fish escaping was originally a separate meter from the catch meter entirely, but the two were later combined for simplicity and easier readability. The stun attack didn't exist at all at first, but was added in later to keep the player more engaged with the minigame.
All art assets (models, 3D animations, and sprites) were created by our 3D artists and UI artist, but all implementation, such as animation graphs and any 2D motion, was done by me.
UI, Game States, and Everything Else
Besides primarily working on the character systems, I also did a few other miscellaneous systems for the game. There were various UI pieces, like the pause menu, dynamic UI based on the current controller type, or a popup when you throw away trash (designed by our UI artist, animated by me).
The dialogue system was largely written by a junior developer, under the mentorship of myself and another developer on the team. After that junior left the project, I took over and finished up the dialogue system, fixing some bugs and adding things like character icons.
The game has a system for controlling game states, which we called the "Application State System." The system was originally made by a different developer on an old project, which was taken over and improved by me on another project with his feedback. That updated system was then imported into the CatFishing project, since we made the system with the intent for it to be generic and usable in any future projects.
The app state system uses ScriptableObjects
to define each state, allowing for each state to contain its own data and settings. Different states can then be SetActive
()
from a reference to that state's ScriptableObject, which then changes the game's ApplicationStateManager
's active state(s) based on the state's settings.
Typically, setting a state active will override any previous state, but settings allow for substates or multiple states to be active at once, or to let states be blocked by other states, stopping them from being active.
Scripts can check if certain states are active, allowing them to make decisions based on the active game states. Alternatively, a custom component called ApplicationStateBasedActivity
can be used to perform actions whenever certain states are enabled or disabled.
By default, this enables/disables objects based on the desired active state, but can be used to perform other functions via UnityEvents as well.
The system can be used for many different things, but it is most commonly used for controlling various UI states, such as a pause menu or a dialogue state.