Abyssal Shade is a current work-in-progress project I am completing as part of my studies with several other students. It is an ominous, ethereal underwater game in which players take control of a manta ray tasked with returning life to a dying reef. Players do so by piloting the manta ray through the environment and collecting Soulfish, small marine creatures filled with the lifeforce of the reef. These fish move on a boid system and are the main interactive point of the game, with the player being able to collect them, guide them, and apply forces to them in various ways to shepherd them to dying areas of the reef.
This project was my first time creating a 3D player controller, and it was one that needed unique movement too. Rather than a simple first or third-person controller, we wanted the player to be able to move in any direction to simulate swimming through the ocean, accompanied by proper camera movement, strafing, and roll to navigate through the environment in any desired path.
I built a similar state machine for the player controller as I did in Quantum’s Pursuit, only this time incorporating 3D movement rather than 2D. This has become a common habit for me when designing player or enemy controllers in games, even if behavior trees will end up being relatively simple, as I think having the modular organization of a state machine system really helps with any debugging, tweaks, and additions later on in development. It also does not take me much longer to create than a normal player controller would after having created the architecture several times on different projects and having a very solid grasp on the flow of inheritance and composition the project uses.
I designed the player movement to feel responsive yet weighty to accurately simulate the feeling of gliding through water. I used separate speed values for moving forward and strafing to help encourage more graceful, ray-like movement through the water to make players feel more like they are really embodying marine life in the game. I also added the ability to apply roll and turn to reorient themselves in their environment. The movement all feeds through Unity’s physics system, adding forces to the player’s Rigidbody component and using fine-tuned drag settings on the player to get the movement feeling just right.
All of this swim functionality was written in a separate SwimMovement script, away from the player state machine script. A reference to this script is simply passed in to any moving player states and its functions to calculate move forces are called to move the player. This allows the player to still be able to move in the same way while also allowing enemies and other marine life to be able to use the same swim script to move, just with their own generated input and speed values. I really wanted to highlight modularity in my code for this project due to its relatively short development time and inherent similarities between different entities in the game, as they are all types of marine life.
I also used Unity’s new Input System package to cleanly handle input for this project, allowing easy input references once set up in the player state machine scripts and simple steps to add future input mappings. The new input system allows players to create different input mappings with lists of actions mapped to certain button presses. This is created through an InputActions asset in Unity, which I then handle through a PlayerControls script that outputs a streamlined series of data for use in player controller scripts. This includes a Vector2 for directionalInput, bools for different button presses, and so on.
Using Unity’s new input package allows for a much more modular, organized, and clean way of handling input, plus additional levels of control being offered in terms of what types of button presses you want to track and mappings for different control schemes. This will allow us to potentially add controller support to our game despite limited development time of several weeks, as the input action asset can simple add another mapping and plug in joystick button presses to our predefined actions, automatically running through the same PlayerControls script to do so.
I chose to streamline our input this way for this project to promote clean coding practices and allow for easier use of modular scripts such as our SwimMovement script for the player, as I can simply pass in values from PlayerControls such as the directionInput vector and have proper physics forces returned to the player to be added.
As previously mentioned, the fish in this game behave using the Boid behavior pattern. First outlined in 1986 in a paper by Craig Renolds, boids are bird or fish-like objects that exhibit incredibly complex and organic schooling or flocking behavior using only a few simple behavior rules, which are as follows:
a) Cohesion: boids will move toward the center of other nearby boids.
b) Alignment: boids will move to point in the same direction as nearby boids.
c) Separation: boids will move apart from each other if they get too close to prevent clumping.
By combining these three behaviors, one is able to create very realistic, interactive, and mesmerising schooling movement with many entities at once. Much of my design of this system was inspired and guided by Sebastian Lague’s boids project, which can be seen in his popular YouTube video here. The boids also have a variety of data values stored in a scriptable objects that can easily be adjusted to tweak aspects of boid behavior, such as speed, turning rate, and weight values for cohesion, alignment, and separation rules. Adjusting these different weights can drastically change how boids behave, allowing a vast range of mechanical customizability by just adjusting a few sliders.
Before tackling this project, I did extensive research into boid patterns to understand their rules, ways the architecture is built, and good practices to handle many boid objects on screen at once. Once I had a good grasp on the concept, I began building the system, creating a BoidManager class that would get references to all boids in the scene at runtime and then call an update function on each boid every frame, passing in a list of all other boids to calculate the appropriate cohesion, alignment, and separation forces for each boid object. Initially, all of the boid rule logic was handled directly in the Boid script, but as recommended in Sebastian Lague’s video, I eventually moved this functionality to a compute shader to increase performance and optimization with more boid objects in the scene. Doing so taught me how to use compute shaders to speed up vector calculations and drastically improved performance, allowing over a thousand boid objects to be present on screen without a hit to performance, where before it could barely handle 300.
Eventually, this system will be incorporated into the main game loop as development progresses to allow boids to follow the player when nearby and interact with ocean currents and other forces to create more dynamic gameplay and increase interactivity with the system.