Plugin documentation

ArcadeOn Engine plugin tutorials & documentation

Build game engine plugins that extend the editor and runtime. Learn installation, manifests, and the plugin API.

ArcadeOn Engine Plugins

Build ArcadeOn Engine plugins to extend the browser-based 2D game engine with custom behaviours, tools, and integrations. This guide covers plugin architecture, installation, and the API surface for both editor and runtime.

Why build plugins for ArcadeOn Engine?

  • Add custom behaviours, events, and commands that appear in the visual editor.
  • Create reusable gameplay systems you can ship across multiple projects.
  • Extend the editor with tools that match your studio workflow.
  • Package integrations and utilities without changing core engine code.

Overview

Plugins are JavaScript modules that run inside the editor and/or play mode. They are loaded per-project, and their code is stored with the project so exports can run without re-installing the plugin.

Important: plugins are scanned before install/load and blocked if they reference browser/Electron APIs. Plugins should only use the engine plugin API and standard JavaScript utilities.

Security restrictions

Plugins are rejected if they reference any of the following identifiers:

  • arcadeon, window, document, globalThis, self, parent, top, frames
  • fetch, XMLHttpRequest, WebSocket, EventSource
  • navigator, location, history
  • localStorage, sessionStorage, indexedDB
  • Notification, open
  • ipcRenderer, require, process, module, exports
  • eval, Function, import
  • Worker, SharedWorker, BroadcastChannel, MessageChannel, postMessage
  • showOpenFilePicker, showSaveFilePicker, showDirectoryPicker
  • File, FileReader

Supported formats

  1. Manifest + entry file (recommended)
  • arcadeon.plugin.json
  • index.js
  1. Single-file plugin (quick testing)
  • my-plugin.js

The editor can install both formats. For single-file plugins, metadata is derived from the filename unless you later add a manifest. In the browser-only build, single-file plugins are the simplest option because the app cannot read adjacent files on disk.

Installing a plugin

  1. Open the editor.
  2. Go to the new Plugins panel in the left sidebar.
  3. Click Install plugin and pick either:
    • arcadeon.plugin.json (manifest), or
    • a .js/.mjs file (single-file plugin)
  4. The plugin is saved into the current project. It will load automatically the next time you open the project.

Manifest format

Create a file named arcadeon.plugin.json:

{
  "id": "com.yourstudio.my-plugin",
  "name": "My Plugin",
  "version": "0.1.0",
  "description": "Short description of what the plugin does.",
  "author": "Your Name",
  "entry": "index.js",
  "targets": ["editor", "runtime"]
}

Notes:

  • id must be unique.
  • targets can be ["editor"], ["runtime"], or both.
  • entry is a relative path from the manifest file.

Plugin module API

A plugin module should export an activate function. It receives an API object:

export function activate(api) {
  api.log.info("Plugin loaded");

  api.behaviours.register({
    type: "PulseScale",
    label: "Pulse Scale",
    category: "FX",
    defaults: { speed: 2, amount: 0.08, axis: "both" },
    schema: {
      speed: { kind: "number", min: 0, max: 10, step: 0.1 },
      amount: { kind: "number", min: 0, max: 1, step: 0.01 },
      axis: { kind: "text", placeholder: "both | x | y" }
    },
    onStart(ctx, e) {
      const b = ctx.getBehaviour?.(e, "PulseScale");
      if (!b || !e.components?.transform) return;
      b.state = b.state || {};
      b.state.baseSx = Number(e.components.transform.sx ?? 1);
      b.state.baseSy = Number(e.components.transform.sy ?? 1);
      b.state.t = 0;
    },
    onUpdate(ctx, e, dt) {
      const b = ctx.getBehaviour?.(e, "PulseScale");
      if (!b || !e.components?.transform) return;
      b.state = b.state || { baseSx: 1, baseSy: 1, t: 0 };
      const speed = Number(b.params?.speed ?? 2);
      const amount = Number(b.params?.amount ?? 0.08);
      const axis = String(b.params?.axis ?? "both").toLowerCase();
      b.state.t += dt;
      const wave = Math.sin(b.state.t * speed) * amount;
      if (axis !== "y") e.components.transform.sx = b.state.baseSx * (1 + wave);
      if (axis !== "x") e.components.transform.sy = b.state.baseSy * (1 + wave);
    }
  });

  return {
    onPlayStart(ctx) {
      api.log.info("Play started");
    },
    onPlayStop(ctx) {
      api.log.info("Play stopped");
    }
  };
}

API surface

  • api.scene, api.view, api.store, api.runtime
  • api.engine (engine API)
  • api.log.info|warn|error
  • api.behaviours.register(def) / api.behaviours.unregister(type)
  • api.entities.list|get|findByTag|findByName|spawn|destroy|setPosition|setTransform
  • api.assets.list|get|findByName|addFromUrl|addFromFile|remove|updateMeta
  • api.sceneTools.camera.get|set|snap
  • api.sceneTools.goToScene|goToDefault|instantiateScene|instantiateInstance
  • api.sceneTools.events.getState|getSwitch|getVariable|setSwitch|setVariable
  • api.storage.get|set|remove|clear|keys (project-scoped plugin data)
  • api.timers.setTimeout|setInterval|clear|clearAll (auto-cleared on unload)
  • api.events.registerCommand(type, handler, meta?)
  • api.events.registerCondition(type, handler, meta?)
  • api.events.registerTrigger(type, handler, meta?)

