目录
创建数据类
创建节点数据类
- 打开Data文件夹,创建C#文件,命名为
NodeData
- 打开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; }
}
}
创建分组数据类
- 继续创建C#文件,命名为
GroupData
- 打开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; }
}
}
创建故事数据类
- 继续创建C#文件,命名为
StoryDataSO
- 打开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();
}
}
}
创建实用类
创建输入输出实用类
- 打开Editor/Scripts/Utility文件夹,创建C#文件,命名为
IOUtility
- 打开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();
}
}
}
创建数据处理实用类
- 继续创建C#文件,命名为
TextUtility
- 打开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);
}
}
}
创建数据处理实用类
- 打开Runtime/Scripts/Utility文件夹,创建C#文件,命名为
DataUtility
- 打开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;
}
}
}
从视图获取数据
- 打开ChoiceData.cs,新增一个构造器:
C#
public ChoiceData(string text, string nextNodeID)
{
this.text = text;
this.nextNodeID = nextNodeID;
}
- 打开BaseGroup.cs,新增
GetGroupData
方法:
C#
// 获取分组数据
public GroupData GetGroupData()
{
GroupData groupData = new()
{
GUID = ID,
Title = title,
Position = GetPosition().position
};
return groupData;
}
- 打开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;
}
- 打开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;
}
}
将数据写入硬盘
- 打开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;
- 修改
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;
}
- 新增以下方法:
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);
}
}
测试效果
最终窗口效果如下:

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