如何强制Unity的SerializedProperty调用OnValidate回调?

时间:2019-11-07 18:59:42

标签: c# unity3d serialization

我正在编写一种扩展方法来简化SerializedProperty

的使用

这个想法是该方法的用户应该能够设置SerializedProperty的值而不必担心类型。如下例所示,应根据serializedPropertymyValue的类型来处理正确的类型。

常规使用SerializedProperty(Unity方式)的示例:

serializedProperty.intValue = myIntValue;
serializedProperty.floatValue = myFloatValue;
serializedProperty.boolValue = myBoolValue;
...

我的SerializedProperty方法扩展的预期语法

serializedProperty.SetValue(myValue);

SerializedProperty方法扩展的当前实现

public static void SetValue<TValue>(this SerializedProperty property, TValue value)
{
    if (property.hasMultipleDifferentValues)
        throw new ArgumentOutOfRangeException();

    Type parentType = property.serializedObject.targetObject.GetType();
    FieldInfo fieldInfo = parentType.GetField(property.propertyPath);
    fieldInfo.SetValue(property.serializedObject.targetObject, value);
}

问题:

此实现不调用OnValidate Unity回调。经常使用(mySerializedProperty.intValue = myInt)。

我的问题:有什么方法可以强制Unity在OnValidate上调用SerializedProperty方法?

我曾考虑过自己通过反射来调用OnValidate,但是由于这已经在Unity中实现,所以我想知道是否有办法将SerializedProperty标记为已更改或类似内容。


为测试此行为,我编写了以下测试(NUnit):

private IntMonoBehaviourMock _mockInstance;

[SetUp]
public void CallBeforeEachTest()
{
    GameObject gameObject = new GameObject();
    this._mockInstance = gameObject.AddComponent<IntMonoBehaviourMock>();
}

[Test]
// This test fails for the current implementation
public void SetValue_ToDifferentValue_OnValidateCalledOnce()
{
    this.SetValueOfSUT(0x1CE1CE);

    int numCallsBefore = this._mockInstance.NumTimesValidateCalled;
    this.SetValueOfSUT(0xBABE);
    int numCallsAfter = this._mockInstance.NumTimesValidateCalled;

    int actualNumCalls = numCallsAfter - numCallsBefore;
    Assert.AreEqual(1, actualNumCalls); // Fails
}

private void SetValueOfSUT(int value)
{
    string fieldName = "publicSerializedField"
    SerializedObject serializedObject = new SerializedObject(this._mockInstance);
    SerializedProperty sut = this._serializedObject.FindProperty(fieldName);

    // This is the call to function being tested!
    // Swapping this for sut.intValue = value, makes the test pass.
    // (But the purpose is to write a function that handles any type correctly)
    sut.SetValue(value);

    this._serializedObject.ApplyModifiedProperties();
}

我在测试中使用的 IntMonoBehaviourMock 的实现是:

public class IntMonoBehaviourMock: MonoBehaviour
{
    [SerializeField]
    public int publicSerializedField = default;

    public int NumTimesValidateCalled { get; private set; }

    protected void OnValidate()
    {
        this.NumTimesValidateCalled++;
    }
}

测试结果为:

Expected: 1
  But was:  0

1 个答案:

答案 0 :(得分:1)

您当然也可以在没有反思的情况下致电OnValidate

public void OnValidate() ...

,然后在编辑器调用中

theProperty.SetValue(5);
((IntMonoBehaviourMock)target).OnValidate();

或者我有时要做的是使Editor成为相应类型的子类,因此您也可以访问privateprotected方法。我通常会使用

将其保存在其他文件中
public partial class IntMonoBehaviourMock
{
    ...

#if UnityEditor
    private partial class IntMonoBehaviourMockEditor { }
#endif
}

,然后在单独的文件中

#if UnityEditor
public partial class IntMonoBehaviourMock
{
    [CustomEditor(typeof(IntMonoBehaviourMock)]
    private partial class IntMonoBehaviourMockEditor : Editor
    {
        ...
    }
}
#endif

可能的类型非常有限

AnimationCurve, BoundsInt, bool, Bounds, Color, double, float, int, long, 
Quaternion, RectInt, Rect, string, Vector2, Vector2Int, Vector3, Vector3Int, Vector4

和一个特殊的

UnityEngine.Object

(几乎)所有Unity类的父类。

我可以考虑使用类似的方法

var type = typeof(TValue);

if(type == typeof(int))
{
    serializedProperty.intValue = value;
}
else if(type == typeof(string)
{
    serializedProperty.stringValue = value;
}

...

else if(type.IsAssignableFrom(typeof(UnityEngine.Object)))
{
    serializedProperty.objectReferenceValue = value;
}
else
{
    Debug.LogError("Unassignable type: " + type.FullName);
}

实际上,您甚至不需要通用的,也可以简单地使用

var type = value.GetType();