internal

package
v0.16.8 Latest Latest
Warning

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

Go to latest
Published: Sep 16, 2026 License: LGPL-3.0 Imports: 12 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ColliderShapeMutableFieldsEqual

func ColliderShapeMutableFieldsEqual(a, b component.ColliderShape) bool

ColliderShapeMutableFieldsEqual compares per-shape fields that Box2D can update without recreating the fixture shape.

func ShapesStructuralEqual

func ShapesStructuralEqual(a, b ShapeSlice) bool

ShapesStructuralEqual reports whether two shape lists match for Box2D fixture shape definition: shape count/order (topology) and, per index, shape type, local transform, and geometry. Differences confined to sensor flag, friction, restitution, density, or filter (category, mask, group) are not structural and can be applied with fixture setters.

Types

type BufferedContactEvent

type BufferedContactEvent struct {
	Kind               ContactLifecycleKind
	FilterA            event.FixtureFilterBits
	FilterB            event.FixtureFilterBits
	EntityA            cardinal.EntityID
	EntityB            cardinal.EntityID
	ShapeIndexA        int
	ShapeIndexB        int
	IsSensorContact    bool
	Normal             component.Vec2
	NormalValid        bool
	Point              component.Vec2
	PointValid         bool
	ManifoldPointCount int
}

BufferedContactEvent is one contact record collected after a physics step for post-step consumption. It is not an engine/game event type — callers translate these records later.

IsSensorContact is true if either shape is a sensor (overlap / trigger semantics). Sensors do not build collision manifolds, so NormalValid and PointValid are usually false for pure sensor overlaps.

FilterA/FilterB are the shape filter bits at event time (category/mask/group), matching ECS per-shape bits so consumers can route without re-querying colliders.

type ContactLifecycleKind

type ContactLifecycleKind uint8

ContactLifecycleKind distinguishes BeginContact vs EndContact.

const (
	// ContactLifecycleBegin is emitted when two shapes start touching.
	ContactLifecycleBegin ContactLifecycleKind = iota
	// ContactLifecycleEnd is emitted when two shapes stop touching.
	ContactLifecycleEnd
)

type ContactPairInfo

type ContactPairInfo struct {
	IsSensor           bool
	FilterA            event.FixtureFilterBits
	FilterB            event.FixtureFilterBits
	Normal             component.Vec2
	NormalValid        bool
	Point              component.Vec2
	PointValid         bool
	ManifoldPointCount int
}

ContactPairInfo stores metadata for an active contact pair. FilterA/FilterB correspond to (EntityA, ShapeIndexA) and (EntityB, ShapeIndexB) after normalization. Manifold fields are best-effort from the last live sample (not serialized to snapshots).

type ContactPairKey

type ContactPairKey struct {
	EntityA     cardinal.EntityID
	ShapeIndexA int
	EntityB     cardinal.EntityID
	ShapeIndexB int
}

ContactPairKey identifies a unique fixture-pair contact. Always normalized so that (EntityA, ShapeIndexA) < (EntityB, ShapeIndexB) lexicographically.

type PhysicsRebuildEntry

type PhysicsRebuildEntry struct {
	EntityID    cardinal.EntityID
	Transform   component.Transform2D
	Velocity    component.Velocity2D
	PhysicsBody component.PhysicsBody2D
}

PhysicsRebuildEntry is one entity's authoritative physics components as read from ECS.

type Runtime

