Consider that you are developing a merchant system for your game in Unity, where the merchant sells a list of items with a different name, price, description, and image.
The game designer in your team already wrote a 100-page document on various items with an entire chapter dedicated to the multiple potion types. As the lead programmer, you need to decide where to store each one of these items in your system, so where would you?
In this article, we’re going to be exploring fast prototyping in Unity and how you can use scriptable objects effectively in your game development.
I believe the initial attempt to tackle this system is to use a serializable struct that encapsulates the items’ properties. Let’s call it the ItemStruct. Then, you would add a List of ItemStruct in the merchant.
Thus, via the inspector, the game designer can input each one of these items manually while leaving you to program the other systems for the game. The code and inspector for such a system can be seen below:
[Serializable] public struct ItemStruct { public string name; public int price; [TextArea] public string description; public Sprite image; }
This approach is actually quite versatile, and for many simple systems, it is certainly the easiest and more stable way to go.
The serializable structs provide an easy way of making changes via the Inspector, while also using the advantages of being a struct in C# such as being more adequate for smaller data aggregation compared to classes, and for having value semantics.
However, your merchant system allows for different merchants across the game world.
One of them, in the starting area, is supposed to sell low-level potions and weak temporary buffs. For the sake of supporting slower players, the more advanced areas in the game have merchants still selling starting potions that cost less than their higher-level alternatives.
If we stick to the ItemStruct approach, it is probably the case now that the different instances of the merchants now have copies of the same ItemStructs.
If you raised one eyebrow already, prepare to raise the other, because there is a very high chance that your game designer will, at some point, try to balance the game and give a discount to all low-level potions. Since the ItemStructs are not connected to each other, every instance of every merchant that sells them needs to be updated. Applause from me if you are already using prefabs and prefab variants, but they all still need updating.
Buying is one of the difficulties with programming these items, but consider that the game also has combat. Defeating enemies yields loot, such as potions.
It should be clear that the solution we are looking for revolves around detaching the ItemStruct from the merchants (and other mechanisms) and letting each one of them use a reference to it.
Thus, any change made to the ItemStruct will immediately affect those who refer to it. Gladly, Unity offers an easy way of doing that: scriptable objects.
Scriptable objects are data-holding objects in Unity that do not need to be attached to game objects.
Thus, we can create them without adding them to an object in our scene or a prefab. They exist on their own and any entity that uses them uses a reference.
Scriptable objects do not inherit from MonoBehavior, the default class for Unity scripts, but they do hold some analogous behaviors and can be used in a similar fashion.
For instance, they have their Awake call, as well as OnEnable and OnDisable. They can have their methods and member variables, as well as benefit from using Attributes.
Moreover, they are as easy to create as regular mono behavior scripts:
[CreateAssetMenu(fileName = "New Item", menuName = "Item", order = 0)] public class ItemScriptableObject : ScriptableObject { public string name; public int price; [TextArea] public string description; public Sprite image; }
As you saw in the example above, to increase the efficiency of creating instances of a scriptable object, we can use the CreateAssetMenuAttribute
to add an editor menu that generates files of that type.
Getting back to the problem we discussed previously with detaching the ItemStructs from the elements in the game, a simple solution now is to replace ItemStructs with the ItemScriptableObject. Every merchant holds a list of the ItemScriptableObjects it sells.
Any discounts or increases in the prices of an item are now done to the item itself. The change is immediately reflected in any entity that refers to it. Let the game designer now rejoice in balancing the game economy.
Furthermore, other mechanisms, such as item drops from monsters or looting treasure chests, benefit just the same. You could even take this further and use the same scriptable objects to devise an inventory system or a crafting system.
The scriptable object we designed as an example can be easily extended to contain other data, such as the image it displays on the UI or a crafting type.
On that, there are two exciting extensions we can do with scriptable objects that are worth mentioning: Inheritance and CustomEditorScripts.
Let us start with Inheritance. Scriptable objects inherit from the Object base class; thus, they can be used in any context a regular C# class can be used.
That means we can have abstract scriptable objects and use them to model behavior, benefiting from all those good old object-oriented programming advantages such as data abstraction and polymorphism.
For example, we could have an abstract scriptable object for the items in our game, which hold standard information such as name, cost, and display image. It would also contain an abstract method Use( )
. Then each item type can specify what it does when it is used.
Then, we could create a concrete scriptable object for the Potions, which extends Item. The Potions scriptable object inherits the member variables from its parent class and is forced to implement the Use
method.
From here we could even branch the hierarchy further by creating a HealthPotion extension or a RejuvenationPotion so that the Use
method has a more precise implementation and we add member variables that better suit the needs of each type.
You might have now stopped to wonder why we could not have done that before as this is a standard object-oriented programming practice.
The fact is that we always could and that is the beauty of using scriptable objects: we do not need to change how we model our systems. The advantage now is that we can create instances for our data detached from the systems themselves.
Moreover, we can implement our systems to be based on Items, as we noted, even though our game designer is now adding the data for the LegendarySwordScriptableObject type.
public abstract class ItemScriptableObject : ScriptableObject { public string name; public int price; [TextArea] public string description; public Sprite image; public abstract void Use(); }
[CreateAssetMenu(fileName = "New Potion", menuName = "Items/Potion", order = 0)] public class PotionScriptableObject : ItemScriptableObject { public override void Use() { //Do Something } }
The other aspect I raised before was empowering scriptable objects with custom editor scripts. Custom editor scripts allow us to change and add behaviors to how a certain entity is displayed in the Unity Inspector.
They are commonly used in regular MonoBehavior scripts to add buttons or to display labels, and generally to speed up development.
ScriptableObjects can also be extended with their own custom editor scripts, which makes them powerful edit tools directly in the inspector and, again, detached from other entities.
For example, we suggested that our Item scriptable object could contain its display image. With custom editor scripts, we could show the selected image directly in the inspector to facilitate checking if the correct sprites are being used.
Specifically for the potions, we could add a button that prints the result of using the potion given its attributes, to quickly assess if its outcomes are appropriate.
[CustomEditor(typeof(PotionScriptableObject))] public class PotionScriptableObjectEditor : UnityEditor.Editor { public override void OnInspectorGUI() { base.OnInspectorGUI(); var potionScriptableObject = (PotionScriptableObject) target; if (potionScriptableObject.image == null) return; GUILayout.Box(potionScriptableObject.image.texture); if (GUILayout.Button("Test Use")) { potionScriptableObject.Use(); } } }
(Note: The sprites used here are originally from the 2D Mega Pack from Brackeys)
Adjusting the values of a scriptable object, using a custom editor script or not, has another advantage compared to regular MonoBehavior scripts: they are persistent between editor and play mode.
This means that if you alter a scriptable object while testing the game in play mode, the changes are saved to the scriptable object and persist in the editor mode.
Notice that this is not the case for persistency between instances of a built game, i.e., you should not use scriptable objects as an alternative to your save system.
Moreover, changes made in scriptable objects also persist between scenes. This advantage allows for a better alternative to singletons when transferring information between scenes and game states.
For example, you could store your player’s information such as health and experience in a scriptable object and have it be referenced by the multiple systems in your game.
If your character loses health in a combat scene and then returns to the world map, the changes to the health are persisted in the scriptable object and do not have to be shared via other mechanisms, such as a PlayerManager flagged to not be destroyed on loading new scenes.
However, there is even another use for scriptable objects that sounds quite counter-intuitive: data-less scriptable objects.
Scriptable object instances are unique by default since each of them will hold a GUID.
Thus, multiple references to an instance of a scriptable object are bound to use the correct one, especially since they are most likely linked in the inspector to their corresponding reference holders.
On the other hand, we could also benefit from this property to compare scriptable objects, either by their content (overriding the Equals method, for example) or by their identifiers.
This also allows them to be excellent keys for dictionaries and other key-based data structures.
As I hinted previously, we could use scriptable objects solely for the purpose of being keys, but maintaining all of their benefits. Consider, for example, that for the global market system in your game, you want to specify a dictionary that links merchants to their corresponding cities.
Every merchant can only be linked to one city and cities can host multiple merchants. What would you use to make the link between merchant and city? A string? An enum?
As you might have guessed, a scriptable object CityScriptableObject, for example, could easily solve this dilemma.
Let’s discuss the alternatives first.
A string is bound to typos and other mistakes, besides being very inefficient for maintenance and updates (a city changed its name mid-development due to an argument between the game writers, for example). Comparing strings is also not very efficient.
An enum works very well, but changes in the code base need to be done for every new addition. Moreover, it can be catastrophic if you remove one entry in the enum and have to fix all of the code that references it (a city was destroyed now due to another argument between the game writers — those folks!).
On the other hand, the scriptable object instance can be created in the Project View (no code is necessary), and it can be referenced as a regular object in the code, instead of relying on switch statements and other control flow structures that specify the possible entries.
As presented before, this same scriptable object can be expanded further by just adding new fields and methods. If we need a key to refer to a city, our scriptable object can be empty and use the asset’s file name itself as the key (it has a GUID anyway).
But, if later we want the merchant to use its city name in the user interface, we can easily add that as a property inside the CityScriptableObject code and that has no collateral effect on any other part of the code.
If you are thinking now about how this seems quite an unorthodox usage of scriptable objects, remember that this practice was initially proposed by Richard Fine, an employee of Unity Technologies itself, and has been validated by many other developers in the community.
Implementing a system that uses scriptable objects instead of enums is not much different than implementing a system that uses a self-made key system.
Entities will hold references to the scriptable objects and systems will consume these scriptable objects for comparison and even use their own member methods. Remember our Use
method? The excerpt of the code below should help you envision a possible approach to this strategy.
[UnityEngine.CreateAssetMenu(fileName = "New Magic Element", menuName = "Magic/Magic Element", order = 0)] public class MagicElementScriptableObject : UnityEngine.ScriptableObject { //Nothing }
public class MagicPotionScriptableObject : PotionScriptableObject { public MagicElementScriptableObject magicElement; public int increaseBy; public override void Use() { character.IncreaseMagicElementPower(magicElement, increaseBy); }
public class Character { public Dictionary<MagicElementScriptableObject, int> magicElements; public void IncreaseMagicElementPower (MagicElementScriptableObject magicElement, int value) { magicElements[magicElement] += value; } }
With their great flexibility and simplicity, scriptable objects offer many new approaches to systems, persistency, data storage, and even as substitutes for singletons and enums.
As stated before, due to their inheritance, their behavior is similar to many other components we are already used to, which makes the process of starting to use them more approachable.
Surely, all good things come at a cost — some of the approaches discussed before could also be implemented with different techniques that might achieve a higher level of performance or stability for the code base.
The flexibility of using scriptable objects to replace enums might be too unorthodox and yield diminishing returns after your game is already well established. It can be then time to perform an inverse substitution and get enums back for their advantages.
Notice that this is likely to happen only after long brainstorming and play test sessions have been done during the prototyping stages.
In terms of performance, scriptable objects behave just like a C# object. Their performance impact could be perceived during their serialization while starting the game and deserialization while saving them to a disk.
However, since they are essentially implemented as YAML files, the cost of both operations is essentially the same as if you were doing that with a regular JSON file. In a sense, using scriptable objects will be as performance heavy as most of your other strategies for data-driven development.
It is worth mentioning that if you are consistently saving data to a scriptable object and you want it to be saved to disk immediately, you might need to call the Unity API through the AssetDatabase.SaveAssets
call, for example.
Besides all of that, there is still plenty of room for experimentation and design. Vu Dinh Pham, in his work Game Architectures in Unity Projects, presents Scriptable Objects as useful tools in Runtime Sets, which help keep track of information among entities by sharing the same scriptable object, abolishing the need for an intermediary singleton.
In the context of mixed reality (MR), Bovo, Giunchi, Steed, and Heinis developed a toolkit for MR which uses scriptable objects as a layer to code sequences, instructions, and even questionnaires for a training tool.
Increase the freedom and versatility of your game design and speed up your prototype stage. The worst thing that can happen is that, after your game is stable and fun, you might need to refactor some parts of the system, which undoubtedly is way better than optimizing a framework for a boring or uninteresting game.
Thanks for reading and let me know if you would like more unorthodox Unity strategies for fast development and prototyping. You can see more of my work here.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]