Compare commits

...
Author SHA1 Message Date
bottlefish 9fc1ebdcb5 test(save): 更新开发跳转存档并加入 Day2Sleep 2026-07-23 00:00:36 +08:00
dingyuntian b20b936966 Merge branch 'feature/demo脚本改动0722' into 'develop'
Feature/demo脚本改动0722

See merge request aibis-dream/aibis-dream!769
2026-07-22 12:19:02 +00:00
Ovid 4a0e3ba0b9 debug 2026-07-22 20:18:29 +08:00
dingyuntian 2a1cfbc208 Merge branch 'feature/demo脚本改动0722' into 'develop'
Feature/demo脚本改动0722

See merge request aibis-dream/aibis-dream!768
2026-07-22 10:40:12 +00:00
Ovid e3762e5462 Merge branch 'develop' into feature/demo脚本改动0722 2026-07-22 17:55:35 +08:00
dingyuntian 5029a4b9b5 Merge branch '7.22春优化' into 'develop'
fix(fixsystem): 修复线缆收放状态与阶段切换竞态

See merge request aibis-dream/aibis-dream!767
2026-07-22 08:33:58 +00:00
bottlefishandCursor 3debdb39e4 merge: 合并 develop 到 7.22春优化
保留 PullUpCable 线缆收起逻辑,并接入 develop 的 BodyModule 退出清屏。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 16:30:10 +08:00
bottlefishandCursor 04edf9b355 chore(devsave): 清理 Huoshan1 开发存档快照
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 16:26:01 +08:00
bottlefishandCursor 6854679bcb yarn(peipei): 补充阶段切换时的放线指令
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 16:25:50 +08:00
bottlefishandCursor 515f57e28c yarn(huoshan): 重构任务面板流程并移除 Stage7 插线检查
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 16:25:36 +08:00
bottlefishandCursor 97ef0972fd art(fixsystem): 调整物理线缆色阶与高光参数
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 16:25:17 +08:00
bottlefishandCursor 2fb32c44e8 fix(fixsystem): 修复发条归零时充能状态未重置
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 16:25:05 +08:00
bottlefishandCursor 3d65941b03 fix(fixsystem): 修复线缆收放状态与阶段切换竞态
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 16:24:52 +08:00
Ovid 5091851ead 结尾调整进行中 2026-07-22 04:23:37 +08:00
dingyuntian 4411bfa4b6 Merge branch 'bugfix/美术素材填充' into 'develop'
fix: 保证离开BodyModule一定清屏

See merge request aibis-dream/aibis-dream!766
2026-07-21 20:06:30 +00:00
dingyuntian 02d3fc04e6 fix: 保证离开BodyModule一定清屏 2026-07-22 04:05:46 +08:00
dingyuntian ef3c14d3de Merge branch 'bugfix/美术素材填充' into 'develop'
Bugfix/美术素材填充

See merge request aibis-dream/aibis-dream!765
2026-07-21 20:02:51 +00:00
dingyuntian b7fdcf97fe fix: 插头不拔出屏幕不刷新 2026-07-22 04:01:52 +08:00
dingyuntian ba0ce38c11 art: 美术素材问题 2026-07-22 03:17:40 +08:00
dingyuntian 3b5d05260e Merge branch 'bugfix/已知问题修复' into 'develop'
Bugfix/已知问题修复

See merge request aibis-dream/aibis-dream!764
2026-07-21 18:33:48 +00:00
dingyuntian f4488a68d0 feat: 检索及彩带Timeline 2026-07-22 02:32:42 +08:00
dingyuntian c6c76666ef feat: 帧动画系统Timeline兼容 2026-07-22 02:32:04 +08:00
Ovid d655887a02 debug一些 2026-07-22 02:25:43 +08:00
Ovid adbcd89918 Merge branch 'develop' into feature/demo脚本改动0722 2026-07-22 02:10:50 +08:00
dingyuntian 9b2caf64c9 art: 部分图像配置与按钮更新 2026-07-22 01:53:07 +08:00
dingyuntian 82b20a8fab art: 火山彩带资源 2026-07-22 01:46:39 +08:00
dingyuntian 0f06041fa2 Merge branch '0720-振彤火山文本迭代' into 'develop'
火山stage8和第二天中午文本

See merge request aibis-dream/aibis-dream!763
2026-07-21 17:26:30 +00:00
dingyuntian 8a5d83d888 Merge branch 'develop' into '0720-振彤火山文本迭代'
# Conflicts:
#   Assets/Yarn/FP/FP_Day2_mid/Day2_mid.yarn
2026-07-21 17:26:22 +00:00
Ovid 71a803fd72 Merge branch 'develop' into feature/demo脚本改动0722 2026-07-22 01:25:08 +08:00
Ovid 672d554b40 PP2调整上传 2026-07-22 01:23:01 +08:00
NeologismWu 4259306e1f 0722提交
0722提交
2026-07-22 01:21:10 +08:00
dingyuntian 8b16aebc6b Merge branch '7.21春优化' into 'develop'
7.21春优化

See merge request aibis-dream/aibis-dream!762
2026-07-21 16:56:47 +00:00
bottlefish 52db661484 fix(dialog): 修复D2 Sleep换图与门观察表现 2026-07-22 00:50:59 +08:00
bottlefish aed9998d1a refactor: 拆分D2 Sleep表现并接入快照 2026-07-22 00:50:16 +08:00
dingyuntian 3919ea9a75 fix: 气泡时序问题 2026-07-22 00:07:54 +08:00
dingyuntian 3891b6bd37 editor: 编辑器微调 2026-07-22 00:06:59 +08:00
dingyuntian 9f61f53b7f art: 地铁火山更新 2026-07-22 00:06:10 +08:00
dingyuntian 6dc0983961 Merge branch 'bugfix/已知问题修复' into 'develop'
Bugfix/已知问题修复

See merge request aibis-dream/aibis-dream!761
2026-07-21 14:29:31 +00:00
dingyuntian 9627586130 Merge branch 'develop' into bugfix/已知问题修复 2026-07-21 22:28:53 +08:00
dingyuntian 28cc1127f5 fix: 部分小问题修复 2026-07-21 22:27:51 +08:00
dingyuntian 532d9fb320 Merge branch '7.20春优化' into 'develop'
revert(huoshan): 将释放log表现回退至6月版本

See merge request aibis-dream/aibis-dream!760
2026-07-21 14:25:04 +00:00
bottlefishandCursor 0d59e7d91e chore(devsave): 移除 Day2Sleep 开发存档条目
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 22:16:56 +08:00
bottlefishandCursor 859d780b78 yarn(FP_Day2_sleep): 简化风筝放手与推门演出并更新梦境素材
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 22:16:46 +08:00
bottlefishandCursor 7ee6cba2ae fix(yarn): 修正 Stage6 hide_full_screen 拼写
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 22:16:46 +08:00
bottlefishandCursor 6159704116 feat(cable): 线缆暖色配色与拉紧驱动线盘转动
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 22:16:41 +08:00
bottlefishandCursor 74f5989802 fix(memory): 调整记忆播放结束复位时序
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 22:16:34 +08:00
bottlefishandCursor 0b848d8040 fix(peipei): 收紧 Eye 聚焦平移边界与视口计算
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 22:13:22 +08:00
bottlefishandCursor 96b309992c refactor(save): 移除 Showcase 快照持久化
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 22:13:11 +08:00
bottlefish 1547055cc2 Revert "scene(peipei): 添加Bloom并调整维修场景布局"
This reverts commit 9ef6bc9bff.
2026-07-21 18:51:48 +08:00
bottlefishandCursor 3d119dcd92 refactor(knob): 移除旋钮旋转光标交互逻辑
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 18:50:57 +08:00
bottlefishandCursor c4ef30b151 style(screen): 调整维修面板标题与日志文本配色
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 18:50:48 +08:00
bottlefishandCursor 12c5735466 feat(ui): 添加 TerminalText 正文颜色设置方法
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 18:50:03 +08:00
bottlefishandCursor 35451363a5 scene(yingli): 调整场景物件位置
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 17:48:30 +08:00
bottlefishandCursor 9ef6bc9bff scene(peipei): 添加Bloom并调整维修场景布局
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 17:48:27 +08:00
bottlefishandCursor 97f0fabd3c scene(clinic): 调整英里初登场等待与Timeline
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 17:44:26 +08:00
bottlefishandCursor 018e9d3b52 fix(bar): 修复醉酒状态切换抽搐并收紧过渡
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 17:44:17 +08:00
bottlefishandCursor c0e58d56b4 feat(scene-mgmt): 调整轱辘拉锯为发力微颤
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 17:43:00 +08:00
bottlefishandCursor cffb99a52c fix(cutline): 剪线后立即禁用避免重复触发
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 17:42:59 +08:00
bottlefishandCursor 597e97c5d7 fix(bodymodule): 切割入场同步全部模块CutText
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 17:42:58 +08:00
bottlefishandCursor 7a8f7dbbd7 fix(cable): 强制收线改用运动学收拢避免甩鞭
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 17:42:50 +08:00
bottlefishandCursor 8fee89db36 fix(heatmap): 按Sprite几何中心旋转风扇避免散架
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 17:42:44 +08:00
bottlefishandCursor 046b1a9efb fix(memory): 优化彩色褪色扫光并复位拉杆视觉
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 17:41:46 +08:00
bottlefishandCursor c7c0e96110 scene(open): 固定Emergency气泡Timeline时长
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 01:41:40 +08:00
bottlefish f6f87c3109 Merge branch 'develop' into 7.20春优化 2026-07-21 01:39:29 +08:00
bottlefishandCursor 98dfb644c5 feat(scene-mgmt): 放缓轱辘拉锯幅度与节奏
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 01:20:13 +08:00
bottlefishandCursor 845f46b924 fix(memory): 记忆结束增加短闪雪花过渡
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 01:20:04 +08:00
bottlefishandCursor 5f31cd5635 fix(peipei-uf): 重绑垂手脸图并加固faceIn
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 01:19:16 +08:00
bottlefishandCursor 1e8ec2e64f feat(eye): 眨眼仅用于想象上色并关闭自动眨眼
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 01:18:46 +08:00
bottlefishandCursor e7a0089a46 feat(cutline): Timeline黑场交接UI避免剪线露头
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 01:18:16 +08:00
bottlefishandCursor 2ac0b4695d feat(fix-panel): Warning回落增加闪烁淡出并对齐等待
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 01:18:03 +08:00
bottlefishandCursor ecfefa5b87 fix(bodymodule): 切割时按舱外状态同步CutText
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 01:17:52 +08:00
bottlefishandCursor 3164fd7d8c feat(shock): 拉杆接入EventTriggerEx并统一光标
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 01:17:41 +08:00
bottlefishandCursor dc0488f085 fix(eventsystem): 修复锁定时松手未清理导致光标卡住
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 01:16:57 +08:00
bottlefishandCursor 164e3dd50e revert(huoshan): 将释放log表现回退至6月版本
保留 Stage6 当前台词,仅恢复粒子与完成表现及 start_expression 参数。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 00:03:31 +08:00
dingyuntian c985cc2843 Merge branch 'bugfix/已知问题修复' into 'develop'
fix: 佩佩场景部分位置

See merge request aibis-dream/aibis-dream!759
2026-07-20 15:26:38 +00:00
dingyuntian b0ee02c62c fix: 佩佩场景部分位置 2026-07-20 23:26:01 +08:00
dingyuntian a043f27035 Merge branch 'bugfix/已知问题修复' into 'develop'
Bugfix/已知问题修复

See merge request aibis-dream/aibis-dream!758
2026-07-20 15:16:29 +00:00
dingyuntian a9f58fc683 fix: 已知问题修复 2026-07-20 23:15:34 +08:00
dingyuntian 94823354b5 feat: 延时auto_next 2026-07-20 22:34:23 +08:00
dingyuntian 4ec5e0b442 Merge branch 'feature/振彤Yarn提交' into 'develop'
Feature/振彤yarn提交

See merge request aibis-dream/aibis-dream!757
2026-07-20 10:14:24 +00:00
dingyuntian 4d44ca387e docs: 新增Demo QA测试说明文档 2026-07-20 18:12:56 +08:00
dingyuntian 1171ad9e87 build: 更新版本号至0.6.0.1-20260720 2026-07-20 18:12:32 +08:00
dingyuntian 1318636bee yarn: 补Day2_mid节点跳转对话 2026-07-20 18:11:26 +08:00
dingyuntian f17a8ab8dd scene(peipei): 调整冷色特效参数与线缆位置 2026-07-20 18:11:05 +08:00
dingyuntian b70f2614af art: 移动光标贴图至Common并更新 2026-07-20 18:10:42 +08:00
dingyuntian 517f4d956a refactor(savesystem): 正式存档目录更名为demo_saves 2026-07-20 18:09:54 +08:00
dingyuntian 46c47ea4be Merge branch 'feature/振彤Yarn提交' into 'develop'
yarn: 文本修改

See merge request aibis-dream/aibis-dream!756
2026-07-20 07:01:25 +00:00
dingyuntian 41d3e07a55 yarn: 文本修改 2026-07-20 14:41:08 +08:00
dingyuntian 2cd0ae92be Merge branch 'feature/demo调整0719true' into 'develop'
Feature/demo调整0719true

See merge request aibis-dream/aibis-dream!755
2026-07-19 19:59:47 +00:00
dingyuntian 3491367bd9 Merge branch '7.19春优化' into 'develop'
fix(heatmap): 低温段改为蓝色噪点并保留发热色带

See merge request aibis-dream/aibis-dream!754
2026-07-19 19:55:33 +00:00
bottlefish dc6fe93afb 提交结尾场景为天桥 2026-07-20 03:53:07 +08:00
Ovid ff89ffe552 GTS幽灵音量+大 2026-07-20 03:52:09 +08:00
bottlefish bd30cdabd0 Merge branch 'develop' into 7.19春优化 2026-07-20 03:50:37 +08:00
Ovid 6ff260150d HS1病例补充动态 2026-07-20 03:50:00 +08:00
bottlefishandCursor 32d2fcc0a2 yarn(dream): 优化 Day2 梦境演出与放手流程
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 03:47:13 +08:00
bottlefishandCursor cca5404f0c feat(scene-mgmt): 支持无线轱辘分层与加速晃动
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 03:47:05 +08:00
bottlefishandCursor 6e5e5d5790 art(dream): 添加 Day2 梦境 aseprite 源文件
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 03:46:54 +08:00
bottlefishandCursor ad759a6e83 art(dream): 新增 Day2 梦境演出贴图并更新素材
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 03:46:33 +08:00
bottlefishandCursor 65205e2552 yarn(huoshan): 简化 Stage7 直接回诊室流程
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 03:46:11 +08:00
dingyuntian cb255f8ca5 Merge branch 'bugfix/佩佩动画迁移' into 'develop'
Bugfix/佩佩动画迁移

See merge request aibis-dream/aibis-dream!753
2026-07-19 19:39:32 +00:00
dingyuntian 5f30f356ec fix: 章节本地化 2026-07-20 03:38:59 +08:00
dingyuntian 3c789cbd65 art: 火山动画及行人动画 2026-07-20 03:38:47 +08:00
Ovid fabef0145d +上没有女孩的椅子 2026-07-20 03:34:03 +08:00
Ovid 785add905d HS1病例上传 2026-07-20 03:20:15 +08:00
Ovid 2fe558846b PP2分析上传 2026-07-20 03:10:49 +08:00
dingyuntian 1fa96151d6 Merge branch 'bugfix/佩佩动画迁移' into 'develop'
Bugfix/佩佩动画迁移

See merge request aibis-dream/aibis-dream!752
2026-07-19 19:01:06 +00:00
dingyuntian c8830d9346 yarn: 多余fadeout 2026-07-20 03:00:19 +08:00
dingyuntian ecf75ae6a6 art: 光标更新 2026-07-20 03:00:04 +08:00
dingyuntian 5f2f6e6365 art: 清除背景残留 2026-07-20 02:59:24 +08:00
dingyuntian 153d36f884 art: 清理废弃资源 2026-07-20 02:59:06 +08:00
NeologismWu 6481858017 Update Day2_mid.yarn 2026-07-20 02:33:35 +08:00
NeologismWu c9ca07a0b3 火山stage8和第二天中午文本
111
2026-07-20 02:29:02 +08:00
Ovid cb1f5b90a7 恢复病历图界面缺失的图层 2026-07-20 02:27:36 +08:00
Ovid 9f4c473fac 序章调整 2026-07-20 02:22:07 +08:00
dingyuntian 4eb72911e1 Merge branch 'bugfix/佩佩动画迁移' into 'develop'
Bugfix/佩佩动画迁移

See merge request aibis-dream/aibis-dream!751
2026-07-19 18:13:56 +00:00
dingyuntian 5f4f9c1721 art(ui): 调整对话气泡图片透明度 2026-07-20 02:10:25 +08:00
dingyuntian 202cdb4c29 feat(peipei): 佩佩动画状态迁移到Flow图 2026-07-20 02:10:25 +08:00
dingyuntian 1ade595f9d yarn(dialog): generate_PunchTape命令改用定义ID参数 2026-07-20 02:10:24 +08:00
dingyuntian 3ed84f79b1 refactor(punchtape): 打孔带数据改为Catalog定义驱动并迁移脚本目录 2026-07-20 02:10:24 +08:00
bottlefishandCursor 1444e43449 chore(scene): 调整 Playtest 结束流程
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 01:27:30 +08:00
bottlefishandCursor 6e7bb3f637 art(bridge): 更新下雪序列帧并接入天桥场景
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 01:22:04 +08:00
bottlefishandCursor 5ab204b476 fix(yarn): 修正 Day2 night BE detour 节点名
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 01:21:55 +08:00
bottlefishandCursor 84192cb7c4 yarn(dialog): 优化 Day2 梦境 Bloom 与全屏演出流程
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 01:21:48 +08:00
bottlefishandCursor c98644c723 art(dream): 新增 Day2 梦境天花板风扇与星图素材
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 01:21:23 +08:00
bottlefishandCursor 15aba8e805 feat(dialog): 新增全屏序列帧与模糊 Yarn 指令
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 01:21:07 +08:00
bottlefishandCursor 90c75d1f45 feat(scene-mgmt): 全屏图柔焦模糊与序列帧播放
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 01:20:52 +08:00
bottlefishandCursor f8821b1b3e fix(fix-system): 切割进入时支持 Cut Text 闪烁
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 01:20:25 +08:00
bottlefishandCursor 41ca1927a0 fix(heatmap): 低温段改为蓝色噪点并保留发热色带
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 01:20:12 +08:00
dingyuntian 6bf967e8b3 Merge branch 'feature/demo脚本调整0719' into 'develop'
Feature/demo脚本调整0719

See merge request aibis-dream/aibis-dream!750
2026-07-19 15:47:28 +00:00
dingyuntian 2514eb0a8d Merge branch 'bugfix/小问题修复' into 'develop'
Bugfix/小问题修复

See merge request aibis-dream/aibis-dream!749
2026-07-19 15:44:28 +00:00
dingyuntian f444978717 fix: 火山表情修复 2026-07-19 23:43:53 +08:00
Ovid 554de790c1 Merge branch 'develop' into feature/demo脚本调整0719 2026-07-19 23:08:02 +08:00
Ovid 1764714cb5 PP2部分调整 2026-07-19 23:07:33 +08:00
dingyuntian a6d541383b Merge branch 'develop' into bugfix/小问题修复 2026-07-19 23:04:53 +08:00
dingyuntian 00b5646b44 feat: 打孔带更新 2026-07-19 23:04:04 +08:00
dingyuntian 9403bd9623 Merge branch '7.17春优化' into 'develop'
7.17春优化

See merge request aibis-dream/aibis-dream!748
2026-07-19 15:00:09 +00:00
bottlefish 14fd64e7bd fix(dialog): 修复 Day2 梦境终端过渡异常 2026-07-19 22:39:19 +08:00
bottlefish 5a37f529ab fix(dialog): 修复快进时后处理渐变回写 2026-07-19 22:36:45 +08:00
bottlefish 06d6127c69 chore(merge): 合并 develop 并整合 Day2 梦境剧情与演出 2026-07-19 21:25:30 +08:00
Ovid 53587c413e D1S手账串接 2026-07-19 20:50:22 +08:00
bottlefish 5408fd5320 build(config): 改用本地 Unity MCP 包 2026-07-19 20:42:51 +08:00
bottlefish 1f6208a094 yarn(dialog): 调整 Day2 天桥后处理权重 2026-07-19 20:42:19 +08:00
bottlefish 772a6bbbe8 yarn(dialog): 优化 Day2 梦境演出流程 2026-07-19 20:41:35 +08:00
bottlefish bfc61a0097 feat(scene-mgmt): 重构梦境门海浪切换演出 2026-07-19 20:40:20 +08:00
bottlefish bfbfe494c4 art(scene-mgmt): 更新梦境门框与海浪动画资源 2026-07-19 20:38:28 +08:00
bottlefish 297075e0f0 feat(dialog): 添加后处理参数 Yarn 指令 2026-07-19 20:37:32 +08:00
dingyuntian 71d248c2e6 Merge branch '0718-振彤优化' into 'develop'
Update Day2_mid.yarn

See merge request aibis-dream/aibis-dream!745
2026-07-19 12:37:31 +00:00
bottlefish 7034b82aa4 perf(scene-mgmt): 优化星空失焦模糊效果 2026-07-19 20:36:06 +08:00
bottlefish eb127371f1 fix(localization): 补充终端特殊字符字形 2026-07-19 20:32:48 +08:00
bottlefish cec2dae5e0 yarn(dialog): 为火山头冷台词添加振动演出 2026-07-19 20:32:09 +08:00
bottlefish ba19c2eff5 feat(scene-mgmt): 添加角色立绘持续振动指令 2026-07-19 20:31:16 +08:00
bottlefish 7fa12e3d46 style(ui): 调整任务条目文字颜色 2026-07-19 20:30:29 +08:00
bottlefish bb6b3dd599 feat(fix-system): 区分维修终端错误提示颜色 2026-07-19 20:30:05 +08:00
bottlefish d7030f36d2 feat(fix-system): 优化低温热力图视觉表现 2026-07-19 20:29:30 +08:00
bottlefish 94a817f5fa feat(tarot): 添加塔罗展示背景压暗 2026-07-19 20:29:01 +08:00
bottlefish bfdcb41116 art(tarot): 更新塔罗牌面资源 2026-07-19 20:28:25 +08:00
bottlefish 6ace48dba2 art(scene-bar): 修正酒吧角色动画表现 2026-07-19 20:27:00 +08:00
dingyuntian 2999f753f7 fix: 进入记忆界面相关修改 2026-07-19 19:41:13 +08:00
dingyuntian c3da27d07c fix: 梦境对话跳Warning 2026-07-19 19:05:06 +08:00
dingyuntian 05fe1ebb60 Merge branch 'feature/demo脚本调整' into bugfix/小问题修复 2026-07-19 18:15:58 +08:00
dingyuntian ad5111a206 fix: 字符缺失问题 2026-07-19 18:14:17 +08:00
dingyuntian 592d3a36dd art: 音量条 2026-07-19 17:23:55 +08:00
dingyuntian 3e2a24421f Merge branch 'feature/demo脚本调整' into 'develop'
Feature/demo脚本调整

See merge request aibis-dream/aibis-dream!747
2026-07-18 16:14:23 +00: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
bottlefish ce00539c18 feat(save): 迁移并完善开发跳转存档 2026-07-18 22:15:52 +08:00
NeologismWu 78d5ad33f0 Update Stage6.yarn 2026-07-18 21:48:06 +08:00
NeologismWu 2fec120568 Update Day2_mid.yarn 2026-07-18 20:45:41 +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 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
1259 changed files with 144961 additions and 30625 deletions
@@ -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` 参数错误
+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
+3
View File
@@ -78,6 +78,9 @@ crashlytics-build.properties
# Local Save Files
/[Aa]ssets/[Ss]treamingAssets/SaveFiles/*
# DevSaveFiles lives outside Assets; Unity metadata from the old location is not needed.
/DevSaveFiles/**/*.meta
# Never ignore DLLs in the FMOD subfolder.
!/[Aa]ssets/Plugins/FMOD/**/lib/*
@@ -15,7 +15,7 @@ MonoBehaviour:
m_DefaultGroup: 09546aca93a801441859af2a811fe53b
m_currentHash:
serializedVersion: 2
Hash: 1616e06b587c7889ae21faab8aef8e68
Hash: e92b9b9776d0601449874b8c1b0ed1ae
m_OptimizeCatalogSize: 0
m_BuildRemoteCatalog: 0
m_BundleLocalCatalog: 0
@@ -59,7 +59,6 @@ MonoBehaviour:
- {fileID: 11400000, guid: 6d259d339c6615248b893837398c1438, type: 2}
- {fileID: 11400000, guid: 8718a71c99923e247a960cbf4e4795f5, type: 2}
- {fileID: 11400000, guid: 1e521cfc13b314642b1e682e3be7ea17, 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}
@@ -68,6 +67,7 @@ MonoBehaviour:
- {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
@@ -52,6 +52,11 @@ MonoBehaviour:
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:
@@ -82,6 +82,16 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1799dbcdb4a76624bb89622707581f92
m_Address: "Timeline/\u82F1\u91CC\u521D\u767B\u573A"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3d0198d0d6d221948a3d5ddf9cd2c34f
m_Address: "FrameAnimation/\u8BCA\u5BA4\u5916\u706B\u5C71"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
@@ -92,6 +92,11 @@ MonoBehaviour:
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:
@@ -32,8 +32,8 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 809f95af8dd580448ab804b94fbd9cb2
m_Address: "Animation/\u4F69\u4F69"
- m_GUID: 26105571c9644aa4194f2b981ee33a58
m_Address: "FrameAnimation/\u4F69\u4F69"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
@@ -22,23 +22,8 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2d4ccf1c582289344976ab5c3af05515
m_Address: "Animation/\u5730\u94C1\u533B\u751F"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0057bf4b5fe16aa4b8c7db27d0ddfc0a
m_Address: "Animation/\u5730\u94C1\u706B\u5C71"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 40f07326ace570241bd93fcfc3decb87
m_Address: "Animation/\u5730\u94C1\u706B\u5C71\u6697"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a63b52b3e8d9e0441aaff086b9c1ae6b
m_Address: "Animation/\u5730\u94C1\u533B\u751F\u6697"
- m_GUID: b0ee8d48dab31814da87651bba9b5041
m_Address: "FrameAnimation/\u5730\u94C1\u533B\u751F"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
@@ -57,6 +42,11 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b02d1e0ae4bc971408a0b6a667597624
m_Address: "FrameAnimation/\u5730\u94C1\u706B\u5C71"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
@@ -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
Binary file not shown.
@@ -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
+123
View File
@@ -0,0 +1,123 @@
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using AibisDream.Utility;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using Yarn.Markup;
using Yarn.Unity;
namespace AibisDream.SystemEditor.Tests
{
public sealed class AutoNextMetadataTests
{
[Test]
public void AutoNextWithoutParameter_UsesExistingFixedDelay()
{
var lineInfo = LineInfo.Generate(CreateLine("auto_next"));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.Null);
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay));
}
[Test]
public void AutoNextWithParameter_UsesSpecifiedSeconds()
{
var lineInfo = LineInfo.Generate(CreateLine("auto_next:4.5"));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(4.5f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(4500));
}
[Test]
public void AutoNextWithZeroDelay_IsValid()
{
var lineInfo = LineInfo.Generate(CreateLine("auto_next:0"));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(0f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.Zero);
}
[Test]
public void AutoNextParameter_UsesInvariantCulture()
{
var originalCulture = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
var lineInfo = LineInfo.Generate(CreateLine("auto_next:4.5"));
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(4.5f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(4500));
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
[TestCase("auto_next:")]
[TestCase("auto_next:abc")]
[TestCase("auto_next:-1")]
[TestCase("auto_next:NaN")]
[TestCase("auto_next:Infinity")]
[TestCase("auto_next:2147484")]
public void AutoNextWithInvalidParameter_FallsBackToExistingFixedDelay(string metadata)
{
LogAssert.Expect(LogType.Warning, new Regex("auto_next 参数无效"));
var lineInfo = LineInfo.Generate(CreateLine(metadata));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.Null);
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay));
}
[TestCase("auto_next_extra")]
[TestCase("auto_next_extra:4.5")]
[TestCase("AUTO_NEXT")]
[TestCase("AUTO_NEXT:4.5")]
public void SimilarMetadata_IsNotRecognized(string metadata)
{
var line = CreateLine(metadata);
Assert.That(line.IsAutoSkipLine(), Is.False);
Assert.That(line.TryGetAutoNextDelaySeconds(out _), Is.False);
}
[Test]
public void ParameterizedTag_TakesPrecedenceOverPlainTag()
{
var lineInfo = LineInfo.Generate(CreateLine("auto_next", "auto_next:2.5"));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(2.5f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(2500));
}
[Test]
public void MultipleParameterizedTags_UseFirstAndWarn()
{
LogAssert.Expect(LogType.Warning, new Regex("存在多个 auto_next 参数标签"));
var lineInfo = LineInfo.Generate(CreateLine("auto_next:2.5", "auto_next:4.5"));
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(2.5f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(2500));
}
private static LocalizedLine CreateLine(params string[] metadata)
{
return new LocalizedLine
{
TextID = "line:auto-next-test",
Metadata = metadata,
Text = new MarkupParseResult("Test line", new List<MarkupAttribute>())
};
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: dd06484c1f627d249ad5a16558c9cdb9
guid: d476e493adf4855468ce46409dae6661
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -148,9 +148,19 @@ namespace AibisDream.SystemEditor
});
// 右侧 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;
mainContainer.Add(_graphView);
graphViewWrapper.Add(_graphView);
mainContainer.Add(graphViewWrapper);
root.Add(mainContainer);
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 33e48b17003f9a340a079e8b5993c786
guid: 9e244d3d62dec3f4f881f7e681da8c03
folderAsset: yes
DefaultImporter:
externalObjects: {}
@@ -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);
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: a351e509c8dd5944f9e6406f59ea5c0b
guid: a21095473b1495440acab244f9e470c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("AibisDream.FrameAnimation.Tests.EditMode")]
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 6b8d5e422f3d4df1a53d34d6ca2aa8ad
guid: 8edeae42e1831084cbf05db9d9e5d1e2
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -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 clip != null && clip.HasStandaloneImportSource ? "Standalone Source" : "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 形式、且不由 Graph ImportSource 管理的 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,353 @@
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 interface IFrameAnimationImportPreviewSink
{
void AddIssue(FrameAnimationImportIssue issue);
void AddSpriteDiff(FrameAnimationSpriteDiff diff);
}
internal sealed class FrameAnimationImportSourcePreview : IFrameAnimationImportPreviewSink
{
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,627 @@
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)));
}
var standaloneOwners = AssetDatabase.FindAssets("t:FrameClip")
.Select(guid => AssetDatabase.LoadAssetAtPath<FrameClip>(AssetDatabase.GUIDToAssetPath(guid)))
.Where(clip => clip?.StandaloneImportSource != null &&
clip.StandaloneImportSource.ManageSpriteSlicing &&
clip.StandaloneImportSource.Texture != null)
.ToArray();
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}"))));
}
var standaloneConflicts = standaloneOwners.Where(clip =>
clip.StandaloneImportSource.Texture == pair.Key.Texture).ToArray();
if (standaloneConflicts.Length > 0)
{
pair.Value.AddIssue(Error(pair.Key, FrameAnimationImportIssueCode.TextureOwnershipConflict,
$"Texture '{pair.Key.Texture.name}' 已被独立 Clip 来源占用:" +
string.Join(", ", standaloneConflicts.Select(clip => clip.name))));
}
}
}
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,91 @@
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 (player.GetComponent<FrameClipPlayer>() != null)
{
EditorGUILayout.HelpBox(
"同一 GameObject 不能同时包含 FrameAnimationPlayer 和 FrameClipPlayer。",
MessageType.Error);
}
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,603 @@
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 =>
TryResolvePreviewClip(clip, 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 bool TryResolvePreviewClip(
FrameClip clip,
out ResolvedPlaybackPlan resolvedPlan,
out FrameAnimationPlaybackError error)
{
return graph != null
? FrameAnimationResolver.TryResolve(graph, clip.Id, default, out resolvedPlan, out error)
: FrameAnimationResolver.TryResolveClip(clip, default, out resolvedPlan, out error);
}
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;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 679ccd2bc27e67c468f002c1282b6045
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,529 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using UnityEditor;
using UnityEditor.U2D.Sprites;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
internal readonly struct FrameAnimationSpriteSourceContext
{
public string SourceId { get; }
public Texture2D Texture { get; }
public TextAsset AsepriteJson { get; }
public Vector2 Pivot { get; }
public bool ManageSpriteSlicing { get; }
public FrameAnimationSpriteSourceContext(
string sourceId,
Texture2D texture,
TextAsset asepriteJson,
Vector2 pivot,
bool manageSpriteSlicing)
{
SourceId = sourceId ?? string.Empty;
Texture = texture;
AsepriteJson = asepriteJson;
Pivot = pivot;
ManageSpriteSlicing = manageSpriteSlicing;
}
public static FrameAnimationSpriteSourceContext From(FrameAnimationImportSource source)
{
return new FrameAnimationSpriteSourceContext(
source?.InternalId, source?.Texture, source?.AsepriteJson,
source?.Pivot ?? new Vector2(0.5f, 0.5f), source?.ManageSpriteSlicing == true);
}
}
internal sealed class FrameAnimationSpritePlan
{
public string TexturePath { get; }
public IReadOnlyList<SpriteRect> ExistingRects { get; }
public IReadOnlyList<SpriteRect> PlannedRects { get; }
public IReadOnlyDictionary<int, string> SpriteNamesBySourceIndex { get; }
public IReadOnlyDictionary<int, Sprite> ReadOnlySpritesBySourceIndex { get; }
public FrameAnimationSpritePlan(
string texturePath,
IReadOnlyList<SpriteRect> existingRects,
IReadOnlyList<SpriteRect> plannedRects,
IReadOnlyDictionary<int, string> spriteNamesBySourceIndex,
IReadOnlyDictionary<int, Sprite> readOnlySpritesBySourceIndex)
{
TexturePath = texturePath ?? string.Empty;
ExistingRects = existingRects ?? Array.Empty<SpriteRect>();
PlannedRects = plannedRects ?? Array.Empty<SpriteRect>();
SpriteNamesBySourceIndex = spriteNamesBySourceIndex ?? new Dictionary<int, string>();
ReadOnlySpritesBySourceIndex = readOnlySpritesBySourceIndex ?? new Dictionary<int, Sprite>();
}
public bool MatchesSprite(int sourceIndex, Sprite sprite)
{
if (sprite == null)
{
return false;
}
if (ReadOnlySpritesBySourceIndex.TryGetValue(sourceIndex, out var readOnlySprite))
{
return readOnlySprite == sprite;
}
return SpriteNamesBySourceIndex.TryGetValue(sourceIndex, out var spriteName) &&
sprite.name == spriteName;
}
public bool TryResolveSprite(
int sourceIndex,
IReadOnlyDictionary<string, Sprite> writableSprites,
out Sprite sprite)
{
if (ReadOnlySpritesBySourceIndex.TryGetValue(sourceIndex, out sprite))
{
return sprite != null;
}
if (SpriteNamesBySourceIndex.TryGetValue(sourceIndex, out var spriteName) &&
writableSprites != null && writableSprites.TryGetValue(spriteName, out sprite))
{
return sprite != null;
}
sprite = null;
return false;
}
}
internal sealed class FrameAnimationTextureSnapshot
{
public string TexturePath { get; }
public TextureImporterType TextureType { get; }
public SpriteImportMode SpriteImportMode { get; }
public IReadOnlyList<SpriteRect> SpriteRects { get; }
public FrameAnimationTextureSnapshot(
string texturePath,
TextureImporterType textureType,
SpriteImportMode spriteImportMode,
IReadOnlyList<SpriteRect> spriteRects)
{
TexturePath = texturePath;
TextureType = textureType;
SpriteImportMode = spriteImportMode;
SpriteRects = spriteRects;
}
}
internal static class FrameAnimationSpriteUtility
{
public static FrameAnimationSpritePlan BuildPlan(
FrameAnimationImportSource source,
AsepriteSourceDocument document,
FrameAnimationImportSourcePreview preview)
{
return BuildPlan(FrameAnimationSpriteSourceContext.From(source), document, preview);
}
public static FrameAnimationSpritePlan BuildPlan(
FrameAnimationSpriteSourceContext source,
AsepriteSourceDocument document,
IFrameAnimationImportPreviewSink preview,
IReadOnlyCollection<int> includedSourceIndices = null)
{
var texturePath = AssetDatabase.GetAssetPath(source.Texture);
var importer = AssetImporter.GetAtPath(texturePath) as TextureImporter;
if (importer == null)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.TextureImporterInvalid,
$"无法读取 TextureImporter{texturePath}"));
return new FrameAnimationSpritePlan(texturePath, null, null, null, null);
}
var existingRects = source.ManageSpriteSlicing && importer.spriteImportMode != SpriteImportMode.Multiple
? Array.Empty<SpriteRect>()
: ReadSpriteRects(importer);
return source.ManageSpriteSlicing
? BuildWritablePlan(source, document, preview, texturePath, existingRects, includedSourceIndices)
: BuildReadOnlyPlan(source, document, preview, texturePath, existingRects, includedSourceIndices);
}
public static Rect ToUnityRect(RectInt asepriteRect, int textureHeight)
{
return new Rect(
asepriteRect.x,
textureHeight - asepriteRect.y - asepriteRect.height,
asepriteRect.width,
asepriteRect.height);
}
public static FrameAnimationTextureSnapshot CaptureSnapshot(FrameAnimationSpritePlan plan)
{
var importer = AssetImporter.GetAtPath(plan.TexturePath) as TextureImporter;
if (importer == null)
{
throw new InvalidOperationException($"无法读取 TextureImporter{plan.TexturePath}");
}
return new FrameAnimationTextureSnapshot(
plan.TexturePath,
importer.textureType,
importer.spriteImportMode,
CloneRects(ReadSpriteRects(importer)));
}
public static void ApplyPlan(FrameAnimationSpritePlan plan)
{
var importer = AssetImporter.GetAtPath(plan.TexturePath) as TextureImporter;
if (importer == null)
{
throw new InvalidOperationException($"无法读取 TextureImporter{plan.TexturePath}");
}
Undo.RecordObject(importer, "Refresh Frame Animation Sprite Slicing");
importer.textureType = TextureImporterType.Sprite;
importer.spriteImportMode = SpriteImportMode.Multiple;
WriteSpriteRects(importer, plan.PlannedRects);
importer.SaveAndReimport();
}
public static void RestoreSnapshot(FrameAnimationTextureSnapshot snapshot)
{
var importer = AssetImporter.GetAtPath(snapshot.TexturePath) as TextureImporter;
if (importer == null)
{
return;
}
importer.textureType = snapshot.TextureType;
importer.spriteImportMode = snapshot.SpriteImportMode;
WriteSpriteRects(importer, snapshot.SpriteRects);
importer.SaveAndReimport();
}
public static IReadOnlyDictionary<string, Sprite> LoadSpritesByName(string texturePath)
{
return AssetDatabase.LoadAllAssetsAtPath(texturePath)
.OfType<Sprite>()
.GroupBy(sprite => sprite.name)
.Where(group => group.Count() == 1)
.ToDictionary(group => group.Key, group => group.Single());
}
public static string ComputeSourceHash(
FrameAnimationImportSource source,
IReadOnlyList<SpriteRect> spriteRects = null)
{
return ComputeSourceHash(FrameAnimationSpriteSourceContext.From(source), spriteRects);
}
public static string ComputeSourceHash(
FrameAnimationSpriteSourceContext source,
IReadOnlyList<SpriteRect> spriteRects = null)
{
if (source.Texture == null || source.AsepriteJson == null)
{
return string.Empty;
}
var texturePath = AssetDatabase.GetAssetPath(source.Texture);
var builder = new StringBuilder();
builder.AppendLine("FrameAnimationImportHash:v2");
builder.AppendLine(source.AsepriteJson.text ?? string.Empty);
builder.AppendLine(AssetDatabase.AssetPathToGUID(texturePath));
builder.AppendLine(AssetDatabase.GetAssetDependencyHash(texturePath).ToString());
builder.AppendLine(source.Pivot.x.ToString("R", CultureInfo.InvariantCulture));
builder.AppendLine(source.Pivot.y.ToString("R", CultureInfo.InvariantCulture));
builder.AppendLine(source.ManageSpriteSlicing ? "write" : "read");
var rects = spriteRects;
if (rects == null && AssetImporter.GetAtPath(texturePath) is TextureImporter importer)
{
rects = ReadSpriteRects(importer);
}
foreach (var rect in (rects ?? Array.Empty<SpriteRect>()).OrderBy(item => item.name))
{
builder.Append(rect.name).Append('|')
.Append(rect.spriteID).Append('|')
.Append(rect.rect.x.ToString("R", CultureInfo.InvariantCulture)).Append(',')
.Append(rect.rect.y.ToString("R", CultureInfo.InvariantCulture)).Append(',')
.Append(rect.rect.width.ToString("R", CultureInfo.InvariantCulture)).Append(',')
.Append(rect.rect.height.ToString("R", CultureInfo.InvariantCulture)).Append('|')
.Append(rect.pivot.x.ToString("R", CultureInfo.InvariantCulture)).Append(',')
.Append(rect.pivot.y.ToString("R", CultureInfo.InvariantCulture)).AppendLine();
}
using var sha = SHA256.Create();
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString()));
return BitConverter.ToString(bytes).Replace("-", string.Empty).ToLowerInvariant();
}
private static FrameAnimationSpritePlan BuildWritablePlan(
FrameAnimationSpriteSourceContext source,
AsepriteSourceDocument document,
IFrameAnimationImportPreviewSink preview,
string texturePath,
IReadOnlyList<SpriteRect> existingRects,
IReadOnlyCollection<int> includedSourceIndices)
{
var planned = CloneRects(existingRects).ToList();
var originalRects = planned.ToArray();
var assigned = new HashSet<SpriteRect>();
var spriteNamesBySourceIndex = new Dictionary<int, string>();
foreach (var slot in EnumerateSlots(document, includedSourceIndices))
{
var unityRect = ToUnityRect(slot.Rect, source.Texture.height);
SpriteRect matched = null;
var slotHasError = false;
foreach (var aliasName in slot.FrameNames)
{
var nameMatches = planned.Where(rect => rect.name == aliasName).ToArray();
if (nameMatches.Length > 1)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.SpriteMatchFailed,
$"Texture 中存在多个名为 '{aliasName}' 的 SpriteRect。"));
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
FrameAnimationSpriteChangeKind.Error,
slot.CanonicalFrameName,
unityRect,
"SpriteRect 名称重复",
slot.FrameNames));
slotHasError = true;
break;
}
if (nameMatches.Length == 1 && !assigned.Contains(nameMatches[0]))
{
matched = nameMatches[0];
break;
}
}
if (slotHasError)
{
continue;
}
if (matched == null)
{
var rectMatches = planned.Where(rect => !assigned.Contains(rect) && RectEquals(rect.rect, unityRect))
.ToArray();
if (rectMatches.Length > 1)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.SpriteMatchFailed,
$"共享帧 '{slot.CanonicalFrameName}' 的 rect 对应多个现有 SpriteRect。"));
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
FrameAnimationSpriteChangeKind.Error,
slot.CanonicalFrameName,
unityRect,
"SpriteRect rect 匹配不唯一",
slot.FrameNames));
continue;
}
matched = rectMatches.SingleOrDefault();
}
if (matched == null)
{
matched = new SpriteRect
{
name = slot.CanonicalFrameName,
rect = unityRect,
pivot = source.Pivot,
alignment = SpriteAlignment.Custom,
spriteID = GUID.Generate()
};
planned.Add(matched);
assigned.Add(matched);
MapSlot(slot, matched.name, spriteNamesBySourceIndex);
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
FrameAnimationSpriteChangeKind.Added,
matched.name,
unityRect,
BuildSlotSummary("新增 SpriteRect", slot),
slot.FrameNames));
continue;
}
var changed = !RectEquals(matched.rect, unityRect) || matched.pivot != source.Pivot ||
matched.alignment != SpriteAlignment.Custom;
matched.rect = unityRect;
matched.pivot = source.Pivot;
matched.alignment = SpriteAlignment.Custom;
assigned.Add(matched);
MapSlot(slot, matched.name, spriteNamesBySourceIndex);
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
changed ? FrameAnimationSpriteChangeKind.Updated : FrameAnimationSpriteChangeKind.Unchanged,
matched.name,
unityRect,
BuildSlotSummary(changed ? "更新 rect/pivot,保留 spriteID" : "无变化", slot),
slot.FrameNames));
}
foreach (var retained in originalRects.Where(rect => !assigned.Contains(rect)))
{
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
FrameAnimationSpriteChangeKind.Retained,
retained.name,
retained.rect,
"源中已消失或不再作为共享帧主 Sprite,保留现有 SpriteRect"));
}
return new FrameAnimationSpritePlan(
texturePath,
existingRects,
planned,
spriteNamesBySourceIndex,
null);
}
private static FrameAnimationSpritePlan BuildReadOnlyPlan(
FrameAnimationSpriteSourceContext source,
AsepriteSourceDocument document,
IFrameAnimationImportPreviewSink preview,
string texturePath,
IReadOnlyList<SpriteRect> existingRects,
IReadOnlyCollection<int> includedSourceIndices)
{
var sprites = AssetDatabase.LoadAllAssetsAtPath(texturePath).OfType<Sprite>().ToArray();
var matches = new Dictionary<int, Sprite>();
foreach (var slot in EnumerateSlots(document, includedSourceIndices))
{
var unityRect = ToUnityRect(slot.Rect, source.Texture.height);
var rectMatches = sprites.Where(sprite => RectEquals(sprite.rect, unityRect)).ToArray();
if (rectMatches.Length != 1)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.SpriteMatchFailed,
$"共享帧 '{slot.CanonicalFrameName}' 无法按 rect 唯一匹配 Sprite,候选数:{rectMatches.Length}。"));
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
FrameAnimationSpriteChangeKind.Error,
slot.CanonicalFrameName,
unityRect,
"只读 Sprite 匹配失败",
slot.FrameNames));
continue;
}
foreach (var sourceIndex in slot.SourceIndices)
{
matches[sourceIndex] = rectMatches[0];
}
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
FrameAnimationSpriteChangeKind.Unchanged,
rectMatches[0].name,
unityRect,
BuildSlotSummary($"匹配 Sprite '{rectMatches[0].name}'", slot),
slot.FrameNames));
}
return new FrameAnimationSpritePlan(texturePath, existingRects, existingRects, null, matches);
}
private static void MapSlot(
AsepriteSpriteSlot slot,
string spriteName,
IDictionary<int, string> spriteNamesBySourceIndex)
{
foreach (var sourceIndex in slot.SourceIndices)
{
spriteNamesBySourceIndex[sourceIndex] = spriteName;
}
}
private static IEnumerable<AsepriteSpriteSlot> EnumerateSlots(
AsepriteSourceDocument document,
IReadOnlyCollection<int> includedSourceIndices)
{
if (includedSourceIndices == null)
{
return document.SpriteSlots;
}
var included = new HashSet<int>(includedSourceIndices);
return document.SpriteSlots.Select(slot =>
{
var indices = new List<int>();
var names = new List<string>();
for (var index = 0; index < slot.SourceIndices.Count; index++)
{
if (!included.Contains(slot.SourceIndices[index])) continue;
indices.Add(slot.SourceIndices[index]);
names.Add(slot.FrameNames[index]);
}
return indices.Count == 0
? null
: new AsepriteSpriteSlot(slot.Rect, names[0], indices, names);
}).Where(slot => slot != null);
}
private static string BuildSlotSummary(string prefix, AsepriteSpriteSlot slot)
{
return slot.SourceIndices.Count > 1
? $"{prefix}{slot.SourceIndices.Count} 个 SourceFrame 共享"
: prefix;
}
private static IReadOnlyList<SpriteRect> ReadSpriteRects(TextureImporter importer)
{
var factories = new SpriteDataProviderFactories();
factories.Init();
var provider = factories.GetSpriteEditorDataProviderFromObject(importer);
if (provider == null)
{
return Array.Empty<SpriteRect>();
}
provider.InitSpriteEditorDataProvider();
return CloneRects(provider.GetSpriteRects());
}
private static void WriteSpriteRects(TextureImporter importer, IReadOnlyList<SpriteRect> rects)
{
var factories = new SpriteDataProviderFactories();
factories.Init();
var provider = factories.GetSpriteEditorDataProviderFromObject(importer);
if (provider == null)
{
throw new InvalidOperationException($"无法获取 Sprite Data Provider{importer.assetPath}");
}
provider.InitSpriteEditorDataProvider();
provider.SetSpriteRects(CloneRects(rects).ToArray());
provider.Apply();
}
private static IReadOnlyList<SpriteRect> CloneRects(IEnumerable<SpriteRect> rects)
{
return (rects ?? Array.Empty<SpriteRect>()).Select(rect => new SpriteRect
{
name = rect.name,
rect = rect.rect,
pivot = rect.pivot,
alignment = rect.alignment,
border = rect.border,
spriteID = rect.spriteID
}).ToArray();
}
private static bool RectEquals(Rect left, Rect right)
{
return Mathf.Approximately(left.x, right.x) &&
Mathf.Approximately(left.y, right.y) &&
Mathf.Approximately(left.width, right.width) &&
Mathf.Approximately(left.height, right.height);
}
private static FrameAnimationImportIssue Error(
FrameAnimationSpriteSourceContext source,
FrameAnimationImportIssueCode code,
string message)
{
return new FrameAnimationImportIssue(
FrameAnimationValidationSeverity.Error,
code,
source.SourceId,
source.SourceId,
message);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 15c72a516e10f7f4dbfdbce9489a935b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,71 @@
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
[CustomEditor(typeof(FrameClip))]
public sealed class FrameClipEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
var clip = (FrameClip)target;
var owners = FrameAnimationAssetReferenceIndex.FindGraphsReferencing(clip);
var kind = clip.IsImported
? "Graph Imported FrameClip"
: clip.HasStandaloneImportSource ? "Source-linked External FrameClip" : "Manual FrameClip";
EditorGUILayout.LabelField(kind, EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.TextField("ID", clip.Id);
EditorGUILayout.TextField("Display Name", clip.DisplayName);
EditorGUILayout.FloatField("Speed", clip.Speed);
EditorGUILayout.EnumPopup("Default End", clip.DefaultEndBehavior);
EditorGUILayout.IntField("Frames", clip.FrameCount);
EditorGUILayout.LongField("Total Duration Ms", clip.TotalDurationMs);
EditorGUILayout.TextField("Storage", AssetDatabase.IsSubAsset(clip) ? "Graph sub-asset" : "External .asset");
EditorGUILayout.TextField("Source Tag", clip.ImportInfo?.SourceTagName ?? string.Empty);
EditorGUILayout.Toggle("Missing", clip.ImportInfo?.IsMissingFromSource ?? false);
EditorGUILayout.ObjectField("Standalone Texture", clip.StandaloneImportSource?.Texture, typeof(Texture2D), false);
EditorGUILayout.ObjectField("Standalone JSON", clip.StandaloneImportSource?.AsepriteJson, typeof(TextAsset), false);
}
if (!AssetDatabase.IsSubAsset(clip) && owners.Count > 1)
{
EditorGUILayout.HelpBox(
"该外部 Manual Clip 被多个 Graph 共享:" + string.Join(", ", owners.Select(graph => graph.name)),
MessageType.Warning);
}
EditorGUILayout.HelpBox(
AssetDatabase.IsSubAsset(clip)
? "Graph sub-asset 的 Clip 数据请在 Frame Animation Graph Editor 中编辑。"
: "双击资产或使用下方按钮,在独立 Frame Clip Editor 中编辑。",
MessageType.Info);
if (!AssetDatabase.IsSubAsset(clip) && GUILayout.Button("Open Frame Clip Editor"))
{
FrameClipEditorWindow.Open(clip);
}
if (owners.Count == 0)
{
EditorGUILayout.HelpBox("当前没有 FrameAnimationGraph 引用该 Clip。", MessageType.Info);
}
else if (owners.Count == 1)
{
if (GUILayout.Button("Open Referencing Graph"))
{
FrameAnimationGraphEditorWindow.Open(owners[0]);
}
}
else if (GUILayout.Button("Choose Referencing Graph"))
{
var menu = new GenericMenu();
foreach (var owner in owners)
{
var captured = owner;
menu.AddItem(new GUIContent(owner.name), false,
() => FrameAnimationGraphEditorWindow.Open(captured));
}
menu.ShowAsContext();
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 842e01eb4f6891e49b3749059adaf6c9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,360 @@
using System;
using System.Linq;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
public sealed class FrameClipEditorWindow : EditorWindow
{
private FrameClip clip;
private SerializedObject clipSerializedObject;
private ReorderableList frameList;
private Vector2 scroll;
private readonly FrameAnimationPreviewCoordinator preview = new FrameAnimationPreviewCoordinator();
private FrameClipImportPreview importPreview;
private double lastUpdateTime;
[MenuItem("Window/AibisDream/Frame Animation/Frame Clip Editor")]
public static void ShowWindow()
{
GetWindow<FrameClipEditorWindow>("Frame Clip Editor");
}
public static void Open(FrameClip value)
{
var window = GetWindow<FrameClipEditorWindow>("Frame Clip Editor");
window.SetClip(value);
window.Show();
window.Focus();
}
private void OnEnable()
{
EditorApplication.update += OnEditorUpdate;
Undo.undoRedoPerformed += OnUndoRedo;
lastUpdateTime = EditorApplication.timeSinceStartup;
}
private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
Undo.undoRedoPerformed -= OnUndoRedo;
}
private void OnUndoRedo()
{
RebuildSerializedState();
preview.RefreshTarget(false);
Repaint();
}
private void OnEditorUpdate()
{
var now = EditorApplication.timeSinceStartup;
var delta = Math.Max(0d, now - lastUpdateTime);
lastUpdateTime = now;
if (preview.Tick(delta, null)) Repaint();
}
private void OnGUI()
{
var next = (FrameClip)EditorGUILayout.ObjectField("FrameClip", clip, typeof(FrameClip), false);
if (next != clip) SetClip(next);
if (clip == null)
{
EditorGUILayout.HelpBox("选择一个外部 FrameClip .asset 开始编辑。", MessageType.Info);
return;
}
if (AssetDatabase.IsSubAsset(clip))
{
EditorGUILayout.HelpBox("Graph sub-asset Clip 请在 Frame Animation Graph Editor 中编辑。", MessageType.Info);
if (GUILayout.Button("Open Referencing Graph"))
{
var owner = FrameAnimationAssetReferenceIndex.FindGraphsReferencing(clip).FirstOrDefault();
if (owner != null) FrameAnimationGraphEditorWindow.Open(owner);
}
return;
}
DrawPreview();
scroll = EditorGUILayout.BeginScrollView(scroll);
DrawClipProperties();
EditorGUILayout.Space();
DrawSourceProperties();
EditorGUILayout.Space();
frameList?.DoLayoutList();
if (clipSerializedObject != null && clipSerializedObject.ApplyModifiedProperties())
{
EditorUtility.SetDirty(clip);
preview.RefreshTarget(false);
}
if (!clip.IsImported && !clip.HasStandaloneImportSource &&
frameList != null && frameList.index >= 0 && GUILayout.Button("Duplicate Selected Frame"))
{
var frames = clipSerializedObject.FindProperty("frames");
frames.InsertArrayElementAtIndex(frameList.index);
clipSerializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(clip);
preview.RefreshTarget(false);
}
EditorGUILayout.EndScrollView();
}
private void DrawPreview()
{
var rect = GUILayoutUtility.GetRect(100f, 260f, GUILayout.ExpandWidth(true));
EditorGUI.DrawRect(rect, new Color(0.12f, 0.12f, 0.12f));
var sprite = preview.CurrentSprite;
if (sprite != null && sprite.texture != null)
{
var uv = new Rect(
sprite.textureRect.x / sprite.texture.width,
sprite.textureRect.y / sprite.texture.height,
sprite.textureRect.width / sprite.texture.width,
sprite.textureRect.height / sprite.texture.height);
var scale = Mathf.Min(rect.width / sprite.rect.width, rect.height / sprite.rect.height);
var size = sprite.rect.size * scale;
var destination = new Rect(rect.center - size * 0.5f, size);
GUI.DrawTextureWithTexCoords(destination, sprite.texture, uv, true);
}
else
{
GUI.Label(rect, preview.Error.Code == FrameAnimationPlaybackErrorCode.None
? "Empty Frame" : preview.Error.Message, EditorStyles.centeredGreyMiniLabel);
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button(preview.State == FrameAnimationPreviewState.Playing ? "Pause" : "Play"))
{
if (preview.State == FrameAnimationPreviewState.Playing) preview.Pause();
else preview.Play();
}
if (GUILayout.Button("Stop")) preview.Stop();
if (GUILayout.Button("<")) preview.Step(-1);
if (GUILayout.Button(">")) preview.Step(1);
}
var duration = preview.Timeline?.DisplayDurationSeconds ?? 0d;
var position = EditorGUILayout.Slider("Time", (float)preview.PositionSeconds, 0f,
Mathf.Max(0.0001f, (float)duration));
if (!Mathf.Approximately(position, (float)preview.PositionSeconds)) preview.Seek(position);
}
private void DrawClipProperties()
{
clipSerializedObject.Update();
EditorGUILayout.LabelField(clip.HasStandaloneImportSource ? "Source-linked External Clip" : "Manual External Clip",
EditorStyles.boldLabel);
var idProperty = clipSerializedObject.FindProperty("id");
var nextId = EditorGUILayout.DelayedTextField("ID", idProperty.stringValue);
if (nextId != idProperty.stringValue) RenameClip(nextId);
EditorGUILayout.PropertyField(clipSerializedObject.FindProperty("displayName"));
EditorGUILayout.PropertyField(clipSerializedObject.FindProperty("speed"));
EditorGUILayout.PropertyField(clipSerializedObject.FindProperty("defaultEndBehavior"));
EditorGUILayout.LabelField("Frame Count", clip.FrameCount.ToString());
EditorGUILayout.LabelField("Total Duration", clip.TotalDurationMs + " ms");
var owners = FrameAnimationAssetReferenceIndex.FindGraphsReferencing(clip);
EditorGUILayout.LabelField("Referencing Graphs", owners.Count.ToString());
if (owners.Count > 1)
EditorGUILayout.HelpBox("该 Clip 被多个 Graph 共享,ID 重命名被锁定。", MessageType.Warning);
if (clipSerializedObject.ApplyModifiedProperties())
{
EditorUtility.SetDirty(clip);
preview.RefreshTarget(false);
importPreview = null;
}
}
private void DrawSourceProperties()
{
EditorGUILayout.LabelField("Standalone Import Source", EditorStyles.boldLabel);
if (!clip.HasStandaloneImportSource)
{
if (GUILayout.Button("Enable Texture + JSON Source"))
{
if (!FrameClipImportService.EnableSource(clip, out var error))
EditorUtility.DisplayDialog("无法启用来源", error, "确定");
RebuildSerializedState();
}
return;
}
clipSerializedObject.Update();
var source = clipSerializedObject.FindProperty("standaloneImportSource");
EditorGUILayout.PropertyField(source.FindPropertyRelative("texture"));
EditorGUILayout.PropertyField(source.FindPropertyRelative("asepriteJson"));
EditorGUILayout.PropertyField(source.FindPropertyRelative("pivot"));
EditorGUILayout.PropertyField(source.FindPropertyRelative("manageSpriteSlicing"));
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.PropertyField(source.FindPropertyRelative("lastImportedTagName"));
EditorGUILayout.PropertyField(source.FindPropertyRelative("lastSourceHash"));
}
if (clipSerializedObject.ApplyModifiedProperties())
{
EditorUtility.SetDirty(clip);
importPreview = null;
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Preview Import")) importPreview = FrameClipImportService.Preview(clip);
if (GUILayout.Button("Refresh"))
{
importPreview = FrameClipImportService.Preview(clip);
ApplyImportPreview();
}
if (GUILayout.Button("Detach Source"))
{
if (EditorUtility.DisplayDialog("断开来源", "保留当前帧并转为手工 Clip", "断开", "取消"))
{
FrameClipImportService.DetachSource(clip);
importPreview = null;
RebuildSerializedState();
}
}
}
DrawImportPreview();
}
private void DrawImportPreview()
{
if (importPreview == null) return;
EditorGUILayout.LabelField("Import Preview", EditorStyles.boldLabel);
EditorGUILayout.LabelField("Selected Tag",
string.IsNullOrEmpty(importPreview.SourceTagName) ? "<all frames>" : importPreview.SourceTagName);
EditorGUILayout.LabelField("Selected Frames", importPreview.SourceIndices.Count.ToString());
EditorGUILayout.LabelField("Frame Changes", importPreview.HasFrameChanges ? "Yes" : "No");
EditorGUILayout.LabelField("Sprite Changes", importPreview.HasSpriteChanges ? "Yes" : "No");
foreach (var issue in importPreview.Issues)
{
var type = issue.Severity == FrameAnimationValidationSeverity.Error
? MessageType.Error : MessageType.Warning;
EditorGUILayout.HelpBox($"{issue.Code}: {issue.Message}", type);
}
}
private void ApplyImportPreview()
{
if (importPreview == null || importPreview.HasErrors) return;
var allow = !importPreview.HasSpriteChanges || EditorUtility.DisplayDialog(
"确认修改 TextureImporter",
"独立 Clip 刷新需要更新 Sprite 切图。是否继续?",
"应用切图并刷新",
"取消");
if (!allow) return;
if (!FrameClipImportService.Apply(importPreview, true, out var error))
{
EditorUtility.DisplayDialog("刷新失败", error, "确定");
return;
}
importPreview = FrameClipImportService.Preview(clip);
RebuildSerializedState();
preview.RefreshTarget(false);
}
private void RenameClip(string newId)
{
if (string.IsNullOrWhiteSpace(newId))
{
EditorUtility.DisplayDialog("无法重命名", "Clip ID 不能为空。", "确定");
return;
}
var owners = FrameAnimationAssetReferenceIndex.FindGraphsReferencing(clip);
if (owners.Count > 1)
{
EditorUtility.DisplayDialog("无法重命名", "共享外部 Clip 被多个 Graph 引用。", "确定");
return;
}
if (owners.Count == 1)
{
if (!FrameAnimationAssetOperations.RenameClip(owners[0], clip, newId, out var error))
EditorUtility.DisplayDialog("无法重命名", error, "确定");
return;
}
Undo.RecordObject(clip, "Rename Standalone Frame Clip");
clip.SetId(newId);
EditorUtility.SetDirty(clip);
}
private void SetClip(FrameClip value)
{
clip = value;
importPreview = null;
RebuildSerializedState();
preview.SetGraph(null);
if (clip != null) preview.SetTarget(FrameAnimationPreviewTargetKind.Clip, clip);
else preview.ClearTarget();
Repaint();
}
private void RebuildSerializedState()
{
clipSerializedObject = clip != null ? new SerializedObject(clip) : null;
frameList = clip != null ? BuildFrameList() : null;
}
private ReorderableList BuildFrameList()
{
var frames = clipSerializedObject.FindProperty("frames");
var editable = !clip.IsImported && !clip.HasStandaloneImportSource;
var list = new ReorderableList(clipSerializedObject, frames, editable, true, editable, editable)
{
elementHeight = 72f,
drawHeaderCallback = rect => EditorGUI.LabelField(rect,
editable ? "Frames" : "Frames (source-linked, read-only)"),
drawElementCallback = (rect, index, active, focused) => DrawFrame(frames, rect, index),
onAddCallback = value =>
{
var index = frames.arraySize;
frames.InsertArrayElementAtIndex(index);
var element = frames.GetArrayElementAtIndex(index);
element.FindPropertyRelative("sprite").objectReferenceValue = null;
element.FindPropertyRelative("durationMs").intValue = 100;
element.FindPropertyRelative("frameName").stringValue = string.Empty;
element.FindPropertyRelative("sourceIndex").intValue = -1;
clipSerializedObject.ApplyModifiedProperties();
value.index = index;
EditorUtility.SetDirty(clip);
preview.RefreshTarget(false);
},
onRemoveCallback = value =>
{
ReorderableList.defaultBehaviours.DoRemoveButton(value);
clipSerializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(clip);
preview.RefreshTarget(false);
},
onReorderCallback = _ =>
{
clipSerializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(clip);
preview.RefreshTarget(false);
}
};
return list;
}
private static void DrawFrame(SerializedProperty frames, Rect rect, int index)
{
if (index < 0 || index >= frames.arraySize) return;
var element = frames.GetArrayElementAtIndex(index);
rect.y += 2f;
var line = EditorGUIUtility.singleLineHeight;
EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, line),
element.FindPropertyRelative("sprite"), new GUIContent($"#{index} Sprite"));
EditorGUI.PropertyField(new Rect(rect.x, rect.y + line + 2f, rect.width, line),
element.FindPropertyRelative("durationMs"));
using (new EditorGUI.DisabledScope(true))
{
var half = (rect.width - 4f) * 0.5f;
EditorGUI.PropertyField(new Rect(rect.x, rect.y + (line + 2f) * 2f, half, line),
element.FindPropertyRelative("frameName"));
EditorGUI.PropertyField(new Rect(rect.x + half + 4f, rect.y + (line + 2f) * 2f, half, line),
element.FindPropertyRelative("sourceIndex"));
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5673c406c6069e241b0fad51881213fe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,333 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
internal sealed class FrameClipImportPreview : IFrameAnimationImportPreviewSink
{
private readonly List<FrameAnimationImportIssue> issues = new List<FrameAnimationImportIssue>();
private readonly List<FrameAnimationSpriteDiff> spriteDiffs = new List<FrameAnimationSpriteDiff>();
public FrameClip Clip { get; }
public AsepriteSourceDocument Document { get; internal set; }
public FrameAnimationSpritePlan SpritePlan { get; internal set; }
public IReadOnlyList<int> SourceIndices { get; internal set; } = Array.Empty<int>();
public string SourceTagName { get; internal set; } = string.Empty;
public string CurrentHash { get; internal set; } = string.Empty;
public bool HasFrameChanges { get; internal set; }
public IReadOnlyList<FrameAnimationImportIssue> Issues => issues;
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 FrameClipImportPreview(FrameClip clip)
{
Clip = clip;
}
public void AddIssue(FrameAnimationImportIssue issue)
{
if (issue != null) issues.Add(issue);
}
public void AddSpriteDiff(FrameAnimationSpriteDiff diff)
{
if (diff != null) spriteDiffs.Add(diff);
}
}
internal static class FrameClipImportService
{
public static bool EnableSource(FrameClip clip, out string error)
{
error = string.Empty;
if (!IsExternalClip(clip))
{
error = "独立来源只能添加到外部 .asset FrameClip。";
return false;
}
if (clip.IsImported)
{
error = "Graph Imported Clip 必须继续由所属 Graph 的 ImportSource 管理。";
return false;
}
if (clip.HasStandaloneImportSource) return true;
Undo.RecordObject(clip, "Enable Standalone Frame Clip Source");
clip.GetOrCreateStandaloneImportSource();
EditorUtility.SetDirty(clip);
return true;
}
public static void DetachSource(FrameClip clip)
{
if (clip == null || !clip.HasStandaloneImportSource) return;
Undo.RecordObject(clip, "Detach Standalone Frame Clip Source");
clip.ClearStandaloneImportSource();
EditorUtility.SetDirty(clip);
}
public static FrameClipImportPreview Preview(FrameClip clip)
{
var preview = new FrameClipImportPreview(clip);
if (!IsExternalClip(clip))
{
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.ImportedClipOwnershipInvalid,
"独立来源只能用于外部 .asset FrameClip。"));
return preview;
}
var source = clip.StandaloneImportSource;
if (source == null)
{
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.ImportSourceReferenceInvalid,
"FrameClip 尚未启用独立来源。"));
return preview;
}
if (source.Texture == null)
{
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TextureMissing, "独立来源缺少 Texture。"));
return preview;
}
if (source.AsepriteJson == null)
{
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.JsonMissing, "独立来源缺少 Aseprite JSON。"));
return preview;
}
var sourceId = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(clip));
if (!AsepriteJsonParser.TryParse(source.AsepriteJson.text, sourceId, out var document, out var parseIssue))
{
preview.AddIssue(parseIssue);
return preview;
}
preview.Document = document;
ValidateFrames(clip, source, document, preview);
if (preview.HasErrors) return preview;
if (document.Tags.Count > 0)
{
var first = document.Tags[0];
preview.SourceTagName = first.Name;
if (string.IsNullOrWhiteSpace(first.Name))
{
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TagNameInvalid,
"第一个 Tag 的名称不能为空。"));
return preview;
}
if (!AsepriteJsonParser.TryExpandTag(first, document.Frames.Count, sourceId,
out var indices, out var tagIssue))
{
preview.AddIssue(tagIssue);
return preview;
}
preview.SourceIndices = indices;
}
else
{
preview.SourceIndices = Enumerable.Range(0, document.Frames.Count).ToArray();
}
ValidateTextureOwnership(clip, source, preview);
if (preview.HasErrors) return preview;
var context = Context(clip, source);
preview.SpritePlan = FrameAnimationSpriteUtility.BuildPlan(
context, document, preview, preview.SourceIndices.Distinct().ToArray());
preview.CurrentHash = FrameAnimationSpriteUtility.ComputeSourceHash(context);
preview.HasFrameChanges = !FramesEqual(clip, preview);
if (!string.IsNullOrEmpty(source.LastSourceHash) && source.LastSourceHash != preview.CurrentHash)
{
preview.AddIssue(new FrameAnimationImportIssue(
FrameAnimationValidationSeverity.Warning,
FrameAnimationImportIssueCode.SourceChanged,
sourceId,
clip.Id,
"来源内容或切图设置自上次刷新后已发生变化。"));
}
return preview;
}
public static bool Apply(FrameClipImportPreview preview, bool allowSpriteChanges, out string error)
{
error = string.Empty;
if (preview?.Clip == null || preview.Document == null || preview.SpritePlan == null)
{
error = "独立 Clip 导入预览无效。";
return false;
}
if (preview.HasErrors)
{
error = "导入预览包含阻断错误,未修改任何资产。";
return false;
}
if (preview.HasSpriteChanges && !allowSpriteChanges)
{
error = "导入需要修改 TextureImporter,但尚未获得确认。";
return false;
}
var clip = preview.Clip;
var source = clip.StandaloneImportSource;
FrameAnimationTextureSnapshot snapshot = null;
Undo.IncrementCurrentGroup();
var group = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Refresh Standalone Frame Clip Source");
try
{
if (source.ManageSpriteSlicing && preview.HasSpriteChanges)
{
snapshot = FrameAnimationSpriteUtility.CaptureSnapshot(preview.SpritePlan);
FrameAnimationSpriteUtility.ApplyPlan(preview.SpritePlan);
}
var sprites = source.ManageSpriteSlicing
? FrameAnimationSpriteUtility.LoadSpritesByName(preview.SpritePlan.TexturePath)
: null;
var frames = preview.SourceIndices.Select(sourceIndex =>
{
var sourceFrame = preview.Document.Frames[sourceIndex];
if (!preview.SpritePlan.TryResolveSprite(sourceIndex, sprites, out var sprite))
throw new InvalidOperationException($"刷新后找不到 SourceFrame '{sourceFrame.FrameName}' 对应的 Sprite。");
return new FrameAnimationFrame(sprite, sourceFrame.DurationMs, sourceFrame.FrameName, sourceIndex);
}).ToArray();
Undo.RecordObject(clip, "Refresh Standalone Frame Clip");
clip.SetFrames(frames);
if (string.IsNullOrWhiteSpace(clip.Id))
{
var initialId = !string.IsNullOrWhiteSpace(preview.SourceTagName)
? preview.SourceTagName
: Path.GetFileNameWithoutExtension(AssetDatabase.GetAssetPath(clip));
clip.SetId(initialId);
if (string.IsNullOrWhiteSpace(clip.DisplayName)) clip.SetDisplayName(initialId);
}
source.SetLastImport(
FrameAnimationSpriteUtility.ComputeSourceHash(Context(clip, source)),
preview.SourceTagName);
EditorUtility.SetDirty(clip);
AssetDatabase.SaveAssets();
Undo.CollapseUndoOperations(group);
return true;
}
catch (Exception exception)
{
Undo.RevertAllDownToGroup(group);
if (snapshot != null) FrameAnimationSpriteUtility.RestoreSnapshot(snapshot);
AssetDatabase.SaveAssets();
error = $"独立 Clip 导入失败,已尝试回滚:{exception.Message}";
Debug.LogException(exception);
return false;
}
}
private static void ValidateFrames(
FrameClip clip,
FrameClipImportSource source,
AsepriteSourceDocument document,
FrameClipImportPreview preview)
{
if (document.Frames.Count == 0)
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.FramesMissing, "Aseprite JSON 的 frames 为空。"));
foreach (var duplicate in document.Frames.GroupBy(frame => frame.FrameName).Where(group => group.Count() > 1))
preview.AddIssue(Error(clip, 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(clip, FrameAnimationImportIssueCode.FrameNameInvalid,
$"SourceFrame[{index}] 的 frameName 为空。"));
if (frame.DurationMs <= 0)
preview.AddIssue(Error(clip, 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(clip, FrameAnimationImportIssueCode.FrameRectInvalid,
$"SourceFrame '{frame.FrameName}' 的 rect 越出 Texture 范围。"));
if (frame.Trimmed)
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TrimmedUnsupported,
$"SourceFrame '{frame.FrameName}' 使用了不支持的 trimmed。"));
if (frame.Rotated)
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.RotatedUnsupported,
$"SourceFrame '{frame.FrameName}' 使用了不支持的 rotated。"));
}
if (document.TextureSize.x != source.Texture.width || document.TextureSize.y != source.Texture.height)
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TextureSizeMismatch,
$"meta.size {document.TextureSize.x}x{document.TextureSize.y} 与 Texture {source.Texture.width}x{source.Texture.height} 不一致。"));
}
private static void ValidateTextureOwnership(
FrameClip clip,
FrameClipImportSource source,
FrameClipImportPreview preview)
{
if (!source.ManageSpriteSlicing) return;
var owners = new List<string>();
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(item => item != null && item.IsEnabled &&
item.ManageSpriteSlicing && item.Texture == source.Texture)
.Select(item => $"Graph '{graph.name}' / {item.DisplayName}"));
}
foreach (var guid in AssetDatabase.FindAssets("t:FrameClip"))
{
var other = AssetDatabase.LoadAssetAtPath<FrameClip>(AssetDatabase.GUIDToAssetPath(guid));
var otherSource = other?.StandaloneImportSource;
if (other == null || other == clip || otherSource == null || !otherSource.ManageSpriteSlicing ||
otherSource.Texture != source.Texture) continue;
owners.Add($"FrameClip '{other.name}'");
}
if (owners.Count > 0)
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TextureOwnershipConflict,
$"Texture '{source.Texture.name}' 已有其他可写来源:{string.Join(", ", owners)}"));
}
private static bool FramesEqual(FrameClip clip, FrameClipImportPreview preview)
{
if (clip.FrameCount != preview.SourceIndices.Count) return false;
for (var index = 0; index < preview.SourceIndices.Count; index++)
{
var sourceIndex = preview.SourceIndices[index];
var sourceFrame = preview.Document.Frames[sourceIndex];
var frame = clip.Frames[index];
if (frame == null || frame.SourceIndex != sourceIndex || frame.FrameName != sourceFrame.FrameName ||
frame.DurationMs != sourceFrame.DurationMs ||
!preview.SpritePlan.MatchesSprite(sourceIndex, frame.Sprite)) return false;
}
return true;
}
private static FrameAnimationSpriteSourceContext Context(FrameClip clip, FrameClipImportSource source)
{
return new FrameAnimationSpriteSourceContext(
AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(clip)),
source.Texture, source.AsepriteJson, source.Pivot, source.ManageSpriteSlicing);
}
private static bool IsExternalClip(FrameClip clip)
{
return clip != null && !AssetDatabase.IsSubAsset(clip) &&
!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(clip));
}
private static FrameAnimationImportIssue Error(
FrameClip clip,
FrameAnimationImportIssueCode code,
string message)
{
var path = clip != null ? AssetDatabase.GetAssetPath(clip) : string.Empty;
return new FrameAnimationImportIssue(
FrameAnimationValidationSeverity.Error,
code,
AssetDatabase.AssetPathToGUID(path),
clip != null ? clip.Id : string.Empty,
message);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4448f0db0909671479f225061710e439
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,51 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
namespace AibisDream.FrameAnimation.Editor
{
[CustomEditor(typeof(FrameClipPlayer))]
public sealed class FrameClipPlayerEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(serializedObject.FindProperty("clip"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("playOnEnable"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("speed"));
DrawWarnings();
serializedObject.ApplyModifiedProperties();
if (!Application.isPlaying) return;
var player = (FrameClipPlayer)target;
EditorGUILayout.Space();
EditorGUILayout.LabelField("Runtime State", EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.EnumPopup("State", player.State);
EditorGUILayout.ObjectField("Clip", player.CurrentClip, typeof(FrameClip), false);
EditorGUILayout.TextField("Clip ID", player.CurrentClipId);
EditorGUILayout.IntField("Frame", player.CurrentFrameIndex);
}
Repaint();
}
private void DrawWarnings()
{
var player = (FrameClipPlayer)target;
var renderer = player.GetComponent<SpriteRenderer>();
var image = player.GetComponent<Image>();
if (player.GetComponent<FrameAnimationPlayer>() != null)
EditorGUILayout.HelpBox("同一 GameObject 不能同时包含 FrameAnimationPlayer 和 FrameClipPlayer。", MessageType.Error);
if (renderer == null && image == null)
EditorGUILayout.HelpBox("同一 GameObject 上必须存在一个 SpriteRenderer 或 Image。", MessageType.Error);
else if (renderer != null && image != null)
EditorGUILayout.HelpBox("同一 GameObject 不能同时存在 SpriteRenderer 和 Image。", MessageType.Error);
if (serializedObject.FindProperty("clip").objectReferenceValue == null)
EditorGUILayout.HelpBox("尚未绑定默认 FrameClip;仍可通过 Play(FrameClip) 临时播放。", MessageType.Info);
var value = serializedObject.FindProperty("speed").floatValue;
if (!FrameAnimationValueUtility.IsValidSpeed(value))
EditorGUILayout.HelpBox("Speed 必须是有限且不小于 0 的数值。", MessageType.Error);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d7eddf60189f61740be2fe59cda06957
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,78 @@
using AibisDream.FrameAnimation;
using AibisDream.SaveSystem;
using AibisDream.Utility;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEngine;
namespace AibisDream.SystemEditor.Tests
{
public sealed class FrameAnimationActorIntegrationTests
{
private const string PrefabPath =
"Assets/Prefabs/FixSystemPrefabs/FrameAnimationActor.prefab";
[Test]
public void ActorType_AppendsFrameAnimationWithoutChangingExistingValues()
{
Assert.That((int)ActorType.Sprite, Is.EqualTo(0));
Assert.That((int)ActorType.Anima, Is.EqualTo(1));
Assert.That((int)ActorType.AnimaEx, Is.EqualTo(2));
Assert.That((int)ActorType.FrameAnimation, Is.EqualTo(3));
Assert.That(
System.Enum.TryParse("FrameAnimation", out ActorType parsed),
Is.True);
Assert.That(parsed, Is.EqualTo(ActorType.FrameAnimation));
}
[Test]
public void Prefab_HasRequiredComponentsAndPersistentAddress()
{
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath);
Assert.That(prefab, Is.Not.Null);
Assert.That(prefab.GetComponent<SpriteRenderer>(), Is.Not.Null);
Assert.That(prefab.GetComponent<FrameAnimationPlayer>(), Is.Not.Null);
Assert.That(prefab.GetComponent<FrameAnimationActor>(), Is.Not.Null);
Assert.That(prefab.GetComponent<FrameAnimationPlayer>().PlayOnEnable, Is.False);
Assert.That(prefab.GetComponent<FrameAnimationPlayer>().Graph, Is.Null);
var guid = AssetDatabase.AssetPathToGUID(PrefabPath);
var entry = AddressableAssetSettingsDefaultObject.Settings.FindAssetEntry(guid);
Assert.That(entry, Is.Not.Null);
Assert.That(entry.address, Is.EqualTo(ConstRef.FrameAnimationActorPrefabName));
}
[Test]
public void CaptureEntry_ReusesActorSnapshotFieldsWithoutSchemaChange()
{
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath);
var actorObject = Object.Instantiate(prefab);
var slotObject = new GameObject("Slot");
try
{
var slot = slotObject.AddComponent<ActorSlot>();
slot.slotName = "test-slot";
slot.sortingLayer = 0;
slot.sortingOrder = 17;
var actor = actorObject.GetComponent<FrameAnimationActor>();
actor.Init("TestActor", slot, ActorType.FrameAnimation);
actor.Show();
ActorEntrySnapshotDto entry = actor.CaptureEntry();
Assert.That(entry.actorName, Is.EqualTo("TestActor"));
Assert.That(entry.slotName, Is.EqualTo("test-slot"));
Assert.That(entry.actorType, Is.EqualTo(nameof(ActorType.FrameAnimation)));
Assert.That(entry.alpha, Is.EqualTo(1f));
Assert.That(entry.stateName, Is.Empty);
Assert.That(SaveSnapshotSchema.CurrentVersion, Is.EqualTo(1));
}
finally
{
Object.DestroyImmediate(actorObject);
Object.DestroyImmediate(slotObject);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 23aa0707ef5c4618b55c455f2a03a612
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,44 @@
using AibisDream.Utility;
using NUnit.Framework;
using Yarn.Unity;
namespace AibisDream.SystemEditor.Tests
{
public sealed class OptionPromptMetadataTests
{
[Test]
public void IsOptionPromptLine_WithExactMetadata_ReturnsTrue()
{
var line = new LocalizedLine
{
Metadata = new[] { "line:0123456", YarnUtil.OptionPrompt }
};
Assert.That(line.IsOptionPromptLine(), Is.True);
}
[TestCase("option_prompts")]
[TestCase("dream_option_prompt")]
[TestCase("OPTION_PROMPT")]
public void IsOptionPromptLine_WithSimilarMetadata_ReturnsFalse(string metadata)
{
var line = new LocalizedLine
{
Metadata = new[] { metadata }
};
Assert.That(line.IsOptionPromptLine(), Is.False);
}
[Test]
public void IsOptionPromptLine_WithoutMetadata_ReturnsFalse()
{
var line = new LocalizedLine
{
Metadata = null
};
Assert.That(line.IsOptionPromptLine(), Is.False);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d5e819fca9154e979c37ac490184620b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,98 @@
using AibisDream.SaveSystem;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
namespace AibisDream.SystemEditor.Tests
{
public sealed class PresentationSnapshotContractTests
{
[Test]
public void PresentationProviders_UseStableIdsAndRestoreInDependencyOrder()
{
var showcase = new ShowcaseSnapshotProvider();
var day2Sleep = new Day2SleepPresentationSnapshotProvider();
var playTool = new PlayToolSnapshotProvider();
var screen = new ScreenSnapshotProvider();
Assert.That(showcase.SaveId, Is.EqualTo("showcase"));
Assert.That(day2Sleep.SaveId, Is.EqualTo("day2SleepPresentation"));
Assert.That(playTool.SaveId, Is.EqualTo("playTool"));
Assert.That(showcase.RestoreOrder, Is.LessThan(day2Sleep.RestoreOrder));
Assert.That(day2Sleep.RestoreOrder, Is.LessThan(playTool.RestoreOrder));
Assert.That(playTool.RestoreOrder, Is.LessThan(screen.RestoreOrder));
}
[Test]
public void PlayToolSection_RoundTripsThroughSnapshotJson()
{
var source = new SaveSnapshot();
source.sections[SnapshotProviderIds.PlayTool] = new PlayToolSnapshotDto
{
isObjVisible = true,
objPicName = "证物",
isFullScreenVisible = true,
fullScreenPicName = "教室"
};
var json = JsonConvert.SerializeObject(source);
var restored = JsonConvert.DeserializeObject<SaveSnapshot>(json);
var playTool = ((JObject)restored.sections[SnapshotProviderIds.PlayTool])
.ToObject<PlayToolSnapshotDto>();
Assert.That(playTool.isObjVisible, Is.True);
Assert.That(playTool.objPicName, Is.EqualTo("证物"));
Assert.That(playTool.isFullScreenVisible, Is.True);
Assert.That(playTool.fullScreenPicName, Is.EqualTo("教室"));
Assert.That(restored.schemaVersion, Is.EqualTo(1));
}
[Test]
public void ShowcaseAndDay2SleepSections_RoundTripSemanticState()
{
var source = new SaveSnapshot();
source.sections[SnapshotProviderIds.Showcase] = new ShowcaseSnapshotDto
{
displayMode = ShowcaseDisplayMode.Large.ToString(),
picName = "D2S星图",
isBackgroundVisible = true
};
source.sections[SnapshotProviderIds.Day2SleepPresentation] =
new Day2SleepPresentationSnapshotDto
{
mode = Day2SleepPresentationMode.LargeEffects.ToString(),
largeBlurAmount = 0.72f,
largeBlurSize = 0.015f,
largeScaleMultiplier = 1.1f,
largeAlpha = 0.4f
};
var json = JsonConvert.SerializeObject(source);
var restored = JsonConvert.DeserializeObject<SaveSnapshot>(json);
var showcase = ((JObject)restored.sections[SnapshotProviderIds.Showcase])
.ToObject<ShowcaseSnapshotDto>();
var day2Sleep = ((JObject)restored.sections[SnapshotProviderIds.Day2SleepPresentation])
.ToObject<Day2SleepPresentationSnapshotDto>();
Assert.That(showcase.displayMode, Is.EqualTo("Large"));
Assert.That(showcase.picName, Is.EqualTo("D2S星图"));
Assert.That(showcase.isBackgroundVisible, Is.True);
Assert.That(day2Sleep.mode, Is.EqualTo("LargeEffects"));
Assert.That(day2Sleep.largeBlurAmount, Is.EqualTo(0.72f));
Assert.That(day2Sleep.largeScaleMultiplier, Is.EqualTo(1.1f));
Assert.That(day2Sleep.largeAlpha, Is.EqualTo(0.4f));
}
[Test]
public void OldSnapshotWithoutPresentationSections_RemainsCompatible()
{
var restored = JsonConvert.DeserializeObject<SaveSnapshot>(
"{\"schemaVersion\":1,\"sections\":{}}");
Assert.That(restored.schemaVersion, Is.EqualTo(SaveSnapshotSchema.CurrentVersion));
Assert.That(restored.sections.ContainsKey(SnapshotProviderIds.Showcase), Is.False);
Assert.That(restored.sections.ContainsKey(SnapshotProviderIds.Day2SleepPresentation), Is.False);
Assert.That(restored.sections.ContainsKey(SnapshotProviderIds.PlayTool), Is.False);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: aa21851c46624f77a67e29c86ec854c9
@@ -0,0 +1,534 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using AibisDream.SaveSystem;
using AibisDream.Utility;
using Newtonsoft.Json;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
namespace AibisDream.EditorTools
{
/// <summary>从本机 testsavs 选择章节、去重排序并提升为可提交测试档。</summary>
public sealed class DevSavePromoteWindow : EditorWindow
{
private readonly List<ArchiveGroup> groups = new();
private readonly List<Candidate> preview = new();
private int selectedGroup;
private string sectionId = string.Empty;
private string sectionTitle = string.Empty;
private string status = string.Empty;
private Vector2 scroll;
[MenuItem("Tools/Aibis/Dev Save Promote")]
public static void Open()
{
var window = GetWindow<DevSavePromoteWindow>("Dev Save Promote");
window.minSize = new Vector2(720f, 520f);
window.Show();
}
private void OnEnable() => RefreshGroups();
private void OnGUI()
{
EditorGUILayout.LabelField("测试存档提升与校验", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"来源是本机 testsavs;目标是项目根目录的 DevSaveFiles。该目录可提交,但不会被 Unity 自动打包。",
MessageType.Info);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("刷新本机归档", GUILayout.Height(26f))) RefreshGroups();
if (GUILayout.Button("校验全部已提交测试档", GUILayout.Height(26f))) ValidateCommittedCatalog();
}
if (groups.Count == 0)
{
EditorGUILayout.HelpBox("本机没有可用 testsavs。", MessageType.Warning);
DrawStatus();
return;
}
var labels = groups.Select(group =>
$"{group.SceneSoName} | {group.YarnProjectId} | {group.SceneName} ({group.Items.Count})").ToArray();
var nextGroup = EditorGUILayout.Popup("章节归档", Mathf.Clamp(selectedGroup, 0, labels.Length - 1), labels);
if (nextGroup != selectedGroup || preview.Count == 0)
{
selectedGroup = nextGroup;
SelectGroup(groups[selectedGroup]);
}
sectionId = EditorGUILayout.TextField("Section Id", sectionId);
sectionTitle = EditorGUILayout.TextField("显示名称", sectionTitle);
EditorGUILayout.LabelField("规则", "每个 Yarn 节点保留 savedAt 最新一份;默认按 savedAt 升序");
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
if (GUILayout.Button("提升当前章节", GUILayout.Width(180f), GUILayout.Height(28f))) Promote();
}
EditorGUILayout.Space(5f);
EditorGUILayout.LabelField($"阶段预览:{preview.Count}", EditorStyles.boldLabel);
scroll = EditorGUILayout.BeginScrollView(scroll);
for (var i = 0; i < preview.Count; i++)
{
var item = preview[i];
using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox))
{
EditorGUILayout.LabelField($"{i + 1:000}", GUILayout.Width(38f));
item.Label = EditorGUILayout.TextField(item.Label, GUILayout.Width(210f));
EditorGUILayout.LabelField(item.NodeName, GUILayout.MinWidth(180f));
EditorGUILayout.LabelField(item.SavedAt, GUILayout.Width(140f));
using (new EditorGUI.DisabledScope(i == 0))
{
if (GUILayout.Button("↑", GUILayout.Width(26f))) Move(i, i - 1);
}
using (new EditorGUI.DisabledScope(i == preview.Count - 1))
{
if (GUILayout.Button("↓", GUILayout.Width(26f))) Move(i, i + 1);
}
}
}
EditorGUILayout.EndScrollView();
DrawStatus();
}
private void DrawStatus()
{
if (!string.IsNullOrWhiteSpace(status))
EditorGUILayout.HelpBox(status, status.StartsWith("OK", StringComparison.Ordinal) ? MessageType.Info : MessageType.Warning);
}
private void RefreshGroups()
{
groups.Clear();
preview.Clear();
var root = ConstRef.TestAutoSaveArchivePath;
if (!Directory.Exists(root))
{
status = $"找不到本机归档:{root}";
return;
}
var candidates = new List<Candidate>();
foreach (var directory in Directory.GetDirectories(root))
{
var snapshotPath = Path.Combine(directory, $"{ConstRef.SaveSnapshotFileName}.json");
if (!File.Exists(snapshotPath)) continue;
try
{
var snapshot = SnapshotPersistence.Load(Path.Combine(directory, ConstRef.SaveSnapshotFileName));
if (snapshot?.anchor == null || string.IsNullOrWhiteSpace(snapshot.anchor.nodeName)) continue;
candidates.Add(new Candidate(
snapshotPath,
Path.Combine(directory, $"{ConstRef.SaveMetaFileName}.json"),
snapshot.scene?.sceneName,
snapshot.anchor.sceneSoName,
snapshot.anchor.yarnProjectId,
snapshot.anchor.nodeName,
snapshot.savedAt));
}
catch (Exception ex)
{
Debug.LogWarning($"[DevSavePromote] 跳过损坏归档 {snapshotPath}: {ex.Message}");
}
}
groups.AddRange(candidates
.GroupBy(item => $"{item.SceneName}\n{item.SceneSoName}\n{item.YarnProjectId}", StringComparer.Ordinal)
.Select(group => new ArchiveGroup(group.ToList()))
.OrderBy(group => group.SceneSoName, StringComparer.Ordinal));
selectedGroup = Mathf.Clamp(selectedGroup, 0, Math.Max(0, groups.Count - 1));
if (groups.Count > 0) SelectGroup(groups[selectedGroup]);
status = $"OK:读取 {candidates.Count} 份归档,分为 {groups.Count} 个章节。";
}
private void SelectGroup(ArchiveGroup group)
{
preview.Clear();
preview.AddRange(group.Items
.GroupBy(item => item.NodeName, StringComparer.Ordinal)
.Select(nodes => nodes.OrderByDescending(item => item.SavedAt, StringComparer.Ordinal).First())
.OrderBy(item => item.SavedAt, StringComparer.Ordinal));
sectionId = SanitizeIdentifier(group.SceneSoName);
sectionTitle = group.SceneSoName;
}
private void Move(int from, int to)
{
if (from < 0 || to < 0 || from >= preview.Count || to >= preview.Count) return;
var item = preview[from];
preview.RemoveAt(from);
preview.Insert(to, item);
GUI.FocusControl(null);
}
private void Promote()
{
if (preview.Count == 0 || string.IsNullOrWhiteSpace(sectionId))
{
status = "没有可提升阶段,或 Section Id 为空。";
return;
}
var validation = ValidateCandidates(preview);
if (validation.Count > 0)
{
status = string.Join("\n", validation);
return;
}
var group = groups[selectedGroup];
var root = ConstRef.TestSaveFilePath;
var targetSection = Path.Combine(root, sectionId);
if (Directory.Exists(targetSection)) Directory.Delete(targetSection, true);
Directory.CreateDirectory(targetSection);
var catalog = LoadCatalogForWrite();
catalog.sections.RemoveAll(section => string.Equals(section.id, sectionId, StringComparison.OrdinalIgnoreCase));
var sectionDto = new DevSaveCatalogSectionDto
{
order = FindDemoChapterOrder(group.SceneSoName),
id = sectionId,
title = string.IsNullOrWhiteSpace(sectionTitle) ? sectionId : sectionTitle,
expectedSceneName = group.SceneName,
expectedSceneSoName = group.SceneSoName,
expectedYarnProjectId = group.YarnProjectId
};
for (var i = 0; i < preview.Count; i++)
{
var item = preview[i];
var folder = $"{i + 1:000}_{SanitizeIdentifier(item.NodeName)}";
var destination = Path.Combine(targetSection, folder);
Directory.CreateDirectory(destination);
File.Copy(item.SnapshotPath, Path.Combine(destination, $"{ConstRef.SaveSnapshotFileName}.json"), true);
if (File.Exists(item.MetaPath))
File.Copy(item.MetaPath, Path.Combine(destination, $"{ConstRef.SaveMetaFileName}.json"), true);
sectionDto.entries.Add(new DevSaveCatalogEntryDto
{
order = i + 1,
label = string.IsNullOrWhiteSpace(item.Label) ? item.NodeName : item.Label,
path = $"{sectionId}/{folder}",
anchorNode = item.NodeName
});
}
catalog.sections.Add(sectionDto);
catalog.sections.Sort((left, right) => left.order.CompareTo(right.order));
WriteCatalog(catalog);
AssetDatabase.Refresh();
ValidateCommittedCatalog();
}
private static List<string> ValidateCandidates(IEnumerable<Candidate> candidates)
{
var errors = new List<string>();
var nodes = new HashSet<string>(StringComparer.Ordinal);
foreach (var item in candidates)
{
if (!nodes.Add(item.NodeName)) errors.Add($"重复节点:{item.NodeName}");
try
{
var snapshot = SnapshotPersistence.Load(Path.Combine(Path.GetDirectoryName(item.SnapshotPath)!, ConstRef.SaveSnapshotFileName));
if (snapshot.schemaVersion != SaveSnapshotSchema.CurrentVersion)
errors.Add($"{item.NodeName}: schema {snapshot.schemaVersion} 不兼容");
}
catch (Exception ex)
{
errors.Add($"{item.NodeName}: {ex.Message}");
}
}
return errors;
}
private void ValidateCommittedCatalog()
{
var sections = DevSaveCatalog.Load(ConstRef.TestSaveFilePath);
var errors = new List<string>();
if (!string.IsNullOrWhiteSpace(DevSaveCatalog.LastError)) errors.Add(DevSaveCatalog.LastError);
foreach (var section in sections)
{
var orders = new HashSet<int>();
var nodes = new HashSet<string>(StringComparer.Ordinal);
foreach (var entry in section.Entries)
{
if (!orders.Add(entry.Order)) errors.Add($"{section.Id}: 重复 order {entry.Order}");
if (!nodes.Add(entry.AnchorNode)) errors.Add($"{section.Id}: 重复节点 {entry.AnchorNode}");
if (!entry.IsValid) errors.Add($"{section.Id}/{entry.Label}: {entry.Error}");
}
}
var dto = LoadCatalogForWrite();
foreach (var section in dto.sections)
{
if (!SceneAddressExists(section.expectedSceneName))
errors.Add($"{section.id}: Addressable 场景不存在 {section.expectedSceneName}");
var sceneSo = FindTalkSceneSo(section.expectedSceneSoName);
if (sceneSo == null)
errors.Add($"{section.id}: TalkSceneSO 不存在 {section.expectedSceneSoName}");
else if (sceneSo.yarnProject == null || sceneSo.yarnProject.name != section.expectedYarnProjectId)
errors.Add($"{section.id}: TalkSceneSO 的 YarnProject 不匹配 {section.expectedYarnProjectId}");
else
{
var nodeNames = new HashSet<string>(sceneSo.yarnProject.NodeNames, StringComparer.Ordinal);
foreach (var entry in section.entries.Where(entry => !nodeNames.Contains(entry.anchorNode)))
errors.Add($"{section.id}: Yarn 节点不存在 {entry.anchorNode}");
}
}
status = errors.Count == 0
? $"OK{sections.Count} 个章节、{sections.Sum(s => s.Entries.Count)} 个阶段全部通过校验。"
: $"校验失败({errors.Count}):\n{string.Join("\n", errors)}";
if (errors.Count > 0) Debug.LogError($"[DevSavePromote] {status}");
else Debug.Log($"[DevSavePromote] {status}");
}
private static bool SceneAddressExists(string address)
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
return settings != null && settings.groups
.Where(group => group != null)
.SelectMany(group => group.entries)
.Any(entry => string.Equals(entry.address, address, StringComparison.Ordinal));
}
private static TalkSceneSO FindTalkSceneSo(string assetName)
{
foreach (var guid in AssetDatabase.FindAssets($"t:TalkSceneSO {assetName}"))
{
var asset = AssetDatabase.LoadAssetAtPath<TalkSceneSO>(AssetDatabase.GUIDToAssetPath(guid));
if (asset != null && asset.name == assetName) return asset;
}
return null;
}
private static int FindDemoChapterOrder(string sceneSoName)
{
const string firstChapterPath = "Assets/ScriptableObjects/SceneSO/Demo/Day0_Prologue.asset";
var chapter = AssetDatabase.LoadAssetAtPath<TalkSceneSO>(firstChapterPath);
var visited = new HashSet<TalkSceneSO>();
var order = 1;
while (chapter != null && visited.Add(chapter))
{
if (string.Equals(chapter.name, sceneSoName, StringComparison.Ordinal)) return order;
chapter = chapter.GetNextScene();
order++;
}
return int.MaxValue;
}
private static DevSaveCatalogDto LoadCatalogForWrite()
{
var path = Path.Combine(ConstRef.TestSaveFilePath, "catalog.json");
if (!File.Exists(path)) return new DevSaveCatalogDto();
try
{
return JsonConvert.DeserializeObject<DevSaveCatalogDto>(File.ReadAllText(path, Encoding.UTF8))
?? new DevSaveCatalogDto();
}
catch
{
return new DevSaveCatalogDto();
}
}
private static void WriteCatalog(DevSaveCatalogDto catalog)
{
Directory.CreateDirectory(ConstRef.TestSaveFilePath);
File.WriteAllText(
Path.Combine(ConstRef.TestSaveFilePath, "catalog.json"),
JsonConvert.SerializeObject(catalog, Formatting.Indented),
new UTF8Encoding(false));
}
private static string SanitizeIdentifier(string value)
{
if (string.IsNullOrWhiteSpace(value)) return "unnamed";
foreach (var character in Path.GetInvalidFileNameChars()) value = value.Replace(character, '_');
value = value.Replace(' ', '_');
return value.Length <= 48 ? value : value.Substring(0, 48);
}
private sealed class ArchiveGroup
{
public readonly List<Candidate> Items;
public string SceneName => Items[0].SceneName;
public string SceneSoName => Items[0].SceneSoName;
public string YarnProjectId => Items[0].YarnProjectId;
public ArchiveGroup(List<Candidate> items) => Items = items;
}
private sealed class Candidate
{
public readonly string SnapshotPath;
public readonly string MetaPath;
public readonly string SceneName;
public readonly string SceneSoName;
public readonly string YarnProjectId;
public readonly string NodeName;
public readonly string SavedAt;
public string Label;
public Candidate(string snapshotPath, string metaPath, string sceneName, string sceneSoName,
string yarnProjectId, string nodeName, string savedAt)
{
SnapshotPath = snapshotPath;
MetaPath = metaPath;
SceneName = sceneName;
SceneSoName = sceneSoName;
YarnProjectId = yarnProjectId;
NodeName = nodeName;
SavedAt = savedAt ?? string.Empty;
Label = nodeName;
}
}
}
[TestFixture]
public sealed class DevSaveCatalogTests
{
private string root;
[SetUp]
public void SetUp()
{
root = Path.Combine(Path.GetTempPath(), $"aibis-dev-save-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
}
[TearDown]
public void TearDown()
{
if (Directory.Exists(root)) Directory.Delete(root, true);
}
[Test]
public void CatalogUsesExplicitOrderAndKeepsInvalidEntriesVisible()
{
WriteCatalog(new DevSaveCatalogEntryDto { order = 20, label = "late", path = "S/late", anchorNode = "Late" },
new DevSaveCatalogEntryDto { order = 10, label = "early", path = "S/early", anchorNode = "Early" });
var section = DevSaveCatalog.Load(root).Single();
Assert.That(section.Entries.Select(entry => entry.Label), Is.EqualTo(new[] { "early", "late" }));
Assert.That(section.Entries.All(entry => !entry.IsValid), Is.True);
Assert.That(section.Entries.All(entry => entry.Error.Contains("缺失")), Is.True);
}
[Test]
public void CatalogRejectsPathTraversal()
{
WriteCatalog(new DevSaveCatalogEntryDto { order = 1, label = "unsafe", path = "../outside", anchorNode = "Start" });
var entry = DevSaveCatalog.Load(root).Single().Entries.Single();
Assert.That(entry.IsValid, Is.False);
Assert.That(entry.Error, Does.Contain("越过"));
}
[Test]
public void CatalogReportsCorruptSchemaAndAnchorMismatch()
{
WriteSnapshot("S/corrupt", "{");
WriteSnapshot("S/schema", JsonConvert.SerializeObject(BuildSnapshot("Node", schema: 99)));
WriteSnapshot("S/node", JsonConvert.SerializeObject(BuildSnapshot("Actual")));
WriteCatalog(
new DevSaveCatalogEntryDto { order = 1, label = "corrupt", path = "S/corrupt", anchorNode = "Node" },
new DevSaveCatalogEntryDto { order = 2, label = "schema", path = "S/schema", anchorNode = "Node" },
new DevSaveCatalogEntryDto { order = 3, label = "node", path = "S/node", anchorNode = "Expected" });
var entries = DevSaveCatalog.Load(root).Single().Entries;
Assert.That(entries[0].Error, Does.Contain("JSON"));
Assert.That(entries[1].Error, Does.Contain("版本"));
Assert.That(entries[2].Error, Does.Contain("节点不匹配"));
}
[Test]
public void CommittedCatalogContainsAllDemoChaptersWithValidUniqueEntries()
{
var sections = DevSaveCatalog.Load(ConstRef.TestSaveFilePath);
Assert.That(DevSaveCatalog.LastError, Is.Null);
Assert.That(sections.Select(section => section.Id), Is.EqualTo(new[]
{
"Day0Prologue", "Day1Begin", "Peipei1", "Day1Mid", "Day1Night", "Day1Sleep",
"Day2Begin", "Huoshan1", "Day2Mid", "Peipei2", "Day2Night", "Day2Sleep", "GeneralEnd"
}));
foreach (var section in sections)
{
Assert.That(section.Entries.All(entry => entry.IsValid), Is.True,
string.Join("\n", section.Entries.Where(entry => !entry.IsValid).Select(entry => $"{entry.Label}: {entry.Error}")));
Assert.That(section.Entries.Select(entry => entry.Order).Distinct().Count(), Is.EqualTo(section.Entries.Count));
Assert.That(section.Entries.Select(entry => entry.AnchorNode).Distinct().Count(), Is.EqualTo(section.Entries.Count));
}
}
[Test]
public void StrictRestoreContextTracksPhaseWarningsAndErrors()
{
var context = new SnapshotRestoreContext(new SaveSnapshot(), strictMode: true);
context.SetPhase("Provider restore");
context.Warn("optional state missing");
context.Error("required state missing");
Assert.That(context.StrictMode, Is.True);
Assert.That(context.CurrentPhase, Is.EqualTo("Provider restore"));
Assert.That(context.Warnings, Is.EqualTo(new[] { "optional state missing" }));
Assert.That(context.Errors, Is.EqualTo(new[] { "required state missing" }));
Assert.That(context.HasErrors, Is.True);
}
private void WriteCatalog(params DevSaveCatalogEntryDto[] entries)
{
var catalog = new DevSaveCatalogDto
{
sections = new List<DevSaveCatalogSectionDto>
{
new()
{
id = "S",
title = "Section",
expectedSceneName = "Scene/Test",
expectedSceneSoName = "TestSO",
expectedYarnProjectId = "TestYarn",
entries = entries.ToList()
}
}
};
File.WriteAllText(Path.Combine(root, "catalog.json"), JsonConvert.SerializeObject(catalog));
}
private void WriteSnapshot(string relativeDirectory, string json)
{
var directory = Path.Combine(root, relativeDirectory.Replace('/', Path.DirectorySeparatorChar));
Directory.CreateDirectory(directory);
File.WriteAllText(Path.Combine(directory, "snapshot.json"), json);
}
private static SaveSnapshot BuildSnapshot(string node, int schema = SaveSnapshotSchema.CurrentVersion)
{
return new SaveSnapshot
{
schemaVersion = schema,
scene = new SceneSnapshotDto { sceneName = "Scene/Test" },
anchor = new AnchorSnapshot
{
sceneSoName = "TestSO",
yarnProjectId = "TestYarn",
nodeName = node
}
};
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b564a87c642e53646baa39708c8f72ac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -167,7 +167,7 @@ namespace AibisDream.SaveSystem.Editor
EditorGUILayout.LabelField("存读档测试工具", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"用于 Play Mode 下验证自动存档、槽位读档与 testsavs 测试归档。\n" +
"正式存档目录: saves/ | 测试归档: testsavs/(每次自动存档独立子文件夹)",
"正式存档目录: demo_saves/ | 测试归档: testsavs/(每次自动存档独立子文件夹)",
MessageType.Info);
if (!Application.isPlaying)
@@ -193,7 +193,7 @@ namespace AibisDream.SaveSystem.Editor
EditorGUILayout.LabelField($"testsavs 路径: {ConstRef.TestAutoSaveArchivePath}", EditorStyles.miniLabel);
EditorGUILayout.HelpBox(
"开启后:每次 AutoSave 仅在 testsavs 下新建一份独立存档,不写入、不覆盖 saves/slot_0。\n" +
"开启后:每次 AutoSave 仅在 testsavs 下新建一份独立存档,不写入、不覆盖 demo_saves/slot_0。\n" +
"存多少次就保留多少份;关闭后恢复正式行为(仅覆盖 slot_0)。",
MessageType.None);
}
@@ -224,7 +224,7 @@ namespace AibisDream.SaveSystem.Editor
private void DrawSlotRestoreSection()
{
EditorGUILayout.LabelField("从正式槽位读档", EditorStyles.boldLabel);
EditorGUILayout.LabelField($"saves 路径: {ConstRef.SaveFilePath}", EditorStyles.miniLabel);
EditorGUILayout.LabelField($"demo_saves 路径: {ConstRef.SaveFilePath}", EditorStyles.miniLabel);
var latest = SlotManager.GetLatestSlotIndex();
EditorGUILayout.LabelField($"最近槽位: {(latest.HasValue ? $"slot_{latest.Value}" : "")}");
@@ -391,7 +391,7 @@ namespace AibisDream.SaveSystem.Editor
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("打开 saves 文件夹"))
if (GUILayout.Button("打开 demo_saves 文件夹"))
{
OpenDirectory(ConstRef.SaveFilePath);
}
@@ -567,7 +567,7 @@ namespace AibisDream.SaveSystem.Editor
return;
}
GameManager.Instance.StartWithSlot(slotIndex);
GameManager.Instance.TryRestoreSlot(slotIndex);
Log($"已启动读档 slot_{slotIndex}。");
}
@@ -585,7 +585,9 @@ namespace AibisDream.SaveSystem.Editor
return;
}
GameManager.Instance.StartWithSaveFile(entry.SnapshotPath);
GameManager.Instance.TryRestoreFile(
entry.SnapshotPath,
RestoreOptions.DevJump);
Log($"已启动读档: {entry.EntryId}");
}
@@ -332,7 +332,7 @@ namespace AibisDream.SaveSystem.Editor
{
EditorGUILayout.LabelField("读档验证 (P4)", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"触发 SaveRestoreOrchestrator.RestoreFromSlot,验证 Phase + Barrier 编排、自动存档抑制与 Provider 还原日志。\n" +
"触发 GameManager.TryRestoreSlot,验证预检、Phase + Barrier 编排、自动存档抑制与 Provider 还原日志。\n" +
"需要在 Play Mode 下执行;不代表正式玩家 UI。",
MessageType.None);
@@ -559,7 +559,7 @@ namespace AibisDream.SaveSystem.Editor
return;
}
if (GameManager.Instance != null && GameManager.Instance.state.isInPause)
if (GameManager.Instance != null && GameManager.Session.IsPaused)
{
Log("全局门控: GamePaused");
return;
@@ -631,7 +631,7 @@ namespace AibisDream.SaveSystem.Editor
return;
}
GameManager.Instance.StartWithSlot(slotIndex);
GameManager.Instance.TryRestoreSlot(slotIndex);
Log($"已启动 Restore slot_{slotIndex}。请观察 Restore Pipeline Log 与场景状态。");
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+203 -197
View File
@@ -1,62 +1,66 @@
共 4041 个不同字符
!"#%'()*+,-./01234
56789:;<=>?ABCDEFGHI
JKLMNOPQRSTUVWXYZ[\]^_
abcdefghijklmnopqrst
uvwxyz{}~λ—―“”…₭ 、。「
」【】〜ぁあいぅうえおかがきぎくぐけげこ
ごさざしじすずせぜそぞただちっつづてでと
どなにぬねのはばぱひびぴふぶへべぺほぼぽ
まみむめもゃやゅゆょよらりるれろわをんァ
アィイウェエォオカガキギクグケゲコゴサザ
シジスズセソゾタダチッツテデトドニネノハ
バパビフブプベペホボポマムメモャヤュラリ
ルレロンー一丁七万丈三上下不与丐丑专且世
丘丙业丛东丝両丢两严並丧个丫中丰串临丸丹
为主丽举乃久么义之乌乍乎乏乐乒乓乔乖乘乙
九乞也习乡书买乱乳乾了予争事二于亏云互五
井亚些亡亢交亥亦产亩享京亭亮亲人亿什仁仅
仆仇今介仍从仑仓仔仕他仗付仙代令以仪们仮
仰仲件价任份仿企伊伍伏伐休众优伙会伝伞伟
传伤伦伪伯估伴伶伸伺似佃但位低住佐佑体何
余佛作你佣佩佳使侄侈例侍供依侠価侣侥侦侧
侨侮侯侵便係促俄俊俏俐俗俘保信俩俭修俯俱
俺個倍倒倔倘候倚借倡値倦债值倾假偉偎偏做
停健側偶偷偽偿傅傍備储催傲傷傻働像僚僧僵
僻儒償優儿允元兄充兆先光克免兑兔党兜兢入
全八公六兮兰共关兴兵其具典兹养兼兽冀内円
冈冉册再冒冗写军农冠冤冥冬冯冰冲决况冶冷
冻净凄准凉凌减凑凛凝几凡凤処凭凯凰凳凶凸
凹出击函凿刀刁刃分切刊刑划列刘则刚创初删
判別刨利别刮到制刷券刹刺刻剂剃則削前剑剔
剖剤剥剧剩剪副割剿劈力劝办功加务劣动助努
劫励劲劳効势勃勇勉勋勒動勘募勢勤勧勺勾勿
匀匂包匆匈匕化北匙匠匣匪匹区医匾匿十千升
午卉半华协卑卒卓協单卖南単博卜占卡卢卤卦
卧卫卯印危即却卵卷卸卿厂厄厅历厉压厌厕厘
厚原厢厦厨厳去县叁参又叉及友双反収发叔取
受变叙叛叠口古句另叨叩只叫召叭叮可台叱史
右叶号司叹叼叽吁吃各吆合吉吊同名后吏吐向
吓吕吗君吝吞吟吠否吧吨吩含听吭吮启吱吴吵
吸吹吻吼吾呀呃呆呈告呐呑呕员呛呜呟呢周味
呵呻呼命咄咋和咏咐咒咔咕咖咙咧咨咪咬咱咳
咸咽哀品哄哆哇哈哉响哎哑哒哗哟哥哦哨哩哪
哭哮哲哺哼唁唆唇唉唐唠唤唧唬售唯唱唾啃啄
商啊問啡啤啥啦啪啰啸啼喂善喇喉喊喔喘喜喝
喧喩喳喷喻嗅嗓嗜嗡嗦嗨嗯嗷嗽嘀嘉嘘嘛嘱嘲
嘴嘶嘹嘻嘿噂噢器噩噪嚎嚣嚷嚼囊囚四回因团
囤园困囱囲围固国图圃圆圈土圣圧在地场圾址
均坊坎坏坐坑块坚坛坝坟坠坡坤坦坪坯坷垂垃
垄型垒垛垢垦垫垮埂埃埋城域埠執培基堂堆堕
堡堤堪堰報場堵塌塑塔塗塘塞塩填境墅墓墙增
墟墨墩壁壊壤士壬壮声壳壶壹处备変复夏夕外
多夜够夢大天太夫夭央夯失头夷夸夹夺奇奈奉
奋奏契奔奖套奠奢奥女奴奶奸她好如妃妄妆妇
妈妒妓妖妙妥妨妮妹妻姆姉姊始姐姑姓委姚姜
姥姨姻姿威娃娄娇娘娜娟娥娱娶婆婉婚婪婴婶
婿媒媚媳嫁嫂嫉嫌嫩嬉子孔孕字存孙孝孟季孤
学孩孵孽宁它宅宇守安宋完宏宗官宙定宛宜宝
 !"#$%&'()*+,-./0123
456789:;<=>?ABCDEFGH
IJKLMNOPQRSTUVWXYZ[\
]^_abcdefghijklmnopq
rstuvwxyz{|}~̀́̂̃̄̅̆
̖̗̘̙̇̈̉̊̋̌̍̎̏̐̑̒̓̔̕̚
̡̢̧̨̛̜̞̟̠̣̤̥̦̩̪̫̬̭̮̯
̰̱̲̳̹̻̼͇͈̽̾̿̀́͂̓̈́͆͊ͅ
͍͎͓͔͕͖͙͚͋͌͐͑͒͗͛͘͜͝͞͠
βλ—―“”…₭℃↑→↓─═█♫⭐ 、。
《》「」【】〜ぁあいぅうえおかがきぎくぐ
けげこごさざしじすずせぜそぞただちっつづ
てでとどなにぬねのはばぱひびぴふぶへべぺ
ほぼぽまみむめもゃやゅゆょよらりるれろわ
をんァアィイウェエォオカガキギクグケゲコ
ゴサザシジスズセソゾタダチッツテデトドニ
ネノハバパビフブプベペホボポマムメモャヤ
ュラリルレロンー㎩一丁七万丈三上下不与丐
丑专且世丘丙业丛东丝両丢两严並丧个丫中丰
串临丸丹为主丽举乃久么义之乌乍乎乏乐乒乓
乔乖乘乙九乞也习乡书买乱乳乾了予争事二于
亏云互五井亚些亡亢交亥亦产亩享京亭亮亲人
亿什仁仅仆仇今介仍从仑仓仔仕他仗付仙代令
以仪们仮仰仲件价任份仿企伊伍伏伐休众优伙
会伝伞伟传伤伦伪伫伯估伴伶伸伺似佃但位低
住佐佑体何余佛作你佣佩佳使侄侈例侍供依侠
価侣侥侦侧侨侮侯侵便係促俄俊俏俐俗俘保信
俩俭修俯俱俺個倍倏倒倔倘候倚借倡値倦债值
倾假偉偎偏做停健側偶偷偽偿傅傍備储催傲傷
傻働像僚僧僵僻儒償優儿允元兄充兆先光克免
兑兔党兜兢入全八公六兮兰共关兴兵其具典兹
养兼兽冀内円冇冈冉册再冒冗写军农冠冤冥冬
冯冰冲决况冶冷冻冽净凄准凉凋凌减凑凛凝几
凡凤処凭凯凰凳凶凸凹出击函凿刀刁刃分切刊
刑划列刘则刚创初删判別刨利别刮到制刷券刹
刺刻剂剃則削前剑剔剖剤剥剧剩剪副割剿劈力
劝办功加务劣动助努劫励劲劳効势勃勇勉勋勒
動勘募勢勤勧勺勾勿匀匂包匆匈匕化北匙匠匣
匪匹区医匾匿十千升午卉半华协卑卒卓協单卖
南単博卜占卡卢卤卦卧卫卯印危即却卵卷卸卿
厂厄厅历厉压厌厕厘厚原厢厦厨厳去县叁参又
叉及友双反収发叔取受变叙叛叠口古句另叨叩
只叫召叭叮可台叱史右叶号司叹叼叽吁吃各吆
合吉吊同名后吏吐向吓吕吗君吝吞吟吠否吧吨
吩含听吭吮启吱吴吵吸吹吻吼吾呀呃呆呈告呐
呑呕员呛呜呟呢周味呵呸呻呼命咄咆咋和咏咐
咒咔咕咖咙咚咦咧咨咪咬咯咱咳咸咽哀品哄哆
哇哈哉响哎哑哒哗哟哥哦哨哩哪哭哮哲哺哼唁
唆唇唉唐唠唤唧唬售唯唱唾啃啄商啊問啡啤啥
啦啧啪啰啸啼喂善喇喉喊喔喘喜喝喧喩喳喷喻
嗅嗓嗜嗡嗦嗨嗯嗷嗽嘀嘈嘉嘘嘛嘞嘱嘲嘴嘶嘹
嘻嘿噂噢器噩噪噬嚎嚣嚷嚼囊囚四回因团囤园
困囱囲围固国图圃圆圈土圣圧在地场圾址均坊
坎坏坐坑块坚坛坝坟坠坡坤坦坪坯坷垂垃垄型
垒垛垢垦垫垮埂埃埋城域埠執培基堂堆堕堡堤
堪堰報場堵塌塑塔塗塘塞塩填境墅墓墙增墟墨
墩壁壊壤士壬壮声壳壶壹处备変复夏夕外多夜
够夢大天太夫夭央夯失头夷夸夹夺奇奈奉奋奏
契奔奖套奠奢奥女奴奶奸她好如妃妄妆妇妈妒
妓妖妙妥妨妮妹妻姆姉姊始姐姑姓委姚姜姥姨
姻姿威娃娄娇娘娜娟娥娱娶婆婉婚婪婴婶婿媒
媚媳嫁嫂嫉嫌嫡嫩嬉子孔孕字存孙孝孟季孤学
孩孵孽宁它宅宇守安宋完宏宕宗官宙定宛宜宝
实実宠审客宣室宦宪宫宰害宴宵家容宽宾宿寂
寄寅密寇富寒寓寝寞察寡寥寨審寸对寺寻导対
寿封専射将尉尊小少尔尖尘尚尝尤尧尬就尴尸
@@ -64,142 +68,144 @@ uvwxyz{}~λ—―“”…₭ 、。「
層履屯山屹屿岁岂岔岖岗岛岩岭岳岸峡峦峨峭
峰峻崇崎崔崖崛崩崭嵌巅巍川州巡巢巣工左巧
巨巩巫差己已巳巴巷巾币市布帅帆师希帐帕帖
帘帚帜帝带席帮帯帰帷常帽幅幌幕幢干平年
幸幻幼幽幾广広庄庆庇床序庐库应底店庙庚
庞废度座庭庵庶康庸廉廊廓延廷建开异弃弄
式弓引弗弘弛弟张弥弦弧弯弱張強弹强归当
形彤彩彪彬彭彰影役彻彼往征径待很徊律後
徒従得徘徙御復循微德徹徽心必忆忌忍志忘
忠忧快忱念忽忿怀态怎怒怔怕怖怜思怠怡急
怨怪怯总恃恋恍恐恒恕恢恤恥恨恩恬恭息恰恳
恶恼悄悉悍悔悖悟悠患悦您悩悪悬悯悲悴悼情
惊惋惑惕惜惟惠惦惧惨惩惫惭惯惰想惶惹愁愈
愉意愕愚愛感愣愤愧愿慈態慌慎慕慢慣慧慨慰
慷憋憎憔憨憶憾懂懈懊懐懒懦戈戊戌戎戏成我
戒或战戚截戳戴户戻房所扁扇扉手才扎扑扒打
扔托扛扣执扩扫扬扭扮扯扰扳扶批扼找承技抄
把抑抒抓投抖抗折抚抛抜択抠抡抢护报披抬抱
抵抹押抽抿拂拄担拆拇拉拌拍拎拐拒拓拔拖拗
拘拙招拜拟拡拢拣拥拦拧拨择括拭拯拱拳拴拷
拼拽拾拿持挂指按挎挑挖挚挟挠挡挣挤挥挨挪
挫振挺挽捂捅捆捉捌捍捎捏捐捕捜捞损捡换捣
捧据捶捷捺捻掀掂授掉掌掏掐排掘掠探接控推
掩措掰掷掺揉揍描提插換握揣揩揪揭援揺揽搀
搁搂搅損搏搓搔搜搞搬搭携摄摆摇摊摔摘摧摩
摸摹撇撑撒撕撞撤撩撬播撮撰撵撼擂擅操擎擒
擦攀支收改攻放政故效敌敏救敗教敛敞敢散敦
敬数敲整敵敷文斋斌斑斗料斜斟斤斥斧斩断斯
新方施旁旅旋族旗无既日旦旧旨早旬旭旱时旷
旺昂昆昌明昏易昔星映春昧昨昭是昼显時晃晋
晌晒晓晕晚晦晨晩普景晰晴晶智晾暂暇暑暖暗
暮暴曙曝曰曲更書曹曼曾替最月有朋服朔朗望
朝期朦木未末本术朱朴朵机朽杀杂权杆杉李杏
材村杖杜束杠条来杨杭杯杰東松板极构枉析枕
林枚果枝枠枢枣枪枫枯架柄柏某柑柒染柔柜柠
查柬柱柳柴査柿栅标栈栋栏树栓栖栗校株样核
根格栽桂桃框案桌桐桑桔档桥桦桨桩桶梁梅梆
梗梢梦梧梨梭梯械梳检棉棋棍棒棕棘棚棠森棱
棵棺椅植椎椒検椭椰椿楚業楷楼楽概榄榆榔榕
榜榨榴構槐様槛槽標樟模権横樱橄橘橙機橡橱
檀檐檬欠次欢欣欧欲欺款歇歉歌止正此步武歧
歩歪歹死歼殃殉殊残殖殴段殷殺殿毁毅母毎每
毒比毕毙毛毡毫毯氏民氓气気氛氢氧氨氮氯水
永汁求汇汉汗汚汛汝汞江池污汤汪汰汹決汽沁
沃沈沉沐沙沛沟没沢沥沦沧沪沫沮河沸油治沼
沽沾沿況泄泉泊泌法泛泞泡波泣泥注泪泰泳泵
泻泼泽洁洋洒洗洛洞津洩洪洲活洼洽派流浅浆
浇浊测济浏浑浓浙浦浩浪浮浴海浸涂消涉涌涕
涛涝涡涣涤润涧涨涩涮涯液涵淀淆淋淌淑淘淡
淤淫淮深淳混淹添清済渉渊渐渔渗渝渠渡渣渤
温港渲渴游渺湃湖湘湾湿溃溅溉源準溜溢溪溯
溶溺滇滋滑滔滚滞满滤滥滨滩滴漂漆漏漓演漠
漫漱漾潇潘潜潤潭潮澄澈澎澜澡澳激濒瀑灌火
灭灯灰灵灶灸灼灾灿炉炊炎炒炕炫炬炭炮炸点
炼烁烂烈烘烙烛烟烤烦烧烫热烹焉焊焕焚無焦
焰然焼煌煎煙煞煤照煩煮煽熄熊熏熔熙熟熬熱
燃燕燥爆爪爬爱爵父爷爸爹爽片版牌牙牛牡牢
牧物牲牵特牺犀犁犬犯状犹狂狈狐狗狞狠狡独
狭狮狰狱狸狼猎猖猛猜猩猪猫猬献猴猾猿玄率
玉王玖玛玩玫环现玲玷玻珊珍珠班現球琅理琉
琐琢琳琴琼瑙瑞瑟瑰璃璧環瓜瓢瓣瓤瓦瓶瓷甘
甚甜生産甥用甩甫田由甲申电男甸町画畅界畏
畔留畜略番異畴畸疆疏疑疗疙疚疟疤疫疮疯疲
疹疼疾病症痊痒痕痘痛痢痪痰痴痹瘟瘤瘦瘩瘪
瘫瘸瘾療癌癣癸発登白百皂的皆皇皓皖皮皱皺
皿盆盈益盏盐监盒盔盖盗盘盛盟目盯盲直相盹
盼盾省眉看真眠眨眩眯眶眷眺眼着睁睐睛睡督
睦睫睬睹瞄瞅瞎瞒瞧瞩瞪瞬瞭瞳瞻矗矛矢矣知
矩矫短矮石矾矿码砂砌砍研砕砖砚砰破砸砾础
硅硕硝硫硬确碌碍碎碑碗碘碟碧碰碱碳確碾磁
磅磊磕磨磷礁示礼社祀祈祖祝神祟祠祥票祭祷
祸禀禁禄禅福禹离禽禾秀私秃秆秉秋种科秒秘
租秤秦秧秩积称秸移秽稀程稍税稚稠稳稻稼稽
稿穆積穏穗穴究穷空穿突窃窄窍窑窒窓窖窗窘
窜窝窟窥窿立竖站竞竟章竣童竭端竹竿笋笑笔
笙笛符笨第笺笼筆等筋筏筐筑筒答策筛筝筷筹
签简箕算管箩箫箭箱節範篇篓篡篮篱篷簇簡簧
簸簿籍米类籽粉粋粒粗粘粟粤粥粪粮粱粹精糊
糕糖糙糟糠糯系紊純素索紧紫累細終組経結絮
絵絶継続綺緑緒線練縛縮績繁繋繰纠红纤约级
纪纫纬纯纱纲纳纵纷纸纹纺纽线练组绅细织终
绊绍绎经绑绒结绕绘给绚络绝绞统绢绣继绩绪
续绰绳维绵绷绸综绽绿缀缅缆缉缎缓缔缕编缘
缚缝缠缤缩缭缰缴缶缸缺罐网罕罗罚罢罩罪置
署羊美羔羚羞羡群羹羽翁翅翔翘翠翩翰翻翼耀
老考者而耍耐耕耗耘耙耳耸耻耽耿聂聆聊聋职
联聘聚聞聪肃肆肇肉肋肌肖肘肚肛肝肠股肢肤
肥肩肪肮肯育肴肺肾肿胀胁胃胆背胎胖胚胜胞
胡胧胰胳胶胸能脂脆脉脊脏脐脑脓脖脚脯脱脳
脸脾腊腋腐腔腕腥腮腰腹腺腻腾腿膀膊膏膛膜
膝膨臀臂臊臣自臭至致臼舀舅舆興舌舍舎舒舔
舞舟航般舰舱舵舶船艇艘良艰色艳艺艾节芋芒
芙芜芝芥芦芬芭芯花芳芸芹芽苇苍苏苑苔苗苛
苞苟若苦英苹茁茂范茄茅茉茎茧茫茬茵茶茸荆
草荐荒荔荡荣荤荧荫药荷莉莫莱莲获莹莺莽菇
菊菌菜菠菩菱菲萄萌萍萎萝萤营萧萨落葉著葛
葡董葫葬葱葵蒂蒋蒙蒜蒲蒸蓄蓉蓝蓬蔑蔓蔗蔚
蔡蔬蔵蔼蔽蕉蕊蕴蕾薄薇薛薪薬薯藉藏藐藕藤
藻蘑虎虏虐虑虚虫虹虽虾蚀蚁蚂蚊蚌蚓蚕蚣蚤
蚪蚯蛀蛇蛋蛙蛛蛤蛮蛾蜀蜂蜈蜒蜓蜕蜗蜘蜜蜡
蜻蝇蝉蝌蝎蝗蝙蝠蝴蝶螃融螺蟀蟆蟋蟹蠕蠢血
衅行衍術衔街衙衝衡衣补表衫衬衰衷袁袄袋袍
袖袜被袭袱裁裂装裏裕裙補裤裳裸裹褂褄複褐
褒褥褪襟西要覆見規視覗覚覧観见观规觅视览
觉角解触言計訓記設許診証評試詰話該詳誉誊
認誓語説読誰調請論謝識警譬議计订认讥讨让
训议讯记讲讳讶许讹论讼讽设访诀证评识诈诉
诊词译试诗诚话诞诡询该详诫诬语误诱诲说诵
请诸诺读诽课谁调谅谆谈谊谋谍谎谐谓谚谜谢
谣谤谦谨谬谭谱谴谷豁豆豊豌豚象豪豫豹豺貌
負費貼資質贅贝贞负贡财责贤败账货质贩贪贫
贬购贮贯贰贱贴贵贷贸费贺贻贼贾贿赁赂赃资
赋赌赎赏赐赔赖赘赚赛赞赠赡赢赣赤赦赫走赴
赵赶起趁超越趋趟趣足趴趾跃跋跌跑跛距跟跡
跤跨跪路跳践跷跺踊踏踝踢踩踪踱蹂蹄蹈蹋蹦
蹬蹭蹲躁躏身躬躯躲躺車転軽載轄车轧轨轩转
轮软轰轴轻载轿较辅辆辈辉辐辑输辖辗辙辛辜
辞辟辣辨辩辫辰辱边辺辻込辽达辿迁迂迄迅过
迈迎运近返还这进远违连迟迢迪迫迭述迷迹追
退送适逃逆选逊透逐递途逗通逛逝逞速造逢連
逮逸逻逼逾遂遅遇運遍過遏道違遗遠遣遥遭遮
遵選避邀邑邓那邦邪邮邻郁郊郎郑部郭都鄂鄙
酉酌配酒酗酝酣酥酪酬酱酵酷酸酿醇醉醋醒采
释里重野量金針鉢鉴鋼錆録鏡针钉钓钙钝钞钟
钠钢钥钦钧钩钮钱钳钻钾铁铃铅铐铛铜铝铭铲
帘帚帜帝带席帮帯帰帷常帽幅幌幕幢干平年
幸幻幼幽幾广広庄庆庇床序庐库应底店庙庚
庞废度座庭庵庶康庸廉廊廓延廷建开异弃弄
式弓引弗弘弛弟张弥弦弧弯弱張強弹强归当
形彤彩彪彬彭彰影役彻彼往征径待很徊律後
徒従得徘徙御復循微德徹徽心必忆忌忍志忘
忠忧快忱念忽忿怀态怎怒怔怕怖怜思怠怡急
怨怪怯总恃恋恍恐恒恕恢恤恥恨恩恬恭息
恰恳恶恼悄悉悍悔悖悟悠患悦您悩悪悬悯悲悴
悼情惊惋惑惕惜惟惠惦惧惨惩惫惭惯惰想惶惹
愁愈愉意愕愚愛感愣愤愧愿慈態慌慎慕慢慣慧
慨慰慷憋憎憔憨憶憾懂懈懊懐懒懦戈戊戌戎戏
成我戒或战戚截戳戴户戻房所扁扇扉手才扎扑
扒打扔托扛扣执扩扫扬扭扮扯扰扳扶批扼找承
技抄把抑抒抓投抖抗折抚抛抜択抠抡抢护报披
抬抱抵抹押抽抿拂拄担拆拇拉拌拍拎拐拒拓拔
拖拗拘拙招拜拟拡拢拣拥拦拧拨择括拭拯拱拳
拴拷拼拽拾拿持挂指按挎挑挖挚挟挠挡挣挤挥
挨挪挫振挺挽捂捅捆捉捌捍捎捏捐捕捜捞损
捡换捣捧据捶捷捺捻掀掂授掉掌掏掐排掘掠探
接控推掩措掰掷掺揉揍描提插換握揣揩揪揭援
揺揽搀搁搂搅損搏搓搔搜搞搬搭携摄摆摇摊摔
摘摧摩摸摹撇撑撒撕撞撤撩撬播撮撰撵撼擂
擅操擎擒擦攀攒攥支收改攻放政故效敌敏救敗
教敛敞敢散敦敬数敲整敵敷文斋斌斑斗料斜斟
斤斥斧斩断斯新方施旁旅旋族旗无既日旦旧旨
早旬旭旱时旷旺昂昆昌明昏易昔星映春昧昨昭
是昼显時晃晋晌晒晓晕晚晦晨晩普景晰晴晶智
晾暂暇暑暖暗暮暴曙曝曰曲更書曹曼曾替最月
有朋服朔朗望朝期朦木未末本术朱朴朵机朽杀
杂权杆杉李杏材村杖杜束杠条来杨杭杯杰東松
板极构枉析枕林枚果枝枠枢枣枪枫枯架柄柏某
柑柒染柔柜柠查柬柱柳柴査柿栅标栈栋栏树栓
栖栗校株样核根格栽桂桃框案桌桐桑桔档桥桦
桨桩桶梁梅梆梗梢梦梧梨梭梯械梳检棉棋棍棒
棕棘棚棠森棱棵棺椅植椎椒検椭椰椿楚業楷楼
楽概榄榆榔榕榜榨榴構槐様槛槽標樟模権横樱
橄橘橙機橡橱檀檐檬欠次欢欣欧欲欺款歇歉歌
止正此步武歧歩歪歹死歼殃殉殊残殖殴段殷殺
殿毁毅母毎每毒比毕毙毛毡毫毯氏民氓气気氛
氢氧氨氮氯水永汁求汇汉汗汚汛汝汞江池污
汤汪汰汹決汽沁沃沈沉沐沙沛沟没沢沥沦沧沪
沫沮河沸油治沼沽沾沿況泄泉泊泌法泛泞泡波
泣泥注泪泰泳泵泻泼泽洁洋洒洗洛洞津洩洪洲
活洼洽派流浅浆浇浊测济浏浑浓浙浦浩浪浮浴
海浸涂消涉涌涕涛涝涡涣涤润涧涨涩涮涯液涵
淀淆淋淌淑淘淡淤淫淮深淳混淹添清済渉渊渐
渔渗渝渠渡渣渤温港渲渴游渺湃湖湘湾湿溃溅
溉源準溜溢溪溯溶溺滇滋滑滔滚滞满滤滥滨滩
滴漂漆漏漓演漠漫漱漾潇潘潜潤潭潮澄澈澎澜
澡澳激濒瀑灌火灭灯灰灵灶灸灼灾灿炉炊炎炒
炕炫炬炭炮炸点炼烁烂烈烘烙烛烟烤烦烧烫热
烹焉焊焕焚無焦焰然焼煌煎煙煞煤照煩煮煽熄
熊熏熔熙熟熬熱燃燕燥爆爪爬爱爵父爷爸爹爽
片版牌牙牛牡牢牧物牲牵特牺犀犁犬犯状犹狂
狈狐狗狞狠狡独狭狮狰狱狸狼猎猖猛猜猩猪猫
猬献猴猾猿玄率玉王玖玛玩玫环现玲玷玻珊珍
珠班現球琅理琉琐琢琳琴琼瑙瑞瑟瑰璃璧環瓜
瓢瓣瓤瓦瓶瓷甘甚甜生産甥用甩甫田由甲申电
男甸町画畅界畏畔留畜略番異畴畸疆疏疑疗疙
疚疟疤疫疮疯疲疹疼疾病症痊痒痕痘痛痢痪痰
痴痹瘟瘤瘦瘩瘪瘫瘸瘾療癌癣癸発登白百皂的
皆皇皓皖皮皱皺皿盆盈益盏盐监盒盔盖盗盘盛
盟目盯盲直相盹盼盾省眉看真眠眨眩眯眶眷眺
眼着睁睐睛睡督睦睫睬睹瞄瞅瞎瞒瞧瞩瞪
瞬瞭瞳瞻矗矛矢矣知矩矫短矮石矾矿码砂砌砍
研砕砖砚砰破砸砾础硅硕硝硫硬确碌碍碎碑碗
碘碟碧碰碱碳確碾磁磅磊磕磨磷礁示礼社祀祈
祖祝神祟祠祥票祭祷祸禀禁禄禅福禹离禽禾秀
私秃秆秉秋种科秒秘租秤秦秧秩积称秸移秽稀
程稍税稚稠稳稻稼稽稿穆積穏穗穴究穷空穿突
窃窄窍窑窒窓窖窗窘窜窝窟窣窥窸窿立竖站竞
竟章竣童竭端竹竿笋笑笔笙笛符笨第笺笼筆等
筋筏筐筑筒答策筛筝筷筹签简箕算管箩箫箭箱
節範篇篓篡篮篱篷簇簡簧簸簿籍米类籽粉粋粒
粗粘粟粤粥粪粮粱粹精糊糕糖糙糟糠糯系紊純
素索紧紫累細終組経結絮絵絶継続綺緑緒線練
縛縮績繁繋繰纠红纤约级纪纫纬纯纱纲纳纵纷
纸纹纺纽线练组绅细织终绊绍绎经绑绒结绕绘
给绚络绝绞统绢绣继绩绪续绰绳维绵绷绸综绽
绿缀缅缆缉缎缓缔缕编缘缚缝缠缤缩缭缰缴缶
缸缺罐网罕罗罚罢罩罪置署羊美羔羚羞羡群羹
羽翁翅翔翘翠翩翰翻翼耀老考者而耍耐耕耗耘
耙耦耳耷耸耻耽耿聂聆聊聋职联聘聚聞聪肃肆
肇肉肋肌肖肘肚肛肝肠股肢肤肥肩肪肮肯育肴
肺肾肿胀胁胃胆背胎胖胚胜胞胡胧胰胳胶胸能
脂脆脉脊脏脐脑脓脖脚脯脱脳脸脾腊腋腐腔腕
腥腮腰腹腺腻腾腿膀膊膏膛膜膝膨臀臂臊臣自
臭至致臼舀舅舆興舌舍舎舒舔舞舟航般舰舱舵
舶船艇艘良艰色艳艺艾节芋芒芙芜芝芥芦芬芭
芯花芳芸芹芽苇苍苏苑苔苗苛苞苟若苦英苹茁
茂范茄茅茉茎茧茫茬茵茶茸荆草荐荒荔荡荣荤
荧荫药荷莉莫莱莲获莹莺莽菇菊菌菜菠菩菱菲
萄萌萍萎萝萤营萧萨落葉著葛葡董葫葬葱葵蒂
蒋蒙蒜蒲蒸蓄蓉蓝蓬蔑蔓蔗蔚蔡蔬蔵蔼蔽蕉蕊
蕴蕾薄薇薛薪薬薯藉藏藐藕藤藻蘑虎虏虐虑虚
虫虹虽虾蚀蚁蚂蚊蚌蚓蚕蚣蚤蚪蚯蛀蛇蛋蛙蛛
蛤蛮蛾蜀蜂蜈蜒蜓蜕蜗蜘蜜蜡蜻蝇蝉蝌蝎蝗蝙
蝠蝴蝶螃融螺蟀蟆蟋蟹蠕蠢血衅行衍術衔街衙
衝衡衣补表衫衬衰衷袁袄袋袍袖袜被袭袱裁裂
装裏裕裙補裤裳裸裹褂褄複褐褒褥褪襟西要覆
見規視覗覚覧観见观规觅视览觉角解触言計訓
記訝設許診証評試詰話該詳誉誊認誓語説読誰
調請論謝識警譬議计订认讥讨让训议讯记讲讳
讶许讹论讼讽设访诀证评识诈诉诊词译试诗诚
话诞诡询该详诫诬语误诱诲说诵请诸诺读诽课
谁调谅谆谈谊谋谍谎谐谓谚谜谢谣谤谦谨谬谭
谱谴谷豁豆豊豌豚象豪豫豹豺貌負費貼資質贅
贝贞负贡财责贤败账货质贩贪贫贬购贮贯贰贱
贴贵贷贸费贺贻贼贾贿赁赂赃资赋赌赎赏赐赔
赖赘赚赛赞赠赡赢赣赤赦赫走赴赵赶起趁超越
趋趟趣足趴趾跃跋跌跑跛距跟跡跤跨跪路跳践
跷跺踊踏踝踢踩踪踮踱蹂蹄蹈蹋蹦蹬蹭蹲躁躏
身躬躯躲躺車転軽載輪轄车轧轨轩转轮软轰轱
轴轻载轿较辅辆辈辉辐辑输辖辗辘辙辛辜辞辟
辣辨辩辫辰辱边辺辻込辽达辿迁迂迄迅过迈迎
运近返还这进远违连迟迢迪迫迭述迷迹追退送
适逃逆选逊透逐递途逗通逛逝逞速造逢連逮逸
逻逼逾遂遅遇運遍過遏道違遗遠遣遥遭遮遵選
避邀邑邓那邦邪邮邻郁郊郎郑部郭都鄂鄙酉酌
配酒酗酝酣酥酪酬酱酵酷酸酿醇醉醋醒采释里
重野量金針鉢鉴鋼錆録鏡针钉钓钙钝钞钟钠钢
钥钦钧钩钮钱钳钻钾铁铃铅铆铐铛铜铝铬铭铲
银铸铺链销锁锄锅锈锋锌锐错锚锡锣锤锥锦键
锯锰锹锻镀镇镐镑镜镰镶長长門閉開間関閲
门闪闭问闯闰闲间闷闸闹闺闻闽阀阁阅阎阐阔
防阳阴阵阶阻阿附际陆陈陋陌降限陕陡院除
陥陨险陪陵陶陷陽隅隆隋階随隐隔隘隙障隠隧
隶难雀雁雄雅集雇雌雏雕離難雨雪雲雳零雷雹
雾需霄震霉霍霎霜霞霧露霸霹青靖静非靠靡面
革靴靶鞋鞍鞠鞭韦韧韩韭音韵響頃項頑頭頼題
額顔願類页顶顷项顺须顽顾顿颁颂预颅领颇颈
颊频颓颖颗题颜额颠颤風风飕飘飛飞食飲餐館
饥饭饮饰饱饲饵饶饺饼饿馁馅馆馈馋馍馏馒首
香馨駄駆騒马驮驯驰驱驳驴驶驹驻驼驾骂骄骆
骇验骏骑骗骚骡骤骨髓高鬓鬼魁魂魄魅魏魔魚
鮮鱼鲁鲍鲜鲤鲨鲫鲸鳄鳍鳖鳞鳥鸟鸡鸣鸥鸦鸭
鸯鸳鸵鸽鸿鹃鹅鹉鹊鹏鹤鹦鹰鹿麗麦麻黄黎黑
黒黔默黙黯鼎鼓鼠鼻齐齿龄龙龟!(),:;
锯锰锹锻镀镇镐镑镜镰镶長长門閉開間関閲
门闪闭问闯闰闲间闷闸闹闺闻闽阀阁阅
阐阔队阱防阳阴阵阶阻阿附际陆陈陋陌降限陕
陡院除陥陨险陪陵陶陷陽隅隆隋階随隐隔隘隙
障隠隧隶难雀雁雄雅集雇雌雏雕離難雨雪雲雳
零雷雹雾需霄震霉霍霎霜霞霧露霸霹青靖静
非靠靡面革靴靶鞋鞍鞠鞭韦韧韩韭音韵響頃項
頑頭頼題額顔願類页顶顷项顺须顽顾顿颁颂预
颅领颇颈颊频颓颖颗题颜额颠颤風风飕飘飛飞
食飲餐館饥饭饮饰饱饲饵饶饺饼饿馁馅馆馈馋
馍馏馒首香馨駄駆騒马驮驯驰驱驳驴驶驹驻驼
驾骂骄骆骇验骏骑骗骚骡骤骨髓高鬓鬼魁魂魄
魅魏魔魚鮮鱼鲁鲍鲜鲤鲨鲫鲸鳄鳍鳖鳞鳥鴉鸚
鸟鸡鸣鸥鸦鸭鸯鸳鸵鸽鸿鹃鹅鹉鹊鹏鹤鹦鹰鹿
麗麦麻黄黎黑黒黔默黙黯鼎鼓鼠鼻齐齿龄龙龟
갚계귨꾣뇤덅됬뚳램묈뷴뼭섉솳숅쉪싧썮썹쐣
펽풆퓳퓵!(),:;?
Binary file not shown.
@@ -0,0 +1,320 @@
fileFormatVersion: 2
guid: 56968f0ede176024f847a4140a39c498
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 20
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 256
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: iPhone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: option
rect:
serializedVersion: 2
x: 0
y: 64
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 60ac169ff803bf248a4aa3898c8259be
internalID: -1463717399
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: interactive
rect:
serializedVersion: 2
x: 64
y: 64
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 4fd714e8aad31b145b8633474876d2f9
internalID: -172482333
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: holding
rect:
serializedVersion: 2
x: 128
y: 64
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 44cadc0e93a1ad344ad08621926d26ed
internalID: -223383872
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: rotating
rect:
serializedVersion: 2
x: 192
y: 64
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 04f7ba4d3bed7f940bb12fe129eae8f2
internalID: -4526039
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: default
rect:
serializedVersion: 2
x: 0
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 9952e7f4c92e4204a90cef0d490914dc
internalID: -48030585
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: talking
rect:
serializedVersion: 2
x: 64
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: c3cc6d832c5f51e4fb86f68a2229caf5
internalID: 2124946583
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: next_talk
rect:
serializedVersion: 2
x: 128
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: de4b691cf288bca4399d2257410ad157
internalID: -2019865267
vertices: []
indices:
edges: []
weights: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable:
default: -48030585
holding: -223383872
interactive: -172482333
next_talk: -2019865267
option: -1463717399
rotating: -4526039
talking: 2124946583
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 9f6d62e6a25088645b040ee8261cdbc6
guid: 1547f70e189f2f94687f6d6cd7be9968
folderAsset: yes
DefaultImporter:
externalObjects: {}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 27ed0cfd47ea42b40a26573a730d541b
guid: 1abc30b848289ae49a4ac7c0a9b21775
folderAsset: yes
DefaultImporter:
externalObjects: {}
@@ -0,0 +1,352 @@
%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: 408fe45af4d321848a6c97f0356e8e5c, type: 3}
m_Name: "\u68C0\u7D22"
m_EditorClassIdentifier:
id: intro
displayName: "\u68C0\u7D22"
frames:
- sprite: {fileID: 7572446778344178281, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 0.aseprite"
sourceIndex: 0
- sprite: {fileID: -362215467011434174, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 1.aseprite"
sourceIndex: 1
- sprite: {fileID: -4996972306367436102, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 2.aseprite"
sourceIndex: 2
- sprite: {fileID: -6337999013739407263, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 3.aseprite"
sourceIndex: 3
- sprite: {fileID: 6998681854070186983, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 4.aseprite"
sourceIndex: 4
- sprite: {fileID: 6998681854070186983, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 5.aseprite"
sourceIndex: 5
- sprite: {fileID: 4876995554275767483, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 6.aseprite"
sourceIndex: 6
- sprite: {fileID: 4876995554275767483, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 7.aseprite"
sourceIndex: 7
- sprite: {fileID: -1351993313840321639, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 8.aseprite"
sourceIndex: 8
- sprite: {fileID: -1351993313840321639, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 9.aseprite"
sourceIndex: 9
- sprite: {fileID: 2953902384930482259, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 10.aseprite"
sourceIndex: 10
- sprite: {fileID: 2953902384930482259, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 11.aseprite"
sourceIndex: 11
- sprite: {fileID: -8223510296345293682, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 12.aseprite"
sourceIndex: 12
- sprite: {fileID: -8223510296345293682, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 13.aseprite"
sourceIndex: 13
- sprite: {fileID: 3051864550455069342, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 14.aseprite"
sourceIndex: 14
- sprite: {fileID: 3051864550455069342, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 15.aseprite"
sourceIndex: 15
- sprite: {fileID: 651861321318766745, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 16.aseprite"
sourceIndex: 16
- sprite: {fileID: 651861321318766745, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 17.aseprite"
sourceIndex: 17
- sprite: {fileID: 6360117332621625605, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 18.aseprite"
sourceIndex: 18
- sprite: {fileID: 2110206911040411466, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 19.aseprite"
sourceIndex: 19
- sprite: {fileID: -8552977822448111447, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 20.aseprite"
sourceIndex: 20
- sprite: {fileID: -2421474681753995496, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 21.aseprite"
sourceIndex: 21
- sprite: {fileID: -7492713014907810856, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 22.aseprite"
sourceIndex: 22
- sprite: {fileID: -7492713014907810856, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 23.aseprite"
sourceIndex: 23
- sprite: {fileID: -7720457564735758752, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 24.aseprite"
sourceIndex: 24
- sprite: {fileID: -7720457564735758752, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 25.aseprite"
sourceIndex: 25
- sprite: {fileID: 1880112101706155564, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 26.aseprite"
sourceIndex: 26
- sprite: {fileID: -3623330345461600317, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 27.aseprite"
sourceIndex: 27
- sprite: {fileID: -4821937940866556909, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 28.aseprite"
sourceIndex: 28
- sprite: {fileID: -4821937940866556909, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 29.aseprite"
sourceIndex: 29
- sprite: {fileID: -1230447193968539563, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 30.aseprite"
sourceIndex: 30
- sprite: {fileID: -1230447193968539563, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 31.aseprite"
sourceIndex: 31
- sprite: {fileID: -3452787567901838671, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 32.aseprite"
sourceIndex: 32
- sprite: {fileID: 2122325496977185700, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 33.aseprite"
sourceIndex: 33
- sprite: {fileID: -3452787567901838671, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 34.aseprite"
sourceIndex: 34
- sprite: {fileID: -3452787567901838671, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 35.aseprite"
sourceIndex: 35
- sprite: {fileID: -3838368664758933209, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 36.aseprite"
sourceIndex: 36
- sprite: {fileID: -3838368664758933209, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 37.aseprite"
sourceIndex: 37
- sprite: {fileID: -3838368664758933209, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 38.aseprite"
sourceIndex: 38
- sprite: {fileID: -3838368664758933209, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 39.aseprite"
sourceIndex: 39
- sprite: {fileID: -8974752115445585213, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 40.aseprite"
sourceIndex: 40
- sprite: {fileID: -8974752115445585213, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 41.aseprite"
sourceIndex: 41
- sprite: {fileID: -8974752115445585213, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 42.aseprite"
sourceIndex: 42
- sprite: {fileID: -8974752115445585213, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 43.aseprite"
sourceIndex: 43
- sprite: {fileID: 862258456695450341, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 200
frameName: "\u68C0\u7D22 44.aseprite"
sourceIndex: 44
- sprite: {fileID: -5458642827783654469, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 200
frameName: "\u68C0\u7D22 45.aseprite"
sourceIndex: 45
- sprite: {fileID: 4197853090659475812, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 200
frameName: "\u68C0\u7D22 46.aseprite"
sourceIndex: 46
- sprite: {fileID: -1155378306054861757, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 200
frameName: "\u68C0\u7D22 47.aseprite"
sourceIndex: 47
- sprite: {fileID: -1204860713, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 48.aseprite"
sourceIndex: 48
- sprite: {fileID: -1204860713, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 49.aseprite"
sourceIndex: 49
- sprite: {fileID: 2080999944, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 50.aseprite"
sourceIndex: 50
- sprite: {fileID: 433384928, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 51.aseprite"
sourceIndex: 51
- sprite: {fileID: -804306678, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 52.aseprite"
sourceIndex: 52
- sprite: {fileID: 472963922, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 53.aseprite"
sourceIndex: 53
- sprite: {fileID: 318794721, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 54.aseprite"
sourceIndex: 54
- sprite: {fileID: 318794721, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 55.aseprite"
sourceIndex: 55
- sprite: {fileID: 1203749929, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 56.aseprite"
sourceIndex: 56
- sprite: {fileID: 1203749929, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 57.aseprite"
sourceIndex: 57
- sprite: {fileID: 1203749929, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 58.aseprite"
sourceIndex: 58
- sprite: {fileID: 1203749929, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 59.aseprite"
sourceIndex: 59
- sprite: {fileID: -1984404506, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 60.aseprite"
sourceIndex: 60
- sprite: {fileID: 1047793495, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 61.aseprite"
sourceIndex: 61
- sprite: {fileID: 174328586, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 62.aseprite"
sourceIndex: 62
- sprite: {fileID: 1047793495, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 63.aseprite"
sourceIndex: 63
- sprite: {fileID: -1198657009, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 64.aseprite"
sourceIndex: 64
- sprite: {fileID: -1198657009, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 65.aseprite"
sourceIndex: 65
- sprite: {fileID: -1198657009, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 66.aseprite"
sourceIndex: 66
- sprite: {fileID: -1198657009, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 67.aseprite"
sourceIndex: 67
- sprite: {fileID: -342147049, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 68.aseprite"
sourceIndex: 68
- sprite: {fileID: -342147049, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 69.aseprite"
sourceIndex: 69
- sprite: {fileID: -342147049, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 70.aseprite"
sourceIndex: 70
- sprite: {fileID: -342147049, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 71.aseprite"
sourceIndex: 71
- sprite: {fileID: 1733318757, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 72.aseprite"
sourceIndex: 72
- sprite: {fileID: 1733318757, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 73.aseprite"
sourceIndex: 73
- sprite: {fileID: 1733318757, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 74.aseprite"
sourceIndex: 74
- sprite: {fileID: 1733318757, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 50
frameName: "\u68C0\u7D22 75.aseprite"
sourceIndex: 75
- sprite: {fileID: -84262779, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 200
frameName: "\u68C0\u7D22 76.aseprite"
sourceIndex: 76
- sprite: {fileID: 1947069765, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 200
frameName: "\u68C0\u7D22 77.aseprite"
sourceIndex: 77
- sprite: {fileID: 1547981314, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 200
frameName: "\u68C0\u7D22 78.aseprite"
sourceIndex: 78
- sprite: {fileID: 1904593827, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
durationMs: 200
frameName: "\u68C0\u7D22 79.aseprite"
sourceIndex: 79
speed: 1
defaultEndBehavior: 2
hasImportInfo: 0
importInfo:
importSourceId:
sourceTagName:
isMissingFromSource: 0
hasStandaloneImportSource: 1
standaloneImportSource:
texture: {fileID: 2800000, guid: 060544baeb316214698dd0ea788a63dd, type: 3}
asepriteJson: {fileID: 4900000, guid: 9281f1c256bc53140a36bb9439d2d5d6, type: 3}
pivot: {x: 0.5, y: 0.5}
manageSpriteSlicing: 1
lastSourceHash: a0c976cb23f6f387b5a0d81c36299682ec4e5b14a0894eb63b5aaf19fc228739
lastImportedTagName: intro
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: dc9666f5991e1be4bb0e5f149055f91a
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,666 @@
{ "frames": {
"检索 0.aseprite": {
"frame": { "x": 0, "y": 0, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 1.aseprite": {
"frame": { "x": 768, "y": 0, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 2.aseprite": {
"frame": { "x": 1536, "y": 0, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 3.aseprite": {
"frame": { "x": 2304, "y": 0, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 4.aseprite": {
"frame": { "x": 3072, "y": 0, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 5.aseprite": {
"frame": { "x": 3072, "y": 0, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 6.aseprite": {
"frame": { "x": 0, "y": 432, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 7.aseprite": {
"frame": { "x": 0, "y": 432, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 8.aseprite": {
"frame": { "x": 768, "y": 432, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 9.aseprite": {
"frame": { "x": 768, "y": 432, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 10.aseprite": {
"frame": { "x": 1536, "y": 432, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 11.aseprite": {
"frame": { "x": 1536, "y": 432, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 12.aseprite": {
"frame": { "x": 2304, "y": 432, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 13.aseprite": {
"frame": { "x": 2304, "y": 432, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 14.aseprite": {
"frame": { "x": 3072, "y": 432, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 15.aseprite": {
"frame": { "x": 3072, "y": 432, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 16.aseprite": {
"frame": { "x": 0, "y": 864, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 17.aseprite": {
"frame": { "x": 0, "y": 864, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 18.aseprite": {
"frame": { "x": 768, "y": 864, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 19.aseprite": {
"frame": { "x": 1536, "y": 864, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 20.aseprite": {
"frame": { "x": 2304, "y": 864, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 21.aseprite": {
"frame": { "x": 3072, "y": 864, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 22.aseprite": {
"frame": { "x": 0, "y": 1296, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 23.aseprite": {
"frame": { "x": 0, "y": 1296, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 24.aseprite": {
"frame": { "x": 768, "y": 1296, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 25.aseprite": {
"frame": { "x": 768, "y": 1296, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 26.aseprite": {
"frame": { "x": 1536, "y": 1296, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 27.aseprite": {
"frame": { "x": 2304, "y": 1296, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 28.aseprite": {
"frame": { "x": 3072, "y": 1296, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 29.aseprite": {
"frame": { "x": 3072, "y": 1296, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 30.aseprite": {
"frame": { "x": 0, "y": 1728, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 31.aseprite": {
"frame": { "x": 0, "y": 1728, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 32.aseprite": {
"frame": { "x": 768, "y": 1728, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 33.aseprite": {
"frame": { "x": 1536, "y": 1728, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 34.aseprite": {
"frame": { "x": 768, "y": 1728, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 35.aseprite": {
"frame": { "x": 768, "y": 1728, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 36.aseprite": {
"frame": { "x": 2304, "y": 1728, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 37.aseprite": {
"frame": { "x": 2304, "y": 1728, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 38.aseprite": {
"frame": { "x": 2304, "y": 1728, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 39.aseprite": {
"frame": { "x": 2304, "y": 1728, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 40.aseprite": {
"frame": { "x": 3072, "y": 1728, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 41.aseprite": {
"frame": { "x": 3072, "y": 1728, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 42.aseprite": {
"frame": { "x": 3072, "y": 1728, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 43.aseprite": {
"frame": { "x": 3072, "y": 1728, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 44.aseprite": {
"frame": { "x": 0, "y": 2160, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 200
},
"检索 45.aseprite": {
"frame": { "x": 768, "y": 2160, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 200
},
"检索 46.aseprite": {
"frame": { "x": 1536, "y": 2160, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 200
},
"检索 47.aseprite": {
"frame": { "x": 2304, "y": 2160, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 200
},
"检索 48.aseprite": {
"frame": { "x": 3072, "y": 2160, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 49.aseprite": {
"frame": { "x": 3072, "y": 2160, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 50.aseprite": {
"frame": { "x": 0, "y": 2592, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 51.aseprite": {
"frame": { "x": 768, "y": 2592, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 52.aseprite": {
"frame": { "x": 1536, "y": 2592, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 53.aseprite": {
"frame": { "x": 2304, "y": 2592, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 54.aseprite": {
"frame": { "x": 3072, "y": 2592, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 55.aseprite": {
"frame": { "x": 3072, "y": 2592, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 56.aseprite": {
"frame": { "x": 0, "y": 3024, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 57.aseprite": {
"frame": { "x": 0, "y": 3024, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 58.aseprite": {
"frame": { "x": 0, "y": 3024, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 59.aseprite": {
"frame": { "x": 0, "y": 3024, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 60.aseprite": {
"frame": { "x": 768, "y": 3024, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 61.aseprite": {
"frame": { "x": 1536, "y": 3024, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 62.aseprite": {
"frame": { "x": 2304, "y": 3024, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 63.aseprite": {
"frame": { "x": 1536, "y": 3024, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 64.aseprite": {
"frame": { "x": 3072, "y": 3024, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 65.aseprite": {
"frame": { "x": 3072, "y": 3024, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 66.aseprite": {
"frame": { "x": 3072, "y": 3024, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 67.aseprite": {
"frame": { "x": 3072, "y": 3024, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 68.aseprite": {
"frame": { "x": 0, "y": 3456, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 69.aseprite": {
"frame": { "x": 0, "y": 3456, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 70.aseprite": {
"frame": { "x": 0, "y": 3456, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 71.aseprite": {
"frame": { "x": 0, "y": 3456, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 72.aseprite": {
"frame": { "x": 768, "y": 3456, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 73.aseprite": {
"frame": { "x": 768, "y": 3456, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 74.aseprite": {
"frame": { "x": 768, "y": 3456, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 75.aseprite": {
"frame": { "x": 768, "y": 3456, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 50
},
"检索 76.aseprite": {
"frame": { "x": 1536, "y": 3456, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 200
},
"检索 77.aseprite": {
"frame": { "x": 2304, "y": 3456, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 200
},
"检索 78.aseprite": {
"frame": { "x": 3072, "y": 3456, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 200
},
"检索 79.aseprite": {
"frame": { "x": 0, "y": 3888, "w": 768, "h": 432 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 432 },
"sourceSize": { "w": 768, "h": 432 },
"duration": 200
}
},
"meta": {
"app": "https://www.aseprite.org/",
"version": "1.3.17.2-x64",
"image": "检索.png",
"format": "RGBA8888",
"size": { "w": 4096, "h": 4320 },
"scale": "1",
"frameTags": [
{ "name": "intro", "from": 0, "to": 79, "direction": "forward", "color": "#000000ff" }
],
"layers": [
{ "name": "black", "opacity": 89, "blendMode": "normal" },
{ "name": "saomiao", "opacity": 255, "blendMode": "normal" },
{ "name": "RIGHT", "opacity": 255, "blendMode": "normal" },
{ "name": "四进制框放大", "opacity": 255, "blendMode": "normal" },
{ "name": "四进制", "opacity": 255, "blendMode": "normal" },
{ "name": "head", "opacity": 255, "blendMode": "normal" },
{ "name": "代码", "opacity": 255, "blendMode": "normal" },
{ "name": "left", "opacity": 255, "blendMode": "normal" }
],
"slices": [
]
}
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9281f1c256bc53140a36bb9439d2d5d6
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,130 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-5039998276998606052
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: 6d1afb0ade1f46879f33b8173a66abbf, type: 3}
m_Name: Frame 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: 3051057393504210916}
m_Duration: 5.2
m_TimeScale: 1
m_ParentTrack: {fileID: -5039998276998606052}
m_EaseInDuration: 0
m_EaseOutDuration: 0
m_BlendInDuration: 0
m_BlendOutDuration: 0
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: "\u68C0\u7D22"
m_Markers:
m_Objects: []
--- !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: "\u68C0\u7D22Timeline"
m_EditorClassIdentifier:
m_Version: 0
m_Tracks:
- {fileID: -5039998276998606052}
m_FixedDuration: 0
m_EditorSettings:
m_Framerate: 60
m_ScenePreview: 1
m_DurationMode: 0
m_MarkerTrack: {fileID: 0}
--- !u!114 &3051057393504210916
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: c908631f9d7446ac86914dce3f1927af, type: 3}
m_Name: FrameAnimationTimelineClip
m_EditorClassIdentifier:
frameClip: {fileID: 11400000, guid: dc9666f5991e1be4bb0e5f149055f91a, type: 2}
synchronizedDurationSource: {fileID: 11400000, guid: dc9666f5991e1be4bb0e5f149055f91a, type: 2}
hasSynchronizedDuration: 1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e1a44318b2fafba47a4a296ddd9a91d2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: d0b8fb23239461942ae20b421af10a7c
guid: 434ffc1efd0f27340b4b6701baefdceb
folderAsset: yes
DefaultImporter:
externalObjects: {}
@@ -0,0 +1,349 @@
fileFormatVersion: 2
guid: 5f045a8dca074cc4f9cebb2bea1c020b
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 62a9f0aa5b59740cfbadc7e5f9823bb0, type: 3}
textureImporterSettings:
alphaSource: 1
mipMapMode: 0
enableMipMap: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
convertToNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
swizzle: 50462976
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
nPOTScale: 1
sRGBTexture: 1
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 30
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 0
flipbookColumns: 0
ignorePngGamma: 0
cookieMode: 0
filterMode: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
normalMap: 0
textureFormat: 0
maxTextureSize: 0
lightmap: 0
compressionQuality: 0
linearTexture: 0
grayScaleToAlpha: 0
rGBM: 0
cubemapConvolutionSteps: 0
cubemapConvolutionExponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
applyGammaDecoding: 0
previousAsepriteImporterSettings:
fileImportMode: 1
importHiddenLayers: 0
layerImportMode: 0
defaultPivotSpace: 1
defaultPivotAlignment: 0
customPivotPosition: {x: 0.5, y: 0.5}
spritePadding: 0
generateModelPrefab: 1
generateAnimationClips: 0
addSortingGroup: 1
addShadowCasters: 0
asepriteImporterSettings:
fileImportMode: 1
importHiddenLayers: 0
layerImportMode: 0
defaultPivotSpace: 1
defaultPivotAlignment: 0
customPivotPosition: {x: 0.5, y: 0.5}
spritePadding: 0
generateModelPrefab: 1
generateAnimationClips: 0
addSortingGroup: 1
addShadowCasters: 0
importFileNodeState: 1
platformSettingsDirtyTick: 0
textureAssetName:
singleSpriteImportData:
- name:
originalName:
pivot: {x: 0, y: 0}
alignment: 0
border: {x: 0, y: 0, z: 0, w: 0}
rect:
serializedVersion: 2
x: 0
y: 0
width: 0
height: 0
spriteID:
spriteBone: []
spriteOutline: []
vertices: []
spritePhysicsOutline: []
indices:
edges: []
tessellationDetail: 0
uvTransform: {x: 0, y: 0}
animatedSpriteImportData:
- name: "\xE9\xA3\x8E\xE6\x89\x87"
originalName:
pivot: {x: 0.5, y: 0.5}
alignment: 9
border: {x: 0, y: 0, z: 0, w: 0}
rect:
serializedVersion: 2
x: 410
y: 4
width: 97
height: 97
spriteID: 8ddf4a2a11e88d64a9cbedc60d11558c
spriteBone: []
spriteOutline: []
vertices: []
spritePhysicsOutline: []
indices:
edges: []
tessellationDetail: 0
uvTransform: {x: 410, y: 4}
- name: "\xE6\xB8\xA9\xE6\x8E\xA7\xE5\x99\xA8"
originalName:
pivot: {x: 0.5, y: 0.5}
alignment: 0
border: {x: 0, y: 0, z: 0, w: 0}
rect:
serializedVersion: 2
x: 4
y: 4
width: 398
height: 108
spriteID: a0ec52eb34ee22f4da8063235bf41d43
spriteBone: []
spriteOutline: []
vertices: []
spritePhysicsOutline: []
indices:
edges: []
tessellationDetail: 0
uvTransform: {x: 4, y: 4}
- name: "\xE6\x8C\x89\xE4\xB8\x8B"
originalName:
pivot: {x: 0.5, y: 0.5}
alignment: 0
border: {x: 0, y: 0, z: 0, w: 0}
rect:
serializedVersion: 2
x: 172
y: 120
width: 23
height: 30
spriteID: c47e232455f921b4aa3e0d565f64bf18
spriteBone: []
spriteOutline: []
vertices: []
spritePhysicsOutline: []
indices:
edges: []
tessellationDetail: 0
uvTransform: {x: 172, y: 120}
- name: "\xE6\x8C\x89\xE9\x92\xAE"
originalName:
pivot: {x: 0.5, y: 0.5}
alignment: 0
border: {x: 0, y: 0, z: 0, w: 0}
rect:
serializedVersion: 2
x: 100
y: 120
width: 28
height: 31
spriteID: 823b2ceb506f8664988f2e4175df1338
spriteBone: []
spriteOutline: []
vertices: []
spritePhysicsOutline: []
indices:
edges: []
tessellationDetail: 0
uvTransform: {x: 100, y: 120}
- name: disable
originalName:
pivot: {x: 0.5, y: 0.5}
alignment: 0
border: {x: 0, y: 0, z: 0, w: 0}
rect:
serializedVersion: 2
x: 136
y: 120
width: 28
height: 31
spriteID: 3fd2126cf0673724c9dccbda31a858cb
spriteBone: []
spriteOutline: []
vertices: []
spritePhysicsOutline: []
indices:
edges: []
tessellationDetail: 0
uvTransform: {x: 136, y: 120}
- name: "\xE6\xB8\xA9\xE5\xBA\xA6\xE6\xA3\x80\xE6\xB5\x8B\xE4\xBB\xAA"
originalName:
pivot: {x: 0.5, y: 0.5}
alignment: 0
border: {x: 0, y: 0, z: 0, w: 0}
rect:
serializedVersion: 2
x: 4
y: 120
width: 88
height: 81
spriteID: 37bb2f271cdbd2547b2fb9f9fafe4d9a
spriteBone: []
spriteOutline: []
vertices: []
spritePhysicsOutline: []
indices:
edges: []
tessellationDetail: 0
uvTransform: {x: 4, y: 120}
spriteSheetImportData: []
asepriteLayers:
- layerIndex: 0
guid: -660926954
name: "\xE9\xA3\x8E\xE6\x89\x87"
layerFlags: 3
layerType: 0
blendMode: 0
cells:
- name: "\xE9\xA3\x8E\xE6\x89\x87"
frameIndex: 0
cellRect:
x: 8
y: 5
width: 97
height: 97
spriteId: 8ddf4a2a11e88d64a9cbedc60d11558c
linkedCells: []
parentIndex: -1
- layerIndex: 1
guid: -1395797509
name: "\xE6\xB8\xA9\xE6\x8E\xA7\xE5\x99\xA8"
layerFlags: 3
layerType: 0
blendMode: 0
cells:
- name: "\xE6\xB8\xA9\xE6\x8E\xA7\xE5\x99\xA8"
frameIndex: 0
cellRect:
x: 0
y: 0
width: 398
height: 108
spriteId: a0ec52eb34ee22f4da8063235bf41d43
linkedCells: []
parentIndex: -1
- layerIndex: 2
guid: -1354117582
name: "\xE6\x8C\x89\xE4\xB8\x8B"
layerFlags: 3
layerType: 0
blendMode: 0
cells:
- name: "\xE6\x8C\x89\xE4\xB8\x8B"
frameIndex: 0
cellRect:
x: 115
y: 66
width: 23
height: 30
spriteId: c47e232455f921b4aa3e0d565f64bf18
linkedCells: []
parentIndex: -1
- layerIndex: 3
guid: 1699965797
name: "\xE6\x8C\x89\xE9\x92\xAE"
layerFlags: 3
layerType: 0
blendMode: 0
cells:
- name: "\xE6\x8C\x89\xE9\x92\xAE"
frameIndex: 0
cellRect:
x: 114
y: 65
width: 28
height: 31
spriteId: 823b2ceb506f8664988f2e4175df1338
linkedCells: []
parentIndex: -1
- layerIndex: 4
guid: 1639194518
name: disable
layerFlags: 3
layerType: 0
blendMode: 0
cells:
- name: disable
frameIndex: 0
cellRect:
x: 114
y: 65
width: 28
height: 31
spriteId: 3fd2126cf0673724c9dccbda31a858cb
linkedCells: []
parentIndex: -1
- layerIndex: 5
guid: -1363906703
name: "\xE6\xB8\xA9\xE5\xBA\xA6\xE6\xA3\x80\xE6\xB5\x8B\xE4\xBB\xAA"
layerFlags: 3
layerType: 0
blendMode: 0
cells:
- name: "\xE6\xB8\xA9\xE5\xBA\xA6\xE6\xA3\x80\xE6\xB5\x8B\xE4\xBB\xAA"
frameIndex: 0
cellRect:
x: 8
y: 13
width: 88
height: 81
spriteId: 37bb2f271cdbd2547b2fb9f9fafe4d9a
linkedCells: []
parentIndex: -1
platformSettings: []
secondarySpriteTextures: []
spritePackingTag:
canvasSize: {x: 399, y: 108}
@@ -0,0 +1,210 @@
fileFormatVersion: 2
guid: 39e49ee62533df1499223125855b3c34
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 21.5
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 512
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: iPhone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: "\u5DE6\u4FA7\u8FB9\u6846"
rect:
serializedVersion: 2
x: 0
y: 0
width: 31
height: 295
alignment: 9
pivot: {x: 0.5, y: 0.36610168}
border: {x: 0, y: 0, z: 0, w: 0}
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: e003d853b0ca86845aafa30181ed0d17
internalID: -359285356
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: "\u7EF4\u4FEE\u754C\u9762"
rect:
serializedVersion: 2
x: 216
y: 0
width: 168
height: 294
alignment: 9
pivot: {x: 0.5, y: 0.36734694}
border: {x: 0, y: 0, z: 0, w: 0}
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: c2e027bcd8e709d4e96c9e2fa1c0e6a5
internalID: 1938320413
vertices: []
indices:
edges: []
weights: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable:
"\u5DE6\u4FA7\u8FB9\u6846": -359285356
"\u7EF4\u4FEE\u754C\u9762": 1938320413
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 0c16b245ccd6c3d44b482e15a01d767b
guid: eddff71c4284b4f45af423d581f4a7ab
folderAsset: yes
DefaultImporter:
externalObjects: {}
@@ -0,0 +1,526 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1308916982058092465
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2192445671151396498}
- component: {fileID: 654060546389872390}
m_Layer: 0
m_Name: "\u6E29\u63A7\u5668"
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &2192445671151396498
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1308916982058092465}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: -0.016666412, y: 0.00000011920929, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7192873335209979720}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &654060546389872390
SpriteRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1308916982058092465}
m_Enabled: 1
m_CastShadows: 0
m_ReceiveShadows: 0
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 0
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 0
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 1134468525
m_SortingLayer: 10
m_SortingOrder: 25
m_Sprite: {fileID: 871935120432241074, guid: 5f045a8dca074cc4f9cebb2bea1c020b, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_FlipX: 0
m_FlipY: 0
m_DrawMode: 0
m_Size: {x: 13.266666, y: 3.6}
m_AdaptiveModeThreshold: 0.5
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_MaskInteraction: 0
m_SpriteSortPoint: 0
--- !u!1 &3582481867631340271
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2641859170065213768}
- component: {fileID: 1089920536786880813}
- component: {fileID: 8570869920292863351}
m_Layer: 0
m_Name: HeatMapcontent
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
--- !u!224 &2641859170065213768
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3582481867631340271}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7192873335209979720}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -4.85, y: -0.030089617}
m_SizeDelta: {x: 3, y: 3}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &1089920536786880813
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3582481867631340271}
m_CullTransparentMesh: 1
--- !u!114 &8570869920292863351
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3582481867631340271}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 2100000, guid: 72b0010e1307b614ebb427b0d8dc54ee, type: 2}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -8590783372244099527, guid: 5f045a8dca074cc4f9cebb2bea1c020b, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &4174295440086032515
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7192873335209979720}
m_Layer: 0
m_Name: "\u6E29\u63A7\u5668"
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &7192873335209979720
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4174295440086032515}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 4.74, y: -0.08, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2192445671151396498}
- {fileID: 4251788116866013210}
- {fileID: 7021022466105150364}
- {fileID: 508172504910473353}
- {fileID: 2641859170065213768}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &5048870714832433823
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 508172504910473353}
- component: {fileID: 2925331204650984445}
- component: {fileID: 3158918622848701779}
- component: {fileID: 6583180485309050584}
m_Layer: 0
m_Name: "\u6E29\u5EA6\u68C0\u6D4B\u4EEAMask"
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &508172504910473353
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5048870714832433823}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7192873335209979720}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -4.859234, y: -0.006214857}
m_SizeDelta: {x: 3.055, y: 3.06}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!212 &2925331204650984445
SpriteRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5048870714832433823}
m_Enabled: 1
m_CastShadows: 0
m_ReceiveShadows: 0
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 0
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 0}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 0
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 1134468525
m_SortingLayer: 10
m_SortingOrder: 30
m_Sprite: {fileID: -8590783372244099527, guid: 5f045a8dca074cc4f9cebb2bea1c020b, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 0}
m_FlipX: 0
m_FlipY: 0
m_DrawMode: 0
m_Size: {x: 3.055, y: 3.06}
m_AdaptiveModeThreshold: 0.5
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_MaskInteraction: 0
m_SpriteSortPoint: 0
--- !u!114 &3158918622848701779
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5048870714832433823}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: ee158225ee1e59f4791627785501d950, type: 3}
m_Name:
m_EditorClassIdentifier:
shaderTypes: 0
normalStrength: 5
normalSmoothing: 1
computingNormal: 0
--- !u!331 &6583180485309050584
SpriteMask:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5048870714832433823}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 0
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 15d0c3709176029428a0da2f8cecf0b5, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_Sprite: {fileID: -8590783372244099527, guid: 5f045a8dca074cc4f9cebb2bea1c020b, type: 3}
m_MaskAlphaCutoff: 0.2
m_FrontSortingLayerID: 0
m_BackSortingLayerID: 0
m_FrontSortingLayer: 0
m_BackSortingLayer: 0
m_FrontSortingOrder: 0
m_BackSortingOrder: 0
m_IsCustomRangeActive: 0
m_SpriteSortPoint: 0
--- !u!1 &8519981987992342745
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4251788116866013210}
- component: {fileID: 8130709809439295461}
m_Layer: 0
m_Name: "\u98CE\u6247"
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &4251788116866013210
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8519981987992342745}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: -4.8, y: -0.016666532, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7192873335209979720}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &8130709809439295461
SpriteRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8519981987992342745}
m_Enabled: 1
m_CastShadows: 0
m_ReceiveShadows: 0
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 0
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 0
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 1134468525
m_SortingLayer: 10
m_SortingOrder: 20
m_Sprite: {fileID: 5693017419858312192, guid: 5f045a8dca074cc4f9cebb2bea1c020b, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_FlipX: 0
m_FlipY: 0
m_DrawMode: 0
m_Size: {x: 3.2333333, y: 3.2333333}
m_AdaptiveModeThreshold: 0.5
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_MaskInteraction: 0
m_SpriteSortPoint: 0
--- !u!1 &8921247343283679770
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7021022466105150364}
- component: {fileID: 7666792333862979253}
m_Layer: 0
m_Name: "\u6E29\u5EA6\u68C0\u6D4B\u4EEA"
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &7021022466105150364
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8921247343283679770}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7192873335209979720}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -4.84, y: -0.02}
m_SizeDelta: {x: 3.055, y: 3.06}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!212 &7666792333862979253
SpriteRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8921247343283679770}
m_Enabled: 1
m_CastShadows: 0
m_ReceiveShadows: 0
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 0
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 72b0010e1307b614ebb427b0d8dc54ee, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 0
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 1134468525
m_SortingLayer: 10
m_SortingOrder: 30
m_Sprite: {fileID: -8590783372244099527, guid: 5f045a8dca074cc4f9cebb2bea1c020b, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_FlipX: 0
m_FlipY: 0
m_DrawMode: 0
m_Size: {x: 3.055, y: 3.06}
m_AdaptiveModeThreshold: 0.5
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_MaskInteraction: 1
m_SpriteSortPoint: 0
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d17b2840171d44a4f964a98554eb55a4
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

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