emeraldengine 2.2.1 → 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 +2198 -968
- package/dist/types/index.d.ts +75 -32
- package/dist/types/src/Animator.d.ts +50 -0
- package/dist/types/{BitmapText.d.ts → src/BitmapText.d.ts} +19 -21
- package/dist/types/src/Camera.d.ts +122 -0
- package/dist/types/src/CameraController.d.ts +107 -0
- package/dist/types/src/CanvasText.d.ts +91 -0
- package/dist/types/src/CollisionLayers.d.ts +58 -0
- package/dist/types/{Color.d.ts → src/Color.d.ts} +6 -6
- package/dist/types/src/Coroutine.d.ts +111 -0
- package/dist/types/src/DebugOverlay.d.ts +86 -0
- package/dist/types/src/Drawable.d.ts +271 -0
- package/dist/types/src/Easing.d.ts +22 -0
- package/dist/types/src/Emerald.d.ts +420 -0
- package/dist/types/src/EmeraldDB.d.ts +159 -0
- package/dist/types/{FPSCounter.d.ts → src/FPSCounter.d.ts} +1 -3
- package/dist/types/src/GLUtils.d.ts +4 -0
- package/dist/types/{Instance.d.ts → src/Instance.d.ts} +37 -15
- package/dist/types/{InstancedTexture.d.ts → src/InstancedTexture.d.ts} +75 -23
- package/dist/types/src/Interpolator.d.ts +66 -0
- package/dist/types/src/Material.d.ts +87 -0
- package/dist/types/src/MathUtils.d.ts +81 -0
- package/dist/types/src/ParticleEmitter.d.ts +131 -0
- package/dist/types/src/Physics.d.ts +337 -0
- package/dist/types/src/Pool.d.ts +53 -0
- package/dist/types/src/PostEffects.d.ts +68 -0
- package/dist/types/src/PostProcessor.d.ts +124 -0
- package/dist/types/src/RenderTarget.d.ts +56 -0
- package/dist/types/src/Scene.d.ts +62 -0
- package/dist/types/src/ScreenEffects.d.ts +111 -0
- package/dist/types/src/Serializer.d.ts +86 -0
- package/dist/types/src/Shaders.d.ts +2 -0
- package/dist/types/{Shapes.d.ts → src/Shapes.d.ts} +4 -6
- package/dist/types/src/SpatialGrid.d.ts +50 -0
- package/dist/types/src/SpriteBatch.d.ts +89 -0
- package/dist/types/src/StateMachine.d.ts +59 -0
- package/dist/types/src/Storage.d.ts +89 -0
- package/dist/types/{Texture.d.ts → src/Texture.d.ts} +3 -4
- package/dist/types/src/TextureAtlas.d.ts +55 -0
- package/dist/types/src/Tilemap.d.ts +91 -0
- package/dist/types/src/Time.d.ts +53 -0
- package/dist/types/src/Timer.d.ts +54 -0
- package/dist/types/src/Transform.d.ts +94 -0
- package/dist/types/src/Tween.d.ts +81 -0
- package/dist/types/src/UI.d.ts +121 -0
- package/dist/types/src/components/Behaviour.d.ts +71 -0
- package/dist/types/{components → src/components}/BoxCollider.d.ts +14 -14
- package/dist/types/src/components/BoxColliderDebug.d.ts +15 -0
- package/dist/types/{components → src/components}/CircleCollider.d.ts +14 -12
- package/dist/types/src/components/CircleColliderDebug.d.ts +15 -0
- package/dist/types/src/components/Collider.d.ts +102 -0
- package/dist/types/src/components/GameObject.d.ts +135 -0
- package/dist/types/src/components/PolygonCollider.d.ts +33 -0
- package/dist/types/src/components/RigidBody.d.ts +456 -0
- package/dist/types/src/importers/Aseprite.d.ts +79 -0
- package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
- package/dist/types/src/importers/TiledMap.d.ts +62 -0
- package/dist/types/{lights → src/lights}/DirectionalLight.d.ts +7 -9
- package/dist/types/{lights → src/lights}/PointLight.d.ts +6 -9
- package/dist/types/src/managers/AssetManager.d.ts +116 -0
- package/dist/types/src/managers/AudioManager.d.ts +259 -0
- package/dist/types/{managers → src/managers}/CameraManager.d.ts +8 -8
- package/dist/types/{managers → src/managers}/EventManager.d.ts +46 -32
- package/dist/types/src/managers/GLManager.d.ts +84 -0
- package/dist/types/src/managers/GLState.d.ts +37 -0
- package/dist/types/src/managers/Gamepad.d.ts +102 -0
- package/dist/types/src/managers/IDManager.d.ts +31 -0
- package/dist/types/src/managers/InputManager.d.ts +381 -0
- package/dist/types/src/managers/NetworkManager.d.ts +93 -0
- package/dist/types/src/managers/RenderStats.d.ts +34 -0
- package/dist/types/src/managers/SceneManager.d.ts +44 -0
- package/dist/types/src/managers/ShaderManager.d.ts +55 -0
- package/dist/types/src/managers/TextureManager.d.ts +102 -0
- package/dist/types/src/particlesystem/Particle.d.ts +64 -0
- package/dist/types/{particlesystem → src/particlesystem}/ParticleSettings.d.ts +32 -24
- package/dist/types/{particlesystem → src/particlesystem}/Particles.d.ts +20 -12
- 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 +78 -0
- package/package.json +73 -60
- package/src/Animator.js +95 -0
- package/src/BitmapText.js +6 -5
- package/src/Camera.js +183 -0
- package/src/CameraController.js +192 -0
- package/src/CanvasText.js +281 -0
- package/src/CollisionLayers.js +86 -0
- package/src/Color.js +26 -18
- package/src/Coroutine.js +259 -0
- package/src/DebugOverlay.js +246 -0
- package/src/Drawable.js +842 -582
- package/src/Easing.js +57 -0
- package/src/Emerald.js +1150 -459
- package/src/EmeraldDB.js +328 -0
- package/src/FPSCounter.js +43 -43
- package/src/GLUtils.js +60 -67
- package/src/Instance.js +41 -5
- package/src/InstancedTexture.js +308 -127
- package/src/Interpolator.js +124 -0
- package/src/Material.js +202 -0
- package/src/MathUtils.js +134 -0
- package/src/ParticleEmitter.js +284 -0
- package/src/Physics.js +431 -40
- package/src/Pool.js +85 -0
- package/src/PostEffects.js +296 -0
- package/src/PostProcessor.js +304 -0
- package/src/RenderTarget.js +134 -0
- package/src/Scene.js +115 -83
- package/src/ScreenEffects.js +266 -0
- package/src/Serializer.js +131 -0
- package/src/Shaders.js +150 -165
- package/src/Shapes.js +118 -129
- package/src/SpatialGrid.js +111 -0
- package/src/SpriteBatch.js +299 -0
- package/src/StateMachine.js +82 -0
- package/src/Storage.js +175 -47
- package/src/Texture.js +58 -67
- package/src/TextureAtlas.js +96 -0
- package/src/Tilemap.js +274 -0
- package/src/Time.js +51 -6
- package/src/Timer.js +99 -0
- package/src/Transform.js +100 -7
- package/src/Tween.js +160 -0
- package/src/UI.js +394 -0
- package/src/components/Behaviour.js +90 -0
- package/src/components/BoxCollider.js +29 -12
- package/src/components/BoxColliderDebug.js +3 -4
- package/src/components/CircleCollider.js +26 -12
- package/src/components/CircleColliderDebug.js +25 -24
- package/src/components/Collider.js +150 -34
- package/src/components/GameObject.js +130 -21
- package/src/components/PolygonCollider.js +55 -0
- package/src/components/RigidBody.js +556 -8
- package/src/importers/Aseprite.js +142 -0
- package/src/importers/ForgeLevel.js +581 -0
- package/src/importers/TiledMap.js +158 -0
- package/src/lights/DirectionalLight.js +6 -15
- package/src/lights/PointLight.js +4 -4
- package/src/managers/AssetManager.js +239 -0
- package/src/managers/AudioManager.js +565 -146
- package/src/managers/EventManager.js +488 -477
- package/src/managers/GLManager.js +57 -0
- package/src/managers/GLState.js +70 -0
- package/src/managers/Gamepad.js +126 -0
- package/src/managers/IDManager.js +24 -2
- package/src/managers/InputManager.js +905 -0
- package/src/managers/NetworkManager.js +178 -0
- package/src/managers/RenderStats.js +34 -0
- package/src/managers/SceneManager.js +30 -0
- package/src/managers/ShaderManager.js +0 -2
- package/src/managers/TextureManager.js +218 -0
- package/src/particlesystem/Particle.js +82 -7
- package/src/particlesystem/ParticleSettings.js +21 -3
- package/src/particlesystem/Particles.js +80 -31
- 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/dist/types/Drawable.d.ts +0 -157
- package/dist/types/Emerald.d.ts +0 -73
- package/dist/types/GLUtils.d.ts +0 -6
- package/dist/types/Physics.d.ts +0 -139
- package/dist/types/Scene.d.ts +0 -39
- package/dist/types/Shaders.d.ts +0 -4
- package/dist/types/Storage.d.ts +0 -46
- package/dist/types/Time.d.ts +0 -22
- package/dist/types/Transform.d.ts +0 -41
- package/dist/types/components/BoxColliderDebug.d.ts +0 -19
- package/dist/types/components/CircleColliderDebug.d.ts +0 -19
- package/dist/types/components/Collider.d.ts +0 -53
- package/dist/types/components/GameObject.d.ts +0 -72
- package/dist/types/components/RigidBody.d.ts +0 -104
- package/dist/types/managers/AudioManager.d.ts +0 -60
- package/dist/types/managers/GLManager.d.ts +0 -47
- package/dist/types/managers/IDManager.d.ts +0 -21
- package/dist/types/managers/SceneManager.d.ts +0 -22
- package/dist/types/particlesystem/Particle.d.ts +0 -42
package/README.md
CHANGED
|
@@ -1,968 +1,2198 @@
|
|
|
1
|
-
# Emerald
|
|
2
|
-
|
|
3
|
-
Emerald is a comprehensive 2D graphics engine that can help you create games easier than ever.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
32
|
-
- [
|
|
33
|
-
|
|
34
|
-
- [
|
|
35
|
-
|
|
36
|
-
- [
|
|
37
|
-
- [Instance
|
|
38
|
-
- [
|
|
39
|
-
|
|
40
|
-
- [
|
|
41
|
-
|
|
42
|
-
- [
|
|
43
|
-
- [
|
|
44
|
-
|
|
45
|
-
- [
|
|
46
|
-
- [
|
|
47
|
-
- [
|
|
48
|
-
|
|
49
|
-
- [
|
|
50
|
-
|
|
51
|
-
- [
|
|
52
|
-
|
|
53
|
-
- [
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- [
|
|
57
|
-
- [
|
|
58
|
-
- [
|
|
59
|
-
- [
|
|
60
|
-
- [
|
|
61
|
-
- [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
- [
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
gameObject
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
###
|
|
500
|
-
|
|
501
|
-
```javascript
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
//
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
###
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
);
|
|
763
|
-
|
|
764
|
-
//
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
//
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
###
|
|
810
|
-
|
|
811
|
-
```javascript
|
|
812
|
-
//
|
|
813
|
-
|
|
814
|
-
console.log(
|
|
815
|
-
|
|
816
|
-
//
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
//
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
(
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
```
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
```
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
//
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1
|
+
# Emerald
|
|
2
|
+
|
|
3
|
+
Emerald is a comprehensive 2D graphics engine that can help you create games easier than ever.
|
|
4
|
+
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Emerald](#emerald)
|
|
10
|
+
- [Table of Contents](#table-of-contents)
|
|
11
|
+
- [Getting Started](#getting-started)
|
|
12
|
+
- [Scene](#scene)
|
|
13
|
+
- [Game Objects](#game-objects)
|
|
14
|
+
- [Creating a new GameObject](#creating-a-new-gameobject)
|
|
15
|
+
- [Behaviour (component lifecycle)](#behaviour-component-lifecycle)
|
|
16
|
+
- [Transform hierarchy](#transform-hierarchy)
|
|
17
|
+
- [Components](#components)
|
|
18
|
+
- [Texture](#texture)
|
|
19
|
+
- [InstancedTexture](#instancedtexture)
|
|
20
|
+
- [Square2D](#square2d)
|
|
21
|
+
- [Triangle2D](#triangle2d)
|
|
22
|
+
- [Circle2D](#circle2d)
|
|
23
|
+
- [RigidBody](#rigidbody)
|
|
24
|
+
- [BoxCollider](#boxcollider)
|
|
25
|
+
- [CircleCollider](#circlecollider)
|
|
26
|
+
- [PolygonCollider](#polygoncollider)
|
|
27
|
+
- [Object methods](#object-methods)
|
|
28
|
+
- [Position](#position)
|
|
29
|
+
- [Rotation](#rotation)
|
|
30
|
+
- [Scale](#scale)
|
|
31
|
+
- [Change color](#change-color)
|
|
32
|
+
- [Sprite flipping, pivot & anchor](#sprite-flipping-pivot--anchor)
|
|
33
|
+
- [Animations](#animations)
|
|
34
|
+
- [Instance System](#instance-system)
|
|
35
|
+
- [Creating Instances](#creating-instances)
|
|
36
|
+
- [Instance Management](#instance-management)
|
|
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)
|
|
41
|
+
- [Physics Engine](#physics-engine)
|
|
42
|
+
- [Setting up Physics](#setting-up-physics)
|
|
43
|
+
- [Time stepping](#time-stepping)
|
|
44
|
+
- [RigidBody methods](#rigidbody-methods)
|
|
45
|
+
- [Collision Detection](#collision-detection)
|
|
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)
|
|
50
|
+
- [Particle System](#particle-system)
|
|
51
|
+
- [Particle Settings](#particle-settings)
|
|
52
|
+
- [Creating Particle Systems](#creating-particle-systems)
|
|
53
|
+
- [Particle System Methods](#particle-system-methods)
|
|
54
|
+
- [ParticleEmitter (simple pooled bursts)](#particleemitter-simple-pooled-bursts)
|
|
55
|
+
- [Lighting System](#lighting-system)
|
|
56
|
+
- [Ambient Light](#ambient-light)
|
|
57
|
+
- [Point Light](#point-light)
|
|
58
|
+
- [Directional Light](#directional-light)
|
|
59
|
+
- [Text Rendering](#text-rendering)
|
|
60
|
+
- [BitmapText](#bitmaptext)
|
|
61
|
+
- [CanvasText](#canvastext)
|
|
62
|
+
- [Input](#input)
|
|
63
|
+
- [Keyboard Events](#keyboard-events)
|
|
64
|
+
- [Mouse Events](#mouse-events)
|
|
65
|
+
- [Object Events](#object-events)
|
|
66
|
+
- [Event Cleanup](#event-cleanup)
|
|
67
|
+
- [InputManager (actions)](#inputmanager-actions)
|
|
68
|
+
- [Gamepads & Controllers](#gamepads--controllers)
|
|
69
|
+
- [AudioManager](#audiomanager)
|
|
70
|
+
- [Adding Audio](#adding-audio)
|
|
71
|
+
- [Playing Audio](#playing-audio)
|
|
72
|
+
- [Audio Control](#audio-control)
|
|
73
|
+
- [Buses & Fades](#buses--fades)
|
|
74
|
+
- [Positional Audio](#positional-audio)
|
|
75
|
+
- [Camera](#camera)
|
|
76
|
+
- [FPSCounter](#fpscounter)
|
|
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)
|
|
98
|
+
- [Advanced Features](#advanced-features)
|
|
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
|
+
|
|
105
|
+
## Getting Started
|
|
106
|
+
|
|
107
|
+
To get started with Emerald, you need to have a canvas element in your HTML and import the necessary classes.
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
import { Emerald, Scene, Color, SceneManager } from "emeraldengine";
|
|
111
|
+
|
|
112
|
+
const emerald = new Emerald(canvas); // You should pass your own canvas element here
|
|
113
|
+
const scene = new Scene();
|
|
114
|
+
SceneManager.setScene(scene);
|
|
115
|
+
|
|
116
|
+
emerald.setBackgroundColor(new Color(20, 20, 30, 255)); // color = new Color(r, g, b, a = 255)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
To draw items on the screen you need some sort of animation loop. You can drive one yourself with `window.requestAnimationFrame`:
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
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);
|
|
130
|
+
```
|
|
131
|
+
|
|
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:
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
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),
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
// later: stop(); // or emerald.stop();
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
See [Auto-pause & lifecycle](#auto-pause--lifecycle) for the pause/resume hooks `run()` accepts.
|
|
150
|
+
|
|
151
|
+
## Scene
|
|
152
|
+
|
|
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
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
// Adding an object to the scene
|
|
157
|
+
scene.add(gameObject);
|
|
158
|
+
|
|
159
|
+
// Removing an object from the scene
|
|
160
|
+
scene.remove(gameObject);
|
|
161
|
+
|
|
162
|
+
// Freeing it for good (buffers + texture reference), instead of just removing it
|
|
163
|
+
scene.remove(gameObject, { dispose: true });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
When changing a scene you should deactivate the current scene to not mess up the event manager:
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
scene.setIsActive(true); // Activate
|
|
170
|
+
scene.setIsActive(false); // Deactivate
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`SceneManager` keeps track of which scene is currently active:
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
import { SceneManager } from "emeraldengine";
|
|
177
|
+
|
|
178
|
+
SceneManager.setScene(scene);
|
|
179
|
+
const currentScene = SceneManager.getScene();
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
It can also switch scenes behind a fade, wired to `ScreenEffects`. See [Scene Transitions & the Game Loop](#scene-transitions--the-game-loop).
|
|
183
|
+
|
|
184
|
+
## Game Objects
|
|
185
|
+
|
|
186
|
+
### Creating a new GameObject
|
|
187
|
+
|
|
188
|
+
```javascript
|
|
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);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
This will create a new empty GameObject. At this stage you will not see anything on the screen until you add some components.
|
|
201
|
+
|
|
202
|
+
### Behaviour (component lifecycle)
|
|
203
|
+
|
|
204
|
+
For your own game logic (rather than rendering/physics), add a `Behaviour` component. It's ticked automatically every frame the object is active.
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
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());
|
|
221
|
+
```
|
|
222
|
+
|
|
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.
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
parent.addChild(child); // or child.setParent(parent)
|
|
229
|
+
child.setParent(null); // detach
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Components
|
|
233
|
+
|
|
234
|
+
There are currently 9 components: Texture, InstancedTexture, Square2D, Circle2D, Triangle2D, RigidBody, BoxCollider, CircleCollider, PolygonCollider
|
|
235
|
+
|
|
236
|
+
#### Texture
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
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)
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Add the texture to a game object
|
|
265
|
+
gameObject.addComponent(texture);
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+

|
|
269
|
+
|
|
270
|
+
#### InstancedTexture
|
|
271
|
+
|
|
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.
|
|
273
|
+
|
|
274
|
+
```javascript
|
|
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
|
+
);
|
|
301
|
+
|
|
302
|
+
// Add the instanced texture to a game object
|
|
303
|
+
gameObject.addComponent(instancedTexture);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+

|
|
307
|
+
|
|
308
|
+
#### Square2D
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
import { Square2D } from "emeraldengine";
|
|
312
|
+
let square = new Square2D();
|
|
313
|
+
gameObject.addComponent(square);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+

|
|
317
|
+
|
|
318
|
+
#### Triangle2D
|
|
319
|
+
|
|
320
|
+
```javascript
|
|
321
|
+
import { Triangle2D } from "emeraldengine";
|
|
322
|
+
let triangle = new Triangle2D();
|
|
323
|
+
gameObject.addComponent(triangle);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+

|
|
327
|
+
|
|
328
|
+
#### Circle2D
|
|
329
|
+
|
|
330
|
+
```javascript
|
|
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
|
+
```
|
|
339
|
+
|
|
340
|
+

|
|
341
|
+
|
|
342
|
+
#### RigidBody
|
|
343
|
+
|
|
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.
|
|
345
|
+
|
|
346
|
+
```javascript
|
|
347
|
+
import { RigidBody, Physics, Vector2 } from "emeraldengine";
|
|
348
|
+
|
|
349
|
+
// Create physics engine first
|
|
350
|
+
const physics = new Physics(-70, 32, 2); // gravity, scale, velocityThreshold
|
|
351
|
+
|
|
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
|
+
);
|
|
369
|
+
|
|
370
|
+
gameObject.addComponent(rigidBody);
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
#### BoxCollider
|
|
374
|
+
|
|
375
|
+
```javascript
|
|
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
|
+
);
|
|
398
|
+
|
|
399
|
+
gameObject.addComponent(boxCollider);
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+

|
|
403
|
+
|
|
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.
|
|
405
|
+
|
|
406
|
+
#### CircleCollider
|
|
407
|
+
|
|
408
|
+
```javascript
|
|
409
|
+
import { CircleCollider } from "emeraldengine";
|
|
410
|
+
|
|
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
|
+
);
|
|
431
|
+
|
|
432
|
+
gameObject.addComponent(circleCollider);
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+

|
|
436
|
+
|
|
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.
|
|
438
|
+
|
|
439
|
+
#### PolygonCollider
|
|
440
|
+
|
|
441
|
+
For a collision shape a box or circle can't approximate, like ramps, wedges, or arbitrary outlines.
|
|
442
|
+
|
|
443
|
+
```javascript
|
|
444
|
+
import { PolygonCollider } from "emeraldengine";
|
|
445
|
+
|
|
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
|
+
);
|
|
470
|
+
|
|
471
|
+
gameObject.addComponent(polygonCollider);
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## Object methods
|
|
475
|
+
|
|
476
|
+
### Position
|
|
477
|
+
|
|
478
|
+
```javascript
|
|
479
|
+
// Set position
|
|
480
|
+
gameObject.transform.position.x = 100;
|
|
481
|
+
gameObject.transform.position.y = 200;
|
|
482
|
+
gameObject.transform.position.z = 0;
|
|
483
|
+
|
|
484
|
+
// Or set all at once
|
|
485
|
+
gameObject.transform.position = new Vector3(100, 200, 0);
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+

|
|
489
|
+
|
|
490
|
+
### Rotation
|
|
491
|
+
|
|
492
|
+
```javascript
|
|
493
|
+
// Set rotation (in radians)
|
|
494
|
+
gameObject.transform.rotation = Math.PI / 4; // 45 degrees
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+

|
|
498
|
+
|
|
499
|
+
### Scale
|
|
500
|
+
|
|
501
|
+
```javascript
|
|
502
|
+
// Set scale
|
|
503
|
+
gameObject.transform.scale.x = 2;
|
|
504
|
+
gameObject.transform.scale.y = 2;
|
|
505
|
+
|
|
506
|
+
// Or set both at once
|
|
507
|
+
gameObject.transform.scale = new Vector2(2, 2);
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+

|
|
511
|
+
|
|
512
|
+
### Change color
|
|
513
|
+
|
|
514
|
+
```javascript
|
|
515
|
+
// For textures
|
|
516
|
+
const texture = gameObject.getComponent(Texture);
|
|
517
|
+
texture.setColor(new Color(255, 0, 0)); // Red
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+

|
|
521
|
+
|
|
522
|
+
### Sprite flipping, pivot & anchor
|
|
523
|
+
|
|
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.
|
|
525
|
+
|
|
526
|
+
```javascript
|
|
527
|
+
tex.setFlipX(facing < 0); // mirror horizontally (e.g. face left)
|
|
528
|
+
tex.setFlipY(true); // mirror vertically
|
|
529
|
+
|
|
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
|
|
534
|
+
|
|
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
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
## Animations
|
|
541
|
+
|
|
542
|
+
For animated textures, `setFrame` jumps straight to a specific frame:
|
|
543
|
+
|
|
544
|
+
```javascript
|
|
545
|
+
const texture = gameObject.getComponent(Texture);
|
|
546
|
+
texture.setFrame(2); // Set to frame 2
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Or play through a sequence of frames:
|
|
550
|
+
|
|
551
|
+
```javascript
|
|
552
|
+
// Play animation
|
|
553
|
+
texture.playAnimation([0, 1, 2, 3], 200); // frames array, speed in ms
|
|
554
|
+
|
|
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
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+

|
|
568
|
+
|
|
569
|
+
For named clips instead of raw frame arrays, use `Animator`:
|
|
570
|
+
|
|
571
|
+
```javascript
|
|
572
|
+
import { Animator } from "emeraldengine";
|
|
573
|
+
|
|
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
|
+
```
|
|
582
|
+
|
|
583
|
+
## Instance System
|
|
584
|
+
|
|
585
|
+
The Instance system allows you to efficiently manage multiple copies of the same texture through `InstancedTexture` (see [Components](#instancedtexture)).
|
|
586
|
+
|
|
587
|
+
### Creating Instances
|
|
588
|
+
|
|
589
|
+
```javascript
|
|
590
|
+
import { Instance } from "emeraldengine";
|
|
591
|
+
|
|
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
|
+
);
|
|
600
|
+
|
|
601
|
+
// Add to InstancedTexture
|
|
602
|
+
const instancedTexture = gameObject.getComponent(InstancedTexture);
|
|
603
|
+
instancedTexture.addInstance(instance);
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### Instance Management
|
|
607
|
+
|
|
608
|
+
```javascript
|
|
609
|
+
// Remove instance
|
|
610
|
+
instancedTexture.removeInstance(instanceId);
|
|
611
|
+
|
|
612
|
+
// Get instance by ID
|
|
613
|
+
const instance = instancedTexture.getInstanceWithId(instanceId);
|
|
614
|
+
|
|
615
|
+
// Get instance at position
|
|
616
|
+
const instance = instancedTexture.getInstanceAtPosition(position, tolerance);
|
|
617
|
+
|
|
618
|
+
// Clear all instances
|
|
619
|
+
instancedTexture.clearInstances();
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Instance Events
|
|
623
|
+
|
|
624
|
+
```javascript
|
|
625
|
+
// Add click event to specific instance
|
|
626
|
+
instancedTexture.addInstanceClickEvent(instanceId, (event) => {
|
|
627
|
+
console.log("Instance clicked!");
|
|
628
|
+
});
|
|
629
|
+
|
|
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
|
+
);
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### Per-instance color
|
|
639
|
+
|
|
640
|
+
Each instance can have an independent RGBA tint (white = unchanged, so existing scenes render identically). The tint multiplies the texture in the shader.
|
|
641
|
+
|
|
642
|
+
```javascript
|
|
643
|
+
import { Color } from "emeraldengine";
|
|
644
|
+
|
|
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
|
|
648
|
+
|
|
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
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
Tip: for additive sparkle/coin glows, set the instanced texture's blend mode: `instancedTexture.setBlendMode("additive")`.
|
|
655
|
+
|
|
656
|
+
### Per-instance atlas regions
|
|
657
|
+
|
|
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:
|
|
659
|
+
|
|
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
|
|
671
|
+
```
|
|
672
|
+
|
|
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.
|
|
674
|
+
|
|
675
|
+
### Per-instance animation
|
|
676
|
+
|
|
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.
|
|
678
|
+
|
|
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
|
|
684
|
+
|
|
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
|
+
```
|
|
692
|
+
|
|
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).
|
|
694
|
+
|
|
695
|
+
## Physics Engine
|
|
696
|
+
|
|
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.
|
|
698
|
+
|
|
699
|
+
### Setting up Physics
|
|
700
|
+
|
|
701
|
+
```javascript
|
|
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);
|
|
710
|
+
```
|
|
711
|
+
|
|
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.
|
|
713
|
+
|
|
714
|
+
You need to process physics in your own update loop:
|
|
715
|
+
|
|
716
|
+
```javascript
|
|
717
|
+
const animate = (currentTime) => {
|
|
718
|
+
physics.process(deltaTime);
|
|
719
|
+
};
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### Time stepping
|
|
723
|
+
|
|
724
|
+
`process(dt)` turns a frame's elapsed time into simulation steps, in one of two modes.
|
|
725
|
+
|
|
726
|
+
```javascript
|
|
727
|
+
physics.setFixedTimeStep(1 / 60, 5); // step, max substeps (default)
|
|
728
|
+
physics.setVariableTimeStep(1 / 30, 5); // max step, max substeps
|
|
729
|
+
```
|
|
730
|
+
|
|
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.
|
|
732
|
+
|
|
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`.
|
|
734
|
+
|
|
735
|
+
```javascript
|
|
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
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### RigidBody methods
|
|
742
|
+
|
|
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.
|
|
744
|
+
|
|
745
|
+
```javascript
|
|
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.
|
|
808
|
+
|
|
809
|
+
### Collision Detection
|
|
810
|
+
|
|
811
|
+
```javascript
|
|
812
|
+
// Handle collision enter
|
|
813
|
+
physics.onCollisionEnter((bodyA, bodyB, contact) => {
|
|
814
|
+
console.log("Collision started!");
|
|
815
|
+
|
|
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)
|
|
826
|
+
|
|
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
|
+
});
|
|
834
|
+
|
|
835
|
+
// Handle collision exit
|
|
836
|
+
physics.onCollisionExit((bodyA, bodyB, contact) => {
|
|
837
|
+
console.log("Collision ended!");
|
|
838
|
+
});
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
Per-object collision events also fire automatically on `Behaviour` components (`onCollisionEnter`/`onCollisionExit`) for any GameObject with a RigidBody. See [Behaviour](#behaviour-component-lifecycle).
|
|
842
|
+
|
|
843
|
+
### Collision layers
|
|
844
|
+
|
|
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.
|
|
846
|
+
|
|
847
|
+
```javascript
|
|
848
|
+
import { CollisionLayers } from "emeraldengine";
|
|
849
|
+
|
|
850
|
+
CollisionLayers.define("ground", "player", "enemy", "pickup");
|
|
851
|
+
|
|
852
|
+
playerCollider
|
|
853
|
+
.setCategory("player")
|
|
854
|
+
.setCollidesWith(["ground", "enemy", "pickup"]);
|
|
855
|
+
enemyCollider.setCategory("enemy").setCollidesWith(["ground", "player"]); // ignore each other
|
|
856
|
+
|
|
857
|
+
// Or up front, in the collider constructor's filter argument:
|
|
858
|
+
new BoxCollider(body, size, 1, 0.2, 0, false, gameObject, {
|
|
859
|
+
category: "pickup",
|
|
860
|
+
collidesWith: ["player"],
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// Raw control if you prefer bits:
|
|
864
|
+
collider.setFilter({ category: 0x0004, mask: 0xffff, group: 0 });
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### Continuous collision detection
|
|
868
|
+
|
|
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:
|
|
870
|
+
|
|
871
|
+
```javascript
|
|
872
|
+
projectile.setContinuous(true); // bullet-mode CCD
|
|
873
|
+
projectile.isContinuous(); // boolean
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
Reserve it for the handful of bodies that actually move fast; it costs more per step.
|
|
877
|
+
|
|
878
|
+
### Raycasting & point queries
|
|
879
|
+
|
|
880
|
+
```javascript
|
|
881
|
+
const hit = physics.raycast({ x, y }, { x: 1, y: 0 }, 500);
|
|
882
|
+
// -> { object, rigidBody, point, normal, fraction } | null
|
|
883
|
+
|
|
884
|
+
const objects = physics.queryPoint({ x, y }); // owners whose collider contains the point
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### Tilemap colliders & auto-tiling
|
|
888
|
+
|
|
889
|
+
A `Tilemap` can generate physics colliders from its map and auto-pick tile frames from a solidity grid.
|
|
890
|
+
|
|
891
|
+
```javascript
|
|
892
|
+
// 1) Build solid colliders from the current map.
|
|
893
|
+
// Solid cells are merged greedily into horizontal runs, so a row of N tiles
|
|
894
|
+
// becomes ONE static box collider instead of N.
|
|
895
|
+
map.setMap(grid, { originX: 0, originY: 0, flipY: true });
|
|
896
|
+
map.buildColliders(physics, {
|
|
897
|
+
isSolid: (frame) => frame != null && frame >= 0, // default
|
|
898
|
+
friction: 0.2,
|
|
899
|
+
restitution: 0,
|
|
900
|
+
density: 0,
|
|
901
|
+
ownerObject: map.gameObject, // collision callbacks resolve back to this
|
|
902
|
+
});
|
|
903
|
+
map.clearColliders(); // destroy the generated bodies (e.g. before a rebuild)
|
|
904
|
+
|
|
905
|
+
// 2) Auto-tiling: turn a boolean solidity grid into frame indices using a
|
|
906
|
+
// 4-bit edge bitmask (up|right|down|left = bits 1,2,4,8). Empty cells -> -1.
|
|
907
|
+
const frames = Tilemap.computeAutoTile(solidGrid, {
|
|
908
|
+
frames: lookup16, // optional length-16 mask -> frame map matching your sheet
|
|
909
|
+
base: 0, // added to every solid frame when no lookup is given
|
|
910
|
+
edgesSolid: true, // treat out-of-bounds as solid
|
|
911
|
+
});
|
|
912
|
+
map.setAutoTiledMap(solidGrid, { base: 0, originX: 0, originY: 0 }); // compute + setMap
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
## Particle System
|
|
916
|
+
|
|
917
|
+
Emerald includes a powerful particle system for creating visual effects.
|
|
918
|
+
|
|
919
|
+

|
|
920
|
+
|
|
921
|
+
### Particle Settings
|
|
922
|
+
|
|
923
|
+
```javascript
|
|
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
|
+
}
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
### ParticleEmitter (simple pooled bursts)
|
|
1012
|
+
|
|
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.
|
|
1014
|
+
|
|
1015
|
+
```javascript
|
|
1016
|
+
import { ParticleEmitter } from "emeraldengine";
|
|
1017
|
+
|
|
1018
|
+
const fx = new ParticleEmitter(scene, {
|
|
1019
|
+
texture: "spark.png",
|
|
1020
|
+
capacity: 256, // pool size (max live particles)
|
|
1021
|
+
layer: 50,
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
fx.burst(12, {
|
|
1025
|
+
x: 200,
|
|
1026
|
+
y: 120,
|
|
1027
|
+
dir: -Math.PI / 2,
|
|
1028
|
+
spread: Math.PI,
|
|
1029
|
+
speed: 180, // emission cone
|
|
1030
|
+
gx: 0,
|
|
1031
|
+
gy: -300,
|
|
1032
|
+
drag: 2,
|
|
1033
|
+
life: 0.4,
|
|
1034
|
+
size: 8,
|
|
1035
|
+
sFrom: 1,
|
|
1036
|
+
sTo: 0.1, // scale over life
|
|
1037
|
+
aFrom: 0.7,
|
|
1038
|
+
aTo: 0, // alpha over life
|
|
1039
|
+
cr: 255,
|
|
1040
|
+
cg: 220,
|
|
1041
|
+
cb: 120,
|
|
1042
|
+
additive: true,
|
|
1043
|
+
rotSpeed: 6,
|
|
1044
|
+
shape: "ring",
|
|
1045
|
+
radius: 12, // "point" | "ring" | "circle" | "box"
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
// in the loop:
|
|
1049
|
+
fx.update(dt);
|
|
1050
|
+
fx.activeCount; // live particles
|
|
1051
|
+
fx.reset(); // kill all immediately
|
|
1052
|
+
fx.destroy(); // remove pooled objects from the scene
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
## Lighting System
|
|
1056
|
+
|
|
1057
|
+
Emerald supports ambient, point, and directional lighting.
|
|
1058
|
+
|
|
1059
|
+
### Ambient Light
|
|
1060
|
+
|
|
1061
|
+
```javascript
|
|
1062
|
+
// Set ambient light
|
|
1063
|
+
emerald.setAmbientLight(new Vector3(0.3, 0.3, 0.3)); // RGB values 0-1
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
### Point Light
|
|
1067
|
+
|
|
1068
|
+
```javascript
|
|
1069
|
+
import { PointLight } from "emeraldengine";
|
|
1070
|
+
|
|
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
|
+
);
|
|
1084
|
+
|
|
1085
|
+
// Add to engine
|
|
1086
|
+
emerald.addPointLight(pointLight);
|
|
1087
|
+
|
|
1088
|
+
// Update position
|
|
1089
|
+
pointLight.position.x = newX;
|
|
1090
|
+
pointLight.position.y = newY;
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
### Directional Light
|
|
1094
|
+
|
|
1095
|
+
```javascript
|
|
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
|
+
);
|
|
1113
|
+
|
|
1114
|
+
// Add to engine
|
|
1115
|
+
emerald.addDirectionalLight(directionalLight);
|
|
1116
|
+
|
|
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;
|
|
1127
|
+
```
|
|
1128
|
+
|
|
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.
|
|
1134
|
+
|
|
1135
|
+

|
|
1136
|
+
|
|
1137
|
+
```javascript
|
|
1138
|
+
import { BitmapText } from "emeraldengine";
|
|
1139
|
+
|
|
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
|
+
);
|
|
1173
|
+
|
|
1174
|
+
// Add to scene
|
|
1175
|
+
scene.add(bitmapText.gameObject);
|
|
1176
|
+
|
|
1177
|
+
// Update text
|
|
1178
|
+
bitmapText.setText("New Text!");
|
|
1179
|
+
bitmapText.setColor(new Color(255, 0, 0));
|
|
1180
|
+
bitmapText.setFontSize(32);
|
|
1181
|
+
bitmapText.setLetterSpacing(20);
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
### CanvasText
|
|
1185
|
+
|
|
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).
|
|
1187
|
+
|
|
1188
|
+
```javascript
|
|
1189
|
+
import { CanvasText } from "emeraldengine";
|
|
1190
|
+
|
|
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);
|
|
1199
|
+
|
|
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
|
+
);
|
|
1205
|
+
|
|
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"
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
## Input
|
|
1215
|
+
|
|
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).
|
|
1217
|
+
|
|
1218
|
+

|
|
1219
|
+
|
|
1220
|
+
```javascript
|
|
1221
|
+
import { EventManager } from "emeraldengine";
|
|
1222
|
+
|
|
1223
|
+
let eventManager = new EventManager(canvas, scene, emerald.camera);
|
|
1224
|
+
```
|
|
1225
|
+
|
|
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
|
|
1229
|
+
|
|
1230
|
+
```javascript
|
|
1231
|
+
// Key down events
|
|
1232
|
+
eventManager.addKeyDown("w", () => {
|
|
1233
|
+
console.log("W key pressed");
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
// Key up events
|
|
1237
|
+
eventManager.addKeyUp("w", () => {
|
|
1238
|
+
console.log("W key released");
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// Check if key is currently pressed
|
|
1242
|
+
if (eventManager.isKeyPressed("w")) {
|
|
1243
|
+
// W key is currently held down
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Remove key events
|
|
1247
|
+
eventManager.removeKeyDown("w", callbackFunction);
|
|
1248
|
+
eventManager.removeKeyUp("w", callbackFunction);
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
### Mouse Events
|
|
1252
|
+
|
|
1253
|
+
```javascript
|
|
1254
|
+
// Get mouse position
|
|
1255
|
+
const mousePos = eventManager.getMousePosition();
|
|
1256
|
+
console.log(mousePos.x, mousePos.y);
|
|
1257
|
+
|
|
1258
|
+
// Check if camera was moved
|
|
1259
|
+
if (eventManager.wasCameraMoved()) {
|
|
1260
|
+
// Camera was moved by dragging
|
|
1261
|
+
eventManager.resetCameraMoved();
|
|
1262
|
+
}
|
|
1263
|
+
```
|
|
1264
|
+
|
|
1265
|
+
### Object Events
|
|
1266
|
+
|
|
1267
|
+
```javascript
|
|
1268
|
+
// Click events
|
|
1269
|
+
eventManager.addClickEvent(gameObject, (event, object) => {
|
|
1270
|
+
console.log("Object clicked!");
|
|
1271
|
+
});
|
|
1272
|
+
|
|
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
|
+
);
|
|
1283
|
+
|
|
1284
|
+
// Remove events
|
|
1285
|
+
eventManager.removeClickEvent(gameObject, callbackFunction);
|
|
1286
|
+
eventManager.removeHoverEvent(gameObject, enterCallback, leaveCallback);
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
### Event Cleanup
|
|
1290
|
+
|
|
1291
|
+
```javascript
|
|
1292
|
+
// Clean up all events when done
|
|
1293
|
+
eventManager.clean();
|
|
1294
|
+
|
|
1295
|
+
// Change scene
|
|
1296
|
+
eventManager.changeScene(newScene);
|
|
1297
|
+
```
|
|
1298
|
+
|
|
1299
|
+
### InputManager (actions)
|
|
1300
|
+
|
|
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:
|
|
1302
|
+
|
|
1303
|
+
```javascript
|
|
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)
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
### Gamepads & Controllers
|
|
1315
|
+
|
|
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.
|
|
1317
|
+
|
|
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:
|
|
1319
|
+
|
|
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 |
|
|
1330
|
+
|
|
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:
|
|
1332
|
+
|
|
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
|
+
});
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
Sticks, triggers and rumble:
|
|
1342
|
+
|
|
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
|
+
```
|
|
1351
|
+
|
|
1352
|
+
Connection events and diagnostics:
|
|
1353
|
+
|
|
1354
|
+
```javascript
|
|
1355
|
+
input.onGamepadConnected((info) =>
|
|
1356
|
+
console.log(info.id, info.mapping, info.buttonCount, info.axesCount)
|
|
1357
|
+
);
|
|
1358
|
+
input.onGamepadDisconnected((info) => pauseFor(info.index));
|
|
1359
|
+
|
|
1360
|
+
input.isGamepadConnected(0);
|
|
1361
|
+
input.getGamepadInfo(0); // { id, mapping, buttonCount, axesCount, standard }
|
|
1362
|
+
input.getPressedButtons(0); // raw indices currently pressed (layout discovery)
|
|
1363
|
+
```
|
|
1364
|
+
|
|
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:
|
|
1366
|
+
|
|
1367
|
+
```javascript
|
|
1368
|
+
InputManager.registerGamepadMapping("my-controller-id", {
|
|
1369
|
+
buttons: { south: 1, east: 2, west: 0, north: 3, start: 9 },
|
|
1370
|
+
});
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
D-pads reported as a hat axis (instead of buttons 12–15) are decoded into the `dpad*` tokens automatically.
|
|
1374
|
+
|
|
1375
|
+
## AudioManager
|
|
1376
|
+
|
|
1377
|
+
Emerald includes a comprehensive audio management system.
|
|
1378
|
+
|
|
1379
|
+
### Adding Audio
|
|
1380
|
+
|
|
1381
|
+
```javascript
|
|
1382
|
+
import { AudioManager } from "emeraldengine";
|
|
1383
|
+
|
|
1384
|
+
const audioManager = new AudioManager();
|
|
1385
|
+
|
|
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 });
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
### Playing Audio
|
|
1392
|
+
|
|
1393
|
+
```javascript
|
|
1394
|
+
// Play audio
|
|
1395
|
+
audioManager.play("soundName"); // restarts from 0
|
|
1396
|
+
|
|
1397
|
+
// Play overlapping copies, for rapid SFX
|
|
1398
|
+
audioManager.playOverlap("soundName");
|
|
1399
|
+
|
|
1400
|
+
// Play exclusively (stops all other audio first)
|
|
1401
|
+
audioManager.playExclusive("soundName");
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
### Audio Control
|
|
1405
|
+
|
|
1406
|
+
```javascript
|
|
1407
|
+
// Stop specific audio
|
|
1408
|
+
audioManager.stop("soundName");
|
|
1409
|
+
|
|
1410
|
+
// Stop all audio
|
|
1411
|
+
audioManager.stopAll();
|
|
1412
|
+
|
|
1413
|
+
// Remove audio
|
|
1414
|
+
audioManager.remove("soundName");
|
|
1415
|
+
|
|
1416
|
+
// Get audio object
|
|
1417
|
+
const sound = audioManager.getSound("soundName");
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
### Buses & Fades
|
|
1421
|
+
|
|
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:
|
|
1423
|
+
|
|
1424
|
+
```javascript
|
|
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
|
|
1430
|
+
```
|
|
1431
|
+
|
|
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):
|
|
1433
|
+
|
|
1434
|
+
```javascript
|
|
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
|
|
1439
|
+
```
|
|
1440
|
+
|
|
1441
|
+
### Positional Audio
|
|
1442
|
+
|
|
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.
|
|
1444
|
+
|
|
1445
|
+
```javascript
|
|
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 });
|
|
1453
|
+
```
|
|
1454
|
+
|
|
1455
|
+
## Camera
|
|
1456
|
+
|
|
1457
|
+
The engine has simple controls for the camera. The camera is stored in the emerald variable.
|
|
1458
|
+
|
|
1459
|
+
```javascript
|
|
1460
|
+
// Set camera position
|
|
1461
|
+
emerald.camera.setPosition(x, y, z);
|
|
1462
|
+
emerald.camera.setZoom(1.5);
|
|
1463
|
+
|
|
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;
|
|
1469
|
+
```
|
|
1470
|
+
|
|
1471
|
+
Multiple cameras are supported, each with a normalized viewport (origin bottom-left), useful for split-screen:
|
|
1472
|
+
|
|
1473
|
+
```javascript
|
|
1474
|
+
import { Camera, CameraController } from "emeraldengine";
|
|
1475
|
+
|
|
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)
|
|
1479
|
+
|
|
1480
|
+
top.clearColor = new Color(10, 14, 20); // optional per-viewport clear
|
|
1481
|
+
top.setIgnoreLayers([11, 20]); // skip these object layers in this camera
|
|
1482
|
+
```
|
|
1483
|
+
|
|
1484
|
+
`CameraController` adds smooth follow, bounds, a deadzone, and shake:
|
|
1485
|
+
|
|
1486
|
+
```javascript
|
|
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);
|
|
1494
|
+
```
|
|
1495
|
+
|
|
1496
|
+
## FPSCounter
|
|
1497
|
+
|
|
1498
|
+
Emerald has a built-in FPS counter.
|
|
1499
|
+
|
|
1500
|
+
```javascript
|
|
1501
|
+
import { FPSCounter } from "emeraldengine";
|
|
1502
|
+
let fpsCounter = new FPSCounter();
|
|
1503
|
+
|
|
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();
|
|
1510
|
+
```
|
|
1511
|
+
|
|
1512
|
+
## Time Management
|
|
1513
|
+
|
|
1514
|
+
```javascript
|
|
1515
|
+
import { Time } from "emeraldengine";
|
|
1516
|
+
|
|
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
|
|
1521
|
+
|
|
1522
|
+
// Time is automatically updated when you call emerald.drawScene()
|
|
1523
|
+
// You can also manually set it
|
|
1524
|
+
Time.setDeltaTime(deltaTime);
|
|
1525
|
+
|
|
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
|
+
```
|
|
1530
|
+
|
|
1531
|
+
## Tween, Timer & StateMachine
|
|
1532
|
+
|
|
1533
|
+
```javascript
|
|
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
|
+
```
|
|
1547
|
+
|
|
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
|
+
```
|
|
1555
|
+
|
|
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
|
+
```
|
|
1568
|
+
|
|
1569
|
+
## SpatialGrid & Pool
|
|
1570
|
+
|
|
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
|
+
```
|
|
1579
|
+
|
|
1580
|
+
```javascript
|
|
1581
|
+
import { Pool } from "emeraldengine";
|
|
1582
|
+
const bullets = new Pool(
|
|
1583
|
+
() => new Bullet(),
|
|
1584
|
+
(b, x, y) => b.spawn(x, y),
|
|
1585
|
+
50
|
|
1586
|
+
);
|
|
1587
|
+
const b = bullets.acquire(px, py);
|
|
1588
|
+
bullets.release(b); // bullets.releaseAll()
|
|
1589
|
+
```
|
|
1590
|
+
|
|
1591
|
+
## MathUtils
|
|
1592
|
+
|
|
1593
|
+
```javascript
|
|
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);
|
|
1604
|
+
```
|
|
1605
|
+
|
|
1606
|
+
## Coroutines
|
|
1607
|
+
|
|
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`.
|
|
1609
|
+
|
|
1610
|
+
```javascript
|
|
1611
|
+
import { Coroutine } from "emeraldengine";
|
|
1612
|
+
|
|
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
|
+
});
|
|
1628
|
+
|
|
1629
|
+
handle.cancel(); // stop it early
|
|
1630
|
+
handle.isRunning(); // boolean
|
|
1631
|
+
await handle.promise; // resolves when the coroutine finishes or is cancelled
|
|
1632
|
+
|
|
1633
|
+
Coroutine.count(); // number of running coroutines
|
|
1634
|
+
Coroutine.clearAll(); // cancel + remove every coroutine (e.g. on scene exit)
|
|
1635
|
+
```
|
|
1636
|
+
|
|
1637
|
+
## Rendering Pipeline
|
|
1638
|
+
|
|
1639
|
+
### Post-processing
|
|
1640
|
+
|
|
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:
|
|
1642
|
+
|
|
1643
|
+
```javascript
|
|
1644
|
+
emerald.enablePostProcessing(); // allocate the scene render target + processor
|
|
1645
|
+
emerald.disablePostProcessing(); // turn it back off
|
|
1646
|
+
|
|
1647
|
+
const bloom = emerald.addPostEffect(PostEffects.bloom()); // returns the effect
|
|
1648
|
+
emerald.removePostEffect(bloom);
|
|
1649
|
+
|
|
1650
|
+
// Effects run in the order they were added. Toggle one without removing it:
|
|
1651
|
+
bloom.enabled = false;
|
|
1652
|
+
```
|
|
1653
|
+
|
|
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`:
|
|
1657
|
+
|
|
1658
|
+
```javascript
|
|
1659
|
+
import { PostEffect } from "emeraldengine";
|
|
1660
|
+
|
|
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
|
+
}
|
|
1672
|
+
);
|
|
1673
|
+
emerald.addPostEffect(tint);
|
|
1674
|
+
```
|
|
1675
|
+
|
|
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:
|
|
1677
|
+
|
|
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
|
+
```
|
|
1683
|
+
|
|
1684
|
+
### PostEffects (built-in)
|
|
1685
|
+
|
|
1686
|
+
Factory functions on the `PostEffects` namespace return a ready `PostEffect`:
|
|
1687
|
+
|
|
1688
|
+
```javascript
|
|
1689
|
+
import { PostEffects } from "emeraldengine";
|
|
1690
|
+
|
|
1691
|
+
emerald.enablePostProcessing();
|
|
1692
|
+
emerald.addPostEffect(
|
|
1693
|
+
PostEffects.bloom({ threshold: 0.6, intensity: 1.2, spread: 1.1 })
|
|
1694
|
+
);
|
|
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());
|
|
1707
|
+
```
|
|
1708
|
+
|
|
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 |
|
|
1718
|
+
|
|
1719
|
+
`bloom` is exported as a class too (`BloomEffect`) if you want to subclass it.
|
|
1720
|
+
|
|
1721
|
+
### RenderTarget
|
|
1722
|
+
|
|
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.
|
|
1724
|
+
|
|
1725
|
+
```javascript
|
|
1726
|
+
import { RenderTarget } from "emeraldengine";
|
|
1727
|
+
|
|
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
|
|
1735
|
+
```
|
|
1736
|
+
|
|
1737
|
+
### Material (custom shaders)
|
|
1738
|
+
|
|
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)`.
|
|
1740
|
+
|
|
1741
|
+
```javascript
|
|
1742
|
+
import { Material, Square2D } from "emeraldengine";
|
|
1743
|
+
|
|
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
|
+
);
|
|
1755
|
+
|
|
1756
|
+
const shape = new Square2D();
|
|
1757
|
+
shape.setMaterial(dissolve); // any Drawable: Texture, Square2D, Circle2D…
|
|
1758
|
+
gameObject.addComponent(shape);
|
|
1759
|
+
|
|
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));
|
|
1763
|
+
```
|
|
1764
|
+
|
|
1765
|
+
### SpriteBatch
|
|
1766
|
+
|
|
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).
|
|
1768
|
+
|
|
1769
|
+
```javascript
|
|
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
|
|
1796
|
+
```
|
|
1797
|
+
|
|
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.
|
|
1799
|
+
|
|
1800
|
+
## In-Engine UI
|
|
1801
|
+
|
|
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.
|
|
1803
|
+
|
|
1804
|
+
Pair it with a UI camera so the UI draws over the game and the game cameras skip the UI layer:
|
|
1805
|
+
|
|
1806
|
+
```javascript
|
|
1807
|
+
import { UI } from "emeraldengine";
|
|
1808
|
+
|
|
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
|
|
1812
|
+
|
|
1813
|
+
const ui = new UI(scene, canvas, { accent: [120, 200, 255] });
|
|
1814
|
+
|
|
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");
|
|
1821
|
+
|
|
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
|
+
});
|
|
1827
|
+
|
|
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 });
|
|
1831
|
+
|
|
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");
|
|
1840
|
+
|
|
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
|
|
1844
|
+
|
|
1845
|
+
UI.LAYER; // 100000, the default UI render layer
|
|
1846
|
+
```
|
|
1847
|
+
|
|
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`.
|
|
1851
|
+
|
|
1852
|
+
```javascript
|
|
1853
|
+
import { ScreenEffects, Color } from "emeraldengine";
|
|
1854
|
+
|
|
1855
|
+
const fx = new ScreenEffects(scene, { layer: 100000, size: 5000 });
|
|
1856
|
+
|
|
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
|
|
1860
|
+
|
|
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();
|
|
1868
|
+
```
|
|
1869
|
+
|
|
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`:
|
|
1875
|
+
|
|
1876
|
+
```javascript
|
|
1877
|
+
import { SceneManager, ScreenEffects, Color } from "emeraldengine";
|
|
1878
|
+
|
|
1879
|
+
const fx = new ScreenEffects(overlayScene); // update()'d each frame by your loop
|
|
1880
|
+
|
|
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
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
// Or lower-level: fade out -> swap -> fade in
|
|
1889
|
+
await fx.transition(() => swapScenes(), { duration: 0.4 });
|
|
1890
|
+
```
|
|
1891
|
+
|
|
1892
|
+
## DebugOverlay
|
|
1893
|
+
|
|
1894
|
+
```javascript
|
|
1895
|
+
import { DebugOverlay } from "emeraldengine";
|
|
1896
|
+
|
|
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();
|
|
1904
|
+
```
|
|
1905
|
+
|
|
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:
|
|
1907
|
+
|
|
1908
|
+
```javascript
|
|
1909
|
+
const { drawCalls, quads, textureBinds } = emerald.getRenderStats();
|
|
1910
|
+
```
|
|
1911
|
+
|
|
1912
|
+
Draw calls growing with level size means something isn't batched. Use `Tilemap`, `SpriteBatch`, or the level loader's instanced tile path.
|
|
1913
|
+
|
|
1914
|
+
## Serializer (save/load scenes)
|
|
1915
|
+
|
|
1916
|
+
```javascript
|
|
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
|
|
1923
|
+
```
|
|
1924
|
+
|
|
1925
|
+
## Storage (versioned saves)
|
|
1926
|
+
|
|
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):
|
|
1928
|
+
|
|
1929
|
+
```javascript
|
|
1930
|
+
import { Storage } from "emeraldengine";
|
|
1931
|
+
|
|
1932
|
+
// Write: version + timestamp envelope, plus a .bak backup by default.
|
|
1933
|
+
Storage.save("profile", { level: 3, coins: 120 }, { version: 2 });
|
|
1934
|
+
|
|
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
|
+
});
|
|
1945
|
+
|
|
1946
|
+
Storage.hasSave("profile"); // true
|
|
1947
|
+
Storage.removeSave("profile"); // deletes the save AND its backup
|
|
1948
|
+
```
|
|
1949
|
+
|
|
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.
|
|
1951
|
+
|
|
1952
|
+
## EmeraldDB (IndexedDB saves)
|
|
1953
|
+
|
|
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.
|
|
1955
|
+
|
|
1956
|
+
```javascript
|
|
1957
|
+
import { EmeraldDB } from "emeraldengine";
|
|
1958
|
+
|
|
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
|
|
1978
|
+
```
|
|
1979
|
+
|
|
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.
|
|
1985
|
+
|
|
1986
|
+
```javascript
|
|
1987
|
+
import { AssetManager } from "emeraldengine";
|
|
1988
|
+
|
|
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");
|
|
1996
|
+
|
|
1997
|
+
assets.onProgress((loaded, total) => bar.set(loaded / total));
|
|
1998
|
+
await assets.load({ continueOnError: false }); // rejects on a failed asset unless true
|
|
1999
|
+
|
|
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();
|
|
2006
|
+
```
|
|
2007
|
+
|
|
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`).
|
|
2011
|
+
|
|
2012
|
+
```javascript
|
|
2013
|
+
import { TiledMap } from "emeraldengine";
|
|
2014
|
+
|
|
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);
|
|
2019
|
+
|
|
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
|
+
```
|
|
2028
|
+
|
|
2029
|
+
```javascript
|
|
2030
|
+
import { Aseprite, Texture, Animator } from "emeraldengine";
|
|
2031
|
+
|
|
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
|
|
2041
|
+
);
|
|
2042
|
+
gameObject.addComponent(tex);
|
|
2043
|
+
|
|
2044
|
+
const anim = new Animator();
|
|
2045
|
+
gameObject.addComponent(anim);
|
|
2046
|
+
Aseprite.applyTo(anim, sheetJson); // registers a clip per frame-tag
|
|
2047
|
+
anim.play("run");
|
|
2048
|
+
|
|
2049
|
+
// Or inspect the clips yourself (handles forward/reverse/pingpong):
|
|
2050
|
+
Aseprite.toClips(sheetJson); // -> [{ name, frames:[...], speed }]
|
|
2051
|
+
```
|
|
2052
|
+
|
|
2053
|
+
```javascript
|
|
2054
|
+
import { ForgeLevel } from "emeraldengine";
|
|
2055
|
+
|
|
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 }
|
|
2074
|
+
```
|
|
2075
|
+
|
|
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.
|
|
2077
|
+
|
|
2078
|
+
## Networking (NetworkManager + Interpolator)
|
|
2079
|
+
|
|
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.
|
|
2081
|
+
|
|
2082
|
+
```javascript
|
|
2083
|
+
import { NetworkManager } from "emeraldengine";
|
|
2084
|
+
|
|
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" });
|
|
2088
|
+
|
|
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
|
|
2101
|
+
```
|
|
2102
|
+
|
|
2103
|
+
`Interpolator` is also exported standalone and is pure (no network/DOM), so you can use it with any transport or in tests:
|
|
2104
|
+
|
|
2105
|
+
```javascript
|
|
2106
|
+
import { Interpolator } from "emeraldengine";
|
|
2107
|
+
|
|
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();
|
|
2114
|
+
```
|
|
2115
|
+
|
|
2116
|
+
## Advanced Features
|
|
2117
|
+
|
|
2118
|
+
### Resize Handling
|
|
2119
|
+
|
|
2120
|
+
```javascript
|
|
2121
|
+
// Handle window resize
|
|
2122
|
+
const handleResize = () => {
|
|
2123
|
+
const { width, height } = getCanvasDimensions();
|
|
2124
|
+
emerald.resize(width, height);
|
|
2125
|
+
};
|
|
2126
|
+
|
|
2127
|
+
window.addEventListener("resize", handleResize);
|
|
2128
|
+
```
|
|
2129
|
+
|
|
2130
|
+
### Resolution independence
|
|
2131
|
+
|
|
2132
|
+
Author your game at one fixed resolution and let the engine scale it to any screen:
|
|
2133
|
+
|
|
2134
|
+
```javascript
|
|
2135
|
+
// Design at 960x540, letterboxed onto whatever screen the player has:
|
|
2136
|
+
emerald.setDesignResolution(960, 540, "fit");
|
|
2137
|
+
|
|
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);
|
|
2148
|
+
```
|
|
2149
|
+
|
|
2150
|
+
### Auto-pause & lifecycle
|
|
2151
|
+
|
|
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:
|
|
2153
|
+
|
|
2154
|
+
```javascript
|
|
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
|
+
});
|
|
2161
|
+
|
|
2162
|
+
emerald.pause(); // e.g. from your own pause menu
|
|
2163
|
+
emerald.resume();
|
|
2164
|
+
```
|
|
2165
|
+
|
|
2166
|
+
The first `dt` after resuming is clamped (`maxDelta`, default 0.25s), so physics never explodes after a long background stint.
|
|
2167
|
+
|
|
2168
|
+
### Production hardening
|
|
2169
|
+
|
|
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:
|
|
2171
|
+
|
|
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
|
|
2177
|
+
```
|
|
2178
|
+
|
|
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:
|
|
2180
|
+
|
|
2181
|
+
```javascript
|
|
2182
|
+
emerald.onContextLost(() => overlay.show("Recovering graphics..."));
|
|
2183
|
+
emerald.onContextRestored(() => overlay.hide());
|
|
2184
|
+
```
|
|
2185
|
+
|
|
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:
|
|
2187
|
+
|
|
2188
|
+
```javascript
|
|
2189
|
+
emerald.camera.setPixelSnap(true); // rendered position rounds; stored position stays smooth
|
|
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 |
|