├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .nvmrc ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── docs └── video │ └── shmup-demo.webm ├── jekyll.config.yml ├── mise.toml ├── netlify.toml ├── packages ├── benchmark │ ├── .gitignore │ ├── .prettierrc │ ├── .vscode │ │ └── settings.json │ ├── README.md │ ├── package.json │ ├── pnpm-lock.yaml │ ├── renovate.json │ ├── src │ │ ├── bench.ts │ │ └── suites │ │ │ ├── add_remove.ts │ │ │ ├── entity_cycle.ts │ │ │ ├── frag_iter.ts │ │ │ ├── packed_5.ts │ │ │ └── simple_iter.ts │ └── tsconfig.json ├── ecs-benchmark │ ├── .github │ │ └── workflows │ │ │ └── main.yml │ ├── .gitignore │ ├── .nvmrc │ ├── .prettierrc │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── bench.js │ │ ├── bench_worker.js │ │ └── cases │ │ │ ├── becsy │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ ├── bitecs │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ ├── ecsy │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ ├── geotic │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ ├── goodluck │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ ├── harmony-ecs │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ ├── jakeklassen-ecs │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ ├── javelin-ecs │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ ├── miniplex │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ ├── objecs │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ ├── perform-ecs │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ ├── picoes │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ ├── piecs │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ ├── tiny-ecs │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ ├── uecs │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ │ │ └── wolf-ecs │ │ │ ├── add_remove.js │ │ │ ├── entity_cycle.js │ │ │ ├── frag_iter.js │ │ │ ├── packed_5.js │ │ │ └── simple_iter.js │ └── tsconfig.json ├── examples │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── assets │ │ └── image │ │ │ ├── gabe-idle-run.png │ │ │ ├── pico8_invaders_sprites.png │ │ │ └── ship.png │ ├── favicon.svg │ ├── index.html │ ├── package.json │ ├── public │ │ ├── css │ │ │ └── demo.css │ │ ├── js │ │ │ ├── feather-init.js │ │ │ ├── feather.min.js │ │ │ └── feather.min.js.map │ │ └── vite.svg │ ├── src │ │ ├── demos │ │ │ ├── basic │ │ │ │ ├── components │ │ │ │ │ └── ball-tag.ts │ │ │ │ ├── entity.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ └── systems │ │ │ │ │ ├── ball-movement-system.ts │ │ │ │ │ └── rendering-system.ts │ │ │ ├── bouncy-rectangles │ │ │ │ ├── entity.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ └── systems │ │ │ │ │ ├── physics-system.ts │ │ │ │ │ └── rendering-system.ts │ │ │ ├── debug-rendering │ │ │ │ ├── components │ │ │ │ │ ├── box-collider.ts │ │ │ │ │ ├── sprite-animation.ts │ │ │ │ │ └── sprite.ts │ │ │ │ ├── entity.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── spritesheet.ts │ │ │ │ ├── structures │ │ │ │ │ ├── animation-details.ts │ │ │ │ │ └── frame.ts │ │ │ │ └── systems │ │ │ │ │ ├── debug-rendering-system.ts │ │ │ │ │ ├── movement-system.ts │ │ │ │ │ ├── player-system.ts │ │ │ │ │ ├── rendering-system.ts │ │ │ │ │ └── sprite-animation-system.ts │ │ │ ├── falling-sand │ │ │ │ ├── README.md │ │ │ │ ├── components │ │ │ │ │ └── grid.ts │ │ │ │ ├── entity.ts │ │ │ │ ├── index.html │ │ │ │ ├── lib │ │ │ │ │ ├── color.ts │ │ │ │ │ ├── position-with-variance.ts │ │ │ │ │ └── sleep.ts │ │ │ │ ├── main.ts │ │ │ │ └── systems │ │ │ │ │ ├── mouse-system.ts │ │ │ │ │ ├── movement-system.ts │ │ │ │ │ ├── remove-render-system.ts │ │ │ │ │ ├── rendering-system.ts │ │ │ │ │ └── swap-system.ts │ │ │ ├── pixel-text │ │ │ │ ├── assets │ │ │ │ │ └── font │ │ │ │ │ │ ├── pico-8_regular_5.png │ │ │ │ │ │ └── pico-8_regular_5.xml │ │ │ │ ├── index.html │ │ │ │ └── main.ts │ │ │ ├── shmup │ │ │ │ ├── README.md │ │ │ │ ├── assets │ │ │ │ │ ├── audio │ │ │ │ │ │ ├── big-explosion-export.wav │ │ │ │ │ │ ├── big-explosion.ogg │ │ │ │ │ │ ├── big-explosion.wav │ │ │ │ │ │ ├── boss-music-export.wav │ │ │ │ │ │ ├── boss-music.ogg │ │ │ │ │ │ ├── boss-music.wav │ │ │ │ │ │ ├── boss-projectile-export.wav │ │ │ │ │ │ ├── boss-projectile.ogg │ │ │ │ │ │ ├── boss-projectile.wav │ │ │ │ │ │ ├── enemy-death.ogg │ │ │ │ │ │ ├── enemy-death.wav │ │ │ │ │ │ ├── enemy-projectile.ogg │ │ │ │ │ │ ├── enemy-projectile.wav │ │ │ │ │ │ ├── extra-life-export.wav │ │ │ │ │ │ ├── extra-life.ogg │ │ │ │ │ │ ├── extra-life.wav │ │ │ │ │ │ ├── game-over.ogg │ │ │ │ │ │ ├── game-over.wav │ │ │ │ │ │ ├── game-start-export.wav │ │ │ │ │ │ ├── game-start.ogg │ │ │ │ │ │ ├── game-start.wav │ │ │ │ │ │ ├── game-won-music.ogg │ │ │ │ │ │ ├── game-won-music.wav │ │ │ │ │ │ ├── no-cherry-bomb.ogg │ │ │ │ │ │ ├── no-cherry-bomb.wav │ │ │ │ │ │ ├── no-spread-shot.ogg │ │ │ │ │ │ ├── no-spread-shot.wav │ │ │ │ │ │ ├── pickup-export.wav │ │ │ │ │ │ ├── pickup.ogg │ │ │ │ │ │ ├── pickup.wav │ │ │ │ │ │ ├── player-death.ogg │ │ │ │ │ │ ├── player-death.wav │ │ │ │ │ │ ├── player-projectile-hit.ogg │ │ │ │ │ │ ├── player-projectile-hit.wav │ │ │ │ │ │ ├── shoot.ogg │ │ │ │ │ │ ├── shoot.wav │ │ │ │ │ │ ├── spread-shot-export.wav │ │ │ │ │ │ ├── spread-shot.ogg │ │ │ │ │ │ ├── spread-shot.wav │ │ │ │ │ │ ├── title-screen-music-export.wav │ │ │ │ │ │ ├── title-screen-music.ogg │ │ │ │ │ │ ├── title-screen-music.wav │ │ │ │ │ │ ├── wave-complete.ogg │ │ │ │ │ │ ├── wave-complete.wav │ │ │ │ │ │ ├── wave-spawn-export.wav │ │ │ │ │ │ ├── wave-spawn.ogg │ │ │ │ │ │ └── wave-spawn.wav │ │ │ │ │ ├── font │ │ │ │ │ │ ├── pico-8_regular_5.png │ │ │ │ │ │ └── pico-8_regular_5.xml │ │ │ │ │ └── image │ │ │ │ │ │ ├── explosions.png │ │ │ │ │ │ ├── play-button.png │ │ │ │ │ │ ├── player-explosions.png │ │ │ │ │ │ ├── promo │ │ │ │ │ │ └── social.png │ │ │ │ │ │ ├── shmup.ase │ │ │ │ │ │ ├── shmup.png │ │ │ │ │ │ └── sprites.png │ │ │ │ ├── bitmasks.ts │ │ │ │ ├── components │ │ │ │ │ ├── sprite-animation.ts │ │ │ │ │ ├── sprite-outline-animation.ts │ │ │ │ │ ├── sprite.ts │ │ │ │ │ ├── text-blink-animation.ts │ │ │ │ │ ├── transform.ts │ │ │ │ │ ├── ttl.ts │ │ │ │ │ └── tween.ts │ │ │ │ ├── config.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── content.ts │ │ │ │ ├── controls.ts │ │ │ │ ├── enemy │ │ │ │ │ ├── determine-pickable-enemies.ts │ │ │ │ │ ├── enemy-bullets.ts │ │ │ │ │ ├── pick-random-enemy.ts │ │ │ │ │ └── switch-enemy-to-attach-mode.ts │ │ │ │ ├── entity-factories │ │ │ │ │ ├── cherry.ts │ │ │ │ │ ├── destroy-player-bullet.ts │ │ │ │ │ ├── enemy.ts │ │ │ │ │ ├── explosion.ts │ │ │ │ │ ├── player │ │ │ │ │ │ └── spread-shot.ts │ │ │ │ │ ├── star.ts │ │ │ │ │ └── wave.ts │ │ │ │ ├── entity.ts │ │ │ │ ├── entity │ │ │ │ │ ├── assert.ts │ │ │ │ │ └── sort-entities-by-position.ts │ │ │ │ ├── game-events.ts │ │ │ │ ├── game-state.ts │ │ │ │ ├── game-time.ts │ │ │ │ ├── index.html │ │ │ │ ├── input.ts │ │ │ │ ├── lib │ │ │ │ │ └── canvas.ts │ │ │ │ ├── main.ts │ │ │ │ ├── scene.ts │ │ │ │ ├── scenes │ │ │ │ │ ├── game-over-screen.ts │ │ │ │ │ ├── game-won-screen.ts │ │ │ │ │ ├── gameplay-screen.ts │ │ │ │ │ ├── loading-screen.ts │ │ │ │ │ └── title-screen.ts │ │ │ │ ├── spritesheet.ts │ │ │ │ ├── structures │ │ │ │ │ ├── animation-details.ts │ │ │ │ │ └── frame.ts │ │ │ │ ├── systems │ │ │ │ │ ├── bomb-system.ts │ │ │ │ │ ├── boss-system.ts │ │ │ │ │ ├── bound-to-viewport-system.ts │ │ │ │ │ ├── camera-shake-system.ts │ │ │ │ │ ├── cherry-text-system.ts │ │ │ │ │ ├── collision-system.ts │ │ │ │ │ ├── debug-rendering-system.ts │ │ │ │ │ ├── destroy-boss-event-system.ts │ │ │ │ │ ├── destroy-on-viewport-exit-system.ts │ │ │ │ │ ├── enemy-pick-system.ts │ │ │ │ │ ├── event-system.ts │ │ │ │ │ ├── flash-rendering-system.ts │ │ │ │ │ ├── game-over-rendering-system.ts │ │ │ │ │ ├── handle-game-over-system.ts │ │ │ │ │ ├── invulnerable-system.ts │ │ │ │ │ ├── lateral-hunter-system.ts │ │ │ │ │ ├── lives-rendering-system.ts │ │ │ │ │ ├── local-transform-system.ts │ │ │ │ │ ├── movement-system.ts │ │ │ │ │ ├── muzzle-flash-rendering-system.ts │ │ │ │ │ ├── muzzle-flash-system.ts │ │ │ │ │ ├── next-wave-event-system.ts │ │ │ │ │ ├── particle-rendering-system.ts │ │ │ │ │ ├── particle-system.ts │ │ │ │ │ ├── player-enemy-collision-event-cleanup-system.ts │ │ │ │ │ ├── player-enemy-collision-event-system.ts │ │ │ │ │ ├── player-pickup-collision-event-system.ts │ │ │ │ │ ├── player-projectile-boss-collision-event-cleanup-system.ts │ │ │ │ │ ├── player-projectile-boss-collision-event-system.ts │ │ │ │ │ ├── player-projectile-enemy-collision-event-cleanup-system.ts │ │ │ │ │ ├── player-projectile-enemy-collision-event-system.ts │ │ │ │ │ ├── player-system.ts │ │ │ │ │ ├── rendering-system.ts │ │ │ │ │ ├── score-system.ts │ │ │ │ │ ├── shockwave-rendering-system.ts │ │ │ │ │ ├── shockwave-system.ts │ │ │ │ │ ├── sound-system.ts │ │ │ │ │ ├── sprite-animation-system.ts │ │ │ │ │ ├── sprite-outline-animation-system.ts │ │ │ │ │ ├── sprite-outline-rendering-system.ts │ │ │ │ │ ├── sprite-rendering-system.ts │ │ │ │ │ ├── starfield-rendering-system.ts │ │ │ │ │ ├── starfield-system.ts │ │ │ │ │ ├── start-game-system.ts │ │ │ │ │ ├── text-blink-animation-system.ts │ │ │ │ │ ├── text-blink-rendering-system.ts │ │ │ │ │ ├── text-rendering-system.ts │ │ │ │ │ ├── text-system.ts │ │ │ │ │ ├── time-to-live-system.ts │ │ │ │ │ ├── timer-system.ts │ │ │ │ │ ├── trigger-enemy-attack-event-system.ts │ │ │ │ │ ├── trigger-enemy-fire-event-system.ts │ │ │ │ │ ├── trigger-game-over-system.ts │ │ │ │ │ ├── trigger-game-won-system.ts │ │ │ │ │ ├── tweens-system.ts │ │ │ │ │ ├── wave-ready-check-system.ts │ │ │ │ │ └── yellow-ship-system.ts │ │ │ │ └── timer.ts │ │ │ ├── sprite-animation │ │ │ │ ├── components │ │ │ │ │ └── sprite-animation.ts │ │ │ │ ├── entity.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── spritesheet.ts │ │ │ │ └── systems │ │ │ │ │ ├── rendering-system.ts │ │ │ │ │ └── sprite-animation-system.ts │ │ │ └── sprite-tweening │ │ │ │ ├── components │ │ │ │ ├── sprite.ts │ │ │ │ └── tween.ts │ │ │ │ ├── entity.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── structures │ │ │ │ └── frame.ts │ │ │ │ └── systems │ │ │ │ ├── rendering-system.ts │ │ │ │ └── tweens-system.ts │ │ ├── lib │ │ │ ├── array.ts │ │ │ ├── asset-loader.ts │ │ │ ├── audio-manager.ts │ │ │ ├── canvas-recorder.ts │ │ │ ├── canvas.ts │ │ │ ├── collision │ │ │ │ └── aabb.ts │ │ │ ├── dom.ts │ │ │ ├── event-emitter.ts │ │ │ ├── math.ts │ │ │ ├── pixel-text │ │ │ │ ├── load-font.ts │ │ │ │ ├── renderer.ts │ │ │ │ └── text-buffer.ts │ │ │ ├── tween.ts │ │ │ └── types │ │ │ │ └── dotted-paths.ts │ │ ├── main.ts │ │ ├── shared │ │ │ ├── components │ │ │ │ ├── color.ts │ │ │ │ ├── index.ts │ │ │ │ ├── position.ts │ │ │ │ ├── rectangle.ts │ │ │ │ ├── transform.ts │ │ │ │ └── velocity.ts │ │ │ └── shared-entity.ts │ │ ├── style.css │ │ ├── typescript.svg │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts └── objecs │ ├── .prettierrc │ ├── README.md │ ├── eslint.config.mjs │ ├── nodemon.json │ ├── package.json │ ├── src │ ├── archetype.test.ts │ ├── archetype.ts │ ├── index.ts │ ├── world.test.ts │ └── world.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tasks.md └── workspace.code-workspace /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 15 | 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | node: ["20.x", "22.x", "24.x"] 20 | os: [ubuntu-24.04] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Use Node ${{ matrix.node }} 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node }} 29 | 30 | - uses: pnpm/action-setup@v4.1.0 31 | with: 32 | version: 10.11.0 33 | 34 | - name: Install 35 | run: pnpm i 36 | 37 | - name: Lint 38 | run: pnpm --filter objecs lint 39 | 40 | - name: Build 41 | run: pnpm --filter objecs build 42 | 43 | - name: Check Exports 44 | run: pnpm --filter objecs check-exports 45 | 46 | - name: Test 47 | run: pnpm --filter objecs test -- --run 48 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-24.04 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 22.x 18 | registry-url: "https://registry.npmjs.org" 19 | 20 | - uses: pnpm/action-setup@v4.1.0 21 | with: 22 | version: 10.11.0 23 | 24 | - run: npx changelogithub 25 | env: 26 | GITHUB_TOKEN: ${{secrets.RELEASER_TOKEN}} 27 | 28 | - run: pnpm i 29 | - run: pnpm publish --no-git-checks --access public --filter objecs 30 | env: 31 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | node_modules 3 | dist 4 | coverage 5 | *.log 6 | .vscode/* 7 | !.vscode/launch.json 8 | !.vscode/settings.json 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | **/tsconfig.json 3 | **/nodemon.json 4 | **/tslint.json 5 | **/webpack.config.ts 6 | **/jest.config.js 7 | demo 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | registry=https://registry.npmjs.org/ 3 | unsafe-perm=true 4 | enable-pre-post-scripts=true 5 | auto-install-peers=true 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "trailingComma": "all", 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug Current Test File", 11 | "autoAttachChildProcesses": true, 12 | "skipFiles": ["/**", "**/node_modules/**"], 13 | "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", 14 | "args": ["run", "${relativeFile}"], 15 | "smartStep": true, 16 | "console": "integratedTerminal" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.importModuleSpecifierEnding": "js" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jake Klassen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # objECS 2 | 3 | Object ECS. 4 | 5 | Checkout the demos [site] and [folder]. 6 | 7 | The featured demo is the [shmup demo] (Cherry Bomb), a port of the [Lazy Devs](https://www.youtube.com/@LazyDevs) pico-8 game. 8 | 9 | [shmup-demo.webm](https://github.com/jakeklassen/objecs/assets/1383068/994302b7-7b98-4b46-b785-fd0fd183ffdc) 10 | 11 | ## Benchmarks 12 | 13 | You can run some benchmarks by running `pnpm --filter ecs-benchmark start`. 14 | 15 | ## Tools 16 | 17 | - [changelogithub] 18 | - This generates a changelog for a github release using [Conventional Commits]. 19 | - [bumpp] 20 | - Interactive version prompt. 21 | 22 | ## How to Work 23 | 24 | - Develop like normal using [Conventional Commits]. 25 | - When you're ready to push a tag, run `bumpp`. 26 | - This will walk you through releasing a `tag`. 27 | - When you're ready to publish, create a github release and the workflow will take over. 28 | - This will use [changelogithub] to generate a changelog and publish to NPM. 29 | 30 | ## Kudos 31 | 32 | - [miniplex] 33 | 34 | [bumpp]: https://www.npmjs.com/package/bumpp 35 | [changelogithub]: https://github.com/antfu/changelogithub 36 | [conventional commits]: https://www.conventionalcommits.org/en/v1.0.0/ 37 | [folder]: https://github.com/jakeklassen/objecs/tree/main/packages/examples/src/demos 38 | [miniplex]: https://www.npmjs.com/package/miniplex 39 | [shmup demo]: https://objecs.netlify.app/src/demos/shmup/ 40 | [site]: https://objecs.netlify.app/ 41 | -------------------------------------------------------------------------------- /docs/video/shmup-demo.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/docs/video/shmup-demo.webm -------------------------------------------------------------------------------- /jekyll.config.yml: -------------------------------------------------------------------------------- 1 | # Used to configure jekyll for typedoc 2 | include: 3 | - '_*_.html' 4 | - '_*_.*.html' 5 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | pnpm = "10.11.0" 3 | node = "22.16.0" 4 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "packages/examples/dist" 3 | 4 | command = "pnpm --version; pnpm install && pnpm --filter objecs build && pnpm --filter examples build" -------------------------------------------------------------------------------- /packages/benchmark/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "trailingComma": "all", 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /packages/benchmark/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /packages/benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Benchmark 2 | 3 | `npx tsx src/suite/suite-to-run.ts` 4 | 5 | Really just been porting the suites from [noctjs/ecs-benchmark](https://github.com/noctjs/ecs-benchmark). 6 | -------------------------------------------------------------------------------- /packages/benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benchmark", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "lint": "prettier --check 'src/**/*.ts'", 9 | "lint:fix": "prettier --write 'src/**/*.ts'", 10 | "start": "tsx src/bench.ts" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@types/benchmark": "^2.1.5", 17 | "@types/node": "^22.15.21", 18 | "tsx": "^4.19.4", 19 | "typescript": "5.8.3" 20 | }, 21 | "dependencies": { 22 | "@thi.ng/bench": "3.6.20", 23 | "benchmark": "^2.1.4", 24 | "miniplex": "2.0.0", 25 | "mitata": "1.0.34", 26 | "objecs": "workspace:*", 27 | "type-fest": "4.41.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/benchmark/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/benchmark/src/bench.ts: -------------------------------------------------------------------------------- 1 | console.log("Run suites from suites/"); 2 | -------------------------------------------------------------------------------- /packages/benchmark/src/suites/add_remove.ts: -------------------------------------------------------------------------------- 1 | import * as miniplex from "miniplex"; 2 | import { bench, run, summary } from "mitata"; 3 | import * as objecs from "objecs"; 4 | 5 | type Entity = { 6 | A?: boolean; 7 | B?: boolean; 8 | }; 9 | 10 | const count = 1_000; 11 | 12 | summary(() => { 13 | { 14 | const world = new objecs.World(); 15 | 16 | for (let i = 0; i < count; i++) { 17 | world.createEntity({ 18 | A: true, 19 | }); 20 | } 21 | 22 | bench("add_remove [objecs]", () => { 23 | for (const entity of world.entities) { 24 | world.addEntityComponents(entity, "B", true); 25 | } 26 | 27 | for (const entity of world.entities) { 28 | world.removeEntityComponents(entity, "B"); 29 | } 30 | }); 31 | } 32 | 33 | { 34 | const world = new miniplex.World(); 35 | 36 | for (let i = 0; i < count; i++) { 37 | world.add({ 38 | A: true, 39 | }); 40 | } 41 | 42 | bench("add_remove [miniplex]", () => { 43 | for (const entity of world.entities) { 44 | world.addComponent(entity, "B", true); 45 | } 46 | 47 | for (const entity of world.entities) { 48 | world.removeComponent(entity, "B"); 49 | } 50 | }); 51 | } 52 | }); 53 | 54 | await run(); 55 | -------------------------------------------------------------------------------- /packages/benchmark/src/suites/entity_cycle.ts: -------------------------------------------------------------------------------- 1 | import * as miniplex from "miniplex"; 2 | import { bench, run, summary } from "mitata"; 3 | import * as objecs from "objecs"; 4 | 5 | type Entity = { 6 | A?: number; 7 | B?: number; 8 | }; 9 | 10 | const count = 1_000; 11 | 12 | summary(() => { 13 | { 14 | const world = new objecs.World(); 15 | 16 | for (let i = 0; i < count; i++) { 17 | world.createEntity({ 18 | A: 1, 19 | }); 20 | } 21 | 22 | const withA = world.archetype("A"); 23 | const withB = world.archetype("B"); 24 | 25 | bench("entity_cycle [objecs]", () => { 26 | for (const _entity of withA.entities) { 27 | world.createEntity({ B: 1 }); 28 | } 29 | 30 | for (const entity of withB.entities) { 31 | world.deleteEntity(entity); 32 | } 33 | }); 34 | } 35 | 36 | { 37 | const world = new miniplex.World(); 38 | 39 | for (let i = 0; i < count; i++) { 40 | world.add({ 41 | A: 1, 42 | }); 43 | } 44 | 45 | const withA = world.with("A"); 46 | const withB = world.with("B"); 47 | 48 | bench("entity_cycle [miniplex]", () => { 49 | for (const _entity of withA.entities) { 50 | world.add({ B: 1 }); 51 | } 52 | 53 | for (const entity of withB.entities) { 54 | world.remove(entity); 55 | } 56 | }); 57 | } 58 | }); 59 | 60 | await run(); 61 | -------------------------------------------------------------------------------- /packages/benchmark/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["dom", "ESNext"], 5 | "allowJs": true, 6 | "checkJs": true, 7 | "module": "NodeNext", 8 | "noEmit": true, 9 | "strict": true, 10 | "verbatimModuleSyntax": true, 11 | "types": ["node"] 12 | }, 13 | "include": ["src/**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - run: npm ci 11 | - run: npm run check-format 12 | bench-obj: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - run: npm ci 17 | - run: npm run start -- @obj 18 | bench-soa: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - run: npm ci 23 | - run: npm run start -- @soa 24 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "trailingComma": "all", 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecs-benchmark", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "start": "node src/bench.js", 7 | "lint": "prettier --check 'src/**/*.js'", 8 | "lint:fix": "prettier --write 'src/**/*.js'" 9 | }, 10 | "dependencies": { 11 | "@jakeklassen/ecs": "4.0.7", 12 | "@javelin/ecs": "0.22.0", 13 | "@lastolivegames/becsy": "0.8.4", 14 | "bitecs": "0.3.40", 15 | "ecsy": "0.4.2", 16 | "geotic": "4.3.2", 17 | "goodluck": "7.0.0", 18 | "harmony-ecs": "0.0.12", 19 | "miniplex": "^2.0.0", 20 | "objecs": "workspace:*", 21 | "perform-ecs": "0.7.8", 22 | "picoes": "1.1.0", 23 | "piecs": "0.4.0", 24 | "tiny-ecs": "2.0.0", 25 | "uecs": "0.4.2", 26 | "wolf-ecs": "2.1.2" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^22.15.21", 30 | "prettier": "^3.5.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/bench_worker.js: -------------------------------------------------------------------------------- 1 | import { performance } from "node:perf_hooks"; 2 | import { parentPort, workerData } from "node:worker_threads"; 3 | import { pathToFileURL } from "node:url"; 4 | 5 | // Load the function to bench 6 | let setup = await import(pathToFileURL(workerData.path).pathname).then( 7 | (module) => module.default, 8 | ); 9 | 10 | let fn = await setup(workerData.config); 11 | 12 | let cycle_n = 1; 13 | let cycle_ms = 0; 14 | let cycle_total_ms = 0; 15 | 16 | // Run multiple cycles to get an estimate 17 | while (cycle_total_ms < 500) { 18 | let elapsed = bench_iter(fn, cycle_n); 19 | cycle_ms = elapsed / cycle_n; 20 | cycle_n *= 2; 21 | cycle_total_ms += elapsed; 22 | } 23 | 24 | // Try to estimate the iteration count for 500ms 25 | let target_n = 500 / cycle_ms; 26 | let total_ms = bench_iter(fn, target_n); 27 | 28 | parentPort.postMessage({ 29 | hz: (target_n / total_ms) * 1_000, // ops/sec 30 | ms: total_ms / target_n, // ms/op 31 | }); 32 | 33 | /** 34 | * 35 | * @param {() => void} fn 36 | * @param {number} count 37 | * @returns 38 | */ 39 | function bench_iter(fn, count) { 40 | let start = performance.now(); 41 | 42 | for (let i = 0; i < count; i++) { 43 | fn(); 44 | } 45 | 46 | let end = performance.now(); 47 | return end - start; 48 | } 49 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/becsy/add_remove.js: -------------------------------------------------------------------------------- 1 | import { System, World } from "@lastolivegames/becsy/perf.js"; 2 | 3 | export default async (count) => { 4 | class A { 5 | static schema = {}; 6 | } 7 | 8 | class B { 9 | static schema = {}; 10 | } 11 | 12 | class AddB extends System { 13 | entities = this.query((q) => q.current.with(A).but.without(B).write); 14 | 15 | execute() { 16 | for (const entity of this.entities.current) { 17 | entity.add(B); 18 | } 19 | } 20 | } 21 | 22 | class RemoveB extends System { 23 | entities = this.query((q) => q.current.with(B).write); 24 | 25 | execute() { 26 | for (const entity of this.entities.current) { 27 | entity.remove(B); 28 | } 29 | } 30 | } 31 | 32 | const world = await World.create({ 33 | maxEntities: count, 34 | maxShapeChangesPerFrame: count * 3, 35 | defs: [A, B, AddB, RemoveB], 36 | }); 37 | 38 | for (let i = 0; i < count; i++) { 39 | world.createEntity(A); 40 | } 41 | 42 | return () => { 43 | world.execute(); 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/becsy/entity_cycle.js: -------------------------------------------------------------------------------- 1 | import { System, Type, World } from "@lastolivegames/becsy/perf.js"; 2 | 3 | export default async (count) => { 4 | class A { 5 | static schema = { 6 | value: Type.int32, 7 | }; 8 | } 9 | 10 | class B { 11 | static schema = { 12 | value: Type.int32, 13 | }; 14 | } 15 | 16 | class SpawnB extends System { 17 | entities = this.query((q) => q.current.with(A).also.using(B).write); 18 | 19 | execute() { 20 | for (const entity of this.entities.current) { 21 | this.createEntity(B, { 22 | value: entity.read(A).value, 23 | }); 24 | } 25 | } 26 | } 27 | 28 | class KillB extends System { 29 | entities = this.query((q) => q.current.with(B).write); 30 | 31 | execute() { 32 | for (const entity of this.entities.current) { 33 | entity.delete(); 34 | } 35 | } 36 | } 37 | 38 | const world = await World.create({ 39 | maxEntities: count * 8, 40 | maxLimboEntities: 10000, 41 | defs: [A, B, SpawnB, KillB], 42 | }); 43 | 44 | for (let i = 0; i < count; i++) { 45 | world.createEntity(A, { value: i }); 46 | } 47 | 48 | return () => { 49 | world.execute(); 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/becsy/frag_iter.js: -------------------------------------------------------------------------------- 1 | import { System, Type, World } from "@lastolivegames/becsy/perf.js"; 2 | 3 | export default async (count) => { 4 | const COMPS = Array.from( 5 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 6 | () => 7 | class { 8 | static schema = { 9 | value: Type.int32, 10 | }; 11 | }, 12 | ); 13 | 14 | const Z = COMPS[25]; 15 | 16 | class Data { 17 | static schema = { 18 | value: Type.int32, 19 | }; 20 | } 21 | 22 | class DataSystem extends System { 23 | entities = this.query((q) => q.current.with(Data).write); 24 | 25 | execute() { 26 | for (const entity of this.entities.current) { 27 | entity.write(Data).value *= 2; 28 | } 29 | } 30 | } 31 | 32 | class ZSystem extends System { 33 | entities = this.query((q) => q.current.with(Z).write); 34 | 35 | execute() { 36 | for (const entity of this.entities.current) { 37 | entity.write(Z).value *= 2; 38 | } 39 | } 40 | } 41 | 42 | const world = await World.create({ 43 | maxEntities: count * COMPS.length, 44 | defs: [COMPS, Data, DataSystem, ZSystem], 45 | }); 46 | 47 | for (let i = 0; i < count; i++) { 48 | for (const Comp of COMPS) { 49 | world.createEntity(Comp, { value: 0 }, Data, { value: 0 }); 50 | } 51 | } 52 | 53 | return () => { 54 | world.execute(); 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/bitecs/add_remove.js: -------------------------------------------------------------------------------- 1 | import { 2 | addComponent, 3 | addEntity, 4 | createWorld, 5 | defineComponent, 6 | defineQuery, 7 | pipe, 8 | removeComponent, 9 | } from "bitecs"; 10 | 11 | export default (count) => { 12 | const world = createWorld(); 13 | 14 | const A = defineComponent(); 15 | const B = defineComponent(); 16 | 17 | const queryA = defineQuery([A]); 18 | const addB = (world) => { 19 | const ents = queryA(world); 20 | for (let i = 0; i < ents.length; i++) { 21 | const eid = ents[i]; 22 | addComponent(world, B, eid); 23 | } 24 | return world; 25 | }; 26 | 27 | const queryB = defineQuery([B]); 28 | const removeB = (world) => { 29 | const ents = queryB(world); 30 | for (let i = 0; i < ents.length; i++) { 31 | const eid = ents[i]; 32 | removeComponent(world, B, eid); 33 | } 34 | return world; 35 | }; 36 | 37 | for (let i = 0; i < count; i++) { 38 | let eid = addEntity(world); 39 | addComponent(world, A, eid); 40 | } 41 | 42 | const pipeline = pipe(addB, removeB); 43 | 44 | return () => { 45 | pipeline(world); 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/bitecs/entity_cycle.js: -------------------------------------------------------------------------------- 1 | import { 2 | addComponent, 3 | addEntity, 4 | createWorld, 5 | defineComponent, 6 | defineQuery, 7 | pipe, 8 | removeEntity, 9 | Types, 10 | } from "bitecs"; 11 | 12 | const { i32 } = Types; 13 | 14 | export default (count) => { 15 | const world = createWorld(); 16 | 17 | const A = defineComponent({ value: i32 }); 18 | const B = defineComponent({ value: i32 }); 19 | 20 | const queryA = defineQuery([A]); 21 | const spawnB = (world) => { 22 | const ents = queryA(world); 23 | for (let i = 0; i < ents.length; i++) { 24 | const eid = addEntity(world); 25 | addComponent(world, B, eid); 26 | B.value[eid] = A.value[ents[i]]; 27 | } 28 | return world; 29 | }; 30 | 31 | const queryB = defineQuery([B]); 32 | const killB = (world) => { 33 | const ents = queryB(world); 34 | for (let i = 0; i < ents.length; i++) { 35 | const eid = ents[i]; 36 | removeEntity(world, eid); 37 | } 38 | return world; 39 | }; 40 | 41 | for (let i = 0; i < count; i++) { 42 | const eid = addEntity(world); 43 | addComponent(world, A, eid); 44 | A.value[eid] = i; 45 | } 46 | 47 | const pipeline = pipe(spawnB, killB); 48 | 49 | return () => { 50 | pipeline(world); 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/bitecs/frag_iter.js: -------------------------------------------------------------------------------- 1 | import { 2 | addComponent, 3 | addEntity, 4 | createWorld, 5 | defineComponent, 6 | defineQuery, 7 | Types, 8 | } from "bitecs"; 9 | 10 | const { i32 } = Types; 11 | 12 | const COMPS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); 13 | 14 | export default (count) => { 15 | const world = createWorld(); 16 | 17 | const components = COMPS.map(() => defineComponent({ value: i32 })); 18 | 19 | const Z = components[25]; 20 | const Data = defineComponent({ value: i32 }); 21 | 22 | const dataQuery = defineQuery([Data]); 23 | const dataSystem = (world) => { 24 | const ents = dataQuery(world); 25 | for (let i = 0; i < ents.length; i++) { 26 | const eid = ents[i]; 27 | Data.value[eid] *= 2; 28 | } 29 | return world; 30 | }; 31 | 32 | const zQuery = defineQuery([Z]); 33 | const zSystem = (world) => { 34 | const ents = zQuery(world); 35 | for (let i = 0; i < ents.length; i++) { 36 | const eid = ents[i]; 37 | Z.value[eid] *= 2; 38 | } 39 | return world; 40 | }; 41 | 42 | for (let i = 0; i < count; i++) { 43 | for (const component of components) { 44 | const e = addEntity(world); 45 | addComponent(world, Data, e); 46 | addComponent(world, component, e); 47 | } 48 | } 49 | 50 | return () => { 51 | dataSystem(world); 52 | zSystem(world); 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/ecsy/add_remove.js: -------------------------------------------------------------------------------- 1 | import { Component, Not, System, Types, World } from "ecsy"; 2 | 3 | class A extends Component { 4 | static schema = {}; 5 | } 6 | 7 | class B extends Component { 8 | static schema = {}; 9 | } 10 | 11 | class AddB extends System { 12 | static queries = { 13 | entities: { components: [A, Not(B)] }, 14 | }; 15 | 16 | execute() { 17 | this.queries.entities.results.forEach((entity) => { 18 | entity.addComponent(B); 19 | }); 20 | } 21 | } 22 | 23 | class RemoveB extends System { 24 | static queries = { 25 | entities: { components: [B] }, 26 | }; 27 | 28 | execute() { 29 | this.queries.entities.results.forEach((entity) => { 30 | entity.removeComponent(B); 31 | }); 32 | } 33 | } 34 | 35 | export default (count) => { 36 | let world = new World(); 37 | 38 | world 39 | .registerComponent(A) 40 | .registerComponent(B) 41 | .registerSystem(AddB) 42 | .registerSystem(RemoveB); 43 | 44 | for (let i = 0; i < count; i++) { 45 | world.createEntity().addComponent(A); 46 | } 47 | 48 | return () => { 49 | world.execute(); 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/ecsy/entity_cycle.js: -------------------------------------------------------------------------------- 1 | import { Component, System, Types, World } from "ecsy"; 2 | 3 | class A extends Component { 4 | static schema = { 5 | value: { type: Types.Number }, 6 | }; 7 | } 8 | 9 | class B extends Component { 10 | static schema = { 11 | value: { type: Types.Number }, 12 | }; 13 | } 14 | 15 | class SpawnB extends System { 16 | static queries = { 17 | entities: { components: [A] }, 18 | }; 19 | 20 | execute() { 21 | this.queries.entities.results.forEach((entity) => { 22 | this.world.createEntity().addComponent(B, { 23 | value: entity.getComponent(A).value, 24 | }); 25 | }); 26 | } 27 | } 28 | 29 | class KillB extends System { 30 | static queries = { 31 | entities: { components: [B] }, 32 | }; 33 | 34 | execute() { 35 | this.queries.entities.results.forEach((entity) => { 36 | entity.remove(); 37 | }); 38 | } 39 | } 40 | 41 | export default (count) => { 42 | let world = new World(); 43 | 44 | world 45 | .registerComponent(A) 46 | .registerComponent(B) 47 | .registerSystem(SpawnB) 48 | .registerSystem(KillB); 49 | 50 | for (let i = 0; i < count; i++) { 51 | world.createEntity().addComponent(A, { value: i }); 52 | } 53 | 54 | return () => { 55 | world.execute(); 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/geotic/add_remove.js: -------------------------------------------------------------------------------- 1 | import { Component, Engine } from "geotic"; 2 | 3 | class A extends Component {} 4 | class B extends Component {} 5 | 6 | export default (count) => { 7 | const engine = new Engine(); 8 | 9 | engine.registerComponent(A); 10 | engine.registerComponent(B); 11 | 12 | const world = engine.createWorld(); 13 | 14 | for (let i = 0; i < count; i++) { 15 | world.createEntity().add(A); 16 | } 17 | 18 | let with_b = world.createQuery({ all: [B] }); 19 | let without_b = world.createQuery({ none: [B] }); 20 | 21 | return () => { 22 | for (let entity of without_b.get()) { 23 | entity.add(B); 24 | } 25 | 26 | for (let entity of with_b.get()) { 27 | entity.remove(entity.b); 28 | } 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/geotic/entity_cycle.js: -------------------------------------------------------------------------------- 1 | import { Component, Engine } from "geotic"; 2 | 3 | class A extends Component {} 4 | class B extends Component {} 5 | 6 | export default (count) => { 7 | const engine = new Engine(); 8 | 9 | engine.registerComponent(A); 10 | engine.registerComponent(B); 11 | 12 | const world = engine.createWorld(); 13 | 14 | for (let i = 0; i < count; i++) { 15 | world.createEntity().add(A, { value: i }); 16 | } 17 | 18 | let with_a = world.createQuery({ all: [A] }); 19 | let with_b = world.createQuery({ all: [B] }); 20 | 21 | return () => { 22 | for (let entity of with_a.get()) { 23 | world.createEntity().add(B, { 24 | value: entity.a.value, 25 | }); 26 | } 27 | 28 | for (let entity of with_b.get()) { 29 | entity.destroy(); 30 | } 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/geotic/frag_iter.js: -------------------------------------------------------------------------------- 1 | import { Component, Engine } from "geotic"; 2 | 3 | const COMPS = Array.from("ABCDEFGHIJKLMNOPQRSTUVWXYZ", (name) => 4 | Function("Component", `return class ${name} extends Component {}`)(Component), 5 | ); 6 | 7 | const Z = COMPS[25]; 8 | 9 | class Data extends Component {} 10 | 11 | export default (count) => { 12 | const engine = new Engine(); 13 | 14 | engine.registerComponent(Data); 15 | 16 | for (let Comp of COMPS) { 17 | engine.registerComponent(Comp); 18 | } 19 | 20 | const world = engine.createWorld(); 21 | 22 | for (let i = 0; i < count; i++) { 23 | for (let Comp of COMPS) { 24 | let e = world.createEntity(); 25 | e.add(Comp, { value: 0 }); 26 | e.add(Data, { value: 0 }); 27 | } 28 | } 29 | 30 | let data = world.createQuery({ all: [Data] }); 31 | let z = world.createQuery({ all: [Z] }); 32 | 33 | return () => { 34 | for (let entity of data.get()) { 35 | entity.data.value *= 2; 36 | } 37 | for (let entity of z.get()) { 38 | entity.z.value *= 2; 39 | } 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/geotic/packed_5.js: -------------------------------------------------------------------------------- 1 | import { Component, Engine } from "geotic"; 2 | 3 | class A extends Component {} 4 | class B extends Component {} 5 | class C extends Component {} 6 | class D extends Component {} 7 | class E extends Component {} 8 | 9 | export default (count) => { 10 | const engine = new Engine(); 11 | 12 | engine.registerComponent(A); 13 | engine.registerComponent(B); 14 | engine.registerComponent(C); 15 | engine.registerComponent(D); 16 | engine.registerComponent(E); 17 | 18 | const world = engine.createWorld(); 19 | 20 | for (let i = 0; i < count; i++) { 21 | let e = world.createEntity(); 22 | e.add(A, { value: 0 }); 23 | e.add(B, { value: 0 }); 24 | e.add(C, { value: 0 }); 25 | e.add(D, { value: 0 }); 26 | e.add(E, { value: 0 }); 27 | } 28 | 29 | let a = world.createQuery({ all: [A] }); 30 | let b = world.createQuery({ all: [B] }); 31 | let c = world.createQuery({ all: [C] }); 32 | let d = world.createQuery({ all: [D] }); 33 | let e = world.createQuery({ all: [E] }); 34 | 35 | return () => { 36 | for (let entity of a.get()) { 37 | entity.a.value *= 2; 38 | } 39 | 40 | for (let entity of b.get()) { 41 | entity.b.value *= 2; 42 | } 43 | 44 | for (let entity of c.get()) { 45 | entity.c.value *= 2; 46 | } 47 | 48 | for (let entity of d.get()) { 49 | entity.d.value *= 2; 50 | } 51 | 52 | for (let entity of e.get()) { 53 | entity.e.value *= 2; 54 | } 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/goodluck/add_remove.js: -------------------------------------------------------------------------------- 1 | import { WorldImpl, instantiate } from "goodluck"; 2 | 3 | class World extends WorldImpl { 4 | A = []; 5 | B = []; 6 | } 7 | 8 | const HAS_A = 1 << 0; 9 | const HAS_B = 1 << 1; 10 | 11 | function a(value) { 12 | return (world, entity) => { 13 | world.Signature[entity] |= HAS_A; 14 | world.A[entity] = value; 15 | }; 16 | } 17 | 18 | export default (count) => { 19 | let world = new World(); 20 | 21 | for (let i = 0; i < count; i++) { 22 | instantiate(world, [a(i)]); 23 | } 24 | 25 | return () => { 26 | for (let i = 0; i < world.Signature.length; i++) { 27 | if ((world.Signature[i] & HAS_B) === 0) { 28 | world.Signature[i] |= HAS_B; 29 | world.B[i] = i; 30 | } 31 | } 32 | 33 | for (let i = 0; i < world.Signature.length; i++) { 34 | if ((world.Signature[i] & HAS_B) === HAS_B) { 35 | world.Signature[i] &= ~HAS_B; 36 | } 37 | } 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/goodluck/entity_cycle.js: -------------------------------------------------------------------------------- 1 | import { instantiate, WorldImpl } from "goodluck"; 2 | 3 | class World extends WorldImpl { 4 | A = []; 5 | B = []; 6 | } 7 | 8 | const HAS_A = 1 << 0; 9 | const HAS_B = 1 << 1; 10 | 11 | class A { 12 | constructor(value) { 13 | this.value = value; 14 | } 15 | } 16 | 17 | class B { 18 | constructor(value) { 19 | this.value = value; 20 | } 21 | } 22 | 23 | function a(value) { 24 | return (world, entity) => { 25 | world.Signature[entity] |= HAS_A; 26 | world.A[entity] = new A(value); 27 | }; 28 | } 29 | 30 | function b(value) { 31 | return (world, entity) => { 32 | world.Signature[entity] |= HAS_B; 33 | world.B[entity] = new B(value); 34 | }; 35 | } 36 | 37 | export default (count) => { 38 | let world = new World(); 39 | 40 | for (let i = 0; i < count; i++) { 41 | instantiate(world, [a(i)]); 42 | } 43 | 44 | return () => { 45 | for (let i = 0; i < world.Signature.length; i++) { 46 | if ((world.Signature[i] & HAS_A) === HAS_A) { 47 | instantiate(world, [b(world.A[i].value)]); 48 | } 49 | } 50 | 51 | for (let i = 0; i < world.Signature.length; i++) { 52 | if ((world.Signature[i] & HAS_B) === HAS_B) { 53 | world.DestroyEntity(i); 54 | } 55 | } 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/harmony-ecs/add_remove.js: -------------------------------------------------------------------------------- 1 | import { Entity, Format, Query, Schema, World } from "harmony-ecs"; 2 | 3 | export default (count) => { 4 | const world = World.make(count); 5 | const A = Schema.makeBinary(world, Format.int32); 6 | const B = Schema.makeBinary(world, Format.int32); 7 | const qa = Query.make(world, [A], Query.not([B])); 8 | const qab = Query.make(world, [A, B]); 9 | 10 | for (let i = 0; i < count; i++) { 11 | Entity.make(world, [A]); 12 | } 13 | 14 | return () => { 15 | for (let i = 0; i < qa.length; i++) { 16 | const [e] = qa[i]; 17 | for (let j = e.length - 1; j >= 0; j--) { 18 | Entity.set(world, e[j], [B]); 19 | } 20 | } 21 | for (let i = 0; i < qab.length; i++) { 22 | const [e] = qab[i]; 23 | for (let j = e.length - 1; j >= 0; j--) { 24 | Entity.unset(world, e[j], [B]); 25 | } 26 | } 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/harmony-ecs/entity_cycle.js: -------------------------------------------------------------------------------- 1 | import { Entity, Format, Query, Schema, World } from "harmony-ecs"; 2 | 3 | export default (count) => { 4 | const world = World.make(count); 5 | const A = Schema.makeBinary(world, Format.float64); 6 | const B = Schema.makeBinary(world, Format.float64); 7 | const qa = Query.make(world, [A]); 8 | const qb = Query.make(world, [B]); 9 | const type = [B]; 10 | 11 | for (let i = 0; i < count; i++) { 12 | Entity.make(world, [A], [i]); 13 | } 14 | 15 | return () => { 16 | for (let i = 0; i < qa.length; i++) { 17 | const [e, [a]] = qa[i]; 18 | for (let j = 0; j < e.length; j++) { 19 | Entity.make(world, type, [a[j]]); 20 | } 21 | } 22 | for (let i = 0; i < qb.length; i++) { 23 | const [e] = qb[i]; 24 | for (let j = e.length - 1; j >= 0; j--) { 25 | Entity.destroy(world, e[j], B); 26 | } 27 | } 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/harmony-ecs/frag_iter.js: -------------------------------------------------------------------------------- 1 | import { Entity, Format, Query, Schema, World } from "harmony-ecs"; 2 | 3 | export default (count) => { 4 | const COMPS = 26; 5 | const schemas = []; 6 | const world = World.make(count); 7 | 8 | for (let i = 0; i < COMPS; i++) { 9 | schemas.push(Schema.makeBinary(world, Format.int32)); 10 | } 11 | 12 | const Z = schemas[25]; 13 | const Data = Schema.makeBinary(world, Format.int32); 14 | const qdata = Query.make(world, [Data]); 15 | const qz = Query.make(world, [Z]); 16 | 17 | for (let i = 0; i < count; i++) { 18 | for (let j = 0; j < COMPS; j++) { 19 | Entity.make(world, [schemas[j], Data]); 20 | } 21 | } 22 | 23 | return () => { 24 | for (let i = 0; i < qdata.length; i++) { 25 | const [e, [d]] = qdata[i]; 26 | for (let j = 0; j < e.length; j++) { 27 | d[j] *= 2; 28 | } 29 | } 30 | for (let i = 0; i < qz.length; i++) { 31 | const [e, [z]] = qz[i]; 32 | for (let j = 0; j < e.length; j++) { 33 | z[j] *= 2; 34 | } 35 | } 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/harmony-ecs/simple_iter.js: -------------------------------------------------------------------------------- 1 | import { Entity, Format, Query, Schema, World } from "harmony-ecs"; 2 | 3 | export default (count) => { 4 | const world = World.make(count); 5 | const A = Schema.makeBinary(world, Format.float64); 6 | const B = Schema.makeBinary(world, Format.float64); 7 | const C = Schema.makeBinary(world, Format.float64); 8 | const D = Schema.makeBinary(world, Format.float64); 9 | const E = Schema.makeBinary(world, Format.float64); 10 | const qab = Query.make(world, [A, B]); 11 | const qcd = Query.make(world, [C, D]); 12 | const qce = Query.make(world, [C, E]); 13 | 14 | for (let i = 0; i < count; i++) { 15 | Entity.make(world, [A, B], [0, 1]); 16 | Entity.make(world, [A, B, C], [0, 1, 2]); 17 | Entity.make(world, [A, B, C, D], [0, 1, 2, 3]); 18 | Entity.make(world, [A, B, C, E], [0, 1, 2, 4]); 19 | } 20 | 21 | return () => { 22 | for (let i = 0; i < qab.length; i++) { 23 | const [e, [a, b]] = qab[i]; 24 | for (let j = 0; j < e.length; j++) { 25 | const x = a[j]; 26 | a[j] = b[j]; 27 | b[j] = x; 28 | } 29 | } 30 | for (let i = 0; i < qcd.length; i++) { 31 | const [e, [c, d]] = qcd[i]; 32 | for (let j = 0; j < e.length; j++) { 33 | const x = c[j]; 34 | c[j] = d[j]; 35 | d[j] = x; 36 | } 37 | } 38 | for (let i = 0; i < qce.length; i++) { 39 | const [e, [c, _e]] = qce[i]; 40 | for (let j = 0; j < e.length; j++) { 41 | const x = c[j]; 42 | c[j] = _e[j]; 43 | _e[j] = x; 44 | } 45 | } 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/jakeklassen-ecs/add_remove.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { Component, World } from "@jakeklassen/ecs"; 3 | 4 | class A extends Component { 5 | a = true; 6 | 7 | constructor(a = true) { 8 | super(); 9 | 10 | this.a = a; 11 | } 12 | } 13 | 14 | class B extends Component { 15 | b = true; 16 | 17 | constructor(b = true) { 18 | super(); 19 | 20 | this.b = b; 21 | } 22 | } 23 | 24 | /** 25 | * @param {number} count 26 | */ 27 | export default async (count) => { 28 | const ecs = new World(); 29 | 30 | for (let i = 0; i < count; i++) { 31 | ecs.addEntityComponents(ecs.createEntity(), new A()); 32 | } 33 | 34 | return () => { 35 | for (const [entity] of ecs.entities) { 36 | ecs.addEntityComponents(entity, new B()); 37 | } 38 | 39 | for (const [entity] of ecs.entities) { 40 | ecs.removeEntityComponents(entity, B); 41 | } 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/jakeklassen-ecs/entity_cycle.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { Component, World } from "@jakeklassen/ecs"; 4 | 5 | class A extends Component { 6 | a = 1; 7 | 8 | constructor(a = 1) { 9 | super(); 10 | 11 | this.a = a; 12 | } 13 | } 14 | 15 | class B extends Component { 16 | b = 1; 17 | 18 | constructor(b = 1) { 19 | super(); 20 | 21 | this.b = b; 22 | } 23 | } 24 | 25 | /** 26 | * @param {number} count 27 | */ 28 | export default (count) => { 29 | const ecs = new World(); 30 | 31 | for (let i = 0; i < count; i++) { 32 | ecs.addEntityComponents(ecs.createEntity(), new A()); 33 | } 34 | 35 | const withA = ecs.view(A); 36 | const withB = ecs.view(B); 37 | 38 | return () => { 39 | for (const [] of withA) { 40 | ecs.addEntityComponents(ecs.createEntity(), new B()); 41 | } 42 | 43 | for (const [entity] of withB) { 44 | ecs.deleteEntity(entity); 45 | } 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/jakeklassen-ecs/frag_iter.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { Component, System, World } from "@jakeklassen/ecs"; 3 | 4 | const COMPS = Array.from( 5 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 6 | () => 7 | class extends Component { 8 | value = 0; 9 | 10 | constructor(value = 0) { 11 | super(); 12 | 13 | this.value = value; 14 | } 15 | }, 16 | ); 17 | 18 | class Data extends Component { 19 | value = 0; 20 | 21 | constructor(value = 0) { 22 | super(); 23 | 24 | this.value = value; 25 | } 26 | } 27 | 28 | class DataSystem extends System { 29 | /** 30 | * 31 | * @param {World} world 32 | */ 33 | update(world) { 34 | for (const [entity, components] of world.view(Data)) { 35 | components.get(Data).value *= 2; 36 | } 37 | } 38 | } 39 | 40 | class ZSystem extends System { 41 | /** 42 | * 43 | * @param {World} world 44 | */ 45 | update(world) { 46 | for (const [entity, components] of world.view(COMPS[25])) { 47 | components.get(COMPS[25]).value *= 2; 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * @param {number} count 54 | */ 55 | export default (count) => { 56 | let ecs = new World(); 57 | 58 | ecs.addSystem(new DataSystem()); 59 | ecs.addSystem(new ZSystem()); 60 | 61 | for (let i = 0; i < count; i++) { 62 | for (let Comp of COMPS) { 63 | ecs.addEntityComponents(ecs.createEntity(), new Comp(0), new Data(0)); 64 | } 65 | } 66 | 67 | return () => { 68 | ecs.update(0); 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/javelin-ecs/add_remove.js: -------------------------------------------------------------------------------- 1 | import { createComponentType, createWorld, number, query } from "@javelin/ecs"; 2 | 3 | const A = createComponentType({ 4 | type: 0, 5 | schema: {}, 6 | }); 7 | 8 | const B = createComponentType({ 9 | type: 1, 10 | schema: {}, 11 | }); 12 | 13 | export default (count) => { 14 | const world = createWorld(); 15 | 16 | const qa = query(A).not(B); 17 | const qb = query(B); 18 | 19 | world.addSystem((world) => { 20 | for (const [entities] of qa) { 21 | for (let i = 0; i < entities.length; i++) { 22 | world.attach(entities[i], world.component(B)); 23 | } 24 | } 25 | }); 26 | 27 | world.addSystem((world) => { 28 | for (const [entities, [b]] of qb) { 29 | for (let i = 0; i < entities.length; i++) { 30 | world.detach(entities[i], b[i]); 31 | } 32 | } 33 | }); 34 | 35 | for (let i = 0; i < count; i++) { 36 | world.spawn(world.component(A)); 37 | } 38 | 39 | return world.tick; 40 | }; 41 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/javelin-ecs/entity_cycle.js: -------------------------------------------------------------------------------- 1 | import { createComponentType, createWorld, number, query } from "@javelin/ecs"; 2 | 3 | const initialize = (c, v) => (c.value = v); 4 | 5 | const A = createComponentType({ 6 | type: 0, 7 | schema: { 8 | value: number, 9 | }, 10 | initialize, 11 | }); 12 | 13 | const B = createComponentType({ 14 | type: 1, 15 | schema: { 16 | value: number, 17 | }, 18 | initialize, 19 | }); 20 | 21 | export default (count) => { 22 | const world = createWorld(); 23 | 24 | for (let i = 0; i < count; i++) { 25 | world.spawn(world.component(A, i)); 26 | } 27 | 28 | const qa = query(A); 29 | const qb = query(B); 30 | 31 | world.addSystem((world) => { 32 | for (const [entities, [a]] of qa) { 33 | for (let i = 0; i < entities.length; i++) { 34 | world.spawn(world.component(B, a[i].value)); 35 | } 36 | } 37 | }); 38 | 39 | world.addSystem((world) => { 40 | for (const [entities] of qb) { 41 | entities.forEach(world.destroy); 42 | } 43 | }); 44 | 45 | return world.tick; 46 | }; 47 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/javelin-ecs/frag_iter.js: -------------------------------------------------------------------------------- 1 | import { createComponentType, createWorld, number, query } from "@javelin/ecs"; 2 | 3 | const COMPS = 26; 4 | const componentTypes = []; 5 | const initialize = (c, v) => (c.value = v); 6 | 7 | for (let i = 0; i < COMPS; i++) { 8 | componentTypes.push( 9 | createComponentType({ 10 | type: i, 11 | schema: { value: number }, 12 | initialize, 13 | }), 14 | ); 15 | } 16 | 17 | const Data = createComponentType({ 18 | type: 26, 19 | schema: { 20 | value: number, 21 | }, 22 | }); 23 | 24 | export default (count) => { 25 | const world = createWorld(); 26 | const qd = query(Data); 27 | const qz = query(componentTypes[25]); 28 | 29 | world.addSystem(() => { 30 | for (const [entities, [d]] of qd) { 31 | for (let i = 0; i < entities.length; i++) { 32 | d[i].value *= 2; 33 | } 34 | } 35 | }); 36 | 37 | world.addSystem(() => { 38 | for (const [entities, [z]] of qz) { 39 | for (let i = 0; i < entities.length; i++) { 40 | z[i].value *= 2; 41 | } 42 | } 43 | }); 44 | 45 | for (let i = 0; i < count; i++) { 46 | for (let i = 0; i < COMPS; i++) { 47 | world.spawn( 48 | world.component(componentTypes[i], 0), 49 | world.component(Data, 0), 50 | ); 51 | } 52 | } 53 | 54 | return world.tick; 55 | }; 56 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/miniplex/add_remove.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { World } from "miniplex"; 3 | 4 | export default async (count) => { 5 | const ecs = new World(); 6 | 7 | for (let i = 0; i < count; i++) { 8 | ecs.add({ A: true }); 9 | } 10 | 11 | return () => { 12 | for (const entity of ecs.entities) { 13 | ecs.addComponent(entity, "B", true); 14 | } 15 | 16 | for (const entity of ecs.entities) { 17 | ecs.removeComponent(entity, "B"); 18 | } 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/miniplex/entity_cycle.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { World } from "miniplex"; 3 | 4 | export default (count) => { 5 | const ecs = new World(); 6 | 7 | for (let i = 0; i < count; i++) { 8 | ecs.add({ A: 1 }); 9 | } 10 | 11 | const withA = ecs.with("A"); 12 | const withB = ecs.with("B"); 13 | 14 | return () => { 15 | for (const entity of withA.entities) { 16 | ecs.add({ B: 1 }); 17 | } 18 | 19 | for (let i = withB.entities.length; i > 0; i--) { 20 | const entity = withB.entities[i - 1]; 21 | ecs.remove(entity); 22 | } 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/miniplex/frag_iter.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { World } from "miniplex"; 3 | 4 | export default async (count) => { 5 | const ecs = new World(); 6 | 7 | Array.from("ABCDEFGHIJKLMNOPQRSTUVWXYZ").forEach((component) => { 8 | for (let i = 0; i < count; i++) { 9 | ecs.add({ [component]: 1, Data: 1 }); 10 | } 11 | }); 12 | 13 | const withZ = ecs.with("Z"); 14 | const withData = ecs.with("Data"); 15 | 16 | return () => { 17 | for (const entity of withZ.entities) { 18 | entity.Z *= 2; 19 | } 20 | 21 | for (const entity of withData.entities) { 22 | entity.Data *= 2; 23 | } 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/miniplex/packed_5.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { World } from "miniplex"; 3 | 4 | export default async (count) => { 5 | const ecs = new World(); 6 | 7 | for (let i = 0; i < count; i++) { 8 | ecs.add({ A: 1, B: 1, C: 1, D: 1, E: 1 }); 9 | } 10 | 11 | const withA = ecs.with("A"); 12 | const withB = ecs.with("B"); 13 | const withC = ecs.with("C"); 14 | const withD = ecs.with("D"); 15 | const withE = ecs.with("E"); 16 | 17 | return () => { 18 | for (const entity of withA.entities) { 19 | entity.A *= 2; 20 | } 21 | 22 | for (const entity of withB.entities) { 23 | entity.B *= 2; 24 | } 25 | 26 | for (const entity of withC.entities) { 27 | entity.C *= 2; 28 | } 29 | 30 | for (const entity of withD.entities) { 31 | entity.D *= 2; 32 | } 33 | 34 | for (const entity of withE.entities) { 35 | entity.E *= 2; 36 | } 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/miniplex/simple_iter.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { World } from "miniplex"; 3 | 4 | export default async (count) => { 5 | const ecs = new World(); 6 | 7 | for (let i = 0; i < count; i++) { 8 | ecs.add({ A: 1, B: 1 }); 9 | } 10 | 11 | for (let i = 0; i < count; i++) { 12 | ecs.add({ A: 1, B: 1, C: 1 }); 13 | } 14 | 15 | for (let i = 0; i < count; i++) { 16 | ecs.add({ A: 1, B: 1, C: 1, D: 1 }); 17 | } 18 | 19 | for (let i = 0; i < count; i++) { 20 | ecs.add({ A: 1, B: 1, C: 1, E: 1 }); 21 | } 22 | 23 | const withAB = ecs.with("A", "B"); 24 | const withCD = ecs.with("C", "D"); 25 | const withCE = ecs.with("C", "E"); 26 | 27 | return () => { 28 | for (const entity of withAB.entities) { 29 | const temp = entity.A; 30 | entity.A = entity.B; 31 | entity.B = temp; 32 | } 33 | 34 | for (const entity of withCD.entities) { 35 | const temp = entity.C; 36 | entity.C = entity.D; 37 | entity.D = temp; 38 | } 39 | 40 | for (const entity of withCE.entities) { 41 | const temp = entity.C; 42 | entity.C = entity.E; 43 | entity.E = temp; 44 | } 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/objecs/add_remove.js: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | 3 | /** 4 | * @typedef {Object} Entity 5 | * @property {boolean} [A] 6 | * @property {boolean} [B] 7 | */ 8 | 9 | /** 10 | * @param {number} count 11 | */ 12 | export default async (count) => { 13 | /** 14 | * @type {World} 15 | */ 16 | const ecs = new World(); 17 | 18 | for (let i = 0; i < count; i++) { 19 | ecs.createEntity({ 20 | A: true, 21 | }); 22 | } 23 | 24 | return () => { 25 | for (const entity of ecs.entities) { 26 | ecs.addEntityComponents(entity, "B", true); 27 | } 28 | 29 | for (const entity of ecs.entities) { 30 | ecs.removeEntityComponents(entity, "B"); 31 | } 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/objecs/entity_cycle.js: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | 3 | /** 4 | * @typedef {Object} Entity 5 | * @property {number} [A] 6 | * @property {number} [B] 7 | */ 8 | 9 | /** 10 | * @param {number} count 11 | */ 12 | export default (count) => { 13 | /** 14 | * @type {World} 15 | */ 16 | const ecs = new World(); 17 | 18 | for (let i = 0; i < count; i++) { 19 | ecs.createEntity({ A: 1 }); 20 | } 21 | 22 | const withA = ecs.archetype("A"); 23 | const withB = ecs.archetype("B"); 24 | 25 | return () => { 26 | for (const _entity of withA.entities) { 27 | ecs.createEntity({ B: 1 }); 28 | } 29 | 30 | for (const entity of withB.entities) { 31 | ecs.deleteEntity(entity); 32 | } 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/objecs/frag_iter.js: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | 3 | /** 4 | * @typedef {Object} Entity 5 | * @property {number} [Z] 6 | * @property {number} [Data] 7 | */ 8 | 9 | /** 10 | * @param {number} count 11 | */ 12 | export default (count) => { 13 | /** 14 | * @type {World} 15 | */ 16 | const ecs = new World(); 17 | 18 | Array.from("ABCDEFGHIJKLMNOPQRSTUVWXYZ").forEach((component) => { 19 | for (let i = 0; i < count; i++) { 20 | ecs.createEntity({ [component]: 1, Data: 1 }); 21 | } 22 | }); 23 | 24 | const withZ = ecs.archetype("Z"); 25 | const withData = ecs.archetype("Data"); 26 | 27 | return () => { 28 | for (const entity of withZ.entities) { 29 | entity.Z *= 2; 30 | } 31 | 32 | for (const entity of withData.entities) { 33 | entity.Data *= 2; 34 | } 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/objecs/packed_5.js: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | 3 | /** 4 | * @typedef {Object} Entity 5 | * @property {number} [A] 6 | * @property {number} [B] 7 | * @property {number} [C] 8 | * @property {number} [D] 9 | * @property {number} [E] 10 | */ 11 | 12 | /** 13 | * @param {number} count 14 | */ 15 | export default (count) => { 16 | /** 17 | * @type {World} 18 | * 19 | */ 20 | let ecs = new World(); 21 | 22 | for (let i = 0; i < count; i++) { 23 | ecs.createEntity({ A: 1, B: 1, C: 1, D: 1, E: 1 }); 24 | } 25 | 26 | const withA = ecs.archetype("A"); 27 | const withB = ecs.archetype("B"); 28 | const withC = ecs.archetype("C"); 29 | const withD = ecs.archetype("D"); 30 | const withE = ecs.archetype("E"); 31 | 32 | return () => { 33 | for (const entity of withA.entities) { 34 | entity.A *= 2; 35 | } 36 | 37 | for (const entity of withB.entities) { 38 | entity.B *= 2; 39 | } 40 | 41 | for (const entity of withC.entities) { 42 | entity.C *= 2; 43 | } 44 | 45 | for (const entity of withD.entities) { 46 | entity.D *= 2; 47 | } 48 | 49 | for (const entity of withE.entities) { 50 | entity.E *= 2; 51 | } 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/objecs/simple_iter.js: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | 3 | /** 4 | * @typedef {Object} Entity 5 | * @property {number} [A] 6 | * @property {number} [B] 7 | * @property {number} [C] 8 | * @property {number} [D] 9 | * @property {number} [E] 10 | */ 11 | 12 | /** 13 | * @param {number} count 14 | */ 15 | export default (count) => { 16 | /** 17 | * @type {World} 18 | */ 19 | const ecs = new World(); 20 | 21 | for (let i = 0; i < count; i++) { 22 | ecs.createEntity({ A: 1, B: 1 }); 23 | } 24 | 25 | for (let i = 0; i < count; i++) { 26 | ecs.createEntity({ A: 1, B: 1, C: 1 }); 27 | } 28 | 29 | for (let i = 0; i < count; i++) { 30 | ecs.createEntity({ A: 1, B: 1, C: 1, D: 1 }); 31 | } 32 | 33 | for (let i = 0; i < count; i++) { 34 | ecs.createEntity({ A: 1, B: 1, C: 1, E: 1 }); 35 | } 36 | 37 | const withAB = ecs.archetype("A", "B"); 38 | const withCD = ecs.archetype("C", "D"); 39 | const withCE = ecs.archetype("C", "E"); 40 | 41 | return () => { 42 | for (const entity of withAB.entities) { 43 | const temp = entity.A; 44 | entity.A = entity.B; 45 | entity.B = temp; 46 | } 47 | 48 | for (const entity of withCD.entities) { 49 | const temp = entity.C; 50 | entity.C = entity.D; 51 | entity.D = temp; 52 | } 53 | 54 | for (const entity of withCE.entities) { 55 | const temp = entity.C; 56 | entity.C = entity.E; 57 | entity.E = temp; 58 | } 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/perform-ecs/add_remove.js: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ECS, 4 | EntityViewFactory, 5 | makeComponent, 6 | System, 7 | } from "perform-ecs"; 8 | 9 | class A extends Component { 10 | reset(obj, a) { 11 | obj.a = a; 12 | } 13 | } 14 | 15 | class B extends Component { 16 | reset(obj, b) { 17 | obj.b = b; 18 | } 19 | } 20 | 21 | makeComponent(A); 22 | makeComponent(B); 23 | 24 | class AddB extends System { 25 | view = EntityViewFactory.createView({ 26 | components: [A], 27 | }); 28 | 29 | update() { 30 | for (let entity of this.view.entities) { 31 | this.ecs.addComponentsToEntity(entity, [{ component: B, args: [0] }]); 32 | } 33 | } 34 | } 35 | 36 | class RemoveB extends System { 37 | view = EntityViewFactory.createView({ 38 | components: [B], 39 | }); 40 | 41 | update() { 42 | for (let entity of this.view.entities) { 43 | this.ecs.removeComponentsFromEntity(entity, B); 44 | } 45 | } 46 | } 47 | 48 | export default (count) => { 49 | let ecs = new ECS(); 50 | 51 | ecs.registerSystem(new AddB()); 52 | ecs.registerSystem(new RemoveB()); 53 | 54 | for (let i = 0; i < count; i++) { 55 | ecs.createEntity([{ component: A, args: [0] }]); 56 | } 57 | 58 | return () => { 59 | ecs.update(0); 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/perform-ecs/entity_cycle.js: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ECS, 4 | EntityViewFactory, 5 | makeComponent, 6 | System, 7 | } from "perform-ecs"; 8 | 9 | class A extends Component { 10 | reset(obj, a) { 11 | obj.a = a; 12 | } 13 | } 14 | 15 | class B extends Component { 16 | reset(obj, b) { 17 | obj.b = b; 18 | } 19 | } 20 | 21 | makeComponent(A); 22 | makeComponent(B); 23 | 24 | class SpawnB extends System { 25 | view = EntityViewFactory.createView({ 26 | components: [A], 27 | }); 28 | 29 | update() { 30 | for (let entity of this.view.entities) { 31 | this.ecs.createEntity([{ component: B, args: [entity.a] }]); 32 | } 33 | } 34 | } 35 | 36 | class KillB extends System { 37 | view = EntityViewFactory.createView({ 38 | components: [B], 39 | }); 40 | 41 | update() { 42 | for (let entity of this.view.entities) { 43 | this.ecs.removeEntity(entity); 44 | } 45 | } 46 | } 47 | 48 | export default (count) => { 49 | let ecs = new ECS(); 50 | 51 | ecs.registerSystem(new SpawnB()); 52 | ecs.registerSystem(new KillB()); 53 | 54 | for (let i = 0; i < count; i++) { 55 | ecs.createEntity([{ component: A, args: [i] }]); 56 | } 57 | 58 | return () => { 59 | ecs.update(0); 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/perform-ecs/frag_iter.js: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ECS, 4 | EntityViewFactory, 5 | makeComponent, 6 | System, 7 | } from "perform-ecs"; 8 | 9 | const COMPS = Array.from( 10 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 11 | (name) => 12 | class extends Component { 13 | reset(obj, value) { 14 | obj[name] = value; 15 | } 16 | }, 17 | ); 18 | 19 | class Data extends Component { 20 | reset(obj, value) { 21 | obj.data = value; 22 | } 23 | } 24 | 25 | for (let Comp of COMPS) { 26 | makeComponent(Comp); 27 | } 28 | 29 | makeComponent(Data); 30 | 31 | class DataSystem extends System { 32 | view = EntityViewFactory.createView({ 33 | components: [Data], 34 | }); 35 | 36 | update() { 37 | for (let entity of this.view.entities) { 38 | entity.data *= 2; 39 | } 40 | } 41 | } 42 | 43 | class ZSystem extends System { 44 | view = EntityViewFactory.createView({ 45 | components: [COMPS[25]], 46 | }); 47 | 48 | update() { 49 | for (let entity of this.view.entities) { 50 | entity.z *= 2; 51 | } 52 | } 53 | } 54 | 55 | export default (count) => { 56 | let ecs = new ECS(); 57 | 58 | ecs.registerSystem(new DataSystem()); 59 | ecs.registerSystem(new ZSystem()); 60 | 61 | for (let i = 0; i < count; i++) { 62 | for (let Comp of COMPS) { 63 | ecs.createEntity([ 64 | { component: Comp, args: [0] }, 65 | { component: Data, args: [0] }, 66 | ]); 67 | } 68 | } 69 | 70 | return () => { 71 | ecs.update(0); 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/picoes/add_remove.js: -------------------------------------------------------------------------------- 1 | import { World } from "picoes"; 2 | 3 | export default (count) => { 4 | let world = new World(); 5 | 6 | for (let i = 0; i < count; i++) { 7 | world.entity().set("a", 0); 8 | } 9 | 10 | return () => { 11 | world.each("a", (_, entity) => { 12 | entity.set("b", 0); 13 | }); 14 | 15 | world.each("b", (_, entity) => { 16 | entity.remove("b"); 17 | }); 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/picoes/entity_cycle.js: -------------------------------------------------------------------------------- 1 | import { World } from "picoes"; 2 | 3 | function Box(value = 0) { 4 | this.value = value; 5 | } 6 | 7 | export default (count) => { 8 | let world = new World(); 9 | 10 | world.component("a", Box); 11 | world.component("b", Box); 12 | 13 | for (let i = 0; i < count; i++) { 14 | world.entity().set("a", i); 15 | } 16 | 17 | return () => { 18 | world.each("a", (a) => { 19 | world.entity().set("b", a.value); 20 | }); 21 | 22 | world.each("b", (_, entity) => { 23 | entity.destroy(); 24 | }); 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/picoes/frag_iter.js: -------------------------------------------------------------------------------- 1 | import { World } from "picoes"; 2 | 3 | const COMPS = "abcdefghijklmnopqrstuvwxyz"; 4 | 5 | function Box(value = 0) { 6 | this.value = value; 7 | } 8 | 9 | function Data(value = 0) { 10 | this.value = value; 11 | } 12 | 13 | export default (count) => { 14 | let world = new World(); 15 | 16 | for (let comp of COMPS) { 17 | world.component(comp, Box); 18 | } 19 | 20 | world.component("data", Data); 21 | 22 | for (let i = 0; i < count; i++) { 23 | for (let comp of COMPS) { 24 | world.entity().set(comp, 0).set("data", 0); 25 | } 26 | } 27 | 28 | return () => { 29 | world.each("data", ({ data }) => { 30 | data.value *= 2; 31 | }); 32 | 33 | world.each("z", ({ z }) => { 34 | z.value *= 2; 35 | }); 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/picoes/packed_5.js: -------------------------------------------------------------------------------- 1 | import { World } from "picoes"; 2 | 3 | function Box(value = 0) { 4 | this.value = value; 5 | } 6 | 7 | export default (count) => { 8 | let world = new World(); 9 | 10 | world.component("a", Box); 11 | world.component("b", Box); 12 | world.component("c", Box); 13 | world.component("d", Box); 14 | world.component("e", Box); 15 | 16 | for (let i = 0; i < count; i++) { 17 | world.entity().set("a", 0).set("b", 0).set("c", 0).set("d", 0).set("e", 0); 18 | } 19 | 20 | return () => { 21 | world.each("a", ({ a }) => { 22 | a.value *= 2; 23 | }); 24 | 25 | world.each("b", ({ b }) => { 26 | b.value *= 2; 27 | }); 28 | 29 | world.each("c", ({ c }) => { 30 | c.value *= 2; 31 | }); 32 | 33 | world.each("d", ({ d }) => { 34 | d.value *= 2; 35 | }); 36 | 37 | world.each("e", ({ e }) => { 38 | e.value *= 2; 39 | }); 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/picoes/simple_iter.js: -------------------------------------------------------------------------------- 1 | import { World } from "picoes"; 2 | 3 | function Box(value = 0) { 4 | this.value = value; 5 | } 6 | 7 | export default (count) => { 8 | let world = new World(); 9 | 10 | world.component("a", Box); 11 | world.component("b", Box); 12 | world.component("c", Box); 13 | world.component("d", Box); 14 | world.component("e", Box); 15 | 16 | for (let i = 0; i < count; i++) { 17 | world.entity().set("a", 0).set("b", 1); 18 | world.entity().set("a", 0).set("b", 1).set("c", 2); 19 | world.entity().set("a", 0).set("b", 1).set("c", 2).set("d", 3); 20 | world.entity().set("a", 0).set("b", 1).set("c", 2).set("e", 4); 21 | } 22 | 23 | return () => { 24 | world.each("a", "b", ({ a, b }) => { 25 | let x = a.value; 26 | a.value = b.value; 27 | b.value = x; 28 | }); 29 | 30 | world.each("c", "d", ({ c, d }) => { 31 | let x = c.value; 32 | c.value = d.value; 33 | d.value = x; 34 | }); 35 | 36 | world.each("c", "e", ({ c, e }) => { 37 | let x = c.value; 38 | c.value = e.value; 39 | e.value = x; 40 | }); 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/piecs/add_remove.js: -------------------------------------------------------------------------------- 1 | import { createEntitySystem, World } from "piecs/dist/index.mjs"; 2 | 3 | export default function createAddRemove(count) { 4 | const world = new World(); 5 | const A = world.createComponentId(); 6 | const B = world.createComponentId(); 7 | const prefabA = world.prefabricate([A]); 8 | 9 | world 10 | .registerSystem( 11 | createEntitySystem( 12 | function addB(entities, world) { 13 | for (let i = entities.length - 1; i >= 0; i--) { 14 | world.addComponent(entities[i], B); 15 | } 16 | }, 17 | (q) => q.every(A), 18 | ), 19 | ) 20 | .registerSystem( 21 | createEntitySystem( 22 | function removeB(entities, world) { 23 | for (let i = entities.length - 1; i >= 0; i--) { 24 | world.removeComponent(entities[i], B); 25 | } 26 | }, 27 | (q) => q.every(B), 28 | ), 29 | ) 30 | .initialize(); 31 | 32 | for (let i = 0; i < count; i++) { 33 | world.createEntity(prefabA); 34 | } 35 | 36 | return function addRemove() { 37 | world.update(); 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/piecs/entity_cycle.js: -------------------------------------------------------------------------------- 1 | import { createEntitySystem, World } from "piecs/dist/index.mjs"; 2 | 3 | export default function createEntityCycle(count) { 4 | const world = new World(); 5 | const A = world.createComponentId(); 6 | const B = world.createComponentId(); 7 | 8 | const prefabA = world.prefabricate([A]); 9 | const prefabB = world.prefabricate([B]); 10 | 11 | world 12 | .registerSystem( 13 | createEntitySystem( 14 | function spawnBs(entities, world) { 15 | for (let i = 0, l = entities.length; i < l; i++) { 16 | world.createEntity(prefabB); 17 | } 18 | }, 19 | (q) => q.every(A), 20 | ), 21 | ) 22 | .registerSystem( 23 | createEntitySystem( 24 | function deleteBs(entities, world) { 25 | for (let i = entities.length - 1; i >= 0; i--) { 26 | world.deleteEntity(entities[i]); 27 | } 28 | }, 29 | (q) => q.every(B), 30 | ), 31 | ) 32 | .initialize(); 33 | 34 | for (let i = 0; i < count; i++) { 35 | world.createEntity(prefabA); 36 | } 37 | 38 | return function entityCycle() { 39 | world.update(); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/piecs/frag_iter.js: -------------------------------------------------------------------------------- 1 | import { World, createEntitySystem } from "piecs/dist/index.mjs"; 2 | 3 | export default function createFragIter(count) { 4 | const world = new World(); 5 | 6 | const components = []; 7 | for (let i = 0; i < 26; i++) { 8 | components.push(world.createComponentId()); 9 | } 10 | const Z = { 11 | id: components[25], 12 | arr: new Int32Array(count), 13 | }; 14 | const Data = { 15 | id: world.createComponentId(), 16 | arr: new Int32Array(count * 26), 17 | }; 18 | 19 | const prefabs = components.map((c) => world.prefabricate([Data, c])); 20 | 21 | world 22 | .registerSystem( 23 | createEntitySystem( 24 | function dataSystem(entities) { 25 | for (let i = 0, l = entities.length; i < l; i++) { 26 | Data.arr[entities[i]] *= 2; 27 | } 28 | }, 29 | (q) => q.every(Data), 30 | ), 31 | ) 32 | .registerSystem( 33 | createEntitySystem( 34 | function zSystem(entities) { 35 | for (let i = 0, l = entities.length; i < l; i++) { 36 | Z.arr[entities[i]] *= 2; 37 | } 38 | }, 39 | (q) => q.every(Z), 40 | ), 41 | ) 42 | .initialize(); 43 | 44 | for (let i = 0; i < count; i++) { 45 | for (const prefab of prefabs) { 46 | world.createEntity(prefab); 47 | } 48 | } 49 | 50 | return function fragIter() { 51 | world.update(); 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/tiny-ecs/add_remove.js: -------------------------------------------------------------------------------- 1 | import pkg from "tiny-ecs"; 2 | 3 | const { EntityManager } = pkg; 4 | 5 | function A() {} 6 | function B() {} 7 | 8 | export default (count) => { 9 | let ecs = new EntityManager(); 10 | 11 | for (let i = 0; i < count; i++) { 12 | ecs.createEntity().addComponent(A); 13 | } 14 | 15 | return () => { 16 | for (let entity of ecs.queryComponents([A])) { 17 | entity.addComponent(B); 18 | } 19 | 20 | for (let entity of ecs.queryComponents([B])) { 21 | entity.removeComponent(B); 22 | } 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/tiny-ecs/entity_cycle.js: -------------------------------------------------------------------------------- 1 | import pkg from "tiny-ecs"; 2 | 3 | const { EntityManager } = pkg; 4 | 5 | function A(value) { 6 | this.value = value; 7 | } 8 | 9 | function B(value) { 10 | this.value = value; 11 | } 12 | 13 | export default (count) => { 14 | let ecs = new EntityManager(); 15 | 16 | for (let i = 0; i < count; i++) { 17 | ecs.createEntity().addComponent(A); 18 | } 19 | 20 | return () => { 21 | for (let entity of ecs.queryComponents([A])) { 22 | ecs.createEntity().addComponent(B).value = entity.a.value; 23 | } 24 | 25 | for (let entity of ecs.queryComponents([B])) { 26 | entity.remove(); 27 | } 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/tiny-ecs/frag_iter.js: -------------------------------------------------------------------------------- 1 | import pkg from "tiny-ecs"; 2 | 3 | const { EntityManager } = pkg; 4 | 5 | const COMPS = Array.from("ABCDEFGHIJKLMNOPQRSTUVWXYZ", (name) => 6 | Function(` 7 | return function ${name} (value = 0) { 8 | this.value = value; 9 | } 10 | `)(), 11 | ); 12 | 13 | const Z = COMPS[25]; 14 | 15 | function Data(value = 0) { 16 | this.value = value; 17 | } 18 | 19 | export default (count) => { 20 | let ecs = new EntityManager(); 21 | 22 | for (let i = 0; i < count; i++) { 23 | for (let Comp of COMPS) { 24 | ecs.createEntity().addComponent(Data).addComponent(Comp); 25 | } 26 | } 27 | 28 | return () => { 29 | for (let entity of ecs.queryComponents([Data])) { 30 | entity.data.value *= 2; 31 | } 32 | for (let entity of ecs.queryComponents([Z])) { 33 | entity.z.value *= 2; 34 | } 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/tiny-ecs/packed_5.js: -------------------------------------------------------------------------------- 1 | import pkg from "tiny-ecs"; 2 | 3 | const { EntityManager } = pkg; 4 | 5 | function A(value) { 6 | this.value = value; 7 | } 8 | 9 | function B(value) { 10 | this.value = value; 11 | } 12 | 13 | function C(value) { 14 | this.value = value; 15 | } 16 | 17 | function D(value) { 18 | this.value = value; 19 | } 20 | 21 | function E(value) { 22 | this.value = value; 23 | } 24 | 25 | export default (count) => { 26 | let ecs = new EntityManager(); 27 | 28 | for (let i = 0; i < count; i++) { 29 | ecs 30 | .createEntity() 31 | .addComponent(A) 32 | .addComponent(B) 33 | .addComponent(C) 34 | .addComponent(D) 35 | .addComponent(E); 36 | } 37 | 38 | return () => { 39 | for (let entity of ecs.queryComponents([A])) { 40 | entity.a.value *= 2; 41 | } 42 | 43 | for (let entity of ecs.queryComponents([B])) { 44 | entity.b.value *= 2; 45 | } 46 | 47 | for (let entity of ecs.queryComponents([C])) { 48 | entity.c.value *= 2; 49 | } 50 | 51 | for (let entity of ecs.queryComponents([D])) { 52 | entity.d.value *= 2; 53 | } 54 | 55 | for (let entity of ecs.queryComponents([E])) { 56 | entity.e.value *= 2; 57 | } 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/uecs/add_remove.js: -------------------------------------------------------------------------------- 1 | import { World } from "uecs"; 2 | 3 | function A() {} 4 | function B() {} 5 | 6 | export default (count) => { 7 | let world = new World(); 8 | 9 | for (let i = 0; i < count; i++) { 10 | world.create(new A()); 11 | } 12 | 13 | return () => { 14 | world.view(A).each((entity) => { 15 | world.emplace(entity, new B()); 16 | }); 17 | 18 | world.view(B).each((entity) => { 19 | world.remove(entity, B); 20 | }); 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/uecs/entity_cycle.js: -------------------------------------------------------------------------------- 1 | import { World } from "uecs"; 2 | 3 | function A(value) { 4 | this.value = value; 5 | } 6 | 7 | function B(value) { 8 | this.value = value; 9 | } 10 | 11 | export default (count) => { 12 | let world = new World(); 13 | 14 | for (let i = 0; i < count; i++) { 15 | world.create(new A(0)); 16 | } 17 | 18 | return () => { 19 | world.view(A).each((entity, a) => { 20 | world.create(new B(a.value)); 21 | }); 22 | 23 | world.view(B).each((entity) => { 24 | world.destroy(entity); 25 | }); 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/uecs/frag_iter.js: -------------------------------------------------------------------------------- 1 | import { World } from "uecs"; 2 | 3 | const COMPS = Array.from("ABCDEFGHIJKLMNOPQRSTUVWXYZ", (name) => 4 | Function(` 5 | return function ${name} (value) { 6 | this.value = value; 7 | } 8 | `)(), 9 | ); 10 | 11 | const Z = COMPS[25]; 12 | 13 | function Data(value = 0) { 14 | this.value = value; 15 | } 16 | 17 | export default (count) => { 18 | let world = new World(); 19 | 20 | for (let i = 0; i < count; i++) { 21 | for (let Comp of COMPS) { 22 | world.create(new Data(0), new Comp(0)); 23 | } 24 | } 25 | 26 | return () => { 27 | world.view(Data).each((entity, data) => { 28 | data.value *= 2; 29 | }); 30 | world.view(Z).each((entity, z) => { 31 | z.value *= 2; 32 | }); 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/uecs/packed_5.js: -------------------------------------------------------------------------------- 1 | import { World } from "uecs"; 2 | 3 | function A(value) { 4 | this.value = value; 5 | } 6 | 7 | function B(value) { 8 | this.value = value; 9 | } 10 | 11 | function C(value) { 12 | this.value = value; 13 | } 14 | 15 | function D(value) { 16 | this.value = value; 17 | } 18 | 19 | function E(value) { 20 | this.value = value; 21 | } 22 | 23 | export default (count) => { 24 | let world = new World(); 25 | 26 | for (let i = 0; i < count; i++) { 27 | world.create(new A(0), new B(0), new C(0), new D(0), new E(0)); 28 | } 29 | 30 | return () => { 31 | world.view(A).each((entity, a) => { 32 | a.value *= 2; 33 | }); 34 | 35 | world.view(B).each((entity, b) => { 36 | b.value *= 2; 37 | }); 38 | 39 | world.view(C).each((entity, c) => { 40 | c.value *= 2; 41 | }); 42 | 43 | world.view(D).each((entity, d) => { 44 | d.value *= 2; 45 | }); 46 | 47 | world.view(E).each((entity, e) => { 48 | e.value *= 2; 49 | }); 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/uecs/simple_iter.js: -------------------------------------------------------------------------------- 1 | import { World } from "uecs"; 2 | 3 | function A(value) { 4 | this.value = value; 5 | } 6 | 7 | function B(value) { 8 | this.value = value; 9 | } 10 | 11 | function C(value) { 12 | this.value = value; 13 | } 14 | 15 | function D(value) { 16 | this.value = value; 17 | } 18 | 19 | function E(value) { 20 | this.value = value; 21 | } 22 | 23 | export default (count) => { 24 | let world = new World(); 25 | 26 | for (let i = 0; i < count; i++) { 27 | world.create(new A(0), new B(1)); 28 | world.create(new A(0), new B(1), new C(2)); 29 | world.create(new A(0), new B(1), new C(2), new D(3)); 30 | world.create(new A(0), new B(1), new C(2), new E(4)); 31 | } 32 | 33 | return () => { 34 | world.view(A, B).each((entity, a, b) => { 35 | let x = a.value; 36 | a.value = b.value; 37 | b.value = x; 38 | }); 39 | 40 | world.view(C, D).each((entity, c, d) => { 41 | let x = c.value; 42 | c.value = d.value; 43 | d.value = x; 44 | }); 45 | 46 | world.view(C, E).each((entity, c, e) => { 47 | let x = c.value; 48 | c.value = e.value; 49 | e.value = x; 50 | }); 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/wolf-ecs/add_remove.js: -------------------------------------------------------------------------------- 1 | import { ECS } from "wolf-ecs"; 2 | 3 | export default (count) => { 4 | const ecs = new ECS(); 5 | 6 | const A = ecs.defineComponent(); 7 | const B = ecs.defineComponent(); 8 | 9 | const qA = ecs.createQuery(A); 10 | function add(lB) { 11 | for (let i = 0; i < qA.length; i++) { 12 | const arch = qA[i]; 13 | for (let j = arch.length - 1; j >= 0; j--) { 14 | ecs.addComponent(arch[j], lB); 15 | } 16 | } 17 | } 18 | 19 | const qB = ecs.createQuery(B); 20 | function remove(lB) { 21 | for (let i = 0; i < qB.length; i++) { 22 | const arch = qB[i]; 23 | for (let j = arch.length - 1; j >= 0; j--) { 24 | ecs.removeComponent(arch[j], lB); 25 | } 26 | } 27 | } 28 | 29 | for (let i = 0; i < count; i++) { 30 | ecs.createEntity(); 31 | ecs.addComponent(i, A); 32 | } 33 | 34 | return () => { 35 | add(B); 36 | remove(B); 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/wolf-ecs/entity_cycle.js: -------------------------------------------------------------------------------- 1 | import { ECS } from "wolf-ecs"; 2 | 3 | export default (count) => { 4 | const ecs = new ECS(); 5 | 6 | const A = ecs.defineComponent(); 7 | const B = ecs.defineComponent(); 8 | 9 | const qA = ecs.createQuery(A); 10 | function create(lB) { 11 | for (let i = 0, l = qA.length; i < l; i++) { 12 | const arch = qA[i]; 13 | for (let j = 0, l = arch.length; j < l; j++) { 14 | ecs.addComponent(ecs.createEntity(), lB); 15 | } 16 | } 17 | } 18 | 19 | const qB = ecs.createQuery(B); 20 | function destroy() { 21 | for (let i = 0, l = qB.length; i < l; i++) { 22 | const arch = qB[i]; 23 | for (let j = arch.length - 1; j >= 0; j--) { 24 | ecs.destroyEntity(arch[j]); 25 | } 26 | } 27 | } 28 | 29 | for (let i = 0; i < count; i++) { 30 | ecs.createEntity(); 31 | ecs.addComponent(i, A); 32 | } 33 | 34 | return () => { 35 | create(B); 36 | destroy(); 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/src/cases/wolf-ecs/frag_iter.js: -------------------------------------------------------------------------------- 1 | import { ECS, types } from "wolf-ecs"; 2 | 3 | export default (count) => { 4 | const ecs = new ECS(); 5 | 6 | const cmps = []; 7 | for (let i = 0; i < 26; i++) { 8 | cmps.push(ecs.defineComponent(types.i32)); 9 | } 10 | 11 | const z = cmps[25]; 12 | const data = ecs.defineComponent(types.i32); 13 | 14 | const dataQuery = ecs.createQuery(data); 15 | function dataSystem(lData) { 16 | for (let i = 0, l = dataQuery.length; i < l; i++) { 17 | const arch = dataQuery[i]; 18 | for (let j = 0, l = arch.length; j < l; j++) { 19 | lData[arch[j]] *= 2; 20 | } 21 | } 22 | } 23 | 24 | const zQuery = ecs.createQuery(z); 25 | function zSystem(lZ) { 26 | for (let i = 0, l = zQuery.length; i < l; i++) { 27 | const arch = zQuery[i]; 28 | for (let j = 0, l = arch.length; j < l; j++) { 29 | lZ[arch[j]] *= 2; 30 | } 31 | } 32 | } 33 | 34 | for (let i = 0; i < count; i++) { 35 | for (let i = 0; i < cmps.length; i++) { 36 | const id = ecs.createEntity(); 37 | ecs.addComponent(id, cmps[i]); 38 | ecs.addComponent(id, data); 39 | data[id] = 0; 40 | cmps[i][id] = 0; 41 | } 42 | } 43 | 44 | return () => { 45 | dataSystem(data); 46 | zSystem(z); 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /packages/ecs-benchmark/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": [], 5 | "allowJs": true, 6 | "checkJs": true, 7 | "module": "NodeNext", 8 | "noEmit": true, 9 | "types": ["node"] 10 | }, 11 | "include": [ 12 | "src/cases/objecs/**/*.js", 13 | "src/cases/excalibur/**/*.js", 14 | "src/bench.js", 15 | "src/bench_worker.js" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/examples/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/examples/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "trailingComma": "all", 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /packages/examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## How to Run 4 | 5 | ```sh 6 | $ pnpm i 7 | $ pnpm dev 8 | ``` 9 | 10 | Open your browser to the provided url. 11 | -------------------------------------------------------------------------------- /packages/examples/assets/image/gabe-idle-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/assets/image/gabe-idle-run.png -------------------------------------------------------------------------------- /packages/examples/assets/image/pico8_invaders_sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/assets/image/pico8_invaders_sprites.png -------------------------------------------------------------------------------- /packages/examples/assets/image/ship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/assets/image/ship.png -------------------------------------------------------------------------------- /packages/examples/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ECS Examples 8 | 9 | 10 |
11 |

