Unity 编辑器的高效拓展设计方案

91 阅读11分钟

        在游戏开发过程中,根据业务需求通常会设计开发一些工具以提升游戏开发效率或在编辑阶段通过工具把某些问题解决。例如我们要做一个闯关类游戏,通常我们需要做一个关卡编辑器或一个地图编辑器。然后用关卡编辑器把关卡或地图编辑完成,然后导入游戏中进行解析运行。除了业务需求类编辑工具拓展外,还有一些适用性比较广泛的工具扩展,如资源管理、打包工具扩展等,这些工具拓展一旦完成,通常是可以迁移到其它游戏中使用的。下面我们一起来讨论一下如何在Unity中高效且又具有一定可复用性的编辑器拓展和设计方法。

        Unity对编辑器的拓展支持,如采用GUI方式的话,总的来说就是靠GUILayout 加上各种如GUIButton类控件通过Position和Style精准定位,然后一个一个绘画出来。而这一切都需要代码一行一行的“制造”出来,非常麻烦且效率低下。Unity并没有提供如UGUI一样的所见即所得的工具编辑器,因此我们对编辑器的拓展,力求能做到复用或统一设计,这样能减少我们的代码量和少干重复发明轮子的事情。下面我们一起来看一看笔者本人最近为游戏所做的编辑器拓展。

Addressable资源包编辑器

发包编辑器

游戏设置编辑器

某款游戏的关卡编辑器

       我们一起观察一下各个编辑器的设计,是不是都有相似之处?它们的布局都是几行几列,各行各列互相独立各自渲染。这种行和列的设计方式很灵活,可以任意的添加多行和多列以适应我们的需求。如我们需要一个简单的编辑器时,只要添加一行或一列的布局就可以了,如上面的发包编辑器。如我们需要一个稍为复杂一点的编辑器,如上面的Addressable资源包编辑器,那就需要添加一行多列的布局。再来一些更复杂的编辑器,如上面的游戏关卡编辑器,那就必须添加多行多列的布局。

       我们设计的思路虽然有了“行”和“列”,但只有行和列是不够的。因为行和列太大了,往里面放上一堆控件或图片,如要定位布局这些元素要花费的代码量还是挺大的,所以我们还得在行和列里面再添加一个“单元格”。把行和列再细分排列,这样真正到业务层拓展各种工具开发时,就免去了大量的元素布局定位代码,大大提高了编辑器的开发效率。

       我们有了“行”、“列”、”单元格“的概念,现在要把这些概念把它组织起来落地,变成真正可以使用的东西。

       ”行“、”列“抽象出来我们可以把它叫作容器,于是我们可以定义一个 UIContainer.cs类,这个类负责渲染它内部的所有元素。

       "单元格"抽象出来就是一个个格子,于是我们可以定义一个UICell.cs类,这个类负责渲染它内部的所有元素。 

       UIContainer可以任意添加或移除多个UICell,并进行自适应的布局。

       有了容器和单元格,接下来我们要把它们组织起来变成真正的"行和列",我们还要定义一个区域多列类UIAreasMultipleColumns.cs,这个类只要负责把容器添加到界面上并负责驱动容器渲染。

       最后我们还要设计一个MainUI主界面,继随于UIAreasMultipleColumns,把MainUI添加到GUI的Window窗口上,这样整个编辑器面板的界面布局就算完成了。下面我们来看一看具体的实现。

UIContainer.cs:

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;

namespace SimpleGUI
{

    public enum CellAlignType
    {
        Horizontal = 0,
        Vertical = 1
    }
    /// <summary>
    /// GUI容器
    /// </summary>
    public class UIContainer
    {
        /// <summary>
        /// 容器的范围
        /// </summary>
        public Rect Area { private set; get; }
        public bool Show { private set; get; } = true;
        /// <summary>
        /// 是否是最后一个容器
        /// </summary>
        public bool Last { private set; get; } = false;

        public CellAlignType CellAlign { private set; get; } = CellAlignType.Horizontal;
        public float PaddingLeft { private set; get; } = 0f;
        public float PaddingRight { private set; get; } = 0f;
        public float PaddingTop { private set; get; } = 0f;
        public float PaddingBottom { private set; get; } = 0f;
        public float Spacing { private set; get; } = 0f;
 

        private List<UICell> _cells = new List<UICell>();

        public UIContainer() 
        {
            SetArea(0, 0, 0, 0);
        }
        public UIContainer(float width, float height)
        {
            SetArea(0, 0, width, height);
        }
        public UIContainer(Rect r)
        {
            SetArea(r);         
        }

        public UIContainer(float x, float y, float width, float height)
        {
            SetArea(x, y, width, height);            
        }
        
        public void SetAlign(CellAlignType align)
        {
            CellAlign = align;
        }

