c# - 为嵌套的 ScriptableObjects 构建编辑器以在纸牌游戏中组合能力

标签 c# unity3d unity3d-editor

我正在构建一个纸牌游戏,我希望有一个干净的纸牌能力架构。
我有一个带有卡片属性的 CardData ScriptableObject。我希望卡片的异能组合在一起来描述卡片的作用,比如一张名为 的卡片。 DrawAndHealCard 打出 2 张牌并治疗 5 点生命值。
我马上意识到这意味着我需要为 CardAbility 的每个变体提供一个具体的 Assets 。所以 DrawAndHealCard 引用了两个 Assets :抽奖卡2 HealPlayer5 .这太荒谬了,我希望所有数据都像在一个 DrawAndHealCard 上一样。
于是我了解到AssetDatabase.AddObjectToAsset() ,这似乎是正确的想法,我可以将能力作为 CardData Assets 的子 Assets ,而不必处理所有这些单独 Assets 的组织。所以现在我正在尝试构建一个 Editor管理这个,这是痛苦的。
我已经阅读了很多关于 Unity 序列化、SO、编辑器脚本等的内容……严重地撞到了这堵墙,并且即将降级到在架构上感觉不那么优雅的东西。如果有更好的方法来做到这一点,我也愿意接受关于完全不同路线的建议。
下面的代码被精简了,但它是我试图弄清楚的要点。
我现在所在的位置是 onAddCallback似乎正确添加了子 Assets ,但 onRemoveCallback不删除它。我的 清除所有能力 按钮确实有效。我找不到关于这些东西的任何好的文档或指南,所以我现在很迷茫。

// CardData.cs
[CreateAssetMenu(fileName = "CardData", menuName = "Card Game/CardData", order = 1)]
public class CardData : ScriptableObject
{
    public Sprite image;
    public string description;

    public CardAbility[] onPlayed;
}

// CardAbility.cs
public class CardAbility : ScriptableObject
{
    public abstract void Resolve();
}

// DrawCards.cs
public class DrawCards : CardAbility
{
    public int numCards = 1;
    public override void Resolve()
    {
        Deck.instance.DrawCards(numCards);
    }
}

// HealPlayer.cs
public class HealPlayer : CardAbility
{
    public int healAmt = 10;
    public override void Resolve()
    {
        Player.instance.Heal(healAmt);
    }
}

// CardDataEditor.cs
[CustomEditor(typeof(CardData))]
public class CardDataEditor : Editor
{
    private ReorderableList abilityList;

    public void OnEnable()
    {
        abilityList = new ReorderableList(
                serializedObject, 
                serializedObject.FindProperty("onPlayed"), 
                draggable: true,
                displayHeader: true,
                displayAddButton: true,
                displayRemoveButton: true);

        abilityList.onRemoveCallback = (ReorderableList l) => {
            l.serializedProperty.serializedObject.Update();
            var obj = l.serializedProperty.GetArrayElementAtIndex(l.index).objectReferenceValue;
            DestroyImmediate(obj, true);
            AssetDatabase.SaveAssets();
            l.serializedProperty.DeleteArrayElementAtIndex(l.index);
            l.serializedProperty.serializedObject.ApplyModifiedProperties();
        };

        abilityList.onAddCallback = (ReorderableList l) => {
            var index = l.serializedProperty.arraySize;
            l.serializedProperty.arraySize++;
            l.index = index;
            var element = l.serializedProperty.GetArrayElementAtIndex(index);

            // Hard coding a specific ability for now
            var cardData = (CardData)target;
            var newAbility = ScriptableObject.CreateInstance<DrawCards>();
            newAbility.name = "test";
            newAbility.numCards = 22;

            element.objectReferenceValue = newAbility;
            AssetDatabase.AddObjectToAsset(newAbility, cardData);
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
            serializedObject.ApplyModifiedProperties();
        };

        // Will use this to provide a menu of abilities to choose from.
        /*
        abilityList.onAddDropdownCallback = (Rect buttonRect, ReorderableList l) => {
            var menu = new GenericMenu();
            var guids = AssetDatabase.FindAssets("", new[]{"Assets/CardAbility"});
            foreach (var guid in guids) {
                var path = AssetDatabase.GUIDToAssetPath(guid);
                menu.AddItem(new GUIContent("Mobs/" + Path.GetFileNameWithoutExtension(path)), false, clickHandler, new WaveCreationParams() {Type = MobWave.WaveType.Mobs, Path = path});
            }
            menu.ShowAsContext();
        };
        */

        // Will use this to render CardAbility properties
        /*
        abilityList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {
        };
        */
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        DrawDefaultInspector();

        abilityList.DoLayoutList();

        // XXX: Ultimately don't expect to use these, experimenting with
        //      other ways of adding/deleting.
        
        if (GUILayout.Button("Add Ability")) {
            var cardData = (CardData)target;
            var newAbility = ScriptableObject.CreateInstance<CardAbility>();

            AssetDatabase.AddObjectToAsset(newAbility, cardData);
            AssetDatabase.SaveAssets();
        }

        if (GUILayout.Button("Clear All Abilities")) {
            var path = AssetDatabase.GetAssetPath(target);
            Object[] assets = AssetDatabase.LoadAllAssetRepresentationsAtPath(path);
            for (int i = 0; i < assets.Length; i++) {
                if (assets[i] is CardAbility) {
                    Object.DestroyImmediate(assets[i], true);
                }
            }
            AssetDatabase.SaveAssets();
        }

        serializedObject.ApplyModifiedProperties();
    }
}

