6
\$\begingroup\$

I am trying to understand the logic and functionality behind Scriptable Objects. I 've watched the provided video 2 times and I keep getting lost. I tried to follow the code as well but with no luck.

Can someone please explain to me what the Scriptable Objects are and how they work in a few lines?

[EDIT:] This Video as well as the following answers helped me understand Scriptable Objects and even better incorporate them in my own game.

\$\endgroup\$
13
\$\begingroup\$

ScriptableObjects are just data... except that they look like assets (drag-and-droppable on objects, contained in "Assets", etc).

Consider this code:

class ItemShop
{
   ItemList MyAvailableItems;
}

Now, for every ItemShop instance you create, MyAvailableItems contains a unique ItemList. Even if every ItemShop sells the same set of items, they will each contain their own unique copies. If one instance of the "shared" list is changed, it will NOT propagate back to the original ItemList source.

If ItemList was a ScriptableObject, you could create 100 ItemShops and drag the same ItemList (from Assets) onto them all. Every instance REFERENCES the original (one and only) ItemList that you assigned. There is never more than one copy of the ItemList in memory.

Additionally, by accessing an entire ItemList through a single reference, you can change the items available at the ItemShop by swapping in a different reference (one line of code (or a drag and drop, if in editor)).

Be aware:

While you are designing your game, you have access to the Editor namespace. That allows you to create and/or modify ScriptableObjects (at run-time) and then persist those changes back into the actual assets. This lets you run the game, actually visit "tier 3", actually visit the actual item shop, actually change the actual list (at run-time) and then "save" the changes, permanently.

Once built, the Editor namespace is lost as well as the ability to persist changes. For that reason, ScriptableObjects are NOT for saving/loading of run-time-generated data in built games; this is a common misunderstanding.

Edit:

//Data
//(De)serialization is probably your next stop
[System.Serializable] //This will become clear then
class AIBehaviorData //or struct
{
   public float AttackPriority = 0.5f;
   public float DefensePriority = 0.5f;
}

//Implementation
abstract class AIBehavior : ScriptableObject
{
   public bool SomeFlag = false;
   public void Update(AIBehaviorData instanceData) = 0;
}
class TurtleAI : AIBehavior 
{
   public override void Update(AIBehaviorData instanceData)
   {
      //Obviously, no need to assign these over and over...
      //Just example data
      instanceData.AttackPriority = 0.3f;
      instanceData.DefensePriority = 0.7f;
      ... //turtle stuff
   }
}
class AggressiveAI : AIBehavior 
{
   public override void Update(AIBehaviorData instanceData)
   {
      //Obviously, no need to assign these over and over...
      //Just example data
      instanceData.AttackPriority = 0.8f;
      instanceData.DefensePriority = 0.2f;
      ... //aggressive stuff          
   }
}

//Dummy wrapper
class AI_NPC : MonoBehavior
{
   public AIBehaviorData MyInstanceData;

   public AIBehavior MyPersonality; //Public access, visible in inspector, drag-and-drop

   //[HideInInspector]
   //public AIBehavior MyPersonality; //Public access, but not visible in inspector
   //Probably assigned by "AIManager" while creating AI instances

   //private AIBehavior MyPersonality; //Must initialize locally with Awake/Start/OnEnable

   public void Update()
   {
      //Modify my data based on my personality
      if (MyPersonality.SomeFlag)
        MyInstanceData.AttackPriority++;

      //or

      //My personality modifies my data
      if (MyPersonality != null)
        MyPersonality.Update(MyInstanceData);
   }
}

--------------------------------------------------
**Editor namespace**
//Will not build if in same file
using UnityEditor;
//These make your objects really come to life (in the editor at least)
[CustomEditor(typeof(AI_NPC))]
class AI_NPC_Editor : Editor
{
   void OnInspectorGUI()
   {
      serializedObject.Update();
      //This is where the inspector panels are assembled
      AI_NPC inspectedAI = (AI_NPC)target;
      if (inspectedAI.MyPersonality is TurtleAI) //polymorphism
      {
         //Display "turtle" properties (data is in same struct either way)
         ...
         inspectedAI.MyInstanceData.SomeFlag = 
            EditorGuiLayout.Toggle(inspectedAI.MyInstanceData.SomeFlag); //checkbox
      }
      else if (inspectedAI.MyPersonality is AggressiveAI)
      {
         //Display "aggressive" properties (data is in same struct either way)
         ...
      }
      serializedObject.ApplyModifiedProperties();
   }
}

