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

-
数据层(Match3LevelData):关卡配置的序列化格式(目标、步数、初始格子、障碍、特殊元素等)。 -
编辑器层(Match3LevelEditorWindow):Unity Editor 窗口或场景工具,用于绘制/编辑格子、设置参数、验证并保存为文件或 ScriptableObject。 -
棋盘单格类型(Match3CellKind):空、障碍或者宝石颜色类型等
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(420, 360);}publicstaticvoidOpenWith(Match3LevelData level){var w = GetWindow<Match3LevelEditorWindow>();w.titleContent = new GUIContent("三消关卡");w.minSize = new Vector2(420, 360);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 == null) return;_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 == null) return;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(20, 20, 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 "橙";default: return k.ToString();}}static Color KindColor(Match3CellKind k){switch (k){case Match3CellKind.Empty: return new Color(0.25f, 0.25f, 0.28f, 1f);case Match3CellKind.Blocked: return new Color(0.15f, 0.15f, 0.15f, 1f);case Match3CellKind.Red: return new Color(0.9f, 0.25f, 0.25f, 1f);case Match3CellKind.Blue: return new Color(0.25f, 0.45f, 0.95f, 1f);case Match3CellKind.Green: return new Color(0.3f, 0.8f, 0.35f, 1f);case Match3CellKind.Yellow: return new Color(0.95f, 0.85f, 0.2f, 1f);case Match3CellKind.Purple: return new Color(0.65f, 0.35f, 0.9f, 1f);case Match3CellKind.Orange: return new Color(0.95f, 0.55f, 0.2f, 1f);default: return Color.gray;}}voidDrawSizeControls(){using (new EditorGUILayout.HorizontalScope()){_editWidth = Mathf.Clamp(EditorGUILayout.IntField("宽", _editWidth, GUILayout.MinWidth(140)), 1, 32);_editHeight = Mathf.Clamp(EditorGUILayout.IntField("高", _editHeight, GUILayout.MinWidth(140)), 1, 32);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.18f, 0.18f, 0.2f, 1f));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{[SerializeField] int width = 8;[SerializeField] int height = 8;[SerializeField] int moveLimit = 30;[SerializeField] int[] 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, 1, 32);newHeight = Mathf.Clamp(newHeight, 1, 32);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 == null) return;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);}elsecells = new int[width * height];}voidEnsureCells(){int len = width * height;if (cells == null || cells.Length != len)cells = new int[len];}voidOnValidate(){width = Mathf.Clamp(width, 1, 32);height = Mathf.Clamp(height, 1, 32);EnsureCells();}}}

夜雨聆风