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