type Runtime struct {
	// World is the pure-Go Box2D world owned by this runtime. Nil until the first
	// FullRebuildFromECS and after Reset.
	World *box2d.World

	// Bodies maps Cardinal entity ids to their Box2D body ids in World.
	Bodies map[cardinal.EntityID]box2d.BodyID

	// Shapes maps entity ids to per-collider-slot Box2D shape ids: slot i corresponds to
	// ColliderShape index i. Chain slots hold a null ShapeID (chains are tracked in Chains)
	// so per-shape mutable setters skip them, matching the CGO bridge behavior.
	Shapes map[cardinal.EntityID][]box2d.ShapeID

	// Chains maps entity ids to the chain shapes created for chain-type collider slots.
	Chains map[cardinal.EntityID][]box2d.ChainID

	// Gravity is the world gravity vector applied on world creation and on rebuild.
	Gravity component.Vec2

	// FixedDT is the simulated time advanced by one physics step, in seconds.
	FixedDT float64

	// SubStepCount is the number of solver sub-steps per physics step.
	SubStepCount int

	// Workers is the worker count handed to box2d.WorldDef.WorkerCount on world
	// creation (0 = serial). Simulation results are byte-identical for every
	// value, so it never participates in rebuild/restore determinism.
	Workers int

	// KnownEntities tracks which Cardinal entities have bodies in the Box2D world.
	KnownEntities map[cardinal.EntityID]struct{}

	// Shadow holds per-entity reconciler snapshots (diff against ECS each tick).
	Shadow map[cardinal.EntityID]ShadowState

	// BufferedContacts collects contact events from the physics step for post-step flush.
	BufferedContacts []BufferedContactEvent

	// Emitter is the current tick's contact flush sink, set by the step driver before Step
	// and cleared in FlushBufferedContacts. Nil means skip emitting for this flush.
	Emitter event.ContactEventEmitter

	// SuppressContactsStep, when true, skips emitting contact/trigger begin/end for this step
	// (e.g. first step after restore).
	SuppressContactsStep bool

	// ActiveContacts is the in-memory working copy of which Begin events have been emitted
	// without a matching End. nil means "not yet loaded from ECS" (e.g. after Reset);
	// the step system populates it from the persisted ActiveContacts component on first access.
	ActiveContacts map[ContactPairKey]ContactPairInfo

	// ActiveContactsDirty is set when ActiveContacts was mutated during the current flush.
	// The step system checks this to decide whether to Set() the ECS component.
	ActiveContactsDirty bool

	// NoPersistedActiveContactsBaseline, when true, the next suppressed contact flush seeds
	// ActiveContacts from the live contact list without emitting Begin/End (physics singleton entity missing).
	NoPersistedActiveContactsBaseline bool
	// contains filtered or unexported fields
}

Runtime owns derived physics state for one Cardinal world instance. ECS remains authoritative; this struct is disposable and rebuilt from components when needed.

One Runtime belongs to exactly one physics2d.Plugin instance. The pure-Go Box2D backend is per-instance state, so multiple live Runtimes in one process simulate independently.

func NewRuntime added in v0.16.6

func NewRuntime(gravity component.Vec2, fixedDT float64, subSteps, workers int) *Runtime

NewRuntime returns an empty runtime with the given simulation parameters. Maps are initialized; Emitter is nil. Non-positive fixedDT or subSteps fall back to 60 Hz / 4 sub-steps. workers is the box2d.WorldDef.WorkerCount for worlds this runtime creates (0 = serial; results are byte-identical for every value). SuppressContactsStep is true so the next armed simulation step does not record contact begin/end; the following FlushBufferedContacts clears suppression when that flush is paired with an emitter (see contact_flush.go). ActiveContacts is nil, signaling "load from ECS on next step".

func (*Runtime) ActiveContactsToComponent added in v0.16.6

func (rt *Runtime) ActiveContactsToComponent() component.ActiveContacts

ActiveContactsToComponent converts the working map to the ECS component format (sorted slice for deterministic snapshots).

func (*Runtime) AttachColliderFixtures added in v0.16.6

func (rt *Runtime) AttachColliderFixtures(entityID cardinal.EntityID, shapes ShapeSlice) error

AttachColliderFixtures creates one shape per ColliderShape on the body identified by entityID. shapeIndex is the index i in shapes. Local offsets and rotations are applied so geometry defined in shape space is placed correctly in body space.

func (*Runtime) BodyIDOf added in v0.16.6

func (rt *Runtime) BodyIDOf(entityID cardinal.EntityID) (box2d.BodyID, bool)

BodyIDOf returns the Box2D body id tracked for entityID and whether one exists. Read-only accessor for the plugin's public lookup API.

func (*Runtime) CircleSweep added in v0.16.6

CircleSweep sweeps a circle along Start->End and returns the earliest TOI hit. A zero-radius or zero-length sweep always returns no hit.

