├── .github
└── workflows
│ └── coverage.yaml
├── .gitignore
├── .nvmrc
├── LICENSE
├── README.md
├── docs
├── .nojekyll
├── assets
│ ├── highlight.css
│ ├── icons.css
│ ├── icons.png
│ ├── icons@2x.png
│ ├── main.js
│ ├── search.js
│ ├── style.css
│ ├── widgets.png
│ └── widgets@2x.png
├── classes
│ ├── Types.OpaqueTag.html
│ └── World.EntityNotRealError.html
├── enums
│ └── Format.FormatKind.html
├── index.html
├── modules.html
└── modules
│ ├── Cache.html
│ ├── Entity.html
│ ├── Format.html
│ ├── Query.html
│ ├── Schema.html
│ ├── Signal.html
│ ├── SparseMap.html
│ ├── Type.html
│ ├── Types.html
│ └── World.html
├── examples
├── compat
│ ├── README.md
│ ├── index.html
│ ├── src
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vite.config.js
├── graph
│ ├── README.md
│ ├── index.html
│ ├── src
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vite.config.js
└── noise
│ ├── README.md
│ ├── index.html
│ ├── src
│ └── index.ts
│ ├── tsconfig.json
│ └── vite.config.js
├── graph.png
├── jest.config.js
├── lib
├── build
│ ├── optimize.js
│ └── plugins
│ │ └── transform-remove-invariant.js
├── src
│ ├── archetype.ts
│ ├── archetype_graph.ts
│ ├── cache.spec.ts
│ ├── cache.ts
│ ├── component.ts
│ ├── component_set.ts
│ ├── debug.ts
│ ├── encode.ts
│ ├── entity.spec.ts
│ ├── entity.ts
│ ├── format.ts
│ ├── index.ts
│ ├── query.ts
│ ├── schema.ts
│ ├── signal.ts
│ ├── sparse_map.spec.ts
│ ├── sparse_map.ts
│ ├── symbols.ts
│ ├── type.spec.ts
│ ├── type.ts
│ ├── types.ts
│ ├── world.spec.ts
│ └── world.ts
└── tsconfig.json
├── package.json
├── perf
├── index.html
├── src
│ ├── index.ts
│ ├── perf.ts
│ └── suite
│ │ ├── archetype_insert.perf.ts
│ │ ├── index.ts
│ │ ├── iter_binary.perf.ts
│ │ ├── iter_hybrid.perf.ts
│ │ ├── iter_native.perf.ts
│ │ └── types.ts
├── tsconfig.json
└── vite.config.js
├── pnpm-lock.yaml
├── prettier.config.cjs
├── test
├── add_remove.spec.ts
├── query_counts.spec.ts
└── query_dynamic.spec.ts
└── tsconfig.json
/.github/workflows/coverage.yaml:
--------------------------------------------------------------------------------
1 | name: codecov
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | strategy:
10 | matrix:
11 | node-version: [14.x, 16.x]
12 |
13 | steps:
14 | - name: checkout
15 | uses: actions/checkout@v2
16 | with:
17 | fetch-depth: 2
18 |
19 | - name: setup node ${{ matrix.node-version }}
20 | uses: actions/setup-node@v1
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 |
24 | - name: install dependencies
25 | run: npm install
26 |
27 | - name: run tests
28 | run: npm test -- --coverage
29 |
30 | - name: upload coverage
31 | uses: codecov/codecov-action@v1
32 | with:
33 | token: ${{ secrets.CODECOV_TOKEN }}
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | tsconfig.tsbuildinfo
3 | dist
4 | .pnpm-debug.log
5 | coverage
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v16.8.0
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2021 Eric McDaniel
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # harmony-ecs
2 |
3 | A compatibility and performance-focused Entity-Component-System (ECS) for JavaScript.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ## Features
15 |
16 | - Hybrid [SoA and AoS](https://en.wikipedia.org/wiki/AoS_and_SoA) storage
17 | - Complex, scalar, and tag components
18 | - Fast iteration and mutation [[1]](https://github.com/ddmills/js-ecs-benchmarks) [[2]](https://github.com/noctjs/ecs-benchmark)
19 | - Fast insert/relocate and auto-updating queries via [connected archetype graph](./graph.png)
20 | - Compatible with third-party libraries like Three.js, Pixi, and Cannon
21 |
22 | ## Installation
23 |
24 | ```sh
25 | npm install harmony-ecs
26 | ```
27 |
28 | ## Documentation
29 |
30 | - [Wiki](https://github.com/3mcd/harmony-ecs/wiki)
31 | - [API docs](https://3mcd.github.io/harmony-ecs)
32 |
33 | ## Examples
34 |
35 | This repo contains examples in the [`examples`](./examples) directory. You can run each project using `npm run example:*`, where `*` is the name of an example subdirectory.
36 |
37 | Below is a sample of Harmony's API, where a TypedArray `Velocity` component is used to update an object `Position` component:
38 |
39 | ```ts
40 | import { World, Schema, Entity, Query, Format } from "harmony-ecs"
41 |
42 | const Vector2 = {
43 | x: Format.float64,
44 | y: Format.float64,
45 | }
46 | const world = World.make(1_000_000)
47 | const Position = Schema.make(world, Vector2)
48 | const Velocity = Schema.makeBinary(world, Vector2)
49 | const Kinetic = [Position, Velocity] as const
50 |
51 | for (let i = 0; i < 1_000_000; i++) {
52 | Entity.make(world, Kinetic)
53 | }
54 |
55 | const kinetics = Query.make(world, Kinetic)
56 |
57 | for (const [entities, [p, v]] of kinetics) {
58 | for (let i = 0; i < entities.length; i++) {
59 | p[i].x += v.x[i]
60 | p[i].y += v.y[i]
61 | }
62 | }
63 | ```
64 |
65 | Harmony does not modify objects, making it highly compatible with third-party libraries. Take the following example where an entity is composed of a Three.js mesh, Cannon.js rigid body, and some proprietary TypedArray-backed data.
66 |
67 | ```ts
68 | const Vector3 = { x: Format.float64 /* etc */ }
69 | const Mesh = Schema.make(world, { position: Vector3 })
70 | const Body = Schema.make(world, { position: Vector3 })
71 | const PlayerInfo = Schema.makeBinary(world, { id: Format.uint32 })
72 | const Player = [Mesh, Body, PlayerInfo] as const
73 |
74 | const mesh = new Three.Mesh(new Three.SphereGeometry(), new Three.MeshBasicMaterial())
75 | const body = new Cannon.Body({ mass: 1, shape: new Cannon.Sphere(1) })
76 |
77 | Entity.make(world, Player, [mesh, body, { id: 123 }])
78 | ```
79 |
80 | Note that we still need to define the shape of third party objects, as seen in the `Mesh` and `Body` variables. This supplies Harmony with static type information for queries and provides the ECS with important runtime information for serialization, etc.
81 |
82 | ## Performance Tests
83 |
84 | Run the performance test suite using `npm run perf:node` or `npm perf:browser`. Example output:
85 |
86 | ```
87 | iterBinary
88 | ----------
89 | iter
90 | iterations 100
91 | ┌─────────┬────────────┐
92 | │ (index) │ Values │
93 | ├─────────┼────────────┤
94 | │ average │ '12.46 ms' │
95 | │ median │ '12.03 ms' │
96 | │ min │ '11.71 ms' │
97 | │ max │ '18.76 ms' │
98 | └─────────┴────────────┘
99 | ```
100 |
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.
--------------------------------------------------------------------------------
/docs/assets/highlight.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --light-hl-0: #000000;
3 | --dark-hl-0: #D4D4D4;
4 | --light-hl-1: #AF00DB;
5 | --dark-hl-1: #C586C0;
6 | --light-hl-2: #001080;
7 | --dark-hl-2: #9CDCFE;
8 | --light-hl-3: #A31515;
9 | --dark-hl-3: #CE9178;
10 | --light-hl-4: #0000FF;
11 | --dark-hl-4: #569CD6;
12 | --light-hl-5: #0070C1;
13 | --dark-hl-5: #4FC1FF;
14 | --light-hl-6: #795E26;
15 | --dark-hl-6: #DCDCAA;
16 | --light-hl-7: #098658;
17 | --dark-hl-7: #B5CEA8;
18 | --light-hl-8: #008000;
19 | --dark-hl-8: #6A9955;
20 | --light-hl-9: #267F99;
21 | --dark-hl-9: #4EC9B0;
22 | --light-code-background: #FFFFFF;
23 | --dark-code-background: #1E1E1E;
24 | }
25 |
26 | @media (prefers-color-scheme: light) { :root {
27 | --hl-0: var(--light-hl-0);
28 | --hl-1: var(--light-hl-1);
29 | --hl-2: var(--light-hl-2);
30 | --hl-3: var(--light-hl-3);
31 | --hl-4: var(--light-hl-4);
32 | --hl-5: var(--light-hl-5);
33 | --hl-6: var(--light-hl-6);
34 | --hl-7: var(--light-hl-7);
35 | --hl-8: var(--light-hl-8);
36 | --hl-9: var(--light-hl-9);
37 | --code-background: var(--light-code-background);
38 | } }
39 |
40 | @media (prefers-color-scheme: dark) { :root {
41 | --hl-0: var(--dark-hl-0);
42 | --hl-1: var(--dark-hl-1);
43 | --hl-2: var(--dark-hl-2);
44 | --hl-3: var(--dark-hl-3);
45 | --hl-4: var(--dark-hl-4);
46 | --hl-5: var(--dark-hl-5);
47 | --hl-6: var(--dark-hl-6);
48 | --hl-7: var(--dark-hl-7);
49 | --hl-8: var(--dark-hl-8);
50 | --hl-9: var(--dark-hl-9);
51 | --code-background: var(--dark-code-background);
52 | } }
53 |
54 | body.light {
55 | --hl-0: var(--light-hl-0);
56 | --hl-1: var(--light-hl-1);
57 | --hl-2: var(--light-hl-2);
58 | --hl-3: var(--light-hl-3);
59 | --hl-4: var(--light-hl-4);
60 | --hl-5: var(--light-hl-5);
61 | --hl-6: var(--light-hl-6);
62 | --hl-7: var(--light-hl-7);
63 | --hl-8: var(--light-hl-8);
64 | --hl-9: var(--light-hl-9);
65 | --code-background: var(--light-code-background);
66 | }
67 |
68 | body.dark {
69 | --hl-0: var(--dark-hl-0);
70 | --hl-1: var(--dark-hl-1);
71 | --hl-2: var(--dark-hl-2);
72 | --hl-3: var(--dark-hl-3);
73 | --hl-4: var(--dark-hl-4);
74 | --hl-5: var(--dark-hl-5);
75 | --hl-6: var(--dark-hl-6);
76 | --hl-7: var(--dark-hl-7);
77 | --hl-8: var(--dark-hl-8);
78 | --hl-9: var(--dark-hl-9);
79 | --code-background: var(--dark-code-background);
80 | }
81 |
82 | .hl-0 { color: var(--hl-0); }
83 | .hl-1 { color: var(--hl-1); }
84 | .hl-2 { color: var(--hl-2); }
85 | .hl-3 { color: var(--hl-3); }
86 | .hl-4 { color: var(--hl-4); }
87 | .hl-5 { color: var(--hl-5); }
88 | .hl-6 { color: var(--hl-6); }
89 | .hl-7 { color: var(--hl-7); }
90 | .hl-8 { color: var(--hl-8); }
91 | .hl-9 { color: var(--hl-9); }
92 | pre, code { background: var(--code-background); }
93 |
--------------------------------------------------------------------------------
/docs/assets/icons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3mcd/harmony-ecs/74acbe129d8e7234c6a885bfd83aae335dd290c6/docs/assets/icons.png
--------------------------------------------------------------------------------
/docs/assets/icons@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3mcd/harmony-ecs/74acbe129d8e7234c6a885bfd83aae335dd290c6/docs/assets/icons@2x.png
--------------------------------------------------------------------------------
/docs/assets/widgets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3mcd/harmony-ecs/74acbe129d8e7234c6a885bfd83aae335dd290c6/docs/assets/widgets.png
--------------------------------------------------------------------------------
/docs/assets/widgets@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3mcd/harmony-ecs/74acbe129d8e7234c6a885bfd83aae335dd290c6/docs/assets/widgets@2x.png
--------------------------------------------------------------------------------
/docs/classes/Types.OpaqueTag.html:
--------------------------------------------------------------------------------
1 | OpaqueTag | harmony-ecs Legend Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/classes/World.EntityNotRealError.html:
--------------------------------------------------------------------------------
1 | EntityNotRealError | harmony-ecs Methods Static capture Stack Tracecapture Stack Trace( targetObject: object , constructorOpt?: Function ) : void Parameters targetObject: object Optional constructorOpt: Function Returns void Properties Static Optional prepare Stack Traceprepare Stack Trace?: ( err: Error , stackTraces: CallSite [] ) => any
Type declaration ( err: Error , stackTraces: CallSite [] ) : any Parameters err: Error stackTraces: CallSite [] Returns any Static stack Trace Limitstack Trace Limit: number
Optional stackstack?: string
Legend Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/enums/Format.FormatKind.html:
--------------------------------------------------------------------------------
1 | FormatKind | harmony-ecs Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules.html:
--------------------------------------------------------------------------------
1 | harmony-ecs Type aliases Data Data
< $SchemaId > : $SchemaId extends Schema . Id < infer $Schema
> ? $Schema extends Schema.ComplexSchema ? DataOfShape < Schema.Shape < $Schema > > : never : never Type parameters Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules/Format.html:
--------------------------------------------------------------------------------
1 | Format | harmony-ecs Type aliases Format Format< $Kind , $Binary > : { kind: $Kind ; binary: $Binary }
Type parameters Type declaration kind: $Kind binary: $Binary Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules/Signal.html:
--------------------------------------------------------------------------------
1 | Signal | harmony-ecs Type aliases Subscriber Subscriber< T > : ( t: T ) => void
Type parameters Functions subscribe Type parameters Parameters Returns ( ) => void dispatch Type parameters Parameters Returns void Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules/World.html:
--------------------------------------------------------------------------------
1 | World | harmony-ecs Type aliases Struct Struct: { rootArchetype: Archetype.Struct ; entityHead: number ; entityIndex: ( Archetype.Struct | undefined ) [] ; schemaIndex: Schema.AnySchema [] ; size: number }
Type declaration root Archetype: Archetype.Struct entity Head: number entity Index: ( Archetype.Struct | undefined ) [] schema Index: Schema.AnySchema [] size: number Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/examples/compat/README.md:
--------------------------------------------------------------------------------
1 | # harmony/compat
2 |
3 | A simple example of using third-party libraries Three.js and cannon-es with Harmony.
4 |
--------------------------------------------------------------------------------
/examples/compat/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | harmony-ecs/examples/compat
8 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/examples/compat/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as Cannon from "cannon-es"
2 | import * as Three from "three"
3 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"
4 | import { Format, World, Schema, Query, Entity, Debug } from "../../../lib/src"
5 |
6 | const BOUNCE_IMPULSE = new Cannon.Vec3(0, 10, 0)
7 |
8 | const Vec3 = { x: Format.float64, y: Format.float64, z: Format.float64 }
9 | const Quaternion = {
10 | x: Format.float64,
11 | y: Format.float64,
12 | z: Format.float64,
13 | w: Format.float64,
14 | }
15 | const Object3D = { position: Vec3, quaternion: Quaternion }
16 | const world = World.make(1_000)
17 | const Body = Schema.make(world, Object3D)
18 | const Mesh = Schema.make(world, Object3D)
19 | const Bounce = Schema.makeBinary(world, Format.float64)
20 | const Tag = Schema.makeTag(world)
21 |
22 | const Tagged = [Tag] as const
23 | const Box = [Body, Mesh] as const
24 | const BoxBounce = [...Box, Bounce] as const
25 | const BoxBounceTagged = [...BoxBounce, ...Tagged] as const
26 |
27 | const canvas = document.getElementById("game") as HTMLCanvasElement
28 | const renderer = new Three.WebGLRenderer({ antialias: true, canvas })
29 | const camera = new Three.PerspectiveCamera(45, 1, 0.1, 2000000)
30 | const controls = new OrbitControls(camera, renderer.domElement)
31 | const scene = new Three.Scene()
32 | const simulation = new Cannon.World({ gravity: new Cannon.Vec3(0, -9.81, 0) })
33 | const bodies = Query.make(world, Box)
34 | const bouncing = Query.make(world, BoxBounce)
35 | const bouncingWithTag = Query.make(world, BoxBounceTagged)
36 | const bouncingWithoutTag = Query.make(world, BoxBounce, Query.not([Tag]))
37 |
38 | scene.add(new Three.AmbientLight(0x404040), new Three.DirectionalLight(0xffffff, 0.5))
39 |
40 | function scale() {
41 | camera.aspect = window.innerWidth / window.innerHeight
42 | camera.updateProjectionMatrix()
43 | renderer.setSize(window.innerWidth, window.innerHeight)
44 | }
45 |
46 | window.addEventListener("resize", scale, false)
47 | scale()
48 |
49 | function createBox(
50 | position = new Cannon.Vec3(0, 0, 0),
51 | halfExtents = new Cannon.Vec3(0.5, 0.5, 0.5),
52 | type: Cannon.BodyType = Cannon.Body.DYNAMIC,
53 | color = 0xff0000,
54 | mass = 1,
55 | ) {
56 | const shape = new Cannon.Box(halfExtents)
57 | const body = new Cannon.Body({ mass, type, position, shape })
58 | const geometry = new Three.BoxGeometry(
59 | halfExtents.x * 2,
60 | halfExtents.y * 2,
61 | halfExtents.z * 2,
62 | )
63 | const material = new Three.MeshLambertMaterial({ color, transparent: true })
64 | const mesh = new Three.Mesh(geometry, material)
65 | return [body, mesh]
66 | }
67 |
68 | function createGround() {
69 | return createBox(
70 | new Cannon.Vec3(0, 0, 0),
71 | new Cannon.Vec3(25, 0.1, 25),
72 | Cannon.Body.STATIC,
73 | 0xffffff,
74 | 0,
75 | )
76 | }
77 |
78 | function copyBodyToMesh(body: Cannon.Body, mesh: Three.Mesh) {
79 | const { x, y, z } = body.interpolatedPosition
80 | const { x: qx, y: qy, z: qz, w: qw } = body.interpolatedQuaternion
81 | mesh.position.x = x
82 | mesh.position.y = y
83 | mesh.position.z = z
84 | mesh.quaternion.x = qx
85 | mesh.quaternion.y = qy
86 | mesh.quaternion.z = qz
87 | mesh.quaternion.w = qw
88 | }
89 |
90 | function random(scale = 2) {
91 | return (0.5 - Math.random()) * scale
92 | }
93 |
94 | let spawnInit = true
95 |
96 | function spawn() {
97 | if (spawnInit) {
98 | // spawn ground
99 | Entity.make(world, [Body, Mesh], createGround())
100 | // spawn boxes
101 | for (let i = 0; i < 100; i++) {
102 | const entity = Entity.make(
103 | world,
104 | [Body, Mesh],
105 | createBox(new Cannon.Vec3(random(25), 20, random(25))),
106 | )
107 | if (i % 2 === 0) Entity.set(world, entity, [Bounce], [0])
108 | }
109 | spawnInit = false
110 | }
111 | }
112 |
113 | let physicsInit = true
114 |
115 | function physics(dt: number) {
116 | const now = performance.now()
117 | if (physicsInit) {
118 | for (let i = 0; i < bodies.length; i++) {
119 | const [entities, [b]] = bodies[i]!
120 | for (let j = 0; j < entities.length; j++) {
121 | simulation.addBody(
122 | // manually cast the component to it's true type since we lose type
123 | // information bb storing it in Harmony
124 | b[j] as Cannon.Body,
125 | )
126 | }
127 | }
128 | physicsInit = false
129 | }
130 | for (let i = 0; i < bouncing.length; i++) {
131 | const [entities, [b, , bo]] = bouncing[i]!
132 | for (let j = 0; j < entities.length; j++) {
133 | const entity = entities[j]!
134 | const body = b[j] as Cannon.Body
135 | if (now - bo[j]! >= 5000) {
136 | bo[j] = now
137 | body.applyImpulse(BOUNCE_IMPULSE)
138 | if (Entity.has(world, entity, Tagged)) {
139 | Entity.unset(world, entity, Tagged)
140 | } else {
141 | Entity.set(world, entity, Tagged)
142 | }
143 | }
144 | }
145 | }
146 | simulation.step(1 / 60, dt / 1000, 5)
147 | }
148 |
149 | let renderInit = true
150 |
151 | function render() {
152 | // add camera to scene
153 | if (renderInit) {
154 | scene.add(camera)
155 | camera.position.x = 50
156 | camera.position.y = 50
157 | camera.position.z = 50
158 | for (let i = 0; i < bodies.length; i++) {
159 | const [entities, [, m]] = bodies[i]!
160 | for (let j = 0; j < entities.length; j++) {
161 | scene.add(m[j] as Three.Mesh)
162 | }
163 | }
164 | renderInit = false
165 | }
166 | for (let i = 0; i < bodies.length; i++) {
167 | const [entities, [b, m]] = bodies[i]!
168 | for (let j = 0; j < entities.length; j++) {
169 | copyBodyToMesh(b[j] as Cannon.Body, m[j] as Three.Mesh)
170 | }
171 | }
172 | for (let i = 0; i < bouncingWithTag.length; i++) {
173 | const [entities, [, m]] = bouncingWithTag[i]!
174 | for (let j = 0; j < entities.length; j++) {
175 | ;((m[j] as Three.Mesh).material as Three.MeshLambertMaterial).color.set(0xff0000)
176 | }
177 | }
178 | for (let i = 0; i < bouncingWithoutTag.length; i++) {
179 | const [entities, [, m]] = bouncingWithoutTag[i]!
180 | for (let j = 0; j < entities.length; j++) {
181 | ;((m[j] as Three.Mesh).material as Three.MeshLambertMaterial).color.set(0x00ffff)
182 | }
183 | }
184 |
185 | // render scene
186 | controls.update()
187 | renderer.render(scene, camera)
188 | }
189 |
190 | let prev = 0
191 |
192 | function step(now: number) {
193 | spawn()
194 | physics(now - (prev || now))
195 | render()
196 | requestAnimationFrame(step)
197 | prev = now
198 | }
199 |
200 | requestAnimationFrame(step)
201 |
--------------------------------------------------------------------------------
/examples/compat/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "noEmit": true
6 | },
7 | "references": [{ "path": "../../lib" }]
8 | }
9 |
--------------------------------------------------------------------------------
/examples/compat/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite"
2 |
3 | export default defineConfig({
4 | root: ".",
5 | build: {
6 | rollupOptions: {
7 | output: {
8 | entryFileNames: `assets/[name].js`,
9 | chunkFileNames: `assets/[name].js`,
10 | assetFileNames: `assets/[name].[ext]`,
11 | },
12 | },
13 | },
14 | })
15 |
--------------------------------------------------------------------------------
/examples/graph/README.md:
--------------------------------------------------------------------------------
1 | # harmony/graph
2 |
--------------------------------------------------------------------------------
/examples/graph/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | harmony-ecs/examples/compat
8 |
25 |
26 |
27 |
28 |
29 |
30 |
Insert
31 |
32 |
33 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/examples/graph/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as Archetype from "../../../lib/src/archetype"
2 | import { Edge, Network, Node } from "vis-network"
3 | import { DataSet } from "vis-data"
4 | import { Entity, Schema, World } from "../../../lib/src"
5 | import * as Type from "../../../lib/src/type"
6 |
7 | function getColor(value: number) {
8 | return ["hsl(", ((1 - value) * 120).toString(10), ",100%,50%)"].join("")
9 | }
10 |
11 | function makeTypeId(type: Type.Struct) {
12 | return type.join(",")
13 | }
14 |
15 | const $log = document.getElementById("log")!
16 | const $Type = document.getElementById("type") as HTMLInputElement
17 | const $network = document.getElementById("network")!
18 | const $insert = document.getElementById("insert")!
19 |
20 | const world = World.make(1_000_000)
21 | const nodes = new DataSet([] as (Node & { count: number })[])
22 | const edges = new DataSet([] as Edge[])
23 | const schemas = Array(10)
24 | .fill(undefined)
25 | .map(() => Schema.make(world, {}))
26 | const edgeIds = new Set()
27 |
28 | function maybeMakeEdge(a: Type.Struct, b: Type.Struct) {
29 | const from = makeTypeId(a)
30 | const to = makeTypeId(b)
31 | const id = a.length > b.length ? `${to}–${from}` : `${from}–${to}`
32 | if (edgeIds.has(id)) {
33 | return
34 | }
35 | edgeIds.add(id)
36 | edges.add({ id, from, to })
37 | }
38 |
39 | function onTableInsert(archetype: Archetype.Struct) {
40 | const id = makeTypeId(archetype.type)
41 | nodes.add({ id: id, label: `(${id})`, count: 0, level: archetype.type.length })
42 | archetype.edgesSet.forEach(({ type }) => maybeMakeEdge(archetype.type, type))
43 | archetype.edgesUnset.forEach(({ type }) => maybeMakeEdge(archetype.type, type))
44 | }
45 |
46 | subscribe(world.rootArchetype.onTableInsert, onTableInsert)
47 | onTableInsert(world.rootArchetype)
48 |
49 | let max = 0
50 |
51 | function onInsertEntity() {
52 | const type = Array.from($Type.value.split(/[\s,]+/).map(Number))
53 | .sort((a, b) => a - b)
54 | .map(id => schemas[id]!)
55 | const id = makeTypeId(type)
56 | Entity.make(world, type)
57 | const node = nodes.get(id)!
58 | const count = node.count + 1
59 | if (count > max) {
60 | max = count
61 | }
62 | nodes.update({ id, count })
63 | nodes.forEach(node =>
64 | nodes.update({
65 | id: node.id,
66 | color: { background: node.count > 0 ? getColor(node.count / max) : undefined },
67 | }),
68 | )
69 | $log.textContent += `${id}\n`
70 | $Type.value = ""
71 | }
72 |
73 | $Type.addEventListener("keydown", e => {
74 | if (e.key === "Enter") {
75 | onInsertEntity()
76 | }
77 | })
78 | $insert.addEventListener("click", onInsertEntity)
79 |
80 | new Network(
81 | $network,
82 | {
83 | nodes,
84 | edges,
85 | },
86 | {
87 | nodes: {
88 | physics: false,
89 | },
90 | layout: {
91 | hierarchical: {
92 | // enabled: true,
93 | levelSeparation: 200,
94 | nodeSpacing: 70,
95 | treeSpacing: 100,
96 | blockShifting: true,
97 | edgeMinimization: true,
98 | parentCentralization: true,
99 | direction: "LR",
100 | sortMethod: "directed", // hubsize, directed,
101 | },
102 | },
103 | },
104 | )
105 |
106 | import * as Harmony from "../../../lib/src"
107 | import { subscribe } from "../../../lib/src/signal"
108 |
109 | //@ts-ignore
110 | globalThis.world = world
111 | // @ts-ignore
112 | globalThis.Harmony = Harmony
113 |
--------------------------------------------------------------------------------
/examples/graph/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "noEmit": true
6 | },
7 | "references": [{ "path": "../../lib" }]
8 | }
9 |
--------------------------------------------------------------------------------
/examples/graph/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite"
2 |
3 | export default defineConfig({
4 | root: ".",
5 | build: {
6 | rollupOptions: {
7 | output: {
8 | entryFileNames: `assets/[name].js`,
9 | chunkFileNames: `assets/[name].js`,
10 | assetFileNames: `assets/[name].[ext]`,
11 | },
12 | },
13 | },
14 | })
15 |
--------------------------------------------------------------------------------
/examples/noise/README.md:
--------------------------------------------------------------------------------
1 | # harmony/noise
2 |
3 | A 500x500 canvas where each pixel is a simulated entity.
4 |
5 | Demonstrates use of `not` queries, binary components and entity-component manipulation.
6 |
--------------------------------------------------------------------------------
/examples/noise/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | harmony-ecs/examples/noise
8 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/examples/noise/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Format, Schema, Entity, Query, World } from "../../../lib/src"
2 |
3 | const SIZE = 500
4 | const COUNT = SIZE * SIZE
5 | const canvas = document.getElementById("game") as HTMLCanvasElement
6 | const world = World.make(COUNT)
7 | const context = canvas.getContext("2d")!
8 | const image = context.getImageData(0, 0, SIZE, SIZE)
9 | const buf = new ArrayBuffer(image.data.length)
10 | const buf8 = new Uint8ClampedArray(buf)
11 | const buf32 = new Uint32Array(buf)
12 |
13 | const Position = Schema.makeBinary(world, { x: Format.uint32, y: Format.uint32 })
14 | const Fixed = Schema.makeBinary(world, Format.uint32)
15 | const Point = [Position] as const
16 | const PointFixed = [...Point, Fixed] as const
17 |
18 | for (let i = 0; i < SIZE; i++) {
19 | for (let j = 0; j < SIZE; j++) {
20 | Entity.make(world, Point, [{ x: i, y: j }])
21 | }
22 | }
23 |
24 | const noise = Query.make(world, Point, Query.not([Fixed]))
25 | const fixed = Query.make(world, PointFixed)
26 | const rand: number[] = []
27 |
28 | let randomHead = 1e6 + 90_000
29 | while (randomHead--) rand.push(Math.round(Math.random()))
30 |
31 | function step() {
32 | for (let i = 0; i < noise.length; i++) {
33 | const [e, [p]] = noise[i]!
34 | for (let j = 0; j < e.length; j++) {
35 | const x = p.x[j]!
36 | const y = p.y[j]!
37 | const random =
38 | (++randomHead >= rand.length ? rand[(randomHead = 0)]! : rand[randomHead]!) * 50
39 | buf32[y * SIZE + x] = (255 << 24) | (random << 16) | (random << 8) | random
40 | }
41 | }
42 | for (let i = 0; i < fixed.length; i++) {
43 | const [e, [p, f]] = fixed[i]!
44 | for (let j = 0; j < e.length; j++) {
45 | const x = p.x[j]!
46 | const y = p.y[j]!
47 | buf32[y * SIZE + x] = f[j]!
48 | }
49 | }
50 | image.data.set(buf8)
51 | context.putImageData(image, 0, 0)
52 | requestAnimationFrame(step)
53 | }
54 |
55 | let rect = canvas.getBoundingClientRect()
56 | let sx = 0
57 | let sy = 0
58 |
59 | function resize() {
60 | rect = canvas.getBoundingClientRect()
61 | sx = canvas.width / rect.width
62 | sy = canvas.height / rect.height
63 | }
64 |
65 | function onMouseMove(event: MouseEvent) {
66 | const x = Math.round((event.clientX - rect.left) * sx)
67 | const y = Math.round((event.clientY - rect.top) * sy)
68 | for (let i = 0; i < noise.length; i++) {
69 | const [e, [p]] = noise[i]!
70 | for (let j = 0; j < e.length; j++) {
71 | if (p.x[j] === x && p.y[j] === y) {
72 | Entity.set(world, e[j]!, PointFixed, [
73 | ,
74 | buf32[y * SIZE + x]! | (100 << 16) | (50 << 8),
75 | ])
76 | break
77 | }
78 | }
79 | }
80 | }
81 |
82 | window.addEventListener("resize", resize)
83 | canvas.addEventListener("mousemove", onMouseMove)
84 |
85 | resize()
86 | step()
87 |
--------------------------------------------------------------------------------
/examples/noise/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "noEmit": true
6 | },
7 | "references": [{ "path": "../../lib" }]
8 | }
9 |
--------------------------------------------------------------------------------
/examples/noise/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite"
2 |
3 | export default defineConfig({
4 | root: ".",
5 | build: {
6 | rollupOptions: {
7 | output: {
8 | entryFileNames: `assets/[name].js`,
9 | chunkFileNames: `assets/[name].js`,
10 | assetFileNames: `assets/[name].[ext]`,
11 | },
12 | },
13 | },
14 | })
15 |
--------------------------------------------------------------------------------
/graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3mcd/harmony-ecs/74acbe129d8e7234c6a885bfd83aae335dd290c6/graph.png
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | preset: "ts-jest",
3 | testEnvironment: "node",
4 | }
5 |
--------------------------------------------------------------------------------
/lib/build/optimize.js:
--------------------------------------------------------------------------------
1 | import babel from "@babel/core"
2 | import { resolve } from "path"
3 | import { promises } from "fs"
4 |
5 | const { readdir, writeFile } = promises
6 |
7 | const options = {
8 | plugins: [
9 | "./lib/build/plugins/transform-remove-invariant.js",
10 | ["add-import-extension", { extension: "js", replace: true }],
11 | ],
12 | }
13 |
14 | async function* getFiles(dir) {
15 | const dirents = await readdir(dir, { withFileTypes: true })
16 | for (const dirent of dirents) {
17 | const res = resolve(dir, dirent.name)
18 | if (dirent.isDirectory()) {
19 | yield* getFiles(res)
20 | } else {
21 | yield res
22 | }
23 | }
24 | }
25 |
26 | ;(async () => {
27 | for await (const file of getFiles(`./lib/dist`)) {
28 | if (!/\.js$/.test(file)) continue
29 | const transformed = await babel.transformFileAsync(file, options)
30 | await writeFile(file, transformed.code)
31 | }
32 | })()
33 |
--------------------------------------------------------------------------------
/lib/build/plugins/transform-remove-invariant.js:
--------------------------------------------------------------------------------
1 | function isInvariant(name) {
2 | return /^invariant/.test(name) || /^[A-Z].*\.invariant/.test(name)
3 | }
4 | export default function () {
5 | return {
6 | name: "transform-remove-invariant",
7 | visitor: {
8 | ImportDeclaration({ node }) {
9 | node.specifiers = node.specifiers.filter(
10 | specifier => !isInvariant(specifier.local.name),
11 | )
12 | },
13 | FunctionDeclaration(path) {
14 | if (isInvariant(path.node.id.name)) {
15 | path.remove()
16 | path.stop()
17 | }
18 | },
19 | CallExpression(path) {
20 | const calleePath = path.get("callee")
21 | if (isInvariant(calleePath)) {
22 | path.remove()
23 | }
24 | },
25 | },
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/src/archetype.ts:
--------------------------------------------------------------------------------
1 | import * as Component from "./component"
2 | import * as Debug from "./debug"
3 | import * as Entity from "./entity"
4 | import * as Format from "./format"
5 | import * as Schema from "./schema"
6 | import * as Signal from "./signal"
7 | import * as Type from "./type"
8 | import * as Types from "./types"
9 | import * as World from "./world"
10 | import * as ComponentSet from "./component_set"
11 |
12 | export type BinaryData<$Shape extends Schema.Shape> =
13 | $Shape extends Format.Format ? number : { [K in keyof $Shape]: number }
14 |
15 | export type NativeData<$Shape extends Schema.Shape> =
16 | $Shape extends Format.Format
17 | ? number
18 | : {
19 | [K in keyof $Shape]: $Shape[K] extends Format.Format
20 | ? number
21 | : $Shape[K] extends Schema.Shape
22 | ? NativeData<$Shape[K]>
23 | : never
24 | }
25 |
26 | export type DataOfShape<$Shape extends Schema.Shape> =
27 | $Shape extends Schema.Shape
28 | ? BinaryData<$Shape>
29 | : $Shape extends Schema.Shape
30 | ? NativeData<$Shape>
31 | : never
32 |
33 | export type Data<$SchemaId extends Schema.Id> = $SchemaId extends Schema.Id
34 | ? $Schema extends Schema.ComplexSchema
35 | ? DataOfShape>
36 | : never
37 | : never
38 |
39 | type ScalarBinaryColumn<
40 | $Schema extends Schema.BinaryScalarSchema = Schema.BinaryScalarSchema,
41 | > = {
42 | kind: Schema.SchemaKind.BinaryScalar
43 | schema: $Schema
44 | data: Types.Construct["binary"]>
45 | }
46 |
47 | type ComplexBinaryColumn<
48 | $Schema extends Schema.BinaryStructSchema = Schema.BinaryStructSchema,
49 | > = {
50 | kind: Schema.SchemaKind.BinaryStruct
51 | schema: $Schema
52 | data: {
53 | [K in keyof Schema.Shape<$Schema>]: Types.Construct<
54 | Schema.Shape<$Schema>[K]["binary"]
55 | >
56 | }
57 | }
58 |
59 | type ScalarNativeColumn<
60 | $Schema extends Schema.NativeScalarSchema = Schema.NativeScalarSchema,
61 | > = {
62 | kind: Schema.SchemaKind.NativeScalar
63 | schema: $Schema
64 | data: number[]
65 | }
66 |
67 | type ComplexNativeColumn<
68 | $Schema extends Schema.NativeObjectSchema = Schema.NativeObjectSchema,
69 | > = {
70 | kind: Schema.SchemaKind.NativeObject
71 | schema: $Schema
72 | data: NativeData>[]
73 | }
74 |
75 | type TagColumn<$Schema extends Schema.TagSchema = Schema.TagSchema> = {
76 | kind: Schema.SchemaKind.Tag
77 | schema: $Schema
78 | data: null
79 | }
80 |
81 | export type ColumnOfSchema<$Schema extends Schema.AnySchema> =
82 | $Schema extends Schema.BinaryScalarSchema
83 | ? ScalarBinaryColumn<$Schema>
84 | : $Schema extends Schema.BinaryStructSchema
85 | ? ComplexBinaryColumn<$Schema>
86 | : $Schema extends Schema.NativeScalarSchema
87 | ? ScalarNativeColumn<$Schema>
88 | : $Schema extends Schema.NativeObjectSchema
89 | ? ComplexNativeColumn<$Schema>
90 | : $Schema extends Schema.TagSchema
91 | ? TagColumn
92 | : never
93 |
94 | export type Column<$SchemaId extends Schema.Id = Schema.Id> = $SchemaId extends Schema.Id<
95 | infer $Schema
96 | >
97 | ? ColumnOfSchema<$Schema>
98 | : never
99 |
100 | export type Store<$Type extends Type.Struct> = {
101 | [K in keyof $Type]: $Type[K] extends Schema.Id ? Column<$Type[K]> : never
102 | }
103 |
104 | export type Struct<$Type extends Type.Struct = Type.Struct> = {
105 | edgesSet: Struct[]
106 | edgesUnset: Struct[]
107 | entities: Entity.Id[]
108 | entityIndex: number[]
109 | layout: number[]
110 | real: boolean
111 | onTableInsert: Signal.Struct
112 | onRealize: Signal.Struct
113 | store: Store<$Type>
114 | type: $Type
115 | }
116 |
117 | export type RowData<$Type extends Type.Struct> = {
118 | [K in keyof $Type]: $Type[K] extends Schema.Id ? Data<$Type[K]> : never
119 | }
120 |
121 | const ArrayBufferConstructor = globalThis.SharedArrayBuffer ?? globalThis.ArrayBuffer
122 |
123 | function makeColumn(schema: Schema.AnySchema, size: number): Column {
124 | let data: Column["data"]
125 | switch (schema.kind) {
126 | case Schema.SchemaKind.BinaryScalar: {
127 | const buffer = new ArrayBufferConstructor(
128 | size * schema.shape.binary.BYTES_PER_ELEMENT,
129 | )
130 | data = new schema.shape.binary(buffer)
131 | break
132 | }
133 | case Schema.SchemaKind.BinaryStruct: {
134 | data = Object.entries(schema.shape).reduce((a, [memberName, memberNode]) => {
135 | const buffer = new ArrayBufferConstructor(
136 | size * memberNode.binary.BYTES_PER_ELEMENT,
137 | )
138 | a[memberName] = new memberNode.binary(buffer)
139 | return a
140 | }, {} as { [key: string]: Types.TypedArray })
141 | break
142 | }
143 | case Schema.SchemaKind.NativeScalar:
144 | case Schema.SchemaKind.NativeObject:
145 | data = []
146 | break
147 | case Schema.SchemaKind.Tag:
148 | data = null
149 | break
150 | }
151 | return { kind: schema.kind, schema, data } as Column
152 | }
153 |
154 | export function makeStore<$Type extends Type.Struct>(
155 | world: World.Struct,
156 | type: $Type,
157 | ): Store<$Type> {
158 | return type.map(id =>
159 | makeColumn(World.findSchemaById(world, id), world.size),
160 | ) as unknown as Store<$Type>
161 | }
162 |
163 | export function makeInner<$Type extends Type.Struct>(
164 | type: $Type,
165 | store: Store<$Type>,
166 | ): Struct<$Type> {
167 | Type.invariantNormalized(type)
168 | const layout: number[] = []
169 | for (let i = 0; i < type.length; i++) {
170 | const id = type[i]
171 | Debug.invariant(id !== undefined)
172 | layout[id] = i
173 | }
174 | return {
175 | edgesSet: [],
176 | edgesUnset: [],
177 | entities: [],
178 | entityIndex: [],
179 | layout,
180 | real: false,
181 | onTableInsert: Signal.make(),
182 | onRealize: Signal.make(),
183 | store,
184 | type,
185 | }
186 | }
187 |
188 | export function make<$Type extends Type.Struct>(
189 | world: World.Struct,
190 | type: $Type,
191 | ): Struct<$Type> {
192 | const store = makeStore(world, type)
193 | return makeInner(type, store)
194 | }
195 |
196 | export function ensureReal(archetype: Struct) {
197 | if (!archetype.real) {
198 | archetype.real = true
199 | Signal.dispatch(archetype.onRealize, undefined)
200 | }
201 | }
202 |
203 | export function insert(
204 | archetype: Struct,
205 | entity: Entity.Id,
206 | components: ComponentSet.Struct,
207 | ) {
208 | const index = archetype.entities.length
209 | for (let i = 0; i < archetype.type.length; i++) {
210 | const id = archetype.type[i]
211 | Debug.invariant(id !== undefined)
212 | const columnIndex = archetype.layout[id as number]
213 | Debug.invariant(columnIndex !== undefined)
214 | const column = archetype.store[columnIndex]
215 | Debug.invariant(column !== undefined)
216 | const kind = column.kind
217 | // insert components, skipping over tag schema
218 | if (kind !== Schema.SchemaKind.Tag) {
219 | const data = components[id] ?? Component.expressSchema(column.schema)
220 | Debug.invariant(data !== undefined)
221 | writeColumnData(column, archetype.entities.length, data)
222 | }
223 | }
224 | archetype.entities[index] = entity
225 | archetype.entityIndex[entity] = index
226 | ensureReal(archetype)
227 | }
228 |
229 | export function remove(archetype: Struct, entity: number) {
230 | const index = archetype.entityIndex[entity]
231 | const head = archetype.entities.pop()
232 |
233 | Debug.invariant(index !== undefined)
234 | Debug.invariant(head !== undefined)
235 |
236 | if (entity === head) {
237 | // pop
238 | for (let i = 0; i < archetype.store.length; i++) {
239 | const column = archetype.store[i]
240 | Debug.invariant(column !== undefined)
241 | switch (column.kind) {
242 | case Schema.SchemaKind.BinaryScalar:
243 | column.data[index] = 0
244 | break
245 | case Schema.SchemaKind.BinaryStruct:
246 | for (const key in column.schema.shape) {
247 | const array = column.data[key]
248 | Debug.invariant(array !== undefined)
249 | array[index] = 0
250 | }
251 | break
252 | case Schema.SchemaKind.NativeScalar:
253 | case Schema.SchemaKind.NativeObject:
254 | column.data.pop()
255 | break
256 | }
257 | }
258 | } else {
259 | // swap
260 | const from = archetype.entities.length - 1
261 | for (let i = 0; i < archetype.store.length; i++) {
262 | const column = archetype.store[i]
263 | Debug.invariant(column !== undefined)
264 | switch (column.kind) {
265 | case Schema.SchemaKind.BinaryScalar:
266 | const data = column.data[from]
267 | Debug.invariant(data !== undefined)
268 | column.data[from] = 0
269 | column.data[index] = data
270 | break
271 | case Schema.SchemaKind.BinaryStruct:
272 | for (const key in column.schema.shape) {
273 | const array = column.data[key]
274 | Debug.invariant(array !== undefined)
275 | const data = array[from]
276 | Debug.invariant(data !== undefined)
277 | array[from] = 0
278 | array[index] = data
279 | }
280 | break
281 | case Schema.SchemaKind.NativeScalar:
282 | case Schema.SchemaKind.NativeObject: {
283 | const data = column.data.pop()
284 | Debug.invariant(data !== undefined)
285 | column.data[index] = data
286 | break
287 | }
288 | }
289 | }
290 | archetype.entities[index] = head
291 | archetype.entityIndex[head] = index
292 | }
293 |
294 | archetype.entityIndex[entity] = -1
295 | }
296 |
297 | export function writeColumnData(column: Column, index: number, data: Data) {
298 | switch (column.kind) {
299 | case Schema.SchemaKind.BinaryStruct:
300 | Debug.invariant(typeof data === "object")
301 | for (const key in column.schema.shape) {
302 | const array = column.data[key]
303 | const value = data[key]
304 | Debug.invariant(array !== undefined)
305 | Debug.invariant(typeof value === "number")
306 | array[index] = value
307 | }
308 | break
309 | case Schema.SchemaKind.BinaryScalar:
310 | case Schema.SchemaKind.NativeScalar:
311 | case Schema.SchemaKind.NativeObject:
312 | column.data[index] = data
313 | break
314 | }
315 | }
316 |
317 | export function copyColumnData(
318 | prev: Column,
319 | next: Column,
320 | prevIndex: number,
321 | nextIndex: number,
322 | ) {
323 | switch (prev.kind) {
324 | case Schema.SchemaKind.BinaryStruct:
325 | Debug.invariant(prev.kind === next.kind)
326 | for (const key in prev.schema.shape) {
327 | const prevArray = prev.data[key]
328 | const nextArray = next.data[key]
329 | Debug.invariant(prevArray !== undefined)
330 | Debug.invariant(nextArray !== undefined)
331 | const value = prevArray[prevIndex]
332 | Debug.invariant(typeof value === "number")
333 | nextArray[nextIndex] = value
334 | }
335 | break
336 | case Schema.SchemaKind.BinaryScalar:
337 | case Schema.SchemaKind.NativeScalar:
338 | case Schema.SchemaKind.NativeObject: {
339 | Debug.invariant(prev.kind === next.kind)
340 | const value = prev.data[prevIndex]
341 | Debug.invariant(value !== undefined)
342 | next.data[nextIndex] = value
343 | break
344 | }
345 | }
346 | }
347 |
348 | export function move(
349 | entity: Entity.Id,
350 | prev: Struct,
351 | next: Struct,
352 | data?: ComponentSet.Struct,
353 | ) {
354 | const nextIndex = next.entities.length
355 | for (let i = 0; i < next.type.length; i++) {
356 | const id = next.type[i]
357 | Debug.invariant(id !== undefined)
358 | const columnIndex = prev.layout[id]
359 | const nextColumn = next.store[i]
360 | Debug.invariant(nextColumn !== undefined)
361 | if (nextColumn.kind === Schema.SchemaKind.Tag) {
362 | continue
363 | }
364 | if (columnIndex === undefined) {
365 | Debug.invariant(data !== undefined)
366 | const value = data[id] ?? Component.expressSchema(nextColumn.schema)
367 | Debug.invariant(value !== undefined)
368 | writeColumnData(nextColumn, nextIndex, value)
369 | } else {
370 | Debug.invariant(columnIndex !== undefined)
371 | const prevIndex = prev.entityIndex[entity]
372 | const prevColumn = prev.store[columnIndex]
373 | const value = data?.[id]
374 | Debug.invariant(prevIndex !== undefined)
375 | Debug.invariant(prevColumn !== undefined)
376 | if (value === undefined) {
377 | copyColumnData(prevColumn, nextColumn, prevIndex, nextIndex)
378 | } else {
379 | writeColumnData(nextColumn, nextIndex, value)
380 | }
381 | }
382 | }
383 | next.entities[nextIndex] = entity
384 | next.entityIndex[entity] = nextIndex
385 | ensureReal(next)
386 | remove(prev, entity)
387 | }
388 |
389 | export function read<$Type extends Type.Struct>(
390 | entity: Entity.Id,
391 | archetype: Struct,
392 | layout: $Type,
393 | out: unknown[],
394 | ) {
395 | const index = archetype.entityIndex[entity]
396 | Debug.invariant(index !== undefined)
397 | for (let i = 0; i < layout.length; i++) {
398 | const schemaId = layout[i]
399 | Debug.invariant(schemaId !== undefined)
400 | const columnIndex = archetype.layout[schemaId]
401 | Debug.invariant(columnIndex !== undefined)
402 | const column = archetype.store[columnIndex]
403 | Debug.invariant(column !== undefined)
404 | let value: unknown
405 | switch (column.kind) {
406 | case Schema.SchemaKind.BinaryStruct:
407 | const data = Component.expressBinaryShape(column.schema.shape)
408 | for (const prop in data) {
409 | data[prop] = column.data[prop]![index]!
410 | }
411 | value = data
412 | break
413 | case Schema.SchemaKind.Tag:
414 | value = undefined
415 | break
416 | default:
417 | value = column.data[index]
418 | break
419 | }
420 | out.push(value)
421 | }
422 | return out as unknown as RowData<$Type>
423 | }
424 |
425 | export function write(
426 | entity: Entity.Id,
427 | archetype: Struct,
428 | components: ComponentSet.Struct,
429 | ) {
430 | const index = archetype.entityIndex[entity]
431 | Debug.invariant(index !== undefined)
432 | for (let i = 0; i < archetype.store.length; i++) {
433 | const column = archetype.store[i]
434 | Debug.invariant(column !== undefined)
435 | const data = components[column.schema.id]
436 | if (data !== undefined) {
437 | writeColumnData(column, index, data)
438 | }
439 | }
440 | }
441 |
--------------------------------------------------------------------------------
/lib/src/archetype_graph.ts:
--------------------------------------------------------------------------------
1 | import * as Archetype from "./archetype"
2 | import * as Debug from "./debug"
3 | import * as Schema from "./schema"
4 | import * as Signal from "./signal"
5 | import * as Type from "./type"
6 | import * as World from "./world"
7 |
8 | export function traverseLeft(
9 | archetype: Archetype.Struct,
10 | iteratee: (archetype: Archetype.Struct) => unknown,
11 | visited = new Set(),
12 | ) {
13 | const stack: (Archetype.Struct | number)[] = [0, archetype]
14 | let i = stack.length
15 | while (i > 0) {
16 | const node = stack[--i] as Archetype.Struct
17 | const index = stack[--i] as number
18 | if (index < node.edgesUnset.length - 1) {
19 | stack[i++] = index + 1
20 | stack[i++] = node
21 | }
22 | const next = node.edgesUnset[index]
23 | if (next && !visited.has(next)) {
24 | visited.add(next)
25 | iteratee(next)
26 | stack[i++] = 0
27 | stack[i++] = next
28 | }
29 | }
30 | }
31 |
32 | export function traverse(
33 | archetype: Archetype.Struct,
34 | iteratee: (archetype: Archetype.Struct) => unknown,
35 | visited = new Set(),
36 | ) {
37 | const stack: (Archetype.Struct | number)[] = [0, archetype]
38 | let i = stack.length
39 | while (i > 0) {
40 | const node = stack[--i] as Archetype.Struct
41 | const index = stack[--i] as number
42 | if (index < node.edgesSet.length - 1) {
43 | stack[i++] = index + 1
44 | stack[i++] = node
45 | }
46 | const next = node.edgesSet[index]
47 | if (next && !visited.has(next)) {
48 | visited.add(next)
49 | iteratee(next)
50 | stack[i++] = 0
51 | stack[i++] = next
52 | }
53 | }
54 | }
55 |
56 | function emitTable(archetype: Archetype.Struct) {
57 | traverseLeft(archetype, t => Signal.dispatch(t.onTableInsert, archetype))
58 | }
59 |
60 | export function find(world: World.Struct, type: Type.Struct) {
61 | let left = world.rootArchetype
62 | for (let i = 0; i < type.length; i++) {
63 | const id = type[i]
64 | Debug.invariant(id !== undefined)
65 | const right = left.edgesSet[id]
66 | if (right === undefined) {
67 | return
68 | }
69 | left = right
70 | }
71 | return left
72 | }
73 |
74 | function makeEdge(left: Archetype.Struct, right: Archetype.Struct, id: Schema.Id) {
75 | left.edgesSet[id] = right
76 | right.edgesUnset[id] = left
77 | }
78 |
79 | function makeArchetypeEnsurePath(
80 | world: World.Struct,
81 | root: Archetype.Struct,
82 | type: Type.Struct,
83 | emit: Archetype.Struct[],
84 | ) {
85 | let left = root
86 | for (let i = 0; i < type.length; i++) {
87 | const id = type[i]!
88 | let right = left.edgesSet[id]
89 | if (right === undefined) {
90 | const type = Type.add(left.type, id)
91 | right = find(world, type)
92 | if (right === undefined) {
93 | right = Archetype.make(world, type)
94 | emit.push(right)
95 | }
96 | makeEdge(left, right, id)
97 | }
98 | left = right
99 | }
100 | return left
101 | }
102 |
103 | function ensurePath(
104 | world: World.Struct,
105 | right: Archetype.Struct,
106 | left: Archetype.Struct,
107 | emit: Archetype.Struct[],
108 | ) {
109 | if (hasPath(left, right)) return
110 | const ids = Type.getIdsBetween(right.type, left.type)
111 | let node = left
112 | for (let i = 0; i < ids.length; i++) {
113 | const id = ids[i]
114 | Debug.invariant(id !== undefined)
115 | const type = Type.add(node.type, id)
116 | let right = find(world, type)
117 | if (right === undefined) {
118 | right = makeArchetypeEnsurePath(world, world.rootArchetype, type, emit)
119 | }
120 | makeEdge(node, right, id)
121 | node = right
122 | }
123 | return node
124 | }
125 |
126 | function hasPathTraverse(left: Archetype.Struct, ids: number[]) {
127 | for (let i = 0; i < ids.length; i++) {
128 | const id = ids[i]
129 | Debug.invariant(id !== undefined)
130 | const next = left.edgesSet[id]
131 | if (next !== undefined) {
132 | if (ids.length === 1) {
133 | return true
134 | }
135 | const nextIds = ids.slice()
136 | const swapId = nextIds.pop()
137 | if (i !== nextIds.length) {
138 | Debug.invariant(swapId !== undefined)
139 | nextIds[i] = swapId
140 | }
141 | if (hasPathTraverse(next, nextIds)) {
142 | return true
143 | }
144 | }
145 | }
146 | return false
147 | }
148 |
149 | function hasPath(left: Archetype.Struct, right: Archetype.Struct) {
150 | const ids = Type.getIdsBetween(right.type, left.type)
151 | return hasPathTraverse(left, ids)
152 | }
153 |
154 | function connectArchetypeTraverse(
155 | world: World.Struct,
156 | visiting: Archetype.Struct,
157 | inserted: Archetype.Struct,
158 | emit: Archetype.Struct[],
159 | visited = new Set(emit),
160 | ) {
161 | visited.add(visiting)
162 | if (Type.isSupersetOf(visiting.type, inserted.type)) {
163 | ensurePath(world, visiting, inserted, emit)
164 | return
165 | }
166 | if (
167 | Type.isSupersetOf(inserted.type, visiting.type) &&
168 | visiting !== world.rootArchetype
169 | ) {
170 | ensurePath(world, inserted, visiting, emit)
171 | }
172 | visiting.edgesSet.forEach(function connectNextArchetype(next) {
173 | if (
174 | !visited.has(next) &&
175 | (Type.maybeSupersetOf(next.type, inserted.type) ||
176 | Type.maybeSupersetOf(inserted.type, next.type))
177 | ) {
178 | connectArchetypeTraverse(world, next, inserted, emit, visited)
179 | }
180 | })
181 | }
182 |
183 | function connectArchetype(
184 | world: World.Struct,
185 | inserted: Archetype.Struct,
186 | emit: Archetype.Struct[],
187 | ) {
188 | world.rootArchetype.edgesSet.forEach(function connectArchetypeFromBase(node) {
189 | connectArchetypeTraverse(world, node, inserted, emit)
190 | })
191 | }
192 |
193 | function insertArchetype(
194 | world: World.Struct,
195 | root: Archetype.Struct,
196 | type: Type.Struct,
197 | emit: Archetype.Struct[],
198 | ) {
199 | let left = root
200 | for (let i = 0; i < type.length; i++) {
201 | const id = type[i]!
202 | let right = left.edgesSet[id]
203 | if (right === undefined) {
204 | const type = Type.add(left.type, id)
205 | right = find(world, type)
206 | let connect = false
207 | if (right === undefined) {
208 | right = Archetype.make(world, type)
209 | emit.push(right)
210 | connect = true
211 | }
212 | makeEdge(left, right, id)
213 | if (connect) {
214 | connectArchetype(world, right, emit)
215 | }
216 | }
217 | left = right
218 | }
219 | return left
220 | }
221 |
222 | export function findOrMakeArchetype(world: World.Struct, type: Type.Struct) {
223 | let emit: Archetype.Struct[] = []
224 | const archetype = insertArchetype(world, world.rootArchetype, type, emit)
225 | for (let i = 0; i < emit.length; i++) {
226 | emitTable(emit[i]!)
227 | }
228 | return archetype
229 | }
230 |
--------------------------------------------------------------------------------
/lib/src/cache.spec.ts:
--------------------------------------------------------------------------------
1 | import * as World from "./world"
2 | import * as Cache from "./cache"
3 | import * as Schema from "./schema"
4 | import * as Query from "./query"
5 | import * as Entity from "./entity"
6 |
7 | describe("Cache", () => {
8 | describe("set", () => {
9 | it("stores and applies set operations", () => {
10 | const world = World.make(10)
11 | const cache = Cache.make()
12 | const A = Schema.make(world, {})
13 | const a = Query.make(world, [A])
14 | Cache.set(cache, Entity.make(world), [A], [{}])
15 | Cache.set(cache, Entity.make(world), [A], [{}])
16 | for (let i = 0; i < a.length; i++) {
17 | const [e] = a[i]!
18 | expect(e!.length).toBe(0)
19 | }
20 | Cache.apply(cache, world)
21 | for (let i = 0; i < a.length; i++) {
22 | const [e] = a[i]!
23 | expect(e!.length).toBe(2)
24 | }
25 | })
26 | })
27 | describe("unset", () => {
28 | it("stores and applies unset operations", () => {
29 | const world = World.make(10)
30 | const cache = Cache.make()
31 | const A = Schema.make(world, {})
32 | const a = Query.make(world, [A])
33 | const e1 = Entity.make(world, [A])
34 | const e2 = Entity.make(world, [A])
35 | for (let i = 0; i < a.length; i++) {
36 | const [e] = a[i]!
37 | expect(e!.length).toBe(2)
38 | }
39 | Cache.unset(cache, e1, [A])
40 | Cache.unset(cache, e2, [A])
41 | Cache.apply(cache, world)
42 | for (let i = 0; i < a.length; i++) {
43 | const [e] = a[i]!
44 | expect(e!.length).toBe(0)
45 | }
46 | })
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/lib/src/cache.ts:
--------------------------------------------------------------------------------
1 | import * as Archetype from "./archetype"
2 | import * as Debug from "./debug"
3 | import * as Entity from "./entity"
4 | import * as Schema from "./schema"
5 | import * as SparseMap from "./sparse_map"
6 | import * as Type from "./type"
7 | import * as World from "./world"
8 | import * as Symbols from "./symbols"
9 |
10 | // An EntityDelta describes a change made to an entity.
11 | export type EntityDelta =
12 | // Remove (destroy) operations are expressed with a tombstone symbol:
13 | | typeof Symbols.$tombstone
14 | // Component changes (add/remove) are expressed as a sparse map, where the
15 | // keys are schema ids, and the values are component data or tombstones, in
16 | // the case a component was removed.
17 | | SparseMap.Struct
18 |
19 | export type Struct = SparseMap.Struct
20 |
21 | function ensureEntityDelta(cache: Struct, entity: Entity.Id) {
22 | let delta = SparseMap.get(cache, entity)
23 | if (delta === undefined) {
24 | delta = SparseMap.make()
25 | SparseMap.set(cache, entity, delta)
26 | }
27 | return delta
28 | }
29 |
30 | export function set<$Type extends Type.Struct>(
31 | cache: Struct,
32 | entity: Entity.Id,
33 | type: Type.Struct,
34 | data: Archetype.RowData<$Type>,
35 | ) {
36 | const delta = ensureEntityDelta(cache, entity)
37 | if (delta === Symbols.$tombstone) return
38 | for (let i = 0; i < type.length; i++) {
39 | const id = type[i]
40 | Debug.invariant(id !== undefined)
41 | SparseMap.set(delta, id, data[i])
42 | }
43 | }
44 |
45 | export function unset(cache: Struct, entity: Entity.Id, type: Type.Struct) {
46 | const delta = ensureEntityDelta(cache, entity)
47 | if (delta === Symbols.$tombstone) return
48 | for (let i = 0; i < type.length; i++) {
49 | const id = type[i]
50 | Debug.invariant(id !== undefined)
51 | SparseMap.set(delta, id, Symbols.$tombstone)
52 | }
53 | }
54 |
55 | export function destroy(cache: Struct, entity: Entity.Id) {
56 | SparseMap.set(cache, entity, Symbols.$tombstone)
57 | }
58 |
59 | export function make(): Struct {
60 | return SparseMap.make()
61 | }
62 |
63 | export function apply(cache: Struct, world: World.Struct) {
64 | SparseMap.forEach(cache, function applyEntityDelta(delta, entity) {
65 | if (delta === Symbols.$tombstone) {
66 | Entity.destroy(world, entity)
67 | } else {
68 | SparseMap.forEach(delta, function applyComponentDelta(data, id) {
69 | if (data === Symbols.$tombstone) {
70 | Entity.unset(world, entity, [id])
71 | } else {
72 | Entity.set(world, entity, [id], [data as any])
73 | }
74 | })
75 | }
76 | })
77 | }
78 |
79 | export function clear(cache: Struct) {
80 | SparseMap.clear(cache)
81 | return cache
82 | }
83 |
--------------------------------------------------------------------------------
/lib/src/component.ts:
--------------------------------------------------------------------------------
1 | import * as Archetype from "./archetype"
2 | import * as Schema from "./schema"
3 | import * as Type from "./type"
4 | import * as World from "./world"
5 |
6 | export function expressBinaryShape<$Shape extends Schema.Shape>(
7 | shape: $Shape,
8 | ): Archetype.BinaryData<$Shape> {
9 | if (Schema.isFormat(shape)) {
10 | return 0 as Archetype.BinaryData<$Shape>
11 | }
12 | const object: { [key: string]: unknown } = {}
13 | for (const key in shape) {
14 | object[key] = 0
15 | }
16 | return object as Archetype.BinaryData<$Shape>
17 | }
18 |
19 | export function expressNativeShape<$Shape extends Schema.Shape>(
20 | shape: $Shape,
21 | ): Archetype.NativeData<$Shape> {
22 | if (Schema.isFormat(shape)) {
23 | return 0 as Archetype.NativeData<$Shape>
24 | }
25 | const object: { [key: string]: unknown } = {}
26 | for (const key in shape) {
27 | object[key] = 0
28 | }
29 | return object as Archetype.NativeData<$Shape>
30 | }
31 |
32 | export function expressSchema<$Schema extends Schema.AnySchema>(schema: $Schema) {
33 | switch (schema.kind) {
34 | case Schema.SchemaKind.BinaryScalar:
35 | case Schema.SchemaKind.BinaryStruct:
36 | return expressBinaryShape(schema.shape)
37 | case Schema.SchemaKind.NativeScalar:
38 | case Schema.SchemaKind.NativeObject:
39 | return expressNativeShape(schema.shape)
40 | default:
41 | return undefined
42 | }
43 | }
44 |
45 | export function expressType<$Type extends Type.Struct>(
46 | world: World.Struct,
47 | type: $Type,
48 | ): Archetype.RowData<$Type> {
49 | return type.map(id =>
50 | expressSchema(World.findSchemaById(world, id)),
51 | ) as unknown as Archetype.RowData<$Type>
52 | }
53 |
--------------------------------------------------------------------------------
/lib/src/component_set.ts:
--------------------------------------------------------------------------------
1 | import * as Archetype from "./archetype"
2 | import * as Debug from "./debug"
3 | import * as Schema from "./schema"
4 | import * as Type from "./type"
5 | import * as World from "./world"
6 |
7 | export type Struct = (Archetype.Data | undefined)[]
8 | export type Init<$Type extends Type.Struct = Type.Struct> = {
9 | [K in keyof $Type]?: Archetype.RowData<$Type>[K]
10 | }
11 |
12 | export function make(world: World.Struct, type: Type.Struct, init: Init): Struct {
13 | const values: Struct = []
14 | for (let i = 0; i < type.length; i++) {
15 | const id = type[i]
16 | Debug.invariant(id !== undefined)
17 | values[id] = init[i]
18 | }
19 | return values
20 | }
21 |
--------------------------------------------------------------------------------
/lib/src/debug.ts:
--------------------------------------------------------------------------------
1 | export class AssertionError extends Error {
2 | readonly inner?: Error
3 | constructor(message: string, inner?: Error) {
4 | super(message)
5 | this.inner = inner
6 | }
7 | }
8 |
9 | export function unwrap(error: unknown) {
10 | let final = error
11 | if (error instanceof AssertionError && error.inner !== undefined) {
12 | final = error.inner
13 | }
14 | return final
15 | }
16 |
17 | export function assert(
18 | predicate: boolean,
19 | message: string = "",
20 | inner?: Error,
21 | ): asserts predicate {
22 | if (predicate === false) {
23 | throw new AssertionError(message, inner)
24 | }
25 | }
26 |
27 | export function invariant(
28 | predicate: boolean,
29 | message: string = "",
30 | inner?: Error,
31 | ): asserts predicate {
32 | if (predicate === false) {
33 | throw new AssertionError(message, inner)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/lib/src/encode.ts:
--------------------------------------------------------------------------------
1 | import * as Format from "./format"
2 | import * as Query from "./query"
3 | import * as Symbols from "./symbols"
4 | import * as Schema from "./schema"
5 | import * as Type from "./type"
6 | import * as World from "./world"
7 | import * as Debug from "./debug"
8 | import * as Entity from "./entity"
9 | import * as Archetype from "./archetype"
10 |
11 | type Part = {
12 | data: number[]
13 | view: Format.Format[]
14 | byteLength: number
15 | }
16 |
17 | function push(part: Part, data: number, format: Format.Format) {
18 | const dataLength = part.data.push(data)
19 | const viewLength = part.view.push(format)
20 | Debug.invariant(dataLength === viewLength)
21 | part.byteLength += format.binary.BYTES_PER_ELEMENT
22 | return dataLength - 1
23 | }
24 |
25 | function write(
26 | part: Part,
27 | handle: number,
28 | data: number,
29 | format: Format.Format = part.view[handle]!,
30 | ) {
31 | part.byteLength +=
32 | format.binary.BYTES_PER_ELEMENT - part.view[handle]!.binary.BYTES_PER_ELEMENT
33 | part.data[handle] = data
34 | part.view[handle] = format
35 | }
36 |
37 | function reset(part: Part) {
38 | while (part.data.pop()) {}
39 | while (part.view.pop()) {}
40 | part.byteLength = 0
41 | }
42 |
43 | function encodeRow<$Type extends Type.Struct>(
44 | part: Part,
45 | index: number,
46 | schemas: Schema.AnySchema[],
47 | recordData: Query.RecordData<$Type>,
48 | ) {
49 | for (let j = 0; j < schemas.length; j++) {
50 | const schema = schemas[j]
51 | Debug.invariant(schema !== undefined)
52 | const data = recordData[j]
53 | Debug.invariant(data !== undefined)
54 | switch (schema.kind) {
55 | case Schema.SchemaKind.BinaryScalar:
56 | case Schema.SchemaKind.NativeScalar:
57 | const value = (data as Archetype.ColumnOfSchema["data"])[index]
58 | Debug.invariant(typeof value === "number")
59 | push(part, value, schema.shape)
60 | break
61 | case Schema.SchemaKind.BinaryStruct: {
62 | for (const prop in schema.shape) {
63 | const format = schema.shape[prop]
64 | Debug.invariant(format !== undefined)
65 | const field = (data as Archetype.ColumnOfSchema["data"])[prop]
66 | Debug.invariant(field !== undefined)
67 | const value = field[index]
68 | Debug.invariant(typeof value === "number")
69 | push(part, value, format)
70 | }
71 | break
72 | }
73 | case Schema.SchemaKind.NativeObject: {
74 | const component = (data as Archetype.ColumnOfSchema["data"])[index]
75 | Debug.invariant(component !== undefined)
76 | for (const prop in schema.shape) {
77 | const value = component[prop]
78 | Debug.invariant(typeof value === "number")
79 | const format = schema.shape[prop]
80 | Debug.invariant(format !== undefined)
81 | push(part, value, format)
82 | }
83 | break
84 | }
85 | }
86 | }
87 | }
88 |
89 | export function encodeQuery<$Type extends Type.Struct>(
90 | world: World.Struct,
91 | query: Query.Struct<$Type>,
92 | part: Part,
93 | filter?: (entity: Entity.Id) => boolean,
94 | ) {
95 | const type = query[Symbols.$type]
96 | const schemas = type.map(schemaId => World.findSchemaById(world, schemaId))
97 | for (let i = 0; i < type.length; i++) {
98 | const schemaId = type[i]
99 | Debug.invariant(schemaId !== undefined)
100 | push(part, schemaId, Format.uint32)
101 | }
102 | let size = 0
103 | let sizeHandle = push(part, size, Format.uint32)
104 | if (filter === undefined) {
105 | for (const [entities, recordData] of query) {
106 | size += entities.length
107 | for (let i = 0; i < entities.length; i++) {
108 | const entity = entities[i]
109 | Debug.invariant(entity !== undefined)
110 | push(part, entity, Format.uint32)
111 | encodeRow(part, i, schemas, recordData)
112 | }
113 | }
114 | } else {
115 | for (const [entities, recordData] of query) {
116 | for (let i = 0; i < entities.length; i++) {
117 | const entity = entities[i]
118 | Debug.invariant(entity !== undefined)
119 | if (filter(entity) === false) continue
120 | size++
121 | push(part, entity, Format.uint32)
122 | encodeRow(part, i, schemas, recordData)
123 | }
124 | }
125 | }
126 | write(part, sizeHandle, size)
127 | }
128 |
129 | export function serialize(
130 | part: Part,
131 | view: DataView = new DataView(new ArrayBuffer(part.byteLength)),
132 | offset = 0,
133 | ) {
134 | Debug.assert(
135 | offset + part.byteLength > view.byteLength,
136 | "Failed to serialize part",
137 | new RangeError("Part will not fit in target ArrayBuffer"),
138 | )
139 | for (let i = 0; i < part.data.length; i++) {
140 | const value = part.data[i]
141 | const format = part.view[i]
142 | Debug.invariant(value !== undefined)
143 | Debug.invariant(format !== undefined)
144 | switch (format.kind) {
145 | case Format.Kind.Uint8:
146 | view.setUint8(offset, value)
147 | break
148 | case Format.Kind.Uint16:
149 | view.setUint16(offset, value)
150 | break
151 | case Format.Kind.Uint32:
152 | view.setUint32(offset, value)
153 | break
154 | case Format.Kind.Int8:
155 | view.setInt8(offset, value)
156 | break
157 | case Format.Kind.Int16:
158 | view.setInt16(offset, value)
159 | break
160 | case Format.Kind.Int32:
161 | view.setInt32(offset, value)
162 | break
163 | case Format.Kind.Float32:
164 | view.setFloat32(offset, value)
165 | break
166 | case Format.Kind.Float64:
167 | view.setFloat64(offset, value)
168 | break
169 | }
170 | offset += format.binary.BYTES_PER_ELEMENT
171 | }
172 | return view.buffer
173 | }
174 |
175 | export function deserializeQuery(world: World.Struct, view: DataView, offset = 0) {}
176 |
--------------------------------------------------------------------------------
/lib/src/entity.spec.ts:
--------------------------------------------------------------------------------
1 | import * as World from "./world"
2 | import * as Entity from "./entity"
3 | import * as Schema from "./schema"
4 | import * as Format from "./format"
5 |
6 | describe("Entity", () => {
7 | describe("make", () => {
8 | it("makes an entity with configured components", () => {
9 | const world = World.make(1)
10 | const Type = [Schema.make(world, Format.uint8)]
11 | const entity = Entity.make(world, Type)
12 | expect(Entity.has(world, entity, Type)).toBe(true)
13 | })
14 | })
15 |
16 | describe("get", () => {
17 | it("returns a slice of entity component data", () => {
18 | const world = World.make(1)
19 | const Type = [
20 | Schema.make(world, { squad: Format.uint8 }),
21 | Schema.makeTag(world),
22 | Schema.makeBinary(world, Format.uint8),
23 | Schema.makeBinary(world, { x: Format.float64, y: Format.float64 }),
24 | ]
25 | const entity = Entity.make(world, Type)
26 | const data = [{ squad: 0 }, undefined, 0, { x: 0, y: 0 }]
27 | expect(Entity.get(world, entity, Type)).toEqual(data)
28 | })
29 | })
30 |
31 | describe("set", () => {
32 | it("updates entity component data", () => {
33 | const world = World.make(1)
34 | const Type = [
35 | Schema.make(world, { squad: Format.uint8 }),
36 | Schema.makeTag(world),
37 | Schema.makeBinary(world, Format.uint8),
38 | Schema.makeBinary(world, { x: Format.float64, y: Format.float64 }),
39 | ]
40 | const entity = Entity.make(world, Type)
41 | const data = [{ squad: 1 }, , 9, { x: 10, y: 11 }]
42 | Entity.set(world, entity, Type, data)
43 | expect(Entity.get(world, entity, Type)).toEqual(data)
44 | })
45 | it("adds new components", () => {
46 | const world = World.make(1)
47 | const TypePrev = [Schema.make(world, Format.uint8)]
48 | const TypeNext = [...TypePrev, Schema.make(world, Format.uint8)]
49 | const entity = Entity.make(world, TypePrev)
50 | expect(Entity.has(world, entity, TypePrev)).toBe(true)
51 | expect(Entity.has(world, entity, TypeNext)).toBe(false)
52 | Entity.set(world, entity, TypeNext)
53 | expect(Entity.has(world, entity, TypePrev)).toBe(true)
54 | expect(Entity.has(world, entity, TypeNext)).toBe(true)
55 | })
56 | it("throws an error if the entity does not exist", () => {
57 | const world = World.make(1)
58 | const Type = [Schema.make(world, Format.uint8)]
59 | expect(() => Entity.set(world, 99, Type)).toThrow()
60 | })
61 | })
62 |
63 | describe("unset", () => {
64 | it("removes entity components", () => {
65 | const world = World.make(1)
66 | const TypePrev = [
67 | Schema.make(world, Format.uint8),
68 | Schema.make(world, Format.uint8),
69 | ]
70 | const TypeRemove = TypePrev.slice(0, 1)
71 | const TypeFinal = TypePrev.slice(1)
72 | const entity = Entity.make(world, TypePrev)
73 | Entity.unset(world, entity, TypeRemove)
74 | expect(Entity.has(world, entity, TypePrev)).toBe(false)
75 | expect(Entity.has(world, entity, TypeFinal)).toBe(true)
76 | })
77 | it("throws an error if the entity does not exist", () => {
78 | const world = World.make(1)
79 | const Type = [Schema.make(world, Format.uint8)]
80 | expect(() => Entity.set(world, 99, Type)).toThrow()
81 | })
82 | })
83 |
84 | describe("destroy", () => {
85 | it("removes all entity components", () => {
86 | const world = World.make(1)
87 | const Type = [Schema.make(world, Format.uint8), Schema.make(world, Format.float64)]
88 | const entity = Entity.make(world, Type)
89 | Entity.destroy(world, entity)
90 | expect(Entity.tryHas(world, entity, Type)).toBe(false)
91 | })
92 | it("throws an error if the entity does not exist", () => {
93 | const world = World.make(1)
94 | expect(() => Entity.destroy(world, 99)).toThrow()
95 | })
96 | })
97 | })
98 |
--------------------------------------------------------------------------------
/lib/src/entity.ts:
--------------------------------------------------------------------------------
1 | import * as Archetype from "./archetype"
2 | import * as Graph from "./archetype_graph"
3 | import * as ComponentSet from "./component_set"
4 | import * as Debug from "./debug"
5 | import * as Type from "./type"
6 | import * as World from "./world"
7 |
8 | /**
9 | * An unsigned integer between 0 and `Number.MAX_SAFE_INTEGER` that uniquely
10 | * identifies an entity or schema within the ECS.
11 | */
12 | export type Id = number
13 |
14 | /**
15 | * Reserve an entity id without inserting it into the world.
16 | *
17 | * @example Add a component to a reserved entity
18 | * ```ts
19 | * const entity = Entity.reserve(world)
20 | * Entity.has(world, entity, [Position]) // Error: Failed ... entity is not real
21 | * Entity.set(world, entity, [Position])
22 | * Entity.has(world, entity, [Position]) // true
23 | * ```
24 | */
25 | export function reserve(world: World.Struct, entity = world.entityHead) {
26 | while (
27 | !(world.entityIndex[entity] === undefined && world.schemaIndex[entity] === undefined)
28 | ) {
29 | entity = world.entityHead++
30 | }
31 | return entity
32 | }
33 |
34 | /**
35 | * Create an entity using an array of schema ids as a template. Optionally
36 | * accepts an array of data used to initialize components. Undefined values
37 | * within the initializer array are filled with defaults (e.g. `0` for numeric
38 | * Format).
39 | *
40 | * @example Make an entity with no components
41 | * ```ts
42 | * Entity.make(world, [])
43 | * ```
44 | * @example Make an entity with one or more components
45 | * ```ts
46 | * Entity.make(world, [Health, Stamina])
47 | * ```
48 | * @example Initialize component values
49 | * ```ts
50 | * Entity.make(world, [Health, Stamina], [120, 100])
51 | * ```
52 | * @example Initialize a single component value
53 | * ```ts
54 | * Entity.make(world, [Position, Velocity], [, { x: -10, y: 42 }])
55 | * ```
56 | */
57 | export function make<$Type extends Type.Struct>(
58 | world: World.Struct,
59 | layout = [] as unknown as $Type,
60 | init = [] as unknown as ComponentSet.Init<$Type>,
61 | ) {
62 | const entity = reserve(world)
63 | const components = ComponentSet.make(world, layout, init)
64 | const archetype = Graph.findOrMakeArchetype(world, Type.normalize(layout))
65 | Archetype.insert(archetype, entity, components)
66 | World.setEntityArchetype(world, entity, archetype)
67 | return entity
68 | }
69 |
70 | /**
71 | * Get the value of one or more components for an entity. Throws an error if
72 | * the entity is not real, or if it does not have a component of each of
73 | * the provided schema ids.
74 | *
75 | * @example Get the value of a single component
76 | * ```ts
77 | * const [position] = Entity.get(world, entity, [Position])
78 | * ```
79 | * @example Get the value of multiple components
80 | * ```ts
81 | * const [health, inventory] = Entity.get(world, entity, [Health, Inventory])
82 | * ```
83 | * @example Re-use an array to avoid allocating intermediate array
84 | * ```ts
85 | * const results = []
86 | * const [health, stats] = Entity.get(world, entity, Player, results)
87 | * ```
88 | */
89 | export function get<$Type extends Type.Struct>(
90 | world: World.Struct,
91 | entity: Id,
92 | layout: $Type,
93 | out: unknown[] = [],
94 | ) {
95 | const archetype = World.getEntityArchetype(world, entity)
96 | Debug.invariant(has(world, entity, layout))
97 | return Archetype.read(entity, archetype, layout, out)
98 | }
99 |
100 | /**
101 | * Update the value of one or more components for an entity. If the entity does
102 | * not yet have components of the provided schema ids, add them. Throws an
103 | * error if the entity is not real.
104 | *
105 | * This function has the same interface as `Entity.make`, that is, it
106 | * optionally accepts a sparse array of initial component values.
107 | *
108 | * @example Add or update a single component
109 | * ```ts
110 | * Entity.set(world, entity, [Position])
111 | * ```
112 | * @example Initialize component values
113 | * ```ts
114 | * Entity.set(world, entity, [Health, Stamina], [100, 120])
115 | * ```
116 | * @example Update an existing component and add a new component
117 | * ```ts
118 | * const entity = Entity.make(world, [Health], [100])
119 | * Entity.set(world, entity, [Health, Stamina], [99])
120 | * ```
121 | */
122 | export function set<$Type extends Type.Struct>(
123 | world: World.Struct,
124 | entity: Id,
125 | layout: $Type,
126 | init = [] as unknown as ComponentSet.Init<$Type>,
127 | ) {
128 | const components = ComponentSet.make(world, layout, init)
129 | const prevArchetype = World.getEntityArchetype(world, entity)
130 | const nextArchetype = Graph.findOrMakeArchetype(
131 | world,
132 | Type.and(prevArchetype.type, layout),
133 | )
134 | if (prevArchetype === nextArchetype) {
135 | Archetype.write(entity, prevArchetype, components)
136 | } else {
137 | Archetype.move(entity, prevArchetype, nextArchetype, components)
138 | }
139 | World.setEntityArchetype(world, entity, nextArchetype)
140 | }
141 |
142 | /**
143 | * Remove one or more components from an entity. Throws an error if the entity
144 | * does not exist.
145 | *
146 | * @example Remove a single component from an entity
147 | * ```ts
148 | * Entity.unset(world, entity, [Health])
149 | * ```
150 | * @example Remove multiple components from an entity
151 | * ```ts
152 | * Entity.unset(world, entity, [Health, Faction])
153 | * ```
154 | */
155 | export function unset<$Type extends Type.Struct>(
156 | world: World.Struct,
157 | entity: Id,
158 | layout: $Type,
159 | ) {
160 | const prevArchetype = World.getEntityArchetype(world, entity)
161 | const nextArchetype = Graph.findOrMakeArchetype(
162 | world,
163 | Type.xor(prevArchetype.type, layout),
164 | )
165 | Archetype.move(entity, prevArchetype, nextArchetype)
166 | World.setEntityArchetype(world, entity, nextArchetype)
167 | }
168 |
169 | /**
170 | * Check if an entity has one or more components. Returns true if the entity
171 | * has a component corresponding to all of the provided schema ids, otherwise
172 | * returns false. Throws an error if the entity is not real.
173 | *
174 | * @example Check if an entity has a single component
175 | * ```ts
176 | * Entity.has(world, entity, [Health])
177 | * ```
178 | * @example Check if an entity has multiple components
179 | * ```ts
180 | * Entity.has(world, entity, [Position, Velocity])
181 | * ```
182 | */
183 | export function has(world: World.Struct, entity: Id, layout: Type.Struct) {
184 | const archetype = World.getEntityArchetype(world, entity)
185 | const type = Type.normalize(layout)
186 | return Type.isEqual(archetype.type, type) || Type.isSupersetOf(archetype.type, type)
187 | }
188 |
189 | /**
190 | * Check if an entity has one or more components. Unlike `has`, `tryHas` will
191 | * not throw if the entity is not real.
192 | */
193 | export function tryHas(world: World.Struct, entity: Id, layout: Type.Struct) {
194 | try {
195 | return has(world, entity, layout)
196 | } catch (error) {
197 | const final = Debug.unwrap(error)
198 | if (final instanceof World.EntityNotRealError) {
199 | return false
200 | }
201 | throw final
202 | }
203 | }
204 |
205 | /**
206 | * Remove all components from an entity. Throws an error if the entity does
207 | * not exist.
208 | *
209 | * @example Destroy an entity
210 | * ```ts
211 | * Entity.destroy(world, entity)
212 | * ```
213 | */
214 | export function destroy(world: World.Struct, entity: Id) {
215 | const archetype = World.getEntityArchetype(world, entity)
216 | Archetype.remove(archetype, entity)
217 | World.unsetEntityArchetype(world, entity)
218 | }
219 |
--------------------------------------------------------------------------------
/lib/src/format.ts:
--------------------------------------------------------------------------------
1 | import * as Types from "./types"
2 | import * as Symbols from "./symbols"
3 |
4 | export enum Kind {
5 | Uint8,
6 | Uint16,
7 | Uint32,
8 | Int8,
9 | Int16,
10 | Int32,
11 | Float32,
12 | Float64,
13 | }
14 |
15 | /**
16 | * Number format. Encloses a TypedArray constructor which corresponds to the
17 | * format kind, (e.g. `FormatKind.Uint8`->`Uint8Array`).
18 | */
19 | export type Format<
20 | $Kind extends Kind = Kind,
21 | $Binary extends Types.TypedArrayConstructor = Types.TypedArrayConstructor,
22 | > = { kind: $Kind; binary: $Binary }
23 |
24 | function makeFormat<$Kind extends Kind, $Binary extends Types.TypedArrayConstructor>(
25 | kind: $Kind,
26 | binary: $Binary,
27 | ): Format<$Kind, $Binary> {
28 | return {
29 | [Symbols.$format]: true,
30 | kind,
31 | binary,
32 | } as { kind: $Kind; binary: $Binary }
33 | }
34 |
35 | export const float32 = makeFormat(Kind.Float32, Float32Array)
36 | export const float64 = makeFormat(Kind.Float64, Float64Array)
37 | export const uint8 = makeFormat(Kind.Uint8, Uint8Array)
38 | export const uint16 = makeFormat(Kind.Uint16, Uint16Array)
39 | export const uint32 = makeFormat(Kind.Uint32, Uint32Array)
40 | export const int8 = makeFormat(Kind.Int8, Int8Array)
41 | export const int16 = makeFormat(Kind.Int16, Int16Array)
42 | export const int32 = makeFormat(Kind.Int32, Int32Array)
43 |
--------------------------------------------------------------------------------
/lib/src/index.ts:
--------------------------------------------------------------------------------
1 | export type { Data } from "./archetype"
2 | /**
3 | * A module used to create and maintain **caches**—temporary stores for world
4 | * operations that can be deferred and applied at a later time, and to other
5 | * worlds.
6 | */
7 | export * as Cache from "./cache"
8 | /**
9 | * A module used to manage entities and their components.
10 | */
11 | export * as Entity from "./entity"
12 | /**
13 | * A module used to locate entities based on their component composition.
14 | */
15 | export * as Query from "./query"
16 | /**
17 | * A module used to create **schema**—component templates.
18 | */
19 | export * as Schema from "./schema"
20 | /**
21 | * A small, stringless, strongly-typed event system.
22 | */
23 | export * as Signal from "./signal"
24 | /**
25 | * A high-performance map that references values using unsigned integers.
26 | */
27 | export * as SparseMap from "./sparse_map"
28 | /**
29 | * A module used to create, combine, and examine **types**—entity component
30 | * types.
31 | */
32 | export * as Type from "./type"
33 | /**
34 | * General-purpose, static TypeScript types.
35 | */
36 | export * as Types from "./types"
37 | /**
38 | * A module used to create and manage **worlds**—the root object of a Harmony game.
39 | */
40 | export * as World from "./world"
41 | /** @internal */
42 | export * as Symbols from "./symbols"
43 | /** @internal */
44 | export * as Debug from "./debug"
45 |
46 | /**
47 | * An enum of Harmony's supported data types.
48 | */
49 | export * as Format from "./format"
50 |
--------------------------------------------------------------------------------
/lib/src/query.ts:
--------------------------------------------------------------------------------
1 | import * as Archetype from "./archetype"
2 | import * as Graph from "./archetype_graph"
3 | import * as Debug from "./debug"
4 | import * as Entity from "./entity"
5 | import * as Schema from "./schema"
6 | import * as Signal from "./signal"
7 | import * as Type from "./type"
8 | import * as World from "./world"
9 | import * as Symbols from "./symbols"
10 |
11 | /**
12 | * Component data sorted to match a query selector layout.
13 | *
14 | * @example Scalar binary query data
15 | * ```ts
16 | * const data: Query.RecordData<[Health]> = [
17 | * Float64Array,
18 | * ]
19 | * ```
20 | * @example Complex binary query data (struct-of-array)
21 | * ```ts
22 | * const data: Query.RecordData<[Position, Health]> = [
23 | * [{ x: Float64Array, y: Float64Array }],
24 | * Float64Array,
25 | * ]
26 | * ```
27 | * @example Complex native query data (array-of-struct)
28 | * ```ts
29 | * const data: Query.RecordData<[Position, Health]> = [
30 | * [{ x: 0, y: 0 }],
31 | * [0],
32 | * ]
33 | * ```
34 | */
35 | export type RecordData<$Type extends Type.Struct> = {
36 | [K in keyof $Type]: $Type[K] extends Schema.Id
37 | ? Archetype.Column<$Type[K]>["data"]
38 | : never
39 | }
40 |
41 | /**
42 | * A single query result.
43 | *
44 | * @example Iterating a query record (array-of-struct)
45 | * ```ts
46 | * const [e, [p, v]] = record
47 | * for (let i = 0; i < e.length; i++) {
48 | * const entity = e[i]
49 | * const position = p[i]
50 | * const velocity = v[i]
51 | * // ...
52 | * }
53 | * ```
54 | */
55 | export type Record<$Type extends Type.Struct> = [
56 | entities: ReadonlyArray,
57 | data: RecordData<$Type>,
58 | ]
59 |
60 | /**
61 | * An iterable list of query records, where each result corresponds to an
62 | * archetype (or archetype) of entities that match the query's selector.
63 | *
64 | * @example Iterate a query
65 | * ```ts
66 | * const query = Query.make(world, [Position, Velocity])
67 | * for (let i = 0; i < query.length; i++) {
68 | * const [entities, [p, v]] = record
69 | * // ...
70 | * }
71 | * ```
72 | */
73 | export type Struct<$Type extends Type.Struct = Type.Struct> = Record<$Type>[] & {
74 | [Symbols.$type]: Type.Struct
75 | }
76 | /**
77 | * A function that is executed when an entity is considered by a query. Returning
78 | * false will exclude the entity from the query results.
79 | */
80 | export type Filter = (type: Type.Struct, archetype: Archetype.Struct) => boolean
81 |
82 | function bindArchetype<$Type extends Type.Struct>(
83 | records: Struct<$Type>,
84 | layout: $Type,
85 | archetype: Archetype.Struct,
86 | ) {
87 | const columns = layout.map(function findColumnDataById(id) {
88 | const columnIndex = archetype.layout[id]
89 | Debug.invariant(columnIndex !== undefined)
90 | const column = archetype.store[columnIndex]
91 | Debug.invariant(column !== undefined)
92 | return column.data
93 | })
94 | records.push([archetype.entities, columns as unknown as RecordData<$Type>])
95 | }
96 |
97 | function maybeBindArchetype<$Type extends Type.Struct>(
98 | records: Struct<$Type>,
99 | type: Type.Struct,
100 | layout: $Type,
101 | archetype: Archetype.Struct,
102 | filters: Filter[],
103 | ) {
104 | if (
105 | filters.every(function testPredicate(predicate) {
106 | return predicate(type, archetype)
107 | })
108 | ) {
109 | if (archetype.real) {
110 | bindArchetype(records, layout, archetype)
111 | } else {
112 | const unsubscribe = Signal.subscribe(
113 | archetype.onRealize,
114 | function bindArchetypeAndUnsubscribe() {
115 | bindArchetype(records, layout, archetype)
116 | unsubscribe()
117 | },
118 | )
119 | }
120 | }
121 | }
122 |
123 | function makeStaticQueryInternal<$Type extends Type.Struct>(
124 | type: Type.Struct,
125 | layout: $Type,
126 | archetype: Archetype.Struct,
127 | filters: Filter[],
128 | ): Struct<$Type> {
129 | Type.invariantNormalized(type)
130 | const query: Struct<$Type> = Object.assign([], { [Symbols.$type]: type })
131 | maybeBindArchetype(query, type, layout, archetype, filters)
132 | Graph.traverse(archetype, function maybeBindNextArchetype(archetype) {
133 | maybeBindArchetype(query, type, layout, archetype, filters)
134 | })
135 | return query
136 | }
137 |
138 | /**
139 | * Create an auto-updating list of entities and matching components that match
140 | * both a set of schema ids and provided query filters, if any. The query will
141 | * attempt to include newly-incorporated archetypes as the ECS grows in
142 | * complexity.
143 | *
144 | * @example A simple motion system (struct-of-array)
145 | * ```ts
146 | * const Kinetic = [Position, Velocity] as const
147 | * const kinetics = Query.make(world, Kinetic)
148 | * for (let i = 0; i < kinetics.length; i++) {
149 | * const [e, [p, v]] = kinetics
150 | * for (let j = 0; j < e.length; j++) {
151 | * // apply motion
152 | * p.x[j] += v.x[j]
153 | * p.y[j] += v.y[j]
154 | * }
155 | * }
156 | * ```
157 | */
158 | export function make<$Type extends Type.Struct>(
159 | world: World.Struct,
160 | layout: $Type,
161 | ...filters: Filter[]
162 | ): Struct<$Type> {
163 | const type = Type.normalize(layout)
164 | const identity = Graph.findOrMakeArchetype(world, type)
165 | const query = makeStaticQueryInternal(type, layout, identity, filters)
166 | Signal.subscribe(
167 | identity.onTableInsert,
168 | function maybeBindInsertedArchetype(archetype) {
169 | maybeBindArchetype(query, type, layout, archetype, filters)
170 | },
171 | )
172 | return query
173 | }
174 |
175 | /**
176 | * Create an auto-updating list of results that match both a set of schema ids
177 | * and provided query filters, if any. The query will **not** attempt to
178 | * include newly-incorporated archetypes.
179 | *
180 | * @example Ignoring new archetypes
181 | * ```ts
182 | * Entity.make(world, [Point])
183 | * const points = Query.make(world, [Point])
184 | * points.reduce((a, [e]) => a + e.length, 0) // 1
185 | * Entity.make(world, [Point, Velocity])
186 | * points.reduce((a, [e]) => a + e.length, 0) // 1 (did not detect (P, V) archetype)
187 | * ```
188 | */
189 | export function makeStatic<$Type extends Type.Struct>(
190 | world: World.Struct,
191 | layout: $Type,
192 | ...filters: Filter[]
193 | ): Struct<$Type> {
194 | const type = Type.normalize(layout)
195 | const identity = Graph.findOrMakeArchetype(world, type)
196 | return makeStaticQueryInternal(type, layout, identity, filters)
197 | }
198 |
199 | /**
200 | * A query filter that will exclude entities with all of the specified
201 | * components.
202 | */
203 | export function not(layout: Type.Struct) {
204 | const type = Type.normalize(layout)
205 | return function isArchetypeNotSupersetOfType(
206 | _: Type.Struct,
207 | archetype: Archetype.Struct,
208 | ) {
209 | return !Type.isSupersetOf(archetype.type, type)
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/lib/src/schema.ts:
--------------------------------------------------------------------------------
1 | import * as Entity from "./entity"
2 | import * as Format from "./format"
3 | import * as Symbols from "./symbols"
4 | import * as Types from "./types"
5 | import * as World from "./world"
6 |
7 | /** @internal */
8 | export enum SchemaKind {
9 | NativeScalar,
10 | NativeObject,
11 | BinaryScalar,
12 | BinaryStruct,
13 | Tag,
14 | }
15 |
16 | /** @internal */
17 | export type NativeScalarSchema<$Shape extends Format.Format = Format.Format> = {
18 | id: number
19 | kind: SchemaKind.NativeScalar
20 | shape: $Shape
21 | }
22 |
23 | /** @internal */
24 | type NativeObjectShape = { [key: string]: Format.Format }
25 |
26 | /** @internal */
27 | export type NativeObjectSchema<$Shape extends NativeObjectShape = NativeObjectShape> = {
28 | id: number
29 | kind: SchemaKind.NativeObject
30 | shape: $Shape
31 | }
32 |
33 | /** @internal */
34 | export type BinaryScalarSchema<$Shape extends Format.Format = Format.Format> = {
35 | id: number
36 | kind: SchemaKind.BinaryScalar
37 | shape: $Shape
38 | }
39 |
40 | /** @internal */
41 | export type BinaryStructSchema<
42 | $Shape extends { [key: string]: Format.Format } = { [key: string]: Format.Format },
43 | > = {
44 | id: number
45 | kind: SchemaKind.BinaryStruct
46 | shape: $Shape
47 | }
48 |
49 | /** @internal */
50 | export type TagSchema = {
51 | id: number
52 | kind: SchemaKind.Tag
53 | shape: null
54 | }
55 |
56 | /** @internal */
57 | export type AnyNativeSchema = NativeScalarSchema | NativeObjectSchema
58 | /** @internal */
59 | export type AnyBinarySchema = BinaryScalarSchema | BinaryStructSchema
60 | /** @internal */
61 | export type ComplexSchema = AnyBinarySchema | AnyNativeSchema
62 | /** @internal */
63 | export type AnySchema = ComplexSchema | TagSchema
64 | /** @internal */
65 | export type Shape<$Type extends { shape: unknown }> = $Type["shape"]
66 |
67 | /**
68 | * An entity id wrapped in a generic type that allows Harmony to infer the
69 | * component shape from the underlying primitive type.
70 | */
71 | export type Id<$Schema extends AnySchema = AnySchema> = Types.Opaque
72 |
73 | /**
74 | * A schema whose components are standard JavaScript objects or scalar values.
75 | * Unlike binary components, native components are stored in an array-of-
76 | * structs architecture.
77 | */
78 | export type NativeSchema<$Shape extends Shape> =
79 | $Shape extends Shape
80 | ? Id>
81 | : $Shape extends Shape
82 | ? Id>
83 | : never
84 |
85 | /**
86 | * A schema whose components are stored in one or more TypedArrays. If the
87 | * schema shape is complex (i.e non-scalar), derived components will be stored
88 | * in a struct-of-array architecture, where each component field is allocated a
89 | * separate, tightly-packed TypedArray.
90 | */
91 | export type BinarySchema<$Shape extends Shape> =
92 | $Shape extends Shape
93 | ? Id>
94 | : $Shape extends Shape
95 | ? Id>
96 | : never
97 |
98 | /** @internal */
99 | export function isFormat(object: object): object is Format.Format {
100 | return Symbols.$format in object
101 | }
102 |
103 | /** @internal */
104 | export function isNativeSchema(schema: ComplexSchema): schema is AnyNativeSchema {
105 | return (
106 | schema.kind === SchemaKind.NativeScalar || schema.kind === SchemaKind.NativeObject
107 | )
108 | }
109 |
110 | /** @internal */
111 | export function isBinarySchema(schema: ComplexSchema): schema is AnyBinarySchema {
112 | return (
113 | schema.kind === SchemaKind.BinaryScalar || schema.kind === SchemaKind.BinaryStruct
114 | )
115 | }
116 |
117 | /**
118 | * Create a native schema. Returns an id that can be used to reference the
119 | * schema throughout Harmony's API.
120 | *
121 | * @example Create a scalar native schema
122 | * ```ts
123 | * const Health = Schema.make(world, Format.uint32)
124 | * ```
125 | * @example Create a complex native schema
126 | * ```ts
127 | * const Position = Schema.make(world, {
128 | * x: Format.float64,
129 | * y: Format.float64
130 | * })
131 | * ```
132 | */
133 | export function make<$Shape extends Shape>(
134 | world: World.Struct,
135 | shape: $Shape,
136 | reserve?: number,
137 | ): NativeSchema<$Shape> {
138 | const id = Entity.reserve(world, reserve)
139 | let schema: AnyNativeSchema
140 | if (isFormat(shape)) {
141 | schema = { id, kind: SchemaKind.NativeScalar, shape }
142 | } else {
143 | schema = { id, kind: SchemaKind.NativeObject, shape }
144 | }
145 | World.registerSchema(world, id, schema)
146 | return id as NativeSchema<$Shape>
147 | }
148 |
149 | /**
150 | * Create a binary schema. Returns an id that is used to reference the schema
151 | * throughout Harmony's API.
152 | *
153 | * @example Create a scalar binary schema
154 | * ```ts
155 | * const Health = Schema.makeBinary(world, Format.uint32)
156 | * ```
157 | * @example Create a complex binary schema
158 | * ```ts
159 | * const Position = Schema.makeBinary(world, {
160 | * x: Format.float64,
161 | * y: Format.float64
162 | * })
163 | * ```
164 | */
165 | export function makeBinary<$Shape extends Shape>(
166 | world: World.Struct,
167 | shape: $Shape,
168 | reserve?: number,
169 | ): BinarySchema<$Shape> {
170 | const id = Entity.reserve(world, reserve)
171 | let schema: AnyBinarySchema
172 | if (isFormat(shape)) {
173 | schema = { id, kind: SchemaKind.BinaryScalar, shape }
174 | } else {
175 | schema = { id, kind: SchemaKind.BinaryStruct, shape }
176 | }
177 | World.registerSchema(world, id, schema)
178 | return id as BinarySchema<$Shape>
179 | }
180 |
181 | /**
182 | * Create a tag schema – a data-less, lightweight component type that is faster
183 | * than complex or scalar components since there is no data to copy when moving
184 | * an entity between archetypes.
185 | *
186 | * Returns an id that is used to reference the schema throughout Harmony's API.
187 | *
188 | * @example Create a tag schema
189 | * ```ts
190 | * const Frozen = Schema.makeTag(world)
191 | * ```
192 | */
193 | export function makeTag(world: World.Struct, reserve?: number): Id {
194 | const id = Entity.reserve(world, reserve)
195 | World.registerSchema(world, id, { id, kind: SchemaKind.Tag, shape: null })
196 | return id as Id
197 | }
198 |
--------------------------------------------------------------------------------
/lib/src/signal.ts:
--------------------------------------------------------------------------------
1 | import * as Debug from "./debug"
2 |
3 | export type Subscriber = (t: T) => void
4 | export type Struct = Subscriber[]
5 |
6 | export function make(): Struct {
7 | return []
8 | }
9 |
10 | export function subscribe(subscribers: Struct, subscriber: Subscriber) {
11 | const index = subscribers.push(subscriber) - 1
12 | return function unsubscribe() {
13 | subscribers.splice(index, 1)
14 | }
15 | }
16 |
17 | export function dispatch(subscribers: Struct, t: T) {
18 | for (let i = subscribers.length - 1; i >= 0; i--) {
19 | const subscriber = subscribers[i]
20 | Debug.invariant(subscriber !== undefined)
21 | subscriber(t!)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/src/sparse_map.spec.ts:
--------------------------------------------------------------------------------
1 | import * as SparseMap from "./sparse_map"
2 |
3 | describe("SparseMap", () => {
4 | describe("make", () => {
5 | it("creates an empty SparseMap when no values are provided", () => {
6 | const sparseMap = SparseMap.make()
7 | expect(sparseMap.size).toBe(0)
8 | })
9 | it("creates a SparseMap using a sparse array, initializing an entry for each index-value pair", () => {
10 | const sparseMap = SparseMap.make([, , , "a"])
11 | expect(SparseMap.get(sparseMap, 3)).toBe("a")
12 | expect(sparseMap.size).toBe(1)
13 | })
14 | })
15 |
16 | describe("get", () => {
17 | it("returns the value of a entry at the provided key", () => {
18 | const sparseMap = SparseMap.make(["a", "b"])
19 | expect(SparseMap.get(sparseMap, 0)).toBe("a")
20 | expect(SparseMap.get(sparseMap, 1)).toBe("b")
21 | })
22 | it("returns undefined for non-existing keys", () => {
23 | const sparseMap = SparseMap.make([, "a", "b"])
24 | expect(SparseMap.get(sparseMap, 0)).toBe(undefined)
25 | })
26 | })
27 |
28 | describe("set", () => {
29 | it("creates new entries at non-existing keys", () => {
30 | const sparseMap = SparseMap.make()
31 | SparseMap.set(sparseMap, 99, "a")
32 | SparseMap.set(sparseMap, 42, "b")
33 | expect(SparseMap.get(sparseMap, 99)).toBe("a")
34 | expect(SparseMap.get(sparseMap, 42)).toBe("b")
35 | expect(sparseMap.size).toBe(2)
36 | })
37 | it("updates existing entries", () => {
38 | const sparseMap = SparseMap.make()
39 | SparseMap.set(sparseMap, 0, "a")
40 | SparseMap.set(sparseMap, 1, "b")
41 | SparseMap.set(sparseMap, 0, "c")
42 | SparseMap.set(sparseMap, 1, "d")
43 | expect(SparseMap.get(sparseMap, 0)).toBe("c")
44 | expect(SparseMap.get(sparseMap, 1)).toBe("d")
45 | expect(sparseMap.size).toBe(2)
46 | })
47 | })
48 |
49 | describe("remove", () => {
50 | it("removes the entry of the specified key", () => {
51 | const sparseMap = SparseMap.make(["a", "b", "c"])
52 | SparseMap.remove(sparseMap, 1)
53 | expect(SparseMap.get(sparseMap, 0)).toBe("a")
54 | expect(SparseMap.get(sparseMap, 1)).toBe(undefined)
55 | expect(SparseMap.get(sparseMap, 2)).toBe("c")
56 | expect(sparseMap.size).toBe(2)
57 | })
58 | it("does not alter the SparseMap when called with a non-existing key", () => {
59 | const sparseMap = SparseMap.make(["a", , "c"])
60 | SparseMap.remove(sparseMap, 1)
61 | expect(SparseMap.get(sparseMap, 0)).toBe("a")
62 | expect(SparseMap.get(sparseMap, 1)).toBe(undefined)
63 | expect(SparseMap.get(sparseMap, 2)).toBe("c")
64 | expect(sparseMap.size).toBe(2)
65 | })
66 | })
67 |
68 | describe("forEach", () => {
69 | it("executes a callback function with the value and key of each entry in the SparseMap", () => {
70 | const data: [number, string][] = [
71 | [0, "a"],
72 | [10_100, "b"],
73 | [9, "c"],
74 | [23, "d"],
75 | [1_000_000, "e"],
76 | [34, "f"],
77 | ]
78 | const entries: [number, string][] = []
79 | const sparseMap = SparseMap.make(
80 | data.reduce((a, [key, value]) => {
81 | a[key] = value
82 | return a
83 | }, [] as string[]),
84 | )
85 | SparseMap.forEach(sparseMap, (value, key) => entries.push([key, value]))
86 | expect(entries).toEqual(data.sort(([keyA], [keyB]) => keyA - keyB))
87 | })
88 | })
89 | })
90 |
--------------------------------------------------------------------------------
/lib/src/sparse_map.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A callback function passed to `SparseMap.forEach`
3 | */
4 | export type Iteratee<$Value, $Key extends number> = (value: $Value, key: $Key) => void
5 |
6 | /**
7 | * A map that references values using unsigned integers. Uses a packed array in
8 | * conjunction with a key-value index for fast lookup, add/remove operations,
9 | * and iteration. SparseMap is used frequently in Harmony since entities
10 | * (including schema and game objects) are represented as unsigned integers.
11 | *
12 | * SparseMap is faster than ES Maps in all cases. It is slower than sparse
13 | * arrays for read/write operations, but faster to iterate.
14 | */
15 | export type Struct<$Value = unknown, $Key extends number = number> = {
16 | /** @readonly */
17 | size: number
18 | /** @internal */
19 | keys: $Key[]
20 | /** @internal */
21 | values: $Value[]
22 | /** @internal */
23 | index: Record<$Key, number | undefined>
24 | }
25 |
26 | /**
27 | * Create a SparseMap.
28 | */
29 | export function make<$Value, $Key extends number = number>(
30 | init: ($Value | undefined)[] = [],
31 | ): Struct<$Value, $Key> {
32 | let size = 0
33 | const index: (number | undefined)[] = []
34 | const keys: $Key[] = []
35 | const values: $Value[] = []
36 | for (let i = 0; i < init.length; i++) {
37 | const value = init[i]
38 | if (value !== undefined) {
39 | keys.push(i as $Key)
40 | values[i] = value
41 | index[i] = size
42 | size++
43 | }
44 | }
45 | return {
46 | size,
47 | keys,
48 | values,
49 | index,
50 | }
51 | }
52 |
53 | /**
54 | * Retrieve the value for a given key from a SparseMap. Returns `undefined` if
55 | * no record exists for the given key.
56 | */
57 | export function get<$Value, $Key extends number>(
58 | map: Struct<$Value, $Key>,
59 | key: $Key,
60 | ): $Value | undefined {
61 | return map.values[key]
62 | }
63 |
64 | /**
65 | * Add or update the value of an entry with the given key within a SparseMap.
66 | */
67 | export function set<$Value, $Key extends number>(
68 | map: Struct<$Value, $Key>,
69 | key: $Key,
70 | value: $Value,
71 | ) {
72 | if (!has(map, key)) {
73 | map.index[key] = map.keys.push(key) - 1
74 | map.size++
75 | }
76 | map.values[key] = value
77 | }
78 |
79 | /**
80 | * Remove an entry by key from a SparseMap.
81 | */
82 | export function remove<$Key extends number>(map: Struct, key: $Key) {
83 | const i = map.index[key]
84 | if (i === undefined) return
85 | const k = map.keys.pop()
86 | const h = --map.size
87 | map.index[key] = map.values[key] = undefined
88 | if (h !== i) {
89 | map.keys[i!] = k!
90 | map.index[k!] = i
91 | }
92 | }
93 |
94 | /**
95 | * Check for the existence of a value by key within a SparseMap. Returns true
96 | * if the SparseMap contains an entry for the provided key.
97 | */
98 | export function has(map: Struct, key: number) {
99 | return map.index[key] !== undefined
100 | }
101 |
102 | /**
103 | * Clear all entries from a SparseMap.
104 | */
105 | export function clear(map: Struct) {
106 | map.keys.length = 0
107 | map.values.length = 0
108 | ;(map.index as number[]).length = 0
109 | map.size = 0
110 | }
111 |
112 | /**
113 | * Iterate a SparseMap using a iteratee function.
114 | */
115 | export function forEach<$Value, $Key extends number>(
116 | map: Struct<$Value, $Key>,
117 | iteratee: Iteratee<$Value, $Key>,
118 | ) {
119 | for (let i = 0; i < map.size; i++) {
120 | const k = map.keys[i]!
121 | const d = map.values[k]!
122 | iteratee(d, k)
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/lib/src/symbols.ts:
--------------------------------------------------------------------------------
1 | export const $type = Symbol("harmony_type")
2 | export const $format = Symbol("harmony_format")
3 | export const $tombstone = Symbol("harmony_tombstone")
4 | export const $timestamp = Symbol("harmony_timestamp")
5 |
--------------------------------------------------------------------------------
/lib/src/type.spec.ts:
--------------------------------------------------------------------------------
1 | import * as Schema from "./schema"
2 | import * as Type from "./type"
3 |
4 | describe("Type", () => {
5 | describe("add", () => {
6 | it("refuses an abnormal type", () => {
7 | const type = [2, 1, 0] as Schema.Id[]
8 | expect(() => Type.add(type, 0 as Schema.Id)).toThrowError("abnormal type")
9 | })
10 | it("prepends a type to a normal type", () => {
11 | const type = [1, 2, 3] as Schema.Id[]
12 | expect(Type.add(type, 0 as Schema.Id)).toEqual([0, 1, 2, 3])
13 | })
14 | it("inserts a type within a normal type", () => {
15 | const type = [0, 1, 3] as Schema.Id[]
16 | expect(Type.add(type, 2 as Schema.Id)).toEqual([0, 1, 2, 3])
17 | })
18 | it("appends a type to a normal type", () => {
19 | const type = [0, 1, 2] as Schema.Id[]
20 | expect(Type.add(type, 3 as Schema.Id)).toEqual([0, 1, 2, 3])
21 | })
22 | })
23 |
24 | describe("remove", () => {
25 | it("refuses an abnormal type", () => {
26 | const type = [2, 1, 0] as Schema.Id[]
27 | expect(() => Type.remove(type, 0 as Schema.Id)).toThrowError("abnormal type")
28 | })
29 | it("shifts a sub-type from a normal type", () => {
30 | const type = [0, 1, 2, 3] as Schema.Id[]
31 | expect(Type.remove(type, 0 as Schema.Id)).toEqual([1, 2, 3])
32 | })
33 | it("splices a sub-type from a normal type", () => {
34 | const type = [0, 1, 2, 3] as Schema.Id[]
35 | expect(Type.remove(type, 2 as Schema.Id)).toEqual([0, 1, 3])
36 | })
37 | it("pops a sub-type from a normal type", () => {
38 | const type = [0, 1, 2, 3] as Schema.Id[]
39 | expect(Type.remove(type, 3 as Schema.Id)).toEqual([0, 1, 2])
40 | })
41 | })
42 |
43 | describe("normalize", () => {
44 | it("sorts a sub-type in ascending order", () => {
45 | const type = [7, 3, 1, 9, 6] as Schema.Id[]
46 | expect(Type.normalize(type)).toEqual([1, 3, 6, 7, 9])
47 | })
48 | })
49 |
50 | describe("isSupersetOf", () => {
51 | it("asserts normal types", () => {
52 | const normal = [0, 1, 2] as Schema.Id[]
53 | const abnormal = [2, 0] as Schema.Id[]
54 | expect(() => Type.isSupersetOf(normal, abnormal)).toThrowError("abnormal type")
55 | expect(() => Type.isSupersetOf(abnormal, normal)).toThrowError("abnormal type")
56 | })
57 | it("matches type where type contains each element of sub-type", () => {
58 | const outer = [0, 3, 8, 12, 17] as Schema.Id[]
59 | const inner = [0, 12, 17] as Schema.Id[]
60 | expect(Type.isSupersetOf(outer, inner)).toBe(true)
61 | })
62 | it("fails to match type where type does not contain each element of sub-type", () => {
63 | const outer = [0, 3, 8, 12, 17] as Schema.Id[]
64 | const inner = [0, 11, 17] as Schema.Id[]
65 | expect(Type.isSupersetOf(outer, inner)).toBe(false)
66 | })
67 | })
68 |
69 | describe("maybeSupersetOf", () => {
70 | it("asserts normal types", () => {
71 | const normal = [0, 1, 2] as Schema.Id[]
72 | const abnormal = [2, 0] as Schema.Id[]
73 | expect(() => Type.maybeSupersetOf(normal, abnormal)).toThrowError("abnormal type")
74 | expect(() => Type.maybeSupersetOf(abnormal, normal)).toThrowError("abnormal type")
75 | })
76 | it("matches type where outer type may be subset of eventual superset of inner type", () => {
77 | expect(Type.maybeSupersetOf([0] as Schema.Id[], [1, 2, 3, 4] as Schema.Id[])).toBe(
78 | true,
79 | )
80 | expect(Type.maybeSupersetOf([7, 9] as Schema.Id[], [9, 10] as Schema.Id[])).toBe(
81 | true,
82 | )
83 | expect(
84 | Type.maybeSupersetOf([5, 6, 7, 8] as Schema.Id[], [6, 99] as Schema.Id[]),
85 | ).toBe(true)
86 | })
87 | it("fails to mach type where outer type may never be subset of superset of inner type", () => {
88 | expect(Type.maybeSupersetOf([0, 5] as Schema.Id[], [4, 5, 6] as Schema.Id[])).toBe(
89 | false,
90 | )
91 | expect(Type.maybeSupersetOf([9, 10] as Schema.Id[], [7, 9] as Schema.Id[])).toBe(
92 | false,
93 | )
94 | expect(Type.maybeSupersetOf([5] as Schema.Id[], [6, 12] as Schema.Id[])).toBe(true)
95 | })
96 | })
97 |
98 | describe("getIdsBetween", () => {
99 | it("asserts outer is superset of subset", () => {
100 | const outer = [0, 2] as Schema.Id[]
101 | const inner = [0, 1, 2] as Schema.Id[]
102 | expect(() => Type.getIdsBetween(outer, inner)).toThrowError("type is not superset")
103 | })
104 | it("builds an array of type ids between inner and outer", () => {
105 | expect(
106 | Type.getIdsBetween([0, 1, 4, 9, 12, 17] as Schema.Id[], [9, 17] as Schema.Id[]),
107 | ).toEqual([0, 1, 4, 12])
108 | expect(Type.getIdsBetween([0, 1, 2] as Schema.Id[], [1] as Schema.Id[])).toEqual([
109 | 0,
110 | ])
111 | expect(Type.getIdsBetween([0, 1, 2, 3] as Schema.Id[], [3] as Schema.Id[])).toEqual(
112 | [0, 1, 2],
113 | )
114 | })
115 | })
116 | })
117 |
--------------------------------------------------------------------------------
/lib/src/type.ts:
--------------------------------------------------------------------------------
1 | import { invariant } from "./debug"
2 | import { Id } from "./schema"
3 |
4 | /**
5 | * An array of schema ids that fully defines the component makeup of an entity.
6 | */
7 | export type Struct = ReadonlyArray
8 |
9 | export function add(type: Struct, add: Id): Struct {
10 | invariantNormalized(type)
11 | const next: number[] = []
12 | let added = false
13 | for (let i = 0; i < type.length; i++) {
14 | const id = type[i]
15 | invariant(id !== undefined)
16 | if (id >= add && !added) {
17 | if (id !== add) {
18 | next.push(add)
19 | }
20 | added = true
21 | }
22 | next.push(id)
23 | }
24 | if (!added) {
25 | next.push(add)
26 | }
27 | return next as unknown as Struct
28 | }
29 |
30 | export function and(a: Struct, b: Struct): Struct {
31 | invariantNormalized(a)
32 | let next = a.slice() as Struct
33 | for (let i = 0; i < b.length; i++) {
34 | const id = b[i]
35 | invariant(id !== undefined)
36 | next = add(next, id)
37 | }
38 | return next
39 | }
40 |
41 | export function xor(a: Struct, b: Struct): Struct {
42 | invariantNormalized(a)
43 | let next = a.slice() as Struct
44 | for (let i = 0; i < b.length; i++) {
45 | const id = b[i]
46 | invariant(id !== undefined)
47 | next = remove(next, id)
48 | }
49 | return next
50 | }
51 |
52 | export function remove(type: Struct, remove: Id): Struct {
53 | invariantNormalized(type)
54 | const next: number[] = []
55 | for (let i = 0; i < type.length; i++) {
56 | const e = type[i]
57 | if (e !== remove) {
58 | invariant(e !== undefined)
59 | next.push(e)
60 | }
61 | }
62 | return next as unknown as Struct
63 | }
64 |
65 | export function normalize(type: Struct) {
66 | return Object.freeze(type.slice().sort((a, b) => a - b))
67 | }
68 |
69 | export function invariantNormalized(type: Struct) {
70 | for (let i = 0; i < type.length - 1; i++) {
71 | const a = type[i]
72 | const b = type[i + 1]
73 | invariant(a !== undefined)
74 | invariant(b !== undefined)
75 | if (a > b) {
76 | throw new TypeError("abnormal type")
77 | }
78 | }
79 | }
80 |
81 | export function isEqual(outer: Struct, inner: Struct) {
82 | if (outer.length !== inner.length) {
83 | return false
84 | }
85 | for (let i = 0; i < outer.length; i++) {
86 | if (outer[i] !== inner[i]) return false
87 | }
88 | return true
89 | }
90 |
91 | export function isSupersetOf(outer: Struct, inner: Struct) {
92 | invariantNormalized(outer)
93 | invariantNormalized(inner)
94 | let o = 0
95 | let i = 0
96 | if (outer.length <= inner.length) {
97 | return false
98 | }
99 | while (o < outer.length && i < inner.length) {
100 | const outerId = outer[o]
101 | const innerId = inner[i]
102 | invariant(outerId !== undefined)
103 | invariant(innerId !== undefined)
104 | if (outerId < innerId) {
105 | o++
106 | } else if (outerId === innerId) {
107 | o++
108 | i++
109 | } else {
110 | return false
111 | }
112 | }
113 | return i === inner.length
114 | }
115 |
116 | export function invariantIsSupersetOf(outer: Struct, inner: Struct) {
117 | if (!isSupersetOf(outer, inner)) {
118 | throw new RangeError("type is not superset")
119 | }
120 | }
121 |
122 | export function maybeSupersetOf(outer: Struct, inner: Struct) {
123 | invariantNormalized(outer)
124 | invariantNormalized(inner)
125 | let o = 0
126 | let i = 0
127 | if (outer.length === 0) {
128 | return true
129 | }
130 | while (o < outer.length && i < inner.length) {
131 | const outerId = outer[o]
132 | const innerId = inner[i]
133 | invariant(outerId !== undefined)
134 | invariant(innerId !== undefined)
135 | if (outerId < innerId) {
136 | o++
137 | } else if (outerId === innerId) {
138 | o++
139 | i++
140 | } else {
141 | return false
142 | }
143 | }
144 | return true
145 | }
146 |
147 | export function getIdsBetween(right: Struct, left: Struct) {
148 | invariantIsSupersetOf(right, left)
149 | let o = 0
150 | let i = 0
151 | const path: Id[] = []
152 | if (right.length - left.length === 1) {
153 | let j = 0
154 | let length = right.length
155 | for (; j < length && right[j] === left[j]; j++) {}
156 | const t = right[j]
157 | invariant(t !== undefined)
158 | path.push(t)
159 | return path
160 | }
161 | while (o < right.length - 1) {
162 | const outerId = right[o]
163 | const innerId = left[i]
164 | invariant(outerId !== undefined)
165 | if (innerId === undefined || outerId < innerId) {
166 | path.push(outerId)
167 | o++
168 | } else if (outerId === innerId) {
169 | o++
170 | i++
171 | }
172 | }
173 | return path
174 | }
175 |
--------------------------------------------------------------------------------
/lib/src/types.ts:
--------------------------------------------------------------------------------
1 | export type TypedArray =
2 | | Float32Array
3 | | Float64Array
4 | | Int8Array
5 | | Int16Array
6 | | Int32Array
7 | | Uint8Array
8 | | Uint8ClampedArray
9 | | Uint16Array
10 | | Uint32Array
11 |
12 | export type TypedArrayConstructor = {
13 | new (length: number | ArrayBufferLike): TypedArray
14 | BYTES_PER_ELEMENT: number
15 | }
16 |
17 | export type Construct<$Ctor> = $Ctor extends { new (...args: any[]): infer _ } ? _ : never
18 |
19 | export declare class OpaqueTag<$Tag> {
20 | protected tag: $Tag
21 | }
22 |
23 | export type Opaque<$Type, $Tag> = $Type & OpaqueTag<$Tag>
24 |
--------------------------------------------------------------------------------
/lib/src/world.spec.ts:
--------------------------------------------------------------------------------
1 | import * as World from "./world"
2 | import * as Schema from "./schema"
3 | import * as Entity from "./entity"
4 |
5 | describe("World", () => {
6 | let world: World.Struct
7 |
8 | beforeEach(() => (world = World.make(10)))
9 |
10 | describe("make", () => {
11 | it("creates a world with a configurable entity size", () => {
12 | const size = 8
13 | const world = World.make(size)
14 | expect(world.size).toBe(size)
15 | })
16 | it("starts entity id counter at 0", () => {
17 | expect(world.entityHead).toBe(0)
18 | })
19 | })
20 |
21 | describe("registerSchema", () => {
22 | it("registers a schema with the world", () => {
23 | const id = 1
24 | const schema: Schema.NativeObjectSchema = {
25 | id,
26 | kind: Schema.SchemaKind.NativeObject,
27 | shape: {},
28 | }
29 | World.registerSchema(world, id, schema)
30 | expect(World.findSchemaById(world, id)).toBe(schema)
31 | })
32 | })
33 |
34 | describe("getEntityArchetype", () => {
35 | it("returns an entity's archetype", () => {
36 | const A = Schema.make(world, {})
37 | const entity = Entity.make(world, [A])
38 | const archetype = World.getEntityArchetype(world, entity)
39 | expect(archetype.type).toEqual([A])
40 | })
41 | it("throws when entity does not exist", () => {
42 | expect(() => World.getEntityArchetype(world, 9)).toThrow()
43 | })
44 | })
45 |
46 | describe("tryGetEntityArchetype", () => {
47 | it("returns undefined when entity does not exist", () => {
48 | expect(World.tryGetEntityArchetype(world, 9)).toBeUndefined()
49 | })
50 | })
51 |
52 | describe("setEntityArchetype", () => {
53 | it("associates an entity with an archetype", () => {
54 | const entity = Entity.reserve(world)
55 | World.setEntityArchetype(world, entity, world.rootArchetype)
56 | expect(World.getEntityArchetype(world, entity)).toBe(world.rootArchetype)
57 | })
58 | })
59 |
60 | describe("unsetEntityArchetype", () => {
61 | it("disassociates an entity from an archetype", () => {
62 | const entity = Entity.reserve(world)
63 | World.setEntityArchetype(world, entity, world.rootArchetype)
64 | World.unsetEntityArchetype(world, entity)
65 | expect(World.tryGetEntityArchetype(world, entity)).toBe(undefined)
66 | })
67 | })
68 | })
69 |
--------------------------------------------------------------------------------
/lib/src/world.ts:
--------------------------------------------------------------------------------
1 | import * as Archetype from "./archetype"
2 | import * as Debug from "./debug"
3 | import * as Entity from "./entity"
4 | import * as Schema from "./schema"
5 | import * as Type from "./type"
6 |
7 | export class EntityNotRealError extends Error {
8 | constructor(entity: Entity.Id) {
9 | super(`Entity "${entity}" is not real`)
10 | }
11 | }
12 |
13 | /**
14 | * The root object of the ECS. A world maintains entity identifiers, stores
15 | * schemas, and associates components with entities in tables called
16 | * archetypes: collections of entities that share the same component makeup.
17 | *
18 | * Archetypes are stored in a connected graph structure, where each node is
19 | * linked to adjacent archetypes by the addition or removal or a single
20 | * component type. An entity's archetype is selected based on its current
21 | * component composition. An entity can belong only to a single archetype at a
22 | * time.
23 | *
24 | * Entities without any components (e.g. destroyed entities) are never
25 | * discarded, but moved to the world's root archetype.
26 | */
27 | export type Struct = {
28 | rootArchetype: Archetype.Struct
29 | entityHead: number
30 | entityIndex: (Archetype.Struct | undefined)[]
31 | schemaIndex: Schema.AnySchema[]
32 | size: number
33 | }
34 |
35 | /** @internal */
36 | export function registerSchema(world: Struct, id: Entity.Id, schema: Schema.AnySchema) {
37 | world.schemaIndex[id] = schema
38 | }
39 |
40 | /** @internal */
41 | export function findSchemaById(world: Struct, id: Entity.Id) {
42 | const schema = world.schemaIndex[id]
43 | Debug.invariant(
44 | schema !== undefined,
45 | `Failed to locate schema: entity "${id}" is not a schema`,
46 | )
47 | return schema
48 | }
49 |
50 | /** @internal */
51 | export function getEntityArchetype(world: Struct, entity: Entity.Id) {
52 | const archetype = world.entityIndex[entity]
53 | Debug.invariant(
54 | archetype !== undefined,
55 | `Failed to locate archetype`,
56 | new EntityNotRealError(entity),
57 | )
58 | return archetype
59 | }
60 |
61 | /** @internal */
62 | export function tryGetEntityArchetype(world: Struct, entity: Entity.Id) {
63 | return world.entityIndex[entity]
64 | }
65 |
66 | /** @internal */
67 | export function setEntityArchetype(
68 | world: Struct,
69 | entity: Entity.Id,
70 | archetype: Archetype.Struct,
71 | ) {
72 | world.entityIndex[entity] = archetype
73 | }
74 |
75 | /** @internal */
76 | export function unsetEntityArchetype(world: Struct, entity: Entity.Id) {
77 | world.entityIndex[entity] = undefined
78 | }
79 |
80 | /**
81 | * Create a world. Requires a maximum entity size which is used to allocate
82 | * fixed memory for binary component arrays.
83 | *
84 | * @example Create a world capable of storing one million entities
85 | * ```ts
86 | * const world = World.make(1e6)
87 | * ```
88 | */
89 | export function make(size: number): Struct {
90 | const type: Type.Struct = []
91 | return {
92 | rootArchetype: Archetype.makeInner(type, []),
93 | entityHead: 0,
94 | entityIndex: [],
95 | schemaIndex: [],
96 | size,
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/lib/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist/esm"
6 | },
7 | "include": ["src"],
8 | "exclude": ["**/*.spec.ts", "**/__mocks__/**/*"]
9 | }
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "harmony-ecs",
3 | "author": "Eric McDaniel ",
4 | "description": "A small archetypal ECS focused on compatibility and performance",
5 | "version": "0.0.12",
6 | "license": "MIT",
7 | "type": "module",
8 | "types": "./lib/dist/esm/index.d.ts",
9 | "main": "./lib/dist/cjs/index.js",
10 | "exports": {
11 | "import": "./lib/dist/esm/index.js",
12 | "require": "./lib/dist/cjs/index.js"
13 | },
14 | "files": [
15 | "lib/dist"
16 | ],
17 | "engines": {
18 | "node": ">=14.18.1"
19 | },
20 | "scripts": {
21 | "perf": "npm run perf:node",
22 | "perf:node": "npm run build && node --loader ts-node/esm --experimental-specifier-resolution=node perf/src/index.ts",
23 | "perf:browser": "npm run build && cd ./perf && vite",
24 | "example:compat": " cd ./examples/compat && vite",
25 | "example:noise": " cd ./examples/noise && vite",
26 | "example:graph": " cd ./examples/graph && vite",
27 | "build": "tsc -b lib && npm run build:optimize && npm run build:cjs && tsc -b perf",
28 | "build:optimize": "node lib/build/optimize.js",
29 | "build:cjs": "esbuild --bundle --target=node12.22 --outdir=lib/dist/cjs --format=cjs lib/dist/esm/index.js",
30 | "build:docs": "typedoc lib/src/index.ts --excludeInternal --sort source-order",
31 | "prepare": "npm run build",
32 | "test": "jest --verbose"
33 | },
34 | "devDependencies": {
35 | "@babel/core": "^7.15.5",
36 | "@types/jest": "^27.0.1",
37 | "@types/three": "^0.131.0",
38 | "babel-plugin-add-import-extension": "^1.6.0",
39 | "cannon-es": "^0.18.0",
40 | "esbuild": "^0.12.25",
41 | "jest": "^27.1.1",
42 | "three": "^0.132.2",
43 | "ts-jest": "^27.0.5",
44 | "ts-node": "^10.2.1",
45 | "typedoc": "^0.22.7",
46 | "typescript": "^4.4.2",
47 | "vis-data": "^7.1.2",
48 | "vis-network": "^9.1.0",
49 | "vite": "^2.5.3"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/perf/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | harmony-ecs/perf
8 |
15 |
16 |
17 |
18 |
19 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/perf/src/index.ts:
--------------------------------------------------------------------------------
1 | // TODO(3mcd): add cli args for filtering
2 |
3 | import { printPerfResults, runPerf } from "./perf"
4 | import { suite } from "./suite"
5 |
6 | for (const [name, module] of Object.entries(suite)) {
7 | console.log(name)
8 | console.log(Array(name.length).fill("-").join(""))
9 | for (const [id, executor] of Object.entries(module)) {
10 | printPerfResults(runPerf(executor, id))
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/perf/src/perf.ts:
--------------------------------------------------------------------------------
1 | const performance = globalThis.performance
2 |
3 | type Perf = { run: () => void; once: boolean }
4 | type PerfStats = { duration: number }
5 | type PerfResults = {
6 | id: string
7 | iterations: number
8 | timing: { average: number; median: number; min: number; max: number }
9 | stats: PerfStats[]
10 | }
11 |
12 | export function makePerf(run: () => void, once = false): Perf {
13 | return {
14 | run,
15 | once,
16 | }
17 | }
18 |
19 | export function makePerfOnce(run: () => void): Perf {
20 | return makePerf(run, true)
21 | }
22 |
23 | function median(arr: T[], iteratee: (element: T) => number) {
24 | const mid = Math.floor(arr.length / 2)
25 | const num = arr.map(iteratee).sort((a, b) => a - b)
26 | return arr.length % 2 !== 0 ? num[mid]! : (num[mid - 1]! + num[mid]!) / 2
27 | }
28 |
29 | export function runPerf(perf: Perf, id: string, iterations = 100): PerfResults {
30 | const stats: PerfStats[] = []
31 |
32 | if (perf.once) {
33 | iterations = 1
34 | }
35 |
36 | for (let i = 0; i < iterations; i++) {
37 | const start = performance.now()
38 | perf.run()
39 | stats.push({ duration: performance.now() - start })
40 | }
41 |
42 | return {
43 | id,
44 | iterations,
45 | timing: {
46 | average: stats.reduce((a, x) => a + x.duration, 0) / stats.length,
47 | median: median(stats, stat => stat.duration),
48 | min: stats.reduce((a, x) => Math.min(a, x.duration), Infinity),
49 | max: stats.reduce((a, x) => Math.max(a, x.duration), 0),
50 | },
51 | stats,
52 | }
53 | }
54 |
55 | function prettyMs(x: number) {
56 | return `${parseFloat(x.toFixed(2)).toLocaleString()} ms`
57 | }
58 |
59 | function mapObject<$Object extends { [key: string]: unknown }, $Value>(
60 | object: $Object,
61 | iteratee: (value: $Object[keyof $Object], key: string) => $Value,
62 | ): { [K in keyof $Object]: $Value } {
63 | return Object.entries(object).reduce((a, [key, value]) => {
64 | a[key as keyof $Object] = iteratee(value as $Object[keyof $Object], key)
65 | return a
66 | }, {} as { [K in keyof $Object]: $Value })
67 | }
68 |
69 | export function printPerfResults(results: PerfResults) {
70 | const { id, timing, iterations } = results
71 | console.log(id)
72 | console.log(`iterations ${iterations}`)
73 | console.log(mapObject(timing, prettyMs))
74 | }
75 |
--------------------------------------------------------------------------------
/perf/src/suite/archetype_insert.perf.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Schema, World } from "../../../lib/src"
2 | import { Struct } from "../../../lib/src/archetype"
3 | import { makePerfOnce } from "../perf"
4 |
5 | function makeFixture() {
6 | const world = World.make(1_000_000)
7 |
8 | const A = Schema.makeBinary(world, {})
9 | const B = Schema.makeBinary(world, {})
10 | const C = Schema.makeBinary(world, {})
11 | const D = Schema.makeBinary(world, {})
12 | const E = Schema.makeBinary(world, {})
13 | const F = Schema.makeBinary(world, {})
14 |
15 | const types = [
16 | [A, B, C],
17 | [A],
18 | [C],
19 | [F],
20 | [A, B],
21 | [A, C],
22 | [B, C],
23 | [B, F],
24 | [A, C, E],
25 | [B, D, F],
26 | [E, F],
27 | [A, C, E, F],
28 | [B, C, D, F],
29 | [C, D, E, F],
30 | [B, C, D, E, F],
31 | [A, B, C, D, E, F],
32 | ]
33 | return { world, types }
34 | }
35 |
36 | const results: number[] = []
37 |
38 | let world: World.Struct
39 |
40 | for (let i = 0; i < 10; i++) {
41 | const fixture = makeFixture()
42 | world = fixture.world
43 | const start = performance.now()
44 | for (let i = fixture.types.length - 1; i >= 0; i--) {
45 | const count = Math.random() * 100
46 | for (let j = 0; j < count; j++) {
47 | Entity.make(world, fixture.types[i]!)
48 | }
49 | }
50 | const end = performance.now()
51 | results.push(end - start)
52 | }
53 |
54 | const avgTime = results.reduce((a, x) => a + x, 0) / 100
55 |
56 | function getUniqueArchetypes(archetype: Struct, visited = new Set()) {
57 | visited.add(archetype)
58 | archetype.edgesSet.forEach(a => getUniqueArchetypes(a, visited))
59 | return visited
60 | }
61 |
62 | console.log(
63 | `${
64 | getUniqueArchetypes(world!.rootArchetype).size
65 | } archetype insert took ${avgTime.toFixed(2)}ms`,
66 | )
67 |
68 | export const run = makePerfOnce(() => {})
69 |
--------------------------------------------------------------------------------
/perf/src/suite/index.ts:
--------------------------------------------------------------------------------
1 | import * as iterBinary from "./iter_binary.perf"
2 | import * as iterHybrid from "./iter_hybrid.perf"
3 | import * as iterNative from "./iter_hybrid.perf"
4 | import * as archetypeInsert from "./archetype_insert.perf"
5 |
6 | export const suite = { iterBinary, iterHybrid, iterNative, archetypeInsert }
7 |
--------------------------------------------------------------------------------
/perf/src/suite/iter_binary.perf.ts:
--------------------------------------------------------------------------------
1 | import { Schema, World, Query, Entity } from "../../../lib/src"
2 | import { makePerf, makePerfOnce } from "../perf"
3 | import { Vector3 } from "./types"
4 |
5 | const world = World.make(1_000_000)
6 | const Position = Schema.makeBinary(world, Vector3)
7 | const Velocity = Schema.makeBinary(world, Vector3)
8 | const Body = [Position, Velocity] as const
9 | const bodies = Query.make(world, Body)
10 |
11 | export const insert = makePerfOnce(() => {
12 | for (let i = 0; i < 1_000_000; i++) {
13 | Entity.make(world, Body, [
14 | { x: 1, y: 1, z: 1 },
15 | { x: 1, y: 1, z: 1 },
16 | ])
17 | }
18 | })
19 |
20 | export const iter = makePerf(() => {
21 | for (const [entities, [p, v]] of bodies) {
22 | for (let i = 0; i < entities.length; i++) {
23 | p.x[i] += v.x[i]!
24 | p.y[i] += v.y[i]!
25 | p.z[i] += v.z[i]!
26 | }
27 | }
28 | })
29 |
--------------------------------------------------------------------------------
/perf/src/suite/iter_hybrid.perf.ts:
--------------------------------------------------------------------------------
1 | import { Schema, Entity, Query, World } from "../../../lib/src"
2 | import { makePerf, makePerfOnce } from "../perf"
3 | import { Vector3 } from "./types"
4 |
5 | const world = World.make(1_000_000)
6 | const Position = Schema.makeBinary(world, Vector3)
7 | const Velocity = Schema.make(world, Vector3)
8 | const Body = [Position, Velocity] as const
9 | const bodies = Query.make(world, Body)
10 |
11 | export const insert = makePerfOnce(() => {
12 | for (let i = 0; i < 1_000_000; i++) {
13 | Entity.make(world, Body, [
14 | { x: 1, y: 1, z: 1 },
15 | { x: 1, y: 1, z: 1 },
16 | ])
17 | }
18 | })
19 |
20 | export const iter = makePerf(() => {
21 | for (const [entities, [p, v]] of bodies) {
22 | for (let i = 0; i < entities.length; i++) {
23 | p.x[i] += v[i]!.x
24 | p.y[i] += v[i]!.y
25 | p.z[i] += v[i]!.z
26 | }
27 | }
28 | })
29 |
30 | export const iterUpdateNative = makePerf(() => {
31 | for (const [entities, [p, v]] of bodies) {
32 | for (let i = 0; i < entities.length; i++) {
33 | v[i]!.x += p.x[i]!
34 | v[i]!.y += p.y[i]!
35 | v[i]!.z += p.z[i]!
36 | }
37 | }
38 | })
39 |
--------------------------------------------------------------------------------
/perf/src/suite/iter_native.perf.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Query, Schema, World } from "../../../lib/src"
2 | import { makePerf, makePerfOnce } from "../perf"
3 | import { Vector3 } from "./types"
4 |
5 | const world = World.make(1_000_000)
6 | const Position = Schema.make(world, Vector3)
7 | const Velocity = Schema.make(world, Vector3)
8 | const Body = [Position, Velocity] as const
9 | const bodies = Query.make(world, Body)
10 |
11 | export const insert = makePerfOnce(() => {
12 | for (let i = 0; i < 1_000_000; i++) {
13 | Entity.make(world, Body, [
14 | { x: 1, y: 1, z: 1 },
15 | { x: 1, y: 1, z: 1 },
16 | ])
17 | }
18 | })
19 |
20 | export const iter = makePerf(() => {
21 | for (const [entities, [p, v]] of bodies) {
22 | for (let i = 0; i < entities.length; i++) {
23 | p[i]!.x += v[i]!.x
24 | p[i]!.y += v[i]!.y
25 | p[i]!.z += v[i]!.z
26 | }
27 | }
28 | })
29 |
--------------------------------------------------------------------------------
/perf/src/suite/types.ts:
--------------------------------------------------------------------------------
1 | import { Format } from "../../../lib/src"
2 |
3 | export const Vector3 = { x: Format.float64, y: Format.float64, z: Format.float64 }
4 |
--------------------------------------------------------------------------------
/perf/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "noEmit": true
6 | },
7 | "references": [{ "path": "../lib" }]
8 | }
9 |
--------------------------------------------------------------------------------
/perf/vite.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | root: ".",
3 | }
4 |
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: "avoid",
3 | bracketSpacing: true,
4 | insertPragma: false,
5 | jsxBracketSameLine: false,
6 | printWidth: 90,
7 | proseWrap: "preserve",
8 | requirePragma: false,
9 | semi: false,
10 | singleQuote: false,
11 | tabWidth: 2,
12 | trailingComma: "all",
13 | useTabs: false,
14 | }
15 |
--------------------------------------------------------------------------------
/test/add_remove.spec.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Format, Query, Schema, World } from "../lib/src"
2 |
3 | describe("add_remove", () => {
4 | it("moves entities between archetypes", () => {
5 | const ENTITY_COUNT = 2
6 | const world = World.make(ENTITY_COUNT)
7 | const A = Schema.makeBinary(world, Format.float64)
8 | const B = Schema.makeBinary(world, Format.float64)
9 | const qa = Query.make(world, [A], Query.not([B]))
10 | const qab = Query.make(world, [A, B])
11 |
12 | for (let i = 0; i < ENTITY_COUNT; i++) {
13 | Entity.make(world, [A])
14 | }
15 |
16 | for (let i = 0; i < qa.length; i++) {
17 | const [e] = qa[i]!
18 | for (let j = e.length - 1; j >= 0; j--) {
19 | Entity.set(world, e[j]!, [B])
20 | }
21 | }
22 |
23 | let qaCountPreUnset = 0
24 | for (let i = 0; i < qa.length; i++) {
25 | qaCountPreUnset += qa[i]![0].length
26 | }
27 |
28 | let qabCountPreUnset = 0
29 | for (let i = 0; i < qab.length; i++) {
30 | qabCountPreUnset += qab[i]![0].length
31 | }
32 |
33 | for (let i = 0; i < qab.length; i++) {
34 | const [e] = qab[i]!
35 | for (let j = e.length - 1; j >= 0; j--) {
36 | Entity.unset(world, e[j]!, [B])
37 | }
38 | }
39 |
40 | let qaCountPostUnset = 0
41 | for (let i = 0; i < qa.length; i++) {
42 | qaCountPostUnset += qa[i]![0].length
43 | }
44 |
45 | let qabCountPostUnset = 0
46 | for (let i = 0; i < qab.length; i++) {
47 | qabCountPostUnset += qab[i]![0].length
48 | }
49 |
50 | for (let i = 0; i < qa.length; i++) {
51 | const [e] = qa[i]!
52 | for (let j = e.length - 1; j >= 0; j--) {
53 | Entity.set(world, e[j]!, [B])
54 | }
55 | }
56 |
57 | // console.log(world.rootArchetype.edgesSet[A]!.edgesSet[B])
58 |
59 | let qaCountPostReset = 0
60 | for (let i = 0; i < qa.length; i++) {
61 | qaCountPostReset += qa[i]![0].length
62 | }
63 |
64 | let qabCountPostReset = 0
65 | for (let i = 0; i < qa.length; i++) {
66 | qabCountPostReset += qab[i]![0].length
67 | }
68 |
69 | expect(qaCountPreUnset).toBe(0)
70 | expect(qabCountPreUnset).toBe(ENTITY_COUNT)
71 | expect(qaCountPostUnset).toBe(ENTITY_COUNT)
72 | expect(qabCountPostUnset).toBe(0)
73 | expect(qaCountPostReset).toBe(0)
74 | expect(qabCountPostReset).toBe(ENTITY_COUNT)
75 | })
76 | })
77 |
--------------------------------------------------------------------------------
/test/query_counts.spec.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Query, Schema, World } from "../lib/src"
2 | import { isEqual, isSupersetOf } from "../lib/src/type"
3 |
4 | describe("query counts", () => {
5 | const ENTITY_COUNT = 1_500
6 | const world = World.make(ENTITY_COUNT)
7 |
8 | const A = Schema.makeBinary(world, {})
9 | const B = Schema.makeBinary(world, {})
10 | const C = Schema.makeBinary(world, {})
11 | const D = Schema.makeBinary(world, {})
12 | const E = Schema.makeBinary(world, {})
13 | const F = Schema.makeBinary(world, {})
14 |
15 | const types = [
16 | [A],
17 | [C],
18 | [F],
19 | [A, B],
20 | [A, C],
21 | [B, C],
22 | [B, F],
23 | [A, C, E],
24 | [B, D, F],
25 | [E, F],
26 | [A, C, E, F],
27 | [B, C, D, F],
28 | [C, D, E, F],
29 | [B, C, D, E, F],
30 | [A, B, C, D, E, F],
31 | ]
32 |
33 | const queries = types.map(type => Query.make(world, type))
34 | const entitiesPerType = ENTITY_COUNT / types.length
35 |
36 | types.forEach(type => {
37 | for (let i = 0; i < entitiesPerType; i++) Entity.make(world, type)
38 | })
39 |
40 | const expectedEntityCounts = types.map(type =>
41 | types.reduce(
42 | (a, t) => a + (isEqual(t, type) || isSupersetOf(t, type) ? entitiesPerType : 0),
43 | 0,
44 | ),
45 | )
46 |
47 | it("yields correct entity counts", () => {
48 | queries.forEach((query, i) => {
49 | let count = 0
50 | for (let i = 0; i < query.length; i++) {
51 | const [entities] = query[i]!
52 | count += entities.length
53 | }
54 | expect(count).toBe(expectedEntityCounts[i])
55 | })
56 | })
57 | })
58 |
--------------------------------------------------------------------------------
/test/query_dynamic.spec.ts:
--------------------------------------------------------------------------------
1 | import { Format, Schema, Entity, Query, World } from "../lib/src"
2 |
3 | describe("query_dynamic", () => {
4 | it("updates dynamic queries with newly created archetypes", () => {
5 | const world = World.make(4)
6 | const A = Schema.makeBinary(world, Format.float64)
7 | const B = Schema.makeBinary(world, Format.float64)
8 | const C = Schema.makeBinary(world, Format.float64)
9 | const D = Schema.makeBinary(world, Format.float64)
10 | const E = Schema.makeBinary(world, Format.float64)
11 | const qab = Query.make(world, [A, B])
12 | const qcd = Query.make(world, [C, D])
13 | const qce = Query.make(world, [C, E])
14 |
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 | expect(qab).toHaveLength(4)
21 | expect(qcd).toHaveLength(1)
22 | expect(qce).toHaveLength(1)
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "declaration": true,
5 | "declarationMap": true,
6 | "downlevelIteration": true,
7 | "esModuleInterop": true,
8 | "module": "esnext",
9 | "moduleResolution": "node",
10 | "noImplicitReturns": true,
11 | "noPropertyAccessFromIndexSignature": true,
12 | "noUncheckedIndexedAccess": true,
13 | "skipLibCheck": true,
14 | "sourceMap": true,
15 | "strict": true,
16 | "target": "esnext"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
Create .stack property on a target object
3 |