Plugins & Resources
An Estella game is an App composed of plugins. Each subsystem — physics,
audio, tilemaps, particles, UI, and so on — is a plugin that registers its
systems and exposes a runtime API as a resource.
Reaching a subsystem: Res(XxxAPI)
Section titled “Reaching a subsystem: Res(XxxAPI)”Inside a system, list the resource you need in the parameter array; the callback receives it in the same order:
import { defineSystem, Res, Audio } from 'esengine';
defineSystem([Res(Audio)], (audio) => { audio.playSFX('assets/hit.wav');});Every subsystem’s imperative API is reached the same way:
| Subsystem | Resource | Guide |
|---|---|---|
| Input | Res(Input) |
Input |
| Audio | Res(Audio) |
Audio |
| Assets | Res(Assets) |
Assets |
| Tilemaps | Res(Tilemaps) |
Tilemaps |
| Particles | Res(Particle) |
Particles |
| Post-processing | Res(PostProcess) |
Post-processing |
| Scenes | Res(SceneManager) |
Scenes |
| Tweens | Res(Tween) |
Animation |
| Frame animation | Res(SpriteAnimation) |
Animation |
| Animator | Res(AnimatorController) |
Animation |
| Timers | Res(TimerRes) |
Scripting |
| Timeline | Res(Timeline) |
Timeline |
| Prefabs | Res(Prefabs) |
Prefabs |
| Navigation / AI | Res(Nav) |
Gameplay AI |
| Localization | Res(Localization) |
Localization |
| Camera view | Res(CameraView) |
Camera |
| UI events | Res(UIEvents) |
UI |
| Physics | Res(Physics) — from esengine/physics |
Physics |
| Spine | Res(Spine) — from esengine/spine |
Spine |
Physics and Spine are optional side-modules loaded on demand, so their APIs
live on subpaths to keep the base bundle small; everything else is in the main
esengine package. All the base subsystems above ship in the standard app — no
plugin wiring needed.
Some subsystems also publish event resources on the same pattern — a
resource whose contents are this frame’s events, read the same frame:
Res(Physics2DEvents) (contacts), Res(SpineEvents) (animation events),
Res(UIEvents) (clicks and input from widgets).
Res(X) hands the system the resource itself. ResMut(X) hands it a
handle wrapped around the resource — .get() to reach the value, .set(v)
to replace it, .modify(fn) to mutate it in place. Use ResMut when the system
replaces or mutates the resource’s state — same contract as Mut on components.
The next section shows both shapes side by side.
Defining your own resource
Section titled “Defining your own resource”A resource is just a value the app owns, and it does not have to be plain
data. Every subsystem API in the table above is a class instance published as a
resource — Res(Audio) is an AudioAPI, Res(SceneManager) is a
SceneManagerState with switchTo / load / unload on it. Your own
resources can carry methods the same way.
defineResource(defaultValue, name) creates the definition — an identity, not
the value itself:
import { defineResource } from 'esengine';
class ScoreState { value = 0; combo = 1;
add(points: number): void { this.value += points * this.combo; }}
export const Score = defineResource<ScoreState>(null!, 'Score');The null! is a deliberate placeholder: the real instance is inserted when the
plugin builds, so each app gets its own. This is exactly how every engine
subsystem is wired — SceneManager is declared defineResource<SceneManagerState>(null!, 'SceneManager')
and its plugin does app.insertResource(SceneManager, new SceneManagerState(app)).
import type { App, Plugin } from 'esengine';
export const scorePlugin: Plugin = { name: 'score', build(app: App) { app.insertResource(Score, new ScoreState()); },};From there it is reached like any other resource:
import { defineSystem, Res, ResMut } from 'esengine';
defineSystem([Res(Score)], (score) => { // score IS the ScoreState instance if (score.value > 1000) { /* … */ }});
defineSystem([ResMut(Score)], (score) => { // score is the handle — unwrap it score.modify((s) => s.add(100));});Outside ECS code — host code, a defineBehavior hook — use
app.getResource(Score), which returns that same live instance.
What the default value costs you
Section titled “What the default value costs you”A resource is materialised the first time it is read, by cloning the default you
passed to defineResource. That clone is structural, not total:
| Default value | What each app ends up with |
|---|---|
| Plain object / array | A deep clone — one independent copy per app. |
| Class instance | The same object, shared by every app that reads it. |
| Function-valued property | Shared by reference — methods always survive the clone. |
Class instances pass through by reference on purpose: cloning them into a bare
object would strip their prototype and with it every method. The practical
consequence is that defineResource(new ScoreState(), 'Score') gives one
score shared by every app in the process — and the editor runs two (edit mode
and play mode). Prefer the null! + insertResource form above whenever the
resource holds state.
Components vs. resources
Section titled “Components vs. resources”- Components are per-entity data. You attach them (in the editor or with
Commands) and iterate them withQuery. C++-backed components are decoded out of the wasm heap on every read, so what you get is a copy — change it and hand it back, or the write is lost (see Scripting). - Resources are per-app singletons — shared state and subsystem APIs — read
with
Res. A resource is the live object, never a copy: calling a method or setting a field on it takes effect immediately.
Getting an entity
Section titled “Getting an entity”Subsystem APIs act on an entity, and that entity comes from a Query (or a
spawn). This is the shape you’ll see throughout the guides:
import { defineSystem, Query, Res, SpriteAnimator, Tween, TweenTarget } from 'esengine';
defineSystem([Query(SpriteAnimator), Res(Tween)], (q, tween) => { for (const [entity] of q) { tween.to(entity, TweenTarget.PositionX, 0, 100, 1.0, {}); }});Spawning: Commands
Section titled “Spawning: Commands”To create entities and attach components from a system, take Commands():
import { defineSystem, Commands, Transform, Sprite } from 'esengine';
defineSystem([Commands()], (cmds) => { cmds.spawn() .insert(Transform, { position: { x: 0, y: 0, z: 0 } }) .insert(Sprite, { size: { x: 32, y: 32 } });});