sttz/trimmer

Support for pre-existing PropertyDrawers?

Closed this issue · 6 comments

I'm in a situation where I need an Option<T> for a type that's very easy to serialize, but difficult to draw in the editor. Here's my use case.

When using OptionAsset<TUnity> to conditionally include assets in builds, these assets are inserted into the first scene at build-time. However, this increases the size and load time of the first scene. In situations where this isn't acceptable, the Addressable Asset System is a good alternative; all assets are referred to with AssetReference objects.

AssetReferences and their subclasses are plain objects, not Unity Objects, meaning they cannot be used with OptionAsset<TUnity>. I could just write an OptionAssetReference that saves the AssetGUID property. And I can.

The problem is Option.EditGUI; AssetReferences have a very simple representation, but their property drawers do a lot of heavy lifting as seen here:

AssetReference usage

It's more than can be replicated with EditorGUI.ObjectField. However, all AssetReference classes have their own PropertyDrawers. Is it possible to use existing PropertyDrawers in custom Trimmer Options? If so, how?

sttz commented

Trimmer uses a custom serialization system to support polymorphic serialization. This was before Unity added support for this using the SerializeReference attribute.

As far as I can tell, there is no way to use a property drawer without a SerializedProperty and for a serialized property, you need to use Unity's serialization system.

A workaround could be to have a dummy ScriptableObject that contains an AssetReference field. Then you can create a SerializedObject from an instance of it, get the SerializedProperty for the field and use EditorGUILayout.PropertyField to edit it. You should be able to set the current value on the serialized property before and read the updated value after, as well as reusing a single instance.

I might look into discarding Trimmer's own serialization system in favor of SerializeReference, which should make it possible to use the regular property drawers by default. But I don't have an ETA for this and it wold limit Trimmer to Unity 2019.4+.

Trimmer uses a custom serialization system to support polymorphic serialization. This was before Unity added support for this using the SerializeReference attribute.

As far as I can tell, there is no way to use a property drawer without a SerializedProperty and for a serialized property, you need to use Unity's serialization system.

Dang.

A workaround could be to have a dummy ScriptableObject that contains an AssetReference field. Then you can create a SerializedObject from an instance of it, get the SerializedProperty for the field and use EditorGUILayout.PropertyField to edit it. You should be able to set the current value on the serialized property before and read the updated value after, as well as reusing a single instance.

What type of Option would I need? A custom Option<AssetReference>? And would I need to create an asset for this ScriptableObject, or could I just forget about it once I get this workaround in place?

I might look into discarding Trimmer's own serialization system in favor of SerializeReference, which should make it possible to use the regular property drawers by default. But I don't have an ETA for this and it wold limit Trimmer to Unity 2019.4+.

I would have no objection to this, but I'm not going to assume the same of others.

I've almost got this, but I'm having problems. Here's my base Option:

using sttz.Trimmer;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;

#endif

namespace CorundumGames.Options.AssetReference
{
    public abstract class OptionAssetReferenceBase<TAssetReference> : Option<TAssetReference>
        where TAssetReference : UnityEngine.AddressableAssets.AssetReference
    {
        public override string Save(TAssetReference input)
        {
            return input.AssetGUID;
        }

        public override string Save()
        {
            return Save(Value);
        }

        public override void Load(string input)
        {
            Value = Parse(input);
        }

        public override TAssetReference Parse(string input)
        {
            if (string.IsNullOrEmpty(input))
                return DefaultValue;

            return ConstructAssetReference(input);
        }

        protected abstract TAssetReference ConstructAssetReference(string guid);


#if UNITY_EDITOR
        private static BaseAssetReferenceDummy<TAssetReference> _dummyInstance;

        internal abstract BaseAssetReferenceDummy<TAssetReference> CreateDummy();

        public override bool EditGUI()
        {
            using (var changeCheck = new EditorGUI.ChangeCheckScope())
            {
                if (_dummyInstance == null)
                {
                    _dummyInstance = CreateDummy();
                }

                using (var serializedObject = new SerializedObject(_dummyInstance))
                {
                    using (var property =
                        serializedObject.FindProperty(
                            nameof(BaseAssetReferenceDummy<TAssetReference>.reference)
                        )
                    )
                    {
                        property.managedReferenceValue = Value;
                        EditorGUILayout.PropertyField(property, GUIContent.none);

                        Value = _dummyInstance.reference;
                        return changeCheck.changed;
                    }
                }
            }
        }
#endif
    }
}

...and here's a subclass for AssetReferenceGameObjects:

using UnityEngine;

namespace CorundumGames.Options.AssetReference
{
    using UnityEngine.AddressableAssets;

    public abstract class OptionAssetReferenceGameObject : OptionAssetReferenceBase<AssetReferenceGameObject>
    {
        protected override AssetReferenceGameObject ConstructAssetReference(string guid)
        {
            return new AssetReferenceGameObject(guid);
        }

#if UNITY_EDITOR
        private sealed class Dummy : BaseAssetReferenceDummy<AssetReferenceGameObject>
        {
        }

        internal override BaseAssetReferenceDummy<AssetReferenceGameObject> CreateDummy()
        {
            return ScriptableObject.CreateInstance<Dummy>();
        }
#endif
    }
}

(I have one for plain AssetReferences as well, and it's pretty much the same except for the type arguments.)

When I set property.managedReferenceValue = Value; inside EditGUI(), an InvalidOperationException is thrown and Trimmer's UI is not drawn:

InvalidOperationException: Attempting to set the managed reference value on a SerializedProperty that is set to a 'AssetReferenceGameObject'

What could I be doing wrong?

@sttz Any thoughts on this?

sttz commented

With SerializedObject/SerializedProperty, you edit the serialized data and need to check what the properties actually are. Only with a property of type UnityEngine.Object can you use managedReferenceValue.

In case of AssetReference and its subclasses, it doesn't contain an object reference but instead two string properties "m_AssetGUID" and "m_SubObjectName", which are used to identify the object*.

To set/get those from the dummy object, you'd use something like this:

var so = new SerializedObject(dummy);
var guidProp = so.FindProperty("reference.m_AssetGUID");
var nameProp = so.FindProperty("reference.m_SubObjectName");

var guid = guidProp.stringValue;
var subName = nameProp.stringValue;

* This also means you probably need to save the combined guid/name in your option instead of only the guid. Otherwise I suspect referencing sub-assets will always revert to referencing the main asset.

Hope this helps!

I'll try that out, thanks! When I get something working I'll share my implementation here. I'll probably also polish it up in a separate package later on.