E Story 故事编辑器开发笔记 #10 保存故事数据

创建数据类

创建节点数据类

  1. 打开Data文件夹,创建C#文件,命名为NodeData
  2. 打开NodeData.cs,将内容修改如下:
C#
using System;
using System.Collections.Generic;
using UnityEngine;

namespace E.Story
{
    // 节点数据
    [Serializable]
    public class NodeData
    {
        [SerializeField] private string guid;
        [SerializeField] private NodeType type;
        [SerializeField] private Vector2 position;

        [SerializeField] private string title;
        [SerializeField] private string note;
        [SerializeField] private List<ChoiceData> choiceDatas;
        [SerializeField] private string groupID;

        [SerializeField] private string roleName;
        [SerializeField] private List<SentenceData> sentenceDatas;


        // 节点GUID
        public string GUID { get => guid; set => guid = value; }

        // 节点类型
        public NodeType Type { get => type; set => type = value; }

        // 节点坐标
        public Vector2 Position { get => position; set => position = value; }

        // 节点标题
        public string Title { get => title; set => title = value; }

        // 节点文本内容
        public string Note { get => note; set => note = value; }

        // 选项视图列表
        public List<ChoiceData> ChoiceDatas { get => choiceDatas; set => choiceDatas = value; }

        // 所属分组GUID
        public string GroupID { get => groupID; set => groupID = value; }

        // 节点角色名称
        public string RoleName { get => roleName; set => roleName = value; }

        // 句子列表
        public List<SentenceData> SentenceDatas { get => sentenceDatas; set => sentenceDatas = value; }
    }
}

创建分组数据类

  1. 继续创建C#文件,命名为GroupData
  2. 打开GroupData.cs,将内容修改如下:
C#
using System;
using UnityEngine;

namespace E.Story
{
    // 分组数据
    [Serializable]
    public class GroupData
    {
        [SerializeField] private string title;
        [SerializeField] private string guid;
        [SerializeField] private Vector2 position;

        // 分组标题
        public string Title { get => title; set => title = value; }

        // 分组GUID
        public string GUID { get => guid; set => guid = value; }

        // 分组坐标
        public Vector2 Position { get => position; set => position = value; }
    }
}

创建故事数据类

  1. 继续创建C#文件,命名为StoryDataSO
  2. 打开StoryDataSO.cs,将内容修改如下:
C#
using System.Collections.Generic;
using UnityEngine;

namespace E.Story
{
    // 故事数据
    public class StoryDataSO : ScriptableObject
    {
        [SerializeField] private string fileName;
        [SerializeField] private List<GroupData> groupDatas;
        [SerializeField] private List<NodeData> nodeDatas;

        // 文件名称
        public string FileName { get => fileName; set => fileName = value; }

        // 分组数据列表
        public List<GroupData> GroupDatas { get => groupDatas; set => groupDatas = value; }

        // 节点数据列表
        public List<NodeData> NodeDatas { get => nodeDatas; set => nodeDatas = value; }
        
        // 初始化
        public void Init(string fileName)
        {
            this.fileName = fileName;
            groupDatas = new();
            nodeDatas = new();
        }
    }
}

创建实用类

创建输入输出实用类

  1. 打开Editor/Scripts/Utility文件夹,创建C#文件,命名为IOUtility
  2. 打开IOUtility.cs,将内容修改如下:
C#
using UnityEditor;
using UnityEngine;

namespace E.Story
{
    // 输入输出实用类
    public static class IOUtility
    {
        // 创建文件夹
        public static void CreateFolder(string path, string folderName)
        {
            // 检测目标文件夹是否存在
            if (AssetDatabase.IsValidFolder($"{path}/{folderName}"))
            {
                return;
            }

            AssetDatabase.CreateFolder(path, folderName);
        }

        // 删除文件夹
        public static void RemoveFolder(string fullPath)
        {
            FileUtil.DeleteFileOrDirectory($"{fullPath}.meta");
            FileUtil.DeleteFileOrDirectory($"{fullPath}/");
        }

        // 创建资产文件
        public static T CreateAsset<T>(string path, string assetName) where T : ScriptableObject
        {
            string fullPath = $"{path}/{assetName}.asset";

            // 尝试加载目标文件
            T asset = LoadAsset<T>(path, assetName);

            // 若文件不存在,则创建一个
            if (asset == null)
            {
                asset = ScriptableObject.CreateInstance<T>();
                AssetDatabase.CreateAsset(asset, fullPath);
            }

            return asset;
        }

        // 载入资产文件
        public static T LoadAsset<T>(string path, string assetName) where T : ScriptableObject
        {
            string fullPath = $"{path}/{assetName}.asset";
            return AssetDatabase.LoadAssetAtPath<T>(fullPath);
        }

        // 载入资产文件
        public static T LoadAsset<T>(string fullPath) where T : ScriptableObject
        {
            return AssetDatabase.LoadAssetAtPath<T>(fullPath);
        }

