Compare commits

...
Author SHA1 Message Date
dingyuntian 592d3a36dd art: 音量条 2026-07-19 17:23:55 +08:00
dingyuntian bc85e4d343 Merge branch 'develop' into feature/demo脚本调整 2026-07-19 00:13:36 +08:00
Ovid 551dd6d183 debug 2026-07-19 00:11:25 +08:00
dingyuntian c9f508bb8e Merge branch 'feature/GameManager相关重构' into 'develop'
feat(log-kit): 重构文件日志系统支持异步结构化日志

See merge request aibis-dream/aibis-dream!746
2026-07-18 16:04:06 +00:00
dingyuntian ba5a9fc2f3 docs(dialog): 添加梦境选项提示语使用文档 2026-07-19 00:00:54 +08:00
dingyuntian d5c4917a8a test(dialog): 添加选项提示语元数据测试
校验 IsOptionPromptLine 仅匹配精确的 option_prompt 元数据。
2026-07-19 00:00:54 +08:00
dingyuntian c751a859ff feat(dialog): 添加梦境选项提示语支持
标记 #option_prompt 的台词不再进入普通台词流程,由 OptionView
暂存并在下一组 Dream Options 出现时显示在选项上方,同时通过
新增 LineHistoryAppend 事件加入对话历史。OnelineOptions 支持
提示语文本与独立选项根节点,并改为按实例追踪销毁按钮。
2026-07-19 00:00:17 +08:00
dingyuntian 081ed0d883 test(save): 添加演出快照契约测试
校验 showcase/playTool/screen 的 SaveId 与还原顺序,
以及快照 section 的 JSON 序列化往返。
2026-07-19 00:00:01 +08:00
dingyuntian a5e417eeae feat(save): 新增梦境演出图与物品图的存档快照
新增 showcase(order 69) 与 playTool(order 70) 两个快照 section,
分别持久化 SpriteShowcase 小图/大图与 PlayToolPanel 物品图/全屏图
的终态,读档时直接还原静止画面。SpriteShowcase 与 PlayToolPanel
补充快照捕获/还原与立即清理逻辑,GameManager 在卸载场景前清理
演出效果。screen 还原顺序顺延至 80。
2026-07-18 23:59:52 +08:00
dingyuntian 2eea496c4b feat(ui): 支持在指定面板上点击推进对话
新增 AllowDialogueAdvanceOnPointer 标记组件,IsPointerOverUI 命中
带该标记的 UI 时不再阻止对话推进;同时缓存 PointerEventData 与
射线结果列表,避免每次检测分配。Play Tool 与 Screen Transition
面板挂上该组件。
2026-07-18 23:59:33 +08:00
dingyuntian d316922b3a fix(fix-system): 修复维修中心销毁时的依赖清理异常
FixSystemCenter.OnSingletonDestroy 在场景销毁阶段访问的 CameraKit、
BubbleSlotKit 可能已清理,且 Director.QuitCurrentMode 协程依赖的
对象可能已拆除。改为判空访问,并新增 Director.Reset 直接清理状态。
2026-07-18 23:59:19 +08:00
dingyuntian 600fd4cebf fix: 修复场景卸载句柄自动释放导致的异常
UnloadSceneAsync 默认 autoReleaseHandle 为 true,yield 完成后访问
Status 会抛异常。改为手动管理句柄生命周期,并将 FixSystemCenter
与 ResourceSystem 的清理移到卸载成功之后。
2026-07-18 23:58:52 +08:00
dingyuntian 4b595fcee9 fix: 对话框底色小改动 2026-07-18 19:24:13 +08:00
dingyuntian 947cfa2fb4 Merge remote-tracking branch 'origin/feature/日志系统翻新' into feature/GameManager相关重构 2026-07-18 17:33:31 +08:00
dingyuntian 0dc1001723 fix: BodyModule可能的注册失败问题 2026-07-18 17:28:06 +08:00
dingyuntian 7476263411 Merge branch 'develop' into feature/GameManager相关重构 2026-07-17 22:21:00 +08:00
dingyuntian 91c3f509e1 yarn: 佩佩2表情替换(待全量检查) 2026-07-17 22:20:22 +08:00
dingyuntian 686c32738d Merge branch 'develop' into feature/demo脚本调整 2026-07-17 20:24:18 +08:00
dingyuntian b40f17ea9f Merge branch 'feature/帧动画系统' into 'develop'
Feature/帧动画系统

See merge request aibis-dream/aibis-dream!744
2026-07-17 12:23:38 +00:00
dingyuntian 4b86068bbf chore: 删除残留 TerminalUI 文件夹 meta 2026-07-17 20:20:25 +08:00
dingyuntian 1077bc551c docs(frame-animation): 补充共享 Sprite 与 Actor 接入文档 2026-07-17 20:20:16 +08:00
dingyuntian a4620dacc8 yarn(dialog): 火山初始化改为 FrameAnimation 类型 2026-07-17 20:20:06 +08:00
dingyuntian 2c8bc17174 chore(addressables): 注册帧动画 Prefab 与 Graph 资源 2026-07-17 20:19:56 +08:00
dingyuntian 24f475fe2f art(frame-animation): 添加帧动画角色 Prefab 与示例素材 2026-07-17 20:19:46 +08:00
dingyuntian 5236066c7b test(frame-animation): 补充共享 Sprite 与 Actor 集成测试 2026-07-17 20:19:34 +08:00
dingyuntian 447d61f941 feat(frame-animation): 编辑器支持共享 SpriteRect 与 Graph 属性面板 2026-07-17 20:19:24 +08:00
dingyuntian 7646cb4db3 feat(scene-mgmt): 添加 FrameAnimation 角色类型与异步加载 2026-07-17 20:19:14 +08:00
dingyuntian 7c04f507db feat(frame-animation): 支持运行时 Graph 绑定与终态恢复 2026-07-17 20:19:04 +08:00
Ovid 5549404fe4 蓝眼无神+入 2026-07-17 17:21:35 +08:00
dingyuntian 667425bd2c feat: 调整日志堆栈等级 2026-07-17 14:25:55 +08:00
dingyuntian 21d51945c3 feat: GameManager相关重构 2026-07-17 14:24:02 +08:00
dingyuntian 2dcfeec938 chore: 去除部分已不再使用的UI代码 2026-07-17 14:22:33 +08:00
Ovid 94130cd97f PP2表情上传 2026-07-17 05:09:02 +08:00
dingyuntian c617e44813 Merge branch 'develop' into feature/帧动画系统 2026-07-16 23:43:54 +08:00
dingyuntian b7ca9ab11f Merge branch 'develop' into feature/demo脚本调整 2026-07-16 23:13:46 +08:00
dingyuntian 847feed926 Merge branch 'develop' into feature/demo脚本调整 2026-07-16 23:13:31 +08:00
dingyuntian e20b499492 Merge branch 'bugfix/预览模式问题' into 'develop'
Bugfix/预览模式问题

See merge request aibis-dream/aibis-dream!743
2026-07-16 15:12:30 +00:00
dingyuntian 5d07012cb6 fix: 预览模式问题 2026-07-16 23:11:36 +08:00
dingyuntian 00a43fcad3 docs: GameManager大重构计划,需要重新审视 2026-07-16 23:07:24 +08:00
dingyuntian e363c6e4a5 Merge branch 'bugfix/合并问题修复' into 'develop'
fix: 冲突合并问题,修复线缆相关

See merge request aibis-dream/aibis-dream!742
2026-07-16 15:06:22 +00:00
dingyuntian 1a113dbd25 fix: 冲突合并问题,修复线缆相关 2026-07-16 23:05:46 +08:00
dingyuntian 3959561686 Merge branch 'develop' into feature/帧动画系统 2026-07-16 20:56:53 +08:00
dingyuntian 031ac79b8d Merge branch 'feature/demo脚本调整' of ssh://114.67.212.218:2222/aibis-dream/aibis-dream into feature/demo脚本调整 2026-07-16 20:24:20 +08:00
Ovid 0db3f75476 D2S结尾调整 2026-07-16 19:39:24 +08:00
dingyuntian f3e1e421a3 Merge branch 'develop' into feature/demo脚本调整 2026-07-16 19:18:06 +08:00
dingyuntian 23b6d9cf87 Merge branch '7.11春优化' into 'develop'
fix(heatmap): 优化温度过渡并修复热点尺寸

See merge request aibis-dream/aibis-dream!741
2026-07-16 11:17:04 +00:00
bottlefish 509e0ae169 Merge branch 'develop' into '7.11春优化'
# Conflicts:
#   Assets/Scripts/FixSystemNew/Screen System/FixPanel/CablePanel.cs
2026-07-16 11:11:07 +00:00
dingyuntian c3c0398905 Merge branch 'bugfix/插孔打开时机问题' into 'develop'
fix: 插孔打开时机

See merge request aibis-dream/aibis-dream!740
2026-07-16 11:09:39 +00:00
bottlefish fd79ff9d93 feat(fix-system): 优化线缆松手下坠与插头朝向 2026-07-16 19:09:25 +08:00
bottlefish 8b1fe28edb fix(fix-system): 移除线缆拖拽的插孔自动吸附 2026-07-16 19:08:31 +08:00
dingyuntian b4f0cebc1d fix: 插孔打开时机 2026-07-16 18:50:00 +08:00
dingyuntian 46602803dc feat: 动画系统第五阶段(未验收) 2026-07-16 18:49:12 +08:00
bottlefish cb1ddc1f93 Merge branch 'develop' into 7.11春优化 2026-07-16 18:42:23 +08:00
bottlefish 6c75ceaa35 docs(save): 补充严格开发跳转使用说明 2026-07-16 18:41:20 +08:00
bottlefish cfb4ff96c4 fix(ui): 修复场景卸载后透明遮罩阻塞交互 2026-07-16 18:40:53 +08:00
bottlefish 7dfd87e868 test(save): 添加开发存档清单校验测试 2026-07-16 18:40:14 +08:00
bottlefish 8deff54e48 feat(save): 增强开发存档跳转与严格校验 2026-07-16 18:39:50 +08:00
bottlefish 7b287a6248 test(save): 更新开发跳转测试存档与清单 2026-07-16 18:37:14 +08:00
dingyuntian 9b6d008263 Merge branch 'develop' into feature/帧动画系统 2026-07-16 18:06:01 +08:00
dingyuntian 02d3d80f46 Merge branch 'feature/转场功能更新' into 'develop'
feat: 添加全局屏幕转场面板

See merge request aibis-dream/aibis-dream!739
2026-07-16 09:58:38 +00:00
dingyuntian 3eea7e9ca6 docs(dialog): 添加 Yarn 全局转场指令表
- 列出 fade_in/out、fade_in_white、flash_white、
  wipe_in/out、shutter_in/out 等指令的用法与示例
2026-07-16 17:57:06 +08:00
dingyuntian 56045b4bcd fix(fade-kit): 修复 FadeKit 空引用和零时长异常
- 增加 Image 空引用保护
- 处理 duration 小于等于 0 的边界情况
- 动画开始前调用 DOKill 避免冲突
- 使用 DOFade 替代 DOBlendableColor,行为更明确
2026-07-16 17:56:59 +08:00
dingyuntian f51a670239 refactor: 将淡入淡出迁移到 ScreenTransitionPanel
- 将 BaseYarnCommand、FixSystemNew 各状态、GameManager、
  SaveSystem、PlayToolPanel 中的 PlayToolPanel 遮罩调用
  统一迁移到 ScreenTransitionPanel
- 从 PlayToolPanel 移除 _fadeImage 及相关 fade 方法
- 从 Play Tool Panel Prefab 移除 Fade Image 子对象
2026-07-16 17:56:51 +08:00
dingyuntian 4100232162 feat: 添加全局屏幕转场面板
- 新增 ScreenTransitionPanel,支持 fade_in/out、fade_in_white、flash_white、
  wipe_in/out、shutter_in/out 等多种转场效果
- 新增 Screen Transition Panel Prefab 并在 Persistence 场景挂载
- UIManager 启动时自动显示该面板
- BaseYarnCommand 新增对应的 Yarn 全局转场指令
2026-07-16 17:56:33 +08:00
bottlefish 51fb062225 build(config): 更新项目构建版本 2026-07-16 17:55:05 +08:00
bottlefish e0318026d7 scene(ui): 调整持久场景终端界面配置 2026-07-16 17:54:50 +08:00
bottlefish cc354cb37d scene(scene-flow): 更新剧情场景流转配置 2026-07-16 17:54:33 +08:00
bottlefish eaba7ea5ab fix(dialog): 修正时段初始化与角色气泡 2026-07-16 17:54:06 +08:00
bottlefish 5ab8498be5 art: 更新医生与英里角色资源 2026-07-16 17:53:51 +08:00
bottlefish fa79a4bed1 art(bar): 更新调酒妹动画与状态机 2026-07-16 17:53:31 +08:00
bottlefish f0a92553fb feat(kite): 完善风筝回合动态表现 2026-07-16 17:53:08 +08:00
bottlefish 85de341cd2 fix(ui): 修复梦境终端输入法交互 2026-07-16 17:52:49 +08:00
bottlefish ea95111f2a feat(dream): 完善梦境门与星空聚焦演出 2026-07-16 17:52:34 +08:00
bottlefish 0039beb5f9 art(huoshan): 调整火山角色表情动画 2026-07-16 17:51:23 +08:00
bottlefish 96164e1887 feat(huoshan): 重构笑话发生器像素噪点表现 2026-07-16 17:51:01 +08:00
bottlefish 754d03b47d feat(fix-system): 优化损坏模块火花表现 2026-07-16 17:50:48 +08:00
bottlefish 3be0bf8201 feat(fix-system): 重构序章维修流程与面板启动表现 2026-07-16 17:50:35 +08:00
Ovid 60200afe9b debug:云的图片名引用错误 2026-07-16 02:15:57 +08:00
Ovid 69315f86f9 添加枯树大图 2026-07-15 22:06:02 +08:00
dingyuntian 8c795f4474 Merge branch 'develop' into feature/新动画系统 2026-07-15 20:21:37 +08:00
bottlefish 8c37a0bd09 Merge remote-tracking branch 'origin/feature/demo脚本调整' into 7.11春优化 2026-07-15 19:55:59 +08:00
bottlefish e9e21920f1 scene: 调整持久化场景默认对象状态 2026-07-15 19:42:30 +08:00
bottlefish 5c6cc5c4a6 scene(fix-system): 同步维修场景与温控器配置 2026-07-15 19:42:00 +08:00
bottlefish 61e3f52a38 fix(dialog): 修正夜间剧情环境初始化 2026-07-15 19:41:16 +08:00
bottlefish 5639bad84a yarn(peipei): 清理失效的模块高亮指令 2026-07-15 19:40:44 +08:00
bottlefish 3d86790bd7 yarn(huoshan): 清理模块高亮并修正阶段跳转 2026-07-15 19:40:17 +08:00
bottlefish fd77b5c248 feat(effects): 优化故障噪声与场景后处理表现 2026-07-15 19:39:43 +08:00
bottlefish fa66dbc7c9 feat(kite): 优化风筝镜头追赶与双捕捉表现 2026-07-15 19:39:12 +08:00
bottlefish 92087b81ef feat(dream): 添加梦境数据溢出与虚焦表现 2026-07-15 19:38:42 +08:00
bottlefish 1f931e987f fix(huoshan): 优化语言粒子布局与状态重置 2026-07-15 19:37:37 +08:00
bottlefish 7ace1f255a fix(fix-system): 修复维修场景鼠标视差偏移 2026-07-15 19:36:58 +08:00
bottlefish 21321074bf fix(fix-system): 优化温控器风扇停止流程 2026-07-15 19:36:24 +08:00
bottlefish 89850c75f3 feat(fix-system): 优化线缆与警报灯表现 2026-07-15 19:35:47 +08:00
bottlefish d075d1e0f8 feat(fix-system): 添加维修模块运行状态表现 2026-07-15 19:34:50 +08:00
bottlefish 161fe6ac7d test(dev-save): 支持从测试归档恢复阶段存档 2026-07-15 19:27:53 +08:00
bottlefish c38c2f75be fix(save): 修复跨章节存档恢复校验 2026-07-15 19:27:18 +08:00
Ovid 52573095f7 Merge branch 'develop' into feature/demo脚本调整 2026-07-15 17:40:59 +08:00
Ovid ac6ded9f66 脚本调整上传 2026-07-15 17:40:29 +08:00
dingyuntian 8a26091590 Merge branch 'feature/录制模式相关改动' into 'develop'
fix(dialog): 修复点击 UI 时对话被误推进

See merge request aibis-dream/aibis-dream!738
2026-07-15 08:53:25 +00:00
dingyuntian 06d85d6460 refactor(ui): 移除 InfoPanel 的 I 键变量栏切换
删除 Update 中通过 I 键切换 variableBar 显隐的调试代码,
避免与现有输入冲突并保持面板职责单一。
2026-07-15 16:52:08 +08:00
dingyuntian 91ffbeb130 feat(ui): 添加录制工具面板
新增 RecordingToolPanel,按 Ctrl+R 唤出侧边栏,可独立控制光标、
版本号、Top Bar 与对话视觉的显隐。移除旧的
UIEventEnum.RecordingModeChanged 事件,改为各子系统提供显式
SetVisible API。在 Persistence 场景中配置面板层级与四个开关。
2026-07-15 16:51:55 +08:00
dingyuntian b14599d18c fix(dialog): 修复点击 UI 时对话被误推进
在 LineAdvanceInput 中通过 UIManager.IsPointerOverUI 判断
鼠标是否位于主 UI Canvas 的射线接收元素上,避免点击录制工具
等 UI 时推进对话。
2026-07-15 16:51:11 +08:00
dingyuntian 53a4fc86c4 feat: 设置修改,方便跑单元测试 2026-07-15 15:14:55 +08:00
dingyuntian 079d57e295 chore: 动画系统原型 2026-07-15 15:14:32 +08:00
dingyuntian 5f8ffb7f78 docs: 动画系统文档 2026-07-15 15:14:18 +08:00
dingyuntian 85a3fc9e14 feat(frameAnima): 动画系统前四个阶段 2026-07-15 15:13:56 +08:00
bottlefishandCursor ddb3aba4e5 art: 更新开盖动画资源
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 14:04:19 +08:00
bottlefishandCursor 1ee2a949e9 art(clinicout): 更新外出医生动画
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 14:02:38 +08:00
bottlefishandCursor dd66a52d65 scene: 将 Persistence 开场对话切到 Day2_FIRST_HS1
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 14:00:48 +08:00
bottlefishandCursor 59bffeab06 test(dev-save): 重建 Huoshan1 阶段存档目录与 catalog
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 13:58:01 +08:00
bottlefishandCursor 423a3954dd feat(dev-save): 强化 Huoshan 阶段存档提升与跳转校验
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 13:57:21 +08:00
bottlefishandCursor 4ab2f83395 yarn(huoshan): 用阶段就绪节点替换中段存档点
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 13:55:33 +08:00
Ovid de85ca2944 录屏版本提交 2026-07-14 13:45:37 +08:00
bottlefishandCursor 9f6c926dcc test(dev-save): 按叙事顺序更新 Huoshan1 测试存档
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 22:55:55 +08:00
bottlefishandCursor d101864397 yarn(huoshan): 规范节点标签并在 Stage 入口补存档
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 22:55:43 +08:00
bottlefishandCursor 1cc5ac32aa scene(huoshan): 同步旋钮三态与发条触发角
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 22:55:27 +08:00
bottlefishandCursor 431401c52f fix(huoshan): 修复发条终点判定与越界拖拽
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 22:55:12 +08:00
bottlefishandCursor 0ed1253a40 feat(huoshan): 旋钮改为三态切图外观
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 22:54:58 +08:00
bottlefishandCursor 4f59e41b03 feat(subway): 添加车厢加速度驱动与扶手摆动
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 22:54:43 +08:00
bottlefishandCursor df6f8c6ec4 yarn: 用 Timeline 替换英里登场与天桥 CG 静图
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 22:54:15 +08:00
bottlefishandCursor d00edd7bb8 scene: 登记叙事 Timeline 并挂入场景
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 22:53:59 +08:00
bottlefishandCursor 1be7b12deb art: 添加英里初登场与天桥夕阳 Timeline 资源
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 22:53:24 +08:00
bottlefishandCursor 51b8d86603 fix(dialog): 兼容 load_scene 多余淡入时长参数
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 22:53:10 +08:00
bottlefishandCursor c4dbdc098b feat(timeline): 添加 wait_timeline 以支持黑屏起播
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 22:52:54 +08:00
dingyuntian 19b6b9c4e2 fix: 章节编辑器问题修复 2026-07-13 22:24:39 +08:00
dingyuntian 92e8cd2f13 docs: 动画文档 2026-07-13 22:24:16 +08:00
bottlefishandCursor 26cfa5d886 test(dev-save): 添加 Day2Sleep 测试存档与晋升脚本
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 20:57:03 +08:00
bottlefishandCursor b98214632e feat(dream): 实现风筝镜头补偿与虚焦驱动
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 20:56:51 +08:00
bottlefishandCursor 2c74ec2030 feat(tarot): 优化摊牌缩放与逆位动画节奏
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 20:56:33 +08:00
bottlefishandCursor 71bfad4dae scene(huoshan): 同步表达与销售模块场景参数
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 20:56:23 +08:00
bottlefishandCursor c601dda9de fix(huoshan): 调整销售滑片角度并完善发条调试可视化
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 20:56:14 +08:00
bottlefishandCursor 1422161844 feat(huoshan): 增强表达模块干扰表现与完成释放效果
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 20:56:00 +08:00
Ovid a494cf99cf D1S梦境调整 2026-07-13 19:42:33 +08:00
bottlefishandCursor ee1d100fb5 chore(render): 启用 ShapesRenderFeature
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 14:25:23 +08:00
bottlefishandCursor d0a08986c6 yarn(huoshan): 调整销售模块文案并补充对话节拍
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 14:24:54 +08:00
bottlefishandCursor 0d04d1a68d scene(huoshan): 同步发条与平静场场景参数
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 14:24:29 +08:00
bottlefishandCursor 52e0e9002f fix(huoshan): 优化发条完成判定并防止销售轮次重复触发
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 14:24:05 +08:00
bottlefishandCursor 4b3260dfcd feat(huoshan): 为聚焦文本添加 Glitch 平静场
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 14:23:41 +08:00
bottlefishandCursor 6689effb87 feat(ui): 为旋钮交互添加旋转光标状态
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 14:22:17 +08:00
bottlefishandCursor b062ab4ddc feat(dev-save): 用通用跳转工具替换佩佩存档跳转
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 14:21:51 +08:00
bottlefishandCursor 4104de4166 scene(persistence): 调整启动对话与单行框状态
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 19:41:17 +08:00
bottlefishandCursor 425cd1e695 chore(tmp): 补充 Fallback 动态字形数据
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 19:40:59 +08:00
bottlefishandCursor a48a6bf6ad scene(peipei): 同步后处理材质并调整场景配置
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 19:40:45 +08:00
bottlefishandCursor 98c558e800 feat(render): 配置 VolFx Ascii 后处理通道
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 19:40:30 +08:00
bottlefishandCursor cd00a524bb fix(heatmap): 优化温度过渡并修复热点尺寸
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 19:40:10 +08:00
dingyuntian 6d12bceb36 yarn: Yarn冲突处理 2026-07-11 16:25:58 +08:00
dingyuntian 52d7656c30 docs: 动画文档未完成版 2026-07-11 16:17:09 +08:00
dingyuntian bd3988424f Merge branch 'feature/录制模式' into 'develop'
feat(record): 增加录制模式,按ctrl+R

See merge request aibis-dream/aibis-dream!737
2026-07-11 07:29:43 +00:00
dingyuntian 49df75279d Merge branch '风筝交互优化' into 'develop'
art: 添加风筝分层底图与风筝层素材

See merge request aibis-dream/aibis-dream!736
2026-07-11 05:18:01 +00:00
dingyuntian 2a43d0af81 Merge branch '7.3春优化' into 'develop'
fix(camera): 场景卸载后重置机位枚举避免读档跳过切换

See merge request aibis-dream/aibis-dream!735
2026-07-11 05:17:39 +00:00
bottlefishandCursor 66672f5188 docs: 补充风筝命令使用指南
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 12:32:17 +08:00
bottlefishandCursor 0590e755ee yarn(dialog): 调整 D2S 风筝乱度与高潮表现
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 12:31:55 +08:00
bottlefishandCursor 467dbfdf84 feat(scene-mgmt): 优化风筝与轱辘交互动效
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 12:31:30 +08:00
bottlefish bc14705fb3 yarn(dialog): 优化 D2S 风筝失控表现 2026-07-11 01:08:38 +08:00
bottlefish 6e939d2a72 feat(scene-mgmt): 添加风筝连续表现指令 2026-07-11 01:07:54 +08:00
bottlefishandCursor 2792d8fafa fix(ui): 修复单行对话框未激活时空引用
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 22:33:28 +08:00
bottlefishandCursor 024eecb404 yarn: 风筝段落接入精灵动效命令
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 22:33:17 +08:00
bottlefishandCursor 8d1f072d55 feat(dialog): 添加精灵动效 Yarn 命令
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 22:33:05 +08:00
bottlefishandCursor 18d6d970f9 feat(scene-mgmt): 实现风筝精灵分层动效系统
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 22:32:52 +08:00
bottlefishandCursor a1bfa4c8b8 art: 添加风筝分层底图与风筝层素材
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 22:32:38 +08:00
dingyuntian ea6b75d73d feat(record): 增加录制模式,按ctrl+R 2026-07-10 18:58:47 +08:00
bottlefish 4eb9aacc47 Merge branch 'develop' into 7.3春优化 2026-07-09 15:30:40 +08:00
dingyuntian 892912e707 Merge branch 'bugfix/火山修复' into 'develop'
fix: 火山修复

See merge request aibis-dream/aibis-dream!734
2026-07-09 07:16:55 +00:00
dingyuntian 3fefa18c65 fix: 火山修复 2026-07-09 15:16:17 +08:00
bottlefish 233cedd528 Merge branch 'develop' into 7.3春优化 2026-07-09 14:19:28 +08:00
bottlefishandCursor b0ece3e0f1 docs(fixsystem): 补充 PhysicCable 实现说明与渲染方案对比
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 14:18:47 +08:00
bottlefishandCursor 8a7b2d0ed2 feat(cable): 优化插线手感与线缆表现
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 14:04:48 +08:00
bottlefishandCursor c905eb4625 scene(peipei): 同步佩佩维修场景线缆参数
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 12:54:54 +08:00
bottlefishandCursor 4a66ba766e fix(cable): 固定插头与线缆排序并移除拖拽切层
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 12:54:45 +08:00
bottlefishandCursor 4ded2b8eab feat(cable): 线缆改用 Mesh 渲染并优化插接物理
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 12:54:36 +08:00
bottlefishandCursor f6997e8816 feat(cable): 新增单 Mesh 线缆渲染器
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 12:54:28 +08:00
bottlefishandCursor 6a5e8eecc9 docs(fix-system): 添加线缆物理移植分析文档
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 12:54:16 +08:00
bottlefish d0f4a1cf09 art(prototype): 添加线缆原型截图素材 2026-07-09 00:52:01 +08:00
bottlefish e1f5484e02 feat(prototype): 更新线缆物理交互原型 2026-07-09 00:51:45 +08:00
bottlefish 2e7c87c886 fix(fix-system): 优化线缆物理拖拽和像素风渲染 2026-07-09 00:51:34 +08:00
bottlefish f273569e7c feat(cable): 线缆优化 2026-07-08 22:31:21 +08:00
bottlefishandCursor 68837581af chore(prototype): 添加线缆切线锁定 HTML 原型
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 20:46:14 +08:00
bottlefishandCursor d8056155c1 chore(prototype): 添加 2D 线缆物理交互 HTML 原型
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 20:45:58 +08:00
dingyuntian 2a8d82fd2e art(ui): 更新字体 SDF 资源
- 更新 ChillBitmap 16px 与 WenQuanYi Bitmap Song 14px 的 SDF 图集
2026-07-08 15:21:33 +08:00
dingyuntian 558706ee08 scene(render): 清理 Renderer2Dtest 中 VolFx 特性
- 移除 VolFx Renderer Feature 及相关引用
2026-07-08 15:21:27 +08:00
dingyuntian 65d895798f scene(camera): 添加 Dream Camera 镜头混合规则
- 新增从任意相机切换到 Dream Camera 的混合配置
2026-07-08 15:21:23 +08:00
dingyuntian a305e5ef42 scene(open-fix): 调整 OpenFixScene 相机与对象位置
- 降低虚拟相机优先级
- 微调 Prefab 本地坐标与路径点位置
2026-07-08 15:21:18 +08:00
dingyuntian 0babb520de yarn(dialog): 修正火山 Stage2 语言标签与运算符格式
- 将 baseLanguage 从 zh-CHS 修正为 zh-Hans
- 补全 $TaskToDo 减法运算符两侧空格
2026-07-08 15:21:13 +08:00
dingyuntian 0265de117a docs(log-kit): 添加日志系统改进留档
- 记录背景、已完成内容、文件格式、平台默认配置
- 说明 GameLog 用法与后续待补足项
2026-07-08 15:21:05 +08:00
dingyuntian 11d88c146c feat(log-kit): 重构文件日志系统支持异步结构化日志
- 使用 ConcurrentQueue + 后台线程批量写入
- 新增 LogLevel、LogCategory、LogEntry、LogFormatter
- 新增 GameLog 业务日志入口
- 新增 LogSettingsProfiles 平台化配置
- 支持按 session 分文件、大小轮转与队列满时低优先级丢弃
2026-07-08 15:20:53 +08:00
bottlefishandCursor b5aab4edc1 scene(peipei): 同步佩佩维修场景线缆布局
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 22:21:56 +08:00
bottlefishandCursor 3092c3554b feat(fix-system): 优化线缆渲染与拖拽排序
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 22:21:29 +08:00
bottlefishandCursor 7d16bfe7bc test(fix-system): 扩展佩佩开发档读档跳转菜单
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 20:22:22 +08:00
bottlefishandCursor d9c8e85f2b fix(save): 读档前恢复 timeScale 并用 unscaledTime 淡入
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 20:21:52 +08:00
bottlefishandCursor b612a0763d fix(camera): 场景卸载后重置机位枚举避免读档跳过切换
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 20:21:46 +08:00
bottlefish c7c1571feb test(fix-system): 添加佩佩记忆跳转测试菜单 2026-07-06 14:50:03 +08:00
bottlefish 6469a6b746 fix(fix-system): 优化记忆调节接近目标视觉反馈 2026-07-06 14:49:42 +08:00
dingyuntian 1fbac2c182 Merge branch 'feature/主界面UI完善' into 'develop'
feat(editor): 添加 Yarn 本地化校验工具

See merge request aibis-dream/aibis-dream!733
2026-07-04 17:28:30 +00:00
dingyuntian ff29142761 chore: 版本号更新 2026-07-05 01:27:40 +08:00
dingyuntian 88dd2c95e8 fix(ui): 修复记录界面滚动问题 2026-07-05 01:27:08 +08:00
dingyuntian f87dcc687f feat(ui): 添加终端章节面板标题本地化 2026-07-05 00:16:34 +08:00
dingyuntian 84764d7b6b scene(ui): 清理 Persistence 场景废弃 Loading UI 2026-07-05 00:16:16 +08:00
dingyuntian 204b2c43fb fix(ui): 修复终端主菜单启动阶段音频通知时机 2026-07-05 00:12:20 +08:00
dingyuntian c5c8729704 fix(fix-system): 修复修复系统气泡加载前相机切换问题 2026-07-05 00:11:46 +08:00
dingyuntian c4ed431753 Merge branch 'develop' into feature/主界面UI完善 2026-07-04 23:24:09 +08:00
dingyuntian 2ff1df2331 Merge branch 'feature/mainMenuSFX' into 'develop'
Feature/main menu sfx

See merge request aibis-dream/aibis-dream!732
2026-07-04 15:23:33 +00:00
Ovid 36a1d783fb 拉太大了回来点 2026-07-04 23:22:44 +08:00
Ovid 9a31c71900 拉高hover音量 2026-07-04 23:21:57 +08:00
dingyuntian 5033923042 Merge branch 'develop' into feature/主界面UI完善 2026-07-04 23:14:13 +08:00
dingyuntian 5033bf0a3d feat(main-ui): 完善主界面场景与终端设置面板配置
- Persistence 场景绑定 TVOnSignal 并调整倒影 GameObject 参数
- 按任意键开始 Timeline 新增 Signal Track 与 TV 开关机动画关键帧
- 新增 TVOnSignal.signal 资源
- TerminalSettingPanelMini 文本添加本地化组件
2026-07-04 23:13:27 +08:00
dingyuntian c75cdc4bdf art(main-ui): 更新主界面电视界面与倒影动画资源
- 新增电视屏幕开关机动画与控制器
- 更新倒影 loop、环境 loop、海浪控制器以适配新资源
- 替换开始电视界面 aseprite 及导出配置
- 删除旧的开始界面倒影控制器与电视倒影资源
2026-07-04 23:13:13 +08:00
dingyuntian c6539c37b9 yarn(FP): 修复 FP Day1 翻译文本与缺失行
- FP_Day1_night 修复首行斜体闭合标签并同步 line id
- FP_Day1_sleep 英日 CSV 新增 Look inside 选项翻译
2026-07-04 23:12:58 +08:00
dingyuntian e015b756f6 feat(audio-kit): 终端 UI 菜单音效与暂停菜单环境音
- 新增 TerminalUIAudio 事件通知与 TerminalUIAudioBehaviour 组件
- TerminalButton/TerminalMenuItemView/SelectItem/SliderItem 接入悬停与点击音效
- AudioManager 监听 TerminalUIEvent 播放 menu_hover/menu_click
- 新增 PauseMenu 环境音状态及恢复逻辑
- 新增 MainMenuStage FMOD 参数控制
- BaseButton 调用基类 DoStateTransition 保证状态事件正确触发
- 为 TerminalChapterPanel Prefab 添加 TerminalUIAudioBehaviour
2026-07-04 23:12:47 +08:00
dingyuntian 04536c4b81 fix(editor): 修复 Aseprite JSON 尾随逗号解析问题
- 移除 AsepriteMeta 中未使用的 layers/slices 字段
- 新增 SanitizeJsonForUnity 正则清理尾随逗号
- 修复 Aseprite 导出 JSON 偶发尾随逗号导致 JsonUtility 解析失败
2026-07-04 23:12:30 +08:00
dingyuntian 7d7c8893fc feat(editor): 添加 Yarn 本地化校验工具
- 新增 Editor 窗口:AIBIS → Yarn 本地化校验
- 新增 BatchMode CLI:Tools/validate_yarn_l10n.py
- 新增文档:Docs/本地化校验工具.md
- 校验 Yarn 原文 string table 与各语言 CSV 的 line id 一致性
2026-07-04 23:12:21 +08:00
dingyuntian d3c6880ce6 Merge branch 'feature/mainMenuSFX' into 'develop'
Feature/main menu sfx

See merge request aibis-dream/aibis-dream!731
2026-07-04 15:06:22 +00:00
dingyuntian 82603c304d Merge branch 'feature/PP1S1本地化补充' into 'develop'
自己翻译了一句。善!

See merge request aibis-dream/aibis-dream!730
2026-07-04 15:05:43 +00:00
Ovid b89a674a28 mainMenu & pauseMenu SFX 2026-07-04 23:00:28 +08:00
Ovid d5c946d322 自己翻译了一句。善! 2026-07-04 21:20:04 +08:00
dingyuntian 575ebebfaf feat(build): 安卓打包版本号自增 2026-07-03 23:31:27 +08:00
dingyuntian aea6483849 feat(UI): 首页UI更新 2026-07-03 23:30:38 +08:00
dingyuntian 1b33124ec1 Merge branch 'feature/mus_0702' into 'develop'
去除临时文本

See merge request aibis-dream/aibis-dream!729
2026-07-03 07:12:31 +00:00
Ovid 76302f53c8 去除临时文本 2026-07-03 15:11:25 +08:00
dingyuntian 969bc2756c Merge branch 'bugfix/72Tap打包' into 'develop'
fix(tap): 安卓端打包

See merge request aibis-dream/aibis-dream!728
2026-07-02 13:23:15 +00:00
dingyuntian 599bb44763 fix(tap): 安卓端打包 2026-07-02 21:22:48 +08:00
dingyuntian 78ac7da055 Merge branch 'feature/mus_0702' into 'develop'
Feature/mus 0702

See merge request aibis-dream/aibis-dream!727
2026-07-02 11:40:32 +00:00
Ovid e1240f023d sfxdebug-D1S结束时海浪fade掉而不是硬关 2026-07-02 19:39:17 +08:00
dingyuntian 5933ae954d Merge branch 'feature/UI更新&安卓适配' into 'develop'
Feature/ui更新&安卓适配

See merge request aibis-dream/aibis-dream!726
2026-07-02 11:35:08 +00:00
dingyuntian e5a10ea0b7 fix(scene): 海报遮罩范围调整 2026-07-02 19:34:40 +08:00
Ovid d782a09671 textdebug 2026-07-02 19:34:32 +08:00
dingyuntian 440979264e Merge branch 'develop' into feature/UI更新&安卓适配 2026-07-02 19:32:13 +08:00
Ovid 39a9eb73bc textdebug 2026-07-02 19:32:02 +08:00
dingyuntian cb4264f26e Merge branch 'feature/mus_0702' into 'develop'
musupdate

See merge request aibis-dream/aibis-dream!724
2026-07-02 11:24:21 +00:00
dingyuntian c67fe1b32d Merge branch '7.2春优化' into 'develop'
fix(ui): 主界面加载前黑屏遮罩,就绪后再淡出

See merge request aibis-dream/aibis-dream!725
2026-07-02 11:23:50 +00:00
Ovid b8773d968e textdebug 2026-07-02 19:21:45 +08:00
dingyuntian 4776066cec build(android): 更新版本号与堆栈跟踪配置 2026-07-02 19:16:41 +08:00
dingyuntian 5305033016 fix(log-kit): 禁用 Android 真机日志写入 2026-07-02 19:16:35 +08:00
dingyuntian bd70442e45 style(fix-system): 补全 Cable 脚本文件末尾换行 2026-07-02 19:16:28 +08:00
dingyuntian a3dadd4d88 scene(peipei): 调整佩佩修复场景排序与线缆位置 2026-07-02 19:16:20 +08:00
dingyuntian 39ce23d5f3 yarn(dialog): 调整佩佩与序章节点保存标签及本地化 2026-07-02 19:16:14 +08:00
Ovid aa0f431955 textdebug 2026-07-02 19:16:06 +08:00
dingyuntian b98681acab fix(audio-kit): 修复 OpenFixScene 环境音状态 2026-07-02 19:15:52 +08:00
dingyuntian 37e9127260 feat(ui): 添加终端继续游戏入口 2026-07-02 19:15:32 +08:00
dingyuntian c5c90eb5a3 feat(localization): 支持设置语言跟随系统语言 2026-07-02 19:15:21 +08:00
dingyuntian 517a03bbc3 perf(save): 优化存档写入性能并支持关闭缩略图 2026-07-02 19:15:11 +08:00
Ovid cb6658e41a debug 2026-07-02 19:09:57 +08:00
Ovid e78d5eba23 新增EMPTYSCENE 2026-07-02 19:09:37 +08:00
bottlefishandCursor 478f431dd3 chore(ui): 移除移动端强制显示快进按钮
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 19:09:16 +08:00
bottlefishandCursor a1d972c074 fix(memory): 放宽调频成功判定阈值
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 19:09:12 +08:00
Ovid 0cf659cc20 破碎坠落时间差处理 2026-07-02 19:04:52 +08:00
bottlefishandCursor a5352ed250 chore(ui): 持久化场景启用并复位快进按钮布局
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 19:03:50 +08:00
bottlefishandCursor 18ab68f841 scene(peipei): 调整剪线特写 PPU 与场景路径坐标
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 19:03:45 +08:00
bottlefishandCursor 870f914950 fix(memory-shader): 补齐 UI Stencil 并修复 aspect 计算
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 19:03:37 +08:00
bottlefishandCursor c08021a2c8 fix(eye-shader): 采样 UV 饱和并统一 half 精度
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 19:03:32 +08:00
bottlefishandCursor 6ac7fcf9da fix(eye): 视口遮罩改用屏幕坐标并复用 Canvas 层级
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 19:03:26 +08:00
Ovid dfed55acbb 主界面audiomanager改到main menu 2026-07-02 18:46:02 +08:00
Ovid 982914f6e7 debug 2026-07-02 18:43:59 +08:00
Ovid 1583b96200 替换新病理分析图 2026-07-02 18:32:03 +08:00
Ovid 3432053239 D2N最后喝醉的时候糊明显一些 2026-07-02 17:59:12 +08:00
bottlefishandCursor d054a57e26 build(shader): Memory 与 Eye shader 加入 Always Included
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 17:56:57 +08:00
bottlefishandCursor 7884812131 fix(eye): 运行时检测 EyeUnifiedSprite 平台支持
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 17:56:51 +08:00
bottlefishandCursor 33be87215e fix(eye-shader): 模糊仅混 RGB 保留 alpha 并减移动端采样
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 17:56:50 +08:00
bottlefishandCursor f3c3994f5c fix(memory): 调频 shader 不可用回退 Volume 并绑定场景引用
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 17:56:41 +08:00
bottlefishandCursor bd94302714 fix(memory-shader): 修复调频模糊导致 alpha 变透明并优化移动端采样
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 17:56:33 +08:00
Ovid 5031906d70 D1N路斯改成你 2026-07-02 17:50:59 +08:00
Ovid bef36bb92f ambBar提升开头音量 2026-07-02 17:49:56 +08:00
Ovid 496ba885af prologueEnd玻璃声 2026-07-02 17:43:54 +08:00
bottlefishandCursor 3c7662679c chore(ui): 移动端临时显示快进按钮
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 17:36:45 +08:00
bottlefishandCursor 0832e4cac1 fix(eye): 遮罩 shader 失败时跳过全屏黑屏
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 17:36:39 +08:00
Ovid 668b735377 musupdate 2026-07-02 17:35:37 +08:00
bottlefishandCursor b26f7d0222 yarn(fp): 移除 Peipei1 模块高亮指令
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 16:38:11 +08:00
bottlefishandCursor 80d32a73d8 fix(ui): 主界面加载前黑屏遮罩,就绪后再淡出
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 16:38:06 +08:00
bottlefish fa3954bbbb 去除测试文本 2026-07-02 16:23:44 +08:00
dingyuntian b9167056cf Merge branch 'feature/UI更新&安卓适配' into 'develop'
Feature/ui更新&安卓适配

See merge request aibis-dream/aibis-dream!723
2026-07-02 06:58:20 +00:00
dingyuntian 784e1e8f0d fix(bloom):bloom问题修复 2026-07-02 14:55:23 +08:00
dingyuntian 76d620d8c3 build(android): 更新版本号与图标 2026-07-02 14:53:09 +08:00
dingyuntian 1918c8c53b chore(config): 修改默认窗口模式为全屏 2026-07-02 14:53:07 +08:00
dingyuntian 07f1e3f5bc yarn(dialog): 更新序章与 Day1/Day2 环境音切换及节点标签 2026-07-02 14:53:05 +08:00
dingyuntian fe8b4ea3c5 chore(scene-so): 更新场景章节与出口名称配置 2026-07-02 14:53:03 +08:00
dingyuntian a1767fc6d3 scene(persistence): 调整持久化场景 UI 布局 2026-07-02 14:53:00 +08:00
dingyuntian 49a51b6ccb chore(render): 清理测试渲染器 VHS 后效 2026-07-02 14:52:58 +08:00
dingyuntian 08d09bf31d art(common): 添加通用 LOGO 资源 2026-07-02 14:52:56 +08:00
dingyuntian 4e6c6dac6e art(ui): 替换功能 UI 资源并调整终端章节卡 2026-07-02 14:52:54 +08:00
dingyuntian a603ad928f refactor(ui): 移除 BoolButton 本地化文本依赖 2026-07-02 14:52:51 +08:00
dingyuntian bde1040c9d feat(audio-kit): 添加主菜单环境音状态 2026-07-02 14:52:49 +08:00
dingyuntian fcd0955ae7 fix(fix-system): 修正线缆连接点与火花 VFX 参数 2026-07-02 14:52:44 +08:00
dingyuntian b1981431f6 Merge branch 'feature/UI更新&安卓适配' into 'develop'
Feature/ui更新&安卓适配

See merge request aibis-dream/aibis-dream!722
2026-07-02 02:54:34 +00:00
dingyuntian 7c15663bf2 Merge branch 'develop' into feature/UI更新&安卓适配 2026-07-02 10:49:12 +08:00
dingyuntian 9f5ea54813 fix(audio): 修正 FMOD Android 平台导入设置 2026-07-02 02:45:41 +08:00
dingyuntian 63d755ee05 build(android): 配置 Android 构建模板、包名与密钥库 2026-07-02 02:45:30 +08:00
dingyuntian 29267659b9 fix(android): 移动端隐藏自定义光标 2026-07-02 02:45:20 +08:00
dingyuntian c912ac89c5 chore(ui): 删除终端旧版构建工具与Timeline 2026-07-02 02:45:10 +08:00
dingyuntian b2174985ed feat(localization): 添加终端界面本地化文本 2026-07-02 02:44:55 +08:00
dingyuntian 5e64ab4c30 scene(ui): 配置终端面板场景实例 2026-07-02 02:44:37 +08:00
dingyuntian cb557c14cf art(ui): 更新终端界面美术资源与预制体 2026-07-02 02:44:28 +08:00
dingyuntian 3e2d1e931a feat(ui): 重构终端面板,拆分主菜单与局内终端 2026-07-02 02:44:02 +08:00
dingyuntian 742125c112 Merge branch 'feature/全文捉虫' into 'develop'
Feature/全文捉虫

See merge request aibis-dream/aibis-dream!721
2026-07-01 18:41:32 +00:00
Ovid a8fcb42f85 Merge branch 'develop' into feature/全文捉虫 2026-07-02 02:40:42 +08:00
Ovid 7ddada9726 musicdebug 2026-07-02 02:38:39 +08:00
Ovid b89fb35559 music debug 2026-07-02 02:37:25 +08:00
Ovid f726472d8a text debug,接音乐 2026-07-02 02:09:15 +08:00
dingyuntian 2ef0cff2f1 Merge branch '7.1春细节优化' into 'develop'
fix(eye): 修复视口遮罩不可见并避免遮挡系统 UI

See merge request aibis-dream/aibis-dream!720
2026-07-01 14:04:29 +00:00
bottlefishandCursor 58068d1a28 art(peipei): 更新佩佩视角屋子导出资源
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 21:13:53 +08:00
bottlefishandCursor 2f1bd5707e yarn(eye): 移除重复聚焦与上色命令
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 21:13:42 +08:00
bottlefishandCursor af3c39dcb3 fix(eye): 修正视觉聚焦上色流程与 indicator 色值
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 21:13:07 +08:00
bottlefishandCursor d6e7afe4b3 fix(memory): 增强记忆调谐清晰度与扭曲表现
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 21:12:51 +08:00
dingyuntian 73e6b86579 fix(android): 安卓兼容 2026-07-01 19:48:33 +08:00
bottlefish df70089ecc 替换塔罗素材 2026-07-01 19:40:09 +08:00
Ovid 79a4976d42 Merge branch 'develop' into feature/全文捉虫 2026-07-01 19:16:17 +08:00
dingyuntian 107a2411e3 Merge branch 'feature/UI更新' into 'develop'
fix(scene-mgmt): 修复场景加载后立即开对话的时序问题

See merge request aibis-dream/aibis-dream!719
2026-07-01 11:14:35 +00:00
dingyuntian 41e02d48d8 fix(chapter): 章节调整 2026-07-01 19:14:15 +08:00
dingyuntian 3b96003c03 Merge branch 'develop' into feature/UI更新 2026-07-01 19:09:45 +08:00
dingyuntianandCursor f4ee41c522 scene(ui): 更新 Terminal UI 预制体与 Persistence 场景
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 19:03:38 +08:00
dingyuntianandCursor 024202b19e feat(ui): 添加 TerminalPanel 统一终端 UI 导航并移除旧面板
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 19:03:14 +08:00
dingyuntianandCursor 4deb565882 refactor(ui): 重构 SelectItem 与 SliderItem 适配 Terminal 设置页
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 19:02:49 +08:00
bottlefishandCursor e6dfab3b51 scene(peipei): 同步视觉模块眨眼演出参数
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 18:28:42 +08:00
bottlefishandCursor 9124a4bd6b feat(eye): 移动结束后先眨眼再触发聚焦或想象效果
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 18:28:36 +08:00
bottlefishandCursor aaf248a0ef feat(eye): 添加聚焦/想象演出眨眼调参项
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 18:28:31 +08:00
bottlefishandCursor 7177039f71 feat(eye): 视口遮罩演出眨眼序列与自动眨眼冷却重置
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 18:28:24 +08:00
Ovid db73e13bcc Merge branch 'develop' into feature/全文捉虫 2026-07-01 18:04:58 +08:00
Ovid 8ddad4fcfc 整体串通 2026-07-01 18:04:48 +08:00
bottlefishandCursor 8f6616920c docs(agent): 添加 AgentBridge 自动化计划草案
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 17:44:37 +08:00
bottlefishandCursor 660f8e24ee fix(eye): 修复视口遮罩不可见并避免遮挡系统 UI
- 将 overlay Canvas 设到 UI 层以匹配 UI Camera culling mask
- sortingOrder 设为 0,位于 UI Canvas(1) 与 Dialog(3) 之下
- 修正 EnterModule 与 Start 时序,避免模块激活后被误关

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 17:39:24 +08:00
bottlefish 929e6f0086 清理佩佩场景 2026-07-01 17:04:24 +08:00
dingyuntian 603dc9dad3 chore(plugin): 删除没有用到的UI插件 2026-07-01 14:27:37 +08:00
dingyuntianandCursor a05d5d476c feat(ui): 添加 Terminal 风格主界面 UI 系统
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 12:06:11 +08:00
dingyuntianandCursor da436479db fix(scene-mgmt): 修复场景加载后立即开对话的时序问题
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 12:05:46 +08:00
dingyuntian 93a5076369 feat(open): 气泡和相机设置迁移 2026-07-01 12:01:56 +08:00
dingyuntian e50e2f1cd5 art(UI): 初始界面素材 2026-07-01 12:00:48 +08:00
dingyuntian cf016ecc93 fix(font): 字体集缺字 2026-07-01 11:56:23 +08:00
dingyuntian 39cf031761 Merge branch '佩佩记忆部分优化' into 'develop'
feat(peipei-eye): 新增 EyeUnifiedSprite 着色器与材质

See merge request aibis-dream/aibis-dream!718
2026-06-30 13:23:23 +00:00
bottlefishandCursor 6d6184e2de scene(peipei): 同步 EyeViewportOverlay sortingOrder
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 21:21:00 +08:00
bottlefishandCursor 9867c84ccf fix(eye): 视觉模块 overlay 不遮挡系统 UI 与对话框
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 21:20:54 +08:00
bottlefishandCursor e4d7b03a89 scene(peipei): 同步 GameContent 资源迁入后的 PeipeiFixScene 引用
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 21:04:20 +08:00
bottlefishandCursor e5306f2b82 art(peipei): 佩佩维修关美术迁入 Scene_PeipeiFix/Art
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 20:55:35 +08:00
bottlefishandCursor 46cdbd3dd2 art(peipei): UFSearchMemory 材质迁入 Scene_PeipeiFix
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 20:55:26 +08:00
bottlefishandCursor 58488547ce refactor(peipei): Eye 材质迁入 Scene_PeipeiFix/Material/Eye
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 20:55:20 +08:00
bottlefishandCursor 6f8e306fb6 art(memory): maskTexture 迁入 Feature_Memory/Render
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 20:55:12 +08:00
bottlefishandCursor 4fa3aac335 art(memory): overlay 笔刷迁入 Feature_Memory/Art/Overlay
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 20:55:04 +08:00
bottlefishandCursor 568e6ff867 refactor(memory): 记忆材质迁入 Feature_Memory/Material
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 20:54:56 +08:00
bottlefishandCursor eefce92f58 scene(peipei): 配置记忆 overlay 与 MemoryProcess 引用
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 20:39:50 +08:00
bottlefishandCursor d33d4b9dc4 perf(memory): 调整记忆高斯模糊卷积核
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 20:39:44 +08:00
bottlefishandCursor c761e9afd7 refactor(memory): 精简 MemoryForFade 材质属性
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 20:39:36 +08:00
bottlefishandCursor e884f4fb42 art(memory): 新增 MemoryOverlay 材质
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 20:39:31 +08:00
bottlefishandCursor f35edeabe8 feat(memory): 搜索 overlay 遮罩与调频目标采样优化
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 20:39:26 +08:00
bottlefishandCursor d001c824a0 feat(memory): 暴露滑条起始值与最大值供目标采样
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 20:39:08 +08:00
Ovid d865244a02 +br和waitfor,语病检查 2026-06-30 19:40:51 +08:00
bottlefishandCursor a97b00072a scene(peipei): 更新 PeipeiFixScene Eye/UF 引用
同步 EyeTarget 运行时材质命名与 UF/错色 overlay 场景配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 16:14:38 +08:00
bottlefishandCursor 06be1d6eef fix(peipei): UF_spriteIn 从 Yarn 变量解析 EyeTarget
切换 UF 模式后 CurrentTarget 可能为空,回退读取 $currentEyeTarget。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 16:14:33 +08:00
bottlefishandCursor 6dbd6917f9 refactor(peipei): UF 效果改由后处理 Volume 驱动
将 UF 想象/记忆/Ascii 状态从 unified shader weight 迁移至 Volume weight  tween。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 16:14:22 +08:00
bottlefishandCursor 57e20d0dce fix(peipei): UF overlay fade 前激活 Sprite
fade in 时启用 overlay GameObject,fade out 完成后隐藏。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 16:14:16 +08:00
bottlefishandCursor 29a1c7d7cb feat(peipei): 错色 Error overlay 淡入淡出驱动
自动绑定 sibling Error Sprite,并在 WrongColor 状态切换时同步 overlay alpha。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 16:14:10 +08:00
dingyuntian e6b24556b5 Merge branch 'feature/PP1病例分析上传新素材' into 'develop'
Feature/pp1病例分析上传新素材

See merge request aibis-dream/aibis-dream!717
2026-06-30 07:44:53 +00:00
bottlefishandCursor 04b441d977 feat(peipei-eye): 新增视口椭圆遮罩与眨眼效果
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 13:55:57 +08:00
bottlefishandCursor 488149e8d4 fix(peipei-eye): 编辑模式下改用 sharedMaterial 避免材质泄漏
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 13:55:51 +08:00
bottlefishandCursor 7a14cc52e9 scene(peipei): 配置 unified eye 与视觉调参
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 12:54:56 +08:00
bottlefishandCursor cc069b1da3 fix(fix-system): 调整视觉模块进入时 UI 时序
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 12:54:42 +08:00
bottlefishandCursor 2fcb998e6f refactor(peipei-eye): 视觉模块平移改用 Cinemachine 锚点
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 12:54:35 +08:00
bottlefishandCursor 140e05b4d1 feat(peipei-eye): UF 效果迁移到 unified sprite 权重
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 12:54:29 +08:00
bottlefishandCursor 07d330f6db refactor(peipei-eye): 重构 Eye 上色为 unified 驱动并集中调参
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 12:54:23 +08:00
bottlefishandCursor 4f6f5a1ae9 feat(peipei-eye): 新增 EyeUnifiedSprite 着色器与材质
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 12:54:08 +08:00
bottlefish db73b64980 Merge branch '佩佩记忆部分优化' of http://114.67.212.218:8011/aibis-dream/aibis-dream into 佩佩记忆部分优化 2026-06-29 22:58:45 +08:00
bottlefish ece0d4ed8e scene(peipei): 配置记忆画面UI显示 2026-06-29 22:58:01 +08:00
bottlefish 268bdd19d9 feat(fix-system): 支持记忆画面使用UI显示 2026-06-29 22:57:14 +08:00
bottlefish 40358dd8a1 feat(fix-system): 支持记忆画面使用UI显示 2026-06-29 22:56:28 +08:00
bottlefish f42ec20dc8 art(ui): 更新默认字体备用字形 2026-06-29 22:23:43 +08:00
bottlefish ca3d4cb3cb scene(peipei): 更新佩佩记忆场景配置 2026-06-29 22:23:26 +08:00
bottlefish 814b659c39 feat(fix-system): 迁移记忆调频到精灵材质 2026-06-29 22:23:10 +08:00
bottlefish c392c81d31 build: 切换 Unity MCP 依赖分支 2026-06-29 22:22:10 +08:00
Ovid 681fe5fc98 autonext处理 2026-06-29 21:54:12 +08:00
Ovid 721e82d768 PP1病理分析上传新素材 2026-06-29 21:45:25 +08:00
dingyuntian 296ce4d657 Merge branch 'bugfix-showobj更新' into 'develop'
fix(ui): show_obj改进

See merge request aibis-dream/aibis-dream!716
2026-06-29 12:21:11 +00:00
dingyuntian 2389c9ef3e fix(ui): show_obj改进 2026-06-29 20:20:04 +08:00
dingyuntian 7e21d33cdd fix(anima): 动画片段及生成器修正 2026-06-29 20:09:39 +08:00
dingyuntian d2ecd1d846 Merge branch 'bugfix/showobj加上时间控制' into 'develop'
fix: 修正场景加载参数并添加展示隐藏时间控制

See merge request aibis-dream/aibis-dream!715
2026-06-29 11:32:32 +00:00
dingyuntian f25674a043 fix: 修正场景加载参数并添加展示隐藏时间控制 2026-06-29 19:31:48 +08:00
dingyuntian d1eab5612d Merge branch '记忆检查shader优化' into 'develop'
记忆检查shader优化

See merge request aibis-dream/aibis-dream!714
2026-06-29 10:17:34 +00:00
dingyuntian 1e60b0005e Merge branch 'develop' into 记忆检查shader优化 2026-06-29 17:52:47 +08:00
dingyuntian 6d262a8796 Merge branch 'develop' into feature/存档 2026-06-29 17:47:05 +08:00
dingyuntian 32d44b8119 chore(peipei): 整理佩佩场景美术资源目录 2026-06-29 17:45:55 +08:00
dingyuntian 7edda0bd6e art(peipei): 调整剪线像素化资源 2026-06-29 17:45:42 +08:00
dingyuntian 2c9f609616 feat(main-ui): 添加开始界面背景与启动流程调整 2026-06-29 17:45:32 +08:00
dingyuntian c0325b0a35 Merge branch 'D1S流程加锁' into 'develop'
Lock Day1 sleep flow and allow early ring pickup

See merge request aibis-dream/aibis-dream!713
2026-06-29 09:39:53 +00:00
bottlefish 228f91e8ab Lock Day1 sleep flow and allow early ring pickup 2026-06-29 17:04:21 +08:00
bottlefish a36dc7c003 Fix mobile render texture black screens 2026-06-29 16:34:26 +08:00
dingyuntian e75bd5e552 Merge branch 'feature/存档' into 'develop'
refactor(save): 合并 SaveSystem 过度拆分的文件

See merge request aibis-dream/aibis-dream!712
2026-06-29 08:03:26 +00:00
dingyuntian 05855e2a9f scene(demo): 调整 Day1_night 初始场景 2026-06-29 16:00:24 +08:00
dingyuntian f188f4c1fc yarn(dialog): 调整 FP 流程节点标签与存档边界 2026-06-29 16:00:08 +08:00
dingyuntian 870bf96c0f docs(save): 更新存档系统 README 与设计方案 2026-06-29 15:59:50 +08:00
dingyuntian 67f3abd825 test(save): 更新验证窗口模拟 OnNodeStart 自动档判定 2026-06-29 15:59:35 +08:00
dingyuntian 3775aed368 feat(save): 支持无节点 anchor 恢复与延迟屏幕遮罩应用 2026-06-29 15:59:21 +08:00
dingyuntian 08b8f34bde fix(timeline-kit): 避免未播放 Timeline 读档时被 Reset 2026-06-29 15:59:05 +08:00
dingyuntian e41756d0f0 fix(camera): 修复立即切换相机时 CustomBlend 仍生效 2026-06-29 15:58:51 +08:00
dingyuntian 2d43af4827 feat(fix-system): 支持 Fix 系统任务面板与身体模块状态读档恢复 2026-06-29 15:58:35 +08:00
dingyuntian 0f681b518e feat(save): 添加 Yarn 节点 InProgress 跟踪与 detour 续跑去重 2026-06-29 15:58:15 +08:00
dingyuntian 382b784605 feat(save): 扩展快照 DTO 支持任务面板与模块状态 2026-06-29 15:57:59 +08:00
bottlefish 5efb2ff157 fix: guard eye module input initialization 2026-06-27 00:34:25 +08:00
bottlefish 43465e8f34 feat: add memory tuning sprite shader 2026-06-26 23:59:10 +08:00
bottlefish bf416f604b fix: handle Yarn initial variable storage fallback 2026-06-26 23:57:38 +08:00
dingyuntian a5aa37ffcc refactor(prefab): 移除 ActorAnimaEx 废弃 actorData 字段 2026-06-24 22:45:42 +08:00
dingyuntian 009776eb32 docs(save): 更新存档规范与 Yarn 节点类型文档 2026-06-24 22:45:32 +08:00
dingyuntian b1fdec64e1 yarn(fp-day1): 调整节点标签与场景切换结构,适配存档边界 2026-06-24 22:45:21 +08:00
dingyuntian 43c3f8531d feat(editor): 新增 SaveSystem 测试窗口并更新验证工具读档入口 2026-06-24 22:45:11 +08:00
dingyuntian 5a0b7a047b feat(save-system): 新增 Yarn <<save>> 显式存档与测试归档模式 2026-06-24 22:45:00 +08:00
dingyuntian e90da7bab7 fix(save-system): 修复读档时 UI 与游戏状态未对齐 2026-06-24 22:44:43 +08:00
dingyuntian b53a3f45b7 refactor(fix-system): 移除 IFixSceneCue.SetArgs,改用 Director 与 Transition 传参 2026-06-24 22:44:01 +08:00
dingyuntian e4ec8fce57 refactor(save-system): EyeSystem 与 PunchTape 迁移为显式 DTO 快照 2026-06-24 22:43:49 +08:00
dingyuntian d780ff59ee refactor(fix-system): FixPanel 与 Cable 迁移为显式快照 2026-06-24 22:43:39 +08:00
dingyuntian 57b03aeb08 refactor(fix-system): BodyModule 持久化从 IData 改为显式快照 2026-06-24 22:43:28 +08:00
dingyuntian b7bfa28fd7 refactor(actor-kit): SubwayCenter 迁移为显式快照 2026-06-24 22:43:17 +08:00
dingyuntian 2a0c34fd7a refactor(actor-kit): Actor 体系迁移为 DTO 快照与异步恢复 2026-06-24 22:43:08 +08:00
dingyuntian 3746f4aeb6 feat(save-system): 新增维修与场景子模块快照 DTO 与 Provider 2026-06-24 22:42:56 +08:00
dingyuntian 5cae227fec feat(framework): 状态机与 AnimatorKit 支持读档即时恢复 2026-06-24 22:34:57 +08:00
dingyuntian d88f794759 refactor(save-system): 移除旧存档基础设施 DeepRepair、IData 与 LoadIndex 2026-06-24 22:34:47 +08:00
dingyuntian 67968b0102 scene: 同步场景与 Prefab 的导演、气泡和相机配置 2026-06-22 19:03:12 +08:00
dingyuntian d2c7837a74 refactor(timeline-kit): 清理 DirectorHandler 自动绑定冗余日志 2026-06-22 19:02:58 +08:00
dingyuntian 07850fcf1c feat: 添加 BubbleSlotKit 与 VirtualCamRegistry 集中管理气泡槽位和虚拟相机 2026-06-22 19:02:52 +08:00
dingyuntian 0368374a98 refactor(fix-system): 将 FixStateMachine 重构为 FixSceneDirector 导演模式 2026-06-22 19:01:02 +08:00
dingyuntian 749b9974c0 chore(meta): 添加项目 AI 助手元数据与技能定义 2026-06-22 14:49:23 +08:00
dingyuntian a434ffd7e0 docs(SaveSystem): 更新 P4 读档编排文档 2026-06-22 14:49:09 +08:00
dingyuntian a080f507d1 yarn(FP_Peipei1): 删除 Stage5 分析废弃节点 2026-06-22 14:48:55 +08:00
dingyuntian 50566a1d42 fix(timeline-kit): Addressable Timeline 恢复改为可等待协程 2026-06-22 14:48:43 +08:00
dingyuntian 67668f18e0 feat(SaveSystem): 读档编排 Phase+Barrier 与自动存档抑制 2026-06-22 14:48:30 +08:00
dingyuntian e2bdd8e0fa refactor: 同步化 Actor/Environment/Audio 快照还原接口 2026-06-22 14:48:14 +08:00
dingyuntian 6a289e0255 refactor(SaveSystem): 拆分 Provider 还原契约为 sync/async 接口 2026-06-22 14:47:59 +08:00
dingyuntian 1fe4c44f0c refactor(FixSystemNew): 拆分状态类到 States 目录 2026-06-22 14:47:43 +08:00
dingyuntianandCursor 3cee30f099 chore: 配置 Yarn Spinner 的 cSpell 忽略规则
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 14:35:03 +08:00
dingyuntianandCursor 4d8707cf4e docs(save): 补充瞬跳节点与存档边界设计说明
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 14:35:03 +08:00
dingyuntianandCursor fbb9e65338 yarn(dialog): 调整佩佩 FP 维修脚本存档边界 tag
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 14:34:42 +08:00
dingyuntianandCursor 0b0d22d009 feat(save): 验证工具增加运行时自动存档历史
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 14:34:29 +08:00
dingyuntianandCursor 0a6e892504 feat(save): 实现 Yarn tag 自动存档判定与异步写盘
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 14:34:19 +08:00
dingyuntian de671ff94c docs(yarn): 更新节点类型规范并新增维修流程规范 2026-06-17 12:51:10 +08:00
dingyuntian fc4b1c8805 fix(peipei): 切割线完成后跳转到新事件节点 2026-06-17 12:51:02 +08:00
dingyuntian 29e698c370 yarn(fp): 规范化维修流程节点标签 2026-06-17 12:50:52 +08:00
dingyuntian b30010a459 scene(save): 配置 Persistence 场景存档系统组件 2026-06-17 12:50:12 +08:00
dingyuntian 181eeb679b test(save): 添加存档系统编辑器验证工具 2026-06-17 12:50:03 +08:00
dingyuntian a633b47d5b fix(save): 快照捕获增加管理器单例空值保护 2026-06-17 12:49:55 +08:00
dingyuntian c55220d739 feat(save): 槽位元数据增加节点名记录 2026-06-17 12:49:46 +08:00
dingyuntian 2796f96215 chore(save): 彻底废弃 auto_save Yarn 命令 2026-06-12 19:58:28 +08:00
dingyuntian e01c8af449 feat(save): 添加可存点判定器与节点事件触发机制 2026-06-12 19:58:20 +08:00
dingyuntian 748c875e93 fix(save): 修复 SlotManager 云存档命名空间引用 2026-06-12 19:58:14 +08:00
dingyuntian afd062bda2 docs(save): 更新 SaveSystem README
同步 README 目录结构:
- 反映已合并的 P1 文件(SaveSnapshotSummary、SnapshotBootstrap 等)
- 补充 P2 新增文件(SlotTypes、SlotFileSystem、SlotManager、CloudSave)
- 更新 Provider 注册与 section 反序列化指引
2026-06-12 17:41:23 +08:00
dingyuntian d6912fb1a6 feat(save): 迁移存读档入口到槽位系统
将游戏流程接入 P2 槽位层:
- SaveRestoreOrchestrator:SaveToAutoSlot / CreateManualSlot / RestoreFromSlot
- GameManager:<<auto_save>> 调用新入口,新增 StartWithSlot 读档入口

保留 RestoreFromFile 旧档兼容路径。
2026-06-12 17:41:10 +08:00
dingyuntian 6fcf340230 feat(save): 添加槽位类型与云存档扩展层
实现 P2 槽位/落盘层核心:
- SlotTypes:槽位常量、sidecar 元数据、UI 视图模型
- SlotFileSystem:槽位路径工具与原子写
- SlotManager:自动档、手动档、读档、继续游戏、P1 迁移
- SlotThumbnailCapture:存档截图 helper
- CloudSave:Steam/TapTap 云存档适配器接口与协调器

在 ConstRef 中补充槽位、截图、文件名常量。
2026-06-12 17:40:57 +08:00
dingyuntian cc3e1dd138 refactor(save): 合并 SaveSystem 过度拆分的文件
将以下小文件合并到相关核心类中,减少文件数量并保持职责清晰:
- SaveSnapshotSummary -> SaveSnapshot
- SnapshotProviderIds + SnapshotBootstrap -> SnapshotRegistry
- SnapshotSectionDeserializer -> SnapshotRestore
- SnapshotSerializer -> SnapshotPersistence

同步更新 SnapshotService 与 YarnVariableStorage 的调用点。
2026-06-12 17:40:36 +08:00
dingyuntian 789ca207bd docs(save): 更新存档系统设计与 README 2026-06-11 21:49:37 +08:00
dingyuntian 4bb0215f3d feat(fix-system): 为读档添加状态机即时切换与快照支持 2026-06-11 21:49:18 +08:00
dingyuntian c67d988365 feat(save): 重构快照结构并添加 Fix 场景支持 2026-06-11 21:48:50 +08:00
dingyuntian 13e51a86e6 chore: 补充 SaveSystem 目录 meta 文件 2026-06-03 19:59:17 +08:00
dingyuntian 2cd3a1a7df docs: 补充存档系统设计文档并更新 CLAUDE.md 2026-06-03 19:58:49 +08:00
dingyuntian 2c1e5d8fa7 chore: 清理旧节点 Prefab 资源 2026-06-03 19:58:36 +08:00
dingyuntian 1065e2b8b9 feat(scene-mgmt): 为场景管理与音频系统添加快照支持 2026-06-03 19:58:02 +08:00
dingyuntian 8e5346da46 feat(fix-system): 修复系统迁移到 DeepRepairDataRegistry 2026-06-03 19:57:46 +08:00
dingyuntian 2c89feec9d feat(save): 移除旧 StorageSystem 并适配 Game Loop 2026-06-03 19:57:29 +08:00
dingyuntian a33d6a9fe1 feat(dialog): 拆分 Yarn 变量存储为独立单例 2026-06-03 19:57:12 +08:00
dingyuntian 33a028f071 feat(save): 添加快照存档系统核心框架 2026-06-03 19:56:45 +08:00
dingyuntian c845af68b9 Merge branch 'feature/石头换碟动画' into 'develop'
石头换碟的动画

See merge request aibis-dream/aibis-dream!711
2026-05-26 07:58:19 +00:00
dingyuntian 874abf66e0 Merge branch 'feature/海浪动画调整' into 'develop'
Feature/海浪动画调整

See merge request aibis-dream/aibis-dream!710
2026-05-26 07:51:08 +00:00
dingyuntian f759c8cfde docs: 添加选项已探索追踪设计讨论 2026-05-26 15:49:16 +08:00
dingyuntian 570961251c feat(dialog): 实现选项已探索追踪与视觉反馈 2026-05-26 15:49:05 +08:00
dingyuntian 0794c5751d docs: 添加 Yarn 节点类型规范文档 2026-05-25 18:44:42 +08:00
dingyuntian 038c624163 chore: 升级 Yarn Project 配置并同步本地化资源 2026-05-25 18:44:30 +08:00
dingyuntian e72d14ed8d yarn(dialog): 调整石头剧情事件节点 2026-05-25 18:43:47 +08:00
dingyuntian 5c56551d79 yarn(dialog): 重构 Day1 睡觉对话节点结构 2026-05-25 18:43:36 +08:00
dingyuntian db5fbb0cb8 feat(dialog): 重构选项已选状态视觉表现 2026-05-25 18:43:23 +08:00
dingyuntian 7e579a2eb0 fix(ui): 修复时间流转动画重叠和泄漏问题 2026-05-25 18:43:10 +08:00
zhouziqian 03d96c8694 石头换碟的动画 2026-05-14 19:00:09 +08:00
dingyuntian cf7b0fc67d Merge branch 'feature/海浪动画调整' into 'develop'
Feature/海浪动画调整

See merge request aibis-dream/aibis-dream!709
2026-05-12 12:25:32 +00:00
dingyuntian 342a854a33 feat: 石头唱片机演示 2026-05-12 20:24:45 +08:00
dingyuntian 6ea5a6cb0e chore: 石头相关资产按规范迁移 2026-05-12 20:24:15 +08:00
dingyuntian 70b69c2302 art: 海浪动画调整 2026-05-12 14:27:05 +08:00
dingyuntian fb00d3ad8e Merge branch 'feature/Yarn3.0升级合入' into 'develop'
chore: yarn项目升级并重新分类

See merge request aibis-dream/aibis-dream!708
2026-05-01 10:57:21 +00:00
dingyuntian b7712f7df8 fix: 本地化索引添加 2026-05-01 18:50:45 +08:00
dingyuntian c84b56df6f feat(yarn): 补充 FP_Huoshan1 Yarn 项目 meta 文件 2026-05-01 18:44:35 +08:00
dingyuntian 35e61146b6 scene: 更新场景配置与 SceneSO YarnProject 引用 2026-05-01 18:43:44 +08:00
dingyuntian 09372b482b feat(yarn): 升级 FP 章节 Yarn 项目配置并添加本地化导出 2026-05-01 18:43:24 +08:00
dingyuntian ff7cb41c47 feat(localization): 注册 Params 本地化表并更新 Addressables 配置 2026-05-01 16:46:51 +08:00
dingyuntian 6679cc1fbd feat(localization): 补充角色名与参数日文翻译 2026-05-01 16:46:24 +08:00
dingyuntian dcf5bf6e09 fix(dialog): 移除重复对话推进调用并清理命名空间引用 2026-05-01 16:46:11 +08:00
dingyuntian 4ef236274d Merge branch 'feature/升级Yarn3.2' into feature/Yarn3.0升级合入 2026-04-30 16:15:49 +08:00
dingyuntian deb3d5182a feat: 添加日语本地化支持 2026-04-30 15:40:08 +08:00
dingyuntian e23185b11d Merge branch 'feature/本地化内容导入' into 'develop'
Feature/本地化内容导入

See merge request aibis-dream/aibis-dream!707
2026-04-30 06:44:40 +00:00
dingyuntian dabf9fc219 feat(localization): 补充日语 Sprite 本地化表注册 2026-04-30 14:43:40 +08:00
dingyuntian aa926afa29 fix(game-loop): 修复存档章节名解析逻辑 2026-04-30 14:41:59 +08:00
dingyuntian bb043aed2b fix(dialog): 修复本地化参数替换方法调用 2026-04-30 14:41:45 +08:00
dingyuntian 64e74cbd2d yarn(dialog): 更新对话脚本及角色配置 2026-04-30 14:41:31 +08:00
dingyuntian 17862b7bab art(localization): 更新现有本地化文本内容 2026-04-30 14:41:12 +08:00
dingyuntian 9faff272cf feat(localization): 新增日语本地化支持 2026-04-30 14:40:35 +08:00
dingyuntian be1f288e1c art: 预制字体增加日文支持 2026-04-30 00:38:41 +08:00
dingyuntian 1d629e78f7 chore: 废弃存档文件删除 2026-04-29 23:17:35 +08:00
dingyuntian e471e5f285 Merge branch 'feature/本地化内容导入' into 'develop'
Feature/本地化内容导入

See merge request aibis-dream/aibis-dream!706
2026-04-29 07:33:21 +00:00
dingyuntian 2a83fc5afe fix: 屏幕适应重构,现在宽屏窄屏均可正常处理 2026-04-29 15:30:44 +08:00
dingyuntian a5fc9d8ba4 fix: 维修任务面板文字重叠 2026-04-28 20:47:13 +08:00
dingyuntian a03fab65e3 Merge branch 'bugfix/章节加载顺序错乱' into 'develop'
fix: 修复章节加载顺序错乱问题

See merge request aibis-dream/aibis-dream!705
2026-04-28 09:34:35 +00:00
dingyuntian 44f001361e fix: 修复章节加载顺序错乱问题 2026-04-28 17:33:16 +08:00
dingyuntian f224e8a2ac Merge branch 'feature/章节管理合入' into 'develop'
feat(chapter): 重构章节系统为多出口图结构

See merge request aibis-dream/aibis-dream!704
2026-04-28 08:38:04 +00:00
dingyuntian a9e14387cd chore: 章节系统更新后调整 2026-04-28 16:37:33 +08:00
dingyuntian 166ec647b4 Merge branch 'feature/章节管理' into feature/章节管理合入 2026-04-28 15:53:30 +08:00
dingyuntian 3bfb77f15e Merge branch 'bugfix/玩家反馈bug' into 'develop'
Bugfix/玩家反馈bug

See merge request aibis-dream/aibis-dream!703
2026-04-27 14:45:32 +00:00
dingyuntian 26ffa8d016 fix: Windows上实际上没有最大化窗口,反而会造成bug 2026-04-27 22:36:52 +08:00
dingyuntian f12368abfb fix(yarn): 修复多处对话标记缺失和符号格式 2026-04-27 22:22:40 +08:00
dingyuntian 71e2f915b9 fix(dialog): 缩短缺失本地化时的错误提示文本 2026-04-27 22:22:26 +08:00
dingyuntian 5e3087f4e9 Merge remote-tracking branch 'origin/feature/最后!的文本调整' into bugfix/玩家反馈bug 2026-04-27 21:23:07 +08:00
dingyuntian c6caae6078 fix(ui): 修复面板暂停时机和背景淡入效果 2026-04-27 21:22:23 +08:00
dingyuntian 030124a0c7 fix(dialog): 重构对话推进模式为响应式状态绑定 2026-04-27 21:22:10 +08:00
dingyuntian 335b8f1bbc fix(localization): 修正 Yarn 项目本地化语言代码 2026-04-27 21:21:56 +08:00
Ovid 0dcb02f725 文本调整 2026-04-27 19:53:37 +08:00
dingyuntian 3656b35525 Merge branch 'feature/DEMO-FP优化' into 'develop'
Feature/demo fp优化

See merge request aibis-dream/aibis-dream!701
2026-04-26 12:51:32 +00:00
dingyuntian e7b1fc5ec1 fix: 修改yarn语法错误 2026-04-26 20:50:14 +08:00
dingyuntian 9264a17f87 Merge branch 'feature/DEMO-FP优化' of ssh://114.67.212.218:2222/aibis-dream/aibis-dream into feature/DEMO-FP优化 2026-04-26 20:48:13 +08:00
dingyuntian 49fcb7240b Merge branch 'develop' into feature/DEMO-FP优化 2026-04-26 20:45:29 +08:00
Ovid 5d7c1037cf DEBUG 2026-04-25 21:10:45 +08:00
Ovid 32fb29f18a DEBUG 2026-04-25 21:09:18 +08:00
Ovid 47edc1296c Merge branch 'develop' into feature/DEMO-FP优化 2026-04-25 21:06:25 +08:00
dingyuntian 3338606660 Merge branch '修复isPeipeiBE变量相关错漏' into 'develop'
修复isPeipeiBE相关错漏

See merge request aibis-dream/aibis-dream!702
2026-04-25 12:58:57 +00:00
bottlefish ebe18fd8e1 修复isPeipeiBE相关错漏 2026-04-25 20:56:52 +08:00
Ovid 7d7bef7c3b 解决本地化报错问题 2026-04-25 20:19:01 +08:00
dingyuntian 94d89330dc feat(chapter): 添加章节图可视化编辑器
基于 UIToolkit 实现 ChapterGraph 编辑器窗口,可视化查看
和编辑 TalkSceneSO 之间的多出口连接关系。
2026-04-25 20:13:55 +08:00
dingyuntian 93012f5f7c feat(chapter): 重构章节系统为多出口图结构
- TalkSceneSO 引入 SceneExit 多出口列表与 displayOrder 排序
- GameManager.NextSceneSo 与 Yarn NextYarn 支持 exitName 跳转
- ChapterController 改用 GameManager 全局 SceneSO 列表
- SceneSO 资源迁移至 Archive/Demo/Fiction/Test 子目录
2026-04-25 20:13:29 +08:00
Ovid b32992bc26 merge本地化表产生的变更 2026-04-25 19:34:02 +08:00
Ovid 439984ca93 Merge branch 'develop' into feature/DEMO-FP优化 2026-04-25 19:30:26 +08:00
Ovid fbac02f641 移除FP YARN中的非放出文本 2026-04-25 19:27:21 +08:00
dingyuntian 5bc48400c5 Merge branch 'bugfix/本地化缺漏' into 'develop'
fix: 本地化修复&版本号升级到0.5.0.13

See merge request aibis-dream/aibis-dream!700
2026-04-24 05:22:58 +00:00
dingyuntian 03682d0ec1 fix: 本地化修复&版本号升级到0.5.0.13 2026-04-24 12:50:39 +08:00
dingyuntian 1db919e472 chore: 废除本地化表索引 2026-04-24 11:45:14 +08:00
dingyuntian c5e3210793 chore: 废除旧本地化表(csv已导出归档) 2026-04-23 22:42:38 +08:00
dingyuntian a027ef2577 Merge branch 'bugfix/插线交互问题修复' into 'develop'
Bugfix/插线交互问题修复

See merge request aibis-dream/aibis-dream!699
2026-04-23 13:41:51 +00:00
dingyuntian 00f3cf3f59 chore: 版本号升级 2026-04-23 21:25:50 +08:00
dingyuntian 39b9ed487c fix: 修复对话中仍能与插头交互的问题 2026-04-23 21:25:35 +08:00
dingyuntian 5be131e020 docs: 更新本地化架构说明和 CSV 配置路径 2026-04-23 19:49:20 +08:00
dingyuntian b9c15216cb chore(yarn): 更新 Yarn Project meta 本地化配置字段 2026-04-23 19:49:16 +08:00
dingyuntian dd9420dbc7 refactor(ui): 删除废弃的 CircularProgress 和 LocalizedText 组件 2026-04-23 19:49:10 +08:00
dingyuntian bec825ce89 fix(dialog): 适配本地化架构变更自动同步对话语言 2026-04-23 19:49:05 +08:00
dingyuntian 461fd18191 refactor(localization): 迁移本地化为 CSV 驱动并支持多语言角色名 2026-04-23 19:49:00 +08:00
dingyuntian 15651facdc fix(tarot): 修复 TarotCard 排序层序列化值 2026-04-23 19:48:54 +08:00
dingyuntian 72c0dfb746 Merge branch '4.23bug修复' into 'develop'
fix(storage): Yarn 全局变量前缀改为 $global_

See merge request aibis-dream/aibis-dream!698
2026-04-23 11:43:35 +00:00
bottlefish 999095b27e 修改UF内的色觉晶圆层级 2026-04-23 16:36:26 +08:00
bottlefish da3fed3c07 fix(yarn,loc): Day1 涂画线文案与 ppStage3 分支
Made-with: Cursor
2026-04-23 16:32:02 +08:00
bottlefish bbc51283dc fix(yarn,loc): 佩佩 BE 外出迁至 Day2 夜并统一 global_isPeipeiBE
Made-with: Cursor
2026-04-23 16:31:49 +08:00
bottlefish 42d1f431b6 fix(storage): Yarn 全局变量前缀改为 $global_
Made-with: Cursor
2026-04-23 16:31:35 +08:00
dingyuntian 2887f51ef8 Merge branch '4.23bug修复' into 'develop'
4.23bug修复

See merge request aibis-dream/aibis-dream!697
2026-04-23 06:33:40 +00:00
bottlefish f48e733663 修复扩展模块层级错误 2026-04-23 14:31:43 +08:00
dingyuntian 027e545646 chore(localization): 添加Fiction_Day2_begin本地化表配置 2026-04-23 11:56:52 +08:00
dingyuntian 53b14b7a04 art: 修复动画资源命名和缺失绑定 2026-04-23 11:56:35 +08:00
dingyuntian 70e820195e style(peipei): 移除UFSystem未使用引用 2026-04-23 11:56:22 +08:00
dingyuntian 92f4d463b3 refactor(fix-system): 清理PunchTapeInteraction未使用事件 2026-04-23 11:56:11 +08:00
dingyuntian 98fa5e5474 fix: 移除废弃YarnCommand适配YarnSpinner 3.2 2026-04-23 11:56:01 +08:00
dingyuntian b447d4784c feat(dialog): 升级Dialog系统到YarnSpinner 3.2 2026-04-23 11:55:43 +08:00
dingyuntian cbae4cdb22 chore: yarn项目升级并重新分类
1. 移除Resources,单独使用一个Yarn文件夹保存
2. 按类型分为Fiction,FP,Test
3. 升级到YarnSpinner3.0
4. 移除所有本地化关联,后续迁移到Yarn内置的本地化系统
2026-04-23 11:47:29 +08:00
dingyuntian 55452b4376 chore: YarnSpinner升级到3.2 2026-04-23 11:42:38 +08:00
dingyuntian 7e422fa457 Merge branch 'develop' into feature/升级Yarn3.2 2026-04-22 15:58:28 +08:00
dingyuntian 5e80772cbf Merge branch 'develop' into feature/升级Yarn3.2 2026-04-22 15:57:40 +08:00
bottlefish b8f45e1466 Merge branch '后处理设置修改' into 'develop'
提交版本号

See merge request aibis-dream/aibis-dream!696
2026-04-21 12:08:17 +00:00
bottlefish ebe0dd7c59 提交版本号 2026-04-21 20:07:38 +08:00
bottlefish f1e7e020f1 Merge branch '后处理设置修改' into 'develop'
修改相机后处理设置

See merge request aibis-dream/aibis-dream!695
2026-04-21 12:00:31 +00:00
bottlefish 922186cf42 修改相机后处理设置 2026-04-21 19:59:49 +08:00
dingyuntian b635d90420 Merge branch 'bugfix/打包修复' into 'develop'
chore: 版本号升级为0.5.0.10

See merge request aibis-dream/aibis-dream!694
2026-04-21 08:14:11 +00:00
dingyuntian f68321e8a8 chore: 版本号升级为0.5.0.10 2026-04-21 16:13:15 +08:00
dingyuntian 4675792617 Merge branch 'bugfix/打包修复' into 'develop'
Bugfix/打包修复

See merge request aibis-dream/aibis-dream!693
2026-04-21 06:32:12 +00:00
dingyuntian 19aea94d95 Merge branch 'develop' into bugfix/打包修复 2026-04-21 14:28:30 +08:00
dingyuntian bf1b23b844 chore: 升级版本号到 0.5.0.9 2026-04-21 14:15:17 +08:00
dingyuntian ea2164acbf art(Feature_Memory): 调整放映机资源和 PunchTapeCanvas 默认状态 2026-04-21 14:15:09 +08:00
dingyuntian fe65071cf7 scene(peipei): 调整佩佩修复场景配置 2026-04-21 14:15:02 +08:00
dingyuntian 054f3822c6 art(Scene_ClinicOut): 重构诊所外落叶和佩佩动画系统 2026-04-21 14:14:52 +08:00
dingyuntian a2d9427299 art(Scene_Bar): 替换酒吧医生动画和美术资源 2026-04-21 14:14:19 +08:00
dingyuntian e3913f425b yarn(dialog): 调整 Day1 对话随机动画和 timeline 指令 2026-04-21 14:11:22 +08:00
dingyuntian 140b8584bc fix(fix-system): 移除 GearFollower 调试日志 2026-04-21 14:11:14 +08:00
dingyuntian 0137d54271 fix(scene-mgmt): 修复相机切换 blend 时间计算 2026-04-21 14:11:08 +08:00
dingyuntian 487ee0a991 feat(scene-mgmt): 添加随机动画播放系统和 Yarn 指令 2026-04-21 14:10:44 +08:00
dingyuntian 0793006e99 Merge branch 'feature/DEMO-FP优化' into 'develop'
火山注释掉多余的解释

See merge request aibis-dream/aibis-dream!692
2026-04-21 04:14:06 +00:00
Ovid 8ab7441321 火山注释掉多余的解释 2026-04-21 12:07:17 +08:00
chenziqin 76cfecc0a4 Merge branch 'feature/DEMO-FP优化' into 'develop'
Feature/demo fp优化

See merge request aibis-dream/aibis-dream!691
2026-04-20 09:30:28 +00:00
Ovid d96bc0bf07 Merge branch 'develop' into feature/DEMO-FP优化 2026-04-20 17:28:37 +08:00
Ovid 6e3143121e 调整放风筝:风筝2一开始没出图修复;风筝2结尾强制升高度 2026-04-20 17:27:49 +08:00
dingyuntian 5cbc46ce73 art: 树叶动画默认大风 2026-04-20 09:07:25 +08:00
dingyuntian 7ad048bef8 fix: 场景相机意外打开 2026-04-20 09:06:10 +08:00
dingyuntian 420f6152a3 refactor: 资源迁移 2026-04-20 09:05:48 +08:00
dingyuntian 7d7b417e35 chore: 版本号升级到0.5.0.8 2026-04-20 08:58:04 +08:00
dingyuntian 277029ff51 fix: Addressables索引找回 2026-04-20 08:57:42 +08:00
dingyuntian 594578c697 Merge branch 'develop' into feature/升级Yarn3.2 2026-04-19 16:23:38 +08:00
dingyuntian 4d319d3b04 Merge branch 'develop' into feature/升级Yarn3.2 2026-04-19 16:22:42 +08:00
dingyuntian ceccff5461 Merge branch 'bugfix/Timeline丢失' into 'develop'
Bugfix/timeline丢失

See merge request aibis-dream/aibis-dream!690
2026-04-19 08:21:47 +00:00
dingyuntian e07e3b05d2 chore: 版本号变动 2026-04-19 16:21:11 +08:00
dingyuntian dbd2fed24a fix: 隐藏options功能 2026-04-19 16:20:43 +08:00
dingyuntian ddb1a97236 chore: 本地化表更新 2026-04-19 16:20:20 +08:00
dingyuntian 57ad1307ff Merge branch 'develop' into bugfix/Timeline丢失 2026-04-19 15:24:42 +08:00
dingyuntian d25c622ecc fix: yarn微调 2026-04-19 15:18:58 +08:00
dingyuntian db29f8f1dd fix: timeline残余片段 2026-04-19 15:18:38 +08:00
dingyuntian b976be9454 build(addressables): 移除冗余 link.xml 配置 2026-04-19 15:01:31 +08:00
dingyuntian 613e48f799 fix(ui): 调整气泡样式与渲染配置 2026-04-19 15:01:13 +08:00
dingyuntian 57c90d9d4f fix(scene): 重构火山修复场景 Timeline 对象架构 2026-04-19 15:00:58 +08:00
dingyuntian 7cabc6418c fix(scene): 调整诊所地铁酒吧场景配置与 Timeline 对象 2026-04-19 15:00:42 +08:00
dingyuntian e77bea6bdc fix(localization): 补充角色名并同步对话多语言数据 2026-04-19 15:00:18 +08:00
dingyuntian fcf5b94472 fix(yarn): 修正 Yarn 对话中 Timeline 调用路径并更新指令 2026-04-19 14:59:58 +08:00
dingyuntian 442e419f80 fix(fix-system): 修复记忆修复滑条交互与 Timeline 状态冲突 2026-04-19 14:59:16 +08:00
dingyuntian 83acdeaecc fix(camera-kit): 修复相机震动叠加导致无法回归原点 2026-04-19 14:58:54 +08:00
dingyuntian d324ec3aa1 fix(timeline-kit): 修复 Timeline 重复播放无法从头开始 2026-04-19 14:58:35 +08:00
chenziqin e948d08573 Merge branch 'feature/DEMO-FP优化' into 'develop'
Feature/demo fp优化

See merge request aibis-dream/aibis-dream!688
2026-04-19 06:57:11 +00:00
Ovid c9dcff33ae 台词优化 2026-04-19 14:56:08 +08:00
dingyuntian af50894c61 chore: 删除一些没用的scene 2026-04-18 13:58:12 +08:00
Ovid 4e5835d658 Merge branch 'develop' into feature/DEMO-FP优化 2026-04-17 15:56:48 +08:00
dingyuntian 5e5cc608df Merge branch 'bugfix/打包' into 'develop'
art(fix-system): 移除旧路径维修系统美术资源

See merge request aibis-dream/aibis-dream!689
2026-04-16 12:04:43 +00:00
dingyuntian 82be2508f0 build: 更新构建配置和版本号至 0.5.0.4
Made-with: Cursor
2026-04-16 20:03:46 +08:00
dingyuntian 271b02220f build: 更新 Addressable 资源组配置
Made-with: Cursor
2026-04-16 20:03:40 +08:00
dingyuntian 607ccf3049 localization(dialog): 同步 Day2 sleep 对话多语言数据
Made-with: Cursor
2026-04-16 20:03:34 +08:00
dingyuntian 2f1d023860 yarn(dialog): 更新 Day1 和 Day2 对话脚本
Made-with: Cursor
2026-04-16 20:03:27 +08:00
dingyuntian 078b61c1a3 fix(ui): 修复 DreamTerminalUI 编辑器编译错误
Made-with: Cursor
2026-04-16 20:03:21 +08:00
dingyuntian 19b854e0d2 feat(fix-system): 更新维修面板预制体配置
Made-with: Cursor
2026-04-16 20:03:14 +08:00
dingyuntian a00ca40a0c art(fix-system): 添加维修系统美术资源到 GameContent
Made-with: Cursor
2026-04-16 20:03:06 +08:00
dingyuntian a0b675c087 art(fix-system): 移除旧路径维修系统美术资源
Made-with: Cursor
2026-04-16 20:02:53 +08:00
Ovid ecbafbebf9 D1S 天文台AMB补充 2026-04-16 18:37:16 +08:00
Ovid 4a63fd88b2 D1N台词节奏优化 2026-04-16 17:34:31 +08:00
Ovid 6adf5661f4 Merge branch 'develop' into feature/DEMO-FP优化 2026-04-16 17:21:49 +08:00
Ovid 4595699172 又微调了一下D1N 2026-04-16 16:43:18 +08:00
bottlefish 47d05a9495 Merge branch 'timelinebug修复' into 'develop'
fix(yarn): Huoshan1 EventNode 增加 LEyePlugIn 桥接节点

See merge request aibis-dream/aibis-dream!687
2026-04-16 08:27:28 +00:00
bottlefish 4f76d956b4 fix(scene): Persistence 相机剔除与酒吧场景坐标
Made-with: Cursor
2026-04-16 16:26:43 +08:00
bottlefish 7be4edff84 fix(scene): PeipeiFix 场景布局与动画曲线
Made-with: Cursor
2026-04-16 16:26:34 +08:00
bottlefish bb3141b7d5 fix(render): Renderer2D 滤镜核参数调整
Made-with: Cursor
2026-04-16 16:26:26 +08:00
bottlefish ca592b3a01 fix(i18n): Day2 中段与睡眠本地化同步
Made-with: Cursor
2026-04-16 16:26:18 +08:00
bottlefish 6a0405a10c fix(i18n): Peipei2 对话本地化同步
Made-with: Cursor
2026-04-16 16:26:09 +08:00
bottlefish e1d0a6dfd4 fix(yarn): Peipei2 Stage10-12 术前手术与结局流程调整
Made-with: Cursor
2026-04-16 16:25:58 +08:00
bottlefish 334dc060a0 chore(addressables): 注册头痛 Timeline 与火山头痛 playable
Made-with: Cursor
2026-04-16 16:25:47 +08:00
bottlefish 023e7abc42 fix(yarn): Huoshan1 EventNode 增加 LEyePlugIn 桥接节点
Made-with: Cursor
2026-04-16 16:25:34 +08:00
Ovid d40cb19155 Merge branch 'develop' into feature/DEMO-FP优化 2026-04-16 16:17:03 +08:00
chenziqin a22461d481 Merge branch 'feature/DEMO-FP优化' into 'develop'
D1N台词节奏优化

See merge request aibis-dream/aibis-dream!686
2026-04-16 08:16:28 +00:00
Ovid 7cc76649b3 D1N台词节奏优化 2026-04-16 16:15:39 +08:00
dingyuntian aa06c1bcaf Merge branch 'bugfix/UI相机切换' into 'develop'
feat: UI相机变化

See merge request aibis-dream/aibis-dream!685
2026-04-16 07:36:20 +00:00
dingyuntian f9b8764941 Merge branch 'develop' of ssh://114.67.212.218:2222/aibis-dream/aibis-dream into develop 2026-04-16 15:33:27 +08:00
dingyuntian 8e2ad4c163 feat: UI相机变化
UI相机从OverlayCamera变为独立Camera,方便应用不同的后处理
2026-04-16 15:33:10 +08:00
bottlefish 03f9cf1ddb Merge branch '4.16Bug修复' into 'develop'
fix(yarn): Day2 中段移除重复诊室外火山 init_actor,并同步本地化

See merge request aibis-dream/aibis-dream!684
2026-04-16 07:29:04 +00:00
bottlefish fb763be06f brige修改 2026-04-16 15:28:29 +08:00
bottlefish 30c2107c88 fix(scene): Bridge 场景 Bloom 与后处理权重调整
Made-with: Cursor
2026-04-16 15:02:48 +08:00
bottlefish 920d137ed1 fix(art): 坠落序列 CG 资源与 meta 调整
Made-with: Cursor
2026-04-16 15:02:38 +08:00
bottlefish 5b833404da fix(yarn): Peipei2 Stage8 停止想象 渐入渐出与 UF 告警
Made-with: Cursor
2026-04-16 15:02:26 +08:00
bottlefish ac5dcc196e fix(yarn): Day2 睡眠段结尾衔接 NextYarn,并同步本地化
Made-with: Cursor
2026-04-16 15:02:15 +08:00
bottlefish 2461e75379 fix(yarn): Day2 中段移除重复诊室外火山 init_actor,并同步本地化
Made-with: Cursor
2026-04-16 15:02:07 +08:00
dingyuntian 824551822f Merge branch 'feature/地铁新闻切换' into 'develop'
chore: 删除旧地铁资源目录

See merge request aibis-dream/aibis-dream!683
2026-04-16 06:24:39 +00:00
dingyuntian 135d0dd8bf chore: 资源重新归位 2026-04-16 14:22:03 +08:00
dingyuntian 68eb30cf6d fix: 地铁索引丢失 2026-04-16 14:19:54 +08:00
dingyuntian ec057d72e2 chore(config): 更新地铁场景 Addressable 配置和场景引用
Made-with: Cursor
2026-04-16 14:15:06 +08:00
dingyuntian f9b38aaba8 feat(scene-mgmt): 添加地铁场景新闻图片切换功能
Made-with: Cursor
2026-04-16 14:14:49 +08:00
dingyuntian ba6e327cd1 art(scene-subway): 迁移地铁场景资源到 GameContent
Made-with: Cursor
2026-04-16 14:14:25 +08:00
dingyuntian 04d26210e3 chore: 删除旧地铁资源目录
Made-with: Cursor
2026-04-16 14:14:05 +08:00
bottlefish 3364a57fb6 Merge branch '修改坠落素材' into 'develop'
替换素材

See merge request aibis-dream/aibis-dream!682
2026-04-16 06:11:06 +00:00
bottlefish c4156a3ee5 替换素材 2026-04-16 14:09:59 +08:00
chenziqin a33fcf28cb Merge branch 'feature/DEMO-FP优化' into 'develop'
把新闻短接成只有1个,把新闻图加上

See merge request aibis-dream/aibis-dream!681
2026-04-15 16:35:49 +00:00
Ovid a2cede7b1a 把新闻短接成只有1个,把新闻图加上 2026-04-16 00:33:23 +08:00
dingyuntian 0009bd1eba Merge branch 'feature/地铁电视' into 'develop'
feat: 电视新闻图片替换

See merge request aibis-dream/aibis-dream!680
2026-04-15 16:33:23 +00:00
dingyuntian 54cdee2f69 feat: 电视新闻图片替换 2026-04-16 00:32:54 +08:00
bottlefish 7c770c4ea3 Merge branch '诊室外医生动画补充' into 'develop'
feat(诊室外): 火山角色动画与原始资源

See merge request aibis-dream/aibis-dream!679
2026-04-15 16:27:03 +00:00
bottlefish aee7143511 Merge branch 'develop' into 诊室外医生动画补充 2026-04-16 00:26:18 +08:00
bottlefish 64361405cb 补提交 2026-04-16 00:25:39 +08:00
bottlefish 3f47aff03f chore(i18n): Test_Huoshan 对白表元数据 rid 刷新
Made-with: Cursor
2026-04-16 00:24:58 +08:00
bottlefish dde81fb38a feat(i18n): 对白 Shared Data 与 Yarn 对齐
Made-with: Cursor
2026-04-16 00:24:45 +08:00
bottlefish deb38e665b fix(yarn): Stage3 ModuleHightLight 参数大小写
Made-with: Cursor
2026-04-16 00:24:36 +08:00
bottlefish 7de23d9157 feat(yarn): Day2 中段诊室外火山演出
Made-with: Cursor
2026-04-16 00:24:27 +08:00
bottlefish 469a85887d feat(scene): Bridge 场景与天桥 Bloom 子资源
Made-with: Cursor
2026-04-16 00:24:17 +08:00
bottlefish 8eeabc594b chore(addressables): Core 组注册诊室外火山动画
Made-with: Cursor
2026-04-16 00:24:09 +08:00
bottlefish 828fca2ea5 chore(诊室外): 调整佩佩 Animator 状态机布局
Made-with: Cursor
2026-04-16 00:24:00 +08:00
bottlefish 9bbc189ef3 feat(诊室外): 火山角色动画与原始资源
新增背影 IDLE 动画、外出火山图集与 Animator 控制器。

Made-with: Cursor
2026-04-16 00:23:51 +08:00
dingyuntian 6bda90ff9f Merge branch 'feature/存档重构' into 'develop'
art(peipei): 迁移佩佩动画资源到新目录结构

See merge request aibis-dream/aibis-dream!678
2026-04-15 16:19:25 +00:00
dingyuntian 7bafeb6278 Merge branch 'develop' into feature/存档重构 2026-04-16 00:19:06 +08:00
dingyuntian fc0d7c7f35 yarn(dialog): 调整树叶场景佩佩状态切换流程
Made-with: Cursor
2026-04-16 00:15:37 +08:00
dingyuntian ae0dbc037a art(peipei): 重构诊室外佩佩接树叶动画资源
Made-with: Cursor
2026-04-16 00:15:30 +08:00
dingyuntian 603a36f575 fix(editor): 修复 AnimationClip 复用时 StopTime 未更新
Made-with: Cursor
2026-04-16 00:14:53 +08:00
chenziqin 4fe36c11a5 Merge branch 'feature/DEMO-FP优化' into 'develop'
Feature/demo fp优化

See merge request aibis-dream/aibis-dream!677
2026-04-15 15:50:38 +00:00
Ovid 7ef2b9d245 补linetag 2026-04-15 23:49:30 +08:00
Ovid 3a4b41f428 Merge branch 'develop' into feature/DEMO-FP优化 2026-04-15 23:47:49 +08:00
Ovid f47b2218e3 笑话更新 2026-04-15 23:45:48 +08:00
dingyuntian 6ef9edc220 chore: 更新编辑器界面和渲染器配置
Made-with: Cursor
2026-04-15 23:41:10 +08:00
dingyuntian 989ee1c623 art(clinc-out): 添加诊室附近佩佩接落叶动画资源
Made-with: Cursor
2026-04-15 23:40:56 +08:00
dingyuntian b27dc52db1 art(clinc-out): 更新外出佩佩动画控制器配置
Made-with: Cursor
2026-04-15 23:40:35 +08:00
dingyuntian 4e7725a584 art(clinc-out): 添加外出佩佩动画切片和资源
Made-with: Cursor
2026-04-15 23:40:22 +08:00
dingyuntian f55e0a9942 art(peipei): 迁移佩佩动画资源到新目录结构
Made-with: Cursor
2026-04-15 23:39:56 +08:00
bottlefish fe642b0a5b Merge branch '4.15优化修复' into 'develop'
fix(tarot): 塔罗逆位用本地 Z 旋转避免镜像闪变

See merge request aibis-dream/aibis-dream!675
2026-04-15 15:29:34 +00:00
bottlefish d97be8d7a2 fix(scene): Persistence 相机位置、UI 透明度与 Rect 子节点顺序
Made-with: Cursor
2026-04-15 23:25:27 +08:00
bottlefish a3fda5a5e1 fix(huoshan): Center 初始 gameStage 与 Test_Huoshan 本地化表同步
Made-with: Cursor
2026-04-15 23:25:21 +08:00
bottlefish e6c2472a3b feat(day2-night): 天桥全屏图资源与 Yarn 演出、FP_Day2_night 本地化同步
Made-with: Cursor
2026-04-15 23:25:13 +08:00
Ovid 43fff54ea4 短接D2B 2026-04-15 23:23:13 +08:00
Ovid cd80e1afd0 text debug 2026-04-15 23:21:48 +08:00
Ovid 3b8c9acbd2 D2M 2026-04-15 23:17:22 +08:00
bottlefish be126ff0f1 chore: 调整 glitch 资源配置
Made-with: Cursor
2026-04-15 22:57:25 +08:00
bottlefish 7e20edf4f4 scene: 更新桥接、诊所外、持久化与酒吧场景
Made-with: Cursor
2026-04-15 22:57:16 +08:00
bottlefish 28b9316cdc scene(huoshan): 更新火山修复场景 HuoShanFixScene
Made-with: Cursor
2026-04-15 22:57:04 +08:00
bottlefish a27b29c046 chore(timeline): 诊室外旋转叶柄 Playable 微调
Made-with: Cursor
2026-04-15 22:56:55 +08:00
bottlefish 5a9d2c0ed3 build(addressables): 更新 Core 资源组条目
Made-with: Cursor
2026-04-15 22:56:46 +08:00
bottlefish bb7c2eb52b refactor(anim): 迁移火山与酒吧医生 Animator 至 GameContent
Made-with: Cursor
2026-04-15 22:56:33 +08:00
bottlefish d78848abc5 chore(i18n): 同步 Day1/Day2 与测试火山本地化表
Made-with: Cursor
2026-04-15 22:56:19 +08:00
bottlefish 525d9a3cc3 feat(yarn): 调整 Day1、火山与佩佩相关对话脚本
Made-with: Cursor
2026-04-15 22:56:06 +08:00
dingyuntian 36aca136ec Merge branch 'develop' into feature/存档重构 2026-04-15 22:25:00 +08:00
dingyuntian 3918fbce98 docs: 更新 CLAUDE.md 项目指南 2026-04-15 22:23:40 +08:00
dingyuntian 0c7d4952a6 chore: 删除废弃的测试脚本 2026-04-15 22:23:36 +08:00
dingyuntian 8fc05c376a scene(peipei): 调整佩佩修复场景和UI预制体参数 2026-04-15 22:23:25 +08:00
dingyuntian 905cf7d680 chore(config): 统一存档路径为发布版本格式 2026-04-15 22:23:16 +08:00
dingyuntian 626c789384 style(fix-system): 修正属性命名规范 2026-04-15 22:23:08 +08:00
dingyuntian 5ce289a687 fix(game-loop): 修复分辨率切换后视口未正确更新问题 2026-04-15 22:23:01 +08:00
dingyuntian ed3a394049 refactor(camera-kit): 清理废弃透视相机代码并增强健壮性 2026-04-15 22:22:53 +08:00
dingyuntian 778529e745 refactor(fix-system): 重构 MemoryProcess 并提取 MemorySlider 组件 2026-04-15 22:22:39 +08:00
dingyuntian 7290bbab1e feat(fix-system): 打孔带收藏夹改为固定3槽位布局 2026-04-15 22:22:30 +08:00
dingyuntian 2f1ee81f6c chore: 迁移记忆系统资源到 Feature_Memory 目录 2026-04-15 22:22:21 +08:00
dingyuntian af8ca0ae6b chore: 重命名 Dream/Peipei 资源目录为 Scene_ 前缀 2026-04-15 22:22:11 +08:00
bottlefish 24304a3706 Merge branch 'develop' into 4.15优化修复 2026-04-15 20:02:24 +08:00
bottlefish e2ff3484de 补提交 2026-04-15 19:57:20 +08:00
chenziqin 146ea1e1ac Merge branch 'feature/DEMO-FP优化' into 'develop'
bug修正

See merge request aibis-dream/aibis-dream!674
2026-04-15 11:53:53 +00:00
Ovid 0c01351eca FP_D1B 2026-04-15 19:52:41 +08:00
bottlefish bd94ea04de scene(huoshan): 微调火山修复场景实例与 LineRenderer 参数
Made-with: Cursor
2026-04-15 19:11:16 +08:00
bottlefish f13b9a7090 chore(localization): 同步多条对话表 metadata 与占位条目
Made-with: Cursor
2026-04-15 19:11:07 +08:00
bottlefish 81510dfa07 yarn(fp_day2_night): 调整天桥佩佩台词与本地化条目
Made-with: Cursor
2026-04-15 19:10:57 +08:00
bottlefish c46e3d8fb6 feat(dream): 终端变形播放 Glitch Timeline 与噪声材质、Addressables
Made-with: Cursor
2026-04-15 19:10:45 +08:00
bottlefish 23b5e48564 scene(clinic-out): 更新诊所外出场景引用佩佩接落叶动画
Made-with: Cursor
2026-04-15 19:10:30 +08:00
bottlefish ba41b226ed art(clinic-out): 添加诊室附近佩佩接落叶动画与源文件
Made-with: Cursor
2026-04-15 19:10:16 +08:00
bottlefish c0b658fee0 fix(tarot): 塔罗逆位用本地 Z 旋转避免镜像闪变
Made-with: Cursor
2026-04-15 19:10:04 +08:00
Ovid 870fbc072a bug修正
修改一句切割时的台词,修改actor name
2026-04-15 16:08:57 +08:00
Ovid 8ccc02fb67 部分文本和节奏优化 2026-04-15 15:31:53 +08:00
bottlefish 6319ba4d92 Merge branch '4.13Demo优化' into 'develop'
[ide-config] chore(cursor): 添加 Yarn 插线流程 skill 与审阅命令

See merge request aibis-dream/aibis-dream!673
2026-04-14 16:07:25 +00:00
bottlefish 89b41147c3 art(dream): 调整梦境图片素材导入 meta
Made-with: Cursor
2026-04-15 00:06:25 +08:00
bottlefish 84ac2ac944 art(peipei): 更新 Day2 UF 立绘与源稿
Made-with: Cursor
2026-04-15 00:06:19 +08:00
bottlefish f1156b4efd scene(dream): 添加姐姐放手 Timeline 并更新梦境场景
Made-with: Cursor
2026-04-15 00:06:11 +08:00
bottlefish 1795efac8c yarn(fp): 调整 Day2 睡眠梦境姐姐段落立绘顺序与占位
Made-with: Cursor
2026-04-15 00:06:04 +08:00
bottlefish 2b06bb0589 scene(bridge): 调整天桥场景 Sprite 与 Timeline 初始状态
Made-with: Cursor
2026-04-14 23:47:08 +08:00
bottlefish 504aa9c522 chore(localization): 同步 FP_Day1_begin Dialog 表元数据
Made-with: Cursor
2026-04-14 23:47:00 +08:00
bottlefish 2ab0a9241a yarn(FP_Peipei2): 注释色彩校准跳转并同步佩佩对话表
Made-with: Cursor
2026-04-14 23:46:47 +08:00
bottlefish 0dce40a56f art(clinic-out): 调整外出医生动画片段时长与 Animator 配置
Made-with: Cursor
2026-04-14 23:46:28 +08:00
bottlefish 8ad61459a5 fix(fix-system): 移除切割流程中重复的 scene 全局参数设置
Made-with: Cursor
2026-04-14 23:46:20 +08:00
dingyuntian 0fdf9faad4 Merge branch 'develop' into feature/存档重构 2026-04-14 20:17:57 +08:00
bottlefish c248531458 Merge branch 'develop' into 4.13Demo优化 2026-04-14 16:14:20 +08:00
bottlefish ce44e73caf art(peipei): 更新佩佩剪线特写 Aseprite 资源
Made-with: Cursor
2026-04-14 16:11:48 +08:00
bottlefish 9c3e6b9c94 scene(peipei): 调整佩佩维修场景与剪线 Timeline
Made-with: Cursor
2026-04-14 16:11:40 +08:00
bottlefish 8369dfbf26 yarn(peipei): 更新佩佩手术 Stage7 对话与本地化表
Made-with: Cursor
2026-04-14 16:11:30 +08:00
bottlefish 065fe2551b yarn(day1): 更新 Day1 开场对话与本地化表
Made-with: Cursor
2026-04-14 16:11:19 +08:00
bottlefish 43af7de3c5 docs(huoshan): 更新火山测试流程图文档
Made-with: Cursor
2026-04-14 16:11:10 +08:00
bottlefish 1151f35aa1 yarn(huoshan): 更新火山 Stage2 与 Center 对话
Made-with: Cursor
2026-04-14 16:11:00 +08:00
bottlefish 1aadabe0d2 [ide-config] chore(cursor): 添加 Yarn 插线流程 skill 与审阅命令
Made-with: Cursor
2026-04-14 16:10:47 +08:00
bottlefish c4c2807b30 Merge branch '4.13优化' into 'develop'
修复day1begin多余字符

See merge request aibis-dream/aibis-dream!672
2026-04-13 06:40:54 +00:00
bottlefish d2a847326c 修复day1begin多余字符 2026-04-13 14:40:01 +08:00
bottlefish cc62d4d73f Merge branch '美术接入' into 'develop'
art(clinicout): 重组诊室外动画资源并更新控制器引用

See merge request aibis-dream/aibis-dream!671
2026-04-11 13:40:13 +00:00
bottlefish aef6474361 fix(narrative): Day1闪回等待与场景微调
- Day1_begin 地铁闪回前增加 wait 1 改善节奏
- 地铁医生控制器状态位置微调
- Persistence 场景主摄像机位置归零修正

Made-with: Cursor
2026-04-11 21:27:18 +08:00
bottlefish 66e12063b9 art(peipei): 接入佩佩剪线CG动画与Timeline
- 新增佩佩剪线动画片段和特写CG素材
- Stage7_手术.yarn 中插入 play_timeline 佩佩剪线CG

Made-with: Cursor
2026-04-11 21:26:58 +08:00
bottlefish 05d6f0ba86 art(clinicout): 重组诊室外动画资源并更新控制器引用
- 将 animator/animatorRaw 迁移至 animation/animationRaw
- 新增外出医生动画(13个动作片段)和小佩佩天桥动画
- 更新外出医生动画控制器的 GUID 引用
- 调整旋转叶柄 playable 时间参数
- ClinicOut 场景位置与 Addressable 配置更新

Made-with: Cursor
2026-04-11 21:26:38 +08:00
bottlefish 6ce636855f Merge branch '4.11优化和修复' into 'develop'
chore(assets): 诊室外动画迁出至 GameContent/Scene_ClincOut

See merge request aibis-dream/aibis-dream!670
2026-04-11 10:28:28 +00:00
bottlefish d379dbaeb0 chore(l10n): 同步对话本地化表
Made-with: Cursor
2026-04-11 18:18:16 +08:00
bottlefish d808ae35d2 fix(scenes): HuoShan、佩佩与梦境场景引用更新
Made-with: Cursor
2026-04-11 18:18:05 +08:00
bottlefish 074224341e fix(narrative): Day1、火山与佩佩剧情节奏与节点
Made-with: Cursor
2026-04-11 18:17:54 +08:00
bottlefish 438a4f4ea9 fix(peipei): 眼睛特效依赖材质的校验与错误提示
Made-with: Cursor
2026-04-11 18:17:31 +08:00
bottlefish 534475c0f7 fix(huoshan): 销售旋钮与发条交互高亮及辉光曲线
Made-with: Cursor
2026-04-11 18:17:14 +08:00
bottlefish 1d63bdbc4f content(dream): 平静醒来 Timeline 与梦境素材及 Addressable 注册
Made-with: Cursor
2026-04-11 18:16:41 +08:00
bottlefish 2c7b831c03 refactor(scene-bridge): 天桥动画资源目录由 Animator 迁至 Animation
Made-with: Cursor
2026-04-11 18:15:51 +08:00
bottlefish 7ca283a7c6 fix(scene): ClinicOut 天桥佩佩挥手排序与位置
Made-with: Cursor
2026-04-11 15:18:34 +08:00
bottlefish df71f7cf9b chore(assets): 诊室外动画迁出至 GameContent/Scene_ClincOut
Made-with: Cursor
2026-04-11 15:18:21 +08:00
bottlefish 7b9a564f8d chore(localization): 同步对话框本地化表与相关资源 2026-04-11 14:45:16 +08:00
bottlefish c0b0ef9b22 scene(peipei-fix): 更新佩佩修复场景与持久化场景 2026-04-11 14:45:01 +08:00
bottlefish ed5b06b9e8 fix(fix-system): 调整热力图温度插值与区间逻辑 2026-04-11 14:44:49 +08:00
bottlefish ef7fc8d697 Merge branch '4.4美术接入' into 'develop'
feat(assets): 热图模块渐变纹理与 UF 资源

See merge request aibis-dream/aibis-dream!669
2026-04-10 12:24:26 +00:00
bottlefish 4cdd7f0e78 chore(scene-mgmt): 添加 AnimatorYarnCommand 文档 meta
Made-with: Cursor
2026-04-10 20:22:08 +08:00
bottlefish 0b8eb52ab7 scene(huoshan): 更新 HuoShanFixScene 场景配置
Made-with: Cursor
2026-04-10 20:21:59 +08:00
bottlefish 2faffcadc9 scene(bridge): 更新天桥、诊所外与持久化场景
Made-with: Cursor
2026-04-10 20:21:49 +08:00
bottlefish df754f6e68 feat(scene-mgmt): 重命名 Prologue 为 Day0 并更新 SceneSO 章节信息
Made-with: Cursor
2026-04-10 20:21:41 +08:00
bottlefish 0fc2214f11 dialog(localization): 更新 FP_Day2_night 与 FP_Peipei2 本地化表
Made-with: Cursor
2026-04-10 20:21:32 +08:00
bottlefish 3b62447ce0 yarn(story): 更新 FP 与 Fiction 多段对话剧本
Made-with: Cursor
2026-04-10 20:21:23 +08:00
bottlefish 3edff2e001 feat(content): 添加诊所外场景 GameContent 资源
Made-with: Cursor
2026-04-10 20:21:15 +08:00
bottlefish 3f51b39795 art(assets): 更新章节封面与梦境楼梯间美术资源
Made-with: Cursor
2026-04-10 20:21:03 +08:00
bottlefish 05a17ed0f6 art(bridge): 更新天桥 Timeline 与接树叶动画
Made-with: Cursor
2026-04-10 20:20:52 +08:00
bottlefish 3cbd668fc7 fix(fix-system): 调整火山热图温度映射区间
Made-with: Cursor
2026-04-10 20:19:04 +08:00
dingyuntian fb291e899a fix: 修复切除部分没有声音的问题 2026-04-10 19:44:25 +08:00
bottlefish 61690f85d7 Merge branch 'develop' into 4.4美术接入 2026-04-10 13:50:03 +08:00
bottlefish fafc3aa460 chore(addressables): Bridge/HuoshanFix 资源组
Made-with: Cursor
2026-04-10 13:35:52 +08:00
bottlefish 60b0060239 feat(scene): 场景与 SceneSO 调整
Made-with: Cursor
2026-04-10 13:35:41 +08:00
bottlefish cd8d2cb360 feat(art): 天桥/梦境/火山美术资源
Made-with: Cursor
2026-04-10 13:35:25 +08:00
bottlefish 713c2ea076 feat(anim): 火山头疼 Timeline 资源
Made-with: Cursor
2026-04-10 13:35:14 +08:00
bottlefish 35f3248381 feat(locale): 本地化与 Yarn 同步
Made-with: Cursor
2026-04-10 13:35:05 +08:00
bottlefish 73d32404da feat(dialog): 多剧本 Yarn 更新
Made-with: Cursor
2026-04-10 13:34:54 +08:00
bottlefish 164edcb9cf chore(render): glitch 后处理权重微调
Made-with: Cursor
2026-04-10 13:34:37 +08:00
bottlefish 5da5c11058 fix(gameplay): 热图降温目标与火山表情淡入强度
Made-with: Cursor
2026-04-10 13:34:18 +08:00
bottlefish 7b3cf9bf61 feat(heat): Test.mat 热图材质绑定新纹理
Made-with: Cursor
2026-04-10 13:33:46 +08:00
bottlefish 1543a151a7 feat(assets): 热图模块渐变纹理与 UF 资源
Made-with: Cursor
2026-04-10 13:31:03 +08:00
dingyuntian 2992f886d5 Merge branch 'feature/Animator管理器' into 'develop'
Feature/animator管理器

See merge request aibis-dream/aibis-dream!667
2026-04-10 05:25:45 +00:00
dingyuntian a0720e72b5 test: 添加AnimatorKit测试资源 2026-04-10 13:24:16 +08:00
dingyuntian 75c71b394d feat(scene-mgmt): 添加AnimatorKit动画管理器 2026-04-10 13:24:04 +08:00
dingyuntian bc8bf2c183 feat(editor): 添加HandlerNameCollector工具 2026-04-10 13:22:43 +08:00
dingyuntian bcb5e90938 chore: 删除TimelineNameCollector旧工具 2026-04-10 13:22:01 +08:00
dingyuntian 65f6b03ff6 chore: 更新HandlerNameCollector缓存路径配置 2026-04-10 13:21:35 +08:00
bottlefish a1643efb32 Merge branch 'develop' into 4.4美术接入 2026-04-09 12:04:36 +08:00
dingyuntian 3515c73cba Merge branch 'bugfix/降温图片找回' into 'develop'
fix: 找回删除的图片

See merge request aibis-dream/aibis-dream!666
2026-04-09 04:01:49 +00:00
dingyuntian 6317ebd167 fix: 找回删除的图片 2026-04-09 12:00:54 +08:00
bottlefish 4c54061e87 演出接入 2026-04-08 22:16:16 +08:00
bottlefish c2a9a44d42 Merge branch 'develop' into 4.4美术接入 2026-04-08 18:29:37 +08:00
bottlefish 7f2d12b5a5 美术合入 2026-04-08 18:28:27 +08:00
dingyuntian 179b785bf9 Merge branch 'feature/环境问题' into 'develop'
feat: 单独控制环境中的物体

See merge request aibis-dream/aibis-dream!665
2026-04-08 06:02:21 +00:00
dingyuntian 2f6177cd33 feat: 单独控制环境中的物体 2026-04-08 13:57:46 +08:00
dingyuntian 04fae6c47c Merge branch 'bugfix/打孔带重构' into 'develop'
fix: 托盘和黑边显示问题

See merge request aibis-dream/aibis-dream!664
2026-04-08 03:54:46 +00:00
bottlefish 79997281c0 合入美术表现 2026-04-07 20:27:51 +08:00
bottlefish 3665f617f4 动画接入 2026-04-04 21:48:59 +08:00
bottlefish b0bb0cdeda Merge branch 'feature/day2梦境接通' into 'develop'
Feature/day2梦境接通

See merge request aibis-dream/aibis-dream!663
2026-04-03 08:37:57 +00:00
bottlefish 6c4eacd9bb 串通 2026-04-03 16:36:36 +08:00
dingyuntian 357ee4898e fix: 托盘和黑边显示问题 2026-04-02 16:09:47 +08:00
bottlefish 948b9a335b 完善梦境脚本 2026-04-02 11:52:27 +08:00
5352 changed files with 304832 additions and 867991 deletions
@@ -0,0 +1,140 @@
---
name: "source-command-split-commit"
description: "将当前未提交改动拆分为多个符合规范的原子提交"
---
# source-command-split-commit
Use this skill when the user asks to run the migrated source command `split-commit`.
## Command Template
将当前未提交改动拆分为多个原子提交。执行前会分析变更、生成分组方案,经用户确认后逐组提交。
## 执行流程
### Step 1: 前置检查
1. **清理残留文件**:若存在 `msg.txt`,先 `rm -f msg.txt`
2. **读取规范**:确认 `COMMIT_CONVENTION.md` 存在且可读
### Step 2: 保护分支检查
保护分支列表(按优先级):`AGENTS.md` > `.cursor/rules/` > `.git/config branch.protected` > 默认(`master|main|develop`)
若当前在保护分支,询问用户:
- **A** - 创建 feature 分支后继续拆分(推荐)
- **B** - 转移到新分支后压成单提交
- **C** - 取消
### Step 3: 盘点变更
执行以下命令分析:
- `git status` - 整体状态
- `git diff --stat` - 文件统计
- `git diff` - 详细改动
- `git diff --cached` - 已暂存改动
### Step 4: 规则化分组
按"意图一致"原则分组,**优先按 type 分,再按 scope 分**。
**必须拆分的情况**
- 同一组可归属多个 type
- 同文件包含多个独立目的(如功能+重构)
- 资源替换与代码逻辑混合
**可以合并的情况**(最小拆分原则):
- 同一功能的多文件改动(脚本+Prefab)
- 同一模块的多个相关 Bug 修复
### Step 5: 生成方案表格
以表格形式展示拆分方案:
| 顺序 | 文件 | 变更目的 | 提交头 | 依据 | 方式 |
|------|------|----------|--------|------|------|
| 1 | `路径` | 描述 | `type(scope): subject` | 原因 | `git add`/`git add -p` |
**Type 选择优先级**`fix` > `feat` > `art` > `audio` > `scene` > `yarn` > `perf|refactor|...`
**Scope 规则**:单模块用模块名(如 `blockpuzzle`),跨模块省略。
### Step 6: 用户确认
询问:`[Y 执行 / n 取消 / 提出调整]`
- 若调整:根据反馈修改方案,再次确认
- 若执行:进入 Step 7
- 若取消:清理并退出
### Step 7: 逐组提交
对每组执行:
1. `git add <file>``git add -p <file>`
2. 复查:`git diff --cached --stat``git diff --cached`
3. 写入 `msg.txt` 并提交:`git commit -F msg.txt`
4. 检查状态:`git status`
**失败处理**
- `git add -p` 跳过关键 hunk → 暂停,提示处理方案
- pre-commit hook 失败 → 询问 [R]重试 [S]跳过 [A]中止
### Step 8: 结果校验
输出:
1. `git log --oneline -<N>`
2. 回放清单(验证 type/scope/subject 合规性)
3. 若不合规,询问是否修正
### Step 9: 清理
`rm -f msg.txt`
---
## Unity 改动速查表
| 改动类型 | type | scope 示例 |
|----------|------|-----------|
| 脚本逻辑修复 | `fix` | `blockpuzzle`, `dialog` |
| 脚本新功能 | `feat` | `huoshan`, `clue` |
| 美术资源(贴图/模型/动画) | `art` | 对应模块或省略 |
| 音频资源 | `audio` | `audio-kit` 或省略 |
| 场景配置(灯光/镜头) | `scene` | 对应模块或省略 |
| Yarn 对话 | `yarn` | `dialog` |
| 测试代码 | `test` | 对应模块 |
---
## 规范校验清单(执行前检查)
- [ ] 格式:`<type>(<scope>): <subject>``<type>: <subject>`
- [ ] type ∈ {`feat`,`fix`,`art`,`audio`,`scene`,`yarn`,`perf`,`refactor`,`style`,`docs`,`build`,`chore`,`test`}
- [ ] subject:中文、动词开头、≤50字、无句号
- [ ] 一次提交只做一件事
---
## 中文提交方法
由于 PowerShell 中文乱码问题,统一使用文件方式:
```bash
echo "type(scope): 描述" > msg.txt
git commit -F msg.txt
rm -f msg.txt
```
---
## 关于 Pre-commit Hooks
若配置了 husky/lint-staged
- 每次提交都会触发检查
- 如需跳过(**不推荐**):`git commit -F msg.txt --no-verify`
- 跳过可能导致 CI 失败
---
开始执行请回复 **"开始"**,或告知需要的调整。
+17
View File
@@ -0,0 +1,17 @@
# Yarn 插线维修对话 — 快速审查
`FP_Huoshan1` 或同类 **BodyModule 插线 + `$gameStage` 多阶段**`.yarn` 做一轮检查。
## 用法
在对话里说明「按 yarn-plug-review 审查某文件/目录」或直接粘贴节点片段。
## 审查项(简版)
1. **跳转条件**:会 `jump` 结束/换阶段的块是否带 `$gameStage==…`,避免后期变量仍为 true 时回退阶段。
2. **重复插线**:同一 stage、同一模块第二次插线是否为短总结 + 下一步提示,而非全长对白与重复 `complete_task`
3. **TaskToDo**`TaskToDo--` 是否与「首次完成」绑定(如 `$speakerFinalOK` / `$saleFinalOK`),避免重复递减。
4. **无序插线**:是否已去掉「分支外无条件 set 完成标记」导致跳剧情。
5. **EventNode**`*PlugIn` 名称与 `moduleName`、目标 `title:` 一致。
完整说明与模式见:`.cursor/skills/yarn-plug-in-flow/SKILL.md`
@@ -0,0 +1,356 @@
---
name: 塔罗牌小游戏实现
overview: 基于 Sprite + EventTriggerEx 实现塔罗牌小游戏,Yarn 命令细粒度控制每个阶段(展示牌堆、展开、等待选牌、翻牌、设置朝向),与对话深度配合。
todos:
- id: tarot-card
content: 创建 TarotCard.cs -- 单张牌组件:IInteraction、SpriteRenderer 正反面、EventTriggerEx 注册、翻牌动画、朝向控制(正/倒)、悬停/点击回调
status: completed
- id: tarot-deck
content: 创建 TarotDeck.cs -- 牌堆管理:动态生成牌、扇形展开布局、悬停推出效果、选牌流程(FadeOut + 居中检视)
status: completed
- id: tarot-manager
content: 创建 TarotManager.cs -- 系统管理器:SystemDic 注册、视图开关、阶段状态机、选牌结果
status: completed
- id: tarot-yarn
content: 创建 TarotYarnCommand.cs -- 细粒度 Yarn 命令集(show/spread/wait_select/flip/set_orientation/rotate/hide
status: completed
isProject: false
---
# 塔罗牌小游戏实现计划
## 设计目标
Yarn 脚本能**细粒度控制**塔罗牌的每个视觉阶段,每个命令之间可以穿插对话,实现对话与交互的深度配合。
## Yarn 脚本示例(对应 FP_Day1_night 第 364-395 行的改造)
```yarn
ql: 我觉得也许你需要这个。
<<show_tarot_deck>>
【它拿出一套崭新的卡牌。】
me: 这是什么?
ql: "塔罗牌"。
ql: 试试吧。抽一张。
<<spread_tarot 5>>
-> 抽牌
<<wait_tarot_select>>
【你看了看牌。】
<<flip_tarot "inverted">>
// 翻到正面,但预设为倒置朝向(inverted)
【上面画着一个张开双臂的机体...像是...在把它吸过去。】
me: 这画的是什么?
ql: 倒吊人。你拿反了。
<<rotate_tarot "normal">>
// Yarn 命令改变朝向:倒置 -> 正向(旋转 180 度动画)
【你把牌转过来。】
【这下你看明白了。这是一个倒吊着的机体...】
// ...更多对话...
<<hide_tarot>>
```
## 架构概览
```mermaid
flowchart TD
Yarn["Yarn 脚本"] -->|"<<show_tarot_deck>>"| CMD["TarotYarnCommand\n(静态 Yarn 命令类)"]
Yarn -->|"<<spread_tarot 5>>"| CMD
Yarn -->|"<<wait_tarot_select>>"| CMD
Yarn -->|"<<flip_tarot inverted>>"| CMD
Yarn -->|"<<rotate_tarot normal>>"| CMD
Yarn -->|"<<hide_tarot>>"| CMD
CMD --> TM["TarotManager\n(SystemDic 注册)"]
TM --> TD["TarotDeck\n牌堆/扇形/选牌"]
TD --> TC["TarotCard x N\nSprite + EventTriggerEx"]
TC -->|IInteraction| ES["EventSystemEx"]
```
## 新增文件结构
```
Assets/Scripts/MiniGame/Tarot/
TarotManager.cs -- 管理器:SystemDic 注册、视图开关、阶段协调
TarotCard.cs -- 单张牌:IInteraction + 翻牌 + 朝向
TarotDeck.cs -- 牌堆逻辑:生成、展开、悬停、选牌
TarotYarnCommand.cs -- 细粒度 Yarn 命令集
```
## 核心设计
### 1. TarotCard -- 单张牌
参考 [KnobController](Assets/Scripts/MiniGame/HuoShan/SalesSystem/KnobController.cs) 的 EventTriggerEx 注册模式(行 165-199)。
- 实现 `IInteraction` 接口(`IsActive`, `IsAvailable`, `GetGameObject()`
- 需要 `Collider2D`BoxCollider2D,牌面大小)
- `SpriteRenderer` + 两个 Sprite 引用:`frontSprite` / `backSprite`
**朝向系统(Orientation**
- 枚举 `TarotOrientation { Normal, Inverted }`
- `Normal`:正面朝上(localScale.y = 1
- `Inverted`:倒置(localScale.y = -1,即上下颠倒)
- `SetOrientation(orientation, animated)` 方法,animated 时用 DOTween 做 Y 轴 scale 过渡
**翻牌动画**
- `Flip(targetOrientation, duration)` 协程
- DOTween 缩放 localScale.x: 1 -> 0(前半),切换 Sprite,再 0 -> 1(后半)
- 翻牌完成时根据 `targetOrientation` 设置 localScale.y
**交互注册**Awake):
- 获取/添加 `EventTriggerEx`
- `EnsureEventTriggerEntries()` 确保 PointerEnter/PointerExit/PointerClick 条目
- `Register(PointerEnter, OnHoverEnter)` / `Register(PointerExit, OnHoverExit)` / `Register(PointerClick, OnClick)`
- 回调通过 `System.Action` 委托给 `TarotDeck`
**状态**
- `bool IsFaceUp` -- 当前是否正面朝上
- `TarotOrientation Orientation` -- 当前朝向
- `bool IsInteractable` -- 是否接受交互(由 Deck 控制)
### 2. TarotDeck -- 牌堆管理
**动态生成**
- `Setup(int cardCount, Sprite front, Sprite back)` -- 创建 N 个 TarotCard 子物体
- 每张牌初始背面朝上,叠在一起(牌堆状态)
**扇形展开**
- `SpreadCards(float duration)` 协程
- 配置项:`fanAngleRange`(扇形总角度,如 120 度)、`fanRadius`(扇形半径)
- 每张牌的目标角度:`centerAngle + (i - (count-1)/2f) * angleStep`
- 每张牌的目标位置:以旋转角度沿圆弧分布
- DOTween `DOLocalMove` + `DOLocalRotate` 同步动画
**悬停推出**
- 牌注册的 `OnHoverEnter` -> 沿牌面法线方向(局部 Y 轴上方)推出一段距离
- `OnHoverExit` -> 回到扇形位置
- 用 DOTween `DOLocalMove`,设短 duration0.15s
**选牌**
- `OnCardClicked(TarotCard card)` -- 锁定交互,触发选牌序列
- 其余牌 `SpriteRenderer.DOFade(0, 0.3f)` 渐隐
- 选中牌移至检视位置(屏幕中央偏上),放大到检视尺寸
- 设置 `_selectedCard`,通知 Manager 选牌完成
**检视位置**
- `[SerializeField] Transform inspectPosition` -- 检视锚点
- `[SerializeField] float inspectScale` -- 检视缩放
### 3. TarotManager -- 管理器
- `Start()``FixSystemCenter.SystemDic.Register(this)`
- 持有 `TarotDeck` 引用(`GetComponentInChildren`
- 状态标记:`_isWaitingForSelection`(是否在等待玩家选牌)、`_isCardSelected`
- `OpenView()` / `CloseView()` 控制根 GameObject 显隐
- `WaitForSelection()` 协程 -- while 循环等待 `_isCardSelected`,供 YarnCommand 使用
- `SelectedCard` 属性 -- 获取当前选中的牌
### 4. TarotYarnCommand -- 细粒度 Yarn 命令集
所有命令通过 `FixSystemCenter.SystemDic.Get<TarotManager>()` 获取 Manager 实例。
| Yarn 命令 | 方法签名 | 说明 |
| ----------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------- |
| `<<show_tarot_deck>>` | `ShowTarotDeck(int count = 5)` | 显示牌堆(叠放状态),可指定牌数 |
| `<<spread_tarot>>``<<spread_tarot 5>>` | `IEnumerator SpreadTarot(int count = -1)` | 扇形展开,count=-1 用已有牌数;协程等待展开动画完成 |
| `<<wait_tarot_select>>` | `IEnumerator WaitTarotSelect()` | 阻塞 Yarn 直到玩家点击选中一张牌;选中后执行选牌动画(其余牌淡出、选中牌居中);结果写入 `$tarotSelectedIndex` |
| `<<flip_tarot "normal">>` | `IEnumerator FlipTarot(string orientation = "normal")` | 翻牌到正面,同时设置朝向;`"normal"` = 正向,`"inverted"` = 倒置 |
| `<<rotate_tarot "normal">>` | `IEnumerator RotateTarot(string orientation)` | 改变已翻开牌的朝向(旋转动画),不翻面 |
| `<<hide_tarot>>` | `IEnumerator HideTarot()` | 淡出并隐藏所有塔罗元素 |
### 5. 完整交互流程
```mermaid
stateDiagram-v2
[*] --> DeckVisible: show_tarot_deck
DeckVisible --> DeckVisible: 对话继续\n(牌堆静态展示)
DeckVisible --> FanOut: spread_tarot
FanOut --> WaitSelect: wait_tarot_select
WaitSelect --> WaitSelect: hover 推出/回位
WaitSelect --> Selected: 玩家点击选牌
Selected --> InspectBack: 其余牌淡出\n选中牌居中(背面)
InspectBack --> InspectBack: 对话继续\n(背面检视)
InspectBack --> InspectFront: flip_tarot\n(预设朝向)
InspectFront --> InspectFront: 对话继续
InspectFront --> InspectRotated: rotate_tarot\n(改变朝向)
InspectRotated --> InspectRotated: 对话继续
InspectRotated --> [*]: hide_tarot
InspectFront --> [*]: hide_tarot
```
关键:每个状态之间 Yarn 对话可以自由穿插,命令只推进视觉阶段,不阻塞对话本身(除了 `wait_tarot_select` 是阻塞等待玩家交互)。
### 6. Sprite 资源
当前使用 `Assets/RawResources/Art/CG/塔罗牌/塔罗牌正面.png``塔罗牌背面.png`,所有牌共用同一对正反面。后续可扩展为每张牌不同正面。
## 场景配置
### 层级结构
```
TarotSystem -- 空 GameObject,放在需要的场景中
├── TarotManager (Component) -- 管理器脚本
├── TarotView -- 视图容器(OpenView/CloseView 控制这个)
│ ├── TarotDeck (Component) -- 牌堆逻辑脚本
│ │ └── [运行时动态生成的牌] -- TarotCard x N
│ │
│ └── InspectAnchor -- 检视锚点(空物体,标记牌选中后居中的位置)
│ (Transform 位置设在画面中央偏上)
└── (可选) Cinemachine Camera -- 如果塔罗需要独立相机视角
```
### 需要手动配置的部分
**1. TarotSystem 根物体**
- 放在对应场景中(如 Day1_night 所在场景)
- 位置在世界空间中合适的地方(与当前相机视角对齐)
**2. TarotManager 组件(Inspector 面板)**
```
[Header("视图")]
TarotView -- 拖入 TarotView 子物体(或自动 Find
[Header("牌面素材")]
FrontSprite -- 拖入 塔罗牌正面 Sprite
BackSprite -- 拖入 塔罗牌背面 Sprite
[Header("牌 Prefab")]
CardPrefab -- 拖入预制的单张牌 Prefab(见下方)
```
**3. 单张牌 Prefab(预制体)**
需要提前制作一个 Prefab,结构如下:
```
TarotCard (Prefab)
Components:
- SpriteRenderer (默认 sprite = 背面,sortingOrder 按需)
- BoxCollider2D (Size 匹配牌面大小,用于点击/悬停检测)
- EventTriggerEx (Inspector 中添加 3 个 Trigger 条目:
PointerEnter, PointerExit, PointerClick)
- TarotCard.cs (脚本自动注册事件)
```
EventTriggerEx 的 Inspector 配置要点:
- 点击 "Add New Event Type"
- 分别添加 `PointerEnter``PointerExit``PointerClick` 三个条目
- 回调列表留空(代码中 `Register` 动态绑定)
> 这与 KnobController 的模式一致:EventTriggerEx 需要 Inspector 中预配好条目,代码中 `TryGetTriggerEvent` 才能找到对应的 callback list。代码中也会 `EnsureEventTriggerEntries()` 作为保底自动添加。
**4. TarotDeck 组件(Inspector 面板)**
```
[Header("扇形展开参数")]
FanAngleRange = 120 -- 扇形总角度(度)
FanRadius = 3.0 -- 扇形半径(世界单位)
FanCenter = (0,0,0) -- 扇形圆心偏移
[Header("悬停")]
HoverOffset = 0.5 -- 悬停推出距离
HoverDuration = 0.15 -- 悬停动画时长
[Header("检视")]
InspectAnchor -- 拖入 InspectAnchor 子物体
InspectScale = 1.5 -- 检视模式缩放倍数
[Header("动画时长")]
SpreadDuration = 0.6 -- 展开动画时长
FadeOutDuration = 0.3 -- 未选中牌淡出时长
MoveToInspectDuration = 0.4 -- 选中牌移至检视位置时长
```
### 运行时流程
```mermaid
sequenceDiagram
participant Y as Yarn
participant M as TarotManager
participant D as TarotDeck
participant C as TarotCard
Y->>M: show_tarot_deck(5)
M->>M: OpenView()
M->>D: Setup(5, frontSprite, backSprite)
D->>D: Instantiate CardPrefab x 5
D->>C: 初始化 (背面, 叠放)
Note over Y: 对话继续...
Y->>D: spread_tarot
D->>C: DOTween 扇形展开动画
Note over Y: 对话继续...
Y->>M: wait_tarot_select (阻塞)
Note over C: 玩家悬停/点击
C-->>D: OnCardClicked
D->>D: 其余牌淡出, 选中牌居中
D-->>M: 选牌完成
M-->>Y: 协程返回
Note over Y: 对话继续...
Y->>M: flip_tarot("inverted")
M->>C: Flip(inverted)
C->>C: X轴缩放动画 + 切换Sprite + Y轴倒置
Note over Y: 对话: "你拿反了"
Y->>M: rotate_tarot("normal")
M->>C: SetOrientation(normal, animated=true)
C->>C: Y轴旋转动画 (倒置->正向)
Note over Y: 对话继续...
Y->>M: hide_tarot
M->>D: 淡出所有牌
M->>M: CloseView()
```
### 不需要手动做的部分(代码自动处理)
- 牌的动态实例化和销毁(`TarotDeck` 负责)
- EventTriggerEx 事件绑定(`TarotCard.Awake` 自动注册)
- SystemDic 注册(`TarotManager.Start` 自动注册)
- 扇形位置计算(`TarotDeck` 按参数自动排布)
## 关键技术点
- **EventTriggerEx 注册模式**:参考 `KnobController.Awake()` (行 165-199),需先 `EnsureEventTriggerEntries()` 确保 triggers 列表有对应条目,再调用 `Register(EventTriggerType, callback)`
- **DOTween 动画**:项目已引入 `DG.Tweening`,用于扇形展开、悬停推出、翻牌、FadeOut
- **IInteraction 实现**`IsActive` / `IsAvailable` 控制交互可用性,配合 `EventSystemEx.isLocked` 在对话期间自动禁用交互(对话进行时 `isLocked=true``wait_tarot_select` 时对话暂停所以 `isLocked=false`,交互自然可用)
- **SystemDic 注册**`FixSystemCenter.SystemDic.Register(this)` 使 YarnCommand 能通过 `Get<TarotManager>()` 获取实例
- **Yarn 变量写入**`StorageSystem.Instance.SetValue("$tarotSelectedIndex", value)` 将选牌结果传回 Yarn
@@ -0,0 +1,94 @@
---
name: narrative-timeline-flow
description: >-
叙事 Timelineaseprite/静图改 Timeline、Addressable 登记、黑屏防穿帮、替换 show_full_screen。
Use when 加叙事 timeline、aseprite 转 timeline、英里初登场、CG 演出、替换 show_full_screen、
hide_full_screen、黑屏穿帮、fade_out 时机、wait_timeline、Day1 begin / Day2 night 天桥 CG、
或用户提到 TimelineKit 叙事接入。
---
# 叙事 Timeline 接入(Aseprite / 静图 → Yarn
API 细节见 [`Assets/Scripts/SceneManagement/TimelineKit/README_TimelineKit.md`](../../Assets/Scripts/SceneManagement/TimelineKit/README_TimelineKit.md)。本 skill 只记**本仓库约定与踩坑**。
## 1. 何时用哪种 Yarn 命令
| 场景 | 命令 |
|------|------|
| 播完再往下走 | `<<play_timeline 名>>`(阻塞) |
| 黑屏下先起播 / 与对白并行 / Loop CG | `<<start_timeline 名>>` |
| 已 `start_timeline`,要等播完 | `<<wait_timeline 名>>`(不重头播) |
| 关掉整棵 Director GO | `<<hide_timeline 名>>` |
名称与场景里 `DirectorHandler.timelineName` **完全一致**(叙事多用中文单名,内嵌 Playable)。
## 2. 资产与 Addressable
1. Aseprite → `Assets/GameContent/Scene_{场景}/animationRaw/`(或现有 `AnimationRaw/`
2. Timeline → `GameContent/Scene_{场景}/Timeline/{名}.playable`
3. Addressable Group = 对应场景线(如 `Scene_ClinicOut``Scene_Bridge`
4. Address = `Timeline/{名}`(必须带 `Timeline/` 前缀)
5. 叙事 Timeline **一般不**加 `TimelineAssetRef`(维修玩法才用)
参考范本:`医生出地铁``day2夕阳医生看佩佩侧脸``英里初登场`
## 3. 场景 GO 约定
- 根节点挂 `PlayableDirector``Play On Awake = false`+ `DirectorHandler`
- **根节点默认 Active**`Start` 里要注册到 `TimelineCenter`;关掉根节点会注册失败
- **带 Sprite 的子物体(如 `Visual`)默认 Inactive**;用 ActivationTrack 或播放时再显
- `hide_timeline` = 根 `SetActive(false)`;之后再播时 `PlayTimeline` 会重新打开
- 全屏 CG 常需调位置(例:天桥侧脸根节点 `Y = -5`);PPU / scale 按相机再调
## 4. 黑屏防穿帮(必做)
**错误**`fade_out` 揭开场景 → 再 `play_timeline`(先露底再播 CG)。
**正确**(切场景后的 intro CG):
```yarn
<<fade_in>>
<<load_scene Scene/ClinicOut>>
<<init_actor ...>>
<<start_timeline 英里初登场>>
<<wait 0.2>>
<<fade_out>>
<<wait_timeline 英里初登场>>
<<hide_timeline 英里初登场>>
<<show_actor ...>>
<<play_timeline 医生出地铁>>
```
要点:黑屏内起播并稳住首帧 → 再结束黑屏 → 再等播完。CG 盖住初登场时,角色可延后到 `hide_timeline` 后再 `show_actor`
## 5. 替换 `show_full_screen` 静图
常驻 / Loop 演出(对白期间一直播):
```yarn
<<start_timeline day2夕阳医生看佩佩侧脸>>
<<fade_out 1>>
# ... 对白 ...
<<fade_in 1>>
<<hide_timeline day2夕阳医生看佩佩侧脸>>
<<fade_out 1>>
```
不要再用 `show_full_screen` / `hide_full_screen` 播同一段。
## 6. 其它坑
- `<<load_scene>>` **只需场景名**。旧脚本第二参数(如 `1`)已兼容忽略,**新代码不要写**。
-`.yarn`**不要动**已有 `#line:`;新行不加 `#line:`
- 只改运行时实际绑定的 Yarn(如 Day1_begin → FP,不是 Fiction 草稿)。
- Aseprite 多 tag:选主演出 clipLoop CG 用循环 clip + Director `Loop` 或长轨;一次性 intro 用有限时长 + `wait_timeline`
## 7. 落地检查清单
- [ ] Timeline 在正确 `GameContent/Scene_* /Timeline/`
- [ ] Addressable`Timeline/{名}` + 正确 Group
- [ ] `timelineName` 与 Yarn 一致;`Play On Awake = false`
- [ ] Visual 默认关;进 Play 不会提前露 CG
- [ ] 切场景 intro:黑屏内 `start` → 短 wait → `fade_out``wait_timeline`
- [ ] 静图替换:`start` / `hide` 成对
- [ ] 跑一次确认无先露场景、无 `load_scene` 参数错误
+103
View File
@@ -0,0 +1,103 @@
---
name: yarn-plug-in-flow
description: >-
Yarn 维修插线对话:gameStage 调度、重复插线短总结、防状态回退与 TaskToDo 重复递减。
Use when 写 FixSystem/BodyModule 插线 Yarn、从自然语言生成维修对话、审阅 HuoShan 类流程、
检查 PlugIn 节点、重复执行、插线检查、或用户提到 yarn 插线/维修界面对话。
---
# Yarn 插线维修对话(FixSystem
本 skill 适用于:`BodyModule` 通过 `{moduleName}PlugIn` 跳入 Yarn、同一节点内用 `$gameStage` 分支多阶段叙事(如 `FP_Huoshan1`)。
## 1. C# 侧事实(设计 Yarn 前必读)
- 插线触发:`BodyModule.PlugIn()``DialogController.StartDialogNode(data.PlugInNodeName)`,节点名为 **`{moduleName}PlugIn`**(见 `BodyModuleData.PlugInNodeTemplate`)。
- **C# 一般不按阶段禁用插线**;若某 Stage 实际不可插线,是场景/交互配置决定,但 Yarn 仍应防御性处理「错误阶段误入」。
- EventNode 里用 `<<jump 检查xxx>>` 桥接到共享节点;共享节点名须与 `*.yarn``title:` 一致。
## 2. 状态变量约定(推荐)
| 用途 | 模式 |
|------|------|
| 流程阶段 | `$gameStage`(数字;仅在 `Center` 等少数节点切换,避免散落 `set` |
| 某模块「本档首次插线检查完」 | `$xxxChecked`(如 `$speakerChecked` / `$colorChecked` / `$emoChecked` |
| 多子任务计数 | `$TaskToDo`,完成时 `--`,**跳转前务必保证不会重复减** |
| 同一 Stage 内「最终检查子项已完成」 | 独立布尔,如 `$speakerFinalOK` / `$saleFinalOK`,防止重复插线再次 `complete_task``--` |
`VarsInit`(或等价入口)用 `<<declare>>` 初始化所有布尔/计数器,避免未定义行为。
## 3. 从自然语言 / 大纲生成 Yarn 时的必做项
1. **列出插线入口**:每个 `*PlugIn` → 目标节点;与模块 `moduleName` 一致。
2. **列出 `$gameStage` 与文档步骤映射**(如 `02_flow.md`),每个共享检查节点有哪些 `elseif $gameStage==N`
3. **任何会 `<<jump>>` 换阶段的块**:条件里加 **`$gameStage==N`**,避免后期 `$xxxChecked` 仍为 true 时误触发旧结局(状态回退)。
4. **同一 `$gameStage` 内、同一模块第二次插线**
- 首次:完整演出 + `complete_task` + 设 `$xxxChecked` / `$xxxFinalOK`
- 再次:`<<if $xxxChecked == true>>`(或对应 FinalOK)→ **短总结 + 自言自语下一步**(去查哪个模块、任务提示),**不**重复长对白、**不**重复任务完成副作用。
5. **多分支共享「完成标记」**:勿在 `if/elseif` 外无条件 `set $colorChecked=true` 等;否则乱序插线会跳过叙事(见此前 HuoShan Stage2 修复)。
## 4. 已完成 Yarn 的审查清单(可逐项打勾)
### 4.1 阶段与跳转
- [ ] 会推进阶段或结束的 `<<jump>>`,外层条件是否包含正确的 `$gameStage`
- [ ] `Center` / Stage 入口是否唯一或清晰,避免死循环、重复 `set gameStage`
### 4.2 重复插线(同 Stage、同模块)
- [ ] Stage2 类:表达 / 销售 / 情绪等主流程,第二次插线是否为「短总结 + 下一步」而非全长?
- [ ] Stage7 类双任务:是否用 **FinalOK**(或等价)避免第二次插线再次 `TaskToDo--` / 重复 `complete_task`
### 4.3 变量与任务
- [ ] `complete_task``$TaskToDo` 递减是否一一对应、且只在「首次成功路径」执行?
- [ ] `$PlugCount` 等统计是否与「首次访问」一致(可用 `visited("NodeName")``== false` 包裹)?
### 4.4 文档与代码
- [ ] `EventNode` 表与 `title:` 节点名一致(如 `SalePlugIn` 非过期的 `ColorPlugIn`)。
## 5. 最小代码模式参考(Yarn 片段)
**防跨阶段误触发结局:**
```yarn
<<if $gameStage==2 && $speakerChecked==true && $colorChecked==true>>
// 波形对比 → 结束本阶段
<<endif>>
```
**同 Stage 重复插线(短总结):**
```yarn
<<if $gameStage==2>>
<<if $speakerChecked == true>>
me: (总结结论)…
me: (下一步该做什么)…
<<else>>
// 首次完整流程
<<set $speakerChecked = true>>
<<endif>>
<<endif>>
```
**Stage7 双任务防重复:**
```yarn
<<elseif $gameStage==7>>
<<if $speakerFinalOK == true>>
me: 已确认…若还有任务去查另一模块。
<<else>>
<<complete_task "...">>
<<set $speakerFinalOK = true>>
<<set $TaskToDo = $TaskToDo - 1>>
<<endif>>
<<endif>>
```
## 6. 项目内参考
- 实例:`Assets/Resources/Yarn/FP_Huoshan1/Stage2.yarn``检查表达` / `检查销售模块` / `检查情绪`)、`Center.yarn``VarsInit`)。
- 流程定义:`Docs/HuoShan/core/02_flow.md`
- C#`Assets/Scripts/FixSystemNew/BodyModule/Modules/BodyModule.cs``PlugIn`)。
+2 -1
View File
@@ -75,7 +75,8 @@
*.psd filter=lfs diff=lfs merge=lfs -text
*.psb filter=lfs diff=lfs merge=lfs -text
*.bank filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
# DOTween editor images - exclude from LFS (stored as regular binaries, Unity re-import causes false changes)
Assets/Plugins/Demigiant/DOTween/Editor/Imgs/*.png -filter -diff -merge
Assets/Plugins/Demigiant/DOTween/Editor/Imgs/*.jpg -filter -diff -merge
Assets/Plugins/Demigiant/DOTween/Editor/Imgs/*.jpg -filter -diff -merge
+7 -2
View File
@@ -88,9 +88,12 @@ crashlytics-build.properties
# Ignore the Cache folder since it is updated locally.
/[Aa]ssets/Plugins/FMOD/Cache/*
# TimelineNameCollector editor cache (regenerated on scan)
# TimelineNameCollector editor cache (legacy path; migration only)
/[Aa]ssets/[Ee]ditor/TimelineNameCollector/Cache/*
# HandlerNameCollector editor cache (regenerated on scan)
/[Aa]ssets/[Ee]ditor/HandlerNameCollector/Cache/*
# 暂时把bank放到Stream里,不忽略
# Ignore bank files in the StreamingAssets folder.
/[Aa]ssets/StreamingAssets/**/*.bank
@@ -107,4 +110,6 @@ fmod_editor.log
.history/*
**/.history/*
.fake
.fake
Keystore/aibis-release-key.keystore
Keystore/keystore.bat
+65 -1
View File
@@ -56,6 +56,70 @@
"temp/": true,
"Temp/": true
},
"files.associations": {
"**/Plugins/Android/*Template.gradle": "plaintext"
},
"dotnet.defaultSolution": "aibis-dream.sln",
"dotnet.preferCSharpExtension": true
"dotnet.preferCSharpExtension": true,
"cSpell.enableFiletypes": [
"yarnspinner"
],
"cSpell.languageSettings": [
{
"languageId": "yarnspinner",
"patterns": [
{
"name": "yarn-commands",
"pattern": "/<<[^>]*>>/g"
},
{
"name": "yarn-expressions",
"pattern": "/\\{[^}]*\\}/g"
},
{
"name": "yarn-variables",
"pattern": "/\\$[a-zA-Z_][a-zA-Z0-9_]*/g"
},
{
"name": "yarn-markup",
"pattern": "/\\[[^\\]]*\\]/g"
},
{
"name": "yarn-hashtags",
"pattern": "/#[a-zA-Z_][a-zA-Z0-9_:-]*/g"
},
{
"name": "yarn-node-markers",
"pattern": "/^(---|===)$/gm"
},
{
"name": "yarn-headers",
"pattern": "/^(title|tags|position|color|colorID|tracking|custom|style):.*/gm"
},
{
"name": "yarn-character-names",
"pattern": "/^\\s*[A-Za-z_][A-Za-z0-9_]*(?=:)/gm"
},
{
"name": "yarn-comments",
"pattern": "/\\/\\/.*$/gm"
}
],
"ignoreRegExpList": [
"yarn-commands",
"yarn-expressions",
"yarn-variables",
"yarn-markup",
"yarn-hashtags",
"yarn-node-markers",
"yarn-headers",
"yarn-character-names",
"yarn-comments"
]
}
],
"cSpell.words": [
"Aibis",
"Newtonsoft"
]
}
+152
View File
@@ -0,0 +1,152 @@
# AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
## Project Overview
AIBIS Dream (also known as "AllOurBrokenParts") is a Unity-based narrative adventure game with investigative elements. The project uses Yarn Spinner for dialogue, features a custom framework with modular systems, and includes multiple mini-games and a modular repair/fix system.
**Unity Version:** 2022.3.7f1c1 (Universal Render Pipeline)
## Architecture
### Core Framework (Assets/Scripts/Framework/)
Modular framework with Kit-pattern utilities:
- **ActionKit**: Action sequencing and command pattern
- **AudioKit**: Audio management (includes FMOD integration)
- **Config / Core**: Configuration and core utilities
- **EventSystemKit**: Decoupled event system
- **LogKit**: Structured logging with categories
- **PoolKit**: Object pooling
- **ResourceKit**: Asset loading via `ResourceSystem` (Addressables-based)
- **Note**: New code must use `ResourceSystem`, never `Resources.Load`. See [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md). Asset placement rules in [Resource Management Spec](Docs/ResourceManagementSpec.md).
- **SelectorKit**: Weighted/batch/non-repeat random selection
- **SingletonKit**: Singleton base classes
- **StateMachineKit**: State management
- **OtherKit**: Miscellaneous utilities (`CameraKit`, `AnimatorKit`, `FadeKit`, `LocalizationKit`, etc.)
- **Attribute**: Custom editor attributes
### Key Game Systems
- **Game Loop** (Scripts/Game Loop/): `GameManager` (Singleton entry), `YarnVariableStorage` (Yarn variables), `SaveRestoreOrchestrator` / `SaveSystem` (save snapshots), `SceneLoader`, `ChapterController`, `TalkSceneSO`
- **Dialog System** (Scripts/Dialog System/): Yarn Spinner integration — `DialogController`, `LineRunner`, `LocalisedLineProvider`, `BaseYarnCommand`
- **SceneManagement** (Scripts/SceneManagement/): `ActorKit/`, `AnimatorKit/`, `EnvironmentKit/`, `SpawnerKit/`, `TimelineKit/`, `SceneCenter/`, `DreamDoorSystem`
- **UI** (Scripts/UI/): `DialogUI/`, `Panel/`, `Form/`, `Components/`, `Cursor/`, `UIManager`
- **MiniGame** (Scripts/MiniGame/): `HuoShan/` (language particles, EmotionWave), `BlockPuzzle/`, `Peipei/` (Eye, CutLine, Memory)
- **FixSystem** (Scripts/FixSystem/): Legacy — HeatMap, PunchTape, Memory, screen effects
- **FixSystemNew** (Scripts/FixSystemNew/): Active repair system — `FixSystemCenter`, `BodyModule/`, `Cable/`, `WhackMole/`, `Screen System/`, `Open/`, `Clinic/`
- **AssetRefs** (Scripts/AssetRefs/): Addressable key constants (`ConstRef`, `FixAssetRef`, `TimelineAssetRef`)
### Content & Localization
- **Assets/GameContent/**: Runtime assets by scope (`Common/`, `UI/`, `Scene_{Name}/`, `Feature_{Name}/`)
- **Assets/Language/**: Unity Localization tables (runtime active: `UIText/`, `Locales/`, `Sprite/`; `Dialog/``ActorName/``Params/` currently kept as migration backups)
- **Assets/Resources/Yarn/**: Current Yarn dialogue files (target migration to `Assets/Narrative/` per spec, not yet complete)
- **Assets/StreamingAssets/Config/**: CSV-driven localized gameplay data (`character.csv` for actor names, `params.csv` for `l10n.*` substitution terms)
- **Assets/Prototype/**: HTML/JS prototypes for mechanic testing
## Critical Dependencies
### Unity Packages
- **Yarn Spinner** 3.2.2 (OpenUPM), **Addressables** 1.21.21, **Cinemachine** 2.9.7
- **URP** 14.0.8, **TextMesh Pro** 3.0.6, **Timeline** 1.7.5, **Localization** 1.4.5
- **Visual Effect Graph** 14.0.8, **Visual Scripting** 1.8.0
### Third-Party
- **FMOD** (audio engine), **DOTween** (tweening), **Shapes** (vector graphics)
- **More Mountains Feedbacks** (game feel), **Destructible 2D**
## Development Guidelines
### Code Organization
1. Each major system self-contained in its own folder under `Assets/Scripts/`
2. Kit-pattern utilities in `Framework/` — reusable across systems
3. Singleton pattern for global managers (derived from `SingletonKit`)
4. Favor composition over inheritance
5. All runtime asset loading via `ResourceSystem` — never `Resources.Load`
### Scene Structure
- Scenes in `Assets/Scenes/`; `Persistence` scene hosts global managers and Persistent Loader
- Fix scenes: `{Character}FixScene` (e.g. `HuoShanFixScene`, `PeipeiFixScene`, `ExpressFixScene`)
- Test scenes: `languageTest`, `HuoShanExpressionTest`, `wavetest`, etc.
### Addressables
- Config in `Assets/AddressableAssetsData/`; Groups: `Core`, `Scene_*`, `Feature_*`, `Shared_*`, `Localization-*`
- Key format: `Category/Path` (e.g. `Animation/Peipei`, `Timeline/BlockPuzzle/PanelOpen`)
- Scene keys use bare PascalCase names (e.g. `ClinicOut`)
- Details in [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md)
### Yarn Dialogue Files
- Current path: `Assets/Resources/Yarn/` (migration to `Assets/Narrative/Yarn/` planned)
- Pattern: `{Character}_{Stage}.yarn`; UTF-8 encoding required for Chinese
#### Yarn Line Tag Maintenance
- **Line tags** (`#line:xxxxxxx`) must be unique across the entire file. Duplicate tags break Yarn Spinner localization.
- Duplicates typically occur when copying an existing node to create a variant (e.g. `地铁ver1``地铁ver3`).
- **Use the dedup tool** (`Tools/dedup_yarn_tags.py`) for bulk cleanup:
```bash
# Preview duplicates from a specific node onward
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/Fiction_Day1_begin/Fiction_Day1_begin.yarn --from-node 地铁ver4
# Fix duplicates from a specific line onward
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/Fiction_Day1_begin/Fiction_Day1_begin.yarn --from-line 535 --fix
# Scan an entire directory (preview only)
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/ --scan-all
```
#### Yarn Script Generation
- **Do NOT add `#line:` tags when generating or editing Yarn dialogue.** Leave all line-tag generation to the user's localization workflow (Unity Localization + Yarn Spinner import tools).
- When creating new `.yarn` files, write plain dialogue lines without any `#line:` suffixes.
### Asset Naming
- **Scripts**: PascalCase (e.g. `DialogController.cs`)
- **Addressable Keys**: `Category/Path` — no spaces, no extensions, ≤3 levels
- **Addressable Groups**: `{Type}_{PascalCaseName}`
### Git & Commits
- LFS for large binaries; ignored: Library/, Temp/, Build/, *.csproj, *.sln
- **Commit format**: `<type>(<scope>): <subject>` — subject in Chinese, type/scope in English
- Extended types: `feat`, `fix`, `art`, `audio`, `scene`, `yarn`, `perf`, `refactor`, `style`, `docs`, `build`, `chore`, `test`
- See [COMMIT_CONVENTION.md](COMMIT_CONVENTION.md) for full details
## Key Build Commands
```bash
# Windows build
"C:\Program Files\Unity\Hub\Editor\2022.3.7f1c1\Editor\Unity.exe" -batchmode -quit -projectPath . -buildWindows64Player "Build/AIBIS_Dream.exe" -logFile Build/log.txt
```
## Common Tasks
### Adding a New Mini-Game
1. Create `Assets/Scripts/MiniGame/{GameName}/`
2. Add assets to `Assets/GameContent/Feature_{GameName}/`
3. Create Addressable Group `Feature_{GameName}`
4. Create test scene in `Assets/Scenes/`
### Adding Yarn Dialogue
1. Create `.yarn` in `Assets/Resources/Yarn/{Module}/`
2. Ensure YarnProject localization assets/CSV are updated (Unity `Assets/Language/Dialog/` is backup-only during migration)
3. Reference in scene's `DialogueRunner`; test with `DialogController`
## Debugging & Performance
- `LogKit` for structured logging; Addressables Event Viewer for resource debugging
- Monitor `SceneResourceLoader.TotalHandleCount` for leak detection
- Always use PoolKit for frequent instantiation; `LoadUncachedAsync` + `EarlyRelease` for large Timeline assets
- Use `LoadAsync<GameObject>` + `Instantiate` for prefabs, never `InstantiateAsync`
## Important Notes
- Narrative-focused game — dialogue and story take precedence
- `FixSystemNew` is the active repair architecture; `FixSystem` is legacy
- `YarnVariableStorage` holds Yarn runtime variables; save/load flows through `SaveRestoreOrchestrator` and `SaveSystem`
## Reference Documents
- [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md) — `ResourceSystem` API, Key/Group/Label naming, lifecycle
- [Resource Management Spec](Docs/ResourceManagementSpec.md) — Asset placement: `GameContent/`, `Narrative/`, `_Archive/`
- [Commit Convention](COMMIT_CONVENTION.md) — Commit message format and branch naming
- [HuoShan Docs](Docs/HuoShan/) — Narrative flow, resources, test flowcharts
---
*This document provides guidance for Codex when working with the AIBIS Dream codebase.*
@@ -15,7 +15,7 @@ MonoBehaviour:
m_DefaultGroup: 09546aca93a801441859af2a811fe53b
m_currentHash:
serializedVersion: 2
Hash: c7686f2c2462e1f187d9ecf6d11dbc45
Hash: cf6c8ca853b0b2e0e22d46c14944816d
m_OptimizeCatalogSize: 0
m_BuildRemoteCatalog: 0
m_BundleLocalCatalog: 0
@@ -59,13 +59,15 @@ MonoBehaviour:
- {fileID: 11400000, guid: 6d259d339c6615248b893837398c1438, type: 2}
- {fileID: 11400000, guid: 8718a71c99923e247a960cbf4e4795f5, type: 2}
- {fileID: 11400000, guid: 1e521cfc13b314642b1e682e3be7ea17, type: 2}
- {fileID: 11400000, guid: aafc8258a2111e94d8339a3152cf50e0, type: 2}
- {fileID: 11400000, guid: 7e4ab0de168458b43a21dc2f9e811ff5, type: 2}
- {fileID: 11400000, guid: 724eed66b2d8eef4faf77a4bee4d2845, type: 2}
- {fileID: 11400000, guid: 62a92c74b9bd5bd439690727c1831500, type: 2}
- {fileID: 11400000, guid: b593dc98ea11a484995123de77d9bf1a, type: 2}
- {fileID: 11400000, guid: d582f9080605ab34cbcde3249c980030, type: 2}
- {fileID: 11400000, guid: 54799118a585dda439cab043d56ec68c, type: 2}
- {fileID: 11400000, guid: dc8d31ed80ea35945812ff6552efbbe5, type: 2}
- {fileID: 11400000, guid: af94af36950b08d4f9a5037546bbe040, type: 2}
- {fileID: 11400000, guid: 60fd2fd8c7094b242b01c63de65de9ea, type: 2}
- {fileID: 11400000, guid: 7e4ab0de168458b43a21dc2f9e811ff5, type: 2}
m_BuildSettings:
m_CompileScriptsInVirtualMode: 0
m_CleanupStreamingAssetsAfterBuilds: 1
@@ -112,6 +114,7 @@ MonoBehaviour:
- Locale-en
- Locale-zh-Hans
- SceneSO
- Locale-ja-JP
m_SchemaTemplates: []
m_GroupTemplateObjects:
- {fileID: 11400000, guid: 0c158052680a3e94184c450d00291514, type: 2}
@@ -28,6 +28,16 @@ MonoBehaviour:
m_SerializedLabels:
- SceneSO
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9e771f24d61d40d42b7e2b4637e8090c
m_Address: "Timeline/\u5934\u75DB"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f80e799655e3ece439f31458425a752b
m_Address: Timeline/Glitch
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
@@ -0,0 +1,31 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
m_Name: Localization-Asset-Tables-Japanese (Japan) (ja-JP)
m_EditorClassIdentifier:
m_GroupName: Localization-Asset-Tables-Japanese (Japan) (ja-JP)
m_Data:
m_SerializedData: []
m_GUID: bac626c44cee44544a45a750681e92e0
m_SerializeEntries:
- m_GUID: 8b2d41616f1a75840a5800ee28d1c4ed
m_Address: sprite_ja-JP
m_ReadOnly: 1
m_SerializedLabels:
- Locale-ja-JP
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 1
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
m_Schemas:
- {fileID: 11400000, guid: 842c6a62aa3abdf4d9b230f249b9fd76, type: 2}
- {fileID: 11400000, guid: 1706e28ae8e595149b2ac1fbea7f8071, type: 2}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: af94af36950b08d4f9a5037546bbe040
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -15,17 +15,11 @@ MonoBehaviour:
m_GroupName: Localization-Assets-English (en)
m_Data:
m_SerializedData: []
m_GUID: 50aef5699023b21459e68677a9449d80
m_SerializeEntries:
- m_GUID: 032b1b5e09139ae45b6de51d35baae79
m_Address: discord-square-color-icon
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
m_GUID: 59bf392f3aef3b24da46dd8d3fa9259f
m_SerializeEntries: []
m_ReadOnly: 1
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
m_Schemas:
- {fileID: 11400000, guid: 895c7c97ef5802446a395377ef704940, type: 2}
- {fileID: 11400000, guid: 8004f70cd10ce4a439cb9e822bb078c2, type: 2}
- {fileID: 11400000, guid: fa74e99b946159549971be154369b2cb, type: 2}
- {fileID: 11400000, guid: 8e1cbbf76f0150145a39ce648fbf3985, type: 2}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: aafc8258a2111e94d8339a3152cf50e0
guid: 60fd2fd8c7094b242b01c63de65de9ea
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
@@ -22,145 +22,6 @@ MonoBehaviour:
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 37c291d6dce732d45afb2e2b62218683
m_Address: Assets/Language/Dialog/FP_Day1_begin/FP_Day1_begin Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c1de143c98cd0804ab3b7b9a36fcf06a
m_Address: Assets/Language/ActorName/ActorName Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 76902d55aa2d3474aa96c8e574b8212f
m_Address: Assets/Language/Dialog/FP_Peipei1/FP_Peipei1 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0cb567219be64c04b9552ecc61f8396a
m_Address: Assets/Language/Dialog/Fiction_Day1_night/Fiction_Day1_night Shared
Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 685539ac5030ab5409faa8feccba91e2
m_Address: Assets/Language/Dialog/FP_Day1_mid/FP_Day1_mid Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4f3df7a54674e8343b4699544a25db67
m_Address: Assets/Language/Dialog/FP_Shitou2/FP_Shitou2 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2c7f3692831aa5f4daa13d00e99ad77c
m_Address: Assets/Language/Dialog/FP_Shitou1/FP_Shitou1 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 949f8d1a26133a94c83ac9cc7e6d94c7
m_Address: Assets/Language/Dialog/FP_Peipei2/FP_Peipei2 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 59e2a0d8fc60a784993a6f6cdfeb9c7d
m_Address: Assets/Language/Dialog/FP_Day1_night/FP_Day1_night Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 831d81945a7dc8d498ccebcdd4d05044
m_Address: Assets/Language/Dialog/FP_Day2_mid/FP_Day2_mid Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 45899a60a64d58a4f81fad32d2171702
m_Address: Assets/Language/Dialog/FP_Day2_night/New Table Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ea388b59e37ddc14da4d8e3e9ead4133
m_Address: Assets/Language/Dialog/Test_Huoshan/Test_Huoshan Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8f3dfd038e75c3a41a865ab8dd730809
m_Address: Assets/Language/Dialog/FP_Prologue/FP_Prologue Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 011b21a8c75449e4a982549cebc1a6ca
m_Address: Assets/Language/Params/Params Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f6d220fec7b888b4f89463b5da038e62
m_Address: Assets/Language/Dialog/Fiction_Day1_mid/Fiction_Day1_mid Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c5f887d31e1d7bc46b8a7ab784f19b9a
m_Address: Assets/Language/Dialog/Fiction_Day1_begin/Fiction_Day1_begin Shared
Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 578cb0ae76a180d45ac70e058c676ac7
m_Address: Assets/Language/Dialog/FP_Peipei3/FP_Peipei3 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f5108536d82f2c14a922f0a1d3e83638
m_Address: Assets/Language/Dialog/FP_Day3_mid/FP_Day3_mid Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0ee8d3542a2395745b4ad6bf55e3b097
m_Address: Assets/Language/Dialog/Fiction_Day2_mid/Fiction_Day2_mid Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 771ed3ef48efbb645827ce5c29d63754
m_Address: Assets/Language/Dialog/Fiction_Day2_night/Fiction_Day2_night Shared
Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: aaa27d8b7dfc3fc4294bda026ae135f2
m_Address: Assets/Language/Dialog/Fiction_Day3_mid/Fiction_Day3_mid Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3c243fd229013b84e9c88091eaa74103
m_Address: Assets/Language/Dialog/Fiction_Day3_night/Fiction_Day3_night Shared
Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: bb8e2763fb266ff4d8b9c0b0f37882e2
m_Address: Assets/Language/Dialog/Fiction_Day4_mid/Fiction_Day4_mid Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: bb6fa95706f665c469e270567be5cfe9
m_Address: Assets/Language/Dialog/Fiction_Huoshan1/New Table Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 042ab1d25056cbd4984621037ac848f0
m_Address: Assets/Language/Dialog/Fiction_Yingli1/Fiction_Yingli1 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 70f8ff033cddcd543a618974781d0bb0
m_Address: Assets/Language/Dialog/FP_Day3_night/FP_Day3_night Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 08cb937dd06038b4486d6a3662539542
m_Address: Assets/Language/Dialog/Test_GeneralEnd/New Table Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9abdd56d318b1ec459c7e866748d6878
m_Address: Assets/Language/Sprite/sprite Shared Data.asset
m_ReadOnly: 1
@@ -172,47 +33,22 @@ MonoBehaviour:
m_SerializedLabels:
- Locale-en
- Locale-zh-Hans
- Locale-ja-JP
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: dd4a0fe1d258fbc4384ab1db825d5ae3
m_Address: Gts1/Fiction_Gts1 Shared Data.asset/Fiction_Gts1 Shared Data.asset
- m_GUID: 4a2d391471bd9cd439150c52659e9846
m_Address: Assets/Language/Params/Params Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 57cfa4d941133b840b4345cf815dda66
m_Address: Assets/Language/Dialog/Fiction_Day2_begin/Fiction_Day2_begin Shared
Data.asset
- m_GUID: 032b1b5e09139ae45b6de51d35baae79
m_Address: discord-square-color-icon
m_ReadOnly: 1
m_SerializedLabels: []
m_SerializedLabels:
- Locale-en
- Locale-ja-JP
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c3d23bd971e02d34cb356b2029523427
m_Address: Assets/Language/Dialog/Fiction_Day1_sleep/Fiction_Day1_sleep Shared
Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e3c75f951d9792449906a3a8010d2e74
m_Address: Assets/Language/Dialog/Fiction_Day2_sleep/Fiction_Day2_sleep Shared
Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 28396c5e50d205645b94b89018d01a2a
m_Address: Assets/Language/Dialog/Ficion_Shitou1/Fiction_Shitou1 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 73c880a15817b7f408ad0596f07a3626
m_Address: Assets/Language/Dialog/FP_Day1_sleep/FP_Day1_sleep Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: afa9b26bd11b9d847bbb9714b8049a4b
m_Address: Assets/Language/Dialog/FP_Day2_begin/FP_Day2_begin Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 90d6cbb308df1484f9146a0ad156202e
m_Address: Assets/Language/Dialog/FP_Day2_sleep/FP_Day2_sleep Shared Data.asset
- m_GUID: 7ea8c68fdc624de4c86434963c06ec55
m_Address: Assets/Language/ChapterInfo/ChapterInfo Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
@@ -29,6 +29,12 @@ MonoBehaviour:
m_SerializedLabels:
- Locale
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9d3da569ceb2f3740bd6f4fd01e51c62
m_Address: Japanese (Japan) (ja-JP)
m_ReadOnly: 1
m_SerializedLabels:
- Locale
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 1
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
@@ -23,206 +23,14 @@ MonoBehaviour:
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c21a8fbe9bf147649b5b01320ada3f11
m_Address: FP_Day1_begin_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0b504f7f6402d8747896b0cf19674e69
m_Address: ActorName_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 247e0ac5e48cbe24ea7e228e1a039590
m_Address: FP_Peipei1_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d6149a0fedb6cc24ba2a318aedab1de0
m_Address: FP_Day1_mid_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8a56c6bff47205b439b07425f6f5ad94
m_Address: FP_Shitou2_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 32954c9c2dce35e42a31befcec1e861f
m_Address: FP_Shitou1_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 860bd75190298994b9d5dee30aebd910
m_Address: FP_Peipei2_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: bfc0211d0386bc9468ecace61129ecd5
m_Address: FP_Day1_night_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 204ffaeec9d0f7e4cb708476b4d459a2
m_Address: FP_Day2_mid_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2516ada36f949234caf993f1deaf79e6
m_Address: FP_Day2_night_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 523b3962bff363849a36683b375adc06
m_Address: Test_Huoshan_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 83f5756a759af6f459aa1871eb248f5b
m_Address: FP_Prologue_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5d4915d0761772c41902e704e2ecfd0b
- m_GUID: aeb318c895d57014b89d5540c6baff95
m_Address: Params_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: fb51befc2226dfa44bbde7203364c019
m_Address: Fiction_Day2_mid_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 6603078d8eb41524e9ad2edd5d182ccc
m_Address: Fiction_Day1_mid_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 72755a9b5a5ca5c4e92028ca44c66a6d
m_Address: FP_Peipei3_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c260e1e2604154648971c06353edad0e
m_Address: FP_Day3_mid_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a75bb4d4a30c22f40ad5c7d70c8276d4
m_Address: Fiction_Day1_night_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b2f0a2aa5e0b95346bf499980c2994d4
m_Address: Fiction_Day2_night_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 6c498406bf979ca45b345e5bdbd3cc4b
m_Address: Fiction_Day3_mid_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8e5812f1d9f10a4488702f35405d197d
m_Address: Fiction_Day3_night_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 35f017da97194574da316391464204b2
m_Address: Fiction_Day4_mid_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 75ed4030af85d794f832919dbe427f51
m_Address: Fiction_Huoshan1_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4b181f70ec2e65f4cab1545662415306
m_Address: Fiction_Yingli1_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a525b6d2bcfeb2446b2f4d2b36b043c7
m_Address: FP_Day3_night_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1f031d78df49e5645a4ceaafa7283f91
m_Address: Test_GeneralEnd_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9cecc6d4b999d5c489b69406176bab99
m_Address: Fiction_Gts1_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: fbfb0ced2ba1bf44e86964158ae53d72
m_Address: Fiction_Day1_sleep_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 999ab1a8772cb1a4cba0325144ff5488
m_Address: Fiction_Day1_begin_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4d88b47f4a7e3f249b44203f411e4adf
m_Address: Fiction_Day2_sleep_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 03f5785bc02343d448436956c6991b38
m_Address: Fiction_Shitou1_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 14a941304f21e2042afd56019acfca6c
m_Address: FP_Day1_sleep_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d9f05be3049f65b4c82800b1bde3b7c2
m_Address: FP_Day2_begin_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 09898f26fbea1454cb10f2c27e2ca907
m_Address: FP_Day2_sleep_zh-Hans
- m_GUID: bdf8ad79a9f707b45805e23c25ccdadb
m_Address: ChapterInfo_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
@@ -23,206 +23,14 @@ MonoBehaviour:
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1e7115110ec21ec4da592999daa0c146
m_Address: FP_Day1_begin_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e80667e8ff7367e4ab9bc4f9421271e8
m_Address: ActorName_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 08ef2a7dddda7ee49b869bcd73858404
m_Address: FP_Peipei1_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 30ab319b5e0bafe46aa622bb93381c7e
m_Address: FP_Day1_mid_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 927a25bda151fc7469749a3724bef6b0
m_Address: FP_Shitou2_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: fda6eb219437ffc40ab860aa89d9446d
m_Address: FP_Shitou1_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 13f447125bbae5e45a4a446c2c9ac397
m_Address: FP_Peipei2_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c65e073d94ffbbb4b9b6107dee05e670
m_Address: FP_Day1_night_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 399374eeba6905048a7342d0bb494f9c
m_Address: FP_Day2_mid_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: cbc3925d28fc66c4cb3293b7bd66db0b
m_Address: FP_Day2_night_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: fb0b8f197b5a607468eeb9326737d547
m_Address: Test_Huoshan_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 49a6e4d039261bf42a18782ca317fca5
m_Address: FP_Prologue_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0a9b8bbf38d109d409792ae7cc48cec7
- m_GUID: 9248cb179e7aafa4c903f1fc54b41ef7
m_Address: Params_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: eb9ebf027dd4b564cb035d7167ff8099
m_Address: Fiction_Day2_mid_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 705a631ebd09db04fa1bda39e20e505e
m_Address: Fiction_Day1_mid_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: fde90ade684482142af49533e4f4ed62
m_Address: FP_Peipei3_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 036c8c6ed1ecc314a8e0ae02fe15ab5e
m_Address: FP_Day3_mid_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0d2928a7d58c26a4ca78abf2f5da1373
m_Address: Fiction_Day1_night_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 59fc388134f5d884c8d7ca28f2d7017e
m_Address: Fiction_Day2_night_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 379ead226bea4f844b1c136c4f748633
m_Address: Fiction_Day3_mid_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: aaf54fb66c2fa9d4ab68626ced632a7a
m_Address: Fiction_Day3_night_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c0c8b8aea11da444ea41eece5a698e5f
m_Address: Fiction_Day4_mid_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2a14b1d82f4aef94aac2b230392605fc
m_Address: Fiction_Huoshan1_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 79ccbc35144b91a45bb9fdf9732d238a
m_Address: Fiction_Yingli1_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0e9bb0974017aa346a379b78126f6f7b
m_Address: FP_Day3_night_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 27ad0341432ae0b46a8fe117eb5c73e2
m_Address: Test_GeneralEnd_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1adbe96b15171134584c9a2d7d69c045
m_Address: Fiction_Gts1_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9d72f86c018628e418747a7d0a66cb21
m_Address: Fiction_Day1_sleep_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 30a0c1ae8c45da34f8e3f90692e96e11
m_Address: Fiction_Day1_begin_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 36d94b46b4a12b54b8e084d8d275e675
m_Address: Fiction_Day2_sleep_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 265c923a085f98042b44f8b18dd8389b
m_Address: Fiction_Shitou1_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ef76bcd9daf9b6a4580eaa28d2073ebf
m_Address: FP_Day1_sleep_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a6b685d3b3e17c64da7ca72add7b3540
m_Address: FP_Day2_begin_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a0e634b20b52dd34abb7999e889ec772
m_Address: FP_Day2_sleep_en
- m_GUID: 8906df9235e52fb41b11fe8776a77032
m_Address: ChapterInfo_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
@@ -0,0 +1,43 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
m_Name: Localization-String-Tables-Japanese (Japan) (ja-JP)
m_EditorClassIdentifier:
m_GroupName: Localization-String-Tables-Japanese (Japan) (ja-JP)
m_Data:
m_SerializedData: []
m_GUID: 62cf9265bfa710b4b9152c336336a38e
m_SerializeEntries:
- m_GUID: 7c757424270c91e43bf45198f69c983a
m_Address: UIText_ja-JP
m_ReadOnly: 1
m_SerializedLabels:
- Locale-ja-JP
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9ed23d1d910a1a147ba5c316a87cbad4
m_Address: Params_ja-JP
m_ReadOnly: 1
m_SerializedLabels:
- Locale-ja-JP
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9d8accfdeabc8034c986233920a17184
m_Address: ChapterInfo_ja-JP
m_ReadOnly: 1
m_SerializedLabels:
- Locale-ja-JP
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 1
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
m_Schemas:
- {fileID: 11400000, guid: 7df396a6b5136fe4e9d2d659731161d2, type: 2}
- {fileID: 11400000, guid: 545ed8780a6508047bd42b964feb82fd, type: 2}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: dc8d31ed80ea35945812ff6552efbbe5
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -37,13 +37,23 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 7ba8eca4e18291341a10ef7c8c858bd9
m_Address: "Animation/\u9152\u5427\u533B\u751F"
- m_GUID: 5f26e5dcbc809104f92f908082de97d5
m_Address: "Animation/\u8C03\u9152\u59B9\u6D4B\u8BD5"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5f26e5dcbc809104f92f908082de97d5
m_Address: "Animation/\u8C03\u9152\u59B9\u6D4B\u8BD5"
- m_GUID: a74e4379fa468c24497808e8a6bae2c4
m_Address: "Animation/\u9152\u4FDD"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1c3c562754993f64c850ae97b4c7852e
m_Address: "Timeline/\u9189\u9152"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2d9baefeca916a942ab57a6714f7dc9e
m_Address: "Timeline/\u8C03\u9152\u8499\u592A\u5947"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
@@ -22,16 +22,41 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0252f0740dd08da4ab8f5aeb7ddcc4ea
- m_GUID: 5253d74898c86f547861ebf57b280b4f
m_Address: "Timeline/\u5929\u6865\u5934\u75DB"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: acd7f1995fefdf24c9e979e45a176b76
m_Address: "Timeline/\u5929\u6865\u98CE\u5439\u6811\u53F6"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4180297bbe280ad478f49933e69dcbce
m_Address: "Timeline/\u5929\u6865\u63A5\u6811\u53F6In"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 71aa8e39cf2f39945af8ba27eb2acd08
m_Address: "Timeline/\u5929\u6865\u63A5\u6811\u53F6Loop"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e67da195744f8de448e46c34eb9badb2
m_Address: "Animation/\u5929\u6865\u533B\u751F"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2359eeb42b1a4f044b01b6ea27390f1d
- m_GUID: ea008b0c912c0d748997bf458879cb74
m_Address: "Animation/\u5929\u6865\u4F69\u4F69"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f19cff809e7020b41ba8ad7da199db86
m_Address: "Timeline/day2\u5915\u9633\u533B\u751F\u770B\u4F69\u4F69\u4FA7\u8138"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
@@ -67,48 +67,23 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 69e2f5249baf3a94f8f84e0d141868cb
m_Address: "Timeline/\u6458\u8D70\u76D6\u5B50"
- m_GUID: 6c4aea2a4a8462344b6ce353f53c7b2a
m_Address: "Timeline/\u533B\u751F\u51FA\u5730\u94C1"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4222e9e117a86064192b7219f7ce0543
m_Address: "Timeline/\u653E\u4E0B\u76D6\u5B50"
- m_GUID: 7760fc67933fb684d81ea5275fac0a50
m_Address: "Animation/\u8BCA\u5BA4\u5916\u706B\u5C71"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 72b922f30ca3059419c6bbc565ed9b4a
m_Address: "Timeline/\u900F\u955Cin"
- m_GUID: 253f47db959b2814998695fb5d1f4d97
m_Address: "Timeline/\u65CB\u8F6C\u53F6\u67C4"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e4815dc72e9f2a54d93eaabda8a0750f
m_Address: "Timeline/\u900F\u955Cout"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0a677c8d19d799d4a80842a5ee1bf5a4
m_Address: "Timeline/\u8FDB\u5165\u6B65\u8FDB\u6A21\u5F0F"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 33a3ab569b0312145ad062ae6936177e
m_Address: "Timeline/\u9000\u51FA\u6B65\u8FDB\u6A21\u5F0F"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 838bd5cc2ceffc1459d167675d96f66a
m_Address: "Timeline/\u706B\u5C71\u8868\u8FBEpanel"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 45c7520cc9a3e5f4faa8db45df605dc1
m_Address: "Timeline/\u706B\u5C71\u8868\u8FBE\u786E\u8BA4"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3b2268ceeee9f934691265d0214b2fc7
m_Address: "Timeline/\u706B\u5C71\u8868\u8FBE\u6DF1\u5165"
- m_GUID: 1799dbcdb4a76624bb89622707581f92
m_Address: "Timeline/\u82F1\u91CC\u521D\u767B\u573A"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
@@ -27,6 +27,11 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 35dfcf569b00d404bb89ac1d8ae4c37a
m_Address: "Timeline/\u59D0\u59D0\u653E\u624B"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
@@ -32,6 +32,71 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 417a7984630786f46a5ade5d2b457852
m_Address: "Animation/\u706B\u5C71"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 838bd5cc2ceffc1459d167675d96f66a
m_Address: "Timeline/\u706B\u5C71\u8868\u8FBEpanel"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 45c7520cc9a3e5f4faa8db45df605dc1
m_Address: "Timeline/\u706B\u5C71\u8868\u8FBE\u786E\u8BA4"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3b2268ceeee9f934691265d0214b2fc7
m_Address: "Timeline/\u706B\u5C71\u8868\u8FBE\u6DF1\u5165"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4222e9e117a86064192b7219f7ce0543
m_Address: "Timeline/\u653E\u4E0B\u76D6\u5B50"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 69e2f5249baf3a94f8f84e0d141868cb
m_Address: "Timeline/\u6458\u8D70\u76D6\u5B50"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e4815dc72e9f2a54d93eaabda8a0750f
m_Address: "Timeline/\u900F\u955Cout"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 72b922f30ca3059419c6bbc565ed9b4a
m_Address: "Timeline/\u900F\u955Cin"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 33a3ab569b0312145ad062ae6936177e
m_Address: "Timeline/\u9000\u51FA\u6B65\u8FDB\u6A21\u5F0F"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0a677c8d19d799d4a80842a5ee1bf5a4
m_Address: "Timeline/\u8FDB\u5165\u6B65\u8FDB\u6A21\u5F0F"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: af4651ca096e81d4d8bcf8b93a91b872
m_Address: "Timeline/\u706B\u5C71\u5934\u75DB2"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e69af1ed498c57f4799f936aa28e4ed4
m_Address: "Timeline/\u706B\u5C71\u5934\u75DB1"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: efa335dcb6e706347bd4acf3222331b9
m_Address: "FrameAnimation/\u706B\u5C71"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
@@ -27,13 +27,18 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d269076a4860dee4f94d1bd63c674f30
m_Address: "Sprite/\u89C6\u89C9\u6A21\u5757\u76D6\u5B50"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 809f95af8dd580448ab804b94fbd9cb2
m_Address: "Animation/\u4F69\u4F69"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d269076a4860dee4f94d1bd63c674f30
m_Address: "Sprite/\u89C6\u89C9\u6A21\u5757\u76D6\u5B50"
- m_GUID: 26105571c9644aa4194f2b981ee33a58
m_Address: "FrameAnimation/\u4F69\u4F69"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
@@ -17,16 +17,6 @@ MonoBehaviour:
m_SerializedData: []
m_GUID: 74922ad77455a1847aa3f1d714a4edda
m_SerializeEntries:
- m_GUID: 3c0d119c1a62bb840846a0e7c58197f7
m_Address: "Subway/\u767D\u5929\u7A97\u5916"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 660c87b6732769b49a9b7119b54ede83
m_Address: "Subway/\u591C\u665A\u7A97\u5916"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1df1eddcbfb714743a3d146cbea2b6a2
m_Address: Scene/SubWay
m_ReadOnly: 0
@@ -52,6 +42,21 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ca80fd96dc71f714a988b27d9ca6c61c
m_Address: Sprite/News
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3c0d119c1a62bb840846a0e7c58197f7
m_Address: "Subway/\u767D\u5929\u7A97\u5916"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 660c87b6732769b49a9b7119b54ede83
m_Address: "Subway/\u591C\u665A\u7A97\u5916"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
@@ -0,0 +1,45 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
m_Name: Localization-Asset-Tables-Japanese (Japan) (ja-JP)_BundledAssetGroupSchema
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: af94af36950b08d4f9a5037546bbe040, type: 2}
m_InternalBundleIdMode: 1
m_Compression: 1
m_IncludeAddressInCatalog: 1
m_IncludeGUIDInCatalog: 1
m_IncludeLabelsInCatalog: 1
m_InternalIdNamingMode: 0
m_CacheClearBehavior: 0
m_IncludeInBuild: 1
m_BundledAssetProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
m_ForceUniqueProvider: 0
m_UseAssetBundleCache: 1
m_UseAssetBundleCrc: 1
m_UseAssetBundleCrcForCachedBundles: 1
m_UseUWRForLocalBundles: 0
m_Timeout: 0
m_ChunkedTransfer: 0
m_RedirectLimit: -1
m_RetryCount: 0
m_BuildPath:
m_Id: 2ae6a7549097a9b4caefe44b61b1fcc5
m_LoadPath:
m_Id: 631ec5c677ab8084fb9be9b9ee39c6ef
m_BundleMode: 0
m_AssetBundleProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
m_BundleNaming: 1
m_AssetLoadMode: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 842c6a62aa3abdf4d9b230f249b9fd76
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
m_Name: Localization-Asset-Tables-Japanese (Japan) (ja-JP)_ContentUpdateGroupSchema
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: af94af36950b08d4f9a5037546bbe040, type: 2}
m_StaticContent: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1706e28ae8e595149b2ac1fbea7f8071
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -12,7 +12,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
m_Name: Localization-Assets-English (en)_BundledAssetGroupSchema
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: aafc8258a2111e94d8339a3152cf50e0, type: 2}
m_Group: {fileID: 11400000, guid: 60fd2fd8c7094b242b01c63de65de9ea, type: 2}
m_InternalBundleIdMode: 1
m_Compression: 1
m_IncludeAddressInCatalog: 1
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 895c7c97ef5802446a395377ef704940
guid: fa74e99b946159549971be154369b2cb
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
@@ -12,5 +12,5 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
m_Name: Localization-Assets-English (en)_ContentUpdateGroupSchema
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: aafc8258a2111e94d8339a3152cf50e0, type: 2}
m_Group: {fileID: 11400000, guid: 60fd2fd8c7094b242b01c63de65de9ea, type: 2}
m_StaticContent: 0
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 8004f70cd10ce4a439cb9e822bb078c2
guid: 8e1cbbf76f0150145a39ce648fbf3985
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
@@ -0,0 +1,45 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
m_Name: Localization-String-Tables-Japanese (Japan) (ja-JP)_BundledAssetGroupSchema
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: dc8d31ed80ea35945812ff6552efbbe5, type: 2}
m_InternalBundleIdMode: 1
m_Compression: 1
m_IncludeAddressInCatalog: 1
m_IncludeGUIDInCatalog: 1
m_IncludeLabelsInCatalog: 1
m_InternalIdNamingMode: 0
m_CacheClearBehavior: 0
m_IncludeInBuild: 1
m_BundledAssetProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
m_ForceUniqueProvider: 0
m_UseAssetBundleCache: 1
m_UseAssetBundleCrc: 1
m_UseAssetBundleCrcForCachedBundles: 1
m_UseUWRForLocalBundles: 0
m_Timeout: 0
m_ChunkedTransfer: 0
m_RedirectLimit: -1
m_RetryCount: 0
m_BuildPath:
m_Id: 2ae6a7549097a9b4caefe44b61b1fcc5
m_LoadPath:
m_Id: 631ec5c677ab8084fb9be9b9ee39c6ef
m_BundleMode: 0
m_AssetBundleProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
m_BundleNaming: 1
m_AssetLoadMode: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7df396a6b5136fe4e9d2d659731161d2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
m_Name: Localization-String-Tables-Japanese (Japan) (ja-JP)_ContentUpdateGroupSchema
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: dc8d31ed80ea35945812ff6552efbbe5, type: 2}
m_StaticContent: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 545ed8780a6508047bd42b964feb82fd
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -32,6 +32,11 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b9e730c7e5824c5ca2858a253fe93c98
m_Address: Prefab/FrameAnimationActor
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: cd996317bdc6ae8478acb2de956f25d9
m_Address: Prefab/AnimaMobileObject
m_ReadOnly: 0
@@ -0,0 +1,550 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-7273616238501396146
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
m_Name: VolumeAsset
m_EditorClassIdentifier:
m_Template:
Weight: 1
m_Volume: {fileID: 11400000, guid: 0af8555a6447b50409db2139400d6ab7, type: 2}
--- !u!114 &-3378792728507138695
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
m_Name: Volume Track (1)
m_EditorClassIdentifier:
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 1
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 0
m_ClipIn: 0
m_Asset: {fileID: 7967868333749088964}
m_Duration: 1.3666666666666667
m_TimeScale: 1
m_ParentTrack: {fileID: -3378792728507138695}
m_EaseInDuration: 0.1
m_EaseOutDuration: 0.6
m_BlendInDuration: -1
m_BlendOutDuration: -1
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 0
m_PostExtrapolationMode: 0
m_PreExtrapolationMode: 0
m_PostExtrapolationTime: 0
m_PreExtrapolationTime: 0
m_DisplayName: VolumeAsset
m_Markers:
m_Objects: []
m_Layer: 0
m_Priority: 0
--- !u!114 &-3117513502812618692
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
m_Name: Volume Track
m_EditorClassIdentifier:
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 1
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 0
m_ClipIn: 0
m_Asset: {fileID: -7273616238501396146}
m_Duration: 1.3666666666666667
m_TimeScale: 1
m_ParentTrack: {fileID: -3117513502812618692}
m_EaseInDuration: 0.1
m_EaseOutDuration: 0.6
m_BlendInDuration: -1
m_BlendOutDuration: -1
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 0
m_PostExtrapolationMode: 0
m_PreExtrapolationMode: 0
m_PostExtrapolationTime: 0
m_PreExtrapolationTime: 0
m_DisplayName: VolumeAsset
m_Markers:
m_Objects: []
m_Layer: 0
m_Priority: 0
--- !u!114 &-2955631063214815081
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
m_Name: Volume Track (2)
m_EditorClassIdentifier:
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 1
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 0
m_ClipIn: 0
m_Asset: {fileID: -1361877534822022742}
m_Duration: 1.3666666666666667
m_TimeScale: 1
m_ParentTrack: {fileID: -2955631063214815081}
m_EaseInDuration: 0.1
m_EaseOutDuration: 0.6
m_BlendInDuration: -1
m_BlendOutDuration: -1
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 0
m_PostExtrapolationMode: 0
m_PreExtrapolationMode: 0
m_PostExtrapolationTime: 0
m_PreExtrapolationTime: 0
m_DisplayName: VolumeAsset
m_Markers:
m_Objects: []
m_Layer: 0
m_Priority: 0
--- !u!114 &-1361877534822022742
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
m_Name: VolumeAsset
m_EditorClassIdentifier:
m_Template:
Weight: 1
m_Volume: {fileID: 11400000, guid: 631b3200517e20440b88f28015fc3a5c, type: 2}
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bfda56da833e2384a9677cd3c976a436, type: 3}
m_Name: "\u706B\u5C71\u5934\u75DB1"
m_EditorClassIdentifier:
m_Version: 0
m_Tracks:
- {fileID: -3117513502812618692}
- {fileID: -3378792728507138695}
- {fileID: -2955631063214815081}
- {fileID: 2112685405309563144}
m_FixedDuration: 0
m_EditorSettings:
m_Framerate: 60
m_ScenePreview: 1
m_DurationMode: 0
m_MarkerTrack: {fileID: 0}
--- !u!74 &1077202520131138910
AnimationClip:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Recorded
serializedVersion: 7
m_Legacy: 0
m_Compressed: 0
m_UseHighQualityCurve: 1
m_RotationCurves: []
m_CompressedRotationCurves: []
m_EulerCurves: []
m_PositionCurves: []
m_ScaleCurves: []
m_FloatCurves:
- serializedVersion: 2
curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.13333334
value: 0.8
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.8333333
value: 0.8
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 1.1333333
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
attribute: m_Color.a
path:
classID: 212
script: {fileID: 0}
flags: 0
m_PPtrCurves: []
m_SampleRate: 60
m_WrapMode: 0
m_Bounds:
m_Center: {x: 0, y: 0, z: 0}
m_Extent: {x: 0, y: 0, z: 0}
m_ClipBindingConstant:
genericBindings:
- serializedVersion: 2
path: 0
attribute: 304273561
script: {fileID: 0}
typeID: 212
customType: 0
isPPtrCurve: 0
isIntCurve: 0
isSerializeReferenceCurve: 0
pptrCurveMapping: []
m_AnimationClipSettings:
serializedVersion: 2
m_AdditiveReferencePoseClip: {fileID: 0}
m_AdditiveReferencePoseTime: 0
m_StartTime: 0
m_StopTime: 1.1333333
m_OrientationOffsetY: 0
m_Level: 0
m_CycleOffset: 0
m_HasAdditiveReferencePose: 0
m_LoopTime: 0
m_LoopBlend: 0
m_LoopBlendOrientation: 0
m_LoopBlendPositionY: 0
m_LoopBlendPositionXZ: 0
m_KeepOriginalOrientation: 0
m_KeepOriginalPositionY: 1
m_KeepOriginalPositionXZ: 0
m_HeightFromFeet: 0
m_Mirror: 0
m_EditorCurves:
- serializedVersion: 2
curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.13333334
value: 0.8
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.8333333
value: 0.8
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 1.1333333
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
attribute: m_Color.a
path:
classID: 212
script: {fileID: 0}
flags: 0
m_EulerEditorCurves: []
m_HasGenericRootTransform: 0
m_HasMotionFloatCurves: 0
m_Events: []
--- !u!114 &2112685405309563144
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
m_Name: Animation Track
m_EditorClassIdentifier:
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips: []
m_Markers:
m_Objects: []
m_InfiniteClipPreExtrapolation: 1
m_InfiniteClipPostExtrapolation: 1
m_InfiniteClipOffsetPosition: {x: 0, y: 0, z: 0}
m_InfiniteClipOffsetEulerAngles: {x: 0, y: 0, z: 0}
m_InfiniteClipTimeOffset: 0
m_InfiniteClipRemoveOffset: 0
m_InfiniteClipApplyFootIK: 1
mInfiniteClipLoop: 0
m_MatchTargetFields: 63
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_AvatarMask: {fileID: 0}
m_ApplyAvatarMask: 1
m_TrackOffset: 0
m_InfiniteClip: {fileID: 1077202520131138910}
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
m_ApplyOffsets: 0
--- !u!114 &7967868333749088964
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
m_Name: VolumeAsset
m_EditorClassIdentifier:
m_Template:
Weight: 1
m_Volume: {fileID: 11400000, guid: 841479fd6ffe3ee40a88ee41bd395f09, type: 2}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e69af1ed498c57f4799f936aa28e4ed4
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,550 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-7273616238501396146
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
m_Name: VolumeAsset
m_EditorClassIdentifier:
m_Template:
Weight: 1
m_Volume: {fileID: 11400000, guid: 0af8555a6447b50409db2139400d6ab7, type: 2}
--- !u!114 &-3378792728507138695
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
m_Name: Volume Track (1)
m_EditorClassIdentifier:
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 0
m_ClipIn: 0
m_Asset: {fileID: 7967868333749088964}
m_Duration: 1.3666666666666667
m_TimeScale: 1
m_ParentTrack: {fileID: -3378792728507138695}
m_EaseInDuration: 0.1
m_EaseOutDuration: 0.6
m_BlendInDuration: -1
m_BlendOutDuration: -1
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 0
m_PostExtrapolationMode: 0
m_PreExtrapolationMode: 0
m_PostExtrapolationTime: 0
m_PreExtrapolationTime: 0
m_DisplayName: VolumeAsset
m_Markers:
m_Objects: []
m_Layer: 0
m_Priority: 0
--- !u!114 &-3117513502812618692
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
m_Name: Volume Track
m_EditorClassIdentifier:
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 0
m_ClipIn: 0
m_Asset: {fileID: -7273616238501396146}
m_Duration: 1.3666666666666667
m_TimeScale: 1
m_ParentTrack: {fileID: -3117513502812618692}
m_EaseInDuration: 0.1
m_EaseOutDuration: 0.6
m_BlendInDuration: -1
m_BlendOutDuration: -1
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 0
m_PostExtrapolationMode: 0
m_PreExtrapolationMode: 0
m_PostExtrapolationTime: 0
m_PreExtrapolationTime: 0
m_DisplayName: VolumeAsset
m_Markers:
m_Objects: []
m_Layer: 0
m_Priority: 0
--- !u!114 &-2955631063214815081
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
m_Name: Volume Track (2)
m_EditorClassIdentifier:
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 0
m_ClipIn: 0
m_Asset: {fileID: -1361877534822022742}
m_Duration: 1.3666666666666667
m_TimeScale: 1
m_ParentTrack: {fileID: -2955631063214815081}
m_EaseInDuration: 0.1
m_EaseOutDuration: 0.6
m_BlendInDuration: -1
m_BlendOutDuration: -1
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 0
m_PostExtrapolationMode: 0
m_PreExtrapolationMode: 0
m_PostExtrapolationTime: 0
m_PreExtrapolationTime: 0
m_DisplayName: VolumeAsset
m_Markers:
m_Objects: []
m_Layer: 0
m_Priority: 0
--- !u!114 &-1361877534822022742
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
m_Name: VolumeAsset
m_EditorClassIdentifier:
m_Template:
Weight: 1
m_Volume: {fileID: 11400000, guid: 631b3200517e20440b88f28015fc3a5c, type: 2}
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bfda56da833e2384a9677cd3c976a436, type: 3}
m_Name: "\u706B\u5C71\u5934\u75DB2"
m_EditorClassIdentifier:
m_Version: 0
m_Tracks:
- {fileID: -3117513502812618692}
- {fileID: -3378792728507138695}
- {fileID: -2955631063214815081}
- {fileID: 2112685405309563144}
m_FixedDuration: 0
m_EditorSettings:
m_Framerate: 60
m_ScenePreview: 1
m_DurationMode: 0
m_MarkerTrack: {fileID: 0}
--- !u!74 &1077202520131138910
AnimationClip:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Recorded
serializedVersion: 7
m_Legacy: 0
m_Compressed: 0
m_UseHighQualityCurve: 1
m_RotationCurves: []
m_CompressedRotationCurves: []
m_EulerCurves: []
m_PositionCurves: []
m_ScaleCurves: []
m_FloatCurves:
- serializedVersion: 2
curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.13333334
value: 0.8
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.8333333
value: 0.8
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 1.1333333
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
attribute: m_Color.a
path:
classID: 212
script: {fileID: 0}
flags: 0
m_PPtrCurves: []
m_SampleRate: 60
m_WrapMode: 0
m_Bounds:
m_Center: {x: 0, y: 0, z: 0}
m_Extent: {x: 0, y: 0, z: 0}
m_ClipBindingConstant:
genericBindings:
- serializedVersion: 2
path: 0
attribute: 304273561
script: {fileID: 0}
typeID: 212
customType: 0
isPPtrCurve: 0
isIntCurve: 0
isSerializeReferenceCurve: 0
pptrCurveMapping: []
m_AnimationClipSettings:
serializedVersion: 2
m_AdditiveReferencePoseClip: {fileID: 0}
m_AdditiveReferencePoseTime: 0
m_StartTime: 0
m_StopTime: 1.1333333
m_OrientationOffsetY: 0
m_Level: 0
m_CycleOffset: 0
m_HasAdditiveReferencePose: 0
m_LoopTime: 0
m_LoopBlend: 0
m_LoopBlendOrientation: 0
m_LoopBlendPositionY: 0
m_LoopBlendPositionXZ: 0
m_KeepOriginalOrientation: 0
m_KeepOriginalPositionY: 1
m_KeepOriginalPositionXZ: 0
m_HeightFromFeet: 0
m_Mirror: 0
m_EditorCurves:
- serializedVersion: 2
curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.13333334
value: 0.8
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.8333333
value: 0.8
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 1.1333333
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
attribute: m_Color.a
path:
classID: 212
script: {fileID: 0}
flags: 0
m_EulerEditorCurves: []
m_HasGenericRootTransform: 0
m_HasMotionFloatCurves: 0
m_Events: []
--- !u!114 &2112685405309563144
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
m_Name: Animation Track
m_EditorClassIdentifier:
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips: []
m_Markers:
m_Objects: []
m_InfiniteClipPreExtrapolation: 1
m_InfiniteClipPostExtrapolation: 1
m_InfiniteClipOffsetPosition: {x: 0, y: 0, z: 0}
m_InfiniteClipOffsetEulerAngles: {x: 0, y: 0, z: 0}
m_InfiniteClipTimeOffset: 0
m_InfiniteClipRemoveOffset: 0
m_InfiniteClipApplyFootIK: 1
mInfiniteClipLoop: 0
m_MatchTargetFields: 63
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_AvatarMask: {fileID: 0}
m_ApplyAvatarMask: 1
m_TrackOffset: 0
m_InfiniteClip: {fileID: 1077202520131138910}
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
m_ApplyOffsets: 0
--- !u!114 &7967868333749088964
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
m_Name: VolumeAsset
m_EditorClassIdentifier:
m_Template:
Weight: 1
m_Volume: {fileID: 11400000, guid: 841479fd6ffe3ee40a88ee41bd395f09, type: 2}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: af4651ca096e81d4d8bcf8b93a91b872
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
-8
View File
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: b51c643e2acc12549a81d17c232253a5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
-8
View File
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: e274bde4016eca842be36e589f6285f6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: efd7db4410b73e3499580d0084463f16
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,231 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-7282877348262261630
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 030f85c3f73729f4f976f66ffb23b875, type: 3}
m_Name: Recorded
m_EditorClassIdentifier:
m_Clip: {fileID: -5154461728888953855}
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_UseTrackMatchFields: 1
m_MatchTargetFields: 63
m_RemoveStartOffset: 0
m_ApplyFootIK: 1
m_Loop: 0
m_Version: 1
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
--- !u!74 &-5154461728888953855
AnimationClip:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Recorded
serializedVersion: 7
m_Legacy: 0
m_Compressed: 0
m_UseHighQualityCurve: 1
m_RotationCurves: []
m_CompressedRotationCurves: []
m_EulerCurves: []
m_PositionCurves: []
m_ScaleCurves: []
m_FloatCurves: []
m_PPtrCurves:
- serializedVersion: 2
curve:
- time: 0
value: {fileID: 21300000, guid: fa6982b25ee84334a95f92811cae79ce, type: 3}
- time: 2
value: {fileID: 21300000, guid: cc9ae6de290b0b345853d7100d065749, type: 3}
- time: 4
value: {fileID: 21300000, guid: fa6982b25ee84334a95f92811cae79ce, type: 3}
attribute: m_Sprite
path:
classID: 212
script: {fileID: 0}
flags: 2
m_SampleRate: 60
m_WrapMode: 0
m_Bounds:
m_Center: {x: 0, y: 0, z: 0}
m_Extent: {x: 0, y: 0, z: 0}
m_ClipBindingConstant:
genericBindings:
- serializedVersion: 2
path: 0
attribute: 0
script: {fileID: 0}
typeID: 212
customType: 23
isPPtrCurve: 1
isIntCurve: 0
isSerializeReferenceCurve: 0
pptrCurveMapping:
- {fileID: 21300000, guid: fa6982b25ee84334a95f92811cae79ce, type: 3}
- {fileID: 21300000, guid: cc9ae6de290b0b345853d7100d065749, type: 3}
- {fileID: 21300000, guid: fa6982b25ee84334a95f92811cae79ce, type: 3}
m_AnimationClipSettings:
serializedVersion: 2
m_AdditiveReferencePoseClip: {fileID: 0}
m_AdditiveReferencePoseTime: 0
m_StartTime: 0
m_StopTime: 4.016667
m_OrientationOffsetY: 0
m_Level: 0
m_CycleOffset: 0
m_HasAdditiveReferencePose: 0
m_LoopTime: 0
m_LoopBlend: 0
m_LoopBlendOrientation: 0
m_LoopBlendPositionY: 0
m_LoopBlendPositionXZ: 0
m_KeepOriginalOrientation: 0
m_KeepOriginalPositionY: 1
m_KeepOriginalPositionXZ: 0
m_HeightFromFeet: 0
m_Mirror: 0
m_EditorCurves: []
m_EulerEditorCurves: []
m_HasGenericRootTransform: 0
m_HasMotionFloatCurves: 0
m_Events: []
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bfda56da833e2384a9677cd3c976a436, type: 3}
m_Name: "\u65CB\u8F6C\u53F6\u67C4"
m_EditorClassIdentifier:
m_Version: 0
m_Tracks:
- {fileID: 6913286052231973111}
m_FixedDuration: 0
m_EditorSettings:
m_Framerate: 60
m_ScenePreview: 1
m_DurationMode: 0
m_MarkerTrack: {fileID: 0}
--- !u!114 &6913286052231973111
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
m_Name: Animation Track
m_EditorClassIdentifier:
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 0
m_ClipIn: 0
m_Asset: {fileID: -7282877348262261630}
m_Duration: 4
m_TimeScale: 1
m_ParentTrack: {fileID: 6913286052231973111}
m_EaseInDuration: 0
m_EaseOutDuration: 0
m_BlendInDuration: -1
m_BlendOutDuration: -1
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 1
m_PostExtrapolationMode: 1
m_PreExtrapolationMode: 1
m_PostExtrapolationTime: Infinity
m_PreExtrapolationTime: 0
m_DisplayName: Recorded
m_Markers:
m_Objects: []
m_InfiniteClipPreExtrapolation: 1
m_InfiniteClipPostExtrapolation: 1
m_InfiniteClipOffsetPosition: {x: 0, y: 0, z: 0}
m_InfiniteClipOffsetEulerAngles: {x: 0, y: 0, z: 0}
m_InfiniteClipTimeOffset: 0
m_InfiniteClipRemoveOffset: 0
m_InfiniteClipApplyFootIK: 1
mInfiniteClipLoop: 0
m_MatchTargetFields: 63
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_AvatarMask: {fileID: 0}
m_ApplyAvatarMask: 1
m_TrackOffset: 0
m_InfiniteClip: {fileID: 0}
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
m_ApplyOffsets: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 253f47db959b2814998695fb5d1f4d97
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,22 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bfda56da833e2384a9677cd3c976a436, type: 3}
m_Name: "\u98CE\u5439\u6811\u53F6"
m_EditorClassIdentifier:
m_Version: 0
m_Tracks: []
m_FixedDuration: 0
m_EditorSettings:
m_Framerate: 60
m_ScenePreview: 1
m_DurationMode: 0
m_MarkerTrack: {fileID: 0}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 30d786a983bc55a4e844ce4b7d6d6fab
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -244,3 +244,14 @@ MonoBehaviour:
m_PreInfinity: 0
m_PostInfinity: 0
m_RotationOrder: 0
- m_From: '**ANY CAMERA**'
m_To: Dream Camera
m_Blend:
m_Style: 0
m_Time: 2
m_CustomCurve:
serializedVersion: 2
m_Curve: []
m_PreInfinity: 0
m_PostInfinity: 0
m_RotationOrder: 0
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 09d2581fcb827614bba6bf5c3279358e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,277 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AibisDream;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace AibisDream.SystemEditor
{
public class ChapterGraphEditorWindow : EditorWindow
{
private ChapterGraphView _graphView;
private ChapterGraphInspector _inspector;
private IMGUIContainer _inspectorContainer;
private const string RootFolderPrefKey = "AibisDream.ChapterGraph.RootFolder";
private const string CurrentFolderPrefKey = "AibisDream.ChapterGraph.CurrentFolder";
private string _rootFolder;
private string _currentFolder;
private DropdownField _folderDropdown;
private readonly Dictionary<string, string> _displayToPath = new();
[MenuItem("Window/Chapter Graph Editor")]
public static void ShowWindow()
{
var window = GetWindow<ChapterGraphEditorWindow>("Chapter Graph Editor");
window.minSize = new Vector2(800, 600);
}
private void OnEnable()
{
if (_graphView == null) return;
_graphView.LoadGraph(_currentFolder);
}
private void OnDisable()
{
if (_graphView != null)
{
_graphView.OnNodeDeleted -= OnNodeDeleted;
}
if (_inspector != null)
{
_inspector.OnColorChanged -= OnNodeColorChanged;
_inspector.Destroy();
}
}
private void CreateGUI()
{
// 根容器:纵向排列
var root = rootVisualElement;
root.style.flexDirection = FlexDirection.Column;
// 顶部工具栏
var toolbar = new Toolbar();
toolbar.Add(CreateToolbarButton("Save", OnSaveClicked));
toolbar.Add(CreateToolbarButton("Auto Layout", OnAutoLayoutClicked));
toolbar.Add(CreateToolbarButton("Validate", OnValidateClicked));
_rootFolder = EditorPrefs.GetString(RootFolderPrefKey, "Assets/ScriptableObjects/SceneSO");
_folderDropdown = new DropdownField("路径");
_folderDropdown.style.width = 160;
_folderDropdown.style.marginLeft = 8;
_folderDropdown.style.marginRight = 4;
RefreshFolderChoices();
_folderDropdown.RegisterValueChangedCallback(OnFolderChanged);
toolbar.Add(_folderDropdown);
var setRootBtn = new Button(OnSetRootFolderClicked) { text = "..." };
setRootBtn.style.width = 28;
toolbar.Add(setRootBtn);
toolbar.Add(new ToolbarSpacer() { style = { flexGrow = 1 } });
toolbar.Add(CreateToolbarButton("Create Chapter", OnCreateChapterClicked));
root.Add(toolbar);
// 主内容区:横向分栏
var mainContainer = new VisualElement();
mainContainer.style.flexGrow = 1;
mainContainer.style.flexDirection = FlexDirection.Row;
mainContainer.style.overflow = Overflow.Hidden;
// 左侧 Inspector 面板
var leftPanel = new VisualElement();
leftPanel.style.width = 320;
leftPanel.style.minWidth = 200;
leftPanel.style.maxWidth = 500;
leftPanel.style.flexShrink = 0;
leftPanel.style.flexDirection = FlexDirection.Column;
leftPanel.style.borderRightWidth = 1;
leftPanel.style.borderRightColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f));
// Inspector 标题
var header = new Label("章节属性")
{
style =
{
unityFontStyleAndWeight = FontStyle.Bold,
fontSize = 14,
paddingTop = 8,
paddingBottom = 8,
paddingLeft = 12,
paddingRight = 12,
borderBottomWidth = 1,
borderBottomColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f))
}
};
leftPanel.Add(header);
// IMGUI Inspector
_inspector = new ChapterGraphInspector();
_inspectorContainer = new IMGUIContainer(() => _inspector.OnGUI());
_inspectorContainer.style.flexGrow = 1;
leftPanel.Add(_inspectorContainer);
mainContainer.Add(leftPanel);
// 拖拽条
var resizer = new VisualElement();
resizer.style.width = 5;
resizer.style.backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f));
mainContainer.Add(resizer);
float startWidth = 0;
float startMouseX = 0;
resizer.RegisterCallback<MouseDownEvent>(evt =>
{
startWidth = leftPanel.resolvedStyle.width;
startMouseX = evt.mousePosition.x;
resizer.CaptureMouse();
evt.StopPropagation();
});
resizer.RegisterCallback<MouseMoveEvent>(evt =>
{
if (!resizer.HasMouseCapture()) return;
var delta = evt.mousePosition.x - startMouseX;
var newWidth = Mathf.Clamp(startWidth + delta, 200, 600);
leftPanel.style.width = newWidth;
});
resizer.RegisterCallback<MouseUpEvent>(evt =>
{
if (resizer.HasMouseCapture())
resizer.ReleaseMouse();
});
// 右侧 GraphView
// 套一层容器:GraphView 与左侧 Inspector 并列放在 Flex Row 中时,
// 内置 RectangleSelector 绘制的框选矩形会出现坐标偏移(已知 Unity 问题)。
// 将 GraphView 放在独立的 Flex 子容器中可使其局部原点与父容器对齐,规避该偏移。
var graphViewWrapper = new VisualElement();
graphViewWrapper.style.flexGrow = 1;
graphViewWrapper.style.flexDirection = FlexDirection.Column;
graphViewWrapper.style.overflow = Overflow.Hidden;
_graphView = new ChapterGraphView();
_graphView.style.flexGrow = 1;
graphViewWrapper.Add(_graphView);
mainContainer.Add(graphViewWrapper);
root.Add(mainContainer);
// 加载图数据
_graphView.LoadGraph(_currentFolder);
// 选中节点同步到 Inspector
_graphView.OnNodeSelected += OnNodeSelected;
_graphView.OnNodeDeleted += OnNodeDeleted;
_inspector.OnColorChanged += OnNodeColorChanged;
}
private void OnNodeDeleted(TalkSceneSO so)
{
_inspector?.SetTarget(null);
_inspectorContainer?.MarkDirtyRepaint();
}
private void OnNodeColorChanged(TalkSceneSO so)
{
_graphView?.UpdateNodeColor(so);
}
private Button CreateToolbarButton(string text, System.Action onClick)
{
var btn = new Button(onClick) { text = text };
return btn;
}
private void OnSaveClicked()
{
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("保存", "所有资源已保存。", "确定");
}
private void OnAutoLayoutClicked()
{
_graphView?.AutoLayout();
}
private void OnValidateClicked()
{
_graphView?.ValidateGraph();
}
private void OnCreateChapterClicked()
{
var center = new Vector2(position.width / 2f, position.height / 2f);
var localPos = _graphView.contentViewContainer.WorldToLocal(center);
var defaultFolder = _currentFolder ?? EditorPrefs.GetString(ChapterGraphView.DefaultFolderPrefKey, _rootFolder);
_graphView?.CreateChapterNodeAt(localPos, defaultFolder);
}
private void OnNodeSelected(TalkSceneSO so)
{
_inspector?.SetTarget(so);
_inspectorContainer?.MarkDirtyRepaint();
}
private void RefreshFolderChoices()
{
_displayToPath.Clear();
_displayToPath["All"] = null;
var subFolders = AssetDatabase.GetSubFolders(_rootFolder);
foreach (var folder in subFolders)
{
var name = Path.GetFileName(folder);
_displayToPath[name] = folder;
}
var rootHasSO = AssetDatabase.FindAssets("t:TalkSceneSO", new[] { _rootFolder })
.Select(g => AssetDatabase.GUIDToAssetPath(g))
.Any(path => Path.GetDirectoryName(path)?.Replace('\\', '/') == _rootFolder);
if (rootHasSO)
{
_displayToPath["(Root)"] = _rootFolder;
}
_folderDropdown.choices = _displayToPath.Keys.ToList();
var saved = EditorPrefs.GetString(CurrentFolderPrefKey, "All");
_folderDropdown.value = _displayToPath.ContainsKey(saved) ? saved : "All";
_currentFolder = _displayToPath.TryGetValue(_folderDropdown.value, out var path) ? path : null;
}
private void OnFolderChanged(ChangeEvent<string> evt)
{
_currentFolder = _displayToPath.TryGetValue(evt.newValue, out var path) ? path : null;
EditorPrefs.SetString(CurrentFolderPrefKey, evt.newValue);
_graphView?.LoadGraph(_currentFolder);
}
private void OnSetRootFolderClicked()
{
var path = EditorUtility.OpenFolderPanel("选择章节根目录", _rootFolder, "");
if (string.IsNullOrEmpty(path)) return;
var absoluteRoot = Application.dataPath.Replace('\\', '/');
var selected = path.Replace('\\', '/');
if (!selected.StartsWith(absoluteRoot))
{
EditorUtility.DisplayDialog("错误", "请选择项目 Assets 目录内的文件夹。", "确定");
return;
}
_rootFolder = "Assets" + selected.Substring(absoluteRoot.Length);
EditorPrefs.SetString(RootFolderPrefKey, _rootFolder);
RefreshFolderChoices();
_graphView?.LoadGraph(_currentFolder);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c30bb2152169f544cb672c29ac8cb5d1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,70 @@
using AibisDream;
using UnityEditor;
using UnityEngine;
namespace AibisDream.SystemEditor
{
public class ChapterGraphInspector
{
private Editor _editor;
private TalkSceneSO _target;
private Vector2 _scrollPosition;
public event System.Action<TalkSceneSO> OnColorChanged;
public void SetTarget(TalkSceneSO so)
{
// 不能用 ==,Unity 重载了 ==,已销毁的对象 == null 为 true
if (ReferenceEquals(_target, so)) return;
_target = so;
if (_editor != null)
{
Object.DestroyImmediate(_editor);
_editor = null;
}
if (so != null)
{
_editor = Editor.CreateEditor(so);
}
}
public void OnGUI()
{
if (_editor == null)
{
EditorGUILayout.HelpBox("请在图中选中一个章节节点以编辑属性", MessageType.Info);
return;
}
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
EditorGUI.BeginChangeCheck();
var newColor = EditorGUILayout.ColorField("节点颜色", _target.nodeColor);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(_target, "Change Node Color");
_target.nodeColor = newColor;
EditorUtility.SetDirty(_target);
OnColorChanged?.Invoke(_target);
}
EditorGUILayout.Space(6);
_editor.OnInspectorGUI();
EditorGUILayout.EndScrollView();
}
public void Destroy()
{
if (_editor != null)
{
Object.DestroyImmediate(_editor);
_editor = null;
}
_target = null;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9b122b6f926732f46b067d9b26eb191b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,503 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AibisDream;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace AibisDream.SystemEditor
{
public class ChapterGraphView : GraphView
{
public event Action<TalkSceneSO> OnNodeSelected;
public event Action<TalkSceneSO> OnNodeDeleted;
private const float HorizontalSpacing = 420f;
private const float VerticalSpacing = 220f;
public const string DefaultFolderPrefKey = "AibisDream.ChapterGraph.DefaultFolder";
public const string DefaultFolder = "Assets/ScriptableObjects/SceneSO";
public ChapterGraphView()
{
style.flexGrow = 1;
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
this.AddManipulator(new ContentDragger());
this.AddManipulator(new SelectionDragger());
this.AddManipulator(new RectangleSelector());
var grid = new GridBackground();
Insert(0, grid);
grid.StretchToParentSize();
graphViewChanged += OnGraphViewChanged;
}
public void UpdateNodeColor(TalkSceneSO so)
{
foreach (var node in graphElements.OfType<ChapterNode>())
{
if (node.SceneSo == so)
{
node.UpdateColor();
break;
}
}
}
public void LoadGraph(string folderPath = null)
{
ClearGraph();
var guids = AssetDatabase.FindAssets("t:TalkSceneSO", folderPath != null ? new[] { folderPath } : null);
var allSos = guids.Select(g => AssetDatabase.LoadAssetAtPath<TalkSceneSO>(AssetDatabase.GUIDToAssetPath(g))).Where(so => so != null).ToList();
var nodeMap = new Dictionary<TalkSceneSO, ChapterNode>();
// 创建节点
foreach (var so in allSos)
{
var node = new ChapterNode(so);
AddElement(node);
nodeMap[so] = node;
}
// 创建连线
foreach (var so in allSos)
{
if (so.exits == null) continue;
if (!nodeMap.TryGetValue(so, out var sourceNode)) continue;
for (int i = 0; i < so.exits.Count; i++)
{
var exit = so.exits[i];
if (exit == null || exit.targetScene == null) continue;
if (!nodeMap.TryGetValue(exit.targetScene, out var targetNode)) continue;
var outputPort = sourceNode.GetOutputPort(i);
var inputPort = targetNode.InputPort;
if (outputPort == null || inputPort == null) continue;
var edge = new Edge
{
output = outputPort,
input = inputPort
};
edge.output.Connect(edge);
edge.input.Connect(edge);
AddElement(edge);
}
}
}
public void ClearGraph()
{
// 清除所有元素
var elementsToRemove = graphElements.ToList();
foreach (var element in elementsToRemove)
{
RemoveElement(element);
}
}
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
base.BuildContextualMenu(evt);
if (evt.target is GraphView)
{
var mousePosition = contentViewContainer.WorldToLocal(evt.mousePosition);
evt.menu.AppendAction("添加章节节点", (_) => CreateChapterNodeAt(mousePosition));
}
}
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
var compatiblePorts = new List<Port>();
var startNode = startPort.node as ChapterNode;
if (startNode == null) return compatiblePorts;
ports.ForEach(port =>
{
var targetNode = port.node as ChapterNode;
if (targetNode == null) return;
if (targetNode == startNode) return;
if (startPort.direction == port.direction) return;
if (startPort.node == port.node) return;
compatiblePorts.Add(port);
});
return compatiblePorts;
}
private GraphViewChange OnGraphViewChanged(GraphViewChange change)
{
// 先处理节点删除确认:用户取消则从移除列表中剔除
if (change.elementsToRemove != null)
{
var nodesToRemove = change.elementsToRemove.OfType<ChapterNode>().ToList();
foreach (var node in nodesToRemove)
{
if (!ConfirmRemoveNode(node))
{
change.elementsToRemove.Remove(node);
// 同时保留与该节点相连的边,避免边被单独删掉
var edgesToKeep = change.elementsToRemove.OfType<Edge>()
.Where(e => e.output?.node == node || e.input?.node == node)
.ToList();
foreach (var edge in edgesToKeep)
{
change.elementsToRemove.Remove(edge);
}
}
}
}
// 处理 Edge 删除
if (change.elementsToRemove != null)
{
foreach (var edge in change.elementsToRemove.OfType<Edge>())
{
HandleEdgeRemoved(edge);
}
foreach (var node in change.elementsToRemove.OfType<ChapterNode>())
{
HandleNodeRemoved(node);
}
}
// 处理 Edge 创建
if (change.edgesToCreate != null)
{
foreach (var edge in change.edgesToCreate)
{
HandleEdgeCreated(edge);
}
}
// 处理节点移动
if (change.movedElements != null)
{
foreach (var element in change.movedElements)
{
if (element is ChapterNode node)
{
node.SyncPositionToSo();
}
}
}
return change;
}
private void HandleEdgeCreated(Edge edge)
{
if (!(edge.output?.node is ChapterNode sourceNode)) return;
if (!(edge.input?.node is ChapterNode targetNode)) return;
var sourceSo = sourceNode.SceneSo;
var targetSo = targetNode.SceneSo;
if (sourceSo == null || targetSo == null) return;
int exitIndex = edge.output.userData is int idx ? idx : -1;
if (exitIndex < 0 || exitIndex >= sourceSo.exits.Count) return;
Undo.RecordObject(sourceSo, "Connect Chapter");
sourceSo.exits[exitIndex].targetScene = targetSo;
EditorUtility.SetDirty(sourceSo);
}
private void HandleEdgeRemoved(Edge edge)
{
if (!(edge.output?.node is ChapterNode sourceNode)) return;
var sourceSo = sourceNode.SceneSo;
if (sourceSo == null) return;
int exitIndex = edge.output.userData is int idx ? idx : -1;
if (exitIndex < 0 || exitIndex >= sourceSo.exits.Count) return;
Undo.RecordObject(sourceSo, "Disconnect Chapter");
sourceSo.exits[exitIndex].targetScene = null;
EditorUtility.SetDirty(sourceSo);
}
private bool ConfirmRemoveNode(ChapterNode node)
{
var so = node.SceneSo;
if (so == null) return true;
var referrers = new List<string>();
foreach (var other in graphElements.OfType<ChapterNode>())
{
if (other == node) continue;
if (other.SceneSo == null || other.SceneSo.exits == null) continue;
foreach (var exit in other.SceneSo.exits)
{
if (exit.targetScene == so)
{
referrers.Add(string.IsNullOrEmpty(other.SceneSo.title) ? other.SceneSo.name : other.SceneSo.title);
break;
}
}
}
var name = string.IsNullOrEmpty(so.title) ? so.name : so.title;
var msg = $"确定要删除章节节点 \"{name}\" 吗?";
if (referrers.Count > 0)
{
msg += $"\n\n以下节点仍引用该节点:\n• {string.Join("\n ", referrers)}";
}
return EditorUtility.DisplayDialog("删除确认", msg, "删除", "取消");
}
private void HandleNodeRemoved(ChapterNode node)
{
var so = node.SceneSo;
if (so == null) return;
var path = AssetDatabase.GetAssetPath(so);
if (!string.IsNullOrEmpty(path))
{
AssetDatabase.DeleteAsset(path);
}
OnNodeDeleted?.Invoke(so);
}
public override void AddToSelection(ISelectable selectable)
{
base.AddToSelection(selectable);
NotifySelectionChanged();
}
public override void RemoveFromSelection(ISelectable selectable)
{
base.RemoveFromSelection(selectable);
NotifySelectionChanged();
}
public override void ClearSelection()
{
base.ClearSelection();
NotifySelectionChanged();
}
private void NotifySelectionChanged()
{
var selectedNode = selection.OfType<ChapterNode>().FirstOrDefault();
OnNodeSelected?.Invoke(selectedNode?.SceneSo);
}
public void CreateChapterNodeAt(Vector2 position, string defaultFolder = null)
{
var folder = defaultFolder ?? EditorPrefs.GetString(DefaultFolderPrefKey, DefaultFolder);
var path = EditorUtility.SaveFilePanelInProject("新建章节", "NewChapter", "asset", "选择新章节的保存位置", folder);
if (string.IsNullOrEmpty(path)) return;
var so = ScriptableObject.CreateInstance<TalkSceneSO>();
so.name = System.IO.Path.GetFileNameWithoutExtension(path);
so.title = "新章节";
so.nodeColor = new Color(0.85f, 0.45f, 0.45f);
so.graphPosition = position;
so.exits = new System.Collections.Generic.List<SceneExit>();
AssetDatabase.CreateAsset(so, path);
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
// 记住文件夹
var savedFolder = System.IO.Path.GetDirectoryName(path)?.Replace('\\', '/');
if (!string.IsNullOrEmpty(savedFolder))
{
EditorPrefs.SetString(DefaultFolderPrefKey, savedFolder);
}
var node = new ChapterNode(so);
AddElement(node);
}
public void AutoLayout()
{
var nodes = graphElements.OfType<ChapterNode>().ToList();
if (nodes.Count == 0) return;
// 计算入度
var inDegree = nodes.ToDictionary(n => n, _ => 0);
var edges = graphElements.OfType<Edge>().ToList();
foreach (var edge in edges)
{
if (edge.input?.node is ChapterNode target && inDegree.ContainsKey(target))
{
inDegree[target]++;
}
}
// 分层 BFS
var layer = new Dictionary<ChapterNode, int>();
var queue = new Queue<ChapterNode>();
// 入度为 0 的节点作为起点
foreach (var node in nodes)
{
if (inDegree[node] == 0)
{
layer[node] = 0;
queue.Enqueue(node);
}
}
// 如果没有入度为 0 的节点(纯循环),任选一个作为起点
if (queue.Count == 0)
{
layer[nodes[0]] = 0;
queue.Enqueue(nodes[0]);
}
while (queue.Count > 0)
{
var current = queue.Dequeue();
var currentLayer = layer[current];
foreach (var edge in edges)
{
if (edge.output?.node == current && edge.input?.node is ChapterNode next)
{
if (!layer.ContainsKey(next))
{
layer[next] = currentLayer + 1;
queue.Enqueue(next);
}
else
{
layer[next] = Math.Max(layer[next], currentLayer + 1);
}
}
}
}
// 为未分配层级的节点(孤立或不连通部分)分配层级
foreach (var node in nodes)
{
if (!layer.ContainsKey(node))
{
layer[node] = 0;
}
}
// 按层级分组并计算位置
var groups = layer.GroupBy(kvp => kvp.Value).OrderBy(g => g.Key).ToList();
Undo.RecordObjects(nodes.Select(n => n.SceneSo).ToArray(), "Auto Layout");
for (int i = 0; i < groups.Count; i++)
{
var group = groups[i];
var groupNodes = group.ToList();
float x = group.Key * HorizontalSpacing;
float startY = -(groupNodes.Count - 1) * VerticalSpacing / 2f;
for (int j = 0; j < groupNodes.Count; j++)
{
var node = groupNodes[j].Key;
var y = startY + j * VerticalSpacing;
var newPos = new Vector2(x, y);
node.SetPosition(new Rect(newPos, Vector2.zero));
node.SceneSo.graphPosition = newPos;
EditorUtility.SetDirty(node.SceneSo);
}
}
}
public void ValidateGraph()
{
var nodes = graphElements.OfType<ChapterNode>().ToList();
var issues = new List<string>();
var soSet = new HashSet<TalkSceneSO>(nodes.Select(n => n.SceneSo));
// 孤立节点
foreach (var node in nodes)
{
var so = node.SceneSo;
bool hasIncoming = graphElements.OfType<Edge>().Any(e => e.input?.node == node);
bool hasOutgoing = so.exits != null && so.exits.Any(e => e.targetScene != null);
if (!hasIncoming && !hasOutgoing)
{
issues.Add($"孤立节点: {so.name} (没有入边也没有出边)");
}
}
// 循环引用 (DFS)
var visited = new HashSet<TalkSceneSO>();
var visiting = new HashSet<TalkSceneSO>();
foreach (var so in soSet)
{
if (!visited.Contains(so))
{
CheckCycle(so, visited, visiting, soSet, issues);
}
}
// 空 exitName
foreach (var node in nodes)
{
var so = node.SceneSo;
if (so.exits == null) continue;
for (int i = 0; i < so.exits.Count; i++)
{
if (string.IsNullOrEmpty(so.exits[i].exitName))
{
issues.Add($"空 exitName: {so.name} 的第 {i + 1} 个出口");
}
}
}
if (issues.Count == 0)
{
EditorUtility.DisplayDialog("验证结果", "图表验证通过,未发现问题。", "确定");
}
else
{
var msg = string.Join("\n", issues.Take(20));
if (issues.Count > 20)
{
msg += $"\n... 还有 {issues.Count - 20} 个问题";
}
EditorUtility.DisplayDialog($"验证发现 {issues.Count} 个问题", msg, "确定");
}
}
private void CheckCycle(TalkSceneSO current, HashSet<TalkSceneSO> visited, HashSet<TalkSceneSO> visiting, HashSet<TalkSceneSO> soSet, List<string> issues)
{
visited.Add(current);
visiting.Add(current);
if (current.exits != null)
{
foreach (var exit in current.exits)
{
var next = exit.targetScene;
if (next == null || !soSet.Contains(next)) continue;
if (visiting.Contains(next))
{
issues.Add($"循环引用: {current.name} -> {next.name}");
continue;
}
if (!visited.Contains(next))
{
CheckCycle(next, visited, visiting, soSet, issues);
}
}
}
visiting.Remove(current);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 913b090485a3d844fab3e436e7bf4f57
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+282
View File
@@ -0,0 +1,282 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AibisDream;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace AibisDream.SystemEditor
{
public class ChapterNode : Node
{
public TalkSceneSO SceneSo { get; }
public Port InputPort { get; private set; }
private const float PortLabelMaxWidth = 120f;
public ChapterNode(TalkSceneSO so)
{
SceneSo = so ?? throw new ArgumentNullException(nameof(so));
// 节点整体放大
style.minWidth = 240;
style.paddingTop = 8;
style.paddingBottom = 8;
style.paddingLeft = 10;
style.paddingRight = 10;
UpdateTitle();
UpdateColor();
BuildInputPort();
BuildOutputPorts();
BuildSubtitle();
BuildAddExitButton();
SetPosition(new Rect(so.graphPosition, Vector2.zero));
// 放大标题
var titleLabel = this.Q<Label>("title-label");
if (titleLabel != null)
{
titleLabel.style.fontSize = 16;
titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
}
}
public void UpdateTitle()
{
title = string.IsNullOrEmpty(SceneSo.title) ? SceneSo.name : SceneSo.title;
}
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
base.BuildContextualMenu(evt);
if (SceneSo.exits != null && SceneSo.exits.Count > 0)
{
for (int i = 0; i < SceneSo.exits.Count; i++)
{
var exit = SceneSo.exits[i];
var name = string.IsNullOrEmpty(exit.exitName) ? $"出口{i}" : exit.exitName;
var idx = i;
evt.menu.AppendAction($"删除出口/{name}", _ => RemoveExitAt(idx));
}
}
}
public void UpdateColor()
{
style.backgroundColor = new StyleColor(SceneSo.nodeColor);
}
private void BuildInputPort()
{
InputPort = Port.Create<Edge>(
Orientation.Horizontal,
Direction.Input,
Port.Capacity.Single,
typeof(TalkSceneSO));
InputPort.portName = "▶";
InputPort.style.fontSize = 14;
InputPort.AddManipulator(new EdgeConnector<Edge>(new EmptyEdgeListener()));
inputContainer.Add(InputPort);
}
private void BuildOutputPorts()
{
outputContainer.Clear();
if (SceneSo.exits == null) return;
for (int i = 0; i < SceneSo.exits.Count; i++)
{
var exit = SceneSo.exits[i];
var port = CreateOutputPort(exit, i);
outputContainer.Add(port);
}
}
private Port CreateOutputPort(SceneExit exit, int index)
{
var port = Port.Create<Edge>(
Orientation.Horizontal,
Direction.Output,
Port.Capacity.Single,
typeof(TalkSceneSO));
port.portName = string.IsNullOrEmpty(exit.exitName) ? $"出口{index}" : exit.exitName;
port.userData = index;
// 限制端口标签宽度并放大字体
var label = port.Q<Label>("type");
if (label != null)
{
label.style.maxWidth = PortLabelMaxWidth;
label.style.fontSize = 12;
}
port.AddManipulator(new EdgeConnector<Edge>(new EmptyEdgeListener()));
port.RegisterCallback<ContextualMenuPopulateEvent>((evt) =>
{
var currentIndex = port.userData is int idx ? idx : -1;
evt.menu.AppendAction("删除出口", (_) => RemoveExitAt(currentIndex));
}, TrickleDown.TrickleDown);
return port;
}
private void BuildSubtitle()
{
var subtitleLabel = new Label($"{SceneSo.name}")
{
style =
{
fontSize = 12,
unityTextAlign = TextAnchor.MiddleCenter,
color = new Color(0.8f, 0.8f, 0.8f),
paddingTop = 4,
paddingBottom = 2
}
};
extensionContainer.Add(subtitleLabel);
RefreshExpandedState();
}
private void BuildAddExitButton()
{
var removeButton = new Button(() => RemoveLastExit())
{
text = "-",
style =
{
width = 26,
height = 22,
fontSize = 16,
unityFontStyleAndWeight = FontStyle.Bold,
marginRight = 4
}
};
titleButtonContainer.Add(removeButton);
var addButton = new Button(() => AddExit())
{
text = "+",
style = { width = 26, height = 22, fontSize = 16, unityFontStyleAndWeight = FontStyle.Bold }
};
titleButtonContainer.Add(addButton);
}
private void RemoveLastExit()
{
if (SceneSo.exits == null || SceneSo.exits.Count == 0) return;
RemoveExitAt(SceneSo.exits.Count - 1);
}
public void AddExit()
{
Undo.RecordObject(SceneSo, "Add Exit");
if (SceneSo.exits == null)
SceneSo.exits = new System.Collections.Generic.List<SceneExit>();
var newExit = new SceneExit
{
exitName = $"exit_{SceneSo.exits.Count}"
};
SceneSo.exits.Add(newExit);
EditorUtility.SetDirty(SceneSo);
var newPort = CreateOutputPort(newExit, SceneSo.exits.Count - 1);
outputContainer.Add(newPort);
RefreshPorts();
}
public void RemoveExitAt(int index)
{
if (SceneSo.exits == null || index < 0 || index >= SceneSo.exits.Count) return;
// 断开并移除该端口的所有边
var portToRemove = GetOutputPort(index);
if (portToRemove != null)
{
var edges = portToRemove.connections.ToList();
foreach (var edge in edges)
{
edge.input?.Disconnect(edge);
edge.output?.Disconnect(edge);
edge.RemoveFromHierarchy();
}
portToRemove.RemoveFromHierarchy();
}
Undo.RecordObject(SceneSo, "Remove Exit");
SceneSo.exits.RemoveAt(index);
EditorUtility.SetDirty(SceneSo);
// 更新后续端口的 userData 和 portName
for (int i = index; i < SceneSo.exits.Count; i++)
{
var port = GetOutputPort(i);
if (port != null)
{
port.userData = i;
var exit = SceneSo.exits[i];
port.portName = string.IsNullOrEmpty(exit.exitName) ? $"出口{i}" : exit.exitName;
}
}
RefreshPorts();
}
public void RefreshOutputPorts()
{
// 先断开所有输出端口的边
var edgesToRemove = outputContainer.Children()
.OfType<Port>()
.SelectMany(p => p.connections.ToList())
.ToList();
foreach (var edge in edgesToRemove)
{
edge.input?.Disconnect(edge);
edge.output?.Disconnect(edge);
edge.RemoveFromHierarchy();
}
BuildOutputPorts();
RefreshPorts();
}
public Port GetOutputPort(int index)
{
return outputContainer.Children().OfType<Port>().ElementAtOrDefault(index);
}
public override void SetPosition(Rect newPos)
{
base.SetPosition(newPos);
if (SceneSo != null)
{
SceneSo.graphPosition = newPos.position;
EditorUtility.SetDirty(SceneSo);
}
}
public void SyncPositionToSo()
{
if (SceneSo == null) return;
var pos = GetPosition();
if (SceneSo.graphPosition != pos.position)
{
SceneSo.graphPosition = pos.position;
EditorUtility.SetDirty(SceneSo);
}
}
private class EmptyEdgeListener : IEdgeConnectorListener
{
public void OnDropOutsidePort(Edge edge, Vector2 position) { }
public void OnDrop(GraphView graphView, Edge edge) { }
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f3a4f1beaaa3a5740b1904f8b9b926b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,32 @@
using UnityEditor;
using UnityEngine;
namespace AibisDream.SystemEditor
{
[CustomEditor(typeof(TalkSceneSO))]
public class TalkSceneSOEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(serializedObject.FindProperty("exits"), true);
EditorGUILayout.PropertyField(serializedObject.FindProperty("yarnProject"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("firstSceneName"));
EditorGUILayout.Space();
EditorGUILayout.LabelField("显示信息", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(serializedObject.FindProperty("title"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("titleKey"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("description"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("coverPic"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("chapter"));
EditorGUILayout.Space();
EditorGUILayout.LabelField("编辑器设置", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(serializedObject.FindProperty("nodeColor"), new GUIContent("节点颜色"));
serializedObject.ApplyModifiedProperties();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4988e2ee30c565e40989210408a234e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9e244d3d62dec3f4f881f7e681da8c03
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,19 @@
{
"name": "AibisDream.FrameAnimation.Editor",
"rootNamespace": "AibisDream.FrameAnimation.Editor",
"references": [
"AibisDream.FrameAnimation.Runtime",
"Unity.2D.Sprite.Editor"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d1ffe139ca2417941a09835467d1e442
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
internal static class AsepriteJsonParser
{
public static bool TryParse(
string json,
string sourceId,
out AsepriteSourceDocument document,
out FrameAnimationImportIssue issue)
{
document = null;
issue = null;
try
{
if (string.IsNullOrWhiteSpace(json))
{
issue = Error(FrameAnimationImportIssueCode.JsonInvalid, sourceId, "Aseprite JSON 为空。");
return false;
}
var root = JObject.Parse(json);
var framesToken = root["frames"];
if (framesToken == null ||
(framesToken.Type != JTokenType.Object && framesToken.Type != JTokenType.Array))
{
issue = Error(FrameAnimationImportIssueCode.FramesMissing, sourceId, "JSON 缺少 Object 或 Array 格式的 frames。");
return false;
}
var frames = ParseFrames(framesToken);
var meta = root["meta"] as JObject;
var size = ParseSize(meta?["size"]);
var imageName = meta?.Value<string>("image") ?? string.Empty;
var tags = ParseTags(meta?["frameTags"]);
document = new AsepriteSourceDocument(frames, tags, size, imageName);
return true;
}
catch (Exception exception)
{
issue = Error(
FrameAnimationImportIssueCode.JsonInvalid,
sourceId,
$"Aseprite JSON 解析失败:{exception.Message}");
return false;
}
}
public static bool TryExpandTag(
AsepriteSourceTag tag,
int frameCount,
string sourceId,
out IReadOnlyList<int> indices,
out FrameAnimationImportIssue issue)
{
indices = Array.Empty<int>();
issue = null;
if (tag == null || tag.From < 0 || tag.To < tag.From || tag.To >= frameCount)
{
issue = Error(
FrameAnimationImportIssueCode.TagRangeInvalid,
sourceId,
$"Tag '{tag?.Name}' 的范围 {tag?.From}..{tag?.To} 无效。");
return false;
}
var result = new List<int>();
switch ((tag.Direction ?? string.Empty).ToLowerInvariant())
{
case "forward":
AddAscending(result, tag.From, tag.To);
break;
case "reverse":
AddDescending(result, tag.To, tag.From);
break;
case "pingpong":
AddAscending(result, tag.From, tag.To);
AddDescending(result, tag.To - 1, tag.From + 1);
break;
case "pingpong_reverse":
AddDescending(result, tag.To, tag.From);
AddAscending(result, tag.From + 1, tag.To - 1);
break;
default:
issue = Error(
FrameAnimationImportIssueCode.TagDirectionInvalid,
sourceId,
$"Tag '{tag.Name}' 使用了不支持的 direction '{tag.Direction}'。");
return false;
}
indices = result;
return true;
}
private static List<AsepriteSourceFrame> ParseFrames(JToken token)
{
var frames = new List<AsepriteSourceFrame>();
if (token is JObject objectFrames)
{
foreach (var property in objectFrames.Properties())
{
frames.Add(ParseFrame(property.Name, property.Value));
}
}
else
{
foreach (var item in (JArray)token)
{
frames.Add(ParseFrame(item.Value<string>("filename") ?? string.Empty, item));
}
}
return frames;
}
private static AsepriteSourceFrame ParseFrame(string frameName, JToken token)
{
return new AsepriteSourceFrame(
frameName,
ParseRect(token["frame"]),
token.Value<int?>("duration") ?? 0,
token.Value<bool?>("rotated") ?? false,
token.Value<bool?>("trimmed") ?? false,
ParseRect(token["spriteSourceSize"]),
ParseSize(token["sourceSize"]));
}
private static List<AsepriteSourceTag> ParseTags(JToken token)
{
var tags = new List<AsepriteSourceTag>();
if (!(token is JArray array))
{
return tags;
}
foreach (var item in array)
{
tags.Add(new AsepriteSourceTag(
item.Value<string>("name") ?? string.Empty,
item.Value<int?>("from") ?? -1,
item.Value<int?>("to") ?? -1,
item.Value<string>("direction") ?? string.Empty));
}
return tags;
}
private static RectInt ParseRect(JToken token)
{
return token == null
? default
: new RectInt(
token.Value<int?>("x") ?? 0,
token.Value<int?>("y") ?? 0,
token.Value<int?>("w") ?? 0,
token.Value<int?>("h") ?? 0);
}
private static Vector2Int ParseSize(JToken token)
{
return token == null
? default
: new Vector2Int(token.Value<int?>("w") ?? 0, token.Value<int?>("h") ?? 0);
}
private static void AddAscending(List<int> target, int from, int to)
{
for (var index = from; index <= to; index++)
{
target.Add(index);
}
}
private static void AddDescending(List<int> target, int from, int to)
{
for (var index = from; index >= to; index--)
{
target.Add(index);
}
}
private static FrameAnimationImportIssue Error(
FrameAnimationImportIssueCode code,
string sourceId,
string message)
{
return new FrameAnimationImportIssue(
FrameAnimationValidationSeverity.Error,
code,
sourceId,
sourceId,
message);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a21095473b1495440acab244f9e470c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("AibisDream.FrameAnimation.Tests.EditMode")]
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8edeae42e1831084cbf05db9d9e5d1e2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,307 @@
using System;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
internal sealed class FrameAnimationTextInputWindow : EditorWindow
{
private string label;
private string value;
private string warning;
private Func<string, string> validate;
private Action<string> confirm;
private Vector2 scroll;
public static void Show(
string title,
string label,
string initialValue,
string warning,
Func<string, string> validate,
Action<string> confirm)
{
var window = CreateInstance<FrameAnimationTextInputWindow>();
window.titleContent = new GUIContent(title);
window.label = label;
window.value = initialValue ?? string.Empty;
window.warning = warning ?? string.Empty;
window.validate = validate;
window.confirm = confirm;
window.minSize = new Vector2(460f, 220f);
window.maxSize = new Vector2(700f, 420f);
window.ShowUtility();
}
private void OnGUI()
{
scroll = EditorGUILayout.BeginScrollView(scroll);
EditorGUILayout.LabelField(label, EditorStyles.boldLabel);
GUI.SetNextControlName("FrameAnimationTextInput");
value = EditorGUILayout.TextField(value);
if (!string.IsNullOrEmpty(warning))
{
EditorGUILayout.HelpBox(warning, MessageType.Warning);
}
var error = validate?.Invoke(value) ?? string.Empty;
if (!string.IsNullOrEmpty(error))
{
EditorGUILayout.HelpBox(error, MessageType.Error);
}
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("取消", GUILayout.Width(90f)))
{
Close();
}
using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(error)))
{
if (GUILayout.Button("确认", GUILayout.Width(90f)))
{
var callback = confirm;
var result = value;
Close();
callback?.Invoke(result);
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndScrollView();
}
private void OnEnable()
{
EditorApplication.delayCall += FocusInput;
}
private void FocusInput()
{
if (this != null)
{
Focus();
EditorGUI.FocusTextInControl("FrameAnimationTextInput");
}
}
}
internal sealed class FrameAnimationManualClipWindow : EditorWindow
{
private FrameAnimationGraph graph;
private FrameClip source;
private string id = "NewClip";
private string displayName = "New Clip";
private bool external;
private Action<FrameClip> completed;
public static void Show(
FrameAnimationGraph graph,
FrameClip source,
Action<FrameClip> completed)
{
var window = CreateInstance<FrameAnimationManualClipWindow>();
window.titleContent = new GUIContent(source == null ? "Create Manual Clip" : "Copy As Manual Clip");
window.graph = graph;
window.source = source;
window.completed = completed;
var baseId = source != null ? source.Id + "_Manual" : "NewClip";
window.id = MakeUniqueId(graph, baseId);
window.displayName = source != null ? source.DisplayName + " Manual" : window.id;
window.minSize = new Vector2(460f, 260f);
window.maxSize = new Vector2(700f, 420f);
window.ShowUtility();
}
private void OnGUI()
{
EditorGUILayout.LabelField(source == null ? "创建 Manual Clip" : "复制为 Manual Clip", EditorStyles.boldLabel);
id = EditorGUILayout.TextField("ID", id);
displayName = EditorGUILayout.TextField("Display Name", displayName);
external = EditorGUILayout.ToggleLeft("保存为外部独立 .asset(默认保存为 Graph sub-asset", external);
var error = FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id)
? string.Empty
: "ID 为空或与当前 Graph 的 Clip / Flow 冲突。";
if (!string.IsNullOrEmpty(error))
{
EditorGUILayout.HelpBox(error, MessageType.Error);
}
if (source != null)
{
EditorGUILayout.HelpBox(
"帧表将深拷贝,但 Sprite 仍复用原资源;ImportSource 关联会被清除。",
MessageType.Info);
}
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("取消", GUILayout.Width(90f)))
{
Close();
}
using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(error)))
{
if (GUILayout.Button("创建", GUILayout.Width(90f)))
{
Create();
}
}
EditorGUILayout.EndHorizontal();
}
private void Create()
{
var path = string.Empty;
if (external)
{
path = EditorUtility.SaveFilePanelInProject(
"保存外部 Manual Clip",
id,
"asset",
"请选择 Assets 下的保存位置。");
if (string.IsNullOrEmpty(path))
{
return;
}
}
var succeeded = source == null
? FrameAnimationAssetOperations.CreateManualClip(graph, id, displayName, path, out var result, out var error)
: FrameAnimationAssetOperations.CopyToManual(graph, source, id, displayName, path, out result, out error);
if (!succeeded)
{
EditorUtility.DisplayDialog("操作失败", error, "确定");
return;
}
var callback = completed;
Close();
callback?.Invoke(result);
}
private static string MakeUniqueId(FrameAnimationGraph graph, string baseId)
{
var candidate = baseId;
var suffix = 2;
while (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, candidate))
{
candidate = baseId + suffix++;
}
return candidate;
}
}
internal sealed class FrameAnimationExistingClipWindow : EditorWindow
{
private FrameAnimationGraph graph;
private FrameClip clip;
private Action<FrameClip> completed;
public static void Show(FrameAnimationGraph graph, Action<FrameClip> completed)
{
var window = CreateInstance<FrameAnimationExistingClipWindow>();
window.titleContent = new GUIContent("Add Existing Manual Clip");
window.graph = graph;
window.completed = completed;
window.minSize = new Vector2(460f, 180f);
window.maxSize = new Vector2(640f, 260f);
window.ShowUtility();
}
private void OnGUI()
{
EditorGUILayout.LabelField("添加外部 Manual Clip", EditorStyles.boldLabel);
clip = (FrameClip)EditorGUILayout.ObjectField("FrameClip", clip, typeof(FrameClip), false);
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("取消", GUILayout.Width(90f)))
{
Close();
}
using (new EditorGUI.DisabledScope(clip == null))
{
if (GUILayout.Button("添加", GUILayout.Width(90f)))
{
if (!FrameAnimationAssetOperations.AddExistingManualClip(graph, clip, out var error))
{
EditorUtility.DisplayDialog("添加失败", error, "确定");
return;
}
var callback = completed;
var result = clip;
Close();
callback?.Invoke(result);
}
}
EditorGUILayout.EndHorizontal();
}
}
internal sealed class FrameAnimationFlowWindow : EditorWindow
{
private FrameAnimationGraph graph;
private AnimationNode entry;
private string id;
private string displayName;
private Action<AnimationFlow> completed;
internal static void Show(
FrameAnimationGraph graph,
AnimationNode entry,
Action<AnimationFlow> completed)
{
var window = CreateInstance<FrameAnimationFlowWindow>();
window.titleContent = new GUIContent("Create Animation Flow");
window.graph = graph;
window.entry = entry;
window.id = FrameAnimationGraphMutationService.MakeUniqueFlowId(graph, entry);
window.displayName = window.id;
window.completed = completed;
window.minSize = new Vector2(460f, 230f);
window.maxSize = new Vector2(700f, 360f);
window.ShowUtility();
}
private void OnGUI()
{
EditorGUILayout.LabelField("从选中节点创建 Flow", EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.TextField("Entry Node", entry?.DisplayName ?? string.Empty);
}
id = EditorGUILayout.TextField("ID", id);
displayName = EditorGUILayout.TextField("Display Name", displayName);
var idError = FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id)
? string.Empty
: "ID 为空或与当前 Graph 中的 Clip / Flow 冲突。";
var owner = graph?.Flows.FirstOrDefault(flow => flow != null && flow.EntryNodeId == entry?.InternalId);
if (owner != null)
{
idError = $"该节点已是 Flow '{owner.Id}' 的入口。";
}
if (!string.IsNullOrEmpty(idError))
{
EditorGUILayout.HelpBox(idError, MessageType.Error);
}
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("取消", GUILayout.Width(90f))) Close();
using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(idError)))
{
if (GUILayout.Button("创建", GUILayout.Width(90f)))
{
if (!FrameAnimationGraphMutationService.CreateFlow(
graph, entry, id, displayName, out var flow, out var error))
{
EditorUtility.DisplayDialog("创建 Flow 失败", error, "确定");
return;
}
var callback = completed;
Close();
callback?.Invoke(flow);
}
}
EditorGUILayout.EndHorizontal();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c8f58e7b80263c24087a98874796e91d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,744 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
internal enum FrameAnimationEditorSelectionKind
{
None,
Graph,
Clip,
Flow,
Source,
Node,
Edge
}
internal readonly struct FrameAnimationEditorSelection
{
public FrameAnimationEditorSelectionKind Kind { get; }
public object Value { get; }
public FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind kind, object value)
{
Kind = kind;
Value = value;
}
}
internal enum FrameAnimationClipFilter
{
All,
Manual,
Imported,
Normal,
Missing,
Referenced,
Unused
}
internal enum FrameAnimationClipSort
{
Name,
Source,
FrameCount,
Duration,
Missing,
ReferenceCount
}
internal static class FrameAnimationAssetReferenceIndex
{
public static IReadOnlyList<FrameAnimationGraph> FindGraphsReferencing(FrameClip clip)
{
if (clip == null)
{
return Array.Empty<FrameAnimationGraph>();
}
return AssetDatabase.FindAssets("t:FrameAnimationGraph")
.Select(guid => AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(AssetDatabase.GUIDToAssetPath(guid)))
.Where(graph => graph != null && graph.Clips.Any(item => item == clip))
.Distinct()
.OrderBy(graph => AssetDatabase.GetAssetPath(graph), StringComparer.Ordinal)
.ToArray();
}
}
internal static class FrameAnimationResourceQuery
{
public static IReadOnlyList<FrameClip> QueryClips(
FrameAnimationGraph graph,
string search,
FrameAnimationClipFilter filter,
FrameAnimationClipSort sort)
{
if (graph == null)
{
return Array.Empty<FrameClip>();
}
var sourceNames = graph.ImportSources.Where(source => source != null)
.GroupBy(source => source.InternalId ?? string.Empty)
.ToDictionary(group => group.Key, group => group.First().DisplayName ?? string.Empty);
var query = graph.Clips.Where(clip => clip != null && MatchesSearch(clip, sourceNames, search));
query = filter switch
{
FrameAnimationClipFilter.Manual => query.Where(clip => !clip.IsImported),
FrameAnimationClipFilter.Imported => query.Where(clip => clip.IsImported),
FrameAnimationClipFilter.Normal => query.Where(clip => !clip.IsImported || !clip.ImportInfo.IsMissingFromSource),
FrameAnimationClipFilter.Missing => query.Where(clip => clip.IsImported && clip.ImportInfo.IsMissingFromSource),
FrameAnimationClipFilter.Referenced => query.Where(clip => ReferenceCount(graph, clip) > 0),
FrameAnimationClipFilter.Unused => query.Where(clip => ReferenceCount(graph, clip) == 0),
_ => query
};
IOrderedEnumerable<FrameClip> ordered = sort switch
{
FrameAnimationClipSort.Source => query.OrderBy(clip => SourceName(clip, sourceNames), StringComparer.Ordinal),
FrameAnimationClipSort.FrameCount => query.OrderBy(clip => clip.FrameCount),
FrameAnimationClipSort.Duration => query.OrderBy(clip => clip.TotalDurationMs),
FrameAnimationClipSort.Missing => query.OrderBy(clip => clip.IsImported && clip.ImportInfo.IsMissingFromSource ? 1 : 0),
FrameAnimationClipSort.ReferenceCount => query.OrderBy(clip => ReferenceCount(graph, clip)),
_ => query.OrderBy(clip => clip.DisplayName ?? clip.Id, StringComparer.Ordinal)
};
return ordered.ThenBy(clip => clip.Id, StringComparer.Ordinal)
.ThenBy(clip => AssetDatabase.GetAssetPath(clip), StringComparer.Ordinal)
.ToArray();
}
public static int ReferenceCount(FrameAnimationGraph graph, FrameClip clip)
{
return graph?.Nodes.Count(node => node != null && node.ClipId == clip?.Id) ?? 0;
}
public static string SourceName(
FrameClip clip,
IReadOnlyDictionary<string, string> sourceNames = null)
{
if (clip == null || !clip.IsImported)
{
return "Manual";
}
var sourceId = clip.ImportInfo?.ImportSourceId;
if (sourceNames != null && !string.IsNullOrEmpty(sourceId) &&
sourceNames.TryGetValue(sourceId, out var name))
{
return name;
}
return sourceId ?? string.Empty;
}
private static bool MatchesSearch(
FrameClip clip,
IReadOnlyDictionary<string, string> sourceNames,
string search)
{
if (string.IsNullOrWhiteSpace(search))
{
return true;
}
return Contains(clip.Id, search) || Contains(clip.DisplayName, search) ||
Contains(clip.ImportInfo?.SourceTagName, search) ||
Contains(SourceName(clip, sourceNames), search);
}
private static bool Contains(string value, string search)
{
return !string.IsNullOrEmpty(value) && value.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0;
}
}
internal static class FrameAnimationAssetOperations
{
public static bool IsPlayableIdAvailable(
FrameAnimationGraph graph,
string id,
FrameClip exceptClip = null,
AnimationFlow exceptFlow = null)
{
return graph != null && !string.IsNullOrWhiteSpace(id) &&
graph.Clips.All(clip => clip == null || clip == exceptClip || clip.Id != id) &&
graph.Flows.All(flow => flow == null || flow == exceptFlow || flow.Id != id);
}
public static bool CreateManualClip(
FrameAnimationGraph graph,
string id,
string displayName,
string externalPath,
out FrameClip clip,
out string error)
{
clip = null;
error = string.Empty;
if (!IsPlayableIdAvailable(graph, id))
{
error = "Clip id 为空或与现有 Clip / Flow 冲突。";
return false;
}
Undo.IncrementCurrentGroup();
var group = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Create Manual Frame Clip");
try
{
Undo.RecordObject(graph, "Add Manual Frame Clip");
clip = ScriptableObject.CreateInstance<FrameClip>();
clip.name = id;
clip.Configure(id, string.IsNullOrWhiteSpace(displayName) ? id : displayName,
Array.Empty<FrameAnimationFrame>(), 1f,
graph.Settings.NewManualClipDefaultEndBehavior);
if (string.IsNullOrWhiteSpace(externalPath))
{
AssetDatabase.AddObjectToAsset(clip, graph);
}
else
{
AssetDatabase.CreateAsset(clip, externalPath);
}
Undo.RegisterCreatedObjectUndo(clip, "Create Manual Frame Clip");
graph.AddClip(clip);
EditorUtility.SetDirty(graph);
EditorUtility.SetDirty(clip);
AssetDatabase.SaveAssets();
Undo.CollapseUndoOperations(group);
return true;
}
catch (Exception exception)
{
Undo.RevertAllDownToGroup(group);
clip = null;
error = exception.Message;
return false;
}
}
public static bool AddExistingManualClip(
FrameAnimationGraph graph,
FrameClip clip,
out string error)
{
error = string.Empty;
if (graph == null || clip == null)
{
error = "Graph 或 Clip 为空。";
return false;
}
if (clip.IsImported || AssetDatabase.IsSubAsset(clip) ||
string.IsNullOrEmpty(AssetDatabase.GetAssetPath(clip)))
{
error = "只能添加独立 .asset 形式的 Manual Clip。";
return false;
}
if (graph.Clips.Contains(clip))
{
error = "当前 Graph 已引用该 Clip。";
return false;
}
if (!IsPlayableIdAvailable(graph, clip.Id))
{
error = $"playable id '{clip.Id}' 与当前 Graph 中的 Clip / Flow 冲突。";
return false;
}
Undo.RecordObject(graph, "Add Existing Manual Frame Clip");
graph.AddClip(clip);
EditorUtility.SetDirty(graph);
return true;
}
public static bool CopyToManual(
FrameAnimationGraph graph,
FrameClip source,
string id,
string displayName,
string externalPath,
out FrameClip copy,
out string error)
{
copy = null;
error = string.Empty;
if (source == null || !IsPlayableIdAvailable(graph, id))
{
error = "来源为空,或新 id 与现有 Clip / Flow 冲突。";
return false;
}
Undo.IncrementCurrentGroup();
var group = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Copy Frame Clip As Manual");
try
{
Undo.RecordObject(graph, "Add Manual Frame Clip Copy");
copy = ScriptableObject.CreateInstance<FrameClip>();
copy.name = id;
copy.Configure(id,
string.IsNullOrWhiteSpace(displayName) ? source.DisplayName : displayName,
source.Frames.Select(frame => frame == null
? null
: new FrameAnimationFrame(frame.Sprite, frame.DurationMs, frame.FrameName, frame.SourceIndex)),
source.Speed,
source.DefaultEndBehavior);
if (string.IsNullOrWhiteSpace(externalPath))
{
AssetDatabase.AddObjectToAsset(copy, graph);
}
else
{
AssetDatabase.CreateAsset(copy, externalPath);
}
Undo.RegisterCreatedObjectUndo(copy, "Create Manual Frame Clip Copy");
graph.AddClip(copy);
EditorUtility.SetDirty(graph);
EditorUtility.SetDirty(copy);
AssetDatabase.SaveAssets();
Undo.CollapseUndoOperations(group);
return true;
}
catch (Exception exception)
{
Undo.RevertAllDownToGroup(group);
copy = null;
error = exception.Message;
return false;
}
}
public static bool RenameClip(
FrameAnimationGraph graph,
FrameClip clip,
string newId,
out string error)
{
error = string.Empty;
if (graph == null || clip == null || !graph.Clips.Contains(clip))
{
error = "Clip 不属于当前 Graph。";
return false;
}
var owners = FrameAnimationAssetReferenceIndex.FindGraphsReferencing(clip);
if (!AssetDatabase.IsSubAsset(clip) && owners.Count > 1)
{
error = "外部 Manual Clip 被多个 Graph 共享,禁止重命名:" +
string.Join(", ", owners.Select(owner => owner.name));
return false;
}
if (!IsPlayableIdAvailable(graph, newId, clip))
{
error = "新 id 为空或与现有 Clip / Flow 冲突。";
return false;
}
var oldId = clip.Id;
Undo.IncrementCurrentGroup();
var group = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Rename Frame Clip");
Undo.RecordObject(graph, "Update Frame Clip References");
Undo.RecordObject(clip, "Rename Frame Clip");
clip.SetId(newId);
foreach (var node in graph.Nodes.Where(node => node != null && node.ClipId == oldId))
{
node.SetClipId(newId);
}
if (graph.Settings.DefaultPlayableId == oldId)
{
graph.Settings.SetDefaultPlayableId(newId);
}
EditorUtility.SetDirty(graph);
EditorUtility.SetDirty(clip);
Undo.CollapseUndoOperations(group);
return true;
}
public static bool RenameFlow(
FrameAnimationGraph graph,
AnimationFlow flow,
string newId,
out string error)
{
error = string.Empty;
if (graph == null || flow == null || !graph.Flows.Contains(flow) ||
!IsPlayableIdAvailable(graph, newId, exceptFlow: flow))
{
error = "Flow 不属于当前 Graph,或新 id 与现有 Clip / Flow 冲突。";
return false;
}
var oldId = flow.Id;
Undo.RecordObject(graph, "Rename Animation Flow");
flow.SetId(newId);
if (graph.Settings.DefaultPlayableId == oldId)
{
graph.Settings.SetDefaultPlayableId(newId);
}
foreach (var data in graph.EditorData.FlowEditorData.Where(data => data != null && data.FlowId == oldId))
{
data.SetFlowId(newId);
}
EditorUtility.SetDirty(graph);
return true;
}
public static bool RenameGraph(FrameAnimationGraph graph, string newId, out string error)
{
error = string.Empty;
if (graph == null || string.IsNullOrWhiteSpace(newId))
{
error = "Graph id 不能为空。";
return false;
}
Undo.RecordObject(graph, "Rename Frame Animation Graph");
graph.SetId(newId);
EditorUtility.SetDirty(graph);
return true;
}
public static bool RemoveClip(FrameAnimationGraph graph, FrameClip clip, out string error)
{
if (!CanRemoveClip(graph, clip, out error))
{
return false;
}
Undo.IncrementCurrentGroup();
var group = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Remove Frame Clip");
Undo.RecordObject(graph, "Remove Frame Clip Reference");
graph.RemoveClip(clip);
if (AssetDatabase.IsSubAsset(clip) && AssetDatabase.GetAssetPath(clip) == AssetDatabase.GetAssetPath(graph))
{
Undo.DestroyObjectImmediate(clip);
}
EditorUtility.SetDirty(graph);
AssetDatabase.SaveAssets();
Undo.CollapseUndoOperations(group);
return true;
}
public static bool CanRemoveClip(FrameAnimationGraph graph, FrameClip clip, out string error)
{
error = string.Empty;
if (graph == null || clip == null || !graph.Clips.Contains(clip))
{
error = "Clip 不属于当前 Graph。";
return false;
}
var nodeCount = graph.Nodes.Count(node => node != null && node.ClipId == clip.Id);
if (nodeCount > 0)
{
error = $"Clip 仍被 {nodeCount} 个 AnimationNode 引用。";
return false;
}
if (graph.Settings.DefaultPlayableId == clip.Id)
{
error = "Clip 仍被 defaultPlayableId 引用。";
return false;
}
return true;
}
public static bool RemoveFlow(FrameAnimationGraph graph, AnimationFlow flow, out string error)
{
if (!CanRemoveFlow(graph, flow, out error))
{
return false;
}
Undo.RecordObject(graph, "Remove Animation Flow");
graph.RemoveFlow(flow);
graph.EditorData.RemoveFlowData(flow.Id);
EditorUtility.SetDirty(graph);
return true;
}
public static bool CanRemoveFlow(FrameAnimationGraph graph, AnimationFlow flow, out string error)
{
error = string.Empty;
if (graph == null || flow == null || !graph.Flows.Contains(flow))
{
error = "Flow 不属于当前 Graph。";
return false;
}
if (graph.Settings.DefaultPlayableId == flow.Id)
{
error = "Flow 仍被 defaultPlayableId 引用。";
return false;
}
return true;
}
public static bool RemoveSource(
FrameAnimationGraph graph,
FrameAnimationImportSource source,
out string error)
{
error = string.Empty;
if (graph == null || source == null || !graph.ImportSources.Contains(source))
{
error = "ImportSource 不属于当前 Graph。";
return false;
}
var clips = graph.Clips.Where(clip => clip != null &&
clip.ImportInfo?.ImportSourceId == source.InternalId).ToArray();
if (clips.Length > 0)
{
error = "ImportSource 仍关联 Imported Clip" + string.Join(", ", clips.Select(clip => clip.Id));
return false;
}
Undo.RecordObject(graph, "Remove Frame Animation ImportSource");
graph.RemoveImportSource(source);
EditorUtility.SetDirty(graph);
return true;
}
}
internal sealed class FrameAnimationEditorIssue
{
public FrameAnimationValidationSeverity Severity { get; }
public string Code { get; }
public FrameAnimationEditorSelection Selection { get; }
public string Message { get; }
public string Suggestion { get; }
public FrameAnimationEditorIssue(
FrameAnimationValidationSeverity severity,
string code,
FrameAnimationEditorSelection selection,
string message,
string suggestion)
{
Severity = severity;
Code = code ?? string.Empty;
Selection = selection;
Message = message ?? string.Empty;
Suggestion = suggestion ?? string.Empty;
}
}
internal static class FrameAnimationEditorValidationService
{
public static IReadOnlyList<FrameAnimationEditorIssue> Validate(
FrameAnimationGraph graph,
bool includeImports,
out FrameAnimationImportPreview importPreview)
{
importPreview = null;
if (graph == null)
{
return Array.Empty<FrameAnimationEditorIssue>();
}
var issues = FrameAnimationGraphValidator.Validate(graph).Issues
.Select(issue => new FrameAnimationEditorIssue(
issue.Severity,
issue.Code.ToString(),
Locate(graph, issue.TargetType, issue.TargetId),
issue.Message,
Suggestion(issue.Code)))
.ToList();
foreach (var duplicate in graph.Clips.Where(clip => clip != null)
.GroupBy(clip => clip).Where(group => group.Count() > 1))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Error,
"DuplicateClipReference",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, duplicate.Key),
$"Graph 重复引用 Clip '{duplicate.Key.Id}'。",
"移除重复的 Clip 引用。"));
}
var graphPath = AssetDatabase.GetAssetPath(graph);
foreach (var clip in graph.Clips.Where(clip => clip != null))
{
if (AssetDatabase.IsSubAsset(clip) && AssetDatabase.GetAssetPath(clip) != graphPath)
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Error,
"ForeignSubAssetClip",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, clip),
$"Clip '{clip.Id}' 是其他资产的 sub-asset。",
"移除该引用,并复制为当前 Graph 的 Manual Clip。"));
}
if (clip.IsImported && (!AssetDatabase.IsSubAsset(clip) || AssetDatabase.GetAssetPath(clip) != graphPath))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Error,
"ImportedClipOwnership",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, clip),
$"Imported Clip '{clip.Id}' 不是当前 Graph 的 sub-asset。",
"删除非法引用并从对应 ImportSource 重新刷新。"));
}
}
foreach (var node in graph.Nodes.Where(node => node != null))
{
var clip = graph.Clips.SingleOrDefault(item => item != null && item.Id == node.ClipId);
if (clip?.ImportInfo?.IsMissingFromSource == true)
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Error,
"NodeReferencesMissingClip",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node),
$"Node '{node.InternalId}' 引用了 Missing Clip '{clip.Id}'。",
"恢复源 Tag、修改节点引用或删除该节点。"));
}
}
var nodeIds = new HashSet<string>(graph.Nodes.Where(node => node != null)
.Select(node => node.InternalId));
foreach (var node in graph.Nodes.Where(node => node != null &&
graph.EditorData.NodeEditorData.All(data => data == null || data.NodeId != node.InternalId)))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Warning,
"EditorDataMissing",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node),
$"Node '{node.InternalId}' 缺少画布位置数据。",
"打开 Graph Editor 初始化位置,或执行自动布局。"));
}
foreach (var data in graph.EditorData.NodeEditorData.Where(data => data != null &&
!nodeIds.Contains(data.NodeId)))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Warning,
"EditorDataOrphan",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
$"存在孤立的 NodeEditorData '{data.NodeId}'。",
"该数据不会影响运行时;确认无引用后可由后续维护工具清理。"));
}
foreach (var duplicate in graph.EditorData.NodeEditorData.Where(data => data != null)
.GroupBy(data => data.NodeId).Where(group => group.Count() > 1))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Error,
"EditorDataDuplicate",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
$"NodeEditorData '{duplicate.Key}' 重复。",
"保留一条对应位置数据并移除重复项。"));
}
var flowIds = new HashSet<string>(graph.Flows.Where(flow => flow != null).Select(flow => flow.Id));
foreach (var flow in graph.Flows.Where(flow => flow != null &&
graph.EditorData.FlowEditorData.All(data => data == null || data.FlowId != flow.Id)))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Warning,
"FlowEditorDataMissing",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Flow, flow),
$"Flow '{flow.Id}' 缺少画布颜色数据。",
"打开 Graph Editor 初始化 Flow 颜色。"));
}
foreach (var data in graph.EditorData.FlowEditorData.Where(data => data != null &&
!flowIds.Contains(data.FlowId)))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Warning,
"FlowEditorDataOrphan",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
$"存在孤立的 FlowEditorData '{data.FlowId}'。",
"确认 Flow 已删除后可由后续维护工具清理。"));
}
foreach (var duplicate in graph.EditorData.FlowEditorData.Where(data => data != null)
.GroupBy(data => data.FlowId).Where(group => group.Count() > 1))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Error,
"FlowEditorDataDuplicate",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
$"FlowEditorData '{duplicate.Key}' 重复。",
"保留一条对应颜色数据并移除重复项。"));
}
if (!includeImports)
{
return issues;
}
importPreview = FrameAnimationImportService.PreviewAll(graph);
foreach (var sourcePreview in importPreview.Sources)
{
foreach (var importIssue in sourcePreview.Issues)
{
issues.Add(new FrameAnimationEditorIssue(
importIssue.Severity,
importIssue.Code.ToString(),
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Source, sourcePreview.Source),
importIssue.Message,
"打开对应 ImportSource,修复来源设置后重新 Preview。"));
}
}
return issues;
}
private static FrameAnimationEditorSelection Locate(
FrameAnimationGraph graph,
FrameAnimationValidationTargetType type,
string id)
{
return type switch
{
FrameAnimationValidationTargetType.Clip => new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Clip, graph.Clips.FirstOrDefault(clip => clip != null && clip.Id == id)),
FrameAnimationValidationTargetType.Flow => new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Flow, graph.Flows.FirstOrDefault(flow => flow != null && flow.Id == id)),
FrameAnimationValidationTargetType.Node => new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Node, graph.Nodes.FirstOrDefault(node => node != null && node.InternalId == id)),
FrameAnimationValidationTargetType.Edge => new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Edge, graph.Edges.FirstOrDefault(edge => edge != null && edge.InternalId == id)),
FrameAnimationValidationTargetType.ImportSource => new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Source, graph.ImportSources.FirstOrDefault(source => source != null && source.InternalId == id)),
_ => new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph)
};
}
private static string Suggestion(FrameAnimationValidationCode code)
{
return code switch
{
FrameAnimationValidationCode.PlayableIdEmpty => "使用正式 Rename 命令设置唯一 playable id。",
FrameAnimationValidationCode.PlayableIdDuplicate => "重命名冲突的 Clip 或 Flow。",
FrameAnimationValidationCode.ClipFramesEmpty => "为 Manual Clip 添加帧,或刷新 Imported Clip 来源。",
FrameAnimationValidationCode.FrameDurationInvalid => "将 durationMs 修改为大于 0 的整数。",
FrameAnimationValidationCode.DefaultPlayableInvalid => "在 Graph 属性中选择有效的默认 Clip 或 Flow。",
FrameAnimationValidationCode.ImportedClipMissingFromSource => "恢复源 Tag,或确认没有引用后删除 Missing Clip。",
_ => "打开问题对象并修复对应字段或引用。"
};
}
}
internal static class FrameAnimationWorkspaceState
{
private const string Prefix = "AibisDream.FrameAnimation.Workspace.";
public static string Key(FrameAnimationGraph graph, string name)
{
var graphPath = graph != null ? AssetDatabase.GetAssetPath(graph) : string.Empty;
var graphGuid = string.IsNullOrEmpty(graphPath) ? "none" : AssetDatabase.AssetPathToGUID(graphPath);
var project = Hash128.Compute(Application.dataPath).ToString();
return Prefix + project + "." + graphGuid + "." + name;
}
public static float GetFloat(FrameAnimationGraph graph, string name, float fallback) =>
EditorPrefs.GetFloat(Key(graph, name), fallback);
public static void SetFloat(FrameAnimationGraph graph, string name, float value) =>
EditorPrefs.SetFloat(Key(graph, name), value);
public static string GetString(FrameAnimationGraph graph, string name, string fallback) =>
EditorPrefs.GetString(Key(graph, name), fallback);
public static void SetString(FrameAnimationGraph graph, string name, string value) =>
EditorPrefs.SetString(Key(graph, name), value ?? string.Empty);
public static bool GetBool(FrameAnimationGraph graph, string name, bool fallback) =>
EditorPrefs.GetBool(Key(graph, name), fallback);
public static void SetBool(FrameAnimationGraph graph, string name, bool value) =>
EditorPrefs.SetBool(Key(graph, name), value);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2643ad174ef237e43b98400d60c4a201
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,603 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
internal static class FrameAnimationFlowFocusPolicy
{
public static AnimationFlow FindFocusedFlow(FrameAnimationGraph graph, string focusedFlowId)
{
return graph?.Flows.FirstOrDefault(flow => flow != null && flow.Id == focusedFlowId);
}
public static bool Contains(
FrameAnimationGraph graph,
string focusedFlowId,
FrameAnimationEditorSelection selection)
{
var flow = FindFocusedFlow(graph, focusedFlowId);
if (flow == null)
{
return false;
}
var reachable = new FrameAnimationGraphTopology(graph).GetReachable(flow.EntryNodeId);
return selection.Value switch
{
AnimationNode node => reachable.Nodes.Contains(node),
AnimationEdge edge => reachable.Edges.Contains(edge),
AnimationFlow selectedFlow => selectedFlow == flow,
_ => false
};
}
public static bool ShouldExitForSelection(
FrameAnimationGraph graph,
string focusedFlowId,
FrameAnimationEditorSelection selection)
{
if (string.IsNullOrEmpty(focusedFlowId))
{
return false;
}
return selection.Kind switch
{
FrameAnimationEditorSelectionKind.None => false,
FrameAnimationEditorSelectionKind.Flow => false,
FrameAnimationEditorSelectionKind.Node => !Contains(graph, focusedFlowId, selection),
FrameAnimationEditorSelectionKind.Edge => !Contains(graph, focusedFlowId, selection),
_ => true
};
}
public static bool ShouldExitForSelectionSet(
FrameAnimationGraph graph,
string focusedFlowId,
IEnumerable<FrameAnimationEditorSelection> selections)
{
if (string.IsNullOrEmpty(focusedFlowId))
{
return false;
}
var items = (selections ?? Array.Empty<FrameAnimationEditorSelection>()).ToArray();
return items.Length > 0 && items.Any(item =>
(item.Kind == FrameAnimationEditorSelectionKind.Node ||
item.Kind == FrameAnimationEditorSelectionKind.Edge)
? !Contains(graph, focusedFlowId, item)
: ShouldExitForSelection(graph, focusedFlowId, item));
}
}
internal sealed class FrameAnimationGraphImpact
{
public IReadOnlyList<AnimationFlow> Flows { get; }
public int LostNodeCount { get; }
internal FrameAnimationGraphImpact(IReadOnlyList<AnimationFlow> flows, int lostNodeCount)
{
Flows = flows ?? Array.Empty<AnimationFlow>();
LostNodeCount = lostNodeCount;
}
}
internal static class FrameAnimationGraphImpactAnalyzer
{
public static FrameAnimationGraphImpact AnalyzeEdgeRemoval(
FrameAnimationGraph graph,
IEnumerable<AnimationEdge> edges)
{
var removed = new HashSet<AnimationEdge>((edges ?? Array.Empty<AnimationEdge>()).Where(edge => edge != null));
return Analyze(graph, Array.Empty<AnimationNode>(), removed);
}
public static FrameAnimationGraphImpact AnalyzeNodeRemoval(
FrameAnimationGraph graph,
IEnumerable<AnimationNode> nodes)
{
var removedNodes = new HashSet<AnimationNode>((nodes ?? Array.Empty<AnimationNode>()).Where(node => node != null));
var removedIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
var removedEdges = new HashSet<AnimationEdge>(graph.Edges.Where(edge => edge != null &&
(removedIds.Contains(edge.FromNodeId) || removedIds.Contains(edge.ToNodeId))));
return Analyze(graph, removedNodes, removedEdges);
}
public static FrameAnimationGraphImpact AnalyzeRemoval(
FrameAnimationGraph graph,
IEnumerable<AnimationNode> nodes,
IEnumerable<AnimationEdge> edges)
{
if (graph == null)
{
return new FrameAnimationGraphImpact(Array.Empty<AnimationFlow>(), 0);
}
var removedNodes = new HashSet<AnimationNode>((nodes ?? Array.Empty<AnimationNode>())
.Where(node => node != null));
var removedIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
var removedEdges = new HashSet<AnimationEdge>((edges ?? Array.Empty<AnimationEdge>())
.Where(edge => edge != null));
removedEdges.UnionWith(graph.Edges.Where(edge => edge != null &&
(removedIds.Contains(edge.FromNodeId) || removedIds.Contains(edge.ToNodeId))));
return Analyze(graph, removedNodes, removedEdges);
}
private static FrameAnimationGraphImpact Analyze(
FrameAnimationGraph graph,
IReadOnlyCollection<AnimationNode> removedNodes,
IReadOnlyCollection<AnimationEdge> removedEdges)
{
if (graph == null)
{
return new FrameAnimationGraphImpact(Array.Empty<AnimationFlow>(), 0);
}
var topology = new FrameAnimationGraphTopology(graph);
var removedNodeIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
var affected = new List<AnimationFlow>();
var lost = new HashSet<string>();
foreach (var flow in graph.Flows.Where(flow => flow != null))
{
var before = topology.GetReachable(flow.EntryNodeId).Nodes
.Where(node => node != null).Select(node => node.InternalId).ToHashSet();
var after = ReachableWithout(graph, flow.EntryNodeId, removedNodeIds, removedEdges);
var flowLost = before.Where(nodeId => !after.Contains(nodeId)).ToArray();
if (flowLost.Length > 0)
{
affected.Add(flow);
lost.UnionWith(flowLost);
}
}
return new FrameAnimationGraphImpact(
affected.OrderBy(flow => flow.Id, StringComparer.Ordinal).ToArray(),
lost.Count);
}
private static HashSet<string> ReachableWithout(
FrameAnimationGraph graph,
string entry,
HashSet<string> removedNodeIds,
IReadOnlyCollection<AnimationEdge> removedEdges)
{
var result = new HashSet<string>();
if (removedNodeIds.Contains(entry ?? string.Empty))
{
return result;
}
var outgoing = graph.Edges.Where(edge => edge != null && !removedEdges.Contains(edge))
.GroupBy(edge => edge.FromNodeId ?? string.Empty)
.ToDictionary(group => group.Key, group => group.ToArray());
var queue = new Queue<string>();
queue.Enqueue(entry ?? string.Empty);
while (queue.Count > 0)
{
var id = queue.Dequeue();
if (removedNodeIds.Contains(id) || !result.Add(id))
{
continue;
}
if (outgoing.TryGetValue(id, out var edges))
{
foreach (var edge in edges)
{
queue.Enqueue(edge.ToNodeId ?? string.Empty);
}
}
}
return result;
}
}
internal static class FrameAnimationGraphMutationService
{
private static readonly Color[] FlowColors =
{
new Color(0.24f, 0.65f, 1f), new Color(0.42f, 0.8f, 0.42f),
new Color(1f, 0.62f, 0.24f), new Color(0.82f, 0.42f, 0.9f),
new Color(1f, 0.38f, 0.52f), new Color(0.25f, 0.82f, 0.78f)
};
public static bool CreateNode(
FrameAnimationGraph graph,
FrameClip clip,
Vector2 position,
out AnimationNode node,
out string error)
{
node = null;
error = string.Empty;
if (graph == null || clip == null || !graph.Clips.Contains(clip))
{
error = "只能为当前 Graph 中的 FrameClip 创建节点。";
return false;
}
Undo.RecordObject(graph, "Create Frame Animation Node");
node = new AnimationNode(clip.Id,
string.IsNullOrWhiteSpace(clip.DisplayName) ? clip.Id : clip.DisplayName);
graph.AddNode(node);
graph.EditorData.GetOrCreateNodeData(node.InternalId, position).SetPosition(position);
Dirty(graph);
return true;
}
public static bool TryConnect(
FrameAnimationGraph graph,
AnimationNode from,
AnimationNode to,
out AnimationEdge edge,
out string error)
{
edge = null;
if (!CanConnect(graph, from, to, out error))
{
return false;
}
Undo.RecordObject(graph, "Connect Frame Animation Nodes");
edge = new AnimationEdge(from.InternalId, to.InternalId);
graph.AddEdge(edge);
Dirty(graph);
return true;
}
public static bool CanConnect(
FrameAnimationGraph graph,
AnimationNode from,
AnimationNode to,
out string error)
{
error = string.Empty;
if (graph == null || from == null || to == null ||
graph.Nodes.Count(node => node == from) != 1 || graph.Nodes.Count(node => node == to) != 1)
{
error = "连接两端必须唯一属于当前 Graph。";
return false;
}
if (from == to || from.InternalId == to.InternalId)
{
error = "节点不允许连接到自身。";
return false;
}
var topology = new FrameAnimationGraphTopology(graph);
if (topology.GetOutgoing(from.InternalId).Count > 0)
{
error = "第一版每个节点最多只能有一个后继。";
return false;
}
if (from.EndBehaviorOverride.HasValue)
{
error = "该节点已设置终点结束行为,请先清除覆盖。";
return false;
}
if (graph.Edges.Any(existing => existing != null &&
existing.FromNodeId == from.InternalId && existing.ToNodeId == to.InternalId))
{
error = "该连接已经存在。";
return false;
}
if (topology.WouldCreateCycle(from.InternalId, to.InternalId))
{
error = "该连接会形成自连接或多节点环路。";
return false;
}
return true;
}
public static void SetNodeDisplayName(FrameAnimationGraph graph, AnimationNode node, string value)
{
Undo.RecordObject(graph, "Rename Frame Animation Node");
node.SetDisplayName(value);
Dirty(graph);
}
public static void SetNodeClip(FrameAnimationGraph graph, AnimationNode node, FrameClip clip)
{
Undo.RecordObject(graph, "Set Frame Animation Node Clip");
node.SetClipId(clip != null ? clip.Id : string.Empty);
Dirty(graph);
}
public static void SetNodeSpeed(FrameAnimationGraph graph, AnimationNode node, float? value)
{
Undo.RecordObject(graph, "Set Frame Animation Node Speed");
node.SetSpeedOverride(value);
Dirty(graph);
}
public static bool SetNodeEndBehavior(
FrameAnimationGraph graph,
AnimationNode node,
FrameClipEndBehavior? value,
bool removeOutgoing,
out string error)
{
error = string.Empty;
var outgoing = graph.Edges.Where(edge => edge != null && edge.FromNodeId == node.InternalId).ToArray();
if (value.HasValue && outgoing.Length > 0 && !removeOutgoing)
{
error = "节点仍有后继 Edge。";
return false;
}
Undo.RecordObject(graph, "Set Frame Animation Node End Behavior");
if (value.HasValue)
{
foreach (var edge in outgoing)
{
graph.RemoveEdge(edge);
}
}
node.SetEndBehaviorOverride(value);
Dirty(graph);
return true;
}
public static void RemoveEdges(FrameAnimationGraph graph, IEnumerable<AnimationEdge> edges)
{
var items = (edges ?? Array.Empty<AnimationEdge>()).Where(edge => edge != null).Distinct().ToArray();
if (graph == null || items.Length == 0)
{
return;
}
Undo.RecordObject(graph, "Disconnect Frame Animation Nodes");
foreach (var edge in items)
{
graph.RemoveEdge(edge);
}
Dirty(graph);
}
public static bool RemoveNodes(
FrameAnimationGraph graph,
IEnumerable<AnimationNode> nodes,
bool deleteEntryFlows,
out string error)
{
return RemoveNodesAndEdges(
graph, nodes, Array.Empty<AnimationEdge>(), deleteEntryFlows, out error);
}
public static bool RemoveNodesAndEdges(
FrameAnimationGraph graph,
IEnumerable<AnimationNode> nodes,
IEnumerable<AnimationEdge> additionalEdges,
bool deleteEntryFlows,
out string error)
{
error = string.Empty;
if (graph == null)
{
error = "Graph 已失效。";
return false;
}
var items = (nodes ?? Array.Empty<AnimationNode>()).Where(node => node != null).Distinct().ToArray();
var extraEdges = (additionalEdges ?? Array.Empty<AnimationEdge>())
.Where(edge => edge != null).Distinct().ToArray();
var ids = new HashSet<string>(items.Select(node => node.InternalId));
var entryFlows = graph.Flows.Where(flow => flow != null && ids.Contains(flow.EntryNodeId)).ToArray();
if (entryFlows.Length > 0 && !deleteEntryFlows)
{
error = "删除项包含 Flow 入口节点。";
return false;
}
Undo.RecordObject(graph, "Delete Frame Animation Graph Elements");
foreach (var edge in graph.Edges.Where(edge => edge != null &&
(ids.Contains(edge.FromNodeId) || ids.Contains(edge.ToNodeId) || extraEdges.Contains(edge))).ToArray())
{
graph.RemoveEdge(edge);
}
foreach (var flow in entryFlows)
{
graph.RemoveFlow(flow);
graph.EditorData.RemoveFlowData(flow.Id);
if (graph.Settings.DefaultPlayableId == flow.Id)
{
graph.Settings.SetDefaultPlayableId(string.Empty);
}
}
foreach (var node in items)
{
graph.RemoveNode(node);
graph.EditorData.RemoveNodeData(node.InternalId);
}
Dirty(graph);
return true;
}
public static string MakeUniqueFlowId(FrameAnimationGraph graph, AnimationNode node)
{
var root = (string.IsNullOrWhiteSpace(node?.DisplayName) ? "Animation" : node.DisplayName) + "Flow";
var candidate = root;
var suffix = 2;
while (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, candidate))
{
candidate = root + suffix++;
}
return candidate;
}
public static bool CreateFlow(
FrameAnimationGraph graph,
AnimationNode entry,
string id,
string displayName,
out AnimationFlow flow,
out string error)
{
flow = null;
error = string.Empty;
if (graph == null || entry == null || !graph.Nodes.Contains(entry))
{
error = "Flow 入口必须属于当前 Graph。";
return false;
}
var owner = graph.Flows.FirstOrDefault(existing => existing != null && existing.EntryNodeId == entry.InternalId);
if (owner != null)
{
error = $"该节点已是 Flow '{owner.Id}' 的入口。";
return false;
}
if (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id))
{
error = "Flow id 为空或与 Clip / Flow 冲突。";
return false;
}
Undo.RecordObject(graph, "Create Animation Flow");
flow = new AnimationFlow(id, string.IsNullOrWhiteSpace(displayName) ? id : displayName, entry.InternalId);
graph.AddFlow(flow);
graph.EditorData.GetOrCreateFlowData(flow.Id, FlowColors[(graph.Flows.Count - 1) % FlowColors.Length]);
Dirty(graph);
return true;
}
public static bool SetFlowEntry(
FrameAnimationGraph graph,
AnimationFlow flow,
AnimationNode entry,
out string error)
{
error = string.Empty;
if (graph == null || flow == null || entry == null ||
!graph.Flows.Contains(flow) || !graph.Nodes.Contains(entry))
{
error = "Flow 或入口节点不属于当前 Graph。";
return false;
}
var owner = graph.Flows.FirstOrDefault(existing => existing != null && existing != flow &&
existing.EntryNodeId == entry.InternalId);
if (owner != null)
{
error = $"该节点已是 Flow '{owner.Id}' 的入口。";
return false;
}
Undo.RecordObject(graph, "Set Animation Flow Entry");
flow.SetEntryNodeId(entry.InternalId);
Dirty(graph);
return true;
}
public static void SetFlowColor(FrameAnimationGraph graph, AnimationFlow flow, Color value)
{
if (graph == null || flow == null)
{
return;
}
Undo.RecordObject(graph, "Set Animation Flow Color");
graph.EditorData.GetOrCreateFlowData(flow.Id, value).SetColor(value);
Dirty(graph);
}
public static bool EnsureEditorData(FrameAnimationGraph graph)
{
if (graph == null)
{
return false;
}
var missingNodes = graph.Nodes.Where(node => node != null &&
graph.EditorData.NodeEditorData.All(data => data == null || data.NodeId != node.InternalId)).ToArray();
var missingFlows = graph.Flows.Where(flow => flow != null &&
graph.EditorData.FlowEditorData.All(data => data == null || data.FlowId != flow.Id)).ToArray();
if (missingNodes.Length == 0 && missingFlows.Length == 0)
{
return false;
}
Undo.RecordObject(graph, "Initialize Frame Animation Graph Layout");
for (var index = 0; index < missingNodes.Length; index++)
{
var position = new Vector2((index % 4) * 300f, (index / 4) * 180f);
graph.EditorData.GetOrCreateNodeData(missingNodes[index].InternalId, position);
}
for (var index = 0; index < missingFlows.Length; index++)
{
graph.EditorData.GetOrCreateFlowData(missingFlows[index].Id,
FlowColors[(graph.Flows.ToList().IndexOf(missingFlows[index]) + FlowColors.Length) % FlowColors.Length]);
}
Dirty(graph);
return true;
}
public static Vector2 GetNodePosition(FrameAnimationGraph graph, AnimationNode node)
{
return graph?.EditorData.NodeEditorData.FirstOrDefault(data =>
data != null && data.NodeId == node?.InternalId)?.Position ?? Vector2.zero;
}
public static void SetNodePositions(
FrameAnimationGraph graph,
IReadOnlyDictionary<AnimationNode, Vector2> positions,
string undoName)
{
if (graph == null || positions == null || positions.Count == 0)
{
return;
}
Undo.RecordObject(graph, undoName);
foreach (var pair in positions)
{
graph.EditorData.GetOrCreateNodeData(pair.Key.InternalId, pair.Value).SetPosition(pair.Value);
}
Dirty(graph);
}
private static void Dirty(FrameAnimationGraph graph)
{
EditorUtility.SetDirty(graph);
}
}
internal static class FrameAnimationGraphLayoutService
{
public static IReadOnlyDictionary<AnimationNode, Vector2> Calculate(
FrameAnimationGraph graph,
IEnumerable<AnimationNode> scope)
{
var source = scope ?? (graph != null
? graph.Nodes
: (IEnumerable<AnimationNode>)Array.Empty<AnimationNode>());
var nodes = source.Where(node => node != null).Distinct()
.GroupBy(node => node.InternalId ?? string.Empty)
.Select(group => group.First()).ToArray();
if (graph == null || nodes.Length == 0)
{
return new Dictionary<AnimationNode, Vector2>();
}
var ids = new HashSet<string>(nodes.Select(node => node.InternalId));
var edges = graph.Edges.Where(edge => edge != null && ids.Contains(edge.FromNodeId) && ids.Contains(edge.ToNodeId)).ToArray();
var incoming = nodes.ToDictionary(node => node.InternalId, _ => 0);
foreach (var edge in edges)
{
incoming[edge.ToNodeId]++;
}
var ranks = nodes.ToDictionary(node => node.InternalId, _ => 0);
var queue = new Queue<AnimationNode>(nodes.Where(node => incoming[node.InternalId] == 0)
.OrderBy(node => node.InternalId, StringComparer.Ordinal));
var processed = new HashSet<string>();
while (queue.Count > 0)
{
var node = queue.Dequeue();
if (!processed.Add(node.InternalId)) continue;
foreach (var edge in edges.Where(edge => edge.FromNodeId == node.InternalId))
{
ranks[edge.ToNodeId] = Math.Max(ranks[edge.ToNodeId], ranks[node.InternalId] + 1);
incoming[edge.ToNodeId]--;
if (incoming[edge.ToNodeId] == 0)
{
queue.Enqueue(nodes.First(item => item.InternalId == edge.ToNodeId));
}
}
}
var oldPositions = nodes.ToDictionary(node => node,
node => FrameAnimationGraphMutationService.GetNodePosition(graph, node));
var originX = oldPositions.Values.Min(position => position.x);
var originY = oldPositions.Values.Min(position => position.y);
var result = new Dictionary<AnimationNode, Vector2>();
foreach (var group in nodes.GroupBy(node => ranks[node.InternalId]).OrderBy(group => group.Key))
{
var ordered = group.OrderBy(node => oldPositions[node].y)
.ThenBy(node => node.InternalId, StringComparer.Ordinal).ToArray();
for (var index = 0; index < ordered.Length; index++)
{
result[ordered[index]] = new Vector2(originX + group.Key * 300f, originY + index * 180f);
}
}
return result;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b397ed96c5b44df89626f866c32e03dd
@@ -0,0 +1,33 @@
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
[CustomEditor(typeof(FrameAnimationGraph))]
public sealed class FrameAnimationGraphEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
var graph = (FrameAnimationGraph)target;
EditorGUILayout.LabelField("FrameAnimationGraph", EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.TextField("ID", graph.Id);
EditorGUILayout.TextField("Display Name", graph.DisplayName);
EditorGUILayout.IntField("Clips", graph.Clips.Count);
EditorGUILayout.IntField("Nodes", graph.Nodes.Count);
EditorGUILayout.IntField("Edges", graph.Edges.Count);
EditorGUILayout.IntField("Flows", graph.Flows.Count);
EditorGUILayout.IntField("Import Sources", graph.ImportSources.Count);
EditorGUILayout.TextField("Default Playable", graph.Settings?.DefaultPlayableId ?? string.Empty);
}
EditorGUILayout.HelpBox(
"Graph 的资源、来源、重命名和删除操作只能在主工作台中执行。",
MessageType.Info);
if (GUILayout.Button("Open Frame Animation Graph Editor"))
{
FrameAnimationGraphEditorWindow.Open(graph);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 74f3b50c2650607499ea1c82a282f00f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,72 @@
.fa-preview {
position: relative;
overflow: hidden;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-color: rgb(25, 25, 25);
border-right-color: rgb(25, 25, 25);
border-top-color: rgb(25, 25, 25);
border-bottom-color: rgb(25, 25, 25);
}
.fa-preview__image {
position: absolute;
}
.fa-preview__empty {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
-unity-text-align: middle-center;
color: rgb(190, 190, 190);
}
.fa-node__preview {
height: 82px;
margin-top: 5px;
}
.fa-node--preview-current {
border-left-color: rgb(55, 174, 240);
border-right-color: rgb(55, 174, 240);
border-top-color: rgb(55, 174, 240);
border-bottom-color: rgb(55, 174, 240);
}
.fa-node--preview-passed {
border-left-color: rgb(83, 154, 113);
}
.fa-node--preview-upcoming {
opacity: 0.72;
}
.fa-clip-preview-panel {
flex-shrink: 0;
padding-left: 6px;
padding-right: 6px;
padding-top: 4px;
padding-bottom: 5px;
border-bottom-width: 1px;
border-bottom-color: rgb(28, 28, 28);
}
.fa-clip-preview-panel .fa-preview {
height: 190px;
margin-bottom: 4px;
}
.fa-preview-controls {
flex-direction: row;
align-items: center;
}
.fa-preview-status {
font-size: 10px;
color: rgb(185, 185, 185);
margin-top: 2px;
}
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: a38dd39c699e42348667cc1264ea9076
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: df89f5fa1de04df448957109886651b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,740 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace AibisDream.FrameAnimation.Editor
{
internal sealed class FrameAnimationClipNodeView : Node
{
private readonly Label subtitle;
private readonly Label behavior;
private readonly Label flowBadge;
private readonly Label issueBadge;
private readonly FrameAnimationPreviewElement preview;
private readonly Action<FrameAnimationClipNodeView> selectedCallback;
public AnimationNode Data { get; }
public Port InputPort { get; }
public Port OutputPort { get; }
internal FrameAnimationClipNodeView(
AnimationNode data,
Action<FrameAnimationClipNodeView> selectedCallback)
{
Data = data;
this.selectedCallback = selectedCallback;
viewDataKey = data.InternalId;
AddToClassList("fa-node");
style.minWidth = 245f;
style.maxWidth = 285f;
InputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi,
typeof(AnimationNode));
InputPort.portName = "In";
inputContainer.Add(InputPort);
OutputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single,
typeof(AnimationNode));
OutputPort.portName = "Next";
outputContainer.Add(OutputPort);
subtitle = new Label();
behavior = new Label();
flowBadge = new Label();
issueBadge = new Label();
issueBadge.style.unityFontStyleAndWeight = FontStyle.Bold;
extensionContainer.Add(subtitle);
extensionContainer.Add(behavior);
extensionContainer.Add(flowBadge);
extensionContainer.Add(issueBadge);
preview = new FrameAnimationPreviewElement();
preview.AddToClassList("fa-node__preview");
extensionContainer.Add(preview);
RefreshExpandedState();
}
public override void OnSelected()
{
base.OnSelected();
selectedCallback?.Invoke(this);
}
internal void Refresh(
FrameAnimationGraph graph,
IReadOnlyList<FrameAnimationEditorIssue> issues,
IReadOnlyList<AnimationFlow> flows,
IReadOnlyCollection<string> entryFlowIds)
{
var clip = graph.Clips.FirstOrDefault(item => item != null && item.Id == Data.ClipId);
title = string.IsNullOrWhiteSpace(Data.DisplayName) ? Data.ClipId : Data.DisplayName;
subtitle.text = clip != null ? $"Clip: {Data.ClipId}" : $"Clip: <Missing {Data.ClipId}>";
var speed = Data.SpeedOverride.HasValue
? $"Speed {Data.SpeedOverride.Value:0.###} (Node)"
: clip != null ? $"Speed {clip.Speed:0.###} (Clip)" : "Speed ?";
var end = Data.EndBehaviorOverride?.ToString() ?? "Continue / terminal fallback";
behavior.text = $"{speed} · {end}";
var used = flows.Select(flow => flow.Id).ToArray();
var entries = used.Where(entryFlowIds.Contains).ToArray();
flowBadge.text = used.Length == 0
? "Unused"
: "Flows: " + string.Join(", ", used.Take(3)) + (used.Length > 3 ? $" +{used.Length - 3}" : string.Empty) +
(entries.Length > 0 ? " [ENTRY]" : string.Empty);
var matching = issues.Where(issue => issue.Selection.Kind == FrameAnimationEditorSelectionKind.Node &&
ReferenceEquals(issue.Selection.Value, Data)).ToArray();
issueBadge.text = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
? "ERROR"
: matching.Length > 0 ? "WARNING" : string.Empty;
issueBadge.style.color = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
? new StyleColor(new Color(1f, 0.35f, 0.3f))
: new StyleColor(new Color(1f, 0.72f, 0.25f));
OutputPort.SetEnabled(!Data.EndBehaviorOverride.HasValue);
OutputPort.tooltip = Data.EndBehaviorOverride.HasValue
? "该节点已设置终点结束行为,请先清除覆盖。"
: "创建唯一的顺序后继";
}
internal void SetDimmed(bool dimmed)
{
style.opacity = dimmed ? 0.45f : 1f;
}
internal void SetPreview(
FrameAnimationPreviewDisplay display,
FrameAnimationPreviewBackground background)
{
preview.SetBackground(background);
preview.SetZoom(FrameAnimationPreviewZoomMode.Fit, 1f);
preview.SetDisplay(display.Sprite, display.IsEmpty);
preview.style.opacity = display.FlowState == FrameAnimationFlowElementState.Upcoming
? 0.55f : display.FlowState == FrameAnimationFlowElementState.Passed ? 0.82f : 1f;
EnableInClassList("fa-node--preview-current", display.FlowState == FrameAnimationFlowElementState.Current);
EnableInClassList("fa-node--preview-passed", display.FlowState == FrameAnimationFlowElementState.Passed);
EnableInClassList("fa-node--preview-upcoming", display.FlowState == FrameAnimationFlowElementState.Upcoming);
}
}
internal sealed class FrameAnimationEdgeView : Edge
{
private readonly Action<FrameAnimationEdgeView> selectedCallback;
private Color baseColor = Color.white;
public AnimationEdge Data { get; }
internal FrameAnimationEdgeView(AnimationEdge data, Action<FrameAnimationEdgeView> selectedCallback)
{
Data = data;
this.selectedCallback = selectedCallback;
viewDataKey = data.InternalId;
userData = data;
}
public override void OnSelected()
{
base.OnSelected();
selectedCallback?.Invoke(this);
}
internal void SetDimmed(bool dimmed)
{
style.opacity = dimmed ? 0.30f : 1f;
}
internal void Refresh(IReadOnlyList<FrameAnimationEditorIssue> issues)
{
var matching = (issues ?? Array.Empty<FrameAnimationEditorIssue>()).Where(issue =>
issue.Selection.Kind == FrameAnimationEditorSelectionKind.Edge &&
ReferenceEquals(issue.Selection.Value, Data)).ToArray();
var color = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
? new Color(1f, 0.3f, 0.25f)
: matching.Length > 0 ? new Color(1f, 0.72f, 0.2f) : Color.white;
baseColor = color;
edgeControl.inputColor = color;
edgeControl.outputColor = color;
tooltip = matching.Length == 0
? $"{Data.ExitName} / {Data.Condition}"
: string.Join("\n", matching.Select(issue => $"[{issue.Code}] {issue.Message}"));
}
internal void SetPreviewState(FrameAnimationFlowElementState state)
{
if (state == FrameAnimationFlowElementState.Current)
{
edgeControl.inputColor = new Color(0.25f, 0.75f, 1f);
edgeControl.outputColor = new Color(0.25f, 0.75f, 1f);
}
else if (state == FrameAnimationFlowElementState.Passed)
{
edgeControl.inputColor = new Color(0.35f, 0.68f, 0.46f);
edgeControl.outputColor = new Color(0.35f, 0.68f, 0.46f);
}
else if (state == FrameAnimationFlowElementState.Upcoming)
{
edgeControl.inputColor = new Color(0.55f, 0.55f, 0.55f);
edgeControl.outputColor = new Color(0.55f, 0.55f, 0.55f);
}
else
{
edgeControl.inputColor = baseColor;
edgeControl.outputColor = baseColor;
}
}
}
internal sealed class FrameAnimationClipSearchProvider : ScriptableObject, ISearchWindowProvider
{
private FrameAnimationGraph graph;
private Action<FrameClip> selected;
internal void Configure(FrameAnimationGraph value, Action<FrameClip> callback)
{
graph = value;
selected = callback;
}
public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
{
var entries = new List<SearchTreeEntry>
{
new SearchTreeGroupEntry(new GUIContent("Create Clip Node"), 0)
};
foreach (var clip in graph.Clips.Where(clip => clip != null)
.OrderBy(clip => clip.DisplayName, StringComparer.Ordinal)
.ThenBy(clip => clip.Id, StringComparer.Ordinal))
{
entries.Add(new SearchTreeEntry(new GUIContent($"{clip.DisplayName} ({clip.Id})"))
{
level = 1,
userData = clip
});
}
return entries;
}
public bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context)
{
if (entry.userData is FrameClip clip)
{
selected?.Invoke(clip);
return true;
}
return false;
}
}
internal sealed class FrameAnimationGraphView : GraphView
{
internal const string ClipDragKey = "AibisDream.FrameAnimation.ClipDrag";
private readonly Dictionary<string, FrameAnimationClipNodeView> nodeViews =
new Dictionary<string, FrameAnimationClipNodeView>();
private readonly Dictionary<string, FrameAnimationEdgeView> edgeViews =
new Dictionary<string, FrameAnimationEdgeView>();
private FrameAnimationGraph graph;
private IReadOnlyList<FrameAnimationEditorIssue> issues = Array.Empty<FrameAnimationEditorIssue>();
private bool rebuilding;
private Vector2 contextPosition;
private string focusedFlowId = string.Empty;
internal event Action<FrameAnimationEditorSelection> SelectionRequested;
internal event Action<IReadOnlyList<AnimationNode>> MultiSelectionChanged;
internal event Action<IReadOnlyList<FrameAnimationEditorSelection>> SelectionSetChanged;
internal event Action<AnimationNode> CreateFlowRequested;
internal event Action<AnimationNode> NodeCreated;
internal event Action ExitFocusRequested;
internal event Action GraphChanged;
internal event Action<string> NotificationRequested;
internal event Action<Vector3, float> WorkspaceViewTransformChanged;
internal FrameAnimationGraphView()
{
style.flexGrow = 1f;
focusable = true;
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
this.AddManipulator(new ContentDragger());
this.AddManipulator(new SelectionDragger());
this.AddManipulator(new RectangleSelector());
var grid = new GridBackground();
Insert(0, grid);
grid.StretchToParentSize();
graphViewChanged = OnGraphViewChanged;
viewTransformChanged += _ =>
WorkspaceViewTransformChanged?.Invoke(viewTransform.position, viewTransform.scale.x);
RegisterCallback<MouseUpEvent>(_ => NotifySelectionSummary());
RegisterCallback<DragUpdatedEvent>(OnDragUpdated);
RegisterCallback<DragPerformEvent>(OnDragPerform);
RegisterCallback<KeyDownEvent>(OnKeyDown);
}
internal void Bind(
FrameAnimationGraph value,
IReadOnlyList<FrameAnimationEditorIssue> validationIssues,
string focusedFlowId)
{
graph = value;
issues = validationIssues ?? Array.Empty<FrameAnimationEditorIssue>();
var selectedIds = selection.OfType<FrameAnimationClipNodeView>()
.Select(view => view.Data.InternalId).ToArray();
var selectedEdgeIds = selection.OfType<FrameAnimationEdgeView>()
.Select(view => view.Data.InternalId).ToArray();
rebuilding = true;
DeleteElements(graphElements.ToList());
nodeViews.Clear();
edgeViews.Clear();
if (graph != null)
{
var topology = new FrameAnimationGraphTopology(graph);
var entries = new HashSet<string>(graph.Flows.Where(flow => flow != null)
.Select(flow => flow.EntryNodeId));
foreach (var node in graph.Nodes.Where(node => node != null))
{
var view = new FrameAnimationClipNodeView(node, OnNodeSelected);
view.SetPosition(new Rect(FrameAnimationGraphMutationService.GetNodePosition(graph, node),
new Vector2(255f, 150f)));
view.Refresh(graph, issues, topology.FindFlowsUsingNode(node.InternalId), entries);
nodeViews[node.InternalId] = view;
AddElement(view);
}
foreach (var edge in graph.Edges.Where(edge => edge != null))
{
if (!nodeViews.TryGetValue(edge.FromNodeId ?? string.Empty, out var from) ||
!nodeViews.TryGetValue(edge.ToNodeId ?? string.Empty, out var to))
{
continue;
}
var view = new FrameAnimationEdgeView(edge, OnEdgeSelected)
{
output = from.OutputPort,
input = to.InputPort
};
view.output.Connect(view);
view.input.Connect(view);
view.Refresh(issues);
edgeViews[edge.InternalId] = view;
AddElement(view);
}
foreach (var id in selectedIds.Where(nodeViews.ContainsKey))
{
AddToSelection(nodeViews[id]);
}
foreach (var id in selectedEdgeIds.Where(edgeViews.ContainsKey))
{
AddToSelection(edgeViews[id]);
}
}
rebuilding = false;
SetFocusedFlow(focusedFlowId);
}
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
if (evt.target == this || evt.target == contentViewContainer)
{
contextPosition = contentViewContainer.WorldToLocal(evt.mousePosition);
evt.menu.AppendAction("Create Clip Node", _ => OpenClipSearch(evt.mousePosition),
graph != null && graph.Clips.Any(clip => clip != null)
? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
}
if (selection.Count == 1 && selection[0] is FrameAnimationClipNodeView nodeView)
{
evt.menu.AppendAction("Create Flow From Node", _ => CreateFlowRequested?.Invoke(nodeView.Data));
}
base.BuildContextualMenu(evt);
}
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
var result = new List<Port>();
if (graph == null || !(startPort.node is FrameAnimationClipNodeView startNode))
{
return result;
}
ports.ForEach(port =>
{
if (!(port.node is FrameAnimationClipNodeView targetNode) || port.direction == startPort.direction ||
targetNode == startNode)
{
return;
}
var from = startPort.direction == Direction.Output ? startNode.Data : targetNode.Data;
var to = startPort.direction == Direction.Output ? targetNode.Data : startNode.Data;
if (FrameAnimationGraphMutationService.CanConnect(graph, from, to, out _))
{
result.Add(port);
}
});
return result;
}
internal void SetFocusedFlow(string flowId)
{
focusedFlowId = flowId ?? string.Empty;
if (graph == null || string.IsNullOrEmpty(flowId))
{
foreach (var view in nodeViews.Values) view.SetDimmed(false);
foreach (var view in edgeViews.Values) view.SetDimmed(false);
return;
}
var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == flowId);
var topology = new FrameAnimationGraphTopology(graph);
var reachable = flow != null
? topology.GetReachable(flow.EntryNodeId)
: new FrameAnimationReachability(Array.Empty<AnimationNode>(), Array.Empty<AnimationEdge>());
var nodeIds = new HashSet<string>(reachable.Nodes.Select(node => node.InternalId));
var edgeIds = new HashSet<string>(reachable.Edges.Select(edge => edge.InternalId));
foreach (var pair in nodeViews) pair.Value.SetDimmed(!nodeIds.Contains(pair.Key));
foreach (var pair in edgeViews) pair.Value.SetDimmed(!edgeIds.Contains(pair.Key));
}
internal void SelectAndFrame(FrameAnimationEditorSelection target)
{
GraphElement element = target.Value switch
{
AnimationNode node when nodeViews.TryGetValue(node.InternalId, out var nodeView) => nodeView,
AnimationEdge edge when edgeViews.TryGetValue(edge.InternalId, out var edgeView) => edgeView,
_ => null
};
if (element == null) return;
ClearSelection();
AddToSelection(element);
FrameSelection();
}
internal IReadOnlyList<AnimationNode> SelectedNodes =>
selection.OfType<FrameAnimationClipNodeView>().Select(view => view.Data).ToArray();
internal IReadOnlyList<AnimationNode> VisibleNodes => nodeViews.Values
.Where(view => view.worldBound.Overlaps(worldBound))
.Select(view => view.Data)
.ToArray();
internal void ApplyPreview(FrameAnimationPreviewCoordinator coordinator)
{
if (coordinator == null) return;
foreach (var view in nodeViews.Values)
{
view.SetPreview(coordinator.GetNodeDisplay(view.Data), coordinator.Background);
}
foreach (var view in edgeViews.Values)
{
view.SetPreviewState(coordinator.GetEdgeState(view.Data));
}
}
internal void RestoreViewTransform(Vector3 position, float scale)
{
var safeScale = float.IsNaN(scale) || float.IsInfinity(scale)
? 1f : Mathf.Clamp(scale, ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
if (float.IsNaN(position.x) || float.IsInfinity(position.x) ||
float.IsNaN(position.y) || float.IsInfinity(position.y))
{
position = Vector3.zero;
}
UpdateViewTransform(position, new Vector3(safeScale, safeScale, 1f));
}
internal void RequestDeleteNodes(IEnumerable<AnimationNode> nodes)
{
var items = (nodes ?? Array.Empty<AnimationNode>()).Where(node => node != null).Distinct().ToArray();
if (graph == null || items.Length == 0)
{
return;
}
var ids = new HashSet<string>(items.Select(node => node.InternalId));
var entryFlows = graph.Flows.Where(flow => flow != null && ids.Contains(flow.EntryNodeId)).ToArray();
var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeNodeRemoval(graph, items);
if (!EditorUtility.DisplayDialog(
"Delete Frame Animation Nodes",
BuildRemovalMessage("Node", items.Length, impact, entryFlows),
entryFlows.Length > 0 ? "Delete Nodes + Flows" : "Delete",
"Cancel"))
{
return;
}
if (!FrameAnimationGraphMutationService.RemoveNodes(graph, items, entryFlows.Length > 0, out var error))
{
NotificationRequested?.Invoke(error);
return;
}
GraphChanged?.Invoke();
}
internal void RequestDeleteEdges(IEnumerable<AnimationEdge> edges)
{
var items = (edges ?? Array.Empty<AnimationEdge>()).Where(edge => edge != null).Distinct().ToArray();
if (graph == null || items.Length == 0)
{
return;
}
var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeEdgeRemoval(graph, items);
if (impact.Flows.Count > 0 && !EditorUtility.DisplayDialog(
"Disconnect Frame Animation Nodes",
BuildRemovalMessage("Edge", items.Length, impact, Array.Empty<AnimationFlow>()),
"Disconnect",
"Cancel"))
{
return;
}
FrameAnimationGraphMutationService.RemoveEdges(graph, items);
GraphChanged?.Invoke();
}
internal void FrameCurrentFlow(string flowId)
{
if (graph == null || string.IsNullOrEmpty(flowId)) return;
var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == flowId);
if (flow == null) return;
var ids = new HashSet<string>(new FrameAnimationGraphTopology(graph)
.GetReachable(flow.EntryNodeId).Nodes.Select(node => node.InternalId));
ClearSelection();
foreach (var pair in nodeViews.Where(pair => ids.Contains(pair.Key))) AddToSelection(pair.Value);
FrameSelection();
}
internal void ApplyCalculatedPositions(IReadOnlyDictionary<AnimationNode, Vector2> positions)
{
foreach (var pair in positions)
{
if (nodeViews.TryGetValue(pair.Key.InternalId, out var view))
{
var rect = view.GetPosition();
rect.position = pair.Value;
view.SetPosition(rect);
}
}
}
private void OpenClipSearch(Vector2 panelPosition)
{
var provider = ScriptableObject.CreateInstance<FrameAnimationClipSearchProvider>();
provider.Configure(graph, clip => CreateNode(clip, contextPosition));
SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(panelPosition)), provider);
}
internal void CreateNode(FrameClip clip, Vector2 position)
{
if (FrameAnimationGraphMutationService.CreateNode(graph, clip, position, out var node, out var error))
{
NodeCreated?.Invoke(node);
GraphChanged?.Invoke();
SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node));
}
else
{
NotificationRequested?.Invoke(error);
}
}
private GraphViewChange OnGraphViewChanged(GraphViewChange change)
{
if (rebuilding || graph == null)
{
return change;
}
if (change.edgesToCreate != null)
{
foreach (var visual in change.edgesToCreate.ToArray())
{
var error = string.Empty;
AnimationEdge data = null;
var from = visual.output?.node as FrameAnimationClipNodeView;
var to = visual.input?.node as FrameAnimationClipNodeView;
if (from == null || to == null ||
!FrameAnimationGraphMutationService.TryConnect(
graph, from.Data, to.Data, out data, out error))
{
change.edgesToCreate.Remove(visual);
NotificationRequested?.Invoke(string.IsNullOrEmpty(error) ? "无法创建连接。" : error);
continue;
}
visual.userData = data;
}
GraphChanged?.Invoke();
}
if (change.elementsToRemove != null)
{
ProcessRemovals(change);
}
if (change.movedElements != null)
{
var positions = change.movedElements.OfType<FrameAnimationClipNodeView>()
.ToDictionary(view => view.Data, view => view.GetPosition().position);
if (positions.Count > 0)
{
FrameAnimationGraphMutationService.SetNodePositions(graph, positions,
"Move Frame Animation Nodes");
GraphChanged?.Invoke();
}
}
return change;
}
private void ProcessRemovals(GraphViewChange change)
{
var nodeViewsToRemove = change.elementsToRemove.OfType<FrameAnimationClipNodeView>().ToArray();
var removedNodeIds = new HashSet<string>(nodeViewsToRemove.Select(view => view.Data.InternalId));
if (nodeViewsToRemove.Length > 0)
{
var nodes = nodeViewsToRemove.Select(view => view.Data).ToArray();
var explicitlySelectedEdges = change.elementsToRemove.OfType<Edge>()
.Select(edge => edge.userData as AnimationEdge)
.Where(edge => edge != null).Distinct().ToArray();
var entryFlows = graph.Flows.Where(flow => flow != null && removedNodeIds.Contains(flow.EntryNodeId)).ToArray();
var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeRemoval(
graph, nodes, explicitlySelectedEdges);
var message = BuildRemovalMessage("Node", nodes.Length, impact, entryFlows);
if (explicitlySelectedEdges.Length > 0)
{
message += $"\n\n同时删除选中的 Edge{explicitlySelectedEdges.Length}";
}
var confirmed = EditorUtility.DisplayDialog("Delete Frame Animation Nodes", message,
entryFlows.Length > 0 ? "Delete Nodes + Flows" : "Delete", "Cancel");
var removed = false;
var error = string.Empty;
if (confirmed)
{
removed = FrameAnimationGraphMutationService.RemoveNodesAndEdges(
graph, nodes, explicitlySelectedEdges, entryFlows.Length > 0, out error);
}
if (!removed)
{
change.elementsToRemove.Clear();
if (!string.IsNullOrEmpty(error)) NotificationRequested?.Invoke(error);
}
GraphChanged?.Invoke();
return;
}
var remainingEdges = change.elementsToRemove.OfType<Edge>()
.Where(edge => !IsIncidentTo(edge, removedNodeIds))
.Select(edge => edge.userData as AnimationEdge).Where(edge => edge != null).Distinct().ToArray();
if (remainingEdges.Length > 0)
{
var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeEdgeRemoval(graph, remainingEdges);
if (impact.Flows.Count > 0 && !EditorUtility.DisplayDialog("Disconnect Frame Animation Nodes",
BuildRemovalMessage("Edge", remainingEdges.Length, impact, Array.Empty<AnimationFlow>()),
"Disconnect", "Cancel"))
{
foreach (var edgeView in change.elementsToRemove.OfType<Edge>()
.Where(view => remainingEdges.Contains(view.userData as AnimationEdge)).ToArray())
{
change.elementsToRemove.Remove(edgeView);
}
}
else
{
FrameAnimationGraphMutationService.RemoveEdges(graph, remainingEdges);
}
}
GraphChanged?.Invoke();
}
private static bool IsIncidentTo(Edge edge, IReadOnlyCollection<string> nodeIds)
{
var from = edge?.output?.node as FrameAnimationClipNodeView;
var to = edge?.input?.node as FrameAnimationClipNodeView;
return from != null && nodeIds.Contains(from.Data.InternalId) ||
to != null && nodeIds.Contains(to.Data.InternalId);
}
private static string BuildRemovalMessage(
string kind,
int count,
FrameAnimationGraphImpact impact,
IReadOnlyCollection<AnimationFlow> entryFlows)
{
var message = $"将删除 {count} 个 {kind}。";
if (impact.Flows.Count > 0)
{
message += $"\n\n受影响 Flow{string.Join(", ", impact.Flows.Select(flow => flow.Id))}" +
$"\n将失去可达关系的 Node{impact.LostNodeCount}";
}
if (entryFlows.Count > 0)
{
message += $"\n\n入口 Flow 将同时删除:{string.Join(", ", entryFlows.Select(flow => flow.Id))}";
}
return message;
}
private void OnNodeSelected(FrameAnimationClipNodeView view)
{
if (rebuilding)
{
return;
}
SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, view.Data));
NotifySelectionSummary();
}
private void OnEdgeSelected(FrameAnimationEdgeView view)
{
if (rebuilding)
{
return;
}
SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Edge, view.Data));
NotifySelectionSummary();
}
private void NotifySelectionSummary()
{
if (rebuilding)
{
return;
}
MultiSelectionChanged?.Invoke(SelectedNodes);
var selected = selection.Select(item => item switch
{
FrameAnimationClipNodeView nodeView => new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Node, nodeView.Data),
FrameAnimationEdgeView edgeView => new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Edge, edgeView.Data),
_ => default
})
.Where(item => item.Kind != FrameAnimationEditorSelectionKind.None)
.ToArray();
SelectionSetChanged?.Invoke(selected);
}
private void OnKeyDown(KeyDownEvent evt)
{
if (!TryHandleFocusExitShortcut(evt.keyCode, evt.target is TextField))
{
return;
}
evt.StopImmediatePropagation();
}
internal bool TryHandleFocusExitShortcut(KeyCode keyCode, bool textInputActive)
{
if (keyCode != KeyCode.Escape || textInputActive || string.IsNullOrEmpty(focusedFlowId))
{
return false;
}
ExitFocusRequested?.Invoke();
return true;
}
private void OnDragUpdated(DragUpdatedEvent evt)
{
if (DragAndDrop.GetGenericData(ClipDragKey) is FrameClip clip && graph?.Clips.Contains(clip) == true)
{
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
evt.StopPropagation();
}
}
private void OnDragPerform(DragPerformEvent evt)
{
if (!(DragAndDrop.GetGenericData(ClipDragKey) is FrameClip clip) || graph?.Clips.Contains(clip) != true)
{
return;
}
DragAndDrop.AcceptDrag();
CreateNode(clip, contentViewContainer.WorldToLocal(evt.mousePosition));
DragAndDrop.SetGenericData(ClipDragKey, null);
evt.StopPropagation();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ffd56cf8d10f4ff9b33510a268d6b615
@@ -0,0 +1,347 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
internal enum FrameAnimationImportIssueCode
{
JsonMissing,
TextureMissing,
JsonInvalid,
FramesMissing,
FrameNameInvalid,
FrameNameDuplicate,
FrameDurationInvalid,
FrameRectInvalid,
TextureSizeMismatch,
TrimmedUnsupported,
RotatedUnsupported,
TagNameInvalid,
TagNameDuplicate,
TagRangeInvalid,
TagDirectionInvalid,
TagConflict,
PlayableIdConflict,
ImportSourceIdInvalid,
ImportSourceIdDuplicate,
ImportSourceReferenceInvalid,
ImportedClipOwnershipInvalid,
TextureOwnershipConflict,
SpriteMatchFailed,
TextureImporterInvalid,
TextureWriteFailed,
SourceChanged,
ImageNameMismatch,
NoTags,
ApplyFailed
}
internal enum FrameAnimationImportChangeKind
{
Added,
Updated,
Missing,
Unchanged,
Error
}
internal enum FrameAnimationSpriteChangeKind
{
Added,
Updated,
Retained,
Unchanged,
Error
}
internal sealed class FrameAnimationImportIssue
{
public FrameAnimationValidationSeverity Severity { get; }
public FrameAnimationImportIssueCode Code { get; }
public string SourceId { get; }
public string TargetId { get; }
public string Message { get; }
public FrameAnimationImportIssue(
FrameAnimationValidationSeverity severity,
FrameAnimationImportIssueCode code,
string sourceId,
string targetId,
string message)
{
Severity = severity;
Code = code;
SourceId = sourceId ?? string.Empty;
TargetId = targetId ?? string.Empty;
Message = message ?? string.Empty;
}
}
internal sealed class AsepriteSourceFrame
{
public string FrameName { get; }
public RectInt Rect { get; }
public int DurationMs { get; }
public bool Rotated { get; }
public bool Trimmed { get; }
public RectInt SpriteSourceSize { get; }
public Vector2Int SourceSize { get; }
public AsepriteSourceFrame(
string frameName,
RectInt rect,
int durationMs,
bool rotated,
bool trimmed,
RectInt spriteSourceSize,
Vector2Int sourceSize)
{
FrameName = frameName ?? string.Empty;
Rect = rect;
DurationMs = durationMs;
Rotated = rotated;
Trimmed = trimmed;
SpriteSourceSize = spriteSourceSize;
SourceSize = sourceSize;
}
}
internal sealed class AsepriteSourceTag
{
public string Name { get; }
public int From { get; }
public int To { get; }
public string Direction { get; }
public AsepriteSourceTag(string name, int from, int to, string direction)
{
Name = name ?? string.Empty;
From = from;
To = to;
Direction = direction ?? string.Empty;
}
}
internal sealed class AsepriteSpriteSlot
{
public RectInt Rect { get; }
public string CanonicalFrameName { get; }
public IReadOnlyList<int> SourceIndices { get; }
public IReadOnlyList<string> FrameNames { get; }
public AsepriteSpriteSlot(
RectInt rect,
string canonicalFrameName,
IReadOnlyList<int> sourceIndices,
IReadOnlyList<string> frameNames)
{
Rect = rect;
CanonicalFrameName = canonicalFrameName ?? string.Empty;
SourceIndices = sourceIndices ?? Array.Empty<int>();
FrameNames = frameNames ?? Array.Empty<string>();
}
}
internal sealed class AsepriteSourceDocument
{
public IReadOnlyList<AsepriteSourceFrame> Frames { get; }
public IReadOnlyList<AsepriteSpriteSlot> SpriteSlots { get; }
public IReadOnlyDictionary<int, AsepriteSpriteSlot> SpriteSlotBySourceIndex { get; }
public IReadOnlyList<AsepriteSourceTag> Tags { get; }
public Vector2Int TextureSize { get; }
public string ImageName { get; }
public AsepriteSourceDocument(
IReadOnlyList<AsepriteSourceFrame> frames,
IReadOnlyList<AsepriteSourceTag> tags,
Vector2Int textureSize,
string imageName)
{
Frames = frames ?? Array.Empty<AsepriteSourceFrame>();
BuildSpriteSlots(Frames, out var spriteSlots, out var slotBySourceIndex);
SpriteSlots = spriteSlots;
SpriteSlotBySourceIndex = slotBySourceIndex;
Tags = tags ?? Array.Empty<AsepriteSourceTag>();
TextureSize = textureSize;
ImageName = imageName ?? string.Empty;
}
private static void BuildSpriteSlots(
IReadOnlyList<AsepriteSourceFrame> frames,
out IReadOnlyList<AsepriteSpriteSlot> spriteSlots,
out IReadOnlyDictionary<int, AsepriteSpriteSlot> slotBySourceIndex)
{
var builders = new List<SpriteSlotBuilder>();
var buildersByRect = new Dictionary<RectInt, SpriteSlotBuilder>();
for (var sourceIndex = 0; sourceIndex < frames.Count; sourceIndex++)
{
var frame = frames[sourceIndex];
if (!buildersByRect.TryGetValue(frame.Rect, out var builder))
{
builder = new SpriteSlotBuilder(frame.Rect, frame.FrameName);
buildersByRect.Add(frame.Rect, builder);
builders.Add(builder);
}
builder.SourceIndices.Add(sourceIndex);
builder.FrameNames.Add(frame.FrameName);
}
var slots = builders.Select(builder => builder.Build()).ToArray();
var index = new Dictionary<int, AsepriteSpriteSlot>();
foreach (var slot in slots)
{
foreach (var sourceIndex in slot.SourceIndices)
{
index[sourceIndex] = slot;
}
}
spriteSlots = slots;
slotBySourceIndex = index;
}
private sealed class SpriteSlotBuilder
{
public RectInt Rect { get; }
public string CanonicalFrameName { get; }
public List<int> SourceIndices { get; } = new List<int>();
public List<string> FrameNames { get; } = new List<string>();
public SpriteSlotBuilder(RectInt rect, string canonicalFrameName)
{
Rect = rect;
CanonicalFrameName = canonicalFrameName ?? string.Empty;
}
public AsepriteSpriteSlot Build()
{
return new AsepriteSpriteSlot(
Rect,
CanonicalFrameName,
SourceIndices.ToArray(),
FrameNames.ToArray());
}
}
}
internal sealed class FrameAnimationClipDiff
{
public FrameAnimationImportChangeKind Kind { get; }
public string SourceTagName { get; }
public FrameClip Clip { get; }
public IReadOnlyList<int> SourceIndices { get; }
public string Summary { get; }
public FrameAnimationClipDiff(
FrameAnimationImportChangeKind kind,
string sourceTagName,
FrameClip clip,
IReadOnlyList<int> sourceIndices,
string summary)
{
Kind = kind;
SourceTagName = sourceTagName ?? string.Empty;
Clip = clip;
SourceIndices = sourceIndices ?? Array.Empty<int>();
Summary = summary ?? string.Empty;
}
}
internal sealed class FrameAnimationSpriteDiff
{
public FrameAnimationSpriteChangeKind Kind { get; }
public string FrameName { get; }
public IReadOnlyList<string> AliasFrameNames { get; }
public int SourceFrameCount => AliasFrameNames.Count;
public Rect Rect { get; }
public string Summary { get; }
public FrameAnimationSpriteDiff(
FrameAnimationSpriteChangeKind kind,
string frameName,
Rect rect,
string summary,
IReadOnlyList<string> aliasFrameNames = null)
{
Kind = kind;
FrameName = frameName ?? string.Empty;
AliasFrameNames = aliasFrameNames ?? Array.Empty<string>();
Rect = rect;
Summary = summary ?? string.Empty;
}
}
internal sealed class FrameAnimationImportSourcePreview
{
private readonly List<FrameAnimationImportIssue> issues = new List<FrameAnimationImportIssue>();
private readonly List<FrameAnimationClipDiff> clipDiffs = new List<FrameAnimationClipDiff>();
private readonly List<FrameAnimationSpriteDiff> spriteDiffs = new List<FrameAnimationSpriteDiff>();
public FrameAnimationImportSource Source { get; }
public AsepriteSourceDocument Document { get; internal set; }
public FrameAnimationSpritePlan SpritePlan { get; internal set; }
public string CurrentHash { get; internal set; } = string.Empty;
public IReadOnlyList<FrameAnimationImportIssue> Issues => issues;
public IReadOnlyList<FrameAnimationClipDiff> ClipDiffs => clipDiffs;
public IReadOnlyList<FrameAnimationSpriteDiff> SpriteDiffs => spriteDiffs;
public bool HasErrors => issues.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error);
public bool HasSpriteChanges => spriteDiffs.Any(diff =>
diff.Kind == FrameAnimationSpriteChangeKind.Added ||
diff.Kind == FrameAnimationSpriteChangeKind.Updated);
public FrameAnimationImportSourcePreview(FrameAnimationImportSource source)
{
Source = source;
}
public void AddIssue(FrameAnimationImportIssue issue)
{
if (issue != null)
{
issues.Add(issue);
}
}
public void AddClipDiff(FrameAnimationClipDiff diff)
{
if (diff != null)
{
clipDiffs.Add(diff);
}
}
public void AddSpriteDiff(FrameAnimationSpriteDiff diff)
{
if (diff != null)
{
spriteDiffs.Add(diff);
}
}
}
internal sealed class FrameAnimationImportPreview
{
private readonly List<FrameAnimationImportSourcePreview> sources =
new List<FrameAnimationImportSourcePreview>();
public FrameAnimationGraph Graph { get; }
public IReadOnlyList<FrameAnimationImportSourcePreview> Sources => sources;
public bool HasErrors => sources.Any(source => source.HasErrors);
public bool HasSpriteChanges => sources.Any(source => source.HasSpriteChanges);
public FrameAnimationImportPreview(FrameAnimationGraph graph)
{
Graph = graph;
}
public void AddSource(FrameAnimationImportSourcePreview preview)
{
if (preview != null)
{
sources.Add(preview);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c9d29e9107c2ec549916dfb45e638943
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,335 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
public static class FrameAnimationImportSampleBuilder
{
private const string SampleFolder = "Assets/GameContent/Test/FrameAnimation/Import";
private const string ReadOnlyTexturePath = SampleFolder + "/中文预切图.png";
private const string WritableTexturePath = SampleFolder + "/中文自动切图.png";
private const string ObjectJsonPath = SampleFolder + "/中文Object.json";
private const string ArrayJsonPath = SampleFolder + "/中文Array.json";
private const string GraphPath = SampleFolder + "/ImportSampleGraph.asset";
private const string IdleNodeId = "40000000000000000000000000000001";
private const string BlinkNodeId = "40000000000000000000000000000002";
private const string SharedNodeId = "40000000000000000000000000000003";
private const string IdleEdgeId = "40000000000000000000000000000011";
private const string BlinkEdgeId = "40000000000000000000000000000012";
private const string IdleFlowId = "SampleIdleToSharedFlow";
private const string BlinkFlowId = "SampleBlinkToSharedFlow";
[MenuItem("Tools/Frame Animation/Rebuild Import Sample")]
private static void BuildFromMenu()
{
BuildFromCommandLine();
}
public static void BuildFromCommandLine()
{
EnsureFolder(SampleFolder);
if (AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(GraphPath) != null)
{
AssetDatabase.DeleteAsset(GraphPath);
}
WriteTexture(ReadOnlyTexturePath);
WriteTexture(WritableTexturePath);
WriteTextAsset(ObjectJsonPath, CreateObjectJson("中文预切图.png", "待机", "眨眼"));
WriteTextAsset(ArrayJsonPath, CreateArrayJson("中文自动切图.png", "转身", "惊讶"));
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
var readOnlyTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(ReadOnlyTexturePath);
var objectJson = AssetDatabase.LoadAssetAtPath<TextAsset>(ObjectJsonPath);
if (!AsepriteJsonParser.TryParse(objectJson.text, "sample-slicing", out var document, out var parseIssue))
{
throw new InvalidOperationException(parseIssue.Message);
}
var slicingSource = new FrameAnimationImportSource(
"Prepare Read Only Texture", readOnlyTexture, objectJson, new Vector2(0.5f, 0.5f), true);
var slicingPreview = new FrameAnimationImportSourcePreview(slicingSource) { Document = document };
var slicingPlan = FrameAnimationSpriteUtility.BuildPlan(slicingSource, document, slicingPreview);
if (slicingPreview.HasErrors)
{
throw new InvalidOperationException(string.Join("\n", System.Linq.Enumerable.Select(
slicingPreview.Issues, issue => issue.Message)));
}
FrameAnimationSpriteUtility.ApplyPlan(slicingPlan);
var graph = ScriptableObject.CreateInstance<FrameAnimationGraph>();
graph.Configure(
"ImportSampleGraph",
"Aseprite 导入与稳定刷新样例",
Array.Empty<FrameClip>(),
Array.Empty<AnimationNode>(),
Array.Empty<AnimationEdge>(),
Array.Empty<AnimationFlow>(),
string.Empty);
graph.AddImportSource(new FrameAnimationImportSource(
"Object / 只读切图",
AssetDatabase.LoadAssetAtPath<Texture2D>(ReadOnlyTexturePath),
AssetDatabase.LoadAssetAtPath<TextAsset>(ObjectJsonPath),
new Vector2(0.5f, 0.5f),
false));
graph.AddImportSource(new FrameAnimationImportSource(
"Array / 自动切图",
AssetDatabase.LoadAssetAtPath<Texture2D>(WritableTexturePath),
AssetDatabase.LoadAssetAtPath<TextAsset>(ArrayJsonPath),
new Vector2(0.5f, 0.5f),
true));
AssetDatabase.CreateAsset(graph, GraphPath);
AssetDatabase.SaveAssets();
var preview = FrameAnimationImportService.PreviewAll(graph);
if (!FrameAnimationImportService.Apply(preview, true, out var error))
{
throw new InvalidOperationException(error + "\n" + string.Join("\n",
System.Linq.Enumerable.SelectMany(preview.Sources,
source => System.Linq.Enumerable.Select(source.Issues, issue => issue.Message))));
}
ExtendForWorkspaceSample(graph);
EditorUtility.SetDirty(graph);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Selection.activeObject = graph;
Debug.Log($"Frame Animation import sample rebuilt: {GraphPath}");
}
[MenuItem("Tools/Frame Animation/Upgrade Phase 4 Sample (Non-Destructive)")]
public static void UpgradePhase4Sample()
{
var graph = AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(GraphPath);
if (graph == null)
{
Debug.LogError($"Frame Animation sample Graph not found: {GraphPath}");
return;
}
var idleClip = graph.Clips.FirstOrDefault(clip => clip?.ImportInfo?.SourceTagName == "待机");
var blinkClip = graph.Clips.FirstOrDefault(clip => clip?.ImportInfo?.SourceTagName == "眨眼");
var sharedClip = AssetDatabase.LoadAssetAtPath<FrameClip>(
"Assets/GameContent/Test/FrameAnimation/Idle.asset") ??
graph.Clips.FirstOrDefault(clip => clip != null && !clip.IsImported);
if (idleClip == null || blinkClip == null || sharedClip == null || !graph.Clips.Contains(sharedClip))
{
Debug.LogError("Phase 4 sample upgrade requires 待机、眨眼 and a Manual Clip already referenced by the Graph.");
return;
}
Undo.RecordObject(graph, "Upgrade Frame Animation Phase 4 Sample");
var idleNode = GetOrAddNode(graph, IdleNodeId, idleClip, "待机 Node");
var blinkNode = GetOrAddNode(graph, BlinkNodeId, blinkClip, "眨眼 Node");
var sharedNode = graph.Nodes.FirstOrDefault(node => node != null && node.InternalId == SharedNodeId);
if (sharedNode == null)
{
sharedNode = new AnimationNode(sharedClip.Id, "Shared Idle Node",
FrameClipEndBehavior.Loop, internalId: SharedNodeId);
graph.AddNode(sharedNode);
}
GetOrAddEdge(graph, IdleEdgeId, idleNode, sharedNode);
GetOrAddEdge(graph, BlinkEdgeId, blinkNode, sharedNode);
GetOrAddFlow(graph, IdleFlowId, "待机到共享 Idle", idleNode,
new Color(0.24f, 0.65f, 1f));
GetOrAddFlow(graph, BlinkFlowId, "眨眼到共享 Idle", blinkNode,
new Color(1f, 0.62f, 0.24f));
graph.EditorData.GetOrCreateNodeData(IdleNodeId, new Vector2(0f, 0f));
graph.EditorData.GetOrCreateNodeData(BlinkNodeId, new Vector2(0f, 220f));
graph.EditorData.GetOrCreateNodeData(SharedNodeId, new Vector2(360f, 110f));
EditorUtility.SetDirty(graph);
AssetDatabase.SaveAssets();
Selection.activeObject = graph;
Debug.Log("Frame Animation Phase 4 sample upgraded without rebuilding existing assets or sources.");
}
private static AnimationNode GetOrAddNode(
FrameAnimationGraph graph,
string internalId,
FrameClip clip,
string displayName)
{
var node = graph.Nodes.FirstOrDefault(item => item != null && item.InternalId == internalId);
if (node != null)
{
return node;
}
node = new AnimationNode(clip.Id, displayName, internalId: internalId);
graph.AddNode(node);
return node;
}
private static void GetOrAddEdge(
FrameAnimationGraph graph,
string internalId,
AnimationNode from,
AnimationNode to)
{
if (graph.Edges.Any(edge => edge != null && edge.InternalId == internalId))
{
return;
}
graph.AddEdge(new AnimationEdge(from.InternalId, to.InternalId, internalId));
}
private static void GetOrAddFlow(
FrameAnimationGraph graph,
string id,
string displayName,
AnimationNode entry,
Color color)
{
var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == id);
if (flow == null && FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id))
{
flow = new AnimationFlow(id, displayName, entry.InternalId);
graph.AddFlow(flow);
}
if (flow != null)
{
graph.EditorData.GetOrCreateFlowData(flow.Id, color);
}
}
private static void ExtendForWorkspaceSample(FrameAnimationGraph graph)
{
var imported = graph.Clips.First(clip => clip.ImportInfo?.SourceTagName == "待机");
var manual = ScriptableObject.CreateInstance<FrameClip>();
manual.name = "ManualWorkspaceSample";
manual.Configure(
"ManualWorkspaceSample",
"Graph 内 Manual 样例",
imported.Frames.Take(1).Select(frame => new FrameAnimationFrame(
frame.Sprite, frame.DurationMs, string.Empty, -1)),
1f,
FrameClipEndBehavior.HoldLastFrame);
AssetDatabase.AddObjectToAsset(manual, graph);
var clips = graph.Clips.Concat(new[] { manual }).ToList();
var external = AssetDatabase.LoadAssetAtPath<FrameClip>(
"Assets/GameContent/Test/FrameAnimation/Idle.asset");
if (external != null && clips.All(clip => clip.Id != external.Id))
{
clips.Add(external);
}
var importedNode = new AnimationNode(imported.Id, "Imported 待机");
var terminalClip = external != null && clips.Contains(external) ? external : manual;
var terminalNode = new AnimationNode(
terminalClip.Id,
"共享 Manual 终点",
endBehaviorOverride: FrameClipEndBehavior.Loop);
var edge = new AnimationEdge(importedNode.InternalId, terminalNode.InternalId);
var flow = new AnimationFlow(
"WorkspaceSampleFlow",
"工作台引用定位样例",
importedNode.InternalId);
graph.Configure(
graph.Id,
graph.DisplayName,
clips,
new[] { importedNode, terminalNode },
new[] { edge },
new[] { flow },
flow.Id);
}
private static JObject CreateFrame(string name, int x, int duration)
{
return new JObject
{
["filename"] = name,
["frame"] = new JObject { ["x"] = x, ["y"] = 0, ["w"] = 2, ["h"] = 2 },
["rotated"] = false,
["trimmed"] = false,
["spriteSourceSize"] = new JObject { ["x"] = 0, ["y"] = 0, ["w"] = 2, ["h"] = 2 },
["sourceSize"] = new JObject { ["w"] = 2, ["h"] = 2 },
["duration"] = duration
};
}
private static string CreateObjectJson(string imageName, string firstTag, string secondTag)
{
var root = CreateRoot(imageName, firstTag, secondTag);
root["frames"] = new JObject
{
["中文帧_00"] = WithoutFilename(CreateFrame("中文帧_00", 0, 160)),
["中文帧_01"] = WithoutFilename(CreateFrame("中文帧_01", 2, 90))
};
return root.ToString(Formatting.Indented);
}
private static string CreateArrayJson(string imageName, string firstTag, string secondTag)
{
var root = CreateRoot(imageName, firstTag, secondTag);
root["frames"] = new JArray(
CreateFrame("自动帧_00", 0, 140),
CreateFrame("自动帧_01", 2, 110));
return root.ToString(Formatting.Indented);
}
private static JObject CreateRoot(string imageName, string firstTag, string secondTag)
{
return new JObject
{
["meta"] = new JObject
{
["image"] = imageName,
["size"] = new JObject { ["w"] = 4, ["h"] = 2 },
["frameTags"] = new JArray(
new JObject
{
["name"] = firstTag, ["from"] = 0, ["to"] = 0, ["direction"] = "forward"
},
new JObject
{
["name"] = secondTag, ["from"] = 0, ["to"] = 1, ["direction"] = "pingpong"
})
}
};
}
private static JObject WithoutFilename(JObject frame)
{
frame.Remove("filename");
return frame;
}
private static void WriteTexture(string path)
{
var texture = new Texture2D(4, 2, TextureFormat.RGBA32, false);
texture.SetPixels(new[]
{
Color.cyan, Color.cyan, Color.magenta, Color.magenta,
Color.cyan, Color.cyan, Color.magenta, Color.magenta
});
texture.Apply();
File.WriteAllBytes(path, texture.EncodeToPNG());
UnityEngine.Object.DestroyImmediate(texture);
}
private static void WriteTextAsset(string path, string content)
{
File.WriteAllText(path, content, new UTF8Encoding(false));
}
private static void EnsureFolder(string path)
{
var segments = path.Split('/');
var current = segments[0];
for (var index = 1; index < segments.Length; index++)
{
var next = current + "/" + segments[index];
if (!AssetDatabase.IsValidFolder(next))
{
AssetDatabase.CreateFolder(current, segments[index]);
}
current = next;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 291df5abf0a29fd4ca1cb0b86eb24a9e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,612 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
internal static class FrameAnimationImportService
{
public static FrameAnimationImportPreview PreviewSource(
FrameAnimationGraph graph,
string sourceId)
{
var source = graph?.ImportSources.FirstOrDefault(item => item != null && item.InternalId == sourceId);
return PreviewInternal(graph, source != null ? new[] { source } : Array.Empty<FrameAnimationImportSource>());
}
public static FrameAnimationImportPreview PreviewAll(FrameAnimationGraph graph)
{
return PreviewInternal(
graph,
graph?.ImportSources.Where(source => source != null && source.IsEnabled).ToArray() ??
Array.Empty<FrameAnimationImportSource>());
}
public static bool RefreshSource(
FrameAnimationGraph graph,
string sourceId,
bool allowSpriteChanges,
out FrameAnimationImportPreview preview,
out string error)
{
preview = PreviewSource(graph, sourceId);
return Apply(preview, allowSpriteChanges, out error);
}
public static bool RefreshAll(
FrameAnimationGraph graph,
bool allowSpriteChanges,
out FrameAnimationImportPreview preview,
out string error)
{
preview = PreviewAll(graph);
return Apply(preview, allowSpriteChanges, out error);
}
public static bool Apply(
FrameAnimationImportPreview preview,
bool allowSpriteChanges,
out string error)
{
error = string.Empty;
if (preview == null || preview.Graph == null)
{
error = "导入预览或 Graph 为空。";
return false;
}
if (preview.HasErrors)
{
error = "导入预览包含阻断错误,未修改任何资产。";
return false;
}
if (preview.HasSpriteChanges && !allowSpriteChanges)
{
error = "导入需要修改 TextureImporter,但尚未获得确认。";
return false;
}
var graphPath = AssetDatabase.GetAssetPath(preview.Graph);
if (string.IsNullOrEmpty(graphPath))
{
error = "FrameAnimationGraph 必须先保存为资产才能创建 Imported Clip sub-asset。";
return false;
}
var snapshots = new List<FrameAnimationTextureSnapshot>();
Undo.IncrementCurrentGroup();
var undoGroup = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Refresh Frame Animation Imports");
try
{
Undo.RecordObject(preview.Graph, "Refresh Frame Animation Imports");
foreach (var sourcePreview in preview.Sources.Where(item =>
item.Source.ManageSpriteSlicing && item.HasSpriteChanges))
{
var snapshot = FrameAnimationSpriteUtility.CaptureSnapshot(sourcePreview.SpritePlan);
snapshots.Add(snapshot);
FrameAnimationSpriteUtility.ApplyPlan(sourcePreview.SpritePlan);
}
foreach (var sourcePreview in preview.Sources)
{
ApplySource(preview.Graph, sourcePreview);
sourcePreview.Source.SetLastSourceHash(
FrameAnimationSpriteUtility.ComputeSourceHash(sourcePreview.Source));
}
EditorUtility.SetDirty(preview.Graph);
AssetDatabase.SaveAssets();
Undo.CollapseUndoOperations(undoGroup);
return true;
}
catch (Exception exception)
{
try
{
Undo.RevertAllDownToGroup(undoGroup);
for (var index = snapshots.Count - 1; index >= 0; index--)
{
FrameAnimationSpriteUtility.RestoreSnapshot(snapshots[index]);
}
AssetDatabase.SaveAssets();
}
catch (Exception rollbackException)
{
Debug.LogError($"Frame Animation 导入回滚失败:{rollbackException}");
}
error = $"导入应用失败,已尝试回滚:{exception.Message}";
Debug.LogException(exception);
return false;
}
}
private static FrameAnimationImportPreview PreviewInternal(
FrameAnimationGraph graph,
IReadOnlyList<FrameAnimationImportSource> targets)
{
var preview = new FrameAnimationImportPreview(graph);
if (graph == null)
{
return preview;
}
var targetSet = new HashSet<FrameAnimationImportSource>(targets);
var targetPreviews = new Dictionary<FrameAnimationImportSource, FrameAnimationImportSourcePreview>();
foreach (var source in targets)
{
var sourcePreview = new FrameAnimationImportSourcePreview(source);
targetPreviews[source] = sourcePreview;
preview.AddSource(sourcePreview);
}
ValidateSourceIds(graph, targetPreviews);
var contextDocuments = ParseEnabledContext(graph, targetPreviews);
ValidateCrossSourceTags(graph, contextDocuments, targetPreviews);
ValidateTextureOwnership(graph, targetPreviews);
foreach (var pair in targetPreviews)
{
var source = pair.Key;
var sourcePreview = pair.Value;
if (!contextDocuments.TryGetValue(source, out var document))
{
continue;
}
sourcePreview.Document = document;
ValidateSource(graph, source, document, sourcePreview);
if (sourcePreview.HasErrors)
{
continue;
}
sourcePreview.SpritePlan = FrameAnimationSpriteUtility.BuildPlan(source, document, sourcePreview);
sourcePreview.CurrentHash = FrameAnimationSpriteUtility.ComputeSourceHash(source);
if (!string.IsNullOrEmpty(source.LastSourceHash) && source.LastSourceHash != sourcePreview.CurrentHash)
{
sourcePreview.AddIssue(new FrameAnimationImportIssue(
FrameAnimationValidationSeverity.Warning,
FrameAnimationImportIssueCode.SourceChanged,
source.InternalId,
source.InternalId,
"来源内容或切图设置自上次刷新后已发生变化。"));
}
if (!sourcePreview.HasErrors)
{
BuildClipDiffs(graph, source, document, sourcePreview);
}
}
return preview;
}
private static Dictionary<FrameAnimationImportSource, AsepriteSourceDocument> ParseEnabledContext(
FrameAnimationGraph graph,
IReadOnlyDictionary<FrameAnimationImportSource, FrameAnimationImportSourcePreview> targetPreviews)
{
var result = new Dictionary<FrameAnimationImportSource, AsepriteSourceDocument>();
foreach (var source in graph.ImportSources.Where(source => source != null && source.IsEnabled))
{
targetPreviews.TryGetValue(source, out var sourcePreview);
if (source.AsepriteJson == null)
{
sourcePreview?.AddIssue(Error(source, FrameAnimationImportIssueCode.JsonMissing, "ImportSource 缺少 Aseprite JSON。"));
continue;
}
if (!AsepriteJsonParser.TryParse(source.AsepriteJson.text, source.InternalId, out var document, out var issue))
{
sourcePreview?.AddIssue(issue);
continue;
}
result[source] = document;
}
return result;
}
private static void ValidateSourceIds(
FrameAnimationGraph graph,
IReadOnlyDictionary<FrameAnimationImportSource, FrameAnimationImportSourcePreview> targetPreviews)
{
foreach (var source in graph.ImportSources.Where(source => source != null))
{
if (!FrameAnimationValueUtility.IsValidInternalId(source.InternalId) &&
targetPreviews.TryGetValue(source, out var sourcePreview))
{
sourcePreview.AddIssue(Error(source, FrameAnimationImportIssueCode.ImportSourceIdInvalid,
"ImportSource internalId 必须是 N 格式 GUID。"));
}
}
foreach (var duplicate in graph.ImportSources
.Where(source => source != null)
.GroupBy(source => source.InternalId)
.Where(group => group.Count() > 1))
{
foreach (var source in duplicate)
{
if (targetPreviews.TryGetValue(source, out var sourcePreview))
{
sourcePreview.AddIssue(Error(source, FrameAnimationImportIssueCode.ImportSourceIdDuplicate,
$"ImportSource internalId '{source.InternalId}' 重复。"));
}
}
}
}
private static void ValidateSource(
FrameAnimationGraph graph,
FrameAnimationImportSource source,
AsepriteSourceDocument document,
FrameAnimationImportSourcePreview preview)
{
if (source.Texture == null)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.TextureMissing, "ImportSource 缺少 Texture。"));
return;
}
if (document.Frames.Count == 0)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.FramesMissing, "Aseprite JSON 的 frames 为空。"));
}
foreach (var duplicate in document.Frames.GroupBy(frame => frame.FrameName).Where(group => group.Count() > 1))
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.FrameNameDuplicate,
$"frameName '{duplicate.Key}' 在同一来源内重复。"));
}
for (var index = 0; index < document.Frames.Count; index++)
{
var frame = document.Frames[index];
if (string.IsNullOrWhiteSpace(frame.FrameName))
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.FrameNameInvalid,
$"SourceFrame[{index}] 的 frameName 为空。"));
}
if (frame.DurationMs <= 0)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.FrameDurationInvalid,
$"SourceFrame '{frame.FrameName}' 的 duration 必须大于 0。"));
}
if (frame.Rect.width <= 0 || frame.Rect.height <= 0 || frame.Rect.x < 0 || frame.Rect.y < 0 ||
frame.Rect.xMax > source.Texture.width || frame.Rect.yMax > source.Texture.height)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.FrameRectInvalid,
$"SourceFrame '{frame.FrameName}' 的 rect 越出 Texture 范围。"));
}
if (frame.Trimmed)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.TrimmedUnsupported,
$"SourceFrame '{frame.FrameName}' 使用了第一版不支持的 trimmed。"));
}
if (frame.Rotated)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.RotatedUnsupported,
$"SourceFrame '{frame.FrameName}' 使用了第一版不支持的 rotated。"));
}
}
if (document.TextureSize.x != source.Texture.width || document.TextureSize.y != source.Texture.height)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.TextureSizeMismatch,
$"meta.size {document.TextureSize.x}x{document.TextureSize.y} 与 Texture {source.Texture.width}x{source.Texture.height} 不一致。"));
}
var textureFileName = Path.GetFileName(AssetDatabase.GetAssetPath(source.Texture));
if (!string.IsNullOrEmpty(document.ImageName) &&
!string.Equals(document.ImageName, textureFileName, StringComparison.Ordinal))
{
preview.AddIssue(Warning(source, FrameAnimationImportIssueCode.ImageNameMismatch,
$"meta.image '{document.ImageName}' 与绑定 Texture '{textureFileName}' 不一致。"));
}
if (document.Tags.Count == 0)
{
preview.AddIssue(Warning(source, FrameAnimationImportIssueCode.NoTags,
"JSON 中没有 frameTags,此来源不会生成 Clip。"));
}
foreach (var duplicate in document.Tags.GroupBy(tag => tag.Name).Where(group => group.Count() > 1))
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.TagNameDuplicate,
$"Tag '{duplicate.Key}' 在同一来源内重复。"));
}
foreach (var tag in document.Tags)
{
if (string.IsNullOrWhiteSpace(tag.Name))
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.TagNameInvalid, "Tag 名不能为空。"));
continue;
}
if (!AsepriteJsonParser.TryExpandTag(tag, document.Frames.Count, source.InternalId, out _, out var issue))
{
preview.AddIssue(issue);
}
}
ValidateClipAssociations(graph, source, preview);
}
private static void ValidateClipAssociations(
FrameAnimationGraph graph,
FrameAnimationImportSource source,
FrameAnimationImportSourcePreview preview)
{
var graphPath = AssetDatabase.GetAssetPath(graph);
foreach (var clip in graph.Clips.Where(clip => clip != null && clip.IsImported))
{
if (!graph.ImportSources.Any(item => item != null && item.InternalId == clip.ImportInfo.ImportSourceId))
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.ImportSourceReferenceInvalid,
$"Imported Clip '{clip.Id}' 指向不存在的 ImportSource。"));
}
if (!AssetDatabase.IsSubAsset(clip) || AssetDatabase.GetAssetPath(clip) != graphPath)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.ImportedClipOwnershipInvalid,
$"Imported Clip '{clip.Id}' 必须是所属 Graph 的 sub-asset。"));
}
}
foreach (var duplicate in graph.Clips
.Where(clip => clip != null && clip.ImportInfo?.ImportSourceId == source.InternalId)
.GroupBy(clip => clip.ImportInfo.SourceTagName)
.Where(group => group.Count() > 1))
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.ImportSourceReferenceInvalid,
$"来源 Tag '{duplicate.Key}' 同时关联了多个 Imported Clip。"));
}
}
private static void ValidateCrossSourceTags(
FrameAnimationGraph graph,
IReadOnlyDictionary<FrameAnimationImportSource, AsepriteSourceDocument> documents,
IReadOnlyDictionary<FrameAnimationImportSource, FrameAnimationImportSourcePreview> targetPreviews)
{
var tags = documents.SelectMany(pair => pair.Value.Tags.Select(tag => (source: pair.Key, tag)));
foreach (var duplicate in tags.Where(item => !string.IsNullOrWhiteSpace(item.tag.Name))
.GroupBy(item => item.tag.Name).Where(group => group.Count() > 1))
{
foreach (var item in duplicate)
{
if (targetPreviews.TryGetValue(item.source, out var sourcePreview))
{
sourcePreview.AddIssue(Error(item.source, FrameAnimationImportIssueCode.TagConflict,
$"Tag '{duplicate.Key}' 与其他启用 ImportSource 重名。"));
}
}
}
foreach (var pair in targetPreviews)
{
if (!documents.TryGetValue(pair.Key, out var document))
{
continue;
}
foreach (var tag in document.Tags)
{
var existing = graph.Clips.Count(clip => clip != null &&
clip.ImportInfo?.ImportSourceId == pair.Key.InternalId &&
clip.ImportInfo.SourceTagName == tag.Name);
if (existing > 0)
{
continue;
}
if (graph.Clips.Any(clip => clip != null && clip.Id == tag.Name) ||
graph.Flows.Any(flow => flow != null && flow.Id == tag.Name))
{
pair.Value.AddIssue(Error(pair.Key, FrameAnimationImportIssueCode.PlayableIdConflict,
$"新 Tag '{tag.Name}' 与现有 Clip / Flow playable id 冲突。"));
}
}
}
}
private static void ValidateTextureOwnership(
FrameAnimationGraph currentGraph,
IReadOnlyDictionary<FrameAnimationImportSource, FrameAnimationImportSourcePreview> targetPreviews)
{
var owners = new List<(FrameAnimationGraph graph, FrameAnimationImportSource source)>();
foreach (var guid in AssetDatabase.FindAssets("t:FrameAnimationGraph"))
{
var graph = AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(AssetDatabase.GUIDToAssetPath(guid));
if (graph == null)
{
continue;
}
owners.AddRange(graph.ImportSources.Where(source => source != null && source.IsEnabled &&
source.ManageSpriteSlicing && source.Texture != null).Select(source => (graph, source)));
}
if (!owners.Any(item => item.graph == currentGraph))
{
owners.AddRange(currentGraph.ImportSources.Where(source => source != null && source.IsEnabled &&
source.ManageSpriteSlicing && source.Texture != null).Select(source => (currentGraph, source)));
}
foreach (var pair in targetPreviews.Where(pair => pair.Key.ManageSpriteSlicing && pair.Key.Texture != null))
{
var conflicts = owners.Where(item => item.source.Texture == pair.Key.Texture &&
!(item.graph == currentGraph && ReferenceEquals(item.source, pair.Key))).ToArray();
if (conflicts.Length > 0)
{
pair.Value.AddIssue(Error(pair.Key, FrameAnimationImportIssueCode.TextureOwnershipConflict,
$"Texture '{pair.Key.Texture.name}' 已被其他可写 ImportSource 占用:" +
string.Join(", ", conflicts.Select(item => $"{item.graph.name}/{item.source.DisplayName}"))));
}
}
}
private static void BuildClipDiffs(
FrameAnimationGraph graph,
FrameAnimationImportSource source,
AsepriteSourceDocument document,
FrameAnimationImportSourcePreview preview)
{
var seenTags = new HashSet<string>();
foreach (var tag in document.Tags)
{
if (!AsepriteJsonParser.TryExpandTag(tag, document.Frames.Count, source.InternalId, out var indices, out _))
{
continue;
}
seenTags.Add(tag.Name);
var clip = graph.Clips.SingleOrDefault(item => item != null &&
item.ImportInfo?.ImportSourceId == source.InternalId &&
item.ImportInfo.SourceTagName == tag.Name);
if (clip == null)
{
preview.AddClipDiff(new FrameAnimationClipDiff(
FrameAnimationImportChangeKind.Added, tag.Name, null, indices, "创建 Imported Clip sub-asset"));
continue;
}
var changed = clip.ImportInfo.IsMissingFromSource ||
!FramesEqual(clip, document, preview.SpritePlan, indices);
preview.AddClipDiff(new FrameAnimationClipDiff(
changed ? FrameAnimationImportChangeKind.Updated : FrameAnimationImportChangeKind.Unchanged,
tag.Name,
clip,
indices,
changed ? "原地更新导入帧" : "无变化"));
}
foreach (var clip in graph.Clips.Where(item => item != null &&
item.ImportInfo?.ImportSourceId == source.InternalId &&
!seenTags.Contains(item.ImportInfo.SourceTagName)))
{
preview.AddClipDiff(new FrameAnimationClipDiff(
clip.ImportInfo.IsMissingFromSource
? FrameAnimationImportChangeKind.Unchanged
: FrameAnimationImportChangeKind.Missing,
clip.ImportInfo.SourceTagName,
clip,
Array.Empty<int>(),
clip.ImportInfo.IsMissingFromSource ? "保持 Missing" : "标记 Missing,不删除 Clip"));
}
}
private static bool FramesEqual(
FrameClip clip,
AsepriteSourceDocument document,
FrameAnimationSpritePlan spritePlan,
IReadOnlyList<int> indices)
{
if (clip.FrameCount != indices.Count)
{
return false;
}
for (var index = 0; index < indices.Count; index++)
{
var sourceIndex = indices[index];
var sourceFrame = document.Frames[sourceIndex];
var frame = clip.Frames[index];
if (frame == null || frame.SourceIndex != sourceIndex ||
frame.FrameName != sourceFrame.FrameName || frame.DurationMs != sourceFrame.DurationMs ||
spritePlan == null || !spritePlan.MatchesSprite(sourceIndex, frame.Sprite))
{
return false;
}
}
return true;
}
private static void ApplySource(
FrameAnimationGraph graph,
FrameAnimationImportSourcePreview preview)
{
var source = preview.Source;
var writableSprites = source.ManageSpriteSlicing
? FrameAnimationSpriteUtility.LoadSpritesByName(AssetDatabase.GetAssetPath(source.Texture))
: null;
foreach (var diff in preview.ClipDiffs)
{
if (diff.Kind == FrameAnimationImportChangeKind.Missing)
{
Undo.RecordObject(diff.Clip, "Mark Imported Clip Missing");
diff.Clip.SetImportMissingState(true);
EditorUtility.SetDirty(diff.Clip);
continue;
}
if (diff.Kind == FrameAnimationImportChangeKind.Unchanged && diff.Clip != null)
{
continue;
}
var frames = diff.SourceIndices.Select(sourceIndex =>
{
var sourceFrame = preview.Document.Frames[sourceIndex];
if (!preview.SpritePlan.TryResolveSprite(sourceIndex, writableSprites, out var sprite))
{
var expectedName = preview.SpritePlan.SpriteNamesBySourceIndex.TryGetValue(
sourceIndex, out var plannedName)
? plannedName
: sourceFrame.FrameName;
throw new InvalidOperationException(
$"刷新后找不到 SourceFrame '{sourceFrame.FrameName}' 对应的共享 Sprite '{expectedName}'。 ");
}
return new FrameAnimationFrame(sprite, sourceFrame.DurationMs, sourceFrame.FrameName, sourceIndex);
}).ToArray();
if (diff.Clip != null)
{
Undo.RecordObject(diff.Clip, "Update Imported Frame Clip");
diff.Clip.ReplaceImportedFrames(frames, source.InternalId, diff.SourceTagName);
EditorUtility.SetDirty(diff.Clip);
continue;
}
var clip = ScriptableObject.CreateInstance<FrameClip>();
clip.name = diff.SourceTagName;
clip.Configure(
diff.SourceTagName,
diff.SourceTagName,
frames,
1f,
source.DefaultNewClipEndBehavior,
new FrameClipImportInfo(source.InternalId, diff.SourceTagName, false));
AssetDatabase.AddObjectToAsset(clip, graph);
Undo.RegisterCreatedObjectUndo(clip, "Create Imported Frame Clip");
graph.AddImportedClip(clip);
EditorUtility.SetDirty(clip);
}
}
private static FrameAnimationImportIssue Error(
FrameAnimationImportSource source,
FrameAnimationImportIssueCode code,
string message)
{
return new FrameAnimationImportIssue(
FrameAnimationValidationSeverity.Error,
code,
source?.InternalId,
source?.InternalId,
message);
}
private static FrameAnimationImportIssue Warning(
FrameAnimationImportSource source,
FrameAnimationImportIssueCode code,
string message)
{
return new FrameAnimationImportIssue(
FrameAnimationValidationSeverity.Warning,
code,
source?.InternalId,
source?.InternalId,
message);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ed2b95cf766a27d4cb93bbc8b2ba472e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,84 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
namespace AibisDream.FrameAnimation.Editor
{
[CustomEditor(typeof(FrameAnimationPlayer))]
public sealed class FrameAnimationPlayerEditor : UnityEditor.Editor
{
private SerializedProperty graphProperty;
private SerializedProperty playOnEnableProperty;
private SerializedProperty speedProperty;
private void OnEnable()
{
graphProperty = serializedObject.FindProperty("graph");
playOnEnableProperty = serializedObject.FindProperty("playOnEnable");
speedProperty = serializedObject.FindProperty("speed");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(graphProperty);
EditorGUILayout.PropertyField(playOnEnableProperty);
EditorGUILayout.PropertyField(speedProperty);
DrawConfigurationWarnings();
serializedObject.ApplyModifiedProperties();
if (Application.isPlaying)
{
DrawRuntimeState();
}
}
private void DrawConfigurationWarnings()
{
var player = (FrameAnimationPlayer)target;
var spriteRenderer = player.GetComponent<SpriteRenderer>();
var image = player.GetComponent<Image>();
if (spriteRenderer == null && image == null)
{
EditorGUILayout.HelpBox(
"同一 GameObject 上必须存在一个 SpriteRenderer 或 Image。",
MessageType.Error);
}
else if (spriteRenderer != null && image != null)
{
EditorGUILayout.HelpBox(
"同一 GameObject 不能同时存在 SpriteRenderer 和 Image。",
MessageType.Error);
}
if (graphProperty.objectReferenceValue == null)
{
EditorGUILayout.HelpBox("尚未绑定 FrameAnimationGraph。", MessageType.Error);
}
if (!FrameAnimationValueUtility.IsValidSpeed(speedProperty.floatValue))
{
EditorGUILayout.HelpBox("Speed 必须是有限且不小于 0 的数值。", MessageType.Error);
}
}
private void DrawRuntimeState()
{
var player = (FrameAnimationPlayer)target;
EditorGUILayout.Space();
EditorGUILayout.LabelField("Runtime State", EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.EnumPopup("State", player.State);
EditorGUILayout.TextField("Playable", player.CurrentPlayableId);
EditorGUILayout.TextField("Clip", player.CurrentClipId);
EditorGUILayout.TextField("Node", player.CurrentNodeId);
EditorGUILayout.IntField("Frame", player.CurrentFrameIndex);
}
Repaint();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 26b26e9170b32ec4693ae2aa8ab88838
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,593 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
internal enum FrameAnimationPreviewTargetKind { None, Clip, Node, Flow }
internal enum FrameAnimationPreviewState { Stopped, Playing, Paused, Failed }
internal enum NodePreviewPolicy { Static, SelectedOnly, AllVisible }
internal enum FrameAnimationPreviewBackground { Checkerboard, Dark, Light }
internal enum FrameAnimationPreviewZoomMode { Fit, One, Two, Three, Four, Eight, Manual }
internal enum FrameAnimationFlowElementState { None, Upcoming, Current, Passed }
internal readonly struct FrameAnimationPreviewSegment
{
public int StepIndex { get; }
public int FrameIndex { get; }
public FrameClip Clip { get; }
public string NodeId { get; }
public double StartSeconds { get; }
public double DurationSeconds { get; }
public bool IsBlocked { get; }
public FrameAnimationPreviewSegment(
int stepIndex,
int frameIndex,
ResolvedPlaybackStep step,
double startSeconds,
double durationSeconds,
bool isBlocked)
{
StepIndex = stepIndex;
FrameIndex = frameIndex;
Clip = step.Clip;
NodeId = step.NodeId;
StartSeconds = startSeconds;
DurationSeconds = durationSeconds;
IsBlocked = isBlocked;
}
}
internal sealed class FrameAnimationPreviewTimeline
{
private const double Epsilon = 0.000000001d;
private readonly List<FrameAnimationPreviewSegment> segments = new List<FrameAnimationPreviewSegment>();
public IReadOnlyList<FrameAnimationPreviewSegment> Segments => segments;
public double DisplayDurationSeconds { get; }
public bool IsInfinite { get; }
public bool IsBlocked { get; }
public int BlockedSegmentIndex { get; } = -1;
public FrameAnimationPreviewTimeline(ResolvedPlaybackPlan plan)
{
if (plan?.Steps == null || plan.Steps.Count == 0)
{
return;
}
var cursor = 0d;
var blockedIndex = -1;
for (var stepIndex = 0; stepIndex < plan.Steps.Count; stepIndex++)
{
var step = plan.Steps[stepIndex];
if (step?.Clip == null)
{
continue;
}
for (var frameIndex = 0; frameIndex < step.Clip.FrameCount; frameIndex++)
{
var frame = step.Clip.Frames[frameIndex];
if (step.PlaybackSpeed == 0f)
{
blockedIndex = segments.Count;
segments.Add(new FrameAnimationPreviewSegment(
stepIndex, frameIndex, step, cursor, 0d, true));
break;
}
var duration = frame.DurationMs / 1000d / step.PlaybackSpeed;
segments.Add(new FrameAnimationPreviewSegment(
stepIndex, frameIndex, step, cursor, duration, false));
cursor += duration;
}
if (blockedIndex >= 0)
{
break;
}
}
DisplayDurationSeconds = cursor;
BlockedSegmentIndex = blockedIndex;
IsBlocked = blockedIndex >= 0;
IsInfinite = IsBlocked || plan.Steps[plan.Steps.Count - 1].TerminalEndBehavior == FrameClipEndBehavior.Loop;
}
public int FindSegmentIndex(double positionSeconds)
{
if (segments.Count == 0)
{
return -1;
}
var position = Math.Max(0d, positionSeconds);
for (var index = 0; index < segments.Count; index++)
{
var segment = segments[index];
if (segment.IsBlocked || position < segment.StartSeconds + segment.DurationSeconds - Epsilon)
{
return index;
}
}
return segments.Count - 1;
}
public double GetSeekSeconds(int segmentIndex)
{
return segmentIndex >= 0 && segmentIndex < segments.Count
? segments[segmentIndex].StartSeconds
: 0d;
}
public double GetPosition(FrameAnimationPlaybackSnapshot snapshot)
{
if (!snapshot.HasStarted || segments.Count == 0)
{
return 0d;
}
var segment = segments.FirstOrDefault(item =>
item.StepIndex == snapshot.StepIndex && item.FrameIndex == snapshot.FrameIndex);
if (segment.Clip == null)
{
return snapshot.IsCompleted ? DisplayDurationSeconds : 0d;
}
if (snapshot.IsCompleted && !IsInfinite)
{
return DisplayDurationSeconds;
}
var elapsed = snapshot.StepSpeed > 0f
? snapshot.FrameElapsedSeconds / snapshot.StepSpeed
: 0d;
return Math.Min(DisplayDurationSeconds, segment.StartSeconds + elapsed);
}
}
internal readonly struct FrameAnimationPreviewDisplay
{
public Sprite Sprite { get; }
public bool IsEmpty { get; }
public bool IsActive { get; }
public FrameAnimationFlowElementState FlowState { get; }
public FrameAnimationPreviewDisplay(
Sprite sprite,
bool isEmpty,
bool isActive,
FrameAnimationFlowElementState flowState)
{
Sprite = sprite;
IsEmpty = isEmpty;
IsActive = isActive;
FlowState = flowState;
}
}
internal sealed class FrameAnimationPreviewCoordinator
{
private sealed class NodeRun
{
public AnimationNode Node;
public FrameAnimationPlaybackSession Session;
public Sprite Sprite;
}
private readonly Dictionary<string, NodeRun> automaticRuns = new Dictionary<string, NodeRun>();
private FrameAnimationGraph graph;
private object target;
private ResolvedPlaybackPlan plan;
private FrameAnimationPlaybackSession session;
private Sprite currentSprite;
private bool terminalTransparent;
private bool suppressSelectedNode;
private bool flowPresentationActive;
public event Action Changed;
public FrameAnimationPreviewTargetKind TargetKind { get; private set; }
public FrameAnimationPreviewState State { get; private set; } = FrameAnimationPreviewState.Stopped;
public NodePreviewPolicy NodePolicy { get; set; } = NodePreviewPolicy.SelectedOnly;
public FrameAnimationPreviewBackground Background { get; set; } = FrameAnimationPreviewBackground.Checkerboard;
public FrameAnimationPreviewZoomMode ZoomMode { get; set; } = FrameAnimationPreviewZoomMode.Fit;
public float ManualZoom { get; set; } = 1f;
public float PreviewSpeed { get; set; } = 1f;
public FrameAnimationPreviewTimeline Timeline { get; private set; }
public FrameAnimationPlaybackError Error { get; private set; } = FrameAnimationPlaybackError.None;
public double PositionSeconds { get; private set; }
public string TransitionFromNodeId { get; private set; } = string.Empty;
public string TransitionToNodeId { get; private set; } = string.Empty;
public object Target => target;
public bool IsFlowPreview => TargetKind == FrameAnimationPreviewTargetKind.Flow &&
flowPresentationActive && State != FrameAnimationPreviewState.Failed;
public FrameAnimationPlaybackSnapshot Snapshot => session != null
? session.Snapshot
: default;
public Sprite CurrentSprite => terminalTransparent ? null : currentSprite;
public void SetGraph(FrameAnimationGraph value)
{
if (ReferenceEquals(graph, value))
{
return;
}
graph = value;
ClearTarget();
automaticRuns.Clear();
}
public void SetTarget(FrameAnimationPreviewTargetKind kind, object value)
{
if (TargetKind == kind && ReferenceEquals(target, value))
{
return;
}
TargetKind = kind;
target = value;
suppressSelectedNode = false;
BuildTarget(false, 0d);
if (kind == FrameAnimationPreviewTargetKind.Node && NodePolicy != NodePreviewPolicy.Static && session != null)
{
State = FrameAnimationPreviewState.Playing;
}
Changed?.Invoke();
}
public void ClearTarget()
{
TargetKind = FrameAnimationPreviewTargetKind.None;
target = null;
plan = null;
session = null;
Timeline = null;
currentSprite = null;
terminalTransparent = false;
Error = FrameAnimationPlaybackError.None;
PositionSeconds = 0d;
State = FrameAnimationPreviewState.Stopped;
flowPresentationActive = false;
TransitionFromNodeId = string.Empty;
TransitionToNodeId = string.Empty;
Changed?.Invoke();
}
public void RefreshTarget(bool topologyChanged)
{
if (TargetKind == FrameAnimationPreviewTargetKind.None)
{
return;
}
if (topologyChanged && TargetKind == FrameAnimationPreviewTargetKind.Flow)
{
Stop();
BuildTarget(false, 0d);
return;
}
var wasPlaying = State == FrameAnimationPreviewState.Playing;
var position = PositionSeconds;
BuildTarget(position > 0d, position);
if (wasPlaying && session != null)
{
State = FrameAnimationPreviewState.Playing;
flowPresentationActive = TargetKind == FrameAnimationPreviewTargetKind.Flow;
}
Changed?.Invoke();
}
public void SetNodePolicy(NodePreviewPolicy value)
{
NodePolicy = value;
automaticRuns.Clear();
suppressSelectedNode = false;
if (TargetKind == FrameAnimationPreviewTargetKind.Node)
{
BuildTarget(false, 0d);
if (session != null && value != NodePreviewPolicy.Static)
{
State = FrameAnimationPreviewState.Playing;
}
}
Changed?.Invoke();
}
public void Play()
{
if (session == null)
{
BuildTarget(false, 0d);
}
if (session == null)
{
return;
}
if (session.Snapshot.IsCompleted)
{
session.Restart(ApplySprite);
PositionSeconds = 0d;
}
terminalTransparent = false;
suppressSelectedNode = false;
flowPresentationActive = TargetKind == FrameAnimationPreviewTargetKind.Flow;
State = FrameAnimationPreviewState.Playing;
Changed?.Invoke();
}
public void Pause()
{
if (session == null || State != FrameAnimationPreviewState.Playing) return;
State = FrameAnimationPreviewState.Paused;
Changed?.Invoke();
}
public void Stop()
{
if (session != null)
{
session.Restart(ApplySprite);
}
State = FrameAnimationPreviewState.Stopped;
PositionSeconds = 0d;
terminalTransparent = false;
suppressSelectedNode = TargetKind == FrameAnimationPreviewTargetKind.Node;
flowPresentationActive = false;
TransitionFromNodeId = string.Empty;
TransitionToNodeId = string.Empty;
Changed?.Invoke();
}
public void Restart()
{
if (session == null)
{
BuildTarget(false, 0d);
}
session?.Restart(ApplySprite);
PositionSeconds = 0d;
terminalTransparent = false;
suppressSelectedNode = false;
flowPresentationActive = TargetKind == FrameAnimationPreviewTargetKind.Flow;
if (session != null) State = FrameAnimationPreviewState.Playing;
Changed?.Invoke();
}
public void Step(int direction)
{
if (session == null || Timeline == null || Timeline.Segments.Count == 0) return;
var current = Timeline.FindSegmentIndex(PositionSeconds);
var next = Mathf.Clamp(current + Math.Sign(direction), 0, Timeline.Segments.Count - 1);
Seek(Timeline.GetSeekSeconds(next));
}
public void Seek(double seconds)
{
if (session == null || Timeline == null) return;
var maximum = Timeline.DisplayDurationSeconds;
var targetSeconds = Math.Max(0d, Math.Min(maximum, seconds));
if (Timeline.IsInfinite && maximum > 0d && targetSeconds >= maximum)
{
targetSeconds = Math.Max(0d, maximum - 0.000001d);
}
terminalTransparent = false;
flowPresentationActive = TargetKind == FrameAnimationPreviewTargetKind.Flow;
var result = session.Seek(targetSeconds, ApplySprite);
ApplyEvaluation(result);
PositionSeconds = targetSeconds;
if (State != FrameAnimationPreviewState.Failed)
{
State = FrameAnimationPreviewState.Paused;
}
Changed?.Invoke();
}
public bool Tick(double deltaSeconds, IReadOnlyCollection<AnimationNode> visibleNodes)
{
var changed = false;
TransitionFromNodeId = string.Empty;
TransitionToNodeId = string.Empty;
if (State == FrameAnimationPreviewState.Playing && session != null)
{
var before = session.Snapshot;
var result = session.Evaluate(deltaSeconds, PreviewSpeed, ApplySprite);
var after = session.Snapshot;
if (before.StepIndex != after.StepIndex)
{
TransitionFromNodeId = before.NodeId;
TransitionToNodeId = after.NodeId;
}
ApplyEvaluation(result);
PositionSeconds = Timeline?.GetPosition(after) ?? 0d;
if (result.IsCompleted && TargetKind == FrameAnimationPreviewTargetKind.Node &&
NodePolicy != NodePreviewPolicy.Static && !suppressSelectedNode && session != null)
{
session.Restart(ApplySprite);
State = FrameAnimationPreviewState.Playing;
PositionSeconds = 0d;
terminalTransparent = false;
}
changed = true;
}
if (!IsFlowPreview)
{
changed |= TickAutomaticNodes(deltaSeconds, visibleNodes);
}
else if (automaticRuns.Count > 0)
{
automaticRuns.Clear();
changed = true;
}
if (changed) Changed?.Invoke();
return changed;
}
public FrameAnimationPreviewDisplay GetNodeDisplay(AnimationNode node)
{
if (node == null)
{
return default;
}
if (IsFlowPreview && plan != null)
{
var current = session?.Snapshot.StepIndex ?? 0;
var stepIndex = IndexOfNode(plan, node.InternalId);
var flowState = stepIndex < 0 ? FrameAnimationFlowElementState.None :
stepIndex < current ? FrameAnimationFlowElementState.Passed :
stepIndex == current ? FrameAnimationFlowElementState.Current : FrameAnimationFlowElementState.Upcoming;
if (flowState == FrameAnimationFlowElementState.Current)
{
return new FrameAnimationPreviewDisplay(CurrentSprite, CurrentSprite == null, true, flowState);
}
return Representative(node, flowState);
}
if (TargetKind == FrameAnimationPreviewTargetKind.Node && ReferenceEquals(target, node) && session != null)
{
if (NodePolicy == NodePreviewPolicy.Static && State == FrameAnimationPreviewState.Stopped &&
!suppressSelectedNode && !session.Snapshot.IsCompleted)
{
return Representative(node, FrameAnimationFlowElementState.None);
}
return new FrameAnimationPreviewDisplay(CurrentSprite, CurrentSprite == null, State == FrameAnimationPreviewState.Playing,
FrameAnimationFlowElementState.None);
}
if (automaticRuns.TryGetValue(node.InternalId, out var run))
{
return new FrameAnimationPreviewDisplay(run.Sprite, run.Sprite == null, true, FrameAnimationFlowElementState.None);
}
return Representative(node, FrameAnimationFlowElementState.None);
}
public FrameAnimationFlowElementState GetEdgeState(AnimationEdge edge)
{
if (!IsFlowPreview || plan == null || edge == null) return FrameAnimationFlowElementState.None;
if (edge.FromNodeId == TransitionFromNodeId && edge.ToNodeId == TransitionToNodeId)
{
return FrameAnimationFlowElementState.Current;
}
var from = IndexOfNode(plan, edge.FromNodeId);
var to = IndexOfNode(plan, edge.ToNodeId);
if (from < 0 || to != from + 1) return FrameAnimationFlowElementState.None;
var current = session?.Snapshot.StepIndex ?? 0;
return to <= current ? FrameAnimationFlowElementState.Passed : FrameAnimationFlowElementState.Upcoming;
}
private bool TickAutomaticNodes(double deltaSeconds, IReadOnlyCollection<AnimationNode> visibleNodes)
{
IEnumerable<AnimationNode> desired = Array.Empty<AnimationNode>();
if (NodePolicy == NodePreviewPolicy.SelectedOnly && TargetKind == FrameAnimationPreviewTargetKind.Node &&
target is AnimationNode selected && !suppressSelectedNode)
{
desired = new[] { selected };
}
else if (NodePolicy == NodePreviewPolicy.AllVisible)
{
desired = visibleNodes ?? Array.Empty<AnimationNode>();
}
var desiredNodes = desired.Where(node => node != null &&
!(TargetKind == FrameAnimationPreviewTargetKind.Node && ReferenceEquals(target, node)))
.GroupBy(node => node.InternalId).Select(group => group.First()).ToArray();
var desiredIds = new HashSet<string>(desiredNodes.Select(node => node.InternalId));
foreach (var id in automaticRuns.Keys.Where(id => !desiredIds.Contains(id)).ToArray())
{
automaticRuns.Remove(id);
}
foreach (var node in desiredNodes)
{
if (!automaticRuns.TryGetValue(node.InternalId, out var run))
{
if (!FrameAnimationResolver.TryResolveNode(graph, node, out var nodePlan, out _)) continue;
run = new NodeRun { Node = node, Session = new FrameAnimationPlaybackSession(nodePlan) };
run.Session.Start(sprite => run.Sprite = sprite);
automaticRuns[node.InternalId] = run;
}
var evaluation = run.Session.Evaluate(deltaSeconds, PreviewSpeed, sprite => run.Sprite = sprite);
if (evaluation.IsCompleted)
{
run.Session.Restart(sprite => run.Sprite = sprite);
}
else if (evaluation.IsFailed)
{
automaticRuns.Remove(node.InternalId);
}
}
return desiredNodes.Length > 0 || automaticRuns.Count > 0;
}
private void BuildTarget(bool seek, double position)
{
plan = null;
session = null;
Timeline = null;
currentSprite = null;
terminalTransparent = false;
PositionSeconds = 0d;
Error = FrameAnimationPlaybackError.None;
State = FrameAnimationPreviewState.Stopped;
flowPresentationActive = false;
var resolved = TargetKind switch
{
FrameAnimationPreviewTargetKind.Clip when target is FrameClip clip =>
FrameAnimationResolver.TryResolve(graph, clip.Id, default, out plan, out var clipError)
? FrameAnimationPlaybackError.None : clipError,
FrameAnimationPreviewTargetKind.Node when target is AnimationNode node =>
FrameAnimationResolver.TryResolveNode(graph, node, out plan, out var nodeError)
? FrameAnimationPlaybackError.None : nodeError,
FrameAnimationPreviewTargetKind.Flow when target is AnimationFlow flow =>
FrameAnimationResolver.TryResolve(graph, flow.Id, default, out plan, out var flowError)
? FrameAnimationPlaybackError.None : flowError,
_ => new FrameAnimationPlaybackError(FrameAnimationPlaybackErrorCode.PlayableNotFound, "没有可预览目标。")
};
if (resolved.Code != FrameAnimationPlaybackErrorCode.None || plan == null)
{
Error = resolved;
State = TargetKind == FrameAnimationPreviewTargetKind.None
? FrameAnimationPreviewState.Stopped : FrameAnimationPreviewState.Failed;
return;
}
Timeline = new FrameAnimationPreviewTimeline(plan);
session = new FrameAnimationPlaybackSession(plan);
session.Start(ApplySprite);
if (seek && position > 0d)
{
Seek(position);
}
}
private void ApplyEvaluation(FrameAnimationSessionEvaluation evaluation)
{
if (evaluation.IsFailed)
{
Error = evaluation.Error;
State = FrameAnimationPreviewState.Failed;
flowPresentationActive = false;
return;
}
if (!evaluation.IsCompleted) return;
State = FrameAnimationPreviewState.Stopped;
terminalTransparent = evaluation.EndBehavior == FrameClipEndBehavior.Clear ||
evaluation.EndBehavior == FrameClipEndBehavior.HideTarget;
}
private void ApplySprite(Sprite sprite)
{
currentSprite = sprite;
terminalTransparent = false;
}
private FrameAnimationPreviewDisplay Representative(AnimationNode node, FrameAnimationFlowElementState flowState)
{
var clip = graph?.Clips.FirstOrDefault(item => item != null && item.Id == node.ClipId);
var sprite = clip?.Frames.FirstOrDefault(frame => frame?.Sprite != null)?.Sprite;
return new FrameAnimationPreviewDisplay(sprite, sprite == null, false, flowState);
}
private static int IndexOfNode(ResolvedPlaybackPlan targetPlan, string nodeId)
{
if (targetPlan?.Steps == null) return -1;
for (var index = 0; index < targetPlan.Steps.Count; index++)
{
if (targetPlan.Steps[index].NodeId == nodeId) return index;
}
return -1;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 14a91ced504e4af4ad79f4f760794c3c
@@ -0,0 +1,163 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace AibisDream.FrameAnimation.Editor
{
internal sealed class FrameAnimationPreviewElement : VisualElement
{
private static Texture2D checkerTexture;
private readonly Image image;
private readonly Label emptyLabel;
private Sprite sprite;
private FrameAnimationPreviewZoomMode zoomMode = FrameAnimationPreviewZoomMode.Fit;
private float manualZoom = 1f;
private Vector2 pan;
private Vector2 dragStart;
private Vector2 panStart;
private bool dragging;
public FrameAnimationPreviewElement()
{
AddToClassList("fa-preview");
focusable = true;
image = new Image { scaleMode = ScaleMode.ScaleToFit, pickingMode = PickingMode.Ignore };
image.AddToClassList("fa-preview__image");
emptyLabel = new Label("Empty Frame") { pickingMode = PickingMode.Ignore };
emptyLabel.AddToClassList("fa-preview__empty");
Add(image);
Add(emptyLabel);
RegisterCallback<GeometryChangedEvent>(_ => UpdateImageLayout());
RegisterCallback<PointerDownEvent>(OnPointerDown);
RegisterCallback<PointerMoveEvent>(OnPointerMove);
RegisterCallback<PointerUpEvent>(OnPointerUp);
SetBackground(FrameAnimationPreviewBackground.Checkerboard);
}
public void SetDisplay(Sprite value, bool isEmpty, string emptyText = "Empty Frame")
{
sprite = value;
image.sprite = value;
image.style.display = value != null ? DisplayStyle.Flex : DisplayStyle.None;
emptyLabel.text = string.IsNullOrEmpty(emptyText) ? "Empty Frame" : emptyText;
emptyLabel.style.display = value == null || isEmpty ? DisplayStyle.Flex : DisplayStyle.None;
UpdateImageLayout();
}
public void SetBackground(FrameAnimationPreviewBackground background)
{
switch (background)
{
case FrameAnimationPreviewBackground.Dark:
style.backgroundImage = StyleKeyword.None;
style.backgroundColor = new StyleColor(new Color(0.08f, 0.08f, 0.08f));
break;
case FrameAnimationPreviewBackground.Light:
style.backgroundImage = StyleKeyword.None;
style.backgroundColor = new StyleColor(new Color(0.72f, 0.72f, 0.72f));
break;
default:
style.backgroundImage = new StyleBackground(GetCheckerTexture());
style.backgroundColor = StyleKeyword.Null;
break;
}
}
public void SetZoom(FrameAnimationPreviewZoomMode mode, float value)
{
zoomMode = mode;
manualZoom = Mathf.Clamp(value, 0.25f, 8f);
if (mode == FrameAnimationPreviewZoomMode.Fit)
{
pan = Vector2.zero;
}
UpdateImageLayout();
}
private void UpdateImageLayout()
{
if (sprite == null || resolvedStyle.width <= 0f || resolvedStyle.height <= 0f)
{
return;
}
var sourceSize = sprite.rect.size;
if (sourceSize.x <= 0f || sourceSize.y <= 0f)
{
return;
}
var scale = zoomMode == FrameAnimationPreviewZoomMode.Fit
? Mathf.Min(resolvedStyle.width / sourceSize.x, resolvedStyle.height / sourceSize.y)
: GetScale();
var size = sourceSize * Mathf.Max(0.0001f, scale);
image.style.width = size.x;
image.style.height = size.y;
image.style.left = (resolvedStyle.width - size.x) * 0.5f + pan.x;
image.style.top = (resolvedStyle.height - size.y) * 0.5f + pan.y;
}
private float GetScale()
{
return zoomMode switch
{
FrameAnimationPreviewZoomMode.One => 1f,
FrameAnimationPreviewZoomMode.Two => 2f,
FrameAnimationPreviewZoomMode.Three => 3f,
FrameAnimationPreviewZoomMode.Four => 4f,
FrameAnimationPreviewZoomMode.Eight => 8f,
FrameAnimationPreviewZoomMode.Manual => manualZoom,
_ => 1f
};
}
private void OnPointerDown(PointerDownEvent evt)
{
if (zoomMode == FrameAnimationPreviewZoomMode.Fit || evt.button != 0) return;
dragging = true;
dragStart = new Vector2(evt.position.x, evt.position.y);
panStart = pan;
this.CapturePointer(evt.pointerId);
evt.StopPropagation();
}
private void OnPointerMove(PointerMoveEvent evt)
{
if (!dragging || !this.HasPointerCapture(evt.pointerId)) return;
var position = new Vector2(evt.position.x, evt.position.y);
pan = panStart + position - dragStart;
UpdateImageLayout();
evt.StopPropagation();
}
private void OnPointerUp(PointerUpEvent evt)
{
if (!dragging || !this.HasPointerCapture(evt.pointerId)) return;
dragging = false;
this.ReleasePointer(evt.pointerId);
evt.StopPropagation();
}
private static Texture2D GetCheckerTexture()
{
if (checkerTexture != null) return checkerTexture;
const int textureSize = 128;
const int cellSize = 8;
checkerTexture = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false)
{
name = "Frame Animation Preview Checker",
hideFlags = HideFlags.HideAndDontSave,
wrapMode = TextureWrapMode.Repeat,
filterMode = FilterMode.Point
};
var light = new Color32(106, 106, 106, 255);
var dark = new Color32(78, 78, 78, 255);
var pixels = new Color32[textureSize * textureSize];
for (var y = 0; y < textureSize; y++)
for (var x = 0; x < textureSize; x++)
{
pixels[y * textureSize + x] = ((x / cellSize) + (y / cellSize)) % 2 == 0 ? light : dark;
}
checkerTexture.SetPixels32(pixels);
checkerTexture.Apply(false, true);
return checkerTexture;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 54aef216ceea42ed8917b581606ecf3d
@@ -0,0 +1,142 @@
using System;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
public static class FrameAnimationPreviewSampleBuilder
{
private const string Folder = "Assets/GameContent/Test/FrameAnimation/Preview";
private const string GraphPath = Folder + "/PreviewSampleGraph.asset";
private const string TexturePath = "Assets/GameContent/Test/AnimatorRaw/手接树叶特写.png";
[MenuItem("Tools/Frame Animation/Create Phase 5 Preview Sample")]
public static void BuildFromCommandLine()
{
if (AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(GraphPath) != null)
{
Debug.Log($"Frame Animation preview sample already exists; preserved without overwrite: {GraphPath}");
return;
}
EnsureFolder(Folder);
var sprites = AssetDatabase.LoadAllAssetsAtPath(TexturePath).OfType<Sprite>()
.OrderBy(sprite => sprite.name, StringComparer.Ordinal).ToArray();
if (sprites.Length < 6)
{
throw new InvalidOperationException("第五阶段预览样例需要至少 6 个已切分 Sprite。");
}
var graph = ScriptableObject.CreateInstance<FrameAnimationGraph>();
graph.name = "PreviewSampleGraph";
AssetDatabase.CreateAsset(graph, GraphPath);
var action = CreateClip(graph, "PreviewAction", "Preview Action", FrameClipEndBehavior.HoldLastFrame,
new[]
{
new FrameAnimationFrame(sprites[0], 100, sprites[0].name, 0),
new FrameAnimationFrame(sprites[1], 160, sprites[1].name, 1),
new FrameAnimationFrame(sprites[2], 220, sprites[2].name, 2)
});
var withEmpty = CreateClip(graph, "PreviewEmpty", "Preview Empty Frame", FrameClipEndBehavior.HoldLastFrame,
new[]
{
new FrameAnimationFrame(sprites[3], 120, sprites[3].name, 3),
new FrameAnimationFrame(null, 300, "empty", -1),
new FrameAnimationFrame(sprites[4], 120, sprites[4].name, 4)
});
var idle = CreateClip(graph, "PreviewIdle", "Preview Idle Loop", FrameClipEndBehavior.Loop,
new[]
{
new FrameAnimationFrame(sprites[4], 140, sprites[4].name, 4),
new FrameAnimationFrame(sprites[5], 140, sprites[5].name, 5)
});
var finiteA = new AnimationNode(action.Id, "Finite Action", speedOverride: 2f,
internalId: "51000000000000000000000000000001");
var finiteB = new AnimationNode(withEmpty.Id, "Finite Empty",
endBehaviorOverride: FrameClipEndBehavior.HoldLastFrame,
internalId: "51000000000000000000000000000002");
var loopA = new AnimationNode(action.Id, "Loop Intro", internalId: "51000000000000000000000000000003");
var loopB = new AnimationNode(idle.Id, "Loop Terminal",
endBehaviorOverride: FrameClipEndBehavior.Loop,
internalId: "51000000000000000000000000000004");
var blocked = new AnimationNode(withEmpty.Id, "Zero Speed Block", speedOverride: 0f,
internalId: "51000000000000000000000000000005");
var clear = new AnimationNode(action.Id, "Clear Terminal",
endBehaviorOverride: FrameClipEndBehavior.Clear,
internalId: "51000000000000000000000000000006");
var hidden = new AnimationNode(idle.Id, "Hide Terminal",
endBehaviorOverride: FrameClipEndBehavior.HideTarget,
internalId: "51000000000000000000000000000007");
var finiteFlow = new AnimationFlow("PreviewFiniteFlow", "Finite Flow", finiteA.InternalId);
var loopFlow = new AnimationFlow("PreviewLoopFlow", "Loop Flow", loopA.InternalId);
var blockedFlow = new AnimationFlow("PreviewBlockedFlow", "Zero Speed Flow", blocked.InternalId);
var clearFlow = new AnimationFlow("PreviewClearFlow", "Clear Flow", clear.InternalId);
var hideFlow = new AnimationFlow("PreviewHideFlow", "Hide Flow", hidden.InternalId);
graph.Configure(
"PreviewSampleGraph",
"Phase 5 Preview Sample",
new[] { action, withEmpty, idle },
new[] { finiteA, finiteB, loopA, loopB, blocked, clear, hidden },
new[]
{
new AnimationEdge(finiteA.InternalId, finiteB.InternalId, "52000000000000000000000000000001"),
new AnimationEdge(loopA.InternalId, loopB.InternalId, "52000000000000000000000000000002")
},
new[] { finiteFlow, loopFlow, blockedFlow, clearFlow, hideFlow },
finiteFlow.Id);
var positions = new[]
{
(finiteA, new Vector2(80f, 80f)), (finiteB, new Vector2(420f, 80f)),
(loopA, new Vector2(80f, 320f)), (loopB, new Vector2(420f, 320f)),
(blocked, new Vector2(80f, 560f)), (clear, new Vector2(420f, 560f)),
(hidden, new Vector2(760f, 560f))
};
foreach (var item in positions)
{
graph.EditorData.GetOrCreateNodeData(item.Item1.InternalId, item.Item2);
}
var colors = new[]
{
new Color(0.25f, 0.65f, 0.95f), new Color(0.55f, 0.4f, 0.9f),
new Color(0.9f, 0.55f, 0.2f), new Color(0.3f, 0.75f, 0.5f), new Color(0.85f, 0.35f, 0.45f)
};
var flows = graph.Flows.ToArray();
for (var index = 0; index < flows.Length; index++)
{
graph.EditorData.GetOrCreateFlowData(flows[index].Id, colors[index]);
}
EditorUtility.SetDirty(graph);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"Frame Animation phase 5 preview sample created: {GraphPath}");
}
private static FrameClip CreateClip(
FrameAnimationGraph graph,
string id,
string displayName,
FrameClipEndBehavior endBehavior,
FrameAnimationFrame[] frames)
{
var clip = ScriptableObject.CreateInstance<FrameClip>();
clip.name = id;
clip.Configure(id, displayName, frames, 1f, endBehavior);
AssetDatabase.AddObjectToAsset(clip, graph);
return clip;
}
private static void EnsureFolder(string path)
{
var segments = path.Split('/');
var current = segments[0];
for (var index = 1; index < segments.Length; index++)
{
var next = current + "/" + segments[index];
if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(current, segments[index]);
current = next;
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f280e66f096a410eaf3eae8cd9c01134
@@ -0,0 +1,223 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace AibisDream.FrameAnimation.Editor
{
public static class FrameAnimationRuntimeSampleBuilder
{
private const string SourceTexturePath =
"Assets/GameContent/Test/AnimatorRaw/手接树叶特写.png";
private const string SampleFolder = "Assets/GameContent/Test/FrameAnimation";
private const string IntroClipPath = SampleFolder + "/Intro.asset";
private const string IdleClipPath = SampleFolder + "/Idle.asset";
private const string FlowGraphPath = SampleFolder + "/FlowSampleGraph.asset";
private const string DirectGraphPath = SampleFolder + "/DirectSampleGraph.asset";
private const string ScenePath = "Assets/Scenes/FrameAnimationRuntimeTest.unity";
[MenuItem("Tools/Frame Animation/Rebuild Runtime Sample")]
private static void BuildFromMenu()
{
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
{
return;
}
BuildFromCommandLine();
}
public static void BuildFromCommandLine()
{
EnsureFolder(SampleFolder);
var sprites = AssetDatabase.LoadAllAssetsAtPath(SourceTexturePath)
.OfType<Sprite>()
.OrderBy(sprite => ExtractFrameNumber(sprite.name))
.ToArray();
if (sprites.Length < 21)
{
throw new InvalidOperationException(
$"样例源 Texture 需要至少 21 个 Sprite,当前只找到 {sprites.Length} 个。");
}
var introClip = CreateOrLoadAsset<FrameClip>(IntroClipPath);
introClip.Configure(
"Intro",
"Intro",
CreateFrames(sprites, 0, 5),
1f,
FrameClipEndBehavior.HoldLastFrame);
EditorUtility.SetDirty(introClip);
var idleClip = CreateOrLoadAsset<FrameClip>(IdleClipPath);
idleClip.Configure(
"Idle",
"Idle",
CreateFrames(sprites, 5, 16),
1f,
FrameClipEndBehavior.Loop);
EditorUtility.SetDirty(idleClip);
var flowGraph = CreateOrLoadAsset<FrameAnimationGraph>(FlowGraphPath);
var introNodeId = FindExistingNodeId(flowGraph, introClip.Id);
var idleNodeId = FindExistingNodeId(flowGraph, idleClip.Id);
var introNode = new AnimationNode(introClip.Id, "Intro", internalId: introNodeId);
var idleNode = new AnimationNode(
idleClip.Id,
"Idle Loop",
FrameClipEndBehavior.Loop,
internalId: idleNodeId);
var flow = new AnimationFlow("IntroToIdle", "Intro To Idle", introNode.InternalId);
flowGraph.Configure(
"FlowSampleGraph",
"Flow Sample Graph",
new[] { introClip, idleClip },
new[] { introNode, idleNode },
new[] { new AnimationEdge(introNode.InternalId, idleNode.InternalId) },
new[] { flow },
flow.Id);
EditorUtility.SetDirty(flowGraph);
var directGraph = CreateOrLoadAsset<FrameAnimationGraph>(DirectGraphPath);
directGraph.Configure(
"DirectSampleGraph",
"Direct Sample Graph",
new[] { introClip, idleClip },
Array.Empty<AnimationNode>(),
Array.Empty<AnimationEdge>(),
Array.Empty<AnimationFlow>(),
idleClip.Id);
EditorUtility.SetDirty(directGraph);
AssetDatabase.SaveAssets();
BuildScene(flowGraph, directGraph);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"Frame Animation runtime sample rebuilt: {ScenePath}");
}
private static IEnumerable<FrameAnimationFrame> CreateFrames(
IReadOnlyList<Sprite> sprites,
int startIndex,
int count)
{
for (var offset = 0; offset < count; offset++)
{
var sourceIndex = startIndex + offset;
var sprite = sprites[sourceIndex];
yield return new FrameAnimationFrame(sprite, 250, sprite.name, sourceIndex);
}
}
private static void BuildScene(
FrameAnimationGraph flowGraph,
FrameAnimationGraph directGraph)
{
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
var cameraObject = new GameObject("Main Camera", typeof(Camera));
cameraObject.tag = "MainCamera";
cameraObject.transform.position = new Vector3(0f, 0f, -10f);
var camera = cameraObject.GetComponent<Camera>();
camera.orthographic = true;
camera.orthographicSize = 6f;
camera.clearFlags = CameraClearFlags.SolidColor;
camera.backgroundColor = new Color(0.08f, 0.09f, 0.12f, 1f);
var spriteObject = new GameObject("Flow Sample - SpriteRenderer", typeof(SpriteRenderer));
spriteObject.transform.position = new Vector3(-3.5f, 0f, 0f);
spriteObject.transform.localScale = Vector3.one * 0.45f;
var spritePlayer = spriteObject.AddComponent<FrameAnimationPlayer>();
spritePlayer.ConfigureForAuthoring(flowGraph, true, 1f);
var canvasObject = new GameObject(
"Direct Clip Sample Canvas",
typeof(Canvas),
typeof(CanvasScaler),
typeof(GraphicRaycaster));
var canvas = canvasObject.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
var scaler = canvasObject.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1280f, 720f);
var imageObject = new GameObject(
"Direct Idle - Image",
typeof(RectTransform),
typeof(CanvasRenderer),
typeof(Image));
imageObject.transform.SetParent(canvasObject.transform, false);
var rectTransform = imageObject.GetComponent<RectTransform>();
rectTransform.sizeDelta = new Vector2(384f, 216f);
rectTransform.anchoredPosition = new Vector2(300f, 0f);
var image = imageObject.GetComponent<Image>();
image.preserveAspect = true;
var imagePlayer = imageObject.AddComponent<FrameAnimationPlayer>();
imagePlayer.ConfigureForAuthoring(directGraph, true, 1f);
EditorSceneManager.MarkSceneDirty(scene);
if (!EditorSceneManager.SaveScene(scene, ScenePath))
{
throw new InvalidOperationException($"无法保存样例场景:{ScenePath}");
}
}
private static T CreateOrLoadAsset<T>(string path) where T : ScriptableObject
{
var asset = AssetDatabase.LoadAssetAtPath<T>(path);
if (asset != null)
{
return asset;
}
asset = ScriptableObject.CreateInstance<T>();
AssetDatabase.CreateAsset(asset, path);
return asset;
}
private static string FindExistingNodeId(FrameAnimationGraph graph, string clipId)
{
var existing = graph.Nodes.FirstOrDefault(node => node != null && node.ClipId == clipId);
return existing != null && FrameAnimationValueUtility.IsValidInternalId(existing.InternalId)
? existing.InternalId
: null;
}
private static int ExtractFrameNumber(string spriteName)
{
if (string.IsNullOrEmpty(spriteName))
{
return int.MaxValue;
}
var spaceIndex = spriteName.LastIndexOf(' ');
var dotIndex = spriteName.LastIndexOf('.');
if (spaceIndex >= 0 && dotIndex > spaceIndex &&
int.TryParse(spriteName.Substring(spaceIndex + 1, dotIndex - spaceIndex - 1), out var number))
{
return number;
}
return int.MaxValue;
}
private static void EnsureFolder(string path)
{
var segments = path.Split('/');
var current = segments[0];
for (var index = 1; index < segments.Length; index++)
{
var next = current + "/" + segments[index];
if (!AssetDatabase.IsValidFolder(next))
{
AssetDatabase.CreateFolder(current, segments[index]);
}
current = next;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More