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.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.
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.{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "tiny-engine",
"strict": true,
"verbatimModuleSyntax": true,
"moduleResolution": "bundler",
"target": "ES2022"
}
}
verbatimModuleSyntax is required by the engine’s own type exports. Make
sure your project uses import type for type-only imports.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.import { Game } from 'tiny-engine'
const root = document.querySelector<HTMLElement>('#root')!
Game.setup({
width: 192,
height: 112,
root,
})
The canvas is sized in logical pixels. tiny-engine handles device pixel ratio scaling internally — your coordinates always work in the logical space.
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.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>
)
}
loadTexture is async — use top-level await in your scene module (requires "target": "ES2022" or higher in tsconfig).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.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>
)
}
The
grayscale and brightness props are only re-evaluated when the frame is drawn — they do not trigger re-renders or node reconstruction.Add the scene to
Game.sceneManager, then call Game.play. Scenes can be registered before or after Game.play is called.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.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()
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
src/main.ts
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
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.