Addons

Physics

Integrate real-time physics in Viber3D using Jolt (or another engine) with ECS traits and systems.

Physics in Viber3D

Viber3D uses an ECS approach for organizing game data. You can integrate a physics engine—like Jolt—by defining traits for physics objects and systems that step the simulation each frame.

This guide covers:

  1. Overview & Key Concepts
  2. Initializing a Physics World
  3. Creating Physics Bodies
  4. Stepping & Syncing Transforms
  5. Collision Handling
  6. Motion Types & Continuous Collision
  7. Common Patterns & Tips
  8. Summary & Next Steps

1. Overview & Key Concepts

  1. PhysicsWorld Trait
    A specialized trait storing the engine’s “world” or “physics interface,” so it’s accessible anywhere in ECS.
  2. RigidBody / PhysicsBody Traits
    Each entity that should have physics (e.g. a crate, sphere, or player) can hold a trait referencing its underlying physics body in Jolt.
  3. Systems
    • A “build bodies” system to create bodies for newly spawned ECS entities.
    • A “step physics” system to advance the physics simulation each frame.
    • A “sync transforms” system to copy the updated positions/rotations back to Three.js objects (or to ECS Position / Rotation traits).

Why Jolt?

  • Speed: Written in C++ and compiled to WebAssembly.
  • Feature-rich: Rigid bodies, collisions, advanced shape definitions, etc.
  • Optional: You can swap in a different library (e.g., Rapier) by following similar patterns.

2. Initializing a Physics World

Goal: Load the Jolt library once, create a Jolt “world,” and store it in a custom ECS trait.

// traits/jolt-world.ts
import { trait } from 'koota'
import type Jolt from 'jolt-physics'

export const JoltWorld = trait(() => new JoltWorldImpl())

export class JoltWorldImpl {
  static JOLT_NATIVE: typeof Jolt
  joltInterface: Jolt.JoltInterface = null!
  bodyInterface: Jolt.BodyInterface = null!
  physicsSystem: Jolt.PhysicsSystem = null!
  initialized = false

  constructor() {
    if (!JoltWorldImpl.JOLT_NATIVE) {
      throw new Error("Jolt not loaded yet.")
    }
    // Create the JoltInterface with default settings
    const J = JoltWorldImpl.JOLT_NATIVE
    const settings = new J.JoltSettings()
    this._setupCollisionFiltering(settings)
    
    const jolt = new J.JoltInterface(settings)
    J.destroy(settings)

    this.physicsSystem = jolt.GetPhysicsSystem()
    this.bodyInterface = this.physicsSystem.GetBodyInterface()
    this.joltInterface = jolt

    this.initialized = true
  }

  stepPhysics(dt: number) {
    // E.g., do multiple substeps if dt is large
    const steps = dt > 1/55 ? 2 : 1
    this.joltInterface.Step(dt, steps)
  }

  _setupCollisionFiltering(settings: Jolt.JoltSettings) {
    // your collision layers, etc.
  }
}

Initialize the library at startup:

// actions/use-jolt-actions.ts
import { createActions } from 'koota/react'
import { JoltWorld, JoltWorldImpl } from '../traits/jolt-world'

export const useJoltActions = createActions((world) => ({
  async initWorld() {
    if (world.has(JoltWorld)) {
      // already initialized
      return
    }

    // Load the Jolt WASM
    const joltInit = await import('jolt-physics')
    // Store the reference globally
    JoltWorldImpl.JOLT_NATIVE = await joltInit.default()

    // Add the JoltWorld trait to ECS
    world.add(JoltWorld())
    console.log("Jolt world created.")
  }
}))

You can call initWorld() from a React component, or anywhere else in your startup code.


3. Creating Physics Bodies

a) “NeedsJoltBody” Trait

Define a trait that marks an entity as needing a physics body. This can include settings: whether the body is dynamic, static, or kinematic, etc.

// traits/needs-jolt-body.ts
import { trait } from 'koota'

export const NeedsJoltBody = trait({
  layer: "moving" as "moving" | "non_moving",
  motionType: "dynamic" as "dynamic" | "static" | "kinematic",
  buildConvexShape: false,
  continuousCollisionMode: false,
})

You might also define a trait JoltBody referencing the final Jolt body object:

// traits/jolt-body.ts
import { trait } from 'koota'
import type Jolt from 'jolt-physics'

export const JoltBody = trait<null | Jolt.Body>(null)

b) System to Build Bodies

When new entities have NeedsJoltBody + a mesh or geometry trait (like MeshRef), we create an actual Jolt Body.

// systems/build-jolt-bodies.ts
import { World } from 'koota'
import { NeedsJoltBody } from '../traits/needs-jolt-body'
import { JoltBody } from '../traits/jolt-body'
import { JoltWorld, JoltWorldImpl } from '../traits/jolt-world'
import { MeshRef } from '../traits/mesh-ref'
import { inferInitTransformsFromMesh, createShapeFromGeometry } from '../misc/jolt-helper'

