Opening in Unity

What the download contains, and why the source does not change

Every published sketch has an Eject button. An unpublished sketch has one too, for its author and nobody else.

Play runs your sketch inside a shared container, which is what makes a sketch a few kilobytes and a fork instant. The price is the container's limits: no scenes, no filesystem, and only the parts of Unity it preserves. When a sketch outgrows them, this is the door, and what comes through it is a Unity folder that runs the moment you drop it in a scene.

The promise is that your sketch file does not change. Not a port, not a rewrite: the same source, running in a project you own.

If you have never used Unity, that is the thing to know before you click: past this point you are using OneJS itself, and onejs.com is its documentation.

Three steps

  1. Install OneJS in a Unity project (6.3 or newer) from the Package Manager: https://github.com/Singtaa/OneJS.git
  2. Unzip the download inside that project's Assets/ folder.
  3. Drag the prefab into a scene.

That is the sketch running. It arrives already built, so there is nothing to compile between the download and the first frame.

To change it, the source is in the ~/ folder that came with it:

cd Assets/OneJSPlay/<YourSketch>/~
npm install
npm run watch

npm run watch rebuilds on save and the running sketch reloads itself. Each download carries its own README with these steps.

From your own machine

Every sketch is a git repository, and the editor is one client of it. Clone a public sketch with nothing, or your own with a personal access token from your account page, sent as the password:

git clone https://play.onejs.com/g/<id>.git
cd <id>
npx onejs-play init   # package.json, tsconfig.json and types, all gitignored
npm install
npx oj run            # the sketch in the site's real container, in a local Chrome
git push origin main  # builds on the site, and becomes what everyone plays

A push to main builds the commit on the site and, if it builds, makes it what everyone plays. The result comes back on the remote: lines, with the file, line and column of any compile error. The push lands either way, so the last commit that built keeps running until the next one that does.

npm install brings the oj command, which wraps the rest: build, run, test, status, push and new. Every clone carries an AGENTS.md describing all of it, so you can read it there rather than here, and an AI agent handed a sketch finds its own way.

The editor and the terminal both work on main, and neither gets to clobber the other: a save refuses a main that moved underneath it, and git refuses a push that is not a fast forward. Whoever is second finds out rather than winning.

This is a clone, not the download below. A clone is the repository and keeps working against the site; the download is a Unity project that does not.

What is in the archive

The download is one folder, laid out as a JSRunner project:

Path What it is
<YourSketch>.prefab A GameObject with a JSRunner. This is the thing you drag into a scene
PanelSettings.asset Marks the folder as the project. JSRunner runs the bundle beside it
app.js.txt Your sketch, already built
~/ Your source, your files, a JSRunner scaffold, and oj/ vendored

Unity ignores a folder named ~. That is the convention JSRunner already uses for its working directory, and it is why the source and your files can sit inside Assets/ without being imported, compiled or shipped in a build.

Your files keep the names your source loads them by, so nothing needs editing to run. .oj/, which holds your catalog cover art, is left behind: a Unity project has no use for it.

oj is vendored rather than depended on: a copy of the code that ran, sitting in your project. It keeps building whether or not the package is ever published, and it cannot change under you later. Your sketch imports from "oj" and a tsconfig paths entry resolves it.

Files the scaffold owns (package.json, tsconfig.json, esbuild.config.mjs, .gitignore, README.md) come from the scaffold even if your sketch shipped a file with that name.

Why the source does not change

oj is a subset of OneJS. Everything a sketch reaches also exists in a normal Unity project, so there is nothing to port.

The parts that only make sense on the site degrade rather than break:

After the download
assetUrl Resolves to the project's assets/ folder, or StreamingAssets in a build
useRoom connected stays false, send() does nothing, no handler fires
scores available is false, submit resolves to null, top returns []
mount() Starts a runtime itself, since there is no container to provide one
A .sl shader Generated as real shader code on its first run, then compiled. Interpreted if there is none
The control theme Travels with you: mount() compiles it, the site does not apply it

A multiplayer sketch therefore still runs. It just runs alone, which is the correct behaviour for a sketch whose server was a site you are no longer on.

What you get that you did not have

The restrictions in the container are the container's, not OneJS's. Once the sketch is a project:

  • Your own C# types, and every part of Unity's API rather than the set the container preserves.
  • Scenes, cameras and 3D. The sketch was a UI panel; now it can sit in front of anything.
  • Every build target. Desktop, mobile, console, XR, and WebGL if you want the web without the site.
  • No sandbox. Files, network, native plugins.

Nothing forces you to use any of it. A sketch that stays exactly as it was will keep building.

One thing to check first

input reads through Unity's real input bridge in a project rather than through the container's browser event backend, and the two report pointer positions in different coordinate spaces. The runtime converts for you, so a pointer driven sketch plays the same.

That conversion is unit tested but is worth a quick look in your own project, because it is the one part of the promise that depends on your setup rather than on arithmetic. Move the pointer, confirm the sketch follows it. Keyboard and gamepad were never in a coordinate space and pass through untouched.

If you use Unity Input System actions, note that a position read out of an action does not go through that conversion and arrives in screen space. Direct input.mouse and input.touches reads are converted; actions are not.