func (*Runtime) CreateBody added in v0.16.6

func (rt *Runtime) CreateBody(
	entityID cardinal.EntityID,
	transform component.Transform2D,
	velocity component.Velocity2D,
	pb component.PhysicsBody2D,
) error

CreateBody creates a body in this runtime's Box2D world. It does not attach shapes; use AttachColliderFixtures next.

func (*Runtime) CreateBodyWithCollider added in v0.16.6

func (rt *Runtime) CreateBodyWithCollider(
	entityID cardinal.EntityID,
	transform component.Transform2D,
	velocity component.Velocity2D,
	pb component.PhysicsBody2D,
) error

CreateBodyWithCollider creates a body and attaches all shapes. If shape attachment fails, the body is destroyed and an error is returned.

func (*Runtime) DestroyEntityBody added in v0.16.6

func (rt *Runtime) DestroyEntityBody(entityID cardinal.EntityID)

DestroyEntityBody destroys the Box2D body for entityID (with all attached shapes and chains) and clears the runtime's body/shape/chain tracking for it. No-op when the entity has no body.

func (*Runtime) FlushBufferedContacts added in v0.16.6

func (rt *Runtime) FlushBufferedContacts()

FlushBufferedContacts turns buffered contact records into physics2d events via the runtime emitter, then clears the buffer and nils Emitter.

On normal ticks, every Begin adds to rt.ActiveContacts and every End removes from it. After applying the step buffer, sustained overlaps are refreshed from the live contact list so IsSensor and filter bits stay aligned if a shape toggles sensor/solid or filters change while the pair remains touching.

On the first step after a rebuild (SuppressContactsStep was true), the buffer is empty because the listener suppressed callbacks. Instead, we diff rt.ActiveContacts (loaded from the persisted ECS component) against the live contact list:

  • Pairs in live but not in the map -> emit Begin, add to map
  • Pairs in the map but not in live -> emit End, remove from map
  • Pairs in both -> no event (game already knows)

If NoPersistedActiveContactsBaseline is set (missing singleton on a suppressed step), the suppressed flush adopts live contacts into the map without emitting events so one-shot Begin handlers do not all fire spuriously; the flag is cleared.

func (*Runtime) FullRebuildFromECS added in v0.16.6

func (rt *Runtime) FullRebuildFromECS(gravity component.Vec2, entries []PhysicsRebuildEntry) error

FullRebuildFromECS replaces all derived physics state on this runtime in one deterministic pass: destroys every body in the Box2D world, clears maps and contact buffer, optionally applies gravity, recreates bodies/shapes from entries, then writes shadow snapshots. Entries are sorted by EntityID before processing; creation order follows that sort.

World is created on first rebuild using gravity; later rebuilds reuse the world and call SetGravity.

On any creation error, bodies created in this pass are destroyed and runtime maps are left empty (same as post-clear); the world remains allocated with no bodies.

After a successful rebuild, Emitter is cleared and SuppressContactsStep is set true for the next simulation step. The step driver must call SetStepEmitter before the step and FlushBufferedContacts after; that flush clears SuppressContactsStep automatically.

Bodies keep their component Awake value; those in the persisted ActiveContacts baseline are woken before the suppressed step instead (see Runtime.wakePersistedContactEntities).

func (*Runtime) KeepRebuildEntriesScratch added in v0.16.6

func (rt *Runtime) KeepRebuildEntriesScratch(entries []PhysicsRebuildEntry) []PhysicsRebuildEntry

KeepRebuildEntriesScratch stores the gathered slice back on the runtime and clears everything past its length. Returns it for convenient chaining.

func (*Runtime) KeepWritebackScratch added in v0.16.6

func (rt *Runtime) KeepWritebackScratch(entries []WritebackEntry) []WritebackEntry

KeepWritebackScratch stores the gathered slice back on the runtime and clears everything past its length. WritebackEntry holds cardinal.Refs, so an uncleared tail keeps archetype storage for destroyed entities alive.

func (*Runtime) LoadActiveContactsFromComponent added in v0.16.6

func (rt *Runtime) LoadActiveContactsFromComponent(ac component.ActiveContacts)