Event editor integration

When you register commands/conditions/triggers, you can pass optional metadata so they appear in the editor:

api.events.registerCommand("myCommand", (ctx, st, cmd) => {
  // ...
}, {
  label: "My Command",
  description: "Does something cool.",
  defaults: { amount: 1 },
  editor: {
    hint: "Example: amount 1-10.",
    fields: [
      { key: "amount", label: "Amount", type: "number", min: 0, max: 10, step: 1 },
      { key: "mode", label: "Mode", type: "select", options: ["a", "b"] }
    ]
  }
});

Notes:

  • If editor.fields is omitted, the editor falls back to a JSON editor for the command.
  • Plugin triggers appear in the Trigger dropdown; use trigger.params for custom data.
  • Plugin conditions can be stored in page.conditions.custom as { type, params } objects.

Custom triggers & conditions

Handlers receive engine context and should return booleans when appropriate:

api.events.registerTrigger("myTrigger", (ctx, entity, evt, page, trigger, dt) => {
  return true; // fire
});

api.events.registerCondition("myCondition", (ctx, cond) => {
  return !!cond?.params?.enabled;
});

Custom page conditions can be stored as:

{
  "conditions": {
    "custom": [
      { "type": "myCondition", "params": { "enabled": true } }
    ]
  }
}

Plugin storage

api.storage persists data per-project under project.pluginData[pluginId]. This data is saved with the project and exported builds.

Lifecycle hooks

Return an object from activate to supply hooks:

  • onUpdate(api, dt) runs every frame in play mode.
  • onEditorUpdate(api, dt) runs every frame in edit mode.
  • onPlayStart(api) and onPlayStop(api) run on mode transitions.
  • onUnload(api) runs when a plugin is disabled or removed.

Working example: Pulse Scale plugin

Create a folder (for example, pulse-scale) with these two files and install arcadeon.plugin.json from the Plugins panel. After installing, add the Pulse Scale behaviour to any entity and press Play.

arcadeon.plugin.json:

{
  "id": "arcadeon.pulse-scale",
  "name": "Pulse Scale",
  "version": "1.0.0",
  "description": "Adds a PulseScale behaviour that rhythmically scales entities.",
  "author": "ArcadeOn Studios",
  "entry": "index.js",
  "targets": ["runtime"]
}

index.js:

export function activate(api) {
  api.behaviours.register({
    type: "PulseScale",
    label: "Pulse Scale",
    category: "FX",
    defaults: {
      speed: 2,
      amount: 0.08,
      axis: "both"
    },
    schema: {
      speed: { kind: "number", min: 0, max: 12, step: 0.1, label: "Speed" },
      amount: { kind: "number", min: 0, max: 1, step: 0.01, label: "Amount" },
      axis: { kind: "text", placeholder: "both | x | y", label: "Axis" }
    },
    onStart(ctx, e) {
      const b = ctx.getBehaviour?.(e, "PulseScale");
      if (!b || !e.components?.transform) return;
      b.state = b.state || {};
      b.state.baseSx = Number(e.components.transform.sx ?? 1);
      b.state.baseSy = Number(e.components.transform.sy ?? 1);
      b.state.t = 0;
    },
    onUpdate(ctx, e, dt) {
      const b = ctx.getBehaviour?.(e, "PulseScale");
      if (!b || !e.components?.transform) return;
      b.state = b.state || { baseSx: 1, baseSy: 1, t: 0 };
      const speed = Number(b.params?.speed ?? 2);
      const amount = Number(b.params?.amount ?? 0.08);
      const axis = String(b.params?.axis ?? "both").trim().toLowerCase();
      b.state.t += dt;
      const wave = Math.sin(b.state.t * speed) * amount;
      if (axis !== "y") e.components.transform.sx = b.state.baseSx * (1 + wave);
      if (axis !== "x") e.components.transform.sy = b.state.baseSy * (1 + wave);
    }
  });

  api.log.info("Pulse Scale plugin loaded");
}

Examples

See sample plugins here:

  • plugins/examples/pulse-scale
  • plugins/examples/play-logger
  • plugins/examples/event-kit
  • plugins/examples/camera-bookmarks

Plugin FAQ

What can a plugin do?

Plugins can add behaviours, event commands, editor tools, and runtime logic through the plugin API. They can target the editor, the runtime, or both.

How do I install a plugin?

Install from the Plugins panel by selecting the manifest (arcadeon.plugin.json) or a single-file plugin (.js/.mjs). The plugin is saved into the project and loads automatically next time.

Do plugins ship with exports?

Yes. Plugin code is stored with the project so HTML5 and desktop exports include the plugin.

Are plugins sandboxed?

Yes. Plugins are scanned and blocked if they reference restricted browser or Electron APIs. Use the documented plugin API instead.

Selling plugins

If you sell plugins, distribute them as a folder containing:

  • arcadeon.plugin.json
  • index.js

Users can download the folder and install the manifest file from the Plugins panel.