        public void SetArea(float x, float y, float width, float height)
        {
            SetArea(new Rect(x, y, width, height));
        }
        public void SetArea(Rect r)
        {
            Area = r;          
        }

        public void SetWidth(float width)
        {
            SetArea(new Rect(Area.x, Area.y, width, Area.height));
        }

        public void SetHeight(float height)
        {
            SetArea(new Rect(Area.x, Area.y, Area.width, height));
        }

        public void SetLast(bool last)
        {
            Last = last;
        }

        public void SetPadding(float left, float top, float right, float bottom)
        {
            PaddingBottom = bottom;
            PaddingLeft = left;
            PaddingRight = right;
            PaddingTop = top;
        }
        public void SetSpacing(float spacing)
        {
            Spacing = spacing;
        }
     
        public void HideCells()
        {
            for (int i = 0; i < _cells.Count; i++)
            {
                var cell = _cells[i];
                cell.SetVisible(false);
            }
        }

        public void ShowCells()
        {
            for (int i = 0; i < _cells.Count; i++)
            {
                var cell = _cells[i];
                cell.SetVisible(true);
            }
        }
      
        public void SetVisible(bool show)
        {
            Show = show;
        }

        public void AddCell(UICell cell)
        {
            _cells.Add(cell);
            cell.SetContainer(this);
        }

        public void RemoveCell(UICell cell)
        {
            _cells.Remove(cell);
        }

        public void ClearCell() 
        {
            _cells.Clear(); 
        }

        ///渲染容器内元素
        public void UpdataContainer()
        {
            UpdateCellArea();
        }

        private void UpdateCellArea()
        {
            var x = 0f;
            var y = 0f;
            var w = 0f;
            var h = 0f;      
            var spacingCounter = 0f;
            var distCounter = 0f;
                 
            for(int i = 0; i < _cells.Count; i++)
            {//渲染所有单元格,计算边距、自适应等
                var cell = _cells[i];         
                var lastIndex = _cells.Count - 1;
                if (CellAlign == CellAlignType.Horizontal)
                {
                    x = PaddingLeft + spacingCounter + distCounter;
                    y = PaddingTop;
                    w = cell.Position.width;
                    if (cell.Autozoom)
                    {
                        var testw = w;
                        var lastw = Area.width - PaddingRight - x;
                        if (lastIndex == i)
                            w = lastw;
                        else if (testw > lastw)
                            w = testw - lastw;
                    }
                    h = Area.height - PaddingTop - PaddingBottom;
                    cell.SetPosition(x, y, w, h);

                    distCounter += w;
                    spacingCounter += Spacing;

                }
                else if (CellAlign == CellAlignType.Vertical)
                {
                    x = PaddingLeft;
                    y = PaddingTop + spacingCounter + distCounter;
                    w = Area.width - PaddingLeft - PaddingRight;
                    h = cell.Position.height;
                    if (cell.Autozoom)
                    {
                        var testh = h;
                        var lasth = Area.height - PaddingBottom - y;
                    
                        if (lastIndex == i)
                            h = lasth;
                        else if (testh > lasth)
                            h = testh - lasth;
                    }
                    cell.SetPosition(x, y, w, h);

                    distCounter += h;
                    spacingCounter += Spacing;
                }
                cell.UpdateContent(); //渲染单元格

            }     

            
        }             
    }
}

UICell.cs:

using UnityEngine;
using UnityEditor;
namespace SimpleGUI
{
    public class UICell
    {
        private UIContainer _container;
        public bool Autozoom = true;
        
        public UICell() 
        {
            var r = new Rect(0, 0, 0, 0);
            Init(r);
        }

        public UICell(float width, float height)
        {
            var r = new Rect(0, 0, width, height);
            Init(r);
        }

        public UICell(float x, float y, float width, float height)
        {
            var r = new Rect(x, y, width, height);
            Init(r);
        }

        public UICell(Rect pos)
        {
            Init(pos);
        }
        
        /// <summary>
        /// 返回位置
        /// </summary>
        public Rect Position { private set; get; }
        /// <summary>
        /// 返回区域范围
        /// </summary>
        public Rect Area { private set; get; }

        public bool Show { private set; get; } = true;
      
        public virtual void Init(Rect pos)
        {
            Position = pos;       
        }

        public void SetPosition(float x, float y, float width, float height)
        {
            Position = new Rect(x, y, width, height);
            SetArea();
        }

        public virtual void OnAreaChanged(Rect old, Rect last)
        {

        }

        private void SetArea()
        {
            var lastr = new Rect(_container.Area.x + Position.x, _container.Area.y + Position.y, Position.width, Position.height);
            var curr = Area;
            if(lastr != Area)
            {
                Area = lastr;
                OnAreaChanged(curr, Area);
            }         
        }

        public void SetVisible(bool show)
        {
            Show = show;            
        }
 
