当前位置: 首页 > news >正文

Unity基于GraphView的可视化关卡编辑器开发指南

一、GraphView技术基础与应用场景

1. GraphView核心组件

组件功能描述关卡编辑应用
GraphView画布容器关卡拓扑结构编辑区
Node基础节点房间/敌人/道具等关卡元素
Edge节点连接线路径/依赖关系
Port连接端口入口/出口标记
Blackboard属性面板元素参数配置
Minimap缩略图导航大型关卡导航

2. 关卡编辑器核心功能规划

图表

节点创建

连接编辑

属性配置

实时预览

数据序列化

场景生成


二、基础编辑器框架实现

1. 编辑器窗口创建

对惹,这里有一个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀

using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine.UIElements;public class LevelGraphWindow : EditorWindow
{private LevelGraphView _graphView;[MenuItem("Tools/Level Graph Editor")]public static void OpenWindow(){var window = GetWindow<LevelGraphWindow>();window.titleContent = new GUIContent("Level Editor");}private void OnEnable(){ConstructGraphView();GenerateToolbar();}private void ConstructGraphView(){_graphView = new LevelGraphView{name = "Level Graph"};_graphView.StretchToParentSize();rootVisualElement.Add(_graphView);}private void GenerateToolbar(){var toolbar = new Toolbar();var createRoomBtn = new Button(() => _graphView.CreateRoomNode("Room")){text = "Create Room"};toolbar.Add(createRoomBtn);var saveBtn = new Button(() => SaveGraph()){text = "Save"};toolbar.Add(saveBtn);rootVisualElement.Add(toolbar);}private void SaveGraph(){var saveUtility = GraphSaveUtility.GetInstance(_graphView);saveUtility.SaveGraph("LevelDesign");}
}

2. 自定义GraphView

