SRPG游戏开发(三十四)第八章 游戏中的数据 - 四 数据编辑器(Data Editor)

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/darkrabbit/article/details/84451899

返回总目录

第八章 游戏中的数据(Data in Game)

在之前的章节中,我们进行地图对象的生成,移动等操作。

这一章本来可以进行战斗的编写,不过数据缺失是一个问题。

所以这一章我们先来建立一些数据,以及如何编辑它们,是否需要生成配置文件等。



四 数据编辑器(Data Editor)

我们将进行一个EditorWindow的编写,并将数据显示在上面。

要解决的主要问题是显示的数量(上一节中提过的,如果数量过多,会造成渲染卡顿)。

除了上一节内容,我没有进行更详细的编辑,你可以对每个数据文件都进行PropertyDrawer的编写。


1 创建编辑器窗口(Create Editor Window)

我们先来创建一个最基本的窗口(EditorSrpgDataEditorWindow.cs):

namespace DR.Book.SRPG_Dev.Models
{
    public class EditorSrpgDataEditorWindow : EditorWindow
    {
        private static EditorSrpgDataEditorWindow s_Window;
        public static EditorSrpgDataEditorWindow OpenEditorSrpgDataEditorWindow()
        {
            if (s_Window != null)
            {
                s_Window.Focus();
                return s_Window;
            }
            s_Window = EditorWindow.GetWindow<EditorSrpgDataEditorWindow>(false, "SRPG Data");
            s_Window.minSize = new Vector2(480, 480);
            s_Window.Show();
            return s_Window;
        }

        private EditorSrpgData m_SrpgData;
        private SerializedObject m_SerializedObject;

        public EditorSrpgData srpgData
        {
            get { return m_SrpgData; }
            set
            {
                if (m_SrpgData == value)
                {
                    return;
                }
                m_SrpgData = value;

                // 删除以前的
                if (m_SerializedObject != null)
                {
                    m_SerializedObject.Dispose();
                    m_SerializedObject = null;
                }

                // 重新建立
                if (m_SrpgData != null)
                {
                    m_SerializedObject = new SerializedObject(m_SrpgData);
                }
            }
        }

        private void OnDestroy()
        {
            this.srpgData = null;
            s_Window = null;
        }

        // TODO 其它需要添加的
    }
}

你会看到我没有使用[MenuItem("Window/SRPG/SRPG Data Editor")],因为我不打算在菜单中打开它。

1.1 打开窗口(Open Editor Window)

将打开窗口和资源放在一起,建立文件EditorSrpgDataEditor.cs

using UnityEngine;
using UnityEditor;

namespace DR.Book.SRPG_Dev.Models
{
    [CustomEditor(typeof(EditorSrpgData))]
    public class EditorSrpgDataEditor : Editor
    {
        #region Property
        public EditorSrpgData srpgData
        {
            get { return target as EditorSrpgData; }
        }
        #endregion

        #region Unity Callback
        public override void OnInspectorGUI()
        {
            EditorGUI.BeginDisabledGroup(true);
            base.OnInspectorGUI();
            EditorGUI.EndDisabledGroup();

            if (GUILayout.Button("Edit Datas"))
            {
                EditorSrpgDataEditorWindow window = EditorSrpgDataEditorWindow.OpenEditorSrpgDataEditorWindow();
                window.srpgData = srpgData;
            }
        }
        #endregion
    }
}

1.2 选择数据(Select Data)

在窗口中不要全部渲染,最好可按文件选择我们的数据。

EditorSrpgData中添加属性:

        public enum ConfigType
        {
            MoveConsumption,
            Class,
            Character,
            Item,
            Text
        }

        [SerializeField]
        public ConfigType currentConfig = ConfigType.MoveConsumption;

最终效果:

EditorSrpgData Inspector

  • 图 8.9 EditorSrpgData Inspector

2 功能接口(IEditorConfigSerializer.cs)

我们的编辑器至少具有的功能:

  • 编辑数据;

  • 保存配置文件;

  • 读取配置文件。

除了这些功能外,最好还能排序(根据key),还要能检测是否有重复的key

建立文件IEditorConfigSerializer.cs

using System;

namespace DR.Book.SRPG_Dev.Models
{
    public interface IEditorConfigSerializer
    {
        Array EditorGetKeys();
        void EditorSortDatas();
        byte[] EditorSerializeToBytes();
        void EditorDeserializeToObject(byte[] bytes);
    }
}

有了接口,我们在EditorSrpgData.cs中加入方法:

        public IEditorConfigSerializer GetCurConfig()
        {
            switch (currentConfig)
            {
                case ConfigType.MoveConsumption:
                    return moveConsumptionConfig;
                case ConfigType.Class:
                    return classConfig;
                case ConfigType.Character:
                    return characterInfoConfig;
                case ConfigType.Item:
                    return itemInfoConfig;
                case ConfigType.Text:
                    return textInfoConfig;
                default:
                    return null;
            }
        }

同时我们让所有配置文件都继承这个接口:

    [Serializable]
    public abstract class BaseXmlConfig<TKey, TData> : XmlConfigFile, IEditorConfigSerializer
        where TData : class, IConfigData<TKey>

    [Serializable]
    public class BaseTxtConfig<TKey, TData> : TxtConfigFile, IEditorConfigSerializer
        where TData : class, ITxtConfigData<TKey>, new()

获取key与排序都是对datas进行,所以应该是一样的:

        /// <summary>
        /// 获取所有Key
        /// </summary>
        /// <returns></returns>
        Array IEditorConfigSerializer.EditorGetKeys()
        {
            if (datas == null)
            {
                return default(Array);
            }
            else
            {
                return datas.Select(data => data.GetKey()).ToArray();
            }
        }

        void IEditorConfigSerializer.EditorSortDatas()
        {
            if (datas != null)
            {
                Array.Sort(datas, (data1, data2) =>
                {
                    return data1.GetKey().GetHashCode().CompareTo(data2.GetKey().GetHashCode());
                });
            }
        }

2.1 保存与读取Xml文件(Save or Load Xml File)

这个和我们的序列化是差不多,曾经我们已经写过了,这里不再阐述。

序列化:

        public virtual byte[] EditorSerializeToBytes()
        {
            byte[] bytes;
            using (MemoryStream ms = new MemoryStream())
            {
                using (StreamWriter sw = new StreamWriter(ms, Encoding.UTF8))
                {
                    XmlSerializer xs = new XmlSerializer(GetType());
                    XmlSerializerNamespaces xsn = new XmlSerializerNamespaces();
                    xsn.Add("", "");
                    xs.Serialize(sw, this, xsn);
                    bytes = ms.ToArray();
                }
            }
            return bytes;
        }

反序列化:

        public virtual void EditorDeserializeToObject(byte[] bytes)
        {
            XmlConfigFile config;
            using (MemoryStream ms = new MemoryStream(bytes))
            {
                using (StreamReader sr = new StreamReader(ms, Encoding.UTF8))
                {
                    XmlSerializer xs = new XmlSerializer(GetType());
                    config = xs.Deserialize(sr) as XmlConfigFile;
                }
            }
            datas = (config as BaseXmlConfig<TKey, TData>).datas;
        }

2.2 保存与读取Txt文件(Save or Load Txt File)

我们之前写过txt文件的反序列化,但那是对字典的;这里我们要对保留的datas进行填充。

也和之前说的一样, txt文件可没有现成的序列化方法,每个文件都是不同的。 这样每个文件都重写一次方法显然是很麻烦, 所以我们采用反射方法,这样除了特殊的txt外,都可以用这个通用的方法。

使用反射保存文件:

        public virtual byte[] EditorSerializeToBytes()
        {
            if (datas == null)
            {
                datas = new TData[0];
            }

            StringBuilder builder = new StringBuilder();

            // 反射获取所有public字段
            Type dataType = typeof(TData);
            FieldInfo[] fields = dataType.GetFields(BindingFlags.Instance 
                | BindingFlags.Public 
                | BindingFlags.GetField 
                | BindingFlags.SetField);

            if (fields.Length != 0)
            {
                // 每一列的名字
                string[] line = fields.Select(field => field.Name).ToArray();
                builder.AppendLine(k_CommentingPrefix + string.Join("\t", line));

                // 每一行数据
                for (int i = 0; i < datas.Length; i++)
                {
                    line = fields.Select(field => field.GetValue(datas[i]).ToString()).ToArray();
                    builder.AppendLine(string.Join("\t", line));
                }
            }
            return Encoding.UTF8.GetBytes(builder.ToString().Trim());
        }

读取文件:

        public virtual void EditorDeserializeToObject(byte[] bytes)
        {
            string buffer = Encoding.UTF8.GetString(bytes).Trim();
            // 分割行
            string[] lines = buffer.Split(
                new string[] { Environment.NewLine }, 
                StringSplitOptions.RemoveEmptyEntries);

            List<TData> loadedDatas = new List<TData>();

            for (int i = 0; i < lines.Length; i++)
            {
                string line = lines[i].Trim();
                // 如果是注释,直接下一条
                if (line.StartsWith(k_CommentingPrefix))
                {
                    continue;
                }

                TData data = new TData();
                if (data.FormatText(line))
                {
                    loadedDatas.Add(data);
                }
            }

            datas = loadedDatas.ToArray();
        }

3 绘制主函数(OnGUI)

回来EditorSrpgDataEditorWindow中,我们来填充我们的OnGUI方法:

        private Vector2 m_Scroll;

        private GUILayoutOption m_BtnWidth = GUILayout.MaxWidth(120);

        private void OnGUI()
        {
            // TODO
        }

首先,我们的对象不能为空,且能够选择绘制的类型:

            EditorGUI.BeginDisabledGroup(true);
            srpgData = (EditorSrpgData)EditorGUILayout.ObjectField("SRPG Data Editor", srpgData, typeof(EditorSrpgData), false);
            EditorGUI.EndDisabledGroup();
            if (srpgData == null || m_SerializedObject == null)
            {
                EditorGUILayout.HelpBox("Please re-open a SRPG Data Editor Window.", MessageType.Info);
                return;
            }

            m_SerializedObject.Update();

            // 绘制选择类型
            SerializedProperty curConfigTypeProperty = m_SerializedObject.FindProperty("currentConfig");
            EditorGUILayout.PropertyField(curConfigTypeProperty, true);
            EditorGUILayout.Space();

            // TODO

其次,是我们的功能按钮(方法之后再填充):

            // 绘制按钮
            if (!DoDrawButtons())
            {
                return;
            }

最后,绘制我们当前的数据(方法之后再填充):

            // 绘制数据
            if (!DoDrawDatas())
            {
                return;
            }

4 绘制按钮(Draw Buttons)

我们按钮的功能主要分为:

  • 保存数据成配置文件:SaveToFile

  • 读取配置文件:LoadFromFile

  • 检测重复的keyCheckDumplicateKeys

  • 根据key排序数据:SortWithKeys

即,创建DoDrawButtons()

        private GUILayoutOption m_BtnWidth = GUILayout.MaxWidth(120);

        /// <summary>
        /// 绘制按钮
        /// </summary>
        private bool DoDrawButtons()
        {
            IEditorConfigSerializer config = srpgData.GetCurConfig();
            if (config == null)
            {
                EditorGUILayout.HelpBox(
                    string.Format("{0} Config is not found.", srpgData.currentConfig.ToString()), 
                    MessageType.Error);
                return false;
            }

            EditorGUILayout.BeginHorizontal();
            {
                if (GUILayout.Button("Save To File", m_BtnWidth))
                {
                    SaveToFile(config);
                }

                if (GUILayout.Button("Load From File", m_BtnWidth))
                {
                    LoadFromFile(config);
                }

                if (GUILayout.Button("Check Keys", m_BtnWidth))
                {
                    CheckDuplicateKeys(config);
                }

                if (GUILayout.Button("Sort Datas", m_BtnWidth))
                {
                    SortWithKeys(config);
                }
            }
            EditorGUILayout.EndHorizontal();

            return true;
        }

我们让它们水平方向排列:

EditorSrpgDataWindow Buttons

  • 图 8.10 EditorSrpgDataWindow Buttons

4.1 保存文件(Save To File)

将配置文件转换成byte[]的方法我们已经写过了,只需要获取相应的路径然后保存就可以了。 在UnityEditor中,获取保存文件路径的方法是 EditorUtility.SaveFilePanel 。而在保存之前,我们需要检测一下文件是否合法(是否有重复的key)。

        private void SaveToFile(IEditorConfigSerializer config)
        {
            string ext = (config is XmlConfigFile) ? "xml" : "txt";
            string path = EditorUtility.SaveFilePanel(
                "Save", Application.streamingAssetsPath, config.GetType().Name, ext);

            if (!string.IsNullOrEmpty(path))
            {
                if (!CheckDuplicateKeys(config))
                {
                    Debug.LogError("Config to save has some `Duplicate Keys`. Save Failure.");
                    return;
                }

                try
                {
                    byte[] bytes = config.EditorSerializeToBytes();
                    File.WriteAllBytes(path, bytes);
                    AssetDatabase.Refresh();
                }
                catch (Exception e)
                {
                    Debug.LogError("Save ERROR: " + e.ToString());
                    return;
                }
            }
        }

4.2 读取文件(Load From File)

类似保存文件, 在UnityEditor中,获取读取文件路径的方法是 EditorUtility.OpenFilePanel 。而在读取之后,我们也检测一下文件是否合法(是否有重复的key)。

        private void LoadFromFile(IEditorConfigSerializer config)
        {
            string ext = (config is XmlConfigFile) ? "xml" : "txt";
            string path = EditorUtility.OpenFilePanel(
                "Load", Application.streamingAssetsPath, ext);

            if (!string.IsNullOrEmpty(path))
            {
                try
                {
                    byte[] bytes = File.ReadAllBytes(path);
                    config.EditorDeserializeToObject(bytes);
                    EditorUtility.SetDirty(srpgData);
                    Repaint();
                }
                catch (Exception e)
                {
                    Debug.LogError("Load ERROR: " + e.ToString());
                    return;
                }

                if (!CheckDuplicateKeys(config))
                {
                    Debug.LogError("Loaded File has some `Duplicate Keys`.");
                    return;
                }
            }
        }

4.3 检测重复键值(Check Dumplicate Keys)

如果只是检测是否有重复的key是非常简单的, 但我们需要知道是哪些数据重复了,这就需要我们保存重复的key与其在datas中相应的下标告知我们。

        /// <summary>
        /// 检查重复的Key
        /// </summary>
        /// <returns></returns>
        private bool CheckDuplicateKeys(IEditorConfigSerializer config)
        {
            // 获取所有key
            Array keys = config.EditorGetKeys();

            // key : index
            Dictionary<object, int> keySet = new Dictionary<object, int>();

            // dumplicate [key : indexes]
            Dictionary<object, HashSet<string>> duplicateKeys = new Dictionary<object, HashSet<string>>();

            for (int i = 0; i < keys.Length; i++)
            {
                object key = keys.GetValue(i);

                // 如果key重复了
                if (keySet.ContainsKey(key))
                {
                    // 如果重复key的set没有建立
                    if (!duplicateKeys.ContainsKey(key))
                    {
                        // 建立set,并加入最初的下标
                        duplicateKeys[key] = new HashSet<string>
                        {
                            keySet[key].ToString()
                        };
                    }

                    // 加入当前下标
                    duplicateKeys[key].Add(i.ToString());
                }
                else
                {
                    keySet.Add(key, i);
                }
            }

            if (duplicateKeys.Count != 0)
            {
                // 打印所有重复的keys
                foreach (var kvp in duplicateKeys)
                {
                    Debug.LogErrorFormat(
                        "Duplicate Keys \"{0}\": Index [{1}]",
                        kvp.Key.ToString(),
                        string.Join(", ", kvp.Value.ToArray()));
                }
                return false;
            }

            return true;
        }

4.4 排序数据(Sort With Keys)

排序方法只需要直接调用:

        private void SortWithKeys(IEditorConfigSerializer config)
        {
            config.EditorSortDatas();
        }

5 绘制数据(Draw Datas)

我们绘制数据的主要目的是为了减轻渲染压力,不要一次性把列表中的所有数据都显示出来;并且可以选择绘制的范围。

为了简化,我没有自定义更多方法(目前够用了)。要自定义更多,可以创建更多的PropertyDrawer

要绘制数据,首先得先获取数据:

        /// <summary>
        /// 获取当前config
        /// </summary>
        /// <returns></returns>
        private SerializedProperty GetConfigProperty()
        {
            switch (srpgData.currentConfig)
            {
                case EditorSrpgData.ConfigType.MoveConsumption:
                    return m_SerializedObject.FindProperty("moveConsumptionConfig");
                case EditorSrpgData.ConfigType.Class:
                    return m_SerializedObject.FindProperty("classConfig");
                case EditorSrpgData.ConfigType.Character:
                    return m_SerializedObject.FindProperty("characterInfoConfig");
                case EditorSrpgData.ConfigType.Item:
                    return m_SerializedObject.FindProperty("itemInfoConfig");
                case EditorSrpgData.ConfigType.Text:
                    return m_SerializedObject.FindProperty("textInfoConfig");
                default:
                    return null;
            }
        }

我们要知道,我们编辑数据全部存储在配置文件中的datas字段中,而这个字段也用于序列化,且它是一个数组;再加上之前的分析,我们目前需要:

  • 获取我们的数组属性,使用一个SerializedProperty

  • 显示的范围,使用一个Vector2Int,规定至少渲染20条数据;

  • 还有一个ScrollView滚动条,在UnityEditor中使用Vector2

所以建立DoDrawDatas()

        private Vector2Int m_SelectedRange;
        private Vector2 m_Scroll;

        /// <summary>
        /// 绘制具体信息
        /// </summary>
        private bool DoDrawDatas()
        {
            SerializedProperty curConfigProperty = GetConfigProperty();
            if (curConfigProperty == null)
            {
                EditorGUILayout.HelpBox(
                    string.Format("{0} Config Property is not found.", srpgData.currentConfig.ToString()),
                    MessageType.Error);
                return false;
            }

            SerializedProperty curArrayDatasProperty = curConfigProperty.FindPropertyRelative("datas");

            EditorGUI.BeginChangeCheck();

            // 设置数量
            int arraySize = Mathf.Max(0, EditorGUILayout.DelayedIntField("Size", curArrayDatasProperty.arraySize));
            curArrayDatasProperty.arraySize = arraySize;

            if (arraySize != 0)
            {
                // 最少显示20个
                m_SelectedRange = EditorGUILayout.Vector2IntField("Index Range", m_SelectedRange);
                m_SelectedRange.x = Mathf.Max(0, Mathf.Min(m_SelectedRange.x, arraySize - 20));
                m_SelectedRange.y = Mathf.Min(arraySize - 1, Mathf.Max(m_SelectedRange.x + 19, m_SelectedRange.y));
                Vector2 range = m_SelectedRange;
                EditorGUILayout.MinMaxSlider(
                    ref range.x,
                    ref range.y,
                    0,
                    arraySize - 1);
                m_SelectedRange.x = (int)range.x;
                m_SelectedRange.y = (int)range.y;

                // 绘制数据
                m_Scroll = EditorGUILayout.BeginScrollView(m_Scroll, "box");
                {
                    for (int i = m_SelectedRange.x; i <= m_SelectedRange.y; i++)
                    {
                        SerializedProperty property = curArrayDatasProperty.GetArrayElementAtIndex(i);
                        EditorGUILayout.PropertyField(property, true);
                    }
                }
                EditorGUILayout.EndScrollView();
            }

            m_SerializedObject.ApplyModifiedProperties();
            if (EditorGUI.EndChangeCheck())
            {
                EditorUtility.SetDirty(srpgData);
            }

            return true;
        }

这样就完成了,打开窗口查看:

EditorSrpgDataWindow Datas

  • 图 8.11 EditorSrpgDataWindow Datas

猜你喜欢

转载自blog.csdn.net/darkrabbit/article/details/84451899