
We recently visited Amsterdam at the
Unite Europe 2016 conference, where we received a lot of emotions and interesting experience. At this conference there were a lot of fascinating reports in different directions and different levels of complexity. The theme of one of the speeches was “Overthrowing the MonoBehavior tyranny in a glorious ScriptableObject revolution”, in which Richard Fine (
https://twitter.com/superpig /
https://github.com/richard-fine ), a specialist from Unity Technologies, He spoke in detail about ScriptableObject and showed by examples how it can be applied in the project.
In his report, Richard raised the following questions:
Next is a free translation / retelling of what Richard was talking about, with various additions.
')
Tyranny MB
General information about MB:
- Most scripts are written as MB.
- They are attached to the GameObject (hereinafter GO).
- They live in scenes or prefabs.
- Get some callbacks from the engine (such as Start, Update, etc.).
What are the disadvantages?
- Reset when leaving the playmod.
- Instantiation creates a complete copy.
- Instances are inconvenient to “fumble” between scenes.
- Instances inconvenient "fumble" between projects.
- Bad VCS granulation (when the script is changed, the whole scene changes on the scene object, conflicts often arise, etc.).
- Can be inconsistently customized. If there are several identical objects in the scene, there is a chance of accidental changes in the properties of one of them, which is not always easily detected and is not quite correct from the point of view of game design, because if all game objects are the same, then they should have the same properties. Although there are exceptions when this behavior is convenient, but these are more rare cases.
- Not quite appropriate conceptually: it is often necessary to operate with clean data with the possibility of native and automatic serialization in the inspector, rather than a component / object with a certain position in space, etc.)
How can you escape the tyranny of MB?
Pure static C # classes?
- Still discarded when exiting the playmod.
- You have to serialize them manually.
- Inside such classes it is inconvenient to operate with Unity-objects.
- You have to write your own inspector.
But we specifically use the engine, which provides all this "out of the box" to avoid inconvenience!
What about
prefabs ?
They solve the problem of storage and transfer from scene to scene and between projects, and do not violate VCS granulation. But this solution also has disadvantages:
- You can easily spoil everything, for example, accidentally instantiating / dragging into the scene.
- There may be additional components (for example, AudioSource), but why are they needed, because the data must be separated from such things!
- Conceptually, it’s still not a perfect solution, perhaps acceptable, but ...
SO comes to the rescue
SO is a class that allows you to store a large amount of information transmitted regardless of the script samples. This class can be inherited if there is a need to create objects that will not be attached to the GO.
Imagine that there is a prefab with a script that has an array of a million integers. The array occupies 4 megabytes of memory and belongs to the prefab. Each time, creating an instance of this prefab, an instance of this array is also created. If you create 10 game objects, then the size of the memory occupied by the arrays for these 10 instances will be equal to 40 megabytes.
When using SO, the result will be completely different. Unity serializes all types of primitives, strings, arrays, lists, specific types such as Vector3, and custom classes with the Serializable attribute as copies relating to the object in which they are defined. This means that when creating an instance of a class SO with an array of one million integers declared in it, this array will be passed along with the sample. In this case, the instances believe that they have different data. SO fields or any UnityEngine.Object fields, such as MonoBehaviour, Mesh, GameObject, etc., are stored in links, as opposed to values. If there is a script that references SO, containing a million integers, then Unity will save in the script data only a reference to SO. turn SO stores the array. 10 copies of the prefab that refer to the SO class using 4 megabytes of memory would end up occupying 4 megabytes instead of 40 mentioned above. This is especially important when it comes to a large number of objects and / or large amounts of data in scripts.
So SO:
- It's like MB, but not a component (it is worth inheriting custom classes from SO, not from MB).
- Cannot be attached to GO / Prefabs.
- It can be serialized and inspected by the Inspector, just like MB.
- It can be placed in an .asset file (you can create custom assets by placing your own textures / materials, etc.).
- Solves some problems of polymorphism. If you look deeper into the serialization code in Unity, it turns out that when inheriting, not all things can be correctly serialized, in the case of SO there are no such problems.
How SO saves us from problems:
- Asset storage prevents dumping when exiting the playmod.
- It can be referenced, not copied when instantiated (memory consumption decreases, instantiation speed increases).
- Like any other asset, it can be used and "roam" between scenes.
- It is easier to “fumble” between projects: it is enough to transfer only one asset file, there is no need to dig through the entire hierarchy and look for dependencies.
- Ideal VCS-granulation (one file - one object).
- No additional unnecessary parts (for example, Transform).
But SO is also not perfect:
- Only three callbacks: OnEnable / OnDisable, OnDestroy. Although this problem can be solved using MB as a proxy.
- General data, in fact, not very common. They can not be changed for a particular entity, which has its advantages and disadvantages, it depends on the selected flow.
How to use SO
The SO class should be used when it is necessary to reduce the memory consumption by avoiding copying values. But it can also be used to determine which data sets to include. It is great for configuration files, such as level settings, global game settings, or individual settings for characters / enemies / buildings, and so on (for example, you can store maximum health, damage, and other parameters). SO is also handy when writing custom tools, level editors, etc.
From SO instances, you can quickly and conveniently create custom assets, reuse them, load them over the network, etc. When you declare a successor to the SO class, you can mark it with the
CreateAssetMenu attribute, which adds an asset creation point for this object to the context menu of Assets / Create.
An example of a simple script with settings:
using UnityEngine; [CreateAssetMenu(fileName="EnemyData", menuName="Prefs/Characters/Enemy", order=1)] public class EnemyPrefs : ScriptableObject { public string objectName = "Enemy"; [Range(10f, 100f)] public float maxHP = 50f; [Range(1f, 10f)] public float maxDamage = 5f; public Vector3[] spawnPoints; }
Creating an asset:

