11. Gameplay Physics Is Responsive Too
A common mistake in HTML5 games: UI is responsive, but gameplay logic still uses old bounds.
In Stickers Merge, physics bounds are recalculated when the screen changes.
LevelPhysicsBoundsResolver uses current virtual size and layout:
const virtualSize = this.screen.getVirtualSize();
const leftInset = this.screen.getOrientation() === 'land'
? this.resolveLandLeftInset(layout, virtualSize)
: 0;
const localTopLeft = playfield.view.toLocal({ x: leftInset, y: 0 }, root.view);
const localTopRight = playfield.view.toLocal({ x: virtualSize.width, y: 0 }, root.view);
const localBottomLeft = playfield.view.toLocal({ x: leftInset, y: virtualSize.height }, root.view);
In landscape, bounds start after the left panel. In portrait, bounds start at zero because the panel is at the bottom.
On SCREEN_CHANGED, the FSM runs:
[SharedUiEvents.ScreenChanged]: {
actions: ['RebuildLevelStickersOnResizeCommand'],
},
The command calls:
await this.getSceneService<LevelStickerResizeRebuildService>(
GameServices.LevelStickerResizeRebuildService,
).rebuildFromCurrentScene({
layout: this.layout,
stickersLayer: this.getStickersLayer(),
});
The service:
- cleans up active drag;
- captures current sticker state;
- recalculates physics bounds;
- removes old physics bodies;
- spawns stickers again in the new layout;
- warms up the physics simulation;
- marks stickers as settled.
const stickersSnapshot = this.captureSnapshot();
this.physics.setBounds(this.boundsResolver.resolveFromLayout(params.layout));
await this.mergeService.consumeManyByIds(stickersSnapshot.map((entry) => entry.id));
const respawnedRecords = await this.spawnFlowService.spawnBatch({
layer: params.stickersLayer,
physics: this.physics,
registrar: this.registrar,
entries: stickersSnapshot,
mode: 'packed_visible',
});
Responsive gameplay means visual resize is not enough. Physics, spawn positions, collision bounds, and drag state must respond too.