Home > Net >  Data safety in Scriptable Object based architecture
Data safety in Scriptable Object based architecture

Time:09-30

Immediately I would like to say that if a similar question has been asked before, please point me to it.

Let's say I have a Scriptable Object that I use to pass a player's health from one system to another.

public class HealthSO: ScriptableObject {
   [ReadOnly]
   public float health;
}

A class called PlayerHealth sets the value in the Scriptable Object so that other systems can use it. E.g: the player's health bar.

It's great because I can freely connect different systems without referencing them, but it is not without its problems, and there is one that concerns me the most.

How do I make sure that the only class that can change the health value in the Scriptable Object is PlayerHealth?

Or maybe it is something that I shouldn't worry about too much? Sure if it is only one person working on a project then there isn't too much to worry about. But what if this approach would be applied in a bigger project?

Thanks!

CodePudding user response:

This might be a bit controversial, but so is using ScriptableObject for this in the first place ^^

Unfortunately Unity still doesn't really support serializing of interface type fields. But in this case there is only two different access levels - read and write.

So you could do something like

// Just going generic here as latest Unity versions finally support it
// and you have way less re-implementation of the same functionality 
public abstract class ReadonlyValueSO<T> : ScriptableObject
{
    [SerializeField]
    [ReadOnly]
    protected T _value;

    public T Value
    {
        get => _value;
    }
}

public abstract class WriteableValueSO<T> : ReadonlyValueSO<T>
{
    public void Set(T value)
    {
        _value = value;
    }
}

// Some constants could even be ReadonlyValueSO if you never want to write over them anyway 
[CreateAssetMenu]
public class HealthSO : WriteableValueSO<float>
{
}

This way in your setter component you would use the writeable type and do e.g.

public class SomeSetter : MonoBehaviour
{
    [SerializeField] WriteableValueSO<float> health;

    private void Update()
    {
        health.Set(health.Value   .1f * Time.deltaTime);
    }
}

while in the consumers you only give it the readable

public class Consumer : MonoBehaviour
{
    [SerializeField] ReadonlyValueSO<float> health;

    private void Update()
    {
        Debug.Log(health.Value);
    }
}

This way you have full control over who can read and who can write.


Another huge advantage: This way you also don't have to poll check values how I did above. You can rather simply add an even to be invoked whenever the value is set:

public abstract class ReadonlyValueSO<T> : ScriptableObject
{
    [SerializeField]
    protected T _value;

    public T Value
    {
        get => _value;
    }

    public abstract event Action<T> ValueChanged;
}

public abstract class WriteableValueSO<T> : ReadonlyValueSO<T>
{
    public void Set(T value)
    {
        _value = value;
        ValueChanged?.Invoke(_value);
    }

    public override event Action<T> ValueChanged;
}

now your consumer could rather look like e.g.

public class Consumer : MonoBehaviour
{
    [SerializeField] ReadonlyValueSO<float> health;

    private void Awake()
    {
        // subscribe to event
        health.ValueChanged -= OnHealthChanged;
        health.ValueChanged  = OnHealthChanged;

        // invoke now once with the current value
        OnHealthChanged(health.Value);
    }

    private void OnDestroy()
    {
        // IMPORTANT: Unsubscribe!
        health.ValueChanged -= OnHealthChanged;
    }

    // Always and only called whenever something sets the value
    private void OnHealthChanged(float newHealth)
    {
        Debug.Log(newHealth);
    }
}
  • Related