Thyseus is a multi-threadable, DX-focused, and highly performant archetypal Entity Component System (ECS) written in Typescript. It provides a simple, expressive, and type-driven API, and includes many features out of the box, including:
- Hassle-free multithreading. Don't worry about scheduling, Mutexes, or workers - just write your systems and let Thyseus take care of the rest.
- A safety-first approach! No
eval
,new Function()
, or creating workers from blobs - Thyseus leverages recent additions to the language and a little bit of ✨ magic ✨ to do what it needs to, and will never use unsafe code. - Archetypal storage for lean memory use and cache-friendly iteration.
- Dynamically sized types with
string
s. - Complex queries with
Optional
,With
,Without
,And
, andOr
filters.
Check out the documentation here!
Please note: Thyseus is in early development and is not yet feature-complete or nearly as performant as it could be. Pre-1.0.0 releases may have frequent breaking changes.
# pnpm
pnpm add thyseus
# yarn
yarn add thyseus
# npm
npm i thyseus
If you're interested in contributing, please have a look at the code of conduct and the contributing guide first.
To get started, define a component:
import { struct, initStruct } from 'thyseus';
@struct
class Vec2 {
@struct.f64 declare x: number;
@struct.f64 declare y: number;
constructor(x = 0, y = 0) {
initStruct(this);
this.x = x;
this.y = y;
}
add(other: Vec2) {
this.x += other.x;
this.y += other.y;
}
addScaled(other: Vec2, scalar: number) {
this.x += other.x * scalar;
this.y += other.y * scalar;
}
}
class Position extends Vec2 {}
class Velocity extends Vec2 {}
Let's add a resource to track the time:
import { struct } from 'thyseus';
@struct
class Time {
@struct.f64 declare current: number;
@struct.f64 declare previous: number;
@struct.f64 declare delta: number;
}
And then a couple systems:
import { defineSystem } from 'thyseus';
import { Time, Position, Velocity } from './someModule';
const updateTime = defineSystem(
({ Res, Mut }) => [Res(Mut(Time))],
function updateTimeSystem(time) {
time.previous = time.current;
time.current = Date.now();
time.delta = (time.current - time.previous) / 1000;
}
);
const mover = defineSystem(
({ Query, Mut, Res }) => [Query([Mut(Position), Velocity]), Res(Time)],
function moverSystem(query, time) {
for (const [pos, vel] of query) {
pos.addScaled(vel, time.delta);
}
},
);
Sweet! Now let's make a world with these systems and get it started.
import { World } from 'thyseus';
// Note that the .build() method returns a promise.
// Top-level await is a convenient way to handle this,
// but it's not a requirement.
export const myWorld = await World.new()
.addSystem(updateTime)
.addSystem(mover)
.build();
And then run it!
import { myWorld } from './someOtherModule';
async function loop() {
await myWorld.update(); // This also returns a promise!
requestAnimationFrame(loop);
}
loop();
If you'd like to run your systems on multiple threads:
// Will spawn one worker thread (default threads count is 1 - no worker threads)
export const myWorld = await World.new({ threads: 2 }, import.meta.url)
.addSystem(...)
...
A full explanation of the few caveats for multithreading will be provided when
documentation is completed. Multithreading relies on
SharedArrayBuffer
and
module workers
(not yet implemented in Firefox).