physics2d

package
v0.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 25, 2026 License: LGPL-3.0 Imports: 7 Imported by: 0

README

physics2d

Box2D v3–backed 2D physics plugin for Cardinal. All simulation state lives on the C side; Cardinal drives it through three ECS components attached to the entities you want simulated.

Registration:

world := cardinal.NewWorld(cardinal.WorldOptions{TickRate: 60})
cardinal.RegisterPlugin(world, physics2d.NewPlugin(physics2d.Config{
    Gravity:      physics2d.Vec2{X: 0, Y: -9.8},
    TickRate:     60, // match WorldOptions.TickRate
    SubStepCount: 4,
}))
world.StartGame()

Enrolling an entity into physics

An entity participates in the simulation when it carries all three of these components. Missing any one → the reconciler skips the entity and no body is created on the C side.

Component Purpose
Transform2D World-space position + rotation (authoritative pose)
Velocity2D Linear + angular velocity
PhysicsBody2D Body kind, damping, flags, and the compound collider (Shapes)

Use the NewPhysicsBody2D constructor — bare struct literals leave Active, Awake, SleepingAllowed at false and GravityScale at 0, which produces an inactive, sleeping, gravity-less body.

Example: a dynamic circle
import (
    "github.com/argus-labs/world-engine/pkg/cardinal"
    "github.com/argus-labs/world-engine/pkg/plugin/physics2d"
)

func SpawnBallSystem(ctx cardinal.WorldContext) error {
    id, err := cardinal.Create(ctx,
        physics2d.Transform2D{
            Position: physics2d.Vec2{X: 0, Y: 10},
            Rotation: 0,
        },
        physics2d.Velocity2D{
            Linear:  physics2d.Vec2{X: 0, Y: 0},
            Angular: 0,
        },
        physics2d.NewPhysicsBody2D(
            physics2d.BodyTypeDynamic,
            physics2d.ColliderShape{
                ShapeType:    physics2d.ShapeTypeCircle,
                Radius:       0.5,
                Density:      1.0,
                Friction:     0.3,
                Restitution:  0.2,
                CategoryBits: 0x0001,
                MaskBits:     0xFFFF,
            },
        ),
    )
    _ = id
    return err
}
Example: a static box (world geometry)
cardinal.Create(ctx,
    physics2d.Transform2D{Position: physics2d.Vec2{X: 0, Y: 0}},
    physics2d.Velocity2D{},
    physics2d.NewPhysicsBody2D(
        physics2d.BodyTypeStatic,
        physics2d.ColliderShape{
            ShapeType:    physics2d.ShapeTypeBox,
            HalfExtents:  physics2d.Vec2{X: 25, Y: 1},
            Friction:     0.5,
            CategoryBits: 0x0002,
            MaskBits:     0xFFFF,
        },
    ),
)
Body-type cheat sheet
  • BodyTypeStatic — immovable world geometry. No writeback.
  • BodyTypeDynamic — full simulation: forces, gravity, collisions. Writeback updates Transform2D/Velocity2D each tick.
  • BodyTypeKinematic — velocity-driven; Box2D integrates position. Writeback on.
  • BodyTypeManual — gameplay owns position/velocity; Box2D is used only for contact detection. No writeback; the reconciler pushes ECS → Box2D each tick. Use for characters/enemies driven by input or AI.
Compound colliders

PhysicsBody2D.Shapes is a slice — each entry is a child fixture with its own LocalOffset, LocalRotation, material, and filter. Shape identity is by index (slot i in Shapes ↔ fixture slot i), so don't reorder shapes after creation if you care about per-shape references in contact events.

Built-in queries

Use these first; they cover most needs and don't require CGO:

  • physics2d.Raycast(RaycastRequest) RaycastResult
  • physics2d.OverlapAABB(AABBOverlapRequest) AABBOverlapResult
  • physics2d.CircleSweep(CircleSweepRequest) CircleSweepResult

All three return zero results when no C-side world exists yet (e.g. before the first reconcile, or right after ResetRuntime).

Custom queries via CGO

