Skip to main content

Custom Gameplay Rooms

Custom gameplay rooms are server-authoritative sessions that keep RPGJS players, state synchronization, actions, GUI messages, and session transfer without loading a Tiled map or starting map physics. They fit turn-based battles, card tables, lobbies, matchmaking screens, and other non-spatial game modes. Maps remain unchanged. Use player.changeMap() for maps and player.changeRoom() for custom rooms.

Runtime model

The same API works in standalone and MMORPG modes. In standalone mode, the server room runs in the browser alongside the client. In MMORPG mode, it runs only in the server bundle and remains authoritative. Keep reusable features in separate server and client entry points so server-only code is not imported by the MMORPG browser bundle.

Define the server room

Create a room class, declare a unique kind and path, then register it in the server entry:
The path placeholders are supplied by the server during a transfer. Parameter values cannot contain /, ?, or #. Duplicate kinds and duplicate paths fail during bootstrap instead of routing players unpredictably. RpgGameplayRoom provides:
  • players, containing synchronized RpgPlayer instances;
  • state, a server-authoritative writable signal;
  • id, params, and descriptor, identifying the resolved room instance;
  • decorated actions through @RpgRoomAction();
  • broadcast(), on(), and off() for ephemeral messages;
  • the standard RPGJS database, save, GUI, and player snapshot behavior.
The base class intentionally does not load RpgEvent instances. Represent non-spatial entities in the room’s serializable state, or synchronize a feature-owned model when a game mode needs more than players. For a reusable module, export the room class or a rooms array from a dedicated /server entry point. The application should only install that public result:

Register the client scene

Each custom server kind needs one CanvasEngine scene adapter on the client. The root may be a composed .ce component and may use DOMContainer for HTML UI. battle-scene.ce:
Register that component from the client entry point:
The component receives the synchronized room and its server-provided descriptor. When a custom scene is active, RPGJS unmounts the map viewport, lighting, map entities, prediction, streaming, projectiles, and map physics. GUI components remain mounted above the custom scene. Export scenes from a dedicated /client entry point when building a reusable module. The application then installs only provideClientScenes(battleScenes); it does not need to know the component tree or socket protocol used internally. onBeforeEnter runs while the previous scene is still mounted, onEnter runs after the new room connection opens, onChanges receives synchronized packets, and onLeave runs before another room replaces the scene. Use onLeave to remove feature-owned socket listeners and controllers. Use the CanvasEngine component unmount lifecycle for component-local effects, animation loops, DOM listeners, and input handlers. RPGJS clears the synchronized room signals when the scene changes, but it cannot dispose listeners created by the feature itself.

Public and private state

RpgGameplayRoom.state is synchronized to every connection in the room. Store only data that every participant and spectator may inspect there. Do not place hidden hands, enemy skills, private inventory contents, secrets, or authorization data in the shared state. Send ephemeral private projections to one player with player.emit(). Store durable player-owned data in synchronized player properties and use $syncWithClient: false for server-only properties. Every client command must be validated again by a @RpgRoomAction() method or another server-side handler; the custom CanvasEngine scene never owns gameplay authority.

Transfer players

Only the server selects the destination:
The transfer token preserves the RPGJS player identity and snapshot across the room boundary. player.getCurrentRoom() returns the active lobby, map, or custom room; player.getCurrentMap() is null in a map-independent room. Store the return map and position in server-owned match or player state before calling changeRoom(). Returning to spatial gameplay must use changeMap() so the normal map loading, streaming, movement, and physics lifecycle is restored. Use the generic hooks when behavior should apply to every room:
The existing canChangeMap, onJoinMap, onLeaveMap, and map hooks continue to run for maps. The generic room hooks run alongside them.

Failure behavior

RPGJS rejects an unknown room kind, missing or unsafe path parameters, duplicate server registrations, duplicate client scene kinds, and custom transfers without a matching client scene. These errors are surfaced through the normal client connection-error hook. Register the same room kinds on both runtimes. A server room without a matching client scene cannot be rendered; a client scene without a matching server room cannot be selected by an authoritative transfer.