        // 删除资产文件
        public static void RemoveAsset(string path, string assetName)
        {
            AssetDatabase.DeleteAsset($"{path}/{assetName}.asset");
        }

        // 将资产文件写入硬盘
        public static void SaveAsset(Object asset)
        {
            EditorUtility.SetDirty(asset);
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
        }
    }
}

创建数据处理实用类

  1. 继续创建C#文件,命名为TextUtility
  2. 打开TextUtility.cs,将内容修改如下:
C#
namespace E.Story
{
    // 文本实用类
    public static class TextUtility
    {
        // 检测是否是空格
        public static bool IsWhitespace(this char character)
        {
            switch (character)
            {
                case '\u0020':
                case '\u00A0':
                case '\u1680':
                case '\u2000':
                case '\u2001':
                case '\u2002':
                case '\u2003':
                case '\u2004':
                case '\u2005':
                case '\u2006':
                case '\u2007':
                case '\u2008':
                case '\u2009':
                case '\u200A':
                case '\u202F':
                case '\u205F':
                case '\u3000':
                case '\u2028':
                case '\u2029':
                case '\u0009':
                case '\u000A':
                case '\u000B':
                case '\u000C':
                case '\u000D':
                case '\u0085':
                    return true;

                default:
                    return false;
            }
        }

        // 检测是否是特殊字符(字母、数字、空格、短横、下划线、句点、小括号之外的字符)
        public static bool IsSpecialCharacter(this char character)
        {
            bool isLetterOrDigit = char.IsLetterOrDigit(character);
            bool isWhitespace = character.IsWhitespace();
            bool isOther = character == '-' || character == '_' || character == '.' || character == '(' || character == ')';

            return !isLetterOrDigit && !isWhitespace && !isOther;
        }

        // 检测是否有空格
        public static bool HasWhitespace(this string text)
        {
            foreach (char c in text)
            {
                // 检测是否是空格
                if (c.IsWhitespace())
                {
                    return true;
                }
            }
            return false;
        }

        // 检测是否有特殊字符
        public static bool HasSpecialCharacter(this string text)
        {
            foreach (char c in text)
            {
                // 检测是否是特殊字符
                if (c.IsSpecialCharacter())
                {
                    return true;
                }
            }
            return false;
        }

        // 移除空格
        public static string RemoveWhitespaces(this string text)
        {
            int textLength = text.Length;
            char[] textCharacters = text.ToCharArray();
            int currentWhitespacelessTextLength = 0;

            // 遍历文本中所有字符
            for (int currentCharacterIndex = 0; currentCharacterIndex < textLength; ++currentCharacterIndex)
            {
                // 获取当前字符
                char currentTextCharacter = textCharacters[currentCharacterIndex];

                // 检测是否是空格
                if (currentTextCharacter.IsWhitespace())
                {
                    continue;
                }

                textCharacters[currentWhitespacelessTextLength++] = currentTextCharacter;
            }

            return new string(textCharacters, 0, currentWhitespacelessTextLength);
        }

        // 移除特殊字符
        public static string RemoveSpecialCharacters(this string text)
        {
            int textLength = text.Length;
            char[] textCharacters = text.ToCharArray();
            int currentWhitespacelessTextLength = 0;

            // 遍历文本中所有字符
            for (int currentCharacterIndex = 0; currentCharacterIndex < textLength; ++currentCharacterIndex)
            {
                // 获取当前字符
                char currentTextCharacter = textCharacters[currentCharacterIndex];

                // 检测是否是特殊字符
                if (currentTextCharacter.IsSpecialCharacter())
                {
                    continue;
                }

                textCharacters[currentWhitespacelessTextLength++] = currentTextCharacter;
            }

            return new string(textCharacters, 0, currentWhitespacelessTextLength);
        }
    }
}

创建数据处理实用类

  1. 打开Runtime/Scripts/Utility文件夹,创建C#文件,命名为DataUtility
  2. 打开DataUtility.cs,将内容修改如下:
C#
using System.Collections.Generic;

namespace E.Story
{
    // 数据处理实用类
    public static class DataUtility
    {
        // 克隆选择数据列表
        public static List<ChoiceData> CloneChoiceDatas(List<ChoiceData> oldDatas)
        {
            List<ChoiceData> newDatas = new();
            foreach (ChoiceData data in oldDatas)
            {
                ChoiceData newData = new(data.Text, data.NextNodeID);
                newDatas.Add(newData);
            }

            return newDatas;
        }

        // 克隆句子数据列表
        public static List<SentenceData> CloneSentenceDatas(List<SentenceData> oldDatas)
        {
            if (oldDatas == null)
            {
                return null;
            }

            List<SentenceData> newDatas = new();
            foreach (SentenceData data in oldDatas)
            {
                SentenceData newData = new(data.Text);
                newDatas.Add(newData);
            }

            return newDatas;
        }
    }
}