        /// <summary>
        /// 更新GUI内容
        /// </summary>
        public void UpdateContent()
        {
            //以BeginArea标签开始,让区域内的GUI按相对坐标计算
            GUILayout.BeginArea(Area);
            DrawContent();
            GUILayout.EndArea();         ;
        }
        /// <summary>
        /// 容器变化时需更新
        /// </summary>
        /// <param name="container"></param>
        public virtual void SetContainer(UIContainer container)
        {
            _container = container;
            SetArea();
            OnAreaChanged(Area, Area);
        }
        

        public void SetWidth(float width)
        {           
            SetPosition(Position.x, Position.y, width, Position.height);
        }

        public void SetHeight(float height)
        {
            SetPosition(Position.x, Position.y, Position.width, height);
        }
        /// <summary>
        /// 开放给业务层,重写以改变GUI
        /// </summary>
        public virtual void DrawContent()
        {
            //GUI.Button(new Rect(1, 1, Area.width, Area.height), UIAssets.PlusIcon, EditorStyles.toolbarButton);
            GUI.Button(new Rect(1, 1, Area.width, Area.height), UIAssets.PlusIcon);
        }      
    }
}

UIAreasMultipleColumns.cs:

using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using NSubstitute.Core;

namespace SimpleGUI
{  
    /// <summary>
    /// UI界面的整体区域布局,上行,下列
    /// </summary>
    public class UIAreasMultipleColumns
    {    
             
        private EditorWindow _window;
        public EditorWindow Window => _window;

        public float PaddingLeft { private set; get; } = 2f;
        public float PaddingRight { private set; get; } = 2f;
        public float PaddingTop { private set; get; } = 2f;
        public float PaddingBottom { private set; get; } = 2f;
        public float Spacing { private set; get; } = 2f;

        private List<UIContainer> _colContainers = new List<UIContainer>();
        private List<UIContainer> _rowContainers = new List<UIContainer>();

        public UIAreasMultipleColumns(EditorWindow window)
        {
            _window = window;           
        }
      
        public void AddRowContainer(UIContainer container)
        {
            container.SetAlign(CellAlignType.Horizontal);
            _rowContainers.Add(container);           
        }

        public void RemoveRowContainer(UIContainer container)
        {
            _rowContainers.Remove(container);
        }
        public void ClearRowContainer() { _rowContainers.Clear(); }

        public void AddColContainer(UIContainer container)
        {
            container.SetAlign(CellAlignType.Vertical);
            _colContainers.Add(container);           
        }
        public void RemoveColContainer(UIContainer container)
        { 
            _colContainers.Remove(container);
        }
        public void ClearColContainer()
        { 
            _colContainers.Clear();
        }
       
        ///渲染容器
        private void UpdateContainer()
        {
            var x = 0f;
            var y = 0f;
            var w = 0f;
            var h = 0f;
            var spacingCounter = 0f;
            var distCounter = 0f;           
  
            for (int i = 0; i < _rowContainers.Count; i++)
            {
                var container = _rowContainers[i];
                var lastIndex = _rowContainers.Count - 1;
                x = PaddingLeft;
                y = PaddingTop + spacingCounter + distCounter;
                w = _window.position.width - PaddingLeft - PaddingRight;
                h = container.Area.height;

                if(_colContainers.Count == 0)
                {
                    var testh = h;
                    var lasth = _window.position.height - PaddingBottom - y;

                    if (lastIndex == i)
                        h = lasth;
                    else if (testh > lasth)
                        h = testh - lasth;
                }
                container.SetArea(x, y, w, h);
                container.UpdataContainer();
                distCounter += h;
                spacingCounter += Spacing;              
            }

            spacingCounter = 0;
            var topHeight = distCounter + Spacing;         
            var height = _window.position.height - topHeight;
            distCounter = 0;

            for (int i = 0; i < _colContainers.Count; i++)
            {
                var container = _colContainers[i];
                var lastIndex = _colContainers.Count - 1;
                x = PaddingLeft + spacingCounter + distCounter;
                y = topHeight;
                w = container.Area.width;
                h = height;

                var testw = w;
                var lastw = _window.position.width - PaddingRight - x;
                if (lastIndex == i)
                    w = lastw;
                else if (testw > lastw)
                    w = testw - lastw;

                container.SetArea(x, y, w, h);
                container.UpdataContainer();

                distCounter += w;
                spacingCounter += Spacing;
              
            }
           
        }

        /// <summary>
        /// 绘制GUI
        /// </summary>
        public virtual void UpdateDrawContent()
        {      
            UpdateContainer();          
        }                                   
                       
    }
   
}

       这样我们三个底层的关于GUI布局设计的类就算完成了,后面我们用到的所有GUI设计都会继承于UICell,一旦继承UICell了,就会自动实现了大部分的界面布局代码,自己只要实现 Cell内部的元素布局与样式就可以了。下面我们利用上面的两个类实现一下上面的”游戏设置编辑器“。

