#Components

A component is a reusable group of objects (for example a coin made of a glow image and a Spine animation) together with the assets it needs and, optionally, a script. You upload it once as a package, it appears in your Asset library with a Component · v1 badge, and your games can use it.

Place copies of it in the editor (see In the editor); builds include everything a copy needs.

#The package

A package is component.json plus the files of its assets:

{
  "name": "Coin",
  "root": {
    "type": "container",
    "id": "coin",
    "name": "Coin",
    "transform": { "position": { "x": 0, "y": 0 }, "rotationDeg": 0, "scale": { "x": 1, "y": 1 } },
    "children": [
      { "type": "sprite", "id": "glow", "name": "Glow", "transform": { "position": { "x": 0, "y": 0 }, "rotationDeg": 0, "scale": { "x": 1, "y": 1 } }, "anchor": { "x": 0.5, "y": 0.5 }, "texture": { "alias": "glow" } },
      { "type": "spine", "id": "logo", "name": "Logo", "transform": { "position": { "x": 0, "y": 0 }, "rotationDeg": 0, "scale": { "x": 1, "y": 1 } }, "skeleton": { "alias": "logoSkeleton" }, "atlas": { "alias": "logoAtlas" } }
    ]
  },
  "exposed": [{ "objectId": "glow", "field": "visible", "label": "Glow on" }],
  "assets": [
    { "alias": "logo", "files": ["LOGO.json", "LOGO.atlas", "LOGO.webp"] },
    { "alias": "glow", "files": ["glow.png"] }
  ]
}
  • root is a container with the component's objects, in the same shapes a game config uses. It may only use the package's own aliases, and it can't contain other components.
  • exposed lists what each placed copy may change, as { objectId, field, label? }. The field must exist on that object, for example visible on a sprite or text on a text object.
  • assets lists each asset with an alias and its files. Each asset is checked like a normal upload (see Asset kinds), including Spine version warnings. A Spine asset with alias logo gives logoSkeleton and logoAtlas. The alias script is kept for the script.

If anything doesn't add up, nothing is kept and the message says why: an alias the root uses but the package doesn't have, an exposed field that doesn't exist, an asset whose files can't be used, or a script that doesn't compile.

#Upload a component

  1. In the Asset library, choose Upload asset, then Upload component at the top.
  2. Drop or browse for the package: either one .zip (see As one .zip), or component.json, the files of the assets it lists and, if the component has one, its script (one .ts or .js file). Separate files can be added in several goes; a file with the same name replaces the earlier one.
  3. The page reads component.json in your browser and lists each asset with its files: green when chosen, red while missing. It also points out files component.json doesn't list, and whether there's a script. If you already have a component with that name, it says the upload becomes its next version.
  4. Tick I have the right to use these files in my games (see Upload rights), then choose Add to library (or Upload new version). The files upload with a progress bar. Then the server checks everything; if anything doesn't add up, nothing is kept and the page shows why:
    • a problem with the package, such as an alias the root uses that no asset gives;
    • the engine's rules for the objects and exposed fields;
    • a script that doesn't compile, one line per error with file, line and column (coin.ts:4:34 …).
  5. Warnings, for example about a Spine export's version, are shown with the result. Preview it opens the component in the library.

The library's Components tab lists your components with their newest version (Component · v2) and a Script badge when they have one.

Scripts and tools can also upload through the API while signed in: POST /api/components/uploads with { "package": <component.json>, "files": [{ "fileName", "contentType", "sizeBytes" }], "script": { "fileName", "source" } }, then PUT each file to its upload URL and POST /api/components/uploads/{versionId}/finalize.

#As one .zip

You can also pack everything into one .zip, component.json, the assets' files and the script, and drop it on Upload component. If the zip can't be used, the screen says why.

  • Folders: one folder around everything (as Compress makes) is fine, and folders inside are ignored: files are matched by name, so two files can't share a name. macOS extras (__MACOSX, .DS_Store) are skipped.
  • The script: the zip may hold one .ts or .js file, which is the script.
  • Where it's unpacked: the zip is unpacked in your browser and uploaded as separate files, so it's checked exactly like they are.
  • Refused: zips with paths that point outside the package (such as ../), password-protected zips, and zips that unpack to more than 512 MB.

#Versions

Uploading a package with the same name as one of your components creates its next version (v2, v3, …). Versions never change and are all kept. Each version has its own aliases (for example c_3f2a9c1d_glow), so different versions never clash in a game. The library shows the newest version.

#Delete a component

Open the component's card in the library and choose Delete component….

  • What goes: every version, all of the component's assets and files, and the storage they used.
  • When it can't be deleted: while a project still uses it, the message names the projects. A project uses it when its game has a copy of any version, or when its live (prod) game was built with it. Remove the copies (or detach them) and publish again, or unpublish, then delete.
  • Stage builds keep playing after a delete, because builds hold their own copies of the files.

