├── .github
├── FUNDING.yml
└── workflows
│ └── pr-test.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── dist
├── index.cjs
├── index.cjs.map
├── index.d.ts
├── index.mjs
└── index.mjs.map
├── docs
├── API.md
├── CHANGELOG.md
├── FAQ.md
└── INTRO.md
├── index.d.ts
├── mozilla-hubs.png
├── package-lock.json
├── package.json
├── scripts
├── build.js
├── docs.js
└── logLogo.js
├── src
├── Component.js
├── Constants.js
├── Entity.js
├── Query.js
├── Serialize.js
├── Storage.js
├── System.js
├── Util.js
├── World.js
├── index.js
└── index.ts
├── test
├── adhoc
│ └── deserialize-enter-query-bug.js
├── integration
│ ├── Component.test.js
│ ├── Entity.test.js
│ ├── Query.test.js
│ ├── Serialize.test.js
│ ├── Storage.test.js
│ ├── System.test.js
│ └── World.test.js
└── unit
│ ├── Serialize.test.js
│ └── World.test.js
└── tsconfig.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: NateTheGreatt
--------------------------------------------------------------------------------
/.github/workflows/pr-test.yml:
--------------------------------------------------------------------------------
1 | name: Test CI
2 |
3 | on:
4 | pull_request:
5 | branches: [ master ]
6 |
7 | jobs:
8 | build:
9 |
10 | runs-on: ubuntu-latest
11 |
12 | strategy:
13 | matrix:
14 | node-version: [14.x]
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Use Node.js ${{ matrix.node-version }}
19 | uses: actions/setup-node@v2
20 | with:
21 | node-version: ${{ matrix.node-version }}
22 | cache: 'npm'
23 | - run: npm ci
24 | - run: npm test
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | sandbox
4 | coverage
5 | docs/CHANGELOG-next.md
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | .gitignore
3 | __tests__
4 | scripts
5 | rollup.config.js
6 | src
7 | sandbox
8 | coverage
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | :warning: [v0.4](https://github.com/NateTheGreatt/bitECS/blob/rc-0-4-0) coming soon! Read the [docs here](https://github.com/NateTheGreatt/bitECS/blob/rc-0-4-0/docs/Intro.md)
2 |
3 |
4 |
5 | ❤ ❤ ❤
6 | bitECS
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Functional, minimal, data-oriented, ultra-high performance ECS library written using JavaScript TypedArrays.
26 |
27 |
28 |
29 |
30 | ## ✨ Features
31 |
32 | | | |
33 | | --------------------------------- | ---------------------------------------- |
34 | | 🔮 Simple, declarative API | 🔥 Blazing fast iteration |
35 | | 🔍 Powerful & performant queries | 💾 Serialization included |
36 | | 🍃 Zero dependencies | 🌐 Node or browser |
37 | | 🤏 `~5kb` minzipped | 🏷 TypeScript support |
38 | | ❤ Made with love | 🔺 [glMatrix](https://github.com/toji/gl-matrix) support |
39 |
40 | ### 📈 Benchmarks
41 |
42 | | | |
43 | | --------------------------------------------------------------- | ------------------------------------------------------------------------- |
44 | | [noctjs/ecs-benchmark](https://github.com/noctjs/ecs-benchmark) | [ddmills/js-ecs-benchmarks](https://github.com/ddmills/js-ecs-benchmarks) |
45 |
46 | ## 💿 Install
47 | ```
48 | npm i bitecs
49 | ```
50 |
51 | ## 📘 Documentation
52 | | |
53 | | ---------------- |
54 | | 🏁 [Getting Started](https://github.com/NateTheGreatt/bitECS/blob/master/docs/INTRO.md) |
55 | | 📑 [API](https://github.com/NateTheGreatt/bitECS/blob/master/docs/API.md) |
56 | | ❔ [FAQ](https://github.com/NateTheGreatt/bitECS/blob/master/docs/FAQ.md) |
57 | | 🏛 [Tutorial](https://github.com/ourcade/phaser3-bitecs-getting-started) |
58 |
59 | ## 🕹 Example
60 |
61 | ```js
62 | import {
63 | createWorld,
64 | Types,
65 | defineComponent,
66 | defineQuery,
67 | addEntity,
68 | addComponent,
69 | pipe,
70 | } from 'bitecs'
71 |
72 | const Vector3 = { x: Types.f32, y: Types.f32, z: Types.f32 }
73 | const Position = defineComponent(Vector3)
74 | const Velocity = defineComponent(Vector3)
75 |
76 | const movementQuery = defineQuery([Position, Velocity])
77 |
78 | const movementSystem = (world) => {
79 | const { time: { delta } } = world
80 | const ents = movementQuery(world)
81 | for (let i = 0; i < ents.length; i++) {
82 | const eid = ents[i]
83 | Position.x[eid] += Velocity.x[eid] * delta
84 | Position.y[eid] += Velocity.y[eid] * delta
85 | Position.z[eid] += Velocity.z[eid] * delta
86 | }
87 | return world
88 | }
89 |
90 | const timeSystem = world => {
91 | const { time } = world
92 | const now = performance.now()
93 | const delta = now - time.then
94 | time.delta = delta
95 | time.elapsed += delta
96 | time.then = now
97 | return world
98 | }
99 |
100 | const pipeline = pipe(movementSystem, timeSystem)
101 |
102 | const world = createWorld()
103 | world.time = { delta: 0, elapsed: 0, then: performance.now() }
104 |
105 | const eid = addEntity(world)
106 | addComponent(world, Position, eid)
107 | addComponent(world, Velocity, eid)
108 | Velocity.x[eid] = 1.23
109 | Velocity.y[eid] = 1.23
110 |
111 | setInterval(() => {
112 | pipeline(world)
113 | }, 16)
114 | ```
115 |
116 | ## 🔌 Powering
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'bitecs' {
2 | export type Type =
3 | | 'i8'
4 | | 'ui8'
5 | | 'ui8c'
6 | | 'i16'
7 | | 'ui16'
8 | | 'i32'
9 | | 'ui32'
10 | | 'f32'
11 | | 'f64'
12 | | 'eid'
13 |
14 | export type ListType = readonly [Type, number];
15 |
16 | export const Types: {
17 | i8: "i8"
18 | ui8: "ui8"
19 | ui8c: "ui8c"
20 | i16: "i16"
21 | ui16: "ui16"
22 | i32: "i32"
23 | ui32: "ui32"
24 | f32: "f32"
25 | f64: "f64"
26 | eid: "eid"
27 | };
28 |
29 | export type TypedArray =
30 | | Uint8Array
31 | | Int8Array
32 | | Uint8Array
33 | | Uint8ClampedArray
34 | | Int16Array
35 | | Uint16Array
36 | | Int32Array
37 | | Uint32Array
38 | | Float32Array
39 | | Float64Array
40 |
41 | export type ArrayByType = {
42 | [Types.i8]: Int8Array;
43 | [Types.ui8]: Uint8Array;
44 | [Types.ui8c]: Uint8ClampedArray;
45 | [Types.i16]: Int16Array;
46 | [Types.ui16]: Uint16Array;
47 | [Types.i32]: Int32Array;
48 | [Types.ui32]: Uint32Array;
49 | [Types.f32]: Float32Array;
50 | [Types.f64]: Float64Array;
51 | [Types.eid]: Uint32Array;
52 | }
53 |
54 | export enum DESERIALIZE_MODE {
55 | REPLACE,
56 | APPEND,
57 | MAP
58 | }
59 |
60 | export type ComponentType = {
61 | [key in keyof T]:
62 | T[key] extends Type
63 | ? ArrayByType[T[key]]
64 | : T[key] extends [infer RT, number]
65 | ? RT extends Type
66 | ? Array
67 | : unknown
68 | : T[key] extends ISchema
69 | ? ComponentType
70 | : unknown;
71 | };
72 |
73 | export type ComponentProp = TypedArray | Array
74 |
75 | export interface IWorld {}
76 |
77 | export interface ISchema {
78 | [key: string]: Type | ListType | ISchema
79 | }
80 |
81 | export interface IComponentProp {
82 | }
83 |
84 | export interface IComponent {
85 | }
86 |
87 | export type Component = IComponent | ComponentType
88 |
89 | export type QueryModifier = (c: (IComponent | IComponentProp)[]) => (world: W) => IComponent | QueryModifier
90 |
91 | export type Query = (world: W, clearDiff?: Boolean) => number[]
92 |
93 | export type System = (world: W, ...args: R) => W
94 |
95 | export type Serializer = (target: W | number[]) => ArrayBuffer
96 | export type Deserializer = (world: W, packet: ArrayBuffer, mode?: DESERIALIZE_MODE) => number[]
97 |
98 | export function setDefaultSize(size: number): void
99 | export function setRemovedRecycleThreshold(newThreshold: number): void
100 | export function createWorld(obj?: W, size?: number): W
101 | export function createWorld(size?: number): W
102 | export function resetWorld(world: W): W
103 | export function deleteWorld(world: W): void
104 | export function addEntity(world: W): number
105 | export function removeEntity(world: W, eid: number): void
106 | export function entityExists(world: W, eid: number): boolean
107 | export function getWorldComponents(world: W): Component[]
108 | export function getAllEntities(world: W): number[]
109 | export function enableManualEntityRecycling(world: W): void
110 | export function flushRemovedEntities(world: W): void
111 |
112 | export function registerComponent(world: W, component: Component): void
113 | export function registerComponents(world: W, components: Component[]): void
114 | export function defineComponent(schema?: T, size?: number): ComponentType
115 | export function defineComponent(schema?: any, size?: number): T
116 | export function addComponent(world: W, component: Component, eid: number, reset?: boolean): void
117 | export function removeComponent(world: W, component: Component, eid: number, reset?: boolean): void
118 | export function hasComponent(world: W, component: Component, eid: number): boolean
119 | export function getEntityComponents(world: W, eid: number): Component[]
120 |
121 | export function defineQuery(components: (Component | QueryModifier)[]): Query
122 | export function Changed(c: Component | ISchema): Component | QueryModifier
123 | export function Not(c: Component | ISchema): Component | QueryModifier
124 | export function enterQuery(query: Query): Query
125 | export function exitQuery(query: Query): Query
126 | export function resetChangedQuery(world: W, query: Query): Query
127 | export function removeQuery(world: W, query: Query): Query
128 | export function commitRemovals(world: W): void
129 |
130 | export function defineSystem(update: (world: W, ...args: R) => W): System
131 |
132 | export function defineSerializer(target: W | Component[] | IComponentProp[] | QueryModifier, maxBytes?: number): Serializer
133 | export function defineDeserializer(target: W | Component[] | IComponentProp[] | QueryModifier): Deserializer
134 |
135 | export function pipe(...fns: ((...args: any[]) => any)[]): (...input: any[]) => any
136 |
137 | export const parentArray: Symbol
138 | }
139 |
--------------------------------------------------------------------------------
/dist/index.mjs:
--------------------------------------------------------------------------------
1 | // src/Constants.js
2 | var TYPES_ENUM = {
3 | i8: "i8",
4 | ui8: "ui8",
5 | ui8c: "ui8c",
6 | i16: "i16",
7 | ui16: "ui16",
8 | i32: "i32",
9 | ui32: "ui32",
10 | f32: "f32",
11 | f64: "f64",
12 | eid: "eid"
13 | };
14 | var TYPES_NAMES = {
15 | i8: "Int8",
16 | ui8: "Uint8",
17 | ui8c: "Uint8Clamped",
18 | i16: "Int16",
19 | ui16: "Uint16",
20 | i32: "Int32",
21 | ui32: "Uint32",
22 | eid: "Uint32",
23 | f32: "Float32",
24 | f64: "Float64"
25 | };
26 | var TYPES = {
27 | i8: Int8Array,
28 | ui8: Uint8Array,
29 | ui8c: Uint8ClampedArray,
30 | i16: Int16Array,
31 | ui16: Uint16Array,
32 | i32: Int32Array,
33 | ui32: Uint32Array,
34 | f32: Float32Array,
35 | f64: Float64Array,
36 | eid: Uint32Array
37 | };
38 | var UNSIGNED_MAX = {
39 | uint8: 2 ** 8,
40 | uint16: 2 ** 16,
41 | uint32: 2 ** 32
42 | };
43 |
44 | // src/Storage.js
45 | var roundToMultiple = (mul) => (x) => Math.ceil(x / mul) * mul;
46 | var roundToMultiple4 = roundToMultiple(4);
47 | var $storeRef = Symbol("storeRef");
48 | var $storeSize = Symbol("storeSize");
49 | var $storeMaps = Symbol("storeMaps");
50 | var $storeFlattened = Symbol("storeFlattened");
51 | var $storeBase = Symbol("storeBase");
52 | var $storeType = Symbol("storeType");
53 | var $storeArrayElementCounts = Symbol("storeArrayElementCounts");
54 | var $storeSubarrays = Symbol("storeSubarrays");
55 | var $subarrayCursors = Symbol("subarrayCursors");
56 | var $subarray = Symbol("subarray");
57 | var $subarrayFrom = Symbol("subarrayFrom");
58 | var $subarrayTo = Symbol("subarrayTo");
59 | var $parentArray = Symbol("parentArray");
60 | var $tagStore = Symbol("tagStore");
61 | var $queryShadow = Symbol("queryShadow");
62 | var $serializeShadow = Symbol("serializeShadow");
63 | var $indexType = Symbol("indexType");
64 | var $indexBytes = Symbol("indexBytes");
65 | var $isEidType = Symbol("isEidType");
66 | var stores = {};
67 | var resize = (ta, size) => {
68 | const newBuffer = new ArrayBuffer(size * ta.BYTES_PER_ELEMENT);
69 | const newTa = new ta.constructor(newBuffer);
70 | newTa.set(ta, 0);
71 | return newTa;
72 | };
73 | var createShadow = (store, key) => {
74 | if (!ArrayBuffer.isView(store)) {
75 | const shadowStore = store[$parentArray].slice(0);
76 | store[key] = store.map((_, eid) => {
77 | const { length } = store[eid];
78 | const start = length * eid;
79 | const end = start + length;
80 | return shadowStore.subarray(start, end);
81 | });
82 | } else {
83 | store[key] = store.slice(0);
84 | }
85 | };
86 | var resetStoreFor = (store, eid) => {
87 | if (store[$storeFlattened]) {
88 | store[$storeFlattened].forEach((ta) => {
89 | if (ArrayBuffer.isView(ta))
90 | ta[eid] = 0;
91 | else
92 | ta[eid].fill(0);
93 | });
94 | }
95 | };
96 | var createTypeStore = (type, length) => {
97 | const totalBytes = length * TYPES[type].BYTES_PER_ELEMENT;
98 | const buffer = new ArrayBuffer(totalBytes);
99 | const store = new TYPES[type](buffer);
100 | store[$isEidType] = type === TYPES_ENUM.eid;
101 | return store;
102 | };
103 | var parentArray = (store) => store[$parentArray];
104 | var createArrayStore = (metadata, type, length) => {
105 | const storeSize = metadata[$storeSize];
106 | const store = Array(storeSize).fill(0);
107 | store[$storeType] = type;
108 | store[$isEidType] = type === TYPES_ENUM.eid;
109 | const cursors = metadata[$subarrayCursors];
110 | const indexType = length <= UNSIGNED_MAX.uint8 ? TYPES_ENUM.ui8 : length <= UNSIGNED_MAX.uint16 ? TYPES_ENUM.ui16 : TYPES_ENUM.ui32;
111 | if (!length)
112 | throw new Error("bitECS - Must define component array length");
113 | if (!TYPES[type])
114 | throw new Error(`bitECS - Invalid component array property type ${type}`);
115 | if (!metadata[$storeSubarrays][type]) {
116 | const arrayElementCount = metadata[$storeArrayElementCounts][type];
117 | const array = new TYPES[type](roundToMultiple4(arrayElementCount * storeSize));
118 | array[$indexType] = TYPES_NAMES[indexType];
119 | array[$indexBytes] = TYPES[indexType].BYTES_PER_ELEMENT;
120 | metadata[$storeSubarrays][type] = array;
121 | }
122 | const start = cursors[type];
123 | const end = start + storeSize * length;
124 | cursors[type] = end;
125 | store[$parentArray] = metadata[$storeSubarrays][type].subarray(start, end);
126 | for (let eid = 0; eid < storeSize; eid++) {
127 | const start2 = length * eid;
128 | const end2 = start2 + length;
129 | store[eid] = store[$parentArray].subarray(start2, end2);
130 | store[eid][$indexType] = TYPES_NAMES[indexType];
131 | store[eid][$indexBytes] = TYPES[indexType].BYTES_PER_ELEMENT;
132 | store[eid][$subarray] = true;
133 | }
134 | return store;
135 | };
136 | var isArrayType = (x) => Array.isArray(x) && typeof x[0] === "string" && typeof x[1] === "number";
137 | var createStore = (schema, size) => {
138 | const $store = Symbol("store");
139 | if (!schema || !Object.keys(schema).length) {
140 | stores[$store] = {
141 | [$storeSize]: size,
142 | [$tagStore]: true,
143 | [$storeBase]: () => stores[$store]
144 | };
145 | return stores[$store];
146 | }
147 | schema = JSON.parse(JSON.stringify(schema));
148 | const arrayElementCounts = {};
149 | const collectArrayElementCounts = (s) => {
150 | const keys = Object.keys(s);
151 | for (const k of keys) {
152 | if (isArrayType(s[k])) {
153 | if (!arrayElementCounts[s[k][0]])
154 | arrayElementCounts[s[k][0]] = 0;
155 | arrayElementCounts[s[k][0]] += s[k][1];
156 | } else if (s[k] instanceof Object) {
157 | collectArrayElementCounts(s[k]);
158 | }
159 | }
160 | };
161 | collectArrayElementCounts(schema);
162 | const metadata = {
163 | [$storeSize]: size,
164 | [$storeMaps]: {},
165 | [$storeSubarrays]: {},
166 | [$storeRef]: $store,
167 | [$subarrayCursors]: Object.keys(TYPES).reduce((a, type) => ({ ...a, [type]: 0 }), {}),
168 | [$storeFlattened]: [],
169 | [$storeArrayElementCounts]: arrayElementCounts
170 | };
171 | if (schema instanceof Object && Object.keys(schema).length) {
172 | const recursiveTransform = (a, k) => {
173 | if (typeof a[k] === "string") {
174 | a[k] = createTypeStore(a[k], size);
175 | a[k][$storeBase] = () => stores[$store];
176 | metadata[$storeFlattened].push(a[k]);
177 | } else if (isArrayType(a[k])) {
178 | const [type, length] = a[k];
179 | a[k] = createArrayStore(metadata, type, length);
180 | a[k][$storeBase] = () => stores[$store];
181 | metadata[$storeFlattened].push(a[k]);
182 | } else if (a[k] instanceof Object) {
183 | a[k] = Object.keys(a[k]).reduce(recursiveTransform, a[k]);
184 | }
185 | return a;
186 | };
187 | stores[$store] = Object.assign(Object.keys(schema).reduce(recursiveTransform, schema), metadata);
188 | stores[$store][$storeBase] = () => stores[$store];
189 | return stores[$store];
190 | }
191 | };
192 |
193 | // src/Util.js
194 | var SparseSet = () => {
195 | const dense = [];
196 | const sparse = [];
197 | dense.sort = function(comparator) {
198 | const result = Array.prototype.sort.call(this, comparator);
199 | for (let i = 0; i < dense.length; i++) {
200 | sparse[dense[i]] = i;
201 | }
202 | return result;
203 | };
204 | const has = (val) => dense[sparse[val]] === val;
205 | const add = (val) => {
206 | if (has(val))
207 | return;
208 | sparse[val] = dense.push(val) - 1;
209 | };
210 | const remove = (val) => {
211 | if (!has(val))
212 | return;
213 | const index = sparse[val];
214 | const swapped = dense.pop();
215 | if (swapped !== val) {
216 | dense[index] = swapped;
217 | sparse[swapped] = index;
218 | }
219 | };
220 | const reset = () => {
221 | dense.length = 0;
222 | sparse.length = 0;
223 | };
224 | return {
225 | add,
226 | remove,
227 | has,
228 | sparse,
229 | dense,
230 | reset
231 | };
232 | };
233 |
234 | // src/Serialize.js
235 | var DESERIALIZE_MODE = {
236 | REPLACE: 0,
237 | APPEND: 1,
238 | MAP: 2
239 | };
240 | var resized = false;
241 | var setSerializationResized = (v) => {
242 | resized = v;
243 | };
244 | var concat = (a, v) => a.concat(v);
245 | var not = (fn) => (v) => !fn(v);
246 | var storeFlattened = (c) => c[$storeFlattened];
247 | var isFullComponent = storeFlattened;
248 | var isProperty = not(isFullComponent);
249 | var isModifier = (c) => typeof c === "function" && c[$modifier];
250 | var isNotModifier = not(isModifier);
251 | var isChangedModifier = (c) => isModifier(c) && c()[1] === "changed";
252 | var isWorld = (w) => Object.getOwnPropertySymbols(w).includes($componentMap);
253 | var fromModifierToComponent = (c) => c()[0];
254 | var canonicalize = (target) => {
255 | if (isWorld(target))
256 | return [[], /* @__PURE__ */ new Map()];
257 | const fullComponentProps = target.filter(isNotModifier).filter(isFullComponent).map(storeFlattened).reduce(concat, []);
258 | const changedComponentProps = target.filter(isChangedModifier).map(fromModifierToComponent).filter(isFullComponent).map(storeFlattened).reduce(concat, []);
259 | const props = target.filter(isNotModifier).filter(isProperty);
260 | const changedProps = target.filter(isChangedModifier).map(fromModifierToComponent).filter(isProperty);
261 | const componentProps = [...fullComponentProps, ...props, ...changedComponentProps, ...changedProps];
262 | const allChangedProps = [...changedComponentProps, ...changedProps].reduce((map, prop) => {
263 | const $ = Symbol();
264 | createShadow(prop, $);
265 | map.set(prop, $);
266 | return map;
267 | }, /* @__PURE__ */ new Map());
268 | return [componentProps, allChangedProps];
269 | };
270 | var defineSerializer = (target, maxBytes = 2e7) => {
271 | const worldSerializer = isWorld(target);
272 | let [componentProps, changedProps] = canonicalize(target);
273 | const buffer = new ArrayBuffer(maxBytes);
274 | const view = new DataView(buffer);
275 | const entityComponentCache = /* @__PURE__ */ new Map();
276 | return (ents) => {
277 | if (resized) {
278 | [componentProps, changedProps] = canonicalize(target);
279 | resized = false;
280 | }
281 | if (worldSerializer) {
282 | componentProps = [];
283 | target[$componentMap].forEach((c, component) => {
284 | if (component[$storeFlattened])
285 | componentProps.push(...component[$storeFlattened]);
286 | else
287 | componentProps.push(component);
288 | });
289 | }
290 | let world;
291 | if (Object.getOwnPropertySymbols(ents).includes($componentMap)) {
292 | world = ents;
293 | ents = ents[$entityArray];
294 | } else {
295 | world = eidToWorld.get(ents[0]);
296 | }
297 | let where = 0;
298 | if (!ents.length)
299 | return buffer.slice(0, where);
300 | const cache = /* @__PURE__ */ new Map();
301 | for (let pid = 0; pid < componentProps.length; pid++) {
302 | const prop = componentProps[pid];
303 | const component = prop[$storeBase]();
304 | const $diff = changedProps.get(prop);
305 | const shadow = $diff ? prop[$diff] : null;
306 | if (!cache.has(component))
307 | cache.set(component, /* @__PURE__ */ new Map());
308 | view.setUint8(where, pid);
309 | where += 1;
310 | const countWhere = where;
311 | where += 4;
312 | let writeCount = 0;
313 | for (let i = 0; i < ents.length; i++) {
314 | const eid = ents[i];
315 | let componentCache = entityComponentCache.get(eid);
316 | if (!componentCache)
317 | componentCache = entityComponentCache.set(eid, /* @__PURE__ */ new Set()).get(eid);
318 | componentCache.add(eid);
319 | const newlyAddedComponent = shadow && cache.get(component).get(eid) || !componentCache.has(component) && hasComponent(world, component, eid);
320 | cache.get(component).set(eid, newlyAddedComponent);
321 | if (newlyAddedComponent) {
322 | componentCache.add(component);
323 | } else if (!hasComponent(world, component, eid)) {
324 | componentCache.delete(component);
325 | continue;
326 | }
327 | const rewindWhere = where;
328 | view.setUint32(where, eid);
329 | where += 4;
330 | if (prop[$tagStore]) {
331 | writeCount++;
332 | continue;
333 | }
334 | if (ArrayBuffer.isView(prop[eid])) {
335 | const type = prop[eid].constructor.name.replace("Array", "");
336 | const indexType = prop[eid][$indexType];
337 | const indexBytes = prop[eid][$indexBytes];
338 | const countWhere2 = where;
339 | where += indexBytes;
340 | let arrayWriteCount = 0;
341 | for (let i2 = 0; i2 < prop[eid].length; i2++) {
342 | if (shadow) {
343 | const changed = shadow[eid][i2] !== prop[eid][i2];
344 | shadow[eid][i2] = prop[eid][i2];
345 | if (!changed && !newlyAddedComponent) {
346 | continue;
347 | }
348 | }
349 | view[`set${indexType}`](where, i2);
350 | where += indexBytes;
351 | const value = prop[eid][i2];
352 | view[`set${type}`](where, value);
353 | where += prop[eid].BYTES_PER_ELEMENT;
354 | arrayWriteCount++;
355 | }
356 | if (arrayWriteCount > 0) {
357 | view[`set${indexType}`](countWhere2, arrayWriteCount);
358 | writeCount++;
359 | } else {
360 | where = rewindWhere;
361 | continue;
362 | }
363 | } else {
364 | if (shadow) {
365 | const changed = shadow[eid] !== prop[eid];
366 | shadow[eid] = prop[eid];
367 | if (!changed && !newlyAddedComponent) {
368 | where = rewindWhere;
369 | continue;
370 | }
371 | }
372 | const type = prop.constructor.name.replace("Array", "");
373 | view[`set${type}`](where, prop[eid]);
374 | where += prop.BYTES_PER_ELEMENT;
375 | writeCount++;
376 | }
377 | }
378 | if (writeCount > 0) {
379 | view.setUint32(countWhere, writeCount);
380 | } else {
381 | where -= 5;
382 | }
383 | }
384 | return buffer.slice(0, where);
385 | };
386 | };
387 | var newEntities = /* @__PURE__ */ new Map();
388 | var defineDeserializer = (target) => {
389 | const isWorld2 = Object.getOwnPropertySymbols(target).includes($componentMap);
390 | let [componentProps] = canonicalize(target);
391 | const deserializedEntities = /* @__PURE__ */ new Set();
392 | return (world, packet, mode = 0) => {
393 | newEntities.clear();
394 | if (resized) {
395 | [componentProps] = canonicalize(target);
396 | resized = false;
397 | }
398 | if (isWorld2) {
399 | componentProps = [];
400 | target[$componentMap].forEach((c, component) => {
401 | if (component[$storeFlattened])
402 | componentProps.push(...component[$storeFlattened]);
403 | else
404 | componentProps.push(component);
405 | });
406 | }
407 | const localEntities = world[$localEntities];
408 | const localEntityLookup = world[$localEntityLookup];
409 | const view = new DataView(packet);
410 | let where = 0;
411 | while (where < packet.byteLength) {
412 | const pid = view.getUint8(where);
413 | where += 1;
414 | const entityCount = view.getUint32(where);
415 | where += 4;
416 | const prop = componentProps[pid];
417 | for (let i = 0; i < entityCount; i++) {
418 | let eid = view.getUint32(where);
419 | where += 4;
420 | if (mode === DESERIALIZE_MODE.MAP) {
421 | if (localEntities.has(eid)) {
422 | eid = localEntities.get(eid);
423 | } else if (newEntities.has(eid)) {
424 | eid = newEntities.get(eid);
425 | } else {
426 | const newEid = addEntity(world);
427 | localEntities.set(eid, newEid);
428 | localEntityLookup.set(newEid, eid);
429 | newEntities.set(eid, newEid);
430 | eid = newEid;
431 | }
432 | }
433 | if (mode === DESERIALIZE_MODE.APPEND || mode === DESERIALIZE_MODE.REPLACE && !world[$entitySparseSet].has(eid)) {
434 | const newEid = newEntities.get(eid) || addEntity(world);
435 | newEntities.set(eid, newEid);
436 | eid = newEid;
437 | }
438 | const component = prop[$storeBase]();
439 | if (!hasComponent(world, component, eid)) {
440 | addComponent(world, component, eid);
441 | }
442 | deserializedEntities.add(eid);
443 | if (component[$tagStore]) {
444 | continue;
445 | }
446 | if (ArrayBuffer.isView(prop[eid])) {
447 | const array = prop[eid];
448 | const count = view[`get${array[$indexType]}`](where);
449 | where += array[$indexBytes];
450 | for (let i2 = 0; i2 < count; i2++) {
451 | const index = view[`get${array[$indexType]}`](where);
452 | where += array[$indexBytes];
453 | const value = view[`get${array.constructor.name.replace("Array", "")}`](where);
454 | where += array.BYTES_PER_ELEMENT;
455 | if (prop[$isEidType]) {
456 | let localEid;
457 | if (localEntities.has(value)) {
458 | localEid = localEntities.get(value);
459 | } else if (newEntities.has(value)) {
460 | localEid = newEntities.get(value);
461 | } else {
462 | const newEid = addEntity(world);
463 | localEntities.set(value, newEid);
464 | localEntityLookup.set(newEid, value);
465 | newEntities.set(value, newEid);
466 | localEid = newEid;
467 | }
468 | prop[eid][index] = localEid;
469 | } else
470 | prop[eid][index] = value;
471 | }
472 | } else {
473 | const value = view[`get${prop.constructor.name.replace("Array", "")}`](where);
474 | where += prop.BYTES_PER_ELEMENT;
475 | if (prop[$isEidType]) {
476 | let localEid;
477 | if (localEntities.has(value)) {
478 | localEid = localEntities.get(value);
479 | } else if (newEntities.has(value)) {
480 | localEid = newEntities.get(value);
481 | } else {
482 | const newEid = addEntity(world);
483 | localEntities.set(value, newEid);
484 | localEntityLookup.set(newEid, value);
485 | newEntities.set(value, newEid);
486 | localEid = newEid;
487 | }
488 | prop[eid] = localEid;
489 | } else
490 | prop[eid] = value;
491 | }
492 | }
493 | }
494 | const ents = Array.from(deserializedEntities);
495 | deserializedEntities.clear();
496 | return ents;
497 | };
498 | };
499 |
500 | // src/Entity.js
501 | var $entityMasks = Symbol("entityMasks");
502 | var $entityComponents = Symbol("entityComponents");
503 | var $entitySparseSet = Symbol("entitySparseSet");
504 | var $entityArray = Symbol("entityArray");
505 | var $entityIndices = Symbol("entityIndices");
506 | var $removedEntities = Symbol("removedEntities");
507 | var defaultSize = 1e5;
508 | var globalEntityCursor = 0;
509 | var globalSize = defaultSize;
510 | var getGlobalSize = () => globalSize;
511 | var removed = [];
512 | var recycled = [];
513 | var defaultRemovedReuseThreshold = 0.01;
514 | var removedReuseThreshold = defaultRemovedReuseThreshold;
515 | var resetGlobals = () => {
516 | globalSize = defaultSize;
517 | globalEntityCursor = 0;
518 | removedReuseThreshold = defaultRemovedReuseThreshold;
519 | removed.length = 0;
520 | recycled.length = 0;
521 | };
522 | var setDefaultSize = (newSize) => {
523 | const oldSize = globalSize;
524 | defaultSize = newSize;
525 | resetGlobals();
526 | globalSize = newSize;
527 | resizeWorlds(newSize);
528 | setSerializationResized(true);
529 | };
530 | var setRemovedRecycleThreshold = (newThreshold) => {
531 | removedReuseThreshold = newThreshold;
532 | };
533 | var getEntityCursor = () => globalEntityCursor;
534 | var eidToWorld = /* @__PURE__ */ new Map();
535 | var flushRemovedEntities = (world) => {
536 | if (!world[$manualEntityRecycling]) {
537 | throw new Error("bitECS - cannot flush removed entities, enable feature with the enableManualEntityRecycling function");
538 | }
539 | removed.push(...recycled);
540 | recycled.length = 0;
541 | };
542 | var addEntity = (world) => {
543 | const eid = world[$manualEntityRecycling] ? removed.length ? removed.shift() : globalEntityCursor++ : removed.length > Math.round(globalSize * removedReuseThreshold) ? removed.shift() : globalEntityCursor++;
544 | if (eid > world[$size])
545 | throw new Error("bitECS - max entities reached");
546 | world[$entitySparseSet].add(eid);
547 | eidToWorld.set(eid, world);
548 | world[$notQueries].forEach((q) => {
549 | const match = queryCheckEntity(world, q, eid);
550 | if (match)
551 | queryAddEntity(q, eid);
552 | });
553 | world[$entityComponents].set(eid, /* @__PURE__ */ new Set());
554 | return eid;
555 | };
556 | var removeEntity = (world, eid) => {
557 | if (!world[$entitySparseSet].has(eid))
558 | return;
559 | world[$queries].forEach((q) => {
560 | queryRemoveEntity(world, q, eid);
561 | });
562 | if (world[$manualEntityRecycling])
563 | recycled.push(eid);
564 | else
565 | removed.push(eid);
566 | world[$entitySparseSet].remove(eid);
567 | world[$entityComponents].delete(eid);
568 | world[$localEntities].delete(world[$localEntityLookup].get(eid));
569 | world[$localEntityLookup].delete(eid);
570 | for (let i = 0; i < world[$entityMasks].length; i++)
571 | world[$entityMasks][i][eid] = 0;
572 | };
573 | var getEntityComponents = (world, eid) => {
574 | if (eid === void 0)
575 | throw new Error("bitECS - entity is undefined.");
576 | if (!world[$entitySparseSet].has(eid))
577 | throw new Error("bitECS - entity does not exist in the world.");
578 | return Array.from(world[$entityComponents].get(eid));
579 | };
580 | var entityExists = (world, eid) => world[$entitySparseSet].has(eid);
581 |
582 | // src/Query.js
583 | var $modifier = Symbol("$modifier");
584 | function modifier(c, mod) {
585 | const inner = () => [c, mod];
586 | inner[$modifier] = true;
587 | return inner;
588 | }
589 | var Not = (c) => modifier(c, "not");
590 | var Changed = (c) => modifier(c, "changed");
591 | function Any(...comps) {
592 | return function QueryAny() {
593 | return comps;
594 | };
595 | }
596 | function All(...comps) {
597 | return function QueryAll() {
598 | return comps;
599 | };
600 | }
601 | function None(...comps) {
602 | return function QueryNone() {
603 | return comps;
604 | };
605 | }
606 | var $queries = Symbol("queries");
607 | var $notQueries = Symbol("notQueries");
608 | var $queryAny = Symbol("queryAny");
609 | var $queryAll = Symbol("queryAll");
610 | var $queryNone = Symbol("queryNone");
611 | var $queryMap = Symbol("queryMap");
612 | var $dirtyQueries = Symbol("$dirtyQueries");
613 | var $queryComponents = Symbol("queryComponents");
614 | var $enterQuery = Symbol("enterQuery");
615 | var $exitQuery = Symbol("exitQuery");
616 | var empty = Object.freeze([]);
617 | var enterQuery = (query) => (world) => {
618 | if (!world[$queryMap].has(query))
619 | registerQuery(world, query);
620 | const q = world[$queryMap].get(query);
621 | if (q.entered.dense.length === 0) {
622 | return empty;
623 | } else {
624 | const results = q.entered.dense.slice();
625 | q.entered.reset();
626 | return results;
627 | }
628 | };
629 | var exitQuery = (query) => (world) => {
630 | if (!world[$queryMap].has(query))
631 | registerQuery(world, query);
632 | const q = world[$queryMap].get(query);
633 | if (q.exited.dense.length === 0) {
634 | return empty;
635 | } else {
636 | const results = q.exited.dense.slice();
637 | q.exited.reset();
638 | return results;
639 | }
640 | };
641 | var registerQuery = (world, query) => {
642 | const components2 = [];
643 | const notComponents = [];
644 | const changedComponents = [];
645 | query[$queryComponents].forEach((c) => {
646 | if (typeof c === "function" && c[$modifier]) {
647 | const [comp, mod] = c();
648 | if (!world[$componentMap].has(comp))
649 | registerComponent(world, comp);
650 | if (mod === "not") {
651 | notComponents.push(comp);
652 | }
653 | if (mod === "changed") {
654 | changedComponents.push(comp);
655 | components2.push(comp);
656 | }
657 | } else {
658 | if (!world[$componentMap].has(c))
659 | registerComponent(world, c);
660 | components2.push(c);
661 | }
662 | });
663 | const mapComponents = (c) => world[$componentMap].get(c);
664 | const allComponents = components2.concat(notComponents).map(mapComponents);
665 | const sparseSet = SparseSet();
666 | const archetypes = [];
667 | const changed = [];
668 | const toRemove = SparseSet();
669 | const entered = SparseSet();
670 | const exited = SparseSet();
671 | const generations = allComponents.map((c) => c.generationId).reduce((a, v) => {
672 | if (a.includes(v))
673 | return a;
674 | a.push(v);
675 | return a;
676 | }, []);
677 | const reduceBitflags = (a, c) => {
678 | if (!a[c.generationId])
679 | a[c.generationId] = 0;
680 | a[c.generationId] |= c.bitflag;
681 | return a;
682 | };
683 | const masks = components2.map(mapComponents).reduce(reduceBitflags, {});
684 | const notMasks = notComponents.map(mapComponents).reduce(reduceBitflags, {});
685 | const hasMasks = allComponents.reduce(reduceBitflags, {});
686 | const flatProps = components2.filter((c) => !c[$tagStore]).map((c) => Object.getOwnPropertySymbols(c).includes($storeFlattened) ? c[$storeFlattened] : [c]).reduce((a, v) => a.concat(v), []);
687 | const shadows = [];
688 | const q = Object.assign(sparseSet, {
689 | archetypes,
690 | changed,
691 | components: components2,
692 | notComponents,
693 | changedComponents,
694 | allComponents,
695 | masks,
696 | notMasks,
697 | hasMasks,
698 | generations,
699 | flatProps,
700 | toRemove,
701 | entered,
702 | exited,
703 | shadows
704 | });
705 | world[$queryMap].set(query, q);
706 | world[$queries].add(q);
707 | allComponents.forEach((c) => {
708 | c.queries.add(q);
709 | });
710 | if (notComponents.length)
711 | world[$notQueries].add(q);
712 | for (let eid = 0; eid < getEntityCursor(); eid++) {
713 | if (!world[$entitySparseSet].has(eid))
714 | continue;
715 | const match = queryCheckEntity(world, q, eid);
716 | if (match)
717 | queryAddEntity(q, eid);
718 | }
719 | };
720 | var generateShadow = (q, pid) => {
721 | const $ = Symbol();
722 | const prop = q.flatProps[pid];
723 | createShadow(prop, $);
724 | q.shadows[pid] = prop[$];
725 | return prop[$];
726 | };
727 | var diff = (q, clearDiff) => {
728 | if (clearDiff)
729 | q.changed = [];
730 | const { flatProps, shadows } = q;
731 | for (let i = 0; i < q.dense.length; i++) {
732 | const eid = q.dense[i];
733 | let dirty = false;
734 | for (let pid = 0; pid < flatProps.length; pid++) {
735 | const prop = flatProps[pid];
736 | const shadow = shadows[pid] || generateShadow(q, pid);
737 | if (ArrayBuffer.isView(prop[eid])) {
738 | for (let i2 = 0; i2 < prop[eid].length; i2++) {
739 | if (prop[eid][i2] !== shadow[eid][i2]) {
740 | dirty = true;
741 | break;
742 | }
743 | }
744 | shadow[eid].set(prop[eid]);
745 | } else {
746 | if (prop[eid] !== shadow[eid]) {
747 | dirty = true;
748 | shadow[eid] = prop[eid];
749 | }
750 | }
751 | }
752 | if (dirty)
753 | q.changed.push(eid);
754 | }
755 | return q.changed;
756 | };
757 | var flatten = (a, v) => a.concat(v);
758 | var aggregateComponentsFor = (mod) => (x) => x.filter((f) => f.name === mod().constructor.name).reduce(flatten);
759 | var getAnyComponents = aggregateComponentsFor(Any);
760 | var getAllComponents = aggregateComponentsFor(All);
761 | var getNoneComponents = aggregateComponentsFor(None);
762 | var defineQuery = (...args) => {
763 | let components2;
764 | let any, all, none;
765 | if (Array.isArray(args[0])) {
766 | components2 = args[0];
767 | } else {
768 | }
769 | if (components2 === void 0 || components2[$componentMap] !== void 0) {
770 | return (world) => world ? world[$entityArray] : components2[$entityArray];
771 | }
772 | const query = function(world, clearDiff = true) {
773 | if (!world[$queryMap].has(query))
774 | registerQuery(world, query);
775 | const q = world[$queryMap].get(query);
776 | commitRemovals(world);
777 | if (q.changedComponents.length)
778 | return diff(q, clearDiff);
779 | return q.dense;
780 | };
781 | query[$queryComponents] = components2;
782 | query[$queryAny] = any;
783 | query[$queryAll] = all;
784 | query[$queryNone] = none;
785 | return query;
786 | };
787 | var queryCheckEntity = (world, q, eid) => {
788 | const { masks, notMasks, generations } = q;
789 | let or = 0;
790 | for (let i = 0; i < generations.length; i++) {
791 | const generationId = generations[i];
792 | const qMask = masks[generationId];
793 | const qNotMask = notMasks[generationId];
794 | const eMask = world[$entityMasks][generationId][eid];
795 | if (qNotMask && (eMask & qNotMask) !== 0) {
796 | return false;
797 | }
798 | if (qMask && (eMask & qMask) !== qMask) {
799 | return false;
800 | }
801 | }
802 | return true;
803 | };
804 | var queryAddEntity = (q, eid) => {
805 | q.toRemove.remove(eid);
806 | q.entered.add(eid);
807 | q.add(eid);
808 | };
809 | var queryCommitRemovals = (q) => {
810 | for (let i = q.toRemove.dense.length - 1; i >= 0; i--) {
811 | const eid = q.toRemove.dense[i];
812 | q.toRemove.remove(eid);
813 | q.remove(eid);
814 | }
815 | };
816 | var commitRemovals = (world) => {
817 | if (!world[$dirtyQueries].size)
818 | return;
819 | world[$dirtyQueries].forEach(queryCommitRemovals);
820 | world[$dirtyQueries].clear();
821 | };
822 | var queryRemoveEntity = (world, q, eid) => {
823 | if (!q.has(eid) || q.toRemove.has(eid))
824 | return;
825 | q.toRemove.add(eid);
826 | world[$dirtyQueries].add(q);
827 | q.exited.add(eid);
828 | };
829 | var resetChangedQuery = (world, query) => {
830 | const q = world[$queryMap].get(query);
831 | q.changed = [];
832 | };
833 | var removeQuery = (world, query) => {
834 | const q = world[$queryMap].get(query);
835 | world[$queries].delete(q);
836 | world[$queryMap].delete(query);
837 | };
838 |
839 | // src/Component.js
840 | var $componentMap = Symbol("componentMap");
841 | var components = [];
842 | var defineComponent = (schema, size) => {
843 | const component = createStore(schema, size || getGlobalSize());
844 | if (schema && Object.keys(schema).length)
845 | components.push(component);
846 | return component;
847 | };
848 | var incrementBitflag = (world) => {
849 | world[$bitflag] *= 2;
850 | if (world[$bitflag] >= 2 ** 31) {
851 | world[$bitflag] = 1;
852 | world[$entityMasks].push(new Uint32Array(world[$size]));
853 | }
854 | };
855 | var registerComponent = (world, component) => {
856 | if (!component)
857 | throw new Error(`bitECS - Cannot register null or undefined component`);
858 | const queries = /* @__PURE__ */ new Set();
859 | const notQueries = /* @__PURE__ */ new Set();
860 | const changedQueries = /* @__PURE__ */ new Set();
861 | world[$queries].forEach((q) => {
862 | if (q.allComponents.includes(component)) {
863 | queries.add(q);
864 | }
865 | });
866 | world[$componentMap].set(component, {
867 | generationId: world[$entityMasks].length - 1,
868 | bitflag: world[$bitflag],
869 | store: component,
870 | queries,
871 | notQueries,
872 | changedQueries
873 | });
874 | incrementBitflag(world);
875 | };
876 | var registerComponents = (world, components2) => {
877 | components2.forEach((c) => registerComponent(world, c));
878 | };
879 | var hasComponent = (world, component, eid) => {
880 | const registeredComponent = world[$componentMap].get(component);
881 | if (!registeredComponent)
882 | return false;
883 | const { generationId, bitflag } = registeredComponent;
884 | const mask = world[$entityMasks][generationId][eid];
885 | return (mask & bitflag) === bitflag;
886 | };
887 | var addComponent = (world, component, eid, reset = false) => {
888 | if (eid === void 0)
889 | throw new Error("bitECS - entity is undefined.");
890 | if (!world[$entitySparseSet].has(eid))
891 | throw new Error("bitECS - entity does not exist in the world.");
892 | if (!world[$componentMap].has(component))
893 | registerComponent(world, component);
894 | if (hasComponent(world, component, eid))
895 | return;
896 | const c = world[$componentMap].get(component);
897 | const { generationId, bitflag, queries, notQueries } = c;
898 | world[$entityMasks][generationId][eid] |= bitflag;
899 | queries.forEach((q) => {
900 | q.toRemove.remove(eid);
901 | const match = queryCheckEntity(world, q, eid);
902 | if (match) {
903 | q.exited.remove(eid);
904 | queryAddEntity(q, eid);
905 | }
906 | if (!match) {
907 | q.entered.remove(eid);
908 | queryRemoveEntity(world, q, eid);
909 | }
910 | });
911 | world[$entityComponents].get(eid).add(component);
912 | if (reset)
913 | resetStoreFor(component, eid);
914 | };
915 | var removeComponent = (world, component, eid, reset = true) => {
916 | if (eid === void 0)
917 | throw new Error("bitECS - entity is undefined.");
918 | if (!world[$entitySparseSet].has(eid))
919 | throw new Error("bitECS - entity does not exist in the world.");
920 | if (!hasComponent(world, component, eid))
921 | return;
922 | const c = world[$componentMap].get(component);
923 | const { generationId, bitflag, queries } = c;
924 | world[$entityMasks][generationId][eid] &= ~bitflag;
925 | queries.forEach((q) => {
926 | q.toRemove.remove(eid);
927 | const match = queryCheckEntity(world, q, eid);
928 | if (match) {
929 | q.exited.remove(eid);
930 | queryAddEntity(q, eid);
931 | }
932 | if (!match) {
933 | q.entered.remove(eid);
934 | queryRemoveEntity(world, q, eid);
935 | }
936 | });
937 | world[$entityComponents].get(eid).delete(component);
938 | if (reset)
939 | resetStoreFor(component, eid);
940 | };
941 |
942 | // src/World.js
943 | var $size = Symbol("size");
944 | var $resizeThreshold = Symbol("resizeThreshold");
945 | var $bitflag = Symbol("bitflag");
946 | var $archetypes = Symbol("archetypes");
947 | var $localEntities = Symbol("localEntities");
948 | var $localEntityLookup = Symbol("localEntityLookup");
949 | var $manualEntityRecycling = Symbol("manualEntityRecycling");
950 | var worlds = [];
951 | var resizeWorlds = (size) => {
952 | worlds.forEach((world) => {
953 | world[$size] = size;
954 | for (let i = 0; i < world[$entityMasks].length; i++) {
955 | const masks = world[$entityMasks][i];
956 | world[$entityMasks][i] = resize(masks, size);
957 | }
958 | world[$resizeThreshold] = world[$size] - world[$size] / 5;
959 | });
960 | };
961 | var createWorld = (...args) => {
962 | const world = typeof args[0] === "object" ? args[0] : {};
963 | const size = typeof args[0] === "number" ? args[0] : typeof args[1] === "number" ? args[1] : getGlobalSize();
964 | resetWorld(world, size);
965 | worlds.push(world);
966 | return world;
967 | };
968 | var enableManualEntityRecycling = (world) => {
969 | world[$manualEntityRecycling] = true;
970 | };
971 | var resetWorld = (world, size = getGlobalSize()) => {
972 | world[$size] = size;
973 | if (world[$entityArray])
974 | world[$entityArray].forEach((eid) => removeEntity(world, eid));
975 | world[$entityMasks] = [new Uint32Array(size)];
976 | world[$entityComponents] = /* @__PURE__ */ new Map();
977 | world[$archetypes] = [];
978 | world[$entitySparseSet] = SparseSet();
979 | world[$entityArray] = world[$entitySparseSet].dense;
980 | world[$bitflag] = 1;
981 | world[$componentMap] = /* @__PURE__ */ new Map();
982 | world[$queryMap] = /* @__PURE__ */ new Map();
983 | world[$queries] = /* @__PURE__ */ new Set();
984 | world[$notQueries] = /* @__PURE__ */ new Set();
985 | world[$dirtyQueries] = /* @__PURE__ */ new Set();
986 | world[$localEntities] = /* @__PURE__ */ new Map();
987 | world[$localEntityLookup] = /* @__PURE__ */ new Map();
988 | world[$manualEntityRecycling] = false;
989 | return world;
990 | };
991 | var deleteWorld = (world) => {
992 | Object.getOwnPropertySymbols(world).forEach(($) => {
993 | delete world[$];
994 | });
995 | Object.keys(world).forEach((key) => {
996 | delete world[key];
997 | });
998 | worlds.splice(worlds.indexOf(world), 1);
999 | };
1000 | var getWorldComponents = (world) => Array.from(world[$componentMap].keys());
1001 | var getAllEntities = (world) => world[$entitySparseSet].dense.slice(0);
1002 |
1003 | // src/System.js
1004 | var defineSystem = (update) => (world, ...args) => {
1005 | update(world, ...args);
1006 | return world;
1007 | };
1008 |
1009 | // src/index.js
1010 | var pipe = (...fns) => (input) => {
1011 | let tmp = input;
1012 | for (let i = 0; i < fns.length; i++) {
1013 | const fn = fns[i];
1014 | tmp = fn(tmp);
1015 | }
1016 | return tmp;
1017 | };
1018 | var Types = TYPES_ENUM;
1019 | export {
1020 | Changed,
1021 | DESERIALIZE_MODE,
1022 | Not,
1023 | Types,
1024 | addComponent,
1025 | addEntity,
1026 | commitRemovals,
1027 | createWorld,
1028 | defineComponent,
1029 | defineDeserializer,
1030 | defineQuery,
1031 | defineSerializer,
1032 | defineSystem,
1033 | deleteWorld,
1034 | enableManualEntityRecycling,
1035 | enterQuery,
1036 | entityExists,
1037 | exitQuery,
1038 | flushRemovedEntities,
1039 | getAllEntities,
1040 | getEntityComponents,
1041 | getWorldComponents,
1042 | hasComponent,
1043 | parentArray,
1044 | pipe,
1045 | registerComponent,
1046 | registerComponents,
1047 | removeComponent,
1048 | removeEntity,
1049 | removeQuery,
1050 | resetChangedQuery,
1051 | resetGlobals,
1052 | resetWorld,
1053 | setDefaultSize,
1054 | setRemovedRecycleThreshold
1055 | };
1056 | //# sourceMappingURL=index.mjs.map
1057 |
--------------------------------------------------------------------------------
/docs/API.md:
--------------------------------------------------------------------------------
1 | ## Constants
2 |
3 |
4 | - defineComponent ⇒
object
5 | Defines a new component store.
6 |
7 | - registerComponent
8 | Registers a component with a world.
9 |
10 | - registerComponents
11 | Registers multiple components with a world.
12 |
13 | - hasComponent ⇒
boolean
14 | Checks if an entity has a component.
15 |
16 | - addComponent
17 | Adds a component to an entity
18 |
19 | - removeComponent
20 | Removes a component from an entity and resets component state unless otherwise specified.
21 |
22 | - setDefaultSize
23 | Sets the default maximum number of entities for worlds and component stores.
24 |
25 | - setRemovedRecycleThreshold
26 | Sets the number of entities that must be removed before removed entity ids begin to be recycled.
27 | This should be set to as a % (0-1) of defaultSize
that you would never likely remove/add on a single frame.
28 |
29 | - addEntity ⇒
number
30 | Adds a new entity to the specified world.
31 |
32 | - removeEntity
33 | Removes an existing entity from the specified world.
34 |
35 | - getEntityComponents
36 | Returns an array of components that an entity possesses.
37 |
38 | - entityExists
39 | Checks the existence of an entity in a world
40 |
41 | - enterQuery ⇒
function
42 | Given an existing query, returns a new function which returns entities who have been added to the given query since the last call of the function.
43 |
44 | - exitQuery ⇒
function
45 | Given an existing query, returns a new function which returns entities who have been removed from the given query since the last call of the function.
46 |
47 | - defineQuery ⇒
function
48 | Defines a query function which returns a matching set of entities when called on a world.
49 |
50 | - resetChangedQuery
51 | Resets a Changed-based query, clearing the underlying list of changed entities.
52 |
53 | - removeQuery
54 | Removes a query from a world.
55 |
56 | - defineSerializer ⇒
function
57 | Defines a new serializer which targets the given components to serialize the data of when called on a world or array of EIDs.
58 |
59 | - defineDeserializer ⇒
function
60 | Defines a new deserializer which targets the given components to deserialize onto a given world.
61 |
62 | - defineSystem ⇒
function
63 | Defines a new system function.
64 |
65 | - createWorld ⇒
object
66 | Creates a new world.
67 |
68 | - resetWorld ⇒
object
69 | Resets a world.
70 |
71 | - deleteWorld
72 | Deletes a world.
73 |
74 | - getWorldComponents ⇒
75 | Returns all components registered to a world
76 |
77 | - getAllEntities ⇒
78 | Returns all existing entities in a world
79 |
80 |
81 |
82 |
83 |
84 |
85 | ## defineComponent ⇒ object
86 | > Defines a new component store.
87 |
88 |
89 | | Param | Type |
90 | | --- | --- |
91 | | schema | object
|
92 |
93 |
94 |
95 |
96 | ## registerComponent
97 | > Registers a component with a world.
98 |
99 |
100 | | Param | Type |
101 | | --- | --- |
102 | | world | World
|
103 | | component | Component
|
104 |
105 |
106 |
107 |
108 | ## registerComponents
109 | > Registers multiple components with a world.
110 |
111 |
112 | | Param | Type |
113 | | --- | --- |
114 | | world | World
|
115 | | components | Component
|
116 |
117 |
118 |
119 |
120 | ## hasComponent ⇒ boolean
121 | > Checks if an entity has a component.
122 |
123 |
124 | | Param | Type |
125 | | --- | --- |
126 | | world | World
|
127 | | component | Component
|
128 | | eid | number
|
129 |
130 |
131 |
132 |
133 | ## addComponent
134 | > Adds a component to an entity
135 |
136 |
137 | | Param | Type | Default |
138 | | --- | --- | --- |
139 | | world | World
| |
140 | | component | Component
| |
141 | | eid | number
| |
142 | | [reset] | boolean
| false
|
143 |
144 |
145 |
146 |
147 | ## removeComponent
148 | > Removes a component from an entity and resets component state unless otherwise specified.
149 |
150 |
151 | | Param | Type | Default |
152 | | --- | --- | --- |
153 | | world | World
| |
154 | | component | Component
| |
155 | | eid | number
| |
156 | | [reset] | boolean
| true
|
157 |
158 |
159 |
160 |
161 | ## setDefaultSize
162 | > Sets the default maximum number of entities for worlds and component stores.
163 |
164 |
165 | | Param | Type |
166 | | --- | --- |
167 | | newSize | number
|
168 |
169 |
170 |
171 |
172 | ## setRemovedRecycleThreshold
173 | > Sets the number of entities that must be removed before removed entity ids begin to be recycled.
> This should be set to as a % (0-1) of `defaultSize` that you would never likely remove/add on a single frame.
174 |
175 |
176 | | Param | Type |
177 | | --- | --- |
178 | | newThreshold | number
|
179 |
180 |
181 |
182 |
183 | ## addEntity ⇒ number
184 | > Adds a new entity to the specified world.
185 |
186 | **Returns**: number
- eid
187 |
188 | | Param | Type |
189 | | --- | --- |
190 | | world | World
|
191 |
192 |
193 |
194 |
195 | ## removeEntity
196 | > Removes an existing entity from the specified world.
197 |
198 |
199 | | Param | Type |
200 | | --- | --- |
201 | | world | World
|
202 | | eid | number
|
203 |
204 |
205 |
206 |
207 | ## getEntityComponents
208 | > Returns an array of components that an entity possesses.
209 |
210 |
211 | | Param | Type |
212 | | --- | --- |
213 | | world | \*
|
214 | | eid | \*
|
215 |
216 |
217 |
218 |
219 | ## entityExists
220 | > Checks the existence of an entity in a world
221 |
222 |
223 | | Param | Type |
224 | | --- | --- |
225 | | world | World
|
226 | | eid | number
|
227 |
228 |
229 |
230 |
231 | ## enterQuery ⇒ function
232 | > Given an existing query, returns a new function which returns entities who have been added to the given query since the last call of the function.
233 |
234 | **Returns**: function
- enteredQuery
235 |
236 | | Param | Type |
237 | | --- | --- |
238 | | query | function
|
239 |
240 |
241 |
242 |
243 | ## exitQuery ⇒ function
244 | > Given an existing query, returns a new function which returns entities who have been removed from the given query since the last call of the function.
245 |
246 | **Returns**: function
- enteredQuery
247 |
248 | | Param | Type |
249 | | --- | --- |
250 | | query | function
|
251 |
252 |
253 |
254 |
255 | ## defineQuery ⇒ function
256 | > Defines a query function which returns a matching set of entities when called on a world.
257 |
258 | **Returns**: function
- query
259 |
260 | | Param | Type |
261 | | --- | --- |
262 | | components | array
|
263 |
264 |
265 |
266 |
267 | ## resetChangedQuery
268 | > Resets a Changed-based query, clearing the underlying list of changed entities.
269 |
270 |
271 | | Param | Type |
272 | | --- | --- |
273 | | world | World
|
274 | | query | function
|
275 |
276 |
277 |
278 |
279 | ## removeQuery
280 | > Removes a query from a world.
281 |
282 |
283 | | Param | Type |
284 | | --- | --- |
285 | | world | World
|
286 | | query | function
|
287 |
288 |
289 |
290 |
291 | ## defineSerializer ⇒ function
292 | > Defines a new serializer which targets the given components to serialize the data of when called on a world or array of EIDs.
293 |
294 | **Returns**: function
- serializer
295 |
296 | | Param | Type | Default |
297 | | --- | --- | --- |
298 | | target | object
, array
| |
299 | | [maxBytes] | number
| 20000000
|
300 |
301 |
302 |
303 |
304 | ## defineDeserializer ⇒ function
305 | > Defines a new deserializer which targets the given components to deserialize onto a given world.
306 |
307 | **Returns**: function
- deserializer
308 |
309 | | Param | Type |
310 | | --- | --- |
311 | | target | object
, array
|
312 |
313 |
314 |
315 |
316 | ## defineSystem ⇒ function
317 | > Defines a new system function.
318 |
319 |
320 | | Param | Type |
321 | | --- | --- |
322 | | update | function
|
323 |
324 |
325 |
326 |
327 | ## createWorld ⇒ object
328 | > Creates a new world.
329 |
330 |
331 |
332 |
333 | ## resetWorld ⇒ object
334 | > Resets a world.
335 |
336 |
337 | | Param | Type |
338 | | --- | --- |
339 | | world | World
|
340 |
341 |
342 |
343 |
344 | ## deleteWorld
345 | > Deletes a world.
346 |
347 |
348 | | Param | Type |
349 | | --- | --- |
350 | | world | World
|
351 |
352 |
353 |
354 |
355 | ## getWorldComponents ⇒
356 | > Returns all components registered to a world
357 |
358 | **Returns**: Array
359 |
360 | | Param | Type |
361 | | --- | --- |
362 | | world | World
|
363 |
364 |
365 |
366 |
367 | ## getAllEntities ⇒
368 | > Returns all existing entities in a world
369 |
370 | **Returns**: Array
371 |
372 | | Param | Type |
373 | | --- | --- |
374 | | world | World
|
375 |
376 |
--------------------------------------------------------------------------------
/docs/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## v0.3.40
2 |
3 | 08-07-2023
4 |
5 | https://github.com/NateTheGreatt/bitECS/compare/07080384e909112025b2f255b1edb6c093513ec0...89486fc9e2da9003077d314367544479e158f41d
6 |
7 | ## v0.3.39
8 |
9 | skipped
10 |
11 | ## v0.3.38
12 |
13 | 03-31-2022
14 |
15 | ### Changed
16 |
17 | - removed `any` property definition from `IComponent` type def for stricter typings
18 |
19 | ## v0.3.37
20 |
21 | 03-14-2022
22 |
23 | ### Added
24 |
25 | - `getAllEntities` function which returns all existing entities in a world
26 |
27 | ### Changed
28 |
29 | - cut initial memory footprint in half by lazily generating shadow state for `Changed` queries
30 | - removed `any` property definition from `IWorld` def for stricter typings
31 |
32 | ## v0.3.36
33 |
34 | 03-10-2022
35 |
36 | ### Added
37 |
38 | - typings
39 | - `getWorldComponents`
40 | - `entityExists`
41 |
42 | ### Changed
43 |
44 | - `addComponent` no longer clears component data by default
45 | - `removeComponent` now clears component data by default
46 | - components which are removed and then re-added in between query calls will appear in both enter and exit queries
47 |
48 | ### Fixed
49 |
50 | - typings
51 | - `createWorld` sizing additions
52 | - `addComponent` sizing additions
53 |
54 | ## v0.3.35
55 |
56 | 02-26-2022
57 |
58 | ### Added
59 |
60 | - `entityExists` - checks the existence of entities
61 | - `getWorldComponents` - returns all components registered to a world
62 |
63 | ### Changed
64 |
65 | - `createWorld` now takes a `size` argument
66 | - `defineComponent` now takes a `size` argument
67 |
68 | ### Fixed
69 |
70 | - entity IDs are now recycled after 1% of max entities have been removed to prevent false-positive returns from `entityExists`
71 |
--------------------------------------------------------------------------------
/docs/FAQ.md:
--------------------------------------------------------------------------------
1 | # FAQ
2 |
3 | ### Is there a string type for components?
4 |
5 | Strings are expensive and usually unnecessary to have the ECS handle. For enum type strings, it is advised to create a mapping of integer to string, and store the integer in the component data as a reference to a string. This makes string serialization minimal and fast.
6 |
7 | Otherwise, you can define a preallocated `ui8` array and encode strings into that with a `TextEncoder`.
8 | ```js
9 | const maxStringLength = 32
10 | const SomeComponent = defineComponent({ string: [Types.ui8, maxStringLength] })
11 | const encoder = new TextEncoder()
12 | SomeComponent.string[eid].set(encoder.encode("hello, world!"))
13 | ```
14 |
15 | ### How do I set an undefined value for a property of type `eid`?
16 |
17 | The `eid` type is defined internally as an unsigned integer, so it cannot have an undefined or null value and setting it to a negative value will cause it to overflow to `INT_MAX`. Instead, create a "null entity" directly after world creation. This will reserve the `eid` of `0` that you can then use to represent an undefined `eid` property.
18 |
19 | ### Can I set default values for my component's properties?
20 |
21 | You cannot set default values via `addComponent` calls. You must either create functions that explicitly set default values after adding the component, or you can achieve deferred default values by utilizing enter queries:
22 |
23 | ```js
24 | const SomeComponent = defineComponent({ value: Types.f32 })
25 | const someQuery = defineQuery([SomeComponent])
26 | const enterSomeQuery = enterQuery(someQuery)
27 |
28 | const setDefaultValuesForSomeComponent = eid => {
29 | SomeComponent.value[eid] = 1
30 | }
31 |
32 | enterSomeQuery.forEach(setDefaultValuesForSomeComponent)
33 | ```
34 |
--------------------------------------------------------------------------------
/docs/INTRO.md:
--------------------------------------------------------------------------------
1 |
2 | ## 🗺 Overview
3 |
4 | Essentials of the API:
5 |
6 | ```js
7 | import {
8 |
9 | createWorld,
10 | addEntity,
11 | removeEntity,
12 |
13 | Types,
14 |
15 | defineComponent,
16 | addComponent,
17 | removeComponent,
18 | hasComponent,
19 |
20 | defineQuery,
21 | Changed,
22 | Not,
23 | enterQuery,
24 | exitQuery,
25 |
26 | defineSerializer,
27 | defineDeserializer,
28 |
29 | pipe,
30 |
31 | } from 'bitecs'
32 | ```
33 |
34 | ## 🌐 World
35 |
36 | A world represents a set of entities and the components that they each possess.
37 |
38 | Worlds do not store actual component data, only their relationships with entities.
39 |
40 | Any number of worlds can be created. An empty object is returned which you can use as a context.
41 |
42 | ```js
43 | const world = createWorld()
44 |
45 | world.name = 'MyWorld'
46 | ```
47 |
48 |
49 | ## 👾 Entity
50 |
51 | An entity is an integer, technically a pointer, which components can be associated with.
52 |
53 | Entities are accessed via queries, components of whom are mutated with systems.
54 |
55 | Add entities to the world:
56 | ```js
57 | const eid = addEntity(world)
58 | const eid2 = addEntity(world)
59 | ```
60 | Remove entities from the world:
61 | ```js
62 | removeEntity(world, eid2)
63 | ```
64 |
65 |
66 | ## 📦 Component
67 |
68 | Components are pure data and added to entities to give them state.
69 |
70 | The object returned from `defineComponent` is a SoA (Structure of Arrays). This is what actually stores the component data.
71 |
72 | Define component stores:
73 | ```js
74 | const Vector3 = { x: Types.f32, y: Types.f32, z: Types.f32 }
75 | const Position = defineComponent(Vector3)
76 | const Velocity = defineComponent(Vector3)
77 | const List = defineComponent({ values: [Types.f32, 3] }) // [type, length]
78 | const Tag = defineComponent()
79 | const Reference = defineComponent({ entity: Types.eid }) // Types.eid is used as a reference type
80 | ```
81 |
82 | Add components to an entity in a world:
83 | ```js
84 | addComponent(world, Position, eid)
85 | addComponent(world, Velocity, eid)
86 | addComponent(world, List, eid)
87 | addComponent(world, Tag, eid)
88 | addComponent(world, Reference, eid)
89 | ```
90 |
91 | Component data is accessed directly via `eid` which is how high performance iteration is achieved:
92 | ```js
93 | Position.x[eid] = 1
94 | Position.y[eid] = 1
95 | ```
96 |
97 | References to other entities can be stored as such:
98 | ```js
99 | Reference.entity[eid] = eid2
100 | ```
101 |
102 | Array types are regular fixed-size TypedArrays:
103 | ```js
104 | List.values[eid].set([1,2,3])
105 | console.log(List.values[eid]) // => Float32Array(3) [ 1, 2, 3 ]
106 | ```
107 |
108 | ## 👥 Component Proxy
109 |
110 | Component proxies are a way to interact with component data using regular objects while maintaining high performance iteration. Not to be confused with ES6 `Proxy`, but the behavior is basically identical with faster iteration speeds.
111 |
112 | This enables cleaner syntax, component references, and enhanced interoperability with other libraries.
113 |
114 | Comes at the cost of some boilerplate and a very slight performance hit (still faster than regular objects tho).
115 |
116 | ⚠ Proxy instances must be reused to maintain high performance iteration.
117 |
118 | ```js
119 | class Vector3Proxy {
120 | constructor(store, eid) {
121 | this.eid = eid
122 | this.store = store
123 | }
124 | get x () { return this.store.x[this.eid] }
125 | set x (val) { this.store.x[this.eid] = val }
126 | get y () { return this.store.y[this.eid] }
127 | set y (val) { this.store.y[this.eid] = val }
128 | get z () { return this.store.z[this.eid] }
129 | set z (val) { this.store.z[this.eid] = val }
130 | }
131 |
132 | class PositionProxy extends Vector3Proxy {
133 | constructor(eid) { super(Position, eid) }
134 | }
135 |
136 | class VelocityProxy extends Vector3Proxy {
137 | constructor(eid) { super(Velocity, eid) }
138 | }
139 |
140 | const position = new PositionProxy(eid)
141 | const velocity = new VelocityProxy(eid)
142 |
143 | position.x = 123
144 |
145 | console.log(Position.x[eid]) // => 123
146 |
147 | // reuse proxies simply by resetting the eid
148 | position.eid = eid2
149 |
150 | position.x = 456
151 |
152 | console.log(Position.x[eid2]) // => 456
153 | ```
154 |
155 | ## 🔍 Query
156 |
157 | A query is defined with components and is used to obtain a specific set of entities from a world.
158 |
159 | Define a query:
160 | ```js
161 | const movementQuery = defineQuery([Position, Velocity])
162 | ```
163 |
164 | Use the query on a world to obtain an array of entities with those components:
165 | ```js
166 | const ents = movementQuery(world)
167 | ```
168 |
169 | Wrapping a component with the `Not` modifier defines a query which returns entities who explicitly do not have the component:
170 | ```js
171 | const positionWithoutVelocityQuery = defineQuery([ Position, Not(Velocity) ])
172 | ```
173 |
174 | Wrapping a component with the `Change` modifier creates a query which returns entities who are marked as changed since last call of the function:
175 |
176 | ⚠ This performs an expensive diff. Use manual dirty flags for more performant mutation detection.
177 | ```js
178 | const changedPositionQuery = defineQuery([ Changed(Position) ])
179 |
180 | let ents = changedPositionQuery(world)
181 | console.log(ents) // => []
182 |
183 | Position.x[eid]++
184 |
185 | ents = changedPositionQuery(world)
186 | console.log(ents) // => [0]
187 | ```
188 |
189 |
190 | `enterQuery` returns a function which can be used to capture entities whose components match the query:
191 | ```js
192 | const enteredMovementQuery = enterQuery(movementQuery)
193 | const enteredEnts = enteredMovementQuery(world)
194 | ```
195 |
196 | `exitQuery` returns a function which can be used to capture entities whose components no longer match the query:
197 | ```js
198 | const exitedMovementQuery = exitQuery(movementQuery)
199 | const exitedEnts = exitedMovementQuery(world)
200 | ```
201 |
202 |
203 | ## 🛸 System
204 |
205 | Systems are regular functions which are run against a world to update component state of entities, or anything else.
206 |
207 | Queries should be used inside of system functions to obtain a relevant set of entities and perform operations on their component data.
208 |
209 | While not required, it is greatly encouraged that you keep all component data mutations inside of systems.
210 |
211 | Define a system that moves entity positions based on their velocity:
212 | ```js
213 | const movementSystem = (world) => {
214 | // optionally apply logic to entities added to the query
215 | const entered = enteredMovementQuery(world)
216 | for (let i = 0; i < entered.length; i++) {
217 | const eid = ents[i]
218 | // ...
219 | }
220 |
221 | // apply system logic
222 | const ents = movementQuery(world)
223 | for (let i = 0; i < ents.length; i++) {
224 | const eid = ents[i]
225 |
226 | // operate directly on SoA data
227 | Position.x[eid] += Velocity.x[eid]
228 | Position.y[eid] += Velocity.y[eid]
229 |
230 | // or reuse component proxies by resetting the eid for each proxy
231 | position.eid = velocity.eid = eid
232 | position.x += velocity.x
233 | position.y += velocity.y
234 | }
235 |
236 | // optionally apply logic to entities removed from the query
237 | const exited = exitedMovementQuery(world)
238 | for (let i = 0; i < exited.length; i++) {
239 | const eid = ents[i]
240 | // ...
241 | }
242 |
243 | return world
244 | }
245 | ```
246 |
247 | Define a system which tracks time:
248 |
249 | ```js
250 | world.time = {
251 | delta: 0,
252 | elapsed: 0,
253 | then: performance.now()
254 | }
255 | const timeSystem = world => {
256 | const { time } = world
257 | const now = performance.now()
258 | const delta = now - time.then
259 | time.delta = delta
260 | time.elapsed += delta
261 | time.then = now
262 | return world
263 | }
264 | ```
265 |
266 | Systems are used to update entities of a world:
267 | ```js
268 | movementSystem(world)
269 | ```
270 |
271 | Pipelines of functions can be composed with the `pipe` function:
272 | ```js
273 | const pipeline = pipe(
274 | movementSystem,
275 | timeSystem,
276 | )
277 |
278 | pipeline(world)
279 | ```
280 |
281 | ## 💾 Serialization
282 |
283 | Performant and highly customizable serialization is built-in. Any subset of data can be targeted and serialized/deserialized with great efficiency and ease.
284 |
285 | Serializers and deserializers need the same configs in order to work properly. Any combination of components and component properties may be used as configs.
286 |
287 | Serialization can take a world as a config and will serialize all component stores registered in that world:
288 | ```js
289 | const serialize = defineSerializer(world)
290 | const deserialize = defineDeserializer(world)
291 | ```
292 |
293 | Serialize all of the world's entities and thier component data:
294 | ```js
295 | const packet = serialize(world)
296 | ```
297 |
298 | Use the deserializer to apply state onto the same or any other world (returns deserialized entity IDs):
299 | * Note: serialized entities and components are automatically (re)created if they do not exist in the target world
300 | ```js
301 | const deserializedEnts = deserialize(world, packet)
302 | ```
303 |
304 | Serialize a more specific set of entities using queries:
305 | ```js
306 | const ents = movementQuery(world)
307 | const packet = serialize(ents)
308 | const deserializedEnts = deserialize(world, packet)
309 | ```
310 |
311 | Serialization for any mixture of components and component properties:
312 | ```js
313 | const config = [Position, Velocity.x, Velocity.y]
314 | const serializeMovement = defineSerializer(config)
315 | const deserializeMovement = defineDeserializer(config)
316 | ```
317 |
318 | Serialize Position data for entities matching the movementQuery, defined with pipe:
319 | ```js
320 | const serializeMovementQueryPositions = pipe(movementQuery, serializePositions)
321 | const packet = serializeMovementQueryPositions(world)
322 | const deserializedEnts = deserializePositions(world, packet)
323 | ```
324 |
325 | Serialization which targets select component stores of entities
326 | whose component state has changed since the last call of the function:
327 |
328 | ℹ Unlike queries, using `Changed` with serializers actually *improves* performance (less iterations).
329 | ```js
330 | const serializeOnlyChangedPositions = defineSerializer([Changed(Position)])
331 |
332 | const serializeChangedMovementQuery = pipe(movementQuery, serializeOnlyChangedPositions)
333 | let packet = serializeChangedMovementQuery(world)
334 | console.log(packet) // => undefined
335 |
336 | Position.x[eid]++
337 |
338 | packet = serializeChangedMovementQuery(world)
339 | console.log(packet.byteLength) // => 13
340 | ```
341 |
342 | ### Deserialize Modes
343 |
344 | There are 3 modes of deserilization, all of which are additive in nature.
345 |
346 | Deserialization will never remove entities, and will only add them.
347 |
348 | - `REPLACE` - (default) overwrites entity data, or creates new entities if the serialized EIDs don't exist in the target world.
349 | - `APPEND` - only creates new entities, never overwrites existing entity data.
350 | - `MAP` - acts like `REPLACE` but every serialized EID is assigned a local EID which is memorized for all subsequent deserializations onto the target world.
351 | - useful when deserializing server ECS state onto a client ECS world to avoid EID collisions but still maintain the server-side EID relationship
352 | - this maintains reference relationships made with `Types.eid`
353 | - returned entities are the locally mapped EIDs
354 |
355 | ```js
356 | const mode = DESERIALIZE_MODE.MAP
357 | const deserializedLocalEntities = deserialize(world, packet, mode)
358 | ```
359 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'bitecs' {
2 | export type Type =
3 | | 'i8'
4 | | 'ui8'
5 | | 'ui8c'
6 | | 'i16'
7 | | 'ui16'
8 | | 'i32'
9 | | 'ui32'
10 | | 'f32'
11 | | 'f64'
12 | | 'eid'
13 |
14 | export type ListType = readonly [Type, number];
15 |
16 | export const Types: {
17 | i8: "i8"
18 | ui8: "ui8"
19 | ui8c: "ui8c"
20 | i16: "i16"
21 | ui16: "ui16"
22 | i32: "i32"
23 | ui32: "ui32"
24 | f32: "f32"
25 | f64: "f64"
26 | eid: "eid"
27 | };
28 |
29 | export type TypedArray =
30 | | Uint8Array
31 | | Int8Array
32 | | Uint8Array
33 | | Uint8ClampedArray
34 | | Int16Array
35 | | Uint16Array
36 | | Int32Array
37 | | Uint32Array
38 | | Float32Array
39 | | Float64Array
40 |
41 | export type ArrayByType = {
42 | [Types.i8]: Int8Array;
43 | [Types.ui8]: Uint8Array;
44 | [Types.ui8c]: Uint8ClampedArray;
45 | [Types.i16]: Int16Array;
46 | [Types.ui16]: Uint16Array;
47 | [Types.i32]: Int32Array;
48 | [Types.ui32]: Uint32Array;
49 | [Types.f32]: Float32Array;
50 | [Types.f64]: Float64Array;
51 | [Types.eid]: Uint32Array;
52 | }
53 |
54 | export enum DESERIALIZE_MODE {
55 | REPLACE,
56 | APPEND,
57 | MAP
58 | }
59 |
60 | export type ComponentType = {
61 | [key in keyof T]:
62 | T[key] extends Type
63 | ? ArrayByType[T[key]]
64 | : T[key] extends [infer RT, number]
65 | ? RT extends Type
66 | ? Array
67 | : unknown
68 | : T[key] extends ISchema
69 | ? ComponentType
70 | : unknown;
71 | };
72 |
73 | export type ComponentProp = TypedArray | Array
74 |
75 | export interface IWorld {}
76 |
77 | export interface ISchema {
78 | [key: string]: Type | ListType | ISchema
79 | }
80 |
81 | export interface IComponentProp {
82 | }
83 |
84 | export interface IComponent {
85 | }
86 |
87 | export type Component = IComponent | ComponentType
88 |
89 | export type QueryModifier = (c: (IComponent | IComponentProp)[]) => (world: W) => IComponent | QueryModifier
90 |
91 | export type Query = (world: W, clearDiff?: Boolean) => number[]
92 |
93 | export type System = (world: W, ...args: R) => W
94 |
95 | export type Serializer = (target: W | number[]) => ArrayBuffer
96 | export type Deserializer = (world: W, packet: ArrayBuffer, mode?: DESERIALIZE_MODE) => number[]
97 |
98 | export function setDefaultSize(size: number): void
99 | export function setRemovedRecycleThreshold(newThreshold: number): void
100 | export function createWorld(obj?: W, size?: number): W
101 | export function createWorld(size?: number): W
102 | export function resetWorld(world: W): W
103 | export function deleteWorld(world: W): void
104 | export function addEntity(world: W): number
105 | export function removeEntity(world: W, eid: number): void
106 | export function entityExists(world: W, eid: number): boolean
107 | export function getWorldComponents(world: W): Component[]
108 | export function getAllEntities(world: W): number[]
109 | export function enableManualEntityRecycling(world: W): void
110 | export function flushRemovedEntities(world: W): void
111 |
112 | export function registerComponent(world: W, component: Component): void
113 | export function registerComponents(world: W, components: Component[]): void
114 | export function defineComponent(schema?: T, size?: number): ComponentType
115 | export function defineComponent(schema?: any, size?: number): T
116 | export function addComponent(world: W, component: Component, eid: number, reset?: boolean): void
117 | export function removeComponent(world: W, component: Component, eid: number, reset?: boolean): void
118 | export function hasComponent(world: W, component: Component, eid: number): boolean
119 | export function getEntityComponents(world: W, eid: number): Component[]
120 |
121 | export function defineQuery(components: (Component | QueryModifier)[]): Query
122 | export function Changed(c: Component | ISchema): Component | QueryModifier
123 | export function Not(c: Component | ISchema): Component | QueryModifier
124 | export function enterQuery(query: Query): Query
125 | export function exitQuery(query: Query): Query
126 | export function resetChangedQuery(world: W, query: Query): Query
127 | export function removeQuery(world: W, query: Query): Query
128 | export function commitRemovals(world: W): void
129 |
130 | export function defineSystem(update: (world: W, ...args: R) => W): System
131 |
132 | export function defineSerializer(target: W | Component[] | IComponentProp[] | QueryModifier, maxBytes?: number): Serializer
133 | export function defineDeserializer(target: W | Component[] | IComponentProp[] | QueryModifier): Deserializer
134 |
135 | export function pipe(...fns: ((...args: any[]) => any)[]): (...input: any[]) => any
136 |
137 | export const parentArray: Symbol
138 | }
139 |
--------------------------------------------------------------------------------
/mozilla-hubs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NateTheGreatt/bitECS/8db9450869ed681591fefcf573ec4c5c2cc9c10c/mozilla-hubs.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bitecs",
3 | "version": "0.3.40",
4 | "description": "Functional, minimal, data-oriented, ultra-high performance ECS library written in Javascript",
5 | "license": "MPL-2.0",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/NateTheGreatt/bitECS"
9 | },
10 | "type": "module",
11 | "main": "./dist/index.cjs",
12 | "module": "./dist/index.mjs",
13 | "types": "./dist/index.d.ts",
14 | "exports": {
15 | ".": {
16 | "types": "./dist/index.d.ts",
17 | "import": "./dist/index.mjs",
18 | "require": "./dist/index.cjs"
19 | }
20 | },
21 | "author": {
22 | "name": "Nathaniel Martin",
23 | "email": "mrtn.nathaniel@gmail.com",
24 | "url": "https://github.com/NateTheGreatt"
25 | },
26 | "contributors": [
27 | {
28 | "name": "Randy Lebeau",
29 | "email": "randylebeau@gmail.com",
30 | "url": "https://github.com/SupremeTechnopriest"
31 | }
32 | ],
33 | "scripts": {
34 | "test": "c8 mocha test --recursive",
35 | "build": "node scripts/build.js",
36 | "watch": "npm run build -- -w",
37 | "twatch": "npm run build -- -w -t",
38 | "docs": "node scripts/docs.js",
39 | "dist": "npm run test && npm run build && npm run docs"
40 | },
41 | "devDependencies": {
42 | "boxen": "^6.2.1",
43 | "brotli-size": "^4.0.0",
44 | "c8": "^7.10.0",
45 | "chalk": "^5.0.0",
46 | "dmd-readable": "^1.2.4",
47 | "esbuild": "^0.14.5",
48 | "fs-extra": "^10.0.0",
49 | "globby": "^12.0.2",
50 | "gradient-string": "^2.0.0",
51 | "gzip-size": "^7.0.0",
52 | "human-readable": "^0.2.1",
53 | "jsdoc-to-markdown": "^7.1.0",
54 | "minimist": "^1.2.5",
55 | "mocha": "^9.1.3",
56 | "ora": "^6.0.1",
57 | "sloc": "^0.2.1",
58 | "typescript": "^4.5.4"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra'
2 | import { performance } from 'perf_hooks'
3 | import { execSync } from 'child_process'
4 | import { buildSync, build } from 'esbuild'
5 | import minimist from 'minimist'
6 | import ora from 'ora'
7 | import boxen from 'boxen'
8 | import chalk from 'chalk'
9 | import gradient from 'gradient-string'
10 | import sloc from 'sloc'
11 | import { gzipSizeFromFileSync } from 'gzip-size'
12 | import brotliSize from 'brotli-size'
13 | import { sizeFormatter } from 'human-readable'
14 | import { logLogo } from './logLogo.js'
15 |
16 | const argv = minimist(process.argv.slice(2))
17 | const types = argv.t || argv.types
18 | const watch = argv.w || argv.watch
19 |
20 | const infile = `./src/index.js`
21 | const outdir = `./dist`
22 | const outfileCjs = `${outdir}/index.cjs`
23 | const outfileEsm = `${outdir}/index.mjs`
24 |
25 | const logs = []
26 | const times = []
27 |
28 | const check = chalk.green.bold('✔')
29 |
30 | function normalizeTime (ms) {
31 | if (ms > 1000) {
32 | ms /= 1000
33 | ms = ms.toFixed(2)
34 | return `${ms} seconds`
35 | } else {
36 | ms = ms.toFixed(2)
37 | return `${ms} ms`
38 | }
39 | }
40 |
41 | const normalizeBytes = sizeFormatter({
42 | std: 'JEDEC',
43 | decimalPlaces: 2,
44 | keepTrailingZeros: false,
45 | render: (literal, symbol) => chalk.green(`${literal} ${symbol}B`)
46 | })
47 |
48 | function startTimer () {
49 | times.push(performance.now())
50 | }
51 |
52 | function log (message, skipTime = false) {
53 | if (skipTime) {
54 | logs.push(message)
55 | return
56 | }
57 |
58 | const startTime = times[times.length - 1]
59 | let duration = performance.now() - startTime
60 | duration = normalizeTime(duration)
61 | logs.push(`${message} ${chalk.green(`(${duration})`)}`)
62 | }
63 |
64 | function endLog () {
65 | const logMessage = logLogo().concat(logs).join('\n')
66 | const boxStyle = {
67 | padding: 1,
68 | margin: 1,
69 | borderColor: 'cyanBright',
70 | borderStyle: 'bold'
71 | }
72 |
73 | console.log(boxen(logMessage, boxStyle))
74 |
75 | logs.length = 0
76 | times.length = 0
77 | }
78 |
79 | function pkg () {
80 | // Capture start time
81 | const startTime = performance.now()
82 |
83 | // Get package.json
84 | const pkg = fs.readJsonSync(`./package.json`)
85 | log(`${chalk.red.bold(pkg.name)}`, true)
86 | log(`${chalk.dim('v')}${chalk.dim.italic(pkg.version)}`, true)
87 | log('', true)
88 |
89 | // Clear dist folder
90 | startTimer()
91 | fs.emptyDirSync(outdir, { recursive: true })
92 | log(`${check} Cleaned target folder`)
93 |
94 | // Build source
95 | startTimer()
96 | console.log()
97 | const cjsBuildProgress = ora('Creating CJS bundle').start()
98 | const cjsBuildResults = buildSync({
99 | entryPoints: [infile],
100 | bundle: true,
101 | format: 'cjs',
102 | platform: 'node',
103 | sourcemap: true,
104 | outfile: outfileCjs
105 | })
106 | cjsBuildProgress.succeed()
107 |
108 | // Check for errors
109 | if (cjsBuildResults.errors.length > 0) {
110 | cjsBuildProgress.fail()
111 | console.log()
112 | console.log(`❌ ${chalk.red('CJS Build Error')}`)
113 | console.log(cjsBuildResults.errors)
114 | process.exit(1)
115 | }
116 |
117 | const esmBuildProgress = ora('Creating ESM bundle').start()
118 | const esmBuildResults = buildSync({
119 | entryPoints: [infile],
120 | bundle: true,
121 | format: 'esm',
122 | platform: 'node',
123 | sourcemap: true,
124 | outfile: outfileEsm
125 | })
126 | esmBuildProgress.succeed()
127 |
128 | // Check for errors
129 | if (esmBuildResults.errors.length > 0) {
130 | esmBuildProgress.fail()
131 | console.log()
132 | console.log(`❌ ${chalk.red('ESM Build Error')}`)
133 | console.log(esmBuildResults.errors)
134 | process.exit(1)
135 | }
136 |
137 | log(`${check} Source code built`)
138 |
139 | // Generate typedefs
140 | if (types) {
141 | startTimer()
142 | const typesProgress = ora('Generating typedefs').start()
143 | try {
144 | execSync('tsc --emitDeclarationOnly')
145 | typesProgress.succeed()
146 | } catch (err) {
147 | typesProgress.fail()
148 | console.log()
149 | console.log(`❌ ${chalk.white.bgRed('Typescript Error')}: ${err.message}`)
150 | if (err.stdout && err.stdout.length) {
151 | console.log(err.stdout.toString())
152 | }
153 |
154 | if (err.stderr && err.stderr.length) {
155 | console.log(err.stderr.toString())
156 | }
157 | process.exit(1)
158 | }
159 |
160 | log(`${check} Typedefs generated`)
161 | }
162 |
163 | // NOTE: Remove this after typescript port is done
164 | if (!types) {
165 | const typeDefProgress = ora('Copying typedefs').start()
166 | try {
167 | fs.copyFileSync('./index.d.ts', `${outdir}/index.d.ts`)
168 | typeDefProgress.succeed()
169 | } catch (err) {
170 | typeDefProgress.fail()
171 | console.log(`❌ ${chalk.white.bgRed('Error')}: ${err.message}`)
172 | process.exit(1)
173 | }
174 | }
175 |
176 | log('', true)
177 |
178 | // Log stats
179 | const loc = sloc(fs.readFileSync(outfileEsm, 'utf-8'), 'js')
180 | const cjsStats = fs.statSync(outfileCjs)
181 | const esmStats = fs.statSync(outfileEsm)
182 | const gzippedSize = gzipSizeFromFileSync(outfileEsm)
183 | const brotliedSize = brotliSize.fileSync(outfileEsm)
184 |
185 | log(`${chalk.yellow.bold('Lines of Code:')} ${chalk.green(loc.source)}`, true)
186 | log(`${chalk.yellow.bold('CJS Bundle: ')} ${normalizeBytes(cjsStats.size)}`, true)
187 | log(`${chalk.yellow.bold('ESM Bundle: ')} ${normalizeBytes(esmStats.size)}`, true)
188 | log(`${chalk.yellow.bold('Gzipped: ')} ${normalizeBytes(gzippedSize)}`, true)
189 | log(`${chalk.yellow.bold('Brotlied: ')} ${normalizeBytes(brotliedSize)}`, true)
190 |
191 | let duration = performance.now() - startTime
192 | duration = normalizeTime(duration)
193 | log('', true)
194 | log(gradient.pastel(`Build complete in ${duration}`), true)
195 | endLog()
196 | }
197 |
198 | if (watch) {
199 | // Initial build
200 | pkg()
201 | // Watch build ESM
202 | await build({
203 | entryPoints: [infile],
204 | bundle: true,
205 | format: 'esm',
206 | platform: 'node',
207 | sourcemap: true,
208 | outfile: outfileEsm,
209 | watch: {
210 | onRebuild (error) {
211 | if (error) {
212 | console.log(`❌ ${chalk.white.bgRed('Error')} ${chalk.red(`ESM ${error.message}`)}`)
213 | } else {
214 | console.log(`${check} ${chalk.green('ESM watch build succeeded')}`)
215 | // Build typedefs if enabled
216 | if (types) {
217 | try {
218 | execSync('tsc --emitDeclarationOnly')
219 | console.log(`${check} ${chalk.green('Typedef watch build succeeded')}`)
220 | } catch (err) {
221 | console.log(`❌ ${chalk.white.bgRed('Typescript Error')}: ${err.message}`)
222 | if (err.stdout && err.stdout.length) {
223 | console.log(err.stdout.toString())
224 | }
225 |
226 | if (err.stderr && err.stderr.length) {
227 | console.log(err.stderr.toString())
228 | }
229 | }
230 | }
231 | }
232 | }
233 | }
234 | })
235 | // Watch build CJS
236 | await build({
237 | entryPoints: [infile],
238 | bundle: true,
239 | format: 'cjs',
240 | platform: 'node',
241 | sourcemap: true,
242 | outfile: outfileCjs,
243 | watch: {
244 | onRebuild(error) {
245 | if (error) {
246 | console.log(`❌ ${chalk.white.bgRed('Error')} ${chalk.red(`CJS ${error.message}`)}`)
247 | } else {
248 | console.log(`${check} ${chalk.green('CJS watch build succeeded')}`)
249 | }
250 | }
251 | }
252 | })
253 | // Log start
254 | console.log(`👁 ${gradient.pastel('Watching source code for changes...')}`)
255 | } else {
256 | pkg()
257 | }
258 |
--------------------------------------------------------------------------------
/scripts/docs.js:
--------------------------------------------------------------------------------
1 | import { join, dirname } from 'path'
2 | import { fileURLToPath } from 'url'
3 | import { writeFileSync } from 'fs'
4 | import { globby } from 'globby'
5 | import jsdoc2md from 'jsdoc-to-markdown'
6 |
7 | const FILENAME = 'API.md'
8 |
9 | const __dirname = dirname(fileURLToPath(import.meta.url))
10 |
11 | async function render (pattern, output) {
12 | const files = await globby([
13 | pattern,
14 | '!**/**/node_modules',
15 | '!**/**/test',
16 | '!**/**/examples',
17 | ])
18 | const md = await jsdoc2md.render({
19 | files,
20 | plugin: 'dmd-readable'
21 | })
22 | writeFileSync(output, md)
23 | }
24 |
25 | async function build () {
26 | await render('src/**/*.js', join(__dirname, '../docs', FILENAME))
27 | }
28 |
29 | build().catch(console.error)
30 |
--------------------------------------------------------------------------------
/scripts/logLogo.js:
--------------------------------------------------------------------------------
1 | const o = ' '
2 | const b = '\x1b[106m \x1b[0m'
3 | const r = '\x1b[101m \x1b[0m'
4 |
5 | const logo = [
6 | [o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o],
7 | [o, o, r, r, o, r, r, o, o, o, r, r, o, r, r, o, o, o, r, r, o, r, r, o, o],
8 | [o, r, r, r, r, r, r, r, o, r, r, r, r, r, r, r, o, r, r, r, r, r, r, r, o],
9 | [o, r, r, r, r, r, r, r, o, r, r, r, r, r, r, r, o, r, r, r, r, r, r, r, o],
10 | [o, o, r, r, r, r, r, o, o, o, r, r, r, r, r, o, o, o, r, r, r, r, r, o, o],
11 | [o, o, o, r, r, r, o, o, o, o, o, r, r, r, o, o, o, o, o, r, r, r, o, o, o],
12 | [o, o, o, o, r, o, o, o, o, o, o, o, r, o, o, o, o, o, o, o, r, o, o, o, o],
13 | [o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o],
14 | [o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o],
15 | [o, b, o, o, o, b, o, o, o, o, o, b, b, b, o, o, b, b, o, o, o, b, b, b, o],
16 | [o, b, o, o, o, o, o, o, b, o, o, b, o, o, o, b, o, o, b, o, b, o, o, o, o],
17 | [o, b, b, b, o, b, o, b, b, b, o, b, b, o, o, b, o, o, o, o, o, b, b, o, o],
18 | [o, b, o, b, o, b, o, o, b, o, o, b, o, o, o, b, o, o, b, o, o, o, o, b, o],
19 | [o, b, b, b, o, b, o, o, b, o, o, b, b, b, o, o, b, b, o, o, b, b, b, o, o],
20 | [o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o],
21 | [o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o]
22 | ]
23 |
24 | export function logLogo () {
25 | const logs = []
26 | for (let i = 0; i < logo.length; i++) {
27 | let str = ''
28 | for (let j = 0; j < logo[i].length; j++) {
29 | str += logo[i][j]
30 | str += logo[i][j]
31 | }
32 | logs.push(str)
33 | }
34 | logs.push('\n')
35 | return logs
36 | }
--------------------------------------------------------------------------------
/src/Component.js:
--------------------------------------------------------------------------------
1 | import { $storeSize, createStore, resetStoreFor, resizeStore } from './Storage.js'
2 | import { $queries, queryAddEntity, queryRemoveEntity, queryCheckEntity, commitRemovals } from './Query.js'
3 | import { $bitflag, $size } from './World.js'
4 | import { $entityMasks, getDefaultSize, eidToWorld, $entityComponents, getGlobalSize, $entitySparseSet } from './Entity.js'
5 |
6 | export const $componentMap = Symbol('componentMap')
7 |
8 | export const components = []
9 |
10 | export const resizeComponents = (size) => {
11 | components.forEach(component => resizeStore(component, size))
12 | }
13 |
14 |
15 | /**
16 | * Defines a new component store.
17 | *
18 | * @param {object} schema
19 | * @returns {object}
20 | */
21 | export const defineComponent = (schema, size) => {
22 | const component = createStore(schema, size || getGlobalSize())
23 | if (schema && Object.keys(schema).length) components.push(component)
24 | return component
25 | }
26 |
27 | export const incrementBitflag = (world) => {
28 | world[$bitflag] *= 2
29 | if (world[$bitflag] >= 2**31) {
30 | world[$bitflag] = 1
31 | world[$entityMasks].push(new Uint32Array(world[$size]))
32 | }
33 | }
34 |
35 |
36 | /**
37 | * Registers a component with a world.
38 | *
39 | * @param {World} world
40 | * @param {Component} component
41 | */
42 | export const registerComponent = (world, component) => {
43 | if (!component) throw new Error(`bitECS - Cannot register null or undefined component`)
44 |
45 | const queries = new Set()
46 | const notQueries = new Set()
47 | const changedQueries = new Set()
48 |
49 | world[$queries].forEach(q => {
50 | if (q.allComponents.includes(component)) {
51 | queries.add(q)
52 | }
53 | })
54 |
55 | world[$componentMap].set(component, {
56 | generationId: world[$entityMasks].length - 1,
57 | bitflag: world[$bitflag],
58 | store: component,
59 | queries,
60 | notQueries,
61 | changedQueries,
62 | })
63 |
64 | incrementBitflag(world)
65 | }
66 |
67 | /**
68 | * Registers multiple components with a world.
69 | *
70 | * @param {World} world
71 | * @param {Component} components
72 | */
73 | export const registerComponents = (world, components) => {
74 | components.forEach(c => registerComponent(world, c))
75 | }
76 |
77 | /**
78 | * Checks if an entity has a component.
79 | *
80 | * @param {World} world
81 | * @param {Component} component
82 | * @param {number} eid
83 | * @returns {boolean}
84 | */
85 | export const hasComponent = (world, component, eid) => {
86 | const registeredComponent = world[$componentMap].get(component)
87 | if (!registeredComponent) return false
88 | const { generationId, bitflag } = registeredComponent
89 | const mask = world[$entityMasks][generationId][eid]
90 | return (mask & bitflag) === bitflag
91 | }
92 |
93 | /**
94 | * Adds a component to an entity
95 | *
96 | * @param {World} world
97 | * @param {Component} component
98 | * @param {number} eid
99 | * @param {boolean} [reset=false]
100 | */
101 | export const addComponent = (world, component, eid, reset=false) => {
102 | if (eid === undefined) throw new Error('bitECS - entity is undefined.')
103 | if (!world[$entitySparseSet].has(eid)) throw new Error('bitECS - entity does not exist in the world.')
104 | if (!world[$componentMap].has(component)) registerComponent(world, component)
105 | if (hasComponent(world, component, eid)) return
106 |
107 | const c = world[$componentMap].get(component)
108 | const { generationId, bitflag, queries, notQueries } = c
109 |
110 | // Add bitflag to entity bitmask
111 | world[$entityMasks][generationId][eid] |= bitflag
112 |
113 | // todo: archetype graph
114 | queries.forEach(q => {
115 | // remove this entity from toRemove if it exists in this query
116 | q.toRemove.remove(eid)
117 | const match = queryCheckEntity(world, q, eid)
118 | if (match) {
119 | q.exited.remove(eid)
120 | queryAddEntity(q, eid)
121 | }
122 | if (!match) {
123 | q.entered.remove(eid)
124 | queryRemoveEntity(world, q, eid)
125 | }
126 | })
127 |
128 | world[$entityComponents].get(eid).add(component)
129 |
130 | // Zero out each property value
131 | if (reset) resetStoreFor(component, eid)
132 | }
133 |
134 | /**
135 | * Removes a component from an entity and resets component state unless otherwise specified.
136 | *
137 | * @param {World} world
138 | * @param {Component} component
139 | * @param {number} eid
140 | * @param {boolean} [reset=true]
141 | */
142 | export const removeComponent = (world, component, eid, reset=true) => {
143 | if (eid === undefined) throw new Error('bitECS - entity is undefined.')
144 | if (!world[$entitySparseSet].has(eid)) throw new Error('bitECS - entity does not exist in the world.')
145 | if (!hasComponent(world, component, eid)) return
146 |
147 | const c = world[$componentMap].get(component)
148 | const { generationId, bitflag, queries } = c
149 |
150 | // Remove flag from entity bitmask
151 | world[$entityMasks][generationId][eid] &= ~bitflag
152 |
153 | // todo: archetype graph
154 | queries.forEach(q => {
155 | // remove this entity from toRemove if it exists in this query
156 | q.toRemove.remove(eid)
157 | const match = queryCheckEntity(world, q, eid)
158 | if (match) {
159 | q.exited.remove(eid)
160 | queryAddEntity(q, eid)
161 | }
162 | if (!match) {
163 | q.entered.remove(eid)
164 | queryRemoveEntity(world, q, eid)
165 | }
166 | })
167 |
168 | world[$entityComponents].get(eid).delete(component)
169 |
170 | // Zero out each property value
171 | if (reset) resetStoreFor(component, eid)
172 | }
173 |
--------------------------------------------------------------------------------
/src/Constants.js:
--------------------------------------------------------------------------------
1 | export const TYPES_ENUM = {
2 | i8: 'i8',
3 | ui8: 'ui8',
4 | ui8c: 'ui8c',
5 | i16: 'i16',
6 | ui16: 'ui16',
7 | i32: 'i32',
8 | ui32: 'ui32',
9 | f32: 'f32',
10 | f64: 'f64',
11 | eid: 'eid',
12 | }
13 |
14 | export const TYPES_NAMES = {
15 | i8: 'Int8',
16 | ui8: 'Uint8',
17 | ui8c: 'Uint8Clamped',
18 | i16: 'Int16',
19 | ui16: 'Uint16',
20 | i32: 'Int32',
21 | ui32: 'Uint32',
22 | eid: 'Uint32',
23 | f32: 'Float32',
24 | f64: 'Float64'
25 | }
26 |
27 | export const TYPES = {
28 | i8: Int8Array,
29 | ui8: Uint8Array,
30 | ui8c: Uint8ClampedArray,
31 | i16: Int16Array,
32 | ui16: Uint16Array,
33 | i32: Int32Array,
34 | ui32: Uint32Array,
35 | f32: Float32Array,
36 | f64: Float64Array,
37 | eid: Uint32Array,
38 | }
39 |
40 | export const UNSIGNED_MAX = {
41 | uint8: 2**8,
42 | uint16: 2**16,
43 | uint32: 2**32
44 | }
45 |
--------------------------------------------------------------------------------
/src/Entity.js:
--------------------------------------------------------------------------------
1 | import { resizeComponents } from './Component.js'
2 | import { $notQueries, $queries, queryAddEntity, queryCheckEntity, queryRemoveEntity } from './Query.js'
3 | import { $localEntities, $localEntityLookup, $manualEntityRecycling, $size, resizeWorlds } from './World.js'
4 | import { setSerializationResized } from './Serialize.js'
5 |
6 | export const $entityMasks = Symbol('entityMasks')
7 | export const $entityComponents = Symbol('entityComponents')
8 | export const $entitySparseSet = Symbol('entitySparseSet')
9 | export const $entityArray = Symbol('entityArray')
10 | export const $entityIndices = Symbol('entityIndices')
11 | export const $removedEntities = Symbol('removedEntities')
12 |
13 | let defaultSize = 100000
14 |
15 | // need a global EID cursor which all worlds and all components know about
16 | // so that world entities can posess entire rows spanning all component tables
17 | let globalEntityCursor = 0
18 | let globalSize = defaultSize
19 | let resizeThreshold = () => globalSize - (globalSize / 5)
20 |
21 | export const getGlobalSize = () => globalSize
22 |
23 | // removed eids should also be global to prevent memory leaks
24 | const removed = []
25 | const recycled = []
26 |
27 | const defaultRemovedReuseThreshold = 0.01
28 | let removedReuseThreshold = defaultRemovedReuseThreshold
29 |
30 | export const resetGlobals = () => {
31 | globalSize = defaultSize
32 | globalEntityCursor = 0
33 | removedReuseThreshold = defaultRemovedReuseThreshold
34 | removed.length = 0
35 | recycled.length = 0
36 | }
37 |
38 | export const getDefaultSize = () => defaultSize
39 |
40 | /**
41 | * Sets the default maximum number of entities for worlds and component stores.
42 | *
43 | * @param {number} newSize
44 | */
45 | export const setDefaultSize = newSize => {
46 | const oldSize = globalSize
47 |
48 | defaultSize = newSize
49 | resetGlobals()
50 |
51 | globalSize = newSize
52 | resizeWorlds(newSize)
53 | setSerializationResized(true)
54 | }
55 |
56 | /**
57 | * Sets the number of entities that must be removed before removed entity ids begin to be recycled.
58 | * This should be set to as a % (0-1) of `defaultSize` that you would never likely remove/add on a single frame.
59 | *
60 | * @param {number} newThreshold
61 | */
62 | export const setRemovedRecycleThreshold = newThreshold => {
63 | removedReuseThreshold = newThreshold
64 | }
65 |
66 | export const getEntityCursor = () => globalEntityCursor
67 | export const getRemovedEntities = () => [...recycled, ...removed]
68 |
69 | export const eidToWorld = new Map()
70 |
71 | export const flushRemovedEntities = (world) => {
72 | if (!world[$manualEntityRecycling]) {
73 | throw new Error("bitECS - cannot flush removed entities, enable feature with the enableManualEntityRecycling function")
74 | }
75 | removed.push(...recycled)
76 | recycled.length = 0
77 | }
78 |
79 | /**
80 | * Adds a new entity to the specified world.
81 | *
82 | * @param {World} world
83 | * @returns {number} eid
84 | */
85 | export const addEntity = (world) => {
86 |
87 | const eid = world[$manualEntityRecycling]
88 | ? removed.length ? removed.shift() : globalEntityCursor++
89 | : removed.length > Math.round(globalSize * removedReuseThreshold) ? removed.shift() : globalEntityCursor++
90 |
91 | if (eid > world[$size]) throw new Error("bitECS - max entities reached")
92 |
93 | world[$entitySparseSet].add(eid)
94 | eidToWorld.set(eid, world)
95 |
96 | world[$notQueries].forEach(q => {
97 | const match = queryCheckEntity(world, q, eid)
98 | if (match) queryAddEntity(q, eid)
99 | })
100 |
101 | world[$entityComponents].set(eid, new Set())
102 |
103 | return eid
104 | }
105 |
106 | /**
107 | * Removes an existing entity from the specified world.
108 | *
109 | * @param {World} world
110 | * @param {number} eid
111 | */
112 | export const removeEntity = (world, eid) => {
113 | // Check if entity is already removed
114 | if (!world[$entitySparseSet].has(eid)) return
115 |
116 | // Remove entity from all queries
117 | // TODO: archetype graph
118 | world[$queries].forEach(q => {
119 | queryRemoveEntity(world, q, eid)
120 | })
121 |
122 | // Free the entity
123 | if (world[$manualEntityRecycling])
124 | recycled.push(eid)
125 | else
126 | removed.push(eid)
127 |
128 | // remove all eid state from world
129 | world[$entitySparseSet].remove(eid)
130 | world[$entityComponents].delete(eid)
131 |
132 | // remove from deserializer mapping
133 | world[$localEntities].delete(world[$localEntityLookup].get(eid))
134 | world[$localEntityLookup].delete(eid)
135 |
136 | // Clear entity bitmasks
137 | for (let i = 0; i < world[$entityMasks].length; i++) world[$entityMasks][i][eid] = 0
138 | }
139 |
140 | /**
141 | * Returns an array of components that an entity possesses.
142 | *
143 | * @param {*} world
144 | * @param {*} eid
145 | */
146 | export const getEntityComponents = (world, eid) => {
147 | if (eid === undefined) throw new Error('bitECS - entity is undefined.')
148 | if (!world[$entitySparseSet].has(eid)) throw new Error('bitECS - entity does not exist in the world.')
149 | return Array.from(world[$entityComponents].get(eid))
150 | }
151 |
152 | /**
153 | * Checks the existence of an entity in a world
154 | *
155 | * @param {World} world
156 | * @param {number} eid
157 | */
158 | export const entityExists = (world, eid) => world[$entitySparseSet].has(eid)
159 |
--------------------------------------------------------------------------------
/src/Query.js:
--------------------------------------------------------------------------------
1 | import { SparseSet } from './Util.js'
2 | import { $queryShadow, $storeFlattened, $tagStore, createShadow } from './Storage.js'
3 | import { $componentMap, registerComponent } from './Component.js'
4 | import { $entityMasks, $entityArray, getEntityCursor, $entitySparseSet } from './Entity.js'
5 |
6 | export const $modifier = Symbol("$modifier")
7 |
8 | function modifier(c, mod) {
9 | const inner = () => [c, mod]
10 | inner[$modifier] = true
11 | return inner
12 | }
13 |
14 | export const Not = (c) => modifier(c, 'not')
15 | export const Or = (c) => modifier(c, 'or')
16 | export const Changed = (c) => modifier(c, 'changed')
17 |
18 | export function Any(...comps) { return function QueryAny() { return comps } }
19 | export function All(...comps) { return function QueryAll() { return comps } }
20 | export function None(...comps) { return function QueryNone() { return comps } }
21 |
22 | export const $queries = Symbol('queries')
23 | export const $notQueries = Symbol('notQueries')
24 |
25 | export const $queryAny = Symbol('queryAny')
26 | export const $queryAll = Symbol('queryAll')
27 | export const $queryNone = Symbol('queryNone')
28 |
29 | export const $queryMap = Symbol('queryMap')
30 | export const $dirtyQueries = Symbol('$dirtyQueries')
31 | export const $queryComponents = Symbol('queryComponents')
32 | export const $enterQuery = Symbol('enterQuery')
33 | export const $exitQuery = Symbol('exitQuery')
34 |
35 | const empty = Object.freeze([])
36 |
37 | /**
38 | * Given an existing query, returns a new function which returns entities who have been added to the given query since the last call of the function.
39 | *
40 | * @param {function} query
41 | * @returns {function} enteredQuery
42 | */
43 | export const enterQuery = query => world => {
44 | if (!world[$queryMap].has(query)) registerQuery(world, query)
45 | const q = world[$queryMap].get(query)
46 | if (q.entered.dense.length === 0) {
47 | return empty
48 | } else {
49 | const results = q.entered.dense.slice()
50 | q.entered.reset()
51 | return results
52 | }
53 | }
54 |
55 | /**
56 | * Given an existing query, returns a new function which returns entities who have been removed from the given query since the last call of the function.
57 | *
58 | * @param {function} query
59 | * @returns {function} enteredQuery
60 | */
61 | export const exitQuery = query => world => {
62 | if (!world[$queryMap].has(query)) registerQuery(world, query)
63 | const q = world[$queryMap].get(query)
64 | if (q.exited.dense.length === 0) {
65 | return empty
66 | } else {
67 | const results = q.exited.dense.slice()
68 | q.exited.reset()
69 | return results
70 | }
71 | }
72 |
73 | export const registerQuery = (world, query) => {
74 |
75 | const components = []
76 | const notComponents = []
77 | const changedComponents = []
78 |
79 | query[$queryComponents].forEach(c => {
80 | if (typeof c === "function" && c[$modifier]) {
81 | const [comp, mod] = c()
82 | if (!world[$componentMap].has(comp)) registerComponent(world, comp)
83 | if (mod === 'not') {
84 | notComponents.push(comp)
85 | }
86 | if (mod === 'changed') {
87 | changedComponents.push(comp)
88 | components.push(comp)
89 | }
90 | // if (mod === 'all') {
91 | // allComponents.push(comp)
92 | // }
93 | // if (mod === 'any') {
94 | // anyComponents.push(comp)
95 | // }
96 | // if (mod === 'none') {
97 | // noneComponents.push(comp)
98 | // }
99 | } else {
100 | if (!world[$componentMap].has(c)) registerComponent(world, c)
101 | components.push(c)
102 | }
103 | })
104 |
105 |
106 | const mapComponents = c => world[$componentMap].get(c)
107 |
108 | const allComponents = components.concat(notComponents).map(mapComponents)
109 |
110 | // const sparseSet = Uint32SparseSet(getGlobalSize())
111 | const sparseSet = SparseSet()
112 |
113 | const archetypes = []
114 | // const changed = SparseSet()
115 | const changed = []
116 | const toRemove = SparseSet()
117 | const entered = SparseSet()
118 | const exited = SparseSet()
119 |
120 | const generations = allComponents
121 | .map(c => c.generationId)
122 | .reduce((a,v) => {
123 | if (a.includes(v)) return a
124 | a.push(v)
125 | return a
126 | }, [])
127 |
128 | const reduceBitflags = (a,c) => {
129 | if (!a[c.generationId]) a[c.generationId] = 0
130 | a[c.generationId] |= c.bitflag
131 | return a
132 | }
133 | const masks = components
134 | .map(mapComponents)
135 | .reduce(reduceBitflags, {})
136 |
137 | const notMasks = notComponents
138 | .map(mapComponents)
139 | .reduce(reduceBitflags, {})
140 |
141 | // const orMasks = orComponents
142 | // .map(mapComponents)
143 | // .reduce(reduceBitmasks, {})
144 |
145 | const hasMasks = allComponents
146 | .reduce(reduceBitflags, {})
147 |
148 | const flatProps = components
149 | .filter(c => !c[$tagStore])
150 | .map(c => Object.getOwnPropertySymbols(c).includes($storeFlattened) ? c[$storeFlattened] : [c])
151 | .reduce((a,v) => a.concat(v), [])
152 |
153 | const shadows = []
154 |
155 | const q = Object.assign(sparseSet, {
156 | archetypes,
157 | changed,
158 | components,
159 | notComponents,
160 | changedComponents,
161 | allComponents,
162 | masks,
163 | notMasks,
164 | // orMasks,
165 | hasMasks,
166 | generations,
167 | flatProps,
168 | toRemove,
169 | entered,
170 | exited,
171 | shadows,
172 | })
173 |
174 | world[$queryMap].set(query, q)
175 | world[$queries].add(q)
176 |
177 | allComponents.forEach(c => {
178 | c.queries.add(q)
179 | })
180 |
181 | if (notComponents.length) world[$notQueries].add(q)
182 |
183 | for (let eid = 0; eid < getEntityCursor(); eid++) {
184 | if (!world[$entitySparseSet].has(eid)) continue
185 | const match = queryCheckEntity(world, q, eid)
186 | if (match) queryAddEntity(q, eid)
187 | }
188 | }
189 |
190 | const generateShadow = (q, pid) => {
191 | const $ = Symbol()
192 | const prop = q.flatProps[pid]
193 | createShadow(prop, $)
194 | q.shadows[pid] = prop[$]
195 | return prop[$]
196 | }
197 |
198 | const diff = (q, clearDiff) => {
199 | if (clearDiff) q.changed = []
200 | const { flatProps, shadows } = q
201 | for (let i = 0; i < q.dense.length; i++) {
202 | const eid = q.dense[i]
203 | let dirty = false
204 | for (let pid = 0; pid < flatProps.length; pid++) {
205 | const prop = flatProps[pid]
206 | const shadow = shadows[pid] || generateShadow(q, pid)
207 | if (ArrayBuffer.isView(prop[eid])) {
208 | for (let i = 0; i < prop[eid].length; i++) {
209 | if (prop[eid][i] !== shadow[eid][i]) {
210 | dirty = true
211 | break
212 | }
213 | }
214 | shadow[eid].set(prop[eid])
215 | } else {
216 | if (prop[eid] !== shadow[eid]) {
217 | dirty = true
218 | shadow[eid] = prop[eid]
219 | }
220 | }
221 | }
222 | if (dirty) q.changed.push(eid)
223 | }
224 | return q.changed
225 | }
226 |
227 | // const queryEntityChanged = (q, eid) => {
228 | // if (q.changed.has(eid)) return
229 | // q.changed.add(eid)
230 | // }
231 |
232 | // export const entityChanged = (world, component, eid) => {
233 | // const { changedQueries } = world[$componentMap].get(component)
234 | // changedQueries.forEach(q => {
235 | // const match = queryCheckEntity(world, q, eid)
236 | // if (match) queryEntityChanged(q, eid)
237 | // })
238 | // }
239 |
240 | const flatten = (a,v) => a.concat(v)
241 |
242 | const aggregateComponentsFor = mod => x => x.filter(f => f.name === mod().constructor.name).reduce(flatten)
243 |
244 | const getAnyComponents = aggregateComponentsFor(Any)
245 | const getAllComponents = aggregateComponentsFor(All)
246 | const getNoneComponents = aggregateComponentsFor(None)
247 |
248 | /**
249 | * Defines a query function which returns a matching set of entities when called on a world.
250 | *
251 | * @param {array} components
252 | * @returns {function} query
253 | */
254 |
255 | export const defineQuery = (...args) => {
256 | let components
257 | let any, all, none
258 | if (Array.isArray(args[0])) {
259 | components = args[0]
260 | } else {
261 | // any = getAnyComponents(args)
262 | // all = getAllComponents(args)
263 | // none = getNoneComponents(args)
264 | }
265 |
266 |
267 | if (components === undefined || components[$componentMap] !== undefined) {
268 | return world => world ? world[$entityArray] : components[$entityArray]
269 | }
270 |
271 | const query = function (world, clearDiff=true) {
272 | if (!world[$queryMap].has(query)) registerQuery(world, query)
273 |
274 | const q = world[$queryMap].get(query)
275 |
276 | commitRemovals(world)
277 |
278 | if (q.changedComponents.length) return diff(q, clearDiff)
279 | // if (q.changedComponents.length) return q.changed.dense
280 |
281 | return q.dense
282 | }
283 |
284 | query[$queryComponents] = components
285 | query[$queryAny] = any
286 | query[$queryAll] = all
287 | query[$queryNone] = none
288 |
289 | return query
290 | }
291 |
292 | const bin = value => {
293 | if (!Number.isSafeInteger(value)) {
294 | throw new TypeError('value must be a safe integer');
295 | }
296 |
297 | const negative = value < 0;
298 | const twosComplement = negative ? Number.MAX_SAFE_INTEGER + value + 1 : value;
299 | const signExtend = negative ? '1' : '0';
300 |
301 | return twosComplement.toString(2).padStart(4, '0').padStart(0, signExtend);
302 | }
303 |
304 | // TODO: archetype graph
305 | export const queryCheckEntity = (world, q, eid) => {
306 | const { masks, notMasks, generations } = q
307 | let or = 0
308 | for (let i = 0; i < generations.length; i++) {
309 | const generationId = generations[i]
310 | const qMask = masks[generationId]
311 | const qNotMask = notMasks[generationId]
312 | // const qOrMask = orMasks[generationId]
313 | const eMask = world[$entityMasks][generationId][eid]
314 |
315 | // any
316 | // if (qOrMask && (eMask & qOrMask) !== qOrMask) {
317 | // continue
318 | // }
319 | // not all
320 | // if (qNotMask && (eMask & qNotMask) === qNotMask) {
321 | // }
322 | // not any
323 | if (qNotMask && (eMask & qNotMask) !== 0) {
324 | return false
325 | }
326 | // all
327 | if (qMask && (eMask & qMask) !== qMask) {
328 | return false
329 | }
330 | }
331 | return true
332 | }
333 |
334 | export const queryCheckComponent = (q, c) => {
335 | const { generationId, bitflag } = c
336 | const { hasMasks } = q
337 | const mask = hasMasks[generationId]
338 | return (mask & bitflag) === bitflag
339 | }
340 |
341 | export const queryAddEntity = (q, eid) => {
342 | q.toRemove.remove(eid)
343 | // if (!q.has(eid))
344 | q.entered.add(eid)
345 | q.add(eid)
346 | }
347 |
348 | const queryCommitRemovals = (q) => {
349 | for (let i = q.toRemove.dense.length-1; i >= 0; i--) {
350 | const eid = q.toRemove.dense[i]
351 | q.toRemove.remove(eid)
352 | q.remove(eid)
353 | }
354 | }
355 |
356 | export const commitRemovals = (world) => {
357 | if (!world[$dirtyQueries].size) return
358 | world[$dirtyQueries].forEach(queryCommitRemovals)
359 | world[$dirtyQueries].clear()
360 | }
361 |
362 | export const queryRemoveEntity = (world, q, eid) => {
363 | if (!q.has(eid) || q.toRemove.has(eid)) return
364 | q.toRemove.add(eid)
365 | world[$dirtyQueries].add(q)
366 | q.exited.add(eid)
367 | }
368 |
369 |
370 | /**
371 | * Resets a Changed-based query, clearing the underlying list of changed entities.
372 | *
373 | * @param {World} world
374 | * @param {function} query
375 | */
376 | export const resetChangedQuery = (world, query) => {
377 | const q = world[$queryMap].get(query)
378 | q.changed = []
379 | }
380 |
381 | /**
382 | * Removes a query from a world.
383 | *
384 | * @param {World} world
385 | * @param {function} query
386 | */
387 | export const removeQuery = (world, query) => {
388 | const q = world[$queryMap].get(query)
389 | world[$queries].delete(q)
390 | world[$queryMap].delete(query)
391 | }
--------------------------------------------------------------------------------
/src/Serialize.js:
--------------------------------------------------------------------------------
1 | import { $indexBytes, $indexType, $isEidType, $serializeShadow, $storeBase, $storeFlattened, $tagStore, createShadow } from "./Storage.js"
2 | import { $componentMap, addComponent, hasComponent } from "./Component.js"
3 | import { $entityArray, $entitySparseSet, addEntity, eidToWorld } from "./Entity.js"
4 | import { $localEntities, $localEntityLookup } from "./World.js"
5 | import { SparseSet } from "./Util.js"
6 | import { $modifier } from "./Query.js"
7 |
8 | export const DESERIALIZE_MODE = {
9 | REPLACE: 0,
10 | APPEND: 1,
11 | MAP: 2
12 | }
13 |
14 | let resized = false
15 |
16 | export const setSerializationResized = v => { resized = v }
17 |
18 | const concat = (a,v) => a.concat(v)
19 | const not = fn => v => !fn(v)
20 |
21 | const storeFlattened = c => c[$storeFlattened]
22 | const isFullComponent = storeFlattened
23 | const isProperty = not(isFullComponent)
24 |
25 | const isModifier = c => typeof c === "function" && c[$modifier]
26 | const isNotModifier = not(isModifier)
27 |
28 | const isChangedModifier = c => isModifier(c) && c()[1] === 'changed'
29 |
30 | const isWorld = w => Object.getOwnPropertySymbols(w).includes($componentMap)
31 |
32 | const fromModifierToComponent = c => c()[0]
33 |
34 | export const canonicalize = target => {
35 |
36 | if (isWorld(target)) return [[],new Map()]
37 |
38 | // aggregate full components
39 | const fullComponentProps = target
40 | .filter(isNotModifier)
41 | .filter(isFullComponent)
42 | .map(storeFlattened).reduce(concat, [])
43 |
44 | // aggregate changed full components
45 | const changedComponentProps = target
46 | .filter(isChangedModifier).map(fromModifierToComponent)
47 | .filter(isFullComponent)
48 | .map(storeFlattened).reduce(concat, [])
49 |
50 | // aggregate props
51 | const props = target
52 | .filter(isNotModifier)
53 | .filter(isProperty)
54 |
55 | // aggregate changed props
56 | const changedProps = target
57 | .filter(isChangedModifier).map(fromModifierToComponent)
58 | .filter(isProperty)
59 |
60 | const componentProps = [...fullComponentProps, ...props, ...changedComponentProps, ...changedProps]
61 | const allChangedProps = [...changedComponentProps, ...changedProps].reduce((map,prop) => {
62 | const $ = Symbol()
63 | createShadow(prop, $)
64 | map.set(prop, $)
65 | return map
66 | }, new Map())
67 |
68 | return [componentProps, allChangedProps]
69 | }
70 |
71 | /**
72 | * Defines a new serializer which targets the given components to serialize the data of when called on a world or array of EIDs.
73 | *
74 | * @param {object|array} target
75 | * @param {number} [maxBytes=20000000]
76 | * @returns {function} serializer
77 | */
78 | export const defineSerializer = (target, maxBytes = 20000000) => {
79 | const worldSerializer = isWorld(target)
80 |
81 | let [componentProps, changedProps] = canonicalize(target)
82 |
83 | // TODO: calculate max bytes based on target & recalc upon resize
84 |
85 | const buffer = new ArrayBuffer(maxBytes)
86 | const view = new DataView(buffer)
87 |
88 | const entityComponentCache = new Map()
89 |
90 | return (ents) => {
91 |
92 | if (resized) {
93 | [componentProps, changedProps] = canonicalize(target)
94 | resized = false
95 | }
96 |
97 | if (worldSerializer) {
98 | componentProps = []
99 | target[$componentMap].forEach((c, component) => {
100 | if (component[$storeFlattened])
101 | componentProps.push(...component[$storeFlattened])
102 | else componentProps.push(component)
103 | })
104 | }
105 |
106 | let world
107 | if (Object.getOwnPropertySymbols(ents).includes($componentMap)) {
108 | world = ents
109 | ents = ents[$entityArray]
110 | } else {
111 | world = eidToWorld.get(ents[0])
112 | }
113 |
114 | let where = 0
115 |
116 | if (!ents.length) return buffer.slice(0, where)
117 |
118 | const cache = new Map()
119 |
120 | // iterate over component props
121 | for (let pid = 0; pid < componentProps.length; pid++) {
122 | const prop = componentProps[pid]
123 | const component = prop[$storeBase]()
124 | const $diff = changedProps.get(prop)
125 | const shadow = $diff ? prop[$diff] : null
126 |
127 | if (!cache.has(component)) cache.set(component, new Map())
128 |
129 | // write pid
130 | view.setUint8(where, pid)
131 | where += 1
132 |
133 | // save space for entity count
134 | const countWhere = where
135 | where += 4
136 |
137 | let writeCount = 0
138 | // write eid,val
139 | for (let i = 0; i < ents.length; i++) {
140 | const eid = ents[i]
141 |
142 | let componentCache = entityComponentCache.get(eid)
143 | if (!componentCache) componentCache = entityComponentCache.set(eid, new Set()).get(eid)
144 |
145 | componentCache.add(eid)
146 |
147 | const newlyAddedComponent =
148 | // if we are diffing
149 | shadow
150 | // and we have already iterated over this component for this entity
151 | // retrieve cached value
152 | && cache.get(component).get(eid)
153 | // or if entity did not have component last call
154 | || !componentCache.has(component)
155 | // and entity has component this call
156 | && hasComponent(world, component, eid)
157 |
158 | cache.get(component).set(eid, newlyAddedComponent)
159 |
160 | if (newlyAddedComponent) {
161 | componentCache.add(component)
162 | } else if (!hasComponent(world, component, eid)) {
163 | // skip if entity doesn't have this component
164 | componentCache.delete(component)
165 | continue
166 | }
167 |
168 |
169 | const rewindWhere = where
170 |
171 | // write eid
172 | view.setUint32(where, eid)
173 | where += 4
174 |
175 | // if it's a tag store we can stop here
176 | if (prop[$tagStore]) {
177 | writeCount++
178 | continue
179 | }
180 |
181 | // if property is an array
182 | if (ArrayBuffer.isView(prop[eid])) {
183 | const type = prop[eid].constructor.name.replace('Array', '')
184 | const indexType = prop[eid][$indexType]
185 | const indexBytes = prop[eid][$indexBytes]
186 |
187 | // save space for count of dirty array elements
188 | const countWhere2 = where
189 | where += indexBytes
190 |
191 | let arrayWriteCount = 0
192 |
193 | // write index,value
194 | for (let i = 0; i < prop[eid].length; i++) {
195 |
196 | if (shadow) {
197 |
198 | const changed = shadow[eid][i] !== prop[eid][i]
199 |
200 | // sync shadow
201 | shadow[eid][i] = prop[eid][i]
202 |
203 | // if state has not changed since the last call
204 | // todo: if newly added then entire component will serialize (instead of only changed values)
205 | if (!changed && !newlyAddedComponent) {
206 | // skip writing this value
207 | continue
208 | }
209 | }
210 |
211 | // write array index
212 | view[`set${indexType}`](where, i)
213 | where += indexBytes
214 |
215 | // write value at that index
216 | const value = prop[eid][i]
217 | view[`set${type}`](where, value)
218 | where += prop[eid].BYTES_PER_ELEMENT
219 | arrayWriteCount++
220 | }
221 |
222 | if (arrayWriteCount > 0) {
223 | // write total element count
224 | view[`set${indexType}`](countWhere2, arrayWriteCount)
225 | writeCount++
226 | } else {
227 | where = rewindWhere
228 | continue
229 | }
230 | } else {
231 |
232 | if (shadow) {
233 |
234 | const changed = shadow[eid] !== prop[eid]
235 |
236 | shadow[eid] = prop[eid]
237 |
238 | // do not write value if diffing and no change
239 | if (!changed && !newlyAddedComponent) {
240 | // rewind the serializer
241 | where = rewindWhere
242 | // skip writing this value
243 | continue
244 | }
245 |
246 | }
247 |
248 |
249 | const type = prop.constructor.name.replace('Array', '')
250 | // set value next [type] bytes
251 | view[`set${type}`](where, prop[eid])
252 | where += prop.BYTES_PER_ELEMENT
253 |
254 | writeCount++
255 | }
256 | }
257 |
258 | if (writeCount > 0) {
259 | // write how many eid/value pairs were written
260 | view.setUint32(countWhere, writeCount)
261 | } else {
262 | // if nothing was written (diffed with no changes)
263 | // then move cursor back 5 bytes (remove PID and countWhere space)
264 | where -= 5
265 | }
266 | }
267 | return buffer.slice(0, where)
268 | }
269 | }
270 |
271 | const newEntities = new Map()
272 |
273 | /**
274 | * Defines a new deserializer which targets the given components to deserialize onto a given world.
275 | *
276 | * @param {object|array} target
277 | * @returns {function} deserializer
278 | */
279 | export const defineDeserializer = (target) => {
280 | const isWorld = Object.getOwnPropertySymbols(target).includes($componentMap)
281 | let [componentProps] = canonicalize(target)
282 |
283 | const deserializedEntities = new Set()
284 |
285 | return (world, packet, mode=0) => {
286 |
287 | newEntities.clear()
288 |
289 | if (resized) {
290 | [componentProps] = canonicalize(target)
291 | resized = false
292 | }
293 |
294 | if (isWorld) {
295 | componentProps = []
296 | target[$componentMap].forEach((c, component) => {
297 | if (component[$storeFlattened])
298 | componentProps.push(...component[$storeFlattened])
299 | else componentProps.push(component)
300 | })
301 | }
302 |
303 | const localEntities = world[$localEntities]
304 | const localEntityLookup = world[$localEntityLookup]
305 |
306 | const view = new DataView(packet)
307 | let where = 0
308 |
309 | while (where < packet.byteLength) {
310 |
311 | // pid
312 | const pid = view.getUint8(where)
313 | where += 1
314 |
315 | // entity count
316 | const entityCount = view.getUint32(where)
317 | where += 4
318 |
319 | // component property
320 | const prop = componentProps[pid]
321 |
322 | // Get the entities and set their prop values
323 | for (let i = 0; i < entityCount; i++) {
324 | let eid = view.getUint32(where) // throws with [changed, c, changed]
325 | where += 4
326 |
327 | if (mode === DESERIALIZE_MODE.MAP) {
328 | if (localEntities.has(eid)) {
329 | eid = localEntities.get(eid)
330 | } else if (newEntities.has(eid)) {
331 | eid = newEntities.get(eid)
332 | } else {
333 | const newEid = addEntity(world)
334 | localEntities.set(eid, newEid)
335 | localEntityLookup.set(newEid, eid)
336 | newEntities.set(eid, newEid)
337 | eid = newEid
338 | }
339 | }
340 |
341 | if (mode === DESERIALIZE_MODE.APPEND ||
342 | mode === DESERIALIZE_MODE.REPLACE && !world[$entitySparseSet].has(eid)
343 | ) {
344 | const newEid = newEntities.get(eid) || addEntity(world)
345 | newEntities.set(eid, newEid)
346 | eid = newEid
347 | }
348 |
349 | const component = prop[$storeBase]()
350 | if (!hasComponent(world, component, eid)) {
351 | addComponent(world, component, eid)
352 | }
353 |
354 | // add eid to deserialized ents after it has been transformed by MAP mode
355 | deserializedEntities.add(eid)
356 |
357 | if (component[$tagStore]) {
358 | continue
359 | }
360 |
361 | if (ArrayBuffer.isView(prop[eid])) {
362 | const array = prop[eid]
363 | const count = view[`get${array[$indexType]}`](where)
364 | where += array[$indexBytes]
365 |
366 | // iterate over count
367 | for (let i = 0; i < count; i++) {
368 | const index = view[`get${array[$indexType]}`](where)
369 | where += array[$indexBytes]
370 |
371 | const value = view[`get${array.constructor.name.replace('Array', '')}`](where)
372 | where += array.BYTES_PER_ELEMENT
373 | if (prop[$isEidType]) {
374 | let localEid
375 | if (localEntities.has(value)) {
376 | localEid = localEntities.get(value)
377 | } else if (newEntities.has(value)) {
378 | localEid = newEntities.get(value)
379 | } else {
380 | const newEid = addEntity(world)
381 | localEntities.set(value, newEid)
382 | localEntityLookup.set(newEid, value)
383 | newEntities.set(value, newEid)
384 | localEid = newEid
385 | }
386 | prop[eid][index] = localEid
387 | } else prop[eid][index] = value
388 | }
389 | } else {
390 | const value = view[`get${prop.constructor.name.replace('Array', '')}`](where)
391 | where += prop.BYTES_PER_ELEMENT
392 |
393 | if (prop[$isEidType]) {
394 | let localEid
395 | if (localEntities.has(value)) {
396 | localEid = localEntities.get(value)
397 | } else if (newEntities.has(value)) {
398 | localEid = newEntities.get(value)
399 | } else {
400 | const newEid = addEntity(world)
401 | localEntities.set(value, newEid)
402 | localEntityLookup.set(newEid, value)
403 | newEntities.set(value, newEid)
404 | localEid = newEid
405 | }
406 | prop[eid] = localEid
407 | } else prop[eid] = value
408 | }
409 | }
410 | }
411 |
412 | const ents = Array.from(deserializedEntities)
413 |
414 | deserializedEntities.clear()
415 |
416 | return ents
417 | }
418 | }
--------------------------------------------------------------------------------
/src/Storage.js:
--------------------------------------------------------------------------------
1 | import { TYPES, TYPES_ENUM, TYPES_NAMES, UNSIGNED_MAX } from './Constants.js'
2 | // import { createAllocator } from './Allocator.js'
3 |
4 | const roundToMultiple = mul => x => Math.ceil(x / mul) * mul
5 | const roundToMultiple4 = roundToMultiple(4)
6 |
7 | export const $storeRef = Symbol('storeRef')
8 | export const $storeSize = Symbol('storeSize')
9 | export const $storeMaps = Symbol('storeMaps')
10 | export const $storeFlattened = Symbol('storeFlattened')
11 | export const $storeBase = Symbol('storeBase')
12 | export const $storeType = Symbol('storeType')
13 |
14 | export const $storeArrayElementCounts = Symbol('storeArrayElementCounts')
15 | export const $storeSubarrays = Symbol('storeSubarrays')
16 | export const $subarrayCursors = Symbol('subarrayCursors')
17 | export const $subarray = Symbol('subarray')
18 | export const $subarrayFrom = Symbol('subarrayFrom')
19 | export const $subarrayTo = Symbol('subarrayTo')
20 | export const $parentArray = Symbol('parentArray')
21 | export const $tagStore = Symbol('tagStore')
22 |
23 | export const $queryShadow = Symbol('queryShadow')
24 | export const $serializeShadow = Symbol('serializeShadow')
25 |
26 | export const $indexType = Symbol('indexType')
27 | export const $indexBytes = Symbol('indexBytes')
28 |
29 | export const $isEidType = Symbol('isEidType')
30 |
31 | const stores = {}
32 |
33 | // const alloc = createAllocator()
34 |
35 | export const resize = (ta, size) => {
36 | const newBuffer = new ArrayBuffer(size * ta.BYTES_PER_ELEMENT)
37 | const newTa = new ta.constructor(newBuffer)
38 | newTa.set(ta, 0)
39 | return newTa
40 | }
41 |
42 | export const createShadow = (store, key) => {
43 | if (!ArrayBuffer.isView(store)) {
44 | const shadowStore = store[$parentArray].slice(0)
45 | store[key] = store.map((_,eid) => {
46 | const { length } = store[eid]
47 | const start = length * eid
48 | const end = start + length
49 | return shadowStore.subarray(start, end)
50 | })
51 | } else {
52 | store[key] = store.slice(0)
53 | }
54 | }
55 |
56 | const resizeSubarray = (metadata, store, storeSize) => {
57 | const cursors = metadata[$subarrayCursors]
58 | let type = store[$storeType]
59 | const length = store[0].length
60 | const indexType =
61 | length <= UNSIGNED_MAX.uint8
62 | ? TYPES_ENUM.ui8
63 | : length <= UNSIGNED_MAX.uint16
64 | ? TYPES_ENUM.ui16
65 | : TYPES_ENUM.ui32
66 |
67 | if (cursors[type] === 0) {
68 |
69 | const arrayElementCount = metadata[$storeArrayElementCounts][type]
70 |
71 | // // for threaded impl
72 | // // const summedBytesPerElement = Array(arrayCount).fill(0).reduce((a, p) => a + TYPES[type].BYTES_PER_ELEMENT, 0)
73 | // // const totalBytes = roundToMultiple4(summedBytesPerElement * summedLength * size)
74 | // // const buffer = new SharedArrayBuffer(totalBytes)
75 |
76 | const array = new TYPES[type](roundToMultiple4(arrayElementCount * storeSize))
77 |
78 | array.set(metadata[$storeSubarrays][type])
79 |
80 | metadata[$storeSubarrays][type] = array
81 |
82 | array[$indexType] = TYPES_NAMES[indexType]
83 | array[$indexBytes] = TYPES[indexType].BYTES_PER_ELEMENT
84 | }
85 |
86 | const start = cursors[type]
87 | const end = start + (storeSize * length)
88 | cursors[type] = end
89 |
90 | store[$parentArray] = metadata[$storeSubarrays][type].subarray(start, end)
91 |
92 | // pre-generate subarrays for each eid
93 | for (let eid = 0; eid < storeSize; eid++) {
94 | const start = length * eid
95 | const end = start + length
96 | store[eid] = store[$parentArray].subarray(start, end)
97 | store[eid][$indexType] = TYPES_NAMES[indexType]
98 | store[eid][$indexBytes] = TYPES[indexType].BYTES_PER_ELEMENT
99 | store[eid][$subarray] = true
100 | }
101 |
102 | }
103 |
104 | const resizeRecursive = (metadata, store, size) => {
105 | Object.keys(store).forEach(key => {
106 | const ta = store[key]
107 | if (Array.isArray(ta)) {
108 | resizeSubarray(metadata, ta, size)
109 | store[$storeFlattened].push(ta)
110 | } else if (ArrayBuffer.isView(ta)) {
111 | store[key] = resize(ta, size)
112 | store[$storeFlattened].push(store[key])
113 | } else if (typeof ta === 'object') {
114 | resizeRecursive(metadata, store[key], size)
115 | }
116 | })
117 | }
118 |
119 | export const resizeStore = (store, size) => {
120 | if (store[$tagStore]) return
121 | store[$storeSize] = size
122 | store[$storeFlattened].length = 0
123 | Object.keys(store[$subarrayCursors]).forEach(k => {
124 | store[$subarrayCursors][k] = 0
125 | })
126 | resizeRecursive(store, store, size)
127 | }
128 |
129 | export const resetStore = store => {
130 | if (store[$storeFlattened]) {
131 | store[$storeFlattened].forEach(ta => {
132 | ta.fill(0)
133 | })
134 | Object.keys(store[$storeSubarrays]).forEach(key => {
135 | store[$storeSubarrays][key].fill(0)
136 | })
137 | }
138 | }
139 |
140 | export const resetStoreFor = (store, eid) => {
141 | if (store[$storeFlattened]) {
142 | store[$storeFlattened].forEach(ta => {
143 | if (ArrayBuffer.isView(ta)) ta[eid] = 0
144 | else ta[eid].fill(0)
145 | })
146 | }
147 | }
148 |
149 | const createTypeStore = (type, length) => {
150 | const totalBytes = length * TYPES[type].BYTES_PER_ELEMENT
151 | const buffer = new ArrayBuffer(totalBytes)
152 | const store = new TYPES[type](buffer)
153 | store[$isEidType] = type === TYPES_ENUM.eid
154 | return store
155 | }
156 |
157 | export const parentArray = store => store[$parentArray]
158 |
159 | const createArrayStore = (metadata, type, length) => {
160 | const storeSize = metadata[$storeSize]
161 | const store = Array(storeSize).fill(0)
162 | store[$storeType] = type
163 | store[$isEidType] = type === TYPES_ENUM.eid
164 |
165 | const cursors = metadata[$subarrayCursors]
166 | const indexType =
167 | length <= UNSIGNED_MAX.uint8
168 | ? TYPES_ENUM.ui8
169 | : length <= UNSIGNED_MAX.uint16
170 | ? TYPES_ENUM.ui16
171 | : TYPES_ENUM.ui32
172 |
173 | if (!length) throw new Error('bitECS - Must define component array length')
174 | if (!TYPES[type]) throw new Error(`bitECS - Invalid component array property type ${type}`)
175 |
176 | // create buffer for type if it does not already exist
177 | if (!metadata[$storeSubarrays][type]) {
178 | const arrayElementCount = metadata[$storeArrayElementCounts][type]
179 |
180 | // for threaded impl
181 | // const summedBytesPerElement = Array(arrayCount).fill(0).reduce((a, p) => a + TYPES[type].BYTES_PER_ELEMENT, 0)
182 | // const totalBytes = roundToMultiple4(summedBytesPerElement * summedLength * size)
183 | // const buffer = new SharedArrayBuffer(totalBytes)
184 |
185 | const array = new TYPES[type](roundToMultiple4(arrayElementCount * storeSize))
186 | array[$indexType] = TYPES_NAMES[indexType]
187 | array[$indexBytes] = TYPES[indexType].BYTES_PER_ELEMENT
188 |
189 | metadata[$storeSubarrays][type] = array
190 |
191 | }
192 |
193 | const start = cursors[type]
194 | const end = start + (storeSize * length)
195 | cursors[type] = end
196 |
197 | store[$parentArray] = metadata[$storeSubarrays][type].subarray(start, end)
198 |
199 | // pre-generate subarrays for each eid
200 | for (let eid = 0; eid < storeSize; eid++) {
201 | const start = length * eid
202 | const end = start + length
203 | store[eid] = store[$parentArray].subarray(start, end)
204 | store[eid][$indexType] = TYPES_NAMES[indexType]
205 | store[eid][$indexBytes] = TYPES[indexType].BYTES_PER_ELEMENT
206 | store[eid][$subarray] = true
207 | }
208 |
209 | return store
210 | }
211 |
212 | const isArrayType = x => Array.isArray(x) && typeof x[0] === 'string' && typeof x[1] === 'number'
213 |
214 | export const createStore = (schema, size) => {
215 | const $store = Symbol('store')
216 |
217 | if (!schema || !Object.keys(schema).length) {
218 | // tag component
219 | stores[$store] = {
220 | [$storeSize]: size,
221 | [$tagStore]: true,
222 | [$storeBase]: () => stores[$store]
223 | }
224 | return stores[$store]
225 | }
226 |
227 | schema = JSON.parse(JSON.stringify(schema))
228 |
229 | const arrayElementCounts = {}
230 | const collectArrayElementCounts = s => {
231 | const keys = Object.keys(s)
232 | for (const k of keys) {
233 | if (isArrayType(s[k])) {
234 | if (!arrayElementCounts[s[k][0]]) arrayElementCounts[s[k][0]] = 0
235 | arrayElementCounts[s[k][0]] += s[k][1]
236 | } else if (s[k] instanceof Object) {
237 | collectArrayElementCounts(s[k])
238 | }
239 | }
240 | }
241 | collectArrayElementCounts(schema)
242 |
243 | const metadata = {
244 | [$storeSize]: size,
245 | [$storeMaps]: {},
246 | [$storeSubarrays]: {},
247 | [$storeRef]: $store,
248 | [$subarrayCursors]: Object.keys(TYPES).reduce((a, type) => ({ ...a, [type]: 0 }), {}),
249 | [$storeFlattened]: [],
250 | [$storeArrayElementCounts]: arrayElementCounts
251 | }
252 |
253 | if (schema instanceof Object && Object.keys(schema).length) {
254 |
255 | const recursiveTransform = (a, k) => {
256 |
257 | if (typeof a[k] === 'string') {
258 |
259 | a[k] = createTypeStore(a[k], size)
260 | a[k][$storeBase] = () => stores[$store]
261 | metadata[$storeFlattened].push(a[k])
262 |
263 | } else if (isArrayType(a[k])) {
264 |
265 | const [type, length] = a[k]
266 | a[k] = createArrayStore(metadata, type, length)
267 | a[k][$storeBase] = () => stores[$store]
268 | metadata[$storeFlattened].push(a[k])
269 | // Object.seal(a[k])
270 |
271 | } else if (a[k] instanceof Object) {
272 |
273 | a[k] = Object.keys(a[k]).reduce(recursiveTransform, a[k])
274 | // Object.seal(a[k])
275 |
276 | }
277 |
278 | return a
279 | }
280 |
281 | stores[$store] = Object.assign(Object.keys(schema).reduce(recursiveTransform, schema), metadata)
282 | stores[$store][$storeBase] = () => stores[$store]
283 |
284 | // Object.seal(stores[$store])
285 |
286 | return stores[$store]
287 |
288 | }
289 | }
290 |
291 | export const free = (store) => {
292 | delete stores[store[$storeRef]]
293 | }
--------------------------------------------------------------------------------
/src/System.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Defines a new system function.
3 | *
4 | * @param {function} update
5 | * @returns {function}
6 | */
7 | export const defineSystem = (update) => (world, ...args) => {
8 | update(world, ...args)
9 | return world
10 | }
--------------------------------------------------------------------------------
/src/Util.js:
--------------------------------------------------------------------------------
1 | export const Uint32SparseSet = (length) => {
2 | const dense = new Uint32Array(length)
3 | const sparse = new Uint32Array(length)
4 |
5 | let cursor = 0
6 | dense.count = () => cursor + 1
7 |
8 | const has = val => dense[sparse[val]] === val
9 |
10 | const add = val => {
11 | if (has(val)) return
12 | sparse[val] = cursor
13 | dense[cursor] = val
14 |
15 | cursor++
16 | }
17 |
18 | const remove = val => {
19 | if (!has(val)) return
20 | const index = sparse[val]
21 | const swapped = dense[cursor]
22 | if (swapped !== val) {
23 | dense[index] = swapped
24 | sparse[swapped] = index
25 | }
26 |
27 | cursor--
28 | }
29 |
30 | return {
31 | add,
32 | remove,
33 | has,
34 | sparse,
35 | dense,
36 | }
37 | }
38 |
39 | export const SparseSet = () => {
40 | const dense = []
41 | const sparse = []
42 |
43 | dense.sort = function (comparator) {
44 | const result = Array.prototype.sort.call(this, comparator)
45 |
46 | for(let i = 0; i < dense.length; i++) {
47 | sparse[dense[i]] = i
48 | }
49 |
50 | return result
51 | }
52 |
53 | const has = val => dense[sparse[val]] === val
54 |
55 | const add = val => {
56 | if (has(val)) return
57 | sparse[val] = dense.push(val) - 1
58 | }
59 |
60 | const remove = val => {
61 | if (!has(val)) return
62 | const index = sparse[val]
63 | const swapped = dense.pop()
64 | if (swapped !== val) {
65 | dense[index] = swapped
66 | sparse[swapped] = index
67 | }
68 | }
69 |
70 | const reset = () => {
71 | dense.length = 0
72 | sparse.length = 0
73 | }
74 |
75 | return {
76 | add,
77 | remove,
78 | has,
79 | sparse,
80 | dense,
81 | reset,
82 | }
83 | }
--------------------------------------------------------------------------------
/src/World.js:
--------------------------------------------------------------------------------
1 | import { $componentMap } from './Component.js'
2 | import { $queryMap, $queries, $dirtyQueries, $notQueries } from './Query.js'
3 | import { $entityArray, $entityComponents, $entityMasks, $entitySparseSet, getGlobalSize, removeEntity } from './Entity.js'
4 | import { resize } from './Storage.js'
5 | import { SparseSet } from './Util.js'
6 |
7 | export const $size = Symbol('size')
8 | export const $resizeThreshold = Symbol('resizeThreshold')
9 | export const $bitflag = Symbol('bitflag')
10 | export const $archetypes = Symbol('archetypes')
11 | export const $localEntities = Symbol('localEntities')
12 | export const $localEntityLookup = Symbol('localEntityLookup')
13 | export const $manualEntityRecycling = Symbol('manualEntityRecycling')
14 |
15 | export const worlds = []
16 |
17 | export const resizeWorlds = (size) => {
18 | worlds.forEach(world => {
19 | world[$size] = size
20 |
21 | for (let i = 0; i < world[$entityMasks].length; i++) {
22 | const masks = world[$entityMasks][i];
23 | world[$entityMasks][i] = resize(masks, size)
24 | }
25 |
26 | world[$resizeThreshold] = world[$size] - (world[$size] / 5)
27 | })
28 | }
29 |
30 | /**
31 | * Creates a new world.
32 | *
33 | * @returns {object}
34 | */
35 | export const createWorld = (...args) => {
36 | const world = typeof args[0] === 'object'
37 | ? args[0]
38 | : {}
39 | const size = typeof args[0] === 'number'
40 | ? args[0]
41 | : typeof args[1] === 'number'
42 | ? args[1]
43 | : getGlobalSize()
44 | resetWorld(world, size)
45 | worlds.push(world)
46 | return world
47 | }
48 |
49 | export const enableManualEntityRecycling = (world) => {
50 | world[$manualEntityRecycling] = true
51 | }
52 |
53 | /**
54 | * Resets a world.
55 | *
56 | * @param {World} world
57 | * @returns {object}
58 | */
59 | export const resetWorld = (world, size = getGlobalSize()) => {
60 | world[$size] = size
61 |
62 | if (world[$entityArray]) world[$entityArray].forEach(eid => removeEntity(world, eid))
63 |
64 | world[$entityMasks] = [new Uint32Array(size)]
65 | world[$entityComponents] = new Map()
66 | world[$archetypes] = []
67 |
68 | world[$entitySparseSet] = SparseSet()
69 | world[$entityArray] = world[$entitySparseSet].dense
70 |
71 | world[$bitflag] = 1
72 |
73 | world[$componentMap] = new Map()
74 |
75 | world[$queryMap] = new Map()
76 | world[$queries] = new Set()
77 | world[$notQueries] = new Set()
78 | world[$dirtyQueries] = new Set()
79 |
80 | world[$localEntities] = new Map()
81 | world[$localEntityLookup] = new Map()
82 |
83 | world[$manualEntityRecycling] = false
84 |
85 | return world
86 | }
87 |
88 | /**
89 | * Deletes a world.
90 | *
91 | * @param {World} world
92 | */
93 | export const deleteWorld = (world) => {
94 | Object.getOwnPropertySymbols(world).forEach($ => { delete world[$] })
95 | Object.keys(world).forEach(key => { delete world[key] })
96 | worlds.splice(worlds.indexOf(world), 1)
97 | }
98 |
99 | /**
100 | * Returns all components registered to a world
101 | *
102 | * @param {World} world
103 | * @returns Array
104 | */
105 | export const getWorldComponents = (world) => Array.from(world[$componentMap].keys())
106 |
107 | /**
108 | * Returns all existing entities in a world
109 | *
110 | * @param {World} world
111 | * @returns Array
112 | */
113 | export const getAllEntities = (world) => world[$entitySparseSet].dense.slice(0)
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { createWorld, resetWorld, deleteWorld, getWorldComponents, getAllEntities, enableManualEntityRecycling } from './World.js'
2 | import { addEntity, removeEntity, setDefaultSize, setRemovedRecycleThreshold, getEntityComponents, entityExists, flushRemovedEntities, resetGlobals } from './Entity.js'
3 | import { defineComponent, registerComponent, registerComponents, hasComponent, addComponent, removeComponent } from './Component.js'
4 | import { defineSystem } from './System.js'
5 | import { defineQuery, enterQuery, exitQuery, Changed, Not, commitRemovals, resetChangedQuery, removeQuery } from './Query.js'
6 | import { defineSerializer, defineDeserializer, DESERIALIZE_MODE } from './Serialize.js'
7 | import { parentArray } from './Storage.js'
8 | import { TYPES_ENUM } from './Constants.js'
9 |
10 | export const pipe = (...fns) => (input) => {
11 | let tmp = input
12 | for (let i = 0; i < fns.length; i++) {
13 | const fn = fns[i]
14 | tmp = fn(tmp)
15 | }
16 | return tmp
17 | }
18 |
19 | export const Types = TYPES_ENUM
20 |
21 | export {
22 |
23 | setDefaultSize,
24 | setRemovedRecycleThreshold,
25 | createWorld,
26 | resetWorld,
27 | deleteWorld,
28 | addEntity,
29 | removeEntity,
30 | entityExists,
31 | getWorldComponents,
32 | enableManualEntityRecycling,
33 | flushRemovedEntities,
34 | getAllEntities,
35 |
36 | registerComponent,
37 | registerComponents,
38 | defineComponent,
39 | addComponent,
40 | removeComponent,
41 | hasComponent,
42 | getEntityComponents,
43 |
44 | defineQuery,
45 | Changed,
46 | Not,
47 | enterQuery,
48 | exitQuery,
49 | commitRemovals,
50 | resetChangedQuery,
51 | removeQuery,
52 |
53 | defineSystem,
54 |
55 | defineSerializer,
56 | defineDeserializer,
57 | DESERIALIZE_MODE,
58 |
59 | parentArray,
60 |
61 | resetGlobals
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // This is here so tsconfig.json doesnt throw.
2 | // Typescript port is coming soon.
--------------------------------------------------------------------------------
/test/adhoc/deserialize-enter-query-bug.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import { resetGlobals } from '../../src/Entity.js'
3 | import {
4 | Types,
5 | addComponent,
6 | addEntity,
7 | createWorld,
8 | defineComponent,
9 | defineSerializer,
10 | defineDeserializer,
11 | defineQuery,
12 | enterQuery,
13 | DESERIALIZE_MODE,
14 | Changed
15 | } from '../../src/index.js'
16 |
17 | function getLocalEid(world, eid) {
18 | const $localEntities = Object.getOwnPropertySymbols(world)[12]
19 | // @ts-ignore
20 | const localEntities = world[$localEntities]
21 | return localEntities.get(eid)
22 | }
23 |
24 | describe('adhoc deserialize enter query bug', () => {
25 | afterEach(() => {
26 | resetGlobals()
27 | })
28 |
29 | it('should', () => {
30 | const world = createWorld()
31 |
32 | const Component = defineComponent({
33 | x: Types.f32,
34 | y: [Types.f32,4],
35 | rotation: Types.f32
36 | })
37 |
38 | const Component2 = defineComponent({
39 | x: Types.f32,
40 | y: [Types.f32,4],
41 | rotation: Types.f32
42 | })
43 |
44 | const eid = addEntity(world)
45 | const x = 5.0
46 | const y = 3.0
47 | const rotation = 1
48 |
49 | addComponent(world, Component, eid)
50 | Component.x[eid] = x
51 | Component.y[eid].set([1,2,3,4])
52 | Component.rotation[eid] = rotation
53 |
54 | const serialize = defineSerializer([ Changed(Component) ])
55 | const deserialize = defineDeserializer([ Component2 ])
56 |
57 | const world2 = createWorld()
58 | const query = defineQuery([ Component2 ])
59 | const enter = enterQuery(query)
60 |
61 | deserialize(world2, serialize(world), DESERIALIZE_MODE.MAP)
62 |
63 | const lid = getLocalEid(world2, eid)
64 |
65 | assert.equal(enter(world2)[0], lid, 'World 2 Enter should be 1')
66 | assert.equal(Component2.x[lid], x, 'Should have x value')
67 | assert.deepEqual(Array.from(Component2.y[lid]), [1,2,3,4], 'Should have y value')
68 | assert.equal(Component2.rotation[lid], rotation, 'Should have rotation value')
69 | })
70 | })
--------------------------------------------------------------------------------
/test/integration/Component.test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import { Types } from '../../src/index.js'
3 | import { createWorld } from '../../src/World.js'
4 | import { $componentMap, addComponent, defineComponent, hasComponent, registerComponent, removeComponent } from '../../src/Component.js'
5 | import { addEntity, resetGlobals } from '../../src/Entity.js'
6 |
7 | describe('Component Integration Tests', () => {
8 | afterEach(() => {
9 | resetGlobals()
10 | })
11 | it('should register components on-demand', () => {
12 | const world = createWorld()
13 | const TestComponent = defineComponent({ value: Types.f32 })
14 |
15 | registerComponent(world, TestComponent)
16 | assert(world[$componentMap].has(TestComponent))
17 | })
18 | it('should register components automatically upon adding to an entity', () => {
19 | const world = createWorld()
20 | const TestComponent = defineComponent({ value: Types.f32 })
21 |
22 | const eid = addEntity(world)
23 |
24 | addComponent(world, TestComponent, eid)
25 | assert(world[$componentMap].has(TestComponent))
26 | })
27 | it('should add and remove components from an entity', () => {
28 | const world = createWorld()
29 | const TestComponent = defineComponent({ value: Types.f32 })
30 |
31 | const eid = addEntity(world)
32 |
33 | addComponent(world, TestComponent, eid)
34 | assert(hasComponent(world, TestComponent, eid))
35 |
36 | removeComponent(world, TestComponent, eid)
37 | assert(hasComponent(world, TestComponent, eid) === false)
38 | })
39 | it('should only remove the component specified', () => {
40 | const world = createWorld()
41 | const TestComponent = defineComponent({ value: Types.f32 })
42 | const TestComponent2 = defineComponent({ value: Types.f32 })
43 |
44 | const eid = addEntity(world)
45 |
46 | addComponent(world, TestComponent, eid)
47 | addComponent(world, TestComponent2, eid)
48 | assert(hasComponent(world, TestComponent, eid))
49 | assert(hasComponent(world, TestComponent2, eid))
50 |
51 | removeComponent(world, TestComponent, eid)
52 | assert(hasComponent(world, TestComponent, eid) === false)
53 | assert(hasComponent(world, TestComponent2, eid) === true)
54 | })
55 | it('should create tag components', () => {
56 | const world = createWorld()
57 | const TestComponent = defineComponent()
58 |
59 | const eid = addEntity(world)
60 |
61 | addComponent(world, TestComponent, eid)
62 | assert(hasComponent(world, TestComponent, eid))
63 |
64 | removeComponent(world, TestComponent, eid)
65 | assert(hasComponent(world, TestComponent, eid) === false)
66 | })
67 | it('should correctly register more than 32 components', () => {
68 | const world = createWorld()
69 |
70 | const eid = addEntity(world)
71 |
72 | Array(1024).fill(null)
73 | .map(_ => defineComponent())
74 | .forEach((c) => {
75 | addComponent(world, c, eid)
76 | assert(hasComponent(world, c, eid))
77 | })
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/test/integration/Entity.test.js:
--------------------------------------------------------------------------------
1 | import { strictEqual } from 'assert'
2 | import { flushRemovedEntities, getEntityCursor, getRemovedEntities, resetGlobals, setRemovedRecycleThreshold } from '../../src/Entity.js'
3 | import { createWorld, addEntity, removeEntity } from '../../src/index.js'
4 | import { enableManualEntityRecycling } from '../../src/World.js'
5 |
6 | describe('Entity Integration Tests', () => {
7 | afterEach(() => {
8 | resetGlobals()
9 | })
10 | it('should add and remove entities', () => {
11 | const world = createWorld()
12 |
13 | const eid1 = addEntity(world)
14 | strictEqual(getEntityCursor(), 1)
15 |
16 | const eid2 = addEntity(world)
17 | strictEqual(getEntityCursor(), 2)
18 |
19 | const eid3 = addEntity(world)
20 | strictEqual(getEntityCursor(), 3)
21 |
22 | strictEqual(eid1, 0)
23 | strictEqual(eid2, 1)
24 | strictEqual(eid3, 2)
25 |
26 | removeEntity(world, eid1)
27 | removeEntity(world, eid2)
28 | removeEntity(world, eid3)
29 |
30 | const removed = getRemovedEntities()
31 |
32 | strictEqual(removed.length, 3)
33 | strictEqual(removed[0], 0)
34 | strictEqual(removed[1], 1)
35 | strictEqual(removed[2], 2)
36 | })
37 | it('should recycle entity IDs after 1% have been removed by default', () => {
38 | const world = createWorld()
39 | const ents = []
40 |
41 | for (let i = 0; i < 1500; i++) {
42 | const eid = addEntity(world)
43 | ents.push(eid)
44 | strictEqual(getEntityCursor(), eid+1)
45 | strictEqual(eid, i)
46 | }
47 |
48 | strictEqual(getEntityCursor(), 1500)
49 |
50 | for (let i = 0; i < 1000; i++) {
51 | const eid = ents[i]
52 | removeEntity(world, eid)
53 | }
54 |
55 | let eid = addEntity(world)
56 | strictEqual(eid, 1500)
57 |
58 | eid = addEntity(world)
59 | strictEqual(eid, 1501)
60 |
61 | eid = addEntity(world)
62 | strictEqual(eid, 1502)
63 |
64 | eid = addEntity(world)
65 | strictEqual(eid, 1503)
66 |
67 | removeEntity(world, eid)
68 |
69 | eid = addEntity(world)
70 | strictEqual(eid, 0)
71 |
72 | })
73 | it('should flush entity IDs', () => {
74 | const world = createWorld()
75 | enableManualEntityRecycling(world)
76 | const ents = []
77 |
78 | for (let i = 0; i < 1500; i++) {
79 | const eid = addEntity(world)
80 | ents.push(eid)
81 | strictEqual(getEntityCursor(), eid+1)
82 | strictEqual(eid, i)
83 | }
84 |
85 | strictEqual(getEntityCursor(), 1500)
86 |
87 | // remove more than 1%
88 | for (let i = 0; i < 1500; i++) {
89 | const eid = ents[i]
90 | removeEntity(world, eid)
91 | }
92 |
93 | // flush removed ents, making them available again
94 | flushRemovedEntities(world)
95 |
96 | let eid = addEntity(world)
97 | strictEqual(eid, 0)
98 |
99 | eid = addEntity(world)
100 | strictEqual(eid, 1)
101 |
102 | eid = addEntity(world)
103 | strictEqual(eid, 2)
104 |
105 | eid = addEntity(world)
106 | strictEqual(eid, 3)
107 |
108 | removeEntity(world, 3)
109 |
110 | eid = addEntity(world)
111 | strictEqual(eid, 4)
112 |
113 | removeEntity(world, 2)
114 |
115 | eid = addEntity(world)
116 | strictEqual(eid, 5)
117 |
118 | })
119 | it('should be able to configure % of removed entity IDs before recycle', () => {
120 | const world = createWorld()
121 |
122 | setRemovedRecycleThreshold(0.012)
123 |
124 | for (let i = 0; i < 1500; i++) {
125 | const eid = addEntity(world)
126 | strictEqual(getEntityCursor(), eid+1)
127 | strictEqual(eid, i)
128 | }
129 |
130 | strictEqual(getEntityCursor(), 1500)
131 |
132 | for (let i = 0; i < 1200; i++) {
133 | removeEntity(world, i)
134 | }
135 |
136 | let eid = addEntity(world)
137 | strictEqual(eid, 1500)
138 |
139 | eid = addEntity(world)
140 | strictEqual(eid, 1501)
141 |
142 | eid = addEntity(world)
143 | strictEqual(eid, 1502)
144 |
145 | eid = addEntity(world)
146 | strictEqual(eid, 1503)
147 |
148 | removeEntity(world, eid)
149 |
150 | eid = addEntity(world)
151 | strictEqual(eid, 0)
152 |
153 | })
154 | })
155 |
--------------------------------------------------------------------------------
/test/integration/Query.test.js:
--------------------------------------------------------------------------------
1 | import { strictEqual } from 'assert'
2 | import { exitQuery, Types } from '../../src/index.js'
3 | import { createWorld } from '../../src/World.js'
4 | import { addComponent, removeComponent, defineComponent } from '../../src/Component.js'
5 | import { addEntity, removeEntity, resetGlobals } from '../../src/Entity.js'
6 | import { Changed, defineQuery, enterQuery, Not } from '../../src/Query.js'
7 |
8 | describe('Query Integration Tests', () => {
9 | afterEach(() => {
10 | resetGlobals()
11 | })
12 | it('should define a query and return matching eids', () => {
13 | const world = createWorld()
14 | const TestComponent = defineComponent({ value: Types.f32 })
15 | const query = defineQuery([TestComponent])
16 | const eid = addEntity(world)
17 | addComponent(world, TestComponent, eid)
18 |
19 | let ents = query(world)
20 |
21 | strictEqual(ents.length, 1)
22 | strictEqual(ents[0], 0)
23 |
24 | removeEntity(world, eid)
25 |
26 | ents = query(world)
27 | strictEqual(ents.length, 0)
28 | })
29 | it('should define a query with Not and return matching eids', () => {
30 | const world = createWorld()
31 | const Foo = defineComponent({ value: Types.f32 })
32 | const notFooQuery = defineQuery([Not(Foo)])
33 |
34 | const eid0 = addEntity(world)
35 |
36 | let ents = notFooQuery(world)
37 | strictEqual(ents.length, 1)
38 | strictEqual(ents[0], eid0)
39 |
40 | addComponent(world, Foo, eid0)
41 |
42 | ents = notFooQuery(world)
43 | strictEqual(ents.length, 0)
44 |
45 | const eid1 = addEntity(world)
46 |
47 | ents = notFooQuery(world)
48 | strictEqual(ents.length, 1)
49 | strictEqual(ents[0], eid1)
50 |
51 | removeEntity(world, eid1)
52 |
53 | ents = notFooQuery(world)
54 | strictEqual(ents.length, 0)
55 | })
56 | it('should correctly populate Not queries when adding/removing components', () => {
57 | const world = createWorld()
58 |
59 | const Foo = defineComponent()
60 | const Bar = defineComponent()
61 |
62 | const fooQuery = defineQuery([Foo])
63 | const notFooQuery = defineQuery([Not(Foo)])
64 |
65 | const fooBarQuery = defineQuery([Foo, Bar])
66 | const notFooBarQuery = defineQuery([Not(Foo), Not(Bar)])
67 |
68 | const eid0 = addEntity(world)
69 | const eid1 = addEntity(world)
70 | const eid2 = addEntity(world)
71 |
72 | /* initial state */
73 |
74 | // foo query should have nothing
75 | let ents = fooQuery(world)
76 | strictEqual(ents.length, 0)
77 |
78 | // notFoo query should have eid 0, 1, and 2
79 | ents = notFooQuery(world)
80 | strictEqual(ents.length, 3)
81 | strictEqual(ents[0], 0)
82 | strictEqual(ents[1], 1)
83 | strictEqual(ents[2], 2)
84 |
85 | /* add components */
86 |
87 | addComponent(world, Foo, eid0)
88 |
89 | addComponent(world, Bar, eid1)
90 |
91 | addComponent(world, Foo, eid2)
92 | addComponent(world, Bar, eid2)
93 |
94 | // now fooQuery should have eid 0 & 2
95 | ents = fooQuery(world)
96 | strictEqual(ents.length, 2)
97 | strictEqual(ents[0], 0)
98 | strictEqual(ents[1], 2)
99 |
100 | // fooBarQuery should only have eid 2
101 | ents = fooBarQuery(world)
102 | strictEqual(ents.length, 1)
103 | strictEqual(ents[0], 2)
104 |
105 | // notFooBarQuery should have nothing
106 | ents = notFooBarQuery(world)
107 | strictEqual(ents.length, 0)
108 |
109 | // and notFooQuery should have eid 1
110 | ents = notFooQuery(world)
111 | strictEqual(ents.length, 1)
112 | strictEqual(ents[0], 1)
113 |
114 |
115 | /* remove components */
116 |
117 | removeComponent(world, Foo, eid0)
118 |
119 | // now fooQuery should only have eid 2
120 | ents = fooQuery(world)
121 | strictEqual(ents.length, 1)
122 | strictEqual(ents[0], 2)
123 |
124 | // notFooQuery should have eid 0 & 1
125 | ents = notFooQuery(world)
126 | strictEqual(ents.length, 2)
127 | strictEqual(ents[0], 1)
128 | strictEqual(ents[1], 0)
129 |
130 | // fooBarQuery should still only have eid 2
131 | ents = fooBarQuery(world)
132 | strictEqual(ents.length, 1)
133 | strictEqual(ents[0], 2)
134 |
135 | // notFooBarQuery should only have eid 0
136 | ents = notFooBarQuery(world)
137 | strictEqual(ents.length, 1)
138 | strictEqual(ents[0], 0)
139 |
140 |
141 | /* remove more components */
142 |
143 | removeComponent(world, Foo, eid2)
144 | removeComponent(world, Bar, eid2)
145 |
146 | // notFooBarQuery should have eid 0 & 2
147 | ents = notFooBarQuery(world)
148 | strictEqual(ents.length, 2)
149 | strictEqual(ents[0], 0)
150 | strictEqual(ents[1], 2)
151 |
152 | // and notFooQuery should have eid 1, 0, & 2
153 | ents = notFooQuery(world)
154 | strictEqual(ents.length, 3)
155 | strictEqual(ents[0], 1)
156 | strictEqual(ents[1], 0)
157 | strictEqual(ents[2], 2)
158 | })
159 | it('should define a query with Changed and return matching eids whose component state has changed', () => {
160 | const world = createWorld()
161 | const TestComponent = defineComponent({ value: Types.f32 })
162 | const query = defineQuery([Changed(TestComponent)])
163 | const eid1 = addEntity(world)
164 | const eid2 = addEntity(world)
165 | addComponent(world, TestComponent, eid1)
166 | addComponent(world, TestComponent, eid2)
167 |
168 | let ents = query(world)
169 | strictEqual(ents.length, 0)
170 |
171 | TestComponent.value[eid1]++
172 |
173 | ents = query(world)
174 |
175 | strictEqual(ents.length, 1)
176 | strictEqual(ents[0], eid1)
177 | })
178 | it('should define a query for an array component with Changed and return matching eids whose component state has changed', () => {
179 | const world = createWorld()
180 | const ArrayComponent = defineComponent({ value: [Types.f32, 3] })
181 | const query = defineQuery([Changed(ArrayComponent)])
182 | const eid1 = addEntity(world)
183 | const eid2 = addEntity(world)
184 | addComponent(world, ArrayComponent, eid1)
185 | addComponent(world, ArrayComponent, eid2)
186 |
187 | let ents = query(world)
188 | strictEqual(ents.length, 0)
189 |
190 | ArrayComponent.value[eid1][1]++
191 |
192 | ents = query(world)
193 |
194 | strictEqual(ents.length, 1)
195 | strictEqual(ents[0], eid1)
196 | })
197 | it('should return entities from enter/exitQuery who entered/exited the query', () => {
198 | const world = createWorld()
199 | const TestComponent = defineComponent({ value: Types.f32 })
200 | const query = defineQuery([TestComponent])
201 | const enteredQuery = enterQuery(query)
202 | const exitedQuery = exitQuery(query)
203 |
204 | const eid = addEntity(world)
205 | addComponent(world, TestComponent, eid)
206 |
207 | const entered = enteredQuery(world)
208 | strictEqual(entered.length, 1)
209 | strictEqual(entered[0], 0)
210 |
211 | let ents = query(world)
212 | strictEqual(ents.length, 1)
213 | strictEqual(ents[0], 0)
214 |
215 | removeEntity(world, eid)
216 |
217 | ents = query(world)
218 | strictEqual(ents.length, 0)
219 |
220 | const exited = exitedQuery(world)
221 | strictEqual(exited.length, 1)
222 | strictEqual(exited[0], 0)
223 | })
224 | it('shouldn\'t pick up entities in enterQuery after adding a component a second time', () => {
225 | const world = createWorld()
226 | const TestComponent = defineComponent({ value: Types.f32 })
227 | const query = defineQuery([TestComponent])
228 | const enteredQuery = enterQuery(query)
229 |
230 | const eid = addEntity(world)
231 | addComponent(world, TestComponent, eid)
232 |
233 | const entered = enteredQuery(world)
234 | strictEqual(entered.length, 1)
235 |
236 | addComponent(world, TestComponent, eid)
237 |
238 | const entered2 = enteredQuery(world)
239 | strictEqual(entered2.length, 0)
240 | })
241 | })
--------------------------------------------------------------------------------
/test/integration/Serialize.test.js:
--------------------------------------------------------------------------------
1 | import assert, { strictEqual } from 'assert'
2 | import { createWorld } from '../../src/World.js'
3 | import { addComponent, defineComponent } from '../../src/Component.js'
4 | import { addEntity, resetGlobals } from '../../src/Entity.js'
5 | // import { defineQuery, defineSerializer, defineDeserializer, Types } from '../../src/index.js'
6 | import { defineDeserializer, defineSerializer, DESERIALIZE_MODE } from '../../src/Serialize.js'
7 | import { Changed, defineQuery } from '../../src/Query.js'
8 | import { TYPES_ENUM } from '../../src/Constants.js'
9 | import { pipe } from '../../src/index.js'
10 | import { strict } from 'assert/strict'
11 |
12 | const Types = TYPES_ENUM
13 |
14 | const arraysEqual = (a,b) => !!a && !!b && !(a {
17 | afterEach(() => {
18 | resetGlobals()
19 | })
20 | it('should serialize/deserialize entire world of entities and all of their components', () => {
21 | const world = createWorld()
22 | const TestComponent = defineComponent({ value: Types.f32 })
23 | const eid = addEntity(world)
24 |
25 | addComponent(world, TestComponent, eid)
26 | const serialize = defineSerializer(world)
27 | const deserialize = defineDeserializer(world)
28 |
29 | const packet = serialize(world)
30 |
31 | strictEqual(packet.byteLength, 13)
32 |
33 | strictEqual(TestComponent.value[eid], 0)
34 |
35 | TestComponent.value[eid]++
36 | deserialize(world, packet)
37 |
38 | strictEqual(TestComponent.value[eid], 0)
39 | })
40 | it('should serialize/deserialize entire world of specific components', () => {
41 | const world = createWorld()
42 | const TestComponent = defineComponent({ value: Types.f32 })
43 | const eid = addEntity(world)
44 | addComponent(world, TestComponent, eid)
45 |
46 | const serialize = defineSerializer([TestComponent])
47 | const deserialize = defineDeserializer([TestComponent])
48 |
49 | const packet = serialize(world)
50 |
51 | strictEqual(packet.byteLength, 13)
52 |
53 | strictEqual(TestComponent.value[eid], 0)
54 |
55 | TestComponent.value[eid]++
56 | deserialize(world, packet)
57 |
58 | strictEqual(TestComponent.value[eid], 0)
59 | })
60 | it('should serialize/deserialize specific components of a queried set of entities', () => {
61 | const world = createWorld()
62 | const TestComponent = defineComponent({ value: Types.f32 })
63 | const query = defineQuery([TestComponent])
64 | const eid = addEntity(world)
65 | addComponent(world, TestComponent, eid)
66 |
67 | const serialize = defineSerializer([TestComponent])
68 | const deserialize = defineDeserializer([TestComponent])
69 |
70 | const packet = serialize(query(world))
71 |
72 | strictEqual(packet.byteLength, 13)
73 |
74 | strictEqual(TestComponent.value[eid], 0)
75 |
76 | TestComponent.value[eid]++
77 | deserialize(world, packet)
78 |
79 | strictEqual(TestComponent.value[eid], 0)
80 | })
81 | it('should serialize/deserialize array types on components', () => {
82 | const world1 = createWorld()
83 | const world2 = createWorld()
84 |
85 | const ArrayComponent = defineComponent({ array: [Types.f32, 4] })
86 | const TestComponent = defineComponent({ value: Types.f32 })
87 | const query = defineQuery([ArrayComponent, TestComponent])
88 |
89 | const serialize = defineSerializer([ArrayComponent, TestComponent])
90 | const deserialize = defineDeserializer([ArrayComponent, TestComponent])
91 |
92 | const eid = addEntity(world1)
93 | addComponent(world1, TestComponent, eid)
94 | addComponent(world1, ArrayComponent, eid)
95 |
96 | TestComponent.value[eid] = 1
97 | ArrayComponent.array[eid].set([1,2,3,4])
98 |
99 | strictEqual(TestComponent.value[eid], 1)
100 | assert(arraysEqual(Array.from(ArrayComponent.array[eid]), [1,2,3,4]))
101 |
102 | const packet = serialize(query(world1))
103 | strictEqual(packet.byteLength, 43)
104 |
105 | TestComponent.value[eid] = 0
106 | ArrayComponent.array[eid].set([0,0,0,0])
107 |
108 | deserialize(world1, packet)
109 |
110 | strictEqual(TestComponent.value[eid], 1)
111 | assert(arraysEqual(Array.from(ArrayComponent.array[eid]), [1,2,3,4]))
112 |
113 | })
114 | it('should deserialize properly with APPEND behavior', () => {
115 | const world = createWorld()
116 | const TestComponent = defineComponent({ value: Types.f32 })
117 | const query = defineQuery([TestComponent])
118 | const eid = addEntity(world)
119 | addComponent(world, TestComponent, eid)
120 |
121 | const serialize = defineSerializer([TestComponent])
122 | const deserialize = defineDeserializer(world)
123 |
124 | const packet = serialize(query(world))
125 |
126 | strictEqual(packet.byteLength, 13)
127 |
128 | strictEqual(TestComponent.value[eid], 0)
129 |
130 | TestComponent.value[eid]++
131 | let ents = deserialize(world, packet, DESERIALIZE_MODE.APPEND)
132 | const appendedEid = eid + 1
133 |
134 | strictEqual(TestComponent.value[eid], 1)
135 | strictEqual(TestComponent.value[appendedEid], 0)
136 | strictEqual(ents[0], appendedEid)
137 | })
138 | it('should deserialize properly with MAP behavior', () => {
139 | const world = createWorld()
140 | const TestComponent = defineComponent({ value: Types.f32 })
141 | const query = defineQuery([TestComponent])
142 | const eid = addEntity(world)
143 | addComponent(world, TestComponent, eid)
144 |
145 | const serialize = defineSerializer([TestComponent])
146 | const deserialize = defineDeserializer(world)
147 |
148 | let packet = serialize(query(world))
149 |
150 | strictEqual(packet.byteLength, 13)
151 |
152 | strictEqual(TestComponent.value[eid], 0)
153 |
154 | TestComponent.value[eid]++
155 | let ents = deserialize(world, packet, DESERIALIZE_MODE.MAP)
156 | const mappedEid = eid + 1
157 |
158 | strictEqual(TestComponent.value[eid], 1)
159 | strictEqual(TestComponent.value[mappedEid], 0)
160 |
161 | TestComponent.value[mappedEid] = 1
162 | packet = serialize(query(world))
163 |
164 | ents = deserialize(world, packet, DESERIALIZE_MODE.MAP)
165 | strictEqual(TestComponent.value[mappedEid], 1)
166 | strictEqual(ents[0], mappedEid)
167 | })
168 | // todo
169 | // it('should maintain references when deserializing', () => {
170 |
171 | // })
172 | it('should only serialize changes for Changed components and component properties', () => {
173 | const world = createWorld()
174 | const TestComponent = defineComponent({ value: Types.f32 })
175 | const ArrayComponent = defineComponent({ values: [Types.f32, 3] })
176 |
177 | const eid = addEntity(world)
178 | addComponent(world, TestComponent, eid)
179 | addComponent(world, ArrayComponent, eid)
180 |
181 | const serialize = defineSerializer([Changed(TestComponent.value), ArrayComponent])
182 | const deserialize = defineDeserializer([TestComponent.value, ArrayComponent])
183 |
184 | let packet = serialize([eid])
185 |
186 | // entire ArrayComponent should be serialized
187 | strictEqual(packet.byteLength, 38)
188 |
189 | TestComponent.value[eid]++
190 |
191 | packet = serialize([eid])
192 |
193 | strictEqual(packet.byteLength, 38)
194 |
195 | TestComponent.value[eid] = 0
196 |
197 | deserialize(world, packet)
198 |
199 | strictEqual(TestComponent.value[eid], 1)
200 |
201 | TestComponent.value[eid]++
202 |
203 | packet = serialize([eid])
204 |
205 | strictEqual(packet.byteLength, 38)
206 |
207 | deserialize(world, packet)
208 |
209 | strictEqual(TestComponent.value[eid], 2)
210 |
211 | })
212 | it('should only serialize changes for Changed array properties', () => {
213 | const world = createWorld()
214 | const ArrayComponent = defineComponent({ values: [Types.f32, 3] })
215 |
216 | const eid = addEntity(world)
217 | addComponent(world, ArrayComponent, eid)
218 |
219 | const serialize = defineSerializer([Changed(ArrayComponent)])
220 | const deserialize = defineDeserializer([ArrayComponent])
221 |
222 | let packet = serialize([eid])
223 |
224 | // add component counts as a change, so first serialization will add components
225 | strictEqual(packet.byteLength, 25)
226 |
227 | packet = serialize([eid])
228 |
229 | // no changes, byteLength 0
230 | strictEqual(packet.byteLength, 0)
231 |
232 | packet = serialize([eid])
233 |
234 | // no changes, byteLength 0
235 | strictEqual(packet.byteLength, 0)
236 |
237 | // value still 0
238 | strictEqual(ArrayComponent.values[eid][0], 0)
239 |
240 | ArrayComponent.values[eid][0]++
241 |
242 | packet = serialize([eid])
243 |
244 | // packet should have changes
245 | strictEqual(packet.byteLength, 15)
246 |
247 | ArrayComponent.values[eid][0] = 0
248 |
249 | deserialize(world, packet)
250 |
251 | strictEqual(ArrayComponent.values[eid][0], 1)
252 |
253 | })
254 | it('shouldn\'t serialize anything using Changed on a component with no changes', () => {
255 | const world = createWorld()
256 | const TestComponent = defineComponent({ value: Types.f32 })
257 | const eid = addEntity(world)
258 |
259 | addComponent(world, TestComponent, eid)
260 | TestComponent.value[eid] = 1
261 |
262 | const serialize = defineSerializer([Changed(TestComponent)])
263 |
264 | serialize([eid]) // run once to pick up current state
265 | const packet = serialize([eid])
266 |
267 | strictEqual(packet.byteLength, 0)
268 | })
269 | it('shouldn\'t serialize anything using Changed on an array component with no changes', () => {
270 | const world = createWorld()
271 | const ArrayComponent = defineComponent({ value: [Types.f32, 3] })
272 | const eid = addEntity(world)
273 |
274 | addComponent(world, ArrayComponent, eid)
275 | ArrayComponent.value[eid][1] = 1
276 |
277 | const serialize = defineSerializer([Changed(ArrayComponent)])
278 |
279 | serialize([eid]) // run once to pick up current state
280 | const packet = serialize([eid])
281 |
282 | strictEqual(packet.byteLength, 0)
283 | })
284 | it('should serialize and deserialize entities with a mix of single value, array value and tag components', () => {
285 | const world = createWorld()
286 | const TestComponent = defineComponent({ value: Types.f32 })
287 | const ArrayComponent = defineComponent({ value: [Types.f32, 3] })
288 | const TagComponent = defineComponent()
289 |
290 | const serialize = defineSerializer([TestComponent, ArrayComponent, TagComponent])
291 | const deserialize = defineDeserializer([TestComponent, ArrayComponent, TagComponent])
292 |
293 | const eid = addEntity(world)
294 | addComponent(world, TestComponent, eid)
295 | addComponent(world, ArrayComponent, eid)
296 | addComponent(world, TagComponent, eid)
297 |
298 | let packet = serialize([eid])
299 | assert(packet.byteLength > 0)
300 | // if this errors we know something is wrong with the packet
301 | deserialize(world, packet)
302 |
303 | const eids = [eid]
304 |
305 | const eid2 = addEntity(world)
306 | addComponent(world, TestComponent, eid2)
307 | TestComponent.value[eid2] = 8
308 | eids.push(eid2)
309 |
310 | const eid3 = addEntity(world)
311 | addComponent(world, TagComponent, eid3)
312 | eids.push(eid3)
313 |
314 | const eid4 = addEntity(world)
315 | addComponent(world, ArrayComponent, eid4)
316 | ArrayComponent.value[eid4][1] = 5
317 | eids.push(eid4)
318 |
319 | const eid5 = addEntity(world)
320 | addComponent(world, TagComponent, eid5)
321 | addComponent(world, ArrayComponent, eid5)
322 | ArrayComponent.value[eid5][0] = 3
323 | eids.push(eid5)
324 |
325 | const eid6 = addEntity(world)
326 | addComponent(world, TagComponent, eid6)
327 | addComponent(world, TestComponent, eid6)
328 | TestComponent.value[eid6] = 3
329 | eids.push(eid6)
330 |
331 | packet = serialize(eids)
332 | assert(packet.byteLength > 0)
333 | // if this errors we know something is wrong with the packet
334 | deserialize(world, packet)
335 |
336 | // run a couple more times for good measure
337 | serialize(eids)
338 | packet = serialize(eids)
339 | assert(packet.byteLength > 0)
340 | // if this errors we know something is wrong with the packet
341 | deserialize(world, packet)
342 |
343 | // verify some values
344 | strictEqual(TestComponent.value[eid2], 8)
345 | strictEqual(ArrayComponent.value[eid4][1], 5)
346 | strictEqual(ArrayComponent.value[eid5][0], 3)
347 | strictEqual(TestComponent.value[eid6], 3)
348 | })
349 | it('should serialize from a query using pipe', () => {
350 | const world = createWorld()
351 | const TestComponent = defineComponent({ value: Types.f32 })
352 | const eid = addEntity(world)
353 | addComponent(world, TestComponent, eid)
354 |
355 | const query = defineQuery([TestComponent])
356 | const serialize = defineSerializer([TestComponent])
357 | const serializeQuery = pipe(query, serialize)
358 | const deserialize = defineDeserializer([TestComponent])
359 |
360 | const packet = serializeQuery(world)
361 | assert(packet.byteLength > 0)
362 | deserialize(world, packet)
363 | strictEqual(query(world).length, 1)
364 | })
365 | it('should only register changes on the first serializer run', () => {
366 | const world = createWorld()
367 | const TestComponent = defineComponent({ value: Types.f32 })
368 | const eid = addEntity(world)
369 | addComponent(world, TestComponent, eid)
370 |
371 | const serialize = defineSerializer([Changed(TestComponent)])
372 |
373 | serialize([eid])
374 | TestComponent.value[eid] = 2
375 | let packet = serialize([eid])
376 | assert(packet.byteLength > 0)
377 | packet = serialize([eid])
378 | strictEqual(packet.byteLength, 0)
379 | })
380 | it('should output an array of unique EIDs when deserializing', () => {
381 | const world = createWorld()
382 | const TestComponent = defineComponent({ value0: Types.f32, value1: Types.f32 })
383 | const eid0 = addEntity(world)
384 | addComponent(world, TestComponent, eid0)
385 | const eid1 = addEntity(world)
386 | addComponent(world, TestComponent, eid1)
387 |
388 | const serialize = defineSerializer([TestComponent])
389 | const deserialize = defineDeserializer([TestComponent])
390 |
391 | const packet = serialize(world)
392 |
393 | const ents = deserialize(world, packet)
394 |
395 | strictEqual(ents.length, 2)
396 | strictEqual(ents[0], 0)
397 | strictEqual(ents[1], 1)
398 | })
399 | it('should create EIDs when deserializing eid type properties with MAP mode even if the entity hasn\'t been created yet', () => {
400 | const world = createWorld()
401 | const EIDComponent = defineComponent({eid: Types.eid})
402 | const eid1 = addEntity(world)
403 | const eid2 = addEntity(world)
404 | addComponent(world, EIDComponent, eid1)
405 | EIDComponent.eid[eid1] = eid2
406 | const serialize = defineSerializer([EIDComponent])
407 | const deserialize = defineDeserializer([EIDComponent])
408 | const packet = serialize([eid1])
409 | const [eid1Mapped] = deserialize(world, packet, DESERIALIZE_MODE.MAP)
410 | assert(EIDComponent.eid[eid1Mapped] != 0)
411 | })
412 | })
413 |
--------------------------------------------------------------------------------
/test/integration/Storage.test.js:
--------------------------------------------------------------------------------
1 | import assert, { strictEqual } from 'assert'
2 | import { getDefaultSize } from '../../src/Entity.js'
3 | import { Types } from '../../src/index.js'
4 | import { createStore, resizeStore } from '../../src/Storage.js'
5 | import { TYPES } from '../../src/Constants.js'
6 |
7 | let defaultSize = getDefaultSize()
8 |
9 | const arraysEqual = (a,b) => !!a && !!b && !(a {
12 | it('should default to size of ' + defaultSize, () => {
13 | const store = createStore({ value: Types.i8 }, defaultSize)
14 | strictEqual(store.value.length, defaultSize)
15 | })
16 | it('should allow custom size', () => {
17 | const store = createStore({ value: Types.i8 }, 10)
18 | strictEqual(store.value.length, 10)
19 | })
20 | Object.keys(Types).forEach(type => {
21 | it('should create a store with ' + type, () => {
22 | const store = createStore({ value: type }, defaultSize)
23 | assert(store.value instanceof TYPES[type])
24 | strictEqual(store.value.length, defaultSize)
25 | })
26 | })
27 | Object.keys(Types).forEach(type => {
28 | it('should create a store with array of ' + type, () => {
29 | const store = createStore({ value: [type, 4] }, 10)
30 | assert(store.value instanceof Object)
31 | strictEqual(Object.keys(store.value).length, 10)
32 | assert(store.value[0] instanceof TYPES[type])
33 | strictEqual(store.value[0].length, 4)
34 | })
35 | it('should correctly set values on arrays of ' + type, () => {
36 | const store = createStore({ array: [type, 4] }, 3)
37 | store.array[0].set([1,2,3,4])
38 | store.array[1].set([5,6,7,8])
39 | store.array[2].set([9,10,11,12])
40 | assert(arraysEqual(Array.from(store.array[0]), [1,2,3,4]))
41 | assert(arraysEqual(Array.from(store.array[1]), [5,6,7,8]))
42 | assert(arraysEqual(Array.from(store.array[2]), [9,10,11,12]))
43 | strictEqual(store.array[3], undefined)
44 | })
45 | it('should resize arrays of ' + type, () => {
46 | const store = createStore({ array: [type, 4] }, 3)
47 | store.array[0].set([1,2,3,4])
48 | store.array[1].set([5,6,7,8])
49 | store.array[2].set([9,10,11,12])
50 |
51 | strictEqual(store.array[3], undefined)
52 |
53 | resizeStore(store, 6)
54 |
55 | assert(store.array[5] !== undefined)
56 | strictEqual(store.array[6], undefined)
57 |
58 | // console.log(store.array[0])
59 | assert(arraysEqual(Array.from(store.array[0]), [1,2,3,4]))
60 | assert(arraysEqual(Array.from(store.array[1]), [5,6,7,8]))
61 | assert(arraysEqual(Array.from(store.array[2]), [9,10,11,12]))
62 |
63 | assert(arraysEqual(Array.from(store.array[3]), [0,0,0,0]))
64 | assert(arraysEqual(Array.from(store.array[4]), [0,0,0,0]))
65 | assert(arraysEqual(Array.from(store.array[5]), [0,0,0,0]))
66 |
67 | })
68 | })
69 | it('should create flat stores', () => {
70 | const store = createStore({ value1: Types.i8, value2: Types.ui16, value3: Types.f32 }, defaultSize)
71 | assert(store.value1 != undefined)
72 | assert(store.value1 instanceof Int8Array)
73 | assert(store.value2 != undefined)
74 | assert(store.value2 instanceof Uint16Array)
75 | assert(store.value3 != undefined)
76 | assert(store.value3 instanceof Float32Array)
77 | })
78 | it('should create nested stores', () => {
79 | const store1 = createStore({ nest: { value: Types.i8 } }, defaultSize)
80 | const store2 = createStore({ nest: { nest: { value: Types.ui32 } } }, defaultSize)
81 | const store3 = createStore({ nest: { nest: { nest: { value: Types.i16 } } } }, defaultSize)
82 | assert(store1.nest.value instanceof Int8Array)
83 | assert(store2.nest.nest.value instanceof Uint32Array)
84 | assert(store3.nest.nest.nest.value instanceof Int16Array)
85 | })
86 | })
--------------------------------------------------------------------------------
/test/integration/System.test.js:
--------------------------------------------------------------------------------
1 | import { strictEqual } from 'assert'
2 | import { createWorld } from '../../src/World.js'
3 | import { addComponent, defineComponent } from '../../src/Component.js'
4 | import { addEntity, resetGlobals } from '../../src/Entity.js'
5 | import { defineSystem } from '../../src/System.js'
6 | import { defineQuery, Types } from '../../src/index.js'
7 |
8 | describe('System Integration Tests', () => {
9 | afterEach(() => {
10 | resetGlobals()
11 | })
12 | it('should run against a world and update state', () => {
13 | const world = createWorld()
14 | const TestComponent = defineComponent({ value: Types.f32 })
15 |
16 | const query = defineQuery([TestComponent])
17 | const eid = addEntity(world)
18 | addComponent(world, TestComponent, eid)
19 |
20 | const system = defineSystem(world =>
21 | query(world).forEach(eid => {
22 | TestComponent.value[eid]++
23 | })
24 | )
25 |
26 | system(world)
27 |
28 | strictEqual(TestComponent.value[eid], 1)
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/test/integration/World.test.js:
--------------------------------------------------------------------------------
1 | import assert, { strictEqual } from 'assert'
2 | import { $entityMasks, resetGlobals, addEntity, getDefaultSize } from '../../src/Entity.js'
3 | import { createWorld } from '../../src/World.js'
4 |
5 | const defaultSize = getDefaultSize()
6 |
7 | const growAmount = defaultSize + defaultSize / 2
8 |
9 | describe('World Integration Tests', () => {
10 | afterEach(() => {
11 | resetGlobals()
12 | })
13 | // it('should resize automatically at 80% of ' + defaultSize, () => {
14 | // const world = createWorld()
15 | // const n = defaultSize * 0.8
16 | // for (let i = 0; i < n; i++) {
17 | // addEntity(world)
18 | // }
19 |
20 | // strictEqual(world[$entityMasks][0].length, growAmount)
21 | // })
22 | })
23 |
--------------------------------------------------------------------------------
/test/unit/Serialize.test.js:
--------------------------------------------------------------------------------
1 | import assert, { strictEqual } from 'assert'
2 | import { defineComponent } from '../../src/Component.js'
3 | import { canonicalize } from '../../src/Serialize.js'
4 | import { Changed } from '../../src/Query.js'
5 | import { TYPES_ENUM } from '../../src/Constants.js'
6 |
7 | const Types = TYPES_ENUM
8 |
9 | describe('Serialize Unit Tests', () => {
10 | it('should canonicalize component', () => {
11 | const C = defineComponent({value:Types.f32})
12 | const target = [C]
13 | const [componentProps, changedProps] = canonicalize(target)
14 | strictEqual(componentProps[0], C.value)
15 | })
16 | it('should canonicalize Changed modifier on properties', () => {
17 | const C = defineComponent({value:Types.f32})
18 | const target = [Changed(C.value)]
19 | const [componentProps, changedProps] = canonicalize(target)
20 | strictEqual(changedProps.has(C.value), true)
21 | })
22 | it('should canonicalize Changed modifier on array properties', () => {
23 | const ArrayComponent = defineComponent({ values: [Types.f32, 3] })
24 | const target = [Changed(ArrayComponent.values)]
25 |
26 | const [componentProps, changedProps] = canonicalize(target)
27 | strictEqual(changedProps.has(ArrayComponent.values), true)
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/test/unit/World.test.js:
--------------------------------------------------------------------------------
1 | import assert, { strictEqual } from 'assert'
2 | import { $componentMap } from '../../src/Component.js'
3 | import { $entityMasks, resetGlobals, getDefaultSize } from '../../src/Entity.js'
4 | import { $dirtyQueries, $queries, $queryMap } from '../../src/Query.js'
5 | import { createWorld, $size, $bitflag } from '../../src/World.js'
6 |
7 | const defaultSize = getDefaultSize()
8 |
9 | describe('World Unit Tests', () => {
10 | afterEach(() => {
11 | resetGlobals()
12 | })
13 | it('should initialize all private state', () => {
14 | const world = createWorld()
15 |
16 | strictEqual(Object.keys(world).length, 0)
17 |
18 | strictEqual(world[$size], defaultSize)
19 |
20 | assert(Array.isArray(world[$entityMasks]))
21 |
22 | strictEqual(world[$entityMasks][0].constructor.name, 'Uint32Array')
23 | strictEqual(world[$entityMasks][0].length, defaultSize)
24 |
25 | strictEqual(world[$bitflag], 1)
26 |
27 | strictEqual(world[$componentMap].constructor.name, 'Map')
28 | strictEqual(world[$queryMap].constructor.name, 'Map')
29 | strictEqual(world[$queries].constructor.name, 'Set')
30 | strictEqual(world[$dirtyQueries].constructor.name, 'Set')
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "ESNext",
4 | "target": "ESNext",
5 | "moduleResolution": "node",
6 | "declaration": true,
7 | "declarationMap": true,
8 | "esModuleInterop": true,
9 | "noImplicitAny": false,
10 | "noLib": false,
11 | "removeComments": true,
12 | "sourceMap": true,
13 | "outDir": "./dist"
14 | },
15 | "include": [
16 | "./src"
17 | ],
18 | "exclude": [
19 | "node_modules",
20 | "test/*",
21 | "examples/*"
22 | ]
23 | }
--------------------------------------------------------------------------------