w
This commit is contained in:
@@ -1,3 +1,7 @@
|
|||||||
|
# Documentation
|
||||||
|
- [Scripting](docs/SCRIPTING.md) — writing gameplay logic in JavaScript.
|
||||||
|
- [UI](docs/UI.md) — building buttons/menus/etc from engine/game C code.
|
||||||
|
|
||||||
# Building
|
# Building
|
||||||
Each build target has different requirements. You can take a look at the git
|
Each build target has different requirements. You can take a look at the git
|
||||||
workflow to see how the builds are done for each target. In addition, for
|
workflow to see how the builds are done for each target. In addition, for
|
||||||
|
|||||||
+7
-6
@@ -1,6 +1,7 @@
|
|||||||
var Actions;
|
var Actions;
|
||||||
var camera, cameraPosition;
|
var camera, cameraPosition;
|
||||||
var cube, cubePosition, cubeRenderable, cubeMesh;
|
var cube, cubePosition, cubeRenderable, cubeMesh;
|
||||||
|
var ground, groundPosition, groundRenderable;
|
||||||
|
|
||||||
// init() is called via scriptManagerCallGlobal(), which pumps the asset
|
// init() is called via scriptManagerCallGlobal(), which pumps the asset
|
||||||
// system + job queue until any promise it returns settles - so it's safe
|
// system + job queue until any promise it returns settles - so it's safe
|
||||||
@@ -20,6 +21,12 @@ async function init() {
|
|||||||
cubeMesh = Mesh.createCube();
|
cubeMesh = Mesh.createCube();
|
||||||
cubeRenderable.mesh = cubeMesh;
|
cubeRenderable.mesh = cubeMesh;
|
||||||
cubeRenderable.color = Color.red();
|
cubeRenderable.color = Color.red();
|
||||||
|
|
||||||
|
ground = new Entity();
|
||||||
|
groundPosition = ground.add(POSITION);
|
||||||
|
groundRenderable = ground.add(RENDERABLE);
|
||||||
|
groundRenderable.mesh = Mesh.createCube();
|
||||||
|
groundRenderable.color = Color.dark_gray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs every frame (including dynamic/interpolation frames) - use for
|
// Runs every frame (including dynamic/interpolation frames) - use for
|
||||||
@@ -32,12 +39,6 @@ function update() {
|
|||||||
// Runs once per fixed timestep only - use for gameplay logic that should
|
// Runs once per fixed timestep only - use for gameplay logic that should
|
||||||
// be deterministic and independent of display refresh rate.
|
// be deterministic and independent of display refresh rate.
|
||||||
function fixedUpdate() {
|
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() {
|
function deinit() {
|
||||||
|
|||||||
@@ -0,0 +1,379 @@
|
|||||||
|
# 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.
|
||||||
+255
@@ -0,0 +1,255 @@
|
|||||||
|
# UI
|
||||||
|
|
||||||
|
Dusk's UI system (buttons, checkboxes, sliders, dropdowns, tabs, menus, focus
|
||||||
|
navigation) is a **C-only API**. It is not exposed to JerryScript — see
|
||||||
|
[SCRIPTING.md](SCRIPTING.md) for what scripts *can* touch. If you need a
|
||||||
|
script to open/react to a menu, wire it through a C callback or a game-side
|
||||||
|
flag scripts can poll; there's no bridge for this today.
|
||||||
|
|
||||||
|
## Mental model
|
||||||
|
|
||||||
|
There is no retained-mode UI tree, no automatic dispatch, no scissor/clip-rect
|
||||||
|
API. Every widget is a plain struct you own (usually as a global or
|
||||||
|
scene-owned variable). You call its `xxxInit(...)` once, then call its
|
||||||
|
`xxxDraw(widget, x, y)` yourself, every frame you want it visible, at whatever
|
||||||
|
screen position you choose. Nothing draws itself automatically except three
|
||||||
|
fixed system overlays (overscan bars, debug console, FPS counter) — see
|
||||||
|
[System overlays](#system-overlays-automatic) below.
|
||||||
|
|
||||||
|
### Where UI rendering happens in the frame
|
||||||
|
|
||||||
|
- `uiInit()` / `uiDispose()` run once, at engine startup/shutdown.
|
||||||
|
- `uiUpdate()` runs once per tick (drives focus-navigation input handling).
|
||||||
|
- `uiRender()` runs once per frame, called from inside `sceneRender()` — i.e.
|
||||||
|
**after** the active scene's own 3D/game-world rendering, using an
|
||||||
|
orthographic screen-space projection. Your own widget `xxxDraw()` calls
|
||||||
|
should happen around the same point — typically from your scene's render
|
||||||
|
callback, after world content, so UI draws on top.
|
||||||
|
|
||||||
|
## Widgets
|
||||||
|
|
||||||
|
Every widget follows the same shape: `xxxInit(widget, ...)` zeroes the struct
|
||||||
|
and sets its fields; `xxxDraw(const widget*, x, y) -> errorret_t` draws it at
|
||||||
|
that screen position.
|
||||||
|
|
||||||
|
> **Init before Draw.** `uislider_t`, `uidropdown_t`, and `uitab_t` cache
|
||||||
|
> their label's measured width/height at `Init` time (an optimization —
|
||||||
|
> label text doesn't change after that point). Calling `Draw` before `Init`,
|
||||||
|
> or mutating `->label` directly instead of re-initializing, leaves stale
|
||||||
|
> layout. `uibutton_t`/`uicheckbox_t` don't have this restriction.
|
||||||
|
|
||||||
|
### Button
|
||||||
|
|
||||||
|
```c
|
||||||
|
void uiButtonInit(uibutton_t *button, const char_t *label);
|
||||||
|
bool_t uiButtonIsHighlighted(const uibutton_t *button);
|
||||||
|
void uiButtonSetHighlighted(uibutton_t *button, bool_t highlighted);
|
||||||
|
errorret_t uiButtonDraw(const uibutton_t *button, float_t x, float_t y);
|
||||||
|
```
|
||||||
|
|
||||||
|
Draws `label` in red when highlighted, white otherwise.
|
||||||
|
|
||||||
|
### Checkbox
|
||||||
|
|
||||||
|
```c
|
||||||
|
void uiCheckboxInit(uicheckbox_t *checkbox, const char_t *label);
|
||||||
|
bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox);
|
||||||
|
void uiCheckboxSetChecked(uicheckbox_t *checkbox, bool_t checked);
|
||||||
|
void uiCheckboxToggle(uicheckbox_t *checkbox);
|
||||||
|
bool_t uiCheckboxIsHighlighted(const uicheckbox_t *checkbox);
|
||||||
|
void uiCheckboxSetHighlighted(uicheckbox_t *checkbox, bool_t highlighted);
|
||||||
|
errorret_t uiCheckboxDraw(const uicheckbox_t *checkbox, float_t x, float_t y);
|
||||||
|
```
|
||||||
|
|
||||||
|
Draws `"Y "`/`"N "` then the label.
|
||||||
|
|
||||||
|
### Slider
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef union { float_t f; int32_t i; } uislidervalue_t;
|
||||||
|
|
||||||
|
void uiSliderInitFloat(uislider_t*, const char_t *label,
|
||||||
|
float_t value, float_t min, float_t max, float_t step);
|
||||||
|
void uiSliderInitInt(uislider_t*, const char_t *label,
|
||||||
|
int32_t value, int32_t min, int32_t max, int32_t step);
|
||||||
|
float_t uiSliderGetFloat(const uislider_t*); // works for either type
|
||||||
|
int32_t uiSliderGetInt(const uislider_t*); // asserts type == INT
|
||||||
|
void uiSliderSetFloat(uislider_t*, float_t value); // asserts type == FLOAT, clamps
|
||||||
|
void uiSliderSetInt(uislider_t*, int32_t value); // asserts type == INT, clamps
|
||||||
|
void uiSliderStepUp(uislider_t*); // wraps to min past max
|
||||||
|
void uiSliderStepDown(uislider_t*); // wraps to max past min
|
||||||
|
float_t uiSliderGetRatio(const uislider_t*); // normalized 0..1
|
||||||
|
int32_t uiSliderGetStepCount(const uislider_t*); // 0 for float sliders
|
||||||
|
bool_t uiSliderIsHighlighted(const uislider_t*);
|
||||||
|
void uiSliderSetHighlighted(uislider_t*, bool_t highlighted);
|
||||||
|
errorret_t uiSliderDraw(const uislider_t*, float_t x, float_t y);
|
||||||
|
```
|
||||||
|
|
||||||
|
Draws label, a track, a fill proportional to the current ratio, discrete step
|
||||||
|
markers if it's an int slider with fewer than 10 steps, then the value as
|
||||||
|
text.
|
||||||
|
|
||||||
|
### Dropdown
|
||||||
|
|
||||||
|
```c
|
||||||
|
void uiDropdownInit(uidropdown_t *dropdown, const char_t *label,
|
||||||
|
const char_t *const *options, uint8_t optionCount,
|
||||||
|
uint8_t selectedIndex);
|
||||||
|
uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown);
|
||||||
|
const char_t *uiDropdownGetSelectedOption(const uidropdown_t *dropdown);
|
||||||
|
void uiDropdownSetSelectedIndex(uidropdown_t *dropdown, uint8_t index);
|
||||||
|
void uiDropdownStepNext(uidropdown_t *dropdown); // wraps
|
||||||
|
void uiDropdownStepPrev(uidropdown_t *dropdown); // wraps
|
||||||
|
bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown);
|
||||||
|
void uiDropdownSetHighlighted(uidropdown_t *dropdown, bool_t highlighted);
|
||||||
|
errorret_t uiDropdownDraw(const uidropdown_t *dropdown, float_t x, float_t y);
|
||||||
|
```
|
||||||
|
|
||||||
|
`options` is a caller-owned array of strings that must outlive the dropdown
|
||||||
|
(it isn't copied). Draws `label` then `"< Option >"`.
|
||||||
|
|
||||||
|
### Tab
|
||||||
|
|
||||||
|
```c
|
||||||
|
void uiTabInit(uitab_t *tab, const char_t *label);
|
||||||
|
bool_t uiTabIsActive(const uitab_t *tab);
|
||||||
|
void uiTabSetActive(uitab_t *tab, bool_t active);
|
||||||
|
errorret_t uiTabDraw(const uitab_t *tab, float_t x, float_t y);
|
||||||
|
```
|
||||||
|
|
||||||
|
Draws a background box sized to the label (green if active, red if inactive)
|
||||||
|
with the label on top.
|
||||||
|
|
||||||
|
## Menus: assembling widgets into a navigable list
|
||||||
|
|
||||||
|
`uimenu_t` is the one aggregate widget — it owns an array of items (labels,
|
||||||
|
spacers, and any of the widgets above), lays them out in a grid, and wires
|
||||||
|
keyboard/gamepad navigation via the focus system for you.
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
UI_MENU_WIDGET_TYPE_NONE, UI_MENU_WIDGET_TYPE_LABEL,
|
||||||
|
UI_MENU_WIDGET_TYPE_SPACER, UI_MENU_WIDGET_TYPE_CHECKBOX,
|
||||||
|
UI_MENU_WIDGET_TYPE_BUTTON, UI_MENU_WIDGET_TYPE_TAB,
|
||||||
|
UI_MENU_WIDGET_TYPE_SLIDER, UI_MENU_WIDGET_TYPE_DROPDOWN,
|
||||||
|
} uimenuwidgettype_t;
|
||||||
|
|
||||||
|
void uiMenuInit(uimenu_t *menu, uimenuselectedcallback_t selected,
|
||||||
|
uimenuclosedcallback_t closed, uimenuchangedcallback_t changed);
|
||||||
|
void uiMenuSetItems(uimenu_t *menu, const uimenuitem_t *items,
|
||||||
|
uint8_t itemCount, uint8_t columns);
|
||||||
|
void uiMenuSetPosition(uimenu_t *menu, uint8_t x, uint8_t y); // focus cursor cell, not pixels
|
||||||
|
void uiMenuOpen(uimenu_t *menu); // pushes onto the focus stack
|
||||||
|
void uiMenuClose(uimenu_t *menu); // pops it
|
||||||
|
bool_t uiMenuIsActive(const uimenu_t *menu);
|
||||||
|
errorret_t uiMenuDraw(const uimenu_t *menu, float_t x, float_t y,
|
||||||
|
float_t width, float_t height);
|
||||||
|
```
|
||||||
|
|
||||||
|
- `selected(menu, index, item)` fires when the player presses accept on an item.
|
||||||
|
- `changed(menu, index, item)` fires when the highlighted item changes.
|
||||||
|
- `closed(menu)` fires when the menu is popped off the focus stack.
|
||||||
|
- LEFT/RIGHT on a highlighted slider/checkbox/dropdown adjusts its value in
|
||||||
|
place instead of moving focus off it (handled internally).
|
||||||
|
|
||||||
|
### Building a menu with the `MENU_*` macros
|
||||||
|
|
||||||
|
`uimenu.h` provides macros that cut the boilerplate of filling in a
|
||||||
|
`uimenuitem_t` array. They expand into statements using local variables named
|
||||||
|
`menu`, `menuIndex`, and `menuCapacity`, so use them together, inside one
|
||||||
|
function, starting with `MENU_BEGIN` and ending with `MENU_END`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static uimenuitem_t optionsItems[8];
|
||||||
|
static uimenu_t optionsMenu;
|
||||||
|
static const char_t *qualityOptions[] = { "Low", "Medium", "High" };
|
||||||
|
|
||||||
|
static void onOptionsSelected(
|
||||||
|
const uimenu_t *menu, const uint8_t index, const uimenuitem_t *item
|
||||||
|
) {
|
||||||
|
if(index == 4) uiMenuClose(&optionsMenu); // "Back" button
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onOptionsClosed(const uimenu_t *menu) {
|
||||||
|
// e.g. return to the previous screen
|
||||||
|
}
|
||||||
|
|
||||||
|
void optionsMenuBuild(void) {
|
||||||
|
MENU_BEGIN(&optionsMenu, optionsItems, onOptionsSelected, onOptionsClosed, NULL);
|
||||||
|
MENU_LABEL("Options");
|
||||||
|
MENU_CHECKBOX("Fullscreen");
|
||||||
|
MENU_SLIDER_FLOAT("Volume", 0.8f, 0.0f, 1.0f, 0.05f);
|
||||||
|
MENU_DROPDOWN("Quality", qualityOptions, 3, 1);
|
||||||
|
MENU_BUTTON("Back");
|
||||||
|
MENU_END(optionsItems, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once, when the menu screen becomes active:
|
||||||
|
uiMenuOpen(&optionsMenu);
|
||||||
|
|
||||||
|
// Every frame the menu should be visible:
|
||||||
|
uiMenuDraw(&optionsMenu, 20.0f, 20.0f, 200.0f, 100.0f);
|
||||||
|
|
||||||
|
// When leaving the menu screen:
|
||||||
|
uiMenuClose(&optionsMenu);
|
||||||
|
```
|
||||||
|
|
||||||
|
`MENU_LABEL`/`MENU_SPACER` force a row break and aren't focusable/selectable.
|
||||||
|
Every other `MENU_*` macro calls the matching widget's own `Init` for you.
|
||||||
|
|
||||||
|
> This example is constructed directly from the widget/menu API surface (all
|
||||||
|
> function and macro signatures above are verified against the source), but
|
||||||
|
> there's currently no real menu-building call site anywhere else in the
|
||||||
|
> engine to cross-check the *pattern* against — treat it as a starting point,
|
||||||
|
> not a copy of shipped code.
|
||||||
|
|
||||||
|
## Focus system: navigation underneath `uimenu`
|
||||||
|
|
||||||
|
If you're building a custom widget that needs keyboard/gamepad navigation
|
||||||
|
without going through `uimenu`, use `ui/focus/uifocus.h` directly. `uimenu`
|
||||||
|
is implemented entirely in terms of this API, so it's a reasonable reference.
|
||||||
|
|
||||||
|
```c
|
||||||
|
uifocusitem_t * uiFocusPush(
|
||||||
|
uint8_t cols, uint8_t rows,
|
||||||
|
uifocusitemcallback_t selected, // fires on accept
|
||||||
|
uifocusitemcallback_t changed, // fires on cursor move (and once immediately)
|
||||||
|
uifocusitemcallback_t closed, // fires on pop
|
||||||
|
uifocusitemdirectioncallback_t direction, // optional pre-empt of a direction press; NULL for default grid movement
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
void uiFocusPop(void);
|
||||||
|
void uiFocusPopItem(uifocusitem_t *item);
|
||||||
|
void uiFocusSetPosition(uifocusitem_t *item, uint8_t x, uint8_t y); // wraps
|
||||||
|
void uiFocusMoveDirection(uifocusitem_t *item, uifocusdirection_t dir);
|
||||||
|
```
|
||||||
|
|
||||||
|
`uiFocusUpdate()` runs automatically from `uiUpdate()` every tick — you don't
|
||||||
|
call it yourself. It reads `INPUT_ACTION_ACCEPT` (fires `selected`),
|
||||||
|
`INPUT_ACTION_CANCEL` (pops the stack), and the four directional actions
|
||||||
|
(with hold-to-repeat timing) to move the cursor within the topmost pushed
|
||||||
|
item. Only the topmost stack entry (max depth 8) receives input at a time —
|
||||||
|
opening a submenu means pushing a new focus item on top; closing it pops back
|
||||||
|
to the parent.
|
||||||
|
|
||||||
|
There's no separate "is this widget focused" query — "focused" is expressed
|
||||||
|
as the pushed item's current `(x, y)` cursor cell matching a given slot, which
|
||||||
|
is exactly how `uimenu`'s `changed` callback decides which item to highlight.
|
||||||
|
|
||||||
|
## System overlays (automatic)
|
||||||
|
|
||||||
|
Three small overlays are wired into a fixed internal list and draw themselves
|
||||||
|
every frame with no call needed from game code:
|
||||||
|
|
||||||
|
- **Overscan bars** (`ui/overlay/uicrop.h`) — draws opaque bars over the
|
||||||
|
screen area outside `SCREEN.scanX/scanY/scanWidth/scanHeight` (the
|
||||||
|
overscan-safe viewport). A no-op on platforms/configs where the scan area
|
||||||
|
already equals the full viewport. `UI_CROP.color` (default black) is the
|
||||||
|
only thing you'd normally touch here.
|
||||||
|
- **Debug console** (`ui/debug/uiconsole.h`) — draws console history when
|
||||||
|
visible.
|
||||||
|
- **FPS counter** (`ui/debug/uifps.h`) — draws a live FPS/frame-time readout.
|
||||||
|
|
||||||
|
None of these have a scissor/clip-rect equivalent for your own widgets —
|
||||||
|
there is no clipping API in this UI system; everything draws unclipped at
|
||||||
|
whatever position you give it.
|
||||||
Reference in New Issue
Block a user