Compare commits
791
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
592d3a36dd | ||
|
|
bc85e4d343 | ||
|
|
551dd6d183 | ||
|
|
c9f508bb8e | ||
|
|
ba5a9fc2f3 | ||
|
|
d5c4917a8a | ||
|
|
c751a859ff | ||
|
|
081ed0d883 | ||
|
|
a5e417eeae | ||
|
|
2eea496c4b | ||
|
|
d316922b3a | ||
|
|
600fd4cebf | ||
|
|
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 | ||
|
|
bd3988424f | ||
|
|
49df75279d | ||
|
|
2a43d0af81 | ||
|
|
66672f5188 | ||
|
|
0590e755ee | ||
|
|
467dbfdf84 | ||
|
|
bc14705fb3 | ||
|
|
6e939d2a72 | ||
|
|
2792d8fafa | ||
|
|
024eecb404 | ||
|
|
8d1f072d55 | ||
|
|
18d6d970f9 | ||
|
|
a1bfa4c8b8 | ||
|
|
ea6b75d73d | ||
|
|
4eb9aacc47 | ||
|
|
892912e707 | ||
|
|
3fefa18c65 | ||
|
|
233cedd528 | ||
|
|
b0ece3e0f1 | ||
|
|
8a7b2d0ed2 | ||
|
|
c905eb4625 | ||
|
|
4a66ba766e | ||
|
|
4ded2b8eab | ||
|
|
f6997e8816 | ||
|
|
6a5e8eecc9 | ||
|
|
d0f4a1cf09 | ||
|
|
e1f5484e02 | ||
|
|
2e7c87c886 | ||
|
|
f273569e7c | ||
|
|
68837581af | ||
|
|
d8056155c1 | ||
|
|
2a8d82fd2e | ||
|
|
558706ee08 | ||
|
|
65d895798f | ||
|
|
a305e5ef42 | ||
|
|
0babb520de | ||
|
|
0265de117a | ||
|
|
11d88c146c | ||
|
|
b5aab4edc1 | ||
|
|
3092c3554b | ||
|
|
7d16bfe7bc | ||
|
|
d9c8e85f2b | ||
|
|
b612a0763d | ||
|
|
c7c1571feb | ||
|
|
6469a6b746 | ||
|
|
1fbac2c182 | ||
|
|
ff29142761 | ||
|
|
88dd2c95e8 | ||
|
|
f87dcc687f | ||
|
|
84764d7b6b | ||
|
|
204b2c43fb | ||
|
|
c5c8729704 | ||
|
|
c4ed431753 | ||
|
|
2ff1df2331 | ||
|
|
36a1d783fb | ||
|
|
9a31c71900 | ||
|
|
5033923042 | ||
|
|
5033bf0a3d | ||
|
|
c75cdc4bdf | ||
|
|
c6539c37b9 | ||
|
|
e015b756f6 | ||
|
|
04536c4b81 | ||
|
|
7d7c8893fc | ||
|
|
d3c6880ce6 | ||
|
|
82603c304d | ||
|
|
b89a674a28 | ||
|
|
d5c946d322 | ||
|
|
575ebebfaf | ||
|
|
aea6483849 | ||
|
|
1b33124ec1 | ||
|
|
76302f53c8 | ||
|
|
969bc2756c | ||
|
|
599bb44763 | ||
|
|
78ac7da055 | ||
|
|
e1240f023d | ||
|
|
5933ae954d | ||
|
|
e5a10ea0b7 | ||
|
|
d782a09671 | ||
|
|
440979264e | ||
|
|
39a9eb73bc | ||
|
|
cb4264f26e | ||
|
|
c67fe1b32d | ||
|
|
b8773d968e | ||
|
|
4776066cec | ||
|
|
5305033016 | ||
|
|
bd70442e45 | ||
|
|
a3dadd4d88 | ||
|
|
39ce23d5f3 | ||
|
|
aa0f431955 | ||
|
|
b98681acab | ||
|
|
37e9127260 | ||
|
|
c5c90eb5a3 | ||
|
|
517a03bbc3 | ||
|
|
cb6658e41a | ||
|
|
e78d5eba23 | ||
|
|
478f431dd3 | ||
|
|
a1d972c074 | ||
|
|
0cf659cc20 | ||
|
|
a5352ed250 | ||
|
|
18ab68f841 | ||
|
|
870f914950 | ||
|
|
c08021a2c8 | ||
|
|
6ac7fcf9da | ||
|
|
dfed55acbb | ||
|
|
982914f6e7 | ||
|
|
1583b96200 | ||
|
|
3432053239 | ||
|
|
d054a57e26 | ||
|
|
7884812131 | ||
|
|
33be87215e | ||
|
|
f3c3994f5c | ||
|
|
bd94302714 | ||
|
|
5031906d70 | ||
|
|
bef36bb92f | ||
|
|
496ba885af | ||
|
|
3c7662679c | ||
|
|
0832e4cac1 | ||
|
|
668b735377 | ||
|
|
b26f7d0222 | ||
|
|
80d32a73d8 | ||
|
|
fa3954bbbb | ||
|
|
b9167056cf | ||
|
|
784e1e8f0d | ||
|
|
76d620d8c3 | ||
|
|
1918c8c53b | ||
|
|
07f1e3f5bc | ||
|
|
fe8b4ea3c5 | ||
|
|
a1767fc6d3 | ||
|
|
49a51b6ccb | ||
|
|
08d09bf31d | ||
|
|
4e6c6dac6e | ||
|
|
a603ad928f | ||
|
|
bde1040c9d | ||
|
|
fcd0955ae7 | ||
|
|
b1981431f6 | ||
|
|
7c15663bf2 | ||
|
|
9f5ea54813 | ||
|
|
63d755ee05 | ||
|
|
29267659b9 | ||
|
|
c912ac89c5 | ||
|
|
b2174985ed | ||
|
|
5e64ab4c30 | ||
|
|
cb557c14cf | ||
|
|
3e2d1e931a | ||
|
|
742125c112 | ||
|
|
a8fcb42f85 | ||
|
|
7ddada9726 | ||
|
|
b89fb35559 | ||
|
|
f726472d8a | ||
|
|
2ef0cff2f1 | ||
|
|
58068d1a28 | ||
|
|
2f1bd5707e | ||
|
|
af3c39dcb3 | ||
|
|
d6e7afe4b3 | ||
|
|
73e6b86579 | ||
|
|
df70089ecc | ||
|
|
79a4976d42 | ||
|
|
107a2411e3 | ||
|
|
41e02d48d8 | ||
|
|
3b96003c03 | ||
|
|
f4ee41c522 | ||
|
|
024202b19e | ||
|
|
4deb565882 | ||
|
|
e6dfab3b51 | ||
|
|
9124a4bd6b | ||
|
|
aaf248a0ef | ||
|
|
7177039f71 | ||
|
|
db73e13bcc | ||
|
|
8ddad4fcfc | ||
|
|
8f6616920c | ||
|
|
660f8e24ee | ||
|
|
929e6f0086 | ||
|
|
603dc9dad3 | ||
|
|
a05d5d476c | ||
|
|
da436479db | ||
|
|
93a5076369 | ||
|
|
e50e2f1cd5 | ||
|
|
cf016ecc93 | ||
|
|
39cf031761 | ||
|
|
6d6184e2de | ||
|
|
9867c84ccf | ||
|
|
e4d7b03a89 | ||
|
|
e5306f2b82 | ||
|
|
46cdbd3dd2 | ||
|
|
58488547ce | ||
|
|
6f8e306fb6 | ||
|
|
4fa3aac335 | ||
|
|
568e6ff867 | ||
|
|
eefce92f58 | ||
|
|
d33d4b9dc4 | ||
|
|
c761e9afd7 | ||
|
|
e884f4fb42 | ||
|
|
f35edeabe8 | ||
|
|
d001c824a0 | ||
|
|
d865244a02 | ||
|
|
a97b00072a | ||
|
|
06be1d6eef | ||
|
|
6dbd6917f9 | ||
|
|
57e20d0dce | ||
|
|
29a1c7d7cb | ||
|
|
e6b24556b5 | ||
|
|
04b441d977 | ||
|
|
488149e8d4 | ||
|
|
7a14cc52e9 | ||
|
|
cc069b1da3 | ||
|
|
2fcb998e6f | ||
|
|
140e05b4d1 | ||
|
|
07d330f6db | ||
|
|
4f6f5a1ae9 | ||
|
|
db73b64980 | ||
|
|
ece0d4ed8e | ||
|
|
268bdd19d9 | ||
|
|
40358dd8a1 | ||
|
|
f42ec20dc8 | ||
|
|
ca3d4cb3cb | ||
|
|
814b659c39 | ||
|
|
c392c81d31 | ||
|
|
681fe5fc98 | ||
|
|
721e82d768 | ||
|
|
296ce4d657 | ||
|
|
2389c9ef3e | ||
|
|
7e21d33cdd | ||
|
|
d2ecd1d846 | ||
|
|
f25674a043 | ||
|
|
d1eab5612d | ||
|
|
1e60b0005e | ||
|
|
6d262a8796 | ||
|
|
32d44b8119 | ||
|
|
7edda0bd6e | ||
|
|
2c9f609616 | ||
|
|
c0325b0a35 | ||
|
|
228f91e8ab | ||
|
|
a36dc7c003 | ||
|
|
e75bd5e552 | ||
|
|
05855e2a9f | ||
|
|
f188f4c1fc | ||
|
|
870bf96c0f | ||
|
|
67f3abd825 | ||
|
|
3775aed368 | ||
|
|
08b8f34bde | ||
|
|
e41756d0f0 | ||
|
|
2d43af4827 | ||
|
|
0f681b518e | ||
|
|
382b784605 | ||
|
|
5efb2ff157 | ||
|
|
43465e8f34 | ||
|
|
bf416f604b | ||
|
|
a5aa37ffcc | ||
|
|
009776eb32 | ||
|
|
b1fdec64e1 | ||
|
|
43c3f8531d | ||
|
|
5a0b7a047b | ||
|
|
e90da7bab7 | ||
|
|
b53a3f45b7 | ||
|
|
e4ec8fce57 | ||
|
|
d780ff59ee | ||
|
|
57b03aeb08 | ||
|
|
b7bfa28fd7 | ||
|
|
2a0c34fd7a | ||
|
|
3746f4aeb6 | ||
|
|
5cae227fec | ||
|
|
d88f794759 | ||
|
|
67968b0102 | ||
|
|
d2c7837a74 | ||
|
|
07850fcf1c | ||
|
|
0368374a98 | ||
|
|
749b9974c0 | ||
|
|
a434ffd7e0 | ||
|
|
a080f507d1 | ||
|
|
50566a1d42 | ||
|
|
67668f18e0 | ||
|
|
e2bdd8e0fa | ||
|
|
6a289e0255 | ||
|
|
1fe4c44f0c | ||
|
|
3cee30f099 | ||
|
|
4d8707cf4e | ||
|
|
fbb9e65338 | ||
|
|
0b0d22d009 | ||
|
|
0a6e892504 | ||
|
|
de671ff94c | ||
|
|
fc4b1c8805 | ||
|
|
29e698c370 | ||
|
|
b30010a459 | ||
|
|
181eeb679b | ||
|
|
a633b47d5b | ||
|
|
c55220d739 | ||
|
|
2796f96215 | ||
|
|
e01c8af449 | ||
|
|
748c875e93 | ||
|
|
afd062bda2 | ||
|
|
d6912fb1a6 | ||
|
|
6fcf340230 | ||
|
|
cc3e1dd138 | ||
|
|
789ca207bd | ||
|
|
4bb0215f3d | ||
|
|
c67d988365 | ||
|
|
13e51a86e6 | ||
|
|
2cd3a1a7df | ||
|
|
2c1e5d8fa7 | ||
|
|
1065e2b8b9 | ||
|
|
8e5346da46 | ||
|
|
2c89feec9d | ||
|
|
a33d6a9fe1 | ||
|
|
33a028f071 | ||
|
|
c845af68b9 | ||
|
|
874abf66e0 | ||
|
|
f759c8cfde | ||
|
|
570961251c | ||
|
|
0794c5751d | ||
|
|
038c624163 | ||
|
|
e72d14ed8d | ||
|
|
5c56551d79 | ||
|
|
db5fbb0cb8 | ||
|
|
7e579a2eb0 | ||
|
|
03d96c8694 | ||
|
|
cf7b0fc67d | ||
|
|
342a854a33 | ||
|
|
6ea5a6cb0e | ||
|
|
70b69c2302 | ||
|
|
fb00d3ad8e | ||
|
|
b7712f7df8 | ||
|
|
c84b56df6f | ||
|
|
35e61146b6 | ||
|
|
09372b482b | ||
|
|
ff7cb41c47 | ||
|
|
6679cc1fbd | ||
|
|
dcf5bf6e09 | ||
|
|
4ef236274d | ||
|
|
deb3d5182a | ||
|
|
e23185b11d | ||
|
|
dabf9fc219 | ||
|
|
aa926afa29 | ||
|
|
bb043aed2b | ||
|
|
64e74cbd2d | ||
|
|
17862b7bab | ||
|
|
9faff272cf | ||
|
|
be1f288e1c | ||
|
|
1d629e78f7 | ||
|
|
e471e5f285 | ||
|
|
2a83fc5afe | ||
|
|
a5fc9d8ba4 | ||
|
|
a03fab65e3 | ||
|
|
44f001361e | ||
|
|
f224e8a2ac | ||
|
|
a9e14387cd | ||
|
|
166ec647b4 | ||
|
|
3bfb77f15e | ||
|
|
26ffa8d016 | ||
|
|
f12368abfb | ||
|
|
71e2f915b9 | ||
|
|
5e3087f4e9 | ||
|
|
c6caae6078 | ||
|
|
030124a0c7 | ||
|
|
335b8f1bbc | ||
|
|
0dcb02f725 | ||
|
|
3656b35525 | ||
|
|
e7b1fc5ec1 | ||
|
|
9264a17f87 | ||
|
|
49fcb7240b | ||
|
|
5d7c1037cf | ||
|
|
32fb29f18a | ||
|
|
47edc1296c | ||
|
|
3338606660 | ||
|
|
ebe18fd8e1 | ||
|
|
7d7bef7c3b | ||
|
|
94d89330dc | ||
|
|
93012f5f7c | ||
|
|
b32992bc26 | ||
|
|
439984ca93 | ||
|
|
fbac02f641 | ||
|
|
5bc48400c5 | ||
|
|
03682d0ec1 | ||
|
|
1db919e472 | ||
|
|
c5e3210793 | ||
|
|
a027ef2577 | ||
|
|
00f3cf3f59 | ||
|
|
39b9ed487c | ||
|
|
5be131e020 | ||
|
|
b9c15216cb | ||
|
|
dd9420dbc7 | ||
|
|
bec825ce89 | ||
|
|
461fd18191 | ||
|
|
15651facdc | ||
|
|
72c0dfb746 | ||
|
|
999095b27e | ||
|
|
da3fed3c07 | ||
|
|
bbc51283dc | ||
|
|
42d1f431b6 | ||
|
|
2887f51ef8 | ||
|
|
f48e733663 | ||
|
|
027e545646 | ||
|
|
53b14b7a04 | ||
|
|
70e820195e | ||
|
|
92f4d463b3 | ||
|
|
98fa5e5474 | ||
|
|
b447d4784c | ||
|
|
cbae4cdb22 | ||
|
|
55452b4376 | ||
|
|
7e422fa457 | ||
|
|
5e80772cbf | ||
|
|
b8f45e1466 | ||
|
|
ebe0dd7c59 | ||
|
|
f1e7e020f1 | ||
|
|
922186cf42 | ||
|
|
b635d90420 | ||
|
|
f68321e8a8 | ||
|
|
4675792617 | ||
|
|
19aea94d95 | ||
|
|
bf1b23b844 | ||
|
|
ea2164acbf | ||
|
|
fe65071cf7 | ||
|
|
054f3822c6 | ||
|
|
a2d9427299 | ||
|
|
e3913f425b | ||
|
|
140b8584bc | ||
|
|
0137d54271 | ||
|
|
487ee0a991 | ||
|
|
0793006e99 | ||
|
|
8ab7441321 | ||
|
|
76cfecc0a4 | ||
|
|
d96bc0bf07 | ||
|
|
6e3143121e | ||
|
|
5cbc46ce73 | ||
|
|
7ad048bef8 | ||
|
|
420f6152a3 | ||
|
|
7d7b417e35 | ||
|
|
277029ff51 | ||
|
|
594578c697 | ||
|
|
4d319d3b04 | ||
|
|
ceccff5461 | ||
|
|
e07e3b05d2 | ||
|
|
dbd2fed24a | ||
|
|
ddb1a97236 | ||
|
|
57ad1307ff | ||
|
|
d25c622ecc | ||
|
|
db29f8f1dd | ||
|
|
b976be9454 | ||
|
|
613e48f799 | ||
|
|
57c90d9d4f | ||
|
|
7cabc6418c | ||
|
|
e77bea6bdc | ||
|
|
fcf5b94472 | ||
|
|
442e419f80 | ||
|
|
83acdeaecc | ||
|
|
d324ec3aa1 | ||
|
|
e948d08573 | ||
|
|
c9dcff33ae | ||
|
|
af50894c61 | ||
|
|
4e5835d658 | ||
|
|
5e5cc608df | ||
|
|
82be2508f0 | ||
|
|
271b02220f | ||
|
|
607ccf3049 | ||
|
|
2f1d023860 | ||
|
|
078b61c1a3 | ||
|
|
19b854e0d2 | ||
|
|
a00ca40a0c | ||
|
|
a0b675c087 | ||
|
|
ecbafbebf9 | ||
|
|
4a63fd88b2 | ||
|
|
6adf5661f4 | ||
|
|
4595699172 | ||
|
|
47d05a9495 | ||
|
|
4f76d956b4 | ||
|
|
7be4edff84 | ||
|
|
bb3141b7d5 | ||
|
|
ca592b3a01 | ||
|
|
6a0405a10c | ||
|
|
e1d0a6dfd4 | ||
|
|
334dc060a0 | ||
|
|
023e7abc42 | ||
|
|
d40cb19155 | ||
|
|
a22461d481 | ||
|
|
7cc76649b3 | ||
|
|
aa06c1bcaf | ||
|
|
f9b8764941 | ||
|
|
8e2ad4c163 | ||
|
|
03f9cf1ddb | ||
|
|
fb763be06f | ||
|
|
30c2107c88 | ||
|
|
920d137ed1 | ||
|
|
5b833404da | ||
|
|
ac5dcc196e | ||
|
|
2461e75379 | ||
|
|
824551822f | ||
|
|
135d0dd8bf | ||
|
|
68eb30cf6d | ||
|
|
ec057d72e2 | ||
|
|
f9b38aaba8 | ||
|
|
ba6e327cd1 | ||
|
|
04d26210e3 | ||
|
|
3364a57fb6 | ||
|
|
c4156a3ee5 | ||
|
|
a33fcf28cb | ||
|
|
a2cede7b1a | ||
|
|
0009bd1eba | ||
|
|
54cdee2f69 | ||
|
|
7c770c4ea3 | ||
|
|
aee7143511 | ||
|
|
64361405cb | ||
|
|
3f47aff03f | ||
|
|
dde81fb38a | ||
|
|
deb38e665b | ||
|
|
7de23d9157 | ||
|
|
469a85887d | ||
|
|
8eeabc594b | ||
|
|
828fca2ea5 | ||
|
|
9bbc189ef3 | ||
|
|
6bda90ff9f | ||
|
|
7bafeb6278 | ||
|
|
fc0d7c7f35 | ||
|
|
ae0dbc037a | ||
|
|
603a36f575 | ||
|
|
4fe36c11a5 | ||
|
|
7ef2b9d245 | ||
|
|
3a4b41f428 | ||
|
|
f47b2218e3 | ||
|
|
6ef9edc220 | ||
|
|
989ee1c623 | ||
|
|
b27dc52db1 | ||
|
|
4e7725a584 | ||
|
|
f55e0a9942 | ||
|
|
fe642b0a5b | ||
|
|
d97be8d7a2 | ||
|
|
a3fda5a5e1 | ||
|
|
e6c2472a3b | ||
|
|
43fff54ea4 | ||
|
|
cd80e1afd0 | ||
|
|
3b8c9acbd2 | ||
|
|
be126ff0f1 | ||
|
|
7e20edf4f4 | ||
|
|
28b9316cdc | ||
|
|
a27b29c046 | ||
|
|
5a9d2c0ed3 | ||
|
|
bb7c2eb52b | ||
|
|
d78848abc5 | ||
|
|
525d9a3cc3 | ||
|
|
36aca136ec | ||
|
|
3918fbce98 | ||
|
|
0c7d4952a6 | ||
|
|
8fc05c376a | ||
|
|
905cf7d680 | ||
|
|
626c789384 | ||
|
|
5ce289a687 | ||
|
|
ed3a394049 | ||
|
|
778529e745 | ||
|
|
7290bbab1e | ||
|
|
2f1ee81f6c | ||
|
|
af8ca0ae6b | ||
|
|
24304a3706 | ||
|
|
e2ff3484de | ||
|
|
146ea1e1ac | ||
|
|
0c01351eca | ||
|
|
bd94ea04de | ||
|
|
f13b9a7090 | ||
|
|
81510dfa07 | ||
|
|
c46e3d8fb6 | ||
|
|
23b5e48564 | ||
|
|
ba41b226ed | ||
|
|
c0b658fee0 | ||
|
|
870fbc072a | ||
|
|
8ccc02fb67 | ||
|
|
6319ba4d92 | ||
|
|
89b41147c3 | ||
|
|
84ac2ac944 | ||
|
|
f1156b4efd | ||
|
|
1795efac8c | ||
|
|
2b06bb0589 | ||
|
|
504aa9c522 | ||
|
|
2ab0a9241a | ||
|
|
0dce40a56f | ||
|
|
8ad61459a5 | ||
|
|
0fdf9faad4 | ||
|
|
c248531458 | ||
|
|
ce44e73caf | ||
|
|
9c3e6b9c94 | ||
|
|
8369dfbf26 | ||
|
|
065fe2551b | ||
|
|
43af7de3c5 | ||
|
|
1151f35aa1 | ||
|
|
1aadabe0d2 | ||
|
|
c4c2807b30 | ||
|
|
d2a847326c | ||
|
|
cc62d4d73f | ||
|
|
aef6474361 | ||
|
|
66e12063b9 | ||
|
|
05d6f0ba86 | ||
|
|
6ce636855f | ||
|
|
d379dbaeb0 | ||
|
|
d808ae35d2 | ||
|
|
074224341e | ||
|
|
438a4f4ea9 | ||
|
|
534475c0f7 | ||
|
|
1d63bdbc4f | ||
|
|
2c7b831c03 | ||
|
|
7ca283a7c6 | ||
|
|
df71f7cf9b | ||
|
|
7b9a564f8d | ||
|
|
c0b0ef9b22 | ||
|
|
ed5b06b9e8 | ||
|
|
ef7fc8d697 | ||
|
|
4cdd7f0e78 | ||
|
|
0b8eb52ab7 | ||
|
|
2faffcadc9 | ||
|
|
df754f6e68 | ||
|
|
0fc2214f11 | ||
|
|
3b62447ce0 | ||
|
|
3edff2e001 | ||
|
|
3f51b39795 | ||
|
|
05a17ed0f6 | ||
|
|
3cbd668fc7 | ||
|
|
fb291e899a | ||
|
|
61690f85d7 | ||
|
|
fafc3aa460 | ||
|
|
60b0060239 | ||
|
|
cd8d2cb360 | ||
|
|
713c2ea076 | ||
|
|
35f3248381 | ||
|
|
73d32404da | ||
|
|
164edcb9cf | ||
|
|
5da5c11058 | ||
|
|
7b3cf9bf61 | ||
|
|
1543a151a7 | ||
|
|
2992f886d5 | ||
|
|
a0720e72b5 | ||
|
|
75c71b394d | ||
|
|
bc8bf2c183 | ||
|
|
bcb5e90938 | ||
|
|
65f6b03ff6 | ||
|
|
a1643efb32 | ||
|
|
3515c73cba | ||
|
|
6317ebd167 | ||
|
|
4c54061e87 | ||
|
|
c2a9a44d42 | ||
|
|
7f2d12b5a5 | ||
|
|
179b785bf9 | ||
|
|
2f6177cd33 | ||
|
|
04fae6c47c | ||
|
|
79997281c0 | ||
|
|
3665f617f4 | ||
|
|
b0bb0cdeda | ||
|
|
6c4eacd9bb | ||
|
|
357ee4898e | ||
|
|
948b9a335b |
@@ -0,0 +1,140 @@
|
||||
---
|
||||
name: "source-command-split-commit"
|
||||
description: "将当前未提交改动拆分为多个符合规范的原子提交"
|
||||
---
|
||||
|
||||
# source-command-split-commit
|
||||
|
||||
Use this skill when the user asks to run the migrated source command `split-commit`.
|
||||
|
||||
## Command Template
|
||||
|
||||
将当前未提交改动拆分为多个原子提交。执行前会分析变更、生成分组方案,经用户确认后逐组提交。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### Step 1: 前置检查
|
||||
|
||||
1. **清理残留文件**:若存在 `msg.txt`,先 `rm -f msg.txt`
|
||||
2. **读取规范**:确认 `COMMIT_CONVENTION.md` 存在且可读
|
||||
|
||||
### Step 2: 保护分支检查
|
||||
|
||||
保护分支列表(按优先级):`AGENTS.md` > `.cursor/rules/` > `.git/config branch.protected` > 默认(`master|main|develop`)
|
||||
|
||||
若当前在保护分支,询问用户:
|
||||
- **A** - 创建 feature 分支后继续拆分(推荐)
|
||||
- **B** - 转移到新分支后压成单提交
|
||||
- **C** - 取消
|
||||
|
||||
### Step 3: 盘点变更
|
||||
|
||||
执行以下命令分析:
|
||||
- `git status` - 整体状态
|
||||
- `git diff --stat` - 文件统计
|
||||
- `git diff` - 详细改动
|
||||
- `git diff --cached` - 已暂存改动
|
||||
|
||||
### Step 4: 规则化分组
|
||||
|
||||
按"意图一致"原则分组,**优先按 type 分,再按 scope 分**。
|
||||
|
||||
**必须拆分的情况**:
|
||||
- 同一组可归属多个 type
|
||||
- 同文件包含多个独立目的(如功能+重构)
|
||||
- 资源替换与代码逻辑混合
|
||||
|
||||
**可以合并的情况**(最小拆分原则):
|
||||
- 同一功能的多文件改动(脚本+Prefab)
|
||||
- 同一模块的多个相关 Bug 修复
|
||||
|
||||
### Step 5: 生成方案表格
|
||||
|
||||
以表格形式展示拆分方案:
|
||||
|
||||
| 顺序 | 文件 | 变更目的 | 提交头 | 依据 | 方式 |
|
||||
|------|------|----------|--------|------|------|
|
||||
| 1 | `路径` | 描述 | `type(scope): subject` | 原因 | `git add`/`git add -p` |
|
||||
|
||||
**Type 选择优先级**:`fix` > `feat` > `art` > `audio` > `scene` > `yarn` > `perf|refactor|...`
|
||||
|
||||
**Scope 规则**:单模块用模块名(如 `blockpuzzle`),跨模块省略。
|
||||
|
||||
### Step 6: 用户确认
|
||||
|
||||
询问:`[Y 执行 / n 取消 / 提出调整]`
|
||||
|
||||
- 若调整:根据反馈修改方案,再次确认
|
||||
- 若执行:进入 Step 7
|
||||
- 若取消:清理并退出
|
||||
|
||||
### Step 7: 逐组提交
|
||||
|
||||
对每组执行:
|
||||
1. `git add <file>` 或 `git add -p <file>`
|
||||
2. 复查:`git diff --cached --stat` 和 `git diff --cached`
|
||||
3. 写入 `msg.txt` 并提交:`git commit -F msg.txt`
|
||||
4. 检查状态:`git status`
|
||||
|
||||
**失败处理**:
|
||||
- `git add -p` 跳过关键 hunk → 暂停,提示处理方案
|
||||
- pre-commit hook 失败 → 询问 [R]重试 [S]跳过 [A]中止
|
||||
|
||||
### Step 8: 结果校验
|
||||
|
||||
输出:
|
||||
1. `git log --oneline -<N>`
|
||||
2. 回放清单(验证 type/scope/subject 合规性)
|
||||
3. 若不合规,询问是否修正
|
||||
|
||||
### Step 9: 清理
|
||||
|
||||
`rm -f msg.txt`
|
||||
|
||||
---
|
||||
|
||||
## Unity 改动速查表
|
||||
|
||||
| 改动类型 | type | scope 示例 |
|
||||
|----------|------|-----------|
|
||||
| 脚本逻辑修复 | `fix` | `blockpuzzle`, `dialog` |
|
||||
| 脚本新功能 | `feat` | `huoshan`, `clue` |
|
||||
| 美术资源(贴图/模型/动画) | `art` | 对应模块或省略 |
|
||||
| 音频资源 | `audio` | `audio-kit` 或省略 |
|
||||
| 场景配置(灯光/镜头) | `scene` | 对应模块或省略 |
|
||||
| Yarn 对话 | `yarn` | `dialog` |
|
||||
| 测试代码 | `test` | 对应模块 |
|
||||
|
||||
---
|
||||
|
||||
## 规范校验清单(执行前检查)
|
||||
|
||||
- [ ] 格式:`<type>(<scope>): <subject>` 或 `<type>: <subject>`
|
||||
- [ ] type ∈ {`feat`,`fix`,`art`,`audio`,`scene`,`yarn`,`perf`,`refactor`,`style`,`docs`,`build`,`chore`,`test`}
|
||||
- [ ] subject:中文、动词开头、≤50字、无句号
|
||||
- [ ] 一次提交只做一件事
|
||||
|
||||
---
|
||||
|
||||
## 中文提交方法
|
||||
|
||||
由于 PowerShell 中文乱码问题,统一使用文件方式:
|
||||
|
||||
```bash
|
||||
echo "type(scope): 描述" > msg.txt
|
||||
git commit -F msg.txt
|
||||
rm -f msg.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关于 Pre-commit Hooks
|
||||
|
||||
若配置了 husky/lint-staged:
|
||||
- 每次提交都会触发检查
|
||||
- 如需跳过(**不推荐**):`git commit -F msg.txt --no-verify`
|
||||
- 跳过可能导致 CI 失败
|
||||
|
||||
---
|
||||
|
||||
开始执行请回复 **"开始"**,或告知需要的调整。
|
||||
@@ -0,0 +1,17 @@
|
||||
# Yarn 插线维修对话 — 快速审查
|
||||
|
||||
对 `FP_Huoshan1` 或同类 **BodyModule 插线 + `$gameStage` 多阶段** 的 `.yarn` 做一轮检查。
|
||||
|
||||
## 用法
|
||||
|
||||
在对话里说明「按 yarn-plug-review 审查某文件/目录」或直接粘贴节点片段。
|
||||
|
||||
## 审查项(简版)
|
||||
|
||||
1. **跳转条件**:会 `jump` 结束/换阶段的块是否带 `$gameStage==…`,避免后期变量仍为 true 时回退阶段。
|
||||
2. **重复插线**:同一 stage、同一模块第二次插线是否为短总结 + 下一步提示,而非全长对白与重复 `complete_task`。
|
||||
3. **TaskToDo**:`TaskToDo--` 是否与「首次完成」绑定(如 `$speakerFinalOK` / `$saleFinalOK`),避免重复递减。
|
||||
4. **无序插线**:是否已去掉「分支外无条件 set 完成标记」导致跳剧情。
|
||||
5. **EventNode**:`*PlugIn` 名称与 `moduleName`、目标 `title:` 一致。
|
||||
|
||||
完整说明与模式见:`.cursor/skills/yarn-plug-in-flow/SKILL.md`。
|
||||
@@ -0,0 +1,356 @@
|
||||
---
|
||||
name: 塔罗牌小游戏实现
|
||||
overview: 基于 Sprite + EventTriggerEx 实现塔罗牌小游戏,Yarn 命令细粒度控制每个阶段(展示牌堆、展开、等待选牌、翻牌、设置朝向),与对话深度配合。
|
||||
todos:
|
||||
- id: tarot-card
|
||||
content: 创建 TarotCard.cs -- 单张牌组件:IInteraction、SpriteRenderer 正反面、EventTriggerEx 注册、翻牌动画、朝向控制(正/倒)、悬停/点击回调
|
||||
status: completed
|
||||
- id: tarot-deck
|
||||
content: 创建 TarotDeck.cs -- 牌堆管理:动态生成牌、扇形展开布局、悬停推出效果、选牌流程(FadeOut + 居中检视)
|
||||
status: completed
|
||||
- id: tarot-manager
|
||||
content: 创建 TarotManager.cs -- 系统管理器:SystemDic 注册、视图开关、阶段状态机、选牌结果
|
||||
status: completed
|
||||
- id: tarot-yarn
|
||||
content: 创建 TarotYarnCommand.cs -- 细粒度 Yarn 命令集(show/spread/wait_select/flip/set_orientation/rotate/hide)
|
||||
status: completed
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# 塔罗牌小游戏实现计划
|
||||
|
||||
## 设计目标
|
||||
|
||||
Yarn 脚本能**细粒度控制**塔罗牌的每个视觉阶段,每个命令之间可以穿插对话,实现对话与交互的深度配合。
|
||||
|
||||
## Yarn 脚本示例(对应 FP_Day1_night 第 364-395 行的改造)
|
||||
|
||||
```yarn
|
||||
ql: 我觉得也许你需要这个。
|
||||
<<show_tarot_deck>>
|
||||
【它拿出一套崭新的卡牌。】
|
||||
|
||||
me: 这是什么?
|
||||
|
||||
ql: "塔罗牌"。
|
||||
ql: 试试吧。抽一张。
|
||||
<<spread_tarot 5>>
|
||||
|
||||
-> 抽牌
|
||||
<<wait_tarot_select>>
|
||||
|
||||
【你看了看牌。】
|
||||
<<flip_tarot "inverted">>
|
||||
// 翻到正面,但预设为倒置朝向(inverted)
|
||||
|
||||
【上面画着一个张开双臂的机体...像是...在把它吸过去。】
|
||||
me: 这画的是什么?
|
||||
|
||||
ql: 倒吊人。你拿反了。
|
||||
|
||||
<<rotate_tarot "normal">>
|
||||
// Yarn 命令改变朝向:倒置 -> 正向(旋转 180 度动画)
|
||||
【你把牌转过来。】
|
||||
【这下你看明白了。这是一个倒吊着的机体...】
|
||||
|
||||
// ...更多对话...
|
||||
|
||||
<<hide_tarot>>
|
||||
```
|
||||
|
||||
## 架构概览
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Yarn["Yarn 脚本"] -->|"<<show_tarot_deck>>"| CMD["TarotYarnCommand\n(静态 Yarn 命令类)"]
|
||||
Yarn -->|"<<spread_tarot 5>>"| CMD
|
||||
Yarn -->|"<<wait_tarot_select>>"| CMD
|
||||
Yarn -->|"<<flip_tarot inverted>>"| CMD
|
||||
Yarn -->|"<<rotate_tarot normal>>"| CMD
|
||||
Yarn -->|"<<hide_tarot>>"| CMD
|
||||
CMD --> TM["TarotManager\n(SystemDic 注册)"]
|
||||
TM --> TD["TarotDeck\n牌堆/扇形/选牌"]
|
||||
TD --> TC["TarotCard x N\nSprite + EventTriggerEx"]
|
||||
TC -->|IInteraction| ES["EventSystemEx"]
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 新增文件结构
|
||||
|
||||
```
|
||||
Assets/Scripts/MiniGame/Tarot/
|
||||
TarotManager.cs -- 管理器:SystemDic 注册、视图开关、阶段协调
|
||||
TarotCard.cs -- 单张牌:IInteraction + 翻牌 + 朝向
|
||||
TarotDeck.cs -- 牌堆逻辑:生成、展开、悬停、选牌
|
||||
TarotYarnCommand.cs -- 细粒度 Yarn 命令集
|
||||
```
|
||||
|
||||
## 核心设计
|
||||
|
||||
### 1. TarotCard -- 单张牌
|
||||
|
||||
参考 [KnobController](Assets/Scripts/MiniGame/HuoShan/SalesSystem/KnobController.cs) 的 EventTriggerEx 注册模式(行 165-199)。
|
||||
|
||||
- 实现 `IInteraction` 接口(`IsActive`, `IsAvailable`, `GetGameObject()`)
|
||||
- 需要 `Collider2D`(BoxCollider2D,牌面大小)
|
||||
- `SpriteRenderer` + 两个 Sprite 引用:`frontSprite` / `backSprite`
|
||||
|
||||
**朝向系统(Orientation)**:
|
||||
|
||||
- 枚举 `TarotOrientation { Normal, Inverted }`
|
||||
- `Normal`:正面朝上(localScale.y = 1)
|
||||
- `Inverted`:倒置(localScale.y = -1,即上下颠倒)
|
||||
- `SetOrientation(orientation, animated)` 方法,animated 时用 DOTween 做 Y 轴 scale 过渡
|
||||
|
||||
**翻牌动画**:
|
||||
|
||||
- `Flip(targetOrientation, duration)` 协程
|
||||
- DOTween 缩放 localScale.x: 1 -> 0(前半),切换 Sprite,再 0 -> 1(后半)
|
||||
- 翻牌完成时根据 `targetOrientation` 设置 localScale.y
|
||||
|
||||
**交互注册**(Awake):
|
||||
|
||||
- 获取/添加 `EventTriggerEx`
|
||||
- `EnsureEventTriggerEntries()` 确保 PointerEnter/PointerExit/PointerClick 条目
|
||||
- `Register(PointerEnter, OnHoverEnter)` / `Register(PointerExit, OnHoverExit)` / `Register(PointerClick, OnClick)`
|
||||
- 回调通过 `System.Action` 委托给 `TarotDeck`
|
||||
|
||||
**状态**:
|
||||
|
||||
- `bool IsFaceUp` -- 当前是否正面朝上
|
||||
- `TarotOrientation Orientation` -- 当前朝向
|
||||
- `bool IsInteractable` -- 是否接受交互(由 Deck 控制)
|
||||
|
||||
### 2. TarotDeck -- 牌堆管理
|
||||
|
||||
**动态生成**:
|
||||
|
||||
- `Setup(int cardCount, Sprite front, Sprite back)` -- 创建 N 个 TarotCard 子物体
|
||||
- 每张牌初始背面朝上,叠在一起(牌堆状态)
|
||||
|
||||
**扇形展开**:
|
||||
|
||||
- `SpreadCards(float duration)` 协程
|
||||
- 配置项:`fanAngleRange`(扇形总角度,如 120 度)、`fanRadius`(扇形半径)
|
||||
- 每张牌的目标角度:`centerAngle + (i - (count-1)/2f) * angleStep`
|
||||
- 每张牌的目标位置:以旋转角度沿圆弧分布
|
||||
- DOTween `DOLocalMove` + `DOLocalRotate` 同步动画
|
||||
|
||||
**悬停推出**:
|
||||
|
||||
- 牌注册的 `OnHoverEnter` -> 沿牌面法线方向(局部 Y 轴上方)推出一段距离
|
||||
- `OnHoverExit` -> 回到扇形位置
|
||||
- 用 DOTween `DOLocalMove`,设短 duration(0.15s)
|
||||
|
||||
**选牌**:
|
||||
|
||||
- `OnCardClicked(TarotCard card)` -- 锁定交互,触发选牌序列
|
||||
- 其余牌 `SpriteRenderer.DOFade(0, 0.3f)` 渐隐
|
||||
- 选中牌移至检视位置(屏幕中央偏上),放大到检视尺寸
|
||||
- 设置 `_selectedCard`,通知 Manager 选牌完成
|
||||
|
||||
**检视位置**:
|
||||
|
||||
- `[SerializeField] Transform inspectPosition` -- 检视锚点
|
||||
- `[SerializeField] float inspectScale` -- 检视缩放
|
||||
|
||||
### 3. TarotManager -- 管理器
|
||||
|
||||
- `Start()` 中 `FixSystemCenter.SystemDic.Register(this)`
|
||||
- 持有 `TarotDeck` 引用(`GetComponentInChildren`)
|
||||
- 状态标记:`_isWaitingForSelection`(是否在等待玩家选牌)、`_isCardSelected`
|
||||
- `OpenView()` / `CloseView()` 控制根 GameObject 显隐
|
||||
- `WaitForSelection()` 协程 -- while 循环等待 `_isCardSelected`,供 YarnCommand 使用
|
||||
- `SelectedCard` 属性 -- 获取当前选中的牌
|
||||
|
||||
### 4. TarotYarnCommand -- 细粒度 Yarn 命令集
|
||||
|
||||
所有命令通过 `FixSystemCenter.SystemDic.Get<TarotManager>()` 获取 Manager 实例。
|
||||
|
||||
|
||||
| Yarn 命令 | 方法签名 | 说明 |
|
||||
| ----------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------- |
|
||||
| `<<show_tarot_deck>>` | `ShowTarotDeck(int count = 5)` | 显示牌堆(叠放状态),可指定牌数 |
|
||||
| `<<spread_tarot>>` 或 `<<spread_tarot 5>>` | `IEnumerator SpreadTarot(int count = -1)` | 扇形展开,count=-1 用已有牌数;协程等待展开动画完成 |
|
||||
| `<<wait_tarot_select>>` | `IEnumerator WaitTarotSelect()` | 阻塞 Yarn 直到玩家点击选中一张牌;选中后执行选牌动画(其余牌淡出、选中牌居中);结果写入 `$tarotSelectedIndex` |
|
||||
| `<<flip_tarot "normal">>` | `IEnumerator FlipTarot(string orientation = "normal")` | 翻牌到正面,同时设置朝向;`"normal"` = 正向,`"inverted"` = 倒置 |
|
||||
| `<<rotate_tarot "normal">>` | `IEnumerator RotateTarot(string orientation)` | 改变已翻开牌的朝向(旋转动画),不翻面 |
|
||||
| `<<hide_tarot>>` | `IEnumerator HideTarot()` | 淡出并隐藏所有塔罗元素 |
|
||||
|
||||
|
||||
### 5. 完整交互流程
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> DeckVisible: show_tarot_deck
|
||||
DeckVisible --> DeckVisible: 对话继续\n(牌堆静态展示)
|
||||
DeckVisible --> FanOut: spread_tarot
|
||||
FanOut --> WaitSelect: wait_tarot_select
|
||||
WaitSelect --> WaitSelect: hover 推出/回位
|
||||
WaitSelect --> Selected: 玩家点击选牌
|
||||
Selected --> InspectBack: 其余牌淡出\n选中牌居中(背面)
|
||||
InspectBack --> InspectBack: 对话继续\n(背面检视)
|
||||
InspectBack --> InspectFront: flip_tarot\n(预设朝向)
|
||||
InspectFront --> InspectFront: 对话继续
|
||||
InspectFront --> InspectRotated: rotate_tarot\n(改变朝向)
|
||||
InspectRotated --> InspectRotated: 对话继续
|
||||
InspectRotated --> [*]: hide_tarot
|
||||
InspectFront --> [*]: hide_tarot
|
||||
```
|
||||
|
||||
|
||||
|
||||
关键:每个状态之间 Yarn 对话可以自由穿插,命令只推进视觉阶段,不阻塞对话本身(除了 `wait_tarot_select` 是阻塞等待玩家交互)。
|
||||
|
||||
### 6. Sprite 资源
|
||||
|
||||
当前使用 `Assets/RawResources/Art/CG/塔罗牌/塔罗牌正面.png` 和 `塔罗牌背面.png`,所有牌共用同一对正反面。后续可扩展为每张牌不同正面。
|
||||
|
||||
## 场景配置
|
||||
|
||||
### 层级结构
|
||||
|
||||
```
|
||||
TarotSystem -- 空 GameObject,放在需要的场景中
|
||||
├── TarotManager (Component) -- 管理器脚本
|
||||
│
|
||||
├── TarotView -- 视图容器(OpenView/CloseView 控制这个)
|
||||
│ ├── TarotDeck (Component) -- 牌堆逻辑脚本
|
||||
│ │ └── [运行时动态生成的牌] -- TarotCard x N
|
||||
│ │
|
||||
│ └── InspectAnchor -- 检视锚点(空物体,标记牌选中后居中的位置)
|
||||
│ (Transform 位置设在画面中央偏上)
|
||||
│
|
||||
└── (可选) Cinemachine Camera -- 如果塔罗需要独立相机视角
|
||||
```
|
||||
|
||||
### 需要手动配置的部分
|
||||
|
||||
**1. TarotSystem 根物体**
|
||||
|
||||
- 放在对应场景中(如 Day1_night 所在场景)
|
||||
- 位置在世界空间中合适的地方(与当前相机视角对齐)
|
||||
|
||||
**2. TarotManager 组件(Inspector 面板)**
|
||||
|
||||
```
|
||||
[Header("视图")]
|
||||
TarotView -- 拖入 TarotView 子物体(或自动 Find)
|
||||
|
||||
[Header("牌面素材")]
|
||||
FrontSprite -- 拖入 塔罗牌正面 Sprite
|
||||
BackSprite -- 拖入 塔罗牌背面 Sprite
|
||||
|
||||
[Header("牌 Prefab")]
|
||||
CardPrefab -- 拖入预制的单张牌 Prefab(见下方)
|
||||
```
|
||||
|
||||
**3. 单张牌 Prefab(预制体)**
|
||||
|
||||
需要提前制作一个 Prefab,结构如下:
|
||||
|
||||
```
|
||||
TarotCard (Prefab)
|
||||
Components:
|
||||
- SpriteRenderer (默认 sprite = 背面,sortingOrder 按需)
|
||||
- BoxCollider2D (Size 匹配牌面大小,用于点击/悬停检测)
|
||||
- EventTriggerEx (Inspector 中添加 3 个 Trigger 条目:
|
||||
PointerEnter, PointerExit, PointerClick)
|
||||
- TarotCard.cs (脚本自动注册事件)
|
||||
```
|
||||
|
||||
EventTriggerEx 的 Inspector 配置要点:
|
||||
|
||||
- 点击 "Add New Event Type"
|
||||
- 分别添加 `PointerEnter`、`PointerExit`、`PointerClick` 三个条目
|
||||
- 回调列表留空(代码中 `Register` 动态绑定)
|
||||
|
||||
> 这与 KnobController 的模式一致:EventTriggerEx 需要 Inspector 中预配好条目,代码中 `TryGetTriggerEvent` 才能找到对应的 callback list。代码中也会 `EnsureEventTriggerEntries()` 作为保底自动添加。
|
||||
|
||||
**4. TarotDeck 组件(Inspector 面板)**
|
||||
|
||||
```
|
||||
[Header("扇形展开参数")]
|
||||
FanAngleRange = 120 -- 扇形总角度(度)
|
||||
FanRadius = 3.0 -- 扇形半径(世界单位)
|
||||
FanCenter = (0,0,0) -- 扇形圆心偏移
|
||||
|
||||
[Header("悬停")]
|
||||
HoverOffset = 0.5 -- 悬停推出距离
|
||||
HoverDuration = 0.15 -- 悬停动画时长
|
||||
|
||||
[Header("检视")]
|
||||
InspectAnchor -- 拖入 InspectAnchor 子物体
|
||||
InspectScale = 1.5 -- 检视模式缩放倍数
|
||||
|
||||
[Header("动画时长")]
|
||||
SpreadDuration = 0.6 -- 展开动画时长
|
||||
FadeOutDuration = 0.3 -- 未选中牌淡出时长
|
||||
MoveToInspectDuration = 0.4 -- 选中牌移至检视位置时长
|
||||
```
|
||||
|
||||
### 运行时流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Y as Yarn
|
||||
participant M as TarotManager
|
||||
participant D as TarotDeck
|
||||
participant C as TarotCard
|
||||
|
||||
Y->>M: show_tarot_deck(5)
|
||||
M->>M: OpenView()
|
||||
M->>D: Setup(5, frontSprite, backSprite)
|
||||
D->>D: Instantiate CardPrefab x 5
|
||||
D->>C: 初始化 (背面, 叠放)
|
||||
Note over Y: 对话继续...
|
||||
|
||||
Y->>D: spread_tarot
|
||||
D->>C: DOTween 扇形展开动画
|
||||
|
||||
Note over Y: 对话继续...
|
||||
|
||||
Y->>M: wait_tarot_select (阻塞)
|
||||
Note over C: 玩家悬停/点击
|
||||
C-->>D: OnCardClicked
|
||||
D->>D: 其余牌淡出, 选中牌居中
|
||||
D-->>M: 选牌完成
|
||||
M-->>Y: 协程返回
|
||||
|
||||
Note over Y: 对话继续...
|
||||
|
||||
Y->>M: flip_tarot("inverted")
|
||||
M->>C: Flip(inverted)
|
||||
C->>C: X轴缩放动画 + 切换Sprite + Y轴倒置
|
||||
|
||||
Note over Y: 对话: "你拿反了"
|
||||
|
||||
Y->>M: rotate_tarot("normal")
|
||||
M->>C: SetOrientation(normal, animated=true)
|
||||
C->>C: Y轴旋转动画 (倒置->正向)
|
||||
|
||||
Note over Y: 对话继续...
|
||||
|
||||
Y->>M: hide_tarot
|
||||
M->>D: 淡出所有牌
|
||||
M->>M: CloseView()
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 不需要手动做的部分(代码自动处理)
|
||||
|
||||
- 牌的动态实例化和销毁(`TarotDeck` 负责)
|
||||
- EventTriggerEx 事件绑定(`TarotCard.Awake` 自动注册)
|
||||
- SystemDic 注册(`TarotManager.Start` 自动注册)
|
||||
- 扇形位置计算(`TarotDeck` 按参数自动排布)
|
||||
|
||||
## 关键技术点
|
||||
|
||||
- **EventTriggerEx 注册模式**:参考 `KnobController.Awake()` (行 165-199),需先 `EnsureEventTriggerEntries()` 确保 triggers 列表有对应条目,再调用 `Register(EventTriggerType, callback)`
|
||||
- **DOTween 动画**:项目已引入 `DG.Tweening`,用于扇形展开、悬停推出、翻牌、FadeOut
|
||||
- **IInteraction 实现**:`IsActive` / `IsAvailable` 控制交互可用性,配合 `EventSystemEx.isLocked` 在对话期间自动禁用交互(对话进行时 `isLocked=true`,`wait_tarot_select` 时对话暂停所以 `isLocked=false`,交互自然可用)
|
||||
- **SystemDic 注册**:`FixSystemCenter.SystemDic.Register(this)` 使 YarnCommand 能通过 `Get<TarotManager>()` 获取实例
|
||||
- **Yarn 变量写入**:`StorageSystem.Instance.SetValue("$tarotSelectedIndex", value)` 将选牌结果传回 Yarn
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: narrative-timeline-flow
|
||||
description: >-
|
||||
叙事 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` 参数错误
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
name: yarn-plug-in-flow
|
||||
description: >-
|
||||
Yarn 维修插线对话:gameStage 调度、重复插线短总结、防状态回退与 TaskToDo 重复递减。
|
||||
Use when 写 FixSystem/BodyModule 插线 Yarn、从自然语言生成维修对话、审阅 HuoShan 类流程、
|
||||
检查 PlugIn 节点、重复执行、插线检查、或用户提到 yarn 插线/维修界面对话。
|
||||
---
|
||||
|
||||
# Yarn 插线维修对话(FixSystem)
|
||||
|
||||
本 skill 适用于:`BodyModule` 通过 `{moduleName}PlugIn` 跳入 Yarn、同一节点内用 `$gameStage` 分支多阶段叙事(如 `FP_Huoshan1`)。
|
||||
|
||||
## 1. C# 侧事实(设计 Yarn 前必读)
|
||||
|
||||
- 插线触发:`BodyModule.PlugIn()` → `DialogController.StartDialogNode(data.PlugInNodeName)`,节点名为 **`{moduleName}PlugIn`**(见 `BodyModuleData.PlugInNodeTemplate`)。
|
||||
- **C# 一般不按阶段禁用插线**;若某 Stage 实际不可插线,是场景/交互配置决定,但 Yarn 仍应防御性处理「错误阶段误入」。
|
||||
- EventNode 里用 `<<jump 检查xxx>>` 桥接到共享节点;共享节点名须与 `*.yarn` 中 `title:` 一致。
|
||||
|
||||
## 2. 状态变量约定(推荐)
|
||||
|
||||
| 用途 | 模式 |
|
||||
|------|------|
|
||||
| 流程阶段 | `$gameStage`(数字;仅在 `Center` 等少数节点切换,避免散落 `set`) |
|
||||
| 某模块「本档首次插线检查完」 | `$xxxChecked`(如 `$speakerChecked` / `$colorChecked` / `$emoChecked`) |
|
||||
| 多子任务计数 | `$TaskToDo`,完成时 `--`,**跳转前务必保证不会重复减** |
|
||||
| 同一 Stage 内「最终检查子项已完成」 | 独立布尔,如 `$speakerFinalOK` / `$saleFinalOK`,防止重复插线再次 `complete_task` 与 `--` |
|
||||
|
||||
在 `VarsInit`(或等价入口)用 `<<declare>>` 初始化所有布尔/计数器,避免未定义行为。
|
||||
|
||||
## 3. 从自然语言 / 大纲生成 Yarn 时的必做项
|
||||
|
||||
1. **列出插线入口**:每个 `*PlugIn` → 目标节点;与模块 `moduleName` 一致。
|
||||
2. **列出 `$gameStage` 与文档步骤映射**(如 `02_flow.md`),每个共享检查节点有哪些 `elseif $gameStage==N`。
|
||||
3. **任何会 `<<jump>>` 换阶段的块**:条件里加 **`$gameStage==N`**,避免后期 `$xxxChecked` 仍为 true 时误触发旧结局(状态回退)。
|
||||
4. **同一 `$gameStage` 内、同一模块第二次插线**:
|
||||
- 首次:完整演出 + `complete_task` + 设 `$xxxChecked` / `$xxxFinalOK`。
|
||||
- 再次:`<<if $xxxChecked == true>>`(或对应 FinalOK)→ **短总结 + 自言自语下一步**(去查哪个模块、任务提示),**不**重复长对白、**不**重复任务完成副作用。
|
||||
5. **多分支共享「完成标记」**:勿在 `if/elseif` 外无条件 `set $colorChecked=true` 等;否则乱序插线会跳过叙事(见此前 HuoShan Stage2 修复)。
|
||||
|
||||
## 4. 已完成 Yarn 的审查清单(可逐项打勾)
|
||||
|
||||
### 4.1 阶段与跳转
|
||||
|
||||
- [ ] 会推进阶段或结束的 `<<jump>>`,外层条件是否包含正确的 `$gameStage`?
|
||||
- [ ] `Center` / Stage 入口是否唯一或清晰,避免死循环、重复 `set gameStage`?
|
||||
|
||||
### 4.2 重复插线(同 Stage、同模块)
|
||||
|
||||
- [ ] Stage2 类:表达 / 销售 / 情绪等主流程,第二次插线是否为「短总结 + 下一步」而非全长?
|
||||
- [ ] Stage7 类双任务:是否用 **FinalOK**(或等价)避免第二次插线再次 `TaskToDo--` / 重复 `complete_task`?
|
||||
|
||||
### 4.3 变量与任务
|
||||
|
||||
- [ ] `complete_task` 与 `$TaskToDo` 递减是否一一对应、且只在「首次成功路径」执行?
|
||||
- [ ] `$PlugCount` 等统计是否与「首次访问」一致(可用 `visited("NodeName")` 或 `== false` 包裹)?
|
||||
|
||||
### 4.4 文档与代码
|
||||
|
||||
- [ ] `EventNode` 表与 `title:` 节点名一致(如 `SalePlugIn` 非过期的 `ColorPlugIn`)。
|
||||
|
||||
## 5. 最小代码模式参考(Yarn 片段)
|
||||
|
||||
**防跨阶段误触发结局:**
|
||||
|
||||
```yarn
|
||||
<<if $gameStage==2 && $speakerChecked==true && $colorChecked==true>>
|
||||
// 波形对比 → 结束本阶段
|
||||
<<endif>>
|
||||
```
|
||||
|
||||
**同 Stage 重复插线(短总结):**
|
||||
|
||||
```yarn
|
||||
<<if $gameStage==2>>
|
||||
<<if $speakerChecked == true>>
|
||||
me: (总结结论)…
|
||||
me: (下一步该做什么)…
|
||||
<<else>>
|
||||
// 首次完整流程
|
||||
<<set $speakerChecked = true>>
|
||||
<<endif>>
|
||||
<<endif>>
|
||||
```
|
||||
|
||||
**Stage7 双任务防重复:**
|
||||
|
||||
```yarn
|
||||
<<elseif $gameStage==7>>
|
||||
<<if $speakerFinalOK == true>>
|
||||
me: 已确认…若还有任务去查另一模块。
|
||||
<<else>>
|
||||
<<complete_task "...">>
|
||||
<<set $speakerFinalOK = true>>
|
||||
<<set $TaskToDo = $TaskToDo - 1>>
|
||||
<<endif>>
|
||||
<<endif>>
|
||||
```
|
||||
|
||||
## 6. 项目内参考
|
||||
|
||||
- 实例:`Assets/Resources/Yarn/FP_Huoshan1/Stage2.yarn`(`检查表达` / `检查销售模块` / `检查情绪`)、`Center.yarn`(`VarsInit`)。
|
||||
- 流程定义:`Docs/HuoShan/core/02_flow.md`。
|
||||
- C#:`Assets/Scripts/FixSystemNew/BodyModule/Modules/BodyModule.cs`(`PlugIn`)。
|
||||
+2
-1
@@ -75,7 +75,8 @@
|
||||
*.psd filter=lfs diff=lfs merge=lfs -text
|
||||
*.psb filter=lfs diff=lfs merge=lfs -text
|
||||
*.bank filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# DOTween editor images - exclude from LFS (stored as regular binaries, Unity re-import causes false changes)
|
||||
Assets/Plugins/Demigiant/DOTween/Editor/Imgs/*.png -filter -diff -merge
|
||||
Assets/Plugins/Demigiant/DOTween/Editor/Imgs/*.jpg -filter -diff -merge
|
||||
Assets/Plugins/Demigiant/DOTween/Editor/Imgs/*.jpg -filter -diff -merge
|
||||
|
||||
+7
-2
@@ -88,9 +88,12 @@ crashlytics-build.properties
|
||||
# Ignore the Cache folder since it is updated locally.
|
||||
/[Aa]ssets/Plugins/FMOD/Cache/*
|
||||
|
||||
# TimelineNameCollector editor cache (regenerated on scan)
|
||||
# TimelineNameCollector editor cache (legacy path; migration only)
|
||||
/[Aa]ssets/[Ee]ditor/TimelineNameCollector/Cache/*
|
||||
|
||||
# HandlerNameCollector editor cache (regenerated on scan)
|
||||
/[Aa]ssets/[Ee]ditor/HandlerNameCollector/Cache/*
|
||||
|
||||
# 暂时把bank放到Stream里,不忽略
|
||||
# Ignore bank files in the StreamingAssets folder.
|
||||
/[Aa]ssets/StreamingAssets/**/*.bank
|
||||
@@ -107,4 +110,6 @@ fmod_editor.log
|
||||
.history/*
|
||||
**/.history/*
|
||||
|
||||
.fake
|
||||
.fake
|
||||
Keystore/aibis-release-key.keystore
|
||||
Keystore/keystore.bat
|
||||
|
||||
Vendored
+65
-1
@@ -56,6 +56,70 @@
|
||||
"temp/": true,
|
||||
"Temp/": true
|
||||
},
|
||||
"files.associations": {
|
||||
"**/Plugins/Android/*Template.gradle": "plaintext"
|
||||
},
|
||||
"dotnet.defaultSolution": "aibis-dream.sln",
|
||||
"dotnet.preferCSharpExtension": true
|
||||
"dotnet.preferCSharpExtension": true,
|
||||
"cSpell.enableFiletypes": [
|
||||
"yarnspinner"
|
||||
],
|
||||
"cSpell.languageSettings": [
|
||||
{
|
||||
"languageId": "yarnspinner",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "yarn-commands",
|
||||
"pattern": "/<<[^>]*>>/g"
|
||||
},
|
||||
{
|
||||
"name": "yarn-expressions",
|
||||
"pattern": "/\\{[^}]*\\}/g"
|
||||
},
|
||||
{
|
||||
"name": "yarn-variables",
|
||||
"pattern": "/\\$[a-zA-Z_][a-zA-Z0-9_]*/g"
|
||||
},
|
||||
{
|
||||
"name": "yarn-markup",
|
||||
"pattern": "/\\[[^\\]]*\\]/g"
|
||||
},
|
||||
{
|
||||
"name": "yarn-hashtags",
|
||||
"pattern": "/#[a-zA-Z_][a-zA-Z0-9_:-]*/g"
|
||||
},
|
||||
{
|
||||
"name": "yarn-node-markers",
|
||||
"pattern": "/^(---|===)$/gm"
|
||||
},
|
||||
{
|
||||
"name": "yarn-headers",
|
||||
"pattern": "/^(title|tags|position|color|colorID|tracking|custom|style):.*/gm"
|
||||
},
|
||||
{
|
||||
"name": "yarn-character-names",
|
||||
"pattern": "/^\\s*[A-Za-z_][A-Za-z0-9_]*(?=:)/gm"
|
||||
},
|
||||
{
|
||||
"name": "yarn-comments",
|
||||
"pattern": "/\\/\\/.*$/gm"
|
||||
}
|
||||
],
|
||||
"ignoreRegExpList": [
|
||||
"yarn-commands",
|
||||
"yarn-expressions",
|
||||
"yarn-variables",
|
||||
"yarn-markup",
|
||||
"yarn-hashtags",
|
||||
"yarn-node-markers",
|
||||
"yarn-headers",
|
||||
"yarn-character-names",
|
||||
"yarn-comments"
|
||||
]
|
||||
}
|
||||
],
|
||||
"cSpell.words": [
|
||||
"Aibis",
|
||||
"Newtonsoft"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
AIBIS Dream (also known as "AllOurBrokenParts") is a Unity-based narrative adventure game with investigative elements. The project uses Yarn Spinner for dialogue, features a custom framework with modular systems, and includes multiple mini-games and a modular repair/fix system.
|
||||
|
||||
**Unity Version:** 2022.3.7f1c1 (Universal Render Pipeline)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Framework (Assets/Scripts/Framework/)
|
||||
|
||||
Modular framework with Kit-pattern utilities:
|
||||
|
||||
- **ActionKit**: Action sequencing and command pattern
|
||||
- **AudioKit**: Audio management (includes FMOD integration)
|
||||
- **Config / Core**: Configuration and core utilities
|
||||
- **EventSystemKit**: Decoupled event system
|
||||
- **LogKit**: Structured logging with categories
|
||||
- **PoolKit**: Object pooling
|
||||
- **ResourceKit**: Asset loading via `ResourceSystem` (Addressables-based)
|
||||
- **Note**: New code must use `ResourceSystem`, never `Resources.Load`. See [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md). Asset placement rules in [Resource Management Spec](Docs/ResourceManagementSpec.md).
|
||||
- **SelectorKit**: Weighted/batch/non-repeat random selection
|
||||
- **SingletonKit**: Singleton base classes
|
||||
- **StateMachineKit**: State management
|
||||
- **OtherKit**: Miscellaneous utilities (`CameraKit`, `AnimatorKit`, `FadeKit`, `LocalizationKit`, etc.)
|
||||
- **Attribute**: Custom editor attributes
|
||||
|
||||
### Key Game Systems
|
||||
|
||||
- **Game Loop** (Scripts/Game Loop/): `GameManager` (Singleton entry), `YarnVariableStorage` (Yarn variables), `SaveRestoreOrchestrator` / `SaveSystem` (save snapshots), `SceneLoader`, `ChapterController`, `TalkSceneSO`
|
||||
- **Dialog System** (Scripts/Dialog System/): Yarn Spinner integration — `DialogController`, `LineRunner`, `LocalisedLineProvider`, `BaseYarnCommand`
|
||||
- **SceneManagement** (Scripts/SceneManagement/): `ActorKit/`, `AnimatorKit/`, `EnvironmentKit/`, `SpawnerKit/`, `TimelineKit/`, `SceneCenter/`, `DreamDoorSystem`
|
||||
- **UI** (Scripts/UI/): `DialogUI/`, `Panel/`, `Form/`, `Components/`, `Cursor/`, `UIManager`
|
||||
- **MiniGame** (Scripts/MiniGame/): `HuoShan/` (language particles, EmotionWave), `BlockPuzzle/`, `Peipei/` (Eye, CutLine, Memory)
|
||||
- **FixSystem** (Scripts/FixSystem/): Legacy — HeatMap, PunchTape, Memory, screen effects
|
||||
- **FixSystemNew** (Scripts/FixSystemNew/): Active repair system — `FixSystemCenter`, `BodyModule/`, `Cable/`, `WhackMole/`, `Screen System/`, `Open/`, `Clinic/`
|
||||
- **AssetRefs** (Scripts/AssetRefs/): Addressable key constants (`ConstRef`, `FixAssetRef`, `TimelineAssetRef`)
|
||||
|
||||
### Content & Localization
|
||||
|
||||
- **Assets/GameContent/**: Runtime assets by scope (`Common/`, `UI/`, `Scene_{Name}/`, `Feature_{Name}/`)
|
||||
- **Assets/Language/**: Unity Localization tables (runtime active: `UIText/`, `Locales/`, `Sprite/`; `Dialog/`、`ActorName/`、`Params/` currently kept as migration backups)
|
||||
- **Assets/Resources/Yarn/**: Current Yarn dialogue files (target migration to `Assets/Narrative/` per spec, not yet complete)
|
||||
- **Assets/StreamingAssets/Config/**: CSV-driven localized gameplay data (`character.csv` for actor names, `params.csv` for `l10n.*` substitution terms)
|
||||
- **Assets/Prototype/**: HTML/JS prototypes for mechanic testing
|
||||
|
||||
## Critical Dependencies
|
||||
|
||||
### Unity Packages
|
||||
- **Yarn Spinner** 3.2.2 (OpenUPM), **Addressables** 1.21.21, **Cinemachine** 2.9.7
|
||||
- **URP** 14.0.8, **TextMesh Pro** 3.0.6, **Timeline** 1.7.5, **Localization** 1.4.5
|
||||
- **Visual Effect Graph** 14.0.8, **Visual Scripting** 1.8.0
|
||||
|
||||
### Third-Party
|
||||
- **FMOD** (audio engine), **DOTween** (tweening), **Shapes** (vector graphics)
|
||||
- **More Mountains Feedbacks** (game feel), **Destructible 2D**
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Code Organization
|
||||
1. Each major system self-contained in its own folder under `Assets/Scripts/`
|
||||
2. Kit-pattern utilities in `Framework/` — reusable across systems
|
||||
3. Singleton pattern for global managers (derived from `SingletonKit`)
|
||||
4. Favor composition over inheritance
|
||||
5. All runtime asset loading via `ResourceSystem` — never `Resources.Load`
|
||||
|
||||
### Scene Structure
|
||||
- Scenes in `Assets/Scenes/`; `Persistence` scene hosts global managers and Persistent Loader
|
||||
- Fix scenes: `{Character}FixScene` (e.g. `HuoShanFixScene`, `PeipeiFixScene`, `ExpressFixScene`)
|
||||
- Test scenes: `languageTest`, `HuoShanExpressionTest`, `wavetest`, etc.
|
||||
|
||||
### Addressables
|
||||
- Config in `Assets/AddressableAssetsData/`; Groups: `Core`, `Scene_*`, `Feature_*`, `Shared_*`, `Localization-*`
|
||||
- Key format: `Category/Path` (e.g. `Animation/Peipei`, `Timeline/BlockPuzzle/PanelOpen`)
|
||||
- Scene keys use bare PascalCase names (e.g. `ClinicOut`)
|
||||
- Details in [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md)
|
||||
|
||||
### Yarn Dialogue Files
|
||||
- Current path: `Assets/Resources/Yarn/` (migration to `Assets/Narrative/Yarn/` planned)
|
||||
- Pattern: `{Character}_{Stage}.yarn`; UTF-8 encoding required for Chinese
|
||||
|
||||
#### Yarn Line Tag Maintenance
|
||||
- **Line tags** (`#line:xxxxxxx`) must be unique across the entire file. Duplicate tags break Yarn Spinner localization.
|
||||
- Duplicates typically occur when copying an existing node to create a variant (e.g. `地铁ver1` → `地铁ver3`).
|
||||
- **Use the dedup tool** (`Tools/dedup_yarn_tags.py`) for bulk cleanup:
|
||||
```bash
|
||||
# Preview duplicates from a specific node onward
|
||||
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/Fiction_Day1_begin/Fiction_Day1_begin.yarn --from-node 地铁ver4
|
||||
# Fix duplicates from a specific line onward
|
||||
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/Fiction_Day1_begin/Fiction_Day1_begin.yarn --from-line 535 --fix
|
||||
# Scan an entire directory (preview only)
|
||||
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/ --scan-all
|
||||
```
|
||||
|
||||
#### Yarn Script Generation
|
||||
- **Do NOT add `#line:` tags when generating or editing Yarn dialogue.** Leave all line-tag generation to the user's localization workflow (Unity Localization + Yarn Spinner import tools).
|
||||
- When creating new `.yarn` files, write plain dialogue lines without any `#line:` suffixes.
|
||||
|
||||
### Asset Naming
|
||||
- **Scripts**: PascalCase (e.g. `DialogController.cs`)
|
||||
- **Addressable Keys**: `Category/Path` — no spaces, no extensions, ≤3 levels
|
||||
- **Addressable Groups**: `{Type}_{PascalCaseName}`
|
||||
|
||||
### Git & Commits
|
||||
- LFS for large binaries; ignored: Library/, Temp/, Build/, *.csproj, *.sln
|
||||
- **Commit format**: `<type>(<scope>): <subject>` — subject in Chinese, type/scope in English
|
||||
- Extended types: `feat`, `fix`, `art`, `audio`, `scene`, `yarn`, `perf`, `refactor`, `style`, `docs`, `build`, `chore`, `test`
|
||||
- See [COMMIT_CONVENTION.md](COMMIT_CONVENTION.md) for full details
|
||||
|
||||
## Key Build Commands
|
||||
|
||||
```bash
|
||||
# Windows build
|
||||
"C:\Program Files\Unity\Hub\Editor\2022.3.7f1c1\Editor\Unity.exe" -batchmode -quit -projectPath . -buildWindows64Player "Build/AIBIS_Dream.exe" -logFile Build/log.txt
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Mini-Game
|
||||
1. Create `Assets/Scripts/MiniGame/{GameName}/`
|
||||
2. Add assets to `Assets/GameContent/Feature_{GameName}/`
|
||||
3. Create Addressable Group `Feature_{GameName}`
|
||||
4. Create test scene in `Assets/Scenes/`
|
||||
|
||||
### Adding Yarn Dialogue
|
||||
1. Create `.yarn` in `Assets/Resources/Yarn/{Module}/`
|
||||
2. Ensure YarnProject localization assets/CSV are updated (Unity `Assets/Language/Dialog/` is backup-only during migration)
|
||||
3. Reference in scene's `DialogueRunner`; test with `DialogController`
|
||||
|
||||
## Debugging & Performance
|
||||
- `LogKit` for structured logging; Addressables Event Viewer for resource debugging
|
||||
- Monitor `SceneResourceLoader.TotalHandleCount` for leak detection
|
||||
- Always use PoolKit for frequent instantiation; `LoadUncachedAsync` + `EarlyRelease` for large Timeline assets
|
||||
- Use `LoadAsync<GameObject>` + `Instantiate` for prefabs, never `InstantiateAsync`
|
||||
|
||||
## Important Notes
|
||||
- Narrative-focused game — dialogue and story take precedence
|
||||
- `FixSystemNew` is the active repair architecture; `FixSystem` is legacy
|
||||
- `YarnVariableStorage` holds Yarn runtime variables; save/load flows through `SaveRestoreOrchestrator` and `SaveSystem`
|
||||
|
||||
## Reference Documents
|
||||
- [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md) — `ResourceSystem` API, Key/Group/Label naming, lifecycle
|
||||
- [Resource Management Spec](Docs/ResourceManagementSpec.md) — Asset placement: `GameContent/`, `Narrative/`, `_Archive/`
|
||||
- [Commit Convention](COMMIT_CONVENTION.md) — Commit message format and branch naming
|
||||
- [HuoShan Docs](Docs/HuoShan/) — Narrative flow, resources, test flowcharts
|
||||
|
||||
---
|
||||
|
||||
*This document provides guidance for Codex when working with the AIBIS Dream codebase.*
|
||||
@@ -15,7 +15,7 @@ MonoBehaviour:
|
||||
m_DefaultGroup: 09546aca93a801441859af2a811fe53b
|
||||
m_currentHash:
|
||||
serializedVersion: 2
|
||||
Hash: c7686f2c2462e1f187d9ecf6d11dbc45
|
||||
Hash: cf6c8ca853b0b2e0e22d46c14944816d
|
||||
m_OptimizeCatalogSize: 0
|
||||
m_BuildRemoteCatalog: 0
|
||||
m_BundleLocalCatalog: 0
|
||||
@@ -59,13 +59,15 @@ MonoBehaviour:
|
||||
- {fileID: 11400000, guid: 6d259d339c6615248b893837398c1438, type: 2}
|
||||
- {fileID: 11400000, guid: 8718a71c99923e247a960cbf4e4795f5, type: 2}
|
||||
- {fileID: 11400000, guid: 1e521cfc13b314642b1e682e3be7ea17, type: 2}
|
||||
- {fileID: 11400000, guid: aafc8258a2111e94d8339a3152cf50e0, type: 2}
|
||||
- {fileID: 11400000, guid: 7e4ab0de168458b43a21dc2f9e811ff5, type: 2}
|
||||
- {fileID: 11400000, guid: 724eed66b2d8eef4faf77a4bee4d2845, type: 2}
|
||||
- {fileID: 11400000, guid: 62a92c74b9bd5bd439690727c1831500, type: 2}
|
||||
- {fileID: 11400000, guid: b593dc98ea11a484995123de77d9bf1a, type: 2}
|
||||
- {fileID: 11400000, guid: d582f9080605ab34cbcde3249c980030, type: 2}
|
||||
- {fileID: 11400000, guid: 54799118a585dda439cab043d56ec68c, type: 2}
|
||||
- {fileID: 11400000, guid: dc8d31ed80ea35945812ff6552efbbe5, type: 2}
|
||||
- {fileID: 11400000, guid: af94af36950b08d4f9a5037546bbe040, type: 2}
|
||||
- {fileID: 11400000, guid: 60fd2fd8c7094b242b01c63de65de9ea, type: 2}
|
||||
- {fileID: 11400000, guid: 7e4ab0de168458b43a21dc2f9e811ff5, type: 2}
|
||||
m_BuildSettings:
|
||||
m_CompileScriptsInVirtualMode: 0
|
||||
m_CleanupStreamingAssetsAfterBuilds: 1
|
||||
@@ -112,6 +114,7 @@ MonoBehaviour:
|
||||
- Locale-en
|
||||
- Locale-zh-Hans
|
||||
- SceneSO
|
||||
- Locale-ja-JP
|
||||
m_SchemaTemplates: []
|
||||
m_GroupTemplateObjects:
|
||||
- {fileID: 11400000, guid: 0c158052680a3e94184c450d00291514, type: 2}
|
||||
|
||||
@@ -28,6 +28,16 @@ MonoBehaviour:
|
||||
m_SerializedLabels:
|
||||
- SceneSO
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 9e771f24d61d40d42b7e2b4637e8090c
|
||||
m_Address: "Timeline/\u5934\u75DB"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: f80e799655e3ece439f31458425a752b
|
||||
m_Address: Timeline/Glitch
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 0
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
|
||||
m_Name: Localization-Asset-Tables-Japanese (Japan) (ja-JP)
|
||||
m_EditorClassIdentifier:
|
||||
m_GroupName: Localization-Asset-Tables-Japanese (Japan) (ja-JP)
|
||||
m_Data:
|
||||
m_SerializedData: []
|
||||
m_GUID: bac626c44cee44544a45a750681e92e0
|
||||
m_SerializeEntries:
|
||||
- m_GUID: 8b2d41616f1a75840a5800ee28d1c4ed
|
||||
m_Address: sprite_ja-JP
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-ja-JP
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 1
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
m_Schemas:
|
||||
- {fileID: 11400000, guid: 842c6a62aa3abdf4d9b230f249b9fd76, type: 2}
|
||||
- {fileID: 11400000, guid: 1706e28ae8e595149b2ac1fbea7f8071, type: 2}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af94af36950b08d4f9a5037546bbe040
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -15,17 +15,11 @@ MonoBehaviour:
|
||||
m_GroupName: Localization-Assets-English (en)
|
||||
m_Data:
|
||||
m_SerializedData: []
|
||||
m_GUID: 50aef5699023b21459e68677a9449d80
|
||||
m_SerializeEntries:
|
||||
- m_GUID: 032b1b5e09139ae45b6de51d35baae79
|
||||
m_Address: discord-square-color-icon
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_GUID: 59bf392f3aef3b24da46dd8d3fa9259f
|
||||
m_SerializeEntries: []
|
||||
m_ReadOnly: 1
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
m_Schemas:
|
||||
- {fileID: 11400000, guid: 895c7c97ef5802446a395377ef704940, type: 2}
|
||||
- {fileID: 11400000, guid: 8004f70cd10ce4a439cb9e822bb078c2, type: 2}
|
||||
- {fileID: 11400000, guid: fa74e99b946159549971be154369b2cb, type: 2}
|
||||
- {fileID: 11400000, guid: 8e1cbbf76f0150145a39ce648fbf3985, type: 2}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aafc8258a2111e94d8339a3152cf50e0
|
||||
guid: 60fd2fd8c7094b242b01c63de65de9ea
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
|
||||
@@ -22,145 +22,6 @@ MonoBehaviour:
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 37c291d6dce732d45afb2e2b62218683
|
||||
m_Address: Assets/Language/Dialog/FP_Day1_begin/FP_Day1_begin Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: c1de143c98cd0804ab3b7b9a36fcf06a
|
||||
m_Address: Assets/Language/ActorName/ActorName Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 76902d55aa2d3474aa96c8e574b8212f
|
||||
m_Address: Assets/Language/Dialog/FP_Peipei1/FP_Peipei1 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0cb567219be64c04b9552ecc61f8396a
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day1_night/Fiction_Day1_night Shared
|
||||
Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 685539ac5030ab5409faa8feccba91e2
|
||||
m_Address: Assets/Language/Dialog/FP_Day1_mid/FP_Day1_mid Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 4f3df7a54674e8343b4699544a25db67
|
||||
m_Address: Assets/Language/Dialog/FP_Shitou2/FP_Shitou2 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 2c7f3692831aa5f4daa13d00e99ad77c
|
||||
m_Address: Assets/Language/Dialog/FP_Shitou1/FP_Shitou1 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 949f8d1a26133a94c83ac9cc7e6d94c7
|
||||
m_Address: Assets/Language/Dialog/FP_Peipei2/FP_Peipei2 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 59e2a0d8fc60a784993a6f6cdfeb9c7d
|
||||
m_Address: Assets/Language/Dialog/FP_Day1_night/FP_Day1_night Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 831d81945a7dc8d498ccebcdd4d05044
|
||||
m_Address: Assets/Language/Dialog/FP_Day2_mid/FP_Day2_mid Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 45899a60a64d58a4f81fad32d2171702
|
||||
m_Address: Assets/Language/Dialog/FP_Day2_night/New Table Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: ea388b59e37ddc14da4d8e3e9ead4133
|
||||
m_Address: Assets/Language/Dialog/Test_Huoshan/Test_Huoshan Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 8f3dfd038e75c3a41a865ab8dd730809
|
||||
m_Address: Assets/Language/Dialog/FP_Prologue/FP_Prologue Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 011b21a8c75449e4a982549cebc1a6ca
|
||||
m_Address: Assets/Language/Params/Params Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: f6d220fec7b888b4f89463b5da038e62
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day1_mid/Fiction_Day1_mid Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: c5f887d31e1d7bc46b8a7ab784f19b9a
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day1_begin/Fiction_Day1_begin Shared
|
||||
Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 578cb0ae76a180d45ac70e058c676ac7
|
||||
m_Address: Assets/Language/Dialog/FP_Peipei3/FP_Peipei3 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: f5108536d82f2c14a922f0a1d3e83638
|
||||
m_Address: Assets/Language/Dialog/FP_Day3_mid/FP_Day3_mid Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0ee8d3542a2395745b4ad6bf55e3b097
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day2_mid/Fiction_Day2_mid Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 771ed3ef48efbb645827ce5c29d63754
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day2_night/Fiction_Day2_night Shared
|
||||
Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: aaa27d8b7dfc3fc4294bda026ae135f2
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day3_mid/Fiction_Day3_mid Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 3c243fd229013b84e9c88091eaa74103
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day3_night/Fiction_Day3_night Shared
|
||||
Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: bb8e2763fb266ff4d8b9c0b0f37882e2
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day4_mid/Fiction_Day4_mid Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: bb6fa95706f665c469e270567be5cfe9
|
||||
m_Address: Assets/Language/Dialog/Fiction_Huoshan1/New Table Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 042ab1d25056cbd4984621037ac848f0
|
||||
m_Address: Assets/Language/Dialog/Fiction_Yingli1/Fiction_Yingli1 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 70f8ff033cddcd543a618974781d0bb0
|
||||
m_Address: Assets/Language/Dialog/FP_Day3_night/FP_Day3_night Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 08cb937dd06038b4486d6a3662539542
|
||||
m_Address: Assets/Language/Dialog/Test_GeneralEnd/New Table Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 9abdd56d318b1ec459c7e866748d6878
|
||||
m_Address: Assets/Language/Sprite/sprite Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
@@ -172,47 +33,22 @@ MonoBehaviour:
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
- Locale-zh-Hans
|
||||
- Locale-ja-JP
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: dd4a0fe1d258fbc4384ab1db825d5ae3
|
||||
m_Address: Gts1/Fiction_Gts1 Shared Data.asset/Fiction_Gts1 Shared Data.asset
|
||||
- m_GUID: 4a2d391471bd9cd439150c52659e9846
|
||||
m_Address: Assets/Language/Params/Params Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 57cfa4d941133b840b4345cf815dda66
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day2_begin/Fiction_Day2_begin Shared
|
||||
Data.asset
|
||||
- m_GUID: 032b1b5e09139ae45b6de51d35baae79
|
||||
m_Address: discord-square-color-icon
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
- Locale-ja-JP
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: c3d23bd971e02d34cb356b2029523427
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day1_sleep/Fiction_Day1_sleep Shared
|
||||
Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: e3c75f951d9792449906a3a8010d2e74
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day2_sleep/Fiction_Day2_sleep Shared
|
||||
Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 28396c5e50d205645b94b89018d01a2a
|
||||
m_Address: Assets/Language/Dialog/Ficion_Shitou1/Fiction_Shitou1 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 73c880a15817b7f408ad0596f07a3626
|
||||
m_Address: Assets/Language/Dialog/FP_Day1_sleep/FP_Day1_sleep Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: afa9b26bd11b9d847bbb9714b8049a4b
|
||||
m_Address: Assets/Language/Dialog/FP_Day2_begin/FP_Day2_begin Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 90d6cbb308df1484f9146a0ad156202e
|
||||
m_Address: Assets/Language/Dialog/FP_Day2_sleep/FP_Day2_sleep Shared Data.asset
|
||||
- m_GUID: 7ea8c68fdc624de4c86434963c06ec55
|
||||
m_Address: Assets/Language/ChapterInfo/ChapterInfo Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
|
||||
@@ -29,6 +29,12 @@ MonoBehaviour:
|
||||
m_SerializedLabels:
|
||||
- Locale
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 9d3da569ceb2f3740bd6f4fd01e51c62
|
||||
m_Address: Japanese (Japan) (ja-JP)
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 1
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
|
||||
+3
-195
@@ -23,206 +23,14 @@ MonoBehaviour:
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: c21a8fbe9bf147649b5b01320ada3f11
|
||||
m_Address: FP_Day1_begin_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0b504f7f6402d8747896b0cf19674e69
|
||||
m_Address: ActorName_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 247e0ac5e48cbe24ea7e228e1a039590
|
||||
m_Address: FP_Peipei1_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: d6149a0fedb6cc24ba2a318aedab1de0
|
||||
m_Address: FP_Day1_mid_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 8a56c6bff47205b439b07425f6f5ad94
|
||||
m_Address: FP_Shitou2_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 32954c9c2dce35e42a31befcec1e861f
|
||||
m_Address: FP_Shitou1_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 860bd75190298994b9d5dee30aebd910
|
||||
m_Address: FP_Peipei2_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: bfc0211d0386bc9468ecace61129ecd5
|
||||
m_Address: FP_Day1_night_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 204ffaeec9d0f7e4cb708476b4d459a2
|
||||
m_Address: FP_Day2_mid_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 2516ada36f949234caf993f1deaf79e6
|
||||
m_Address: FP_Day2_night_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 523b3962bff363849a36683b375adc06
|
||||
m_Address: Test_Huoshan_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 83f5756a759af6f459aa1871eb248f5b
|
||||
m_Address: FP_Prologue_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 5d4915d0761772c41902e704e2ecfd0b
|
||||
- m_GUID: aeb318c895d57014b89d5540c6baff95
|
||||
m_Address: Params_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: fb51befc2226dfa44bbde7203364c019
|
||||
m_Address: Fiction_Day2_mid_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 6603078d8eb41524e9ad2edd5d182ccc
|
||||
m_Address: Fiction_Day1_mid_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 72755a9b5a5ca5c4e92028ca44c66a6d
|
||||
m_Address: FP_Peipei3_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: c260e1e2604154648971c06353edad0e
|
||||
m_Address: FP_Day3_mid_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: a75bb4d4a30c22f40ad5c7d70c8276d4
|
||||
m_Address: Fiction_Day1_night_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: b2f0a2aa5e0b95346bf499980c2994d4
|
||||
m_Address: Fiction_Day2_night_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 6c498406bf979ca45b345e5bdbd3cc4b
|
||||
m_Address: Fiction_Day3_mid_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 8e5812f1d9f10a4488702f35405d197d
|
||||
m_Address: Fiction_Day3_night_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 35f017da97194574da316391464204b2
|
||||
m_Address: Fiction_Day4_mid_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 75ed4030af85d794f832919dbe427f51
|
||||
m_Address: Fiction_Huoshan1_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 4b181f70ec2e65f4cab1545662415306
|
||||
m_Address: Fiction_Yingli1_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: a525b6d2bcfeb2446b2f4d2b36b043c7
|
||||
m_Address: FP_Day3_night_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 1f031d78df49e5645a4ceaafa7283f91
|
||||
m_Address: Test_GeneralEnd_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 9cecc6d4b999d5c489b69406176bab99
|
||||
m_Address: Fiction_Gts1_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: fbfb0ced2ba1bf44e86964158ae53d72
|
||||
m_Address: Fiction_Day1_sleep_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 999ab1a8772cb1a4cba0325144ff5488
|
||||
m_Address: Fiction_Day1_begin_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 4d88b47f4a7e3f249b44203f411e4adf
|
||||
m_Address: Fiction_Day2_sleep_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 03f5785bc02343d448436956c6991b38
|
||||
m_Address: Fiction_Shitou1_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 14a941304f21e2042afd56019acfca6c
|
||||
m_Address: FP_Day1_sleep_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: d9f05be3049f65b4c82800b1bde3b7c2
|
||||
m_Address: FP_Day2_begin_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 09898f26fbea1454cb10f2c27e2ca907
|
||||
m_Address: FP_Day2_sleep_zh-Hans
|
||||
- m_GUID: bdf8ad79a9f707b45805e23c25ccdadb
|
||||
m_Address: ChapterInfo_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
|
||||
+3
-195
@@ -23,206 +23,14 @@ MonoBehaviour:
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 1e7115110ec21ec4da592999daa0c146
|
||||
m_Address: FP_Day1_begin_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: e80667e8ff7367e4ab9bc4f9421271e8
|
||||
m_Address: ActorName_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 08ef2a7dddda7ee49b869bcd73858404
|
||||
m_Address: FP_Peipei1_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 30ab319b5e0bafe46aa622bb93381c7e
|
||||
m_Address: FP_Day1_mid_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 927a25bda151fc7469749a3724bef6b0
|
||||
m_Address: FP_Shitou2_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: fda6eb219437ffc40ab860aa89d9446d
|
||||
m_Address: FP_Shitou1_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 13f447125bbae5e45a4a446c2c9ac397
|
||||
m_Address: FP_Peipei2_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: c65e073d94ffbbb4b9b6107dee05e670
|
||||
m_Address: FP_Day1_night_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 399374eeba6905048a7342d0bb494f9c
|
||||
m_Address: FP_Day2_mid_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: cbc3925d28fc66c4cb3293b7bd66db0b
|
||||
m_Address: FP_Day2_night_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: fb0b8f197b5a607468eeb9326737d547
|
||||
m_Address: Test_Huoshan_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 49a6e4d039261bf42a18782ca317fca5
|
||||
m_Address: FP_Prologue_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0a9b8bbf38d109d409792ae7cc48cec7
|
||||
- m_GUID: 9248cb179e7aafa4c903f1fc54b41ef7
|
||||
m_Address: Params_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: eb9ebf027dd4b564cb035d7167ff8099
|
||||
m_Address: Fiction_Day2_mid_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 705a631ebd09db04fa1bda39e20e505e
|
||||
m_Address: Fiction_Day1_mid_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: fde90ade684482142af49533e4f4ed62
|
||||
m_Address: FP_Peipei3_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 036c8c6ed1ecc314a8e0ae02fe15ab5e
|
||||
m_Address: FP_Day3_mid_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0d2928a7d58c26a4ca78abf2f5da1373
|
||||
m_Address: Fiction_Day1_night_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 59fc388134f5d884c8d7ca28f2d7017e
|
||||
m_Address: Fiction_Day2_night_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 379ead226bea4f844b1c136c4f748633
|
||||
m_Address: Fiction_Day3_mid_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: aaf54fb66c2fa9d4ab68626ced632a7a
|
||||
m_Address: Fiction_Day3_night_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: c0c8b8aea11da444ea41eece5a698e5f
|
||||
m_Address: Fiction_Day4_mid_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 2a14b1d82f4aef94aac2b230392605fc
|
||||
m_Address: Fiction_Huoshan1_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 79ccbc35144b91a45bb9fdf9732d238a
|
||||
m_Address: Fiction_Yingli1_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0e9bb0974017aa346a379b78126f6f7b
|
||||
m_Address: FP_Day3_night_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 27ad0341432ae0b46a8fe117eb5c73e2
|
||||
m_Address: Test_GeneralEnd_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 1adbe96b15171134584c9a2d7d69c045
|
||||
m_Address: Fiction_Gts1_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 9d72f86c018628e418747a7d0a66cb21
|
||||
m_Address: Fiction_Day1_sleep_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 30a0c1ae8c45da34f8e3f90692e96e11
|
||||
m_Address: Fiction_Day1_begin_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 36d94b46b4a12b54b8e084d8d275e675
|
||||
m_Address: Fiction_Day2_sleep_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 265c923a085f98042b44f8b18dd8389b
|
||||
m_Address: Fiction_Shitou1_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: ef76bcd9daf9b6a4580eaa28d2073ebf
|
||||
m_Address: FP_Day1_sleep_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: a6b685d3b3e17c64da7ca72add7b3540
|
||||
m_Address: FP_Day2_begin_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: a0e634b20b52dd34abb7999e889ec772
|
||||
m_Address: FP_Day2_sleep_en
|
||||
- m_GUID: 8906df9235e52fb41b11fe8776a77032
|
||||
m_Address: ChapterInfo_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
|
||||
m_Name: Localization-String-Tables-Japanese (Japan) (ja-JP)
|
||||
m_EditorClassIdentifier:
|
||||
m_GroupName: Localization-String-Tables-Japanese (Japan) (ja-JP)
|
||||
m_Data:
|
||||
m_SerializedData: []
|
||||
m_GUID: 62cf9265bfa710b4b9152c336336a38e
|
||||
m_SerializeEntries:
|
||||
- m_GUID: 7c757424270c91e43bf45198f69c983a
|
||||
m_Address: UIText_ja-JP
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-ja-JP
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 9ed23d1d910a1a147ba5c316a87cbad4
|
||||
m_Address: Params_ja-JP
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-ja-JP
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 9d8accfdeabc8034c986233920a17184
|
||||
m_Address: ChapterInfo_ja-JP
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-ja-JP
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 1
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
m_Schemas:
|
||||
- {fileID: 11400000, guid: 7df396a6b5136fe4e9d2d659731161d2, type: 2}
|
||||
- {fileID: 11400000, guid: 545ed8780a6508047bd42b964feb82fd, type: 2}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc8d31ed80ea35945812ff6552efbbe5
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -37,13 +37,23 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 7ba8eca4e18291341a10ef7c8c858bd9
|
||||
m_Address: "Animation/\u9152\u5427\u533B\u751F"
|
||||
- m_GUID: 5f26e5dcbc809104f92f908082de97d5
|
||||
m_Address: "Animation/\u8C03\u9152\u59B9\u6D4B\u8BD5"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 5f26e5dcbc809104f92f908082de97d5
|
||||
m_Address: "Animation/\u8C03\u9152\u59B9\u6D4B\u8BD5"
|
||||
- m_GUID: a74e4379fa468c24497808e8a6bae2c4
|
||||
m_Address: "Animation/\u9152\u4FDD"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 1c3c562754993f64c850ae97b4c7852e
|
||||
m_Address: "Timeline/\u9189\u9152"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 2d9baefeca916a942ab57a6714f7dc9e
|
||||
m_Address: "Timeline/\u8C03\u9152\u8499\u592A\u5947"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
|
||||
@@ -22,16 +22,41 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0252f0740dd08da4ab8f5aeb7ddcc4ea
|
||||
- m_GUID: 5253d74898c86f547861ebf57b280b4f
|
||||
m_Address: "Timeline/\u5929\u6865\u5934\u75DB"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: acd7f1995fefdf24c9e979e45a176b76
|
||||
m_Address: "Timeline/\u5929\u6865\u98CE\u5439\u6811\u53F6"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 4180297bbe280ad478f49933e69dcbce
|
||||
m_Address: "Timeline/\u5929\u6865\u63A5\u6811\u53F6In"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 71aa8e39cf2f39945af8ba27eb2acd08
|
||||
m_Address: "Timeline/\u5929\u6865\u63A5\u6811\u53F6Loop"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: e67da195744f8de448e46c34eb9badb2
|
||||
m_Address: "Animation/\u5929\u6865\u533B\u751F"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 2359eeb42b1a4f044b01b6ea27390f1d
|
||||
- m_GUID: ea008b0c912c0d748997bf458879cb74
|
||||
m_Address: "Animation/\u5929\u6865\u4F69\u4F69"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: f19cff809e7020b41ba8ad7da199db86
|
||||
m_Address: "Timeline/day2\u5915\u9633\u533B\u751F\u770B\u4F69\u4F69\u4FA7\u8138"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 0
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
|
||||
@@ -67,48 +67,23 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 69e2f5249baf3a94f8f84e0d141868cb
|
||||
m_Address: "Timeline/\u6458\u8D70\u76D6\u5B50"
|
||||
- m_GUID: 6c4aea2a4a8462344b6ce353f53c7b2a
|
||||
m_Address: "Timeline/\u533B\u751F\u51FA\u5730\u94C1"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 4222e9e117a86064192b7219f7ce0543
|
||||
m_Address: "Timeline/\u653E\u4E0B\u76D6\u5B50"
|
||||
- m_GUID: 7760fc67933fb684d81ea5275fac0a50
|
||||
m_Address: "Animation/\u8BCA\u5BA4\u5916\u706B\u5C71"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 72b922f30ca3059419c6bbc565ed9b4a
|
||||
m_Address: "Timeline/\u900F\u955Cin"
|
||||
- m_GUID: 253f47db959b2814998695fb5d1f4d97
|
||||
m_Address: "Timeline/\u65CB\u8F6C\u53F6\u67C4"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: e4815dc72e9f2a54d93eaabda8a0750f
|
||||
m_Address: "Timeline/\u900F\u955Cout"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0a677c8d19d799d4a80842a5ee1bf5a4
|
||||
m_Address: "Timeline/\u8FDB\u5165\u6B65\u8FDB\u6A21\u5F0F"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 33a3ab569b0312145ad062ae6936177e
|
||||
m_Address: "Timeline/\u9000\u51FA\u6B65\u8FDB\u6A21\u5F0F"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 838bd5cc2ceffc1459d167675d96f66a
|
||||
m_Address: "Timeline/\u706B\u5C71\u8868\u8FBEpanel"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 45c7520cc9a3e5f4faa8db45df605dc1
|
||||
m_Address: "Timeline/\u706B\u5C71\u8868\u8FBE\u786E\u8BA4"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 3b2268ceeee9f934691265d0214b2fc7
|
||||
m_Address: "Timeline/\u706B\u5C71\u8868\u8FBE\u6DF1\u5165"
|
||||
- m_GUID: 1799dbcdb4a76624bb89622707581f92
|
||||
m_Address: "Timeline/\u82F1\u91CC\u521D\u767B\u573A"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
|
||||
@@ -27,6 +27,11 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 35dfcf569b00d404bb89ac1d8ae4c37a
|
||||
m_Address: "Timeline/\u59D0\u59D0\u653E\u624B"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 0
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
|
||||
@@ -32,6 +32,71 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 417a7984630786f46a5ade5d2b457852
|
||||
m_Address: "Animation/\u706B\u5C71"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 838bd5cc2ceffc1459d167675d96f66a
|
||||
m_Address: "Timeline/\u706B\u5C71\u8868\u8FBEpanel"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 45c7520cc9a3e5f4faa8db45df605dc1
|
||||
m_Address: "Timeline/\u706B\u5C71\u8868\u8FBE\u786E\u8BA4"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 3b2268ceeee9f934691265d0214b2fc7
|
||||
m_Address: "Timeline/\u706B\u5C71\u8868\u8FBE\u6DF1\u5165"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 4222e9e117a86064192b7219f7ce0543
|
||||
m_Address: "Timeline/\u653E\u4E0B\u76D6\u5B50"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 69e2f5249baf3a94f8f84e0d141868cb
|
||||
m_Address: "Timeline/\u6458\u8D70\u76D6\u5B50"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: e4815dc72e9f2a54d93eaabda8a0750f
|
||||
m_Address: "Timeline/\u900F\u955Cout"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 72b922f30ca3059419c6bbc565ed9b4a
|
||||
m_Address: "Timeline/\u900F\u955Cin"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 33a3ab569b0312145ad062ae6936177e
|
||||
m_Address: "Timeline/\u9000\u51FA\u6B65\u8FDB\u6A21\u5F0F"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0a677c8d19d799d4a80842a5ee1bf5a4
|
||||
m_Address: "Timeline/\u8FDB\u5165\u6B65\u8FDB\u6A21\u5F0F"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: af4651ca096e81d4d8bcf8b93a91b872
|
||||
m_Address: "Timeline/\u706B\u5C71\u5934\u75DB2"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: e69af1ed498c57f4799f936aa28e4ed4
|
||||
m_Address: "Timeline/\u706B\u5C71\u5934\u75DB1"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: efa335dcb6e706347bd4acf3222331b9
|
||||
m_Address: "FrameAnimation/\u706B\u5C71"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 0
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
|
||||
@@ -27,13 +27,18 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: d269076a4860dee4f94d1bd63c674f30
|
||||
m_Address: "Sprite/\u89C6\u89C9\u6A21\u5757\u76D6\u5B50"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 809f95af8dd580448ab804b94fbd9cb2
|
||||
m_Address: "Animation/\u4F69\u4F69"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: d269076a4860dee4f94d1bd63c674f30
|
||||
m_Address: "Sprite/\u89C6\u89C9\u6A21\u5757\u76D6\u5B50"
|
||||
- m_GUID: 26105571c9644aa4194f2b981ee33a58
|
||||
m_Address: "FrameAnimation/\u4F69\u4F69"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
|
||||
@@ -17,16 +17,6 @@ MonoBehaviour:
|
||||
m_SerializedData: []
|
||||
m_GUID: 74922ad77455a1847aa3f1d714a4edda
|
||||
m_SerializeEntries:
|
||||
- m_GUID: 3c0d119c1a62bb840846a0e7c58197f7
|
||||
m_Address: "Subway/\u767D\u5929\u7A97\u5916"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 660c87b6732769b49a9b7119b54ede83
|
||||
m_Address: "Subway/\u591C\u665A\u7A97\u5916"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 1df1eddcbfb714743a3d146cbea2b6a2
|
||||
m_Address: Scene/SubWay
|
||||
m_ReadOnly: 0
|
||||
@@ -52,6 +42,21 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: ca80fd96dc71f714a988b27d9ca6c61c
|
||||
m_Address: Sprite/News
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 3c0d119c1a62bb840846a0e7c58197f7
|
||||
m_Address: "Subway/\u767D\u5929\u7A97\u5916"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 660c87b6732769b49a9b7119b54ede83
|
||||
m_Address: "Subway/\u591C\u665A\u7A97\u5916"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 0
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
|
||||
m_Name: Localization-Asset-Tables-Japanese (Japan) (ja-JP)_BundledAssetGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: af94af36950b08d4f9a5037546bbe040, type: 2}
|
||||
m_InternalBundleIdMode: 1
|
||||
m_Compression: 1
|
||||
m_IncludeAddressInCatalog: 1
|
||||
m_IncludeGUIDInCatalog: 1
|
||||
m_IncludeLabelsInCatalog: 1
|
||||
m_InternalIdNamingMode: 0
|
||||
m_CacheClearBehavior: 0
|
||||
m_IncludeInBuild: 1
|
||||
m_BundledAssetProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
|
||||
m_ForceUniqueProvider: 0
|
||||
m_UseAssetBundleCache: 1
|
||||
m_UseAssetBundleCrc: 1
|
||||
m_UseAssetBundleCrcForCachedBundles: 1
|
||||
m_UseUWRForLocalBundles: 0
|
||||
m_Timeout: 0
|
||||
m_ChunkedTransfer: 0
|
||||
m_RedirectLimit: -1
|
||||
m_RetryCount: 0
|
||||
m_BuildPath:
|
||||
m_Id: 2ae6a7549097a9b4caefe44b61b1fcc5
|
||||
m_LoadPath:
|
||||
m_Id: 631ec5c677ab8084fb9be9b9ee39c6ef
|
||||
m_BundleMode: 0
|
||||
m_AssetBundleProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
|
||||
m_BundleNaming: 1
|
||||
m_AssetLoadMode: 0
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 842c6a62aa3abdf4d9b230f249b9fd76
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
|
||||
m_Name: Localization-Asset-Tables-Japanese (Japan) (ja-JP)_ContentUpdateGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: af94af36950b08d4f9a5037546bbe040, type: 2}
|
||||
m_StaticContent: 0
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1706e28ae8e595149b2ac1fbea7f8071
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+1
-1
@@ -12,7 +12,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
|
||||
m_Name: Localization-Assets-English (en)_BundledAssetGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: aafc8258a2111e94d8339a3152cf50e0, type: 2}
|
||||
m_Group: {fileID: 11400000, guid: 60fd2fd8c7094b242b01c63de65de9ea, type: 2}
|
||||
m_InternalBundleIdMode: 1
|
||||
m_Compression: 1
|
||||
m_IncludeAddressInCatalog: 1
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 895c7c97ef5802446a395377ef704940
|
||||
guid: fa74e99b946159549971be154369b2cb
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
|
||||
+1
-1
@@ -12,5 +12,5 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
|
||||
m_Name: Localization-Assets-English (en)_ContentUpdateGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: aafc8258a2111e94d8339a3152cf50e0, type: 2}
|
||||
m_Group: {fileID: 11400000, guid: 60fd2fd8c7094b242b01c63de65de9ea, type: 2}
|
||||
m_StaticContent: 0
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8004f70cd10ce4a439cb9e822bb078c2
|
||||
guid: 8e1cbbf76f0150145a39ce648fbf3985
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
|
||||
m_Name: Localization-String-Tables-Japanese (Japan) (ja-JP)_BundledAssetGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: dc8d31ed80ea35945812ff6552efbbe5, type: 2}
|
||||
m_InternalBundleIdMode: 1
|
||||
m_Compression: 1
|
||||
m_IncludeAddressInCatalog: 1
|
||||
m_IncludeGUIDInCatalog: 1
|
||||
m_IncludeLabelsInCatalog: 1
|
||||
m_InternalIdNamingMode: 0
|
||||
m_CacheClearBehavior: 0
|
||||
m_IncludeInBuild: 1
|
||||
m_BundledAssetProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
|
||||
m_ForceUniqueProvider: 0
|
||||
m_UseAssetBundleCache: 1
|
||||
m_UseAssetBundleCrc: 1
|
||||
m_UseAssetBundleCrcForCachedBundles: 1
|
||||
m_UseUWRForLocalBundles: 0
|
||||
m_Timeout: 0
|
||||
m_ChunkedTransfer: 0
|
||||
m_RedirectLimit: -1
|
||||
m_RetryCount: 0
|
||||
m_BuildPath:
|
||||
m_Id: 2ae6a7549097a9b4caefe44b61b1fcc5
|
||||
m_LoadPath:
|
||||
m_Id: 631ec5c677ab8084fb9be9b9ee39c6ef
|
||||
m_BundleMode: 0
|
||||
m_AssetBundleProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
|
||||
m_BundleNaming: 1
|
||||
m_AssetLoadMode: 0
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7df396a6b5136fe4e9d2d659731161d2
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
|
||||
m_Name: Localization-String-Tables-Japanese (Japan) (ja-JP)_ContentUpdateGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: dc8d31ed80ea35945812ff6552efbbe5, type: 2}
|
||||
m_StaticContent: 0
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 545ed8780a6508047bd42b964feb82fd
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -32,6 +32,11 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: b9e730c7e5824c5ca2858a253fe93c98
|
||||
m_Address: Prefab/FrameAnimationActor
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: cd996317bdc6ae8478acb2de956f25d9
|
||||
m_Address: Prefab/AnimaMobileObject
|
||||
m_ReadOnly: 0
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-7273616238501396146
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
|
||||
m_Name: VolumeAsset
|
||||
m_EditorClassIdentifier:
|
||||
m_Template:
|
||||
Weight: 1
|
||||
m_Volume: {fileID: 11400000, guid: 0af8555a6447b50409db2139400d6ab7, type: 2}
|
||||
--- !u!114 &-3378792728507138695
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
|
||||
m_Name: Volume Track (1)
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 3
|
||||
m_AnimClip: {fileID: 0}
|
||||
m_Locked: 0
|
||||
m_Muted: 1
|
||||
m_CustomPlayableFullTypename:
|
||||
m_Curves: {fileID: 0}
|
||||
m_Parent: {fileID: 11400000}
|
||||
m_Children: []
|
||||
m_Clips:
|
||||
- m_Version: 1
|
||||
m_Start: 0
|
||||
m_ClipIn: 0
|
||||
m_Asset: {fileID: 7967868333749088964}
|
||||
m_Duration: 1.3666666666666667
|
||||
m_TimeScale: 1
|
||||
m_ParentTrack: {fileID: -3378792728507138695}
|
||||
m_EaseInDuration: 0.1
|
||||
m_EaseOutDuration: 0.6
|
||||
m_BlendInDuration: -1
|
||||
m_BlendOutDuration: -1
|
||||
m_MixInCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_MixOutCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_BlendInCurveMode: 0
|
||||
m_BlendOutCurveMode: 0
|
||||
m_ExposedParameterNames: []
|
||||
m_AnimationCurves: {fileID: 0}
|
||||
m_Recordable: 0
|
||||
m_PostExtrapolationMode: 0
|
||||
m_PreExtrapolationMode: 0
|
||||
m_PostExtrapolationTime: 0
|
||||
m_PreExtrapolationTime: 0
|
||||
m_DisplayName: VolumeAsset
|
||||
m_Markers:
|
||||
m_Objects: []
|
||||
m_Layer: 0
|
||||
m_Priority: 0
|
||||
--- !u!114 &-3117513502812618692
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
|
||||
m_Name: Volume Track
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 3
|
||||
m_AnimClip: {fileID: 0}
|
||||
m_Locked: 0
|
||||
m_Muted: 1
|
||||
m_CustomPlayableFullTypename:
|
||||
m_Curves: {fileID: 0}
|
||||
m_Parent: {fileID: 11400000}
|
||||
m_Children: []
|
||||
m_Clips:
|
||||
- m_Version: 1
|
||||
m_Start: 0
|
||||
m_ClipIn: 0
|
||||
m_Asset: {fileID: -7273616238501396146}
|
||||
m_Duration: 1.3666666666666667
|
||||
m_TimeScale: 1
|
||||
m_ParentTrack: {fileID: -3117513502812618692}
|
||||
m_EaseInDuration: 0.1
|
||||
m_EaseOutDuration: 0.6
|
||||
m_BlendInDuration: -1
|
||||
m_BlendOutDuration: -1
|
||||
m_MixInCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_MixOutCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_BlendInCurveMode: 0
|
||||
m_BlendOutCurveMode: 0
|
||||
m_ExposedParameterNames: []
|
||||
m_AnimationCurves: {fileID: 0}
|
||||
m_Recordable: 0
|
||||
m_PostExtrapolationMode: 0
|
||||
m_PreExtrapolationMode: 0
|
||||
m_PostExtrapolationTime: 0
|
||||
m_PreExtrapolationTime: 0
|
||||
m_DisplayName: VolumeAsset
|
||||
m_Markers:
|
||||
m_Objects: []
|
||||
m_Layer: 0
|
||||
m_Priority: 0
|
||||
--- !u!114 &-2955631063214815081
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
|
||||
m_Name: Volume Track (2)
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 3
|
||||
m_AnimClip: {fileID: 0}
|
||||
m_Locked: 0
|
||||
m_Muted: 1
|
||||
m_CustomPlayableFullTypename:
|
||||
m_Curves: {fileID: 0}
|
||||
m_Parent: {fileID: 11400000}
|
||||
m_Children: []
|
||||
m_Clips:
|
||||
- m_Version: 1
|
||||
m_Start: 0
|
||||
m_ClipIn: 0
|
||||
m_Asset: {fileID: -1361877534822022742}
|
||||
m_Duration: 1.3666666666666667
|
||||
m_TimeScale: 1
|
||||
m_ParentTrack: {fileID: -2955631063214815081}
|
||||
m_EaseInDuration: 0.1
|
||||
m_EaseOutDuration: 0.6
|
||||
m_BlendInDuration: -1
|
||||
m_BlendOutDuration: -1
|
||||
m_MixInCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_MixOutCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_BlendInCurveMode: 0
|
||||
m_BlendOutCurveMode: 0
|
||||
m_ExposedParameterNames: []
|
||||
m_AnimationCurves: {fileID: 0}
|
||||
m_Recordable: 0
|
||||
m_PostExtrapolationMode: 0
|
||||
m_PreExtrapolationMode: 0
|
||||
m_PostExtrapolationTime: 0
|
||||
m_PreExtrapolationTime: 0
|
||||
m_DisplayName: VolumeAsset
|
||||
m_Markers:
|
||||
m_Objects: []
|
||||
m_Layer: 0
|
||||
m_Priority: 0
|
||||
--- !u!114 &-1361877534822022742
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
|
||||
m_Name: VolumeAsset
|
||||
m_EditorClassIdentifier:
|
||||
m_Template:
|
||||
Weight: 1
|
||||
m_Volume: {fileID: 11400000, guid: 631b3200517e20440b88f28015fc3a5c, type: 2}
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bfda56da833e2384a9677cd3c976a436, type: 3}
|
||||
m_Name: "\u706B\u5C71\u5934\u75DB1"
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 0
|
||||
m_Tracks:
|
||||
- {fileID: -3117513502812618692}
|
||||
- {fileID: -3378792728507138695}
|
||||
- {fileID: -2955631063214815081}
|
||||
- {fileID: 2112685405309563144}
|
||||
m_FixedDuration: 0
|
||||
m_EditorSettings:
|
||||
m_Framerate: 60
|
||||
m_ScenePreview: 1
|
||||
m_DurationMode: 0
|
||||
m_MarkerTrack: {fileID: 0}
|
||||
--- !u!74 &1077202520131138910
|
||||
AnimationClip:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Recorded
|
||||
serializedVersion: 7
|
||||
m_Legacy: 0
|
||||
m_Compressed: 0
|
||||
m_UseHighQualityCurve: 1
|
||||
m_RotationCurves: []
|
||||
m_CompressedRotationCurves: []
|
||||
m_EulerCurves: []
|
||||
m_PositionCurves: []
|
||||
m_ScaleCurves: []
|
||||
m_FloatCurves:
|
||||
- serializedVersion: 2
|
||||
curve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.13333334
|
||||
value: 0.8
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.8333333
|
||||
value: 0.8
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 1.1333333
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: m_Color.a
|
||||
path:
|
||||
classID: 212
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
m_PPtrCurves: []
|
||||
m_SampleRate: 60
|
||||
m_WrapMode: 0
|
||||
m_Bounds:
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
m_Extent: {x: 0, y: 0, z: 0}
|
||||
m_ClipBindingConstant:
|
||||
genericBindings:
|
||||
- serializedVersion: 2
|
||||
path: 0
|
||||
attribute: 304273561
|
||||
script: {fileID: 0}
|
||||
typeID: 212
|
||||
customType: 0
|
||||
isPPtrCurve: 0
|
||||
isIntCurve: 0
|
||||
isSerializeReferenceCurve: 0
|
||||
pptrCurveMapping: []
|
||||
m_AnimationClipSettings:
|
||||
serializedVersion: 2
|
||||
m_AdditiveReferencePoseClip: {fileID: 0}
|
||||
m_AdditiveReferencePoseTime: 0
|
||||
m_StartTime: 0
|
||||
m_StopTime: 1.1333333
|
||||
m_OrientationOffsetY: 0
|
||||
m_Level: 0
|
||||
m_CycleOffset: 0
|
||||
m_HasAdditiveReferencePose: 0
|
||||
m_LoopTime: 0
|
||||
m_LoopBlend: 0
|
||||
m_LoopBlendOrientation: 0
|
||||
m_LoopBlendPositionY: 0
|
||||
m_LoopBlendPositionXZ: 0
|
||||
m_KeepOriginalOrientation: 0
|
||||
m_KeepOriginalPositionY: 1
|
||||
m_KeepOriginalPositionXZ: 0
|
||||
m_HeightFromFeet: 0
|
||||
m_Mirror: 0
|
||||
m_EditorCurves:
|
||||
- serializedVersion: 2
|
||||
curve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.13333334
|
||||
value: 0.8
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.8333333
|
||||
value: 0.8
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 1.1333333
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: m_Color.a
|
||||
path:
|
||||
classID: 212
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
m_HasMotionFloatCurves: 0
|
||||
m_Events: []
|
||||
--- !u!114 &2112685405309563144
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
|
||||
m_Name: Animation Track
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 3
|
||||
m_AnimClip: {fileID: 0}
|
||||
m_Locked: 0
|
||||
m_Muted: 0
|
||||
m_CustomPlayableFullTypename:
|
||||
m_Curves: {fileID: 0}
|
||||
m_Parent: {fileID: 11400000}
|
||||
m_Children: []
|
||||
m_Clips: []
|
||||
m_Markers:
|
||||
m_Objects: []
|
||||
m_InfiniteClipPreExtrapolation: 1
|
||||
m_InfiniteClipPostExtrapolation: 1
|
||||
m_InfiniteClipOffsetPosition: {x: 0, y: 0, z: 0}
|
||||
m_InfiniteClipOffsetEulerAngles: {x: 0, y: 0, z: 0}
|
||||
m_InfiniteClipTimeOffset: 0
|
||||
m_InfiniteClipRemoveOffset: 0
|
||||
m_InfiniteClipApplyFootIK: 1
|
||||
mInfiniteClipLoop: 0
|
||||
m_MatchTargetFields: 63
|
||||
m_Position: {x: 0, y: 0, z: 0}
|
||||
m_EulerAngles: {x: 0, y: 0, z: 0}
|
||||
m_AvatarMask: {fileID: 0}
|
||||
m_ApplyAvatarMask: 1
|
||||
m_TrackOffset: 0
|
||||
m_InfiniteClip: {fileID: 1077202520131138910}
|
||||
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_ApplyOffsets: 0
|
||||
--- !u!114 &7967868333749088964
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
|
||||
m_Name: VolumeAsset
|
||||
m_EditorClassIdentifier:
|
||||
m_Template:
|
||||
Weight: 1
|
||||
m_Volume: {fileID: 11400000, guid: 841479fd6ffe3ee40a88ee41bd395f09, type: 2}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e69af1ed498c57f4799f936aa28e4ed4
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,550 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-7273616238501396146
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
|
||||
m_Name: VolumeAsset
|
||||
m_EditorClassIdentifier:
|
||||
m_Template:
|
||||
Weight: 1
|
||||
m_Volume: {fileID: 11400000, guid: 0af8555a6447b50409db2139400d6ab7, type: 2}
|
||||
--- !u!114 &-3378792728507138695
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
|
||||
m_Name: Volume Track (1)
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 3
|
||||
m_AnimClip: {fileID: 0}
|
||||
m_Locked: 0
|
||||
m_Muted: 0
|
||||
m_CustomPlayableFullTypename:
|
||||
m_Curves: {fileID: 0}
|
||||
m_Parent: {fileID: 11400000}
|
||||
m_Children: []
|
||||
m_Clips:
|
||||
- m_Version: 1
|
||||
m_Start: 0
|
||||
m_ClipIn: 0
|
||||
m_Asset: {fileID: 7967868333749088964}
|
||||
m_Duration: 1.3666666666666667
|
||||
m_TimeScale: 1
|
||||
m_ParentTrack: {fileID: -3378792728507138695}
|
||||
m_EaseInDuration: 0.1
|
||||
m_EaseOutDuration: 0.6
|
||||
m_BlendInDuration: -1
|
||||
m_BlendOutDuration: -1
|
||||
m_MixInCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_MixOutCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_BlendInCurveMode: 0
|
||||
m_BlendOutCurveMode: 0
|
||||
m_ExposedParameterNames: []
|
||||
m_AnimationCurves: {fileID: 0}
|
||||
m_Recordable: 0
|
||||
m_PostExtrapolationMode: 0
|
||||
m_PreExtrapolationMode: 0
|
||||
m_PostExtrapolationTime: 0
|
||||
m_PreExtrapolationTime: 0
|
||||
m_DisplayName: VolumeAsset
|
||||
m_Markers:
|
||||
m_Objects: []
|
||||
m_Layer: 0
|
||||
m_Priority: 0
|
||||
--- !u!114 &-3117513502812618692
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
|
||||
m_Name: Volume Track
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 3
|
||||
m_AnimClip: {fileID: 0}
|
||||
m_Locked: 0
|
||||
m_Muted: 0
|
||||
m_CustomPlayableFullTypename:
|
||||
m_Curves: {fileID: 0}
|
||||
m_Parent: {fileID: 11400000}
|
||||
m_Children: []
|
||||
m_Clips:
|
||||
- m_Version: 1
|
||||
m_Start: 0
|
||||
m_ClipIn: 0
|
||||
m_Asset: {fileID: -7273616238501396146}
|
||||
m_Duration: 1.3666666666666667
|
||||
m_TimeScale: 1
|
||||
m_ParentTrack: {fileID: -3117513502812618692}
|
||||
m_EaseInDuration: 0.1
|
||||
m_EaseOutDuration: 0.6
|
||||
m_BlendInDuration: -1
|
||||
m_BlendOutDuration: -1
|
||||
m_MixInCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_MixOutCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_BlendInCurveMode: 0
|
||||
m_BlendOutCurveMode: 0
|
||||
m_ExposedParameterNames: []
|
||||
m_AnimationCurves: {fileID: 0}
|
||||
m_Recordable: 0
|
||||
m_PostExtrapolationMode: 0
|
||||
m_PreExtrapolationMode: 0
|
||||
m_PostExtrapolationTime: 0
|
||||
m_PreExtrapolationTime: 0
|
||||
m_DisplayName: VolumeAsset
|
||||
m_Markers:
|
||||
m_Objects: []
|
||||
m_Layer: 0
|
||||
m_Priority: 0
|
||||
--- !u!114 &-2955631063214815081
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
|
||||
m_Name: Volume Track (2)
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 3
|
||||
m_AnimClip: {fileID: 0}
|
||||
m_Locked: 0
|
||||
m_Muted: 0
|
||||
m_CustomPlayableFullTypename:
|
||||
m_Curves: {fileID: 0}
|
||||
m_Parent: {fileID: 11400000}
|
||||
m_Children: []
|
||||
m_Clips:
|
||||
- m_Version: 1
|
||||
m_Start: 0
|
||||
m_ClipIn: 0
|
||||
m_Asset: {fileID: -1361877534822022742}
|
||||
m_Duration: 1.3666666666666667
|
||||
m_TimeScale: 1
|
||||
m_ParentTrack: {fileID: -2955631063214815081}
|
||||
m_EaseInDuration: 0.1
|
||||
m_EaseOutDuration: 0.6
|
||||
m_BlendInDuration: -1
|
||||
m_BlendOutDuration: -1
|
||||
m_MixInCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_MixOutCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_BlendInCurveMode: 0
|
||||
m_BlendOutCurveMode: 0
|
||||
m_ExposedParameterNames: []
|
||||
m_AnimationCurves: {fileID: 0}
|
||||
m_Recordable: 0
|
||||
m_PostExtrapolationMode: 0
|
||||
m_PreExtrapolationMode: 0
|
||||
m_PostExtrapolationTime: 0
|
||||
m_PreExtrapolationTime: 0
|
||||
m_DisplayName: VolumeAsset
|
||||
m_Markers:
|
||||
m_Objects: []
|
||||
m_Layer: 0
|
||||
m_Priority: 0
|
||||
--- !u!114 &-1361877534822022742
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
|
||||
m_Name: VolumeAsset
|
||||
m_EditorClassIdentifier:
|
||||
m_Template:
|
||||
Weight: 1
|
||||
m_Volume: {fileID: 11400000, guid: 631b3200517e20440b88f28015fc3a5c, type: 2}
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bfda56da833e2384a9677cd3c976a436, type: 3}
|
||||
m_Name: "\u706B\u5C71\u5934\u75DB2"
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 0
|
||||
m_Tracks:
|
||||
- {fileID: -3117513502812618692}
|
||||
- {fileID: -3378792728507138695}
|
||||
- {fileID: -2955631063214815081}
|
||||
- {fileID: 2112685405309563144}
|
||||
m_FixedDuration: 0
|
||||
m_EditorSettings:
|
||||
m_Framerate: 60
|
||||
m_ScenePreview: 1
|
||||
m_DurationMode: 0
|
||||
m_MarkerTrack: {fileID: 0}
|
||||
--- !u!74 &1077202520131138910
|
||||
AnimationClip:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Recorded
|
||||
serializedVersion: 7
|
||||
m_Legacy: 0
|
||||
m_Compressed: 0
|
||||
m_UseHighQualityCurve: 1
|
||||
m_RotationCurves: []
|
||||
m_CompressedRotationCurves: []
|
||||
m_EulerCurves: []
|
||||
m_PositionCurves: []
|
||||
m_ScaleCurves: []
|
||||
m_FloatCurves:
|
||||
- serializedVersion: 2
|
||||
curve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.13333334
|
||||
value: 0.8
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.8333333
|
||||
value: 0.8
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 1.1333333
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: m_Color.a
|
||||
path:
|
||||
classID: 212
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
m_PPtrCurves: []
|
||||
m_SampleRate: 60
|
||||
m_WrapMode: 0
|
||||
m_Bounds:
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
m_Extent: {x: 0, y: 0, z: 0}
|
||||
m_ClipBindingConstant:
|
||||
genericBindings:
|
||||
- serializedVersion: 2
|
||||
path: 0
|
||||
attribute: 304273561
|
||||
script: {fileID: 0}
|
||||
typeID: 212
|
||||
customType: 0
|
||||
isPPtrCurve: 0
|
||||
isIntCurve: 0
|
||||
isSerializeReferenceCurve: 0
|
||||
pptrCurveMapping: []
|
||||
m_AnimationClipSettings:
|
||||
serializedVersion: 2
|
||||
m_AdditiveReferencePoseClip: {fileID: 0}
|
||||
m_AdditiveReferencePoseTime: 0
|
||||
m_StartTime: 0
|
||||
m_StopTime: 1.1333333
|
||||
m_OrientationOffsetY: 0
|
||||
m_Level: 0
|
||||
m_CycleOffset: 0
|
||||
m_HasAdditiveReferencePose: 0
|
||||
m_LoopTime: 0
|
||||
m_LoopBlend: 0
|
||||
m_LoopBlendOrientation: 0
|
||||
m_LoopBlendPositionY: 0
|
||||
m_LoopBlendPositionXZ: 0
|
||||
m_KeepOriginalOrientation: 0
|
||||
m_KeepOriginalPositionY: 1
|
||||
m_KeepOriginalPositionXZ: 0
|
||||
m_HeightFromFeet: 0
|
||||
m_Mirror: 0
|
||||
m_EditorCurves:
|
||||
- serializedVersion: 2
|
||||
curve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.13333334
|
||||
value: 0.8
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.8333333
|
||||
value: 0.8
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 1.1333333
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: m_Color.a
|
||||
path:
|
||||
classID: 212
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
m_HasMotionFloatCurves: 0
|
||||
m_Events: []
|
||||
--- !u!114 &2112685405309563144
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
|
||||
m_Name: Animation Track
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 3
|
||||
m_AnimClip: {fileID: 0}
|
||||
m_Locked: 0
|
||||
m_Muted: 0
|
||||
m_CustomPlayableFullTypename:
|
||||
m_Curves: {fileID: 0}
|
||||
m_Parent: {fileID: 11400000}
|
||||
m_Children: []
|
||||
m_Clips: []
|
||||
m_Markers:
|
||||
m_Objects: []
|
||||
m_InfiniteClipPreExtrapolation: 1
|
||||
m_InfiniteClipPostExtrapolation: 1
|
||||
m_InfiniteClipOffsetPosition: {x: 0, y: 0, z: 0}
|
||||
m_InfiniteClipOffsetEulerAngles: {x: 0, y: 0, z: 0}
|
||||
m_InfiniteClipTimeOffset: 0
|
||||
m_InfiniteClipRemoveOffset: 0
|
||||
m_InfiniteClipApplyFootIK: 1
|
||||
mInfiniteClipLoop: 0
|
||||
m_MatchTargetFields: 63
|
||||
m_Position: {x: 0, y: 0, z: 0}
|
||||
m_EulerAngles: {x: 0, y: 0, z: 0}
|
||||
m_AvatarMask: {fileID: 0}
|
||||
m_ApplyAvatarMask: 1
|
||||
m_TrackOffset: 0
|
||||
m_InfiniteClip: {fileID: 1077202520131138910}
|
||||
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_ApplyOffsets: 0
|
||||
--- !u!114 &7967868333749088964
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
|
||||
m_Name: VolumeAsset
|
||||
m_EditorClassIdentifier:
|
||||
m_Template:
|
||||
Weight: 1
|
||||
m_Volume: {fileID: 11400000, guid: 841479fd6ffe3ee40a88ee41bd395f09, type: 2}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af4651ca096e81d4d8bcf8b93a91b872
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b51c643e2acc12549a81d17c232253a5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e274bde4016eca842be36e589f6285f6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: efd7db4410b73e3499580d0084463f16
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,231 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-7282877348262261630
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 030f85c3f73729f4f976f66ffb23b875, type: 3}
|
||||
m_Name: Recorded
|
||||
m_EditorClassIdentifier:
|
||||
m_Clip: {fileID: -5154461728888953855}
|
||||
m_Position: {x: 0, y: 0, z: 0}
|
||||
m_EulerAngles: {x: 0, y: 0, z: 0}
|
||||
m_UseTrackMatchFields: 1
|
||||
m_MatchTargetFields: 63
|
||||
m_RemoveStartOffset: 0
|
||||
m_ApplyFootIK: 1
|
||||
m_Loop: 0
|
||||
m_Version: 1
|
||||
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
--- !u!74 &-5154461728888953855
|
||||
AnimationClip:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Recorded
|
||||
serializedVersion: 7
|
||||
m_Legacy: 0
|
||||
m_Compressed: 0
|
||||
m_UseHighQualityCurve: 1
|
||||
m_RotationCurves: []
|
||||
m_CompressedRotationCurves: []
|
||||
m_EulerCurves: []
|
||||
m_PositionCurves: []
|
||||
m_ScaleCurves: []
|
||||
m_FloatCurves: []
|
||||
m_PPtrCurves:
|
||||
- serializedVersion: 2
|
||||
curve:
|
||||
- time: 0
|
||||
value: {fileID: 21300000, guid: fa6982b25ee84334a95f92811cae79ce, type: 3}
|
||||
- time: 2
|
||||
value: {fileID: 21300000, guid: cc9ae6de290b0b345853d7100d065749, type: 3}
|
||||
- time: 4
|
||||
value: {fileID: 21300000, guid: fa6982b25ee84334a95f92811cae79ce, type: 3}
|
||||
attribute: m_Sprite
|
||||
path:
|
||||
classID: 212
|
||||
script: {fileID: 0}
|
||||
flags: 2
|
||||
m_SampleRate: 60
|
||||
m_WrapMode: 0
|
||||
m_Bounds:
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
m_Extent: {x: 0, y: 0, z: 0}
|
||||
m_ClipBindingConstant:
|
||||
genericBindings:
|
||||
- serializedVersion: 2
|
||||
path: 0
|
||||
attribute: 0
|
||||
script: {fileID: 0}
|
||||
typeID: 212
|
||||
customType: 23
|
||||
isPPtrCurve: 1
|
||||
isIntCurve: 0
|
||||
isSerializeReferenceCurve: 0
|
||||
pptrCurveMapping:
|
||||
- {fileID: 21300000, guid: fa6982b25ee84334a95f92811cae79ce, type: 3}
|
||||
- {fileID: 21300000, guid: cc9ae6de290b0b345853d7100d065749, type: 3}
|
||||
- {fileID: 21300000, guid: fa6982b25ee84334a95f92811cae79ce, type: 3}
|
||||
m_AnimationClipSettings:
|
||||
serializedVersion: 2
|
||||
m_AdditiveReferencePoseClip: {fileID: 0}
|
||||
m_AdditiveReferencePoseTime: 0
|
||||
m_StartTime: 0
|
||||
m_StopTime: 4.016667
|
||||
m_OrientationOffsetY: 0
|
||||
m_Level: 0
|
||||
m_CycleOffset: 0
|
||||
m_HasAdditiveReferencePose: 0
|
||||
m_LoopTime: 0
|
||||
m_LoopBlend: 0
|
||||
m_LoopBlendOrientation: 0
|
||||
m_LoopBlendPositionY: 0
|
||||
m_LoopBlendPositionXZ: 0
|
||||
m_KeepOriginalOrientation: 0
|
||||
m_KeepOriginalPositionY: 1
|
||||
m_KeepOriginalPositionXZ: 0
|
||||
m_HeightFromFeet: 0
|
||||
m_Mirror: 0
|
||||
m_EditorCurves: []
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
m_HasMotionFloatCurves: 0
|
||||
m_Events: []
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bfda56da833e2384a9677cd3c976a436, type: 3}
|
||||
m_Name: "\u65CB\u8F6C\u53F6\u67C4"
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 0
|
||||
m_Tracks:
|
||||
- {fileID: 6913286052231973111}
|
||||
m_FixedDuration: 0
|
||||
m_EditorSettings:
|
||||
m_Framerate: 60
|
||||
m_ScenePreview: 1
|
||||
m_DurationMode: 0
|
||||
m_MarkerTrack: {fileID: 0}
|
||||
--- !u!114 &6913286052231973111
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
|
||||
m_Name: Animation Track
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 3
|
||||
m_AnimClip: {fileID: 0}
|
||||
m_Locked: 0
|
||||
m_Muted: 0
|
||||
m_CustomPlayableFullTypename:
|
||||
m_Curves: {fileID: 0}
|
||||
m_Parent: {fileID: 11400000}
|
||||
m_Children: []
|
||||
m_Clips:
|
||||
- m_Version: 1
|
||||
m_Start: 0
|
||||
m_ClipIn: 0
|
||||
m_Asset: {fileID: -7282877348262261630}
|
||||
m_Duration: 4
|
||||
m_TimeScale: 1
|
||||
m_ParentTrack: {fileID: 6913286052231973111}
|
||||
m_EaseInDuration: 0
|
||||
m_EaseOutDuration: 0
|
||||
m_BlendInDuration: -1
|
||||
m_BlendOutDuration: -1
|
||||
m_MixInCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_MixOutCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_BlendInCurveMode: 0
|
||||
m_BlendOutCurveMode: 0
|
||||
m_ExposedParameterNames: []
|
||||
m_AnimationCurves: {fileID: 0}
|
||||
m_Recordable: 1
|
||||
m_PostExtrapolationMode: 1
|
||||
m_PreExtrapolationMode: 1
|
||||
m_PostExtrapolationTime: Infinity
|
||||
m_PreExtrapolationTime: 0
|
||||
m_DisplayName: Recorded
|
||||
m_Markers:
|
||||
m_Objects: []
|
||||
m_InfiniteClipPreExtrapolation: 1
|
||||
m_InfiniteClipPostExtrapolation: 1
|
||||
m_InfiniteClipOffsetPosition: {x: 0, y: 0, z: 0}
|
||||
m_InfiniteClipOffsetEulerAngles: {x: 0, y: 0, z: 0}
|
||||
m_InfiniteClipTimeOffset: 0
|
||||
m_InfiniteClipRemoveOffset: 0
|
||||
m_InfiniteClipApplyFootIK: 1
|
||||
mInfiniteClipLoop: 0
|
||||
m_MatchTargetFields: 63
|
||||
m_Position: {x: 0, y: 0, z: 0}
|
||||
m_EulerAngles: {x: 0, y: 0, z: 0}
|
||||
m_AvatarMask: {fileID: 0}
|
||||
m_ApplyAvatarMask: 1
|
||||
m_TrackOffset: 0
|
||||
m_InfiniteClip: {fileID: 0}
|
||||
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_ApplyOffsets: 0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 253f47db959b2814998695fb5d1f4d97
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bfda56da833e2384a9677cd3c976a436, type: 3}
|
||||
m_Name: "\u98CE\u5439\u6811\u53F6"
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 0
|
||||
m_Tracks: []
|
||||
m_FixedDuration: 0
|
||||
m_EditorSettings:
|
||||
m_Framerate: 60
|
||||
m_ScenePreview: 1
|
||||
m_DurationMode: 0
|
||||
m_MarkerTrack: {fileID: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30d786a983bc55a4e844ce4b7d6d6fab
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -244,3 +244,14 @@ MonoBehaviour:
|
||||
m_PreInfinity: 0
|
||||
m_PostInfinity: 0
|
||||
m_RotationOrder: 0
|
||||
- m_From: '**ANY CAMERA**'
|
||||
m_To: Dream Camera
|
||||
m_Blend:
|
||||
m_Style: 0
|
||||
m_Time: 2
|
||||
m_CustomCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve: []
|
||||
m_PreInfinity: 0
|
||||
m_PostInfinity: 0
|
||||
m_RotationOrder: 0
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09d2581fcb827614bba6bf5c3279358e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,277 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using AibisDream;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace AibisDream.SystemEditor
|
||||
{
|
||||
public class ChapterGraphEditorWindow : EditorWindow
|
||||
{
|
||||
private ChapterGraphView _graphView;
|
||||
private ChapterGraphInspector _inspector;
|
||||
private IMGUIContainer _inspectorContainer;
|
||||
|
||||
private const string RootFolderPrefKey = "AibisDream.ChapterGraph.RootFolder";
|
||||
private const string CurrentFolderPrefKey = "AibisDream.ChapterGraph.CurrentFolder";
|
||||
private string _rootFolder;
|
||||
private string _currentFolder;
|
||||
private DropdownField _folderDropdown;
|
||||
private readonly Dictionary<string, string> _displayToPath = new();
|
||||
|
||||
[MenuItem("Window/Chapter Graph Editor")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
var window = GetWindow<ChapterGraphEditorWindow>("Chapter Graph Editor");
|
||||
window.minSize = new Vector2(800, 600);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_graphView == null) return;
|
||||
_graphView.LoadGraph(_currentFolder);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_graphView != null)
|
||||
{
|
||||
_graphView.OnNodeDeleted -= OnNodeDeleted;
|
||||
}
|
||||
if (_inspector != null)
|
||||
{
|
||||
_inspector.OnColorChanged -= OnNodeColorChanged;
|
||||
_inspector.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateGUI()
|
||||
{
|
||||
// 根容器:纵向排列
|
||||
var root = rootVisualElement;
|
||||
root.style.flexDirection = FlexDirection.Column;
|
||||
|
||||
// 顶部工具栏
|
||||
var toolbar = new Toolbar();
|
||||
toolbar.Add(CreateToolbarButton("Save", OnSaveClicked));
|
||||
toolbar.Add(CreateToolbarButton("Auto Layout", OnAutoLayoutClicked));
|
||||
toolbar.Add(CreateToolbarButton("Validate", OnValidateClicked));
|
||||
|
||||
_rootFolder = EditorPrefs.GetString(RootFolderPrefKey, "Assets/ScriptableObjects/SceneSO");
|
||||
|
||||
_folderDropdown = new DropdownField("路径");
|
||||
_folderDropdown.style.width = 160;
|
||||
_folderDropdown.style.marginLeft = 8;
|
||||
_folderDropdown.style.marginRight = 4;
|
||||
RefreshFolderChoices();
|
||||
_folderDropdown.RegisterValueChangedCallback(OnFolderChanged);
|
||||
toolbar.Add(_folderDropdown);
|
||||
|
||||
var setRootBtn = new Button(OnSetRootFolderClicked) { text = "..." };
|
||||
setRootBtn.style.width = 28;
|
||||
toolbar.Add(setRootBtn);
|
||||
|
||||
toolbar.Add(new ToolbarSpacer() { style = { flexGrow = 1 } });
|
||||
toolbar.Add(CreateToolbarButton("Create Chapter", OnCreateChapterClicked));
|
||||
root.Add(toolbar);
|
||||
|
||||
// 主内容区:横向分栏
|
||||
var mainContainer = new VisualElement();
|
||||
mainContainer.style.flexGrow = 1;
|
||||
mainContainer.style.flexDirection = FlexDirection.Row;
|
||||
mainContainer.style.overflow = Overflow.Hidden;
|
||||
|
||||
// 左侧 Inspector 面板
|
||||
var leftPanel = new VisualElement();
|
||||
leftPanel.style.width = 320;
|
||||
leftPanel.style.minWidth = 200;
|
||||
leftPanel.style.maxWidth = 500;
|
||||
leftPanel.style.flexShrink = 0;
|
||||
leftPanel.style.flexDirection = FlexDirection.Column;
|
||||
leftPanel.style.borderRightWidth = 1;
|
||||
leftPanel.style.borderRightColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f));
|
||||
|
||||
// Inspector 标题
|
||||
var header = new Label("章节属性")
|
||||
{
|
||||
style =
|
||||
{
|
||||
unityFontStyleAndWeight = FontStyle.Bold,
|
||||
fontSize = 14,
|
||||
paddingTop = 8,
|
||||
paddingBottom = 8,
|
||||
paddingLeft = 12,
|
||||
paddingRight = 12,
|
||||
borderBottomWidth = 1,
|
||||
borderBottomColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f))
|
||||
}
|
||||
};
|
||||
leftPanel.Add(header);
|
||||
|
||||
// IMGUI Inspector
|
||||
_inspector = new ChapterGraphInspector();
|
||||
_inspectorContainer = new IMGUIContainer(() => _inspector.OnGUI());
|
||||
_inspectorContainer.style.flexGrow = 1;
|
||||
leftPanel.Add(_inspectorContainer);
|
||||
|
||||
mainContainer.Add(leftPanel);
|
||||
|
||||
// 拖拽条
|
||||
var resizer = new VisualElement();
|
||||
resizer.style.width = 5;
|
||||
resizer.style.backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f));
|
||||
mainContainer.Add(resizer);
|
||||
|
||||
float startWidth = 0;
|
||||
float startMouseX = 0;
|
||||
resizer.RegisterCallback<MouseDownEvent>(evt =>
|
||||
{
|
||||
startWidth = leftPanel.resolvedStyle.width;
|
||||
startMouseX = evt.mousePosition.x;
|
||||
resizer.CaptureMouse();
|
||||
evt.StopPropagation();
|
||||
});
|
||||
resizer.RegisterCallback<MouseMoveEvent>(evt =>
|
||||
{
|
||||
if (!resizer.HasMouseCapture()) return;
|
||||
var delta = evt.mousePosition.x - startMouseX;
|
||||
var newWidth = Mathf.Clamp(startWidth + delta, 200, 600);
|
||||
leftPanel.style.width = newWidth;
|
||||
});
|
||||
resizer.RegisterCallback<MouseUpEvent>(evt =>
|
||||
{
|
||||
if (resizer.HasMouseCapture())
|
||||
resizer.ReleaseMouse();
|
||||
});
|
||||
|
||||
// 右侧 GraphView
|
||||
// 套一层容器:GraphView 与左侧 Inspector 并列放在 Flex Row 中时,
|
||||
// 内置 RectangleSelector 绘制的框选矩形会出现坐标偏移(已知 Unity 问题)。
|
||||
// 将 GraphView 放在独立的 Flex 子容器中可使其局部原点与父容器对齐,规避该偏移。
|
||||
var graphViewWrapper = new VisualElement();
|
||||
graphViewWrapper.style.flexGrow = 1;
|
||||
graphViewWrapper.style.flexDirection = FlexDirection.Column;
|
||||
graphViewWrapper.style.overflow = Overflow.Hidden;
|
||||
|
||||
_graphView = new ChapterGraphView();
|
||||
_graphView.style.flexGrow = 1;
|
||||
graphViewWrapper.Add(_graphView);
|
||||
|
||||
mainContainer.Add(graphViewWrapper);
|
||||
|
||||
root.Add(mainContainer);
|
||||
|
||||
// 加载图数据
|
||||
_graphView.LoadGraph(_currentFolder);
|
||||
|
||||
// 选中节点同步到 Inspector
|
||||
_graphView.OnNodeSelected += OnNodeSelected;
|
||||
_graphView.OnNodeDeleted += OnNodeDeleted;
|
||||
_inspector.OnColorChanged += OnNodeColorChanged;
|
||||
}
|
||||
|
||||
private void OnNodeDeleted(TalkSceneSO so)
|
||||
{
|
||||
_inspector?.SetTarget(null);
|
||||
_inspectorContainer?.MarkDirtyRepaint();
|
||||
}
|
||||
|
||||
private void OnNodeColorChanged(TalkSceneSO so)
|
||||
{
|
||||
_graphView?.UpdateNodeColor(so);
|
||||
}
|
||||
|
||||
private Button CreateToolbarButton(string text, System.Action onClick)
|
||||
{
|
||||
var btn = new Button(onClick) { text = text };
|
||||
return btn;
|
||||
}
|
||||
|
||||
private void OnSaveClicked()
|
||||
{
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorUtility.DisplayDialog("保存", "所有资源已保存。", "确定");
|
||||
}
|
||||
|
||||
private void OnAutoLayoutClicked()
|
||||
{
|
||||
_graphView?.AutoLayout();
|
||||
}
|
||||
|
||||
private void OnValidateClicked()
|
||||
{
|
||||
_graphView?.ValidateGraph();
|
||||
}
|
||||
|
||||
private void OnCreateChapterClicked()
|
||||
{
|
||||
var center = new Vector2(position.width / 2f, position.height / 2f);
|
||||
var localPos = _graphView.contentViewContainer.WorldToLocal(center);
|
||||
var defaultFolder = _currentFolder ?? EditorPrefs.GetString(ChapterGraphView.DefaultFolderPrefKey, _rootFolder);
|
||||
_graphView?.CreateChapterNodeAt(localPos, defaultFolder);
|
||||
}
|
||||
|
||||
private void OnNodeSelected(TalkSceneSO so)
|
||||
{
|
||||
_inspector?.SetTarget(so);
|
||||
_inspectorContainer?.MarkDirtyRepaint();
|
||||
}
|
||||
|
||||
private void RefreshFolderChoices()
|
||||
{
|
||||
_displayToPath.Clear();
|
||||
_displayToPath["All"] = null;
|
||||
|
||||
var subFolders = AssetDatabase.GetSubFolders(_rootFolder);
|
||||
foreach (var folder in subFolders)
|
||||
{
|
||||
var name = Path.GetFileName(folder);
|
||||
_displayToPath[name] = folder;
|
||||
}
|
||||
|
||||
var rootHasSO = AssetDatabase.FindAssets("t:TalkSceneSO", new[] { _rootFolder })
|
||||
.Select(g => AssetDatabase.GUIDToAssetPath(g))
|
||||
.Any(path => Path.GetDirectoryName(path)?.Replace('\\', '/') == _rootFolder);
|
||||
|
||||
if (rootHasSO)
|
||||
{
|
||||
_displayToPath["(Root)"] = _rootFolder;
|
||||
}
|
||||
|
||||
_folderDropdown.choices = _displayToPath.Keys.ToList();
|
||||
|
||||
var saved = EditorPrefs.GetString(CurrentFolderPrefKey, "All");
|
||||
_folderDropdown.value = _displayToPath.ContainsKey(saved) ? saved : "All";
|
||||
_currentFolder = _displayToPath.TryGetValue(_folderDropdown.value, out var path) ? path : null;
|
||||
}
|
||||
|
||||
private void OnFolderChanged(ChangeEvent<string> evt)
|
||||
{
|
||||
_currentFolder = _displayToPath.TryGetValue(evt.newValue, out var path) ? path : null;
|
||||
EditorPrefs.SetString(CurrentFolderPrefKey, evt.newValue);
|
||||
_graphView?.LoadGraph(_currentFolder);
|
||||
}
|
||||
|
||||
private void OnSetRootFolderClicked()
|
||||
{
|
||||
var path = EditorUtility.OpenFolderPanel("选择章节根目录", _rootFolder, "");
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
var absoluteRoot = Application.dataPath.Replace('\\', '/');
|
||||
var selected = path.Replace('\\', '/');
|
||||
if (!selected.StartsWith(absoluteRoot))
|
||||
{
|
||||
EditorUtility.DisplayDialog("错误", "请选择项目 Assets 目录内的文件夹。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
_rootFolder = "Assets" + selected.Substring(absoluteRoot.Length);
|
||||
EditorPrefs.SetString(RootFolderPrefKey, _rootFolder);
|
||||
RefreshFolderChoices();
|
||||
_graphView?.LoadGraph(_currentFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c30bb2152169f544cb672c29ac8cb5d1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,70 @@
|
||||
using AibisDream;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.SystemEditor
|
||||
{
|
||||
public class ChapterGraphInspector
|
||||
{
|
||||
private Editor _editor;
|
||||
private TalkSceneSO _target;
|
||||
private Vector2 _scrollPosition;
|
||||
|
||||
public event System.Action<TalkSceneSO> OnColorChanged;
|
||||
|
||||
public void SetTarget(TalkSceneSO so)
|
||||
{
|
||||
// 不能用 ==,Unity 重载了 ==,已销毁的对象 == null 为 true
|
||||
if (ReferenceEquals(_target, so)) return;
|
||||
|
||||
_target = so;
|
||||
if (_editor != null)
|
||||
{
|
||||
Object.DestroyImmediate(_editor);
|
||||
_editor = null;
|
||||
}
|
||||
|
||||
if (so != null)
|
||||
{
|
||||
_editor = Editor.CreateEditor(so);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnGUI()
|
||||
{
|
||||
if (_editor == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("请在图中选中一个章节节点以编辑属性", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var newColor = EditorGUILayout.ColorField("节点颜色", _target.nodeColor);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
Undo.RecordObject(_target, "Change Node Color");
|
||||
_target.nodeColor = newColor;
|
||||
EditorUtility.SetDirty(_target);
|
||||
OnColorChanged?.Invoke(_target);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
|
||||
_editor.OnInspectorGUI();
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
if (_editor != null)
|
||||
{
|
||||
Object.DestroyImmediate(_editor);
|
||||
_editor = null;
|
||||
}
|
||||
|
||||
_target = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b122b6f926732f46b067d9b26eb191b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,503 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AibisDream;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace AibisDream.SystemEditor
|
||||
{
|
||||
public class ChapterGraphView : GraphView
|
||||
{
|
||||
public event Action<TalkSceneSO> OnNodeSelected;
|
||||
public event Action<TalkSceneSO> OnNodeDeleted;
|
||||
|
||||
private const float HorizontalSpacing = 420f;
|
||||
private const float VerticalSpacing = 220f;
|
||||
public const string DefaultFolderPrefKey = "AibisDream.ChapterGraph.DefaultFolder";
|
||||
public const string DefaultFolder = "Assets/ScriptableObjects/SceneSO";
|
||||
|
||||
public ChapterGraphView()
|
||||
{
|
||||
style.flexGrow = 1;
|
||||
|
||||
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
|
||||
this.AddManipulator(new ContentDragger());
|
||||
this.AddManipulator(new SelectionDragger());
|
||||
this.AddManipulator(new RectangleSelector());
|
||||
|
||||
var grid = new GridBackground();
|
||||
Insert(0, grid);
|
||||
grid.StretchToParentSize();
|
||||
|
||||
graphViewChanged += OnGraphViewChanged;
|
||||
}
|
||||
|
||||
public void UpdateNodeColor(TalkSceneSO so)
|
||||
{
|
||||
foreach (var node in graphElements.OfType<ChapterNode>())
|
||||
{
|
||||
if (node.SceneSo == so)
|
||||
{
|
||||
node.UpdateColor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadGraph(string folderPath = null)
|
||||
{
|
||||
ClearGraph();
|
||||
|
||||
var guids = AssetDatabase.FindAssets("t:TalkSceneSO", folderPath != null ? new[] { folderPath } : null);
|
||||
var allSos = guids.Select(g => AssetDatabase.LoadAssetAtPath<TalkSceneSO>(AssetDatabase.GUIDToAssetPath(g))).Where(so => so != null).ToList();
|
||||
var nodeMap = new Dictionary<TalkSceneSO, ChapterNode>();
|
||||
|
||||
// 创建节点
|
||||
foreach (var so in allSos)
|
||||
{
|
||||
var node = new ChapterNode(so);
|
||||
AddElement(node);
|
||||
nodeMap[so] = node;
|
||||
}
|
||||
|
||||
// 创建连线
|
||||
foreach (var so in allSos)
|
||||
{
|
||||
if (so.exits == null) continue;
|
||||
if (!nodeMap.TryGetValue(so, out var sourceNode)) continue;
|
||||
|
||||
for (int i = 0; i < so.exits.Count; i++)
|
||||
{
|
||||
var exit = so.exits[i];
|
||||
if (exit == null || exit.targetScene == null) continue;
|
||||
if (!nodeMap.TryGetValue(exit.targetScene, out var targetNode)) continue;
|
||||
|
||||
var outputPort = sourceNode.GetOutputPort(i);
|
||||
var inputPort = targetNode.InputPort;
|
||||
if (outputPort == null || inputPort == null) continue;
|
||||
|
||||
var edge = new Edge
|
||||
{
|
||||
output = outputPort,
|
||||
input = inputPort
|
||||
};
|
||||
edge.output.Connect(edge);
|
||||
edge.input.Connect(edge);
|
||||
AddElement(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearGraph()
|
||||
{
|
||||
// 清除所有元素
|
||||
var elementsToRemove = graphElements.ToList();
|
||||
foreach (var element in elementsToRemove)
|
||||
{
|
||||
RemoveElement(element);
|
||||
}
|
||||
}
|
||||
|
||||
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
|
||||
{
|
||||
base.BuildContextualMenu(evt);
|
||||
|
||||
if (evt.target is GraphView)
|
||||
{
|
||||
var mousePosition = contentViewContainer.WorldToLocal(evt.mousePosition);
|
||||
evt.menu.AppendAction("添加章节节点", (_) => CreateChapterNodeAt(mousePosition));
|
||||
}
|
||||
}
|
||||
|
||||
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
|
||||
{
|
||||
var compatiblePorts = new List<Port>();
|
||||
var startNode = startPort.node as ChapterNode;
|
||||
if (startNode == null) return compatiblePorts;
|
||||
|
||||
ports.ForEach(port =>
|
||||
{
|
||||
var targetNode = port.node as ChapterNode;
|
||||
if (targetNode == null) return;
|
||||
if (targetNode == startNode) return;
|
||||
if (startPort.direction == port.direction) return;
|
||||
if (startPort.node == port.node) return;
|
||||
|
||||
compatiblePorts.Add(port);
|
||||
});
|
||||
|
||||
return compatiblePorts;
|
||||
}
|
||||
|
||||
private GraphViewChange OnGraphViewChanged(GraphViewChange change)
|
||||
{
|
||||
// 先处理节点删除确认:用户取消则从移除列表中剔除
|
||||
if (change.elementsToRemove != null)
|
||||
{
|
||||
var nodesToRemove = change.elementsToRemove.OfType<ChapterNode>().ToList();
|
||||
foreach (var node in nodesToRemove)
|
||||
{
|
||||
if (!ConfirmRemoveNode(node))
|
||||
{
|
||||
change.elementsToRemove.Remove(node);
|
||||
// 同时保留与该节点相连的边,避免边被单独删掉
|
||||
var edgesToKeep = change.elementsToRemove.OfType<Edge>()
|
||||
.Where(e => e.output?.node == node || e.input?.node == node)
|
||||
.ToList();
|
||||
foreach (var edge in edgesToKeep)
|
||||
{
|
||||
change.elementsToRemove.Remove(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 Edge 删除
|
||||
if (change.elementsToRemove != null)
|
||||
{
|
||||
foreach (var edge in change.elementsToRemove.OfType<Edge>())
|
||||
{
|
||||
HandleEdgeRemoved(edge);
|
||||
}
|
||||
|
||||
foreach (var node in change.elementsToRemove.OfType<ChapterNode>())
|
||||
{
|
||||
HandleNodeRemoved(node);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 Edge 创建
|
||||
if (change.edgesToCreate != null)
|
||||
{
|
||||
foreach (var edge in change.edgesToCreate)
|
||||
{
|
||||
HandleEdgeCreated(edge);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理节点移动
|
||||
if (change.movedElements != null)
|
||||
{
|
||||
foreach (var element in change.movedElements)
|
||||
{
|
||||
if (element is ChapterNode node)
|
||||
{
|
||||
node.SyncPositionToSo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
private void HandleEdgeCreated(Edge edge)
|
||||
{
|
||||
if (!(edge.output?.node is ChapterNode sourceNode)) return;
|
||||
if (!(edge.input?.node is ChapterNode targetNode)) return;
|
||||
|
||||
var sourceSo = sourceNode.SceneSo;
|
||||
var targetSo = targetNode.SceneSo;
|
||||
if (sourceSo == null || targetSo == null) return;
|
||||
|
||||
int exitIndex = edge.output.userData is int idx ? idx : -1;
|
||||
if (exitIndex < 0 || exitIndex >= sourceSo.exits.Count) return;
|
||||
|
||||
Undo.RecordObject(sourceSo, "Connect Chapter");
|
||||
sourceSo.exits[exitIndex].targetScene = targetSo;
|
||||
EditorUtility.SetDirty(sourceSo);
|
||||
}
|
||||
|
||||
private void HandleEdgeRemoved(Edge edge)
|
||||
{
|
||||
if (!(edge.output?.node is ChapterNode sourceNode)) return;
|
||||
|
||||
var sourceSo = sourceNode.SceneSo;
|
||||
if (sourceSo == null) return;
|
||||
|
||||
int exitIndex = edge.output.userData is int idx ? idx : -1;
|
||||
if (exitIndex < 0 || exitIndex >= sourceSo.exits.Count) return;
|
||||
|
||||
Undo.RecordObject(sourceSo, "Disconnect Chapter");
|
||||
sourceSo.exits[exitIndex].targetScene = null;
|
||||
EditorUtility.SetDirty(sourceSo);
|
||||
}
|
||||
|
||||
private bool ConfirmRemoveNode(ChapterNode node)
|
||||
{
|
||||
var so = node.SceneSo;
|
||||
if (so == null) return true;
|
||||
|
||||
var referrers = new List<string>();
|
||||
foreach (var other in graphElements.OfType<ChapterNode>())
|
||||
{
|
||||
if (other == node) continue;
|
||||
if (other.SceneSo == null || other.SceneSo.exits == null) continue;
|
||||
foreach (var exit in other.SceneSo.exits)
|
||||
{
|
||||
if (exit.targetScene == so)
|
||||
{
|
||||
referrers.Add(string.IsNullOrEmpty(other.SceneSo.title) ? other.SceneSo.name : other.SceneSo.title);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var name = string.IsNullOrEmpty(so.title) ? so.name : so.title;
|
||||
var msg = $"确定要删除章节节点 \"{name}\" 吗?";
|
||||
if (referrers.Count > 0)
|
||||
{
|
||||
msg += $"\n\n以下节点仍引用该节点:\n• {string.Join("\n• ", referrers)}";
|
||||
}
|
||||
|
||||
return EditorUtility.DisplayDialog("删除确认", msg, "删除", "取消");
|
||||
}
|
||||
|
||||
private void HandleNodeRemoved(ChapterNode node)
|
||||
{
|
||||
var so = node.SceneSo;
|
||||
if (so == null) return;
|
||||
|
||||
var path = AssetDatabase.GetAssetPath(so);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(path);
|
||||
}
|
||||
|
||||
OnNodeDeleted?.Invoke(so);
|
||||
}
|
||||
|
||||
public override void AddToSelection(ISelectable selectable)
|
||||
{
|
||||
base.AddToSelection(selectable);
|
||||
NotifySelectionChanged();
|
||||
}
|
||||
|
||||
public override void RemoveFromSelection(ISelectable selectable)
|
||||
{
|
||||
base.RemoveFromSelection(selectable);
|
||||
NotifySelectionChanged();
|
||||
}
|
||||
|
||||
public override void ClearSelection()
|
||||
{
|
||||
base.ClearSelection();
|
||||
NotifySelectionChanged();
|
||||
}
|
||||
|
||||
private void NotifySelectionChanged()
|
||||
{
|
||||
var selectedNode = selection.OfType<ChapterNode>().FirstOrDefault();
|
||||
OnNodeSelected?.Invoke(selectedNode?.SceneSo);
|
||||
}
|
||||
|
||||
public void CreateChapterNodeAt(Vector2 position, string defaultFolder = null)
|
||||
{
|
||||
var folder = defaultFolder ?? EditorPrefs.GetString(DefaultFolderPrefKey, DefaultFolder);
|
||||
var path = EditorUtility.SaveFilePanelInProject("新建章节", "NewChapter", "asset", "选择新章节的保存位置", folder);
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
var so = ScriptableObject.CreateInstance<TalkSceneSO>();
|
||||
so.name = System.IO.Path.GetFileNameWithoutExtension(path);
|
||||
so.title = "新章节";
|
||||
so.nodeColor = new Color(0.85f, 0.45f, 0.45f);
|
||||
so.graphPosition = position;
|
||||
so.exits = new System.Collections.Generic.List<SceneExit>();
|
||||
|
||||
AssetDatabase.CreateAsset(so, path);
|
||||
EditorUtility.SetDirty(so);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
// 记住文件夹
|
||||
var savedFolder = System.IO.Path.GetDirectoryName(path)?.Replace('\\', '/');
|
||||
if (!string.IsNullOrEmpty(savedFolder))
|
||||
{
|
||||
EditorPrefs.SetString(DefaultFolderPrefKey, savedFolder);
|
||||
}
|
||||
|
||||
var node = new ChapterNode(so);
|
||||
AddElement(node);
|
||||
}
|
||||
|
||||
public void AutoLayout()
|
||||
{
|
||||
var nodes = graphElements.OfType<ChapterNode>().ToList();
|
||||
if (nodes.Count == 0) return;
|
||||
|
||||
// 计算入度
|
||||
var inDegree = nodes.ToDictionary(n => n, _ => 0);
|
||||
var edges = graphElements.OfType<Edge>().ToList();
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
if (edge.input?.node is ChapterNode target && inDegree.ContainsKey(target))
|
||||
{
|
||||
inDegree[target]++;
|
||||
}
|
||||
}
|
||||
|
||||
// 分层 BFS
|
||||
var layer = new Dictionary<ChapterNode, int>();
|
||||
var queue = new Queue<ChapterNode>();
|
||||
|
||||
// 入度为 0 的节点作为起点
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (inDegree[node] == 0)
|
||||
{
|
||||
layer[node] = 0;
|
||||
queue.Enqueue(node);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有入度为 0 的节点(纯循环),任选一个作为起点
|
||||
if (queue.Count == 0)
|
||||
{
|
||||
layer[nodes[0]] = 0;
|
||||
queue.Enqueue(nodes[0]);
|
||||
}
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
var currentLayer = layer[current];
|
||||
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
if (edge.output?.node == current && edge.input?.node is ChapterNode next)
|
||||
{
|
||||
if (!layer.ContainsKey(next))
|
||||
{
|
||||
layer[next] = currentLayer + 1;
|
||||
queue.Enqueue(next);
|
||||
}
|
||||
else
|
||||
{
|
||||
layer[next] = Math.Max(layer[next], currentLayer + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 为未分配层级的节点(孤立或不连通部分)分配层级
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (!layer.ContainsKey(node))
|
||||
{
|
||||
layer[node] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 按层级分组并计算位置
|
||||
var groups = layer.GroupBy(kvp => kvp.Value).OrderBy(g => g.Key).ToList();
|
||||
Undo.RecordObjects(nodes.Select(n => n.SceneSo).ToArray(), "Auto Layout");
|
||||
|
||||
for (int i = 0; i < groups.Count; i++)
|
||||
{
|
||||
var group = groups[i];
|
||||
var groupNodes = group.ToList();
|
||||
float x = group.Key * HorizontalSpacing;
|
||||
float startY = -(groupNodes.Count - 1) * VerticalSpacing / 2f;
|
||||
|
||||
for (int j = 0; j < groupNodes.Count; j++)
|
||||
{
|
||||
var node = groupNodes[j].Key;
|
||||
var y = startY + j * VerticalSpacing;
|
||||
var newPos = new Vector2(x, y);
|
||||
node.SetPosition(new Rect(newPos, Vector2.zero));
|
||||
node.SceneSo.graphPosition = newPos;
|
||||
EditorUtility.SetDirty(node.SceneSo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ValidateGraph()
|
||||
{
|
||||
var nodes = graphElements.OfType<ChapterNode>().ToList();
|
||||
var issues = new List<string>();
|
||||
var soSet = new HashSet<TalkSceneSO>(nodes.Select(n => n.SceneSo));
|
||||
|
||||
// 孤立节点
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var so = node.SceneSo;
|
||||
bool hasIncoming = graphElements.OfType<Edge>().Any(e => e.input?.node == node);
|
||||
bool hasOutgoing = so.exits != null && so.exits.Any(e => e.targetScene != null);
|
||||
|
||||
if (!hasIncoming && !hasOutgoing)
|
||||
{
|
||||
issues.Add($"孤立节点: {so.name} (没有入边也没有出边)");
|
||||
}
|
||||
}
|
||||
|
||||
// 循环引用 (DFS)
|
||||
var visited = new HashSet<TalkSceneSO>();
|
||||
var visiting = new HashSet<TalkSceneSO>();
|
||||
|
||||
foreach (var so in soSet)
|
||||
{
|
||||
if (!visited.Contains(so))
|
||||
{
|
||||
CheckCycle(so, visited, visiting, soSet, issues);
|
||||
}
|
||||
}
|
||||
|
||||
// 空 exitName
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var so = node.SceneSo;
|
||||
if (so.exits == null) continue;
|
||||
for (int i = 0; i < so.exits.Count; i++)
|
||||
{
|
||||
if (string.IsNullOrEmpty(so.exits[i].exitName))
|
||||
{
|
||||
issues.Add($"空 exitName: {so.name} 的第 {i + 1} 个出口");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (issues.Count == 0)
|
||||
{
|
||||
EditorUtility.DisplayDialog("验证结果", "图表验证通过,未发现问题。", "确定");
|
||||
}
|
||||
else
|
||||
{
|
||||
var msg = string.Join("\n", issues.Take(20));
|
||||
if (issues.Count > 20)
|
||||
{
|
||||
msg += $"\n... 还有 {issues.Count - 20} 个问题";
|
||||
}
|
||||
EditorUtility.DisplayDialog($"验证发现 {issues.Count} 个问题", msg, "确定");
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckCycle(TalkSceneSO current, HashSet<TalkSceneSO> visited, HashSet<TalkSceneSO> visiting, HashSet<TalkSceneSO> soSet, List<string> issues)
|
||||
{
|
||||
visited.Add(current);
|
||||
visiting.Add(current);
|
||||
|
||||
if (current.exits != null)
|
||||
{
|
||||
foreach (var exit in current.exits)
|
||||
{
|
||||
var next = exit.targetScene;
|
||||
if (next == null || !soSet.Contains(next)) continue;
|
||||
|
||||
if (visiting.Contains(next))
|
||||
{
|
||||
issues.Add($"循环引用: {current.name} -> {next.name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!visited.Contains(next))
|
||||
{
|
||||
CheckCycle(next, visited, visiting, soSet, issues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visiting.Remove(current);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 913b090485a3d844fab3e436e7bf4f57
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,282 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AibisDream;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace AibisDream.SystemEditor
|
||||
{
|
||||
public class ChapterNode : Node
|
||||
{
|
||||
public TalkSceneSO SceneSo { get; }
|
||||
public Port InputPort { get; private set; }
|
||||
|
||||
private const float PortLabelMaxWidth = 120f;
|
||||
|
||||
public ChapterNode(TalkSceneSO so)
|
||||
{
|
||||
SceneSo = so ?? throw new ArgumentNullException(nameof(so));
|
||||
|
||||
// 节点整体放大
|
||||
style.minWidth = 240;
|
||||
style.paddingTop = 8;
|
||||
style.paddingBottom = 8;
|
||||
style.paddingLeft = 10;
|
||||
style.paddingRight = 10;
|
||||
|
||||
UpdateTitle();
|
||||
UpdateColor();
|
||||
BuildInputPort();
|
||||
BuildOutputPorts();
|
||||
BuildSubtitle();
|
||||
BuildAddExitButton();
|
||||
SetPosition(new Rect(so.graphPosition, Vector2.zero));
|
||||
|
||||
// 放大标题
|
||||
var titleLabel = this.Q<Label>("title-label");
|
||||
if (titleLabel != null)
|
||||
{
|
||||
titleLabel.style.fontSize = 16;
|
||||
titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateTitle()
|
||||
{
|
||||
title = string.IsNullOrEmpty(SceneSo.title) ? SceneSo.name : SceneSo.title;
|
||||
}
|
||||
|
||||
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
|
||||
{
|
||||
base.BuildContextualMenu(evt);
|
||||
|
||||
if (SceneSo.exits != null && SceneSo.exits.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < SceneSo.exits.Count; i++)
|
||||
{
|
||||
var exit = SceneSo.exits[i];
|
||||
var name = string.IsNullOrEmpty(exit.exitName) ? $"出口{i}" : exit.exitName;
|
||||
var idx = i;
|
||||
evt.menu.AppendAction($"删除出口/{name}", _ => RemoveExitAt(idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateColor()
|
||||
{
|
||||
style.backgroundColor = new StyleColor(SceneSo.nodeColor);
|
||||
}
|
||||
|
||||
private void BuildInputPort()
|
||||
{
|
||||
InputPort = Port.Create<Edge>(
|
||||
Orientation.Horizontal,
|
||||
Direction.Input,
|
||||
Port.Capacity.Single,
|
||||
typeof(TalkSceneSO));
|
||||
InputPort.portName = "▶";
|
||||
InputPort.style.fontSize = 14;
|
||||
InputPort.AddManipulator(new EdgeConnector<Edge>(new EmptyEdgeListener()));
|
||||
inputContainer.Add(InputPort);
|
||||
}
|
||||
|
||||
private void BuildOutputPorts()
|
||||
{
|
||||
outputContainer.Clear();
|
||||
if (SceneSo.exits == null) return;
|
||||
|
||||
for (int i = 0; i < SceneSo.exits.Count; i++)
|
||||
{
|
||||
var exit = SceneSo.exits[i];
|
||||
var port = CreateOutputPort(exit, i);
|
||||
outputContainer.Add(port);
|
||||
}
|
||||
}
|
||||
|
||||
private Port CreateOutputPort(SceneExit exit, int index)
|
||||
{
|
||||
var port = Port.Create<Edge>(
|
||||
Orientation.Horizontal,
|
||||
Direction.Output,
|
||||
Port.Capacity.Single,
|
||||
typeof(TalkSceneSO));
|
||||
|
||||
port.portName = string.IsNullOrEmpty(exit.exitName) ? $"出口{index}" : exit.exitName;
|
||||
port.userData = index;
|
||||
|
||||
// 限制端口标签宽度并放大字体
|
||||
var label = port.Q<Label>("type");
|
||||
if (label != null)
|
||||
{
|
||||
label.style.maxWidth = PortLabelMaxWidth;
|
||||
label.style.fontSize = 12;
|
||||
}
|
||||
|
||||
port.AddManipulator(new EdgeConnector<Edge>(new EmptyEdgeListener()));
|
||||
|
||||
port.RegisterCallback<ContextualMenuPopulateEvent>((evt) =>
|
||||
{
|
||||
var currentIndex = port.userData is int idx ? idx : -1;
|
||||
evt.menu.AppendAction("删除出口", (_) => RemoveExitAt(currentIndex));
|
||||
}, TrickleDown.TrickleDown);
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
private void BuildSubtitle()
|
||||
{
|
||||
var subtitleLabel = new Label($"{SceneSo.name}")
|
||||
{
|
||||
style =
|
||||
{
|
||||
fontSize = 12,
|
||||
unityTextAlign = TextAnchor.MiddleCenter,
|
||||
color = new Color(0.8f, 0.8f, 0.8f),
|
||||
paddingTop = 4,
|
||||
paddingBottom = 2
|
||||
}
|
||||
};
|
||||
extensionContainer.Add(subtitleLabel);
|
||||
RefreshExpandedState();
|
||||
}
|
||||
|
||||
private void BuildAddExitButton()
|
||||
{
|
||||
var removeButton = new Button(() => RemoveLastExit())
|
||||
{
|
||||
text = "-",
|
||||
style =
|
||||
{
|
||||
width = 26,
|
||||
height = 22,
|
||||
fontSize = 16,
|
||||
unityFontStyleAndWeight = FontStyle.Bold,
|
||||
marginRight = 4
|
||||
}
|
||||
};
|
||||
titleButtonContainer.Add(removeButton);
|
||||
|
||||
var addButton = new Button(() => AddExit())
|
||||
{
|
||||
text = "+",
|
||||
style = { width = 26, height = 22, fontSize = 16, unityFontStyleAndWeight = FontStyle.Bold }
|
||||
};
|
||||
titleButtonContainer.Add(addButton);
|
||||
}
|
||||
|
||||
private void RemoveLastExit()
|
||||
{
|
||||
if (SceneSo.exits == null || SceneSo.exits.Count == 0) return;
|
||||
RemoveExitAt(SceneSo.exits.Count - 1);
|
||||
}
|
||||
|
||||
public void AddExit()
|
||||
{
|
||||
Undo.RecordObject(SceneSo, "Add Exit");
|
||||
if (SceneSo.exits == null)
|
||||
SceneSo.exits = new System.Collections.Generic.List<SceneExit>();
|
||||
|
||||
var newExit = new SceneExit
|
||||
{
|
||||
exitName = $"exit_{SceneSo.exits.Count}"
|
||||
};
|
||||
SceneSo.exits.Add(newExit);
|
||||
EditorUtility.SetDirty(SceneSo);
|
||||
|
||||
var newPort = CreateOutputPort(newExit, SceneSo.exits.Count - 1);
|
||||
outputContainer.Add(newPort);
|
||||
RefreshPorts();
|
||||
}
|
||||
|
||||
public void RemoveExitAt(int index)
|
||||
{
|
||||
if (SceneSo.exits == null || index < 0 || index >= SceneSo.exits.Count) return;
|
||||
|
||||
// 断开并移除该端口的所有边
|
||||
var portToRemove = GetOutputPort(index);
|
||||
if (portToRemove != null)
|
||||
{
|
||||
var edges = portToRemove.connections.ToList();
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
edge.input?.Disconnect(edge);
|
||||
edge.output?.Disconnect(edge);
|
||||
edge.RemoveFromHierarchy();
|
||||
}
|
||||
portToRemove.RemoveFromHierarchy();
|
||||
}
|
||||
|
||||
Undo.RecordObject(SceneSo, "Remove Exit");
|
||||
SceneSo.exits.RemoveAt(index);
|
||||
EditorUtility.SetDirty(SceneSo);
|
||||
|
||||
// 更新后续端口的 userData 和 portName
|
||||
for (int i = index; i < SceneSo.exits.Count; i++)
|
||||
{
|
||||
var port = GetOutputPort(i);
|
||||
if (port != null)
|
||||
{
|
||||
port.userData = i;
|
||||
var exit = SceneSo.exits[i];
|
||||
port.portName = string.IsNullOrEmpty(exit.exitName) ? $"出口{i}" : exit.exitName;
|
||||
}
|
||||
}
|
||||
|
||||
RefreshPorts();
|
||||
}
|
||||
|
||||
public void RefreshOutputPorts()
|
||||
{
|
||||
// 先断开所有输出端口的边
|
||||
var edgesToRemove = outputContainer.Children()
|
||||
.OfType<Port>()
|
||||
.SelectMany(p => p.connections.ToList())
|
||||
.ToList();
|
||||
|
||||
foreach (var edge in edgesToRemove)
|
||||
{
|
||||
edge.input?.Disconnect(edge);
|
||||
edge.output?.Disconnect(edge);
|
||||
edge.RemoveFromHierarchy();
|
||||
}
|
||||
|
||||
BuildOutputPorts();
|
||||
RefreshPorts();
|
||||
}
|
||||
|
||||
public Port GetOutputPort(int index)
|
||||
{
|
||||
return outputContainer.Children().OfType<Port>().ElementAtOrDefault(index);
|
||||
}
|
||||
|
||||
public override void SetPosition(Rect newPos)
|
||||
{
|
||||
base.SetPosition(newPos);
|
||||
if (SceneSo != null)
|
||||
{
|
||||
SceneSo.graphPosition = newPos.position;
|
||||
EditorUtility.SetDirty(SceneSo);
|
||||
}
|
||||
}
|
||||
|
||||
public void SyncPositionToSo()
|
||||
{
|
||||
if (SceneSo == null) return;
|
||||
var pos = GetPosition();
|
||||
if (SceneSo.graphPosition != pos.position)
|
||||
{
|
||||
SceneSo.graphPosition = pos.position;
|
||||
EditorUtility.SetDirty(SceneSo);
|
||||
}
|
||||
}
|
||||
|
||||
private class EmptyEdgeListener : IEdgeConnectorListener
|
||||
{
|
||||
public void OnDropOutsidePort(Edge edge, Vector2 position) { }
|
||||
public void OnDrop(GraphView graphView, Edge edge) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3a4f1beaaa3a5740b1904f8b9b926b7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,32 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.SystemEditor
|
||||
{
|
||||
[CustomEditor(typeof(TalkSceneSO))]
|
||||
public class TalkSceneSOEditor : Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("exits"), true);
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("yarnProject"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("firstSceneName"));
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("显示信息", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("title"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("titleKey"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("description"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("coverPic"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("chapter"));
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("编辑器设置", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("nodeColor"), new GUIContent("节点颜色"));
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4988e2ee30c565e40989210408a234e3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e244d3d62dec3f4f881f7e681da8c03
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "AibisDream.FrameAnimation.Editor",
|
||||
"rootNamespace": "AibisDream.FrameAnimation.Editor",
|
||||
"references": [
|
||||
"AibisDream.FrameAnimation.Runtime",
|
||||
"Unity.2D.Sprite.Editor"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1ffe139ca2417941a09835467d1e442
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,198 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal static class AsepriteJsonParser
|
||||
{
|
||||
public static bool TryParse(
|
||||
string json,
|
||||
string sourceId,
|
||||
out AsepriteSourceDocument document,
|
||||
out FrameAnimationImportIssue issue)
|
||||
{
|
||||
document = null;
|
||||
issue = null;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
issue = Error(FrameAnimationImportIssueCode.JsonInvalid, sourceId, "Aseprite JSON 为空。");
|
||||
return false;
|
||||
}
|
||||
|
||||
var root = JObject.Parse(json);
|
||||
var framesToken = root["frames"];
|
||||
if (framesToken == null ||
|
||||
(framesToken.Type != JTokenType.Object && framesToken.Type != JTokenType.Array))
|
||||
{
|
||||
issue = Error(FrameAnimationImportIssueCode.FramesMissing, sourceId, "JSON 缺少 Object 或 Array 格式的 frames。");
|
||||
return false;
|
||||
}
|
||||
|
||||
var frames = ParseFrames(framesToken);
|
||||
var meta = root["meta"] as JObject;
|
||||
var size = ParseSize(meta?["size"]);
|
||||
var imageName = meta?.Value<string>("image") ?? string.Empty;
|
||||
var tags = ParseTags(meta?["frameTags"]);
|
||||
document = new AsepriteSourceDocument(frames, tags, size, imageName);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
issue = Error(
|
||||
FrameAnimationImportIssueCode.JsonInvalid,
|
||||
sourceId,
|
||||
$"Aseprite JSON 解析失败:{exception.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryExpandTag(
|
||||
AsepriteSourceTag tag,
|
||||
int frameCount,
|
||||
string sourceId,
|
||||
out IReadOnlyList<int> indices,
|
||||
out FrameAnimationImportIssue issue)
|
||||
{
|
||||
indices = Array.Empty<int>();
|
||||
issue = null;
|
||||
if (tag == null || tag.From < 0 || tag.To < tag.From || tag.To >= frameCount)
|
||||
{
|
||||
issue = Error(
|
||||
FrameAnimationImportIssueCode.TagRangeInvalid,
|
||||
sourceId,
|
||||
$"Tag '{tag?.Name}' 的范围 {tag?.From}..{tag?.To} 无效。");
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = new List<int>();
|
||||
switch ((tag.Direction ?? string.Empty).ToLowerInvariant())
|
||||
{
|
||||
case "forward":
|
||||
AddAscending(result, tag.From, tag.To);
|
||||
break;
|
||||
case "reverse":
|
||||
AddDescending(result, tag.To, tag.From);
|
||||
break;
|
||||
case "pingpong":
|
||||
AddAscending(result, tag.From, tag.To);
|
||||
AddDescending(result, tag.To - 1, tag.From + 1);
|
||||
break;
|
||||
case "pingpong_reverse":
|
||||
AddDescending(result, tag.To, tag.From);
|
||||
AddAscending(result, tag.From + 1, tag.To - 1);
|
||||
break;
|
||||
default:
|
||||
issue = Error(
|
||||
FrameAnimationImportIssueCode.TagDirectionInvalid,
|
||||
sourceId,
|
||||
$"Tag '{tag.Name}' 使用了不支持的 direction '{tag.Direction}'。");
|
||||
return false;
|
||||
}
|
||||
|
||||
indices = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<AsepriteSourceFrame> ParseFrames(JToken token)
|
||||
{
|
||||
var frames = new List<AsepriteSourceFrame>();
|
||||
if (token is JObject objectFrames)
|
||||
{
|
||||
foreach (var property in objectFrames.Properties())
|
||||
{
|
||||
frames.Add(ParseFrame(property.Name, property.Value));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in (JArray)token)
|
||||
{
|
||||
frames.Add(ParseFrame(item.Value<string>("filename") ?? string.Empty, item));
|
||||
}
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
private static AsepriteSourceFrame ParseFrame(string frameName, JToken token)
|
||||
{
|
||||
return new AsepriteSourceFrame(
|
||||
frameName,
|
||||
ParseRect(token["frame"]),
|
||||
token.Value<int?>("duration") ?? 0,
|
||||
token.Value<bool?>("rotated") ?? false,
|
||||
token.Value<bool?>("trimmed") ?? false,
|
||||
ParseRect(token["spriteSourceSize"]),
|
||||
ParseSize(token["sourceSize"]));
|
||||
}
|
||||
|
||||
private static List<AsepriteSourceTag> ParseTags(JToken token)
|
||||
{
|
||||
var tags = new List<AsepriteSourceTag>();
|
||||
if (!(token is JArray array))
|
||||
{
|
||||
return tags;
|
||||
}
|
||||
|
||||
foreach (var item in array)
|
||||
{
|
||||
tags.Add(new AsepriteSourceTag(
|
||||
item.Value<string>("name") ?? string.Empty,
|
||||
item.Value<int?>("from") ?? -1,
|
||||
item.Value<int?>("to") ?? -1,
|
||||
item.Value<string>("direction") ?? string.Empty));
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
private static RectInt ParseRect(JToken token)
|
||||
{
|
||||
return token == null
|
||||
? default
|
||||
: new RectInt(
|
||||
token.Value<int?>("x") ?? 0,
|
||||
token.Value<int?>("y") ?? 0,
|
||||
token.Value<int?>("w") ?? 0,
|
||||
token.Value<int?>("h") ?? 0);
|
||||
}
|
||||
|
||||
private static Vector2Int ParseSize(JToken token)
|
||||
{
|
||||
return token == null
|
||||
? default
|
||||
: new Vector2Int(token.Value<int?>("w") ?? 0, token.Value<int?>("h") ?? 0);
|
||||
}
|
||||
|
||||
private static void AddAscending(List<int> target, int from, int to)
|
||||
{
|
||||
for (var index = from; index <= to; index++)
|
||||
{
|
||||
target.Add(index);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddDescending(List<int> target, int from, int to)
|
||||
{
|
||||
for (var index = from; index >= to; index--)
|
||||
{
|
||||
target.Add(index);
|
||||
}
|
||||
}
|
||||
|
||||
private static FrameAnimationImportIssue Error(
|
||||
FrameAnimationImportIssueCode code,
|
||||
string sourceId,
|
||||
string message)
|
||||
{
|
||||
return new FrameAnimationImportIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
code,
|
||||
sourceId,
|
||||
sourceId,
|
||||
message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a21095473b1495440acab244f9e470c6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("AibisDream.FrameAnimation.Tests.EditMode")]
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8edeae42e1831084cbf05db9d9e5d1e2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,307 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal sealed class FrameAnimationTextInputWindow : EditorWindow
|
||||
{
|
||||
private string label;
|
||||
private string value;
|
||||
private string warning;
|
||||
private Func<string, string> validate;
|
||||
private Action<string> confirm;
|
||||
private Vector2 scroll;
|
||||
|
||||
public static void Show(
|
||||
string title,
|
||||
string label,
|
||||
string initialValue,
|
||||
string warning,
|
||||
Func<string, string> validate,
|
||||
Action<string> confirm)
|
||||
{
|
||||
var window = CreateInstance<FrameAnimationTextInputWindow>();
|
||||
window.titleContent = new GUIContent(title);
|
||||
window.label = label;
|
||||
window.value = initialValue ?? string.Empty;
|
||||
window.warning = warning ?? string.Empty;
|
||||
window.validate = validate;
|
||||
window.confirm = confirm;
|
||||
window.minSize = new Vector2(460f, 220f);
|
||||
window.maxSize = new Vector2(700f, 420f);
|
||||
window.ShowUtility();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
scroll = EditorGUILayout.BeginScrollView(scroll);
|
||||
EditorGUILayout.LabelField(label, EditorStyles.boldLabel);
|
||||
GUI.SetNextControlName("FrameAnimationTextInput");
|
||||
value = EditorGUILayout.TextField(value);
|
||||
if (!string.IsNullOrEmpty(warning))
|
||||
{
|
||||
EditorGUILayout.HelpBox(warning, MessageType.Warning);
|
||||
}
|
||||
var error = validate?.Invoke(value) ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
EditorGUILayout.HelpBox(error, MessageType.Error);
|
||||
}
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button("取消", GUILayout.Width(90f)))
|
||||
{
|
||||
Close();
|
||||
}
|
||||
using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(error)))
|
||||
{
|
||||
if (GUILayout.Button("确认", GUILayout.Width(90f)))
|
||||
{
|
||||
var callback = confirm;
|
||||
var result = value;
|
||||
Close();
|
||||
callback?.Invoke(result);
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
EditorApplication.delayCall += FocusInput;
|
||||
}
|
||||
|
||||
private void FocusInput()
|
||||
{
|
||||
if (this != null)
|
||||
{
|
||||
Focus();
|
||||
EditorGUI.FocusTextInControl("FrameAnimationTextInput");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationManualClipWindow : EditorWindow
|
||||
{
|
||||
private FrameAnimationGraph graph;
|
||||
private FrameClip source;
|
||||
private string id = "NewClip";
|
||||
private string displayName = "New Clip";
|
||||
private bool external;
|
||||
private Action<FrameClip> completed;
|
||||
|
||||
public static void Show(
|
||||
FrameAnimationGraph graph,
|
||||
FrameClip source,
|
||||
Action<FrameClip> completed)
|
||||
{
|
||||
var window = CreateInstance<FrameAnimationManualClipWindow>();
|
||||
window.titleContent = new GUIContent(source == null ? "Create Manual Clip" : "Copy As Manual Clip");
|
||||
window.graph = graph;
|
||||
window.source = source;
|
||||
window.completed = completed;
|
||||
var baseId = source != null ? source.Id + "_Manual" : "NewClip";
|
||||
window.id = MakeUniqueId(graph, baseId);
|
||||
window.displayName = source != null ? source.DisplayName + " Manual" : window.id;
|
||||
window.minSize = new Vector2(460f, 260f);
|
||||
window.maxSize = new Vector2(700f, 420f);
|
||||
window.ShowUtility();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.LabelField(source == null ? "创建 Manual Clip" : "复制为 Manual Clip", EditorStyles.boldLabel);
|
||||
id = EditorGUILayout.TextField("ID", id);
|
||||
displayName = EditorGUILayout.TextField("Display Name", displayName);
|
||||
external = EditorGUILayout.ToggleLeft("保存为外部独立 .asset(默认保存为 Graph sub-asset)", external);
|
||||
var error = FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id)
|
||||
? string.Empty
|
||||
: "ID 为空或与当前 Graph 的 Clip / Flow 冲突。";
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
EditorGUILayout.HelpBox(error, MessageType.Error);
|
||||
}
|
||||
if (source != null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"帧表将深拷贝,但 Sprite 仍复用原资源;ImportSource 关联会被清除。",
|
||||
MessageType.Info);
|
||||
}
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button("取消", GUILayout.Width(90f)))
|
||||
{
|
||||
Close();
|
||||
}
|
||||
using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(error)))
|
||||
{
|
||||
if (GUILayout.Button("创建", GUILayout.Width(90f)))
|
||||
{
|
||||
Create();
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void Create()
|
||||
{
|
||||
var path = string.Empty;
|
||||
if (external)
|
||||
{
|
||||
path = EditorUtility.SaveFilePanelInProject(
|
||||
"保存外部 Manual Clip",
|
||||
id,
|
||||
"asset",
|
||||
"请选择 Assets 下的保存位置。");
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var succeeded = source == null
|
||||
? FrameAnimationAssetOperations.CreateManualClip(graph, id, displayName, path, out var result, out var error)
|
||||
: FrameAnimationAssetOperations.CopyToManual(graph, source, id, displayName, path, out result, out error);
|
||||
if (!succeeded)
|
||||
{
|
||||
EditorUtility.DisplayDialog("操作失败", error, "确定");
|
||||
return;
|
||||
}
|
||||
var callback = completed;
|
||||
Close();
|
||||
callback?.Invoke(result);
|
||||
}
|
||||
|
||||
private static string MakeUniqueId(FrameAnimationGraph graph, string baseId)
|
||||
{
|
||||
var candidate = baseId;
|
||||
var suffix = 2;
|
||||
while (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, candidate))
|
||||
{
|
||||
candidate = baseId + suffix++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationExistingClipWindow : EditorWindow
|
||||
{
|
||||
private FrameAnimationGraph graph;
|
||||
private FrameClip clip;
|
||||
private Action<FrameClip> completed;
|
||||
|
||||
public static void Show(FrameAnimationGraph graph, Action<FrameClip> completed)
|
||||
{
|
||||
var window = CreateInstance<FrameAnimationExistingClipWindow>();
|
||||
window.titleContent = new GUIContent("Add Existing Manual Clip");
|
||||
window.graph = graph;
|
||||
window.completed = completed;
|
||||
window.minSize = new Vector2(460f, 180f);
|
||||
window.maxSize = new Vector2(640f, 260f);
|
||||
window.ShowUtility();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.LabelField("添加外部 Manual Clip", EditorStyles.boldLabel);
|
||||
clip = (FrameClip)EditorGUILayout.ObjectField("FrameClip", clip, typeof(FrameClip), false);
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button("取消", GUILayout.Width(90f)))
|
||||
{
|
||||
Close();
|
||||
}
|
||||
using (new EditorGUI.DisabledScope(clip == null))
|
||||
{
|
||||
if (GUILayout.Button("添加", GUILayout.Width(90f)))
|
||||
{
|
||||
if (!FrameAnimationAssetOperations.AddExistingManualClip(graph, clip, out var error))
|
||||
{
|
||||
EditorUtility.DisplayDialog("添加失败", error, "确定");
|
||||
return;
|
||||
}
|
||||
var callback = completed;
|
||||
var result = clip;
|
||||
Close();
|
||||
callback?.Invoke(result);
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationFlowWindow : EditorWindow
|
||||
{
|
||||
private FrameAnimationGraph graph;
|
||||
private AnimationNode entry;
|
||||
private string id;
|
||||
private string displayName;
|
||||
private Action<AnimationFlow> completed;
|
||||
|
||||
internal static void Show(
|
||||
FrameAnimationGraph graph,
|
||||
AnimationNode entry,
|
||||
Action<AnimationFlow> completed)
|
||||
{
|
||||
var window = CreateInstance<FrameAnimationFlowWindow>();
|
||||
window.titleContent = new GUIContent("Create Animation Flow");
|
||||
window.graph = graph;
|
||||
window.entry = entry;
|
||||
window.id = FrameAnimationGraphMutationService.MakeUniqueFlowId(graph, entry);
|
||||
window.displayName = window.id;
|
||||
window.completed = completed;
|
||||
window.minSize = new Vector2(460f, 230f);
|
||||
window.maxSize = new Vector2(700f, 360f);
|
||||
window.ShowUtility();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.LabelField("从选中节点创建 Flow", EditorStyles.boldLabel);
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUILayout.TextField("Entry Node", entry?.DisplayName ?? string.Empty);
|
||||
}
|
||||
id = EditorGUILayout.TextField("ID", id);
|
||||
displayName = EditorGUILayout.TextField("Display Name", displayName);
|
||||
var idError = FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id)
|
||||
? string.Empty
|
||||
: "ID 为空或与当前 Graph 中的 Clip / Flow 冲突。";
|
||||
var owner = graph?.Flows.FirstOrDefault(flow => flow != null && flow.EntryNodeId == entry?.InternalId);
|
||||
if (owner != null)
|
||||
{
|
||||
idError = $"该节点已是 Flow '{owner.Id}' 的入口。";
|
||||
}
|
||||
if (!string.IsNullOrEmpty(idError))
|
||||
{
|
||||
EditorGUILayout.HelpBox(idError, MessageType.Error);
|
||||
}
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button("取消", GUILayout.Width(90f))) Close();
|
||||
using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(idError)))
|
||||
{
|
||||
if (GUILayout.Button("创建", GUILayout.Width(90f)))
|
||||
{
|
||||
if (!FrameAnimationGraphMutationService.CreateFlow(
|
||||
graph, entry, id, displayName, out var flow, out var error))
|
||||
{
|
||||
EditorUtility.DisplayDialog("创建 Flow 失败", error, "确定");
|
||||
return;
|
||||
}
|
||||
var callback = completed;
|
||||
Close();
|
||||
callback?.Invoke(flow);
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8f58e7b80263c24087a98874796e91d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,744 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal enum FrameAnimationEditorSelectionKind
|
||||
{
|
||||
None,
|
||||
Graph,
|
||||
Clip,
|
||||
Flow,
|
||||
Source,
|
||||
Node,
|
||||
Edge
|
||||
}
|
||||
|
||||
internal readonly struct FrameAnimationEditorSelection
|
||||
{
|
||||
public FrameAnimationEditorSelectionKind Kind { get; }
|
||||
public object Value { get; }
|
||||
|
||||
public FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind kind, object value)
|
||||
{
|
||||
Kind = kind;
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
internal enum FrameAnimationClipFilter
|
||||
{
|
||||
All,
|
||||
Manual,
|
||||
Imported,
|
||||
Normal,
|
||||
Missing,
|
||||
Referenced,
|
||||
Unused
|
||||
}
|
||||
|
||||
internal enum FrameAnimationClipSort
|
||||
{
|
||||
Name,
|
||||
Source,
|
||||
FrameCount,
|
||||
Duration,
|
||||
Missing,
|
||||
ReferenceCount
|
||||
}
|
||||
|
||||
internal static class FrameAnimationAssetReferenceIndex
|
||||
{
|
||||
public static IReadOnlyList<FrameAnimationGraph> FindGraphsReferencing(FrameClip clip)
|
||||
{
|
||||
if (clip == null)
|
||||
{
|
||||
return Array.Empty<FrameAnimationGraph>();
|
||||
}
|
||||
|
||||
return AssetDatabase.FindAssets("t:FrameAnimationGraph")
|
||||
.Select(guid => AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(AssetDatabase.GUIDToAssetPath(guid)))
|
||||
.Where(graph => graph != null && graph.Clips.Any(item => item == clip))
|
||||
.Distinct()
|
||||
.OrderBy(graph => AssetDatabase.GetAssetPath(graph), StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationResourceQuery
|
||||
{
|
||||
public static IReadOnlyList<FrameClip> QueryClips(
|
||||
FrameAnimationGraph graph,
|
||||
string search,
|
||||
FrameAnimationClipFilter filter,
|
||||
FrameAnimationClipSort sort)
|
||||
{
|
||||
if (graph == null)
|
||||
{
|
||||
return Array.Empty<FrameClip>();
|
||||
}
|
||||
|
||||
var sourceNames = graph.ImportSources.Where(source => source != null)
|
||||
.GroupBy(source => source.InternalId ?? string.Empty)
|
||||
.ToDictionary(group => group.Key, group => group.First().DisplayName ?? string.Empty);
|
||||
var query = graph.Clips.Where(clip => clip != null && MatchesSearch(clip, sourceNames, search));
|
||||
query = filter switch
|
||||
{
|
||||
FrameAnimationClipFilter.Manual => query.Where(clip => !clip.IsImported),
|
||||
FrameAnimationClipFilter.Imported => query.Where(clip => clip.IsImported),
|
||||
FrameAnimationClipFilter.Normal => query.Where(clip => !clip.IsImported || !clip.ImportInfo.IsMissingFromSource),
|
||||
FrameAnimationClipFilter.Missing => query.Where(clip => clip.IsImported && clip.ImportInfo.IsMissingFromSource),
|
||||
FrameAnimationClipFilter.Referenced => query.Where(clip => ReferenceCount(graph, clip) > 0),
|
||||
FrameAnimationClipFilter.Unused => query.Where(clip => ReferenceCount(graph, clip) == 0),
|
||||
_ => query
|
||||
};
|
||||
|
||||
IOrderedEnumerable<FrameClip> ordered = sort switch
|
||||
{
|
||||
FrameAnimationClipSort.Source => query.OrderBy(clip => SourceName(clip, sourceNames), StringComparer.Ordinal),
|
||||
FrameAnimationClipSort.FrameCount => query.OrderBy(clip => clip.FrameCount),
|
||||
FrameAnimationClipSort.Duration => query.OrderBy(clip => clip.TotalDurationMs),
|
||||
FrameAnimationClipSort.Missing => query.OrderBy(clip => clip.IsImported && clip.ImportInfo.IsMissingFromSource ? 1 : 0),
|
||||
FrameAnimationClipSort.ReferenceCount => query.OrderBy(clip => ReferenceCount(graph, clip)),
|
||||
_ => query.OrderBy(clip => clip.DisplayName ?? clip.Id, StringComparer.Ordinal)
|
||||
};
|
||||
return ordered.ThenBy(clip => clip.Id, StringComparer.Ordinal)
|
||||
.ThenBy(clip => AssetDatabase.GetAssetPath(clip), StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static int ReferenceCount(FrameAnimationGraph graph, FrameClip clip)
|
||||
{
|
||||
return graph?.Nodes.Count(node => node != null && node.ClipId == clip?.Id) ?? 0;
|
||||
}
|
||||
|
||||
public static string SourceName(
|
||||
FrameClip clip,
|
||||
IReadOnlyDictionary<string, string> sourceNames = null)
|
||||
{
|
||||
if (clip == null || !clip.IsImported)
|
||||
{
|
||||
return "Manual";
|
||||
}
|
||||
var sourceId = clip.ImportInfo?.ImportSourceId;
|
||||
if (sourceNames != null && !string.IsNullOrEmpty(sourceId) &&
|
||||
sourceNames.TryGetValue(sourceId, out var name))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
return sourceId ?? string.Empty;
|
||||
}
|
||||
|
||||
private static bool MatchesSearch(
|
||||
FrameClip clip,
|
||||
IReadOnlyDictionary<string, string> sourceNames,
|
||||
string search)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return Contains(clip.Id, search) || Contains(clip.DisplayName, search) ||
|
||||
Contains(clip.ImportInfo?.SourceTagName, search) ||
|
||||
Contains(SourceName(clip, sourceNames), search);
|
||||
}
|
||||
|
||||
private static bool Contains(string value, string search)
|
||||
{
|
||||
return !string.IsNullOrEmpty(value) && value.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationAssetOperations
|
||||
{
|
||||
public static bool IsPlayableIdAvailable(
|
||||
FrameAnimationGraph graph,
|
||||
string id,
|
||||
FrameClip exceptClip = null,
|
||||
AnimationFlow exceptFlow = null)
|
||||
{
|
||||
return graph != null && !string.IsNullOrWhiteSpace(id) &&
|
||||
graph.Clips.All(clip => clip == null || clip == exceptClip || clip.Id != id) &&
|
||||
graph.Flows.All(flow => flow == null || flow == exceptFlow || flow.Id != id);
|
||||
}
|
||||
|
||||
public static bool CreateManualClip(
|
||||
FrameAnimationGraph graph,
|
||||
string id,
|
||||
string displayName,
|
||||
string externalPath,
|
||||
out FrameClip clip,
|
||||
out string error)
|
||||
{
|
||||
clip = null;
|
||||
error = string.Empty;
|
||||
if (!IsPlayableIdAvailable(graph, id))
|
||||
{
|
||||
error = "Clip id 为空或与现有 Clip / Flow 冲突。";
|
||||
return false;
|
||||
}
|
||||
|
||||
Undo.IncrementCurrentGroup();
|
||||
var group = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Create Manual Frame Clip");
|
||||
try
|
||||
{
|
||||
Undo.RecordObject(graph, "Add Manual Frame Clip");
|
||||
clip = ScriptableObject.CreateInstance<FrameClip>();
|
||||
clip.name = id;
|
||||
clip.Configure(id, string.IsNullOrWhiteSpace(displayName) ? id : displayName,
|
||||
Array.Empty<FrameAnimationFrame>(), 1f,
|
||||
graph.Settings.NewManualClipDefaultEndBehavior);
|
||||
if (string.IsNullOrWhiteSpace(externalPath))
|
||||
{
|
||||
AssetDatabase.AddObjectToAsset(clip, graph);
|
||||
}
|
||||
else
|
||||
{
|
||||
AssetDatabase.CreateAsset(clip, externalPath);
|
||||
}
|
||||
Undo.RegisterCreatedObjectUndo(clip, "Create Manual Frame Clip");
|
||||
graph.AddClip(clip);
|
||||
EditorUtility.SetDirty(graph);
|
||||
EditorUtility.SetDirty(clip);
|
||||
AssetDatabase.SaveAssets();
|
||||
Undo.CollapseUndoOperations(group);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Undo.RevertAllDownToGroup(group);
|
||||
clip = null;
|
||||
error = exception.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool AddExistingManualClip(
|
||||
FrameAnimationGraph graph,
|
||||
FrameClip clip,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || clip == null)
|
||||
{
|
||||
error = "Graph 或 Clip 为空。";
|
||||
return false;
|
||||
}
|
||||
if (clip.IsImported || AssetDatabase.IsSubAsset(clip) ||
|
||||
string.IsNullOrEmpty(AssetDatabase.GetAssetPath(clip)))
|
||||
{
|
||||
error = "只能添加独立 .asset 形式的 Manual Clip。";
|
||||
return false;
|
||||
}
|
||||
if (graph.Clips.Contains(clip))
|
||||
{
|
||||
error = "当前 Graph 已引用该 Clip。";
|
||||
return false;
|
||||
}
|
||||
if (!IsPlayableIdAvailable(graph, clip.Id))
|
||||
{
|
||||
error = $"playable id '{clip.Id}' 与当前 Graph 中的 Clip / Flow 冲突。";
|
||||
return false;
|
||||
}
|
||||
|
||||
Undo.RecordObject(graph, "Add Existing Manual Frame Clip");
|
||||
graph.AddClip(clip);
|
||||
EditorUtility.SetDirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CopyToManual(
|
||||
FrameAnimationGraph graph,
|
||||
FrameClip source,
|
||||
string id,
|
||||
string displayName,
|
||||
string externalPath,
|
||||
out FrameClip copy,
|
||||
out string error)
|
||||
{
|
||||
copy = null;
|
||||
error = string.Empty;
|
||||
if (source == null || !IsPlayableIdAvailable(graph, id))
|
||||
{
|
||||
error = "来源为空,或新 id 与现有 Clip / Flow 冲突。";
|
||||
return false;
|
||||
}
|
||||
|
||||
Undo.IncrementCurrentGroup();
|
||||
var group = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Copy Frame Clip As Manual");
|
||||
try
|
||||
{
|
||||
Undo.RecordObject(graph, "Add Manual Frame Clip Copy");
|
||||
copy = ScriptableObject.CreateInstance<FrameClip>();
|
||||
copy.name = id;
|
||||
copy.Configure(id,
|
||||
string.IsNullOrWhiteSpace(displayName) ? source.DisplayName : displayName,
|
||||
source.Frames.Select(frame => frame == null
|
||||
? null
|
||||
: new FrameAnimationFrame(frame.Sprite, frame.DurationMs, frame.FrameName, frame.SourceIndex)),
|
||||
source.Speed,
|
||||
source.DefaultEndBehavior);
|
||||
if (string.IsNullOrWhiteSpace(externalPath))
|
||||
{
|
||||
AssetDatabase.AddObjectToAsset(copy, graph);
|
||||
}
|
||||
else
|
||||
{
|
||||
AssetDatabase.CreateAsset(copy, externalPath);
|
||||
}
|
||||
Undo.RegisterCreatedObjectUndo(copy, "Create Manual Frame Clip Copy");
|
||||
graph.AddClip(copy);
|
||||
EditorUtility.SetDirty(graph);
|
||||
EditorUtility.SetDirty(copy);
|
||||
AssetDatabase.SaveAssets();
|
||||
Undo.CollapseUndoOperations(group);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Undo.RevertAllDownToGroup(group);
|
||||
copy = null;
|
||||
error = exception.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool RenameClip(
|
||||
FrameAnimationGraph graph,
|
||||
FrameClip clip,
|
||||
string newId,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || clip == null || !graph.Clips.Contains(clip))
|
||||
{
|
||||
error = "Clip 不属于当前 Graph。";
|
||||
return false;
|
||||
}
|
||||
var owners = FrameAnimationAssetReferenceIndex.FindGraphsReferencing(clip);
|
||||
if (!AssetDatabase.IsSubAsset(clip) && owners.Count > 1)
|
||||
{
|
||||
error = "外部 Manual Clip 被多个 Graph 共享,禁止重命名:" +
|
||||
string.Join(", ", owners.Select(owner => owner.name));
|
||||
return false;
|
||||
}
|
||||
if (!IsPlayableIdAvailable(graph, newId, clip))
|
||||
{
|
||||
error = "新 id 为空或与现有 Clip / Flow 冲突。";
|
||||
return false;
|
||||
}
|
||||
|
||||
var oldId = clip.Id;
|
||||
Undo.IncrementCurrentGroup();
|
||||
var group = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Rename Frame Clip");
|
||||
Undo.RecordObject(graph, "Update Frame Clip References");
|
||||
Undo.RecordObject(clip, "Rename Frame Clip");
|
||||
clip.SetId(newId);
|
||||
foreach (var node in graph.Nodes.Where(node => node != null && node.ClipId == oldId))
|
||||
{
|
||||
node.SetClipId(newId);
|
||||
}
|
||||
if (graph.Settings.DefaultPlayableId == oldId)
|
||||
{
|
||||
graph.Settings.SetDefaultPlayableId(newId);
|
||||
}
|
||||
EditorUtility.SetDirty(graph);
|
||||
EditorUtility.SetDirty(clip);
|
||||
Undo.CollapseUndoOperations(group);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool RenameFlow(
|
||||
FrameAnimationGraph graph,
|
||||
AnimationFlow flow,
|
||||
string newId,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || flow == null || !graph.Flows.Contains(flow) ||
|
||||
!IsPlayableIdAvailable(graph, newId, exceptFlow: flow))
|
||||
{
|
||||
error = "Flow 不属于当前 Graph,或新 id 与现有 Clip / Flow 冲突。";
|
||||
return false;
|
||||
}
|
||||
|
||||
var oldId = flow.Id;
|
||||
Undo.RecordObject(graph, "Rename Animation Flow");
|
||||
flow.SetId(newId);
|
||||
if (graph.Settings.DefaultPlayableId == oldId)
|
||||
{
|
||||
graph.Settings.SetDefaultPlayableId(newId);
|
||||
}
|
||||
foreach (var data in graph.EditorData.FlowEditorData.Where(data => data != null && data.FlowId == oldId))
|
||||
{
|
||||
data.SetFlowId(newId);
|
||||
}
|
||||
EditorUtility.SetDirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool RenameGraph(FrameAnimationGraph graph, string newId, out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || string.IsNullOrWhiteSpace(newId))
|
||||
{
|
||||
error = "Graph id 不能为空。";
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Rename Frame Animation Graph");
|
||||
graph.SetId(newId);
|
||||
EditorUtility.SetDirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool RemoveClip(FrameAnimationGraph graph, FrameClip clip, out string error)
|
||||
{
|
||||
if (!CanRemoveClip(graph, clip, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Undo.IncrementCurrentGroup();
|
||||
var group = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Remove Frame Clip");
|
||||
Undo.RecordObject(graph, "Remove Frame Clip Reference");
|
||||
graph.RemoveClip(clip);
|
||||
if (AssetDatabase.IsSubAsset(clip) && AssetDatabase.GetAssetPath(clip) == AssetDatabase.GetAssetPath(graph))
|
||||
{
|
||||
Undo.DestroyObjectImmediate(clip);
|
||||
}
|
||||
EditorUtility.SetDirty(graph);
|
||||
AssetDatabase.SaveAssets();
|
||||
Undo.CollapseUndoOperations(group);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CanRemoveClip(FrameAnimationGraph graph, FrameClip clip, out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || clip == null || !graph.Clips.Contains(clip))
|
||||
{
|
||||
error = "Clip 不属于当前 Graph。";
|
||||
return false;
|
||||
}
|
||||
var nodeCount = graph.Nodes.Count(node => node != null && node.ClipId == clip.Id);
|
||||
if (nodeCount > 0)
|
||||
{
|
||||
error = $"Clip 仍被 {nodeCount} 个 AnimationNode 引用。";
|
||||
return false;
|
||||
}
|
||||
if (graph.Settings.DefaultPlayableId == clip.Id)
|
||||
{
|
||||
error = "Clip 仍被 defaultPlayableId 引用。";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool RemoveFlow(FrameAnimationGraph graph, AnimationFlow flow, out string error)
|
||||
{
|
||||
if (!CanRemoveFlow(graph, flow, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Remove Animation Flow");
|
||||
graph.RemoveFlow(flow);
|
||||
graph.EditorData.RemoveFlowData(flow.Id);
|
||||
EditorUtility.SetDirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CanRemoveFlow(FrameAnimationGraph graph, AnimationFlow flow, out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || flow == null || !graph.Flows.Contains(flow))
|
||||
{
|
||||
error = "Flow 不属于当前 Graph。";
|
||||
return false;
|
||||
}
|
||||
if (graph.Settings.DefaultPlayableId == flow.Id)
|
||||
{
|
||||
error = "Flow 仍被 defaultPlayableId 引用。";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool RemoveSource(
|
||||
FrameAnimationGraph graph,
|
||||
FrameAnimationImportSource source,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || source == null || !graph.ImportSources.Contains(source))
|
||||
{
|
||||
error = "ImportSource 不属于当前 Graph。";
|
||||
return false;
|
||||
}
|
||||
var clips = graph.Clips.Where(clip => clip != null &&
|
||||
clip.ImportInfo?.ImportSourceId == source.InternalId).ToArray();
|
||||
if (clips.Length > 0)
|
||||
{
|
||||
error = "ImportSource 仍关联 Imported Clip:" + string.Join(", ", clips.Select(clip => clip.Id));
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Remove Frame Animation ImportSource");
|
||||
graph.RemoveImportSource(source);
|
||||
EditorUtility.SetDirty(graph);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationEditorIssue
|
||||
{
|
||||
public FrameAnimationValidationSeverity Severity { get; }
|
||||
public string Code { get; }
|
||||
public FrameAnimationEditorSelection Selection { get; }
|
||||
public string Message { get; }
|
||||
public string Suggestion { get; }
|
||||
|
||||
public FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity severity,
|
||||
string code,
|
||||
FrameAnimationEditorSelection selection,
|
||||
string message,
|
||||
string suggestion)
|
||||
{
|
||||
Severity = severity;
|
||||
Code = code ?? string.Empty;
|
||||
Selection = selection;
|
||||
Message = message ?? string.Empty;
|
||||
Suggestion = suggestion ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationEditorValidationService
|
||||
{
|
||||
public static IReadOnlyList<FrameAnimationEditorIssue> Validate(
|
||||
FrameAnimationGraph graph,
|
||||
bool includeImports,
|
||||
out FrameAnimationImportPreview importPreview)
|
||||
{
|
||||
importPreview = null;
|
||||
if (graph == null)
|
||||
{
|
||||
return Array.Empty<FrameAnimationEditorIssue>();
|
||||
}
|
||||
|
||||
var issues = FrameAnimationGraphValidator.Validate(graph).Issues
|
||||
.Select(issue => new FrameAnimationEditorIssue(
|
||||
issue.Severity,
|
||||
issue.Code.ToString(),
|
||||
Locate(graph, issue.TargetType, issue.TargetId),
|
||||
issue.Message,
|
||||
Suggestion(issue.Code)))
|
||||
.ToList();
|
||||
|
||||
foreach (var duplicate in graph.Clips.Where(clip => clip != null)
|
||||
.GroupBy(clip => clip).Where(group => group.Count() > 1))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
"DuplicateClipReference",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, duplicate.Key),
|
||||
$"Graph 重复引用 Clip '{duplicate.Key.Id}'。",
|
||||
"移除重复的 Clip 引用。"));
|
||||
}
|
||||
|
||||
var graphPath = AssetDatabase.GetAssetPath(graph);
|
||||
foreach (var clip in graph.Clips.Where(clip => clip != null))
|
||||
{
|
||||
if (AssetDatabase.IsSubAsset(clip) && AssetDatabase.GetAssetPath(clip) != graphPath)
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
"ForeignSubAssetClip",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, clip),
|
||||
$"Clip '{clip.Id}' 是其他资产的 sub-asset。",
|
||||
"移除该引用,并复制为当前 Graph 的 Manual Clip。"));
|
||||
}
|
||||
if (clip.IsImported && (!AssetDatabase.IsSubAsset(clip) || AssetDatabase.GetAssetPath(clip) != graphPath))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
"ImportedClipOwnership",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, clip),
|
||||
$"Imported Clip '{clip.Id}' 不是当前 Graph 的 sub-asset。",
|
||||
"删除非法引用并从对应 ImportSource 重新刷新。"));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var node in graph.Nodes.Where(node => node != null))
|
||||
{
|
||||
var clip = graph.Clips.SingleOrDefault(item => item != null && item.Id == node.ClipId);
|
||||
if (clip?.ImportInfo?.IsMissingFromSource == true)
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
"NodeReferencesMissingClip",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node),
|
||||
$"Node '{node.InternalId}' 引用了 Missing Clip '{clip.Id}'。",
|
||||
"恢复源 Tag、修改节点引用或删除该节点。"));
|
||||
}
|
||||
}
|
||||
|
||||
var nodeIds = new HashSet<string>(graph.Nodes.Where(node => node != null)
|
||||
.Select(node => node.InternalId));
|
||||
foreach (var node in graph.Nodes.Where(node => node != null &&
|
||||
graph.EditorData.NodeEditorData.All(data => data == null || data.NodeId != node.InternalId)))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Warning,
|
||||
"EditorDataMissing",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node),
|
||||
$"Node '{node.InternalId}' 缺少画布位置数据。",
|
||||
"打开 Graph Editor 初始化位置,或执行自动布局。"));
|
||||
}
|
||||
foreach (var data in graph.EditorData.NodeEditorData.Where(data => data != null &&
|
||||
!nodeIds.Contains(data.NodeId)))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Warning,
|
||||
"EditorDataOrphan",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
|
||||
$"存在孤立的 NodeEditorData '{data.NodeId}'。",
|
||||
"该数据不会影响运行时;确认无引用后可由后续维护工具清理。"));
|
||||
}
|
||||
foreach (var duplicate in graph.EditorData.NodeEditorData.Where(data => data != null)
|
||||
.GroupBy(data => data.NodeId).Where(group => group.Count() > 1))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
"EditorDataDuplicate",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
|
||||
$"NodeEditorData '{duplicate.Key}' 重复。",
|
||||
"保留一条对应位置数据并移除重复项。"));
|
||||
}
|
||||
|
||||
var flowIds = new HashSet<string>(graph.Flows.Where(flow => flow != null).Select(flow => flow.Id));
|
||||
foreach (var flow in graph.Flows.Where(flow => flow != null &&
|
||||
graph.EditorData.FlowEditorData.All(data => data == null || data.FlowId != flow.Id)))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Warning,
|
||||
"FlowEditorDataMissing",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Flow, flow),
|
||||
$"Flow '{flow.Id}' 缺少画布颜色数据。",
|
||||
"打开 Graph Editor 初始化 Flow 颜色。"));
|
||||
}
|
||||
foreach (var data in graph.EditorData.FlowEditorData.Where(data => data != null &&
|
||||
!flowIds.Contains(data.FlowId)))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Warning,
|
||||
"FlowEditorDataOrphan",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
|
||||
$"存在孤立的 FlowEditorData '{data.FlowId}'。",
|
||||
"确认 Flow 已删除后可由后续维护工具清理。"));
|
||||
}
|
||||
foreach (var duplicate in graph.EditorData.FlowEditorData.Where(data => data != null)
|
||||
.GroupBy(data => data.FlowId).Where(group => group.Count() > 1))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
"FlowEditorDataDuplicate",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
|
||||
$"FlowEditorData '{duplicate.Key}' 重复。",
|
||||
"保留一条对应颜色数据并移除重复项。"));
|
||||
}
|
||||
|
||||
if (!includeImports)
|
||||
{
|
||||
return issues;
|
||||
}
|
||||
|
||||
importPreview = FrameAnimationImportService.PreviewAll(graph);
|
||||
foreach (var sourcePreview in importPreview.Sources)
|
||||
{
|
||||
foreach (var importIssue in sourcePreview.Issues)
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
importIssue.Severity,
|
||||
importIssue.Code.ToString(),
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Source, sourcePreview.Source),
|
||||
importIssue.Message,
|
||||
"打开对应 ImportSource,修复来源设置后重新 Preview。"));
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
private static FrameAnimationEditorSelection Locate(
|
||||
FrameAnimationGraph graph,
|
||||
FrameAnimationValidationTargetType type,
|
||||
string id)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
FrameAnimationValidationTargetType.Clip => new FrameAnimationEditorSelection(
|
||||
FrameAnimationEditorSelectionKind.Clip, graph.Clips.FirstOrDefault(clip => clip != null && clip.Id == id)),
|
||||
FrameAnimationValidationTargetType.Flow => new FrameAnimationEditorSelection(
|
||||
FrameAnimationEditorSelectionKind.Flow, graph.Flows.FirstOrDefault(flow => flow != null && flow.Id == id)),
|
||||
FrameAnimationValidationTargetType.Node => new FrameAnimationEditorSelection(
|
||||
FrameAnimationEditorSelectionKind.Node, graph.Nodes.FirstOrDefault(node => node != null && node.InternalId == id)),
|
||||
FrameAnimationValidationTargetType.Edge => new FrameAnimationEditorSelection(
|
||||
FrameAnimationEditorSelectionKind.Edge, graph.Edges.FirstOrDefault(edge => edge != null && edge.InternalId == id)),
|
||||
FrameAnimationValidationTargetType.ImportSource => new FrameAnimationEditorSelection(
|
||||
FrameAnimationEditorSelectionKind.Source, graph.ImportSources.FirstOrDefault(source => source != null && source.InternalId == id)),
|
||||
_ => new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph)
|
||||
};
|
||||
}
|
||||
|
||||
private static string Suggestion(FrameAnimationValidationCode code)
|
||||
{
|
||||
return code switch
|
||||
{
|
||||
FrameAnimationValidationCode.PlayableIdEmpty => "使用正式 Rename 命令设置唯一 playable id。",
|
||||
FrameAnimationValidationCode.PlayableIdDuplicate => "重命名冲突的 Clip 或 Flow。",
|
||||
FrameAnimationValidationCode.ClipFramesEmpty => "为 Manual Clip 添加帧,或刷新 Imported Clip 来源。",
|
||||
FrameAnimationValidationCode.FrameDurationInvalid => "将 durationMs 修改为大于 0 的整数。",
|
||||
FrameAnimationValidationCode.DefaultPlayableInvalid => "在 Graph 属性中选择有效的默认 Clip 或 Flow。",
|
||||
FrameAnimationValidationCode.ImportedClipMissingFromSource => "恢复源 Tag,或确认没有引用后删除 Missing Clip。",
|
||||
_ => "打开问题对象并修复对应字段或引用。"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationWorkspaceState
|
||||
{
|
||||
private const string Prefix = "AibisDream.FrameAnimation.Workspace.";
|
||||
|
||||
public static string Key(FrameAnimationGraph graph, string name)
|
||||
{
|
||||
var graphPath = graph != null ? AssetDatabase.GetAssetPath(graph) : string.Empty;
|
||||
var graphGuid = string.IsNullOrEmpty(graphPath) ? "none" : AssetDatabase.AssetPathToGUID(graphPath);
|
||||
var project = Hash128.Compute(Application.dataPath).ToString();
|
||||
return Prefix + project + "." + graphGuid + "." + name;
|
||||
}
|
||||
|
||||
public static float GetFloat(FrameAnimationGraph graph, string name, float fallback) =>
|
||||
EditorPrefs.GetFloat(Key(graph, name), fallback);
|
||||
|
||||
public static void SetFloat(FrameAnimationGraph graph, string name, float value) =>
|
||||
EditorPrefs.SetFloat(Key(graph, name), value);
|
||||
|
||||
public static string GetString(FrameAnimationGraph graph, string name, string fallback) =>
|
||||
EditorPrefs.GetString(Key(graph, name), fallback);
|
||||
|
||||
public static void SetString(FrameAnimationGraph graph, string name, string value) =>
|
||||
EditorPrefs.SetString(Key(graph, name), value ?? string.Empty);
|
||||
|
||||
public static bool GetBool(FrameAnimationGraph graph, string name, bool fallback) =>
|
||||
EditorPrefs.GetBool(Key(graph, name), fallback);
|
||||
|
||||
public static void SetBool(FrameAnimationGraph graph, string name, bool value) =>
|
||||
EditorPrefs.SetBool(Key(graph, name), value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2643ad174ef237e43b98400d60c4a201
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,603 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal static class FrameAnimationFlowFocusPolicy
|
||||
{
|
||||
public static AnimationFlow FindFocusedFlow(FrameAnimationGraph graph, string focusedFlowId)
|
||||
{
|
||||
return graph?.Flows.FirstOrDefault(flow => flow != null && flow.Id == focusedFlowId);
|
||||
}
|
||||
|
||||
public static bool Contains(
|
||||
FrameAnimationGraph graph,
|
||||
string focusedFlowId,
|
||||
FrameAnimationEditorSelection selection)
|
||||
{
|
||||
var flow = FindFocusedFlow(graph, focusedFlowId);
|
||||
if (flow == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var reachable = new FrameAnimationGraphTopology(graph).GetReachable(flow.EntryNodeId);
|
||||
return selection.Value switch
|
||||
{
|
||||
AnimationNode node => reachable.Nodes.Contains(node),
|
||||
AnimationEdge edge => reachable.Edges.Contains(edge),
|
||||
AnimationFlow selectedFlow => selectedFlow == flow,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public static bool ShouldExitForSelection(
|
||||
FrameAnimationGraph graph,
|
||||
string focusedFlowId,
|
||||
FrameAnimationEditorSelection selection)
|
||||
{
|
||||
if (string.IsNullOrEmpty(focusedFlowId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return selection.Kind switch
|
||||
{
|
||||
FrameAnimationEditorSelectionKind.None => false,
|
||||
FrameAnimationEditorSelectionKind.Flow => false,
|
||||
FrameAnimationEditorSelectionKind.Node => !Contains(graph, focusedFlowId, selection),
|
||||
FrameAnimationEditorSelectionKind.Edge => !Contains(graph, focusedFlowId, selection),
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
public static bool ShouldExitForSelectionSet(
|
||||
FrameAnimationGraph graph,
|
||||
string focusedFlowId,
|
||||
IEnumerable<FrameAnimationEditorSelection> selections)
|
||||
{
|
||||
if (string.IsNullOrEmpty(focusedFlowId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var items = (selections ?? Array.Empty<FrameAnimationEditorSelection>()).ToArray();
|
||||
return items.Length > 0 && items.Any(item =>
|
||||
(item.Kind == FrameAnimationEditorSelectionKind.Node ||
|
||||
item.Kind == FrameAnimationEditorSelectionKind.Edge)
|
||||
? !Contains(graph, focusedFlowId, item)
|
||||
: ShouldExitForSelection(graph, focusedFlowId, item));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationGraphImpact
|
||||
{
|
||||
public IReadOnlyList<AnimationFlow> Flows { get; }
|
||||
public int LostNodeCount { get; }
|
||||
|
||||
internal FrameAnimationGraphImpact(IReadOnlyList<AnimationFlow> flows, int lostNodeCount)
|
||||
{
|
||||
Flows = flows ?? Array.Empty<AnimationFlow>();
|
||||
LostNodeCount = lostNodeCount;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationGraphImpactAnalyzer
|
||||
{
|
||||
public static FrameAnimationGraphImpact AnalyzeEdgeRemoval(
|
||||
FrameAnimationGraph graph,
|
||||
IEnumerable<AnimationEdge> edges)
|
||||
{
|
||||
var removed = new HashSet<AnimationEdge>((edges ?? Array.Empty<AnimationEdge>()).Where(edge => edge != null));
|
||||
return Analyze(graph, Array.Empty<AnimationNode>(), removed);
|
||||
}
|
||||
|
||||
public static FrameAnimationGraphImpact AnalyzeNodeRemoval(
|
||||
FrameAnimationGraph graph,
|
||||
IEnumerable<AnimationNode> nodes)
|
||||
{
|
||||
var removedNodes = new HashSet<AnimationNode>((nodes ?? Array.Empty<AnimationNode>()).Where(node => node != null));
|
||||
var removedIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
|
||||
var removedEdges = new HashSet<AnimationEdge>(graph.Edges.Where(edge => edge != null &&
|
||||
(removedIds.Contains(edge.FromNodeId) || removedIds.Contains(edge.ToNodeId))));
|
||||
return Analyze(graph, removedNodes, removedEdges);
|
||||
}
|
||||
|
||||
public static FrameAnimationGraphImpact AnalyzeRemoval(
|
||||
FrameAnimationGraph graph,
|
||||
IEnumerable<AnimationNode> nodes,
|
||||
IEnumerable<AnimationEdge> edges)
|
||||
{
|
||||
if (graph == null)
|
||||
{
|
||||
return new FrameAnimationGraphImpact(Array.Empty<AnimationFlow>(), 0);
|
||||
}
|
||||
var removedNodes = new HashSet<AnimationNode>((nodes ?? Array.Empty<AnimationNode>())
|
||||
.Where(node => node != null));
|
||||
var removedIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
|
||||
var removedEdges = new HashSet<AnimationEdge>((edges ?? Array.Empty<AnimationEdge>())
|
||||
.Where(edge => edge != null));
|
||||
removedEdges.UnionWith(graph.Edges.Where(edge => edge != null &&
|
||||
(removedIds.Contains(edge.FromNodeId) || removedIds.Contains(edge.ToNodeId))));
|
||||
return Analyze(graph, removedNodes, removedEdges);
|
||||
}
|
||||
|
||||
private static FrameAnimationGraphImpact Analyze(
|
||||
FrameAnimationGraph graph,
|
||||
IReadOnlyCollection<AnimationNode> removedNodes,
|
||||
IReadOnlyCollection<AnimationEdge> removedEdges)
|
||||
{
|
||||
if (graph == null)
|
||||
{
|
||||
return new FrameAnimationGraphImpact(Array.Empty<AnimationFlow>(), 0);
|
||||
}
|
||||
var topology = new FrameAnimationGraphTopology(graph);
|
||||
var removedNodeIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
|
||||
var affected = new List<AnimationFlow>();
|
||||
var lost = new HashSet<string>();
|
||||
foreach (var flow in graph.Flows.Where(flow => flow != null))
|
||||
{
|
||||
var before = topology.GetReachable(flow.EntryNodeId).Nodes
|
||||
.Where(node => node != null).Select(node => node.InternalId).ToHashSet();
|
||||
var after = ReachableWithout(graph, flow.EntryNodeId, removedNodeIds, removedEdges);
|
||||
var flowLost = before.Where(nodeId => !after.Contains(nodeId)).ToArray();
|
||||
if (flowLost.Length > 0)
|
||||
{
|
||||
affected.Add(flow);
|
||||
lost.UnionWith(flowLost);
|
||||
}
|
||||
}
|
||||
return new FrameAnimationGraphImpact(
|
||||
affected.OrderBy(flow => flow.Id, StringComparer.Ordinal).ToArray(),
|
||||
lost.Count);
|
||||
}
|
||||
|
||||
private static HashSet<string> ReachableWithout(
|
||||
FrameAnimationGraph graph,
|
||||
string entry,
|
||||
HashSet<string> removedNodeIds,
|
||||
IReadOnlyCollection<AnimationEdge> removedEdges)
|
||||
{
|
||||
var result = new HashSet<string>();
|
||||
if (removedNodeIds.Contains(entry ?? string.Empty))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
var outgoing = graph.Edges.Where(edge => edge != null && !removedEdges.Contains(edge))
|
||||
.GroupBy(edge => edge.FromNodeId ?? string.Empty)
|
||||
.ToDictionary(group => group.Key, group => group.ToArray());
|
||||
var queue = new Queue<string>();
|
||||
queue.Enqueue(entry ?? string.Empty);
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var id = queue.Dequeue();
|
||||
if (removedNodeIds.Contains(id) || !result.Add(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (outgoing.TryGetValue(id, out var edges))
|
||||
{
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
queue.Enqueue(edge.ToNodeId ?? string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationGraphMutationService
|
||||
{
|
||||
private static readonly Color[] FlowColors =
|
||||
{
|
||||
new Color(0.24f, 0.65f, 1f), new Color(0.42f, 0.8f, 0.42f),
|
||||
new Color(1f, 0.62f, 0.24f), new Color(0.82f, 0.42f, 0.9f),
|
||||
new Color(1f, 0.38f, 0.52f), new Color(0.25f, 0.82f, 0.78f)
|
||||
};
|
||||
|
||||
public static bool CreateNode(
|
||||
FrameAnimationGraph graph,
|
||||
FrameClip clip,
|
||||
Vector2 position,
|
||||
out AnimationNode node,
|
||||
out string error)
|
||||
{
|
||||
node = null;
|
||||
error = string.Empty;
|
||||
if (graph == null || clip == null || !graph.Clips.Contains(clip))
|
||||
{
|
||||
error = "只能为当前 Graph 中的 FrameClip 创建节点。";
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Create Frame Animation Node");
|
||||
node = new AnimationNode(clip.Id,
|
||||
string.IsNullOrWhiteSpace(clip.DisplayName) ? clip.Id : clip.DisplayName);
|
||||
graph.AddNode(node);
|
||||
graph.EditorData.GetOrCreateNodeData(node.InternalId, position).SetPosition(position);
|
||||
Dirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryConnect(
|
||||
FrameAnimationGraph graph,
|
||||
AnimationNode from,
|
||||
AnimationNode to,
|
||||
out AnimationEdge edge,
|
||||
out string error)
|
||||
{
|
||||
edge = null;
|
||||
if (!CanConnect(graph, from, to, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Connect Frame Animation Nodes");
|
||||
edge = new AnimationEdge(from.InternalId, to.InternalId);
|
||||
graph.AddEdge(edge);
|
||||
Dirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CanConnect(
|
||||
FrameAnimationGraph graph,
|
||||
AnimationNode from,
|
||||
AnimationNode to,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || from == null || to == null ||
|
||||
graph.Nodes.Count(node => node == from) != 1 || graph.Nodes.Count(node => node == to) != 1)
|
||||
{
|
||||
error = "连接两端必须唯一属于当前 Graph。";
|
||||
return false;
|
||||
}
|
||||
if (from == to || from.InternalId == to.InternalId)
|
||||
{
|
||||
error = "节点不允许连接到自身。";
|
||||
return false;
|
||||
}
|
||||
var topology = new FrameAnimationGraphTopology(graph);
|
||||
if (topology.GetOutgoing(from.InternalId).Count > 0)
|
||||
{
|
||||
error = "第一版每个节点最多只能有一个后继。";
|
||||
return false;
|
||||
}
|
||||
if (from.EndBehaviorOverride.HasValue)
|
||||
{
|
||||
error = "该节点已设置终点结束行为,请先清除覆盖。";
|
||||
return false;
|
||||
}
|
||||
if (graph.Edges.Any(existing => existing != null &&
|
||||
existing.FromNodeId == from.InternalId && existing.ToNodeId == to.InternalId))
|
||||
{
|
||||
error = "该连接已经存在。";
|
||||
return false;
|
||||
}
|
||||
if (topology.WouldCreateCycle(from.InternalId, to.InternalId))
|
||||
{
|
||||
error = "该连接会形成自连接或多节点环路。";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void SetNodeDisplayName(FrameAnimationGraph graph, AnimationNode node, string value)
|
||||
{
|
||||
Undo.RecordObject(graph, "Rename Frame Animation Node");
|
||||
node.SetDisplayName(value);
|
||||
Dirty(graph);
|
||||
}
|
||||
|
||||
public static void SetNodeClip(FrameAnimationGraph graph, AnimationNode node, FrameClip clip)
|
||||
{
|
||||
Undo.RecordObject(graph, "Set Frame Animation Node Clip");
|
||||
node.SetClipId(clip != null ? clip.Id : string.Empty);
|
||||
Dirty(graph);
|
||||
}
|
||||
|
||||
public static void SetNodeSpeed(FrameAnimationGraph graph, AnimationNode node, float? value)
|
||||
{
|
||||
Undo.RecordObject(graph, "Set Frame Animation Node Speed");
|
||||
node.SetSpeedOverride(value);
|
||||
Dirty(graph);
|
||||
}
|
||||
|
||||
public static bool SetNodeEndBehavior(
|
||||
FrameAnimationGraph graph,
|
||||
AnimationNode node,
|
||||
FrameClipEndBehavior? value,
|
||||
bool removeOutgoing,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
var outgoing = graph.Edges.Where(edge => edge != null && edge.FromNodeId == node.InternalId).ToArray();
|
||||
if (value.HasValue && outgoing.Length > 0 && !removeOutgoing)
|
||||
{
|
||||
error = "节点仍有后继 Edge。";
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Set Frame Animation Node End Behavior");
|
||||
if (value.HasValue)
|
||||
{
|
||||
foreach (var edge in outgoing)
|
||||
{
|
||||
graph.RemoveEdge(edge);
|
||||
}
|
||||
}
|
||||
node.SetEndBehaviorOverride(value);
|
||||
Dirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void RemoveEdges(FrameAnimationGraph graph, IEnumerable<AnimationEdge> edges)
|
||||
{
|
||||
var items = (edges ?? Array.Empty<AnimationEdge>()).Where(edge => edge != null).Distinct().ToArray();
|
||||
if (graph == null || items.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Undo.RecordObject(graph, "Disconnect Frame Animation Nodes");
|
||||
foreach (var edge in items)
|
||||
{
|
||||
graph.RemoveEdge(edge);
|
||||
}
|
||||
Dirty(graph);
|
||||
}
|
||||
|
||||
public static bool RemoveNodes(
|
||||
FrameAnimationGraph graph,
|
||||
IEnumerable<AnimationNode> nodes,
|
||||
bool deleteEntryFlows,
|
||||
out string error)
|
||||
{
|
||||
return RemoveNodesAndEdges(
|
||||
graph, nodes, Array.Empty<AnimationEdge>(), deleteEntryFlows, out error);
|
||||
}
|
||||
|
||||
public static bool RemoveNodesAndEdges(
|
||||
FrameAnimationGraph graph,
|
||||
IEnumerable<AnimationNode> nodes,
|
||||
IEnumerable<AnimationEdge> additionalEdges,
|
||||
bool deleteEntryFlows,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null)
|
||||
{
|
||||
error = "Graph 已失效。";
|
||||
return false;
|
||||
}
|
||||
var items = (nodes ?? Array.Empty<AnimationNode>()).Where(node => node != null).Distinct().ToArray();
|
||||
var extraEdges = (additionalEdges ?? Array.Empty<AnimationEdge>())
|
||||
.Where(edge => edge != null).Distinct().ToArray();
|
||||
var ids = new HashSet<string>(items.Select(node => node.InternalId));
|
||||
var entryFlows = graph.Flows.Where(flow => flow != null && ids.Contains(flow.EntryNodeId)).ToArray();
|
||||
if (entryFlows.Length > 0 && !deleteEntryFlows)
|
||||
{
|
||||
error = "删除项包含 Flow 入口节点。";
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Delete Frame Animation Graph Elements");
|
||||
foreach (var edge in graph.Edges.Where(edge => edge != null &&
|
||||
(ids.Contains(edge.FromNodeId) || ids.Contains(edge.ToNodeId) || extraEdges.Contains(edge))).ToArray())
|
||||
{
|
||||
graph.RemoveEdge(edge);
|
||||
}
|
||||
foreach (var flow in entryFlows)
|
||||
{
|
||||
graph.RemoveFlow(flow);
|
||||
graph.EditorData.RemoveFlowData(flow.Id);
|
||||
if (graph.Settings.DefaultPlayableId == flow.Id)
|
||||
{
|
||||
graph.Settings.SetDefaultPlayableId(string.Empty);
|
||||
}
|
||||
}
|
||||
foreach (var node in items)
|
||||
{
|
||||
graph.RemoveNode(node);
|
||||
graph.EditorData.RemoveNodeData(node.InternalId);
|
||||
}
|
||||
Dirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string MakeUniqueFlowId(FrameAnimationGraph graph, AnimationNode node)
|
||||
{
|
||||
var root = (string.IsNullOrWhiteSpace(node?.DisplayName) ? "Animation" : node.DisplayName) + "Flow";
|
||||
var candidate = root;
|
||||
var suffix = 2;
|
||||
while (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, candidate))
|
||||
{
|
||||
candidate = root + suffix++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
public static bool CreateFlow(
|
||||
FrameAnimationGraph graph,
|
||||
AnimationNode entry,
|
||||
string id,
|
||||
string displayName,
|
||||
out AnimationFlow flow,
|
||||
out string error)
|
||||
{
|
||||
flow = null;
|
||||
error = string.Empty;
|
||||
if (graph == null || entry == null || !graph.Nodes.Contains(entry))
|
||||
{
|
||||
error = "Flow 入口必须属于当前 Graph。";
|
||||
return false;
|
||||
}
|
||||
var owner = graph.Flows.FirstOrDefault(existing => existing != null && existing.EntryNodeId == entry.InternalId);
|
||||
if (owner != null)
|
||||
{
|
||||
error = $"该节点已是 Flow '{owner.Id}' 的入口。";
|
||||
return false;
|
||||
}
|
||||
if (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id))
|
||||
{
|
||||
error = "Flow id 为空或与 Clip / Flow 冲突。";
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Create Animation Flow");
|
||||
flow = new AnimationFlow(id, string.IsNullOrWhiteSpace(displayName) ? id : displayName, entry.InternalId);
|
||||
graph.AddFlow(flow);
|
||||
graph.EditorData.GetOrCreateFlowData(flow.Id, FlowColors[(graph.Flows.Count - 1) % FlowColors.Length]);
|
||||
Dirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool SetFlowEntry(
|
||||
FrameAnimationGraph graph,
|
||||
AnimationFlow flow,
|
||||
AnimationNode entry,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || flow == null || entry == null ||
|
||||
!graph.Flows.Contains(flow) || !graph.Nodes.Contains(entry))
|
||||
{
|
||||
error = "Flow 或入口节点不属于当前 Graph。";
|
||||
return false;
|
||||
}
|
||||
var owner = graph.Flows.FirstOrDefault(existing => existing != null && existing != flow &&
|
||||
existing.EntryNodeId == entry.InternalId);
|
||||
if (owner != null)
|
||||
{
|
||||
error = $"该节点已是 Flow '{owner.Id}' 的入口。";
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Set Animation Flow Entry");
|
||||
flow.SetEntryNodeId(entry.InternalId);
|
||||
Dirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void SetFlowColor(FrameAnimationGraph graph, AnimationFlow flow, Color value)
|
||||
{
|
||||
if (graph == null || flow == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Undo.RecordObject(graph, "Set Animation Flow Color");
|
||||
graph.EditorData.GetOrCreateFlowData(flow.Id, value).SetColor(value);
|
||||
Dirty(graph);
|
||||
}
|
||||
|
||||
public static bool EnsureEditorData(FrameAnimationGraph graph)
|
||||
{
|
||||
if (graph == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var missingNodes = graph.Nodes.Where(node => node != null &&
|
||||
graph.EditorData.NodeEditorData.All(data => data == null || data.NodeId != node.InternalId)).ToArray();
|
||||
var missingFlows = graph.Flows.Where(flow => flow != null &&
|
||||
graph.EditorData.FlowEditorData.All(data => data == null || data.FlowId != flow.Id)).ToArray();
|
||||
if (missingNodes.Length == 0 && missingFlows.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Initialize Frame Animation Graph Layout");
|
||||
for (var index = 0; index < missingNodes.Length; index++)
|
||||
{
|
||||
var position = new Vector2((index % 4) * 300f, (index / 4) * 180f);
|
||||
graph.EditorData.GetOrCreateNodeData(missingNodes[index].InternalId, position);
|
||||
}
|
||||
for (var index = 0; index < missingFlows.Length; index++)
|
||||
{
|
||||
graph.EditorData.GetOrCreateFlowData(missingFlows[index].Id,
|
||||
FlowColors[(graph.Flows.ToList().IndexOf(missingFlows[index]) + FlowColors.Length) % FlowColors.Length]);
|
||||
}
|
||||
Dirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Vector2 GetNodePosition(FrameAnimationGraph graph, AnimationNode node)
|
||||
{
|
||||
return graph?.EditorData.NodeEditorData.FirstOrDefault(data =>
|
||||
data != null && data.NodeId == node?.InternalId)?.Position ?? Vector2.zero;
|
||||
}
|
||||
|
||||
public static void SetNodePositions(
|
||||
FrameAnimationGraph graph,
|
||||
IReadOnlyDictionary<AnimationNode, Vector2> positions,
|
||||
string undoName)
|
||||
{
|
||||
if (graph == null || positions == null || positions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Undo.RecordObject(graph, undoName);
|
||||
foreach (var pair in positions)
|
||||
{
|
||||
graph.EditorData.GetOrCreateNodeData(pair.Key.InternalId, pair.Value).SetPosition(pair.Value);
|
||||
}
|
||||
Dirty(graph);
|
||||
}
|
||||
|
||||
private static void Dirty(FrameAnimationGraph graph)
|
||||
{
|
||||
EditorUtility.SetDirty(graph);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationGraphLayoutService
|
||||
{
|
||||
public static IReadOnlyDictionary<AnimationNode, Vector2> Calculate(
|
||||
FrameAnimationGraph graph,
|
||||
IEnumerable<AnimationNode> scope)
|
||||
{
|
||||
var source = scope ?? (graph != null
|
||||
? graph.Nodes
|
||||
: (IEnumerable<AnimationNode>)Array.Empty<AnimationNode>());
|
||||
var nodes = source.Where(node => node != null).Distinct()
|
||||
.GroupBy(node => node.InternalId ?? string.Empty)
|
||||
.Select(group => group.First()).ToArray();
|
||||
if (graph == null || nodes.Length == 0)
|
||||
{
|
||||
return new Dictionary<AnimationNode, Vector2>();
|
||||
}
|
||||
var ids = new HashSet<string>(nodes.Select(node => node.InternalId));
|
||||
var edges = graph.Edges.Where(edge => edge != null && ids.Contains(edge.FromNodeId) && ids.Contains(edge.ToNodeId)).ToArray();
|
||||
var incoming = nodes.ToDictionary(node => node.InternalId, _ => 0);
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
incoming[edge.ToNodeId]++;
|
||||
}
|
||||
var ranks = nodes.ToDictionary(node => node.InternalId, _ => 0);
|
||||
var queue = new Queue<AnimationNode>(nodes.Where(node => incoming[node.InternalId] == 0)
|
||||
.OrderBy(node => node.InternalId, StringComparer.Ordinal));
|
||||
var processed = new HashSet<string>();
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var node = queue.Dequeue();
|
||||
if (!processed.Add(node.InternalId)) continue;
|
||||
foreach (var edge in edges.Where(edge => edge.FromNodeId == node.InternalId))
|
||||
{
|
||||
ranks[edge.ToNodeId] = Math.Max(ranks[edge.ToNodeId], ranks[node.InternalId] + 1);
|
||||
incoming[edge.ToNodeId]--;
|
||||
if (incoming[edge.ToNodeId] == 0)
|
||||
{
|
||||
queue.Enqueue(nodes.First(item => item.InternalId == edge.ToNodeId));
|
||||
}
|
||||
}
|
||||
}
|
||||
var oldPositions = nodes.ToDictionary(node => node,
|
||||
node => FrameAnimationGraphMutationService.GetNodePosition(graph, node));
|
||||
var originX = oldPositions.Values.Min(position => position.x);
|
||||
var originY = oldPositions.Values.Min(position => position.y);
|
||||
var result = new Dictionary<AnimationNode, Vector2>();
|
||||
foreach (var group in nodes.GroupBy(node => ranks[node.InternalId]).OrderBy(group => group.Key))
|
||||
{
|
||||
var ordered = group.OrderBy(node => oldPositions[node].y)
|
||||
.ThenBy(node => node.InternalId, StringComparer.Ordinal).ToArray();
|
||||
for (var index = 0; index < ordered.Length; index++)
|
||||
{
|
||||
result[ordered[index]] = new Vector2(originX + group.Key * 300f, originY + index * 180f);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b397ed96c5b44df89626f866c32e03dd
|
||||
@@ -0,0 +1,33 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
[CustomEditor(typeof(FrameAnimationGraph))]
|
||||
public sealed class FrameAnimationGraphEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
var graph = (FrameAnimationGraph)target;
|
||||
EditorGUILayout.LabelField("FrameAnimationGraph", EditorStyles.boldLabel);
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUILayout.TextField("ID", graph.Id);
|
||||
EditorGUILayout.TextField("Display Name", graph.DisplayName);
|
||||
EditorGUILayout.IntField("Clips", graph.Clips.Count);
|
||||
EditorGUILayout.IntField("Nodes", graph.Nodes.Count);
|
||||
EditorGUILayout.IntField("Edges", graph.Edges.Count);
|
||||
EditorGUILayout.IntField("Flows", graph.Flows.Count);
|
||||
EditorGUILayout.IntField("Import Sources", graph.ImportSources.Count);
|
||||
EditorGUILayout.TextField("Default Playable", graph.Settings?.DefaultPlayableId ?? string.Empty);
|
||||
}
|
||||
EditorGUILayout.HelpBox(
|
||||
"Graph 的资源、来源、重命名和删除操作只能在主工作台中执行。",
|
||||
MessageType.Info);
|
||||
if (GUILayout.Button("Open Frame Animation Graph Editor"))
|
||||
{
|
||||
FrameAnimationGraphEditorWindow.Open(graph);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74f3b50c2650607499ea1c82a282f00f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,72 @@
|
||||
.fa-preview {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-left-width: 1px;
|
||||
border-right-width: 1px;
|
||||
border-top-width: 1px;
|
||||
border-bottom-width: 1px;
|
||||
border-left-color: rgb(25, 25, 25);
|
||||
border-right-color: rgb(25, 25, 25);
|
||||
border-top-color: rgb(25, 25, 25);
|
||||
border-bottom-color: rgb(25, 25, 25);
|
||||
}
|
||||
|
||||
.fa-preview__image {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.fa-preview__empty {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
-unity-text-align: middle-center;
|
||||
color: rgb(190, 190, 190);
|
||||
}
|
||||
|
||||
.fa-node__preview {
|
||||
height: 82px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.fa-node--preview-current {
|
||||
border-left-color: rgb(55, 174, 240);
|
||||
border-right-color: rgb(55, 174, 240);
|
||||
border-top-color: rgb(55, 174, 240);
|
||||
border-bottom-color: rgb(55, 174, 240);
|
||||
}
|
||||
|
||||
.fa-node--preview-passed {
|
||||
border-left-color: rgb(83, 154, 113);
|
||||
}
|
||||
|
||||
.fa-node--preview-upcoming {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.fa-clip-preview-panel {
|
||||
flex-shrink: 0;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 5px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: rgb(28, 28, 28);
|
||||
}
|
||||
|
||||
.fa-clip-preview-panel .fa-preview {
|
||||
height: 190px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.fa-preview-controls {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fa-preview-status {
|
||||
font-size: 10px;
|
||||
color: rgb(185, 185, 185);
|
||||
margin-top: 2px;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a38dd39c699e42348667cc1264ea9076
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df89f5fa1de04df448957109886651b8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,740 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal sealed class FrameAnimationClipNodeView : Node
|
||||
{
|
||||
private readonly Label subtitle;
|
||||
private readonly Label behavior;
|
||||
private readonly Label flowBadge;
|
||||
private readonly Label issueBadge;
|
||||
private readonly FrameAnimationPreviewElement preview;
|
||||
private readonly Action<FrameAnimationClipNodeView> selectedCallback;
|
||||
|
||||
public AnimationNode Data { get; }
|
||||
public Port InputPort { get; }
|
||||
public Port OutputPort { get; }
|
||||
|
||||
internal FrameAnimationClipNodeView(
|
||||
AnimationNode data,
|
||||
Action<FrameAnimationClipNodeView> selectedCallback)
|
||||
{
|
||||
Data = data;
|
||||
this.selectedCallback = selectedCallback;
|
||||
viewDataKey = data.InternalId;
|
||||
AddToClassList("fa-node");
|
||||
style.minWidth = 245f;
|
||||
style.maxWidth = 285f;
|
||||
|
||||
InputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi,
|
||||
typeof(AnimationNode));
|
||||
InputPort.portName = "In";
|
||||
inputContainer.Add(InputPort);
|
||||
OutputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single,
|
||||
typeof(AnimationNode));
|
||||
OutputPort.portName = "Next";
|
||||
outputContainer.Add(OutputPort);
|
||||
|
||||
subtitle = new Label();
|
||||
behavior = new Label();
|
||||
flowBadge = new Label();
|
||||
issueBadge = new Label();
|
||||
issueBadge.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
extensionContainer.Add(subtitle);
|
||||
extensionContainer.Add(behavior);
|
||||
extensionContainer.Add(flowBadge);
|
||||
extensionContainer.Add(issueBadge);
|
||||
preview = new FrameAnimationPreviewElement();
|
||||
preview.AddToClassList("fa-node__preview");
|
||||
extensionContainer.Add(preview);
|
||||
RefreshExpandedState();
|
||||
}
|
||||
|
||||
public override void OnSelected()
|
||||
{
|
||||
base.OnSelected();
|
||||
selectedCallback?.Invoke(this);
|
||||
}
|
||||
|
||||
internal void Refresh(
|
||||
FrameAnimationGraph graph,
|
||||
IReadOnlyList<FrameAnimationEditorIssue> issues,
|
||||
IReadOnlyList<AnimationFlow> flows,
|
||||
IReadOnlyCollection<string> entryFlowIds)
|
||||
{
|
||||
var clip = graph.Clips.FirstOrDefault(item => item != null && item.Id == Data.ClipId);
|
||||
title = string.IsNullOrWhiteSpace(Data.DisplayName) ? Data.ClipId : Data.DisplayName;
|
||||
subtitle.text = clip != null ? $"Clip: {Data.ClipId}" : $"Clip: <Missing {Data.ClipId}>";
|
||||
var speed = Data.SpeedOverride.HasValue
|
||||
? $"Speed {Data.SpeedOverride.Value:0.###} (Node)"
|
||||
: clip != null ? $"Speed {clip.Speed:0.###} (Clip)" : "Speed ?";
|
||||
var end = Data.EndBehaviorOverride?.ToString() ?? "Continue / terminal fallback";
|
||||
behavior.text = $"{speed} · {end}";
|
||||
var used = flows.Select(flow => flow.Id).ToArray();
|
||||
var entries = used.Where(entryFlowIds.Contains).ToArray();
|
||||
flowBadge.text = used.Length == 0
|
||||
? "Unused"
|
||||
: "Flows: " + string.Join(", ", used.Take(3)) + (used.Length > 3 ? $" +{used.Length - 3}" : string.Empty) +
|
||||
(entries.Length > 0 ? " [ENTRY]" : string.Empty);
|
||||
var matching = issues.Where(issue => issue.Selection.Kind == FrameAnimationEditorSelectionKind.Node &&
|
||||
ReferenceEquals(issue.Selection.Value, Data)).ToArray();
|
||||
issueBadge.text = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
|
||||
? "ERROR"
|
||||
: matching.Length > 0 ? "WARNING" : string.Empty;
|
||||
issueBadge.style.color = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
|
||||
? new StyleColor(new Color(1f, 0.35f, 0.3f))
|
||||
: new StyleColor(new Color(1f, 0.72f, 0.25f));
|
||||
OutputPort.SetEnabled(!Data.EndBehaviorOverride.HasValue);
|
||||
OutputPort.tooltip = Data.EndBehaviorOverride.HasValue
|
||||
? "该节点已设置终点结束行为,请先清除覆盖。"
|
||||
: "创建唯一的顺序后继";
|
||||
}
|
||||
|
||||
internal void SetDimmed(bool dimmed)
|
||||
{
|
||||
style.opacity = dimmed ? 0.45f : 1f;
|
||||
}
|
||||
|
||||
internal void SetPreview(
|
||||
FrameAnimationPreviewDisplay display,
|
||||
FrameAnimationPreviewBackground background)
|
||||
{
|
||||
preview.SetBackground(background);
|
||||
preview.SetZoom(FrameAnimationPreviewZoomMode.Fit, 1f);
|
||||
preview.SetDisplay(display.Sprite, display.IsEmpty);
|
||||
preview.style.opacity = display.FlowState == FrameAnimationFlowElementState.Upcoming
|
||||
? 0.55f : display.FlowState == FrameAnimationFlowElementState.Passed ? 0.82f : 1f;
|
||||
EnableInClassList("fa-node--preview-current", display.FlowState == FrameAnimationFlowElementState.Current);
|
||||
EnableInClassList("fa-node--preview-passed", display.FlowState == FrameAnimationFlowElementState.Passed);
|
||||
EnableInClassList("fa-node--preview-upcoming", display.FlowState == FrameAnimationFlowElementState.Upcoming);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationEdgeView : Edge
|
||||
{
|
||||
private readonly Action<FrameAnimationEdgeView> selectedCallback;
|
||||
private Color baseColor = Color.white;
|
||||
public AnimationEdge Data { get; }
|
||||
|
||||
internal FrameAnimationEdgeView(AnimationEdge data, Action<FrameAnimationEdgeView> selectedCallback)
|
||||
{
|
||||
Data = data;
|
||||
this.selectedCallback = selectedCallback;
|
||||
viewDataKey = data.InternalId;
|
||||
userData = data;
|
||||
}
|
||||
|
||||
public override void OnSelected()
|
||||
{
|
||||
base.OnSelected();
|
||||
selectedCallback?.Invoke(this);
|
||||
}
|
||||
|
||||
internal void SetDimmed(bool dimmed)
|
||||
{
|
||||
style.opacity = dimmed ? 0.30f : 1f;
|
||||
}
|
||||
|
||||
internal void Refresh(IReadOnlyList<FrameAnimationEditorIssue> issues)
|
||||
{
|
||||
var matching = (issues ?? Array.Empty<FrameAnimationEditorIssue>()).Where(issue =>
|
||||
issue.Selection.Kind == FrameAnimationEditorSelectionKind.Edge &&
|
||||
ReferenceEquals(issue.Selection.Value, Data)).ToArray();
|
||||
var color = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
|
||||
? new Color(1f, 0.3f, 0.25f)
|
||||
: matching.Length > 0 ? new Color(1f, 0.72f, 0.2f) : Color.white;
|
||||
baseColor = color;
|
||||
edgeControl.inputColor = color;
|
||||
edgeControl.outputColor = color;
|
||||
tooltip = matching.Length == 0
|
||||
? $"{Data.ExitName} / {Data.Condition}"
|
||||
: string.Join("\n", matching.Select(issue => $"[{issue.Code}] {issue.Message}"));
|
||||
}
|
||||
|
||||
internal void SetPreviewState(FrameAnimationFlowElementState state)
|
||||
{
|
||||
if (state == FrameAnimationFlowElementState.Current)
|
||||
{
|
||||
edgeControl.inputColor = new Color(0.25f, 0.75f, 1f);
|
||||
edgeControl.outputColor = new Color(0.25f, 0.75f, 1f);
|
||||
}
|
||||
else if (state == FrameAnimationFlowElementState.Passed)
|
||||
{
|
||||
edgeControl.inputColor = new Color(0.35f, 0.68f, 0.46f);
|
||||
edgeControl.outputColor = new Color(0.35f, 0.68f, 0.46f);
|
||||
}
|
||||
else if (state == FrameAnimationFlowElementState.Upcoming)
|
||||
{
|
||||
edgeControl.inputColor = new Color(0.55f, 0.55f, 0.55f);
|
||||
edgeControl.outputColor = new Color(0.55f, 0.55f, 0.55f);
|
||||
}
|
||||
else
|
||||
{
|
||||
edgeControl.inputColor = baseColor;
|
||||
edgeControl.outputColor = baseColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationClipSearchProvider : ScriptableObject, ISearchWindowProvider
|
||||
{
|
||||
private FrameAnimationGraph graph;
|
||||
private Action<FrameClip> selected;
|
||||
|
||||
internal void Configure(FrameAnimationGraph value, Action<FrameClip> callback)
|
||||
{
|
||||
graph = value;
|
||||
selected = callback;
|
||||
}
|
||||
|
||||
public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
|
||||
{
|
||||
var entries = new List<SearchTreeEntry>
|
||||
{
|
||||
new SearchTreeGroupEntry(new GUIContent("Create Clip Node"), 0)
|
||||
};
|
||||
foreach (var clip in graph.Clips.Where(clip => clip != null)
|
||||
.OrderBy(clip => clip.DisplayName, StringComparer.Ordinal)
|
||||
.ThenBy(clip => clip.Id, StringComparer.Ordinal))
|
||||
{
|
||||
entries.Add(new SearchTreeEntry(new GUIContent($"{clip.DisplayName} ({clip.Id})"))
|
||||
{
|
||||
level = 1,
|
||||
userData = clip
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
public bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context)
|
||||
{
|
||||
if (entry.userData is FrameClip clip)
|
||||
{
|
||||
selected?.Invoke(clip);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationGraphView : GraphView
|
||||
{
|
||||
internal const string ClipDragKey = "AibisDream.FrameAnimation.ClipDrag";
|
||||
|
||||
private readonly Dictionary<string, FrameAnimationClipNodeView> nodeViews =
|
||||
new Dictionary<string, FrameAnimationClipNodeView>();
|
||||
private readonly Dictionary<string, FrameAnimationEdgeView> edgeViews =
|
||||
new Dictionary<string, FrameAnimationEdgeView>();
|
||||
private FrameAnimationGraph graph;
|
||||
private IReadOnlyList<FrameAnimationEditorIssue> issues = Array.Empty<FrameAnimationEditorIssue>();
|
||||
private bool rebuilding;
|
||||
private Vector2 contextPosition;
|
||||
private string focusedFlowId = string.Empty;
|
||||
|
||||
internal event Action<FrameAnimationEditorSelection> SelectionRequested;
|
||||
internal event Action<IReadOnlyList<AnimationNode>> MultiSelectionChanged;
|
||||
internal event Action<IReadOnlyList<FrameAnimationEditorSelection>> SelectionSetChanged;
|
||||
internal event Action<AnimationNode> CreateFlowRequested;
|
||||
internal event Action<AnimationNode> NodeCreated;
|
||||
internal event Action ExitFocusRequested;
|
||||
internal event Action GraphChanged;
|
||||
internal event Action<string> NotificationRequested;
|
||||
internal event Action<Vector3, float> WorkspaceViewTransformChanged;
|
||||
|
||||
internal FrameAnimationGraphView()
|
||||
{
|
||||
style.flexGrow = 1f;
|
||||
focusable = true;
|
||||
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
|
||||
this.AddManipulator(new ContentDragger());
|
||||
this.AddManipulator(new SelectionDragger());
|
||||
this.AddManipulator(new RectangleSelector());
|
||||
var grid = new GridBackground();
|
||||
Insert(0, grid);
|
||||
grid.StretchToParentSize();
|
||||
graphViewChanged = OnGraphViewChanged;
|
||||
viewTransformChanged += _ =>
|
||||
WorkspaceViewTransformChanged?.Invoke(viewTransform.position, viewTransform.scale.x);
|
||||
RegisterCallback<MouseUpEvent>(_ => NotifySelectionSummary());
|
||||
RegisterCallback<DragUpdatedEvent>(OnDragUpdated);
|
||||
RegisterCallback<DragPerformEvent>(OnDragPerform);
|
||||
RegisterCallback<KeyDownEvent>(OnKeyDown);
|
||||
}
|
||||
|
||||
internal void Bind(
|
||||
FrameAnimationGraph value,
|
||||
IReadOnlyList<FrameAnimationEditorIssue> validationIssues,
|
||||
string focusedFlowId)
|
||||
{
|
||||
graph = value;
|
||||
issues = validationIssues ?? Array.Empty<FrameAnimationEditorIssue>();
|
||||
var selectedIds = selection.OfType<FrameAnimationClipNodeView>()
|
||||
.Select(view => view.Data.InternalId).ToArray();
|
||||
var selectedEdgeIds = selection.OfType<FrameAnimationEdgeView>()
|
||||
.Select(view => view.Data.InternalId).ToArray();
|
||||
rebuilding = true;
|
||||
DeleteElements(graphElements.ToList());
|
||||
nodeViews.Clear();
|
||||
edgeViews.Clear();
|
||||
if (graph != null)
|
||||
{
|
||||
var topology = new FrameAnimationGraphTopology(graph);
|
||||
var entries = new HashSet<string>(graph.Flows.Where(flow => flow != null)
|
||||
.Select(flow => flow.EntryNodeId));
|
||||
foreach (var node in graph.Nodes.Where(node => node != null))
|
||||
{
|
||||
var view = new FrameAnimationClipNodeView(node, OnNodeSelected);
|
||||
view.SetPosition(new Rect(FrameAnimationGraphMutationService.GetNodePosition(graph, node),
|
||||
new Vector2(255f, 150f)));
|
||||
view.Refresh(graph, issues, topology.FindFlowsUsingNode(node.InternalId), entries);
|
||||
nodeViews[node.InternalId] = view;
|
||||
AddElement(view);
|
||||
}
|
||||
foreach (var edge in graph.Edges.Where(edge => edge != null))
|
||||
{
|
||||
if (!nodeViews.TryGetValue(edge.FromNodeId ?? string.Empty, out var from) ||
|
||||
!nodeViews.TryGetValue(edge.ToNodeId ?? string.Empty, out var to))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var view = new FrameAnimationEdgeView(edge, OnEdgeSelected)
|
||||
{
|
||||
output = from.OutputPort,
|
||||
input = to.InputPort
|
||||
};
|
||||
view.output.Connect(view);
|
||||
view.input.Connect(view);
|
||||
view.Refresh(issues);
|
||||
edgeViews[edge.InternalId] = view;
|
||||
AddElement(view);
|
||||
}
|
||||
foreach (var id in selectedIds.Where(nodeViews.ContainsKey))
|
||||
{
|
||||
AddToSelection(nodeViews[id]);
|
||||
}
|
||||
foreach (var id in selectedEdgeIds.Where(edgeViews.ContainsKey))
|
||||
{
|
||||
AddToSelection(edgeViews[id]);
|
||||
}
|
||||
}
|
||||
rebuilding = false;
|
||||
SetFocusedFlow(focusedFlowId);
|
||||
}
|
||||
|
||||
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
|
||||
{
|
||||
if (evt.target == this || evt.target == contentViewContainer)
|
||||
{
|
||||
contextPosition = contentViewContainer.WorldToLocal(evt.mousePosition);
|
||||
evt.menu.AppendAction("Create Clip Node", _ => OpenClipSearch(evt.mousePosition),
|
||||
graph != null && graph.Clips.Any(clip => clip != null)
|
||||
? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
|
||||
}
|
||||
if (selection.Count == 1 && selection[0] is FrameAnimationClipNodeView nodeView)
|
||||
{
|
||||
evt.menu.AppendAction("Create Flow From Node", _ => CreateFlowRequested?.Invoke(nodeView.Data));
|
||||
}
|
||||
base.BuildContextualMenu(evt);
|
||||
}
|
||||
|
||||
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
|
||||
{
|
||||
var result = new List<Port>();
|
||||
if (graph == null || !(startPort.node is FrameAnimationClipNodeView startNode))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
ports.ForEach(port =>
|
||||
{
|
||||
if (!(port.node is FrameAnimationClipNodeView targetNode) || port.direction == startPort.direction ||
|
||||
targetNode == startNode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var from = startPort.direction == Direction.Output ? startNode.Data : targetNode.Data;
|
||||
var to = startPort.direction == Direction.Output ? targetNode.Data : startNode.Data;
|
||||
if (FrameAnimationGraphMutationService.CanConnect(graph, from, to, out _))
|
||||
{
|
||||
result.Add(port);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
internal void SetFocusedFlow(string flowId)
|
||||
{
|
||||
focusedFlowId = flowId ?? string.Empty;
|
||||
if (graph == null || string.IsNullOrEmpty(flowId))
|
||||
{
|
||||
foreach (var view in nodeViews.Values) view.SetDimmed(false);
|
||||
foreach (var view in edgeViews.Values) view.SetDimmed(false);
|
||||
return;
|
||||
}
|
||||
var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == flowId);
|
||||
var topology = new FrameAnimationGraphTopology(graph);
|
||||
var reachable = flow != null
|
||||
? topology.GetReachable(flow.EntryNodeId)
|
||||
: new FrameAnimationReachability(Array.Empty<AnimationNode>(), Array.Empty<AnimationEdge>());
|
||||
var nodeIds = new HashSet<string>(reachable.Nodes.Select(node => node.InternalId));
|
||||
var edgeIds = new HashSet<string>(reachable.Edges.Select(edge => edge.InternalId));
|
||||
foreach (var pair in nodeViews) pair.Value.SetDimmed(!nodeIds.Contains(pair.Key));
|
||||
foreach (var pair in edgeViews) pair.Value.SetDimmed(!edgeIds.Contains(pair.Key));
|
||||
}
|
||||
|
||||
internal void SelectAndFrame(FrameAnimationEditorSelection target)
|
||||
{
|
||||
GraphElement element = target.Value switch
|
||||
{
|
||||
AnimationNode node when nodeViews.TryGetValue(node.InternalId, out var nodeView) => nodeView,
|
||||
AnimationEdge edge when edgeViews.TryGetValue(edge.InternalId, out var edgeView) => edgeView,
|
||||
_ => null
|
||||
};
|
||||
if (element == null) return;
|
||||
ClearSelection();
|
||||
AddToSelection(element);
|
||||
FrameSelection();
|
||||
}
|
||||
|
||||
internal IReadOnlyList<AnimationNode> SelectedNodes =>
|
||||
selection.OfType<FrameAnimationClipNodeView>().Select(view => view.Data).ToArray();
|
||||
|
||||
internal IReadOnlyList<AnimationNode> VisibleNodes => nodeViews.Values
|
||||
.Where(view => view.worldBound.Overlaps(worldBound))
|
||||
.Select(view => view.Data)
|
||||
.ToArray();
|
||||
|
||||
internal void ApplyPreview(FrameAnimationPreviewCoordinator coordinator)
|
||||
{
|
||||
if (coordinator == null) return;
|
||||
foreach (var view in nodeViews.Values)
|
||||
{
|
||||
view.SetPreview(coordinator.GetNodeDisplay(view.Data), coordinator.Background);
|
||||
}
|
||||
foreach (var view in edgeViews.Values)
|
||||
{
|
||||
view.SetPreviewState(coordinator.GetEdgeState(view.Data));
|
||||
}
|
||||
}
|
||||
|
||||
internal void RestoreViewTransform(Vector3 position, float scale)
|
||||
{
|
||||
var safeScale = float.IsNaN(scale) || float.IsInfinity(scale)
|
||||
? 1f : Mathf.Clamp(scale, ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
|
||||
if (float.IsNaN(position.x) || float.IsInfinity(position.x) ||
|
||||
float.IsNaN(position.y) || float.IsInfinity(position.y))
|
||||
{
|
||||
position = Vector3.zero;
|
||||
}
|
||||
UpdateViewTransform(position, new Vector3(safeScale, safeScale, 1f));
|
||||
}
|
||||
|
||||
internal void RequestDeleteNodes(IEnumerable<AnimationNode> nodes)
|
||||
{
|
||||
var items = (nodes ?? Array.Empty<AnimationNode>()).Where(node => node != null).Distinct().ToArray();
|
||||
if (graph == null || items.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var ids = new HashSet<string>(items.Select(node => node.InternalId));
|
||||
var entryFlows = graph.Flows.Where(flow => flow != null && ids.Contains(flow.EntryNodeId)).ToArray();
|
||||
var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeNodeRemoval(graph, items);
|
||||
if (!EditorUtility.DisplayDialog(
|
||||
"Delete Frame Animation Nodes",
|
||||
BuildRemovalMessage("Node", items.Length, impact, entryFlows),
|
||||
entryFlows.Length > 0 ? "Delete Nodes + Flows" : "Delete",
|
||||
"Cancel"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!FrameAnimationGraphMutationService.RemoveNodes(graph, items, entryFlows.Length > 0, out var error))
|
||||
{
|
||||
NotificationRequested?.Invoke(error);
|
||||
return;
|
||||
}
|
||||
GraphChanged?.Invoke();
|
||||
}
|
||||
|
||||
internal void RequestDeleteEdges(IEnumerable<AnimationEdge> edges)
|
||||
{
|
||||
var items = (edges ?? Array.Empty<AnimationEdge>()).Where(edge => edge != null).Distinct().ToArray();
|
||||
if (graph == null || items.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeEdgeRemoval(graph, items);
|
||||
if (impact.Flows.Count > 0 && !EditorUtility.DisplayDialog(
|
||||
"Disconnect Frame Animation Nodes",
|
||||
BuildRemovalMessage("Edge", items.Length, impact, Array.Empty<AnimationFlow>()),
|
||||
"Disconnect",
|
||||
"Cancel"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
FrameAnimationGraphMutationService.RemoveEdges(graph, items);
|
||||
GraphChanged?.Invoke();
|
||||
}
|
||||
|
||||
internal void FrameCurrentFlow(string flowId)
|
||||
{
|
||||
if (graph == null || string.IsNullOrEmpty(flowId)) return;
|
||||
var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == flowId);
|
||||
if (flow == null) return;
|
||||
var ids = new HashSet<string>(new FrameAnimationGraphTopology(graph)
|
||||
.GetReachable(flow.EntryNodeId).Nodes.Select(node => node.InternalId));
|
||||
ClearSelection();
|
||||
foreach (var pair in nodeViews.Where(pair => ids.Contains(pair.Key))) AddToSelection(pair.Value);
|
||||
FrameSelection();
|
||||
}
|
||||
|
||||
internal void ApplyCalculatedPositions(IReadOnlyDictionary<AnimationNode, Vector2> positions)
|
||||
{
|
||||
foreach (var pair in positions)
|
||||
{
|
||||
if (nodeViews.TryGetValue(pair.Key.InternalId, out var view))
|
||||
{
|
||||
var rect = view.GetPosition();
|
||||
rect.position = pair.Value;
|
||||
view.SetPosition(rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenClipSearch(Vector2 panelPosition)
|
||||
{
|
||||
var provider = ScriptableObject.CreateInstance<FrameAnimationClipSearchProvider>();
|
||||
provider.Configure(graph, clip => CreateNode(clip, contextPosition));
|
||||
SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(panelPosition)), provider);
|
||||
}
|
||||
|
||||
internal void CreateNode(FrameClip clip, Vector2 position)
|
||||
{
|
||||
if (FrameAnimationGraphMutationService.CreateNode(graph, clip, position, out var node, out var error))
|
||||
{
|
||||
NodeCreated?.Invoke(node);
|
||||
GraphChanged?.Invoke();
|
||||
SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node));
|
||||
}
|
||||
else
|
||||
{
|
||||
NotificationRequested?.Invoke(error);
|
||||
}
|
||||
}
|
||||
|
||||
private GraphViewChange OnGraphViewChanged(GraphViewChange change)
|
||||
{
|
||||
if (rebuilding || graph == null)
|
||||
{
|
||||
return change;
|
||||
}
|
||||
if (change.edgesToCreate != null)
|
||||
{
|
||||
foreach (var visual in change.edgesToCreate.ToArray())
|
||||
{
|
||||
var error = string.Empty;
|
||||
AnimationEdge data = null;
|
||||
var from = visual.output?.node as FrameAnimationClipNodeView;
|
||||
var to = visual.input?.node as FrameAnimationClipNodeView;
|
||||
if (from == null || to == null ||
|
||||
!FrameAnimationGraphMutationService.TryConnect(
|
||||
graph, from.Data, to.Data, out data, out error))
|
||||
{
|
||||
change.edgesToCreate.Remove(visual);
|
||||
NotificationRequested?.Invoke(string.IsNullOrEmpty(error) ? "无法创建连接。" : error);
|
||||
continue;
|
||||
}
|
||||
visual.userData = data;
|
||||
}
|
||||
GraphChanged?.Invoke();
|
||||
}
|
||||
if (change.elementsToRemove != null)
|
||||
{
|
||||
ProcessRemovals(change);
|
||||
}
|
||||
if (change.movedElements != null)
|
||||
{
|
||||
var positions = change.movedElements.OfType<FrameAnimationClipNodeView>()
|
||||
.ToDictionary(view => view.Data, view => view.GetPosition().position);
|
||||
if (positions.Count > 0)
|
||||
{
|
||||
FrameAnimationGraphMutationService.SetNodePositions(graph, positions,
|
||||
"Move Frame Animation Nodes");
|
||||
GraphChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
return change;
|
||||
}
|
||||
|
||||
private void ProcessRemovals(GraphViewChange change)
|
||||
{
|
||||
var nodeViewsToRemove = change.elementsToRemove.OfType<FrameAnimationClipNodeView>().ToArray();
|
||||
var removedNodeIds = new HashSet<string>(nodeViewsToRemove.Select(view => view.Data.InternalId));
|
||||
if (nodeViewsToRemove.Length > 0)
|
||||
{
|
||||
var nodes = nodeViewsToRemove.Select(view => view.Data).ToArray();
|
||||
var explicitlySelectedEdges = change.elementsToRemove.OfType<Edge>()
|
||||
.Select(edge => edge.userData as AnimationEdge)
|
||||
.Where(edge => edge != null).Distinct().ToArray();
|
||||
var entryFlows = graph.Flows.Where(flow => flow != null && removedNodeIds.Contains(flow.EntryNodeId)).ToArray();
|
||||
var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeRemoval(
|
||||
graph, nodes, explicitlySelectedEdges);
|
||||
var message = BuildRemovalMessage("Node", nodes.Length, impact, entryFlows);
|
||||
if (explicitlySelectedEdges.Length > 0)
|
||||
{
|
||||
message += $"\n\n同时删除选中的 Edge:{explicitlySelectedEdges.Length}";
|
||||
}
|
||||
var confirmed = EditorUtility.DisplayDialog("Delete Frame Animation Nodes", message,
|
||||
entryFlows.Length > 0 ? "Delete Nodes + Flows" : "Delete", "Cancel");
|
||||
var removed = false;
|
||||
var error = string.Empty;
|
||||
if (confirmed)
|
||||
{
|
||||
removed = FrameAnimationGraphMutationService.RemoveNodesAndEdges(
|
||||
graph, nodes, explicitlySelectedEdges, entryFlows.Length > 0, out error);
|
||||
}
|
||||
if (!removed)
|
||||
{
|
||||
change.elementsToRemove.Clear();
|
||||
if (!string.IsNullOrEmpty(error)) NotificationRequested?.Invoke(error);
|
||||
}
|
||||
GraphChanged?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
var remainingEdges = change.elementsToRemove.OfType<Edge>()
|
||||
.Where(edge => !IsIncidentTo(edge, removedNodeIds))
|
||||
.Select(edge => edge.userData as AnimationEdge).Where(edge => edge != null).Distinct().ToArray();
|
||||
if (remainingEdges.Length > 0)
|
||||
{
|
||||
var impact = FrameAnimationGraphImpactAnalyzer.AnalyzeEdgeRemoval(graph, remainingEdges);
|
||||
if (impact.Flows.Count > 0 && !EditorUtility.DisplayDialog("Disconnect Frame Animation Nodes",
|
||||
BuildRemovalMessage("Edge", remainingEdges.Length, impact, Array.Empty<AnimationFlow>()),
|
||||
"Disconnect", "Cancel"))
|
||||
{
|
||||
foreach (var edgeView in change.elementsToRemove.OfType<Edge>()
|
||||
.Where(view => remainingEdges.Contains(view.userData as AnimationEdge)).ToArray())
|
||||
{
|
||||
change.elementsToRemove.Remove(edgeView);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FrameAnimationGraphMutationService.RemoveEdges(graph, remainingEdges);
|
||||
}
|
||||
}
|
||||
GraphChanged?.Invoke();
|
||||
}
|
||||
|
||||
private static bool IsIncidentTo(Edge edge, IReadOnlyCollection<string> nodeIds)
|
||||
{
|
||||
var from = edge?.output?.node as FrameAnimationClipNodeView;
|
||||
var to = edge?.input?.node as FrameAnimationClipNodeView;
|
||||
return from != null && nodeIds.Contains(from.Data.InternalId) ||
|
||||
to != null && nodeIds.Contains(to.Data.InternalId);
|
||||
}
|
||||
|
||||
private static string BuildRemovalMessage(
|
||||
string kind,
|
||||
int count,
|
||||
FrameAnimationGraphImpact impact,
|
||||
IReadOnlyCollection<AnimationFlow> entryFlows)
|
||||
{
|
||||
var message = $"将删除 {count} 个 {kind}。";
|
||||
if (impact.Flows.Count > 0)
|
||||
{
|
||||
message += $"\n\n受影响 Flow:{string.Join(", ", impact.Flows.Select(flow => flow.Id))}" +
|
||||
$"\n将失去可达关系的 Node:{impact.LostNodeCount}";
|
||||
}
|
||||
if (entryFlows.Count > 0)
|
||||
{
|
||||
message += $"\n\n入口 Flow 将同时删除:{string.Join(", ", entryFlows.Select(flow => flow.Id))}";
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
private void OnNodeSelected(FrameAnimationClipNodeView view)
|
||||
{
|
||||
if (rebuilding)
|
||||
{
|
||||
return;
|
||||
}
|
||||
SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, view.Data));
|
||||
NotifySelectionSummary();
|
||||
}
|
||||
|
||||
private void OnEdgeSelected(FrameAnimationEdgeView view)
|
||||
{
|
||||
if (rebuilding)
|
||||
{
|
||||
return;
|
||||
}
|
||||
SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Edge, view.Data));
|
||||
NotifySelectionSummary();
|
||||
}
|
||||
|
||||
private void NotifySelectionSummary()
|
||||
{
|
||||
if (rebuilding)
|
||||
{
|
||||
return;
|
||||
}
|
||||
MultiSelectionChanged?.Invoke(SelectedNodes);
|
||||
var selected = selection.Select(item => item switch
|
||||
{
|
||||
FrameAnimationClipNodeView nodeView => new FrameAnimationEditorSelection(
|
||||
FrameAnimationEditorSelectionKind.Node, nodeView.Data),
|
||||
FrameAnimationEdgeView edgeView => new FrameAnimationEditorSelection(
|
||||
FrameAnimationEditorSelectionKind.Edge, edgeView.Data),
|
||||
_ => default
|
||||
})
|
||||
.Where(item => item.Kind != FrameAnimationEditorSelectionKind.None)
|
||||
.ToArray();
|
||||
SelectionSetChanged?.Invoke(selected);
|
||||
}
|
||||
|
||||
private void OnKeyDown(KeyDownEvent evt)
|
||||
{
|
||||
if (!TryHandleFocusExitShortcut(evt.keyCode, evt.target is TextField))
|
||||
{
|
||||
return;
|
||||
}
|
||||
evt.StopImmediatePropagation();
|
||||
}
|
||||
|
||||
internal bool TryHandleFocusExitShortcut(KeyCode keyCode, bool textInputActive)
|
||||
{
|
||||
if (keyCode != KeyCode.Escape || textInputActive || string.IsNullOrEmpty(focusedFlowId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ExitFocusRequested?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnDragUpdated(DragUpdatedEvent evt)
|
||||
{
|
||||
if (DragAndDrop.GetGenericData(ClipDragKey) is FrameClip clip && graph?.Clips.Contains(clip) == true)
|
||||
{
|
||||
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
||||
evt.StopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDragPerform(DragPerformEvent evt)
|
||||
{
|
||||
if (!(DragAndDrop.GetGenericData(ClipDragKey) is FrameClip clip) || graph?.Clips.Contains(clip) != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
DragAndDrop.AcceptDrag();
|
||||
CreateNode(clip, contentViewContainer.WorldToLocal(evt.mousePosition));
|
||||
DragAndDrop.SetGenericData(ClipDragKey, null);
|
||||
evt.StopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ffd56cf8d10f4ff9b33510a268d6b615
|
||||
@@ -0,0 +1,347 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal enum FrameAnimationImportIssueCode
|
||||
{
|
||||
JsonMissing,
|
||||
TextureMissing,
|
||||
JsonInvalid,
|
||||
FramesMissing,
|
||||
FrameNameInvalid,
|
||||
FrameNameDuplicate,
|
||||
FrameDurationInvalid,
|
||||
FrameRectInvalid,
|
||||
TextureSizeMismatch,
|
||||
TrimmedUnsupported,
|
||||
RotatedUnsupported,
|
||||
TagNameInvalid,
|
||||
TagNameDuplicate,
|
||||
TagRangeInvalid,
|
||||
TagDirectionInvalid,
|
||||
TagConflict,
|
||||
PlayableIdConflict,
|
||||
ImportSourceIdInvalid,
|
||||
ImportSourceIdDuplicate,
|
||||
ImportSourceReferenceInvalid,
|
||||
ImportedClipOwnershipInvalid,
|
||||
TextureOwnershipConflict,
|
||||
SpriteMatchFailed,
|
||||
TextureImporterInvalid,
|
||||
TextureWriteFailed,
|
||||
SourceChanged,
|
||||
ImageNameMismatch,
|
||||
NoTags,
|
||||
ApplyFailed
|
||||
}
|
||||
|
||||
internal enum FrameAnimationImportChangeKind
|
||||
{
|
||||
Added,
|
||||
Updated,
|
||||
Missing,
|
||||
Unchanged,
|
||||
Error
|
||||
}
|
||||
|
||||
internal enum FrameAnimationSpriteChangeKind
|
||||
{
|
||||
Added,
|
||||
Updated,
|
||||
Retained,
|
||||
Unchanged,
|
||||
Error
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationImportIssue
|
||||
{
|
||||
public FrameAnimationValidationSeverity Severity { get; }
|
||||
public FrameAnimationImportIssueCode Code { get; }
|
||||
public string SourceId { get; }
|
||||
public string TargetId { get; }
|
||||
public string Message { get; }
|
||||
|
||||
public FrameAnimationImportIssue(
|
||||
FrameAnimationValidationSeverity severity,
|
||||
FrameAnimationImportIssueCode code,
|
||||
string sourceId,
|
||||
string targetId,
|
||||
string message)
|
||||
{
|
||||
Severity = severity;
|
||||
Code = code;
|
||||
SourceId = sourceId ?? string.Empty;
|
||||
TargetId = targetId ?? string.Empty;
|
||||
Message = message ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class AsepriteSourceFrame
|
||||
{
|
||||
public string FrameName { get; }
|
||||
public RectInt Rect { get; }
|
||||
public int DurationMs { get; }
|
||||
public bool Rotated { get; }
|
||||
public bool Trimmed { get; }
|
||||
public RectInt SpriteSourceSize { get; }
|
||||
public Vector2Int SourceSize { get; }
|
||||
|
||||
public AsepriteSourceFrame(
|
||||
string frameName,
|
||||
RectInt rect,
|
||||
int durationMs,
|
||||
bool rotated,
|
||||
bool trimmed,
|
||||
RectInt spriteSourceSize,
|
||||
Vector2Int sourceSize)
|
||||
{
|
||||
FrameName = frameName ?? string.Empty;
|
||||
Rect = rect;
|
||||
DurationMs = durationMs;
|
||||
Rotated = rotated;
|
||||
Trimmed = trimmed;
|
||||
SpriteSourceSize = spriteSourceSize;
|
||||
SourceSize = sourceSize;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class AsepriteSourceTag
|
||||
{
|
||||
public string Name { get; }
|
||||
public int From { get; }
|
||||
public int To { get; }
|
||||
public string Direction { get; }
|
||||
|
||||
public AsepriteSourceTag(string name, int from, int to, string direction)
|
||||
{
|
||||
Name = name ?? string.Empty;
|
||||
From = from;
|
||||
To = to;
|
||||
Direction = direction ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class AsepriteSpriteSlot
|
||||
{
|
||||
public RectInt Rect { get; }
|
||||
public string CanonicalFrameName { get; }
|
||||
public IReadOnlyList<int> SourceIndices { get; }
|
||||
public IReadOnlyList<string> FrameNames { get; }
|
||||
|
||||
public AsepriteSpriteSlot(
|
||||
RectInt rect,
|
||||
string canonicalFrameName,
|
||||
IReadOnlyList<int> sourceIndices,
|
||||
IReadOnlyList<string> frameNames)
|
||||
{
|
||||
Rect = rect;
|
||||
CanonicalFrameName = canonicalFrameName ?? string.Empty;
|
||||
SourceIndices = sourceIndices ?? Array.Empty<int>();
|
||||
FrameNames = frameNames ?? Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class AsepriteSourceDocument
|
||||
{
|
||||
public IReadOnlyList<AsepriteSourceFrame> Frames { get; }
|
||||
public IReadOnlyList<AsepriteSpriteSlot> SpriteSlots { get; }
|
||||
public IReadOnlyDictionary<int, AsepriteSpriteSlot> SpriteSlotBySourceIndex { get; }
|
||||
public IReadOnlyList<AsepriteSourceTag> Tags { get; }
|
||||
public Vector2Int TextureSize { get; }
|
||||
public string ImageName { get; }
|
||||
|
||||
public AsepriteSourceDocument(
|
||||
IReadOnlyList<AsepriteSourceFrame> frames,
|
||||
IReadOnlyList<AsepriteSourceTag> tags,
|
||||
Vector2Int textureSize,
|
||||
string imageName)
|
||||
{
|
||||
Frames = frames ?? Array.Empty<AsepriteSourceFrame>();
|
||||
BuildSpriteSlots(Frames, out var spriteSlots, out var slotBySourceIndex);
|
||||
SpriteSlots = spriteSlots;
|
||||
SpriteSlotBySourceIndex = slotBySourceIndex;
|
||||
Tags = tags ?? Array.Empty<AsepriteSourceTag>();
|
||||
TextureSize = textureSize;
|
||||
ImageName = imageName ?? string.Empty;
|
||||
}
|
||||
|
||||
private static void BuildSpriteSlots(
|
||||
IReadOnlyList<AsepriteSourceFrame> frames,
|
||||
out IReadOnlyList<AsepriteSpriteSlot> spriteSlots,
|
||||
out IReadOnlyDictionary<int, AsepriteSpriteSlot> slotBySourceIndex)
|
||||
{
|
||||
var builders = new List<SpriteSlotBuilder>();
|
||||
var buildersByRect = new Dictionary<RectInt, SpriteSlotBuilder>();
|
||||
for (var sourceIndex = 0; sourceIndex < frames.Count; sourceIndex++)
|
||||
{
|
||||
var frame = frames[sourceIndex];
|
||||
if (!buildersByRect.TryGetValue(frame.Rect, out var builder))
|
||||
{
|
||||
builder = new SpriteSlotBuilder(frame.Rect, frame.FrameName);
|
||||
buildersByRect.Add(frame.Rect, builder);
|
||||
builders.Add(builder);
|
||||
}
|
||||
builder.SourceIndices.Add(sourceIndex);
|
||||
builder.FrameNames.Add(frame.FrameName);
|
||||
}
|
||||
|
||||
var slots = builders.Select(builder => builder.Build()).ToArray();
|
||||
var index = new Dictionary<int, AsepriteSpriteSlot>();
|
||||
foreach (var slot in slots)
|
||||
{
|
||||
foreach (var sourceIndex in slot.SourceIndices)
|
||||
{
|
||||
index[sourceIndex] = slot;
|
||||
}
|
||||
}
|
||||
|
||||
spriteSlots = slots;
|
||||
slotBySourceIndex = index;
|
||||
}
|
||||
|
||||
private sealed class SpriteSlotBuilder
|
||||
{
|
||||
public RectInt Rect { get; }
|
||||
public string CanonicalFrameName { get; }
|
||||
public List<int> SourceIndices { get; } = new List<int>();
|
||||
public List<string> FrameNames { get; } = new List<string>();
|
||||
|
||||
public SpriteSlotBuilder(RectInt rect, string canonicalFrameName)
|
||||
{
|
||||
Rect = rect;
|
||||
CanonicalFrameName = canonicalFrameName ?? string.Empty;
|
||||
}
|
||||
|
||||
public AsepriteSpriteSlot Build()
|
||||
{
|
||||
return new AsepriteSpriteSlot(
|
||||
Rect,
|
||||
CanonicalFrameName,
|
||||
SourceIndices.ToArray(),
|
||||
FrameNames.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationClipDiff
|
||||
{
|
||||
public FrameAnimationImportChangeKind Kind { get; }
|
||||
public string SourceTagName { get; }
|
||||
public FrameClip Clip { get; }
|
||||
public IReadOnlyList<int> SourceIndices { get; }
|
||||
public string Summary { get; }
|
||||
|
||||
public FrameAnimationClipDiff(
|
||||
FrameAnimationImportChangeKind kind,
|
||||
string sourceTagName,
|
||||
FrameClip clip,
|
||||
IReadOnlyList<int> sourceIndices,
|
||||
string summary)
|
||||
{
|
||||
Kind = kind;
|
||||
SourceTagName = sourceTagName ?? string.Empty;
|
||||
Clip = clip;
|
||||
SourceIndices = sourceIndices ?? Array.Empty<int>();
|
||||
Summary = summary ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationSpriteDiff
|
||||
{
|
||||
public FrameAnimationSpriteChangeKind Kind { get; }
|
||||
public string FrameName { get; }
|
||||
public IReadOnlyList<string> AliasFrameNames { get; }
|
||||
public int SourceFrameCount => AliasFrameNames.Count;
|
||||
public Rect Rect { get; }
|
||||
public string Summary { get; }
|
||||
|
||||
public FrameAnimationSpriteDiff(
|
||||
FrameAnimationSpriteChangeKind kind,
|
||||
string frameName,
|
||||
Rect rect,
|
||||
string summary,
|
||||
IReadOnlyList<string> aliasFrameNames = null)
|
||||
{
|
||||
Kind = kind;
|
||||
FrameName = frameName ?? string.Empty;
|
||||
AliasFrameNames = aliasFrameNames ?? Array.Empty<string>();
|
||||
Rect = rect;
|
||||
Summary = summary ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationImportSourcePreview
|
||||
{
|
||||
private readonly List<FrameAnimationImportIssue> issues = new List<FrameAnimationImportIssue>();
|
||||
private readonly List<FrameAnimationClipDiff> clipDiffs = new List<FrameAnimationClipDiff>();
|
||||
private readonly List<FrameAnimationSpriteDiff> spriteDiffs = new List<FrameAnimationSpriteDiff>();
|
||||
|
||||
public FrameAnimationImportSource Source { get; }
|
||||
public AsepriteSourceDocument Document { get; internal set; }
|
||||
public FrameAnimationSpritePlan SpritePlan { get; internal set; }
|
||||
public string CurrentHash { get; internal set; } = string.Empty;
|
||||
public IReadOnlyList<FrameAnimationImportIssue> Issues => issues;
|
||||
public IReadOnlyList<FrameAnimationClipDiff> ClipDiffs => clipDiffs;
|
||||
public IReadOnlyList<FrameAnimationSpriteDiff> SpriteDiffs => spriteDiffs;
|
||||
public bool HasErrors => issues.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error);
|
||||
public bool HasSpriteChanges => spriteDiffs.Any(diff =>
|
||||
diff.Kind == FrameAnimationSpriteChangeKind.Added ||
|
||||
diff.Kind == FrameAnimationSpriteChangeKind.Updated);
|
||||
|
||||
public FrameAnimationImportSourcePreview(FrameAnimationImportSource source)
|
||||
{
|
||||
Source = source;
|
||||
}
|
||||
|
||||
public void AddIssue(FrameAnimationImportIssue issue)
|
||||
{
|
||||
if (issue != null)
|
||||
{
|
||||
issues.Add(issue);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddClipDiff(FrameAnimationClipDiff diff)
|
||||
{
|
||||
if (diff != null)
|
||||
{
|
||||
clipDiffs.Add(diff);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddSpriteDiff(FrameAnimationSpriteDiff diff)
|
||||
{
|
||||
if (diff != null)
|
||||
{
|
||||
spriteDiffs.Add(diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationImportPreview
|
||||
{
|
||||
private readonly List<FrameAnimationImportSourcePreview> sources =
|
||||
new List<FrameAnimationImportSourcePreview>();
|
||||
|
||||
public FrameAnimationGraph Graph { get; }
|
||||
public IReadOnlyList<FrameAnimationImportSourcePreview> Sources => sources;
|
||||
public bool HasErrors => sources.Any(source => source.HasErrors);
|
||||
public bool HasSpriteChanges => sources.Any(source => source.HasSpriteChanges);
|
||||
|
||||
public FrameAnimationImportPreview(FrameAnimationGraph graph)
|
||||
{
|
||||
Graph = graph;
|
||||
}
|
||||
|
||||
public void AddSource(FrameAnimationImportSourcePreview preview)
|
||||
{
|
||||
if (preview != null)
|
||||
{
|
||||
sources.Add(preview);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9d29e9107c2ec549916dfb45e638943
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,335 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
public static class FrameAnimationImportSampleBuilder
|
||||
{
|
||||
private const string SampleFolder = "Assets/GameContent/Test/FrameAnimation/Import";
|
||||
private const string ReadOnlyTexturePath = SampleFolder + "/中文预切图.png";
|
||||
private const string WritableTexturePath = SampleFolder + "/中文自动切图.png";
|
||||
private const string ObjectJsonPath = SampleFolder + "/中文Object.json";
|
||||
private const string ArrayJsonPath = SampleFolder + "/中文Array.json";
|
||||
private const string GraphPath = SampleFolder + "/ImportSampleGraph.asset";
|
||||
private const string IdleNodeId = "40000000000000000000000000000001";
|
||||
private const string BlinkNodeId = "40000000000000000000000000000002";
|
||||
private const string SharedNodeId = "40000000000000000000000000000003";
|
||||
private const string IdleEdgeId = "40000000000000000000000000000011";
|
||||
private const string BlinkEdgeId = "40000000000000000000000000000012";
|
||||
private const string IdleFlowId = "SampleIdleToSharedFlow";
|
||||
private const string BlinkFlowId = "SampleBlinkToSharedFlow";
|
||||
|
||||
[MenuItem("Tools/Frame Animation/Rebuild Import Sample")]
|
||||
private static void BuildFromMenu()
|
||||
{
|
||||
BuildFromCommandLine();
|
||||
}
|
||||
|
||||
public static void BuildFromCommandLine()
|
||||
{
|
||||
EnsureFolder(SampleFolder);
|
||||
if (AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(GraphPath) != null)
|
||||
{
|
||||
AssetDatabase.DeleteAsset(GraphPath);
|
||||
}
|
||||
|
||||
WriteTexture(ReadOnlyTexturePath);
|
||||
WriteTexture(WritableTexturePath);
|
||||
WriteTextAsset(ObjectJsonPath, CreateObjectJson("中文预切图.png", "待机", "眨眼"));
|
||||
WriteTextAsset(ArrayJsonPath, CreateArrayJson("中文自动切图.png", "转身", "惊讶"));
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
|
||||
var readOnlyTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(ReadOnlyTexturePath);
|
||||
var objectJson = AssetDatabase.LoadAssetAtPath<TextAsset>(ObjectJsonPath);
|
||||
if (!AsepriteJsonParser.TryParse(objectJson.text, "sample-slicing", out var document, out var parseIssue))
|
||||
{
|
||||
throw new InvalidOperationException(parseIssue.Message);
|
||||
}
|
||||
var slicingSource = new FrameAnimationImportSource(
|
||||
"Prepare Read Only Texture", readOnlyTexture, objectJson, new Vector2(0.5f, 0.5f), true);
|
||||
var slicingPreview = new FrameAnimationImportSourcePreview(slicingSource) { Document = document };
|
||||
var slicingPlan = FrameAnimationSpriteUtility.BuildPlan(slicingSource, document, slicingPreview);
|
||||
if (slicingPreview.HasErrors)
|
||||
{
|
||||
throw new InvalidOperationException(string.Join("\n", System.Linq.Enumerable.Select(
|
||||
slicingPreview.Issues, issue => issue.Message)));
|
||||
}
|
||||
FrameAnimationSpriteUtility.ApplyPlan(slicingPlan);
|
||||
|
||||
var graph = ScriptableObject.CreateInstance<FrameAnimationGraph>();
|
||||
graph.Configure(
|
||||
"ImportSampleGraph",
|
||||
"Aseprite 导入与稳定刷新样例",
|
||||
Array.Empty<FrameClip>(),
|
||||
Array.Empty<AnimationNode>(),
|
||||
Array.Empty<AnimationEdge>(),
|
||||
Array.Empty<AnimationFlow>(),
|
||||
string.Empty);
|
||||
graph.AddImportSource(new FrameAnimationImportSource(
|
||||
"Object / 只读切图",
|
||||
AssetDatabase.LoadAssetAtPath<Texture2D>(ReadOnlyTexturePath),
|
||||
AssetDatabase.LoadAssetAtPath<TextAsset>(ObjectJsonPath),
|
||||
new Vector2(0.5f, 0.5f),
|
||||
false));
|
||||
graph.AddImportSource(new FrameAnimationImportSource(
|
||||
"Array / 自动切图",
|
||||
AssetDatabase.LoadAssetAtPath<Texture2D>(WritableTexturePath),
|
||||
AssetDatabase.LoadAssetAtPath<TextAsset>(ArrayJsonPath),
|
||||
new Vector2(0.5f, 0.5f),
|
||||
true));
|
||||
AssetDatabase.CreateAsset(graph, GraphPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
var preview = FrameAnimationImportService.PreviewAll(graph);
|
||||
if (!FrameAnimationImportService.Apply(preview, true, out var error))
|
||||
{
|
||||
throw new InvalidOperationException(error + "\n" + string.Join("\n",
|
||||
System.Linq.Enumerable.SelectMany(preview.Sources,
|
||||
source => System.Linq.Enumerable.Select(source.Issues, issue => issue.Message))));
|
||||
}
|
||||
|
||||
ExtendForWorkspaceSample(graph);
|
||||
EditorUtility.SetDirty(graph);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
Selection.activeObject = graph;
|
||||
Debug.Log($"Frame Animation import sample rebuilt: {GraphPath}");
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Frame Animation/Upgrade Phase 4 Sample (Non-Destructive)")]
|
||||
public static void UpgradePhase4Sample()
|
||||
{
|
||||
var graph = AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(GraphPath);
|
||||
if (graph == null)
|
||||
{
|
||||
Debug.LogError($"Frame Animation sample Graph not found: {GraphPath}");
|
||||
return;
|
||||
}
|
||||
var idleClip = graph.Clips.FirstOrDefault(clip => clip?.ImportInfo?.SourceTagName == "待机");
|
||||
var blinkClip = graph.Clips.FirstOrDefault(clip => clip?.ImportInfo?.SourceTagName == "眨眼");
|
||||
var sharedClip = AssetDatabase.LoadAssetAtPath<FrameClip>(
|
||||
"Assets/GameContent/Test/FrameAnimation/Idle.asset") ??
|
||||
graph.Clips.FirstOrDefault(clip => clip != null && !clip.IsImported);
|
||||
if (idleClip == null || blinkClip == null || sharedClip == null || !graph.Clips.Contains(sharedClip))
|
||||
{
|
||||
Debug.LogError("Phase 4 sample upgrade requires 待机、眨眼 and a Manual Clip already referenced by the Graph.");
|
||||
return;
|
||||
}
|
||||
|
||||
Undo.RecordObject(graph, "Upgrade Frame Animation Phase 4 Sample");
|
||||
var idleNode = GetOrAddNode(graph, IdleNodeId, idleClip, "待机 Node");
|
||||
var blinkNode = GetOrAddNode(graph, BlinkNodeId, blinkClip, "眨眼 Node");
|
||||
var sharedNode = graph.Nodes.FirstOrDefault(node => node != null && node.InternalId == SharedNodeId);
|
||||
if (sharedNode == null)
|
||||
{
|
||||
sharedNode = new AnimationNode(sharedClip.Id, "Shared Idle Node",
|
||||
FrameClipEndBehavior.Loop, internalId: SharedNodeId);
|
||||
graph.AddNode(sharedNode);
|
||||
}
|
||||
GetOrAddEdge(graph, IdleEdgeId, idleNode, sharedNode);
|
||||
GetOrAddEdge(graph, BlinkEdgeId, blinkNode, sharedNode);
|
||||
GetOrAddFlow(graph, IdleFlowId, "待机到共享 Idle", idleNode,
|
||||
new Color(0.24f, 0.65f, 1f));
|
||||
GetOrAddFlow(graph, BlinkFlowId, "眨眼到共享 Idle", blinkNode,
|
||||
new Color(1f, 0.62f, 0.24f));
|
||||
graph.EditorData.GetOrCreateNodeData(IdleNodeId, new Vector2(0f, 0f));
|
||||
graph.EditorData.GetOrCreateNodeData(BlinkNodeId, new Vector2(0f, 220f));
|
||||
graph.EditorData.GetOrCreateNodeData(SharedNodeId, new Vector2(360f, 110f));
|
||||
EditorUtility.SetDirty(graph);
|
||||
AssetDatabase.SaveAssets();
|
||||
Selection.activeObject = graph;
|
||||
Debug.Log("Frame Animation Phase 4 sample upgraded without rebuilding existing assets or sources.");
|
||||
}
|
||||
|
||||
private static AnimationNode GetOrAddNode(
|
||||
FrameAnimationGraph graph,
|
||||
string internalId,
|
||||
FrameClip clip,
|
||||
string displayName)
|
||||
{
|
||||
var node = graph.Nodes.FirstOrDefault(item => item != null && item.InternalId == internalId);
|
||||
if (node != null)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
node = new AnimationNode(clip.Id, displayName, internalId: internalId);
|
||||
graph.AddNode(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
private static void GetOrAddEdge(
|
||||
FrameAnimationGraph graph,
|
||||
string internalId,
|
||||
AnimationNode from,
|
||||
AnimationNode to)
|
||||
{
|
||||
if (graph.Edges.Any(edge => edge != null && edge.InternalId == internalId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
graph.AddEdge(new AnimationEdge(from.InternalId, to.InternalId, internalId));
|
||||
}
|
||||
|
||||
private static void GetOrAddFlow(
|
||||
FrameAnimationGraph graph,
|
||||
string id,
|
||||
string displayName,
|
||||
AnimationNode entry,
|
||||
Color color)
|
||||
{
|
||||
var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == id);
|
||||
if (flow == null && FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id))
|
||||
{
|
||||
flow = new AnimationFlow(id, displayName, entry.InternalId);
|
||||
graph.AddFlow(flow);
|
||||
}
|
||||
if (flow != null)
|
||||
{
|
||||
graph.EditorData.GetOrCreateFlowData(flow.Id, color);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtendForWorkspaceSample(FrameAnimationGraph graph)
|
||||
{
|
||||
var imported = graph.Clips.First(clip => clip.ImportInfo?.SourceTagName == "待机");
|
||||
var manual = ScriptableObject.CreateInstance<FrameClip>();
|
||||
manual.name = "ManualWorkspaceSample";
|
||||
manual.Configure(
|
||||
"ManualWorkspaceSample",
|
||||
"Graph 内 Manual 样例",
|
||||
imported.Frames.Take(1).Select(frame => new FrameAnimationFrame(
|
||||
frame.Sprite, frame.DurationMs, string.Empty, -1)),
|
||||
1f,
|
||||
FrameClipEndBehavior.HoldLastFrame);
|
||||
AssetDatabase.AddObjectToAsset(manual, graph);
|
||||
|
||||
var clips = graph.Clips.Concat(new[] { manual }).ToList();
|
||||
var external = AssetDatabase.LoadAssetAtPath<FrameClip>(
|
||||
"Assets/GameContent/Test/FrameAnimation/Idle.asset");
|
||||
if (external != null && clips.All(clip => clip.Id != external.Id))
|
||||
{
|
||||
clips.Add(external);
|
||||
}
|
||||
|
||||
var importedNode = new AnimationNode(imported.Id, "Imported 待机");
|
||||
var terminalClip = external != null && clips.Contains(external) ? external : manual;
|
||||
var terminalNode = new AnimationNode(
|
||||
terminalClip.Id,
|
||||
"共享 Manual 终点",
|
||||
endBehaviorOverride: FrameClipEndBehavior.Loop);
|
||||
var edge = new AnimationEdge(importedNode.InternalId, terminalNode.InternalId);
|
||||
var flow = new AnimationFlow(
|
||||
"WorkspaceSampleFlow",
|
||||
"工作台引用定位样例",
|
||||
importedNode.InternalId);
|
||||
graph.Configure(
|
||||
graph.Id,
|
||||
graph.DisplayName,
|
||||
clips,
|
||||
new[] { importedNode, terminalNode },
|
||||
new[] { edge },
|
||||
new[] { flow },
|
||||
flow.Id);
|
||||
}
|
||||
|
||||
private static JObject CreateFrame(string name, int x, int duration)
|
||||
{
|
||||
return new JObject
|
||||
{
|
||||
["filename"] = name,
|
||||
["frame"] = new JObject { ["x"] = x, ["y"] = 0, ["w"] = 2, ["h"] = 2 },
|
||||
["rotated"] = false,
|
||||
["trimmed"] = false,
|
||||
["spriteSourceSize"] = new JObject { ["x"] = 0, ["y"] = 0, ["w"] = 2, ["h"] = 2 },
|
||||
["sourceSize"] = new JObject { ["w"] = 2, ["h"] = 2 },
|
||||
["duration"] = duration
|
||||
};
|
||||
}
|
||||
|
||||
private static string CreateObjectJson(string imageName, string firstTag, string secondTag)
|
||||
{
|
||||
var root = CreateRoot(imageName, firstTag, secondTag);
|
||||
root["frames"] = new JObject
|
||||
{
|
||||
["中文帧_00"] = WithoutFilename(CreateFrame("中文帧_00", 0, 160)),
|
||||
["中文帧_01"] = WithoutFilename(CreateFrame("中文帧_01", 2, 90))
|
||||
};
|
||||
return root.ToString(Formatting.Indented);
|
||||
}
|
||||
|
||||
private static string CreateArrayJson(string imageName, string firstTag, string secondTag)
|
||||
{
|
||||
var root = CreateRoot(imageName, firstTag, secondTag);
|
||||
root["frames"] = new JArray(
|
||||
CreateFrame("自动帧_00", 0, 140),
|
||||
CreateFrame("自动帧_01", 2, 110));
|
||||
return root.ToString(Formatting.Indented);
|
||||
}
|
||||
|
||||
private static JObject CreateRoot(string imageName, string firstTag, string secondTag)
|
||||
{
|
||||
return new JObject
|
||||
{
|
||||
["meta"] = new JObject
|
||||
{
|
||||
["image"] = imageName,
|
||||
["size"] = new JObject { ["w"] = 4, ["h"] = 2 },
|
||||
["frameTags"] = new JArray(
|
||||
new JObject
|
||||
{
|
||||
["name"] = firstTag, ["from"] = 0, ["to"] = 0, ["direction"] = "forward"
|
||||
},
|
||||
new JObject
|
||||
{
|
||||
["name"] = secondTag, ["from"] = 0, ["to"] = 1, ["direction"] = "pingpong"
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static JObject WithoutFilename(JObject frame)
|
||||
{
|
||||
frame.Remove("filename");
|
||||
return frame;
|
||||
}
|
||||
|
||||
private static void WriteTexture(string path)
|
||||
{
|
||||
var texture = new Texture2D(4, 2, TextureFormat.RGBA32, false);
|
||||
texture.SetPixels(new[]
|
||||
{
|
||||
Color.cyan, Color.cyan, Color.magenta, Color.magenta,
|
||||
Color.cyan, Color.cyan, Color.magenta, Color.magenta
|
||||
});
|
||||
texture.Apply();
|
||||
File.WriteAllBytes(path, texture.EncodeToPNG());
|
||||
UnityEngine.Object.DestroyImmediate(texture);
|
||||
}
|
||||
|
||||
private static void WriteTextAsset(string path, string content)
|
||||
{
|
||||
File.WriteAllText(path, content, new UTF8Encoding(false));
|
||||
}
|
||||
|
||||
private static void EnsureFolder(string path)
|
||||
{
|
||||
var segments = path.Split('/');
|
||||
var current = segments[0];
|
||||
for (var index = 1; index < segments.Length; index++)
|
||||
{
|
||||
var next = current + "/" + segments[index];
|
||||
if (!AssetDatabase.IsValidFolder(next))
|
||||
{
|
||||
AssetDatabase.CreateFolder(current, segments[index]);
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 291df5abf0a29fd4ca1cb0b86eb24a9e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,612 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal static class FrameAnimationImportService
|
||||
{
|
||||
public static FrameAnimationImportPreview PreviewSource(
|
||||
FrameAnimationGraph graph,
|
||||
string sourceId)
|
||||
{
|
||||
var source = graph?.ImportSources.FirstOrDefault(item => item != null && item.InternalId == sourceId);
|
||||
return PreviewInternal(graph, source != null ? new[] { source } : Array.Empty<FrameAnimationImportSource>());
|
||||
}
|
||||
|
||||
public static FrameAnimationImportPreview PreviewAll(FrameAnimationGraph graph)
|
||||
{
|
||||
return PreviewInternal(
|
||||
graph,
|
||||
graph?.ImportSources.Where(source => source != null && source.IsEnabled).ToArray() ??
|
||||
Array.Empty<FrameAnimationImportSource>());
|
||||
}
|
||||
|
||||
public static bool RefreshSource(
|
||||
FrameAnimationGraph graph,
|
||||
string sourceId,
|
||||
bool allowSpriteChanges,
|
||||
out FrameAnimationImportPreview preview,
|
||||
out string error)
|
||||
{
|
||||
preview = PreviewSource(graph, sourceId);
|
||||
return Apply(preview, allowSpriteChanges, out error);
|
||||
}
|
||||
|
||||
public static bool RefreshAll(
|
||||
FrameAnimationGraph graph,
|
||||
bool allowSpriteChanges,
|
||||
out FrameAnimationImportPreview preview,
|
||||
out string error)
|
||||
{
|
||||
preview = PreviewAll(graph);
|
||||
return Apply(preview, allowSpriteChanges, out error);
|
||||
}
|
||||
|
||||
public static bool Apply(
|
||||
FrameAnimationImportPreview preview,
|
||||
bool allowSpriteChanges,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (preview == null || preview.Graph == null)
|
||||
{
|
||||
error = "导入预览或 Graph 为空。";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preview.HasErrors)
|
||||
{
|
||||
error = "导入预览包含阻断错误,未修改任何资产。";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preview.HasSpriteChanges && !allowSpriteChanges)
|
||||
{
|
||||
error = "导入需要修改 TextureImporter,但尚未获得确认。";
|
||||
return false;
|
||||
}
|
||||
|
||||
var graphPath = AssetDatabase.GetAssetPath(preview.Graph);
|
||||
if (string.IsNullOrEmpty(graphPath))
|
||||
{
|
||||
error = "FrameAnimationGraph 必须先保存为资产才能创建 Imported Clip sub-asset。";
|
||||
return false;
|
||||
}
|
||||
|
||||
var snapshots = new List<FrameAnimationTextureSnapshot>();
|
||||
Undo.IncrementCurrentGroup();
|
||||
var undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Refresh Frame Animation Imports");
|
||||
|
||||
try
|
||||
{
|
||||
Undo.RecordObject(preview.Graph, "Refresh Frame Animation Imports");
|
||||
foreach (var sourcePreview in preview.Sources.Where(item =>
|
||||
item.Source.ManageSpriteSlicing && item.HasSpriteChanges))
|
||||
{
|
||||
var snapshot = FrameAnimationSpriteUtility.CaptureSnapshot(sourcePreview.SpritePlan);
|
||||
snapshots.Add(snapshot);
|
||||
FrameAnimationSpriteUtility.ApplyPlan(sourcePreview.SpritePlan);
|
||||
}
|
||||
|
||||
foreach (var sourcePreview in preview.Sources)
|
||||
{
|
||||
ApplySource(preview.Graph, sourcePreview);
|
||||
sourcePreview.Source.SetLastSourceHash(
|
||||
FrameAnimationSpriteUtility.ComputeSourceHash(sourcePreview.Source));
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(preview.Graph);
|
||||
AssetDatabase.SaveAssets();
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
Undo.RevertAllDownToGroup(undoGroup);
|
||||
for (var index = snapshots.Count - 1; index >= 0; index--)
|
||||
{
|
||||
FrameAnimationSpriteUtility.RestoreSnapshot(snapshots[index]);
|
||||
}
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
catch (Exception rollbackException)
|
||||
{
|
||||
Debug.LogError($"Frame Animation 导入回滚失败:{rollbackException}");
|
||||
}
|
||||
|
||||
error = $"导入应用失败,已尝试回滚:{exception.Message}";
|
||||
Debug.LogException(exception);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static FrameAnimationImportPreview PreviewInternal(
|
||||
FrameAnimationGraph graph,
|
||||
IReadOnlyList<FrameAnimationImportSource> targets)
|
||||
{
|
||||
var preview = new FrameAnimationImportPreview(graph);
|
||||
if (graph == null)
|
||||
{
|
||||
return preview;
|
||||
}
|
||||
|
||||
var targetSet = new HashSet<FrameAnimationImportSource>(targets);
|
||||
var targetPreviews = new Dictionary<FrameAnimationImportSource, FrameAnimationImportSourcePreview>();
|
||||
foreach (var source in targets)
|
||||
{
|
||||
var sourcePreview = new FrameAnimationImportSourcePreview(source);
|
||||
targetPreviews[source] = sourcePreview;
|
||||
preview.AddSource(sourcePreview);
|
||||
}
|
||||
|
||||
ValidateSourceIds(graph, targetPreviews);
|
||||
var contextDocuments = ParseEnabledContext(graph, targetPreviews);
|
||||
ValidateCrossSourceTags(graph, contextDocuments, targetPreviews);
|
||||
ValidateTextureOwnership(graph, targetPreviews);
|
||||
|
||||
foreach (var pair in targetPreviews)
|
||||
{
|
||||
var source = pair.Key;
|
||||
var sourcePreview = pair.Value;
|
||||
if (!contextDocuments.TryGetValue(source, out var document))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sourcePreview.Document = document;
|
||||
ValidateSource(graph, source, document, sourcePreview);
|
||||
if (sourcePreview.HasErrors)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sourcePreview.SpritePlan = FrameAnimationSpriteUtility.BuildPlan(source, document, sourcePreview);
|
||||
sourcePreview.CurrentHash = FrameAnimationSpriteUtility.ComputeSourceHash(source);
|
||||
if (!string.IsNullOrEmpty(source.LastSourceHash) && source.LastSourceHash != sourcePreview.CurrentHash)
|
||||
{
|
||||
sourcePreview.AddIssue(new FrameAnimationImportIssue(
|
||||
FrameAnimationValidationSeverity.Warning,
|
||||
FrameAnimationImportIssueCode.SourceChanged,
|
||||
source.InternalId,
|
||||
source.InternalId,
|
||||
"来源内容或切图设置自上次刷新后已发生变化。"));
|
||||
}
|
||||
|
||||
if (!sourcePreview.HasErrors)
|
||||
{
|
||||
BuildClipDiffs(graph, source, document, sourcePreview);
|
||||
}
|
||||
}
|
||||
|
||||
return preview;
|
||||
}
|
||||
|
||||
private static Dictionary<FrameAnimationImportSource, AsepriteSourceDocument> ParseEnabledContext(
|
||||
FrameAnimationGraph graph,
|
||||
IReadOnlyDictionary<FrameAnimationImportSource, FrameAnimationImportSourcePreview> targetPreviews)
|
||||
{
|
||||
var result = new Dictionary<FrameAnimationImportSource, AsepriteSourceDocument>();
|
||||
foreach (var source in graph.ImportSources.Where(source => source != null && source.IsEnabled))
|
||||
{
|
||||
targetPreviews.TryGetValue(source, out var sourcePreview);
|
||||
if (source.AsepriteJson == null)
|
||||
{
|
||||
sourcePreview?.AddIssue(Error(source, FrameAnimationImportIssueCode.JsonMissing, "ImportSource 缺少 Aseprite JSON。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!AsepriteJsonParser.TryParse(source.AsepriteJson.text, source.InternalId, out var document, out var issue))
|
||||
{
|
||||
sourcePreview?.AddIssue(issue);
|
||||
continue;
|
||||
}
|
||||
result[source] = document;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ValidateSourceIds(
|
||||
FrameAnimationGraph graph,
|
||||
IReadOnlyDictionary<FrameAnimationImportSource, FrameAnimationImportSourcePreview> targetPreviews)
|
||||
{
|
||||
foreach (var source in graph.ImportSources.Where(source => source != null))
|
||||
{
|
||||
if (!FrameAnimationValueUtility.IsValidInternalId(source.InternalId) &&
|
||||
targetPreviews.TryGetValue(source, out var sourcePreview))
|
||||
{
|
||||
sourcePreview.AddIssue(Error(source, FrameAnimationImportIssueCode.ImportSourceIdInvalid,
|
||||
"ImportSource internalId 必须是 N 格式 GUID。"));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var duplicate in graph.ImportSources
|
||||
.Where(source => source != null)
|
||||
.GroupBy(source => source.InternalId)
|
||||
.Where(group => group.Count() > 1))
|
||||
{
|
||||
foreach (var source in duplicate)
|
||||
{
|
||||
if (targetPreviews.TryGetValue(source, out var sourcePreview))
|
||||
{
|
||||
sourcePreview.AddIssue(Error(source, FrameAnimationImportIssueCode.ImportSourceIdDuplicate,
|
||||
$"ImportSource internalId '{source.InternalId}' 重复。"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSource(
|
||||
FrameAnimationGraph graph,
|
||||
FrameAnimationImportSource source,
|
||||
AsepriteSourceDocument document,
|
||||
FrameAnimationImportSourcePreview preview)
|
||||
{
|
||||
if (source.Texture == null)
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.TextureMissing, "ImportSource 缺少 Texture。"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.Frames.Count == 0)
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.FramesMissing, "Aseprite JSON 的 frames 为空。"));
|
||||
}
|
||||
|
||||
foreach (var duplicate in document.Frames.GroupBy(frame => frame.FrameName).Where(group => group.Count() > 1))
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.FrameNameDuplicate,
|
||||
$"frameName '{duplicate.Key}' 在同一来源内重复。"));
|
||||
}
|
||||
|
||||
for (var index = 0; index < document.Frames.Count; index++)
|
||||
{
|
||||
var frame = document.Frames[index];
|
||||
if (string.IsNullOrWhiteSpace(frame.FrameName))
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.FrameNameInvalid,
|
||||
$"SourceFrame[{index}] 的 frameName 为空。"));
|
||||
}
|
||||
if (frame.DurationMs <= 0)
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.FrameDurationInvalid,
|
||||
$"SourceFrame '{frame.FrameName}' 的 duration 必须大于 0。"));
|
||||
}
|
||||
if (frame.Rect.width <= 0 || frame.Rect.height <= 0 || frame.Rect.x < 0 || frame.Rect.y < 0 ||
|
||||
frame.Rect.xMax > source.Texture.width || frame.Rect.yMax > source.Texture.height)
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.FrameRectInvalid,
|
||||
$"SourceFrame '{frame.FrameName}' 的 rect 越出 Texture 范围。"));
|
||||
}
|
||||
if (frame.Trimmed)
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.TrimmedUnsupported,
|
||||
$"SourceFrame '{frame.FrameName}' 使用了第一版不支持的 trimmed。"));
|
||||
}
|
||||
if (frame.Rotated)
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.RotatedUnsupported,
|
||||
$"SourceFrame '{frame.FrameName}' 使用了第一版不支持的 rotated。"));
|
||||
}
|
||||
}
|
||||
|
||||
if (document.TextureSize.x != source.Texture.width || document.TextureSize.y != source.Texture.height)
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.TextureSizeMismatch,
|
||||
$"meta.size {document.TextureSize.x}x{document.TextureSize.y} 与 Texture {source.Texture.width}x{source.Texture.height} 不一致。"));
|
||||
}
|
||||
|
||||
var textureFileName = Path.GetFileName(AssetDatabase.GetAssetPath(source.Texture));
|
||||
if (!string.IsNullOrEmpty(document.ImageName) &&
|
||||
!string.Equals(document.ImageName, textureFileName, StringComparison.Ordinal))
|
||||
{
|
||||
preview.AddIssue(Warning(source, FrameAnimationImportIssueCode.ImageNameMismatch,
|
||||
$"meta.image '{document.ImageName}' 与绑定 Texture '{textureFileName}' 不一致。"));
|
||||
}
|
||||
|
||||
if (document.Tags.Count == 0)
|
||||
{
|
||||
preview.AddIssue(Warning(source, FrameAnimationImportIssueCode.NoTags,
|
||||
"JSON 中没有 frameTags,此来源不会生成 Clip。"));
|
||||
}
|
||||
|
||||
foreach (var duplicate in document.Tags.GroupBy(tag => tag.Name).Where(group => group.Count() > 1))
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.TagNameDuplicate,
|
||||
$"Tag '{duplicate.Key}' 在同一来源内重复。"));
|
||||
}
|
||||
|
||||
foreach (var tag in document.Tags)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tag.Name))
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.TagNameInvalid, "Tag 名不能为空。"));
|
||||
continue;
|
||||
}
|
||||
if (!AsepriteJsonParser.TryExpandTag(tag, document.Frames.Count, source.InternalId, out _, out var issue))
|
||||
{
|
||||
preview.AddIssue(issue);
|
||||
}
|
||||
}
|
||||
|
||||
ValidateClipAssociations(graph, source, preview);
|
||||
}
|
||||
|
||||
private static void ValidateClipAssociations(
|
||||
FrameAnimationGraph graph,
|
||||
FrameAnimationImportSource source,
|
||||
FrameAnimationImportSourcePreview preview)
|
||||
{
|
||||
var graphPath = AssetDatabase.GetAssetPath(graph);
|
||||
foreach (var clip in graph.Clips.Where(clip => clip != null && clip.IsImported))
|
||||
{
|
||||
if (!graph.ImportSources.Any(item => item != null && item.InternalId == clip.ImportInfo.ImportSourceId))
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.ImportSourceReferenceInvalid,
|
||||
$"Imported Clip '{clip.Id}' 指向不存在的 ImportSource。"));
|
||||
}
|
||||
if (!AssetDatabase.IsSubAsset(clip) || AssetDatabase.GetAssetPath(clip) != graphPath)
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.ImportedClipOwnershipInvalid,
|
||||
$"Imported Clip '{clip.Id}' 必须是所属 Graph 的 sub-asset。"));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var duplicate in graph.Clips
|
||||
.Where(clip => clip != null && clip.ImportInfo?.ImportSourceId == source.InternalId)
|
||||
.GroupBy(clip => clip.ImportInfo.SourceTagName)
|
||||
.Where(group => group.Count() > 1))
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.ImportSourceReferenceInvalid,
|
||||
$"来源 Tag '{duplicate.Key}' 同时关联了多个 Imported Clip。"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateCrossSourceTags(
|
||||
FrameAnimationGraph graph,
|
||||
IReadOnlyDictionary<FrameAnimationImportSource, AsepriteSourceDocument> documents,
|
||||
IReadOnlyDictionary<FrameAnimationImportSource, FrameAnimationImportSourcePreview> targetPreviews)
|
||||
{
|
||||
var tags = documents.SelectMany(pair => pair.Value.Tags.Select(tag => (source: pair.Key, tag)));
|
||||
foreach (var duplicate in tags.Where(item => !string.IsNullOrWhiteSpace(item.tag.Name))
|
||||
.GroupBy(item => item.tag.Name).Where(group => group.Count() > 1))
|
||||
{
|
||||
foreach (var item in duplicate)
|
||||
{
|
||||
if (targetPreviews.TryGetValue(item.source, out var sourcePreview))
|
||||
{
|
||||
sourcePreview.AddIssue(Error(item.source, FrameAnimationImportIssueCode.TagConflict,
|
||||
$"Tag '{duplicate.Key}' 与其他启用 ImportSource 重名。"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var pair in targetPreviews)
|
||||
{
|
||||
if (!documents.TryGetValue(pair.Key, out var document))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (var tag in document.Tags)
|
||||
{
|
||||
var existing = graph.Clips.Count(clip => clip != null &&
|
||||
clip.ImportInfo?.ImportSourceId == pair.Key.InternalId &&
|
||||
clip.ImportInfo.SourceTagName == tag.Name);
|
||||
if (existing > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (graph.Clips.Any(clip => clip != null && clip.Id == tag.Name) ||
|
||||
graph.Flows.Any(flow => flow != null && flow.Id == tag.Name))
|
||||
{
|
||||
pair.Value.AddIssue(Error(pair.Key, FrameAnimationImportIssueCode.PlayableIdConflict,
|
||||
$"新 Tag '{tag.Name}' 与现有 Clip / Flow playable id 冲突。"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateTextureOwnership(
|
||||
FrameAnimationGraph currentGraph,
|
||||
IReadOnlyDictionary<FrameAnimationImportSource, FrameAnimationImportSourcePreview> targetPreviews)
|
||||
{
|
||||
var owners = new List<(FrameAnimationGraph graph, FrameAnimationImportSource source)>();
|
||||
foreach (var guid in AssetDatabase.FindAssets("t:FrameAnimationGraph"))
|
||||
{
|
||||
var graph = AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(AssetDatabase.GUIDToAssetPath(guid));
|
||||
if (graph == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
owners.AddRange(graph.ImportSources.Where(source => source != null && source.IsEnabled &&
|
||||
source.ManageSpriteSlicing && source.Texture != null).Select(source => (graph, source)));
|
||||
}
|
||||
if (!owners.Any(item => item.graph == currentGraph))
|
||||
{
|
||||
owners.AddRange(currentGraph.ImportSources.Where(source => source != null && source.IsEnabled &&
|
||||
source.ManageSpriteSlicing && source.Texture != null).Select(source => (currentGraph, source)));
|
||||
}
|
||||
|
||||
foreach (var pair in targetPreviews.Where(pair => pair.Key.ManageSpriteSlicing && pair.Key.Texture != null))
|
||||
{
|
||||
var conflicts = owners.Where(item => item.source.Texture == pair.Key.Texture &&
|
||||
!(item.graph == currentGraph && ReferenceEquals(item.source, pair.Key))).ToArray();
|
||||
if (conflicts.Length > 0)
|
||||
{
|
||||
pair.Value.AddIssue(Error(pair.Key, FrameAnimationImportIssueCode.TextureOwnershipConflict,
|
||||
$"Texture '{pair.Key.Texture.name}' 已被其他可写 ImportSource 占用:" +
|
||||
string.Join(", ", conflicts.Select(item => $"{item.graph.name}/{item.source.DisplayName}"))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildClipDiffs(
|
||||
FrameAnimationGraph graph,
|
||||
FrameAnimationImportSource source,
|
||||
AsepriteSourceDocument document,
|
||||
FrameAnimationImportSourcePreview preview)
|
||||
{
|
||||
var seenTags = new HashSet<string>();
|
||||
foreach (var tag in document.Tags)
|
||||
{
|
||||
if (!AsepriteJsonParser.TryExpandTag(tag, document.Frames.Count, source.InternalId, out var indices, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
seenTags.Add(tag.Name);
|
||||
var clip = graph.Clips.SingleOrDefault(item => item != null &&
|
||||
item.ImportInfo?.ImportSourceId == source.InternalId &&
|
||||
item.ImportInfo.SourceTagName == tag.Name);
|
||||
if (clip == null)
|
||||
{
|
||||
preview.AddClipDiff(new FrameAnimationClipDiff(
|
||||
FrameAnimationImportChangeKind.Added, tag.Name, null, indices, "创建 Imported Clip sub-asset"));
|
||||
continue;
|
||||
}
|
||||
|
||||
var changed = clip.ImportInfo.IsMissingFromSource ||
|
||||
!FramesEqual(clip, document, preview.SpritePlan, indices);
|
||||
preview.AddClipDiff(new FrameAnimationClipDiff(
|
||||
changed ? FrameAnimationImportChangeKind.Updated : FrameAnimationImportChangeKind.Unchanged,
|
||||
tag.Name,
|
||||
clip,
|
||||
indices,
|
||||
changed ? "原地更新导入帧" : "无变化"));
|
||||
}
|
||||
|
||||
foreach (var clip in graph.Clips.Where(item => item != null &&
|
||||
item.ImportInfo?.ImportSourceId == source.InternalId &&
|
||||
!seenTags.Contains(item.ImportInfo.SourceTagName)))
|
||||
{
|
||||
preview.AddClipDiff(new FrameAnimationClipDiff(
|
||||
clip.ImportInfo.IsMissingFromSource
|
||||
? FrameAnimationImportChangeKind.Unchanged
|
||||
: FrameAnimationImportChangeKind.Missing,
|
||||
clip.ImportInfo.SourceTagName,
|
||||
clip,
|
||||
Array.Empty<int>(),
|
||||
clip.ImportInfo.IsMissingFromSource ? "保持 Missing" : "标记 Missing,不删除 Clip"));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool FramesEqual(
|
||||
FrameClip clip,
|
||||
AsepriteSourceDocument document,
|
||||
FrameAnimationSpritePlan spritePlan,
|
||||
IReadOnlyList<int> indices)
|
||||
{
|
||||
if (clip.FrameCount != indices.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (var index = 0; index < indices.Count; index++)
|
||||
{
|
||||
var sourceIndex = indices[index];
|
||||
var sourceFrame = document.Frames[sourceIndex];
|
||||
var frame = clip.Frames[index];
|
||||
if (frame == null || frame.SourceIndex != sourceIndex ||
|
||||
frame.FrameName != sourceFrame.FrameName || frame.DurationMs != sourceFrame.DurationMs ||
|
||||
spritePlan == null || !spritePlan.MatchesSprite(sourceIndex, frame.Sprite))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ApplySource(
|
||||
FrameAnimationGraph graph,
|
||||
FrameAnimationImportSourcePreview preview)
|
||||
{
|
||||
var source = preview.Source;
|
||||
var writableSprites = source.ManageSpriteSlicing
|
||||
? FrameAnimationSpriteUtility.LoadSpritesByName(AssetDatabase.GetAssetPath(source.Texture))
|
||||
: null;
|
||||
|
||||
foreach (var diff in preview.ClipDiffs)
|
||||
{
|
||||
if (diff.Kind == FrameAnimationImportChangeKind.Missing)
|
||||
{
|
||||
Undo.RecordObject(diff.Clip, "Mark Imported Clip Missing");
|
||||
diff.Clip.SetImportMissingState(true);
|
||||
EditorUtility.SetDirty(diff.Clip);
|
||||
continue;
|
||||
}
|
||||
if (diff.Kind == FrameAnimationImportChangeKind.Unchanged && diff.Clip != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var frames = diff.SourceIndices.Select(sourceIndex =>
|
||||
{
|
||||
var sourceFrame = preview.Document.Frames[sourceIndex];
|
||||
if (!preview.SpritePlan.TryResolveSprite(sourceIndex, writableSprites, out var sprite))
|
||||
{
|
||||
var expectedName = preview.SpritePlan.SpriteNamesBySourceIndex.TryGetValue(
|
||||
sourceIndex, out var plannedName)
|
||||
? plannedName
|
||||
: sourceFrame.FrameName;
|
||||
throw new InvalidOperationException(
|
||||
$"刷新后找不到 SourceFrame '{sourceFrame.FrameName}' 对应的共享 Sprite '{expectedName}'。 ");
|
||||
}
|
||||
return new FrameAnimationFrame(sprite, sourceFrame.DurationMs, sourceFrame.FrameName, sourceIndex);
|
||||
}).ToArray();
|
||||
|
||||
if (diff.Clip != null)
|
||||
{
|
||||
Undo.RecordObject(diff.Clip, "Update Imported Frame Clip");
|
||||
diff.Clip.ReplaceImportedFrames(frames, source.InternalId, diff.SourceTagName);
|
||||
EditorUtility.SetDirty(diff.Clip);
|
||||
continue;
|
||||
}
|
||||
|
||||
var clip = ScriptableObject.CreateInstance<FrameClip>();
|
||||
clip.name = diff.SourceTagName;
|
||||
clip.Configure(
|
||||
diff.SourceTagName,
|
||||
diff.SourceTagName,
|
||||
frames,
|
||||
1f,
|
||||
source.DefaultNewClipEndBehavior,
|
||||
new FrameClipImportInfo(source.InternalId, diff.SourceTagName, false));
|
||||
AssetDatabase.AddObjectToAsset(clip, graph);
|
||||
Undo.RegisterCreatedObjectUndo(clip, "Create Imported Frame Clip");
|
||||
graph.AddImportedClip(clip);
|
||||
EditorUtility.SetDirty(clip);
|
||||
}
|
||||
}
|
||||
|
||||
private static FrameAnimationImportIssue Error(
|
||||
FrameAnimationImportSource source,
|
||||
FrameAnimationImportIssueCode code,
|
||||
string message)
|
||||
{
|
||||
return new FrameAnimationImportIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
code,
|
||||
source?.InternalId,
|
||||
source?.InternalId,
|
||||
message);
|
||||
}
|
||||
|
||||
private static FrameAnimationImportIssue Warning(
|
||||
FrameAnimationImportSource source,
|
||||
FrameAnimationImportIssueCode code,
|
||||
string message)
|
||||
{
|
||||
return new FrameAnimationImportIssue(
|
||||
FrameAnimationValidationSeverity.Warning,
|
||||
code,
|
||||
source?.InternalId,
|
||||
source?.InternalId,
|
||||
message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed2b95cf766a27d4cb93bbc8b2ba472e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,84 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
[CustomEditor(typeof(FrameAnimationPlayer))]
|
||||
public sealed class FrameAnimationPlayerEditor : UnityEditor.Editor
|
||||
{
|
||||
private SerializedProperty graphProperty;
|
||||
private SerializedProperty playOnEnableProperty;
|
||||
private SerializedProperty speedProperty;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
graphProperty = serializedObject.FindProperty("graph");
|
||||
playOnEnableProperty = serializedObject.FindProperty("playOnEnable");
|
||||
speedProperty = serializedObject.FindProperty("speed");
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
EditorGUILayout.PropertyField(graphProperty);
|
||||
EditorGUILayout.PropertyField(playOnEnableProperty);
|
||||
EditorGUILayout.PropertyField(speedProperty);
|
||||
|
||||
DrawConfigurationWarnings();
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
DrawRuntimeState();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawConfigurationWarnings()
|
||||
{
|
||||
var player = (FrameAnimationPlayer)target;
|
||||
var spriteRenderer = player.GetComponent<SpriteRenderer>();
|
||||
var image = player.GetComponent<Image>();
|
||||
|
||||
if (spriteRenderer == null && image == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"同一 GameObject 上必须存在一个 SpriteRenderer 或 Image。",
|
||||
MessageType.Error);
|
||||
}
|
||||
else if (spriteRenderer != null && image != null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"同一 GameObject 不能同时存在 SpriteRenderer 和 Image。",
|
||||
MessageType.Error);
|
||||
}
|
||||
|
||||
if (graphProperty.objectReferenceValue == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("尚未绑定 FrameAnimationGraph。", MessageType.Error);
|
||||
}
|
||||
|
||||
if (!FrameAnimationValueUtility.IsValidSpeed(speedProperty.floatValue))
|
||||
{
|
||||
EditorGUILayout.HelpBox("Speed 必须是有限且不小于 0 的数值。", MessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawRuntimeState()
|
||||
{
|
||||
var player = (FrameAnimationPlayer)target;
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("Runtime State", EditorStyles.boldLabel);
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUILayout.EnumPopup("State", player.State);
|
||||
EditorGUILayout.TextField("Playable", player.CurrentPlayableId);
|
||||
EditorGUILayout.TextField("Clip", player.CurrentClipId);
|
||||
EditorGUILayout.TextField("Node", player.CurrentNodeId);
|
||||
EditorGUILayout.IntField("Frame", player.CurrentFrameIndex);
|
||||
}
|
||||
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26b26e9170b32ec4693ae2aa8ab88838
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,593 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal enum FrameAnimationPreviewTargetKind { None, Clip, Node, Flow }
|
||||
internal enum FrameAnimationPreviewState { Stopped, Playing, Paused, Failed }
|
||||
internal enum NodePreviewPolicy { Static, SelectedOnly, AllVisible }
|
||||
internal enum FrameAnimationPreviewBackground { Checkerboard, Dark, Light }
|
||||
internal enum FrameAnimationPreviewZoomMode { Fit, One, Two, Three, Four, Eight, Manual }
|
||||
internal enum FrameAnimationFlowElementState { None, Upcoming, Current, Passed }
|
||||
|
||||
internal readonly struct FrameAnimationPreviewSegment
|
||||
{
|
||||
public int StepIndex { get; }
|
||||
public int FrameIndex { get; }
|
||||
public FrameClip Clip { get; }
|
||||
public string NodeId { get; }
|
||||
public double StartSeconds { get; }
|
||||
public double DurationSeconds { get; }
|
||||
public bool IsBlocked { get; }
|
||||
|
||||
public FrameAnimationPreviewSegment(
|
||||
int stepIndex,
|
||||
int frameIndex,
|
||||
ResolvedPlaybackStep step,
|
||||
double startSeconds,
|
||||
double durationSeconds,
|
||||
bool isBlocked)
|
||||
{
|
||||
StepIndex = stepIndex;
|
||||
FrameIndex = frameIndex;
|
||||
Clip = step.Clip;
|
||||
NodeId = step.NodeId;
|
||||
StartSeconds = startSeconds;
|
||||
DurationSeconds = durationSeconds;
|
||||
IsBlocked = isBlocked;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationPreviewTimeline
|
||||
{
|
||||
private const double Epsilon = 0.000000001d;
|
||||
private readonly List<FrameAnimationPreviewSegment> segments = new List<FrameAnimationPreviewSegment>();
|
||||
|
||||
public IReadOnlyList<FrameAnimationPreviewSegment> Segments => segments;
|
||||
public double DisplayDurationSeconds { get; }
|
||||
public bool IsInfinite { get; }
|
||||
public bool IsBlocked { get; }
|
||||
public int BlockedSegmentIndex { get; } = -1;
|
||||
|
||||
public FrameAnimationPreviewTimeline(ResolvedPlaybackPlan plan)
|
||||
{
|
||||
if (plan?.Steps == null || plan.Steps.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cursor = 0d;
|
||||
var blockedIndex = -1;
|
||||
for (var stepIndex = 0; stepIndex < plan.Steps.Count; stepIndex++)
|
||||
{
|
||||
var step = plan.Steps[stepIndex];
|
||||
if (step?.Clip == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (var frameIndex = 0; frameIndex < step.Clip.FrameCount; frameIndex++)
|
||||
{
|
||||
var frame = step.Clip.Frames[frameIndex];
|
||||
if (step.PlaybackSpeed == 0f)
|
||||
{
|
||||
blockedIndex = segments.Count;
|
||||
segments.Add(new FrameAnimationPreviewSegment(
|
||||
stepIndex, frameIndex, step, cursor, 0d, true));
|
||||
break;
|
||||
}
|
||||
|
||||
var duration = frame.DurationMs / 1000d / step.PlaybackSpeed;
|
||||
segments.Add(new FrameAnimationPreviewSegment(
|
||||
stepIndex, frameIndex, step, cursor, duration, false));
|
||||
cursor += duration;
|
||||
}
|
||||
if (blockedIndex >= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
DisplayDurationSeconds = cursor;
|
||||
BlockedSegmentIndex = blockedIndex;
|
||||
IsBlocked = blockedIndex >= 0;
|
||||
IsInfinite = IsBlocked || plan.Steps[plan.Steps.Count - 1].TerminalEndBehavior == FrameClipEndBehavior.Loop;
|
||||
}
|
||||
|
||||
public int FindSegmentIndex(double positionSeconds)
|
||||
{
|
||||
if (segments.Count == 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
var position = Math.Max(0d, positionSeconds);
|
||||
for (var index = 0; index < segments.Count; index++)
|
||||
{
|
||||
var segment = segments[index];
|
||||
if (segment.IsBlocked || position < segment.StartSeconds + segment.DurationSeconds - Epsilon)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return segments.Count - 1;
|
||||
}
|
||||
|
||||
public double GetSeekSeconds(int segmentIndex)
|
||||
{
|
||||
return segmentIndex >= 0 && segmentIndex < segments.Count
|
||||
? segments[segmentIndex].StartSeconds
|
||||
: 0d;
|
||||
}
|
||||
|
||||
public double GetPosition(FrameAnimationPlaybackSnapshot snapshot)
|
||||
{
|
||||
if (!snapshot.HasStarted || segments.Count == 0)
|
||||
{
|
||||
return 0d;
|
||||
}
|
||||
var segment = segments.FirstOrDefault(item =>
|
||||
item.StepIndex == snapshot.StepIndex && item.FrameIndex == snapshot.FrameIndex);
|
||||
if (segment.Clip == null)
|
||||
{
|
||||
return snapshot.IsCompleted ? DisplayDurationSeconds : 0d;
|
||||
}
|
||||
if (snapshot.IsCompleted && !IsInfinite)
|
||||
{
|
||||
return DisplayDurationSeconds;
|
||||
}
|
||||
var elapsed = snapshot.StepSpeed > 0f
|
||||
? snapshot.FrameElapsedSeconds / snapshot.StepSpeed
|
||||
: 0d;
|
||||
return Math.Min(DisplayDurationSeconds, segment.StartSeconds + elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct FrameAnimationPreviewDisplay
|
||||
{
|
||||
public Sprite Sprite { get; }
|
||||
public bool IsEmpty { get; }
|
||||
public bool IsActive { get; }
|
||||
public FrameAnimationFlowElementState FlowState { get; }
|
||||
|
||||
public FrameAnimationPreviewDisplay(
|
||||
Sprite sprite,
|
||||
bool isEmpty,
|
||||
bool isActive,
|
||||
FrameAnimationFlowElementState flowState)
|
||||
{
|
||||
Sprite = sprite;
|
||||
IsEmpty = isEmpty;
|
||||
IsActive = isActive;
|
||||
FlowState = flowState;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationPreviewCoordinator
|
||||
{
|
||||
private sealed class NodeRun
|
||||
{
|
||||
public AnimationNode Node;
|
||||
public FrameAnimationPlaybackSession Session;
|
||||
public Sprite Sprite;
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, NodeRun> automaticRuns = new Dictionary<string, NodeRun>();
|
||||
private FrameAnimationGraph graph;
|
||||
private object target;
|
||||
private ResolvedPlaybackPlan plan;
|
||||
private FrameAnimationPlaybackSession session;
|
||||
private Sprite currentSprite;
|
||||
private bool terminalTransparent;
|
||||
private bool suppressSelectedNode;
|
||||
private bool flowPresentationActive;
|
||||
|
||||
public event Action Changed;
|
||||
|
||||
public FrameAnimationPreviewTargetKind TargetKind { get; private set; }
|
||||
public FrameAnimationPreviewState State { get; private set; } = FrameAnimationPreviewState.Stopped;
|
||||
public NodePreviewPolicy NodePolicy { get; set; } = NodePreviewPolicy.SelectedOnly;
|
||||
public FrameAnimationPreviewBackground Background { get; set; } = FrameAnimationPreviewBackground.Checkerboard;
|
||||
public FrameAnimationPreviewZoomMode ZoomMode { get; set; } = FrameAnimationPreviewZoomMode.Fit;
|
||||
public float ManualZoom { get; set; } = 1f;
|
||||
public float PreviewSpeed { get; set; } = 1f;
|
||||
public FrameAnimationPreviewTimeline Timeline { get; private set; }
|
||||
public FrameAnimationPlaybackError Error { get; private set; } = FrameAnimationPlaybackError.None;
|
||||
public double PositionSeconds { get; private set; }
|
||||
public string TransitionFromNodeId { get; private set; } = string.Empty;
|
||||
public string TransitionToNodeId { get; private set; } = string.Empty;
|
||||
public object Target => target;
|
||||
public bool IsFlowPreview => TargetKind == FrameAnimationPreviewTargetKind.Flow &&
|
||||
flowPresentationActive && State != FrameAnimationPreviewState.Failed;
|
||||
public FrameAnimationPlaybackSnapshot Snapshot => session != null
|
||||
? session.Snapshot
|
||||
: default;
|
||||
public Sprite CurrentSprite => terminalTransparent ? null : currentSprite;
|
||||
|
||||
public void SetGraph(FrameAnimationGraph value)
|
||||
{
|
||||
if (ReferenceEquals(graph, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
graph = value;
|
||||
ClearTarget();
|
||||
automaticRuns.Clear();
|
||||
}
|
||||
|
||||
public void SetTarget(FrameAnimationPreviewTargetKind kind, object value)
|
||||
{
|
||||
if (TargetKind == kind && ReferenceEquals(target, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
TargetKind = kind;
|
||||
target = value;
|
||||
suppressSelectedNode = false;
|
||||
BuildTarget(false, 0d);
|
||||
if (kind == FrameAnimationPreviewTargetKind.Node && NodePolicy != NodePreviewPolicy.Static && session != null)
|
||||
{
|
||||
State = FrameAnimationPreviewState.Playing;
|
||||
}
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public void ClearTarget()
|
||||
{
|
||||
TargetKind = FrameAnimationPreviewTargetKind.None;
|
||||
target = null;
|
||||
plan = null;
|
||||
session = null;
|
||||
Timeline = null;
|
||||
currentSprite = null;
|
||||
terminalTransparent = false;
|
||||
Error = FrameAnimationPlaybackError.None;
|
||||
PositionSeconds = 0d;
|
||||
State = FrameAnimationPreviewState.Stopped;
|
||||
flowPresentationActive = false;
|
||||
TransitionFromNodeId = string.Empty;
|
||||
TransitionToNodeId = string.Empty;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public void RefreshTarget(bool topologyChanged)
|
||||
{
|
||||
if (TargetKind == FrameAnimationPreviewTargetKind.None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (topologyChanged && TargetKind == FrameAnimationPreviewTargetKind.Flow)
|
||||
{
|
||||
Stop();
|
||||
BuildTarget(false, 0d);
|
||||
return;
|
||||
}
|
||||
var wasPlaying = State == FrameAnimationPreviewState.Playing;
|
||||
var position = PositionSeconds;
|
||||
BuildTarget(position > 0d, position);
|
||||
if (wasPlaying && session != null)
|
||||
{
|
||||
State = FrameAnimationPreviewState.Playing;
|
||||
flowPresentationActive = TargetKind == FrameAnimationPreviewTargetKind.Flow;
|
||||
}
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public void SetNodePolicy(NodePreviewPolicy value)
|
||||
{
|
||||
NodePolicy = value;
|
||||
automaticRuns.Clear();
|
||||
suppressSelectedNode = false;
|
||||
if (TargetKind == FrameAnimationPreviewTargetKind.Node)
|
||||
{
|
||||
BuildTarget(false, 0d);
|
||||
if (session != null && value != NodePreviewPolicy.Static)
|
||||
{
|
||||
State = FrameAnimationPreviewState.Playing;
|
||||
}
|
||||
}
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public void Play()
|
||||
{
|
||||
if (session == null)
|
||||
{
|
||||
BuildTarget(false, 0d);
|
||||
}
|
||||
if (session == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (session.Snapshot.IsCompleted)
|
||||
{
|
||||
session.Restart(ApplySprite);
|
||||
PositionSeconds = 0d;
|
||||
}
|
||||
terminalTransparent = false;
|
||||
suppressSelectedNode = false;
|
||||
flowPresentationActive = TargetKind == FrameAnimationPreviewTargetKind.Flow;
|
||||
State = FrameAnimationPreviewState.Playing;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
if (session == null || State != FrameAnimationPreviewState.Playing) return;
|
||||
State = FrameAnimationPreviewState.Paused;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (session != null)
|
||||
{
|
||||
session.Restart(ApplySprite);
|
||||
}
|
||||
State = FrameAnimationPreviewState.Stopped;
|
||||
PositionSeconds = 0d;
|
||||
terminalTransparent = false;
|
||||
suppressSelectedNode = TargetKind == FrameAnimationPreviewTargetKind.Node;
|
||||
flowPresentationActive = false;
|
||||
TransitionFromNodeId = string.Empty;
|
||||
TransitionToNodeId = string.Empty;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public void Restart()
|
||||
{
|
||||
if (session == null)
|
||||
{
|
||||
BuildTarget(false, 0d);
|
||||
}
|
||||
session?.Restart(ApplySprite);
|
||||
PositionSeconds = 0d;
|
||||
terminalTransparent = false;
|
||||
suppressSelectedNode = false;
|
||||
flowPresentationActive = TargetKind == FrameAnimationPreviewTargetKind.Flow;
|
||||
if (session != null) State = FrameAnimationPreviewState.Playing;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public void Step(int direction)
|
||||
{
|
||||
if (session == null || Timeline == null || Timeline.Segments.Count == 0) return;
|
||||
var current = Timeline.FindSegmentIndex(PositionSeconds);
|
||||
var next = Mathf.Clamp(current + Math.Sign(direction), 0, Timeline.Segments.Count - 1);
|
||||
Seek(Timeline.GetSeekSeconds(next));
|
||||
}
|
||||
|
||||
public void Seek(double seconds)
|
||||
{
|
||||
if (session == null || Timeline == null) return;
|
||||
var maximum = Timeline.DisplayDurationSeconds;
|
||||
var targetSeconds = Math.Max(0d, Math.Min(maximum, seconds));
|
||||
if (Timeline.IsInfinite && maximum > 0d && targetSeconds >= maximum)
|
||||
{
|
||||
targetSeconds = Math.Max(0d, maximum - 0.000001d);
|
||||
}
|
||||
terminalTransparent = false;
|
||||
flowPresentationActive = TargetKind == FrameAnimationPreviewTargetKind.Flow;
|
||||
var result = session.Seek(targetSeconds, ApplySprite);
|
||||
ApplyEvaluation(result);
|
||||
PositionSeconds = targetSeconds;
|
||||
if (State != FrameAnimationPreviewState.Failed)
|
||||
{
|
||||
State = FrameAnimationPreviewState.Paused;
|
||||
}
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public bool Tick(double deltaSeconds, IReadOnlyCollection<AnimationNode> visibleNodes)
|
||||
{
|
||||
var changed = false;
|
||||
TransitionFromNodeId = string.Empty;
|
||||
TransitionToNodeId = string.Empty;
|
||||
if (State == FrameAnimationPreviewState.Playing && session != null)
|
||||
{
|
||||
var before = session.Snapshot;
|
||||
var result = session.Evaluate(deltaSeconds, PreviewSpeed, ApplySprite);
|
||||
var after = session.Snapshot;
|
||||
if (before.StepIndex != after.StepIndex)
|
||||
{
|
||||
TransitionFromNodeId = before.NodeId;
|
||||
TransitionToNodeId = after.NodeId;
|
||||
}
|
||||
ApplyEvaluation(result);
|
||||
PositionSeconds = Timeline?.GetPosition(after) ?? 0d;
|
||||
if (result.IsCompleted && TargetKind == FrameAnimationPreviewTargetKind.Node &&
|
||||
NodePolicy != NodePreviewPolicy.Static && !suppressSelectedNode && session != null)
|
||||
{
|
||||
session.Restart(ApplySprite);
|
||||
State = FrameAnimationPreviewState.Playing;
|
||||
PositionSeconds = 0d;
|
||||
terminalTransparent = false;
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!IsFlowPreview)
|
||||
{
|
||||
changed |= TickAutomaticNodes(deltaSeconds, visibleNodes);
|
||||
}
|
||||
else if (automaticRuns.Count > 0)
|
||||
{
|
||||
automaticRuns.Clear();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) Changed?.Invoke();
|
||||
return changed;
|
||||
}
|
||||
|
||||
public FrameAnimationPreviewDisplay GetNodeDisplay(AnimationNode node)
|
||||
{
|
||||
if (node == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
if (IsFlowPreview && plan != null)
|
||||
{
|
||||
var current = session?.Snapshot.StepIndex ?? 0;
|
||||
var stepIndex = IndexOfNode(plan, node.InternalId);
|
||||
var flowState = stepIndex < 0 ? FrameAnimationFlowElementState.None :
|
||||
stepIndex < current ? FrameAnimationFlowElementState.Passed :
|
||||
stepIndex == current ? FrameAnimationFlowElementState.Current : FrameAnimationFlowElementState.Upcoming;
|
||||
if (flowState == FrameAnimationFlowElementState.Current)
|
||||
{
|
||||
return new FrameAnimationPreviewDisplay(CurrentSprite, CurrentSprite == null, true, flowState);
|
||||
}
|
||||
return Representative(node, flowState);
|
||||
}
|
||||
if (TargetKind == FrameAnimationPreviewTargetKind.Node && ReferenceEquals(target, node) && session != null)
|
||||
{
|
||||
if (NodePolicy == NodePreviewPolicy.Static && State == FrameAnimationPreviewState.Stopped &&
|
||||
!suppressSelectedNode && !session.Snapshot.IsCompleted)
|
||||
{
|
||||
return Representative(node, FrameAnimationFlowElementState.None);
|
||||
}
|
||||
return new FrameAnimationPreviewDisplay(CurrentSprite, CurrentSprite == null, State == FrameAnimationPreviewState.Playing,
|
||||
FrameAnimationFlowElementState.None);
|
||||
}
|
||||
if (automaticRuns.TryGetValue(node.InternalId, out var run))
|
||||
{
|
||||
return new FrameAnimationPreviewDisplay(run.Sprite, run.Sprite == null, true, FrameAnimationFlowElementState.None);
|
||||
}
|
||||
return Representative(node, FrameAnimationFlowElementState.None);
|
||||
}
|
||||
|
||||
public FrameAnimationFlowElementState GetEdgeState(AnimationEdge edge)
|
||||
{
|
||||
if (!IsFlowPreview || plan == null || edge == null) return FrameAnimationFlowElementState.None;
|
||||
if (edge.FromNodeId == TransitionFromNodeId && edge.ToNodeId == TransitionToNodeId)
|
||||
{
|
||||
return FrameAnimationFlowElementState.Current;
|
||||
}
|
||||
var from = IndexOfNode(plan, edge.FromNodeId);
|
||||
var to = IndexOfNode(plan, edge.ToNodeId);
|
||||
if (from < 0 || to != from + 1) return FrameAnimationFlowElementState.None;
|
||||
var current = session?.Snapshot.StepIndex ?? 0;
|
||||
return to <= current ? FrameAnimationFlowElementState.Passed : FrameAnimationFlowElementState.Upcoming;
|
||||
}
|
||||
|
||||
private bool TickAutomaticNodes(double deltaSeconds, IReadOnlyCollection<AnimationNode> visibleNodes)
|
||||
{
|
||||
IEnumerable<AnimationNode> desired = Array.Empty<AnimationNode>();
|
||||
if (NodePolicy == NodePreviewPolicy.SelectedOnly && TargetKind == FrameAnimationPreviewTargetKind.Node &&
|
||||
target is AnimationNode selected && !suppressSelectedNode)
|
||||
{
|
||||
desired = new[] { selected };
|
||||
}
|
||||
else if (NodePolicy == NodePreviewPolicy.AllVisible)
|
||||
{
|
||||
desired = visibleNodes ?? Array.Empty<AnimationNode>();
|
||||
}
|
||||
var desiredNodes = desired.Where(node => node != null &&
|
||||
!(TargetKind == FrameAnimationPreviewTargetKind.Node && ReferenceEquals(target, node)))
|
||||
.GroupBy(node => node.InternalId).Select(group => group.First()).ToArray();
|
||||
var desiredIds = new HashSet<string>(desiredNodes.Select(node => node.InternalId));
|
||||
foreach (var id in automaticRuns.Keys.Where(id => !desiredIds.Contains(id)).ToArray())
|
||||
{
|
||||
automaticRuns.Remove(id);
|
||||
}
|
||||
foreach (var node in desiredNodes)
|
||||
{
|
||||
if (!automaticRuns.TryGetValue(node.InternalId, out var run))
|
||||
{
|
||||
if (!FrameAnimationResolver.TryResolveNode(graph, node, out var nodePlan, out _)) continue;
|
||||
run = new NodeRun { Node = node, Session = new FrameAnimationPlaybackSession(nodePlan) };
|
||||
run.Session.Start(sprite => run.Sprite = sprite);
|
||||
automaticRuns[node.InternalId] = run;
|
||||
}
|
||||
var evaluation = run.Session.Evaluate(deltaSeconds, PreviewSpeed, sprite => run.Sprite = sprite);
|
||||
if (evaluation.IsCompleted)
|
||||
{
|
||||
run.Session.Restart(sprite => run.Sprite = sprite);
|
||||
}
|
||||
else if (evaluation.IsFailed)
|
||||
{
|
||||
automaticRuns.Remove(node.InternalId);
|
||||
}
|
||||
}
|
||||
return desiredNodes.Length > 0 || automaticRuns.Count > 0;
|
||||
}
|
||||
|
||||
private void BuildTarget(bool seek, double position)
|
||||
{
|
||||
plan = null;
|
||||
session = null;
|
||||
Timeline = null;
|
||||
currentSprite = null;
|
||||
terminalTransparent = false;
|
||||
PositionSeconds = 0d;
|
||||
Error = FrameAnimationPlaybackError.None;
|
||||
State = FrameAnimationPreviewState.Stopped;
|
||||
flowPresentationActive = false;
|
||||
var resolved = TargetKind switch
|
||||
{
|
||||
FrameAnimationPreviewTargetKind.Clip when target is FrameClip clip =>
|
||||
FrameAnimationResolver.TryResolve(graph, clip.Id, default, out plan, out var clipError)
|
||||
? FrameAnimationPlaybackError.None : clipError,
|
||||
FrameAnimationPreviewTargetKind.Node when target is AnimationNode node =>
|
||||
FrameAnimationResolver.TryResolveNode(graph, node, out plan, out var nodeError)
|
||||
? FrameAnimationPlaybackError.None : nodeError,
|
||||
FrameAnimationPreviewTargetKind.Flow when target is AnimationFlow flow =>
|
||||
FrameAnimationResolver.TryResolve(graph, flow.Id, default, out plan, out var flowError)
|
||||
? FrameAnimationPlaybackError.None : flowError,
|
||||
_ => new FrameAnimationPlaybackError(FrameAnimationPlaybackErrorCode.PlayableNotFound, "没有可预览目标。")
|
||||
};
|
||||
if (resolved.Code != FrameAnimationPlaybackErrorCode.None || plan == null)
|
||||
{
|
||||
Error = resolved;
|
||||
State = TargetKind == FrameAnimationPreviewTargetKind.None
|
||||
? FrameAnimationPreviewState.Stopped : FrameAnimationPreviewState.Failed;
|
||||
return;
|
||||
}
|
||||
Timeline = new FrameAnimationPreviewTimeline(plan);
|
||||
session = new FrameAnimationPlaybackSession(plan);
|
||||
session.Start(ApplySprite);
|
||||
if (seek && position > 0d)
|
||||
{
|
||||
Seek(position);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyEvaluation(FrameAnimationSessionEvaluation evaluation)
|
||||
{
|
||||
if (evaluation.IsFailed)
|
||||
{
|
||||
Error = evaluation.Error;
|
||||
State = FrameAnimationPreviewState.Failed;
|
||||
flowPresentationActive = false;
|
||||
return;
|
||||
}
|
||||
if (!evaluation.IsCompleted) return;
|
||||
State = FrameAnimationPreviewState.Stopped;
|
||||
terminalTransparent = evaluation.EndBehavior == FrameClipEndBehavior.Clear ||
|
||||
evaluation.EndBehavior == FrameClipEndBehavior.HideTarget;
|
||||
}
|
||||
|
||||
private void ApplySprite(Sprite sprite)
|
||||
{
|
||||
currentSprite = sprite;
|
||||
terminalTransparent = false;
|
||||
}
|
||||
|
||||
private FrameAnimationPreviewDisplay Representative(AnimationNode node, FrameAnimationFlowElementState flowState)
|
||||
{
|
||||
var clip = graph?.Clips.FirstOrDefault(item => item != null && item.Id == node.ClipId);
|
||||
var sprite = clip?.Frames.FirstOrDefault(frame => frame?.Sprite != null)?.Sprite;
|
||||
return new FrameAnimationPreviewDisplay(sprite, sprite == null, false, flowState);
|
||||
}
|
||||
|
||||
private static int IndexOfNode(ResolvedPlaybackPlan targetPlan, string nodeId)
|
||||
{
|
||||
if (targetPlan?.Steps == null) return -1;
|
||||
for (var index = 0; index < targetPlan.Steps.Count; index++)
|
||||
{
|
||||
if (targetPlan.Steps[index].NodeId == nodeId) return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14a91ced504e4af4ad79f4f760794c3c
|
||||
@@ -0,0 +1,163 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal sealed class FrameAnimationPreviewElement : VisualElement
|
||||
{
|
||||
private static Texture2D checkerTexture;
|
||||
private readonly Image image;
|
||||
private readonly Label emptyLabel;
|
||||
private Sprite sprite;
|
||||
private FrameAnimationPreviewZoomMode zoomMode = FrameAnimationPreviewZoomMode.Fit;
|
||||
private float manualZoom = 1f;
|
||||
private Vector2 pan;
|
||||
private Vector2 dragStart;
|
||||
private Vector2 panStart;
|
||||
private bool dragging;
|
||||
|
||||
public FrameAnimationPreviewElement()
|
||||
{
|
||||
AddToClassList("fa-preview");
|
||||
focusable = true;
|
||||
image = new Image { scaleMode = ScaleMode.ScaleToFit, pickingMode = PickingMode.Ignore };
|
||||
image.AddToClassList("fa-preview__image");
|
||||
emptyLabel = new Label("Empty Frame") { pickingMode = PickingMode.Ignore };
|
||||
emptyLabel.AddToClassList("fa-preview__empty");
|
||||
Add(image);
|
||||
Add(emptyLabel);
|
||||
RegisterCallback<GeometryChangedEvent>(_ => UpdateImageLayout());
|
||||
RegisterCallback<PointerDownEvent>(OnPointerDown);
|
||||
RegisterCallback<PointerMoveEvent>(OnPointerMove);
|
||||
RegisterCallback<PointerUpEvent>(OnPointerUp);
|
||||
SetBackground(FrameAnimationPreviewBackground.Checkerboard);
|
||||
}
|
||||
|
||||
public void SetDisplay(Sprite value, bool isEmpty, string emptyText = "Empty Frame")
|
||||
{
|
||||
sprite = value;
|
||||
image.sprite = value;
|
||||
image.style.display = value != null ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
emptyLabel.text = string.IsNullOrEmpty(emptyText) ? "Empty Frame" : emptyText;
|
||||
emptyLabel.style.display = value == null || isEmpty ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
UpdateImageLayout();
|
||||
}
|
||||
|
||||
public void SetBackground(FrameAnimationPreviewBackground background)
|
||||
{
|
||||
switch (background)
|
||||
{
|
||||
case FrameAnimationPreviewBackground.Dark:
|
||||
style.backgroundImage = StyleKeyword.None;
|
||||
style.backgroundColor = new StyleColor(new Color(0.08f, 0.08f, 0.08f));
|
||||
break;
|
||||
case FrameAnimationPreviewBackground.Light:
|
||||
style.backgroundImage = StyleKeyword.None;
|
||||
style.backgroundColor = new StyleColor(new Color(0.72f, 0.72f, 0.72f));
|
||||
break;
|
||||
default:
|
||||
style.backgroundImage = new StyleBackground(GetCheckerTexture());
|
||||
style.backgroundColor = StyleKeyword.Null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetZoom(FrameAnimationPreviewZoomMode mode, float value)
|
||||
{
|
||||
zoomMode = mode;
|
||||
manualZoom = Mathf.Clamp(value, 0.25f, 8f);
|
||||
if (mode == FrameAnimationPreviewZoomMode.Fit)
|
||||
{
|
||||
pan = Vector2.zero;
|
||||
}
|
||||
UpdateImageLayout();
|
||||
}
|
||||
|
||||
private void UpdateImageLayout()
|
||||
{
|
||||
if (sprite == null || resolvedStyle.width <= 0f || resolvedStyle.height <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var sourceSize = sprite.rect.size;
|
||||
if (sourceSize.x <= 0f || sourceSize.y <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var scale = zoomMode == FrameAnimationPreviewZoomMode.Fit
|
||||
? Mathf.Min(resolvedStyle.width / sourceSize.x, resolvedStyle.height / sourceSize.y)
|
||||
: GetScale();
|
||||
var size = sourceSize * Mathf.Max(0.0001f, scale);
|
||||
image.style.width = size.x;
|
||||
image.style.height = size.y;
|
||||
image.style.left = (resolvedStyle.width - size.x) * 0.5f + pan.x;
|
||||
image.style.top = (resolvedStyle.height - size.y) * 0.5f + pan.y;
|
||||
}
|
||||
|
||||
private float GetScale()
|
||||
{
|
||||
return zoomMode switch
|
||||
{
|
||||
FrameAnimationPreviewZoomMode.One => 1f,
|
||||
FrameAnimationPreviewZoomMode.Two => 2f,
|
||||
FrameAnimationPreviewZoomMode.Three => 3f,
|
||||
FrameAnimationPreviewZoomMode.Four => 4f,
|
||||
FrameAnimationPreviewZoomMode.Eight => 8f,
|
||||
FrameAnimationPreviewZoomMode.Manual => manualZoom,
|
||||
_ => 1f
|
||||
};
|
||||
}
|
||||
|
||||
private void OnPointerDown(PointerDownEvent evt)
|
||||
{
|
||||
if (zoomMode == FrameAnimationPreviewZoomMode.Fit || evt.button != 0) return;
|
||||
dragging = true;
|
||||
dragStart = new Vector2(evt.position.x, evt.position.y);
|
||||
panStart = pan;
|
||||
this.CapturePointer(evt.pointerId);
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
private void OnPointerMove(PointerMoveEvent evt)
|
||||
{
|
||||
if (!dragging || !this.HasPointerCapture(evt.pointerId)) return;
|
||||
var position = new Vector2(evt.position.x, evt.position.y);
|
||||
pan = panStart + position - dragStart;
|
||||
UpdateImageLayout();
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
private void OnPointerUp(PointerUpEvent evt)
|
||||
{
|
||||
if (!dragging || !this.HasPointerCapture(evt.pointerId)) return;
|
||||
dragging = false;
|
||||
this.ReleasePointer(evt.pointerId);
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
private static Texture2D GetCheckerTexture()
|
||||
{
|
||||
if (checkerTexture != null) return checkerTexture;
|
||||
const int textureSize = 128;
|
||||
const int cellSize = 8;
|
||||
checkerTexture = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false)
|
||||
{
|
||||
name = "Frame Animation Preview Checker",
|
||||
hideFlags = HideFlags.HideAndDontSave,
|
||||
wrapMode = TextureWrapMode.Repeat,
|
||||
filterMode = FilterMode.Point
|
||||
};
|
||||
var light = new Color32(106, 106, 106, 255);
|
||||
var dark = new Color32(78, 78, 78, 255);
|
||||
var pixels = new Color32[textureSize * textureSize];
|
||||
for (var y = 0; y < textureSize; y++)
|
||||
for (var x = 0; x < textureSize; x++)
|
||||
{
|
||||
pixels[y * textureSize + x] = ((x / cellSize) + (y / cellSize)) % 2 == 0 ? light : dark;
|
||||
}
|
||||
checkerTexture.SetPixels32(pixels);
|
||||
checkerTexture.Apply(false, true);
|
||||
return checkerTexture;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54aef216ceea42ed8917b581606ecf3d
|
||||
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
public static class FrameAnimationPreviewSampleBuilder
|
||||
{
|
||||
private const string Folder = "Assets/GameContent/Test/FrameAnimation/Preview";
|
||||
private const string GraphPath = Folder + "/PreviewSampleGraph.asset";
|
||||
private const string TexturePath = "Assets/GameContent/Test/AnimatorRaw/手接树叶特写.png";
|
||||
|
||||
[MenuItem("Tools/Frame Animation/Create Phase 5 Preview Sample")]
|
||||
public static void BuildFromCommandLine()
|
||||
{
|
||||
if (AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(GraphPath) != null)
|
||||
{
|
||||
Debug.Log($"Frame Animation preview sample already exists; preserved without overwrite: {GraphPath}");
|
||||
return;
|
||||
}
|
||||
EnsureFolder(Folder);
|
||||
var sprites = AssetDatabase.LoadAllAssetsAtPath(TexturePath).OfType<Sprite>()
|
||||
.OrderBy(sprite => sprite.name, StringComparer.Ordinal).ToArray();
|
||||
if (sprites.Length < 6)
|
||||
{
|
||||
throw new InvalidOperationException("第五阶段预览样例需要至少 6 个已切分 Sprite。");
|
||||
}
|
||||
|
||||
var graph = ScriptableObject.CreateInstance<FrameAnimationGraph>();
|
||||
graph.name = "PreviewSampleGraph";
|
||||
AssetDatabase.CreateAsset(graph, GraphPath);
|
||||
var action = CreateClip(graph, "PreviewAction", "Preview Action", FrameClipEndBehavior.HoldLastFrame,
|
||||
new[]
|
||||
{
|
||||
new FrameAnimationFrame(sprites[0], 100, sprites[0].name, 0),
|
||||
new FrameAnimationFrame(sprites[1], 160, sprites[1].name, 1),
|
||||
new FrameAnimationFrame(sprites[2], 220, sprites[2].name, 2)
|
||||
});
|
||||
var withEmpty = CreateClip(graph, "PreviewEmpty", "Preview Empty Frame", FrameClipEndBehavior.HoldLastFrame,
|
||||
new[]
|
||||
{
|
||||
new FrameAnimationFrame(sprites[3], 120, sprites[3].name, 3),
|
||||
new FrameAnimationFrame(null, 300, "empty", -1),
|
||||
new FrameAnimationFrame(sprites[4], 120, sprites[4].name, 4)
|
||||
});
|
||||
var idle = CreateClip(graph, "PreviewIdle", "Preview Idle Loop", FrameClipEndBehavior.Loop,
|
||||
new[]
|
||||
{
|
||||
new FrameAnimationFrame(sprites[4], 140, sprites[4].name, 4),
|
||||
new FrameAnimationFrame(sprites[5], 140, sprites[5].name, 5)
|
||||
});
|
||||
|
||||
var finiteA = new AnimationNode(action.Id, "Finite Action", speedOverride: 2f,
|
||||
internalId: "51000000000000000000000000000001");
|
||||
var finiteB = new AnimationNode(withEmpty.Id, "Finite Empty",
|
||||
endBehaviorOverride: FrameClipEndBehavior.HoldLastFrame,
|
||||
internalId: "51000000000000000000000000000002");
|
||||
var loopA = new AnimationNode(action.Id, "Loop Intro", internalId: "51000000000000000000000000000003");
|
||||
var loopB = new AnimationNode(idle.Id, "Loop Terminal",
|
||||
endBehaviorOverride: FrameClipEndBehavior.Loop,
|
||||
internalId: "51000000000000000000000000000004");
|
||||
var blocked = new AnimationNode(withEmpty.Id, "Zero Speed Block", speedOverride: 0f,
|
||||
internalId: "51000000000000000000000000000005");
|
||||
var clear = new AnimationNode(action.Id, "Clear Terminal",
|
||||
endBehaviorOverride: FrameClipEndBehavior.Clear,
|
||||
internalId: "51000000000000000000000000000006");
|
||||
var hidden = new AnimationNode(idle.Id, "Hide Terminal",
|
||||
endBehaviorOverride: FrameClipEndBehavior.HideTarget,
|
||||
internalId: "51000000000000000000000000000007");
|
||||
var finiteFlow = new AnimationFlow("PreviewFiniteFlow", "Finite Flow", finiteA.InternalId);
|
||||
var loopFlow = new AnimationFlow("PreviewLoopFlow", "Loop Flow", loopA.InternalId);
|
||||
var blockedFlow = new AnimationFlow("PreviewBlockedFlow", "Zero Speed Flow", blocked.InternalId);
|
||||
var clearFlow = new AnimationFlow("PreviewClearFlow", "Clear Flow", clear.InternalId);
|
||||
var hideFlow = new AnimationFlow("PreviewHideFlow", "Hide Flow", hidden.InternalId);
|
||||
graph.Configure(
|
||||
"PreviewSampleGraph",
|
||||
"Phase 5 Preview Sample",
|
||||
new[] { action, withEmpty, idle },
|
||||
new[] { finiteA, finiteB, loopA, loopB, blocked, clear, hidden },
|
||||
new[]
|
||||
{
|
||||
new AnimationEdge(finiteA.InternalId, finiteB.InternalId, "52000000000000000000000000000001"),
|
||||
new AnimationEdge(loopA.InternalId, loopB.InternalId, "52000000000000000000000000000002")
|
||||
},
|
||||
new[] { finiteFlow, loopFlow, blockedFlow, clearFlow, hideFlow },
|
||||
finiteFlow.Id);
|
||||
|
||||
var positions = new[]
|
||||
{
|
||||
(finiteA, new Vector2(80f, 80f)), (finiteB, new Vector2(420f, 80f)),
|
||||
(loopA, new Vector2(80f, 320f)), (loopB, new Vector2(420f, 320f)),
|
||||
(blocked, new Vector2(80f, 560f)), (clear, new Vector2(420f, 560f)),
|
||||
(hidden, new Vector2(760f, 560f))
|
||||
};
|
||||
foreach (var item in positions)
|
||||
{
|
||||
graph.EditorData.GetOrCreateNodeData(item.Item1.InternalId, item.Item2);
|
||||
}
|
||||
var colors = new[]
|
||||
{
|
||||
new Color(0.25f, 0.65f, 0.95f), new Color(0.55f, 0.4f, 0.9f),
|
||||
new Color(0.9f, 0.55f, 0.2f), new Color(0.3f, 0.75f, 0.5f), new Color(0.85f, 0.35f, 0.45f)
|
||||
};
|
||||
var flows = graph.Flows.ToArray();
|
||||
for (var index = 0; index < flows.Length; index++)
|
||||
{
|
||||
graph.EditorData.GetOrCreateFlowData(flows[index].Id, colors[index]);
|
||||
}
|
||||
EditorUtility.SetDirty(graph);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
Debug.Log($"Frame Animation phase 5 preview sample created: {GraphPath}");
|
||||
}
|
||||
|
||||
private static FrameClip CreateClip(
|
||||
FrameAnimationGraph graph,
|
||||
string id,
|
||||
string displayName,
|
||||
FrameClipEndBehavior endBehavior,
|
||||
FrameAnimationFrame[] frames)
|
||||
{
|
||||
var clip = ScriptableObject.CreateInstance<FrameClip>();
|
||||
clip.name = id;
|
||||
clip.Configure(id, displayName, frames, 1f, endBehavior);
|
||||
AssetDatabase.AddObjectToAsset(clip, graph);
|
||||
return clip;
|
||||
}
|
||||
|
||||
private static void EnsureFolder(string path)
|
||||
{
|
||||
var segments = path.Split('/');
|
||||
var current = segments[0];
|
||||
for (var index = 1; index < segments.Length; index++)
|
||||
{
|
||||
var next = current + "/" + segments[index];
|
||||
if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(current, segments[index]);
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f280e66f096a410eaf3eae8cd9c01134
|
||||
@@ -0,0 +1,223 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
public static class FrameAnimationRuntimeSampleBuilder
|
||||
{
|
||||
private const string SourceTexturePath =
|
||||
"Assets/GameContent/Test/AnimatorRaw/手接树叶特写.png";
|
||||
private const string SampleFolder = "Assets/GameContent/Test/FrameAnimation";
|
||||
private const string IntroClipPath = SampleFolder + "/Intro.asset";
|
||||
private const string IdleClipPath = SampleFolder + "/Idle.asset";
|
||||
private const string FlowGraphPath = SampleFolder + "/FlowSampleGraph.asset";
|
||||
private const string DirectGraphPath = SampleFolder + "/DirectSampleGraph.asset";
|
||||
private const string ScenePath = "Assets/Scenes/FrameAnimationRuntimeTest.unity";
|
||||
|
||||
[MenuItem("Tools/Frame Animation/Rebuild Runtime Sample")]
|
||||
private static void BuildFromMenu()
|
||||
{
|
||||
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BuildFromCommandLine();
|
||||
}
|
||||
|
||||
public static void BuildFromCommandLine()
|
||||
{
|
||||
EnsureFolder(SampleFolder);
|
||||
var sprites = AssetDatabase.LoadAllAssetsAtPath(SourceTexturePath)
|
||||
.OfType<Sprite>()
|
||||
.OrderBy(sprite => ExtractFrameNumber(sprite.name))
|
||||
.ToArray();
|
||||
if (sprites.Length < 21)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"样例源 Texture 需要至少 21 个 Sprite,当前只找到 {sprites.Length} 个。");
|
||||
}
|
||||
|
||||
var introClip = CreateOrLoadAsset<FrameClip>(IntroClipPath);
|
||||
introClip.Configure(
|
||||
"Intro",
|
||||
"Intro",
|
||||
CreateFrames(sprites, 0, 5),
|
||||
1f,
|
||||
FrameClipEndBehavior.HoldLastFrame);
|
||||
EditorUtility.SetDirty(introClip);
|
||||
|
||||
var idleClip = CreateOrLoadAsset<FrameClip>(IdleClipPath);
|
||||
idleClip.Configure(
|
||||
"Idle",
|
||||
"Idle",
|
||||
CreateFrames(sprites, 5, 16),
|
||||
1f,
|
||||
FrameClipEndBehavior.Loop);
|
||||
EditorUtility.SetDirty(idleClip);
|
||||
|
||||
var flowGraph = CreateOrLoadAsset<FrameAnimationGraph>(FlowGraphPath);
|
||||
var introNodeId = FindExistingNodeId(flowGraph, introClip.Id);
|
||||
var idleNodeId = FindExistingNodeId(flowGraph, idleClip.Id);
|
||||
var introNode = new AnimationNode(introClip.Id, "Intro", internalId: introNodeId);
|
||||
var idleNode = new AnimationNode(
|
||||
idleClip.Id,
|
||||
"Idle Loop",
|
||||
FrameClipEndBehavior.Loop,
|
||||
internalId: idleNodeId);
|
||||
var flow = new AnimationFlow("IntroToIdle", "Intro To Idle", introNode.InternalId);
|
||||
flowGraph.Configure(
|
||||
"FlowSampleGraph",
|
||||
"Flow Sample Graph",
|
||||
new[] { introClip, idleClip },
|
||||
new[] { introNode, idleNode },
|
||||
new[] { new AnimationEdge(introNode.InternalId, idleNode.InternalId) },
|
||||
new[] { flow },
|
||||
flow.Id);
|
||||
EditorUtility.SetDirty(flowGraph);
|
||||
|
||||
var directGraph = CreateOrLoadAsset<FrameAnimationGraph>(DirectGraphPath);
|
||||
directGraph.Configure(
|
||||
"DirectSampleGraph",
|
||||
"Direct Sample Graph",
|
||||
new[] { introClip, idleClip },
|
||||
Array.Empty<AnimationNode>(),
|
||||
Array.Empty<AnimationEdge>(),
|
||||
Array.Empty<AnimationFlow>(),
|
||||
idleClip.Id);
|
||||
EditorUtility.SetDirty(directGraph);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
BuildScene(flowGraph, directGraph);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
Debug.Log($"Frame Animation runtime sample rebuilt: {ScenePath}");
|
||||
}
|
||||
|
||||
private static IEnumerable<FrameAnimationFrame> CreateFrames(
|
||||
IReadOnlyList<Sprite> sprites,
|
||||
int startIndex,
|
||||
int count)
|
||||
{
|
||||
for (var offset = 0; offset < count; offset++)
|
||||
{
|
||||
var sourceIndex = startIndex + offset;
|
||||
var sprite = sprites[sourceIndex];
|
||||
yield return new FrameAnimationFrame(sprite, 250, sprite.name, sourceIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildScene(
|
||||
FrameAnimationGraph flowGraph,
|
||||
FrameAnimationGraph directGraph)
|
||||
{
|
||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||
|
||||
var cameraObject = new GameObject("Main Camera", typeof(Camera));
|
||||
cameraObject.tag = "MainCamera";
|
||||
cameraObject.transform.position = new Vector3(0f, 0f, -10f);
|
||||
var camera = cameraObject.GetComponent<Camera>();
|
||||
camera.orthographic = true;
|
||||
camera.orthographicSize = 6f;
|
||||
camera.clearFlags = CameraClearFlags.SolidColor;
|
||||
camera.backgroundColor = new Color(0.08f, 0.09f, 0.12f, 1f);
|
||||
|
||||
var spriteObject = new GameObject("Flow Sample - SpriteRenderer", typeof(SpriteRenderer));
|
||||
spriteObject.transform.position = new Vector3(-3.5f, 0f, 0f);
|
||||
spriteObject.transform.localScale = Vector3.one * 0.45f;
|
||||
var spritePlayer = spriteObject.AddComponent<FrameAnimationPlayer>();
|
||||
spritePlayer.ConfigureForAuthoring(flowGraph, true, 1f);
|
||||
|
||||
var canvasObject = new GameObject(
|
||||
"Direct Clip Sample Canvas",
|
||||
typeof(Canvas),
|
||||
typeof(CanvasScaler),
|
||||
typeof(GraphicRaycaster));
|
||||
var canvas = canvasObject.GetComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
var scaler = canvasObject.GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1280f, 720f);
|
||||
|
||||
var imageObject = new GameObject(
|
||||
"Direct Idle - Image",
|
||||
typeof(RectTransform),
|
||||
typeof(CanvasRenderer),
|
||||
typeof(Image));
|
||||
imageObject.transform.SetParent(canvasObject.transform, false);
|
||||
var rectTransform = imageObject.GetComponent<RectTransform>();
|
||||
rectTransform.sizeDelta = new Vector2(384f, 216f);
|
||||
rectTransform.anchoredPosition = new Vector2(300f, 0f);
|
||||
var image = imageObject.GetComponent<Image>();
|
||||
image.preserveAspect = true;
|
||||
var imagePlayer = imageObject.AddComponent<FrameAnimationPlayer>();
|
||||
imagePlayer.ConfigureForAuthoring(directGraph, true, 1f);
|
||||
|
||||
EditorSceneManager.MarkSceneDirty(scene);
|
||||
if (!EditorSceneManager.SaveScene(scene, ScenePath))
|
||||
{
|
||||
throw new InvalidOperationException($"无法保存样例场景:{ScenePath}");
|
||||
}
|
||||
}
|
||||
|
||||
private static T CreateOrLoadAsset<T>(string path) where T : ScriptableObject
|
||||
{
|
||||
var asset = AssetDatabase.LoadAssetAtPath<T>(path);
|
||||
if (asset != null)
|
||||
{
|
||||
return asset;
|
||||
}
|
||||
|
||||
asset = ScriptableObject.CreateInstance<T>();
|
||||
AssetDatabase.CreateAsset(asset, path);
|
||||
return asset;
|
||||
}
|
||||
|
||||
private static string FindExistingNodeId(FrameAnimationGraph graph, string clipId)
|
||||
{
|
||||
var existing = graph.Nodes.FirstOrDefault(node => node != null && node.ClipId == clipId);
|
||||
return existing != null && FrameAnimationValueUtility.IsValidInternalId(existing.InternalId)
|
||||
? existing.InternalId
|
||||
: null;
|
||||
}
|
||||
|
||||
private static int ExtractFrameNumber(string spriteName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(spriteName))
|
||||
{
|
||||
return int.MaxValue;
|
||||
}
|
||||
|
||||
var spaceIndex = spriteName.LastIndexOf(' ');
|
||||
var dotIndex = spriteName.LastIndexOf('.');
|
||||
if (spaceIndex >= 0 && dotIndex > spaceIndex &&
|
||||
int.TryParse(spriteName.Substring(spaceIndex + 1, dotIndex - spaceIndex - 1), out var number))
|
||||
{
|
||||
return number;
|
||||
}
|
||||
|
||||
return int.MaxValue;
|
||||
}
|
||||
|
||||
private static void EnsureFolder(string path)
|
||||
{
|
||||
var segments = path.Split('/');
|
||||
var current = segments[0];
|
||||
for (var index = 1; index < segments.Length; index++)
|
||||
{
|
||||
var next = current + "/" + segments[index];
|
||||
if (!AssetDatabase.IsValidFolder(next))
|
||||
{
|
||||
AssetDatabase.CreateFolder(current, segments[index]);
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user