最佳答案

好吧,我终于想通了。我阅读了一百篇堆栈溢出和论坛帖子试图理解这一点,所以我正在支付它,希望这可以帮助其他人解决这个问题。这会生成一个如下图所示的编辑器,其中 OnPlayed 是一个多态 ScriptableObjects 数组。这些 CardAbility SO 作为子 Assets 存储在拥有的 ScriptableObject (CardData) 上。还有更多要在这里清理,它可以变得更通用,但对于尝试这样做的其他人来说应该是一个好的开始。
inspector
[+] 按钮生成可添加的所有 CardAbility SO 的列表。
并且具体 CardAbility 的属性是动态呈现的。
关于这一切最奇怪的事情之一是您无法渲染 objectReferenceValue 的内容。使用 PropertyField ,你必须构造一个 SerializedObject首先是这样的:SerializedObject nestedObject = new SerializedObject(element.objectReferenceValue);感谢 Unity: Inspector can't find field of ScriptableObject对于那个小费。
ReorderableList 的其他一些重要资源:

  • https://va.lent.in/unity-make-your-lists-functional-with-reorderablelist/
  • https://sites.google.com/site/tuxnots/gamming/unity3d/unitymakeyourlistsfunctionalwithreorderablelist
  • https://sandordaemen.nl/blog/unity-3d-extending-the-editor-part-3/

  • // CardData.cs
    [CreateAssetMenu(fileName = "CardData", menuName = "Card Game/CardData", order = 1)]
    public class CardData : ScriptableObject
    {
        public enum CardType
        {
            Attack,
            Skill
        }
        public CardType type;
        public Sprite image;
        public string description;
    
        // XXX: Hidden in inspector because it will be drawn by custom Editor.
        [HideInInspector]
        public CardAbility[] onPlayed;
    }
    
    // CardAbility.cs
    public abstract class CardAbility : ScriptableObject
    {
        public abstract void Resolve();
    }
    
    // DrawCards.cs
    public class DrawCards : CardAbility
    {
        public int numCards = 1;
        public override void Resolve()
        {
            Deck.instance.DrawCards(numCards);
        }
    }
    
    // HealPlayer.cs
    public class HealPlayer : CardAbility
    {
        public int healAmount = 10;
        public override void Resolve()
        {
            Player.instance.Heal(healAmount);
        }
    }
    
    // CardDataEditor.cs
    [CustomEditor(typeof(CardData))]
    [CanEditMultipleObjects]
    public class CardDataEditor : Editor
    {
        private ReorderableList abilityList;
    
        private SerializedProperty onPlayedProp;
    
        private struct AbilityCreationParams {
            public string Path;
        }
    
        public void OnEnable()
        {
            onPlayedProp = serializedObject.FindProperty("onPlayed");
    
            abilityList = new ReorderableList(
                    serializedObject, 
                    onPlayedProp, 
                    draggable: true,
                    displayHeader: true,
                    displayAddButton: true,
                    displayRemoveButton: true);
    
            abilityList.drawHeaderCallback = (Rect rect) => {
                EditorGUI.LabelField(rect, "OnPlayed Abilities");
            };
    
            abilityList.onRemoveCallback = (ReorderableList l) => {
                var element = l.serializedProperty.GetArrayElementAtIndex(l.index); 
                var obj = element.objectReferenceValue;
    
                AssetDatabase.RemoveObjectFromAsset(obj);
    
                DestroyImmediate(obj, true);
                l.serializedProperty.DeleteArrayElementAtIndex(l.index);
    
                AssetDatabase.SaveAssets();
                AssetDatabase.Refresh();
                
                ReorderableList.defaultBehaviours.DoRemoveButton(l);
            };
    
            abilityList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {
                SerializedProperty element = onPlayedProp.GetArrayElementAtIndex(index);
    
                rect.y += 2;
                rect.width -= 10;
                rect.height = EditorGUIUtility.singleLineHeight;
    
                if (element.objectReferenceValue == null) {
                    return;
                }
                string label = element.objectReferenceValue.name;
                EditorGUI.LabelField(rect, label, EditorStyles.boldLabel);
    
                // Convert this element's data to a SerializedObject so we can iterate
                // through each SerializedProperty and render a PropertyField.
                SerializedObject nestedObject = new SerializedObject(element.objectReferenceValue);
    
                // Loop over all properties and render them
                SerializedProperty prop = nestedObject.GetIterator();
                float y = rect.y;
                while (prop.NextVisible(true)) {
                    if (prop.name == "m_Script") {
                        continue;
                    }
    
                    rect.y += EditorGUIUtility.singleLineHeight;
                    EditorGUI.PropertyField(rect, prop);
                }
    
                nestedObject.ApplyModifiedProperties();
    
                // Mark edits for saving
                if (GUI.changed) {
                    EditorUtility.SetDirty(target);
                }
    
            };
    
            abilityList.elementHeightCallback = (int index) => {
                float baseProp = EditorGUI.GetPropertyHeight(
                    abilityList.serializedProperty.GetArrayElementAtIndex(index), true);
    
                float additionalProps = 0;
                SerializedProperty element = onPlayedProp.GetArrayElementAtIndex(index);
                if (element.objectReferenceValue != null) {
                    SerializedObject ability = new SerializedObject(element.objectReferenceValue);
                    SerializedProperty prop = ability.GetIterator();
                    while (prop.NextVisible(true)) {
                        // XXX: This logic stays in sync with loop in drawElementCallback.
                        if (prop.name == "m_Script") {
                            continue;
                        }
                        additionalProps += EditorGUIUtility.singleLineHeight;
                    }
                }
    
                float spacingBetweenElements = EditorGUIUtility.singleLineHeight / 2;
    
                return baseProp + spacingBetweenElements + additionalProps;
            };
    
            abilityList.onAddDropdownCallback = (Rect buttonRect, ReorderableList l) => {
                var menu = new GenericMenu();
                var guids = AssetDatabase.FindAssets("", new[]{"Assets/CardAbility"});
                foreach (var guid in guids) {
                    var path = AssetDatabase.GUIDToAssetPath(guid);
                    var type = AssetDatabase.LoadAssetAtPath(path, typeof(UnityEngine.Object));
                    if (type.name == "CardAbility") {
                        continue;
                    }
    
                    menu.AddItem(
                        new GUIContent(Path.GetFileNameWithoutExtension(path)),
                        false,
                        addClickHandler,
                        new AbilityCreationParams() {Path = path});
                }
                menu.ShowAsContext();
            };
        }
    
        private void addClickHandler(object dataObj) {
            // Make room in list
            var data = (AbilityCreationParams)dataObj;
            var index = abilityList.serializedProperty.arraySize;
            abilityList.serializedProperty.arraySize++;
            abilityList.index = index;
            var element = abilityList.serializedProperty.GetArrayElementAtIndex(index);
    
            // Create the new Ability
            var type = AssetDatabase.LoadAssetAtPath(data.Path, typeof(UnityEngine.Object));
            var newAbility = ScriptableObject.CreateInstance(type.name);
            newAbility.name = type.name;
    
            // Add it to CardData
            var cardData = (CardData)target;
            AssetDatabase.AddObjectToAsset(newAbility, cardData);
            AssetDatabase.SaveAssets();
            element.objectReferenceValue = newAbility;
            serializedObject.ApplyModifiedProperties();
        }
    
        public override void OnInspectorGUI()
        {
            serializedObject.Update();
    
            DrawDefaultInspector();
    
            abilityList.DoLayoutList();
    
            if (GUILayout.Button("Delete All Abilities")) {
                var path = AssetDatabase.GetAssetPath(target);
                Object[] assets = AssetDatabase.LoadAllAssetRepresentationsAtPath(path);
                for (int i = 0; i < assets.Length; i++) {
                    if (assets[i] is CardAbility) {
                        Object.DestroyImmediate(assets[i], true);
                    }
                }
                AssetDatabase.SaveAssets();
            }
    
            serializedObject.ApplyModifiedProperties();
        }
    }
    

    关于c# - 为嵌套的 ScriptableObjects 构建编辑器以在纸牌游戏中组合能力,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62768791/

    相关文章:

    c# - Unity3d 点击并拖动 Gizmos

    c# - 让EditorWindow的最后一个EditorGUILayout填充剩余空间?

    时间:2019-05-08 标签:c#asp.net : capture the raiserror() message from an SQL stored procedure

    c# - DataLoadOptions 等同于 LINQ to Entities?

    C# - Console.WriteLine() 不显示第二个参数

    c# - Unity插件:编码(marshal)C++ double *将C#中的数组翻倍

    c# - 我如何计算在体素游戏中应该首先加载哪些 block ?

    c# - NUnit 测试 - 循环 - C#

    c# - 如何为 UIImage 添加渐变阴影

    c# - Unity3d 可视化脚本框架在幕后如何工作?