@lagless/create 0.0.44 → 0.0.48
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/dist/index.js +15 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/pixi-react/AGENTS.md +1 -2
- package/templates/pixi-react/CLAUDE.md +3 -0
- package/templates/pixi-react/__packageName__-frontend/package.json +3 -2
- package/templates/pixi-react/__packageName__-frontend/src/app/components/debug-panel.tsx +0 -5
- package/templates/pixi-react/__packageName__-frontend/src/app/game-view/grid-background.tsx +28 -3
- package/templates/pixi-react/__packageName__-frontend/src/app/game-view/runner-provider.tsx +34 -40
- package/templates/pixi-react/__packageName__-simulation/src/lib/schema/ecs.yaml +0 -18
- package/templates/pixi-react/__packageName__-simulation/src/lib/signals/index.ts +2 -4
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/apply-move-input.system.ts +9 -2
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/index.ts +0 -2
- package/templates/pixi-react/docs/08-physics2d.md +53 -39
- package/templates/pixi-react/docs/08-physics3d.md +60 -33
- package/templates/pixi-react/docs/09-recipes.md +60 -0
- package/templates/pixi-react/docs/10-common-mistakes.md +4 -9
- package/templates/pixi-react/docs/11-2d-map-generation.md +707 -0
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/hash-verification.system.ts +0 -17
|
@@ -35,12 +35,13 @@ BodyType.KINEMATIC_VELOCITY // 3 — moved by setting velocity directly
|
|
|
35
35
|
|
|
36
36
|
## Creating Bodies and Colliders
|
|
37
37
|
|
|
38
|
-
Use `PhysicsWorldManager2d` to create physics bodies
|
|
38
|
+
Use `PhysicsWorldManager2d` to create physics bodies. The manager provides factory methods that return Rapier body/collider objects. You configure them via Rapier's native API, then store handles in the ECS `PhysicsRefs` component and register the collider for entity lookup.
|
|
39
39
|
|
|
40
40
|
```typescript
|
|
41
|
-
import { ECSSystem, IECSSystem } from '@lagless/core';
|
|
41
|
+
import { ECSSystem, IECSSystem, EntitiesManager } from '@lagless/core';
|
|
42
42
|
import { PhysicsWorldManager2d } from '@lagless/physics2d';
|
|
43
43
|
import { BodyType, CollisionLayers } from '@lagless/physics-shared';
|
|
44
|
+
import { Transform2d, PhysicsRefs } from '../code-gen/core.js';
|
|
44
45
|
|
|
45
46
|
@ECSSystem()
|
|
46
47
|
export class SpawnSystem implements IECSSystem {
|
|
@@ -48,6 +49,7 @@ export class SpawnSystem implements IECSSystem {
|
|
|
48
49
|
private readonly _physics: PhysicsWorldManager2d,
|
|
49
50
|
private readonly _entities: EntitiesManager,
|
|
50
51
|
private readonly _transform: Transform2d,
|
|
52
|
+
private readonly _physicsRefs: PhysicsRefs,
|
|
51
53
|
) {}
|
|
52
54
|
|
|
53
55
|
update(tick: number): void {
|
|
@@ -56,45 +58,60 @@ export class SpawnSystem implements IECSSystem {
|
|
|
56
58
|
this._entities.addComponent(entity, Transform2d);
|
|
57
59
|
this._entities.addComponent(entity, PhysicsRefs);
|
|
58
60
|
|
|
59
|
-
// Set initial position
|
|
60
|
-
this._transform.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
61
|
+
// Set initial position in ECS (including prev for interpolation)
|
|
62
|
+
const t = this._transform.unsafe;
|
|
63
|
+
t.positionX[entity] = 100;
|
|
64
|
+
t.positionY[entity] = 200;
|
|
65
|
+
t.prevPositionX[entity] = 100;
|
|
66
|
+
t.prevPositionY[entity] = 200;
|
|
67
|
+
t.rotation[entity] = 0;
|
|
68
|
+
t.prevRotation[entity] = 0;
|
|
69
|
+
|
|
70
|
+
// Create a dynamic Rapier body and configure it
|
|
71
|
+
const body = this._physics.createDynamicBody();
|
|
72
|
+
body.setTranslation({ x: 100, y: 200 }, true);
|
|
73
|
+
body.setLinearDamping(5.0);
|
|
74
|
+
|
|
75
|
+
// Create a ball collider attached to the body
|
|
76
|
+
const groups = CollisionLayers.get('player');
|
|
77
|
+
const collider = this._physics.createBallCollider(20, body, groups);
|
|
78
|
+
|
|
79
|
+
// Store handles in ECS for later lookup
|
|
80
|
+
const pr = this._physicsRefs.unsafe;
|
|
81
|
+
pr.bodyHandle[entity] = body.handle;
|
|
82
|
+
pr.colliderHandle[entity] = collider.handle;
|
|
83
|
+
pr.bodyType[entity] = BodyType.DYNAMIC;
|
|
84
|
+
pr.collisionLayer[entity] = groups;
|
|
85
|
+
|
|
86
|
+
// Register collider→entity mapping (used by collision events)
|
|
87
|
+
this._physics.registerCollider(collider.handle, entity);
|
|
80
88
|
}
|
|
81
89
|
}
|
|
82
90
|
```
|
|
83
91
|
|
|
84
92
|
### Collider Shapes
|
|
85
93
|
|
|
94
|
+
All collider factories take an optional `parent` body, `groups` (collision groups), and `activeEvents` bitmask:
|
|
95
|
+
|
|
86
96
|
```typescript
|
|
87
97
|
// Circle
|
|
88
|
-
|
|
98
|
+
this._physics.createBallCollider(radius, parent, groups, activeEvents);
|
|
89
99
|
|
|
90
|
-
// Rectangle
|
|
91
|
-
|
|
100
|
+
// Rectangle (half-extents)
|
|
101
|
+
this._physics.createCuboidCollider(hx, hy, parent, groups, activeEvents);
|
|
92
102
|
|
|
93
103
|
// Capsule
|
|
94
|
-
|
|
104
|
+
this._physics.createCapsuleCollider(halfHeight, radius, parent, groups, activeEvents);
|
|
105
|
+
|
|
106
|
+
// Convex polygon (returns null if hull computation fails)
|
|
107
|
+
this._physics.createConvexHullCollider(new Float32Array([x1,y1, x2,y2, ...]), parent, groups, activeEvents);
|
|
95
108
|
|
|
96
|
-
//
|
|
97
|
-
|
|
109
|
+
// Triangle mesh (static geometry only)
|
|
110
|
+
this._physics.createTrimeshCollider(vertices, indices, parent, groups, activeEvents);
|
|
111
|
+
|
|
112
|
+
// Custom collider from a Rapier ColliderDesc
|
|
113
|
+
const desc = this._physics.rapier.ColliderDesc.ball(10).setDensity(2.0).setFriction(0.5);
|
|
114
|
+
this._physics.createColliderFromDesc(desc, parent);
|
|
98
115
|
```
|
|
99
116
|
|
|
100
117
|
## Collision Layers
|
|
@@ -199,9 +216,7 @@ You generally don't interact with this directly — it's managed by `PhysicsWorl
|
|
|
199
216
|
On rollback:
|
|
200
217
|
1. ArrayBuffer is restored → ECS state reverts
|
|
201
218
|
2. Rapier world snapshot is restored → physics state reverts
|
|
202
|
-
3. `
|
|
203
|
-
|
|
204
|
-
**Critical fix applied:** `World.restoreSnapshot()` creates a world with an **empty** QueryPipeline (not serialized). The framework calls `updateSceneQueries()` after restore to fix this. Without it, ray casts and shape casts fail on the first tick after rollback.
|
|
219
|
+
3. `ColliderEntityMap` is rebuilt automatically
|
|
205
220
|
|
|
206
221
|
## State Transfer
|
|
207
222
|
|
|
@@ -217,7 +232,7 @@ This is handled automatically by the physics runner. You don't need to do anythi
|
|
|
217
232
|
```typescript
|
|
218
233
|
import { ECSSystem, IECSSystem, AbstractInputProvider, ECSConfig, EntitiesManager } from '@lagless/core';
|
|
219
234
|
import { MathOps } from '@lagless/math';
|
|
220
|
-
import { PhysicsWorldManager2d
|
|
235
|
+
import { PhysicsWorldManager2d } from '@lagless/physics2d';
|
|
221
236
|
import { BodyType } from '@lagless/physics-shared';
|
|
222
237
|
import { Transform2d, PhysicsRefs, PlayerBody, PlayerFilter, MoveInput } from '../code-gen/core.js';
|
|
223
238
|
|
|
@@ -228,9 +243,9 @@ export class ApplyMoveInputSystem implements IECSSystem {
|
|
|
228
243
|
constructor(
|
|
229
244
|
private readonly _input: AbstractInputProvider,
|
|
230
245
|
private readonly _physics: PhysicsWorldManager2d,
|
|
246
|
+
private readonly _physicsRefs: PhysicsRefs,
|
|
231
247
|
private readonly _playerBody: PlayerBody,
|
|
232
248
|
private readonly _filter: PlayerFilter,
|
|
233
|
-
private readonly _config: ECSConfig,
|
|
234
249
|
) {}
|
|
235
250
|
|
|
236
251
|
update(tick: number): void {
|
|
@@ -244,11 +259,10 @@ export class ApplyMoveInputSystem implements IECSSystem {
|
|
|
244
259
|
if (this._playerBody.unsafe.playerSlot[entity] !== slot) continue;
|
|
245
260
|
|
|
246
261
|
const speed = 300;
|
|
247
|
-
//
|
|
248
|
-
this._physics.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
});
|
|
262
|
+
// Get the Rapier body via its handle stored in PhysicsRefs
|
|
263
|
+
const body = this._physics.getBody(this._physicsRefs.unsafe.bodyHandle[entity]);
|
|
264
|
+
// Apply velocity directly on the Rapier body
|
|
265
|
+
body.setLinvel({ x: dirX * speed, y: dirY * speed }, true);
|
|
252
266
|
break;
|
|
253
267
|
}
|
|
254
268
|
}
|
|
@@ -37,9 +37,13 @@ BodyType.KINEMATIC_VELOCITY // 3 — moved by setting velocity
|
|
|
37
37
|
|
|
38
38
|
## Creating Bodies and Colliders
|
|
39
39
|
|
|
40
|
+
The manager provides factory methods that return Rapier body/collider objects. You configure them via Rapier's native API, then store handles in the ECS `PhysicsRefs` component and register the collider for entity lookup.
|
|
41
|
+
|
|
40
42
|
```typescript
|
|
43
|
+
import { ECSSystem, IECSSystem, EntitiesManager } from '@lagless/core';
|
|
41
44
|
import { PhysicsWorldManager3d } from '@lagless/physics3d';
|
|
42
45
|
import { BodyType, CollisionLayers } from '@lagless/physics-shared';
|
|
46
|
+
import { Transform3d, PhysicsRefs } from '../code-gen/core.js';
|
|
43
47
|
|
|
44
48
|
@ECSSystem()
|
|
45
49
|
export class SpawnSystem implements IECSSystem {
|
|
@@ -47,6 +51,7 @@ export class SpawnSystem implements IECSSystem {
|
|
|
47
51
|
private readonly _physics: PhysicsWorldManager3d,
|
|
48
52
|
private readonly _entities: EntitiesManager,
|
|
49
53
|
private readonly _transform: Transform3d,
|
|
54
|
+
private readonly _physicsRefs: PhysicsRefs,
|
|
50
55
|
) {}
|
|
51
56
|
|
|
52
57
|
update(tick: number): void {
|
|
@@ -54,50 +59,74 @@ export class SpawnSystem implements IECSSystem {
|
|
|
54
59
|
this._entities.addComponent(entity, Transform3d);
|
|
55
60
|
this._entities.addComponent(entity, PhysicsRefs);
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
});
|
|
62
|
+
// Set initial position in ECS (including prev for interpolation)
|
|
63
|
+
const t = this._transform.unsafe;
|
|
64
|
+
t.positionX[entity] = 0;
|
|
65
|
+
t.positionY[entity] = 5;
|
|
66
|
+
t.positionZ[entity] = 0;
|
|
67
|
+
t.rotationX[entity] = 0;
|
|
68
|
+
t.rotationY[entity] = 0;
|
|
69
|
+
t.rotationZ[entity] = 0;
|
|
70
|
+
t.rotationW[entity] = 1;
|
|
71
|
+
t.prevPositionX[entity] = 0;
|
|
72
|
+
t.prevPositionY[entity] = 5;
|
|
73
|
+
t.prevPositionZ[entity] = 0;
|
|
74
|
+
t.prevRotationX[entity] = 0;
|
|
75
|
+
t.prevRotationY[entity] = 0;
|
|
76
|
+
t.prevRotationZ[entity] = 0;
|
|
77
|
+
t.prevRotationW[entity] = 1;
|
|
78
|
+
|
|
79
|
+
// Create a dynamic Rapier body and configure it
|
|
80
|
+
const body = this._physics.createDynamicBody();
|
|
81
|
+
body.setTranslation({ x: 0, y: 5, z: 0 }, true);
|
|
82
|
+
body.setRotation({ x: 0, y: 0, z: 0, w: 1 }, true);
|
|
83
|
+
|
|
84
|
+
// Create a ball collider attached to the body
|
|
85
|
+
const groups = CollisionLayers.get('player');
|
|
86
|
+
const collider = this._physics.createBallCollider(0.5, body, groups);
|
|
87
|
+
|
|
88
|
+
// Store handles in ECS for later lookup
|
|
89
|
+
const pr = this._physicsRefs.unsafe;
|
|
90
|
+
pr.bodyHandle[entity] = body.handle;
|
|
91
|
+
pr.colliderHandle[entity] = collider.handle;
|
|
92
|
+
pr.bodyType[entity] = BodyType.DYNAMIC;
|
|
93
|
+
pr.collisionLayer[entity] = groups;
|
|
94
|
+
|
|
95
|
+
// Register collider→entity mapping (used by collision events)
|
|
96
|
+
this._physics.registerCollider(collider.handle, entity);
|
|
77
97
|
}
|
|
78
98
|
}
|
|
79
99
|
```
|
|
80
100
|
|
|
81
101
|
### Collider Shapes (3D)
|
|
82
102
|
|
|
103
|
+
All collider factories take an optional `parent` body, `groups` (collision groups), and `activeEvents` bitmask:
|
|
104
|
+
|
|
83
105
|
```typescript
|
|
84
106
|
// Sphere
|
|
85
|
-
|
|
107
|
+
this._physics.createBallCollider(radius, parent, groups, activeEvents);
|
|
86
108
|
|
|
87
|
-
// Box
|
|
88
|
-
|
|
109
|
+
// Box (half-extents, 3 dimensions)
|
|
110
|
+
this._physics.createCuboidCollider(hx, hy, hz, parent, groups, activeEvents);
|
|
89
111
|
|
|
90
112
|
// Capsule
|
|
91
|
-
|
|
113
|
+
this._physics.createCapsuleCollider(halfHeight, radius, parent, groups, activeEvents);
|
|
114
|
+
|
|
115
|
+
// Cylinder (3D only)
|
|
116
|
+
this._physics.createCylinderCollider(halfHeight, radius, parent, groups, activeEvents);
|
|
92
117
|
|
|
93
|
-
//
|
|
94
|
-
|
|
118
|
+
// Cone (3D only)
|
|
119
|
+
this._physics.createConeCollider(halfHeight, radius, parent, groups, activeEvents);
|
|
95
120
|
|
|
96
|
-
// Convex hull
|
|
97
|
-
|
|
121
|
+
// Convex hull (returns null if hull computation fails)
|
|
122
|
+
this._physics.createConvexHullCollider(new Float32Array([...]), parent, groups, activeEvents);
|
|
98
123
|
|
|
99
|
-
// Triangle mesh (static only)
|
|
100
|
-
|
|
124
|
+
// Triangle mesh (static geometry only)
|
|
125
|
+
this._physics.createTrimeshCollider(vertices, indices, parent, groups, activeEvents);
|
|
126
|
+
|
|
127
|
+
// Custom collider from a Rapier ColliderDesc
|
|
128
|
+
const desc = this._physics.rapier.ColliderDesc.ball(0.5).setDensity(2.0).setFriction(0.5);
|
|
129
|
+
this._physics.createColliderFromDesc(desc, parent);
|
|
101
130
|
```
|
|
102
131
|
|
|
103
132
|
## Collision Layers and Events
|
|
@@ -281,11 +310,9 @@ export const systems = [
|
|
|
281
310
|
On rollback:
|
|
282
311
|
1. ArrayBuffer is restored → ECS state reverts
|
|
283
312
|
2. Rapier 3D world snapshot is restored → physics state reverts
|
|
284
|
-
3. `
|
|
313
|
+
3. `ColliderEntityMap` is rebuilt automatically
|
|
285
314
|
4. KCC controllers are recreated via `recreateAll()`
|
|
286
315
|
|
|
287
|
-
**Critical fix:** `World.restoreSnapshot()` creates a world with an empty QueryPipeline. The framework calls `updateSceneQueries()` after restore. Without this, `computeColliderMovement()` queries fail on the first tick after rollback.
|
|
288
|
-
|
|
289
316
|
## State Transfer
|
|
290
317
|
|
|
291
318
|
After `applyExternalState()`:
|
|
@@ -360,3 +360,63 @@ export class ProjectileLifetimeSystem implements IECSSystem {
|
|
|
360
360
|
this._arenaConfig.radius -= this._arenaConfig.shrinkRate;
|
|
361
361
|
}
|
|
362
362
|
```
|
|
363
|
+
<% if (simulationType === 'physics2d') { -%>
|
|
364
|
+
|
|
365
|
+
## Add Procedural 2D Map
|
|
366
|
+
|
|
367
|
+
1. **Install packages:**
|
|
368
|
+
```bash
|
|
369
|
+
pnpm add @lagless/2d-map-generator @lagless/2d-map-renderer
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
2. **Define object types** in `simulation/src/lib/map-config/objects.ts`:
|
|
373
|
+
```typescript
|
|
374
|
+
import type { MapObjectDef, MapObjectRegistry } from '@lagless/2d-map-generator';
|
|
375
|
+
import { RenderLayer, ShapeType, CANOPY_SENSOR_TAG } from '@lagless/2d-map-generator';
|
|
376
|
+
|
|
377
|
+
export enum ObjectType { Tree = 0 }
|
|
378
|
+
|
|
379
|
+
const TREE: MapObjectDef = {
|
|
380
|
+
typeId: ObjectType.Tree,
|
|
381
|
+
colliders: [
|
|
382
|
+
{ shape: { type: ShapeType.Circle, radius: 30 } },
|
|
383
|
+
{ shape: { type: ShapeType.Circle, radius: 128 }, isSensor: true, tag: CANOPY_SENSOR_TAG },
|
|
384
|
+
],
|
|
385
|
+
visuals: [
|
|
386
|
+
{ texture: 'tree-trunk', layer: RenderLayer.Ground },
|
|
387
|
+
{ texture: 'tree-foliage', layer: RenderLayer.Canopy },
|
|
388
|
+
],
|
|
389
|
+
scaleRange: [0.1, 0.2],
|
|
390
|
+
includeSensorsInBounds: true,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
export const OBJECT_REGISTRY: MapObjectRegistry = new Map([[0, TREE]]);
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
3. **Create generator factory** in `simulation/src/lib/map-config/create-map-generator.ts`:
|
|
397
|
+
```typescript
|
|
398
|
+
import { MapGenerator, BiomeFeature, ShoreFeature, GrassFeature,
|
|
399
|
+
ObjectPlacementFeature, PlacementKind, TerrainZone, STANDARD_BIOME,
|
|
400
|
+
} from '@lagless/2d-map-generator';
|
|
401
|
+
import { OBJECT_REGISTRY, ObjectType } from './objects.js';
|
|
402
|
+
|
|
403
|
+
export function createMapGenerator(): MapGenerator {
|
|
404
|
+
return new MapGenerator({ baseWidth: 720, baseHeight: 720, scale: 1, extension: 80, gridSize: 16 })
|
|
405
|
+
.addFeature(new BiomeFeature(), STANDARD_BIOME)
|
|
406
|
+
.addFeature(new ShoreFeature(), { inset: 48, divisions: 12, variation: 4 })
|
|
407
|
+
.addFeature(new GrassFeature(), { inset: 18, variation: 3 })
|
|
408
|
+
.addFeature(new ObjectPlacementFeature(), {
|
|
409
|
+
registry: OBJECT_REGISTRY,
|
|
410
|
+
stages: [{ kind: PlacementKind.Density, typeId: ObjectType.Tree, density: 100, terrainZone: TerrainZone.Grass }],
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
4. **Extend runner** — generate map + create physics colliders in constructor, call `capturePreStartState()` after.
|
|
416
|
+
|
|
417
|
+
5. **Render** — use `MapTerrainRenderer` and `MapObjectRenderer` from `@lagless/2d-map-renderer`.
|
|
418
|
+
|
|
419
|
+
6. **Canopy transparency** — use `extractCanopyZones()` + `isInsideCanopyZone()` per frame.
|
|
420
|
+
|
|
421
|
+
> Full details: [docs/11-2d-map-generation.md](11-2d-map-generation.md)
|
|
422
|
+
<% } -%>
|
|
@@ -178,17 +178,12 @@ this._transform.unsafe.positionX[entity] = newX; // Physics will overwrite!
|
|
|
178
178
|
```
|
|
179
179
|
**Correct:**
|
|
180
180
|
```typescript
|
|
181
|
-
this._physics.
|
|
181
|
+
const body = this._physics.getBody(this._physicsRefs.unsafe.bodyHandle[entity]);
|
|
182
|
+
body.setLinvel({ x: vx, y: vy }, true);
|
|
182
183
|
// or
|
|
183
|
-
|
|
184
|
+
body.applyImpulse({ x: fx, y: fy }, true);
|
|
184
185
|
```
|
|
185
|
-
**Why:** PhysicsStep syncs Rapier→ECS, overwriting manual position changes. Move dynamic bodies via forces/velocity.
|
|
186
|
-
|
|
187
|
-
### Forgetting updateSceneQueries After Restore
|
|
188
|
-
|
|
189
|
-
The framework handles this automatically, but if you're doing custom Rapier operations:
|
|
190
|
-
**Always** call `world.updateSceneQueries()` after `World.restoreSnapshot()`.
|
|
191
|
-
**Why:** Restored worlds have empty QueryPipeline — ray casts and shape casts will miss all colliders.
|
|
186
|
+
**Why:** PhysicsStep syncs Rapier→ECS, overwriting manual position changes. Move dynamic bodies via forces/velocity on the Rapier body.
|
|
192
187
|
|
|
193
188
|
## Multiplayer
|
|
194
189
|
|