添加节点类型
打开NodeType.cs,添加四个节点类型:
C#
namespace E.Story
{
public enum NodeType
{
/* ... 此处代码已省略 ... */
// 开始
Start = 21,
// 结束
End = 41,
// 对话
Dialogue = 51,
// 分支
Branch = 61
}
}
创建句子数据类
- 打开Data文件夹,创建C#文件,命名为
SentenceData
- 打开SentenceData.cs,将内容修改如下:
C#
using System;
using UnityEngine;
namespace E.Story
{
// 句子数据
[Serializable]
public class SentenceData
{
[SerializeField] private string text;
// 句子文本
public string Text { get => text; set => text = value; }
// 构造器
public SentenceData(string text)
{
this.text = text;
}
}
}
创建实用派生节点
创建对话节点类
- 打开UI Node文件夹,创建C#文件,命名为
DialogueNode
- 打开DialogueNode.cs,将内容修改如下:
C#
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace E.Story
{
// 对话节点
public class DialogueNode : SingleInSingleOutNode
{
// 角色名称
public string RoleName { get; set; }
// 句子列表
public List SentenceDatas { get; set; }
public override void Init(StoryGraphView graphView, string title, Vector2 position)
{
base.Init(graphView, title, position);
// 重设属性默认值
Type = NodeType.Dialogue;
RoleName = "角色名称";
SentenceDatas = new()
{
new SentenceData("发言内容")
};
}
protected override void DrawExtensionContainer()
{
// 创建自定义容器
customDataContainer = new();
// 创建折叠框
foldout = ElementUtility.CreateFoldout("节点内容");
// 创建角色信息容器
VisualElement roleInfoRowContainer = new();
VisualElement roleInfoColContainer = new();
// 创建角色名称输入框
TextField tfdRoleName = ElementUtility.CreateTextField(RoleName, null, callback =>
{
RoleName = callback.newValue;
});
// 将UI元素放置到对应位置
roleInfoColContainer.Add(tfdRoleName);
roleInfoRowContainer.Add(roleInfoColContainer);
foldout.Add(roleInfoRowContainer);
// 创建添加按钮
Button btnAdd = ElementUtility.CreateButton("添加句子", () =>
{
SentenceData sentenceData = new("新句子");
SentenceDatas.Add(sentenceData);
VisualElement lineContainer = CreateSentenceData(sentenceData);
foldout.Add(lineContainer);
});
// 放置UI元素
foldout.Add(btnAdd);
customDataContainer.Add(foldout);
extensionContainer.Add(customDataContainer);
// 遍历列表并创建句子条目
foreach (SentenceData sentenceData in SentenceDatas)
{
VisualElement lineContainer = CreateSentenceData(sentenceData);
foldout.Add(lineContainer);
}
RefreshExpandedState();
}
// 创建句子数据
private VisualElement CreateSentenceData(object userData)
{
// 获取句子数据
SentenceData sentenceData = (SentenceData)userData;
// 创建行容器
VisualElement lineContainer = new();
lineContainer.userData = userData;
// 创建句子输入框
TextField tfdSentence = ElementUtility.CreateTextArea(sentenceData.Text, null, callback =>
{
sentenceData.Text = callback.newValue;
});
// 创建删除按钮
Button btnDelete = ElementUtility.CreateButton("X", () =>
{
// 检测列表数量,至少保留一个
if (SentenceDatas.Count == 1)
{
Debug.LogWarning("需至少保留一条句子");
return;
}
// 从数据列表移除
SentenceDatas.Remove(sentenceData);
// 从UI移除
foldout.Remove(lineContainer);
});
// 放置UI元素
lineContainer.Add(tfdSentence);
lineContainer.Add(btnDelete);
return lineContainer;
}
}
}
创建分支节点类
- 继续创建C#文件,命名为
BranchNode
- 打开BranchNode.cs,将内容修改如下:
C#
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace E.Story
{
// 分支节点
public class BranchNode : SingleInMultiOutNode
{
public override void Init(StoryGraphView graphView, string title, Vector2 position)
{
base.Init(graphView, title, position);
// 重设属性默认值
Type = NodeType.Branch;
// 清除并添加默认选项
ChoiceDatas.Clear();
ChoiceDatas.Add(new("选项文本一"));
ChoiceDatas.Add(new("选项文本二"));
}
protected override void DrawOutputContainer()
{
// 遍历选项视图列表,创建对应端口
for (int i = 0; i < ChoiceDatas.Count; i++)
{
ChoiceData choiceData = ChoiceDatas[i];
output = this.CreatePort(choiceData.Text);
output.userData = choiceData;
outputContainer.Add(output);
}
}
protected override void DrawExtensionContainer()
{
// 创建自定义容器
customDataContainer = new();
// 创建折叠框
foldout = ElementUtility.CreateFoldout("节点内容");
// 创建添加按钮
Button btnAdd = ElementUtility.CreateButton("添加选项", () =>
{
ChoiceData choiceData = new("选项文本");
ChoiceDatas.Add(choiceData);
VisualElement lineContainer = CreateChoiceData(choiceData);
foldout.Add(lineContainer);
OnAddChoiceData(choiceData);
});
// 放置UI元素
foldout.Add(btnAdd);
customDataContainer.Add(foldout);
extensionContainer.Add(customDataContainer);
// 遍历列表并创建选项条目
foreach (ChoiceData choiceData in ChoiceDatas)
{
VisualElement lineContainer = CreateChoiceData(choiceData);
foldout.Add(lineContainer);
}
RefreshExpandedState();
}
// 创建选项数据UI
private VisualElement CreateChoiceData(object userData)
{
// 获取选项数据
ChoiceData choiceData = (ChoiceData)userData;
// 创建选项容器
VisualElement choiceContainer = new();
// 创建行容器
VisualElement lineContainer = new();
lineContainer.userData = userData;
// 创建条件列表容器
VisualElement conditionsContainer = new();
// 创建句子输入框
TextField tfdChoice = ElementUtility.CreateTextArea(choiceData.Text, null, callback =>
{
choiceData.Text = callback.newValue;
OnEditChoiceText(choiceData);
});
// 创建删除选项按钮
Button btnDelete = ElementUtility.CreateButton("X", () =>
{
// 检测列表数量,至少保留一个
if (ChoiceDatas.Count == 1)
{
Debug.LogWarning("需至少保留一条选项");
return;
}
// 从数据列表移除
ChoiceDatas.Remove(choiceData);
// 从UI移除
foldout.Remove(choiceContainer);
OnRemoveChoiceData(choiceData);
});
// 放置UI元素
lineContainer.Add(tfdChoice);
lineContainer.Add(btnDelete);
choiceContainer.Add(lineContainer);
choiceContainer.Add(conditionsContainer);
return choiceContainer;
}
// 当编辑选项文本时
private void OnEditChoiceText(ChoiceData choiceData)
{
// 遍历获取端口元素
foreach (Port port in outputContainer.Children())
{
if (port.userData == choiceData)
{
port.portName = choiceData.Text;
break;
}
}
}
// 当添加选项数据时
private void OnAddChoiceData(ChoiceData choiceData)
{
// 更新端口信息
// 创建新端口
Port newPort = this.CreatePort(choiceData.Text);
newPort.userData = choiceData;
outputContainer.Add(newPort);
}
// 当删除选项数据时
private void OnRemoveChoiceData(ChoiceData choiceData)
{
Port portToRemove = null;
// 遍历获取端口元素
foreach (Port port in outputContainer.Children())
{
if (port.userData == choiceData)
{
portToRemove = port;
break;
}
}
// 删除多余端口
outputContainer.Remove(portToRemove);
}
}
}
创建开始节点类
- 继续创建C#文件,命名为
StartNode
- 打开StartNode.cs,将内容修改如下:
C#
using UnityEngine;
namespace E.Story
{
// 开始节点
public class StartNode : ZeroInSingleOutNode
{
public override void Init(StoryGraphView graphView, string title, Vector2 position)
{
base.Init(graphView, title, position);
// 重设属性默认值
Type = NodeType.Start;
}
}
}
创建结束节点类
- 继续创建C#文件,命名为
EndNode
- 打开EndNode.cs,将内容修改如下:
C#
using UnityEngine;
namespace E.Story
{
// 结束节点
public class EndNode : SingleInZeroOutNode
{
public override void Init(StoryGraphView graphView, string title, Vector2 position)
{
base.Init(graphView, title, position);
// 重设属性默认值
Type = NodeType.End;
}
}
}
扩展上下文菜单
打开StoryGraphView.cs,修改以下方法:
C#
// 构建上下文菜单
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
/* ... 此处代码已省略 ... */
evt.menu.AppendAction("添加对话节点", action =>
{
CreateNode("对话节点", NodeType.Dialogue, Vector2.zero);
});
evt.menu.AppendAction("添加分支节点", action =>
{
CreateNode("分支节点", NodeType.Branch, Vector2.zero);
});
evt.menu.AppendAction("添加开始节点", action =>
{
CreateNode("开始节点", NodeType.Start, Vector2.zero);
});
evt.menu.AppendAction("添加结束节点", action =>
{
CreateNode("结束节点", NodeType.End, Vector2.zero);
});
}
测试效果
最终窗口效果如下:

相关链接
- 完整代码:https://gitee.com/helloestar/e-story/wikis/DevelopNotes/05
- 视频版本:制作中……