#In the editor

#Place a copy

  • Choose + Add › Component and pick one from your library, or drag a component card from the Assets panel onto the stage (its + adds it to the middle).
  • The first copy brings the component's newest version and its assets into the game. A copy goes into the selected group or Spine slot, otherwise the scene on screen, and gets an id from the component's name (goldCoin, goldCoin_2, …).

#A copy in the tree and properties

  • A copy is a single row in the tree with a CMP badge and its version (v3); JS means the component has a script. The ▸ shows what's inside, read-only.
  • Its properties are the usual id, name, visibility, alpha, position, size, rotation and align, plus the component's exposed fields: what the component lets each copy change, such as a label's text or whether a glow shows. Changing one only changes this copy; Reset goes back to the component's value.
  • The objects inside a copy can't be selected or changed on their own. To change them for every game, upload a new version; to change just this copy freely, detach it.

#New versions

  • When your library has a newer version, the row shows ↑v4 and the properties say Update available (v3 → v4). Update every copy to v4 moves all copies of that component in the game to the new version.
  • Each copy keeps its exposed-field values that the new version still has. Values for fields the new version no longer exposes are dropped, and the editor lists them. Undo goes back to the old version.
  • The editor checks for new versions when it opens and whenever you come back to its tab, so a version uploaded in the meantime shows up.

#Detach

Detach… (after a confirmation) turns a copy into a plain group with the copy's id, name and place, holding ordinary copies of the component's objects with their exposed-field values. Their ids are free ids in the game (glow, or glow_2 when glow is taken), and buttons inside follow. The group no longer follows the component's updates. Undo brings the copy back.

#Scripts

View script opens the component's script, read-only. It shows the compiled JavaScript the game runs, because TypeScript is compiled when you upload. Scripts never run while you edit; they run in Play and in the published game.

#Preview

Choose Preview on a component's card. It shows the component as a placed copy looks by default, what a copy can change, and the assets it's made of. Scripts don't run in the preview; they run in your games.

#Scripts

A script makes a component do something by itself, such as spin, blink or react to game events. Write it in TypeScript (or JavaScript) and send it with the upload as script. It's checked against the component API when you upload. Errors come back with file, line and column, for example coin.ts:4:34 Argument of type 'number' is not assignable to parameter of type 'string'.

import { defineComponent } from "@oxinator/engine/component-api";

export default defineComponent({
  elapsedMs: 0,
  onCreate(ctx) {
    ctx.objects("glow").setVisible(Boolean(ctx.props["glow.visible"]));
    ctx.on("spin:start", () => ctx.objects("logo").playAnimation("spin", false));
  },
  onUpdate(ctx, dtMs) {
    this.elapsedMs += dtMs;
    ctx.objects("glow").setAlpha(0.6 + 0.4 * Math.sin(this.elapsedMs / 200));
  },
  onDestroy(ctx) {
    ctx.emit("coin:gone", { id: ctx.id });
  },
});
  • Shape: the script's default export is defineComponent({ … }) with any of these hooks.

    • onCreate(ctx) runs when the copy is built.
    • onUpdate(ctx, dtMs) runs every frame while the game runs.
    • onEvent(ctx, name, payload) runs for every game event.
    • onDestroy(ctx) runs when the game stops.

    Hooks are called on the object you pass, so this can keep state for the copy.

  • Imports: the only import allowed is @oxinator/engine/component-api.

  • ctx is all a script gets:

    • ctx.id is the copy's id.
    • ctx.objects(id) gives one of the copy's own objects by its id in root: position, rotation and scale (getTransform/setTransform), setVisible, setAlpha, setTexture (one of the component's own aliases), Spine playAnimation and setSkin, and setText.
    • ctx.props holds the copy's exposed fields, keyed "objectId.field".
    • ctx.runAction(name, params?) runs one of the game's actions.
    • ctx.emit(name, payload?) and ctx.on(name, handler) send and receive game events.
    • ctx.setTimeout, ctx.setInterval and their clears are game-time timers; they stop with the copy.
    • ctx.random() gives a random number. Math.random and Date.now aren't available.
  • Isolation: scripts run in a locked-down sandbox with no window, document, network, storage, eval or Function.

    • An error stops only that copy's script; the game keeps running. Script errors are counted in your game's errors (see Game analytics).
    • Scripts never run on the dashboard: not while you edit, and not in the library preview.
    • Published games with scripts need a slightly looser page policy than games without them ('unsafe-eval', which the sandbox uses). Oxinator only sets it for games that contain a component with a script.
  • Size: a script can be up to 100 KB.