Documentation
¶
Index ¶
- func AttachColliderFixtures(entityID cardinal.EntityID, shapes []component.ColliderShape) error
- func ColliderShapeMutableFieldsEqual(a, b component.ColliderShape) bool
- func CreateBody(entityID cardinal.EntityID, transform component.Transform2D, ...) error
- func CreateBodyWithCollider(entityID cardinal.EntityID, transform component.Transform2D, ...) error
- func FlushBufferedContacts()
- func FullRebuildFromECS(gravity component.Vec2, entries []PhysicsRebuildEntry) error
- func ReconcileFromECS(entries []PhysicsRebuildEntry) error
- func ResetRuntime()
- func SetBufferedContactsFromStep(events []cbridge.ContactEvent)
- func SetStepEmitter(emitter event.ContactEventEmitter)
- func ShapesStructuralEqual(a, b []component.ColliderShape) bool
- func WritebackFromStepResults(states []cbridge.BodyState, entries []WritebackEntry)
- type BufferedContactEvent
- type ContactLifecycleKind
- type ContactPairInfo
- type ContactPairKey
- type PhysicsRebuildEntry
- type PhysicsRuntime
- 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 WritebackEntry
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AttachColliderFixtures ¶
func AttachColliderFixtures(entityID cardinal.EntityID, shapes []component.ColliderShape) error
AttachColliderFixtures creates one shape per ColliderShape on the C-side body identified by entityID. shapeIndex is the slice index i in shapes. Local offsets and rotations are applied so geometry defined in shape space is placed correctly in body space.
func ColliderShapeMutableFieldsEqual ¶
func ColliderShapeMutableFieldsEqual(a, b component.ColliderShape) bool
ColliderShapeMutableFieldsEqual compares per-shape fields that Box2D can update without recreating the fixture shape.
func CreateBody ¶
func CreateBody( entityID cardinal.EntityID, transform component.Transform2D, velocity component.Velocity2D, pb component.PhysicsBody2D, ) error
CreateBody creates a body on the C-side Box2D world via cbridge. It does not attach shapes; use AttachColliderFixtures next.
func CreateBodyWithCollider ¶
func 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 FlushBufferedContacts ¶
func 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 FullRebuildFromECS ¶
func FullRebuildFromECS(gravity component.Vec2, entries []PhysicsRebuildEntry) error
FullRebuildFromECS replaces all derived physics state for the package Runtime in one deterministic pass: destroys every body in the C-side 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.
Requires ResetRuntime (or prior init) so Runtime() is non-nil. 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.
func ReconcileFromECS ¶
func ReconcileFromECS(entries []PhysicsRebuildEntry) error
ReconcileFromECS incrementally syncs the C-side 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 non-nil Runtime() with a live C-side world (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 ResetRuntime ¶
func ResetRuntime()
ResetRuntime replaces the package runtime with a fresh PhysicsRuntime. If a C-side world exists, it is destroyed first.
func SetBufferedContactsFromStep ¶
func SetBufferedContactsFromStep(events []cbridge.ContactEvent)
SetBufferedContactsFromStep converts cbridge.ContactEvent results from Step into BufferedContactEvents and stores them in rt.BufferedContacts for the next flush. Called by the pipeline between cbridge.Step and FlushBufferedContacts.
func SetStepEmitter ¶
func 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 ShapesStructuralEqual ¶
func ShapesStructuralEqual(a, b []component.ColliderShape) bool
ShapesStructuralEqual reports whether two shape slices 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.
func WritebackFromStepResults ¶
func WritebackFromStepResults(states []cbridge.BodyState, entries []WritebackEntry)
WritebackFromStepResults reads post-step positions, rotations, and velocities from the cbridge.Step body states and writes them into the corresponding ECS Transform2D and Velocity2D components. It also updates the shadow state so the next ReconcileFromECS tick sees no diff for these values.
Writeback applies to dynamic and kinematic bodies only. Static bodies (BodyType==1) never move. Manual bodies (ECS BodyTypeManual==4) 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 bridge body type.
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 PhysicsRuntime ¶
type PhysicsRuntime struct {
// KnownEntities tracks which Cardinal entities have bodies in the C-side 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 cbridge.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 ResetRuntime);
// 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
}
PhysicsRuntime owns derived physics state for one Cardinal world instance. ECS remains authoritative; this struct is disposable and rebuilt from components when needed.
func NewPhysicsRuntime ¶
func NewPhysicsRuntime() *PhysicsRuntime
NewPhysicsRuntime returns an empty runtime. Maps are initialized; Emitter is nil. 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 ¶
func Runtime() *PhysicsRuntime
Runtime returns the current package-scoped physics runtime. It does not create one lazily: callers must invoke ResetRuntime first; otherwise this returns nil.
func (*PhysicsRuntime) ActiveContactsToComponent ¶
func (rt *PhysicsRuntime) ActiveContactsToComponent() component.ActiveContacts
ActiveContactsToComponent converts the working map to the ECS component format (sorted slice for deterministic snapshots).
func (*PhysicsRuntime) LoadActiveContactsFromComponent ¶
func (rt *PhysicsRuntime) 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 (*PhysicsRuntime) PruneActiveContactsInvolvingEntity ¶
func (rt *PhysicsRuntime) 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.
func (*PhysicsRuntime) WorldExists ¶
func (rt *PhysicsRuntime) WorldExists() bool
WorldExists reports whether the C-side Box2D world has been created and is alive.
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 slice backing with live ECS data so in-place component edits do not corrupt the snapshot.
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 Vertices / ChainPoints are cloned).
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 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.