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.

This guide walks you through a complete tiny-engine setup from a blank project to a working interactive scene. By the end you will have a canvas running at 60FPS with a reactive component that responds to clicks.
1
Install the package
2
pnpm
pnpm add tiny-engine
npm
npm install tiny-engine
yarn
yarn add tiny-engine
3
Configure TypeScript
4
Add the custom JSX runtime to your tsconfig.json. The jsxImportSource setting tells TypeScript to resolve JSX transforms from tiny-engine/jsx-runtime instead of React.
5
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "tiny-engine",
    "strict": true,
    "verbatimModuleSyntax": true,
    "moduleResolution": "bundler",
    "target": "ES2022"
  }
}
6
verbatimModuleSyntax is required by the engine’s own type exports. Make sure your project uses import type for type-only imports.
7
Set up the canvas
8
Game.setup creates the <canvas> element, appends it to a root HTMLElement, and configures the game dimensions. Call this once before registering scenes or calling Game.play.
9
import { Game } from 'tiny-engine'

const root = document.querySelector<HTMLElement>('#root')!

Game.setup({
  width: 192,
  height: 112,
  root,
})
10
The canvas is sized in logical pixels. tiny-engine handles device pixel ratio scaling internally — your coordinates always work in the logical space.
11
Create your first scene
12
A scene is a Scene instance whose render function returns the root node of the scene graph. Export a JSX function component as the default export and tiny-engine’s renderToNodes will construct the node tree when the scene loads.
13
import { loadTexture } from 'tiny-engine'

const PLAYER_TEX = await loadTexture('/assets/player.png')

export default function GameScene() {
  return (
    <transform position={[16, 16]}>
      <sprite textureId={PLAYER_TEX} displaySize={[32, 32]} />
    </transform>
  )
}
14
loadTexture is async — use top-level await in your scene module (requires "target": "ES2022" or higher in tsconfig).
15
Add a reactive component with useSignal
16
useSignal returns a getter/setter pair. Pass the getter as an inline function to any JSX attribute and it becomes an auto-computed prop: the engine re-reads the function every frame and applies the result to the node property without any diffing.
17
import { loadTexture } from 'tiny-engine'
import { useSignal } from 'tiny-engine/hooks'

const PLAYER_TEX = await loadTexture('/assets/player.png')

export default function GameScene() {
  const [health, setHealth] = useSignal(100)

  return (
    <transform position={[16, 16]}>
      <sprite
        textureId={PLAYER_TEX}
        displaySize={[32, 32]}
        grayscale={() => (health() <= 0 ? 1 : 0)}
        brightness={() => 0.5 + health() / 200}
      >
        <clickable
          size={[32, 32]}
          onClick={() => setHealth(health() - 10)}
        />
      </sprite>
    </transform>
  )
}
18
The grayscale and brightness props are only re-evaluated when the frame is drawn — they do not trigger re-renders or node reconstruction.
19
Register the scene and start the game
20
Add the scene to Game.sceneManager, then call Game.play. Scenes can be registered before or after Game.play is called.
21
Because Scene.render must return a Node (not a JSX function component), use renderToNodes from tiny-engine/jsx to convert the JSX component into the node the engine expects.
22
import { Game, Scene } from 'tiny-engine'
import { renderToNodes } from 'tiny-engine/jsx'

const root = document.querySelector<HTMLElement>('#root')!

Game.setup({
  width: 192,
  height: 112,
  root,
})

await Game.sceneManager.addScene(
  'main',
  new Scene(async () => {
    const { default: GameScene } = await import('./scenes/game-scene.js')
    return renderToNodes(GameScene())[0]
  }),
  true, // set as active scene immediately
)

Game.play()
23
The third argument to addScene is setit: when true, the scene is loaded and set as the current scene before the promise resolves.

Complete example

The following is a self-contained scene with a health bar player: reactive state, a sprite with computed filters, and a <clickable> that decrements health on each click.
src/scenes/player-scene.tsx
import { loadTexture } from 'tiny-engine'
import { useSignal, useEffect } from 'tiny-engine/hooks'

const PLAYER_TEX = await loadTexture('/assets/player.png')
const BAR_TEX = await loadTexture('/assets/health-bar.png')

export default function PlayerScene() {
  const [health, setHealth] = useSignal(100)
  const maxHealth = 100

  useEffect(() => {
    // Runs whenever health changes
    if (health() <= 0) {
      console.log('Player defeated!')
    }
  })

  return (
    <transform>
      {/* Health bar sprite — width scales with current health */}
      <sprite
        textureId={BAR_TEX}
        position={[8, 4]}
        displaySize={[() => (health() / maxHealth) * 48, 4]}
      />

      {/* Player sprite — grayscale when dead, dim when hurt */}
      <sprite
        textureId={PLAYER_TEX}
        position={[16, 16]}
        displaySize={[32, 32]}
        grayscale={() => (health() <= 0 ? 1 : 0)}
        brightness={() => 0.4 + (health() / maxHealth) * 0.6}
      >
        {/* Clickable area covers the full sprite */}
        <clickable
          size={[32, 32]}
          disabled={() => health() <= 0}
          onClick={() => setHealth(Math.max(0, health() - 10))}
        />
      </sprite>
    </transform>
  )
}
Wire it up from your entry point:
src/main.ts
import { Game, Scene } from 'tiny-engine'
import { renderToNodes } from 'tiny-engine/jsx'

const root = document.querySelector<HTMLElement>('#root')!

Game.setup({ width: 192, height: 112, root })

await Game.sceneManager.addScene(
  'player',
  new Scene(async () => {
    const { default: PlayerScene } = await import('./scenes/player-scene.js')
    return renderToNodes(PlayerScene())[0]
  }),
  true,
)

Game.play()

Alternative: JSX-first setup with createGame

If you prefer to keep all configuration in JSX, createGame from tiny-engine/jsx wraps Game.setup, scene registration, and Game.play into a single declarative call:
src/main.tsx
import { createGame, Game, Scene } from 'tiny-engine/jsx'

const game = createGame(
  <Game width={192} height={112} defaultScene="player">
    <Scene name="player" component={() => import('./scenes/player-scene.js')} />
  </Game>,
  document.querySelector<HTMLElement>('#root')!,
)
createGame returns a GameControls object with play, pause, changeScene, preloadScene, and getSize methods. Note that createGame calls Game.play() internally — you do not need to call it separately.
Pass testOptions={{ showClickables: true }} to Game.setup (or the Game JSX component) to render a semi-transparent overlay over every <clickable> area. This makes it easy to verify hit regions are positioned and sized correctly during development.
Game.setup({
  width: 192,
  height: 112,
  root,
  testOptions: { showClickables: true },
})

Build docs developers (and LLMs) love