If you need a Box2D feature the plugin doesn't expose (joints, shape casts, custom query filters, sensor-only overlap, etc.), call Box2D directly from your own CGO package. The plugin exposes the raw world handle via physics2d.WorldID(), which returns the Box2D v3 b2WorldId packed as a uint32. Reconstruct it in C with b2LoadWorldId.

Userdata encoding

The bridge stuffs identity into Box2D userdata pointers (see bridge.c:214-217):

  • Body userdata = entity ID, packed as (void*)(uintptr_t)entity_id — unpack with (uint32_t)(uintptr_t)b2Body_GetUserData(bodyId).
  • Shape userdata = shape slot index (the index into PhysicsBody2D.Shapes), packed the same way but as int32_t.

So inside any Box2D callback you can recover the ECS entity with one line.

Example: custom AABB overlap that returns every hit, including sensors
package myphysics

/*
#cgo CFLAGS: -I${SRCDIR}/../../vendor/world-engine/pkg/plugin/physics2d/third_party/box2d/include
#include "box2d/box2d.h"
#include <stdint.h>

static bool overlap_cb(b2ShapeId shapeId, void* ctx) {
    // Recover the ECS entity ID from body userdata.
    b2BodyId body = b2Shape_GetBody(shapeId);
    uint32_t* out = (uint32_t*)ctx;
    // ... append unpack_uint32(b2Body_GetUserData(body)) to your buffer ...
    (void)out;
    return true; // keep going
}

static int my_overlap_all(
    uint32_t world_id_packed,
    float minX, float minY, float maxX, float maxY,
    uint32_t* out_entities, int32_t cap
) {
    b2WorldId world = b2LoadWorldId(world_id_packed);
    if (!b2World_IsValid(world)) return 0;

    b2AABB aabb = { {minX, minY}, {maxX, maxY} };
    b2QueryFilter filter = b2DefaultQueryFilter(); // matches everything
    b2World_OverlapAABB(world, aabb, filter, overlap_cb, out_entities);
    // ... return fill count ...
    return 0;
}
*/
import "C"

import "github.com/argus-labs/world-engine/pkg/plugin/physics2d"

func OverlapAll(minX, minY, maxX, maxY float64) []uint32 {
    worldID := physics2d.WorldID()
    if worldID == 0 {
        return nil // no world yet: before init or after ResetRuntime
    }
    buf := make([]uint32, 256)
    n := C.my_overlap_all(
        C.uint32_t(worldID),
        C.float(minX), C.float(minY), C.float(maxX), C.float(maxY),
        (*C.uint32_t)(&buf[0]), C.int32_t(len(buf)),
    )
    return buf[:int(n)]
}
Rules of engagement
  • Always null-check WorldID() — it returns 0 before the first PreUpdate reconcile and after ResetRuntime. Treat 0 as "no world, skip the query."
  • Never mutate world state from a system. WorldID() gives you a raw Box2D handle; calling b2Body_SetTransform / b2DestroyBody / etc. directly will desync the bridge's entity→body map and the reconciler will fight you next tick. For mutations, go through ECS components — the reconciler pushes changes to Box2D before each step.
  • Queries are fine. Raycasts, overlaps, shape casts, sensor iteration, contact walks — read-only Box2D calls are safe to make any time after you've confirmed WorldID() != 0.
  • Don't cache the handle across ticks. WorldID() is cheap; call it at the top of each query. After ResetRuntime (e.g. snapshot restore) the old id is invalid.

Contact events

Contacts and triggers flow through Cardinal's system-event bus. Register an emitter with physics2d.SetStepContactEmitter and the plugin flushes ContactBeginEvent / ContactEndEvent / TriggerBeginEvent / TriggerEndEvent each tick. The events carry both entity IDs and both shape indices, so you can look up the exact ColliderShape that produced the contact.

Documentation

Overview

Package physics2d is a Box2D-backed 2D physics plugin for Cardinal. ECS components live in component; simulation and reconciliation systems live in system; package-level runtime state is owned here (see Runtime, ResetRuntime).

Usage:

world := cardinal.NewWorld(cardinal.WorldOptions{...})
cardinal.RegisterPlugin(world, physics2d.NewPlugin(physics2d.Config{}))
world.StartGame()

