AgentSkillsCN

engine-collision

在此引擎中实现碰撞检测与响应机制。当用户询问有关碰撞、触发器、物理模拟、碰撞层、OBB 碰撞,或需要让实体之间产生真实的物理交互时,可选用此技能。涵盖 Collider 组件、碰撞数据以及相应的响应模式。

SKILL.md
--- frontmatter
name: engine-collision
description: 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. Covers Collider component, Colliding data, and response patterns.

Collision System Guide

This skill guides implementation of collision detection and response using the engine's OBB (Oriented Bounding Box) collision system.

Architecture Overview

  1. Collider Component: Defines collision volume (size, offset, layers)
  2. CollisionSystem (engine): Runs each frame, detects overlaps using SAT algorithm
  3. Colliding Component: Auto-added by engine with contact data
  4. 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.

FieldPurpose
LayerWhich layer this entity belongs to (0-31)
LayerMaskBitmask 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

  1. System Order: CollisionSystem runs after your game systems but before TransformSystem
  2. Colliding Component: Only present if entity has active collisions this frame
  3. Contact State: Enter = first frame, Stay = ongoing, Exit = last frame (separated)
  4. OBB vs AABB: This engine uses oriented bounding boxes, not axis-aligned
  5. Layer Masks: Use bitwise operations for layer filtering
  6. Trigger vs Solid: IsTrigger=true for detection only, false for 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