This tutorial will demonstrate a method to have variables that are independent of a ScriptableObject.

This is particularly useful when you are using ScriptableObjects to assign behaviours to other objects. In this example, we have a CauseExplosion class that inherits from an abstract ScriptableObject called ItemEffect. Without making the variables independent, you will probably have to create a new ScriptableObject for each explosion size.

ItemEffect.cs:

using UnityEngine; public abstract class ItemEffect : ScriptableObject { public abstract void ApplyItemEffect (); }

CauseExplosion.cs:

using UnityEngine; [CreateAssetMenu(fileName = "Cause Explosion",menuName = "Item Effects/Cause Explosion")] public class CauseExplosion : ItemEffect { public float explosionSize; public override void ApplyItemEffect () { Debug.Log(explosionSize); } }

To avoid having to create a CauseExplosion ScriptableObject for each use, we can make objects reference an ItemEffectContainer instead. The ItemEffectContainer will hold a reference to an ItemEffect, and a float array that the ItemEffect will get data from.



ItemEffectContainer.cs:

using System; [Serializable] public class ItemEffectContainer { public ItemEffect itemEffect; public float[] variables; public void ApplyItemEffect () { itemEffect.ApplyItemEffect(this); } }

The ApplyEffect function in ItemEffect will then need to have a reference to the ItemEffectContainer.

ItemEffect.cs:﻿

public abstract void ApplyItemEffect (ItemEffectContainer _container);

At this point, it is possible to do _container.variables[0] to get your explosionSize, but you’ll have to manually resize the array in inspector every time you change to a different type of ItemEffect, and without any labels, you might input the wrong values into the array.

Here’s where the custom inspector comes in:

Editor/ItemEffectContainerDrawer.cs:

using UnityEngine; using UnityEditor; [CustomPropertyDrawer(typeof(ItemEffectContainer))] public class ItemEffectContainerDrawer : PropertyDrawer { public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { label = EditorGUI.BeginProperty(position, label, property); Rect contentPosition = EditorGUI.PrefixLabel(position,label); EditorGUI.indentLevel = 0; // --- show itemEffect's property field contentPosition.height = 16f; SerializedProperty itemEffect = property.FindPropertyRelative("itemEffect"); EditorGUI.PropertyField(contentPosition,itemEffect,GUIContent.none); if (itemEffect != null) { SerializedProperty variables = property.FindPropertyRelative("variables"); ItemEffect itemEffectObject = (ItemEffect)itemEffect.objectReferenceValue; // --- only show variables array if there's an ItemEffect referenced if (itemEffectObject != null) { string[] variableNames = itemEffectObject.variableNames; // --- set container's variables size to be the same as the ItemEffect's variableNames size variables.arraySize = variableNames.Length; // --- show each array element's property field with the variable name as label for (int i = 0; i < variableNames.Length; i++) { SerializedProperty floatInArray = variables.GetArrayElementAtIndex(i); contentPosition.y += 18f; EditorGUI.PropertyField(contentPosition,floatInArray,new GUIContent(variableNames[i])); } } else variables.arraySize = 0; } EditorGUI.EndProperty(); } // --- override the default height so we can fit in the variables array public override float GetPropertyHeight (SerializedProperty property, GUIContent label) { int variableArraySize = property.FindPropertyRelative("variables").arraySize; return 16f + (variableArraySize * 18f); } }

Essentially, this custom property drawer for ItemEffectContainer will make it look like a normal slot you can drag an ItemEffect into, but it will automatically display and resize the variables array when needed. Instead of displaying variables like a normal array, it will display it with the labels provided by the ItemEffect.

Your ItemEffect class will then need a string array called variableNames to get the labels from.

ItemEffect.cs:﻿

public abstract string[] variableNames { get; }

Any class that inherits from ItemEffect (e.g. CauseExplosion) must then override this property with the variables it will require.

CauseExplosion.cs:

using UnityEngine; [CreateAssetMenu(fileName = "Cause Explosion",menuName = "Item Effects/Cause Explosion")] public class CauseExplosion : ItemEffect { readonly string[] explosionVariableNames = { "Explosion Size" }; public override string[] variableNames { get { return explosionVariableNames; } } public override void ApplyItemEffect (ItemEffectContainer _container) { Debug.Log(_container.variables[0]); } }

This completes the custom inspector, and it will display the variables correctly and neatly with labels when you drag any ItemEffect into the ItemEffectContainer. To add more variables, you can just modify the readonly string array.

CauseExplosion.cs:﻿

readonly string[] explosionVariableNames = { "Explosion Size", "Explosion Damage", "Explosion Fuse Duration" };

You can now safely get your explosionSize as long as you get the index right, but what I like to do is add another layer of assistance in ItemEffectContainer in case I rearranged or added more variableNames.

ItemEffectContainer.cs:

public float GetVariable (string _variableName) { int index = Array.IndexOf(itemEffect.variableNames,_variableName); if (index < 0) return 0; return variables[index]; } public void SetVariable (string _variableName, float _value) { int index = Array.IndexOf(itemEffect.variableNames,_variableName); if (index < 0) return; variables[index] = _value; }

That way, I can search for the value in the variables array with the name of the variable, and not the integer index in the array. I can then easily get the value of explosionSize, or even modify it without worrying about shenanigans arising from using a shared ScriptableObject or values being saved after runtime.

CauseExplosion.cs:﻿

public override void ApplyItemEffect (ItemEffectContainer _container) { float explosionSize = _container.GetVariable("Explosion Size"); Debug.Log(explosionSize); }

SomeGameScript.cs:

public ItemEffectContainer effectOnClick; void Update () { if (Input.GetMouseButtonDown(0)) { effectOnClick.ApplyItemEffect(); } }

And that’s it! Keep in mind that there are several caveats with this method:



You need an array for every type of variable you want. In this example, we used a float variables array. If you need boolean variables, you will need to create a boolVariables array and modify the custom property drawer accordingly.

Searching through two different arrays for a single variable every frame can be costly, so you may need to cache the indexes or values somewhere.

Modifying the values at runtime can get a bit tricky, so this is more suited for values that are defined by the game designer and aren’t modified during runtime. Examples of this can be predefined item effects or enemy behaviours.

If this tutorial has been of assistance to you, I would appreciate it if you follow me on Twitter, or join my mailing list!