@origingame/engine 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/og.mjs +25 -0
- package/dist/web/origin-engine.d.ts +1233 -0
- package/dist/web/origin-engine.js +8628 -0
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +366 -0
- package/dist/web-publish/origin-engine-lanes.js +8509 -0
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +1176 -0
- package/dist/web-publish/origin-engine.js +8313 -0
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +347 -0
- package/dist/web-publish/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
- package/dist/web-publish-2d/origin-engine-lanes.js +8509 -0
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +1176 -0
- package/dist/web-publish-2d/origin-engine.js +8313 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +347 -0
- package/dist/web-publish-2d/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
- package/knowledge/README.md +31 -0
- package/knowledge/knowledge.json +13 -0
- package/knowledge/looks/catalogue.json +1871 -0
- package/knowledge/package.json +33 -0
- package/knowledge/rules/platform.json +329 -0
- package/knowledge/scaffold/README.md +27 -0
- package/knowledge/scaffold/distribution.json +48 -0
- package/knowledge/scaffold/origin-engine.json +19 -0
- package/knowledge/scaffold/template/.origin/setup +14 -0
- package/knowledge/scaffold/template/LICENSES.md +23 -0
- package/knowledge/scaffold/template/README.md +37 -0
- package/knowledge/scaffold/template/assets/ai/guard.ogt +48 -0
- package/knowledge/scaffold/template/assets/audio/click.ogt +9 -0
- package/knowledge/scaffold/template/assets/fonts/NotoSansSC-Regular-subset.ttf.ogt +12 -0
- package/knowledge/scaffold/template/assets/fonts/OFL.txt +93 -0
- package/knowledge/scaffold/template/assets/looks/origin-game.ogt +12 -0
- package/knowledge/scaffold/template/assets/nav/yard.ogt +27 -0
- package/knowledge/scaffold/template/assets/scenes/main.ogt +34 -0
- package/knowledge/scaffold/template/assets/strings/en.ogt +14 -0
- package/knowledge/scaffold/template/assets/textures/stone_n.png.ogt +7 -0
- package/knowledge/scaffold/template/gitattributes +22 -0
- package/knowledge/scaffold/template/package.json +18 -0
- package/knowledge/scaffold/template/project.ogt +25 -0
- package/knowledge/scaffold/template/src/main.ts +246 -0
- package/knowledge/scaffold/template/tests/guard.og.ts +67 -0
- package/knowledge/scaffold/template/tests/smoke.og.ts +43 -0
- package/knowledge/scaffold/template/tsconfig.json +17 -0
- package/knowledge/scripts/lib.ts +203 -0
- package/knowledge/scripts/mount.ts +210 -0
- package/knowledge/scripts/pack.ts +24 -0
- package/knowledge/scripts/scaffold.ts +72 -0
- package/knowledge/scripts/validate.ts +749 -0
- package/knowledge/skills/og-ai/SKILL.md +386 -0
- package/knowledge/skills/og-audio/SKILL.md +191 -0
- package/knowledge/skills/og-build/SKILL.md +330 -0
- package/knowledge/skills/og-core/SKILL.md +285 -0
- package/knowledge/skills/og-feel/SKILL.md +385 -0
- package/knowledge/skills/og-game/SKILL.md +1479 -0
- package/knowledge/skills/og-i18n/SKILL.md +220 -0
- package/knowledge/skills/og-platform-sdk/SKILL.md +199 -0
- package/knowledge/skills/og-taste/SKILL.md +537 -0
- package/knowledge/skills/og-ui/SKILL.md +444 -0
- package/knowledge/wiki/ai.md +627 -0
- package/knowledge/wiki/anim.md +1187 -0
- package/knowledge/wiki/atmosphere.md +636 -0
- package/knowledge/wiki/audio.md +732 -0
- package/knowledge/wiki/camera.md +711 -0
- package/knowledge/wiki/commands.md +357 -0
- package/knowledge/wiki/game.md +220 -0
- package/knowledge/wiki/i18n.md +213 -0
- package/knowledge/wiki/index.md +1027 -0
- package/knowledge/wiki/input.md +1714 -0
- package/knowledge/wiki/load.md +1280 -0
- package/knowledge/wiki/math.md +166 -0
- package/knowledge/wiki/nav.md +1154 -0
- package/knowledge/wiki/physics.md +3142 -0
- package/knowledge/wiki/platform.md +857 -0
- package/knowledge/wiki/probe.md +1692 -0
- package/knowledge/wiki/reflect.md +285 -0
- package/knowledge/wiki/render.md +3511 -0
- package/knowledge/wiki/rng.md +393 -0
- package/knowledge/wiki/stream.md +376 -0
- package/knowledge/wiki/system.md +197 -0
- package/knowledge/wiki/systems.md +43 -0
- package/knowledge/wiki/terrain.md +727 -0
- package/knowledge/wiki/tilemap.md +422 -0
- package/knowledge/wiki/time.md +150 -0
- package/knowledge/wiki/transform.md +230 -0
- package/knowledge/wiki/tween.md +580 -0
- package/knowledge/wiki/ui.md +8000 -0
- package/knowledge/wiki/world.md +909 -0
- package/package.json +68 -4
- package/schema/api.schema.json +29675 -0
- package/shell/README.md +22 -0
- package/shell/brand/README.md +23 -0
- package/shell/brand/icon-192.png +0 -0
- package/shell/brand/icon-512.png +0 -0
- package/shell/brand/mark-on-paper.svg +6 -0
- package/shell/brand/origin-mark.svg +5 -0
- package/shell/brand/origin-motion.css +251 -0
- package/shell/brand/tokens.css +20 -0
- package/shell/brand.ts +132 -0
- package/shell/page.ts +715 -0
- package/shell/platform.ts +203 -0
- package/shell/player.ts +238 -0
- package/shell/shell.css +185 -0
- package/shell/sw.ts +177 -0
- package/shell/transport.ts +515 -0
- package/shell/wire.ts +369 -0
- package/src/e6.ts +69 -0
- package/src/env.d.ts +100 -0
- package/src/generated/abi.ts +980 -0
- package/src/index.ts +193 -0
- package/src/node/audio.ts +86 -0
- package/src/node/build.ts +109 -0
- package/src/node/device.ts +981 -0
- package/src/node/doctor.ts +60 -0
- package/src/node/index.ts +102 -0
- package/src/node/platform.ts +24 -0
- package/src/node/playtest-run.ts +249 -0
- package/src/node/png.ts +130 -0
- package/src/node/polish.ts +976 -0
- package/src/node/run.ts +373 -0
- package/src/node/web-build.ts +808 -0
- package/src/p2.ts +35 -0
- package/src/platform-local.ts +211 -0
- package/src/playtest.ts +131 -0
- package/src/runtime.ts +1509 -0
- package/src/systems/cameras.ts +247 -0
- package/src/ui-kit/bind.ts +141 -0
- package/src/ui-kit/huds.ts +303 -0
- package/src/ui-kit/index.ts +36 -0
- package/src/ui-kit/menus.ts +187 -0
- package/src/ui-kit/settings.ts +131 -0
- package/src/ui-kit/store.ts +222 -0
- package/src/ui-kit/story.ts +89 -0
- package/src/ui-kit/util.ts +106 -0
- package/src/web/a11y.ts +429 -0
- package/src/web/audio-worklet.ts +193 -0
- package/src/web/audio.ts +307 -0
- package/src/web/basis.ts +239 -0
- package/src/web/gamepad.ts +123 -0
- package/src/web/host.ts +166 -0
- package/src/web/input.ts +237 -0
- package/src/web/lanes.ts +21 -0
- package/src/web/play.ts +20 -0
- package/src/web/rpc-ring.ts +110 -0
- package/src/web/settle.ts +122 -0
- package/src/web/shell.ts +643 -0
- package/src/web/text-input.ts +248 -0
- package/src/web/worker.ts +1002 -0
- package/types/bindings.json +13654 -0
- package/types/index.d.ts +20090 -0
- package/vendor/basis/LICENSE +201 -0
- package/vendor/basis/NOTICE +15 -0
- package/vendor/basis/README.md +18 -0
- package/vendor/basis/basis_transcoder.js +19 -0
- package/vendor/basis/basis_transcoder.wasm +0 -0
- package/README.md +0 -3
|
@@ -0,0 +1,1479 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: og-game
|
|
3
|
+
description: Building a game on Origin Engine — the project layout, components and systems on the ECS, the game entry, scenes as .ogt text, assets cooked from assets/ to cooked/ (glTF and MagicaVoxel models), the og.load loaders, streaming groups, sprites, texture arrays and drop shadows, skeletal and stepped animation, effects and physical materials, looks (the frame's art direction as data: og.Stylise, og.MaterialStyle, presets), tone mapping, exposure, environment maps, shadows and antialiasing, fog, sky, day and night, weather and water, terrain, material shaders in WGSL, physics beyond rigid bodies (joints, character and platformer controllers, vehicles, ropes, cloth, 2D and 3D fluids, ragdolls) and its queries, and the performance budget; the entry point, with the map of the other nine skills and the modules each teaches. Use first, and when creating, structuring or extending a TypeScript Origin Engine game (a project with a project.ogt; not a Godot project.godot).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
An Origin Engine project is plain files: TypeScript in `src/`, source assets
|
|
7
|
+
in `assets/`, text scenes, and a manifest. `npm create @origingame/game`
|
|
8
|
+
produces it; everything after that is code and text, never an editor. This
|
|
9
|
+
skill is where to start: what a game is made of, and which skill to read
|
|
10
|
+
next for each part.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## What works
|
|
14
|
+
|
|
15
|
+
Three games were written from this package alone: `examples/lantern-vale`
|
|
16
|
+
(a 3D top-down adventure), `examples/hollowbell` (a 2D platformer) and
|
|
17
|
+
`examples/kestrel-stair` (a 3D third-person action-adventure). What they
|
|
18
|
+
asked for the documented way worked the first time: a Tiled map
|
|
19
|
+
with a blob47 auto-tiled terrain, Tiled's parallax and objects with
|
|
20
|
+
properties (§8); flipbook frame events driving footsteps, strikes and a
|
|
21
|
+
bell; texture arrays as world sprites, screen sprites and emitter layers;
|
|
22
|
+
streaming groups of `.vox` models and textures loading as the camera
|
|
23
|
+
neared them (§6); a look applied before `start` and a second one at a tick
|
|
24
|
+
for the results (§15); `og.physics.overlap` on a layer and with sensors;
|
|
25
|
+
a character controller shoving a pile of bodies (§13); a skinned glTF on
|
|
26
|
+
twos (§9). In The Kestrel Stair: one clip library retargeted at cook time
|
|
27
|
+
onto a Mixamo-named, A-posed character, and at run time
|
|
28
|
+
(`og.anim.retarget`) onto an Unreal-named one (its bones detected) and
|
|
29
|
+
onto one whose bones only a `type rig` file names, all three playing
|
|
30
|
+
through blend trees the first time, the clips' `hit` markers coming
|
|
31
|
+
through both retargets (§9); a slam lunging about a metre by root motion,
|
|
32
|
+
`Skeleton.rootMotion` planar for its length; a navmesh baked from the
|
|
33
|
+
level's glTF (og-ai §1); the 3D character controller over triangle-mesh
|
|
34
|
+
slopes, steps and ledges, refusing a scree too steep (§13); a cape, four
|
|
35
|
+
banners in a gusting wind and a chain with a lamp at its last link (§13);
|
|
36
|
+
`world.snapshot()` and `world.restore()` across a relaunch, bringing back
|
|
37
|
+
93 entities with their cloth, paths and minds (§17). `og cook` took their
|
|
38
|
+
47, 69 and 62 assets cold in 8.2 s, 3.6 s and 8.5 s, about 0.1 s after a
|
|
39
|
+
change, without a warning, and its refusals named the line and the fix.
|
|
40
|
+
|
|
41
|
+
## Writing a game from this package
|
|
42
|
+
|
|
43
|
+
The order the games found productive:
|
|
44
|
+
|
|
45
|
+
1. **`project.ogt`** (§2): name, id, the engine pin, the profile, tick and
|
|
46
|
+
seed. The scaffold writes it; change it there and nowhere else.
|
|
47
|
+
2. **`GameDesc`** in `src/main.ts` (§3): the scene, the look, the systems,
|
|
48
|
+
and from the start a command for each state a test must reach (`play`,
|
|
49
|
+
`warp:<where>`) and facts for what it must check.
|
|
50
|
+
3. **Assets by script**: `tools/make-assets.*` writes every texture,
|
|
51
|
+
tileset, map, flipbook, emitter, sound and look deterministically (every
|
|
52
|
+
game and archetype does), with import settings beside each source
|
|
53
|
+
(§5, §8, §10). Rerun it after a change; never hand-edit its outputs.
|
|
54
|
+
4. **Cook** (`og cook`, og-build §1): read every refusal, it names the line
|
|
55
|
+
and the fix.
|
|
56
|
+
5. **In 3D, the navmesh before the minds** (og-ai §1): bake it from the
|
|
57
|
+
level (its glTF's `model` line), load it in `start` and check
|
|
58
|
+
`og.nav.nearest` at every spawn and route point; only then write the
|
|
59
|
+
behaviours.
|
|
60
|
+
6. **Run with commands** (og-build §1, §4): `og run . --headless
|
|
61
|
+
--commands "...;screenshot:x.png;...;quit"`, then open each frame and
|
|
62
|
+
say what it shows. Do this after every change that shows on screen.
|
|
63
|
+
7. **Playtests with reference frames** (og-build §3): `tests/*.og.ts`
|
|
64
|
+
asserting on facts, `get:` answers and dumps, one per feature, and a
|
|
65
|
+
frames playtest comparing screenshots with `references/<tier>/`, its
|
|
66
|
+
frame reads inside `t.frame`: in a sandbox without a renderer each
|
|
67
|
+
screenshot is reported "no renderer, frame unchecked" and passes while
|
|
68
|
+
every other check runs, and the frames are verified where there is a
|
|
69
|
+
renderer (the author's machine, the browser-check sandbox, an executor
|
|
70
|
+
with a GPU).
|
|
71
|
+
8. **Polish** (og-build §5): `og polish` until its findings are gone; each
|
|
72
|
+
names the section that fixes it (og-taste §10, og-feel §13).
|
|
73
|
+
|
|
74
|
+
Mistakes the games made, or would have, before reading the right page:
|
|
75
|
+
|
|
76
|
+
- **The resource handle.** A system reads or writes a resource only when
|
|
77
|
+
it lists it among its `reads` or `writes` (og-core §4: undeclared, it
|
|
78
|
+
throws naming the system and the record). A field read or write is a
|
|
79
|
+
typed-array access whatever the resource's size, and `world.resource(R)`
|
|
80
|
+
answers the same handle wherever it is called: keep the run's state in
|
|
81
|
+
one resource and write its fields where they change, never through a
|
|
82
|
+
plain copy (og-core §4 has the cost model: a field read or write about
|
|
83
|
+
0.03 µs, the first read after an engine call about 2 µs while the record
|
|
84
|
+
is fetched again, writes sent in one call before the next engine call
|
|
85
|
+
and when the system returns).
|
|
86
|
+
- **A look replaces resources whole.** `GameDesc.look` and
|
|
87
|
+
`og.render.applyLook` set every field of each resource the look names
|
|
88
|
+
(the rest to their defaults), so a per-game value written before the
|
|
89
|
+
look is lost: write it in the look, or in `start` after it (§15).
|
|
90
|
+
- **Where probe outputs go.** `og run` writes screenshots and dumps under
|
|
91
|
+
`og-out/` (`--out` chooses), `og test` each playtest's under
|
|
92
|
+
`og-out/test/<name>/`, and a playtest reads them back by the name its
|
|
93
|
+
step gave (`run.json("shop.json")`). Give every step's file its own
|
|
94
|
+
name, so a check reads the step it means (og-build §1, §3).
|
|
95
|
+
- **An agent off the mesh.** Place every navmesh agent at its
|
|
96
|
+
`og.nav.nearest` point (idle or seeking a target on the mesh), and have a
|
|
97
|
+
playtest read each agent's position and `status` a few ticks in: still
|
|
98
|
+
where it was put, its height too, and not `NAV_UNREACHABLE` (og-ai §2).
|
|
99
|
+
|
|
100
|
+
## 1. The skills
|
|
101
|
+
|
|
102
|
+
| Skill | Read it for | Its modules (`reference/<module>.md`) |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| **og-core** | the rules every line obeys: TypeScript only, the fixed tick, Results, archetype views, reflection | math, reflect, world, system, rng |
|
|
105
|
+
| **og-game** (this one) | the project, scenes, assets and loading, input, 2D, animation, effects, atmosphere, terrain, shaders, physics, performance, looks, light and shadows | game, transform, input, load, stream, render, physics, atmosphere, anim, tilemap, terrain |
|
|
106
|
+
| **og-build** | the loop: cook, run with a script, read frames and dumps, hot reload, playtests, `og polish` (§5 there: the one list of what it scores), shipping | commands, probe |
|
|
107
|
+
| **og-feel** | how much: hit stop, shake, squash, easing, floating text, the camera, frame rates, sorting, parallax, controller tunables | time, tween, camera, systems |
|
|
108
|
+
| **og-ui** | screens and HUDs: templates, widgets, focus, phones, skins and motion | ui |
|
|
109
|
+
| **og-taste** | the look: a style from the premise, composition, colour, type, motion, feedback, atmosphere by intent, the done checklist | — (it uses ui and atmosphere) |
|
|
110
|
+
| **og-ai** | enemies and NPCs: navigation and minds as data | nav, ai |
|
|
111
|
+
| **og-audio** | sound: events, presets, UI sounds, the mix, music, space | audio |
|
|
112
|
+
| **og-i18n** | languages: string tables, messages, fonts, line breaking, right to left | i18n |
|
|
113
|
+
| **og-platform-sdk** | the platform from inside the game: saves, settings, leaderboards, telemetry, points, AI marks | platform |
|
|
114
|
+
|
|
115
|
+
Each module's page names its skill, and `reference/index.md` groups the
|
|
116
|
+
modules the same way. When two skills seem to touch the same thing, the one
|
|
117
|
+
in this table owns it and the other points there.
|
|
118
|
+
|
|
119
|
+
## 2. The project
|
|
120
|
+
|
|
121
|
+
| Path | What it is |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `project.ogt` | The manifest: `name`, `id`, the pinned `engine` version, `entry`, `profile` (`2d` or `3d`), `tick`, `seed`, `scenes`, the first `scene`, `publish` settings |
|
|
124
|
+
| `src/main.ts` | The game entry: its default export is `og.game({...})` |
|
|
125
|
+
| `assets/` | Source assets: glTF and `.vox` models, PNG/JPEG textures, WAV sounds, TTF/OTF fonts, `.ogt` scenes and looks, and `<file>.ogt` import settings |
|
|
126
|
+
| `cooked/` | `og cook` output (`.ogb` blobs and `.cook.ogt`); never committed, the only thing the runtime reads |
|
|
127
|
+
| `tests/*.og.ts` | Playtest scripts `og test` runs headless |
|
|
128
|
+
| `dist/` | `og build` output |
|
|
129
|
+
|
|
130
|
+
The engine version in `project.ogt` is a pin. Upgrading it is an explicit
|
|
131
|
+
change with its own run, never a side effect. Its `tick`, `seed`, `scenes`
|
|
132
|
+
and `scene` are the defaults of the `GameDesc` fields of those names: what
|
|
133
|
+
`og.game({...})` leaves unset takes the manifest's value (else 60, 0, none
|
|
134
|
+
and `main`). Write each in one place; set in both to different values, the
|
|
135
|
+
engine refuses to start and names both.
|
|
136
|
+
|
|
137
|
+
`og check` type-checks the project (tsrs, a Rust port of TypeScript 7,
|
|
138
|
+
installed with the engine package; where it has no package, tsgo through
|
|
139
|
+
`bun x`). `og run`, `og test` and `og build` check before they bundle and
|
|
140
|
+
stop on a type error, printing each diagnostic as `file(line,col): error
|
|
141
|
+
TS<code>: message`; with `--json` the diagnostics are a `check` event
|
|
142
|
+
(`{"event":"check","ok":false,"diagnostics":[{"file":"src/main.ts","line":12,"col":7,"code":2322,"message":"..."}]}`)
|
|
143
|
+
on the same stream as the run's other events. `og dev` checks each save
|
|
144
|
+
beside the rebuild: the reload still happens, the `reload` event carries a
|
|
145
|
+
`check` object of `ok`, `ms` and `errors` (`null` while the check runs)
|
|
146
|
+
and a `check` event with the diagnostics follows; `og test --json` ends
|
|
147
|
+
with a `test` event whose `check` object has the same three. Read the
|
|
148
|
+
diagnostics and fix them before anything else; `--no-check` skips the
|
|
149
|
+
check (never for `og build --publish`). A created project's
|
|
150
|
+
`tsconfig.json` resolves `@origingame/engine` (the generated
|
|
151
|
+
`types/index.d.ts`) and `@origingame/engine/playtest` from `node_modules`.
|
|
152
|
+
An example inside the engine repository has no `node_modules`: its
|
|
153
|
+
`tsconfig.json` maps both through `paths` to
|
|
154
|
+
`packages/engine/types/index.d.ts` and `packages/engine/src/playtest.ts`
|
|
155
|
+
(with `packages/engine/src/env.d.ts` and a `host.d.ts` for the `node:`
|
|
156
|
+
modules and globals its bun scripts touch), and `og check .` reads it
|
|
157
|
+
(`examples/hollowbell`, `examples/lantern-vale`).
|
|
158
|
+
|
|
159
|
+
## 3. Components, resources, systems, the game
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { og, defineComponent, defineSystem, f32, Vector3 } from "@origingame/engine";
|
|
163
|
+
|
|
164
|
+
export const Health = defineComponent("Health", { hp: f32(100, { min: 0, max: 1000, tunable: true }), max: f32(100) });
|
|
165
|
+
export const Velocity = defineComponent("Velocity", { v: Vector3() });
|
|
166
|
+
|
|
167
|
+
export const Move = defineSystem("Move", { reads: [Velocity], writes: [og.Transform] }, (q, time) => {
|
|
168
|
+
for (const a of q.archetypes()) {
|
|
169
|
+
const p = a.column(og.Transform).position;
|
|
170
|
+
const v = a.column(Velocity).v;
|
|
171
|
+
for (let i = 0; i < a.count * 3; i++) p[i] += v[i] * time.dt;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
export default og.game({
|
|
176
|
+
tick: 60,
|
|
177
|
+
scenes: { main: "scenes/main.ogt" },
|
|
178
|
+
systems: [Move],
|
|
179
|
+
commands: { spawn_wave: (world, args) => spawnWave(world, Number(args[0])) },
|
|
180
|
+
commandHelp: { spawn_wave: { usage: "<count>", doc: "Spawns a wave of <count> enemies now." } },
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- A **component** is plain data per entity; a **resource** is a reflected
|
|
185
|
+
singleton (tuning tables, game state) read with `world.resource(token)`.
|
|
186
|
+
Engine-owned ones are tokens on `og`: `og.Transform`, `og.Time`, `og.Rng`,
|
|
187
|
+
`og.Input`, `og.Camera`.
|
|
188
|
+
- A **system** is a function over its matched archetypes, once per tick
|
|
189
|
+
(og-core §4 has the rules). Keep each system to one job; share state through
|
|
190
|
+
components and resources, never module variables.
|
|
191
|
+
- **Entities** are spawned with `world.spawn({ name, parent, position })` and
|
|
192
|
+
addressed by name everywhere outside the hot loop: `world.find(name)`, the
|
|
193
|
+
probe's ids, `teleport`, `set`, `dump_entity`. Give every entity a person
|
|
194
|
+
will ask about a stable name.
|
|
195
|
+
- **Commands** in `GameDesc.commands` are snake_case actions the command
|
|
196
|
+
channel and playtests can call (`0.5:spawn_wave:3`), with their usage in
|
|
197
|
+
`GameDesc.commandHelp` for `help` (og-build §1). Add one for each state a
|
|
198
|
+
test needs to reach quickly, instead of playing there.
|
|
199
|
+
- **Facts** (`GameDesc.facts`) return a small JSON object for the probe's
|
|
200
|
+
`state()`: score, phase, lives. Tests assert on facts and dumps.
|
|
201
|
+
|
|
202
|
+
`reference/game.md` is `og.game` and every `GameDesc` field;
|
|
203
|
+
`reference/transform.md` is `og.Transform`, parents and the matrix helpers.
|
|
204
|
+
|
|
205
|
+
## 4. Scenes are text
|
|
206
|
+
|
|
207
|
+
A scene is an `.ogt` file under `assets/`, cooked like any asset:
|
|
208
|
+
|
|
209
|
+
```text
|
|
210
|
+
type scene
|
|
211
|
+
version 1
|
|
212
|
+
---
|
|
213
|
+
name "Main"
|
|
214
|
+
entity "ship" {
|
|
215
|
+
model "models/dropship.glb"
|
|
216
|
+
Transform {
|
|
217
|
+
position 0 1.5 0
|
|
218
|
+
}
|
|
219
|
+
entity "light" {
|
|
220
|
+
Transform {
|
|
221
|
+
position 0 3 0
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Each `entity` has a name, optional `model` (a glTF under `assets/` whose
|
|
228
|
+
default scene becomes the entity's children), component blocks keyed by the
|
|
229
|
+
component's reflected name with one line per field, and nested child
|
|
230
|
+
entities. Fields not written take their defaults. Edit scenes as text and
|
|
231
|
+
diff them like code; the cook checks field names and lane counts against the
|
|
232
|
+
registered schema and refuses a model path that does not exist.
|
|
233
|
+
|
|
234
|
+
## 5. Assets: assets/ to cooked/
|
|
235
|
+
|
|
236
|
+
- Put sources in `assets/`; `og cook` turns them into `cooked/`: glTF into
|
|
237
|
+
meshopt-compressed meshes with LODs, textures into KTX2 Basis Universal
|
|
238
|
+
(ETC1S for colour, UASTC for normal and linear maps), WAV into QOA, fonts
|
|
239
|
+
into MSDF atlases, scenes into component tables. Import settings live next
|
|
240
|
+
to the source as `<file>.ogt` (`texture { kind normal }`,
|
|
241
|
+
`font { charset latin chars "开始游戏" }`).
|
|
242
|
+
- At load a cooked texture reaches the GPU block-compressed in the device's
|
|
243
|
+
format with its mips (BC7, and BC5 for normal maps, on desktop GPUs; ASTC
|
|
244
|
+
on Apple and most Android; ETC2 and EAC elsewhere), a quarter of RGBA8's
|
|
245
|
+
memory. Give textures sizes in multiples of 4 (powers of two are best): a
|
|
246
|
+
size that is not, pixel art (`kind pixel`) and fonts stay RGBA8.
|
|
247
|
+
- Declare the characters a font must show (`chars`) rather than cooking a
|
|
248
|
+
whole CJK charset: the full set is several times the size of the font.
|
|
249
|
+
- MagicaVoxel `.vox` files are models: the cook meshes every model of the
|
|
250
|
+
file (greedy quads, flat-shaded, the palette as vertex colours) and its
|
|
251
|
+
scene graph into a glTF, z-up turned y-up, each model about its centre
|
|
252
|
+
(MagicaVoxel's pivot, with or without a scene graph), with one white
|
|
253
|
+
rough material your `og.MaterialStyle` or own material replaces. Load
|
|
254
|
+
and place it as any glTF: `og.load.gltf(world, "models/hut.vox")`, or a
|
|
255
|
+
scene's `model "models/hut.vox"`. The `voxel-worlds` look (§15) suits
|
|
256
|
+
them. Its import settings (`models/hut.vox.ogt`) set the size and the
|
|
257
|
+
origin; without them a voxel is one metre and the origin is the editor's
|
|
258
|
+
(a character 16 voxels tall would be 16 m, its feet 8 m below its
|
|
259
|
+
position):
|
|
260
|
+
|
|
261
|
+
```text
|
|
262
|
+
type import
|
|
263
|
+
version 1
|
|
264
|
+
---
|
|
265
|
+
model {
|
|
266
|
+
voxScale 0.1 # metres per voxel (default 1)
|
|
267
|
+
voxOrigin bottom # center (default: the editor's origin) | bottom (the lowest voxel on y = 0)
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
A `.vox` with frames (MagicaVoxel's animation) cooks to clips that swap
|
|
272
|
+
the frames' models: play them as skinned clips (§9). `voxFps 8` sets the
|
|
273
|
+
rate (default 10), `voxClip walk 0 3` names frames 0 to 3 (repeatable;
|
|
274
|
+
without it one clip `frames` holds every frame). Each frame is centred on
|
|
275
|
+
its own pivot, as in the editor: give the frames one height (or pad them)
|
|
276
|
+
for a character whose feet stay on the ground.
|
|
277
|
+
- Reusable production assets come through the platform's asset catalog into
|
|
278
|
+
`assets/`; record each file's licence next to it.
|
|
279
|
+
|
|
280
|
+
## 6. Loading
|
|
281
|
+
|
|
282
|
+
`og.load.*` returns a handle at once and completes at a tick boundary; under
|
|
283
|
+
`og run` and in replays every load completes before the next tick, so the
|
|
284
|
+
tick it lands on is deterministic. Paths are asset ids (the path under
|
|
285
|
+
`assets/`). Check `og.load.status` (or the `AssetReady` / `AssetFailed`
|
|
286
|
+
events) before using an asset, and spawn a loaded scene or model with
|
|
287
|
+
`world.instantiate(asset, { name, position })`. The scene in
|
|
288
|
+
`GameDesc.scene` is spawned for you when the world starts. `reference/load.md`
|
|
289
|
+
lists every loader and what each returns.
|
|
290
|
+
|
|
291
|
+
**Streaming groups** (`reference/stream.md`) keep a big world's assets off
|
|
292
|
+
the GPU until the player is near them. A scene names glTF models (no skins
|
|
293
|
+
or morph targets), textures, texture arrays, sounds, environment maps,
|
|
294
|
+
palettes, looks and colour LUTs (`.cube`) that load and unload together,
|
|
295
|
+
and what loads them:
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
stream "harbour" {
|
|
299
|
+
asset "models/harbour.glb"
|
|
300
|
+
asset "textures/harbour-sign.png"
|
|
301
|
+
asset "audio/gulls.wav"
|
|
302
|
+
distance 0 0 -200 80 # the camera within 80 m of (0, 0, -200)
|
|
303
|
+
unload 120 # kept until past 120 m (1.25 x the radius by default)
|
|
304
|
+
priority 2 # higher loads first and is evicted last
|
|
305
|
+
}
|
|
306
|
+
stream "cave" {
|
|
307
|
+
asset "models/cave.glb"
|
|
308
|
+
volume "cave-mouth" # the camera inside that entity's box
|
|
309
|
+
}
|
|
310
|
+
stream "boss" {
|
|
311
|
+
asset "models/boss-arena.glb" # no trigger: og.stream.request only
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Load and spawn grouped assets as usual: until the group is resident a model
|
|
316
|
+
draws its proxy (its coarsest LOD, untextured), a texture is a grey 1x1 and
|
|
317
|
+
a sound plays silence of its own length; then the real ones take over the
|
|
318
|
+
same handles. Physics always collides with the proxy, so the simulation and
|
|
319
|
+
the world hash never depend on what is resident. Texture arrays,
|
|
320
|
+
environment maps, palettes, looks and LUTs have no stand-in: load them once
|
|
321
|
+
the group is resident; when it unloads their handles wait (the sky lights
|
|
322
|
+
the scene, `og.render.sprite` of the array is refused, an `og.Sprite` or a
|
|
323
|
+
particle emitter of it draws nothing, `PostEffects.lut` grades nothing) and
|
|
324
|
+
come back with the group. One the frame still uses when its group unloads
|
|
325
|
+
is kept, with a warning, until the frame stops using it, and its bytes
|
|
326
|
+
count in the budget meanwhile (`og.stream.budget().kept`). Groups are admitted by
|
|
327
|
+
priority within `og.stream.setBudget` (256 MiB by default); a lower-priority
|
|
328
|
+
group is evicted to make room for a higher one. `og.stream.status`,
|
|
329
|
+
`og.stream.budget`, `og.StreamEvent` (state `STREAM_RESIDENT` and the rest)
|
|
330
|
+
and the probe's `dump_stream` say where each group is. `og build --publish`
|
|
331
|
+
ships each group as its own file, fetched only when the group loads.
|
|
332
|
+
|
|
333
|
+
There is no unload call for a single asset: memory comes back when a
|
|
334
|
+
group unloads (its trigger lets go, or `og.stream.release(world, "boss")`
|
|
335
|
+
after `og.stream.request`). Keep the handles you have; they wait and come
|
|
336
|
+
back with the group, never load them again. To let a kept asset go, stop
|
|
337
|
+
using it (`og.Environment.map` or `PostEffects.lut` to 0, another look).
|
|
338
|
+
`StreamStatus` (`og.stream.status(world, "harbour")`) has the group's
|
|
339
|
+
`state`, `trigger`, `memory`, `kept`, `loads` and `reason`; `StreamBudget`
|
|
340
|
+
has `budget`, `used` (kept bytes included), `kept` and `peak`. At most one
|
|
341
|
+
group is put in place per tick boundary, and a group larger than the whole
|
|
342
|
+
budget fails. A chunked sound (og-audio §5) may join a group: its first
|
|
343
|
+
chunk comes in the pack and its other chunks are read ahead of its
|
|
344
|
+
playheads only while the group is resident.
|
|
345
|
+
|
|
346
|
+
## 7. Input
|
|
347
|
+
|
|
348
|
+
`world.resource(og.Input)` is the keyboard, the mouse and the first touch
|
|
349
|
+
(`down("KeyW")`, `pressed`, `axis`, `buttonDown`). Two more resources carry
|
|
350
|
+
what one pointer cannot: `og.Pointers` (pointer 0 is og.Input's, 1 to 4
|
|
351
|
+
further touches: `down(id)`, `pressed(id)`, `position(id)`, `start(id)`)
|
|
352
|
+
and `og.Gamepad` (up to four pads: `connected(pad)`, `down("a", pad)`,
|
|
353
|
+
`pressed`, `released`, `axis("leftX", pad)`; sticks are y down). All three
|
|
354
|
+
are world state, so playtests and replays reproduce them; script them with
|
|
355
|
+
`touch:<id>:<down|move|up>:<x,y>` and `gamepad:<a+b+...|->[@pad][:lx,ly][:lt,rt]`
|
|
356
|
+
(og-build §1). The fields and methods are `reference/input.md`.
|
|
357
|
+
|
|
358
|
+
A press happens somewhere inside a tick, and the tick reads it at its
|
|
359
|
+
start: `og.KeyDown`, `og.KeyUp`, `og.PointerDown` and `og.PointerUp` carry
|
|
360
|
+
`at`, when it happened in ticks from the start of the tick that reads it
|
|
361
|
+
(-1 to 1; from the browser event's time in a page, from the step's time
|
|
362
|
+
in a command script; 0 for the edges a tick's end raises and for gamepad
|
|
363
|
+
presses, which are polled). Timing that matters to the millisecond (a
|
|
364
|
+
rhythm game's judgement) uses `time.elapsed + e.at * time.dt`; everything
|
|
365
|
+
else ignores it. The stamp is world state, so a replay reproduces it.
|
|
366
|
+
|
|
367
|
+
## 8. 2D: sprites, flipbooks, tilemaps
|
|
368
|
+
|
|
369
|
+
- **Sprites** are components: an entity with `og.Transform` and `og.Sprite`
|
|
370
|
+
(`width`, `height`, `anchor`, `color`, `opacity`, `layer`, `sortY`,
|
|
371
|
+
`flipX`, `flash`, `shadow`, nine-slice `sliceX`/`sliceY`) is drawn every
|
|
372
|
+
frame at its transform, its scale multiplying the size (so squash and
|
|
373
|
+
stretch is a tween of `og.Transform.scale`). `og.render.sprite` stays for
|
|
374
|
+
one-tick, immediate drawing (HUDs, debug).
|
|
375
|
+
- **Pixel sprites in a 3D set** stand on the ground with `og.Sprite.dropShadow`
|
|
376
|
+
(or `SpriteDesc.dropShadow`): 0.5 casts the sprite's shape, darkened and
|
|
377
|
+
flattened onto the ground plane (y 0) along the sun, the first
|
|
378
|
+
`og.DirectionalLight`, as a 3D model's shadow would fall. It needs a
|
|
379
|
+
perspective camera over the xz ground (a 2D camera sees the plane edge
|
|
380
|
+
on), draws nothing with the sun below the horizon, and set geometry in
|
|
381
|
+
front hides it. `shadow` is the soft blob under the feet instead; the
|
|
382
|
+
`pixel-3d-hybrid` look (§15) wants both.
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
world.insert(hero, og.Sprite, { texture: tex, width: 1, height: 1.5, anchor: { x: 0.5, y: 0 }, dropShadow: 0.5, shadow: 0.3 });
|
|
386
|
+
```
|
|
387
|
+
- **World UI that must not change with the light** (speech bubbles, name
|
|
388
|
+
tags, markers) takes `og.Sprite.toneMapped: false` (or
|
|
389
|
+
`SpriteDesc.toneMapped`), three.js's `Material.toneMapped`: still placed
|
|
390
|
+
and depth tested in the world, but shown in its colours as given,
|
|
391
|
+
outside auto exposure, bloom, fog and the tone curve, so a cream bubble
|
|
392
|
+
stays cream at dusk instead of blowing out to white. Alpha-blended
|
|
393
|
+
sprites only.
|
|
394
|
+
- **Sprite sheets** are `.ogt` files of `type flipbook` next to the PNG
|
|
395
|
+
(grid or named frames, or an Aseprite / TexturePacker JSON `atlas`; clips
|
|
396
|
+
with `fps`, `mode loop | pingPong | once` and `event <frame> "name"`).
|
|
397
|
+
`og.load.flipbook(world, "sprites/hero.ogt")` answers the sheet and its
|
|
398
|
+
clips; `og.Flipbook { sheet, clip: og.anim.clip(info, "walk") }` on the
|
|
399
|
+
sprite's entity plays it, and `og.FlipbookEvent` reports the frames named
|
|
400
|
+
by `event` (footsteps, the frame a hit lands). Give pixel art
|
|
401
|
+
`texture { kind pixel }` import settings (lossless, nearest).
|
|
402
|
+
- **Texture arrays** batch frames: `og.load.textureArray` (a `type
|
|
403
|
+
texture-array` of same-sized images) and `og.Sprite { array, arrayLayer }`
|
|
404
|
+
(or `og.render.sprite`'s `array`, world or `screen`) draw every sprite of
|
|
405
|
+
one array in one draw whichever layer each shows, glow and blend
|
|
406
|
+
included. A flipbook with `array true` cooks its frames into an array
|
|
407
|
+
(frame n is layer n, one size); a 2D emitter's `array "<id>" random` (or
|
|
408
|
+
`life <cycles>`, `fps <n>`) draws its particles from the layers.
|
|
409
|
+
|
|
410
|
+
```text
|
|
411
|
+
type texture-array
|
|
412
|
+
version 1
|
|
413
|
+
---
|
|
414
|
+
kind pixel # color (default), normal, linear or pixel
|
|
415
|
+
layer "tiles/grass.png" # 1 to 256 layers, all one size
|
|
416
|
+
layer "tiles/dirt.png"
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
const tiles = must(og.load.textureArray(world, "tiles/ground.ogt")); // TextureArrayInfo: array, width, height, layers, bytes
|
|
421
|
+
world.insert(cell, og.Sprite, { array: tiles.array, arrayLayer: 1, width: 1, height: 1 });
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
`og.Sprite.arrayLayer` past the last layer draws the last, while
|
|
425
|
+
`SpriteDesc.arrayLayer` past it is refused. An array sheet's
|
|
426
|
+
`FlipbookInfo.array` is its array (its `texture` is 0). Emitters take
|
|
427
|
+
arrays in sprite mode only (`render sprite <layer>`), with `round true` to
|
|
428
|
+
fade each particle to a round edge; materials and terrain layers do not
|
|
429
|
+
sample arrays.
|
|
430
|
+
- **Tilemaps**: a tileset `.ogt` (`type tileset`: grid, `solid` tiles,
|
|
431
|
+
`collide` rects, animated tiles, `terrain` auto-tiling rules) and a map:
|
|
432
|
+
a Tiled `.tmj`, an LDtk `.ldtk` or a hand-written `.ogt` of `type tilemap`
|
|
433
|
+
(layers of rows, objects). `og.load.tilemap` answers its layers and
|
|
434
|
+
objects; an entity with `og.Tilemap { map }` draws it from its position
|
|
435
|
+
(top-left corner) and gives the collision layers static colliders.
|
|
436
|
+
`og.tilemap.setTile` edits a cell (terrain layers re-tile their
|
|
437
|
+
neighbours, colliders follow); `getTile` answers the tile there (on a
|
|
438
|
+
terrain layer the one auto-tiling chose, -1 when empty), `cellAt` and
|
|
439
|
+
`cellCenter` convert positions. Layers scroll by their `parallax`;
|
|
440
|
+
`repeat x` tiles a background (`reference/tilemap.md`). Every line of a
|
|
441
|
+
tileset and a map, and what the cook reads of a Tiled map, is the
|
|
442
|
+
grammar at the end of this section.
|
|
443
|
+
|
|
444
|
+
How many frames, how fast, which layer and how much parallax is og-feel
|
|
445
|
+
§9–§11. `templates/archetypes/topdown` (a Zelda-like dungeon: a Tiled map
|
|
446
|
+
with auto-tiled walls and collision, a four-direction flipbook hero on a
|
|
447
|
+
top-down character controller, an enemy with a hurt flash and knockback,
|
|
448
|
+
pickups with floating text, a door to a second room the camera scrolls to),
|
|
449
|
+
`templates/archetypes/runner` and `templates/archetypes/match3` use all of
|
|
450
|
+
this; `templates/archetypes/roguelike` carves a tilemap at run time from
|
|
451
|
+
og.Rng and keeps its fog of war on a layer, and
|
|
452
|
+
`templates/archetypes/tower-defense` writes placements to a map's layer.
|
|
453
|
+
|
|
454
|
+
### Tilesets, maps and Tiled: the grammar
|
|
455
|
+
|
|
456
|
+
A **tileset** is an `.ogt` of `type tileset` beside its atlas PNG (give the
|
|
457
|
+
PNG `texture { kind pixel }` import settings). Tile ids count from 0, left
|
|
458
|
+
to right and top to bottom of the grid. Every line, with its default:
|
|
459
|
+
|
|
460
|
+
```text
|
|
461
|
+
type tileset
|
|
462
|
+
version 1
|
|
463
|
+
---
|
|
464
|
+
texture "tiles/dungeon.png" # the atlas, an asset id (required)
|
|
465
|
+
grid 16 16 8 8 # <tile width px> <tile height px> <columns> <rows> (required)
|
|
466
|
+
margin 0 # pixels round the grid (default 0)
|
|
467
|
+
spacing 0 # pixels between tiles (default 0)
|
|
468
|
+
filter nearest # nearest (default) | linear
|
|
469
|
+
solid 0 1 2 3 # tiles that collide over the whole cell (any number of lines)
|
|
470
|
+
tile 50 {
|
|
471
|
+
collide 0 8 16 8 # a collision rectangle: x y w h in pixels from the tile's top-left, y down
|
|
472
|
+
}
|
|
473
|
+
tile 60 {
|
|
474
|
+
solid # the whole cell (as a `solid` line)
|
|
475
|
+
}
|
|
476
|
+
tile 51 {
|
|
477
|
+
frames 51 52 53 52 # an animated tile: these tiles in turn
|
|
478
|
+
fps 7 # its rate, or `durations 0.1 0.1 0.2 0.1` (seconds, one per frame)
|
|
479
|
+
}
|
|
480
|
+
terrain wall { # an auto-tiling terrain, named by a map layer
|
|
481
|
+
mode blob47 # blob47 (eight neighbours, 47 shapes) | bitmask16 (four edges, 16 shapes)
|
|
482
|
+
rule 0 0 # <neighbour mask> <tile>: one line per shape drawn
|
|
483
|
+
default 46 # the tile for a mask no rule names (required)
|
|
484
|
+
edge filled # filled (default): cells off the map count as filled | empty
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
The neighbour mask sums the neighbours filled with the same terrain:
|
|
489
|
+
bitmask16 counts N 1, E 2, S 4, W 8; blob47 counts N 1, NE 2, E 4, SE 8,
|
|
490
|
+
S 16, SW 32, W 64, NW 128, a corner only when both its edges are set, so
|
|
491
|
+
exactly 47 masks occur and the cook refuses a `rule` for any other with
|
|
492
|
+
its line. The collision of a tile counts only on a layer marked
|
|
493
|
+
`collision`. `templates/archetypes/topdown/tools/make-assets.py` draws the
|
|
494
|
+
47 wall shapes and writes their rules (`BLOB`, the sorted reduced masks,
|
|
495
|
+
tile id = index); its `assets/tiles/dungeon.ogt`, the rules shortened:
|
|
496
|
+
|
|
497
|
+
```text
|
|
498
|
+
type tileset
|
|
499
|
+
version 1
|
|
500
|
+
---
|
|
501
|
+
# tiles 0-46 are the wall terrain's 47 blob shapes, 47-49 floor, 50 moss, 51-53 the torch, 54-55 the door
|
|
502
|
+
texture "tiles/dungeon.png"
|
|
503
|
+
grid 16 16 8 8
|
|
504
|
+
filter nearest
|
|
505
|
+
solid 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
|
506
|
+
tile 51 {
|
|
507
|
+
frames 51 52 53 52
|
|
508
|
+
fps 7
|
|
509
|
+
}
|
|
510
|
+
terrain wall {
|
|
511
|
+
mode blob47
|
|
512
|
+
rule 0 0
|
|
513
|
+
rule 1 1
|
|
514
|
+
rule 4 2
|
|
515
|
+
# ... one rule per reduced mask, in order: 5 7 16 17 20 21 23 28 29 31 64 65 68 69 71 80 81 84 85 87
|
|
516
|
+
# 92 93 95 112 113 116 117 119 124 125 127 193 197 199 209 213 215 221 223 241 245 247 253
|
|
517
|
+
rule 255 46
|
|
518
|
+
default 46
|
|
519
|
+
edge filled
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
A **hand-written map** is an `.ogt` of `type tilemap` (the runner's and
|
|
524
|
+
the tower-defense archetype's `assets/maps/`):
|
|
525
|
+
|
|
526
|
+
```text
|
|
527
|
+
type tilemap
|
|
528
|
+
version 1
|
|
529
|
+
---
|
|
530
|
+
size 32 18 # cells across and down (required)
|
|
531
|
+
tile 16 16 # a tile's pixels (required)
|
|
532
|
+
unit 1 # world units per tile width (default 1)
|
|
533
|
+
tileset "tiles/dungeon.ogt" # one line per tileset, indexed from 0 (at least one)
|
|
534
|
+
layer walls {
|
|
535
|
+
tileset 0 # which tileset (default 0)
|
|
536
|
+
z 5 # sprite layer offset from og.Tilemap.layer (default: the layer's index)
|
|
537
|
+
parallax 0.5 1 # scroll factors (default 1 1; 0 stays with the camera)
|
|
538
|
+
offset 0 0 # world units (default 0 0)
|
|
539
|
+
opacity 1 # (default 1)
|
|
540
|
+
visible true # (default true)
|
|
541
|
+
collision true # its tiles' collision becomes static colliders (default false)
|
|
542
|
+
lit false # the layer takes no 2D light (og.Lighting2D); default true: lit like a sprite in the lit range
|
|
543
|
+
repeat x # x, y or both: tile the layer endlessly (a background)
|
|
544
|
+
terrain wall # a terrain layer: cells are 1 (filled) or 0, the terrain picks the tiles
|
|
545
|
+
legend "#" 1 # a character and its tile id (-1 empty; on a terrain layer 1 filled)
|
|
546
|
+
row "################" # one row per map row, top first, `size` wide; "." is empty unless legended
|
|
547
|
+
# or numeric rows: row 3 3 4 -1 5 (tile ids, -1 empty)
|
|
548
|
+
}
|
|
549
|
+
object door {
|
|
550
|
+
kind "door"
|
|
551
|
+
rect 15 8 1 2 # x y w h in cells (y down); or `at 4 6` for a point
|
|
552
|
+
prop target "room2" # a property, answered as text in TileObject.properties
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
**Tiled** (`.tmj`, saved as JSON): orthogonal and finite maps, layer data
|
|
557
|
+
as CSV or uncompressed Base64 (a compressed layer is refused with the menu
|
|
558
|
+
to change), horizontal and vertical flips kept (a rotated tile is
|
|
559
|
+
refused), group layers flattened, one tileset per layer. An external
|
|
560
|
+
tileset's `source` resolves against the map's folder and its extension
|
|
561
|
+
becomes `.ogt`: `maps/tomb.tmj` naming `../tiles/dungeon.tsx` reads
|
|
562
|
+
`assets/tiles/dungeon.ogt`, which holds the collision, animation and
|
|
563
|
+
terrains (the `.tsx` is Tiled's own and may sit beside it; the cook
|
|
564
|
+
refuses a missing `.ogt`, naming the path). An embedded single-image
|
|
565
|
+
tileset is imported with its tiles' animations, the first rectangle of
|
|
566
|
+
each tile's collision editor and a boolean `solid` tile property. What the
|
|
567
|
+
cook reads of a layer and an object:
|
|
568
|
+
|
|
569
|
+
<!-- grammar: tiled -->
|
|
570
|
+
| Tiled | Where | Read as |
|
|
571
|
+
|---|---|---|
|
|
572
|
+
| `autotile` | tile layer custom property (string) | the layer is a terrain layer of that terrain of its tileset; any tile in a cell marks it filled, the terrain picks the drawn tile |
|
|
573
|
+
| `collision` | tile layer custom property (bool) | the layer's tile collision becomes static colliders |
|
|
574
|
+
| `lit` | tile layer custom property (bool) | false keeps the layer out of og.Lighting2D (a sky or window layer); default true |
|
|
575
|
+
| `z` | tile layer custom property (int) | the sprite layer offset from og.Tilemap.layer (default: the layer's index among the tile layers) |
|
|
576
|
+
| `repeatx`, `repeaty` | tile layer custom properties (bool), or Tiled's own layer fields | the layer repeats along x or y (`repeat` is not a name the cook reads) |
|
|
577
|
+
| `parallaxx`, `parallaxy` | Tiled's own layer fields (Layer Properties, Parallax Factor) | the layer's parallax |
|
|
578
|
+
| `offsetx`, `offsety` | Tiled's own layer and group fields | the layer's offset, a group's added to its layers' |
|
|
579
|
+
| `opacity`, `visible` | Tiled's own layer fields | as named |
|
|
580
|
+
| `type` or `class` | an object's | TileObject.kind |
|
|
581
|
+
| any other | an object's custom properties | TileObject.properties, every value as text |
|
|
582
|
+
| `solid` | an embedded tileset's tile custom property (bool) | the tile collides as a whole cell |
|
|
583
|
+
|
|
584
|
+
`og cook` warns on every custom property it does not read, naming the
|
|
585
|
+
property, its layer, object or tile and the names read there: a tile
|
|
586
|
+
layer's other than the six names above (or one whose value has another
|
|
587
|
+
type: a `collision` written as the string "true"), a tile's other than
|
|
588
|
+
`solid`, the map's, a group's, an object layer's or an image layer's own
|
|
589
|
+
(none is imported), and an object's class-typed one (no text to give). A
|
|
590
|
+
warning fails nothing; the map cooks without the property.
|
|
591
|
+
|
|
592
|
+
Objects keep their name, size and position (a tile object is anchored at
|
|
593
|
+
its bottom, as Tiled draws it). Positions arrive in map-local world units,
|
|
594
|
+
y up from the map's top-left corner. A map's import settings
|
|
595
|
+
(`maps/tomb.tmj.ogt` beside `maps/tomb.tmj`, for any map format):
|
|
596
|
+
|
|
597
|
+
```text
|
|
598
|
+
type import
|
|
599
|
+
version 1
|
|
600
|
+
---
|
|
601
|
+
tilemap {
|
|
602
|
+
unit 1 # world units per tile width (default 1)
|
|
603
|
+
tileset "tiles/x.ogt" # use this tileset for every tileset of the map
|
|
604
|
+
level "Level_0" # LDtk: the level, by identifier (default the first)
|
|
605
|
+
solid 1 2 # LDtk: the IntGrid values that collide (default 1)
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
`templates/archetypes/topdown/tools/make-assets.py` writes
|
|
610
|
+
`assets/maps/tomb.tmj` (a floor layer, a `walls` layer with `autotile`
|
|
611
|
+
`wall` and `collision` true, a `decor` layer at `z` 15 drawn over the hero,
|
|
612
|
+
and an object layer of spawns, coins, the door and two rooms) beside its
|
|
613
|
+
tileset: the pattern for a map made by a script.
|
|
614
|
+
|
|
615
|
+
## 9. Animation
|
|
616
|
+
|
|
617
|
+
Skinned glTF models animate through `og.anim` (`reference/anim.md`):
|
|
618
|
+
`og.load.animation(world, "models/walker.glb")` answers the rig's skeleton
|
|
619
|
+
and clips (`og.load.gltfInfo(world, model).animations` lists the clip
|
|
620
|
+
names too); on the instantiated model's entity, `og.Skeleton { asset }`
|
|
621
|
+
poses it and `og.AnimationPlayer { clip, speed, loop, fade }` plays a clip
|
|
622
|
+
(changing `clip` cross-fades over `fade`; `stepFps` holds each pose for
|
|
623
|
+
1/stepFps seconds: 12 is stop motion on twos, 8 on threes, for clay and
|
|
624
|
+
puppet looks, while events and root motion stay smooth).
|
|
625
|
+
`og.AnimationGraph` runs a blend
|
|
626
|
+
tree with layered state machines registered by `og.anim.graph` (parameters
|
|
627
|
+
such as speed pick the motion; its grammar ends this section), `og.IkChain`
|
|
628
|
+
turns a chain of bones to a target after the clips, `og.Blendshapes` weighs
|
|
629
|
+
morph targets, and `og.AnimationEvent` reports a clip's markers (below).
|
|
630
|
+
An `og.AnimationPlayer` beside a graph plays over it by its `weight`: a
|
|
631
|
+
strike, a bite or a fall at weight 1, back to 0 when `finished`.
|
|
632
|
+
**Root motion** is `og.Skeleton.rootMotion`: `planar` moves the entity's
|
|
633
|
+
Transform by the root joint's xz travel and yaw, `full` by all of it,
|
|
634
|
+
`off` (the default) leaves the hips travelling inside the model; switch it
|
|
635
|
+
to `planar` for a lunge's clip and back to `off` after.
|
|
636
|
+
`templates/archetypes/third-person` (`src/anim.ts`: a 1D blend by speed,
|
|
637
|
+
a hero retargeted at cook time and sentinels at run time) is the pattern,
|
|
638
|
+
and for a 3D action game as a whole: a CharacterController over a level,
|
|
639
|
+
clips retargeted at cook and at run time, blend trees, a navmesh and
|
|
640
|
+
behaviour machines (og-ai).
|
|
641
|
+
`og.anim.clip` finds a clip by name, a model's as a flipbook's (§8):
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
const anim = must(og.load.animation(world, "models/slime.vox"));
|
|
645
|
+
const chase = og.anim.clip(anim, "chase"); // a ClipAsset; 0 when the model has none
|
|
646
|
+
world.insert(slime, og.Skeleton, { asset: anim.skeleton });
|
|
647
|
+
world.insert(slime, og.AnimationPlayer, { clip: chase, loop: true, stepFps: 12 });
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
A `.vox` with frames (§5) is played the same way: its clips (`frames`, or
|
|
651
|
+
the `voxClip` names) swap the frames' models on the frame boundaries.
|
|
652
|
+
|
|
653
|
+
**Markers** are a clip's named times: the frame a strike lands, a
|
|
654
|
+
footstep. The cook reads them from the glTF, in each animation's extras
|
|
655
|
+
(`animations[i].extras."og.events"`, no other key):
|
|
656
|
+
|
|
657
|
+
```json
|
|
658
|
+
{ "name": "attack", "extras": { "og.events": [{ "name": "hit", "time": 0.42 }] } }
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
or from the model's import settings, `event "attack" "hit" 0.42` (clip,
|
|
662
|
+
name, seconds; the `animation` block below). A name is 1 to 64 bytes, a
|
|
663
|
+
time inside the clip and an `event` line's clip one of the glTF's own, or
|
|
664
|
+
the cook refuses it naming the clip; a library's clips bring the markers
|
|
665
|
+
the library carries (in its glTF or its own import settings).
|
|
666
|
+
`ClipInfo.markers` lists a clip's marker names in time order (an empty
|
|
667
|
+
list: the glTF carried none). On the tick playback passes one, through a
|
|
668
|
+
player or a graph, `og.AnimationEvent { entity, clip, name, time, weight }`
|
|
669
|
+
fires; a system that lists `og.AnimationEvent` in its `reads` reads them
|
|
670
|
+
with `world.events(og.AnimationEvent)`. Markers survive retargeting, at
|
|
671
|
+
cook time and at run time, so a library's `hit` lands on every character.
|
|
672
|
+
|
|
673
|
+
**Retargeting** — one clip on characters of other proportions and bone
|
|
674
|
+
names. Keep shared clips in an animation library (a glTF with a skeleton
|
|
675
|
+
and clips, no mesh). At cook time, a character's import settings
|
|
676
|
+
(`models/hero.glb.ogt`) pull them in. A model's `animation` block, every
|
|
677
|
+
line with its default:
|
|
678
|
+
|
|
679
|
+
```text
|
|
680
|
+
type import
|
|
681
|
+
version 1
|
|
682
|
+
---
|
|
683
|
+
animation {
|
|
684
|
+
rate 30 # resampling rate of every clip, 1 to 120 Hz (default: the source's key rate)
|
|
685
|
+
event "attack" "hit" 0.42 # a marker on a clip of this glTF's own: clip, name, seconds (repeatable; or glTF extras."og.events")
|
|
686
|
+
library "anims/humanoid-clips.glb" # its clips land in this model's #anim, retargeted (repeatable)
|
|
687
|
+
rig "rigs/hero.ogt" # how this model's bones are named (`type rig`); default: detected from the node names
|
|
688
|
+
footIk true # retargeting solves the legs so planted feet stay planted (default true)
|
|
689
|
+
compress false # quantised samples and keyframe reduction (default false: f32)
|
|
690
|
+
tolerance 0.0001 # the most compression may move a sampled value (default 0.0001, at most 0.1)
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
A clip the model has of its own wins over a library's of the same name.
|
|
695
|
+
|
|
696
|
+
At run time, `og.anim.retarget(world, { clip, source, target })` answers a
|
|
697
|
+
new clip for `target` (both skeletons from `og.load.animation`). The bones
|
|
698
|
+
are found from the node names (Mixamo, VRM, Unity, Rigify, Unreal, Blender);
|
|
699
|
+
when they are not, write a `type rig` .ogt (`scheme mixamo`, or
|
|
700
|
+
`hips "Root_Pelvis"` per bone), load it with `og.load.rig` and pass it as
|
|
701
|
+
`sourceRig`/`targetRig`. `og.anim.humanoid(world, { skeleton })` says which
|
|
702
|
+
bones were found. The target keeps its bone lengths, its hips travel the
|
|
703
|
+
source's distance scaled by the leg ratio (so root motion matches its
|
|
704
|
+
stride), and its legs are solved so planted feet stay on the ground
|
|
705
|
+
(`footIk`, on by default). The fx-showcase `retarget` station is the
|
|
706
|
+
pattern.
|
|
707
|
+
|
|
708
|
+
**Compression** shrinks a model's clips in the bundle (not in memory: they
|
|
709
|
+
are rebuilt at load). In the same `animation` block, `compress true`
|
|
710
|
+
quantises rotations to 48 bits and the rest to 16 and drops the keyframes
|
|
711
|
+
interpolation rebuilds within `tolerance` (default 0.0001: about 0.1 mm
|
|
712
|
+
and 0.01°); a channel 16 bits cannot hold within it stays exact. Turn it on
|
|
713
|
+
for a game with many clips; leave it off while a clip's motion is still
|
|
714
|
+
being judged.
|
|
715
|
+
|
|
716
|
+
### Blend trees: the graph grammar
|
|
717
|
+
|
|
718
|
+
A graph is `.ogt` text of `type animgraph` passed to `og.anim.graph(world,
|
|
719
|
+
{ text, clips })`; each `clip` line names a clip by `ClipInfo.name` among
|
|
720
|
+
`clips` (a model's `og.load.animation(...).clips`, or retargeted ones).
|
|
721
|
+
Motions (clips and blends of them) form a tree; layers run state machines
|
|
722
|
+
over them. Every line, with its default:
|
|
723
|
+
|
|
724
|
+
```text
|
|
725
|
+
type animgraph
|
|
726
|
+
version 1
|
|
727
|
+
---
|
|
728
|
+
param speed # a parameter: og.AnimationGraph.p0, the next p1, ... (at most 8)
|
|
729
|
+
param wave
|
|
730
|
+
param x
|
|
731
|
+
param y
|
|
732
|
+
clip idle "idle" # clip <motion> "<clip name>"
|
|
733
|
+
clip walk "walk" {
|
|
734
|
+
speed 1 # duration multiplier, above 0 (default 1)
|
|
735
|
+
loop true # false: holds its last frame (default true)
|
|
736
|
+
}
|
|
737
|
+
clip run "run"
|
|
738
|
+
clip waving "wave"
|
|
739
|
+
blend1d move speed { # blend1d <motion> <param>: the two children around the value blend linearly, clamped at the ends
|
|
740
|
+
child idle 0 # child <motion> <threshold>; any order (at least one)
|
|
741
|
+
child walk 1.5
|
|
742
|
+
child run 4
|
|
743
|
+
}
|
|
744
|
+
blend2d strafe x y { # blend2d <motion> <param x> <param y>: children at points, gradient-band weights
|
|
745
|
+
child walk 0 1 # child <motion> <x> <y>
|
|
746
|
+
child idle 0 0
|
|
747
|
+
}
|
|
748
|
+
layer base { # the first layer is the base: root motion and og.AnimationGraph.state come from it
|
|
749
|
+
state still idle # state <name> <motion>
|
|
750
|
+
state move move {
|
|
751
|
+
speed 1 # playback rate, 0 or more (default 1)
|
|
752
|
+
}
|
|
753
|
+
entry still # the state it starts in (default: the first)
|
|
754
|
+
transition still move { # transition <from | any> <to>; in file order, the first that holds fires
|
|
755
|
+
when speed gt 0.2 # when <param> gt | ge | lt | le | eq | ne <number>; every `when` must hold
|
|
756
|
+
exit 0.9 # and the source has played this many cycles (a `when` or an `exit` is required)
|
|
757
|
+
fade 0.25 # cross-fade seconds (default 0.2)
|
|
758
|
+
}
|
|
759
|
+
transition any still { # any: from every state but the target
|
|
760
|
+
when speed le 0.2
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
layer upper {
|
|
764
|
+
mode additive # override (default): replaces the pose below | additive: adds each clip's difference from its first frame
|
|
765
|
+
weight wave # a number 0 to 1, or a parameter clamped to it (default 1)
|
|
766
|
+
state wave waving
|
|
767
|
+
}
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
Each state advances one phase by `dt × AnimationGraph.speed × state speed
|
|
771
|
+
/ duration`, the duration weighted across a blend's clips, so blended
|
|
772
|
+
clips stay in step. Per tick and layer the transitions are checked first,
|
|
773
|
+
then the phases advance, then the poses blend; a transition during a fade
|
|
774
|
+
replaces it. `og.anim.graph` refuses a parse error naming the line, a
|
|
775
|
+
clip name `clips` lacks, a parameter not declared and a motion that
|
|
776
|
+
contains itself. Write the parameters from a system every tick
|
|
777
|
+
(`world.set(body, og.AnimationGraph, { p0: speed })`); `state` and
|
|
778
|
+
`stateTime` read back the base layer's.
|
|
779
|
+
|
|
780
|
+
The third-person archetype's hero (`src/anim.ts`): idle, walk and run by
|
|
781
|
+
the speed across the ground, and the run held mid-stride, slowed, in the
|
|
782
|
+
air:
|
|
783
|
+
|
|
784
|
+
```text
|
|
785
|
+
type animgraph
|
|
786
|
+
version 1
|
|
787
|
+
---
|
|
788
|
+
param speed
|
|
789
|
+
param air
|
|
790
|
+
clip idle "idle"
|
|
791
|
+
clip walk "walk" {
|
|
792
|
+
speed 1.1
|
|
793
|
+
}
|
|
794
|
+
clip run "run"
|
|
795
|
+
blend1d move speed {
|
|
796
|
+
child idle 0
|
|
797
|
+
child walk 2.2
|
|
798
|
+
child run 5.6
|
|
799
|
+
}
|
|
800
|
+
layer base {
|
|
801
|
+
state ground move
|
|
802
|
+
state air run {
|
|
803
|
+
speed 0.3
|
|
804
|
+
}
|
|
805
|
+
entry ground
|
|
806
|
+
transition ground air {
|
|
807
|
+
when air gt 0.5
|
|
808
|
+
fade 0.15
|
|
809
|
+
}
|
|
810
|
+
transition air ground {
|
|
811
|
+
when air lt 0.5
|
|
812
|
+
fade 0.1
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
```ts
|
|
818
|
+
const anim = must(og.load.animation(world, "models/strider.glb"));
|
|
819
|
+
const graph = must(og.anim.graph(world, { text: HERO_GRAPH, clips: anim.clips }));
|
|
820
|
+
world.insert(body, og.Skeleton, { asset: anim.skeleton });
|
|
821
|
+
world.insert(body, og.AnimationGraph, { asset: graph });
|
|
822
|
+
// every tick: world.set(body, og.AnimationGraph, { p0: groundSpeed, p1: airborne ? 1 : 0 });
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
## 10. Effects
|
|
826
|
+
|
|
827
|
+
Every effect is off until the game turns it on, as data:
|
|
828
|
+
|
|
829
|
+
- **Post** — the `og.PostEffects` resource: `ao`, `outline`,
|
|
830
|
+
`depthOfField`, `motionBlur`, `godRays`, `glow`, `lut` (a handle from
|
|
831
|
+
`og.load.lut(world, "luts/look.cube")`), `vignette`,
|
|
832
|
+
`chromaticAberration`, `filmGrain`, `flash`, and a fixed shake of the
|
|
833
|
+
frame through `shakeTrauma` (an earthquake that lasts; a hit's shake is
|
|
834
|
+
`og.camera.shake`, which decays by itself: og-feel §4). A game with fast
|
|
835
|
+
motion wants `render: { antialias: "taa" }` in `og.game`.
|
|
836
|
+
- **Particles** — an emitter is an `.ogt` asset of `type particles`
|
|
837
|
+
(shape, rate, bursts, lifetime, speed, curves over life, texture or
|
|
838
|
+
array, 3D or sprite rendering; every line is the grammar at the end of
|
|
839
|
+
this section) loaded with `og.load.emitter`; an entity with `og.ParticleSystem
|
|
840
|
+
{ emitter, seed, startTick }` emits it at its transform. Particles are
|
|
841
|
+
drawn by the renderer from the tick, deterministic and never part of the
|
|
842
|
+
simulation.
|
|
843
|
+
- **Trails and beams** — components `og.Trail` (follows the entity) and
|
|
844
|
+
`og.Beam` (between two points); `og.render.ribbon(world, desc)` draws a
|
|
845
|
+
polyline this tick.
|
|
846
|
+
- **Decals** — `og.Decal` projects an atlas rectangle
|
|
847
|
+
(`og.RenderSettings.decalAtlas`) onto what its box covers.
|
|
848
|
+
- **2D lighting** — `og.Lighting2D` (ambient, the lit layer range),
|
|
849
|
+
`og.Light2D` and `og.Occluder2D` components, `castShadow` on sprites;
|
|
850
|
+
sprites also take `flash`, `glow` and `blend`.
|
|
851
|
+
- **Materials** — toon, rim, dissolve, hit flash and outline are
|
|
852
|
+
`og.render.standardMaterial` fields; animate them with
|
|
853
|
+
`og.render.updateMaterial(world, material, { dissolve: t })`. Beside
|
|
854
|
+
three.js's standard fields (metalness, roughness, emissive, `normalMap` in
|
|
855
|
+
glTF's tangent frame, `alphaTest`, `transparent`, `side: "double"`,
|
|
856
|
+
`unlit`), three.js `MeshPhysicalMaterial`'s fields work on every tier:
|
|
857
|
+
`clearcoat` (with `clearcoatMap`, `clearcoatNormalMap`) for car paint and
|
|
858
|
+
varnish, `sheen` and `sheenColor` for cloth and velvet, `transmission`
|
|
859
|
+
with `thickness`, `ior` and `attenuationColor` for glass and water that
|
|
860
|
+
refract what is behind them, `iridescence` for soap film and oil,
|
|
861
|
+
`anisotropy` for brushed metal; a glTF's `KHR_materials_*` extensions
|
|
862
|
+
load into them. A non-uniform `scale` shades correctly. Set `sheenColor`
|
|
863
|
+
with `sheen` (black is no sheen); `thickness` is in the mesh's units and
|
|
864
|
+
`attenuationDistance` 0 absorbs nothing. A transmissive material refracts
|
|
865
|
+
only what was drawn before it (not other glass or water), and unlit and
|
|
866
|
+
toon materials ignore the physical fields:
|
|
867
|
+
|
|
868
|
+
```ts
|
|
869
|
+
const amber = og.render.standardMaterial(world, { color: { r: 1, g: 1, b: 1 }, roughness: 0.02, transmission: 1, thickness: 1, ior: 1.5, attenuationColor: { r: 0.95, g: 0.45, b: 0.08 }, attenuationDistance: 0.35 });
|
|
870
|
+
```
|
|
871
|
+
- **Texture repeat** — three.js's `texture.repeat` is the geometry's
|
|
872
|
+
`uvRepeat` here: `og.render.planeGeometry`, `boxGeometry` and
|
|
873
|
+
`sphereGeometry` scale their UVs from 0..1 to 0..`uvRepeat`, and material
|
|
874
|
+
textures sample with repeat wrapping, so the texture tiles. A ground grid
|
|
875
|
+
every 10 m on a 1,600 m plane (`references/metal/ground-grid.png` in
|
|
876
|
+
og-tests):
|
|
877
|
+
|
|
878
|
+
```ts
|
|
879
|
+
const ground = must(og.render.planeGeometry(world, { width: 1600, height: 1600, uvRepeat: { x: 160, y: 160 } }));
|
|
880
|
+
const grid = og.render.standardMaterial(world, { map: must(og.load.texture(world, "textures/grid.png")), roughness: 0.9 });
|
|
881
|
+
// turn it -pi/2 about x for a floor
|
|
882
|
+
```
|
|
883
|
+
- **HUD** — `og.render.sprite` / `og.render.text` with `screen: true` draw
|
|
884
|
+
in CSS pixels from the top-left over the finished frame, untouched by
|
|
885
|
+
post effects, with an optional `clip`. `og.ui` draws in the same
|
|
886
|
+
overlay at `UI_LAYER` (1000): screen sprites and text at a layer up to
|
|
887
|
+
1000 draw under every UI element, above 1000 over every one (modals and
|
|
888
|
+
the screen transition too), so a HUD icon made of sprites sits on a UI
|
|
889
|
+
panel at `layer: UI_LAYER + 1` while its screen shows.
|
|
890
|
+
|
|
891
|
+
`templates/fx-showcase` shows every one of these on its first five stations;
|
|
892
|
+
every field is in `reference/render.md`. Floating text,
|
|
893
|
+
`og.render.floatingText(world, { text: "-12", position, color })`, pops a
|
|
894
|
+
damage number or a pickup that rises and fades by itself (amounts: og-feel
|
|
895
|
+
§7).
|
|
896
|
+
|
|
897
|
+
### Particle emitters: the grammar
|
|
898
|
+
|
|
899
|
+
An emitter is an `.ogt` of `type particles` under `assets/` (`fx/` by
|
|
900
|
+
custom). Every line is optional; what is left out keeps its default (a
|
|
901
|
+
small fountain of soft white points). A `<min> <max>` pair is a random
|
|
902
|
+
range per particle, one number is both:
|
|
903
|
+
|
|
904
|
+
```text
|
|
905
|
+
type particles
|
|
906
|
+
version 1
|
|
907
|
+
---
|
|
908
|
+
maxParticles 256 # the ring: the newest overwrite the oldest (1 to 1048576)
|
|
909
|
+
shape cone 15 0.05 # point | sphere <radius> [surface] | cone <angle°> <radius> | box <x> <y> <z> (half extents)
|
|
910
|
+
spread 0 # degrees of random deviation from the shape's direction (180: every way)
|
|
911
|
+
rate 40 # particles per second; 0 for bursts only
|
|
912
|
+
burst 0 30 # <time s> <count> [<cycles> <interval s>]: at that time of each cycle (repeatable)
|
|
913
|
+
duration 1 # seconds per emission cycle
|
|
914
|
+
looping true # false: one cycle, then the particles live out
|
|
915
|
+
lifetime 1 1.5 # seconds
|
|
916
|
+
speed 1.5 2.5 # units per second, along the shape's direction
|
|
917
|
+
size 0.08 0.12 # the quad's side in world units
|
|
918
|
+
sizeOverLife 0 1 1 0 # <t> <value> pairs, t 0 (birth) to 1 (death), at most 8; multiplies size (default 1)
|
|
919
|
+
colorOverLife { # keys <t> <r> <g> <b> <a>, linear, at most 8 (default: white, fading over the last quarter)
|
|
920
|
+
key 0 1 1 1 1
|
|
921
|
+
key 1 1 1 1 0
|
|
922
|
+
}
|
|
923
|
+
color 1 1 1 1 # a tint, <r> <g> <b> [<a>]; above 1 blooms
|
|
924
|
+
rotation 0 0 # start rotation, degrees
|
|
925
|
+
angularVelocity 0 0 # degrees per second
|
|
926
|
+
gravity 0 -2 0 # acceleration, units per second²
|
|
927
|
+
drag 0 # velocity scales by 1 / (1 + drag × dt)
|
|
928
|
+
noise 0 0.5 0.3 # <strength> [<frequency> <scroll>]: a moving turbulence
|
|
929
|
+
texture "fx/spark.png" # an asset id; left out: a soft round dot
|
|
930
|
+
sheet 4 4 16 life 1 # <columns> <rows> [<frames> [off | life <cycles> | fps <n> | random]]
|
|
931
|
+
blend additive # additive (fire, sparks, magic) | alpha (smoke, dust)
|
|
932
|
+
space world # world: born at the emitter, then free | local: carried with it
|
|
933
|
+
align camera # camera | velocity <stretch>: stretched along its motion (sparks, rain)
|
|
934
|
+
softDistance 0 # fade where the quad nears the scene behind it (0: off)
|
|
935
|
+
collision 0.4 0.2 # <bounce> <friction>: bounce off the depth buffer (3D billboards, world space)
|
|
936
|
+
render billboard # billboard (3D) | sprite <layer>: 2D, drawn in the sprite batch on that layer
|
|
937
|
+
glow 0 # sprite mode: strength in the 2D glow
|
|
938
|
+
array "fx/smoke.ogt" random # sprite mode: draw from a texture array, [<layers>] [off | life <cycles> | fps <n> | random]
|
|
939
|
+
round false # sprite mode: fade each particle to a round edge over its texture or layer
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
- Directions: `point`, `cone` and `box` emit along the entity's +y
|
|
943
|
+
(`spread 180` sends a point every way), `sphere` outwards. In a 2D game
|
|
944
|
+
set `render sprite <layer>`: positions stay in the xy plane, sizes and
|
|
945
|
+
speeds are in world units like a sprite's, and the particles take the
|
|
946
|
+
sprite batch's order and glow like any sprite.
|
|
947
|
+
- A one-shot effect (a hit, a pickup, landing dust) is `rate 0`,
|
|
948
|
+
`looping false`, one `burst 0 <count>` and a short `duration`; restart
|
|
949
|
+
it by writing the tick to its `og.ParticleSystem.startTick` (moving the
|
|
950
|
+
entity first). A lasting one (embers, motes, rain) keeps the defaults
|
|
951
|
+
`looping true` and a `rate`.
|
|
952
|
+
- `sheet` animates a sprite sheet texture: `life 1` plays its frames once
|
|
953
|
+
over each particle's life, `fps 12` at a rate, `random` one frame per
|
|
954
|
+
particle. `array` does the same over a texture array's layers (§8), one
|
|
955
|
+
draw for the whole emitter; a billboard emitter with an array is refused.
|
|
956
|
+
- `collision` bounces 3D billboards off what the depth buffer shows on the
|
|
957
|
+
full tier (sparks on a floor); sprite-mode emitters skip it. It is a
|
|
958
|
+
look, never gameplay: particles never touch the simulation.
|
|
959
|
+
- The cook refuses an unknown line, a missing texture or array and a
|
|
960
|
+
value of the wrong shape, naming the line.
|
|
961
|
+
|
|
962
|
+
A hit's sparks in a 2D game (Hollowbell's `fx/hit.ogt`):
|
|
963
|
+
|
|
964
|
+
```text
|
|
965
|
+
type particles
|
|
966
|
+
version 1
|
|
967
|
+
---
|
|
968
|
+
maxParticles 24
|
|
969
|
+
shape point
|
|
970
|
+
spread 180
|
|
971
|
+
rate 0
|
|
972
|
+
burst 0 10
|
|
973
|
+
duration 0.1
|
|
974
|
+
looping false
|
|
975
|
+
lifetime 0.18 0.32
|
|
976
|
+
speed 3 6
|
|
977
|
+
size 0.12 0.22
|
|
978
|
+
sizeOverLife 0 1 1 0
|
|
979
|
+
colorOverLife {
|
|
980
|
+
key 0 1 0.95 0.75 1
|
|
981
|
+
key 1 0.95 0.55 0.2 0
|
|
982
|
+
}
|
|
983
|
+
drag 4
|
|
984
|
+
texture "fx/spark.png"
|
|
985
|
+
blend additive
|
|
986
|
+
render sprite 12
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
`templates/archetypes/idle/assets/fx/` (taps, stars, a milestone),
|
|
990
|
+
`templates/fx-showcase/assets/fx/` (fire, smoke, sparks with collision,
|
|
991
|
+
2D embers) and `templates/archetypes/third-person/assets/fx/` hold more.
|
|
992
|
+
|
|
993
|
+
## 11. Atmosphere, water and terrain
|
|
994
|
+
|
|
995
|
+
`og.atmosphere` is data too, and off until set (`reference/atmosphere.md`):
|
|
996
|
+
|
|
997
|
+
- **Fog** — the `og.Fog` resource: `enabled`, distance fog (`density` per
|
|
998
|
+
metre from `start`, or a linear ramp to `end`), height fog
|
|
999
|
+
(`heightDensity` below `height`, thinning by `falloff`), `sunColor` for
|
|
1000
|
+
the glow looking into the sun, `sky` for how much of the sky it covers.
|
|
1001
|
+
- **Sky** — `og.Sky { enabled, zenith, horizon, ground, sunDirection,
|
|
1002
|
+
sunColor, sunSize, glow, stars }` draws the sky per pixel and lights the
|
|
1003
|
+
scene by it; `moonSize` (degrees, 0 none), `moonPhase` (0 new, 0.25 first
|
|
1004
|
+
quarter, 0.5 full, 0.75 last quarter), `moonIntensity`, `moonColor` and
|
|
1005
|
+
`moonDirection` (zero: opposite the sun) add a moon disc over the stars.
|
|
1006
|
+
- **Day and night** — `og.DayNight { enabled, start, speed, tilt, azimuth,
|
|
1007
|
+
moonSize, moonPhase }`: the clock is `start + seconds × speed` hours from
|
|
1008
|
+
the tick, so it replays; it drives the first `og.DirectionalLight` (the
|
|
1009
|
+
sun by day, a dim moon by night, where the moon disc hangs), the sky, the
|
|
1010
|
+
ambient light and the fog's tint. Read the clock with
|
|
1011
|
+
`og.atmosphere.timeOfDay(world)` (declare `og.DayNight` among the system's
|
|
1012
|
+
reads) to light lamps at dusk.
|
|
1013
|
+
- **Weather** — `og.Weather { kind: WEATHER_RAIN | WEATHER_SNOW |
|
|
1014
|
+
WEATHER_DUST | WEATHER_MIST, intensity, wind, radius }`: particles around
|
|
1015
|
+
the camera, the preset's fog (an enabled `og.Fog` wins) and dimmed light.
|
|
1016
|
+
With an orthographic camera the particles are 2D sprites on `layer`.
|
|
1017
|
+
- **Water** — `og.Water` on an entity draws a surface on its xz plane
|
|
1018
|
+
(`size`, `shallow`/`deep` colours over `depthFade`, `foamWidth` where it
|
|
1019
|
+
meets geometry, `waveStrength`, `reflectivity`); with an
|
|
1020
|
+
`og.WaterSurface` on the same entity its waves move the surface and bodies
|
|
1021
|
+
splash it (`og.Splash` events) and float. In 2D, `og.WaterLine` with
|
|
1022
|
+
`og.Water2D` is a side-on pool drawn in the sprite order. Three more
|
|
1023
|
+
cost frame time and are off until set: `planarReflection` (0.25 to 1, the
|
|
1024
|
+
share of the screen's resolution) mirrors the scene in a still lake or a
|
|
1025
|
+
pool you look across, where the sky reflection alone looks fake;
|
|
1026
|
+
`shadows` (0 to 1) darkens it under the cascaded shadow (a pier, a cliff);
|
|
1027
|
+
`caustics` throws rippling light on what lies below it. With the camera
|
|
1028
|
+
below the surface the frame's fog becomes `underwaterColor` at
|
|
1029
|
+
`underwaterDensity` and the surface from below shows the sky only
|
|
1030
|
+
straight up (on by default; set the density to 0 to see through).
|
|
1031
|
+
|
|
1032
|
+
**Terrain** (`reference/terrain.md`) is a height field cooked from a
|
|
1033
|
+
`type terrain` .ogt beside a 16-bit grey PNG (or an EXR in metres):
|
|
1034
|
+
|
|
1035
|
+
```text
|
|
1036
|
+
type terrain
|
|
1037
|
+
version 1
|
|
1038
|
+
---
|
|
1039
|
+
heightmap "island.png" # 16-bit grey: black and white are `height`'s ends
|
|
1040
|
+
height -20 120 # metres
|
|
1041
|
+
size 2048 2048 # metres along x and z, centred on the entity
|
|
1042
|
+
control "island-control.png" # optional RGBA: layer 0 in R, 1 in G, 2 in B, 3 in A
|
|
1043
|
+
heightBlend 0.15 # sharpen the blends by each albedo's alpha (its height)
|
|
1044
|
+
layer { albedo "textures/grass.png" normal "textures/grass-n.png" tile 6 slope 0 30 }
|
|
1045
|
+
layer { albedo "textures/rock.png" tile 12 triplanar true slope 28 90 }
|
|
1046
|
+
layer { albedo "textures/snow.png" altitude 90 1000 altitudeFade 10 }
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
A layer shows where its control channel (1 without a map) times its slope
|
|
1050
|
+
band (degrees) times its altitude band (metres above the terrain's entity)
|
|
1051
|
+
is high; give cliffs a `triplanar` layer so the texture does not stretch.
|
|
1052
|
+
In the game, load it in `start` and place it:
|
|
1053
|
+
|
|
1054
|
+
```ts
|
|
1055
|
+
const info = must(og.terrain.load(world, "terrain/island.ogt"));
|
|
1056
|
+
const ground = must(world.spawn({ name: "island", position: { x: 0, y: 0, z: 0 } }));
|
|
1057
|
+
world.insert(ground, og.Terrain, { data: info.data });
|
|
1058
|
+
world.insert(ground, og.Collider, { shape: SHAPE_HEIGHTFIELD, mesh: info.geometry });
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
It is drawn around the camera at finer detail nearby (casting and
|
|
1062
|
+
receiving shadows, under the fog), placed by its entity's world transform
|
|
1063
|
+
(turn or scale the entity and the terrain turns and scales with it), and
|
|
1064
|
+
`og.CharacterController` walks on it up to `maxSlope`.
|
|
1065
|
+
`og.terrain.sample(world, { data, origin, quaternion, scale, x, z })`
|
|
1066
|
+
(the entity's position, rotation and scale; the last two may be left out
|
|
1067
|
+
for an unturned, unscaled terrain) gives the height, normal and slope
|
|
1068
|
+
anywhere (to place trees, spawn on the ground, or check a slope); `og.terrain.edit` raises, lowers, flattens,
|
|
1069
|
+
smooths or paints under a round brush (craters, building pads, a paint
|
|
1070
|
+
tool) and `og.terrain.layer` / `setLayer` retune a layer live. Edits are
|
|
1071
|
+
world state: a snapshot keeps them and restoring it brings the ground
|
|
1072
|
+
back as it was then. Grass, flowers and stones are an `og.TerrainScatter` on the
|
|
1073
|
+
terrain's entity: `{ data, geometry, material, layer, density, distance }`
|
|
1074
|
+
grows copies of the mesh where that layer shows, within `distance` of the
|
|
1075
|
+
camera (thinning out over its outer `fade`, never shrinking), each fixed to its spot
|
|
1076
|
+
as the camera moves. Keep `density × distance²` modest: 4 a square metre
|
|
1077
|
+
within 60 m is about 36,000 copies. A navmesh .ogt's `terrain "terrain/island.ogt" { step 4 }` bakes
|
|
1078
|
+
the walkable slopes for og-ai. Lower `lodRing` on og.Terrain to trade
|
|
1079
|
+
detail for frame time.
|
|
1080
|
+
Its tiles stream: only the tiles the camera needs are on the GPU (16 MiB a
|
|
1081
|
+
terrain by default), decoded off the frame, the coarser level standing in
|
|
1082
|
+
until each arrives, and nothing a tick reads changes with it. In a probe
|
|
1083
|
+
script `terrain_stream:resident` decodes every wanted tile each frame (for
|
|
1084
|
+
captures to compare), `terrain_stream:stream:<MiB>` streams within that
|
|
1085
|
+
budget, and `dump_stream` gives the terrain's tiles.
|
|
1086
|
+
|
|
1087
|
+
Which look for which mood is og-taste §8.
|
|
1088
|
+
|
|
1089
|
+
## 12. Material shaders
|
|
1090
|
+
|
|
1091
|
+
A material can run your own WGSL in four hooks of the lit shader. Write the
|
|
1092
|
+
hooks in a `.wgsl` file and declare it in an `.ogt` of `type shader`:
|
|
1093
|
+
|
|
1094
|
+
```text
|
|
1095
|
+
type shader
|
|
1096
|
+
version 1
|
|
1097
|
+
---
|
|
1098
|
+
source "shaders/dissolve.wgsl"
|
|
1099
|
+
param amount float 0
|
|
1100
|
+
param edgeColor color 4 1.6 0.4
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
```wgsl
|
|
1104
|
+
// Any of the four; the others stay the identity.
|
|
1105
|
+
fn vertex_offset(h: Hook, p: Params) -> vec3<f32> // world-space offset
|
|
1106
|
+
fn surface_color(h: Hook, p: Params, c: vec4<f32>) -> vec4<f32> // base colour and alpha
|
|
1107
|
+
fn surface_normal(h: Hook, p: Params, n: vec3<f32>) -> vec3<f32> // world normal, unit length
|
|
1108
|
+
fn surface_emissive(h: Hook, p: Params, e: vec3<f32>) -> vec3<f32> // emitted light
|
|
1109
|
+
// h: world, local (object-space) position, normal, uv, color, time (s).
|
|
1110
|
+
// p: the declared parameters by name (p.amount). value_noise3(p, seed) is
|
|
1111
|
+
// the engine's 3D value noise (0..1).
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
The cook compiles every shader for every back end (WebGPU, Metal, Vulkan,
|
|
1115
|
+
and GLSL ES for WebGL2) and refuses one that does not compile with naga's
|
|
1116
|
+
message, its lines counted in your hooks. In the game:
|
|
1117
|
+
`og.render.shaderMaterial(world, must(og.load.shader(world,
|
|
1118
|
+
"shaders/dissolve.ogt")), { color, alphaTest: 0.5 })` makes the material,
|
|
1119
|
+
`og.render.setShaderParam(world, material, "amount", [0.4])` sets a
|
|
1120
|
+
parameter, and `og.render.setMaterialShader(world, material, shader)` gives
|
|
1121
|
+
an existing material (a loaded model's, from `GltfInfo.materials`) the
|
|
1122
|
+
hooks. Alpha below `alphaTest` discards the fragment (a dissolve). Skinned
|
|
1123
|
+
meshes run the hooks too, and shadows and the depth prepass see the mesh
|
|
1124
|
+
where `vertex_offset` moved it. `templates/fx-showcase/assets/shaders/` has a
|
|
1125
|
+
dissolve, wind-swayed grass and a ripple on the walking walker.
|
|
1126
|
+
|
|
1127
|
+
## 13. Physics
|
|
1128
|
+
|
|
1129
|
+
`og.RigidBody` and `og.Collider` on an entity make it a body in the
|
|
1130
|
+
profile's simulation (rapier3d in 3D, rapier2d in 2D); contacts and sensors
|
|
1131
|
+
arrive as events, and `og.physics` answers queries. `og.Velocity` is
|
|
1132
|
+
optional: the step adds it to a dynamic body that has none, and a kinematic
|
|
1133
|
+
body (`BODY_KINEMATIC`) moves by it each tick, its Transform written back:
|
|
1134
|
+
a lift is a kinematic body with a Velocity whose system only turns the
|
|
1135
|
+
velocity round at the ends, and a PlatformerController standing on it
|
|
1136
|
+
rides along. A kinematic body without one stays where its Transform puts
|
|
1137
|
+
it (move that by hand), and writing a moving one's Transform teleports it
|
|
1138
|
+
there. `world.set(e, og.Velocity, ...)` refuses on
|
|
1139
|
+
an entity without one (a body in `start`, before its first step):
|
|
1140
|
+
`world.insert` it there, which adds and sets it.
|
|
1141
|
+
|
|
1142
|
+
```ts
|
|
1143
|
+
const hit = og.physics.raycast(world, { origin: eye, direction: { x: 0, y: 0, z: -1 }, maxDistance: 30, filter: { mask: 0b10, exclude: player } });
|
|
1144
|
+
if (hit.ok && hit.value) { /* hit.value.entity, distance, point, normal */ }
|
|
1145
|
+
const under = og.physics.shapeCast(world, { shape: { shape: SHAPE_SPHERE, radius: 0.4 }, position: feet, direction: { x: 0, y: -1, z: 0 }, maxDistance: 2 });
|
|
1146
|
+
const near = og.physics.overlap(world, { shape: { shape: SHAPE_BOX, size: { x: 4, y: 2, z: 4 } }, position: door }); // entities, by id
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
Push a dynamic body with `og.physics.applyForce(world, e, force, point?)`
|
|
1150
|
+
(newtons over the next step: call it every tick for thrust, wind or a
|
|
1151
|
+
car's downforce), `applyImpulse(world, e, impulse, point?)` (newton-seconds
|
|
1152
|
+
at once: a kick, a hit, a jump; its velocity changes by impulse / mass),
|
|
1153
|
+
`applyTorque` and `applyTorqueImpulse` (about each world axis; 2D uses z).
|
|
1154
|
+
A point off the centre of mass spins it too. They apply at the og.physics
|
|
1155
|
+
step after the systems, in the order given; a hit stop drops forces and
|
|
1156
|
+
keeps impulses for after it; those not yet applied are in snapshots.
|
|
1157
|
+
`og.RigidBody.centerOfMass` moves the centre of mass (a roly-poly toy, a
|
|
1158
|
+
car that keeps its wheels down).
|
|
1159
|
+
|
|
1160
|
+
Inside a system the queries see the colliders as they were at the start of
|
|
1161
|
+
the tick (`og.physics.queries` records them before the game's systems), so
|
|
1162
|
+
the answer does not depend on system order; `QueryFilter.includeSensors`
|
|
1163
|
+
adds sensors. The step itself runs on the lanes (§14) with the same result
|
|
1164
|
+
as one thread. Beyond rigid bodies, all are components over the same simulation,
|
|
1165
|
+
deterministic and part of snapshots (`reference/physics.md`):
|
|
1166
|
+
|
|
1167
|
+
- `og.Joint` — fixed, hinge (`JOINT_REVOLUTE`, limits, a velocity or
|
|
1168
|
+
position motor), slider, spring, rope, ball; to another body (`other`) or
|
|
1169
|
+
a world point (`otherAnchor`); `value` reads the angle or slide.
|
|
1170
|
+
- `og.CharacterController` (3D, or top-down with `gravityScale` 0) — set
|
|
1171
|
+
`move` every tick and `jump` to jump; it climbs `maxSlope` and
|
|
1172
|
+
`stepHeight`, snaps to the ground, rides moving platforms, slides along
|
|
1173
|
+
walls, pushes bodies; read `grounded`, `velocity`, `ground`. Its
|
|
1174
|
+
og.Collider's `layers` and `mask` decide what blocks it, as for bodies
|
|
1175
|
+
(a ghost through a gate: leave the gate's layer out of its `mask`); the
|
|
1176
|
+
same for the PlatformerController.
|
|
1177
|
+
- `og.PlatformerController` (2D) — `moveX`, `jump`, `down` in; jump height
|
|
1178
|
+
and time to apex, coyote time, jump buffer, wall jumps, one-way layers,
|
|
1179
|
+
corner correction as tunables (og-feel §12 has values by genre);
|
|
1180
|
+
`impulse` (m/s, taken once on the next step) knocks it back or launches
|
|
1181
|
+
it;
|
|
1182
|
+
`og.CharacterEvent` reports jumps and landings for dust and sounds.
|
|
1183
|
+
`templates/archetypes/runner`'s courier is one.
|
|
1184
|
+
- `og.Vehicle` — a raycast car (3D, `throttle`, `steer`, `brake`; wheels
|
|
1185
|
+
from `og.physics.vehicleWheels`) or a car on wheel joints (2D). Its
|
|
1186
|
+
defaults drive a 1,000 kg road car: set the chassis's
|
|
1187
|
+
`og.RigidBody.mass` (og-feel §12 has the numbers for a racer, a truck
|
|
1188
|
+
and a kart). Its wheel rays meet only what its chassis's og.Collider
|
|
1189
|
+
`layers` and `mask` meet, so a ghost car on its own layer drives through
|
|
1190
|
+
the player's. `rollInfluence` 0 (the default) never tips it over in a
|
|
1191
|
+
bend; `og.RigidBody.centerOfMass` lowers its weight. It writes `speed`,
|
|
1192
|
+
`contacts` and `slip` (the most any tyre slides, 0 to 1); each wheel's
|
|
1193
|
+
`slip` and `sideSpeed` come with its pose, for smoke, skid marks and a
|
|
1194
|
+
squeal.
|
|
1195
|
+
- `og.Rope` and `og.Cloth` — Verlet, drawn by the engine when `visible`.
|
|
1196
|
+
A cloth that folds or piles up (a dropped sheet, a cape on the ground)
|
|
1197
|
+
wants `thickness` about its `spacing` so it never passes through
|
|
1198
|
+
itself, `substeps` 2–4 and `friction` near 1 so it lies where it lands
|
|
1199
|
+
(self-collision costs about 1 ms a tick per 1,000 points).
|
|
1200
|
+
- `og.Fluid2D` — a particle fluid drawn as metaballs on a sprite layer.
|
|
1201
|
+
Give two or more fluids the same `group` and they interact: by
|
|
1202
|
+
`density` the heavier sinks under the lighter (oil on water), by
|
|
1203
|
+
`mixing` 0 they stay in layers and by 1 they blend, colour and weight
|
|
1204
|
+
evening out where they touch; a group draws as one surface. Up to
|
|
1205
|
+
sixteen fluids draw a frame (four to a group).
|
|
1206
|
+
- `og.Fluid3D` — a 3D particle fluid (3D profile) drawn as one
|
|
1207
|
+
screen-space surface that refracts and tints what lies behind it,
|
|
1208
|
+
reflects the sky and takes the scene's lights. Give it `bounds` (a
|
|
1209
|
+
container around the entity) or colliders and terrain to hold it; an
|
|
1210
|
+
emitter (`emitRate`, `emitOffset`, `emitVelocity`, up to `maxCount`) pours
|
|
1211
|
+
and a sink box (`sinkSize`, `sinkOffset`) drains. Groups work as in 2D:
|
|
1212
|
+
`density` 3 under 1 gives honey under water, `mixing` 1 blends their
|
|
1213
|
+
colours. Look: `color`, `opacity` per metre, `reflectivity`,
|
|
1214
|
+
`refraction`, `smoothing` (0 shows the beads); fluids drawn in one frame
|
|
1215
|
+
share one surface and the first fluid's look. Cost is the particles:
|
|
1216
|
+
about 0.9 ms of CPU a tick for 2,000 at one substep and 1.8 ms at the
|
|
1217
|
+
default two (native and wasm alike), 4 ms for 4,000 and 10 ms for 8,000
|
|
1218
|
+
at two, roughly double that when they pile deep, so budget a scene to a
|
|
1219
|
+
few thousand; the surface costs about 1.1 ms on the GPU whatever the count,
|
|
1220
|
+
growing with the pixels it covers. `og.physics.fluid3dParticles` reads
|
|
1221
|
+
the positions (the fx-showcase `fluids3d` station).
|
|
1222
|
+
- `og.Ragdoll` — give the entity a rig
|
|
1223
|
+
(`og.physics.setRagdollRig(world, e, must(og.physics.ragdollRigFromSkeleton(world, anim.skeleton)))`),
|
|
1224
|
+
then set `weight` 1 to go limp. At weight 0 its bodies follow the
|
|
1225
|
+
animation's pose every tick, so the fall starts from the pose and its
|
|
1226
|
+
velocity; the model is drawn blended from the animation to the bones over
|
|
1227
|
+
`blendTime`. In a 2D world (`og.Physics.profile` 2) the same works for a
|
|
1228
|
+
side-on skinned model whose bones lie in the xy plane: the bones are
|
|
1229
|
+
rapier2d bodies on hinges about z (the fx-showcase `ragdoll2d` station).
|
|
1230
|
+
|
|
1231
|
+
Readouts (`ropePoints`, `clothPoints`, `fluidParticles`,
|
|
1232
|
+
`fluid3dParticles`, `waterHeight`, `vehicleWheels`, `ragdollPose`) show
|
|
1233
|
+
the last step. `templates/fx-showcase` has a station for each.
|
|
1234
|
+
|
|
1235
|
+
## 14. Performance
|
|
1236
|
+
|
|
1237
|
+
A frame is 16.7 ms at 60 Hz; keep step + render under half of it on a
|
|
1238
|
+
desktop so phones have room. `og run --json` prints `advanceMs`, the
|
|
1239
|
+
run's `frames` (step and render median, p95 and max) and its `systems`
|
|
1240
|
+
(each system's mean milliseconds a tick, slowest first: the game's and the
|
|
1241
|
+
engine's own, physics, the UI's end of frame, animation and the rest, which the
|
|
1242
|
+
probe's `dump_world` also reports as `ms`), `og
|
|
1243
|
+
polish` the p95 step and render time of every playtest (og-build §5), and
|
|
1244
|
+
`og run --device` the first frame and frame times in a real browser or
|
|
1245
|
+
phone (og-build §6). The usual costs:
|
|
1246
|
+
draws (share geometry and one material per look), particle systems (cap
|
|
1247
|
+
`maxParticles` in the emitter's .ogt, despawn emitters that are done),
|
|
1248
|
+
per-tick TypeScript over many entities (iterate archetype columns, never
|
|
1249
|
+
`world.get` per entity; spread expensive work over ticks, as og.ai's
|
|
1250
|
+
`thinkEvery` does), and loads in the middle of play (load in `start`).
|
|
1251
|
+
Point and spot lights are clustered (Forward+): a pixel shades only the
|
|
1252
|
+
lights whose `distance` reaches it, so a few hundred lamps, torches or
|
|
1253
|
+
neon signs cost little; keep each `distance` as short as the look allows.
|
|
1254
|
+
Up to `MAX_LIGHTS` (256) in view are drawn, the brightest kept past that,
|
|
1255
|
+
and up to `RenderCapabilities.maxShadowedLights` with `castShadow` (8 on
|
|
1256
|
+
the full tier, 4 on WebGL2) cast shadows (shadows are the expensive part;
|
|
1257
|
+
§16).
|
|
1258
|
+
Engine work spreads over the cores on the lanes: physics from about 500
|
|
1259
|
+
bodies, skinning, extraction and culling, with results identical to one
|
|
1260
|
+
thread, so world hashes and replays do not change. Game systems stay on
|
|
1261
|
+
the engine thread. On the web the lanes need cross-origin isolation
|
|
1262
|
+
(`publish { isolation preferred }`, the default, in `project.ogt`; og-build
|
|
1263
|
+
§7); without it the same game runs on one thread.
|
|
1264
|
+
Simulated matter is paid for per point on the CPU every tick: a 3D fluid's
|
|
1265
|
+
particles (about 1 ms per 1,000 at two substeps) and a self-colliding
|
|
1266
|
+
cloth's points (about 1 ms per 1,000) are the ones to count (§13).
|
|
1267
|
+
|
|
1268
|
+
## 15. Looks: the frame's art direction as data
|
|
1269
|
+
|
|
1270
|
+
A **look** is an `.ogt` of `type look` under `assets/looks/` that sets the
|
|
1271
|
+
whole treatment of the frame at once: the resources `RenderSettings`,
|
|
1272
|
+
`PostEffects`, `Stylise`, `MaterialStyle`, `Fog`, `Sky` and `Environment`
|
|
1273
|
+
(one block each, fields by name), the UI `skin` and the `antialias`.
|
|
1274
|
+
Choosing one for a premise is og-taste §1 and §12; this is the mechanism.
|
|
1275
|
+
|
|
1276
|
+
```text
|
|
1277
|
+
type look
|
|
1278
|
+
version 1
|
|
1279
|
+
---
|
|
1280
|
+
name "Harbour"
|
|
1281
|
+
extends "anime-cel-3d" # a built-in preset (templates/looks/<slug>.ogt) or another look's id
|
|
1282
|
+
skin "skins/harbour/skin.ogt"
|
|
1283
|
+
Stylise {
|
|
1284
|
+
ink true
|
|
1285
|
+
inkWidth 1.5
|
|
1286
|
+
}
|
|
1287
|
+
MaterialStyle {
|
|
1288
|
+
shadowTint "#3b3566" # sRGB written, stored linear
|
|
1289
|
+
shadowTintAmount 0.6
|
|
1290
|
+
}
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1293
|
+
```ts
|
|
1294
|
+
export default og.game({ look: "looks/harbour.ogt", start, systems }); // GameDesc.look: applied after the first scene, before start
|
|
1295
|
+
|
|
1296
|
+
const flashback = must(og.load.look(world, "looks/flashback.ogt")); // at any tick
|
|
1297
|
+
must(og.render.applyLook(world, flashback));
|
|
1298
|
+
og.render.look(world)?.name; // the look last applied (LookInfo)
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
- Applying a look replaces each resource it names **whole** (the fields it
|
|
1302
|
+
sets, the rest their defaults; `PostEffects.flash` and `shakeTrauma`
|
|
1303
|
+
keep theirs) and leaves the others alone, then configures the UI with
|
|
1304
|
+
its skin. Write per-game tweaks in `start` after it, or in the look. A
|
|
1305
|
+
system that applies one declares those resources among its writes.
|
|
1306
|
+
- `extends` flattens at cook time, field by field (a field written wins);
|
|
1307
|
+
`assets/looks/<slug>.ogt` of the game's own replaces the preset of that
|
|
1308
|
+
slug for every look that extends it. The cook refuses an unknown field,
|
|
1309
|
+
a value out of range and an asset not in the bundle, naming the line.
|
|
1310
|
+
- `og.load.lookInfo` (and `og.render.look`) answer `LookInfo`: `name`,
|
|
1311
|
+
`extends`, `resources`, `skin`, `antialias`. A look's `antialias` (tier,
|
|
1312
|
+
off, msaa4 or taa) is the renderer's when `GameDesc.render` names none.
|
|
1313
|
+
- **`og.Stylise`** moves the frame away from the photographic, every
|
|
1314
|
+
primitive off at zero: the pixel grid (`pixelHeight`, `pixelUpscale`,
|
|
1315
|
+
`pixelSnap`), fewer tones or a fixed palette through a dither
|
|
1316
|
+
(`posterize`, `palette` from `og.load.palette` of a `.hex` or `.gpl`,
|
|
1317
|
+
`dither`, `ditherStrength`), `halftone`, `hatching`, painterly
|
|
1318
|
+
smoothing (`paintRadius`, `paintEdgeDarkening`, `paintBleed`,
|
|
1319
|
+
`paintCanvas`), `ink` lines (with `inkDepth` for silhouettes and
|
|
1320
|
+
creases), `depthBands`, and early-3D `vertexSnap`, `affineTexturing`
|
|
1321
|
+
and `depthBits`. The UI and screen sprites stay unstylised.
|
|
1322
|
+
- **`og.MaterialStyle`** sets every lit material that leaves the field at
|
|
1323
|
+
its default: `toon` (with a `gradientMap`), `shadowTint` and
|
|
1324
|
+
`shadowTintAmount`, `rimIntensity`, `outlineWidth` (with
|
|
1325
|
+
`outlineDistance` to thin lines with depth) and `flatShading`. Unlit and
|
|
1326
|
+
shader materials, sprites and the UI are untouched, and so is a material
|
|
1327
|
+
made with `styled: false` (exactly as declared under any look). The
|
|
1328
|
+
style's `toon` passes by a material with a physical lobe (`clearcoat`,
|
|
1329
|
+
`sheen`, `transmission`, `iridescence`, `anisotropy`, an `ior` other
|
|
1330
|
+
than 1.5), which keeps its physical shading and takes the rest; give it
|
|
1331
|
+
`toon: true` itself to make it toon anyway. An outline is a hull
|
|
1332
|
+
outline: each object's silhouette and its creases against other objects
|
|
1333
|
+
(the ground under it), never the edges between its own faces, so `.vox`
|
|
1334
|
+
models and flat-shaded meshes keep clean faces; for lines on every
|
|
1335
|
+
crease, `Stylise.inkDepth`.
|
|
1336
|
+
- Built-in assets a look may name without owning a copy:
|
|
1337
|
+
`"@canvas/paper"`, `"@canvas/linen"`, `"@canvas/watercolor"` for
|
|
1338
|
+
`Stylise.paintCanvas`, and `"@palettes/lcd2"`, `"@palettes/handheld4"`,
|
|
1339
|
+
`"@palettes/dusk16"`, `"@palettes/bright32"`, `"@palettes/1bit"` for
|
|
1340
|
+
`Stylise.palette`.
|
|
1341
|
+
- `RenderCapabilities.stylise` (`og.render.capabilities(world)`) names the
|
|
1342
|
+
primitive groups this device draws; a look using another still applies,
|
|
1343
|
+
without that primitive. A look in a streaming group applies once the
|
|
1344
|
+
group is resident (§6).
|
|
1345
|
+
|
|
1346
|
+
## 16. Light, exposure, shadows and antialiasing
|
|
1347
|
+
|
|
1348
|
+
- **Tone mapping and exposure** (`og.RenderSettings`): `toneMapping` is
|
|
1349
|
+
`aces` (the default), `agx` (bright saturated lights go to white instead
|
|
1350
|
+
of shifting hue: neon, fire, magic) or `none`; `toneMapped false` clamps
|
|
1351
|
+
so 2D colours come out as authored. `exposure` multiplies every camera's
|
|
1352
|
+
`Camera.exposure`. `autoExposure` meters each frame to middle grey;
|
|
1353
|
+
`adaptSpeedUp` and `adaptSpeedDown` (EV per second of game time, so a
|
|
1354
|
+
replay adapts the same) make it adapt like an eye: 1 up and 3 down,
|
|
1355
|
+
walking out of a cave into sun.
|
|
1356
|
+
- **Environment maps** light PBR materials from a photographed sky: an
|
|
1357
|
+
`.ogt` of `type environment` beside an equirectangular `.hdr` or `.exr`
|
|
1358
|
+
(`source "studio.hdr"`, `size 128`, `intensity`, `rotation` in degrees,
|
|
1359
|
+
`samples`), prefiltered at cook time. Set it on `og.Environment`:
|
|
1360
|
+
|
|
1361
|
+
```ts
|
|
1362
|
+
const env = must(world.resource(og.Environment));
|
|
1363
|
+
env.map = must(og.load.environment(world, "env/studio.ogt"));
|
|
1364
|
+
env.rotation = 40; // degrees about +Y; light and background turn together
|
|
1365
|
+
env.background = true; // draw it behind the scene (Camera.background and og.Sky give way)
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
`intensity` replaces `RenderSettings.environmentIntensity` while a map is
|
|
1369
|
+
set; `backgroundIntensity` dims the background alone;
|
|
1370
|
+
`StandardMaterialDesc.envMapIntensity` scales it per material. A look's
|
|
1371
|
+
`Environment` block sets it too.
|
|
1372
|
+
- **Shadows**: the sun (`og.DirectionalLight`) casts three cascades;
|
|
1373
|
+
`shadowSoftness` (0.01–0.05) widens the penumbra with the distance from
|
|
1374
|
+
caster to receiver, as a real sun's does, and `cascadeBlend` (0.1–0.2)
|
|
1375
|
+
hides the seams between cascades. Point and spot lights with
|
|
1376
|
+
`castShadow` cast cube shadows on the full tier and paraboloid ones on
|
|
1377
|
+
WebGL2.
|
|
1378
|
+
- **Antialiasing and renderer options** are `GameDesc.render`, read once
|
|
1379
|
+
when the renderer attaches:
|
|
1380
|
+
|
|
1381
|
+
```ts
|
|
1382
|
+
og.game({ render: { antialias: "taa", taaHistory: "catmullRom", taaSharpen: 0.3, pointShadows: "auto", shadowMapSize: 2048 }, ... });
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
`antialias`: `tier` (TAA on WebGPU, Metal and Vulkan, 4x MSAA on WebGL2),
|
|
1386
|
+
`taa` (any tier, converges over about 8 frames: fast motion, thin
|
|
1387
|
+
geometry), `msaa4`, `fxaa` (one cheap pass after tone mapping; pixel and
|
|
1388
|
+
low-resolution looks), `off` (pixel art). `taaHistory: "catmullRom"`
|
|
1389
|
+
stays sharper under camera motion and `taaSharpen` (0–1) restores
|
|
1390
|
+
detail TAA softens. `RenderCapabilities.antialias` says what the device
|
|
1391
|
+
resolved it to.
|
|
1392
|
+
|
|
1393
|
+
## 17. Saving progress
|
|
1394
|
+
|
|
1395
|
+
A save is either the whole world or a record the game writes; both go in
|
|
1396
|
+
the platform's store (og-platform-sdk §2), which takes any JSON value up
|
|
1397
|
+
to 64 KiB a key: a record in one key, the world in a slot
|
|
1398
|
+
(`og.platform.saveSlot`, any size up to 512 KiB).
|
|
1399
|
+
|
|
1400
|
+
- **The world** (`world.snapshot()`, `world.restore(save)`): every entity,
|
|
1401
|
+
component, name, resource (og.Time and og.Rng too), pending event and
|
|
1402
|
+
module state (physics, animation, tweens, tile edits), as text
|
|
1403
|
+
(`og-save:1:...`), in every build including publish and alike on both
|
|
1404
|
+
hosts. Restoring brings that world back to the hash it had; the session
|
|
1405
|
+
stays (input, the player's volumes, the platform's calls, the language,
|
|
1406
|
+
the command channel's scheduled steps). Use it where the moment itself
|
|
1407
|
+
is the save: a puzzle mid-move, a sandbox, a run that resumes exactly.
|
|
1408
|
+
|
|
1409
|
+
```ts
|
|
1410
|
+
// Save (a lantern lit, Save and quit): the world, and what the title shows of the slot.
|
|
1411
|
+
const s = world.snapshot();
|
|
1412
|
+
const asked = s.ok ? og.platform.saveSlot(world, { slot: "slot1", text: s.value, meta: { name, chimes } }) : s;
|
|
1413
|
+
if (asked.ok) run.saveRequest = asked.value; // one request for the whole slot
|
|
1414
|
+
// The title, on its first tick: the meta alone, no parts read.
|
|
1415
|
+
run.slotRequest = must(og.platform.loadSlot(world, { slot: "slot1", metaOnly: true }));
|
|
1416
|
+
// Continue: the text, once loadSlot's answer landed.
|
|
1417
|
+
run.loadRequest = must(og.platform.loadSlot(world, { slot: "slot1" }));
|
|
1418
|
+
const back = world.restore(answer.data.text); // in a system: applies when the tick ends
|
|
1419
|
+
if (!back.ok) toast(back.reason); // a save from a build whose types changed names them
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
How big a save is, and so where it goes: a world of a few entities
|
|
1423
|
+
snapshots to about 11 KiB of text (most of it the table of every
|
|
1424
|
+
registered type), Hollowbell (a 2D platformer of 189 entities) to about
|
|
1425
|
+
25 KiB, Kestrel Stair (a 3D world of 93 entities: five skinned
|
|
1426
|
+
characters on graphs, five cloths and a rope, five navmesh agents with
|
|
1427
|
+
minds) to about 78 KB. The store takes 64 KiB a key (a platform limit,
|
|
1428
|
+
with 64 keys and 1 MiB per player and game), so a world goes through
|
|
1429
|
+
`og.platform.saveSlot`/`loadSlot`, never one `storeSet`: the slot's
|
|
1430
|
+
text is cut into parts of a key each (at most eight: 512 KiB), written
|
|
1431
|
+
in the bank of keys its last save did not use (`slot1.a0..`,
|
|
1432
|
+
`slot1.b0..`), and the manifest (`slot1`: the bank, the parts, the
|
|
1433
|
+
length, a checksum and your `meta`) last, at the revision read, so a
|
|
1434
|
+
save cut short leaves the last one whole and another device's save
|
|
1435
|
+
landing meanwhile is a `conflict`, never a mix. One request answers
|
|
1436
|
+
for all of it; loadSlot checks the parts against the manifest. The
|
|
1437
|
+
text is already deflated: a tighter encoding was measured (deflate at
|
|
1438
|
+
its highest level saves 0.7 %, LZMA 31 % at the cost of a second
|
|
1439
|
+
decoder in every build) and not taken.
|
|
1440
|
+
|
|
1441
|
+
Physics carries a triangle-mesh, convex-hull or height-field collider
|
|
1442
|
+
by its geometry's handle and scale, not its triangles (a 956 m
|
|
1443
|
+
circuit's road and walls, 6,700 triangles, were 384 KB of a save
|
|
1444
|
+
before; they are now a few bytes): load the glTF or call
|
|
1445
|
+
`og.physics.setMesh` before restoring, as for every asset, or the
|
|
1446
|
+
restore is refused naming the mesh.
|
|
1447
|
+
|
|
1448
|
+
A restore asked for in a system or a command applies when the tick
|
|
1449
|
+
ends; in `start`, at once. A save taken in a system is the middle of
|
|
1450
|
+
its tick: restored, the next step completes that tick (the systems
|
|
1451
|
+
after the saver and the physics step), so a replay from it (the
|
|
1452
|
+
countdown's world restored, the taped inputs fed again) runs the saved
|
|
1453
|
+
race tick for tick. Take it as the system's last write of the tick:
|
|
1454
|
+
what the system does after the call is not in it. A saved type whose fields changed since (a
|
|
1455
|
+
release added one) is refused naming the type and fields; pass
|
|
1456
|
+
`{ migrate: true }` to map it by field name (new fields take their
|
|
1457
|
+
defaults), and keep a saved type's fields stable between releases where
|
|
1458
|
+
you can. Keep progress in components and resources, never in module
|
|
1459
|
+
variables (they are not in the save), and keep request numbers and the
|
|
1460
|
+
session's own state (a best score read from the store, an open menu)
|
|
1461
|
+
where a restore cannot roll them back: reset them after it. Asset
|
|
1462
|
+
handles are saved as numbers: load in `start`, in the same order every
|
|
1463
|
+
launch.
|
|
1464
|
+
- **A record** (`storeSet(world, { key: "save", value: { chimes, glimmers,
|
|
1465
|
+
checkpoint } })`): a few hundred bytes of what progress means, rebuilt
|
|
1466
|
+
into a world on Continue. Smaller, readable on a title screen, and it
|
|
1467
|
+
survives any change to the game's types; right for checkpoint games
|
|
1468
|
+
(examples/hollowbell's `src/save.ts`).
|
|
1469
|
+
|
|
1470
|
+
Test either across a relaunch with a playtest's `store: "keep"` and
|
|
1471
|
+
`then` (og-platform-sdk §4). A watch replay (the start's snapshot and a
|
|
1472
|
+
tape of inputs) and a ghost (a tape of poses) are og-core §2's
|
|
1473
|
+
"Recording, replaying and ghosts".
|
|
1474
|
+
|
|
1475
|
+
## 18. Where to look things up
|
|
1476
|
+
|
|
1477
|
+
`reference/index.md` (the generated wiki shipped with this skill) maps every
|
|
1478
|
+
module (grouped by skill, as in §1), type and function to its page, with the IDL text, defaults, ranges
|
|
1479
|
+
and units of every field.
|