Getting started
A 10-minute tour of the Meep engine, from install to first running simulation.
Meep is a pure-ECS, zero-allocation JavaScript game engine. It’s designed for engineers who want to own the architecture of their game and aren’t afraid of writing code instead of clicking through an editor.
What you’ll need
- Node.js 24 or newer.
- A bundler that supports ES modules and
@rollup/plugin-strip. Vite, Rollup, esbuild, and Webpack 5 all work. - A valid Meep license. The Free tier covers evaluation and most hobby projects.
Install
npm install @woosh/meep-engine
The package ships with the full engine source under src/ and a prebuilt module bundle under build/. There are no binary blobs.
The two core pieces
Meep splits the ECS into two cooperating objects:
EntityComponentDatasetowns the entities and components — the data.EntityManagerowns the systems and orchestrates the simulation loop — the logic.
You usually create one of each, register your systems on the EntityManager, attach the dataset, and then call simulate(dt) once per frame.
Spawning entities (high-level API)
The fluent way to build an entity is through the Entity class. It’s a thin wrapper that lets you compose an entity in one expression and then commit it to a dataset.
First, a component to attach. Components are just classes — tiny, focused data containers. Transform is built in; here’s a Velocity of your own:
class Velocity {
velocity = new Vector3(0, 0, 0);
}
Now compose an entity from a Transform and a Velocity:
const ecd = new EntityComponentDataset();
const t = new Transform();
t.position.set(0, 0, 0);
const v = new Velocity();
v.velocity.set(1, 0, 0); // moving along +X at 1 unit/sec
const id = new Entity()
.add(t)
.add(v)
.build(ecd);
Entity is the high-level builder. It hides the verbosity of the low-level dataset.createEntity() + addComponentToEntity() calls and gives you a chainable API.
You can also construct a Transform from JSON:
const t = Transform.fromJSON({ position: { x: 0, y: 0, z: 0 } });
Transform carries position, rotation, and scale; the Velocity above carries a single velocity vector. Components hold no logic of their own — that lives in systems.
Iterating entities
To do something with every entity matching a component shape, use traverseEntities:
ecd.traverseEntities(
[Transform, Velocity],
(transform, vel, entity) => {
transform.position.addScaled(vel.velocity, dt);
}
);
The callback receives one argument per requested component, plus the entity id. Internally the dataset walks a tight typed-array index — querying for a component shape with a million matches has the same per-iteration cost as querying for a hundred.
Wiring up a system
Most of the time you don’t call traverseEntities directly from your game loop — you put the logic in a System and let the EntityManager schedule it for you.
class MovementSystem extends System {
dependencies = [Transform, Velocity];
update(timeDelta) {
this.entityManager.dataset.traverseEntities(
[Transform, Velocity],
(transform, vel) => {
transform.position.addScaled(vel.velocity, timeDelta);
}
);
}
}
const em = new EntityManager();
em.addSystem(new MovementSystem());
em.attachDataset(ecd);
em.startup();
// Per-frame:
em.simulate(0.016); // advance the simulation by 16ms
The dependencies array tells the engine which components the system cares about, which lets it compute an efficient execution order. The base System class also provides a fixedUpdate(dt) hook for physics-stable stepping, and link(...)/unlink(...) hooks for per-entity setup and teardown.
MovementSystem is deliberately the smallest system that does something — a hand-rolled integrator to show the shape. You won’t write your own for real physics: Meep ships a built-in PhysicsSystem that simulates RigidBody and Collider components (gravity, contacts, joints, raycasts) for you. See the physics docs for that path.
A live example
Everything above, running right here on the page — a single cube spawned with
the high-level Entity builder and drawn by ShadedGeometrySystem. Drag to
orbit, scroll to zoom. This is a real meep build embedded in the docs, not a
video. (EngineHarness is the only extra: it spins up the canvas, camera, and a
sun + ambient light so there’s something to look at.)
Source
import { EngineHarness } from "@woosh/meep-engine/src/engine/EngineHarness.js";
import Entity from "@woosh/meep-engine/src/engine/ecs/Entity.js";
import { Transform } from "@woosh/meep-engine/src/engine/ecs/transform/Transform.js";
import { ShadedGeometry } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js";
import { ShadedGeometrySystem } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js";
import Vector3 from "@woosh/meep-engine/src/core/geom/Vector3.js";
// EngineHarness.bootstrap creates the EntityManager + dataset, attaches a
// canvas, and gives us a hook to register systems. ShadedGeometrySystem is what
// actually draws any entity carrying a ShadedGeometry component.
const engine = await EngineHarness.bootstrap({
configuration: (config, engine) => {
config.addSystem(new ShadedGeometrySystem(engine));
},
});
// buildBasics frames the camera, drops in a sun + ambient light, and wires the
// orbital drag-to-look / WASD-to-pan / wheel-to-zoom controls.
await EngineHarness.buildBasics({
engine,
enableTerrain: false,
enableWater: false,
enableLights: true,
enableShadows: false,
focus: new Vector3(0, 0, 0),
distance: 4,
pitch: 0.6,
yaw: 0.4,
showFps: false,
});
const ecd = engine.entityManager.dataset;
// The shape and material come from Three.js; ShadedGeometry.from wraps them as a
// Meep component so the cube participates in batching and frustum culling.
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshStandardMaterial({ color: 0x4ef0a8 });
const cubeMesh = ShadedGeometry.from(cubeGeometry, cubeMaterial);
const t = new Transform();
t.position.set(0, 0, 0);
new Entity()
.add(t)
.add(cubeMesh)
.build(ecd);Production builds
In development, the engine runs with thousands of assertions that catch errors at the source. For production, strip them with @rollup/plugin-strip:
// vite.config.js
import { defineConfig } from "vite";
import strip from "@rollup/plugin-strip";
export default defineConfig({
plugins: [
{ ...strip({ functions: ["assert.*"] }), apply: "build" },
],
});
Stripped builds run at native speed and tree-shake down to only the modules you actually import.
Next up
- Installation guide — bundler-specific configuration.
- ECS overview — the architecture and design philosophy.
- FAQ — answers to the most common questions.
For licensing — tiers, pricing, contract language — see /pricing and /license.
Heads up — this documentation is being built out as we go. Subsystems with thinner coverage are marked
draftin the sidebar. Email us if there’s a specific area you need expanded sooner.