Skip to content

Sprite Collisions

Tecs can automatically create physics collision shapes from Aseprite slices, allowing you to define collision boxes visually and have them applied to physics bodies.

Quick Start

teal
local tecs = require("tecs")
local physics = require("tecs2d.physics")
local gfx = require("tecs2d.gfx")

local world = tecs.newWorld()

-- Initialize physics
love.physics.setMeter(64)
world:addPlugin(physics.new({ world = love.physics.newWorld(0, 800, true) }))

-- Register a collision template
gfx.SpriteCollision.addTemplate("PLAYER", {
    shape = "circle",
    restitution = 0.0,
    friction = 1.0,
})

-- Spawn sprite with collision from slice
world:spawn(
    tecs.builtins.Transform(100, 100),
    gfx.Sprite.fromAseprite("player.png", "idle"),
    gfx.SpriteCollision.new({
        bodyType = "dynamic",
        fixedRotation = true,  -- default for SpriteCollision dynamics
        slices = {
            collision = "PLAYER"  -- Use "collision" slice with PLAYER template
        }
    })
)

Defining Collision Slices in Aseprite

  1. Open your sprite in Aseprite
  2. Create a slice named "collision" (or any name you prefer)
  3. Position and size the slice to define your collision box
  4. Export with Slices enabled

Platformer Characters

For platformer characters, place the collision slice at the character's feet (lower half). This prevents the head from catching on platforms.

SpriteCollision Component

teal
gfx.SpriteCollision.new({
    bodyType = "dynamic",  -- or "static"
    fixedRotation = true,  -- dynamic only; defaults to true
    slices = {
        collision = "TEMPLATE_NAME",  -- Slice name → template name
        hitbox = "HITBOX",            -- Multiple slices supported
    }
})

Configuration

PropertyTypeDescription
bodyTypestring"dynamic" or "static"
fixedRotationbooleanKeeps auto-attached dynamic bodies upright. Defaults to true.
slicestableMap of slice names to templates or inline configs

Body Types

TypeDescriptionUse For
"dynamic"Affected by forces and gravityPlayers, enemies, projectiles
"static"ImmovablePlatforms, walls, terrain

Collision Templates

Templates define reusable physics configurations. Using templates enables serialization since template names are stored instead of full configs.

Registering Templates

teal
-- Register at startup
gfx.SpriteCollision.addTemplate("PLAYER", {
    shape = "circle",
    restitution = 0.0,
    friction = 1.0,
})

gfx.SpriteCollision.addTemplate("BOUNCY", {
    shape = "circle",
    restitution = 0.8,
    friction = 0.3,
})

gfx.SpriteCollision.addTemplate("TRIGGER", {
    shape = "rectangle",
    sensor = true,  -- Detects overlaps without collision
})

Template Properties

PropertyTypeDefaultDescription
shapestring"rectangle""circle" or "rectangle"
sensorbooleanfalseDetect overlaps without physical collision
restitutionnumber0Bounciness (0 = none, 1 = full)
frictionnumber0.3Surface friction
categoriesinteger0xFFFFCollision category bitmask
maskinteger0xFFFFCollision mask bitmask
groupIndexinteger0Group index (-N never collides with same group)

Retrieving Templates

teal
local template = gfx.SpriteCollision.getTemplate("PLAYER")

Inline Configuration

Instead of templates, define collision properties inline:

teal
gfx.SpriteCollision.new({
    bodyType = "dynamic",
    slices = {
        collision = {
            shape = "circle",
            restitution = 0.0,
            friction = 1.0,
        }
    }
})

Multiple Slices

Define multiple shapes from different slices:

teal
gfx.SpriteCollision.addTemplate("BODY", {
    shape = "circle",
    restitution = 0.0,
})

gfx.SpriteCollision.addTemplate("HITBOX", {
    shape = "rectangle",
    sensor = true,  -- No physical collision
})

world:spawn(
    tecs.builtins.Transform(100, 100),
    gfx.Sprite.fromAseprite("player.png", "idle"),
    gfx.SpriteCollision.new({
        bodyType = "dynamic",
        fixedRotation = true,
        slices = {
            collision = "BODY",   -- Physical collision
            hitbox = "HITBOX",    -- Damage detection sensor
        }
    })
)

Shape Calculation

Circle Shapes

For circles, the radius is calculated as min(width, height) / 2 from the slice bounds.

Rectangle Shapes

Rectangles use the full width and height of the slice bounds.

Offset Calculation

Shape positions are offset from the sprite's geometric center:

offsetX = (slice.x + slice.width/2) - (spriteWidth/2)
offsetY = (slice.y + slice.height/2) - (spriteHeight/2)

A slice centered on the sprite has offset (0, 0). A slice at the sprite's feet has a positive Y offset.

Sensors

Sensors detect overlaps without causing physical collision:

teal
gfx.SpriteCollision.addTemplate("PICKUP_ZONE", {
    shape = "circle",
    sensor = true,
})

Use cases:

  • Hitboxes for combat
  • Trigger zones for events
  • Pickup detection for items
  • Ground detection for jumping

Required Components

SpriteCollision expects these systems/components to be available:

ComponentPurpose
SpriteProvides slice data
physics pluginCreates Box2D bodies

When physics is available, SpriteCollision auto-attaches a default physics.Collider plus either physics.RigidBody or physics.StaticBody based on bodyType.

teal
world:spawn(
    tecs.builtins.Transform(x, y),
    gfx.Sprite.fromAseprite("player.png", "idle"),
    gfx.SpriteCollision.new({
        bodyType = "dynamic",
        fixedRotation = true,  -- optional; default is true
        slices = { ... },
    })
)

Collision Events

Handle collision events using the physics event system:

teal
world:observe(playerId, physics.BeginContact, function(event: physics.BeginContact)
    print("Collided with:", event.other)
    -- Access slice name via shape user data
    local data = event.shape:getUserData()
    if data then print("Slice:", data.name) end
end)

world:observe(playerId, physics.EndContact, function(event)
    print("Stopped colliding with:", event.other)
end)

Limitations

First Frame Only

Collision shapes are created from the slice position in the first frame of the current animation tag. If your slice has per-frame keys, only the first frame is used. For animated collision shapes, you'll need to update shapes manually.

Example: Platformer Character

teal
gfx.SpriteCollision.addTemplate("PLAYER_BODY", {
    shape = "circle",
    restitution = 0.0,
    friction = 0.0,  -- Low friction for responsive movement
})

gfx.SpriteCollision.addTemplate("GROUND_SENSOR", {
    shape = "rectangle",
    sensor = true,
})

local playerId = world:spawn(
    tecs.builtins.Transform(100, 300),
    gfx.Sprite.fromAseprite("player.png", "idle"),
    gfx.SpriteCollision.new({
        bodyType = "dynamic",
        fixedRotation = true,
        slices = {
            collision = "PLAYER_BODY",
            feet = "GROUND_SENSOR",
        }
    })
)

-- Track ground contact
local onGround = false
world:observe(playerId, physics.BeginContact, function(event: physics.BeginContact)
    local data = event.shape:getUserData()
    if data and data.name == "feet" then
        onGround = true
    end
end)
world:observe(playerId, physics.EndContact, function(event: physics.EndContact)
    local data = event.shape:getUserData()
    if data and data.name == "feet" then
        onGround = false
    end
end)