Now you can drag-and-drop personalities onto AI's. One copy of each is resident in memory at any time, regardless of how many times it is referenced.


Edit:

The ScriptableObject asset will be "reset to factory", every time the game loads. If you go to the definition of ScriptableObject, you will find that it inherits from Object which means you can Instantiate<>() (clone) them. Those clones can be overwritten with run-time data, loaded from disk. The class/struct that holds the item-counts for a particular shop can be a ScriptableObject, that references a specific ItemList (a ScriptableObject). In that case, a quantity means "there are ten ItemList[0]'s available at this shop, right now" (the item list can be swapped out).

If the majority of this answer and ScriptableObject makes sense to you, move on to serialization. When you have the save/load framework in place, "where things belong" should become more obvious; you'll probably find that the quantities belong within the ItemShop instance, not the ItemList (an ItemShop "has" an ItemList and item-counts, an ItemList "has" only items). An InventoryList (class/struct or ScriptableObject) could reference an ItemList (ScriptableObject) and contain quantities. Both can be dynamically modified at run-time during save/load.

Based on where they intuitively belong, "should I use a ScriptableObject here?" will also be more obvious. If each ItemShop has its' own item-count list, there's really nothing to reference; since you HAVE to have multiple instances of the class/struct, the clear benefit of using a ScriptableObject instead is diminished. If multiple ItemShops can "share inventory", then ScriptableObject may be the way to go. You have to consider the size of one class/struct instance, as well as how many identical copies you will be making.

From the tutorial, the "list of 1000 int's" (Int32) would occupy approximately 32 KB of memory, so ten copies of that list would occupy approximately 320 KB. Depending on the platform you are developing for, the "excessive memory usage" may not be worth fixing. My PC has about 6,291,456 KB (6 GB) of memory, so 320 KB is 0.005% of my total memory that I don't mind you using. But, if your game can't run on my preferred device (an older Android), I can't buy it.

Some ItemShops sharing, and some not, is probably your "worst case", in terms of complexity:

[Serializable]
class QuantityData
{
   public ItemList Items;
   ... //Probably a few List<int> representing min/max/current
}
class ScriptableQuantityData : ScriptableObject
{
   public QuantityData MyQuantityData;
}
class ItemShop : MonoBehavior
{
   public QuantityData MyQuantityData = null;
   public ScriptableQuantityData MyScriptableQuantityData = null;

   void DoSomething(int itemID)
   {
      if (MyScriptableQuantityData != null) //Shared inventory
      {
         //MyScriptableQuantityData.MyQuantityData.Items[itemID]
         ...
      }
      else if (MyQuantityData != null) //Private inventory
      {
         //MyQuantityData.Items[itemID]
         ...
      }
      else
      {
         Debug.Log("Error: no QuantityData assigned");
         //throw new exception("Error: no QuantityData assigned");
      }
   }
}

Regardless of how you end up layering the data structures, you'll have to provide the (de)serialization mechanism for anything you want to include in save/load. Classes derived from ScriptableObject are not [Serializable] so you'll need to add enough information for saving so you can reconstruct the objects during load.

It is easy to drag-and-drop ScriptableObjects in the Editor. How does an ItemShop, loaded from disk, know which of the QuantityDatas, also loaded from disk, belongs to this shop? Do they have names or unique ID's? You can count on ItemList[0] being the same list, time after time, as long as you load/save the same way every time.

Serialization is going to drive most, if not all, of your decision-making from now on.

