使用Xnode试着做了简单的对话系统
可视化了看起来是挺方便的,就是节点多了窗口会卡…
运行效果预览
需要的插件
- Xnode
用于绘制节点
- Odin
用于定制inspector窗口
自定义节点
首先要新建一个Graph脚本
[CreateAssetMenu(menuName = "Graph/对话图")]
public class DialogueGraph : NodeGraph {
}
然后我们需要开始结束,普通对话,选择分支,触发事件这些最基本的功能
所以分别写以下脚本用来定义节点
对话框
public class DialogueNode : Node {
[LabelText("说话人")] public string speaker;
[PreviewField(Alignment = ObjectFieldAlignment.Left), LabelText("头像")]
public Sprite head;
[TextArea, LabelText("说话内容")] public List<string> contents;
[Input(ShowBackingValue.Never), LabelText("上一段")] public string pre;
[LabelText("下一个是")] public NextType nextType;
[ShowIf("nextType", NextType.Dialogue), Output, LabelText("下一段对话")]
public DialogueNode nextDialogue;
[ShowIf("nextType", NextType.Branch), Output, LabelText("下一段分支")]
public BranchNode nextBranch;
[ShowIf("nextType", NextType.Flag), Output, LabelText("下一段标记")]
public FlagNode nextFlag;
[Output, LabelText("触发事件")] public EventNode trigger;
//下一个节点类型
public enum NextType {
[LabelText("对话框")] Dialogue,
[LabelText("分支框")] Branch,
[LabelText("标记框")] Flag
}
//类型与名字存起来 对连接进行限制时使用
private Dictionary<NextType, string> singleDt = new Dictionary<NextType, string>(){
{NextType.Dialogue, nameof(nextDialogue)},
{NextType.Branch, nameof(nextBranch)},
{NextType.Flag, nameof(nextFlag)}
};
protected override void Init() {
base.Init();
}
public override object GetValue(NodePort port) {
return null;
}
//当值更新时 (编辑器下)
private void OnValidate() {
//切换下一个类型的选项时 对所连接的节点进行限制
foreach (var s in singleDt) {
if (nextType!=s.Key) {
GetPort(s.Value).ClearConnections();
}
}
}
public override void OnCreateConnection(NodePort from, NodePort to) {
//限定连接节点类型
if (Outputs.Contains(from)) {
if (from.ValueType != to.node.GetType()) {
Debug.LogError("不能将" + from.ValueType + "端口连接到" + to.node.GetType() + "节点!");
GetPort(from.fieldName).Disconnect(to);
}
}
}
}
示例
分支框
public class BranchNode : Node {
[LabelText("提问人")]
public string speaker;
[PreviewField(Alignment = ObjectFieldAlignment.Left),LabelText("头像")]
public Sprite head;
[TextArea, LabelText("问题")] public string question;
[Input(ShowBackingValue.Never), LabelText("上一段")] public string pre;
[Output(dynamicPortList = true), LabelText("分支选项"), TextArea]
public List<string> branchs;
public override object GetValue(NodePort port) {
return null; // Replace this
}
}
示例
标记框和事件框
其实这俩可以写在一起的,但是懒得改了
public class FlagNode : Node {
[LabelText("节点类型")]
public FlagNodeType flagType;
public enum FlagNodeType {
Start,End
}
[Input((ShowBackingValue.Never)),ShowIf("flagType",FlagNodeType.End)]
public string pre;
[Output(),LabelText("下一段"),ShowIf("flagType",FlagNodeType.Start)] public string next;
public override object GetValue(NodePort port) {
return null;
}
}
public class EventNode : Node {
[Input(ShowBackingValue.Never), LabelText("触发对象")] public string triggerObj;
[LabelText("事件名称")] public string eventName;
public override object GetValue(NodePort port) {
return null; // Replace this
}
}
对话管理
首先写一些扩展方法
public static class NodeTool {
/// <summary>
/// 获取端口连接节点
/// </summary>
/// <param name="node"></param>
/// <param name="fieldName"></param>
/// <returns></returns>
public static Node GetNodeByField(this Node node, string fieldName) {
if (!node.HasPort(fieldName)) {
Debug.LogWarning("不存在 "+fieldName+" 端口!");
return null;
}
var port = node.GetPort(fieldName);
if (!port.IsConnected) {
Debug.LogWarning( fieldName+" 端口未连接!");
return null;
}
return port.Connection.node;
}
/// <summary>
/// 获取端口属性的值
/// </summary>
/// <param name="node"></param>
/// <param name="fieldName"></param>
/// <returns></returns>
public static object GetValuesByField(this Node node,string fieldName) {
return node.GetType().GetField(fieldName).GetValue(node);
}
}
原理就是获取结点然后依次解析其中的内容,然后更新UI
public class DialogueManager : MonoBehaviour {
public GameObject dialogueUi;
public DialogueGraph dialogueGraph;
private Text content;
private Text speaker;
private Image head;
[ShowInInspector] private Node currentNode;
[ShowInInspector] private List<string> contentList = new List<string>();
[ShowInInspector] private List<Button> branchBtns = new List<Button>();
public GameObject branchPb;
private void Awake() {
content = dialogueUi.transform.Find("Content").GetComponent<Text>();
speaker = dialogueUi.transform.Find("Speaker").GetComponent<Text>();
head = dialogueUi.transform.Find("Head").GetComponent<Image>();
}
void Start() {
dialogueUi.SetActive(false);
}
void Update() {
if (Input.GetKeyDown(KeyCode.A)) {
}
if (Input.GetKeyDown(KeyCode.E)) {
if (currentNode == null) {
dialogueUi.SetActive(true);
currentNode = dialogueGraph.nodes[0].GetOutputPort("next").Connection.node;
Debug.Log(currentNode);
UpdateDialogueUi(currentNode);
}
else if (currentNode.GetType() == typeof(DialogueNode)) {
ShowContent();
}
}
}
#region "对话系统逻辑部分"
private void UpdateDialogueUi(Node current) {
if (current.GetType() == typeof(DialogueNode)) {
var node = current as DialogueNode;
if (node != null) {
contentList.AddFromList(node.contents);
content.text = contentList[0];
speaker.text = node.speaker;
head.sprite = node.head;
}
}
else if (current.GetType() == typeof(BranchNode)) {
var branchNode = currentNode as BranchNode;
if (branchNode != null) {
contentList.Add(branchNode.question);
speaker.text = branchNode.speaker;
head.sprite = branchNode.head;
}
}
}
/// <summary>
/// 显示对话内容
/// </summary>
void ShowContent() {
if (contentList.Count > 0) {
contentList.RemoveAt(0);
if (contentList.Count == 0) {
foreach (var connection in currentNode.GetOutputPort("trigger").GetConnections()) {
TriggerEvent(connection.node as EventNode);
}
switch (currentNode.GetValuesByField("nextType")) {
case DialogueNode.NextType.Dialogue:
Debug.Log("进入对话框节点");
currentNode = currentNode.GetNodeByField("nextDialogue");
var node = currentNode as DialogueNode;
if (node != null) {
contentList.AddFromList(node.contents);
speaker.text = node.speaker;
head.sprite = node.head;
}
break;
case DialogueNode.NextType.Branch:
Debug.Log("进入分支框节点");
currentNode = currentNode.GetNodeByField("nextBranch");
UpdateDialogueUi(currentNode);
AddBranchClick(currentNode as BranchNode);
break;
case DialogueNode.NextType.Flag:
Debug.Log("进入标记框节点");
currentNode = currentNode.GetNodeByField("nextFlag");
FlagNode flagNode = currentNode as FlagNode;
if (flagNode != null && flagNode.flagType == FlagNode.FlagNodeType.End) {
Debug.Log("对话流程结束!");
dialogueUi.SetActive(false);
}
break;
}
}
if (contentList.Count > 0) {
content.text = contentList[0];
}
}
}
/// <summary>
/// 触发对话框连接的事件
/// </summary>
/// <param name="node"></param>
private void TriggerEvent(EventNode node) {
Debug.Log("触发事件:" + node.eventName);
}
void AddBranchClick(BranchNode node) {
for (int i = 0; i < node.branchs.Count; i++) {
var branchPort = node.GetOutputPort("branchs " + i);
var text = node.branchs[i];
Debug.Log("增加" + text + "分支");
var btn = Instantiate(branchPb, dialogueUi.transform.Find("Select").transform, false)
.GetComponent<Button>();
branchBtns.Add(btn);
btn.GetComponentInChildren<Text>().text = text;
if (branchPort.IsConnected) {
var i1 = i;
btn.onClick.AddListener(delegate {
foreach (var connection in branchPort.GetConnections()) {
if (connection.node.GetType() == typeof(EventNode)) {
TriggerEvent(connection.node as EventNode);
}
if (connection.node.GetType() == typeof(DialogueNode)) {
Debug.Log("点击分支" + i1 + "进入对话框节点");
currentNode = connection.node;
contentList.RemoveAt(0);
UpdateDialogueUi(currentNode);
}
}
//清楚所有按钮
for (int j = branchBtns.Count - 1; j >= 0; j--) {
Destroy(branchBtns[j].gameObject);
branchBtns.Remove(branchBtns[j]);
}
});
}
}
}
#endregion
}
UI层级与文件
最后将脚本挂到物体上即可
需要的可以直接在这下载packagehttps://legroft.lanzous.com/iH0Mqoy1q8d
大佬能不能分享下你的博客主题
这个是付费主题handsom
handsome
感谢回复,我去看看
大佬,这个博客也太优美了,爱了
感谢分享 赞一个