Unity Graph View打造图形化对话编辑系统(四)——视图类
目录
视频效果演示
Graph View打造图形化对话编辑系统效果展示
最终源码先附在此
DialogueTreeView
这个应该算是核心了吧,处理节点的编辑。关键有这么几处:
// 当视图内容改变时,比如用户建立了连线、删除了节点等等操作
private GraphViewChange OnGraphViewChanged(GraphViewChange graphViewChange)
{
if (treeData != null)
{
if (graphViewChange.elementsToRemove != null)
{
graphViewChange.elementsToRemove.ForEach(x =>
{
if (x is NodeView node)
{
// 删除节点
node.OnNodeSelected -= WhenNodeSelected;
treeData.RemoveNode(node.nodeData);
}
else if (x is Edge edge)
{
// 删除连线
NodeView parent = edge.output.node as NodeView;
string key = parent.GetOutputPortIndex(edge.output);
if( ! string.IsNullOrEmpty(key))
{
parent.nodeData.BreakNode(key);
}
}
});
}
if( graphViewChange.edgesToCreate != null )
{
// 创建连线
graphViewChange.edgesToCreate.ForEach(e =>
{
NodeView parent = e.output.node as NodeView;
string key = parent.GetOutputPortIndex(e.output);
if( ! string.IsNullOrWhiteSpace( key ))
{
NodeView child = e.input.node as NodeView;
parent.nodeData.LinkNode(key, child.nodeData);
}
});
}
}
return graphViewChange;
}
还有我们重新构造一下右键菜单,用来创建我们自己定义的节点:
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
if (treeData != null)
{
// 枚举所有的类型,这样当我们创建新的类型时,原则上可以不用改这里的代码。
foreach (Type t in TypeCache.GetTypesDerivedFrom<DialogNodeBase>())
{
string menuItem;
if (t == typeof(StartNode))
{
// 一棵树有且只有一个开始节点
if (treeData.startNode != null)
return;
menuItem = "创建[开始]节点";
}
else if (t == typeof(SelectorBranch))
menuItem = "创建[玩家]节点";
else if (t == typeof(RandomBranch))
menuItem = "创建[NPC]节点";
else
menuItem = $"创建[{t.name}]节点";
evt.menu.AppendAction( menuItem, x =>
{
DialogNodeBase node = treeData.CreateNode(t);
BuildNodeView(node);
});
}
}
}
还有关键的一点,我们需要知道节点中哪一些port是兼容的,也就是可以连线的。由于我们对话系统中,只有一种连线,不涉及不同的数据类型的连线,因此这里就比较简单了:
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
// 相同方向的连线不兼容,就是说一个节点的输出必须接到另一个节点的输入上,而不能输出接输出,或输入接输入。
// 自己的输出不能接自己的输入
return ports.ToList().Where(endPorts =>
endPorts.direction != startPort.direction &&
endPorts.node != startPort.node &&
endPorts.portType == startPort.portType
).ToList();
}
NodeView
这个当然是用来处理节点的了,也是非常关键的类。
比较关键的是创建接口了:
private void CreateOutputPorts()
{
if (nodeData != null && nodeData.outputItems != null)
{
foreach (var item in nodeData.outputItems)
{
Port o = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(bool));
o.name = "OutPut";
o.portName = item;
o.portColor = Color.green;
outputPorts.Add(item, o);
outputContainer.Add(o);
}
}
}
private void CreateInputPorts()
{
if( ! ( nodeData is StartNode ))
{
Port i = InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi, typeof(bool));
i.name = "InPut";
i.portName = "";
i.portColor = Color.green;
inputPort = i;
inputContainer.Add(i);
}
}
事实上,这里创建接口时,InstantiatePort的最后一个参数,是什么类型无所谓的。甚至可以是自定义的类型,只要保持一致即可。如果将来有不同的连线类别,那这里就可以来区分一下。
还有处理位置信息的函数:
// 将位置信息记录起来,以便下次加载时,将节点设置到正确的位置上
public override void SetPosition(Rect newPos)
{
base.SetPosition(newPos);
nodeData.position = newPos.min;
}
InspectorView
整个属性面板是最简单的了:
public class InspectorView : VisualElement
{
public new class UxmlFactory : UxmlFactory<InspectorView, VisualElement.UxmlTraits> { }
private Editor editor = null;
public void UpdateSelection(NodeView node)
{
Clear();
if (editor != null)
Object.DestroyImmediate(editor);
editor = Editor.CreateEditor(node.nodeData);
IMGUIContainer container = new IMGUIContainer(() => editor.OnInspectorGUI());
Add(container);
}
}
除此之外
上面列出的都是 一些关键性的代码,除此之外,就是各种消息的处理了。比如当节点被选择时,在inspector面板中显示属性,其实就是调用(InspectorView.UpdateSelection,如上已列出),当节点信息被改变时,重新构造一下NodeView里面的接口。