乐于分享
好东西不私藏

Unity编辑器扩展(六):三消关卡编辑器

Unity编辑器扩展(六):三消关卡编辑器

如图所示:实现一个简单的三消关卡编辑器功能:支持保存(ScriptableObject Asset) 与加载
总体架构(模块划分)
  • 数据层(Match3LevelData):关卡配置的序列化格式(目标、步数、初始格子、障碍、特殊元素等)。
  • 编辑器层(Match3LevelEditorWindow):Unity Editor 窗口或场景工具,用于绘制/编辑格子、设置参数、验证并保存为文件或 ScriptableObject。
  • 棋盘单格类型(Match3CellKind):空、障碍或者宝石颜色类型等
Match3LevelData类的具体实现
using System;using UnityEditor;using UnityEngine;namespace Match3.EditorTools{    public sealed class Match3LevelEditorWindow : EditorWindow    {        const float CellSize = 28f;        const float PaletteButtonHeight = 22f;        Match3LevelData _asset;        Match3LevelData _working;        Match3CellKind _brush = Match3CellKind.Red;        Vector2 _scroll;        bool _dirty;        int _editWidth = 8;        int _editHeight = 8;        [MenuItem("Window/Match3/关卡编辑器")]        publicstaticvoidOpen()        {            var w = GetWindow<Match3LevelEditorWindow>();            w.titleContent = new GUIContent("三消关卡");            w.minSize = new Vector2(420360);        }        publicstaticvoidOpenWith(Match3LevelData level)        {            var w = GetWindow<Match3LevelEditorWindow>();            w.titleContent = new GUIContent("三消关卡");            w.minSize = new Vector2(420360);            w.LoadLevel(level);        }        voidLoadLevel(Match3LevelData level)        {            if (_working == null)                _working = CreateInstance<Match3LevelData>();            _asset = level;            if (level != null)            {                _working.CopyFrom(level);                SyncSizeFieldsFromWorking();                _dirty = false;            }        }        voidOnEnable()        {            if (_working == null)                _working = CreateInstance<Match3LevelData>();            SyncSizeFieldsFromWorking();        }        voidSyncSizeFieldsFromWorking()        {            if (_working == nullreturn;            _editWidth = _working.Width;            _editHeight = _working.Height;        }        voidOnDisable()        {            if (_working != null)                DestroyImmediate(_working);            _working = null;        }        voidOnGUI()        {            if (_working == null)                _working = CreateInstance<Match3LevelData>();            EditorGUILayout.Space(4);            DrawToolbar();            EditorGUILayout.Space(6);            EditorGUILayout.LabelField("笔刷", EditorStyles.boldLabel);            DrawPalette();            EditorGUILayout.Space(6);            EditorGUILayout.LabelField("棋盘", EditorStyles.boldLabel);            DrawSizeControls();            EditorGUILayout.Space(4);            DrawGrid();            if (_dirty)                Repaint();        }        voidDrawToolbar()        {            using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))            {                var newAsset = (Match3LevelData)EditorGUILayout.ObjectField(_asset, typeof(Match3LevelData), false, GUILayout.Width(220));                if (newAsset != _asset)                {                    _asset = newAsset;                    if (_asset != null)                    {                        _working.CopyFrom(_asset);                        SyncSizeFieldsFromWorking();                        _dirty = false;                    }                }                GUILayout.FlexibleSpace();                if (GUILayout.Button("新建", EditorStyles.toolbarButton, GUILayout.Width(48)))                {                    if (EditorUtility.DisplayDialog("新建关卡""创建新的关卡资源?""确定""取消"))                    {                        CreateNewLevelAsset();                    }                }                using (new EditorGUI.DisabledScope(_asset == null))                {                    if (GUILayout.Button("保存", EditorStyles.toolbarButton, GUILayout.Width(48)))                        SaveToAsset();                }                if (GUILayout.Button("全部清空", EditorStyles.toolbarButton, GUILayout.Width(64)))                {                    _working.Fill(Match3CellKind.Empty);                    MarkDirty();                }            }        }        voidCreateNewLevelAsset()        {            string path = EditorUtility.SaveFilePanelInProject(                "新建三消关卡",                "Match3Level_New",                "asset",                "选择保存路径");            if (string.IsNullOrEmpty(path))                return;            var data = CreateInstance<Match3LevelData>();            data.CopyFrom(_working);            AssetDatabase.CreateAsset(data, path);            AssetDatabase.SaveAssets();            _asset = AssetDatabase.LoadAssetAtPath<Match3LevelData>(path);            _working.CopyFrom(_asset);            SyncSizeFieldsFromWorking();            Selection.activeObject = _asset;            EditorGUIUtility.PingObject(_asset);            _dirty = false;        }        voidSaveToAsset()        {            if (_asset == nullreturn;            Undo.RecordObject(_asset, "Save Match3 Level");            _asset.CopyFrom(_working);            EditorUtility.SetDirty(_asset);            AssetDatabase.SaveAssets();            _dirty = false;        }        voidMarkDirty()        {            _dirty = true;        }        voidDrawPalette()        {            foreach (Match3CellKind k in Enum.GetValues(typeof(Match3CellKind)))            {                using (new EditorGUILayout.HorizontalScope())                {                    var prev = EditorGUIUtility.labelWidth;                    EditorGUIUtility.labelWidth = 72;                    bool sel = _brush == k;                    EditorGUILayout.LabelField(GetKindLabel(k), GUILayout.Width(80));                    EditorGUIUtility.labelWidth = prev;                    var c = KindColor(k);                    GUI.backgroundColor = sel ? Color.Lerp(c, Color.white, 0.35f) : Color.white;                    if (GUILayout.Button(sel ? "当前" : "选用", GUILayout.Height(PaletteButtonHeight), GUILayout.Width(56)))                    {                        _brush = k;                    }                    GUI.backgroundColor = Color.white;                    EditorGUILayout.LabelField("", GUIStyle.none, GUILayout.Width(8));                    EditorGUI.DrawRect(GUILayoutUtility.GetRect(2020, GUILayout.Width(20), GUILayout.Height(20)), c);                }            }        }        staticstringGetKindLabel(Match3CellKind k)        {            switch (k)            {                case Match3CellKind.Empty: return "空";                case Match3CellKind.Blocked: return "阻挡";                case Match3CellKind.Red: return "红";                case Match3CellKind.Blue: return "蓝";                case Match3CellKind.Green: return "绿";                case Match3CellKind.Yellow: return "黄";                case Match3CellKind.Purple: return "紫";                case Match3CellKind.Orange: return "橙";                defaultreturn k.ToString();            }        }        static Color KindColor(Match3CellKind k)        {            switch (k)            {                case Match3CellKind.Empty: return new Color(0.25f0.25f0.28f1f);                case Match3CellKind.Blocked: return new Color(0.15f0.15f0.15f1f);                case Match3CellKind.Red: return new Color(0.9f0.25f0.25f1f);                case Match3CellKind.Blue: return new Color(0.25f0.45f0.95f1f);                case Match3CellKind.Green: return new Color(0.3f0.8f0.35f1f);                case Match3CellKind.Yellow: return new Color(0.95f0.85f0.2f1f);                case Match3CellKind.Purple: return new Color(0.65f0.35f0.9f1f);                case Match3CellKind.Orange: return new Color(0.95f0.55f0.2f1f);                defaultreturn Color.gray;            }        }        voidDrawSizeControls()        {            using (new EditorGUILayout.HorizontalScope())            {                _editWidth = Mathf.Clamp(EditorGUILayout.IntField("宽", _editWidth, GUILayout.MinWidth(140)), 132);                _editHeight = Mathf.Clamp(EditorGUILayout.IntField("高", _editHeight, GUILayout.MinWidth(140)), 132);                if (GUILayout.Button("应用尺寸", GUILayout.Width(72)))                {                    if (_editWidth != _working.Width || _editHeight != _working.Height)                    {                        bool preserve = EditorUtility.DisplayDialog(                            "调整棋盘大小",                            "是否保留已有格子内容?(超出部分会丢弃)",                            "保留",                            "清空重设");                        _working.SetSize(_editWidth, _editHeight, preserve);                        SyncSizeFieldsFromWorking();                        MarkDirty();                    }                }            }            using (new EditorGUILayout.HorizontalScope())            {                EditorGUILayout.LabelField("步数限制", GUILayout.Width(64));                int moves = EditorGUILayout.IntField(_working.MoveLimit, GUILayout.Width(80));                if (moves != _working.MoveLimit)                {                    _working.SetMoveLimit(moves);                    MarkDirty();                }            }        }        voidDrawGrid()        {            int gw = _working.Width;            int gh = _working.Height;            float totalW = gw * CellSize + 24f;            float totalH = gh * CellSize + 24f;            _scroll = EditorGUILayout.BeginScrollView(_scroll, GUILayout.ExpandHeight(true));            var area = GUILayoutUtility.GetRect(totalW, totalH);            EditorGUI.DrawRect(area, new Color(0.18f0.18f0.2f1f));            var e = Event.current;            if (e.type == EventType.MouseDown || e.type == EventType.MouseDrag)            {                if (e.button == 0)                {                    Vector2 local = e.mousePosition - new Vector2(area.x + 8f, area.y + 8f);                    if (local.x >= 0 && local.y >= 0)                    {                        int cx = Mathf.FloorToInt(local.x / CellSize);                        int cy = Mathf.FloorToInt(local.y / CellSize);                        if (cx >= 0 && cy >= 0 && cx < gw && cy < gh)                        {                            var current = (Match3CellKind)_working.GetCell(cx, cy);                            if (current != _brush || e.type == EventType.MouseDown)                            {                                _working.SetCell(cx, cy, _brush);                                MarkDirty();                            }                            e.Use();                        }                    }                }            }            if (e.type == EventType.Repaint)            {                for (int y = 0; y < gh; y++)                for (int x = 0; x < gw; x++)                {                    var kind = (Match3CellKind)_working.GetCell(x, y);                    var r = new Rect(area.x + 8f + x * CellSize, area.y + 8f + y * CellSize, CellSize - 2f, CellSize - 2f);                    EditorGUI.DrawRect(r, KindColor(kind));                }            }            EditorGUILayout.EndScrollView();        }    }}
Match3CellKind类的具体实现:
namespace Match3{    ///<summary>棋盘单格类型:空、障碍或宝石颜色。</summary>    public enum Match3CellKind : byte    {        Empty = 0,        Blocked = 1,        Red = 10,        Blue = 11,        Green = 12,        Yellow = 13,        Purple = 14,        Orange = 15    }}

Match3LevelData的具体实现:
using UnityEngine;namespace Match3{    ///<summary>三消关卡数据,可在 Project 中创建资源并由关卡编辑器编辑。</summary>    [CreateAssetMenu(fileName = "Match3Level", menuName = "Match3/Level Data", order = 0)]    public class Match3LevelData : ScriptableObject    {        [SerializeFieldint width = 8;        [SerializeFieldint height = 8;        [SerializeFieldint moveLimit = 30;        [SerializeFieldint[] cells;        public int Width => width;        public int Height => height;        public int MoveLimit => moveLimit;        ///<summary>        /// 步数限制        ///</summary>        ///<param name="moves"></param>        publicvoidSetMoveLimit(int moves)        {            moveLimit = Mathf.Max(1, moves);        }        ///<summary>        ///        ///</summary>        ///<param name="x"></param>        ///<param name="y"></param>        ///<returns></returns>        publicintGetCell(int x, int y)        {            if (x < 0 || y < 0 || x >= width || y >= height) return (int)Match3CellKind.Empty;            int i = y * width + x;            if (cells == null || i < 0 || i >= cells.Length) return (int)Match3CellKind.Empty;            return cells[i];        }        publicvoidSetCell(int x, int y, Match3CellKind kind)        {            if (x < 0 || y < 0 || x >= width || y >= height) return;            EnsureCells();            cells[y * width + x] = (int)kind;        }        publicvoidSetSize(int newWidth, int newHeight, bool preserveContent)        {            newWidth = Mathf.Clamp(newWidth, 132);            newHeight = Mathf.Clamp(newHeight, 132);            if (!preserveContent || cells == null || width <= 0 || height <= 0)            {                width = newWidth;                height = newHeight;                cells = new int[width * height];                return;            }            var old = cells;            int ow = width;            int oh = height;            width = newWidth;            height = newHeight;            cells = new int[width * height];            int copyW = Mathf.Min(ow, newWidth);            int copyH = Mathf.Min(oh, newHeight);            for (int y = 0; y < copyH; y++)            for (int x = 0; x < copyW; x++)                cells[y * newWidth + x] = old[y * ow + x];        }        publicvoidFill(Match3CellKind kind)        {            EnsureCells();            int v = (int)kind;            for (int i = 0; i < cells.Length; i++)                cells[i] = v;        }        publicvoidCopyFrom(Match3LevelData other)        {            if (other == nullreturn;            width = other.width;            height = other.height;            moveLimit = other.moveLimit;            if (other.cells != null)            {                cells = new int[other.cells.Length];                System.Array.Copy(other.cells, cells, other.cells.Length);            }            else                cells = new int[width * height];        }        voidEnsureCells()        {            int len = width * height;            if (cells == null || cells.Length != len)                cells = new int[len];        }        voidOnValidate()        {            width = Mathf.Clamp(width, 132);            height = Mathf.Clamp(height, 132);            EnsureCells();        }    }}

同时也可以在通过以下方式直接创建