# Writing a Cart

A cart is one React tree and one frame loop. Everything else on this page is detail.

If you have never written React, that is the thing to learn first. Components, JSX and hooks are assumed everywhere here, and [react.dev/learn](https://react.dev/learn) is the place to start. None of it is Unity specific, so any React you know already applies.

```tsx
import { useRef } from "react"
import { View, Text, mount, useFrame, input, type VisualElement } from "oj"

function Game() {
    const x = useRef(100)
    const ref = useRef<VisualElement>(null)

    useFrame((dt) => {
        x.current += input.keyboard.wasd().x * 300 * dt
        if (ref.current) ref.current.style.left = x.current
    })

    return (
        <View style={{ flexGrow: 1, backgroundColor: "#14181d" }}>
            <View ref={ref} style={{ position: "absolute", top: 100, width: 40, height: 40, backgroundColor: "#ffd166" }} />
            <Text style={{ color: "white" }}>Move with A and D</Text>
        </View>
    )
}

mount(<Game />)
```

What you draw with, and how styling and events work, is [Elements and Styling](/docs/elements-and-styling).

## `mount()`

`mount(<Game />)` is the entry point. The container already knows the root and the [stage](/docs/stage), so a cart plumbs neither through its tree.

```tsx
mount(<Game />)
mount(<Game />, { theme: false })   // skip the built-in control theme
```

`mount()` also styles the controls the runtime provides (`Button`, `Slider`, `TextField` and friends), because UI Toolkit's own runtime theme is built for the editor and looks out of place next to a cart. It touches only those controls, an inline style beats it, and `{ theme: false }` turns it off. It comes with you [into Unity](/docs/unity), since `mount()` applies it rather than the site.

## `useFrame()`

```tsx
useFrame((dt) => {
    // dt is seconds since the last frame
})
```

The callback runs every frame for as long as the component is mounted. `dt` is what makes a cart run at the same speed on a 60 Hz laptop and a 144 Hz monitor, so multiply movement by it rather than assuming a fixed step.

Clamp it if one slow frame would break your simulation. A backgrounded tab can hand you a `dt` of several seconds, enough for a fast object to pass through a wall:

```tsx
useFrame((dt) => {
    const step = Math.min(dt, 1 / 20)
    // ...
})
```

**Keep per-frame work out of React.** Setting state sixty times a second re-renders the tree sixty times a second; writing to `ref.current.style` does not. Drive motion through refs in `useFrame`, and let React handle what changes a few times a second, like a score or a menu.

The callback can read state and props directly. Each frame runs the callback from your latest render, so a comparison against a state value sees the current one. The optional second argument, a dependency list, only decides when the frame subscription is remade, and you will rarely need it.

## Input

Read the player through [`input`](https://onejs.com/docs/guides/input), the same module a normal OneJS project uses.

```tsx
if (input.keyboard.wasKeyPressed("Space")) jump()
const move = input.keyboard.wasd()
const p = input.mouse.position
for (const touch of input.touches) aimAt(touch.position)
```

Two things are specific to Play:

- **Positions arrive in window pixels**, the same numbers as [`useStage()`](/docs/stage) and pointer events. In a normal OneJS project `input.mouse.position` is in Unity screen coordinates.
- **Key names are Unity's**: `"W"`, `"Space"`, `"LeftArrow"`, `"Escape"`. They name the physical key rather than the letter printed on it, so WASD stays the same three keys on an AZERTY keyboard.
- **`wasd()` and `arrows()` give y as +1 for up**, Unity's way round, while the stage counts y downward. Subtract it to move up the screen.

**Read positions through `input` in a frame loop.** The same code then sees the mouse and every touch, so you get touch for free. When a pointer event is the right tool, `e.localX` and `e.localY` are relative to the element the handler is on.

A swipe, by finger or mouse drag, is one hook:

```tsx
import { useSwipe } from "oj"

useSwipe((direction) => push(direction))   // "left" | "right" | "up" | "down"
```

It fires once per gesture, after the pointer has travelled 28 pixels (`{ threshold }` changes that), and reads the same `input` your frame loop does, so a mouse and a finger need no separate code.

## Layout that follows the stage

`useStage()` gives you the window size to lay out against, and the responsive hooks read that same size: `useBreakpoint()`, `useScreenSize()`, `useResponsive()` and `useMediaQuery()`. `mount()` provides them; there is nothing to wrap.

## The manifest

`oj.json` sits beside your source and describes the cart:

```json
{
    "name": "Sumo",
    "author": "singtaa",
    "description": "Everyone is a blob on a platform that keeps getting smaller.",
    "entry": "index.tsx",
    "tags": ["physics", "arcade"],
    "controls": ["keyboard", "pointer", "touch"]
}
```

| Field | |
|---|---|
| `entry` | The file to build from. Defaults to `index.tsx`, and has to name a file this cart ships |
| `name` | What the cart is called. Renaming changes its URL, and the old one redirects to the new |
| `description` | One or two sentences, shown on the cart's card and page |
| `tags`, `controls` | How the catalog files and filters it |
| `author` | Your handle. Not read by the site |
| `version` | This cart's version, like `1.2.0` |
| `exports` | The file other carts import from, when this cart is a piece for them |
| `dependencies` | The carts this one uses, each at an exact version. `ojplay add` writes it |
| `license` | The license for your code and your own files. MIT when you leave it out |
| `credits` | Files that are not yours to license, and what the cart's Stats say about them |

Every one of those applies on every push, so editing the manifest is how you rename a cart or change how the catalog files it.

### Versions

Changing `version` is how you release. The first push that builds with a new number makes that build the version, and the console says `Released 1.3.0.` A released version never changes: a later push under the same number still runs, and the console tells you once that the version stays as it was. Nothing releases until you press Push, so a number typed early waits for the rest of the change.

### Using another cart

A cart can use another one's pieces, or run a whole one. From a terminal, with `ojplay` installed once (`npm install -g ojplay`):

```bash
ojplay add @singtaa/lightning
```

That adds it to `dependencies` at its newest version, fetches its source and art, and prints how to import it: `import { Bolt } from "@singtaa/lightning"`. A cart with no `exports` is a whole cart, and `import "@singtaa/lightning"` runs it. In an empty folder, `ojplay add` starts a new cart that uses it. `ojplay update` moves to newer versions, and `ojplay remove` takes one out. In the editor, the carts a cart uses are listed under its files, read only.

### Files that are not yours

A license covers only what you own. Mark each file that belongs to somebody else under `credits`, and the cart's Stats, in its editor, list it beside the license. A key ending in `/` covers a folder:

```json
{
    "credits": {
        "portraits/": "game art © Blizzard Entertainment"
    }
}
```

**A field you leave out is left alone, not reset.** A manifest that never mentions `tags` does not clear them; it says nothing about them. That is what makes a short manifest safe.

An older manifest may still carry a `stage`. It publishes as before, with a note that the field is not read: every cart gets the whole window.

An older cart may carry `schema` or `runtime`. Nothing reads either: publishing puts your cart on the current container and records which one, so a cart you leave alone keeps working.

## What else `oj` gives you

Beyond the components you already know:

| | |
|---|---|
| `Mathf`, `Vector2`, `Color` | Unity shaped, implemented in plain JavaScript. No interop cost |
| `random` | `random.int(0, 10)` straight away, or `random("daily")` for a seeded generator: daily challenges, replays and reproducible bugs |
| `drawing`, `useDrawing`, `Painter`, `Transform2D` | [Vector drawing](https://onejs.com/docs/guides/vector-drawing), with a transform stack; `useDrawing(ref, draw, "frame")` redraws every frame |
| `usePhysics` | A [2D physics](https://onejs.com/docs/guides/physics) world, simulated in C# and pumped for you |
| [`audio`](https://onejs.com/docs/guides/audio) | Sound over Unity's audio, not WebAudio, so it works [in Unity](/docs/unity) too |
| `Code` | Syntax highlighted source, for a cart that shows its own |
| `useParticles`, `fx`, `sl` | [Effects](/docs/effects) |
| `useTexture`, `assetUrl` | [Your own files](/docs/files) |

`usePhysics` reads its config once, on mount, because the simulation lives in C# and a re-render must not throw it away. Change a running world through its methods. A world also cannot grow, so create every body you need up front and pool them.

## Naming C# directly

`oj` wraps what most carts need, but the long tail of Unity and the BCL is not reachable any other way, so a cart can name C# itself:

```tsx
const v = new CS.UnityEngine.Vector3(0, 1, 0)
```

What you can reach is the set of namespaces the container preserves. A published cart is pinned to the runtime it was built against and that set only ever grows, so a cart that names something today keeps working. Something outside the set is not there at all, rather than being there and failing later.

Prefer `oj`'s own `Vector2`, `Color` and `Mathf` where they cover what you need: they are plain JavaScript, so they cost nothing to cross.

Completions for `CS` come from a type package the editor fetches, so they can be missing when the cart itself runs perfectly well.

## Things that behave differently from plain JavaScript

These follow Unity rather than the language, which is the point, but they surprise people:

- **`Mathf.Sign(0)` is `1`**, not `0`, and `Mathf.Round` is banker's rounding, so `Mathf.Round(0.5)` is `0` and `Mathf.Round(2.5)` is `2`. `Math.round` differs on both counts.
- **`Vector2` is a class, not a struct.** `a = b` aliases where C# would have copied, so mutating one mutates the other. Use `clone()`.
- **Positive y is down.** The stage is a screen space with its origin at the top left, so a key press that should move something up subtracts.
- **`random` ranges are max exclusive** for both integers and floats. `UnityEngine.Random` is exclusive for integers and inclusive for floats.

## Testing

Cart rules are ordinary functions and can be tested without a screen. Keeping them in their own file, with the drawing separate, means the rules get unit tests and the drawing needs none. It is also what makes a cart readable to somebody who forks it.

The cart as a whole is tested from a [local clone](/docs/start#work-locally). `npx ojplay test` runs it in the site's own container in a headless Chrome, and fails on a console error, an asset the site would not serve, or a row of controls out of line. Give it a script, `npx ojplay test play.mjs`, and the script's default export receives the running cart to drive: `click(x, y)`, `press("Space")`, `type("crane")`, `until(predicate)`, `read()` for the text on screen, and `shot(file)`. A thrown error fails the run.