LoadActiveContactsFromComponent populates the in-memory working map from the persisted ECS component. Called by the step system after a restore when ActiveContacts is nil.

func (*Runtime) OverlapAABB added in v0.16.6

OverlapAABB returns distinct (Entity, ShapeIndex) pairs overlapping the query AABB, sorted by (Entity, ShapeIndex). Broadphase traversal order is an implementation detail and was never part of the wrapper contract; sorting makes results deterministic. A zero-area AABB (Min == Max on any axis) always returns no hits.

func (*Runtime) PruneActiveContactsInvolvingEntity added in v0.16.6

func (rt *Runtime) PruneActiveContactsInvolvingEntity(entityID cardinal.EntityID)

PruneActiveContactsInvolvingEntity removes every active-contact key that references entityID. Call when that entity's body is destroyed or its fixtures are structurally replaced so end-of-tick persistence and the next suppressed diff do not retain stale pair keys.

Each pruned pair was touching, so its contact genuinely ends here. The engine does report end-touch events for a destroyed body, but by the time they are drained the shapes are gone and the records can no longer be resolved to entities, so those events are dropped (see bufferContactEventsFromWorld). Synthesize the End from the persisted pair metadata instead and hold it until the next flush; without this a consumer that latches state on Begin (an "is grounded" flag, say) would never be told the contact ended.

func (*Runtime) Raycast added in v0.16.6

func (rt *Runtime) Raycast(req query.RaycastRequest) query.RaycastResult

Raycast returns the closest fixture hit along [req.Origin, req.End], or Hit=false. A zero-length segment (Origin == End) always returns no hit.

func (*Runtime) RebuildEntriesScratch added in v0.16.6

func (rt *Runtime) RebuildEntriesScratch() []PhysicsRebuildEntry

RebuildEntriesScratch returns the runtime-owned buffer for the pipeline system's reconcile/rebuild gather, emptied and ready to append into. Hand the grown slice back with KeepRebuildEntriesScratch so the capacity survives the tick and the tail stops pinning ECS memory.

func (*Runtime) ReconcileFromECS added in v0.16.6

func (rt *Runtime) ReconcileFromECS(entries []PhysicsRebuildEntry) error

ReconcileFromECS incrementally syncs the Box2D world from authoritative ECS entries using shadow-copy diffing. It is the hot-path counterpart to FullRebuildFromECS.

Structural vs mutable changes:

  • Structural: anything that changes shape identity -- shape count/order, per-shape type, local offset/rotation, or geometry (radius, half-extents, vertices, chain points). Handled by destroying all shapes on the body and re-attaching from ECS.

  • Mutable: body transform, linear/angular velocity, body type/damping/gravity scale, and per-shape sensor, friction, restitution, density, and filter category/mask/group. Applied in place without recreating shapes.

Requires a live world on this runtime (for example after an initial FullRebuildFromECS). Entries are sorted by EntityID; duplicate IDs are an error. Entities absent from entries are removed from the runtime (body destroyed, shadow dropped).

ReconcileFromECS does not touch SuppressContactsStep or Emitter; it does not step the world.

func (*Runtime) Reset added in v0.16.6

func (rt *Runtime) Reset()

Reset drops all derived physics state on this runtime, returning it to the state of a freshly constructed Runtime. If a Box2D world exists, it is destroyed first. Simulation parameters (Gravity, FixedDT, SubStepCount, Workers) are preserved.

func (*Runtime) SetStepEmitter added in v0.16.6

func (rt *Runtime) SetStepEmitter(emitter event.ContactEventEmitter)

SetStepEmitter stores the contact event sink for the upcoming simulation step. The step driver should set this before the step and call FlushBufferedContacts after the step.

func (*Runtime) ShapeIDsOf added in v0.16.6

func (rt *Runtime) ShapeIDsOf(entityID cardinal.EntityID) ([]box2d.ShapeID, bool)

ShapeIDsOf returns a copy of the per-collider-slot shape ids tracked for entityID and whether any exist. The copy keeps callers from mutating the runtime's own slice.

func (*Runtime) Step added in v0.16.6

func (rt *Runtime) Step()

