接口在编辑器中的拓展

前言

经常会希望在Unity编辑器中有这样一种功能的实现,可以直接引用(序列化)一个实现了指定接口的组件。
面向接口可以大大降低代码的耦合,实现依赖反转,同时更加灵活,后续的修改与扩展都更加方便。
但官方就是不做,可以从这个帖子中看到官方的态度。
所以下面就列举一下自己实现这个功能的各种方法。

问题描述

假使有这样一个接口

public interface ICanGetBool
{
    bool GetBool();
}

然后有若干MonoBehaviour

public class TestMono : MonoBehaviour
{
    public ICanGetBool canGetBool;
}

public class CanGetBoolMono : MonoBehaviour,ICanGetBool
{
    public bool GetBool()
    {
        return true;
    }
}

public class CanGetBoolFalseMono : MonoBehaviour,ICanGetBool
{
    public bool GetBool()
    {
        return false;
    }
}

目的是在编辑器中将一CanGetBoolMono或者CanGetBoolFalseMono的实例拖拽到TestMonocanGetBool属性上去。

OnValidate判断

思路是用一个通用的比如Component属性来接收,然后在OnValidate中判断是否是对应的接口,不是就重置为null。然后在使用的时候要手动转换一次。

public class TestMono : MonoBehaviour
{
    public ICanGetBool canGetBool;
    public Component canGetBoolComponent;
    
    public void Start()
    {
        canGetBool = canGetBoolComponent as ICanGetBool;
        Debug.Log(canGetBool.GetBool());
    }

    private void OnValidate()
    {
        if (canGetBoolComponent is ICanGetBool component)
        {
            canGetBool = component;
            Debug.Log(canGetBool.GetBool());
        }
        else
        {
            Debug.LogError("interface not implemented");
            canGetBoolComponent = null;
        }
    }
}

这种简单快捷,但缺点是要为每个类似需求的MonoBehaviour都写对应的OnValidate,还是不方便。

构造容器

也是大同小异,区别在于利用泛型生成一个新的类,用新的类型作为容器来存储真正的对象。也是可以放入任意类型,在PropertyDrawer中验证类型是否符合。
摘自

[Serializable]
public abstract class ATypedContainer
{
    [CanBeNull]
    [SerializeField]
    protected MonoBehaviour obj = default;

    public void Validate()
    {
        OnValidate();
    }

    protected abstract void OnValidate();
}

[Serializable]
public class TypedContainer<T> : ATypedContainer where T : class
{
    [CanBeNull]
    public T Value => obj as T;

    protected override void OnValidate()
    {
        if (Value == null)
            obj = null;
    }
}

[CustomPropertyDrawer(typeof(ATypedContainer), true)]
public class TypedContainerClassDrawer : PropertyDrawer
{
    const string OBJ_FIELD_NAME = "obj";

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var tc = fieldInfo.GetValue(property.serializedObject.targetObject) as ATypedContainer;
        tc?.Validate();

        var objProp = property.FindPropertyRelative(OBJ_FIELD_NAME);
        if (objProp == null)
            throw new InvalidCastException($"Can't find {OBJ_FIELD_NAME} field in {property.type}");

        EditorGUI.PropertyField(position, objProp, label);
    }
}

[Serializable]
public class CanGetBoolContainer: TypedContainer<ICanGetBool> {}

这种省去了在目标MonoBehaviour中写模板代码,可以为每个接口实现一个容器类,也可以在使用时直接声明TypedContainer<ICanGetBool>

SerializableInterface

最后介绍一个项目Unity3D-SerializableInterface
不仅实现了第二种的容器类的方式,还支持从资源文件,场景组件,甚至仅引用一个接口,一共三种方式来序列化接口。
而且提供了untiyPackage的安装方式。强烈推荐。


接口在编辑器中的拓展
https://www.kuanmi.top/2023/06/14/InterfaceInEditor/
作者
KuanMi
发布于
2023年6月15日
许可协议