unity - how to update an object when a serialized field is changed?

Viewed 2417

I'm sure the question is already out there but I cannot find it, sorry.

I'm trying to sync a Serialized field of an object with other of its components.

let's say I have a field "size" which should impact the object transform scale:

[SerializeField]
int _size;

I'm looking for an event handler or something that allow me to do:

void onSerializedPropertyChange() {
    transform.localScale = new Vector3(_size,_size,_size);
}

Does a method like that exists ?

In the end the idea would be to work with more than one property and to tweak object properties while preview the result.

4 Answers

Update object based on serialized field?

You can update a variable that has SerializeField, when you add the OnChangedCall Element.

Member Variable:

[SerializeField]
[OnChangedCall("onSerializedPropertyChange")]
private int _size;

You should now be able to just add the function as a String in the brackets and it should be called on change.

The method called must be public or it will produce an Error!

Method:

public void onSerializedPropertyChange() {
    transform.localScale = new Vector3(_size,_size,_size);
}

The OnChangedCall is a custom PropertyAttribute and needs to inherit from it.

OnChangedCall Class:

using System.Linq;
using UnityEngine;
using UnityEditor;
using System.Reflection;

public class OnChangedCallAttribute : PropertyAttribute
{
    public string methodName;
    public OnChangedCallAttribute(string methodNameNoArguments)
    {
        methodName = methodNameNoArguments;
    }
}

#if UNITY_EDITOR

[CustomPropertyDrawer(typeof(OnChangedCallAttribute))]
public class OnChangedCallAttributePropertyDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginChangeCheck();
        EditorGUI.PropertyField(position, property, label);
        if(EditorGUI.EndChangeCheck())
        {
            OnChangedCallAttribute at = attribute as OnChangedCallAttribute;
            MethodInfo method = property.serializedObject.targetObject.GetType().GetMethods().Where(m => m.Name == at.methodName).First();

            if (method != null && method.GetParameters().Count() == 0)// Only instantiate methods with 0 parameters
                method.Invoke(property.serializedObject.targetObject, null);
        }
    }
}

#endif

Here's my version of the OnChangedCallAttributePropertyDrawer from IndieGameDev's answer.

The main change being the addition of the following line

property.serializedObject.ApplyModifiedProperties();

This will ensure that the SerializedField is updated before the function is called, fixing a bug where the scene would not be properly updated when the serialized field is changed.

OnChangedCallAttributePropertyDrawer :

#if UNITY_EDITOR

[CustomPropertyDrawer(typeof(OnChangedCallAttribute))]
public class OnChangedCallAttributePropertyDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginChangeCheck();
        EditorGUI.PropertyField(position, property, label);
        if (!EditorGUI.EndChangeCheck()) return;

        var targetObject = property.serializedObject.targetObject;
        
        var callAttribute = attribute as OnChangedCallAttribute;
        var methodName = callAttribute?.MethodName;

        var classType = targetObject.GetType();
        var methodInfo = classType.GetMethods().FirstOrDefault(info => info.Name == methodName);

        // Update the serialized field
        property.serializedObject.ApplyModifiedProperties();
        
        // If we found a public function with the given name that takes no parameters, invoke it
        if (methodInfo != null && !methodInfo.GetParameters().Any())
        {
            methodInfo.Invoke(targetObject, null);
        }
        else
        {
            // TODO: Create proper exception
            Debug.LogError($"OnChangedCall error : No public function taking no " +
                           $"argument named {methodName} in class {classType.Name}");
        }
    }
}
#endif

Edit :

I'd just like to share a small tip for those who might not know, I'm not a fan of referencing functions with a string literal, it's usually prone to error and you most likely won't get help from your IDE.

You can instead use nameof() which will allow you to use your IDE autocompletion, Rider will also recognize the function as being used.

[SerializeField]
[OnChangedCall(nameof(UpdateHeuristic))]
private AStar.Heuristic heuristic;

public void UpdateHeuristic()
{
    _pathfinding?.SetHeuristic(heuristic);
}
 public class UIAnimatorGroup:MonoBehaviour
    {
        public UIAnimatorTransform _animatorTransform;

    }

[System.Serializable]
public class UIAnimatorTransform
{
    [HideInInspector] public Matrix4x4 LocalMatrix4X4; // 局部
    [HideInInspector] public Matrix4x4 GobleMatrix4X4;
    [HideInInspector] public UIAnimatorTransform parent;

    [SerializeField] 
    [OnChangedCall("onSerializedPropertyChange","UIAnimatorTransform")]
    private Vector3 position;

    [SerializeField] 
    [OnChangedCall("onSerializedPropertyChange","UIAnimatorTransform")]
    private Vector3 rotation;

    [SerializeField] 
    [OnChangedCall("onSerializedPropertyChange","UIAnimatorTransform")]
    private Vector3 scale;

    public void onSerializedPropertyChange()
    {
        SetLocalMatrixByTransform();
        CalculateGobleTransform();
    }
}

How about [ExecuteInEditMode]?

Then you can use the normal Update() method.

Related