├── .github └── workflows │ ├── codeql-analysis.yml │ └── node.js.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── main.js │ ├── navigation.js │ ├── search.js │ └── style.css ├── classes │ ├── Entity.html │ ├── EntityAdded.html │ ├── EntityBuilder.html │ ├── EntityRemoved.html │ ├── EventBus.html │ ├── Pipeline.html │ ├── PreptimeWorld.html │ ├── PushDownAutomaton.html │ ├── Query.html │ ├── RuntimeWorld.html │ ├── Scheduler.html │ ├── SerDe.html │ ├── SerialFormat.html │ ├── SimECSPushDownAutomaton.html │ ├── Stage.html │ ├── State.html │ ├── SyncPoint.html │ ├── SystemBuilder.html │ ├── SystemError.html │ └── WorldBuilder.html ├── enums │ ├── EAccess.html │ ├── EExistence.html │ ├── EQueryType.html │ └── ETargetType.html ├── functions │ ├── Read.html │ ├── ReadEntity.html │ ├── ReadEvents.html │ ├── ReadOptional.html │ ├── ReadResource.html │ ├── Storage.html │ ├── With.html │ ├── WithTag.html │ ├── Without.html │ ├── WithoutTag.html │ ├── Write.html │ ├── WriteEvents.html │ ├── WriteOptional.html │ ├── WriteResource.html │ ├── addSyncPoint.html │ ├── addWorld.html │ ├── buildWorld.html │ ├── clearRegistry.html │ ├── createSystem.html │ ├── defaultSchedulingAlgorithm.html │ ├── defaultStageSchedulingAlgorithm.html │ ├── getEntity.html │ ├── getQueriesFromSystem.html │ ├── getSyncPoint.html │ ├── getSystemRunParameters.html │ ├── getWorld.html │ ├── getWorlds.html │ ├── hmrSwapSystem.html │ ├── queryComponents.html │ ├── queryEntities.html │ ├── registerEntity.html │ ├── removeSyncPoint.html │ ├── removeWorld.html │ ├── unregisterEntity.html │ └── unregisterEntityId.html ├── index.html ├── interfaces │ ├── IAccessDescriptor.html │ ├── IAccessQuery.html │ ├── ICommands.html │ ├── IComponentsQuery.html │ ├── IDeserializerOutput.html │ ├── IEntitiesQuery.html │ ├── IEntity.html │ ├── IEntityBuilder.html │ ├── IEventBus.html │ ├── IEventMap.html │ ├── IExistenceDescriptor.html │ ├── IIStateProto.html │ ├── IImmutableWorld.html │ ├── IMutableWorld.html │ ├── IObjectRegistrationOptions.html │ ├── IPipeline.html │ ├── IPreptimeData.html │ ├── IPreptimeOptions.html │ ├── IPreptimeWorld.html │ ├── IPreptimeWorldConfig.html │ ├── IPushDownAutomaton.html │ ├── IQuery.html │ ├── IReadOnlyEntity.html │ ├── IResourceRegistrationOptions.html │ ├── IRuntimeWorld.html │ ├── IRuntimeWorldData.html │ ├── IRuntimeWorldInitConfig.html │ ├── IRuntimeWorldInitData.html │ ├── IScheduler.html │ ├── ISerDe.html │ ├── ISerDeDataSet.html │ ├── ISerDeOperations.html │ ├── ISerDeOptions.html │ ├── ISerialFormat.html │ ├── IStage.html │ ├── IState.html │ ├── IStateProto.html │ ├── ISyncPoint.html │ ├── ISyncPointPrefab.html │ ├── ISystem.html │ ├── ISystemActions.html │ ├── ISystemBuilder.html │ ├── ISystemError.html │ ├── ISystemResource.html │ ├── ITransitionActions.html │ ├── IWorld.html │ └── IWorldBuilder.html ├── media │ ├── error.png │ └── pong.png ├── modules.html ├── types │ ├── TAccessQueryData.html │ ├── TAccessQueryParameter.html │ ├── TAddComponentEventHandler.html │ ├── TAddTagEventHandler.html │ ├── TCloneEventHandler.html │ ├── TCommand.html │ ├── TCustomDeserializer.html │ ├── TDeserializer.html │ ├── TEntityBuilderProto.html │ ├── TEntityId.html │ ├── TEntityProto.html │ ├── TExecutionFunction.html │ ├── TExistenceQuery.html │ ├── TExistenceQueryParameter.html │ ├── TGroupHandle.html │ ├── TOptionalAccessQueryParameter.html │ ├── TPushDownAutomatonProto.html │ ├── TRemoveComponentEventHandler.html │ ├── TRemoveTagEventHandler.html │ ├── TSchedulingAlgorithm.html │ ├── TSerializable.html │ ├── TSerializer.html │ ├── TStageSchedulingAlgorithm.html │ ├── TSubscriber.html │ ├── TSystemFunction.html │ ├── TSystemParameter.html │ ├── TSystemParameterDesc.html │ └── TTag.html └── variables │ ├── Actions.html │ ├── CIdMarker.html │ ├── CMarkerSeparator.html │ ├── CRefMarker.html │ ├── CResourceMarker.html │ ├── CResourceMarkerValue.html │ └── CTagMarker.html ├── examples ├── bench │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── benchmark.spec.ts │ │ ├── benchmark.ts │ │ ├── index.ts │ │ ├── libraries │ │ │ ├── _ape-ecs │ │ │ │ ├── index.ts │ │ │ │ ├── schedule.ts │ │ │ │ ├── serialize-save.ts │ │ │ │ ├── simple-insert.ts │ │ │ │ └── simple-iter.ts │ │ │ ├── _bitecs │ │ │ │ ├── index.ts │ │ │ │ ├── schedule.ts │ │ │ │ ├── simple-insert.ts │ │ │ │ └── simple-iter.ts │ │ │ ├── _javelin │ │ │ │ ├── index.ts │ │ │ │ ├── schedule.ts │ │ │ │ ├── serialize-save.ts │ │ │ │ ├── simple-insert.ts │ │ │ │ └── simple-iter.ts │ │ │ ├── _sim-ecs │ │ │ │ ├── _.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schedule.ts │ │ │ │ ├── serialize-save.ts │ │ │ │ ├── simple-insert.ts │ │ │ │ └── simple-iter.ts │ │ │ ├── _tick-knock │ │ │ │ ├── index.ts │ │ │ │ ├── schedule.ts │ │ │ │ ├── simple-insert.ts │ │ │ │ └── simple-iter.ts │ │ │ └── index.ts │ │ ├── suite.spec.ts │ │ └── suites │ │ │ ├── default.ts │ │ │ └── index.ts │ └── tsconfig.json ├── counter.ts ├── events.ts ├── pong │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── actions.ts │ │ │ ├── persistence.ts │ │ │ └── util.ts │ │ ├── components │ │ │ ├── collision.ts │ │ │ ├── paddle.ts │ │ │ ├── position.ts │ │ │ ├── shape.ts │ │ │ ├── ui-item.ts │ │ │ ├── velocity.ts │ │ │ └── wall.ts │ │ ├── index.ts │ │ ├── main.css │ │ ├── main.ts │ │ ├── models │ │ │ ├── dimensions.ts │ │ │ ├── game-store.ts │ │ │ ├── paddle-transforms.ts │ │ │ ├── score-board.ts │ │ │ ├── tags.ts │ │ │ └── transform.ts │ │ ├── prefabs │ │ │ ├── game.ts │ │ │ ├── menu.ts │ │ │ ├── pause.ts │ │ │ └── savable.ts │ │ ├── schedules │ │ │ ├── default.ts │ │ │ ├── game.ts │ │ │ └── pause.ts │ │ ├── states │ │ │ ├── game.ts │ │ │ ├── menu.ts │ │ │ └── pause.ts │ │ └── systems │ │ │ ├── animation.ts │ │ │ ├── ball.ts │ │ │ ├── before-step.ts │ │ │ ├── collision.ts │ │ │ ├── error.ts │ │ │ ├── input.ts │ │ │ ├── menu.ts │ │ │ ├── paddle.ts │ │ │ ├── pause.ts │ │ │ ├── render-game.ts │ │ │ └── render-ui.ts │ ├── tsconfig.json │ └── vite.config.js ├── system-error.ts └── tsconfig.json ├── media ├── error.png └── pong.png ├── package.json ├── qodana.sarif.json ├── qodana.yaml ├── src ├── _.spec.ts ├── ecs │ ├── ecs-entity.ts │ ├── ecs-query.ts │ ├── ecs-sync-point.ts │ └── ecs-world.ts ├── entity │ ├── entity-builder.spec.ts │ ├── entity-builder.ts │ ├── entity.spec.ts │ ├── entity.test.ts │ └── entity.ts ├── events │ ├── _.ts │ ├── event-bus.spec.ts │ ├── event-bus.ts │ ├── event-reader.spec.ts │ ├── event-reader.ts │ ├── event-writer.spec.ts │ ├── event-writer.ts │ ├── internal-events.test.ts │ └── internal-events.ts ├── index.ts ├── pda │ ├── pda.spec.ts │ ├── pda.test.ts │ ├── pda.ts │ └── sim-ecs-pda.ts ├── query │ ├── _.ts │ ├── components-query.ts │ ├── entities-query.ts │ ├── query.spec.ts │ ├── query.test.ts │ ├── query.ts │ └── query.util.ts ├── scheduler │ ├── pipeline │ │ ├── pipeline.spec.ts │ │ ├── pipeline.ts │ │ ├── stage.spec.ts │ │ ├── stage.ts │ │ ├── sync-point.spec.ts │ │ └── sync-point.ts │ ├── scheduler.spec.ts │ └── scheduler.ts ├── serde │ ├── README.md │ ├── _.ts │ ├── default-handlers.ts │ ├── referencing.spec.ts │ ├── referencing.ts │ ├── serde.spec.ts │ ├── serde.test.ts │ ├── serde.ts │ ├── serial-format.spec.ts │ └── serial-format.ts ├── state │ ├── state.spec.ts │ └── state.ts ├── system │ ├── _.ts │ ├── system-builder.spec.ts │ ├── system-builder.ts │ ├── system.spec.ts │ ├── system.ts │ ├── system_context.spec.ts │ └── system_context.ts ├── test-data │ ├── components.ts │ └── systems.ts ├── util │ └── instance-map.ts └── world │ ├── actions.spec.ts │ ├── common │ ├── world_entities.ts │ ├── world_groups.ts │ ├── world_misc.ts │ └── world_resources.ts │ ├── error.spec.ts │ ├── error.ts │ ├── events.ts │ ├── preptime │ ├── preptime-world.spec.ts │ ├── preptime-world.ts │ ├── preptime-world_entities.ts │ ├── preptime-world_prefabs.ts │ └── preptime-world_resources.ts │ ├── runtime │ ├── commands │ │ ├── command-entity-builder.spec.ts │ │ ├── command-entity-builder.ts │ │ ├── commands-aggregator.spec.ts │ │ ├── commands-aggregator.ts │ │ ├── commands.spec.ts │ │ └── commands.ts │ ├── runtime-world.spec.ts │ ├── runtime-world.ts │ ├── runtime-world_entities.ts │ ├── runtime-world_prefabs.ts │ ├── runtime-world_resources.ts │ └── runtime-world_states.ts │ ├── world-builder.spec.ts │ ├── world-builder.test.ts │ ├── world-builder.ts │ ├── world-builder.util.ts │ └── world.spec.ts ├── tsconfig-tsc.json └── tsconfig.json /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '40 19 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'typescript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Automated build test 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | - release/* 11 | pull_request: 12 | branches: 13 | - master 14 | - release/* 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | matrix: 22 | node-version: [18, 20, latest] 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | - run: npm i 31 | - run: npm run ci 32 | 33 | # todo: coverage 34 | # todo: generate benchmarks 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .nyc_output/ 3 | coverage/ 4 | dist/ 5 | node_modules/ 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .nyc_output/ 3 | coverage/ 4 | examples/ 5 | node_modules/ 6 | tests/ 7 | 8 | .gitignore 9 | package-lock.json 10 | tsconfig.json 11 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #795E26; 3 | --dark-hl-0: #DCDCAA; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #A31515; 7 | --dark-hl-2: #CE9178; 8 | --light-hl-3: #AF00DB; 9 | --dark-hl-3: #C586C0; 10 | --light-hl-4: #0000FF; 11 | --dark-hl-4: #569CD6; 12 | --light-hl-5: #001080; 13 | --dark-hl-5: #9CDCFE; 14 | --light-hl-6: #0070C1; 15 | --dark-hl-6: #4FC1FF; 16 | --light-hl-7: #267F99; 17 | --dark-hl-7: #4EC9B0; 18 | --light-hl-8: #008000; 19 | --dark-hl-8: #6A9955; 20 | --light-hl-9: #098658; 21 | --dark-hl-9: #B5CEA8; 22 | --light-hl-10: #000000; 23 | --dark-hl-10: #C8C8C8; 24 | --light-code-background: #FFFFFF; 25 | --dark-code-background: #1E1E1E; 26 | } 27 | 28 | @media (prefers-color-scheme: light) { :root { 29 | --hl-0: var(--light-hl-0); 30 | --hl-1: var(--light-hl-1); 31 | --hl-2: var(--light-hl-2); 32 | --hl-3: var(--light-hl-3); 33 | --hl-4: var(--light-hl-4); 34 | --hl-5: var(--light-hl-5); 35 | --hl-6: var(--light-hl-6); 36 | --hl-7: var(--light-hl-7); 37 | --hl-8: var(--light-hl-8); 38 | --hl-9: var(--light-hl-9); 39 | --hl-10: var(--light-hl-10); 40 | --code-background: var(--light-code-background); 41 | } } 42 | 43 | @media (prefers-color-scheme: dark) { :root { 44 | --hl-0: var(--dark-hl-0); 45 | --hl-1: var(--dark-hl-1); 46 | --hl-2: var(--dark-hl-2); 47 | --hl-3: var(--dark-hl-3); 48 | --hl-4: var(--dark-hl-4); 49 | --hl-5: var(--dark-hl-5); 50 | --hl-6: var(--dark-hl-6); 51 | --hl-7: var(--dark-hl-7); 52 | --hl-8: var(--dark-hl-8); 53 | --hl-9: var(--dark-hl-9); 54 | --hl-10: var(--dark-hl-10); 55 | --code-background: var(--dark-code-background); 56 | } } 57 | 58 | :root[data-theme='light'] { 59 | --hl-0: var(--light-hl-0); 60 | --hl-1: var(--light-hl-1); 61 | --hl-2: var(--light-hl-2); 62 | --hl-3: var(--light-hl-3); 63 | --hl-4: var(--light-hl-4); 64 | --hl-5: var(--light-hl-5); 65 | --hl-6: var(--light-hl-6); 66 | --hl-7: var(--light-hl-7); 67 | --hl-8: var(--light-hl-8); 68 | --hl-9: var(--light-hl-9); 69 | --hl-10: var(--light-hl-10); 70 | --code-background: var(--light-code-background); 71 | } 72 | 73 | :root[data-theme='dark'] { 74 | --hl-0: var(--dark-hl-0); 75 | --hl-1: var(--dark-hl-1); 76 | --hl-2: var(--dark-hl-2); 77 | --hl-3: var(--dark-hl-3); 78 | --hl-4: var(--dark-hl-4); 79 | --hl-5: var(--dark-hl-5); 80 | --hl-6: var(--dark-hl-6); 81 | --hl-7: var(--dark-hl-7); 82 | --hl-8: var(--dark-hl-8); 83 | --hl-9: var(--dark-hl-9); 84 | --hl-10: var(--dark-hl-10); 85 | --code-background: var(--dark-code-background); 86 | } 87 | 88 | .hl-0 { color: var(--hl-0); } 89 | .hl-1 { color: var(--hl-1); } 90 | .hl-2 { color: var(--hl-2); } 91 | .hl-3 { color: var(--hl-3); } 92 | .hl-4 { color: var(--hl-4); } 93 | .hl-5 { color: var(--hl-5); } 94 | .hl-6 { color: var(--hl-6); } 95 | .hl-7 { color: var(--hl-7); } 96 | .hl-8 { color: var(--hl-8); } 97 | .hl-9 { color: var(--hl-9); } 98 | .hl-10 { color: var(--hl-10); } 99 | pre, code { background: var(--code-background); } 100 | -------------------------------------------------------------------------------- /docs/assets/navigation.js: -------------------------------------------------------------------------------- 1 | window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAACpWZTXPbNhCG/4vOmbpxk7TNTZHtVgePXMnTHDo9QCQkMSFBFgQcq53+94CfwsdiF7rowH3fhyC4BHahv/5bKP6qFh8X98ss4227eLNomDqZC1zoqr0ZL/9wUlVpYl8LkS8+/vL/m4vv/rVoFRcZD6xzBHH/obk8P5+b0D1HEPczk0euQPslhPiFKtT5Ys1K1rbcmPvrru/tbehc5jnPY/Y+SDM+6aLMuYxRxjDN2fKqfomPZgyjnBcu1CfdAogxgrmfioaXheChe4qgbskbVVT8cy1L4BmcMMrR7emu/iaWWtUVU7UAWL4E4/VJGDL6y5hvqwXyOHYUo+yyE891CWXHHEL9XN4BL6S/TPgKVj7U0swPaJ+jKKWo7le7hDcSEaJsxY7Qk3WXCZ+CfQr3nUX2VBcCmo4phPvNSlhFv3UnTHPupayjlD6IMfq0iw7FjmKU9bAx3PE2k0Wj7PGYueDywEz4JlC5yNv3H0Kk982FNODr80CruqqYyFuYMkUpRFMLs+5h4/FEKNDMQf/hFP9yudGq0QpmhjoU2y/vBcdG6UhoGEZJsQepFVLA/PJhwXbkcMAtCUI8sgZBmCiOmGoYKtEBIQpe92vOk6xVDQMtAQ6qKq3YvvQ3G4flaFDcIwl7TEVt9l94prb8aGZGMlXUYtN0v5FXGpWjNwnrDpsJ1x4+Yqwu7phiEYylSEKhD+qJkoDI+0CKIwy2qsWhOCYgByEOjm/yDhbf4j0osqLRK9mWs3wjyjO2orkaAtfWWmY8OZkRA34jsHB0yNHiEUHFU9tXJSPXolBYDsHaq/Bpo56UKBoopW1epJz2IW497QDCmhoyd+Pc8cjObyto1KbhQ1pFMtATpQBpWhIK6B08VKR/8FFuke8wwkI/NKu42a/2ITOyNyduzUDX4FDgziEGMYvyge0J1CAigF2bEON0sQT7MsOSxZYkwNCKEemNQJjXHIUooEECQdMSjrEmDYp7lky0RTcZ6KwFMhSK7BD01gC3gAEjYdKfrZbMXa7VuTEgP+6xfvz157fvbyO8JyZZxZU9xBA6i2hyns/9Wl/7/26awBKiR4Qpd3hmR4rtSUjqqjQjQaGBgmYOLXBIGq7Tft2aEs5uVUNUICGpKO8qktNmegv5yAslidR1OG1TIJGADShxJK88090i8aBFv1iENF+RwBw7WK/wnoF2+Epa/FOO6Ej+b7LWzZDuAdOKkZyhpmFl0rqDick7Bd0PnAawjKQPB/xpCxyiTbwPtcyBKpI9VuGFOC7LYy0LdaoCMqChuePC0R1bhEArmEwCHnmXvjr11WvSw0aE9B30vjuF2kPjnEM0pa9xokuMG06kxT8uL34trzt3o5idhuSarA045hrlC2q7F2YSwiRVewPWcz/Z5tU6f2Tyqz0rF/scxACDYscb86zOIaXF8TQYbssPyIDmKI4YamOUY0vSYX+yUvMEYq/DsOa9IsObowiiO0S6mA/jx9DedNdd24d3ns0/m3LN0KlUiOiW1zaK6KMEYtrTYpApTmDCbsnFwJ2Sg9mZnHQ6/gthDCHmz2ZdhJzddcLmfPCuM/juA3Nt/4/jmmv/rxvIjNx8iGIIsxmA09UHKGM8d6wwBcGyxxFQICx/HAECYnkOHLlcOHYcx3jdtYMA2mvHvu9aiijgEkUQWcmZHI+OwQXCEWAgyZni/omPxbHiCCbnB6ZLhZYsF2hcnXALsjQK75NUJDk3O3IVX3znIA7oqv+Ctw+yruLzC+lwLJrAdpzC9MdTWsy1D/ilw0ocHU3tKZZgjw1mCCKAUyV331gTn3FHgID+6Zq3y//3EMqTULDpb/YoahIgINl/0lzGs9NVoKiuBUPTyZOQsOibt8IIRAv68XzNFbg1OLRQFSD//g7UBXvPBikAAA==" -------------------------------------------------------------------------------- /docs/media/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NSSTC/sim-ecs/630d947c37e6468607ea0823aff49f8d12958b01/docs/media/error.png -------------------------------------------------------------------------------- /docs/media/pong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NSSTC/sim-ecs/630d947c37e6468607ea0823aff49f8d12958b01/docs/media/pong.png -------------------------------------------------------------------------------- /examples/bench/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .nyc_output/ 3 | coverage/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /examples/bench/README.md: -------------------------------------------------------------------------------- 1 | # ECS Bench 2 | 3 | This is a TypeScript ECS benchmark. It works by comparing multiple ECS implementations for TypeScript. 4 | This benchmark is based on the Rust GameDev [ecs_bench_suite](https://github.com/rust-gamedev/ecs_bench_suite) cases. 5 | -------------------------------------------------------------------------------- /examples/bench/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecs-bench", 3 | "version": "0.3.0", 4 | "private": true, 5 | "dependencies": { 6 | "@javelin/ecs": "1.0.0-alpha.13", 7 | "ape-ecs": "1.3.1", 8 | "benchmark": "^2", 9 | "bitecs": "0.3.40", 10 | "fs-extra": "^11", 11 | "sim-ecs": "file:../../src", 12 | "tick-knock": "4.2.0", 13 | "tsx": "^4", 14 | "tslib": "^2", 15 | "typescript": "^5" 16 | }, 17 | "devDependencies": { 18 | "@types/benchmark": "^2", 19 | "@types/fs-extra": "^11" 20 | }, 21 | "scripts": { 22 | "bench": "tsx src/index.ts", 23 | "bench-js": "npm run build && node dist/examples/bench/src/index.js", 24 | "build": "tsc --project ." 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/bench/src/benchmark.spec.ts: -------------------------------------------------------------------------------- 1 | export interface IBenchmark { 2 | readonly name: string 3 | comment?: string 4 | 5 | init(): Promise | void 6 | reset(): Promise | void 7 | run(): Promise | void 8 | } 9 | 10 | export interface IBenchmarkConstructor { 11 | new(iterCount: number): IBenchmark 12 | } 13 | 14 | export interface ICaseResult { 15 | readonly name: string 16 | 17 | averageTime: number 18 | fastestTime: number 19 | slowestTime: number 20 | totalTime: number 21 | comment: string 22 | } 23 | 24 | export interface ISuiteResult { 25 | readonly name: string 26 | 27 | currentResult: ICaseResult 28 | results: ISuiteResults 29 | } 30 | 31 | export interface ISuiteResults { 32 | [caseName: string]: ICaseResult 33 | } 34 | -------------------------------------------------------------------------------- /examples/bench/src/benchmark.ts: -------------------------------------------------------------------------------- 1 | import {suites} from "./suites"; 2 | import {IBenchmarkConstructor, ISuiteResult} from "./benchmark.spec"; 3 | import {scheduleBenchmarks, serializeBenchmarks, simpleInsertBenchmarks, simpleIterBenchmarks} from "./libraries"; 4 | 5 | interface IBenchDesc { 6 | name: string, 7 | bench: IBenchmarkConstructor[] 8 | iterCount: number 9 | probeCount: number 10 | } 11 | 12 | (async () => { 13 | const libBenches: IBenchDesc[] = [ 14 | { name: 'Simple Insert', bench: simpleInsertBenchmarks, iterCount: 100, probeCount: 50 }, 15 | { name: 'Simple Iter', bench: simpleIterBenchmarks, iterCount: 5, probeCount: 10 }, 16 | { name: 'Schedule', bench: scheduleBenchmarks, iterCount: 5, probeCount: 10 }, 17 | { name: 'Serialize', bench: serializeBenchmarks, iterCount: 100, probeCount: 50 }, 18 | ]; 19 | 20 | for (const libBench of libBenches) { 21 | for (const suite of suites) { 22 | let lastResult: ISuiteResult; 23 | 24 | console.log('\n', `**${suite.name} / ${libBench.name}**`); 25 | console.log( 26 | '\n| Library | Points | Deviation | Comment |', 27 | '\n| ---: | ---: | :--- | :--- |', 28 | ); 29 | 30 | await suite.init(libBench.bench, libBench.iterCount, libBench.probeCount); 31 | 32 | for await (const result of suite.run()) { 33 | const bench = result.currentResult; 34 | 35 | lastResult = result; 36 | 37 | // see https://calculator.academy/percentage-difference-calculator/ 38 | const deviations = [ 39 | Math.abs( bench.slowestTime - bench.averageTime ) / ((bench.slowestTime + bench.averageTime) / 2), 40 | Math.abs( bench.fastestTime - bench.averageTime ) / ((bench.slowestTime + bench.averageTime) / 2), 41 | ]; 42 | 43 | console.log( 44 | '|', bench.name, 45 | '|', Math.round(1 / (bench.averageTime / 1000)), 46 | '|', '±', Math.max.apply(Math, deviations).toPrecision(2).toString() + '%', 47 | '|', bench.comment, 48 | '|', 49 | ) 50 | } 51 | 52 | await suite.reset(); 53 | console.log('\n'); 54 | } 55 | } 56 | })(); 57 | -------------------------------------------------------------------------------- /examples/bench/src/index.ts: -------------------------------------------------------------------------------- 1 | import {readFileSync} from "fs-extra"; 2 | import {arch, cpus, release, platform, type} from 'os'; 3 | 4 | const nodeVersion = process.version; 5 | 6 | const benchVersion = JSON.parse(readFileSync('package.json').toString()).version; 7 | const tsVersion = JSON.parse(readFileSync('node_modules/typescript/package.json').toString()).version; 8 | const tsLibVersion = JSON.parse(readFileSync('node_modules/tslib/package.json').toString()).version; 9 | const tsxVersion = JSON.parse(readFileSync('node_modules/tsx/package.json').toString()).version; 10 | 11 | const apeVersion = JSON.parse(readFileSync('node_modules/ape-ecs/package.json').toString()).version; 12 | const bitVersion = JSON.parse(readFileSync('node_modules/bitecs/package.json').toString()).version; 13 | const javelinVersion = JSON.parse(readFileSync('node_modules/@javelin/ecs/package.json').toString()).version; 14 | const simVersion = JSON.parse(readFileSync('../../package.json').toString()).version; 15 | const tickKnockVersion = JSON.parse(readFileSync('node_modules/tick-knock/package.json').toString()).version; 16 | 17 | const getUniqueCPUs = () => cpus() 18 | .filter((cpu, i, cpus) => 19 | !cpus.find((c, j) => j < i && cpu.model == c.model) 20 | ); 21 | const now = new Date(); 22 | const date = now.getDate().toString(); 23 | const dateCounter = date.endsWith('1') 24 | ? 'st' 25 | : date.endsWith('2') 26 | ? 'nd' 27 | : date.endsWith('3') 28 | ? 'rd' 29 | : 'th'; 30 | const months = [ 31 | 'January', 'February', 'March', 'April', 'May', 'June', 32 | 'July', 'August', 'September', 'October', 'November', 'December' 33 | ]; 34 | 35 | console.log(` 36 | -------------------------------------------------------------------------------- 37 | TypeScript ECS Bench 38 | -------------------------------------------------------------------------------- 39 | 40 | ${date}${dateCounter} ${months[now.getMonth()]} ${now.getFullYear()} 41 | 42 | Platform: ${type()} ${platform()} ${arch()} v${release()} 43 | CPU: ${getUniqueCPUs().map(cpu => `${cpu.model.trim()}@${cpu.speed}MHz`).join(', ')} 44 | NodeJS: ${nodeVersion} 45 | 46 | Bench\t\tv${benchVersion} 47 | TypeScript\tv${tsVersion} 48 | TS-Lib\t\tv${tsLibVersion} 49 | TSX\t\tv${tsxVersion} 50 | 51 | Ape-ECS\t\tv${apeVersion} 52 | bitecs\t\tv${bitVersion} 53 | Javelin\t\tv${javelinVersion} 54 | sim-ecs\t\tv${simVersion} 55 | tick-knock\tv${tickKnockVersion} 56 | 57 | Measured in "points" for comparison. More is better! 58 | `); 59 | 60 | 61 | import './benchmark'; 62 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_ape-ecs/index.ts: -------------------------------------------------------------------------------- 1 | import {Benchmark as Schedule} from "./schedule"; 2 | import {Benchmark as SerializeSave} from "./serialize-save"; 3 | import {Benchmark as SimpleInsert} from "./simple-insert"; 4 | import {Benchmark as SimpleIter} from "./simple-iter"; 5 | 6 | export { 7 | Schedule, 8 | SerializeSave, 9 | SimpleInsert, 10 | SimpleIter, 11 | }; 12 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_ape-ecs/serialize-save.ts: -------------------------------------------------------------------------------- 1 | import {Component, World} from 'ape-ecs'; 2 | import {IBenchmark} from "../../benchmark.spec"; 3 | 4 | class Transform extends Component {} 5 | class Position extends Component { x = 0 } 6 | class Rotation extends Component {} 7 | class Velocity extends Component { x = 1 } 8 | 9 | export class Benchmark implements IBenchmark { 10 | readonly name = 'Ape-ECS'; 11 | comment = ''; 12 | world: World; 13 | world2: World; 14 | 15 | constructor( 16 | protected iterCount: number 17 | ) { 18 | this.world = new World(); 19 | this.world.registerComponent(Transform); 20 | this.world.registerComponent(Position); 21 | this.world.registerComponent(Rotation); 22 | this.world.registerComponent(Velocity); 23 | 24 | this.world2 = new World(); 25 | this.world2.registerComponent(Transform); 26 | this.world2.registerComponent(Position); 27 | this.world2.registerComponent(Rotation); 28 | this.world2.registerComponent(Velocity); 29 | 30 | for (let i = 0; i < 1000; i++) { 31 | this.world.createEntity({ 32 | c: { 33 | [Transform.name]: { type: Transform.name }, 34 | [Position.name]: { type: Position.name }, 35 | [Rotation.name]: { type: Rotation.name }, 36 | [Velocity.name]: { type: Velocity.name }, 37 | } 38 | }) 39 | } 40 | 41 | { 42 | const json = JSON.stringify(this.world.getObject()); 43 | this.comment = `file size: ${new TextEncoder().encode(json).length / 1024} KB`; 44 | } 45 | } 46 | 47 | init() {} 48 | 49 | reset() {} 50 | 51 | run() { 52 | const json = JSON.stringify(this.world.getObject()); 53 | this.world2.createEntities(JSON.parse(json)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_ape-ecs/simple-insert.ts: -------------------------------------------------------------------------------- 1 | import {Component, World} from 'ape-ecs'; 2 | import {IBenchmark} from "../../benchmark.spec"; 3 | 4 | class Transform extends Component {} 5 | class Position extends Component { x = 0 } 6 | class Rotation extends Component {} 7 | class Velocity extends Component { x = 1 } 8 | 9 | export class Benchmark implements IBenchmark { 10 | readonly name = 'Ape-ECS'; 11 | world: World; 12 | 13 | constructor( 14 | protected iterCount: number 15 | ) { 16 | this.world = new World(); 17 | this.world.registerComponent(Transform); 18 | this.world.registerComponent(Position); 19 | this.world.registerComponent(Rotation); 20 | this.world.registerComponent(Velocity); 21 | } 22 | 23 | init() {} 24 | 25 | reset() { 26 | for (const entity of [ 27 | ...this.world.getEntities(Transform), 28 | ...this.world.getEntities(Position), 29 | ...this.world.getEntities(Rotation), 30 | ...this.world.getEntities(Velocity), 31 | ]) { 32 | this.world.removeEntity(entity); 33 | } 34 | } 35 | 36 | run() { 37 | for (let i = 0; i < this.iterCount; i++) { 38 | this.world.createEntity({ 39 | c: { 40 | [Transform.name]: { type: Transform.name }, 41 | [Position.name]: { type: Position.name }, 42 | [Rotation.name]: { type: Rotation.name }, 43 | [Velocity.name]: { type: Velocity.name }, 44 | } 45 | }) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_ape-ecs/simple-iter.ts: -------------------------------------------------------------------------------- 1 | import {Component, System, World} from 'ape-ecs'; 2 | import {IBenchmark} from "../../benchmark.spec"; 3 | 4 | class Transform extends Component {} 5 | class Position extends Component { static properties = { x: 0 } } 6 | class Rotation extends Component {} 7 | class Velocity extends Component { static properties = { x: 1 } } 8 | 9 | class SimpleIterSystem extends System { 10 | q = this.createQuery().fromAll('Position', 'Velocity').persist(); 11 | 12 | update() { 13 | const entities = this.q.execute(); 14 | let entity; 15 | for (entity of entities) { 16 | // this is in the examples and API doc, but it's 10x slower than the below alternative! 17 | //entity.getOne('Position')!.x += entity.getOne('Velocity')!.x; 18 | // this is taken from the benchmark script in the ape repository 19 | entity.c.Position.x += entity.c.Velocity.x; 20 | } 21 | } 22 | } 23 | 24 | export class Benchmark implements IBenchmark { 25 | readonly name = 'Ape-ECS'; 26 | world: World; 27 | 28 | constructor( 29 | protected iterCount: number 30 | ) { 31 | this.world = new World({ trackChanges: false, entityPool: 1000 }); 32 | this.world.registerSystem('step', SimpleIterSystem); 33 | this.world.registerComponent(Transform); 34 | this.world.registerComponent(Position); 35 | this.world.registerComponent(Rotation); 36 | this.world.registerComponent(Velocity); 37 | 38 | for (let i = 0; i < 1000; i++) { 39 | this.world.createEntity({ 40 | components: [ 41 | { type: Transform.name, key: Transform.name }, 42 | { type: Position.name, key: Position.name, x: 0 }, 43 | { type: Rotation.name, key: Rotation.name }, 44 | { type: Velocity.name, key: Velocity.name, x: 1 }, 45 | ] 46 | }) 47 | } 48 | } 49 | 50 | init() {} 51 | 52 | reset() {} 53 | 54 | run() { 55 | for (let i = 0; i < this.iterCount; i++) { 56 | this.world.runSystems('step'); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_bitecs/index.ts: -------------------------------------------------------------------------------- 1 | import {Benchmark as Schedule} from "./schedule"; 2 | //import {Benchmark as SerializeSave} from "./serialize-save"; 3 | import {Benchmark as SimpleInsert} from "./simple-insert"; 4 | import {Benchmark as SimpleIter} from "./simple-iter"; 5 | 6 | export { 7 | Schedule, 8 | //SerializeSave, 9 | SimpleInsert, 10 | SimpleIter, 11 | }; 12 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_bitecs/schedule.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addComponent, 3 | addEntity, 4 | createWorld, 5 | defineComponent, defineQuery, 6 | IWorld, pipe, 7 | setDefaultSize, 8 | Types 9 | } from 'bitecs'; 10 | import { IBenchmark } from '../../benchmark.spec'; 11 | 12 | const A = defineComponent({ val: Types.ui32 }); 13 | const B = defineComponent({ val: Types.ui32 }); 14 | const C = defineComponent({ val: Types.ui32 }); 15 | const D = defineComponent({ val: Types.ui32 }); 16 | const E = defineComponent({ val: Types.ui32 }); 17 | 18 | const queryAB = defineQuery([A, B]); 19 | const queryCD = defineQuery([C, D]); 20 | const queryCE = defineQuery([C, E]); 21 | 22 | const systemAB = (world: IWorld) => { 23 | const ents = queryAB(world); 24 | let eid; 25 | 26 | for (let i = 0; i < ents.length; i++) { 27 | eid = ents[i]; 28 | [A.val[eid], B.val[eid]] = [B.val[eid], A.val[eid]]; 29 | } 30 | 31 | return world; 32 | }; 33 | const systemCD = (world: IWorld) => { 34 | const ents = queryCD(world); 35 | let eid; 36 | 37 | for (let i = 0; i < ents.length; i++) { 38 | eid = ents[i]; 39 | [C.val[eid], D.val[eid]] = [D.val[eid], C.val[eid]]; 40 | } 41 | 42 | return world; 43 | }; 44 | const systemCE = (world: IWorld) => { 45 | const ents = queryCE(world); 46 | let eid; 47 | 48 | for (let i = 0; i < ents.length; i++) { 49 | eid = ents[i]; 50 | [C.val[eid], E.val[eid]] = [E.val[eid], C.val[eid]]; 51 | } 52 | 53 | return world; 54 | }; 55 | const pipeline = pipe(systemAB, systemCD, systemCE); 56 | 57 | export class Benchmark implements IBenchmark { 58 | readonly name = 'bitecs'; 59 | world!: IWorld; 60 | 61 | constructor( 62 | protected readonly iterCount: number 63 | ) { 64 | setDefaultSize(1000001); 65 | this.world = createWorld(); 66 | 67 | { 68 | let eid; 69 | 70 | for (let i = 0; i < 1000; i++) { 71 | eid = addEntity(this.world); 72 | addComponent(this.world, A, eid); 73 | } 74 | 75 | for (let i = 0; i < 1000; i++) { 76 | eid = addEntity(this.world); 77 | addComponent(this.world, A, eid); 78 | addComponent(this.world, B, eid); 79 | } 80 | 81 | for (let i = 0; i < 1000; i++) { 82 | eid = addEntity(this.world); 83 | addComponent(this.world, A, eid); 84 | addComponent(this.world, B, eid); 85 | addComponent(this.world, C, eid); 86 | } 87 | 88 | for (let i = 0; i < 1000; i++) { 89 | eid = addEntity(this.world); 90 | addComponent(this.world, A, eid); 91 | addComponent(this.world, B, eid); 92 | addComponent(this.world, C, eid); 93 | addComponent(this.world, D, eid); 94 | } 95 | 96 | for (let i = 0; i < 1000; i++) { 97 | eid = addEntity(this.world); 98 | addComponent(this.world, A, eid); 99 | addComponent(this.world, B, eid); 100 | addComponent(this.world, C, eid); 101 | addComponent(this.world, E, eid); 102 | } 103 | } 104 | } 105 | 106 | init() {} 107 | 108 | reset() {} 109 | 110 | run() { 111 | for (let i = 0; i < this.iterCount; i++) { 112 | pipeline(this.world); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_bitecs/simple-insert.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addComponent, 3 | addEntity, 4 | createWorld, 5 | defineComponent, 6 | IWorld, 7 | removeEntity, 8 | setDefaultSize, 9 | Types 10 | } from 'bitecs'; 11 | import { IBenchmark } from '../../benchmark.spec'; 12 | 13 | const Transform = defineComponent({}); 14 | const Position = defineComponent({ x: Types.i32 }); 15 | const Rotation = defineComponent({}); 16 | const Velocity = defineComponent({ x: Types.i32 }); 17 | 18 | export class Benchmark implements IBenchmark { 19 | readonly name = 'bitecs'; 20 | entityIds: number[] = []; 21 | world!: IWorld; 22 | 23 | constructor( 24 | protected readonly iterCount: number 25 | ) { 26 | setDefaultSize(1000001); 27 | this.world = createWorld(); 28 | } 29 | 30 | init() {} 31 | 32 | async reset() { 33 | this.entityIds.forEach(id => removeEntity(this.world, id)); 34 | this.entityIds.length = 0; 35 | } 36 | 37 | run() { 38 | let eid; 39 | for (let i = 0; i < this.iterCount; i++) { 40 | eid = addEntity(this.world); 41 | this.entityIds.push(eid); 42 | 43 | addComponent(this.world, Transform, eid); 44 | addComponent(this.world, Position, eid); 45 | addComponent(this.world, Rotation, eid); 46 | addComponent(this.world, Velocity, eid); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_bitecs/simple-iter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addComponent, 3 | addEntity, 4 | createWorld, 5 | defineComponent, defineQuery, 6 | IWorld, pipe, 7 | setDefaultSize, 8 | Types 9 | } from 'bitecs'; 10 | import { IBenchmark } from '../../benchmark.spec'; 11 | 12 | const Transform = defineComponent({}); 13 | const Position = defineComponent({ x: Types.ui32 }); 14 | const Rotation = defineComponent({}); 15 | const Velocity = defineComponent({ x: Types.ui32 }); 16 | 17 | const query = defineQuery([Position, Velocity]); 18 | const simpleIterSystem = (world: IWorld) => { 19 | const ents = query(world); 20 | let eid; 21 | 22 | for (let i = 0; i < ents.length; i++) { 23 | eid = ents[i]; 24 | Position.x[eid] += Velocity.x[eid]; 25 | } 26 | 27 | return world; 28 | }; 29 | const pipeline = pipe(simpleIterSystem); 30 | 31 | export class Benchmark implements IBenchmark { 32 | readonly name = 'bitecs'; 33 | world!: IWorld; 34 | 35 | constructor( 36 | protected readonly iterCount: number 37 | ) { 38 | setDefaultSize(1000001); 39 | this.world = createWorld(); 40 | 41 | { 42 | let eid; 43 | for (let i = 0; i < 1000; i++) { 44 | eid = addEntity(this.world); 45 | addComponent(this.world, Transform, eid); 46 | addComponent(this.world, Position, eid); 47 | addComponent(this.world, Rotation, eid); 48 | addComponent(this.world, Velocity, eid); 49 | } 50 | } 51 | } 52 | 53 | init() {} 54 | 55 | reset() {} 56 | 57 | run() { 58 | for (let i = 0; i < this.iterCount; i++) { 59 | pipeline(this.world); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_javelin/index.ts: -------------------------------------------------------------------------------- 1 | //import {Benchmark as Schedule} from "./schedule"; 2 | import {Benchmark as SerializeSave} from "./serialize-save"; 3 | import {Benchmark as SimpleInsert} from "./simple-insert"; 4 | import {Benchmark as SimpleIter} from "./simple-iter"; 5 | import {Benchmark as Schedule} from "./schedule"; 6 | 7 | export { 8 | Schedule, 9 | SerializeSave, 10 | SimpleInsert, 11 | SimpleIter, 12 | }; 13 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_javelin/schedule.ts: -------------------------------------------------------------------------------- 1 | import {createWorld, component, World, number, createQuery} from "@javelin/ecs" 2 | import { IBenchmark } from '../../benchmark.spec'; 3 | 4 | const A = { val: number }; 5 | const B = { val: number }; 6 | const C = { val: number }; 7 | const D = { val: number }; 8 | const E = { val: number }; 9 | 10 | export class Benchmark implements IBenchmark { 11 | readonly name = 'javelin'; 12 | world!: World; 13 | 14 | constructor( 15 | protected readonly iterCount: number 16 | ) { 17 | this.world = createWorld(); 18 | this.world.addSystem(() => 19 | createQuery(A, B) 20 | ((_e, [a, b]) => { 21 | [a.val, b.val] = [b.val, a.val]; 22 | }) 23 | ); 24 | 25 | this.world.addSystem(() => 26 | createQuery(C, D) 27 | ((_e, [c, d]) => { 28 | [c.val, d.val] = [d.val, c.val]; 29 | }) 30 | ); 31 | 32 | this.world.addSystem(() => 33 | createQuery(C, E) 34 | ((_e, [c, e]) => { 35 | [c.val, e.val] = [e.val, c.val]; 36 | }) 37 | ); 38 | 39 | for (let i = 0; i < 10000; i++) { 40 | this.world.create( 41 | component(A, { val: 0 }), 42 | ); 43 | } 44 | 45 | for (let i = 0; i < 10000; i++) { 46 | this.world.create( 47 | component(A, { val: 0 }), 48 | component(B, { val: 0 }), 49 | ); 50 | } 51 | 52 | for (let i = 0; i < 10000; i++) { 53 | this.world.create( 54 | component(A, { val: 0 }), 55 | component(B, { val: 0 }), 56 | component(C, { val: 0 }), 57 | ); 58 | } 59 | 60 | for (let i = 0; i < 10000; i++) { 61 | this.world.create( 62 | component(A, { val: 0 }), 63 | component(B, { val: 0 }), 64 | component(C, { val: 0 }), 65 | component(D, { val: 0 }), 66 | ); 67 | } 68 | 69 | for (let i = 0; i < 10000; i++) { 70 | this.world.create( 71 | component(A, { val: 0 }), 72 | component(B, { val: 0 }), 73 | component(C, { val: 0 }), 74 | component(E, { val: 0 }), 75 | ); 76 | } 77 | } 78 | 79 | init(): Promise | void {} 80 | 81 | reset(): void {} 82 | 83 | run(): void { 84 | let i; 85 | for (i = 0; i < this.iterCount; i++) { 86 | this.world.step(null); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_javelin/serialize-save.ts: -------------------------------------------------------------------------------- 1 | import {createWorld, component, World, number, createQuery} from "@javelin/ecs" 2 | import {IBenchmark} from "../../benchmark.spec"; 3 | 4 | const Transform = {} 5 | const Position = { x: number } 6 | const Rotation = {} 7 | const Velocity = { x: number } 8 | 9 | export class Benchmark implements IBenchmark { 10 | readonly name = 'Javelin'; 11 | comment = ''; 12 | world: World; 13 | 14 | constructor( 15 | protected iterCount: number 16 | ) { 17 | this.world = createWorld(); 18 | } 19 | 20 | reset() {} 21 | 22 | async init(): Promise { 23 | for (let i = 0; i < 1000; i++) { 24 | this.world.create( 25 | component(Transform), 26 | component(Position, { x: 0 }), 27 | component(Rotation), 28 | component(Velocity, { x: 1 }), 29 | ); 30 | } 31 | 32 | this.world.step(null); 33 | 34 | { 35 | const json = JSON.stringify(this.world.createSnapshot()); 36 | this.comment = `file size: ${new TextEncoder().encode(json).length / 1024} KB`; 37 | } 38 | } 39 | 40 | async run() { 41 | const snapshot = JSON.stringify(this.world.createSnapshot()); 42 | createWorld({ snapshot: JSON.parse(snapshot) }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_javelin/simple-insert.ts: -------------------------------------------------------------------------------- 1 | import {createWorld, component, World, number} from "@javelin/ecs" 2 | import { IBenchmark } from '../../benchmark.spec'; 3 | 4 | const Transform = {} 5 | const Position = { x: number } 6 | const Rotation = {} 7 | const Velocity = { x: number } 8 | 9 | export class Benchmark implements IBenchmark { 10 | readonly name = 'javelin'; 11 | world!: World; 12 | 13 | constructor( 14 | protected readonly iterCount: number 15 | ) { 16 | this.world = createWorld(); 17 | } 18 | 19 | init(): Promise | void {} 20 | 21 | async reset() { 22 | this.world.reset(); 23 | } 24 | 25 | run() { 26 | for (let i = 0; i < this.iterCount; i++) { 27 | this.world.create( 28 | component(Transform), 29 | component(Position, { x: 0 }), 30 | component(Rotation), 31 | component(Velocity, { x: 1 }), 32 | ); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_javelin/simple-iter.ts: -------------------------------------------------------------------------------- 1 | import {createWorld, component, World, number, createQuery} from "@javelin/ecs" 2 | import { IBenchmark } from '../../benchmark.spec'; 3 | 4 | const Transform = {}; 5 | const Position = { x: number }; 6 | const Rotation = {}; 7 | const Velocity = { x: number }; 8 | 9 | export class Benchmark implements IBenchmark { 10 | readonly name = 'javelin'; 11 | world!: World; 12 | 13 | constructor( 14 | protected readonly iterCount: number 15 | ) { 16 | const query = createQuery(Position, Velocity); 17 | 18 | this.world = createWorld(); 19 | this.world.addSystem(() => 20 | query((_e, [pos, vel]) => { 21 | pos.x += vel.x; 22 | }) 23 | ); 24 | 25 | for (let i = 0; i < 1000; i++) { 26 | this.world.create( 27 | component(Transform), 28 | component(Position, { x: 0 }), 29 | component(Rotation), 30 | component(Velocity, { x: 1 }), 31 | ); 32 | } 33 | } 34 | 35 | init(): Promise | void {} 36 | 37 | reset(): void {} 38 | 39 | run(): void { 40 | let i; 41 | for (i = 0; i < this.iterCount; i++) { 42 | this.world.step(null); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_sim-ecs/_.ts: -------------------------------------------------------------------------------- 1 | import {Actions, createSystem, WriteResource} from "../../../../.."; 2 | 3 | export class CounterResource { 4 | count = 0 5 | 6 | constructor( 7 | public requiredIterCount: number 8 | ) {} 9 | } 10 | 11 | export const CheckEndSystem = createSystem({ 12 | actions: Actions, 13 | counter: WriteResource(CounterResource), 14 | }).withRunFunction(({actions, counter}) => { 15 | if (++counter.count >= counter.requiredIterCount) { 16 | actions.commands.stopRun(); 17 | } 18 | }).build(); 19 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_sim-ecs/index.ts: -------------------------------------------------------------------------------- 1 | import {Benchmark as Schedule} from "./schedule"; 2 | import {Benchmark as SerializeSave} from "./serialize-save"; 3 | import {Benchmark as SimpleInsert} from "./simple-insert"; 4 | import {Benchmark as SimpleIter} from "./simple-iter"; 5 | 6 | export { 7 | Schedule, 8 | SerializeSave, 9 | SimpleInsert, 10 | SimpleIter, 11 | }; 12 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_sim-ecs/schedule.ts: -------------------------------------------------------------------------------- 1 | import {buildWorld, createSystem, IPreptimeWorld, IRuntimeWorld, queryComponents, Write} from '../../../../..'; 2 | import type {IBenchmark} from "../../benchmark.spec"; 3 | import {CheckEndSystem, CounterResource} from "./_"; 4 | 5 | class A { constructor(public val: number = 0) {} } 6 | class B { constructor(public val: number = 0) {} } 7 | class C { constructor(public val: number = 0) {} } 8 | class D { constructor(public val: number = 0) {} } 9 | class E { constructor(public val: number = 0) {} } 10 | 11 | 12 | const ABSystem = createSystem({ 13 | query: queryComponents({ 14 | a: Write(A), 15 | b: Write(B) 16 | }) 17 | }).withRunFunction(({query}) => { 18 | let a, b; 19 | for ({a, b} of query.iter()) { 20 | [a.val, b.val] = [b.val, a.val]; 21 | } 22 | }).build(); 23 | 24 | 25 | const CDSystem = createSystem({ 26 | query: queryComponents({ 27 | c: Write(C), 28 | d: Write(D) 29 | }) 30 | }).withRunFunction(({query}) => { 31 | let c, d; 32 | for ({c, d} of query.iter()) { 33 | [c.val, d.val] = [d.val, c.val]; 34 | } 35 | }).build(); 36 | 37 | const CESystem = createSystem({ 38 | query: queryComponents({ 39 | c: Write(C), 40 | e: Write(E) 41 | }) 42 | }).withRunFunction(({query}) => { 43 | let c, e; 44 | for ({c, e} of query.iter()) { 45 | [c.val, e.val] = [e.val, c.val]; 46 | } 47 | }).build(); 48 | 49 | 50 | export class Benchmark implements IBenchmark { 51 | readonly name = 'sim-ecs'; 52 | count = 0; 53 | prepWorld: IPreptimeWorld; 54 | runWorld!: IRuntimeWorld; 55 | 56 | constructor( 57 | protected iterCount: number 58 | ) { 59 | this.prepWorld = buildWorld() 60 | .withDefaultScheduling(root => root 61 | .addNewStage(stage => stage 62 | .addSystem(ABSystem) 63 | .addSystem(CDSystem) 64 | .addSystem(CESystem) 65 | .addSystem(CheckEndSystem) 66 | ) 67 | ) 68 | .withComponents(A, B, C, D, E) 69 | .build(); 70 | 71 | this.prepWorld.addResource(CounterResource, iterCount); 72 | 73 | for (let i = 0; i < 10000; i++) { 74 | this.prepWorld.buildEntity() 75 | .with(A, 0) 76 | .build(); 77 | } 78 | 79 | for (let i = 0; i < 10000; i++) { 80 | this.prepWorld.buildEntity() 81 | .withAll(A, B) 82 | .build(); 83 | } 84 | 85 | for (let i = 0; i < 10000; i++) { 86 | this.prepWorld.buildEntity() 87 | .withAll(A, B, C) 88 | .build(); 89 | } 90 | 91 | for (let i = 0; i < 10000; i++) { 92 | this.prepWorld.buildEntity() 93 | .withAll(A, B, C, D) 94 | .build(); 95 | } 96 | 97 | for (let i = 0; i < 10000; i++) { 98 | this.prepWorld.buildEntity() 99 | .withAll(A, B, C, E) 100 | .build(); 101 | } 102 | } 103 | 104 | reset() { 105 | this.runWorld.getResource(CounterResource).count = 0; 106 | } 107 | 108 | async init(): Promise { 109 | this.runWorld = await this.prepWorld.prepareRun({ 110 | // to make the comparison fair, we will iterate in a sync loop over the steps, just like the others do 111 | executionFunction: (fn: Function) => fn(), 112 | }); 113 | } 114 | 115 | run() { 116 | return this.runWorld.start(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_sim-ecs/serialize-save.ts: -------------------------------------------------------------------------------- 1 | import {buildWorld, IPreptimeWorld, SerialFormat} from '../../../../..'; 2 | import {IBenchmark} from "../../benchmark.spec"; 3 | 4 | class Transform {} 5 | class Position { x = 0 } 6 | class Rotation {} 7 | class Velocity { x = 1 } 8 | 9 | export class Benchmark implements IBenchmark { 10 | readonly name = 'sim-ecs'; 11 | comment = ''; 12 | world: IPreptimeWorld; 13 | world2: IPreptimeWorld; 14 | 15 | constructor( 16 | protected iterCount: number 17 | ) { 18 | this.world = buildWorld() 19 | .withComponents( 20 | Transform, 21 | Position, 22 | Rotation, 23 | Velocity, 24 | ) 25 | .build(); 26 | 27 | this.world2 = buildWorld() 28 | .withComponents( 29 | Transform, 30 | Position, 31 | Rotation, 32 | Velocity, 33 | ) 34 | .build(); 35 | } 36 | 37 | reset() {} 38 | 39 | async init(): Promise { 40 | for (let i = 0; i < 1000; i++) { 41 | this.world.buildEntity() 42 | .withAll( 43 | Transform, 44 | Position, 45 | Rotation, 46 | Velocity, 47 | ) 48 | .build(); 49 | } 50 | 51 | { 52 | const json = this.world.save().toJSON(); 53 | this.comment = `file size: ${new TextEncoder().encode(json).length / 1024} KB`; 54 | } 55 | } 56 | 57 | async run() { 58 | const json = this.world.save().toJSON(); 59 | this.world2.load(SerialFormat.fromJSON(json)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_sim-ecs/simple-insert.ts: -------------------------------------------------------------------------------- 1 | import {buildWorld, IPreptimeWorld} from '../../../../..'; 2 | import { IBenchmark } from '../../benchmark.spec'; 3 | 4 | class Transform {} 5 | class Position { x = 0 } 6 | class Rotation {} 7 | class Velocity { x = 1 } 8 | 9 | export class Benchmark implements IBenchmark { 10 | readonly name = 'sim-ecs'; 11 | world!: IPreptimeWorld; 12 | 13 | constructor( 14 | protected readonly iterCount: number 15 | ) {} 16 | 17 | async init(): Promise { 18 | this.world = await buildWorld() 19 | .withComponents( 20 | Transform, 21 | Position, 22 | Rotation, 23 | Velocity, 24 | ) 25 | .build(); 26 | } 27 | 28 | async reset() { 29 | this.world.clearEntities(); 30 | } 31 | 32 | run() { 33 | for (let i = 0; i < this.iterCount; i++) { 34 | this.world.buildEntity() 35 | .withAll( 36 | Transform, 37 | Position, 38 | Rotation, 39 | Velocity, 40 | ) 41 | .build(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_sim-ecs/simple-iter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildWorld, 3 | createSystem, 4 | IPreptimeWorld, 5 | IRuntimeWorld, 6 | queryComponents, 7 | Read, 8 | Write 9 | } from '../../../../..'; 10 | import type {IBenchmark} from "../../benchmark.spec"; 11 | import {CheckEndSystem, CounterResource} from "./_"; 12 | 13 | class Transform { 14 | } 15 | 16 | class Position { 17 | x = 0 18 | } 19 | 20 | class Rotation { 21 | } 22 | 23 | class Velocity { 24 | x = 1 25 | } 26 | 27 | 28 | const SimpleIterSystem = createSystem({ 29 | query: queryComponents({ 30 | pos: Write(Position), 31 | vel: Read(Velocity) 32 | }) 33 | }).withRunFunction(({query}) => { 34 | let pos, vel; 35 | for ({pos, vel} of query.iter()) { 36 | pos.x += vel.x; 37 | } 38 | }).build(); 39 | 40 | 41 | export class Benchmark implements IBenchmark { 42 | readonly name = 'sim-ecs'; 43 | prepWorld: IPreptimeWorld; 44 | runWorld!: IRuntimeWorld; 45 | 46 | constructor( 47 | protected iterCount: number 48 | ) { 49 | this.prepWorld = buildWorld() 50 | .withDefaultScheduling(root => root 51 | .addNewStage(stage => stage 52 | .addSystem(SimpleIterSystem) 53 | .addSystem(CheckEndSystem) 54 | ) 55 | ) 56 | .withComponents( 57 | Transform, 58 | Position, 59 | Rotation, 60 | Velocity, 61 | ) 62 | .build(); 63 | 64 | this.prepWorld.addResource(CounterResource, iterCount); 65 | 66 | for (let i = 0; i < 1000; i++) { 67 | this.prepWorld.buildEntity() 68 | .withAll( 69 | Transform, 70 | Position, 71 | Rotation, 72 | Velocity, 73 | ) 74 | .build(); 75 | } 76 | } 77 | 78 | reset(): void { 79 | this.runWorld.getResource(CounterResource).count = 0; 80 | } 81 | 82 | async init(): Promise { 83 | this.runWorld = await this.prepWorld.prepareRun({ 84 | // to make the comparison fair, we will iterate in a sync loop over the steps, just like the others do 85 | executionFunction: (fn: Function) => fn() 86 | }); 87 | return this.runWorld.transitionActions.flushCommands(); 88 | } 89 | 90 | run(): Promise { 91 | return this.runWorld.start(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_tick-knock/index.ts: -------------------------------------------------------------------------------- 1 | import {Benchmark as Schedule} from "./schedule"; 2 | import {Benchmark as SimpleInsert} from "./simple-insert"; 3 | import {Benchmark as SimpleIter} from "./simple-iter"; 4 | 5 | export { 6 | Schedule, 7 | SimpleInsert, 8 | SimpleIter, 9 | }; 10 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_tick-knock/simple-insert.ts: -------------------------------------------------------------------------------- 1 | import {Engine, Entity} from 'tick-knock'; 2 | import {IBenchmark} from "../../benchmark.spec"; 3 | 4 | class Transform {} 5 | class Position { x = 0 } 6 | class Rotation {} 7 | class Velocity { x = 1 } 8 | 9 | export class Benchmark implements IBenchmark { 10 | readonly name = 'tick-knock'; 11 | world: Engine; 12 | 13 | constructor( 14 | protected iterCount: number 15 | ) { 16 | this.world = new Engine(); 17 | } 18 | 19 | init() {} 20 | 21 | reset() { 22 | this.world.removeAllEntities(); 23 | } 24 | 25 | run() { 26 | for (let i = 0; i < this.iterCount; i++) { 27 | this.world.addEntity(new Entity() 28 | .add(new Transform()) 29 | .add(new Position()) 30 | .add(new Rotation()) 31 | .add(new Velocity())); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/_tick-knock/simple-iter.ts: -------------------------------------------------------------------------------- 1 | import {Engine, Entity, Query, System} from 'tick-knock'; 2 | import {IBenchmark} from "../../benchmark.spec"; 3 | 4 | class Transform {} 5 | class Position { x = 0 } 6 | class Rotation {} 7 | class Velocity { x = 1 } 8 | 9 | class SimpleIterSystem extends System { 10 | query = new Query(entity => entity.hasAll(Position, Velocity)); 11 | 12 | update() { 13 | let entity; 14 | for (entity of this.query.entities) { 15 | entity.get(Position)!.x += entity.get(Velocity)!.x; 16 | } 17 | } 18 | } 19 | 20 | export class Benchmark implements IBenchmark { 21 | readonly name = 'tick-knock'; 22 | world: Engine; 23 | 24 | constructor( 25 | protected iterCount: number 26 | ) { 27 | const system = new SimpleIterSystem(); 28 | 29 | this.world = new Engine() 30 | .addSystem(system) 31 | .addQuery(system.query); 32 | 33 | for (let i = 0; i < 1000; i++) { 34 | this.world.addEntity(new Entity() 35 | .add(new Transform()) 36 | .add(new Position()) 37 | .add(new Rotation()) 38 | .add(new Velocity())); 39 | } 40 | } 41 | 42 | init() {} 43 | 44 | reset() {} 45 | 46 | run() { 47 | for (let i = 0; i < this.iterCount; i++) { 48 | this.world.update(0); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/bench/src/libraries/index.ts: -------------------------------------------------------------------------------- 1 | import * as ApeECS from "./_ape-ecs"; 2 | import * as BitECS from "./_bitecs"; 3 | import * as Javelin from "./_javelin"; 4 | import * as SimECS from "./_sim-ecs"; 5 | import * as TickKnock from "./_tick-knock"; 6 | import {IBenchmarkConstructor} from "../benchmark.spec"; 7 | 8 | 9 | export const scheduleBenchmarks: IBenchmarkConstructor[] = [ 10 | ApeECS.Schedule, 11 | BitECS.Schedule, 12 | Javelin.Schedule, 13 | SimECS.Schedule, 14 | TickKnock.Schedule, 15 | ]; 16 | 17 | export const serializeBenchmarks: IBenchmarkConstructor[] = [ 18 | ApeECS.SerializeSave, 19 | Javelin.SerializeSave, 20 | SimECS.SerializeSave, 21 | ]; 22 | 23 | export const simpleInsertBenchmarks: IBenchmarkConstructor[] = [ 24 | ApeECS.SimpleInsert, 25 | BitECS.SimpleInsert, 26 | Javelin.SimpleInsert, 27 | SimECS.SimpleInsert, 28 | TickKnock.SimpleInsert, 29 | ]; 30 | 31 | export const simpleIterBenchmarks: IBenchmarkConstructor[] = [ 32 | ApeECS.SimpleIter, 33 | BitECS.SimpleIter, 34 | Javelin.SimpleIter, 35 | SimECS.SimpleIter, 36 | TickKnock.SimpleIter, 37 | ]; 38 | -------------------------------------------------------------------------------- /examples/bench/src/suite.spec.ts: -------------------------------------------------------------------------------- 1 | import {IBenchmarkConstructor, ISuiteResult} from "./benchmark.spec"; 2 | 3 | export interface ISuite { 4 | readonly name: string 5 | 6 | init(libBenches: IBenchmarkConstructor[], iterCount: number, probeCount: number): Promise | void 7 | reset(): Promise | void 8 | run(): AsyncIterableIterator 9 | } 10 | -------------------------------------------------------------------------------- /examples/bench/src/suites/default.ts: -------------------------------------------------------------------------------- 1 | import {ISuite} from "../suite.spec"; 2 | import {IBenchmark, IBenchmarkConstructor, ISuiteResult, ISuiteResults} from "../benchmark.spec"; 3 | 4 | const defaultSuite = { 5 | benchmarks: [] as IBenchmark[], 6 | name: 'Default Suite', 7 | probeCount: 0, 8 | 9 | init(libBenches: IBenchmarkConstructor[], iterCount: number, probeCount: number) { 10 | this.probeCount = probeCount; 11 | this.benchmarks.length = 0; 12 | 13 | libBenches.forEach(Benchmark => { 14 | this.benchmarks.push(new Benchmark(iterCount)); 15 | }); 16 | }, 17 | 18 | async reset() { 19 | for (const bench of this.benchmarks) { 20 | await bench.reset(); 21 | } 22 | }, 23 | 24 | async *run(): AsyncIterableIterator { 25 | const results: ISuiteResults = {}; 26 | 27 | for (const bench of this.benchmarks) { 28 | const runTimes: number[] = []; 29 | 30 | await bench.init(); 31 | 32 | // burn-in 33 | for (let i = 0; i < 1000; i++) { 34 | await bench.run(); 35 | await bench.reset(); 36 | } 37 | 38 | for (let i = 0; i < this.probeCount; i++) { 39 | const start = process.hrtime.bigint(); 40 | await bench.run(); 41 | runTimes.push(Number(process.hrtime.bigint() - start) / 1000000); 42 | await bench.reset(); 43 | } 44 | 45 | const totalTime = runTimes.reduce((acc, val) => acc + val, 0); 46 | results[bench.name] = { 47 | name: bench.name, 48 | comment: bench.comment ?? '', 49 | averageTime: totalTime / runTimes.length, 50 | fastestTime: Math.min.apply(Math, runTimes), 51 | slowestTime: Math.max.apply(Math, runTimes), 52 | totalTime, 53 | }; 54 | 55 | yield { 56 | name: this.name, 57 | currentResult: results[bench.name], 58 | results, 59 | } as ISuiteResult; 60 | } 61 | } 62 | }; 63 | 64 | export const suite: ISuite = defaultSuite; 65 | -------------------------------------------------------------------------------- /examples/bench/src/suites/index.ts: -------------------------------------------------------------------------------- 1 | import {suite as defaultSuite} from "./default"; 2 | 3 | export const suites = [ 4 | defaultSuite, 5 | ]; 6 | -------------------------------------------------------------------------------- /examples/bench/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "declaration": false, 5 | "downlevelIteration": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "importHelpers": true, 8 | "lib": [ 9 | "esnext", 10 | "dom" 11 | ], 12 | "module": "CommonJS", 13 | "moduleResolution": "node", 14 | "noImplicitAny": true, 15 | "preserveConstEnums": true, 16 | "removeComments": true, 17 | "sourceMap": true, 18 | "strict": true, 19 | "target": "es2020" 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "dist" 24 | ], 25 | "files": [ 26 | "src/index.ts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /examples/events.ts: -------------------------------------------------------------------------------- 1 | import {buildWorld, createSystem, ReadEvents, Storage, WriteEvents} from "../src/index.ts"; 2 | 3 | /// This example creates a new event, a system that triggers the event once per second, 4 | /// and a system that prints a message whenever the event is received. 5 | 6 | class MyEvent { 7 | constructor( 8 | public message: string 9 | ) {} 10 | } 11 | 12 | const EventTriggerSystem = createSystem({ 13 | myEvents: WriteEvents(MyEvent), 14 | lastEvent: Storage({timestamp: 0}), 15 | }).withRunFunction(async ({myEvents, lastEvent}) => { 16 | if (Date.now() - lastEvent.timestamp >= 1000) { 17 | await myEvents.publish(new MyEvent('My event just happened!')); 18 | lastEvent.timestamp = Date.now(); 19 | } 20 | }).build(); 21 | 22 | const EventListenerSystem = createSystem({ 23 | myEvents: ReadEvents(MyEvent), 24 | }).withRunFunction(({myEvents}) => { 25 | let myEvent; 26 | for (myEvent of myEvents.iter()) { 27 | console.log(myEvent.message); 28 | } 29 | }).build(); 30 | 31 | 32 | const prepWorld = buildWorld() 33 | .withDefaultScheduling(root => root 34 | /// Stages will run after one another, however the systems inside may run in any order and even in parallel 35 | .addNewStage(stage => stage.addSystem(EventTriggerSystem)) 36 | /// So, if we want to receive the shutdown event on the same step, we need to use a later stage 37 | .addNewStage(stage => stage.addSystem(EventListenerSystem)) 38 | ) 39 | .build(); 40 | 41 | (async () => { 42 | const runWorld = await prepWorld.prepareRun(); 43 | await runWorld.start(); 44 | })().catch(console.error).then(() => console.log('Finished.')); 45 | -------------------------------------------------------------------------------- /examples/pong/.gitignore: -------------------------------------------------------------------------------- 1 | !package-lock.json 2 | public/ 3 | -------------------------------------------------------------------------------- /examples/pong/README.md: -------------------------------------------------------------------------------- 1 | # Pong 2 | 3 | This is a user-journey example game written using sim-ecs. 4 | It shows how to create a simple application with this library and also highlights possible problems to its developers. 5 | -------------------------------------------------------------------------------- /examples/pong/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pong 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/pong/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pong", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "sim-ecs": "file:../../src", 7 | "tslib": "~2" 8 | }, 9 | "engines": { 10 | "node": ">=16.20" 11 | }, 12 | "devDependencies": { 13 | "typescript": "^5", 14 | "vite": "^5" 15 | }, 16 | "scripts": { 17 | "build": "vite build", 18 | "dev": "vite --open", 19 | "install-deps": "npm i --lockfile-version 2", 20 | "start": "vite --open" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/pong/src/app/actions.ts: -------------------------------------------------------------------------------- 1 | export enum EActions { 2 | Continue, 3 | Exit, 4 | Play, 5 | } 6 | -------------------------------------------------------------------------------- /examples/pong/src/app/persistence.ts: -------------------------------------------------------------------------------- 1 | import {type ITransitionActions, queryEntities, SerialFormat, type TGroupHandle, WithTag} from "sim-ecs"; 2 | import {ScoreBoard} from "../models/score-board.ts"; 3 | import {ETags} from "../models/tags.ts"; 4 | 5 | 6 | const saveKey = 'save'; 7 | 8 | export function load(actions: ITransitionActions): TGroupHandle { 9 | const save = localStorage.getItem(saveKey); 10 | 11 | if (!save) { 12 | throw new Error('No save available. Cannot load!'); 13 | } 14 | 15 | return actions.commands.load(SerialFormat.fromJSON(save), { 16 | replaceResources: true, 17 | }); 18 | } 19 | 20 | export function save(actions: ITransitionActions) { 21 | localStorage.setItem(saveKey, actions.save({ 22 | entities: queryEntities(WithTag(ETags.save)), 23 | resources: [ScoreBoard], 24 | }).toJSON()); 25 | } 26 | -------------------------------------------------------------------------------- /examples/pong/src/app/util.ts: -------------------------------------------------------------------------------- 1 | export function relToScreenCoords(canvas: HTMLCanvasElement, x: number, y: number): [number, number] { 2 | return [x * canvas.width, y * canvas.height]; 3 | } 4 | -------------------------------------------------------------------------------- /examples/pong/src/components/collision.ts: -------------------------------------------------------------------------------- 1 | import { IEntity } from "sim-ecs"; 2 | 3 | export class Collision { 4 | collisionObjects: IEntity[] = []; 5 | occurred = false; 6 | } 7 | -------------------------------------------------------------------------------- /examples/pong/src/components/paddle.ts: -------------------------------------------------------------------------------- 1 | export enum EPaddleSide { 2 | Left, 3 | Right, 4 | } 5 | 6 | export class Paddle { 7 | constructor( 8 | public side: EPaddleSide, 9 | ) {} 10 | } 11 | -------------------------------------------------------------------------------- /examples/pong/src/components/position.ts: -------------------------------------------------------------------------------- 1 | export class Position { 2 | constructor( 3 | public x = 0, 4 | public y = 0, 5 | ) {} 6 | } 7 | -------------------------------------------------------------------------------- /examples/pong/src/components/shape.ts: -------------------------------------------------------------------------------- 1 | import {Dimensions} from "../models/dimensions.ts"; 2 | 3 | export class Shape { 4 | constructor( 5 | public dimensions: Dimensions, 6 | public color = 'white', 7 | ) {} 8 | } 9 | -------------------------------------------------------------------------------- /examples/pong/src/components/ui-item.ts: -------------------------------------------------------------------------------- 1 | import {EActions} from "../app/actions.ts"; 2 | 3 | export class UIItem { 4 | public captionMod?: (str: string) => string | undefined; 5 | 6 | constructor( 7 | public caption: string, 8 | public color: string, 9 | public fontSize: number, 10 | public action?: EActions, 11 | public active?: boolean, 12 | public activeColor?: string, 13 | ) {} 14 | } 15 | -------------------------------------------------------------------------------- /examples/pong/src/components/velocity.ts: -------------------------------------------------------------------------------- 1 | export class Velocity { 2 | constructor( 3 | public x = 0, 4 | public y = 0, 5 | ) {} 6 | } 7 | -------------------------------------------------------------------------------- /examples/pong/src/components/wall.ts: -------------------------------------------------------------------------------- 1 | export enum EWallType { 2 | Horizontal, 3 | Vertical, 4 | } 5 | 6 | export enum EWallSide { 7 | Left, 8 | None, 9 | Right, 10 | } 11 | 12 | export class Wall { 13 | constructor( 14 | public wallType: EWallType, 15 | public wallSide: EWallSide = EWallSide.None 16 | ) {} 17 | } 18 | -------------------------------------------------------------------------------- /examples/pong/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./main.ts"; 2 | -------------------------------------------------------------------------------- /examples/pong/src/main.css: -------------------------------------------------------------------------------- 1 | html, body, canvas { 2 | display: block; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | width: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /examples/pong/src/models/dimensions.ts: -------------------------------------------------------------------------------- 1 | export class Dimensions { 2 | constructor( 3 | public width: number, 4 | public height?: number, 5 | ) {} 6 | } 7 | -------------------------------------------------------------------------------- /examples/pong/src/models/game-store.ts: -------------------------------------------------------------------------------- 1 | import { EKeyState } from '../systems/input' 2 | import {IState} from "sim-ecs"; 3 | 4 | export enum EMovement { 5 | up, 6 | halt, 7 | down, 8 | } 9 | 10 | export class GameStore { 11 | continue = false 12 | currentState?: IState 13 | lastFrameDeltaTime = 0 14 | input: { 15 | actions: { 16 | leftPaddleMovement: EMovement 17 | menuConfirm: boolean 18 | menuMovement: EMovement 19 | togglePause: boolean 20 | rightPaddleMovement: EMovement 21 | } 22 | keyStates: { 23 | [key: string]: EKeyState | undefined 24 | } 25 | } = { 26 | actions: { 27 | leftPaddleMovement: EMovement.halt, 28 | menuConfirm: false, 29 | menuMovement: EMovement.halt, 30 | togglePause: false, 31 | rightPaddleMovement: EMovement.halt, 32 | }, 33 | keyStates: {}, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/pong/src/models/paddle-transforms.ts: -------------------------------------------------------------------------------- 1 | import {Transform} from "./transform.ts"; 2 | 3 | export class PaddleTransforms { 4 | constructor( 5 | public left: Transform, 6 | public right: Transform, 7 | ) {} 8 | } 9 | -------------------------------------------------------------------------------- /examples/pong/src/models/score-board.ts: -------------------------------------------------------------------------------- 1 | export class ScoreBoard { 2 | left = 0; 3 | right = 0; 4 | } 5 | -------------------------------------------------------------------------------- /examples/pong/src/models/tags.ts: -------------------------------------------------------------------------------- 1 | export enum ETags { 2 | ball, 3 | save, 4 | } 5 | -------------------------------------------------------------------------------- /examples/pong/src/models/transform.ts: -------------------------------------------------------------------------------- 1 | import {Position} from "../components/position.ts"; 2 | import {Dimensions} from "./dimensions.ts"; 3 | 4 | export class Transform { 5 | constructor( 6 | public position: Position, 7 | public dimensions: Dimensions, 8 | ) {} 9 | } 10 | -------------------------------------------------------------------------------- /examples/pong/src/prefabs/menu.ts: -------------------------------------------------------------------------------- 1 | import {EActions} from "../app/actions.ts"; 2 | import {type Position} from "../components/position.ts"; 3 | import {type UIItem} from "../components/ui-item.ts"; 4 | 5 | // This could also be pure JSON, but in order to use TS types and have static checks it is recommended to write it as TS array. 6 | export const menuPrefab = [ 7 | { // Title 8 | Position: { 9 | x: 0.05, 10 | y: 0.05, 11 | } satisfies Position, 12 | UIItem: { 13 | caption: 'PONG', 14 | color: '#ddd', 15 | fontSize: 64, 16 | } satisfies UIItem, 17 | }, 18 | { // Sub title 19 | Position: { 20 | x: 0.05, 21 | y: 0.12, 22 | } satisfies Position, 23 | UIItem: { 24 | caption: 'A sim-ecs usage demo', 25 | color: '#ddd', 26 | fontSize: 24, 27 | } satisfies UIItem, 28 | }, 29 | { 30 | Position: { 31 | x: 0.05, 32 | y: 0.2, 33 | } satisfies Position, 34 | UIItem: { 35 | caption: 'How to play: Left paddle: W/S ; Right paddle: Up/Down ; Pause: Escape', 36 | color: '#ddd', 37 | fontSize: 24, 38 | } satisfies UIItem, 39 | }, 40 | { 41 | Position: { 42 | x: 0.05, 43 | y: 0.24, 44 | } satisfies Position, 45 | UIItem: { 46 | caption: 'The game will be saved upon pausing!', 47 | color: '#ddd', 48 | fontSize: 24, 49 | } satisfies UIItem, 50 | }, 51 | { 52 | Position: { 53 | x: 0.15, 54 | y: 0.35, 55 | } satisfies Position, 56 | UIItem: { 57 | action: EActions.Play, 58 | active: true, 59 | color: '#ddd', 60 | caption: 'Play', 61 | fontSize: 32, 62 | } satisfies UIItem, 63 | }, 64 | { 65 | Position: { 66 | x: 0.15, 67 | y: 0.4, 68 | } satisfies Position, 69 | UIItem: { 70 | action: EActions.Continue, 71 | color: '#ddd', 72 | caption: 'Continue', 73 | fontSize: 32, 74 | } satisfies UIItem, 75 | }, 76 | { 77 | Position: { 78 | x: 0.15, 79 | y: 0.45, 80 | } satisfies Position, 81 | UIItem: { 82 | action: EActions.Exit, 83 | color: '#ddd', 84 | caption: 'Exit', 85 | fontSize: 32, 86 | } satisfies UIItem, 87 | }, 88 | ]; 89 | -------------------------------------------------------------------------------- /examples/pong/src/prefabs/pause.ts: -------------------------------------------------------------------------------- 1 | import {type UIItem} from "../components/ui-item.ts"; 2 | import {type Position} from "../components/position.ts"; 3 | 4 | // This could also be pure JSON, but in order to use TS types and have static checks it is recommended to write it as TS array. 5 | export const pausePrefab = [ 6 | { 7 | Position: { 8 | x: 0.05, 9 | y: 0.02, 10 | } satisfies Position, 11 | UIItem: { 12 | caption: '❚❚ PAUSE', 13 | color: '#ddd', 14 | fontSize: 64, 15 | } satisfies UIItem, 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /examples/pong/src/prefabs/savable.ts: -------------------------------------------------------------------------------- 1 | import {ETags} from "../models/tags.ts"; 2 | import {type Collision} from "../components/collision.ts"; 3 | import {type Position} from "../components/position.ts"; 4 | import {type Shape} from "../components/shape.ts"; 5 | import {type Velocity} from "../components/velocity.ts"; 6 | import {EPaddleSide, type Paddle} from "../components/paddle.ts"; 7 | import {defaultBallPositionX, defaultBallPositionY, defaultBallVelocityX, defaultBallVelocityY} from "./game.ts"; 8 | import {CTagMarker} from "sim-ecs"; 9 | 10 | export const savablePrefab = [ 11 | { // Ball 12 | [CTagMarker]: [ETags.ball, ETags.save], 13 | Collision: { 14 | collisionObjects: [], 15 | occurred: false, 16 | } satisfies Collision, 17 | Position: { 18 | x: defaultBallPositionX, 19 | y: defaultBallPositionY, 20 | } satisfies Position, 21 | Shape: { 22 | color: 'red', 23 | dimensions: { 24 | width: 0.01, 25 | }, 26 | } satisfies Shape, 27 | Velocity: { 28 | x: defaultBallVelocityX, 29 | y: defaultBallVelocityY, 30 | } satisfies Velocity, 31 | }, 32 | { // Left paddle 33 | [CTagMarker]: [ETags.save], 34 | Collision: { 35 | collisionObjects: [], 36 | occurred: false, 37 | } satisfies Collision, 38 | Paddle: { 39 | side: EPaddleSide.Left, 40 | } satisfies Paddle, 41 | Shape: { 42 | color: '#ddd', 43 | dimensions: { 44 | height: 0.15, 45 | width: 0.005, 46 | }, 47 | } satisfies Shape, 48 | }, 49 | { // Right paddle 50 | [CTagMarker]: [ETags.save], 51 | Collision: { 52 | collisionObjects: [], 53 | occurred: false, 54 | } satisfies Collision, 55 | Paddle: { 56 | side: EPaddleSide.Right, 57 | } satisfies Paddle, 58 | Shape: { 59 | color: '#ddd', 60 | dimensions: { 61 | height: 0.15, 62 | width: 0.005, 63 | }, 64 | } satisfies Shape, 65 | }, 66 | ]; -------------------------------------------------------------------------------- /examples/pong/src/schedules/default.ts: -------------------------------------------------------------------------------- 1 | import {type ISyncPointPrefab} from "sim-ecs"; 2 | import {BeforeStepSystem} from "../systems/before-step.ts"; 3 | import {MenuSystem} from "../systems/menu.ts"; 4 | import {RenderGameSystem} from "../systems/render-game.ts"; 5 | import {RenderUISystem} from "../systems/render-ui.ts"; 6 | import {InputSystem} from "../systems/input.ts"; 7 | import {ErrorSystem} from "../systems/error.ts"; 8 | 9 | 10 | export const defaultSchedule: ISyncPointPrefab = { 11 | stages: [ 12 | [BeforeStepSystem], 13 | [InputSystem], 14 | [MenuSystem], 15 | [ 16 | RenderGameSystem, 17 | RenderUISystem, 18 | ], 19 | [ErrorSystem], 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /examples/pong/src/schedules/game.ts: -------------------------------------------------------------------------------- 1 | import {type ISyncPointPrefab} from "sim-ecs"; 2 | import {BeforeStepSystem} from "../systems/before-step.ts"; 3 | import {MenuSystem} from "../systems/menu.ts"; 4 | import {PaddleSystem} from "../systems/paddle.ts"; 5 | import {PauseSystem} from "../systems/pause.ts"; 6 | import {CollisionSystem} from "../systems/collision.ts"; 7 | import {BallSystem} from "../systems/ball.ts"; 8 | import {AnimationSystem} from "../systems/animation.ts"; 9 | import {RenderGameSystem} from "../systems/render-game.ts"; 10 | import {RenderUISystem} from "../systems/render-ui.ts"; 11 | import {InputSystem} from "../systems/input.ts"; 12 | import {ErrorSystem} from "../systems/error.ts"; 13 | 14 | 15 | export const gameSchedule: ISyncPointPrefab = { 16 | stages: [ 17 | [BeforeStepSystem], 18 | [InputSystem], 19 | [ 20 | MenuSystem, 21 | PaddleSystem, 22 | PauseSystem, 23 | ], 24 | [CollisionSystem], 25 | [BallSystem], 26 | [AnimationSystem], 27 | [ 28 | RenderGameSystem, 29 | RenderUISystem, 30 | ], 31 | [ErrorSystem], 32 | ], 33 | }; 34 | -------------------------------------------------------------------------------- /examples/pong/src/schedules/pause.ts: -------------------------------------------------------------------------------- 1 | import {type ISyncPointPrefab} from "sim-ecs"; 2 | import {BeforeStepSystem} from "../systems/before-step.ts"; 3 | import {InputSystem} from "../systems/input.ts"; 4 | import {PauseSystem} from "../systems/pause.ts"; 5 | import {RenderGameSystem} from "../systems/render-game.ts"; 6 | import {RenderUISystem} from "../systems/render-ui.ts"; 7 | import {ErrorSystem} from "../systems/error.ts"; 8 | 9 | 10 | export const pauseSchedule: ISyncPointPrefab = { 11 | stages: [ 12 | [BeforeStepSystem], 13 | [InputSystem], 14 | [PauseSystem], 15 | [ 16 | RenderGameSystem, 17 | RenderUISystem, 18 | ], 19 | [ErrorSystem], 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /examples/pong/src/states/game.ts: -------------------------------------------------------------------------------- 1 | import {type ITransitionActions, queryEntities, SerialFormat, State, type TGroupHandle, With} from "sim-ecs"; 2 | import {gamePrefab} from "../prefabs/game.ts"; 3 | import {EPaddleSide, Paddle} from "../components/paddle.ts"; 4 | import {Position} from "../components/position.ts"; 5 | import {GameStore} from "../models/game-store.ts"; 6 | import {Velocity} from "../components/velocity.ts"; 7 | import {load} from "../app/persistence.ts"; 8 | import {Shape} from "../components/shape.ts"; 9 | import {UIItem} from "../components/ui-item.ts"; 10 | import {ScoreBoard} from "../models/score-board.ts"; 11 | import {savablePrefab} from "../prefabs/savable.ts"; 12 | 13 | export class GameState extends State { 14 | saveDataPrefabHandle?: TGroupHandle; 15 | staticDataPrefabHandle?: TGroupHandle; 16 | 17 | activate(actions: ITransitionActions) { 18 | actions.getResource(GameStore).currentState = this; 19 | } 20 | 21 | async create(actions: ITransitionActions) { 22 | const gameStore = actions.getResource(GameStore); 23 | 24 | this.staticDataPrefabHandle = createNewGame(actions); 25 | if (gameStore.continue) { 26 | this.saveDataPrefabHandle = load(actions); 27 | } else { 28 | this.saveDataPrefabHandle = await createGameFromPrefab(actions); 29 | } 30 | 31 | await actions.flushCommands(); 32 | setScoreCaptionMod(actions); 33 | } 34 | 35 | destroy(actions: ITransitionActions) { 36 | if (this.staticDataPrefabHandle) { 37 | actions.commands.removeGroup(this.staticDataPrefabHandle); 38 | } 39 | 40 | if (this.saveDataPrefabHandle) { 41 | actions.commands.removeGroup(this.saveDataPrefabHandle); 42 | } 43 | 44 | return actions.flushCommands(); 45 | } 46 | } 47 | 48 | const createNewGame = function (actions: ITransitionActions) { 49 | return actions.commands.load(SerialFormat.fromArray(gamePrefab)); 50 | }; 51 | 52 | const createGameFromPrefab = async function (actions: ITransitionActions) { 53 | const prefabHandle = actions.commands.load(SerialFormat.fromArray(savablePrefab)); 54 | await actions.flushCommands(); 55 | 56 | for (const entity of actions.getEntities(queryEntities( 57 | With(Paddle), 58 | With(Shape), 59 | ))) { 60 | actions.commands.mutateEntity(entity, entity => { 61 | entity 62 | .addComponent(new Position( 63 | entity.getComponent(Paddle)!.side == EPaddleSide.Left 64 | ? 0 65 | : 1 - entity.getComponent(Shape)!.dimensions.width, 66 | )) 67 | .addComponent(new Velocity()); 68 | } 69 | ); 70 | } 71 | 72 | return prefabHandle; 73 | }; 74 | 75 | const setScoreCaptionMod = function (actions: ITransitionActions) { 76 | const score = actions.getResource(ScoreBoard); 77 | 78 | for (const entity of actions.getEntities(queryEntities( 79 | With(Paddle), 80 | With(UIItem), 81 | ))) { 82 | const ui = entity.getComponent(UIItem)!; 83 | const paddle = entity.getComponent(Paddle)!; 84 | 85 | if (paddle.side == EPaddleSide.Left) { 86 | ui.captionMod = strIn => strIn.replace('{}', score.left.toString()); 87 | } else { 88 | ui.captionMod = strIn => strIn.replace('{}', score.right.toString()); 89 | } 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /examples/pong/src/states/menu.ts: -------------------------------------------------------------------------------- 1 | import {type ITransitionActions, SerialFormat, State, type TGroupHandle} from "sim-ecs"; 2 | import {menuPrefab} from "../prefabs/menu.ts"; 3 | import {GameStore} from "../models/game-store.ts"; 4 | 5 | export class MenuState extends State { 6 | prefabHandle!: TGroupHandle; 7 | 8 | activate(actions: ITransitionActions) { 9 | actions.getResource(GameStore).currentState = this; 10 | this.prefabHandle = actions.commands.load(SerialFormat.fromArray(menuPrefab)); 11 | return actions.flushCommands(); 12 | } 13 | 14 | deactivate(actions: ITransitionActions) { 15 | actions.commands.removeGroup(this.prefabHandle); 16 | return actions.flushCommands(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/pong/src/states/pause.ts: -------------------------------------------------------------------------------- 1 | import {type ITransitionActions, SerialFormat, State, type TGroupHandle} from "sim-ecs"; 2 | import {pausePrefab} from "../prefabs/pause.ts"; 3 | import {GameStore} from "../models/game-store.ts"; 4 | import {save} from "../app/persistence.ts"; 5 | 6 | export class PauseState extends State { 7 | prefabHandle!: TGroupHandle; 8 | 9 | activate(actions: ITransitionActions) { 10 | const gameStore = actions.getResource(GameStore); 11 | save(actions); 12 | 13 | gameStore.currentState = this; 14 | this.prefabHandle = actions.commands.load(SerialFormat.fromArray(pausePrefab)); 15 | return actions.flushCommands(); 16 | } 17 | 18 | deactivate(actions: ITransitionActions) { 19 | actions.commands.removeGroup(this.prefabHandle); 20 | return actions.flushCommands(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/pong/src/systems/animation.ts: -------------------------------------------------------------------------------- 1 | import {Position} from "../components/position.ts"; 2 | import {createSystem, hmrSwapSystem, ISystem, queryComponents, Read, ReadResource, Write} from "sim-ecs"; 3 | import {Velocity} from "../components/velocity.ts"; 4 | import {GameStore} from "../models/game-store.ts"; 5 | 6 | 7 | export const AnimationSystem = createSystem({ 8 | gameStore: ReadResource(GameStore), 9 | query: queryComponents({ 10 | pos: Write(Position), 11 | vel: Read(Velocity), 12 | }), 13 | }) 14 | .withName('AnimationSystem') 15 | .withRunFunction(({gameStore, query}) => { 16 | const k = gameStore.lastFrameDeltaTime / 10; 17 | return query.execute(({pos, vel}) => { 18 | pos.x += vel.x * k; 19 | pos.y += vel.y * k; 20 | }); 21 | }) 22 | .build(); 23 | 24 | // @ts-ignore 25 | hmr:if (import.meta.hot) { 26 | // @ts-ignore 27 | import.meta.hot.accept(mod => hmrSwapSystem(mod[Object.getOwnPropertyNames(mod)[0]])); 28 | } 29 | -------------------------------------------------------------------------------- /examples/pong/src/systems/ball.ts: -------------------------------------------------------------------------------- 1 | import {createSystem, hmrSwapSystem, queryComponents, Read, WithTag, Write, WriteResource} from "sim-ecs"; 2 | import {Velocity} from "../components/velocity.ts"; 3 | import {Collision} from "../components/collision.ts"; 4 | import {EWallSide, EWallType, Wall} from "../components/wall.ts"; 5 | import {Paddle} from "../components/paddle.ts"; 6 | import {ScoreBoard} from "../models/score-board.ts"; 7 | import {Position} from "../components/position.ts"; 8 | import {defaultBallPositionX, defaultBallPositionY} from "../prefabs/game.ts"; 9 | import {ETags} from "../models/tags.ts"; 10 | 11 | 12 | export const BallSystem = createSystem({ 13 | scoreBoard: WriteResource(ScoreBoard), 14 | query: queryComponents({ 15 | _ball: WithTag(ETags.ball), 16 | collisionData: Read(Collision), 17 | pos: Write(Position), 18 | vel: Write(Velocity), 19 | }) 20 | }) 21 | .withName('BallSystem') 22 | .withRunFunction(({scoreBoard, query}) => { 23 | let wallCollisionHorizontal = false; 24 | let wallCollisionVertical = EWallSide.None; 25 | let paddleCollision = false; 26 | 27 | return query.execute(({collisionData, pos, vel}) => { 28 | if (collisionData.occurred) { 29 | for (const obj of collisionData.collisionObjects) { 30 | if (obj.hasComponent(Wall)) { 31 | if (obj.getComponent(Wall)!.wallType == EWallType.Horizontal) { 32 | wallCollisionHorizontal = true; 33 | } else { 34 | wallCollisionVertical = obj.getComponent(Wall)!.wallSide; 35 | } 36 | } else if (obj.hasComponent(Paddle)) { 37 | paddleCollision = true; 38 | } 39 | } 40 | 41 | if (paddleCollision) { 42 | vel.x *= -1; 43 | } 44 | 45 | if (!paddleCollision && wallCollisionVertical != EWallSide.None) { 46 | // Point for one side, restart 47 | if (wallCollisionVertical == EWallSide.Left) { 48 | scoreBoard.left++; 49 | } else { 50 | scoreBoard.right++; 51 | } 52 | 53 | pos.x = defaultBallPositionX; 54 | pos.y = defaultBallPositionY; 55 | vel.x *= -1; 56 | vel.y *= Math.random() > .5 ? 1 : -1; 57 | } 58 | 59 | if (wallCollisionHorizontal) { 60 | vel.y *= -1; 61 | } 62 | } 63 | }); 64 | }) 65 | .build(); 66 | 67 | // @ts-ignore 68 | hmr:if (import.meta.hot) { 69 | // @ts-ignore 70 | import.meta.hot.accept(mod => hmrSwapSystem(mod[Object.getOwnPropertyNames(mod)[0]])); 71 | } 72 | -------------------------------------------------------------------------------- /examples/pong/src/systems/before-step.ts: -------------------------------------------------------------------------------- 1 | import {createSystem, hmrSwapSystem, Storage, WriteResource} from "sim-ecs"; 2 | import {GameStore} from "../models/game-store.ts"; 3 | 4 | 5 | const CFrameTimeCap = 33; 6 | 7 | export const BeforeStepSystem = createSystem({ 8 | gameStore: WriteResource(GameStore), 9 | ctx: WriteResource(CanvasRenderingContext2D), 10 | storage: Storage({ lastTransition: 0 }) 11 | }) 12 | .withName('BeforeStepSystem') 13 | .withRunFunction(({gameStore, ctx, storage}) => { 14 | { // Update delta time 15 | const now = Date.now(); 16 | gameStore.lastFrameDeltaTime = Math.min(now - storage.lastTransition, CFrameTimeCap); 17 | storage.lastTransition = now; 18 | } 19 | 20 | { // Clear canvas 21 | ctx.beginPath(); 22 | ctx.fillStyle = '#222'; 23 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); 24 | } 25 | }) 26 | .build(); 27 | 28 | // @ts-ignore 29 | hmr:if (import.meta.hot) { 30 | // @ts-ignore 31 | import.meta.hot.accept(mod => hmrSwapSystem(mod[Object.getOwnPropertyNames(mod)[0]])); 32 | } 33 | -------------------------------------------------------------------------------- /examples/pong/src/systems/collision.ts: -------------------------------------------------------------------------------- 1 | import {createSystem, hmrSwapSystem, queryComponents, Read, ReadEntity, Write} from "sim-ecs"; 2 | import {Shape} from "../components/shape.ts"; 3 | import {Collision} from "../components/collision.ts"; 4 | import {Position} from "../components/position.ts"; 5 | 6 | export const CollisionSystem = createSystem({ 7 | query: queryComponents({ 8 | collision: Write(Collision), 9 | entity: ReadEntity(), 10 | position: Read(Position), 11 | shape: Read(Shape) 12 | }), 13 | }) 14 | .withName('CollisionSystem') 15 | .withRunFunction(({query}) => { 16 | const rects = Array.from(query.iter()).map(({collision, entity, position, shape}) => { 17 | // ideally, this should be two separate steps, 18 | // but JS would loop twice. 19 | // As an optimization, I will include this data change into the map() function 20 | collision.collisionObjects.length = 0; 21 | collision.occurred = false; 22 | 23 | return { 24 | collisionData: collision, 25 | entity, 26 | height: shape.dimensions.height ?? shape.dimensions.width, 27 | width: shape.dimensions.width, 28 | x: position.x, 29 | y: position.y, 30 | }; 31 | }); 32 | 33 | // check for collision between all collision-enabled shapes 34 | // in this simple game, we only have 7 collision objects, so we don't need anything fancy! 35 | for (let i = 0; i < rects.length; i++) { 36 | for (let j = 0; j < rects.length; j++) { 37 | if (i == j) { 38 | continue; 39 | } 40 | 41 | const rect1 = rects[i]; 42 | const rect2 = rects[j]; 43 | 44 | // https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection 45 | if ( 46 | rect1.x < rect2.x + rect2.width && 47 | rect1.x + rect1.width > rect2.x && 48 | rect1.y < rect2.y + rect2.height && 49 | rect1.y + rect1.height > rect2.y 50 | ) { 51 | rect1.collisionData.occurred = true; 52 | rect1.collisionData.collisionObjects.push(rect2.entity); 53 | 54 | rect2.collisionData.occurred = true; 55 | rect2.collisionData.collisionObjects.push(rect1.entity); 56 | } 57 | } 58 | } 59 | }) 60 | .build(); 61 | 62 | // @ts-ignore 63 | hmr:if (import.meta.hot) { 64 | // @ts-ignore 65 | import.meta.hot.accept(mod => hmrSwapSystem(mod[Object.getOwnPropertyNames(mod)[0]])); 66 | } 67 | -------------------------------------------------------------------------------- /examples/pong/src/systems/error.ts: -------------------------------------------------------------------------------- 1 | import {createSystem, hmrSwapSystem, ReadEvents, SystemError} from "sim-ecs"; 2 | 3 | export const ErrorSystem = createSystem({ 4 | errors: ReadEvents(Error), 5 | systemErrors: ReadEvents(SystemError), 6 | }) 7 | .withName('ErrorSystem') 8 | .withRunFunction(({errors, systemErrors}) => { 9 | let error; 10 | 11 | for (error of errors.iter()) { 12 | console.error('HANDLED ERROR!', error); 13 | } 14 | 15 | for (error of systemErrors.iter()) { 16 | console.error('HANDLED ERROR! System:', error.System, '; Cause:', error.cause); 17 | } 18 | }).build(); 19 | 20 | // @ts-ignore 21 | hmr:if (import.meta.hot) { 22 | // @ts-ignore 23 | import.meta.hot.accept(mod => hmrSwapSystem(mod[Object.getOwnPropertyNames(mod)[0]])); 24 | } 25 | -------------------------------------------------------------------------------- /examples/pong/src/systems/menu.ts: -------------------------------------------------------------------------------- 1 | import {Actions, createSystem, hmrSwapSystem, queryComponents, Storage, Write, WriteResource} from "sim-ecs"; 2 | import {UIItem} from "../components/ui-item.ts"; 3 | import {EMovement, GameStore} from "../models/game-store.ts"; 4 | import {EActions} from "../app/actions.ts"; 5 | import {GameState} from "../states/game.ts"; 6 | 7 | 8 | export const MenuSystem = createSystem({ 9 | actions: Actions, 10 | gameStore: WriteResource(GameStore), 11 | storage: Storage({ menuAction: EActions.Play }), 12 | query: queryComponents({ 13 | uiItem: Write(UIItem) 14 | }), 15 | }) 16 | .withName('MenuSystem') 17 | .withRunFunction(({actions, gameStore, storage, query}) => { 18 | // todo: use index 19 | if (gameStore.input.actions.menuMovement == EMovement.down) { 20 | switch (storage.menuAction) { 21 | case EActions.Play: storage.menuAction = EActions.Continue; break; 22 | case EActions.Continue: storage.menuAction = EActions.Exit; break; 23 | case EActions.Exit: storage.menuAction = EActions.Play; break; 24 | default: { 25 | throw new Error(`Action ${storage.menuAction} not implemented!`); 26 | } 27 | } 28 | } 29 | else if (gameStore.input.actions.menuMovement == EMovement.up) { 30 | switch (storage.menuAction) { 31 | case EActions.Play: storage.menuAction = EActions.Exit; break; 32 | case EActions.Continue: storage.menuAction = EActions.Play; break; 33 | case EActions.Exit: storage.menuAction = EActions.Continue; break; 34 | default: { 35 | throw new Error(`Action ${storage.menuAction} not implemented!`); 36 | } 37 | } 38 | } 39 | 40 | if (gameStore.input.actions.menuConfirm) { 41 | if (storage.menuAction == EActions.Play) { 42 | actions.commands.pushState(GameState); 43 | } 44 | else if (storage.menuAction == EActions.Continue) { 45 | if (localStorage.getItem('save') == null) { 46 | return; 47 | } 48 | 49 | gameStore.continue = true; 50 | actions.commands.pushState(GameState); 51 | } 52 | else { 53 | actions.commands.stopRun(); 54 | } 55 | 56 | return; 57 | } 58 | 59 | for (const {uiItem} of query.iter()) { 60 | uiItem.active = uiItem.action == storage.menuAction; 61 | } 62 | }) 63 | .build(); 64 | 65 | // @ts-ignore 66 | hmr:if (import.meta.hot) { 67 | // @ts-ignore 68 | import.meta.hot.accept(mod => hmrSwapSystem(mod[Object.getOwnPropertyNames(mod)[0]])); 69 | } 70 | -------------------------------------------------------------------------------- /examples/pong/src/systems/paddle.ts: -------------------------------------------------------------------------------- 1 | import {createSystem, hmrSwapSystem, queryComponents, Read, ReadResource, Write} from "sim-ecs"; 2 | import {EMovement, GameStore} from "../models/game-store.ts"; 3 | import {EPaddleSide, Paddle} from "../components/paddle.ts"; 4 | import {Position} from "../components/position.ts"; 5 | import {Velocity} from "../components/velocity.ts"; 6 | import {Shape} from "../components/shape.ts"; 7 | import {PaddleTransforms} from "../models/paddle-transforms.ts"; 8 | import {Dimensions} from "../models/dimensions.ts"; 9 | import {Transform} from "../models/transform.ts"; 10 | 11 | export const PaddleSystem = createSystem({ 12 | gameStore: ReadResource(GameStore), 13 | paddleTrans: ReadResource(PaddleTransforms), 14 | query: queryComponents({ 15 | paddle: Read(Paddle), 16 | pos: Read(Position), 17 | shape: Read(Shape), 18 | vel: Write(Velocity) 19 | }), 20 | }) 21 | .withName('PaddleSystem') 22 | .withRunFunction(({gameStore, paddleTrans, query}) => { 23 | return query.execute(({paddle, pos, shape, vel}) => { 24 | updateTransformationResource(paddle.side, pos, shape.dimensions, paddleTrans); 25 | updateVelocity( 26 | pos, 27 | vel, 28 | shape.dimensions.height ?? shape.dimensions.width, 29 | gameStore.lastFrameDeltaTime, 30 | paddle.side == EPaddleSide.Left 31 | ? gameStore.input.actions.leftPaddleMovement 32 | : gameStore.input.actions.rightPaddleMovement 33 | ); 34 | }); 35 | }) 36 | .build(); 37 | 38 | 39 | function updateTransformationResource(side: EPaddleSide, pos: Position, dim: Dimensions, paddleTrans: PaddleTransforms) { 40 | const update = (trans: Transform) => { 41 | trans.position.x = pos.x; 42 | trans.position.y = pos.y; 43 | trans.dimensions.height = dim.height; 44 | trans.dimensions.width = dim.width; 45 | } 46 | 47 | switch (side) { 48 | case EPaddleSide.Left: { 49 | update(paddleTrans.left); 50 | break; 51 | } 52 | case EPaddleSide.Right: { 53 | update(paddleTrans.right); 54 | break; 55 | } 56 | } 57 | } 58 | 59 | function updateVelocity(pos: Position, vel: Velocity, paddleHeight: number, deltaTime: number, movement: EMovement) { 60 | switch (movement) { 61 | case EMovement.down: { 62 | if (pos.y + paddleHeight >= 1) { 63 | pos.y = 1 - paddleHeight; 64 | } else { 65 | vel.y = (1 - paddleHeight) * deltaTime / 500; 66 | } 67 | 68 | break; 69 | } 70 | case EMovement.halt: { 71 | vel.y = 0; 72 | break; 73 | } 74 | case EMovement.up: { 75 | if (pos.y <= 0) { 76 | pos.y = 0; 77 | } else { 78 | vel.y = -((1 - paddleHeight) * deltaTime / 500); 79 | } 80 | 81 | break; 82 | } 83 | } 84 | } 85 | 86 | // @ts-ignore 87 | hmr:if (import.meta.hot) { 88 | // @ts-ignore 89 | import.meta.hot.accept(mod => hmrSwapSystem(mod[Object.getOwnPropertyNames(mod)[0]])); 90 | } 91 | -------------------------------------------------------------------------------- /examples/pong/src/systems/pause.ts: -------------------------------------------------------------------------------- 1 | import {Actions, createSystem, hmrSwapSystem, ReadResource} from "sim-ecs"; 2 | import {GameStore} from "../models/game-store.ts"; 3 | import {GameState} from "../states/game.ts"; 4 | import {PauseState} from "../states/pause.ts"; 5 | 6 | 7 | export const PauseSystem = createSystem({ 8 | actions: Actions, 9 | gameStore: ReadResource(GameStore), 10 | }) 11 | .withName('PauseSystem') 12 | .withRunFunction(({actions, gameStore}) => { 13 | const isGameState = gameStore.currentState?.constructor == GameState; 14 | const isPauseState = gameStore.currentState?.constructor == PauseState; 15 | 16 | if (!isGameState && !isPauseState) { 17 | return; 18 | } 19 | 20 | if (gameStore.input.actions.togglePause) { 21 | if (isGameState) { 22 | actions.commands.pushState(PauseState); 23 | } else { 24 | actions.commands.popState(); 25 | } 26 | } 27 | }) 28 | .build(); 29 | 30 | // @ts-ignore 31 | hmr:if (import.meta.hot) { 32 | // @ts-ignore 33 | import.meta.hot.accept(mod => hmrSwapSystem(mod[Object.getOwnPropertyNames(mod)[0]])); 34 | } 35 | -------------------------------------------------------------------------------- /examples/pong/src/systems/render-game.ts: -------------------------------------------------------------------------------- 1 | import {createSystem, hmrSwapSystem, queryComponents, Read, Storage, WriteResource} from "sim-ecs"; 2 | import {Position} from "../components/position.ts"; 3 | import {Shape} from "../components/shape.ts"; 4 | import {relToScreenCoords} from "../app/util.ts"; 5 | 6 | export const RenderGameSystem = createSystem({ 7 | ctx: WriteResource(CanvasRenderingContext2D), 8 | storage: Storage<{ toScreenCoords: (x: number, y: number) => [number, number] }> ({toScreenCoords: () => [0, 0]}), 9 | query: queryComponents({ 10 | pos: Read(Position), 11 | shape: Read(Shape) 12 | }) 13 | }) 14 | .withName('RenderGameSystem') 15 | .withSetupFunction(({ctx, storage}) => { 16 | storage.toScreenCoords = relToScreenCoords.bind(undefined, ctx.canvas); 17 | }) 18 | .withRunFunction(({ctx, storage, query}) => { 19 | return query.execute(({pos, shape}) => { 20 | const screenDim = storage.toScreenCoords(shape.dimensions.width, shape.dimensions.height ?? 0); 21 | const screenPos = storage.toScreenCoords(pos.x, pos.y); 22 | 23 | if (!shape.dimensions.height) { 24 | screenDim[1] = screenDim[0]; 25 | } 26 | 27 | ctx.fillStyle = shape.color; 28 | ctx.fillRect(screenPos[0], screenPos[1], screenDim[0], screenDim[1]); 29 | }); 30 | }) 31 | .build(); 32 | 33 | // @ts-ignore 34 | hmr:if (import.meta.hot) { 35 | // @ts-ignore 36 | import.meta.hot.accept(mod => hmrSwapSystem(mod[Object.getOwnPropertyNames(mod)[0]])); 37 | } 38 | -------------------------------------------------------------------------------- /examples/pong/src/systems/render-ui.ts: -------------------------------------------------------------------------------- 1 | import {Position} from "../components/position.ts"; 2 | import {createSystem, hmrSwapSystem, queryComponents, Read, Storage, WriteResource} from "sim-ecs"; 3 | import {UIItem} from "../components/ui-item.ts"; 4 | import {relToScreenCoords} from "../app/util.ts"; 5 | 6 | export const RenderUISystem = createSystem({ 7 | ctx: WriteResource(CanvasRenderingContext2D), 8 | storage: Storage<{ toScreenCoords: (x: number, y: number) => [number, number] }> ({toScreenCoords: () => [0, 0]}), 9 | query: queryComponents({ 10 | pos: Read(Position), 11 | ui: Read(UIItem) 12 | }) 13 | }) 14 | .withName('RenderUISystem') 15 | .withSetupFunction(({ctx, storage}) => { 16 | storage.toScreenCoords = relToScreenCoords.bind(undefined, ctx.canvas); 17 | }) 18 | .withRunFunction(({ctx, storage, query}) => { 19 | ctx.textBaseline = 'top'; 20 | 21 | return query.execute(({pos, ui}) => { 22 | const screenPos = storage.toScreenCoords!(pos.x, pos.y); 23 | 24 | ctx.fillStyle = ui.active 25 | ? ui.activeColor ?? 'red' 26 | : ui.color; 27 | ctx.font = ui.active 28 | ? `${ui.fontSize * 1.2}px serif` 29 | : `${ui.fontSize}px serif`; 30 | ctx.fillText(getFinalCaption(ui), screenPos[0], screenPos[1]); 31 | }); 32 | }) 33 | .build(); 34 | 35 | function getFinalCaption(ui: UIItem): string { 36 | return ui.captionMod?.(ui.caption) ?? ui.caption; 37 | } 38 | 39 | // @ts-ignore 40 | hmr:if (import.meta.hot) { 41 | // @ts-ignore 42 | import.meta.hot.accept(mod => hmrSwapSystem(mod[Object.getOwnPropertyNames(mod)[0]])); 43 | } 44 | -------------------------------------------------------------------------------- /examples/pong/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": true, 4 | "alwaysStrict": true, 5 | "declaration": false, 6 | "downlevelIteration": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "importHelpers": true, 10 | "lib": [ 11 | "dom", 12 | "esnext" 13 | ], 14 | "module": "ESNext", 15 | "moduleResolution": "bundler", 16 | "noEmit": true, 17 | "noImplicitAny": true, 18 | "outDir": "dist", 19 | "preserveConstEnums": true, 20 | "removeComments": true, 21 | "sourceMap": true, 22 | "strict": true, 23 | "target": "es2020" 24 | }, 25 | "exclude": [ 26 | "node_modules", 27 | "dist" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/pong/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { resolve } from 'path'; 3 | 4 | 5 | export default defineConfig({ 6 | resolve: { 7 | alias: { 8 | 'sim-ecs': resolve(__dirname, 'node_modules/sim-ecs'), 9 | } 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /examples/system-error.ts: -------------------------------------------------------------------------------- 1 | import {Actions, buildWorld, createSystem, ReadEvents, SystemError} from "../src/index.ts"; 2 | 3 | 4 | const ErrorTriggerSystem = createSystem({}) 5 | .withName('ErrorTriggerSystem') 6 | .withRunFunction(() => { 7 | throw new Error('I was thrown in a system!'); 8 | }) 9 | .build(); 10 | 11 | const ErrorHandlerSystem = createSystem({ 12 | actions: Actions, 13 | errors: ReadEvents(Error), 14 | systemErrors: ReadEvents(SystemError), 15 | }) 16 | .withName('ErrorHandlerSystem') 17 | .withRunFunction(({actions, errors, systemErrors}) => { 18 | let error; 19 | let foundError = false; 20 | 21 | for (error of errors.iter()) { 22 | console.error('HANDLED ERROR!', error); 23 | foundError = true; 24 | } 25 | 26 | for (error of systemErrors.iter()) { 27 | console.error('HANDLED ERROR! System:', error.System, '; Cause:', error.cause); 28 | foundError = true; 29 | } 30 | 31 | if (foundError) { 32 | actions.commands.stopRun(); 33 | } 34 | }) 35 | .build(); 36 | 37 | 38 | const prepWorld = buildWorld() 39 | .withDefaultScheduling(root => root 40 | .addNewStage(stage => stage.addSystem(ErrorTriggerSystem)) 41 | .addNewStage(stage => stage.addSystem(ErrorHandlerSystem)) 42 | ) 43 | .build(); 44 | 45 | (async () => { 46 | const runWorld = await prepWorld.prepareRun(); 47 | await runWorld.start(); 48 | })().catch(console.error).then(() => console.log('Finished.')); 49 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": true, 4 | "alwaysStrict": true, 5 | "declaration": false, 6 | "downlevelIteration": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "importHelpers": true, 10 | "lib": [ 11 | "dom", 12 | "esnext" 13 | ], 14 | "module": "ESNext", 15 | "moduleResolution": "bundler", 16 | "noEmit": true, 17 | "noImplicitAny": true, 18 | "preserveConstEnums": true, 19 | "removeComments": true, 20 | "sourceMap": true, 21 | "strict": true, 22 | "target": "es2020" 23 | }, 24 | "exclude": [ 25 | "node_modules", 26 | "dist" 27 | ], 28 | "files": [ 29 | "counter.ts", 30 | "events.ts", 31 | "system-error.ts" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /media/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NSSTC/sim-ecs/630d947c37e6468607ea0823aff49f8d12958b01/media/error.png -------------------------------------------------------------------------------- /media/pong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NSSTC/sim-ecs/630d947c37e6468607ea0823aff49f8d12958b01/media/pong.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sim-ecs", 3 | "version": "0.6.5", 4 | "license": "MPL 2.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/NSSTC/sim-ecs.git" 8 | }, 9 | "homepage": "https://nsstc.github.io/sim-ecs/", 10 | "type": "module", 11 | "main": "dist/sim-ecs.cjs", 12 | "module": "dist/sim-ecs.mjs", 13 | "sideEffects": false, 14 | "types": "dist/index.d.ts", 15 | "keywords": [ 16 | "bevy", 17 | "bun", 18 | "code-splitting", 19 | "component", 20 | "data", 21 | "data-driven", 22 | "decoupling", 23 | "deno", 24 | "ecs", 25 | "entity", 26 | "event", 27 | "game", 28 | "game-dev", 29 | "game-engine", 30 | "modularization", 31 | "prefab", 32 | "scheduler", 33 | "scheduling", 34 | "simulation", 35 | "sim", 36 | "sim-ecs", 37 | "state", 38 | "splitting", 39 | "system", 40 | "typescript", 41 | "world" 42 | ], 43 | "exports": { 44 | ".": { 45 | "import": [ 46 | "./dist/sim-ecs.mjs", 47 | "./dist/index.d.ts" 48 | ], 49 | "require": "./dist/sim-ecs.cjs" 50 | } 51 | }, 52 | "engines": { 53 | "node": ">=18" 54 | }, 55 | "dependencies": { 56 | "tslib": "^2" 57 | }, 58 | "devDependencies": { 59 | "@types/chai": "^4", 60 | "@types/mocha": "^10", 61 | "chai": "^5", 62 | "esbuild": "~0.21", 63 | "madge": "^7", 64 | "mocha": "^10", 65 | "nyc": "^15", 66 | "tsx": "^4", 67 | "typedoc": "~0.25", 68 | "typescript": "^5" 69 | }, 70 | "files": [ 71 | "dist" 72 | ], 73 | "scripts": { 74 | "prebuild": "npm run loop-test-ts && npm run test", 75 | "bench": "cd examples/bench && npm run bench", 76 | "build": "npm run esmbuild && npm run dtsbuild && npm run doc", 77 | "build-examples": "cd examples && tsc --project .", 78 | "bun-example-counter": "bun examples/counter.ts", 79 | "bun-example-events": "bun examples/events.ts", 80 | "bun-example-system-error": "bun examples/system-error.ts", 81 | "ci": "npm run prebuild && npm run cjsbuild && npm run esmbuild && npm run postbuild", 82 | "doc": "typedoc src/index.ts --out docs --includeVersion --excludeInternal --excludePrivate --excludeProtected --media ./media", 83 | "cjsbuild": "esbuild src/index.ts --bundle --outfile=dist/sim-ecs.cjs --allow-overwrite --format=cjs --sourcemap --target=es2020 --minify", 84 | "coverage": "nyc -r lcov -e .ts -x \"**/*.test.ts\" -x \"**/*.spec.ts\" -x \"src/tests\" npm run test", 85 | "deno-example-counter": "deno run examples/counter.ts", 86 | "deno-example-events": "deno run examples/events.ts", 87 | "deno-example-system-error": "deno run examples/system-error.ts", 88 | "dtsbuild": "tsc -P ./tsconfig-tsc.json --emitDeclarationOnly", 89 | "esmbuild": "esbuild src/index.ts --bundle --outfile=dist/sim-ecs.mjs --allow-overwrite --format=esm --sourcemap --target=es2020 --minify", 90 | "example-counter": "tsx --tsconfig tsconfig.json examples/counter.ts", 91 | "example-events": "tsx --tsconfig tsconfig.json examples/events.ts", 92 | "example-system-error": "tsx --tsconfig tsconfig.json examples/system-error.ts", 93 | "loop-test-js": "madge -c .", 94 | "loop-test-ts": "madge -c --extensions ts --warning --orphans ./src", 95 | "prepare": "npm run build", 96 | "test": "mocha --experimental-specifier-resolution=node --import=tsx src/**/*.test.ts", 97 | "postbuild": "npm run loop-test-js" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /qodana.yaml: -------------------------------------------------------------------------------- 1 | #-------------------------------------------------------------------------------# 2 | # Qodana analysis is configured by qodana.yaml file # 3 | # https://www.jetbrains.com/help/qodana/qodana-yaml.html # 4 | #-------------------------------------------------------------------------------# 5 | version: "1.0" 6 | #Specify inspection profile for code analysis 7 | profile: 8 | name: qodana.starter 9 | #Enable inspections 10 | #include: 11 | # - name: 12 | #Disable inspections 13 | exclude: 14 | - name: All 15 | paths: 16 | - docs 17 | #Execute shell command before Qodana execution (Applied in CI/CD pipeline) 18 | #bootstrap: sh ./prepare-qodana.sh 19 | #Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) 20 | #plugins: 21 | # - id: #(plugin id can be found at https://plugins.jetbrains.com) 22 | #Specify Qodana linter for analysis (Applied in CI/CD pipeline) 23 | linter: jetbrains/qodana-jvm:latest 24 | include: 25 | - name: CheckDependencyLicenses 26 | - name: DuplicatedCode 27 | -------------------------------------------------------------------------------- /src/_.spec.ts: -------------------------------------------------------------------------------- 1 | export type TTypeProto = (new (...args: any[]) => T) & Function; 2 | export type TObjectProto = TTypeProto; 3 | 4 | export type TExecutor = () => void | Promise; 5 | -------------------------------------------------------------------------------- /src/ecs/ecs-entity.ts: -------------------------------------------------------------------------------- 1 | import type {IEntity, TEntityId} from "../entity/entity.spec.ts"; 2 | 3 | const entities = new Map>(); 4 | 5 | 6 | /** 7 | * Remove any referenced deleted entities 8 | */ 9 | export function clearRegistry(): void { 10 | let entity; 11 | let entityRef; 12 | 13 | for (entityRef of entities.values()) { 14 | entity = entityRef.deref(); 15 | 16 | if (entity) { 17 | unregisterEntity(entity); 18 | } 19 | } 20 | } 21 | 22 | /** 23 | * Get a tracked entity 24 | * @param id 25 | */ 26 | export function getEntity(id: TEntityId): IEntity | undefined { 27 | return entities.get(id)?.deref(); 28 | } 29 | 30 | /** 31 | * Register an entity by its ID 32 | * @param entity 33 | */ 34 | export function registerEntity(entity: Readonly) { 35 | entities.set(entity.id, new WeakRef(entity)); 36 | } 37 | 38 | /** 39 | * Remove an entity 40 | * @param entity 41 | */ 42 | export function unregisterEntity(entity: Readonly): void { 43 | unregisterEntityId(entity.id); 44 | } 45 | 46 | /** 47 | * Remove an entity by id 48 | * @param id 49 | */ 50 | export function unregisterEntityId(id: TEntityId): void { 51 | entities.delete(id); 52 | } 53 | -------------------------------------------------------------------------------- /src/ecs/ecs-query.ts: -------------------------------------------------------------------------------- 1 | import type {IAccessQuery, IComponentsQuery, IEntitiesQuery, TExistenceQuery} from "../query/query.spec.ts"; 2 | import {EntitiesQuery} from "../query/entities-query.ts"; 3 | import {ComponentsQuery} from "../query/components-query.ts"; 4 | import type {TObjectProto} from "../_.spec.ts"; 5 | 6 | export function queryComponents>(query: Readonly): IComponentsQuery { 7 | return new ComponentsQuery(query); 8 | } 9 | 10 | export function queryEntities(...query: Readonly>): IEntitiesQuery { 11 | return new EntitiesQuery(query); 12 | } 13 | -------------------------------------------------------------------------------- /src/ecs/ecs-sync-point.ts: -------------------------------------------------------------------------------- 1 | import type {ISyncPoint} from "../scheduler/pipeline/sync-point.spec.ts"; 2 | 3 | const syncPoints = new Map(); 4 | 5 | /** 6 | * Register a sync-point by name 7 | * @param syncPoint 8 | */ 9 | export function addSyncPoint(syncPoint: Readonly): void { 10 | if (!syncPoint.name) { 11 | throw new Error('Cannot register a sync point without a name!'); 12 | } 13 | 14 | { 15 | const name = syncPoint.name; 16 | 17 | if (syncPoints.has(name) && syncPoints.get(name) != syncPoint) { 18 | throw new Error(`Another sync point with the name "${name}" has already been registered!`); 19 | } 20 | 21 | syncPoints.set(name, syncPoint); 22 | } 23 | } 24 | 25 | /** 26 | * Find a sync-point by name, if it exists 27 | * @param name 28 | */ 29 | export function getSyncPoint(name: string): ISyncPoint | undefined { 30 | return syncPoints.get(name); 31 | } 32 | 33 | /** 34 | * Remove a sync-point from the registry by name, if it exists 35 | * @param name 36 | */ 37 | export function removeSyncPoint(name: string): void { 38 | syncPoints.delete(name); 39 | } 40 | -------------------------------------------------------------------------------- /src/ecs/ecs-world.ts: -------------------------------------------------------------------------------- 1 | import {type IWorldBuilder, WorldBuilder} from "../world/world-builder.ts"; 2 | import {SerDe} from "../serde/serde.ts"; 3 | import type {IPreptimeWorld} from "../world/preptime/preptime-world.spec.ts"; 4 | import type {IRuntimeWorld} from "../world/runtime/runtime-world.spec.ts"; 5 | import type {ISystem} from "../system/system.spec.ts"; 6 | import {PreptimeWorld} from "../world/preptime/preptime-world.ts"; 7 | 8 | const worlds = new Set(); 9 | 10 | /** 11 | * Register a world 12 | * @param world 13 | */ 14 | export function addWorld(world: Readonly) { 15 | worlds.add(world); 16 | } 17 | 18 | /** 19 | * Build a new world and automatically add it to the list of worlds inside the ECS 20 | */ 21 | export function buildWorld(): IWorldBuilder { 22 | const serde = new SerDe(); 23 | return new WorldBuilder(serde).addCallback(world => worlds.add(world)); 24 | } 25 | 26 | /** 27 | * Get a world with a name 28 | * @param name 29 | */ 30 | export function getWorld(name: string): IPreptimeWorld | IRuntimeWorld | undefined { 31 | let world; 32 | for (world of worlds) { 33 | if (world.name == name) { 34 | return world; 35 | } 36 | } 37 | } 38 | 39 | /** 40 | * Iterate over all registered worlds 41 | */ 42 | export function getWorlds(): IterableIterator { 43 | return worlds.values(); 44 | } 45 | 46 | export function hmrSwapSystem(system: ISystem): void { 47 | let world; 48 | let ptWorld; 49 | 50 | for (world of worlds) { 51 | if (world instanceof PreptimeWorld) { 52 | for (ptWorld of world.getExistingRuntimeWorlds()) { 53 | ptWorld.hmrReplaceSystem(system); 54 | } 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * Remove a world 61 | * @param world 62 | */ 63 | export function removeWorld(world: Readonly) { 64 | worlds.delete(world); 65 | } 66 | -------------------------------------------------------------------------------- /src/entity/entity-builder.spec.ts: -------------------------------------------------------------------------------- 1 | import type {IEntity} from "./entity.spec.ts"; 2 | import type {TObjectProto} from "../_.spec.ts"; 3 | 4 | export interface IEntityBuilder { 5 | /** 6 | * Create entity and add it to the world 7 | */ 8 | build(): IEntity 9 | 10 | /** 11 | * Add component to target entity 12 | * @param component 13 | * @param args 14 | */ 15 | with(component: Readonly, ...args: ReadonlyArray): IEntityBuilder 16 | 17 | /** 18 | * Add all components to target entity 19 | * @param component 20 | */ 21 | withAll(...component: ReadonlyArray): IEntityBuilder 22 | } 23 | 24 | export type TEntityBuilderProto = { new(): TEntityBuilderProto }; 25 | -------------------------------------------------------------------------------- /src/entity/entity-builder.ts: -------------------------------------------------------------------------------- 1 | import {Entity, type TEntityId} from "./entity.ts"; 2 | import type {IEntityBuilder} from "./entity-builder.spec.ts"; 3 | import type {TObjectProto} from "../_.spec.ts"; 4 | 5 | export * from './entity-builder.spec.ts'; 6 | 7 | export class EntityBuilder implements IEntityBuilder { 8 | protected components = new Map, ReadonlyArray>(); 9 | 10 | constructor( 11 | protected uuid?: TEntityId, 12 | protected callback?: (entity: Entity) => void, 13 | ) {} 14 | 15 | build(): Entity { 16 | const entity = new Entity(this.uuid); 17 | let component; 18 | 19 | for (component of this.components) { 20 | entity.addComponent(component[0], ...component[1]); 21 | } 22 | 23 | this.callback?.(entity); 24 | return entity; 25 | } 26 | 27 | with(component: Readonly, ...args: ReadonlyArray): EntityBuilder { 28 | this.components.set(component, args); 29 | return this; 30 | } 31 | 32 | withAll(...components: ReadonlyArray): EntityBuilder { 33 | let component; 34 | for (component of components) { 35 | this.with(component); 36 | } 37 | 38 | return this; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/events/_.ts: -------------------------------------------------------------------------------- 1 | import type {TObjectProto} from "../_.spec.ts"; 2 | 3 | export type TSubscriber = (event: Readonly>) => Promise | void; 4 | -------------------------------------------------------------------------------- /src/events/event-bus.spec.ts: -------------------------------------------------------------------------------- 1 | import type {TObjectProto} from "../_.spec.ts"; 2 | import type {IEventReader} from "./event-reader.spec.ts"; 3 | import type {IEventWriter} from "./event-writer.spec.ts"; 4 | import type {TSubscriber} from "./_.ts"; 5 | 6 | export * from "./_.ts"; 7 | 8 | export interface IEventBus { 9 | createReader(Event: Readonly): IEventReader 10 | createWriter(): IEventWriter 11 | publish(event: Readonly): Promise 12 | subscribe(Event: Readonly, handler: TSubscriber): void 13 | subscribeReader(reader: Readonly>): void 14 | unsubscribe(Event: Readonly, handler: TSubscriber): void 15 | unsubscribeReader(reader: Readonly>): void 16 | } 17 | -------------------------------------------------------------------------------- /src/events/event-bus.ts: -------------------------------------------------------------------------------- 1 | import type {IEventBus} from "./event-bus.spec.ts"; 2 | import type {TObjectProto} from "../_.spec.ts"; 3 | import type {IEventReader} from "./event-reader.spec.ts"; 4 | import {EventReader} from "./event-reader.ts"; 5 | import {EventWriter} from "./event-writer.ts"; 6 | import type {TSubscriber} from "./_.ts"; 7 | 8 | export * from "./event-bus.spec.ts"; 9 | 10 | export class EventBus implements IEventBus { 11 | protected subscribers = new Map, Set>>(); 12 | 13 | createReader(Event: Readonly): EventReader { 14 | return new EventReader(this, Event); 15 | } 16 | 17 | createWriter(): EventWriter { 18 | return new EventWriter(this); 19 | } 20 | 21 | async publish(event: Readonly): Promise { 22 | const subscribers = this.subscribers.get(event.constructor as TObjectProto) ?? []; 23 | let handler; 24 | 25 | for (handler of subscribers.values()) { 26 | await handler(event); 27 | } 28 | } 29 | 30 | subscribe(Event: T, handler: TSubscriber): void { 31 | let subscriberList = this.subscribers.get(Event); 32 | 33 | if (!subscriberList) { 34 | subscriberList = new Set(); 35 | this.subscribers.set(Event, subscriberList); 36 | } 37 | 38 | subscriberList.add(handler); 39 | } 40 | 41 | subscribeReader(reader: Readonly>): void { 42 | this.subscribe(reader.eventType, reader.eventHandler); 43 | } 44 | 45 | unsubscribe(Event: T, handler: TSubscriber): void { 46 | let subscriberList = this.subscribers.get(Event); 47 | 48 | if (!subscriberList) { 49 | subscriberList = new Set(); 50 | this.subscribers.set(Event, subscriberList); 51 | } 52 | 53 | subscriberList.delete(handler); 54 | } 55 | 56 | unsubscribeReader(reader: Readonly>): void { 57 | this.unsubscribe(reader.eventType, reader.eventHandler); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/events/event-reader.spec.ts: -------------------------------------------------------------------------------- 1 | import type {TObjectProto} from "../_.spec.ts"; 2 | import type {TSubscriber} from "./_.ts"; 3 | 4 | export interface IEventReader { 5 | readonly eventHandler: TSubscriber 6 | readonly eventType: Readonly 7 | 8 | execute(handler: (event: Readonly>) => void | Promise): Promise 9 | getOne(): Readonly> | undefined 10 | iter(): IterableIterator>> 11 | } 12 | -------------------------------------------------------------------------------- /src/events/event-reader.ts: -------------------------------------------------------------------------------- 1 | import type {IEventReader} from "./event-reader.spec.ts"; 2 | import type {IEventBus} from "./event-bus.spec.ts"; 3 | import type {TObjectProto} from "../_.spec.ts"; 4 | import type {TSubscriber} from "./_.ts"; 5 | 6 | export * from "./event-reader.spec.ts"; 7 | 8 | export class EventReader implements IEventReader { 9 | protected _eventHandler: TSubscriber = event => { this.eventCache.push(event) }; 10 | protected eventCache: Array>> = []; 11 | 12 | constructor( 13 | protected bus: IEventBus, 14 | protected _eventType: Readonly, 15 | ) { 16 | bus.subscribe(_eventType, this._eventHandler); 17 | } 18 | 19 | get eventHandler(): TSubscriber { 20 | return this._eventHandler; 21 | } 22 | 23 | get eventType(): Readonly { 24 | return this._eventType; 25 | } 26 | 27 | async execute(handler: (event: Readonly>) => (void | Promise)): Promise { 28 | let event; 29 | for (event of this.iter()) { 30 | await handler(event); 31 | } 32 | } 33 | 34 | getOne(): Readonly> | undefined { 35 | return this.eventCache.shift(); 36 | } 37 | 38 | iter(): IterableIterator>> { 39 | const events = Array.from(this.eventCache); 40 | this.eventCache.length = 0; 41 | return events.values(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/events/event-writer.spec.ts: -------------------------------------------------------------------------------- 1 | import type {TObjectProto} from "../_.spec.ts"; 2 | 3 | export interface IEventWriter { 4 | publish(event: Readonly>): Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/events/event-writer.ts: -------------------------------------------------------------------------------- 1 | import type {IEventBus} from "./event-bus.spec.ts"; 2 | import type {IEventWriter} from "./event-writer.spec.ts"; 3 | import type {TObjectProto} from "../_.spec.ts"; 4 | 5 | export * from "./event-writer.spec.ts"; 6 | 7 | export class EventWriter implements IEventWriter{ 8 | constructor( 9 | protected bus: IEventBus 10 | ) {} 11 | 12 | publish(event: Readonly>): Promise { 13 | return this.bus.publish(event); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/events/internal-events.test.ts: -------------------------------------------------------------------------------- 1 | import {buildWorld} from "../ecs/ecs-world"; 2 | import {Actions, createSystem, ReadEvents} from "../system/system"; 3 | import {SimECSAddEntityEvent} from "./internal-events"; 4 | import {Entity} from "../entity/entity"; 5 | import {assert} from "chai"; 6 | 7 | describe('Test internal events', () => { 8 | it('Entity added', async () => { 9 | // Count the number of times the add entity event was fired and read 10 | let firedCount = 0; 11 | // Count the amount of steps we already did 12 | let loopCounter = 0; 13 | 14 | // Will listen for added entity events 15 | // and add to the firedCount 16 | const EventListenerSystem = createSystem({ 17 | myEvents: ReadEvents(SimECSAddEntityEvent), 18 | }).withRunFunction(({myEvents}) => { 19 | myEvents.execute(() => { 20 | firedCount++ 21 | }); 22 | }).build(); 23 | 24 | // Will add entities 25 | const EventTriggerSystem = createSystem({ 26 | actions: Actions, 27 | }).withRunFunction(async ({actions}) => { 28 | actions.commands.addEntity(new Entity()); 29 | }).build(); 30 | 31 | // Will count the number of steps we took and end the simulation 32 | const LoopCounterSystem = createSystem({ 33 | actions: Actions, 34 | }).withRunFunction(({ actions }) => { 35 | loopCounter++; 36 | if (loopCounter >= 3) { 37 | actions.commands.stopRun(); 38 | } 39 | }).build(); 40 | 41 | const prepWorld = buildWorld() 42 | .withDefaultScheduling(root => root 43 | .addNewStage(stage => stage 44 | .addSystem(EventTriggerSystem) 45 | .addSystem(EventListenerSystem) 46 | .addSystem(LoopCounterSystem) 47 | ) 48 | ) 49 | .build(); 50 | 51 | const runWorld = await prepWorld.prepareRun(); 52 | 53 | // Start and await the end of the simulation 54 | await runWorld.start(); 55 | 56 | assert.equal(loopCounter, 3, 'Did not get to three steps exactly!'); 57 | // Since entities are added after the step, the first step will not fire an event, 58 | // so the number of events will always be one smaller than the number of steps. 59 | assert.equal(firedCount, loopCounter - 1, 'Not all added entities fired an event!'); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Polyfills 2 | 3 | // @ts-ignore 4 | Symbol.dispose ??= Symbol('Symbol.dispose'); 5 | // @ts-ignore 6 | Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose'); 7 | 8 | 9 | // Import all 10 | 11 | export * from './ecs/ecs-entity.ts'; 12 | export * from './ecs/ecs-query.ts'; 13 | export * from './ecs/ecs-sync-point.ts'; 14 | export * from './ecs/ecs-world.ts'; 15 | export * from './entity/entity.ts'; 16 | export * from './entity/entity-builder.ts'; 17 | export * from './query/query.ts'; 18 | export * from './events/event-bus.ts'; 19 | export * from './events/internal-events.ts'; 20 | export * from './pda/sim-ecs-pda.ts'; 21 | export * from './scheduler/scheduler.ts'; 22 | export * from './scheduler/pipeline/pipeline.ts'; 23 | export * from './scheduler/pipeline/stage.ts'; 24 | export * from './scheduler/pipeline/sync-point.ts'; 25 | export * from './serde/serde.ts'; 26 | export * from './serde/serial-format.ts'; 27 | export * from './state/state.ts'; 28 | export * from './system/system.ts'; 29 | export * from './system/system-builder.ts'; 30 | export * from './world/error.ts'; 31 | export * from './world/error.spec.ts'; 32 | export * from './world/events.ts'; 33 | export * from './world/actions.spec.ts'; 34 | export * from './world/world.spec.ts'; 35 | export * from './world/preptime/preptime-world.ts'; 36 | export * from './world/runtime/runtime-world.ts'; 37 | export * from './world/world-builder.ts'; 38 | -------------------------------------------------------------------------------- /src/pda/pda.spec.ts: -------------------------------------------------------------------------------- 1 | // todo: this PushDownAutomaton could get its own package on npm 2 | import type {TTypeProto} from "../_.spec.ts"; 3 | 4 | export interface IPushDownAutomaton { 5 | /** 6 | * Number of states in the PDA 7 | */ 8 | readonly size: number 9 | /** 10 | * Current state 11 | */ 12 | readonly state?: T 13 | 14 | /** 15 | * Clear the PDA by GC-ing all entries 16 | */ 17 | clear(): void 18 | 19 | /** 20 | * Remove the current state from the stack and return it 21 | */ 22 | pop(): T | undefined 23 | 24 | /** 25 | * Put a new state on the stack and return a ref to the created instance 26 | * @param state 27 | */ 28 | push

