Unity Atoms — Tiny modular pieces utilizing the power of Scriptable Objects

Adam Ramberg
12 min readNov 15, 2018

--

Unity Atoms is an open source library that aims to make your Unity game code modular, editable and debuggable. The library can be found here on GitHub.

I’ve been working with Unity for several years, creating mobile games for iOS and Android as well as using it during several Ludum Dare game jams. I generally really like Unity and everything that you get for free using it. However, seeing myself as a programmer first, I have always struggled finding a good way of creating a modular and maintainable code base. The general approach of building scripts in Unity (shown in most tutorials online) often yields very tightly coupled and monolithic scripts. This makes the code cumbersome to test, hard to debug and tricky to understand. It gets even worse when many developers are working together on a larger project. The problems described above have always bothered me and made me less motivated when working on my games and has sometimes even made me a sad developer 😢

The Revelation of ScriptableObject

Seeing this talk by Richard Fine during Unite 2016 really opened up my eyes to the use of Scriptable Objects in Unity. Ryan Hipple’s talk from Unite 2017 took the use of Scriptable Objects further and introduced the audience to the concept of shared state variables as Scriptable Objects and a nifty event-listener pattern. I really recommend watching both talks above as well as reading this article about game architecture and Scriptable Objects.

Even though I really believe that you should watch the videos above and wrap your head around Scriptable Objects before continuing, I understand that it is a big commitment. Therefore, I will try to explain and summarize why Scriptable Objects is a great tool in the Unity toolbox by listing some of its properties that makes it powerful, but also what makes it different from MonoBehaviours:

  • Unlike MonoBehaviours, Scriptable Objects do not live in a scene, are not attached to GameObjects and do not have a transform component.
  • Can be saved as .asset files, but can also be instantiated at runtime and live in memory.
  • Gets serialized in the Unity editor and are therefore viewable in the Unity Inspector.
  • Do not have most lifecycle callbacks that you are used to on MonoBehaviours, besides OnEnable and OnDisable.
  • Excellent for data storage.

All these features of Scriptable Objects make them extremely suited for replacing state and functionality that otherwise would live in MonoBehaviours. By defining small pieces of state (eg. just a variable), breaking out tiny reusable functions to Scriptable Objects and at the same time viewing Unity’s Inspector as a way of inject dependency, we can build modular pieces of code and state that can be reused and injected to our MonoBehaviours.

ScriptableObjects are indeed very cozy.

⚛️ Unity Atoms

Unity Atoms is an open source library that I have started to develop during the development of my current game project. It is based and derived from Ryan Hipple’s talk from Unite 2017 mentioned above. The library is still in its infancy and implementations are therefore due to change over the course of the game’s development. The main idea is to create a set of reusable building blocks that could be reused between projects and shared with others in the Unity community. My philosophy is that ideas and concepts thrives when they are shared and worked on together, so hit me up if you get excited about Unity Atoms and want to contribute or discuss new ideas to implement.

But enough about that. Lets dig into Unity Atoms! There are four fundamental building blocks of Unity Atoms that are essential to understand when working with the library: Variables, Game Events, Game Event Listeners and Responses. Below we will unfold what each of them do and how they interact with each other.

📦 Variables

Variables are storing data, for example primitives, reference types or structs as Scriptable Objects. Because Variables are stored as Scriptable Objects they are not part of any scene, but could instead be seen as part of the game’s global shared state. Variables are designed to make it easy to inject them (via the Unity Inspector) and share them between your MonoBehaviours. Lets see an example!

Imagine you have a PlayerHealth.cs script that contains the health of the game’s player. We will attach the script to a GameObject with a SpriteRenderer, BoxCollider2D and a Rigidbody2D called Player. The health is represented by an int, which corresponds to an IntVariable in Unity Atoms. The script will look like this:

In the game the player’s health will decrease when hitting something harmful. We will attach this Harmful.cs script to a GameObject called Harmful that also has a SpriteRenderer and a BoxCollider2D (as a trigger):

Finally we will add an UI HealthBar.cs script that we attach to a GameObject (inside a UI Canvas) with a RectTransforn, CanvasRenderer and UI Image component. The HealthBar.cs script will update the Image representing the HealthBar when the player’s health is changing:

