Skip to main content

Message Best Practices

Optimizing your real-time multiplayer messaging is crucial for creating responsive, scalable games. This guide covers best practices for message design, performance optimization, and common patterns.

Message Design Principles​

1. Keep Messages Small​

Smaller messages reduce bandwidth usage and latency.

// ❌ Bad: Large JSON with redundant data
const badMessage = {
type: "player_position_update",
timestamp: 1703123456789,
playerId: "player_12345",
playerName: "SuperGamer123",
position: { x: 123.456, y: 789.012, z: 0.000 },
velocity: { x: 1.5, y: 0.0, z: 0.0 },
rotation: { x: 0, y: 45, z: 0 },
health: 95,
maxHealth: 100,
armor: 50,
maxArmor: 50,
level: 15,
experience: 12500,
team: "blue",
isAlive: true,
isMoving: true,
isJumping: false,
isAttacking: false,
isDefending: false,
currentWeapon: "rifle",
ammo: 30,
maxAmmo: 90
};

// ✅ Good: Compact binary message
function createCompactPositionUpdate(playerId, x, y, flags) {
const buffer = new ArrayBuffer(7); // Much smaller
const view = new DataView(buffer);

view.setUint8(0, getPlayerId(playerId)); // 1 byte
view.setInt16(1, Math.round(x * 100), true); // 2 bytes
view.setInt16(3, Math.round(y * 100), true); // 2 bytes
view.setUint8(5, flags); // 1 byte
view.setUint8(6, calculateChecksum(buffer)); // 1 byte

return buffer;
}

2. Send Only What Changes​

Use delta compression to avoid sending unchanged data.

class DeltaSender {
constructor() {
this.lastState = new Map();
}

sendPositionUpdate(playerId, x, y) {
const key = `pos_${playerId}`;
const lastPos = this.lastState.get(key);

// Only send if position changed significantly
if (!lastPos ||
Math.abs(lastPos.x - x) > 0.5 ||
Math.abs(lastPos.y - y) > 0.5) {

const buffer = CompactEncoder.encodePosition(x, y);
MoitribeSDK('my-game', 'sendmsg', {
message: buffer,
isReliable: false
});

this.lastState.set(key, { x, y });
}
}
}

Combine multiple small updates into single messages.

// ❌ Bad: Multiple separate messages
function sendBadUpdates(player) {
sendPosition(player.x, player.y);
sendHealth(player.health);
sendFlags(player.flags);
sendScore(player.score);
}

// ✅ Good: Single batched message
function sendBatchedUpdate(player) {
const buffer = createPlayerUpdate(player);
MoitribeSDK('my-game', 'sendmsg', {
message: buffer,
isReliable: false
});
}

function createPlayerUpdate(player) {
const buffer = new ArrayBuffer(10);
const view = new DataView(buffer);

view.setInt16(0, Math.round(player.x * 100), true);
view.setInt16(2, Math.round(player.y * 100), true);
view.setUint8(4, player.health);
view.setUint8(5, player.flags);
view.setUint32(6, player.score, true);

return buffer;
}

Performance Optimization​

1. Message Rate Limiting​

Control message frequency to prevent network flooding.

class RateLimiter {
constructor(maxMessagesPerSecond = 30) {
this.maxRate = maxMessagesPerSecond;
this.messages = [];
}

canSendMessage() {
const now = Date.now();

// Remove old messages (older than 1 second)
this.messages = this.messages.filter(time => now - time < 1000);

if (this.messages.length < this.maxRate) {
this.messages.push(now);
return true;
}

return false;
}

sendWithRateLimit(messageData, isReliable = false) {
if (this.canSendMessage()) {
MoitribeSDK('my-game', 'sendmsg', {
message: messageData,
isReliable
});
return true;
}

return false; // Rate limited
}
}

// Usage
const positionLimiter = new RateLimiter(30); // 30 updates per second
const actionLimiter = new RateLimiter(10); // 10 actions per second

function sendPosition(x, y) {
const buffer = CompactEncoder.encodePosition(x, y);
positionLimiter.sendWithRateLimit(buffer, false);
}

function sendAction(action) {
const buffer = ActionEncoder.encode(action);
actionLimiter.sendWithRateLimit(buffer, true);
}

2. Priority-Based Messaging​

Send important messages first, drop less critical ones when needed.

class MessageQueue {
constructor() {
this.queues = {
critical: [], // Game state changes, player actions
high: [], // Score updates, important events
normal: [], // Position updates, animations
low: [] // Cosmetic effects, debug info
};

this.maxPerFrame = {
critical: 5,
high: 3,
normal: 10,
low: 2
};
}

enqueue(message, priority = 'normal') {
this.queues[priority].push({
data: message,
timestamp: Date.now()
});
}

processQueue() {
// Process in priority order
const priorities = ['critical', 'high', 'normal', 'low'];

priorities.forEach(priority => {
const queue = this.queues[priority];
const maxToSend = this.maxPerFrame[priority];

for (let i = 0; i < Math.min(queue.length, maxToSend); i++) {
const message = queue.shift();
this.sendMessage(message);
}

// Drop old messages from normal/low priority queues
if (priority === 'normal' || priority === 'low') {
const now = Date.now();
this.queues[priority] = queue.filter(msg => now - msg.timestamp < 100);
}
});
}

sendMessage(message) {
MoitribeSDK('my-game', 'sendmsg', {
message: message.data,
isReliable: message.priority === 'critical' || message.priority === 'high'
});
}
}

3. Adaptive Quality​

Adjust message frequency based on network conditions.

class AdaptiveQuality {
constructor() {
this.baseUpdateRate = 30; // 30 updates per second
this.currentUpdateRate = this.baseUpdateRate;
this.lastPingTime = 0;
this.pingHistory = [];
this.messageHistory = [];
}

recordPing(pingMs) {
this.pingHistory.push(pingMs);
if (this.pingHistory.length > 10) {
this.pingHistory.shift();
}

this.adjustQuality();
}

adjustQuality() {
const avgPing = this.pingHistory.reduce((a, b) => a + b, 0) / this.pingHistory.length;

if (avgPing > 200) {
// Poor connection - reduce update rate
this.currentUpdateRate = Math.max(10, this.baseUpdateRate * 0.5);
} else if (avgPing > 100) {
// Moderate connection - slight reduction
this.currentUpdateRate = Math.max(20, this.baseUpdateRate * 0.75);
} else if (avgPing < 50) {
// Good connection - can increase rate
this.currentUpdateRate = Math.min(60, this.baseUpdateRate * 1.5);
} else {
// Normal connection
this.currentUpdateRate = this.baseUpdateRate;
}
}

shouldSendUpdate() {
const now = Date.now();
const interval = 1000 / this.currentUpdateRate;

if (now - this.lastPingTime >= interval) {
this.lastPingTime = now;
return true;
}

return false;
}
}

Network Optimization​

1. Client-Side Prediction​

Reduce perceived latency by predicting outcomes.

class ClientPredictor {
constructor() {
this.pendingActions = [];
this.serverState = null;
this.clientState = null;
}

sendAction(action) {
// Apply action immediately on client
this.applyActionLocally(action);

// Add to pending actions
this.pendingActions.push({
action,
timestamp: Date.now(),
id: this.generateActionId()
});

// Send to server
const buffer = ActionEncoder.encode(action);
MoitribeSDK('my-game', 'sendmsg', {
message: buffer,
isReliable: true
});
}

applyActionLocally(action) {
// Update local state immediately
switch (action.type) {
case 'move':
this.clientState.x += action.dx;
this.clientState.y += action.dy;
break;
case 'attack':
this.clientState.isAttacking = true;
setTimeout(() => {
this.clientState.isAttacking = false;
}, 500);
break;
}
}

reconcileWithServer(serverState) {
this.serverState = serverState;

// Find which actions were processed by server
const processedActions = this.pendingActions.filter(
pending => pending.timestamp <= serverState.lastProcessedTime
);

// Remove processed actions
this.pendingActions = this.pendingActions.filter(
pending => pending.timestamp > serverState.lastProcessedTime
);

// Re-apply unprocessed actions to server state
let currentState = { ...serverState };
this.pendingActions.forEach(pending => {
currentState = this.applyActionToState(currentState, pending.action);
});

this.clientState = currentState;
}
}

