Scriptable Objects

Let's continue with the high-level-architecture tips-and-tricks stuff ;-). Beside using reusable components I've also learned to fall in love with ScriptableObjects. I'm using them extensivly since I saw this here:



That guy really changed my mind ... and the whole archtitecture of my first game! Things suddenly became really easy and reusable. I will explain you how I'm using them.

But first: What is a ScriptableObject

A ScriptableObject is compareable to a MonoBehavior. The main difference is that you can not attach it to an GameObject. So it is not a component. But you can store instances of it in your assets. So it can become used by MonoBehaviors.


ScriptableObjects can hold data, reference other unity-objects and also can contain logic.


But you should be aware of the following behavior: The data a ScriptableObject holds won't become restored after your game ends. But don't worry. I will explain you how to handle this.

Setup definitions

Let's start with this - because I guess this is the common use case. And again I will use an example of my upcoming game. There you can produce products and every product has a product type. Instead of having an unflexible enum I'm using ScriptableObjects to define them:




The benefit should be clear: When I want to have more product types I just can create them without having a code change. And all of the types can have individual parameters. As another example: My Shops are also ScriptableObjects which refers to the producty types to describe, which kind of product they're offering.


My recommendation for this usage is: don't change anything within those ScriptableObject-instances through runtime! If you want to change them during runtime then you mixed your setup predefinements with your mutable game data. But those data should be handled seperated! The next section will explain how to handle such mutable states.

States

Of course a game knows same states which should be changable. Therefor I've an own ScriptableObject defining a GameState. In order to have a known state I reset every value of this ScriptableObject to default values within the OnEnable method:


Code
  1. private void OnEnable() {
  2. _companyName = "";
  3. _cityName = "";
  4. _citizen = 0;
  5. _money = 10000;
  6. }

Benefit: a previous run of the game is not able to mess up my data! But we've a problem: the data of the previous run is lost. But we can save our data very easily by just serializing the ScriptableObject. You can do this by using the JsonUtility:


Code
  1. public void Save(ScriptableObject source) {
  2. var objectAsjson = JsonUtility.ToJson(source);
  3. File.WriteAllText(Application.persistentDataPath + "/data.json", objectAsjson);
  4. }

Application.persistentDataPath is provided by Unity3D and returns a path to writable directory based on your running device. We just extend this path by a filename. File.WriteallText is just an easy way to write this file.


But be aware when your ScriptableObject is refering another one. JsonUtility doen't serialize the refered ScriptableObject. Instead it is only writting the instance-id of the refered object to the json-file. This isn't a problem in case when you've something kind of a Setup-Definition. But it can end up in unexpected result when the refered ScriptableObject was created during runtime.


My recommendation here is to copy all values you'd like to change during the game into the ScriptableObject holding your GameState. Specially when you want to persist date based on anthoer ScriptableObject created during the runtime.

Services

This is soooo awesome! You can write your Services as ScriptableObjects and use them e.g. in onClick-Events of a button! And combined with the use-cases above you can see the full power of ScriptableObjects. Here an example:



This is my GameManager. It is even more simple than it looks like on the image. Let's see some source code:


Code
  1. public class GameManager : MonoBehaviour {
  2. public UnityEvent OnGameStart;
  3. private void Start() {
  4. OnGameStart.Invoke();
  5. }
  6. }


That's all (at the moment). But my GameManager is actually doing the following:


When the game starts it checks if there is a savegame file and a config file. If existing - those files will become loaded and applied to the ScriptableObject which is holding the state. As you can see the GameManager itself doesn't really know about this. He only knows, that he's to execute something.


Before I was using this architecture my GameManager had to know about the Services. I'd to hardcode them into it. Every new service required also code changes on the game manager class. And in the end my Managers really had a lot of locs (Line of Codes).


My recommendation for this usage is: use it! But like I've explained within my last article about components: don't try to write the ultimate service. Write small services which are responsible for a single thing.

Conclusion

Like in the last article this here is an additional way how you can have resuable and testable code. It really made my life easier and I only can recommend: use them!


P.S.: If you want to know what this "strange" Game Listener is ... watch the youtube video mentioned at the very beginning. It is really worth it!

Comments 1

  • That's interesting. I've just had an introduction to scriptable objects during a text adventure tutorial I've looked into. This video is meant to be a good explanation of the benefit of scriptable objects: