@unboxy/phaser-sdk 0.2.5 → 0.2.6
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 +88 -18
- package/dist/core/Unboxy.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/realtime/UnboxyRoom.d.ts +33 -1
- package/dist/realtime/UnboxyRoom.js +60 -1
- package/package.json +1 -1
package/SDK-GUIDE.md
CHANGED
|
@@ -185,7 +185,12 @@ Rule of thumb: if two different players would write to the same key, it's `gameD
|
|
|
185
185
|
|
|
186
186
|
### Multiplayer rooms — `unboxy.rooms`
|
|
187
187
|
|
|
188
|
-
Realtime rooms backed by unboxy-realtime-service. The
|
|
188
|
+
Realtime rooms backed by unboxy-realtime-service. The platform is **game-agnostic** — the server tracks who's in the room and relays data, but does not know what kind of game you're building. Two opaque primitives are your building blocks:
|
|
189
|
+
|
|
190
|
+
1. **Delta-synced KV state** (`room.player.*`, `room.data.*`) — server-maintained maps. Values are JSON-serializable. Every client sees changes via `onStateChange`. State is preserved while the room exists; new joiners see the current snapshot.
|
|
191
|
+
2. **Transient message relay** (`room.send` + `room.on`) — fire-and-forget. Payloads are stamped with `from: senderSessionId` and broadcast to every other client in the room. Not persisted.
|
|
192
|
+
|
|
193
|
+
#### Join a room
|
|
189
194
|
|
|
190
195
|
```ts
|
|
191
196
|
// Join or create a room scoped to this game. The SDK fetches a short-lived
|
|
@@ -194,37 +199,101 @@ const room = await unboxy.rooms.joinOrCreate('lobby', {
|
|
|
194
199
|
displayName: unboxy.user?.name ?? 'guest',
|
|
195
200
|
});
|
|
196
201
|
|
|
197
|
-
//
|
|
198
|
-
room.
|
|
199
|
-
room.
|
|
200
|
-
|
|
202
|
+
room.id; // room identifier
|
|
203
|
+
room.sessionId; // this connection's session id within the room
|
|
204
|
+
room.state; // proxy of the server-authoritative schema
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### Delta-synced state — `room.player` and `room.data`
|
|
208
|
+
|
|
209
|
+
Use this for anything that represents the *current world*: positions, hp, score, turn state, scoreboard, etc. The server delta-syncs diffs to every client.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
// Per-player (only you can write your own entry)
|
|
213
|
+
room.player.set('pos', { x: 120, y: 340 });
|
|
214
|
+
room.player.set('hp', 80);
|
|
215
|
+
room.player.delete('pos');
|
|
216
|
+
|
|
217
|
+
// Read another player's data at any time
|
|
218
|
+
const otherPos = room.player.get<{x:number; y:number}>(otherSessionId, 'pos');
|
|
219
|
+
|
|
220
|
+
// Per-room (any client can write)
|
|
221
|
+
room.data.set('currentTurn', playerSessionId);
|
|
222
|
+
room.data.set('scoreboard', [{ sid: 's1', score: 12 }, ...]);
|
|
223
|
+
const board = room.data.get<Entry[]>('scoreboard') ?? [];
|
|
224
|
+
|
|
225
|
+
// React to any state change
|
|
226
|
+
const offState = room.onStateChange(() => {
|
|
227
|
+
room.state.players.forEach((p, sid) => {
|
|
228
|
+
const pos = room.player.get<{x:number;y:number}>(sid, 'pos');
|
|
229
|
+
// ... re-render
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Values are JSON-stringified under the hood. Use for anything JSON-serializable.
|
|
235
|
+
|
|
236
|
+
#### Transient events — `room.send` and `room.on`
|
|
237
|
+
|
|
238
|
+
Use this for ephemeral actions: fires, hits, emotes, explosions, chat. Not persisted.
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
// Sender
|
|
242
|
+
room.send('fire', { x: 100, y: 200, angle: 1.5 });
|
|
201
243
|
|
|
202
|
-
//
|
|
203
|
-
const
|
|
204
|
-
|
|
244
|
+
// Every other client (sender does NOT receive their own)
|
|
245
|
+
const offFire = room.on('fire', (msg: { from: string; x: number; y: number; angle: number }) => {
|
|
246
|
+
spawnProjectile(msg.x, msg.y, msg.angle);
|
|
205
247
|
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### Server-tracked membership
|
|
251
|
+
|
|
252
|
+
`room.state.players` is server-owned. Each entry exposes:
|
|
253
|
+
- `userId` — Unboxy identity (from the JWT)
|
|
254
|
+
- `displayName` — set by the joining client
|
|
255
|
+
- `joinedAt` — ms timestamp
|
|
256
|
+
- `data` — the MapSchema<string> the player writes to via `room.player.set`
|
|
206
257
|
|
|
207
|
-
|
|
208
|
-
const offHit = room.on('hit', (payload) => { /* apply a hit effect */ });
|
|
209
|
-
room.send('move', { x: 120, y: 340 });
|
|
258
|
+
Games never mutate these directly; read them from `room.state.players`.
|
|
210
259
|
|
|
211
|
-
|
|
260
|
+
#### Connection lifecycle
|
|
261
|
+
|
|
262
|
+
```ts
|
|
212
263
|
room.onLeave((code) => console.log('disconnected', code));
|
|
213
264
|
room.onError((code, message) => console.warn('room error', code, message));
|
|
214
265
|
|
|
215
|
-
// Clean up
|
|
266
|
+
// Clean up on scene shutdown
|
|
216
267
|
offState();
|
|
217
|
-
|
|
268
|
+
offFire();
|
|
218
269
|
await room.leave();
|
|
219
270
|
```
|
|
220
271
|
|
|
221
|
-
|
|
272
|
+
#### Picking the right primitive
|
|
273
|
+
|
|
274
|
+
| Need | Use | Why |
|
|
275
|
+
|---|---|---|
|
|
276
|
+
| Smooth position sync of a player | `room.player.set('pos', {x,y})` at ~10-20 Hz + `onStateChange` | Delta-compressed, survives reconnect, server is source of truth. |
|
|
277
|
+
| Per-frame local movement feel | client-side prediction + interpolate remotes from state | State arrives at ~20 Hz; lerp toward it. |
|
|
278
|
+
| Scoreboard shared by all | `room.data.set('scoreboard', [...])` | One writer wins on conflict; use optimistic patterns or turn-based writes. |
|
|
279
|
+
| Projectile spawn / hit effect | `room.send('fire', {...})` + `room.on('fire', ...)` | Ephemeral, not needed on reconnect. |
|
|
280
|
+
| Chat message | `room.send('chat', text)` + `room.on('chat', ...)` | Same — ephemeral relay. |
|
|
281
|
+
| Current turn in a turn-based game | `room.data.set('turn', sid)` | Authoritative shared state. |
|
|
282
|
+
|
|
283
|
+
Rules of thumb:
|
|
284
|
+
- **If someone joining mid-game should see it, it's state** (`room.player.*` or `room.data.*`).
|
|
285
|
+
- **If it's a one-shot action, it's a message** (`room.send` / `room.on`).
|
|
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
|
+
- **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
|
+
|
|
289
|
+
#### Availability
|
|
290
|
+
|
|
222
291
|
- Requires sign-in. Anonymous players get `RpcError('UNAUTHENTICATED')`.
|
|
223
292
|
- Requires a host with multiplayer enabled. Standalone games (no host) get `RpcError('REALTIME_UNAVAILABLE')` — guard with `if (unboxy.host === 'unboxy-home-ui')` if your game can run both hosted and standalone.
|
|
224
293
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
-
|
|
294
|
+
#### Available room types
|
|
295
|
+
|
|
296
|
+
- `lobby` — the generic room described above. Use for any multiplayer gameplay.
|
|
228
297
|
|
|
229
298
|
**Rules of the road**
|
|
230
299
|
- The server is the source of truth. Never trust `send()` payloads coming from other clients — the server validates.
|
|
@@ -245,6 +314,7 @@ await room.leave();
|
|
|
245
314
|
|
|
246
315
|
## Changelog
|
|
247
316
|
|
|
317
|
+
- **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`.
|
|
248
318
|
- **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.
|
|
249
319
|
- **0.2.4** — added `unboxy.gameData` module (game-scope key-value store for scoreboards, shared state)
|
|
250
320
|
- **0.2.3** — anonymous users now use localStorage even inside a host (was throwing UNAUTHENTICATED when calling `saves` in home-ui without login)
|
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.6';
|
|
8
8
|
/**
|
|
9
9
|
* Unboxy platform services bound to the current (game, user).
|
|
10
10
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { SavesModule } from './saves/SavesModule.js';
|
|
|
9
9
|
export { GameDataModule } from './gamedata/GameDataModule.js';
|
|
10
10
|
export { RealtimeModule } from './realtime/RealtimeModule.js';
|
|
11
11
|
export type { JoinOptions } from './realtime/RealtimeModule.js';
|
|
12
|
-
export { UnboxyRoom } from './realtime/UnboxyRoom.js';
|
|
12
|
+
export { UnboxyRoom, PlayerDataFacade, RoomDataFacade } from './realtime/UnboxyRoom.js';
|
|
13
13
|
export { RpcError } from './core/Transport.js';
|
|
14
14
|
export type { Transport, TransportKind } from './core/Transport.js';
|
|
15
15
|
export type { UnboxyUser } from './protocol.js';
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,6 @@ export { Unboxy } from './core/Unboxy.js';
|
|
|
10
10
|
export { SavesModule } from './saves/SavesModule.js';
|
|
11
11
|
export { GameDataModule } from './gamedata/GameDataModule.js';
|
|
12
12
|
export { RealtimeModule } from './realtime/RealtimeModule.js';
|
|
13
|
-
export { UnboxyRoom } from './realtime/UnboxyRoom.js';
|
|
13
|
+
export { UnboxyRoom, PlayerDataFacade, RoomDataFacade } from './realtime/UnboxyRoom.js';
|
|
14
14
|
export { RpcError } from './core/Transport.js';
|
|
15
15
|
export { PROTOCOL_VERSION, } from './protocol.js';
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import type { Room } from 'colyseus.js';
|
|
2
2
|
type Unsubscribe = () => void;
|
|
3
|
+
/**
|
|
4
|
+
* Facade for per-player state — one entry per player, only the owner can write
|
|
5
|
+
* to their own entry. Delta-synced via Colyseus schema.
|
|
6
|
+
*/
|
|
7
|
+
export declare class PlayerDataFacade {
|
|
8
|
+
private readonly room;
|
|
9
|
+
constructor(room: Room<unknown>);
|
|
10
|
+
/** Write a JSON-serializable value to the caller's own player data. */
|
|
11
|
+
set(key: string, value: unknown): void;
|
|
12
|
+
/** Remove a key from the caller's own player data. */
|
|
13
|
+
delete(key: string): void;
|
|
14
|
+
/** Read a player's data value. Returns null if the player or key is absent. */
|
|
15
|
+
get<T = unknown>(sessionId: string, key: string): T | null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Facade for room-scope shared state — one map per room, any client may write.
|
|
19
|
+
* Delta-synced via Colyseus schema. Use for shared game state like current
|
|
20
|
+
* turn, scoreboard, shared level-of-the-day, etc.
|
|
21
|
+
*/
|
|
22
|
+
export declare class RoomDataFacade {
|
|
23
|
+
private readonly room;
|
|
24
|
+
constructor(room: Room<unknown>);
|
|
25
|
+
/** Write a JSON-serializable value into the room's shared data map. */
|
|
26
|
+
set(key: string, value: unknown): void;
|
|
27
|
+
/** Remove a key from the room's shared data map. */
|
|
28
|
+
delete(key: string): void;
|
|
29
|
+
/** Read a shared room value. Returns null if the key is absent. */
|
|
30
|
+
get<T = unknown>(key: string): T | null;
|
|
31
|
+
}
|
|
3
32
|
/**
|
|
4
33
|
* Unboxy-flavored wrapper around a Colyseus Room. Exposes only the surface
|
|
5
34
|
* documented in SDK-GUIDE.md so games are portable to a different realtime
|
|
@@ -7,6 +36,8 @@ type Unsubscribe = () => void;
|
|
|
7
36
|
*/
|
|
8
37
|
export declare class UnboxyRoom<State = unknown> {
|
|
9
38
|
private readonly room;
|
|
39
|
+
readonly player: PlayerDataFacade;
|
|
40
|
+
readonly data: RoomDataFacade;
|
|
10
41
|
constructor(room: Room<State>);
|
|
11
42
|
get id(): string;
|
|
12
43
|
get name(): string;
|
|
@@ -17,7 +48,8 @@ export declare class UnboxyRoom<State = unknown> {
|
|
|
17
48
|
* may change state.
|
|
18
49
|
*/
|
|
19
50
|
get state(): State;
|
|
20
|
-
/** Send a
|
|
51
|
+
/** Send a transient message. Relayed to every other client in the room
|
|
52
|
+
* with `from: sessionId` stamped onto the payload. Not persisted in state. */
|
|
21
53
|
send(type: string, payload?: unknown): void;
|
|
22
54
|
/** Register a handler for a server-sent message type. Returns unsubscribe. */
|
|
23
55
|
on(type: string, handler: (payload: unknown) => void): Unsubscribe;
|
|
@@ -1,3 +1,59 @@
|
|
|
1
|
+
function parseOrNull(raw) {
|
|
2
|
+
if (raw == null)
|
|
3
|
+
return null;
|
|
4
|
+
try {
|
|
5
|
+
return JSON.parse(raw);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Facade for per-player state — one entry per player, only the owner can write
|
|
13
|
+
* to their own entry. Delta-synced via Colyseus schema.
|
|
14
|
+
*/
|
|
15
|
+
export class PlayerDataFacade {
|
|
16
|
+
constructor(room) {
|
|
17
|
+
this.room = room;
|
|
18
|
+
}
|
|
19
|
+
/** Write a JSON-serializable value to the caller's own player data. */
|
|
20
|
+
set(key, value) {
|
|
21
|
+
this.room.send('player.set', { key, value });
|
|
22
|
+
}
|
|
23
|
+
/** Remove a key from the caller's own player data. */
|
|
24
|
+
delete(key) {
|
|
25
|
+
this.room.send('player.delete', { key });
|
|
26
|
+
}
|
|
27
|
+
/** Read a player's data value. Returns null if the player or key is absent. */
|
|
28
|
+
get(sessionId, key) {
|
|
29
|
+
const state = this.room.state;
|
|
30
|
+
const player = state?.players?.get?.(sessionId);
|
|
31
|
+
return parseOrNull(player?.data?.get?.(key));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Facade for room-scope shared state — one map per room, any client may write.
|
|
36
|
+
* Delta-synced via Colyseus schema. Use for shared game state like current
|
|
37
|
+
* turn, scoreboard, shared level-of-the-day, etc.
|
|
38
|
+
*/
|
|
39
|
+
export class RoomDataFacade {
|
|
40
|
+
constructor(room) {
|
|
41
|
+
this.room = room;
|
|
42
|
+
}
|
|
43
|
+
/** Write a JSON-serializable value into the room's shared data map. */
|
|
44
|
+
set(key, value) {
|
|
45
|
+
this.room.send('room.set', { key, value });
|
|
46
|
+
}
|
|
47
|
+
/** Remove a key from the room's shared data map. */
|
|
48
|
+
delete(key) {
|
|
49
|
+
this.room.send('room.delete', { key });
|
|
50
|
+
}
|
|
51
|
+
/** Read a shared room value. Returns null if the key is absent. */
|
|
52
|
+
get(key) {
|
|
53
|
+
const state = this.room.state;
|
|
54
|
+
return parseOrNull(state?.data?.get?.(key));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
1
57
|
/**
|
|
2
58
|
* Unboxy-flavored wrapper around a Colyseus Room. Exposes only the surface
|
|
3
59
|
* documented in SDK-GUIDE.md so games are portable to a different realtime
|
|
@@ -6,6 +62,8 @@
|
|
|
6
62
|
export class UnboxyRoom {
|
|
7
63
|
constructor(room) {
|
|
8
64
|
this.room = room;
|
|
65
|
+
this.player = new PlayerDataFacade(room);
|
|
66
|
+
this.data = new RoomDataFacade(room);
|
|
9
67
|
}
|
|
10
68
|
get id() { return this.room.roomId; }
|
|
11
69
|
get name() { return this.room.name; }
|
|
@@ -16,7 +74,8 @@ export class UnboxyRoom {
|
|
|
16
74
|
* may change state.
|
|
17
75
|
*/
|
|
18
76
|
get state() { return this.room.state; }
|
|
19
|
-
/** Send a
|
|
77
|
+
/** Send a transient message. Relayed to every other client in the room
|
|
78
|
+
* with `from: sessionId` stamped onto the payload. Not persisted in state. */
|
|
20
79
|
send(type, payload) {
|
|
21
80
|
this.room.send(type, payload);
|
|
22
81
|
}
|