>(state: P): T 29 | } 30 | 31 | export type TPushDownAutomatonProto = { new(): IPushDownAutomaton }; 32 | export default IPushDownAutomaton; 33 | -------------------------------------------------------------------------------- /src/pda/pda.test.ts: -------------------------------------------------------------------------------- 1 | import {PushDownAutomaton} from './pda'; 2 | import {assert} from 'chai'; 3 | 4 | describe('Test PDA', () => { 5 | class State {} 6 | class State1 extends State {} 7 | 8 | it('push', () => { 9 | const pda = new PushDownAutomaton(); 10 | assert.equal(pda.size, 0); 11 | pda.push(State1); 12 | assert.equal(pda.size, 1); 13 | assert.isTrue(pda.state instanceof State1); 14 | }); 15 | 16 | it('pop', () => { 17 | const pda = new PushDownAutomaton(); 18 | pda.push(State1); 19 | assert.equal(pda.size, 1); 20 | assert.isTrue(pda.pop() instanceof State1); 21 | assert.equal(pda.size, 0); 22 | assert.equal(pda.pop(), undefined); 23 | assert.equal(pda.size, 0); 24 | }); 25 | 26 | it('clear', () => { 27 | const pda = new PushDownAutomaton(); 28 | pda.push(State1); 29 | pda.push(State1); 30 | pda.push(State1); 31 | pda.push(State1); 32 | pda.push(State1); 33 | 34 | assert.equal(pda.size, 5); 35 | 36 | pda.clear(); 37 | 38 | assert.equal(pda.size, 0); 39 | assert.equal(pda.pop(), undefined); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/pda/pda.ts: -------------------------------------------------------------------------------- 1 | import type IPushDownAutomaton from "./pda.spec.ts"; 2 | import type {TTypeProto} from "../_.spec.ts"; 3 | 4 | export * from "./pda.spec.ts"; 5 | 6 | type TStateNode = { 7 | state: T, 8 | prevNode?: TStateNode, 9 | }; 10 | 11 | export class PushDownAutomaton implements IPushDownAutomaton { 12 | protected currentState?: T; 13 | #size = 0; 14 | protected statesTail?: TStateNode; 15 | 16 | get size(): number { 17 | return this.#size; 18 | } 19 | 20 | get state(): Readonly { 21 | return this.currentState; 22 | } 23 | 24 | clear(): void { 25 | this.currentState = undefined; 26 | this.statesTail = undefined; 27 | this.#size = 0; 28 | } 29 | 30 | pop(): T | undefined { 31 | if (!this.statesTail) return; 32 | 33 | const oldTail = this.statesTail; 34 | 35 | this.statesTail = this.statesTail.prevNode; 36 | this.currentState = this.statesTail?.state; 37 | this.#size--; 38 | 39 | return oldTail.state; 40 | } 41 | 42 | push

>(State: P): T { 43 | this.currentState = new State(); 44 | this.statesTail = { 45 | prevNode: this.statesTail, 46 | state: this.currentState, 47 | }; 48 | 49 | this.#size++; 50 | return this.currentState; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/pda/sim-ecs-pda.ts: -------------------------------------------------------------------------------- 1 | import {PushDownAutomaton} from "./pda.ts"; 2 | import type {IRuntimeWorld} from "../world/runtime/runtime-world.spec.ts"; 3 | import type {TTypeProto} from "../_.spec.ts"; 4 | import {SimECSPDAPopStateEvent, SimECSPDAPushStateEvent} from "../events/internal-events.ts"; 5 | import type {IState} from "../state/state.spec.ts"; 6 | import type {ITransitionActions} from "../world/actions.spec.ts"; 7 | 8 | export * from "./pda.ts"; 9 | 10 | export class SimECSPushDownAutomaton { 11 | #pda = new PushDownAutomaton(); 12 | 13 | constructor( 14 | protected world: IRuntimeWorld 15 | ) {} 16 | 17 | get state(): T | undefined { 18 | return this.#pda.state; 19 | } 20 | 21 | clear(actions: Readonly): void { 22 | while (this.#pda.state !== undefined) { 23 | this.#pda.pop()!.destroy(actions); 24 | } 25 | 26 | this.#pda.clear(); 27 | } 28 | 29 | async pop(): Promise { 30 | const oldState = this.#pda.pop(); 31 | await this.world.eventBus.publish(new SimECSPDAPopStateEvent(oldState, this.#pda.state)) 32 | return oldState as T | undefined; 33 | } 34 | 35 | async push

>(State: P): Promise { 36 | const oldState = this.state; 37 | const newState = this.#pda.push(State); 38 | await this.world.eventBus.publish(new SimECSPDAPushStateEvent(oldState, newState)); 39 | return newState; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/query/_.ts: -------------------------------------------------------------------------------- 1 | export const addEntitySym: unique symbol = Symbol(); 2 | export const clearEntitiesSym: unique symbol = Symbol(); 3 | export const removeEntitySym: unique symbol = Symbol(); 4 | export const runSortSym: unique symbol = Symbol(); 5 | export const setEntitiesSym: unique symbol = Symbol(); 6 | 7 | export const accessDescSym: unique symbol = Symbol(); 8 | export const existenceDescSym: unique symbol = Symbol(); 9 | 10 | export const entitySym: unique symbol = Symbol(); 11 | -------------------------------------------------------------------------------- /src/query/components-query.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IAccessDescriptor, 3 | IAccessQuery, 4 | IComponentsQuery, 5 | TAccessQueryData, 6 | TAccessQueryParameter, 7 | } from "./query.spec.ts"; 8 | import {EQueryType, ETargetType} from "./query.spec.ts"; 9 | import {Query} from "./query.ts"; 10 | import type {TObjectProto} from "../_.spec.ts"; 11 | import type {IEntity, TTag} from "../entity/entity.spec.ts"; 12 | import {accessDescSym, addEntitySym, entitySym} from "./_.ts"; 13 | 14 | export class ComponentsQuery> extends Query> implements IComponentsQuery { 15 | constructor( 16 | protected queryDescriptor: Readonly, 17 | ) { 18 | super(EQueryType.Components, queryDescriptor); 19 | } 20 | 21 | [addEntitySym](entity: Readonly): void { 22 | if (this.matchesEntity(entity)) { 23 | this.isSortDirty = true; 24 | this.queryResult.push({ 25 | [entitySym]: entity, 26 | ...this.getComponentDataFromEntity(entity, this.queryDescriptor), 27 | }); 28 | } 29 | } 30 | 31 | protected getComponentDataFromEntity(entity: Readonly, descriptor: Readonly): Readonly> { 32 | const components: Record> = {}; 33 | let accessDesc; 34 | let componentDesc: Readonly>; 35 | let componentName: string; 36 | 37 | for ([componentName, componentDesc] of Object.entries(descriptor)) { 38 | accessDesc = (componentDesc as IAccessDescriptor)[accessDescSym]; 39 | 40 | components[componentName] = accessDesc.targetType == ETargetType.component 41 | ? (entity.getComponent(accessDesc.target as TObjectProto) ?? entity) 42 | : entity; 43 | } 44 | 45 | return components as TAccessQueryData; 46 | } 47 | 48 | matchesEntity(entity: Readonly): boolean { 49 | let componentDesc: Readonly>; 50 | 51 | // @ts-ignore todo: figure out typing. Something is still wrong somewhere 52 | for (componentDesc of Object.values(this.queryDescriptor)) { 53 | if ( 54 | componentDesc[accessDescSym].targetType == ETargetType.tag 55 | && !entity.hasTag(componentDesc[accessDescSym].target as TTag) 56 | ) { 57 | return false; 58 | } 59 | 60 | if ( 61 | componentDesc[accessDescSym].targetType == ETargetType.component 62 | && !entity.hasComponent(componentDesc[accessDescSym].target as TObjectProto) 63 | ) { 64 | if (componentDesc[accessDescSym].optional) { 65 | continue; 66 | } 67 | 68 | return false; 69 | } 70 | 71 | if ( 72 | componentDesc[accessDescSym].targetType == ETargetType.entity 73 | && componentDesc[accessDescSym].data !== undefined 74 | && componentDesc[accessDescSym].data != entity.id 75 | ) { 76 | return false; 77 | } 78 | } 79 | 80 | return true; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/query/entities-query.ts: -------------------------------------------------------------------------------- 1 | import type {IEntitiesQuery, TExistenceQuery} from "./query.spec.ts"; 2 | import {EExistence, EQueryType, ETargetType} from "./query.spec.ts"; 3 | import {Query} from "./query.ts"; 4 | import type {IEntity, TTag} from "../entity/entity.spec.ts"; 5 | import {addEntitySym, entitySym, existenceDescSym} from "./_.ts"; 6 | import type {TObjectProto} from "../_.spec.ts"; 7 | 8 | export class EntitiesQuery extends Query, IEntity> implements IEntitiesQuery { 9 | constructor( 10 | protected queryDesc: Readonly> 11 | ) { 12 | super(EQueryType.Entities, queryDesc); 13 | } 14 | 15 | [addEntitySym](entity: Readonly): void { 16 | if (this.matchesEntity(entity)) { 17 | this.isSortDirty = true; 18 | this.queryResult.push({ [entitySym]: entity, ...entity }); 19 | } 20 | } 21 | 22 | matchesEntity(entity: Readonly): boolean { 23 | let componentDesc; 24 | 25 | for (componentDesc of this.queryDescriptor) { 26 | if ( 27 | componentDesc[existenceDescSym].targetType == ETargetType.tag 28 | && entity.hasTag(componentDesc[existenceDescSym].target as TTag) != (componentDesc[existenceDescSym].type == EExistence.set) 29 | ) { 30 | return false; 31 | } 32 | 33 | if ( 34 | componentDesc[existenceDescSym].targetType == ETargetType.component 35 | && entity.hasComponent(componentDesc[existenceDescSym].target as TObjectProto) != (componentDesc[existenceDescSym].type == EExistence.set) 36 | ) { 37 | return false; 38 | } 39 | } 40 | 41 | return true; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/query/query.spec.ts: -------------------------------------------------------------------------------- 1 | import type {TObjectProto, TTypeProto} from "../_.spec.ts"; 2 | import type {IEntity, TTag} from "../entity/entity.spec.ts"; 3 | import {accessDescSym, addEntitySym, clearEntitiesSym, existenceDescSym, removeEntitySym, setEntitiesSym} from "./_.ts"; 4 | 5 | export type TAccessQueryParameter = C & IAccessDescriptor>; 6 | export type TOptionalAccessQueryParameter = IAccessDescriptor : undefined> & C extends TObjectProto ? C : undefined; 7 | export interface IAccessQuery { [componentName: string]: TAccessQueryParameter | TOptionalAccessQueryParameter } 8 | 9 | export type TExistenceQueryParameter = IExistenceDescriptor; 10 | export type TExistenceQuery = Array>; 11 | 12 | export enum EAccess { 13 | meta, 14 | read, 15 | write, 16 | } 17 | 18 | export enum EExistence { 19 | set, 20 | unset, 21 | } 22 | 23 | export enum ETargetType { 24 | component, 25 | entity, 26 | tag, 27 | } 28 | 29 | export enum EQueryType { 30 | Components, 31 | Entities, 32 | } 33 | 34 | export type TAccessQueryData> = { 35 | [P in keyof DESC]: DESC[P] extends TAccessQueryParameter 36 | ? Required, keyof IAccessDescriptor>> 37 | : (Required, keyof IAccessDescriptor>> | undefined) 38 | } 39 | 40 | export type TComparator = (a: DATA, b: DATA) => number; 41 | 42 | export interface IAccessDescriptor { 43 | /** 44 | * @internal 45 | */ 46 | [accessDescSym]: { 47 | readonly data?: string 48 | readonly optional: boolean 49 | readonly target: TTypeProto | TTag 50 | readonly targetType: ETargetType 51 | readonly type: EAccess 52 | } 53 | } 54 | 55 | export interface IExistenceDescriptor { 56 | /** 57 | * @internal 58 | */ 59 | [existenceDescSym]: { 60 | readonly target: Readonly | TTag 61 | readonly targetType: ETargetType 62 | readonly type: EExistence 63 | } 64 | } 65 | 66 | export interface IQuery { 67 | readonly descriptor: Readonly 68 | readonly queryType: EQueryType 69 | readonly resultLength: number 70 | 71 | /** @internal */ 72 | [addEntitySym](entity: Readonly): void 73 | /** @internal */ 74 | [clearEntitiesSym](): void 75 | /** @internal */ 76 | [removeEntitySym](entity: Readonly): void 77 | /** @internal */ 78 | [setEntitiesSym](entities: Readonly>>): void 79 | 80 | execute(handler: (data: DATA) => Promise | void): Promise 81 | getFirst(): DATA | undefined 82 | iter(): IterableIterator 83 | matchesEntity(entity: Readonly): boolean 84 | toArray(): Array 85 | } 86 | 87 | export interface IQueryDescriptor extends IQuery { 88 | sort(comparator: TComparator): IQueryDescriptor 89 | } 90 | 91 | export interface IComponentsQuery> extends IQuery> {} 92 | export interface IComponentsQueryDescriptor> extends IQueryDescriptor> {} 93 | export interface IEntitiesQuery extends IQuery, IEntity> {} 94 | export interface IEntitiesQueryDescriptor extends IQueryDescriptor, IEntity> {} 95 | -------------------------------------------------------------------------------- /src/query/query.test.ts: -------------------------------------------------------------------------------- 1 | import {Read, ReadEntity, Write} from "./query"; 2 | import {queryComponents} from "../ecs/ecs-query"; 3 | 4 | class Component { 5 | health = 100 6 | } 7 | 8 | describe('Test Query', () => { 9 | it('pop', () => { 10 | const query = queryComponents({ 11 | entity: ReadEntity(), 12 | testR: Read(Component), 13 | testW: Write(Component), 14 | }); 15 | 16 | for (const {testW} of query.iter()) { 17 | testW.health++; 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/query/query.ts: -------------------------------------------------------------------------------- 1 | import {EQueryType, type IQuery, IQueryDescriptor, TComparator} from "./query.spec.ts"; 2 | import type {IEntity} from "../entity/entity.spec.ts"; 3 | import {addEntitySym, clearEntitiesSym, entitySym, removeEntitySym, runSortSym, setEntitiesSym} from "./_.ts"; 4 | 5 | export * from "./query.spec.ts"; 6 | export { 7 | Read, 8 | ReadEntity, 9 | ReadOptional, 10 | With, 11 | WithTag, 12 | Without, 13 | Write, 14 | WithoutTag, 15 | WriteOptional, 16 | } from "./query.util.ts"; 17 | 18 | 19 | export abstract class Query implements IQuery, IQueryDescriptor { 20 | protected isSortDirty = false; 21 | protected queryResult: Array = []; 22 | protected sortComparator: TComparator | undefined; 23 | 24 | protected constructor( 25 | protected _queryType: EQueryType, 26 | protected queryDescriptor: Readonly, 27 | ) {} 28 | 29 | get descriptor(): Readonly { 30 | return this.queryDescriptor; 31 | } 32 | 33 | get queryType(): EQueryType { 34 | return this._queryType; 35 | } 36 | 37 | get resultLength(): number { 38 | return this.queryResult.length; 39 | } 40 | 41 | /** @internal */ 42 | abstract [addEntitySym](entity: Readonly): void; 43 | 44 | /** @internal */ 45 | [clearEntitiesSym]() { 46 | this.queryResult.length = 0; 47 | } 48 | 49 | /** @internal */ 50 | [removeEntitySym](entity: Readonly): void { 51 | const entityIndex = this.queryResult.findIndex(data => data[entitySym] === entity); 52 | 53 | if (entityIndex < 0) { 54 | return; 55 | } 56 | 57 | this.queryResult.splice(entityIndex, 1); 58 | } 59 | 60 | /** @internal */ 61 | [runSortSym](): void { 62 | if (!this.isSortDirty || !this.sortComparator) { 63 | return; 64 | } 65 | 66 | this.queryResult = this.queryResult.sort(this.sortComparator); 67 | this.isSortDirty = false; 68 | } 69 | 70 | /** @internal */ 71 | [setEntitiesSym](entities: Readonly>>): void { 72 | let entity; 73 | 74 | this[clearEntitiesSym](); 75 | 76 | for (entity of entities) { 77 | this[addEntitySym](entity); 78 | } 79 | } 80 | 81 | async execute(handler: (data: DATA) => Promise | void): Promise { 82 | let data; 83 | for (data of this.queryResult) { 84 | await handler(data); 85 | } 86 | } 87 | 88 | getFirst(): DATA | undefined { 89 | return this.queryResult[0]; 90 | } 91 | 92 | iter(): IterableIterator { 93 | return this.queryResult.values(); 94 | } 95 | 96 | abstract matchesEntity(entity: Readonly): boolean; 97 | 98 | sort(comparator: TComparator): IQueryDescriptor { 99 | this.sortComparator = comparator; 100 | return this; 101 | } 102 | 103 | toArray(): DATA[] { 104 | return [...this.queryResult]; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/scheduler/pipeline/pipeline.spec.ts: -------------------------------------------------------------------------------- 1 | import type {ISyncPoint} from "./sync-point.spec.ts"; 2 | 3 | export interface IPipeline { 4 | readonly root: Readonly 5 | 6 | /** 7 | * Get all sync groups of this pipeline in the correct chronological order 8 | */ 9 | getGroups(): ReadonlyArray> 10 | } 11 | -------------------------------------------------------------------------------- /src/scheduler/pipeline/pipeline.ts: -------------------------------------------------------------------------------- 1 | import type {IPipeline} from "./pipeline.spec.ts"; 2 | import {type ISyncPoint, SyncPoint} from "./sync-point.ts"; 3 | 4 | export * from "./pipeline.spec.ts"; 5 | 6 | export class Pipeline implements IPipeline { 7 | protected _root: ISyncPoint = new SyncPoint(); 8 | 9 | get root(): Readonly { 10 | return this._root; 11 | } 12 | 13 | getGroups(): ReadonlyArray> { 14 | const orderedPoints: ISyncPoint[] = []; 15 | const traversePoint = (point: Readonly) => { 16 | point.before && traversePoint(point.before); 17 | orderedPoints.push(point); 18 | point.after && traversePoint(point.after); 19 | }; 20 | 21 | traversePoint(this._root); 22 | return orderedPoints; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/scheduler/pipeline/stage.spec.ts: -------------------------------------------------------------------------------- 1 | import type {ISystem} from "../../system/system.spec.ts"; 2 | import type {TExecutor} from "../../_.spec.ts"; 3 | import type {IEventBus} from "../../events/event-bus.spec.ts"; 4 | 5 | export type TStageSchedulingAlgorithm = (systems: ReadonlyArray>, eventBus: Readonly) => Promise; 6 | 7 | export interface IStage { 8 | schedulingAlgorithm: TStageSchedulingAlgorithm 9 | systems: Array 10 | 11 | /** 12 | * Append a system to this stage 13 | * @param system 14 | */ 15 | addSystem(system: Readonly>): IStage 16 | 17 | /** 18 | * Get executor to run this stage once 19 | * @param eventBus 20 | */ 21 | getExecutor(eventBus: Readonly): TExecutor 22 | } 23 | -------------------------------------------------------------------------------- /src/scheduler/pipeline/stage.ts: -------------------------------------------------------------------------------- 1 | import type {IStage, TStageSchedulingAlgorithm} from "./stage.spec.ts"; 2 | import type {ISystem} from "../../system/system.spec.ts"; 3 | import type {TExecutor, TTypeProto} from "../../_.spec.ts"; 4 | import {systemRunParamSym} from "../../system/_.ts"; 5 | import {SystemError} from "../../world/error.ts"; 6 | import type {IEventBus} from "../../events/event-bus.spec.ts"; 7 | 8 | export * from "./stage.spec.ts"; 9 | 10 | export async function defaultStageSchedulingAlgorithm(systems: ReadonlyArray>, eventBus: Readonly): Promise { 11 | let system; 12 | 13 | try { 14 | for (system of systems) { 15 | // todo: check WRITE constraints to speed it up... 16 | await system.runFunction.call(system, system[systemRunParamSym]!); 17 | } 18 | } catch (error) { 19 | if (error instanceof Error && !!system) { 20 | await eventBus.publish(new SystemError(error, system.constructor as TTypeProto)); 21 | } else { 22 | throw error; 23 | } 24 | } 25 | } 26 | 27 | export class Stage implements IStage { 28 | public schedulingAlgorithm: TStageSchedulingAlgorithm = defaultStageSchedulingAlgorithm; 29 | public systems: Array = []; 30 | 31 | addSystem(System: Readonly): Stage { 32 | this.systems.push(System); 33 | return this; 34 | } 35 | 36 | getExecutor(eventBus: Readonly): TExecutor { 37 | return this.schedulingAlgorithm.bind(this, this.systems, eventBus); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/scheduler/pipeline/sync-point.spec.ts: -------------------------------------------------------------------------------- 1 | import type {IStage} from "./stage.spec.ts"; 2 | import type {ISystem} from "../../system/system.spec.ts"; 3 | 4 | 5 | export interface ISyncPointPrefab { 6 | name?: string 7 | after?: ISyncPointPrefab 8 | before?: ISyncPointPrefab 9 | stages?: Array>>> 10 | } 11 | 12 | export interface ISyncPoint { 13 | after?: ISyncPoint 14 | before?: ISyncPoint 15 | name?: string 16 | stages: Array 17 | 18 | /** 19 | * Add a stage to this group 20 | */ 21 | addNewStage(handler: (stage: IStage) => void): ISyncPoint 22 | 23 | /** 24 | * Add a handler which is called when the sync-point is done and all activities are finished 25 | * @param handler 26 | */ 27 | addOnSyncHandler(handler: Function): ISyncPoint 28 | 29 | /** 30 | * Remove all sync handlers 31 | */ 32 | clearOnSyncHandlers(): ISyncPoint 33 | 34 | /** 35 | * Execute all sync handlers 36 | */ 37 | executeOnSyncHandlers(): Promise 38 | 39 | /** 40 | * Create an execution tree from a schedule-prefab 41 | * @param prefab 42 | */ 43 | fromPrefab(prefab: Readonly): ISyncPoint 44 | 45 | /** 46 | * Remove a sync handler 47 | * @param handler 48 | */ 49 | removeOnSyncHandler(handler: Function): ISyncPoint 50 | } 51 | -------------------------------------------------------------------------------- /src/scheduler/pipeline/sync-point.ts: -------------------------------------------------------------------------------- 1 | import type {ISyncPoint, ISyncPointPrefab} from "./sync-point.spec.ts"; 2 | import {type IStage, Stage} from "./stage.ts"; 3 | import type {ISystem} from "../../system/system.spec.ts"; 4 | 5 | export * from "./sync-point.spec.ts"; 6 | 7 | export class SyncPoint implements ISyncPoint { 8 | public after?: ISyncPoint; 9 | public before?: ISyncPoint; 10 | public name?: string 11 | public stages: Array = []; 12 | protected syncPointHandlers = new Set(); 13 | 14 | 15 | 16 | addNewStage(handler: (stage: IStage) => void): SyncPoint { 17 | const stage = new Stage(); 18 | this.stages.push(stage); 19 | handler(stage); 20 | return this; 21 | } 22 | 23 | addOnSyncHandler(handler: Function): SyncPoint { 24 | this.syncPointHandlers.add(handler); 25 | return this; 26 | } 27 | 28 | clearOnSyncHandlers(): ISyncPoint { 29 | this.syncPointHandlers.clear(); 30 | return this; 31 | } 32 | 33 | async executeOnSyncHandlers(): Promise { 34 | let handler; 35 | 36 | for (handler of this.syncPointHandlers) { 37 | await handler(); 38 | } 39 | 40 | return this; 41 | } 42 | 43 | fromPrefab({after, before, stages = []}: Readonly): SyncPoint { 44 | this.after = after 45 | ? new SyncPoint().fromPrefab(after) 46 | : undefined; 47 | this.before = before 48 | ? new SyncPoint().fromPrefab(before) 49 | : undefined; 50 | this.stages.length = 0; 51 | 52 | { 53 | let stage: ISystem[]; 54 | let system: ISystem; 55 | for (stage of stages) { 56 | this.addNewStage(newStage => { 57 | for (system of stage) { 58 | newStage.addSystem(system); 59 | } 60 | }); 61 | } 62 | } 63 | 64 | return this; 65 | } 66 | 67 | removeOnSyncHandler(handler: Function): SyncPoint { 68 | this.syncPointHandlers.delete(handler); 69 | return this; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/scheduler/scheduler.spec.ts: -------------------------------------------------------------------------------- 1 | import type {IPipeline} from "./pipeline/pipeline.spec.ts"; 2 | import type {TExecutor} from "../_.spec.ts"; 3 | import type {IRuntimeWorld} from "../world/runtime/runtime-world.spec.ts"; 4 | import type {IEventBus} from "../events/event-bus.spec.ts"; 5 | import type {ISystem} from "../system/system.spec.ts"; 6 | 7 | export type TSchedulingAlgorithm = (stageExecutors: ReadonlyArray) => Promise; 8 | 9 | /** 10 | * The sim-ecs scheduler works by breaking down scheduling in a way which makes it simple to define a pipeline and 11 | * extend it in unpredictable ways (for example by 3rd party plugins). This is possible by assigning Systems to stages 12 | * (which can have their own scheduling logic each), which are then assigned to named "sync-points". 13 | * These are constructs which can be hooked into later on easily. A pipeline is then created of all sync-points and a 14 | * (custom) scheduling logic is run over it, forming the scheduler. 15 | */ 16 | export interface IScheduler { 17 | readonly isPrepared: boolean 18 | pipeline: IPipeline 19 | schedulingAlgorithm: TSchedulingAlgorithm 20 | 21 | /** 22 | * Execute this schedule once 23 | */ 24 | getExecutor(eventBus: Readonly): TExecutor 25 | 26 | /** 27 | * Get all unique systems in this schedule 28 | */ 29 | getSystems(): ReadonlySet 30 | 31 | /** 32 | * Prepare this scheduler for usage 33 | * @param world 34 | */ 35 | prepare(world: Readonly): Promise 36 | } 37 | -------------------------------------------------------------------------------- /src/scheduler/scheduler.ts: -------------------------------------------------------------------------------- 1 | import type {IScheduler} from "./scheduler.spec.ts"; 2 | import {type IPipeline, Pipeline} from "./pipeline/pipeline.ts"; 3 | import type {TExecutor} from "../_.spec.ts"; 4 | import {systemRunParamSym} from "../system/_.ts"; 5 | import {getSystemRunParameters, ISystem} from "../system/system.ts"; 6 | import type {IRuntimeWorld} from "../world/runtime/runtime-world.spec.ts"; 7 | import type {IEventBus} from "../events/event-bus.spec.ts"; 8 | 9 | export * from "./scheduler.spec.ts"; 10 | 11 | 12 | export async function defaultSchedulingAlgorithm(stageExecutors: ReadonlyArray) { 13 | let stageExecutor; 14 | for (stageExecutor of stageExecutors) { 15 | await stageExecutor(); 16 | } 17 | } 18 | 19 | export class Scheduler implements IScheduler { 20 | #isPrepared = false; 21 | #pipeline: IPipeline = new Pipeline(); 22 | schedulingAlgorithm = defaultSchedulingAlgorithm; 23 | 24 | get isPrepared(): boolean { 25 | return this.#isPrepared; 26 | } 27 | 28 | get pipeline(): IPipeline { 29 | return this.#pipeline; 30 | } 31 | 32 | set pipeline(newPipeline: Readonly) { 33 | if (this.#isPrepared) { 34 | throw new Error('This scheduler was already prepared or is executing and cannot be changed right now!'); 35 | } 36 | 37 | this.#pipeline = newPipeline; 38 | } 39 | 40 | getExecutor(eventBus: Readonly): TExecutor { 41 | const stageExecutors: TExecutor[] = []; 42 | 43 | for (const group of this.#pipeline.getGroups()) { 44 | for (const stage of group.stages) { 45 | stageExecutors.push(stage.getExecutor(eventBus)); 46 | } 47 | 48 | stageExecutors.push(group.executeOnSyncHandlers.bind(group) as () => Promise); 49 | } 50 | 51 | return this.schedulingAlgorithm.bind(this, stageExecutors); 52 | } 53 | 54 | getSystems(): ReadonlySet { 55 | const systems = new Set(); 56 | let group, stage, system; 57 | 58 | for (group of this.pipeline.getGroups()) { 59 | for (stage of group.stages) { 60 | for (system of stage.systems) { 61 | systems.add(system); 62 | } 63 | } 64 | } 65 | 66 | return systems; 67 | } 68 | 69 | async prepare(world: Readonly): Promise { 70 | let stage; 71 | let syncPoint; 72 | let system; 73 | 74 | this.#isPrepared = false; 75 | 76 | for (syncPoint of this.pipeline.getGroups().values()) { 77 | for (stage of syncPoint.stages) { 78 | for (system of stage.systems) { 79 | system[systemRunParamSym] = getSystemRunParameters(system, world); 80 | await system.setupFunction.call(system, system[systemRunParamSym]!); 81 | } 82 | } 83 | } 84 | 85 | this.#isPrepared = true; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/serde/README.md: -------------------------------------------------------------------------------- 1 | # sim-ecs serDe 2 | 3 | This is a serializer / deserializer for sim-ecs. It was specifically optimized for usage in this ECS. 4 | 5 | 6 | ## Serial Format 7 | 8 | The serial format is typed as `Array`. Note that `TEntity`, as opposed to `IEntity`, 9 | is a `Record` of all components on the entity in their _serialized_ form. 10 | 11 | It's also important to note that resources are converted to a `TEntity` and will always be stored 12 | on **index 0** of the serial format. 13 | -------------------------------------------------------------------------------- /src/serde/_.ts: -------------------------------------------------------------------------------- 1 | export type TEntity = { [componentName: string]: unknown } 2 | -------------------------------------------------------------------------------- /src/serde/referencing.spec.ts: -------------------------------------------------------------------------------- 1 | export enum EReferenceType { 2 | Entity = 'ENTITY', 3 | } 4 | 5 | export interface IReference { 6 | /** 7 | * ID of the referenced object 8 | */ 9 | readonly id: string 10 | /** 11 | * Type of the referenced object 12 | */ 13 | readonly type: EReferenceType 14 | 15 | /** 16 | * Convert this reference to string. 17 | * The string can later on be read to create a new Reference using the static method Reference.fromString() 18 | */ 19 | toString(): string 20 | } 21 | -------------------------------------------------------------------------------- /src/serde/referencing.ts: -------------------------------------------------------------------------------- 1 | import {EReferenceType, type IReference} from "./referencing.spec.ts"; 2 | import {CMarkerSeparator, CRefMarker} from "./serde.spec.ts"; 3 | 4 | export class Reference implements IReference { 5 | constructor( 6 | public readonly type: EReferenceType, 7 | public readonly id: string, 8 | ) {} 9 | 10 | static fromString(refString: string): Readonly | undefined { 11 | const [marker, type, ...idTokens] = refString.split(CMarkerSeparator); 12 | 13 | if (marker != CRefMarker) { 14 | return undefined; 15 | } 16 | 17 | if (!type) { 18 | return undefined; 19 | } 20 | 21 | if (!(Object.values(EReferenceType) as string[]).includes(type)) { 22 | return undefined; 23 | } 24 | 25 | return new Reference(type as EReferenceType, idTokens.join()); 26 | } 27 | 28 | static isReferenceString (str: string): boolean { 29 | const [marker, type] = str.split(CMarkerSeparator); 30 | 31 | if (!type) { 32 | return false; 33 | } 34 | 35 | return marker === CRefMarker && (Object.values(EReferenceType) as string[]).includes(type); 36 | } 37 | 38 | toString(): string { 39 | return `${CRefMarker}${CMarkerSeparator}${this.type}${CMarkerSeparator}${this.id}`; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/serde/serde.spec.ts: -------------------------------------------------------------------------------- 1 | import type {IEntity} from "../entity/entity.spec.ts"; 2 | import type {TObjectProto} from "../_.spec.ts"; 3 | import type {ISerialFormat} from "./serial-format.spec.ts"; 4 | import type {IEntitiesQuery} from "../query/query.spec.ts"; 5 | 6 | /// stores the constructor name and the data blob on indices 0 and 1 accordingly 7 | export type TCustomDeserializer = (data: unknown) => IDeserializerOutput; 8 | export type TDeserializer = (constructorName: string, data: unknown) => IDeserializerOutput; 9 | export type TSerializable = unknown; 10 | export type TSerializer = (component: unknown) => TSerializable; 11 | 12 | 13 | export interface ISerDeOptions { 14 | entities?: Readonly 15 | fallbackHandler?: T 16 | /** 17 | * Replace resources in the world with loaded data 18 | * @default true 19 | */ 20 | replaceResources?: boolean 21 | resources?: ReadonlyArray 22 | useDefaultHandler?: boolean 23 | useRegisteredHandlers?: boolean 24 | } 25 | 26 | export interface IDeserializerOutput { 27 | containsRefs: boolean 28 | data: object 29 | type: Readonly 30 | } 31 | 32 | export interface ISerDeDataSet { 33 | entities: IterableIterator> 34 | resources: Readonly>> 35 | } 36 | 37 | export interface ISerDeOperations { 38 | deserializer: TCustomDeserializer 39 | serializer: TSerializer 40 | } 41 | 42 | export interface ISerDe { 43 | /** 44 | * Transform writable data to usable data objects 45 | * @param data 46 | * @param options 47 | */ 48 | deserialize(data: Readonly, options?: Readonly>): ISerDeDataSet 49 | 50 | /** 51 | * Get an overview over all registered type handlers; useful for debugging 52 | */ 53 | getRegisteredTypeHandlers(): IterableIterator<[string, Readonly]> 54 | 55 | /** 56 | * Register type handlers for transformations 57 | * @param Type 58 | * @param deserializer 59 | * @param serializer 60 | */ 61 | registerTypeHandler(Type: Readonly, deserializer: TCustomDeserializer, serializer: TSerializer): void 62 | 63 | /** 64 | * Transform data objects into writable data 65 | * @param data 66 | * @param options 67 | */ 68 | serialize(data: Readonly, options?: Readonly>): ISerialFormat 69 | 70 | /** 71 | * Remove a type handler registration 72 | * @param Type 73 | */ 74 | unregisterTypeHandler(Type: Readonly): void 75 | } 76 | 77 | 78 | export const CIdMarker = '#ID' as const; 79 | export const CMarkerSeparator = '|' as const; 80 | export const CRefMarker = '*****' as const; 81 | export const CResourceMarker = '#RES' as const; 82 | export const CResourceMarkerValue = 1 as const; 83 | export const CTagMarker = '#TAGS' as const; 84 | -------------------------------------------------------------------------------- /src/serde/serial-format.spec.ts: -------------------------------------------------------------------------------- 1 | import type {TEntity} from "./_.ts"; 2 | 3 | export interface ISerialFormat extends Array { 4 | /** 5 | * IMPORTANT: This method also exists as a static member. 6 | * Tracking for static members in TS: https://github.com/microsoft/TypeScript/issues/33892 7 | * Copy an external array 8 | * @param arr 9 | */ 10 | fromArray(arr: ReadonlyArray): ISerialFormat 11 | 12 | /** 13 | * IMPORTANT: This method also exists as a static member. 14 | * Tracking for static members in TS: https://github.com/microsoft/TypeScript/issues/33892 15 | * Read a JSON string into this structure 16 | * @param json 17 | */ 18 | fromJSON(json: string): ISerialFormat 19 | 20 | /** 21 | * Transform into JSON string 22 | * @param indentation - optional indentation, useful for human readability 23 | */ 24 | toJSON(indentation?: string | number): string 25 | } 26 | -------------------------------------------------------------------------------- /src/serde/serial-format.ts: -------------------------------------------------------------------------------- 1 | import type {ISerialFormat} from "./serial-format.spec.ts"; 2 | import type {TEntity} from "./_.ts"; 3 | 4 | export * from "./serial-format.spec.ts"; 5 | 6 | export class SerialFormat extends Array implements ISerialFormat { 7 | static fromArray(arr: ReadonlyArray): SerialFormat { 8 | return new SerialFormat().fromArray(arr); 9 | } 10 | 11 | static fromJSON(json: string): SerialFormat { 12 | return new SerialFormat().fromJSON(json); 13 | } 14 | 15 | fromArray(arr: ReadonlyArray): SerialFormat { 16 | Object.assign(this, arr); 17 | return this; 18 | } 19 | 20 | fromJSON(json: string): SerialFormat { 21 | this.length = 0; 22 | 23 | const newVals: Readonly = JSON.parse(json); 24 | 25 | if (!Array.isArray(newVals)) { 26 | throw new Error('Input JSON must be an array!'); 27 | } 28 | 29 | for (const entity of newVals) { 30 | this.push(entity); 31 | } 32 | 33 | return this; 34 | } 35 | 36 | toJSON(indentation?: string | number): string { 37 | return JSON.stringify(Array.from(this), undefined, indentation); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/state/state.spec.ts: -------------------------------------------------------------------------------- 1 | import type {ITransitionActions} from "../world/actions.spec.ts"; 2 | 3 | export interface IState { 4 | /** 5 | * Called to run tasks for state activation in the PDA 6 | */ 7 | activate(actions: Readonly): void | Promise 8 | 9 | /** 10 | * Called to run tasks, when this state is created in the PDA 11 | * @param actions 12 | */ 13 | create(actions: Readonly): void | Promise 14 | 15 | /** 16 | * Called to run deactivation tasks in the PDA 17 | */ 18 | deactivate(actions: Readonly): void | Promise 19 | 20 | /** 21 | * Called to run tasks, when this state is destroyed in the PDA 22 | * @param actions 23 | */ 24 | destroy(actions: Readonly): void | Promise 25 | } 26 | 27 | export interface IIStateProto { new(): IState } 28 | -------------------------------------------------------------------------------- /src/state/state.ts: -------------------------------------------------------------------------------- 1 | import type {IState, IIStateProto} from "./state.spec.ts"; 2 | import type {ITransitionActions} from "../world/actions.spec.ts"; 3 | 4 | export * from './state.spec.ts'; 5 | 6 | export class State implements IState { 7 | activate(_actions: Readonly): void | Promise {} 8 | 9 | create(_actions: Readonly): void | Promise {} 10 | 11 | deactivate(_actions: Readonly): void | Promise {} 12 | 13 | destroy(_actions: Readonly): void | Promise {} 14 | } 15 | 16 | export interface IStateProto extends IIStateProto { new(): State } -------------------------------------------------------------------------------- /src/system/_.ts: -------------------------------------------------------------------------------- 1 | export const systemEventReaderSym = Symbol(); 2 | export const systemEventWriterSym = Symbol(); 3 | export const systemResourceTypeSym = Symbol(); 4 | export const systemRunParamSym = Symbol(); 5 | -------------------------------------------------------------------------------- /src/system/system-builder.spec.ts: -------------------------------------------------------------------------------- 1 | import type {ISystem, TSystemFunction, TSystemParameterDesc} from "./system.spec.ts"; 2 | 3 | export interface ISystemBuilder { 4 | parameterDesc: Readonly 5 | setupFunction: TSystemFunction> 6 | runFunction: TSystemFunction> 7 | 8 | /** 9 | * Build the system as defined in code 10 | */ 11 | build(): Readonly>> 12 | 13 | /** 14 | * Alias for [withName]{@link ISystemBuilder#withName} 15 | * @param name 16 | */ 17 | name(name: string): ISystemBuilder> 18 | 19 | /** 20 | * Alias for [withRunFunction]{@link ISystemBuilder#withRunFunction} 21 | * @param fn 22 | */ 23 | run(fn: TSystemFunction>): ISystemBuilder> 24 | 25 | /** 26 | * Alias for [withSetupFunction]{@link ISystemBuilder#withSetupFunction} 27 | * @param fn 28 | */ 29 | setup(fn: TSystemFunction>): ISystemBuilder> 30 | 31 | /** 32 | * Give the system a name which is used for registration and error messages 33 | * @param name 34 | */ 35 | withName(name: string): ISystemBuilder> 36 | 37 | /** 38 | * Add an executor to the system which is called on every relevant step 39 | * @param fn 40 | */ 41 | withRunFunction(fn: TSystemFunction>): ISystemBuilder> 42 | 43 | /** 44 | * Add a setup function which is called when the system is instantiated (e.g. on a new world run) 45 | * @param fn 46 | */ 47 | withSetupFunction(fn: TSystemFunction>): ISystemBuilder> 48 | } 49 | -------------------------------------------------------------------------------- /src/system/system-builder.ts: -------------------------------------------------------------------------------- 1 | import type {ISystemBuilder} from "./system-builder.spec.ts"; 2 | import type {ISystem, TSystemFunction, TSystemParameterDesc} from "./system.spec.ts"; 3 | import {setRuntimeContext, unsetRuntimeContext} from "./system_context.ts"; 4 | import type {IRuntimeWorld} from "../world/runtime/runtime-world.spec.ts"; 5 | import type {ISystemContext} from "./system_context.spec.ts"; 6 | 7 | export * from "./system-builder.spec.ts"; 8 | 9 | export class SystemBuilder implements ISystemBuilder> { 10 | systemName: string = 'ECS_Unnamed_System'; 11 | parameterDesc: Readonly; 12 | setupFunction: TSystemFunction> = () => {}; 13 | runFunction: TSystemFunction> = () => {}; 14 | 15 | 16 | constructor(params: Readonly) { 17 | this.parameterDesc = params; 18 | } 19 | 20 | build(): Readonly>> { 21 | const self = this; 22 | const System = class implements Readonly>>, ISystemContext { 23 | /** @internal */ 24 | _context = undefined; 25 | /** @internal */ 26 | _handlers = new Map(); 27 | 28 | readonly name = self.systemName; 29 | readonly parameterDesc = self.parameterDesc; 30 | readonly runFunction = self.runFunction; 31 | readonly setupFunction = self.setupFunction; 32 | 33 | get runtimeContext(): IRuntimeWorld | undefined { 34 | return this._context; 35 | } 36 | 37 | /** @internal */ 38 | setRuntimeContext = setRuntimeContext; 39 | /** @internal */ 40 | unsetRuntimeContext = unsetRuntimeContext; 41 | }; 42 | 43 | Object.defineProperty(System, 'name', { 44 | configurable: false, 45 | writable: false, 46 | enumerable: false, 47 | value: this.systemName, 48 | }); 49 | 50 | return new System(); 51 | } 52 | 53 | withName(name: string): SystemBuilder> { 54 | this.systemName = name; 55 | return this; 56 | } 57 | 58 | withRunFunction(fn: TSystemFunction>): SystemBuilder> { 59 | this.runFunction = fn; 60 | return this; 61 | } 62 | 63 | withSetupFunction(fn: TSystemFunction>): SystemBuilder> { 64 | this.setupFunction = fn; 65 | return this; 66 | } 67 | 68 | name = this.withName; 69 | run = this.withRunFunction; 70 | setup = this.withSetupFunction; 71 | } 72 | -------------------------------------------------------------------------------- /src/system/system.spec.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IAccessQuery, 3 | IComponentsQueryDescriptor, 4 | IEntitiesQueryDescriptor 5 | } from "../query/query.spec.ts"; 6 | import type {TObjectProto, TTypeProto} from "../_.spec.ts"; 7 | import type {ISystemActions} from "../world/actions.spec.ts"; 8 | import {systemEventReaderSym, systemEventWriterSym, systemResourceTypeSym, systemRunParamSym} from "./_.ts"; 9 | import type {IEventReader} from "../events/event-reader.spec.ts"; 10 | import type {IEventWriter} from "../events/event-writer.spec.ts"; 11 | import type {IRuntimeWorld} from "../world/runtime/runtime-world.spec.ts"; 12 | 13 | export type TSystemParameter = 14 | IEntitiesQueryDescriptor 15 | | IEventReader 16 | | IComponentsQueryDescriptor> 17 | | ISystemActions 18 | | ISystemResource 19 | | ISystemStorage; 20 | export type TSystemParameterDesc = { [name: string]: TSystemParameter }; 21 | export type TSystemFunction = (params: Readonly) => void | Promise; 22 | 23 | export interface ISystem { 24 | /** @internal */ 25 | [systemRunParamSym]?: Readonly 26 | 27 | readonly name: string 28 | readonly parameterDesc: PDESC 29 | readonly runFunction: TSystemFunction> 30 | readonly runtimeContext: IRuntimeWorld | undefined 31 | readonly setupFunction: TSystemFunction> 32 | 33 | /** 34 | * Set world in which the System will be executed. 35 | * This will be used to register event readers for speedy cache-syncs 36 | * 37 | * @internal 38 | * @param world 39 | */ 40 | setRuntimeContext(world: IRuntimeWorld): void 41 | 42 | /** 43 | * Unset the world in which the system was executed. 44 | * 45 | * @internal 46 | */ 47 | unsetRuntimeContext(world: IRuntimeWorld): void 48 | } 49 | 50 | export interface ISystemResource { 51 | /** @internal */ 52 | [systemResourceTypeSym]: TTypeProto 53 | } 54 | 55 | interface ISystemStorage {} 56 | 57 | export const Actions: ISystemActions = { [Symbol()]: undefined } as unknown as Readonly; 58 | 59 | export function ReadEvents(type: T | ErrorConstructor): Readonly> { 60 | return { 61 | [systemEventReaderSym]: type, 62 | } as unknown as IEventReader; 63 | } 64 | 65 | export function ReadResource(type: TTypeProto): ISystemResource & T { 66 | return { 67 | [systemResourceTypeSym]: type, 68 | } as ISystemResource & Readonly; 69 | } 70 | 71 | export function Storage(initializer: T): ISystemStorage & T { 72 | return initializer as ISystemStorage & T; 73 | } 74 | 75 | export function WriteEvents(type: T): Readonly> { 76 | return { 77 | [systemEventWriterSym]: type, 78 | } as unknown as IEventWriter; 79 | } 80 | 81 | export function WriteResource(type: TTypeProto): ISystemResource & T { 82 | return { 83 | [systemResourceTypeSym]: type, 84 | } as ISystemResource & T; 85 | } 86 | -------------------------------------------------------------------------------- /src/system/system_context.spec.ts: -------------------------------------------------------------------------------- 1 | import type {IRuntimeWorld} from "../world/runtime/runtime-world.spec.ts"; 2 | import type {SimECSEvent} from "../events/internal-events.ts"; 3 | import type {TTypeProto} from "../_.spec.ts"; 4 | import type {TSubscriber} from "../events/_.ts"; 5 | 6 | /** @internal */ 7 | export interface ISystemContext { 8 | /** @internal */ 9 | _context: IRuntimeWorld | undefined 10 | /** @internal */ 11 | _handlers: Map, TSubscriber>> 12 | } 13 | -------------------------------------------------------------------------------- /src/system/system_context.ts: -------------------------------------------------------------------------------- 1 | import type {ISystem} from "./system.spec.ts"; 2 | import { 3 | SimECSReplaceResourceEvent, 4 | } from "../events/internal-events.ts"; 5 | import {RuntimeWorld} from "../world/runtime/runtime-world.ts"; 6 | import type {TObjectProto, TTypeProto} from "../_.spec.ts"; 7 | import type {ISystemContext} from "./system_context.spec.ts"; 8 | import type {TSubscriber} from "../events/_.ts"; 9 | 10 | 11 | export function setRuntimeContext(this: ISystem & ISystemContext, context: RuntimeWorld): void { 12 | { 13 | const handler: TSubscriber>> = handleSimECSReplaceResourceEvent.bind(this); 14 | this._handlers.set(SimECSReplaceResourceEvent, handler); 15 | context.eventBus.subscribe(SimECSReplaceResourceEvent, handler); 16 | } 17 | 18 | this._context = context; 19 | } 20 | 21 | export function unsetRuntimeContext(this: ISystem & ISystemContext, context: RuntimeWorld): void { 22 | let handler; 23 | for (handler of this._handlers) { 24 | context.eventBus.unsubscribe(handler[0], handler[1]); 25 | } 26 | 27 | this._context = undefined; 28 | } 29 | 30 | 31 | function handleSimECSReplaceResourceEvent(this: ISystem, event: Readonly>) { 32 | Object.entries(this.parameterDesc) 33 | .filter(param => param[1] instanceof event.resourceType) 34 | .forEach(param => this.parameterDesc[param[0]] = event.resourceInstance); 35 | } 36 | -------------------------------------------------------------------------------- /src/test-data/components.ts: -------------------------------------------------------------------------------- 1 | class TestBase { a = 0 } 2 | export class C1 extends TestBase {} 3 | -------------------------------------------------------------------------------- /src/test-data/systems.ts: -------------------------------------------------------------------------------- 1 | import {Actions, createSystem} from "../system/system.ts"; 2 | import {C1} from "./components.ts"; 3 | import {Write} from "../query/query.ts"; 4 | import {queryComponents} from "../ecs/ecs-query.ts"; 5 | import type {ISystemActions} from "../world/actions.spec.ts"; 6 | 7 | export const S1 = (handler?: (c1:C1)=>void) => createSystem({ 8 | query: queryComponents({ c1: Write(C1) }) 9 | }).withRunFunction(({query}) => { 10 | return query.execute(({c1}) => handler?.(c1)); 11 | }).build(); 12 | 13 | export const S2 = (handler?: (actions: ISystemActions)=>void) => createSystem({ actions: Actions }) 14 | .withRunFunction(({actions}) => handler?.(actions)) 15 | .build(); 16 | -------------------------------------------------------------------------------- /src/util/instance-map.ts: -------------------------------------------------------------------------------- 1 | import type {TObjectProto} from "../_.spec.ts"; 2 | 3 | export class InstanceMap extends Map>> {} 4 | export interface ReadonlyInstanceMap extends ReadonlyMap> {} 5 | -------------------------------------------------------------------------------- /src/world/actions.spec.ts: -------------------------------------------------------------------------------- 1 | import type {IIStateProto, IState} from "../state/state.spec.ts"; 2 | import type {ICommands} from "./runtime/commands/commands.spec.ts"; 3 | import type {TTypeProto} from "../_.spec.ts"; 4 | import type {IEntitiesQuery} from "../query/query.spec.ts"; 5 | import type {ISerDeOptions, TSerializer} from "../serde/serde.spec.ts"; 6 | import type {ISerialFormat} from "../serde/serial-format.spec.ts"; 7 | import type {IEventBus} from "../events/event-bus.spec.ts"; 8 | import type {IReadOnlyEntity} from "../entity/entity.spec.ts"; 9 | 10 | export interface ITransitionActions extends ISystemActions { 11 | eventBus: Readonly 12 | 13 | flushCommands(): Promise 14 | popState(): Promise 15 | pushState(NewState: Readonly): Promise 16 | save(options?: Readonly>): ISerialFormat 17 | } 18 | 19 | export interface ISystemActions { 20 | commands: Readonly 21 | currentState: Readonly | undefined 22 | 23 | getEntities(query?: Readonly): IterableIterator 24 | getResource(type: TTypeProto): T 25 | hasResource(type: Readonly | TTypeProto): boolean 26 | } 27 | -------------------------------------------------------------------------------- /src/world/common/world_entities.ts: -------------------------------------------------------------------------------- 1 | import {type PreptimeWorld} from "../preptime/preptime-world.ts"; 2 | import type {IEntity} from "../../entity/entity.spec.ts"; 3 | import type {IEntityBuilder} from "../../entity/entity-builder.spec.ts"; 4 | import type {IEntitiesQuery} from "../../query/query.spec.ts"; 5 | import {EntityBuilder} from "../../entity/entity-builder.ts"; 6 | import {Entity} from "../../entity/entity.ts"; 7 | import {type RuntimeWorld} from "../runtime/runtime-world.ts"; 8 | import type {IMutableWorld} from "../world.spec.ts"; 9 | 10 | 11 | export function buildEntity(this: IMutableWorld, uuid?: string): IEntityBuilder { 12 | const self = this; 13 | return new EntityBuilder(uuid, entity => self.addEntity(entity)); 14 | } 15 | 16 | export function clearEntities(this: IMutableWorld & (PreptimeWorld | RuntimeWorld)): void { 17 | let entity; 18 | for (entity of this.data.entities) { 19 | this.removeEntity(entity); 20 | } 21 | 22 | this.clearGroups(); 23 | } 24 | 25 | export function createEntity(this: IMutableWorld): IEntity { 26 | const entity = new Entity(); 27 | this.addEntity(entity); 28 | return entity; 29 | } 30 | 31 | export function getEntities(this: PreptimeWorld | RuntimeWorld, query?: Readonly): IterableIterator { 32 | if (!query) { 33 | return this.data.entities.values(); 34 | } 35 | 36 | return Array.from(this.data.entities.values()) 37 | .filter(entity => query.matchesEntity(entity)) 38 | .values(); 39 | } 40 | -------------------------------------------------------------------------------- /src/world/common/world_groups.ts: -------------------------------------------------------------------------------- 1 | import type {TGroupHandle} from "../world.spec.ts"; 2 | import type {IEntity} from "../../entity/entity.spec.ts"; 3 | import type {IPreptimeWorld} from "../preptime/preptime-world.spec.ts"; 4 | import {type PreptimeWorld} from "../preptime/preptime-world.ts"; 5 | import {type RuntimeWorld} from "../runtime/runtime-world.ts"; 6 | import type {IMutableWorld} from "../world.spec.ts"; 7 | 8 | 9 | export function addEntityToGroup(this: PreptimeWorld | RuntimeWorld, groupHandle: TGroupHandle, entity: Readonly): void { 10 | this.addEntitiesToGroup(groupHandle, [entity]); 11 | } 12 | 13 | export function addEntitiesToGroup( 14 | this: PreptimeWorld | RuntimeWorld, 15 | groupHandle: TGroupHandle, 16 | entities: ReadonlyArray> | IterableIterator>, 17 | ): void { 18 | const link = getLink(this, groupHandle); 19 | let entity; 20 | for (entity of entities) { 21 | link.add(entity); 22 | } 23 | } 24 | 25 | export function assimilateGroup( 26 | this: PreptimeWorld | RuntimeWorld, 27 | otherWorld: Readonly, 28 | handle: TGroupHandle, 29 | ): TGroupHandle { 30 | const entities = otherWorld.getGroupEntities(handle); 31 | const newGroup = this.createGroup(); 32 | 33 | otherWorld.removeGroup(handle); 34 | this.addEntitiesToGroup(newGroup, entities); 35 | 36 | return newGroup; 37 | } 38 | 39 | export function clearGroups(this: PreptimeWorld | RuntimeWorld): void { 40 | this.data.groups.entityLinks.clear(); 41 | this.data.groups.nextHandle = 0; 42 | } 43 | 44 | export function createGroup(this: PreptimeWorld | RuntimeWorld): TGroupHandle { 45 | const handle = this.data.groups.nextHandle++; 46 | this.data.groups.entityLinks.set(handle, new Set()); 47 | return handle; 48 | } 49 | 50 | export function getGroupEntities( 51 | this: PreptimeWorld | RuntimeWorld, 52 | groupHandle: TGroupHandle, 53 | ): IterableIterator { 54 | return getLink(this, groupHandle).keys(); 55 | } 56 | 57 | function getLink(world: PreptimeWorld | RuntimeWorld, groupHandle: TGroupHandle): Set { 58 | const link = world.data.groups.entityLinks.get(groupHandle); 59 | 60 | if (!link) { 61 | throw new Error(`The group "${groupHandle}" does not exist in the world "${world.name}"`); 62 | } 63 | 64 | return link; 65 | } 66 | 67 | export function removeGroup(this: IMutableWorld & (PreptimeWorld | RuntimeWorld), groupHandle: TGroupHandle): void { 68 | const link = getLink(this, groupHandle); 69 | 70 | let entity; 71 | for (entity of link) { 72 | this.removeEntity(entity); 73 | } 74 | 75 | this.data.groups.entityLinks.delete(groupHandle); 76 | } 77 | -------------------------------------------------------------------------------- /src/world/common/world_misc.ts: -------------------------------------------------------------------------------- 1 | import type {TGroupHandle} from "../world.spec.ts"; 2 | import type {IEntity} from "../../entity/entity.spec.ts"; 3 | import type {IPreptimeWorld} from "../preptime/preptime-world.spec.ts"; 4 | import {type PreptimeWorld} from "../preptime/preptime-world.ts"; 5 | import {type RuntimeWorld} from "../runtime/runtime-world.ts"; 6 | import type {IMutableWorld} from "../world.spec.ts"; 7 | 8 | export function merge( 9 | this: IMutableWorld & (PreptimeWorld | RuntimeWorld), 10 | elsewhere: Readonly, 11 | intoGroup?: TGroupHandle, 12 | ): [TGroupHandle, Array] { 13 | const groupHandle = intoGroup ?? this.data.groups.nextHandle++; 14 | const entities = []; 15 | let entity; 16 | 17 | for (entity of elsewhere.getEntities()) { 18 | elsewhere.removeEntity(entity); 19 | this.addEntity(entity); 20 | entities.push(entity); 21 | } 22 | 23 | return [groupHandle, entities]; 24 | } 25 | -------------------------------------------------------------------------------- /src/world/common/world_resources.ts: -------------------------------------------------------------------------------- 1 | import type {TObjectProto, TTypeProto} from "../../_.spec.ts"; 2 | import {type PreptimeWorld} from "../preptime/preptime-world.ts"; 3 | import {type RuntimeWorld} from "../runtime/runtime-world.ts"; 4 | import type {TExistenceQuery} from "../../query/query.spec.ts"; 5 | 6 | 7 | export function clearResources(this: PreptimeWorld | RuntimeWorld): void { 8 | this.data.resources.clear(); 9 | } 10 | 11 | export function getResource(this: RuntimeWorld, type: TTypeProto): T { 12 | if (!this.data.resources.has(type)) { 13 | throw new Error(`Resource of type "${type.name}" does not exist!`); 14 | } 15 | 16 | return this.data.resources.get(type) as T; 17 | } 18 | 19 | export function *getResources( 20 | this: PreptimeWorld | RuntimeWorld, 21 | types?: Readonly>, 22 | ): IterableIterator { 23 | if (!types) { 24 | return this.data.resources.values(); 25 | } 26 | 27 | const typesArray = Array.isArray(types) 28 | ? types 29 | : Array.from(types); 30 | 31 | { 32 | let resource; 33 | let type; 34 | 35 | for ([type, resource] of this.data.resources.entries()) { 36 | if (typesArray.includes(type as TObjectProto)) { 37 | yield resource; 38 | } 39 | } 40 | } 41 | } 42 | 43 | export function hasResource(this: PreptimeWorld | RuntimeWorld, obj: Readonly | TTypeProto): boolean { 44 | let type: TTypeProto; 45 | 46 | if (typeof obj === 'object') { 47 | type = obj.constructor as TTypeProto; 48 | } else { 49 | type = obj; 50 | } 51 | 52 | return this.data.resources.has(type); 53 | } 54 | -------------------------------------------------------------------------------- /src/world/error.spec.ts: -------------------------------------------------------------------------------- 1 | import type {TTypeProto} from "../_.spec.ts"; 2 | import type {ISystem} from "../system/system.spec.ts"; 3 | 4 | export interface ISystemError { 5 | readonly cause: Readonly 6 | readonly System: Readonly> 7 | } 8 | -------------------------------------------------------------------------------- /src/world/error.ts: -------------------------------------------------------------------------------- 1 | import type {ISystemError} from "./error.spec.ts"; 2 | import type {TTypeProto} from "../_.spec.ts"; 3 | import type {ISystem} from "../system/system.spec.ts"; 4 | 5 | export class SystemError implements ISystemError { 6 | constructor( 7 | protected _cause: Readonly, 8 | protected _System: Readonly> 9 | ) {} 10 | 11 | get cause(): Error { 12 | return this._cause; 13 | } 14 | 15 | get System(): Readonly> { 16 | return this._System; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/world/events.ts: -------------------------------------------------------------------------------- 1 | import type {IEntity} from "../entity/entity.spec.ts"; 2 | 3 | export class EntityAdded { 4 | constructor( 5 | public entity: Readonly 6 | ) {} 7 | } 8 | 9 | export class EntityRemoved { 10 | constructor( 11 | public entity: Readonly 12 | ) {} 13 | } 14 | -------------------------------------------------------------------------------- /src/world/preptime/preptime-world.spec.ts: -------------------------------------------------------------------------------- 1 | import type {IWorld} from "../world.spec.ts"; 2 | import type {IRuntimeWorld, TExecutionFunction} from "../runtime/runtime-world.spec.ts"; 3 | import type {IScheduler} from "../../scheduler/scheduler.spec.ts"; 4 | import type {ISerDe} from "../../serde/serde.spec.ts"; 5 | import type {IIStateProto} from "../../state/state.spec.ts"; 6 | import type {TObjectProto} from "../../_.spec.ts"; 7 | import type {IEntity} from "../../entity/entity.spec.ts"; 8 | import type {TGroupHandle} from "../world.spec.ts"; 9 | 10 | 11 | export interface IPreptimeData { 12 | entities: Set 13 | groups: { 14 | nextHandle: TGroupHandle, 15 | entityLinks: Map>, 16 | } 17 | resources: Map> 18 | } 19 | 20 | export interface IPreptimeOptions { 21 | executionFunction: TExecutionFunction 22 | initialState: IIStateProto 23 | } 24 | 25 | export interface IPreptimeWorldConfig { 26 | defaultScheduler: Readonly 27 | serde: Readonly 28 | stateSchedulers: Map> 29 | } 30 | 31 | export interface IPreptimeWorld extends IWorld { 32 | /** 33 | * Configuration of how a runtime should work 34 | */ 35 | config: IPreptimeWorldConfig 36 | /** 37 | * Initial data to operate on 38 | */ 39 | data: IPreptimeData 40 | /** 41 | * World's name 42 | */ 43 | readonly name?: string 44 | 45 | /** 46 | * Get a list of RuntimeWorlds which was generated from this PreptimeWorld 47 | */ 48 | getExistingRuntimeWorlds(): ReadonlyArray 49 | 50 | /** 51 | * Prepare a runtime environment from this world 52 | */ 53 | prepareRun(options?: Readonly>): Promise 54 | } 55 | -------------------------------------------------------------------------------- /src/world/preptime/preptime-world_entities.ts: -------------------------------------------------------------------------------- 1 | import {type PreptimeWorld} from "./preptime-world.ts"; 2 | import type {IEntity} from "../../entity/entity.spec.ts"; 3 | 4 | export function addEntity(this: PreptimeWorld, entity: Readonly): void { 5 | this.data.entities.add(entity); 6 | } 7 | 8 | export function hasEntity(this: PreptimeWorld, entity: Readonly): boolean { 9 | return this.data.entities.has(entity); 10 | } 11 | 12 | export function removeEntity(this: PreptimeWorld, entity: Readonly): void { 13 | this.data.entities.delete(entity); 14 | } -------------------------------------------------------------------------------- /src/world/preptime/preptime-world_prefabs.ts: -------------------------------------------------------------------------------- 1 | import type {ISerDeOptions, TSerializer} from "../../serde/serde.spec.ts"; 2 | import type {ISerialFormat} from "../../serde/serial-format.spec.ts"; 3 | import {type PreptimeWorld} from "./preptime-world.ts"; 4 | import type {TGroupHandle} from "../world.spec.ts"; 5 | import type {TDeserializer} from "../../serde/serde.spec.ts"; 6 | import type {IEntity} from "../../entity/entity.spec.ts"; 7 | import type {TObjectProto} from "../../_.spec.ts"; 8 | 9 | export function load( 10 | this: PreptimeWorld, 11 | prefab: Readonly, 12 | options?: Readonly>, 13 | intoGroup?: TGroupHandle, 14 | ): TGroupHandle { 15 | let groupHandle = intoGroup; 16 | if (groupHandle == undefined || !this.data.groups.entityLinks.has(groupHandle)) { 17 | groupHandle = this.createGroup(); 18 | } 19 | 20 | const serdeOut = this.config.serde.deserialize(prefab, options); 21 | 22 | { 23 | const entities = this.data.groups.entityLinks.get(groupHandle)!; 24 | let entity: IEntity; 25 | 26 | for (entity of serdeOut.entities) { 27 | this.addEntity(entity); 28 | entities.add(entity); 29 | } 30 | } 31 | 32 | { 33 | let resource: object | TObjectProto; 34 | for (resource of Object.values(serdeOut.resources)) { 35 | // @ts-ignore should work 36 | this.addResource(resource); 37 | } 38 | } 39 | 40 | return groupHandle; 41 | } 42 | 43 | export function save(this: PreptimeWorld, options?: Readonly>): ISerialFormat { 44 | const resources = Object.fromEntries(options?.resources?.map(type => [type.constructor.name]) ?? []); 45 | return this.config.serde.serialize({ 46 | entities: this.getEntities(options?.entities), 47 | resources, 48 | }, options); 49 | } 50 | -------------------------------------------------------------------------------- /src/world/preptime/preptime-world_resources.ts: -------------------------------------------------------------------------------- 1 | import type {TTypeProto} from "../../_.spec.ts"; 2 | import {type PreptimeWorld} from "./preptime-world.ts"; 3 | 4 | export function addResource( 5 | this: PreptimeWorld, 6 | Type: T | TTypeProto, 7 | ...args: ReadonlyArray 8 | ): T | TTypeProto { 9 | this.data.resources.set(Type, args); 10 | return Type; 11 | } 12 | 13 | export function removeResource(this: PreptimeWorld, type: TTypeProto): void { 14 | if (!this.data.resources.has(type)) { 15 | throw new Error(`Resource with name "${type.name}" does not exists!`); 16 | } 17 | 18 | this.data.resources.delete(type); 19 | } 20 | -------------------------------------------------------------------------------- /src/world/runtime/commands/command-entity-builder.spec.ts: -------------------------------------------------------------------------------- 1 | import type {TObjectProto} from "../../../_.spec.ts"; 2 | 3 | export interface ICommandEntityBuilder { 4 | /** 5 | * Create entity and add it to the world as a command 6 | */ 7 | build(): void 8 | 9 | /** 10 | * Add component to target entity 11 | * @param component 12 | * @param args 13 | */ 14 | with(component: object | TObjectProto, ...args: ReadonlyArray): ICommandEntityBuilder 15 | 16 | /** 17 | * Add all components to target entity 18 | * @param component 19 | */ 20 | withAll(...component: ReadonlyArray): ICommandEntityBuilder 21 | } 22 | -------------------------------------------------------------------------------- /src/world/runtime/commands/command-entity-builder.ts: -------------------------------------------------------------------------------- 1 | import {Entity} from "../../../entity/entity.ts"; 2 | import type {ICommandEntityBuilder} from "./command-entity-builder.spec.ts"; 3 | import type {TObjectProto} from "../../../_.spec.ts"; 4 | import type {IWorld} from "../../world.spec.ts"; 5 | import type {ICommands} from "./commands.spec.ts"; 6 | 7 | export * from './command-entity-builder.spec.ts'; 8 | 9 | export class CommandEntityBuilder implements ICommandEntityBuilder { 10 | protected entity: Entity = new Entity(); 11 | 12 | constructor( 13 | protected world: Readonly, 14 | protected commands: Readonly, 15 | ) {} 16 | 17 | build(): void { 18 | this.commands.addEntity(this.entity); 19 | } 20 | 21 | with(component: object | TObjectProto, ...args: ReadonlyArray): CommandEntityBuilder { 22 | this.entity.addComponent(component, ...args); 23 | return this; 24 | } 25 | 26 | withAll(...components: ReadonlyArray): CommandEntityBuilder { 27 | let component; 28 | 29 | for (component of components) { 30 | this.with(component); 31 | } 32 | 33 | return this; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/world/runtime/commands/commands-aggregator.spec.ts: -------------------------------------------------------------------------------- 1 | export type TCommand = () => Promise | void; 2 | 3 | export interface ICommandsAggregator { 4 | /** 5 | * Add a command to be executed later 6 | * @param command 7 | */ 8 | addCommand(command: TCommand): void 9 | 10 | /** 11 | * Execute all commands which have been aggregated 12 | */ 13 | executeAll(): Promise 14 | } 15 | -------------------------------------------------------------------------------- /src/world/runtime/commands/commands-aggregator.ts: -------------------------------------------------------------------------------- 1 | import type {ICommandsAggregator, TCommand} from "./commands-aggregator.spec.ts"; 2 | 3 | 4 | export * from "./commands-aggregator.spec.ts"; 5 | 6 | export class CommandsAggregator implements ICommandsAggregator { 7 | commands: Array = []; 8 | 9 | addCommand(command: TCommand): void { 10 | this.commands.push(command); 11 | } 12 | 13 | async executeAll(): Promise { 14 | const length = this.commands.length; 15 | 16 | for (let i = 0; i < length; i++) { 17 | await this.commands[i](); 18 | } 19 | 20 | this.commands.length = 0; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/world/runtime/commands/commands.spec.ts: -------------------------------------------------------------------------------- 1 | import type {IEntity} from "../../../entity/entity.spec.ts"; 2 | import type {TTypeProto} from "../../../_.spec.ts"; 3 | import type {TDeserializer, ISerDeOptions} from "../../../serde/serde.spec.ts"; 4 | import type {ISerialFormat} from "../../../serde/serial-format.spec.ts"; 5 | import type {IWorld, TGroupHandle} from "../../world.spec.ts"; 6 | import type {IIStateProto} from "../../../state/state.spec.ts"; 7 | import type {ICommandEntityBuilder} from "./command-entity-builder.spec.ts"; 8 | import type {IReadOnlyEntity} from "../../../entity/entity.spec.ts"; 9 | 10 | 11 | export type TCommand = () => Promise | void; 12 | 13 | /** 14 | * Commands is an async interface, which aggregates commands for later execution. 15 | * The primary usage is to issue commands during system runtime and have them take effect on a common sync point, 16 | * like after all systems ran 17 | */ 18 | export interface ICommands { 19 | /** 20 | * Add an entity to the world 21 | * @param entity 22 | */ 23 | addEntity(entity: Readonly): void 24 | 25 | /** 26 | * Add a resource to this world and returns the resource instance 27 | * @param type 28 | * @param args constructor parameters 29 | */ 30 | addResource(type: T | TTypeProto, ...args: ReadonlyArray): T 31 | 32 | /** 33 | * Build an entity and add it to this world using an entity builder 34 | */ 35 | buildEntity(): ICommandEntityBuilder 36 | 37 | /** 38 | * Remove all entities from this world 39 | */ 40 | clearEntities(): void 41 | 42 | /** 43 | * Load entities with components from a prefab or save 44 | * @param prefab 45 | * @param options 46 | */ 47 | load(prefab: Readonly, options?: Readonly>): TGroupHandle 48 | 49 | /** 50 | * Merge entities from another world into this one 51 | * @param world 52 | */ 53 | merge(world: Readonly): TGroupHandle 54 | 55 | /** 56 | * Provides an environment to securely change an entity's data 57 | * @param entity 58 | * @param mutator 59 | */ 60 | mutateEntity(entity: Readonly, mutator: (entity: IEntity) => Promise | void): void 61 | 62 | /** 63 | * Revert the running world to a previous state 64 | */ 65 | popState(): void 66 | 67 | /** 68 | * Change the running world to a new state 69 | * @param NewState 70 | */ 71 | pushState(NewState: IIStateProto): void 72 | 73 | /** 74 | * Queue custom command for execution 75 | * @param command 76 | */ 77 | queueCommand(command: TCommand): void 78 | 79 | /** 80 | * Remove an entity from the world, deleting all of its components 81 | * @param entity 82 | */ 83 | removeEntity(entity: Readonly): void 84 | 85 | /** 86 | * Remove a group and all entities inside from this world 87 | * @param handle 88 | */ 89 | removeGroup(handle: TGroupHandle): void 90 | 91 | /** 92 | * Remove a resource from the world 93 | * @param type 94 | */ 95 | removeResource(type: TTypeProto): void 96 | 97 | /** 98 | * Replace a resource from this world 99 | * @param type 100 | * @param args constructor parameters 101 | */ 102 | replaceResource(type: T | TTypeProto, ...args: unknown[]): void 103 | 104 | /** 105 | * Signal the world to stop its dispatch-loop 106 | */ 107 | stopRun(): void 108 | } 109 | -------------------------------------------------------------------------------- /src/world/runtime/runtime-world_entities.ts: -------------------------------------------------------------------------------- 1 | import {type RuntimeWorld} from "./runtime-world.ts"; 2 | import type {IEntity, IEventMap} from "../../entity/entity.spec.ts"; 3 | import {addEntitySym, removeEntitySym} from "../../query/_.ts"; 4 | import { 5 | SimECSAddEntityEvent, 6 | SimECSCloneEntityEvent, 7 | SimECSEntityAddComponentEvent, 8 | SimECSEntityAddTagEvent, 9 | SimECSEntityRemoveComponentEvent, 10 | SimECSEntityRemoveTagEvent, 11 | SimECSRemoveEntityEvent, 12 | } from "../../events/internal-events.ts"; 13 | 14 | 15 | export function addEntity(this: RuntimeWorld, entity: Readonly): void { 16 | this.data.entities.add(entity); 17 | 18 | { // wire up entity events 19 | const eventMap: IEventMap = { 20 | addComponent: event => this.eventBus.publish( 21 | new SimECSEntityAddComponentEvent(entity, event.componentType, event.componentInstance) 22 | ), 23 | addTag: event => this.eventBus.publish( 24 | new SimECSEntityAddTagEvent(entity, event.tag) 25 | ), 26 | clone: event => this.eventBus.publish( 27 | new SimECSCloneEntityEvent(entity, event.clone) 28 | ), 29 | removeComponent: event => this.eventBus.publish( 30 | new SimECSEntityRemoveComponentEvent(entity, event.componentType, event.componentInstance) 31 | ), 32 | removeTag: event => this.eventBus.publish( 33 | new SimECSEntityRemoveTagEvent(entity, event.tag) 34 | ), 35 | }; 36 | 37 | entity.addEventListener("addComponent", eventMap.addComponent); 38 | entity.addEventListener("addTag", eventMap.addTag); 39 | entity.addEventListener("clone", eventMap.clone); 40 | entity.addEventListener("removeComponent", eventMap.removeComponent); 41 | entity.addEventListener("removeTag", eventMap.removeTag); 42 | 43 | this.entityEventHandlers.set(entity, eventMap); 44 | } 45 | 46 | { // add entity to queries 47 | let query; 48 | for (query of this.queries) { 49 | query[addEntitySym](entity); 50 | } 51 | } 52 | 53 | // todo: await in 0.7.0 54 | this.eventBus.publish(new SimECSAddEntityEvent(entity)); 55 | } 56 | 57 | export function hasEntity(this: RuntimeWorld, entity: Readonly): boolean { 58 | return this.data.entities.has(entity); 59 | } 60 | 61 | export function refreshEntityQueryRegistration(this: RuntimeWorld, entity: Readonly): void { 62 | let query; 63 | for (query of this.queries) { 64 | query[removeEntitySym](entity); 65 | query[addEntitySym](entity); 66 | } 67 | } 68 | 69 | export function removeEntity(this: RuntimeWorld, entity: Readonly): void { 70 | if (!this.data.entities.has(entity)) { 71 | return; 72 | } 73 | 74 | this.data.entities.delete(entity); 75 | 76 | { // Remove entity from all queries 77 | let query; 78 | for (query of this.queries) { 79 | query[removeEntitySym](entity); 80 | } 81 | } 82 | 83 | { // unregister entity events 84 | const eventMap = this.entityEventHandlers.get(entity)!; 85 | 86 | entity.removeEventListener("addComponent", eventMap.addComponent); 87 | entity.removeEventListener("addTag", eventMap.addTag); 88 | entity.removeEventListener("clone", eventMap.clone); 89 | entity.removeEventListener("removeComponent", eventMap.removeComponent); 90 | entity.removeEventListener("removeTag", eventMap.removeTag); 91 | 92 | this.entityEventHandlers.delete(entity); 93 | } 94 | 95 | // todo: await in 0.7.0 96 | this.eventBus.publish(new SimECSRemoveEntityEvent(entity)); 97 | } -------------------------------------------------------------------------------- /src/world/runtime/runtime-world_prefabs.ts: -------------------------------------------------------------------------------- 1 | import type {ISerDeOptions, TSerializer} from "../../serde/serde.spec.ts"; 2 | import type {ISerialFormat} from "../../serde/serial-format.spec.ts"; 3 | import {type RuntimeWorld} from "./runtime-world.ts"; 4 | import type {TGroupHandle} from "../world.spec.ts"; 5 | import type {TDeserializer} from "../../serde/serde.spec.ts"; 6 | import type {IEntity} from "../../entity/entity.spec.ts"; 7 | import type {TObjectProto} from "../../_.spec.ts"; 8 | 9 | 10 | export function load( 11 | this: RuntimeWorld, 12 | prefab: Readonly, 13 | options?: Readonly>, 14 | intoGroup?: TGroupHandle, 15 | ): TGroupHandle { 16 | let groupHandle = intoGroup; 17 | if (groupHandle == undefined || !this.data.groups.entityLinks.has(groupHandle)) { 18 | groupHandle = this.createGroup(); 19 | } 20 | 21 | const serdeOut = this.config.serde.deserialize(prefab, options); 22 | 23 | { 24 | const entities = this.data.groups.entityLinks.get(groupHandle)!; 25 | let entity: IEntity; 26 | 27 | for (entity of serdeOut.entities) { 28 | this.addEntity(entity); 29 | entities.add(entity); 30 | } 31 | } 32 | 33 | { 34 | let resource: object | TObjectProto; 35 | for (resource of Object.values(serdeOut.resources)) { 36 | if (this.hasResource(resource)) { 37 | this.replaceResource(resource); 38 | } else { 39 | // @ts-ignore should work 40 | this.addResource(resource); 41 | } 42 | } 43 | } 44 | 45 | return groupHandle; 46 | } 47 | 48 | export function save(this: RuntimeWorld, options?: Readonly>): ISerialFormat { 49 | const resources = Object.fromEntries( 50 | options?.resources 51 | ?.map(type => [type.constructor.name, (this as RuntimeWorld).getResource(type)!]) ?? [] 52 | ); 53 | return this.config.serde.serialize({ 54 | entities: this.getEntities(options?.entities), 55 | resources, 56 | }, options); 57 | } 58 | -------------------------------------------------------------------------------- /src/world/runtime/runtime-world_resources.ts: -------------------------------------------------------------------------------- 1 | import {type RuntimeWorld} from "./runtime-world.ts"; 2 | import type {TTypeProto} from "../../_.spec.ts"; 3 | import { 4 | SimECSAddResourceEvent, 5 | SimECSRemoveResourceEvent, 6 | SimECSReplaceResourceEvent, 7 | } from "../../events/internal-events.ts"; 8 | 9 | 10 | export function addResource( 11 | this: RuntimeWorld, 12 | obj: Readonly | TTypeProto, 13 | ...args: ReadonlyArray 14 | ): T { 15 | let type: TTypeProto; 16 | let instance: T; 17 | 18 | if (typeof obj === 'object') { 19 | type = obj.constructor as TTypeProto; 20 | instance = obj; 21 | } else { 22 | type = obj; 23 | try { 24 | instance = new (obj.prototype.constructor.bind(obj, ...args))(); 25 | } catch (err: any) { 26 | if (err instanceof TypeError && err.message.startsWith('Illegal constructor')) { 27 | // @ts-ignore This may happen for some built-in constructors. They must be replaced later! 28 | instance = null; 29 | } else { 30 | throw err; 31 | } 32 | } 33 | } 34 | 35 | if (this.data.resources.has(type)) { 36 | throw new Error(`Resource with name "${type.name}" already exists!`); 37 | } 38 | 39 | this.data.resources.set(type, instance); 40 | // todo: await in 0.7.0 41 | this.eventBus.publish(new SimECSAddResourceEvent(type, instance)); 42 | 43 | return instance; 44 | } 45 | 46 | export function removeResource(this: RuntimeWorld, type: TTypeProto): void { 47 | if (!this.data.resources.has(type)) { 48 | throw new Error(`Resource with name "${type.name}" does not exists!`); 49 | } 50 | 51 | const instance = this.data.resources.get(type)!; 52 | this.data.resources.delete(type); 53 | // todo: await in 0.7.0 54 | this.eventBus.publish(new SimECSRemoveResourceEvent(type, instance as T)); 55 | } 56 | 57 | export async function replaceResource( 58 | this: RuntimeWorld, 59 | obj: Readonly | TTypeProto, 60 | ...args: ReadonlyArray 61 | ): Promise { 62 | let type: TTypeProto; 63 | 64 | if (typeof obj === 'object') { 65 | type = obj.constructor as TTypeProto; 66 | } else { 67 | type = obj; 68 | } 69 | 70 | if (!this.data.resources.has(type)) { 71 | throw new Error(`Resource with name "${type.name}" does not exists!`); 72 | } 73 | 74 | this.data.resources.delete(type); 75 | const resourceObj = this.addResource(obj, ...args); 76 | 77 | await this.eventBus.publish(new SimECSReplaceResourceEvent( 78 | type, 79 | resourceObj, 80 | )); 81 | } 82 | -------------------------------------------------------------------------------- /src/world/runtime/runtime-world_states.ts: -------------------------------------------------------------------------------- 1 | import {type RuntimeWorld} from "./runtime-world.ts"; 2 | import type {IIStateProto} from "../../state/state.spec.ts"; 3 | import {State} from "../../state/state.ts"; 4 | import type {IScheduler} from "../../scheduler/scheduler.spec.ts"; 5 | import {EventReader} from "../../events/event-reader.ts"; 6 | import {EventBus} from "../../events/event-bus.ts"; 7 | 8 | export async function popState(this: RuntimeWorld): Promise { 9 | unsubscribeEventsOfSchedulerSystems(this.eventBus, this.currentScheduler!); 10 | await (await this.pda.pop())?.deactivate(this.transitionWorld); 11 | 12 | const newState = this.pda.state; 13 | if (!newState) { 14 | return; 15 | } 16 | 17 | await newState.activate(this.transitionWorld); 18 | this.currentScheduler = this.config.stateSchedulers.get(newState.constructor as IIStateProto) ?? this.config.defaultScheduler; 19 | this.currentSchedulerExecutor = this.currentScheduler.getExecutor(this.eventBus); 20 | subscribeEventsOfSchedulerSystems(this.eventBus, this.currentScheduler); 21 | } 22 | 23 | export async function pushState(this: RuntimeWorld, NewState: IIStateProto): Promise { 24 | if (this.currentScheduler) { 25 | unsubscribeEventsOfSchedulerSystems(this.eventBus, this.currentScheduler); 26 | } 27 | 28 | await this.pda.state?.deactivate(this.transitionWorld); 29 | await this.pda.push(NewState); 30 | 31 | const newState = this.pda.state! as State; 32 | await newState.create(this.transitionWorld); 33 | await newState.activate(this.transitionWorld); 34 | await this.commands.executeAll(); 35 | this.currentScheduler = this.config.stateSchedulers.get(NewState) ?? this.config.defaultScheduler; 36 | 37 | if (!this.currentScheduler) { 38 | throw new Error(`There is no DefaultScheduler or Scheduler for ${NewState.name}!`); 39 | } 40 | 41 | this.currentSchedulerExecutor = this.currentScheduler.getExecutor(this.eventBus); 42 | subscribeEventsOfSchedulerSystems(this.eventBus, this.currentScheduler); 43 | } 44 | 45 | export function subscribeEventsOfSchedulerSystems(eventBus: EventBus, scheduler: IScheduler): void { 46 | const systems = scheduler.pipeline.getGroups().map(g => g.stages).flat().map(s => s.systems).flat(); 47 | let system; 48 | let systemParam; 49 | 50 | for (system of systems) { 51 | for (systemParam of Object.values(system.parameterDesc)) { 52 | if (systemParam instanceof EventReader) { 53 | eventBus.subscribeReader(systemParam); 54 | } 55 | } 56 | } 57 | } 58 | 59 | export function unsubscribeEventsOfSchedulerSystems(eventBus: EventBus, scheduler: IScheduler): void { 60 | const systems = scheduler.pipeline.getGroups().map(g => g.stages).flat().map(s => s.systems).flat(); 61 | let system; 62 | let systemParam; 63 | 64 | for (system of systems) { 65 | for (systemParam of Object.values(system.parameterDesc)) { 66 | if (systemParam instanceof EventReader) { 67 | eventBus.unsubscribeReader(systemParam); 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/world/world-builder.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {dataStructDeserializer, dataStructSerializer} from "./world-builder.util"; 3 | import {WorldBuilder} from "./world-builder"; 4 | import {SerDe} from "../serde/serde"; 5 | import {Entity, IEntity} from "../entity/entity"; 6 | import {SerialFormat} from "../serde/serial-format"; 7 | 8 | describe('Test WorldBuilder', () => { 9 | const worldName = 'world1' as const; 10 | 11 | 12 | it('Default De-/Serializer', () => { 13 | const Component = class { 14 | a = 45 15 | b = 'foo' 16 | c = { d: 17 } 17 | }; 18 | 19 | const component = new Component(); 20 | 21 | component.a = 17; 22 | 23 | const jsonObj = JSON.stringify(component); 24 | const serializedObj = dataStructSerializer(component); 25 | const deserializedObj = dataStructDeserializer(Component, serializedObj).data; 26 | 27 | expect(JSON.stringify(serializedObj)).eq(jsonObj); 28 | expect(deserializedObj).deep.eq(component); 29 | expect(deserializedObj instanceof Component).eq(true); 30 | }); 31 | 32 | it('Default De-/Serialize non-empty object component with methods', () => { 33 | const entity1 = new Entity(); 34 | 35 | class AComponent { 36 | constructor( 37 | private _foo = 1, 38 | public bar = 'baz', 39 | ) {} 40 | 41 | get foo() { 42 | return this._foo / 100; 43 | } 44 | 45 | set foo(val: number) { 46 | this._foo = val * 100; 47 | } 48 | 49 | public getInternalFoo() { 50 | return this._foo; 51 | } 52 | } 53 | 54 | entity1.addComponent(new AComponent()); 55 | 56 | { 57 | const serde = new SerDe(); 58 | const serdeOptions = { 59 | useDefaultHandler: false, 60 | useRegisteredHandlers: true, 61 | }; 62 | 63 | serde.registerTypeHandler( 64 | AComponent, 65 | dataStructDeserializer.bind(undefined, AComponent), 66 | dataStructSerializer 67 | ); 68 | 69 | const serial = serde.serialize({ 70 | entities: [entity1].values(), 71 | resources: {}, 72 | }, serdeOptions).toJSON(); 73 | const deserialized = serde.deserialize(SerialFormat.fromJSON(serial), serdeOptions); 74 | const deserializedEntity1 = deserialized.entities.next().value as IEntity | null; 75 | const deserializedAComponent = deserializedEntity1?.getComponent(AComponent); 76 | 77 | expect(deserializedAComponent instanceof AComponent).eq(true); 78 | // The setter isn't invoked when deserializing the object 79 | expect(deserializedAComponent!.getInternalFoo()).eq(1); 80 | // However the getter is, here! 81 | expect(deserializedAComponent!.foo).eq(0.01); 82 | } 83 | }); 84 | 85 | it('Aliases', () => { 86 | const worldBuilder = new WorldBuilder(new SerDe()); 87 | const world = worldBuilder.name(worldName).build(); 88 | 89 | // Value is set correctly 90 | expect(world.name).eq(worldName); 91 | // Ref was updated 92 | expect(worldBuilder.c).eq(worldBuilder.withComponent); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/world/world-builder.util.ts: -------------------------------------------------------------------------------- 1 | import type {TObjectProto} from "../_.spec.ts"; 2 | import type {IDeserializerOutput} from "../serde/serde.spec.ts"; 3 | 4 | // todo: read the Constructor parameters in order to throw early if a field is missing 5 | export function dataStructDeserializer(Constructor: TObjectProto, data: unknown): IDeserializerOutput { 6 | if (typeof data != 'object') { 7 | throw new Error(`Cannot default-deserialize ${Constructor.name}, because the data is of type ${typeof data}!`); 8 | } 9 | 10 | const obj: { [key: string]: any } = new Constructor(); 11 | 12 | for (const kv of Object.entries(data as object)) { 13 | obj[kv[0]] = kv[1]; 14 | } 15 | 16 | return { 17 | containsRefs: false, 18 | data: obj, 19 | type: Constructor, 20 | }; 21 | } 22 | 23 | export function dataStructSerializer(component: unknown): unknown { 24 | return component; 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig-tsc.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": true, 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "declarationDir": "dist", 7 | "downlevelIteration": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "importHelpers": true, 11 | "lib": [ 12 | "esnext", 13 | "dom" 14 | ], 15 | "module": "ESNext", 16 | "moduleResolution": "bundler", 17 | "noImplicitAny": true, 18 | "outDir": "dist", 19 | "preserveConstEnums": true, 20 | "removeComments": false, 21 | "sourceMap": true, 22 | "strict": true, 23 | "stripInternal": true, 24 | "target": "ESNext" 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "dist" 29 | ], 30 | "files": [ 31 | "src/index.ts" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": true, 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "declarationDir": "dist", 7 | "downlevelIteration": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "importHelpers": true, 11 | "lib": [ 12 | "esnext", 13 | "dom" 14 | ], 15 | "module": "ESNext", 16 | "moduleResolution": "bundler", 17 | "noEmit": true, 18 | "noImplicitAny": true, 19 | "noUncheckedIndexedAccess": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "outDir": "dist", 23 | "preserveConstEnums": true, 24 | "removeComments": false, 25 | "sourceMap": true, 26 | "strict": true, 27 | "stripInternal": true, 28 | "target": "ESNext" 29 | }, 30 | "exclude": [ 31 | "node_modules", 32 | "dist" 33 | ], 34 | "files": [ 35 | "src/index.ts" 36 | ] 37 | } 38 | --------------------------------------------------------------------------------