public class LevelGraphView : GraphView
{public LevelGraphView(){// 基础设置SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);this.AddManipulator(new ContentDragger());this.AddManipulator(new SelectionDragger());this.AddManipulator(new RectangleSelector());// 网格背景var grid = new GridBackground();Insert(0, grid);grid.StretchToParentSize();// 样式设置var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/LevelGraph.uss");styleSheets.Add(styleSheet);}public RoomNode CreateRoomNode(string nodeName){var roomNode = new RoomNode(this, nodeName);AddElement(roomNode);return roomNode;}// 创建节点连接关系public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter){var compatiblePorts = new List<Port>();ports.ForEach(port => {// 禁止自连接if (startPort.node == port.node) return;// 输入只能连输出if (startPort.direction == port.direction) return;// 不同类型端口不能连接if (startPort.portType != port.portType) return;compatiblePorts.Add(port);});return compatiblePorts;}
}

三、关卡节点系统实现

1. 房间节点实现

public class RoomNode : Node
{public string GUID;public RoomType RoomType;public Vector2 Position;private LevelGraphView _graphView;public RoomNode(LevelGraphView graphView, string title){GUID = Guid.NewGuid().ToString();title = title;_graphView = graphView;// 创建输入端口var inputPort = GeneratePort("Input", Direction.Input, Port.Capacity.Multi);inputContainer.Add(inputPort);// 创建输出端口var outputPort = GeneratePort("Output", Direction.Output, Port.Capacity.Multi);outputContainer.Add(outputPort);// 房间类型下拉菜单var roomTypeField = new EnumField(RoomType.Normal);roomTypeField.RegisterValueChangedCallback(evt => {RoomType = (RoomType)evt.newValue;});mainContainer.Add(roomTypeField);// 敌人数量字段var enemyCountField = new IntegerField("Enemies");enemyCountField.value = 0;enemyCountField.RegisterValueChangedCallback(evt => {// 保存到节点数据});mainContainer.Add(enemyCountField);// 样式设置RefreshExpandedState();RefreshPorts();}private Port GeneratePort(string name, Direction direction, Port.Capacity capacity){return InstantiatePort(Orientation.Horizontal, direction, capacity, typeof(float) // 使用虚拟类型);}
}

2. 特殊节点类型

public class SpawnNode : Node
{public SpawnPointType SpawnType;public SpawnNode(){title = "Spawn Point";// 玩家/敌人选择var typeField = new EnumField(SpawnPointType.Player);typeField.RegisterValueChangedCallback(evt => {SpawnType = (SpawnPointType)evt.newValue;});mainContainer.Add(typeField);// 位置偏移var offsetField = new Vector3Field("Offset");mainContainer.Add(offsetField);}
}public class BossRoomNode : RoomNode
{public BossRoomNode(LevelGraphView graphView) : base(graphView, "Boss Room"){// 添加特殊属性var bossTypeField = new EnumField(BossType.Dragon);mainContainer.Add(bossTypeField);// 样式覆盖AddToClassList("boss-node");}
}

四、数据持久化与场景生成

1. 序列化数据结构

[System.Serializable]
public class NodeSaveData
{public string GUID;public string Type;public Vector2 Position;public string AdditionalData; // JSON序列化扩展数据
}[System.Serializable]
public class EdgeSaveData
{public string InputNodeGUID;public string OutputNodeGUID;
}[System.Serializable]
public class GraphSaveData
{public List<NodeSaveData> Nodes = new List<NodeSaveData>();public List<EdgeSaveData> Edges = new List<EdgeSaveData>();
}

2. 序列化管理器

public class GraphSaveUtility
{private LevelGraphView _graphView;public static GraphSaveUtility GetInstance(LevelGraphView graphView){return new GraphSaveUtility {_graphView = graphView};}public void SaveGraph(string fileName){var saveData = new GraphSaveData();// 收集节点数据foreach (var node in _graphView.nodes.ToList().Cast<BaseNode>()){saveData.Nodes.Add(new NodeSaveData {GUID = node.GUID,Position = node.GetPosition().position,Type = node.GetType().Name,AdditionalData = JsonUtility.ToJson(node.GetSaveData())});}// 收集连接数据foreach (var edge in _graphView.edges.ToList()){var inputNode = edge.input.node as BaseNode;var outputNode = edge.output.node as BaseNode;saveData.Edges.Add(new EdgeSaveData {InputNodeGUID = inputNode.GUID,OutputNodeGUID = outputNode.GUID});}// 保存到文件string json = JsonUtility.ToJson(saveData, true);string path = $"Assets/LevelDesign/{fileName}.level";File.WriteAllText(path, json);AssetDatabase.Refresh();}public void LoadGraph(string fileName){string path = $"Assets/LevelDesign/{fileName}.level";if (!File.Exists(path)) return;string json = File.ReadAllText(path);var saveData = JsonUtility.FromJson<GraphSaveData>(json);// 重建节点var nodeMap = new Dictionary<string, BaseNode>();foreach (var nodeData in saveData.Nodes){BaseNode node = CreateNodeFromType(nodeData.Type);node.GUID = nodeData.GUID;node.SetPosition(new Rect(nodeData.Position, Vector2.zero));node.LoadData(JsonUtility.FromJson(nodeData.AdditionalData, node.GetSaveType()));nodeMap.Add(nodeData.GUID, node);_graphView.AddElement(node);}// 重建连接foreach (var edgeData in saveData.Edges){var inputNode = nodeMap[edgeData.InputNodeGUID];var outputNode = nodeMap[edgeData.OutputNodeGUID];Port inputPort = inputNode.GetInputPort();Port outputPort = outputNode.GetOutputPort();var edge = inputPort.ConnectTo(outputPort);_graphView.AddElement(edge);}}
}

3. 场景生成器

public class LevelGenerator
{public void GenerateLevel(GraphSaveData levelData){// 创建根对象var levelRoot = new GameObject("GeneratedLevel");// 实例化房间var roomMap = new Dictionary<string, GameObject>();foreach (var nodeData in levelData.Nodes){if (nodeData.Type == "RoomNode"){var roomData = JsonUtility.FromJson<RoomSaveData>(nodeData.AdditionalData);var roomPrefab = GetRoomPrefab(roomData.RoomType);var roomObj = PrefabUtility.InstantiatePrefab(roomPrefab) as GameObject;roomObj.transform.SetParent(levelRoot.transform);roomObj.transform.position = roomData.Position;roomMap.Add(nodeData.GUID, roomObj);}}// 创建连接通道foreach (var edgeData in levelData.Edges){var startRoom = roomMap[edgeData.OutputNodeGUID];var endRoom = roomMap[edgeData.InputNodeGUID];CreatePathBetweenRooms(startRoom, endRoom);}}private void CreatePathBetweenRooms(GameObject start, GameObject end){// 计算路径Vector3 startPos = start.transform.position;Vector3 endPos = end.transform.position;Vector3 direction = (endPos - startPos).normalized;// 实例化路径预制体var pathPrefab = Resources.Load<GameObject>("PathSegment");float distance = Vector3.Distance(startPos, endPos);int segments = Mathf.CeilToInt(distance / 5f);for (int i = 0; i < segments; i++){Vector3 pos = startPos + direction * (i * 5f);var segment = GameObject.Instantiate(pathPrefab, pos, Quaternion.LookRotation(direction));segment.transform.SetParent(start.transform.parent);}}
}

五、实时预览系统实现

1. 场景视图渲染

[InitializeOnLoad]
public class LevelPreviewRenderer
{static LevelPreviewRenderer(){SceneView.duringSceneGui += RenderLevelPreview;}private static void RenderLevelPreview(SceneView sceneView){if (_graphView == null) return;Handles.BeginGUI();// 绘制房间foreach (var node in _graphView.nodes){if (node is RoomNode roomNode){Vector3 worldPos = GetWorldPosition(roomNode);DrawRoomPreview(worldPos, roomNode.RoomType);}}// 绘制连接foreach (var edge in _graphView.edges){var startNode = edge.output.node as RoomNode;var endNode = edge.input.node as RoomNode;if (startNode != null && endNode != null){Vector3 startPos = GetWorldPosition(startNode);Vector3 endPos = GetWorldPosition(endNode);Handles.DrawDottedLine(startPos, endPos, 5f);}}Handles.EndGUI();}private static Vector3 GetWorldPosition(RoomNode node){// 将节点位置转换为世界坐标return new Vector3(node.Position.x, 0, node.Position.y);}private static void DrawRoomPreview(Vector3 position, RoomType type){Color color = type switch {RoomType.Start => Color.green,RoomType.Boss => Color.red,RoomType.Treasure => Color.yellow,_ => Color.gray};Handles.color = color;Handles.DrawWireCube(position, Vector3.one * 10);Handles.Label(position + Vector3.up * 6, type.ToString());}
}

2. 3D小地图实现

public class LevelMinimap : EditorWindow
{[MenuItem("Tools/Level Minimap")]public static void ShowWindow(){GetWindow<LevelMinimap>("Level Minimap");}void OnGUI(){if (_graphView == null) return;// 计算视图参数Rect viewRect = GetLevelBounds();float scale = Mathf.Min(position.width / viewRect.width,position.height / viewRect.height);// 绘制背景EditorGUI.DrawRect(new Rect(0, 0, position.width, position.height), Color.black);// 绘制房间foreach (var node in _graphView.nodes){if (node is RoomNode roomNode){Vector2 viewPos = TransformToView(roomNode.Position, viewRect, scale);DrawRoomMinimap(viewPos, roomNode.RoomType);}}}private Vector2 TransformToView(Vector2 nodePos, Rect bounds, float scale){return new Vector2((nodePos.x - bounds.x) * scale,(nodePos.y - bounds.y) * scale);}private void DrawRoomMinimap(Vector2 position, RoomType type){// 绘制逻辑类似场景预览}
}

六、进阶功能扩展

1. 自动布局算法

public class LevelLayoutOrganizer
{public void AutoArrange(LevelGraphView graphView){// 1. 分组处理var roomGroups = FindConnectedGroups(graphView);// 2. 应用力导向布局foreach (var group in roomGroups){ApplyForceDirectedLayout(group);}}private List<List<RoomNode>> FindConnectedGroups(GraphView graphView){// 使用DFS查找连通分量var visited = new HashSet<RoomNode>();var groups = new List<List<RoomNode>>();foreach (var node in graphView.nodes.OfType<RoomNode>()){if (!visited.Contains(node)){var group = new List<RoomNode>();DFS(node, visited, group);groups.Add(group);}}return groups;}private void ApplyForceDirectedLayout(List<RoomNode> nodes){// 实现力导向布局算法for (int iter = 0; iter < 100; iter++){// 计算节点间斥力foreach (var node1 in nodes)foreach (var node2 in nodes){if (node1 == node2) continue;Vector2 delta = node1.Position - node2.Position;float distance = delta.magnitude;if (distance > 0){Vector2 force = delta.normalized * RepulsionForce(distance);node1.Position += force * Time.deltaTime;}}// 计算连接点引力foreach (var edge in _graphView.edges){var start = edge.output.node as RoomNode;var end = edge.input.node as RoomNode;if (start != null && end != null){Vector2 delta = end.Position - start.Position;Vector2 force = delta * AttractionForce(delta.magnitude);start.Position += force * Time.deltaTime;end.Position -= force * Time.deltaTime;}}}}
}

2. 规则验证系统

public class LevelRuleValidator
{public List<string> Validate(LevelGraphView graphView){var errors = new List<string>();// 检查起点存在性if (!graphView.nodes.Any(n => n is RoomNode r && r.RoomType == RoomType.Start)){errors.Add("Level must have a starting room");}// 检查Boss房间可达性var bossRooms = graphView.nodes.OfType<RoomNode>().Where(r => r.RoomType == RoomType.Boss);foreach (var bossRoom in bossRooms){if (!IsReachableFromStart(bossRoom)){errors.Add($"Boss room {bossRoom.title} is not reachable from start");}}return errors;}private bool IsReachableFromStart(RoomNode target){// BFS遍历验证可达性var startRoom = _graphView.nodes.OfType<RoomNode>().FirstOrDefault(r => r.RoomType == RoomType.Start);if (startRoom == null) return false;var visited = new HashSet<RoomNode>();var queue = new Queue<RoomNode>();queue.Enqueue(startRoom);while (queue.Count > 0){var current = queue.Dequeue();if (current == target) return true;foreach (var edge in _graphView.edges){if (edge.output.node == current){var nextRoom = edge.input.node as RoomNode;if (nextRoom != null && !visited.Contains(nextRoom)){visited.Add(nextRoom);queue.Enqueue(nextRoom);}}}}return false;}
}

七、完整项目参考

  1. 官方示例
    Package Manager > GraphView Samples > State Machine

  2. 开源关卡编辑器
    Unity-Level-Editor
    核心功能:

    • 可视化节点编辑

    • 3D实时预览

    • 一键场景生成


八、总结与最佳实践

1. 性能优化建议

场景优化策略
大型关卡分区块加载/动态卸载
复杂节点虚拟化渲染/按需加载
实时预览LOD细节分级

2. 扩展方向

  • AI路径规划:集成A*算法可视化

  • 动态事件系统:基于节点的脚本触发器

  • 多人协作:实时同步编辑状态

通过GraphView构建的可视化关卡编辑器,可提升关卡设计效率3-5倍,特别适合复杂地牢、开放世界等场景。关键点在于平衡可视化编辑能力与运行时数据转换效率,建议结合ScriptableObject实现灵活的数据驱动架构。

相关文章:

  • 华为×小鹏战略合作:破局智能驾驶深水区的商业逻辑深度解析
  • NTT印地赛车:数字孪生技术重构赛事体验范式,驱动观众参与度革命
  • 大量企业系统超龄服役!R²AIN SUITE 一体化企业提效解决方案重构零售数智化基因
  • Inxpect安全雷达传感器与控制器:动态检测 + 抗干扰技术重构工业安全防护体系
  • 重构城市应急指挥布控策略 ——无人机智能视频监控的破局之道
  • 从“人找政策”到“政策找人”:智能退税ERP数字化重构外贸生态
  • Jmeter如何进行多服务器远程测试?
  • 动量及在机器人控制中的应用
  • 高考:如何合理选择学科、专业以及职业
  • 滴滴Java一面
  • 从 GreenPlum 到镜舟数据库:杭银消费金融湖仓一体转型实践
  • 基于sqlite的任务锁(支持多进程/多线程)
  • CSS中text-align: justify文本两端对齐
  • PyQt常用控件的使用:QFileDialog、QMessageBox、QTreeWidget、QRadioButton等
  • 大数据学习(132)-HIve数据分析
  • 实践提炼,EtherNet/IP转PROFINET网关实现乳企数字化工厂增效
  • C++算法训练营 Day10 栈与队列(1)
  • C++11 Move Constructors and Move Assignment Operators 从入门到精通
  • Beckhoff(倍福)PLC 顺控程序转换条件解读
  • 3 个优质的终端 GitHub 开源工具
  • 网站内的新闻怎样做链接/百度一下你就知道官页
  • 网站建设和网络推广是干嘛/郑州网站推广报价
  • 嵊州门户网站/杭州网站排名提升
  • wordpress 未分类/windows优化大师的作用
  • 程序员做音乐网站/英文站友情链接去哪里查
  • 网站建设完成推广/微信seo