Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/sanchedev/tiny-engine/llms.txt

Use this file to discover all available pages before exploring further.

tiny-engine’s collision system pairs a spatial hash broadphase with a precise narrowphase detector to keep collision checks efficient even in dense scenes. Colliders register themselves automatically when they start, and the system is updated each frame by Game.loop() — you never call the collision pipeline directly.

Shapes

The shapes factory creates typed shape objects that describe a collider’s geometry.

Rectangle

import { shapes } from 'tiny-engine'

<collider shape={shapes.rectangle(32, 32)} group={['player']} collidesWith={['enemy']} />
shapes.rectangle(width, height) returns a RectangleShape:
interface RectangleShape {
  type: 'rectangle'
  size: Vector2   // { x: width, y: height }
}

Circle

<collider shape={shapes.circle(16)} group={['projectile']} collidesWith={['zombie']} />
shapes.circle(radius) returns a CircleShape:
interface CircleShape {
  type: 'circle'
  radius: number
}
The Shape union type is discriminated by shape.type:
type Shape = RectangleShape | CircleShape

function getArea(shape: Shape): number {
  if (shape.type === 'rectangle') {
    return shape.size.x * shape.size.y
  }
  return Math.PI * shape.radius * shape.radius
}

Collider JSX

<collider
  shape={shapes.rectangle(24, 24)}
  group={['player']}
  collidesWith={['enemy', 'wall']}
/>
PropTypeDescription
shapeShapeThe collision geometry. Required.
groupstring[]Group labels this collider belongs to.
collidesWithstring[]Group labels this collider should test against.

Collision events

Events fire on both participating colliders each frame. Subscribe with useEvent:
EventPayloadDescription
colliderEnteredColliderFires the first frame two colliders overlap.
collidedColliderFires every frame while the overlap continues.
colliderExitedColliderFires the first frame the overlap ends.
import { useEvent, useRefNode } from 'tiny-engine/hooks'
import { PrimaryNode } from 'tiny-engine'

function Player() {
  const collider = useRefNode(PrimaryNode.Collider)

  useEvent(collider, 'colliderEntered', (other) => {
    console.log('Started overlapping with', other)
  })

  useEvent(collider, 'collided', (other) => {
    // Runs every frame while touching
  })

  useEvent(collider, 'colliderExited', (other) => {
    console.log('Stopped overlapping with', other)
  })

  return (
    <collider
      ref={collider}
      shape={shapes.rectangle(24, 24)}
      group={['player']}
      collidesWith={['enemy']}
    />
  )
}

Full example: Projectile with damage

import { useEvent, useRefNode } from 'tiny-engine/hooks'
import { PrimaryNode, shapes } from 'tiny-engine'

function Projectile() {
  const collider = useRefNode(PrimaryNode.Collider)

  useEvent(collider, 'colliderEntered', (enemyCollider) => {
    // Access the enemy's script and apply damage
    enemyCollider.parent.script.applyDamage(20)
    // Destroy the projectile on first hit
    collider.node.destroy()
  })

  return (
    <collider
      ref={collider}
      shape={shapes.circle(4)}
      group={['projectile']}
      collidesWith={['enemy']}
    />
  )
}
The colliderEntered callback receives the other Collider node. From there, you can walk collider.parent to reach the owning node and call methods on its attached script.

Raycasting

A <ray-cast> node projects a ray in a given direction and tracks which collider it first hits.
import { useEvent, useRefNode } from 'tiny-engine/hooks'
import { PrimaryNode, Vector2 } from 'tiny-engine'

function EnemySensor() {
  const ray = useRefNode(PrimaryNode.RayCast)

  useEvent(ray, 'colliderEntered', (collider) => {
    console.log('Ray hit:', collider)
  })

  useEvent(ray, 'colliderExited', (collider) => {
    console.log('Ray left:', collider)
  })

  return (
    <ray-cast
      ref={ray}
      direction={new Vector2(100, 0)}
      collidesWith={['enemy']}
    />
  )
}
PropTypeDescription
directionVector2The direction vector of the ray. Its magnitude determines the cast length.
collidesWithstring[]Group labels the ray should detect.

Ray-cast events and methods

MemberDescription
colliderEnteredFires when the ray begins hitting a new collider.
colliderExitedFires when the ray stops hitting the current collider.
getCollider()Returns the currently detected Collider, or null if the ray hits nothing.
The ray always tracks only the nearest collider in its collidesWith groups. If a closer collider enters the ray path, the old one receives a colliderExited event and the new one receives colliderEntered.

Group filtering

group declares which logical layers a collider belongs to. collidesWith declares which layers it tests against. A collision is considered only when collider A’s collidesWith contains at least one group present in collider B’s group.
// This projectile tests against 'enemy' only
<collider shape={shapes.circle(4)} group={['projectile']} collidesWith={['enemy']} />

// This enemy tests against 'player' and 'projectile'
<collider shape={shapes.rectangle(16, 16)} group={['enemy']} collidesWith={['player', 'projectile']} />
You can give a collider multiple group labels to let different collider types interact with it:
<collider shape={shapes.rectangle(16, 16)} group={['wall', 'obstacle']} collidesWith={[]} />

How the spatial hash works

The spatial hash broadphase divides the world into a fixed-size grid (cell size 64). Each frame, every registered collider is inserted into the cells it overlaps — based on its AABB — and only pairs sharing a cell proceed to narrowphase shape testing. Raycasts bypass the spatial hash entirely. They query the #colliderGroups map directly, which indexes colliders by group string, and then run a per-shape distance test to find the nearest hit.
group and collidesWith are set at construction time and are immutable afterwards. If you need dynamic layer membership, spawn a new collider node with the updated groups.

Build docs developers (and LLMs) love