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. Dismiss Notice

Scriptable Objects pattern for player's position

Discussion in 'Scripting' started by Long2904, Nov 19, 2020.

  1. Long2904

    Long2904

    Joined:
    May 13, 2019
    Posts:
    81
    I've just watched the "Game Architecture with Scriptable Objects" Unite talk 2017. In my game, a lot of time I just need the position of the player so should I make a Vector3Variable and a Vector3Reference (or maybe a TransformVariable)? What is the benefit of making a bunch of reference scriptable objects rather than just make the player a singleton and access his data?
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,714
  3. Havyx

    Havyx

    Joined:
    Oct 20, 2020
    Posts:
    140
    I disagree that they are "read-only" predefined data as Unity themselves suggest using Scriptable Objects as one method to persist data between scene (which means you would need to write to them instead of just read).

    In my opinion, they are both readable and writable but should be considered "temporary" storage through the game session (because an SO will reset back to it's default data values when a game session is ended). The scriptable object assets are used to store default and changed value states, then these are used when serializing data to a binary file.

    When the player loads your game up, you deserialize data from a binary file and pack it back into the scriptable object.

    The workflow is like this:

    - unpack data from a binary file
    - deserialize data into scriptable objects
    - various scripts read and modify this data
    - when saving the game, the scriptable object data is passed to the required scripts
    - new data is serialized into a binary


    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. [CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/GenericIntSO", order = 1)]
    4. public class GenericIntSO : ScriptableObject
    5. {
    6.     public int value;
    7. }
    Scene 1
    Code (CSharp):
    1. public class Scene1Script : MonoBehaviour
    2. {
    3.      GenericIntSO playerHealth;
    4.  
    5.     void Start()
    6.     {
    7.         playerHealth.value = 500;
    8.     }
    9. }

    Scene 2
    Code (CSharp):
    1. public class Scene2Script : MonoBehaviour
    2. {
    3.      GenericIntSO playerHealth;
    4.  
    5.     void Start()
    6.     {
    7.         Debug.Log(playerHealth.value);
    8.         playerHealth.value += 100;
    9.         Debug.Log(playerHealth.value);
    10.     }
    11. }

    Generally speaking scriptable objects allow you to pass data between scenes without the need of singleton patterns. Also, as shown above, a single scriptable object asset can be used for multiple things in a sort of generic manner.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. [CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/GenericIntSO", order = 1)]
    4. public class GenericIntSO : ScriptableObject
    5. {
    6.     public int value;
    7. }

    Code (CSharp):
    1. public class Scene1Script : MonoBehaviour
    2. {
    3.      GenericIntSO playerHealth;
    4.      GenericIntSO playerScore;
    5.      GenericIntSO playerReputation;
    6.      GenericIntSO playerID;
    7. }

    You could use something generic that can be used by anything that needs to track a position.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. [CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/SpawnManagerScriptableObject", order = 1)]
    4. public class GenericTransformSO : ScriptableObject
    5. {
    6.     public int xPosition;
    7.     public int yPosition;
    8.     public int zPosition;
    9. }
    Then create a new SO called "PlayerTransform"


    Code (CSharp):
    1. public class PlayerManager : MonoBehaviour
    2. {
    3.      GenericTransformSO playerPosition;
    4.  
    5.     void Update()
    6.     {
    7.         playerPosition.xPosition = gameObject.transform.position.x;
    8.         playerPosition.yPosition = gameObject.transform.position.y;
    9.         playerPosition.zPosition = gameObject.transform.position.z;
    10.  
    11.         Debug.Log(playerPosition.xPosition, playerPosition.yPosition, playerPosition.zPosition);
    12.     }
    13.  
    14. }
    I don't think there is much benefit though compared to a singleton here in your case. Scriptable Objects are really good at things like sharing with prefabs.

    https://docs.unity3d.com/Manual/class-ScriptableObject.html

    A ScriptableObject is a data container that you can use to save large amounts of data, independent of class instances. One of the main use cases for ScriptableObjects is to reduce your Project’s memory usage by avoiding copies of values. This is useful if your Project has a Prefab that stores unchanging data in attached MonoBehaviour scripts.
    .
    Every time you instantiate that Prefab, it will get its own copy of that data. Instead of using the method, and storing duplicated data, you can use a ScriptableObject to store the data and then access it by reference from all of the Prefabs. This means that there is one copy of the data in memory.
     
    Last edited: Nov 19, 2020
  4. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    664
    Again, I have to disagree with my respected colleague on this. SOs are great for communications, shared data, decoupled code (particularly the strategy pattern), and all the other things you saw in Ryan Hipple's talk. Also, see Richard Fine's earlier talk about them from Unite 2016:
     
    Kurt-Dekker likes this.
  5. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    664
    Yes, that's important to remember, and easy to forget as they do persist across editor sessions. For persistent data between runs of a built game, one must use PlayerPrefs, or else serialize to a file (with JsonUtility, or something along those lines).
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,714
    I agree with this but find it a confusing way to code: if we agree that one use is for predefined data in the editor (which it certainly is), then it feels wonky to me to also treat other fields within in that same structure as runtime data. I accept that you CAN, I just think that if I did so, then one month later tried to reason about it, I would miss that subtlety.

    Instead for longer-term self-documenting code, I feel better about treating the SO itself as read only, and then have a separate runtime class that deals with changes to that base template.

    Code (csharp):
    1. public class Weapon : ScriptableObject
    2. {
    3.   public int Damage;
    4. }
    Now... the instance of a Sword (which is a Weapon) might start with Damage of 10 when I find it.

    But over time it would degrade, so the WeaponInstance in the game would look like this:

    Code (csharp):
    1. public class WeaponInstance
    2. {
    3.   public Weapon TemplateWeapon;
    4.  
    5.   public int WearAndTear;
    6.  
    7.   public int EffectiveDamage
    8.   {
    9.    get
    10.    {
    11.      // or whatever encapsulating logic you want
    12.      return TemplateWeapon.Damage - WearAndTear;
    13.    }
    14.   }
    15. }
    If all that was in the base Weapon SO I think it would not read as well what the intended lifecycle is. I get that you CAN do it, it certainly results in less classes, but I just don't think it captures the code meaning as well.

    In short, I love how with an SO it's effectively a "hot link" to the on-disk item (at least for public fields), so you can pull it up in the editor and fiddle with stuff at runtime. But that should never mask the fact that you really are changing the on-disk asset, in much the same way as if you adjust Material values at runtime on a non-instantiated Material.
     
    Havyx likes this.
  7. Long2904

    Long2904

    Joined:
    May 13, 2019
    Posts:
    81
    Why use 3 ints rather than a Vector3? It looks simpler and has a lot of handy functions. Is it because you can't serialize a Vector3 by default?
     
    Last edited: Nov 20, 2020
  8. Havyx

    Havyx

    Joined:
    Oct 20, 2020
    Posts:
    140
    Yes afaik neither vector3 or quaternions are serializable by default. However, you could use a struct.


    Code (CSharp):
    1. using UnityEngine;
    2. using System;
    3. using System.Collections;
    4.  
    5. [System.Serializable]
    6. public struct SerializableVector3
    7. {
    8.  
    9.      public float x;
    10.      public float y;
    11.      public float z;
    12.  
    13.      public SerializableVector3(float _X, float _Y, float _Z)
    14.      {
    15.          x = _X;
    16.          y = _Y;
    17.          z = _Z;
    18.      }
    19.  
    20.      .....
    21. }
    or you could also use the ISerializationSurrogate Interface.

    https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.iserializationsurrogate

    "Implements a serialization surrogate selector that allows one object to perform serialization and deserialization of another."

    but for the sake of clarity I thought it would be easier to just use 3 integers (you could also use floats instead). I guess it depends on your specific needs.
     
  9. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    The idea of the pattern you're talking about is that your components could have a "shared drag&drop reference" without making them aware of something complex as players, enemies or other components.

    That's the whole point of it. A system may only require to know about a position in order to work, which happens to be the player's position in a given context and something else in a different context. If said component referenced your entire player script, it's dependent on the existence of such component types and thus depends on a larger context that might be specific to the current project.

    Suppose you have a smooth camera component that requires a reference to something that provides a position. Let's call it target. The most trivial thing is to just reference a Transform or a GameObject directly.

    While that's often sufficient and a new target can be set via properties or methods, changing/updating the target can only happen when you have a reference to that component in order to call said property or method. And that might need to be done for multiple components.
    Now if you have a shared SO, that connection is not necessary, as you can just change that SOs value, and everyone who's got the reference to it sees the changes.

    The pattern also has downsides. It won't be obvious who changes what under which circumstances. It might also lead to horrible design decisions that noone will be able to reason about in the future if you use it all over your project.
     
  10. eses

    eses

    Joined:
    Feb 26, 2013
    Posts:
    2,637
    @Havyx

    "Yes afaik neither vector3 or quaternions are serializable by default."


    BTW Vector3 and I think Quaternions too serialize just fine if you use Unity serializer, so JsonUtility works with Vector3 etc.

    However, C# BinaryFormatter and some other 3rd party JSON serializers might not be able to serialize these directly.
     
    Havyx likes this.
  11. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    664
    Correct. I wrote this:
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class SerializeThis : MonoBehaviour
    4. {
    5.     void Start()
    6.     {
    7.         SomeData someData = new SomeData();
    8.         Debug.Log(JsonUtility.ToJson(someData));
    9.     }
    10. }
    11.  
    12. public class SomeData
    13. {
    14.     public Vector3 position;
    15.     public Quaternion rotation;
    16. }
    And it gave me this:
    Code (JavaScript):
    1. {"position":{"x":0.0,"y":0.0,"z":0.0},"rotation":{"x":0.0,"y":0.0,"z":0.0,"w":0.0}}
    Or, if you prefer, this:
    Code (JavaScript):
    1. {
    2.     "position":
    3.     {
    4.         "x":0.0,
    5.         "y":0.0,
    6.         "z":0.0
    7.     },
    8.  
    9.     "rotation":
    10.     {
    11.         "x":0.0,
    12.         "y":0.0,
    13.         "z":0.0,
    14.         "w":0.0
    15.     }
    16. }
     
    Havyx likes this.
  12. Long2904

    Long2904

    Joined:
    May 13, 2019
    Posts:
    81
    It annoyed me that the value of scriptable objects get saved in play mode so I added a private field "defaultValue" (with serializable attribute) and assign the defaultValue to the value in OnEnable.
    Code (CSharp):
    1.  
    2.     [HideInInspector] public float value;
    3.     [SerializeField] private float defaultValue; // I just need to change this in the inspector
    4.  
    5.     private void OnEnable()
    6.     {
    7.         value = defaultValue;
    8.     }
    Is this good enough? I did some research and found that I should add a hideFlag DontUnloadUnusedAsset. Why?
     
  13. Michael-Ryan

    Michael-Ryan

    Joined:
    Apr 10, 2009
    Posts:
    184
    Unity will eventually unload any objects that are not referenced by any other object. The DontUnloadUnusedAsset prevents the object from being unloaded automatically when no references to it exist.
     
    Long2904 likes this.