Documentation
¶
Index ¶
- func ColliderShapeMutableFieldsEqual(a, b component.ColliderShape) bool
- func ShapesStructuralEqual(a, b ShapeSlice) bool
- type BufferedContactEvent
- type ContactLifecycleKind
- type ContactPairInfo
- type ContactPairKey
- type PhysicsRebuildEntry
- type Runtime
- func (rt *Runtime) ActiveContactsToComponent() component.ActiveContacts
- func (rt *Runtime) AttachColliderFixtures(entityID cardinal.EntityID, shapes ShapeSlice) error
- func (rt *Runtime) BodyIDOf(entityID cardinal.EntityID) (box2d.BodyID, bool)
- func (rt *Runtime) CircleSweep(req query.CircleSweepRequest) query.CircleSweepResult
- func (rt *Runtime) CreateBody(entityID cardinal.EntityID, transform component.Transform2D, ...) error
- func (rt *Runtime) CreateBodyWithCollider(entityID cardinal.EntityID, transform component.Transform2D, ...) error
- func (rt *Runtime) DestroyEntityBody(entityID cardinal.EntityID)
- func (rt *Runtime) FlushBufferedContacts()
- func (rt *Runtime) FullRebuildFromECS(gravity component.Vec2, entries []PhysicsRebuildEntry) error
- func (rt *Runtime) KeepRebuildEntriesScratch(entries []PhysicsRebuildEntry) []PhysicsRebuildEntry
- func (rt *Runtime) KeepWritebackScratch(entries []WritebackEntry) []WritebackEntry
- func (rt *Runtime) LoadActiveContactsFromComponent(ac component.ActiveContacts)
- func (rt *Runtime) OverlapAABB(req query.AABBOverlapRequest) query.AABBOverlapResult
- func (rt *Runtime) PruneActiveContactsInvolvingEntity(entityID cardinal.EntityID)
- func (rt *Runtime) Raycast(req query.RaycastRequest) query.RaycastResult
- func (rt *Runtime) RebuildEntriesScratch() []PhysicsRebuildEntry
- func (rt *Runtime) ReconcileFromECS(entries []PhysicsRebuildEntry) error
- func (rt *Runtime) Reset()
- func (rt *Runtime) SetStepEmitter(emitter event.ContactEventEmitter)
- func (rt *Runtime) ShapeIDsOf(entityID cardinal.EntityID) ([]box2d.ShapeID, bool)
- func (rt *Runtime) Step()
- func (rt *Runtime) WorldExists() bool
- func (rt *Runtime) WritebackFromStepResults(entries []WritebackEntry)
- func (rt *Runtime) WritebackScratch() []WritebackEntry
- type ShadowState
- func (s ShadowState) BodyParamsDiffer(p component.PhysicsBody2D) bool
- func (s ShadowState) PhysicsDiffers(t component.Transform2D, v component.Velocity2D, p component.PhysicsBody2D) bool
- func (s ShadowState) ShapesDiffer(p component.PhysicsBody2D) bool
- func (s ShadowState) TransformDiffers(t component.Transform2D) bool
- func (s ShadowState) VelocityDiffers(v component.Velocity2D) bool
- type ShapeSlice
- type WritebackEntry
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
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
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
func (rt *Runtime) CircleSweep(req query.CircleSweepRequest) query.CircleSweepResult
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
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
func (rt *Runtime) OverlapAABB(req query.AABBOverlapRequest) query.AABBOverlapResult
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
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
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
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 ¶
func NewShadowState( t component.Transform2D, v component.Velocity2D, pb component.PhysicsBody2D, ) ShadowState
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 ¶
func (s ShadowState) PhysicsDiffers( t component.Transform2D, v component.Velocity2D, p component.PhysicsBody2D, ) bool
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.