Scriptable Objects: How to keep original values.

Have you ever needed to keep values in your ScriptableObjects fixed to their original value? Have you been “reseting” their values from a MonoBehaviour based component? Then this article might give you some ideas on how to improve your process, making ScriptableObjects (SO from now on) more maintainable and seamless integrated with the inspector.

Before we start, keep in mind:

(Feel free to skip to “The idea!”)

  • The code and ideas I’ll describe are working great on my project and feel very clean to me, but still, they are experimental.
    I encourage you to try them and maybe get some new ideas.
  • I know, I know… SO are not meant to work like MonoBehaviours, but I found some situations where having this option for certain values makes the whole process easier. Remember this feature is optional.
  • If you haven’t yet watched it, I strongly recommend that you watch this Unite talk by Ryan Hipple that sets the bases for an SO game architecture.
  • Last but not least, I’m an indie developer in my free time. I actually have been a web dev for the past +10 years, that’s why this event driven architecture is so appealing to me. Also my C# and Unity knowledge is not so strong, so you’ll find a lot to improve (probably).

Why you need this?

I created a SO named NumericCondition that offers a generic solution for tracking condition triggers by setting 2 numeric values and an operator.
In this case I wanted to trigger the “OnConditionMet” event when at least 6 enemies got killed.

This event (another SO like the one presented at the Unite talk) is then connected to different GameObjects that react to this event, e.g. opening the exit door.
This is great, everything works as expected. The only thing I didn’t consider is that this was a tutorial and it can be replayed, so condition A needs to be reseted back to 0.

No need to say I considered all kinds of solutions. Moving the class from SO to MonoBehaviour would be easy, but then I’ll have to initialise/reset/maintain values on my GameObjects. In addition to instantiate GO that will only hold the purpose of monitoring values. This would also hurt the portability / modularity of this generic “Condition”.

I was so close to what I wanted, just needed some values to be reseted on these SO. So after considering quite a few approaches using MonoBehaviour I decided to give it a shot to making values on SO “resettable”.

I googled a lot, saw many people trying different approaches. Using JSON, storing serialised data in different places, using indexes to keep values being reseted and some crazy stuff too.

The idea!

Unity already has the behaviour I want on MonoBehaviour scripts and GameObjects. But I didn’t want to maintain GO, indexes, setting up scripts for “Reseting” specific values on Awake/Start…

So, for me, it’s not about forcing SO or MonoBehaviours to work differently but to combine features on a maintainable easy to work way.

Intercepting values

I’m not gonna assume you saw the video, so the wrapper class would be something like this:

[Serializable]
public class FloatVolatile
{
public float ConstantValue;
public bool IsVolatile = false;
public FloatVolatile(float value)
{
ConstantValue = value;
}
// Check if actual value should be altered or not
bool UseVolatile(){
return IsVolatile && Application.isPlaying;
}
public float Value
{
get
{
return UseVolatile() ? GetVolatile() : ConstantValue;
}
set
{
if(UseVolatile())
SetVolatile(value);
else
ConstantValue = value;
}
}
// We'll need this method to access the "real" value
// from the singleton at runtime and avoid recursion
public float GetNonVolatileConstant()
{
return ConstantValue;
}
float GetVolatile()
{
return VolatileMemory.Instance.GetFloat(this);
}
void SetVolatile(float value)
{
VolatileMemory.Instance.SetFloat(this,value);
}
public static implicit operator float(FloatVolatile reference)
{
return reference.Value;
}
}

Here’s a little example on how to use it:

public class NumericCondition : ScriptableObject
{
public FloatVolatile A;
public FloatVolatile B;
...
void AddToA(float num)
{
// You can add operator overload here to make it fancy
A.Value = num + A.Value;
}
}

I did some extra work to make it all fit naturally in the inspector like this:

Recap:

  • We wrap our values in a new class with a Value property.
  • The class also holds a IsVolatile boolean property to indicate if we want this value to change during runtime or not.
  • set & get intercept values and connect the operations to the correct storage either the VolatileMemory singleton or the default SO value.

VolatileMemory Singleton

It felt ironic since I’ve been using SO to replace Singletons

public class VolatileMemory : MonoBehaviour
{
#region singleton code starts here
public static VolatileMemory _instance;
public static VolatileMemory Instance
{
get{
if (_instance == null)
{
_instance = GameObject.FindObjectOfType<VolatileMemory>();
if (_instance == null)
{
GameObject container = new GameObject("VolatileMemory");
_instance = container.AddComponent<VolatileMemory>();
}
}
return _instance;
}
}
#endregion

Dictionary<FloatVolatile,float> floatValues =
new Dictionary<FloatVolatile,float>();
public float GetFloat(FloatVolatile reference){
if(!floatValues.ContainsKey(reference))
// Here's where we need to special method
SetFloat(reference,reference.GetNonVolatileConstant());
return floatValues[reference];
}
public void SetFloat(FloatVolatile reference,float value){
floatValues[reference] = value;
}
}

Conclusion

We can use this pattern for all types needed and it can be enabled/disabled for individual values even at runtime.
We’re adding another layer of flexibility on top of our individual values.
I’m still not a 100% sure using a singleton is optimal, but I’ve got really good results and I feel better knowing I’m not forcing neither class to absorbe the other, we’re just masking the connections.

I hope this helps you or at least gives you some ideas.