export function BuildJoltBodies({ world }: { world: World }) {
  if (!world.has(JoltWorld)) return

  const joltWorld = world.get(JoltWorld)
  const J = JoltWorldImpl.JOLT_NATIVE

  world.query(NeedsJoltBody, MeshRef).updateEach(([needs, meshRef], entity) => {
    const { pos, rot } = inferInitTransformsFromMesh(meshRef.ref)
    const shape = needs.buildConvexShape
      ? createShapeFromGeometry(meshRef.ref.geometry)
      : /* other shape logic, e.g. BoxShape */ new J.BoxShape(1,1,1)

    // create BodyCreationSettings
    const layer = needs.layer === 'moving' ? 1 : 0
    const motionType = {
      dynamic: J.EMotionType_Dynamic,
      static: J.EMotionType_Static,
      kinematic: J.EMotionType_Kinematic
    }[needs.motionType]

    const creation = new J.BodyCreationSettings(shape, pos, rot, motionType, layer)
    const body = joltWorld.bodyInterface.CreateBody(creation)
    J.destroy(creation)

    // add the body to jolt
    joltWorld.bodyInterface.AddBody(body.GetID(), J.EActivation_Activate)

    // handle continuous collision
    if (needs.continuousCollisionMode) {
      joltWorld.bodyInterface.SetMotionQuality(body.GetID(), J.EMotionQuality_LinearCast)
    }

    // remove the NeedsJoltBody trait (we built the body)
    entity.remove(NeedsJoltBody)
    // add the JoltBody trait referencing the new body
    entity.add(JoltBody(body))
  })
}

This approach ensures that each newly spawned entity gets a Jolt body exactly once.


4. Stepping & Syncing Transforms

a) Update the Physics World

A system that calls stepPhysics each frame:

// systems/update-jolt.ts
import { World } from 'koota'
import { JoltWorld } from '../traits/jolt-world'

export function UpdateJolt({ world, delta }: { world: World; delta: number }) {
  if (!world.has(JoltWorld)) return
  const joltWorld = world.get(JoltWorld)
  joltWorld.stepPhysics(delta)
}

b) Sync to ECS or Three.js

You can store transforms in ECS Position / Rotation traits, or directly sync to a MeshRef. For example:

// systems/sync-three-to-jolt.ts
import { World } from 'koota'
import { JoltBody } from '../traits/jolt-body'
import { MeshRef } from '../traits/mesh-ref'
import { JoltWorld } from '../traits/jolt-world'

export function SyncThreeToJolt({ world }: { world: World }) {
  if (!world.has(JoltWorld)) return

  world.query(JoltBody, MeshRef).useStores(([rb, meshRef], entities) => {
    for (const e of entities) {
      const body = rb[e.id()]
      const mesh = meshRef[e.id()]
      if (!body || !mesh) continue

      // read Jolt transform
      const pos = body.GetPosition()
      const rot = body.GetRotation()

      // apply to Three.js mesh
      mesh.position.set(pos.GetX(), pos.GetY(), pos.GetZ())
      mesh.quaternion.set(rot.GetX(), rot.GetY(), rot.GetZ(), rot.GetW())
    }
  })
}

Note: useStores bypasses some overhead by accessing data in a more raw manner—useful for performance-critical loops.


5. Collision Handling

a) Basic Approach

Jolt can track collisions or contact points. Depending on your version, you might:

  • Read a contact list or events each step (GetCollisionsThisFrame(), or similar).
  • Subscribe to collision callbacks via Jolt’s event system.

Once you have collisions, you can do ECS logic:

function handleCollisions(world: World) {
  const joltWorld = world.get(JoltWorld)
  // some hypothetical API
  const collisions = joltWorld.physicsSystem.GetCollisionEvents()
  for (const c of collisions) {
    const bodyA = c.bodyA
    const bodyB = c.bodyB
    // find which entities correspond to bodyA, bodyB, then apply logic
  }
}

Because Koota queries are trait-based, you might store a map of BodyID -> Entity to help quickly find which entity is colliding. You can store that map in your JoltWorldImpl or a separate trait.


6. Motion Types & Continuous Collision

Jolt supports different motion types:

  • Static: For immovable objects (floors, walls).
  • Dynamic: Fully simulated by physics (gravity, collisions).
  • Kinematic: Driven by user code, but can push dynamic bodies out of the way.

Continuous Collision (CCD) helps fast-moving objects (e.g., bullets) avoid tunneling. In the code, we check continuousCollisionMode and set SetMotionQuality(body.GetID(), J.EMotionQuality_LinearCast).


7. Common Patterns & Tips

  1. Separation of Concerns
    • Keep “build bodies” in one system, “update the world” in another, and “sync transforms” in a third.
  2. Deferred Body Creation
    • If your engine loads slowly or geometry is not ready, a system can wait until necessary data is present.
  3. Destroying Entities
    • Remember to remove or destroy the underlying Jolt body. For example, if an entity has a DestroyMe trait, a system can remove its Jolt body from the world.
  4. Performance
    • Potentially run heavy collision checks at a lower frequency if your game can tolerate it.
  5. Accessing ECS or React
    • In React code, prefer using actions or hooking into ECS queries to do spawns, impulses, etc.

8. Summary & Next Steps

  • Initialize Jolt in a single ECS trait (JoltWorld), which you can reference anywhere.
  • Build bodies for new ECS entities using a system that checks a NeedsJoltBody trait or similar.
  • Step the physics in an UpdateJolt system each frame, and sync transforms with your ECS or Three.js objects.
  • For collisions or advanced features (e.g. joints, triggers), adapt the same pattern: define traits, attach them, and process with systems.

Next Steps:

  1. Batched Mesh: If you have many objects, see Batch Meshes for rendering optimizations.
  2. Actions: Create ECS actions to spawn, remove, or apply forces to bodies in a centralized, testable manner.
  3. In-Depth: Explore official Jolt Physics docs for advanced usage: constraints, sleeping, broad-phase tuning, etc.

That’s it! You now have a fully integrated ECS + Jolt physics pipeline in Viber3D.