一、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实现灵活的数据驱动架构。