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);
}
}