top of page
  • Foto do escritorGabriel A. Pereira

The Benefits of an Architecture with ScriptableObjects in Unity


ScriptableObject architecture in Unity

When I started to learn game design independently, one of the first questions that came to me was: how can we connect, in an efficient and modular way, all the systems that make a videogame?


When speaking of systems, I am obviously referring to the individual components that operate simultaneously to offer a playable experience. For instance, the common elements we find in platform games like Super Mario are: the protagonist, the enemies, the obstacles, the collectibles...

Among that, there is a need to communicate to the player the result of their actions, something that typically happens by displaying information in the screen via the HUD (heads up display), or user interface, either through texts, icons, graphics or other visual elements. Each one of these will be linked to a core system that controls them: the commonly named Managers. Therefore, they are independent from one another... the user interface does not need to know about the protagonist, however it needs to display information regarding health points, or how many coins have been collected; when the player reaches a checkpoint, this has to somehow be communicated to a dedicated system so that, in case the player loses a life, the protagonist can respawn at that point.

But how can we achieve this without breaking the game, at the same time that we ensure the possibility of expanding and adding new elements, while avoiding repetition an redundant steps?


In the beginning, one of the methods I adopted in order to integrate core systems was the Singleton pattern. Loved by a few, hated by many, this seems to be one of the most ancient patterns of game development, consisting in the creation of a script that is in charge of controlling a certain system, where we insert a generic method that allows all other game components to access it through code, without needing a direct reference. This means that, to update the protagonist's health points in the UI when they suffer some kind of damage, I would have to simply type «UIManager.Instance.UpdateHealth(5)», where "Instance" represents the global variable to reference the single copy of that component that exists in the current scene (or level).

However, this pattern has a series of setbacks. First, it requires that an object with the referenced component exists in the scene, otherwise it will be automatically created once a script refers to it. This applies to ALL of the game scenes, requiring the designers to ensure the instance is actually available and correctly adjusted.

But why resort to such a boring and repetitive method to incorporate a global system when there are alternatives that provide a much modular development flow?


Around September 2020, Unity presented an initiative called Unity Open Projects, which purpose was to invite the community to participate in the creation of a videogame from scratch, along some official team members. From concept to release, the idea was to share the project with the community in an open-source fashion through GitHub, so everyone interested, no matter their level of expertise, could contribute, or at least learn with the development process.

When I came across this initiative, the idea of learning by accessing a game architecture developed by professionals from different levels of experience excited me a lot, since it would allow me to expand my own knowledge. One of the first subjects that was debated in the project's discussion forums was the establishment of a foundation for the game's architecture. Initially, the idea of using the controversial Singletons came along from some participants, but the Unity team presented the approach of basing the game's systems and communication between them in ScriptableObjects, and it was unanimously accepted.


But what are ScriptableObjects?

The term alone is self-explanatory. Since Unity is based on an object-oriented architecture, ScriptableObjects are little code snippets designed to compress an isolated behaviour inside a dedicated object, which allows them to be adjusted through exposed parameters, also allowing other objects to reference them. These are like Lego pieces that can be freely modified for those that want to use them. offering a large range of connections that avoid constantly resorting to code.

Yes, a ScriptableObject has to be programmed beforehand. However, it can be created and stored in the project's folder so that designers use them freely, within their potential.

These are elements that, in my opinion, must only be introduced to those that already have a solid knowledge of the general concepts that make game design in Unity, and are familiarized with the way the engine operates. I don't expect someone to use them in their first amateur project. But, once we discover their potential - and I can speak for my own experience - ScriptableObjects open a new door of opportunities in the long and arduous process of developing a videogame, not only solving structural gaps, but also avoiding heavily coupled and time consuming design patterns.


Going back to the Open Project initiative by Unity, one of the first things that caught my attention when I opened the project and started to explore what had already been made by some contributors was the implementation of an event system based on ScriptableObjects. Events are one of the main game design solutions that allow for code decoupling, capable of sending and receiving messages which trigger reactions in response to them.

Producing events while developing a videogame requires some planning, since they can present a few minor challenges. For instance, we need to ensure that, in terms of order of execution, events are not raised before the creation of objects that need to subscribe to them, to avoid scenarios where the broadcasted message is not received.

With ScriptableObject we can always design a modular event system, capable of being referenced directly in the Unity inspector. This way, the ScriptableObject containing the event will be the mediation layer that connects the instance sending messages to those that want to receive it, without the need to establish direct references between one another.

ScriptableObjects event architecture in Unity
Showcase of an event system based on ScriptableObjects

