Getting started

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.

This guide gets you from npm install to a running simulation in about ten minutes.

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 hobby and small commercial use; pick a paid tier once you cross the revenue cap.

Install

npm install @woosh/meep-engine

The package ships with the full engine source under src/, a prebuilt module bundle under build/, and the editor scaffolding under editor/. There are no binary blobs.

The two core pieces

Meep splits the ECS into two cooperating objects:

  • EntityComponentDataset owns the entities and components — the data.
  • EntityManager owns 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:

const ecd = new EntityComponentDataset();

const t = new Transform();
t.position.set(0, 0, 0);

const m = new Motion();
m.velocity.set(1, 0, 0); // moving along +X at 1 unit/sec

const id = new Entity()
  .add(t)
  .add(m)
  .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 } });

Components are tiny, focused data containers. Transform carries position, rotation, and scale. Motion carries a velocity vector. They don’t contain logic — that lives in systems.

Iterating entities

To do something with every entity matching a component shape, use traverseEntities:

ecd.traverseEntities(
  [Transform, Motion],
  (transform, motion, entity) => {
    transform.position.x += motion.velocity.x * dt;
    transform.position.y += motion.velocity.y * dt;
    transform.position.z += motion.velocity.z * 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, Motion];

  update(timeDelta) {
    this.entityManager.dataset.traverseEntities(
      [Transform, Motion],
      (transform, motion) => {
        transform.position.x += motion.velocity.x * timeDelta;
        transform.position.y += motion.velocity.y * timeDelta;
        transform.position.z += motion.velocity.z * 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.

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

Heads up — this documentation is being built out as we go. Subsystems with thinner coverage are marked draft in the sidebar. Email me if there’s a specific area you need expanded sooner.