bloody-engine 1.0.8 → 1.0.10
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 +483 -43
- package/dist/node/index.js +4 -8
- package/dist/node/networking/state-snapshot.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/node/tests/determinism.test.d.ts +0 -10
- package/dist/node/tests/determinism.test.d.ts.map +0 -1
- package/dist/node/tests/projection.test.d.ts +0 -5
- package/dist/node/tests/projection.test.d.ts.map +0 -1
- package/dist/node/tests/state-sync.test.d.ts +0 -13
- package/dist/node/tests/state-sync.test.d.ts.map +0 -1
- package/dist/node/tests/visual-regression.test.d.ts +0 -13
- package/dist/node/tests/visual-regression.test.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
# Bloody Engine
|
|
2
2
|
|
|
3
|
-
A WebGL-based 2.5D graphics engine for isometric rendering on Node.js, written in TypeScript. Designed for server-side rendering
|
|
3
|
+
A WebGL-based 2.5D graphics engine for isometric rendering on Node.js, written in TypeScript. Designed for server-side rendering, headless graphics processing, and networked multiplayer games.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **2.5D Rendering** - Optimized for isometric and dimetric projections
|
|
7
|
+
- **2.5D Rendering** - Optimized for isometric and dimetric projections with depth sorting
|
|
8
8
|
- **Server-Side Rendering** - Headless WebGL rendering on Node.js using `gl` and `@kmamal/sdl`
|
|
9
9
|
- **Batch Rendering** - Efficient sprite batching with GPU-accelerated transformations
|
|
10
|
-
- **Resource Management** - Unified asset loading pipeline for textures and
|
|
10
|
+
- **Resource Management** - Unified asset loading pipeline for textures and shaders
|
|
11
|
+
- **Input System** - Command queue pattern supporting SDL and network input sources
|
|
12
|
+
- **Networking** - Client-side prediction, server reconciliation, and state synchronization
|
|
13
|
+
- **Simulation** - Pure game logic simulation system with entity management
|
|
14
|
+
- **Game Loop** - Fixed timestep ticker for deterministic game logic
|
|
11
15
|
- **TypeScript** - Fully typed for excellent developer experience
|
|
12
|
-
- **
|
|
16
|
+
- **Object Pooling** - Memory-efficient object reuse patterns
|
|
13
17
|
- **Window Management** - SDL-based window creation for interactive applications
|
|
14
18
|
|
|
15
19
|
## Installation
|
|
@@ -18,15 +22,73 @@ A WebGL-based 2.5D graphics engine for isometric rendering on Node.js, written i
|
|
|
18
22
|
npm install bloody-engine
|
|
19
23
|
```
|
|
20
24
|
|
|
25
|
+
## API Overview
|
|
26
|
+
|
|
27
|
+
### Core Graphics
|
|
28
|
+
|
|
29
|
+
| Class | Description |
|
|
30
|
+
|-------|-------------|
|
|
31
|
+
| [GraphicsDevice](src/core/grahpic-device.ts) | Main graphics device with WebGL context management |
|
|
32
|
+
| [Shader](src/core/shader.ts) | Shader program compilation and uniform/attribute management |
|
|
33
|
+
| [Texture](src/core/texture.ts) | Texture creation, binding, and management |
|
|
34
|
+
| [VertexBuffer](src/core/buffer.ts) / [IndexBuffer](src/core/buffer.ts) | GPU buffer management for geometry |
|
|
35
|
+
| [Camera](src/rendering/camera.ts) | 2D camera with position, zoom, and view matrix |
|
|
36
|
+
|
|
37
|
+
### Rendering
|
|
38
|
+
|
|
39
|
+
| Class | Description |
|
|
40
|
+
|-------|-------------|
|
|
41
|
+
| [BatchRenderer](src/rendering/batch-renderer.ts) | Generic quad batch rendering |
|
|
42
|
+
| [SpriteBatchRenderer](src/rendering/batch-renderer.ts) | Sprite-specific batch renderer with depth sorting |
|
|
43
|
+
| [ProjectionConfig](src/rendering/projection.ts) | Isometric/dimetric projection utilities |
|
|
44
|
+
| [SpatialHash](src/rendering/spatial-hash.ts) | Spatial partitioning for efficient queries |
|
|
45
|
+
|
|
46
|
+
### Resource Loading
|
|
47
|
+
|
|
48
|
+
| Class | Description |
|
|
49
|
+
|-------|-------------|
|
|
50
|
+
| [NodeResourceLoader](src/platforms/node/node-resource-loader.ts) | File system resource loader for Node.js |
|
|
51
|
+
| [NodeTextureLoader](src/platforms/node/node-texture-loader.ts) | PNG texture loading for Node.js |
|
|
52
|
+
| [ResourcePipeline](src/core/resource-pipeline.ts) | Batch resource loading with caching |
|
|
53
|
+
| [TextureAtlas](src/core/sprite-atlas.ts) | Sprite atlas packing and UV coordinate management |
|
|
54
|
+
|
|
55
|
+
### Input System
|
|
56
|
+
|
|
57
|
+
| Class | Description |
|
|
58
|
+
|-------|-------------|
|
|
59
|
+
| [CommandQueue](src/input/command-queue.ts) | Thread-safe command queue for input |
|
|
60
|
+
| [SDLInputSource](src/input/sdl-input-source.ts) | SDL keyboard/mouse input |
|
|
61
|
+
| [NetworkInputSource](src/input/networking-input-source.ts) | Network-based input for multiplayer |
|
|
62
|
+
|
|
63
|
+
### Simulation & Networking
|
|
64
|
+
|
|
65
|
+
| Class | Description |
|
|
66
|
+
|-------|-------------|
|
|
67
|
+
| [Ticker](src/core/ticker.ts) | Fixed timestep game loop |
|
|
68
|
+
| [Entity](src/simulation/entity.ts) / [EntityManager](src/simulation/entity-manager.ts) | Entity component system |
|
|
69
|
+
| [SimulationLoop](src/simulation/simulation-loop.ts) | Deterministic game logic simulation |
|
|
70
|
+
| [ClientPredictor](src/networking/client-predictor.ts) | Client-side prediction for lag compensation |
|
|
71
|
+
| [ServerReconciler](src/networking/server-reconciler.ts) | Server-side reconciliation |
|
|
72
|
+
| [StateSnapshot](src/networking/state-snapshot.ts) | World state serialization |
|
|
73
|
+
| [BinarySerializer](src/networking/binary-serializer.ts) | Efficient binary serialization |
|
|
74
|
+
|
|
75
|
+
### Utilities
|
|
76
|
+
|
|
77
|
+
| Class | Description |
|
|
78
|
+
|-------|-------------|
|
|
79
|
+
| [ObjectPool](src/core/object-pool.ts) | Generic object pooling for GC optimization |
|
|
80
|
+
| [Matrix4Pool](src/core/matrix-pool.ts) | Matrix4 specific pooling |
|
|
81
|
+
| [lerp](src/core/interpolation.ts), [lerpVec2](src/core/interpolation.ts), [lerpVec3](src/core/interpolation.ts) | Interpolation utilities |
|
|
82
|
+
|
|
21
83
|
## Quick Start
|
|
22
84
|
|
|
85
|
+
### Basic Rendering Setup
|
|
86
|
+
|
|
23
87
|
```typescript
|
|
24
|
-
import { GraphicsDevice, Shader, Texture } from 'bloody-engine';
|
|
88
|
+
import { GraphicsDevice, Shader, Texture, VertexBuffer } from 'bloody-engine';
|
|
25
89
|
|
|
26
|
-
// Create
|
|
90
|
+
// Create graphics device (800x600)
|
|
27
91
|
const device = new GraphicsDevice(800, 600);
|
|
28
|
-
|
|
29
|
-
// Get the WebGL context
|
|
30
92
|
const gl = device.getGLContext();
|
|
31
93
|
|
|
32
94
|
// Create a shader
|
|
@@ -46,67 +108,392 @@ const shader = device.createShader(`
|
|
|
46
108
|
}
|
|
47
109
|
`);
|
|
48
110
|
|
|
49
|
-
// Create a texture
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
111
|
+
// Create a gradient texture
|
|
112
|
+
const texture = Texture.createGradient(gl, 256, 256);
|
|
113
|
+
|
|
114
|
+
// Create geometry
|
|
115
|
+
const vertices = new Float32Array([
|
|
116
|
+
// x, y, z, u, v
|
|
117
|
+
-0.5, -0.5, 0, 0, 1,
|
|
118
|
+
0.5, -0.5, 0, 1, 1,
|
|
119
|
+
0.5, 0.5, 0, 1, 0,
|
|
120
|
+
-0.5, -0.5, 0, 0, 1,
|
|
121
|
+
0.5, 0.5, 0, 1, 0,
|
|
122
|
+
-0.5, 0.5, 0, 0, 0
|
|
123
|
+
]);
|
|
124
|
+
const buffer = new VertexBuffer(gl, vertices, 20); // 5 floats * 4 bytes
|
|
55
125
|
|
|
56
|
-
//
|
|
126
|
+
// Setup and render
|
|
57
127
|
device.clear({ r: 0.1, g: 0.1, b: 0.1, a: 1.0 });
|
|
58
128
|
shader.use();
|
|
59
|
-
|
|
129
|
+
buffer.bind();
|
|
130
|
+
// ... configure attributes ...
|
|
131
|
+
gl.drawArrays(gl.TRIANGLES, 0, buffer.getVertexCount());
|
|
60
132
|
device.present();
|
|
61
|
-
|
|
62
|
-
// For headless rendering, capture the output
|
|
63
|
-
const context = device.getRenderingContext();
|
|
64
|
-
const pixels = context.readPixels();
|
|
65
133
|
```
|
|
66
134
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
### Sprite Batch Rendering
|
|
135
|
+
### Sprite Batch Rendering with Camera
|
|
70
136
|
|
|
71
137
|
```typescript
|
|
72
|
-
import { SpriteBatchRenderer, Camera, Texture } from 'bloody-engine';
|
|
138
|
+
import { SpriteBatchRenderer, Camera, Texture, GraphicsDevice } from 'bloody-engine';
|
|
139
|
+
|
|
140
|
+
const device = new GraphicsDevice(800, 600);
|
|
141
|
+
const gl = device.getGLContext();
|
|
73
142
|
|
|
143
|
+
// Create shader (use built-in V2 shader for sprites)
|
|
144
|
+
const shader = device.createShader(vertexSource, fragmentSource);
|
|
145
|
+
|
|
146
|
+
// Create sprite batch renderer (capacity: 1000 sprites)
|
|
74
147
|
const batchRenderer = new SpriteBatchRenderer(gl, shader, 1000);
|
|
75
|
-
|
|
148
|
+
batchRenderer.setTexture(Texture.createGradient(gl, 256, 256));
|
|
149
|
+
|
|
150
|
+
// Create camera
|
|
151
|
+
const camera = new Camera(0, 0, 1.0); // x=0, y=0, zoom=1x
|
|
76
152
|
|
|
77
|
-
// Add sprites
|
|
153
|
+
// Add sprites to batch
|
|
78
154
|
batchRenderer.addQuad({
|
|
79
|
-
x: 100,
|
|
80
|
-
|
|
81
|
-
z: 0,
|
|
82
|
-
width: 64,
|
|
83
|
-
height: 64,
|
|
155
|
+
x: 100, y: 100, z: 0,
|
|
156
|
+
width: 64, height: 64,
|
|
84
157
|
rotation: 0,
|
|
85
158
|
color: { r: 1, g: 1, b: 1, a: 1 },
|
|
86
|
-
|
|
159
|
+
texIndex: 0
|
|
87
160
|
});
|
|
88
161
|
|
|
89
|
-
// Render
|
|
162
|
+
// Render with camera
|
|
163
|
+
device.clear({ r: 0.1, g: 0.1, b: 0.1, a: 1.0 });
|
|
90
164
|
batchRenderer.render(camera);
|
|
165
|
+
device.present();
|
|
91
166
|
```
|
|
92
167
|
|
|
93
168
|
### Resource Loading
|
|
94
169
|
|
|
95
170
|
```typescript
|
|
96
|
-
import {
|
|
97
|
-
|
|
98
|
-
|
|
171
|
+
import {
|
|
172
|
+
ResourceLoaderFactory,
|
|
173
|
+
createResourcePipeline,
|
|
174
|
+
NodeTextureLoader
|
|
175
|
+
} from 'bloody-engine';
|
|
176
|
+
|
|
177
|
+
// Create resource pipeline
|
|
178
|
+
const pipeline = await createResourcePipeline({
|
|
179
|
+
concurrency: 5,
|
|
180
|
+
cache: true,
|
|
181
|
+
baseDir: process.cwd()
|
|
182
|
+
});
|
|
99
183
|
|
|
100
|
-
// Load
|
|
101
|
-
const
|
|
102
|
-
|
|
184
|
+
// Load shaders
|
|
185
|
+
const shaders = await pipeline.loadShaders([
|
|
186
|
+
{ name: 'basic', vertex: 'shaders/basic.vert', fragment: 'shaders/basic.frag' }
|
|
187
|
+
]);
|
|
103
188
|
|
|
104
|
-
// Batch load
|
|
105
|
-
const { succeeded, failed } = await
|
|
189
|
+
// Batch load resources
|
|
190
|
+
const { succeeded, failed } = await pipeline.loadMultiple([
|
|
106
191
|
'textures/sprite1.png',
|
|
107
|
-
'textures/sprite2.png'
|
|
108
|
-
'shaders/shader.vert'
|
|
192
|
+
'textures/sprite2.png'
|
|
109
193
|
]);
|
|
194
|
+
|
|
195
|
+
// Load texture from PNG
|
|
196
|
+
const textureLoader = new NodeTextureLoader();
|
|
197
|
+
const texture = await textureLoader.loadTexture(gl, 'textures/sprite.png');
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Game Loop with Fixed Timestep
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { Ticker, type TickerConfig } from 'bloody-engine';
|
|
204
|
+
|
|
205
|
+
const config: TickerConfig = {
|
|
206
|
+
targetFPS: 60,
|
|
207
|
+
fixedDeltaTime: 1 / 60, // 60 physics updates per second
|
|
208
|
+
maxFrameTime: 0.25 // Prevent spiral of death
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const ticker = new Ticker(config);
|
|
212
|
+
|
|
213
|
+
ticker.start({
|
|
214
|
+
update: (deltaTime) => {
|
|
215
|
+
// Game logic update (fixed timestep)
|
|
216
|
+
console.log(`Update: ${deltaTime.toFixed(3)}s`);
|
|
217
|
+
},
|
|
218
|
+
render: (interpolation) => {
|
|
219
|
+
// Render with interpolation factor
|
|
220
|
+
console.log(`Render: interpolation=${interpolation.toFixed(3)}`);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Get performance metrics
|
|
225
|
+
const metrics = ticker.getMetrics();
|
|
226
|
+
console.log(`FPS: ${metrics.fps}, Delta Time: ${metrics.deltaTime}s`);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Entity System
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { Entity, EntityManager, type EntityState } from 'bloody-engine';
|
|
233
|
+
|
|
234
|
+
// Create entity
|
|
235
|
+
const entity = new Entity({
|
|
236
|
+
id: 'player-1',
|
|
237
|
+
x: 0, y: 0, z: 0,
|
|
238
|
+
rotation: 0,
|
|
239
|
+
type: 'player',
|
|
240
|
+
health: 100
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Create entity manager
|
|
244
|
+
const manager = new EntityManager();
|
|
245
|
+
manager.add(entity);
|
|
246
|
+
|
|
247
|
+
// Query entities
|
|
248
|
+
const players = manager.query({ type: 'player' });
|
|
249
|
+
const nearby = manager.queryInRegion(0, 0, 100);
|
|
250
|
+
|
|
251
|
+
// Update entity
|
|
252
|
+
entity.x += 10;
|
|
253
|
+
entity.updatedAt = Date.now();
|
|
254
|
+
manager.update(entity);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Input System with Command Queue
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import {
|
|
261
|
+
CommandQueue,
|
|
262
|
+
SDLInputSource,
|
|
263
|
+
createSDLInputSource,
|
|
264
|
+
CommandType
|
|
265
|
+
} from 'bloody-engine';
|
|
266
|
+
|
|
267
|
+
// Create command queue
|
|
268
|
+
const queue = new CommandQueue();
|
|
269
|
+
|
|
270
|
+
// Create SDL input source (requires SDL window)
|
|
271
|
+
const sdlWindow = new SDLWindow(800, 600, 'Game');
|
|
272
|
+
const inputSource = createSDLInputSource(sdlWindow, {
|
|
273
|
+
keyMapping: {
|
|
274
|
+
moveUp: ['w', 'arrowup'],
|
|
275
|
+
moveDown: ['s', 'arrowdown'],
|
|
276
|
+
moveLeft: ['a', 'arrowleft'],
|
|
277
|
+
moveRight: ['d', 'arrowright']
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Process input in game loop
|
|
282
|
+
while (running) {
|
|
283
|
+
// Collect input commands
|
|
284
|
+
inputSource.update(queue);
|
|
285
|
+
|
|
286
|
+
// Process commands
|
|
287
|
+
while (queue.hasCommands()) {
|
|
288
|
+
const command = queue.dequeue();
|
|
289
|
+
switch (command.type) {
|
|
290
|
+
case CommandType.Move:
|
|
291
|
+
handleMove(command);
|
|
292
|
+
break;
|
|
293
|
+
case CommandType.Attack:
|
|
294
|
+
handleAttack(command);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Networking - Client-Side Prediction
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import {
|
|
305
|
+
createClientPredictor,
|
|
306
|
+
ClientPredictor,
|
|
307
|
+
type ClientInputMessage
|
|
308
|
+
} from 'bloody-engine';
|
|
309
|
+
|
|
310
|
+
// Create predictor with config
|
|
311
|
+
const predictor = createClientPredictor({
|
|
312
|
+
maxPredictedTicks: 100,
|
|
313
|
+
reconciliationDelay: 100 // ms
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Client loop: send input
|
|
317
|
+
const onInput = (input: MoveCommand) => {
|
|
318
|
+
const tick = currentTick;
|
|
319
|
+
predictor.addLocalInput(tick, input);
|
|
320
|
+
|
|
321
|
+
// Send to server
|
|
322
|
+
socket.send(JSON.stringify({
|
|
323
|
+
type: 'client_input',
|
|
324
|
+
tick,
|
|
325
|
+
input
|
|
326
|
+
} as ClientInputMessage));
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Receive server update
|
|
330
|
+
const onServerUpdate = (message: ServerStateUpdateMessage) => {
|
|
331
|
+
const result = predictor.reconcile(message);
|
|
332
|
+
|
|
333
|
+
if (result.corrected) {
|
|
334
|
+
console.log(`Reconciled: corrected=${result.corrected}, error=${result.error}`);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Object Pooling for Performance
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { ObjectPool, type ObjectPoolConfig } from 'bloody-engine';
|
|
343
|
+
|
|
344
|
+
// Create pool for Vector3 objects
|
|
345
|
+
const pool = new ObjectPool<Vector3>({
|
|
346
|
+
initialSize: 100,
|
|
347
|
+
growthFactor: 2,
|
|
348
|
+
factory: () => ({ x: 0, y: 0, z: 0 }),
|
|
349
|
+
reset: (obj) => { obj.x = 0; obj.y = 0; obj.z = 0; }
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Acquire from pool
|
|
353
|
+
const vec = pool.acquire();
|
|
354
|
+
vec.x = 10; vec.y = 20; vec.z = 30;
|
|
355
|
+
|
|
356
|
+
// Return to pool when done
|
|
357
|
+
pool.release(vec);
|
|
358
|
+
|
|
359
|
+
// Get pool statistics
|
|
360
|
+
const stats = pool.getStats();
|
|
361
|
+
console.log(`Size: ${stats.size}, Active: ${stats.active}, Hits: ${stats.hits}`);
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Isometric Projection
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { ProjectionConfig, gridToScreen, screenToGrid } from 'bloody-engine';
|
|
368
|
+
|
|
369
|
+
// Configure isometric projection
|
|
370
|
+
const config = new ProjectionConfig({
|
|
371
|
+
tileWidth: 64,
|
|
372
|
+
tileHeight: 32,
|
|
373
|
+
angle: Math.PI / 6, // 30 degrees
|
|
374
|
+
screenWidth: 800,
|
|
375
|
+
screenHeight: 600
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Convert grid to screen coordinates
|
|
379
|
+
const gridPos = { xgrid: 5, ygrid: 3, zheight: 0 };
|
|
380
|
+
const screenPos = gridToScreen(gridPos, config);
|
|
381
|
+
console.log(`Screen: x=${screenPos.xscreen}, y=${screenPos.yscreen}`);
|
|
382
|
+
|
|
383
|
+
// Convert screen to grid coordinates
|
|
384
|
+
const gridPos2 = screenToGrid(screenPos, config);
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Texture Atlas for Sprite Sheets
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { TextureAtlas, AtlasLoader } from 'bloody-engine';
|
|
391
|
+
|
|
392
|
+
// Load sprite atlas
|
|
393
|
+
const atlas = await AtlasLoader.loadFromJSON(gl, 'atlas.json');
|
|
394
|
+
|
|
395
|
+
// Get sprite info
|
|
396
|
+
const sprite = atlas.getSprite('player_idle_01');
|
|
397
|
+
|
|
398
|
+
// Use UV rect for rendering
|
|
399
|
+
batchRenderer.addQuad({
|
|
400
|
+
x: 100, y: 100, z: 0,
|
|
401
|
+
width: sprite.pixelRect.width,
|
|
402
|
+
height: sprite.pixelRect.height,
|
|
403
|
+
uvRect: sprite.uvRect
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Advanced Examples
|
|
408
|
+
|
|
409
|
+
### Networked Game Architecture
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import {
|
|
413
|
+
SimulationLoop,
|
|
414
|
+
Entity,
|
|
415
|
+
ClientPredictor,
|
|
416
|
+
ServerReconciler,
|
|
417
|
+
StateSnapshot,
|
|
418
|
+
Ticker
|
|
419
|
+
} from 'bloody-engine';
|
|
420
|
+
|
|
421
|
+
// Server-side simulation
|
|
422
|
+
const serverSim = new SimulationLoop({
|
|
423
|
+
fixedDeltaTime: 1 / 60
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Client-side prediction
|
|
427
|
+
const clientPredictor = createClientPredictor({
|
|
428
|
+
maxPredictedTicks: 100
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Server reconciliation
|
|
432
|
+
const serverReconciler = createServerReconciler({
|
|
433
|
+
maxRewindTicks: 50
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Game loop on client
|
|
437
|
+
const ticker = new Ticker({ targetFPS: 60 });
|
|
438
|
+
ticker.start({
|
|
439
|
+
update: (deltaTime) => {
|
|
440
|
+
// 1. Collect input and send to server
|
|
441
|
+
const input = collectInput();
|
|
442
|
+
socket.send({ type: 'input', input, tick: currentTick });
|
|
443
|
+
|
|
444
|
+
// 2. Predict locally
|
|
445
|
+
clientPredictor.addLocalInput(currentTick, input);
|
|
446
|
+
const predictedState = predictState();
|
|
447
|
+
|
|
448
|
+
// 3. Handle server updates
|
|
449
|
+
onServerUpdate = (update) => {
|
|
450
|
+
clientPredictor.reconcile(update);
|
|
451
|
+
};
|
|
452
|
+
},
|
|
453
|
+
render: (interpolation) => {
|
|
454
|
+
renderGame(clientPredictor.getLatestState(), interpolation);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Deterministic Simulation Testing
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { SimulationLoop, Entity } from 'bloody-engine';
|
|
463
|
+
|
|
464
|
+
// Create two simulations for testing determinism
|
|
465
|
+
const sim1 = new SimulationLoop({ fixedDeltaTime: 1 / 60, seed: 12345 });
|
|
466
|
+
const sim2 = new SimulationLoop({ fixedDeltaTime: 1 / 60, seed: 12345 });
|
|
467
|
+
|
|
468
|
+
// Add identical entities
|
|
469
|
+
sim1.addEntity(new Entity({ id: '1', x: 0, y: 0 }));
|
|
470
|
+
sim2.addEntity(new Entity({ id: '1', x: 0, y: 0 }));
|
|
471
|
+
|
|
472
|
+
// Run simulations
|
|
473
|
+
for (let i = 0; i < 1000; i++) {
|
|
474
|
+
sim1.update(1 / 60);
|
|
475
|
+
sim2.update(1 / 60);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Verify determinism
|
|
479
|
+
const state1 = sim1.getStateSnapshot();
|
|
480
|
+
const state2 = sim2.getStateSnapshot();
|
|
481
|
+
console.log('Deterministic:', JSON.stringify(state1) === JSON.stringify(state2));
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
## Testing
|
|
485
|
+
|
|
486
|
+
The engine includes comprehensive tests for determinism and visual regression:
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
# Run all tests
|
|
490
|
+
npm test
|
|
491
|
+
|
|
492
|
+
# Run specific test suites
|
|
493
|
+
npm run test:determinism # Test simulation determinism
|
|
494
|
+
npm run test:visual # Visual regression tests
|
|
495
|
+
npm run test:state-sync # State synchronization tests
|
|
496
|
+
npm run test:coverage # Generate coverage report
|
|
110
497
|
```
|
|
111
498
|
|
|
112
499
|
## Dependencies
|
|
@@ -115,8 +502,61 @@ const { succeeded, failed } = await loader.loadMultiple([
|
|
|
115
502
|
- **@kmamal/sdl** - SDL2 bindings for window and input management
|
|
116
503
|
- **pngjs** - PNG image decoding
|
|
117
504
|
|
|
505
|
+
## Platform Support
|
|
506
|
+
|
|
507
|
+
| Platform | Status | Notes |
|
|
508
|
+
|----------|--------|-------|
|
|
509
|
+
| Node.js (Linux) | ✅ Full | Headless rendering + SDL window |
|
|
510
|
+
| Node.js (macOS) | ✅ Full | Headless rendering + SDL window |
|
|
511
|
+
| Node.js (Windows) | ✅ Full | Headless rendering + SDL window |
|
|
512
|
+
| Browser | ⚠️ Planned | WebGL rendering planned |
|
|
513
|
+
|
|
118
514
|
## Documentation
|
|
119
515
|
|
|
516
|
+
### Source Code Organization
|
|
517
|
+
|
|
518
|
+
```
|
|
519
|
+
src/
|
|
520
|
+
├── core/ # Core graphics and utilities
|
|
521
|
+
│ ├── grahpic-device.ts
|
|
522
|
+
│ ├── shader.ts
|
|
523
|
+
│ ├── texture.ts
|
|
524
|
+
│ ├── buffer.ts
|
|
525
|
+
│ ├── object-pool.ts
|
|
526
|
+
│ └── ticker.ts
|
|
527
|
+
├── rendering/ # Rendering systems
|
|
528
|
+
│ ├── batch-renderer.ts
|
|
529
|
+
│ ├── camera.ts
|
|
530
|
+
│ ├── projection.ts
|
|
531
|
+
│ └── spatial-hash.ts
|
|
532
|
+
├── input/ # Input system (command queue)
|
|
533
|
+
│ ├── command-queue.ts
|
|
534
|
+
│ ├── sdl-input-source.ts
|
|
535
|
+
│ └── network-input-source.ts
|
|
536
|
+
├── simulation/ # Game logic simulation
|
|
537
|
+
│ ├── entity.ts
|
|
538
|
+
│ ├── entity-manager.ts
|
|
539
|
+
│ └── simulation-loop.ts
|
|
540
|
+
├── networking/ # Networking for multiplayer
|
|
541
|
+
│ ├── client-predictor.ts
|
|
542
|
+
│ ├── server-reconciler.ts
|
|
543
|
+
│ ├── state-snapshot.ts
|
|
544
|
+
│ └── binary-serializer.ts
|
|
545
|
+
└── platforms/
|
|
546
|
+
└── node/ # Node.js-specific implementations
|
|
547
|
+
├── node-context.ts
|
|
548
|
+
├── node-resource-loader.ts
|
|
549
|
+
└── sdl-window.ts
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Key Concepts
|
|
553
|
+
|
|
554
|
+
- **Separation of Concerns**: Rendering, input, simulation, and networking are completely separate systems
|
|
555
|
+
- **Deterministic Simulation**: Game logic runs in fixed timestep for consistency across clients
|
|
556
|
+
- **Command Pattern**: All input goes through a command queue for easy recording/replay
|
|
557
|
+
- **Client-Side Prediction**: Reduces perceived lag in networked games
|
|
558
|
+
- **Object Pooling**: Minimizes garbage collection for smooth performance
|
|
559
|
+
|
|
120
560
|
For detailed documentation and architecture, see [docs/README.MD](docs/README.MD).
|
|
121
561
|
|
|
122
562
|
## Building
|
package/dist/node/index.js
CHANGED
|
@@ -5237,15 +5237,13 @@ class StateSnapshot {
|
|
|
5237
5237
|
* Serialize snapshot to binary format
|
|
5238
5238
|
*/
|
|
5239
5239
|
toBinary() {
|
|
5240
|
-
const
|
|
5241
|
-
const serializer = new BinarySerializer2();
|
|
5240
|
+
const serializer = new BinarySerializer();
|
|
5242
5241
|
serializer.writeUint32(this.tick);
|
|
5243
5242
|
serializer.writeFloat64(this.timestamp);
|
|
5244
5243
|
serializer.writeUint16(this.entities.size);
|
|
5245
5244
|
for (const [entityId, state] of this.entities.entries()) {
|
|
5246
5245
|
serializer.writeString(entityId);
|
|
5247
|
-
const
|
|
5248
|
-
const stateData = EntitySerializer2.serializeEntityState(state);
|
|
5246
|
+
const stateData = EntitySerializer.serializeEntityState(state);
|
|
5249
5247
|
serializer.writeBytes(stateData);
|
|
5250
5248
|
}
|
|
5251
5249
|
return serializer.toBuffer();
|
|
@@ -5254,9 +5252,7 @@ class StateSnapshot {
|
|
|
5254
5252
|
* Deserialize snapshot from binary format
|
|
5255
5253
|
*/
|
|
5256
5254
|
static fromBinary(data) {
|
|
5257
|
-
const
|
|
5258
|
-
const { EntitySerializer: EntitySerializer2 } = require("./entity-serializer");
|
|
5259
|
-
const reader = new BinaryReader2(data);
|
|
5255
|
+
const reader = new BinaryReader(data);
|
|
5260
5256
|
const tick = reader.readUint32();
|
|
5261
5257
|
const timestamp = reader.readFloat64();
|
|
5262
5258
|
const entityCount = reader.readUint16();
|
|
@@ -5267,7 +5263,7 @@ class StateSnapshot {
|
|
|
5267
5263
|
reader.setOffset(reader.getOffset() - 1);
|
|
5268
5264
|
const stateDataLength = StateSnapshot.estimateEntityStateSize();
|
|
5269
5265
|
const actualData = reader.readBytes(stateDataLength);
|
|
5270
|
-
const state =
|
|
5266
|
+
const state = EntitySerializer.deserializeEntityState(actualData);
|
|
5271
5267
|
entities.set(entityId, state);
|
|
5272
5268
|
}
|
|
5273
5269
|
return new StateSnapshot(tick, entities, timestamp);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-snapshot.d.ts","sourceRoot":"","sources":["../../../src/networking/state-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"state-snapshot.d.ts","sourceRoot":"","sources":["../../../src/networking/state-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAI3D;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,qBAAa,aAAc,YAAW,mBAAmB;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC5C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM;IAMhF;;OAEG;IACH,MAAM,CAAC,YAAY,CACjB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAAE,EAClB,OAAO,GAAE,eAAoB,GAC5B,aAAa;IAsBhB;;OAEG;IACH,MAAM,CAAC,OAAO,CACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EACnC,SAAS,CAAC,EAAE,MAAM,GACjB,aAAa;IAchB;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAczD;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIpC;;OAEG;IACH,YAAY,IAAI,MAAM,EAAE;IAIxB;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;OAEG;IACH,OAAO,IAAI,OAAO;IAIlB;;OAEG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,aAAa;IAe1C;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,aAAa,GAAG,aAAa;IA4B1C;;OAEG;IACH,KAAK,IAAI,aAAa;IAItB;;OAEG;IACH,OAAO,IAAI,mBAAmB;IAQ9B;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,mBAAmB,GAAG,aAAa;IAI3D;;OAEG;IACH,QAAQ,IAAI,UAAU;IAyBtB;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,aAAa;IA8BlD;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAKtC;;OAEG;IACH,QAAQ,IAAI,MAAM;IAIlB;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO;CA+BtC"}
|
package/package.json
CHANGED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Determinism Test for Bloody Engine
|
|
3
|
-
*
|
|
4
|
-
* This test verifies that the simulation is deterministic:
|
|
5
|
-
* - Runs two instances of the SimulationLoop with the same input seed
|
|
6
|
-
* - Compares the state of all entities after 1000 ticks
|
|
7
|
-
* - They must match perfectly
|
|
8
|
-
*/
|
|
9
|
-
export {};
|
|
10
|
-
//# sourceMappingURL=determinism.test.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"determinism.test.d.ts","sourceRoot":"","sources":["../../../src/tests/determinism.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"projection.test.d.ts","sourceRoot":"","sources":["../../../src/tests/projection.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* State Synchronization Integration Test
|
|
3
|
-
*
|
|
4
|
-
* Tests the complete state synchronization pipeline:
|
|
5
|
-
* 1. Binary serialization
|
|
6
|
-
* 2. State snapshots
|
|
7
|
-
* 3. Client-side prediction
|
|
8
|
-
* 4. Server reconciliation
|
|
9
|
-
*
|
|
10
|
-
* Run with: npm run test:state-sync
|
|
11
|
-
*/
|
|
12
|
-
export {};
|
|
13
|
-
//# sourceMappingURL=state-sync.test.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"state-sync.test.d.ts","sourceRoot":"","sources":["../../../src/tests/state-sync.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Visual Regression Test for Bloody Engine
|
|
3
|
-
*
|
|
4
|
-
* This test:
|
|
5
|
-
* 1. Initializes the engine in headless mode
|
|
6
|
-
* 2. Loads a test scene
|
|
7
|
-
* 3. Renders one frame
|
|
8
|
-
* 4. Captures the framebuffer using gl.readPixels
|
|
9
|
-
* 5. Saves as output.png
|
|
10
|
-
* 6. Compares against reference.png (if exists)
|
|
11
|
-
*/
|
|
12
|
-
export {};
|
|
13
|
-
//# sourceMappingURL=visual-regression.test.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"visual-regression.test.d.ts","sourceRoot":"","sources":["../../../src/tests/visual-regression.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|