Anti-Patterns

Common mistakes that cause bugs, poor performance, or confusion in nape-js. Each section shows the wrong approach, explains why it's a problem, and gives the correct alternative.


Memory & Performance

Creating Vec2 every frame

Allocating new Vec2() inside the game loop creates garbage every frame, triggering GC pauses that cause visible stuttering.

// BAD — 60 allocations/sec, GC pressure
function update() {
  body.applyImpulse(new Vec2(10, 0));
}

// GOOD — reuse a pre-allocated vector
const impulse = new Vec2(10, 0);
function update() {
  body.applyImpulse(impulse);
}

// ALSO GOOD — use the object pool
function update() {
  const v = Vec2.get(10, 0);
  body.applyImpulse(v);
  v.dispose(); // returns to pool
}

Using Vec2.weak() and keeping a reference

Weak vectors are automatically disposed after their first use as a method argument. Holding a reference to them leads to "disposed Vec2" errors.

// BAD — weak is auto-disposed after applyImpulse reads it
const v = Vec2.weak(10, 0);
body.applyImpulse(v);
console.log(v.x); // ERROR: disposed

// GOOD — use Vec2.get() when you need the value after
const v = Vec2.get(10, 0);
body.applyImpulse(v);
console.log(v.x); // OK
v.dispose();

Iterating space.bodies to find one body

Looping through all bodies every frame to find a specific one is O(n).

// BAD — linear search every frame
function update() {
  for (const body of space.bodies) {
    if (body.userData.id === "player") {
      // do something
    }
  }
}

// GOOD — keep a direct reference
const player = new Body(BodyType.DYNAMIC);
// ... later:
function update() {
  player.position; // direct access, O(1)
}

Body & Shape Setup

Using DYNAMIC bodies for character movement

Force/impulse-based character movement on dynamic bodies is inherently imprecise — the character slides on slopes, bounces off walls, and can't do pixel-perfect movement.

// BAD — sloppy, slides, hard to control
function update() {
  player.applyImpulse(new Vec2(moveX * 100, 0));
  if (jump) player.applyImpulse(new Vec2(0, -5000));
}

// GOOD — geometric controller for precise platformer movement
const cc = new CharacterController(space, player, {
  maxSlopeAngle: Math.PI / 4,
});
cc.setVelocity(moveX * 200, player.velocity.y);

Setting body.space on compound members

Bodies inside a Compound share the compound's space assignment. Setting .space on a member throws an error.

// BAD — throws
const child = compound.bodies.at(0);
child.space = space;

// GOOD — assign space on the root compound
compound.space = space;

Setting position directly on kinematic bodies

Setting position directly on a kinematic body teleports it — the physics solver doesn't know it moved, so it won't push dynamic bodies out of the way.

// BAD — teleports, dynamic bodies clip through
platform.position.x = targetX;

// GOOD — set velocity so the solver handles contacts
platform.velocity.x = (targetX - platform.position.x) / dt;

Forgetting body.allowRotation = false on characters

Characters without rotation locking will tumble and spin when they hit walls or land on slopes.

// BAD — character rotates randomly
const player = new Body(BodyType.DYNAMIC);
player.shapes.add(new Circle(14));

// GOOD — lock rotation for character controllers
const player = new Body(BodyType.DYNAMIC);
player.shapes.add(new Circle(14));
player.allowRotation = false;

Constraints

Using CbType.ANY_CONSTRAINT for BREAK events

This is a known gotcha — ANY_CONSTRAINT only works for generic constraint queries, not for BREAK or SLEEP event listeners.

// BAD — listener never fires
space.listeners.add(
  new ConstraintListener(CbEvent.BREAK, CbType.ANY_CONSTRAINT, handler),
);

// GOOD — create a dedicated CbType
const breakableTag = new CbType();
joint.cbTypes.add(breakableTag);
space.listeners.add(
  new ConstraintListener(CbEvent.BREAK, breakableTag, handler),
);

Making every constraint stiff

Stiff constraints (the default) create rigid connections. For natural-looking physics (ragdolls, ropes, vehicles), soft constraints are almost always better.

// BAD — rigid, robotic ragdoll joints
const neck = new AngleJoint(torso, head, -0.4, 0.4);
neck.space = space;

