Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/sanchedev/fraxel/llms.txt

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

As a game grows, keeping all logic inline with JSX components becomes difficult to maintain. FraxelScript provides a class-based alternative: write game logic — health systems, state machines, AI routines — in a plain TypeScript class that extends FraxelScript, then attach it to any JSX node via the script prop. The script receives a typed reference to its host node and can subscribe to the node’s lifecycle events through setup() and connect().
Use scripts for complex, stateful game objects like players, enemies, and bosses where you want reusable logic that is independent of how the node is rendered. For simple cases — a one-off animation or a short event handler — inline hooks (useMount, useEvent) are usually cleaner.

Creating a Script

Extend FraxelScript<PrimaryNode.X> where X is the PrimaryNode enum value matching the node type the script will be attached to. Override setup() to register event listeners and initialise state.
import { FraxelScript } from 'fraxel/scripts'
import { PrimaryNode } from 'fraxel'

class PlayerScript extends FraxelScript<PrimaryNode.Transform> {
  health = 100

  setup() {
    this.connect('started', () => {
      console.log('Player spawned!')
    })

    this.connect('destroyed', () => {
      console.log('Player destroyed!')
    })
  }

  applyDamage(amount: number) {
    this.health -= amount
    if (this.health <= 0) {
      this.me.destroy()
    }
  }
}
The type parameter (PrimaryNode.Transform) tells the compiler which node instance this.me returns and which event names connect() accepts — both are fully type-checked.

Attaching to a Node

Pass a script instance to the script prop on any JSX node. The node constructor calls script.init(node) and then script.setup() automatically.
function Player() {
  return (
    <transform script={new PlayerScript()}>
      <sprite textureId={PLAYER} />
      <collider shape={shapes.rectangle(16, 24)} group={['player']} collidesWith={['ground']} />
      <rigid-body />
    </transform>
  )
}
Each render of the component creates a new script instance. Scripts are not shared between component instances.

Accessing Scripts

Use the useScript hook to retrieve the script from a node reference. It returns a SignalGetter<T> — call it to get the current script instance.
import { useNode, useMount, useScript } from 'fraxel/hooks'
import { PrimaryNode } from 'fraxel'

function HUD() {
  const playerNode = useNode(PrimaryNode.Transform)
  const playerScript = useScript<PlayerScript>(playerNode)

  useMount(() => {
    // Retrieve the script and call a method on it
    const script = playerScript()
    if (script) {
      script.applyDamage(20)
    }
  })

  return (
    <transform>
      <transform ref={playerNode} script={new PlayerScript()}>
        <sprite textureId={PLAYER} />
      </transform>
    </transform>
  )
}
useScript returns a reactive getter, so you can use it inside useComputed or useEffect and it will re-run whenever the referenced node changes.

Script API

me

The me property returns the node instance the script is attached to. Its type is NodeInstances[T] — fully typed based on the generic parameter you supplied.
// Inside a FraxelScript<PrimaryNode.Transform>
this.me           // → Transform node instance
this.me.destroy() // → calls destroy() on the transform
this.me.position  // → Vector2
Accessing me before init() has been called throws a NodeNotInitializedError. In practice setup() is called immediately after init(), so me is always available inside setup() and any event callbacks registered there.

connect(eventName, callback)

connect is a type-safe wrapper around the node’s event system. The first argument is an event name string; the compiler infers the correct callback signature from the node type.
setup() {
  // 'started', 'destroyed', 'updated' are available on every node
  this.connect('started', () => {
    console.log('Node started!')
  })

  this.connect('destroyed', () => {
    console.log('Node destroyed!')
  })

  this.connect('updated', (delta) => {
    // delta: number — time since last frame in seconds
    console.log('Delta:', delta)
  })
}
For nodes with additional events (e.g. Collider’s colliderEntered), those event names are also accepted by connect() with correct callback types.

setup()

setup() is an abstract method — you must override it. It is called once, right after the script is bound to its node, before the first frame runs. It is the correct place to register event listeners and set up any long-lived subscriptions.

Game.destroy()

Call Game.destroy() to fully tear down the engine: stops the animation loop, releases the wake lock, removes all window event listeners, and destroys the current scene and all its nodes.
import { Game } from 'fraxel'

const game = Game // static class — no instance needed

// Later, when you want to remove the game entirely (e.g. navigating away):
Game.destroy()
Game.destroy() also calls Game.input.destroy() internally, ensuring all keyboard and pointer listeners are cleaned up. Omitting this call in single-page applications causes input listeners to accumulate across navigations. For the full node event reference, see API: Nodes (2D) and API: Nodes (Utility). For reactive hooks used alongside scripts, see API: Hooks (Core).

Build docs developers (and LLMs) love