Both Health and MaxHealth are global variables stored as .assets files that are (or could be) shared between scripts. To create these .assets files we can right click somewhere in the Project window, and go Create / Unity Atoms / Int / Variable. Variables look like this in the Unity Inspector:

The Developer Description is a text describing the Variable in order to document it, the Value is the actual value of the Variable, and Old Value is the last value the Variable had after it was changed via code. Changed and Changed With History will be explained in the Game Events section further down in this post.

We name the IntVariables created to Health and MaxHealth and set both their initial value (in this case 100). After they are created we can drop them on the PlayerHealth and HealthBar components via Unity’s inspector like this:

Variables gives us a way of separating our game’s shared state from the actual implementation. It also makes our code less coupled since we do not need to reference other MonoBehaviours in our scripts, eg. we do not need to reference the PlayerHealth.cs script in our HealthBar.cs script like this:

[SerializeField]
private Player player;

Hurray for less coupled code! 🎉

❗️Game Events

Game Events are things that happens in our game that other scripts or entities could listen and subscribe to. Game Events are (like Variables) also Scriptable Objects that lives outside of a specific scene. In Unity Atoms Game Events can be of different types and thereby pass a long data to listeners. Variables do by default have the possibility to raise two specific Game Events:

  • Changed — raised every time a Variable’s value is changed. The Game Event contains the new value.
  • Changed With History — also raised every time a Variable’s value is changed. However, this Game Event contains both the new and the old value.

This makes it easier to make our game more data driven than just using Variables. Lets take a look at how that looks in our last example. We can create a new IntEvent as a .asset file by right clicking and go Create / Unity Atoms / Int / Event and name it HealthChangedEvent:

And then drop it on our IntVariable for the player’s health like this:

We can then modify our HealthBar.cs script to look like this (do not worry about the IGameEventListener<int> stuff for now):

And then inject the HealthChangedEvent to our HealthBar component:

We now react to global state changes instead of checking the Variable value each Update tick. In other words we only update our Image component when we actually need to. That is pretty sweet!

👂Game Event Listeners

There is still an issue that the HealthBar.cs script is in charge of registering itself as a listener and at the same time defining what happens when a Game Event is raised. We need to seperate its concerns! This brings us to the third concept of Unity Atoms, Game Event Listeners. A Game Event Listener listens (sometimes also referred to as observes or subscribes) to a Game Event and responds by firing off zero to many responses. Game Event Listeners are MonoBehaviours and therefore lives in a scene. They can be seen as the glue between Game Events and Responses (see the next section of this post).

The HealthBar.cs script from our last example is actually a Game Event Listener, but a very specific implementation of it. We can do better than that! Lets create a GameObject in our scene and call it HealthListener. Unity Atoms comes with some predefined Game Event Listeners. In this case we want to listen to an IntEvent so we will press the Add Component button on our HealthListener, create an IntListener and drop in the HealthChangedEvent:

We can now shave off some of the code in our HealthBar.cs script to look like this:

And then go back to our HealthListener’s IntListener component, press the + to add an Unity Event Response, drop in the HealthBar component (from the scene) and under the dynamic dropdown section select the HealthChanged function defined above:

The HealthBar.cs script is now only responsible for what happens when our player’s health is changing. Pretty great, huh?

💌 Responses

The HealthChanged function created above is actually a Response of type Unity Event. However, in Unity Atoms there is also the possibility to create Responses as Scriptable Objects called Game Actions. A Game Action is a function as a Scriptable Object that does not return a value. As a side note there are also Game Functions in Unity Atoms, which are exactly like Game Actions but does return a value. To demonstrate the concept of a Game Action as a Response lets create a Game Action called HealthLogger.cs that gives some love to the console and logs the player’s health whenever it changes:

It is possible to create the HealthLogger by right clicking and go Create / Unity Atoms / Examples / Intro / Game Actions / Health Logger (this is available due to the call to CreateAssetMenu). When created HealthLogger we can add it as a Game Action Response to the HealthListener:

Every time the player’s health is changed we now log out the player’s health. This particular example is pretty simple, but I’m sure you can come up with lots of other use cases for it (for example play a sound or emit some particles).