(I also agree that the tutorial falls short in some critical way, although I can't say precisely how)

\$\endgroup\$
  • \$\begingroup\$ So the ItemList would only have one instance accessed by every Shop. So if in that list there are 5 Potions and one of the shops sells one, does that mean all other shops would show 4 available? \$\endgroup\$ – Valamorde Apr 17 '16 at 15:32
  • \$\begingroup\$ @Karaflakos, check out the edit. \$\endgroup\$ – Jon Apr 18 '16 at 12:40
  • \$\begingroup\$ Hmm, I like your answer a lot, it is very thorough and it helped me to finally catch the idea of SO but I am not yet sure if I need it in my game. As your personal opinion, would you prefer SO to create an item "database" or would you use another method? \$\endgroup\$ – Valamorde Apr 18 '16 at 21:32
  • \$\begingroup\$ @Karaflakos, for a single, constant, game-wide "database", I'd start with SO. I wouldn't want to spend more time making an elaborate database editor (not even available in builds) than I do on the game, so I would use a custom editor for my SO to add save/load buttons, and then modify the database externally, using Excel. At that point, the only real benefit of SO is drag-and-drop since you could provide "global access" to the same dataset using a regular class/struct singleton and still avoid duplicating the data. (It is relatively easy to change your mind later) \$\endgroup\$ – Jon Apr 18 '16 at 22:26
  • \$\begingroup\$ Ok, I have managed to create a SO generator that generates weapons. In the generated Weapon.asset I have added a GameObject which will store the 3D model of the weapon. However I cannot load it into the seen. What am I doing wrong? Am I missing something? \$\endgroup\$ – Valamorde May 14 '16 at 5:48
3
\$\begingroup\$

Scriptable objects are data containers to be used in the editor. It is basically a standard data class, except you can make a custom window in the editor to edit that data. It is for making tools to extend Unity.

Disclaimer: I just started with scriptable objects myself, so take my answer with a grain of salt, and please correct me if I am wrong.

\$\endgroup\$
  • 1
    \$\begingroup\$ Hmm I see! So could we roughly say that Scriptable Objects are a form of a local database? I am guessing it would be a good "substitute" to an XML file or a database which would involve a lot of parsing! \$\endgroup\$ – Valamorde Apr 17 '16 at 17:42
  • 2
    \$\begingroup\$ @Karaflakos I believe so. Comparable to scriptable objects is passing XML data, or having a GameObject you edit in the inspector but never actually use in the Scene. \$\endgroup\$ – Evorlor Apr 17 '16 at 18:09
  • 1
    \$\begingroup\$ @Karaflakos, look for void OnInspectorGUI() in my answer; Evorlor is referring to custom Editors, which you can create for all of your objects, not just ScriptableObjects. The OnXXGUI() methods use the UnityEditor.SerializableObject class to request information from an object that may or may not have it. When a member is requested from a class that doesn't contain it, null is returned (instead of an exception). That allows you to optimize the editor experience for whatever type of data happens to be getting inspected at this exact moment. \$\endgroup\$ – Jon Apr 18 '16 at 13:03
  • 1
    \$\begingroup\$ @Karaflakos, also, if an object with a custom Editor references objects with custom editors, the outer-most editor is able to "borrow" the custom editors of the referenced objects. So, instead of just a one line, drag-and-drop object field, you get the correct inspector for "whatever" is dropped there. Inheritance applies, so if ChildClass1 and ChildClass2 both inherit BaseClass which inherits ScriptableObject, you can drop either of them on a BaseClass object field. BaseClass can be abstract. \$\endgroup\$ – Jon Apr 18 '16 at 13:19
  • 1
    \$\begingroup\$ @Karaflakos, for you both, although SO aren't [Serializable], serializing them is rather trivial. I think the serialization tutorial introduces the BinaryFormatter but there are other options available to output XML or even plain-text. Once everything serializes correctly, you can swap the formatter for another. \$\endgroup\$ – Jon Apr 18 '16 at 13:30
3
\$\begingroup\$

Adding to the great answers, ScriptableObjects can also contain methods. You could create something like an EventService asset for messaging, passing an instance to your scripts for them to subscribe and use.

You could say that whenever you find yourself considering Singleton patterns, ScriptableObjects as assets usually work just as well (or even better).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.