So, returning to the question regarding the broadcast of information about health updates, a ScriptableObject based event would be an efficient alternative to the Singleton pattern. This way, the protagonist would not need to reference the UI Manager directly through code: in this case, they would just need to reference and raise events like OnPlayerDamaged, OnPlayerHealed, or OnPlayerDied, to which the UI Manager would be listening and be in charge of updating the health bar image accordingly.


With the creative freedom that ScriptableObjects provide, I have been looking for more ways to implement them in my projects for different purposes that would otherwise present themselves as a big challenge. Besides events, one of the functionalities I have implemented through ScriptableObjects in a prototype I have been working for some months, was an ability system. This system allows the game's protagonist to acquire abilities when they collect a certain type of objects.

First I've created a component called Ability System, which is attached to the protagonist and is in charge of controlling its current abilities. Each ability consists of an individual ScriptableObject that, on a code basis, comprises all of the inherent logical operations, while exposing a series of adjustable parameters in the inspector to allow the creation of different variants. With this, the game can add and remove abilities to the protagonist without any sort of issue, even when debugging, since I made sure that the system supports directly swapping abilities at runtime in the Unity editor.

Ability System based on ScriptableObjects in Unity
Game prototype with an ability system based on ScriptableObjects

Compared to less designer-friendly approaches, ScriptableObjects come with the great advantage that we can see all information and modifications directly in the inspector, without needing to constantly use "Debug.Log" to register game information to the console.

This is why I also decided to use ScriptableObjects for storing relevant game progress data, not only because it ensures that information can be accessed directly by various systems, but above all because it makes it easier to visualize all data in an organized manner and, in extent, identify a possible mistake in case something fails.

One of those examples is a ScriptableObject which I've called Gameplay Stream. It only contains two reference fields: one that indicates the level the player is currently playing, and another that indicates what was the last completed level (more specifically, that in which the protagonist reached the goal).

And who needs to access this? The first one can be used by the UI Manager to access level data, and thus display relevant information according to, for instance, the type of level (traversal, boss, etc...). On the other hand, the reference to the last finished level can be used by the World Map (the area where the player selects the level they want to play), so the protagonist respawns at the point where they've entered that level, after it is finished, conveying the idea of continuity.

Using a ScriptableObject to store relevant Player Data in Unity
Usage of a ScriptableObject to store in-game progress information

Another variant of a ScriptableObject I've developed was one called Player Data, which comprises all information relative to player progress, like collectibles found in each level, unlocked characters, among other relevant data that can be stored and retrieved between game sessions.


As you can see, ScriptableObjects have different usages for a productive, intuitive and modular game design flow. However, they are not limited to the implementations mentioned here. In fact, these components provide an infinite range of possibilities, whose only limits are the programmer's imagination and invention.

Another usage also present in Unity's Open Project is the implementation of State Machines, that is, components that regulate logic operations according to the current state of an object, commonly used to control character behaviour. The community contribution to this mechanism has settled in implementing the behaviour of each state through a specific ScriptableObject, and the same applies to state transitions, which are in charge of analysing conditions to check when they should be triggered.

The protagonist is in the "Walking" state but the player has pressed the "jump" button? Then the system transitions to the "Airborne" state. The protagonist has lost vertical velocity and has touched the ground? Then the system transitions back to the "Walking" state... All this is dynamically implemented through ScriptableObjects, which are available to any designer to freely use without needing to code.


In my yet short Unity game design and C# programming learning journey, I must say I found ScriptableObjects to be a thrilling concept, which prompted me to seek new knowledge sources, share opinions and re-think the architecture of amateur projects I have developed as a way to hone my skills and learn to solve problems.

I must admit that I am unaware if similar approaches exist in other popular engines, with which I am not yet familiar on a practical level. However, I can state that I wish to continue discovering the multiple applications of this tool that Unity provides and has been gaining more and more popularity over time.


As many state, while an architecture strongly restricted to code is normally preferred by veterans of the area, a focus on the object and modular design - which is what Unity's ScriptableObjects provide - is becoming a trend among the new generation of programmers and designers, since they tend to lean towards more pragmatic approaches.

It is true that there can be disadvantages in both, as well as some sort of logic inconsistencies, but if we take into account the old saying that "the machine is always right", I would dare to say that a modular approach can help us find that reason faster when looking for bugs in a project, because it allows us to follow the trail left behind, leading us right to the core of the issue. This way, we avoid resorting to an architecture exclusively based on code and complex formulas.



Recommended links:

Comments


bottom of page