Shipping Files
Sprites, sounds and fonts, named so they survive an eject
A game can ship binary files alongside its source: images, audio, video and fonts. One function resolves a name to wherever those files ended up.
import { assetUrl, useTexture, audio } from "oj"
const glow = useTexture("glow.png") // a texture, or null while loading
const pop = await audio.load(assetUrl("pop.wav"))
<Image src={assetUrl("logo.png")} />assetUrl(name) |
The address of one of your files, as a string |
loadTexture(name) |
Loads an image and resolves to a texture |
useTexture(name) |
The hook form. null until it arrives |
Use the name as it appears in the sidebar
"glow.png" for a file at the top, "sprites/hero.png" for one in a folder. Never "assets/glow.png", and never an absolute path.
That is the whole rule, and it is what makes the same source run in two places that store files completely differently:
| Where | assetUrl("sprites/hero.png") resolves to |
|---|---|
| On play.onejs.com | /assets/sprites/hero.png on the game's own origin |
| After an eject | The project's own assets folder, keeping the same folders |
Folders are part of the name rather than a thing you create: putting a file at sprites/hero.png is what makes sprites exist, and the folder is gone again when the last file leaves it.
A leading ./ is accepted and stripped, because it is what a web habit produces and means the thing the function is already about. So is a leading assets/, which is why assets is the one folder name your own files may not use at the top level: assetUrl("assets/glow.png") cannot mean both "the file glow.png" and "the file glow.png inside a folder called assets", and it has always meant the first. Anything that already resolves (a full URL, or an absolute path in a Unity project) is handed back untouched, so passing a remote image through assetUrl is harmless.
Why it is explicit
assetUrl at the call site is a deliberate choice over teaching every loader a hidden base. A bare "glow.png" resolving through machinery you cannot see would be hard to reason about, and two loaders that disagreed about the base would be a bug with no visible cause.
So loadTexture and useTexture take the bare name, matching onejs-unity's loadImageAsync, while audio.load takes a URL and gets assetUrl(...) at the call site. That is not an inconsistency: audio.load is onejs-unity's own function passed through unchanged rather than a variant of it, and keeping it identical is what lets an ejected game keep working.
What you can ship
Images (png, jpg, webp, gif), audio (ogg, wav, mp3), video (mp4, webm), and fonts (ttf, otf, woff, woff2).
Assets are stored apart from your source and served from the game's own origin. A save re-uploads your code and leaves the art alone, which is what you want given art changes far less often than code.
A file you add is uploaded straight away, so pressing Run loads it immediately: the preview runs your game against the stored files, and something that only existed in your browser could not be served to it. Deleting one removes it straight away too, for the same reason.
That is the one place assets differ from source. Your code lives in the editor until you publish; your files are stored as soon as you add them, and count against your storage from that moment. Publishing decides which of them the published game ships, and that has not changed.
Loading takes a frame or two
useTexture returns null until the file arrives, so render a placeholder rather than assuming:
function Sparks() {
const ref = useRef(null)
const glow = useTexture("glow.png")
useParticles(ref, {
emitters: [{ rate: 40, texture: glow, additiveness: 1 }],
})
return <View ref={ref} style={{ flexGrow: 1 }} />
}Textures are cached by URL, so a second component asking for the same file does not pay for the bytes again, and a component that unmounts mid-load does not cancel it.
Covers
A file called cover.html beside your source becomes your game's card in the catalog. It is a whole document, so CSS animation, canvas and inline script all work.
An image or a video works too, and is cheaper: a card is a tag the browser already draws, where HTML is a whole page. A gallery shows a lot of these at once, so keep whichever you choose light.
Covers render in a sandboxed frame on the game's own origin, with no network access. A cover cannot phone home, which matters because it renders for every visitor who scrolls past it.
After an eject
Your assets come down with the game, under the same assets/ folder they were served from and in the same folders you put them in, so the source that ran on the site runs unchanged. The scaffold's README covers where that folder maps to in a Unity build.
This is the reason to write the name the sidebar shows you: it is the one form that means the same thing on both sides.