Submariner

Release: 3rd Nov 2023
Genre: Adventure
Engine: Unity
Platforms: Windows
Store page: Itch.io

I worked on this project over the course of three months in my spare time. I began with the general concept of piloting a small submarine trapped in a vicious storm, expanding it out to a simple adventure-game structure where the player must collect various items to Macgyver together a solution to the storm problem.

After fleshing out the general game structure, I created a relationship diagram to model the various systems I would need to build and how they would interact with each other. While not 100% accurate to the final product, this initial design is quite close and ensured the implementation phase progressed smoothly and efficiently.

Key features

Buoyancy

Video showing waves applying physical forces to buoyant objects

Functional waves are a core component of the game and consist of three cumulative Gerstner wave functions that displace the vertices of a procedural mesh. Visual displacement is handled by a custom shader, but the WaveManager class is responsible for calculating displacement used in physics calculations.

To maintain performance, the WaveManager processes requests on-demand and only calculates displacement within a small sample radius of an object’s world-space position, using a grid-based lookup to reduce performance costs further. These results are cached per-frame to avoid repeated calculations for bodies in close proximity to one another.

On the buoyant bodies themselves, a buoyancy controller tracks changes in wave height over time and uses the normal to calculate the strength of the physics force to apply.

Game Events

The GameEvent ScriptableObject is my implementation of the Observer/Subscriber pattern, acting as a conduit for decoupled communication between classes. When one class needs to inform others of a specific event, a change in data state, or requires another class to perform some function, it calls the FireEvent method of the relevant GameEvent. Subscribers to that GameEvent are then notified, allowing them to respond accordingly without a direct dependency on the calling class.

My implementation here supports passing a variety of data types as parameters, as well as allowing for callback functions for when some measure of dependency is unavoidable.

Vertex displacement used to simulate a fish swimming

Audio Controller

To minimise the number of audio sources in the scene and streamline audio management, I implemented an audio controller to handle audio requests. The controller maintains separate pools of audio sources for one-off audio requests of various purposes (UI/2D audio, 3D spatial audio) as well as a pool for loaned “proxy” audio sources for classes that produce looping or frequent audio requests.

When audio requests are made, the controller gets the next available audio source from the corresponding pool and configures it according to the request parameters as well as the current game state, adjusting volume, mixer groups, and location (for 3D audio) before playing the requested audio clip.

This approach ensures consistency between audio-emitting classes, and simplifies the process of adding audio to additional classes. It also allows for features such as automatic cross-fading between music tracks (for instance, when pausing the game) as well as easy updating of volume levels when the player changes audio settings.

As mentioned above, the audio controller also provides a ProxyAudioSource object for classes that require continuous or frequent audio emission, such as the engine of the submarine. The ProxyAudioSource acts as a wrapper for a native AudioSource component, managing volume, cross-fading, and other audio properties to once again ensure consistency with the rest of the audio in the game. This proxy approach also provides automatic reclamation of the audio source if the owning class is destroyed or otherwise removed from the scene.

Submariner screenshot