游戏设置编辑器的实现

       游戏可能有很多种设置要编辑,我们首先实现一个选项卡,这个选项卡还是通用的。如下:

定义一个GUIElement.cs

namespace SimpleGUI
{
    public class GUIElement
    {
        public int Id;
        public int Index;
        public string Text;
        public float Width = -1;
        public float Height = -1;
        public int Tag;        
    }
}

定义一个GUITab.cs

using System;
using System.Collections.Generic;

using UnityEditor;
using UnityEngine;

namespace SimpleGUI
{
    public class GUIButton : GUIElement
    {
        public Action<GUIButton> OkCall;
    }

    public class GUITab
    {
        private List<GUIButton> _buttons = new List<GUIButton>(); 
        public int CurIndex { private set; get; } = -1;
        public Color SelectedBackColor = Color.green;

        public GUIButton AddButton(string text, Action<GUIButton> okCall)
        {
            var button = new GUIButton();
            button.Text = text;          
            button.OkCall = okCall;
            button.Index = _buttons.Count;
            _buttons.Add(button);
            return button;
        }

        public GUIButton AddButton(string text, float width, Action<GUIButton> okCall)
        {
            var button = new GUIButton();
            button.Text = text;
            button.Width = width;     
            button.OkCall = okCall;
            button.Index = _buttons.Count;
            _buttons.Add(button);
            return button;
        }
        public GUIButton AddButton(string text, float width, float height, Action<GUIButton> okCall)
        {
            var button = new GUIButton();
            button.Text = text;
            button.Width = width;
            button.Height = height;
            button.OkCall = okCall;
            button.Index = _buttons.Count;
            _buttons.Add(button);
            return button;
        }
        public GUIButton AddButton(string text, int tag, float width, float height, Action<GUIButton> okCall)
        {
            var button = AddButton(text, width, height, okCall);
            button.Tag = tag;
            return button;
        }

        public GUIButton AddButton(GUIButton button)
        {
            _buttons.Add(button);
            return button;
        }

        public void ClearButton()
        {
            _buttons.Clear();
        }
        public void DrawContent()
        {
            bool ok;
            for(int i = 0; i < _buttons.Count; i++)
            {
                var button = _buttons[i];
                if (CurIndex == i)
                {
                    var orgBg = GUI.backgroundColor;
                    GUI.backgroundColor = SelectedBackColor;
                    ok = DrawButton(button);
                    GUI.backgroundColor = orgBg;
                }
                else
                    ok = DrawButton(button);

                if (ok)
                {
                    CurIndex = i;
                    button.OkCall?.Invoke(button);
                }
            }
           
        }     
        
        private bool DrawButton(GUIButton button)
        {
            bool ok;
            if (button.Width > 0 && button.Height > 0)
                ok = GUILayout.Button(button.Text, EditorStyles.toolbarButton, GUILayout.Width(button.Width), GUILayout.Height(button.Height));
            else if (button.Width > 0)
                ok = GUILayout.Button(button.Text, EditorStyles.toolbarButton, GUILayout.Width(button.Width));
            else
                ok = GUILayout.Button(button.Text, EditorStyles.toolbarButton);
            return ok;
        }

        public void Click(int index)
        {
            var button = _buttons[index];
            button?.OkCall(button);
            CurIndex = index;
        }
        public void SetCurIndex(int index)
        {
            CurIndex = index;
        }
    }
}

有了选项卡,我们要把它添加到菜单栏里,定义一个UIMenu.cs,这个UIMenu继承UICell:

using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using SimpleGUI;

namespace EditorGameSetting
{
    /// <summary>
    /// 菜单
    /// </summary>
    public class UIMenu : UICell
    {   

        public GUITab Tab { private set; get; } = new GUITab();
        public UIMenu(float width, float height) : base(width, height) { }
        public UIMenu(Rect pos) : base(pos)
        {
                   
        }

        public override void Init(Rect pos)
        {
            base.Init(pos);
            Tab.ClearButton();           
            Tab.AddButton("宏定义", 50, OnSymbolDef);
            Tab.AddButton("日志设置", 75, OnDebugLog);          
            
        }
             
        public override void DrawContent()
        {        
            GUILayout.BeginHorizontal(GUIStyleDefine.NewToolbarContainerStyle);
           
            Tab.DrawContent();

            GUILayout.FlexibleSpace();
            
            GUILayout.EndHorizontal();                                 
        }
     
        private void OnSymbolDef(GUIButton button)
        {           
            WinGameSetting.MainUI.ShowSymbolView();
        }

        private void OnDebugLog(GUIButton button)
        {
            WinGameSetting.MainUI.ShowSymbolView();
        }

    }
}

菜单栏下面还有一个宏定义工具栏,定义一个 UISymbolsToolbar.cs,同样继承UICell:

using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.Linq;
using SimpleGUI;
using Gamelogic;
using EditorLevel;

namespace EditorGameSetting
{
    /// <summary>
    /// 宏工具栏
    /// </summary>
    public class UISymbolsToolbar : UICell
    {
        public Action<CfgGroup> OnGroupAdded; 
        public UISymbolsToolbar(float width, float height) : base(width, height)
        {
                   
        }

        public override void DrawContent()
        {
            //GUIStyleDefine.DrawAreaBg(new Rect(0, 0, Area.width, Area.height), Color.black);
            GUILayout.BeginHorizontal(GUIStyleDefine.NewToolbarContainerStyle);
            var tips = $">宏 ({SymbolMgr.CurSymbolContorler.Symbols.Count})";
            GUILayout.Label(tips);

            GUILayout.FlexibleSpace();
            if (GUILayout.Button("保存", EditorStyles.toolbarButton, GUILayout.Width(60)))
            {
                SymbolMgr.Save();         
                GUITools.ShowDialog("保存", "保存成功", "OK");
                AssetDatabase.Refresh();
                AssetDatabase.SaveAssets();
            }
            if (GUILayout.Button("更新到项目", EditorStyles.toolbarButton, GUILayout.Width(100)))
            {
                SymbolMgr.Save();
                SymbolMgr.CurSymbolContorler.SetSymbols2Project();
                GUITools.ShowDialog("更新成功", "更新成功", "OK");
                AssetDatabase.Refresh();
                AssetDatabase.SaveAssets();
            }
            if (GUILayout.Button(UIAssets.PlusIcon, EditorStyles.toolbarButton, GUILayout.Width(30)))
            {
                var ndata = new SymbolData();
                WinGameSetting.MainUI.SymbolEdit.SetItem(ndata, false);
            }
          
            GUILayout.EndHorizontal();
        }   
                   

    }
}

界面左边有一列是游戏要设置的宏,它是个列表,定义一个UISymbols.cs,同样继承UICell:

using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using EditorTreeView;
using UnityEditor.IMGUI.Controls;
using SimpleGUI;
using Gamelogic;

namespace EditorGameSetting
{

    internal class SymbolsTreeNode : TreeNode
    {
        public SymbolData Symbol;
      
        public SymbolsTreeNode() { }
        public SymbolsTreeNode(int id, string name, int depth) : base(id, name, depth)
        {

        }
    }

    internal class SymbolsTreeView : TreeView<SymbolsTreeNode>
    {
        public UISymbols Window;      
        public SymbolsTreeView(TreeViewState state, MultiColumnHeader multiColumnHeader, string rootName) : base(state, multiColumnHeader, rootName)
        {

        }

        protected override void RowGUI(RowGUIArgs args)
        {
            var item = (TreeViewItem<SymbolsTreeNode>)args.item;
            var cols = args.GetNumVisibleColumns();
            for (int i = 0; i < cols; i++)
            {
                var cellr = args.GetCellRect(i);
                if (i == 0)
                {
                    var bol = item.Data.Symbol;
                    var width = 200;
                    var height = cellr.height;
                    var offsetx = 5;
                    var offsety = 0;
                    var iconRect = GetRect(cellr, offsetx, offsety, width, height);            
                    EditorGUI.LabelField(iconRect, bol.Name);

                    var x = cellr.width - 70;
                    var namew = 35;
                    var nameRect = GetRect(cellr, x, offsety, namew, height);
                    bol.Enable = EditorGUI.Toggle(nameRect, bol.Enable);

                    x = cellr.width - 35;
                    var randombw = 30f;
                    var addRandomRect = GetRect(cellr, x, 2, randombw, height - 5);
                    if (GUI.Button(addRandomRect, "-"))
                    {
                        SymbolMgr.CurSymbolContorler.Remove(bol.Name);
                        Window.UpdateUI();
                    }
                }
            }         
        }

        protected override void SingleClickedItem(int id)
        {
            var item = Model.GetNode(id);
            Window.SelectedItem = item.Symbol;
        }

        protected override bool CanRename(TreeViewItem item)
        {
            return false;
        }  
                
    }

    /// <summary>
    /// 宏定义
    /// </summary>
    public class UISymbols : UICell
    {
        private TreeViewState _viewState;
        private SymbolsTreeView _tv;
        private SymbolContorler _symbolContorler;
        public Action<SymbolData, SymbolData> OnSelected;      
        private SymbolData _selectedItem;
        public SymbolData SelectedItem
        {
            set
            {
                OnSelected?.Invoke(_selectedItem, value);
                _selectedItem = value;
            }
            get { return _selectedItem; }
        }
     
        public override void Init(Rect pos)
        {
            base.Init(pos);  
            _symbolContorler = SymbolMgr.CurSymbolContorler;       
        }

