E Story 故事编辑器开发笔记 #5 创建实用派生节点

添加节点类型

打开NodeType.cs,添加四个节点类型:

C#
namespace E.Story
{
    public enum NodeType
    {
        /* ... 此处代码已省略 ... */

        // 开始
        Start = 21,
        // 结束
        End = 41,
        // 对话
        Dialogue = 51,
        // 分支
        Branch = 61
    }
}

创建句子数据类

  1. 打开Data文件夹,创建C#文件,命名为SentenceData
  2. 打开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;
        }
    }
}

创建实用派生节点

创建对话节点类

  1. 打开UI Node文件夹,创建C#文件,命名为DialogueNode
  2. 打开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;
        }
    }
}

创建分支节点类

  1. 继续创建C#文件,命名为BranchNode
  2. 打开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);
        }
    }
}

创建开始节点类

  1. 继续创建C#文件,命名为StartNode
  2. 打开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;
        }
    }
}

创建结束节点类

  1. 继续创建C#文件,命名为EndNode
  2. 打开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);
    });
}

测试效果

最终窗口效果如下:

相关链接

留下评论