emeraldengine 3.0.0 → 3.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/README.md +1498 -1659
- package/dist/types/index.d.ts +4 -1
- package/dist/types/src/Animator.d.ts +1 -1
- package/dist/types/src/CollisionLayers.d.ts +2 -2
- package/dist/types/src/Color.d.ts +1 -0
- package/dist/types/src/Drawable.d.ts +1 -1
- package/dist/types/src/EmeraldDB.d.ts +2 -2
- package/dist/types/src/InstancedTexture.d.ts +17 -2
- package/dist/types/src/Material.d.ts +2 -2
- package/dist/types/src/MathUtils.d.ts +2 -1
- package/dist/types/src/ParticleEmitter.d.ts +1 -1
- package/dist/types/src/Physics.d.ts +148 -18
- package/dist/types/src/Scene.d.ts +1 -1
- package/dist/types/src/Shaders.d.ts +2 -2
- package/dist/types/src/Tilemap.d.ts +1 -1
- package/dist/types/src/UI.d.ts +1 -1
- package/dist/types/src/components/Behaviour.d.ts +2 -2
- package/dist/types/src/components/Collider.d.ts +7 -1
- package/dist/types/src/components/GameObject.d.ts +2 -2
- package/dist/types/src/components/PolygonCollider.d.ts +33 -0
- package/dist/types/src/components/RigidBody.d.ts +281 -8
- package/dist/types/src/importers/Aseprite.d.ts +2 -2
- package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
- package/dist/types/src/importers/TiledMap.d.ts +1 -1
- package/dist/types/src/managers/EventManager.d.ts +1 -1
- package/dist/types/src/managers/Gamepad.d.ts +102 -0
- package/dist/types/src/managers/InputManager.d.ts +93 -2
- package/dist/types/src/managers/NetworkManager.d.ts +2 -2
- package/dist/types/src/managers/RenderStats.d.ts +1 -1
- package/dist/types/src/managers/TextureManager.d.ts +1 -1
- package/dist/types/src/physics/AABB.d.ts +92 -0
- package/dist/types/src/physics/Body.d.ts +435 -0
- package/dist/types/src/physics/BodyType.d.ts +6 -0
- package/dist/types/src/physics/BroadPhase.d.ts +210 -0
- package/dist/types/src/physics/Collision.d.ts +102 -0
- package/dist/types/src/physics/Contact.d.ts +206 -0
- package/dist/types/src/physics/ContactSolver.d.ts +108 -0
- package/dist/types/src/physics/Distance.d.ts +54 -0
- package/dist/types/src/physics/DistanceJoint.d.ts +90 -0
- package/dist/types/src/physics/Fixture.d.ts +221 -0
- package/dist/types/src/physics/Island.d.ts +52 -0
- package/dist/types/src/physics/Joint.d.ts +59 -0
- package/dist/types/src/physics/Math2D.d.ts +371 -0
- package/dist/types/src/physics/RevoluteJoint.d.ts +119 -0
- package/dist/types/src/physics/Settings.d.ts +22 -0
- package/dist/types/src/physics/Shapes.d.ts +207 -0
- package/dist/types/src/physics/TimeOfImpact.d.ts +22 -0
- package/dist/types/src/physics/World.d.ts +274 -0
- package/dist/types/src/physics/index.d.ts +34 -0
- package/index.js +6 -0
- package/package.json +2 -3
- package/src/Animator.js +1 -1
- package/src/CollisionLayers.js +3 -3
- package/src/Color.js +8 -0
- package/src/Drawable.js +1 -1
- package/src/Emerald.js +1 -1
- package/src/EmeraldDB.js +2 -2
- package/src/InstancedTexture.js +57 -9
- package/src/Material.js +2 -2
- package/src/MathUtils.js +2 -1
- package/src/ParticleEmitter.js +1 -1
- package/src/Physics.js +270 -60
- package/src/Scene.js +1 -1
- package/src/Shaders.js +20 -20
- package/src/Tilemap.js +1 -1
- package/src/UI.js +1 -1
- package/src/components/Behaviour.js +2 -2
- package/src/components/BoxCollider.js +7 -9
- package/src/components/BoxColliderDebug.js +3 -4
- package/src/components/CircleCollider.js +7 -9
- package/src/components/CircleColliderDebug.js +3 -2
- package/src/components/Collider.js +13 -3
- package/src/components/GameObject.js +2 -2
- package/src/components/PolygonCollider.js +55 -0
- package/src/components/RigidBody.js +441 -14
- package/src/importers/Aseprite.js +2 -2
- package/src/importers/ForgeLevel.js +581 -0
- package/src/importers/TiledMap.js +1 -1
- package/src/managers/EventManager.js +1 -1
- package/src/managers/Gamepad.js +126 -0
- package/src/managers/InputManager.js +129 -3
- package/src/managers/NetworkManager.js +2 -2
- package/src/managers/RenderStats.js +1 -1
- package/src/managers/TextureManager.js +1 -1
- package/src/physics/AABB.js +207 -0
- package/src/physics/Body.js +862 -0
- package/src/physics/BodyType.js +16 -0
- package/src/physics/BroadPhase.js +641 -0
- package/src/physics/Collision.js +534 -0
- package/src/physics/Contact.js +500 -0
- package/src/physics/ContactSolver.js +526 -0
- package/src/physics/Distance.js +403 -0
- package/src/physics/DistanceJoint.js +227 -0
- package/src/physics/Fixture.js +346 -0
- package/src/physics/Island.js +203 -0
- package/src/physics/Joint.js +78 -0
- package/src/physics/Math2D.js +573 -0
- package/src/physics/RevoluteJoint.js +278 -0
- package/src/physics/Settings.js +78 -0
- package/src/physics/Shapes.js +549 -0
- package/src/physics/TimeOfImpact.js +87 -0
- package/src/physics/World.js +731 -0
- package/src/physics/index.js +79 -0
package/README.md
CHANGED
|
@@ -2,55 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Emerald is a comprehensive 2D graphics engine that can help you create games easier than ever.
|
|
4
4
|
|
|
5
|
-
**New to the engine?**
|
|
6
|
-
it walks from an empty page to a playable sprite with input, tiles, audio,
|
|
7
|
-
saves, and a debug overlay.
|
|
5
|
+
**New to the engine?** The [Getting Started guide](docs/getting-started.md) walks from an empty page to a playable sprite with input, tiles, audio, saves, and a debug overlay.
|
|
8
6
|
|
|
9
7
|
## Table of Contents
|
|
10
8
|
|
|
11
9
|
- [Emerald](#emerald)
|
|
12
10
|
- [Table of Contents](#table-of-contents)
|
|
13
11
|
- [Getting Started](#getting-started)
|
|
14
|
-
- [Core Systems](#core-systems)
|
|
15
|
-
- [Sprites: flipping, pivot & anchor](#sprites-flipping-pivot--anchor)
|
|
16
|
-
- [Rendering Pipeline (post-processing, materials, batching)](#rendering-pipeline-post-processing-materials-batching)
|
|
17
|
-
- [Post-processing](#post-processing)
|
|
18
|
-
- [PostEffects (built-in)](#posteffects-built-in)
|
|
19
|
-
- [RenderTarget](#rendertarget)
|
|
20
|
-
- [Material (custom shaders)](#material-custom-shaders)
|
|
21
|
-
- [SpriteBatch](#spritebatch)
|
|
22
|
-
- [In-Engine UI](#in-engine-ui)
|
|
23
|
-
- [Game Loop & Scene Transitions](#game-loop--scene-transitions)
|
|
24
|
-
- [ScreenEffects (transitions)](#screeneffects-transitions)
|
|
25
|
-
- [Coroutines](#coroutines)
|
|
26
|
-
- [Gamepads & Controllers](#gamepads--controllers)
|
|
27
|
-
- [Per-instance color & tinting](#per-instance-color--tinting)
|
|
28
|
-
- [Physics: collision layers & continuous detection](#physics-collision-layers--continuous-detection)
|
|
29
|
-
- [RigidBody velocity helpers](#rigidbody-velocity-helpers)
|
|
30
|
-
- [Tilemap colliders & auto-tiling](#tilemap-colliders--auto-tiling)
|
|
31
|
-
- [Camera follow deadzone](#camera-follow-deadzone)
|
|
32
|
-
- [ParticleEmitter (pooled sprites)](#particleemitter-pooled-sprites)
|
|
33
|
-
- [Advanced Particles](#advanced-particles)
|
|
34
|
-
- [Positional (spatial) Audio](#positional-spatial-audio)
|
|
35
|
-
- [Audio buses & fades](#audio-buses--fades)
|
|
36
|
-
- [AssetManager](#assetmanager-loading)
|
|
37
|
-
- [Asset importers (Tiled & Aseprite)](#asset-importers-tiled--aseprite)
|
|
38
|
-
- [Networking (NetworkManager + Interpolator)](#networking-networkmanager--interpolator)
|
|
39
|
-
- [DebugOverlay (upgraded)](#debugoverlay-upgraded)
|
|
40
|
-
- [Storage (versioned saves)](#storage-versioned-saves)
|
|
41
|
-
- [EmeraldDB (IndexedDB saves)](#emeralddb-indexeddb-saves)
|
|
42
|
-
- [Resolution independence](#resolution-independence-design-resolution)
|
|
43
|
-
- [Auto-pause & lifecycle](#auto-pause--lifecycle)
|
|
44
|
-
- [Production hardening](#production-hardening-resource-lifecycle-context-loss-render-stats)
|
|
45
|
-
- [Usage](#usage)
|
|
46
|
-
- [Basic Setup](#basic-setup)
|
|
47
|
-
- [Set the background color for the engine](#set-the-background-color-for-the-engine)
|
|
48
|
-
- [Drawing the scene](#drawing-the-scene)
|
|
49
12
|
- [Scene](#scene)
|
|
50
|
-
- [Adding and removing items from the scene](#adding-and-removing-items-from-the-scene)
|
|
51
|
-
- [Set Active Scene](#set-active-scene)
|
|
52
13
|
- [Game Objects](#game-objects)
|
|
53
14
|
- [Creating a new GameObject](#creating-a-new-gameobject)
|
|
15
|
+
- [Behaviour (component lifecycle)](#behaviour-component-lifecycle)
|
|
16
|
+
- [Transform hierarchy](#transform-hierarchy)
|
|
54
17
|
- [Components](#components)
|
|
55
18
|
- [Texture](#texture)
|
|
56
19
|
- [InstancedTexture](#instancedtexture)
|
|
@@ -60,849 +23,826 @@ saves, and a debug overlay.
|
|
|
60
23
|
- [RigidBody](#rigidbody)
|
|
61
24
|
- [BoxCollider](#boxcollider)
|
|
62
25
|
- [CircleCollider](#circlecollider)
|
|
26
|
+
- [PolygonCollider](#polygoncollider)
|
|
63
27
|
- [Object methods](#object-methods)
|
|
64
28
|
- [Position](#position)
|
|
65
29
|
- [Rotation](#rotation)
|
|
66
30
|
- [Scale](#scale)
|
|
67
31
|
- [Change color](#change-color)
|
|
68
|
-
- [
|
|
32
|
+
- [Sprite flipping, pivot & anchor](#sprite-flipping-pivot--anchor)
|
|
69
33
|
- [Animations](#animations)
|
|
70
34
|
- [Instance System](#instance-system)
|
|
71
35
|
- [Creating Instances](#creating-instances)
|
|
72
36
|
- [Instance Management](#instance-management)
|
|
73
37
|
- [Instance Events](#instance-events)
|
|
38
|
+
- [Per-instance color](#per-instance-color)
|
|
39
|
+
- [Per-instance atlas regions](#per-instance-atlas-regions)
|
|
40
|
+
- [Per-instance animation](#per-instance-animation)
|
|
74
41
|
- [Physics Engine](#physics-engine)
|
|
75
42
|
- [Setting up Physics](#setting-up-physics)
|
|
76
|
-
- [
|
|
43
|
+
- [Time stepping](#time-stepping)
|
|
44
|
+
- [RigidBody methods](#rigidbody-methods)
|
|
77
45
|
- [Collision Detection](#collision-detection)
|
|
78
|
-
- [Collision
|
|
46
|
+
- [Collision layers](#collision-layers)
|
|
47
|
+
- [Continuous collision detection](#continuous-collision-detection)
|
|
48
|
+
- [Raycasting & point queries](#raycasting--point-queries)
|
|
49
|
+
- [Tilemap colliders & auto-tiling](#tilemap-colliders--auto-tiling)
|
|
79
50
|
- [Particle System](#particle-system)
|
|
80
51
|
- [Particle Settings](#particle-settings)
|
|
81
52
|
- [Creating Particle Systems](#creating-particle-systems)
|
|
82
53
|
- [Particle System Methods](#particle-system-methods)
|
|
54
|
+
- [ParticleEmitter (simple pooled bursts)](#particleemitter-simple-pooled-bursts)
|
|
83
55
|
- [Lighting System](#lighting-system)
|
|
84
56
|
- [Ambient Light](#ambient-light)
|
|
85
57
|
- [Point Light](#point-light)
|
|
86
58
|
- [Directional Light](#directional-light)
|
|
87
59
|
- [Text Rendering](#text-rendering)
|
|
88
60
|
- [BitmapText](#bitmaptext)
|
|
89
|
-
|
|
61
|
+
- [CanvasText](#canvastext)
|
|
62
|
+
- [Input](#input)
|
|
90
63
|
- [Keyboard Events](#keyboard-events)
|
|
91
64
|
- [Mouse Events](#mouse-events)
|
|
92
65
|
- [Object Events](#object-events)
|
|
93
66
|
- [Event Cleanup](#event-cleanup)
|
|
67
|
+
- [InputManager (actions)](#inputmanager-actions)
|
|
68
|
+
- [Gamepads & Controllers](#gamepads--controllers)
|
|
94
69
|
- [AudioManager](#audiomanager)
|
|
95
70
|
- [Adding Audio](#adding-audio)
|
|
96
71
|
- [Playing Audio](#playing-audio)
|
|
97
72
|
- [Audio Control](#audio-control)
|
|
73
|
+
- [Buses & Fades](#buses--fades)
|
|
74
|
+
- [Positional Audio](#positional-audio)
|
|
98
75
|
- [Camera](#camera)
|
|
99
76
|
- [FPSCounter](#fpscounter)
|
|
100
|
-
- [Scene Management](#scene-management)
|
|
101
77
|
- [Time Management](#time-management)
|
|
78
|
+
- [Tween, Timer & StateMachine](#tween-timer--statemachine)
|
|
79
|
+
- [SpatialGrid & Pool](#spatialgrid--pool)
|
|
80
|
+
- [MathUtils](#mathutils)
|
|
81
|
+
- [Coroutines](#coroutines)
|
|
82
|
+
- [Rendering Pipeline](#rendering-pipeline)
|
|
83
|
+
- [Post-processing](#post-processing)
|
|
84
|
+
- [PostEffects (built-in)](#posteffects-built-in)
|
|
85
|
+
- [RenderTarget](#rendertarget)
|
|
86
|
+
- [Material (custom shaders)](#material-custom-shaders)
|
|
87
|
+
- [SpriteBatch](#spritebatch)
|
|
88
|
+
- [In-Engine UI](#in-engine-ui)
|
|
89
|
+
- [ScreenEffects (transitions)](#screeneffects-transitions)
|
|
90
|
+
- [Scene Transitions & the Game Loop](#scene-transitions--the-game-loop)
|
|
91
|
+
- [DebugOverlay](#debugoverlay)
|
|
92
|
+
- [Serializer (save/load scenes)](#serializer-saveload-scenes)
|
|
93
|
+
- [Storage (versioned saves)](#storage-versioned-saves)
|
|
94
|
+
- [EmeraldDB (IndexedDB saves)](#emeralddb-indexeddb-saves)
|
|
95
|
+
- [AssetManager](#assetmanager)
|
|
96
|
+
- [Asset importers (Tiled, Aseprite & Forge)](#asset-importers-tiled-aseprite--forge)
|
|
97
|
+
- [Networking (NetworkManager + Interpolator)](#networking-networkmanager--interpolator)
|
|
102
98
|
- [Advanced Features](#advanced-features)
|
|
103
99
|
- [Resize Handling](#resize-handling)
|
|
100
|
+
- [Resolution independence](#resolution-independence)
|
|
101
|
+
- [Auto-pause & lifecycle](#auto-pause--lifecycle)
|
|
102
|
+
- [Production hardening](#production-hardening)
|
|
103
|
+
- [NPM scripts](#npm-scripts)
|
|
104
104
|
|
|
105
105
|
## Getting Started
|
|
106
106
|
|
|
107
107
|
To get started with Emerald, you need to have a canvas element in your HTML and import the necessary classes.
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
```javascript
|
|
110
|
+
import { Emerald, Scene, Color, SceneManager } from "emeraldengine";
|
|
110
111
|
|
|
111
|
-
|
|
112
|
+
const emerald = new Emerald(canvas); // You should pass your own canvas element here
|
|
113
|
+
const scene = new Scene();
|
|
114
|
+
SceneManager.setScene(scene);
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
emerald.setBackgroundColor(new Color(20, 20, 30, 255)); // color = new Color(r, g, b, a = 255)
|
|
117
|
+
```
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
To draw items on the screen you need some sort of animation loop. You can drive one yourself with `window.requestAnimationFrame`:
|
|
116
120
|
|
|
117
121
|
```javascript
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
let lastTime = 0;
|
|
123
|
+
const animate = (currentTime) => {
|
|
124
|
+
const deltaTime = (currentTime - lastTime) / 1000;
|
|
125
|
+
lastTime = currentTime;
|
|
126
|
+
emerald.drawScene(scene, deltaTime); // You need this line to tell the engine what to draw
|
|
127
|
+
window.requestAnimationFrame(animate);
|
|
128
|
+
};
|
|
129
|
+
animate(0);
|
|
123
130
|
```
|
|
124
131
|
|
|
125
|
-
|
|
132
|
+
Or let `emerald.run()` own the loop for you. It computes a clamped delta time, pauses automatically when the tab is hidden, and optionally drives a fixed-timestep simulation alongside your rendering:
|
|
126
133
|
|
|
127
134
|
```javascript
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
const stop = emerald.run(
|
|
136
|
+
(dt, alpha) => {
|
|
137
|
+
world.update(dt);
|
|
138
|
+
emerald.drawScene(scene, dt);
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
maxDelta: 0.25, // clamp dt after a tab-switch stall
|
|
142
|
+
fixedStep: 1 / 60, // optional fixed simulation step (0 = off)
|
|
143
|
+
fixedUpdate: (step) => physics.process(step),
|
|
136
144
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
onDestroy() {}
|
|
140
|
-
}
|
|
141
|
-
obj.addComponent(new Spinner());
|
|
142
|
-
scene.update(dt); // ticks all Behaviours on active objects
|
|
145
|
+
);
|
|
146
|
+
// later: stop(); // or emerald.stop();
|
|
143
147
|
```
|
|
144
148
|
|
|
145
|
-
|
|
149
|
+
See [Auto-pause & lifecycle](#auto-pause--lifecycle) for the pause/resume hooks `run()` accepts.
|
|
146
150
|
|
|
147
|
-
|
|
148
|
-
parent.addChild(child); // or child.setParent(parent)
|
|
149
|
-
child.setParent(null); // detach
|
|
150
|
-
// child.transform composes on top of the parent's position/rotation/scale
|
|
151
|
-
```
|
|
151
|
+
## Scene
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
Emerald has multiple scenes support. In order to render any object it has to be added to the scene using the `add` method.
|
|
154
154
|
|
|
155
155
|
```javascript
|
|
156
|
-
|
|
156
|
+
// Adding an object to the scene
|
|
157
|
+
scene.add(gameObject);
|
|
157
158
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
const bottom = new Camera({ viewport: { x: 0, y: 0, width: 1, height: 0.5 } });
|
|
161
|
-
emerald.setCameras([top, bottom]); // or emerald.addCamera(cam) / removeCamera(cam)
|
|
159
|
+
// Removing an object from the scene
|
|
160
|
+
scene.remove(gameObject);
|
|
162
161
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
// Freeing it for good (buffers + texture reference), instead of just removing it
|
|
163
|
+
scene.remove(gameObject, { dispose: true });
|
|
164
|
+
```
|
|
166
165
|
|
|
167
|
-
|
|
168
|
-
top.setIgnoreLayers([11, 20]); // skip these object layers in this camera
|
|
166
|
+
When changing a scene you should deactivate the current scene to not mess up the event manager:
|
|
169
167
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
cam.shake(10, 0.3);
|
|
174
|
-
// each frame: cam.update(dt);
|
|
168
|
+
```javascript
|
|
169
|
+
scene.setIsActive(true); // Activate
|
|
170
|
+
scene.setIsActive(false); // Deactivate
|
|
175
171
|
```
|
|
176
172
|
|
|
177
|
-
|
|
173
|
+
`SceneManager` keeps track of which scene is currently active:
|
|
178
174
|
|
|
179
175
|
```javascript
|
|
180
|
-
import {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// in the loop:
|
|
185
|
-
if (input.justPressed("jump")) player.jump();
|
|
186
|
-
const move = input.getAxis("left", "right"); // -1 / 0 / +1
|
|
187
|
-
input.update(); // call once per frame (edge detection + gamepad polling)
|
|
176
|
+
import { SceneManager } from "emeraldengine";
|
|
177
|
+
|
|
178
|
+
SceneManager.setScene(scene);
|
|
179
|
+
const currentScene = SceneManager.getScene();
|
|
188
180
|
```
|
|
189
181
|
|
|
190
|
-
|
|
191
|
-
pads) are documented in [Gamepads & Controllers](#gamepads--controllers).
|
|
182
|
+
It can also switch scenes behind a fade, wired to `ScreenEffects`. See [Scene Transitions & the Game Loop](#scene-transitions--the-game-loop).
|
|
192
183
|
|
|
193
|
-
|
|
184
|
+
## Game Objects
|
|
194
185
|
|
|
195
|
-
###
|
|
186
|
+
### Creating a new GameObject
|
|
196
187
|
|
|
197
188
|
```javascript
|
|
198
|
-
import {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
Tween.killOf(target);
|
|
208
|
-
Tween.killAll();
|
|
209
|
-
// driven automatically by drawScene
|
|
189
|
+
import { GameObject, Vector3, Vector2 } from "emeraldengine";
|
|
190
|
+
/*
|
|
191
|
+
ARGUMENTS:
|
|
192
|
+
1. name: string = Name of the new GameObject
|
|
193
|
+
2. position: Vector3 = Position of the new GameObject
|
|
194
|
+
3. rotation: number = Rotation of the new GameObject
|
|
195
|
+
4. scale: Vector2 = Scale of the new GameObject
|
|
196
|
+
*/
|
|
197
|
+
const gameObject = new GameObject(name, position, rotation, scale);
|
|
210
198
|
```
|
|
211
199
|
|
|
212
|
-
|
|
200
|
+
This will create a new empty GameObject. At this stage you will not see anything on the screen until you add some components.
|
|
213
201
|
|
|
214
|
-
|
|
215
|
-
import { Timer } from "emeraldengine";
|
|
216
|
-
Timer.after(2, () => spawnEnemy()); // once
|
|
217
|
-
const h = Timer.every(0.5, () => tick(), 10); // 10 times (omit count = forever)
|
|
218
|
-
Timer.clear(h);
|
|
219
|
-
Timer.clearAll();
|
|
220
|
-
```
|
|
202
|
+
### Behaviour (component lifecycle)
|
|
221
203
|
|
|
222
|
-
|
|
204
|
+
For your own game logic (rather than rendering/physics), add a `Behaviour` component. It's ticked automatically every frame the object is active.
|
|
223
205
|
|
|
224
206
|
```javascript
|
|
225
|
-
import {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
},
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
//
|
|
207
|
+
import { Behaviour } from "emeraldengine";
|
|
208
|
+
|
|
209
|
+
class Spinner extends Behaviour {
|
|
210
|
+
start() {
|
|
211
|
+
this.speed = 2;
|
|
212
|
+
} // once, before the first update
|
|
213
|
+
update(dt) {
|
|
214
|
+
this.gameObject.transform.rotation += this.speed * dt;
|
|
215
|
+
}
|
|
216
|
+
onCollisionEnter(other, contact) {} // requires physics ticking
|
|
217
|
+
onCollisionExit(other, contact) {}
|
|
218
|
+
onDestroy() {}
|
|
219
|
+
}
|
|
220
|
+
gameObject.addComponent(new Spinner());
|
|
235
221
|
```
|
|
236
222
|
|
|
237
|
-
###
|
|
223
|
+
### Transform hierarchy
|
|
224
|
+
|
|
225
|
+
GameObjects can be parented to one another. A child's transform composes on top of its parent's position/rotation/scale, so moving the parent moves the whole group.
|
|
238
226
|
|
|
239
227
|
```javascript
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
grid.clear();
|
|
243
|
-
for (const e of enemies)
|
|
244
|
-
grid.insert(e, e.transform.position.x, e.transform.position.y);
|
|
245
|
-
const near = grid.queryRadius(px, py, 100); // or grid.queryRect(...)
|
|
228
|
+
parent.addChild(child); // or child.setParent(parent)
|
|
229
|
+
child.setParent(null); // detach
|
|
246
230
|
```
|
|
247
231
|
|
|
248
|
-
###
|
|
232
|
+
### Components
|
|
233
|
+
|
|
234
|
+
There are currently 9 components: Texture, InstancedTexture, Square2D, Circle2D, Triangle2D, RigidBody, BoxCollider, CircleCollider, PolygonCollider
|
|
235
|
+
|
|
236
|
+
#### Texture
|
|
249
237
|
|
|
250
238
|
```javascript
|
|
251
|
-
import {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
239
|
+
import { Texture } from "emeraldengine";
|
|
240
|
+
/*
|
|
241
|
+
ARGUMENTS:
|
|
242
|
+
1. texturePath = Specify the path for the texture that you want to use.
|
|
243
|
+
2. frameWidth: number = The width of each frame.
|
|
244
|
+
3. frameHeight: number = The height of each frame.
|
|
245
|
+
4. framesPerRow: number = How many frames are in one row in your spritesheet.
|
|
246
|
+
5. totalFrames: number = How many total frames does your spritesheet have.
|
|
247
|
+
6. animationSpeed: number = Speed of change of every frame.
|
|
248
|
+
7. autoPlay: boolean = Specify if you want the animation to play automatically. If you don't want any animation then pass false for it.
|
|
249
|
+
8. pixelart: boolean = Specify whether the texture should be rendered in pixel art style. (THIS IS OPTIONAL. If you don't specify it then it will be defaulted to true)
|
|
250
|
+
9. useLighting: boolean = Specify whether the texture should react to lighting or not. If you don't want any lighting then pass false for it. (THIS IS OPTIONAL. If you don't specify it then it will be defaulted to true)
|
|
251
|
+
*/
|
|
252
|
+
const texture = new Texture(
|
|
253
|
+
texturePath,
|
|
254
|
+
frameWidth,
|
|
255
|
+
frameHeight,
|
|
256
|
+
framesPerRow,
|
|
257
|
+
totalFrames,
|
|
258
|
+
animationSpeed,
|
|
259
|
+
autoPlay,
|
|
260
|
+
(pixelart = true),
|
|
261
|
+
(useLighting = true)
|
|
256
262
|
);
|
|
257
|
-
|
|
258
|
-
|
|
263
|
+
|
|
264
|
+
// Add the texture to a game object
|
|
265
|
+
gameObject.addComponent(texture);
|
|
259
266
|
```
|
|
260
267
|
|
|
261
|
-
|
|
268
|
+

|
|
262
269
|
|
|
263
|
-
|
|
264
|
-
import { MathUtils } from "emeraldengine";
|
|
265
|
-
MathUtils.clamp(v, 0, 1);
|
|
266
|
-
MathUtils.lerp(a, b, t);
|
|
267
|
-
MathUtils.map(v, 0, 10, 0, 100);
|
|
268
|
-
MathUtils.degToRad(90);
|
|
269
|
-
MathUtils.randomRange(0, 5);
|
|
270
|
-
MathUtils.randomInt(1, 6);
|
|
271
|
-
MathUtils.distance(a, b);
|
|
272
|
-
MathUtils.normalize(v);
|
|
273
|
-
MathUtils.angleBetween(a, b);
|
|
274
|
-
```
|
|
270
|
+
#### InstancedTexture
|
|
275
271
|
|
|
276
|
-
|
|
272
|
+
InstancedTexture is perfect for rendering many objects with the same texture efficiently, such as tiles, particles, or repeating elements. The whole batch is drawn in a single draw call. See the [Instance System](#instance-system) section for how to add, manage, and animate instances.
|
|
277
273
|
|
|
278
274
|
```javascript
|
|
279
|
-
|
|
280
|
-
|
|
275
|
+
import { InstancedTexture } from "emeraldengine";
|
|
276
|
+
/*
|
|
277
|
+
ARGUMENTS:
|
|
278
|
+
1. texturePath = Specify the path for the texture that you want to use.
|
|
279
|
+
2. instanceCount: number = How many instances of the texture you want to create.
|
|
280
|
+
3. frameWidth: number = The width of each frame.
|
|
281
|
+
4. frameHeight: number = The height of each frame.
|
|
282
|
+
5. framesPerRow: number = How many frames are in one row in your spritesheet.
|
|
283
|
+
6. totalFrames: number = How many total frames does your spritesheet have.
|
|
284
|
+
7. animationSpeed: number = Speed of change of every frame.
|
|
285
|
+
8. autoPlay: boolean = Specify if you want the animation to play automatically. If you don't want any animation then pass false for it.
|
|
286
|
+
9. pixelart: boolean = Specify whether the texture should be rendered in pixel art style. (THIS IS OPTIONAL. If you don't specify it then it will be defaulted to true)
|
|
287
|
+
10. useLighting: boolean = Specify whether the texture should react to lighting or not. If you don't want any lighting then pass false for it. (THIS IS OPTIONAL. If you don't specify it then it will be defaulted to true)
|
|
288
|
+
*/
|
|
289
|
+
const instancedTexture = new InstancedTexture(
|
|
290
|
+
texturePath,
|
|
291
|
+
instanceCount,
|
|
292
|
+
frameWidth,
|
|
293
|
+
frameHeight,
|
|
294
|
+
framesPerRow,
|
|
295
|
+
totalFrames,
|
|
296
|
+
animationSpeed,
|
|
297
|
+
autoPlay,
|
|
298
|
+
(pixelart = true),
|
|
299
|
+
(useLighting = true)
|
|
300
|
+
);
|
|
281
301
|
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
// -> { object, rigidBody, point, normal, fraction } | null
|
|
285
|
-
const objects = physics.queryPoint({ x, y }); // owners whose collider contains the point
|
|
302
|
+
// Add the instanced texture to a game object
|
|
303
|
+
gameObject.addComponent(instancedTexture);
|
|
286
304
|
```
|
|
287
305
|
|
|
288
|
-
|
|
306
|
+

|
|
307
|
+
|
|
308
|
+
#### Square2D
|
|
289
309
|
|
|
290
310
|
```javascript
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
311
|
+
import { Square2D } from "emeraldengine";
|
|
312
|
+
let square = new Square2D();
|
|
313
|
+
gameObject.addComponent(square);
|
|
314
|
+
```
|
|
294
315
|
|
|
295
|
-
|
|
296
|
-
drawable.setBlendMode("additive"); // "normal" | "additive" | "multiply"
|
|
316
|
+

|
|
297
317
|
|
|
298
|
-
|
|
299
|
-
emerald.setCullingEnabled(true);
|
|
300
|
-
particles.alwaysVisible = true; // opt an object out (e.g. emitters with spread)
|
|
318
|
+
#### Triangle2D
|
|
301
319
|
|
|
302
|
-
|
|
303
|
-
import {
|
|
304
|
-
|
|
305
|
-
|
|
320
|
+
```javascript
|
|
321
|
+
import { Triangle2D } from "emeraldengine";
|
|
322
|
+
let triangle = new Triangle2D();
|
|
323
|
+
gameObject.addComponent(triangle);
|
|
324
|
+
```
|
|
306
325
|
|
|
307
|
-
|
|
308
|
-
import { CanvasText } from "emeraldengine";
|
|
309
|
-
const label = CanvasText.create("Score: 0", {
|
|
310
|
-
font: "bold 28px monospace",
|
|
311
|
-
color: "#6ee7b7",
|
|
312
|
-
screenSpace: true,
|
|
313
|
-
});
|
|
314
|
-
scene.add(label);
|
|
315
|
-
label.getComponent(CanvasText).setText("Score: 120");
|
|
316
|
-
|
|
317
|
-
// Shared/cached GL textures
|
|
318
|
-
import { TextureManager } from "emeraldengine";
|
|
319
|
-
await TextureManager.preload(["a.png", "b.png"], true);
|
|
320
|
-
```
|
|
326
|
+

|
|
321
327
|
|
|
322
|
-
|
|
328
|
+
#### Circle2D
|
|
323
329
|
|
|
324
330
|
```javascript
|
|
325
|
-
import {
|
|
331
|
+
import { Circle2D } from "emeraldengine";
|
|
332
|
+
/*
|
|
333
|
+
ARGUMENTS:
|
|
334
|
+
1. segments = number of segments that the circle will have. Default is 32.
|
|
335
|
+
*/
|
|
336
|
+
let circle = new Circle2D(segments);
|
|
337
|
+
gameObject.addComponent(circle);
|
|
338
|
+
```
|
|
326
339
|
|
|
327
|
-
|
|
328
|
-
anim
|
|
329
|
-
.addClip("run", [0, 1, 2, 3], { speed: 100 })
|
|
330
|
-
.addClip("jump", [8, 9], { loop: false });
|
|
331
|
-
obj.addComponent(texture);
|
|
332
|
-
obj.addComponent(anim);
|
|
333
|
-
anim.play("run");
|
|
340
|
+

|
|
334
341
|
|
|
335
|
-
|
|
336
|
-
tileSize: 32,
|
|
337
|
-
frameWidth: 16,
|
|
338
|
-
frameHeight: 16,
|
|
339
|
-
framesPerRow: 4,
|
|
340
|
-
totalFrames: 16,
|
|
341
|
-
});
|
|
342
|
-
map.setMap([
|
|
343
|
-
[0, 0, 0],
|
|
344
|
-
[1, -1, 1],
|
|
345
|
-
]); // -1 = empty; one instanced draw call
|
|
346
|
-
scene.add(map.gameObject);
|
|
347
|
-
```
|
|
342
|
+
#### RigidBody
|
|
348
343
|
|
|
349
|
-
|
|
344
|
+
RigidBody is a component that allows you to add physics to your game objects. However, it won't work until you create a Physics instance at the top of your code. `RigidBody` itself is the body: every physics operation (position, velocity, forces, sleep state, mass) is a method on it directly; see [RigidBody methods](#rigidbody-methods) in the Physics Engine section for the full list.
|
|
350
345
|
|
|
351
346
|
```javascript
|
|
352
|
-
|
|
353
|
-
audio.play("jump"); // restart from 0
|
|
354
|
-
audio.playOverlap("jump"); // overlapping copies for rapid SFX
|
|
355
|
-
audio.setMasterVolume(0.5);
|
|
356
|
-
audio.setBusVolume("music", 0.6); // mix buses: master * bus * sound
|
|
357
|
-
audio.crossfade("theme", "boss", 2.0); // timed fades: fadeIn/fadeOut/fadeTo too
|
|
358
|
-
```
|
|
347
|
+
import { RigidBody, Physics, Vector2 } from "emeraldengine";
|
|
359
348
|
|
|
360
|
-
|
|
349
|
+
// Create physics engine first
|
|
350
|
+
const physics = new Physics(-70, 32, 2); // gravity, scale, velocityThreshold
|
|
361
351
|
|
|
362
|
-
|
|
352
|
+
/*
|
|
353
|
+
ARGUMENTS:
|
|
354
|
+
1. physics: Physics = Instance of the Physics class that you created at the top of your code.
|
|
355
|
+
2. type: string = Type of the rigid body. It can be "dynamic", "kinematic", or "static".
|
|
356
|
+
3. position: Vector2 = Position of the rigid body is Vector2 because it doesn't need any Z index.
|
|
357
|
+
4. fixedRotation: boolean = Specify whether the rigid body should have a fixed rotation or not. Default is false.
|
|
358
|
+
5. parentObject: GameObject = (OPTIONAL) If you want to attach the rigid body to a GameObject you can pass it here. If you don't want to attach it to any GameObject then pass null.
|
|
359
|
+
6. offset: Vector2 = (OPTIONAL) Offset from the GameObject's position.
|
|
360
|
+
*/
|
|
361
|
+
const rigidBody = new RigidBody(
|
|
362
|
+
physics,
|
|
363
|
+
"dynamic",
|
|
364
|
+
new Vector2(0, 0),
|
|
365
|
+
false,
|
|
366
|
+
gameObject,
|
|
367
|
+
new Vector2(0, 0)
|
|
368
|
+
);
|
|
363
369
|
|
|
364
|
-
|
|
365
|
-
import { Serializer } from "emeraldengine";
|
|
366
|
-
Serializer.register("coin", (data) => makeCoin(data.value));
|
|
367
|
-
coin.prefabType = "coin";
|
|
368
|
-
coin.serialize = () => ({ value: 5 });
|
|
369
|
-
const json = Serializer.toJSON(scene); // save
|
|
370
|
-
Serializer.fromJSON(json, new Scene()); // load
|
|
370
|
+
gameObject.addComponent(rigidBody);
|
|
371
371
|
```
|
|
372
372
|
|
|
373
|
-
|
|
373
|
+
#### BoxCollider
|
|
374
374
|
|
|
375
375
|
```javascript
|
|
376
|
-
import {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
376
|
+
import { BoxCollider } from "emeraldengine";
|
|
377
|
+
|
|
378
|
+
/*
|
|
379
|
+
ARGUMENTS:
|
|
380
|
+
1. rigidBody: RigidBody = The rigid body component that this collider will be attached to.
|
|
381
|
+
2. size: Vector2 = Size of the box collider.
|
|
382
|
+
3. density: number = Density of the collider.
|
|
383
|
+
4. friction: number = Friction of the collider.
|
|
384
|
+
5. restitution: number = Restitution (bounciness) of the collider.
|
|
385
|
+
6. isSensor: boolean = Whether this collider is a sensor (triggers events but doesn't collide physically).
|
|
386
|
+
7. parentObject: GameObject = (OPTIONAL) Parent GameObject.
|
|
387
|
+
8. filter: Object = (OPTIONAL) Collision filter spec (see Collision layers below).
|
|
388
|
+
*/
|
|
389
|
+
const boxCollider = new BoxCollider(
|
|
390
|
+
rigidBody,
|
|
391
|
+
new Vector2(1, 1),
|
|
392
|
+
1,
|
|
393
|
+
0.3,
|
|
394
|
+
0.1,
|
|
395
|
+
false,
|
|
396
|
+
gameObject
|
|
397
|
+
);
|
|
380
398
|
|
|
381
|
-
|
|
399
|
+
gameObject.addComponent(boxCollider);
|
|
400
|
+
```
|
|
382
401
|
|
|
383
|
-
|
|
384
|
-
| ----------------- | -------------------------------------------- |
|
|
385
|
-
| `npm test` | Node test suite (`node --test test/`) |
|
|
386
|
-
| `npm run types` | Regenerate `dist/types` from JSDoc via `tsc` |
|
|
387
|
-
| `npm run format` | Prettier |
|
|
402
|
+

|
|
388
403
|
|
|
389
|
-
|
|
404
|
+
The `BoxCollider` is specifically made bigger than the `Square2D` component in this image to demonstrate how it works. You can adjust the size of the collider to fit your needs.
|
|
390
405
|
|
|
391
|
-
|
|
392
|
-
touching the GameObject's scale — handy for characters that face left/right and
|
|
393
|
-
for putting a sprite's origin at its feet.
|
|
406
|
+
#### CircleCollider
|
|
394
407
|
|
|
395
408
|
```javascript
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
tex.setFlipX(facing < 0); // mirror horizontally (e.g. face left)
|
|
399
|
-
tex.setFlipY(true); // mirror vertically
|
|
409
|
+
import { CircleCollider } from "emeraldengine";
|
|
400
410
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
411
|
+
/*
|
|
412
|
+
ARGUMENTS:
|
|
413
|
+
1. rigidBody: RigidBody = The rigid body component that this collider will be attached to.
|
|
414
|
+
2. radius: number = Radius of the circle collider.
|
|
415
|
+
3. density: number = Density of the collider.
|
|
416
|
+
4. friction: number = Friction of the collider.
|
|
417
|
+
5. restitution: number = Restitution (bounciness) of the collider.
|
|
418
|
+
6. isSensor: boolean = Whether this collider is a sensor.
|
|
419
|
+
7. parentObject: GameObject = (OPTIONAL) Parent GameObject.
|
|
420
|
+
8. filter: Object = (OPTIONAL) Collision filter spec (see Collision layers below).
|
|
421
|
+
*/
|
|
422
|
+
const circleCollider = new CircleCollider(
|
|
423
|
+
rigidBody,
|
|
424
|
+
1.5,
|
|
425
|
+
1,
|
|
426
|
+
0.3,
|
|
427
|
+
0.8,
|
|
428
|
+
false,
|
|
429
|
+
gameObject
|
|
430
|
+
);
|
|
405
431
|
|
|
406
|
-
|
|
407
|
-
tex.setAnchor(0.5, 1); // bottom-center
|
|
408
|
-
tex.setAnchor(0.5, 0.5); // back to center
|
|
432
|
+
gameObject.addComponent(circleCollider);
|
|
409
433
|
```
|
|
410
434
|
|
|
411
|
-
|
|
435
|
+

|
|
412
436
|
|
|
413
|
-
|
|
437
|
+
The `CircleCollider` is specifically made bigger than the `Circle2D` component in this image to demonstrate how it works. You can adjust the radius of the collider to fit your needs.
|
|
414
438
|
|
|
415
|
-
|
|
439
|
+
#### PolygonCollider
|
|
416
440
|
|
|
417
|
-
|
|
418
|
-
texture and then runs a chain of full-screen shader passes (ping-ponging between two
|
|
419
|
-
render targets) before drawing the final image to the canvas. You manage it entirely
|
|
420
|
-
through the `Emerald` instance:
|
|
441
|
+
For a collision shape a box or circle can't approximate, like ramps, wedges, or arbitrary outlines.
|
|
421
442
|
|
|
422
443
|
```javascript
|
|
423
|
-
|
|
424
|
-
emerald.disablePostProcessing(); // turn it back off
|
|
444
|
+
import { PolygonCollider } from "emeraldengine";
|
|
425
445
|
|
|
426
|
-
|
|
427
|
-
|
|
446
|
+
/*
|
|
447
|
+
ARGUMENTS:
|
|
448
|
+
1. rigidBody: RigidBody = The rigid body component that this collider will be attached to.
|
|
449
|
+
2. points: Array<{x:number, y:number}> = Local-space points, in physics units, in any order. The convex hull of these points is used, so a concave outline needs more than one collider.
|
|
450
|
+
3. density: number = Density of the collider.
|
|
451
|
+
4. friction: number = Friction of the collider.
|
|
452
|
+
5. restitution: number = Restitution (bounciness) of the collider.
|
|
453
|
+
6. isSensor: boolean = (OPTIONAL) Whether this collider is a sensor. Default is false.
|
|
454
|
+
7. parentObject: GameObject = (OPTIONAL) Parent GameObject.
|
|
455
|
+
8. filter: Object = (OPTIONAL) Collision filter spec (see Collision layers below).
|
|
456
|
+
*/
|
|
457
|
+
const polygonCollider = new PolygonCollider(
|
|
458
|
+
rigidBody,
|
|
459
|
+
[
|
|
460
|
+
{ x: -1, y: -0.5 },
|
|
461
|
+
{ x: 1, y: -0.5 },
|
|
462
|
+
{ x: 0, y: 1 },
|
|
463
|
+
],
|
|
464
|
+
1,
|
|
465
|
+
0.3,
|
|
466
|
+
0.1,
|
|
467
|
+
false,
|
|
468
|
+
gameObject
|
|
469
|
+
);
|
|
428
470
|
|
|
429
|
-
|
|
430
|
-
bloom.enabled = false;
|
|
471
|
+
gameObject.addComponent(polygonCollider);
|
|
431
472
|
```
|
|
432
473
|
|
|
433
|
-
|
|
434
|
-
exists; if none do, it draws straight to the screen with zero overhead.
|
|
474
|
+
## Object methods
|
|
435
475
|
|
|
436
|
-
|
|
437
|
-
`vUV` (0–1 screen UV), `uScene` (the previous pass), `uResolution`, and `uTime`
|
|
438
|
-
for free — declare any extra uniforms and set them in `setUniforms`:
|
|
476
|
+
### Position
|
|
439
477
|
|
|
440
478
|
```javascript
|
|
441
|
-
|
|
479
|
+
// Set position
|
|
480
|
+
gameObject.transform.position.x = 100;
|
|
481
|
+
gameObject.transform.position.y = 200;
|
|
482
|
+
gameObject.transform.position.z = 0;
|
|
442
483
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
`
|
|
446
|
-
uniform vec3 uTint;
|
|
447
|
-
void main() {
|
|
448
|
-
gl_FragColor = texture2D(uScene, vUV) * vec4(uTint, 1.0);
|
|
449
|
-
}`,
|
|
450
|
-
{
|
|
451
|
-
setUniforms: (gl, loc) => gl.uniform3f(loc("uTint"), 1.0, 0.85, 0.7),
|
|
452
|
-
enabled: true,
|
|
453
|
-
}
|
|
454
|
-
);
|
|
455
|
-
emerald.addPostEffect(tint);
|
|
484
|
+
// Or set all at once
|
|
485
|
+
gameObject.transform.position = new Vector3(100, 200, 0);
|
|
456
486
|
```
|
|
457
487
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
488
|
+

|
|
489
|
+
|
|
490
|
+
### Rotation
|
|
461
491
|
|
|
462
492
|
```javascript
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
hudObject.setLayer(100); // and restrict cameras via setOnlyLayers/ignoreLayers
|
|
493
|
+
// Set rotation (in radians)
|
|
494
|
+
gameObject.transform.rotation = Math.PI / 4; // 45 degrees
|
|
466
495
|
```
|
|
467
496
|
|
|
468
|
-
|
|
497
|
+

|
|
469
498
|
|
|
470
|
-
|
|
499
|
+
### Scale
|
|
471
500
|
|
|
472
501
|
```javascript
|
|
473
|
-
|
|
502
|
+
// Set scale
|
|
503
|
+
gameObject.transform.scale.x = 2;
|
|
504
|
+
gameObject.transform.scale.y = 2;
|
|
474
505
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
PostEffects.bloom({ threshold: 0.6, intensity: 1.2, spread: 1.1 })
|
|
478
|
-
);
|
|
479
|
-
emerald.addPostEffect(
|
|
480
|
-
PostEffects.vignette({ intensity: 0.5, radius: 0.75, softness: 0.45 })
|
|
481
|
-
);
|
|
482
|
-
emerald.addPostEffect(
|
|
483
|
-
PostEffects.colorGrade({ brightness: 0.02, contrast: 1.08, saturation: 1.15 })
|
|
484
|
-
);
|
|
485
|
-
emerald.addPostEffect(PostEffects.chromaticAberration({ amount: 0.003 }));
|
|
486
|
-
emerald.addPostEffect(PostEffects.scanlines({ intensity: 0.15, count: 480 }));
|
|
487
|
-
emerald.addPostEffect(
|
|
488
|
-
PostEffects.crt({ curvature: 4.0, scanlineIntensity: 0.2, vignette: 0.3 })
|
|
489
|
-
);
|
|
490
|
-
emerald.addPostEffect(PostEffects.grayscale());
|
|
506
|
+
// Or set both at once
|
|
507
|
+
gameObject.transform.scale = new Vector2(2, 2);
|
|
491
508
|
```
|
|
492
509
|
|
|
493
|
-
|
|
494
|
-
| --------------------- | ----------------------------------------------------------- |
|
|
495
|
-
| `bloom` | `threshold 0.7`, `intensity 1.0`, `spread 1.0` (multi-pass) |
|
|
496
|
-
| `vignette` | `intensity 0.5`, `radius 0.75`, `softness 0.45` |
|
|
497
|
-
| `colorGrade` | `brightness 0`, `contrast 1`, `saturation 1` |
|
|
498
|
-
| `chromaticAberration` | `amount 0.003` |
|
|
499
|
-
| `scanlines` | `intensity 0.15`, `count 480` |
|
|
500
|
-
| `crt` | `curvature 4.0`, `scanlineIntensity 0.2`, `vignette 0.3` |
|
|
501
|
-
| `grayscale` | — |
|
|
502
|
-
|
|
503
|
-
`bloom` is exported as a class too (`BloomEffect`) if you want to subclass it.
|
|
504
|
-
|
|
505
|
-
### RenderTarget
|
|
510
|
+

|
|
506
511
|
|
|
507
|
-
|
|
508
|
-
Used internally by the post-processor, but useful on its own for minimaps, mirrors,
|
|
509
|
-
or picture-in-picture.
|
|
512
|
+
### Change color
|
|
510
513
|
|
|
511
514
|
```javascript
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
rt.bind(); // binds the FBO and sets the viewport to its size
|
|
516
|
-
// ...draw...
|
|
517
|
-
rt.unbind(); // restore the canvas framebuffer
|
|
518
|
-
// rt.texture now holds the rendered image (a WebGLTexture)
|
|
519
|
-
rt.resize(1024, 1024); // reallocates only if the size changed
|
|
520
|
-
rt.dispose(); // free GL resources
|
|
515
|
+
// For textures
|
|
516
|
+
const texture = gameObject.getComponent(Texture);
|
|
517
|
+
texture.setColor(new Color(255, 0, 0)); // Red
|
|
521
518
|
```
|
|
522
519
|
|
|
523
|
-
|
|
520
|
+

|
|
524
521
|
|
|
525
|
-
|
|
526
|
-
standard vertex shader, so transforms, the camera, and instancing keep working.
|
|
527
|
-
Your fragment program automatically has `vTexCoord`, `vFragPos`, `vInstanceColor`,
|
|
528
|
-
`uSampler`, `uColor`, `uOpacity`, and `uTime` — **don't redeclare them**; declare
|
|
529
|
-
any extra uniforms and push values with `set(name, value)`.
|
|
522
|
+
### Sprite flipping, pivot & anchor
|
|
530
523
|
|
|
531
|
-
|
|
532
|
-
import { Material, Square2D } from "emeraldengine";
|
|
524
|
+
Any `Texture` (or other `Drawable`) can be mirrored and re-pivoted without touching the GameObject's scale, handy for characters that face left/right and for putting a sprite's origin at its feet.
|
|
533
525
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
void main() {
|
|
538
|
-
vec4 c = texture2D(uSampler, vTexCoord);
|
|
539
|
-
if (c.a < uAmount) discard;
|
|
540
|
-
gl_FragColor = c * uColor * uOpacity;
|
|
541
|
-
}
|
|
542
|
-
`,
|
|
543
|
-
{ uniforms: { uAmount: 0.0 } }
|
|
544
|
-
);
|
|
526
|
+
```javascript
|
|
527
|
+
tex.setFlipX(facing < 0); // mirror horizontally (e.g. face left)
|
|
528
|
+
tex.setFlipY(true); // mirror vertically
|
|
545
529
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
530
|
+
// Pivot: which local point sits on the GameObject's position and acts as the
|
|
531
|
+
// rotation/scale center. (0,0) = center (default); x in [-1,1] left..right,
|
|
532
|
+
// y in [-1,1] bottom..top.
|
|
533
|
+
tex.setPivot(0, -1); // bottom-center, feet on the ground
|
|
549
534
|
|
|
550
|
-
//
|
|
551
|
-
|
|
552
|
-
|
|
535
|
+
// Anchor: the same thing in 0..1 with a top-left origin (CSS-style).
|
|
536
|
+
tex.setAnchor(0.5, 1); // bottom-center
|
|
537
|
+
tex.setAnchor(0.5, 0.5); // back to center
|
|
553
538
|
```
|
|
554
539
|
|
|
555
|
-
|
|
540
|
+
## Animations
|
|
541
|
+
|
|
542
|
+
For animated textures, `setFrame` jumps straight to a specific frame:
|
|
556
543
|
|
|
557
544
|
```javascript
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
float pulse = 0.5 + 0.5 * sin(uTime * 4.0 + vTexCoord.y * 6.2831);
|
|
561
|
-
vec3 col = mix(vec3(0.40, 1.0, 0.70), vec3(1.0, 0.9, 0.4), pulse);
|
|
562
|
-
gl_FragColor = vec4(col, 0.5 * pulse);
|
|
563
|
-
}
|
|
564
|
-
`);
|
|
565
|
-
square.setMaterial(glow);
|
|
566
|
-
square.setBlendMode("additive");
|
|
545
|
+
const texture = gameObject.getComponent(Texture);
|
|
546
|
+
texture.setFrame(2); // Set to frame 2
|
|
567
547
|
```
|
|
568
548
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
A dynamic batched renderer with its own minimal shader. Instead of one draw call
|
|
572
|
-
per sprite, it accumulates sprites that share a texture into a single interleaved
|
|
573
|
-
buffer and submits them in one `drawElements` call — ideal for many same-atlas
|
|
574
|
-
quads (bullets, tiles, text glyphs).
|
|
549
|
+
Or play through a sequence of frames:
|
|
575
550
|
|
|
576
551
|
```javascript
|
|
577
|
-
|
|
552
|
+
// Play animation
|
|
553
|
+
texture.playAnimation([0, 1, 2, 3], 200); // frames array, speed in ms
|
|
578
554
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
rotation: e.angle,
|
|
589
|
-
originX: 0.5,
|
|
590
|
-
originY: 0.5,
|
|
591
|
-
u0: e.u0,
|
|
592
|
-
v0: e.v0,
|
|
593
|
-
u1: e.u1,
|
|
594
|
-
v1: e.v1, // UV sub-rect (defaults 0..1)
|
|
595
|
-
r: 1,
|
|
596
|
-
g: 1,
|
|
597
|
-
b: 1,
|
|
598
|
-
a: 1, // per-vertex tint
|
|
599
|
-
});
|
|
555
|
+
// Play once, then stop instead of looping
|
|
556
|
+
texture.playAnimationOnce([0, 1, 2, 3], null, 200, () => console.log("done"));
|
|
557
|
+
|
|
558
|
+
// Stop animation
|
|
559
|
+
texture.stopAnimation();
|
|
560
|
+
|
|
561
|
+
// Check if playing
|
|
562
|
+
if (texture.isPlaying) {
|
|
563
|
+
// Animation is currently playing
|
|
600
564
|
}
|
|
601
|
-
batch.end(); // flushes remaining sprites
|
|
602
|
-
console.log(batch.drawCalls); // GL draw calls emitted this frame
|
|
603
565
|
```
|
|
604
566
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
`UI` is a retained-mode toolkit drawn entirely by the engine — no DOM/HTML overlay.
|
|
608
|
-
Elements are screen-space objects on a dedicated high layer with their own pointer
|
|
609
|
-
and keyboard hit-testing. Positions are **pixels from the viewport center (y up)**,
|
|
610
|
-
either a literal `{x, y}` or a responsive `(viewW, viewH) => ({x, y})` function;
|
|
611
|
-
call `relayout()` after a resize.
|
|
567
|
+

|
|
612
568
|
|
|
613
|
-
|
|
614
|
-
UI layer:
|
|
569
|
+
For named clips instead of raw frame arrays, use `Animator`:
|
|
615
570
|
|
|
616
571
|
```javascript
|
|
617
|
-
import {
|
|
618
|
-
|
|
619
|
-
const uiCam = UI.createCamera(); // a full-screen camera that renders ONLY UI.LAYER
|
|
620
|
-
gameCamera.ignoreLayer(UI.LAYER); // keep the UI out of the game viewport(s)
|
|
621
|
-
emerald.setCameras([gameCamera, uiCam]); // add the UI camera last
|
|
572
|
+
import { Animator } from "emeraldengine";
|
|
622
573
|
|
|
623
|
-
const
|
|
574
|
+
const anim = new Animator();
|
|
575
|
+
anim
|
|
576
|
+
.addClip("run", [0, 1, 2, 3], { speed: 100 })
|
|
577
|
+
.addClip("jump", [8, 9], { loop: false });
|
|
578
|
+
gameObject.addComponent(texture);
|
|
579
|
+
gameObject.addComponent(anim);
|
|
580
|
+
anim.play("run");
|
|
581
|
+
```
|
|
624
582
|
|
|
625
|
-
|
|
626
|
-
const score = ui.label(() => ({ x: 0, y: 200 }), "Score: 0", {
|
|
627
|
-
font: "700 30px system-ui, sans-serif",
|
|
628
|
-
color: "#eaf2ff",
|
|
629
|
-
});
|
|
630
|
-
score.setText("Score: 120");
|
|
583
|
+
## Instance System
|
|
631
584
|
|
|
632
|
-
|
|
633
|
-
ui.button(() => ({ x: 0, y: 0 }), "START", 240, 56, {
|
|
634
|
-
accent: [120, 220, 160],
|
|
635
|
-
onClick: () => startGame(),
|
|
636
|
-
});
|
|
585
|
+
The Instance system allows you to efficiently manage multiple copies of the same texture through `InstancedTexture` (see [Components](#instancedtexture)).
|
|
637
586
|
|
|
638
|
-
|
|
639
|
-
ui.dim(0.6); // full-screen backdrop
|
|
640
|
-
ui.panel(() => ({ x: 0, y: 0 }), 480, 320, { opacity: 0.94 });
|
|
587
|
+
### Creating Instances
|
|
641
588
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
placeholder: "Your name",
|
|
645
|
-
maxLength: 16,
|
|
646
|
-
onChange: (v) => console.log(v),
|
|
647
|
-
});
|
|
648
|
-
name.getValue();
|
|
649
|
-
name.setValue("P1");
|
|
589
|
+
```javascript
|
|
590
|
+
import { Instance } from "emeraldengine";
|
|
650
591
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
592
|
+
// Create an instance
|
|
593
|
+
const instance = new Instance(
|
|
594
|
+
"InstanceName",
|
|
595
|
+
new Vector3(x, y, z),
|
|
596
|
+
new Vector2(width, height),
|
|
597
|
+
rotation,
|
|
598
|
+
frame
|
|
599
|
+
);
|
|
654
600
|
|
|
655
|
-
|
|
601
|
+
// Add to InstancedTexture
|
|
602
|
+
const instancedTexture = gameObject.getComponent(InstancedTexture);
|
|
603
|
+
instancedTexture.addInstance(instance);
|
|
656
604
|
```
|
|
657
605
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
`emerald.run(update, options)` owns the `requestAnimationFrame` loop for you: it
|
|
661
|
-
computes a clamped delta time, optionally advances a fixed-timestep simulation,
|
|
662
|
-
and calls your `update(dt, alpha)` each frame. `alpha` is the 0..1 interpolation
|
|
663
|
-
factor between fixed steps (1 when no fixed step is configured), so you can render
|
|
664
|
-
smoothly between simulation ticks.
|
|
606
|
+
### Instance Management
|
|
665
607
|
|
|
666
608
|
```javascript
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
world.update(dt); // variable-step game logic
|
|
670
|
-
emerald.drawScene(scene, dt);
|
|
671
|
-
},
|
|
672
|
-
{
|
|
673
|
-
maxDelta: 0.25, // clamp dt after a tab-switch stall
|
|
674
|
-
fixedStep: 1 / 60, // optional fixed simulation step (0 = off)
|
|
675
|
-
fixedUpdate: (step) => physics.process(step),
|
|
676
|
-
maxSubSteps: 5,
|
|
677
|
-
}
|
|
678
|
-
);
|
|
679
|
-
// later: stop(); // or emerald.stop();
|
|
680
|
-
```
|
|
609
|
+
// Remove instance
|
|
610
|
+
instancedTexture.removeInstance(instanceId);
|
|
681
611
|
|
|
682
|
-
|
|
683
|
-
|
|
612
|
+
// Get instance by ID
|
|
613
|
+
const instance = instancedTexture.getInstanceWithId(instanceId);
|
|
684
614
|
|
|
685
|
-
|
|
686
|
-
|
|
615
|
+
// Get instance at position
|
|
616
|
+
const instance = instancedTexture.getInstanceAtPosition(position, tolerance);
|
|
687
617
|
|
|
688
|
-
|
|
618
|
+
// Clear all instances
|
|
619
|
+
instancedTexture.clearInstances();
|
|
620
|
+
```
|
|
689
621
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
622
|
+
### Instance Events
|
|
623
|
+
|
|
624
|
+
```javascript
|
|
625
|
+
// Add click event to specific instance
|
|
626
|
+
instancedTexture.addInstanceClickEvent(instanceId, (event) => {
|
|
627
|
+
console.log("Instance clicked!");
|
|
695
628
|
});
|
|
696
629
|
|
|
697
|
-
//
|
|
698
|
-
|
|
630
|
+
// Add hover events to specific instance
|
|
631
|
+
instancedTexture.addInstanceHoverEvent(
|
|
632
|
+
instanceId,
|
|
633
|
+
(event) => console.log("Mouse entered"),
|
|
634
|
+
(event) => console.log("Mouse left")
|
|
635
|
+
);
|
|
699
636
|
```
|
|
700
637
|
|
|
701
|
-
|
|
638
|
+
### Per-instance color
|
|
702
639
|
|
|
703
|
-
|
|
704
|
-
(no CSS overlay), so they survive resolution changes, post-processing and
|
|
705
|
-
split-screen. `fadeOut`/`fadeIn`/`flash` return promises. Call `update(dt)` each
|
|
706
|
-
frame before `drawScene`.
|
|
640
|
+
Each instance can have an independent RGBA tint (white = unchanged, so existing scenes render identically). The tint multiplies the texture in the shader.
|
|
707
641
|
|
|
708
642
|
```javascript
|
|
709
|
-
import {
|
|
710
|
-
|
|
711
|
-
const fx = new ScreenEffects(scene, { layer: 100000, size: 5000 });
|
|
712
|
-
|
|
713
|
-
await fx.fadeOut(0.4, new Color(0, 0, 0, 255)); // fade to black
|
|
714
|
-
loadNextLevel();
|
|
715
|
-
await fx.fadeIn(0.4); // fade back in
|
|
643
|
+
import { Color } from "emeraldengine";
|
|
716
644
|
|
|
717
|
-
|
|
718
|
-
|
|
645
|
+
// On the Instance directly (Color uses 0..255 channels; raw form is 0..1):
|
|
646
|
+
instance.setColor(new Color(255, 120, 60)); // warm tint
|
|
647
|
+
instance.setColor(1.0, 0.4, 0.2, 1.0); // same, as raw 0..1 RGBA
|
|
719
648
|
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
//
|
|
723
|
-
fx.destroy();
|
|
649
|
+
// Or drive it through the InstancedTexture by index:
|
|
650
|
+
instancedTexture.updateInstanceColor(0); // re-read instance 0's tint
|
|
651
|
+
instancedTexture.updateAllInstanceColors(); // re-read every instance's tint
|
|
724
652
|
```
|
|
725
653
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
Generator-based sequencing layered on the same per-frame delta the rest of the
|
|
729
|
-
engine uses. It's driven automatically from `Emerald.drawScene` (so coroutines
|
|
730
|
-
honor pause/slow-mo via `Time.timeScale`).
|
|
731
|
-
|
|
732
|
-
```javascript
|
|
733
|
-
import { Coroutine } from "emeraldengine";
|
|
654
|
+
Tip: for additive sparkle/coin glows, set the instanced texture's blend mode: `instancedTexture.setBlendMode("additive")`.
|
|
734
655
|
|
|
735
|
-
|
|
736
|
-
big.setText("3");
|
|
737
|
-
audio.beep();
|
|
738
|
-
yield 0.7; // wait 0.7 seconds
|
|
739
|
-
big.setText("2");
|
|
740
|
-
audio.beep();
|
|
741
|
-
yield 0.7;
|
|
742
|
-
yield Coroutine.waitFrames(3); // wait 3 frames
|
|
743
|
-
yield Coroutine.waitUntil(() => player.ready); // block until predicate is truthy
|
|
744
|
-
yield Coroutine.waitWhile(() => paused); // block while predicate is truthy
|
|
745
|
-
yield fetch("/level.json"); // await any promise
|
|
746
|
-
yield Coroutine.tween(0, 1, 0.5, (v) => (door.openAmount = v)); // drive a value
|
|
747
|
-
yield otherCoroutineHandle; // wait for a nested coroutine
|
|
748
|
-
start();
|
|
749
|
-
});
|
|
656
|
+
### Per-instance atlas regions
|
|
750
657
|
|
|
751
|
-
|
|
752
|
-
handle.isRunning(); // boolean
|
|
753
|
-
await handle.promise; // resolves when the coroutine finishes or is cancelled
|
|
658
|
+
Normally every instance samples the shared frame grid (`instance.frame`). With `setTexCoords` an instance carries its own UV quad instead, so a single InstancedTexture (one draw call) can batch tiles from an atlas with margins and spacing, apply per-instance flips, or mix arbitrary sprite regions:
|
|
754
659
|
|
|
755
|
-
|
|
756
|
-
|
|
660
|
+
```javascript
|
|
661
|
+
const tile = new Instance(
|
|
662
|
+
"tile",
|
|
663
|
+
new Vector3(x, y, 0),
|
|
664
|
+
new Vector2(32, 32),
|
|
665
|
+
rotation
|
|
666
|
+
);
|
|
667
|
+
// 8 floats, one vec2 per corner in getFrameTexCoords order: (R,B) (L,B) (R,T) (L,T)
|
|
668
|
+
tile.setTexCoords([right, bottom, left, bottom, right, top, left, top]);
|
|
669
|
+
instancedTexture.addInstance(tile);
|
|
670
|
+
instancedTexture.setStatic(true); // non-moving batch: matrices upload once
|
|
757
671
|
```
|
|
758
672
|
|
|
759
|
-
|
|
673
|
+
This is exactly how the Tile Forge level loader renders a whole layer of sliced, rotated, flipped tiles as one draw call. Pass `null` to return an instance to the frame grid.
|
|
760
674
|
|
|
761
|
-
|
|
762
|
-
**semantic button names** that resolve through each pad's mapping, rumble,
|
|
763
|
-
connect/disconnect events, and a registry for non-standard controllers. Gamepad
|
|
764
|
-
input flows through the same `isDown` / `justPressed` / `getAxis` machinery as the
|
|
765
|
-
keyboard, so a token like `"pad:0:south"` works anywhere a key token does
|
|
766
|
-
(including `justPressed` edge detection).
|
|
675
|
+
### Per-instance animation
|
|
767
676
|
|
|
768
|
-
|
|
677
|
+
Instancing and sprite animation aren't mutually exclusive: each `Instance` can run its own independent frame sequence, still batched into the same single draw call.
|
|
769
678
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
679
|
+
```javascript
|
|
680
|
+
// Every instance can animate on its own, at its own pace:
|
|
681
|
+
instancedTexture.animateInstance(zombie1.id, [0, 1, 2, 3], 150);
|
|
682
|
+
instancedTexture.animateInstance(zombie2.id, [4, 5, 6], 250); // a different clip, different speed
|
|
683
|
+
instancedTexture.stopInstanceAnimation(zombie1.id, true); // stop, and revert to its original frame
|
|
773
684
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
| `pad:<i>:<n>` | raw button index (mapping-independent) |
|
|
782
|
-
| `pad:<i>:axis<n>+` / `axis<n>-` | analog axis past the deadzone |
|
|
783
|
-
| `pad:<i>:leftStickUp/Down/Left/Right`, `rightStick...` | analog stick as a d-pad |
|
|
685
|
+
// Or animate every instance together, in lockstep: current ones immediately,
|
|
686
|
+
// and any added later automatically join in:
|
|
687
|
+
instancedTexture.playAnimation([0, 1, 2, 3], 150); // loops
|
|
688
|
+
instancedTexture.playAnimationOnce([10, 11, 12], 150); // plays once, holds the last frame
|
|
689
|
+
instancedTexture.getAnimation(); // -> the frames array currently set this way
|
|
690
|
+
instancedTexture.stopAnimation(); // stop the shared animation on every instance
|
|
691
|
+
```
|
|
784
692
|
|
|
785
|
-
|
|
693
|
+
`playAnimation`/`playAnimationOnce` set the animation every instance plays by default; `animateInstance` overrides that for one instance specifically (a boss that should stay in its own attack animation while the rest of the horde keeps walking, say).
|
|
786
694
|
|
|
787
|
-
|
|
788
|
-
center, not per axis), so diagonals aren't clipped square and direction is
|
|
789
|
-
preserved exactly. Values are **rescaled** — 0 at the deadzone edge, 1 at full
|
|
790
|
-
deflection — so there's no jump at the threshold and the full range stays
|
|
791
|
-
reachable. An optional response curve shapes the middle:
|
|
695
|
+
## Physics Engine
|
|
792
696
|
|
|
793
|
-
|
|
794
|
-
input.setGamepadDeadzone(0.25);
|
|
795
|
-
input.setGamepadCurve(2); // finer control near center (great for camera sticks)
|
|
796
|
-
const { x, y, magnitude, angle } = input.getGamepadStick("left", 0, { invertY: true });
|
|
797
|
-
```
|
|
697
|
+
Emerald ships its own 2D rigid-body physics engine, with no external dependency. It has a dynamic AABB tree broadphase, a separating-axis narrowphase, an impulse solver with warm starting so stacks settle instead of sinking or jittering, island-based sleeping, and continuous collision detection for fast bodies.
|
|
798
698
|
|
|
799
|
-
|
|
800
|
-
value passes `stickPressThreshold` (default 0.5, configurable), so drift never
|
|
801
|
-
triggers menus. Common **DirectInput** pads — Logitech Dual Action, generic
|
|
802
|
-
Twin-USB PS2 adapters, 8BitDo in D-input mode — are recognized out of the box;
|
|
803
|
-
`InputManager.registerGamepadMapping()` overrides always win.
|
|
699
|
+
### Setting up Physics
|
|
804
700
|
|
|
805
701
|
```javascript
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
702
|
+
import { Physics } from "emeraldengine";
|
|
703
|
+
/*
|
|
704
|
+
ARGUMENTS:
|
|
705
|
+
1. gravity: number = Gravity force (negative for downward)
|
|
706
|
+
2. scale: number = Scale factor for physics units to pixels
|
|
707
|
+
3. velocityThreshold: number = Minimum velocity threshold
|
|
708
|
+
*/
|
|
709
|
+
const physics = new Physics(-70, 32, 2);
|
|
809
710
|
```
|
|
810
711
|
|
|
811
|
-
|
|
712
|
+
The simulation runs in physics units (meters, radians, seconds); `scale` is the pixels-per-meter conversion. Aim for bodies roughly 0.1–10 units in size; that's the range the solver tolerances are tuned for. The world itself lives at `physics.world`, and the raw classes are in `src/physics` if you want a bare world without the pixel wrapper.
|
|
812
713
|
|
|
813
|
-
|
|
814
|
-
const { x, y } = input.getGamepadStick("left"); // deadzoned -1..1
|
|
815
|
-
const aim = input.getGamepadStick("right");
|
|
816
|
-
const t = input.getGamepadTrigger("right"); // 0..1
|
|
817
|
-
input.getGamepadButton("south").pressed; // also .value, .index
|
|
818
|
-
input.setGamepadDeadzone(0.25);
|
|
714
|
+
You need to process physics in your own update loop:
|
|
819
715
|
|
|
820
|
-
|
|
716
|
+
```javascript
|
|
717
|
+
const animate = (currentTime) => {
|
|
718
|
+
physics.process(deltaTime);
|
|
719
|
+
};
|
|
821
720
|
```
|
|
822
721
|
|
|
823
|
-
###
|
|
722
|
+
### Time stepping
|
|
824
723
|
|
|
825
|
-
|
|
826
|
-
input.onGamepadConnected((info) =>
|
|
827
|
-
console.log(info.id, info.mapping, info.buttonCount, info.axesCount)
|
|
828
|
-
);
|
|
829
|
-
input.onGamepadDisconnected((info) => pauseFor(info.index));
|
|
724
|
+
`process(dt)` turns a frame's elapsed time into simulation steps, in one of two modes.
|
|
830
725
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
726
|
+
```javascript
|
|
727
|
+
physics.setFixedTimeStep(1 / 60, 5); // step, max substeps (default)
|
|
728
|
+
physics.setVariableTimeStep(1 / 30, 5); // max step, max substeps
|
|
834
729
|
```
|
|
835
730
|
|
|
836
|
-
|
|
731
|
+
**Fixed** banks real time and simulates constant-size slices. It's deterministic: the same inputs give the same result on every machine, and a slow frame can't destabilise the solver. The catch is that motion updates at the step rate, not the display rate, so on a high-refresh screen anything moved in the render frame can slide against sprites that only move every other step.
|
|
837
732
|
|
|
838
|
-
|
|
839
|
-
and work out of the box. For a controller that reports a non-standard mapping (its
|
|
840
|
-
raw button indices differ), register a mapping once — it only applies to pads
|
|
841
|
-
whose id matches **and** that aren't already standard, so a correctly-reporting
|
|
842
|
-
pad is never remapped:
|
|
733
|
+
**Variable** advances once per call using the frame's own delta, so physics runs at exactly the rendering rate. It's not deterministic, and a long frame is a coarser solve. Frames longer than `maxStep` are split into equal steps rather than simulated in one lump, up to `maxSubSteps`.
|
|
843
734
|
|
|
844
735
|
```javascript
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
736
|
+
const steps = physics.process(dt); // how many steps actually ran
|
|
737
|
+
physics.getTimeStepMode(); // "fixed" | "variable"
|
|
738
|
+
physics.getInterpolationAlpha(); // 0..1 through the current fixed step, for interpolating renderables
|
|
848
739
|
```
|
|
849
740
|
|
|
850
|
-
|
|
851
|
-
`dpad*` tokens automatically.
|
|
852
|
-
|
|
853
|
-
## Per-instance color & tinting
|
|
741
|
+
### RigidBody methods
|
|
854
742
|
|
|
855
|
-
|
|
856
|
-
unchanged, so existing scenes render identically). The tint multiplies the
|
|
857
|
-
texture in the shader.
|
|
743
|
+
Position, velocity, forces, sleep state, mass: every operation the physics engine supports is a method on `RigidBody` itself, in world (pixel) units; the engine converts to/from physics units internally, so you never touch the scale factor.
|
|
858
744
|
|
|
859
745
|
```javascript
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
746
|
+
// Position, rotation, velocity
|
|
747
|
+
rigidBody.setPosition(new Vector2(100, 200));
|
|
748
|
+
rigidBody.setTransform(new Vector2(100, 200), Math.PI / 2); // position + angle, atomically
|
|
749
|
+
rigidBody.getPosition(); // Vector2, current position (not the spawn point)
|
|
750
|
+
rigidBody.getInitialPosition(); // Vector2, the position it was created at
|
|
751
|
+
rigidBody.setRotation(Math.PI);
|
|
752
|
+
rigidBody.setLinearVelocity(380, 0); // world units per second
|
|
753
|
+
rigidBody.getLinearVelocity(); // { x, y } in world units/sec
|
|
754
|
+
rigidBody.setAngularVelocity(2); // radians/sec
|
|
755
|
+
rigidBody.getAngularVelocity();
|
|
756
|
+
rigidBody.getLinearVelocityFromWorldPoint({ x, y }); // velocity at a point on the body, spin included
|
|
757
|
+
|
|
758
|
+
// Forces and impulses (world units)
|
|
759
|
+
rigidBody.applyForce(fx, fy); // accumulates; cleared automatically each step
|
|
760
|
+
rigidBody.applyForce(fx, fy, { x, y }); // applied off-center, adds torque
|
|
761
|
+
rigidBody.applyForceToCenter(fx, fy); // never adds torque
|
|
762
|
+
rigidBody.applyTorque(torque);
|
|
763
|
+
rigidBody.applyImpulse(ix, iy); // instantaneous, at the center: jumps, knockback
|
|
764
|
+
rigidBody.applyImpulse(ix, iy, { x, y }); // instantaneous, off-center, adds spin
|
|
765
|
+
rigidBody.applyAngularImpulse(impulse);
|
|
766
|
+
|
|
767
|
+
// Sleeping, activity, rotation lock
|
|
768
|
+
rigidBody.setAwake(true); // wake (or sleep) the body
|
|
769
|
+
rigidBody.isAwake();
|
|
770
|
+
rigidBody.setSleepingAllowed(false); // this body should never sleep
|
|
771
|
+
rigidBody.isSleepingAllowed();
|
|
772
|
+
rigidBody.setActive(false); // pull out of collision detection without destroying it
|
|
773
|
+
rigidBody.isActive();
|
|
774
|
+
rigidBody.setFixedRotation(true); // lock rotation at runtime
|
|
775
|
+
rigidBody.isFixedRotation();
|
|
776
|
+
rigidBody.setType("kinematic"); // change body type at runtime
|
|
777
|
+
|
|
778
|
+
// Damping, gravity, mass
|
|
779
|
+
rigidBody.setLinearDamping(0.5);
|
|
780
|
+
rigidBody.getLinearDamping();
|
|
781
|
+
rigidBody.setAngularDamping(0.2);
|
|
782
|
+
rigidBody.getAngularDamping();
|
|
783
|
+
rigidBody.setGravityScale(2); // 0 disables gravity for this body, 2 doubles it
|
|
784
|
+
rigidBody.getGravityScale();
|
|
785
|
+
rigidBody.getMass();
|
|
786
|
+
rigidBody.getInertia();
|
|
787
|
+
rigidBody.resetMassData(); // re-derive from fixtures after changing a density
|
|
788
|
+
rigidBody.setMassData({ mass, center, I }); // override directly; center is world units
|
|
789
|
+
|
|
790
|
+
// Local/world point and vector conversions (world units in and out)
|
|
791
|
+
rigidBody.getWorldPoint(localPoint);
|
|
792
|
+
rigidBody.getLocalPoint(worldPoint);
|
|
793
|
+
rigidBody.getWorldVector(localVector);
|
|
794
|
+
rigidBody.getLocalVector(worldVector);
|
|
795
|
+
|
|
796
|
+
// Fixtures, without going through a Collider component
|
|
797
|
+
rigidBody.createFixture(shape, { density, friction, restitution });
|
|
798
|
+
rigidBody.destroyFixture(fixture);
|
|
799
|
+
|
|
800
|
+
// Your own data, and the world/contacts this body belongs to
|
|
801
|
+
rigidBody.setUserData({ kind: "crate", hp: 3 });
|
|
802
|
+
rigidBody.getUserData();
|
|
803
|
+
rigidBody.getWorld();
|
|
804
|
+
rigidBody.getContactList();
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
`setUserData`/`getUserData` are entirely separate from the physics engine's own internal bookkeeping (which is how collisions get routed back to this RigidBody), so setting your own data can never interfere with that.
|
|
868
808
|
|
|
869
|
-
|
|
870
|
-
instancedTexture.updateInstanceColor(0); // re-read instance 0's tint
|
|
871
|
-
instancedTexture.updateAllInstanceColors(); // re-read every instance's tint
|
|
872
|
-
```
|
|
809
|
+
### Collision Detection
|
|
873
810
|
|
|
874
|
-
|
|
875
|
-
|
|
811
|
+
```javascript
|
|
812
|
+
// Handle collision enter
|
|
813
|
+
physics.onCollisionEnter((bodyA, bodyB, contact) => {
|
|
814
|
+
console.log("Collision started!");
|
|
876
815
|
|
|
877
|
-
|
|
816
|
+
// Get collision normal. It always points from fixture A's body towards
|
|
817
|
+
// fixture B's, so read it relative to the body you care about:
|
|
818
|
+
const manifold = contact.getWorldManifold();
|
|
819
|
+
const normal = manifold.normal;
|
|
820
|
+
const facingPlayer = contact.getFixtureA().getBody() === playerBody;
|
|
821
|
+
const n = facingPlayer ? normal : { x: -normal.x, y: -normal.y };
|
|
822
|
+
// n.y = -1 the player is standing on something (with y-down gravity)
|
|
823
|
+
// n.y = 1 the player hit a ceiling
|
|
824
|
+
// n.x = -1 / 1 the player hit a wall on that side
|
|
825
|
+
// manifold.separations[i] is how deep contact point i is (negative = overlap)
|
|
878
826
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
827
|
+
// Check if bodies are sensors
|
|
828
|
+
const fixtureA = contact.getFixtureA();
|
|
829
|
+
const fixtureB = contact.getFixtureB();
|
|
830
|
+
if (fixtureA.isSensor() || fixtureB.isSensor()) {
|
|
831
|
+
// Handle sensor collision
|
|
832
|
+
}
|
|
833
|
+
});
|
|
883
834
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
tile.setTexCoords([right, bottom, left, bottom, right, top, left, top]);
|
|
889
|
-
inst.addInstance(tile);
|
|
890
|
-
inst.setStatic(true); // non-moving batch: matrices upload once
|
|
835
|
+
// Handle collision exit
|
|
836
|
+
physics.onCollisionExit((bodyA, bodyB, contact) => {
|
|
837
|
+
console.log("Collision ended!");
|
|
838
|
+
});
|
|
891
839
|
```
|
|
892
840
|
|
|
893
|
-
|
|
894
|
-
sliced, rotated, flipped tiles as one draw call. Pass `null` to return an
|
|
895
|
-
instance to the frame grid.
|
|
841
|
+
Per-object collision events also fire automatically on `Behaviour` components (`onCollisionEnter`/`onCollisionExit`) for any GameObject with a RigidBody. See [Behaviour](#behaviour-component-lifecycle).
|
|
896
842
|
|
|
897
|
-
|
|
843
|
+
### Collision layers
|
|
898
844
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
`CollisionLayers` maps human-readable layer names to the category bits planck uses
|
|
902
|
-
for filtering, so you can express "players collide with ground and enemies, but
|
|
903
|
-
not each other" without juggling bitmasks. Two fixtures collide only when each
|
|
904
|
-
one's category is in the other's mask (symmetric by construction; up to 16
|
|
905
|
-
layers).
|
|
845
|
+
`CollisionLayers` maps human-readable layer names to the category bits the physics engine uses for filtering, so you can express "players collide with ground and enemies, but not each other" without juggling bitmasks. Two fixtures collide only when each one's category is in the other's mask.
|
|
906
846
|
|
|
907
847
|
```javascript
|
|
908
848
|
import { CollisionLayers } from "emeraldengine";
|
|
@@ -914,7 +854,7 @@ playerCollider
|
|
|
914
854
|
.setCollidesWith(["ground", "enemy", "pickup"]);
|
|
915
855
|
enemyCollider.setCategory("enemy").setCollidesWith(["ground", "player"]); // ignore each other
|
|
916
856
|
|
|
917
|
-
// Or up front, in the collider constructor
|
|
857
|
+
// Or up front, in the collider constructor's filter argument:
|
|
918
858
|
new BoxCollider(body, size, 1, 0.2, 0, false, gameObject, {
|
|
919
859
|
category: "pickup",
|
|
920
860
|
collidesWith: ["player"],
|
|
@@ -924,44 +864,29 @@ new BoxCollider(body, size, 1, 0.2, 0, false, gameObject, {
|
|
|
924
864
|
collider.setFilter({ category: 0x0004, mask: 0xffff, group: 0 });
|
|
925
865
|
```
|
|
926
866
|
|
|
927
|
-
### Continuous collision detection
|
|
867
|
+
### Continuous collision detection
|
|
928
868
|
|
|
929
|
-
Fast bodies (a dash, a projectile, a hard fall) can move far enough in one physics
|
|
930
|
-
step to tunnel through thin walls. Mark them continuous so planck solves the swept
|
|
931
|
-
path against static geometry instead:
|
|
869
|
+
Fast bodies (a dash, a projectile, a hard fall) can move far enough in one physics step to tunnel through thin walls. Mark them continuous so the engine sweeps their path against static geometry instead of testing only where they ended up:
|
|
932
870
|
|
|
933
871
|
```javascript
|
|
934
872
|
projectile.setContinuous(true); // bullet-mode CCD
|
|
935
873
|
projectile.isContinuous(); // boolean
|
|
936
874
|
```
|
|
937
875
|
|
|
938
|
-
Reserve it for the handful of bodies that actually move fast
|
|
939
|
-
step.
|
|
940
|
-
|
|
941
|
-
## RigidBody velocity helpers
|
|
876
|
+
Reserve it for the handful of bodies that actually move fast; it costs more per step.
|
|
942
877
|
|
|
943
|
-
|
|
944
|
-
the engine converts to/from physics units internally, so you never touch the
|
|
945
|
-
scale factor.
|
|
878
|
+
### Raycasting & point queries
|
|
946
879
|
|
|
947
880
|
```javascript
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
rigidBody.applyImpulse(0, 900); // impulse at the body's center
|
|
951
|
-
rigidBody.setAwake(true); // wake (or sleep) the body
|
|
881
|
+
const hit = physics.raycast({ x, y }, { x: 1, y: 0 }, 500);
|
|
882
|
+
// -> { object, rigidBody, point, normal, fraction } | null
|
|
952
883
|
|
|
953
|
-
|
|
954
|
-
rigidBody.getPosition(); // Vector2 — current position (not the spawn point)
|
|
955
|
-
rigidBody.getInitialPosition(); // Vector2 — the position it was created at
|
|
884
|
+
const objects = physics.queryPoint({ x, y }); // owners whose collider contains the point
|
|
956
885
|
```
|
|
957
886
|
|
|
958
|
-
|
|
959
|
-
straightforward without reaching into the underlying planck body.
|
|
960
|
-
|
|
961
|
-
## Tilemap colliders & auto-tiling
|
|
887
|
+
### Tilemap colliders & auto-tiling
|
|
962
888
|
|
|
963
|
-
A `Tilemap` can
|
|
964
|
-
frames from a solidity grid.
|
|
889
|
+
A `Tilemap` can generate physics colliders from its map and auto-pick tile frames from a solidity grid.
|
|
965
890
|
|
|
966
891
|
```javascript
|
|
967
892
|
// 1) Build solid colliders from the current map.
|
|
@@ -987,35 +912,111 @@ const frames = Tilemap.computeAutoTile(solidGrid, {
|
|
|
987
912
|
map.setAutoTiledMap(solidGrid, { base: 0, originX: 0, originY: 0 }); // compute + setMap
|
|
988
913
|
```
|
|
989
914
|
|
|
990
|
-
##
|
|
915
|
+
## Particle System
|
|
916
|
+
|
|
917
|
+
Emerald includes a powerful particle system for creating visual effects.
|
|
918
|
+
|
|
919
|
+

|
|
991
920
|
|
|
992
|
-
|
|
993
|
-
once the target leaves a box around the current focus, so small movements don't jitter
|
|
994
|
-
the view. Chainable with the existing follow/bounds/shake API.
|
|
921
|
+
### Particle Settings
|
|
995
922
|
|
|
996
923
|
```javascript
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
.
|
|
1001
|
-
|
|
1002
|
-
|
|
924
|
+
import { ParticleSettings, Vector2, Color } from "emeraldengine";
|
|
925
|
+
|
|
926
|
+
const particleSettings = new ParticleSettings({
|
|
927
|
+
lifetime: 1.2,
|
|
928
|
+
velocity: new Vector2(200, 300),
|
|
929
|
+
gravity: new Vector2(0, -400),
|
|
930
|
+
amount: 16,
|
|
931
|
+
direction: new Vector2(0, 1), // upward
|
|
932
|
+
spread: Math.PI * 2,
|
|
933
|
+
emissionRate: Infinity, // one-shot emission
|
|
934
|
+
frame: 0,
|
|
935
|
+
offset: 5,
|
|
936
|
+
rotation: 0,
|
|
937
|
+
scale: new Vector2(5, 5),
|
|
938
|
+
animation: { frames: [0, 1, 2], speed: 200 },
|
|
939
|
+
|
|
940
|
+
// Emitter shape: where new particles spawn relative to the emit point:
|
|
941
|
+
// "point" | "circle" | "ring" | "box" | "cone" (default)
|
|
942
|
+
shape: "ring",
|
|
943
|
+
shapeRadius: 24, // used by circle/ring
|
|
944
|
+
shapeSize: new Vector2(40, 10), // used by box
|
|
945
|
+
|
|
946
|
+
// Over-lifetime curves ({ from, to } interpolated by normalized age):
|
|
947
|
+
scaleOverLife: { from: 1.4, to: 0.0 }, // size multiplier
|
|
948
|
+
alphaOverLife: { from: 1.0, to: 0.0 }, // opacity
|
|
949
|
+
colorOverLife: {
|
|
950
|
+
from: new Color(255, 240, 180),
|
|
951
|
+
to: new Color(255, 90, 60),
|
|
952
|
+
},
|
|
953
|
+
|
|
954
|
+
rotationSpeed: Math.PI, // radians/sec per particle
|
|
955
|
+
drag: 1.2, // velocity damping per second (0 = none)
|
|
956
|
+
});
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
### Creating Particle Systems
|
|
960
|
+
|
|
961
|
+
```javascript
|
|
962
|
+
import { Particles } from "emeraldengine";
|
|
963
|
+
|
|
964
|
+
/*
|
|
965
|
+
ARGUMENTS:
|
|
966
|
+
1. name: string = Name of the particle system
|
|
967
|
+
2. texturePath: string = Path to the texture
|
|
968
|
+
3. frameWidth: number = Width of each frame
|
|
969
|
+
4. frameHeight: number = Height of each frame
|
|
970
|
+
5. framesPerRow: number = Frames per row in spritesheet
|
|
971
|
+
6. totalFrames: number = Total frames in spritesheet
|
|
972
|
+
7. duration: number = Duration of the effect
|
|
973
|
+
8. settings: ParticleSettings = Particle settings object
|
|
974
|
+
*/
|
|
975
|
+
const particles = new Particles(
|
|
976
|
+
"explosion",
|
|
977
|
+
texturePath,
|
|
978
|
+
16,
|
|
979
|
+
16,
|
|
980
|
+
9,
|
|
981
|
+
27,
|
|
982
|
+
1.2,
|
|
983
|
+
particleSettings
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
// Add to scene
|
|
987
|
+
scene.add(particles.gameObject);
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
### Particle System Methods
|
|
991
|
+
|
|
992
|
+
```javascript
|
|
993
|
+
// Play particle effect at position
|
|
994
|
+
particles.play(new Vector3(x, y, z));
|
|
995
|
+
|
|
996
|
+
// Stop particle system
|
|
997
|
+
particles.stop();
|
|
998
|
+
|
|
999
|
+
// Reset particle system
|
|
1000
|
+
particles.reset();
|
|
1001
|
+
|
|
1002
|
+
// Update particles (call in your animation loop)
|
|
1003
|
+
particles.update(deltaTime);
|
|
1004
|
+
|
|
1005
|
+
// Check if active
|
|
1006
|
+
if (particles.active) {
|
|
1007
|
+
// Particles are currently active
|
|
1008
|
+
}
|
|
1003
1009
|
```
|
|
1004
1010
|
|
|
1005
|
-
|
|
1011
|
+
### ParticleEmitter (simple pooled bursts)
|
|
1006
1012
|
|
|
1007
|
-
`ParticleEmitter` is a
|
|
1008
|
-
fixed pool of ordinary textured GameObjects. Every live particle's transform, tint
|
|
1009
|
-
and opacity are driven by hand each frame — there's no instanced-draw buffer
|
|
1010
|
-
lifecycle to desync, so it keeps rendering for the whole session. Spawn with
|
|
1011
|
-
`burst(n, cfg)` / `emit(cfg)` and advance with `update(dt)`; every `cfg` field is
|
|
1012
|
-
optional.
|
|
1013
|
+
If you don't need per-particle curves, `ParticleEmitter` is a simpler, allocation-free system built from a fixed pool of textured GameObjects, good for one-off bursts (dust, sparkles, confetti, hit effects). Spawn with `burst(n, cfg)` / `emit(cfg)`; every `cfg` field is optional.
|
|
1013
1014
|
|
|
1014
1015
|
```javascript
|
|
1015
1016
|
import { ParticleEmitter } from "emeraldengine";
|
|
1016
1017
|
|
|
1017
1018
|
const fx = new ParticleEmitter(scene, {
|
|
1018
|
-
texture: "spark.png",
|
|
1019
|
+
texture: "spark.png",
|
|
1019
1020
|
capacity: 256, // pool size (max live particles)
|
|
1020
1021
|
layer: 50,
|
|
1021
1022
|
});
|
|
@@ -1028,7 +1029,7 @@ fx.burst(12, {
|
|
|
1028
1029
|
speed: 180, // emission cone
|
|
1029
1030
|
gx: 0,
|
|
1030
1031
|
gy: -300,
|
|
1031
|
-
drag: 2,
|
|
1032
|
+
drag: 2,
|
|
1032
1033
|
life: 0.4,
|
|
1033
1034
|
size: 8,
|
|
1034
1035
|
sFrom: 1,
|
|
@@ -1038,7 +1039,7 @@ fx.burst(12, {
|
|
|
1038
1039
|
cr: 255,
|
|
1039
1040
|
cg: 220,
|
|
1040
1041
|
cb: 120,
|
|
1041
|
-
additive: true,
|
|
1042
|
+
additive: true,
|
|
1042
1043
|
rotSpeed: 6,
|
|
1043
1044
|
shape: "ring",
|
|
1044
1045
|
radius: 12, // "point" | "ring" | "circle" | "box"
|
|
@@ -1051,1309 +1052,1147 @@ fx.reset(); // kill all immediately
|
|
|
1051
1052
|
fx.destroy(); // remove pooled objects from the scene
|
|
1052
1053
|
```
|
|
1053
1054
|
|
|
1054
|
-
|
|
1055
|
-
the curve-driven, instanced system see [Advanced Particles](#advanced-particles)
|
|
1056
|
-
below.
|
|
1055
|
+
## Lighting System
|
|
1057
1056
|
|
|
1058
|
-
|
|
1057
|
+
Emerald supports ambient, point, and directional lighting.
|
|
1059
1058
|
|
|
1060
|
-
|
|
1061
|
-
`rotationSpeed` and `drag`. These layer on top of the existing particle fields.
|
|
1059
|
+
### Ambient Light
|
|
1062
1060
|
|
|
1063
1061
|
```javascript
|
|
1064
|
-
|
|
1062
|
+
// Set ambient light
|
|
1063
|
+
emerald.setAmbientLight(new Vector3(0.3, 0.3, 0.3)); // RGB values 0-1
|
|
1064
|
+
```
|
|
1065
1065
|
|
|
1066
|
-
|
|
1067
|
-
lifetime: 1.0,
|
|
1068
|
-
amount: 24,
|
|
1069
|
-
velocity: new Vector2(0, 260),
|
|
1070
|
-
gravity: new Vector2(0, -300),
|
|
1066
|
+
### Point Light
|
|
1071
1067
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
shape: "ring",
|
|
1075
|
-
shapeRadius: 24, // used by circle/ring
|
|
1076
|
-
shapeSize: new Vector2(40, 10), // used by box
|
|
1068
|
+
```javascript
|
|
1069
|
+
import { PointLight } from "emeraldengine";
|
|
1077
1070
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1071
|
+
/*
|
|
1072
|
+
ARGUMENTS:
|
|
1073
|
+
1. position: Vector2 = Position of the light
|
|
1074
|
+
2. color: Color = Color of the light
|
|
1075
|
+
3. intensity: number = Light intensity
|
|
1076
|
+
4. radius: number = Light radius
|
|
1077
|
+
*/
|
|
1078
|
+
const pointLight = new PointLight(
|
|
1079
|
+
new Vector2(100, 0),
|
|
1080
|
+
new Color(255, 204, 153),
|
|
1081
|
+
1.5,
|
|
1082
|
+
400
|
|
1083
|
+
);
|
|
1086
1084
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
});
|
|
1090
|
-
```
|
|
1085
|
+
// Add to engine
|
|
1086
|
+
emerald.addPointLight(pointLight);
|
|
1091
1087
|
|
|
1092
|
-
|
|
1088
|
+
// Update position
|
|
1089
|
+
pointLight.position.x = newX;
|
|
1090
|
+
pointLight.position.y = newY;
|
|
1091
|
+
```
|
|
1093
1092
|
|
|
1094
|
-
|
|
1095
|
-
the Web Audio `StereoPanner` when available and falls back to volume-only panning
|
|
1096
|
-
otherwise.
|
|
1093
|
+
### Directional Light
|
|
1097
1094
|
|
|
1098
1095
|
```javascript
|
|
1099
|
-
|
|
1100
|
-
|
|
1096
|
+
import { DirectionalLight } from "emeraldengine";
|
|
1097
|
+
|
|
1098
|
+
/*
|
|
1099
|
+
ARGUMENTS:
|
|
1100
|
+
1. position: Vector2 = Position of the light
|
|
1101
|
+
2. direction: Vector2 = Direction vector
|
|
1102
|
+
3. color: Color = Color of the light
|
|
1103
|
+
4. intensity: number = Light intensity
|
|
1104
|
+
5. width: number = Width of the light beam
|
|
1105
|
+
*/
|
|
1106
|
+
const directionalLight = new DirectionalLight(
|
|
1107
|
+
new Vector2(0, 300),
|
|
1108
|
+
new Vector2(0, -1), // pointing down
|
|
1109
|
+
new Color(255, 255, 255),
|
|
1110
|
+
3.0,
|
|
1111
|
+
200
|
|
1112
|
+
);
|
|
1101
1113
|
|
|
1102
|
-
|
|
1114
|
+
// Add to engine
|
|
1115
|
+
emerald.addDirectionalLight(directionalLight);
|
|
1103
1116
|
|
|
1104
|
-
//
|
|
1105
|
-
const
|
|
1117
|
+
// Rotate direction
|
|
1118
|
+
const angle = 0.1;
|
|
1119
|
+
const newX =
|
|
1120
|
+
directionalLight.direction.x * Math.cos(angle) -
|
|
1121
|
+
directionalLight.direction.y * Math.sin(angle);
|
|
1122
|
+
const newY =
|
|
1123
|
+
directionalLight.direction.x * Math.sin(angle) +
|
|
1124
|
+
directionalLight.direction.y * Math.cos(angle);
|
|
1125
|
+
directionalLight.direction.x = newX;
|
|
1126
|
+
directionalLight.direction.y = newY;
|
|
1106
1127
|
```
|
|
1107
1128
|
|
|
1108
|
-
##
|
|
1129
|
+
## Text Rendering
|
|
1130
|
+
|
|
1131
|
+
### BitmapText
|
|
1132
|
+
|
|
1133
|
+
Emerald supports bitmap font rendering using the BitmapText component. This allows you to display text with custom fonts and styles.
|
|
1109
1134
|
|
|
1110
|
-
|
|
1111
|
-
default (new sounds land on `"sfx"`), and any name you use creates a bus on the
|
|
1112
|
-
fly. Effective volume is `master × bus × sound × fade`, so one slider mutes all
|
|
1113
|
-
music without touching the SFX:
|
|
1135
|
+

|
|
1114
1136
|
|
|
1115
1137
|
```javascript
|
|
1116
|
-
|
|
1117
|
-
audio.add("jump.wav", "jump"); // default bus: "sfx"
|
|
1138
|
+
import { BitmapText } from "emeraldengine";
|
|
1118
1139
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1140
|
+
/*
|
|
1141
|
+
ARGUMENTS:
|
|
1142
|
+
1. text: string = Text to display
|
|
1143
|
+
2. texturePath: string = Path to bitmap font texture
|
|
1144
|
+
3. letters: string = String containing all available characters
|
|
1145
|
+
4. letterSpacing: number = Spacing between letters
|
|
1146
|
+
5. frameWidth: number = Width of each character frame
|
|
1147
|
+
6. frameHeight: number = Height of each character frame
|
|
1148
|
+
7. framesPerRow: number = Characters per row in font texture
|
|
1149
|
+
8. totalFrames: number = Total character frames
|
|
1150
|
+
9. pixelArt: boolean = Whether to use pixel art rendering
|
|
1151
|
+
10. fontSize: number = Font size
|
|
1152
|
+
11. color: Color = Text color
|
|
1153
|
+
12. position: Vector3 = Text position
|
|
1154
|
+
13. rotation: number = Text rotation
|
|
1155
|
+
14. useLighting: boolean = Whether text should react to lighting
|
|
1156
|
+
*/
|
|
1157
|
+
const bitmapText = new BitmapText(
|
|
1158
|
+
"Hello World!",
|
|
1159
|
+
fontTexturePath,
|
|
1160
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?.",
|
|
1161
|
+
16,
|
|
1162
|
+
32,
|
|
1163
|
+
32,
|
|
1164
|
+
10,
|
|
1165
|
+
95,
|
|
1166
|
+
true,
|
|
1167
|
+
24,
|
|
1168
|
+
new Color(255, 255, 255),
|
|
1169
|
+
new Vector3(0, 200, 0),
|
|
1170
|
+
0,
|
|
1171
|
+
false
|
|
1172
|
+
);
|
|
1124
1173
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
game loop):
|
|
1174
|
+
// Add to scene
|
|
1175
|
+
scene.add(bitmapText.gameObject);
|
|
1128
1176
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1177
|
+
// Update text
|
|
1178
|
+
bitmapText.setText("New Text!");
|
|
1179
|
+
bitmapText.setColor(new Color(255, 0, 0));
|
|
1180
|
+
bitmapText.setFontSize(32);
|
|
1181
|
+
bitmapText.setLetterSpacing(20);
|
|
1134
1182
|
```
|
|
1135
1183
|
|
|
1136
|
-
|
|
1184
|
+
### CanvasText
|
|
1137
1185
|
|
|
1138
|
-
|
|
1139
|
-
JSON, text, and web fonts — with deduplication and aggregate progress for a
|
|
1140
|
-
loading bar. Images are routed through `TextureManager`, so the GL upload cache is
|
|
1141
|
-
shared with the rest of the engine.
|
|
1186
|
+
`CanvasText` renders any CSS font (including loaded webfonts) into a texture, at the device pixel ratio so text is crisp on Retina/HiDPI displays, with support for multi-line strings, word-wrapping, and alignment. Use `BitmapText` for retro/pixel fonts from a glyph sheet, `CanvasText` for everything else (UI, dialogue, any real font).
|
|
1142
1187
|
|
|
1143
1188
|
```javascript
|
|
1144
|
-
import {
|
|
1189
|
+
import { CanvasText } from "emeraldengine";
|
|
1145
1190
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1191
|
+
// Factory: returns a GameObject already sized to the text
|
|
1192
|
+
const label = CanvasText.create("Score: 0", {
|
|
1193
|
+
font: "700 24px 'Pixelify Sans', sans-serif",
|
|
1194
|
+
color: "#8fe0ff",
|
|
1195
|
+
screenSpace: true, // HUD: fixed on screen, position in px from center
|
|
1196
|
+
position: new Vector3(0, 240, 0),
|
|
1197
|
+
});
|
|
1198
|
+
scene.add(label);
|
|
1153
1199
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1200
|
+
// Multi-line + wrapping
|
|
1201
|
+
const dialog = CanvasText.create(
|
|
1202
|
+
"A long line of dialogue that wraps automatically.\nExplicit breaks work too.",
|
|
1203
|
+
{ font: "16px system-ui", maxWidth: 320, align: "left", lineHeight: 22 }
|
|
1204
|
+
);
|
|
1156
1205
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1206
|
+
// Updating (re-renders the texture; attached GameObject rescales to fit)
|
|
1207
|
+
const text = label.getComponent(CanvasText);
|
|
1208
|
+
text.setText("Score: 120");
|
|
1209
|
+
text.setColor("#ffd166");
|
|
1210
|
+
text.setMaxWidth(400);
|
|
1211
|
+
text.setAlign("center"); // "left" | "center" | "right"
|
|
1163
1212
|
```
|
|
1164
1213
|
|
|
1165
|
-
##
|
|
1214
|
+
## Input
|
|
1166
1215
|
|
|
1167
|
-
|
|
1168
|
-
from [Aseprite](https://www.aseprite.org). Both are pure parsers — hand them the
|
|
1169
|
-
already-parsed JSON (load it with `AssetManager.json` or `fetch`).
|
|
1216
|
+
Emerald supports keyboard, mouse, click, and hover events through the built-in `EventManager` class, plus a higher-level action-mapping `InputManager` for gameplay input (keyboard, mouse and gamepad through one API).
|
|
1170
1217
|
|
|
1171
|
-
|
|
1218
|
+

|
|
1172
1219
|
|
|
1173
1220
|
```javascript
|
|
1174
|
-
import {
|
|
1175
|
-
|
|
1176
|
-
// Build a ready-to-render Tilemap from a Tiled JSON map + its tile sheet.
|
|
1177
|
-
const map = TiledMap.toTilemap(mapJson, "tiles.png", { layer: "ground" });
|
|
1178
|
-
scene.add(map.gameObject);
|
|
1179
|
-
map.buildColliders(physics);
|
|
1180
|
-
|
|
1181
|
-
// Or just the frame grid (for your own Tilemap.setMap call):
|
|
1182
|
-
const grid = TiledMap.toFrameGrid(mapJson, { layer: "ground" });
|
|
1221
|
+
import { EventManager } from "emeraldengine";
|
|
1183
1222
|
|
|
1184
|
-
|
|
1185
|
-
// flattened into `props`, and flipY converts to a y-up world.
|
|
1186
|
-
const spawns = TiledMap.objects(mapJson, { layer: "spawns", flipY: true });
|
|
1187
|
-
// -> [{ name, type, x, y, width, height, gid, props, ... }]
|
|
1223
|
+
let eventManager = new EventManager(canvas, scene, emerald.camera);
|
|
1188
1224
|
```
|
|
1189
1225
|
|
|
1190
|
-
|
|
1226
|
+
`EventManager` hits only the topmost object under the pointer, and its `screenToWorld(clientX, clientY)` accounts for camera zoom, DPR, and viewport.
|
|
1227
|
+
|
|
1228
|
+
### Keyboard Events
|
|
1191
1229
|
|
|
1192
1230
|
```javascript
|
|
1193
|
-
|
|
1231
|
+
// Key down events
|
|
1232
|
+
eventManager.addKeyDown("w", () => {
|
|
1233
|
+
console.log("W key pressed");
|
|
1234
|
+
});
|
|
1194
1235
|
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
cfg.frameHeight,
|
|
1200
|
-
cfg.framesPerRow,
|
|
1201
|
-
cfg.totalFrames,
|
|
1202
|
-
0,
|
|
1203
|
-
false
|
|
1204
|
-
);
|
|
1205
|
-
obj.addComponent(tex);
|
|
1236
|
+
// Key up events
|
|
1237
|
+
eventManager.addKeyUp("w", () => {
|
|
1238
|
+
console.log("W key released");
|
|
1239
|
+
});
|
|
1206
1240
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1241
|
+
// Check if key is currently pressed
|
|
1242
|
+
if (eventManager.isKeyPressed("w")) {
|
|
1243
|
+
// W key is currently held down
|
|
1244
|
+
}
|
|
1211
1245
|
|
|
1212
|
-
//
|
|
1213
|
-
|
|
1246
|
+
// Remove key events
|
|
1247
|
+
eventManager.removeKeyDown("w", callbackFunction);
|
|
1248
|
+
eventManager.removeKeyUp("w", callbackFunction);
|
|
1214
1249
|
```
|
|
1215
1250
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
A thin, **optional** multiplayer layer over [Colyseus](https://colyseus.io).
|
|
1219
|
-
`colyseus.js` is a peer dependency imported dynamically, so games that don't use
|
|
1220
|
-
networking never load it. It bundles an `Interpolator` for smooth remote entities.
|
|
1251
|
+
### Mouse Events
|
|
1221
1252
|
|
|
1222
1253
|
```javascript
|
|
1223
|
-
|
|
1254
|
+
// Get mouse position
|
|
1255
|
+
const mousePos = eventManager.getMousePosition();
|
|
1256
|
+
console.log(mousePos.x, mousePos.y);
|
|
1224
1257
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1258
|
+
// Check if camera was moved
|
|
1259
|
+
if (eventManager.wasCameraMoved()) {
|
|
1260
|
+
// Camera was moved by dragging
|
|
1261
|
+
eventManager.resetCameraMoved();
|
|
1262
|
+
}
|
|
1263
|
+
```
|
|
1228
1264
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1265
|
+
### Object Events
|
|
1266
|
+
|
|
1267
|
+
```javascript
|
|
1268
|
+
// Click events
|
|
1269
|
+
eventManager.addClickEvent(gameObject, (event, object) => {
|
|
1270
|
+
console.log("Object clicked!");
|
|
1232
1271
|
});
|
|
1233
|
-
net.onLeave((code) => showDisconnected(code));
|
|
1234
1272
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1273
|
+
// Hover events
|
|
1274
|
+
eventManager.addHoverEvent(
|
|
1275
|
+
gameObject,
|
|
1276
|
+
(event) => {
|
|
1277
|
+
console.log("Mouse entered object");
|
|
1278
|
+
},
|
|
1279
|
+
(event) => {
|
|
1280
|
+
console.log("Mouse left object");
|
|
1281
|
+
}
|
|
1282
|
+
);
|
|
1238
1283
|
|
|
1239
|
-
//
|
|
1240
|
-
|
|
1284
|
+
// Remove events
|
|
1285
|
+
eventManager.removeClickEvent(gameObject, callbackFunction);
|
|
1286
|
+
eventManager.removeHoverEvent(gameObject, enterCallback, leaveCallback);
|
|
1241
1287
|
```
|
|
1242
1288
|
|
|
1243
|
-
|
|
1244
|
-
use it with any transport or in tests:
|
|
1289
|
+
### Event Cleanup
|
|
1245
1290
|
|
|
1246
1291
|
```javascript
|
|
1247
|
-
|
|
1292
|
+
// Clean up all events when done
|
|
1293
|
+
eventManager.clean();
|
|
1248
1294
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
const smoothed = interp.sample(entityId, nowSeconds); // each frame
|
|
1252
|
-
interp.prune(nowSeconds); // bound memory for long-lived entities
|
|
1253
|
-
interp.remove(entityId); // when an entity leaves
|
|
1254
|
-
interp.clear();
|
|
1295
|
+
// Change scene
|
|
1296
|
+
eventManager.changeScene(newScene);
|
|
1255
1297
|
```
|
|
1256
1298
|
|
|
1257
|
-
|
|
1299
|
+
### InputManager (actions)
|
|
1258
1300
|
|
|
1259
|
-
|
|
1260
|
-
usage, accepts custom metric rows, and can visualize physics colliders.
|
|
1301
|
+
For gameplay, `InputManager` lets you bind named actions once and read them everywhere: keyboard keys, mouse buttons, gamepad buttons and analog stick directions are all just tokens:
|
|
1261
1302
|
|
|
1262
1303
|
```javascript
|
|
1263
|
-
import {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
//
|
|
1270
|
-
|
|
1271
|
-
debug.destroy();
|
|
1304
|
+
import { InputManager } from "emeraldengine";
|
|
1305
|
+
const input = new InputManager();
|
|
1306
|
+
input.mapAction("jump", ["Space", " ", "pad:0:south"]); // keyboard + gamepad
|
|
1307
|
+
input.mapAction("left", ["a", "ArrowLeft", "pad:0:dpadLeft"]);
|
|
1308
|
+
// in the loop:
|
|
1309
|
+
if (input.justPressed("jump")) player.jump();
|
|
1310
|
+
const move = input.getAxis("left", "right"); // -1 / 0 / +1
|
|
1311
|
+
input.update(); // call once per frame (edge detection + gamepad polling)
|
|
1272
1312
|
```
|
|
1273
1313
|
|
|
1274
|
-
|
|
1314
|
+
### Gamepads & Controllers
|
|
1275
1315
|
|
|
1276
|
-
`
|
|
1277
|
-
(`{ v, t, data }`) with an automatic `.bak` mirror, so saves survive both
|
|
1278
|
-
corrupted writes (a torn write recovers from backup) and **schema changes**
|
|
1279
|
-
(old saves migrate forward instead of being discarded):
|
|
1316
|
+
Full controller support is built into `InputManager`: analog sticks/triggers, semantic button names that resolve through each pad's mapping, rumble, connect/disconnect events, and a registry for non-standard controllers. Gamepad input flows through the same `isDown`/`justPressed`/`getAxis` machinery as the keyboard, so a token like `"pad:0:south"` works anywhere a key token does.
|
|
1280
1317
|
|
|
1281
|
-
|
|
1282
|
-
import { Storage } from "emeraldengine";
|
|
1318
|
+
`pad:<i>:<name>` targets pad index `<i>`. Names resolve through the pad's mapping, so `south` is always the bottom face button whether the pad reports Xbox or PlayStation ordering:
|
|
1283
1319
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1320
|
+
| Tokens | Buttons |
|
|
1321
|
+
| -------------------------------------------------------------------------------------- | --------------------------------------- |
|
|
1322
|
+
| `south`/`a`/`cross`, `east`/`b`/`circle`, `west`/`x`/`square`, `north`/`y`/`triangle` | face buttons |
|
|
1323
|
+
| `l1`/`lb`, `r1`/`rb`, `l2`/`lt`, `r2`/`rt` | shoulders / triggers |
|
|
1324
|
+
| `select`/`back`/`view`/`share`, `start`/`menu`/`options`, `guide`/`home` | center |
|
|
1325
|
+
| `l3`/`leftStick`, `r3`/`rightStick` | stick clicks |
|
|
1326
|
+
| `dpadUp`/`up`, `dpadDown`/`down`, `dpadLeft`/`left`, `dpadRight`/`right` | d-pad |
|
|
1327
|
+
| `pad:<i>:<n>` | raw button index (mapping-independent) |
|
|
1328
|
+
| `pad:<i>:axis<n>+` / `axis<n>-` | analog axis past the deadzone |
|
|
1329
|
+
| `pad:<i>:leftStickUp/Down/Left/Right`, `rightStick...` | analog stick as a d-pad |
|
|
1286
1330
|
|
|
1287
|
-
|
|
1288
|
-
const profile = Storage.load("profile", {
|
|
1289
|
-
version: 2,
|
|
1290
|
-
fallback: { level: 1, coins: 0 },
|
|
1291
|
-
migrate: (old, fromVersion) => {
|
|
1292
|
-
// v1 saves had no coins field — upgrade them instead of losing progress
|
|
1293
|
-
return { ...old, coins: old.coins ?? 0 };
|
|
1294
|
-
},
|
|
1295
|
-
// rewrite: true (default) re-saves migrated data in the new format
|
|
1296
|
-
});
|
|
1331
|
+
`getGamepadStick` applies a radial deadzone (on the stick's distance from center, not per axis), rescaled so there's no jump at the threshold, with an optional response curve:
|
|
1297
1332
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1333
|
+
```javascript
|
|
1334
|
+
input.setGamepadDeadzone(0.25);
|
|
1335
|
+
input.setGamepadCurve(2); // finer control near center (great for camera sticks)
|
|
1336
|
+
const { x, y, magnitude, angle } = input.getGamepadStick("left", 0, {
|
|
1337
|
+
invertY: true,
|
|
1338
|
+
});
|
|
1300
1339
|
```
|
|
1301
1340
|
|
|
1302
|
-
|
|
1303
|
-
existing game is safe. (For whole-scene snapshots, see
|
|
1304
|
-
[Serializer](#serializer-saveload-scenes); for raw key/value access,
|
|
1305
|
-
`Storage.saveToLocalStorage` / `readFromLocalStorage` still exist.)
|
|
1341
|
+
Sticks, triggers and rumble:
|
|
1306
1342
|
|
|
1307
|
-
|
|
1343
|
+
```javascript
|
|
1344
|
+
const { x, y } = input.getGamepadStick("left"); // deadzoned -1..1
|
|
1345
|
+
const aim = input.getGamepadStick("right");
|
|
1346
|
+
const t = input.getGamepadTrigger("right"); // 0..1
|
|
1347
|
+
input.getGamepadButton("south").pressed; // also .value, .index
|
|
1348
|
+
|
|
1349
|
+
input.rumble(0, { duration: 120, strong: 0.6, weak: 0.4 }); // where supported
|
|
1350
|
+
```
|
|
1308
1351
|
|
|
1309
|
-
|
|
1310
|
-
settings and high scores, not for a big persistent world (every tile's state,
|
|
1311
|
-
chests, NPC relationships...). `EmeraldDB` is the async, big-world companion:
|
|
1312
|
-
same versioned envelope, `.bak` backup, and migration semantics, backed by
|
|
1313
|
-
IndexedDB — effectively unlimited, and values are **structured-cloned** (no
|
|
1314
|
-
JSON round-trip), so Maps, Sets, Dates, and typed arrays save as-is and large
|
|
1315
|
-
saves stay fast.
|
|
1352
|
+
Connection events and diagnostics:
|
|
1316
1353
|
|
|
1317
1354
|
```javascript
|
|
1318
|
-
|
|
1355
|
+
input.onGamepadConnected((info) =>
|
|
1356
|
+
console.log(info.id, info.mapping, info.buttonCount, info.axesCount)
|
|
1357
|
+
);
|
|
1358
|
+
input.onGamepadDisconnected((info) => pauseFor(info.index));
|
|
1319
1359
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
fallback: makeNewWorld(),
|
|
1325
|
-
migrate: (old, fromVersion) => upgradeWorld(old, fromVersion),
|
|
1326
|
-
});
|
|
1327
|
-
await EmeraldDB.hasSave("world"); // true (checks the .bak too)
|
|
1328
|
-
await EmeraldDB.removeSave("world"); // deletes save + backup
|
|
1360
|
+
input.isGamepadConnected(0);
|
|
1361
|
+
input.getGamepadInfo(0); // { id, mapping, buttonCount, axesCount, standard }
|
|
1362
|
+
input.getPressedButtons(0); // raw indices currently pressed (layout discovery)
|
|
1363
|
+
```
|
|
1329
1364
|
|
|
1330
|
-
|
|
1331
|
-
await EmeraldDB.set("settings", { volume: 0.8, keybinds: new Map() });
|
|
1332
|
-
const settings = await EmeraldDB.get("settings", {});
|
|
1333
|
-
await EmeraldDB.keys(); // every key in the store
|
|
1365
|
+
Most pads (and anything via XInput / Steam Input) report `mapping === "standard"` and work out of the box. Common DirectInput pads (Logitech Dual Action, generic Twin-USB PS2 adapters, 8BitDo in D-input mode) are recognized out of the box too. For anything else, register a mapping once; it only applies to pads whose id matches and that aren't already standard:
|
|
1334
1366
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1367
|
+
```javascript
|
|
1368
|
+
InputManager.registerGamepadMapping("my-controller-id", {
|
|
1369
|
+
buttons: { south: 1, east: 2, west: 0, north: 3, start: 9 },
|
|
1370
|
+
});
|
|
1339
1371
|
```
|
|
1340
1372
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
the world.
|
|
1373
|
+
D-pads reported as a hat axis (instead of buttons 12–15) are decoded into the `dpad*` tokens automatically.
|
|
1374
|
+
|
|
1375
|
+
## AudioManager
|
|
1345
1376
|
|
|
1346
|
-
|
|
1377
|
+
Emerald includes a comprehensive audio management system.
|
|
1347
1378
|
|
|
1348
|
-
|
|
1349
|
-
screen. The world renders at the design size and is fitted per mode; input
|
|
1350
|
-
helpers convert back, so gameplay code never sees the difference:
|
|
1379
|
+
### Adding Audio
|
|
1351
1380
|
|
|
1352
1381
|
```javascript
|
|
1353
|
-
|
|
1354
|
-
emerald.setDesignResolution(960, 540, "fit");
|
|
1382
|
+
import { AudioManager } from "emeraldengine";
|
|
1355
1383
|
|
|
1356
|
-
|
|
1357
|
-
// "fit" letterbox — whole design visible, bars if aspect differs
|
|
1358
|
-
// "fill" cover — fills the screen, crops the overflow
|
|
1359
|
-
// "stretch" distorts to fill exactly (no bars, no crop)
|
|
1360
|
-
// "pixel" integer scaling — crisp for pixel art
|
|
1361
|
-
emerald.clearDesignResolution(); // back to 1:1 CSS pixels
|
|
1384
|
+
const audioManager = new AudioManager();
|
|
1362
1385
|
|
|
1363
|
-
//
|
|
1364
|
-
|
|
1365
|
-
|
|
1386
|
+
// Add audio files
|
|
1387
|
+
audioManager.add("path/to/sound.wav", "soundName", { volume: 0.8, loop: false });
|
|
1388
|
+
audioManager.add("path/to/music.mp3", "backgroundMusic", { bus: "music", loop: true });
|
|
1366
1389
|
```
|
|
1367
1390
|
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
`run()` pauses the loop when the tab is hidden (stops audio-desync, timer
|
|
1371
|
-
pileups, and giant delta-time spikes on return). Hooks let you pause music or
|
|
1372
|
-
show an overlay; you can also pause manually:
|
|
1391
|
+
### Playing Audio
|
|
1373
1392
|
|
|
1374
1393
|
```javascript
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
pauseOnWindowBlur: false, // stricter: also pause when the window loses focus
|
|
1378
|
-
onPause: () => audio.setMasterVolume(0),
|
|
1379
|
-
onResume: () => audio.setMasterVolume(1),
|
|
1380
|
-
});
|
|
1394
|
+
// Play audio
|
|
1395
|
+
audioManager.play("soundName"); // restarts from 0
|
|
1381
1396
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1397
|
+
// Play overlapping copies, for rapid SFX
|
|
1398
|
+
audioManager.playOverlap("soundName");
|
|
1399
|
+
|
|
1400
|
+
// Play exclusively (stops all other audio first)
|
|
1401
|
+
audioManager.playExclusive("soundName");
|
|
1384
1402
|
```
|
|
1385
1403
|
|
|
1386
|
-
|
|
1387
|
-
physics never explodes after a long background stint.
|
|
1404
|
+
### Audio Control
|
|
1388
1405
|
|
|
1389
|
-
|
|
1406
|
+
```javascript
|
|
1407
|
+
// Stop specific audio
|
|
1408
|
+
audioManager.stop("soundName");
|
|
1390
1409
|
|
|
1391
|
-
|
|
1410
|
+
// Stop all audio
|
|
1411
|
+
audioManager.stopAll();
|
|
1392
1412
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
reference-counted and freed when their last user disposes:
|
|
1413
|
+
// Remove audio
|
|
1414
|
+
audioManager.remove("soundName");
|
|
1396
1415
|
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
gameObject.destroy(); // same, plus physics bodies + Behaviour.onDestroy
|
|
1400
|
-
scene.dispose(); // tear down an entire level/screen
|
|
1401
|
-
drawable.dispose(); // lowest level, safe to call twice
|
|
1416
|
+
// Get audio object
|
|
1417
|
+
const sound = audioManager.getSound("soundName");
|
|
1402
1418
|
```
|
|
1403
1419
|
|
|
1404
|
-
###
|
|
1420
|
+
### Buses & Fades
|
|
1405
1421
|
|
|
1406
|
-
|
|
1407
|
-
automatically: rendering pauses on loss, and on restore the engine recompiles
|
|
1408
|
-
shaders, re-uploads every cached texture from the surviving image cache,
|
|
1409
|
-
rebuilds all drawable buffers, custom `Material`s, post effects, and render
|
|
1410
|
-
targets, then resumes. Optional hooks:
|
|
1422
|
+
Every sound belongs to a named mix bus. `"music"` and `"sfx"` exist by default (new sounds land on `"sfx"`), and any name you use creates a bus on the fly. Effective volume is `master × bus × sound × fade`, so one slider mutes all music without touching the SFX:
|
|
1411
1423
|
|
|
1412
1424
|
```javascript
|
|
1413
|
-
|
|
1414
|
-
|
|
1425
|
+
audioManager.setMasterVolume(0.5);
|
|
1426
|
+
audioManager.setBusVolume("music", 0.5); // the settings-menu "music volume" slider
|
|
1427
|
+
audioManager.setBusVolume("sfx", 0.8);
|
|
1428
|
+
audioManager.setSoundBus("thunder", "ambience"); // move a sound, creating the bus
|
|
1429
|
+
audioManager.getBusVolume("music"); // 0.5
|
|
1415
1430
|
```
|
|
1416
1431
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
Every draw site reports into per-frame counters, shown automatically by
|
|
1420
|
-
`DebugOverlay` (`draws` / `quads` / `binds`) and readable in code:
|
|
1432
|
+
Fades run on the manager's clock (self-driven via rAF by default; pass `{ autoTick: false }` and call `audioManager.update(dt)` yourself to tie them to the game loop):
|
|
1421
1433
|
|
|
1422
1434
|
```javascript
|
|
1423
|
-
|
|
1435
|
+
audioManager.fadeIn("theme", 1.5); // play from silence to full over 1.5s
|
|
1436
|
+
audioManager.fadeOut("theme", 2.0); // fade to silence, then stop
|
|
1437
|
+
audioManager.fadeTo("theme", 0.2, 0.5); // duck under dialogue
|
|
1438
|
+
audioManager.crossfade("theme", "boss", 2.0); // level -> boss music, one call
|
|
1424
1439
|
```
|
|
1425
1440
|
|
|
1426
|
-
|
|
1427
|
-
`Tilemap`, `SpriteBatch`, or the level loader's instanced tile path.
|
|
1441
|
+
### Positional Audio
|
|
1428
1442
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
Spritesheet frame UVs are inset half a texel everywhere, so frames never bleed
|
|
1432
|
-
into neighboring cells (the classic "white seams between tiles" artifact). For
|
|
1433
|
-
pixel-art games also snap the camera to whole pixels:
|
|
1443
|
+
`AudioManager` can also attenuate and pan sounds based on a listener position, using the Web Audio `StereoPanner` where available and falling back to volume-only panning otherwise.
|
|
1434
1444
|
|
|
1435
1445
|
```javascript
|
|
1436
|
-
|
|
1446
|
+
audioManager.setListener(player.x, player.y); // usually the camera/player each frame
|
|
1447
|
+
audioManager.setSpatialRange(100, 800); // full volume <100px, silent >800px
|
|
1448
|
+
|
|
1449
|
+
audioManager.playSpatial("explosion", { x: 1200, y: 50 }); // one-shot, positioned
|
|
1450
|
+
|
|
1451
|
+
// Pure helper (also used internally), handy for custom routing/tests:
|
|
1452
|
+
const { volume, pan, distance } = audioManager.computeSpatial({ x, y });
|
|
1437
1453
|
```
|
|
1438
1454
|
|
|
1439
|
-
##
|
|
1455
|
+
## Camera
|
|
1440
1456
|
|
|
1441
|
-
|
|
1457
|
+
The engine has simple controls for the camera. The camera is stored in the emerald variable.
|
|
1442
1458
|
|
|
1443
1459
|
```javascript
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
import { SceneManager } from "emeraldengine";
|
|
1460
|
+
// Set camera position
|
|
1461
|
+
emerald.camera.setPosition(x, y, z);
|
|
1462
|
+
emerald.camera.setZoom(1.5);
|
|
1448
1463
|
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1464
|
+
// Access camera transform directly
|
|
1465
|
+
emerald.camera.transform.position.x = 100;
|
|
1466
|
+
emerald.camera.transform.position.y = 200;
|
|
1467
|
+
emerald.camera.transform.scale.x = 1.5;
|
|
1468
|
+
emerald.camera.transform.scale.y = 1.5;
|
|
1452
1469
|
```
|
|
1453
1470
|
|
|
1454
|
-
|
|
1471
|
+
Multiple cameras are supported, each with a normalized viewport (origin bottom-left), useful for split-screen:
|
|
1455
1472
|
|
|
1456
1473
|
```javascript
|
|
1457
|
-
|
|
1458
|
-
```
|
|
1459
|
-
|
|
1460
|
-
### Drawing the scene
|
|
1474
|
+
import { Camera, CameraController } from "emeraldengine";
|
|
1461
1475
|
|
|
1462
|
-
|
|
1476
|
+
const top = new Camera({ viewport: { x: 0, y: 0.5, width: 1, height: 0.5 } });
|
|
1477
|
+
const bottom = new Camera({ viewport: { x: 0, y: 0, width: 1, height: 0.5 } });
|
|
1478
|
+
emerald.setCameras([top, bottom]); // or emerald.addCamera(cam) / removeCamera(cam)
|
|
1463
1479
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
const animate = (currentTime) => {
|
|
1467
|
-
const deltaTime = (currentTime - lastTime) / 1000;
|
|
1468
|
-
lastTime = currentTime;
|
|
1469
|
-
emerald.drawScene(scene, deltaTime); // You need this line to tell the engine what to draw
|
|
1470
|
-
window.requestAnimationFrame(animate);
|
|
1471
|
-
};
|
|
1472
|
-
animate(0);
|
|
1480
|
+
top.clearColor = new Color(10, 14, 20); // optional per-viewport clear
|
|
1481
|
+
top.setIgnoreLayers([11, 20]); // skip these object layers in this camera
|
|
1473
1482
|
```
|
|
1474
1483
|
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
Emerald has multiple scenes support. In order to render any object it has to be added to the scene using the `add` method.
|
|
1478
|
-
|
|
1479
|
-
### Adding and removing items from the scene
|
|
1484
|
+
`CameraController` adds smooth follow, bounds, a deadzone, and shake:
|
|
1480
1485
|
|
|
1481
1486
|
```javascript
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
//
|
|
1486
|
-
|
|
1487
|
+
const cam = new CameraController(emerald.camera);
|
|
1488
|
+
cam
|
|
1489
|
+
.follow(player.gameObject, 0.12)
|
|
1490
|
+
.setDeadzone(90, 60) // half-extents in world units; camera only scrolls once the target leaves this box; 0/0 or null disables
|
|
1491
|
+
.setBounds(-1000, -1000, 1000, 1000);
|
|
1492
|
+
cam.shake(10, 0.3);
|
|
1493
|
+
// each frame: cam.update(dt);
|
|
1487
1494
|
```
|
|
1488
1495
|
|
|
1489
|
-
|
|
1496
|
+
## FPSCounter
|
|
1497
|
+
|
|
1498
|
+
Emerald has a built-in FPS counter.
|
|
1490
1499
|
|
|
1491
1500
|
```javascript
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
// Activate Scene
|
|
1495
|
-
scene.setIsActive(true);
|
|
1501
|
+
import { FPSCounter } from "emeraldengine";
|
|
1502
|
+
let fpsCounter = new FPSCounter();
|
|
1496
1503
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1504
|
+
const animate = (currentTime) => {
|
|
1505
|
+
emerald.drawScene(scene, deltaTime);
|
|
1506
|
+
fpsCounter.update(); // Call this in your animation loop
|
|
1507
|
+
window.requestAnimationFrame(animate);
|
|
1508
|
+
};
|
|
1509
|
+
animate();
|
|
1499
1510
|
```
|
|
1500
1511
|
|
|
1501
|
-
##
|
|
1502
|
-
|
|
1503
|
-
### Creating a new GameObject
|
|
1512
|
+
## Time Management
|
|
1504
1513
|
|
|
1505
1514
|
```javascript
|
|
1506
|
-
import {
|
|
1507
|
-
import { Vector3, Vector2 } from "emeraldengine";
|
|
1508
|
-
/*
|
|
1509
|
-
ARGUMENTS:
|
|
1510
|
-
1. name: string = Name of the new GameObject
|
|
1511
|
-
2. position: Vector3 = Position of the new GameObject
|
|
1512
|
-
3. rotation: number = Rotation of the new GameObject
|
|
1513
|
-
4. scale: Vector2 = Scale of the new GameObject
|
|
1514
|
-
*/
|
|
1515
|
-
const gameObject = new GameObject(name, position, rotation, scale);
|
|
1516
|
-
```
|
|
1515
|
+
import { Time } from "emeraldengine";
|
|
1517
1516
|
|
|
1518
|
-
|
|
1517
|
+
// Get delta time
|
|
1518
|
+
const deltaTime = Time.deltaTime; // or Time.getDeltaTime()
|
|
1519
|
+
Time.getUnscaledDeltaTime(); // raw delta, ignores timeScale
|
|
1520
|
+
Time.getElapsedTime(); // total accumulated time
|
|
1519
1521
|
|
|
1520
|
-
|
|
1522
|
+
// Time is automatically updated when you call emerald.drawScene()
|
|
1523
|
+
// You can also manually set it
|
|
1524
|
+
Time.setDeltaTime(deltaTime);
|
|
1521
1525
|
|
|
1522
|
-
|
|
1526
|
+
// Slow down / speed up / pause the whole game:
|
|
1527
|
+
Time.setTimeScale(0.5); // 0 = paused, 1 = normal, 2 = double speed
|
|
1528
|
+
Time.getTimeScale();
|
|
1529
|
+
```
|
|
1523
1530
|
|
|
1524
|
-
|
|
1531
|
+
## Tween, Timer & StateMachine
|
|
1525
1532
|
|
|
1526
1533
|
```javascript
|
|
1527
|
-
import {
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
const texture = new Texture(
|
|
1541
|
-
texturePath,
|
|
1542
|
-
frameWidth,
|
|
1543
|
-
frameHeight,
|
|
1544
|
-
framesPerRow,
|
|
1545
|
-
totalFrames,
|
|
1546
|
-
animationSpeed,
|
|
1547
|
-
autoPlay,
|
|
1548
|
-
(pixelart = true),
|
|
1549
|
-
(useLighting = true)
|
|
1550
|
-
);
|
|
1534
|
+
import { Tween, Easing } from "emeraldengine";
|
|
1535
|
+
Tween.to(sprite.transform.position, { x: 200, y: -50 }, 0.6, {
|
|
1536
|
+
easing: Easing.outBack, // linear, inOutQuad, outCubic, outBounce, outElastic, ...
|
|
1537
|
+
delay: 0,
|
|
1538
|
+
loop: false,
|
|
1539
|
+
yoyo: false,
|
|
1540
|
+
onUpdate: (t) => {},
|
|
1541
|
+
onComplete: () => {},
|
|
1542
|
+
}).then(() => console.log("done"));
|
|
1543
|
+
Tween.killOf(target);
|
|
1544
|
+
Tween.killAll();
|
|
1545
|
+
// driven automatically by drawScene
|
|
1546
|
+
```
|
|
1551
1547
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1548
|
+
```javascript
|
|
1549
|
+
import { Timer } from "emeraldengine";
|
|
1550
|
+
Timer.after(2, () => spawnEnemy()); // once
|
|
1551
|
+
const h = Timer.every(0.5, () => tick(), 10); // 10 times (omit count = forever)
|
|
1552
|
+
Timer.clear(h);
|
|
1553
|
+
Timer.clearAll();
|
|
1554
1554
|
```
|
|
1555
1555
|
|
|
1556
|
-
|
|
1556
|
+
```javascript
|
|
1557
|
+
import { StateMachine } from "emeraldengine";
|
|
1558
|
+
const fsm = new StateMachine();
|
|
1559
|
+
fsm.add("idle", {
|
|
1560
|
+
update: (dt, sm) => {
|
|
1561
|
+
if (seen) sm.set("chase");
|
|
1562
|
+
},
|
|
1563
|
+
});
|
|
1564
|
+
fsm.add("chase", { enter: () => roar(), update: (dt) => move(dt) });
|
|
1565
|
+
fsm.set("idle");
|
|
1566
|
+
// in update(dt): fsm.update(dt); -> fsm.is("chase")
|
|
1567
|
+
```
|
|
1557
1568
|
|
|
1558
|
-
|
|
1569
|
+
## SpatialGrid & Pool
|
|
1559
1570
|
|
|
1560
|
-
|
|
1571
|
+
```javascript
|
|
1572
|
+
import { SpatialGrid } from "emeraldengine";
|
|
1573
|
+
const grid = new SpatialGrid(64);
|
|
1574
|
+
grid.clear();
|
|
1575
|
+
for (const e of enemies)
|
|
1576
|
+
grid.insert(e, e.transform.position.x, e.transform.position.y);
|
|
1577
|
+
const near = grid.queryRadius(px, py, 100); // or grid.queryRect(...)
|
|
1578
|
+
```
|
|
1561
1579
|
|
|
1562
1580
|
```javascript
|
|
1563
|
-
import {
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
3. frameWidth: number = The width of each frame.
|
|
1569
|
-
4. frameHeight: number = The height of each frame.
|
|
1570
|
-
5. framesPerRow: number = How many frames are in one row in your spritesheet.
|
|
1571
|
-
6. totalFrames: number = How many total frames does your spritesheet have.
|
|
1572
|
-
7. animationSpeed: number = Speed of change of every frame.
|
|
1573
|
-
8. autoPlay: boolean = Specify if you want the animation to play automatically. If you don't want any animation then pass false for it.
|
|
1574
|
-
9. pixelart: boolean = Specify whether the texture should be rendered in pixel art style. (THIS IS OPTIONAL. If you don't specify it then it will be defaulted to true)
|
|
1575
|
-
10. useLighting: boolean = Specify whether the texture should react to lighting or not. If you don't want any lighting then pass false for it. (THIS IS OPTIONAL. If you don't specify it then it will be defaulted to true)
|
|
1576
|
-
*/
|
|
1577
|
-
const instancedTexture = new InstancedTexture(
|
|
1578
|
-
texturePath,
|
|
1579
|
-
instanceCount,
|
|
1580
|
-
frameWidth,
|
|
1581
|
-
frameHeight,
|
|
1582
|
-
framesPerRow,
|
|
1583
|
-
totalFrames,
|
|
1584
|
-
animationSpeed,
|
|
1585
|
-
autoPlay,
|
|
1586
|
-
(pixelart = true),
|
|
1587
|
-
(useLighting = true)
|
|
1581
|
+
import { Pool } from "emeraldengine";
|
|
1582
|
+
const bullets = new Pool(
|
|
1583
|
+
() => new Bullet(),
|
|
1584
|
+
(b, x, y) => b.spawn(x, y),
|
|
1585
|
+
50
|
|
1588
1586
|
);
|
|
1589
|
-
|
|
1590
|
-
//
|
|
1591
|
-
gameObject.addComponent(instancedTexture);
|
|
1587
|
+
const b = bullets.acquire(px, py);
|
|
1588
|
+
bullets.release(b); // bullets.releaseAll()
|
|
1592
1589
|
```
|
|
1593
1590
|
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
#### Square2D
|
|
1591
|
+
## MathUtils
|
|
1597
1592
|
|
|
1598
1593
|
```javascript
|
|
1599
|
-
import {
|
|
1600
|
-
|
|
1601
|
-
|
|
1594
|
+
import { MathUtils } from "emeraldengine";
|
|
1595
|
+
MathUtils.clamp(v, 0, 1);
|
|
1596
|
+
MathUtils.lerp(a, b, t);
|
|
1597
|
+
MathUtils.map(v, 0, 10, 0, 100);
|
|
1598
|
+
MathUtils.degToRad(90);
|
|
1599
|
+
MathUtils.randomRange(0, 5);
|
|
1600
|
+
MathUtils.randomInt(1, 6);
|
|
1601
|
+
MathUtils.distance(a, b);
|
|
1602
|
+
MathUtils.normalize(v);
|
|
1603
|
+
MathUtils.angleBetween(a, b);
|
|
1602
1604
|
```
|
|
1603
1605
|
|
|
1604
|
-
|
|
1606
|
+
## Coroutines
|
|
1605
1607
|
|
|
1606
|
-
|
|
1608
|
+
Generator-based sequencing layered on the same per-frame delta the rest of the engine uses. It's driven automatically from `Emerald.drawScene`, so coroutines honor pause/slow-mo via `Time.timeScale`.
|
|
1607
1609
|
|
|
1608
1610
|
```javascript
|
|
1609
|
-
import {
|
|
1610
|
-
let triangle = new Triangle2D();
|
|
1611
|
-
gameObject.addComponent(triangle);
|
|
1612
|
-
```
|
|
1611
|
+
import { Coroutine } from "emeraldengine";
|
|
1613
1612
|
|
|
1614
|
-
|
|
1613
|
+
const handle = Coroutine.start(function* () {
|
|
1614
|
+
big.setText("3");
|
|
1615
|
+
audio.beep();
|
|
1616
|
+
yield 0.7; // wait 0.7 seconds
|
|
1617
|
+
big.setText("2");
|
|
1618
|
+
audio.beep();
|
|
1619
|
+
yield 0.7;
|
|
1620
|
+
yield Coroutine.waitFrames(3); // wait 3 frames
|
|
1621
|
+
yield Coroutine.waitUntil(() => player.ready); // block until predicate is truthy
|
|
1622
|
+
yield Coroutine.waitWhile(() => paused); // block while predicate is truthy
|
|
1623
|
+
yield fetch("/level.json"); // await any promise
|
|
1624
|
+
yield Coroutine.tween(0, 1, 0.5, (v) => (door.openAmount = v)); // drive a value
|
|
1625
|
+
yield otherCoroutineHandle; // wait for a nested coroutine
|
|
1626
|
+
start();
|
|
1627
|
+
});
|
|
1615
1628
|
|
|
1616
|
-
|
|
1629
|
+
handle.cancel(); // stop it early
|
|
1630
|
+
handle.isRunning(); // boolean
|
|
1631
|
+
await handle.promise; // resolves when the coroutine finishes or is cancelled
|
|
1617
1632
|
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
/*
|
|
1621
|
-
ARGUMENTS:
|
|
1622
|
-
1. segments = number of segments that the circle will have. Default is 32.
|
|
1623
|
-
*/
|
|
1624
|
-
let circle = new Circle2D(segments);
|
|
1625
|
-
gameObject.addComponent(circle);
|
|
1633
|
+
Coroutine.count(); // number of running coroutines
|
|
1634
|
+
Coroutine.clearAll(); // cancel + remove every coroutine (e.g. on scene exit)
|
|
1626
1635
|
```
|
|
1627
1636
|
|
|
1628
|
-
|
|
1637
|
+
## Rendering Pipeline
|
|
1629
1638
|
|
|
1630
|
-
|
|
1639
|
+
### Post-processing
|
|
1631
1640
|
|
|
1632
|
-
|
|
1641
|
+
When post-processing is enabled, Emerald renders the whole scene into an offscreen texture and then runs a chain of full-screen shader passes before drawing the final image to the canvas. You manage it entirely through the `Emerald` instance:
|
|
1633
1642
|
|
|
1634
1643
|
```javascript
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
// Create physics engine first
|
|
1638
|
-
const physics = new Physics(-70, 32, 2); // gravity, scale, velocityThreshold
|
|
1644
|
+
emerald.enablePostProcessing(); // allocate the scene render target + processor
|
|
1645
|
+
emerald.disablePostProcessing(); // turn it back off
|
|
1639
1646
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
1. physics: Physics = Instance of the Physics class that you created at the top of your code.
|
|
1643
|
-
2. type: string = Type of the rigid body. It can be "dynamic", "kinematic", or "static".
|
|
1644
|
-
3. position: Vector2 = Position of the rigid body is Vector2 because it doesn't need any Z index.
|
|
1645
|
-
4. fixedRotation: boolean = Specify whether the rigid body should have a fixed rotation or not. Default is false.
|
|
1646
|
-
5. parentObject: GameObject = (OPTIONAL) If you want to attach the rigid body to a GameObject you can pass it here. If you don't want to attach it to any GameObject then pass null.
|
|
1647
|
-
6. offset: Vector2 = (OPTIONAL) Offset from the GameObject's position.
|
|
1648
|
-
*/
|
|
1649
|
-
const rigidBody = new RigidBody(
|
|
1650
|
-
physics,
|
|
1651
|
-
"dynamic",
|
|
1652
|
-
new Vector2(0, 0),
|
|
1653
|
-
false,
|
|
1654
|
-
gameObject,
|
|
1655
|
-
new Vector2(0, 0)
|
|
1656
|
-
);
|
|
1647
|
+
const bloom = emerald.addPostEffect(PostEffects.bloom()); // returns the effect
|
|
1648
|
+
emerald.removePostEffect(bloom);
|
|
1657
1649
|
|
|
1658
|
-
|
|
1650
|
+
// Effects run in the order they were added. Toggle one without removing it:
|
|
1651
|
+
bloom.enabled = false;
|
|
1659
1652
|
```
|
|
1660
1653
|
|
|
1661
|
-
|
|
1654
|
+
`drawScene` automatically routes through the processor while any enabled effect exists; if none do, it draws straight to the screen with zero overhead.
|
|
1655
|
+
|
|
1656
|
+
Write a custom pass by constructing a `PostEffect`. Your fragment shader gets `vUV` (0–1 screen UV), `uScene` (the previous pass), `uResolution`, and `uTime` for free; declare any extra uniforms and set them in `setUniforms`:
|
|
1662
1657
|
|
|
1663
1658
|
```javascript
|
|
1664
|
-
import {
|
|
1659
|
+
import { PostEffect } from "emeraldengine";
|
|
1665
1660
|
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
rigidBody,
|
|
1678
|
-
new Vector2(1, 1),
|
|
1679
|
-
1,
|
|
1680
|
-
0.3,
|
|
1681
|
-
0.1,
|
|
1682
|
-
false,
|
|
1683
|
-
gameObject
|
|
1661
|
+
const tint = new PostEffect(
|
|
1662
|
+
"tint",
|
|
1663
|
+
`
|
|
1664
|
+
uniform vec3 uTint;
|
|
1665
|
+
void main() {
|
|
1666
|
+
gl_FragColor = texture2D(uScene, vUV) * vec4(uTint, 1.0);
|
|
1667
|
+
}`,
|
|
1668
|
+
{
|
|
1669
|
+
setUniforms: (gl, loc) => gl.uniform3f(loc("uTint"), 1.0, 0.85, 0.7),
|
|
1670
|
+
enabled: true,
|
|
1671
|
+
}
|
|
1684
1672
|
);
|
|
1685
|
-
|
|
1686
|
-
gameObject.addComponent(boxCollider);
|
|
1673
|
+
emerald.addPostEffect(tint);
|
|
1687
1674
|
```
|
|
1688
1675
|
|
|
1689
|
-
|
|
1676
|
+
Keep UI crisp by rendering it on a camera excluded from post-processing: it draws straight to the screen after the effect chain, so bloom never blows out your buttons and text:
|
|
1690
1677
|
|
|
1691
|
-
|
|
1678
|
+
```javascript
|
|
1679
|
+
const uiCam = new Camera({ excludeFromPost: true }); // or uiCam.setExcludeFromPost(true)
|
|
1680
|
+
emerald.addCamera(uiCam);
|
|
1681
|
+
hudObject.setLayer(100); // and restrict cameras via setOnlyLayers/ignoreLayers
|
|
1682
|
+
```
|
|
1692
1683
|
|
|
1693
|
-
|
|
1684
|
+
### PostEffects (built-in)
|
|
1685
|
+
|
|
1686
|
+
Factory functions on the `PostEffects` namespace return a ready `PostEffect`:
|
|
1694
1687
|
|
|
1695
1688
|
```javascript
|
|
1696
|
-
import {
|
|
1689
|
+
import { PostEffects } from "emeraldengine";
|
|
1697
1690
|
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
2. radius: number = Radius of the circle collider.
|
|
1702
|
-
3. density: number = Density of the collider.
|
|
1703
|
-
4. friction: number = Friction of the collider.
|
|
1704
|
-
5. restitution: number = Restitution (bounciness) of the collider.
|
|
1705
|
-
6. isSensor: boolean = Whether this collider is a sensor.
|
|
1706
|
-
7. parentObject: GameObject = (OPTIONAL) Parent GameObject.
|
|
1707
|
-
*/
|
|
1708
|
-
const circleCollider = new CircleCollider(
|
|
1709
|
-
rigidBody,
|
|
1710
|
-
1.5,
|
|
1711
|
-
1,
|
|
1712
|
-
0.3,
|
|
1713
|
-
0.8,
|
|
1714
|
-
false,
|
|
1715
|
-
gameObject
|
|
1691
|
+
emerald.enablePostProcessing();
|
|
1692
|
+
emerald.addPostEffect(
|
|
1693
|
+
PostEffects.bloom({ threshold: 0.6, intensity: 1.2, spread: 1.1 })
|
|
1716
1694
|
);
|
|
1717
|
-
|
|
1718
|
-
|
|
1695
|
+
emerald.addPostEffect(
|
|
1696
|
+
PostEffects.vignette({ intensity: 0.5, radius: 0.75, softness: 0.45 })
|
|
1697
|
+
);
|
|
1698
|
+
emerald.addPostEffect(
|
|
1699
|
+
PostEffects.colorGrade({ brightness: 0.02, contrast: 1.08, saturation: 1.15 })
|
|
1700
|
+
);
|
|
1701
|
+
emerald.addPostEffect(PostEffects.chromaticAberration({ amount: 0.003 }));
|
|
1702
|
+
emerald.addPostEffect(PostEffects.scanlines({ intensity: 0.15, count: 480 }));
|
|
1703
|
+
emerald.addPostEffect(
|
|
1704
|
+
PostEffects.crt({ curvature: 4.0, scanlineIntensity: 0.2, vignette: 0.3 })
|
|
1705
|
+
);
|
|
1706
|
+
emerald.addPostEffect(PostEffects.grayscale());
|
|
1719
1707
|
```
|
|
1720
1708
|
|
|
1721
|
-
|
|
1709
|
+
| Effect | Options (defaults) |
|
|
1710
|
+
| --------------------- | ------------------------------------------------------------ |
|
|
1711
|
+
| `bloom` | `threshold 0.7`, `intensity 1.0`, `spread 1.0` (multi-pass) |
|
|
1712
|
+
| `vignette` | `intensity 0.5`, `radius 0.75`, `softness 0.45` |
|
|
1713
|
+
| `colorGrade` | `brightness 0`, `contrast 1`, `saturation 1` |
|
|
1714
|
+
| `chromaticAberration` | `amount 0.003` |
|
|
1715
|
+
| `scanlines` | `intensity 0.15`, `count 480` |
|
|
1716
|
+
| `crt` | `curvature 4.0`, `scanlineIntensity 0.2`, `vignette 0.3` |
|
|
1717
|
+
| `grayscale` | none |
|
|
1722
1718
|
|
|
1723
|
-
|
|
1719
|
+
`bloom` is exported as a class too (`BloomEffect`) if you want to subclass it.
|
|
1724
1720
|
|
|
1725
|
-
|
|
1721
|
+
### RenderTarget
|
|
1726
1722
|
|
|
1727
|
-
|
|
1723
|
+
An offscreen framebuffer backed by a color texture (and an optional depth buffer). Used internally by the post-processor, but useful on its own for minimaps, mirrors, or picture-in-picture.
|
|
1728
1724
|
|
|
1729
1725
|
```javascript
|
|
1730
|
-
|
|
1731
|
-
gameObject.transform.position.x = 100;
|
|
1732
|
-
gameObject.transform.position.y = 200;
|
|
1733
|
-
gameObject.transform.position.z = 0;
|
|
1726
|
+
import { RenderTarget } from "emeraldengine";
|
|
1734
1727
|
|
|
1735
|
-
|
|
1736
|
-
|
|
1728
|
+
const rt = new RenderTarget(512, 512, { depth: false, pixelart: false });
|
|
1729
|
+
rt.bind(); // binds the FBO and sets the viewport to its size
|
|
1730
|
+
// ...draw...
|
|
1731
|
+
rt.unbind(); // restore the canvas framebuffer
|
|
1732
|
+
// rt.texture now holds the rendered image (a WebGLTexture)
|
|
1733
|
+
rt.resize(1024, 1024); // reallocates only if the size changed
|
|
1734
|
+
rt.dispose(); // free GL resources
|
|
1737
1735
|
```
|
|
1738
1736
|
|
|
1739
|
-
|
|
1737
|
+
### Material (custom shaders)
|
|
1740
1738
|
|
|
1741
|
-
|
|
1739
|
+
A `Material` replaces a Drawable's fragment shader while reusing the engine's standard vertex shader, so transforms, the camera, and instancing keep working. Your fragment program automatically has `vTexCoord`, `vFragPos`, `vInstanceColor`, `uSampler`, `uColor`, `uOpacity`, and `uTime`. Don't redeclare them; declare any extra uniforms and push values with `set(name, value)`.
|
|
1742
1740
|
|
|
1743
1741
|
```javascript
|
|
1744
|
-
|
|
1745
|
-
gameObject.transform.rotation = Math.PI / 4; // 45 degrees
|
|
1746
|
-
```
|
|
1747
|
-
|
|
1748
|
-

|
|
1742
|
+
import { Material, Square2D } from "emeraldengine";
|
|
1749
1743
|
|
|
1750
|
-
|
|
1744
|
+
const dissolve = new Material(
|
|
1745
|
+
`
|
|
1746
|
+
uniform float uAmount;
|
|
1747
|
+
void main() {
|
|
1748
|
+
vec4 c = texture2D(uSampler, vTexCoord);
|
|
1749
|
+
if (c.a < uAmount) discard;
|
|
1750
|
+
gl_FragColor = c * uColor * uOpacity;
|
|
1751
|
+
}
|
|
1752
|
+
`,
|
|
1753
|
+
{ uniforms: { uAmount: 0.0 } }
|
|
1754
|
+
);
|
|
1751
1755
|
|
|
1752
|
-
|
|
1753
|
-
//
|
|
1754
|
-
gameObject.
|
|
1755
|
-
gameObject.transform.scale.y = 2;
|
|
1756
|
+
const shape = new Square2D();
|
|
1757
|
+
shape.setMaterial(dissolve); // any Drawable: Texture, Square2D, Circle2D…
|
|
1758
|
+
gameObject.addComponent(shape);
|
|
1756
1759
|
|
|
1757
|
-
//
|
|
1758
|
-
|
|
1760
|
+
// Animate a uniform (numbers, vec2/3/4 arrays, or functions are accepted):
|
|
1761
|
+
dissolve.set("uAmount", 0.5);
|
|
1762
|
+
dissolve.set("uPulse", () => 0.5 + 0.5 * Math.sin(performance.now() / 300));
|
|
1759
1763
|
```
|
|
1760
1764
|
|
|
1761
|
-
|
|
1765
|
+
### SpriteBatch
|
|
1762
1766
|
|
|
1763
|
-
|
|
1767
|
+
A dynamic batched renderer with its own minimal shader. Instead of one draw call per sprite, it accumulates sprites that share a texture into a single interleaved buffer and submits them in one `drawElements` call, ideal for many same-atlas quads (bullets, tiles, text glyphs).
|
|
1764
1768
|
|
|
1765
1769
|
```javascript
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1770
|
+
import { SpriteBatch } from "emeraldengine";
|
|
1771
|
+
|
|
1772
|
+
const batch = new SpriteBatch({ maxQuads: 2000 });
|
|
1773
|
+
batch.begin(projectionMatrix, viewMatrix); // gl-matrix mat4 / Float32Array(16)
|
|
1774
|
+
for (const e of entities) {
|
|
1775
|
+
batch.draw({
|
|
1776
|
+
texture: atlasTexture, // a WebGLTexture; changing it flushes the batch
|
|
1777
|
+
x: e.x,
|
|
1778
|
+
y: e.y,
|
|
1779
|
+
w: 32,
|
|
1780
|
+
h: 32,
|
|
1781
|
+
rotation: e.angle,
|
|
1782
|
+
originX: 0.5,
|
|
1783
|
+
originY: 0.5,
|
|
1784
|
+
u0: e.u0,
|
|
1785
|
+
v0: e.v0,
|
|
1786
|
+
u1: e.u1,
|
|
1787
|
+
v1: e.v1, // UV sub-rect (defaults 0..1)
|
|
1788
|
+
r: 1,
|
|
1789
|
+
g: 1,
|
|
1790
|
+
b: 1,
|
|
1791
|
+
a: 1, // per-vertex tint
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
batch.end(); // flushes remaining sprites
|
|
1795
|
+
console.log(batch.drawCalls); // GL draw calls emitted this frame
|
|
1769
1796
|
```
|
|
1770
1797
|
|
|
1771
|
-
|
|
1798
|
+
Also worth knowing about: `obj.setLayer(10)` (layers sort before z), `hudObj.setScreenSpace(true)` (ignore the camera, position in pixels from viewport center), `drawable.setBlendMode("additive" | "normal" | "multiply")`, off-screen culling (`emerald.setCullingEnabled(true)`, `particles.alwaysVisible = true` to opt out), `TextureAtlas.load`/`applyTo` for atlas sub-rects, and `TextureManager.preload([...])` for shared/cached GL textures.
|
|
1772
1799
|
|
|
1773
|
-
|
|
1800
|
+
## In-Engine UI
|
|
1774
1801
|
|
|
1775
|
-
|
|
1776
|
-
// For animated textures
|
|
1777
|
-
const texture = gameObject.getComponent(Texture);
|
|
1778
|
-
texture.setFrame(2); // Set to frame 2
|
|
1779
|
-
```
|
|
1802
|
+
`UI` is a retained-mode toolkit drawn entirely by the engine, with no DOM/HTML overlay. Elements are screen-space objects on a dedicated high layer with their own pointer and keyboard hit-testing. Positions are pixels from the viewport center (y up), either a literal `{x, y}` or a responsive `(viewW, viewH) => ({x, y})` function; call `relayout()` after a resize.
|
|
1780
1803
|
|
|
1781
|
-
|
|
1804
|
+
Pair it with a UI camera so the UI draws over the game and the game cameras skip the UI layer:
|
|
1782
1805
|
|
|
1783
1806
|
```javascript
|
|
1784
|
-
|
|
1785
|
-
texture.playAnimation([0, 1, 2, 3], 200); // frames array, speed in ms
|
|
1786
|
-
|
|
1787
|
-
// Stop animation
|
|
1788
|
-
texture.stopAnimation();
|
|
1807
|
+
import { UI } from "emeraldengine";
|
|
1789
1808
|
|
|
1790
|
-
//
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
}
|
|
1794
|
-
```
|
|
1809
|
+
const uiCam = UI.createCamera(); // a full-screen camera that renders ONLY UI.LAYER
|
|
1810
|
+
gameCamera.ignoreLayer(UI.LAYER); // keep the UI out of the game viewport(s)
|
|
1811
|
+
emerald.setCameras([gameCamera, uiCam]); // add the UI camera last
|
|
1795
1812
|
|
|
1796
|
-
|
|
1813
|
+
const ui = new UI(scene, canvas, { accent: [120, 200, 255] });
|
|
1797
1814
|
|
|
1798
|
-
|
|
1815
|
+
// Labels (return a handle with setText)
|
|
1816
|
+
const score = ui.label(() => ({ x: 0, y: 200 }), "Score: 0", {
|
|
1817
|
+
font: "700 30px system-ui, sans-serif",
|
|
1818
|
+
color: "#eaf2ff",
|
|
1819
|
+
});
|
|
1820
|
+
score.setText("Score: 120");
|
|
1799
1821
|
|
|
1800
|
-
|
|
1822
|
+
// Buttons (panel + centered label, hover highlight, click handler)
|
|
1823
|
+
ui.button(() => ({ x: 0, y: 0 }), "START", 240, 56, {
|
|
1824
|
+
accent: [120, 220, 160],
|
|
1825
|
+
onClick: () => startGame(),
|
|
1826
|
+
});
|
|
1801
1827
|
|
|
1802
|
-
|
|
1828
|
+
// Panels and a modal dimmer behind a dialog
|
|
1829
|
+
ui.dim(0.6); // full-screen backdrop
|
|
1830
|
+
ui.panel(() => ({ x: 0, y: 0 }), 480, 320, { opacity: 0.94 });
|
|
1803
1831
|
|
|
1804
|
-
|
|
1805
|
-
|
|
1832
|
+
// Editable single-line text field
|
|
1833
|
+
const name = ui.textField(() => ({ x: 0, y: -80 }), 280, 44, {
|
|
1834
|
+
placeholder: "Your name",
|
|
1835
|
+
maxLength: 16,
|
|
1836
|
+
onChange: (v) => console.log(v),
|
|
1837
|
+
});
|
|
1838
|
+
name.getValue();
|
|
1839
|
+
name.setValue("P1");
|
|
1806
1840
|
|
|
1807
|
-
//
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
new Vector3(x, y, z),
|
|
1811
|
-
new Vector2(width, height),
|
|
1812
|
-
rotation,
|
|
1813
|
-
frame
|
|
1814
|
-
);
|
|
1841
|
+
ui.relayout(); // after creating/anchoring or on window resize
|
|
1842
|
+
ui.isOver(clientX, clientY); // true if an interactive element is under the pointer
|
|
1843
|
+
ui.destroy(); // remove all UI objects + detach listeners
|
|
1815
1844
|
|
|
1816
|
-
//
|
|
1817
|
-
const instancedTexture = gameObject.getComponent(InstancedTexture);
|
|
1818
|
-
instancedTexture.addInstance(instance);
|
|
1845
|
+
UI.LAYER; // 100000, the default UI render layer
|
|
1819
1846
|
```
|
|
1820
1847
|
|
|
1821
|
-
|
|
1848
|
+
## ScreenEffects (transitions)
|
|
1849
|
+
|
|
1850
|
+
Full-screen camera transitions drawn with the engine's own screen-space quads (no CSS overlay), so they survive resolution changes, post-processing and split-screen. `fadeOut`/`fadeIn`/`flash` return promises. Call `update(dt)` each frame before `drawScene`.
|
|
1822
1851
|
|
|
1823
1852
|
```javascript
|
|
1824
|
-
|
|
1825
|
-
instancedTexture.removeInstance(instanceId);
|
|
1853
|
+
import { ScreenEffects, Color } from "emeraldengine";
|
|
1826
1854
|
|
|
1827
|
-
|
|
1828
|
-
const instance = instancedTexture.getInstanceWithId(instanceId);
|
|
1855
|
+
const fx = new ScreenEffects(scene, { layer: 100000, size: 5000 });
|
|
1829
1856
|
|
|
1830
|
-
//
|
|
1831
|
-
|
|
1857
|
+
await fx.fadeOut(0.4, new Color(0, 0, 0, 255)); // fade to black
|
|
1858
|
+
loadNextLevel();
|
|
1859
|
+
await fx.fadeIn(0.4); // fade back in
|
|
1832
1860
|
|
|
1833
|
-
//
|
|
1834
|
-
|
|
1861
|
+
fx.flash(new Color(255, 255, 255, 255), 0.25); // quick screen flash
|
|
1862
|
+
fx.setLetterbox(80); // animate cinematic bars to 80px; pass 0 to retract
|
|
1863
|
+
|
|
1864
|
+
// in the loop:
|
|
1865
|
+
fx.update(dt);
|
|
1866
|
+
// when leaving the scene:
|
|
1867
|
+
fx.destroy();
|
|
1835
1868
|
```
|
|
1836
1869
|
|
|
1837
|
-
|
|
1870
|
+
## Scene Transitions & the Game Loop
|
|
1871
|
+
|
|
1872
|
+
`emerald.run(update, options)` (see [Getting Started](#getting-started)) computes a clamped delta time, optionally advances a fixed-timestep simulation, and calls your `update(dt, alpha)` each frame. `alpha` is the 0..1 interpolation factor between fixed steps (1 when no fixed step is configured).
|
|
1873
|
+
|
|
1874
|
+
Switch scenes behind a fade with `SceneManager.transitionTo` (wired to `ScreenEffects`), or drive the fade directly with `ScreenEffects.transition`:
|
|
1838
1875
|
|
|
1839
1876
|
```javascript
|
|
1840
|
-
|
|
1841
|
-
instancedTexture.addInstanceClickEvent(instanceId, (event) => {
|
|
1842
|
-
console.log("Instance clicked!");
|
|
1843
|
-
});
|
|
1877
|
+
import { SceneManager, ScreenEffects, Color } from "emeraldengine";
|
|
1844
1878
|
|
|
1845
|
-
//
|
|
1846
|
-
instancedTexture.addInstanceHoverEvent(
|
|
1847
|
-
instanceId,
|
|
1848
|
-
(event) => console.log("Mouse entered"),
|
|
1849
|
-
(event) => console.log("Mouse left")
|
|
1850
|
-
);
|
|
1851
|
-
```
|
|
1852
|
-
|
|
1853
|
-
## Physics Engine
|
|
1854
|
-
|
|
1855
|
-
Emerald includes a comprehensive physics engine built on top of Planck.js.
|
|
1856
|
-
|
|
1857
|
-
### Setting up Physics
|
|
1858
|
-
|
|
1859
|
-
```javascript
|
|
1860
|
-
import { Physics } from "emeraldengine";
|
|
1861
|
-
/*
|
|
1862
|
-
ARGUMENTS:
|
|
1863
|
-
1. gravity: number = Gravity force (negative for downward)
|
|
1864
|
-
2. scale: number = Scale factor for physics units to pixels
|
|
1865
|
-
3. velocityThreshold: number = Minimum velocity threshold
|
|
1866
|
-
*/
|
|
1867
|
-
const physics = new Physics(-70, 32, 2);
|
|
1868
|
-
```
|
|
1869
|
-
|
|
1870
|
-
### Physics Bodies
|
|
1871
|
-
|
|
1872
|
-
```javascript
|
|
1873
|
-
// Get the physics body from a RigidBody component
|
|
1874
|
-
const body = rigidBody.getBody();
|
|
1875
|
-
|
|
1876
|
-
// Set velocity
|
|
1877
|
-
body.setLinearVelocity(new Vector2(10, 0));
|
|
1878
|
-
|
|
1879
|
-
// Get velocity
|
|
1880
|
-
const velocity = body.getLinearVelocity();
|
|
1881
|
-
|
|
1882
|
-
// Get position
|
|
1883
|
-
const position = body.getPosition();
|
|
1884
|
-
```
|
|
1885
|
-
|
|
1886
|
-
### Collision Detection
|
|
1887
|
-
|
|
1888
|
-
```javascript
|
|
1889
|
-
// Handle collision enter
|
|
1890
|
-
physics.onCollisionEnter((bodyA, bodyB, contact) => {
|
|
1891
|
-
console.log("Collision started!");
|
|
1892
|
-
|
|
1893
|
-
// Get collision normal
|
|
1894
|
-
const normal = contact.getWorldManifold().normal;
|
|
1895
|
-
//normal.y = -1 when player is on the ground
|
|
1896
|
-
//normal.y = 1 when player hits the ceiling
|
|
1897
|
-
//normal.x = -1 when player hits the left wall
|
|
1898
|
-
//normal.x = 1 when player hits the right wall
|
|
1899
|
-
|
|
1900
|
-
// Check if bodies are sensors
|
|
1901
|
-
const fixtureA = contact.getFixtureA();
|
|
1902
|
-
const fixtureB = contact.getFixtureB();
|
|
1903
|
-
if (fixtureA.isSensor() || fixtureB.isSensor()) {
|
|
1904
|
-
// Handle sensor collision
|
|
1905
|
-
}
|
|
1906
|
-
});
|
|
1907
|
-
|
|
1908
|
-
// Handle collision exit
|
|
1909
|
-
physics.onCollisionExit((bodyA, bodyB, contact) => {
|
|
1910
|
-
console.log("Collision ended!");
|
|
1911
|
-
});
|
|
1912
|
-
```
|
|
1913
|
-
|
|
1914
|
-
### Collision Events
|
|
1915
|
-
|
|
1916
|
-
```javascript
|
|
1917
|
-
// Process physics in your update loop
|
|
1918
|
-
const animate = (currentTime) => {
|
|
1919
|
-
physics.process(deltaTime);
|
|
1920
|
-
};
|
|
1921
|
-
```
|
|
1922
|
-
|
|
1923
|
-
## Particle System
|
|
1924
|
-
|
|
1925
|
-
Emerald includes a powerful particle system for creating visual effects.
|
|
1926
|
-
|
|
1927
|
-

|
|
1928
|
-
|
|
1929
|
-
### Particle Settings
|
|
1930
|
-
|
|
1931
|
-
```javascript
|
|
1932
|
-
import { ParticleSettings } from "emeraldengine";
|
|
1879
|
+
const fx = new ScreenEffects(overlayScene); // update()'d each frame by your loop
|
|
1933
1880
|
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
direction: new Vector2(0, 1), // upward
|
|
1940
|
-
spread: Math.PI * 2,
|
|
1941
|
-
emissionRate: Infinity, // one-shot emission
|
|
1942
|
-
frame: 0,
|
|
1943
|
-
offset: 5,
|
|
1944
|
-
rotation: 0,
|
|
1945
|
-
scale: new Vector2(5, 5),
|
|
1946
|
-
animation: { frames: [0, 1, 2], speed: 200 },
|
|
1881
|
+
await SceneManager.transitionTo(nextScene, {
|
|
1882
|
+
screenEffects: fx,
|
|
1883
|
+
duration: 0.4,
|
|
1884
|
+
color: new Color(0, 0, 0, 255),
|
|
1885
|
+
onSwap: (scene) => buildLevel(scene), // runs while the screen is covered
|
|
1947
1886
|
});
|
|
1948
|
-
```
|
|
1949
|
-
|
|
1950
|
-
### Creating Particle Systems
|
|
1951
1887
|
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
/*
|
|
1956
|
-
ARGUMENTS:
|
|
1957
|
-
1. name: string = Name of the particle system
|
|
1958
|
-
2. texturePath: string = Path to the texture
|
|
1959
|
-
3. frameWidth: number = Width of each frame
|
|
1960
|
-
4. frameHeight: number = Height of each frame
|
|
1961
|
-
5. framesPerRow: number = Frames per row in spritesheet
|
|
1962
|
-
6. totalFrames: number = Total frames in spritesheet
|
|
1963
|
-
7. duration: number = Duration of the effect
|
|
1964
|
-
8. settings: ParticleSettings = Particle settings object
|
|
1965
|
-
*/
|
|
1966
|
-
const particles = new Particles(
|
|
1967
|
-
"explosion",
|
|
1968
|
-
texturePath,
|
|
1969
|
-
16,
|
|
1970
|
-
16,
|
|
1971
|
-
9,
|
|
1972
|
-
27,
|
|
1973
|
-
1.2,
|
|
1974
|
-
particleSettings
|
|
1975
|
-
);
|
|
1976
|
-
|
|
1977
|
-
// Add to scene
|
|
1978
|
-
scene.add(particles.gameObject);
|
|
1979
|
-
```
|
|
1980
|
-
|
|
1981
|
-
### Particle System Methods
|
|
1982
|
-
|
|
1983
|
-
```javascript
|
|
1984
|
-
// Play particle effect at position
|
|
1985
|
-
particles.play(new Vector3(x, y, z));
|
|
1986
|
-
|
|
1987
|
-
// Stop particle system
|
|
1988
|
-
particles.stop();
|
|
1989
|
-
|
|
1990
|
-
// Reset particle system
|
|
1991
|
-
particles.reset();
|
|
1992
|
-
|
|
1993
|
-
// Update particles (call in your animation loop)
|
|
1994
|
-
particles.update(deltaTime);
|
|
1995
|
-
|
|
1996
|
-
// Check if active
|
|
1997
|
-
if (particles.active) {
|
|
1998
|
-
// Particles are currently active
|
|
1999
|
-
}
|
|
2000
|
-
```
|
|
2001
|
-
|
|
2002
|
-
## Lighting System
|
|
2003
|
-
|
|
2004
|
-
Emerald supports ambient, point, and directional lighting.
|
|
2005
|
-
|
|
2006
|
-
### Ambient Light
|
|
2007
|
-
|
|
2008
|
-
```javascript
|
|
2009
|
-
// Set ambient light
|
|
2010
|
-
emerald.setAmbientLight(new Vector3(0.3, 0.3, 0.3)); // RGB values 0-1
|
|
1888
|
+
// Or lower-level: fade out -> swap -> fade in
|
|
1889
|
+
await fx.transition(() => swapScenes(), { duration: 0.4 });
|
|
2011
1890
|
```
|
|
2012
1891
|
|
|
2013
|
-
|
|
1892
|
+
## DebugOverlay
|
|
2014
1893
|
|
|
2015
1894
|
```javascript
|
|
2016
|
-
import {
|
|
2017
|
-
|
|
2018
|
-
/*
|
|
2019
|
-
ARGUMENTS:
|
|
2020
|
-
1. position: Vector2 = Position of the light
|
|
2021
|
-
2. color: Color = Color of the light
|
|
2022
|
-
3. intensity: number = Light intensity
|
|
2023
|
-
4. radius: number = Light radius
|
|
2024
|
-
*/
|
|
2025
|
-
const pointLight = new PointLight(
|
|
2026
|
-
new Vector2(100, 0),
|
|
2027
|
-
new Color(255, 204, 153),
|
|
2028
|
-
1.5,
|
|
2029
|
-
400
|
|
2030
|
-
);
|
|
2031
|
-
|
|
2032
|
-
// Add to engine
|
|
2033
|
-
emerald.addPointLight(pointLight);
|
|
1895
|
+
import { DebugOverlay } from "emeraldengine";
|
|
2034
1896
|
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
1897
|
+
const debug = new DebugOverlay();
|
|
1898
|
+
debug.setVisible(true); // toggle (e.g. bind to F3)
|
|
1899
|
+
debug.setMetric("enemies", enemies.length); // add/refresh a custom row
|
|
1900
|
+
debug.showColliders(scene, true); // overlay collider shapes for the scene
|
|
1901
|
+
// after drawScene each frame:
|
|
1902
|
+
debug.update(emerald, scene); // FPS / frame-time graph / objects / cameras
|
|
1903
|
+
debug.destroy();
|
|
2038
1904
|
```
|
|
2039
1905
|
|
|
2040
|
-
|
|
1906
|
+
It shows a frame-time sparkline with min/avg/max milliseconds and heap usage, and its `draws`/`quads`/`binds` numbers come from the same render stats you can read yourself:
|
|
2041
1907
|
|
|
2042
1908
|
```javascript
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
/*
|
|
2046
|
-
ARGUMENTS:
|
|
2047
|
-
1. position: Vector2 = Position of the light
|
|
2048
|
-
2. direction: Vector2 = Direction vector
|
|
2049
|
-
3. color: Color = Color of the light
|
|
2050
|
-
4. intensity: number = Light intensity
|
|
2051
|
-
5. width: number = Width of the light beam
|
|
2052
|
-
*/
|
|
2053
|
-
const directionalLight = new DirectionalLight(
|
|
2054
|
-
new Vector2(0, 300),
|
|
2055
|
-
new Vector2(0, -1), // pointing down
|
|
2056
|
-
new Color(255, 255, 255),
|
|
2057
|
-
3.0,
|
|
2058
|
-
200
|
|
2059
|
-
);
|
|
2060
|
-
|
|
2061
|
-
// Add to engine
|
|
2062
|
-
emerald.addDirectionalLight(directionalLight);
|
|
2063
|
-
|
|
2064
|
-
// Rotate direction
|
|
2065
|
-
const angle = 0.1;
|
|
2066
|
-
const newX =
|
|
2067
|
-
directionalLight.direction.x * Math.cos(angle) -
|
|
2068
|
-
directionalLight.direction.y * Math.sin(angle);
|
|
2069
|
-
const newY =
|
|
2070
|
-
directionalLight.direction.x * Math.sin(angle) +
|
|
2071
|
-
directionalLight.direction.y * Math.cos(angle);
|
|
2072
|
-
directionalLight.direction.x = newX;
|
|
2073
|
-
directionalLight.direction.y = newY;
|
|
1909
|
+
const { drawCalls, quads, textureBinds } = emerald.getRenderStats();
|
|
2074
1910
|
```
|
|
2075
1911
|
|
|
2076
|
-
|
|
1912
|
+
Draw calls growing with level size means something isn't batched. Use `Tilemap`, `SpriteBatch`, or the level loader's instanced tile path.
|
|
2077
1913
|
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
Emerald supports bitmap font rendering using the BitmapText component. This allows you to display text with custom fonts and styles.
|
|
2081
|
-
|
|
2082
|
-

|
|
1914
|
+
## Serializer (save/load scenes)
|
|
2083
1915
|
|
|
2084
1916
|
```javascript
|
|
2085
|
-
import {
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
3. letters: string = String containing all available characters
|
|
2092
|
-
4. letterSpacing: number = Spacing between letters
|
|
2093
|
-
5. frameWidth: number = Width of each character frame
|
|
2094
|
-
6. frameHeight: number = Height of each character frame
|
|
2095
|
-
7. framesPerRow: number = Characters per row in font texture
|
|
2096
|
-
8. totalFrames: number = Total character frames
|
|
2097
|
-
9. pixelArt: boolean = Whether to use pixel art rendering
|
|
2098
|
-
10. fontSize: number = Font size
|
|
2099
|
-
11. color: Color = Text color
|
|
2100
|
-
12. position: Vector3 = Text position
|
|
2101
|
-
13. rotation: number = Text rotation
|
|
2102
|
-
14. useLighting: boolean = Whether text should react to lighting
|
|
2103
|
-
*/
|
|
2104
|
-
const bitmapText = new BitmapText(
|
|
2105
|
-
"Hello World!",
|
|
2106
|
-
fontTexturePath,
|
|
2107
|
-
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?.",
|
|
2108
|
-
16,
|
|
2109
|
-
32,
|
|
2110
|
-
32,
|
|
2111
|
-
10,
|
|
2112
|
-
95,
|
|
2113
|
-
true,
|
|
2114
|
-
24,
|
|
2115
|
-
new Color(255, 255, 255),
|
|
2116
|
-
new Vector3(0, 200, 0),
|
|
2117
|
-
0,
|
|
2118
|
-
false
|
|
2119
|
-
);
|
|
2120
|
-
|
|
2121
|
-
// Add to scene
|
|
2122
|
-
scene.add(bitmapText.gameObject);
|
|
2123
|
-
|
|
2124
|
-
// Update text
|
|
2125
|
-
bitmapText.setText("New Text!");
|
|
2126
|
-
bitmapText.setColor(new Color(255, 0, 0));
|
|
2127
|
-
bitmapText.setFontSize(32);
|
|
2128
|
-
bitmapText.setLetterSpacing(20);
|
|
1917
|
+
import { Serializer } from "emeraldengine";
|
|
1918
|
+
Serializer.register("coin", (data) => makeCoin(data.value));
|
|
1919
|
+
coin.prefabType = "coin";
|
|
1920
|
+
coin.serialize = () => ({ value: 5 });
|
|
1921
|
+
const json = Serializer.toJSON(scene); // save
|
|
1922
|
+
Serializer.fromJSON(json, new Scene()); // load
|
|
2129
1923
|
```
|
|
2130
1924
|
|
|
2131
|
-
|
|
1925
|
+
## Storage (versioned saves)
|
|
2132
1926
|
|
|
2133
|
-
`
|
|
2134
|
-
It renders at the device pixel ratio, so text is crisp on Retina/HiDPI
|
|
2135
|
-
displays, and it supports multi-line strings with word-wrapping and alignment:
|
|
1927
|
+
`Storage.save`/`Storage.load` wrap your data in a versioned envelope (`{ v, t, data }`) with an automatic `.bak` mirror, so saves survive both corrupted writes (a torn write recovers from backup) and schema changes (old saves migrate forward instead of being discarded):
|
|
2136
1928
|
|
|
2137
1929
|
```javascript
|
|
2138
|
-
import {
|
|
1930
|
+
import { Storage } from "emeraldengine";
|
|
2139
1931
|
|
|
2140
|
-
//
|
|
2141
|
-
|
|
2142
|
-
font: "700 24px 'Pixelify Sans', sans-serif",
|
|
2143
|
-
color: "#8fe0ff",
|
|
2144
|
-
screenSpace: true, // HUD: fixed on screen, position in px from center
|
|
2145
|
-
position: new Vector3(0, 240, 0),
|
|
2146
|
-
});
|
|
2147
|
-
scene.add(label);
|
|
1932
|
+
// Write: version + timestamp envelope, plus a .bak backup by default.
|
|
1933
|
+
Storage.save("profile", { level: 3, coins: 120 }, { version: 2 });
|
|
2148
1934
|
|
|
2149
|
-
//
|
|
2150
|
-
const
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
)
|
|
1935
|
+
// Read: falls back, recovers from backup, and migrates old versions.
|
|
1936
|
+
const profile = Storage.load("profile", {
|
|
1937
|
+
version: 2,
|
|
1938
|
+
fallback: { level: 1, coins: 0 },
|
|
1939
|
+
migrate: (old, fromVersion) => {
|
|
1940
|
+
// v1 saves had no coins field, upgrade them instead of losing progress
|
|
1941
|
+
return { ...old, coins: old.coins ?? 0 };
|
|
1942
|
+
},
|
|
1943
|
+
// rewrite: true (default) re-saves migrated data in the new format
|
|
1944
|
+
});
|
|
2154
1945
|
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
text.setText("Score: 120");
|
|
2158
|
-
text.setColor("#ffd166");
|
|
2159
|
-
text.setMaxWidth(400);
|
|
2160
|
-
text.setAlign("center"); // "left" | "center" | "right"
|
|
1946
|
+
Storage.hasSave("profile"); // true
|
|
1947
|
+
Storage.removeSave("profile"); // deletes the save AND its backup
|
|
2161
1948
|
```
|
|
2162
1949
|
|
|
2163
|
-
|
|
2164
|
-
`CanvasText` for everything else (UI, dialogue, any real font).
|
|
2165
|
-
|
|
2166
|
-
## EventManager
|
|
1950
|
+
Plain pre-versioning values load as version 0, so adopting the envelope on an existing game is safe. For raw key/value access, `Storage.saveToLocalStorage`/`readFromLocalStorage` still exist.
|
|
2167
1951
|
|
|
2168
|
-
|
|
1952
|
+
## EmeraldDB (IndexedDB saves)
|
|
2169
1953
|
|
|
2170
|
-
|
|
1954
|
+
`Storage` lives on localStorage, which caps out around 5MB: plenty for settings and high scores, not for a big persistent world. `EmeraldDB` is the async, big-world companion: same versioned envelope, `.bak` backup, and migration semantics, backed by IndexedDB (effectively unlimited), and values are structured-cloned (no JSON round-trip), so Maps, Sets, Dates, and typed arrays save as-is.
|
|
2171
1955
|
|
|
2172
1956
|
```javascript
|
|
2173
|
-
import {
|
|
1957
|
+
import { EmeraldDB } from "emeraldengine";
|
|
2174
1958
|
|
|
2175
|
-
|
|
1959
|
+
// Versioned world save, mirrors Storage.save/load, but async:
|
|
1960
|
+
await EmeraldDB.save("world", world, { version: 3 });
|
|
1961
|
+
const world = await EmeraldDB.load("world", {
|
|
1962
|
+
version: 3,
|
|
1963
|
+
fallback: makeNewWorld(),
|
|
1964
|
+
migrate: (old, fromVersion) => upgradeWorld(old, fromVersion),
|
|
1965
|
+
});
|
|
1966
|
+
await EmeraldDB.hasSave("world"); // true (checks the .bak too)
|
|
1967
|
+
await EmeraldDB.removeSave("world"); // deletes save + backup
|
|
1968
|
+
|
|
1969
|
+
// Plain async key/value (no envelope):
|
|
1970
|
+
await EmeraldDB.set("settings", { volume: 0.8, keybinds: new Map() });
|
|
1971
|
+
const settings = await EmeraldDB.get("settings", {});
|
|
1972
|
+
await EmeraldDB.keys(); // every key in the store
|
|
1973
|
+
|
|
1974
|
+
// Optional setup:
|
|
1975
|
+
EmeraldDB.configure({ name: "my-game", store: "saves" }); // before first use
|
|
1976
|
+
EmeraldDB.isSupported(); // feature-detect (falls back to Storage if false)
|
|
1977
|
+
await EmeraldDB.importFromStorage("profile"); // one-time upgrade of an old localStorage save
|
|
2176
1978
|
```
|
|
2177
1979
|
|
|
2178
|
-
|
|
1980
|
+
Rule of thumb: `Storage` for small synchronous bits (settings, best times), `EmeraldDB` for the world.
|
|
1981
|
+
|
|
1982
|
+
## AssetManager
|
|
1983
|
+
|
|
1984
|
+
One async loader for everything a game needs at startup: images/textures, audio, JSON, text, and web fonts, with deduplication and aggregate progress for a loading bar. Images are routed through `TextureManager`, so the GL upload cache is shared with the rest of the engine.
|
|
2179
1985
|
|
|
2180
1986
|
```javascript
|
|
2181
|
-
|
|
2182
|
-
eventManager.addKeyDown("w", () => {
|
|
2183
|
-
console.log("W key pressed");
|
|
2184
|
-
});
|
|
1987
|
+
import { AssetManager } from "emeraldengine";
|
|
2185
1988
|
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
1989
|
+
const assets = new AssetManager();
|
|
1990
|
+
assets
|
|
1991
|
+
.image("player", "player.png", { pixelart: true })
|
|
1992
|
+
.audio("jump", "jump.wav")
|
|
1993
|
+
.json("level1", "levels/1.json")
|
|
1994
|
+
.text("credits", "credits.txt")
|
|
1995
|
+
.font("Press Start 2P", "fonts/press-start.woff2");
|
|
2190
1996
|
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
// W key is currently held down
|
|
2194
|
-
}
|
|
1997
|
+
assets.onProgress((loaded, total) => bar.set(loaded / total));
|
|
1998
|
+
await assets.load({ continueOnError: false }); // rejects on a failed asset unless true
|
|
2195
1999
|
|
|
2196
|
-
//
|
|
2197
|
-
|
|
2198
|
-
|
|
2000
|
+
assets.get("player"); // HTMLImageElement
|
|
2001
|
+
assets.get("level1"); // parsed JSON
|
|
2002
|
+
assets.has("jump"); // boolean
|
|
2003
|
+
assets.progress(); // 0..1
|
|
2004
|
+
await assets.getTexture("player"); // { texture, width, height } from the GL cache
|
|
2005
|
+
assets.clear();
|
|
2199
2006
|
```
|
|
2200
2007
|
|
|
2201
|
-
|
|
2008
|
+
## Asset importers (Tiled, Aseprite & Forge)
|
|
2009
|
+
|
|
2010
|
+
Import maps from [Tiled](https://www.mapeditor.org), sprite-sheet animations from [Aseprite](https://www.aseprite.org), and levels from Emerald's own Tile Forge editor. All three are pure parsers/builders: hand them the already-parsed JSON (load it with `AssetManager.json` or `fetch`).
|
|
2202
2011
|
|
|
2203
2012
|
```javascript
|
|
2204
|
-
|
|
2205
|
-
const mousePos = eventManager.getMousePosition();
|
|
2206
|
-
console.log(mousePos.x, mousePos.y);
|
|
2013
|
+
import { TiledMap } from "emeraldengine";
|
|
2207
2014
|
|
|
2208
|
-
//
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
}
|
|
2213
|
-
```
|
|
2015
|
+
// Build a ready-to-render Tilemap from a Tiled JSON map + its tile sheet.
|
|
2016
|
+
const map = TiledMap.toTilemap(mapJson, "tiles.png", { layer: "ground" });
|
|
2017
|
+
scene.add(map.gameObject);
|
|
2018
|
+
map.buildColliders(physics);
|
|
2214
2019
|
|
|
2215
|
-
|
|
2020
|
+
// Or just the frame grid (for your own Tilemap.setMap call):
|
|
2021
|
+
const grid = TiledMap.toFrameGrid(mapJson, { layer: "ground" });
|
|
2022
|
+
|
|
2023
|
+
// Object layers (spawn points, triggers) as plain data; Tiled `properties` are
|
|
2024
|
+
// flattened into `props`, and flipY converts to a y-up world.
|
|
2025
|
+
const spawns = TiledMap.objects(mapJson, { layer: "spawns", flipY: true });
|
|
2026
|
+
// -> [{ name, type, x, y, width, height, gid, props, ... }]
|
|
2027
|
+
```
|
|
2216
2028
|
|
|
2217
2029
|
```javascript
|
|
2218
|
-
|
|
2219
|
-
eventManager.addClickEvent(gameObject, (event, object) => {
|
|
2220
|
-
console.log("Object clicked!");
|
|
2221
|
-
});
|
|
2030
|
+
import { Aseprite, Texture, Animator } from "emeraldengine";
|
|
2222
2031
|
|
|
2223
|
-
//
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2032
|
+
const cfg = Aseprite.spriteConfig(sheetJson); // { frameWidth, frameHeight, framesPerRow, totalFrames }
|
|
2033
|
+
const tex = new Texture(
|
|
2034
|
+
"hero.png",
|
|
2035
|
+
cfg.frameWidth,
|
|
2036
|
+
cfg.frameHeight,
|
|
2037
|
+
cfg.framesPerRow,
|
|
2038
|
+
cfg.totalFrames,
|
|
2039
|
+
0,
|
|
2040
|
+
false
|
|
2232
2041
|
);
|
|
2042
|
+
gameObject.addComponent(tex);
|
|
2233
2043
|
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2044
|
+
const anim = new Animator();
|
|
2045
|
+
gameObject.addComponent(anim);
|
|
2046
|
+
Aseprite.applyTo(anim, sheetJson); // registers a clip per frame-tag
|
|
2047
|
+
anim.play("run");
|
|
2238
2048
|
|
|
2239
|
-
|
|
2049
|
+
// Or inspect the clips yourself (handles forward/reverse/pingpong):
|
|
2050
|
+
Aseprite.toClips(sheetJson); // -> [{ name, frames:[...], speed }]
|
|
2051
|
+
```
|
|
2240
2052
|
|
|
2241
2053
|
```javascript
|
|
2242
|
-
|
|
2243
|
-
eventManager.clean();
|
|
2054
|
+
import { ForgeLevel } from "emeraldengine";
|
|
2244
2055
|
|
|
2245
|
-
|
|
2246
|
-
|
|
2056
|
+
/*
|
|
2057
|
+
ARGUMENTS (options object):
|
|
2058
|
+
1. scene: Scene = Scene to add the layer objects to.
|
|
2059
|
+
2. physics: Physics = (OPTIONAL) Physics engine; omit to skip colliders.
|
|
2060
|
+
3. filter: Object = (OPTIONAL) Collision filter spec for the colliders.
|
|
2061
|
+
4. ownerObject: GameObject = (OPTIONAL) Owner reported by collision events.
|
|
2062
|
+
5. pixelart: boolean = (OPTIONAL) NEAREST filtering for the atlas. Default is true.
|
|
2063
|
+
6. layerOrder: string = (OPTIONAL) "top-first" or "bottom-first": whether layers[0] is the topmost or bottommost layer. Default is "top-first".
|
|
2064
|
+
*/
|
|
2065
|
+
const map = ForgeLevel.load(levelJson, {
|
|
2066
|
+
scene,
|
|
2067
|
+
physics,
|
|
2068
|
+
filter: LAYERS.ground,
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
emerald.setBackgroundColor(Color.fromHex(map.background));
|
|
2072
|
+
// map -> { tileSize, cols, rows, width, height, background, bounds,
|
|
2073
|
+
// layers, colliders, objects, entityTypes, toWorld }
|
|
2247
2074
|
```
|
|
2248
2075
|
|
|
2249
|
-
|
|
2076
|
+
Each tile layer is drawn as one draw call (an `InstancedTexture` per tileset, batched by atlas). Solid tiles become static bodies: full-tile runs are merged into single `BoxCollider`s, and a tile whose collider is a shape other than the full tile (a ramp, a wedge) gets a real `PolygonCollider` built from that shape's own points, not a bounding-box approximation. Object layers (spawns, pickups, triggers) come back as plain data in `map.objects`, already converted from grid cells to world space.
|
|
2250
2077
|
|
|
2251
|
-
|
|
2078
|
+
## Networking (NetworkManager + Interpolator)
|
|
2252
2079
|
|
|
2253
|
-
|
|
2080
|
+
A thin, optional multiplayer layer over [Colyseus](https://colyseus.io). `colyseus.js` is a peer dependency imported dynamically, so games that don't use networking never load it.
|
|
2254
2081
|
|
|
2255
2082
|
```javascript
|
|
2256
|
-
import {
|
|
2083
|
+
import { NetworkManager } from "emeraldengine";
|
|
2257
2084
|
|
|
2258
|
-
const
|
|
2085
|
+
const net = new NetworkManager({ interpolation: { delay: 0.1 } });
|
|
2086
|
+
await net.connect("wss://my-server:2567"); // dynamically imports colyseus.js
|
|
2087
|
+
const room = await net.join("arena", { name: "P1" });
|
|
2259
2088
|
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2089
|
+
net.onMessage("hit", (msg) => applyHit(msg));
|
|
2090
|
+
net.onStateChange((state) => {
|
|
2091
|
+
for (const [id, p] of state.players) net.interpolator.push(id, p, net.now());
|
|
2092
|
+
});
|
|
2093
|
+
net.onLeave((code) => showDisconnected(code));
|
|
2094
|
+
|
|
2095
|
+
net.send("move", { dir: 1 });
|
|
2096
|
+
net.sessionId; // this client's id
|
|
2097
|
+
await net.leave();
|
|
2098
|
+
|
|
2099
|
+
// each frame, render remote entities "in the past" for smoothness:
|
|
2100
|
+
const pos = net.interpolator.sample(remoteId, net.now()); // { x, y } | null
|
|
2263
2101
|
```
|
|
2264
2102
|
|
|
2265
|
-
|
|
2103
|
+
`Interpolator` is also exported standalone and is pure (no network/DOM), so you can use it with any transport or in tests:
|
|
2266
2104
|
|
|
2267
2105
|
```javascript
|
|
2268
|
-
|
|
2269
|
-
audioManager.play("soundName");
|
|
2106
|
+
import { Interpolator } from "emeraldengine";
|
|
2270
2107
|
|
|
2271
|
-
|
|
2272
|
-
|
|
2108
|
+
const interp = new Interpolator({ delay: 0.1, maxBuffer: 60 });
|
|
2109
|
+
interp.push(entityId, { x, y }, serverTimeSeconds); // on each authoritative update
|
|
2110
|
+
const smoothed = interp.sample(entityId, nowSeconds); // each frame
|
|
2111
|
+
interp.prune(nowSeconds); // bound memory for long-lived entities
|
|
2112
|
+
interp.remove(entityId); // when an entity leaves
|
|
2113
|
+
interp.clear();
|
|
2273
2114
|
```
|
|
2274
2115
|
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
```javascript
|
|
2278
|
-
// Stop specific audio
|
|
2279
|
-
audioManager.stop("soundName");
|
|
2116
|
+
## Advanced Features
|
|
2280
2117
|
|
|
2281
|
-
|
|
2282
|
-
audioManager.stopAll();
|
|
2118
|
+
### Resize Handling
|
|
2283
2119
|
|
|
2284
|
-
|
|
2285
|
-
|
|
2120
|
+
```javascript
|
|
2121
|
+
// Handle window resize
|
|
2122
|
+
const handleResize = () => {
|
|
2123
|
+
const { width, height } = getCanvasDimensions();
|
|
2124
|
+
emerald.resize(width, height);
|
|
2125
|
+
};
|
|
2286
2126
|
|
|
2287
|
-
|
|
2288
|
-
const sound = audioManager.getSound("soundName");
|
|
2127
|
+
window.addEventListener("resize", handleResize);
|
|
2289
2128
|
```
|
|
2290
2129
|
|
|
2291
|
-
|
|
2130
|
+
### Resolution independence
|
|
2292
2131
|
|
|
2293
|
-
|
|
2132
|
+
Author your game at one fixed resolution and let the engine scale it to any screen:
|
|
2294
2133
|
|
|
2295
2134
|
```javascript
|
|
2296
|
-
//
|
|
2297
|
-
emerald.
|
|
2135
|
+
// Design at 960x540, letterboxed onto whatever screen the player has:
|
|
2136
|
+
emerald.setDesignResolution(960, 540, "fit");
|
|
2298
2137
|
|
|
2299
|
-
//
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2138
|
+
// Modes:
|
|
2139
|
+
// "fit" letterbox: whole design visible, bars if aspect differs
|
|
2140
|
+
// "fill" cover: fills the screen, crops the overflow
|
|
2141
|
+
// "stretch" distorts to fill exactly (no bars, no crop)
|
|
2142
|
+
// "pixel" integer scaling, crisp for pixel art
|
|
2143
|
+
emerald.clearDesignResolution(); // back to 1:1 CSS pixels
|
|
2144
|
+
|
|
2145
|
+
// Mouse/touch coordinates -> world space (accounts for the design scale,
|
|
2146
|
+
// letterbox offset, camera zoom/position, and DPR):
|
|
2147
|
+
const world = emerald.screenToWorld(input.mouse.x, input.mouse.y);
|
|
2304
2148
|
```
|
|
2305
2149
|
|
|
2306
|
-
|
|
2150
|
+
### Auto-pause & lifecycle
|
|
2307
2151
|
|
|
2308
|
-
|
|
2152
|
+
`run()` pauses the loop when the tab is hidden (stops audio-desync, timer pileups, and giant delta-time spikes on return). Hooks let you pause music or show an overlay; you can also pause manually:
|
|
2309
2153
|
|
|
2310
2154
|
```javascript
|
|
2311
|
-
|
|
2312
|
-
|
|
2155
|
+
emerald.run(update, {
|
|
2156
|
+
pauseOnBlur: true, // default: pause when the tab is hidden
|
|
2157
|
+
pauseOnWindowBlur: false, // stricter: also pause when the window loses focus
|
|
2158
|
+
onPause: () => audio.setMasterVolume(0),
|
|
2159
|
+
onResume: () => audio.setMasterVolume(1),
|
|
2160
|
+
});
|
|
2313
2161
|
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
fpsCounter.update(); // Call this in your animation loop
|
|
2317
|
-
window.requestAnimationFrame(animate);
|
|
2318
|
-
};
|
|
2319
|
-
animate();
|
|
2162
|
+
emerald.pause(); // e.g. from your own pause menu
|
|
2163
|
+
emerald.resume();
|
|
2320
2164
|
```
|
|
2321
2165
|
|
|
2322
|
-
|
|
2166
|
+
The first `dt` after resuming is clamped (`maxDelta`, default 0.25s), so physics never explodes after a long background stint.
|
|
2323
2167
|
|
|
2324
|
-
|
|
2325
|
-
import { SceneManager } from "emeraldengine";
|
|
2168
|
+
### Production hardening
|
|
2326
2169
|
|
|
2327
|
-
|
|
2328
|
-
SceneManager.setScene(scene);
|
|
2170
|
+
Removing an object from a scene keeps its GPU resources alive so it can be re-added. When something is gone for good, dispose it: shared textures are reference-counted and freed when their last user disposes:
|
|
2329
2171
|
|
|
2330
|
-
|
|
2331
|
-
|
|
2172
|
+
```javascript
|
|
2173
|
+
scene.remove(enemy, { dispose: true }); // buffers + texture reference freed
|
|
2174
|
+
gameObject.destroy(); // same, plus physics bodies + Behaviour.onDestroy
|
|
2175
|
+
scene.dispose(); // tear down an entire level/screen
|
|
2176
|
+
drawable.dispose(); // lowest level, safe to call twice
|
|
2332
2177
|
```
|
|
2333
2178
|
|
|
2334
|
-
|
|
2179
|
+
Lost WebGL contexts (mobile tab switches, GPU resets, laptops waking) are survived automatically: rendering pauses on loss, and on restore the engine recompiles shaders, re-uploads every cached texture, rebuilds all drawable buffers, custom `Material`s, post effects, and render targets, then resumes. Optional hooks:
|
|
2335
2180
|
|
|
2336
2181
|
```javascript
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
// Get delta time
|
|
2340
|
-
const deltaTime = Time.deltaTime;
|
|
2341
|
-
|
|
2342
|
-
// Time is automatically updated when you call emerald.drawScene()
|
|
2343
|
-
// You can also manually set it
|
|
2344
|
-
Time.setDeltaTime(deltaTime);
|
|
2182
|
+
emerald.onContextLost(() => overlay.show("Recovering graphics..."));
|
|
2183
|
+
emerald.onContextRestored(() => overlay.hide());
|
|
2345
2184
|
```
|
|
2346
2185
|
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
### Resize Handling
|
|
2186
|
+
Spritesheet frame UVs are inset half a texel everywhere, so frames never bleed into neighboring cells. For pixel-art games also snap the camera to whole pixels:
|
|
2350
2187
|
|
|
2351
2188
|
```javascript
|
|
2352
|
-
//
|
|
2353
|
-
const handleResize = () => {
|
|
2354
|
-
const { width, height } = getCanvasDimensions();
|
|
2355
|
-
emerald.resize(width, height);
|
|
2356
|
-
};
|
|
2357
|
-
|
|
2358
|
-
window.addEventListener("resize", handleResize);
|
|
2189
|
+
emerald.camera.setPixelSnap(true); // rendered position rounds; stored position stays smooth
|
|
2359
2190
|
```
|
|
2191
|
+
|
|
2192
|
+
## NPM scripts
|
|
2193
|
+
|
|
2194
|
+
| Script | Purpose |
|
|
2195
|
+
| ---------------- | --------------------------------------------- |
|
|
2196
|
+
| `npm test` | Node test suite (`node --test test/`) |
|
|
2197
|
+
| `npm run types` | Regenerate `dist/types` from JSDoc via `tsc` |
|
|
2198
|
+
| `npm run format` | Prettier |
|