Web Engine Docs
Preparing documentation
Use the search bar to quickly find any topic
Preparing documentation
Use the search bar to quickly find any topic
Send custom messages and remote procedure calls between clients and server for game events, chat, and actions.
Beyond automatic state synchronization, Web Engine provides flexible messaging systems for game events, player actions, and remote procedure calls (RPCs) with type safety and reliability options.
Send any serializable data between clients and server with custom message types.
Type-safe RPC system for invoking server/client methods with return values.
Choose between unreliable (fast) or reliable (guaranteed) delivery per message.
Send raw binary data for maximum performance and minimal overhead.
Web Engine defines several packet types for common multiplayer operations:
import { PacketType } from '@web-engine-dev/core/network';export enum PacketType {CONNECT = 0, // Initial connectionWELCOME = 1, // Server welcome with client IDSTATE_UPDATE = 2, // Entity state synchronizationINPUT = 3, // Player input packetsSPAWN = 4, // Entity spawningDESPAWN = 5, // Entity removalCHAT = 6, // Chat messagesSDP_OFFER = 7, // WebRTC offer (P2P)SDP_ANSWER = 8, // WebRTC answer (P2P)ICE_CANDIDATE = 9, // WebRTC ICE candidate (P2P)SCENE_DATA = 10 // Scene serialization}
Send custom messages using the packet system with type-safe interfaces:
import { NetworkManager, PacketType } from '@web-engine-dev/core/network';const network = NetworkManager.getInstance();// Define custom packet interfaceinterface ChatPacket {type: PacketType.CHAT;senderId: number;timestamp: number;name: string;message: string;}// Send chat messagefunction sendChatMessage(message: string) {const packet: ChatPacket = {type: PacketType.CHAT,senderId: network.getClientId(),timestamp: network.getServerTime(),name: 'Player1',message: message};network.sendPacket(packet);}// Listen for chat messagesnetwork.on('chat', (packet: ChatPacket) => {console.log(`${packet.name}: ${packet.message}`);displayChat(packet.name, packet.message);});
When using Colyseus transport, you can also use the room's message system directly:
// In GameRoom.ts (server)export class GameRoom extends Room<GameRoomState> {onCreate(options: any) {// Register message handlerthis.onMessage('player_action', (client, message) => {console.log(`Player ${client.sessionId} action:`, message);// Validate actionif (!this.isValidAction(client, message)) {return;}// Process actionthis.handlePlayerAction(client.sessionId, message);// Broadcast to other playersthis.broadcast('player_action', {playerId: client.sessionId,action: message.action,timestamp: Date.now()}, { except: client });});}private isValidAction(client: Client, message: any): boolean {// Server-side validationconst player = this.state.players.get(client.sessionId);return player && player.canPerformAction(message.action);}}
// Client-side: send message to roomconst transport = network.getTransport();if (transport instanceof ColyseusTransport) {const room = transport.room;// Send message to serverroom.send('player_action', {action: 'jump',timestamp: Date.now()});// Listen for messages from serverroom.onMessage('player_action', (message) => {console.log('Player action:', message);handleRemotePlayerAction(message);});}
The RPC system provides type-safe remote method invocation with automatic serialization:
import { NetworkRPC, getRPCSystem } from '@web-engine-dev/core/network';// Define networked class with RPC methodsclass PlayerController {@NetworkRPC({ target: 'server', reliable: true })async requestJump(timestamp: number): Promise<boolean> {// This method runs on the serverconsole.log('Jump request at', timestamp);// Validate jumpconst canJump = this.validateJump();if (canJump) {this.applyJumpForce();}return canJump;}@NetworkRPC({ target: 'client', reliable: false })playEffect(effectId: string, position: [number, number, number]): void {// This method runs on the clientthis.effectSystem.play(effectId, position);}}// Call RPC from clientconst rpc = getRPCSystem();const success = await rpc.call<boolean>('PlayerController','requestJump',Date.now());if (success) {console.log('Jump successful!');} else {console.log('Jump denied by server');}
RPC Benefits
RPCs provide type-safe method calls with automatic serialization, timeout handling, and error propagation. They're ideal for request-response patterns like inventory actions, purchases, or any operation that needs confirmation.
Choose the appropriate delivery guarantee for your messages:
Fast but not guaranteed to arrive or arrive in order. Use for:
Guaranteed delivery with automatic retransmission. Use for:
import {ReliableUDP,PacketChannel,NetworkManager} from '@web-engine-dev/core/network';const network = NetworkManager.getInstance();// Send with reliable ordered deliverynetwork.sendWithChannel(packet, PacketChannel.RELIABLE_ORDERED);// Send with unreliable sequenced delivery (drop old)network.sendWithChannel(stateUpdate, PacketChannel.UNRELIABLE_SEQUENCED);// Channel options:// - RELIABLE_ORDERED: Guaranteed delivery, in order// - RELIABLE_UNORDERED: Guaranteed delivery, any order// - UNRELIABLE_SEQUENCED: Drop old packets, keep newest// - UNRELIABLE: Fast, no guarantees
Messages are automatically serialized to JSON or binary formats. For maximum performance, use binary serialization:
import { PacketSerializer } from '@web-engine-dev/core/network';const serializer = new PacketSerializer();// Serialize packet to binaryconst buffer = serializer.serialize(packet);// Deserialize binary to packetconst packet = serializer.deserialize(buffer);// Send binary data directlyconst binaryData = new ArrayBuffer(64);const view = new DataView(binaryData);// Write headerview.setUint8(0, PacketType.SPAWN);view.setUint32(1, senderId, true);view.setFloat64(5, timestamp, true);// Write entity dataview.setFloat32(13, position.x, true);view.setFloat32(17, position.y, true);view.setFloat32(21, position.z, true);// Send binarynetwork.sendBinary(binaryData);
When to Use Binary
Use binary serialization for high-frequency updates (state sync, input) or when sending large amounts of data (scene serialization). For infrequent events (chat, actions), JSON serialization is more convenient and readable.
Always validate incoming packets to prevent malicious or malformed data:
import {validateIncomingPacket,isInputPacket,InputPacket} from '@web-engine-dev/core/network';// Type guards for packet validationnetwork.on('packet', (packet) => {// Validate packet structureconst validated = validateIncomingPacket(packet);if (!validated) {console.warn('Invalid packet received:', packet);return;}// Type-specific validationif (isInputPacket(validated)) {handleInput(validated);}});function handleInput(packet: InputPacket) {// Validate input rangesconst move = packet.inputs.move;if (Math.abs(move[0]) > 1 || Math.abs(move[1]) > 1) {console.warn('Invalid move vector:', move);return;}// Validate sequence number (prevent replay attacks)const lastSeq = getLastSequence(packet.senderId);if (packet.sequence <= lastSeq) {console.warn('Out-of-order or duplicate input:', packet.sequence);return;}// Process valid inputprocessPlayerInput(packet.senderId, packet.inputs);setLastSequence(packet.senderId, packet.sequence);}
Use priority queues to ensure important messages are sent first during bandwidth constraints:
import { PacketPriorityQueue } from '@web-engine-dev/core/network';const queue = new PacketPriorityQueue(1000);// Enqueue with priority (higher = more important)queue.enqueue(chatPacket, 1); // Low priorityqueue.enqueue(stateUpdate, 5); // Normal priorityqueue.enqueue(spawnPacket, 10); // High priorityqueue.enqueue(criticalEvent, 100); // Critical priority// Process queue (sends highest priority first)while (queue.size() > 0 && bandwidthAvailable()) {const item = queue.dequeue();if (item) {network.sendPacket(item.packet);}}