@unboxy/phaser-sdk 0.2.6 → 0.2.8
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/SDK-GUIDE.md
CHANGED
|
@@ -286,6 +286,40 @@ Rules of thumb:
|
|
|
286
286
|
- **Do not implement position sync via messages.** You will reinvent delta compression badly and spend more bandwidth. Use `room.player.set('pos', {x,y})`.
|
|
287
287
|
- **Server is authoritative but permissive** — it doesn't validate what you put in `data`. Treat inbound values from other players as untrusted (don't eval, validate shape before reading).
|
|
288
288
|
|
|
289
|
+
#### Making remote motion feel smooth
|
|
290
|
+
|
|
291
|
+
State updates arrive at roughly 20 Hz (every ~50 ms). If you re-render remote entities by snapping directly to `room.player.get(sid, 'pos')` each frame, their movement will visibly step. Pick a smoothing technique that fits the game:
|
|
292
|
+
|
|
293
|
+
**Interpolation** — good for continuous motion (platformer, shooter, racer). Keep a `target` on each remote sprite, update it from state, lerp toward it each frame.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
// On state change, just update the target
|
|
297
|
+
room.onStateChange(() => {
|
|
298
|
+
room.state.players.forEach((_p, sid) => {
|
|
299
|
+
if (sid === room.sessionId) return;
|
|
300
|
+
const pos = room.player.get<{x:number;y:number}>(sid, 'pos');
|
|
301
|
+
if (pos) remoteSprites.get(sid)!.target = pos;
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// In your scene's update() loop
|
|
306
|
+
remoteSprites.forEach((s) => {
|
|
307
|
+
s.sprite.x = Phaser.Math.Linear(s.sprite.x, s.target.x, 0.2);
|
|
308
|
+
s.sprite.y = Phaser.Math.Linear(s.sprite.y, s.target.y, 0.2);
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Pair this with **client-side prediction for the LOCAL player**: render your own sprite from a locally-predicted position (updated by input every frame) so input feels instant, not gated on the round trip. Publish the predicted position to state at ~10–20 Hz with `room.player.set('pos', predicted)`. You rarely need to reconcile in casual games; trust the local prediction.
|
|
313
|
+
|
|
314
|
+
**Extrapolation** — good for projectiles and other predictable trajectories. Send `{ x, y, vx, vy, t }` once on spawn, let each client simulate locally with `x += vx * dt`. Only re-sync on events that change the trajectory (hit, bounce, destroy). Uses far less bandwidth than per-frame position sync.
|
|
315
|
+
|
|
316
|
+
**Snap** — the right choice for:
|
|
317
|
+
- Infrequent updates: score, hp, current turn, chat messages, lobby ready flags.
|
|
318
|
+
- Turn-based games: pieces jumping to their next square shouldn't glide.
|
|
319
|
+
- Discrete grids: same reason.
|
|
320
|
+
|
|
321
|
+
Don't blindly apply interpolation to everything. It's wrong for a chess piece and wasteful on a hp bar.
|
|
322
|
+
|
|
289
323
|
#### Availability
|
|
290
324
|
|
|
291
325
|
- Requires sign-in. Anonymous players get `RpcError('UNAUTHENTICATED')`.
|
|
@@ -314,6 +348,8 @@ Rules of thumb:
|
|
|
314
348
|
|
|
315
349
|
## Changelog
|
|
316
350
|
|
|
351
|
+
- **0.2.8** — docs: added "Making remote motion feel smooth" section to the multiplayer chapter with concrete examples for interpolation (continuous motion), extrapolation (projectiles), and snap (discrete/turn-based/infrequent). No code changes — version bumped so existing workspaces pick up the new guidance via `npm update @unboxy/phaser-sdk`.
|
|
352
|
+
- **0.2.7** — fixed handshake race on slow-mounting hosts. `PostMessageTransport.connect` now retries `unboxy:hello` every 200 ms until `unboxy:init` arrives (previously one-shot → lost on Android/mobile where React mounted after the iframe first fired hello). Default handshake timeout bumped from 2 s → 5 s.
|
|
317
353
|
- **0.2.6** — redesigned `unboxy.rooms` around two generic primitives: delta-synced KV state (`room.player.set/get/delete`, `room.data.set/get/delete`) + transient relay (`room.send` / `room.on`). Server no longer bakes in game-specific fields like x/y/color/ready or a `move` handler — games define their own shapes via opaque JSON values, same contract as `gameData` / `saves`.
|
|
318
354
|
- **0.2.5** — added `unboxy.rooms` module (server-authoritative multiplayer rooms backed by Colyseus on unboxy-realtime-service). Requires sign-in. Host must advertise `realtime` capability. Colyseus client loaded lazily — single-player games do not pay for the dependency.
|
|
319
355
|
- **0.2.4** — added `unboxy.gameData` module (game-scope key-value store for scoreboards, shared state)
|
package/dist/core/Unboxy.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { GameDataModule } from '../gamedata/GameDataModule.js';
|
|
|
4
4
|
import { RealtimeModule } from '../realtime/RealtimeModule.js';
|
|
5
5
|
import type { UnboxyUser } from '../protocol.js';
|
|
6
6
|
export interface UnboxyInitOptions {
|
|
7
|
-
/** Handshake timeout in ms when running inside a host. Default
|
|
7
|
+
/** Handshake timeout in ms when running inside a host. Default 5000. */
|
|
8
8
|
handshakeTimeoutMs?: number;
|
|
9
9
|
/**
|
|
10
10
|
* Game ID used for the localStorage fallback. Ignored when connected to a
|
package/dist/core/Unboxy.js
CHANGED
|
@@ -4,7 +4,7 @@ import { SavesModule } from '../saves/SavesModule.js';
|
|
|
4
4
|
import { GameDataModule } from '../gamedata/GameDataModule.js';
|
|
5
5
|
import { RealtimeModule } from '../realtime/RealtimeModule.js';
|
|
6
6
|
// Kept in sync with package.json on each publish.
|
|
7
|
-
const SDK_VERSION = '0.2.
|
|
7
|
+
const SDK_VERSION = '0.2.8';
|
|
8
8
|
/**
|
|
9
9
|
* Unboxy platform services bound to the current (game, user).
|
|
10
10
|
*
|
|
@@ -50,7 +50,7 @@ export class Unboxy {
|
|
|
50
50
|
* Discord Activity support is designed for but not shipped in this version.
|
|
51
51
|
*/
|
|
52
52
|
static async init(options = {}) {
|
|
53
|
-
const handshakeTimeoutMs = options.handshakeTimeoutMs ??
|
|
53
|
+
const handshakeTimeoutMs = options.handshakeTimeoutMs ?? 5000;
|
|
54
54
|
const standaloneGameId = options.standaloneGameId ?? 'standalone';
|
|
55
55
|
const pm = await PostMessageTransport.connect(handshakeTimeoutMs, SDK_VERSION);
|
|
56
56
|
if (pm)
|
|
@@ -15,9 +15,14 @@ export declare class PostMessageTransport implements Transport {
|
|
|
15
15
|
private constructor();
|
|
16
16
|
/**
|
|
17
17
|
* Perform the handshake. Resolves once parent has replied with `unboxy:init`.
|
|
18
|
-
*
|
|
18
|
+
* Resolves to null if no response within `timeoutMs`.
|
|
19
|
+
*
|
|
20
|
+
* The hello is retried at `helloRetryMs` intervals because the parent's
|
|
21
|
+
* RPC-host message listener may not be mounted yet when the iframe first
|
|
22
|
+
* fires hello — especially on slower mobile devices where React takes
|
|
23
|
+
* longer to boot. One-shot hello was losing the handshake on Android.
|
|
19
24
|
*/
|
|
20
|
-
static connect(timeoutMs?: number, sdkVersion?: string): Promise<PostMessageTransport | null>;
|
|
25
|
+
static connect(timeoutMs?: number, sdkVersion?: string, helloRetryMs?: number): Promise<PostMessageTransport | null>;
|
|
21
26
|
private installResultListener;
|
|
22
27
|
call<T = unknown>(method: string, params?: unknown): Promise<T>;
|
|
23
28
|
}
|
|
@@ -15,35 +15,51 @@ export class PostMessageTransport {
|
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Perform the handshake. Resolves once parent has replied with `unboxy:init`.
|
|
18
|
-
*
|
|
18
|
+
* Resolves to null if no response within `timeoutMs`.
|
|
19
|
+
*
|
|
20
|
+
* The hello is retried at `helloRetryMs` intervals because the parent's
|
|
21
|
+
* RPC-host message listener may not be mounted yet when the iframe first
|
|
22
|
+
* fires hello — especially on slower mobile devices where React takes
|
|
23
|
+
* longer to boot. One-shot hello was losing the handshake on Android.
|
|
19
24
|
*/
|
|
20
|
-
static async connect(timeoutMs =
|
|
25
|
+
static async connect(timeoutMs = 5000, sdkVersion = '0.0.0', helloRetryMs = 200) {
|
|
21
26
|
if (typeof window === 'undefined' || window.parent === window)
|
|
22
27
|
return null;
|
|
23
28
|
const transport = new PostMessageTransport(window.parent);
|
|
24
|
-
const
|
|
29
|
+
const hello = {
|
|
30
|
+
type: 'unboxy:hello',
|
|
31
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
32
|
+
sdkVersion,
|
|
33
|
+
};
|
|
34
|
+
const init = await new Promise((resolve) => {
|
|
35
|
+
let settled = false;
|
|
36
|
+
const finish = (value) => {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
settled = true;
|
|
40
|
+
window.removeEventListener('message', onMessage);
|
|
41
|
+
clearInterval(helloInterval);
|
|
42
|
+
clearTimeout(timeoutHandle);
|
|
43
|
+
resolve(value);
|
|
44
|
+
};
|
|
25
45
|
const onMessage = (event) => {
|
|
26
46
|
if (event.source !== window.parent)
|
|
27
47
|
return;
|
|
28
48
|
const data = event.data;
|
|
29
49
|
if (!data || data.type !== 'unboxy:init')
|
|
30
50
|
return;
|
|
31
|
-
|
|
32
|
-
resolve(data);
|
|
51
|
+
finish(data);
|
|
33
52
|
};
|
|
34
53
|
window.addEventListener('message', onMessage);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
// Fire the first hello immediately, then keep retrying until init
|
|
55
|
+
// arrives or timeout fires. Retries are cheap (a postMessage with a
|
|
56
|
+
// small payload); the parent's RPC host replies on first valid hello.
|
|
57
|
+
transport.parent.postMessage(hello, '*');
|
|
58
|
+
const helloInterval = setInterval(() => {
|
|
59
|
+
transport.parent.postMessage(hello, '*');
|
|
60
|
+
}, helloRetryMs);
|
|
61
|
+
const timeoutHandle = setTimeout(() => finish(null), timeoutMs);
|
|
39
62
|
});
|
|
40
|
-
const hello = {
|
|
41
|
-
type: 'unboxy:hello',
|
|
42
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
43
|
-
sdkVersion,
|
|
44
|
-
};
|
|
45
|
-
transport.parent.postMessage(hello, '*');
|
|
46
|
-
const init = await initPromise;
|
|
47
63
|
if (!init)
|
|
48
64
|
return null;
|
|
49
65
|
if (init.protocolVersion !== PROTOCOL_VERSION) {
|