Call ResetRuntime from init/restore hooks when you rebuild the Cardinal world or after FromProto so the derived physics state matches ECS.

ResetRuntime (this package) discards derived physics state; the next PreUpdate reconcile performs a full ECS->Box2D rebuild when it sees no world on the C side.

Index

Constants

View Source
const (
	BodyTypeStatic    = component.BodyTypeStatic
	BodyTypeDynamic   = component.BodyTypeDynamic
	BodyTypeKinematic = component.BodyTypeKinematic
	BodyTypeManual    = component.BodyTypeManual
)

Body kinds (PhysicsBody2D).

View Source
const (
	ShapeTypeCircle          = component.ShapeTypeCircle
	ShapeTypeBox             = component.ShapeTypeBox
	ShapeTypeConvexPolygon   = component.ShapeTypeConvexPolygon
	ShapeTypeStaticChain     = component.ShapeTypeStaticChain
	ShapeTypeStaticChainLoop = component.ShapeTypeStaticChainLoop
	ShapeTypeEdge            = component.ShapeTypeEdge
	ShapeTypeCapsule         = component.ShapeTypeCapsule
)

Collider shape kinds (ColliderShape).

Variables

This section is empty.

Functions

func FlushBufferedContacts

func FlushBufferedContacts()

FlushBufferedContacts emits all contact/trigger events from the last physics step via the ContactEventEmitter set by SetStepContactEmitter, then clears the buffer and the emitter slot. If an emitter was set for that step, one-shot contact suppression (after reset/rebuild) ends here. Ordering matches C-side callback order for that step only; see internal.FlushBufferedContacts.

func ResetRuntime

func ResetRuntime()

ResetRuntime drops all derived physics simulation state (no world, no bodies, empty maps). ECS components are unchanged. The next PhysicsPipelineSystem (PreUpdate) runs FullRebuildFromECS from current physics entities, same as recovering after snapshot restore.

func SetStepContactEmitter

func SetStepContactEmitter(e ContactEventEmitter)

SetStepContactEmitter assigns the sink for the next physics step's flushed contact events.

func WorldID

func WorldID() uint32

WorldID returns the Box2D v3 world id packed as a uint32. Returns 0 when no world exists (before init or after ResetRuntime). Customers can use this with their own CGO code to call any Box2D v3 function via b2LoadWorldId(). This enables custom queries, joints, or any Box2D feature not directly exposed by the plugin.

Types

type AABBOverlapHit

type AABBOverlapHit = physicsquery.AABBOverlapHit

Query API (v1): raycast, AABB overlap, circle sweep.

type AABBOverlapRequest

type AABBOverlapRequest = physicsquery.AABBOverlapRequest

Query API (v1): raycast, AABB overlap, circle sweep.

type AABBOverlapResult

type AABBOverlapResult = physicsquery.AABBOverlapResult

Query API (v1): raycast, AABB overlap, circle sweep.

func OverlapAABB

func OverlapAABB(req AABBOverlapRequest) AABBOverlapResult

OverlapAABB returns distinct (entity, shape index) pairs whose shapes overlap the world-space AABB.

type ActiveContacts

type ActiveContacts = component.ActiveContacts

Re-export component types for callers that import the plugin root only.

type BodyType

type BodyType = component.BodyType

Re-export component types for callers that import the plugin root only.

type CircleSweepRequest

type CircleSweepRequest = physicsquery.CircleSweepRequest

Query API (v1): raycast, AABB overlap, circle sweep.

type CircleSweepResult

type CircleSweepResult = physicsquery.CircleSweepResult

Query API (v1): raycast, AABB overlap, circle sweep.

func CircleSweep

func CircleSweep(req CircleSweepRequest) CircleSweepResult

CircleSweep sweeps a circle from req.Start to req.End and returns the earliest TOI hit along that segment.

type ColliderShape

type ColliderShape = component.ColliderShape

Re-export component types for callers that import the plugin root only.

type Config

