Skip to content

Contacts & events

For gameplay reactions—pickups, damage zones, bumping into walls—you usually implement per-entity contact events: enter/stay/exit hooks on the entity types that should care, instead of one central function that switches on every pair.

Call EnableContactEvents once during setup (after SetCollisionBackend). Then implement any of the six opt-in interfaces on your entity types; the server dispatches them from the same contact list the backend produces each tick.

srv.SetCollisionBackend(backend)
srv.EnableContactEvents()

EnableContactEvents requires a collision backend; without one no contacts are produced and no events fire.

These per-entity enter/stay/exit interfaces are for 2D collision backends. The 3D collision MVP reports contacts through OnContact3D; see 3D collision.

InterfaceMethodWhen called
golem.TriggerEnterOnTriggerEnter(other Entity)First tick two trigger shapes overlap
golem.TriggerStayOnTriggerStay(other Entity)Every subsequent tick they remain overlapping
golem.TriggerExitOnTriggerExit(other Entity)First tick they no longer overlap
golem.CollisionEnterOnCollisionEnter(other Entity, normal CollisionVec2, depth float64)First tick two solid shapes overlap
golem.CollisionStayOnCollisionStay(other Entity, normal CollisionVec2, depth float64)Every subsequent tick they remain overlapping
golem.CollisionExitOnCollisionExit(other Entity)First tick they no longer overlap

Implement only the interfaces you need — unused ones have no overhead.

normal in CollisionEnter and CollisionStay is a unit vector pointing away from other toward the receiver. Both sides receive the correct outward normal: entity A gets the raw contact normal; entity B gets the negated vector. This matches the per-collider convention from Unity.

type Player struct{ *synced.SyncedPlayer }
func (p *Player) OnTriggerEnter(other golem.Entity) {
if pickup, ok := other.(*Pickup); ok {
pickup.Collect(p)
}
}
func (p *Player) OnCollisionEnter(other golem.Entity, normal golem.CollisionVec2, depth float64) {
// normal points away from other, toward p
log.Printf("player bumped into %T (depth=%.2f)", other, depth)
}

other is nil when the partner entity was removed from the registry before the event fires (typically in the same tick). This most commonly occurs in OnTriggerExit and OnCollisionExit. Always guard against nil if your callback dereferences other:

func (p *Player) OnTriggerExit(other golem.Entity) {
if other == nil {
return // partner was deleted
}
// ...
}

OnTriggerStay fires every tick for every active overlap, not just on transitions. For large numbers of simultaneously overlapping entities this scales linearly with overlap count. Prefer OnTriggerEnter/OnTriggerExit when you only need to know when the state changes.

If you prefer one function that receives every overlapping pair—for a quick prototype, logging, or a small game with simple global rules—register OnContact. It does not replace per-entity events; it runs in addition when both are set. It does not require EnableContactEvents.

Each golem.CollisionContact has:

  • A, B — the two entity IDs involved.
  • Normal — a unit vector pointing in the push-out direction for entity A (away from B).
  • Depth — the penetration distance. 0 for trigger contacts.
srv.OnContact(func(contacts []golem.CollisionContact) {
for _, c := range contacts {
if c.Depth == 0 {
// trigger overlap between c.A and c.B
handleTrigger(c.A, c.B)
} else {
// solid collision — c.Normal and c.Depth describe the push
handleBump(c.A, c.B, c.Normal, c.Depth)
}
}
})

OnContact is only called when there is at least one contact. If nothing overlaps in a given tick, your handler is not called.

Previous: Colliders. Next: Chipmunk (cp) backend, Overlap & cast queries, 3D collision.