        private void InitTreeViewHeader()
        {
            if (_viewState == null)
                _viewState = new TreeViewState();
            var col1 = TreeViewColumn.CreateColumn($"宏定义", Area.width - 15, Area.width - 15, 500, TextAlignment.Center, true);
            var headerState = TreeViewColumn.CreateMultiColumnHeaderState(new MultiColumnHeaderState.Column[] { col1 });
            var header = new TreeViewColumn(headerState);
            _tv = new SymbolsTreeView(_viewState, header, "root");
            _tv.SetShowBorder(true);
            _tv.SetShowAlternatingRowBg(true);
            _tv.SetRowHeight(25);

            _tv.Window = this;
        }

        public override void OnAreaChanged(Rect old, Rect last)
        {
            base.OnAreaChanged(old, last);
            UpdateUI();
        }

        public override void DrawContent()
        {         
            if(_symbolContorler != null)
            {
                float leftSpace = 0;
                float rightSpace = 0;
                float bottomSpace = 0;
                float topSpace = 0;
                float height = topSpace;

                var r = new Rect(leftSpace, height, Area.width - rightSpace, Area.height - height - bottomSpace);
                _tv.OnGUI(r);
            }
            
                           
        }         
        
        public void SelectItem(int index)
        {
            var i = -1;
            foreach(var data in _symbolContorler.Symbols)
            {
                i++;
                if(index == i)
                {
                    SelectedItem = data.Value;
                    return;
                }
            }           
        }
       
        public void UpdateUI()
        {
            InitTreeViewHeader();
            var model = _tv.Model;
            model.Root.Childrens.Clear();
          
            foreach (var sym in _symbolContorler.Symbols)
            {                            
                var node = new SymbolsTreeNode(model.GetUniqueId(), sym.Key, 0);
                node.Symbol = sym.Value;
                model.Root.AddChildren(node);
            }
            _tv.Reload();           
        }       
    }
}

我们点击宏列表的时候要对宏进行编辑,所以要定义一个编辑宏的类UISymbolEdit.cs,同样继承UICell:

using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
using System.IO;
using Newtonsoft.Json;
using System.Collections.Generic;
using System;
using Simple.HotUpdate;
using SimpleGUI;
using Gamelogic;
using DG.DemiEditor;

namespace EditorGameSetting
{
    /// <summary>
    /// 宏编辑
    /// </summary>
    public class UISymbolEdit : UICell
    {
        public SymbolData ItemData { private set; get; }
        private bool _edit = false;
        private GUIStyle _lblStyle;
  
        public override void Init(Rect pos)
        {
            base.Init(pos);
            InitData();
        }
        private void InitData()
        {
           
        }        

        public void SetItem(SymbolData item, bool edit)
        {
            ItemData = item;
            _edit = edit;
        }

        public override void DrawContent()
        {
            GUILayout.BeginHorizontal();
            GUILayout.Label("项目宏数据: " + SymbolMgr.CurSymbolContorler.GetProjectSymbols2string(), GUILayout.Width(Area.width), GUILayout.Height(20));
            GUILayout.EndHorizontal();

            if (ItemData != null)
            {
                _lblStyle = GUIStyleDefine.NewLableStyle;
                _lblStyle.alignment = TextAnchor.MiddleLeft;
                _lblStyle.fixedWidth = 70;

                var containerStyle = new GUIStyle(GUI.skin.box);
                containerStyle.fixedWidth = Area.width;
                containerStyle.fixedHeight = Area.height;

                var width = GUILayout.Width(Area.width - _lblStyle.fixedWidth - 15);
                GUILayout.BeginVertical(containerStyle);
                GUILayout.Space(5);

                if(_edit)
                    GUILayout.Label(">编辑", GUILayout.Width(100));
                else
                    GUILayout.Label(">添加", GUILayout.Width(100));

                GUILayout.Space(5);
                GUILayout.BeginHorizontal();
                GUILayout.Label("Name:", _lblStyle);
                ItemData.Name = EditorGUILayout.TextField(ItemData.Name, width);
                GUILayout.EndHorizontal(); 
                GUILayout.Space(5);

                GUILayout.BeginHorizontal();
                GUILayout.Label("Desc:", _lblStyle);
                ItemData.Desc = EditorGUILayout.TextField(ItemData.Desc, width);
                GUILayout.EndHorizontal();
                GUILayout.Space(5);

                GUILayout.BeginHorizontal();
                GUILayout.Label("Enable:", _lblStyle);
                ItemData.Enable = EditorGUILayout.Toggle(ItemData.Enable, width);
                GUILayout.EndHorizontal();
                GUILayout.Space(5);

                GUILayout.BeginHorizontal();
               
                GUILayout.FlexibleSpace();               
                if ( GUILayout.Button("Save", GUIStyleDefine.NewButtonStyle))
                {
                    var ok = true;
                    if(string.IsNullOrEmpty(ItemData.Name))
                    {
                        GUITools.ShowDialog("保存", "名称不能为空", "OK");
                        ok = false;
                    }

                    if(ok)
                    {
                        if (!_edit)//添加的情况下加入列表
                            SymbolMgr.CurSymbolContorler.Add(ItemData);

                        WinGameSetting.MainUI.Symbols.UpdateUI();
                        SymbolMgr.Save();
                        GUITools.ShowDialog("保存", "保存成功", "OK");
                        AssetDatabase.Refresh();
                        AssetDatabase.SaveAssets();
                    }                   
                }
                GUILayout.Space(50);
                GUILayout.EndHorizontal();
                GUILayout.EndVertical();
            }
            

        }        
    }
}

