# Unity故事编辑器 **Repository Path**: PXY2333/unity-story-editor ## Basic Information - **Project Name**: Unity故事编辑器 - **Description**: 基于UItookit和GraphView,开发的一款可视化故事编辑器,可以快速地完成对话编辑 - **Primary Language**: C# - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 6 - **Forks**: 0 - **Created**: 2025-12-17 - **Last Updated**: 2026-02-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Unity 故事图编辑器 (Story Graph Editor) 一个基于 Unity 的可视化故事/对话系统编辑器,支持单人对话、多人对话、事件处理、分支选择等功能。提供完整的可视化编辑和运行时播放功能,适用于 galgame 或带剧情对话的中小型游戏。当前包版本:0.9.3(见 `Assets/unity-story-editor/package.json`)。 ## 特性概览 - **可视化节点编辑**:基于 Unity GraphView 的可视化编辑界面,支持拖拽布局和连接 - **多种节点类型**:对话、多人对话、分支、事件、跳转、章节跳转、开始、结束节点 - **角色管理系统**:角色注册、头像配置、表情管理、全局角色注册表 - **事件驱动架构**:灵活的事件监听和触发机制,支持自定义事件 - **运行时播放器**:完整的对话播放和用户交互系统,支持故事切换和章节跳转 - **UI 自动绑定**:基于属性的 UI 组件自动绑定,支持自定义UI渲染 - **导入导出工具**:支持对话和角色的CSV/XLSX导入导出,包含表情数据 - **对话转图工具**:TXT文件转结构化CSV/XLSX,支持章节分割和角色匹配 - **自动布局**:节点图按ID顺序自动排列,避免堆叠 - **跨章节功能**:支持跨章节复制粘贴、章节跳转、共享角色数据 - **统一API配置**:集中管理所有AI工具的API密钥,支持DeepSeek、通义千问和火山引擎 - **AI 写作工具**:支持 DeepSeek / 通义千问,按统一 TXT 规范生成章节/整书文本,支持续写功能 - **AI 绘制角色**:使用AI生成角色头像和表情,支持提示词生成和火山引擎图像生成 ## 目录结构 ``` StoryGraph/ ├── Editor/ # 编辑器模块 │ ├── Icon/ # 编辑器图标 │ ├── Scripts/ │ │ ├── AITools/ # AI工具(API配置、写作、绘图) │ │ ├── Inspector/ # Inspector 扩展 │ │ ├── Panels/ # 配置面板 │ │ ├── UI/ # 编辑器主界面 │ │ ├── UINode/ # 节点 UI 类 │ │ └── Utility/ # 工具类 │ ├── StyleSheet/ # 样式文件 │ └── StoryGraph.Editor.asmdef # 编辑器程序集定义 ├── RunTime/ # 运行时模块 │ └── Scripts/ │ ├── Data/ # 数据类 (节点数据、角色数据等) │ ├── SO/ # ScriptableObject 类 │ └── Utility/ # 运行时工具类 └── StorySpeaker/ # 运行时播放器组件 ├── Editor/ # 播放器编辑器扩展 └── Utility/ # 播放器工具类 ``` ## 编辑器模块 编辑器模块提供了完整的可视化故事编辑环境。 ### 主要功能 #### 1. 故事编辑器窗口 - **可视化节点图**:拖拽式节点布局和连接 - **多种节点类型**: - **开始节点**:故事入口点 - **对话节点**:单人对话,支持多句话和表情切换 - **多人对话节点**:多角色轮流对话 - **分支节点**:提供多个选项供玩家选择 - **事件节点**:触发自定义游戏事件 - **跳转节点**:跳转到指定节点 - **章节跳转节点**:跳转到指定章节并运行其开始节点 - **结束节点**:故事结束点 #### 2. 角色管理 - **角色注册面板**:注册和管理故事角色 - **头像配置**:为角色配置头像图片 - **表情系统**:为角色配置多种表情,可在对话中切换 #### 3. 数据管理 - **故事数据保存**:将节点图保存为 ScriptableObject - **自动保存**:支持定时自动保存功能(默认每 5 秒),可手动开启/关闭 - **导入/导出**:支持故事和角色的导入导出(CSV/XLSX格式) - **撤销/重做**:完整的操作历史记录 - **全局角色匹配**:导入时可挂载全局角色配置SO,自动匹配角色数据 #### 4. 编辑器特性 - **复制粘贴**:节点和连接线的复制粘贴,支持跨章节复制粘贴 - **跨章节复制粘贴**:可以在不同章节(GraphView)之间复制粘贴节点 - **连接关系保持**:复制粘贴时自动保持节点间的连接关系 - **相对位置保持**:粘贴时保持节点的相对位置关系 - **NumID 自动重算**:粘贴到新章节时自动生成新的唯一 NumID - **分组管理**:将相关节点分组管理 - **搜索功能**:快速查找节点和角色(按空格键打开搜索树) - **自动布局**:节点图按ID顺序自动排列,避免堆叠 - **未保存提示**:窗口标题显示未保存更改标记(*) - **智能节点布局**:打开节点图时自动按ID排序布局 #### 5. 故事书和章节管理 - **故事书系统**:使用 `StoryBookSO` 管理多个章节 - **章节列表**:可视化章节列表,支持创建、打开、导入、删除章节 - **章节切换**:快速在不同章节间切换编辑 - **全局角色注册表**:故事书可关联全局角色注册表,所有章节共享角色数据 - **章节删除**:删除章节时自动删除对应的 `StoryDataSO` 资产文件 ### 使用方式 #### 基础使用流程 1. **打开编辑器**:在 Unity 菜单中选择 `Tools/故事工具/故事编辑器` 2. **选择或创建故事书**: - 首次打开会显示故事书选择面板 - 可以选择现有的 `StoryBookSO` 或创建新的故事书 - 创建故事书:输入名称后点击"创建新故事书" 3. **管理章节**: - 在章节列表中点击"新建章节"创建新章节 - 点击章节名称打开编辑 - 点击"导入章节"导入已有的 `StoryDataSO` 文件 - 点击"删除章节"删除章节(会同时删除资产文件) 4. **编辑故事**: - 添加节点:右键点击画布选择节点类型,或按空格键打开搜索树 - 连接节点:拖动节点的端口进行连接 - 配置节点:选中节点后在 Inspector 中配置属性 5. **保存故事**: - 手动保存:点击工具栏的"保存"按钮 - 自动保存:勾选工具栏的"自动保存"选项(每 5 秒自动保存一次) #### 故事书系统 - **创建故事书**:在 Project 窗口右键创建 `StoryGraph/StoryBookSO` - **关联全局角色注册表**:在故事书的 Inspector 中设置 `GlobalRoleRegistry`,所有章节将共享该注册表中的角色 - **章节管理**:故事书管理多个章节(`StoryDataSO`),每个章节是独立的故事数据 ## 运行时模块 运行时模块负责在游戏中播放编辑好的故事,提供完整的对话播放和用户交互功能。 ### 核心组件 #### 1. StorySpeaker 主要的运行时播放器组件,继承自 `StorySpeakerBase`,负责: - 故事数据的加载和解析 - 节点逻辑的处理 - UI 更新和用户输入处理 - 事件触发和监听 #### 2. StorySpeakerBase 播放器基类,实现 `IStoryProcessor` 接口,提供: - 故事处理状态管理 - 节点跳转逻辑 - 已完成节点跟踪 - UI 管理器集成 - 故事切换功能(支持按名称、按引用切换) #### 3. StoryUIManager UI 管理器,负责: - 自动绑定标记了属性的 UI 组件 - 更新角色名、对话文本、头像图片 - 管理对话面板的显示/隐藏 #### 4. StoryEventManager 事件管理器,提供: - 自定义事件的注册和触发 - 故事节点事件的监听(开始、结束、对话) - 基于故事索引的精确事件控制 ### 运行时功能 #### 1. 对话播放 支持单人和多人对话的逐句播放,自动更新 UI 并等待用户点击继续。 #### 2. 分支选择 当遇到分支节点时,显示选项按钮供玩家选择,根据选择跳转到不同分支。 #### 3. 事件触发 事件节点可以触发自定义游戏事件,其他系统可以通过事件监听器响应。 #### 4. 节点跳转 跳转节点允许在故事中实现循环、回退或条件跳转。 #### 5. 进度跟踪 系统跟踪已完成的节点,可用于实现存档/读档功能。 #### 6. 故事切换 支持在运行时动态切换不同故事,可以通过名称、引用或索引切换故事,支持动态添加和移除故事数据。 #### 7. 章节切换 当使用 `StoryBookSO` 时,支持在运行时切换不同章节: - 使用 `ChangeChapter(int chapterIndex)` 按索引切换章节 - 使用 `ChangeChapterByName(string chapterName)` 按名称切换章节 - 章节跳转节点自动处理章节切换,始终落到目标章节的 Start 节点 ### AI 写作与导入导出格式 #### AI写作工具 - **访问方式**: - 通过故事编辑器:点击工具栏"工具面板 ▼" > "AI写故事工具" - 通过菜单:`Tools > 故事工具 > AI写故事工具` - **支持的AI提供商**: - DeepSeek Chat API - 阿里云通义千问API - **写作模式**: - 章节模式:生成单个故事章节 - 完整故事模式:生成包含多个章节的完整故事 - 续写模式:续写已有的故事文件(支持 .txt / .csv / .xlsx 格式) - **续写功能**: - 支持文件格式:TXT、CSV、XLSX - 自动读取并解析文件内容 - 可配置续写字数 - 可添加额外的续写提示词 - 保持原文风格和格式一致 - **配置要求**:需要配置对应的API密钥 #### AI绘制角色工具 - **访问方式**: - 通过故事编辑器:点击工具栏"工具面板 ▼" > "AI绘制角色" - 通过菜单:`Tools > 故事工具 > AI绘制角色` - **主要功能**: - 挂载全局角色注册表,选择角色和表情 - 使用AI生成绘画提示词(正向和负向) - 使用火山引擎API生成角色图像 - 直接保存为角色头像或表情图片 - **支持的API**: - 文本生成:DeepSeek / 通义千问(用于生成提示词) - 图像生成:火山引擎视觉API - **配置要求**: - 文本模型API密钥(DeepSeek 或 通义千问) - 火山引擎API密钥 - 全局角色注册表SO #### 统一文本格式 - AI 写作输出及 TXT/CSV/XLSX 转换统一格式: - 对话:`角色名 "对话内容"` - 表情:`角色名[表情] "对话内容"` - 心理活动:`角色名 心理活动内容` - 旁白:`旁白 描写内容`(旁白视为角色,不过滤) - 章节标题:`## 章节标题`(章节间空行) #### 导入导出功能 - 导出 TXT 支持整书导出并写入章节标题;导入 TXT 支持 `## 章节标题`、带/不带表情的引号对话 - 导入 CSV/XLSX:先遍历并写入角色与表情到 `GlobalRoleRegistrySO`,再导入节点;自动为对话/多人对话节点选择角色和表情(缺失表情会留空) - 路径记忆:导入、转换工具会记住上次使用的文件路径 - 调试:对话转节点流程输出 Debug 日志辅助排查 ### 代码示例 #### 示例 1:基本 StorySpeaker 配置 ```csharp using StoryGraph; using UnityEngine; using UnityEngine.UI; using TMPro; public class MyStoryController : MonoBehaviour { [SerializeField] private StorySpeaker storySpeaker; [SerializeField] private TMP_Text roleNameText; [SerializeField] private TMP_Text dialogueText; [SerializeField] private Image avatarImage; private void Start() { // StorySpeaker 会自动绑定标记了属性的 UI 组件 // 确保在 Inspector 中为 storySpeaker 分配故事数据 } } ``` #### 示例 2:事件监听 ```csharp using StoryGraph; using UnityEngine; public class EventListenerExample : MonoBehaviour { private void Start() { // 注册事件监听器 StoryEventManager.Instance.RegisterEventListeners(this); } // 监听指定名称的事件 [Hear("玩家获得物品")] private void OnPlayerGetItem() { Debug.Log("玩家获得了物品!"); // 执行相关游戏逻辑 } // 监听指定节点 NumID 的事件 [Hear(1001)] private void OnNode1001Event() { Debug.Log("触发了节点 1001 的事件"); } // 监听指定故事中指定名称的事件 [Hear(0, "故事0特定事件")] private void OnStory0SpecificEvent() { Debug.Log("故事0中触发了特定名称的事件"); } // 监听指定故事中指定节点 NumID 的事件 [Hear(0, 2001)] private void OnStory0Node2001Event() { Debug.Log("故事0中触发了节点2001的事件"); } // 监听指定故事的开始节点事件 [HearStart(0)] // 监听索引为 0 的故事的开始事件 private void OnStory0Start() { Debug.Log("故事 0 开始了!"); } // 监听指定故事的结束节点事件 [HearEnd(0, 999)] // 监听故事 0 中 NumID 为 999 的结束节点 private void OnStory0End() { Debug.Log("故事 0 结束了!"); } // 监听对话事件 [HearDialogue(0, 101, 1)] // 监听故事 0,节点 101,第 1 句话(对话节点没有回合概念,回合索引固定为0,句子索引从1开始) private void OnDialoguePlayed() { Debug.Log("播放了特定的对话内容"); } // 监听对话事件范围 [HearDialogueFromTo(0, 101, 1, 3)] // 监听故事 0,节点 101,从第 1 句到第 3 句 private void OnDialogueRangePlayed() { Debug.Log("播放了对话范围的内容"); } // 监听多人对话事件范围 [HearMutiDialogueFromTo(0, 102, 1, 1, 2, 3)] // 监听故事 0,节点 102,从第 1 轮第 1 句到第 2 轮第 3 句 private void OnMultiDialogueRangePlayed() { Debug.Log("播放了多人对话范围的内容"); } // 同一个方法监听多个事件(支持多个属性标记) [Hear("玩家获得物品")] [Hear("玩家等级提升")] [Hear(3001)] // 同时监听节点 3001 的事件 private void OnMultipleEvents() { Debug.Log("多个事件中的一个被触发!"); } // 同一个方法监听多个故事开始事件 [HearStart(0)] [HearStart(1)] [HearStart(2)] private void OnMultipleStoryStarts() { Debug.Log("某个故事开始了!"); } } ``` #### 示例 3:自定义 UI 绑定 ```csharp using StoryGraph; using UnityEngine; using TMPro; using UnityEngine.UI; public class CustomStoryUI : StorySpeaker { // 使用属性标记 UI 组件,StoryUIManager 会自动绑定 [RoleNameText] public TMP_Text customRoleName; [DialogueText] public TMP_Text customDialogue; [AvatarImage] public Image customAvatar; // 自定义 UI 元素 [UIElement("Background")] public Image dialogueBackground; [UIElement("ContinueButton")] public Button continueButton; protected override void Start() { base.Start(); // 自定义初始化逻辑 if (dialogueBackground != null) { dialogueBackground.color = new Color(0.1f, 0.1f, 0.1f, 0.8f); } } } ``` #### 示例 4:动态故事控制 ```csharp using StoryGraph; using StoryGraph.Data; using StoryGraph.SO; using UnityEngine; public class DynamicStoryControl : MonoBehaviour { [SerializeField] private StorySpeaker storySpeaker; [SerializeField] private StoryDataSO dynamicStoryData; public void LoadNewStory(StoryDataSO storyData) { // 方法一:动态添加并切换到新故事(推荐) // 1. 添加故事数据到列表 storySpeaker.AddStoryData(storyData); // 2. 切换到新故事 bool success = storySpeaker.ChangeStoryByReference(storyData); if (success) { Debug.Log($"已加载并切换到故事: {storyData.Name}"); } // 方法二:旧方法(清除列表并添加新故事) // storySpeaker.storyDataSOs.Clear(); // storySpeaker.storyDataSOs.Add(storyData); // storySpeaker.StoryIndex = 0; // 注意:StartPlay()是protected方法,无法直接调用 // 应使用 storySpeaker.GetNext(); 来开始播放 } public void JumpToNode(int nodeNumID) { // 跳转到指定节点 if (storySpeaker.currentData != null) { // 创建跳转节点数据 var jumpNode = new JumpNodeData { targetNodeNumID = nodeNumID }; // 执行跳转 storySpeaker.currentData = jumpNode; storySpeaker.GetNext(); } } public bool CheckNodeCompleted(int nodeNumID) { // 检查节点是否已完成 return storySpeaker.IsNodeCompleted(nodeNumID); } } ``` #### 示例 5:分支选项处理 ```csharp using StoryGraph; using StoryGraph.Data; using UnityEngine; using UnityEngine.UI; using TMPro; using System.Collections.Generic; public class BranchOptionHandler : MonoBehaviour { [SerializeField] private StorySpeaker storySpeaker; [SerializeField] private GameObject optionPanel; [SerializeField] private Button[] optionButtons; [SerializeField] private TMP_Text[] optionTexts; private BranchNodeData currentBranchNode; private void Start() { // 监听分支节点事件 StoryEventManager.Instance.RegisterEventListeners(this); // 初始化选项按钮 for (int i = 0; i < optionButtons.Length; i++) { int index = i; // 闭包捕获 optionButtons[i].onClick.AddListener(() => OnOptionSelected(index)); optionButtons[i].gameObject.SetActive(false); } optionPanel.SetActive(false); } [HearEnd(-1, 200)] // 监听所有故事中NumID为200的分支节点结束事件 private void OnBranchNodeTriggered() { if (storySpeaker.currentData is BranchNodeData branchData) { currentBranchNode = branchData; ShowOptions(branchData.choiceDatas); } } private void ShowOptions(List choices) { optionPanel.SetActive(true); // 显示可用的选项 for (int i = 0; i < optionButtons.Length; i++) { if (i < choices.Count) { optionButtons[i].gameObject.SetActive(true); optionTexts[i].text = choices[i].text; } else { optionButtons[i].gameObject.SetActive(false); } } } private void OnOptionSelected(int optionIndex) { if (currentBranchNode != null && optionIndex < currentBranchNode.choiceDatas.Count) { // 根据选择跳转到对应的下一个节点 int targetNodeID = currentBranchNode.choiceDatas[optionIndex].targetNodeNumID; // 隐藏选项面板 optionPanel.SetActive(false); // 执行跳转 var jumpNode = new JumpNodeData { targetNodeNumID = targetNodeID }; storySpeaker.currentData = jumpNode; storySpeaker.GetNext(); } } } ``` #### 示例 6:故事切换功能 ```csharp using StoryGraph; using StoryGraph.SO; using UnityEngine; public class StorySwitcherExample : MonoBehaviour { [SerializeField] private StorySpeaker storySpeaker; [SerializeField] private StoryDataSO newStoryData; public void SwitchToStoryByName(string storyName) { // 按名称切换故事 bool success = storySpeaker.ChangeStoryByName(storyName); if (success) { Debug.Log($"成功切换到故事: {storyName}"); } else { Debug.LogError($"切换故事失败: {storyName}"); } } public void SwitchToStoryByReference() { // 按StoryDataSO引用切换故事 bool success = storySpeaker.ChangeStoryByReference(newStoryData); if (success) { Debug.Log($"成功切换到故事: {newStoryData.Name}"); } else { Debug.LogError($"切换故事失败: {newStoryData.Name}"); } } public int GetStoryIndex(string storyName) { // 获取故事索引 int index = storySpeaker.GetStoryIndexByName(storyName); if (index >= 0) { Debug.Log($"故事 '{storyName}' 的索引是: {index}"); } else { Debug.LogError($"未找到故事: {storyName}"); } return index; } public void AddStoryDynamically(StoryDataSO storyData) { // 动态添加故事数据 storySpeaker.AddStoryData(storyData); Debug.Log($"已添加故事: {storyData.Name}"); } public bool RemoveStory(string storyName) { // 按名称移除故事 bool success = storySpeaker.RemoveStoryDataByName(storyName); if (success) { Debug.Log($"已移除故事: {storyName}"); } else { Debug.LogError($"移除故事失败: {storyName}"); } return success; } public void ListenToStoryChange() { // 监听故事切换事件 storySpeaker.OnStoryChanged += OnStoryChanged; } private void OnStoryChanged(int previousStoryIndex, int newStoryIndex) { Debug.Log($"故事已切换: 从索引 {previousStoryIndex} 切换到 {newStoryIndex}"); // 获取故事名称 if (storySpeaker.storyDataSOs != null && newStoryIndex >= 0 && newStoryIndex < storySpeaker.storyDataSOs.Count) { var storyData = storySpeaker.storyDataSOs[newStoryIndex]; if (storyData != null) { Debug.Log($"当前故事: {storyData.Name}"); } } } } ``` #### 示例 7:章节切换功能 ```csharp using StoryGraph; using StoryGraph.SO; using UnityEngine; public class ChapterSwitcherExample : MonoBehaviour { [SerializeField] private StorySpeaker storySpeaker; [SerializeField] private StoryBookSO storyBook; private void Start() { // 确保 StorySpeaker 关联了 StoryBookSO if (storySpeaker != null && storyBook != null) { // 设置故事书 var speakerBase = storySpeaker as StorySpeakerBase; if (speakerBase != null) { speakerBase.currentStoryBook = storyBook; } } } public void SwitchToChapterByIndex(int chapterIndex) { // 按索引切换章节 var speakerBase = storySpeaker as StorySpeakerBase; if (speakerBase != null) { speakerBase.ChangeChapter(chapterIndex); Debug.Log($"已尝试切换到章节索引: {chapterIndex}"); } } public void SwitchToChapterByName(string chapterName) { // 按名称切换章节 var speakerBase = storySpeaker as StorySpeakerBase; if (speakerBase != null) { speakerBase.ChangeChapterByName(chapterName); Debug.Log($"已尝试切换到章节: {chapterName}"); } } } ``` #### 示例 8:模拟按钮点击 StorySpeaker 提供了公共方法,允许外部脚本模拟玩家点击按钮,实现自动播放、测试或自定义交互逻辑。 ```csharp using StoryGraph; using StoryGraph.Data; using UnityEngine; public class AutoPlayController : MonoBehaviour { [SerializeField] private StorySpeaker storySpeaker; [SerializeField] private float autoPlayDelay = 2f; private bool isAutoPlaying = false; private void Update() { // 按空格键切换自动播放 if (Input.GetKeyDown(KeyCode.Space)) { isAutoPlaying = !isAutoPlaying; } } private void Start() { // 监听对话完成事件,自动继续 if (storySpeaker != null) { StartCoroutine(AutoPlayCoroutine()); } } private System.Collections.IEnumerator AutoPlayCoroutine() { while (true) { if (isAutoPlaying && storySpeaker != null) { // 使用智能方法自动判断节点类型并调用相应的按钮 storySpeaker.SimulateButtonClick(); } yield return new WaitForSeconds(autoPlayDelay); } } // 手动触发按钮点击(推荐方式) public void TriggerButtonClick() { if (storySpeaker != null) { // 智能方法自动判断节点类型 storySpeaker.SimulateButtonClick(); } } // 手动触发继续按钮 public void TriggerNextButton() { if (storySpeaker != null) { storySpeaker.OnNextButtonClick(); } } // 手动触发对话按钮 public void TriggerDialogueButton() { if (storySpeaker != null) { storySpeaker.OnDialogueButtonClick(); } } // 手动选择分支选项 public void SelectBranchOption(int optionIndex) { if (storySpeaker != null) { // 方式一:使用智能方法(推荐) storySpeaker.SimulateButtonClick(optionIndex); // 方式二:直接调用分支方法 // var speakerBase = storySpeaker as StorySpeakerBase; // if (speakerBase != null) // { // speakerBase.CallBranchOption(optionIndex); // } } } } ``` **可用的公共方法:** - `SimulateButtonClick(int branchOptionIndex = -1)`:**推荐使用**,智能按钮点击方法,自动根据当前节点类型调用相应的按钮处理方法 - `OnNextButtonClick()`:模拟点击继续按钮,执行节点跳转逻辑 - `OnDialogueButtonClick()`:模拟点击对话按钮,处理对话播放逻辑 - `CallBranchOption(int optionIndex)`:模拟选择分支选项(选项索引从0开始) **使用场景:** - **自动播放模式**:定时自动触发按钮点击,实现自动播放对话 - **测试工具**:编写测试脚本自动验证故事流程 - **自定义交互**:使用键盘、手柄等输入设备触发按钮操作 - **AI对话系统**:集成AI系统自动选择分支或继续对话 **推荐用法:** ```csharp // 最简单的方式:直接调用智能方法,自动判断节点类型 storySpeaker.SimulateButtonClick(); // 分支节点需要提供选项索引 storySpeaker.SimulateButtonClick(0); // 选择第一个选项 ``` ### 节点类型详解 #### 1. 开始节点 (StartNode) - **功能**:故事入口点,不包含实际内容 - **运行时行为**:触发 `HearStart` 事件,立即跳转到下一个节点 - **使用场景**:每个故事链的起点 #### 2. 对话节点 (DialogueNode) - **功能**:单人对话,支持多句话 - **运行时行为**:逐句显示对话,每句话等待用户点击继续 - **可配置项**:角色、对话内容、每句话的表情 - **事件**:触发 `HearDialogue` 事件 #### 3. 多人对话节点 (MultiDialogueNode) - **功能**:多角色轮流对话 - **运行时行为**:按回合逐句显示对话,自动切换角色 - **可配置项**:多个对话回合,每个回合包含角色和对话内容 - **事件**:触发 `HearMultiDialogue` 事件 #### 4. 分支节点 (BranchNode) - **功能**:提供多个选项供玩家选择 - **运行时行为**:显示选项按钮,根据选择跳转到不同分支 - **可配置项**:选项文本和对应的目标节点 - **事件**:触发自定义分支事件 #### 5. 事件节点 (EventNode) - **功能**:触发自定义游戏事件 - **运行时行为**:触发指定名称的事件,然后继续流程 - **可配置项**:事件名称 - **事件**:触发指定名称的 `Hear` 事件 #### 6. 跳转节点 (JumpNode) - **功能**:跳转到指定节点 - **运行时行为**:直接跳转到目标节点,继续故事 - **可配置项**:目标节点 NumID - **使用场景**:实现循环、回退、条件跳转 #### 7. 章节跳转节点 (ChapterJumpNode) - **功能**:跳转到指定章节并继续执行 - **运行时行为**: - 直接调用 `StorySpeakerBase.ChangeChapter(int)` 切换章节 - 始终落到目标章节的 Start 节点(不再按 NumID 对应) - 自动更新事件监听器并刷新事件阅读器 - 清理旧章节的已完成节点状态,保证重新进入状态正确 - **可配置项**:章节索引(在 `StoryBookSO` 中的索引,内部 0 基,导入/导出支持 1 基显示) - **使用场景**:跨章节流程、多章节游戏 - **注意事项**:需要 `StoryBookSO` 关联;章节索引越界会被忽略并报错日志 #### 8. 结束节点 (EndNode) - **功能**:故事结束点 - **运行时行为**:触发 `HearEnd` 事件,结束故事播放 - **使用场景**:故事链的终点 ### 属性系统 运行时模块提供了多种属性用于标记和配置功能: #### UI 绑定属性 - `[RoleNameText]`:标记角色名称文本组件 - `[DialogueText]`:标记对话文本组件 - `[AvatarImage]`:标记头像图片组件 - `[UIElement("元素名")]`:标记自定义 UI 组件 #### 事件监听属性 以下属性都支持在同一个方法上多次使用(AllowMultiple = true): - `[Hear("事件名")]`:监听所有故事中指定名称的事件 - `[Hear(事件ID)]`:监听所有故事中指定 NumID 的事件 - `[Hear(故事索引, "事件名")]`:监听指定故事中指定名称的事件 - `[Hear(故事索引, 事件ID)]`:监听指定故事中指定 NumID 的事件 - `[HearStart(故事索引)]`:监听故事开始事件 - `[HearEnd(故事索引, 节点ID)]`:监听故事结束事件 - `[HearDialogue(故事索引, 节点ID, 句子索引)]`:监听对话事件(对话节点的回合索引固定为0) - `[HearDialogueFromTo(故事索引, 节点ID, 起始句子索引, 结束句子索引)]`:监听对话事件的范围(从第几句到第几句) - `[HearMultiDialogue(故事索引, 节点ID, 回合索引, 句子索引)]`:监听多人对话事件(指定回合和句子) - `[HearMultiDialogue(故事索引, 节点ID, 回合索引)]`:监听多人对话事件(指定回合的所有句子) - `[HearMutiDialogueFromTo(故事索引, 节点ID, 起始回合索引, 起始句子索引, 结束回合索引, 结束句子索引)]`:监听多人对话事件的范围(从第几轮第几句话到第几轮第几句话) - `[HearUI]`:监听 UI 渲染前事件,获取渲染参数(角色名、对话内容、头像) - `[SetUI(故事索引, 节点ID, 句子索引)]`:修改单人对话的 UI 渲染参数(必须与 `[HearDialogue]` 配合使用) - `[SetUI(故事索引, 节点ID, 回合索引, 句子索引)]`:修改多人对话的 UI 渲染参数(必须与 `[HearMultiDialogue]` 配合使用) **使用示例**: ```csharp // 同一个方法监听多个事件 [Hear("玩家获得物品")] [Hear("玩家等级提升")] [Hear(3001)] private void OnMultipleEvents() { Debug.Log("多个事件中的一个被触发!"); } // 同一个方法监听多个故事开始事件 [HearStart(0)] [HearStart(1)] [HearStart(2)] private void OnMultipleStoryStarts() { Debug.Log("某个故事开始了!"); } ``` ## 安装和使用 ### 系统要求 - **Unity 版本**:2021.3 LTS 或更高版本 - **依赖包**:TextMeshPro (Unity 包管理器中安装) - **操作系统**:Windows/macOS/Linux - **内存要求**:至少 4GB RAM(推荐 8GB+) ### 安装步骤 #### 方法一:Unity Package Manager(推荐) 1. 下载插件包文件(.unitypackage) 2. 在 Unity 中选择 `Assets > Import Package > Custom Package` 3. 选择下载的插件包文件 4. 点击 "Import" 导入所有文件 #### 方法二:手动导入到Assets 1. 下载或克隆插件源码 2. 将 `unity-story-editor` 文件夹复制到 Unity 项目的 `Assets` 目录 3. Unity 会自动编译相关程序集 4. 首次导入可能需要几分钟编译时间 #### 方法三:安装为Unity包(推荐用于团队开发) 1. 下载或克隆插件源码 2. 将 `unity-story-editor` 文件夹复制到 Unity 项目的 `Packages` 目录 3. 或者在 `manifest.json` 中添加包引用: ```json { "dependencies": { "com.pxy2333.storygraph": "file:../relative/path/to/unity-story-editor" } } ``` 4. Unity 会自动识别并加载包 #### 数据路径迁移(重要) 如果您之前使用过旧版本插件,数据文件可能存储在旧路径 `Assets/StoryDatabases` 中。 **自动迁移:** 1. 打开 `Tools/故事工具/路径调试` 2. 点击"从旧路径结构迁移数据"按钮 3. 系统会自动将旧路径中的文件移动到新结构中 **手动迁移:** 如果需要手动迁移,将以下目录中的文件移动到对应的新位置: - `Assets/StoryDatabases/` → `Assets/StoryGraphData/Stories/` - 全局注册表文件 → `Assets/StoryGraphData/Registries/` #### 验证安装 安装完成后,您应该能在 Unity 菜单中看到: - `故事图/故事编辑器` - `Tools/故事工具/AI写故事工具` - `Tools/故事工具/AI绘制角色` - `故事图/事件阅读器` - `故事图/导入工具` - `故事图/导出工具` - `故事图/对话转图工具` - `故事图/路径调试` (用于调试路径解析) #### 安装位置差异说明 插件支持安装在两种位置,各有优缺点: | 特性 | Assets安装 | Packages安装 | | ------------ | ---------------------- | ---------------------- | | **编辑权限** | 可直接编辑 | 只读(推荐) | | **版本控制** | 包含在项目中 | 可独立管理 | | **团队协作** | 所有人都可修改 | 统一版本 | | **更新方式** | 手动替换文件 | 包管理器更新 | | **数据存储** | Assets/StoryGraphData/ | Assets/StoryGraphData/ | **推荐使用场景:** - **Assets安装**:个人项目、原型开发、需要自定义修改 - **Packages安装**:团队项目、正式开发、版本管理严格的项目 #### 数据存储路径说明 插件使用结构化的数据存储路径,便于管理和维护: ``` Assets/StoryGraphData/ ├── Stories/ # 故事数据存储目录 │ ├── *.asset # StoryDataSO 和 StoryBookSO 文件 │ └── *.meta # Unity 元数据文件 ├── Registries/ # 全局注册表存储目录 │ └── *.asset # GlobalRoleRegistrySO 文件 ├── Imported/ # 导入数据缓存目录 │ └── *.xlsx # 临时导入的文件 └── Exported/ # 导出数据存储目录 ├── *.xlsx # 导出的Excel文件 └── *.txt # 导出的文本文件 ``` **路径优势:** - **分类清晰**:不同类型的数据存储在对应目录 - **版本管理友好**:数据文件与插件代码分离 - **团队协作**:避免冲突,支持并行开发 - **备份便利**:按类型进行数据备份 ### 快速开始指南 本插件提供了两种主要的使用模式:**故事书模式**(推荐用于大型项目)和**独立故事模式**(适合简单项目)。 #### ?? 方式一:故事书模式(推荐) 适合需要多个章节、复杂故事线的游戏项目。 ##### 1. 创建项目结构 建议在项目中创建以下文件夹结构: ``` Assets/ ├── StoryGraphData/ # 插件数据存储根目录(自动创建) ├── UI/ # UI预制件文件夹 └── Scenes/ # 场景文件夹 ``` ##### 2. 创建故事书 1. 在 Project 窗口中右键选择 `Create > StoryGraph > StoryBookSO` 2. 命名您的故事书(如:"MainStory") 3. (可选)创建全局角色注册表: - 右键选择 `Create > StoryGraph > GlobalRoleRegistrySO` - 在故事书的 Inspector 中关联该注册表 ##### 3. 设置角色系统 1. 打开角色注册器:`Tools > 故事工具 > 角色注册器` 2. 注册游戏角色,为每个角色配置: - 头像图片 - 动画控制器(可选) - 表情列表 3. 保存角色配置 ##### 4. 创建故事章节 1. 打开故事编辑器:`Tools > 故事工具 > 故事编辑器` 2. 选择刚创建的故事书 3. 在章节面板中点击"新建章节" 4. 为章节命名(如:"第一章-相遇") ##### 5. 编辑故事内容 1. **添加节点**: - 右键点击空白区域选择节点类型 - 或按空格键打开搜索菜单 2. **配置节点**: - 对话节点:选择角色,输入对话内容,选择表情 - 分支节点:设置选项文本和跳转目标 - 事件节点:输入事件名称 3. **连接节点**:拖拽节点间的连接点建立故事流程 ##### 6. 创建运行时播放器 1. 在场景中创建空 GameObject 2. 添加 `StorySpeaker` 组件 3. 配置组件: - **Current Story Book**:拖入故事书SO - **Current Story**:拖入第一个章节的StoryDataSO ##### 7. 设置UI界面 ```csharp // 创建UI结构 Canvas ├── DialoguePanel (背景面板) │ ├── CharacterName (TMP_Text) │ ├── DialogueText (TMP_Text) │ └── CharacterAvatar (Image) ├── ChoicePanel (选项面板) │ └── ChoiceButton (Button) - 复制多个 └── ContinueButton (继续按钮) ``` ##### 8. 绑定UI组件 在 StorySpeaker 的 Inspector 中: - 拖入角色名文本组件到 "Role Name Text" 字段 - 拖入对话文本组件到 "Dialogue Text" 字段 - 拖入头像图片组件到 "Avatar Image" 字段 - 配置选项按钮列表 ##### 9. 测试运行 1. 运行场景 2. StorySpeaker 会自动开始播放故事 3. 测试对话播放、分支选择、事件触发等功能 #### ? 方式二:独立故事模式 适合简单的单章节故事或原型开发。 ##### 1. 创建故事数据 1. 在 Project 窗口中右键选择 `Create > StoryGraph > StoryDataSO` 2. 命名您的故事(如:"DemoStory") ##### 2. 编辑故事 1. 双击 StoryDataSO 文件打开编辑器 2. 或通过菜单 `Tools > 故事工具 > 故事编辑器` 打开,选择故事数据 3. 添加和连接节点 ##### 3. 配置播放器 1. 创建 GameObject 并添加 `StorySpeaker` 组件 2. 将 StoryDataSO 拖入 `Story Data SOs` 列表 3. 设置 `Story Index` 为 0 ##### 4. 设置UI并测试 参考故事书模式的UI设置步骤 ### 高级用法指南 #### ? 数据导入导出工作流 1. **批量导入对话**:使用Excel创建对话表格,通过导入工具批量导入 2. **角色数据管理**:导出角色到XLSX,在Excel中编辑后重新导入 3. **全局角色匹配**:导入时挂载GlobalRoleRegistrySO,自动匹配角色数据 #### ? 角色系统深度使用 - **表情系统**:在角色注册器中为角色添加表情,在对话节点中为每句话选择表情 - **全局角色注册表**:使用`GlobalRoleRegistrySO`在不同故事间共享角色数据 #### ? 自定义开发 ##### 扩展节点类型 1. 创建新节点类继承 `BaseNode` 2. 实现 `ProcessNode` 方法定义运行时行为 3. 在编辑器中注册新节点类型 ##### 自定义UI绑定 ```csharp using StoryGraph; public class CustomUI : StorySpeaker { [RoleNameText] public TextMeshProUGUI customNameText; [DialogueText] public TextMeshProUGUI customDialogueText; [AvatarImage] public Image customAvatar; // 自定义UI逻辑 protected override void Start() { base.Start(); // 自定义初始化 } } ``` ##### 事件系统扩展 ```csharp using StoryGraph; public class GameEventHandler : MonoBehaviour { void Start() { StoryEventManager.Instance.RegisterEventListeners(this); } // 监听自定义事件 [Hear("玩家获得物品")] private void OnItemAcquired() { Debug.Log("玩家获得了新物品!"); // 执行游戏逻辑 } // 监听对话事件 [HearDialogue(0, 5, 2)] private void OnSpecificDialogue() { Debug.Log("播放了关键对话!"); } } ``` #### ? 完整游戏集成示例 ##### 游戏管理器 ```csharp using UnityEngine; using StoryGraph; using StoryGraph.SO; public class GameManager : MonoBehaviour { public static GameManager Instance { get; private set; } [Header("故事系统")] [SerializeField] private StoryBookSO mainStoryBook; [SerializeField] private StorySpeaker storySpeaker; [Header("游戏状态")] private bool isInDialogue = false; private int currentChapterIndex = 0; private void Awake() { if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); } else { Destroy(gameObject); } } private void Start() { InitializeStorySystem(); } private void InitializeStorySystem() { if (storySpeaker != null && mainStoryBook != null) { storySpeaker.currentStoryBook = mainStoryBook; storySpeaker.OnStoryChanged += OnStoryChanged; storySpeaker.OnDialogueCompleted += OnDialogueCompleted; } } // 开始新游戏 public void StartNewGame() { currentChapterIndex = 0; storySpeaker.ChangeChapter(currentChapterIndex); storySpeaker.GetNext(); // 开始播放 } // 加载游戏进度 public void LoadGame(int chapterIndex, int completedNodesCount) { currentChapterIndex = chapterIndex; storySpeaker.ChangeChapter(currentChapterIndex); // 跳过已完成的节点 for (int i = 0; i < completedNodesCount; i++) { storySpeaker.GetNext(); } } // 处理故事变化 private void OnStoryChanged(int previousIndex, int newIndex) { Debug.Log($"章节切换: {previousIndex} -> {newIndex}"); currentChapterIndex = newIndex; // 保存游戏进度 SaveGameProgress(); } // 处理对话完成 private void OnDialogueCompleted() { Debug.Log("对话完成"); // 可以在这里处理对话结束后的逻辑 } // 保存游戏进度 private void SaveGameProgress() { PlayerPrefs.SetInt("CurrentChapter", currentChapterIndex); PlayerPrefs.SetInt("CompletedNodes", storySpeaker.GetCompletedNodeIds().Count); PlayerPrefs.Save(); } // 触发游戏事件 public void TriggerGameEvent(string eventName) { StoryEventManager.Instance.TriggerEvent(eventName); } } ``` ##### 存档系统实现 ```csharp using UnityEngine; using System.Collections.Generic; using StoryGraph; [System.Serializable] public class GameSaveData { public int currentChapterIndex; public List completedNodeIds; public Dictionary gameVariables; public GameSaveData(int chapter, List completedNodes, Dictionary variables) { currentChapterIndex = chapter; completedNodeIds = new List(completedNodes); gameVariables = new Dictionary(variables); } } public class SaveLoadSystem : MonoBehaviour { private const string SAVE_KEY_PREFIX = "GameSave_"; // 保存游戏 public void SaveGame(int slotIndex) { if (GameManager.Instance == null || GameManager.Instance.storySpeaker == null) return; var storySpeaker = GameManager.Instance.storySpeaker; var saveData = new GameSaveData( GameManager.Instance.currentChapterIndex, storySpeaker.GetCompletedNodeIds(), new Dictionary() // 可以扩展游戏变量 ); string json = JsonUtility.ToJson(saveData); PlayerPrefs.SetString(SAVE_KEY_PREFIX + slotIndex, json); PlayerPrefs.Save(); Debug.Log($"游戏已保存到槽位 {slotIndex}"); } // 加载游戏 public bool LoadGame(int slotIndex) { string saveKey = SAVE_KEY_PREFIX + slotIndex; if (!PlayerPrefs.HasKey(saveKey)) return false; string json = PlayerPrefs.GetString(saveKey); GameSaveData saveData = JsonUtility.FromJson(json); if (GameManager.Instance != null) { GameManager.Instance.LoadGame(saveData.currentChapterIndex, saveData.completedNodeIds.Count); return true; } return false; } // 检查存档是否存在 public bool HasSaveData(int slotIndex) { return PlayerPrefs.HasKey(SAVE_KEY_PREFIX + slotIndex); } } ``` ##### UI控制器实现 ```csharp using UnityEngine; using UnityEngine.UI; using TMPro; using StoryGraph; public class DialogueUIController : MonoBehaviour { [Header("UI组件")] [SerializeField] private GameObject dialoguePanel; [SerializeField] private TMP_Text characterNameText; [SerializeField] private TMP_Text dialogueText; [SerializeField] private Image characterPortrait; [SerializeField] private Button continueButton; [SerializeField] private GameObject choicePanel; [SerializeField] private Button[] choiceButtons; [SerializeField] private TMP_Text[] choiceTexts; [Header("故事播放器")] [SerializeField] private StorySpeaker storySpeaker; private bool isWaitingForChoice = false; private void Start() { // 绑定UI组件 if (storySpeaker != null) { storySpeaker.roleNameText = characterNameText; storySpeaker.dialogueText = dialogueText; storySpeaker.avatarImage = characterPortrait; } // 设置按钮事件 continueButton.onClick.AddListener(OnContinueClicked); for (int i = 0; i < choiceButtons.Length; i++) { int index = i; // 闭包捕获 choiceButtons[i].onClick.AddListener(() => OnChoiceSelected(index)); } // 注册事件监听 StoryEventManager.Instance.RegisterEventListeners(this); // 隐藏UI dialoguePanel.SetActive(false); choicePanel.SetActive(false); } private void OnContinueClicked() { if (!isWaitingForChoice) { storySpeaker.GetNext(); } } private void OnChoiceSelected(int choiceIndex) { if (isWaitingForChoice && storySpeaker.currentData is BranchNodeData branchData) { if (choiceIndex < branchData.choiceDatas.Count) { // 选择分支 int targetNodeId = branchData.choiceDatas[choiceIndex].targetNodeNumID; // 执行跳转 var jumpNode = new JumpNodeData { targetNodeNumID = targetNodeId }; storySpeaker.currentData = jumpNode; storySpeaker.GetNext(); // 隐藏选项面板 choicePanel.SetActive(false); isWaitingForChoice = false; } } } // 监听分支节点开始 [HearEnd(-1, 200)] // 假设分支节点的NumID是200 private void OnBranchNodeActivated() { if (storySpeaker.currentData is BranchNodeData branchData) { ShowChoices(branchData.choiceDatas); } } private void ShowChoices(List choices) { isWaitingForChoice = true; choicePanel.SetActive(true); // 显示选项 for (int i = 0; i < choiceButtons.Length; i++) { if (i < choices.Count) { choiceButtons[i].gameObject.SetActive(true); choiceTexts[i].text = choices[i].text; } else { choiceButtons[i].gameObject.SetActive(false); } } } // 监听对话开始 [HearStart(0)] private void OnDialogueStarted() { dialoguePanel.SetActive(true); } // 监听对话结束 [HearEnd(0, 999)] // 假设结束节点的NumID是999 private void OnDialogueEnded() { dialoguePanel.SetActive(false); } } ``` ### 最佳实践指南 #### ? 性能优化建议 - **对象池管理**:对频繁创建销毁的UI元素使用对象池 - **资源管理**:使用AssetBundle管理故事资源,按章节异步加载 - **内存优化**:及时释放不用的故事数据,使用纹理压缩 #### ? 调试技巧 - **事件阅读器**:实时监控事件触发,检查事件监听器注册 - **故事调试**:在编辑器中测试节点连接,使用断点调试 - **性能分析**:使用Unity Profiler监控性能,检查UI重绘频率 #### ? 项目扩展建议 - **本地化支持**:将对话文本分离到本地化文件,支持多语言切换 - **存档扩展**:支持多存档位,云存档同步 - **高级功能**:语音合成集成,自动播放模式,跳过已读对话 ### 实际案例 - **视觉小说游戏**:使用故事书模式管理多章节,全局角色注册表统一管理角色,导入导出工具批量处理数据 - **RPG剧情系统**:独立故事模式按任务分配剧情,事件驱动架构与游戏系统交互 - **教育互动故事**:章节跳转实现分支学习路径,事件系统记录学习行为 ### AI 工具使用指南 #### 统一API配置管理 从v0.9.3版本开始,所有AI工具的API密钥通过统一的配置面板管理,无需在各个工具中分别设置。 ##### 打开API配置面板 在Unity菜单中选择 `Tools/故事工具/AI API配置` ##### API配置类型 **故事API配置** - 用于AI写故事工具的文本生成 - 用于AI绘制角色的提示词生成 - 支持的提供商: - DeepSeek:高性价比的文本生成模型 - 通义千问:阿里云文本生成模型 **图片API配置** - 用于AI绘制角色的图像生成 - 当前支持:火山引擎方舟平台(豆包模型) ##### 配置步骤 1. **选择故事API提供商** - 在下拉菜单中选择 DeepSeek 或 通义千问 - 点击"获取"按钮跳转到对应平台申请API密钥 - 将获取的API密钥粘贴到密钥输入框 2. **配置火山引擎图片API** 火山引擎需要两个参数: a. **获取API密钥**: - 访问火山引擎方舟平台:https://console.volcengine.com/ark - 在账户设置中创建并复制API密钥 b. **选择图像生成模型**: - 在"图像生成模型"下拉菜单中选择预设模型: * `豆包-生图4.0`:文生图模型(推荐,模型ID: doubao-seedream-4-0-250828) * `豆包-生图4.5`:文生图模型(最新版本,模型ID: doubao-seedream-4-5-251128) * `豆包-生图3.0`:文生图模型(模型ID: doubao-seedream-3-0-t2i-250415) * `豆包-编辑3.0`:图生图模型(模型ID: doubao-seededit-3-0-i2i-250628) * `自定义模型或Endpoint`:手动输入模型ID或Endpoint ID c. **填写配置**: - 将API密钥粘贴到"火山引擎 API密钥" - 如果选择自定义模型,需要手动输入"模型ID / Endpoint ID" - 预设模型会自动使用对应的模型ID,无需手动输入 3. **保存配置** - 点击"保存"按钮保存所有配置 - 配置会自动应用到所有AI工具 ##### 功能说明 - **显示密钥**:勾选可查看明文密钥,方便检查输入 - **测试连接**:实际测试故事API连接(发送测试请求验证API密钥有效性) - **重置配置**:清空所有API配置 - **配置状态提示**:实时显示各个API的配置状态 - **使用此API的功能**:显示哪些工具会使用当前配置的API ##### 在AI工具中使用 配置完成后,所有AI工具会自动使用统一配置的API: - **AI写故事工具**:使用配置的故事API - **AI绘制角色**:提示词生成使用故事API,图像生成使用图片API 如果API未配置,工具会显示警告并引导您打开配置面板。 #### AI写故事工具 支持DeepSeek/通义千问,按统一TXT规范生成章节/整书文本,支持续写功能。 ##### 使用步骤 1. 打开工具:`Tools/故事工具/AI写故事工具` 2. 确保已在API配置面板中设置故事API 3. 选择写作模式: - 章节模式:生成单个章节 - 整书模式:生成完整故事(多章节) - 续写模式:基于现有文件续写 4. 输入相应的标题和提示词 5. 点击"开始生成" 6. 生成完成后可保存为TXT文件 ##### 续写功能 - 支持格式:TXT、CSV、XLSX - 自动提取文件内容作为上文 - 可指定续写字数和续写要求 - 保持原有风格和格式 **CSV/XLSX 智能节点续写** 当续写CSV或XLSX格式的故事文件时,工具提供额外的智能节点生成功能: 1. **自动节点生成**:勾选"自动生成节点"选项 2. **选择目标故事数据**:指定要添加节点的 StoryDataSO 3. **角色匹配**(可选):挂载 GlobalRoleRegistrySO 自动匹配角色数据 4. **一键导入**:续写完成后,确认即可自动解析文本并添加节点到故事数据 节点生成规则: - 自动解析对话格式(`角色名 "对话内容"` 和 `角色名[表情] "对话内容"`) - 每个节点包含最多10句对话 - 自动分配唯一的节点ID - 支持角色和表情自动匹配 - 生成的节点直接保存到StoryDataSO中 #### AI绘制角色工具 使用AI生成角色头像和表情图片,支持单图生成、表情全家桶和全员头像模式。 ##### 使用步骤 1. **打开工具**:`Tools/故事工具/AI绘制角色` 2. **配置API**:确保已在API配置面板中设置故事API和图片API 3. **挂载角色注册表**:在"全局角色注册表"字段中挂载 GlobalRoleRegistrySO 4. **选择生成模式**: - **单图模式**:生成一张独立的角色图片 - **表情全家桶**:自动为角色的所有表情生成图片,统一风格并自动关联 - **全员头像**:自动为角色注册表中的所有角色生成头像,并自动装配到StoryDataSO 5. **选择角色和表情**(单图和表情全家桶模式): - 从下拉菜单中选择目标角色 - 如果生成单图,选择目标表情 6. **输入角色描述**:描述角色的外貌、服装、特征等,用于生成提示词 7. **生成提示词**(可选): - 点击"AI生成"按钮自动生成正向和负向提示词 - 也可以手动编辑提示词 8. **配置图像设置**: - **尺寸预设**:选择图片尺寸(512x512、1024x1024等) - **自定义尺寸**:在"自定义"预设下可手动调整宽度和高度 - **保存设置**:设置默认保存路径,可自动创建不存在的路径 - **自动应用**:开启后生成完成会自动保存为Sprite并应用到角色SO 9. **风格一致性控制**(表情全家桶和全员头像模式): - **一致性强度**:调整风格统一程度(0.5-1.0) - **风格参考**:使用第一张作为风格参考,后续图片会参考其风格特征 - **风格关键词**:添加额外的风格关键词,强化风格一致性 ##### 高级功能 - **批量生成**:表情全家桶模式可一次性为角色的所有表情生成图片 - **自动关联**:生成的表情图片会自动关联到角色的表情列表 - **风格统一**:通过风格一致性控制和风格参考,确保生成的图片风格统一 - **全员处理**:全员头像模式可批量为所有角色生成头像,提高工作效率 - **智能提示词**:基于角色描述自动生成高质量的绘画提示词 - **灵活配置**:支持自定义尺寸、保存路径和自动应用设置 ##### 生成模式说明 | 生成模式 | 功能描述 | 适用场景 | | ---------- | -------------------------------- | ------------------------------ | | 单图模式 | 生成单张独立的角色图片 | 快速生成单个头像或表情 | | 表情全家桶 | 为角色所有表情生成图片,统一风格 | 完整的角色表情系统建设 | | 全员头像 | 为所有角色生成头像,自动装配 | 批量创建角色头像,快速启动项目 | ### 更新日志 #### v0.9.3 (最新) - **统一API配置**:新增API配置管理面板,集中管理所有AI工具的API密钥 - AI工具集成:将AIStoryWriter集成到StoryGraph框架,通过编辑器"工具面板"或菜单访问 - AI绘制角色:新增AI角色绘画工具,支持提示词生成和火山引擎图像生成 - **AI智能续写**:支持续写TXT/CSV/XLSX格式的故事文件,CSV/XLSX格式支持自动生成节点并添加到故事数据 - 程序集定义:添加Editor程序集定义文件,支持在Packages中使用 - 菜单优化:AI工具统一放在`Tools/故事工具`下,提供一致的访问入口 - 架构改进:AI工具模块化设计,统一API管理,支持DeepSeek、通义千问和火山引擎API #### v0.9.2 - [核心] 章节跳转:`ChangeChapter`/`ChangeChapterByName` 为 `void`,章节跳转节点强制落到目标章节 Start 节点,事件阅读器同步 - [改进] 导入/导出/转换:章节索引 0/1 基转换一致,旁白作为角色参与节点生成,路径记忆与调试日志完善 - [改进] 角色/表情:导入前先写入全局注册表,节点自动匹配角色与表情(缺失表情留空) - [核心] AI/格式:DeepSeek/通义千问统一输出 `角色名 "对话内容"` / `角色名[表情] "对话内容"` / `旁白 描写` / `## 章节标题` #### v0.9.1 - [核心] 章节跳转:`ChangeChapter`/`ChangeChapterByName` 改为 `void`,章节跳转节点始终跳到目标章节 Start 节点,事件阅读器同步 - [改进] 导入/导出:章节索引内外 0/1 基转换一致,旁白作为角色参与节点生成 - [改进] 角色/表情:导入时先注册角色与表情到全局注册表,再导入节点 - [核心] AI/格式:统一 TXT 规范 `角色名 "对话内容"`、`角色名[表情] "对话内容"`、`旁白 描写`、`## 章节标题` #### v0.9.0 - [新增] 新增全局角色自动匹配、对话转图工具、节点图智能布局 - [优化] 优化导入导出性能,修复XLSX导出稳定性问题 - [文档] 完善文档和使用指南 ### 技术支持 - **项目仓库**:[Gitee](https://gitee.com/PXY2333/unity-story-editor) - **问题反馈**:在仓库中提交 Issue - **功能建议**:通过 Issue 或 Pull Request 提出 - **开源协议**:MIT 许可证,详见 LICENSE 文件 --- **开始您的故事创作之旅吧!** 使用StoryGraph,您可以轻松创建引人入胜的交互式故事。无论您是独立开发者还是工作室团队,都能在这个强大的工具中找到创作的乐趣。 ## 常见问题 ### Q: 如何实现存档/读档功能? A: 使用 `StorySpeaker.GetCompletedNodeIds()` 获取已完成的节点列表,保存到游戏存档中。读档时,可以根据存档数据跳过已完成的节点。 ### Q: 如何动态修改故事内容? A: 可以在运行时修改 `StoryDataSO` 的数据,或创建新的 `NodeData` 对象动态替换原有节点。 ### Q: 如何集成到现有的对话系统中? A: 可以通过事件监听器将故事图事件转发到现有系统,或继承 `StorySpeakerBase` 实现自定义的播放逻辑。 ### Q: 如何支持多语言? A: 对话文本可以使用本地化 key,在运行时通过本地化系统替换为实际文本。 ### Q: 如何调试故事流程? A: 在编辑器中可以启用调试模式,查看节点执行顺序和事件触发情况。 ### Q: 故事书和独立故事数据有什么区别? A: 故事书(`StoryBookSO`)用于管理多个章节,适合多章节游戏。独立故事数据(`StoryDataSO`)适合单章节或简单的故事。使用故事书可以更好地组织和管理大型项目。 ### Q: 章节跳转节点如何使用? A: 确保 `StorySpeaker` 关联了 `StoryBookSO`,添加章节跳转节点并设置目标章节索引(0 基,导出/导入时支持 1 基显示)。运行时会直接跳到目标章节的 Start 节点。 ### Q: 如何在不同章节之间复制粘贴节点? A: 选择节点后按`Ctrl+C`复制,切换到目标章节后按`Ctrl+V`粘贴。系统自动处理连接关系、保持相对位置并生成新NumID。 ### Q: 事件阅读器如何与章节跳转同步? A: 章节跳转时自动通知事件阅读器更新,显示当前章节的事件信息。 ### Q: 自动保存功能如何工作? A: 勾选工具栏"自动保存"选项,每5秒自动保存。未保存更改在窗口标题显示`*`标记。 ### Q: 如何使用全局角色自动匹配功能? A: 创建`GlobalRoleRegistrySO`并注册角色,在导入工具中关联该SO。导入时自动匹配角色名称并添加完整角色数据。 ### Q: 对话转图工具支持哪些输入格式? A: 支持TXT格式(`角色名 "对话内容"`,支持中英文引号),可转换为CSV或XLSX格式。 ### Q: 节点图为什么会自动重新排列? A: 智能布局功能,打开时按ID顺序自动排列避免堆叠,可在排列后手动调整。 ## 扩展开发 - **创建自定义节点**:继承`NodeData`和`BaseNode`,实现`ProcessNode`方法,在编辑器中注册 - **扩展事件系统**:创建新的事件属性类(继承`Attribute`),在`StoryEventManager`中注册 - **自定义UI渲染**:继承`StoryUIManager`实现自定义绑定逻辑,创建新的UI属性标记 ## 版本兼容性 - **Unity 版本**:支持 Unity 2021.3 LTS 及更高版本(已测试 Unity 2022.3.62f2c1) - **依赖包**:需要 TextMeshPro 包(版本 3.0.9) - **最新版本 (v0.9.0)**:包含故事书系统、章节跳转、导入导出、对话转图、全局角色匹配等完整功能 ## 许可证与支持 - **开源协议**:MIT 许可证,详见 [LICENSE](LICENSE) 文件 - **项目仓库**:[Gitee](https://gitee.com/PXY2333/unity-story-editor) - **问题反馈**:在仓库中提交 Issue - **功能建议**:通过 Issue 或 Pull Request 提出 --- *更多详细文档和示例请查看项目文档和示例场景。*