1K Views
May 08, 17
スライド概要
講演者:イアン・ダンドア(Unity Technologies)
こんな人におすすめ
・開発のイテレーションとワークフローを改善したい中級~上級レベルのプログラマー
受講者が得られる知見
・ScriptableObjectの内部的な仕組み
・ScriptableObjectを効果的に使用するためのワークフロー
講演動画:https://youtu.be/NawYyrB_5No
リアルタイム3Dコンテンツを制作・運用するための世界的にリードするプラットフォームである「Unity」の日本国内における販売、サポート、コミュニティ活動、研究開発、教育支援を行っています。ゲーム開発者からアーティスト、建築家、自動車デザイナー、映画製作者など、さまざまなクリエイターがUnityを使い想像力を発揮しています。
Ian Dundore Lead Developer Relations Engineer, Unity
Scriptable Objects What 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 C# Object (Serialization, InstanceID) (Your Code)
A Wild Reference Appears! Native Object C# Object (Serialization, InstanceID) (Your Code) C# Reference
After Destroy() X Native Object C# Object (Serialization, InstanceID) (Your Code) C# Reference
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!