Ink Inside was my first long-term project, and as the sole programmer*, it was such a rewarding challenge! I gained so much experience with Unity, C#, developing tools, optimization, and code architecture.
*Minus a small shader contracting job and porting work :)
Ink Inside uses a mechanic similar to the old top-down Zelda games, where the player travels from room to room, or screen to screen. We needed a tool to easily create these spaces and adjust their settings, and so Spacey was born- the most important tool in Ink!
Spacey's freecam scene view, which shows rooms, bridges between rooms, and beans (points of interest that are used in cutscenes or when teleporting characters around)
Spacey also has a preview camera mode! This lets the level designers see what they're building with the in-game camera angle and field of view, while still having the ability to zoom in/out.
The Spacey interface! This is the Map tab, where you change how, when, and where a room will show up in the player's map. Presets are included, and the designer can also choose to copy the marked settings to all connected rooms in a flood-fill fashion.
Spacey has a lot of settings for each room- more than 30! Including:
Scripted events that can play when entering the room, completing the combat encounter, or dying
Audio/music (audio settings can also be inherited from other rooms)
Lighting/postprocessing options
Combat respawn settings
Camera adjustments
Deep Dialogue
Deep Dialogue is a modular dialogue system that I put together to handle some scenarios where we wanted to possibly trigger any of a number of cutscenes, depending on context. For example, when the player takes a moment to rest, the characters might start a conversation about a quest they currently have, or talk strategy about enemies they've seen.
With the cutscene system we had at the time, we would've needed a giant if-else block for each action that could trigger a cutscene. Instead, you can assign cutscenes to an event, such as resting. Then give them requirements, and a priority in case there are multiple cutscenes that want to play at once.
Possible conversations that can trigger when the player rests.
How each event would probably look with our old method of cutscene logic.
During gameplay, Deep Dialogue will notice an event/player action, and try to find a cutscene that could play at that moment. If there are none, that's okay too! We just don't have a cutscene then.
An example of a possible conversation when the player rests, which only plays when 2 or more Sogged Tall Bugs (or their Sugar Swamp variant) have been defeated.
The interface could be better, but it did its job well enough! Deep Dialogue was added later in development, and wasn't used consistently enough to warrant taking the time to rework it.
Quests & Jobs
Every RPG needs quests! We knew early on that Ink Inside was no different, so I started on the quest system.
Quest data can include gameplay tasks, rewards, quest markers, post-completion events, and more. Many of the quests in Ink are gameplay-focused rather than story-focused. "Damage 5 Enemies with the Environment" or "Upgrade 3 Items" are the kinds of tasks you'll often see, so each of those task types needed to be implemented. Quests can also be manually completed by the designers during cutscenes to allow for story-focused quests.
Editing a Quest and its requirements. Yes, there are a lot of unique task types, because...
A long while after implementing quests, we added Jobs, which are unique passive abilities that the player unlocks by doing certain (often very specific) actions. With a little abstraction, I was able to reuse the existing quest task code, and the two are now largely the same system under the hood!
Editing a Job. Does the Requirements section at the bottom look familiar?
When using Unity's TextMeshPro, you can insert sprites into text, but you have to pack them into a sprite atlas and create a glyph table first. The table holds data such as how large specific glyphs should be in text, how they should be positioned, etc. Unfortunately, I found working with the glyph tables to be pretty clunky, and updating the sprite atlas with our TexturePacker workflow would often lose the data anyway! So I created a tool that would instantly generate the glyph table for us, while allowing easy tweaking at the same time.
This is the whole tool!
It's extremely simple to use; there are default parameters for all glyphs, and then you add individual tweaks on top of that for any glyphs that need it. You can also group glyphs to have the same tweaks if you want. Best of all, it all updates in real time so you can see how everything looks together!
Sectors
There was a point in Ink where some scenes started to become bigger than originally expected, and CPU performance was suffering on lower-end devices. Up to that point we hadn't had many issues, but we needed to reduce the number of active objects drastically.
My first thought was using asynchronous scene loading to only load in different areas when the player approaches them. The main problems with this method were:
We would have to separate every scene into their own smaller scenes, losing the majority of the Spacey data in the process, as Spacey data is linked to the scenes themselves.
I would need to rewrite Spacey to work seamlessly across multiple scenes.
The workflow of creating/linking rooms and ensuring they don't overlap would be much more awkward, because everything isn't all together in one place anymore.
Admittedly, this was more of a problem with the implementation of Spacey than anything, but it would be a huge undertaking to change it at this point in development. I decided to try a quick and dirty test that avoids those issues entirely: what if each zone/sector was defined as two massive boxes- one to detect if the player is in that sector, and a slightly smaller one that would activate or deactivate the GameObjects inside it. This test ended up working really nicely for the issue we were trying to fix, so... we kept it!
GameObjects in the red box will be active only if a player is inside the green box.
Later, while our publisher was working on porting the game, I was informed that it ran Really Badly on the Switch- who could've guessed? We needed even more optimizations...
Why Is My GPU Melting?
Ink was actually one of my first real 3D projects, so I had a lot to learn about optimization on the GPU. There were two important changes I initially learned about and made during this process, increasing GPU performance for all devices:
Applying GPU Instancing and reusing materials across the entire game wherever possible
Removing every unnecessarily transparent material
I knew embarrassingly little about transparencies and their downfalls early on, so every little flower, rock, and decal was using a transparent material instead of opaque. No wonder we had so many sorting issues!
Those changes alone boosted our FPS by around 60%, which was great for many devices, but not enough for the Switch. I also needed to add the ability to disable postprocessing effects, as those were the new bottleneck. Allowing or forcing effects to be disabled such as bloom, depth of field, and our custom hand-drawn/outline shader helped the game run much better!
Quality Objects
To get the game running smoothly on low end devices without sacrificing visual quality for everyone else, I added more quality options and a new system to go with it: Quality Objects. Quality objects are able to change properties about themselves depending on the quality the user has assigned to any category (particles, physics, or details). At low settings, many environmental physics objects will lose their rigidbodies, particle systems will have reduced particle count (or none at all), and the few remaining transparent materials can be swapped for opaque ones, or hidden entirely (like some shadows).
The transparent lollipops turn opaque, and the shadows are disabled when changing Details Quality to Low.