That is it! We have covered the four most fundamental core pieces of Unity Atoms. Next we will dive into some other handy tools included in the library.

🎣 Mono Hooks

When you start thinking about this event-listener-response pattern you will soon realize that everything in your game can be expressed this way. The native Unity lifecycle methods can actually be thought of as events that we listen and react to. The bridge between Unity’s lifecycle methods and Game Events is called MonoHooks.

In our example we can use the MonoHook OnTriggerEnter2DHook to replace the Harmful.cs script completely. We will start by removing the Harmful.cs script from our project and remove its component on the Harmful GameObject. Next we will add two new components to the Harmful GameObject, OnTriggerEnter2DHook and Collider2DListener (both predefined in Unity Atoms). Then we will create a Collider2DEvent (Create / Unity Atoms / Collider2D / Event) and call it HarmfulTriggerEvent. We will drop the HarmfulTriggerEvent on the Event for both the OnTriggerEnter2DHook and Collider2DListener that we just created. Lastly we will create a new Collider2DAction called DecreasePlayersHealth.cs that looks like this:

We can then create a DecreasePlayersHealth.asset file (Create / Unity Atoms / Examples / Intro / Game Actions / Decrease Players Health) and inject it as a Game Action Response on our Harmful GameObject:

We can also add the MonoHook OnStartHook to set the initial value of our Health variable when the scene starts. To do so we will start by creating a new GameObject in our scene and add a OnStartHook component and a VoidListener component to it (both included by default in Unity Atoms). Then we will create a Game Event of type void and add it to Event for both of the components. Finally we can create a predefined Game Action called SetIntVariable (Create / Unity Atoms / Int / Set Variable) that will actually set the value on our Health variable and name it SetPlayerHealth:

This will properly set the player’s health to 100 when the scene starts.

Some further notes about MonoHooks: Event With GO Ref is a Game Event that will be raised just like the regular Event, but with a reference to the GameObject that has the OnStartHook component on it (by default). If Select GO Ref is defined we can select what GameObject will be referenced by the Event With GO Ref. This could be useful when we for example want to reference a parent or a child of the GameObject with the MonoHook component.

©️ Constants

A Constant is a stripped version of a Variable. It doesn’t contain any Game Events for when the value is changed neither the Old Value field. It is not possible to change a Constant’s value via code. The thought is to use Constants for global shared values that are not due to change, eg. tags, layer names, multipliers, etc. In our example it would be preferable for MaxHealth to be a constant:

And to change the player tag in our DecreasePlayersHealth.cs script:

®️ References

References can be toggled between use constant or use variable via the Unity Inspector. When a reference is set to use constant it functions exactly like a regular serialized variable in a MonoBehaviour script. However, when it is set to use variable it functions exactly like a Variable. This is something that is copied directly from Ryan Hipple’s Unite 2017 talk.

📋 Lists

A list is an array of values that is stored as a Scriptable Object. There is a possibility to add Game Events to a List when the following happens:

  • An item is added to the list.
  • An item is removed from the list.
  • The list is cleared.

Lists are perfect for storing data or references to Game Objects that need to get tracked and created at runtime.

📇 Some Final Words

Unity Atoms is open source and can be downloaded from Github and used in your project by going here. The project contains the final version of the example used throughout this post.

The idea from here on is to add more features and more general Game Actions and Game Functions that could be reused in not only my current game, but in future ones or yours as well. If it is possible to reuse well tested modular pieces of code proven to work instead of writing all functionality from scratch in your game it will result in less bugs (less code = less bugs). Some examples of reusable snippets that I have already created are:

  • GetUnusedGameObject — A Game Function that queries a GameObject List for an unused GameObject. If it finds one it returns it, otherwise it Instantiate a new one using a given prefab and adds it to the list.
  • ChangeScene — A Game Action that loads a scene by string name.

I hope that you have enjoyed reading this blog post and that you after reading it have realized all the great things that are possible using Scriptable Objects. Would love to hear what you think about this post in the comments below. Thanks for reading! 💚 Give me some 👏 and follow me on twitter if you liked what you just read. Cheers!

--

--