Compare commits
747 Commits
v2.1.11
...
@esengine/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54c8ff4d8f | ||
|
|
caf3be72cd | ||
|
|
ec3e449681 | ||
|
|
b95a46edaf | ||
|
|
f493f2d6cc | ||
|
|
6970394717 | ||
|
|
0e4b66aac4 | ||
|
|
7399e91a5b | ||
|
|
c84addaa0b | ||
|
|
61da38faf5 | ||
|
|
f333b81298 | ||
|
|
69bb6bd946 | ||
|
|
3b6fc8266f | ||
|
|
db22bd3028 | ||
|
|
b80e967829 | ||
|
|
9e87eb39b9 | ||
|
|
ff549f3c2a | ||
|
|
15c1d98305 | ||
|
|
4a3d8c3962 | ||
|
|
0f5aa633d8 | ||
|
|
85171a0a5c | ||
|
|
35d81880a7 | ||
|
|
71022abc99 | ||
|
|
87f71e2251 | ||
|
|
b9ea8d14cf | ||
|
|
10d0fb1d5c | ||
|
|
71e111415f | ||
|
|
0de45279e6 | ||
|
|
cc6f12d470 | ||
|
|
902c0a1074 | ||
|
|
d3e489aad3 | ||
|
|
12051d987f | ||
|
|
b38fe5ebf4 | ||
|
|
f01ce1e320 | ||
|
|
094133a71a | ||
|
|
3e5b7783be | ||
|
|
ebcb4d00a8 | ||
|
|
d2af9caae9 | ||
|
|
bb696c6a60 | ||
|
|
ffd35a71cd | ||
|
|
1f3a76aabe | ||
|
|
ddc7d1f726 | ||
|
|
04b08f3f07 | ||
|
|
d9969d0b08 | ||
|
|
bdbbf8a80a | ||
|
|
1368473c71 | ||
|
|
b28169b186 | ||
|
|
e2598b2292 | ||
|
|
2e3889abed | ||
|
|
d21caa974e | ||
|
|
a08a84b7db | ||
|
|
449bd420a6 | ||
|
|
1f297ac769 | ||
|
|
4cf868a769 | ||
|
|
afdeb00b4d | ||
|
|
764ce67742 | ||
|
|
61a13baca2 | ||
|
|
1cfa64aa0f | ||
|
|
3b978384c7 | ||
|
|
10c3891abd | ||
|
|
18af48a0fc | ||
|
|
d4cef828e1 | ||
|
|
2d46ccf896 | ||
|
|
fb8bde6485 | ||
|
|
30437dc5d5 | ||
|
|
9f84c2f870 | ||
|
|
e9ea52d9b3 | ||
|
|
0662b07445 | ||
|
|
838cda91aa | ||
|
|
a000cc07d7 | ||
|
|
1316d7de49 | ||
|
|
9c41181875 | ||
|
|
9f3f9a547a | ||
|
|
18df9d1cda | ||
|
|
9a4b3388e0 | ||
|
|
66d5dc27f7 | ||
|
|
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 |
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)
|
||||
56
.changeset/config.json
Normal file
56
.changeset/config.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"$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/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
|
||||
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
|
||||
77
.github/workflows/release-changesets.yml
vendored
Normal file
77
.github/workflows/release-changesets.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
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/server" build
|
||||
pnpm --filter "@esengine/database-drivers" build
|
||||
pnpm --filter "@esengine/database" build
|
||||
pnpm --filter "@esengine/transaction" build
|
||||
pnpm --filter "@esengine/cli" build
|
||||
pnpm --filter "create-esengine-server" build
|
||||
pnpm --filter "@esengine/node-editor" 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
|
||||
37
.gitignore
vendored
37
.gitignore
vendored
@@ -7,7 +7,6 @@ yarn-error.log*
|
||||
# 构建输出
|
||||
bin/
|
||||
dist/
|
||||
wasm/
|
||||
*.tgz
|
||||
|
||||
# TypeScript
|
||||
@@ -17,6 +16,10 @@ wasm/
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
.build-cache/
|
||||
|
||||
# Turborepo
|
||||
.turbo/
|
||||
|
||||
# IDE 配置
|
||||
.idea/
|
||||
@@ -45,22 +48,27 @@ logs/
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# 代码签名证书(敏感文件)
|
||||
certs/
|
||||
*.pfx
|
||||
*.p12
|
||||
*.cer
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# 测试覆盖率
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# Rust 构建文件
|
||||
src/wasm/*/target/
|
||||
src/wasm/*/pkg/
|
||||
Cargo.lock
|
||||
|
||||
# 包管理器锁文件(保留npm的,忽略其他的)
|
||||
# 包管理器锁文件(忽略yarn,保留pnpm)
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
|
||||
# 文档生成
|
||||
docs/api/
|
||||
docs/build/
|
||||
docs/.vitepress/cache/
|
||||
docs/.vitepress/dist/
|
||||
|
||||
# 备份文件
|
||||
*.bak
|
||||
@@ -73,3 +81,16 @@ docs/build/
|
||||
/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/
|
||||
|
||||
18
.gitmodules
vendored
Normal file
18
.gitmodules
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
[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
|
||||
@@ -23,12 +23,6 @@ node_modules/
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Rust 构建文件(保留编译后的WASM)
|
||||
src/wasm/rust-ecs-core/target/
|
||||
src/wasm/rust-ecs-core/Cargo.lock
|
||||
src/wasm/rust-ecs-core/pkg/
|
||||
!bin/wasm/
|
||||
|
||||
# 文档草稿
|
||||
docs/draft/
|
||||
*.draft.md
|
||||
|
||||
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.
|
||||
|
||||
482
README.md
482
README.md
@@ -1,326 +1,312 @@
|
||||
# ECS Framework
|
||||
<h1 align="center">
|
||||
<img src="https://raw.githubusercontent.com/esengine/esengine/master/docs/public/logo.svg" alt="ESEngine" width="180">
|
||||
<br>
|
||||
ESEngine
|
||||
</h1>
|
||||
|
||||
[](https://badge.fury.io/js/%40esengine%2Fecs-framework)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
<p align="center">
|
||||
<strong>Modular Game Framework for TypeScript</strong>
|
||||
</p>
|
||||
|
||||
一个专业级的 TypeScript ECS(Entity-Component-System)框架,采用现代化架构设计,专为高性能游戏开发打造。
|
||||
<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">
|
||||
<b>English</b> | <a href="./README_CN.md">中文</a>
|
||||
</p>
|
||||
|
||||
- 🏗️ **现代化 ECS 架构** - 完整的实体组件系统,提供清晰的代码结构
|
||||
- 📡 **类型安全事件系统** - 增强的事件总线,支持异步事件、优先级、批处理和装饰器
|
||||
- ⏰ **定时器管理系统** - 完整的定时器管理,支持延迟和重复任务
|
||||
- 🔍 **智能查询系统** - 支持复杂的实体查询,流式API设计
|
||||
- ⚡ **高性能优化** - 组件索引、Archetype系统、脏标记机制三重优化
|
||||
- 🛠️ **开发者友好** - 完整的TypeScript支持,丰富的调试工具
|
||||
- 📦 **轻量级设计** - 最小化依赖,适用于各种游戏引擎
|
||||
<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
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
## Features
|
||||
|
||||
### 1. 基础设置
|
||||
| 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 |
|
||||
| **Procgen** | Procedural generation (noise, random, sampling) | No |
|
||||
| **RPC** | High-performance RPC communication framework | No |
|
||||
| **Server** | Game server framework with rooms, auth, rate limiting | No |
|
||||
| **Network** | Client networking with prediction, AOI, delta compression | No |
|
||||
| **Transaction** | Game transaction system with Redis/Memory storage | No |
|
||||
| **World Streaming** | Open world chunk loading and streaming | No |
|
||||
|
||||
```typescript
|
||||
import { Core, CoreEvents, Scene } from '@esengine/ecs-framework';
|
||||
> All framework modules can be used standalone with any rendering engine.
|
||||
|
||||
// 创建 Core 实例
|
||||
const core = Core.create(true); // 开启调试模式
|
||||
## Quick Start
|
||||
|
||||
// 创建场景
|
||||
class GameScene extends Scene {
|
||||
public initialize() {
|
||||
// 场景初始化逻辑
|
||||
}
|
||||
}
|
||||
### Using CLI (Recommended)
|
||||
|
||||
// 在游戏循环中更新框架
|
||||
function gameLoop() {
|
||||
Core.emitter.emit(CoreEvents.frameUpdated);
|
||||
}
|
||||
The easiest way to add ECS to your existing project:
|
||||
|
||||
```bash
|
||||
# In your project directory
|
||||
npx @esengine/cli init
|
||||
```
|
||||
|
||||
### 2. 创建实体和组件
|
||||
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.
|
||||
|
||||
### Manual Setup
|
||||
|
||||
```typescript
|
||||
import { Component, Entity } from '@esengine/ecs-framework';
|
||||
import {
|
||||
Core, Scene, Entity, Component, EntitySystem,
|
||||
Matcher, Time, ECSComponent, ECSSystem
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
class PositionComponent extends Component {
|
||||
constructor(public x: number = 0, public y: number = 0) {
|
||||
super();
|
||||
}
|
||||
// Define components (data only)
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x = 0;
|
||||
y = 0;
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
constructor(public dx: number = 0, public dy: number = 0) {
|
||||
super();
|
||||
}
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx = 0;
|
||||
dy = 0;
|
||||
}
|
||||
|
||||
// 创建实体
|
||||
const entity = scene.createEntity("Player");
|
||||
entity.addComponent(new PositionComponent(100, 100));
|
||||
entity.addComponent(new VelocityComponent(10, 0));
|
||||
```
|
||||
|
||||
### 3. 创建处理系统
|
||||
|
||||
```typescript
|
||||
import { EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
// Define system (logic)
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
public process(entities: Entity[]) {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
const velocity = entity.getComponent(VelocityComponent);
|
||||
|
||||
if (position && velocity) {
|
||||
position.x += velocity.dx;
|
||||
position.y += velocity.dy;
|
||||
}
|
||||
const pos = entity.getComponent(Position);
|
||||
const vel = entity.getComponent(Velocity);
|
||||
pos.x += vel.dx * Time.deltaTime;
|
||||
pos.y += vel.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加系统到场景
|
||||
scene.addEntityProcessor(new MovementSystem());
|
||||
```
|
||||
// Initialize
|
||||
Core.create();
|
||||
const scene = new Scene();
|
||||
scene.addSystem(new MovementSystem());
|
||||
|
||||
### 4. 实体查询和管理
|
||||
const player = scene.createEntity('Player');
|
||||
player.addComponent(new Position());
|
||||
player.addComponent(new Velocity());
|
||||
|
||||
```typescript
|
||||
import { EntityManager } from '@esengine/ecs-framework';
|
||||
Core.setScene(scene);
|
||||
|
||||
// 使用EntityManager进行高级查询
|
||||
const entityManager = new EntityManager();
|
||||
|
||||
// 查询具有特定组件的实体
|
||||
const movingEntities = entityManager
|
||||
.query()
|
||||
.withAll(PositionComponent, VelocityComponent)
|
||||
.execute();
|
||||
|
||||
// 查询带标签的实体
|
||||
const enemies = entityManager.getEntitiesByTag(1);
|
||||
|
||||
// 批量创建实体
|
||||
const bullets = entityManager.createEntities(100, "bullet");
|
||||
```
|
||||
|
||||
### 5. 事件系统
|
||||
|
||||
```typescript
|
||||
import { EventBus, ECSEventType, EventHandler } from '@esengine/ecs-framework';
|
||||
|
||||
// 获取事件总线
|
||||
const eventBus = entityManager.eventBus;
|
||||
|
||||
// 监听实体创建事件
|
||||
eventBus.onEntityCreated((data) => {
|
||||
console.log(`Entity created: ${data.entityName}`);
|
||||
});
|
||||
|
||||
// 监听组件添加事件
|
||||
eventBus.onComponentAdded((data) => {
|
||||
console.log(`Component ${data.componentType} added to entity ${data.entityId}`);
|
||||
});
|
||||
|
||||
// 使用装饰器自动注册事件监听器
|
||||
class GameManager {
|
||||
@EventHandler(ECSEventType.ENTITY_DESTROYED)
|
||||
onEntityDestroyed(data) {
|
||||
console.log('Entity destroyed:', data.entityName);
|
||||
}
|
||||
// Integrate with your game loop
|
||||
function gameLoop(currentTime: number) {
|
||||
Core.update(currentTime / 1000);
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// 自定义事件
|
||||
eventBus.emit('player:levelup', { playerId: 123, newLevel: 5 });
|
||||
requestAnimationFrame(gameLoop);
|
||||
```
|
||||
|
||||
## 🆚 框架对比
|
||||
## Using with Other Engines
|
||||
|
||||
与其他 TypeScript ECS 框架相比,我们的优势:
|
||||
ESEngine's framework modules are designed to work alongside your preferred rendering engine:
|
||||
|
||||
| 特性 | @esengine/ecs-framework | bitecs | ecsy | Miniplex |
|
||||
|------|-------------------------|-------|------|----------|
|
||||
| **TypeScript 支持** | ✅ 原生支持 | ✅ 完整支持 | ⚠️ 部分支持 | ✅ 原生支持 |
|
||||
| **事件系统** | ✅ 类型安全+装饰器 | ❌ 无内置事件系统 | ⚠️ 基础事件 | ✅ 响应式事件 |
|
||||
| **查询系统** | ✅ 智能查询+流式API | ✅ 高性能 | ✅ 基础查询 | ✅ 响应式查询 |
|
||||
| **性能优化** | ✅ 多层优化系统 | ✅ WASM优化 | ⚠️ 基础优化 | ✅ React集成优化 |
|
||||
| **实体管理器** | ✅ 统一管理接口 | ❌ 无统一接口 | ✅ 基础管理 | ✅ 响应式管理 |
|
||||
| **组件索引** | ✅ 哈希+位图索引 | ✅ 原生支持 | ❌ 无索引系统 | ✅ 自动索引 |
|
||||
| **Archetype系统** | ✅ 内置支持 | ✅ 内置支持 | ❌ 无Archetype | ❌ 无Archetype |
|
||||
| **脏标记系统** | ✅ 细粒度追踪 | ⚠️ 基础支持 | ❌ 无脏标记 | ✅ React级追踪 |
|
||||
| **批量操作** | ✅ 全面的批量API | ✅ 批量支持 | ⚠️ 有限支持 | ⚠️ 有限支持 |
|
||||
| **游戏引擎集成** | ✅ 通用设计 | ✅ 通用设计 | ✅ 通用设计 | ⚠️ 主要针对React |
|
||||
| **学习曲线** | 🟢 中等 | 🟡 较陡峭 | 🟢 简单 | 🟡 需要React知识 |
|
||||
| **社区生态** | 🟡 成长中 | 🟢 活跃 | 🟡 稳定 | 🟡 小众但精品 |
|
||||
|
||||
### 为什么选择我们?
|
||||
|
||||
**相比 bitecs**:
|
||||
- 更友好的 TypeScript API,无需手动管理内存
|
||||
- 完整的实体管理器,开发体验更佳
|
||||
- 内置类型安全事件系统,bitecs需要自己实现
|
||||
- 多种索引系统可选,适应不同场景
|
||||
|
||||
**相比 ecsy**:
|
||||
- 现代化的性能优化系统(组件索引、Archetype、脏标记)
|
||||
- 更完整的 TypeScript 类型定义
|
||||
- 增强的事件系统,支持装饰器和异步事件
|
||||
- 活跃的维护和功能更新
|
||||
|
||||
**相比 Miniplex**:
|
||||
- 不依赖 React 生态,可用于任何游戏引擎
|
||||
- 专门针对游戏开发优化
|
||||
- 更轻量级的核心设计
|
||||
- 传统事件模式,更适合游戏开发习惯
|
||||
|
||||
## 📚 核心概念
|
||||
|
||||
### Entity(实体)
|
||||
实体是游戏世界中的基本对象,可以挂载组件和运行系统。
|
||||
### With Cocos Creator
|
||||
|
||||
```typescript
|
||||
// 创建实体
|
||||
const entity = scene.createEntity("Player");
|
||||
import { Component as CCComponent, _decorator } from 'cc';
|
||||
import { Core, Scene, Matcher, EntitySystem } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
|
||||
|
||||
// 设置实体属性
|
||||
entity.tag = 1;
|
||||
entity.updateOrder = 0;
|
||||
entity.enabled = true;
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
// 批量创建实体
|
||||
const entities = scene.createEntities(100, "Enemy");
|
||||
```
|
||||
@ccclass('GameManager')
|
||||
export class GameManager extends CCComponent {
|
||||
private ecsScene!: Scene;
|
||||
|
||||
### Component(组件)
|
||||
组件存储数据,定义实体的属性和状态。
|
||||
start() {
|
||||
Core.create();
|
||||
this.ecsScene = new Scene();
|
||||
|
||||
```typescript
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
// Add ECS systems
|
||||
this.ecsScene.addSystem(new BehaviorTreeExecutionSystem());
|
||||
this.ecsScene.addSystem(new MyGameSystem());
|
||||
|
||||
class HealthComponent extends Component {
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
|
||||
public takeDamage(damage: number) {
|
||||
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||
if (this.currentHealth <= 0) {
|
||||
this.entity.destroy();
|
||||
}
|
||||
Core.setScene(this.ecsScene);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加组件到实体
|
||||
entity.addComponent(new HealthComponent());
|
||||
```
|
||||
|
||||
### System(系统)
|
||||
系统处理具有特定组件的实体集合,实现游戏逻辑。
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
class HealthSystem extends EntitySystem {
|
||||
protected process(entities: Entity[]) {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health && health.currentHealth <= 0) {
|
||||
// 处理实体死亡逻辑
|
||||
entity.destroy();
|
||||
}
|
||||
}
|
||||
update(dt: number) {
|
||||
Core.update(dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
### With Laya 3.x
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
npm run test
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { FSMSystem } from '@esengine/fsm';
|
||||
|
||||
# 性能基准测试
|
||||
npm run benchmark
|
||||
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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📖 文档
|
||||
## Packages
|
||||
|
||||
- [快速入门](docs/getting-started.md) - 从零开始学习框架使用
|
||||
- [EntityManager 使用指南](docs/entity-manager-example.md) - 详细了解实体管理器的高级功能
|
||||
- [事件系统使用指南](docs/event-system-example.md) - 学习类型安全事件系统的完整用法
|
||||
- [性能优化指南](docs/performance-optimization.md) - 深入了解三大性能优化系统
|
||||
- [核心概念](docs/core-concepts.md) - 深入了解 ECS 架构和设计原理
|
||||
- [查询系统使用指南](docs/query-system-usage.md) - 学习高性能查询系统的详细用法
|
||||
### Framework (Engine-Agnostic)
|
||||
|
||||
## 🔗 扩展库
|
||||
|
||||
- [路径寻找库](https://github.com/esengine/ecs-astar) - A*、广度优先、Dijkstra、GOAP 算法
|
||||
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI 系统
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
### 开发环境设置
|
||||
These packages have **zero rendering dependencies** and work with any engine:
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/esengine/ecs-framework.git
|
||||
cd ecs-framework
|
||||
|
||||
# 运行基准测试
|
||||
node benchmark.js
|
||||
|
||||
# 开发构建 (在source目录)
|
||||
cd source && npm install && npm run build
|
||||
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/procgen # Procedural generation
|
||||
npm install @esengine/rpc # RPC framework
|
||||
npm install @esengine/server # Game server
|
||||
npm install @esengine/network # Client networking
|
||||
npm install @esengine/transaction # Transaction system
|
||||
npm install @esengine/world-streaming # World streaming
|
||||
```
|
||||
|
||||
### 构建要求
|
||||
### ESEngine Runtime (Optional)
|
||||
|
||||
- Node.js >= 14.0.0
|
||||
- TypeScript >= 4.0.0
|
||||
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` |
|
||||
|
||||
本项目采用 [MIT](LICENSE) 许可证。
|
||||
### Editor (Optional)
|
||||
|
||||
## 💬 交流群
|
||||
A visual editor built with Tauri for scene management:
|
||||
|
||||
加入 QQ 群讨论:[ecs游戏框架交流](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
|
||||
- Download from [Releases](https://github.com/esengine/esengine/releases)
|
||||
- [Build from source](./packages/editor/editor-app/README.md)
|
||||
- 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
|
||||
│ │ ├── rpc/ # RPC framework
|
||||
│ │ ├── server/ # Game server
|
||||
│ │ ├── network/ # Client networking
|
||||
│ │ ├── transaction/ # Transaction system
|
||||
│ │ └── world-streaming/ # World streaming
|
||||
│ │
|
||||
│ ├── engine/ # ESEngine runtime
|
||||
│ ├── rendering/ # Rendering modules
|
||||
│ ├── physics/ # Physics modules
|
||||
│ ├── editor/ # Visual editor
|
||||
│ └── rust/ # WASM renderer
|
||||
│
|
||||
├── docs/ # Documentation
|
||||
└── examples/ # Examples
|
||||
```
|
||||
|
||||
## Building from Source
|
||||
|
||||
```bash
|
||||
实体创建: 640,000+ 个/秒
|
||||
组件查询: O(1) 复杂度(使用索引)
|
||||
内存优化: 30-50% 减少分配
|
||||
批量操作: 显著提升处理效率
|
||||
git clone https://github.com/esengine/esengine.git
|
||||
cd esengine
|
||||
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
# Type check framework packages
|
||||
pnpm type-check:framework
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
```
|
||||
|
||||
### 🎯 性能优化技术
|
||||
## Documentation
|
||||
|
||||
- **组件索引系统**: 哈希和位图双重索引,支持 O(1) 查询
|
||||
- **Archetype 系统**: 按组件组合分组,减少查询开销
|
||||
- **脏标记机制**: 细粒度变更追踪,避免不必要的计算
|
||||
- **批量操作 API**: 减少函数调用开销,提升大规模操作效率
|
||||
- **智能缓存**: 查询结果缓存和延迟清理机制
|
||||
- [ECS Framework Guide](./packages/framework/core/README.md)
|
||||
- [Behavior Tree Guide](./packages/framework/behavior-tree/README.md)
|
||||
- [Editor Setup Guide](./packages/editor/editor-app/README.md) ([中文](./packages/editor/editor-app/README_CN.md))
|
||||
- [API Reference](https://esengine.cn/api/README)
|
||||
|
||||
### 🔧 性能建议
|
||||
## Community
|
||||
|
||||
1. **大规模场景**: 使用批量API和组件索引
|
||||
2. **频繁查询**: 启用Archetype系统进行快速筛选
|
||||
3. **实时游戏**: 利用脏标记减少无效更新
|
||||
4. **移动端**: 建议实体数量控制在20,000以内
|
||||
- [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
|
||||
|
||||
运行 `npm run benchmark` 查看在您的环境中的具体性能表现。
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please read our contributing guidelines before submitting a pull request.
|
||||
|
||||
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
|
||||
|
||||
## License
|
||||
|
||||
ESEngine is licensed under the [MIT License](LICENSE). Free for personal and commercial use.
|
||||
|
||||
---
|
||||
|
||||
**ECS Framework** - 让游戏开发更简单、更高效!
|
||||
<p align="center">
|
||||
Made with care by the ESEngine community
|
||||
</p>
|
||||
|
||||
313
README_CN.md
Normal file
313
README_CN.md
Normal file
@@ -0,0 +1,313 @@
|
||||
<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* 和导航网格寻路 | 否 |
|
||||
| **程序化生成** | 噪声、随机、采样等生成算法 | 否 |
|
||||
| **RPC** | 高性能 RPC 通信框架 | 否 |
|
||||
| **服务端** | 游戏服务器框架,支持房间、认证、速率限制 | 否 |
|
||||
| **网络** | 客户端网络,支持预测、AOI、增量压缩 | 否 |
|
||||
| **事务系统** | 游戏事务系统,支持 Redis/内存存储 | 否 |
|
||||
| **世界流送** | 开放世界分块加载和流送 | 否 |
|
||||
|
||||
> 所有框架模块都可以独立使用,无需依赖特定渲染引擎。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 使用 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/procgen # 程序化生成
|
||||
npm install @esengine/rpc # RPC 框架
|
||||
npm install @esengine/server # 游戏服务器
|
||||
npm install @esengine/network # 客户端网络
|
||||
npm install @esengine/transaction # 事务系统
|
||||
npm install @esengine/world-streaming # 世界流送
|
||||
```
|
||||
|
||||
### 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) 下载
|
||||
- [从源码构建](./packages/editor/editor-app/README.md)
|
||||
- 支持行为树编辑、Tilemap 绘制、可视化脚本
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
esengine/
|
||||
├── packages/
|
||||
│ ├── framework/ # 引擎无关模块(可发布到 NPM)
|
||||
│ │ ├── core/ # ECS 框架
|
||||
│ │ ├── math/ # 数学工具
|
||||
│ │ ├── behavior-tree/ # AI 行为树
|
||||
│ │ ├── blueprint/ # 可视化脚本
|
||||
│ │ ├── fsm/ # 有限状态机
|
||||
│ │ ├── timer/ # 定时器系统
|
||||
│ │ ├── spatial/ # 空间查询
|
||||
│ │ ├── pathfinding/ # 寻路
|
||||
│ │ ├── procgen/ # 程序化生成
|
||||
│ │ ├── rpc/ # RPC 框架
|
||||
│ │ ├── server/ # 游戏服务器
|
||||
│ │ ├── network/ # 客户端网络
|
||||
│ │ ├── transaction/ # 事务系统
|
||||
│ │ └── world-streaming/ # 世界流送
|
||||
│ │
|
||||
│ ├── 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)
|
||||
- [编辑器启动指南](./packages/editor/editor-app/README_CN.md) ([English](./packages/editor/editor-app/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>
|
||||
76
SECURITY.md
76
SECURITY.md
@@ -1,13 +1,71 @@
|
||||
# Security Policy / 安全政策
|
||||
|
||||
**English** | [中文](#安全政策-1)
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We provide security updates for the following versions:
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.x.x | :white_check_mark: |
|
||||
| 1.x.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability, please report it through the following channels:
|
||||
|
||||
### 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.0.x | :white_check_mark: |
|
||||
| 1.0.x | :x: |
|
||||
| 2.x.x | :white_check_mark: |
|
||||
| 1.x.x | :x: |
|
||||
|
||||
## 报告漏洞
|
||||
|
||||
@@ -15,10 +73,10 @@
|
||||
|
||||
### 报告渠道
|
||||
|
||||
- **邮箱**: [安全邮箱将在实际部署时提供]
|
||||
- **GitHub**: 创建私有安全报告(推荐)
|
||||
- **GitHub 安全公告**: [报告漏洞](https://github.com/esengine/esengine/security/advisories/new)(推荐)
|
||||
- **邮箱**: security@esengine.dev
|
||||
|
||||
### 报告流程
|
||||
### 报告指南
|
||||
|
||||
1. **不要**在公开的 issue 中报告安全漏洞
|
||||
2. 提供详细的漏洞描述,包括:
|
||||
@@ -40,9 +98,9 @@
|
||||
3. 发布安全更新
|
||||
4. 在修复发布后,会在相关渠道公布漏洞详情
|
||||
|
||||
### 安全最佳实践
|
||||
## 安全最佳实践
|
||||
|
||||
使用 ECS Framework 时,请遵循以下安全建议:
|
||||
使用 ESEngine 时,请遵循以下安全建议:
|
||||
|
||||
- 始终使用最新的稳定版本
|
||||
- 定期更新依赖项
|
||||
@@ -50,4 +108,6 @@
|
||||
- 验证所有外部输入数据
|
||||
- 不要在客户端存储敏感信息
|
||||
|
||||
感谢您帮助保持 ECS Framework 的安全性!
|
||||
感谢您帮助保持 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
|
||||
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).
|
||||
365
docs/astro.config.mjs
Normal file
365
docs/astro.config.mjs
Normal file
@@ -0,0 +1,365 @@
|
||||
// @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: '服务端', slug: 'modules/rpc/server', translations: { en: 'Server' } },
|
||||
{ label: '客户端', slug: 'modules/rpc/client', translations: { en: 'Client' } },
|
||||
{ label: '编解码', slug: 'modules/rpc/codec', translations: { en: 'Codec' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
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: 'HTTP 路由', slug: 'modules/network/http', translations: { en: 'HTTP Routing' } },
|
||||
{ label: '认证系统', slug: 'modules/network/auth', translations: { en: 'Authentication' } },
|
||||
{ label: '速率限制', slug: 'modules/network/rate-limit', translations: { en: 'Rate Limiting' } },
|
||||
{ label: '状态同步', slug: 'modules/network/sync', translations: { en: 'State Sync' } },
|
||||
{ label: '客户端预测', slug: 'modules/network/prediction', translations: { en: 'Prediction' } },
|
||||
{ label: 'AOI 兴趣区域', slug: 'modules/network/aoi', translations: { en: 'AOI' } },
|
||||
{ label: '增量压缩', slug: 'modules/network/delta', translations: { en: 'Delta Compression' } },
|
||||
{ label: 'API 参考', slug: 'modules/network/api', translations: { en: 'API Reference' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '事务系统',
|
||||
translations: { en: 'Transaction' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/transaction', translations: { en: 'Overview' } },
|
||||
{ label: '核心概念', slug: 'modules/transaction/core', translations: { en: 'Core Concepts' } },
|
||||
{ label: '存储层', slug: 'modules/transaction/storage', translations: { en: 'Storage Layer' } },
|
||||
{ label: '操作', slug: 'modules/transaction/operations', translations: { en: 'Operations' } },
|
||||
{ label: '分布式事务', slug: 'modules/transaction/distributed', translations: { en: 'Distributed' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据库',
|
||||
translations: { en: 'Database' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/database', translations: { en: 'Overview' } },
|
||||
{ label: '仓储模式', slug: 'modules/database/repository', translations: { en: 'Repository' } },
|
||||
{ label: '用户仓储', slug: 'modules/database/user', translations: { en: 'User Repository' } },
|
||||
{ label: '查询构建器', slug: 'modules/database/query', translations: { en: 'Query Builder' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据库驱动',
|
||||
translations: { en: 'Database Drivers' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/database-drivers', translations: { en: 'Overview' } },
|
||||
{ label: 'MongoDB', slug: 'modules/database-drivers/mongo', translations: { en: 'MongoDB' } },
|
||||
{ label: 'Redis', slug: 'modules/database-drivers/redis', translations: { en: 'Redis' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '世界流式加载',
|
||||
translations: { en: 'World Streaming' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/world-streaming', translations: { en: 'Overview' } },
|
||||
{ label: '区块管理', slug: 'modules/world-streaming/chunk-manager', translations: { en: 'Chunk Manager' } },
|
||||
{ label: '流式系统', slug: 'modules/world-streaming/streaming-system', translations: { en: 'Streaming System' } },
|
||||
{ label: '序列化', slug: 'modules/world-streaming/serialization', translations: { en: 'Serialization' } },
|
||||
{ label: '实际示例', slug: 'modules/world-streaming/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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/transaction', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/transaction/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/rpc', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/rpc/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()],
|
||||
},
|
||||
});
|
||||
@@ -1,622 +0,0 @@
|
||||
# 核心概念
|
||||
|
||||
ECS Framework 基于 Entity-Component-System 架构模式,这是一种高度模块化和可扩展的游戏开发架构。本文档将详细介绍框架的核心概念。
|
||||
|
||||
## ECS 架构概述
|
||||
|
||||
ECS 架构将传统的面向对象设计分解为三个核心部分:
|
||||
|
||||
- **Entity(实体)** - 游戏世界中的对象,包含基本属性如位置、旋转、缩放
|
||||
- **Component(组件)** - 包含数据和行为的功能模块
|
||||
- **System(系统)** - 处理实体集合的逻辑处理单元
|
||||
|
||||
## Core(核心)
|
||||
|
||||
Core 是框架的核心管理类,负责游戏的生命周期管理。
|
||||
|
||||
### 创建和配置
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建核心实例(调试模式)
|
||||
const core = Core.create(true);
|
||||
|
||||
// 创建核心实例(发布模式)
|
||||
const core = Core.create(false);
|
||||
```
|
||||
|
||||
### 事件系统
|
||||
|
||||
```typescript
|
||||
import { CoreEvents } from '@esengine/ecs-framework';
|
||||
|
||||
// 监听核心事件
|
||||
Core.emitter.addObserver(CoreEvents.frameUpdated, this.onUpdate, this);
|
||||
|
||||
// 发送帧更新事件
|
||||
Core.emitter.emit(CoreEvents.frameUpdated);
|
||||
|
||||
// 发送自定义事件
|
||||
Core.emitter.emit("customEvent", { data: "value" });
|
||||
```
|
||||
|
||||
### 定时器系统
|
||||
|
||||
```typescript
|
||||
// 延迟执行
|
||||
Core.schedule(2.0, false, this, (timer) => {
|
||||
console.log("2秒后执行");
|
||||
});
|
||||
|
||||
// 重复执行
|
||||
Core.schedule(1.0, true, this, (timer) => {
|
||||
console.log("每秒执行一次");
|
||||
});
|
||||
```
|
||||
|
||||
## Scene(场景)
|
||||
|
||||
场景是游戏世界的容器,管理实体和系统的生命周期。
|
||||
|
||||
### 创建和使用场景
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建场景
|
||||
const scene = new Scene();
|
||||
scene.name = "GameScene";
|
||||
|
||||
// 设置为当前场景
|
||||
Core.scene = scene;
|
||||
|
||||
// 场景生命周期
|
||||
scene.begin(); // 开始场景
|
||||
scene.update(); // 更新场景
|
||||
scene.end(); // 结束场景
|
||||
```
|
||||
|
||||
### 批量实体管理
|
||||
|
||||
```typescript
|
||||
// 批量创建实体 - 高性能
|
||||
const entities = scene.createEntities(1000, "Enemy");
|
||||
|
||||
// 批量添加实体(延迟缓存清理)
|
||||
entities.forEach(entity => {
|
||||
scene.addEntity(entity, false); // 延迟清理
|
||||
});
|
||||
scene.querySystem.clearCache(); // 手动清理缓存
|
||||
|
||||
// 获取性能统计
|
||||
const stats = scene.getPerformanceStats();
|
||||
console.log(`实体数量: ${stats.entityCount}`);
|
||||
```
|
||||
|
||||
## Entity(实体)
|
||||
|
||||
实体是游戏世界中的基本对象,包含位置、旋转、缩放等基本属性。
|
||||
|
||||
### 实体的基本属性
|
||||
|
||||
```typescript
|
||||
import { Vector2 } from '@esengine/ecs-framework';
|
||||
|
||||
const entity = scene.createEntity("MyEntity");
|
||||
|
||||
// 位置
|
||||
entity.position = new Vector2(100, 200);
|
||||
entity.position = entity.position.add(new Vector2(10, 0));
|
||||
|
||||
// 旋转(弧度)
|
||||
entity.rotation = Math.PI / 4;
|
||||
|
||||
// 缩放
|
||||
entity.scale = new Vector2(2, 2);
|
||||
|
||||
// 标签(用于分类)
|
||||
entity.tag = 1;
|
||||
|
||||
// 启用状态
|
||||
entity.enabled = true;
|
||||
|
||||
// 活跃状态
|
||||
entity.active = true;
|
||||
|
||||
// 更新顺序
|
||||
entity.updateOrder = 10;
|
||||
```
|
||||
|
||||
### 实体层级关系
|
||||
|
||||
```typescript
|
||||
// 添加子实体
|
||||
const parent = scene.createEntity("Parent");
|
||||
const child = scene.createEntity("Child");
|
||||
parent.addChild(child);
|
||||
|
||||
// 获取父实体
|
||||
const parentEntity = child.parent;
|
||||
|
||||
// 获取所有子实体
|
||||
const children = parent.children;
|
||||
|
||||
// 查找子实体
|
||||
const foundChild = parent.findChild("Child");
|
||||
|
||||
// 按标签查找子实体
|
||||
const taggedChildren = parent.findChildrenByTag(1);
|
||||
|
||||
// 移除子实体
|
||||
parent.removeChild(child);
|
||||
|
||||
// 移除所有子实体
|
||||
parent.removeAllChildren();
|
||||
```
|
||||
|
||||
### 实体生命周期
|
||||
|
||||
```typescript
|
||||
// 检查实体是否被销毁
|
||||
if (!entity.isDestroyed) {
|
||||
// 实体仍然有效
|
||||
}
|
||||
|
||||
// 销毁实体
|
||||
entity.destroy();
|
||||
|
||||
// 获取实体调试信息
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
## Component(组件)
|
||||
|
||||
组件包含数据和行为,定义了实体的特性和能力。
|
||||
|
||||
### 创建组件
|
||||
|
||||
```typescript
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
class HealthComponent extends Component {
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
|
||||
public takeDamage(damage: number) {
|
||||
this.currentHealth -= damage;
|
||||
if (this.currentHealth <= 0) {
|
||||
this.entity.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public heal(amount: number) {
|
||||
this.currentHealth = Math.min(this.maxHealth, this.currentHealth + amount);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组件生命周期
|
||||
|
||||
```typescript
|
||||
class MyComponent extends Component {
|
||||
public onAddedToEntity() {
|
||||
// 组件被添加到实体时调用
|
||||
console.log("组件已添加到实体:", this.entity.name);
|
||||
}
|
||||
|
||||
public onRemovedFromEntity() {
|
||||
// 组件从实体移除时调用
|
||||
console.log("组件已从实体移除");
|
||||
}
|
||||
|
||||
public onEnabled() {
|
||||
// 组件启用时调用
|
||||
console.log("组件已启用");
|
||||
}
|
||||
|
||||
public onDisabled() {
|
||||
// 组件禁用时调用
|
||||
console.log("组件已禁用");
|
||||
}
|
||||
|
||||
public update() {
|
||||
// 每帧更新(如果组件启用)
|
||||
console.log("组件更新");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组件管理
|
||||
|
||||
```typescript
|
||||
// 添加组件
|
||||
const health = entity.addComponent(new HealthComponent());
|
||||
|
||||
// 创建并添加组件
|
||||
const movement = entity.createComponent(MovementComponent, 200); // 传递构造参数
|
||||
|
||||
// 获取组件
|
||||
const healthComp = entity.getComponent(HealthComponent);
|
||||
|
||||
// 检查组件是否存在
|
||||
if (entity.hasComponent(HealthComponent)) {
|
||||
// 处理逻辑
|
||||
}
|
||||
|
||||
// 获取或创建组件
|
||||
const weapon = entity.getOrCreateComponent(WeaponComponent);
|
||||
|
||||
// 获取多个同类型组件
|
||||
const allHealthComps = entity.getComponents(HealthComponent);
|
||||
|
||||
// 移除组件
|
||||
entity.removeComponent(healthComp);
|
||||
|
||||
// 按类型移除组件
|
||||
entity.removeComponentByType(HealthComponent);
|
||||
|
||||
// 移除所有组件
|
||||
entity.removeAllComponents();
|
||||
```
|
||||
|
||||
### 组件对象池优化
|
||||
|
||||
```typescript
|
||||
import { Component, ComponentPoolManager } from '@esengine/ecs-framework';
|
||||
|
||||
class BulletComponent extends Component {
|
||||
public damage: number = 10;
|
||||
public speed: number = 300;
|
||||
|
||||
// 对象池重置方法
|
||||
public reset() {
|
||||
this.damage = 10;
|
||||
this.speed = 300;
|
||||
}
|
||||
}
|
||||
|
||||
// 注册组件池
|
||||
ComponentPoolManager.getInstance().registerPool(BulletComponent, 1000);
|
||||
|
||||
// 使用对象池获取组件
|
||||
const bullet = ComponentPoolManager.getInstance().getComponent(BulletComponent);
|
||||
entity.addComponent(bullet);
|
||||
|
||||
// 释放组件回对象池
|
||||
ComponentPoolManager.getInstance().releaseComponent(bullet);
|
||||
|
||||
// 预热组件池
|
||||
ComponentPoolManager.getInstance().preWarmPools({
|
||||
BulletComponent: 1000,
|
||||
EffectComponent: 500
|
||||
});
|
||||
|
||||
// 获取池统计
|
||||
const stats = ComponentPoolManager.getInstance().getPoolStats();
|
||||
console.log('组件池统计:', stats);
|
||||
```
|
||||
|
||||
## Scene(场景)
|
||||
|
||||
场景是实体和系统的容器,管理游戏世界的状态。
|
||||
|
||||
### 场景生命周期
|
||||
|
||||
```typescript
|
||||
class GameScene extends es.Scene {
|
||||
public initialize() {
|
||||
// 场景初始化,创建实体和系统
|
||||
this.setupEntities();
|
||||
this.setupSystems();
|
||||
}
|
||||
|
||||
public onStart() {
|
||||
// 场景开始运行时调用
|
||||
console.log("场景开始");
|
||||
}
|
||||
|
||||
public unload() {
|
||||
// 场景卸载时调用
|
||||
console.log("场景卸载");
|
||||
}
|
||||
|
||||
private setupEntities() {
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new PlayerComponent());
|
||||
}
|
||||
|
||||
private setupSystems() {
|
||||
this.addEntityProcessor(new MovementSystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实体管理
|
||||
|
||||
```typescript
|
||||
// 创建实体
|
||||
const entity = scene.createEntity("MyEntity");
|
||||
|
||||
// 添加现有实体
|
||||
scene.addEntity(entity);
|
||||
|
||||
// 查找实体
|
||||
const player = scene.findEntity("Player");
|
||||
const entityById = scene.findEntityById(123);
|
||||
const entitiesByTag = scene.findEntitiesByTag(1);
|
||||
|
||||
// 销毁所有实体
|
||||
scene.destroyAllEntities();
|
||||
|
||||
// 获取场景统计信息
|
||||
const stats = scene.getStats();
|
||||
console.log("实体数量:", stats.entityCount);
|
||||
console.log("系统数量:", stats.processorCount);
|
||||
```
|
||||
|
||||
## System(系统)
|
||||
|
||||
系统处理实体集合,实现游戏的核心逻辑。
|
||||
|
||||
### EntitySystem
|
||||
|
||||
最常用的系统类型,处理实体集合:
|
||||
|
||||
```typescript
|
||||
class MovementSystem extends es.EntitySystem {
|
||||
protected process(entities: es.Entity[]) {
|
||||
for (const entity of entities) {
|
||||
const movement = entity.getComponent(MovementComponent);
|
||||
if (movement) {
|
||||
movement.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ProcessingSystem
|
||||
|
||||
定期处理的系统:
|
||||
|
||||
```typescript
|
||||
class HealthRegenerationSystem extends es.ProcessingSystem {
|
||||
protected process(entities: es.Entity[]) {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health && health.currentHealth < health.maxHealth) {
|
||||
health.currentHealth += 10 * es.Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IntervalSystem
|
||||
|
||||
按时间间隔执行的系统:
|
||||
|
||||
```typescript
|
||||
class SpawnSystem extends es.IntervalSystem {
|
||||
constructor() {
|
||||
super(3.0); // 每3秒执行一次
|
||||
}
|
||||
|
||||
protected processSystem() {
|
||||
// 生成敌人
|
||||
const enemy = this.scene.createEntity("Enemy");
|
||||
enemy.addComponent(new EnemyComponent());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PassiveSystem
|
||||
|
||||
被动系统,不自动处理实体:
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends es.PassiveSystem {
|
||||
public checkCollisions() {
|
||||
// 手动调用的碰撞检测逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Time(时间)
|
||||
|
||||
时间管理工具类,提供游戏时间相关功能:
|
||||
|
||||
```typescript
|
||||
// 获取时间信息
|
||||
console.log("帧时间:", es.Time.deltaTime);
|
||||
console.log("总时间:", es.Time.totalTime);
|
||||
console.log("帧数:", es.Time.frameCount);
|
||||
console.log("时间缩放:", es.Time.timeScale);
|
||||
|
||||
// 设置时间缩放(慢动作效果)
|
||||
es.Time.timeScale = 0.5;
|
||||
|
||||
// 检查时间间隔
|
||||
if (es.Time.checkEvery(1.0, lastCheckTime)) {
|
||||
// 每秒执行一次
|
||||
}
|
||||
```
|
||||
|
||||
## Vector2(二维向量)
|
||||
|
||||
二维向量类,提供数学运算:
|
||||
|
||||
```typescript
|
||||
// 创建向量
|
||||
const vec1 = new es.Vector2(10, 20);
|
||||
const vec2 = es.Vector2.zero;
|
||||
const vec3 = es.Vector2.one;
|
||||
|
||||
// 向量运算
|
||||
const sum = vec1.add(vec2);
|
||||
const diff = vec1.subtract(vec2);
|
||||
const scaled = vec1.multiply(2);
|
||||
const normalized = vec1.normalize();
|
||||
|
||||
// 向量属性
|
||||
console.log("长度:", vec1.length);
|
||||
console.log("长度平方:", vec1.lengthSquared);
|
||||
|
||||
// 静态方法
|
||||
const distance = es.Vector2.distance(vec1, vec2);
|
||||
const lerped = es.Vector2.lerp(vec1, vec2, 0.5);
|
||||
const fromAngle = es.Vector2.fromAngle(Math.PI / 4);
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
框架内置性能监控工具:
|
||||
|
||||
```typescript
|
||||
// 获取性能监控实例
|
||||
const monitor = es.PerformanceMonitor.instance;
|
||||
|
||||
// 查看性能数据
|
||||
console.log("平均FPS:", monitor.averageFPS);
|
||||
console.log("最小FPS:", monitor.minFPS);
|
||||
console.log("最大FPS:", monitor.maxFPS);
|
||||
console.log("内存使用:", monitor.memoryUsage);
|
||||
|
||||
// 重置性能数据
|
||||
monitor.reset();
|
||||
```
|
||||
|
||||
## 对象池
|
||||
|
||||
内存管理优化工具:
|
||||
|
||||
```typescript
|
||||
// 创建对象池
|
||||
class BulletPool extends es.Pool<Bullet> {
|
||||
protected createObject(): Bullet {
|
||||
return new Bullet();
|
||||
}
|
||||
}
|
||||
|
||||
const bulletPool = new BulletPool();
|
||||
|
||||
// 使用对象池
|
||||
const bullet = bulletPool.obtain();
|
||||
// 使用bullet...
|
||||
bulletPool.free(bullet);
|
||||
|
||||
// 清空对象池
|
||||
bulletPool.clear();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 实体设计
|
||||
|
||||
- 实体只包含基本属性,功能通过组件实现
|
||||
- 合理使用实体层级关系
|
||||
- 及时销毁不需要的实体
|
||||
|
||||
### 2. 组件设计
|
||||
|
||||
- 组件保持单一职责
|
||||
- 使用生命周期方法进行初始化和清理
|
||||
- 避免组件间直接依赖
|
||||
|
||||
### 3. 系统设计
|
||||
|
||||
- 系统专注于特定逻辑处理
|
||||
- 合理设置系统更新顺序
|
||||
- 使用被动系统处理特殊逻辑
|
||||
|
||||
### 4. 性能优化
|
||||
|
||||
- 使用对象池减少内存分配
|
||||
- 监控性能数据
|
||||
- 合理使用时间缩放
|
||||
|
||||
## 高级性能优化功能
|
||||
|
||||
### 位掩码优化器
|
||||
|
||||
位掩码优化器可以预计算和缓存常用的组件掩码,提升查询性能。
|
||||
|
||||
```typescript
|
||||
import { BitMaskOptimizer } from '@esengine/ecs-framework';
|
||||
|
||||
const optimizer = BitMaskOptimizer.getInstance();
|
||||
|
||||
// 注册组件类型
|
||||
optimizer.registerComponentType(PositionComponent);
|
||||
optimizer.registerComponentType(VelocityComponent);
|
||||
optimizer.registerComponentType(RenderComponent);
|
||||
|
||||
// 预计算常用掩码组合
|
||||
optimizer.precomputeCommonMasks();
|
||||
|
||||
// 获取优化的掩码
|
||||
const positionMask = optimizer.getComponentMask(PositionComponent);
|
||||
const movementMask = optimizer.getCombinedMask([PositionComponent, VelocityComponent]);
|
||||
|
||||
// 掩码操作
|
||||
const hasBothComponents = optimizer.hasAllComponents(entityMask, movementMask);
|
||||
const hasAnyComponent = optimizer.hasAnyComponent(entityMask, movementMask);
|
||||
|
||||
// 获取掩码分析
|
||||
const analysis = optimizer.analyzeMask(entityMask);
|
||||
console.log('掩码包含的组件类型:', analysis.componentTypes);
|
||||
```
|
||||
|
||||
### 延迟索引更新器
|
||||
|
||||
批量更新索引可以显著提升大规模实体操作的性能。
|
||||
|
||||
```typescript
|
||||
import { IndexUpdateBatcher } from '@esengine/ecs-framework';
|
||||
|
||||
const batcher = new IndexUpdateBatcher((updates) => {
|
||||
// 处理批量更新
|
||||
console.log(`批量处理 ${updates.length} 个索引更新`);
|
||||
});
|
||||
|
||||
// 配置批量大小和延迟
|
||||
batcher.configure(100, 16); // 批量大小100,延迟16ms
|
||||
|
||||
// 添加更新任务
|
||||
batcher.addUpdate("add", entity, componentMask);
|
||||
batcher.addUpdate("remove", entity, componentMask);
|
||||
|
||||
// 强制刷新
|
||||
batcher.flush();
|
||||
```
|
||||
|
||||
### 批量操作API
|
||||
|
||||
```typescript
|
||||
// 批量创建实体 - 最高性能
|
||||
const entities = scene.createEntities(10000, "Bullets");
|
||||
|
||||
// 延迟缓存清理
|
||||
entities.forEach(entity => {
|
||||
scene.addEntity(entity, false); // 延迟清理
|
||||
});
|
||||
scene.querySystem.clearCache(); // 手动清理
|
||||
|
||||
// 批量查询优化
|
||||
const movingEntities = scene.getEntitiesWithComponents([PositionComponent, VelocityComponent]);
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
ECS Framework 提供了完整的实体组件系统架构:
|
||||
|
||||
- **Core** 管理游戏生命周期和全局功能
|
||||
- **Entity** 作为游戏对象的基础容器
|
||||
- **Component** 实现具体的功能模块,支持对象池优化
|
||||
- **System** 处理游戏逻辑
|
||||
- **Scene** 管理游戏世界状态,支持批量操作
|
||||
- **高级优化** 位掩码优化器、组件对象池、批量操作等
|
||||
|
||||
通过合理使用这些核心概念和优化功能,可以构建出高性能、结构清晰、易于维护的游戏代码。
|
||||
@@ -1,482 +0,0 @@
|
||||
# 实体使用指南
|
||||
|
||||
本指南详细介绍 ECS Framework 中实体(Entity)的所有功能和使用方法。
|
||||
|
||||
## 实体概述
|
||||
|
||||
实体(Entity)是 ECS 架构中的核心概念之一,它作为组件的容器存在。实体本身不包含游戏逻辑,所有功能都通过添加不同的组件来实现。
|
||||
|
||||
### 实体的特点
|
||||
|
||||
- **轻量级容器**:实体只是组件的载体,不包含具体的游戏逻辑
|
||||
- **唯一标识**:每个实体都有唯一的ID和名称
|
||||
- **层次结构**:支持父子关系,可以构建复杂的实体层次
|
||||
- **高性能查询**:基于位掩码的组件查询系统
|
||||
- **生命周期管理**:完整的创建、更新、销毁流程
|
||||
|
||||
## 创建实体
|
||||
|
||||
### 基本创建方式
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 通过场景创建实体
|
||||
const scene = new Scene();
|
||||
const entity = scene.createEntity("Player");
|
||||
|
||||
console.log(entity.name); // "Player"
|
||||
console.log(entity.id); // 唯一的数字ID
|
||||
```
|
||||
|
||||
### 批量创建实体(推荐)
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
const scene = new Scene();
|
||||
|
||||
// 批量创建1000个实体 - 高性能
|
||||
const entities = scene.createEntities(1000, "Enemy");
|
||||
|
||||
// 批量配置
|
||||
entities.forEach((entity, index) => {
|
||||
entity.tag = 2; // 敌人标签
|
||||
// 添加组件...
|
||||
});
|
||||
```
|
||||
|
||||
### 使用流式API创建
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 使用ECS流式API
|
||||
const entity = Core.ecsAPI
|
||||
?.entity("Enemy")
|
||||
.withComponent(new PositionComponent(100, 200))
|
||||
.withComponent(new HealthComponent(50))
|
||||
.withTag(2)
|
||||
.build();
|
||||
```
|
||||
|
||||
## 实体属性
|
||||
|
||||
### 基本属性
|
||||
|
||||
```typescript
|
||||
// 实体名称 - 用于调试和标识
|
||||
entity.name = "Player";
|
||||
|
||||
// 实体ID - 只读,场景内唯一
|
||||
console.log(entity.id); // 例如: 1
|
||||
|
||||
// 标签 - 用于分类和快速查询
|
||||
entity.tag = 1; // 玩家标签
|
||||
entity.tag = 2; // 敌人标签
|
||||
|
||||
// 更新顺序 - 控制实体在系统中的处理优先级
|
||||
entity.updateOrder = 0; // 数值越小优先级越高
|
||||
```
|
||||
|
||||
### 状态控制
|
||||
|
||||
```typescript
|
||||
// 启用状态 - 控制实体是否参与更新和处理
|
||||
entity.enabled = true; // 启用实体
|
||||
entity.enabled = false; // 禁用实体
|
||||
|
||||
// 激活状态 - 控制实体及其子实体的活跃状态
|
||||
entity.active = true; // 激活实体
|
||||
entity.active = false; // 停用实体
|
||||
|
||||
// 检查层次结构中的激活状态
|
||||
if (entity.activeInHierarchy) {
|
||||
// 实体在整个层次结构中都是激活的
|
||||
}
|
||||
|
||||
// 检查销毁状态
|
||||
if (entity.isDestroyed) {
|
||||
// 实体已被销毁
|
||||
}
|
||||
```
|
||||
|
||||
### 更新间隔
|
||||
|
||||
```typescript
|
||||
// 控制实体更新频率
|
||||
entity.updateInterval = 1; // 每帧更新
|
||||
entity.updateInterval = 2; // 每2帧更新一次
|
||||
entity.updateInterval = 5; // 每5帧更新一次
|
||||
```
|
||||
|
||||
## 组件管理
|
||||
|
||||
### 添加组件
|
||||
|
||||
```typescript
|
||||
// 创建并添加组件
|
||||
const healthComponent = entity.addComponent(new HealthComponent(100));
|
||||
|
||||
// 使用工厂方法创建组件
|
||||
const positionComponent = entity.createComponent(PositionComponent, 100, 200);
|
||||
|
||||
// 批量添加组件
|
||||
const components = entity.addComponents([
|
||||
new PositionComponent(0, 0),
|
||||
new VelocityComponent(50, 0),
|
||||
new HealthComponent(100)
|
||||
]);
|
||||
```
|
||||
|
||||
### 获取组件
|
||||
|
||||
```typescript
|
||||
// 获取单个组件
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health) {
|
||||
console.log(`当前生命值: ${health.currentHealth}`);
|
||||
}
|
||||
|
||||
// 获取或创建组件(如果不存在则创建)
|
||||
const position = entity.getOrCreateComponent(PositionComponent, 0, 0);
|
||||
|
||||
// 获取多个同类型组件(如果组件可以重复添加)
|
||||
const allHealthComponents = entity.getComponents(HealthComponent);
|
||||
```
|
||||
|
||||
### 检查组件
|
||||
|
||||
```typescript
|
||||
// 检查是否拥有指定组件
|
||||
if (entity.hasComponent(HealthComponent)) {
|
||||
// 实体拥有生命值组件
|
||||
}
|
||||
|
||||
// 检查组件掩码(高性能)
|
||||
const mask = entity.componentMask;
|
||||
console.log(`组件掩码: ${mask.toString(2)}`);
|
||||
```
|
||||
|
||||
### 移除组件
|
||||
|
||||
```typescript
|
||||
// 移除指定组件实例
|
||||
const healthComponent = entity.getComponent(HealthComponent);
|
||||
if (healthComponent) {
|
||||
entity.removeComponent(healthComponent);
|
||||
}
|
||||
|
||||
// 按类型移除组件
|
||||
const removedHealth = entity.removeComponentByType(HealthComponent);
|
||||
|
||||
// 批量移除组件
|
||||
const removedComponents = entity.removeComponentsByTypes([
|
||||
HealthComponent,
|
||||
VelocityComponent
|
||||
]);
|
||||
|
||||
// 移除所有组件
|
||||
entity.removeAllComponents();
|
||||
```
|
||||
|
||||
## 层次结构管理
|
||||
|
||||
### 父子关系
|
||||
|
||||
```typescript
|
||||
// 创建父子实体
|
||||
const player = scene.createEntity("Player");
|
||||
const weapon = scene.createEntity("Weapon");
|
||||
const shield = scene.createEntity("Shield");
|
||||
|
||||
// 添加子实体
|
||||
player.addChild(weapon);
|
||||
player.addChild(shield);
|
||||
|
||||
// 获取父实体
|
||||
console.log(weapon.parent === player); // true
|
||||
|
||||
// 获取所有子实体
|
||||
const children = player.children;
|
||||
console.log(children.length); // 2
|
||||
|
||||
// 获取子实体数量
|
||||
console.log(player.childCount); // 2
|
||||
```
|
||||
|
||||
### 查找子实体
|
||||
|
||||
```typescript
|
||||
// 按名称查找子实体
|
||||
const weapon = player.findChild("Weapon");
|
||||
|
||||
// 递归查找子实体
|
||||
const deepChild = player.findChild("DeepChild", true);
|
||||
|
||||
// 按标签查找子实体
|
||||
const enemies = player.findChildrenByTag(2); // 查找所有敌人标签的子实体
|
||||
|
||||
// 递归按标签查找
|
||||
const allEnemies = player.findChildrenByTag(2, true);
|
||||
```
|
||||
|
||||
### 层次结构操作
|
||||
|
||||
```typescript
|
||||
// 移除子实体
|
||||
const removed = player.removeChild(weapon);
|
||||
|
||||
// 移除所有子实体
|
||||
player.removeAllChildren();
|
||||
|
||||
// 获取根实体
|
||||
const root = weapon.getRoot();
|
||||
|
||||
// 检查祖先关系
|
||||
if (player.isAncestorOf(weapon)) {
|
||||
// player 是 weapon 的祖先
|
||||
}
|
||||
|
||||
// 检查后代关系
|
||||
if (weapon.isDescendantOf(player)) {
|
||||
// weapon 是 player 的后代
|
||||
}
|
||||
|
||||
// 获取实体在层次结构中的深度
|
||||
const depth = weapon.getDepth(); // 从根实体开始计算的深度
|
||||
```
|
||||
|
||||
### 遍历子实体
|
||||
|
||||
```typescript
|
||||
// 遍历直接子实体
|
||||
player.forEachChild((child, index) => {
|
||||
console.log(`子实体 ${index}: ${child.name}`);
|
||||
});
|
||||
|
||||
// 递归遍历所有子实体
|
||||
player.forEachChild((child, index) => {
|
||||
console.log(`子实体 ${index}: ${child.name} (深度: ${child.getDepth()})`);
|
||||
}, true);
|
||||
```
|
||||
|
||||
## 实体生命周期
|
||||
|
||||
### 更新循环
|
||||
|
||||
```typescript
|
||||
// 手动更新实体(通常由场景自动调用)
|
||||
entity.update();
|
||||
|
||||
// 实体会自动调用所有组件的update方法
|
||||
class MyComponent extends Component {
|
||||
public update(): void {
|
||||
// 组件更新逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 销毁实体
|
||||
|
||||
```typescript
|
||||
// 销毁实体
|
||||
entity.destroy();
|
||||
|
||||
// 检查是否已销毁
|
||||
if (entity.isDestroyed) {
|
||||
console.log("实体已被销毁");
|
||||
}
|
||||
|
||||
// 销毁实体时会自动:
|
||||
// 1. 移除所有组件
|
||||
// 2. 从父实体中移除
|
||||
// 3. 销毁所有子实体
|
||||
// 4. 从场景中移除
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 组件缓存
|
||||
|
||||
```typescript
|
||||
// 预热组件缓存(提高后续访问性能)
|
||||
entity.warmUpComponentCache();
|
||||
|
||||
// 清理组件缓存
|
||||
entity.cleanupComponentCache();
|
||||
|
||||
// 获取缓存统计信息
|
||||
const cacheStats = entity.getComponentCacheStats();
|
||||
console.log(`缓存命中率: ${cacheStats.cacheStats.hitRate}`);
|
||||
console.log(`组件访问统计:`, cacheStats.accessStats);
|
||||
```
|
||||
|
||||
### 批量操作
|
||||
|
||||
```typescript
|
||||
// 批量添加组件(比单个添加更高效)
|
||||
const components = entity.addComponents([
|
||||
new PositionComponent(0, 0),
|
||||
new VelocityComponent(50, 0),
|
||||
new HealthComponent(100)
|
||||
]);
|
||||
|
||||
// 批量移除组件
|
||||
const removed = entity.removeComponentsByTypes([
|
||||
HealthComponent,
|
||||
VelocityComponent
|
||||
]);
|
||||
```
|
||||
|
||||
## 调试和监控
|
||||
|
||||
### 调试信息
|
||||
|
||||
```typescript
|
||||
// 获取详细的调试信息
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log("实体调试信息:", debugInfo);
|
||||
|
||||
// 调试信息包含:
|
||||
// - 基本属性(名称、ID、状态等)
|
||||
// - 组件信息(数量、类型、掩码等)
|
||||
// - 层次结构信息(父子关系、深度等)
|
||||
// - 性能统计(缓存命中率、访问统计等)
|
||||
```
|
||||
|
||||
### 实体比较
|
||||
|
||||
```typescript
|
||||
// 比较两个实体的优先级
|
||||
const result = entity1.compareTo(entity2);
|
||||
if (result < 0) {
|
||||
// entity1 优先级更高
|
||||
} else if (result > 0) {
|
||||
// entity2 优先级更高
|
||||
} else {
|
||||
// 优先级相同
|
||||
}
|
||||
|
||||
// 实体的字符串表示
|
||||
console.log(entity.toString()); // "Entity[Player:1]"
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理使用标签
|
||||
|
||||
```typescript
|
||||
// 定义标签常量
|
||||
const Tags = {
|
||||
PLAYER: 1,
|
||||
ENEMY: 2,
|
||||
PROJECTILE: 3,
|
||||
PICKUP: 4
|
||||
} as const;
|
||||
|
||||
// 使用标签进行分类
|
||||
player.tag = Tags.PLAYER;
|
||||
enemy.tag = Tags.ENEMY;
|
||||
```
|
||||
|
||||
### 2. 优化更新顺序
|
||||
|
||||
```typescript
|
||||
// 设置合理的更新顺序
|
||||
player.updateOrder = 0; // 玩家最先更新
|
||||
enemy.updateOrder = 1; // 敌人其次
|
||||
projectile.updateOrder = 2; // 投射物最后
|
||||
```
|
||||
|
||||
### 3. 合理使用层次结构
|
||||
|
||||
```typescript
|
||||
// 创建复合实体
|
||||
const tank = scene.createEntity("Tank");
|
||||
const turret = scene.createEntity("Turret");
|
||||
const barrel = scene.createEntity("Barrel");
|
||||
|
||||
// 建立层次关系
|
||||
tank.addChild(turret);
|
||||
turret.addChild(barrel);
|
||||
|
||||
// 这样可以通过控制父实体来影响整个层次结构
|
||||
tank.active = false; // 整个坦克都会被停用
|
||||
```
|
||||
|
||||
### 4. 组件缓存优化
|
||||
|
||||
```typescript
|
||||
// 对于频繁访问的组件,预热缓存
|
||||
entity.warmUpComponentCache();
|
||||
|
||||
// 定期清理不常用的缓存
|
||||
setInterval(() => {
|
||||
entity.cleanupComponentCache();
|
||||
}, 5000);
|
||||
```
|
||||
|
||||
### 5. 避免内存泄漏
|
||||
|
||||
```typescript
|
||||
// 确保正确销毁实体
|
||||
if (entity.isDestroyed) {
|
||||
return; // 避免操作已销毁的实体
|
||||
}
|
||||
|
||||
// 在适当的时候销毁不需要的实体
|
||||
if (enemy.getComponent(HealthComponent)?.isDead()) {
|
||||
enemy.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 实体可以在不同场景间移动吗?
|
||||
|
||||
A: 不可以。实体与场景紧密绑定,如果需要在场景间传递数据,应该序列化实体的组件数据,然后在新场景中重新创建。
|
||||
|
||||
### Q: 如何实现实体的位置、旋转、缩放?
|
||||
|
||||
A: 框架本身不提供这些属性,需要通过组件来实现:
|
||||
|
||||
```typescript
|
||||
class TransformComponent extends Component {
|
||||
public position = { x: 0, y: 0 };
|
||||
public rotation = 0;
|
||||
public scale = { x: 1, y: 1 };
|
||||
}
|
||||
|
||||
const transform = entity.addComponent(new TransformComponent());
|
||||
transform.position.x = 100;
|
||||
transform.rotation = Math.PI / 4;
|
||||
```
|
||||
|
||||
### Q: 实体的更新顺序如何影响性能?
|
||||
|
||||
A: 更新顺序主要影响游戏逻辑的执行顺序,对性能影响较小。但合理的更新顺序可以避免一些逻辑问题,比如确保输入处理在移动之前执行。
|
||||
|
||||
### Q: 如何处理大量实体的性能问题?
|
||||
|
||||
A:
|
||||
1. 使用对象池重用实体
|
||||
2. 合理使用组件缓存
|
||||
3. 避免不必要的组件查询
|
||||
4. 使用批量操作
|
||||
5. 定期清理销毁的实体
|
||||
|
||||
```typescript
|
||||
// 使用对象池
|
||||
class EntityPool extends Pool<Entity> {
|
||||
protected createObject(): Entity {
|
||||
return scene.createEntity("PooledEntity");
|
||||
}
|
||||
|
||||
protected resetObject(entity: Entity): void {
|
||||
entity.removeAllComponents();
|
||||
entity.active = true;
|
||||
entity.enabled = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,421 +0,0 @@
|
||||
# EntityManager 使用指南
|
||||
|
||||
EntityManager 是 ECS Framework 的核心管理系统,提供统一的实体管理、高性能查询和自动优化功能。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 创建实体管理器
|
||||
|
||||
```typescript
|
||||
import { EntityManager, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 通常在游戏管理器中创建
|
||||
const scene = new Scene();
|
||||
const entityManager = new EntityManager(scene);
|
||||
```
|
||||
|
||||
### 基础实体操作
|
||||
|
||||
```typescript
|
||||
// 创建单个实体
|
||||
const player = entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(100, 100));
|
||||
player.addComponent(new HealthComponent(100));
|
||||
player.tag = "player";
|
||||
|
||||
// 批量创建实体
|
||||
const enemies = entityManager.createEntities(50, "Enemy");
|
||||
enemies.forEach((enemy, index) => {
|
||||
enemy.addComponent(new PositionComponent(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new HealthComponent(30));
|
||||
enemy.tag = "enemy";
|
||||
});
|
||||
|
||||
// 销毁实体
|
||||
entityManager.destroyEntity(player);
|
||||
```
|
||||
|
||||
## 高性能查询系统
|
||||
|
||||
EntityManager 提供多种查询方式,自动选择最优的查询策略。
|
||||
|
||||
### 基础查询
|
||||
|
||||
```typescript
|
||||
// 通过ID查询(O(1))
|
||||
const entity = entityManager.getEntity(123);
|
||||
|
||||
// 通过名称查询(O(1) 哈希查找)
|
||||
const player = entityManager.getEntityByName("Player");
|
||||
|
||||
// 通过标签查询(O(1) 索引查找)
|
||||
const enemies = entityManager.getEntitiesByTag("enemy");
|
||||
|
||||
// 组件查询(使用O(1)组件索引)
|
||||
const healthEntities = entityManager.getEntitiesWithComponent(HealthComponent);
|
||||
|
||||
// 多组件查询(使用Archetype优化)
|
||||
const movingEntities = entityManager.getEntitiesWithComponents([
|
||||
PositionComponent,
|
||||
VelocityComponent
|
||||
]);
|
||||
```
|
||||
|
||||
### 流式查询API
|
||||
|
||||
EntityManager 提供强大的流式查询构建器:
|
||||
|
||||
```typescript
|
||||
// 基础查询构建
|
||||
const results = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent]) // 必须包含这些组件
|
||||
.withoutTag("dead") // 不能有死亡标签
|
||||
.active(true) // 必须激活
|
||||
.execute();
|
||||
|
||||
// 复杂条件查询
|
||||
const livingEnemies = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent])
|
||||
.withTag("enemy")
|
||||
.withoutTag("dead")
|
||||
.where(entity => {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
return health && health.currentHealth > 0;
|
||||
})
|
||||
.execute();
|
||||
|
||||
// 查询变体
|
||||
const firstEnemy = entityManager
|
||||
.query()
|
||||
.withTag("enemy")
|
||||
.first(); // 获取第一个匹配
|
||||
|
||||
const enemyCount = entityManager
|
||||
.query()
|
||||
.withTag("enemy")
|
||||
.count(); // 获取数量
|
||||
|
||||
// 直接处理查询结果
|
||||
entityManager
|
||||
.query()
|
||||
.withAll([HealthComponent])
|
||||
.forEach(entity => {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health.currentHealth <= 0) {
|
||||
entity.addTag("dead");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 高级查询选项
|
||||
|
||||
```typescript
|
||||
// 组合条件查询
|
||||
const combatUnits = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent]) // AND条件
|
||||
.withAny([WeaponComponent, MagicComponent]) // OR条件
|
||||
.without([DeadComponent]) // NOT条件
|
||||
.withTag("combatant")
|
||||
.withoutTag("peaceful")
|
||||
.active(true)
|
||||
.enabled(true)
|
||||
.execute();
|
||||
|
||||
// 使用自定义过滤器
|
||||
const nearbyEnemies = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent])
|
||||
.withTag("enemy")
|
||||
.where(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(pos.x - playerPos.x, 2) +
|
||||
Math.pow(pos.y - playerPos.y, 2)
|
||||
);
|
||||
return distance < 100; // 距离玩家100像素内
|
||||
})
|
||||
.execute();
|
||||
```
|
||||
|
||||
## 批量操作
|
||||
|
||||
EntityManager 提供高效的批量操作方法:
|
||||
|
||||
```typescript
|
||||
// 遍历所有实体
|
||||
entityManager.forEachEntity(entity => {
|
||||
// 处理每个实体
|
||||
if (entity.position.x < 0) {
|
||||
entity.position.x = 0;
|
||||
}
|
||||
});
|
||||
|
||||
// 遍历特定组件的实体
|
||||
entityManager.forEachEntityWithComponent(HealthComponent, (entity, health) => {
|
||||
if (health.currentHealth <= 0) {
|
||||
entity.addTag("dead");
|
||||
entity.enabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 批量创建并配置实体
|
||||
const bullets = entityManager.createEntities(100, "Bullet", (bullet, index) => {
|
||||
bullet.addComponent(new PositionComponent(
|
||||
100 + index * 10,
|
||||
100
|
||||
));
|
||||
bullet.addComponent(new VelocityComponent(0, -200));
|
||||
bullet.tag = "projectile";
|
||||
});
|
||||
```
|
||||
|
||||
## 性能优化系统
|
||||
|
||||
EntityManager 内置了三个性能优化系统:
|
||||
|
||||
### 1. 组件索引系统
|
||||
|
||||
自动为组件查询提供O(1)性能:
|
||||
|
||||
```typescript
|
||||
// 获取组件索引统计
|
||||
const componentIndex = entityManager.getComponentIndex();
|
||||
const stats = componentIndex.getPerformanceStats();
|
||||
|
||||
console.log('组件索引统计:', {
|
||||
totalQueries: stats.totalQueries,
|
||||
indexHits: stats.indexHits,
|
||||
hitRate: (stats.indexHits / stats.totalQueries * 100).toFixed(2) + '%'
|
||||
});
|
||||
|
||||
// 手动优化(通常自动进行)
|
||||
componentIndex.optimize();
|
||||
```
|
||||
|
||||
### 2. Archetype系统
|
||||
|
||||
按组件组合分组实体,优化批量查询:
|
||||
|
||||
```typescript
|
||||
// 获取Archetype统计
|
||||
const archetypeSystem = entityManager.getArchetypeSystem();
|
||||
const archetypeStats = archetypeSystem.getStatistics();
|
||||
|
||||
console.log('Archetype统计:', {
|
||||
totalArchetypes: archetypeStats.totalArchetypes,
|
||||
totalEntities: archetypeStats.totalEntities,
|
||||
queryCacheSize: archetypeStats.queryCacheSize
|
||||
});
|
||||
|
||||
// 查看所有原型
|
||||
console.log('当前原型:', archetypeSystem.getAllArchetypes());
|
||||
```
|
||||
|
||||
### 3. 脏标记系统
|
||||
|
||||
追踪实体变更,避免不必要的更新:
|
||||
|
||||
```typescript
|
||||
// 获取脏标记统计
|
||||
const dirtyTracking = entityManager.getDirtyTrackingSystem();
|
||||
const dirtyStats = dirtyTracking.getPerformanceStats();
|
||||
|
||||
console.log('脏标记统计:', {
|
||||
totalMarks: dirtyStats.totalMarks,
|
||||
batchesProcessed: dirtyStats.batchesProcessed,
|
||||
listenersNotified: dirtyStats.listenersNotified
|
||||
});
|
||||
|
||||
// 手动处理脏标记
|
||||
dirtyTracking.processDirtyMarks();
|
||||
```
|
||||
|
||||
## 实体管理器统计
|
||||
|
||||
获取EntityManager的综合性能数据:
|
||||
|
||||
```typescript
|
||||
const stats = entityManager.getStatistics();
|
||||
|
||||
console.log('EntityManager统计:', {
|
||||
// 基础统计
|
||||
entityCount: stats.entityCount,
|
||||
activeEntityCount: stats.activeEntityCount,
|
||||
|
||||
// 查询统计
|
||||
totalQueries: stats.totalQueries,
|
||||
indexHits: stats.indexHits,
|
||||
archetypeHits: stats.archetypeHits,
|
||||
|
||||
// 性能指标
|
||||
averageQueryTime: stats.averageQueryTime,
|
||||
hitRate: (stats.indexHits / stats.totalQueries * 100).toFixed(2) + '%'
|
||||
});
|
||||
```
|
||||
|
||||
## 系统优化和清理
|
||||
|
||||
```typescript
|
||||
// 手动触发优化
|
||||
entityManager.optimize();
|
||||
|
||||
// 内存清理
|
||||
entityManager.cleanup();
|
||||
|
||||
// 压缩数据结构
|
||||
entityManager.compact();
|
||||
|
||||
// 获取内存使用情况
|
||||
const memoryStats = entityManager.getMemoryUsage();
|
||||
console.log('内存使用:', {
|
||||
entityIndexSize: memoryStats.entityIndex,
|
||||
componentIndexSize: memoryStats.componentIndex,
|
||||
archetypeSize: memoryStats.archetype
|
||||
});
|
||||
```
|
||||
|
||||
## 实际使用案例
|
||||
|
||||
### 游戏系统集成
|
||||
|
||||
```typescript
|
||||
class MovementSystem extends EntitySystem {
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
super();
|
||||
this.entityManager = new EntityManager(scene);
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
// 使用高效查询获取移动实体
|
||||
const movingEntities = this.entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, VelocityComponent])
|
||||
.active(true)
|
||||
.execute();
|
||||
|
||||
// 批量处理
|
||||
movingEntities.forEach(entity => {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
const velocity = entity.getComponent(VelocityComponent);
|
||||
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 复杂查询示例
|
||||
|
||||
```typescript
|
||||
// 战斗系统:查找攻击范围内的敌人
|
||||
class CombatSystem {
|
||||
private entityManager: EntityManager;
|
||||
|
||||
findTargetsInRange(attacker: Entity, range: number): Entity[] {
|
||||
const attackerPos = attacker.getComponent(PositionComponent);
|
||||
if (!attackerPos) return [];
|
||||
|
||||
return this.entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent])
|
||||
.withTag("enemy")
|
||||
.withoutTag("dead")
|
||||
.where(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(pos.x - attackerPos.x, 2) +
|
||||
Math.pow(pos.y - attackerPos.y, 2)
|
||||
);
|
||||
return distance <= range;
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// 优化版本:使用空间分区(如果实现了的话)
|
||||
findTargetsInRangeOptimized(attacker: Entity, range: number): Entity[] {
|
||||
// 首先通过空间查询缩小范围
|
||||
const nearbyEntities = this.spatialIndex.queryRange(
|
||||
attackerPos.x - range,
|
||||
attackerPos.y - range,
|
||||
attackerPos.x + range,
|
||||
attackerPos.y + range
|
||||
);
|
||||
|
||||
// 然后使用EntityManager进行精确过滤
|
||||
return this.entityManager
|
||||
.query()
|
||||
.withAll([HealthComponent])
|
||||
.withTag("enemy")
|
||||
.withoutTag("dead")
|
||||
.where(entity => nearbyEntities.includes(entity))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能建议
|
||||
|
||||
### 查询优化
|
||||
|
||||
1. **利用索引**: 优先使用组件查询和标签查询,它们具有O(1)性能
|
||||
2. **减少自定义过滤**: `where()`条件虽然灵活,但会降低性能
|
||||
3. **缓存查询结果**: 对于不经常变化的查询结果,考虑缓存
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:使用索引查询
|
||||
const enemies = entityManager.getEntitiesByTag("enemy");
|
||||
|
||||
// ⚠️ 谨慎:自定义过滤
|
||||
const enemies = entityManager
|
||||
.query()
|
||||
.where(entity => entity.name.includes("Enemy"))
|
||||
.execute();
|
||||
```
|
||||
|
||||
### 批量操作优化
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:批量创建
|
||||
const bullets = entityManager.createEntities(100, "Bullet");
|
||||
|
||||
// ❌ 避免:循环单独创建
|
||||
for (let i = 0; i < 100; i++) {
|
||||
entityManager.createEntity("Bullet");
|
||||
}
|
||||
```
|
||||
|
||||
### 内存管理
|
||||
|
||||
```typescript
|
||||
// 定期清理
|
||||
setInterval(() => {
|
||||
entityManager.cleanup();
|
||||
}, 30000); // 每30秒清理一次
|
||||
|
||||
// 监控性能
|
||||
const stats = entityManager.getStatistics();
|
||||
if (stats.indexHits / stats.totalQueries < 0.8) {
|
||||
console.warn('查询命中率较低,考虑优化查询策略');
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
EntityManager 提供了:
|
||||
|
||||
- **统一接口**: 所有实体操作通过一个管理器完成
|
||||
- **自动优化**: 内置三个性能优化系统
|
||||
- **灵活查询**: 从简单的ID查找到复杂的条件查询
|
||||
- **性能监控**: 完整的统计和诊断信息
|
||||
- **批量操作**: 高效的批量处理能力
|
||||
|
||||
通过合理使用EntityManager,您可以构建高性能、可维护的ECS游戏系统。
|
||||
@@ -1,496 +0,0 @@
|
||||
# ECS事件系统使用指南
|
||||
|
||||
本文档介绍如何使用ECS框架的增强事件系统,包括类型安全的事件发布订阅、预定义的ECS事件类型和高级功能。
|
||||
|
||||
## 目录
|
||||
|
||||
1. [基础用法](#基础用法)
|
||||
2. [预定义ECS事件](#预定义ecs事件)
|
||||
3. [事件装饰器](#事件装饰器)
|
||||
4. [高级功能](#高级功能)
|
||||
5. [性能优化](#性能优化)
|
||||
6. [最佳实践](#最佳实践)
|
||||
|
||||
## 基础用法
|
||||
|
||||
### 创建事件总线
|
||||
|
||||
```typescript
|
||||
import { EventBus, GlobalEventBus } from './ECS';
|
||||
|
||||
// 方式1:创建独立的事件总线
|
||||
const eventBus = new EventBus(true); // true启用调试模式
|
||||
|
||||
// 方式2:使用全局事件总线
|
||||
const globalEventBus = GlobalEventBus.getInstance(true);
|
||||
```
|
||||
|
||||
### 基本事件发布订阅
|
||||
|
||||
```typescript
|
||||
// 定义事件数据类型
|
||||
interface PlayerDiedEvent {
|
||||
playerId: number;
|
||||
cause: string;
|
||||
position: { x: number; y: number };
|
||||
}
|
||||
|
||||
// 监听事件
|
||||
const listenerId = eventBus.on<PlayerDiedEvent>('player:died', (data) => {
|
||||
console.log(`Player ${data.playerId} died at (${data.position.x}, ${data.position.y})`);
|
||||
console.log(`Cause: ${data.cause}`);
|
||||
});
|
||||
|
||||
// 发射事件
|
||||
eventBus.emit('player:died', {
|
||||
playerId: 123,
|
||||
cause: 'enemy_attack',
|
||||
position: { x: 100, y: 200 }
|
||||
});
|
||||
|
||||
// 移除监听器
|
||||
eventBus.off('player:died', listenerId);
|
||||
```
|
||||
|
||||
### 一次性事件监听
|
||||
|
||||
```typescript
|
||||
// 只监听一次
|
||||
eventBus.once<PlayerDiedEvent>('player:died', (data) => {
|
||||
console.log('This will only be called once');
|
||||
});
|
||||
```
|
||||
|
||||
### 异步事件处理
|
||||
|
||||
```typescript
|
||||
// 异步事件监听
|
||||
eventBus.onAsync<PlayerDiedEvent>('player:died', async (data) => {
|
||||
await savePlayerDeathToDatabase(data);
|
||||
await updateLeaderboard(data.playerId);
|
||||
});
|
||||
|
||||
// 异步事件发射
|
||||
await eventBus.emitAsync('player:died', playerData);
|
||||
```
|
||||
|
||||
## 预定义ECS事件
|
||||
|
||||
框架提供了完整的ECS事件类型定义,支持实体、组件、系统等核心概念的事件。
|
||||
|
||||
### 实体事件
|
||||
|
||||
```typescript
|
||||
import { ECSEventType, IEntityEventData } from './ECS';
|
||||
|
||||
// 监听实体创建事件
|
||||
eventBus.onEntityCreated((data: IEntityEventData) => {
|
||||
console.log(`Entity created: ${data.entityName} (ID: ${data.entityId})`);
|
||||
});
|
||||
|
||||
// 监听实体销毁事件
|
||||
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_DESTROYED, (data) => {
|
||||
console.log(`Entity destroyed: ${data.entityName}`);
|
||||
});
|
||||
|
||||
// 手动发射实体事件
|
||||
eventBus.emitEntityCreated({
|
||||
timestamp: Date.now(),
|
||||
source: 'GameManager',
|
||||
entityId: 123,
|
||||
entityName: 'Player',
|
||||
entityTag: 'player'
|
||||
});
|
||||
```
|
||||
|
||||
### 组件事件
|
||||
|
||||
```typescript
|
||||
import { IComponentEventData } from './ECS';
|
||||
|
||||
// 监听组件添加事件
|
||||
eventBus.onComponentAdded((data: IComponentEventData) => {
|
||||
console.log(`Component ${data.componentType} added to entity ${data.entityId}`);
|
||||
});
|
||||
|
||||
// 监听组件移除事件
|
||||
eventBus.on<IComponentEventData>(ECSEventType.COMPONENT_REMOVED, (data) => {
|
||||
console.log(`Component ${data.componentType} removed from entity ${data.entityId}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 系统事件
|
||||
|
||||
```typescript
|
||||
import { ISystemEventData } from './ECS';
|
||||
|
||||
// 监听系统错误
|
||||
eventBus.onSystemError((data: ISystemEventData) => {
|
||||
console.error(`System error in ${data.systemName}: ${data.systemType}`);
|
||||
});
|
||||
|
||||
// 监听系统处理开始/结束
|
||||
eventBus.on<ISystemEventData>(ECSEventType.SYSTEM_PROCESSING_START, (data) => {
|
||||
console.log(`System ${data.systemName} started processing`);
|
||||
});
|
||||
```
|
||||
|
||||
### 性能事件
|
||||
|
||||
```typescript
|
||||
import { IPerformanceEventData } from './ECS';
|
||||
|
||||
// 监听性能警告
|
||||
eventBus.onPerformanceWarning((data: IPerformanceEventData) => {
|
||||
console.warn(`Performance warning: ${data.operation} took ${data.executionTime}ms`);
|
||||
});
|
||||
|
||||
// 监听内存使用过高
|
||||
eventBus.on<IPerformanceEventData>(ECSEventType.MEMORY_USAGE_HIGH, (data) => {
|
||||
console.warn(`High memory usage: ${data.memoryUsage}MB`);
|
||||
});
|
||||
```
|
||||
|
||||
## 事件装饰器
|
||||
|
||||
使用装饰器可以自动注册事件监听器,简化代码编写。
|
||||
|
||||
### 基础装饰器
|
||||
|
||||
```typescript
|
||||
import { EventHandler, AsyncEventHandler, EventPriority } from './ECS';
|
||||
|
||||
class GameManager {
|
||||
@EventHandler(ECSEventType.ENTITY_CREATED, { priority: EventPriority.HIGH })
|
||||
onEntityCreated(data: IEntityEventData) {
|
||||
console.log(`New entity: ${data.entityName}`);
|
||||
}
|
||||
|
||||
@AsyncEventHandler(ECSEventType.ENTITY_DESTROYED)
|
||||
async onEntityDestroyed(data: IEntityEventData) {
|
||||
await this.cleanupEntityResources(data.entityId);
|
||||
}
|
||||
|
||||
@EventHandler('custom:game:event', { once: true })
|
||||
onGameStart(data: any) {
|
||||
console.log('Game started!');
|
||||
}
|
||||
|
||||
// 需要手动调用初始化方法
|
||||
constructor() {
|
||||
// 如果类有initEventListeners方法,会自动注册装饰器定义的监听器
|
||||
if (this.initEventListeners) {
|
||||
this.initEventListeners();
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanupEntityResources(entityId: number) {
|
||||
// 清理实体相关资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 优先级和配置
|
||||
|
||||
```typescript
|
||||
class SystemManager {
|
||||
@EventHandler(ECSEventType.SYSTEM_ERROR, {
|
||||
priority: EventPriority.CRITICAL,
|
||||
context: this
|
||||
})
|
||||
handleSystemError(data: ISystemEventData) {
|
||||
this.logError(data);
|
||||
this.restartSystem(data.systemName);
|
||||
}
|
||||
|
||||
@AsyncEventHandler(ECSEventType.PERFORMANCE_WARNING, {
|
||||
priority: EventPriority.LOW,
|
||||
async: true
|
||||
})
|
||||
async handlePerformanceWarning(data: IPerformanceEventData) {
|
||||
await this.optimizePerformance(data);
|
||||
}
|
||||
|
||||
private logError(data: ISystemEventData) {
|
||||
// 错误日志记录
|
||||
}
|
||||
|
||||
private restartSystem(systemName: string) {
|
||||
// 重启系统
|
||||
}
|
||||
|
||||
private async optimizePerformance(data: IPerformanceEventData) {
|
||||
// 性能优化逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 事件批处理
|
||||
|
||||
```typescript
|
||||
// 设置批处理配置
|
||||
eventBus.setBatchConfig('entity:update', 100, 16); // 批量100个,延迟16ms
|
||||
|
||||
// 发射事件(会被批处理)
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
eventBus.emit('entity:update', { entityId: i, data: 'update' });
|
||||
}
|
||||
|
||||
// 手动刷新批处理队列
|
||||
eventBus.flushBatch('entity:update');
|
||||
```
|
||||
|
||||
### 事件统计和监控
|
||||
|
||||
```typescript
|
||||
// 获取单个事件统计
|
||||
const stats = eventBus.getStats('entity:created');
|
||||
console.log(`Event triggered ${stats.triggerCount} times`);
|
||||
console.log(`Average execution time: ${stats.averageExecutionTime}ms`);
|
||||
|
||||
// 获取所有事件统计
|
||||
const allStats = eventBus.getStats();
|
||||
if (allStats instanceof Map) {
|
||||
allStats.forEach((stat, eventType) => {
|
||||
console.log(`${eventType}: ${stat.triggerCount} triggers`);
|
||||
});
|
||||
}
|
||||
|
||||
// 重置统计
|
||||
eventBus.resetStats('entity:created');
|
||||
```
|
||||
|
||||
### 事件类型验证
|
||||
|
||||
```typescript
|
||||
import { EventTypeValidator } from './ECS';
|
||||
|
||||
// 检查事件类型是否有效
|
||||
if (EventTypeValidator.isValid('entity:created')) {
|
||||
eventBus.emit('entity:created', data);
|
||||
}
|
||||
|
||||
// 添加自定义事件类型
|
||||
EventTypeValidator.addCustomType('game:custom:event');
|
||||
|
||||
// 获取所有有效事件类型
|
||||
const validTypes = EventTypeValidator.getAllValidTypes();
|
||||
console.log('Valid event types:', validTypes);
|
||||
```
|
||||
|
||||
### 调试和日志
|
||||
|
||||
```typescript
|
||||
// 启用调试模式
|
||||
eventBus.setDebugMode(true);
|
||||
|
||||
// 设置最大监听器数量
|
||||
eventBus.setMaxListeners(50);
|
||||
|
||||
// 检查是否有监听器
|
||||
if (eventBus.hasListeners('entity:created')) {
|
||||
console.log('Has listeners for entity:created');
|
||||
}
|
||||
|
||||
// 获取监听器数量
|
||||
const count = eventBus.getListenerCount('entity:created');
|
||||
console.log(`${count} listeners for entity:created`);
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 事件过滤和条件监听
|
||||
|
||||
```typescript
|
||||
// 使用条件过滤减少不必要的事件处理
|
||||
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_CREATED, (data) => {
|
||||
// 只处理玩家实体
|
||||
if (data.entityTag === 'player') {
|
||||
handlePlayerCreated(data);
|
||||
}
|
||||
});
|
||||
|
||||
// 更好的方式:使用具体的事件类型
|
||||
eventBus.on<IEntityEventData>('entity:player:created', handlePlayerCreated);
|
||||
```
|
||||
|
||||
### 内存管理
|
||||
|
||||
```typescript
|
||||
class EventManager {
|
||||
private listeners: string[] = [];
|
||||
|
||||
public setupListeners() {
|
||||
// 保存监听器ID以便清理
|
||||
this.listeners.push(
|
||||
eventBus.on('event1', this.handler1.bind(this)),
|
||||
eventBus.on('event2', this.handler2.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
public cleanup() {
|
||||
// 清理所有监听器
|
||||
this.listeners.forEach(id => {
|
||||
eventBus.off('event1', id);
|
||||
eventBus.off('event2', id);
|
||||
});
|
||||
this.listeners.length = 0;
|
||||
}
|
||||
|
||||
private handler1(data: any) { /* ... */ }
|
||||
private handler2(data: any) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### 异步事件优化
|
||||
|
||||
```typescript
|
||||
// 使用Promise.all并行处理多个异步事件
|
||||
const promises = [
|
||||
eventBus.emitAsync('save:player', playerData),
|
||||
eventBus.emitAsync('update:leaderboard', scoreData),
|
||||
eventBus.emitAsync('notify:friends', notificationData)
|
||||
];
|
||||
|
||||
await Promise.all(promises);
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 事件命名规范
|
||||
|
||||
```typescript
|
||||
// 推荐的事件命名格式:模块:对象:动作
|
||||
const EVENT_NAMES = {
|
||||
// 实体相关
|
||||
ENTITY_PLAYER_CREATED: 'entity:player:created',
|
||||
ENTITY_ENEMY_DESTROYED: 'entity:enemy:destroyed',
|
||||
|
||||
// 游戏逻辑相关
|
||||
GAME_LEVEL_COMPLETED: 'game:level:completed',
|
||||
GAME_SCORE_UPDATED: 'game:score:updated',
|
||||
|
||||
// UI相关
|
||||
UI_MENU_OPENED: 'ui:menu:opened',
|
||||
UI_BUTTON_CLICKED: 'ui:button:clicked'
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 类型安全
|
||||
|
||||
```typescript
|
||||
// 定义强类型的事件数据接口
|
||||
interface GameEvents {
|
||||
'player:levelup': { playerId: number; newLevel: number; experience: number };
|
||||
'inventory:item:added': { itemId: string; quantity: number; playerId: number };
|
||||
'combat:damage:dealt': { attackerId: number; targetId: number; damage: number };
|
||||
}
|
||||
|
||||
// 创建类型安全的事件发射器
|
||||
class TypedEventBus {
|
||||
private eventBus = new EventBus();
|
||||
|
||||
emit<K extends keyof GameEvents>(eventType: K, data: GameEvents[K]) {
|
||||
this.eventBus.emit(eventType, data);
|
||||
}
|
||||
|
||||
on<K extends keyof GameEvents>(
|
||||
eventType: K,
|
||||
handler: (data: GameEvents[K]) => void
|
||||
) {
|
||||
return this.eventBus.on(eventType, handler);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
```typescript
|
||||
// 在事件处理器中添加错误处理
|
||||
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_CREATED, (data) => {
|
||||
try {
|
||||
processEntityCreation(data);
|
||||
} catch (error) {
|
||||
console.error('Error processing entity creation:', error);
|
||||
// 发射错误事件
|
||||
eventBus.emit(ECSEventType.ERROR_OCCURRED, {
|
||||
timestamp: Date.now(),
|
||||
source: 'EntityCreationHandler',
|
||||
error: error.message,
|
||||
context: data
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 模块化事件管理
|
||||
|
||||
```typescript
|
||||
// 为不同模块创建专门的事件管理器
|
||||
class PlayerEventManager {
|
||||
constructor(private eventBus: EventBus) {
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
private setupListeners() {
|
||||
this.eventBus.onEntityCreated(this.onPlayerCreated.bind(this));
|
||||
this.eventBus.on('player:levelup', this.onPlayerLevelUp.bind(this));
|
||||
this.eventBus.on('player:died', this.onPlayerDied.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerCreated(data: IEntityEventData) {
|
||||
if (data.entityTag === 'player') {
|
||||
// 处理玩家创建逻辑
|
||||
}
|
||||
}
|
||||
|
||||
private onPlayerLevelUp(data: any) {
|
||||
// 处理玩家升级逻辑
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any) {
|
||||
// 处理玩家死亡逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 与EntityManager集成
|
||||
|
||||
```typescript
|
||||
import { EntityManager } from './ECS';
|
||||
|
||||
// EntityManager会自动设置事件总线
|
||||
const entityManager = new EntityManager();
|
||||
|
||||
// 获取事件总线实例
|
||||
const eventBus = entityManager.eventBus;
|
||||
|
||||
// 监听自动发射的ECS事件
|
||||
eventBus.onEntityCreated((data) => {
|
||||
console.log('Entity created automatically:', data);
|
||||
});
|
||||
|
||||
eventBus.onComponentAdded((data) => {
|
||||
console.log('Component added automatically:', data);
|
||||
});
|
||||
|
||||
// 创建实体时会自动发射事件
|
||||
const entity = entityManager.createEntity('Player');
|
||||
|
||||
// 添加组件时会自动发射事件
|
||||
entity.addComponent(new HealthComponent(100));
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
ECS框架的事件系统提供了:
|
||||
|
||||
- **类型安全**:完整的TypeScript类型支持
|
||||
- **高性能**:批处理、缓存和优化机制
|
||||
- **易用性**:装饰器、预定义事件类型
|
||||
- **可扩展**:自定义事件类型和验证
|
||||
- **调试友好**:详细的统计信息和调试模式
|
||||
|
||||
通过合理使用事件系统,可以实现松耦合的模块化架构,提高代码的可维护性和扩展性。
|
||||
@@ -1,549 +0,0 @@
|
||||
# 快速入门
|
||||
|
||||
本指南将帮助您快速上手 ECS Framework,这是一个专业级的实体组件系统框架,采用现代化架构设计,专为高性能游戏开发打造。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
ecs-framework/
|
||||
├── source/
|
||||
│ ├── src/ # 源代码
|
||||
│ │ ├── ECS/ # ECS核心系统
|
||||
│ │ ├── Types/ # 类型定义
|
||||
│ │ ├── Utils/ # 工具类
|
||||
│ │ └── Testing/ # 测试文件
|
||||
│ ├── scripts/ # 构建脚本
|
||||
│ └── tsconfig.json # TypeScript配置
|
||||
└── docs/ # 文档
|
||||
```
|
||||
|
||||
## 安装和使用
|
||||
|
||||
### NPM 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
### 从源码构建
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/esengine/ecs-framework.git
|
||||
|
||||
# 进入源码目录
|
||||
cd ecs-framework/source
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 编译TypeScript
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 基础设置
|
||||
|
||||
### 1. 导入框架
|
||||
|
||||
```typescript
|
||||
// 导入核心类
|
||||
import {
|
||||
Core,
|
||||
Entity,
|
||||
Component,
|
||||
Scene,
|
||||
EntitySystem,
|
||||
EntityManager,
|
||||
ComponentIndexManager,
|
||||
ArchetypeSystem,
|
||||
DirtyTrackingSystem
|
||||
} from '@esengine/ecs-framework';
|
||||
```
|
||||
|
||||
### 2. 创建基础管理器
|
||||
|
||||
```typescript
|
||||
class GameManager {
|
||||
private core: Core;
|
||||
private scene: Scene;
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor() {
|
||||
// 创建核心实例
|
||||
this.core = Core.create(true);
|
||||
|
||||
// 创建场景
|
||||
this.scene = new Scene();
|
||||
this.scene.name = "GameScene";
|
||||
|
||||
// 设置当前场景
|
||||
Core.scene = this.scene;
|
||||
|
||||
// 初始化实体管理器
|
||||
this.entityManager = new EntityManager(this.scene);
|
||||
|
||||
// 初始化性能优化
|
||||
this.setupPerformanceOptimizations();
|
||||
}
|
||||
|
||||
private setupPerformanceOptimizations() {
|
||||
// 启用组件索引(自动优化查询性能)
|
||||
// EntityManager内部已自动启用
|
||||
|
||||
// 可选:手动配置优化系统
|
||||
const componentIndex = this.entityManager.getComponentIndex();
|
||||
const archetypeSystem = this.entityManager.getArchetypeSystem();
|
||||
const dirtyTracking = this.entityManager.getDirtyTrackingSystem();
|
||||
|
||||
// 优化系统会自动工作,通常无需手动配置
|
||||
}
|
||||
|
||||
public update(deltaTime: number): void {
|
||||
// 更新场景
|
||||
this.scene.update();
|
||||
|
||||
// 处理系统逻辑
|
||||
this.updateSystems(deltaTime);
|
||||
}
|
||||
|
||||
private updateSystems(deltaTime: number): void {
|
||||
// 在这里添加您的系统更新逻辑
|
||||
}
|
||||
|
||||
// 提供实体管理器访问
|
||||
public getEntityManager(): EntityManager {
|
||||
return this.entityManager;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 游戏循环
|
||||
|
||||
```typescript
|
||||
const gameManager = new GameManager();
|
||||
let lastTime = performance.now();
|
||||
|
||||
function gameLoop() {
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - lastTime) / 1000; // 转换为秒
|
||||
lastTime = currentTime;
|
||||
|
||||
gameManager.update(deltaTime);
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// 启动游戏循环
|
||||
gameLoop();
|
||||
```
|
||||
|
||||
## 创建实体和组件
|
||||
|
||||
### 1. 定义组件
|
||||
|
||||
```typescript
|
||||
import { Component, ComponentPoolManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 位置组件
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
// 对象池重置方法
|
||||
public reset() {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 速度组件
|
||||
class VelocityComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 生命值组件
|
||||
class HealthComponent extends Component {
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
|
||||
constructor(maxHealth: number = 100) {
|
||||
super();
|
||||
this.maxHealth = maxHealth;
|
||||
this.currentHealth = maxHealth;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.maxHealth = 100;
|
||||
this.currentHealth = 100;
|
||||
}
|
||||
|
||||
public takeDamage(damage: number): void {
|
||||
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||
}
|
||||
|
||||
public heal(amount: number): void {
|
||||
this.currentHealth = Math.min(this.maxHealth, this.currentHealth + amount);
|
||||
}
|
||||
|
||||
public isDead(): boolean {
|
||||
return this.currentHealth <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 简单的组件定义
|
||||
// 注:框架会自动优化组件的存储和查询
|
||||
```
|
||||
|
||||
## 使用 EntityManager
|
||||
|
||||
EntityManager 是框架的核心功能,提供统一的实体管理和高性能查询接口。
|
||||
|
||||
### 1. 基础用法
|
||||
|
||||
```typescript
|
||||
// 获取EntityManager实例(在GameManager中已创建)
|
||||
const entityManager = gameManager.getEntityManager();
|
||||
|
||||
// 创建单个实体
|
||||
const player = entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(100, 100));
|
||||
player.addComponent(new VelocityComponent(50, 0));
|
||||
|
||||
// 批量创建实体
|
||||
const enemies = entityManager.createEntities(50, "Enemy");
|
||||
enemies.forEach((enemy, index) => {
|
||||
enemy.addComponent(new PositionComponent(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new HealthComponent(30));
|
||||
enemy.tag = "enemy";
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 高性能查询
|
||||
|
||||
```typescript
|
||||
// 流式查询API - 支持复杂查询条件
|
||||
const movingEntities = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, VelocityComponent])
|
||||
.withoutTag("dead")
|
||||
.active(true)
|
||||
.execute();
|
||||
|
||||
// 快速组件查询(使用O(1)索引)
|
||||
const healthEntities = entityManager.getEntitiesWithComponent(HealthComponent);
|
||||
|
||||
// 标签查询
|
||||
const allEnemies = entityManager.getEntitiesByTag("enemy");
|
||||
|
||||
// 名称查询
|
||||
const specificEnemy = entityManager.getEntityByName("BossEnemy");
|
||||
|
||||
// 复合查询
|
||||
const livingEnemies = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent])
|
||||
.withTag("enemy")
|
||||
.withoutTag("dead")
|
||||
.where(entity => {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
return health && health.currentHealth > 0;
|
||||
})
|
||||
.execute();
|
||||
```
|
||||
|
||||
### 3. 批量操作
|
||||
|
||||
```typescript
|
||||
// 批量处理实体
|
||||
entityManager.forEachEntity(entity => {
|
||||
// 处理所有实体
|
||||
if (entity.tag === "bullet" && entity.position.y < 0) {
|
||||
entity.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// 批量处理特定组件的实体
|
||||
entityManager.forEachEntityWithComponent(HealthComponent, (entity, health) => {
|
||||
if (health.currentHealth <= 0) {
|
||||
entity.addTag("dead");
|
||||
entity.enabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 获取统计信息
|
||||
const stats = entityManager.getStatistics();
|
||||
console.log(`总实体数: ${stats.entityCount}`);
|
||||
console.log(`索引命中率: ${stats.indexHits}/${stats.totalQueries}`);
|
||||
```
|
||||
|
||||
### 4. 性能优化功能
|
||||
|
||||
```typescript
|
||||
// 获取性能优化系统
|
||||
const componentIndex = entityManager.getComponentIndex();
|
||||
const archetypeSystem = entityManager.getArchetypeSystem();
|
||||
const dirtyTracking = entityManager.getDirtyTrackingSystem();
|
||||
|
||||
// 查看性能统计
|
||||
console.log('组件索引统计:', componentIndex.getPerformanceStats());
|
||||
console.log('Archetype统计:', archetypeSystem.getStatistics());
|
||||
console.log('脏标记统计:', dirtyTracking.getPerformanceStats());
|
||||
|
||||
// 手动优化(通常自动进行)
|
||||
entityManager.optimize();
|
||||
|
||||
// 内存清理
|
||||
entityManager.cleanup();
|
||||
```
|
||||
|
||||
## 创建系统
|
||||
|
||||
系统处理具有特定组件的实体集合,实现游戏逻辑。
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
protected process(entities: Entity[]): void {
|
||||
// 使用EntityManager进行高效查询
|
||||
const entityManager = new EntityManager(this.scene);
|
||||
const movingEntities = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, VelocityComponent])
|
||||
.execute();
|
||||
|
||||
movingEntities.forEach(entity => {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
const velocity = entity.getComponent(VelocityComponent);
|
||||
|
||||
if (position && velocity) {
|
||||
// 更新位置
|
||||
position.x += velocity.x * 0.016; // 假设60FPS
|
||||
position.y += velocity.y * 0.016;
|
||||
|
||||
// 边界检查
|
||||
if (position.x < 0 || position.x > 800) {
|
||||
velocity.x = -velocity.x;
|
||||
}
|
||||
if (position.y < 0 || position.y > 600) {
|
||||
velocity.y = -velocity.y;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class HealthSystem extends EntitySystem {
|
||||
protected process(entities: Entity[]): void {
|
||||
const entityManager = new EntityManager(this.scene);
|
||||
|
||||
// 查找所有有生命值的实体
|
||||
entityManager.forEachEntityWithComponent(HealthComponent, (entity, health) => {
|
||||
if (health.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加系统到场景
|
||||
gameManager.scene.addEntityProcessor(new MovementSystem());
|
||||
gameManager.scene.addEntityProcessor(new HealthSystem());
|
||||
```
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 事件系统
|
||||
|
||||
```typescript
|
||||
import { Core, CoreEvents } from '@esengine/ecs-framework';
|
||||
|
||||
// 监听框架事件
|
||||
Core.emitter.addObserver(CoreEvents.frameUpdated, this.onFrameUpdate, this);
|
||||
|
||||
// 发射自定义事件
|
||||
Core.emitter.emit("playerDied", { player: entity, score: 1000 });
|
||||
|
||||
// 移除监听
|
||||
Core.emitter.removeObserver(CoreEvents.frameUpdated, this.onFrameUpdate);
|
||||
```
|
||||
|
||||
### 性能监控
|
||||
|
||||
```typescript
|
||||
// 获取EntityManager性能统计
|
||||
const stats = entityManager.getStatistics();
|
||||
console.log(`总实体数: ${stats.entityCount}`);
|
||||
console.log(`索引命中率: ${stats.indexHits}/${stats.totalQueries}`);
|
||||
|
||||
// 获取各优化系统的统计
|
||||
console.log('组件索引:', entityManager.getComponentIndex().getPerformanceStats());
|
||||
console.log('Archetype:', entityManager.getArchetypeSystem().getStatistics());
|
||||
console.log('脏标记:', entityManager.getDirtyTrackingSystem().getPerformanceStats());
|
||||
```
|
||||
|
||||
## 简单示例
|
||||
|
||||
以下是一个完整的示例,展示了框架的主要功能:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Core,
|
||||
Entity,
|
||||
Component,
|
||||
Scene,
|
||||
EntitySystem,
|
||||
EntityManager
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// 游戏管理器
|
||||
class SimpleGame {
|
||||
private core: Core;
|
||||
private scene: Scene;
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor() {
|
||||
this.core = Core.create(true);
|
||||
this.scene = new Scene();
|
||||
this.scene.name = "GameScene";
|
||||
Core.scene = this.scene;
|
||||
|
||||
this.entityManager = new EntityManager(this.scene);
|
||||
this.setupSystems();
|
||||
}
|
||||
|
||||
private setupSystems(): void {
|
||||
this.scene.addEntityProcessor(new MovementSystem());
|
||||
this.scene.addEntityProcessor(new HealthSystem());
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
// 创建游戏实体
|
||||
this.createPlayer();
|
||||
this.createEnemies(50);
|
||||
|
||||
// 启动游戏循环
|
||||
this.gameLoop();
|
||||
}
|
||||
|
||||
private createPlayer(): Entity {
|
||||
const player = this.entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(400, 300));
|
||||
player.addComponent(new VelocityComponent(0, 0));
|
||||
player.addComponent(new HealthComponent(100));
|
||||
player.tag = "player";
|
||||
return player;
|
||||
}
|
||||
|
||||
private createEnemies(count: number): Entity[] {
|
||||
const enemies = this.entityManager.createEntities(count, "Enemy");
|
||||
|
||||
enemies.forEach((enemy, index) => {
|
||||
enemy.addComponent(new PositionComponent(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new VelocityComponent(
|
||||
(Math.random() - 0.5) * 100,
|
||||
(Math.random() - 0.5) * 100
|
||||
));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
enemy.tag = "enemy";
|
||||
});
|
||||
|
||||
return enemies;
|
||||
}
|
||||
|
||||
private gameLoop(): void {
|
||||
const update = () => {
|
||||
// 更新场景
|
||||
this.scene.update();
|
||||
requestAnimationFrame(update);
|
||||
};
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
// 启动游戏
|
||||
const game = new SimpleGame();
|
||||
game.start();
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 大规模实体处理
|
||||
- 使用 `EntityManager.createEntities()` 批量创建实体
|
||||
- 利用组件索引系统进行高效查询
|
||||
- 启用Archetype系统减少查询遍历
|
||||
|
||||
### 2. 查询优化
|
||||
- 使用 `EntityManager.query()` 流式API构建复杂查询
|
||||
- 缓存频繁查询的结果
|
||||
- 利用脏标记系统避免不必要的更新
|
||||
|
||||
### 3. 性能监控
|
||||
- 定期检查 `EntityManager.getStatistics()` 获取性能数据
|
||||
- 监控组件索引命中率
|
||||
- 使用框架提供的性能统计功能
|
||||
|
||||
## 下一步
|
||||
|
||||
现在您已经掌握了 ECS Framework 的基础用法,可以继续学习:
|
||||
|
||||
- [EntityManager 使用指南](entity-manager-example.md) - 详细了解实体管理器的高级功能
|
||||
- [性能优化指南](performance-optimization.md) - 深入了解三大性能优化系统
|
||||
- [核心概念](core-concepts.md) - 深入了解 ECS 架构和设计原理
|
||||
- [查询系统使用指南](query-system-usage.md) - 学习高性能查询系统的详细用法
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何在不同游戏引擎中集成?
|
||||
|
||||
A: ECS Framework 是引擎无关的,您只需要:
|
||||
1. 通过npm安装框架 `npm install @esengine/ecs-framework`
|
||||
2. 在游戏引擎的主循环中调用 `scene.update()`
|
||||
3. 根据需要集成渲染、输入等引擎特定功能
|
||||
|
||||
### Q: 如何处理输入?
|
||||
|
||||
A: 框架本身不提供输入处理,建议:
|
||||
1. 创建一个输入组件来存储输入状态
|
||||
2. 在游戏引擎的输入回调中更新输入组件
|
||||
3. 创建输入处理系统来响应输入状态
|
||||
|
||||
### Q: 如何优化大规模实体性能?
|
||||
|
||||
A: 关键优化策略:
|
||||
1. 使用 `EntityManager` 的高级查询功能
|
||||
2. 启用组件索引系统进行快速查询
|
||||
3. 利用Archetype系统减少查询遍历
|
||||
4. 使用脏标记系统避免不必要的更新
|
||||
|
||||
### Q: EntityManager 有什么优势?
|
||||
|
||||
A: EntityManager 提供了:
|
||||
- O(1) 复杂度的组件查询(使用索引)
|
||||
- 流式API的复杂查询构建
|
||||
- 自动的性能优化系统集成
|
||||
- 统一的实体管理接口
|
||||
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,497 +0,0 @@
|
||||
# 性能优化指南
|
||||
|
||||
ECS Framework 提供了多层性能优化系统,确保在各种规模的游戏中都能提供卓越的性能表现。
|
||||
|
||||
## 性能优化架构
|
||||
|
||||
### 三大核心优化系统
|
||||
|
||||
1. **组件索引系统 (ComponentIndex)** - 提供 O(1) 组件查询性能
|
||||
2. **Archetype系统** - 按组件组合分组实体,减少查询遍历
|
||||
3. **脏标记系统 (DirtyTracking)** - 细粒度变更追踪,避免不必要更新
|
||||
|
||||
这三个系统协同工作,为不同场景提供最优的性能表现。
|
||||
|
||||
## 性能基准
|
||||
|
||||
### 核心操作性能
|
||||
|
||||
```
|
||||
实体创建: 640,000+ 个/秒
|
||||
组件查询: O(1) 复杂度(使用索引)
|
||||
内存优化: 30-50% 减少分配
|
||||
批量操作: 显著提升处理效率
|
||||
```
|
||||
|
||||
### 查询性能对比
|
||||
|
||||
| 查询类型 | 传统方式 | 使用索引 | 性能提升 |
|
||||
|----------|----------|----------|----------|
|
||||
| 单组件查询 | O(n) | O(1) | 1000x+ |
|
||||
| 多组件查询 | O(n*m) | O(k) | 100x+ |
|
||||
| 标签查询 | O(n) | O(1) | 1000x+ |
|
||||
| 复合查询 | O(n*m*k) | O(min(k1,k2)) | 500x+ |
|
||||
|
||||
*n=实体数量, m=组件种类, k=匹配实体数量*
|
||||
|
||||
## 组件索引系统
|
||||
|
||||
### 索引类型选择
|
||||
|
||||
框架提供两种索引实现:
|
||||
|
||||
#### 哈希索引 (HashComponentIndex)
|
||||
- **适用场景**: 通用查询,平衡的读写性能
|
||||
- **优势**: O(1) 查询,较低内存开销
|
||||
- **缺点**: 哈希冲突时性能下降
|
||||
|
||||
```typescript
|
||||
// 自动选择最优索引类型
|
||||
const componentIndex = entityManager.getComponentIndex();
|
||||
|
||||
// 手动配置哈希索引
|
||||
componentIndex.setIndexType(HealthComponent, 'hash');
|
||||
```
|
||||
|
||||
#### 位图索引 (BitmapComponentIndex)
|
||||
- **适用场景**: 大规模实体,频繁的组合查询
|
||||
- **优势**: 超快的 AND/OR 操作,空间压缩
|
||||
- **缺点**: 更新成本较高,内存开销随实体数量增长
|
||||
|
||||
```typescript
|
||||
// 配置位图索引用于大规模查询
|
||||
componentIndex.setIndexType(PositionComponent, 'bitmap');
|
||||
```
|
||||
|
||||
### 智能索引管理
|
||||
|
||||
ComponentIndexManager 会根据使用模式自动优化:
|
||||
|
||||
```typescript
|
||||
// 获取索引性能统计
|
||||
const stats = componentIndex.getPerformanceStats();
|
||||
console.log('索引性能:', {
|
||||
queriesPerSecond: stats.queriesPerSecond,
|
||||
hitRate: stats.hitRate,
|
||||
indexType: stats.recommendedType
|
||||
});
|
||||
|
||||
// 自动优化索引类型
|
||||
componentIndex.optimize(); // 根据使用模式切换索引类型
|
||||
```
|
||||
|
||||
## Archetype系统优化
|
||||
|
||||
### 原型分组策略
|
||||
|
||||
Archetype系统将实体按组件组合分组,实现快速批量操作:
|
||||
|
||||
```typescript
|
||||
// 获取Archetype统计
|
||||
const archetypeSystem = entityManager.getArchetypeSystem();
|
||||
const stats = archetypeSystem.getStatistics();
|
||||
|
||||
console.log('Archetype优化:', {
|
||||
totalArchetypes: stats.totalArchetypes, // 原型数量
|
||||
avgEntitiesPerArchetype: stats.averageEntitiesPerArchetype,
|
||||
queryCacheHits: stats.queryCacheHits // 缓存命中次数
|
||||
});
|
||||
```
|
||||
|
||||
### 查询缓存机制
|
||||
|
||||
```typescript
|
||||
// 启用查询缓存(默认开启)
|
||||
archetypeSystem.enableQueryCache(true);
|
||||
|
||||
// 缓存大小限制(避免内存泄漏)
|
||||
archetypeSystem.setMaxCacheSize(1000);
|
||||
|
||||
// 清理过期缓存
|
||||
archetypeSystem.cleanCache();
|
||||
```
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **组件设计**: 避免创建过多单独的原型
|
||||
2. **批量操作**: 利用原型批量处理相同组件组合的实体
|
||||
3. **缓存管理**: 定期清理查询缓存
|
||||
|
||||
```typescript
|
||||
// ✅ 好的设计:复用组件组合
|
||||
class MovementSystem extends EntitySystem {
|
||||
process() {
|
||||
// 一次查询处理所有移动实体
|
||||
const movingEntities = this.entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, VelocityComponent])
|
||||
.execute(); // 利用Archetype快速获取
|
||||
|
||||
// 批量处理
|
||||
movingEntities.forEach(entity => {
|
||||
// 更新逻辑
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 避免:频繁查询不同组合
|
||||
class BadSystem extends EntitySystem {
|
||||
process() {
|
||||
// 多次小查询,无法充分利用Archetype
|
||||
const players = this.queryPlayers();
|
||||
const enemies = this.queryEnemies();
|
||||
const bullets = this.queryBullets();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 脏标记系统优化
|
||||
|
||||
### 脏标记类型
|
||||
|
||||
系统提供细粒度的脏标记追踪:
|
||||
|
||||
```typescript
|
||||
enum DirtyType {
|
||||
COMPONENT_ADDED, // 组件添加
|
||||
COMPONENT_REMOVED, // 组件移除
|
||||
COMPONENT_MODIFIED, // 组件修改
|
||||
ENTITY_ENABLED, // 实体启用
|
||||
ENTITY_DISABLED, // 实体禁用
|
||||
TAG_ADDED, // 标签添加
|
||||
TAG_REMOVED // 标签移除
|
||||
}
|
||||
```
|
||||
|
||||
### 批量处理配置
|
||||
|
||||
```typescript
|
||||
const dirtyTracking = entityManager.getDirtyTrackingSystem();
|
||||
|
||||
// 配置批量处理参数
|
||||
dirtyTracking.configure({
|
||||
batchSize: 100, // 每批处理100个脏标记
|
||||
timeSliceMs: 16, // 每帧最多处理16ms
|
||||
processingInterval: 1 // 每帧处理一次
|
||||
});
|
||||
|
||||
// 监听脏标记事件
|
||||
dirtyTracking.addListener(DirtyType.COMPONENT_MODIFIED, (entity, component) => {
|
||||
// 响应组件修改
|
||||
this.invalidateRenderCache(entity);
|
||||
}, { priority: 10 });
|
||||
```
|
||||
|
||||
### 性能监控
|
||||
|
||||
```typescript
|
||||
const dirtyStats = dirtyTracking.getPerformanceStats();
|
||||
console.log('脏标记性能:', {
|
||||
totalMarks: dirtyStats.totalMarks,
|
||||
batchesProcessed: dirtyStats.batchesProcessed,
|
||||
averageBatchTime: dirtyStats.averageBatchTime,
|
||||
queueSize: dirtyStats.currentQueueSize
|
||||
});
|
||||
```
|
||||
|
||||
## 查询优化策略
|
||||
|
||||
### 查询层次选择
|
||||
|
||||
根据查询复杂度选择最优方法:
|
||||
|
||||
```typescript
|
||||
// 1. 简单查询:直接使用索引
|
||||
const healthEntities = entityManager.getEntitiesWithComponent(HealthComponent);
|
||||
|
||||
// 2. 双组件查询:使用Archetype
|
||||
const movingEntities = entityManager.getEntitiesWithComponents([
|
||||
PositionComponent,
|
||||
VelocityComponent
|
||||
]);
|
||||
|
||||
// 3. 复杂查询:组合使用
|
||||
const combatants = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent]) // Archetype预筛选
|
||||
.withTag("combat") // 索引过滤
|
||||
.where(entity => { // 自定义精确过滤
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
return health.currentHealth > health.maxHealth * 0.3;
|
||||
})
|
||||
.execute();
|
||||
```
|
||||
|
||||
### 查询缓存策略
|
||||
|
||||
```typescript
|
||||
class CombatSystem extends EntitySystem {
|
||||
private cachedEnemies: Entity[] = [];
|
||||
private lastEnemyCacheUpdate = 0;
|
||||
|
||||
process() {
|
||||
const currentTime = performance.now();
|
||||
|
||||
// 每200ms更新一次敌人缓存
|
||||
if (currentTime - this.lastEnemyCacheUpdate > 200) {
|
||||
this.cachedEnemies = this.entityManager
|
||||
.getEntitiesByTag("enemy");
|
||||
this.lastEnemyCacheUpdate = currentTime;
|
||||
}
|
||||
|
||||
// 使用缓存的结果
|
||||
this.processCombat(this.cachedEnemies);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 内存优化
|
||||
|
||||
### 内存使用监控
|
||||
|
||||
```typescript
|
||||
// 获取各系统内存使用情况
|
||||
const memoryStats = entityManager.getMemoryUsage();
|
||||
console.log('内存使用情况:', {
|
||||
entityIndex: memoryStats.entityIndex, // 实体索引
|
||||
componentIndex: memoryStats.componentIndex, // 组件索引
|
||||
archetype: memoryStats.archetype, // 原型系统
|
||||
dirtyTracking: memoryStats.dirtyTracking, // 脏标记
|
||||
total: memoryStats.total
|
||||
});
|
||||
```
|
||||
|
||||
### 内存清理策略
|
||||
|
||||
```typescript
|
||||
// 定期内存清理
|
||||
setInterval(() => {
|
||||
entityManager.cleanup(); // 清理无效引用
|
||||
entityManager.compact(); // 压缩数据结构
|
||||
}, 30000); // 每30秒清理一次
|
||||
|
||||
// 游戏场景切换时的深度清理
|
||||
function switchScene() {
|
||||
entityManager.destroyAllEntities();
|
||||
entityManager.cleanup();
|
||||
entityManager.compact();
|
||||
|
||||
// 重置优化系统
|
||||
entityManager.getComponentIndex().reset();
|
||||
entityManager.getArchetypeSystem().clearCache();
|
||||
entityManager.getDirtyTrackingSystem().clear();
|
||||
}
|
||||
```
|
||||
|
||||
## 实战优化案例
|
||||
|
||||
### 大规模射击游戏优化
|
||||
|
||||
```typescript
|
||||
class BulletSystem extends EntitySystem {
|
||||
private bulletPool: Entity[] = [];
|
||||
private maxBullets = 1000;
|
||||
|
||||
constructor(entityManager: EntityManager) {
|
||||
super();
|
||||
this.prewarmBulletPool();
|
||||
}
|
||||
|
||||
private prewarmBulletPool() {
|
||||
// 预创建子弹池
|
||||
this.bulletPool = this.entityManager.createEntities(
|
||||
this.maxBullets,
|
||||
"Bullet"
|
||||
);
|
||||
|
||||
// 初始化为非激活状态
|
||||
this.bulletPool.forEach(bullet => {
|
||||
bullet.enabled = false;
|
||||
bullet.addComponent(new PositionComponent());
|
||||
bullet.addComponent(new VelocityComponent());
|
||||
bullet.addComponent(new BulletComponent());
|
||||
});
|
||||
}
|
||||
|
||||
public spawnBullet(x: number, y: number, vx: number, vy: number): Entity | null {
|
||||
// 从池中获取非激活子弹(使用索引快速查询)
|
||||
const availableBullet = this.entityManager
|
||||
.query()
|
||||
.withAll([BulletComponent])
|
||||
.active(false)
|
||||
.first();
|
||||
|
||||
if (availableBullet) {
|
||||
// 重用现有子弹
|
||||
const pos = availableBullet.getComponent(PositionComponent);
|
||||
const vel = availableBullet.getComponent(VelocityComponent);
|
||||
|
||||
pos.x = x; pos.y = y;
|
||||
vel.x = vx; vel.y = vy;
|
||||
availableBullet.enabled = true;
|
||||
|
||||
return availableBullet;
|
||||
}
|
||||
|
||||
return null; // 池已满
|
||||
}
|
||||
|
||||
process() {
|
||||
// 批量处理所有激活的子弹
|
||||
this.entityManager.forEachEntityWithComponent(
|
||||
BulletComponent,
|
||||
(entity, bullet) => {
|
||||
if (!entity.enabled) return;
|
||||
|
||||
// 更新位置
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const vel = entity.getComponent(VelocityComponent);
|
||||
|
||||
pos.x += vel.x * Time.deltaTime;
|
||||
pos.y += vel.y * Time.deltaTime;
|
||||
|
||||
// 边界检查,回收到池中
|
||||
if (pos.x < 0 || pos.x > 800 || pos.y < 0 || pos.y > 600) {
|
||||
entity.enabled = false; // 回收而不是销毁
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AI系统性能优化
|
||||
|
||||
```typescript
|
||||
class AISystem extends EntitySystem {
|
||||
private spatialGrid: SpatialGrid;
|
||||
private updateFrequency = 60; // 60Hz更新频率
|
||||
private lastUpdate = 0;
|
||||
|
||||
process() {
|
||||
const currentTime = performance.now();
|
||||
|
||||
// 控制更新频率
|
||||
if (currentTime - this.lastUpdate < 1000 / this.updateFrequency) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用空间分区优化邻居查询
|
||||
const aiEntities = this.entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, AIComponent])
|
||||
.active(true)
|
||||
.execute();
|
||||
|
||||
// 分批处理AI实体
|
||||
const batchSize = 50;
|
||||
for (let i = 0; i < aiEntities.length; i += batchSize) {
|
||||
const batch = aiEntities.slice(i, i + batchSize);
|
||||
this.processBatch(batch);
|
||||
|
||||
// 时间片控制,避免单帧卡顿
|
||||
if (performance.now() - currentTime > 10) { // 10ms时间片
|
||||
break; // 下一帧继续处理
|
||||
}
|
||||
}
|
||||
|
||||
this.lastUpdate = currentTime;
|
||||
}
|
||||
|
||||
private processBatch(entities: Entity[]) {
|
||||
entities.forEach(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const ai = entity.getComponent(AIComponent);
|
||||
|
||||
// 空间查询优化邻居搜索
|
||||
const neighbors = this.spatialGrid.queryRadius(pos.x, pos.y, ai.sightRange);
|
||||
|
||||
// AI决策逻辑
|
||||
ai.update(neighbors);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控工具
|
||||
|
||||
### 实时性能仪表板
|
||||
|
||||
```typescript
|
||||
class PerformanceDashboard {
|
||||
private stats: any = {};
|
||||
private updateInterval = 1000; // 1秒更新一次
|
||||
|
||||
constructor(private entityManager: EntityManager) {
|
||||
setInterval(() => this.updateStats(), this.updateInterval);
|
||||
}
|
||||
|
||||
private updateStats() {
|
||||
this.stats = {
|
||||
// 基础统计
|
||||
entities: this.entityManager.getStatistics(),
|
||||
|
||||
// 组件索引
|
||||
componentIndex: this.entityManager.getComponentIndex().getPerformanceStats(),
|
||||
|
||||
// Archetype系统
|
||||
archetype: this.entityManager.getArchetypeSystem().getStatistics(),
|
||||
|
||||
// 脏标记系统
|
||||
dirtyTracking: this.entityManager.getDirtyTrackingSystem().getPerformanceStats(),
|
||||
|
||||
// 内存使用
|
||||
memory: this.entityManager.getMemoryUsage(),
|
||||
|
||||
// 计算性能指标
|
||||
performance: this.calculatePerformanceMetrics()
|
||||
};
|
||||
|
||||
this.displayStats();
|
||||
}
|
||||
|
||||
private calculatePerformanceMetrics() {
|
||||
const componentStats = this.stats.componentIndex;
|
||||
const archetypeStats = this.stats.archetype;
|
||||
|
||||
return {
|
||||
queryHitRate: componentStats.hitRate,
|
||||
archetypeEfficiency: archetypeStats.averageEntitiesPerArchetype,
|
||||
memoryEfficiency: this.stats.memory.compressionRatio,
|
||||
overallPerformance: this.calculateOverallScore()
|
||||
};
|
||||
}
|
||||
|
||||
private displayStats() {
|
||||
console.log('=== ECS性能仪表板 ===');
|
||||
console.log('查询命中率:', this.stats.performance.queryHitRate.toFixed(2) + '%');
|
||||
console.log('内存使用:', (this.stats.memory.total / 1024 / 1024).toFixed(2) + 'MB');
|
||||
console.log('整体性能评分:', this.stats.performance.overallPerformance.toFixed(1) + '/10');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 优化检查清单
|
||||
|
||||
### 开发阶段
|
||||
|
||||
- [ ] 使用EntityManager而不是直接操作Scene
|
||||
- [ ] 优先使用组件查询和标签查询
|
||||
- [ ] 设计合理的组件组合,避免过度碎片化
|
||||
- [ ] 实现对象池机制减少频繁创建/销毁
|
||||
|
||||
### 运行时优化
|
||||
|
||||
- [ ] 监控查询命中率,保持在80%以上
|
||||
- [ ] 控制Archetype数量,避免过度分散
|
||||
- [ ] 配置适当的脏标记批量处理参数
|
||||
- [ ] 定期进行内存清理和数据压缩
|
||||
|
||||
### 性能监控
|
||||
|
||||
- [ ] 定期检查性能统计数据
|
||||
- [ ] 监控内存使用趋势
|
||||
- [ ] 设置性能预警阈值
|
||||
- [ ] 在不同设备上进行性能测试
|
||||
|
||||
通过系统性地应用这些优化策略,您可以构建出在各种规模下都能提供卓越性能的ECS游戏系统。
|
||||
@@ -1,306 +0,0 @@
|
||||
# ECS框架性能基准
|
||||
|
||||
本文档展示了ECS框架的真实性能数据和瓶颈分析。
|
||||
|
||||
## 🚀 快速测试
|
||||
|
||||
```bash
|
||||
# 快速性能基准测试
|
||||
npm run benchmark
|
||||
|
||||
# 完整性能测试
|
||||
npm run test:performance
|
||||
|
||||
# 单元测试
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
## 📊 性能基准数据
|
||||
|
||||
> 测试环境: Node.js, Windows 10, 现代桌面CPU
|
||||
|
||||
### 1. 实体创建性能
|
||||
|
||||
| 实体数量 | 创建时间 | 创建速度 | 每个实体耗时 | 性能等级 |
|
||||
|---------|---------|---------|-------------|---------|
|
||||
| 1,000 | 1.56ms | 640,697个/秒 | 0.0016ms | 🚀 极致 |
|
||||
| 5,000 | 19.47ms | 256,805个/秒 | 0.0039ms | 🚀 极致 |
|
||||
| 10,000 | 39.94ms | 250,345个/秒 | 0.0040ms | 🚀 极致 |
|
||||
| 50,000 | 258.17ms | 193,673个/秒 | 0.0052ms | ✅ 优秀 |
|
||||
| 100,000 | 463.04ms | 215,963个/秒 | 0.0046ms | ✅ 优秀 |
|
||||
| 500,000 | 3,087ms | 161,990个/秒 | 0.0062ms | ✅ 优秀 |
|
||||
|
||||
**结论**: 🚀 实体创建性能达到极致水平,大规模创建50万实体仅需3秒
|
||||
|
||||
### 2. 性能瓶颈分析 (500,000个实体)
|
||||
|
||||
**当前瓶颈分布**:
|
||||
```
|
||||
实体创建: 46.3% (1,429ms)
|
||||
组件添加: 53.5% (1,651ms) ← 主要瓶颈
|
||||
标签分配: 0.2% (7ms)
|
||||
```
|
||||
|
||||
**特征**: 框架实现了均衡的性能分布,各部分开销相对合理
|
||||
|
||||
### 3. 组件添加性能详细分析
|
||||
|
||||
| 组件类型 | 添加速度 | 平均耗时 | 性能等级 |
|
||||
|---------|---------|---------|---------|
|
||||
| PositionComponent | 596,929组件/秒 | 0.0017ms | 🚀 极致 |
|
||||
| VelocityComponent | 1,186,770组件/秒 | 0.0008ms | 🚀 极致 |
|
||||
| HealthComponent | 841,982组件/秒 | 0.0012ms | 🚀 极致 |
|
||||
| RenderComponent | 763,351组件/秒 | 0.0013ms | 🚀 极致 |
|
||||
| AIComponent | 185,964组件/秒 | 0.0054ms | ✅ 优秀 |
|
||||
|
||||
### 4. 优化技术性能影响
|
||||
|
||||
| 优化技术 | 性能提升 | 内存影响 | 适用场景 |
|
||||
|---------|---------|---------|---------|
|
||||
| 组件对象池 | 30-50% | 减少分配 | 频繁创建/销毁 |
|
||||
| 位掩码优化器 | 20-40% | 缓存开销 | 大量查询操作 |
|
||||
| 批量操作 | 显著提升 | 无明显影响 | 大规模实体创建 |
|
||||
| 延迟索引更新 | 60-80% | 临时内存增加 | 批量实体操作 |
|
||||
| 索引去重优化 | 避免O(n) | 轻微内存增加 | 防止重复实体 |
|
||||
|
||||
### 5. 查询系统性能
|
||||
|
||||
#### 5.1 基础查询性能
|
||||
| 查询类型 | 查询速度 | 每次查询耗时 | 性能等级 |
|
||||
|---------|---------|-------------|---------|
|
||||
| 单组件查询 | 12,178次/秒 | 0.082ms | ✅ 优秀 |
|
||||
| 多组件查询 | 9,439次/秒 | 0.106ms | ✅ 优秀 |
|
||||
| 复合查询 | 7,407次/秒 | 0.135ms | ✅ 良好 |
|
||||
|
||||
#### 5.2 缓存查询性能
|
||||
| 缓存状态 | 访问速度 | 性能特征 |
|
||||
|---------|---------|---------|
|
||||
| 缓存命中 | 零延迟 | 🚀 即时响应 |
|
||||
| 缓存未命中 | 标准查询 | ✅ 自动构建 |
|
||||
| 缓存清理 | 批量延迟 | 🔧 优化策略 |
|
||||
|
||||
### 6. 新功能性能基准
|
||||
|
||||
#### 6.1 组件对象池性能
|
||||
```
|
||||
📊 对象池 vs 直接创建 (10,000次操作)
|
||||
对象池获取: 1.65ms (6,060,606次/秒)
|
||||
直接创建: 1.51ms (6,622,516次/秒)
|
||||
|
||||
⚠️ 小规模测试中对象池可能略慢,但在大规模应用中:
|
||||
- 减少30-50%的内存分配
|
||||
- 避免垃圾回收压力
|
||||
- 提升长期运行稳定性
|
||||
```
|
||||
|
||||
#### 6.2 位掩码优化器性能
|
||||
```
|
||||
🔥 位掩码操作性能 (100,000次操作)
|
||||
单个掩码创建: 20.00ms (5,000,000次/秒)
|
||||
组合掩码创建: 53.69ms (1,862,285次/秒)
|
||||
缓存掩码访问: <1ms (近零延迟)
|
||||
```
|
||||
|
||||
## 🎯 性能扩展性分析
|
||||
|
||||
### 实体创建扩展性
|
||||
```
|
||||
📈 创建速度趋势分析
|
||||
1K-10K实体: 250,000-640,000 实体/秒 (优秀)
|
||||
10K-100K实体: 200,000-250,000 实体/秒 (良好)
|
||||
100K-500K实体: 160,000-220,000 实体/秒 (稳定)
|
||||
|
||||
结论: 性能随规模稳定下降,无突然性能悬崖
|
||||
```
|
||||
|
||||
### 内存使用效率
|
||||
| 实体数量 | 内存使用 | 每实体内存 | 内存效率 |
|
||||
|---------|---------|-----------|---------|
|
||||
| 1,000 | 3.5MB | 3.5KB | 🚀 极致 |
|
||||
| 5,000 | 7.1MB | 1.4KB | 🚀 极致 |
|
||||
| 10,000 | 20.8MB | 2.1KB | ✅ 优秀 |
|
||||
| 50,000 | ~100MB | ~2KB | ✅ 优秀 |
|
||||
|
||||
## 💡 性能优化建议
|
||||
|
||||
### 1. 实体创建最佳实践
|
||||
|
||||
**✅ 推荐做法**:
|
||||
```typescript
|
||||
// 使用批量创建API
|
||||
const entities = scene.createEntities(10000, "Enemies");
|
||||
|
||||
// 延迟缓存清理
|
||||
entities.forEach(entity => {
|
||||
scene.addEntity(entity, false); // 延迟清理
|
||||
});
|
||||
scene.querySystem.clearCache(); // 手动清理
|
||||
```
|
||||
|
||||
**❌ 避免做法**:
|
||||
```typescript
|
||||
// 避免循环单个创建
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
scene.createEntity("Enemy" + i); // 每次触发缓存清理
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 组件池优化策略
|
||||
|
||||
**预热策略**:
|
||||
```typescript
|
||||
// 预热常用组件池
|
||||
ComponentPoolManager.getInstance().preWarmPools({
|
||||
BulletComponent: 2000, // 子弹大量创建
|
||||
EffectComponent: 1000, // 特效频繁使用
|
||||
PickupComponent: 500 // 道具适量缓存
|
||||
});
|
||||
```
|
||||
|
||||
**使用模式**:
|
||||
```typescript
|
||||
// 高效的组件复用
|
||||
const bullet = ComponentPoolManager.getInstance().getComponent(BulletComponent);
|
||||
bullet.reset(); // 重置状态
|
||||
entity.addComponent(bullet);
|
||||
|
||||
// 销毁时释放到池
|
||||
ComponentPoolManager.getInstance().releaseComponent(bullet);
|
||||
```
|
||||
|
||||
### 3. 查询优化策略
|
||||
|
||||
**缓存策略**:
|
||||
```typescript
|
||||
// 缓存频繁查询结果
|
||||
class MovementSystem extends EntitySystem {
|
||||
private cachedMovingEntities: Entity[];
|
||||
private lastCacheFrame: number = 0;
|
||||
|
||||
protected process(entities: Entity[]) {
|
||||
// 每5帧更新一次缓存
|
||||
if (Time.frameCount - this.lastCacheFrame > 5) {
|
||||
this.cachedMovingEntities = scene.getEntitiesWithComponents([Position, Velocity]);
|
||||
this.lastCacheFrame = Time.frameCount;
|
||||
}
|
||||
|
||||
// 使用缓存结果
|
||||
this.processMovement(this.cachedMovingEntities);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 不同规模应用建议
|
||||
|
||||
#### 小型游戏 (< 5,000实体)
|
||||
- ✅ 可以随意使用所有功能
|
||||
- ✅ 不需要特殊优化
|
||||
- ✅ 专注于游戏逻辑开发
|
||||
|
||||
#### 中型游戏 (5,000-50,000实体)
|
||||
- ✅ 使用批量操作API
|
||||
- ✅ 启用组件对象池
|
||||
- ⚠️ 注意查询频率
|
||||
|
||||
#### 大型游戏 (50,000+实体)
|
||||
- 🚀 必须使用批量操作
|
||||
- 🚀 必须启用对象池
|
||||
- 🚀 必须缓存查询结果
|
||||
- 🚀 考虑分区处理
|
||||
|
||||
## 🌍 平台性能对比
|
||||
|
||||
### Windows 桌面端 (测试平台)
|
||||
- **实体创建**: 640,697实体/秒
|
||||
- **组件操作**: 596,929组件/秒
|
||||
- **推荐实体数**: ≤ 200,000
|
||||
|
||||
### 预估其他平台性能
|
||||
|
||||
| 平台类型 | 预估性能比例 | 推荐实体数 | 特殊注意 |
|
||||
|---------|-------------|-----------|---------|
|
||||
| macOS桌面 | 90-100% | ≤ 180,000 | 内存管理优秀 |
|
||||
| Linux桌面 | 95-105% | ≤ 200,000 | 性能最优 |
|
||||
| Chrome浏览器 | 60-80% | ≤ 100,000 | V8引擎优化 |
|
||||
| Firefox浏览器 | 50-70% | ≤ 80,000 | SpiderMonkey限制 |
|
||||
| Safari浏览器 | 55-75% | ≤ 90,000 | JavaScriptCore |
|
||||
| Node.js服务器 | 100-110% | ≤ 500,000 | 服务器级性能 |
|
||||
| Android Chrome | 30-50% | ≤ 30,000 | 移动端限制 |
|
||||
| iOS Safari | 40-60% | ≤ 40,000 | iOS优化较好 |
|
||||
|
||||
## 🔬 测试环境详情
|
||||
|
||||
### 硬件环境
|
||||
- **操作系统**: Windows 10 (Build 26100)
|
||||
- **处理器**: 现代桌面CPU
|
||||
- **内存**: 充足RAM
|
||||
- **存储**: SSD高速存储
|
||||
|
||||
### 软件环境
|
||||
- **Node.js**: v16+
|
||||
- **TypeScript**: v5.8.3
|
||||
- **ECS框架版本**: v2.0.6
|
||||
- **测试工具**: 内置基准测试套件
|
||||
|
||||
### 测试方法
|
||||
- **实体配置**: 位置、速度、生命值、渲染、AI组件随机分配
|
||||
- **测试迭代**: 多次测试取平均值
|
||||
- **内存监控**: 实时内存使用情况
|
||||
- **性能指标**: performance.now()高精度计时
|
||||
|
||||
## 📋 性能测试清单
|
||||
|
||||
### 运行完整性能测试
|
||||
|
||||
```bash
|
||||
# 1. 快速基准测试 (2-3分钟)
|
||||
npm run benchmark
|
||||
|
||||
# 2. 完整性能测试 (10-15分钟)
|
||||
npm run test:performance
|
||||
|
||||
# 3. 单元测试验证 (30秒)
|
||||
npm run test:unit
|
||||
|
||||
# 4. 所有测试 (15-20分钟)
|
||||
npm run test
|
||||
```
|
||||
|
||||
### 自定义性能测试
|
||||
|
||||
```typescript
|
||||
import { runEntityCreationBenchmark } from '@esengine/ecs-framework/Testing/Performance/benchmark';
|
||||
|
||||
// 自定义规模测试
|
||||
await runEntityCreationBenchmark([1000, 5000, 10000]);
|
||||
|
||||
// 组件性能测试
|
||||
await runComponentPerformanceTest();
|
||||
|
||||
// 查询性能测试
|
||||
await runQueryPerformanceTest();
|
||||
```
|
||||
|
||||
## 🏆 性能总结
|
||||
|
||||
### 🎯 核心能力
|
||||
1. **实体创建速度**: 最高64万实体/秒
|
||||
2. **大规模处理**: 50万实体仅需3秒创建
|
||||
3. **均衡性能**: 各组件开销分布合理
|
||||
4. **扩展性**: 性能随规模线性下降,无突然悬崖
|
||||
|
||||
### 🔧 技术特点
|
||||
1. **批量操作架构** - 大幅减少单次操作开销
|
||||
2. **智能缓存策略** - 延迟清理机制
|
||||
3. **索引系统优化** - 避免O(n)操作
|
||||
4. **内存管理优化** - 对象池和位掩码缓存
|
||||
|
||||
### 🌟 实际应用价值
|
||||
- **小型游戏**: 性能过剩,专注玩法
|
||||
- **中型游戏**: 性能充足,适度优化
|
||||
- **大型游戏**: 需要优化策略,但完全可行
|
||||
- **服务器端**: 可处理大规模实体管理
|
||||
|
||||
---
|
||||
|
||||
**结论**: ECS框架达到了产品级性能标准,能够满足从休闲小游戏到复杂RTS游戏的各种需求。框架层面的性能已经充分优化,为开发者提供了坚实的性能基础。
|
||||
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 |
@@ -1,324 +0,0 @@
|
||||
# QuerySystem 使用指南
|
||||
|
||||
QuerySystem 是 ECS Framework 中的高性能实体查询系统,支持多级索引、智能缓存和类型安全的查询操作。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 1. 获取查询系统
|
||||
|
||||
```typescript
|
||||
import { Scene, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建场景,查询系统会自动创建
|
||||
const scene = new Scene();
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// 或者从Core获取当前场景的查询系统
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
const currentQuerySystem = Core.scene?.querySystem;
|
||||
```
|
||||
|
||||
### 2. 基本查询操作
|
||||
|
||||
```typescript
|
||||
// 查询包含所有指定组件的实体
|
||||
const result = querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
console.log(`找到 ${result.count} 个实体`);
|
||||
|
||||
// 查询包含任意指定组件的实体
|
||||
const anyResult = querySystem.queryAny(HealthComponent, ManaComponent);
|
||||
|
||||
// 查询不包含指定组件的实体
|
||||
const noneResult = querySystem.queryNone(DeadComponent);
|
||||
```
|
||||
|
||||
### 3. 类型安全查询
|
||||
|
||||
```typescript
|
||||
// 类型安全的查询,返回实体和对应的组件
|
||||
const typedResult = querySystem.queryAllTyped(PositionComponent, VelocityComponent);
|
||||
for (let i = 0; i < typedResult.entities.length; i++) {
|
||||
const entity = typedResult.entities[i];
|
||||
const [position, velocity] = typedResult.components[i];
|
||||
// position 和 velocity 都是类型安全的
|
||||
}
|
||||
|
||||
// 查询单个组件类型
|
||||
const healthEntities = querySystem.queryComponentTyped(HealthComponent);
|
||||
healthEntities.forEach(({ entity, component }) => {
|
||||
console.log(`实体 ${entity.name} 的生命值: ${component.value}`);
|
||||
});
|
||||
|
||||
// 查询两个组件类型
|
||||
const movableEntities = querySystem.queryTwoComponents(PositionComponent, VelocityComponent);
|
||||
movableEntities.forEach(({ entity, component1: position, component2: velocity }) => {
|
||||
// 更新位置
|
||||
position.x += velocity.x;
|
||||
position.y += velocity.y;
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 使用查询构建器
|
||||
|
||||
```typescript
|
||||
// 创建复杂查询
|
||||
const query = querySystem.createQuery()
|
||||
.withAll(PositionComponent, RenderComponent)
|
||||
.without(HiddenComponent)
|
||||
.withTag(1) // 特定标签
|
||||
.orderByName()
|
||||
.limit(10);
|
||||
|
||||
const result = query.execute();
|
||||
|
||||
// 链式操作
|
||||
const visibleEnemies = querySystem.createQuery()
|
||||
.withAll(EnemyComponent, PositionComponent)
|
||||
.without(DeadComponent, HiddenComponent)
|
||||
.filter(entity => entity.name.startsWith('Boss'))
|
||||
.orderBy((a, b) => a.id - b.id);
|
||||
|
||||
// 迭代结果
|
||||
visibleEnemies.forEach((entity, index) => {
|
||||
console.log(`敌人 ${index}: ${entity.name}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 5. 高级查询功能
|
||||
|
||||
```typescript
|
||||
// 复合查询
|
||||
const complexResult = querySystem.queryComplex(
|
||||
{
|
||||
type: QueryConditionType.ALL,
|
||||
componentTypes: [PositionComponent, VelocityComponent],
|
||||
mask: /* 位掩码 */
|
||||
},
|
||||
{
|
||||
type: QueryConditionType.NONE,
|
||||
componentTypes: [DeadComponent],
|
||||
mask: /* 位掩码 */
|
||||
}
|
||||
);
|
||||
|
||||
// 批量查询
|
||||
const batchResults = querySystem.batchQuery([
|
||||
{ type: QueryConditionType.ALL, componentTypes: [HealthComponent], mask: /* 位掩码 */ },
|
||||
{ type: QueryConditionType.ALL, componentTypes: [ManaComponent], mask: /* 位掩码 */ }
|
||||
]);
|
||||
|
||||
// 并行查询
|
||||
const parallelResults = await querySystem.parallelQuery([
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PositionComponent], mask: /* 位掩码 */ },
|
||||
{ type: QueryConditionType.ALL, componentTypes: [VelocityComponent], mask: /* 位掩码 */ }
|
||||
]);
|
||||
```
|
||||
|
||||
## 场景级别的实体查询
|
||||
|
||||
除了使用QuerySystem,您还可以直接使用Scene提供的便捷查询方法:
|
||||
|
||||
### 基本场景查询
|
||||
|
||||
```typescript
|
||||
// 按名称查找实体
|
||||
const player = scene.findEntity("Player");
|
||||
const playerAlt = scene.getEntityByName("Player"); // 别名方法
|
||||
|
||||
// 按ID查找实体
|
||||
const entity = scene.findEntityById(123);
|
||||
|
||||
// 按标签查找实体
|
||||
const enemies = scene.findEntitiesByTag(2);
|
||||
const enemiesAlt = scene.getEntitiesByTag(2); // 别名方法
|
||||
|
||||
// 获取所有实体
|
||||
const allEntities = scene.entities.buffer;
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 缓存管理
|
||||
|
||||
```typescript
|
||||
// 设置缓存配置
|
||||
querySystem.setCacheConfig(200, 2000); // 最大200个缓存项,2秒超时
|
||||
|
||||
// 清空缓存
|
||||
querySystem.clearCache();
|
||||
|
||||
// 预热常用查询
|
||||
const commonQueries = [
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PositionComponent], mask: /* 位掩码 */ },
|
||||
{ type: QueryConditionType.ALL, componentTypes: [VelocityComponent], mask: /* 位掩码 */ }
|
||||
];
|
||||
querySystem.warmUpCache(commonQueries);
|
||||
```
|
||||
|
||||
### 2. 索引优化
|
||||
|
||||
```typescript
|
||||
// 自动优化索引配置
|
||||
querySystem.optimizeIndexes();
|
||||
|
||||
// 获取性能统计
|
||||
const stats = querySystem.getStats();
|
||||
console.log(`缓存命中率: ${(stats.hitRate * 100).toFixed(1)}%`);
|
||||
console.log(`实体数量: ${stats.entityCount}`);
|
||||
|
||||
// 获取详细性能报告
|
||||
const report = querySystem.getPerformanceReport();
|
||||
console.log(report);
|
||||
```
|
||||
|
||||
### 3. 查询监听和快照
|
||||
|
||||
```typescript
|
||||
// 监听查询结果变更
|
||||
const unwatch = querySystem.watchQuery(
|
||||
{ type: QueryConditionType.ALL, componentTypes: [EnemyComponent], mask: /* 位掩码 */ },
|
||||
(entities, changeType) => {
|
||||
console.log(`敌人实体${changeType}: ${entities.length}个`);
|
||||
}
|
||||
);
|
||||
|
||||
// 取消监听
|
||||
unwatch();
|
||||
|
||||
// 创建查询快照
|
||||
const snapshot1 = querySystem.createSnapshot(
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PlayerComponent], mask: /* 位掩码 */ }
|
||||
);
|
||||
|
||||
// 稍后创建另一个快照
|
||||
const snapshot2 = querySystem.createSnapshot(
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PlayerComponent], mask: /* 位掩码 */ }
|
||||
);
|
||||
|
||||
// 比较快照
|
||||
const diff = querySystem.compareSnapshots(snapshot1, snapshot2);
|
||||
console.log(`新增: ${diff.added.length}, 移除: ${diff.removed.length}`);
|
||||
```
|
||||
|
||||
## 实际使用示例
|
||||
|
||||
### 移动系统示例
|
||||
|
||||
```typescript
|
||||
import { EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
public update(): void {
|
||||
// 查询所有可移动的实体
|
||||
const movableEntities = this.scene.querySystem.queryTwoComponents(
|
||||
PositionComponent,
|
||||
VelocityComponent
|
||||
);
|
||||
|
||||
movableEntities.forEach(({ entity, component1: position, component2: velocity }) => {
|
||||
// 更新位置
|
||||
position.x += velocity.x * Time.deltaTime;
|
||||
position.y += velocity.y * Time.deltaTime;
|
||||
|
||||
// 边界检查
|
||||
if (position.x < 0 || position.x > 800) {
|
||||
velocity.x = -velocity.x;
|
||||
}
|
||||
if (position.y < 0 || position.y > 600) {
|
||||
velocity.y = -velocity.y;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 碰撞检测示例
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends EntitySystem {
|
||||
public update(): void {
|
||||
// 获取所有具有碰撞器的实体
|
||||
const collidableEntities = this.scene.querySystem.queryTwoComponents(
|
||||
PositionComponent,
|
||||
ColliderComponent
|
||||
);
|
||||
|
||||
// 检测碰撞
|
||||
for (let i = 0; i < collidableEntities.length; i++) {
|
||||
for (let j = i + 1; j < collidableEntities.length; j++) {
|
||||
const entityA = collidableEntities[i];
|
||||
const entityB = collidableEntities[j];
|
||||
|
||||
if (this.checkCollision(entityA, entityB)) {
|
||||
this.handleCollision(entityA.entity, entityB.entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(entityA: any, entityB: any): boolean {
|
||||
// 简单的距离检测
|
||||
const posA = entityA.component1;
|
||||
const posB = entityB.component1;
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(posA.x - posB.x, 2) + Math.pow(posA.y - posB.y, 2)
|
||||
);
|
||||
return distance < 50;
|
||||
}
|
||||
|
||||
private handleCollision(entityA: Entity, entityB: Entity): void {
|
||||
console.log(`碰撞检测: ${entityA.name} 与 ${entityB.name}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 生命值管理示例
|
||||
|
||||
```typescript
|
||||
class HealthSystem extends EntitySystem {
|
||||
public update(): void {
|
||||
// 查询所有具有生命值的实体
|
||||
const healthEntities = this.scene.querySystem.queryComponentTyped(HealthComponent);
|
||||
const deadEntities: Entity[] = [];
|
||||
|
||||
healthEntities.forEach(({ entity, component: health }) => {
|
||||
// 检查死亡
|
||||
if (health.currentHealth <= 0) {
|
||||
deadEntities.push(entity);
|
||||
}
|
||||
});
|
||||
|
||||
// 移除死亡实体
|
||||
deadEntities.forEach(entity => {
|
||||
console.log(`实体 ${entity.name} 已死亡`);
|
||||
entity.destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 查询优化
|
||||
|
||||
- 尽量使用类型安全的查询方法
|
||||
- 避免在每帧都创建新的查询
|
||||
- 合理使用缓存机制
|
||||
|
||||
### 2. 性能监控
|
||||
|
||||
- 定期检查查询性能报告
|
||||
- 监控缓存命中率
|
||||
- 优化频繁使用的查询
|
||||
|
||||
### 3. 内存管理
|
||||
|
||||
- 及时清理不需要的查询监听器
|
||||
- 合理设置缓存大小
|
||||
- 避免创建过多的查询快照
|
||||
|
||||
### 4. 代码组织
|
||||
|
||||
- 将复杂查询封装到专门的方法中
|
||||
- 使用查询构建器创建可读性更好的查询
|
||||
- 在系统中合理组织查询逻辑
|
||||
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` |
|
||||
140
docs/src/content/docs/en/guide/scene/index.md
Normal file
140
docs/src/content/docs/en/guide/scene/index.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Runtime Environment
|
||||
|
||||
For networked games, you can configure the runtime environment to distinguish between server and client logic.
|
||||
|
||||
### Global Configuration (Recommended)
|
||||
|
||||
Set the runtime environment once at the Core level - all Scenes will inherit this setting:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// Method 1: Set in Core.create()
|
||||
Core.create({ runtimeEnvironment: 'server' });
|
||||
|
||||
// Method 2: Set static property directly
|
||||
Core.runtimeEnvironment = 'server';
|
||||
```
|
||||
|
||||
### Per-Scene Override
|
||||
|
||||
Individual scenes can override the global setting:
|
||||
|
||||
```typescript
|
||||
const clientScene = new Scene({ runtimeEnvironment: 'client' });
|
||||
```
|
||||
|
||||
### Environment Types
|
||||
|
||||
| Environment | Use Case |
|
||||
|-------------|----------|
|
||||
| `'standalone'` | Single-player games (default) |
|
||||
| `'server'` | Game server, authoritative logic |
|
||||
| `'client'` | Game client, rendering/input |
|
||||
|
||||
### Checking Environment in Systems
|
||||
|
||||
```typescript
|
||||
class CollectibleSpawnSystem extends EntitySystem {
|
||||
private checkCollections(): void {
|
||||
// Skip on client - only server handles authoritative logic
|
||||
if (!this.scene.isServer) return;
|
||||
|
||||
// Server-authoritative spawn logic...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [System Runtime Decorators](/en/guide/system/index#runtime-environment-decorators) for decorator-based approach.
|
||||
|
||||
### 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 |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user