Implement collision detection and response in this engine. Use when user asks about collisions, triggers, physics, collision layers, OBB collision, or needs entities to interact physically...
This skill guides implementation of collision detection and response using the engine's OBB (Oriented Bounding Box) collision system.
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)
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()
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 |
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)
The engine adds a Colliding component to entities that have active collisions:
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)
)
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
}
}
Triggers detect overlap without physical response:
// 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:
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)
}
}
}
}
For solid collisions, implement movement that respects obstacles:
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()
}
}
}
For smoother movement, slide along surfaces:
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()
}
}
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()
}
}
}
}
Enable debug drawing in collider:
collider.DebugDraw = true
Or draw custom visualization in a system:
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)
}
}
Enter = first frame, Stay = ongoing, Exit = last frame (separated)IsTrigger=true for detection only, false for physical obstaclesSee existing collision code:
engine/component/collider.goengine/collision/obb.goengine/core/system/collision.gogame/system/trigger.gogame/system/bullet_collision.go