Eu4ng / TIL

Today I Learned
1 stars 0 forks source link

[Unity] 인터페이스 직렬화 #295

Open Eu4ng opened 9 months ago

Eu4ng commented 9 months ago

인터페이스 직렬화

참고

개요

UnityEngine.Object 레퍼런스는 C# 클래스에 저장될 수 없기 때문에 인터페이스 변수에는 SerializeField 어트리뷰트가 적용되지 않습니다.

Eu4ng commented 9 months ago

Two Fields Solution

참고

Feature Request - Serialized interface fields #23

아이디어

using UnityEngine;

public class Demo : MonoBehaviour
{
    public Object characterDataObject;
    private ICharacterData characterData;

    private void Start()
    {
        characterData = characterDataObject as ICharacterData;
    }
}

문제점

  1. 잘못된 오브젝트 레퍼런스 할당 가능성이 존재합니다.
  2. 캐스팅 코드가 필수입니다.

해결 방법

  1. OnBeforeSerialize 에서 인터페이스 상속 여부 확인
  2. OnAfterDeserialize 에서 인터페이스 캐스팅

OnBeforeSerialize 에서 인터페이스 캐스팅을 하면 null 이 되는데 이유를 정확히 모르겠습니다.

예시 코드

// SerializedInterface.cs

using System;
using UnityEngine;

[Serializable]
public class SerializedInterface<T> : ISerializationCallbackReceiver where T : class
{
    /* 필드 */
    [SerializeField] MonoBehaviour m_Reference;
    T m_Interface;

    /* API */
    public T Get() => m_Interface;

    /* ISerializationCallbackReceiver 인터페이스 */
    public void OnBeforeSerialize()
    {
        // null 검사
        if (m_Reference == null) return;

        // 인터페이스 검사
        if (m_Reference is not T)
        {
            m_Reference = m_Reference.GetComponent<T>() as MonoBehaviour;
        }
    }

    public void OnAfterDeserialize()
    {
        // null 검사
        if (m_Reference == null) return;

        // 인터페이스 할당
        m_Interface = m_Reference as T;
    }
}
// Test.cs

using UnityEngine;

public interface ITest
{
    public void Print();
}

public class Test : MonoBehaviour
{
    [SerializeField] SerializedInterface<ITest> testInterface;

    ITest TestInterface => testInterface.Get();

    void Start()
    {
        TestInterface.Print();
    }
}

image

Eu4ng commented 9 months ago

// TODO 연구 예정

PropertyDrawer

WeakReference

Entities 패키지의 WeakObjectReference 의 경우 커스텀 프로퍼티를 사용하였습니다. 이를 활용하면 동일한 방식으로 구성할 수 있을 것 같습니다.

Normal

image

Debug

image

WeakObjectReference.cs

#if !UNITY_DOTSRUNTIME
using System;
using System.Runtime.InteropServices;
using Unity.Collections;
using Unity.Entities.Serialization;
using UnityEngine;

namespace Unity.Entities.Content
{
    /// <summary>
    /// Weak reference to an object.  This allows control over when an object is loaded and unloaded.
    /// </summary>
    /// <typeparam name="TObject">The type of UnityEngine.Object this reference points to.</typeparam>
    [Serializable]
    public struct WeakObjectReference<TObject> : IEquatable<WeakObjectReference<TObject>> where TObject : UnityEngine.Object
    {
        [SerializeField]
        internal UntypedWeakReferenceId Id;

        /// <summary>
        /// Returns true if the reference has a valid id.  In the editor, additional checks for the correct GenerationType and the existence of the referenced asset are performed.
        /// </summary>
        public bool IsReferenceValid
        {
            get
            {
                if (!Id.IsValid)
                    return false;
#if UNITY_EDITOR
                if (Id.GenerationType != WeakReferenceGenerationType.UnityObject)
                    return false;

                if (UntypedWeakReferenceId.GetEditorObject(Id) == null)
                    return false;
#endif
                return true;
            }
        }

        /// <summary>
        /// Get the loading status of the referenced object.
        /// </summary>
        public ObjectLoadingStatus LoadingStatus => RuntimeContentManager.GetObjectLoadingStatus(Id);

        /// <summary>
        /// The value of the referenced object.  This returns a valid object if IsLoaded is true.
        /// </summary>
        public TObject Result
        {
            get
            {
                return RuntimeContentManager.GetObjectValue<TObject>(Id);
            }
        }

#if UNITY_EDITOR
        public WeakObjectReference(TObject unityObject)
        {
            this.Id = UntypedWeakReferenceId.CreateFromObjectInstance(unityObject);
        }
#endif

        /// <summary>
        /// Construct a WeakObjectReference with an existing WeakObjectReference id <paramref name="id"/>.
        /// </summary>
        /// <param name="id">Existing id for some weakly referenced data.</param>
        public WeakObjectReference(UntypedWeakReferenceId id)
        {
            this.Id = id;
        }

        /// <summary>
        /// Directs the object to begin loading.  This will increase the reference count for each call to the same id.  Release must be called for each Load call to properly release resources.
        /// </summary>
        public void LoadAsync()
        {
            RuntimeContentManager.LoadObjectAsync(Id);
        }

        /// <summary>
        /// Releases the object.  This will decrement the reference count of this object.  When an objects reference count reaches 0, the archive file is released.  The archive file is only
        /// unloaded when its reference count reaches zero, which will then release the archive it was loaded from.  Archives will be unmounted when their reference count reaches 0.
        /// </summary>
        public void Release()
        {
            RuntimeContentManager.ReleaseObjectAsync(Id);
        }

