Skip to content

Project components & Component Studio

Project components are custom component types stored inside your scene file. They behave like built-ins in the inspector, Object Flow catalog, and runtime— but you author them in the Components workspace without editing engine source.


Opening the Studio

  1. Click the Components tab in the editor mode bar (alongside Arrange, Object Flow, Motion).
  2. The stage shows Component Studio: sidebar list + detail pane.
  3. Built-in rows are labeled Built-in; yours show Project.

+ New creates a blank project component and selects it for editing.


Project component record

Saved in scene.projectComponents[]:

json
{
  "id": "rage_meter",
  "label": "Rage Meter",
  "category": "Combat",
  "description": "Builds rage on hit; fires rageFull at max.",
  "version": 1,
  "fields": {
    "value": { "type": "number", "default": 0, "min": 0, "max": 100 },
    "max":   { "type": "number", "default": 100 }
  },
  "events": {
    "rageFull": { "label": "Rage Full" }
  },
  "actions": {
    "charge": {
      "label": "Charge",
      "params": { "amount": { "type": "number", "default": 5 } },
      "kind": "steps",
      "steps": [ /* … */ ]
    }
  },
  "conditions": {
    "isFull": {
      "label": "Is Full",
      "kind": "script",
      "body": "return self.value >= self.max;"
    }
  }
}

On scene load, syncProjectComponents() compiles each record and registers it in the global registry so objects can attach rage_meter like any built-in.


Authoring fields

Supported field types (same as built-ins):

TypeInspector widgetUse for
number / intNumber inputStats, timers
booleanCheckboxFlags
stringTextNames, keys
enumSelectFixed options
vectorX/Y pairOffsets
colorColor pickerUI tints
objectRefObject pickerTargets in scene
componentRefObject picker (filtered)Objects with a component
assetRefAsset referenceSprites, sounds
eventKeyEvent nameCustom subscriptions

Hidden and read-only flags work the same as built-in field specs.


Action bodies: Script vs Steps

Each action chooses a kind:

Script (kind: "script")

A JavaScript function body compiled with compileScript. Available identifiers:

NameMeaning
selfThis component’s fields object
paramsAction parameters from the graph or caller
eventCurrent flow event (if any)
source / targetEvent actors
thingHost Thing
componentsComponentHost — call components.run("health", "damage", { amount: 5 })

Use emit("eventName", { ... }) when you need to fire declared events (provided in the action context).

Example — charge rage:

javascript
const next = Math.min(self.max, self.value + (params.amount || 0));
self.value = next;
if (self.value >= self.max) emit("rageFull");

Steps (kind: "steps")

Visual blocks edited in the Studio— no semicolons required. Good for designers and for logic that stays inspectable in the scene JSON.

See Visual step language below.

Toggle Script / Steps on each action in the Studio; switching kind preserves separate body vs steps arrays.


Conditions

Project conditions support script bodies only (return boolean):

javascript
return self.value >= 10;

They appear in Object Flow as Component Condition nodes like built-ins.


Attaching project components

  1. Arrange → select object → Components card → + Add Component.
  2. Your project types appear in the same menu as built-ins (search by label or id).
  3. Field editors are generated from your schema.

Deleting a project component

In the Studio detail header, Delete removes the record from projectComponents[], unregisters the type, and saves the scene. Objects that still had the type attached may warn on load and drop unknown types.

Rename the id carefully— saved scenes and blueprints reference the id string.


Visual step language

Steps are an ordered list executed synchronously when an action runs. Nested lists appear under If branches and For Each loops.

Step types

TypePurpose
Set FieldWrite self.field, another component’s field, or a Thing property
Call Componenthost.run(componentId, method, params) on self, source, target, or by id
Emit EventFire an event from this component’s event list
If / ElseEvaluate expression; run then or else step lists
For EachLoop an array; bind item / index (configurable names)
ScriptEscape hatch — same sandbox as Script action bodies
CommentDocumentation only (no runtime effect)

Expressions in steps

  • Values that support dynamic data can start with = followed by a JavaScript expression.
    • Example: =self.hp + params.amount
  • If and For Each expression fields are always evaluated as JS (you may omit the = prefix in the editor; the runtime adds it).

Set Field targets

TargetWrites to
selfCurrent component fields
hostThe Thing (object-level property)
<componentId>Sibling component’s fields on the same Thing

Call Component targets

TargetRuns on
self / omittedCurrent Thing’s host
source / targetEvent source or target Thing
Object id or =expressionResolved Thing in the scene

Example — charge with cap and emit

text
If / Else:  self.value < self.max
  Then:
    Set Field:  self.value  =  Math.min(self.max, self.value + params.amount)
    If / Else:  self.value >= self.max
      Then:
        Emit Event:  rageFull  (payload optional)
  Else:
    Comment:  Already full

Limitations (by design)

  • Steps run synchronously in one frame— no await, delays, or tweens.
  • Use Object Flow for timelines, waits, and scene-wide orchestration.
  • Errors in a step are logged; later steps still attempt to run unless the interpreter throws.

Studio UI reference

ControlEffect
+ NewNew project component with default id
Sidebar row clickSelect; built-ins are read-only detail
Add Field / Event / Action / ConditionExtend schema
Add Param on actionsTyped parameters for graphs
Delete (header)Remove project component
Step ↑ ↓ ×Reorder or remove steps
+ Set Field, + If, …Add step at end of list

Built-in components show a read-only breakdown of fields, events, actions, and conditions— useful as a live reference while authoring project types.


Compilation pipeline

text
Scene load
  → syncProjectComponents(records)
       → compileProjectComponent(each)
            → defineComponent({ …, actions with run fns })
       → registerComponent(definition)
  → createSpriteThing / hydrate Thing.components

Script bodies compile once and cache. Step bodies compile to a function that calls runSteps(steps, ctx).


Tips

  1. Start with Health as a template — open src/game/components/built-in/health.js while designing project fields/events.
  2. Keep actions small — one action per user-visible verb (charge, spend, reset).
  3. Declare events in the Studio even if you only emit them from steps— they show up in documentation and future graph tooling.
  4. Test in Play with a throwaway object that only has your component before wiring full battle graphs.

Next: Object Flow integration · Architecture

Opal Engine — MIT licensed