Compare commits
319
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fc1ebdcb5 | ||
|
|
b20b936966 | ||
|
|
4a0e3ba0b9 | ||
|
|
2a1cfbc208 | ||
|
|
e3762e5462 | ||
|
|
5029a4b9b5 | ||
|
|
3debdb39e4 | ||
|
|
04edf9b355 | ||
|
|
6854679bcb | ||
|
|
515f57e28c | ||
|
|
97ef0972fd | ||
|
|
2fb32c44e8 | ||
|
|
3d65941b03 | ||
|
|
5091851ead | ||
|
|
4411bfa4b6 | ||
|
|
02d3fc04e6 | ||
|
|
ef3c14d3de | ||
|
|
b7fdcf97fe | ||
|
|
ba0ce38c11 | ||
|
|
3b5d05260e | ||
|
|
f4488a68d0 | ||
|
|
c6c76666ef | ||
|
|
d655887a02 | ||
|
|
adbcd89918 | ||
|
|
9b2caf64c9 | ||
|
|
82b20a8fab | ||
|
|
0f06041fa2 | ||
|
|
8a5d83d888 | ||
|
|
71a803fd72 | ||
|
|
672d554b40 | ||
|
|
4259306e1f | ||
|
|
8b16aebc6b | ||
|
|
52db661484 | ||
|
|
aed9998d1a | ||
|
|
3919ea9a75 | ||
|
|
3891b6bd37 | ||
|
|
9f61f53b7f | ||
|
|
6dc0983961 | ||
|
|
9627586130 | ||
|
|
28cc1127f5 | ||
|
|
532d9fb320 | ||
|
|
0d59e7d91e | ||
|
|
859d780b78 | ||
|
|
7ee6cba2ae | ||
|
|
6159704116 | ||
|
|
74f5989802 | ||
|
|
0b848d8040 | ||
|
|
96b309992c | ||
|
|
1547055cc2 | ||
|
|
3d119dcd92 | ||
|
|
c4ef30b151 | ||
|
|
12c5735466 | ||
|
|
35451363a5 | ||
|
|
9ef6bc9bff | ||
|
|
97f0fabd3c | ||
|
|
018e9d3b52 | ||
|
|
c0e58d56b4 | ||
|
|
cffb99a52c | ||
|
|
597e97c5d7 | ||
|
|
7a8f7dbbd7 | ||
|
|
8fee89db36 | ||
|
|
046b1a9efb | ||
|
|
c7c0e96110 | ||
|
|
f6f87c3109 | ||
|
|
98dfb644c5 | ||
|
|
845f46b924 | ||
|
|
5f31cd5635 | ||
|
|
1e8ec2e64f | ||
|
|
e7a0089a46 | ||
|
|
2ac0b4695d | ||
|
|
ecfefa5b87 | ||
|
|
3164fd7d8c | ||
|
|
dc0488f085 | ||
|
|
164e3dd50e | ||
|
|
c985cc2843 | ||
|
|
b0ee02c62c | ||
|
|
a043f27035 | ||
|
|
a9f58fc683 | ||
|
|
94823354b5 | ||
|
|
4ec5e0b442 | ||
|
|
4d44ca387e | ||
|
|
1171ad9e87 | ||
|
|
1318636bee | ||
|
|
f17a8ab8dd | ||
|
|
b70f2614af | ||
|
|
517f4d956a | ||
|
|
46c47ea4be | ||
|
|
41d3e07a55 | ||
|
|
2cd0ae92be | ||
|
|
3491367bd9 | ||
|
|
dc6fe93afb | ||
|
|
ff89ffe552 | ||
|
|
bd30cdabd0 | ||
|
|
6ff260150d | ||
|
|
32d2fcc0a2 | ||
|
|
cca5404f0c | ||
|
|
6e5e5d5790 | ||
|
|
ad759a6e83 | ||
|
|
65205e2552 | ||
|
|
cb255f8ca5 | ||
|
|
5f30f356ec | ||
|
|
3c789cbd65 | ||
|
|
fabef0145d | ||
|
|
785add905d | ||
|
|
2fe558846b | ||
|
|
1fa96151d6 | ||
|
|
c8830d9346 | ||
|
|
ecf75ae6a6 | ||
|
|
5f2f6e6365 | ||
|
|
153d36f884 | ||
|
|
6481858017 | ||
|
|
c9ca07a0b3 | ||
|
|
cb1f5b90a7 | ||
|
|
9f4c473fac | ||
|
|
4eb72911e1 | ||
|
|
5f4f9c1721 | ||
|
|
202cdb4c29 | ||
|
|
1ade595f9d | ||
|
|
3ed84f79b1 | ||
|
|
1444e43449 | ||
|
|
6e7bb3f637 | ||
|
|
5ab204b476 | ||
|
|
84192cb7c4 | ||
|
|
c98644c723 | ||
|
|
15aba8e805 | ||
|
|
90c75d1f45 | ||
|
|
f8821b1b3e | ||
|
|
41ca1927a0 | ||
|
|
6bf967e8b3 | ||
|
|
2514eb0a8d | ||
|
|
f444978717 | ||
|
|
554de790c1 | ||
|
|
1764714cb5 | ||
|
|
a6d541383b | ||
|
|
00b5646b44 | ||
|
|
9403bd9623 | ||
|
|
14fd64e7bd | ||
|
|
5a37f529ab | ||
|
|
06d6127c69 | ||
|
|
53587c413e | ||
|
|
5408fd5320 | ||
|
|
1f6208a094 | ||
|
|
772a6bbbe8 | ||
|
|
bfc61a0097 | ||
|
|
bfbfe494c4 | ||
|
|
297075e0f0 | ||
|
|
71d248c2e6 | ||
|
|
7034b82aa4 | ||
|
|
eb127371f1 | ||
|
|
cec2dae5e0 | ||
|
|
ba19c2eff5 | ||
|
|
7fa12e3d46 | ||
|
|
bb6b3dd599 | ||
|
|
d7030f36d2 | ||
|
|
94a817f5fa | ||
|
|
bfdcb41116 | ||
|
|
6ace48dba2 | ||
|
|
2999f753f7 | ||
|
|
c3da27d07c | ||
|
|
05fe1ebb60 | ||
|
|
ad5111a206 | ||
|
|
592d3a36dd | ||
|
|
3e2a24421f | ||
|
|
bc85e4d343 | ||
|
|
551dd6d183 | ||
|
|
c9f508bb8e | ||
|
|
ba5a9fc2f3 | ||
|
|
d5c4917a8a | ||
|
|
c751a859ff | ||
|
|
081ed0d883 | ||
|
|
a5e417eeae | ||
|
|
2eea496c4b | ||
|
|
d316922b3a | ||
|
|
600fd4cebf | ||
|
|
ce00539c18 | ||
|
|
78d5ad33f0 | ||
|
|
2fec120568 | ||
|
|
4b595fcee9 | ||
|
|
947cfa2fb4 | ||
|
|
0dc1001723 | ||
|
|
7476263411 | ||
|
|
91c3f509e1 | ||
|
|
686c32738d | ||
|
|
b40f17ea9f | ||
|
|
4b86068bbf | ||
|
|
1077bc551c | ||
|
|
a4620dacc8 | ||
|
|
2c8bc17174 | ||
|
|
24f475fe2f | ||
|
|
5236066c7b | ||
|
|
447d61f941 | ||
|
|
7646cb4db3 | ||
|
|
7c04f507db | ||
|
|
5549404fe4 | ||
|
|
667425bd2c | ||
|
|
21d51945c3 | ||
|
|
2dcfeec938 | ||
|
|
94130cd97f | ||
|
|
c617e44813 | ||
|
|
b7ca9ab11f | ||
|
|
847feed926 | ||
|
|
e20b499492 | ||
|
|
5d07012cb6 | ||
|
|
00a43fcad3 | ||
|
|
e363c6e4a5 | ||
|
|
1a113dbd25 | ||
|
|
3959561686 | ||
|
|
031ac79b8d | ||
|
|
0db3f75476 | ||
|
|
f3e1e421a3 | ||
|
|
23b6d9cf87 | ||
|
|
509e0ae169 | ||
|
|
c3c0398905 | ||
|
|
fd79ff9d93 | ||
|
|
8b1fe28edb | ||
|
|
b4f0cebc1d | ||
|
|
46602803dc | ||
|
|
cb1ddc1f93 | ||
|
|
6c75ceaa35 | ||
|
|
cfb4ff96c4 | ||
|
|
7dfd87e868 | ||
|
|
8deff54e48 | ||
|
|
7b287a6248 | ||
|
|
9b6d008263 | ||
|
|
02d3d80f46 | ||
|
|
3eea7e9ca6 | ||
|
|
56045b4bcd | ||
|
|
f51a670239 | ||
|
|
4100232162 | ||
|
|
51fb062225 | ||
|
|
e0318026d7 | ||
|
|
cc354cb37d | ||
|
|
eaba7ea5ab | ||
|
|
5ab8498be5 | ||
|
|
fa79a4bed1 | ||
|
|
f0a92553fb | ||
|
|
85de341cd2 | ||
|
|
ea95111f2a | ||
|
|
0039beb5f9 | ||
|
|
96164e1887 | ||
|
|
754d03b47d | ||
|
|
3be0bf8201 | ||
|
|
60200afe9b | ||
|
|
69315f86f9 | ||
|
|
8c795f4474 | ||
|
|
8c37a0bd09 | ||
|
|
e9e21920f1 | ||
|
|
5c6cc5c4a6 | ||
|
|
61e3f52a38 | ||
|
|
5639bad84a | ||
|
|
3d86790bd7 | ||
|
|
fd77b5c248 | ||
|
|
fa66dbc7c9 | ||
|
|
92087b81ef | ||
|
|
1f931e987f | ||
|
|
7ace1f255a | ||
|
|
21321074bf | ||
|
|
89850c75f3 | ||
|
|
d075d1e0f8 | ||
|
|
161fe6ac7d | ||
|
|
c38c2f75be | ||
|
|
52573095f7 | ||
|
|
ac6ded9f66 | ||
|
|
8a26091590 | ||
|
|
06d85d6460 | ||
|
|
91ffbeb130 | ||
|
|
b14599d18c | ||
|
|
53a4fc86c4 | ||
|
|
079d57e295 | ||
|
|
5f8ffb7f78 | ||
|
|
85a3fc9e14 | ||
|
|
ddb3aba4e5 | ||
|
|
1ee2a949e9 | ||
|
|
dd66a52d65 | ||
|
|
59bffeab06 | ||
|
|
423a3954dd | ||
|
|
4ab2f83395 | ||
|
|
de85ca2944 | ||
|
|
9f6c926dcc | ||
|
|
d101864397 | ||
|
|
1cc5ac32aa | ||
|
|
431401c52f | ||
|
|
0ed1253a40 | ||
|
|
4f59e41b03 | ||
|
|
df6f8c6ec4 | ||
|
|
d00edd7bb8 | ||
|
|
1be7b12deb | ||
|
|
51b8d86603 | ||
|
|
c4dbdc098b | ||
|
|
19b6b9c4e2 | ||
|
|
92e8cd2f13 | ||
|
|
26cfa5d886 | ||
|
|
b98214632e | ||
|
|
2c74ec2030 | ||
|
|
71bfad4dae | ||
|
|
c601dda9de | ||
|
|
1422161844 | ||
|
|
a494cf99cf | ||
|
|
ee1d100fb5 | ||
|
|
d0a08986c6 | ||
|
|
0d04d1a68d | ||
|
|
52e0e9002f | ||
|
|
4b3260dfcd | ||
|
|
6689effb87 | ||
|
|
b062ab4ddc | ||
|
|
4104de4166 | ||
|
|
425cd1e695 | ||
|
|
a48a6bf6ad | ||
|
|
98c558e800 | ||
|
|
cd00a524bb | ||
|
|
6d12bceb36 | ||
|
|
52d7656c30 | ||
|
|
2a8d82fd2e | ||
|
|
558706ee08 | ||
|
|
65d895798f | ||
|
|
a305e5ef42 | ||
|
|
0babb520de | ||
|
|
0265de117a | ||
|
|
11d88c146c |
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: narrative-timeline-flow
|
||||
description: >-
|
||||
叙事 Timeline:aseprite/静图改 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:选主演出 clip;Loop 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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
@@ -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
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f6d62e6a25088645b040ee8261cdbc6
|
||||
guid: 1547f70e189f2f94687f6d6cd7be9968
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
+1
-1
@@ -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:
|
||||
Binary file not shown.
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
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0b8fb23239461942ae20b421af10a7c
|
||||
guid: 434ffc1efd0f27340b4b6701baefdceb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
Binary file not shown.
@@ -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}
|
||||
Binary file not shown.
@@ -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
-1
@@ -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
Reference in New Issue
Block a user