Step advances this runtime's Box2D world by FixedDT with SubStepCount solver sub-steps, then buffers the step's contact and sensor events into rt.BufferedContacts for the next FlushBufferedContacts. Event order matches the CGO bridge's drain order exactly: contact begins, contact ends, sensor begins, sensor ends.

func (*Runtime) WorldExists added in v0.16.6

func (rt *Runtime) WorldExists() bool

WorldExists reports whether this runtime's Box2D world has been created and is alive.

func (*Runtime) WritebackFromStepResults added in v0.16.6

func (rt *Runtime) WritebackFromStepResults(entries []WritebackEntry)

WritebackFromStepResults reads post-step positions, rotations, velocities, and awake state from the Box2D world and writes them into the corresponding ECS Transform2D, Velocity2D, and PhysicsBody2D components. It also updates the shadow state so the next ReconcileFromECS tick sees no diff for these values.

Iteration is driven by entries (ECS iteration order, EntityID-sorted), never by the runtime's Go maps, so the write order is deterministic.

Writeback applies to dynamic and kinematic bodies only. Static bodies never move. Manual bodies (ECS BodyTypeManual) are skipped because ECS/gameplay code owns their position. Since both BodyTypeKinematic and BodyTypeManual map to Box2D kinematic bodies, the ECS body type is checked rather than the Box2D body type.

Awake is mirrored back on change so the component tracks solver sleep state instead of freezing at its creation value. Disabled bodies are skipped: IsBodyAwake reads false for them, which is disablement, not sleep.

func (*Runtime) WritebackScratch added in v0.16.6

func (rt *Runtime) WritebackScratch() []WritebackEntry

WritebackScratch returns the runtime-owned buffer for the pipeline system's writeback gather, emptied and ready to append into. Pair with KeepWritebackScratch.

type ShadowState

type ShadowState struct {
	Transform   component.Transform2D
	Velocity    component.Velocity2D
	PhysicsBody component.PhysicsBody2D
}

ShadowState is a deep snapshot of the ECS physics components last applied to Box2D for one entity. It must not share backing storage with live ECS data so in-place component edits do not corrupt the snapshot.

An immutable.Slice is not that protection on its own: it hides its array from indexing, but its derivations write straight through it, so a system that does Shapes.With(0, sh) edits the very array a shadow taken earlier would still be reading. Hence the copies below.

func NewShadowState

NewShadowState returns a shadow snapshot with deep-copied shapes (Shapes and per-shape ChainPoints are cloned; Vertices is a fixed array and copies with the struct).

func (ShadowState) BodyParamsDiffer

func (s ShadowState) BodyParamsDiffer(p component.PhysicsBody2D) bool

BodyParamsDiffer reports whether rigidbody simulation parameters differ from the shadow.

func (ShadowState) PhysicsDiffers

PhysicsDiffers is true if any reconciled field differs from the given live components.

func (ShadowState) ShapesDiffer

func (s ShadowState) ShapesDiffer(p component.PhysicsBody2D) bool

ShapesDiffer reports deep differences in compound collider data: shape count/order (topology), per-shape type, transform, sensor, geometry, friction/restitution/density, and filter (category, mask, group).

func (ShadowState) TransformDiffers

func (s ShadowState) TransformDiffers(t component.Transform2D) bool

TransformDiffers reports whether the live transform differs from the shadow.

func (ShadowState) VelocityDiffers

func (s ShadowState) VelocityDiffers(v component.Velocity2D) bool

VelocityDiffers reports whether the live velocity differs from the shadow.

type ShapeSlice added in v0.16.8

type ShapeSlice = immutable.Slice[component.ColliderShape]

ShapeSlice is the compound collider list carried by PhysicsBody2D, named here so the reconciler's signatures read as shapes rather than as their generic spelling.

type WritebackEntry

type WritebackEntry struct {
	EntityID    cardinal.EntityID
	Transform   cardinal.Ref[component.Transform2D]
	Velocity    cardinal.Ref[component.Velocity2D]
	PhysicsBody cardinal.Ref[component.PhysicsBody2D]
}

WritebackEntry holds the ECS refs needed to write Box2D results back to components.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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