最后我们要定义主界面了,定义一个GameSettingMainUI.cs,它继承自UIAreasMultipleColumns:

using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
using System.IO;
using Newtonsoft.Json;
using System.Collections.Generic;
using SimpleGUI;
using Gamelogic;
using EditorLevel;

namespace EditorGameSetting
{
    /// <summary>
    /// UI主界面
    /// </summary>
    public class GameSettingMainUI : UIAreasMultipleColumns
    {                       
        public UIMenu Menu { get; set; }
        public UISymbols Symbols { get; set; }
        public UISymbolEdit SymbolEdit { get; set; }    
        public UISymbolsToolbar SymbolsToolbar { get; set; }
        public GameSettingMainUI(EditorWindow window) : base(window)
        {
            SymbolMgr.Init();
                 
            Menu = new UIMenu(0, 30);
            var row1 = new UIContainer(0, 30);
            row1.AddCell(Menu);
            AddRowContainer(row1);//添加一行,这一行是存放菜单

            Symbols = new UISymbols();
            Symbols.OnSelected = OnSymbolSelected;
            SymbolEdit = new UISymbolEdit();
            SymbolsToolbar = new UISymbolsToolbar(0, 30);

            ShowSymbolView();
            Symbols.SelectItem(0);

        }

        public override void UpdateDrawContent()
        {
            base.UpdateDrawContent();            
        }

        public void ShowSymbolView()
        {
            ClearColContainer();
         
            var col1 = new UIContainer(250, 0);
            col1.AddCell(SymbolsToolbar); 
            col1.AddCell(Symbols);
            AddColContainer(col1 );//添加一列,只要是存放列表

            var col2 = new UIContainer();
            col2.AddCell(SymbolEdit);
            AddColContainer(col2);//添加二列,只 要是存放列表内容编辑面板
        }

        private void OnSymbolSelected(SymbolData oldData, SymbolData newData)
        {
            SymbolEdit.SetItem(newData, true);
        }            
    }
}

定义编辑器入口,WinGameSetting.cs:

using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
using System.IO;
using Newtonsoft.Json;
using System.Collections.Generic;

namespace EditorGameSetting
{
    /// <summary>
    /// 游戏设置窗口入口
    /// </summary>
    public class WinGameSetting : EditorWindow
    {
        public static GameSettingMainUI MainUI { private set; get; }
        private static WinGameSetting _window;

        [MenuItem("Tools/GameSetting", false)]
        static void DoIt()
        {
            InitWindow();
        }

        private static void InitWindow()
        {
            _window = GetWindow<WinGameSetting>(false, "GameSetting");
            _window.minSize = new Vector2(1300, 900);
            _window.Show();
            MainUI = new GameSettingMainUI(_window);
        }
       
        private void OnGUI()
        {
            if (_window == null)
                InitWindow();

            MainUI.UpdateDrawContent();
        }

        private void OnInspectorUpdate()
        {
            Repaint();
        }
    }
}

定义一个宏管理器,没有它编辑器就没作用了,SymbolMgr.cs:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using Newtonsoft.Json;
using OfficeOpenXml.Drawing;
using UnityEditor;

namespace EditorGameSetting
{
    public class SymbolData
    {
        public string Name;
        public string Desc;
        public bool Enable = false;
    }

    public class SymbolConfig
    {
        public Dictionary<string, SymbolData> AndroidSymbols = new Dictionary<string, SymbolData>();
        public Dictionary<string, SymbolData> IosSymbols = new Dictionary<string, SymbolData>();
        public Dictionary<string, SymbolData> StandaloneSymbols = new Dictionary<string, SymbolData>();
    }

    public class SymbolContorler
    {
        private BuildTargetGroup _targetGroup;
        public Dictionary<string, SymbolData> Symbols { private set; get; }  
        public SymbolContorler(Dictionary<string, SymbolData> symbols, BuildTargetGroup targetGroup)
        {
            Symbols = symbols;
            _targetGroup = targetGroup;
        }
        public void Enable(string name)
        {
            var sm = Symbols[name];
            sm.Enable = true;
        }