Serialization of an asset with the data in the inspector:

When working in the inspector with SO instances, you can double-click on the reference field to open the inspector for your SO. It is also possible to create a custom editor to define the type of inspector of its type to help manage the data it represents.
An SO instance can be created without binding to an .asset file: programmatically, using
SriptableObject.CreateInstance <> .
SO lifetime:
- Same as any other asset.
- When it is persistent (linked to an .asset file, AssetBundle, etc.):
- SO can be unloaded by GC through Resources.UnloadUnusedAssets ,
- continues to exist in memory, if it is referenced in other scripts,
- can be reloaded if necessary,
- When it is not persistent (created using CreateInstance <> and not tied to any .asset):
Patterns of use
Most developers consider SO as a data container, but in reality it is something more. Consider some of the patterns of its application.
As data objects and tables:- Plain-Old-Data (POD) class bound to the .asset file.
- You can edit in the Inspector, commit as a single file in VCS. For example, a game designer can change some settings or other data without affecting the scenes or prefabs, which is very convenient.
- With the help of a custom editor, you can make it even more convenient when used and configured in the Inspector.
- It is worth choosing an approach when using SO: apply one object per entity or one per table / set of entities. For example, when creating a localization table or a configuration file, one common object will be enough, and if you need to create templates for different units, then you will need your own for each one.
- Examples of use: localization tables, inventory items, unit patterns, level configurations, etc. None of the above does not require a position in space, or some kind of logic. It's just data that can be changed without affecting the scenes and prefabs, thereby, without interfering with the work of other team members.
Example:
class EnemyInfo : ScriptableObject { public int MaximumHealth; public int DamagePerMeleeHit; } class Enemy : MonoBehaviour { public EnemyInfo info; }
As expandable listings:- In the form of an empty SO, bound to the .asset file.
- Can only be used to check for equality with other such objects, or to check for null.
- Like enums, but instead of writing code, they can be created by designers directly in the editor.
- Examples of use: inventory items, event categories, damage types, unit types, etc.
- If necessary, they can be easily expanded to data objects / tables by adding the necessary properties.
Example:
class AmmoType : ScriptableObject { } … if (inventory[weapon.ammoType] == 0) { PlayOutOfAmmoSound(); return; } … inventory[weapon.ammoType] -= 1; ...
Dual serializationAs mentioned earlier, one of the advantages of SO is full compatibility with the Unity serialization system. Wherein:
- SOs can interact with JsonUtility .
- As a result, you can get a mixture of assets created during the design phase using the Unity serializer, and assets created after this programmatically using JsonUtility.
- Examples of use: built-in levels (design, saved by the Unity serializer) + user levels (created in runtime and saved using JsonUtility). From the point of view of architecture, there is no need to worry, with the help of which such levels were created, they can be loaded into memory as SO and work with them universally.
Example:
[CreateAssetMenu] class LevelData : ScriptableObject { }
- You can create directly in the editor through the menu, adjust the values, etc.
LevelData LoadLevelFromFile(string path) { string json = File.ReadAllText(path); LevelData result = CreateInstance<>(LevelData); JsonUtility.FromJsonOverwrite(result, json); return result; }
- the method allows you to read a text file with JSON (which was created locally during the level creation process or, for example, came from the server) and transfer it to SO
As singletons:- SO + static instance variable (SO instance is created, and the link to it is stored in a static variable).
- FindObjectWithType to restore an instance after level reload.
- Examples of use: global game states.
Example:
class GameState: ScriptableObject { public int lives, score; static GameState _instance; public GameState Instance { get { if (!_instance) _instance = FindObjectOfType<GameState>(); if (!_instance) _instance = CreateDefaultGameState(); return _instance; } } }
As delegates:- SO, which contain methods.
- The MB transmits the reference to itself to the SO methods; SO performs the work, using MB if necessary.
- This makes it possible to implement embedded and customizable behavior.
- Examples of use: AI types, various power-ups, buffs / debuffs.
Example:
abstaract class PowerupEffect : ScriptableObject { public abstract void ApplyTo(GameObject go); } class HealthBooster : PowerupEffect { public int Amount; public override void ApplyTo(GameObject go) { go.GetComponent<Health>().currentValue += Amount; } } class Powerup : MonoBehaviour { public PowerupEffect effect; public void OnTriggerEnter(Collider other) { effect.ApplyTo(other.gameObject); } }
Thus, Powerup MB delegates its work to PowerupEffect SO, which is the pattern.
Conclusion
Summarizing the above, we can conclude that SO has its pros and cons, some of which are given below:
Benefits:
- It is perfectly serialized "out of the box", both with the help of JsonUtility, and visually in the editor (as opposed to static classes, for example).
- Changes are saved and not reset after exiting the playmod.
- You can conveniently fumble between other scripts.
- Can easily inherit.
- Not placed in the inspector, thus does not litter it.
- It does not have an overhead projector and is more lightweight than MB.
- It is not attached to the GO, respectively, and is not deleted when the object is destroyed.
- It can be stored in the scene as long as at least one object in this scene refers to it.
- It can be saved to asset and reused in other scenes using AssetDatabase, runtime in already deployed builds, etc.
Disadvantages:
- It is not displayed in the scene - it is visually unclear how many instances of SO are currently in use, which can make debugging difficult.
- There is a chance to implicitly destroy it if you destroy the last object that references it (in the scene).
- Not so convenient to use in prefabs.
- It is not obvious who owns the object, when it should be destroyed, in what area it is located.
- Since SO is not attached to the GO, there is no way to quickly get links through GetComponents.
SO is a very important tool that, although it cannot completely replace MB, it can be used in conjunction with it, eliminating its tyranny. SO should not be used for everything, but it gives you more flexibility. Much more than those who are familiar with SO superficially or not at all. It also provides an opportunity to build an excellent workflow for your project, create convenient and useful tools and templates for designers, etc.
Richard at the beginning of his speech stated: “MonoBehavior sucks” (MonoBehaviour sucks :)). It is difficult to completely agree with this, because one way or another, one can hardly do without it (MB). But the main thing is to understand that you should not use it always and everywhere, and that there are various alternatives, one of which is the powerful, flexible and convenient ScriptableObject. It is necessary to correctly choose one or other means, based on the task under specific conditions.