Compare commits
777 Commits
v1.2.0
...
create-ese
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a3e54cb45 | ||
|
|
b6f1235239 | ||
|
|
41529f6fbb | ||
|
|
7dadacc8f9 | ||
|
|
7940f581a6 | ||
|
|
8605888f11 | ||
|
|
d57a007a42 | ||
|
|
89cdfe396b | ||
|
|
fac4bc19c5 | ||
|
|
aed91dbe45 | ||
|
|
c7f8208b6f | ||
|
|
5131ec3c52 | ||
|
|
7d74623710 | ||
|
|
044463dd5f | ||
|
|
ce2db4e48a | ||
|
|
0a88c6f2fc | ||
|
|
b0b95c60b4 | ||
|
|
683ac7a7d4 | ||
|
|
1e240e86f2 | ||
|
|
4d6c2fe7ff | ||
|
|
67c06720c5 | ||
|
|
33e98b9a75 | ||
|
|
a42f2412d7 | ||
|
|
fdb19a33fb | ||
|
|
1e31e9101b | ||
|
|
d66c18041e | ||
|
|
881ffad3bc | ||
|
|
4a16e30794 | ||
|
|
76691cc198 | ||
|
|
27b9e174eb | ||
|
|
ede440d277 | ||
|
|
5cb83f0743 | ||
|
|
7cbf92b8c7 | ||
|
|
a049bbe2f5 | ||
|
|
ec72df7af5 | ||
|
|
9327c1cef5 | ||
|
|
da5bf2116a | ||
|
|
67e97f89c6 | ||
|
|
31fd34b221 | ||
|
|
c4f7a13b74 | ||
|
|
155411e743 | ||
|
|
a84ff902e4 | ||
|
|
54038e3250 | ||
|
|
5544fca002 | ||
|
|
88b5ffc0a7 | ||
|
|
0c0a5f10f7 | ||
|
|
56e322de7f | ||
|
|
c2ebd387f2 | ||
|
|
e5e647f1a4 | ||
|
|
4d501ba448 | ||
|
|
275124b66c | ||
|
|
25936c19e9 | ||
|
|
f43631a1e1 | ||
|
|
f8c181836e | ||
|
|
840eb3452e | ||
|
|
0bf849e193 | ||
|
|
ebb984d354 | ||
|
|
068ca4bf69 | ||
|
|
4089051731 | ||
|
|
6b8b65ae16 | ||
|
|
a75c61c049 | ||
|
|
770c05402d | ||
|
|
9d581ccd8d | ||
|
|
235c432edb | ||
|
|
dbc6793dc4 | ||
|
|
58f70a5783 | ||
|
|
828ff969e1 | ||
|
|
49dd6a91c6 | ||
|
|
1e048d5c04 | ||
|
|
dff2ec564b | ||
|
|
2381919a5c | ||
|
|
66d9f428b3 | ||
|
|
a1e1189f9d | ||
|
|
96b5403d14 | ||
|
|
4b74db3f2d | ||
|
|
e24c850568 | ||
|
|
ecdb8f2021 | ||
|
|
536c4c5593 | ||
|
|
958933cd76 | ||
|
|
fbc911463a | ||
|
|
5b7746af79 | ||
|
|
9e195ae3fd | ||
|
|
a18eb5aa3c | ||
|
|
48d3d14af2 | ||
|
|
ed8f6e283b | ||
|
|
d834ca5e77 | ||
|
|
85e95ec18c | ||
|
|
ff9bc00729 | ||
|
|
7451a78e60 | ||
|
|
23ee2393c6 | ||
|
|
cd6ef222d1 | ||
|
|
b5158b6ac6 | ||
|
|
beaa1d09de | ||
|
|
a716d8006c | ||
|
|
1b0d38edce | ||
|
|
995fa2d514 | ||
|
|
c71a47f2b0 | ||
|
|
6c99b811ec | ||
|
|
40a38b8b88 | ||
|
|
ad96edfad0 | ||
|
|
240b165970 | ||
|
|
c3b7250f85 | ||
|
|
2476379af1 | ||
|
|
e0d659fe46 | ||
|
|
9ff03c04f3 | ||
|
|
7b45fbeab3 | ||
|
|
a733a53d3e | ||
|
|
dfd0dfc7f9 | ||
|
|
52bbccd53c | ||
|
|
d92c2a7b66 | ||
|
|
568b327425 | ||
|
|
1fb702169e | ||
|
|
3617f40309 | ||
|
|
0c03b13d74 | ||
|
|
3cbfa1e4cb | ||
|
|
397f79caa5 | ||
|
|
972c1d5357 | ||
|
|
32d35ef2ee | ||
|
|
57e165779e | ||
|
|
690d7859c8 | ||
|
|
8f9a7d8581 | ||
|
|
3d5fcc1a55 | ||
|
|
823e0c1d94 | ||
|
|
13a149c3a2 | ||
|
|
dd130eacb0 | ||
|
|
d0238add2d | ||
|
|
fe96d72ac6 | ||
|
|
b2b8df9340 | ||
|
|
2d56eaf11a | ||
|
|
2cb9c471f9 | ||
|
|
e8fc7f497b | ||
|
|
6702f0bfad | ||
|
|
d7454e3ca4 | ||
|
|
0d9bab910e | ||
|
|
3d16bbdc64 | ||
|
|
b4e7ba2abd | ||
|
|
374b26f7c6 | ||
|
|
dbebb4f4fb | ||
|
|
eec89b626c | ||
|
|
763d23e960 | ||
|
|
3b56ed17fe | ||
|
|
4b8d22ac32 | ||
|
|
9cd873da14 | ||
|
|
c1799bf7b3 | ||
|
|
85be826b62 | ||
|
|
dd1ae97de7 | ||
|
|
63f006ab62 | ||
|
|
caf7622aa0 | ||
|
|
d746cf3bb8 | ||
|
|
88af781d78 | ||
|
|
15d5d37e50 | ||
|
|
b9aaf894d7 | ||
|
|
460cdb5af4 | ||
|
|
290bd9858e | ||
|
|
b42a7b4e43 | ||
|
|
189714c727 | ||
|
|
987051acd4 | ||
|
|
374e08a79e | ||
|
|
359886c72f | ||
|
|
f03b73b58e | ||
|
|
18d20df4da | ||
|
|
c5642a8605 | ||
|
|
673f5e5855 | ||
|
|
cabb625a17 | ||
|
|
b8f05b79b0 | ||
|
|
b22faaac86 | ||
|
|
107439d70c | ||
|
|
71869b1a58 | ||
|
|
9aed3134cf | ||
|
|
3ff57aff37 | ||
|
|
152c0541b8 | ||
|
|
7b14fa2da4 | ||
|
|
3fb6f919f8 | ||
|
|
551ca7805d | ||
|
|
8ab25fe293 | ||
|
|
eea7ed9e58 | ||
|
|
0279cf6d27 | ||
|
|
0dff1ad2ad | ||
|
|
95fbcca66f | ||
|
|
a61baa83a7 | ||
|
|
afebeecd68 | ||
|
|
f4e9925319 | ||
|
|
32460ac133 | ||
|
|
4d95a7f044 | ||
|
|
57f919fbe0 | ||
|
|
1cb9a0e58f | ||
|
|
1da43ee822 | ||
|
|
f4c7563763 | ||
|
|
a3f7cc38b1 | ||
|
|
b15cbab313 | ||
|
|
504b9ffb66 | ||
|
|
6226e3ff06 | ||
|
|
2621d7f659 | ||
|
|
a768b890fd | ||
|
|
8b9616837d | ||
|
|
0d2948e60c | ||
|
|
ecfef727c8 | ||
|
|
caed5428d5 | ||
|
|
bce3a6e253 | ||
|
|
eac660b1a0 | ||
|
|
af49870084 | ||
|
|
e2b316b3cc | ||
|
|
3a0544629d | ||
|
|
609baace73 | ||
|
|
b12cfba353 | ||
|
|
6242c6daf3 | ||
|
|
b5337de278 | ||
|
|
3512199ff4 | ||
|
|
e03b106652 | ||
|
|
f9afa22406 | ||
|
|
adfc7e91b3 | ||
|
|
40cde9c050 | ||
|
|
ddc7a7750e | ||
|
|
50a01d9dd3 | ||
|
|
793aad0a5e | ||
|
|
9c1bf8dbed | ||
|
|
620f3eecc7 | ||
|
|
4355538d8d | ||
|
|
3ad5dc9ca3 | ||
|
|
57c7e7be3f | ||
|
|
6778ccace4 | ||
|
|
1264232533 | ||
|
|
61813e67b6 | ||
|
|
c58e3411fd | ||
|
|
011d795361 | ||
|
|
3f40a04370 | ||
|
|
fc042bb7d9 | ||
|
|
d051e52131 | ||
|
|
fb4316aeb9 | ||
|
|
683203919f | ||
|
|
a0cddbcae6 | ||
|
|
4e81fc7eba | ||
|
|
b410e2de47 | ||
|
|
9868c746e1 | ||
|
|
f0b4453a5f | ||
|
|
6b49471734 | ||
|
|
fe791e83a8 | ||
|
|
edbc9eb27f | ||
|
|
2f63034d9a | ||
|
|
dee0e0284a | ||
|
|
890e591f2a | ||
|
|
cb6561e27b | ||
|
|
7ef70d7f9a | ||
|
|
86405c1dcd | ||
|
|
60fa259285 | ||
|
|
27f86eece2 | ||
|
|
4cee396ea9 | ||
|
|
d2ad295b48 | ||
|
|
009f8af4e1 | ||
|
|
0cd99209c4 | ||
|
|
3ea55303dc | ||
|
|
c458a5e036 | ||
|
|
c511725d1f | ||
|
|
3876d9b92b | ||
|
|
f863c48ab0 | ||
|
|
10096795a1 | ||
|
|
8b146c8d5f | ||
|
|
1208c4ffeb | ||
|
|
ec5de97973 | ||
|
|
0daa92cfb7 | ||
|
|
130f466026 | ||
|
|
f93de87940 | ||
|
|
367d97e9bb | ||
|
|
77701f214c | ||
|
|
b5b64f8c41 | ||
|
|
ab04ad30f1 | ||
|
|
330d9a6fdb | ||
|
|
e762343142 | ||
|
|
fce9e3d4d6 | ||
|
|
0e5855ee4e | ||
|
|
ba61737bc7 | ||
|
|
bd7ea1f713 | ||
|
|
1abd20edf5 | ||
|
|
496513c641 | ||
|
|
848b637f45 | ||
|
|
39049601d4 | ||
|
|
e31cdd17d3 | ||
|
|
2a3f2d49b8 | ||
|
|
ca452889d7 | ||
|
|
2df501ec07 | ||
|
|
6b1e6c6fdc | ||
|
|
570e970e1c | ||
|
|
f4e3505d52 | ||
|
|
9af2b9859a | ||
|
|
d99d314621 | ||
|
|
c5b8b18e33 | ||
|
|
7280265a64 | ||
|
|
35fa0ef884 | ||
|
|
9c778cb71b | ||
|
|
4a401744c1 | ||
|
|
5f6b2d4d40 | ||
|
|
bf25218af2 | ||
|
|
1f7f9d9f84 | ||
|
|
2be53a04e4 | ||
|
|
7f56ebc786 | ||
|
|
a801e4f50e | ||
|
|
a9f9ad9b94 | ||
|
|
3cf1dab5b9 | ||
|
|
63165bbbfc | ||
|
|
61caad2bef | ||
|
|
b826bbc4c7 | ||
|
|
2ce7dad8d8 | ||
|
|
dff400bf22 | ||
|
|
27ce902344 | ||
|
|
33ee0a04c6 | ||
|
|
d68f6922f8 | ||
|
|
f8539d7958 | ||
|
|
14dc911e0a | ||
|
|
deccb6bf84 | ||
|
|
dacbfcae95 | ||
|
|
1b69ed17b7 | ||
|
|
241acc9050 | ||
|
|
8fa921930c | ||
|
|
011e43811a | ||
|
|
9f16debd75 | ||
|
|
92c56c439b | ||
|
|
7de6a5af0f | ||
|
|
173a063781 | ||
|
|
e04ac7c909 | ||
|
|
a6e49e1d47 | ||
|
|
f0046c7dc2 | ||
|
|
2a17c47c25 | ||
|
|
8d741bf1b9 | ||
|
|
c676006632 | ||
|
|
5bcfd597b9 | ||
|
|
3cda3c2238 | ||
|
|
43bdd7e43b | ||
|
|
1ec7892338 | ||
|
|
6bcfd48a2f | ||
|
|
345ef70972 | ||
|
|
c876edca0c | ||
|
|
fcf3def284 | ||
|
|
6f1a2896dd | ||
|
|
62381f4160 | ||
|
|
171805debf | ||
|
|
619abcbfbc | ||
|
|
03909924c2 | ||
|
|
f4ea077114 | ||
|
|
956ccf9195 | ||
|
|
e880925e3f | ||
|
|
0a860920ad | ||
|
|
fb7a1b1282 | ||
|
|
59970ef7c3 | ||
|
|
a7750c2894 | ||
|
|
b69b81f63a | ||
|
|
00fc6dfd67 | ||
|
|
82451e9fd3 | ||
|
|
d0fcc0e447 | ||
|
|
285279629e | ||
|
|
cbfe09b5e9 | ||
|
|
b757c1d06c | ||
|
|
4550a6146a | ||
|
|
3224bb9696 | ||
|
|
3a5e73266e | ||
|
|
1cf5641c4c | ||
|
|
85dad41e60 | ||
|
|
bd839cf431 | ||
|
|
b20b2ae4ce | ||
|
|
cac6aedf78 | ||
|
|
a572c80967 | ||
|
|
a09e8261db | ||
|
|
62e8ebe926 | ||
|
|
96e0a9126f | ||
|
|
4afb195814 | ||
|
|
7da5366bca | ||
|
|
d979c38615 | ||
|
|
7def06126b | ||
|
|
9f600f88b0 | ||
|
|
e3c4d5f0c0 | ||
|
|
b97f3a8431 | ||
|
|
3b917a06af | ||
|
|
360106fb92 | ||
|
|
507ed5005f | ||
|
|
8bea5d5e68 | ||
|
|
99076adb72 | ||
|
|
d1a6230b23 | ||
|
|
cfe3916934 | ||
|
|
d798995876 | ||
|
|
43e6b7bf88 | ||
|
|
9253686de1 | ||
|
|
7e7eae2d1a | ||
|
|
1924d979d6 | ||
|
|
ed84394301 | ||
|
|
bb99cf5389 | ||
|
|
2d0700f441 | ||
|
|
e3ead8a695 | ||
|
|
942043f0b0 | ||
|
|
23d81bca35 | ||
|
|
701f538e57 | ||
|
|
bb3017ffc2 | ||
|
|
f2b9c5cc5a | ||
|
|
532a52acfc | ||
|
|
c19b5ae9a7 | ||
|
|
5f507532ed | ||
|
|
6e48f22540 | ||
|
|
66aa9f4f20 | ||
|
|
62f895efe0 | ||
|
|
4a060e1ce3 | ||
|
|
a0177c9163 | ||
|
|
f45af34614 | ||
|
|
14a8d755f0 | ||
|
|
b67ab80c75 | ||
|
|
ae71af856b | ||
|
|
279c1d9bc9 | ||
|
|
9068a109b0 | ||
|
|
7850fc610c | ||
|
|
536871d09b | ||
|
|
1af2cf5f99 | ||
|
|
b13132b259 | ||
|
|
a1a6970ea4 | ||
|
|
41bbe23404 | ||
|
|
1d2a3e283e | ||
|
|
62d7521384 | ||
|
|
bf14b59a28 | ||
|
|
0a0f64510f | ||
|
|
9445c735c3 | ||
|
|
7339e7ecec | ||
|
|
79f7c89e23 | ||
|
|
e724e5a1ba | ||
|
|
fdaa94a61d | ||
|
|
6af0074c36 | ||
|
|
97a69fed09 | ||
|
|
959879440d | ||
|
|
fd1bbb0e00 | ||
|
|
072e68cf43 | ||
|
|
610232e6b0 | ||
|
|
69c46f32eb | ||
|
|
06b3f92007 | ||
|
|
c631290049 | ||
|
|
f41c1a3ca3 | ||
|
|
bd6ba84087 | ||
|
|
1512409eb3 | ||
|
|
bcb5feeb1c | ||
|
|
da8b7cf601 | ||
|
|
316527c459 | ||
|
|
da70818b22 | ||
|
|
5ea3b72b2b | ||
|
|
632864b361 | ||
|
|
952247def0 | ||
|
|
51debede52 | ||
|
|
ce7b731bcf | ||
|
|
86e2dc8fdb | ||
|
|
78047134c2 | ||
|
|
125a1686ab | ||
|
|
d542ac48b8 | ||
|
|
1ac0227c90 | ||
|
|
a5e70bcd99 | ||
|
|
38763de7f6 | ||
|
|
db73b077c5 | ||
|
|
0969d09da1 | ||
|
|
a07108a431 | ||
|
|
6693b56ab8 | ||
|
|
a7349bd360 | ||
|
|
e92c0040b5 | ||
|
|
f448fa48c4 | ||
|
|
aa33cad4fa | ||
|
|
d0cb7d5359 | ||
|
|
90153b98fe | ||
|
|
8c4e8d523e | ||
|
|
90ad4b3ec4 | ||
|
|
62bc6b547e | ||
|
|
be11060674 | ||
|
|
d62bf9f7f9 | ||
|
|
61fcd52c65 | ||
|
|
2947ddeb64 | ||
|
|
d9b752c180 | ||
|
|
b82891caee | ||
|
|
05f04ef37e | ||
|
|
66dc9780b9 | ||
|
|
d48b22c656 | ||
|
|
727b1864eb | ||
|
|
de3bfd7551 | ||
|
|
dedb91379f | ||
|
|
1dfcd008aa | ||
|
|
cf2dc91af6 | ||
|
|
a66f80a766 | ||
|
|
f4e49c316e | ||
|
|
d1cd72bbb2 | ||
|
|
6178851def | ||
|
|
945f772c30 | ||
|
|
b546c9c712 | ||
|
|
413ce93b31 | ||
|
|
cffe32911d | ||
|
|
4f651eb42e | ||
|
|
6da1585b6b | ||
|
|
b988e81a1b | ||
|
|
1a1c1087d2 | ||
|
|
1a1549230f | ||
|
|
64ea53eba1 | ||
|
|
5e052a7e7d | ||
|
|
cf9ea495d0 | ||
|
|
9603c6423b | ||
|
|
457eef585e | ||
|
|
1ade449c4d | ||
|
|
aa9d73a810 | ||
|
|
cc266a7ba9 | ||
|
|
d8ea324018 | ||
|
|
60566e8d78 | ||
|
|
306d2994dc | ||
|
|
e6a8791fc3 | ||
|
|
6cbbc06998 | ||
|
|
0b4244fd8e | ||
|
|
367ddfbf8a | ||
|
|
168e028098 | ||
|
|
042ded37d2 | ||
|
|
4137eb2bce | ||
|
|
20a3f03e12 | ||
|
|
7792710694 | ||
|
|
dbddbbdfb8 | ||
|
|
4869f5741e | ||
|
|
bda547dd2e | ||
|
|
ef80b03a44 | ||
|
|
6e511ae949 | ||
|
|
94541d0abb | ||
|
|
586a0e5d14 | ||
|
|
814842dbaf | ||
|
|
70a993573f | ||
|
|
21659cbb13 | ||
|
|
a44251cc55 | ||
|
|
69616bbddc | ||
|
|
0a1d7ac083 | ||
|
|
364bc4cdab | ||
|
|
2504eb24e1 | ||
|
|
bdbef0bd0d | ||
|
|
e4e38ee4e6 | ||
|
|
021e892e33 | ||
|
|
c27d5022fd | ||
|
|
6730a5d625 | ||
|
|
32092f992d | ||
|
|
a5f0c8f6b5 | ||
|
|
85cd93e51a | ||
|
|
0b7e623748 | ||
|
|
62f250b43c | ||
|
|
25136349ff | ||
|
|
baeb047e27 | ||
|
|
56dd18b983 | ||
|
|
86cb70a94f | ||
|
|
9f76d37a82 | ||
|
|
a026ed9428 | ||
|
|
c178e2fbcc | ||
|
|
b88bb1dc87 | ||
|
|
3069e28224 | ||
|
|
d69b3af99b | ||
|
|
7398b7c6d0 | ||
|
|
5d57904d22 | ||
|
|
7daf352a25 | ||
|
|
6a49f6a534 | ||
|
|
5bce08683a | ||
|
|
edc60fc3d8 | ||
|
|
1361fd8a90 | ||
|
|
d539bb3dd9 | ||
|
|
3b9ae4f384 | ||
|
|
2783448de5 | ||
|
|
6e21ff08d5 | ||
|
|
e56278e4a6 | ||
|
|
fc9bf816dd | ||
|
|
854fd7df3a | ||
|
|
87dd564a12 | ||
|
|
2d389308ea | ||
|
|
ea8523be35 | ||
|
|
4479f0fab0 | ||
|
|
7a000318a6 | ||
|
|
9a08ae74b6 | ||
|
|
f3d2950df3 | ||
|
|
8cfba4a166 | ||
|
|
51e6bba2a7 | ||
|
|
ccbfa78070 | ||
|
|
69655f1936 | ||
|
|
6ea366cfed | ||
|
|
b7d17fb16d | ||
|
|
f3dc8c6344 | ||
|
|
69ec545854 | ||
|
|
65386ff731 | ||
|
|
01fa33e122 | ||
|
|
0411aa9aef | ||
|
|
4a5c890121 | ||
|
|
4c11fdc176 | ||
|
|
d99e7a45ea | ||
|
|
52528ff1b7 | ||
|
|
4a9317f3f4 | ||
|
|
9450dd5869 | ||
|
|
d5471e4828 | ||
|
|
2f71785add | ||
|
|
608f5030b2 | ||
|
|
dd8f3714ed | ||
|
|
abec2b3648 | ||
|
|
ea06a9f07d | ||
|
|
9f54759cc5 | ||
|
|
55dd5f9ed0 | ||
|
|
05455421fb | ||
|
|
af61067f08 | ||
|
|
19cda88248 | ||
|
|
0edb2738a1 | ||
|
|
e1bc364525 | ||
|
|
2925ee380d | ||
|
|
731edf5872 | ||
|
|
7b85039b17 | ||
|
|
2bc45fa574 | ||
|
|
d2b4455205 | ||
|
|
bce4a26197 | ||
|
|
1da5040d60 | ||
|
|
afd33e053b | ||
|
|
171d03c006 | ||
|
|
34d5237aaa | ||
|
|
037c3d6a05 | ||
|
|
5596ba634e | ||
|
|
a5f69065f4 | ||
|
|
969ef249ea | ||
|
|
a37183851f | ||
|
|
4cf3e1a769 | ||
|
|
354e5a2761 | ||
|
|
c9fd8cc2a7 | ||
|
|
bb19f752a1 | ||
|
|
6bd9c1055c | ||
|
|
dff77097c6 | ||
|
|
b4dc1c5661 | ||
|
|
992338d924 | ||
|
|
f88a402b0c | ||
|
|
5938d36149 | ||
|
|
78577db3f9 | ||
|
|
0b4a6b77e2 | ||
|
|
01084a8897 | ||
|
|
0f18a1979e | ||
|
|
68a615bc7b | ||
|
|
add1068c1a | ||
|
|
7a40df9965 | ||
|
|
3e6a1aa59a | ||
|
|
d3fe79cf39 | ||
|
|
48fa547c8f | ||
|
|
80e2f7df71 | ||
|
|
0107f1f58a | ||
|
|
d29c9a96f4 | ||
|
|
37d75c3281 | ||
|
|
666ded7b89 | ||
|
|
73a882f75e | ||
|
|
310f5f2349 | ||
|
|
8c86d6b696 | ||
|
|
82cd163adc | ||
|
|
802ee25621 | ||
|
|
f48ebb65ba | ||
|
|
aaa2a8ed2c | ||
|
|
5a06f5420b | ||
|
|
343f5a44f2 | ||
|
|
92125aee3a | ||
|
|
96f651b7ca | ||
|
|
06ea01e928 | ||
|
|
577f1e429a | ||
|
|
7808f64fe5 | ||
|
|
e6789e49e4 | ||
|
|
797619aece | ||
|
|
1b5363611d | ||
|
|
103f773286 | ||
|
|
d9ef0b587e | ||
|
|
d5b98256f0 | ||
|
|
efcceaa898 | ||
|
|
e4aad11965 | ||
|
|
47207fad52 | ||
|
|
202bf82896 | ||
|
|
0e3274a743 | ||
|
|
b06174926d | ||
|
|
abb23a3c02 | ||
|
|
0c8f232282 | ||
|
|
ef023d27bf | ||
|
|
7a591825eb | ||
|
|
e71c49d596 | ||
|
|
e6ce8995ba | ||
|
|
f6250b6d5b | ||
|
|
757eff2937 | ||
|
|
996a7f3ddf | ||
|
|
94c050bacb | ||
|
|
3f4aa59a29 | ||
|
|
bee7cf4278 | ||
|
|
b9db6f0b40 | ||
|
|
8967cba3c7 | ||
|
|
d04ad2eea9 | ||
|
|
f2d3880a06 | ||
|
|
ec5f70ecfc | ||
|
|
40b3fe7165 | ||
|
|
4095f1e946 | ||
|
|
e219fc47ba | ||
|
|
6e2e7a4af5 | ||
|
|
2e7f764d6c | ||
|
|
ce64de5b3d | ||
|
|
35ca1dd7ea | ||
|
|
8d0ad6b871 | ||
|
|
0aa4791cf7 | ||
|
|
082c2b46d0 | ||
|
|
50420f9052 | ||
|
|
499cbf8a60 | ||
|
|
2e38284d6e | ||
|
|
00cc3a11c6 | ||
|
|
9909a7f7b0 | ||
|
|
3363fca160 | ||
|
|
78e0b09c7a | ||
|
|
caa3ffc8f5 | ||
|
|
323fb6a5fe | ||
|
|
1adc5f1729 | ||
|
|
78079252c9 | ||
|
|
72fdabd099 | ||
|
|
64bd6aa055 | ||
|
|
6329200b84 | ||
|
|
3f3fd16110 | ||
|
|
34f0c4ac2d | ||
|
|
dde04d514e | ||
|
|
ffddadd798 | ||
|
|
5dca337b92 | ||
|
|
131df181e6 | ||
|
|
e207952786 | ||
|
|
3f7ef284fc | ||
|
|
1a41533d15 | ||
|
|
85f7bbbf1a | ||
|
|
ccc603b59f | ||
|
|
8135f99616 | ||
|
|
713f4ae18b | ||
|
|
f3f5d0bbd1 | ||
|
|
4a9e11c480 | ||
|
|
cd94326aad | ||
|
|
a4b971bba0 | ||
|
|
5d1609111c | ||
|
|
d54ccaf629 | ||
|
|
6adea240e2 | ||
|
|
3486d403d2 | ||
|
|
f533186c8d | ||
|
|
0beadf8e5a | ||
|
|
8bc06f0476 | ||
|
|
8fd8f74b27 | ||
|
|
9fc9b60de5 | ||
|
|
2a026726db | ||
|
|
f9a99f8b09 | ||
|
|
95b8dc765d | ||
|
|
052d6e25e7 | ||
|
|
fd82486bbc | ||
|
|
ef07f7555f | ||
|
|
67859b7be0 | ||
|
|
1e5ddadd00 | ||
|
|
7bab76d765 | ||
|
|
416f243bda | ||
|
|
77ad112f67 | ||
|
|
dc3d639824 | ||
|
|
13a001c258 | ||
|
|
85bdd97d48 | ||
|
|
3d9c8699e7 | ||
|
|
ea482dab48 | ||
|
|
96a41eb2cc | ||
|
|
e5dfb20aa2 | ||
|
|
fe308d35f2 | ||
|
|
219b90fc5d | ||
|
|
44e2ca07e5 | ||
|
|
79d684caae | ||
|
|
4db8734a34 | ||
|
|
8b7baf7f86 | ||
|
|
28145e876f | ||
|
|
53625cf87b | ||
|
|
88f9779dd0 | ||
|
|
561a44b26b | ||
|
|
26068aaf6f | ||
|
|
fb0f9dc608 | ||
|
|
6c44d38c10 | ||
|
|
da3ab02a8d | ||
|
|
9c7703eb2d | ||
|
|
84922794fb | ||
|
|
1dcce99d87 | ||
|
|
fd62aa469a | ||
|
|
63d307b95a | ||
|
|
9f12971eda | ||
|
|
47edd4f583 | ||
|
|
e94b7b23e4 | ||
|
|
5c77e6c56d | ||
|
|
522cbe0e6e | ||
|
|
f7061f360d | ||
|
|
16cdfa0426 | ||
|
|
74bd0c161f | ||
|
|
da595e1eb9 | ||
|
|
5277c0de7d | ||
|
|
b3c85e00f9 | ||
|
|
0748652a8d | ||
|
|
9fa0442b20 |
62
.all-contributorsrc
Normal file
62
.all-contributorsrc
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"projectName": "ecs-framework",
|
||||
"projectOwner": "esengine",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"files": ["README.md"],
|
||||
"imageSize": 100,
|
||||
"commit": true,
|
||||
"commitConvention": "angular",
|
||||
"contributors": [
|
||||
{
|
||||
"login": "yhh",
|
||||
"name": "Frank Huang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/145575?v=4",
|
||||
"profile": "https://github.com/yhh",
|
||||
"contributions": ["code"]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"contributorsSortAlphabetically": false,
|
||||
"badgeTemplate": "[](#contributors)",
|
||||
"contributorTemplate": "<a href=\"<%= contributor.profile %>\"><img src=\"<%= contributor.avatar_url %>\" width=\"<%= options.imageSize %>px;\" alt=\"<%= contributor.name %>\"/><br /><sub><b><%= contributor.name %></b></sub></a>",
|
||||
"types": {
|
||||
"code": {
|
||||
"symbol": "💻",
|
||||
"description": "Code",
|
||||
"link": "[<%= symbol %>](<%= url %> \"Code\")"
|
||||
},
|
||||
"doc": {
|
||||
"symbol": "📖",
|
||||
"description": "Documentation",
|
||||
"link": "[<%= symbol %>](<%= url %> \"Documentation\")"
|
||||
},
|
||||
"test": {
|
||||
"symbol": "⚠️",
|
||||
"description": "Tests",
|
||||
"link": "[<%= symbol %>](<%= url %> \"Tests\")"
|
||||
},
|
||||
"bug": {
|
||||
"symbol": "🐛",
|
||||
"description": "Bug reports",
|
||||
"link": "[<%= symbol %>](<%= url %> \"Bug reports\")"
|
||||
},
|
||||
"example": {
|
||||
"symbol": "💡",
|
||||
"description": "Examples",
|
||||
"link": "[<%= symbol %>](<%= url %> \"Examples\")"
|
||||
},
|
||||
"design": {
|
||||
"symbol": "🎨",
|
||||
"description": "Design",
|
||||
"link": "[<%= symbol %>](<%= url %> \"Design\")"
|
||||
},
|
||||
"ideas": {
|
||||
"symbol": "🤔",
|
||||
"description": "Ideas & Planning",
|
||||
"link": "[<%= symbol %>](<%= url %> \"Ideas & Planning\")"
|
||||
}
|
||||
},
|
||||
"skipCi": true
|
||||
}
|
||||
|
||||
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
57
.changeset/config.json
Normal file
57
.changeset/config.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
|
||||
"changelog": [
|
||||
"@changesets/changelog-github",
|
||||
{ "repo": "esengine/esengine" }
|
||||
],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [
|
||||
["@esengine/ecs-framework", "@esengine/ecs-framework-math"]
|
||||
],
|
||||
"access": "public",
|
||||
"baseBranch": "master",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": [
|
||||
"@esengine/engine-core",
|
||||
"@esengine/runtime-core",
|
||||
"@esengine/asset-system",
|
||||
"@esengine/material-system",
|
||||
"@esengine/ecs-engine-bindgen",
|
||||
"@esengine/script-runtime",
|
||||
"@esengine/platform-common",
|
||||
"@esengine/platform-web",
|
||||
"@esengine/platform-wechat",
|
||||
"@esengine/sprite",
|
||||
"@esengine/camera",
|
||||
"@esengine/particle",
|
||||
"@esengine/tilemap",
|
||||
"@esengine/mesh-3d",
|
||||
"@esengine/effect",
|
||||
"@esengine/audio",
|
||||
"@esengine/fairygui",
|
||||
"@esengine/physics-rapier2d",
|
||||
"@esengine/rapier2d",
|
||||
"@esengine/world-streaming",
|
||||
"@esengine/editor-core",
|
||||
"@esengine/editor-runtime",
|
||||
"@esengine/editor-app",
|
||||
"@esengine/sprite-editor",
|
||||
"@esengine/camera-editor",
|
||||
"@esengine/particle-editor",
|
||||
"@esengine/tilemap-editor",
|
||||
"@esengine/mesh-3d-editor",
|
||||
"@esengine/fairygui-editor",
|
||||
"@esengine/physics-rapier2d-editor",
|
||||
"@esengine/behavior-tree-editor",
|
||||
"@esengine/blueprint-editor",
|
||||
"@esengine/asset-system-editor",
|
||||
"@esengine/material-editor",
|
||||
"@esengine/shader-editor",
|
||||
"@esengine/world-streaming-editor",
|
||||
"@esengine/node-editor",
|
||||
"@esengine/sdk",
|
||||
"@esengine/worker-generator",
|
||||
"@esengine/engine"
|
||||
]
|
||||
}
|
||||
36
.coderabbit.yaml
Normal file
36
.coderabbit.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
# CodeRabbit 配置文件
|
||||
# https://docs.coderabbit.ai/configuration
|
||||
|
||||
language: "zh-CN" # 使用中文评论
|
||||
reviews:
|
||||
# 审查级别
|
||||
profile: "chill" # "chill" 或 "strict" 或 "assertive"
|
||||
|
||||
# 自动审查设置
|
||||
auto_review:
|
||||
enabled: true
|
||||
drafts: false # 草稿 PR 不自动审查
|
||||
base_branches:
|
||||
- master
|
||||
- main
|
||||
|
||||
# 审查内容
|
||||
request_changes_workflow: false # 不阻止 PR 合并
|
||||
high_level_summary: true # 生成高层次摘要
|
||||
poem: false # 不生成诗歌(可以改为 true 增加趣味)
|
||||
review_status: true # 显示审查状态
|
||||
|
||||
# 忽略的文件
|
||||
path_filters:
|
||||
- "!**/*.md" # 不审查 markdown
|
||||
- "!**/package-lock.json" # 不审查 lock 文件
|
||||
- "!**/dist/**" # 不审查构建输出
|
||||
- "!**/*.min.js" # 不审查压缩文件
|
||||
|
||||
# 聊天设置
|
||||
chat:
|
||||
auto_reply: true # 自动回复问题
|
||||
|
||||
# 提交建议
|
||||
suggestions:
|
||||
enabled: true # 启用代码建议
|
||||
29
.commitlintrc.json
Normal file
29
.commitlintrc.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": ["@commitlint/config-conventional"],
|
||||
"rules": {
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"feat",
|
||||
"fix",
|
||||
"docs",
|
||||
"style",
|
||||
"refactor",
|
||||
"perf",
|
||||
"test",
|
||||
"build",
|
||||
"ci",
|
||||
"chore",
|
||||
"revert"
|
||||
]
|
||||
],
|
||||
"scope-enum": [
|
||||
0
|
||||
],
|
||||
"scope-empty": [0],
|
||||
"subject-empty": [2, "never"],
|
||||
"subject-case": [0],
|
||||
"header-max-length": [2, "always", 100]
|
||||
}
|
||||
}
|
||||
35
.editorconfig
Normal file
35
.editorconfig
Normal file
@@ -0,0 +1,35 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# 所有文件的默认设置
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# TypeScript/JavaScript 文件
|
||||
[*.{ts,tsx,js,jsx,mjs,cjs}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# JSON 文件
|
||||
[*.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# YAML 文件
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Markdown 文件
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
indent_size = 2
|
||||
|
||||
# 包管理文件
|
||||
[{package.json,package-lock.json,tsconfig.json}]
|
||||
indent_size = 2
|
||||
4
.fleet/settings.json
Normal file
4
.fleet/settings.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"toolchains": [],
|
||||
"backend.maxHeapSizeMb": 896
|
||||
}
|
||||
44
.gitattributes
vendored
Normal file
44
.gitattributes
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# 自动检测文本文件并规范化换行符
|
||||
* text=auto
|
||||
|
||||
# 源代码文件强制使用 LF
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
*.js text eol=lf
|
||||
*.jsx text eol=lf
|
||||
*.mjs text eol=lf
|
||||
*.cjs text eol=lf
|
||||
*.json text eol=lf
|
||||
*.md text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
|
||||
# 配置文件强制使用 LF
|
||||
.gitignore text eol=lf
|
||||
.gitattributes text eol=lf
|
||||
.editorconfig text eol=lf
|
||||
.prettierrc text eol=lf
|
||||
.prettierignore text eol=lf
|
||||
.eslintrc.json text eol=lf
|
||||
tsconfig.json text eol=lf
|
||||
|
||||
# Shell 脚本强制使用 LF
|
||||
*.sh text eol=lf
|
||||
|
||||
# Windows 批处理文件使用 CRLF
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
|
||||
# 二进制文件不转换
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.svg binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
*.otf binary
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ['https://github.com/esengine/ecs-framework/blob/master/sponsor/alipay.jpg', 'https://github.com/esengine/ecs-framework/blob/master/sponsor/wechatpay.png'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
custom: ['https://github.com/esengine/esengine/blob/master/sponsor/alipay.jpg', 'https://github.com/esengine/esengine/blob/master/sponsor/wechatpay.png'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
|
||||
130
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
130
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
name: 🐛 Bug Report / 错误报告
|
||||
description: Report a bug or issue / 报告一个错误或问题
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢你提交 Bug 报告!请填写以下信息帮助我们更快定位问题。
|
||||
Thanks for reporting a bug! Please fill in the information below to help us locate the issue faster.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 问题描述 / Bug Description
|
||||
description: 清晰简洁地描述遇到的问题 / A clear and concise description of the bug
|
||||
placeholder: 例如:当我创建超过1000个实体时,游戏卡顿严重...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: 复现步骤 / Steps to Reproduce
|
||||
description: 如何复现这个问题?/ How can we reproduce this issue?
|
||||
placeholder: |
|
||||
1. 创建场景
|
||||
2. 添加 1000 个实体
|
||||
3. 运行游戏
|
||||
4. 观察卡顿
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: 期望行为 / Expected Behavior
|
||||
description: 你期望发生什么?/ What did you expect to happen?
|
||||
placeholder: 游戏应该流畅运行,FPS 保持在 60...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: 实际行为 / Actual Behavior
|
||||
description: 实际发生了什么?/ What actually happened?
|
||||
placeholder: FPS 降到 20,游戏严重卡顿...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: 版本 / Version
|
||||
description: 使用的 @esengine/ecs-framework 版本 / Version of @esengine/ecs-framework
|
||||
placeholder: 例如 / e.g., 2.2.8
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: 平台 / Platform
|
||||
description: 在哪个平台遇到问题?/ Which platform did you encounter the issue?
|
||||
multiple: true
|
||||
options:
|
||||
- Web / 浏览器
|
||||
- Cocos Creator
|
||||
- Laya Engine
|
||||
- WeChat Mini Game / 微信小游戏
|
||||
- Other / 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: 环境信息 / Environment
|
||||
description: |
|
||||
相关环境信息 / Relevant environment information
|
||||
例如:操作系统、浏览器版本、Node.js 版本等
|
||||
placeholder: |
|
||||
- OS: Windows 11
|
||||
- Browser: Chrome 120
|
||||
- Node.js: 20.10.0
|
||||
value: |
|
||||
- OS:
|
||||
- Browser:
|
||||
- Node.js:
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: code
|
||||
attributes:
|
||||
label: 代码示例 / Code Sample
|
||||
description: 如果可能,提供最小可复现代码 / If possible, provide minimal reproducible code
|
||||
render: typescript
|
||||
placeholder: |
|
||||
import { Core, Scene, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
// 你的代码 / Your code here
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: 错误日志 / Error Logs
|
||||
description: 相关的错误日志或截图 / Relevant error logs or screenshots
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: 检查清单 / Checklist
|
||||
options:
|
||||
- label: 我已经搜索过类似的 issue / I have searched for similar issues
|
||||
required: true
|
||||
- label: 我使用的是最新版本 / I am using the latest version
|
||||
required: false
|
||||
- label: 我愿意提交 PR 修复此问题 / I am willing to submit a PR to fix this issue
|
||||
required: false
|
||||
17
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
17
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 📚 文档 / Documentation
|
||||
url: https://esengine.github.io/ecs-framework/
|
||||
about: 查看完整文档和教程 / View full documentation and tutorials
|
||||
|
||||
- name: 🤖 AI 文档助手 / AI Documentation Assistant
|
||||
url: https://deepwiki.com/esengine/esengine
|
||||
about: 使用 AI 助手快速找到答案 / Use AI assistant to quickly find answers
|
||||
|
||||
- name: 💬 QQ 交流群 / QQ Group
|
||||
url: https://jq.qq.com/?_wv=1027&k=29w1Nud6
|
||||
about: 加入社区交流群 / Join the community group
|
||||
|
||||
- name: 🌟 GitHub Discussions
|
||||
url: https://github.com/esengine/esengine/discussions
|
||||
about: 参与社区讨论 / Join community discussions
|
||||
90
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
90
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
name: ✨ Feature Request / 功能建议
|
||||
description: Suggest a new feature or enhancement / 建议新功能或改进
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢你的功能建议!请详细描述你的想法。
|
||||
Thanks for your feature suggestion! Please describe your idea in detail.
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 问题描述 / Problem Description
|
||||
description: 这个功能解决什么问题?/ What problem does this feature solve?
|
||||
placeholder: 当我需要...的时候,现在很不方便,因为... / When I need to..., it's inconvenient because...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: 建议的解决方案 / Proposed Solution
|
||||
description: 你希望如何实现这个功能?/ How would you like this feature to work?
|
||||
placeholder: 可以添加一个新的 API,例如... / Could add a new API, for example...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: 其他方案 / Alternatives
|
||||
description: 你考虑过哪些替代方案?/ What alternatives have you considered?
|
||||
placeholder: 也可以通过...来实现,但是... / Could also achieve this by..., but...
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: examples
|
||||
attributes:
|
||||
label: 使用示例 / Usage Example
|
||||
description: 展示这个功能如何使用 / Show how this feature would be used
|
||||
render: typescript
|
||||
placeholder: |
|
||||
// 理想的 API 设计 / Ideal API design
|
||||
const pool = new ComponentPool(MyComponent, { size: 100 });
|
||||
const component = pool.acquire();
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: scope
|
||||
attributes:
|
||||
label: 影响范围 / Scope
|
||||
description: 这个功能主要影响哪个部分?/ Which part does this feature mainly affect?
|
||||
options:
|
||||
- Core / 核心框架
|
||||
- Performance / 性能
|
||||
- API Design / API 设计
|
||||
- Developer Experience / 开发体验
|
||||
- Documentation / 文档
|
||||
- Editor / 编辑器
|
||||
- Other / 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: 优先级 / Priority
|
||||
description: 你认为这个功能有多重要?/ How important do you think this feature is?
|
||||
options:
|
||||
- High / 高 - 非常需要这个功能
|
||||
- Medium / 中 - 有会更好
|
||||
- Low / 低 - 可有可无
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: 检查清单 / Checklist
|
||||
options:
|
||||
- label: 我已经搜索过类似的功能请求 / I have searched for similar feature requests
|
||||
required: true
|
||||
- label: 这个功能不会破坏现有 API / This feature won't break existing APIs
|
||||
required: false
|
||||
- label: 我愿意提交 PR 实现此功能 / I am willing to submit a PR to implement this feature
|
||||
required: false
|
||||
64
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
64
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: ❓ Question / 问题咨询
|
||||
description: Ask a question about using the framework / 询问框架使用问题
|
||||
title: "[Question]: "
|
||||
labels: ["question"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
💡 提示:如果是简单问题,可以先查看:
|
||||
- [📚 文档](https://esengine.github.io/ecs-framework/)
|
||||
- [📖 AI 文档助手](https://deepwiki.com/esengine/esengine)
|
||||
- [💬 QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
|
||||
|
||||
💡 Tip: For simple questions, please check first:
|
||||
- [📚 Documentation](https://esengine.github.io/ecs-framework/)
|
||||
- [📖 AI Documentation](https://deepwiki.com/esengine/esengine)
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: 你的问题 / Your Question
|
||||
description: 清晰描述你的问题 / Describe your question clearly
|
||||
placeholder: 如何在 Cocos Creator 中使用 ECS Framework?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: 背景信息 / Context
|
||||
description: 提供更多上下文帮助理解问题 / Provide more context to help understand
|
||||
placeholder: |
|
||||
我正在开发一个多人在线游戏...
|
||||
我尝试过...但是...
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: code
|
||||
attributes:
|
||||
label: 相关代码 / Related Code
|
||||
description: 如果适用,提供相关代码片段 / If applicable, provide relevant code snippet
|
||||
render: typescript
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: 版本 / Version
|
||||
description: 使用的框架版本 / Framework version you're using
|
||||
placeholder: 例如 / e.g., 2.2.8
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: 检查清单 / Checklist
|
||||
options:
|
||||
- label: 我已经查看过文档 / I have checked the documentation
|
||||
required: true
|
||||
- label: 我已经搜索过类似问题 / I have searched for similar questions
|
||||
required: true
|
||||
13
.github/codeql/codeql-config.yml
vendored
Normal file
13
.github/codeql/codeql-config.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: "CodeQL Config"
|
||||
|
||||
# Paths to exclude from analysis
|
||||
paths-ignore:
|
||||
- thirdparty
|
||||
- "**/node_modules"
|
||||
- "**/dist"
|
||||
- "**/bin"
|
||||
- "**/tests"
|
||||
- "**/*.test.ts"
|
||||
- "**/*.spec.ts"
|
||||
- "**/test"
|
||||
- "**/__tests__"
|
||||
95
.github/labels.yml
vendored
Normal file
95
.github/labels.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
# GitHub Labels 配置
|
||||
# 可以使用 https://github.com/Financial-Times/github-label-sync 来同步标签
|
||||
|
||||
# Size Labels (PR 大小)
|
||||
- name: 'size/XS'
|
||||
color: '00ff00'
|
||||
description: '极小的改动 (< 10 行)'
|
||||
|
||||
- name: 'size/S'
|
||||
color: '00ff00'
|
||||
description: '小改动 (10-100 行)'
|
||||
|
||||
- name: 'size/M'
|
||||
color: 'ffff00'
|
||||
description: '中等改动 (100-500 行)'
|
||||
|
||||
- name: 'size/L'
|
||||
color: 'ff9900'
|
||||
description: '大改动 (500-1000 行)'
|
||||
|
||||
- name: 'size/XL'
|
||||
color: 'ff0000'
|
||||
description: '超大改动 (> 1000 行)'
|
||||
|
||||
# Type Labels
|
||||
- name: 'bug'
|
||||
color: 'd73a4a'
|
||||
description: '错误或问题'
|
||||
|
||||
- name: 'enhancement'
|
||||
color: 'a2eeef'
|
||||
description: '新功能或改进'
|
||||
|
||||
- name: 'documentation'
|
||||
color: '0075ca'
|
||||
description: '文档相关'
|
||||
|
||||
- name: 'question'
|
||||
color: 'd876e3'
|
||||
description: '问题咨询'
|
||||
|
||||
- name: 'performance'
|
||||
color: 'ff6b6b'
|
||||
description: '性能相关'
|
||||
|
||||
# Scope Labels
|
||||
- name: 'core'
|
||||
color: '5319e7'
|
||||
description: '核心包相关'
|
||||
|
||||
- name: 'editor'
|
||||
color: '5319e7'
|
||||
description: '编辑器相关'
|
||||
|
||||
- name: 'network'
|
||||
color: '5319e7'
|
||||
description: '网络相关'
|
||||
|
||||
# Status Labels
|
||||
- name: 'stale'
|
||||
color: 'ededed'
|
||||
description: '长时间无活动'
|
||||
|
||||
- name: 'wip'
|
||||
color: 'fbca04'
|
||||
description: '进行中'
|
||||
|
||||
- name: 'help wanted'
|
||||
color: '008672'
|
||||
description: '需要帮助'
|
||||
|
||||
- name: 'good first issue'
|
||||
color: '7057ff'
|
||||
description: '适合新手'
|
||||
|
||||
- name: 'quick-review'
|
||||
color: '00ff00'
|
||||
description: '小改动,快速 Review'
|
||||
|
||||
- name: 'automerge'
|
||||
color: 'bfdadc'
|
||||
description: '自动合并'
|
||||
|
||||
- name: 'pinned'
|
||||
color: 'c2e0c6'
|
||||
description: '置顶,不会被标记为 stale'
|
||||
|
||||
- name: 'security'
|
||||
color: 'ee0701'
|
||||
description: '安全相关,高优先级'
|
||||
|
||||
# Dependencies
|
||||
- name: 'dependencies'
|
||||
color: '0366d6'
|
||||
description: '依赖更新'
|
||||
113
.github/workflows/ci.yml
vendored
Normal file
113
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, main, develop ]
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
- 'jest.config.*'
|
||||
- '.github/workflows/ci.yml'
|
||||
pull_request:
|
||||
branches: [ master, main, develop ]
|
||||
# Run on all PRs to satisfy branch protection, but skip build if no code changes
|
||||
|
||||
jobs:
|
||||
# Check if we need to run the full CI
|
||||
check-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should-run: ${{ steps.filter.outputs.code }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
code:
|
||||
- 'packages/**'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
- 'jest.config.*'
|
||||
|
||||
ci:
|
||||
needs: check-changes
|
||||
if: needs.check-changes.outputs.should-run == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
# 构建 framework 包 (可独立发布的通用库,无外部依赖)
|
||||
- name: Build framework packages
|
||||
run: |
|
||||
pnpm --filter @esengine/ecs-framework build
|
||||
pnpm --filter @esengine/ecs-framework-math build
|
||||
pnpm --filter @esengine/behavior-tree build
|
||||
pnpm --filter @esengine/blueprint build
|
||||
pnpm --filter @esengine/fsm build
|
||||
pnpm --filter @esengine/timer build
|
||||
pnpm --filter @esengine/spatial build
|
||||
pnpm --filter @esengine/procgen build
|
||||
pnpm --filter @esengine/pathfinding build
|
||||
pnpm --filter @esengine/network-protocols build
|
||||
pnpm --filter @esengine/rpc build
|
||||
pnpm --filter @esengine/network build
|
||||
|
||||
# 类型检查 (仅 framework 包)
|
||||
- name: Type check (framework packages)
|
||||
run: pnpm run type-check:framework
|
||||
|
||||
# Lint 检查 (仅 framework 包)
|
||||
- name: Lint check (framework packages)
|
||||
run: pnpm run lint:framework
|
||||
|
||||
# 测试 (仅 framework 包)
|
||||
- name: Run tests with coverage
|
||||
run: pnpm run test:ci:framework
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: false
|
||||
|
||||
# 构建 npm 包 (core 和 math)
|
||||
- name: Build npm packages
|
||||
run: |
|
||||
pnpm run build:npm:core
|
||||
pnpm run build:npm:math
|
||||
|
||||
# 上传构建产物
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: |
|
||||
packages/framework/**/dist/
|
||||
packages/framework/**/bin/
|
||||
retention-days: 7
|
||||
49
.github/workflows/codecov.yml
vendored
Normal file
49
.github/workflows/codecov.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Code Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, main ]
|
||||
pull_request:
|
||||
branches: [ master, main ]
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: |
|
||||
cd packages/framework/core
|
||||
pnpm run test:coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/framework/core/coverage/coverage-final.json
|
||||
flags: core
|
||||
name: core-coverage
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: packages/framework/core/coverage/
|
||||
42
.github/workflows/codeql.yml
vendored
Normal file
42
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "main" ]
|
||||
pull_request:
|
||||
branches: [ "master", "main" ]
|
||||
schedule:
|
||||
- cron: '0 0 * * 1' # 每周一运行
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-and-quality
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
34
.github/workflows/commitlint.yml
vendored
Normal file
34
.github/workflows/commitlint.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Commit Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install commitlint
|
||||
run: |
|
||||
pnpm add -D @commitlint/config-conventional @commitlint/cli
|
||||
|
||||
- name: Validate PR commits
|
||||
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
|
||||
70
.github/workflows/docs.yml
vendored
Normal file
70
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
name: Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'packages/**'
|
||||
- 'typedoc.json'
|
||||
- 'package.json'
|
||||
- '.github/workflows/docs.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build core package
|
||||
run: pnpm run build:core
|
||||
|
||||
- name: Generate API documentation
|
||||
run: pnpm run docs:api
|
||||
|
||||
- name: Build documentation
|
||||
run: pnpm run docs:build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs/dist
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
34
.github/workflows/node.js.yml
vendored
34
.github/workflows/node.js.yml
vendored
@@ -1,34 +0,0 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [8.x]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: cmd
|
||||
working-directory: ./source
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
- run: npm run build --if-present
|
||||
- run: gulp build
|
||||
71
.github/workflows/release-changesets.yml
vendored
Normal file
71
.github/workflows/release-changesets.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Release (Changesets)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.changeset/**'
|
||||
- 'packages/*/package.json'
|
||||
- 'packages/*/*/package.json'
|
||||
- 'packages/*/*/*/package.json'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build framework packages
|
||||
run: |
|
||||
# Only build packages managed by Changesets (not in ignore list)
|
||||
pnpm --filter "@esengine/ecs-framework" build
|
||||
pnpm --filter "@esengine/ecs-framework-math" build
|
||||
pnpm --filter "@esengine/behavior-tree" build
|
||||
pnpm --filter "@esengine/blueprint" build
|
||||
pnpm --filter "@esengine/fsm" build
|
||||
pnpm --filter "@esengine/timer" build
|
||||
pnpm --filter "@esengine/spatial" build
|
||||
pnpm --filter "@esengine/procgen" build
|
||||
pnpm --filter "@esengine/pathfinding" build
|
||||
pnpm --filter "@esengine/network-protocols" build
|
||||
pnpm --filter "@esengine/rpc" build
|
||||
pnpm --filter "@esengine/network" build
|
||||
pnpm --filter "@esengine/cli" build
|
||||
|
||||
- name: Create Release Pull Request or Publish
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
version: pnpm changeset:version
|
||||
publish: pnpm changeset:publish
|
||||
title: 'chore: release packages'
|
||||
commit: 'chore: release packages'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
253
.github/workflows/release-editor.yml
vendored
Normal file
253
.github/workflows/release-editor.yml
vendored
Normal file
@@ -0,0 +1,253 @@
|
||||
name: Release Editor App
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'editor-v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version (e.g., 1.0.0)'
|
||||
required: true
|
||||
default: '1.0.0'
|
||||
|
||||
jobs:
|
||||
build-tauri:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
arch: x64
|
||||
- platform: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
arch: x64
|
||||
- platform: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
arch: arm64
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: packages/editor/editor-app/src-tauri
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install dependencies (Ubuntu)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Update version in config files (for manual trigger)
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
cd packages/editor/editor-app
|
||||
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||
node scripts/sync-version.js
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: cargo install wasm-pack
|
||||
|
||||
# 使用 Turborepo 自动按依赖顺序构建所有包
|
||||
# 这会自动处理:core -> asset-system -> editor-core -> ui -> 等等
|
||||
- name: Build all packages with Turborepo
|
||||
run: pnpm run build
|
||||
|
||||
- name: Copy WASM files to ecs-engine-bindgen
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p packages/engine/ecs-engine-bindgen/src/wasm
|
||||
cp packages/rust/engine/pkg/es_engine.js packages/engine/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/rust/engine/pkg/es_engine.d.ts packages/engine/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/rust/engine/pkg/es_engine_bg.wasm packages/engine/ecs-engine-bindgen/src/wasm/
|
||||
cp packages/rust/engine/pkg/es_engine_bg.wasm.d.ts packages/engine/ecs-engine-bindgen/src/wasm/
|
||||
|
||||
- name: Bundle runtime files for Tauri
|
||||
run: |
|
||||
cd packages/editor/editor-app
|
||||
node scripts/bundle-runtime.mjs
|
||||
|
||||
- name: Build Tauri app
|
||||
id: tauri
|
||||
uses: tauri-apps/tauri-action@v0.5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
with:
|
||||
projectPath: packages/editor/editor-app
|
||||
tagName: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
|
||||
releaseName: 'ECS Editor v${{ github.event.inputs.version || github.ref_name }}'
|
||||
releaseBody: 'See the assets to download this version and install.'
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
includeUpdaterJson: true
|
||||
updaterJsonKeepUniversal: false
|
||||
args: ${{ matrix.platform == 'macos-latest' && format('--target {0}', matrix.target) || '' }}
|
||||
|
||||
# Windows 构建上传 artifact 供 SignPath 签名
|
||||
- name: Upload Windows artifacts for signing
|
||||
if: matrix.platform == 'windows-latest'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-unsigned
|
||||
path: |
|
||||
packages/editor/editor-app/src-tauri/target/release/bundle/nsis/*.exe
|
||||
packages/editor/editor-app/src-tauri/target/release/bundle/msi/*.msi
|
||||
retention-days: 1
|
||||
|
||||
# SignPath 代码签名(Windows)
|
||||
# SignPath OSS code signing for Windows
|
||||
#
|
||||
# 配置步骤 | Setup Steps:
|
||||
# 1. 在 SignPath 门户创建项目 | Create project in SignPath portal
|
||||
# 2. 导入 .signpath/artifact-configuration.xml | Import artifact configuration
|
||||
# 3. 使用 'test-signing' 策略测试 | Use 'test-signing' policy for testing
|
||||
# 生产环境改为 'release-signing' | Change to 'release-signing' for production
|
||||
# 4. 配置 GitHub Secrets | Configure GitHub Secrets:
|
||||
# - SIGNPATH_API_TOKEN: API token from SignPath
|
||||
# - SIGNPATH_ORGANIZATION_ID: Your organization ID
|
||||
#
|
||||
# 文档 | Documentation: https://about.signpath.io/documentation/trusted-build-systems/github
|
||||
sign-windows:
|
||||
needs: build-tauri
|
||||
runs-on: ubuntu-latest
|
||||
# 只有在构建成功时才运行 | Only run on successful build
|
||||
if: success()
|
||||
|
||||
steps:
|
||||
- name: Check SignPath configuration
|
||||
id: check-signpath
|
||||
run: |
|
||||
if [ -n "${{ secrets.SIGNPATH_API_TOKEN }}" ] && [ -n "${{ secrets.SIGNPATH_ORGANIZATION_ID }}" ]; then
|
||||
echo "enabled=true" >> $GITHUB_OUTPUT
|
||||
echo "SignPath is configured, proceeding with code signing"
|
||||
else
|
||||
echo "enabled=false" >> $GITHUB_OUTPUT
|
||||
echo "SignPath secrets not configured, skipping code signing"
|
||||
echo "To enable: add SIGNPATH_API_TOKEN and SIGNPATH_ORGANIZATION_ID secrets"
|
||||
fi
|
||||
|
||||
- name: Checkout
|
||||
if: steps.check-signpath.outputs.enabled == 'true'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get artifact ID
|
||||
if: steps.check-signpath.outputs.enabled == 'true'
|
||||
id: get-artifact
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# 获取 windows-unsigned artifact 的 ID
|
||||
ARTIFACT_ID=$(gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
|
||||
--jq '.artifacts[] | select(.name == "windows-unsigned") | .id')
|
||||
|
||||
if [ -z "$ARTIFACT_ID" ]; then
|
||||
echo "Error: Could not find artifact 'windows-unsigned'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "artifact-id=$ARTIFACT_ID" >> $GITHUB_OUTPUT
|
||||
echo "Found artifact ID: $ARTIFACT_ID"
|
||||
|
||||
- name: Submit to SignPath for code signing
|
||||
if: steps.check-signpath.outputs.enabled == 'true'
|
||||
id: signpath
|
||||
uses: signpath/github-action-submit-signing-request@v1
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: 'ecs-framework'
|
||||
signing-policy-slug: 'test-signing'
|
||||
artifact-configuration-slug: 'initial'
|
||||
github-artifact-id: ${{ steps.get-artifact.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
wait-for-completion-timeout-in-seconds: 600
|
||||
output-artifact-directory: './signed'
|
||||
|
||||
- name: Upload signed artifacts to release
|
||||
if: steps.check-signpath.outputs.enabled == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ./signed/*
|
||||
tag_name: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
|
||||
# 保持 Draft 状态,需要手动发布 | Keep as draft, require manual publish
|
||||
draft: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# 构建成功后,创建 PR 更新版本号
|
||||
# Create PR to update version after successful build
|
||||
update-version-pr:
|
||||
needs: [build-tauri, sign-windows]
|
||||
# 即使签名跳过也要运行 | Run even if signing is skipped
|
||||
if: github.event_name == 'workflow_dispatch' && !failure()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Update version files
|
||||
run: |
|
||||
cd packages/editor/editor-app
|
||||
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||
node scripts/sync-version.js
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "chore(editor): bump version to ${{ github.event.inputs.version }}"
|
||||
branch: release/editor-v${{ github.event.inputs.version }}
|
||||
delete-branch: true
|
||||
title: "chore(editor): Release v${{ github.event.inputs.version }}"
|
||||
body: |
|
||||
## Release v${{ github.event.inputs.version }}
|
||||
|
||||
This PR updates the editor version after successful release build.
|
||||
|
||||
### Changes
|
||||
- Updated `packages/editor/editor-app/package.json` → `${{ github.event.inputs.version }}`
|
||||
- Updated `packages/editor/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}`
|
||||
|
||||
### Release
|
||||
- [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/editor-v${{ github.event.inputs.version }})
|
||||
|
||||
---
|
||||
*This PR was automatically created by the release workflow.*
|
||||
labels: |
|
||||
release
|
||||
editor
|
||||
automated pr
|
||||
225
.github/workflows/release.yml
vendored
Normal file
225
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,225 @@
|
||||
name: Release NPM Packages
|
||||
|
||||
on:
|
||||
# Tag trigger: supports v* and {package}-v* formats
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- 'core-v*'
|
||||
- 'behavior-tree-v*'
|
||||
- 'editor-core-v*'
|
||||
- 'node-editor-v*'
|
||||
- 'blueprint-v*'
|
||||
- 'tilemap-v*'
|
||||
- 'physics-rapier2d-v*'
|
||||
- 'worker-generator-v*'
|
||||
|
||||
# Manual trigger option
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: 'Select package to publish'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- core
|
||||
- behavior-tree
|
||||
- editor-core
|
||||
- node-editor
|
||||
- blueprint
|
||||
- tilemap
|
||||
- physics-rapier2d
|
||||
- worker-generator
|
||||
version_type:
|
||||
description: 'Version bump type'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
release-package:
|
||||
name: Release Package
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Parse tag or input
|
||||
id: parse
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
# Parse package and version from tag
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Parse format: v1.0.0 or package-v1.0.0
|
||||
if [[ "$TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then
|
||||
PACKAGE="core"
|
||||
VERSION="${BASH_REMATCH[1]}"
|
||||
elif [[ "$TAG" =~ ^([a-z-]+)-v([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then
|
||||
PACKAGE="${BASH_REMATCH[1]}"
|
||||
VERSION="${BASH_REMATCH[2]}"
|
||||
else
|
||||
echo "::error::Invalid tag format: $TAG"
|
||||
echo "Expected: v1.0.0 or package-v1.0.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "mode=tag" >> $GITHUB_OUTPUT
|
||||
echo "Package: $PACKAGE"
|
||||
echo "Version: $VERSION"
|
||||
else
|
||||
# Manual trigger: read from package.json and bump version
|
||||
PACKAGE="${{ github.event.inputs.package }}"
|
||||
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
|
||||
echo "mode=manual" >> $GITHUB_OUTPUT
|
||||
echo "version_type=${{ github.event.inputs.version_type }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Verify version (tag mode)
|
||||
if: steps.parse.outputs.mode == 'tag'
|
||||
run: |
|
||||
PACKAGE="${{ steps.parse.outputs.package }}"
|
||||
EXPECTED_VERSION="${{ steps.parse.outputs.version }}"
|
||||
|
||||
# Get version from package.json
|
||||
ACTUAL_VERSION=$(node -p "require('./packages/$PACKAGE/package.json').version")
|
||||
|
||||
if [ "$EXPECTED_VERSION" != "$ACTUAL_VERSION" ]; then
|
||||
echo "::error::Version mismatch!"
|
||||
echo "Tag version: $EXPECTED_VERSION"
|
||||
echo "package.json version: $ACTUAL_VERSION"
|
||||
echo ""
|
||||
echo "Please update packages/$PACKAGE/package.json to version $EXPECTED_VERSION before tagging."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Version verified: $EXPECTED_VERSION"
|
||||
|
||||
- name: Bump version (manual mode)
|
||||
if: steps.parse.outputs.mode == 'manual'
|
||||
id: bump
|
||||
run: |
|
||||
PACKAGE="${{ steps.parse.outputs.package }}"
|
||||
cd packages/$PACKAGE
|
||||
|
||||
CURRENT=$(node -p "require('./package.json').version")
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
||||
|
||||
case "${{ steps.parse.outputs.version_type }}" in
|
||||
major) NEW_VERSION="$((MAJOR+1)).0.0" ;;
|
||||
minor) NEW_VERSION="$MAJOR.$((MINOR+1)).0" ;;
|
||||
patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH+1))" ;;
|
||||
esac
|
||||
|
||||
# Update package.json
|
||||
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||
|
||||
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Bumped version: $CURRENT -> $NEW_VERSION"
|
||||
|
||||
- name: Set final version
|
||||
id: version
|
||||
run: |
|
||||
if [ "${{ steps.parse.outputs.mode }}" = "tag" ]; then
|
||||
echo "value=${{ steps.parse.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "value=${{ steps.bump.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build core package (if needed)
|
||||
if: ${{ steps.parse.outputs.package != 'core' && steps.parse.outputs.package != 'node-editor' && steps.parse.outputs.package != 'worker-generator' }}
|
||||
run: |
|
||||
cd packages/framework/core
|
||||
pnpm run build
|
||||
|
||||
- name: Build node-editor package (if needed for blueprint)
|
||||
if: ${{ steps.parse.outputs.package == 'blueprint' }}
|
||||
run: |
|
||||
cd packages/node-editor
|
||||
pnpm run build
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
cd packages/${{ steps.parse.outputs.package }}
|
||||
pnpm run build:npm
|
||||
|
||||
- name: Publish to npm
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
cd packages/${{ steps.parse.outputs.package }}/dist
|
||||
pnpm publish --access public --no-git-checks
|
||||
|
||||
- name: Create GitHub Release (tag mode)
|
||||
if: steps.parse.outputs.mode == 'tag'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.parse.outputs.tag }}
|
||||
name: "${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}"
|
||||
make_latest: false
|
||||
body: |
|
||||
## @esengine/${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}
|
||||
|
||||
**NPM**: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
|
||||
|
||||
```bash
|
||||
npm install @esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}
|
||||
```
|
||||
|
||||
---
|
||||
*Auto-released by GitHub Actions*
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create Pull Request (manual mode)
|
||||
if: steps.parse.outputs.mode == 'manual'
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "chore(${{ steps.parse.outputs.package }}): release v${{ steps.version.outputs.value }}"
|
||||
branch: release/${{ steps.parse.outputs.package }}-v${{ steps.version.outputs.value }}
|
||||
delete-branch: true
|
||||
title: "chore(${{ steps.parse.outputs.package }}): Release v${{ steps.version.outputs.value }}"
|
||||
body: |
|
||||
## Release v${{ steps.version.outputs.value }}
|
||||
|
||||
This PR updates `@esengine/${{ steps.parse.outputs.package }}` package version.
|
||||
|
||||
### Changes
|
||||
- Published to npm: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
|
||||
- Updated `packages/${{ steps.parse.outputs.package }}/package.json` to `${{ steps.version.outputs.value }}`
|
||||
|
||||
---
|
||||
*This PR was automatically created by the release workflow*
|
||||
labels: |
|
||||
release
|
||||
${{ steps.parse.outputs.package }}
|
||||
automated pr
|
||||
60
.github/workflows/stale.yml
vendored
Normal file
60
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Stale Issues and PRs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # 每天运行一次
|
||||
workflow_dispatch: # 允许手动触发
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Stale Bot
|
||||
uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Issue 配置
|
||||
stale-issue-message: |
|
||||
这个 issue 已经 60 天没有活动了,将在 14 天后自动关闭。
|
||||
如果这个问题仍然存在,请留言说明情况。
|
||||
|
||||
This issue has been inactive for 60 days and will be closed in 14 days.
|
||||
If this issue is still relevant, please leave a comment.
|
||||
close-issue-message: |
|
||||
由于长时间无活动,这个 issue 已被自动关闭。
|
||||
如需重新打开,请留言说明。
|
||||
|
||||
This issue has been automatically closed due to inactivity.
|
||||
Please comment if you'd like to reopen it.
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-close: 14
|
||||
stale-issue-label: 'stale'
|
||||
exempt-issue-labels: 'pinned,security,enhancement,help wanted'
|
||||
|
||||
# PR 配置
|
||||
stale-pr-message: |
|
||||
这个 PR 已经 30 天没有活动了,将在 7 天后自动关闭。
|
||||
如果你还在处理这个 PR,请更新一下。
|
||||
|
||||
This PR has been inactive for 30 days and will be closed in 7 days.
|
||||
If you're still working on it, please update it.
|
||||
close-pr-message: |
|
||||
由于长时间无活动,这个 PR 已被自动关闭。
|
||||
如需继续,请重新打开或创建新的 PR。
|
||||
|
||||
This PR has been automatically closed due to inactivity.
|
||||
Please reopen or create a new PR to continue.
|
||||
days-before-pr-stale: 30
|
||||
days-before-pr-close: 7
|
||||
stale-pr-label: 'stale'
|
||||
exempt-pr-labels: 'pinned,security,wip'
|
||||
|
||||
# 其他配置
|
||||
operations-per-run: 100
|
||||
remove-stale-when-updated: true
|
||||
ascending: true
|
||||
106
.gitignore
vendored
106
.gitignore
vendored
@@ -1,10 +1,96 @@
|
||||
/source/node_modules
|
||||
/demo/bin-debug
|
||||
/demo/bin-release
|
||||
/.idea
|
||||
/.vscode
|
||||
/demo_wxgame
|
||||
/demo/.wing
|
||||
/demo/.idea
|
||||
/demo/.vscode
|
||||
/source/docs
|
||||
# 依赖目录
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# 构建输出
|
||||
bin/
|
||||
dist/
|
||||
*.tgz
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
.build-cache/
|
||||
|
||||
# Turborepo
|
||||
.turbo/
|
||||
|
||||
# IDE 配置
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# 操作系统文件
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# 日志文件
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# 环境配置
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# 代码签名证书(敏感文件)
|
||||
certs/
|
||||
*.pfx
|
||||
*.p12
|
||||
*.cer
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# 测试覆盖率
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# 包管理器锁文件(忽略yarn,保留pnpm)
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
||||
# 文档生成
|
||||
docs/api/
|
||||
docs/build/
|
||||
docs/.vitepress/cache/
|
||||
docs/.vitepress/dist/
|
||||
|
||||
# 备份文件
|
||||
*.bak
|
||||
*.backup
|
||||
|
||||
# 演示项目构建产物
|
||||
/demo/bin-debug/
|
||||
/demo/bin-release/
|
||||
/demo/.wing/
|
||||
/demo/.idea/
|
||||
/demo/.vscode/
|
||||
/demo_wxgame/
|
||||
|
||||
# Tauri 构建产物
|
||||
**/src-tauri/target/
|
||||
**/src-tauri/WixTools/
|
||||
**/src-tauri/gen/
|
||||
|
||||
# Tauri 捆绑输出
|
||||
**/src-tauri/target/release/bundle/
|
||||
**/src-tauri/target/debug/bundle/
|
||||
|
||||
# Rust 构建产物
|
||||
**/engine-shared/target/
|
||||
external/
|
||||
|
||||
33
.gitmodules
vendored
33
.gitmodules
vendored
@@ -1,15 +1,18 @@
|
||||
[submodule "demo/egret_demo"]
|
||||
path = demo/egret_demo
|
||||
url = https://github.com/esengine/ecs-egret-demo
|
||||
[submodule "demo/laya_demo"]
|
||||
path = demo/laya_demo
|
||||
url = https://github.com/esengine/ecs-laya-demo.git
|
||||
[submodule "extensions/ecs-star"]
|
||||
path = extensions/ecs-star
|
||||
url = https://github.com/esengine/ecs-astar
|
||||
[submodule "extensions/behaviourTree-ai"]
|
||||
path = extensions/behaviourTree-ai
|
||||
url = https://github.com/esengine/BehaviourTree-ai
|
||||
[submodule "extensions/ecs-tween"]
|
||||
path = extensions/ecs-tween
|
||||
url = https://github.com/esengine/ecs-tween
|
||||
[submodule "thirdparty/BehaviourTree-ai"]
|
||||
path = thirdparty/BehaviourTree-ai
|
||||
url = https://github.com/esengine/BehaviourTree-ai.git
|
||||
[submodule "thirdparty/admin-backend"]
|
||||
path = thirdparty/admin-backend
|
||||
url = https://github.com/esengine/admin-backend.git
|
||||
[submodule "thirdparty/mvvm-ui-framework"]
|
||||
path = thirdparty/mvvm-ui-framework
|
||||
url = https://github.com/esengine/mvvm-ui-framework.git
|
||||
[submodule "thirdparty/cocos-nexus"]
|
||||
path = thirdparty/cocos-nexus
|
||||
url = https://github.com/esengine/cocos-nexus.git
|
||||
[submodule "thirdparty/ecs-astar"]
|
||||
path = thirdparty/ecs-astar
|
||||
url = https://github.com/esengine/ecs-astar.git
|
||||
[submodule "examples/lawn-mower-demo"]
|
||||
path = examples/lawn-mower-demo
|
||||
url = https://github.com/esengine/lawn-mower-demo.git
|
||||
40
.npmignore
Normal file
40
.npmignore
Normal file
@@ -0,0 +1,40 @@
|
||||
# 源代码文件
|
||||
src/
|
||||
tsconfig*.json
|
||||
*.ts
|
||||
!bin/**/*.d.ts
|
||||
|
||||
# 开发文件
|
||||
dev-bin/
|
||||
scripts/
|
||||
.vscode/
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# 测试文件
|
||||
**/*.test.*
|
||||
**/*.spec.*
|
||||
**/test/
|
||||
**/tests/
|
||||
|
||||
# 构建缓存
|
||||
node_modules/
|
||||
*.log
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# 文档草稿
|
||||
docs/draft/
|
||||
*.draft.md
|
||||
|
||||
# 编辑器文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# 环境文件
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
2
.npmrc
Normal file
2
.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
link-workspace-packages=true
|
||||
prefer-workspace-packages=true
|
||||
49
.prettierignore
Normal file
49
.prettierignore
Normal file
@@ -0,0 +1,49 @@
|
||||
# 依赖和构建输出
|
||||
node_modules/
|
||||
dist/
|
||||
bin/
|
||||
build/
|
||||
coverage/
|
||||
*.min.js
|
||||
*.min.css
|
||||
|
||||
# 编译输出
|
||||
**/*.d.ts
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# 日志
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# 第三方库
|
||||
thirdparty/
|
||||
examples/lawn-mower-demo/
|
||||
extensions/
|
||||
|
||||
# 文档生成
|
||||
docs/.vitepress/cache/
|
||||
docs/.vitepress/dist/
|
||||
docs/api/
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 编辑器
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# 其他
|
||||
*.backup
|
||||
CHANGELOG.md
|
||||
LICENSE
|
||||
README.md
|
||||
14
.prettierrc
Normal file
14
.prettierrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 120,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"bracketSpacing": true,
|
||||
"quoteProps": "as-needed",
|
||||
"jsxSingleQuote": false,
|
||||
"proseWrap": "preserve"
|
||||
}
|
||||
25
.size-limit.json
Normal file
25
.size-limit.json
Normal file
@@ -0,0 +1,25 @@
|
||||
[
|
||||
{
|
||||
"name": "@esengine/ecs-framework (ESM)",
|
||||
"path": "packages/core/dist/esm/index.js",
|
||||
"import": "*",
|
||||
"limit": "50 KB",
|
||||
"webpack": false,
|
||||
"gzip": true
|
||||
},
|
||||
{
|
||||
"name": "@esengine/ecs-framework (UMD)",
|
||||
"path": "packages/core/dist/umd/ecs-framework.js",
|
||||
"limit": "60 KB",
|
||||
"webpack": false,
|
||||
"gzip": true
|
||||
},
|
||||
{
|
||||
"name": "Core Runtime (Tree-shaking)",
|
||||
"path": "packages/core/dist/esm/index.js",
|
||||
"import": "{ Core, Scene, Entity, Component }",
|
||||
"limit": "30 KB",
|
||||
"webpack": false,
|
||||
"gzip": true
|
||||
}
|
||||
]
|
||||
130
CONTRIBUTING.md
Normal file
130
CONTRIBUTING.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# 贡献指南 / Contributing Guide
|
||||
|
||||
感谢你对 ECS Framework 的关注!
|
||||
|
||||
Thank you for your interest in contributing to ECS Framework!
|
||||
|
||||
## Commit 规范 / Commit Convention
|
||||
|
||||
本项目使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范。
|
||||
|
||||
This project follows the [Conventional Commits](https://www.conventionalcommits.org/) specification.
|
||||
|
||||
### 格式 / Format
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
### 类型 / Types
|
||||
|
||||
- **feat**: 新功能 / New feature
|
||||
- **fix**: 错误修复 / Bug fix
|
||||
- **docs**: 文档变更 / Documentation changes
|
||||
- **style**: 代码格式(不影响代码运行) / Code style changes
|
||||
- **refactor**: 重构(既不是新功能也不是修复) / Code refactoring
|
||||
- **perf**: 性能优化 / Performance improvements
|
||||
- **test**: 测试相关 / Test changes
|
||||
- **build**: 构建系统或依赖变更 / Build system changes
|
||||
- **ci**: CI 配置变更 / CI configuration changes
|
||||
- **chore**: 其他变更 / Other changes
|
||||
|
||||
### 范围 / Scope
|
||||
|
||||
- **core**: 核心包 @esengine/ecs-framework
|
||||
- **math**: 数学库包
|
||||
- **editor**: 编辑器
|
||||
- **docs**: 文档
|
||||
|
||||
### 示例 / Examples
|
||||
|
||||
```bash
|
||||
# 新功能
|
||||
feat(core): add component pooling system
|
||||
|
||||
# 错误修复
|
||||
fix(core): fix entity deletion memory leak
|
||||
|
||||
# 破坏性变更
|
||||
feat(core): redesign system lifecycle
|
||||
|
||||
BREAKING CHANGE: System.initialize() now requires Scene parameter
|
||||
```
|
||||
|
||||
## 自动发布 / Automatic Release
|
||||
|
||||
本项目使用 Semantic Release 自动发布。
|
||||
|
||||
This project uses Semantic Release for automatic publishing.
|
||||
|
||||
### 版本规则 / Versioning Rules
|
||||
|
||||
根据你的 commit 类型,版本号会自动更新:
|
||||
|
||||
Based on your commit type, the version will be automatically updated:
|
||||
|
||||
- `feat`: 增加 **minor** 版本 (0.x.0)
|
||||
- `fix`, `perf`, `refactor`: 增加 **patch** 版本 (0.0.x)
|
||||
- `BREAKING CHANGE`: 增加 **major** 版本 (x.0.0)
|
||||
|
||||
### 发布流程 / Release Process
|
||||
|
||||
1. 提交代码到 `master` 分支 / Push commits to `master` branch
|
||||
2. GitHub Actions 自动运行测试 / GitHub Actions runs tests automatically
|
||||
3. Semantic Release 分析 commits / Semantic Release analyzes commits
|
||||
4. 自动更新版本号 / Version is automatically updated
|
||||
5. 自动生成 CHANGELOG.md / CHANGELOG.md is automatically generated
|
||||
6. 自动发布到 npm / Package is automatically published to npm
|
||||
7. 自动创建 GitHub Release / GitHub Release is automatically created
|
||||
|
||||
## 开发流程 / Development Workflow
|
||||
|
||||
1. Fork 本仓库 / Fork this repository
|
||||
2. 创建特性分支 / Create a feature branch
|
||||
```bash
|
||||
git checkout -b feat/my-feature
|
||||
```
|
||||
3. 提交你的变更 / Commit your changes
|
||||
```bash
|
||||
git commit -m "feat(core): add new feature"
|
||||
```
|
||||
4. 推送到你的 Fork / Push to your fork
|
||||
```bash
|
||||
git push origin feat/my-feature
|
||||
```
|
||||
5. 创建 Pull Request / Create a Pull Request
|
||||
|
||||
## 本地测试 / Local Testing
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 运行测试
|
||||
npm test
|
||||
|
||||
# 构建
|
||||
npm run build
|
||||
|
||||
# 代码检查
|
||||
npm run lint
|
||||
|
||||
# 代码格式化
|
||||
npm run format
|
||||
```
|
||||
|
||||
## 问题反馈 / Issue Reporting
|
||||
|
||||
如果你发现了 bug 或有新功能建议,请[创建 Issue](https://github.com/esengine/esengine/issues/new)。
|
||||
|
||||
If you find a bug or have a feature request, please [create an issue](https://github.com/esengine/esengine/issues/new).
|
||||
|
||||
## 许可证 / License
|
||||
|
||||
通过贡献代码,你同意你的贡献将遵循 MIT 许可证。
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the MIT License.
|
||||
214
LICENSE
214
LICENSE
@@ -1,201 +1,21 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
MIT License
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Copyright (c) 2025 ESEngine Contributors
|
||||
|
||||
1. Definitions.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
435
README.md
435
README.md
@@ -1,231 +1,296 @@
|
||||
这是一套ecs游戏框架,里面包含ECS框架用于管理场景实体,一些常用2D碰撞检测及游戏中常用的工具
|
||||
<h1 align="center">
|
||||
<img src="https://raw.githubusercontent.com/esengine/esengine/master/docs/public/logo.svg" alt="ESEngine" width="180">
|
||||
<br>
|
||||
ESEngine
|
||||
</h1>
|
||||
|
||||
# 项目规划及讨论
|
||||
- [点击参与及查看](https://github.com/esengine/ecs-framework/discussions/36)
|
||||
<p align="center">
|
||||
<strong>Modular Game Framework for TypeScript</strong>
|
||||
</p>
|
||||
|
||||
## 交流群
|
||||
点击链接加入群聊【ecs游戏框架交流】:https://jq.qq.com/?_wv=1027&k=29w1Nud6
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/@esengine/ecs-framework"><img src="https://img.shields.io/npm/v/@esengine/ecs-framework?style=flat-square&color=blue" alt="npm"></a>
|
||||
<a href="https://github.com/esengine/esengine/actions"><img src="https://img.shields.io/github/actions/workflow/status/esengine/esengine/ci.yml?branch=master&style=flat-square" alt="build"></a>
|
||||
<a href="https://github.com/esengine/esengine/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license"></a>
|
||||
<a href="https://github.com/esengine/esengine/stargazers"><img src="https://img.shields.io/github/stars/esengine/esengine?style=flat-square" alt="stars"></a>
|
||||
<img src="https://img.shields.io/badge/TypeScript-5.0+-blue?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript">
|
||||
</p>
|
||||
|
||||
## 快速开始
|
||||
- 初始化框架
|
||||
```typescript
|
||||
es.Core.create();
|
||||
```
|
||||
- 派发帧事件
|
||||
```typescript
|
||||
es.Core.emitter.emit(es.CoreEvents.frameUpdated);
|
||||
```
|
||||
完成以上对框架的初始化,您还需要一个默认场景并激活它,新建一个场景类并继承`es.Scene`
|
||||
<p align="center">
|
||||
<b>English</b> | <a href="./README_CN.md">中文</a>
|
||||
</p>
|
||||
|
||||
```typescript
|
||||
class MainScene extends es.Scene { }
|
||||
<p align="center">
|
||||
<a href="https://esengine.cn/">Documentation</a> ·
|
||||
<a href="https://esengine.cn/api/README">API Reference</a> ·
|
||||
<a href="./examples/">Examples</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## What is ESEngine?
|
||||
|
||||
ESEngine is a collection of **engine-agnostic game development modules** for TypeScript. Use them with Cocos Creator, Laya, Phaser, PixiJS, or any JavaScript game engine.
|
||||
|
||||
The core is a high-performance **ECS (Entity-Component-System)** framework, accompanied by optional modules for AI, networking, physics, and more.
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
- 激活场景
|
||||
```typescript
|
||||
es.Core.scene = new MainScene();
|
||||
## Features
|
||||
|
||||
| Module | Description | Engine Required |
|
||||
|--------|-------------|:---------------:|
|
||||
| **ECS Core** | Entity-Component-System framework with reactive queries | No |
|
||||
| **Behavior Tree** | AI behavior trees with visual editor support | No |
|
||||
| **Blueprint** | Visual scripting system | No |
|
||||
| **FSM** | Finite state machine | No |
|
||||
| **Timer** | Timer and cooldown systems | No |
|
||||
| **Spatial** | Spatial indexing and queries (QuadTree, Grid) | No |
|
||||
| **Pathfinding** | A* and navigation mesh pathfinding | No |
|
||||
| **Network** | Client/server networking with TSRPC | No |
|
||||
|
||||
> All framework modules can be used standalone with any rendering engine.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using CLI (Recommended)
|
||||
|
||||
The easiest way to add ECS to your existing project:
|
||||
|
||||
```bash
|
||||
# In your project directory
|
||||
npx @esengine/cli init
|
||||
```
|
||||
|
||||
至此框架已完全开始运作,您可以使用框架内所有提供的方法。
|
||||
The CLI automatically detects your project type (Cocos Creator 2.x/3.x, LayaAir 3.x, or Node.js) and generates the necessary integration code.
|
||||
|
||||
## ECS框架的基本使用
|
||||
|
||||
#### 创建实体
|
||||
|
||||
创建实体一般由场景控制。您需要在场景内进行创建或添加实体(`createEntity` / `addEntity`)
|
||||
|
||||
> 注意:创建实体需要在场景`onStart`方法进行创建,而不是直接在`initialize`方法内创建。`initialize` 只是在构造函数完成后进行调用,此时该场景并未被激活。
|
||||
### Manual Setup
|
||||
|
||||
```typescript
|
||||
class MainScene extends es.Scene {
|
||||
onStart() {
|
||||
// 创建一个名为player的实体
|
||||
const player = this.createEntity("player");
|
||||
import {
|
||||
Core, Scene, Entity, Component, EntitySystem,
|
||||
Matcher, Time, ECSComponent, ECSSystem
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// Define components (data only)
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x = 0;
|
||||
y = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx = 0;
|
||||
dy = 0;
|
||||
}
|
||||
|
||||
// Define system (logic)
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(Position);
|
||||
const vel = entity.getComponent(Velocity);
|
||||
pos.x += vel.dx * Time.deltaTime;
|
||||
pos.y += vel.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
Core.create();
|
||||
const scene = new Scene();
|
||||
scene.addSystem(new MovementSystem());
|
||||
|
||||
const player = scene.createEntity('Player');
|
||||
player.addComponent(new Position());
|
||||
player.addComponent(new Velocity());
|
||||
|
||||
Core.setScene(scene);
|
||||
|
||||
// Integrate with your game loop
|
||||
function gameLoop(currentTime: number) {
|
||||
Core.update(currentTime / 1000);
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
requestAnimationFrame(gameLoop);
|
||||
```
|
||||
|
||||
## Using with Other Engines
|
||||
|
||||
ESEngine's framework modules are designed to work alongside your preferred rendering engine:
|
||||
|
||||
### With Cocos Creator
|
||||
|
||||
```typescript
|
||||
import { Component as CCComponent, _decorator } from 'cc';
|
||||
import { Core, Scene, Matcher, EntitySystem } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('GameManager')
|
||||
export class GameManager extends CCComponent {
|
||||
private ecsScene!: Scene;
|
||||
|
||||
start() {
|
||||
Core.create();
|
||||
this.ecsScene = new Scene();
|
||||
|
||||
// Add ECS systems
|
||||
this.ecsScene.addSystem(new BehaviorTreeExecutionSystem());
|
||||
this.ecsScene.addSystem(new MyGameSystem());
|
||||
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
Core.update(dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 添加组件
|
||||
|
||||
每个实体都会有多个组件,如果我们需要实体能够拥有一个新的能力,则需要给它添加相对应的组件,组件必须继承 `es.Component` 来标志为组件。
|
||||
|
||||
> 注意:组件一般没有具体逻辑,一般只存放一些属性,逻辑应该由系统进行调用
|
||||
### With Laya 3.x
|
||||
|
||||
```typescript
|
||||
class MovementComponent extends es.Component {
|
||||
// 定义该实体的移动速度
|
||||
public moveSpeed: number = 100;
|
||||
}
|
||||
```
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { FSMSystem } from '@esengine/fsm';
|
||||
|
||||
- 将组件添加至Player实体上(`addComponent`)
|
||||
const { regClass } = Laya;
|
||||
|
||||
```typescript
|
||||
class MainScene extends es.Scene {
|
||||
onStart() {
|
||||
// 创建一个名为player的实体,createEntity方法会将创建的实体返回给你
|
||||
const player = this.createEntity("player");
|
||||
// 为实体添加一个移动组件,addComponent会将新建的移动组件返回给你
|
||||
const moveComponent = player.addComponent(new MovementComponent())
|
||||
@regClass()
|
||||
export class ECSManager extends Laya.Script {
|
||||
private ecsScene = new Scene();
|
||||
|
||||
onAwake(): void {
|
||||
Core.create();
|
||||
this.ecsScene.addSystem(new FSMSystem());
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
|
||||
onUpdate(): void {
|
||||
Core.update(Laya.timer.delta / 1000);
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 添加系统
|
||||
## Packages
|
||||
|
||||
系统里会使用`Matcher`类帮助过滤它所感兴趣的实体并对其进行更新。我们一般定义系统会与对应的组件相匹配,刚我们创建了一个移动组件,则相对应的就会有一个移动系统来处理该类型的组件。
|
||||
### Framework (Engine-Agnostic)
|
||||
|
||||
这里我们使用`es.EntityProcessingSystem`,因为我们要对带有移动组件的实体进行更新。
|
||||
These packages have **zero rendering dependencies** and work with any engine:
|
||||
|
||||
> 系统也分多种类型,分为管理实体的系统(`es.EntityProcessingSystem`)/管理系统的系统(`es.ProcessingSystem`)
|
||||
|
||||
```typescript
|
||||
class MoveSystem extends es.EntityProcessingSystem {
|
||||
// 必须实现
|
||||
processEntity(entity: Entity) {
|
||||
// 这里会传入带有MoveComponent的实体
|
||||
// 为什么会传入只带有MoveComponent的实体?
|
||||
// 因为在构造函数中有一个Matcher匹配器,在你初始化MoveSystem的时候需要你传入匹配的对象. 见下方如何定义匹配带MoveComponent的匹配器
|
||||
|
||||
// 该方法每帧都会执行,请确保您的操作尽可能的小或者在使用大数据时采用缓存的方法进行获取操作
|
||||
const moveComponent = entity.getComponent(MovementComponent);
|
||||
if (!moveComponent.enabled)
|
||||
return;
|
||||
|
||||
// 根据moveComponent的数据执行移动的逻辑
|
||||
}
|
||||
}
|
||||
```bash
|
||||
npm install @esengine/ecs-framework # Core ECS
|
||||
npm install @esengine/behavior-tree # AI behavior trees
|
||||
npm install @esengine/blueprint # Visual scripting
|
||||
npm install @esengine/fsm # State machines
|
||||
npm install @esengine/timer # Timers & cooldowns
|
||||
npm install @esengine/spatial # Spatial indexing
|
||||
npm install @esengine/pathfinding # Pathfinding
|
||||
npm install @esengine/network # Networking
|
||||
```
|
||||
|
||||
##### 定义Matcher匹配器匹配所有带MoveComponent的实体
|
||||
### ESEngine Runtime (Optional)
|
||||
|
||||
```typescript
|
||||
// 这里你可以传入多个组件类型
|
||||
const matcher = Matcher.empty().all(MovementComponent);
|
||||
If you want a complete engine solution with rendering:
|
||||
|
||||
| Category | Packages |
|
||||
|----------|----------|
|
||||
| **Core** | `engine-core`, `asset-system`, `material-system` |
|
||||
| **Rendering** | `sprite`, `tilemap`, `particle`, `camera`, `mesh-3d` |
|
||||
| **Physics** | `physics-rapier2d` |
|
||||
| **Platform** | `platform-web`, `platform-wechat` |
|
||||
|
||||
### Editor (Optional)
|
||||
|
||||
A visual editor built with Tauri for scene management:
|
||||
|
||||
- Download from [Releases](https://github.com/esengine/esengine/releases)
|
||||
- Supports behavior tree editing, tilemap painting, visual scripting
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
esengine/
|
||||
├── packages/
|
||||
│ ├── framework/ # Engine-agnostic modules (NPM publishable)
|
||||
│ │ ├── core/ # ECS Framework
|
||||
│ │ ├── math/ # Math utilities
|
||||
│ │ ├── behavior-tree/ # AI behavior trees
|
||||
│ │ ├── blueprint/ # Visual scripting
|
||||
│ │ ├── fsm/ # Finite state machine
|
||||
│ │ ├── timer/ # Timer system
|
||||
│ │ ├── spatial/ # Spatial queries
|
||||
│ │ ├── pathfinding/ # Pathfinding
|
||||
│ │ ├── procgen/ # Procedural generation
|
||||
│ │ └── network/ # Networking
|
||||
│ │
|
||||
│ ├── engine/ # ESEngine runtime
|
||||
│ ├── rendering/ # Rendering modules
|
||||
│ ├── physics/ # Physics modules
|
||||
│ ├── editor/ # Visual editor
|
||||
│ └── rust/ # WASM renderer
|
||||
│
|
||||
├── docs/ # Documentation
|
||||
└── examples/ # Examples
|
||||
```
|
||||
|
||||
#### 激活系统
|
||||
## Building from Source
|
||||
|
||||
使用`addEntityProcessor`方法进行添加系统
|
||||
```bash
|
||||
git clone https://github.com/esengine/esengine.git
|
||||
cd esengine
|
||||
|
||||
```typescript
|
||||
class MainScene extends es.Scene {
|
||||
onStart() {
|
||||
const matcher = Matcher.empty().all(MovementComponent);
|
||||
// 将所有带MoveComponent的实体送到该系统处理
|
||||
this.addEntityProcessor(new MoveSystem(matcher));
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
// 创建一个名为player的实体,createEntity方法会将创建的实体返回给你
|
||||
const player = this.createEntity("player");
|
||||
// 为实体添加一个移动组件,addComponent会将新建的移动组件返回给你
|
||||
const moveComponent = player.addComponent(new MovementComponent());
|
||||
}
|
||||
}
|
||||
# Type check framework packages
|
||||
pnpm type-check:framework
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
```
|
||||
|
||||
至此所有的快速开始的学习到此为止,如果上述运行过程您已看懂,则框架的基本知识你已经掌握,框架内还含有部分内置组件和更高级的用法,您刚所学习的只是框架的冰山一角,如果您需要学习更多的话可以查看源码或查阅更多资料进行学习。
|
||||
## Documentation
|
||||
|
||||
## 关于 Physics/Collision
|
||||
框架中的物理不是一个真实的物理模拟。它只提供了游戏物理。您可以执行一些操作,如检测碰撞器、重叠检查、碰撞检查、扫描测试等。不是一个完整的刚体模拟。
|
||||
- [ECS Framework Guide](./packages/framework/core/README.md)
|
||||
- [Behavior Tree Guide](./packages/framework/behavior-tree/README.md)
|
||||
- [API Reference](https://esengine.cn/api/README)
|
||||
|
||||
### Colliders 物理系统的根本
|
||||
没有Collider,在物理系统中什么也不会发生。 碰撞器存在于实体类中,有几种类型:BoxCollider,CircleCollider和PolygonCollider。 您可以像这样添加Collider:`entity.addComponent(new BoxCollider())`. 将碰撞器添加到Entity时,它们会自动添加到SpatialHash中。
|
||||
## Community
|
||||
|
||||
### SpatialHash:你永远不会用到它,但它仍然对你很重要
|
||||
SpatialHash类是一个隐藏类,该类为您的游戏全局管理 `collider`。静态物理类是SpatialHash的公共包装器。 SpatialHash没有设置大小限制,用于快速进行碰撞/线投射/重叠检查。例如,如果你有一个英雄在世界各地移动,而不必检查每个对撞机(可能是数百个)是否发生碰撞,则只需向SpatialHash询问英雄附近的所有collider即可。这大大缩小了您的碰撞检查范围。
|
||||
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug reports and feature requests
|
||||
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - Questions and ideas
|
||||
- [Discord](https://discord.gg/gCAgzXFW) - Chat with the community
|
||||
|
||||
SpatialHash有一个可配置的方面,它可以极大地影响其性能:单元大小。 SpatialHash将空间分成一个网格,选择适当的网格大小可以将可能发生的碰撞查询减到最少。默认情况下,网格大小为100像素。您可以通过在创建场景之前设置`Physics.SpatialHashCellSize`来更改此设置。选择比您的平均玩家/敌人人数稍大的尺寸通常效果最佳。
|
||||
## Contributing
|
||||
|
||||
### Physics 类
|
||||
物理类是物理的核心类。 您可以设置一些属性,例如前面提到的spatialHashCellSize,raycastsHitTriggers和raycastsStartInColliders。
|
||||
- linecast:从开始到结束投射一条线,并返回与layerMask相匹配的碰撞器的第一次命中
|
||||
- overlapRectangle:检查是否有任何collider在矩形区域内
|
||||
- overlapCircle:检查是否有任何collider在圆形区域内
|
||||
- boxcastBroadphase:返回边界与collider.bounds相交的所有碰撞器。 请注意,这是一个broadphase检查,因此它只检查边界,不执行单个碰撞器到碰撞器的检查!
|
||||
Contributions are welcome! Please read our contributing guidelines before submitting a pull request.
|
||||
|
||||
会注意到上面提到的layerMask。 layerMask允许您确定与哪些碰撞器碰撞。 每个collider都可以设置其物理层,以便在查询物理系统时可以选择仅检索与传入的layerMask匹配的对撞机。 所有物理方法都接受默认为所有图层的图层蒙版参数。 使用此选项可以过滤冲突检查,并通过不执行不必要的冲突检查来使性能保持最佳状态。
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
### 使用物理系统
|
||||
射线检测对于检查敌人的视线、探测实体的空间环境、快速移动的子弹等各种事情都非常有用。下面是一个从头到尾投射线条的示例,如果击中某个物体,它只会记录数据:
|
||||
```ts
|
||||
const hit = Physics.linecast( start, end );
|
||||
if( hit.Collider != null )
|
||||
console.log( `ray hit ${hit}, entity: {hit.collider.entity}`);
|
||||
```
|
||||
## License
|
||||
|
||||
我们使用了一些更先进的碰撞/重叠检查方法,如Minkowski和、分离轴定理和古老的三角法。这些都被包装在Collider类上的简单易用的方法中。让我们看一些例子。
|
||||
ESEngine is licensed under the [MIT License](LICENSE). Free for personal and commercial use.
|
||||
|
||||
第一个例子是处理碰撞的最简单方法。deltaMovement是您希望移动实体的量,通常是velocity*Time.deltaTime. collidesWithAny方法将检查所有碰撞并调整deltaMovement以解决任何碰撞。
|
||||
|
||||
```ts
|
||||
// 碰撞结果将包含一些非常有用的信息,例如被撞的collider,表面命中的法线和最小平移矢量(MTV)。 MTV可用于将碰撞实体直接移动到命中的碰撞器附近。
|
||||
let collisionResult = null;
|
||||
|
||||
// 进行检查以查看entity.getComponent<Collider>(实体上的第一个碰撞器)是否与场景中的任何其他碰撞器发生碰撞。请注意,如果您有多个碰撞器,则可以获取并遍历它们,而不是仅检查第一个碰撞器。
|
||||
if( entity.getComponent<Collider>().collidesWithAny( deltaMovement, collisionResult ) )
|
||||
{
|
||||
// 记录CollisionResult。 您可能需要使用它来添加一些粒子效果或与您的游戏相关的任何其他内容。
|
||||
console.log( `collision result: ${collisionResult}` );
|
||||
}
|
||||
|
||||
// 将实体移到新位置。 已经调整了deltaMovement为我们解决冲突。
|
||||
entity.position = Vector2.add(entity.position, deltaMovement);
|
||||
```
|
||||
|
||||
如果您需要对碰撞发生时的情况进行更多控制,则也可以手动检查是否与其他collider发生碰撞。 请注意,执行此操作时,deltaMovement不会为您调整。 解决冲突时,您需要考虑最小平移矢量。
|
||||
|
||||
```ts
|
||||
let collisionResult = null;
|
||||
|
||||
// 进行检查以查看entity.getComponent<Collider>是否与一些其他Collider发生碰撞
|
||||
if( entity.getComponent<Collider>().collidesWith( someOtherCollider, deltaMovement, collisionResult ) )
|
||||
{
|
||||
// 将实体移动到与命中Collider相邻的位置,然后记录CollisionResult
|
||||
entity.position = Vector2.add(entity.position, Vector2.substract(deltaMovement, collisionResult.minimumTranslationVector));
|
||||
console.log( `collision result: ${collisionResult}` );
|
||||
}
|
||||
```
|
||||
我们可以使用前面提到的Physics.boxcastBroadphase方法或更具体地讲,将自身排除在查询之外的版本,使上述示例更进一步。 该方法将为我们提供场景中所有在我们附近的collider,然后我们可以使用这些对撞机进行实际的碰撞检查。
|
||||
|
||||
```ts
|
||||
// 在我们自身以外的位置获取可能与之重叠的任何东西
|
||||
let neighborColliders = Physics.boxcastBroadphaseExcludingSelf( entity.getComponent<Collider>() );
|
||||
|
||||
// 遍历并检查每个对撞机是否重叠
|
||||
for( let collider of neighborColliders )
|
||||
{
|
||||
if( entity.getComponent<Collider>().overlaps( collider ) )
|
||||
console.log( `我们正在重叠一个collider : ${collider}` );
|
||||
}
|
||||
```
|
||||
|
||||
### 示例地址
|
||||
|
||||
#### [laya-demo](https://github.com/esengine/ecs-laya-demo)
|
||||
#### [egret-demo](https://github.com/esengine/ecs-egret-demo)
|
||||
|
||||
## 扩展库
|
||||
|
||||
#### [基于ecs-framework开发的astar/BreadthFirst/Dijkstra/GOAP目标导向计划 路径寻找库](https://github.com/esengine/ecs-astar)
|
||||
#### [基于ecs-framework开发的AI(BehaviourTree、UtilityAI)系统](https://github.com/esengine/BehaviourTree-ai))
|
||||
|
||||
## 关于用ecs框架(typescript/javascript)
|
||||
ecs 是功能强大的实体组件系统。typescript与其他语言不同,因此我对ecs的设计尽可能的支持typescript特性。虽然ecs拥有标准实体组件系统,但在细节上有很大不同。创建标准ecs通常处于原始速度、缓存位置和其他性能原因。使用typescript,我们没有struct,因为没有必要匹配标准实体组件系统的设计方式,因为我们对内存布局没有那种控制。
|
||||
|
||||
ecs更灵活,可以更好的集中、组织、排序和过滤游戏中的对象。ecs让您拥有轻量级实体和组件,这些组件可以由系统批量处理。
|
||||
例如,您在制作一个射手,您可能会有几十到几百个子弹,这些作为轻量级实体由系统批量处理。
|
||||
|
||||
所以ecs在设计当中拥有四种重要类型:世界(Scene),过滤器(Matcher),系统(System)和实体(Entity)
|
||||
|
||||
## [世界(Scene)](https://github.com/esengine/egret-framework/wiki/%E5%9C%BA%E6%99%AF-Scene)
|
||||
Scene是ecs包含系统和实体最外面的容器。
|
||||
|
||||
## 实体(Entity)
|
||||
实体只由系统处理。
|
||||
|
||||
## 组件(Component)
|
||||
组件应该只包含数据而没有逻辑代码。对数据进行逻辑是系统的工作。
|
||||
|
||||
## 系统(System)
|
||||
ecs中的系统会不断的更新实体。系统使用过滤器选择某些实体,然后仅更新那些选择的实体。
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
Made with care by the ESEngine community
|
||||
</p>
|
||||
|
||||
297
README_CN.md
Normal file
297
README_CN.md
Normal file
@@ -0,0 +1,297 @@
|
||||
<h1 align="center">
|
||||
<img src="https://raw.githubusercontent.com/esengine/esengine/master/docs/public/logo.svg" alt="ESEngine" width="180">
|
||||
<br>
|
||||
ESEngine
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>TypeScript 模块化游戏框架</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/@esengine/ecs-framework"><img src="https://img.shields.io/npm/v/@esengine/ecs-framework?style=flat-square&color=blue" alt="npm"></a>
|
||||
<a href="https://github.com/esengine/esengine/actions"><img src="https://img.shields.io/github/actions/workflow/status/esengine/esengine/ci.yml?branch=master&style=flat-square" alt="build"></a>
|
||||
<a href="https://github.com/esengine/esengine/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license"></a>
|
||||
<a href="https://github.com/esengine/esengine/stargazers"><img src="https://img.shields.io/github/stars/esengine/esengine?style=flat-square" alt="stars"></a>
|
||||
<img src="https://img.shields.io/badge/TypeScript-5.0+-blue?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md">English</a> | <b>中文</b>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://esengine.cn/">文档</a> ·
|
||||
<a href="https://esengine.cn/api/README">API 参考</a> ·
|
||||
<a href="./examples/">示例</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## ESEngine 是什么?
|
||||
|
||||
ESEngine 是一套**引擎无关的游戏开发模块**,可与 Cocos Creator、Laya、Phaser、PixiJS 等任何 JavaScript 游戏引擎配合使用。
|
||||
|
||||
核心是一个高性能的 **ECS(实体-组件-系统)** 框架,配套 AI、网络、物理等可选模块。
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## 功能模块
|
||||
|
||||
| 模块 | 描述 | 需要渲染引擎 |
|
||||
|------|------|:----------:|
|
||||
| **ECS 核心** | 实体-组件-系统框架,支持响应式查询 | 否 |
|
||||
| **行为树** | AI 行为树,支持可视化编辑 | 否 |
|
||||
| **蓝图** | 可视化脚本系统 | 否 |
|
||||
| **状态机** | 有限状态机 | 否 |
|
||||
| **定时器** | 定时器和冷却系统 | 否 |
|
||||
| **空间索引** | 空间查询(四叉树、网格) | 否 |
|
||||
| **寻路** | A* 和导航网格寻路 | 否 |
|
||||
| **网络** | 客户端/服务端网络通信 (TSRPC) | 否 |
|
||||
|
||||
> 所有框架模块都可以独立使用,无需依赖特定渲染引擎。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 使用 CLI(推荐)
|
||||
|
||||
在现有项目中添加 ECS 的最简单方式:
|
||||
|
||||
```bash
|
||||
# 在项目目录中运行
|
||||
npx @esengine/cli init
|
||||
```
|
||||
|
||||
CLI 会自动检测项目类型(Cocos Creator 2.x/3.x、LayaAir 3.x 或 Node.js)并生成相应的集成代码。
|
||||
|
||||
### 手动配置
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Core, Scene, Entity, Component, EntitySystem,
|
||||
Matcher, Time, ECSComponent, ECSSystem
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件(纯数据)
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x = 0;
|
||||
y = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx = 0;
|
||||
dy = 0;
|
||||
}
|
||||
|
||||
// 定义系统(逻辑)
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(Position);
|
||||
const vel = entity.getComponent(Velocity);
|
||||
pos.x += vel.dx * Time.deltaTime;
|
||||
pos.y += vel.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
Core.create();
|
||||
const scene = new Scene();
|
||||
scene.addSystem(new MovementSystem());
|
||||
|
||||
const player = scene.createEntity('Player');
|
||||
player.addComponent(new Position());
|
||||
player.addComponent(new Velocity());
|
||||
|
||||
Core.setScene(scene);
|
||||
|
||||
// 集成到你的游戏循环
|
||||
function gameLoop(currentTime: number) {
|
||||
Core.update(currentTime / 1000);
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
requestAnimationFrame(gameLoop);
|
||||
```
|
||||
|
||||
## 与其他引擎配合使用
|
||||
|
||||
ESEngine 的框架模块设计为可与你喜欢的渲染引擎配合使用:
|
||||
|
||||
### 与 Cocos Creator 配合
|
||||
|
||||
```typescript
|
||||
import { Component as CCComponent, _decorator } from 'cc';
|
||||
import { Core, Scene, Matcher, EntitySystem } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('GameManager')
|
||||
export class GameManager extends CCComponent {
|
||||
private ecsScene!: Scene;
|
||||
|
||||
start() {
|
||||
Core.create();
|
||||
this.ecsScene = new Scene();
|
||||
|
||||
// 添加 ECS 系统
|
||||
this.ecsScene.addSystem(new BehaviorTreeExecutionSystem());
|
||||
this.ecsScene.addSystem(new MyGameSystem());
|
||||
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
Core.update(dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 与 Laya 3.x 配合
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { FSMSystem } from '@esengine/fsm';
|
||||
|
||||
const { regClass } = Laya;
|
||||
|
||||
@regClass()
|
||||
export class ECSManager extends Laya.Script {
|
||||
private ecsScene = new Scene();
|
||||
|
||||
onAwake(): void {
|
||||
Core.create();
|
||||
this.ecsScene.addSystem(new FSMSystem());
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
|
||||
onUpdate(): void {
|
||||
Core.update(Laya.timer.delta / 1000);
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 包列表
|
||||
|
||||
### 框架包(引擎无关)
|
||||
|
||||
这些包**零渲染依赖**,可与任何引擎配合使用:
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework # ECS 核心
|
||||
npm install @esengine/behavior-tree # AI 行为树
|
||||
npm install @esengine/blueprint # 可视化脚本
|
||||
npm install @esengine/fsm # 状态机
|
||||
npm install @esengine/timer # 定时器和冷却
|
||||
npm install @esengine/spatial # 空间索引
|
||||
npm install @esengine/pathfinding # 寻路
|
||||
npm install @esengine/network # 网络
|
||||
```
|
||||
|
||||
### ESEngine 运行时(可选)
|
||||
|
||||
如果你需要完整的引擎解决方案:
|
||||
|
||||
| 分类 | 包名 |
|
||||
|------|------|
|
||||
| **核心** | `engine-core`, `asset-system`, `material-system` |
|
||||
| **渲染** | `sprite`, `tilemap`, `particle`, `camera`, `mesh-3d` |
|
||||
| **物理** | `physics-rapier2d` |
|
||||
| **平台** | `platform-web`, `platform-wechat` |
|
||||
|
||||
### 编辑器(可选)
|
||||
|
||||
基于 Tauri 构建的可视化编辑器:
|
||||
|
||||
- 从 [Releases](https://github.com/esengine/esengine/releases) 下载
|
||||
- 支持行为树编辑、Tilemap 绘制、可视化脚本
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
esengine/
|
||||
├── packages/
|
||||
│ ├── framework/ # 引擎无关模块(可发布到 NPM)
|
||||
│ │ ├── core/ # ECS 框架
|
||||
│ │ ├── math/ # 数学工具
|
||||
│ │ ├── behavior-tree/ # AI 行为树
|
||||
│ │ ├── blueprint/ # 可视化脚本
|
||||
│ │ ├── fsm/ # 有限状态机
|
||||
│ │ ├── timer/ # 定时器系统
|
||||
│ │ ├── spatial/ # 空间查询
|
||||
│ │ ├── pathfinding/ # 寻路
|
||||
│ │ ├── procgen/ # 程序化生成
|
||||
│ │ └── network/ # 网络
|
||||
│ │
|
||||
│ ├── engine/ # ESEngine 运行时
|
||||
│ ├── rendering/ # 渲染模块
|
||||
│ ├── physics/ # 物理模块
|
||||
│ ├── editor/ # 可视化编辑器
|
||||
│ └── rust/ # WASM 渲染器
|
||||
│
|
||||
├── docs/ # 文档
|
||||
└── examples/ # 示例
|
||||
```
|
||||
|
||||
## 从源码构建
|
||||
|
||||
```bash
|
||||
git clone https://github.com/esengine/esengine.git
|
||||
cd esengine
|
||||
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
# 框架包类型检查
|
||||
pnpm type-check:framework
|
||||
|
||||
# 运行测试
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## 文档
|
||||
|
||||
- [ECS 框架指南](./packages/framework/core/README.md)
|
||||
- [行为树指南](./packages/framework/behavior-tree/README.md)
|
||||
- [API 参考](https://esengine.cn/api/README)
|
||||
|
||||
## 社区
|
||||
|
||||
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
|
||||
- [Discord](https://discord.gg/gCAgzXFW) - 国际社区
|
||||
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug 反馈和功能建议
|
||||
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - 问题和想法
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎贡献代码!提交 PR 前请阅读贡献指南。
|
||||
|
||||
1. Fork 仓库
|
||||
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
|
||||
3. 提交修改 (`git commit -m 'Add amazing feature'`)
|
||||
4. 推送分支 (`git push origin feature/amazing-feature`)
|
||||
5. 发起 Pull Request
|
||||
|
||||
## 许可证
|
||||
|
||||
ESEngine 基于 [MIT 协议](LICENSE) 开源,个人和商业使用均免费。
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
由 ESEngine 社区用心打造
|
||||
</p>
|
||||
114
SECURITY.md
114
SECURITY.md
@@ -1,21 +1,113 @@
|
||||
# Security Policy
|
||||
# Security Policy / 安全政策
|
||||
|
||||
**English** | [中文](#安全政策-1)
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
We provide security updates for the following versions:
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 5.1.x | :white_check_mark: |
|
||||
| 5.0.x | :x: |
|
||||
| 4.0.x | :white_check_mark: |
|
||||
| < 4.0 | :x: |
|
||||
| 2.x.x | :white_check_mark: |
|
||||
| 1.x.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use this section to tell people how to report a vulnerability.
|
||||
If you discover a security vulnerability, please report it through the following channels:
|
||||
|
||||
Tell them where to go, how often they can expect to get an update on a
|
||||
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||
declined, etc.
|
||||
### Reporting Channels
|
||||
|
||||
- **GitHub Security Advisories**: [Report a vulnerability](https://github.com/esengine/esengine/security/advisories/new) (Recommended)
|
||||
- **Email**: security@esengine.dev
|
||||
|
||||
### Reporting Guidelines
|
||||
|
||||
1. **Do NOT** report security vulnerabilities in public issues
|
||||
2. Provide a detailed description of the vulnerability, including:
|
||||
- Affected versions
|
||||
- Steps to reproduce
|
||||
- Potential impact
|
||||
- Suggested fix (if available)
|
||||
|
||||
### Response Timeline
|
||||
|
||||
- **Acknowledgment**: Within 72 hours
|
||||
- **Initial Assessment**: Within 1 week
|
||||
- **Fix Release**: Typically within 2-4 weeks, depending on severity
|
||||
|
||||
### Process
|
||||
|
||||
1. We will confirm the existence and severity of the vulnerability
|
||||
2. Develop and test a fix
|
||||
3. Release a security update
|
||||
4. Publicly disclose the vulnerability details after the fix is released
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
When using ESEngine, please follow these security recommendations:
|
||||
|
||||
- Always use the latest stable version
|
||||
- Regularly update dependencies
|
||||
- Disable debug mode in production
|
||||
- Validate all external input data
|
||||
- Do not store sensitive information on the client side
|
||||
|
||||
---
|
||||
|
||||
# 安全政策
|
||||
|
||||
[English](#security-policy--安全政策) | **中文**
|
||||
|
||||
## 支持的版本
|
||||
|
||||
我们为以下版本提供安全更新:
|
||||
|
||||
| 版本 | 支持状态 |
|
||||
| ------- | ------------------ |
|
||||
| 2.x.x | :white_check_mark: |
|
||||
| 1.x.x | :x: |
|
||||
|
||||
## 报告漏洞
|
||||
|
||||
如果您发现了安全漏洞,请通过以下方式报告:
|
||||
|
||||
### 报告渠道
|
||||
|
||||
- **GitHub 安全公告**: [报告漏洞](https://github.com/esengine/esengine/security/advisories/new)(推荐)
|
||||
- **邮箱**: security@esengine.dev
|
||||
|
||||
### 报告指南
|
||||
|
||||
1. **不要**在公开的 issue 中报告安全漏洞
|
||||
2. 提供详细的漏洞描述,包括:
|
||||
- 受影响的版本
|
||||
- 复现步骤
|
||||
- 潜在的影响范围
|
||||
- 如果可能,提供修复建议
|
||||
|
||||
### 响应时间
|
||||
|
||||
- **确认收到**: 72小时内
|
||||
- **初步评估**: 1周内
|
||||
- **修复发布**: 根据严重程度,通常在2-4周内
|
||||
|
||||
### 处理流程
|
||||
|
||||
1. 我们会确认漏洞的存在和严重程度
|
||||
2. 开发修复方案并进行测试
|
||||
3. 发布安全更新
|
||||
4. 在修复发布后,会在相关渠道公布漏洞详情
|
||||
|
||||
## 安全最佳实践
|
||||
|
||||
使用 ESEngine 时,请遵循以下安全建议:
|
||||
|
||||
- 始终使用最新的稳定版本
|
||||
- 定期更新依赖项
|
||||
- 在生产环境中禁用调试模式
|
||||
- 验证所有外部输入数据
|
||||
- 不要在客户端存储敏感信息
|
||||
|
||||
感谢您帮助保持 ESEngine 的安全性!
|
||||
|
||||
Thank you for helping keep ESEngine secure!
|
||||
|
||||
53
codecov.yml
Normal file
53
codecov.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
# Codecov 配置文件
|
||||
# https://docs.codecov.com/docs/codecov-yaml
|
||||
|
||||
coverage:
|
||||
status:
|
||||
# 项目整体覆盖率要求
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 1%
|
||||
base: auto
|
||||
|
||||
# 补丁覆盖率要求(针对 PR 中的新代码)
|
||||
patch:
|
||||
default:
|
||||
target: 50% # 降低补丁覆盖率要求到 50%
|
||||
threshold: 5%
|
||||
base: auto
|
||||
|
||||
# 精确度设置
|
||||
precision: 2
|
||||
round: down
|
||||
range: "70...100"
|
||||
|
||||
# 注释设置
|
||||
comment:
|
||||
layout: "reach,diff,flags,tree,files"
|
||||
behavior: default
|
||||
require_changes: false
|
||||
require_base: false
|
||||
require_head: true
|
||||
|
||||
# 忽略的文件/目录
|
||||
ignore:
|
||||
- "tests/**/*"
|
||||
- "**/*.test.ts"
|
||||
- "**/*.spec.ts"
|
||||
- "**/test/**/*"
|
||||
- "**/tests/**/*"
|
||||
- "bin/**/*"
|
||||
- "dist/**/*"
|
||||
- "node_modules/**/*"
|
||||
|
||||
# 标志组
|
||||
flags:
|
||||
core:
|
||||
paths:
|
||||
- packages/core/src/
|
||||
carryforward: true
|
||||
|
||||
# GitHub Checks 配置
|
||||
github_checks:
|
||||
annotations: true
|
||||
Submodule demo/egret_demo deleted from 7fd9460053
Submodule demo/laya_demo deleted from e4273ff0f5
21
docs/.gitignore
vendored
Normal file
21
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
49
docs/README.md
Normal file
49
docs/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Starlight Starter Kit: Basics
|
||||
|
||||
[](https://starlight.astro.build)
|
||||
|
||||
```
|
||||
npm create astro@latest -- --template starlight
|
||||
```
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
||||
|
||||
```
|
||||
.
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── assets/
|
||||
│ ├── content/
|
||||
│ │ └── docs/
|
||||
│ └── content.config.ts
|
||||
├── astro.config.mjs
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
|
||||
|
||||
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
|
||||
|
||||
Static assets, like favicons, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
|
||||
313
docs/astro.config.mjs
Normal file
313
docs/astro.config.mjs
Normal file
@@ -0,0 +1,313 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
import vue from '@astrojs/vue';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'ESEngine',
|
||||
logo: {
|
||||
src: './src/assets/logo.svg',
|
||||
replacesTitle: false,
|
||||
},
|
||||
social: [
|
||||
{ icon: 'github', label: 'GitHub', href: 'https://github.com/esengine/esengine' }
|
||||
],
|
||||
defaultLocale: 'root',
|
||||
locales: {
|
||||
root: {
|
||||
label: '简体中文',
|
||||
lang: 'zh-CN',
|
||||
},
|
||||
en: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
},
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
label: '快速开始',
|
||||
translations: { en: 'Getting Started' },
|
||||
items: [
|
||||
{ label: '快速入门', slug: 'guide/getting-started', translations: { en: 'Quick Start' } },
|
||||
{ label: '指南概览', slug: 'guide', translations: { en: 'Guide Overview' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '核心概念',
|
||||
translations: { en: 'Core Concepts' },
|
||||
items: [
|
||||
{
|
||||
label: '实体',
|
||||
translations: { en: 'Entity' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/entity', translations: { en: 'Overview' } },
|
||||
{ label: '组件操作', slug: 'guide/entity/component-operations', translations: { en: 'Component Operations' } },
|
||||
{ label: '实体句柄', slug: 'guide/entity/entity-handle', translations: { en: 'Entity Handle' } },
|
||||
{ label: '生命周期', slug: 'guide/entity/lifecycle', translations: { en: 'Lifecycle' } },
|
||||
],
|
||||
},
|
||||
{ label: '层级结构', slug: 'guide/hierarchy', translations: { en: 'Hierarchy' } },
|
||||
{
|
||||
label: '组件',
|
||||
translations: { en: 'Component' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/component', translations: { en: 'Overview' } },
|
||||
{ label: '生命周期', slug: 'guide/component/lifecycle', translations: { en: 'Lifecycle' } },
|
||||
{ label: 'EntityRef 装饰器', slug: 'guide/component/entity-ref', translations: { en: 'EntityRef' } },
|
||||
{ label: '最佳实践', slug: 'guide/component/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '实体查询',
|
||||
translations: { en: 'Entity Query' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/entity-query', translations: { en: 'Overview' } },
|
||||
{ label: 'Matcher API', slug: 'guide/entity-query/matcher-api', translations: { en: 'Matcher API' } },
|
||||
{ label: '编译查询', slug: 'guide/entity-query/compiled-query', translations: { en: 'Compiled Query' } },
|
||||
{ label: '最佳实践', slug: 'guide/entity-query/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '系统',
|
||||
translations: { en: 'System' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/system', translations: { en: 'Overview' } },
|
||||
{ label: '系统类型', slug: 'guide/system/types', translations: { en: 'System Types' } },
|
||||
{ label: '生命周期', slug: 'guide/system/lifecycle', translations: { en: 'Lifecycle' } },
|
||||
{ label: '命令缓冲区', slug: 'guide/system/command-buffer', translations: { en: 'Command Buffer' } },
|
||||
{ label: '系统调度', slug: 'guide/system/scheduling', translations: { en: 'Scheduling' } },
|
||||
{ label: '变更检测', slug: 'guide/system/change-detection', translations: { en: 'Change Detection' } },
|
||||
{ label: '最佳实践', slug: 'guide/system/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '场景',
|
||||
translations: { en: 'Scene' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/scene', translations: { en: 'Overview' } },
|
||||
{ label: '生命周期', slug: 'guide/scene/lifecycle', translations: { en: 'Lifecycle' } },
|
||||
{ label: '实体管理', slug: 'guide/scene/entity-management', translations: { en: 'Entity Management' } },
|
||||
{ label: '系统管理', slug: 'guide/scene/system-management', translations: { en: 'System Management' } },
|
||||
{ label: '事件系统', slug: 'guide/scene/events', translations: { en: 'Events' } },
|
||||
{ label: '调试与监控', slug: 'guide/scene/debugging', translations: { en: 'Debugging' } },
|
||||
{ label: '最佳实践', slug: 'guide/scene/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '序列化',
|
||||
translations: { en: 'Serialization' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/serialization', translations: { en: 'Overview' } },
|
||||
{ label: '装饰器与继承', slug: 'guide/serialization/decorators', translations: { en: 'Decorators & Inheritance' } },
|
||||
{ label: '增量序列化', slug: 'guide/serialization/incremental', translations: { en: 'Incremental' } },
|
||||
{ label: '版本迁移', slug: 'guide/serialization/migration', translations: { en: 'Migration' } },
|
||||
{ label: '使用场景', slug: 'guide/serialization/use-cases', translations: { en: 'Use Cases' } },
|
||||
],
|
||||
},
|
||||
{ label: '事件系统', slug: 'guide/event-system', translations: { en: 'Event System' } },
|
||||
{ label: '时间与定时器', slug: 'guide/time-and-timers', translations: { en: 'Time & Timers' } },
|
||||
{ label: '日志系统', slug: 'guide/logging', translations: { en: 'Logging' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '高级功能',
|
||||
translations: { en: 'Advanced Features' },
|
||||
items: [
|
||||
{
|
||||
label: '服务容器',
|
||||
translations: { en: 'Service Container' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/service-container', translations: { en: 'Overview' } },
|
||||
{ label: '内置服务', slug: 'guide/service-container/built-in-services', translations: { en: 'Built-in Services' } },
|
||||
{ label: '依赖注入', slug: 'guide/service-container/dependency-injection', translations: { en: 'Dependency Injection' } },
|
||||
{ label: 'PluginServiceRegistry', slug: 'guide/service-container/plugin-service-registry', translations: { en: 'PluginServiceRegistry' } },
|
||||
{ label: '高级用法', slug: 'guide/service-container/advanced', translations: { en: 'Advanced' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '插件系统',
|
||||
translations: { en: 'Plugin System' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/plugin-system', translations: { en: 'Overview' } },
|
||||
{ label: '插件开发', slug: 'guide/plugin-system/development', translations: { en: 'Development' } },
|
||||
{ label: '服务与系统', slug: 'guide/plugin-system/services-systems', translations: { en: 'Services & Systems' } },
|
||||
{ label: '依赖管理', slug: 'guide/plugin-system/dependencies', translations: { en: 'Dependencies' } },
|
||||
{ label: '插件管理', slug: 'guide/plugin-system/management', translations: { en: 'Management' } },
|
||||
{ label: '示例插件', slug: 'guide/plugin-system/examples', translations: { en: 'Examples' } },
|
||||
{ label: '最佳实践', slug: 'guide/plugin-system/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Worker 系统',
|
||||
translations: { en: 'Worker System' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/worker-system', translations: { en: 'Overview' } },
|
||||
{ label: '配置选项', slug: 'guide/worker-system/configuration', translations: { en: 'Configuration' } },
|
||||
{ label: '完整示例', slug: 'guide/worker-system/examples', translations: { en: 'Examples' } },
|
||||
{ label: '微信小游戏', slug: 'guide/worker-system/wechat', translations: { en: 'WeChat' } },
|
||||
{ label: '最佳实践', slug: 'guide/worker-system/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '平台适配器',
|
||||
translations: { en: 'Platform Adapters' },
|
||||
items: [
|
||||
{ label: '概览', slug: 'guide/platform-adapter', translations: { en: 'Overview' } },
|
||||
{ label: '浏览器', slug: 'guide/platform-adapter/browser', translations: { en: 'Browser' } },
|
||||
{ label: '微信小游戏', slug: 'guide/platform-adapter/wechat-minigame', translations: { en: 'WeChat Mini Game' } },
|
||||
{ label: 'Node.js', slug: 'guide/platform-adapter/nodejs', translations: { en: 'Node.js' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '模块',
|
||||
translations: { en: 'Modules' },
|
||||
items: [
|
||||
{ label: '模块总览', slug: 'modules', translations: { en: 'Modules Overview' } },
|
||||
{
|
||||
label: '行为树',
|
||||
translations: { en: 'Behavior Tree' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/behavior-tree', translations: { en: 'Overview' } },
|
||||
{ label: '快速开始', slug: 'modules/behavior-tree/getting-started', translations: { en: 'Getting Started' } },
|
||||
{ label: '核心概念', slug: 'modules/behavior-tree/core-concepts', translations: { en: 'Core Concepts' } },
|
||||
{ label: '编辑器指南', slug: 'modules/behavior-tree/editor-guide', translations: { en: 'Editor Guide' } },
|
||||
{ label: '编辑器工作流', slug: 'modules/behavior-tree/editor-workflow', translations: { en: 'Editor Workflow' } },
|
||||
{ label: '资产管理', slug: 'modules/behavior-tree/asset-management', translations: { en: 'Asset Management' } },
|
||||
{ label: '自定义节点', slug: 'modules/behavior-tree/custom-actions', translations: { en: 'Custom Actions' } },
|
||||
{ label: '高级用法', slug: 'modules/behavior-tree/advanced-usage', translations: { en: 'Advanced Usage' } },
|
||||
{ label: '最佳实践', slug: 'modules/behavior-tree/best-practices', translations: { en: 'Best Practices' } },
|
||||
{ label: 'Cocos 集成', slug: 'modules/behavior-tree/cocos-integration', translations: { en: 'Cocos Integration' } },
|
||||
{ label: 'Laya 集成', slug: 'modules/behavior-tree/laya-integration', translations: { en: 'Laya Integration' } },
|
||||
{ label: 'Node.js 使用', slug: 'modules/behavior-tree/nodejs-usage', translations: { en: 'Node.js Usage' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '状态机',
|
||||
translations: { en: 'FSM' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/fsm', translations: { en: 'Overview' } },
|
||||
{ label: 'API 参考', slug: 'modules/fsm/api', translations: { en: 'API Reference' } },
|
||||
{ label: '实际示例', slug: 'modules/fsm/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '定时器',
|
||||
translations: { en: 'Timer' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/timer', translations: { en: 'Overview' } },
|
||||
{ label: 'API 参考', slug: 'modules/timer/api', translations: { en: 'API Reference' } },
|
||||
{ label: '实际示例', slug: 'modules/timer/examples', translations: { en: 'Examples' } },
|
||||
{ label: '最佳实践', slug: 'modules/timer/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '空间索引',
|
||||
translations: { en: 'Spatial' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/spatial', translations: { en: 'Overview' } },
|
||||
{ label: '空间索引 API', slug: 'modules/spatial/spatial-index', translations: { en: 'Spatial Index API' } },
|
||||
{ label: 'AOI 兴趣区域', slug: 'modules/spatial/aoi', translations: { en: 'AOI' } },
|
||||
{ label: '实际示例', slug: 'modules/spatial/examples', translations: { en: 'Examples' } },
|
||||
{ label: '工具与优化', slug: 'modules/spatial/utilities', translations: { en: 'Utilities' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '寻路',
|
||||
translations: { en: 'Pathfinding' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/pathfinding', translations: { en: 'Overview' } },
|
||||
{ label: '网格地图 API', slug: 'modules/pathfinding/grid-map', translations: { en: 'Grid Map API' } },
|
||||
{ label: '导航网格 API', slug: 'modules/pathfinding/navmesh', translations: { en: 'NavMesh API' } },
|
||||
{ label: '路径平滑', slug: 'modules/pathfinding/smoothing', translations: { en: 'Path Smoothing' } },
|
||||
{ label: '实际示例', slug: 'modules/pathfinding/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '蓝图',
|
||||
translations: { en: 'Blueprint' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/blueprint', translations: { en: 'Overview' } },
|
||||
{ label: '虚拟机 API', slug: 'modules/blueprint/vm', translations: { en: 'VM API' } },
|
||||
{ label: '自定义节点', slug: 'modules/blueprint/custom-nodes', translations: { en: 'Custom Nodes' } },
|
||||
{ label: '内置节点', slug: 'modules/blueprint/nodes', translations: { en: 'Built-in Nodes' } },
|
||||
{ label: '蓝图组合', slug: 'modules/blueprint/composition', translations: { en: 'Composition' } },
|
||||
{ label: '实际示例', slug: 'modules/blueprint/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '程序生成',
|
||||
translations: { en: 'Procgen' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/procgen', translations: { en: 'Overview' } },
|
||||
{ label: '噪声函数', slug: 'modules/procgen/noise', translations: { en: 'Noise Functions' } },
|
||||
{ label: '种子随机数', slug: 'modules/procgen/random', translations: { en: 'Seeded Random' } },
|
||||
{ label: '采样工具', slug: 'modules/procgen/sampling', translations: { en: 'Sampling' } },
|
||||
{ label: '实际示例', slug: 'modules/procgen/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'RPC 通信',
|
||||
translations: { en: 'RPC' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/rpc', translations: { en: 'Overview' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '网络同步',
|
||||
translations: { en: 'Network' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/network', translations: { en: 'Overview' } },
|
||||
{ label: '客户端', slug: 'modules/network/client', translations: { en: 'Client' } },
|
||||
{ label: '服务器', slug: 'modules/network/server', translations: { en: 'Server' } },
|
||||
{ label: '状态同步', slug: 'modules/network/sync', translations: { en: 'State Sync' } },
|
||||
{ label: 'API 参考', slug: 'modules/network/api', translations: { en: 'API Reference' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '示例',
|
||||
translations: { en: 'Examples' },
|
||||
items: [
|
||||
{ label: '示例总览', slug: 'examples', translations: { en: 'Examples Overview' } },
|
||||
{ label: 'Worker 系统演示', slug: 'examples/worker-system-demo', translations: { en: 'Worker System Demo' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'API 参考',
|
||||
translations: { en: 'API Reference' },
|
||||
autogenerate: { directory: 'api' },
|
||||
},
|
||||
{
|
||||
label: '更新日志',
|
||||
translations: { en: 'Changelog' },
|
||||
items: [
|
||||
{ label: '@esengine/ecs-framework', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/core/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/behavior-tree', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/behavior-tree/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/fsm', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/fsm/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/timer', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/timer/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/network', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/network/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/cli', link: 'https://github.com/esengine/esengine/blob/master/packages/tools/cli/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
customCss: ['./src/styles/custom.css'],
|
||||
head: [
|
||||
{ tag: 'meta', attrs: { name: 'theme-color', content: '#646cff' } },
|
||||
],
|
||||
components: {
|
||||
Head: './src/components/Head.astro',
|
||||
ThemeSelect: './src/components/ThemeSelect.astro',
|
||||
},
|
||||
}),
|
||||
vue(),
|
||||
],
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
});
|
||||
22
docs/package.json
Normal file
22
docs/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@esengine/docs",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.37.1",
|
||||
"@astrojs/vue": "^5.1.3",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"astro": "^5.6.1",
|
||||
"sharp": "^0.34.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vue": "^3.5.26"
|
||||
}
|
||||
}
|
||||
1
docs/public/CNAME
Normal file
1
docs/public/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
esengine.cn
|
||||
118
docs/public/coi-serviceworker.js
Normal file
118
docs/public/coi-serviceworker.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
|
||||
let coepCredentialless = false;
|
||||
if (typeof window === 'undefined') {
|
||||
self.addEventListener("install", () => self.skipWaiting());
|
||||
self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
|
||||
|
||||
self.addEventListener("message", (ev) => {
|
||||
if (!ev.data) {
|
||||
return;
|
||||
} else if (ev.data.type === "deregister") {
|
||||
self.registration
|
||||
.unregister()
|
||||
.then(() => {
|
||||
return self.clients.matchAll();
|
||||
})
|
||||
.then(clients => {
|
||||
clients.forEach((client) => client.navigate(client.url));
|
||||
});
|
||||
} else if (ev.data.type === "coepCredentialless") {
|
||||
coepCredentialless = ev.data.value;
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", function (event) {
|
||||
const r = event.request;
|
||||
if (r.cache === "only-if-cached" && r.mode !== "same-origin") {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = (coepCredentialless && r.mode === "no-cors")
|
||||
? new Request(r, {
|
||||
credentials: "omit",
|
||||
})
|
||||
: r;
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
if (response.status === 0) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const newHeaders = new Headers(response.headers);
|
||||
newHeaders.set("Cross-Origin-Embedder-Policy",
|
||||
coepCredentialless ? "credentialless" : "require-corp"
|
||||
);
|
||||
if (!coepCredentialless) {
|
||||
newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
|
||||
}
|
||||
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
})
|
||||
.catch((e) => console.error(e))
|
||||
);
|
||||
});
|
||||
|
||||
} else {
|
||||
(() => {
|
||||
// You can customize the behavior of this script through a global `coi` variable.
|
||||
const coi = {
|
||||
shouldRegister: () => true,
|
||||
shouldDeregister: () => false,
|
||||
coepCredentialless: () => !(window.chrome || window.netscape),
|
||||
doReload: () => window.location.reload(),
|
||||
quiet: false,
|
||||
...window.coi
|
||||
};
|
||||
|
||||
const n = navigator;
|
||||
|
||||
if (n.serviceWorker && n.serviceWorker.controller) {
|
||||
n.serviceWorker.controller.postMessage({
|
||||
type: "coepCredentialless",
|
||||
value: coi.coepCredentialless(),
|
||||
});
|
||||
|
||||
if (coi.shouldDeregister()) {
|
||||
n.serviceWorker.controller.postMessage({ type: "deregister" });
|
||||
}
|
||||
}
|
||||
|
||||
// If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are
|
||||
// already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here.
|
||||
if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return;
|
||||
|
||||
if (!window.isSecureContext) {
|
||||
!coi.quiet && console.log("COOP/COEP Service Worker not registered, a secure context is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
// In some environments (e.g. Chrome incognito mode) this won't be available
|
||||
if (n.serviceWorker) {
|
||||
n.serviceWorker.register(window.document.currentScript.src).then(
|
||||
(registration) => {
|
||||
!coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope);
|
||||
|
||||
registration.addEventListener("updatefound", () => {
|
||||
!coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker.");
|
||||
coi.doReload();
|
||||
});
|
||||
|
||||
// If the registration is active, but it's not controlling the page
|
||||
if (registration.active && !n.serviceWorker.controller) {
|
||||
!coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker.");
|
||||
coi.doReload();
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
!coi.quiet && console.error("COOP/COEP Service Worker failed to register:", err);
|
||||
}
|
||||
);
|
||||
}
|
||||
})();
|
||||
}
|
||||
12849
docs/public/demos/worker-system/assets/index-CrID--xK.js
Normal file
12849
docs/public/demos/worker-system/assets/index-CrID--xK.js
Normal file
File diff suppressed because it is too large
Load Diff
210
docs/public/demos/worker-system/index.html
Normal file
210
docs/public/demos/worker-system/index.html
Normal file
@@ -0,0 +1,210 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ECS Framework Worker System Demo</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.demo-area {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
border: 2px solid #4a9eff;
|
||||
background: #000;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 300px;
|
||||
background: #2a2a2a;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.control-group input, .control-group button {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #555;
|
||||
background: #3a3a3a;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.control-group button {
|
||||
background: #4a9eff;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.control-group button:hover {
|
||||
background: #3a8eef;
|
||||
}
|
||||
|
||||
.control-group button:disabled {
|
||||
background: #555;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.stats {
|
||||
background: #2a2a2a;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stats h3 {
|
||||
margin-top: 0;
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.stat-line {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.worker-enabled {
|
||||
color: #4eff4a;
|
||||
}
|
||||
|
||||
.worker-disabled {
|
||||
color: #ff4a4a;
|
||||
}
|
||||
|
||||
.performance-high {
|
||||
color: #4eff4a;
|
||||
}
|
||||
|
||||
.performance-medium {
|
||||
color: #ffff4a;
|
||||
}
|
||||
|
||||
.performance-low {
|
||||
color: #ff4a4a;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/ecs-framework/demos/worker-system/assets/index-CrID--xK.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>ECS Framework Worker System 演示</h1>
|
||||
|
||||
<div class="demo-area">
|
||||
<div class="canvas-container">
|
||||
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label>实体数量:</label>
|
||||
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100">
|
||||
<span id="entityCountValue">1000</span>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Worker 设置:</label>
|
||||
<button id="toggleWorker">禁用 Worker</button>
|
||||
<button id="toggleSAB">禁用 SharedArrayBuffer</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<button id="spawnParticles">生成粒子系统</button>
|
||||
<button id="clearEntities">清空所有实体</button>
|
||||
<button id="resetDemo">重置演示</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>物理参数:</label>
|
||||
<input type="range" id="gravity" min="0" max="500" value="100" step="10">
|
||||
<label>重力: <span id="gravityValue">100</span></label>
|
||||
|
||||
<input type="range" id="friction" min="0" max="100" value="95" step="5">
|
||||
<label>摩擦力: <span id="frictionValue">95%</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<h3>性能统计</h3>
|
||||
<div class="stat-line">FPS: <span id="fps">0</span></div>
|
||||
<div class="stat-line">实体数量: <span id="entityCountStat">0</span></div>
|
||||
<div class="stat-line">Worker状态: <span id="workerStatus" class="worker-disabled">未启用</span></div>
|
||||
<div class="stat-line">Worker负载: <span id="workerLoad">N/A</span></div>
|
||||
<div class="stat-line">运行模式: <span id="sabStatus" class="worker-disabled">同步模式</span></div>
|
||||
<div class="stat-line">物理系统耗时: <span id="physicsTime">0</span>ms</div>
|
||||
<div class="stat-line">渲染系统耗时: <span id="renderTime">0</span>ms</div>
|
||||
<div class="stat-line">总帧时间: <span id="frameTime">0</span>ms</div>
|
||||
<div class="stat-line">内存使用: <span id="memoryUsage">0</span>MB</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用 coi-serviceworker 启用 SharedArrayBuffer 支持 -->
|
||||
<script src="/ecs-framework/coi-serviceworker.js"></script>
|
||||
|
||||
<script>
|
||||
// Check SharedArrayBuffer support and display info
|
||||
function checkSharedArrayBufferSupport() {
|
||||
const hasSharedArrayBuffer = typeof SharedArrayBuffer !== 'undefined';
|
||||
const isCrossOriginIsolated = self.crossOriginIsolated || false;
|
||||
const isLocalhost = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
|
||||
const isGitHubPages = location.hostname === 'esengine.github.io';
|
||||
|
||||
console.log('=== SharedArrayBuffer 支持检测 ===');
|
||||
console.log('SharedArrayBuffer 存在:', hasSharedArrayBuffer);
|
||||
console.log('跨域隔离状态:', isCrossOriginIsolated);
|
||||
console.log('是否本地环境:', isLocalhost);
|
||||
console.log('是否 GitHub Pages:', isGitHubPages);
|
||||
|
||||
if (hasSharedArrayBuffer && isCrossOriginIsolated) {
|
||||
console.log('✅ SharedArrayBuffer 功能已启用!');
|
||||
console.log('系统将使用高性能的 SharedArrayBuffer 模式');
|
||||
} else if (isGitHubPages) {
|
||||
console.log('ℹ️ 如果页面刷新,可能是 coi-serviceworker 正在设置跨域隔离');
|
||||
console.log('刷新后 SharedArrayBuffer 应该可用');
|
||||
}
|
||||
|
||||
return hasSharedArrayBuffer && isCrossOriginIsolated;
|
||||
}
|
||||
|
||||
// Run check after page load
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(checkSharedArrayBufferSupport, 1000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
docs/public/favicon.svg
Normal file
1
docs/public/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill-rule="evenodd" d="M81 36 64 0 47 36l-1 2-9-10a6 6 0 0 0-9 9l10 10h-2L0 64l36 17h2L28 91a6 6 0 1 0 9 9l9-10 1 2 17 36 17-36v-2l9 10a6 6 0 1 0 9-9l-9-9 2-1 36-17-36-17-2-1 9-9a6 6 0 1 0-9-9l-9 10v-2Zm-17 2-2 5c-4 8-11 15-19 19l-5 2 5 2c8 4 15 11 19 19l2 5 2-5c4-8 11-15 19-19l5-2-5-2c-8-4-15-11-19-19l-2-5Z" clip-rule="evenodd"/><path d="M118 19a6 6 0 0 0-9-9l-3 3a6 6 0 1 0 9 9l3-3Zm-96 4c-2 2-6 2-9 0l-3-3a6 6 0 1 1 9-9l3 3c3 2 3 6 0 9Zm0 82c-2-2-6-2-9 0l-3 3a6 6 0 1 0 9 9l3-3c3-2 3-6 0-9Zm96 4a6 6 0 0 1-9 9l-3-3a6 6 0 1 1 9-9l3 3Z"/><style>path{fill:#000}@media (prefers-color-scheme:dark){path{fill:#fff}}</style></svg>
|
||||
|
After Width: | Height: | Size: 696 B |
45
docs/public/logo.svg
Normal file
45
docs/public/logo.svg
Normal file
@@ -0,0 +1,45 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<!-- Dark gradient background -->
|
||||
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2d2d2d"/>
|
||||
<stop offset="100%" style="stop-color:#1a1a1a"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Clean white text -->
|
||||
<linearGradient id="whiteGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#ffffff"/>
|
||||
<stop offset="100%" style="stop-color:#e8e8e8"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Subtle inner shadow -->
|
||||
<filter id="innerShadow">
|
||||
<feOffset dx="0" dy="2"/>
|
||||
<feGaussianBlur stdDeviation="1" result="offset-blur"/>
|
||||
<feComposite operator="out" in="SourceGraphic" in2="offset-blur" result="inverse"/>
|
||||
<feFlood flood-color="black" flood-opacity="0.2" result="color"/>
|
||||
<feComposite operator="in" in="color" in2="inverse" result="shadow"/>
|
||||
<feComposite operator="over" in="shadow" in2="SourceGraphic"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="512" height="512" fill="url(#bgGrad)"/>
|
||||
|
||||
<!-- Subtle border -->
|
||||
<rect x="1" y="1" width="510" height="510" fill="none" stroke="#3d3d3d" stroke-width="2"/>
|
||||
|
||||
<!-- ES Text -->
|
||||
<g filter="url(#innerShadow)">
|
||||
<!-- E -->
|
||||
<polygon points="72,120 72,392 240,392 240,340 140,340 140,282 220,282 220,230 140,230 140,172 240,172 240,120"
|
||||
fill="url(#whiteGrad)"/>
|
||||
|
||||
<!-- S -->
|
||||
<path d="M 280 172 Q 280 120 340 120 L 420 120 Q 450 120 450 160 L 450 186 L 398 186 L 398 168 Q 398 158 384 158 L 350 158 Q 320 158 320 188 Q 320 218 350 218 L 400 218 Q 450 218 450 274 L 450 332 Q 450 392 390 392 L 310 392 Q 270 392 270 340 L 270 314 L 322 314 L 322 340 Q 322 354 340 354 L 384 354 Q 404 354 404 324 L 404 290 Q 404 260 380 260 L 330 260 Q 280 260 280 208 Z"
|
||||
fill="url(#whiteGrad)"/>
|
||||
</g>
|
||||
|
||||
<!-- Accent line -->
|
||||
<rect x="72" y="424" width="368" height="4" fill="#ffffff" opacity="0.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
BIN
docs/src/assets/houston.webp
Normal file
BIN
docs/src/assets/houston.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
45
docs/src/assets/logo.svg
Normal file
45
docs/src/assets/logo.svg
Normal file
@@ -0,0 +1,45 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<!-- Dark gradient background -->
|
||||
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2d2d2d"/>
|
||||
<stop offset="100%" style="stop-color:#1a1a1a"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Clean white text -->
|
||||
<linearGradient id="whiteGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#ffffff"/>
|
||||
<stop offset="100%" style="stop-color:#e8e8e8"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Subtle inner shadow -->
|
||||
<filter id="innerShadow">
|
||||
<feOffset dx="0" dy="2"/>
|
||||
<feGaussianBlur stdDeviation="1" result="offset-blur"/>
|
||||
<feComposite operator="out" in="SourceGraphic" in2="offset-blur" result="inverse"/>
|
||||
<feFlood flood-color="black" flood-opacity="0.2" result="color"/>
|
||||
<feComposite operator="in" in="color" in2="inverse" result="shadow"/>
|
||||
<feComposite operator="over" in="shadow" in2="SourceGraphic"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="512" height="512" fill="url(#bgGrad)"/>
|
||||
|
||||
<!-- Subtle border -->
|
||||
<rect x="1" y="1" width="510" height="510" fill="none" stroke="#3d3d3d" stroke-width="2"/>
|
||||
|
||||
<!-- ES Text -->
|
||||
<g filter="url(#innerShadow)">
|
||||
<!-- E -->
|
||||
<polygon points="72,120 72,392 240,392 240,340 140,340 140,282 220,282 220,230 140,230 140,172 240,172 240,120"
|
||||
fill="url(#whiteGrad)"/>
|
||||
|
||||
<!-- S -->
|
||||
<path d="M 280 172 Q 280 120 340 120 L 420 120 Q 450 120 450 160 L 450 186 L 398 186 L 398 168 Q 398 158 384 158 L 350 158 Q 320 158 320 188 Q 320 218 350 218 L 400 218 Q 450 218 450 274 L 450 332 Q 450 392 390 392 L 310 392 Q 270 392 270 340 L 270 314 L 322 314 L 322 340 Q 322 354 340 354 L 384 354 Q 404 354 404 324 L 404 290 Q 404 260 380 260 L 330 260 Q 280 260 280 208 Z"
|
||||
fill="url(#whiteGrad)"/>
|
||||
</g>
|
||||
|
||||
<!-- Accent line -->
|
||||
<rect x="72" y="424" width="368" height="4" fill="#ffffff" opacity="0.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
20
docs/src/components/Head.astro
Normal file
20
docs/src/components/Head.astro
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
import type { Props } from '@astrojs/starlight/props';
|
||||
import Default from '@astrojs/starlight/components/Head.astro';
|
||||
---
|
||||
|
||||
<Default {...Astro.props}><slot /></Default>
|
||||
|
||||
<!-- Preload fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Force dark mode -->
|
||||
<script is:inline>
|
||||
document.documentElement.dataset.theme = 'dark';
|
||||
localStorage.setItem('starlight-theme', 'dark');
|
||||
</script>
|
||||
3
docs/src/components/ThemeSelect.astro
Normal file
3
docs/src/components/ThemeSelect.astro
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
// Empty component to disable theme selection (dark mode only)
|
||||
---
|
||||
7
docs/src/content.config.ts
Normal file
7
docs/src/content.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { docsLoader } from '@astrojs/starlight/loaders';
|
||||
import { docsSchema } from '@astrojs/starlight/schema';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
||||
};
|
||||
64
docs/src/content/docs/api/index.md
Normal file
64
docs/src/content/docs/api/index.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: API 参考
|
||||
description: ESEngine 完整 API 文档
|
||||
---
|
||||
|
||||
# API 参考
|
||||
|
||||
ESEngine 提供完整的 TypeScript API 文档,涵盖所有核心类、接口和方法。
|
||||
|
||||
## 核心模块
|
||||
|
||||
### 基础类
|
||||
|
||||
| 类名 | 描述 |
|
||||
|------|------|
|
||||
| [Core](/api/classes/Core) | 框架核心单例,管理整个 ECS 生命周期 |
|
||||
| [Scene](/api/classes/Scene) | 场景类,包含实体和系统 |
|
||||
| [World](/api/classes/World) | 游戏世界,可包含多个场景 |
|
||||
| [Entity](/api/classes/Entity) | 实体类,组件的容器 |
|
||||
| [Component](/api/classes/Component) | 组件基类,纯数据容器 |
|
||||
|
||||
### 系统类
|
||||
|
||||
| 类名 | 描述 |
|
||||
|------|------|
|
||||
| [EntitySystem](/api/classes/EntitySystem) | 实体系统基类 |
|
||||
| [ProcessingSystem](/api/classes/ProcessingSystem) | 处理系统,逐个处理实体 |
|
||||
| [IntervalSystem](/api/classes/IntervalSystem) | 间隔执行系统 |
|
||||
| [PassiveSystem](/api/classes/PassiveSystem) | 被动系统,不自动执行 |
|
||||
|
||||
### 工具类
|
||||
|
||||
| 类名 | 描述 |
|
||||
|------|------|
|
||||
| [Matcher](/api/classes/Matcher) | 实体匹配器,用于过滤实体 |
|
||||
| [Time](/api/classes/Time) | 时间管理器 |
|
||||
| [PerformanceMonitor](/api/classes/PerformanceMonitor) | 性能监控 |
|
||||
|
||||
## 装饰器
|
||||
|
||||
| 装饰器 | 描述 |
|
||||
|--------|------|
|
||||
| [@ECSComponent](/api/functions/ECSComponent) | 组件装饰器,用于注册组件 |
|
||||
| [@ECSSystem](/api/functions/ECSSystem) | 系统装饰器,用于注册系统 |
|
||||
|
||||
## 枚举
|
||||
|
||||
| 枚举 | 描述 |
|
||||
|------|------|
|
||||
| [ECSEventType](/api/enumerations/ECSEventType) | ECS 事件类型 |
|
||||
| [LogLevel](/api/enumerations/LogLevel) | 日志级别 |
|
||||
|
||||
## 接口
|
||||
|
||||
| 接口 | 描述 |
|
||||
|------|------|
|
||||
| [IScene](/api/interfaces/IScene) | 场景接口 |
|
||||
| [IComponent](/api/interfaces/IComponent) | 组件接口 |
|
||||
| [ISystemBase](/api/interfaces/ISystemBase) | 系统基础接口 |
|
||||
| [ICoreConfig](/api/interfaces/ICoreConfig) | Core 配置接口 |
|
||||
|
||||
:::tip[API 文档生成]
|
||||
完整 API 文档由 TypeDoc 自动生成,详见 GitHub 仓库中的 `/docs/api` 目录。
|
||||
:::
|
||||
20
docs/src/content/docs/en/examples/index.md
Normal file
20
docs/src/content/docs/en/examples/index.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: "Examples"
|
||||
description: "ESEngine example projects and demos"
|
||||
---
|
||||
|
||||
Explore example projects to learn ESEngine best practices.
|
||||
|
||||
## Available Examples
|
||||
|
||||
### [Worker System Demo](/en/examples/worker-system-demo/)
|
||||
Demonstrates how to use Web Workers for parallel processing, offloading heavy computations from the main thread.
|
||||
|
||||
## External Examples
|
||||
|
||||
### [Lawn Mower Demo](https://github.com/esengine/lawn-mower-demo)
|
||||
A complete game demo showcasing ESEngine features including:
|
||||
- Entity-Component-System architecture
|
||||
- Behavior tree AI
|
||||
- Scene management
|
||||
- Platform adaptation
|
||||
312
docs/src/content/docs/en/guide/component/best-practices.md
Normal file
312
docs/src/content/docs/en/guide/component/best-practices.md
Normal file
@@ -0,0 +1,312 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Component design patterns and complex examples"
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Keep Components Simple
|
||||
|
||||
```typescript
|
||||
// ✅ Good component design - single responsibility
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ❌ Avoid this design - too many responsibilities
|
||||
@ECSComponent('GameObject')
|
||||
class GameObject extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
health: number;
|
||||
damage: number;
|
||||
sprite: string;
|
||||
// Too many unrelated properties
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Constructor for Initialization
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
|
||||
constructor(x = 0, y = 0, rotation = 0, scale = 1) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
this.scale = scale;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Clear Type Definitions
|
||||
|
||||
```typescript
|
||||
interface InventoryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
type: 'weapon' | 'consumable' | 'misc';
|
||||
}
|
||||
|
||||
@ECSComponent('Inventory')
|
||||
class Inventory extends Component {
|
||||
items: InventoryItem[] = [];
|
||||
maxSlots: number;
|
||||
|
||||
constructor(maxSlots: number = 20) {
|
||||
super();
|
||||
this.maxSlots = maxSlots;
|
||||
}
|
||||
|
||||
addItem(item: InventoryItem): boolean {
|
||||
if (this.items.length < this.maxSlots) {
|
||||
this.items.push(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeItem(itemId: string): InventoryItem | null {
|
||||
const index = this.items.findIndex(item => item.id === itemId);
|
||||
if (index !== -1) {
|
||||
return this.items.splice(index, 1)[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Referencing Other Entities
|
||||
|
||||
When components need to reference other entities (like parent-child relationships, follow targets), **the recommended approach is to store entity IDs**, then look up in System:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number;
|
||||
followDistance: number = 50;
|
||||
|
||||
constructor(targetId: number) {
|
||||
super();
|
||||
this.targetId = targetId;
|
||||
}
|
||||
}
|
||||
|
||||
// Look up target entity and handle logic in System
|
||||
class FollowerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Follower, Position));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const position = entity.getComponent(Position)!;
|
||||
|
||||
// Look up target entity through scene
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
const targetPos = target.getComponent(Position);
|
||||
if (targetPos) {
|
||||
// Follow logic
|
||||
const dx = targetPos.x - position.x;
|
||||
const dy = targetPos.y - position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > follower.followDistance) {
|
||||
// Move closer to target
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Advantages of this approach:
|
||||
- Components stay simple, only store basic data types
|
||||
- Follows data-oriented design
|
||||
- Unified lookup and logic handling in System
|
||||
- Easy to understand and maintain
|
||||
|
||||
**Avoid storing entity references directly in components**:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong example: Storing entity reference directly
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // Still holds reference after entity destroyed, may cause memory leak
|
||||
}
|
||||
```
|
||||
|
||||
## Complex Component Examples
|
||||
|
||||
### State Machine Component
|
||||
|
||||
```typescript
|
||||
enum EntityState {
|
||||
Idle,
|
||||
Moving,
|
||||
Attacking,
|
||||
Dead
|
||||
}
|
||||
|
||||
@ECSComponent('StateMachine')
|
||||
class StateMachine extends Component {
|
||||
private _currentState: EntityState = EntityState.Idle;
|
||||
private _previousState: EntityState = EntityState.Idle;
|
||||
private _stateTimer: number = 0;
|
||||
|
||||
get currentState(): EntityState {
|
||||
return this._currentState;
|
||||
}
|
||||
|
||||
get previousState(): EntityState {
|
||||
return this._previousState;
|
||||
}
|
||||
|
||||
get stateTimer(): number {
|
||||
return this._stateTimer;
|
||||
}
|
||||
|
||||
changeState(newState: EntityState): void {
|
||||
if (this._currentState !== newState) {
|
||||
this._previousState = this._currentState;
|
||||
this._currentState = newState;
|
||||
this._stateTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
updateTimer(deltaTime: number): void {
|
||||
this._stateTimer += deltaTime;
|
||||
}
|
||||
|
||||
isInState(state: EntityState): boolean {
|
||||
return this._currentState === state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Data Component
|
||||
|
||||
```typescript
|
||||
interface WeaponData {
|
||||
damage: number;
|
||||
range: number;
|
||||
fireRate: number;
|
||||
ammo: number;
|
||||
}
|
||||
|
||||
@ECSComponent('WeaponConfig')
|
||||
class WeaponConfig extends Component {
|
||||
data: WeaponData;
|
||||
|
||||
constructor(weaponData: WeaponData) {
|
||||
super();
|
||||
this.data = { ...weaponData }; // Deep copy to avoid shared reference
|
||||
}
|
||||
|
||||
// Provide convenience methods
|
||||
getDamage(): number {
|
||||
return this.data.damage;
|
||||
}
|
||||
|
||||
canFire(): boolean {
|
||||
return this.data.ammo > 0;
|
||||
}
|
||||
|
||||
consumeAmmo(): boolean {
|
||||
if (this.data.ammo > 0) {
|
||||
this.data.ammo--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tag Components
|
||||
|
||||
```typescript
|
||||
// Tag components: No data, only for identification
|
||||
@ECSComponent('Player')
|
||||
class PlayerTag extends Component {}
|
||||
|
||||
@ECSComponent('Enemy')
|
||||
class EnemyTag extends Component {}
|
||||
|
||||
@ECSComponent('Dead')
|
||||
class DeadTag extends Component {}
|
||||
|
||||
// Use tags for querying
|
||||
class EnemySystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(EnemyTag, Health).none(DeadTag));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Buffer Components
|
||||
|
||||
```typescript
|
||||
// Event/command buffer component
|
||||
@ECSComponent('DamageBuffer')
|
||||
class DamageBuffer extends Component {
|
||||
damages: { amount: number; source: number; timestamp: number }[] = [];
|
||||
|
||||
addDamage(amount: number, sourceId: number): void {
|
||||
this.damages.push({
|
||||
amount,
|
||||
source: sourceId,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.damages.length = 0;
|
||||
}
|
||||
|
||||
getTotalDamage(): number {
|
||||
return this.damages.reduce((sum, d) => sum + d.amount, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: How large should a component be?
|
||||
|
||||
**A**: Follow single responsibility principle. If a component contains unrelated data, split into multiple components.
|
||||
|
||||
### Q: Can components have methods?
|
||||
|
||||
**A**: Yes, but they should be data-related helper methods (like `isDead()`), not business logic. Business logic goes in Systems.
|
||||
|
||||
### Q: How to handle dependencies between components?
|
||||
|
||||
**A**: Handle inter-component interactions in Systems, don't directly access other components within a component.
|
||||
|
||||
### Q: When to use EntityRef?
|
||||
|
||||
**A**: Only when you need frequent access to referenced entity and the reference relationship is stable (like parent-child). Storing IDs is better for most cases.
|
||||
|
||||
---
|
||||
|
||||
Components are the data carriers of ECS architecture. Properly designing components makes your game code more modular, maintainable, and performant.
|
||||
228
docs/src/content/docs/en/guide/component/entity-ref.md
Normal file
228
docs/src/content/docs/en/guide/component/entity-ref.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
title: "EntityRef Decorator"
|
||||
description: "Safe entity reference tracking mechanism"
|
||||
---
|
||||
|
||||
The framework provides the `@EntityRef` decorator for **special scenarios** to safely store entity references. This is an advanced feature; storing IDs is recommended for most cases.
|
||||
|
||||
## When Do You Need EntityRef?
|
||||
|
||||
`@EntityRef` can simplify code in these scenarios:
|
||||
|
||||
1. **Parent-Child Relationships**: Need to directly access parent or child entities in components
|
||||
2. **Complex Associations**: Multiple reference relationships between entities
|
||||
3. **Frequent Access**: Need to access referenced entity in multiple places, ID lookup has performance overhead
|
||||
|
||||
## Core Features
|
||||
|
||||
The `@EntityRef` decorator automatically tracks references through **ReferenceTracker**:
|
||||
|
||||
- When the referenced entity is destroyed, all `@EntityRef` properties pointing to it are automatically set to `null`
|
||||
- Prevents cross-scene references (outputs warning and refuses to set)
|
||||
- Prevents references to destroyed entities (outputs warning and sets to `null`)
|
||||
- Uses WeakRef to avoid memory leaks (automatic GC support)
|
||||
- Automatically cleans up reference registration when component is removed
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Parent')
|
||||
class ParentComponent extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
}
|
||||
|
||||
// Usage example
|
||||
const scene = new Scene();
|
||||
const parent = scene.createEntity('Parent');
|
||||
const child = scene.createEntity('Child');
|
||||
|
||||
const comp = child.addComponent(new ParentComponent());
|
||||
comp.parent = parent;
|
||||
|
||||
console.log(comp.parent); // Entity { name: 'Parent' }
|
||||
|
||||
// When parent is destroyed, comp.parent automatically becomes null
|
||||
parent.destroy();
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
## Multiple Reference Properties
|
||||
|
||||
A component can have multiple `@EntityRef` properties:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Combat')
|
||||
class CombatComponent extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
ally: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
lastAttacker: Entity | null = null;
|
||||
}
|
||||
|
||||
// Usage example
|
||||
const player = scene.createEntity('Player');
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
const npc = scene.createEntity('NPC');
|
||||
|
||||
const combat = player.addComponent(new CombatComponent());
|
||||
combat.target = enemy;
|
||||
combat.ally = npc;
|
||||
|
||||
// After enemy is destroyed, only target becomes null, ally remains valid
|
||||
enemy.destroy();
|
||||
console.log(combat.target); // null
|
||||
console.log(combat.ally); // Entity { name: 'NPC' }
|
||||
```
|
||||
|
||||
## Safety Checks
|
||||
|
||||
`@EntityRef` provides multiple safety checks:
|
||||
|
||||
```typescript
|
||||
const scene1 = new Scene();
|
||||
const scene2 = new Scene();
|
||||
|
||||
const entity1 = scene1.createEntity('Entity1');
|
||||
const entity2 = scene2.createEntity('Entity2');
|
||||
|
||||
const comp = entity1.addComponent(new ParentComponent());
|
||||
|
||||
// Cross-scene reference fails
|
||||
comp.parent = entity2; // Outputs error log, comp.parent is null
|
||||
console.log(comp.parent); // null
|
||||
|
||||
// Reference to destroyed entity fails
|
||||
const entity3 = scene1.createEntity('Entity3');
|
||||
entity3.destroy();
|
||||
comp.parent = entity3; // Outputs warning log, comp.parent is null
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
## Implementation Principle
|
||||
|
||||
`@EntityRef` uses the following mechanisms for automatic reference tracking:
|
||||
|
||||
1. **ReferenceTracker**: Scene holds a reference tracker that records all entity reference relationships
|
||||
2. **WeakRef**: Uses weak references to store components, avoiding memory leaks from circular references
|
||||
3. **Property Interception**: Intercepts getter/setter through `Object.defineProperty`
|
||||
4. **Automatic Cleanup**: When entity is destroyed, ReferenceTracker traverses all references and sets them to null
|
||||
|
||||
```typescript
|
||||
// Simplified implementation principle
|
||||
class ReferenceTracker {
|
||||
// entityId -> all component records referencing this entity
|
||||
private _references: Map<number, Set<{ component: WeakRef<Component>, propertyKey: string }>>;
|
||||
|
||||
// Called when entity is destroyed
|
||||
clearReferencesTo(entityId: number): void {
|
||||
const records = this._references.get(entityId);
|
||||
if (records) {
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
// Set component's reference property to null
|
||||
(component as any)[record.propertyKey] = null;
|
||||
}
|
||||
}
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
`@EntityRef` introduces some performance overhead:
|
||||
|
||||
- **Write Overhead**: Need to update ReferenceTracker each time a reference is set
|
||||
- **Memory Overhead**: ReferenceTracker needs to maintain reference mapping table
|
||||
- **Destroy Overhead**: Need to traverse all references and clean up when entity is destroyed
|
||||
|
||||
For most scenarios, this overhead is acceptable. But with **many entities and frequent reference changes**, storing IDs may be more efficient.
|
||||
|
||||
## Debug Support
|
||||
|
||||
ReferenceTracker provides debug interfaces:
|
||||
|
||||
```typescript
|
||||
// View which components reference an entity
|
||||
const references = scene.referenceTracker.getReferencesTo(entity.id);
|
||||
console.log(`Entity ${entity.name} is referenced by ${references.length} components`);
|
||||
|
||||
// Get complete debug info
|
||||
const debugInfo = scene.referenceTracker.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
## Comparison with Storing IDs
|
||||
|
||||
### Storing IDs (Recommended for Most Cases)
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number | null = null;
|
||||
}
|
||||
|
||||
// Look up in System
|
||||
class FollowerSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
// Follow logic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using EntityRef (For Complex Associations)
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
|
||||
position: { x: number, y: number } = { x: 0, y: 0 };
|
||||
|
||||
// Can directly access parent entity's component
|
||||
getWorldPosition(): { x: number, y: number } {
|
||||
if (!this.parent) {
|
||||
return { ...this.position };
|
||||
}
|
||||
|
||||
const parentTransform = this.parent.getComponent(Transform);
|
||||
if (parentTransform) {
|
||||
const parentPos = parentTransform.getWorldPosition();
|
||||
return {
|
||||
x: parentPos.x + this.position.x,
|
||||
y: parentPos.y + this.position.y
|
||||
};
|
||||
}
|
||||
|
||||
return { ...this.position };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
| Approach | Use Case | Pros | Cons |
|
||||
|----------|----------|------|------|
|
||||
| Store ID | Most cases | Simple, no extra overhead | Need to lookup in System |
|
||||
| @EntityRef | Parent-child, complex associations | Auto-cleanup, cleaner code | Has performance overhead |
|
||||
|
||||
- **Recommended**: Use store ID + System lookup for most cases
|
||||
- **EntityRef Use Cases**: Parent-child relationships, complex associations, when component needs direct access to referenced entity
|
||||
- **Core Advantage**: Automatic cleanup, prevents dangling references, cleaner code
|
||||
- **Considerations**: Has performance overhead, not suitable for many dynamic references
|
||||
226
docs/src/content/docs/en/guide/component/index.md
Normal file
226
docs/src/content/docs/en/guide/component/index.md
Normal file
@@ -0,0 +1,226 @@
|
||||
---
|
||||
title: "Component System"
|
||||
description: "ECS component basics and creation methods"
|
||||
---
|
||||
|
||||
In ECS architecture, Components are carriers of data and behavior. Components define the properties and functionality that entities possess, and are the core building blocks of ECS architecture.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
Components are concrete classes that inherit from the `Component` abstract base class, used for:
|
||||
- Storing entity data (such as position, velocity, health, etc.)
|
||||
- Defining behavior methods related to the data
|
||||
- Providing lifecycle callback hooks
|
||||
- Supporting serialization and debugging
|
||||
|
||||
## Creating Components
|
||||
|
||||
### Basic Component Definition
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
// Components can contain behavior methods
|
||||
takeDamage(damage: number): void {
|
||||
this.current = Math.max(0, this.current - damage);
|
||||
}
|
||||
|
||||
heal(amount: number): void {
|
||||
this.current = Math.min(this.max, this.current + amount);
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## @ECSComponent Decorator
|
||||
|
||||
`@ECSComponent` is a required decorator for component classes, providing type identification and metadata management.
|
||||
|
||||
### Why It's Required
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Type Identification** | Provides stable type name that remains correct after code obfuscation |
|
||||
| **Serialization Support** | Uses this name as type identifier during serialization/deserialization |
|
||||
| **Component Registration** | Auto-registers to ComponentRegistry, assigns unique bitmask |
|
||||
| **Debug Support** | Shows readable component names in debug tools and logs |
|
||||
|
||||
### Basic Syntax
|
||||
|
||||
```typescript
|
||||
@ECSComponent(typeName: string)
|
||||
```
|
||||
|
||||
- `typeName`: Component's type name, recommended to use same or similar name as class name
|
||||
|
||||
### Usage Examples
|
||||
|
||||
```typescript
|
||||
// ✅ Correct usage
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ✅ Recommended: Keep type name consistent with class name
|
||||
@ECSComponent('PlayerController')
|
||||
class PlayerController extends Component {
|
||||
speed: number = 5;
|
||||
}
|
||||
|
||||
// ❌ Wrong usage - no decorator
|
||||
class BadComponent extends Component {
|
||||
// Components defined this way may have issues in production:
|
||||
// 1. Class name changes after minification, can't serialize correctly
|
||||
// 2. Component not registered to framework, queries may fail
|
||||
}
|
||||
```
|
||||
|
||||
### Using with @Serializable
|
||||
|
||||
When components need serialization support, use `@ECSComponent` and `@Serializable` together:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
name: string = '';
|
||||
|
||||
@Serialize()
|
||||
level: number = 1;
|
||||
|
||||
// Fields without @Serialize() won't be serialized
|
||||
private _cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: `@ECSComponent`'s `typeName` and `@Serializable`'s `typeId` can differ. If `@Serializable` doesn't specify `typeId`, it defaults to `@ECSComponent`'s `typeName`.
|
||||
|
||||
### Type Name Uniqueness
|
||||
|
||||
Each component's type name should be unique:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Two components using the same type name
|
||||
@ECSComponent('Health')
|
||||
class HealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('Health') // Conflict!
|
||||
class EnemyHealthComponent extends Component { }
|
||||
|
||||
// ✅ Correct: Use different type names
|
||||
@ECSComponent('PlayerHealth')
|
||||
class PlayerHealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('EnemyHealth')
|
||||
class EnemyHealthComponent extends Component { }
|
||||
```
|
||||
|
||||
## Component Properties
|
||||
|
||||
Each component has some built-in properties:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
someData: string = "example";
|
||||
|
||||
onAddedToEntity(): void {
|
||||
console.log(`Component ID: ${this.id}`); // Unique component ID
|
||||
console.log(`Entity ID: ${this.entityId}`); // Owning entity's ID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Component and Entity Relationship
|
||||
|
||||
Components store the owning entity's ID (`entityId`), not a direct entity reference. This is a reflection of ECS's data-oriented design, avoiding circular references.
|
||||
|
||||
In practice, **entity and component interactions should be handled in Systems**, not within components:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Recommended: Handle logic in System
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Health, Damage));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health)!;
|
||||
const damage = entity.getComponent(Damage)!;
|
||||
|
||||
health.current -= damage.value;
|
||||
|
||||
if (health.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
|
||||
// Remove Damage component after applying damage
|
||||
entity.removeComponent(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## More Topics
|
||||
|
||||
- [Lifecycle](/en/guide/component/lifecycle) - Component lifecycle hooks
|
||||
- [EntityRef Decorator](/en/guide/component/entity-ref) - Safe entity references
|
||||
- [Best Practices](/en/guide/component/best-practices) - Component design patterns and examples
|
||||
182
docs/src/content/docs/en/guide/component/lifecycle.md
Normal file
182
docs/src/content/docs/en/guide/component/lifecycle.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: "Component Lifecycle"
|
||||
description: "Component lifecycle hooks and events"
|
||||
---
|
||||
|
||||
Components provide lifecycle hooks that can be overridden to execute specific logic.
|
||||
|
||||
## Lifecycle Methods
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
private resource: SomeResource | null = null;
|
||||
|
||||
/**
|
||||
* Called when component is added to entity
|
||||
* Use for initializing resources, establishing references, etc.
|
||||
*/
|
||||
onAddedToEntity(): void {
|
||||
console.log(`Component ${this.constructor.name} added, Entity ID: ${this.entityId}`);
|
||||
this.resource = new SomeResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when component is removed from entity
|
||||
* Use for cleaning up resources, breaking references, etc.
|
||||
*/
|
||||
onRemovedFromEntity(): void {
|
||||
console.log(`Component ${this.constructor.name} removed`);
|
||||
if (this.resource) {
|
||||
this.resource.cleanup();
|
||||
this.resource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle Order
|
||||
|
||||
```
|
||||
Entity created
|
||||
↓
|
||||
addComponent() called
|
||||
↓
|
||||
onAddedToEntity() triggered
|
||||
↓
|
||||
Component in normal use...
|
||||
↓
|
||||
removeComponent() or entity.destroy() called
|
||||
↓
|
||||
onRemovedFromEntity() triggered
|
||||
↓
|
||||
Component removed/destroyed
|
||||
```
|
||||
|
||||
## Practical Use Cases
|
||||
|
||||
### Resource Management
|
||||
|
||||
```typescript
|
||||
@ECSComponent('TextureComponent')
|
||||
class TextureComponent extends Component {
|
||||
private _texture: Texture | null = null;
|
||||
texturePath: string = '';
|
||||
|
||||
onAddedToEntity(): void {
|
||||
// Load texture resource
|
||||
this._texture = TextureManager.load(this.texturePath);
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
// Release texture resource
|
||||
if (this._texture) {
|
||||
TextureManager.release(this._texture);
|
||||
this._texture = null;
|
||||
}
|
||||
}
|
||||
|
||||
get texture(): Texture | null {
|
||||
return this._texture;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Event Listening
|
||||
|
||||
```typescript
|
||||
@ECSComponent('InputListener')
|
||||
class InputListener extends Component {
|
||||
private _boundHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||
|
||||
onAddedToEntity(): void {
|
||||
this._boundHandler = this.handleKeyDown.bind(this);
|
||||
window.addEventListener('keydown', this._boundHandler);
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
if (this._boundHandler) {
|
||||
window.removeEventListener('keydown', this._boundHandler);
|
||||
this._boundHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeyDown(e: KeyboardEvent): void {
|
||||
// Handle keyboard input
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Registering with External Systems
|
||||
|
||||
```typescript
|
||||
@ECSComponent('PhysicsBody')
|
||||
class PhysicsBody extends Component {
|
||||
private _body: PhysicsWorld.Body | null = null;
|
||||
|
||||
onAddedToEntity(): void {
|
||||
// Create rigid body in physics world
|
||||
this._body = PhysicsWorld.createBody({
|
||||
entityId: this.entityId,
|
||||
type: 'dynamic'
|
||||
});
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
// Remove rigid body from physics world
|
||||
if (this._body) {
|
||||
PhysicsWorld.removeBody(this._body);
|
||||
this._body = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Avoid Accessing Other Components in Lifecycle
|
||||
|
||||
```typescript
|
||||
@ECSComponent('BadComponent')
|
||||
class BadComponent extends Component {
|
||||
onAddedToEntity(): void {
|
||||
// ⚠️ Not recommended: Other components may not be added yet
|
||||
const other = this.entity?.getComponent(OtherComponent);
|
||||
if (other) {
|
||||
// May be null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Recommended: Use System to Handle Inter-Component Interactions
|
||||
|
||||
```typescript
|
||||
@ECSSystem('InitializationSystem')
|
||||
class InitializationSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(ComponentA, ComponentB));
|
||||
}
|
||||
|
||||
// Use onAdded event to ensure both components exist
|
||||
onAdded(entity: Entity): void {
|
||||
const a = entity.getComponent(ComponentA)!;
|
||||
const b = entity.getComponent(ComponentB)!;
|
||||
// Safely initialize interaction
|
||||
a.linkTo(b);
|
||||
}
|
||||
|
||||
onRemoved(entity: Entity): void {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Comparison with System Lifecycle
|
||||
|
||||
| Feature | Component Lifecycle | System Lifecycle |
|
||||
|---------|---------------------|------------------|
|
||||
| Trigger Timing | When component added/removed | When match conditions met |
|
||||
| Use Case | Resource init/cleanup | Business logic processing |
|
||||
| Access Other Components | Not recommended | Safe |
|
||||
| Access Scene | Limited | Full |
|
||||
225
docs/src/content/docs/en/guide/entity-query/best-practices.md
Normal file
225
docs/src/content/docs/en/guide/entity-query/best-practices.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Entity query optimization and practical applications"
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Prefer EntitySystem
|
||||
|
||||
```typescript
|
||||
// ✅ Recommended: Use EntitySystem
|
||||
class GoodSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Automatically get matching entities, updated each frame
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Not recommended: Manual query in update
|
||||
class BadSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Manual query each frame wastes performance
|
||||
const result = this.scene!.querySystem.queryAll(HealthComponent);
|
||||
for (const entity of result.entities) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use none() for Exclusions
|
||||
|
||||
```typescript
|
||||
// Exclude dead enemies
|
||||
class EnemyAISystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(EnemyTag, AIComponent)
|
||||
.none(DeadTag) // Don't process dead enemies
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Tags for Query Optimization
|
||||
|
||||
```typescript
|
||||
// ❌ Bad: Query all entities then filter
|
||||
const allEntities = scene.querySystem.getAllEntities();
|
||||
const players = allEntities.filter(e => e.hasComponent(PlayerTag));
|
||||
|
||||
// ✅ Good: Query directly by tag
|
||||
const players = scene.querySystem.queryByTag(Tags.PLAYER).entities;
|
||||
```
|
||||
|
||||
### 4. Avoid Overly Complex Query Conditions
|
||||
|
||||
```typescript
|
||||
// ❌ Not recommended: Too complex
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(A, B, C, D)
|
||||
.any(E, F, G)
|
||||
.none(H, I, J)
|
||||
);
|
||||
|
||||
// ✅ Recommended: Split into multiple simple systems
|
||||
class SystemAB extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(A, B));
|
||||
}
|
||||
}
|
||||
|
||||
class SystemCD extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(C, D));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Practical Applications
|
||||
|
||||
### Scenario 1: Physics System
|
||||
|
||||
```typescript
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, RigidbodyComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const rigidbody = entity.getComponent(RigidbodyComponent)!;
|
||||
|
||||
// Apply gravity
|
||||
rigidbody.velocity.y -= 9.8 * Time.deltaTime;
|
||||
|
||||
// Update position
|
||||
transform.position.x += rigidbody.velocity.x * Time.deltaTime;
|
||||
transform.position.y += rigidbody.velocity.y * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scenario 2: Render System
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(TransformComponent, SpriteComponent)
|
||||
.none(InvisibleTag) // Exclude invisible entities
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Sort by z-order
|
||||
const sorted = entities.slice().sort((a, b) => {
|
||||
const zA = a.getComponent(TransformComponent)!.z;
|
||||
const zB = b.getComponent(TransformComponent)!.z;
|
||||
return zA - zB;
|
||||
});
|
||||
|
||||
// Render entities
|
||||
for (const entity of sorted) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const sprite = entity.getComponent(SpriteComponent)!;
|
||||
|
||||
renderer.drawSprite(sprite.texture, transform.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scenario 3: Collision Detection
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, ColliderComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Simple O(n²) collision detection
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
for (let j = i + 1; j < entities.length; j++) {
|
||||
this.checkCollision(entities[i], entities[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(a: Entity, b: Entity): void {
|
||||
const transA = a.getComponent(TransformComponent)!;
|
||||
const transB = b.getComponent(TransformComponent)!;
|
||||
const colliderA = a.getComponent(ColliderComponent)!;
|
||||
const colliderB = b.getComponent(ColliderComponent)!;
|
||||
|
||||
if (this.isOverlapping(transA, colliderA, transB, colliderB)) {
|
||||
// Trigger collision event
|
||||
scene.eventSystem.emit('collision', { entityA: a, entityB: b });
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scenario 4: One-Time Query
|
||||
|
||||
```typescript
|
||||
// Execute one-time query outside of systems
|
||||
class GameManager {
|
||||
private scene: Scene;
|
||||
|
||||
public countEnemies(): number {
|
||||
const result = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
return result.count;
|
||||
}
|
||||
|
||||
public findNearestEnemy(playerPos: Vector2): Entity | null {
|
||||
const enemies = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
|
||||
let nearest: Entity | null = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const enemy of enemies.entities) {
|
||||
const transform = enemy.getComponent(TransformComponent);
|
||||
if (!transform) continue;
|
||||
|
||||
const distance = Vector2.distance(playerPos, transform.position);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearest = enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Statistics
|
||||
|
||||
```typescript
|
||||
const stats = querySystem.getStats();
|
||||
console.log('Total queries:', stats.queryStats.totalQueries);
|
||||
console.log('Cache hit rate:', stats.queryStats.cacheHitRate);
|
||||
console.log('Cache size:', stats.cacheStats.size);
|
||||
```
|
||||
|
||||
## Related APIs
|
||||
|
||||
- [Matcher](/api/classes/Matcher/) - Query condition descriptor API reference
|
||||
- [QuerySystem](/api/classes/QuerySystem/) - Query system API reference
|
||||
- [EntitySystem](/api/classes/EntitySystem/) - Entity system API reference
|
||||
- [Entity](/api/classes/Entity/) - Entity API reference
|
||||
133
docs/src/content/docs/en/guide/entity-query/compiled-query.md
Normal file
133
docs/src/content/docs/en/guide/entity-query/compiled-query.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "Compiled Query"
|
||||
description: "CompiledQuery type-safe query tool"
|
||||
---
|
||||
|
||||
> **v2.4.0+**
|
||||
|
||||
CompiledQuery is a lightweight query tool providing type-safe component access and change detection support. Suitable for ad-hoc queries, tool development, and simple iteration scenarios.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
// Create compiled query
|
||||
const query = scene.querySystem.compile(Position, Velocity);
|
||||
|
||||
// Type-safe iteration - component parameters auto-infer types
|
||||
query.forEach((entity, pos, vel) => {
|
||||
pos.x += vel.vx * deltaTime;
|
||||
pos.y += vel.vy * deltaTime;
|
||||
});
|
||||
|
||||
// Get entity count
|
||||
console.log(`Matched entities: ${query.count}`);
|
||||
|
||||
// Get first matched entity
|
||||
const first = query.first();
|
||||
if (first) {
|
||||
const [entity, pos, vel] = first;
|
||||
console.log(`First entity: ${entity.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Change Detection
|
||||
|
||||
CompiledQuery supports epoch-based change detection:
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
private _query: CompiledQuery<[typeof Transform, typeof Sprite]>;
|
||||
private _lastEpoch = 0;
|
||||
|
||||
protected onInitialize(): void {
|
||||
this._query = this.scene!.querySystem.compile(Transform, Sprite);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process entities where Transform or Sprite changed
|
||||
this._query.forEachChanged(this._lastEpoch, (entity, transform, sprite) => {
|
||||
this.updateRenderData(entity, transform, sprite);
|
||||
});
|
||||
|
||||
// Save current epoch as next check starting point
|
||||
this._lastEpoch = this.scene!.epochManager.current;
|
||||
}
|
||||
|
||||
private updateRenderData(entity: Entity, transform: Transform, sprite: Sprite): void {
|
||||
// Update render data
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Functional API
|
||||
|
||||
CompiledQuery provides rich functional APIs:
|
||||
|
||||
```typescript
|
||||
const query = scene.querySystem.compile(Position, Health);
|
||||
|
||||
// map - Transform entity data
|
||||
const positions = query.map((entity, pos, health) => ({
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
healthPercent: health.current / health.max
|
||||
}));
|
||||
|
||||
// filter - Filter entities
|
||||
const lowHealthEntities = query.filter((entity, pos, health) => {
|
||||
return health.current < health.max * 0.2;
|
||||
});
|
||||
|
||||
// find - Find first matching entity
|
||||
const target = query.find((entity, pos, health) => {
|
||||
return health.current > 0 && pos.x > 100;
|
||||
});
|
||||
|
||||
// toArray - Convert to array
|
||||
const allData = query.toArray();
|
||||
for (const [entity, pos, health] of allData) {
|
||||
console.log(`${entity.name}: ${pos.x}, ${pos.y}`);
|
||||
}
|
||||
|
||||
// any/empty - Check for matches
|
||||
if (query.any()) {
|
||||
console.log('Has matching entities');
|
||||
}
|
||||
if (query.empty()) {
|
||||
console.log('No matching entities');
|
||||
}
|
||||
```
|
||||
|
||||
## CompiledQuery vs EntitySystem
|
||||
|
||||
| Feature | CompiledQuery | EntitySystem |
|
||||
|---------|---------------|--------------|
|
||||
| **Purpose** | Lightweight query tool | Complete system logic |
|
||||
| **Lifecycle** | None | Full (onInitialize, onDestroy, etc.) |
|
||||
| **Scheduling Integration** | None | Supports @Stage, @Before, @After |
|
||||
| **Change Detection** | forEachChanged | forEachChanged |
|
||||
| **Event Listening** | None | addEventListener |
|
||||
| **Command Buffer** | None | this.commands |
|
||||
| **Type-Safe Components** | forEach params auto-infer | Need manual getComponent |
|
||||
| **Use Cases** | Ad-hoc queries, tools, prototypes | Core game logic |
|
||||
|
||||
**Selection Advice**:
|
||||
|
||||
- Use **EntitySystem** for core game logic (movement, combat, AI, etc.)
|
||||
- Use **CompiledQuery** for one-time queries, tool development, or simple iteration
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `forEach(callback)` | Iterate all matched entities, type-safe component params |
|
||||
| `forEachChanged(sinceEpoch, callback)` | Only iterate changed entities |
|
||||
| `first()` | Get first matched entity and components |
|
||||
| `toArray()` | Convert to [entity, ...components] array |
|
||||
| `map(callback)` | Map transformation |
|
||||
| `filter(predicate)` | Filter entities |
|
||||
| `find(predicate)` | Find first entity meeting condition |
|
||||
| `any()` | Whether any matches exist |
|
||||
| `empty()` | Whether no matches exist |
|
||||
| `count` | Number of matched entities |
|
||||
| `entities` | Matched entity list (read-only) |
|
||||
244
docs/src/content/docs/en/guide/entity-query/index.md
Normal file
244
docs/src/content/docs/en/guide/entity-query/index.md
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: "Entity Query System"
|
||||
description: "ECS entity query core concepts and basic usage"
|
||||
---
|
||||
|
||||
Entity querying is one of the core features of ECS architecture. This guide introduces how to use Matcher and QuerySystem to query and filter entities.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Matcher - Query Condition Descriptor
|
||||
|
||||
Matcher is a chainable API used to describe entity query conditions. It doesn't execute queries itself but passes conditions to EntitySystem or QuerySystem.
|
||||
|
||||
### QuerySystem - Query Execution Engine
|
||||
|
||||
QuerySystem is responsible for actually executing queries, using reactive query mechanisms internally for automatic performance optimization.
|
||||
|
||||
## Using Matcher in EntitySystem
|
||||
|
||||
This is the most common usage. EntitySystem automatically filters and processes entities matching conditions through Matcher.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, Entity, Component } from '@esengine/ecs-framework';
|
||||
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
public vx: number = 0;
|
||||
public vy: number = 0;
|
||||
}
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Method 1: Use Matcher.empty().all()
|
||||
super(Matcher.empty().all(PositionComponent, VelocityComponent));
|
||||
|
||||
// Method 2: Use Matcher.all() directly (equivalent)
|
||||
// super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(PositionComponent)!;
|
||||
const vel = entity.getComponent(VelocityComponent)!;
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add to scene
|
||||
scene.addEntityProcessor(new MovementSystem());
|
||||
```
|
||||
|
||||
### Matcher Chainable API
|
||||
|
||||
#### all() - Must Include All Components
|
||||
|
||||
```typescript
|
||||
class HealthSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Entity must have both Health and Position components
|
||||
super(Matcher.empty().all(HealthComponent, PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process entities with both components
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### any() - Include At Least One Component
|
||||
|
||||
```typescript
|
||||
class DamageableSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Entity must have at least Health or Shield
|
||||
super(Matcher.any(HealthComponent, ShieldComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Process entities with health or shield
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### none() - Must Not Include Components
|
||||
|
||||
```typescript
|
||||
class AliveEntitySystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Entity must not have DeadTag component
|
||||
super(Matcher.all(HealthComponent).none(DeadTag));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process living entities
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Combined Conditions
|
||||
|
||||
```typescript
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(PositionComponent, HealthComponent) // Must have position and health
|
||||
.any(WeaponComponent, MagicComponent) // At least weapon or magic
|
||||
.none(DeadTag, FrozenTag) // Not dead or frozen
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Process living entities that can fight
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### nothing() - Match No Entities
|
||||
|
||||
Used for systems that only need lifecycle methods (`onBegin`, `onEnd`) but don't process entities.
|
||||
|
||||
```typescript
|
||||
class FrameTimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Match no entities
|
||||
super(Matcher.nothing());
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// Execute at frame start
|
||||
Performance.markFrameStart();
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Never called because no matching entities
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// Execute at frame end
|
||||
Performance.markFrameEnd();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### empty() vs nothing()
|
||||
|
||||
| Method | Behavior | Use Case |
|
||||
|--------|----------|----------|
|
||||
| `Matcher.empty()` | Match **all** entities | Process all entities in scene |
|
||||
| `Matcher.nothing()` | Match **no** entities | Only need lifecycle callbacks |
|
||||
|
||||
## Using QuerySystem Directly
|
||||
|
||||
If you don't need to create a system, you can use Scene's querySystem directly.
|
||||
|
||||
### Basic Query Methods
|
||||
|
||||
```typescript
|
||||
// Get scene's query system
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// Query entities with all specified components
|
||||
const result1 = querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
console.log(`Found ${result1.count} moving entities`);
|
||||
console.log(`Query time: ${result1.executionTime.toFixed(2)}ms`);
|
||||
|
||||
// Query entities with any specified component
|
||||
const result2 = querySystem.queryAny(WeaponComponent, MagicComponent);
|
||||
console.log(`Found ${result2.count} combat units`);
|
||||
|
||||
// Query entities without specified components
|
||||
const result3 = querySystem.queryNone(DeadTag);
|
||||
console.log(`Found ${result3.count} living entities`);
|
||||
```
|
||||
|
||||
### Query by Tag and Name
|
||||
|
||||
```typescript
|
||||
// Query by tag
|
||||
const playerResult = querySystem.queryByTag(Tags.PLAYER);
|
||||
for (const player of playerResult.entities) {
|
||||
console.log('Player:', player.name);
|
||||
}
|
||||
|
||||
// Query by name
|
||||
const bossResult = querySystem.queryByName('Boss');
|
||||
if (bossResult.count > 0) {
|
||||
const boss = bossResult.entities[0];
|
||||
console.log('Found Boss:', boss);
|
||||
}
|
||||
|
||||
// Query by single component
|
||||
const healthResult = querySystem.queryByComponent(HealthComponent);
|
||||
console.log(`${healthResult.count} entities have health`);
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Automatic Caching
|
||||
|
||||
QuerySystem uses reactive queries internally with automatic caching:
|
||||
|
||||
```typescript
|
||||
// First query, executes actual query
|
||||
const result1 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result1.fromCache); // false
|
||||
|
||||
// Second same query, uses cache
|
||||
const result2 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result2.fromCache); // true
|
||||
```
|
||||
|
||||
### Automatic Updates on Entity Changes
|
||||
|
||||
Query cache updates automatically when entities add/remove components:
|
||||
|
||||
```typescript
|
||||
// Query entities with weapons
|
||||
const before = querySystem.queryAll(WeaponComponent);
|
||||
console.log('Before:', before.count); // Assume 5
|
||||
|
||||
// Add weapon to entity
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new WeaponComponent());
|
||||
|
||||
// Query again, automatically includes new entity
|
||||
const after = querySystem.queryAll(WeaponComponent);
|
||||
console.log('After:', after.count); // Now 6
|
||||
```
|
||||
|
||||
## More Topics
|
||||
|
||||
- [Matcher API](/en/guide/entity-query/matcher-api) - Complete Matcher API reference
|
||||
- [Compiled Query](/en/guide/entity-query/compiled-query) - CompiledQuery advanced usage
|
||||
- [Best Practices](/en/guide/entity-query/best-practices) - Query optimization and practical applications
|
||||
118
docs/src/content/docs/en/guide/entity-query/matcher-api.md
Normal file
118
docs/src/content/docs/en/guide/entity-query/matcher-api.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: "Matcher API"
|
||||
description: "Complete Matcher API reference"
|
||||
---
|
||||
|
||||
## Static Creation Methods
|
||||
|
||||
| Method | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `Matcher.all(...types)` | Must include all specified components | `Matcher.all(Position, Velocity)` |
|
||||
| `Matcher.any(...types)` | Include at least one specified component | `Matcher.any(Health, Shield)` |
|
||||
| `Matcher.none(...types)` | Must not include any specified components | `Matcher.none(Dead)` |
|
||||
| `Matcher.byTag(tag)` | Query by tag | `Matcher.byTag(1)` |
|
||||
| `Matcher.byName(name)` | Query by name | `Matcher.byName("Player")` |
|
||||
| `Matcher.byComponent(type)` | Query by single component | `Matcher.byComponent(Health)` |
|
||||
| `Matcher.empty()` | Create empty matcher (matches all entities) | `Matcher.empty()` |
|
||||
| `Matcher.nothing()` | Match no entities | `Matcher.nothing()` |
|
||||
| `Matcher.complex()` | Create complex query builder | `Matcher.complex()` |
|
||||
|
||||
## Chainable Methods
|
||||
|
||||
| Method | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `.all(...types)` | Add required components | `.all(Position)` |
|
||||
| `.any(...types)` | Add optional components (at least one) | `.any(Weapon, Magic)` |
|
||||
| `.none(...types)` | Add excluded components | `.none(Dead)` |
|
||||
| `.exclude(...types)` | Alias for `.none()` | `.exclude(Disabled)` |
|
||||
| `.one(...types)` | Alias for `.any()` | `.one(Player, Enemy)` |
|
||||
| `.withTag(tag)` | Add tag condition | `.withTag(1)` |
|
||||
| `.withName(name)` | Add name condition | `.withName("Boss")` |
|
||||
| `.withComponent(type)` | Add single component condition | `.withComponent(Health)` |
|
||||
|
||||
## Utility Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `.getCondition()` | Get query condition (read-only) |
|
||||
| `.isEmpty()` | Check if empty condition |
|
||||
| `.isNothing()` | Check if nothing matcher |
|
||||
| `.clone()` | Clone matcher |
|
||||
| `.reset()` | Reset all conditions |
|
||||
| `.toString()` | Get string representation |
|
||||
|
||||
## Common Combination Examples
|
||||
|
||||
```typescript
|
||||
// Basic movement system
|
||||
Matcher.all(Position, Velocity)
|
||||
|
||||
// Attackable living entities
|
||||
Matcher.all(Position, Health)
|
||||
.any(Weapon, Magic)
|
||||
.none(Dead, Disabled)
|
||||
|
||||
// All tagged enemies
|
||||
Matcher.byTag(Tags.ENEMY)
|
||||
.all(AIComponent)
|
||||
|
||||
// System only needing lifecycle
|
||||
Matcher.nothing()
|
||||
```
|
||||
|
||||
## Query by Tag
|
||||
|
||||
```typescript
|
||||
class PlayerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Query entities with specific tag
|
||||
super(Matcher.empty().withTag(Tags.PLAYER));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process player entities
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Query by Name
|
||||
|
||||
```typescript
|
||||
class BossSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Query entities with specific name
|
||||
super(Matcher.empty().withName('Boss'));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process entities named 'Boss'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Matcher is Immutable
|
||||
|
||||
```typescript
|
||||
const matcher = Matcher.empty().all(PositionComponent);
|
||||
|
||||
// Chain calls return new Matcher instances
|
||||
const matcher2 = matcher.any(VelocityComponent);
|
||||
|
||||
// Original matcher unchanged
|
||||
console.log(matcher === matcher2); // false
|
||||
```
|
||||
|
||||
### Query Results are Read-Only
|
||||
|
||||
```typescript
|
||||
const result = querySystem.queryAll(PositionComponent);
|
||||
|
||||
// Don't modify returned array
|
||||
result.entities.push(someEntity); // Wrong!
|
||||
|
||||
// If modification needed, copy first
|
||||
const mutableArray = [...result.entities];
|
||||
mutableArray.push(someEntity); // Correct
|
||||
```
|
||||
273
docs/src/content/docs/en/guide/entity/component-operations.md
Normal file
273
docs/src/content/docs/en/guide/entity/component-operations.md
Normal file
@@ -0,0 +1,273 @@
|
||||
---
|
||||
title: "Component Operations"
|
||||
description: "Detailed guide to adding, getting, and removing entity components"
|
||||
---
|
||||
|
||||
Entities gain functionality by adding components. This section details all component operation APIs.
|
||||
|
||||
## Adding Components
|
||||
|
||||
### addComponent
|
||||
|
||||
Add an already-created component instance:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
const player = scene.createEntity("Player");
|
||||
const position = new Position(100, 200);
|
||||
player.addComponent(position);
|
||||
```
|
||||
|
||||
### createComponent
|
||||
|
||||
Pass the component type and constructor arguments directly—the entity creates the instance (recommended):
|
||||
|
||||
```typescript
|
||||
// Create and add component
|
||||
const position = player.createComponent(Position, 100, 200);
|
||||
const health = player.createComponent(Health, 150);
|
||||
|
||||
// Equivalent to:
|
||||
// const position = new Position(100, 200);
|
||||
// player.addComponent(position);
|
||||
```
|
||||
|
||||
### addComponents
|
||||
|
||||
Add multiple components at once:
|
||||
|
||||
```typescript
|
||||
const components = player.addComponents([
|
||||
new Position(100, 200),
|
||||
new Health(150),
|
||||
new Velocity(0, 0)
|
||||
]);
|
||||
```
|
||||
|
||||
:::note[Important Notes]
|
||||
- An entity cannot have two components of the same type—an exception will be thrown
|
||||
- The entity must be added to a scene before adding components
|
||||
:::
|
||||
|
||||
## Getting Components
|
||||
|
||||
### getComponent
|
||||
|
||||
Get a component of a specific type:
|
||||
|
||||
```typescript
|
||||
// Returns Position | null
|
||||
const position = player.getComponent(Position);
|
||||
|
||||
if (position) {
|
||||
position.x += 10;
|
||||
position.y += 20;
|
||||
}
|
||||
```
|
||||
|
||||
### hasComponent
|
||||
|
||||
Check if an entity has a specific component type:
|
||||
|
||||
```typescript
|
||||
if (player.hasComponent(Position)) {
|
||||
const position = player.getComponent(Position)!;
|
||||
// Use ! because we confirmed it exists
|
||||
}
|
||||
```
|
||||
|
||||
### getComponents
|
||||
|
||||
Get all components of a specific type (for multi-component scenarios):
|
||||
|
||||
```typescript
|
||||
const allHealthComponents = player.getComponents(Health);
|
||||
```
|
||||
|
||||
### getComponentByType
|
||||
|
||||
Get components with inheritance support using `instanceof` checking:
|
||||
|
||||
```typescript
|
||||
// Find CompositeNodeComponent or any subclass
|
||||
const composite = entity.getComponentByType(CompositeNodeComponent);
|
||||
if (composite) {
|
||||
// composite could be SequenceNode, SelectorNode, etc.
|
||||
}
|
||||
```
|
||||
|
||||
Difference from `getComponent()`:
|
||||
|
||||
| Method | Lookup Method | Performance | Use Case |
|
||||
|--------|---------------|-------------|----------|
|
||||
| `getComponent` | Exact type match (bitmask) | High | Known exact type |
|
||||
| `getComponentByType` | `instanceof` check | Lower | Need inheritance support |
|
||||
|
||||
### getOrCreateComponent
|
||||
|
||||
Get or create a component—automatically creates if it doesn't exist:
|
||||
|
||||
```typescript
|
||||
// Ensure entity has Position component
|
||||
const position = player.getOrCreateComponent(Position, 0, 0);
|
||||
position.x = 100;
|
||||
|
||||
// If exists, returns existing component
|
||||
// If not, creates new component with (0, 0) args
|
||||
```
|
||||
|
||||
### components Property
|
||||
|
||||
Get all entity components (read-only):
|
||||
|
||||
```typescript
|
||||
const allComponents = player.components; // readonly Component[]
|
||||
|
||||
allComponents.forEach(component => {
|
||||
console.log(component.constructor.name);
|
||||
});
|
||||
```
|
||||
|
||||
## Removing Components
|
||||
|
||||
### removeComponent
|
||||
|
||||
Remove by component instance:
|
||||
|
||||
```typescript
|
||||
const healthComponent = player.getComponent(Health);
|
||||
if (healthComponent) {
|
||||
player.removeComponent(healthComponent);
|
||||
}
|
||||
```
|
||||
|
||||
### removeComponentByType
|
||||
|
||||
Remove by component type:
|
||||
|
||||
```typescript
|
||||
const removedHealth = player.removeComponentByType(Health);
|
||||
if (removedHealth) {
|
||||
console.log("Health component removed");
|
||||
}
|
||||
```
|
||||
|
||||
### removeComponentsByTypes
|
||||
|
||||
Remove multiple component types at once:
|
||||
|
||||
```typescript
|
||||
const removedComponents = player.removeComponentsByTypes([
|
||||
Position,
|
||||
Health,
|
||||
Velocity
|
||||
]);
|
||||
```
|
||||
|
||||
### removeAllComponents
|
||||
|
||||
Remove all components:
|
||||
|
||||
```typescript
|
||||
player.removeAllComponents();
|
||||
```
|
||||
|
||||
## Change Detection
|
||||
|
||||
### markDirty
|
||||
|
||||
Mark components as modified for frame-level change detection:
|
||||
|
||||
```typescript
|
||||
const pos = entity.getComponent(Position)!;
|
||||
pos.x = 100;
|
||||
entity.markDirty(pos);
|
||||
|
||||
// Or mark multiple components
|
||||
const vel = entity.getComponent(Velocity)!;
|
||||
entity.markDirty(pos, vel);
|
||||
```
|
||||
|
||||
Use with reactive queries:
|
||||
|
||||
```typescript
|
||||
// Query for components modified this frame
|
||||
const changedQuery = scene.createReactiveQuery({
|
||||
all: [Position],
|
||||
changed: [Position] // Only match modified this frame
|
||||
});
|
||||
|
||||
for (const entity of changedQuery.getEntities()) {
|
||||
// Handle entities with position changes
|
||||
}
|
||||
```
|
||||
|
||||
## Component Mask
|
||||
|
||||
Each entity maintains a component bitmask for efficient `hasComponent` checks:
|
||||
|
||||
```typescript
|
||||
// Get component mask (internal use)
|
||||
const mask = entity.componentMask;
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
constructor(public x = 0, public y = 0) { super(); }
|
||||
}
|
||||
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
constructor(public current = 100, public max = 100) { super(); }
|
||||
}
|
||||
|
||||
// Create entity and add components
|
||||
const player = scene.createEntity("Player");
|
||||
player.createComponent(Position, 100, 200);
|
||||
player.createComponent(Health, 150, 150);
|
||||
|
||||
// Get and modify component
|
||||
const position = player.getComponent(Position);
|
||||
if (position) {
|
||||
position.x += 10;
|
||||
player.markDirty(position);
|
||||
}
|
||||
|
||||
// Get or create component
|
||||
const velocity = player.getOrCreateComponent(Velocity, 0, 0);
|
||||
|
||||
// Check component existence
|
||||
if (player.hasComponent(Health)) {
|
||||
const health = player.getComponent(Health)!;
|
||||
health.current -= 10;
|
||||
}
|
||||
|
||||
// Remove component
|
||||
player.removeComponentByType(Velocity);
|
||||
|
||||
// List all components
|
||||
console.log(player.components.map(c => c.constructor.name));
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Entity Handle](/en/guide/entity/entity-handle/) - Safe cross-frame entity references
|
||||
- [Component System](/en/guide/component/) - Component definition and lifecycle
|
||||
265
docs/src/content/docs/en/guide/entity/entity-handle.md
Normal file
265
docs/src/content/docs/en/guide/entity/entity-handle.md
Normal file
@@ -0,0 +1,265 @@
|
||||
---
|
||||
title: "Entity Handle"
|
||||
description: "Using EntityHandle to safely reference entities and avoid referencing destroyed entities"
|
||||
---
|
||||
|
||||
Entity handles (EntityHandle) provide a safe way to reference entities, solving the "referencing destroyed entities" problem.
|
||||
|
||||
## The Problem
|
||||
|
||||
Imagine your AI system needs to track a target enemy:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Storing entity reference directly
|
||||
class AISystem extends EntitySystem {
|
||||
private targetEnemy: Entity | null = null;
|
||||
|
||||
setTarget(enemy: Entity) {
|
||||
this.targetEnemy = enemy;
|
||||
}
|
||||
|
||||
process() {
|
||||
if (this.targetEnemy) {
|
||||
// Danger! Enemy might be destroyed but reference still exists
|
||||
// Worse: This memory location might be reused by a new entity
|
||||
const health = this.targetEnemy.getComponent(Health);
|
||||
// Might operate on wrong entity!
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What is EntityHandle
|
||||
|
||||
EntityHandle is a numeric entity identifier containing:
|
||||
- **Index**: Entity's position in the array
|
||||
- **Generation**: Number of times the entity slot has been reused
|
||||
|
||||
When an entity is destroyed, even if its index is reused by a new entity, the generation increases, invalidating old handles.
|
||||
|
||||
```typescript
|
||||
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
|
||||
|
||||
// Each entity gets a handle when created
|
||||
const handle: EntityHandle = entity.handle;
|
||||
|
||||
// Null handle constant
|
||||
const emptyHandle = NULL_HANDLE;
|
||||
|
||||
// Check if handle is non-null
|
||||
if (isValidHandle(handle)) {
|
||||
// Handle is valid
|
||||
}
|
||||
```
|
||||
|
||||
## The Correct Approach
|
||||
|
||||
```typescript
|
||||
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
|
||||
|
||||
class AISystem extends EntitySystem {
|
||||
// ✅ Store handle instead of entity reference
|
||||
private targetHandle: EntityHandle = NULL_HANDLE;
|
||||
|
||||
setTarget(enemy: Entity) {
|
||||
this.targetHandle = enemy.handle;
|
||||
}
|
||||
|
||||
process() {
|
||||
if (!isValidHandle(this.targetHandle)) {
|
||||
return; // No target
|
||||
}
|
||||
|
||||
// Get entity via handle (auto-validates)
|
||||
const enemy = this.scene.findEntityByHandle(this.targetHandle);
|
||||
|
||||
if (!enemy) {
|
||||
// Enemy destroyed, clear reference
|
||||
this.targetHandle = NULL_HANDLE;
|
||||
return;
|
||||
}
|
||||
|
||||
// Safe operation
|
||||
const health = enemy.getComponent(Health);
|
||||
if (health) {
|
||||
// Deal damage to enemy
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Getting Handle
|
||||
|
||||
```typescript
|
||||
// Get handle from entity
|
||||
const handle = entity.handle;
|
||||
```
|
||||
|
||||
### Validating Handle
|
||||
|
||||
```typescript
|
||||
import { isValidHandle, NULL_HANDLE } from '@esengine/ecs-framework';
|
||||
|
||||
// Check if handle is non-null
|
||||
if (isValidHandle(handle)) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Check if entity is alive
|
||||
const alive = scene.handleManager.isAlive(handle);
|
||||
```
|
||||
|
||||
### Getting Entity by Handle
|
||||
|
||||
```typescript
|
||||
// Returns Entity | null
|
||||
const entity = scene.findEntityByHandle(handle);
|
||||
|
||||
if (entity) {
|
||||
// Entity exists and is valid
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example: Skill Target Locking
|
||||
|
||||
```typescript
|
||||
import {
|
||||
EntitySystem,
|
||||
Entity,
|
||||
EntityHandle,
|
||||
NULL_HANDLE,
|
||||
isValidHandle
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
@ECSSystem('SkillTargeting')
|
||||
class SkillTargetingSystem extends EntitySystem {
|
||||
// Store multiple target handles
|
||||
private lockedTargets: Map<number, EntityHandle> = new Map();
|
||||
|
||||
// Lock target
|
||||
lockTarget(casterId: number, target: Entity) {
|
||||
this.lockedTargets.set(casterId, target.handle);
|
||||
}
|
||||
|
||||
// Get locked target
|
||||
getLockedTarget(casterId: number): Entity | null {
|
||||
const handle = this.lockedTargets.get(casterId);
|
||||
|
||||
if (!handle || !isValidHandle(handle)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const target = this.scene.findEntityByHandle(handle);
|
||||
|
||||
if (!target) {
|
||||
// Target dead, clear lock
|
||||
this.lockedTargets.delete(casterId);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
// Cast skill
|
||||
castSkill(caster: Entity) {
|
||||
const target = this.getLockedTarget(caster.id);
|
||||
|
||||
if (!target) {
|
||||
console.log('Target lost, skill cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const health = target.getComponent(Health);
|
||||
if (health) {
|
||||
health.current -= 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear target for specific caster
|
||||
clearTarget(casterId: number) {
|
||||
this.lockedTargets.delete(casterId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
| Scenario | Recommended Approach |
|
||||
|----------|---------------------|
|
||||
| Same-frame temporary use | Direct `Entity` reference |
|
||||
| Cross-frame storage (AI target, skill target) | Use `EntityHandle` |
|
||||
| Serialization/save | Use `EntityHandle` (numeric type) |
|
||||
| Network sync | Use `EntityHandle` (directly transferable) |
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- EntityHandle is a numeric type with small memory footprint
|
||||
- `findEntityByHandle` is O(1) operation
|
||||
- Safer and more reliable than checking `entity.isDestroyed` every frame
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Optional Target Reference
|
||||
|
||||
```typescript
|
||||
class FollowComponent extends Component {
|
||||
private _targetHandle: EntityHandle = NULL_HANDLE;
|
||||
|
||||
setTarget(target: Entity | null) {
|
||||
this._targetHandle = target?.handle ?? NULL_HANDLE;
|
||||
}
|
||||
|
||||
getTarget(scene: IScene): Entity | null {
|
||||
if (!isValidHandle(this._targetHandle)) {
|
||||
return null;
|
||||
}
|
||||
return scene.findEntityByHandle(this._targetHandle);
|
||||
}
|
||||
|
||||
hasTarget(): boolean {
|
||||
return isValidHandle(this._targetHandle);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Target Tracking
|
||||
|
||||
```typescript
|
||||
class MultiTargetComponent extends Component {
|
||||
private targets: EntityHandle[] = [];
|
||||
|
||||
addTarget(target: Entity) {
|
||||
this.targets.push(target.handle);
|
||||
}
|
||||
|
||||
removeTarget(target: Entity) {
|
||||
const index = this.targets.indexOf(target.handle);
|
||||
if (index >= 0) {
|
||||
this.targets.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
getValidTargets(scene: IScene): Entity[] {
|
||||
const valid: Entity[] = [];
|
||||
const stillValid: EntityHandle[] = [];
|
||||
|
||||
for (const handle of this.targets) {
|
||||
const entity = scene.findEntityByHandle(handle);
|
||||
if (entity) {
|
||||
valid.push(entity);
|
||||
stillValid.push(handle);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up invalid handles
|
||||
this.targets = stillValid;
|
||||
return valid;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Lifecycle](/en/guide/entity/lifecycle/) - Entity destruction and persistence
|
||||
- [Entity Reference](/en/guide/component/entity-ref/) - Entity reference decorators in components
|
||||
174
docs/src/content/docs/en/guide/entity/index.md
Normal file
174
docs/src/content/docs/en/guide/entity/index.md
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
title: "Entity Overview"
|
||||
description: "Basic concepts and usage of entities in ECS architecture"
|
||||
---
|
||||
|
||||
In ECS architecture, an Entity is a fundamental object in the game world. Entities contain no game logic or data themselves—they are simply containers that combine different components to achieve various functionalities.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
An entity is a lightweight object primarily used for:
|
||||
- Acting as a container for components
|
||||
- Providing unique identifiers (ID and persistentId)
|
||||
- Managing component lifecycles
|
||||
|
||||
:::tip[About Parent-Child Hierarchy]
|
||||
Parent-child relationships between entities are managed through `HierarchyComponent` and `HierarchySystem`, not built-in Entity properties. This design follows ECS composition principles—only entities that need hierarchy relationships add this component.
|
||||
|
||||
See the [Hierarchy System](/en/guide/hierarchy/) documentation for details.
|
||||
:::
|
||||
|
||||
## Creating Entities
|
||||
|
||||
**Entities must be created through the scene, not manually.**
|
||||
|
||||
```typescript
|
||||
// Correct: Create entity through scene
|
||||
const player = scene.createEntity("Player");
|
||||
|
||||
// ❌ Wrong: Manual creation
|
||||
// const entity = new Entity("MyEntity", 1);
|
||||
```
|
||||
|
||||
Creating through the scene ensures:
|
||||
- Entity is properly added to the scene's entity management system
|
||||
- Entity is added to the query system for use by systems
|
||||
- Entity gets the correct scene reference
|
||||
- Related lifecycle events are triggered
|
||||
|
||||
### Batch Creation
|
||||
|
||||
The framework provides high-performance batch creation:
|
||||
|
||||
```typescript
|
||||
// Batch create 100 bullet entities
|
||||
const bullets = scene.createEntities(100, "Bullet");
|
||||
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.createComponent(Position, Math.random() * 800, Math.random() * 600);
|
||||
bullet.createComponent(Velocity, Math.random() * 100, Math.random() * 100);
|
||||
});
|
||||
```
|
||||
|
||||
`createEntities()` batches ID allocation, optimizes query system updates, and reduces system cache clearing.
|
||||
|
||||
## Entity Identifiers
|
||||
|
||||
Each entity has three types of identifiers:
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `id` | `number` | Runtime unique identifier for fast lookups |
|
||||
| `persistentId` | `string` | GUID for maintaining reference consistency during serialization |
|
||||
| `handle` | `EntityHandle` | Lightweight handle, see [Entity Handle](/en/guide/entity/entity-handle/) |
|
||||
|
||||
```typescript
|
||||
const entity = scene.createEntity("Player");
|
||||
|
||||
console.log(entity.id); // 1
|
||||
console.log(entity.persistentId); // "a1b2c3d4-..."
|
||||
console.log(entity.handle); // Numeric handle
|
||||
```
|
||||
|
||||
## Entity Properties
|
||||
|
||||
### Name and Tag
|
||||
|
||||
```typescript
|
||||
// Name - for debugging and lookup
|
||||
entity.name = "Player";
|
||||
|
||||
// Tag - for fast categorization and querying
|
||||
entity.tag = 1; // Player tag
|
||||
enemy.tag = 2; // Enemy tag
|
||||
```
|
||||
|
||||
### State Control
|
||||
|
||||
```typescript
|
||||
// Enable/disable state
|
||||
entity.enabled = false;
|
||||
|
||||
// Active state
|
||||
entity.active = false;
|
||||
|
||||
// Update order (lower values have higher priority)
|
||||
entity.updateOrder = 10;
|
||||
```
|
||||
|
||||
## Finding Entities
|
||||
|
||||
The scene provides multiple ways to find entities:
|
||||
|
||||
```typescript
|
||||
// Find by name
|
||||
const player = scene.findEntity("Player");
|
||||
// Or use alias
|
||||
const player2 = scene.getEntityByName("Player");
|
||||
|
||||
// Find by ID
|
||||
const entity = scene.findEntityById(123);
|
||||
|
||||
// Find all entities by tag
|
||||
const enemies = scene.findEntitiesByTag(2);
|
||||
// Or use alias
|
||||
const allEnemies = scene.getEntitiesByTag(2);
|
||||
|
||||
// Find by handle
|
||||
const entity = scene.findEntityByHandle(handle);
|
||||
```
|
||||
|
||||
## Entity Events
|
||||
|
||||
Entity changes trigger events:
|
||||
|
||||
```typescript
|
||||
// Listen for component additions
|
||||
scene.eventSystem.on('component:added', (data) => {
|
||||
console.log(`${data.entityName} added ${data.componentType}`);
|
||||
});
|
||||
|
||||
// Listen for component removals
|
||||
scene.eventSystem.on('component:removed', (data) => {
|
||||
console.log(`${data.entityName} removed ${data.componentType}`);
|
||||
});
|
||||
|
||||
// Listen for entity creation
|
||||
scene.eventSystem.on('entity:created', (data) => {
|
||||
console.log(`Entity created: ${data.entityName}`);
|
||||
});
|
||||
|
||||
// Listen for active state changes
|
||||
scene.eventSystem.on('entity:activeChanged', (data) => {
|
||||
console.log(`${data.entity.name} active: ${data.active}`);
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```typescript
|
||||
// Get entity debug info
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
// {
|
||||
// name: "Player",
|
||||
// id: 1,
|
||||
// persistentId: "a1b2c3d4-...",
|
||||
// enabled: true,
|
||||
// active: true,
|
||||
// destroyed: false,
|
||||
// componentCount: 3,
|
||||
// componentTypes: ["Position", "Health", "Velocity"],
|
||||
// ...
|
||||
// }
|
||||
|
||||
// Entity string representation
|
||||
console.log(entity.toString());
|
||||
// "Entity[Player:1:a1b2c3d4]"
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Component Operations](/en/guide/entity/component-operations/) - Add, get, and remove components
|
||||
- [Entity Handle](/en/guide/entity/entity-handle/) - Safe entity reference method
|
||||
- [Lifecycle](/en/guide/entity/lifecycle/) - Destruction and persistence
|
||||
238
docs/src/content/docs/en/guide/entity/lifecycle.md
Normal file
238
docs/src/content/docs/en/guide/entity/lifecycle.md
Normal file
@@ -0,0 +1,238 @@
|
||||
---
|
||||
title: "Lifecycle"
|
||||
description: "Entity lifecycle management, destruction, and persistence"
|
||||
---
|
||||
|
||||
Entity lifecycle includes three phases: creation, runtime, and destruction. This section covers how to properly manage entity lifecycles.
|
||||
|
||||
## Destroying Entities
|
||||
|
||||
### Basic Destruction
|
||||
|
||||
```typescript
|
||||
// Destroy entity
|
||||
player.destroy();
|
||||
|
||||
// Check if entity is destroyed
|
||||
if (player.isDestroyed) {
|
||||
console.log("Entity has been destroyed");
|
||||
}
|
||||
```
|
||||
|
||||
When destroying an entity:
|
||||
1. All components are removed (triggering `onRemovedFromEntity` callbacks)
|
||||
2. Entity is removed from query systems
|
||||
3. Entity is removed from scene entity list
|
||||
4. All reference tracking is cleaned up
|
||||
|
||||
### Conditional Destruction
|
||||
|
||||
```typescript
|
||||
// Common pattern: Destroy when health depleted
|
||||
const health = enemy.getComponent(Health);
|
||||
if (health && health.current <= 0) {
|
||||
enemy.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
### Destruction Safety
|
||||
|
||||
Destruction is idempotent—multiple calls won't cause errors:
|
||||
|
||||
```typescript
|
||||
player.destroy();
|
||||
player.destroy(); // Safe, no error
|
||||
```
|
||||
|
||||
## Persistent Entities
|
||||
|
||||
By default, entities are destroyed during scene transitions. Persistence allows entities to survive across scenes.
|
||||
|
||||
### Setting Persistence
|
||||
|
||||
```typescript
|
||||
// Method 1: Chain call
|
||||
const player = scene.createEntity('Player')
|
||||
.setPersistent()
|
||||
.createComponent(PlayerComponent);
|
||||
|
||||
// Method 2: Separate call
|
||||
player.setPersistent();
|
||||
|
||||
// Check persistence
|
||||
if (player.isPersistent) {
|
||||
console.log("This is a persistent entity");
|
||||
}
|
||||
```
|
||||
|
||||
### Removing Persistence
|
||||
|
||||
```typescript
|
||||
// Restore to scene-local entity
|
||||
player.setSceneLocal();
|
||||
```
|
||||
|
||||
### Lifecycle Policies
|
||||
|
||||
Entities have two lifecycle policies:
|
||||
|
||||
| Policy | Description |
|
||||
|--------|-------------|
|
||||
| `SceneLocal` | Default, destroyed with scene |
|
||||
| `Persistent` | Survives scene transitions |
|
||||
|
||||
```typescript
|
||||
import { EEntityLifecyclePolicy } from '@esengine/ecs-framework';
|
||||
|
||||
// Get current policy
|
||||
const policy = entity.lifecyclePolicy;
|
||||
|
||||
if (policy === EEntityLifecyclePolicy.Persistent) {
|
||||
// Persistent entity
|
||||
}
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
Persistent entities are suitable for:
|
||||
- Player characters
|
||||
- Global managers
|
||||
- UI entities
|
||||
- Game state that needs to survive scene transitions
|
||||
|
||||
```typescript
|
||||
// Player character
|
||||
const player = scene.createEntity('Player')
|
||||
.setPersistent();
|
||||
|
||||
// Game manager
|
||||
const gameManager = scene.createEntity('GameManager')
|
||||
.setPersistent()
|
||||
.createComponent(GameStateComponent);
|
||||
|
||||
// Score manager
|
||||
const scoreManager = scene.createEntity('ScoreManager')
|
||||
.setPersistent()
|
||||
.createComponent(ScoreComponent);
|
||||
```
|
||||
|
||||
## Scene Transition Behavior
|
||||
|
||||
```typescript
|
||||
// Scene manager switches scenes
|
||||
sceneManager.loadScene('Level2');
|
||||
|
||||
// During transition:
|
||||
// 1. SceneLocal entities are destroyed
|
||||
// 2. Persistent entities migrate to new scene
|
||||
// 3. New scene entities are created
|
||||
```
|
||||
|
||||
:::caution[Note]
|
||||
Persistent entities automatically migrate to the new scene during transitions, but other non-persistent entities they reference may be destroyed. Use [EntityHandle](/en/guide/entity/entity-handle/) to safely handle this situation.
|
||||
:::
|
||||
|
||||
## Reference Cleanup
|
||||
|
||||
The framework provides reference tracking that automatically cleans up references when entities are destroyed:
|
||||
|
||||
```typescript
|
||||
// Reference tracker cleans up all references to this entity on destruction
|
||||
scene.referenceTracker?.clearReferencesTo(entity.id);
|
||||
```
|
||||
|
||||
Using the `@entityRef` decorator handles this automatically:
|
||||
|
||||
```typescript
|
||||
class FollowComponent extends Component {
|
||||
@entityRef()
|
||||
targetId: number | null = null;
|
||||
}
|
||||
|
||||
// When target is destroyed, targetId is automatically set to null
|
||||
```
|
||||
|
||||
See [Component References](/en/guide/component/entity-ref/) for details.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Destroy Unneeded Entities Promptly
|
||||
|
||||
```typescript
|
||||
// Destroy bullets that fly off screen
|
||||
if (position.x < 0 || position.x > screenWidth) {
|
||||
bullet.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Object Pools Instead of Frequent Create/Destroy
|
||||
|
||||
```typescript
|
||||
class BulletPool {
|
||||
private pool: Entity[] = [];
|
||||
|
||||
acquire(scene: Scene): Entity {
|
||||
if (this.pool.length > 0) {
|
||||
const bullet = this.pool.pop()!;
|
||||
bullet.enabled = true;
|
||||
return bullet;
|
||||
}
|
||||
return scene.createEntity('Bullet');
|
||||
}
|
||||
|
||||
release(bullet: Entity) {
|
||||
bullet.enabled = false;
|
||||
this.pool.push(bullet);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Persistence Sparingly
|
||||
|
||||
Only use persistence for entities that truly need to survive scene transitions—too many persistent entities increase memory usage.
|
||||
|
||||
### 4. Clean Up References Before Destruction
|
||||
|
||||
```typescript
|
||||
// Notify related systems before destruction
|
||||
const aiSystem = scene.getSystem(AISystem);
|
||||
aiSystem?.clearTarget(enemy.id);
|
||||
|
||||
enemy.destroy();
|
||||
```
|
||||
|
||||
## Lifecycle Events
|
||||
|
||||
You can listen to entity destruction events:
|
||||
|
||||
```typescript
|
||||
// Method 1: Through event system
|
||||
scene.eventSystem.on('entity:destroyed', (data) => {
|
||||
console.log(`Entity ${data.entityName} destroyed`);
|
||||
});
|
||||
|
||||
// Method 2: In component
|
||||
class MyComponent extends Component {
|
||||
onRemovedFromEntity() {
|
||||
console.log('Component removed, entity may be destroying');
|
||||
// Clean up resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```typescript
|
||||
// Get entity status
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log({
|
||||
destroyed: debugInfo.destroyed,
|
||||
enabled: debugInfo.enabled,
|
||||
active: debugInfo.active
|
||||
});
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Component Operations](/en/guide/entity/component-operations/) - Adding and removing components
|
||||
- [Scene Management](/en/guide/scene/) - Scene switching and management
|
||||
260
docs/src/content/docs/en/guide/event-system.md
Normal file
260
docs/src/content/docs/en/guide/event-system.md
Normal file
@@ -0,0 +1,260 @@
|
||||
---
|
||||
title: "Event System"
|
||||
description: "Type-safe event system with sync/async events, priorities, and batching"
|
||||
---
|
||||
|
||||
The ECS framework includes a powerful type-safe event system supporting sync/async events, priorities, batching, and more advanced features. The event system is the core mechanism for inter-component and inter-system communication.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
The event system implements a publish-subscribe pattern with these core concepts:
|
||||
- **Event Publisher**: Object that emits events
|
||||
- **Event Listener**: Object that listens for and handles specific events
|
||||
- **Event Type**: String identifier to distinguish different event types
|
||||
- **Event Data**: Related information carried by the event
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Using Event System in Scene
|
||||
|
||||
Each scene has a built-in event system:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Listen for events
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('score_changed', this.onScoreChanged.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: { player: Entity, cause: string }): void {
|
||||
console.log(`Player died, cause: ${data.cause}`);
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: { enemy: Entity, position: { x: number, y: number } }): void {
|
||||
console.log('Enemy spawned at:', data.position);
|
||||
}
|
||||
|
||||
private onScoreChanged(data: { newScore: number, oldScore: number }): void {
|
||||
console.log(`Score changed: ${data.oldScore} -> ${data.newScore}`);
|
||||
}
|
||||
|
||||
// Emit events in systems
|
||||
someGameLogic(): void {
|
||||
this.eventSystem.emitSync('score_changed', {
|
||||
newScore: 1000,
|
||||
oldScore: 800
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Events in Systems
|
||||
|
||||
Systems can conveniently listen for and send events:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, Combat));
|
||||
}
|
||||
|
||||
protected onInitialize(): void {
|
||||
// Use system's event listener method (auto-cleanup)
|
||||
this.addEventListener('player_attack', this.onPlayerAttack.bind(this));
|
||||
this.addEventListener('enemy_death', this.onEnemyDeath.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerAttack(data: { damage: number, target: Entity }): void {
|
||||
const health = data.target.getComponent(Health);
|
||||
if (health) {
|
||||
health.current -= data.damage;
|
||||
|
||||
if (health.current <= 0) {
|
||||
this.scene?.eventSystem.emitSync('enemy_death', {
|
||||
enemy: data.target,
|
||||
killer: 'player'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onEnemyDeath(data: { enemy: Entity, killer: string }): void {
|
||||
data.enemy.destroy();
|
||||
this.scene?.eventSystem.emitSync('experience_gained', {
|
||||
amount: 100,
|
||||
source: 'enemy_kill'
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### One-Time Listeners
|
||||
|
||||
```typescript
|
||||
// Listen only once
|
||||
this.eventSystem.once('game_start', this.onGameStart.bind(this));
|
||||
|
||||
// Or use configuration object
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this), {
|
||||
once: true
|
||||
});
|
||||
```
|
||||
|
||||
### Priority Control
|
||||
|
||||
```typescript
|
||||
// Higher priority listeners execute first
|
||||
this.eventSystem.on('damage_dealt', this.onDamageDealt.bind(this), {
|
||||
priority: 100 // High priority
|
||||
});
|
||||
|
||||
this.eventSystem.on('damage_dealt', this.updateUI.bind(this), {
|
||||
priority: 0 // Default priority
|
||||
});
|
||||
|
||||
this.eventSystem.on('damage_dealt', this.logDamage.bind(this), {
|
||||
priority: -100 // Low priority, executes last
|
||||
});
|
||||
```
|
||||
|
||||
### Async Event Handling
|
||||
|
||||
```typescript
|
||||
protected initialize(): void {
|
||||
this.eventSystem.onAsync('save_game', this.onSaveGame.bind(this));
|
||||
}
|
||||
|
||||
private async onSaveGame(data: { saveSlot: number }): Promise<void> {
|
||||
console.log(`Saving game to slot ${data.saveSlot}`);
|
||||
await this.saveGameData(data.saveSlot);
|
||||
console.log('Game saved');
|
||||
}
|
||||
|
||||
// Emit async event
|
||||
public async triggerSave(): Promise<void> {
|
||||
await this.eventSystem.emit('save_game', { saveSlot: 1 });
|
||||
console.log('All async save operations complete');
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
|
||||
For high-frequency events, use batching to improve performance:
|
||||
|
||||
```typescript
|
||||
protected onInitialize(): void {
|
||||
// Configure batch processing for position updates
|
||||
this.scene?.eventSystem.setBatchConfig('position_updated', {
|
||||
batchSize: 50,
|
||||
delay: 16,
|
||||
enabled: true
|
||||
});
|
||||
|
||||
// Listen for batch events
|
||||
this.addEventListener('position_updated:batch', this.onPositionBatch.bind(this));
|
||||
}
|
||||
|
||||
private onPositionBatch(batchData: any): void {
|
||||
console.log(`Batch processing ${batchData.count} position updates`);
|
||||
for (const event of batchData.events) {
|
||||
this.updateMinimap(event.entityId, event.position);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Global Event Bus
|
||||
|
||||
For cross-scene event communication:
|
||||
|
||||
```typescript
|
||||
import { GlobalEventBus } from '@esengine/ecs-framework';
|
||||
|
||||
class GameManager {
|
||||
private eventBus = GlobalEventBus.getInstance();
|
||||
|
||||
constructor() {
|
||||
this.eventBus.on('player_level_up', this.onPlayerLevelUp.bind(this));
|
||||
this.eventBus.on('achievement_unlocked', this.onAchievementUnlocked.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerLevelUp(data: { level: number }): void {
|
||||
console.log(`Player leveled up to ${data.level}!`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Event Naming Convention
|
||||
|
||||
```typescript
|
||||
// ✅ Good naming
|
||||
this.eventSystem.emitSync('player:health_changed', data);
|
||||
this.eventSystem.emitSync('enemy:spawned', data);
|
||||
this.eventSystem.emitSync('ui:score_updated', data);
|
||||
|
||||
// ❌ Avoid
|
||||
this.eventSystem.emitSync('event1', data);
|
||||
this.eventSystem.emitSync('update', data);
|
||||
```
|
||||
|
||||
### 2. Type-Safe Event Data
|
||||
|
||||
```typescript
|
||||
interface PlayerHealthChangedEvent {
|
||||
entityId: number;
|
||||
oldHealth: number;
|
||||
newHealth: number;
|
||||
cause: 'damage' | 'healing';
|
||||
}
|
||||
|
||||
class HealthSystem extends EntitySystem {
|
||||
private onHealthChanged(data: PlayerHealthChangedEvent): void {
|
||||
// TypeScript provides full type checking
|
||||
console.log(`Health changed: ${data.oldHealth} -> ${data.newHealth}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Avoid Event Loops
|
||||
|
||||
```typescript
|
||||
// ❌ Avoid: May cause infinite loop
|
||||
private onScoreChanged(data: any): void {
|
||||
this.scene?.eventSystem.emitSync('score_changed', newData); // Dangerous!
|
||||
}
|
||||
|
||||
// ✅ Correct: Use guard flag
|
||||
private isProcessingScore = false;
|
||||
|
||||
private onScoreChanged(data: any): void {
|
||||
if (this.isProcessingScore) return;
|
||||
|
||||
this.isProcessingScore = true;
|
||||
this.updateUI(data);
|
||||
this.isProcessingScore = false;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Clean Up Event Listeners
|
||||
|
||||
```typescript
|
||||
class TemporaryUI {
|
||||
private listenerId: string;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
this.listenerId = scene.eventSystem.on('ui_update', this.onUpdate.bind(this));
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
if (this.listenerId) {
|
||||
scene.eventSystem.off('ui_update', this.listenerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
443
docs/src/content/docs/en/guide/getting-started.md
Normal file
443
docs/src/content/docs/en/guide/getting-started.md
Normal file
@@ -0,0 +1,443 @@
|
||||
---
|
||||
title: "Quick Start"
|
||||
---
|
||||
|
||||
This guide will help you get started with ECS Framework, from installation to creating your first ECS application.
|
||||
|
||||
## Installation
|
||||
|
||||
### Using CLI (Recommended)
|
||||
|
||||
The easiest way to add ECS to your existing project:
|
||||
|
||||
```bash
|
||||
# In your project directory
|
||||
npx @esengine/cli init
|
||||
```
|
||||
|
||||
The CLI automatically detects your project type (Cocos Creator 2.x/3.x, LayaAir 3.x, or Node.js) and generates the necessary integration code, including:
|
||||
|
||||
- `ECSManager` component/script - Manages ECS lifecycle
|
||||
- Example components and systems - Helps you get started quickly
|
||||
- Automatic dependency installation
|
||||
|
||||
### Manual NPM Installation
|
||||
|
||||
If you prefer manual configuration:
|
||||
|
||||
```bash
|
||||
# Using npm
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## Initialize Core
|
||||
|
||||
### Basic Initialization
|
||||
|
||||
The core of ECS Framework is the `Core` class, a singleton that manages the entire framework lifecycle.
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework'
|
||||
|
||||
// Method 1: Using config object (recommended)
|
||||
const core = Core.create({
|
||||
debug: true, // Enable debug mode for detailed logs and performance monitoring
|
||||
debugConfig: { // Optional: Advanced debug configuration
|
||||
enabled: false, // Whether to enable WebSocket debug server
|
||||
websocketUrl: 'ws://localhost:8080',
|
||||
debugFrameRate: 30, // Debug data send frame rate
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Method 2: Simplified creation (backward compatible)
|
||||
const core = Core.create(true); // Equivalent to { debug: true }
|
||||
|
||||
// Method 3: Production environment configuration
|
||||
const core = Core.create({
|
||||
debug: false // Disable debug in production
|
||||
});
|
||||
```
|
||||
|
||||
### Core Configuration Details
|
||||
|
||||
```typescript
|
||||
interface ICoreConfig {
|
||||
/** Enable debug mode - affects log level and performance monitoring */
|
||||
debug?: boolean;
|
||||
|
||||
/** Advanced debug configuration - for dev tools integration */
|
||||
debugConfig?: {
|
||||
enabled: boolean; // Enable debug server
|
||||
websocketUrl: string; // WebSocket server URL
|
||||
autoReconnect?: boolean; // Auto reconnect
|
||||
debugFrameRate?: 60 | 30 | 15; // Debug data send frame rate
|
||||
channels: { // Data channel configuration
|
||||
entities: boolean; // Entity data
|
||||
systems: boolean; // System data
|
||||
performance: boolean; // Performance data
|
||||
components: boolean; // Component data
|
||||
scenes: boolean; // Scene data
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Core Instance Management
|
||||
|
||||
Core uses singleton pattern, accessible via static property after creation:
|
||||
|
||||
```typescript
|
||||
// Create instance
|
||||
const core = Core.create(true);
|
||||
|
||||
// Get created instance
|
||||
const instance = Core.Instance; // Returns current instance, null if not created
|
||||
```
|
||||
|
||||
### Game Loop Integration
|
||||
|
||||
**Important**: Before creating entities and systems, you need to understand how to integrate ECS Framework into your game engine.
|
||||
|
||||
`Core.update(deltaTime)` is the framework heartbeat, must be called every frame. It handles:
|
||||
- Updating the built-in Time class
|
||||
- Updating all global managers (timers, object pools, etc.)
|
||||
- Updating all entity systems in all scenes
|
||||
- Processing entity creation and destruction
|
||||
- Collecting performance data (in debug mode)
|
||||
|
||||
See engine integration examples: [Game Engine Integration](#game-engine-integration)
|
||||
|
||||
## Create Your First ECS Application
|
||||
|
||||
### 1. Define Components
|
||||
|
||||
Components are pure data containers that store entity state:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework'
|
||||
|
||||
// Position component
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0
|
||||
y: number = 0
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super()
|
||||
this.x = x
|
||||
this.y = y
|
||||
}
|
||||
}
|
||||
|
||||
// Velocity component
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0
|
||||
dy: number = 0
|
||||
|
||||
constructor(dx: number = 0, dy: number = 0) {
|
||||
super()
|
||||
this.dx = dx
|
||||
this.dy = dy
|
||||
}
|
||||
}
|
||||
|
||||
// Sprite component
|
||||
@ECSComponent('Sprite')
|
||||
class Sprite extends Component {
|
||||
texture: string = ''
|
||||
width: number = 32
|
||||
height: number = 32
|
||||
|
||||
constructor(texture: string, width: number = 32, height: number = 32) {
|
||||
super()
|
||||
this.texture = texture
|
||||
this.width = width
|
||||
this.height = height
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create Entity Systems
|
||||
|
||||
Systems contain game logic and process entities with specific components. ECS Framework provides Matcher-based entity filtering:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, Time, ECSSystem } from '@esengine/ecs-framework'
|
||||
|
||||
// Movement system - handles position and velocity
|
||||
@ECSSystem('MovementSystem')
|
||||
class MovementSystem extends EntitySystem {
|
||||
|
||||
constructor() {
|
||||
// Use Matcher to define target entities: must have both Position and Velocity
|
||||
super(Matcher.empty().all(Position, Velocity))
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// process method receives all matching entities
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position)!
|
||||
const velocity = entity.getComponent(Velocity)!
|
||||
|
||||
// Update position (using framework's Time class)
|
||||
position.x += velocity.dx * Time.deltaTime
|
||||
position.y += velocity.dy * Time.deltaTime
|
||||
|
||||
// Boundary check example
|
||||
if (position.x < 0) position.x = 0
|
||||
if (position.y < 0) position.y = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render system - handles visible objects
|
||||
@ECSSystem('RenderSystem')
|
||||
class RenderSystem extends EntitySystem {
|
||||
|
||||
constructor() {
|
||||
// Must have Position and Sprite, optional Velocity (for direction)
|
||||
super(Matcher.empty().all(Position, Sprite).any(Velocity))
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position)!
|
||||
const sprite = entity.getComponent(Sprite)!
|
||||
const velocity = entity.getComponent(Velocity) // May be null
|
||||
|
||||
// Flip sprite based on velocity direction (optional logic)
|
||||
let flipX = false
|
||||
if (velocity && velocity.dx < 0) {
|
||||
flipX = true
|
||||
}
|
||||
|
||||
// Render logic (pseudocode here)
|
||||
this.drawSprite(sprite.texture, position.x, position.y, sprite.width, sprite.height, flipX)
|
||||
}
|
||||
}
|
||||
|
||||
private drawSprite(texture: string, x: number, y: number, width: number, height: number, flipX: boolean = false) {
|
||||
// Actual render implementation depends on your game engine
|
||||
const direction = flipX ? '<-' : '->'
|
||||
console.log(`Render ${texture} at (${x.toFixed(1)}, ${y.toFixed(1)}) direction: ${direction}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 3. Create Scene
|
||||
|
||||
Recommended to extend Scene class for custom scenes:
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework'
|
||||
|
||||
// Recommended: Extend Scene for custom scene
|
||||
class GameScene extends Scene {
|
||||
|
||||
initialize(): void {
|
||||
// Scene initialization logic
|
||||
this.name = "MainScene";
|
||||
|
||||
// Add systems to scene
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
// Logic when scene starts running
|
||||
console.log("Game scene started");
|
||||
}
|
||||
|
||||
unload(): void {
|
||||
// Cleanup logic when scene unloads
|
||||
console.log("Game scene unloaded");
|
||||
}
|
||||
}
|
||||
|
||||
// Create and set scene
|
||||
const gameScene = new GameScene();
|
||||
Core.setScene(gameScene);
|
||||
```
|
||||
|
||||
### 4. Create Entities
|
||||
|
||||
```typescript
|
||||
// Create player entity
|
||||
const player = gameScene.createEntity("Player");
|
||||
player.addComponent(new Position(100, 100));
|
||||
player.addComponent(new Velocity(50, 30)); // Move 50px/sec (x), 30px/sec (y)
|
||||
player.addComponent(new Sprite("player.png", 64, 64));
|
||||
```
|
||||
|
||||
## Scene Management
|
||||
|
||||
Core has built-in scene management, very simple to use:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// Initialize Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// Create and set scene
|
||||
class GameScene extends Scene {
|
||||
initialize(): void {
|
||||
this.name = "GamePlay";
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
}
|
||||
|
||||
const gameScene = new GameScene();
|
||||
Core.setScene(gameScene);
|
||||
|
||||
// Game loop (auto-updates scene)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Auto-updates global services and scene
|
||||
}
|
||||
|
||||
// Switch scenes
|
||||
Core.loadScene(new MenuScene()); // Delayed switch (next frame)
|
||||
Core.setScene(new GameScene()); // Immediate switch
|
||||
|
||||
// Access current scene
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// Using fluent API
|
||||
const player = Core.ecsAPI?.createEntity('Player')
|
||||
.addComponent(Position, 100, 100)
|
||||
.addComponent(Velocity, 50, 0);
|
||||
```
|
||||
|
||||
### Advanced: Using WorldManager for Multi-World
|
||||
|
||||
Only for complex server-side applications (MMO game servers, game room systems, etc.):
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// Initialize Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// Get WorldManager from service container (Core auto-creates and registers it)
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// Create multiple independent game worlds
|
||||
const room1 = worldManager.createWorld('room_001');
|
||||
const room2 = worldManager.createWorld('room_002');
|
||||
|
||||
// Create scenes in each world
|
||||
const gameScene1 = room1.createScene('game', new GameScene());
|
||||
const gameScene2 = room2.createScene('game', new GameScene());
|
||||
|
||||
// Activate scenes
|
||||
room1.setSceneActive('game', true);
|
||||
room2.setSceneActive('game', true);
|
||||
|
||||
// Game loop (need to manually update worlds)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Update global services
|
||||
worldManager.updateAll(); // Manually update all worlds
|
||||
}
|
||||
```
|
||||
|
||||
## Game Engine Integration
|
||||
|
||||
### Laya 3.x Engine Integration
|
||||
|
||||
Using `Laya.Script` component to manage ECS lifecycle is recommended:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
const { regClass } = Laya;
|
||||
|
||||
@regClass()
|
||||
export class ECSManager extends Laya.Script {
|
||||
private ecsScene = new GameScene();
|
||||
|
||||
onAwake(): void {
|
||||
// Initialize ECS
|
||||
Core.create({ debug: true });
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
|
||||
onUpdate(): void {
|
||||
// Auto-updates global services and scene
|
||||
Core.update(Laya.timer.delta / 1000);
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
// Cleanup resources
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In Laya IDE, attach the `ECSManager` script to a node in your scene.
|
||||
|
||||
### Cocos Creator Integration
|
||||
|
||||
```typescript
|
||||
import { Component, _decorator } from 'cc';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('ECSGameManager')
|
||||
export class ECSGameManager extends Component {
|
||||
onLoad() {
|
||||
// Initialize ECS
|
||||
Core.create(true);
|
||||
Core.setScene(new GameScene());
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
// Auto-updates global services and scene
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// Cleanup resources
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Next Steps
|
||||
|
||||
You've successfully created your first ECS application! Next you can:
|
||||
|
||||
- Check the complete [API Documentation](/api/README)
|
||||
- Explore more [practical examples](/examples/)
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why isn't my system executing?
|
||||
|
||||
Ensure:
|
||||
1. System is added to scene: `this.addSystem(system)` (in Scene's initialize method)
|
||||
2. Scene is set: `Core.setScene(scene)`
|
||||
3. Game loop is calling: `Core.update(deltaTime)`
|
||||
|
||||
### How to debug ECS applications?
|
||||
|
||||
Enable debug mode:
|
||||
|
||||
```typescript
|
||||
Core.create({ debug: true })
|
||||
|
||||
// Get debug data
|
||||
const debugData = Core.getDebugData()
|
||||
console.log(debugData)
|
||||
```
|
||||
202
docs/src/content/docs/en/guide/hierarchy.md
Normal file
202
docs/src/content/docs/en/guide/hierarchy.md
Normal file
@@ -0,0 +1,202 @@
|
||||
---
|
||||
title: "Hierarchy System"
|
||||
description: "Parent-child entity relationships using component-based design"
|
||||
---
|
||||
|
||||
In game development, parent-child hierarchy relationships between entities are common requirements. ECS Framework manages hierarchy relationships through a component-based approach using `HierarchyComponent` and `HierarchySystem`, fully adhering to ECS composition principles.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
### Why Not Built-in Hierarchy in Entity?
|
||||
|
||||
Traditional game object models build hierarchy into entities. ECS Framework chose a component-based approach because:
|
||||
|
||||
1. **ECS Composition Principle**: Hierarchy is a "feature" that should be added through components, not inherent to all entities
|
||||
2. **On-Demand Usage**: Only entities that need hierarchy add `HierarchyComponent`
|
||||
3. **Data-Logic Separation**: `HierarchyComponent` stores data, `HierarchySystem` handles logic
|
||||
4. **Serialization-Friendly**: Hierarchy as component data can be easily serialized/deserialized
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
### HierarchyComponent
|
||||
|
||||
Component storing hierarchy relationship data:
|
||||
|
||||
```typescript
|
||||
import { HierarchyComponent } from '@esengine/ecs-framework';
|
||||
|
||||
interface HierarchyComponent {
|
||||
parentId: number | null; // Parent entity ID, null means root
|
||||
childIds: number[]; // Child entity ID list
|
||||
depth: number; // Depth in hierarchy (maintained by system)
|
||||
bActiveInHierarchy: boolean; // Active in hierarchy (maintained by system)
|
||||
}
|
||||
```
|
||||
|
||||
### HierarchySystem
|
||||
|
||||
System handling hierarchy logic, provides all hierarchy operation APIs:
|
||||
|
||||
```typescript
|
||||
import { HierarchySystem } from '@esengine/ecs-framework';
|
||||
|
||||
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Add System to Scene
|
||||
|
||||
```typescript
|
||||
import { Scene, HierarchySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.addSystem(new HierarchySystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Establish Parent-Child Relationships
|
||||
|
||||
```typescript
|
||||
const parent = scene.createEntity("Parent");
|
||||
const child1 = scene.createEntity("Child1");
|
||||
const child2 = scene.createEntity("Child2");
|
||||
|
||||
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||
|
||||
// Set parent-child relationship (auto-adds HierarchyComponent)
|
||||
hierarchySystem.setParent(child1, parent);
|
||||
hierarchySystem.setParent(child2, parent);
|
||||
```
|
||||
|
||||
### Query Hierarchy
|
||||
|
||||
```typescript
|
||||
// Get parent entity
|
||||
const parentEntity = hierarchySystem.getParent(child1);
|
||||
|
||||
// Get all children
|
||||
const children = hierarchySystem.getChildren(parent);
|
||||
|
||||
// Get child count
|
||||
const count = hierarchySystem.getChildCount(parent);
|
||||
|
||||
// Check if has children
|
||||
const hasKids = hierarchySystem.hasChildren(parent);
|
||||
|
||||
// Get depth in hierarchy
|
||||
const depth = hierarchySystem.getDepth(child1); // Returns 1
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Parent-Child Operations
|
||||
|
||||
```typescript
|
||||
// Set parent
|
||||
hierarchySystem.setParent(child, parent);
|
||||
|
||||
// Move to root (no parent)
|
||||
hierarchySystem.setParent(child, null);
|
||||
|
||||
// Insert child at position
|
||||
hierarchySystem.insertChildAt(parent, child, 0);
|
||||
|
||||
// Remove child (becomes root)
|
||||
hierarchySystem.removeChild(parent, child);
|
||||
|
||||
// Remove all children
|
||||
hierarchySystem.removeAllChildren(parent);
|
||||
```
|
||||
|
||||
### Hierarchy Queries
|
||||
|
||||
```typescript
|
||||
// Get root of entity
|
||||
const root = hierarchySystem.getRoot(deepChild);
|
||||
|
||||
// Get all root entities
|
||||
const roots = hierarchySystem.getRootEntities();
|
||||
|
||||
// Check ancestor/descendant relationships
|
||||
const isAncestor = hierarchySystem.isAncestorOf(grandparent, child);
|
||||
const isDescendant = hierarchySystem.isDescendantOf(child, grandparent);
|
||||
```
|
||||
|
||||
### Hierarchy Traversal
|
||||
|
||||
```typescript
|
||||
// Find child by name
|
||||
const child = hierarchySystem.findChild(parent, "ChildName");
|
||||
|
||||
// Recursive search
|
||||
const deepChild = hierarchySystem.findChild(parent, "DeepChild", true);
|
||||
|
||||
// Find children by tag
|
||||
const tagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY, true);
|
||||
|
||||
// Iterate children
|
||||
hierarchySystem.forEachChild(parent, (child) => {
|
||||
console.log(child.name);
|
||||
}, true); // true for recursive
|
||||
```
|
||||
|
||||
### Hierarchy State
|
||||
|
||||
```typescript
|
||||
// Check if active in hierarchy (considers all ancestors)
|
||||
const activeInHierarchy = hierarchySystem.isActiveInHierarchy(child);
|
||||
|
||||
// Get depth (root = 0)
|
||||
const depth = hierarchySystem.getDepth(entity);
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private hierarchySystem!: HierarchySystem;
|
||||
|
||||
protected initialize(): void {
|
||||
this.hierarchySystem = new HierarchySystem();
|
||||
this.addSystem(this.hierarchySystem);
|
||||
this.createPlayerHierarchy();
|
||||
}
|
||||
|
||||
private createPlayerHierarchy(): void {
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(0, 0));
|
||||
|
||||
const body = this.createEntity("Body");
|
||||
body.addComponent(new Sprite("body.png"));
|
||||
this.hierarchySystem.setParent(body, player);
|
||||
|
||||
const weapon = this.createEntity("Weapon");
|
||||
weapon.addComponent(new Sprite("sword.png"));
|
||||
this.hierarchySystem.setParent(weapon, body);
|
||||
|
||||
console.log(`Player depth: ${this.hierarchySystem.getDepth(player)}`); // 0
|
||||
console.log(`Weapon depth: ${this.hierarchySystem.getDepth(weapon)}`); // 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Avoid Deep Nesting**: System limits max depth to 32 levels
|
||||
2. **Batch Operations**: Set up all parent-child relationships at once when building complex hierarchies
|
||||
3. **On-Demand Addition**: Only add `HierarchyComponent` to entities that truly need hierarchy
|
||||
4. **Cache System Reference**: Avoid getting `HierarchySystem` on every call
|
||||
|
||||
```typescript
|
||||
// Good practice
|
||||
class MySystem extends EntitySystem {
|
||||
private hierarchySystem!: HierarchySystem;
|
||||
|
||||
onAddedToScene() {
|
||||
this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!;
|
||||
}
|
||||
}
|
||||
```
|
||||
45
docs/src/content/docs/en/guide/index.md
Normal file
45
docs/src/content/docs/en/guide/index.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: "Guide"
|
||||
---
|
||||
|
||||
Welcome to the ECS Framework Guide. This guide covers the core concepts and usage of the framework.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### [Entity](/guide/entity)
|
||||
Learn the basics of ECS architecture - how to use entities, lifecycle management, and best practices.
|
||||
|
||||
### [Component](/guide/component)
|
||||
Learn how to create and use components for modular game feature design.
|
||||
|
||||
### [System](/guide/system)
|
||||
Master system development to implement game logic processing.
|
||||
|
||||
### [Entity Query & Matcher](/guide/entity-query)
|
||||
Learn to use Matcher for entity filtering and queries with `all`, `any`, `none`, `nothing` conditions.
|
||||
|
||||
### [Scene](/guide/scene)
|
||||
Understand scene lifecycle, system management, and entity container features.
|
||||
|
||||
### [Event System](/guide/event-system)
|
||||
Master the type-safe event system for component communication and system coordination.
|
||||
|
||||
### [Serialization](/guide/serialization)
|
||||
Master serialization for scenes, entities, and components. Supports full and incremental serialization for game saves, network sync, and more.
|
||||
|
||||
### [Time and Timers](/guide/time-and-timers)
|
||||
Learn time management and timer systems for precise game logic timing control.
|
||||
|
||||
### [Logging](/guide/logging)
|
||||
Master the leveled logging system for debugging, monitoring, and error tracking.
|
||||
|
||||
### [Platform Adapter](/guide/platform-adapter)
|
||||
Learn how to implement and register platform adapters for browsers, mini-games, Node.js, and more.
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### [Service Container](/guide/service-container)
|
||||
Master dependency injection and service management for loosely-coupled architecture.
|
||||
|
||||
### [Plugin System](/guide/plugin-system)
|
||||
Learn how to develop and use plugins to extend framework functionality.
|
||||
225
docs/src/content/docs/en/guide/logging.md
Normal file
225
docs/src/content/docs/en/guide/logging.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
title: "Logging System"
|
||||
description: "Multi-level logging with colors, prefixes, and flexible configuration"
|
||||
---
|
||||
|
||||
The ECS framework provides a powerful hierarchical logging system supporting multiple log levels, color output, custom prefixes, and flexible configuration options.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
- **Log Levels**: Debug < Info < Warn < Error < Fatal < None
|
||||
- **Logger**: Named log outputter, each module can have its own logger
|
||||
- **Logger Manager**: Singleton managing all loggers globally
|
||||
- **Color Config**: Supports console color output
|
||||
|
||||
## Log Levels
|
||||
|
||||
```typescript
|
||||
import { LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
LogLevel.Debug // 0 - Debug information
|
||||
LogLevel.Info // 1 - General information
|
||||
LogLevel.Warn // 2 - Warning information
|
||||
LogLevel.Error // 3 - Error information
|
||||
LogLevel.Fatal // 4 - Fatal errors
|
||||
LogLevel.None // 5 - No output
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Using Default Logger
|
||||
|
||||
```typescript
|
||||
import { Logger } from '@esengine/ecs-framework';
|
||||
|
||||
class GameSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
Logger.debug('Processing entities:', entities.length);
|
||||
Logger.info('System running normally');
|
||||
Logger.warn('Performance issue detected');
|
||||
Logger.error('Error during processing', new Error('Example'));
|
||||
Logger.fatal('Fatal error, system stopping');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Creating Named Logger
|
||||
|
||||
```typescript
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
private logger = createLogger('MovementSystem');
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
this.logger.info(`Processing ${entities.length} moving entities`);
|
||||
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
this.logger.debug(`Entity ${entity.id} moved to (${position.x}, ${position.y})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Log Configuration
|
||||
|
||||
### Set Global Log Level
|
||||
|
||||
```typescript
|
||||
import { setGlobalLogLevel, LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
// Development: show all logs
|
||||
setGlobalLogLevel(LogLevel.Debug);
|
||||
|
||||
// Production: show warnings and above
|
||||
setGlobalLogLevel(LogLevel.Warn);
|
||||
|
||||
// Disable all logs
|
||||
setGlobalLogLevel(LogLevel.None);
|
||||
```
|
||||
|
||||
### Custom Logger Configuration
|
||||
|
||||
```typescript
|
||||
import { ConsoleLogger, LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
// Development logger
|
||||
const debugLogger = new ConsoleLogger({
|
||||
level: LogLevel.Debug,
|
||||
enableTimestamp: true,
|
||||
enableColors: true,
|
||||
prefix: 'DEV'
|
||||
});
|
||||
|
||||
// Production logger
|
||||
const productionLogger = new ConsoleLogger({
|
||||
level: LogLevel.Error,
|
||||
enableTimestamp: true,
|
||||
enableColors: false,
|
||||
prefix: 'PROD'
|
||||
});
|
||||
```
|
||||
|
||||
## Color Configuration
|
||||
|
||||
```typescript
|
||||
import { Colors, setLoggerColors } from '@esengine/ecs-framework';
|
||||
|
||||
setLoggerColors({
|
||||
debug: Colors.BRIGHT_BLACK,
|
||||
info: Colors.BLUE,
|
||||
warn: Colors.YELLOW,
|
||||
error: Colors.RED,
|
||||
fatal: Colors.BRIGHT_RED
|
||||
});
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Hierarchical Loggers
|
||||
|
||||
```typescript
|
||||
import { LoggerManager } from '@esengine/ecs-framework';
|
||||
|
||||
const manager = LoggerManager.getInstance();
|
||||
|
||||
// Create child loggers
|
||||
const movementLogger = manager.createChildLogger('GameSystems', 'Movement');
|
||||
const renderLogger = manager.createChildLogger('GameSystems', 'Render');
|
||||
|
||||
// Child logger shows full path: [GameSystems.Movement]
|
||||
movementLogger.debug('Movement system initialized');
|
||||
```
|
||||
|
||||
### Third-Party Logger Integration
|
||||
|
||||
```typescript
|
||||
import { setLoggerFactory } from '@esengine/ecs-framework';
|
||||
|
||||
// Integrate with Winston
|
||||
setLoggerFactory((name?: string) => winston.createLogger({ /* ... */ }));
|
||||
|
||||
// Integrate with Pino
|
||||
setLoggerFactory((name?: string) => pino({ name }));
|
||||
|
||||
// Integrate with NestJS Logger
|
||||
setLoggerFactory((name?: string) => new Logger(name));
|
||||
```
|
||||
|
||||
### Custom Output
|
||||
|
||||
```typescript
|
||||
const fileLogger = new ConsoleLogger({
|
||||
level: LogLevel.Info,
|
||||
output: (level: LogLevel, message: string) => {
|
||||
this.writeToFile(LogLevel[level], message);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Choose Appropriate Log Levels
|
||||
|
||||
```typescript
|
||||
// Debug - Detailed debug info
|
||||
this.logger.debug('Variable values', { x: 10, y: 20 });
|
||||
|
||||
// Info - Important state changes
|
||||
this.logger.info('System startup complete');
|
||||
|
||||
// Warn - Abnormal but non-fatal
|
||||
this.logger.warn('Resource not found, using default');
|
||||
|
||||
// Error - Errors but program can continue
|
||||
this.logger.error('Save failed, will retry', new Error('Network timeout'));
|
||||
|
||||
// Fatal - Fatal errors, program cannot continue
|
||||
this.logger.fatal('Out of memory, exiting');
|
||||
```
|
||||
|
||||
### 2. Structured Log Data
|
||||
|
||||
```typescript
|
||||
this.logger.info('User action', {
|
||||
userId: 12345,
|
||||
action: 'move',
|
||||
position: { x: 100, y: 200 },
|
||||
timestamp: Date.now()
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Avoid Performance Issues
|
||||
|
||||
```typescript
|
||||
// ✅ Check log level before expensive computation
|
||||
if (this.logger.debug) {
|
||||
const expensiveData = this.calculateExpensiveDebugInfo();
|
||||
this.logger.debug('Debug info', expensiveData);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Environment-Based Configuration
|
||||
|
||||
```typescript
|
||||
class LoggingConfiguration {
|
||||
public static setupLogging(): void {
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
if (isDevelopment) {
|
||||
setGlobalLogLevel(LogLevel.Debug);
|
||||
setLoggerColors({
|
||||
debug: Colors.CYAN,
|
||||
info: Colors.GREEN,
|
||||
warn: Colors.YELLOW,
|
||||
error: Colors.RED,
|
||||
fatal: Colors.BRIGHT_RED
|
||||
});
|
||||
} else {
|
||||
setGlobalLogLevel(LogLevel.Warn);
|
||||
LoggerManager.getInstance().resetColors();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
364
docs/src/content/docs/en/guide/persistent-entity.md
Normal file
364
docs/src/content/docs/en/guide/persistent-entity.md
Normal file
@@ -0,0 +1,364 @@
|
||||
---
|
||||
title: "persistent-entity"
|
||||
---
|
||||
|
||||
# Persistent Entity
|
||||
|
||||
> **Version**: v2.3.0+
|
||||
|
||||
Persistent Entity is a special type of entity that automatically migrates to the new scene during scene transitions. It is suitable for game objects that need to maintain state across scenes, such as players, game managers, audio managers, etc.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
In the ECS framework, entities have two lifecycle policies:
|
||||
|
||||
| Policy | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `SceneLocal` | Scene-local entity, destroyed when scene changes | ✓ |
|
||||
| `Persistent` | Persistent entity, automatically migrates during scene transitions | |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Creating a Persistent Entity
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Create a persistent player entity
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Position(100, 200));
|
||||
player.addComponent(new PlayerData('Hero', 500));
|
||||
|
||||
// Create a normal enemy entity (destroyed when scene changes)
|
||||
const enemy = this.createEntity('Enemy');
|
||||
enemy.addComponent(new Position(300, 200));
|
||||
enemy.addComponent(new EnemyAI());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Behavior During Scene Transitions
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// Initial scene
|
||||
class Level1Scene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Player - persistent, will migrate to the next scene
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Position(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// Enemy - scene-local, destroyed when scene changes
|
||||
const enemy = this.createEntity('Enemy');
|
||||
enemy.addComponent(new Position(100, 100));
|
||||
}
|
||||
}
|
||||
|
||||
// Target scene
|
||||
class Level2Scene extends Scene {
|
||||
protected initialize(): void {
|
||||
// New enemy
|
||||
const enemy = this.createEntity('Boss');
|
||||
enemy.addComponent(new Position(200, 200));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// Player has automatically migrated to this scene
|
||||
const player = this.findEntity('Player');
|
||||
console.log(player !== null); // true
|
||||
|
||||
// Position and health data are fully preserved
|
||||
const position = player?.getComponent(Position);
|
||||
const health = player?.getComponent(Health);
|
||||
console.log(position?.x, position?.y); // 0, 0
|
||||
console.log(health?.value); // 100
|
||||
}
|
||||
}
|
||||
|
||||
// Switch scenes
|
||||
Core.create({ debug: true });
|
||||
Core.setScene(new Level1Scene());
|
||||
|
||||
// Later switch to Level2
|
||||
Core.loadScene(new Level2Scene());
|
||||
// Player entity migrates automatically, Enemy entity is destroyed
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Entity Methods
|
||||
|
||||
#### setPersistent()
|
||||
|
||||
Marks the entity as persistent, preventing destruction during scene transitions.
|
||||
|
||||
```typescript
|
||||
public setPersistent(): this
|
||||
```
|
||||
|
||||
**Returns**: Returns the entity itself for method chaining
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
const player = scene.createEntity('Player')
|
||||
.setPersistent();
|
||||
|
||||
player.addComponent(new Position(100, 200));
|
||||
```
|
||||
|
||||
#### setSceneLocal()
|
||||
|
||||
Restores the entity to scene-local policy (default).
|
||||
|
||||
```typescript
|
||||
public setSceneLocal(): this
|
||||
```
|
||||
|
||||
**Returns**: Returns the entity itself for method chaining
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
// Dynamically cancel persistence
|
||||
player.setSceneLocal();
|
||||
```
|
||||
|
||||
#### isPersistent
|
||||
|
||||
Checks if the entity is persistent.
|
||||
|
||||
```typescript
|
||||
public get isPersistent(): boolean
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
if (entity.isPersistent) {
|
||||
console.log('This is a persistent entity');
|
||||
}
|
||||
```
|
||||
|
||||
#### lifecyclePolicy
|
||||
|
||||
Gets the entity's lifecycle policy.
|
||||
|
||||
```typescript
|
||||
public get lifecyclePolicy(): EEntityLifecyclePolicy
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
import { EEntityLifecyclePolicy } from '@esengine/ecs-framework';
|
||||
|
||||
if (entity.lifecyclePolicy === EEntityLifecyclePolicy.Persistent) {
|
||||
console.log('Persistent entity');
|
||||
}
|
||||
```
|
||||
|
||||
### Scene Methods
|
||||
|
||||
#### findPersistentEntities()
|
||||
|
||||
Finds all persistent entities in the scene.
|
||||
|
||||
```typescript
|
||||
public findPersistentEntities(): Entity[]
|
||||
```
|
||||
|
||||
**Returns**: Array of persistent entities
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
const persistentEntities = scene.findPersistentEntities();
|
||||
console.log(`Scene has ${persistentEntities.length} persistent entities`);
|
||||
```
|
||||
|
||||
#### extractPersistentEntities()
|
||||
|
||||
Extracts and removes all persistent entities from the scene (typically called internally by the framework).
|
||||
|
||||
```typescript
|
||||
public extractPersistentEntities(): Entity[]
|
||||
```
|
||||
|
||||
**Returns**: Array of extracted persistent entities
|
||||
|
||||
#### receiveMigratedEntities()
|
||||
|
||||
Receives migrated entities (typically called internally by the framework).
|
||||
|
||||
```typescript
|
||||
public receiveMigratedEntities(entities: Entity[]): void
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `entities` - Array of entities to receive
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Player Entity Across Levels
|
||||
|
||||
```typescript
|
||||
class PlayerSetupScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Player maintains state across all levels
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Transform(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
player.addComponent(new Inventory());
|
||||
player.addComponent(new PlayerStats());
|
||||
}
|
||||
}
|
||||
|
||||
class Level1 extends Scene { /* ... */ }
|
||||
class Level2 extends Scene { /* ... */ }
|
||||
class Level3 extends Scene { /* ... */ }
|
||||
|
||||
// Player entity automatically migrates between all levels
|
||||
Core.setScene(new PlayerSetupScene());
|
||||
// ... game progresses
|
||||
Core.loadScene(new Level1());
|
||||
// ... level complete
|
||||
Core.loadScene(new Level2());
|
||||
// Player data (health, inventory, stats) fully preserved
|
||||
```
|
||||
|
||||
### 2. Global Managers
|
||||
|
||||
```typescript
|
||||
class BootstrapScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Audio manager - persists across scenes
|
||||
const audioManager = this.createEntity('AudioManager').setPersistent();
|
||||
audioManager.addComponent(new AudioController());
|
||||
|
||||
// Achievement manager - persists across scenes
|
||||
const achievementManager = this.createEntity('AchievementManager').setPersistent();
|
||||
achievementManager.addComponent(new AchievementTracker());
|
||||
|
||||
// Game settings - persists across scenes
|
||||
const settings = this.createEntity('GameSettings').setPersistent();
|
||||
settings.addComponent(new SettingsData());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Dynamically Toggling Persistence
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Initially created as a normal entity
|
||||
const companion = this.createEntity('Companion');
|
||||
companion.addComponent(new Transform(0, 0));
|
||||
companion.addComponent(new CompanionAI());
|
||||
|
||||
// Listen for recruitment event
|
||||
this.eventSystem.on('companion:recruited', () => {
|
||||
// After recruitment, become persistent
|
||||
companion.setPersistent();
|
||||
console.log('Companion joined the party, will follow player across scenes');
|
||||
});
|
||||
|
||||
// Listen for dismissal event
|
||||
this.eventSystem.on('companion:dismissed', () => {
|
||||
// After dismissal, restore to scene-local
|
||||
companion.setSceneLocal();
|
||||
console.log('Companion left the party, will no longer persist across scenes');
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Clearly Identify Persistent Entities
|
||||
|
||||
```typescript
|
||||
// Recommended: Mark immediately when creating
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
|
||||
// Not recommended: Marking after creation (easy to forget)
|
||||
const player = this.createEntity('Player');
|
||||
// ... lots of code ...
|
||||
player.setPersistent(); // Easy to forget
|
||||
```
|
||||
|
||||
### 2. Use Persistence Appropriately
|
||||
|
||||
```typescript
|
||||
// ✓ Entities suitable for persistence
|
||||
const player = this.createEntity('Player').setPersistent(); // Player
|
||||
const gameManager = this.createEntity('GameManager').setPersistent(); // Global manager
|
||||
const audioManager = this.createEntity('AudioManager').setPersistent(); // Audio system
|
||||
|
||||
// ✗ Entities that should NOT be persistent
|
||||
const bullet = this.createEntity('Bullet'); // Temporary objects
|
||||
const enemy = this.createEntity('Enemy'); // Level-specific enemies
|
||||
const particle = this.createEntity('Particle'); // Effect particles
|
||||
```
|
||||
|
||||
### 3. Check Migrated Entities
|
||||
|
||||
```typescript
|
||||
class NewScene extends Scene {
|
||||
public onStart(): void {
|
||||
// Check if expected persistent entities exist
|
||||
const player = this.findEntity('Player');
|
||||
if (!player) {
|
||||
console.error('Player entity did not migrate correctly!');
|
||||
// Handle error case
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Avoid Circular References
|
||||
|
||||
```typescript
|
||||
// ✗ Avoid: Persistent entity referencing scene-local entity
|
||||
class BadScene extends Scene {
|
||||
protected initialize(): void {
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
const enemy = this.createEntity('Enemy');
|
||||
|
||||
// Dangerous: player is persistent but enemy is not
|
||||
// After scene change, enemy is destroyed, reference becomes invalid
|
||||
player.addComponent(new TargetComponent(enemy));
|
||||
}
|
||||
}
|
||||
|
||||
// ✓ Recommended: Use ID references or event system
|
||||
class GoodScene extends Scene {
|
||||
protected initialize(): void {
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
const enemy = this.createEntity('Enemy');
|
||||
|
||||
// Store ID instead of direct reference
|
||||
player.addComponent(new TargetComponent(enemy.id));
|
||||
|
||||
// Or use event system for communication
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Destroyed entities will not migrate**: If an entity is destroyed before scene transition, it will not migrate even if marked as persistent.
|
||||
|
||||
2. **Component data is fully preserved**: All components and their state are preserved during migration.
|
||||
|
||||
3. **Scene reference is updated**: After migration, the entity's `scene` property will point to the new scene.
|
||||
|
||||
4. **Query system is updated**: Migrated entities are automatically registered in the new scene's query system.
|
||||
|
||||
5. **Delayed transitions also work**: Persistent entities migrate when using `Core.loadScene()` for delayed transitions as well.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Scene](./scene) - Learn the basics of scenes
|
||||
- [SceneManager](./scene-manager) - Learn about scene transitions
|
||||
- [WorldManager](./world-manager) - Learn about multi-world management
|
||||
291
docs/src/content/docs/en/guide/platform-adapter.md
Normal file
291
docs/src/content/docs/en/guide/platform-adapter.md
Normal file
@@ -0,0 +1,291 @@
|
||||
---
|
||||
title: "Platform Adapter"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The ECS framework provides a platform adapter interface that allows users to implement custom platform adapters for different runtime environments.
|
||||
|
||||
**The core library only provides interface definitions. Platform adapter implementations should be copied from the documentation.**
|
||||
|
||||
## Why No Separate Adapter Packages?
|
||||
|
||||
1. **Flexibility**: Different projects may have different platform adaptation needs. Copying code allows users to freely modify as needed
|
||||
2. **Reduce Dependencies**: Avoid introducing unnecessary dependency packages, keeping the core framework lean
|
||||
3. **Customization**: Users can customize according to specific runtime environments and requirements
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
### [Browser Adapter](./platform-adapter/browser/)
|
||||
|
||||
Supports all modern browser environments, including Chrome, Firefox, Safari, Edge, etc.
|
||||
|
||||
**Feature Support**:
|
||||
- Worker (Web Worker)
|
||||
- SharedArrayBuffer (requires COOP/COEP)
|
||||
- Transferable Objects
|
||||
- Module Worker (modern browsers)
|
||||
|
||||
**Use Cases**: Web games, Web applications, PWA
|
||||
|
||||
---
|
||||
|
||||
### [WeChat Mini Game Adapter](./platform-adapter/wechat-minigame/)
|
||||
|
||||
Designed specifically for the WeChat Mini Game environment, handling special restrictions and APIs.
|
||||
|
||||
**Feature Support**:
|
||||
- Worker (max 1, requires game.json configuration)
|
||||
- SharedArrayBuffer (not supported)
|
||||
- Transferable Objects (not supported)
|
||||
- WeChat Device Info API
|
||||
|
||||
**Use Cases**: WeChat Mini Game development
|
||||
|
||||
---
|
||||
|
||||
### [Node.js Adapter](./platform-adapter/nodejs/)
|
||||
|
||||
Provides support for Node.js server environments, suitable for game servers and compute servers.
|
||||
|
||||
**Feature Support**:
|
||||
- Worker Threads
|
||||
- SharedArrayBuffer
|
||||
- Transferable Objects
|
||||
- Complete system information
|
||||
|
||||
**Use Cases**: Game servers, compute servers, CLI tools
|
||||
|
||||
---
|
||||
|
||||
## Core Interfaces
|
||||
|
||||
### IPlatformAdapter
|
||||
|
||||
```typescript
|
||||
export interface IPlatformAdapter {
|
||||
readonly name: string;
|
||||
readonly version?: string;
|
||||
|
||||
isWorkerSupported(): boolean;
|
||||
isSharedArrayBufferSupported(): boolean;
|
||||
getHardwareConcurrency(): number;
|
||||
createWorker(script: string, options?: WorkerCreationOptions): PlatformWorker;
|
||||
createSharedArrayBuffer(length: number): SharedArrayBuffer | null;
|
||||
getHighResTimestamp(): number;
|
||||
getPlatformConfig(): PlatformConfig;
|
||||
getPlatformConfigAsync?(): Promise<PlatformConfig>;
|
||||
}
|
||||
```
|
||||
|
||||
### PlatformWorker Interface
|
||||
|
||||
```typescript
|
||||
export interface PlatformWorker {
|
||||
postMessage(message: any, transfer?: Transferable[]): void;
|
||||
onMessage(handler: (event: { data: any }) => void): void;
|
||||
onError(handler: (error: ErrorEvent) => void): void;
|
||||
terminate(): void;
|
||||
readonly state: 'running' | 'terminated';
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Choose the Appropriate Platform Adapter
|
||||
|
||||
Select the corresponding adapter based on your runtime environment:
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
|
||||
// Browser environment
|
||||
if (typeof window !== 'undefined') {
|
||||
const { BrowserAdapter } = await import('./platform/BrowserAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new BrowserAdapter());
|
||||
}
|
||||
|
||||
// WeChat Mini Game environment
|
||||
else if (typeof wx !== 'undefined') {
|
||||
const { WeChatMiniGameAdapter } = await import('./platform/WeChatMiniGameAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new WeChatMiniGameAdapter());
|
||||
}
|
||||
|
||||
// Node.js environment
|
||||
else if (typeof process !== 'undefined' && process.versions?.node) {
|
||||
const { NodeAdapter } = await import('./platform/NodeAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new NodeAdapter());
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Check Adapter Status
|
||||
|
||||
```typescript
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
// Check if adapter is registered
|
||||
if (manager.hasAdapter()) {
|
||||
const adapter = manager.getAdapter();
|
||||
console.log('Current platform:', adapter.name);
|
||||
console.log('Platform version:', adapter.version);
|
||||
|
||||
// Check feature support
|
||||
console.log('Worker support:', manager.supportsFeature('worker'));
|
||||
console.log('SharedArrayBuffer support:', manager.supportsFeature('shared-array-buffer'));
|
||||
}
|
||||
```
|
||||
|
||||
## Creating Custom Adapters
|
||||
|
||||
If existing platform adapters don't meet your needs, you can create custom adapters:
|
||||
|
||||
### 1. Implement the Interface
|
||||
|
||||
```typescript
|
||||
import type { IPlatformAdapter, PlatformWorker, WorkerCreationOptions, PlatformConfig } from '@esengine/ecs-framework';
|
||||
|
||||
export class CustomAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'custom';
|
||||
public readonly version = '1.0.0';
|
||||
|
||||
public isWorkerSupported(): boolean {
|
||||
// Implement your Worker support check logic
|
||||
return false;
|
||||
}
|
||||
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
// Implement your SharedArrayBuffer support check logic
|
||||
return false;
|
||||
}
|
||||
|
||||
public getHardwareConcurrency(): number {
|
||||
// Return your platform's concurrency count
|
||||
return 1;
|
||||
}
|
||||
|
||||
public createWorker(script: string, options?: WorkerCreationOptions): PlatformWorker {
|
||||
throw new Error('Worker not supported on this platform');
|
||||
}
|
||||
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getHighResTimestamp(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: 1,
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: false,
|
||||
limitations: {
|
||||
workerNotSupported: true
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Register Custom Adapter
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { CustomAdapter } from './CustomAdapter';
|
||||
|
||||
const customAdapter = new CustomAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(customAdapter);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Platform Detection Order
|
||||
|
||||
Recommend detecting and registering platform adapters in this order:
|
||||
|
||||
```typescript
|
||||
async function initializePlatform() {
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
try {
|
||||
// 1. WeChat Mini Game (highest priority, most distinctive environment)
|
||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
||||
const { WeChatMiniGameAdapter } = await import('./platform/WeChatMiniGameAdapter');
|
||||
manager.registerAdapter(new WeChatMiniGameAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Node.js environment
|
||||
if (typeof process !== 'undefined' && process.versions?.node) {
|
||||
const { NodeAdapter } = await import('./platform/NodeAdapter');
|
||||
manager.registerAdapter(new NodeAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Browser environment (last check, broadest coverage)
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
const { BrowserAdapter } = await import('./platform/BrowserAdapter');
|
||||
manager.registerAdapter(new BrowserAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Unknown environment, use default adapter
|
||||
console.warn('Unrecognized platform environment, using default adapter');
|
||||
manager.registerAdapter(new CustomAdapter());
|
||||
|
||||
} catch (error) {
|
||||
console.error('Platform adapter initialization failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Feature Degradation Handling
|
||||
|
||||
```typescript
|
||||
function createWorkerSystem() {
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
if (!manager.hasAdapter()) {
|
||||
throw new Error('No platform adapter registered');
|
||||
}
|
||||
|
||||
const config: WorkerSystemConfig = {
|
||||
enableWorker: manager.supportsFeature('worker'),
|
||||
workerCount: manager.supportsFeature('worker') ?
|
||||
manager.getAdapter().getHardwareConcurrency() : 1,
|
||||
useSharedArrayBuffer: manager.supportsFeature('shared-array-buffer')
|
||||
};
|
||||
|
||||
// If Worker not supported, automatically degrade to synchronous processing
|
||||
if (!config.enableWorker) {
|
||||
console.info('Current platform does not support Worker, using synchronous processing mode');
|
||||
}
|
||||
|
||||
return new PhysicsSystem(config);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await initializePlatform();
|
||||
|
||||
// Validate adapter functionality
|
||||
const manager = PlatformManager.getInstance();
|
||||
const adapter = manager.getAdapter();
|
||||
|
||||
console.log(`Platform adapter initialized: ${adapter.name} v${adapter.version}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Platform initialization failed:', error);
|
||||
|
||||
// Provide fallback solution
|
||||
const fallbackAdapter = new CustomAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(fallbackAdapter);
|
||||
|
||||
console.warn('Using fallback adapter to continue running');
|
||||
}
|
||||
```
|
||||
372
docs/src/content/docs/en/guide/platform-adapter/browser.md
Normal file
372
docs/src/content/docs/en/guide/platform-adapter/browser.md
Normal file
@@ -0,0 +1,372 @@
|
||||
---
|
||||
title: "Browser Adapter"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The browser platform adapter provides support for standard web browser environments, including modern browsers like Chrome, Firefox, Safari, Edge, etc.
|
||||
|
||||
## Feature Support
|
||||
|
||||
- **Worker**: Supports Web Worker and Module Worker
|
||||
- **SharedArrayBuffer**: Supported (requires COOP/COEP headers)
|
||||
- **Transferable Objects**: Fully supported
|
||||
- **High-Resolution Time**: Uses `performance.now()`
|
||||
- **Basic Info**: Browser version and basic configuration
|
||||
|
||||
## Complete Implementation
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Browser platform adapter
|
||||
* Supports standard web browser environments
|
||||
*/
|
||||
export class BrowserAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'browser';
|
||||
public readonly version: string;
|
||||
|
||||
constructor() {
|
||||
this.version = this.getBrowserInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Worker is supported
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
return typeof Worker !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SharedArrayBuffer is supported
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return typeof SharedArrayBuffer !== 'undefined' && this.checkSharedArrayBufferEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hardware concurrency (CPU core count)
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
return navigator.hardwareConcurrency || 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Worker
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('Browser does not support Worker');
|
||||
}
|
||||
|
||||
try {
|
||||
return new BrowserWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create browser Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SharedArrayBuffer
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
if (!this.isSharedArrayBufferSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SharedArrayBuffer(length);
|
||||
} catch (error) {
|
||||
console.warn('SharedArrayBuffer creation failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get high-resolution timestamp
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
return performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform configuration
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: this.getHardwareConcurrency(),
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: true,
|
||||
maxSharedArrayBufferSize: 1024 * 1024 * 1024, // 1GB
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: false,
|
||||
requiresWorkerInit: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get browser information
|
||||
*/
|
||||
private getBrowserInfo(): string {
|
||||
const userAgent = navigator.userAgent;
|
||||
if (userAgent.includes('Chrome')) {
|
||||
const match = userAgent.match(/Chrome\/([0-9.]+)/);
|
||||
return match ? `Chrome ${match[1]}` : 'Chrome';
|
||||
} else if (userAgent.includes('Firefox')) {
|
||||
const match = userAgent.match(/Firefox\/([0-9.]+)/);
|
||||
if (match) return `Firefox ${match[1]}`;
|
||||
} else if (userAgent.includes('Safari')) {
|
||||
const match = userAgent.match(/Version\/([0-9.]+)/);
|
||||
if (match) return `Safari ${match[1]}`;
|
||||
}
|
||||
return 'Unknown Browser';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SharedArrayBuffer is actually available
|
||||
*/
|
||||
private checkSharedArrayBufferEnabled(): boolean {
|
||||
try {
|
||||
new SharedArrayBuffer(8);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser Worker wrapper
|
||||
*/
|
||||
class BrowserWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: Worker;
|
||||
|
||||
constructor(script: string, options: WorkerCreationOptions = {}) {
|
||||
this.worker = this.createBrowserWorker(script, options);
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker has been terminated');
|
||||
}
|
||||
|
||||
try {
|
||||
if (transfer && transfer.length > 0) {
|
||||
this.worker.postMessage(message, transfer);
|
||||
} else {
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to send message to Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
this.worker.onmessage = (event: MessageEvent) => {
|
||||
handler({ data: event.data });
|
||||
};
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
this.worker.onerror = handler;
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
try {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
} catch (error) {
|
||||
console.error('Failed to terminate Worker:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create browser Worker
|
||||
*/
|
||||
private createBrowserWorker(script: string, options: WorkerCreationOptions): Worker {
|
||||
try {
|
||||
// Create Blob URL
|
||||
const blob = new Blob([script], { type: 'application/javascript' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create Worker
|
||||
const worker = new Worker(url, {
|
||||
type: options.type || 'classic',
|
||||
credentials: options.credentials,
|
||||
name: options.name
|
||||
});
|
||||
|
||||
// Clean up Blob URL (delayed to ensure Worker has loaded)
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 1000);
|
||||
|
||||
return worker;
|
||||
} catch (error) {
|
||||
throw new Error(`Cannot create browser Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Copy the Code
|
||||
|
||||
Copy the above code to your project, e.g., `src/platform/BrowserAdapter.ts`.
|
||||
|
||||
### 2. Register the Adapter
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { BrowserAdapter } from './platform/BrowserAdapter';
|
||||
|
||||
// Create and register browser adapter
|
||||
const browserAdapter = new BrowserAdapter();
|
||||
PlatformManager.registerAdapter(browserAdapter);
|
||||
|
||||
// Framework will automatically detect and use the appropriate adapter
|
||||
```
|
||||
|
||||
### 3. Use WorkerEntitySystem
|
||||
|
||||
The browser adapter works with WorkerEntitySystem, and the framework automatically handles Worker script creation:
|
||||
|
||||
```typescript
|
||||
import { WorkerEntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends WorkerEntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: navigator.hardwareConcurrency || 4,
|
||||
useSharedArrayBuffer: true,
|
||||
systemConfig: { gravity: 9.8 }
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6; // x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
return {
|
||||
x: transform.x,
|
||||
y: transform.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: 1,
|
||||
radius: 10
|
||||
};
|
||||
}
|
||||
|
||||
// This function is automatically serialized and executed in Worker
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(entity => {
|
||||
// Apply gravity
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// Update position
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
transform.x = result.x;
|
||||
transform.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
}
|
||||
|
||||
interface PhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Verify Adapter Status
|
||||
|
||||
```typescript
|
||||
// Verify adapter is working properly
|
||||
const adapter = new BrowserAdapter();
|
||||
console.log('Adapter name:', adapter.name);
|
||||
console.log('Browser version:', adapter.version);
|
||||
console.log('Worker support:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer support:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('CPU core count:', adapter.getHardwareConcurrency());
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### SharedArrayBuffer Support
|
||||
|
||||
SharedArrayBuffer requires special security configuration:
|
||||
|
||||
1. **HTTPS**: Must be used in a secure context
|
||||
2. **COOP/COEP Headers**: Requires correct cross-origin isolation headers
|
||||
|
||||
```html
|
||||
<!-- Set in HTML -->
|
||||
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin">
|
||||
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp">
|
||||
```
|
||||
|
||||
Or set in server configuration:
|
||||
|
||||
```
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
```
|
||||
|
||||
### Browser Compatibility
|
||||
|
||||
- **Worker**: All modern browsers supported
|
||||
- **Module Worker**: Chrome 80+, Firefox 114+
|
||||
- **SharedArrayBuffer**: Chrome 68+, Firefox 79+ (requires COOP/COEP)
|
||||
- **Transferable Objects**: All modern browsers supported
|
||||
|
||||
## Performance Optimization Tips
|
||||
|
||||
1. **Worker Pool**: Reuse Worker instances to avoid frequent creation and destruction
|
||||
2. **Data Transfer**: Use Transferable Objects to reduce data copying
|
||||
3. **SharedArrayBuffer**: Use SharedArrayBuffer for large data sharing
|
||||
4. **Module Worker**: Use module Workers in supported browsers for better code organization
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
```typescript
|
||||
// Check browser support
|
||||
const adapter = new BrowserAdapter();
|
||||
console.log('Worker support:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer support:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('Hardware concurrency:', adapter.getHardwareConcurrency());
|
||||
console.log('Platform config:', adapter.getPlatformConfig());
|
||||
```
|
||||
560
docs/src/content/docs/en/guide/platform-adapter/nodejs.md
Normal file
560
docs/src/content/docs/en/guide/platform-adapter/nodejs.md
Normal file
@@ -0,0 +1,560 @@
|
||||
---
|
||||
title: "Node.js Adapter"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Node.js platform adapter provides support for Node.js server environments, suitable for game servers, compute servers, or other server applications that need ECS architecture.
|
||||
|
||||
## Feature Support
|
||||
|
||||
- **Worker**: Supported (via `worker_threads` module)
|
||||
- **SharedArrayBuffer**: Supported (Node.js 16.17.0+)
|
||||
- **Transferable Objects**: Fully supported
|
||||
- **High-Resolution Time**: Uses `process.hrtime.bigint()`
|
||||
- **Device Info**: Complete system and process information
|
||||
|
||||
## Complete Implementation
|
||||
|
||||
```typescript
|
||||
import { worker_threads, Worker, isMainThread, parentPort } from 'worker_threads';
|
||||
import * as os from 'os';
|
||||
import * as process from 'process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig,
|
||||
NodeDeviceInfo
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Node.js platform adapter
|
||||
* Supports Node.js server environments
|
||||
*/
|
||||
export class NodeAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'nodejs';
|
||||
public readonly version: string;
|
||||
|
||||
constructor() {
|
||||
this.version = process.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Worker is supported
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
try {
|
||||
// Check if worker_threads module is available
|
||||
return typeof worker_threads !== 'undefined' && typeof Worker !== 'undefined';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SharedArrayBuffer is supported
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
// Node.js supports SharedArrayBuffer
|
||||
return typeof SharedArrayBuffer !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hardware concurrency (CPU core count)
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
return os.cpus().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Worker
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('Node.js environment does not support Worker Threads');
|
||||
}
|
||||
|
||||
try {
|
||||
return new NodeWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create Node.js Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SharedArrayBuffer
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
if (!this.isSharedArrayBufferSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SharedArrayBuffer(length);
|
||||
} catch (error) {
|
||||
console.warn('SharedArrayBuffer creation failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get high-resolution timestamp (nanoseconds)
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
// Return milliseconds, consistent with browser performance.now()
|
||||
return Number(process.hrtime.bigint()) / 1000000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform configuration
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: this.getHardwareConcurrency(),
|
||||
supportsModuleWorker: true, // Node.js supports ES modules
|
||||
supportsTransferableObjects: true,
|
||||
maxSharedArrayBufferSize: this.getMaxSharedArrayBufferSize(),
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: false, // Node.js supports eval
|
||||
requiresWorkerInit: false
|
||||
},
|
||||
extensions: {
|
||||
platform: 'nodejs',
|
||||
nodeVersion: process.version,
|
||||
v8Version: process.versions.v8,
|
||||
uvVersion: process.versions.uv,
|
||||
zlibVersion: process.versions.zlib,
|
||||
opensslVersion: process.versions.openssl,
|
||||
architecture: process.arch,
|
||||
endianness: os.endianness(),
|
||||
pid: process.pid,
|
||||
ppid: process.ppid
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Node.js device information
|
||||
*/
|
||||
public getDeviceInfo(): NodeDeviceInfo {
|
||||
const cpus = os.cpus();
|
||||
const networkInterfaces = os.networkInterfaces();
|
||||
const userInfo = os.userInfo();
|
||||
|
||||
return {
|
||||
// System info
|
||||
platform: os.platform(),
|
||||
arch: os.arch(),
|
||||
type: os.type(),
|
||||
release: os.release(),
|
||||
version: os.version(),
|
||||
hostname: os.hostname(),
|
||||
|
||||
// CPU info
|
||||
cpus: cpus.map(cpu => ({
|
||||
model: cpu.model,
|
||||
speed: cpu.speed,
|
||||
times: cpu.times
|
||||
})),
|
||||
|
||||
// Memory info
|
||||
totalMemory: os.totalmem(),
|
||||
freeMemory: os.freemem(),
|
||||
usedMemory: os.totalmem() - os.freemem(),
|
||||
|
||||
// Load info
|
||||
loadAverage: os.loadavg(),
|
||||
|
||||
// Network interfaces
|
||||
networkInterfaces: Object.fromEntries(
|
||||
Object.entries(networkInterfaces).map(([name, interfaces]) => [
|
||||
name,
|
||||
(interfaces || []).map(iface => ({
|
||||
address: iface.address,
|
||||
netmask: iface.netmask,
|
||||
family: iface.family as 'IPv4' | 'IPv6',
|
||||
mac: iface.mac,
|
||||
internal: iface.internal,
|
||||
cidr: iface.cidr,
|
||||
scopeid: iface.scopeid
|
||||
}))
|
||||
])
|
||||
),
|
||||
|
||||
// Process info
|
||||
process: {
|
||||
pid: process.pid,
|
||||
ppid: process.ppid,
|
||||
version: process.version,
|
||||
versions: process.versions,
|
||||
uptime: process.uptime()
|
||||
},
|
||||
|
||||
// User info
|
||||
userInfo: {
|
||||
uid: userInfo.uid,
|
||||
gid: userInfo.gid,
|
||||
username: userInfo.username,
|
||||
homedir: userInfo.homedir,
|
||||
shell: userInfo.shell
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SharedArrayBuffer maximum size limit
|
||||
*/
|
||||
private getMaxSharedArrayBufferSize(): number {
|
||||
const totalMemory = os.totalmem();
|
||||
// Limit to 50% of total system memory
|
||||
return Math.floor(totalMemory * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Node.js Worker wrapper
|
||||
*/
|
||||
class NodeWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: Worker;
|
||||
private isTemporaryFile: boolean = false;
|
||||
private scriptPath: string;
|
||||
|
||||
constructor(script: string, options: WorkerCreationOptions = {}) {
|
||||
try {
|
||||
// Determine if script is a file path or script content
|
||||
if (this.isFilePath(script)) {
|
||||
// Use file path directly
|
||||
this.scriptPath = script;
|
||||
this.isTemporaryFile = false;
|
||||
} else {
|
||||
// Write script content to temporary file
|
||||
this.scriptPath = this.writeScriptToFile(script, options.name);
|
||||
this.isTemporaryFile = true;
|
||||
}
|
||||
|
||||
// Create Worker
|
||||
this.worker = new Worker(this.scriptPath, {
|
||||
// Node.js Worker options
|
||||
workerData: options.name ? { name: options.name } : undefined
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create Node.js Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if string is a file path
|
||||
*/
|
||||
private isFilePath(script: string): boolean {
|
||||
// Check if it looks like a file path
|
||||
return (script.endsWith('.js') || script.endsWith('.mjs') || script.endsWith('.ts')) &&
|
||||
!script.includes('\n') &&
|
||||
!script.includes(';') &&
|
||||
script.length < 500; // File paths are typically not too long
|
||||
}
|
||||
|
||||
/**
|
||||
* Write script content to temporary file
|
||||
*/
|
||||
private writeScriptToFile(script: string, name?: string): string {
|
||||
const tmpDir = os.tmpdir();
|
||||
const fileName = name ? `worker-${name}-${Date.now()}.js` : `worker-${Date.now()}.js`;
|
||||
const filePath = path.join(tmpDir, fileName);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filePath, script, 'utf8');
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to write Worker script file: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker has been terminated');
|
||||
}
|
||||
|
||||
try {
|
||||
if (transfer && transfer.length > 0) {
|
||||
// Node.js Worker supports Transferable Objects
|
||||
this.worker.postMessage(message, transfer);
|
||||
} else {
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to send message to Node.js Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
this.worker.on('message', (data: any) => {
|
||||
handler({ data });
|
||||
});
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
this.worker.on('error', (error: Error) => {
|
||||
// Convert Error to ErrorEvent format
|
||||
const errorEvent = {
|
||||
message: error.message,
|
||||
filename: '',
|
||||
lineno: 0,
|
||||
colno: 0,
|
||||
error: error
|
||||
} as ErrorEvent;
|
||||
handler(errorEvent);
|
||||
});
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
try {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
|
||||
// Clean up temporary script file
|
||||
this.cleanupScriptFile();
|
||||
} catch (error) {
|
||||
console.error('Failed to terminate Node.js Worker:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up temporary script file
|
||||
*/
|
||||
private cleanupScriptFile(): void {
|
||||
// Only clean up temporarily created files, not user-provided file paths
|
||||
if (this.scriptPath && this.isTemporaryFile) {
|
||||
try {
|
||||
fs.unlinkSync(this.scriptPath);
|
||||
} catch (error) {
|
||||
console.warn('Failed to clean up Worker script file:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Copy the Code
|
||||
|
||||
Copy the above code to your project, e.g., `src/platform/NodeAdapter.ts`.
|
||||
|
||||
### 2. Register the Adapter
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { NodeAdapter } from './platform/NodeAdapter';
|
||||
|
||||
// Check if in Node.js environment
|
||||
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
|
||||
const nodeAdapter = new NodeAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(nodeAdapter);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use WorkerEntitySystem
|
||||
|
||||
The Node.js adapter works with WorkerEntitySystem, and the framework automatically handles Worker script creation:
|
||||
|
||||
```typescript
|
||||
import { WorkerEntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
import * as os from 'os';
|
||||
|
||||
class PhysicsSystem extends WorkerEntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: os.cpus().length, // Use all CPU cores
|
||||
useSharedArrayBuffer: true,
|
||||
systemConfig: { gravity: 9.8 }
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6; // x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
return {
|
||||
x: transform.x,
|
||||
y: transform.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: 1,
|
||||
radius: 10
|
||||
};
|
||||
}
|
||||
|
||||
// This function is automatically serialized and executed in Worker
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(entity => {
|
||||
// Apply gravity
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// Update position
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
transform.x = result.x;
|
||||
transform.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
}
|
||||
|
||||
interface PhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Get System Information
|
||||
|
||||
```typescript
|
||||
const manager = PlatformManager.getInstance();
|
||||
if (manager.hasAdapter()) {
|
||||
const adapter = manager.getAdapter();
|
||||
const deviceInfo = adapter.getDeviceInfo();
|
||||
|
||||
console.log('Node.js version:', deviceInfo.process?.version);
|
||||
console.log('CPU core count:', deviceInfo.cpus?.length);
|
||||
console.log('Total memory:', Math.round(deviceInfo.totalMemory! / 1024 / 1024), 'MB');
|
||||
console.log('Available memory:', Math.round(deviceInfo.freeMemory! / 1024 / 1024), 'MB');
|
||||
}
|
||||
```
|
||||
|
||||
## Official Documentation Reference
|
||||
|
||||
Node.js Worker Threads related official documentation:
|
||||
|
||||
- [Worker Threads Official Docs](https://nodejs.org/api/worker_threads.html)
|
||||
- [SharedArrayBuffer Support](https://nodejs.org/api/globals.html#class-sharedarraybuffer)
|
||||
- [OS Module Docs](https://nodejs.org/api/os.html)
|
||||
- [Process Module Docs](https://nodejs.org/api/process.html)
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Worker Threads Requirements
|
||||
|
||||
- **Node.js Version**: Requires Node.js 10.5.0+ (12+ recommended)
|
||||
- **Module Type**: Supports both CommonJS and ES modules
|
||||
- **Thread Limit**: Theoretically unlimited, but recommended not to exceed 2x CPU core count
|
||||
|
||||
### Performance Optimization Tips
|
||||
|
||||
#### 1. Worker Pool Management
|
||||
|
||||
```typescript
|
||||
class ServerPhysicsSystem extends WorkerEntitySystem {
|
||||
constructor() {
|
||||
const cpuCount = os.cpus().length;
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: Math.min(cpuCount * 2, 16), // Max 16 Workers
|
||||
entitiesPerWorker: 1000, // 1000 entities per Worker
|
||||
useSharedArrayBuffer: true,
|
||||
systemConfig: {
|
||||
gravity: 9.8,
|
||||
timeStep: 1/60
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Memory Management
|
||||
|
||||
```typescript
|
||||
class MemoryMonitor {
|
||||
public static checkMemoryUsage(): void {
|
||||
const used = process.memoryUsage();
|
||||
|
||||
console.log('Memory usage:');
|
||||
console.log(` RSS: ${Math.round(used.rss / 1024 / 1024)} MB`);
|
||||
console.log(` Heap Used: ${Math.round(used.heapUsed / 1024 / 1024)} MB`);
|
||||
console.log(` Heap Total: ${Math.round(used.heapTotal / 1024 / 1024)} MB`);
|
||||
console.log(` External: ${Math.round(used.external / 1024 / 1024)} MB`);
|
||||
|
||||
// Trigger warning when memory usage is too high
|
||||
if (used.heapUsed > used.heapTotal * 0.9) {
|
||||
console.warn('Memory usage too high, consider optimizing or restarting');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check memory usage periodically
|
||||
setInterval(() => {
|
||||
MemoryMonitor.checkMemoryUsage();
|
||||
}, 30000); // Check every 30 seconds
|
||||
```
|
||||
|
||||
#### 3. Server Environment Optimization
|
||||
|
||||
```typescript
|
||||
// Set process title
|
||||
process.title = 'ecs-game-server';
|
||||
|
||||
// Handle uncaught exceptions
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('Unhandled Promise rejection:', reason);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('Received SIGTERM signal, shutting down server...');
|
||||
// Clean up resources
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
```typescript
|
||||
// Check Node.js environment support
|
||||
const adapter = new NodeAdapter();
|
||||
console.log('Node.js version:', adapter.version);
|
||||
console.log('Worker support:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer support:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('CPU core count:', adapter.getHardwareConcurrency());
|
||||
|
||||
// Get detailed configuration
|
||||
const config = adapter.getPlatformConfig();
|
||||
console.log('Platform config:', JSON.stringify(config, null, 2));
|
||||
|
||||
// System resource monitoring
|
||||
const deviceInfo = adapter.getDeviceInfo();
|
||||
console.log('System load:', deviceInfo.loadAverage);
|
||||
console.log('Network interfaces:', Object.keys(deviceInfo.networkInterfaces!));
|
||||
```
|
||||
@@ -0,0 +1,485 @@
|
||||
---
|
||||
title: "WeChat Mini Game Adapter"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The WeChat Mini Game platform adapter is designed specifically for the WeChat Mini Game environment, handling special restrictions and APIs.
|
||||
|
||||
## Feature Support
|
||||
|
||||
| Feature | Support | Notes |
|
||||
|---------|---------|-------|
|
||||
| **Worker** | Supported | Requires precompiled file, configure `workerScriptPath` |
|
||||
| **SharedArrayBuffer** | Not Supported | WeChat Mini Game environment doesn't support this |
|
||||
| **Transferable Objects** | Not Supported | Only serializable objects supported |
|
||||
| **High-Resolution Time** | Supported | Uses `wx.getPerformance()` |
|
||||
| **Device Info** | Supported | Complete WeChat Mini Game device info |
|
||||
|
||||
## WorkerEntitySystem Usage
|
||||
|
||||
### Important: WeChat Mini Game Worker Restrictions
|
||||
|
||||
WeChat Mini Game Workers have the following restrictions:
|
||||
- **Worker scripts must be in the code package**, cannot be dynamically generated
|
||||
- **Must be configured in `game.json`** with `workers` directory
|
||||
- **Maximum of 1 Worker** can be created
|
||||
|
||||
Therefore, when using `WorkerEntitySystem`, there are two approaches:
|
||||
1. **Recommended: Use CLI tool** to automatically generate Worker files
|
||||
2. Manually create Worker files
|
||||
|
||||
### Method 1: Use CLI Tool for Auto-Generation (Recommended)
|
||||
|
||||
We provide the `@esengine/worker-generator` tool that can automatically extract `workerProcess` functions from your TypeScript code and generate WeChat Mini Game compatible Worker files.
|
||||
|
||||
#### Installation
|
||||
|
||||
```bash
|
||||
pnpm add -D @esengine/worker-generator
|
||||
# or
|
||||
npm install --save-dev @esengine/worker-generator
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
```bash
|
||||
# Scan src directory, generate Worker files to workers directory
|
||||
npx esengine-worker-gen --src ./src --out ./workers --wechat
|
||||
|
||||
# View help
|
||||
npx esengine-worker-gen --help
|
||||
```
|
||||
|
||||
#### Parameter Reference
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `-s, --src <dir>` | Source code directory | `./src` |
|
||||
| `-o, --out <dir>` | Output directory | `./workers` |
|
||||
| `-w, --wechat` | Generate WeChat Mini Game compatible code | `false` |
|
||||
| `-m, --mapping` | Generate worker-mapping.json | `true` |
|
||||
| `-t, --tsconfig <path>` | TypeScript config file path | Auto-detect |
|
||||
| `-v, --verbose` | Verbose output | `false` |
|
||||
|
||||
#### Example Output
|
||||
|
||||
```
|
||||
ESEngine Worker Generator
|
||||
|
||||
Source directory: /project/src
|
||||
Output directory: /project/workers
|
||||
WeChat mode: Yes
|
||||
|
||||
Scanning for WorkerEntitySystem classes...
|
||||
|
||||
Found 1 WorkerEntitySystem class(es):
|
||||
- PhysicsSystem (src/systems/PhysicsSystem.ts)
|
||||
|
||||
Generating Worker files...
|
||||
|
||||
Successfully generated 1 Worker file(s):
|
||||
- PhysicsSystem -> workers/physics-system-worker.js
|
||||
|
||||
Usage:
|
||||
1. Copy the generated files to your project's workers/ directory
|
||||
2. Configure game.json (WeChat): { "workers": "workers" }
|
||||
3. In your System constructor, add:
|
||||
workerScriptPath: 'workers/physics-system-worker.js'
|
||||
```
|
||||
|
||||
#### Integration in Build Process
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"build:workers": "esengine-worker-gen --src ./src --out ./workers --wechat",
|
||||
"build": "pnpm build:workers && your-build-command"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Method 2: Manually Create Worker Files
|
||||
|
||||
If you don't want to use the CLI tool, you can manually create Worker files.
|
||||
|
||||
Create `workers/entity-worker.js` in your project:
|
||||
|
||||
```javascript
|
||||
// workers/entity-worker.js
|
||||
// WeChat Mini Game WorkerEntitySystem Generic Worker Template
|
||||
|
||||
let sharedFloatArray = null;
|
||||
|
||||
worker.onMessage(function(e) {
|
||||
const { type, id, entities, deltaTime, systemConfig, startIndex, endIndex, sharedBuffer } = e.data;
|
||||
|
||||
try {
|
||||
// Handle SharedArrayBuffer initialization
|
||||
if (type === 'init' && sharedBuffer) {
|
||||
sharedFloatArray = new Float32Array(sharedBuffer);
|
||||
worker.postMessage({ type: 'init', success: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle SharedArrayBuffer data
|
||||
if (type === 'shared' && sharedFloatArray) {
|
||||
processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig);
|
||||
worker.postMessage({ id, result: null });
|
||||
return;
|
||||
}
|
||||
|
||||
// Traditional processing method
|
||||
if (entities) {
|
||||
const result = workerProcess(entities, deltaTime, systemConfig);
|
||||
|
||||
// Handle Promise return value
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(function(finalResult) {
|
||||
worker.postMessage({ id, result: finalResult });
|
||||
}).catch(function(error) {
|
||||
worker.postMessage({ id, error: error.message });
|
||||
});
|
||||
} else {
|
||||
worker.postMessage({ id, result: result });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
worker.postMessage({ id, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Entity processing function - Modify this function based on your business logic
|
||||
* @param {Array} entities - Entity data array
|
||||
* @param {number} deltaTime - Frame interval time
|
||||
* @param {Object} systemConfig - System configuration
|
||||
* @returns {Array} Processed entity data
|
||||
*/
|
||||
function workerProcess(entities, deltaTime, systemConfig) {
|
||||
// ====== Write your processing logic here ======
|
||||
// Example: Physics calculation
|
||||
return entities.map(function(entity) {
|
||||
// Apply gravity
|
||||
entity.vy += (systemConfig.gravity || 100) * deltaTime;
|
||||
|
||||
// Update position
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
// Apply friction
|
||||
entity.vx *= (systemConfig.friction || 0.95);
|
||||
entity.vy *= (systemConfig.friction || 0.95);
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SharedArrayBuffer processing function (optional)
|
||||
*/
|
||||
function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) {
|
||||
if (!sharedFloatArray) return;
|
||||
|
||||
// ====== Implement SharedArrayBuffer processing logic as needed ======
|
||||
// Note: WeChat Mini Game doesn't support SharedArrayBuffer, this function typically won't be called
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Configure game.json
|
||||
|
||||
Add workers configuration in `game.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"showStatusBar": false,
|
||||
"workers": "workers"
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Use WorkerEntitySystem
|
||||
|
||||
```typescript
|
||||
import { WorkerEntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
interface PhysicsData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
}
|
||||
|
||||
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: 1, // WeChat Mini Game limits to 1 Worker
|
||||
workerScriptPath: 'workers/entity-worker.js', // Specify precompiled Worker file
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6;
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: transform.x,
|
||||
y: transform.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: physics.mass
|
||||
};
|
||||
}
|
||||
|
||||
// Note: In WeChat Mini Game, this method won't be used
|
||||
// Worker processing logic is in workers/entity-worker.js workerProcess function
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
|
||||
return entities.map(entity => {
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
entity.vx *= config.friction;
|
||||
entity.vy *= config.friction;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
transform.x = result.x;
|
||||
transform.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
|
||||
// SharedArrayBuffer related methods (not supported in WeChat Mini Game, can be omitted)
|
||||
protected writeEntityToBuffer(data: PhysicsData, offset: number): void {}
|
||||
protected readEntityFromBuffer(offset: number): PhysicsData | null { return null; }
|
||||
}
|
||||
```
|
||||
|
||||
### Temporarily Disable Worker (Fallback to Sync Mode)
|
||||
|
||||
If you encounter issues, you can temporarily disable Worker:
|
||||
|
||||
```typescript
|
||||
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: false, // Disable Worker, use main thread synchronous processing
|
||||
// ... other config
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Adapter Implementation
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* WeChat Mini Game platform adapter
|
||||
*/
|
||||
export class WeChatMiniGameAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'wechat-minigame';
|
||||
public readonly version: string;
|
||||
private systemInfo: any;
|
||||
|
||||
constructor() {
|
||||
this.systemInfo = this.getSystemInfo();
|
||||
this.version = this.systemInfo.SDKVersion || 'unknown';
|
||||
}
|
||||
|
||||
public isWorkerSupported(): boolean {
|
||||
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
|
||||
}
|
||||
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getHardwareConcurrency(): number {
|
||||
return 1; // WeChat Mini Game max 1 Worker
|
||||
}
|
||||
|
||||
public createWorker(scriptPath: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('WeChat Mini Game environment does not support Worker');
|
||||
}
|
||||
|
||||
// scriptPath must be a file path in the code package
|
||||
const worker = wx.createWorker(scriptPath, {
|
||||
useExperimentalWorker: true
|
||||
});
|
||||
|
||||
return new WeChatWorker(worker);
|
||||
}
|
||||
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getHighResTimestamp(): number {
|
||||
if (typeof wx !== 'undefined' && wx.getPerformance) {
|
||||
return wx.getPerformance().now();
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: 1,
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: false,
|
||||
maxSharedArrayBufferSize: 0,
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: true, // Important: Mark that dynamic scripts not supported
|
||||
requiresWorkerInit: false,
|
||||
memoryLimit: 512 * 1024 * 1024,
|
||||
workerNotSupported: false,
|
||||
workerLimitations: [
|
||||
'Maximum of 1 Worker can be created',
|
||||
'Worker scripts must be in the code package',
|
||||
'workers path must be configured in game.json',
|
||||
'workerScriptPath configuration required'
|
||||
]
|
||||
},
|
||||
extensions: {
|
||||
platform: 'wechat-minigame',
|
||||
sdkVersion: this.systemInfo.SDKVersion
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private getSystemInfo(): any {
|
||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
||||
try {
|
||||
return wx.getSystemInfoSync();
|
||||
} catch (error) {
|
||||
console.warn('Failed to get WeChat system info:', error);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WeChat Worker wrapper
|
||||
*/
|
||||
class WeChatWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: any;
|
||||
|
||||
constructor(worker: any) {
|
||||
this.worker = worker;
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker has been terminated');
|
||||
}
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
this.worker.onMessage((res: any) => {
|
||||
handler({ data: res });
|
||||
});
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
if (this.worker.onError) {
|
||||
this.worker.onError(handler);
|
||||
}
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Register the Adapter
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
|
||||
|
||||
// Register adapter at game startup
|
||||
if (typeof wx !== 'undefined') {
|
||||
const adapter = new WeChatMiniGameAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(adapter);
|
||||
}
|
||||
```
|
||||
|
||||
## Official Documentation Reference
|
||||
|
||||
- [wx.createWorker API](https://developers.weixin.qq.com/minigame/dev/api/worker/wx.createWorker.html)
|
||||
- [Worker.postMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.postMessage.html)
|
||||
- [Worker.onMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.onMessage.html)
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Worker Restrictions
|
||||
|
||||
| Restriction | Description |
|
||||
|-------------|-------------|
|
||||
| Quantity Limit | Maximum of 1 Worker can be created |
|
||||
| Version Requirement | Requires base library 1.9.90 or above |
|
||||
| Script Location | Must be in code package, dynamic generation not supported |
|
||||
| Lifecycle | Must terminate() before creating new Worker |
|
||||
|
||||
### Memory Limits
|
||||
|
||||
- Typically limited to 256MB - 512MB
|
||||
- Need to release unused resources promptly
|
||||
- Recommend listening for memory warnings:
|
||||
|
||||
```typescript
|
||||
wx.onMemoryWarning(() => {
|
||||
console.warn('Received memory warning, starting resource cleanup');
|
||||
// Clean up unnecessary resources
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
```typescript
|
||||
// Check Worker configuration
|
||||
const adapter = PlatformManager.getInstance().getAdapter();
|
||||
const config = adapter.getPlatformConfig();
|
||||
|
||||
console.log('Worker support:', adapter.isWorkerSupported());
|
||||
console.log('Max Worker count:', config.maxWorkerCount);
|
||||
console.log('Platform limitations:', config.limitations);
|
||||
```
|
||||
151
docs/src/content/docs/en/guide/plugin-system/best-practices.md
Normal file
151
docs/src/content/docs/en/guide/plugin-system/best-practices.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Plugin design guidelines and common issues"
|
||||
---
|
||||
|
||||
## Naming Convention
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
// Use lowercase letters and hyphens
|
||||
readonly name = 'my-awesome-plugin'; // OK
|
||||
|
||||
// Follow semantic versioning
|
||||
readonly version = '1.0.0'; // OK
|
||||
}
|
||||
```
|
||||
|
||||
## Resource Cleanup
|
||||
|
||||
Always clean up all resources created by the plugin in `uninstall`:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private timerId?: number;
|
||||
private listener?: () => void;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Add timer
|
||||
this.timerId = setInterval(() => {
|
||||
// ...
|
||||
}, 1000);
|
||||
|
||||
// Add event listener
|
||||
this.listener = () => {};
|
||||
window.addEventListener('resize', this.listener);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Clear timer
|
||||
if (this.timerId) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = undefined;
|
||||
}
|
||||
|
||||
// Remove event listener
|
||||
if (this.listener) {
|
||||
window.removeEventListener('resize', this.listener);
|
||||
this.listener = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
try {
|
||||
await this.loadConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to load plugin config:', error);
|
||||
throw error; // Re-throw to let framework know installation failed
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
try {
|
||||
await this.cleanup();
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup plugin:', error);
|
||||
// Don't block uninstall even if cleanup fails
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig() { /* ... */ }
|
||||
private async cleanup() { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Allow users to configure plugin behavior:
|
||||
|
||||
```typescript
|
||||
interface NetworkPluginConfig {
|
||||
serverUrl: string;
|
||||
autoReconnect: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: NetworkPluginConfig) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const network = new NetworkService(this.config);
|
||||
services.registerInstance(NetworkService, network);
|
||||
}
|
||||
|
||||
uninstall(): void {}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const plugin = new NetworkPlugin({
|
||||
serverUrl: 'ws://localhost:8080',
|
||||
autoReconnect: true,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
await Core.installPlugin(plugin);
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Plugin Installation Failed
|
||||
|
||||
**Causes**:
|
||||
- Dependencies not satisfied
|
||||
- Exception in install method
|
||||
- Service registration conflict
|
||||
|
||||
**Solutions**:
|
||||
1. Check if dependencies are installed
|
||||
2. Review error logs
|
||||
3. Ensure service names don't conflict
|
||||
|
||||
### Side Effects After Uninstall
|
||||
|
||||
**Cause**: Resources not properly cleaned in uninstall
|
||||
|
||||
**Solution**: Ensure uninstall cleans up:
|
||||
- Timers
|
||||
- Event listeners
|
||||
- WebSocket connections
|
||||
- System references
|
||||
|
||||
### When to Use Plugins
|
||||
|
||||
| Good for Plugins | Not Good for Plugins |
|
||||
|------------------|---------------------|
|
||||
| Optional features (debug tools, profiling) | Core game logic |
|
||||
| Third-party integration (network libs, physics) | Simple utilities |
|
||||
| Cross-project reusable modules | Project-specific features |
|
||||
106
docs/src/content/docs/en/guide/plugin-system/dependencies.md
Normal file
106
docs/src/content/docs/en/guide/plugin-system/dependencies.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: "Dependency Management"
|
||||
description: "Declare and check plugin dependencies"
|
||||
---
|
||||
|
||||
## Declaring Dependencies
|
||||
|
||||
Plugins can declare dependencies on other plugins:
|
||||
|
||||
```typescript
|
||||
class AdvancedPhysicsPlugin implements IPlugin {
|
||||
readonly name = 'advanced-physics';
|
||||
readonly version = '2.0.0';
|
||||
|
||||
// Declare dependency on base physics plugin
|
||||
readonly dependencies = ['physics-plugin'] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Can safely use services from physics-plugin
|
||||
const physicsService = services.resolve(PhysicsService);
|
||||
// ...
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Checking
|
||||
|
||||
The framework automatically checks dependencies and throws an error if not satisfied:
|
||||
|
||||
```typescript
|
||||
// Error: physics-plugin not installed
|
||||
try {
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// Plugin advanced-physics has unmet dependencies: physics-plugin
|
||||
}
|
||||
|
||||
// Correct: install dependency first
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
```
|
||||
|
||||
## Uninstall Order
|
||||
|
||||
The framework checks dependencies to prevent uninstalling plugins required by others:
|
||||
|
||||
```typescript
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
|
||||
// Error: physics-plugin is required by advanced-physics
|
||||
try {
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// Cannot uninstall plugin physics-plugin: it is required by advanced-physics
|
||||
}
|
||||
|
||||
// Correct: uninstall dependent plugin first
|
||||
await Core.uninstallPlugin('advanced-physics');
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
```
|
||||
|
||||
## Dependency Graph Example
|
||||
|
||||
```
|
||||
physics-plugin (base)
|
||||
↑
|
||||
advanced-physics (depends on physics-plugin)
|
||||
↑
|
||||
game-physics (depends on advanced-physics)
|
||||
```
|
||||
|
||||
Install order: `physics-plugin` → `advanced-physics` → `game-physics`
|
||||
|
||||
Uninstall order: `game-physics` → `advanced-physics` → `physics-plugin`
|
||||
|
||||
## Multiple Dependencies
|
||||
|
||||
```typescript
|
||||
class GamePlugin implements IPlugin {
|
||||
readonly name = 'game';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
// Declare multiple dependencies
|
||||
readonly dependencies = [
|
||||
'physics-plugin',
|
||||
'network-plugin',
|
||||
'audio-plugin'
|
||||
] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// All dependencies are available
|
||||
const physics = services.resolve(PhysicsService);
|
||||
const network = services.resolve(NetworkService);
|
||||
const audio = services.resolve(AudioService);
|
||||
}
|
||||
|
||||
uninstall(): void {}
|
||||
}
|
||||
```
|
||||
139
docs/src/content/docs/en/guide/plugin-system/development.md
Normal file
139
docs/src/content/docs/en/guide/plugin-system/development.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
title: "Plugin Development"
|
||||
description: "IPlugin interface and lifecycle"
|
||||
---
|
||||
|
||||
## IPlugin Interface
|
||||
|
||||
All plugins must implement the `IPlugin` interface:
|
||||
|
||||
```typescript
|
||||
export interface IPlugin {
|
||||
// Unique plugin name
|
||||
readonly name: string;
|
||||
|
||||
// Plugin version (semver recommended)
|
||||
readonly version: string;
|
||||
|
||||
// Dependencies on other plugins (optional)
|
||||
readonly dependencies?: readonly string[];
|
||||
|
||||
// Called when plugin is installed
|
||||
install(core: Core, services: ServiceContainer): void | Promise<void>;
|
||||
|
||||
// Called when plugin is uninstalled
|
||||
uninstall(): void | Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle Methods
|
||||
|
||||
### install Method
|
||||
|
||||
Called when the plugin is installed, used for initialization:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. Register services
|
||||
services.registerSingleton(MyService);
|
||||
|
||||
// 2. Access current scene
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
// 3. Add systems
|
||||
scene.addSystem(new MySystem());
|
||||
}
|
||||
|
||||
// 4. Other initialization
|
||||
console.log('Plugin initialized');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### uninstall Method
|
||||
|
||||
Called when the plugin is uninstalled, used for cleanup:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private myService?: MyService;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.myService = new MyService();
|
||||
services.registerInstance(MyService, this.myService);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup service
|
||||
if (this.myService) {
|
||||
this.myService.dispose();
|
||||
this.myService = undefined;
|
||||
}
|
||||
|
||||
// Remove event listeners
|
||||
// Release other resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Async Plugins
|
||||
|
||||
Both `install` and `uninstall` methods support async:
|
||||
|
||||
```typescript
|
||||
class AsyncPlugin implements IPlugin {
|
||||
readonly name = 'async-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
// Async load resources
|
||||
const config = await fetch('/plugin-config.json').then(r => r.json());
|
||||
|
||||
// Initialize service with loaded config
|
||||
const service = new MyService(config);
|
||||
services.registerInstance(MyService, service);
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
// Async cleanup
|
||||
await this.saveState();
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
// Save plugin state
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
await Core.installPlugin(new AsyncPlugin());
|
||||
```
|
||||
|
||||
## Lifecycle Flow
|
||||
|
||||
```
|
||||
Install: Core.installPlugin(plugin)
|
||||
↓
|
||||
Dependency check: Verify dependencies are satisfied
|
||||
↓
|
||||
Call install(): Register services, add systems
|
||||
↓
|
||||
State update: Mark as installed
|
||||
|
||||
Uninstall: Core.uninstallPlugin(name)
|
||||
↓
|
||||
Dependency check: Verify not required by other plugins
|
||||
↓
|
||||
Call uninstall(): Cleanup resources
|
||||
↓
|
||||
State update: Remove from plugin list
|
||||
```
|
||||
188
docs/src/content/docs/en/guide/plugin-system/examples.md
Normal file
188
docs/src/content/docs/en/guide/plugin-system/examples.md
Normal file
@@ -0,0 +1,188 @@
|
||||
---
|
||||
title: "Example Plugins"
|
||||
description: "Complete plugin implementation examples"
|
||||
---
|
||||
|
||||
## Network Sync Plugin
|
||||
|
||||
```typescript
|
||||
import { IPlugin, IService, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkSyncService implements IService {
|
||||
private ws?: WebSocket;
|
||||
|
||||
connect(url: string) {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
};
|
||||
}
|
||||
|
||||
private handleMessage(data: any) {
|
||||
// Handle network messages
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkSyncPlugin implements IPlugin {
|
||||
readonly name = 'network-sync';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Register network service
|
||||
services.registerSingleton(NetworkSyncService);
|
||||
|
||||
// Auto connect
|
||||
const network = services.resolve(NetworkSyncService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Service will auto-dispose
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Analysis Plugin
|
||||
|
||||
```typescript
|
||||
class PerformanceAnalysisPlugin implements IPlugin {
|
||||
readonly name = 'performance-analysis';
|
||||
readonly version = '1.0.0';
|
||||
private frameCount = 0;
|
||||
private totalTime = 0;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const monitor = services.resolve(PerformanceMonitor);
|
||||
monitor.enable();
|
||||
|
||||
// Periodic performance report
|
||||
const timer = services.resolve(TimerManager);
|
||||
timer.schedule(5.0, true, null, () => {
|
||||
this.printReport(monitor);
|
||||
});
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup
|
||||
}
|
||||
|
||||
private printReport(monitor: PerformanceMonitor) {
|
||||
console.log('=== Performance Report ===');
|
||||
console.log(`FPS: ${monitor.getFPS()}`);
|
||||
console.log(`Memory: ${monitor.getMemoryUsage()} MB`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debug Tools Plugin
|
||||
|
||||
```typescript
|
||||
class DebugToolsPlugin implements IPlugin {
|
||||
readonly name = 'debug-tools';
|
||||
readonly version = '1.0.0';
|
||||
private debugUI?: DebugUI;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Create debug UI
|
||||
this.debugUI = new DebugUI();
|
||||
this.debugUI.mount(document.body);
|
||||
|
||||
// Register hotkey
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
// Add debug system
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
scene.addSystem(new DebugRenderSystem());
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Remove UI
|
||||
if (this.debugUI) {
|
||||
this.debugUI.unmount();
|
||||
this.debugUI = undefined;
|
||||
}
|
||||
|
||||
// Remove event listener
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'F12') {
|
||||
this.debugUI?.toggle();
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Audio Plugin
|
||||
|
||||
```typescript
|
||||
class AudioPlugin implements IPlugin {
|
||||
readonly name = 'audio';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: { volume: number }) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const audioService = new AudioService(this.config);
|
||||
services.registerInstance(AudioService, audioService);
|
||||
|
||||
// Add audio system
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
scene.addSystem(new AudioSystem());
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Stop all audio
|
||||
const audio = Core.services.resolve(AudioService);
|
||||
audio.stopAll();
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
await Core.installPlugin(new AudioPlugin({ volume: 0.8 }));
|
||||
```
|
||||
|
||||
## Input Manager Plugin
|
||||
|
||||
```typescript
|
||||
class InputPlugin implements IPlugin {
|
||||
readonly name = 'input';
|
||||
readonly version = '1.0.0';
|
||||
private inputManager?: InputManager;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.inputManager = new InputManager();
|
||||
services.registerInstance(InputManager, this.inputManager);
|
||||
|
||||
// Bind default keys
|
||||
this.inputManager.bind('jump', ['Space', 'KeyW']);
|
||||
this.inputManager.bind('attack', ['MouseLeft', 'KeyJ']);
|
||||
|
||||
// Add input system
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
scene.addSystem(new InputSystem());
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
if (this.inputManager) {
|
||||
this.inputManager.dispose();
|
||||
this.inputManager = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
85
docs/src/content/docs/en/guide/plugin-system/index.md
Normal file
85
docs/src/content/docs/en/guide/plugin-system/index.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: "Plugin System"
|
||||
description: "Extend ECS Framework in a modular way"
|
||||
---
|
||||
|
||||
The plugin system allows you to extend ECS Framework functionality in a modular way. Through plugins, you can encapsulate specific features (like network sync, physics engines, debug tools) and reuse them across multiple projects.
|
||||
|
||||
## What is a Plugin
|
||||
|
||||
A plugin is a class that implements the `IPlugin` interface and can be dynamically installed into the framework at runtime. Plugins can:
|
||||
|
||||
- Register custom services to the service container
|
||||
- Add systems to scenes
|
||||
- Register custom components
|
||||
- Extend framework functionality
|
||||
|
||||
## Plugin Benefits
|
||||
|
||||
| Benefit | Description |
|
||||
|---------|-------------|
|
||||
| **Modular** | Encapsulate functionality as independent modules |
|
||||
| **Reusable** | Use the same plugin across multiple projects |
|
||||
| **Decoupled** | Separate core framework from extensions |
|
||||
| **Hot-swappable** | Dynamically install and uninstall at runtime |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Create a Plugin
|
||||
|
||||
```typescript
|
||||
import { IPlugin, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class DebugPlugin implements IPlugin {
|
||||
readonly name = 'debug-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
console.log('Debug plugin installed');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
console.log('Debug plugin uninstalled');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Install a Plugin
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
|
||||
// Install plugin
|
||||
await Core.installPlugin(new DebugPlugin());
|
||||
|
||||
// Check if plugin is installed
|
||||
if (Core.isPluginInstalled('debug-plugin')) {
|
||||
console.log('Debug plugin is running');
|
||||
}
|
||||
```
|
||||
|
||||
### Uninstall a Plugin
|
||||
|
||||
```typescript
|
||||
await Core.uninstallPlugin('debug-plugin');
|
||||
```
|
||||
|
||||
### Get Plugin Instance
|
||||
|
||||
```typescript
|
||||
const plugin = Core.getPlugin('debug-plugin');
|
||||
if (plugin) {
|
||||
console.log(`Plugin version: ${plugin.version}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Development](./development/) - IPlugin interface and lifecycle
|
||||
- [Services & Systems](./services-systems/) - Register services and add systems
|
||||
- [Dependencies](./dependencies/) - Declare and check dependencies
|
||||
- [Management](./management/) - Manage via Core and PluginManager
|
||||
- [Examples](./examples/) - Complete examples
|
||||
- [Best Practices](./best-practices/) - Design guidelines
|
||||
93
docs/src/content/docs/en/guide/plugin-system/management.md
Normal file
93
docs/src/content/docs/en/guide/plugin-system/management.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: "Plugin Management"
|
||||
description: "Manage plugins via Core and PluginManager"
|
||||
---
|
||||
|
||||
## Via Core
|
||||
|
||||
Core class provides convenient plugin management methods:
|
||||
|
||||
```typescript
|
||||
// Install plugin
|
||||
await Core.installPlugin(myPlugin);
|
||||
|
||||
// Uninstall plugin
|
||||
await Core.uninstallPlugin('plugin-name');
|
||||
|
||||
// Check if plugin is installed
|
||||
if (Core.isPluginInstalled('plugin-name')) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Get plugin instance
|
||||
const plugin = Core.getPlugin('plugin-name');
|
||||
```
|
||||
|
||||
## Via PluginManager
|
||||
|
||||
You can also use the PluginManager service directly:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// Get all plugins
|
||||
const allPlugins = pluginManager.getAllPlugins();
|
||||
console.log(`Total plugins: ${allPlugins.length}`);
|
||||
|
||||
// Get plugin metadata
|
||||
const metadata = pluginManager.getMetadata('my-plugin');
|
||||
if (metadata) {
|
||||
console.log(`State: ${metadata.state}`);
|
||||
console.log(`Installed at: ${new Date(metadata.installedAt!)}`);
|
||||
}
|
||||
|
||||
// Get all plugin metadata
|
||||
const allMetadata = pluginManager.getAllMetadata();
|
||||
for (const meta of allMetadata) {
|
||||
console.log(`${meta.name} v${meta.version} - ${meta.state}`);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Core Static Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `installPlugin(plugin)` | Install plugin |
|
||||
| `uninstallPlugin(name)` | Uninstall plugin |
|
||||
| `isPluginInstalled(name)` | Check if installed |
|
||||
| `getPlugin(name)` | Get plugin instance |
|
||||
|
||||
### PluginManager Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `getAllPlugins()` | Get all plugins |
|
||||
| `getMetadata(name)` | Get plugin metadata |
|
||||
| `getAllMetadata()` | Get all plugin metadata |
|
||||
|
||||
## Plugin States
|
||||
|
||||
```typescript
|
||||
enum PluginState {
|
||||
Pending = 'pending',
|
||||
Installing = 'installing',
|
||||
Installed = 'installed',
|
||||
Uninstalling = 'uninstalling',
|
||||
Failed = 'failed'
|
||||
}
|
||||
```
|
||||
|
||||
## Metadata Information
|
||||
|
||||
```typescript
|
||||
interface PluginMetadata {
|
||||
name: string;
|
||||
version: string;
|
||||
state: PluginState;
|
||||
dependencies?: string[];
|
||||
installedAt?: number;
|
||||
error?: Error;
|
||||
}
|
||||
```
|
||||
133
docs/src/content/docs/en/guide/plugin-system/services-systems.md
Normal file
133
docs/src/content/docs/en/guide/plugin-system/services-systems.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "Services & Systems"
|
||||
description: "Register services and add systems in plugins"
|
||||
---
|
||||
|
||||
## Registering Services
|
||||
|
||||
Plugins can register their own services to the service container:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkService implements IService {
|
||||
connect(url: string) {
|
||||
console.log(`Connecting to ${url}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('Network service disposed');
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Register network service
|
||||
services.registerSingleton(NetworkService);
|
||||
|
||||
// Resolve and use service
|
||||
const network = services.resolve(NetworkService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Service container will auto-call service's dispose method
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Service Registration Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `registerSingleton(Type)` | Register singleton service |
|
||||
| `registerInstance(Type, instance)` | Register existing instance |
|
||||
| `registerTransient(Type)` | Create new instance per resolve |
|
||||
|
||||
## Adding Systems
|
||||
|
||||
Plugins can add custom systems to scenes:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PhysicsBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Physics simulation logic
|
||||
}
|
||||
}
|
||||
|
||||
class PhysicsPlugin implements IPlugin {
|
||||
readonly name = 'physics-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private physicsSystem?: PhysicsSystem;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
this.physicsSystem = new PhysicsSystem();
|
||||
scene.addSystem(this.physicsSystem);
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Remove system
|
||||
if (this.physicsSystem) {
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
scene.removeSystem(this.physicsSystem);
|
||||
}
|
||||
this.physicsSystem = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Combined Usage
|
||||
|
||||
```typescript
|
||||
class GamePlugin implements IPlugin {
|
||||
readonly name = 'game-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private systems: EntitySystem[] = [];
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. Register services
|
||||
services.registerSingleton(ScoreService);
|
||||
services.registerSingleton(AudioService);
|
||||
|
||||
// 2. Add systems
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
const systems = [
|
||||
new InputSystem(),
|
||||
new MovementSystem(),
|
||||
new ScoringSystem()
|
||||
];
|
||||
|
||||
systems.forEach(system => {
|
||||
scene.addSystem(system);
|
||||
this.systems.push(system);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Remove all systems
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
this.systems.forEach(system => {
|
||||
scene.removeSystem(system);
|
||||
});
|
||||
}
|
||||
this.systems = [];
|
||||
}
|
||||
}
|
||||
```
|
||||
440
docs/src/content/docs/en/guide/scene-manager.md
Normal file
440
docs/src/content/docs/en/guide/scene-manager.md
Normal file
@@ -0,0 +1,440 @@
|
||||
---
|
||||
title: "scene-manager"
|
||||
---
|
||||
|
||||
# SceneManager
|
||||
|
||||
SceneManager is a lightweight scene manager provided by ECS Framework, suitable for 95% of game applications. It provides a simple and intuitive API with support for scene transitions and delayed loading.
|
||||
|
||||
## Use Cases
|
||||
|
||||
SceneManager is suitable for:
|
||||
- Single-player games
|
||||
- Simple multiplayer games
|
||||
- Mobile games
|
||||
- Games requiring scene transitions (menu, game, pause, etc.)
|
||||
- Projects that don't need multi-World isolation
|
||||
|
||||
## Features
|
||||
|
||||
- Lightweight, zero extra overhead
|
||||
- Simple and intuitive API
|
||||
- Supports delayed scene transitions (avoids switching mid-frame)
|
||||
- Automatic ECS fluent API management
|
||||
- Automatic scene lifecycle handling
|
||||
- Integrated with Core, auto-updated
|
||||
- Supports [Persistent Entity](./persistent-entity) migration across scenes (v2.3.0+)
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Recommended: Using Core's Static Methods
|
||||
|
||||
This is the simplest and recommended approach, suitable for most applications:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 1. Initialize Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 2. Create and set scene
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// Add systems
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// Create initial entities
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("Game scene started");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Set scene
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 4. Game loop (Core.update automatically updates the scene)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Automatically updates all services and scenes
|
||||
}
|
||||
|
||||
// Laya engine integration
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime);
|
||||
});
|
||||
|
||||
// Cocos Creator integration
|
||||
update(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced: Using SceneManager Directly
|
||||
|
||||
If you need more control, you can use SceneManager directly:
|
||||
|
||||
```typescript
|
||||
import { Core, SceneManager, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// Initialize Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// Get SceneManager (already auto-created and registered by Core)
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
|
||||
// Set scene
|
||||
const gameScene = new GameScene();
|
||||
sceneManager.setScene(gameScene);
|
||||
|
||||
// Game loop (still use Core.update)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Core automatically calls sceneManager.update()
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: Regardless of which approach you use, you should only call `Core.update()` in the game loop. It automatically updates SceneManager and scenes. You don't need to manually call `sceneManager.update()`.
|
||||
|
||||
## Scene Transitions
|
||||
|
||||
### Immediate Transition
|
||||
|
||||
Use `Core.setScene()` or `sceneManager.setScene()` to immediately switch scenes:
|
||||
|
||||
```typescript
|
||||
// Method 1: Using Core (recommended)
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// Method 2: Using SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new MenuScene());
|
||||
```
|
||||
|
||||
### Delayed Transition
|
||||
|
||||
Use `Core.loadScene()` or `sceneManager.loadScene()` for delayed scene transition, which takes effect on the next frame:
|
||||
|
||||
```typescript
|
||||
// Method 1: Using Core (recommended)
|
||||
Core.loadScene(new GameOverScene());
|
||||
|
||||
// Method 2: Using SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
When switching scenes from within a System, use delayed transitions:
|
||||
|
||||
```typescript
|
||||
class GameOverSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
const player = entities.find(e => e.name === 'Player');
|
||||
const health = player?.getComponent(Health);
|
||||
|
||||
if (health && health.value <= 0) {
|
||||
// Delayed transition to game over scene (takes effect next frame)
|
||||
Core.loadScene(new GameOverScene());
|
||||
// Current frame continues execution, won't interrupt current system processing
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Core Static Methods (Recommended)
|
||||
|
||||
#### Core.setScene()
|
||||
|
||||
Immediately switch scenes.
|
||||
|
||||
```typescript
|
||||
public static setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `scene` - The scene instance to set
|
||||
|
||||
**Returns**:
|
||||
- Returns the set scene instance
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
const gameScene = Core.setScene(new GameScene());
|
||||
console.log(gameScene.name);
|
||||
```
|
||||
|
||||
#### Core.loadScene()
|
||||
|
||||
Delayed scene loading (switches on next frame).
|
||||
|
||||
```typescript
|
||||
public static loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `scene` - The scene instance to load
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
Core.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
#### Core.scene
|
||||
|
||||
Get the currently active scene.
|
||||
|
||||
```typescript
|
||||
public static get scene(): IScene | null
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
- Current scene instance, or null if no scene
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
const currentScene = Core.scene;
|
||||
if (currentScene) {
|
||||
console.log(`Current scene: ${currentScene.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
### SceneManager Methods (Advanced)
|
||||
|
||||
If you need to use SceneManager directly, get it through the service container:
|
||||
|
||||
```typescript
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
```
|
||||
|
||||
#### setScene()
|
||||
|
||||
Immediately switch scenes.
|
||||
|
||||
```typescript
|
||||
public setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
#### loadScene()
|
||||
|
||||
Delayed scene loading.
|
||||
|
||||
```typescript
|
||||
public loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
#### currentScene
|
||||
|
||||
Get the current scene.
|
||||
|
||||
```typescript
|
||||
public get currentScene(): IScene | null
|
||||
```
|
||||
|
||||
#### hasScene
|
||||
|
||||
Check if there's an active scene.
|
||||
|
||||
```typescript
|
||||
public get hasScene(): boolean
|
||||
```
|
||||
|
||||
#### hasPendingScene
|
||||
|
||||
Check if there's a pending scene transition.
|
||||
|
||||
```typescript
|
||||
public get hasPendingScene(): boolean
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Core's Static Methods
|
||||
|
||||
```typescript
|
||||
// Recommended: Use Core's static methods
|
||||
Core.setScene(new GameScene());
|
||||
Core.loadScene(new MenuScene());
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// Not recommended: Don't directly use SceneManager unless you have special needs
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
### 2. Only Call Core.update()
|
||||
|
||||
```typescript
|
||||
// Correct: Only call Core.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Automatically updates all services and scenes
|
||||
}
|
||||
|
||||
// Incorrect: Don't manually call sceneManager.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
sceneManager.update(); // Duplicate update, will cause issues!
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Delayed Transitions to Avoid Issues
|
||||
|
||||
When switching scenes from within a System, use `loadScene()` instead of `setScene()`:
|
||||
|
||||
```typescript
|
||||
// Recommended: Delayed transition
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.loadScene(new GameOverScene());
|
||||
// Current frame continues processing other entities
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not recommended: Immediate transition may cause issues
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.setScene(new GameOverScene());
|
||||
// Scene switches immediately, other entities in current frame may not process correctly
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Scene Responsibility Separation
|
||||
|
||||
Each scene should be responsible for only one specific game state:
|
||||
|
||||
```typescript
|
||||
// Good design - clear responsibilities
|
||||
class MenuScene extends Scene {
|
||||
// Only handles menu-related logic
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// Only handles gameplay logic
|
||||
}
|
||||
|
||||
class PauseScene extends Scene {
|
||||
// Only handles pause screen logic
|
||||
}
|
||||
|
||||
// Avoid this design - mixed responsibilities
|
||||
class MegaScene extends Scene {
|
||||
// Contains menu, game, pause, and all other logic
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Resource Management
|
||||
|
||||
Clean up resources in the scene's `unload()` method:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
this.textures.set('player', loadTexture('player.png'));
|
||||
this.sounds.set('bgm', loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Cleanup resources
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('Scene resources cleaned up');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Event-Driven Scene Transitions
|
||||
|
||||
Use the event system to trigger scene transitions, keeping code decoupled:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Listen to scene transition events
|
||||
this.eventSystem.on('goto:menu', () => {
|
||||
Core.loadScene(new MenuScene());
|
||||
});
|
||||
|
||||
this.eventSystem.on('goto:gameover', (data) => {
|
||||
Core.loadScene(new GameOverScene());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger events in System
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
if (levelComplete) {
|
||||
this.scene.eventSystem.emitSync('goto:gameover', {
|
||||
score: 1000,
|
||||
level: 5
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
SceneManager's position in ECS Framework:
|
||||
|
||||
```
|
||||
Core (Global Services)
|
||||
└── SceneManager (Scene Management, auto-updated)
|
||||
└── Scene (Current Scene)
|
||||
├── EntitySystem (Systems)
|
||||
├── Entity (Entities)
|
||||
└── Component (Components)
|
||||
```
|
||||
|
||||
## Comparison with WorldManager
|
||||
|
||||
| Feature | SceneManager | WorldManager |
|
||||
|---------|--------------|--------------|
|
||||
| Use Case | 95% of game applications | Advanced multi-world isolation scenarios |
|
||||
| Complexity | Simple | Complex |
|
||||
| Scene Count | Single scene (switchable) | Multiple Worlds, each with multiple scenes |
|
||||
| Performance Overhead | Minimal | Higher |
|
||||
| Usage | `Core.setScene()` | `worldManager.createWorld()` |
|
||||
|
||||
**When to use SceneManager**:
|
||||
- Single-player games
|
||||
- Simple multiplayer games
|
||||
- Mobile games
|
||||
- Scenes that need transitions but don't need to run simultaneously
|
||||
|
||||
**When to use WorldManager**:
|
||||
- MMO game servers (one World per room)
|
||||
- Game lobby systems (complete isolation per game room)
|
||||
- Need to run multiple completely independent game instances
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Persistent Entity](./persistent-entity) - Learn how to keep entities across scene transitions
|
||||
- [WorldManager](./world-manager) - Learn about advanced multi-world isolation features
|
||||
|
||||
SceneManager provides simple yet powerful scene management capabilities for most games. Through Core's static methods, you can easily manage scene transitions.
|
||||
179
docs/src/content/docs/en/guide/scene/best-practices.md
Normal file
179
docs/src/content/docs/en/guide/scene/best-practices.md
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Scene design patterns and complete examples"
|
||||
---
|
||||
|
||||
## Scene Responsibility Separation
|
||||
|
||||
```typescript
|
||||
// Good scene design - clear responsibilities
|
||||
class MenuScene extends Scene {
|
||||
// Only handles menu-related logic
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// Only handles gameplay logic
|
||||
}
|
||||
|
||||
class InventoryScene extends Scene {
|
||||
// Only handles inventory logic
|
||||
}
|
||||
|
||||
// Avoid this design - mixed responsibilities
|
||||
class MegaScene extends Scene {
|
||||
// Contains menu, game, inventory, and all other logic
|
||||
}
|
||||
```
|
||||
|
||||
## Resource Management
|
||||
|
||||
```typescript
|
||||
class ResourceScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
this.textures.set('player', this.loadTexture('player.png'));
|
||||
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Cleanup resources
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('Scene resources cleaned up');
|
||||
}
|
||||
|
||||
private loadTexture(path: string): any { return null; }
|
||||
private loadSound(path: string): any { return null; }
|
||||
}
|
||||
```
|
||||
|
||||
## Initialization Order
|
||||
|
||||
```typescript
|
||||
class ProperInitScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 1. First set scene configuration
|
||||
this.name = "GameScene";
|
||||
|
||||
// 2. Then add systems (by dependency order)
|
||||
this.addSystem(new InputSystem());
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 3. Create entities last
|
||||
this.createEntities();
|
||||
|
||||
// 4. Setup event listeners
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
private createEntities(): void { /* ... */ }
|
||||
private setupEvents(): void { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
// Define components
|
||||
class Transform {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
class Velocity {
|
||||
constructor(public vx: number, public vy: number) {}
|
||||
}
|
||||
|
||||
class Health {
|
||||
constructor(public value: number) {}
|
||||
}
|
||||
|
||||
// Define system
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (transform && velocity) {
|
||||
transform.x += velocity.vx;
|
||||
transform.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define scene
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// Add systems
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
// Create player
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Velocity(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// Create enemies
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Transform(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new Velocity(
|
||||
Math.random() * 100 - 50,
|
||||
Math.random() * 100 - 50
|
||||
));
|
||||
enemy.addComponent(new Health(50));
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
this.eventSystem.on('player_died', () => {
|
||||
console.log('Player died!');
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('Game scene started');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('Game scene unloaded');
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Use scene
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
| Principle | Description |
|
||||
|-----------|-------------|
|
||||
| Single Responsibility | Each scene handles one game state |
|
||||
| Resource Cleanup | Clean up all resources in `unload()` |
|
||||
| System Order | Add systems: Input → Logic → Render |
|
||||
| Event Decoupling | Use event system for scene communication |
|
||||
| Layered Initialization | Config → Systems → Entities → Events |
|
||||
124
docs/src/content/docs/en/guide/scene/debugging.md
Normal file
124
docs/src/content/docs/en/guide/scene/debugging.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: "Debugging & Monitoring"
|
||||
description: "Scene statistics, performance monitoring and debugging"
|
||||
---
|
||||
|
||||
Scene includes complete debugging and performance monitoring features.
|
||||
|
||||
## Scene Statistics
|
||||
|
||||
```typescript
|
||||
class StatsScene extends Scene {
|
||||
public showStats(): void {
|
||||
const stats = this.getStats();
|
||||
console.log(`Entity count: ${stats.entityCount}`);
|
||||
console.log(`System count: ${stats.processorCount}`);
|
||||
console.log('Component storage stats:', stats.componentStorageStats);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debug Information
|
||||
|
||||
```typescript
|
||||
public showDebugInfo(): void {
|
||||
const debugInfo = this.getDebugInfo();
|
||||
console.log('Scene debug info:', debugInfo);
|
||||
|
||||
// Display all entity info
|
||||
debugInfo.entities.forEach(entity => {
|
||||
console.log(`Entity ${entity.name}(${entity.id}): ${entity.componentCount} components`);
|
||||
console.log('Component types:', entity.componentTypes);
|
||||
});
|
||||
|
||||
// Display all system info
|
||||
debugInfo.processors.forEach(processor => {
|
||||
console.log(`System ${processor.name}: processing ${processor.entityCount} entities`);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
```typescript
|
||||
class PerformanceScene extends Scene {
|
||||
public showPerformance(): void {
|
||||
// Get performance data
|
||||
const perfData = this.performanceMonitor?.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log('FPS:', perfData.fps);
|
||||
console.log('Frame time:', perfData.frameTime);
|
||||
console.log('Entity update time:', perfData.entityUpdateTime);
|
||||
console.log('System update time:', perfData.systemUpdateTime);
|
||||
}
|
||||
|
||||
// Get performance report
|
||||
const report = this.performanceMonitor?.generateReport();
|
||||
if (report) {
|
||||
console.log('Performance report:', report);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### getStats()
|
||||
|
||||
Returns scene statistics:
|
||||
|
||||
```typescript
|
||||
interface SceneStats {
|
||||
entityCount: number;
|
||||
processorCount: number;
|
||||
componentStorageStats: ComponentStorageStats;
|
||||
}
|
||||
```
|
||||
|
||||
### getDebugInfo()
|
||||
|
||||
Returns detailed debug information:
|
||||
|
||||
```typescript
|
||||
interface DebugInfo {
|
||||
entities: EntityDebugInfo[];
|
||||
processors: ProcessorDebugInfo[];
|
||||
}
|
||||
|
||||
interface EntityDebugInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
componentCount: number;
|
||||
componentTypes: string[];
|
||||
}
|
||||
|
||||
interface ProcessorDebugInfo {
|
||||
name: string;
|
||||
entityCount: number;
|
||||
}
|
||||
```
|
||||
|
||||
### performanceMonitor
|
||||
|
||||
Performance monitor interface:
|
||||
|
||||
```typescript
|
||||
interface PerformanceMonitor {
|
||||
getPerformanceData(): PerformanceData;
|
||||
generateReport(): string;
|
||||
}
|
||||
|
||||
interface PerformanceData {
|
||||
fps: number;
|
||||
frameTime: number;
|
||||
entityUpdateTime: number;
|
||||
systemUpdateTime: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
1. **Debug mode** - Enable with `Core.create({ debug: true })`
|
||||
2. **Performance analysis** - Call `getStats()` periodically
|
||||
3. **Memory monitoring** - Check `componentStorageStats` for issues
|
||||
4. **System performance** - Use `performanceMonitor` to identify slow systems
|
||||
125
docs/src/content/docs/en/guide/scene/entity-management.md
Normal file
125
docs/src/content/docs/en/guide/scene/entity-management.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
title: "Entity Management"
|
||||
description: "Entity creation, finding and destruction in scenes"
|
||||
---
|
||||
|
||||
Scene provides complete entity management APIs for creating, finding, and destroying entities.
|
||||
|
||||
## Creating Entities
|
||||
|
||||
### Single Entity
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createGameEntities(): void {
|
||||
// Create named entity
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Creation
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createBullets(): void {
|
||||
// Batch create entities (high performance)
|
||||
const bullets = this.createEntities(100, "Bullet");
|
||||
|
||||
// Add components to batch-created entities
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(index * 10, 100));
|
||||
bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Finding Entities
|
||||
|
||||
### By Name
|
||||
|
||||
```typescript
|
||||
// Find by name (returns first match)
|
||||
const player = this.findEntity("Player");
|
||||
const player2 = this.getEntityByName("Player"); // Alias
|
||||
|
||||
if (player) {
|
||||
console.log(`Found player: ${player.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
### By ID
|
||||
|
||||
```typescript
|
||||
// Find by unique ID
|
||||
const entity = this.findEntityById(123);
|
||||
|
||||
if (entity) {
|
||||
console.log(`Found entity: ${entity.id}`);
|
||||
}
|
||||
```
|
||||
|
||||
### By Tag
|
||||
|
||||
```typescript
|
||||
// Find by tag (returns array)
|
||||
const enemies = this.findEntitiesByTag(2);
|
||||
const enemies2 = this.getEntitiesByTag(2); // Alias
|
||||
|
||||
console.log(`Found ${enemies.length} enemies`);
|
||||
```
|
||||
|
||||
## Destroying Entities
|
||||
|
||||
### Single Entity
|
||||
|
||||
```typescript
|
||||
const enemy = this.findEntity("Enemy_1");
|
||||
if (enemy) {
|
||||
enemy.destroy(); // Entity is automatically removed from scene
|
||||
}
|
||||
```
|
||||
|
||||
### All Entities
|
||||
|
||||
```typescript
|
||||
// Destroy all entities in scene
|
||||
this.destroyAllEntities();
|
||||
```
|
||||
|
||||
## Entity Queries
|
||||
|
||||
Scene provides a component query system:
|
||||
|
||||
```typescript
|
||||
class QueryScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Create test entities
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const entity = this.createEntity(`Entity_${i}`);
|
||||
entity.addComponent(new Transform(i * 10, 0));
|
||||
entity.addComponent(new Velocity(1, 0));
|
||||
}
|
||||
}
|
||||
|
||||
public queryEntities(): void {
|
||||
// Query through QuerySystem
|
||||
const entities = this.querySystem.query([Transform, Velocity]);
|
||||
console.log(`Found ${entities.length} entities with Transform and Velocity`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `createEntity(name)` | `Entity` | Create single entity |
|
||||
| `createEntities(count, prefix)` | `Entity[]` | Batch create entities |
|
||||
| `findEntity(name)` | `Entity \| undefined` | Find by name |
|
||||
| `findEntityById(id)` | `Entity \| undefined` | Find by ID |
|
||||
| `findEntitiesByTag(tag)` | `Entity[]` | Find by tag |
|
||||
| `destroyAllEntities()` | `void` | Destroy all entities |
|
||||
122
docs/src/content/docs/en/guide/scene/events.md
Normal file
122
docs/src/content/docs/en/guide/scene/events.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: "Event System"
|
||||
description: "Scene's built-in type-safe event system"
|
||||
---
|
||||
|
||||
Scene includes a built-in type-safe event system for decoupled communication within scenes.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Listening to Events
|
||||
|
||||
```typescript
|
||||
class EventScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Listen to events
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('Player died event');
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: any): void {
|
||||
console.log('Enemy spawned event');
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('Level complete event');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sending Events
|
||||
|
||||
```typescript
|
||||
public triggerGameEvent(): void {
|
||||
// Send event (synchronous)
|
||||
this.eventSystem.emitSync('custom_event', {
|
||||
message: "This is a custom event",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Send event (asynchronous)
|
||||
this.eventSystem.emit('async_event', {
|
||||
data: "Async event data"
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `on(event, callback)` | Listen to event |
|
||||
| `once(event, callback)` | Listen once (auto-unsubscribe) |
|
||||
| `off(event, callback)` | Stop listening |
|
||||
| `emitSync(event, data)` | Send event (synchronous) |
|
||||
| `emit(event, data)` | Send event (asynchronous) |
|
||||
| `clear()` | Clear all event listeners |
|
||||
|
||||
## Event Handling Patterns
|
||||
|
||||
### Centralized Event Management
|
||||
|
||||
```typescript
|
||||
class EventHandlingScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
this.eventSystem.on('game_pause', this.onGamePause.bind(this));
|
||||
this.eventSystem.on('game_resume', this.onGameResume.bind(this));
|
||||
this.eventSystem.on('player_input', this.onPlayerInput.bind(this));
|
||||
}
|
||||
|
||||
private onGamePause(): void {
|
||||
// Pause game logic
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onGameResume(): void {
|
||||
// Resume game logic
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onPlayerInput(data: any): void {
|
||||
// Handle player input
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cleanup Event Listeners
|
||||
|
||||
Clean up event listeners on scene unload to avoid memory leaks:
|
||||
|
||||
```typescript
|
||||
public unload(): void {
|
||||
// Clear all event listeners
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
| Category | Example Events |
|
||||
|----------|----------------|
|
||||
| Game State | `game_start`, `game_pause`, `game_over` |
|
||||
| Player Actions | `player_died`, `player_jump`, `player_attack` |
|
||||
| Enemy Actions | `enemy_spawned`, `enemy_killed` |
|
||||
| Level Progress | `level_start`, `level_complete` |
|
||||
| UI Interaction | `button_click`, `menu_open` |
|
||||
91
docs/src/content/docs/en/guide/scene/index.md
Normal file
91
docs/src/content/docs/en/guide/scene/index.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: "Scene"
|
||||
description: "Core container of ECS framework, managing entity, system and component lifecycles"
|
||||
---
|
||||
|
||||
In the ECS architecture, a Scene is a container for the game world, responsible for managing the lifecycle of entities, systems, and components.
|
||||
|
||||
## Core Features
|
||||
|
||||
Scene is the core container of the ECS framework, providing:
|
||||
- Entity creation, management, and destruction
|
||||
- System registration and execution scheduling
|
||||
- Component storage and querying
|
||||
- Event system support
|
||||
- Performance monitoring and debugging information
|
||||
|
||||
## Scene Management Options
|
||||
|
||||
ECS Framework provides two scene management approaches:
|
||||
|
||||
| Manager | Use Case | Features |
|
||||
|---------|----------|----------|
|
||||
| **SceneManager** | 95% of games | Lightweight, scene transitions |
|
||||
| **WorldManager** | MMO servers, room systems | Multi-World, full isolation |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Inherit Scene Class
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// Add systems
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// Create initial entities
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("Game scene started");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("Game scene unloaded");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Scene Configuration
|
||||
|
||||
```typescript
|
||||
import { ISceneConfig } from '@esengine/ecs-framework';
|
||||
|
||||
const config: ISceneConfig = {
|
||||
name: "MainGame",
|
||||
enableEntityDirectUpdate: false
|
||||
};
|
||||
|
||||
class ConfiguredScene extends Scene {
|
||||
constructor() {
|
||||
super(config);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Running a Scene
|
||||
|
||||
```typescript
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Lifecycle](./lifecycle/) - Scene lifecycle methods
|
||||
- [Entity Management](./entity-management/) - Create, find, destroy entities
|
||||
- [System Management](./system-management/) - System control
|
||||
- [Events](./events/) - Scene event communication
|
||||
- [Debugging](./debugging/) - Performance and debugging
|
||||
- [Best Practices](./best-practices/) - Scene design patterns
|
||||
103
docs/src/content/docs/en/guide/scene/lifecycle.md
Normal file
103
docs/src/content/docs/en/guide/scene/lifecycle.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: "Scene Lifecycle"
|
||||
description: "Scene lifecycle methods and execution order"
|
||||
---
|
||||
|
||||
Scene provides complete lifecycle management for proper resource initialization and cleanup.
|
||||
|
||||
## Lifecycle Methods
|
||||
|
||||
```typescript
|
||||
class ExampleScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 1. Scene initialization: setup systems and initial entities
|
||||
console.log("Scene initializing");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// 2. Scene starts running: game logic begins execution
|
||||
console.log("Scene starting");
|
||||
}
|
||||
|
||||
public update(deltaTime: number): void {
|
||||
// 3. Per-frame update (called by scene manager)
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 4. Scene unloading: cleanup resources
|
||||
console.log("Scene unloading");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Execution Order
|
||||
|
||||
| Phase | Method | Description |
|
||||
|-------|--------|-------------|
|
||||
| Initialize | `initialize()` | Setup systems and initial entities |
|
||||
| Start | `begin()` / `onStart()` | Scene starts running |
|
||||
| Update | `update()` | Per-frame update (auto-called) |
|
||||
| End | `end()` / `unload()` | Cleanup resources |
|
||||
|
||||
## Lifecycle Example
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private resourcesLoaded = false;
|
||||
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 1. Add systems (by dependency order)
|
||||
this.addSystem(new InputSystem());
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 2. Create initial entities
|
||||
this.createPlayer();
|
||||
this.createEnemies();
|
||||
|
||||
// 3. Setup event listeners
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
this.resourcesLoaded = true;
|
||||
console.log("Scene resources loaded, game starting");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Cleanup event listeners
|
||||
this.eventSystem.clear();
|
||||
|
||||
// Cleanup other resources
|
||||
this.resourcesLoaded = false;
|
||||
console.log("Scene resources cleaned up");
|
||||
}
|
||||
|
||||
private createPlayer(): void {
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
}
|
||||
|
||||
private createEnemies(): void {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
}
|
||||
}
|
||||
|
||||
private setupEvents(): void {
|
||||
this.eventSystem.on('player_died', () => {
|
||||
console.log('Player died');
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
1. **initialize() called once** - For initial state setup
|
||||
2. **onStart() on scene activation** - May be called multiple times
|
||||
3. **unload() must cleanup resources** - Avoid memory leaks
|
||||
4. **update() managed by framework** - No manual calls needed
|
||||
115
docs/src/content/docs/en/guide/scene/system-management.md
Normal file
115
docs/src/content/docs/en/guide/scene/system-management.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: "System Management"
|
||||
description: "System addition, removal and control in scenes"
|
||||
---
|
||||
|
||||
Scene manages system registration, execution order, and lifecycle.
|
||||
|
||||
## Adding Systems
|
||||
|
||||
```typescript
|
||||
class SystemScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Add system
|
||||
const movementSystem = new MovementSystem();
|
||||
this.addSystem(movementSystem);
|
||||
|
||||
// Set system update order (lower runs first)
|
||||
movementSystem.updateOrder = 1;
|
||||
|
||||
// Add more systems
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Getting Systems
|
||||
|
||||
```typescript
|
||||
// Get system of specific type
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
if (physicsSystem) {
|
||||
console.log("Found physics system");
|
||||
}
|
||||
```
|
||||
|
||||
## Removing Systems
|
||||
|
||||
```typescript
|
||||
public removeUnnecessarySystems(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
if (physicsSystem) {
|
||||
this.removeSystem(physicsSystem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Controlling Systems
|
||||
|
||||
### Enable/Disable Systems
|
||||
|
||||
```typescript
|
||||
public pausePhysics(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
if (physicsSystem) {
|
||||
physicsSystem.enabled = false; // Disable system
|
||||
}
|
||||
}
|
||||
|
||||
public resumePhysics(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
if (physicsSystem) {
|
||||
physicsSystem.enabled = true; // Enable system
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get All Systems
|
||||
|
||||
```typescript
|
||||
public getAllSystems(): EntitySystem[] {
|
||||
return this.systems; // Get all registered systems
|
||||
}
|
||||
```
|
||||
|
||||
## System Organization Best Practice
|
||||
|
||||
Group systems by function:
|
||||
|
||||
```typescript
|
||||
class OrganizedScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Add systems by function and dependencies
|
||||
this.addInputSystems();
|
||||
this.addLogicSystems();
|
||||
this.addRenderSystems();
|
||||
}
|
||||
|
||||
private addInputSystems(): void {
|
||||
this.addSystem(new InputSystem());
|
||||
}
|
||||
|
||||
private addLogicSystems(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new CollisionSystem());
|
||||
}
|
||||
|
||||
private addRenderSystems(): void {
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new UISystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `addSystem(system)` | `void` | Add system to scene |
|
||||
| `removeSystem(system)` | `void` | Remove system from scene |
|
||||
| `getEntityProcessor(Type)` | `T \| undefined` | Get system by type |
|
||||
| `systems` | `EntitySystem[]` | Get all systems |
|
||||
165
docs/src/content/docs/en/guide/serialization/decorators.md
Normal file
165
docs/src/content/docs/en/guide/serialization/decorators.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
title: "Decorators & Inheritance"
|
||||
description: "Advanced serialization decorator usage and component inheritance"
|
||||
---
|
||||
|
||||
## Advanced Decorators
|
||||
|
||||
### Field Serialization Options
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Advanced')
|
||||
@Serializable({ version: 1 })
|
||||
class AdvancedComponent extends Component {
|
||||
// Use alias
|
||||
@Serialize({ alias: 'playerName' })
|
||||
public name: string = '';
|
||||
|
||||
// Custom serializer
|
||||
@Serialize({
|
||||
serializer: (value: Date) => value.toISOString(),
|
||||
deserializer: (value: string) => new Date(value)
|
||||
})
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
// Ignore serialization
|
||||
@IgnoreSerialization()
|
||||
public cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
### Collection Type Serialization
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Collections')
|
||||
@Serializable({ version: 1 })
|
||||
class CollectionsComponent extends Component {
|
||||
// Map serialization
|
||||
@SerializeAsMap()
|
||||
public inventory: Map<string, number> = new Map();
|
||||
|
||||
// Set serialization
|
||||
@SerializeAsSet()
|
||||
public acquiredSkills: Set<string> = new Set();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.inventory.set('gold', 100);
|
||||
this.inventory.set('silver', 50);
|
||||
this.acquiredSkills.add('attack');
|
||||
this.acquiredSkills.add('defense');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Component Inheritance and Serialization
|
||||
|
||||
The framework fully supports component class inheritance. Subclasses automatically inherit parent class serialization fields while adding their own.
|
||||
|
||||
### Basic Inheritance
|
||||
|
||||
```typescript
|
||||
// Base component
|
||||
@ECSComponent('Collider2DBase')
|
||||
@Serializable({ version: 1, typeId: 'Collider2DBase' })
|
||||
abstract class Collider2DBase extends Component {
|
||||
@Serialize()
|
||||
public friction: number = 0.5;
|
||||
|
||||
@Serialize()
|
||||
public restitution: number = 0.0;
|
||||
|
||||
@Serialize()
|
||||
public isTrigger: boolean = false;
|
||||
}
|
||||
|
||||
// Subclass component - automatically inherits parent's serialization fields
|
||||
@ECSComponent('BoxCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
class BoxCollider2DComponent extends Collider2DBase {
|
||||
@Serialize()
|
||||
public width: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
public height: number = 1.0;
|
||||
}
|
||||
|
||||
// Another subclass component
|
||||
@ECSComponent('CircleCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
class CircleCollider2DComponent extends Collider2DBase {
|
||||
@Serialize()
|
||||
public radius: number = 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
### Inheritance Rules
|
||||
|
||||
1. **Field Inheritance**: Subclasses automatically inherit all `@Serialize()` marked fields from parent
|
||||
2. **Independent Metadata**: Each subclass maintains independent serialization metadata; modifying subclass doesn't affect parent or other subclasses
|
||||
3. **typeId Distinction**: Use `typeId` option to specify unique identifier for each class, ensuring correct component type recognition during deserialization
|
||||
|
||||
### Importance of Using typeId
|
||||
|
||||
When using component inheritance, it's **strongly recommended** to set a unique `typeId` for each class:
|
||||
|
||||
```typescript
|
||||
// ✅ Recommended: Explicitly specify typeId
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
class BoxCollider2DComponent extends Collider2DBase { }
|
||||
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
class CircleCollider2DComponent extends Collider2DBase { }
|
||||
|
||||
// ⚠️ Not recommended: Relying on class name as typeId
|
||||
// Class names may change after code minification, causing deserialization failure
|
||||
@Serializable({ version: 1 })
|
||||
class BoxCollider2DComponent extends Collider2DBase { }
|
||||
```
|
||||
|
||||
### Subclass Overriding Parent Fields
|
||||
|
||||
Subclasses can redeclare parent fields to modify their serialization options:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('SpecialCollider')
|
||||
@Serializable({ version: 1, typeId: 'SpecialCollider' })
|
||||
class SpecialColliderComponent extends Collider2DBase {
|
||||
// Override parent field with different alias
|
||||
@Serialize({ alias: 'fric' })
|
||||
public override friction: number = 0.8;
|
||||
|
||||
@Serialize()
|
||||
public specialProperty: string = '';
|
||||
}
|
||||
```
|
||||
|
||||
### Ignoring Inherited Fields
|
||||
|
||||
Use `@IgnoreSerialization()` to ignore fields inherited from parent in subclass:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('TriggerOnly')
|
||||
@Serializable({ version: 1, typeId: 'TriggerOnly' })
|
||||
class TriggerOnlyCollider extends Collider2DBase {
|
||||
// Ignore parent's friction and restitution fields
|
||||
// Because Trigger doesn't need physics material properties
|
||||
@IgnoreSerialization()
|
||||
public override friction: number = 0;
|
||||
|
||||
@IgnoreSerialization()
|
||||
public override restitution: number = 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Decorator Reference
|
||||
|
||||
| Decorator | Description |
|
||||
|-----------|-------------|
|
||||
| `@Serializable({ version, typeId })` | Mark component as serializable |
|
||||
| `@Serialize()` | Mark field as serializable |
|
||||
| `@Serialize({ alias })` | Serialize field with alias |
|
||||
| `@Serialize({ serializer, deserializer })` | Custom serialization logic |
|
||||
| `@SerializeAsMap()` | Serialize Map type |
|
||||
| `@SerializeAsSet()` | Serialize Set type |
|
||||
| `@IgnoreSerialization()` | Ignore field serialization |
|
||||
228
docs/src/content/docs/en/guide/serialization/incremental.md
Normal file
228
docs/src/content/docs/en/guide/serialization/incremental.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
title: "Incremental Serialization"
|
||||
description: "Only serialize changes in the scene"
|
||||
---
|
||||
|
||||
Incremental serialization only saves the changed parts of a scene, suitable for network synchronization, undo/redo, time rewinding, and other scenarios requiring frequent state saving.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### 1. Create Base Snapshot
|
||||
|
||||
```typescript
|
||||
// Create base snapshot before starting to record changes
|
||||
scene.createIncrementalSnapshot();
|
||||
```
|
||||
|
||||
### 2. Modify Scene
|
||||
|
||||
```typescript
|
||||
// Add entity
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new PositionComponent(100, 200));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
|
||||
// Modify component
|
||||
const player = scene.findEntity('Player');
|
||||
const pos = player.getComponent(PositionComponent);
|
||||
pos.x = 300;
|
||||
pos.y = 400;
|
||||
|
||||
// Remove component
|
||||
player.removeComponentByType(BuffComponent);
|
||||
|
||||
// Delete entity
|
||||
const oldEntity = scene.findEntity('ToDelete');
|
||||
oldEntity.destroy();
|
||||
|
||||
// Modify scene data
|
||||
scene.sceneData.set('score', 1000);
|
||||
```
|
||||
|
||||
### 3. Get Incremental Changes
|
||||
|
||||
```typescript
|
||||
// Get all changes relative to base snapshot
|
||||
const incremental = scene.serializeIncremental();
|
||||
|
||||
// View change statistics
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
console.log('Total changes:', stats.totalChanges);
|
||||
console.log('Added entities:', stats.addedEntities);
|
||||
console.log('Removed entities:', stats.removedEntities);
|
||||
console.log('Added components:', stats.addedComponents);
|
||||
console.log('Updated components:', stats.updatedComponents);
|
||||
```
|
||||
|
||||
### 4. Serialize Incremental Data
|
||||
|
||||
```typescript
|
||||
// JSON format (default)
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json'
|
||||
});
|
||||
|
||||
// Binary format (smaller size, higher performance)
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// Pretty print JSON output (for debugging)
|
||||
const prettyJson = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json',
|
||||
pretty: true
|
||||
});
|
||||
|
||||
// Send or save
|
||||
socket.send(binaryData); // Use binary for network transmission
|
||||
localStorage.setItem('changes', jsonData); // JSON for local storage
|
||||
```
|
||||
|
||||
### 5. Apply Incremental Changes
|
||||
|
||||
```typescript
|
||||
// Apply changes to another scene
|
||||
const otherScene = new Scene();
|
||||
|
||||
// Directly apply incremental object
|
||||
otherScene.applyIncremental(incremental);
|
||||
|
||||
// Apply from JSON string
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
|
||||
otherScene.applyIncremental(jsonData);
|
||||
|
||||
// Apply from binary Uint8Array
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
|
||||
otherScene.applyIncremental(binaryData);
|
||||
```
|
||||
|
||||
## Incremental Snapshot Management
|
||||
|
||||
### Update Snapshot Base
|
||||
|
||||
After applying incremental changes, you can update the snapshot base:
|
||||
|
||||
```typescript
|
||||
// Create initial snapshot
|
||||
scene.createIncrementalSnapshot();
|
||||
|
||||
// First modification
|
||||
entity.addComponent(new VelocityComponent(5, 0));
|
||||
const incremental1 = scene.serializeIncremental();
|
||||
|
||||
// Update base (set current state as new base)
|
||||
scene.updateIncrementalSnapshot();
|
||||
|
||||
// Second modification (incremental will be based on updated base)
|
||||
entity.getComponent(VelocityComponent).dx = 10;
|
||||
const incremental2 = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
### Clear Snapshot
|
||||
|
||||
```typescript
|
||||
// Release memory used by snapshot
|
||||
scene.clearIncrementalSnapshot();
|
||||
|
||||
// Check if snapshot exists
|
||||
if (scene.hasIncrementalSnapshot()) {
|
||||
console.log('Incremental snapshot exists');
|
||||
}
|
||||
```
|
||||
|
||||
## Incremental Serialization Options
|
||||
|
||||
```typescript
|
||||
interface IncrementalSerializationOptions {
|
||||
// Whether to perform deep comparison of component data
|
||||
// Default true, set to false to improve performance but may miss internal field changes
|
||||
deepComponentComparison?: boolean;
|
||||
|
||||
// Whether to track scene data changes
|
||||
// Default true
|
||||
trackSceneData?: boolean;
|
||||
|
||||
// Whether to compress snapshot (using JSON serialization)
|
||||
// Default false
|
||||
compressSnapshot?: boolean;
|
||||
|
||||
// Serialization format
|
||||
// 'json': JSON format (readable, convenient for debugging)
|
||||
// 'binary': MessagePack binary format (smaller, higher performance)
|
||||
// Default 'json'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// Whether to pretty print JSON output (only effective when format='json')
|
||||
// Default false
|
||||
pretty?: boolean;
|
||||
}
|
||||
|
||||
// Using options
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: true,
|
||||
trackSceneData: true
|
||||
});
|
||||
```
|
||||
|
||||
## Incremental Data Structure
|
||||
|
||||
Incremental snapshots contain the following change types:
|
||||
|
||||
```typescript
|
||||
interface IncrementalSnapshot {
|
||||
version: number; // Snapshot version number
|
||||
timestamp: number; // Timestamp
|
||||
sceneName: string; // Scene name
|
||||
baseVersion: number; // Base version number
|
||||
entityChanges: EntityChange[]; // Entity changes
|
||||
componentChanges: ComponentChange[]; // Component changes
|
||||
sceneDataChanges: SceneDataChange[]; // Scene data changes
|
||||
}
|
||||
|
||||
// Change operation types
|
||||
enum ChangeOperation {
|
||||
EntityAdded = 'entity_added',
|
||||
EntityRemoved = 'entity_removed',
|
||||
EntityUpdated = 'entity_updated',
|
||||
ComponentAdded = 'component_added',
|
||||
ComponentRemoved = 'component_removed',
|
||||
ComponentUpdated = 'component_updated',
|
||||
SceneDataUpdated = 'scene_data_updated'
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### For High-Frequency Sync
|
||||
|
||||
```typescript
|
||||
// Disable deep comparison to improve performance
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: false // Only detect component addition/removal
|
||||
});
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
|
||||
```typescript
|
||||
// Batch modify then serialize
|
||||
scene.entities.buffer.forEach(entity => {
|
||||
// Batch modifications
|
||||
});
|
||||
|
||||
// Serialize all changes at once
|
||||
const incremental = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `scene.createIncrementalSnapshot(options?)` | Create base snapshot |
|
||||
| `scene.serializeIncremental()` | Get incremental changes |
|
||||
| `scene.applyIncremental(data)` | Apply incremental changes |
|
||||
| `scene.updateIncrementalSnapshot()` | Update snapshot base |
|
||||
| `scene.clearIncrementalSnapshot()` | Clear snapshot |
|
||||
| `scene.hasIncrementalSnapshot()` | Check if snapshot exists |
|
||||
| `IncrementalSerializer.getIncrementalStats(snapshot)` | Get change statistics |
|
||||
| `IncrementalSerializer.serializeIncremental(snapshot, options)` | Serialize incremental data |
|
||||
170
docs/src/content/docs/en/guide/serialization/index.md
Normal file
170
docs/src/content/docs/en/guide/serialization/index.md
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
title: "Serialization System"
|
||||
description: "ECS serialization system overview and full serialization"
|
||||
---
|
||||
|
||||
The serialization system provides a complete solution for persisting scene, entity, and component data. It supports both full serialization and incremental serialization modes, suitable for game saves, network synchronization, scene editors, time rewinding, and more.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
The serialization system has two layers:
|
||||
|
||||
- **Full Serialization**: Serializes the complete scene state, including all entities, components, and scene data
|
||||
- **Incremental Serialization**: Only serializes changes relative to a base snapshot, greatly reducing data size
|
||||
|
||||
### Supported Data Formats
|
||||
|
||||
- **JSON Format**: Human-readable, convenient for debugging and editing
|
||||
- **Binary Format**: Uses MessagePack, smaller size and better performance
|
||||
|
||||
> **v2.2.2 Important Change**
|
||||
>
|
||||
> Starting from v2.2.2, binary serialization returns `Uint8Array` instead of Node.js `Buffer` to ensure browser compatibility:
|
||||
> - `serialize({ format: 'binary' })` returns `string | Uint8Array` (was `string | Buffer`)
|
||||
> - `deserialize(data)` accepts `string | Uint8Array` (was `string | Buffer`)
|
||||
> - `applyIncremental(data)` accepts `IncrementalSnapshot | string | Uint8Array` (was including `Buffer`)
|
||||
>
|
||||
> **Migration Impact**:
|
||||
> - **Runtime Compatible**: Node.js `Buffer` inherits from `Uint8Array`, existing code works directly
|
||||
> - **Type Checking**: If your TypeScript code explicitly uses `Buffer` type, change to `Uint8Array`
|
||||
> - **Browser Support**: `Uint8Array` is a standard JavaScript type supported by all modern browsers
|
||||
|
||||
## Full Serialization
|
||||
|
||||
### Basic Usage
|
||||
|
||||
#### 1. Mark Serializable Components
|
||||
|
||||
Use `@Serializable` and `@Serialize` decorators to mark components and fields for serialization:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
@Serialize()
|
||||
public experience: number = 0;
|
||||
|
||||
@Serialize()
|
||||
public position: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
// Fields without @Serialize() won't be serialized
|
||||
private tempData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Serialize Scene
|
||||
|
||||
```typescript
|
||||
// JSON format serialization
|
||||
const jsonData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true // Pretty print output
|
||||
});
|
||||
|
||||
// Save to local storage
|
||||
localStorage.setItem('gameSave', jsonData);
|
||||
|
||||
// Binary format serialization (smaller size)
|
||||
const binaryData = scene.serialize({
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// Save to file (Node.js environment)
|
||||
// Note: binaryData is Uint8Array type, Node.js fs can write it directly
|
||||
fs.writeFileSync('save.bin', binaryData);
|
||||
```
|
||||
|
||||
#### 3. Deserialize Scene
|
||||
|
||||
```typescript
|
||||
// Restore from JSON
|
||||
const saveData = localStorage.getItem('gameSave');
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace' // Replace current scene content
|
||||
});
|
||||
}
|
||||
|
||||
// Restore from Binary
|
||||
const binaryData = fs.readFileSync('save.bin');
|
||||
scene.deserialize(binaryData, {
|
||||
strategy: 'merge' // Merge into existing scene
|
||||
});
|
||||
```
|
||||
|
||||
### Serialization Options
|
||||
|
||||
#### SerializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneSerializationOptions {
|
||||
// Component types to serialize (optional)
|
||||
components?: ComponentType[];
|
||||
|
||||
// Serialization format: 'json' or 'binary'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// Pretty print JSON output
|
||||
pretty?: boolean;
|
||||
|
||||
// Include metadata
|
||||
includeMetadata?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```typescript
|
||||
// Only serialize specific component types
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
components: [PlayerComponent, InventoryComponent],
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
```
|
||||
|
||||
#### DeserializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneDeserializationOptions {
|
||||
// Deserialization strategy
|
||||
strategy?: 'merge' | 'replace';
|
||||
|
||||
// Component type registry (optional, uses global registry by default)
|
||||
componentRegistry?: Map<string, ComponentType>;
|
||||
}
|
||||
```
|
||||
|
||||
### Scene Custom Data
|
||||
|
||||
Besides entities and components, you can also serialize scene-level configuration data:
|
||||
|
||||
```typescript
|
||||
// Set scene data
|
||||
scene.sceneData.set('weather', 'rainy');
|
||||
scene.sceneData.set('difficulty', 'hard');
|
||||
scene.sceneData.set('checkpoint', { x: 100, y: 200 });
|
||||
|
||||
// Scene data is automatically included when serializing
|
||||
const saveData = scene.serialize({ format: 'json' });
|
||||
|
||||
// Scene data is restored after deserialization
|
||||
scene.deserialize(saveData);
|
||||
console.log(scene.sceneData.get('weather')); // 'rainy'
|
||||
```
|
||||
|
||||
## More Topics
|
||||
|
||||
- [Decorators & Inheritance](/en/guide/serialization/decorators) - Advanced decorator usage and component inheritance
|
||||
- [Incremental Serialization](/en/guide/serialization/incremental) - Only serialize changes
|
||||
- [Version Migration](/en/guide/serialization/migration) - Handle data structure changes
|
||||
- [Use Cases](/en/guide/serialization/use-cases) - Save system, network sync, undo/redo examples
|
||||
151
docs/src/content/docs/en/guide/serialization/migration.md
Normal file
151
docs/src/content/docs/en/guide/serialization/migration.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: "Version Migration"
|
||||
description: "Handle component and scene data structure changes"
|
||||
---
|
||||
|
||||
When component structure changes, the version migration system can automatically upgrade old version save data.
|
||||
|
||||
## Register Migration Function
|
||||
|
||||
```typescript
|
||||
import { VersionMigrationManager } from '@esengine/ecs-framework';
|
||||
|
||||
// Assume PlayerComponent v1 has hp field
|
||||
// v2 changes to health and maxHealth fields
|
||||
|
||||
// Register migration from version 1 to version 2
|
||||
VersionMigrationManager.registerComponentMigration(
|
||||
'Player',
|
||||
1, // From version
|
||||
2, // To version
|
||||
(data) => {
|
||||
// Migration logic
|
||||
const newData = {
|
||||
...data,
|
||||
health: data.hp,
|
||||
maxHealth: data.hp,
|
||||
};
|
||||
delete newData.hp;
|
||||
return newData;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Using Migration Builder
|
||||
|
||||
```typescript
|
||||
import { MigrationBuilder } from '@esengine/ecs-framework';
|
||||
|
||||
new MigrationBuilder()
|
||||
.forComponent('Player')
|
||||
.fromVersionToVersion(2, 3)
|
||||
.migrate((data) => {
|
||||
// Migrate from version 2 to version 3
|
||||
data.experience = data.exp || 0;
|
||||
delete data.exp;
|
||||
return data;
|
||||
});
|
||||
```
|
||||
|
||||
## Scene-Level Migration
|
||||
|
||||
```typescript
|
||||
// Register scene-level migration
|
||||
VersionMigrationManager.registerSceneMigration(
|
||||
1, // From version
|
||||
2, // To version
|
||||
(scene) => {
|
||||
// Migrate scene structure
|
||||
scene.metadata = {
|
||||
...scene.metadata,
|
||||
migratedFrom: 1
|
||||
};
|
||||
return scene;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Check Migration Path
|
||||
|
||||
```typescript
|
||||
// Check if migration is possible
|
||||
const canMigrate = VersionMigrationManager.canMigrateComponent(
|
||||
'Player',
|
||||
1, // From version
|
||||
3 // To version
|
||||
);
|
||||
|
||||
if (canMigrate) {
|
||||
// Safe to migrate
|
||||
scene.deserialize(oldSaveData);
|
||||
}
|
||||
|
||||
// Get migration path
|
||||
const path = VersionMigrationManager.getComponentMigrationPath('Player');
|
||||
console.log('Available migration versions:', path); // [1, 2, 3]
|
||||
```
|
||||
|
||||
## Migration Chain Example
|
||||
|
||||
When migrating across multiple versions, the system automatically chains migrations:
|
||||
|
||||
```typescript
|
||||
// Register v1 -> v2
|
||||
VersionMigrationManager.registerComponentMigration('Player', 1, 2, (data) => {
|
||||
data.health = data.hp;
|
||||
delete data.hp;
|
||||
return data;
|
||||
});
|
||||
|
||||
// Register v2 -> v3
|
||||
VersionMigrationManager.registerComponentMigration('Player', 2, 3, (data) => {
|
||||
data.stats = { health: data.health, mana: 100 };
|
||||
delete data.health;
|
||||
return data;
|
||||
});
|
||||
|
||||
// When loading v1 data, it will automatically execute v1 -> v2 -> v3
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Maintain Backward Compatibility
|
||||
|
||||
```typescript
|
||||
// Document data structure changes for each version
|
||||
// v1: { hp: number }
|
||||
// v2: { health: number, maxHealth: number }
|
||||
// v3: { stats: { health: number, mana: number } }
|
||||
```
|
||||
|
||||
### 2. Test Migration Paths
|
||||
|
||||
```typescript
|
||||
// Test all possible migration paths
|
||||
const testData = { hp: 100 };
|
||||
const migrated = VersionMigrationManager.migrateComponent('Player', testData, 1, 3);
|
||||
expect(migrated.stats.health).toBe(100);
|
||||
```
|
||||
|
||||
### 3. Keep Backup of Original Data
|
||||
|
||||
```typescript
|
||||
// Backup before migration
|
||||
const backup = JSON.parse(JSON.stringify(saveData));
|
||||
try {
|
||||
scene.deserialize(saveData);
|
||||
} catch (e) {
|
||||
// Restore on migration failure
|
||||
console.error('Migration failed:', e);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `registerComponentMigration(type, from, to, fn)` | Register component migration function |
|
||||
| `registerSceneMigration(from, to, fn)` | Register scene migration function |
|
||||
| `canMigrateComponent(type, from, to)` | Check if migration is possible |
|
||||
| `getComponentMigrationPath(type)` | Get component's migration version path |
|
||||
| `migrateComponent(type, data, from, to)` | Execute component data migration |
|
||||
306
docs/src/content/docs/en/guide/serialization/use-cases.md
Normal file
306
docs/src/content/docs/en/guide/serialization/use-cases.md
Normal file
@@ -0,0 +1,306 @@
|
||||
---
|
||||
title: "Use Cases"
|
||||
description: "Practical examples of the serialization system"
|
||||
---
|
||||
|
||||
## Game Save System
|
||||
|
||||
```typescript
|
||||
class SaveSystem {
|
||||
private static SAVE_KEY = 'game_save';
|
||||
|
||||
// Save game
|
||||
public static saveGame(scene: Scene): void {
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: false
|
||||
});
|
||||
|
||||
localStorage.setItem(this.SAVE_KEY, saveData);
|
||||
console.log('Game saved');
|
||||
}
|
||||
|
||||
// Load game
|
||||
public static loadGame(scene: Scene): boolean {
|
||||
const saveData = localStorage.getItem(this.SAVE_KEY);
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
console.log('Game loaded');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if save exists
|
||||
public static hasSave(): boolean {
|
||||
return localStorage.getItem(this.SAVE_KEY) !== null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Network Synchronization
|
||||
|
||||
```typescript
|
||||
class NetworkSync {
|
||||
private baseSnapshot?: any;
|
||||
private syncInterval: number = 100; // Sync every 100ms
|
||||
|
||||
constructor(private scene: Scene, private socket: WebSocket) {
|
||||
this.setupSync();
|
||||
}
|
||||
|
||||
private setupSync(): void {
|
||||
// Create base snapshot
|
||||
this.scene.createIncrementalSnapshot();
|
||||
|
||||
// Periodically send incremental updates
|
||||
setInterval(() => {
|
||||
this.sendIncremental();
|
||||
}, this.syncInterval);
|
||||
|
||||
// Receive remote incremental updates
|
||||
this.socket.onmessage = (event) => {
|
||||
this.receiveIncremental(event.data);
|
||||
};
|
||||
}
|
||||
|
||||
private sendIncremental(): void {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
|
||||
// Only send when there are changes
|
||||
if (stats.totalChanges > 0) {
|
||||
// Use binary format to reduce network transmission
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
this.socket.send(binaryData);
|
||||
|
||||
// Update base
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
private receiveIncremental(data: ArrayBuffer): void {
|
||||
// Directly apply binary data (ArrayBuffer to Uint8Array)
|
||||
const uint8Array = new Uint8Array(data);
|
||||
this.scene.applyIncremental(uint8Array);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Undo/Redo System
|
||||
|
||||
```typescript
|
||||
class UndoRedoSystem {
|
||||
private history: IncrementalSnapshot[] = [];
|
||||
private currentIndex: number = -1;
|
||||
private maxHistory: number = 50;
|
||||
|
||||
constructor(private scene: Scene) {
|
||||
// Create initial snapshot
|
||||
this.scene.createIncrementalSnapshot();
|
||||
this.saveState('Initial');
|
||||
}
|
||||
|
||||
// Save current state
|
||||
public saveState(label: string): void {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
|
||||
// Remove history after current position
|
||||
this.history = this.history.slice(0, this.currentIndex + 1);
|
||||
|
||||
// Add new state
|
||||
this.history.push(incremental);
|
||||
this.currentIndex++;
|
||||
|
||||
// Limit history count
|
||||
if (this.history.length > this.maxHistory) {
|
||||
this.history.shift();
|
||||
this.currentIndex--;
|
||||
}
|
||||
|
||||
// Update snapshot base
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
}
|
||||
|
||||
// Undo
|
||||
public undo(): boolean {
|
||||
if (this.currentIndex > 0) {
|
||||
this.currentIndex--;
|
||||
const incremental = this.history[this.currentIndex];
|
||||
this.scene.applyIncremental(incremental);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Redo
|
||||
public redo(): boolean {
|
||||
if (this.currentIndex < this.history.length - 1) {
|
||||
this.currentIndex++;
|
||||
const incremental = this.history[this.currentIndex];
|
||||
this.scene.applyIncremental(incremental);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public canUndo(): boolean {
|
||||
return this.currentIndex > 0;
|
||||
}
|
||||
|
||||
public canRedo(): boolean {
|
||||
return this.currentIndex < this.history.length - 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Level Editor
|
||||
|
||||
```typescript
|
||||
class LevelEditor {
|
||||
// Export level
|
||||
public exportLevel(scene: Scene, filename: string): void {
|
||||
const levelData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
|
||||
// Browser environment
|
||||
const blob = new Blob([levelData], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Import level
|
||||
public importLevel(scene: Scene, fileContent: string): void {
|
||||
scene.deserialize(fileContent, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
}
|
||||
|
||||
// Validate level data
|
||||
public validateLevel(saveData: string): boolean {
|
||||
const validation = SceneSerializer.validate(saveData);
|
||||
if (!validation.valid) {
|
||||
console.error('Level data invalid:', validation.errors);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get level info (without full deserialization)
|
||||
public getLevelInfo(saveData: string): any {
|
||||
const info = SceneSerializer.getInfo(saveData);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
### 1. Choose the Right Format
|
||||
|
||||
- **Development**: Use JSON format for easy debugging and inspection
|
||||
- **Production**: Use Binary format to reduce 30-50% data size
|
||||
|
||||
### 2. Serialize On-Demand
|
||||
|
||||
```typescript
|
||||
// Only serialize components that need persistence
|
||||
const saveData = scene.serialize({
|
||||
format: 'binary',
|
||||
components: [PlayerComponent, InventoryComponent, QuestComponent]
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Optimize Incremental Serialization
|
||||
|
||||
```typescript
|
||||
// For high-frequency sync, disable deep comparison for better performance
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: false // Only detect component addition/removal
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Explicitly Mark Serialized Fields
|
||||
|
||||
```typescript
|
||||
// Clearly mark fields that need serialization
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
// Runtime data not serialized
|
||||
private _cachedSprite: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Version Control
|
||||
|
||||
```typescript
|
||||
// Specify version for components
|
||||
@Serializable({ version: 2 })
|
||||
class PlayerComponent extends Component {
|
||||
// Version 2 fields
|
||||
}
|
||||
|
||||
// Register migration function to ensure compatibility
|
||||
VersionMigrationManager.registerComponentMigration('Player', 1, 2, migrateV1ToV2);
|
||||
```
|
||||
|
||||
### 3. Avoid Circular References
|
||||
|
||||
```typescript
|
||||
// Don't directly reference other entities in components
|
||||
@ECSComponent('Follower')
|
||||
@Serializable({ version: 1 })
|
||||
class FollowerComponent extends Component {
|
||||
// Store entity ID instead of entity reference
|
||||
@Serialize()
|
||||
public targetId: number = 0;
|
||||
|
||||
// Find target entity through scene
|
||||
public getTarget(scene: Scene): Entity | null {
|
||||
return scene.entities.findEntityById(this.targetId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Compress Large Data
|
||||
|
||||
```typescript
|
||||
// For large data structures, use custom serialization
|
||||
@ECSComponent('LargeData')
|
||||
@Serializable({ version: 1 })
|
||||
class LargeDataComponent extends Component {
|
||||
@Serialize({
|
||||
serializer: (data: LargeObject) => compressData(data),
|
||||
deserializer: (data: CompressedData) => decompressData(data)
|
||||
})
|
||||
public data: LargeObject;
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Comparison
|
||||
|
||||
| Scenario | JSON Format | Binary Format | Savings |
|
||||
|----------|-------------|---------------|---------|
|
||||
| Small save (100 entities) | 50KB | 35KB | 30% |
|
||||
| Medium save (1000 entities) | 500KB | 300KB | 40% |
|
||||
| Large save (10000 entities) | 5MB | 2.5MB | 50% |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user