Collision Detection
The math2d package provides AABB collision detection and resolution for 2D games.
import "github.com/drpaneas/gosprite64/math2d"
AABB collision functions
All functions operate on math2d.Rect values - axis-aligned bounding boxes defined by position and size. See Rectangles for the full Rect API.
AABBOverlap
Returns true if two rectangles overlap:
player := math2d.Rect{X: 50, Y: 50, W: 16, H: 24}
coin := math2d.Rect{X: 55, Y: 48, W: 8, H: 8}
if math2d.AABBOverlap(player, coin) {
collectCoin()
}
AABBPenetration
Returns the minimum translation vector (MTV) needed to push rect a out of rect b. The MTV points along the axis of least overlap. Returns false if there is no overlap.
pen, ok := math2d.AABBPenetration(player, wall)
if ok {
// pen is a Vec2 that moves 'player' out of 'wall'
player.X += pen.X
player.Y += pen.Y
}
The MTV always pushes along a single axis (the shorter overlap), so either pen.X or pen.Y will be zero.
AABBResolve
A convenience function that returns a copy of rect a moved so it no longer overlaps b:
playerRect = math2d.AABBResolve(playerRect, wallRect)
This is equivalent to computing the penetration vector and adding it to a's position.
AABBSweep
Performs a swept (continuous) collision test. Moves rect a by a velocity vector and checks for collision with static rect b. Returns three values:
hit- whether a collision occurred during the sweept- the fraction of velocity at first contact (0.0 to 1.0)normal- the surface normal at the collision point
func AABBSweep(a Rect, vel Vec2, b Rect) (hit bool, t float32, normal Vec2)
vel := math2d.Vec2{X: 5, Y: 0}
hit, t, normal := math2d.AABBSweep(player, vel, wall)
if hit {
// Move only up to the contact point
player.X += vel.X * t
player.Y += vel.Y * t
// Slide along the wall using the normal
if normal.X != 0 {
vel.X = 0 // hit a vertical wall, stop horizontal movement
}
if normal.Y != 0 {
vel.Y = 0 // hit a horizontal wall, stop vertical movement
}
}
If the two rects already overlap before the sweep, it returns (true, 0, {0,0}).
Layers and colliders
For games with many object types (player, enemies, projectiles, pickups), use layers to control which objects can collide with each other.
Layer
Layer is a uint32 bitmask. Define your game's layers as powers of two:
const (
LayerPlayer math2d.Layer = 1 << 0 // 0x01
LayerEnemy math2d.Layer = 1 << 1 // 0x02
LayerProjectile math2d.Layer = 1 << 2 // 0x04
LayerPickup math2d.Layer = 1 << 3 // 0x08
)
Two built-in constants:
math2d.LayerNone(0x00000000) - matches nothingmath2d.LayerAll(0xFFFFFFFF) - matches everything
Use Matches to test whether two layers share any bits:
a := LayerPlayer | LayerEnemy
b := LayerEnemy
a.Matches(b) // true - both include LayerEnemy
Collider
A Collider combines a bounding box with layer membership:
type Collider struct {
Bounds Rect // the AABB
Layer Layer // what this object IS
Mask Layer // what this object COLLIDES WITH
}
Layeridentifies the object's type.Maskdefines which layers this object reacts to.
playerCol := math2d.Collider{
Bounds: math2d.Rect{X: 50, Y: 50, W: 16, H: 24},
Layer: LayerPlayer,
Mask: LayerEnemy | LayerPickup, // collide with enemies and pickups
}
enemyCol := math2d.Collider{
Bounds: math2d.Rect{X: 80, Y: 50, W: 16, H: 16},
Layer: LayerEnemy,
Mask: LayerPlayer | LayerProjectile, // collide with player and bullets
}
ColliderOverlap
Tests both spatial overlap and layer compatibility. The check is bidirectional - either collider's mask matching the other's layer is sufficient:
if math2d.ColliderOverlap(playerCol, enemyCol) {
player.TakeDamage()
}
Two objects only collide if their bounding boxes overlap AND at least one of them has a mask that includes the other's layer.
Platformer collision example
const (
LayerPlayer math2d.Layer = 1 << 0
LayerSolid math2d.Layer = 1 << 1
LayerCoin math2d.Layer = 1 << 2
)
type Entity struct {
Pos math2d.Vec2
Size math2d.Vec2
Vel math2d.Vec2
Collider math2d.Collider
}
func (e *Entity) Bounds() math2d.Rect {
return math2d.Rect{X: e.Pos.X, Y: e.Pos.Y, W: e.Size.X, H: e.Size.Y}
}
func (g *Game) Update() {
p := &g.player
// Apply gravity
p.Vel.Y += 0.5
// Sweep against all solid tiles
bounds := p.Bounds()
for _, wall := range g.walls {
hit, t, normal := math2d.AABBSweep(bounds, p.Vel, wall)
if hit {
// Move up to contact
p.Vel = math2d.Vec2{
X: p.Vel.X * t,
Y: p.Vel.Y * t,
}
// Cancel velocity into the wall
if normal.Y < 0 {
p.Vel.Y = 0
p.OnGround = true
}
if normal.Y > 0 {
p.Vel.Y = 0 // hit ceiling
}
if normal.X != 0 {
p.Vel.X = 0 // hit side wall
}
}
}
// Apply velocity
p.Pos.X += p.Vel.X
p.Pos.Y += p.Vel.Y
// Check coin pickups (simple overlap, no physics)
playerCol := math2d.Collider{
Bounds: p.Bounds(),
Layer: LayerPlayer,
Mask: LayerCoin,
}
for i, coin := range g.coins {
coinCol := math2d.Collider{
Bounds: coin,
Layer: LayerCoin,
Mask: LayerPlayer,
}
if math2d.ColliderOverlap(playerCol, coinCol) {
g.score++
g.coins = append(g.coins[:i], g.coins[i+1:]...)
break
}
}
}