2. Interpolation and Extrapolation​

Smooth movement between network updates.

class Interpolator {
constructor() {
this.targetPosition = { x: 0, y: 0 };
this.currentPosition = { x: 0, y: 0 };
this.velocity = { x: 0, y: 0 };
this.lastUpdateTime = 0;
this.interpolationDelay = 100; // 100ms delay for smoothness
}

updatePosition(newX, newY, timestamp) {
// Apply delay for interpolation
const adjustedTime = timestamp - this.interpolationDelay;

if (adjustedTime > this.lastUpdateTime) {
// Calculate velocity for extrapolation
const dt = (adjustedTime - this.lastUpdateTime) / 1000;
if (dt > 0) {
this.velocity.x = (newX - this.targetPosition.x) / dt;
this.velocity.y = (newY - this.targetPosition.y) / dt;
}

this.targetPosition = { x: newX, y: newY };
this.lastUpdateTime = adjustedTime;
}
}

getCurrentPosition(currentTime) {
const timeSinceUpdate = (currentTime - this.lastUpdateTime) / 1000;

// Interpolate towards target
const interpolationSpeed = 10; // Adjust for smoothness
this.currentPosition.x += (this.targetPosition.x - this.currentPosition.x) * interpolationSpeed * 0.016;
this.currentPosition.y += (this.targetPosition.y - this.currentPosition.y) * interpolationSpeed * 0.016;

// Extrapolate if no recent updates
if (timeSinceUpdate > 0.1) {
this.currentPosition.x += this.velocity.x * 0.016;
this.currentPosition.y += this.velocity.y * 0.016;
}

return { ...this.currentPosition };
}
}

Memory Management​

1. Object Pooling​

Reuse objects to reduce garbage collection.

class ObjectPool {
constructor(createFn, resetFn, initialSize = 10) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];

// Pre-allocate objects
for (let i = 0; i < initialSize; i++) {
this.pool.push(this.createFn());
}
}

acquire() {
if (this.pool.length > 0) {
return this.pool.pop();
}

return this.createFn();
}

release(obj) {
this.resetFn(obj);
this.pool.push(obj);
}
}

// Usage for message buffers
const bufferPool = new ObjectPool(
() => new ArrayBuffer(1024),
(buffer) => {
// Reset buffer if needed
const view = new DataView(buffer);
for (let i = 0; i < buffer.byteLength; i++) {
view.setUint8(i, 0);
}
},
50
);

function getBuffer() {
return bufferPool.acquire();
}

function releaseBuffer(buffer) {
bufferPool.release(buffer);
}

2. Efficient Data Structures​

Use appropriate data structures for message handling.

class CircularBuffer {
constructor(size) {
this.buffer = new Array(size);
this.size = size;
this.head = 0;
this.tail = 0;
this.count = 0;
}

push(item) {
this.buffer[this.tail] = item;
this.tail = (this.tail + 1) % this.size;

if (this.count < this.size) {
this.count++;
} else {
this.head = (this.head + 1) % this.size; // Overwrite oldest
}
}

pop() {
if (this.count === 0) return null;

const item = this.buffer[this.head];
this.head = (this.head + 1) % this.size;
this.count--;

return item;
}

peek() {
return this.count > 0 ? this.buffer[this.head] : null;
}

isEmpty() {
return this.count === 0;
}

isFull() {
return this.count === this.size;
}
}

Debugging and Monitoring​

1. Message Statistics​

Track message performance and identify issues.

class MessageStats {
constructor() {
this.stats = {
sent: 0,
received: 0,
failed: 0,
totalBytes: 0,
averageSize: 0,
messagesPerSecond: 0,
latency: []
};

this.startTime = Date.now();
this.lastSecond = Date.now();
this.messagesThisSecond = 0;
}

recordSent(bytes) {
this.stats.sent++;
this.stats.totalBytes += bytes;
this.stats.averageSize = this.stats.totalBytes / this.stats.sent;
this.messagesThisSecond++;

this.updateMessagesPerSecond();
}

recordReceived(bytes, latency) {
this.stats.received++;
this.stats.latency.push(latency);

// Keep only last 100 latency measurements
if (this.stats.latency.length > 100) {
this.stats.latency.shift();
}
}

recordFailed() {
this.stats.failed++;
}

updateMessagesPerSecond() {
const now = Date.now();
if (now - this.lastSecond >= 1000) {
this.stats.messagesPerSecond = this.messagesThisSecond;
this.messagesThisSecond = 0;
this.lastSecond = now;
}
}

getReport() {
const avgLatency = this.stats.latency.length > 0
? this.stats.latency.reduce((a, b) => a + b, 0) / this.stats.latency.length
: 0;

return {
...this.stats,
averageLatency: Math.round(avgLatency),
uptime: Date.now() - this.startTime,
successRate: this.stats.sent > 0
? ((this.stats.sent - this.stats.failed) / this.stats.sent * 100).toFixed(2) + '%'
: '0%'
};
}
}

2. Network Quality Monitoring​

class NetworkMonitor {
constructor() {
this.pingHistory = [];
this.packetLoss = 0;
this.bandwidthUsage = 0;
this.lastQualityCheck = Date.now();
}

measurePing() {
const startTime = Date.now();

MoitribeSDK('my-game', 'ping', {}, (response) => {
const ping = Date.now() - startTime;
this.pingHistory.push(ping);

if (this.pingHistory.length > 20) {
this.pingHistory.shift();
}

this.assessNetworkQuality();
});
}

assessNetworkQuality() {
const avgPing = this.pingHistory.reduce((a, b) => a + b, 0) / this.pingHistory.length;

let quality = 'excellent';
if (avgPing > 200) quality = 'poor';
else if (avgPing > 150) quality = 'fair';
else if (avgPing > 100) quality = 'good';

console.log(`Network Quality: ${quality} (avg ping: ${avgPing.toFixed(0)}ms)`);

// Adjust message rates based on quality
this.adjustMessageRates(quality);
}

adjustMessageRates(quality) {
const rates = {
excellent: { position: 60, action: 20 },
good: { position: 30, action: 15 },
fair: { position: 20, action: 10 },
poor: { position: 10, action: 5 }
};

const targetRates = rates[quality];

// Update rate limiters
if (window.positionLimiter) {
window.positionLimiter.maxRate = targetRates.position;
}
if (window.actionLimiter) {
window.actionLimiter.maxRate = targetRates.action;
}
}
}

Common Pitfalls to Avoid​

1. Over-sending Messages​

// ❌ Bad: Sending every frame
function gameLoop() {
sendPlayerPosition(player.x, player.y); // 60 times per second!
}

// ✅ Good: Rate-limited sending
function gameLoop() {
if (shouldSendUpdate()) {
sendPlayerPosition(player.x, player.y);
}
}

2. Ignoring Network Conditions​

// ❌ Bad: Fixed message rate
setInterval(() => {
sendPositionUpdate();
}, 16); // Always 60 FPS

// ✅ Good: Adaptive rate
function sendUpdate() {
if (networkQuality.isGood() && shouldSendUpdate()) {
sendPositionUpdate();
}
}

3. Large Message Payloads​

// ❌ Bad: Sending entire game state
function sendGameState() {
const fullState = getCompleteGameState(); // 2KB+
sendMessage(JSON.stringify(fullState));
}

// ✅ Good: Sending only changes
function sendGameState() {
const delta = calculateStateDelta(); // 200 bytes
sendMessage(encodeDelta(delta));
}

Next Steps​