type Config struct {
	// Gravity is applied to the Box2D world (world gravity vector).
	Gravity Vec2
	// TickRate is simulation steps per second: each Cardinal tick calls cbridge.Step(1/TickRate, sub-steps).
	// Match cardinal.WorldOptions.TickRate so simulated time advances one tick of wall-clock intent per tick.
	// Zero or negative defaults to 60 (same as historical FixedDT 1/60).
	TickRate float64
	// SubStepCount is the number of sub-steps per physics step. Zero defaults to 4.
	SubStepCount int
}

Config holds plugin options for simulation and stepping.

type ContactBeginEvent

type ContactBeginEvent = physicevent.ContactBeginEvent

Contact / trigger system events (implement ecs.SystemEvent; register with WithSystemEventEmitter).

type ContactEndEvent

type ContactEndEvent = physicevent.ContactEndEvent

Contact / trigger system events (implement ecs.SystemEvent; register with WithSystemEventEmitter).

type ContactEventEmitter

type ContactEventEmitter = physicevent.ContactEventEmitter

Contact / trigger system events (implement ecs.SystemEvent; register with WithSystemEventEmitter).

type ContactEventPayload

type ContactEventPayload = physicevent.ContactEventPayload

Contact / trigger system events (implement ecs.SystemEvent; register with WithSystemEventEmitter).

type ContactPairEntry

type ContactPairEntry = component.ContactPairEntry

Re-export component types for callers that import the plugin root only.

type Filter

type Filter = physicsquery.Filter

Query API (v1): raycast, AABB overlap, circle sweep.

type FixtureFilterBits

type FixtureFilterBits = physicevent.FixtureFilterBits

Contact / trigger system events (implement ecs.SystemEvent; register with WithSystemEventEmitter).

type PhysicsBody2D

type PhysicsBody2D = component.PhysicsBody2D

Components entities require to participate in physics simulation.

type PhysicsSingletonTag

type PhysicsSingletonTag = component.PhysicsSingletonTag

Re-export component types for callers that import the plugin root only.

type Plugin

type Plugin struct {
	// contains filtered or unexported fields
}

Plugin implements cardinal.Plugin for the physics2d package.

func NewPlugin

func NewPlugin(config Config) *Plugin

NewPlugin builds a physics2d plugin instance.

func (*Plugin) Register

func (p *Plugin) Register(world *cardinal.World)

Register implements cardinal.Plugin: resets runtime state and registers systems.

type RaycastRequest

type RaycastRequest = physicsquery.RaycastRequest

Query API (v1): raycast, AABB overlap, circle sweep.

type RaycastResult

type RaycastResult = physicsquery.RaycastResult

Query API (v1): raycast, AABB overlap, circle sweep.

func Raycast

func Raycast(req RaycastRequest) RaycastResult

Raycast casts a ray along the segment from req.Origin to req.End and returns the closest hit. Requires an initialized physics runtime with a C-side world (e.g. after FullRebuildFromECS). A zero-length segment returns Hit=false. When Filter is nil, all category/mask pairs match and sensors are skipped (same as Filter{CategoryBits: ^uint64(0), MaskBits: ^uint64(0), IncludeSensors: false}).

type ShapeType

type ShapeType = component.ShapeType

Re-export component types for callers that import the plugin root only.

type Transform2D

type Transform2D = component.Transform2D

Components entities require to participate in physics simulation.

type TriggerBeginEvent

type TriggerBeginEvent = physicevent.TriggerBeginEvent

Contact / trigger system events (implement ecs.SystemEvent; register with WithSystemEventEmitter).

type TriggerEndEvent

type TriggerEndEvent = physicevent.TriggerEndEvent

Contact / trigger system events (implement ecs.SystemEvent; register with WithSystemEventEmitter).

type Vec2

type Vec2 = component.Vec2

Re-export component types for callers that import the plugin root only.

type Velocity2D

type Velocity2D = component.Velocity2D

Components entities require to participate in physics simulation.

Directories

Path Synopsis
cbridge
Package cbridge wraps Box2D v3 (C) via CGO for the physics2d plugin.
Package cbridge wraps Box2D v3 (C) via CGO for the physics2d plugin.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL