Systems Code in Detail


I prepared a short video showcasing the code within the Unity project roughly taling about the same things like this explanation here.

Progression System

That is a parasite. It is a sin. It was death we needed to die. An open knife, I knew we would jump into… and we jumped. We needed to be able to get data from about everywhere in the game so this was my solution to get connected to that data without coupling the progression system too tightly to game components.

Connecting is done on start-up in our progression driver (and tutorial driver but it is essentially copied until it is not. Tutorials and quests were one system until their requirements changed and it was better to split them. Yet they work very similiar and I may copied the progression driver). It is those simple two lambda functions. Honestly I am not proud about them but you can read that in the comment above. Oh and I like offensive programming. I like to shove myself error messages and stop execution rather than behaving as if nothing happend and wondering later why certain systems don’t work as expected. Don’t treat a bug as an exception where operation in a degressed state is needed. I don’t like to operate in degressed states. I do that way too often. I have migraine.

Anyway, our quest is the data struct containing everything we need. We needed both references to other quests as well as references to scene objects. That is unfortunate because in Unity you can’t have both. Hence we have the quest as ScriptableObject saved as data object to disk and as serialised object saved inside the scene. That way we reference using the saved files but save the completion requirements, rewards, and prerequisites inside the wrapper.

In Unity this monster turns into an UX hell describing a quest. I mean it was fine without further tools for our limited scope but I would have invested into a quest and tutorial tool if we would have had more than the seventeen quests and tutorials.

The scriptable object itself just contains the name, success message, failure message, and wether or not the start message should be shown.

The scriptable object itself just contains the name, success message, failure message, and wether or not the start message should be shown.

The wrapper doesn’t fit into the list space granted by Unity. It contains the prerequisites that all need to become true to start the quest. These can be entities and other quests in any state we choose. Then we have the completion requirements which can be any number of progression emitters with a target value and the comparision method. All completion requirements must be met to finish the quest. Quests cannot fail on their own. The only way to fail a quest is by receiving a fail state change.

The wrapper doesn’t fit into the list space granted by Unity. It contains the prerequisites that all need to become true to start the quest. These can be entities and other quests in any state we choose. Then we have the completion requirements which can be any number of progression emitters with a target value and the comparision method. All completion requirements must be met to finish the quest. Quests cannot fail on their own. The only way to fail a quest is by receiving a fail state change.

Lastly the wrapper contains the rewards. Quests can unlock entites, preplaced tiles, and modules. You can also use reward receivers to to something special but we removed all usages of them in the end as designes changed.

Lastly the wrapper contains the rewards. Quests can unlock entites, preplaced tiles, and modules. You can also use reward receivers to to something special but we removed all usages of them in the end as designes changed.

Speaking of tutorials, they are kind of similar but they are not. They all are active by default and you can silently finish them with certain actions and never see most of them. The main reason was to make them as unobtrusive as possible. We tried to think of ways to detect when players are stuck but that ended up being a dead end, so instead it is only time based with only one “active active” tutorial. This tutorial reveals itself after a certain timeout and it will show its helper(s) after another. However I must stress while this one “active active” tutorial will run and play its animation and helpers, all other can still be completed.

And because these are tutorials, we are happy if you meet one of their completion requirements

If you choose to take a closer look at the tutorial driver, you will notice the heavy use of LINQed statements (ok that one fell flat xD) but I don’t consider these drivers to be super performance critical as they have a lot of early returns, operate on little data, and I also didn’t find the drivers on my profiler. Because we did have a performance issue but it was caused by our outline plugin because it did something very stupid causing … sub-optimal run-time behavior. Something like O(nnn)… Changing that, was my most impactful oneliner ever. Here again very familiar code just changed so it returns true when one requirement is met.

This code along with most of both drivers should should have been unified but these changes happend shortly before our release so that’s the lame excuse why the copied code is still copied-ish.

The tutorials have the aforementioned helpers. We wanted to provide contextual help and needed to delegate the specifics because our helpers are very different.

We want your attention! Click on the entity to switch into the micro layer.

We want your attention! Click on the entity to switch into the micro layer.

I AM IMPORTANT! Click on the module to open its info panel.

I AM IMPORTANT! Click on the module to open its info panel.

Blink, blink. Click here and the connection build mode will start.

Blink, blink. Click here and the connection build mode will start.

I think, you should click that button. It may toggle the delete mode :D

I think, you should click that button. It may toggle the delete mode :D

True, these are a bit on the nose but after a fair share of games not working due to information starvation and playtests indicating sys:logic was not working either because concepts were not understood (and we already put a lot of effort into information presentation of our games state), we figured it was time for drastic solutions.

This should cover most of our progression system. The interesting bits at least. The related code can be found in the progression system folder.

Tile System (Execution Graph)

More interesting than the progression however is the tile system. It is what essentially drives out game logic. Everything else is related to player-game-communication. But this very system is very distinct and the core. It gets context through the embedding into our world that is required to ease access to this system. However let’s get started with the tile itself. I am going to get into details as we move on.

I should have added a trigger warning… HEAVY USE OF OOP FEATURES. We have a deep inheritance structure with many virtual functions and some abstract classes. The tile is a MonoBehaviour because we use Unity and these are components that live on GameObjects. Each tile knows its Cell. However it is important that the value is invalid as long as they are not added to the Grid. The default value of a Vector2Int is (0, 0) and caused quite a mess with our pathfinding and … “interesting” recursions when paths actually crossed (0, 0) that led to a stack overflow. Lovely. Also because this value is serialised, setting a new default value didn’t help at all. But we will come to the second part of the fix later.

The Grid is a reference to the owning, well, grid. We operate quite often on the grid itself. Thankfully I chose to do dependency injection here instead of making the grid a singleton. A singleton would have worked because in earlier version of the game we had only one grid. But since I didn’t, reworking the game from a single grid to multiple grids owned by an entity took only two days plus a few days to stabilise the game again.

The Inactive property (yes, it is not a C# property but it is a property of the tile in terms of “when set it behaves differently”) is used by preplaced tiles to register them but render a question mark instead. The actual behavioural implementation is in Module and I don’t know why I put it here. My bad.

Ok the next parts are quick. OnRemoveTile() and OnAddTile() are callbacks that are triggered when a tile was added to a grid or is about to be removed from it. GetNeighbours<T>() had once the implementation that the grid now has and as it was used I just put a redirect there. We needed the implementation in other parts where we didn’t have a tile object and just a cell instead. Remove() was born out of lazyness however in my defence: Without it, someone would have simply used Destroy and introduced an invalid state in the grid. It is also not straight forward that you need to remove the tile on the grid itself. I thought about using the OnDestroy() message but decided against as most removals are actually triggered by outside code. It is a rare case that tiles delete themselves. And we cannot have both as that would trigger two removals which we consider a bug.

All our tiles live in a grid structure called TileMap. It maps world space coordinates to grid space coordinates and keeps track of all tiles. It also comes with a good amount of utility functions that we build upon. It started as this data only structure but progressed to also generate the visible mesh and partake in the progression as well. Still we try to decouple it from the execution graph and progression systems through the usage of events.

Because we want to use types when possible and need to know which prefabs to spawn when we want to call something like GridMap.AddTile<Buffer>(new Vector2Int(2, 3)), we have a tile database called TileDB. It consists of a handful of settings for each module we add and is heavily used throughout our game.

For the descriptions we chose a static naming system to minimise the available options a keep consistency. In general, the usage of the Unity Localisation package helped us quite a bit even if we had only one language. Because we were able to treat these strings as assets. The proper structure gave us a speed advantage as we developed the game in both German and English at the same time. This was due to us release it on itch.io to a multi-lingual audiance and being a German research project attending German speaking conferences.

Now that we got the basics of the tile system covered, let’s move up a bit to the next class inheriting from tile: Module.

See, there was a rule, especially in the beginning, to write doc comments as we knew the project would be travel through various hands with most knowledge getting lost in the process. And this was even more true for fundamental classes and methods.

Modules are the essential base class of our system. A module is an interactable thing that is meant to partake in the micro layer graph puzzle game play. That was on purpose because we considered adding obstacles to the grid and that would have worked perfectly fine with this approach as they could have inherited from Tile, block these tiles and do stuff without interacting with the rest.

Modules also come with ports and are connected to other modules and connections. This got a bit messy admittedly. The reason being our connections and pathfinding, and because we wanted to exclude some modules from the execution graph. More on that later. Modules also must have UI. The rest is used for feedback and our colour pallete system (Grids can have a defined colour pallete and the modules need to match them obviously).

Here are some excerpts of functions that are implemented for further usage. Please, please ignore the if (this is Connection)s. I know this is an architectual problem based on the very fact that Module contains the ConnectedModules, Connections among other things and not ExecutableModule. However, as I am just showing this system here isolated, I cannot show you how widespread our use of Module in the codebase is. It should have been done but life happend and we fixed it dirty. Then our time was over :/ It is not like that this a super hard refactoring to be made and one that should have been done. However these kind of fixes tend to get in at the least convenient times and quite frankly, our base classes were not touched that often, so forgot about it and every time me saw it, something else was way more important.

Our messengers are back! They notify the tiles around them about their arrival and departure. Also on removal of a module, we destroy all connections to that module as connections cannot live alone.

Awake and Start are both needed in child classes so these are marked as virtual. SetModuleOutline has the third parameter because of a child override that needs it. Just some basic stuff to ensure UI is set up, we find our modules in the hierachy, and our ports are spawned.

That is the last part of the interface. Note how you should not believe everything that is doc comments. Sigh. Sadly, such wrongness in one place biting you once, leaves you with the feeling that the documentation may not be in a good shape overall. That is a huge problem, especially since we wrote these comments to fasten future work. We provide a basic UI showing method that can and has to be customised. It offers dismantling of tiles if they have a button (They should not have the button, if you cannot dismantle them). You then override this method to initialise your UI state when you have more, like sliders, buttons, or combo boxes.