Phaser integration
When your Phaser 4 game moves between menus, levels, and overlays, the network connection and synchronized state should remain available while each scene creates only the presentation it needs. Golem provides a persistent Phaser plugin plus generated, type-safe entity-view registries for sprites, custom prefabs, GPU layers, and entities with no visual object.
Installation and code generation
Section titled “Installation and code generation”Install the client runtime, Phaser helper package, and Phaser:
npm install golem-engine golem-phaser phaserGenerate the full JavaScript client and its Phaser binding from the same schemas:
integrations: js-client: out: src/synced/ phaser: out: src/phaser/ protocol_import: "../synced/"Run golem-bake after changing a schema. The js-client output contains synchronized entity classes, managers, codecs, and createClient(). The phaser output contains GolemPhaser.ts, which provides defineEntityViews() with one typed builder for every entity in your schemas.
Keep one client across scenes
Section titled “Keep one client across scenes”Register GolemPlugin as a global Phaser plugin. The fixed golem mapping makes the generated client available as this.golem.client in ordinary Phaser.Scene classes:
import Phaser from 'phaser';import { GOLEM_PLUGIN_KEY, GOLEM_PLUGIN_MAPPING, GolemPlugin,} from 'golem-phaser';import { createClient } from './synced/client.js';import { BattleScene } from './scenes/BattleScene.js';
new Phaser.Game({ type: Phaser.AUTO, width: 1280, height: 720, scene: [BattleScene], plugins: { global: [ { key: GOLEM_PLUGIN_KEY, plugin: GolemPlugin, mapping: GOLEM_PLUGIN_MAPPING, start: true, data: { createClient, connectionOptions: () => `ws://${location.hostname}:8080/ws`, maxReconnectAttempts: 5, // 0 means unlimited reconnectBaseDelay: 1500, }, }, ], },});The plugin creates the generated client once and reconnects unexpected drops with exponential backoff. Shutting down a scene does not disconnect the client or clear its entity and world managers. Call this.golem.disconnect() only when the game should end the session.
Set autoConnect: false in the plugin data when a login or matchmaking flow should call this.golem.connect() later.
Define and mount entity views
Section titled “Define and mount entity views”Create one registry that selects a presentation for every generated entity type. Use headless() when an entity intentionally has no Phaser object:
import { defineEntityViews } from './phaser/GolemPhaser.js';
export const battleViews = defineEntityViews((views) => ({ Player: views.Player.sprite({ texture: (player) => player.team === 1 ? 'blue-player' : 'red-player', frame: (player) => player.animationFrame, interpolation: 75, sync: (sprite, player) => { sprite.setRotation(player.heading); }, }),
Projectile: views.Projectile.sprite({ texture: 'projectiles', frame: (projectile) => projectile.kind, }),
TriggerZone: views.TriggerZone.headless(),}));Player, Projectile, TriggerZone, and their fields are examples of names from game schemas. TypeScript reports a missing registry entry after golem-bake adds another entity type, which makes visual omissions explicit.
Mount the registry from a normal scene:
import Phaser from 'phaser';import { battleViews } from '../battleViews.js';
export class BattleScene extends Phaser.Scene { preload() { this.load.image('blue-player', '/assets/blue-player.png'); this.load.image('red-player', '/assets/red-player.png'); this.load.spritesheet('projectiles', '/assets/projectiles.png', { frameWidth: 16, frameHeight: 16, }); }
create() { this.golem.mount(this, battleViews); }}Mounting creates views for entities that arrived before the scene started, then follows future spawns, updates, removals, and entity-targeted events. Scene shutdown destroys its views and subscriptions. Entering another scene can mount the same registry or a different one against the same synchronized entities.
Two active scenes can mount independent registries at the same time. A world scene might render sprites while a UI or minimap scene presents the same entities differently.
Use a Phaser Editor prefab
Section titled “Use a Phaser Editor prefab”Use prefab() when an entity needs a custom GameObject or a Phaser Editor-generated class instead of a single sprite:
import { PlayerPrefab } from './prefabs/PlayerPrefab.js';import { defineEntityViews } from './phaser/GolemPhaser.js';
export const battleViews = defineEntityViews((views) => ({ Player: views.Player.prefab({ create: (scene, player) => new PlayerPrefab(scene, player.posX, player.posY),
sync: (prefab, player) => { prefab.health = player.health; prefab.animationState = player.animationState; },
onRemove: (prefab) => { prefab.stopDamageEffects(); }, }),
Projectile: views.Projectile.headless(), TriggerZone: views.TriggerZone.headless(),}));The prefab can be a Sprite, Container, or any other Phaser GameObject. Golem adds the returned object with scene.add.existing(), synchronizes it after accepted entity updates, and destroys it on removal or scene shutdown. Set addToScene: false when the create callback already adds the object. Provide destroy only when teardown should differ from the GameObject’s normal destroy() method.
This keeps the generated synchronized entity as network state and the prefab as scene-owned presentation. The same entity can therefore receive a new prefab when the game enters another scene without reconnecting or waiting for another spawn frame.
Smooth movement and local prediction
Section titled “Smooth movement and local prediction”Sprite interpolation starts each accepted target from the currently displayed position:
import Phaser from 'phaser';import { defineEntityViews } from './phaser/GolemPhaser.js';
export const battleViews = defineEntityViews((views) => ({ Player: views.Player.sprite({ texture: 'player', interpolation: { duration: 75, ease: Phaser.Math.Easing.Sine.Out, }, }), Projectile: views.Projectile.headless(), TriggerZone: views.TriggerZone.headless(),}));Omit interpolation, set it to false, or provide a non-positive duration for immediate position updates.
When one entity type contains both a predicted local player and interpolated remote players, use externalPosition to choose transform ownership:
Player: views.Player.sprite({ texture: 'player', interpolation: 75, externalPosition: (player) => player.entityId === localPlayerId,}),The local sprite starts at its authoritative spawn position. While the predicate is true, Golem stops writing position and cancels pending interpolation so the game’s prediction and reconciliation code owns that transform. Frame selection, custom sync callbacks, events, and cleanup continue normally.
externalPosition does not implement prediction. It prevents synchronized presentation from competing with an existing prediction system.
Render high-count entities on the GPU
Section titled “Render high-count entities on the GPU”Projectiles, crowds, pickups, and effects can share a Phaser 4 SpriteGPULayer. Create the pool inside the registry so each mounted scene owns its layer:
import { SpriteGpuEntityPool } from 'golem-phaser';import { defineEntityViews } from './phaser/GolemPhaser.js';import type { SyncedProjectile } from './synced/ProjectileSynced.js';
export const battleViews = defineEntityViews((views) => ({ Player: views.Player.headless(),
Projectile: views.Projectile.gpu({ createPool: (scene) => new SpriteGpuEntityPool<SyncedProjectile>(scene, { texture: 'projectiles', capacity: 4096, growBy: 512, member: (projectile) => ({ frame: projectile.kind, rotation: projectile.heading, tintTopLeft: projectile.tint, tintTopRight: projectile.tint, tintBottomLeft: projectile.tint, tintBottomRight: projectile.tint, }), }), }),
TriggerZone: views.TriggerZone.headless(),}));This path requires WebGL and one source texture. The pool assigns stable slots, batches changed members before rendering, and hides and reuses slots when entities leave the client’s field of interest. It throws when capacity is exhausted unless growBy permits buffer growth.
GPU members are not individual GameObjects. Use sprite() or prefab() for entities that need per-object input, physics, or display-list behavior. The pool exposes its layer through layer, and configureLayer applies depth, blend mode, lighting, or GPU animation configuration when the layer is created.
Handle entity-targeted events
Section titled “Handle entity-targeted events”Entity event schemas become typed handlers on the matching registry entry. The handler receives the active view, synchronized entity, and generated event payload:
Player: views.Player.prefab({ create: (scene, player) => new PlayerPrefab(scene, player.posX, player.posY),
events: { Hit(prefab, player, event) { prefab.flashDamage(event.amount); console.log('remaining health', player.health); }, },}),Sprite event handlers receive the Sprite. GPU handlers receive the member slot number. A headless handler receives only the entity and event:
TriggerZone: views.TriggerZone.headless({ events: { Activated(trigger, event) { playZoneSound(trigger.entityId, event.sound); }, },}),An event declared with foi_only: true is delivered only while its target entity is in the client’s field of interest.
Mount Tiled world data
Section titled “Mount Tiled world data”Because world state also survives scene transitions, mount the current value first and subscribe to later updates:
import Phaser from 'phaser';import { loadTiledWorld } from 'golem-phaser';import type { ZoneData } from './synced/world_pb.js';
async function mountZone(scene: Phaser.Scene, zone: ZoneData) { return loadTiledWorld(scene, 'zone', zone, { tilesets: { terrain: '/assets/terrain.png', props: { key: 'shared-props' }, }, layerOptions: { Ground: { tilesets: 'terrain' }, Props: { tilesets: 'props', mode: 'cpu' }, }, });}
create() { this.golem.mount(this, battleViews);
const world = this.golem.client.world; if (world.zone) { void mountZone(this, world.zone); }
const unsubscribe = world.onZoneUpdate((zone) => { void mountZone(this, zone); }); this.events.once(Phaser.Scenes.Events.SHUTDOWN, unsubscribe);}loadTiledWorld accepts generated tileData or mapUrl world objects. It loads missing tileset images, creates selected layers, and replaces an earlier mount with the same scene and key. Compatible WebGL, orthogonal, single-tileset layers use Phaser 4 TilemapGPULayer in the default auto mode; other layers use TilemapLayer.
Use createTiledLayer when you already own a Phaser.Tilemaps.Tilemap and only need GPU selection. Use loadTiledMap for low-level loading without automatic tileset registration, layer creation, replacement, and teardown.
See World schemas and Map file serving for the server side of tileData and mapUrl delivery.
Show connection status
Section titled “Show connection status”Subscribe from a persistent UI scene or another game-owned service when players need reconnect feedback:
const unsubscribe = this.golem.onStatus((status) => { switch (status.type) { case 'reconnecting': showConnectionMessage( `Reconnecting (${status.attempt}), retrying in ${status.delayMs} ms`, ); break; case 'connected': hideConnectionMessage(); break; case 'failed': this.scene.start('MainMenu', { error: 'Connection lost' }); break; }});
this.events.once(Phaser.Scenes.Events.SHUTDOWN, unsubscribe);Status notifications are connecting, connected, reconnecting, disconnected, and failed. The plugin does not create an overlay, so the game’s UI remains consistent with its own scene and navigation design. Read this.golem.connected when a newly started scene needs the current state before the next notification.
See also JavaScript client, Channels and transports, and State updates.