一、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; } }
七、完整项目参考
官方示例
Package Manager > GraphView Samples > State Machine
开源关卡编辑器
Unity-Level-Editor
核心功能:可视化节点编辑
3D实时预览
一键场景生成
八、总结与最佳实践
1. 性能优化建议
场景 | 优化策略 |
---|---|
大型关卡 | 分区块加载/动态卸载 |
复杂节点 | 虚拟化渲染/按需加载 |
实时预览 | LOD细节分级 |
2. 扩展方向
AI路径规划:集成A*算法可视化
动态事件系统:基于节点的脚本触发器
多人协作:实时同步编辑状态
通过GraphView构建的可视化关卡编辑器,可提升关卡设计效率3-5倍,特别适合复杂地牢、开放世界等场景。关键点在于平衡可视化编辑能力与运行时数据转换效率,建议结合ScriptableObject实现灵活的数据驱动架构。