2281 字
11 分钟
My ARPG 开发日志:场景切换、A*、对话与任务系统

项目仓库: Gitee | GitHub

开发记录覆盖场景切换、状态恢复、A*、NPC 状态、条件对话、任务、商店和 UI 路由。代码组织围绕事件、ScriptableObject 数据和多场景结构展开。每个模块单独建类,数据依赖和调用顺序落在同一条主链上。

2026 年 3 月 18 日到 2026 年 3 月 20 日:项目初始化与基础结构#

工程已有玩家状态机、击退处理、商店入口和部分目录结构。初始化部分先整理脚本目录、命名和公共依赖。单位脚本、对话脚本、场景脚本、UI 脚本、ScriptableObject 资源分别归档。预制件、动画和 SO 资源同步分组。任务、对话、商店和多场景切换共享统一目录结构与 Inspector 绑定规则。

目录结构固定后,脚本职责随目录边界拆开。单位逻辑与 UI 逻辑不放在同一目录,对话系统与场景系统各自持有独立入口。多系统协作依赖稳定命名、稳定资源路径和稳定事件引用。

2026 年 3 月 21 日到 2026 年 3 月 24 日:场景切换与状态保存打底#

场景切换和状态恢复由 InitialLoadSceneChangerDataManagerISaveable 四层承担。入口场景通过 Addressables.LoadSceneAsync(persistentScene) 加载持久场景,后续加载事件由 SceneLoadEventSO 广播给 SceneChangerSceneChanger 收到请求后禁用 PlayerMovement、重置血量、暂停时间、记录目标场景和目标位置,再按淡入标记控制过渡动画。

SceneChanger 的核心流程是:卸载当前场景,以 LoadSceneMode.Additive 异步加载目标场景,设置玩家坐标,在 OnLoadCompleted() 中恢复输入和时间。输入禁用、血量重置、暂停和位置切换都集中在同一个类内,传送门、菜单和碰撞脚本只负责发起请求。

状态恢复层由 DataManagerISaveable 组成。DataManager 带有 [DefaultExecutionOrder(-100)],初始化顺序早于普通脚本,内部维护 List<ISaveable>。保存事件触发时,Save() 遍历注册对象,把 Data 实例交给各对象写入。读取事件触发时,Load() 再把同一份 Data 发回各对象。对象侧实现 GetDataID()SaveData()LoadData() 即可。

战利品状态接入保存流程。Data.lootsStatsDic 记录战利品位置和拾取状态,用于重载场景时恢复交互物状态。GUID 与对象状态进入同一张数据表。

2026 年 3 月 25 日到 2026 年 3 月 31 日:A* 寻路#

AStarPathFinder 完成主要结构。路径搜索接口是 FindPath(Vector3 optPos, Vector3 startPos, Vector3 endPos),内部先把世界坐标转成网格坐标,再用 Dictionary<Vector3, PathFinderDetails> 维护开启列表,用 HashSet<Vector3> 维护关闭列表。SearchCheapestCost() 负责取总代价最低节点,RetracePath() 负责从终点回溯父节点并生成路径栈。

optPos 是一层前置优化。optPos 不为零时,程序先检查该点对应网格是否可通过,再用 NoCoverObstacleNodes() 判断起点到 optPos 的直线段是否存在障碍。直线路径可用时,搜索起点前移到 optPos,开启列表规模随之缩小。

八方向移动与对角穿墙检测是寻路层的另一条规则。AddNodeToOpen() 在展开邻居时调用 CanWalkDiagonally()。斜向移动时,如果横向和纵向相邻格子都被障碍占用,路径搜索禁止沿对角线穿过。路径结果因此与碰撞结果保持一致。

导航数据由 AStarNodeManager 从 Tilemap 和 Collider2D 生成。地图切换后,沿用相同图层与碰撞规则即可刷新网格数据。NPC、敌人和交互对象共享这层导航数据。

2026 年 4 月 1 日到 2026 年 4 月 6 日:NPC、动画与对话入口#

NPC 行为按状态拆开。NPCStateController 管理 IdleWanderPatrolChatNPCWanderNPCPatrol 处理移动,NPCChat 处理交互入口和对话触发。状态切换通过启用或禁用脚本完成。角色接近 NPC 并触发交互时,移动脚本停下,对话入口接管当前状态。

动画与交互时机跟随同一条状态链移动。NPCChat 负责拉起 DialogManagerNPCStateController 负责停掉游荡或巡逻脚本。移动、动画、交互、面板打开分别由不同脚本承担。

店主和普通 NPC 沿用同样的拆分方式。店主脚本只负责商店入口,商品内容和商店 UI 交给 ShopManager。普通 NPC 只负责对话入口,对话条件与历史回写交给 DialogManager 和历史管理器。

2026 年 4 月 7 日到 2026 年 4 月 10 日:菜单、暂停与 Retry 逻辑#

ESC 菜单、暂停和 Retry 共用输入、动画、场景和玩家状态。时序问题集中在暂停状态、死亡重载、淡入淡出和输入恢复几个位置。由UI 管理层统一控制,不让单个按钮各自维护一套条件。

UI 射线阻挡、面板状态残留、按钮事件和场景状态不同步。问题在 UI 管理层。商店、任务、对话、属性和 ESC 面板共用一套开关路由,状态切换由统一管理器分发。

2026 年 4 月 13 日到 2026 年 4 月 19 日:条件对话与对话历史#

DialogSODialogManagerConversationHistoryManagerItemHistoryManager 组成一套结构。DialogSO 保存主说话角色、对话文本数组、选项分支、角色前置条件、物品前置条件、拒绝对话节点和一次性触发限制。对话节点从“文本 + 下一句”扩展成带条件、带分支、带历史回写的数据对象。

DialogManager.StartDialog() 先执行 MatchConditionsToStartDialog()。角色条件通过 ConversationHistoryManager.CharactersHasChated 检查,物品条件通过 ItemHistoryManager.HasPickedOverAmount() 检查,一次性对话通过 DialogSO.HasChated 检查。条件不满足时,StartRefuseDialog()ChatType 切到拒绝分支,界面继续显示对应内容。

对话结束时,EndDialog()mainCharacter 写进 ConversationHistoryManagerConversationHistoryManagerHashSet<CharacterSO> 记录已触发角色,ItemHistoryManagerDictionary<ItemSO, int> 记录物品累计数量。历史系统服务条件判断。任务节点、分支对话和拒绝对话都从历史系统读取结果。

选项按钮纳入 DialogManagerShowChoices() 遍历 nextDialogOptions,把选项文本写进按钮,再通过 onClick 注册进入下一节点。主分支、拒绝分支、一次性对话和普通默认对话共用同一套结构。

2026 年 4 月 20 日到 2026 年 4 月 24 日:任务系统成型#

任务系统主体是 QuestManager。类内部维护 Dictionary<QuestSO, QuestProgressData>,每个任务对应一个状态和一张目标进度表。QuestProgressData 使用 Dictionary<QuestObjective, int> 保存目标当前值。任务板打开时,OnReFreshQuestState() 先初始化进度表,再刷新 QuestLogSlot。任务板为空时,详情页与提示页切到空任务状态。

任务状态流转由 QuestStateChanged() 控制。状态变化时,接取、放弃、完成按钮区域同步切换,QuestLogUI 刷新目标文本。任务完成时,RaiseRewardEvent() 遍历奖励列表,把奖励写进 InventorySlotsStatsSO。奖励发放由事件进入库存更新流程,不直接改背包槽位。

任务进度统计直接依赖历史系统。UpdateObjectiveProgress() 在目标是物品时读取 ItemHistoryManager 的累计数量,在目标是角色时读取 ConversationHistoryManager 的对话记录。任务脚本不需要维护 NPC 交互细节。任务目标和对话历史、拾取记录共享同一套底层数据。

QuestObjective 同时包含目标物品、目标角色和目标地点字段。地点统计接口已经留在数据结构中,地图触发点逻辑预留在该字段上。

2026 年 4 月 26 日到 2026 年 4 月 29 日:奖励、商店、UIManager 与 Addressables#

奖励发放、商店、UI 路由和 Addressables 场景加载整理成同一条调用链。QuestManager.RaiseRewardEvent() 把奖励发给库存系统,ShopManager 接收商品列表并打开商店面板,UIManager 负责面板互斥,场景链路由 SceneChanger 和 Addressables 维持。

ShopManager 内部维护一般物品、武器、防具三张列表。OnShopLoad() 接到 ShopLoadEventSO 后,把列表保存到管理器,再打开商店 CanvasGroup。购买调用 RaiseInventoryUpdateRequest(item, price, 1)。出售沿用同一接口,只把价格和数量改成负值。买卖逻辑共用同一条库存更新链路。

UIManagerUpdate() 中读取 ESC、技能、属性、任务、对话、商店等输入,判断当前是否已有面板打开,再通过一组 ToggleCanvasEventSO 广播目标状态。管理器不直接硬编码每个面板的开关。面板开关和互斥关系都经事件系统分发。场景切换事件进入时,ResetCanvas() 把托管面板全部关掉,清空跨场景残留。

Addressables 场景链路固定为统一顺序。场景加载、场景卸载、淡入淡出、玩家状态恢复按同一顺序执行,普通加载接口不混入该路径。场景调试路径和构建路径保持一致。

结果#

场景切换、状态恢复、A*、NPC 行为、条件对话、任务进度、奖励发放、商店和 UI 管理纳入同一条主链。系统边界如下:场景切换用事件和 Addressables,状态恢复用 DataManager,路径搜索用 A*,对话和任务读取历史系统,面板互斥由 UIManager 统一处理。


项目介绍:My ARPG

My ARPG 开发日志:场景切换、A*、对话与任务系统
https://www.yonagi.world/posts/unity-arpg-dev-log/
作者
YONAGI
发布于
2026-04-29
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
遠い日に想いを馳せて
Laplacian
封面
遠い日に想いを馳せて
Laplacian
0:00 / 0:00