Collision System Guide
This skill guides implementation of collision detection and response using the engine's OBB (Oriented Bounding Box) collision system.
Architecture Overview
- •Collider Component: Defines collision volume (size, offset, layers)
- •CollisionSystem (engine): Runs each frame, detects overlaps using SAT algorithm
- •Colliding Component: Auto-added by engine with contact data
- •Your Systems: Query for Colliding component and respond
Adding a Collider to an Entity
Via Scene API
go
colliderData := component.ColliderData{
Size: math.NewVector3(1, 1, 1), // Dimensions of OBB
Offset: math.Vector3Zero(), // Local space offset from transform
Layer: 0, // This entity's layer (0-31)
LayerMask: 0xFFFFFFFF, // Which layers to collide with (all)
IsTrigger: false, // true = detection only, false = solid
DebugDraw: true, // Show wireframe in gizmo phase
}
scene.SetCollider(entityId, colliderData)
Via Entity Builder
go
entityId := scene.Spawn("Player").
At(math.NewVector3(0, 1, 0)).
Mesh("cube", "blue").
Collider(
math.NewVector3(1, 2, 1), // Size
math.NewVector3(0, 1, 0), // Offset (centered at feet)
).
Build()
Layer System
Layers control which entities can collide with each other.
| Field | Purpose |
|---|---|
Layer | Which layer this entity belongs to (0-31) |
LayerMask | Bitmask of layers this entity can collide with |
Layer Examples
go
const (
LayerPlayer = 0
LayerEnemy = 1
LayerBullet = 2
LayerPickup = 3
LayerWall = 4
)
// Player collides with enemies, pickups, walls
playerCollider.Layer = LayerPlayer
playerCollider.LayerMask = (1 << LayerEnemy) | (1 << LayerPickup) | (1 << LayerWall)
// Bullet collides with enemies and walls only
bulletCollider.Layer = LayerBullet
bulletCollider.LayerMask = (1 << LayerEnemy) | (1 << LayerWall)
// Enemy collides with player and bullets
enemyCollider.Layer = LayerEnemy
enemyCollider.LayerMask = (1 << LayerPlayer) | (1 << LayerBullet)
Reading Collision Data
The engine adds a Colliding component to entities that have active collisions:
go
type CollidingData struct {
Contacts []ContactData
}
type ContactData struct {
Point math.Vector3 // World-space contact point
Normal math.Vector3 // Surface normal (pointing out of other)
PenetrationDepth float32 // Overlap distance
OtherEntity uint64 // Entity ID we collided with
State CollisionState // Enter, Stay, or Exit
}
const (
CollisionStateEnter = iota // First frame of contact
CollisionStateStay // Ongoing contact
CollisionStateExit // Last frame (no longer touching)
)
Collision Response System
go
type CollisionResponseSystem struct{}
func (s *CollisionResponseSystem) Update(world donburi.World, scene *engineScene.Scene, dt float32) {
query := donburi.NewQuery(filter.Contains(
engineComponent.Transform,
engineComponent.Colliding,
gameComponent.Health,
))
for entry := range query.Iter(world) {
colliding := engineComponent.Colliding.Get(entry)
health := gameComponent.Health.Get(entry)
for _, contact := range colliding.Contacts {
switch contact.State {
case component.CollisionStateEnter:
// First frame of collision
s.onCollisionEnter(world, entry, contact)
case component.CollisionStateStay:
// Ongoing collision
s.onCollisionStay(world, entry, contact)
case component.CollisionStateExit:
// Collision ended
s.onCollisionExit(world, entry, contact)
}
}
}
}
func (s *CollisionResponseSystem) onCollisionEnter(world donburi.World, entry *donburi.Entry, contact component.ContactData) {
otherEntry := world.Entry(donburi.Entity(contact.OtherEntity))
// Check if other has damage component
if otherEntry.HasComponent(gameComponent.Damage) {
dmg := gameComponent.Damage.Get(otherEntry)
health := gameComponent.Health.Get(entry)
health.Current -= dmg.Amount
}
}
Trigger Zone Pattern
Triggers detect overlap without physical response:
go
// Create trigger zone
trigger := scene.Spawn("DoorTrigger").
At(math.NewVector3(5, 1, 0)).
Scale(math.NewVector3(3, 2, 3)).
Build()
scene.SetCollider(trigger, component.ColliderData{
Size: math.NewVector3(3, 2, 3),
IsTrigger: true, // Detection only
Layer: LayerTrigger,
LayerMask: 1 << LayerPlayer,
})
scene.AddComponent(trigger, gameComponent.DoorTrigger, gameComponent.DoorTriggerData{
DoorEntity: doorEntityId,
})
Trigger response system:
go
func (s *TriggerSystem) Update(world donburi.World, scene *engineScene.Scene, dt float32) {
query := donburi.NewQuery(filter.Contains(
engineComponent.Colliding,
gameComponent.DoorTrigger,
))
for entry := range query.Iter(world) {
colliding := engineComponent.Colliding.Get(entry)
doorTrigger := gameComponent.DoorTrigger.Get(entry)
for _, contact := range colliding.Contacts {
if contact.State == component.CollisionStateEnter {
// Player entered trigger zone
s.openDoor(scene, doorTrigger.DoorEntity)
}
if contact.State == component.CollisionStateExit {
// Player left trigger zone
s.closeDoor(scene, doorTrigger.DoorEntity)
}
}
}
}
Movement with Collision
For solid collisions, implement movement that respects obstacles:
go
func (s *MovementSystem) Update(world donburi.World, scene *engineScene.Scene, dt float32) {
query := donburi.NewQuery(filter.Contains(
engineComponent.Transform,
engineComponent.Collider,
gameComponent.Movement,
))
for entry := range query.Iter(world) {
transform := engineComponent.Transform.Get(entry)
collider := engineComponent.Collider.Get(entry)
movement := gameComponent.Movement.Get(entry)
// Calculate desired movement
moveVector := movement.Direction.Scale(movement.Speed * dt)
newPos := transform.Position.Add(moveVector)
// Build OBB at new position for predictive check
newOBB := collision.BuildObbAtPosition(collider, transform, newPos)
// Check collision with all potential obstacles
hasCollision := false
obstacleQuery := donburi.NewQuery(filter.Contains(
engineComponent.Transform,
engineComponent.Collider,
))
for otherEntry := range obstacleQuery.Iter(world) {
if otherEntry.Entity() == entry.Entity() {
continue // Skip self
}
otherTransform := engineComponent.Transform.Get(otherEntry)
otherCollider := engineComponent.Collider.Get(otherEntry)
if otherCollider.IsTrigger {
continue // Ignore triggers
}
otherOBB := collision.TransformToOBB(otherTransform, otherCollider)
if collision.TestOBBOverlap(newOBB, otherOBB) {
hasCollision = true
break
}
}
if !hasCollision {
transform.Position = newPos
transform.MarkDirty()
}
}
}
Slide Along Walls
For smoother movement, slide along surfaces:
go
func slideAlongSurface(moveVector, normal math.Vector3) math.Vector3 {
// Project movement onto plane perpendicular to normal
dot := moveVector.Dot(normal)
return moveVector.Subtract(normal.Scale(dot))
}
// Usage in movement system:
if hasCollision {
// Try sliding along the collision surface
slideVector := slideAlongSurface(moveVector, contact.Normal)
slidePos := transform.Position.Add(slideVector)
// Check if slide position is valid...
if !checkCollision(slidePos) {
transform.Position = slidePos
transform.MarkDirty()
}
}
Pushing Entities
go
func (s *PushSystem) Update(world donburi.World, scene *engineScene.Scene, dt float32) {
query := donburi.NewQuery(filter.Contains(
engineComponent.Transform,
engineComponent.Colliding,
gameComponent.Pushable,
))
for entry := range query.Iter(world) {
transform := engineComponent.Transform.Get(entry)
colliding := engineComponent.Colliding.Get(entry)
for _, contact := range colliding.Contacts {
if contact.State == component.CollisionStateStay {
// Push away from penetration
pushDir := contact.Normal.Scale(-1) // Opposite of collision normal
pushAmount := contact.PenetrationDepth * 0.5
transform.Position = transform.Position.Add(
pushDir.Scale(pushAmount),
)
transform.MarkDirty()
}
}
}
}
Collision Debug Visualization
Enable debug drawing in collider:
go
collider.DebugDraw = true
Or draw custom visualization in a system:
go
func (s *CollisionDebugSystem) Render(phase sys.RenderPhase, world donburi.World, scene *engineScene.Scene) {
if phase != sys.RenderPhaseGizmos {
return
}
query := donburi.NewQuery(filter.Contains(
engineComponent.Transform,
engineComponent.Collider,
))
for entry := range query.Iter(world) {
transform := engineComponent.Transform.Get(entry)
collider := engineComponent.Collider.Get(entry)
obb := collision.TransformToOBB(transform, collider)
// Draw OBB wireframe
color := rl.Green
if entry.HasComponent(engineComponent.Colliding) {
color = rl.Red // Highlight active collisions
}
math.DrawOBBWireframe(obb.ToBoundingBox(), transform.WorldMatrix, color)
}
}
Important Notes
- •System Order: CollisionSystem runs after your game systems but before TransformSystem
- •Colliding Component: Only present if entity has active collisions this frame
- •Contact State:
Enter= first frame,Stay= ongoing,Exit= last frame (separated) - •OBB vs AABB: This engine uses oriented bounding boxes, not axis-aligned
- •Layer Masks: Use bitwise operations for layer filtering
- •Trigger vs Solid:
IsTrigger=truefor detection only,falsefor physical obstacles
Example Implementations
See existing collision code:
- •Collider component:
engine/component/collider.go - •Collision detection:
engine/collision/obb.go - •Collision system:
engine/core/system/collision.go - •Trigger system:
game/system/trigger.go - •Bullet collision:
game/system/bullet_collision.go