【unite 2017...
TRANSCRIPT
Ian DundoreLead Developer Relations Engineer, Unity
Scriptable ObjectsWhat They Are & Why To Use Them
Scriptable Objects?
• “A class, derived from Unity’s Object class, whose references and fields can be serialized.”
• That statement is deceptively simple.
Well, what’s a MonoBehaviour?
• It’s a script.
• It receives callbacks from Unity.
• At runtime, it is attached to GameObjects.
• Its data is saved into Scenes and Prefabs.
• Serialization support; can be easily viewed in the Inspector.
Okay, what’s a ScriptableObject?
• It’s a script.
• It doesn’t receive (most) callbacks from Unity.
• At runtime, it is not attached to any specific GameObject.
• Each different instance can be saved to its own file.
• Serialization support; can be easily viewed in the Inspector.
It’s all about the files.
• MonoBehaviours are always serialized alongside other objects • The GameObject to which they’re attached • That GameObject’s Transform • … plus all other Components & MonoBehaviours on the GameObject
• ScriptableObjects can always be saved into their own unique file.
• This is makes version control systems much easier to use.
Shared Data should not be duplicated
• Consider a MonoBehaviour that runs an NPC’s health. • Determines current & max health. • Changes AI behavior when health is low.
• Might look like this
public class NPCHealth : MonoBehaviour { [Range(10, 100)] public int maxHealth;
[Range(10, 100)] public int healthThreshold;
public NPCAIStateEnum goodHealthAi; public NPCAIStateEnum lowHealthAi;
[System.NonSerialized] public int currentHealth; }
Problems
• Changing any NPC in a Scene or Prefab? • Tell everyone else not to change that scene or prefab!
• Want to change all NPCs of some type? • Change the prefab (see above). • Change every instance in every Scene.
• Someone mistakenly edits MaxHealth or HealthThreshold somewhere? • Write complex content-checking tools or hope QA catches it.
public class NPCHealthV2 : MonoBehaviour { public NPCHealthConfig config;
[System.NonSerialized] public int currentHealth; }
[CreateAssetMenu(menuName = "Content/Health Config")] public class NPCHealthConfig : ScriptableObject { [Range(10, 100)] public int MaxHealth;
[Range(10, 100)] public int HealthThreshold;
public NPCAIStateEnum GoodHealthAi; public NPCAIStateEnum LowHealthAi; }
[CreateAssetMenu] ?
• Adds this to your “Create” menu:
Use Case #1: Shared Data Container
• ScriptableObject looks like this
• MonoBehaviour looks like this
Benefits
• Clean separation of concerns.
• Changing the Health Config changes zero other files. • Make changes to all my Cool NPCs in one place.
• Optional: Make a custom Property Drawer for NPCHealthConfig • Can show the ScriptableObject’s data inline. • Makes designers’ lives easier.
Potential benefit
• Editing ScriptableObject instances during play mode? • No problem!
• Can be good — let designers iterate while in play mode. • Can be bad — don’t forget to revert unwanted changes!
Extra bonus
• Your scenes and prefabs now save & load faster.
Unity serializes everything
• When saving Scenes & Prefabs, Unity serializes everything inside them.
• Every Component. • Every GameObject. • Every public field.
• No duplicate data checking. • No compression.
More data saved = slower reads/writes
• Disk I/O is one of the slowest operations on a computer. • Yes, even in today’s world of SSDs.
• A reference to a ScriptableObject is just one small property.
• As the size of the duplicated data grows, the difference grows quickly.
Quick API Reminder
Creating ScriptableObjects
• Make new instances: • ScriptableObject.CreateInstance<MyScriptableObjectClass>(); • Works both at runtime and in the Editor.
• Save ScriptableObjects to files: • New asset file: AssetDatabase.CreateAsset(); • Existing asset file: AssetDatabase.AddObjectToFile(); • Use the [CreateAssetMenu] attribute, like before. • (Unity Editor only.)
ScriptableObject callbacks
• OnEnable • Called when the ScriptableObject is instantiated/loaded. • Executes during ScriptableObject.CreateInstance() call. • Also called in the Editor after script recompilation.
ScriptableObject callbacks (2)
• OnDestroy • Called right before the ScriptableObject is destroyed. • Executes during explicit Object.Destroy() calls, after OnDisable.
• OnDisable • Called when the ScriptableObject is about to be destroyed. • Executes during explicit Object.Destroy() calls, before OnDestroy. • Executed just before Object is garbage-collected!
• Also called in the Editor before script recompilation.
ScriptableObject lifecycle
• Created and loaded just like other assets, such as Textures & AudioClips.
• Kept alive just like other assets.
• Will eventually get unloaded: • Via Object.Destroy or Object.DestroyImmediate • Or, when there are no references to it and Asset GC runs • e.g. Resources.UnloadUnusedAssets or scene changes
Warning! Unity is not a C# Engine.
• ScriptableObjects, like other UnityEngine.Object classes, lead a dual life.
• C++ side manages serialization, identity (InstanceID), etc. • C# side provides an API to you, the developer.
Native Object (Serialization, InstanceID)
C# Object (Your Code)
Native Object (Serialization, InstanceID)
C# Object (Your Code)
C# Reference
A Wild Reference Appears!
Native Object (Serialization, InstanceID)
C# Object (Your Code)
C# Reference
After Destroy()
X
Common Scenarios
Plain Data Container
• We saw this earlier.
• Great way to hold design data, or other authored data. • For example, use it to save your App Store keys. • Bake data tables in expensive formats down to ScriptableObjects. • Convert that JSON blob or XML file during your build!
Friendly, Easy-to-Extend Enumerations
• Use different instances of empty ScriptableObjects to represent distinct values of the same type.
• Basically an enum, but turns into content.
• Consider, for example, an RPG Item…
class GameItem: ScriptableObject { public Sprite icon; public GameItemSlot slot;
public void OnEquip(GameCharacter c) { … } public void OnRemove(GameCharacter c) { … }
}
class GameItemSlot: ScriptableObject {}
It’s easy.
• Slots are just content, like everything else.
• Designers can add new values with no code changes.
Adding data to existing content is simple.
• Can always add some fields to the GameItemSlot class.
• Maybe we want to add some types of items the user can’t equip. • Just add a bool isEquippable flag to existing GameItemSlot class
Let’s add behavior!
class GameItem: ScriptableObject { public Sprite icon; public GameItemSlot slot; public GameItemEffect[] effects;
public void OnEquip(GameCharacter c) { // Apply effects here…?
}
public void OnRemove(GameCharacter c) { // Remove effects here…?
} }
class GameItemEffect: ScriptableObject { public GameCharacterStat stat; public int statChange; }
Should GameItemEffect just carry data?
• What if designers want to do something other than just add stats? • Every effect type’s code has to go into GameItem.OnEquip
• But ScriptableObjects are just classes…
• Why not embed the logic in the GameItemEffect class itself?
abstract class GameItemEffect: ScriptableObject { public abstract void OnEquip(GameCharacter c); public abstract void OnRemove(GameCharacter c);
}
class GameItemEffectAddStat: GameItemEffect { public GameStat stat; public int amountToAdd;
public override void OnEquip(GameCharacter c) { c.AddStat(statToChange, amountToAdd);
}
public override void OnRemove(GameCharacter c) { c.AddStat(statToChange, -1 * amountToAdd);
} }
class GameItemEffectTransferStat: GameItemEffect { public GameStat statToDecrease; public GameState statToIncrease; public int amountToTransfer;
public override void OnEquip(GameCharacter c) { c.AddStat(statToReduce, -1 * amountToAdd); c.AddStat(statToIncrease, amountToTransfer);
}
public override void OnRemove(GameCharacter c) { c.AddStat(statToReduce, amountToAdd); c.AddStat(statToIncrease, -1 * amountToTransfer);
} }
class GameItem: ScriptableObject { public Sprite icon; public GameItemSlot slot; public GameItemEffect[] effects;
public bool OnEquip(GameCharacter c) { for(int i = 0; i < effects.Length; ++i) {
effects[i].OnEquip(c); }
}
public bool OnRemove(GameCharacter c) { … } }
Nice editor workflow!
Serializable game logic!
• Each effect now carries only the data it needs.
• Applies its operations through a simple (testable?) interface.
• Designers can drag & drop everything.
• Add new logic without refactoring existing content.
Serializable… delegates?
• Consider a simple enemy AI, with a few different types of behavior.
• Could just pack this all into a MonoBehaviour, use an enum or variable to determine AI type.
• Or…
class GameNPC: MonoBehaviour { public GameAI brain;
void Update() { brain.Update(this);
} }
abstract class GameAI: ScriptableObject { abstract void Update(GameNPC me);
}
class PassiveAI: GameAI { … }
class AggressiveAI: GameAI { … }
class FriendlyAI: GameAI { … }
Easier to implement, extend & test
• Imagine our designers, later on, wanted to add an AI that would attack you when you attacked one of its friends.
• With this model: • Add a new AI type • Allow the designer to define an array of friends. • When one is attacked, set the current AI module to an AggressiveAI.
• No changes to other code or content needed!
So in sum…
ScriptableObjects are great!
• Use them to make version control easier.
• Use them to speed up data loading.
• Use them to give your designers an easier workflow.
• Use them to configure your logic via content.
Thank you!