Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Question ScriptableObject immutable/static data design? What should i do

Discussion in 'Scripting' started by Deleted User, Feb 23, 2023.

  1. Deleted User

    Deleted User

    Guest

    Hi everyone

    I have been developing a game where i sometimes use Scriptable Objects. One example of this is my UnitData scriptable object which could look something like this

    Code (CSharp):
    1. public int Level;
    2. public int MaxLevel;
    3. public int Status; //(Locked, Unlocked, Owned)
    4. public int[] HealthDB;
    5. public int[] DamageDB;
    6. public int[] MoveSpeedDB;
    7.  
    Some of the data is immutable/static such as MaxLevel, HealthDB, DamageDB and MoveSpeedDB (these are configuration data typed in by designers in the editor), but some data is also mutable (such as Level and Status).

    Right now i have been using ScriptableObjects with mutable data, but i have heard this is a bad pratice. Other than that i use the ScriptableObject as an in memory data persistence between scenes. I save the neccesary data such as Level and Status throughout the game to a File on the users computer/phone and load this data in when the game starts.

    What would i do if i did not have Level and Status as part of the scriptable object? I literally have no idea. Should i create a new class that contains Level and Status and then also has a slot for the ScriptableObject containing now only the static/immutable data? What do you guys typically do?
     
  2. AngryProgrammer

    AngryProgrammer

    Joined:
    Jun 4, 2019
    Posts:
    437
    I always separate game data from Unity. So the metadata of the game that it loads and saves works independently of the Unity engine layer. On the other hand, Game Object is for me something that is supposed to present/interpret this data, and Scriptable Object is a dictionary from which I can extract it.

    For example, if I have "opponent:1" in the metadata of the game, my Game Object that loads the levels references the Scriptable Object with some list to extract what that 1 is, i.e. a prefab.

    Edit: What you have presented is game metadata for me and should not be in Scriptable Object. Of course, that's my personal preference for code structure.
     
    Last edited: Feb 23, 2023
  3. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    I will usually only put Read-Only Data in the ScriptableObject. Normally by using a property with a public getter, private setter, and Serializing the backing field so it can be set in the inspector:
    Code (CSharp):
    1. [field: SerializeField]
    2. public float Speed { get; private set; } // = 5f; // You can add a hardcoded default if you want.
    If you have to use mutable variables in your SO, you'd want to Instantiate a copy of it, then do everything using that instead. The copy will only exist in RAM.
     
  4. Deleted User

    Deleted User

    Guest

    So in my example. I have a unit that contains some static data, but the player is also able to unlock this unit, and upgrade this unit (this is the mutable data). Would you still use a ScriptableObject for the static data (MaxLevel, HealthDB etc) and then have a script. Lets say

    Code (CSharp):
    1. public int Level;
    2. public int Status;
    3. public UnitData UnitData; (i would drag the ScriptableObject containing the static data in the inspector into here)
     
  5. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    That would be how I normally do it, yes.
    Keep the Config of your Object (MaxHealth, BaseDmg, etc.) on the SO.
    Put everything else (CurrHealth, CurrLevel) on the object itself.

    As far as unlocking & upgrades goes: This will most likely become a part of your save-file at some point. So you'll want to read it from there.
    When spawning, simply set the Level of your Unit & base everything off of that. You could have an array of 'BaseDamage per Level' on your SO if you want to.

    Edit: Instead of an array, you can also use AnimationCurve. Makes it easier for Designers (i.e. non-developers) to work with.
    E.g. a "MaxHealthAtLevel" where Y = HP & X = Level.
     
  6. Deleted User

    Deleted User

    Guest

    Thanks for the reply. I don't exactly understand where you then would place this "metadata". What would it look like in a real Unity project? Lets say with the UnitData example. Each type of unit has this. An archer, a knight, a mage, an orc etc.
     
  7. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,571
    Well, what they have proposed is to use a ScriptableObject to store that persistant metadata so the same instance can be shared by all objects. However instance values would be inside the instance itself.

    Though note that there are other approaches how ScriptableObject may be used. I can highly recommend watching this Unite talk by Ryan Hipple from 2017. It took the usage of ScriptableObjects to a new level. Though that may not be the best solution in all cases. It wouldn't really work well for highly dynamic games.
     
  8. AngryProgrammer

    AngryProgrammer

    Joined:
    Jun 4, 2019
    Posts:
    437
    My poor English must have confused you. I meant something like this.
    1. I have a class that is independent of anything from Unity. Based on it, I can serialize/deserialize the progress in the game, i.e. I have a list where I can store a set of opponents and their positions, but at this level it is just a number.
    2. The game starts, there is some GameManager that already depends on Unity. It generates some simple menu, I can start a new game in it (create a new object from point 1) or load a saved game (deserialize data from point 1).
    3. Interpretation begins. So I know it's an enemy, but what kind? I check in Scriptable Object which prefab corresponds to it and introduce it to the game world. If it's new enemy it will have it's default max hp, if he was injured I have this info in meta data from point 1 (override it).
     
  9. Deleted User

    Deleted User

    Guest

    Right now i do it the following way. I have a a ScriptableObject containing this

    Code (CSharp):
    1. public int Level;
    2. public int MaxLevel;
    3. public int Status; //(Locked, Unlocked, Owned)
    4. public int[] HealthDB;
    5. public int[] DamageDB;
    6. public int[] MoveSpeedDB;
    When the game stars, the SaveManager loads the SaveFile which contains data such as a Units Level and Status. This data is loaded into the ScriptableObject (Only the Level and Status is saved in the savefile and put into the ScriptableObject).

    What i think you are suggesting is maybe having a Singleton like this
    Code (CSharp):
    1. SINGLETON
    2.  
    3. public UnitData[] Units;
    4.  
    5. public class UnitData
    6. {
    7.    public string ID;
    8.    public int Level;
    9.    public int Status;
    10.    // This is the scriptable object containing immutable data
    11.    // such as MaxLevel, HealthDB
    12.    public int UnitStats UnitStats;
    13. }
    Now lets say i have a Prefab called Archer with an ArcherManager Monobehaviour component attached to it. This MonoBehaviour has the string field ID. I would then in the start of this ArcherManager access the Signleton.Instance.(access the object by the id) and then populate that archer? It seems like a lot more work than having a scriptableobject that can be just plugged into the ArcherManager. The most annyoing thing with ScriptableObject is the fact that runtime editor data swaps out the original values when testing
     
  10. Deleted User

    Deleted User

    Guest

    Im still a bit confused on how to achieve this. I kind of got the idea, but not 100 %.
     
  11. Deleted User

    Deleted User

    Guest

    Every unit in the game has its own scriptable object.

    The humans have: Knight, Archer, Peasant, Wizard etc. The data from this scriptable object is used to populate the actual monobehaviours when the player spawns a knight or a wizard etc. The units can be upgraded in the upgrade menu (not part of the level) between levels.
     
  12. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    This is pretty much what you're doing here:

    By 'independenta of anything' @AngryProgrammer basically means 'not a MonoBehaviour' (so just a basic C#-class).
     
    AngryProgrammer likes this.
  13. simplebitstudios

    simplebitstudios

    Joined:
    Feb 14, 2018
    Posts:
    11
    Ah okay. If i want to use a Dictionary and have the scriptableobject as the key and the other data as the value the problem is that, that will not be serialized in the inspector :/
     
  14. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    Unfortunately normal dictionaries are indeed not serialized.
    There are workarounds (such as serializing 2 lists/arrays and creating the Dict in Awake)
    There are also custom implementations (such as SerializableDictionary | Integration | Unity Asset Store azixMcAze/Unity-SerializableDictionary: Serializable dictionary class for Unity (github.com))
     
  15. simplebitstudios

    simplebitstudios

    Joined:
    Feb 14, 2018
    Posts:
    11
    Would you recommend to use a dictionary or just have an ID on the UnitData object (typed in by the designer or developer) and have that same ID in the UnitScriptableObject. Then use a for loop that goes through all the units and finds the matching ID's. I guess dictionaries would be the best solution in this instance.
     
  16. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    Dictionary would be a better choice, indeed. Because of Dictionary.ContainsKey() being O(1) (vs O(log(n)) for your loop).
     
  17. simplebitstudios

    simplebitstudios

    Joined:
    Feb 14, 2018
    Posts:
    11
    This is my basic solution. Thank you so much for you help. Means the world to me

    I have a array UnitDictionary, where i can input the UnitSO and UnitData. Then at runtime i add the array to a dictionary, which i will use throughout the game to acces the UnitData, by having the UnitSO as the key etc. That way the Archers MonoBehaviour would have the UnitSO plugged in. On start it would do the following:
    Code (CSharp):
    1.     private void Start()
    2.     {
    3.         int level = Singleton.Instance.Units[UnitSO].Level;
    4.  
    5.         Health = UnitSO.HealthDB[level];
    6.     }
    SINGLETON
    Code (CSharp):
    1. public class Singleton : MonoBehaviour
    2. {
    3.     public static Singleton Instance { get; private set; }
    4.     private void Awake()
    5.     {
    6.         // If there is an instance, and it's not me, delete myself.
    7.  
    8.         if (Instance != null && Instance != this)
    9.         {
    10.             Destroy(this);
    11.         }
    12.         else
    13.         {
    14.             Instance = this;
    15.         }
    16.     }
    17.  
    18.     public UnitDictionary[] UnitDictionary;
    19.     public Dictionary<UnitSO, UnitData> Units = new Dictionary<UnitSO, UnitData>();
    20.  
    21.     private void Start()
    22.     {
    23.         for (int i = 0; i < UnitDictionary.Length; i++)
    24.         {
    25.             Units.Add(UnitDictionary[i].UnitSO, UnitDictionary[i].UnitData);
    26.         }
    27.     }
    28. }
    UnitDictionary
    Code (CSharp):
    1. [Serializable]
    2. public class UnitDictionary
    3. {
    4.     public UnitSO UnitSO;
    5.     public UnitData UnitData;
    6. }
    UnitSO (Scriptable Object - immutable/static data)
    Code (CSharp):
    1. public class UnitSO : ScriptableObject
    2. {
    3.     public int MaxLevel = 10;
    4.     public int[] HealthDB;
    5. }
    UnitData (Level, Status - mutable data)
    Code (CSharp):
    1. public class UnitData
    2. {
    3.     public int Level = 1;
    4.     public int Status = 2;
    5. }
    6.  
     
  18. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    Yeah, that would work fine.
    I'd personally replace the fields in your SO with Properties though.
    Just to prevent someone from accidentally writing to them

    Code (CSharp):
    1. public class UnitSO : ScriptableObject
    2. {
    3.     [field: SerializeField]
    4.     public int MaxLevel { get; private set; } = 10;
    5.     [field: SerializeField]
    6.     public int[] HealthDB { get; private set; }
    7. }
     
  19. simplebitstudios

    simplebitstudios

    Joined:
    Feb 14, 2018
    Posts:
    11
    Thank you. What is the point of writing [field: SerializeField], rather than just [SerializeField]?
     
  20. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    https://learn.microsoft.com/en-us/d...-guide/concepts/attributes/#attribute-targets
    It tells the Attribute to apply to the underlying field, instead of the Property itself.
    The underlying field (that is auto-created) is called "<PROPERTYNAME>k__BackingField" by the compiler (and is private).
     
  21. simplebitstudios

    simplebitstudios

    Joined:
    Feb 14, 2018
    Posts:
    11
    I think the main problem with this system: Signelton, UnitDictionary, UnitSo, UnitData is that now i cannot just test a Archer vs a Knight very easily. I have to go through several scenes to test basic fuctionality. Thats why i really liked to work with ScriptableObjects containing the mutable data, such as CurrentLevel. I really wish it was possible to keep working this way since it gives so much flexibility
     
  22. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    You can.. Simply instantiate() the SO at runtime, and do your stuff using that copy.
    Then Destroy() the copy when exiting playmode.
     
    Nefisto, SisusCo and Kurt-Dekker like this.
  23. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    You could also add some code to your scriptable objects to reset their state when exiting play mode.
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public abstract class AutoResettingScriptableObject : ScriptableObject
    4. {
    5. #if UNITY_EDITOR
    6.     [System.NonSerialized] private string initialState = null;
    7.     [System.NonSerialized] private bool isPlaying = false;
    8.  
    9.     private void OnEnable()
    10.     {
    11.         if(string.IsNullOrEmpty(initialState))
    12.         {
    13.             initialState = JsonUtility.ToJson(this);
    14.         }
    15.  
    16.         UnityEditor.EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
    17.         UnityEditor.EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
    18.     }
    19.  
    20.     private void OnValidate()
    21.     {
    22.         if(!isPlaying)
    23.         {
    24.             initialState = JsonUtility.ToJson(this);
    25.         }
    26.  
    27.         UnityEditor.EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
    28.         UnityEditor.EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
    29.     }
    30.  
    31.     private void OnPlayModeStateChanged(UnityEditor.PlayModeStateChange state)
    32.     {
    33.         isPlaying = state != UnityEditor.PlayModeStateChange.EnteredEditMode;
    34.         if(!isPlaying)
    35.         {
    36.             ResetToInitialState();
    37.         }
    38.     }
    39.  
    40.     private void ResetToInitialState()
    41.     {
    42.         if(!string.IsNullOrEmpty(initialState))
    43.         {
    44.             JsonUtility.FromJsonOverwrite(initialState, this);
    45.         }
    46.     }
    47. #endif
    48. }
    With this approach you could easily share one scriptable object instance among all units, and you would be able to see the scriptable object's current state in Play Mode when inspecting the asset, but the asset would also reset back to its original state the moment you exit play mode.

    Usage:
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. [CreateAssetMenu]
    4. public sealed class UnitData : AutoResettingScriptableObject
    5. {
    6.     [Header("Mutable State")]
    7.     public int Level;
    8.     public Status Status;
    9.  
    10.     [Header("Immutable State")]
    11.     [SerializeField] private int maxLevel;
    12.     [SerializeField] private int[] healthDB;
    13.     [SerializeField] private int[] damageDB;
    14.     [SerializeField] private int[] moveSpeedDB;
    15.  
    16.     public int MaxLevel => maxLevel;
    17.     public int[] HealthDB => healthDB;
    18.     public int[] DamageDB => damageDB;
    19.     public int[] MoveSpeedDB => moveSpeedDB;
    20. }
     
    Last edited: Feb 24, 2023
  24. simplebitstudios

    simplebitstudios

    Joined:
    Feb 14, 2018
    Posts:
    11
    This is a really genius way of doing it. Right now i was doing the following

    Code (CSharp):
    1.     [SerializeField] private int _baseLevel;
    2.     [HideInInspector] public int CurrentLevel;
    3.  
    4.     private void OnEnable()
    5.     {
    6.         CurrentLevel = _baseLevel;
    7.     }
    A really simple way of setting it up and it does what it is supposed to do, but your way is a lot smarter since you wont have to make a Base and Current value for every mutable variable
     
    SisusCo likes this.
  25. Nefisto

    Nefisto

    Joined:
    Sep 17, 2014
    Posts:
    324
    Maybe I'm missing something here, the code from auto-reset SO seems to be a pretty good idea but imo it's breaking the concept of using SO as a template I mean, in this sample he is creating a UnitData so for multiple units u will need multiple SO each one referencing an auto-reset SO, as noted by @SF_FrankvHoof you can simply make use of Instantiate and unity will clone your SO into memory and then if you wish, you can just double click an exposed reference of this clone and change values in the inspector.
     
  26. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    @Nefisto As far as I understood it, the desired effect is for changes made to a scriptable object asset to affect all MonoBehaviour instances that have a reference to that asset.

    So when the player upgrades the "Knight" to level 2, this should cause all MonoBehaviour instances in the next level that use the "Knight" scriptable object to be at level 2.

     
    Last edited: Feb 24, 2023
  27. Nefisto

    Nefisto

    Joined:
    Sep 17, 2014
    Posts:
    324
    Oh, I've missed it, my bad