        public void Disable(string name)
        {
            var sm = Symbols[name];
            sm.Enable = false;
        }

        public void Add(string name, string desc, bool enable)
        {
            if(!Symbols.ContainsKey(name))
            {
                var sm = new SymbolData();
                sm.Name = name;
                sm.Desc = desc;
                sm.Enable = enable;
                Symbols.Add(name, sm);
            }            
        }

        public void Add(SymbolData data)
        {
            if (!Symbols.ContainsKey(data.Name))
            {                
                Symbols.Add(data.Name, data);
            }
        }

        public void Edit(string name, string desc, bool enable)
        {
            if (Symbols.ContainsKey(name))
            {
                var sm = Symbols[name];
                sm.Name = name;
                sm.Desc = desc;
                sm.Enable = enable;                
            }
        }
        public void Remove(string name)
        {
            Symbols.Remove(name);
        }

        public string GetString()
        {
            var smStr = "";
            foreach(var symbol in Symbols)
            {
                var data = symbol.Value;
                smStr += data.Name + ";";
            }
            if (smStr.Length > 0)
                smStr = smStr.TrimEnd(';');
            return smStr;
        }

        public void SetSymbols2Project()
        {
            PlayerSettings.GetScriptingDefineSymbolsForGroup(_targetGroup, out string[] symbols);
            var sms = new List<string>();
            sms.AddRange(symbols);
            foreach(var symbol in Symbols)
            {
                var data = symbol.Value;
                if(data.Enable && !sms.Contains(data.Name))
                {
                    sms.Add(data.Name);
                }
            }  
            foreach(var symbol in Symbols)
            {
                var data = symbol.Value;
                if (sms.Contains(data.Name) && !data.Enable)
                {
                    sms.Remove(data.Name);
                }
            }
            PlayerSettings.SetScriptingDefineSymbolsForGroup(_targetGroup, sms.ToArray());          
        }   
        
        public string GetProjectSymbols2string()
        {
            var smStr = "";
            PlayerSettings.GetScriptingDefineSymbolsForGroup(_targetGroup, out string[] bols);
            foreach (var bol in bols)
            {              
                smStr += bol + ";";
            }
            if (smStr.Length > 0)
                smStr = smStr.TrimEnd(';');
            return smStr;
        }
    }

    public class SymbolMgr
    {
        /// <summary>
        /// 存储所有关卡的配置文件
        /// </summary>
        public static readonly string ConfigFile = "Assets/Scripts/Editor/GameSetting/SymbolConfig.json";
        public static SymbolContorler AndroidSymbol = new SymbolContorler(Config.AndroidSymbols, BuildTargetGroup.Android);
        public static SymbolContorler IosSymbol = new SymbolContorler(Config.IosSymbols, BuildTargetGroup.iOS);
        public static SymbolContorler StandaloneSymbol = new SymbolContorler(Config.StandaloneSymbols, BuildTargetGroup.Standalone);
        public static SymbolContorler CurSymbolContorler { private set; get; }
        private static SymbolConfig _config;
        public static SymbolConfig Config { 
            get
            {
                if(_config == null)
                {
                    if(!File.Exists(ConfigFile))
                    {
                        _config = new SymbolConfig();
                        var sym = new SymbolData();
                        sym.Name = "AdMod";
                        sym.Desc = "开启AdMod广告集合";
                        sym.Enable = false;
                        _config.AndroidSymbols.Add(sym.Name, sym);

                        var sym1 = new SymbolData();
                        sym1.Name = "AdMax";
                        sym1.Desc = "开启Max广告集合";
                        sym1.Enable = false;
                        _config.AndroidSymbols.Add(sym1.Name, sym1);
                        Save();
                    }
                    else
                    {
                        var content = File.ReadAllText(ConfigFile);
                        _config = JsonConvert.DeserializeObject<SymbolConfig>(content);
                    }
                }
                return _config;
            }
        }

        public static void Init()
        {
            var butarget = EditorUserBuildSettings.activeBuildTarget;
            if (butarget == BuildTarget.Android)
            {
                CurSymbolContorler = AndroidSymbol;
            }
            else if (butarget == BuildTarget.iOS)
            {
                CurSymbolContorler = IosSymbol;
            }
            else if (butarget == BuildTarget.StandaloneWindows || butarget == BuildTarget.StandaloneWindows64)
            {
                CurSymbolContorler = StandaloneSymbol;
            }
        }

        public static void Save()
        {
            var content = JsonConvert.SerializeObject(_config, Formatting.Indented);
            File.WriteAllText(ConfigFile, content); 
        }      
        
        
    }
}

至此,我们通过提出概念和思路,再到理解和设计,然后落地和实现完成了一款可复用、可拓展、可自动布局的编辑器框架,并通过一个例子实现了应用,希望对大家有所帮助。