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)