        /// <summary>
        /// Wait for object load in main thread.  This will force synchronous loading and may cause performance issues.
        /// </summary>
        /// <param name="timeoutMs">The number of milliseconds to wait.  If set to 0, the load will either complet or fail before returning.</param>
        /// <returns>True if the load completes within the timeout.</returns>
        public bool WaitForCompletion(int timeoutMs = 0)
        {
            return RuntimeContentManager.WaitForObjectCompletion(Id, timeoutMs);
        }

        /// <summary>
        /// String conversion override.
        /// </summary>
        /// <returns>String representation of reference which includes type, guid and local id.</returns>
        public override string ToString() => $"WeakObjectReference<{typeof(TObject)}> -> {Id}";

        /// <inheritdoc/>
        public bool Equals(WeakObjectReference<TObject> other)
        {
            return Id.Equals(other.Id);
        }

        /// <summary>
        /// Gets the hash code of this reference.
        /// </summary>
        /// <returns>The hash code of this reference.</returns>
        public override int GetHashCode()
        {
            return Id.GetHashCode();
        }
    }
}
#endif

WeakReferencePropertyDrawer.cs

#if !UNITY_DOTSRUNTIME
using UnityEditor;
using UnityEngine;
using Unity.Entities.Content;
using Unity.Entities.Serialization;
using System;
using System.Collections;
using UnityEditor.UIElements;
using UnityEngine.UIElements;

namespace Unity.Entities.Editor
{
    abstract class WeakReferencePropertyDrawerBase : PropertyDrawer
    {
        public abstract WeakReferenceGenerationType GenerationType { get; }

        public override VisualElement CreatePropertyGUI(SerializedProperty property)
        {
            var container = new VisualElement();

            var targetObjectType = DetermineTargetType(fieldInfo.FieldType);
            var gidName = $"{property.propertyPath}.Id.GlobalId.AssetGUID.Value";
            var propertyX = property.serializedObject.FindProperty($"{gidName}.x");
            var propertyY = property.serializedObject.FindProperty($"{gidName}.y");
            var propertyZ = property.serializedObject.FindProperty($"{gidName}.z");
            var propertyW = property.serializedObject.FindProperty($"{gidName}.w");
            var propertyObjectId = property.serializedObject.FindProperty($"{property.propertyPath}.Id.GlobalId.SceneObjectIdentifier0");
            var propertyIdType = property.serializedObject.FindProperty($"{property.propertyPath}.Id.GlobalId.IdentifierType");
            var genType = property.serializedObject.FindProperty($"{property.propertyPath}.Id.GenerationType");
            var uwrid = new UntypedWeakReferenceId(
                new Hash128((uint)propertyX.longValue, (uint)propertyY.longValue, (uint)propertyZ.longValue, (uint)propertyW.longValue),
                propertyObjectId.longValue,
                propertyIdType.intValue,
                GenerationType);

            var currObject = UntypedWeakReferenceId.GetEditorObject(uwrid);
            var objectField = new ObjectField
            {
                objectType = targetObjectType,
                allowSceneObjects = false,
                label = property.name
            };

            objectField.SetValueWithoutNotify(currObject);
            objectField.RegisterCallback((ChangeEvent<UnityEngine.Object> e) =>
            {
                var uwr = UntypedWeakReferenceId.CreateFromObjectInstance(e.newValue);
                propertyX.longValue = uwr.GlobalId.AssetGUID.Value.x;
                propertyY.longValue = uwr.GlobalId.AssetGUID.Value.y;
                propertyZ.longValue = uwr.GlobalId.AssetGUID.Value.z;
                propertyW.longValue = uwr.GlobalId.AssetGUID.Value.w;
                propertyObjectId.longValue = uwr.GlobalId.SceneObjectIdentifier0;
                propertyIdType.intValue = uwr.GlobalId.IdentifierType;
                genType.enumValueIndex = (int)GenerationType;
                property.serializedObject.ApplyModifiedProperties();
            });
            container.Add(objectField);

            return container;
        }

        private static Type GetElementType(Type t)
        {
            if (t == typeof(WeakObjectSceneReference))
                return typeof(SceneAsset);
            if (t == typeof(EntityPrefabReference))
                return typeof(GameObject);
            if (t == typeof(EntitySceneReference))
                return typeof(SceneAsset);
            if (t.IsGenericType)
                return t.GetGenericArguments()[0];
            return t;
        }

        internal static Type DetermineTargetType(Type t)
        {
            if (typeof(IEnumerable).IsAssignableFrom(t) && t.IsGenericType)
            {
                return GetElementType(t.GetGenericArguments()[0]);
            }
            else if (t.IsArray)
            {
                return GetElementType(t.GetElementType());
            }
            return GetElementType(t);
        }
    }

    [CustomPropertyDrawer(typeof(WeakObjectReference<>), true)]
    class WeakObjectReferencePropertyDrawer : WeakReferencePropertyDrawerBase
    {
        public override WeakReferenceGenerationType GenerationType => WeakReferenceGenerationType.UnityObject;
    }

    [CustomPropertyDrawer(typeof(WeakObjectSceneReference), true)]
    class WeakObjectSceneReferencePropertyDrawer : WeakReferencePropertyDrawerBase
    {
        public override WeakReferenceGenerationType GenerationType => WeakReferenceGenerationType.GameObjectScene;
    }

    [CustomPropertyDrawer(typeof(EntitySceneReference), true)]
    class EntitySceneReferencePropertyDrawer : WeakReferencePropertyDrawerBase
    {
        public override WeakReferenceGenerationType GenerationType => WeakReferenceGenerationType.EntityScene;
    }

    [CustomPropertyDrawer(typeof(EntityPrefabReference), true)]
    class EntityPrefabReferencePropertyDrawer : WeakReferencePropertyDrawerBase
    {
        public override WeakReferenceGenerationType GenerationType => WeakReferenceGenerationType.EntityPrefab;
    }
}
#endif