Demos

12 | Basic
13 | Bouncy Rectangles
14 | Debug Rendering
15 | Falling Sand
16 | Pixel Text
17 | Shmup
18 | Sprite Animation
19 | Sprite Tweening 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /packages/examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "prettier --check 'src/**/*.ts'", 10 | "lint:fix": "prettier --write 'src/**/*.ts'", 11 | "preview": "vite preview" 12 | }, 13 | "devDependencies": { 14 | "@types/color": "4.2.0", 15 | "@types/file-saver": "2.0.7", 16 | "@types/wicg-file-system-access": "2023.10.6", 17 | "prettier": "^3.5.3", 18 | "typescript": "5.8.3", 19 | "vite": "^6.3.5" 20 | }, 21 | "dependencies": { 22 | "@tweakpane/core": "2.0.5", 23 | "color": "5.0.0", 24 | "dot-path-value": "0.0.11", 25 | "fast-xml-parser": "5.2.3", 26 | "file-saver": "2.0.5", 27 | "gameinput": "0.0.9", 28 | "jszip": "3.10.1", 29 | "just-group-by": "2.2.0", 30 | "just-random": "3.2.0", 31 | "just-random-integer": "4.2.0", 32 | "just-safe-get": "4.2.0", 33 | "just-safe-set": "4.2.1", 34 | "just-shuffle": "4.2.0", 35 | "objecs": "workspace:*", 36 | "sharp": "0.34.2", 37 | "tweakpane": "4.0.5", 38 | "type-fest": "4.41.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/examples/public/css/demo.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | background-color: #1a1d21; 4 | padding: 0; 5 | margin: 0; 6 | overflow: hidden; 7 | } 8 | 9 | #container { 10 | margin: 0; 11 | padding: 0; 12 | height: 100vh; 13 | width: 100%; 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | #wrapper { 19 | position: relative; 20 | margin: auto; 21 | } 22 | 23 | #status { 24 | position: absolute; 25 | top: 0; 26 | right: 0; 27 | padding: 1em; 28 | /* common monospace font */ 29 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; 30 | /* pleasing off white color */ 31 | color: #cacaca; 32 | z-index: 2; 33 | } 34 | 35 | .hidden { 36 | display: none; 37 | } 38 | -------------------------------------------------------------------------------- /packages/examples/public/js/feather-init.js: -------------------------------------------------------------------------------- 1 | const timer = setInterval(() => { 2 | if (globalThis.feather == null) { 3 | return; 4 | } 5 | 6 | globalThis.feather.replace(); 7 | 8 | clearInterval(timer); 9 | }, 50); 10 | -------------------------------------------------------------------------------- /packages/examples/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/examples/src/demos/basic/components/ball-tag.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "../entity.ts"; 2 | 3 | export function ballTagFactory(): NonNullable { 4 | return true; 5 | } 6 | -------------------------------------------------------------------------------- /packages/examples/src/demos/basic/entity.ts: -------------------------------------------------------------------------------- 1 | import { SharedEntity } from "#/shared/shared-entity.ts"; 2 | 3 | export type Entity = SharedEntity & { 4 | ballTag?: boolean; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/examples/src/demos/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Demos - Basic 8 | 9 | 10 | 11 | 12 | 23 | 24 | 25 |
26 |
27 | 28 | 34 |
35 | 36 |
37 | 38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /packages/examples/src/demos/basic/main.ts: -------------------------------------------------------------------------------- 1 | import { colorFactory } from "#/shared/components/color.ts"; 2 | import { transformFactory } from "#/shared/components/transform.ts"; 3 | import { World } from "objecs"; 4 | import "../../style.css"; 5 | import { ballTagFactory } from "./components/ball-tag.ts"; 6 | import { Entity } from "./entity.ts"; 7 | import { ballMovementSystemFactory } from "./systems/ball-movement-system.ts"; 8 | import { redneringSystemFactory } from "./systems/rendering-system.ts"; 9 | import { obtainCanvas2dContext } from "#/lib/dom.ts"; 10 | 11 | const canvas = document.querySelector("#canvas") as HTMLCanvasElement; 12 | const ctx = obtainCanvas2dContext(canvas); 13 | 14 | const world = new World(); 15 | 16 | world.createEntity({ 17 | ballTag: ballTagFactory(), 18 | color: colorFactory("red"), 19 | rectangle: { width: 12, height: 12 }, 20 | transform: transformFactory({ x: 10, y: 10 }), 21 | velocity: { x: 100, y: 200 }, 22 | }); 23 | 24 | const ballMovementSystem = ballMovementSystemFactory(world, { 25 | width: canvas.width, 26 | height: canvas.height, 27 | }); 28 | 29 | const renderingSystem = redneringSystemFactory(world, ctx); 30 | 31 | let last = performance.now(); 32 | 33 | /** 34 | * The game loop. 35 | */ 36 | const frame = (hrt: DOMHighResTimeStamp) => { 37 | const dt = Math.min(1000, hrt - last) / 1000; 38 | 39 | ballMovementSystem(dt); 40 | renderingSystem(); 41 | 42 | last = hrt; 43 | 44 | requestAnimationFrame(frame); 45 | }; 46 | 47 | // Start the game loop. 48 | requestAnimationFrame(frame); 49 | -------------------------------------------------------------------------------- /packages/examples/src/demos/basic/systems/ball-movement-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function ballMovementSystemFactory( 5 | world: World, 6 | viewport: { width: number; height: number }, 7 | ) { 8 | const balls = world.archetype( 9 | "ballTag", 10 | "rectangle", 11 | "transform", 12 | "velocity", 13 | ); 14 | 15 | return function ballMovementSystem(dt: number) { 16 | for (const ball of balls.entities) { 17 | ball.transform.position.x += ball.velocity.x * dt; 18 | ball.transform.position.y += ball.velocity.y * dt; 19 | 20 | if (ball.transform.position.x + ball.rectangle.width > viewport.width) { 21 | ball.transform.position.x = viewport.width - ball.rectangle.width; 22 | ball.velocity.x *= -1; 23 | } else if (ball.transform.position.x < 0) { 24 | ball.transform.position.x = 0; 25 | ball.velocity.x *= -1; 26 | } 27 | 28 | if (ball.transform.position.y + ball.rectangle.height > viewport.height) { 29 | ball.transform.position.y = viewport.height - ball.rectangle.height; 30 | ball.velocity.y *= -1; 31 | } else if (ball.transform.position.y < 0) { 32 | ball.transform.position.y = 0; 33 | ball.velocity.y *= -1; 34 | } 35 | } 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /packages/examples/src/demos/basic/systems/rendering-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function redneringSystemFactory( 5 | world: World, 6 | context: CanvasRenderingContext2D, 7 | ) { 8 | const renderables = world.archetype("color", "rectangle", "transform"); 9 | 10 | return function renderingSystem() { 11 | context.clearRect(0, 0, 640, 480); 12 | 13 | for (const entity of renderables.entities) { 14 | context.fillStyle = entity.color; 15 | context.fillRect( 16 | entity.transform.position.x, 17 | entity.transform.position.y, 18 | entity.rectangle.width, 19 | entity.rectangle.height, 20 | ); 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /packages/examples/src/demos/bouncy-rectangles/entity.ts: -------------------------------------------------------------------------------- 1 | import { SharedEntity } from "#/shared/shared-entity.ts"; 2 | 3 | export type Entity = SharedEntity; 4 | -------------------------------------------------------------------------------- /packages/examples/src/demos/bouncy-rectangles/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Demos - Bouncy Rectangles 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 |
20 |
21 | 22 | 28 |
29 | 30 |
31 | 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /packages/examples/src/demos/bouncy-rectangles/systems/physics-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function physicsSystemFactory( 5 | world: World, 6 | viewport: { width: number; height: number }, 7 | ) { 8 | const moving = world.archetype("position", "rectangle", "velocity"); 9 | 10 | return function physicsSystem(dt: number) { 11 | for (const entity of moving.entities) { 12 | entity.position.x += entity.velocity.x * dt; 13 | entity.position.y += entity.velocity.y * dt; 14 | 15 | if (entity.position.x + entity.rectangle.width > viewport.width) { 16 | entity.position.x = viewport.width - entity.rectangle.width; 17 | entity.velocity.x *= -1; 18 | } else if (entity.position.x < 0) { 19 | entity.position.x = 0; 20 | entity.velocity.x *= -1; 21 | } 22 | 23 | if (entity.position.y + entity.rectangle.height > viewport.height) { 24 | entity.position.y = viewport.height - entity.rectangle.height; 25 | entity.velocity.y *= -1; 26 | } else if (entity.position.y < 0) { 27 | entity.position.y = 0; 28 | entity.velocity.y *= -1; 29 | } 30 | } 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/examples/src/demos/bouncy-rectangles/systems/rendering-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function redneringSystemFactory( 5 | world: World, 6 | context: CanvasRenderingContext2D, 7 | ) { 8 | const renderables = world.archetype("color", "position", "rectangle"); 9 | const canvas = context.canvas; 10 | 11 | return function renderingSystem() { 12 | context.clearRect(0, 0, canvas.width, canvas.height); 13 | 14 | for (const entity of renderables.entities) { 15 | context.fillStyle = entity.color; 16 | context.fillRect( 17 | entity.position.x, 18 | entity.position.y, 19 | entity.rectangle.width, 20 | entity.rectangle.height, 21 | ); 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/examples/src/demos/debug-rendering/components/box-collider.ts: -------------------------------------------------------------------------------- 1 | export const boxColliderFactory = ( 2 | offsetX: number, 3 | offsetY: number, 4 | width: number, 5 | height: number, 6 | ) => ({ 7 | offsetX, 8 | offsetY, 9 | width, 10 | height, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/examples/src/demos/debug-rendering/components/sprite.ts: -------------------------------------------------------------------------------- 1 | import { Frame } from "../entity.ts"; 2 | 3 | export const spriteFactory = (frame: Frame, opacity = 1) => ({ 4 | frame, 5 | opacity, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/examples/src/demos/debug-rendering/entity.ts: -------------------------------------------------------------------------------- 1 | import { SharedEntity } from "#/shared/shared-entity.ts"; 2 | 3 | type AnimationDetails = { 4 | name: string; 5 | sourceX: number; 6 | sourceY: number; 7 | width: number; 8 | height: number; 9 | frameWidth: number; 10 | frameHeight: number; 11 | }; 12 | 13 | type BoxCollider = { 14 | offsetX: number; 15 | offsetY: number; 16 | width: number; 17 | height: number; 18 | }; 19 | 20 | export type Frame = { 21 | sourceX: number; 22 | sourceY: number; 23 | width: number; 24 | height: number; 25 | }; 26 | 27 | type SpriteAnimation = { 28 | delta: number; 29 | durationMs: number; 30 | frameRate: number; 31 | currentFrame: number; 32 | finished: boolean; 33 | loop: boolean; 34 | frames: Frame[]; 35 | frameSequence: number[]; 36 | animationDetails: AnimationDetails; 37 | }; 38 | 39 | type Vector2d = { 40 | x: number; 41 | y: number; 42 | }; 43 | 44 | export type Entity = SharedEntity & { 45 | boxCollider?: BoxCollider; 46 | direction?: Vector2d; 47 | playerTag?: boolean; 48 | sprite?: { 49 | frame: Frame; 50 | opacity: number; 51 | }; 52 | spriteAnimation?: SpriteAnimation; 53 | transform?: { 54 | position: Vector2d; 55 | rotation: number; 56 | scale: Vector2d; 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /packages/examples/src/demos/debug-rendering/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Demos - Debug Rendering 8 | 9 | 10 | 11 | 12 | 20 | 21 | 22 |
23 |
24 | 25 | 31 |
32 | 33 |
34 | 35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /packages/examples/src/demos/debug-rendering/spritesheet.ts: -------------------------------------------------------------------------------- 1 | export const SpriteSheet = { 2 | gabe: { 3 | idle: { 4 | sourceX: 0, 5 | sourceY: 0, 6 | width: 24, 7 | height: 24, 8 | }, 9 | animations: { 10 | run: { 11 | sourceX: 24, 12 | sourceY: 0, 13 | width: 144, 14 | height: 24, 15 | frameWidth: 24, 16 | frameHeight: 24, 17 | }, 18 | }, 19 | }, 20 | enemy: { 21 | littleGreenGuy: { 22 | frame0: { 23 | sourceX: 24, 24 | sourceY: 0, 25 | width: 8, 26 | height: 8, 27 | }, 28 | frame1: { 29 | sourceX: 24, 30 | sourceY: 8, 31 | width: 8, 32 | height: 8, 33 | }, 34 | animations: { 35 | idle: { 36 | sourceX: 24, 37 | sourceY: 0, 38 | width: 8, 39 | height: 16, 40 | frameWidth: 8, 41 | frameHeight: 8, 42 | }, 43 | death: { 44 | sourceX: 0, 45 | sourceY: 128, 46 | width: 256, 47 | height: 160, 48 | frameWidth: 32, 49 | frameHeight: 32, 50 | positionXOffset: -5, 51 | positionYOffset: -5, 52 | }, 53 | }, 54 | }, 55 | }, 56 | } as const; 57 | 58 | export type SpriteSheet = typeof SpriteSheet; 59 | -------------------------------------------------------------------------------- /packages/examples/src/demos/debug-rendering/structures/animation-details.ts: -------------------------------------------------------------------------------- 1 | export const animationDetailsFactory = ( 2 | name: string, 3 | sourceX: number, 4 | sourceY: number, 5 | width: number, 6 | height: number, 7 | frameWidth: number, 8 | frameHeight: number, 9 | ) => ({ 10 | name, 11 | sourceX, 12 | sourceY, 13 | width, 14 | height, 15 | frameWidth, 16 | frameHeight, 17 | }); 18 | -------------------------------------------------------------------------------- /packages/examples/src/demos/debug-rendering/structures/frame.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This represents a frame of a sprite sheet. 3 | */ 4 | export const frameFactory = ( 5 | sourceX: number, 6 | sourceY: number, 7 | width: number, 8 | height: number, 9 | ) => ({ 10 | sourceX, 11 | sourceY, 12 | width, 13 | height, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/examples/src/demos/debug-rendering/systems/debug-rendering-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function debugRenderingSystemFactory( 5 | world: World, 6 | context: CanvasRenderingContext2D, 7 | config: Readonly<{ debug: boolean }>, 8 | ) { 9 | const debuggables = world.archetype("boxCollider", "transform"); 10 | 11 | return function debugRenderingSystem(_dt: number) { 12 | if (config.debug === false) { 13 | return; 14 | } 15 | 16 | for (const entity of debuggables.entities) { 17 | context.translate( 18 | entity.transform.position.x, 19 | entity.transform.position.y, 20 | ); 21 | context.rotate(entity.transform.rotation); 22 | context.scale(entity.transform.scale.x, entity.transform.scale.y); 23 | 24 | context.globalAlpha = 0.3; 25 | 26 | context.fillStyle = "red"; 27 | context.fillRect( 28 | entity.transform.scale.x > 0 29 | ? entity.boxCollider.offsetX 30 | : -entity.boxCollider.offsetX - entity.boxCollider.width, 31 | entity.transform.scale.y > 0 32 | ? entity.boxCollider.offsetY 33 | : -entity.boxCollider.offsetY - entity.boxCollider.height, 34 | entity.boxCollider.width, 35 | entity.boxCollider.height, 36 | ); 37 | 38 | context.globalAlpha = 1; 39 | context.resetTransform(); 40 | } 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /packages/examples/src/demos/debug-rendering/systems/movement-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function movementSystemFactory(world: World) { 5 | const movables = world.archetype("direction", "transform", "velocity"); 6 | 7 | return function movementSystem(dt: number) { 8 | for (const entity of movables.entities) { 9 | entity.transform.position.x += 10 | entity.velocity.x * entity.direction.x * dt; 11 | 12 | entity.transform.position.y += 13 | entity.velocity.y * entity.direction.y * dt; 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/examples/src/demos/debug-rendering/systems/player-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function playerSystemFactory( 5 | world: World, 6 | viewport: { width: number; height: number }, 7 | ) { 8 | const players = world.archetype( 9 | "boxCollider", 10 | "direction", 11 | "playerTag", 12 | "transform", 13 | ); 14 | 15 | return function playerSystem(_dt: number) { 16 | for (const entity of players.entities) { 17 | if ( 18 | entity.transform.position.x + entity.boxCollider.offsetX > 19 | viewport.width - entity.boxCollider.width 20 | ) { 21 | entity.transform.position.x = 22 | viewport.width - 23 | entity.boxCollider.width - 24 | entity.boxCollider.offsetX; 25 | entity.transform.scale.x = -1; 26 | entity.direction.x = -1; 27 | } else if (entity.transform.position.x + entity.boxCollider.offsetX < 0) { 28 | entity.transform.position.x = -entity.boxCollider.offsetX; 29 | entity.transform.scale.x = 1; 30 | entity.direction.x = 1; 31 | } 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /packages/examples/src/demos/debug-rendering/systems/rendering-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function renderingSystemFactory( 5 | world: World, 6 | context: CanvasRenderingContext2D, 7 | spriteSheet: HTMLImageElement, 8 | ) { 9 | const renderables = world.archetype("boxCollider", "sprite", "transform"); 10 | 11 | return function renderingSystem(_dt: number) { 12 | context.clearRect(0, 0, context.canvas.width, context.canvas.height); 13 | 14 | for (const entity of renderables.entities) { 15 | const { frame, opacity } = entity.sprite; 16 | const { position, rotation, scale } = entity.transform; 17 | 18 | context.globalAlpha = opacity; 19 | 20 | context.translate(position.x, position.y); 21 | context.rotate(rotation); 22 | context.scale(scale.x, scale.y); 23 | 24 | context.drawImage( 25 | spriteSheet, 26 | frame.sourceX, 27 | frame.sourceY, 28 | frame.width, 29 | frame.height, 30 | scale.x > 0 ? 0 : -frame.width, 31 | scale.y > 0 ? 0 : -frame.height, 32 | frame.width, 33 | frame.height, 34 | ); 35 | 36 | context.globalAlpha = 1; 37 | context.resetTransform(); 38 | } 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /packages/examples/src/demos/debug-rendering/systems/sprite-animation-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function spriteAnimationSystemFactory(world: World) { 5 | const spriteAnimatables = world.archetype("spriteAnimation", "sprite"); 6 | 7 | return function spriteAnimationSystem(dt: number) { 8 | for (const { spriteAnimation, sprite } of spriteAnimatables.entities) { 9 | if (spriteAnimation.finished && !spriteAnimation.loop) { 10 | // You could do something like spawn a SpriteAnimationFinishedEvent here. 11 | // Then handle it in another system. 12 | } 13 | 14 | spriteAnimation.delta += dt; 15 | 16 | if (spriteAnimation.delta >= spriteAnimation.frameRate) { 17 | spriteAnimation.delta = 0; 18 | 19 | spriteAnimation.currentFrame = 20 | (spriteAnimation.currentFrame + 1) % 21 | spriteAnimation.frameSequence.length; 22 | 23 | const frameIndex = 24 | spriteAnimation.frameSequence[spriteAnimation.currentFrame]; 25 | 26 | const frame = spriteAnimation.frames[frameIndex]; 27 | 28 | sprite.frame = frame; 29 | 30 | if ( 31 | spriteAnimation.currentFrame === 32 | spriteAnimation.frameSequence.length - 1 33 | ) { 34 | spriteAnimation.finished = true; 35 | } 36 | } 37 | } 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /packages/examples/src/demos/falling-sand/README.md: -------------------------------------------------------------------------------- 1 | # Falling Sand 2 | 3 | Partial port of https://jason.today/falling-sand to my ECS. 4 | -------------------------------------------------------------------------------- /packages/examples/src/demos/falling-sand/components/grid.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "../entity.ts"; 2 | 3 | export function gridFactory( 4 | width: number, 5 | height: number, 6 | entityGrid: Entity[], 7 | ) { 8 | return { 9 | width, 10 | height, 11 | entities: entityGrid, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/examples/src/demos/falling-sand/entity.ts: -------------------------------------------------------------------------------- 1 | export type Entity = { 2 | color?: string; 3 | empty?: true; 4 | gridIndex: number; 5 | render?: true; 6 | moving?: true; 7 | swap?: { 8 | with: Entity; 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/examples/src/demos/falling-sand/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Demos - Falling Sand 8 | 9 | 10 | 11 | 12 | 23 | 24 | 25 |
26 |
27 | 28 | 34 |
35 | 36 |
37 | 38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /packages/examples/src/demos/falling-sand/lib/color.ts: -------------------------------------------------------------------------------- 1 | import Color from "color"; 2 | import randomInteger from "just-random-integer"; 3 | 4 | export const varyColor = (colorString: string) => { 5 | const color = Color(colorString); 6 | const hue = color.hue(); 7 | const saturation = color.saturationl() + randomInteger(-20, 0); 8 | const lightness = color.lightness() + randomInteger(-10, 10); 9 | 10 | return color.hsl(hue, saturation, lightness); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/examples/src/demos/falling-sand/lib/position-with-variance.ts: -------------------------------------------------------------------------------- 1 | export const positionWithVariance = ( 2 | x: number, 3 | y: number, 4 | radius = 2, 5 | probability = 1.0, 6 | ) => { 7 | let radiusSq = radius * radius; 8 | 9 | for (let y1 = -radius; y1 <= radius; y1++) { 10 | for (let x1 = -radius; x1 <= radius; x1++) { 11 | if (x1 * x1 + y1 * y1 <= radiusSq && Math.random() < probability) { 12 | return { 13 | x: x + x1, 14 | y: y + y1, 15 | }; 16 | } 17 | } 18 | } 19 | 20 | return { 21 | x, 22 | y, 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/examples/src/demos/falling-sand/lib/sleep.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (ms: number) => 2 | new Promise((resolve) => setTimeout(resolve, ms)); 3 | -------------------------------------------------------------------------------- /packages/examples/src/demos/falling-sand/systems/remove-render-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function removeRenderSystemFactory(world: World) { 5 | const rerenderable = world.archetype("render"); 6 | 7 | /** 8 | * This system removes the `render` component from entities that have it. 9 | */ 10 | return function removeRenderSystem() { 11 | for (const entity of rerenderable.entities) { 12 | world.removeEntityComponents(entity, "render"); 13 | } 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /packages/examples/src/demos/falling-sand/systems/rendering-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function renderingSystemFactory(world: World) { 5 | const renderables = world.archetype("color", "render"); 6 | 7 | /** 8 | * This system is responsible for rendering the entities 9 | * that have the `render` component. 10 | */ 11 | return function renderingSystem( 12 | context: CanvasRenderingContext2D, 13 | _dt: number, 14 | ) { 15 | for (const entity of renderables.entities) { 16 | const { color } = entity; 17 | const x = entity.gridIndex % context.canvas.width; 18 | const y = Math.floor(entity.gridIndex / context.canvas.width); 19 | 20 | context.fillStyle = color; 21 | context.fillRect(x, y, 1, 1); 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/examples/src/demos/falling-sand/systems/swap-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function swapSystemFactory(world: World, entityGrid: Entity[]) { 5 | const requireSwap = world.archetype("swap"); 6 | 7 | return function swapSystem() { 8 | for (const entity of requireSwap.entities) { 9 | // Swap entities on the grid 10 | [entityGrid[entity.swap.with.gridIndex], entityGrid[entity.gridIndex]] = [ 11 | entity, 12 | entity.swap.with, 13 | ]; 14 | 15 | // Swap their grid indices 16 | [entity.gridIndex, entity.swap.with.gridIndex] = [ 17 | entity.swap.with.gridIndex, 18 | entity.gridIndex, 19 | ]; 20 | 21 | world.addEntityComponents(entity, "render", true); 22 | world.removeEntityComponents(entity, "swap"); 23 | } 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/examples/src/demos/pixel-text/assets/font/pico-8_regular_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/pixel-text/assets/font/pico-8_regular_5.png -------------------------------------------------------------------------------- /packages/examples/src/demos/pixel-text/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Demos - Pixel Text 8 | 9 | 10 | 11 | 12 | 23 | 24 | 25 |
26 |
27 | 28 | 34 |
35 | 36 |
37 | 38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/README.md: -------------------------------------------------------------------------------- 1 | # Shmup - Cherry Bomb 2 | 3 | A port of the fantastic series by [Lazy Devs Academy](https://www.youtube.com/@LazyDevs). The Pico-8 playlist is found on [YouTube](https://www.youtube.com/playlist?list=PLea8cjCua_P3Sfq4XJqNVbd1vsWnh7LZd). 4 | 5 | ## Differences 6 | 7 | > Any differences between the original and this port I made. I might revert them to stay closer to the original. 8 | 9 | I didn't bother with the `smol_spark` (small single pixel spark) spawned when the projectile hits the enemy, but doesn't destroy it. I found it got lost in the noise of the starfield. 10 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/big-explosion-export.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/big-explosion-export.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/big-explosion.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/big-explosion.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/big-explosion.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/big-explosion.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/boss-music-export.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/boss-music-export.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/boss-music.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/boss-music.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/boss-music.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/boss-music.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/boss-projectile-export.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/boss-projectile-export.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/boss-projectile.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/boss-projectile.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/boss-projectile.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/boss-projectile.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/enemy-death.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/enemy-death.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/enemy-death.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/enemy-death.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/enemy-projectile.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/enemy-projectile.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/enemy-projectile.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/enemy-projectile.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/extra-life-export.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/extra-life-export.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/extra-life.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/extra-life.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/extra-life.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/extra-life.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/game-over.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/game-over.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/game-over.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/game-over.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/game-start-export.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/game-start-export.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/game-start.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/game-start.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/game-start.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/game-start.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/game-won-music.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/game-won-music.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/game-won-music.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/game-won-music.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/no-cherry-bomb.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/no-cherry-bomb.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/no-cherry-bomb.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/no-cherry-bomb.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/no-spread-shot.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/no-spread-shot.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/no-spread-shot.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/no-spread-shot.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/pickup-export.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/pickup-export.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/pickup.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/pickup.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/pickup.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/pickup.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/player-death.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/player-death.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/player-death.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/player-death.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/player-projectile-hit.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/player-projectile-hit.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/player-projectile-hit.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/player-projectile-hit.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/shoot.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/shoot.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/shoot.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/shoot.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/spread-shot-export.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/spread-shot-export.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/spread-shot.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/spread-shot.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/spread-shot.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/spread-shot.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/title-screen-music-export.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/title-screen-music-export.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/title-screen-music.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/title-screen-music.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/title-screen-music.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/title-screen-music.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/wave-complete.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/wave-complete.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/wave-complete.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/wave-complete.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/wave-spawn-export.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/wave-spawn-export.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/wave-spawn.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/wave-spawn.ogg -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/audio/wave-spawn.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/audio/wave-spawn.wav -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/font/pico-8_regular_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/font/pico-8_regular_5.png -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/image/explosions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/image/explosions.png -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/image/play-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/image/play-button.png -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/image/player-explosions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/image/player-explosions.png -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/image/promo/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/image/promo/social.png -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/image/shmup.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/image/shmup.ase -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/image/shmup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/image/shmup.png -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/assets/image/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakeklassen/objecs/9d848f41f89d2f6e6f3ffecc22be6f7b1fde9de2/packages/examples/src/demos/shmup/assets/image/sprites.png -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/bitmasks.ts: -------------------------------------------------------------------------------- 1 | export const CollisionMasks = { 2 | Player: 1 << 0, 3 | PlayerProjectile: 1 << 1, 4 | Enemy: 1 << 2, 5 | EnemyProjectile: 1 << 3, 6 | Pickup: 1 << 4, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/components/sprite-outline-animation.ts: -------------------------------------------------------------------------------- 1 | import { Entity, HexColor } from "../entity.ts"; 2 | 3 | export function spriteOutlineAnimationFactory({ 4 | colors, 5 | colorSequence, 6 | durationMs, 7 | }: { 8 | colors: HexColor[]; 9 | colorSequence: number[]; 10 | durationMs: number; 11 | }): NonNullable { 12 | const delta = 0; 13 | const currentColorIndex = 0; 14 | const color = colors[colorSequence[currentColorIndex]]; 15 | const frameRate = durationMs / 1_000 / colorSequence.length; 16 | 17 | return { 18 | color, 19 | colors, 20 | colorSequence, 21 | currentColorIndex, 22 | delta, 23 | durationMs, 24 | frameRate, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/components/sprite.ts: -------------------------------------------------------------------------------- 1 | import { SetRequired } from "type-fest"; 2 | import { SpriteLayer } from "../constants.ts"; 3 | import { Entity } from "../entity.ts"; 4 | 5 | export function spriteFactory( 6 | sprite: SetRequired>, "frame">, 7 | ): NonNullable { 8 | return { 9 | frame: { 10 | sourceX: sprite.frame.sourceX, 11 | sourceY: sprite.frame.sourceY, 12 | width: sprite.frame.width, 13 | height: sprite.frame.height, 14 | }, 15 | layer: sprite.layer ?? SpriteLayer.Base, 16 | opacity: sprite.opacity ?? 1, 17 | paletteSwaps: sprite.paletteSwaps ?? [], 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/components/text-blink-animation.ts: -------------------------------------------------------------------------------- 1 | import { Entity, HexColor } from "../entity.ts"; 2 | 3 | export function textBlinkAnimationFactory({ 4 | colors, 5 | colorSequence, 6 | durationMs, 7 | }: { 8 | colors: HexColor[]; 9 | colorSequence: number[]; 10 | durationMs: number; 11 | }): NonNullable { 12 | const delta = 0; 13 | const currentColorIndex = 0; 14 | const color = colors[colorSequence[currentColorIndex]]; 15 | const frameRate = durationMs / 1_000 / colorSequence.length; 16 | 17 | return { 18 | color, 19 | colors, 20 | colorSequence, 21 | currentColorIndex, 22 | delta, 23 | durationMs, 24 | frameRate, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/components/transform.ts: -------------------------------------------------------------------------------- 1 | import { PartialDeep } from "type-fest"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function transformFactory( 5 | transform: PartialDeep = {}, 6 | ): NonNullable { 7 | return { 8 | position: { 9 | x: transform.position?.x ?? 0, 10 | y: transform.position?.y ?? 0, 11 | }, 12 | rotation: transform.rotation ?? 0, 13 | scale: { 14 | x: transform.scale?.x ?? 1, 15 | y: transform.scale?.y ?? 1, 16 | }, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/components/ttl.ts: -------------------------------------------------------------------------------- 1 | import { SetRequired } from "type-fest"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function ttlFactory( 5 | opts: SetRequired>, "durationMs">, 6 | ): NonNullable { 7 | return { 8 | durationMs: opts.durationMs, 9 | elapsedMs: opts.elapsedMs ?? 0, 10 | onComplete: opts.onComplete ?? "entity:destroy", 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/components/tween.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Tween, 3 | TweenableEntity, 4 | TweenablePaths, 5 | TweenOptions, 6 | } from "../entity.ts"; 7 | 8 | export function tweenFactory

( 9 | property: P, 10 | options: TweenOptions, 11 | ): Tween { 12 | return { 13 | completed: false, 14 | progress: 0, 15 | iterations: 0, 16 | maxIterations: options.maxIterations ?? 1, 17 | time: 0, 18 | property, 19 | start: options.from, 20 | end: options.to, 21 | change: options.to - options.from, 22 | delay: (options.delay ?? 0) / 1000, 23 | destroyAfter: (options.destroyAfter ?? 0) / 1000, 24 | duration: options.duration / 1000, 25 | from: options.from, 26 | to: options.to, 27 | fullSwing: options.fullSwing ?? false, 28 | easing: options.easing, 29 | yoyo: options.yoyo ?? false, 30 | events: options.events ?? [], 31 | onComplete: options.onComplete ?? "remove", 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pico-8 palette object 3 | */ 4 | export const Pico8Colors = { 5 | Color0: "#000000", 6 | Color1: "#1d2b53", 7 | Color2: "#7E2553", 8 | Color3: "#008751", 9 | Color4: "#ab5236", 10 | Color5: "#5F574F", 11 | Color6: "#c2c3c7", 12 | Color7: "#fff1e8", 13 | Color8: "#FF004D", 14 | Color9: "#FFA300", 15 | Color10: "#FFEC27", 16 | Color11: "#00e436", 17 | Color12: "#29adff", 18 | Color13: "#83769c", 19 | Color14: "#ff77a8", 20 | Color15: "#ffccaa", 21 | } as const; 22 | 23 | export const EnemyType = { 24 | GreenAlien: "greenAlien", 25 | RedFlameGuy: "redFlameGuy", 26 | SpinningShip: "spinningShip", 27 | YellowShip: "yellowShip", 28 | Boss: "boss", 29 | } as const; 30 | 31 | export const SpriteLayer = { 32 | Background: -1, 33 | Base: 0, 34 | Entity: 1, 35 | Explosion: 2, 36 | } as const; 37 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/controls.ts: -------------------------------------------------------------------------------- 1 | import { Gamepad, Keyboard, or } from "gameinput"; 2 | 3 | export const keyboard = new Keyboard(); 4 | export const gamepad = new Gamepad(); 5 | 6 | export const controls = { 7 | any: or(gamepad.button("*").trigger, keyboard.key("any").trigger), 8 | left: or(gamepad.button("Left"), keyboard.key("ArrowLeft")), 9 | right: or(gamepad.button("Right"), keyboard.key("ArrowRight")), 10 | up: or(gamepad.button("Up"), keyboard.key("ArrowUp")), 11 | down: or(gamepad.button("Down"), keyboard.key("ArrowDown")), 12 | fire: or(gamepad.button("A"), keyboard.key("Z")), 13 | bomb: or(gamepad.button("X"), keyboard.key("B")), 14 | debug: or(gamepad.button("RB").trigger, keyboard.key("D").trigger), 15 | confirm: or(gamepad.button("B").trigger, keyboard.key("X").trigger), 16 | quit: or(gamepad.button("Start").trigger, keyboard.key("Escape").trigger), 17 | win: or(gamepad.button("Back").trigger, keyboard.key("W").trigger), 18 | }; 19 | 20 | export type Controls = typeof controls; 21 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/enemy/determine-pickable-enemies.ts: -------------------------------------------------------------------------------- 1 | import { SetRequired } from "type-fest"; 2 | import { sortEntitiesByPosition } from "../entity/sort-entities-by-position.ts"; 3 | import { Entity } from "../entity.ts"; 4 | 5 | export function determinePickableEnemies< 6 | T extends SetRequired, 7 | >(entities: ReadonlySet) { 8 | return sortEntitiesByPosition( 9 | Array.from(entities).filter((entity) => entity.enemyState === "protect"), 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/enemy/pick-random-enemy.ts: -------------------------------------------------------------------------------- 1 | import { rndInt } from "#/lib/math.ts"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function pickRandomEnemy( 5 | enemies: T[], 6 | elementsFromLast = enemies.length, 7 | ) { 8 | if (enemies.length === 0) { 9 | return; 10 | } 11 | 12 | const max = Math.min(enemies.length, elementsFromLast); 13 | const randomIndex = rndInt(max, 1); 14 | const enemyIndex = enemies.length - randomIndex; 15 | 16 | return enemies.at(enemyIndex); 17 | } 18 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/entity-factories/destroy-player-bullet.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { transformFactory } from "../components/transform.ts"; 3 | import { Pico8Colors } from "../constants.ts"; 4 | import { Entity } from "../entity.ts"; 5 | 6 | export function destroyPlayerBulletFactory({ 7 | bullet, 8 | shockwave, 9 | world, 10 | }: { 11 | bullet: Entity; 12 | shockwave?: { 13 | location: NonNullable["position"]; 14 | }; 15 | world: World; 16 | }) { 17 | world.deleteEntity(bullet); 18 | 19 | world.createEntity({ 20 | eventPlaySound: { 21 | track: "player-projectile-hit", 22 | options: { 23 | loop: false, 24 | }, 25 | }, 26 | }); 27 | 28 | if (shockwave == null) { 29 | return; 30 | } 31 | 32 | world.createEntity({ 33 | shockwave: { 34 | radius: 3, 35 | targetRadius: 6, 36 | color: Pico8Colors.Color9, 37 | speed: 30, 38 | }, 39 | transform: transformFactory({ 40 | position: { 41 | x: shockwave.location.x, 42 | y: shockwave.location.y, 43 | }, 44 | }), 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/entity-factories/explosion.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | import { transformFactory } from "../components/transform.ts"; 4 | 5 | interface ExplosionFactoryOptions { 6 | count: number; 7 | directionFn?: () => Entity["direction"]; 8 | particleFn: () => NonNullable; 9 | position: NonNullable["position"]; 10 | velocityFn: () => NonNullable; 11 | } 12 | 13 | function randomDirection() { 14 | return { 15 | x: 1 * Math.sign(Math.random() * 2 - 1), 16 | y: 1 * Math.sign(Math.random() * 2 - 1), 17 | }; 18 | } 19 | 20 | export function explosionFactory( 21 | world: World, 22 | { 23 | count, 24 | directionFn = randomDirection, 25 | particleFn, 26 | position, 27 | velocityFn, 28 | }: ExplosionFactoryOptions, 29 | ) { 30 | for (let i = 0; i < count; i++) { 31 | world.createEntity({ 32 | direction: directionFn(), 33 | particle: particleFn(), 34 | transform: transformFactory({ 35 | position, 36 | }), 37 | velocity: velocityFn(), 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/entity-factories/star.ts: -------------------------------------------------------------------------------- 1 | import { rndFromList } from "#/lib/array.ts"; 2 | import { rndInt } from "#/lib/math.ts"; 3 | import { World } from "objecs"; 4 | import { Pico8Colors } from "../constants.ts"; 5 | import { Entity } from "../entity.ts"; 6 | 7 | interface StarFactoryOptions { 8 | position: NonNullable["position"]; 9 | world: World; 10 | } 11 | 12 | export function starFactory({ position, world }: StarFactoryOptions) { 13 | const entity = world.createEntity({ 14 | direction: { 15 | x: 0, 16 | y: 1, 17 | }, 18 | star: { 19 | color: "white", 20 | }, 21 | transform: { 22 | position, 23 | rotation: 0, 24 | scale: { 25 | x: 1, 26 | y: 1, 27 | }, 28 | }, 29 | velocity: { 30 | x: 0, 31 | y: rndFromList([60, 30, 20]), 32 | }, 33 | }); 34 | 35 | // Adjust star color based on velocity 36 | if (entity.velocity.y < 30) { 37 | entity.star.color = Pico8Colors.Color1; 38 | } else if (entity.velocity.y < 60) { 39 | entity.star.color = Pico8Colors.Color13; 40 | } 41 | } 42 | 43 | interface StarfieldFactoryOptions { 44 | areaHeight: number; 45 | areaWidth: number; 46 | count: number; 47 | world: World; 48 | } 49 | 50 | export function starfieldFactory({ 51 | areaHeight, 52 | areaWidth, 53 | count, 54 | world, 55 | }: StarfieldFactoryOptions) { 56 | for (let i = 0; i < count; i++) { 57 | starFactory({ 58 | position: { 59 | x: rndInt(areaWidth, 1), 60 | y: rndInt(areaHeight, 1), 61 | }, 62 | world, 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/entity/assert.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "../entity.ts"; 2 | 3 | function assertEnityHas( 4 | entity: T, 5 | ...components: Array 6 | ): entity is T & Required> { 7 | return components.every((component) => component in entity); 8 | } 9 | 10 | export function assertEnityHasOrThrow( 11 | entity: T, 12 | ...components: Array 13 | ): asserts entity is T & Required> { 14 | if (!assertEnityHas(entity, ...components)) { 15 | throw new Error(`Entity is missing components: ${components.join(", ")}`); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/entity/sort-entities-by-position.ts: -------------------------------------------------------------------------------- 1 | import { SetRequired } from "type-fest"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | function positionSort( 5 | a: SetRequired, 6 | b: SetRequired, 7 | ) { 8 | if (a.transform.position.y < b.transform.position.y) { 9 | return -1; 10 | } 11 | 12 | if (a.transform.position.y > b.transform.position.y) { 13 | return 1; 14 | } 15 | 16 | if (a.transform.position.x < b.transform.position.x) { 17 | return -1; 18 | } 19 | 20 | if (a.transform.position.x > b.transform.position.x) { 21 | return 1; 22 | } 23 | 24 | return 0; 25 | } 26 | 27 | export function sortEntitiesByPosition< 28 | T extends SetRequired, 29 | >(entities: ReadonlyArray): T[] { 30 | const entitiesArray = Array.from(entities); 31 | 32 | return entitiesArray.sort(positionSort); 33 | } 34 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/game-events.ts: -------------------------------------------------------------------------------- 1 | export enum GameEvent { 2 | GameOver = "GameOver", 3 | GameWon = "GameWon", 4 | RestartGame = "RestartGame", 5 | StartGame = "StartGame", 6 | } 7 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/game-state.ts: -------------------------------------------------------------------------------- 1 | export const gameState = { 2 | bombLocked: true, 3 | cherries: 0, 4 | score: 0, 5 | lives: 1, 6 | maxLives: 4, 7 | paused: false, 8 | gameOver: false, 9 | wave: 0, 10 | waveReady: false, 11 | maxWaves: 9, 12 | }; 13 | 14 | export const resetGameState = (gameState: GameState) => { 15 | gameState.bombLocked = true; 16 | gameState.cherries = 0; 17 | gameState.score = 0; 18 | gameState.lives = gameState.maxLives; 19 | gameState.paused = false; 20 | gameState.gameOver = false; 21 | gameState.wave = 0; 22 | gameState.waveReady = false; 23 | }; 24 | 25 | export type GameState = typeof gameState; 26 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/game-time.ts: -------------------------------------------------------------------------------- 1 | export class GameTime { 2 | #frame = 0; 3 | #hrt: DOMHighResTimeStamp = 0; 4 | #last = performance.now(); 5 | #seconds = 0; 6 | #time = performance.now(); 7 | 8 | update(hrt: DOMHighResTimeStamp) { 9 | const deltaTime = (hrt - this.#last) / 1000; 10 | 11 | this.#frame++; 12 | this.#hrt = hrt; 13 | this.#seconds = Math.floor(hrt / 1000); 14 | this.#time += deltaTime; 15 | 16 | this.#last = hrt; 17 | } 18 | 19 | public get frame() { 20 | return this.#frame; 21 | } 22 | 23 | public get hrt() { 24 | return this.#hrt; 25 | } 26 | 27 | public get seconds() { 28 | return this.#seconds; 29 | } 30 | 31 | public get time() { 32 | return this.#time; 33 | } 34 | } 35 | 36 | export const gameTime = new GameTime(); 37 | 38 | // @ts-ignore 39 | window.gameTime = gameTime; 40 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/structures/animation-details.ts: -------------------------------------------------------------------------------- 1 | export const animationDetailsFactory = ( 2 | name: string, 3 | sourceX: number, 4 | sourceY: number, 5 | width: number, 6 | height: number, 7 | frameWidth: number, 8 | frameHeight: number, 9 | ) => ({ 10 | name, 11 | sourceX, 12 | sourceY, 13 | width, 14 | height, 15 | frameWidth, 16 | frameHeight, 17 | }); 18 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/structures/frame.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This represents a frame of a sprite sheet. 3 | */ 4 | export const frameFactory = ( 5 | sourceX: number, 6 | sourceY: number, 7 | width: number, 8 | height: number, 9 | ) => ({ 10 | sourceX, 11 | sourceY, 12 | width, 13 | height, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/camera-shake-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function cameraShakeSystemFactory({ 5 | camera, 6 | world, 7 | }: { 8 | camera: { x: number; y: number }; 9 | world: World; 10 | }) { 11 | const cameraShakeEvents = world.archetype("eventTriggerCameraShake"); 12 | 13 | return function cameraShake(dt: number) { 14 | for (const entity of cameraShakeEvents.entities) { 15 | const { eventTriggerCameraShake } = entity; 16 | 17 | eventTriggerCameraShake.durationMs -= dt * 1000; 18 | 19 | if (eventTriggerCameraShake.durationMs > 0) { 20 | camera.x = eventTriggerCameraShake.strength * (Math.random() - 0.5); 21 | camera.y = eventTriggerCameraShake.strength * (Math.random() - 0.5); 22 | } else { 23 | camera.x = 0; 24 | camera.y = 0; 25 | 26 | world.deleteEntity(entity); 27 | } 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/cherry-text-system.ts: -------------------------------------------------------------------------------- 1 | import { TextBuffer } from "#/lib/pixel-text/text-buffer.ts"; 2 | import { World } from "objecs"; 3 | import { Entity } from "../entity.ts"; 4 | import { GameState } from "../game-state.ts"; 5 | 6 | /** 7 | * System to update the cherry text to reflect the current number of cherries. 8 | */ 9 | export function cherryTextSystemFactory({ 10 | gameState, 11 | textCache, 12 | world, 13 | }: { 14 | gameState: GameState; 15 | textCache: Map; 16 | world: World; 17 | }) { 18 | const cherryTextEntities = world.archetype("tagTextCherries", "text"); 19 | const previousState = structuredClone(gameState); 20 | 21 | return function cherryTextSystem() { 22 | if (gameState.cherries === previousState.cherries) { 23 | return; 24 | } 25 | 26 | previousState.cherries = gameState.cherries; 27 | 28 | for (const entity of cherryTextEntities.entities) { 29 | const textBuffer = textCache.get(entity); 30 | 31 | textBuffer?.updateText(`${gameState.cherries}`); 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/debug-rendering-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | /** 5 | * System that renders box colliders for debugging. 6 | */ 7 | export function debugRenderingSystemFactory({ 8 | world, 9 | context, 10 | config, 11 | }: { 12 | world: World; 13 | context: CanvasRenderingContext2D; 14 | config: Readonly<{ debug: boolean }>; 15 | }) { 16 | const debuggables = world.archetype("boxCollider", "transform"); 17 | 18 | return function debugRenderingSystem() { 19 | if (config.debug === false) { 20 | return; 21 | } 22 | 23 | for (const entity of debuggables.entities) { 24 | context.translate( 25 | entity.transform.position.x | 0, 26 | entity.transform.position.y | 0, 27 | ); 28 | context.rotate(entity.transform.rotation); 29 | context.scale(entity.transform.scale.x, entity.transform.scale.y); 30 | 31 | context.globalAlpha = 0.3; 32 | 33 | context.fillStyle = "red"; 34 | context.fillRect( 35 | entity.transform.scale.x > 0 36 | ? entity.boxCollider.offsetX 37 | : -entity.boxCollider.offsetX - entity.boxCollider.width, 38 | entity.transform.scale.y > 0 39 | ? entity.boxCollider.offsetY 40 | : -entity.boxCollider.offsetY - entity.boxCollider.height, 41 | entity.boxCollider.width, 42 | entity.boxCollider.height, 43 | ); 44 | 45 | context.globalAlpha = 1; 46 | context.resetTransform(); 47 | } 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/destroy-on-viewport-exit-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function destroyOnViewportExitSystemFactory({ 5 | world, 6 | viewport, 7 | }: { 8 | world: World; 9 | viewport: { width: number; height: number }; 10 | }) { 11 | const boundToViewport = world.archetype( 12 | "boxCollider", 13 | "destroyOnViewportExit", 14 | "transform", 15 | ); 16 | 17 | return (_dt: number) => { 18 | for (const entity of boundToViewport.entities) { 19 | if (entity.enemyState === "flyin") { 20 | // Don't destroy enemies that are flying in. 21 | // They start off screen. 22 | continue; 23 | } 24 | 25 | let destroy = false; 26 | 27 | if ( 28 | entity.transform.position.x + entity.boxCollider.offsetX > 29 | viewport.width 30 | ) { 31 | destroy = true; 32 | } else if (entity.transform.position.x + entity.boxCollider.offsetX < 0) { 33 | destroy = true; 34 | } 35 | 36 | if ( 37 | entity.transform.position.y + entity.boxCollider.offsetY > 38 | viewport.height 39 | ) { 40 | destroy = true; 41 | } else if (entity.transform.position.y <= -entity.boxCollider.height) { 42 | destroy = true; 43 | } 44 | 45 | if (destroy) { 46 | world.deleteEntity(entity); 47 | } 48 | } 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/enemy-pick-system.ts: -------------------------------------------------------------------------------- 1 | import { rnd } from "#/lib/math.ts"; 2 | import { World } from "objecs"; 3 | import { Config } from "../config.ts"; 4 | import { Entity } from "../entity.ts"; 5 | import { GameState } from "../game-state.ts"; 6 | 7 | /** 8 | * This system is responsible for picking when enemies should attack and fire. 9 | */ 10 | export function enemyPickSystemFactory({ 11 | config, 12 | gameState, 13 | world, 14 | }: { 15 | config: Config; 16 | gameState: GameState; 17 | world: World; 18 | }) { 19 | let attackFrequencyTimer = 0; 20 | let fireFrequencyTimer = 0; 21 | let nextFireTime = 0; 22 | 23 | return function enemyPickSystem(dt: number) { 24 | if (gameState.waveReady === false) { 25 | return; 26 | } 27 | 28 | const wave = config.waves[gameState.wave]; 29 | 30 | if (wave == null) { 31 | return; 32 | } 33 | 34 | attackFrequencyTimer += dt; 35 | fireFrequencyTimer += dt; 36 | 37 | if (attackFrequencyTimer >= wave.attackFrequency) { 38 | attackFrequencyTimer = 0; 39 | 40 | world.createEntity({ 41 | eventTriggerEnemyAttack: true, 42 | }); 43 | } 44 | 45 | if (fireFrequencyTimer > nextFireTime) { 46 | fireFrequencyTimer = 0; 47 | nextFireTime = wave.fireFrequency + rnd(wave.fireFrequency); 48 | 49 | world.createEntity({ 50 | eventTriggerEnemyFire: true, 51 | }); 52 | } 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/event-system.ts: -------------------------------------------------------------------------------- 1 | import { rndInt } from "#/lib/math.ts"; 2 | import { World } from "objecs"; 3 | import { Entity } from "../entity.ts"; 4 | 5 | export function eventSystemFactory({ world }: { world: World }) { 6 | const events = world.archetype("event"); 7 | 8 | return function eventSystem() { 9 | for (const entity of events.entities) { 10 | const { event } = entity; 11 | 12 | world.deleteEntity(entity); 13 | 14 | switch (event.type) { 15 | case "TweenEnd": { 16 | const { entity } = event; 17 | 18 | if ( 19 | entity.tagStartScreenGreenAlien === true && 20 | entity.transform != null 21 | ) { 22 | entity.transform.position.x = 30 + rndInt(60); 23 | } 24 | 25 | break; 26 | } 27 | } 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/game-over-rendering-system.ts: -------------------------------------------------------------------------------- 1 | export function gameOverRenderingSystemFactory({ 2 | context, 3 | imageData, 4 | }: { 5 | context: CanvasRenderingContext2D; 6 | imageData: ImageData; 7 | }) { 8 | return function gameOverRenderingSystem() { 9 | context.putImageData(imageData, 0, 0); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/handle-game-over-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | import { GameEvent } from "../game-events.ts"; 4 | import { Scene } from "../scene.ts"; 5 | 6 | export function handleGameOverSystemFactory({ 7 | scene, 8 | world, 9 | }: { 10 | scene: Scene; 11 | world: World; 12 | }) { 13 | const events = world.archetype("eventGameOver"); 14 | 15 | return () => { 16 | if (events.entities.size === 0) { 17 | return; 18 | } 19 | 20 | // cleanup 21 | for (const entity of events.entities) { 22 | world.deleteEntity(entity); 23 | } 24 | 25 | scene.emit(GameEvent.GameOver); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/invulnerable-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function invulnerableSystemFactory({ world }: { world: World }) { 5 | const invulnerables = world.archetype("invulnerable"); 6 | 7 | return function invulnerableSystem(dt: number) { 8 | for (const entity of invulnerables.entities) { 9 | const { invulnerable } = entity; 10 | 11 | invulnerable.elapsedMs += dt * 1000; 12 | 13 | if (invulnerable.elapsedMs >= invulnerable.durationMs) { 14 | world.removeEntityComponents(entity, "invulnerable"); 15 | } 16 | } 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/lateral-hunter-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | import { Timer } from "../timer.ts"; 4 | 5 | /** 6 | * This system handles the spinning ship enemy type. 7 | * When the enemy is in the attack state, if the player crosses it's path 8 | * along x, it will stop moving vertically and move towards the player. 9 | */ 10 | export function lateralHunterSystemFactory({ 11 | world, 12 | }: { 13 | timer: Timer; 14 | world: World; 15 | }) { 16 | const movables = world.archetype( 17 | "direction", 18 | "tagEnemy", 19 | "tagLateralHunter", 20 | "transform", 21 | "velocity", 22 | ); 23 | 24 | const players = world.archetype("boxCollider", "tagPlayer", "transform"); 25 | 26 | return function lateralHunterSystem() { 27 | const [player] = players.entities; 28 | 29 | if (player == null) { 30 | return; 31 | } 32 | 33 | for (const entity of movables.entities) { 34 | if (entity.enemyState !== "attack") { 35 | continue; 36 | } 37 | 38 | if (entity.transform.position.y > player.transform.position.y) { 39 | entity.direction.y = 0; 40 | entity.direction.x = 41 | entity.transform.position.x > player.transform.position.x ? -1 : 1; 42 | 43 | entity.velocity.x = 60; 44 | entity.velocity.y = 0; 45 | 46 | // No need to continue checking 47 | world.removeEntityComponents(entity, "tagLateralHunter"); 48 | } 49 | } 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/lives-rendering-system.ts: -------------------------------------------------------------------------------- 1 | import { LoadedContent } from "../content.ts"; 2 | import { GameState } from "../game-state.ts"; 3 | 4 | export function livesRenderingSystemFactory({ 5 | gameState, 6 | content, 7 | context, 8 | }: { 9 | gameState: GameState; 10 | content: LoadedContent; 11 | context: CanvasRenderingContext2D; 12 | }) { 13 | return function livesRenderingSystem() { 14 | for (let i = 0; i < gameState.maxLives; i++) { 15 | if (i < gameState.lives) { 16 | context.drawImage(content.sprite.hud.heartFull, (i + 1) * 9 - 8, 1); 17 | } else { 18 | context.drawImage(content.sprite.hud.heartEmpty, (i + 1) * 9 - 8, 1); 19 | } 20 | } 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/local-transform-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.js"; 3 | 4 | /** 5 | * Position entities relative to their parent. 6 | */ 7 | export function localTransformSystemFactory({ 8 | world, 9 | }: { 10 | world: World; 11 | }) { 12 | const children = world.archetype("localTransform", "parent", "transform"); 13 | 14 | return function localTransformSystem() { 15 | for (const entity of children.entities) { 16 | entity.transform.position.x = 17 | entity.parent.transform.position.x + entity.localTransform.position.x; 18 | 19 | entity.transform.position.y = 20 | entity.parent.transform.position.y + entity.localTransform.position.y; 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/movement-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function movementSystemFactory({ world }: { world: World }) { 5 | const movables = world.archetype("direction", "transform", "velocity"); 6 | 7 | return function movementSystem(dt: number) { 8 | for (const entity of movables.entities) { 9 | entity.transform.position.x += 10 | entity.velocity.x * entity.direction.x * dt; 11 | 12 | entity.transform.position.y += 13 | entity.velocity.y * entity.direction.y * dt; 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/muzzle-flash-rendering-system.ts: -------------------------------------------------------------------------------- 1 | import { fillCircle } from "#/lib/canvas.ts"; 2 | import { World } from "objecs"; 3 | import { Pico8Colors } from "../constants.ts"; 4 | import { Entity } from "../entity.ts"; 5 | 6 | export function muzzleFlashRenderingSystemFactory({ 7 | world, 8 | context, 9 | }: { 10 | world: World; 11 | context: CanvasRenderingContext2D; 12 | }) { 13 | const muzzleFlashes = world.archetype("muzzleFlash", "transform"); 14 | 15 | return function muzzleFlashRenderingSystem() { 16 | for (const entity of muzzleFlashes.entities) { 17 | const { muzzleFlash, transform } = entity; 18 | 19 | context.translate(transform.position.x | 0, transform.position.y | 0); 20 | context.rotate(transform.rotation); 21 | context.scale(transform.scale.x, transform.scale.y); 22 | 23 | fillCircle(context, 0, 0, muzzleFlash.size, Pico8Colors.Color7); 24 | 25 | context.resetTransform(); 26 | } 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/muzzle-flash-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function muzzleFlashSystemFactory({ world }: { world: World }) { 5 | const muzzleFlashes = world.archetype( 6 | "muzzleFlash", 7 | "trackPlayer", 8 | "transform", 9 | ); 10 | const players = world.archetype("tagPlayer", "transform"); 11 | 12 | return function muzzleFlashSystem(dt: number) { 13 | const [player] = players.entities; 14 | 15 | for (const entity of muzzleFlashes.entities) { 16 | const { muzzleFlash, trackPlayer, transform } = entity; 17 | muzzleFlash.elapsed += dt; 18 | muzzleFlash.size -= 0.5; 19 | 20 | transform.position.x = player.transform.position.x + trackPlayer.offset.x; 21 | transform.position.y = player.transform.position.y + trackPlayer.offset.y; 22 | 23 | if ( 24 | muzzleFlash.size < 0 || 25 | muzzleFlash.elapsed >= muzzleFlash.durationMs 26 | ) { 27 | world.deleteEntity(entity); 28 | } 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/particle-rendering-system.ts: -------------------------------------------------------------------------------- 1 | import { fillCircle } from "#/lib/canvas.ts"; 2 | import { World } from "objecs"; 3 | import { Pico8Colors } from "../constants.ts"; 4 | import { Entity } from "../entity.ts"; 5 | 6 | export function particleRenderingSystemFactory({ 7 | world, 8 | context, 9 | }: { 10 | world: World; 11 | context: CanvasRenderingContext2D; 12 | }) { 13 | const renderables = world.archetype("particle", "transform"); 14 | 15 | return function particleRenderingSystem() { 16 | for (const entity of renderables.entities) { 17 | const { particle, transform } = entity; 18 | 19 | context.translate(transform.position.x | 0, transform.position.y | 0); 20 | context.rotate(transform.rotation); 21 | context.scale(transform.scale.x, transform.scale.y); 22 | 23 | if (particle.spark === true) { 24 | context.fillStyle = Pico8Colors.Color7; 25 | context.fillRect(0, 0, 1, 1); 26 | } else if (particle.shape === "circle") { 27 | fillCircle(context, 0, 0, particle.radius | 0, particle.color); 28 | } 29 | 30 | context.globalAlpha = 1; 31 | context.resetTransform(); 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/player-enemy-collision-event-cleanup-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function playerEnemyCollisionEventCleanupSystemFactory({ 5 | world, 6 | }: { 7 | world: World; 8 | }) { 9 | const events = world.archetype("eventPlayerEnemyCollision"); 10 | 11 | return function playerEnemyCollisionEventCleanupSystem() { 12 | for (const entity of events.entities) { 13 | world.deleteEntity(entity); 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/player-projectile-boss-collision-event-cleanup-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function playerProjectileBossCollisionEventCleanupSystemFactory({ 5 | world, 6 | }: { 7 | world: World; 8 | }) { 9 | const events = world.archetype("eventPlayerProjectileBossCollision"); 10 | 11 | return () => { 12 | for (const entity of events.entities) { 13 | world.deleteEntity(entity); 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/player-projectile-enemy-collision-event-cleanup-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function playerProjectileEnemyCollisionEventCleanupSystemFactory({ 5 | world, 6 | }: { 7 | world: World; 8 | }) { 9 | const events = world.archetype("eventPlayerProjectileEnemyCollision"); 10 | 11 | return function playerProjectileEnemyCollisionEventCleanupSystem() { 12 | for (const entity of events.entities) { 13 | world.deleteEntity(entity); 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/rendering-system.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The rendering system is handed a buffer canvas and a context to draw to. 3 | * Using a buffer allows us to separate cleaning for camera movement. 4 | * e.g. camera shake. 5 | */ 6 | export function renderingSystemFactory({ 7 | buffer, 8 | camera, 9 | context, 10 | }: { 11 | buffer: HTMLCanvasElement; 12 | camera: { x: number; y: number }; 13 | context: CanvasRenderingContext2D; 14 | }) { 15 | return function renderingSystem() { 16 | context.translate(camera.x, camera.y); 17 | 18 | context.clearRect(0, 0, context.canvas.width, context.canvas.height); 19 | context.fillStyle = "black"; 20 | context.fillRect(0, 0, context.canvas.width, context.canvas.height); 21 | 22 | // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter 23 | // Holy shit this is cool 24 | // context.filter = 'contrast(1.4) sepia(1) blur(1px)'; 25 | 26 | context.drawImage(buffer, 0, 0); 27 | 28 | context.resetTransform(); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/score-system.ts: -------------------------------------------------------------------------------- 1 | import { TextBuffer } from "#/lib/pixel-text/text-buffer.ts"; 2 | import { World } from "objecs"; 3 | import { Entity } from "../entity.ts"; 4 | import { GameState } from "../game-state.ts"; 5 | 6 | export function scoreSystemFactory({ 7 | gameState, 8 | textCache, 9 | world, 10 | }: { 11 | gameState: GameState; 12 | textCache: Map; 13 | world: World; 14 | }) { 15 | const scoreTextEntities = world.archetype("tagTextScore", "text"); 16 | const previousState = structuredClone(gameState); 17 | 18 | return function scoreSystem() { 19 | if (gameState.score === previousState.score) { 20 | return; 21 | } 22 | 23 | previousState.score = gameState.score; 24 | 25 | for (const entity of scoreTextEntities.entities) { 26 | const textBuffer = textCache.get(entity); 27 | 28 | textBuffer?.updateText(`Score:${gameState.score}`); 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/shockwave-rendering-system.ts: -------------------------------------------------------------------------------- 1 | import { circ } from "#/lib/canvas.ts"; 2 | import { World } from "objecs"; 3 | import { Entity } from "../entity.ts"; 4 | 5 | export function shockwaveRenderingSystemFactory({ 6 | context, 7 | world, 8 | }: { 9 | context: CanvasRenderingContext2D; 10 | world: World; 11 | }) { 12 | const shockwaves = world.archetype("shockwave", "transform"); 13 | 14 | return function shockwaveRenderingSystem() { 15 | for (const entity of shockwaves.entities) { 16 | const { shockwave, transform } = entity; 17 | 18 | context.fillStyle = shockwave.color; 19 | 20 | circ( 21 | context, 22 | transform.position.x | 0, 23 | transform.position.y | 0, 24 | shockwave.radius, 25 | shockwave.color, 26 | ); 27 | } 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/shockwave-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | /** 5 | * Shockwaves are expanding circles that are used to indicate the 6 | * area of effect. They expand to a target radius, then are destroyed. 7 | */ 8 | export function shockwaveSystemFactory({ world }: { world: World }) { 9 | const shockwaves = world.archetype("shockwave"); 10 | 11 | return function shockwaveSystem(dt: number) { 12 | for (const entity of shockwaves.entities) { 13 | const { shockwave } = entity; 14 | 15 | shockwave.radius += shockwave.speed * dt; 16 | 17 | if (shockwave.radius >= shockwave.targetRadius) { 18 | world.deleteEntity(entity); 19 | } 20 | } 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/sound-system.ts: -------------------------------------------------------------------------------- 1 | import { AudioManager } from "#/lib/audio-manager.ts"; 2 | import { World } from "objecs"; 3 | import { Entity } from "../entity.ts"; 4 | 5 | export function soundSystemFactory({ 6 | audioManager, 7 | world, 8 | }: { 9 | audioManager: AudioManager; 10 | world: World; 11 | }) { 12 | const soundEvents = world.archetype("eventPlaySound"); 13 | 14 | return function soundSystem() { 15 | for (const soundEvent of soundEvents.entities) { 16 | audioManager.play( 17 | soundEvent.eventPlaySound.track, 18 | soundEvent.eventPlaySound.options, 19 | ); 20 | 21 | world.deleteEntity(soundEvent); 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/sprite-animation-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function spriteAnimationSystemFactory({ 5 | world, 6 | }: { 7 | world: World; 8 | }) { 9 | const spriteAnimatables = world.archetype("spriteAnimation", "sprite"); 10 | 11 | return function spriteAnimationSystem(dt: number) { 12 | for (const entity of spriteAnimatables.entities) { 13 | const { spriteAnimation, sprite } = entity; 14 | 15 | if (spriteAnimation.finished && !spriteAnimation.loop) { 16 | world.removeEntityComponents(entity, "spriteAnimation"); 17 | 18 | continue; 19 | } 20 | 21 | spriteAnimation.delta += dt; 22 | 23 | if (spriteAnimation.delta >= spriteAnimation.frameRate) { 24 | spriteAnimation.delta = 0; 25 | 26 | spriteAnimation.currentFrame = 27 | (spriteAnimation.currentFrame + 1) % 28 | spriteAnimation.frameSequence.length; 29 | 30 | const frameIndex = 31 | spriteAnimation.frameSequence[spriteAnimation.currentFrame]; 32 | 33 | const frame = spriteAnimation.frames[frameIndex]; 34 | 35 | sprite.frame = frame; 36 | 37 | if ( 38 | spriteAnimation.currentFrame === 39 | spriteAnimation.frameSequence.length - 1 40 | ) { 41 | spriteAnimation.finished = true; 42 | } 43 | } 44 | } 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/sprite-outline-animation-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function spriteOutlineAnimationSystemFactory({ 5 | world, 6 | }: { 7 | world: World; 8 | }) { 9 | const entities = world.archetype("spriteOutline", "spriteOutlineAnimation"); 10 | 11 | return function spriteOutlineAnimationSystem(dt: number) { 12 | for (const entity of entities.entities) { 13 | const { spriteOutline, spriteOutlineAnimation } = entity; 14 | 15 | spriteOutlineAnimation.delta += dt; 16 | 17 | if (spriteOutlineAnimation.delta >= spriteOutlineAnimation.frameRate) { 18 | spriteOutlineAnimation.delta = 0; 19 | spriteOutlineAnimation.currentColorIndex = 20 | (spriteOutlineAnimation.currentColorIndex + 1) % 21 | spriteOutlineAnimation.colorSequence.length; 22 | } 23 | 24 | spriteOutlineAnimation.color = 25 | spriteOutlineAnimation.colors[ 26 | spriteOutlineAnimation.colorSequence[ 27 | spriteOutlineAnimation.currentColorIndex 28 | ] 29 | ]; 30 | 31 | spriteOutline.color = spriteOutlineAnimation.color; 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/starfield-rendering-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function starfieldRenderingSystemFactory({ 5 | world, 6 | context, 7 | }: { 8 | world: World; 9 | context: CanvasRenderingContext2D; 10 | }) { 11 | const stars = world.archetype("star", "transform"); 12 | 13 | return function starfieldRenderingSystem() { 14 | for (const { star, transform } of stars.entities) { 15 | context.globalAlpha = 1; 16 | 17 | context.translate(transform.position.x | 0, transform.position.y | 0); 18 | context.rotate(transform.rotation); 19 | context.scale(transform.scale.x, transform.scale.y); 20 | 21 | context.fillStyle = star.color; 22 | 23 | context.fillRect(0, 0, 1, 1); 24 | 25 | context.globalAlpha = 1; 26 | context.resetTransform(); 27 | } 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/starfield-system.ts: -------------------------------------------------------------------------------- 1 | import { rndInt } from "#/lib/math.ts"; 2 | import { World } from "objecs"; 3 | import { Entity } from "../entity.ts"; 4 | 5 | export function starfieldSystemFactory({ world }: { world: World }) { 6 | const stars = world.archetype("star", "transform"); 7 | 8 | return function starfieldSystem() { 9 | for (const { transform } of stars.entities) { 10 | // Just wrap the stars in y and reset their x position 11 | if (transform.position.y > 128) { 12 | transform.position.y = -1; 13 | transform.position.x = rndInt(127, 1); 14 | } 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/start-game-system.ts: -------------------------------------------------------------------------------- 1 | import { GameEvent } from "../game-events.ts"; 2 | import { Input } from "../input.ts"; 3 | import { Scene } from "../scene.ts"; 4 | 5 | export function startGameSystemFactory({ 6 | input, 7 | scene, 8 | }: { 9 | input: Input; 10 | scene: Scene; 11 | }) { 12 | // Swallow remaining input. 13 | // This is to prevent the game from starting immediately if the input 14 | // query still reads as true from the previous scene. 15 | input.any.query(); 16 | 17 | return function startGameSystem() { 18 | if (input.any.query()) { 19 | scene.emit(GameEvent.StartGame); 20 | } 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/text-blink-animation-system.ts: -------------------------------------------------------------------------------- 1 | import { TextBuffer } from "#/lib/pixel-text/text-buffer.ts"; 2 | import { World } from "objecs"; 3 | import { Entity } from "../entity.ts"; 4 | 5 | export function textBlinkAnimationSystemFactory({ 6 | textCache, 7 | world, 8 | }: { 9 | textCache: Map; 10 | world: World; 11 | }) { 12 | const entities = world.archetype("textBlinkAnimation", "text"); 13 | 14 | return function textBlinkAnimationSystem(dt: number) { 15 | for (const entity of entities.entities) { 16 | const { textBlinkAnimation } = entity; 17 | 18 | textBlinkAnimation.delta += dt; 19 | 20 | if (textBlinkAnimation.delta >= textBlinkAnimation.frameRate) { 21 | textBlinkAnimation.delta = 0; 22 | textBlinkAnimation.currentColorIndex = 23 | (textBlinkAnimation.currentColorIndex + 1) % 24 | textBlinkAnimation.colorSequence.length; 25 | } 26 | 27 | textBlinkAnimation.color = 28 | textBlinkAnimation.colors[ 29 | textBlinkAnimation.colorSequence[textBlinkAnimation.currentColorIndex] 30 | ]; 31 | 32 | const textBuffer = textCache.get(entity); 33 | textBuffer?.updateText(textBuffer?.text, { 34 | color: textBlinkAnimation.color, 35 | }); 36 | } 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/text-rendering-system.ts: -------------------------------------------------------------------------------- 1 | import { TextBuffer } from "#/lib/pixel-text/text-buffer.ts"; 2 | import { World } from "objecs"; 3 | import { Entity } from "../entity.ts"; 4 | 5 | export function textRenderingSystemFactory({ 6 | context, 7 | textCache, 8 | world, 9 | }: { 10 | context: CanvasRenderingContext2D; 11 | textCache: Map; 12 | world: World; 13 | }) { 14 | const textRenderables = world.archetype("text", "transform"); 15 | 16 | return function textRenderingSystem() { 17 | for (const entity of textRenderables.entities) { 18 | const { transform } = entity; 19 | 20 | const textBuffer = textCache.get(entity); 21 | 22 | if (textBuffer == null) { 23 | throw new Error(`Text buffer not found for entity`); 24 | } 25 | 26 | context.translate(transform.position.x | 0, transform.position.y | 0); 27 | context.rotate(transform.rotation); 28 | context.scale(transform.scale.x, transform.scale.y); 29 | 30 | context.drawImage(textBuffer.renderable, 0, 0); 31 | 32 | context.globalAlpha = 1; 33 | context.resetTransform(); 34 | } 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/text-system.ts: -------------------------------------------------------------------------------- 1 | import { TextBuffer, TextBufferFont } from "#/lib/pixel-text/text-buffer.ts"; 2 | import { World } from "objecs"; 3 | import { Entity } from "../entity.ts"; 4 | 5 | export function textSystemFactory({ 6 | fontCache, 7 | textCache, 8 | world, 9 | }: { 10 | fontCache: Map; 11 | textCache: Map; 12 | world: World; 13 | }) { 14 | const entities = world.archetype("text", "transform"); 15 | 16 | return function textSystem() { 17 | for (const entity of entities.entities) { 18 | const { text, transform } = entity; 19 | 20 | // Register this text entity if it hasn't been registered yet 21 | if (!textCache.has(entity)) { 22 | const font = fontCache.get(text.font); 23 | 24 | if (font == null) { 25 | throw new Error(`Font ${text.font} not found`); 26 | } 27 | 28 | const textBuffer = new TextBuffer({ 29 | color: text.color, 30 | font, 31 | text: text.message, 32 | }); 33 | 34 | if (text.align === "center") { 35 | transform.position.x = 36 | transform.position.x - textBuffer.renderable.width / 2; 37 | } 38 | 39 | textCache.set(entity, textBuffer); 40 | } 41 | } 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/time-to-live-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function timeToLiveSystemFactory({ world }: { world: World }) { 5 | const entities = world.archetype("ttl"); 6 | 7 | return function timeToLiveSystem(dt: number) { 8 | for (const entity of entities.entities) { 9 | const { ttl } = entity; 10 | 11 | ttl.elapsedMs += dt * 1000; 12 | 13 | if ( 14 | ttl.elapsedMs >= ttl.durationMs && 15 | ttl.onComplete === "entity:destroy" 16 | ) { 17 | world.deleteEntity(entity); 18 | 19 | if (ttl.trigger == null) { 20 | continue; 21 | } 22 | 23 | if (ttl.trigger.startsWith("nextWave")) { 24 | const [_event, waveString] = ttl.trigger.split(":"); 25 | const wave = parseInt(waveString, 10); 26 | 27 | world.createEntity({ 28 | eventSpawnWave: { 29 | waveNumber: wave, 30 | }, 31 | }); 32 | } 33 | } 34 | } 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/timer-system.ts: -------------------------------------------------------------------------------- 1 | import { Timer } from "../timer.ts"; 2 | 3 | export function timerSystemFactory({ timer }: { timer: Timer }) { 4 | return function timerSystem(dt: number) { 5 | timer.update(dt); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/trigger-game-over-system.ts: -------------------------------------------------------------------------------- 1 | import { GameEvent } from "../game-events.ts"; 2 | import { Input } from "../input.ts"; 3 | import { Scene } from "../scene.ts"; 4 | 5 | export function triggerGameOverSystemFactory({ 6 | input, 7 | scene, 8 | }: { 9 | input: Input; 10 | scene: Scene; 11 | }) { 12 | return function triggerGameOverSystem() { 13 | if (input.quit.query()) { 14 | scene.emit(GameEvent.GameOver); 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/trigger-game-won-system.ts: -------------------------------------------------------------------------------- 1 | import { GameEvent } from "../game-events.ts"; 2 | import { Input } from "../input.ts"; 3 | import { Scene } from "../scene.ts"; 4 | 5 | export function triggerGameWonSystemFactory({ 6 | input, 7 | scene, 8 | }: { 9 | input: Input; 10 | scene: Scene; 11 | }) { 12 | return function triggerGameWonSystem() { 13 | if (input.win.query()) { 14 | scene.emit(GameEvent.GameWon); 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/systems/yellow-ship-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { fireSpread } from "../enemy/enemy-bullets.ts"; 3 | import { Entity } from "../entity.ts"; 4 | 5 | export function yellowShipSystemFactory({ world }: { world: World }) { 6 | const yellowShips = world.archetype( 7 | "enemyState", 8 | "tagYellowShip", 9 | "transform", 10 | "velocity", 11 | ); 12 | 13 | let spreadshotTimer = 0; 14 | 15 | return function yellowShipSystem(dt: number) { 16 | spreadshotTimer += dt; 17 | 18 | for (const yellowShip of yellowShips.entities) { 19 | if (yellowShip.enemyState === "attack") { 20 | if (yellowShip.transform.position.y > 110) { 21 | yellowShip.velocity.y = 30; 22 | } else { 23 | // When in attack mode, about every second, fire a spread of bullets 24 | if (spreadshotTimer > 1) { 25 | fireSpread({ 26 | count: 8, 27 | enemy: yellowShip, 28 | speed: 40, 29 | world, 30 | }); 31 | } 32 | } 33 | } 34 | } 35 | 36 | if (spreadshotTimer > 1) { 37 | spreadshotTimer = 0; 38 | } 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /packages/examples/src/demos/shmup/timer.ts: -------------------------------------------------------------------------------- 1 | export class TimeSpan { 2 | #duration: number; 3 | #elapsed: number; 4 | #repeat: boolean; 5 | #totalElapsed = 0; 6 | 7 | constructor(durationMs: number, elapsed = 0, repeat = false) { 8 | this.#duration = durationMs / 1000; 9 | this.#elapsed = elapsed; 10 | this.#repeat = repeat; 11 | } 12 | 13 | public get expired() { 14 | return this.#elapsed >= this.#duration; 15 | } 16 | 17 | public get repeat() { 18 | return this.#repeat; 19 | } 20 | 21 | public get elapsed() { 22 | return this.#elapsed; 23 | } 24 | 25 | public get totalElapsed() { 26 | return this.#totalElapsed; 27 | } 28 | 29 | update(dt: number) { 30 | this.#elapsed += dt; 31 | this.#totalElapsed += dt; 32 | } 33 | 34 | reset() { 35 | this.#elapsed = 0; 36 | this.#totalElapsed = 0; 37 | } 38 | } 39 | 40 | export class Timer { 41 | #timers = new Map void>(); 42 | 43 | add(time: TimeSpan, callback: () => void) { 44 | this.#timers.set(time, callback); 45 | } 46 | 47 | remove(time: TimeSpan) { 48 | this.#timers.delete(time); 49 | } 50 | 51 | clear() { 52 | this.#timers.clear(); 53 | } 54 | 55 | update(dt: number) { 56 | for (const [time, callback] of this.#timers) { 57 | time.update(dt); 58 | 59 | if (time.expired) { 60 | callback(); 61 | 62 | if (time.repeat) { 63 | time.reset(); 64 | } else { 65 | this.#timers.delete(time); 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/examples/src/demos/sprite-animation/entity.ts: -------------------------------------------------------------------------------- 1 | type AnimationDetails = { 2 | name: string; 3 | sourceX: number; 4 | sourceY: number; 5 | width: number; 6 | height: number; 7 | frameWidth: number; 8 | frameHeight: number; 9 | }; 10 | 11 | export type Frame = { 12 | sourceX: number; 13 | sourceY: number; 14 | width: number; 15 | height: number; 16 | }; 17 | 18 | type SpriteAnimation = { 19 | delta: number; 20 | durationMs: number; 21 | frameRate: number; 22 | currentFrame: number; 23 | finished: boolean; 24 | loop: boolean; 25 | frames: Frame[]; 26 | frameSequence: number[]; 27 | animationDetails: AnimationDetails; 28 | }; 29 | 30 | type Vector2d = { 31 | x: number; 32 | y: number; 33 | }; 34 | 35 | export type Entity = { 36 | transform?: { 37 | position: Vector2d; 38 | scale: Vector2d; 39 | rotation: number; 40 | }; 41 | sprite?: { 42 | frame: Frame; 43 | opacity: number; 44 | }; 45 | spriteAnimation?: SpriteAnimation; 46 | }; 47 | -------------------------------------------------------------------------------- /packages/examples/src/demos/sprite-animation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Demos - Sprite Animation 8 | 9 | 10 | 11 | 12 | 20 | 21 | 22 |

23 |
24 | 25 | 31 |
32 | 33 |
34 | 35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /packages/examples/src/demos/sprite-animation/spritesheet.ts: -------------------------------------------------------------------------------- 1 | export const SpriteSheet = { 2 | gabe: { 3 | idle: { 4 | sourceX: 0, 5 | sourceY: 0, 6 | width: 24, 7 | height: 24, 8 | }, 9 | animations: { 10 | run: { 11 | sourceX: 24, 12 | sourceY: 0, 13 | width: 144, 14 | height: 24, 15 | frameWidth: 24, 16 | frameHeight: 24, 17 | }, 18 | }, 19 | }, 20 | enemy: { 21 | littleGreenGuy: { 22 | frame0: { 23 | sourceX: 24, 24 | sourceY: 0, 25 | width: 8, 26 | height: 8, 27 | }, 28 | frame1: { 29 | sourceX: 24, 30 | sourceY: 8, 31 | width: 8, 32 | height: 8, 33 | }, 34 | animations: { 35 | idle: { 36 | sourceX: 24, 37 | sourceY: 0, 38 | width: 8, 39 | height: 16, 40 | frameWidth: 8, 41 | frameHeight: 8, 42 | }, 43 | death: { 44 | sourceX: 0, 45 | sourceY: 128, 46 | width: 256, 47 | height: 160, 48 | frameWidth: 32, 49 | frameHeight: 32, 50 | positionXOffset: -5, 51 | positionYOffset: -5, 52 | }, 53 | }, 54 | }, 55 | }, 56 | } as const; 57 | 58 | export type SpriteSheet = typeof SpriteSheet; 59 | -------------------------------------------------------------------------------- /packages/examples/src/demos/sprite-animation/systems/rendering-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function renderingSystemFactory(world: World) { 5 | const renderables = world.archetype("sprite", "transform"); 6 | 7 | return function renderingSystem( 8 | context: CanvasRenderingContext2D, 9 | spriteSheet: HTMLImageElement, 10 | _dt: number, 11 | ) { 12 | const canvas = context.canvas; 13 | 14 | context.clearRect(0, 0, canvas.width, canvas.height); 15 | 16 | for (const { sprite, transform } of renderables.entities) { 17 | context.globalAlpha = sprite.opacity; 18 | 19 | context.translate(transform.position.x, transform.position.y); 20 | context.rotate(transform.rotation); 21 | context.scale(transform.scale.x, transform.scale.y); 22 | 23 | context.drawImage( 24 | spriteSheet, 25 | sprite.frame.sourceX, 26 | sprite.frame.sourceY, 27 | sprite.frame.width, 28 | sprite.frame.height, 29 | transform.scale.x > 0 ? 0 : -sprite.frame.width, 30 | transform.scale.y > 0 ? 0 : -sprite.frame.height, 31 | sprite.frame.width, 32 | sprite.frame.height, 33 | ); 34 | 35 | context.globalAlpha = 1; 36 | context.resetTransform(); 37 | } 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /packages/examples/src/demos/sprite-animation/systems/sprite-animation-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function spriteAnimationSystemFactory(world: World) { 5 | const spriteAnimatables = world.archetype("spriteAnimation", "sprite"); 6 | 7 | return function spriteAnimationSystem(dt: number) { 8 | for (const { spriteAnimation, sprite } of spriteAnimatables.entities) { 9 | if (spriteAnimation.finished && !spriteAnimation.loop) { 10 | // You could do something like spawn a SpriteAnimationFinishedEvent here. 11 | // Then handle it in another system. 12 | } 13 | 14 | spriteAnimation.delta += dt; 15 | 16 | if (spriteAnimation.delta >= spriteAnimation.frameRate) { 17 | spriteAnimation.delta = 0; 18 | 19 | spriteAnimation.currentFrame = 20 | (spriteAnimation.currentFrame + 1) % 21 | spriteAnimation.frameSequence.length; 22 | 23 | const frameIndex = 24 | spriteAnimation.frameSequence[spriteAnimation.currentFrame]; 25 | 26 | const frame = spriteAnimation.frames[frameIndex]; 27 | 28 | sprite.frame = frame; 29 | 30 | if ( 31 | spriteAnimation.currentFrame === 32 | spriteAnimation.frameSequence.length - 1 33 | ) { 34 | spriteAnimation.finished = true; 35 | } 36 | } 37 | } 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /packages/examples/src/demos/sprite-tweening/components/sprite.ts: -------------------------------------------------------------------------------- 1 | import { Frame } from "../entity.ts"; 2 | 3 | export const spriteFactory = (frame: Frame, opacity = 1) => ({ 4 | frame, 5 | opacity, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/examples/src/demos/sprite-tweening/components/tween.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Tween, 3 | TweenableEntity, 4 | TweenablePaths, 5 | TweenOptions, 6 | } from "../entity.ts"; 7 | 8 | export function tweenFactory

( 9 | property: P, 10 | options: TweenOptions, 11 | ): Tween { 12 | return { 13 | completed: false, 14 | progress: 0, 15 | iterations: 0, 16 | maxIterations: options.maxIterations ?? Infinity, 17 | time: 0, 18 | property, 19 | start: options.from, 20 | end: options.to, 21 | change: options.to - options.from, 22 | duration: options.duration / 1000, 23 | from: options.from, 24 | to: options.to, 25 | easing: options.easing, 26 | yoyo: options.yoyo ?? false, 27 | onComplete: options.onComplete ?? "remove", 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/examples/src/demos/sprite-tweening/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Demos - Sprite Tweening 8 | 9 | 10 | 11 | 12 | 20 | 21 | 22 |

23 |
24 | 25 | 31 |
32 | 33 |
34 | 35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /packages/examples/src/demos/sprite-tweening/structures/frame.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This represents a frame of a sprite sheet. 3 | */ 4 | export const frameFactory = ( 5 | sourceX: number, 6 | sourceY: number, 7 | width: number, 8 | height: number, 9 | ) => ({ 10 | sourceX, 11 | sourceY, 12 | width, 13 | height, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/examples/src/demos/sprite-tweening/systems/rendering-system.ts: -------------------------------------------------------------------------------- 1 | import { World } from "objecs"; 2 | import { Entity } from "../entity.ts"; 3 | 4 | export function renderingSystemFactory( 5 | world: World, 6 | context: CanvasRenderingContext2D, 7 | ship: HTMLImageElement, 8 | ) { 9 | const renderables = world.archetype("sprite", "transform"); 10 | const canvas = context.canvas; 11 | 12 | return function renderingSystem(_dt: number) { 13 | context.clearRect(0, 0, canvas.width, canvas.height); 14 | 15 | for (const entity of renderables.entities) { 16 | const { sprite, transform } = entity; 17 | 18 | context.globalAlpha = sprite.opacity; 19 | context.translate(transform.position.x, transform.position.y); 20 | context.scale(transform.scale.x, transform.scale.y); 21 | context.rotate(transform.rotation); 22 | 23 | context.drawImage( 24 | ship, 25 | sprite.frame.sourceX, 26 | sprite.frame.sourceY, 27 | sprite.frame.width, 28 | sprite.frame.height, 29 | -sprite.frame.width / 2, 30 | -sprite.frame.height / 2, 31 | sprite.frame.width, 32 | sprite.frame.height, 33 | ); 34 | 35 | context.globalAlpha = 1; 36 | context.resetTransform(); 37 | } 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /packages/examples/src/lib/array.ts: -------------------------------------------------------------------------------- 1 | export const rndFromList = (list: T[]): T => 2 | list[Math.floor(Math.random() * list.length)]; 3 | -------------------------------------------------------------------------------- /packages/examples/src/lib/asset-loader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Loads an image 3 | * @param path image URL 4 | * @returns 5 | */ 6 | export const loadImage = (path: string): Promise => 7 | new Promise((resolve, reject) => { 8 | const image = new Image(); 9 | image.onload = () => resolve(image); 10 | image.onerror = (err) => reject(err); 11 | 12 | image.src = path; 13 | }); 14 | -------------------------------------------------------------------------------- /packages/examples/src/lib/collision/aabb.ts: -------------------------------------------------------------------------------- 1 | type Vector2d = { 2 | x: number; 3 | y: number; 4 | }; 5 | 6 | type AABB = { 7 | position: Vector2d; 8 | width: number; 9 | height: number; 10 | }; 11 | 12 | /** 13 | * Return left of AABB 14 | */ 15 | export const aabbLeft = (aabb: AABB) => aabb.position.x; 16 | 17 | /** 18 | * Return right of AABB 19 | */ 20 | export const aabbRight = (aabb: AABB) => aabb.position.x + aabb.width; 21 | 22 | /** 23 | * Return top of AABB 24 | */ 25 | export const aabbTop = (aabb: AABB) => aabb.position.y; 26 | 27 | /** 28 | * Return bottom of AABB 29 | */ 30 | export const aabbBottom = (aabb: AABB) => aabb.position.y + aabb.height; 31 | 32 | /** 33 | * Axis-Aligned Bounding Box - no rotation 34 | * https://developer.mozilla.org/kab/docs/Games/Techniques/2D_collision_detection 35 | */ 36 | export const intersects = (aabb1: AABB, aabb2: AABB) => 37 | aabbLeft(aabb1) < aabbRight(aabb2) && 38 | aabbRight(aabb1) > aabbLeft(aabb2) && 39 | aabbTop(aabb1) < aabbBottom(aabb2) && 40 | aabbBottom(aabb1) > aabbTop(aabb2); 41 | -------------------------------------------------------------------------------- /packages/examples/src/lib/dom.ts: -------------------------------------------------------------------------------- 1 | export const obtainCanvasAndContext2d = (id?: string) => { 2 | const canvas = document.querySelector(id ?? "canvas"); 3 | 4 | if (canvas == null) { 5 | throw new Error("failed to obtain canvas element"); 6 | } 7 | 8 | const context = canvas.getContext("2d"); 9 | 10 | if (context == null) { 11 | throw new Error("failed to obtain canvas 2d context"); 12 | } 13 | 14 | return { 15 | canvas, 16 | context, 17 | }; 18 | }; 19 | 20 | /** 21 | * Helper to safely return a canvas rendering 2d context. 22 | * @param canvas canvas element 23 | * @returns 24 | */ 25 | export function obtainCanvas2dContext( 26 | canvas: HTMLCanvasElement, 27 | options?: CanvasRenderingContext2DSettings, 28 | ): CanvasRenderingContext2D; 29 | export function obtainCanvas2dContext( 30 | canvas: OffscreenCanvas, 31 | options?: CanvasRenderingContext2DSettings, 32 | ): OffscreenCanvasRenderingContext2D; 33 | export function obtainCanvas2dContext( 34 | canvas: HTMLCanvasElement | OffscreenCanvas, 35 | options?: CanvasRenderingContext2DSettings, 36 | ): CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D { 37 | const context = canvas.getContext("2d", options); 38 | 39 | if (context === null) { 40 | throw new Error("Could not obtain 2d context"); 41 | } 42 | 43 | return context; 44 | } 45 | -------------------------------------------------------------------------------- /packages/examples/src/lib/event-emitter.ts: -------------------------------------------------------------------------------- 1 | export class EventEmitter< 2 | T extends Record any>, 3 | > extends Map { 4 | public on(event: K, listener: T[K]) { 5 | if (!this.has(event)) { 6 | this.set(event, []); 7 | } 8 | 9 | this.get(event)?.push(listener); 10 | } 11 | 12 | public off(event: K, listener: T[K]) { 13 | if (!this.has(event)) { 14 | return; 15 | } 16 | 17 | const listeners = this.get(event) ?? []; 18 | const index = listeners.indexOf(listener); 19 | 20 | if (index === -1) { 21 | return; 22 | } 23 | 24 | listeners.splice(index, 1); 25 | } 26 | 27 | public emit(event: K, ...args: Parameters) { 28 | if (!this.has(event)) { 29 | return; 30 | } 31 | 32 | for (const listener of this.get(event) ?? []) { 33 | listener(args); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/examples/src/lib/math.ts: -------------------------------------------------------------------------------- 1 | export const rnd = (max: number, min = 0) => Math.random() * (max - min) + min; 2 | 3 | export const rndInt = (max: number, min = 0) => Math.floor(rnd(max, min)); 4 | 5 | export const deg2rad = (deg: number) => (deg * Math.PI) / 180; 6 | 7 | export const rad2deg = (rad: number) => (rad * 180) / Math.PI; 8 | -------------------------------------------------------------------------------- /packages/examples/src/lib/tween.ts: -------------------------------------------------------------------------------- 1 | // https://spicyyoghurt.com/tools/easing-functions 2 | 3 | export const Easing = { 4 | InSine: "inSine", 5 | Linear: "linear", 6 | OutSine: "outSine", 7 | OutQuart: "outQuart", 8 | } as const; 9 | 10 | export const easeLinear = ( 11 | time: number, 12 | start: number, 13 | change: number, 14 | duration: number, 15 | ): number => (change * time) / duration + start; 16 | 17 | export const easeInSine = ( 18 | time: number, 19 | start: number, 20 | change: number, 21 | duration: number, 22 | ): number => 23 | -change * Math.cos((time / duration) * (Math.PI / 2)) + change + start; 24 | 25 | export const easeOutSine = ( 26 | time: number, 27 | start: number, 28 | change: number, 29 | duration: number, 30 | ): number => change * Math.sin((time / duration) * (Math.PI / 2)) + start; 31 | 32 | export const easeOutQuart = ( 33 | time: number, 34 | start: number, 35 | change: number, 36 | duration: number, 37 | ): number => 38 | -change * ((time = time / duration - 1) * time * time * time - 1) + start; 39 | -------------------------------------------------------------------------------- /packages/examples/src/main.ts: -------------------------------------------------------------------------------- 1 | import "./style.css"; 2 | -------------------------------------------------------------------------------- /packages/examples/src/shared/components/color.ts: -------------------------------------------------------------------------------- 1 | import { SharedEntity } from "../shared-entity.ts"; 2 | 3 | export function colorFactory( 4 | color: SharedEntity["color"], 5 | ): NonNullable { 6 | return color ?? `#${Math.floor(Math.random() * 16777215).toString(16)}`; 7 | } 8 | -------------------------------------------------------------------------------- /packages/examples/src/shared/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./color.ts"; 2 | export * from "./rectangle.ts"; 3 | export * from "./transform.ts"; 4 | export * from "./position.ts"; 5 | export * from "./velocity.ts"; 6 | -------------------------------------------------------------------------------- /packages/examples/src/shared/components/position.ts: -------------------------------------------------------------------------------- 1 | import { SharedEntity } from "../shared-entity.ts"; 2 | 3 | export function positionFactory(): NonNullable { 4 | return { 5 | x: 0, 6 | y: 0, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /packages/examples/src/shared/components/rectangle.ts: -------------------------------------------------------------------------------- 1 | import { SharedEntity } from "../shared-entity.ts"; 2 | 3 | export function rectangleFactory( 4 | width: number, 5 | height: number, 6 | ): NonNullable { 7 | return { 8 | width, 9 | height, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/examples/src/shared/components/transform.ts: -------------------------------------------------------------------------------- 1 | import { SharedEntity } from "../shared-entity.ts"; 2 | 3 | export function transformFactory( 4 | position = { x: 0, y: 0 }, 5 | rotation = 0, 6 | scale = { x: 1, y: 1 }, 7 | ): NonNullable { 8 | return { 9 | position, 10 | rotation, 11 | scale, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/examples/src/shared/components/velocity.ts: -------------------------------------------------------------------------------- 1 | import { SharedEntity } from "../shared-entity.ts"; 2 | 3 | export function velocityFactory(): NonNullable { 4 | return { 5 | x: 0, 6 | y: 0, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /packages/examples/src/shared/shared-entity.ts: -------------------------------------------------------------------------------- 1 | type Vector2d = { 2 | x: number; 3 | y: number; 4 | }; 5 | 6 | type Rectangle = { 7 | width: number; 8 | height: number; 9 | }; 10 | 11 | export type SharedEntity = { 12 | color?: string; 13 | position?: Vector2d; 14 | rectangle?: Rectangle; 15 | transform?: { 16 | position: Vector2d; 17 | rotation: number; 18 | scale: Vector2d; 19 | }; 20 | velocity?: Vector2d; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/examples/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #1a1d21; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | padding: 0; 30 | display: flex; 31 | place-items: center; 32 | min-width: 320px; 33 | min-height: 100vh; 34 | } 35 | 36 | #app { 37 | max-width: 1280px; 38 | margin: 0 auto; 39 | padding: 2rem; 40 | text-align: center; 41 | } 42 | -------------------------------------------------------------------------------- /packages/examples/src/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/examples/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "inlineSourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": false, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noImplicitOverride": true, 18 | "skipLibCheck": true, 19 | "baseUrl": ".", 20 | "types": ["@types/wicg-file-system-access"], 21 | "allowImportingTsExtensions": true, 22 | "paths": { 23 | "#/*": ["src/*"], 24 | "#/assets/*": ["assets/*"] 25 | } 26 | }, 27 | "include": ["src", "basic"] 28 | } 29 | -------------------------------------------------------------------------------- /packages/examples/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import fs from 'node:fs/promises'; 3 | import { defineConfig } from 'vite'; 4 | 5 | const demoInputs = await fs 6 | .readdir(resolve(__dirname, './src/demos')) 7 | .then((directories) => 8 | Object.fromEntries( 9 | directories.map((directory) => [ 10 | directory, 11 | `src/demos/${directory}/index.html`, 12 | ]), 13 | ), 14 | ); 15 | 16 | export default defineConfig({ 17 | build: { 18 | rollupOptions: { 19 | input: { 20 | main: resolve(__dirname, 'index.html'), 21 | ...demoInputs, 22 | }, 23 | }, 24 | target: 'esnext', 25 | }, 26 | esbuild: { 27 | minifyIdentifiers: false, 28 | }, 29 | resolve: { 30 | // https://github.com/vitejs/vite/issues/88#issuecomment-784441588 31 | alias: { 32 | '#/assets': resolve(__dirname, 'assets'), 33 | '#': resolve(__dirname, 'src'), 34 | }, 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /packages/objecs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "trailingComma": "all", 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /packages/objecs/README.md: -------------------------------------------------------------------------------- 1 | # objECS 2 | 3 | Object ECS 4 | 5 | ## Tools 6 | 7 | - [changelogithub] 8 | - This generates a changelog for a github release using [Conventional Commits]. 9 | - [bumpp] 10 | - Interactive version prompt. 11 | 12 | ## How to Work 13 | 14 | - Develop like normal using [Conventional Commits]. 15 | - When you're ready to push a tag, run `bumpp`. 16 | - This will walk you through releasing a `tag`. 17 | - When you're ready to publish, create a github release and the workflow will take over. 18 | - This will use [changelogithub] to generate a changelog and publish to NPM. 19 | 20 | [bumpp]: https://www.npmjs.com/package/bumpp 21 | [changelogithub]: https://github.com/antfu/changelogithub 22 | [conventional commits]: https://www.conventionalcommits.org/en/v1.0.0/ 23 | -------------------------------------------------------------------------------- /packages/objecs/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["./dist/", "./README.md"], 3 | "watch": ["./src/"], 4 | "ext": "js,json,ts,tsx" 5 | } 6 | -------------------------------------------------------------------------------- /packages/objecs/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./world.js"; 2 | export * from "./archetype.js"; 3 | -------------------------------------------------------------------------------- /packages/objecs/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src", "!src/**/*.test.ts"], 5 | sourcemap: true, 6 | format: ["cjs", "esm"], 7 | dts: true, 8 | clean: true, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/objecs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { defineConfig } from "vite"; 3 | 4 | const config = defineConfig({ 5 | resolve: { 6 | // https://github.com/vitejs/vite/issues/88#issuecomment-784441588 7 | alias: { 8 | "#": path.resolve(__dirname, "src"), 9 | }, 10 | }, 11 | build: { 12 | target: "chrome101", 13 | }, 14 | }); 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in direct subdirs of packages/ 3 | - 'packages/*' 4 | # exclude packages that are inside test directories 5 | - '!**/test/**' -------------------------------------------------------------------------------- /tasks.md: -------------------------------------------------------------------------------- 1 | # Tasks 2 | 3 | [ ] Port https://wickedengine.net/2019/09/entity-component-system/ ideas. Archetypes _need_ to support sorting. 4 | -------------------------------------------------------------------------------- /workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "✨ root", 5 | "path": "." 6 | }, 7 | { 8 | "name": "📦 objecs", 9 | "path": "packages/objecs" 10 | }, 11 | { 12 | "name": "📦 benchmark", 13 | "path": "packages/benchmark" 14 | }, 15 | { 16 | "name": "📦 ecs-benchmark", 17 | "path": "packages/ecs-benchmark" 18 | }, 19 | { 20 | "name": "📦 examples", 21 | "path": "packages/examples" 22 | } 23 | ], 24 | "settings": { 25 | "typescript.preferences.importModuleSpecifierEnding": "js" 26 | } 27 | } 28 | --------------------------------------------------------------------------------