【Unity】对话与任务系统基础架构 (一)_unity任务系统
对话与任务系统基础架构 (一)
前言
最近在做一个剧情向的游戏项目,需要实现一套完整的对话和任务系统。经过一段时间的开发和迭代,总算是搭建出了一套相对完善的架构。想着分享出来,一方面是做个记录,另一方面也希望能给有类似需求的朋友一些参考。
这个系列预计会分3篇来写:
- 第一篇(本篇):整体架构设计和核心思路
- 第二篇:对话系统的具体实现细节
- 第三篇:任务系统的具体实现细节
项目背景
我们的游戏是一个剧情向的RPG,对话和任务是核心玩法。在设计之初就明确了几个需求:
对话系统需求
- 支持多角色对话,带头像、姓名等信息
- 支持选择分支,不同选择有不同后续
- 支持对话完成后触发各种事件(任务、跳转等)
- 策划能够方便地配置对话内容
- 支持跳过、快进等用户交互
任务系统需求
- 支持多种任务类型(对话、收集、交互等)
- 任务之间有前置关系,支持任务链
- 任务进度要能保存和加载
- UI要能实时显示任务状态
- 方便后续扩展新的任务类型
整体架构思路
经过一番思考,我们采用了分层架构 + 事件驱动的设计模式。为什么这么选择呢?
分层架构的好处
- 职责清晰:每一层只管自己的事情,数据层管数据,UI层管显示
- 易于维护:修改某一层的逻辑不会影响其他层
- 便于测试:可以单独测试每一层的功能
事件驱动的好处
- 解耦合:系统之间通过事件通信,不直接依赖
- 易扩展:新增功能只需要监听相应事件即可
- 灵活性:可以动态添加或移除事件监听器
系统架构图
先上个整体的架构图,让大家有个直观的认识:
┌─────────────────┐ ┌─────────────────┐│ UI Layer │ │ Event System ││ (对话UI/任务UI) │◄──►│ (UniFramework) ││ │ │ │└─────────────────┘ └─────────────────┘ ▲ ▲ │ │┌─────────────────┐ ┌─────────────────┐│ Manager Layer │ │ Logic Layer ││ (对话管理器 │◄──►│ (任务收集器 ││ 任务管理器) │ │ 事件处理器) │└─────────────────┘ └─────────────────┘ ▲ ▲ │ │┌─────────────────┐ ┌─────────────────┐│ Data Layer │ │ Storage Layer ││ (配置表数据 │◄──►│ (存档系统 ││ 运行时数据) │ │ 数据持久化) │└─────────────────┘ └─────────────────┘
各层职责说明
UI Layer(UI层)
- 负责界面显示和用户交互
- 接收用户输入,显示系统状态
- 通过事件与下层通信
Manager Layer(管理层)
- 系统的核心控制逻辑
- 协调各个子系统的工作
- 处理业务流程
Logic Layer(逻辑层)
- 具体的业务逻辑实现
- 各种策略和算法
- 事件的具体处理
Data Layer(数据层)
- 配置表数据管理
- 运行时数据结构
- 数据的增删改查
Storage Layer(存储层)
- 数据持久化
- 存档加载
- 数据序列化
核心设计原则
在架构设计过程中,我们遵循了几个核心原则:
1. 单一职责原则
每个类只负责一件事情。比如:
DialogueManager
只管对话流程控制TaskDataManager
只管任务数据DialogueUI
只管对话界面显示
2. 开闭原则
对扩展开放,对修改封闭。比如:
- 新增任务类型不需要修改现有代码
- 新增对话事件类型通过配置即可
3. 依赖倒置原则
高层模块不依赖低层模块,都依赖抽象。比如:
// 管理器依赖接口,不依赖具体实现public class DialogueManager{ private IDialogueUI _dialogueUI; // 依赖抽象 // 而不是依赖具体的 TestDialogueView}
4. 接口隔离原则
接口要小而专,不要大而全。比如:
// 分离不同的接口public interface ITaskCollector { }public interface ITaskEventListener { } public interface ITaskCollectorUpdate { }
数据驱动设计
我们的系统是完全数据驱动的,所有的内容都通过配置表来管理。
配置表结构设计
对话系统采用三级结构:
DialogueMain (章节) ├── DialogueGroup (对话组)│ ├── DialogueData (单条对话1)│ ├── DialogueData (单条对话2) │ └── DialogueOption[] (选择选项)└── DialogueGroup (对话组2)
任务系统也是三级结构:
TaskGroup (任务组)├── TaskData (任务1)│ ├── TaskCompleteCondition (完成条件)│ └── TaskCollector (收集器实例)└── TaskData (任务2)
数据驱动的好处
- 策划友好:策划可以直接修改配置表,不需要程序员参与
- 热更新支持:配置表可以热更新,不需要重新打包
- 版本控制:配置表变更可以通过版本控制系统管理
- 本地化支持:多语言版本只需要替换配置表
事件系统设计
我们使用了UniFramework的事件系统作为基础,设计了一套完整的事件驱动架构。
事件分类
// 对话相关事件public class StartDialogueChapter : IEventMessage{ public int ChapterId { get; set; } public int GroupId { get; set; }}// 任务相关事件 public class TaskDialogueCollectorEvent : IEventMessage{ public int DialogueGroupId { get; set; }}
事件流转示例
用户点击NPC → 发送StartDialogue事件 → DialogueManager接收 → 显示对话UI → 用户选择选项 → 发送TaskEvent事件 → TaskManager接收 → 更新任务进度 → 发送UI更新事件 → UI刷新
扩展性设计
工厂模式支持扩展
// 任务收集器工厂public static class TaskCollectorFactory{ public static TaskCollectorBase CreateTaskCollector(TaskData taskData) { return taskData.taskType switch { TaskType.Dialogue => new TaskDialogueCollector(taskData), TaskType.Collection => new TaskCollectionCollector(taskData), TaskType.Interaction => new TaskInteractionCollector(taskData), _ => null }; }}
策略模式支持多样化
// 不同类型的任务有不同的处理策略public abstract class TaskCollectorBase{ public abstract void OnTaskEventReceived(IEventMessage eventMessage); public abstract bool CheckTaskComplete();}
开发过程中的坑
分享几个开发过程中踩过的坑:
1. 事件监听器的生命周期管理
问题:事件监听器没有正确释放,导致内存泄漏和重复触发。
解决方案:使用EventGroup统一管理事件监听器的生命周期。
public class TaskCollectorBase{ protected EventGroup eventGroup; public virtual void Init() { eventGroup = new EventGroup(); eventGroup.AddListener<TaskDialogueCollectorEvent>(OnTaskEventReceived); } public virtual void Release() { eventGroup?.RemoveAllListener(); }}
2. 配置表数据的循环引用
问题:任务A的完成条件依赖任务B,任务B的完成条件又依赖任务A,形成死锁。
解决方案:在数据加载时进行循环依赖检测,并提供清晰的错误提示。
3. UI状态同步问题
问题:任务状态更新了,但UI没有及时刷新。
解决方案:建立完善的UI事件通知机制,确保数据变更时UI能及时响应。
性能优化考虑
1. 事件过滤
不是所有收集器都需要监听所有事件,要做好事件过滤:
// 只监听相关的事件类型if (eventMessage is TaskDialogueCollectorEvent dialogueEvent){ // 处理对话事件}
2. 数据懒加载
配置表数据按需加载,避免一次性加载所有数据:
public TaskData GetTaskData(int taskId){ if (!_taskDataCache.ContainsKey(taskId)) { _taskDataCache[taskId] = LoadTaskDataFromTable(taskId); } return _taskDataCache[taskId];}
3. UI更新优化
避免频繁的UI更新,使用脏标记机制:
private bool _isDirty = false;public void MarkDirty(){ _isDirty = true;}private void Update(){ if (_isDirty) { RefreshUI(); _isDirty = false; }}
小结
本篇主要介绍了对话与任务系统的整体架构设计思路。核心要点总结如下:
- 分层架构:清晰的职责分离,便于维护和扩展
- 事件驱动:系统间松耦合,提高灵活性
- 数据驱动:配置表管理内容,策划友好
- 扩展性设计:工厂模式和策略模式支持功能扩展
- 性能优化:合理的缓存和更新策略
下一篇文章将详细介绍对话系统的具体实现,包括UI状态机、配置表设计、选择分支处理等细节。如果大家有什么问题或建议,欢迎在评论区讨论!
相关资源:
- Unity版本:2022.3 LTS
- 事件系统:UniFramework
- 异步框架:ET Framework
- 配置表:Excel + 代码生成
下期预告:
《对话与任务系统基础架构 (二) - 对话系统实现详解》将深入介绍对话系统的UI状态机、配置表解析、选择分支逻辑等核心实现。