// GOOD — soft joint with natural give
const neck = new AngleJoint(torso, head, -0.4, 0.4);
neck.stiff = false;
neck.frequency = 8;
neck.damping = 0.6;
neck.space = space;

Collision & Filtering

Enabling CCD on every body

CCD (continuous collision detection) adds significant CPU cost. Only use it on bodies that actually move fast enough to tunnel.

// BAD — unnecessary CCD on slow objects
for (const body of space.bodies) {
  body.isBullet = true;
}

// GOOD — only on fast-moving bodies
bullet.isBullet = true;
// Leave slow boxes, platforms, etc. with isBullet = false (default)

Not calling space.step() before raycasting

The broadphase only registers shapes after at least one simulation step. Raycasting against static bodies before any step returns null.

// BAD — raycast on freshly created space
const space = new Space(new Vec2(0, 600));
floor.space = space;
space.rayCast(ray); // null — broadphase hasn't indexed yet

// GOOD — step first
space.step(1 / 60);
space.rayCast(ray); // works

Forgetting that ParticleEmitter particles are real bodies

Every particle is a full Body with shape, mass, and collisions, so it shows up in every filter check — including the CharacterController ground/wall raycasts. With the default auto-generated CC filter, the player can stand on their own bullets/sparks/debris and "fly" by spamming fire.

// BAD — bullets emitted from the player's centre count as ground.
//   Default CC filter only excludes the character itself; particles still
//   intersect the downward ground-detection ray, so cc.grounded stays true
//   above a floating bullet and Space-spam → infinite jumps.
new ParticleEmitter({ origin: player, /* ... */ });
new CharacterController(space, player, { /* no `filter` */ });

// GOOD — pick a dedicated bit for particles, mask it out of the CC filter.
const PARTICLE_GROUP = 1 << 10;
const CHAR_GROUP     = 1 << 8;

new ParticleEmitter({
  origin: player,
  particleFilter: new InteractionFilter(PARTICLE_GROUP, ~(CHAR_GROUP | PARTICLE_GROUP)),
  /* ... */
});

new CharacterController(space, player, {
  filter: new InteractionFilter(1, ~(CHAR_GROUP | PARTICLE_GROUP)), // skip self + particles
  /* ... */
});

The same fix prevents bullets from deflecting off floating debris/spark clouds left by previous shots — give every emitter the same PARTICLE_GROUP and have projectiles mask it out of their own mask. See the Particle Emitter cookbook recipe for a complete setup.


Serialization

Assuming binary preserves userData

Binary serialization (spaceToBinary) is compact but does not preserve userData on bodies. Use JSON if you need custom data.

// BAD — userData lost
body.userData.type = "player";
const bin = spaceToBinary(space);
const restored = spaceFromBinary(bin);
restored.bodies.at(0).userData.type; // undefined!

// GOOD — use JSON for userData preservation
const json = spaceToJSON(space);
const restored = spaceFromJSON(json);
restored.bodies.at(0).userData.type; // "player"

Serializing every frame in multiplayer

Full space serialization is expensive. For real-time multiplayer, send only position/velocity deltas or use binary snapshots at a low rate (e.g., 10 Hz) with client-side interpolation.


Simulation

Using huge gravity values

Gravity is in pixels/s² (not meters/s²). Earth gravity in a 600px-tall world is around 600, not 9.8 or 9800.

// BAD — objects move at light speed
const space = new Space(new Vec2(0, 9800));

// GOOD — typical for a 600px viewport
const space = new Space(new Vec2(0, 600));

Spawning overlapping bodies

Bodies created inside each other generate extreme separation forces, causing them to "explode" apart on the first step.

// BAD — instant physics explosion
for (let i = 0; i < 10; i++) {
  const b = new Body(BodyType.DYNAMIC, new Vec2(400, 300)); // all same position!
  b.shapes.add(new Circle(20));
  b.space = space;
}

// GOOD — space them out
for (let i = 0; i < 10; i++) {
  const b = new Body(BodyType.DYNAMIC, new Vec2(400, 100 + i * 45));
  b.shapes.add(new Circle(20));
  b.space = space;
}

Not disposing removed bodies

Bodies removed from the space should be properly cleaned up to avoid memory leaks in long-running games.

// BAD — body lingers in memory
body.space = null;

// GOOD — also clear references
body.space = null;
body.shapes.clear();
// Drop your reference to the body so GC can collect it