从视图获取数据

  1. 打开ChoiceData.cs,新增一个构造器:
C#
public ChoiceData(string text, string nextNodeID)
{
    this.text = text;
    this.nextNodeID = nextNodeID;
}
  1. 打开BaseGroup.cs,新增GetGroupData方法:
C#
// 获取分组数据
public GroupData GetGroupData()
{
    GroupData groupData = new()
    {
        GUID = ID,
        Title = title,
        Position = GetPosition().position
    };

    return groupData;
}
  1. 打开BaseNode.cs,新增Group属性和GetNodeData方法:
C#
// 所属分组
public BaseGroup Group { get; set; }

// 获取节点数据
public virtual NodeData GetNodeData()
{
    List<ChoiceData> choiceDatas = DataUtility.CloneChoiceDatas(ChoiceDatas);

    NodeData nodeData = new()
    {
        GUID = GUID,
        Type = Type,
        Position = GetPosition().position,
        Title = Title,
        Note = Note,
        ChoiceDatas = choiceDatas,
        GroupID = Group?.ID,
    };

    return nodeData;
}
  1. 打开StoryGraphView.cs,新增Groups属性和Nodes属性:
C#
public List<BaseGroup> Groups
{
    get
    {
        // 遍历获取元素
        List<BaseGroup> groups = new();
        graphElements.ForEach(element =>
        {
            if (element is BaseGroup group)
            {
                groups.Add(group);
                return;
            }
        });
        return groups;
    }
}

public List<BaseNode> Nodes
{
    get
    {
        // 遍历获取元素
        List<BaseNode> baseNodes = new();
        nodes.ForEach(element =>
        {
            if (element is BaseNode node)
            {
                baseNodes.Add(node);
                return;
            }
        });
        return baseNodes;
    }
}

将数据写入硬盘

  1. 打开StoryEditorWindow.cs,新增以下字段:
C#
// 文件夹路径
private readonly string eStoryFolderPath = "Assets/E Tool/E Story";
private readonly string exampleFolderPath = "Assets/E Tool/E Story/Example";
private readonly string exampleFolderName = "Example";
private readonly string storyDatasFolderPath = "Assets/E Tool/E Story/Example/Story Datas";
private readonly string storyDatasFolderName = "Story Datas";

// 临时变量
private string fileName;
private StoryDataSO storyData;
  1. 修改AddToolbar方法:
C#
// 添加工具栏
private void AddToolbar()
{
    // 创建UI元素
    tfdFileName = ElementUtility.CreateTextField(defaultFileName, "当前故事", callback =>
    {
        if (callback.newValue.HasSpecialCharacter())
        {
            string temp = callback.newValue.RemoveSpecialCharacters();
            tfdFileName.value = temp;
            fileName = temp;
        }
        else
        {
            fileName = callback.newValue;
        }
    });
    btnSave = ElementUtility.CreateButton("保存", () => SaveStory());
    
    /* ... 此处代码已省略 ... */

    // 初始化文件名
    fileName = defaultFileName;
}
  1. 新增以下方法:
C#
// 保存故事
private void SaveStory()
{
    // 检测文件名是否为空
    if (string.IsNullOrEmpty(fileName))
    {
        string str = "故事名称不能为空。";
        EditorUtility.DisplayDialog("警告", str, "明白");
        return;
    }

    // 创建存档文件夹
    IOUtility.CreateFolder(eStoryFolderPath, exampleFolderName);
    IOUtility.CreateFolder(exampleFolderPath, storyDatasFolderName);

    // 创建图形文件
    storyData = IOUtility.CreateAsset<StoryDataSO>(storyDatasFolderPath, $"{fileName}");
    storyData.Init(fileName);

    // 保存数据
    SaveDatas();

    // 提示消息
    string message = $"故事已保存";
    ShowNotification(new GUIContent(message));
}

// 保存数据
private void SaveDatas()
{
    SaveGroupDatas(graphView.Groups);
    SaveNodeDatas(graphView.Nodes);

    // 写入硬盘
    IOUtility.SaveAsset(storyData);
}

// 保存分组数据
private void SaveGroupDatas(List<BaseGroup> groups)
{
    // 遍历分组列表
    foreach (BaseGroup group in groups)
    {
        // 创建分组数据
        GroupData groupData = group.GetGroupData();
        // 加入列表
        storyData.GroupDatas.Add(groupData);
    }
}

// 保存节点数据
private void SaveNodeDatas(List<BaseNode> nodes)
{
    // 遍历节点列表
    foreach (BaseNode node in nodes)
    {
        // 创建节点数据
        NodeData nodeData = node.GetNodeData();
        // 加入列表
        storyData.NodeDatas.Add(nodeData);
    }
}

测试效果

最终窗口效果如下:

相关链接

留下评论