Pine is an open-world game, developed with Unity, where the player plays as Hue, a human living on a mysterious island full of various living species. His goal is to find a new place to live, since the destruction of his village. The species are controlled by very complex AIs, living in villages, creating raid groups, and interacting with each other. These species evolve over time, even if the player is on the other side of the island.

- Surface type detection

- Localisation

- Village area detection

- Debug tools

- Audio

Surface type detection

The surface type detection was the second most important task I was asked to do. The goal was to be able to assign a surface type to any collider in the whole project, and to be able to get it back at runtime. These surface types are used to detect if an object is made of wood, stone, rock, metal, flesh... in order to play the according sounds and vfxs (walking on grass should not make the same sound as walking on a bridge). After some research, I decided to use Unity's PhysicMaterial system. These PhysicMaterial can be placed directly on colliders, and are usually used to define friction/bounciness. I then decided to split my work in two: the system to retreive the surface types at runtime to play the right feedbacks, then an editor window to assign these PhysicMaterial to any collider in any prefab of the project.

After creating the PhysicMaterial that I needed, I made a ScriptableObject to bind the PhysicMaterial instances to a SurfaceType enum. At runtime, a SurfaceTypeManager then loads the AssetBundle containing that ScriptableObject. The SurfaceTypeManager then stores all the bindings into a dictionary. Any script can then ask directly to the SurfaceTypeManager singleton the SurfaceType of any collider. However, this system does not work for TerrainColliders, since in that case the SurfaceType depends on the texture of the ground instead of colliders.

Once the SurfaceType detection system was done, I started creating the editor window. I decide to divide the window in three columns: the first one containing a list of loaded prefabs, the second one containing the colliders of the prefabs selected in column one, and the third showing the information about the colliders selected in column two. To use that window, you first have to load the prefabs you want to edit. A button "Open all project prefabs" can open all of them, but it can get quite slow and memory extensive. This was, at the time, the most complex editor window I ever made, and I was quite happy with the result. This tool was almost immediately used by the designers, and almost all SurfaceTypes were defined in less than a day.

Localisation

The most important task I was asked to work on was the localisation of the whole game. The lead programmer asked me to make a system that had to reserve in memory only the fields that might get called in the next few minutes. The system would be loading and unloading data all the time, in order to keep the memory usage as low as possible. The system works as follows: at any moment, any script can ask the LocalisationManager the translated version of a string id. When doing so, the script can ask for an automatic loading, or a strict loading. Automatic loading means that the string will get loaded then returned using a callback whenever it's loaded, whereas strict loading assumes that the data containing that string was already loaded previously. For instance: you might want to have a strict loading for the quest-related strings, since there's no point in loading the strings of the quest #37 if you don't have that quest in your quest log. On the other hand, you might want to have automatic loading for more common strings, such as "grab", "talk" or "interact".

The string ids are contained in ScriptableObjects, so we quickly needed an editor window to manage everything. This editor window got crazily complex, but I'll try to explain it briefly. The editor window must allow the creation, editing and removal of LocalizedScriptableObjects. The editor window must also allow the creation, editing and removal of any field of any scriptable in any language. The window generates a lot of code (enums, string Ids, classes, structs, ScriptableObjects) and modifies a lot of ScriptableObject data. This editor window is still, to this day, the most complex editor window I had to make. However, I feel like this whole system wasn't really needed, since the game is not that big, and doesn't really need such complex systems. A simple downloader and parser of a Google Drive file would have been enough in my opinion.

Village areas

Before my arrival at Twirlbound, the village detection was done using a point at the middle of the village, along with a radius. This system was preventing the designers from creating long or large villages, but most important, was causing issues in terms of village height. A big village would have a big radius, thus a big height, which was causing issues if the player was under a village (in a cave), or above (in the mountains). I was asked to create a system that would allow them to define geometric shapes that they could move around in the scene. In order to avoid the shapes cluttering up the scene view, it was important to have the collision detection only work from code (thus preventing me from using Unity's colliders). In the inspector, I made a toggle to turn on and off village editing. When on, the village collision boxes can be seen and moved around using Unity's handles, and when turned off, these boxes are converted into a BoxArea class, that stores the position, scale and rotation of the boxes, and that can check at runtime if a point is contained inside the box.

Debug tools

I started this internship by exploring the codebase, in order to understand and to adapt to the code syntax of that company. Once I felt comfortable with the codebase, I decided to improve the current debug tools of the company.

When pressing a certain button combination, they already had a system to unattach the camera from the player, in order to move freely around the island. However, I quickly realized they didn't have any way to teleport the player to the current position of the camera. This simple feature was integrated in less than a week, and has saved a lot of time to the designers, especially when they had to iterate on the level design of some puzzle sections.

After this task, I was asked to create a system to spawn any item from a debug panel. Since they already had a debug panel (that could be accessed by pressing F5), I decided to add my spawn system to that debug panel. To do so, I added two separate dropdowns: one for the item type, and one for the actual items of that type. When an element is selected, the corresponding game item is spawned either in front of the player if the camera is attached to the player, or in front of the camera in the other case.

Audio

After the debug tools, I was asked to develop and improve the audio systems of the game. In Pine, all species AIs are made of huge intricated state-machines. Each state defines what happens when we enter or when we exit that state. In order to reduce the amount of code needed, it's also possible to define "modules", which automatically run some predefined code on enter and/or on exit. I decided to create a module to automatically play the corresponding sfx when we enter that state. When that module is created, we can define wether we want the sound to be stopped when leaving the state or if we would rather have the sound fade out on its own.

One of the biggest tasks of this internship was to manage the region music and ambient sounds. The system I ended up with is quite complex, but can be simplified and explained like this:
The MusicManager subscribes to some events. When these events get invoked, the information is sent to the SoundClipManager. The SoundClipManager then checks if the clip we want to play is included in the list of clips that are fading out. If so, we recycle it by making it fade in instead, otherwise we ask a new clip to the pooling system. Then, we check if the new clip overrides some other clips that are being played (combat music should override village music for instance). For each clip that gets overriden, we make the fade out. Once a clip completely fades out, it gets sent back to the pooling system in order to be released and reused.