├── .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 | Version 12 | 13 | 14 | Minzipped 15 | 16 | 17 | Downloads 18 | 19 | 20 | License 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 |
defineComponentobject
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 |
hasComponentboolean
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 |
addEntitynumber
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 |
enterQueryfunction
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 |
exitQueryfunction
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 |
defineQueryfunction
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 |
defineSerializerfunction
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 |
defineDeserializerfunction
60 |

Defines a new deserializer which targets the given components to deserialize onto a given world.

61 |
62 |
defineSystemfunction
63 |

Defines a new system function.

64 |
65 |
createWorldobject
66 |

Creates a new world.

67 |
68 |
resetWorldobject
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 | } --------------------------------------------------------------------------------