380 lines
15 KiB
Markdown
380 lines
15 KiB
Markdown
# Scripting
|
||
|
||
Dusk embeds [JerryScript](https://github.com/jerryscript-project/jerryscript) to
|
||
drive gameplay logic from JavaScript. The engine itself (rendering, physics,
|
||
asset loading, entity storage) is all C; scripts sit on top and manipulate that
|
||
state through a small set of bound objects.
|
||
|
||
This document covers the JS-facing scripting API. **UI (buttons, sliders,
|
||
menus, etc.) is not exposed to scripts** — it's a separate, C-only API. See
|
||
[UI.md](UI.md) if you're building screens/menus from engine/game C code.
|
||
|
||
## Lifecycle
|
||
|
||
On startup, `engineInit()` loads and evaluates `assets/engine.js` as the main
|
||
script, then calls the global `init()` function if one is defined. From then
|
||
on, every engine tick calls (in order):
|
||
|
||
1. `fixedUpdate()` — once per fixed timestep. Use this for gameplay logic that
|
||
must be deterministic and independent of display refresh rate (movement,
|
||
physics-adjacent input handling, etc). Skipped on interpolation/dynamic
|
||
frames when the build has variable-timestep rendering enabled.
|
||
2. `update()` — once per rendered frame, including interpolation frames. Use
|
||
this for smooth, purely presentational animation (nothing that needs to be
|
||
deterministic).
|
||
|
||
On shutdown, `deinit()` is called once.
|
||
|
||
All four hooks (`init`, `update`, `fixedUpdate`, `deinit`) are **optional** —
|
||
if a script doesn't define one, the engine simply skips it, no error.
|
||
|
||
```js
|
||
function update() {
|
||
cubePosition.rotation.y += TIME.delta * 1.5;
|
||
}
|
||
```
|
||
|
||
### `async init()` and `include()`
|
||
|
||
`init` (or any of the other hooks) can be declared `async` and use `await`
|
||
freely, including awaiting `include()` (see below), even though the engine
|
||
calls these functions synchronously from C with no external JS event loop.
|
||
When a hook returns a pending `Promise`, the engine keeps driving the asset
|
||
system and JerryScript's job queue until that promise settles before
|
||
continuing — so by the time e.g. `init()` "returns" from the engine's point of
|
||
view, everything it awaited has actually finished.
|
||
|
||
```js
|
||
async function init() {
|
||
Actions = await include("input.js");
|
||
// ...
|
||
}
|
||
```
|
||
|
||
If the awaited work throws/rejects, it surfaces as a C-level error.
|
||
|
||
## Loading other scripts: `include(path)`
|
||
|
||
```js
|
||
var Actions = await include("input.js");
|
||
```
|
||
|
||
`include(path)` always returns a `Promise`. The named file is loaded and
|
||
evaluated once no matter how many times (or from how many different scripts)
|
||
you `include()` it — later calls for the same path are handed the same
|
||
in-flight/resolved promise rather than re-running the file.
|
||
|
||
The included script communicates its result back by assigning to the bare
|
||
global `module`:
|
||
|
||
```js
|
||
// input.js
|
||
Input.bind("w", INPUT_ACTION_UP);
|
||
// ...
|
||
|
||
module = {
|
||
UP: INPUT_ACTION_UP,
|
||
DOWN: INPUT_ACTION_DOWN,
|
||
// ...
|
||
};
|
||
```
|
||
|
||
Whatever `input.js` assigns to `module` becomes the resolved value of the
|
||
promise `include("input.js")` returned — that's what `Actions` ends up being
|
||
in the example above. If the included script throws, the promise rejects
|
||
instead.
|
||
|
||
## Full example
|
||
|
||
This is the actual shipped example content (`assets/engine.js` +
|
||
`assets/input.js`):
|
||
|
||
```js
|
||
// input.js — binds physical buttons to abstract actions, then exports the
|
||
// action constants so other scripts don't need to know raw INPUT_ACTION_* names.
|
||
Input.bind("w", INPUT_ACTION_UP);
|
||
Input.bind("s", INPUT_ACTION_DOWN);
|
||
Input.bind("a", INPUT_ACTION_LEFT);
|
||
Input.bind("d", INPUT_ACTION_RIGHT);
|
||
Input.bind("space", INPUT_ACTION_ACCEPT);
|
||
Input.bind("escape", INPUT_ACTION_RAGEQUIT);
|
||
|
||
if(typeof INPUT_GAMEPAD !== "undefined") {
|
||
Input.bind("gamepad_up", INPUT_ACTION_UP);
|
||
Input.bind("gamepad_down", INPUT_ACTION_DOWN);
|
||
Input.bind("gamepad_left", INPUT_ACTION_LEFT);
|
||
Input.bind("gamepad_right", INPUT_ACTION_RIGHT);
|
||
Input.bind("gamepad_a", INPUT_ACTION_ACCEPT);
|
||
Input.bind("gamepad_start", INPUT_ACTION_RAGEQUIT);
|
||
}
|
||
|
||
module = {
|
||
UP: INPUT_ACTION_UP,
|
||
DOWN: INPUT_ACTION_DOWN,
|
||
LEFT: INPUT_ACTION_LEFT,
|
||
RIGHT: INPUT_ACTION_RIGHT,
|
||
ACCEPT: INPUT_ACTION_ACCEPT,
|
||
CANCEL: INPUT_ACTION_CANCEL,
|
||
RAGEQUIT: INPUT_ACTION_RAGEQUIT
|
||
};
|
||
```
|
||
|
||
```js
|
||
// engine.js
|
||
var Actions;
|
||
var camera, cameraPosition;
|
||
var cube, cubePosition, cubeRenderable, cubeMesh;
|
||
|
||
async function init() {
|
||
Actions = await include("input.js");
|
||
|
||
camera = new Entity();
|
||
cameraPosition = camera.add(POSITION);
|
||
camera.add(CAMERA);
|
||
cameraPosition.position = new Vec3(3, 3, -6);
|
||
cameraPosition.lookAt(new Vec3(0, 0, 0));
|
||
|
||
cube = new Entity();
|
||
cubePosition = cube.add(POSITION);
|
||
cubeRenderable = cube.add(RENDERABLE);
|
||
cubeMesh = Mesh.createCube();
|
||
cubeRenderable.mesh = cubeMesh;
|
||
cubeRenderable.color = Color.red();
|
||
}
|
||
|
||
function update() {
|
||
cubePosition.rotation.y += TIME.delta * 1.5;
|
||
cubePosition.rotation.x += TIME.delta * 0.7;
|
||
}
|
||
|
||
function fixedUpdate() {
|
||
var move = 3.0 * TIME.delta;
|
||
if(Input.isDown(Actions.LEFT)) cubePosition.position.x -= move;
|
||
if(Input.isDown(Actions.RIGHT)) cubePosition.position.x += move;
|
||
if(Input.isDown(Actions.UP)) cubePosition.position.z += move;
|
||
if(Input.isDown(Actions.DOWN)) cubePosition.position.z -= move;
|
||
if(Input.pressed(Actions.ACCEPT)) cubePosition.position = new Vec3(0, 0, 0);
|
||
}
|
||
|
||
function deinit() {
|
||
cube.dispose();
|
||
camera.dispose();
|
||
}
|
||
```
|
||
|
||
## API reference
|
||
|
||
### `TIME`
|
||
|
||
Plain global object, live getters (read fresh engine state every access, not
|
||
snapshotted):
|
||
|
||
| Property | Type | Description |
|
||
|---|---|---|
|
||
| `TIME.delta` | number | Seconds since the last frame. |
|
||
| `TIME.time` | number | Total elapsed engine time, in seconds. |
|
||
|
||
### `PLATFORM`
|
||
|
||
A single global string constant — the compile-time target name, e.g.
|
||
`"linux"`, `"psp"`, `"vita"`, `"dolphin"`. Individual platform builds may
|
||
inject additional platform-specific globals via their own
|
||
`modulePlatformPlatform()` hook; those aren't documented here since they vary
|
||
per target.
|
||
|
||
### `Input`
|
||
|
||
Static namespace (not constructible — there's no `new Input()`).
|
||
|
||
| Method | Description |
|
||
|---|---|
|
||
| `Input.bind(buttonName, action)` | Binds a physical button/key (string, e.g. `"w"`, `"space"`, `"gamepad_up"`) to an abstract `INPUT_ACTION_*` constant. Many buttons can bind to the same action. Throws on an empty/unrecognized button name or invalid action. |
|
||
| `Input.isDown(action)` → boolean | Is the action currently held. |
|
||
| `Input.pressed(action)` → boolean | Action transitioned to down this frame. |
|
||
| `Input.released(action)` → boolean | Action transitioned to up this frame. |
|
||
| `Input.getValue(action)` → number | Current analog value for the action. |
|
||
| `Input.axis(negAction, posAction)` → number | Combined axis value from two opposing actions. |
|
||
| `Input.axis2D(negX, posX, negY, posY)` → `Vec2` | Combined 2D axis from four actions. |
|
||
|
||
Global `INPUT_ACTION_*` constants (names are stable API; treat the numeric
|
||
values as opaque/build-specific): `INPUT_ACTION_UP`, `INPUT_ACTION_DOWN`,
|
||
`INPUT_ACTION_LEFT`, `INPUT_ACTION_RIGHT`, `INPUT_ACTION_ACCEPT`,
|
||
`INPUT_ACTION_CANCEL`, `INPUT_ACTION_PAUSE`, `INPUT_ACTION_RAGEQUIT`,
|
||
`INPUT_ACTION_CONSOLE`, `INPUT_ACTION_POINTERX`, `INPUT_ACTION_POINTERY`.
|
||
|
||
Conditionally-defined boolean globals reflecting build capability — only
|
||
present at all if the corresponding input method is compiled in, so
|
||
feature-test with `typeof`, don't assume they exist:
|
||
`INPUT_KEYBOARD`, `INPUT_GAMEPAD`, `INPUT_POINTER`, `INPUT_TOUCH`.
|
||
|
||
### `Vec2` / `Vec3` / `Vec4`
|
||
|
||
`new Vec2(x?, y?)`, `new Vec3(x?, y?, z?)`, `new Vec4(x?, y?, z?, w?)` — all
|
||
components optional, default `0`.
|
||
|
||
Common instance surface across all three: `.dot(other)`, `.length()`,
|
||
`.lengthSq()`, `.normalize()`, `.negate()`, `.add(other)`, `.sub(other)`,
|
||
`.scale(n)`, `.lerp(other, t)` — each of `add`/`sub`/`scale`/`negate`/
|
||
`normalize`/`lerp` returns a **new** vector (non-mutating). `Vec3` additionally
|
||
has `.cross(other)` and `.distance(other)`; `Vec2` has `.distance(other)` too;
|
||
`Vec4` has neither `.cross()` nor `.distance()`.
|
||
|
||
`Vec4` also has UV aliases over the same four floats: `.u0` (= `.x`), `.v0`
|
||
(= `.y`), `.u1` (= `.z`), `.v1` (= `.w`) — handy for texture-rect style code.
|
||
|
||
All three have `.x`/`.y`(/`.z`/`.w`) get/set properties and a `.toString()`
|
||
like `"Vec3(1, 2, 3)"`.
|
||
|
||
**"Vec3Ref" — live references.** Several engine properties (entity
|
||
`position`/`rotation`/`scale`, physics `velocity`, a mesh vertex's `position`)
|
||
return a vector-*like* object instead of a plain `Vec3`. It has the identical
|
||
`.x`/`.y`/`.z` surface, but reads/writes go straight into the underlying
|
||
native buffer — writing `.x` on `entity.position.position` immediately moves
|
||
the entity, no separate assignment needed. Anywhere the API expects a `Vec3`
|
||
argument, a Vec3Ref works too. You never construct one directly; you only
|
||
ever receive them from properties like the ones above.
|
||
|
||
### `Mat4`
|
||
|
||
`new Mat4()` — always constructs identity; no other constructor form.
|
||
|
||
| Member | Description |
|
||
|---|---|
|
||
| `.mul(other)` → `Mat4` | `this * other`. |
|
||
| `.transpose()` → `Mat4` | |
|
||
| `.inverse()` → `Mat4` | |
|
||
| `.determinant()` → number | |
|
||
| `.mulVec3(vec3, w?)` → `Vec3` | `w` defaults to `1.0` (point); pass `0` for a direction. |
|
||
| `.mulVec4(vec4)` → `Vec4` | |
|
||
| `.translate(vec3)` → `Mat4` | Non-mutating — returns a translated copy. |
|
||
| `.scale(vec3)` → `Mat4` | Non-mutating — returns a scaled copy. |
|
||
| `Mat4.identity()` → `Mat4` | Static. |
|
||
| `Mat4.perspective(fov, aspect, near, far)` → `Mat4` | Static, all 4 args required. |
|
||
| `Mat4.lookAt(eye, center, up)` → `Mat4` | Static, all 3 args required `Vec3`s. |
|
||
|
||
### `Color`
|
||
|
||
`new Color(r?, g?, b?, a?)` — each an int `0..255`, default `255` (so
|
||
`new Color()` is opaque white). Properties `.r`/`.g`/`.b`/`.a` get/set.
|
||
|
||
Named factories, each a zero-arg static returning a new opaque `Color`
|
||
(alpha `255` unless noted): `Color.black()`, `Color.white()`, `Color.red()`,
|
||
`Color.green()`, `Color.blue()`, `Color.yellow()`, `Color.cyan()`,
|
||
`Color.magenta()`, `Color.transparent()` (alpha 0), `Color.transparent_white()`
|
||
(alpha 0), `Color.transparent_black()` (alpha 0), `Color.gray()`,
|
||
`Color.light_gray()`, `Color.dark_gray()`, `Color.orange()`, `Color.purple()`,
|
||
`Color.brown()`, `Color.pink()`, `Color.lime()`, `Color.navy()`,
|
||
`Color.teal()`, `Color.cornflower_blue()`.
|
||
|
||
`Color.rainbow(t?, speed?)` → `Color` — `t` defaults to `TIME.time * 4.0`;
|
||
produces a shifting rainbow color, useful for debug visuals.
|
||
|
||
### `Mesh`
|
||
|
||
`new Mesh(vertexCount)` — allocates an uninitialized CPU-side vertex buffer
|
||
(not yet uploaded to the GPU).
|
||
|
||
| Member | Description |
|
||
|---|---|
|
||
| `.vertices` | Array of vertex wrappers, each with a `.position` (Vec3Ref, writes straight into that vertex). |
|
||
| `.vertexCount` | Read-only. |
|
||
| `.flush()` | Uploads to the GPU. First call initializes the GPU mesh; later calls re-upload the current vertex data — call this after editing `.vertices[i].position`. |
|
||
| `.dispose()` | Frees GPU + CPU resources. |
|
||
|
||
Static engine-owned singletons (read-only, not something you dispose):
|
||
`Mesh.DEFAULT_CUBE`, `Mesh.DEFAULT_QUAD`, `Mesh.DEFAULT_SPHERE`,
|
||
`Mesh.DEFAULT_PLANE`, `Mesh.DEFAULT_CAPSULE`, `Mesh.DEFAULT_TRIPRISM`.
|
||
|
||
Static factories (each builds and uploads a brand-new `Mesh`):
|
||
|
||
| Factory | Notes |
|
||
|---|---|
|
||
| `Mesh.createCube(min?, max?)` | Both `Vec3`, default `(-0.5,-0.5,-0.5)`..`(0.5,0.5,0.5)`. |
|
||
| `Mesh.createQuad(minX?, minY?, maxX?, maxY?)` | Default `-0.5..0.5` both axes; UV fixed `0,0`–`1,1`. |
|
||
| `Mesh.createSphere(radius?, stacks?, sectors?)` | `radius` default `0.5`. |
|
||
| `Mesh.createPlane(width?, height?)` | Defaults `1.0`/`1.0`; XZ-aligned, centered at origin. |
|
||
| `Mesh.createCapsule(radius?, halfHeight?, capRings?, sectors?)` | Defaults `0.5`, `0.5`. |
|
||
| `Mesh.createTriPrism(x0, y0, x1, y1, x2, y2, minZ, maxZ)` | All 8 args required — a triangular cross-section extruded along Z. |
|
||
|
||
### `Entity` and components
|
||
|
||
```js
|
||
var e = new Entity();
|
||
var pos = e.add(POSITION);
|
||
```
|
||
|
||
`new Entity()` allocates an entity. `.id` is the read-only numeric engine ID.
|
||
`.add(TYPE)` adds a component and returns its wrapper (`TYPE` is one of the
|
||
constants below). `.dispose()` removes the entity and all its components.
|
||
|
||
Component-type constants: `POSITION`, `CAMERA`, `RENDERABLE`, `PHYSICS`,
|
||
`TRIGGER`. Each entity also exposes a lowercase getter that returns the
|
||
existing wrapper if the component is present, or `undefined` if not (it does
|
||
**not** add the component — use `.add()` for that): `entity.position`,
|
||
`entity.camera`, `entity.renderable`, `entity.physics`, `entity.trigger`.
|
||
|
||
#### `entity.add(POSITION)` → position component
|
||
|
||
| Member | Description |
|
||
|---|---|
|
||
| `.position` | Vec3Ref. Writing rebuilds the transform automatically. |
|
||
| `.rotation` | Vec3Ref, Euler angles. Same rebuild-on-write behavior. |
|
||
| `.scale` | Vec3Ref. Same rebuild-on-write behavior. |
|
||
| `.parent` | Get/set another position-component wrapper, or `null` to clear parenting. |
|
||
| `.lookAt(target, up?)` | `target` a `Vec3`; `up` defaults to `(0,1,0)`. |
|
||
|
||
#### `entity.add(CAMERA)` → camera component
|
||
|
||
| Member | Description |
|
||
|---|---|
|
||
| `.zNear` / `.zFar` | Numbers. |
|
||
| `.fov` | Only meaningful when `projectionType` is `CAMERA_TYPE_PERSPECTIVE`; otherwise get returns `undefined` and set is a no-op. |
|
||
| `.projectionType` | `CAMERA_TYPE_PERSPECTIVE` or `CAMERA_TYPE_ORTHOGRAPHIC`. |
|
||
| `.orthoTop` / `.orthoBottom` / `.orthoLeft` / `.orthoRight` | Only meaningful in orthographic mode, same undefined/no-op rule otherwise. |
|
||
|
||
#### `entity.add(RENDERABLE)` → renderable component
|
||
|
||
| Member | Description |
|
||
|---|---|
|
||
| `.type` | `ENTITY_RENDERABLE_TYPE_MATERIAL`, `_SPRITEBATCH`, or `_CALLBACK`. |
|
||
| `.mesh` | Get/set a `Mesh` instance or a `Mesh.DEFAULT_*` singleton. |
|
||
| `.color` | Get/set a `Color` instance (throws if given something else). |
|
||
| `.addSprite({ min?, max?, uvMin?, uvMax? })` | Adds a sprite to this renderable's sprite batch; all fields optional, default zero. |
|
||
| `.clearSprites()` | Clears the sprite batch. |
|
||
| `.setCallback(fn?)` | Switches to `ENTITY_RENDERABLE_TYPE_CALLBACK` and calls `fn()` on every render of this entity. Omit/pass non-function to clear. Exceptions inside `fn` surface as a C error. |
|
||
|
||
#### `entity.add(PHYSICS)` → physics component
|
||
|
||
| Member | Description |
|
||
|---|---|
|
||
| `.velocity` | Vec3Ref, plain (no rebuild-on-write). |
|
||
| `.onGround` | Read-only boolean. |
|
||
| `.bodyType` | `PHYSICS_BODY_STATIC`, `PHYSICS_BODY_DYNAMIC`, `PHYSICS_BODY_KINEMATIC`. |
|
||
| `.applyImpulse(vec3)` | Adds to velocity. No-op on static bodies. |
|
||
| `.setShapeCube(halfExtents)` | `halfExtents` a `Vec3`. |
|
||
| `.setShapeSphere(radius)` | Number. |
|
||
| `.setShapeCapsule(radius, halfHeight)` | Two numbers. |
|
||
| `.setShapePlane(normal, distance)` | `Vec3` + number. |
|
||
|
||
Shape-type constants (for reading `.type` on the underlying shape, not for
|
||
`.bodyType`): `PHYSICS_SHAPE_CUBE`, `PHYSICS_SHAPE_SPHERE`,
|
||
`PHYSICS_SHAPE_CAPSULE`, `PHYSICS_SHAPE_PLANE`.
|
||
|
||
#### `entity.add(TRIGGER)` → trigger component
|
||
|
||
| Member | Description |
|
||
|---|---|
|
||
| `.min` / `.max` | Plain `Vec3` values (copies, not live refs). |
|
||
| `.setBounds(min, max)` | Sets both at once. |
|
||
| `.contains(point)` → boolean | `point` a `Vec3`. |
|
||
|
||
## Not yet available to scripts
|
||
|
||
The following C modules exist and are fully implemented, but aren't currently
|
||
wired into script registration (`moduleRegister()` in
|
||
`src/dusk/script/module/module.h`), so none of these globals exist in a
|
||
script today: `Screen`, `SpriteBatch`, `Text`, `Scene`, `Easing`, `Console`,
|
||
`Engine`. If you need one of these from a script, it needs to be registered
|
||
in `moduleRegister()` first — see the existing entries there and the modules
|
||
under `src/dusk/script/module/` for the pattern to follow.
|