-
Notifications
You must be signed in to change notification settings - Fork 16
Scriptable Architecture
Version 3+ dont using fully scriptable achirtecture. You can still see the shadow of the scriptable event or scriptable const is still retained
Scriptable Objects are an immensely powerful yet often underutilized feature of Unity. Learn how to get the most out of this versatile data structure and build more extensible systems and data patterns. In this talk, Schell Games shares specific examples of how they have used the Scriptable Object for everything from a hierarchical state machine, to an event system, to a method of cross scene communication.
Leverages the power of scriptable objects, which are native objects from the Unity Engine. Scriptable objects are usually used for data storage. However, because they are assets, they can be referenced by other assets and are accessible at runtime and in editor. See GDC talk of Ryan Hipple
It was useful mostly for and while making Casual and Hyper casual game. Because the nature of these games is simple and the power of ScriptableObject is flexibility. For more complex games it can be hindered by the complexity spike due to the amount of scriptables generated and easy to lose control, but this can still be avoided by simply applying apply it to the part of the game where you want to reduce dependencies.
Avoid coupling your classes by using a reference to a common Scriptable object as a bridge or by using Scriptable Events. This is particularly true in the Hypercasual market for several reasons:
- Many features are enabled/disabled through independent AB Tests. Therefore, we want to avoid noisy code and hardcoding our features into the core of our game. Having them “hooked” with an independent architecture makes it easy to add or remove them.
- Because you make a lot of games quickly, you want to reuse features as much as you can. Therefore, having features as “drag and drop” can be a huge time saver over the long run for things that generally don’t change too much across games
- Subscribe to what you need and have each class handle itself.
- Reduce code complexity.
- Avoid useless managers. Avoid creating new classes for simple behaviors
- Be able to debug visually and have the game react in real time
A scriptable variable is a scriptable object of a certain type containing a value. Example string variable look like:
-
Category
: works similar to GameObject's Tag, You can create categories inside the scriptable wizard window -
Show In Wizard
: Open Scriptable Wizard and select this scriptable -
Value
: the current value of the variable. Can be changed in inspector, at runtime, by code or in unity events. Changing the value will trigger an event “OnValueChanged” that can be registered to by code. -
Debug Log Enabled
: if true, will log in the console whenever this value is changed. -
Saved
: If true, the value of the variable will be saved toData
when it changes.
-
Default Value
: If "Saved" is true, then you can set a default value. This is used the first time you load fromData
if there is no save yet. -
Guid Create Mode
: If "Saved" is true, then you can chooseMode
createguid
Auto
orManual
, If set toAuto
then theguid
value will be automatically generated usingSystem.Guid.NewGuid()
-
Reset On
: When is this variable reset (or loaded, if saved)?-
Scene Loaded
: Whenever a scene is loaded. Ignores Additive scene loading. Use this if you want the variable to be reset if it is only used in a single scene. -
Application Start
: Reset once when the game starts. Useful if you want changes made to the variable to persist across scenes. -
AdditiveSceneLoaded
: Whenever a scene loaded byLoadSceneMode.Additive
. Use this option for compatibility with the use ofLoadSceneMode.Additive
instead ofLoadSingle
scene introduced in scene flow, to keep the variable's value reset behavior similar toSceneLoaded
. If you are not using a flow load scene like in scene flow or you are not sure how to reset the value when the load scene is adaptive, do not use this option.
-
-
Is Clamped
: Specific toFloatVariable
andIntVariable
, gives you the ability to clamp it if you need. -
Reset to initial value
which lets you quickly reset the value of the variable to the initial value.
- Notes: In the Editor, ScriptableVariables automatically reset to their initial value (the value in the inspector before entering play mode) when exiting play mode.
To create a new variable, access via menu Create
> Pancake
> Scriptable
> ScriptableVariables
Or via short-cut Alt + 1
Or use button create in property to create a new instance of that scriptable variable in the folder (_Root/Scripts/Generated
).
When you are in play mode, the objects (and their component) that have registered to the OnValueChanged
Event of a scriptable variable are display in the inspector.
As you see 1 object registered to this variable. Object name is Main Camera
and script is Sample
Lists are useful to avoid need a manager hold that list and we must access to manager to get list (high-coupling).
-
Reset On
: When is this list cleared?-
Scene Loaded
: whenever a scene is loaded. Ignores additive scene loading -
Application Start
: Reset once when the game starts -
AdditiveSceneLoaded
: Whenever a scene loaded byLoadSceneMode.Additive
. Use this option for compatibility with the use ofLoadSceneMode.Additive
instead ofLoadSingle
scene introduced in scene flow, to keep the variable's value reset behavior similar toSceneLoaded
. If you are not using a flow load scene like in scene flow or you are not sure how to reset the value when the load scene is adaptive, do not use this option.
-
In play mode, you can see all the elements that populate the list.
In the Editor, ScriptableLists automatically clear themselves when exiting play mode
You can create Scriptable List via menu Create
> Pancake
> Scriptable
> ScriptableLists
This ScriptableObject-based event system is our solution to create a solid game architecture and make objects communicate with each other, at the same time avoiding the use of the Singleton pattern.
The reasons why we want to avoid using Singletons are multiple. They create rigid connections between different systems in the game, and that makes it so they can't exist separately and they will always depend on each other (i.e. SystemA always requires SystemB in the scene to work, and so on). This is of course hard to maintain and reuse, and makes testing individual systems harder without testing the whole game.
How it works At the base of the system we have a series of ScriptableObjects that we call "Event Channels". They act as channels (like a radio) on which scripts can "broadcast" events. Other scripts can in turn listen to a specific channel, and they would pick that event up, and implement a reaction (callback) to it. The graph at the top of this page visualises this structure.
Both the script that fires the event and the event listener are Monobehaviours. Since we are using the ScriptableObject Event Channel (which is an asset) to connect the 2 systems, those Monobehaviours can live in 2 different scenes in a completely independent way.
For instance, an event can be raised when pressing a button, and broadcasted on a "Button_X_Pressed" Event Channel ScriptableObject. On the other hand, we can have one or more objects listening to this event and react with different functionality: one of them spawns some particles, one plays a sound, one starts a cutscene.
You can create Scriptable Event via menu Create
> Pancake
> Scriptable
> Scriptable Event
-
There are two types of scriptable events:
- Event with out param
- Event with param (int, bool, float, string, vector2 ...)
Events can be triggered by code, unity actions or the inspector (via the raise button).
Raising events in the inspector can be useful to quickly debug your game.
When you in play mode, you can see all object that registered to that event:
To listen to these events when they are fired, you need to attach an Event Listener component
(of the same type) to your GameObjects
-
Binding
-
UNTIL_DESTROY
: Will register inAwake()
and unsubscribeOnDestroy()
-
UNTIL_DISABLE
: Will register inOnEnable()
and unsubscribeOnDisable()
-
-
Disable after subscribing
: If true, will deactivate the GameObject after registering to the event. Useful for UI elements -
Event Respones
: here you can add multiple events and trigger things with unity events when they are fired. You can also register to events directly from code. -
Delay
: Time in seconds delay to call response
As the saying goes No Silver Bullet
. ScriptableObject architecture will solve coupling problems but it is not perfect in itself and has its own weaknesses.
Because ScriptableObject on Editor is also an asset like other assets: prefab, texture, material .. so I will also see the same "diseases" of assets.
- Reference count : oh, does anyone use this file, I've been here for a long time but I'm too scared to delete it
- Dependency : well do these ScriptableObject have any objects on the next scene ref? then is there any prefab that uses it, and then the ScriptableObject itself refs to other assets?
To solve this problem we can use the reference search tools in unity.
Inside heart also has a built-in tool called Finder
you can open it with the keyboard shortcut ctrl + shift + k
.
The strength of UnityEngine is its Serialization, ie the abstraction between script and data. Binding data to the script's behavior is done by unity at runtime.
Ex: You write scripts, reference to prefab, in runtime spawn prefab to use
- From the script, you only know that it will spawn, but who "injects" it, you don't know, just drag and drop the correct reference into the editor and use it.
- From the Editor you won't know who the prefab is used by.
Over time the number of scriptables created is increasing. features update more, there will be many Events, old data is no longer used but new data still needs to be created for new feature.
And sometimes the project is developed by more than one developer, each person will take care of a feature, they need event, data.. but didn't know it was already there, he continued to create a new one.
Actually this is not a scriptable object's own problem
Programs can be severely stateful when the number of variables is too large and perform conditional forking for these variables. Looks a bit like the state machine in animator.
In essence, at runtime, ScriptableObject is also a C# object. It is created in a late-binding style (only if anyone uses it to create it, if not), and it will still be collected by GC if no one else refs to it.
Suppose we have 3 scenes A, B and C
-
SceneA
: In SceneA we have a GameObjectA that references the ScriptableObject and changes its value, there is a button to switch to SceneB -
SceneB
: There is nothing in SceneB but only a button to switch to SceneC -
SceneC
: In SceneC we have a GameObjectC that references the ScriptableObject in SceneA and logs out its current value.
A strange behavior happens here, to be more specific we have a ScriptableObject declared as follows
[CreateAssetMenu]
public class DemoScriptable : ScriptableObject
{
public int value;
}
We create the scriptable from DemoScriptable and name it Coin.asset
and change value default is 100.
In SceneA We reference to Coin.asset and decrease 10 value
The problem is that when going to SceneC
the log output value is reset to default 100 instead of 90 after 10 is subtracted in SceneA
(Value of your scriptable is reset to the original state, it no longer contains the update state from sceneA).
And more terrible is that it is not on the Editor, but only on the build in device.
Actually it happens quite randomly sometimes it's 100, sometimes it's 90
What this happens is is the transition to SceneB where the ScriptableObject coin is not being used by any Objects in this scene (no longer referencing the scriptable coin) resulting in the GC releasing the state of the scriptableObject which is no longer in use.
The built-in implementation of the scriptable architecture in heart already provides a ResetOn option to define this behavior more explicitly to help you avoid unexpected behavior that occurs on a real device differently than on the editor.