Real-Time Multiplayer Best Practices
This guide covers best practices for building performant, scalable real-time multiplayer games with the Moitribe JavaScript SDK.
Performance Optimization
Message Design
Smaller messages reduce latency and bandwidth usage. Send only what's necessary.
// ✅ Good - Compact position update
const position = new ArrayBuffer(12); // 3 floats (x, y, z)
const view = new DataView(position);
view.setFloat32(0, player.x, true);
view.setFloat32(4, player.y, true);
view.setFloat32(8, player.z, true);
// ❌ Bad - Verbose JSON
const position = {
x: player.x,
y: player.y,
z: player.z,
timestamp: Date.now(),
playerId: player.id,
room: currentRoom.id
};
Rate Limiting
Control message frequency to prevent network congestion:
class RateLimiter {
constructor(maxMessagesPerSecond) {
this.maxMessages = maxMessagesPerSecond;
this.messages = [];
}
canSend() {
const now = Date.now();
this.messages = this.messages.filter(time => now - time < 1000);
return this.messages.length < this.maxMessages;
}
recordSend() {
this.messages.push(Date.now());
}
}
const positionLimiter = new RateLimiter(30); // 30 updates per second
function sendPosition(position) {
if (positionLimiter.canSend()) {
MoitribeSDK(GAME_ID, 'standardmessage', {
messageData: encodePosition(position),
participantIds: [],
isReliable: false
});
positionLimiter.recordSend();
}
}
Message Batching
Combine multiple updates into single messages:
class MessageBatcher {
constructor(maxSize = 1024, flushInterval = 50) {
this.maxSize = maxSize;
this.flushInterval = flushInterval;
this.buffer = new ArrayBuffer(maxSize);
this.offset = 0;
this.timer = null;
}
add(data) {
if (this.offset + data.byteLength > this.maxSize) {
this.flush();
}
new Uint8Array(this.buffer, this.offset).set(new Uint8Array(data));
this.offset += data.byteLength;
if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.flushInterval);
}
}
flush() {
if (this.offset > 0) {
const message = this.buffer.slice(0, this.offset);
MoitribeSDK(GAME_ID, 'standardmessagetoall', {
messageData: message,
isReliable: false
});
this.offset = 0;
}
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
}
Network Optimization
Adaptive Quality
Adjust message reliability based on network conditions:
class AdaptiveQuality {
constructor() {
this.latency = 0;
this.packetLoss = 0;
this.quality = 'high';
}
updateMetrics(latency, packetLoss) {
this.latency = latency;
this.packetLoss = packetLoss;
if (latency > 200 || packetLoss > 0.05) {
this.quality = 'low';
} else if (latency > 100 || packetLoss > 0.02) {
this.quality = 'medium';
} else {
this.quality = 'high';
}
}
shouldUseReliable() {
return this.quality === 'low';
}
getUpdateRate() {
switch (this.quality) {
case 'high': return 60; // 60 FPS
case 'medium': return 30; // 30 FPS
case 'low': return 15; // 15 FPS
}
}
}
Client-Side Prediction
Reduce perceived latency by predicting game state:
class ClientPrediction {
constructor() {
this.pendingInputs = [];
this.serverState = null;
this.clientState = null;
}
addInput(input) {
this.pendingInputs.push({
input: input,
timestamp: Date.now()
});
// Apply prediction immediately
this.applyInput(input);
}
applyServerState(state) {
this.serverState = state;
// Reconcile with pending inputs
this.pendingInputs.forEach(pending => {
this.applyInput(pending.input);
});
}
applyInput(input) {
// Apply input to client state
// This depends on your game logic
}
}
Memory Management
Object Pooling
Reuse objects to reduce garbage collection:
class ObjectPool {
constructor(createFn, resetFn, initialSize = 10) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
for (let i = 0; i < initialSize; i++) {
this.pool.push(this.createFn());
}
}
get() {
if (this.pool.length > 0) {
return this.pool.pop();
}
return this.createFn();
}
release(obj) {
this.resetFn(obj);
this.pool.push(obj);
}
}
// Usage for message objects
const messagePool = new ObjectPool(
() => ({ data: null, timestamp: 0 }),
(obj) => { obj.data = null; obj.timestamp = 0; }
);
function createMessage(data) {
const message = messagePool.get();
message.data = data;
message.timestamp = Date.now();
return message;
}
function releaseMessage(message) {
messagePool.release(message);
}
ArrayBuffer Management
Efficiently handle binary data:
class BufferManager {
constructor(initialSize = 1024) {
this.buffer = new ArrayBuffer(initialSize);
this.view = new DataView(this.buffer);
this.offset = 0;
}
reset() {
this.offset = 0;
}
ensureCapacity(size) {
if (this.offset + size > this.buffer.byteLength) {
const newSize = Math.max(this.buffer.byteLength * 2, this.offset + size);
const newBuffer = new ArrayBuffer(newSize);
new Uint8Array(newBuffer).set(new Uint8Array(this.buffer));
this.buffer = newBuffer;
this.view = new DataView(this.buffer);
}
}
writeFloat32(value) {
this.ensureCapacity(4);
this.view.setFloat32(this.offset, value, true);
this.offset += 4;
}
writeUint8(value) {
this.ensureCapacity(1);
this.view.setUint8(this.offset, value);
this.offset += 1;
}
getData() {
return this.buffer.slice(0, this.offset);
}
}
Room Management
Connection Handling
Robust connection management:
class RoomManager {
constructor() {
this.currentRoom = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
}
joinRoom(roomId, callbacks) {
this.callbacks = callbacks;
this.currentRoom = roomId;
this.reconnectAttempts = 0;
this.attemptJoin();
}
attemptJoin() {
MoitribeSDK(GAME_ID, 'joinstandardroominvcode', {
invitationID: this.currentRoom,
onJoinedRoom: (success, room) => {
if (success) {
this.reconnectAttempts = 0;
this.callbacks.onJoinedRoom(room);
} else {
this.handleJoinFailure();
}
},
onRoomDisconnected: () => {
this.handleDisconnection();
}
});
}
handleJoinFailure() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
setTimeout(() => {
console.log(`Reconnecting attempt ${this.reconnectAttempts}...`);
this.attemptJoin();
}, delay);
} else {
this.callbacks.onJoinFailed();
}
}
handleDisconnection() {
console.log('Room disconnected, attempting to reconnect...');
this.handleJoinFailure();
}
}
Player State Synchronization
Efficient state synchronization:
class StateSynchronizer {
constructor() {
this.lastState = {};
this.stateBuffer = [];
this.bufferSize = 10;
}
updateState(playerId, state) {
const timestamp = Date.now();
const update = {
playerId,
state,
timestamp,
delta: this.calculateDelta(this.lastState[playerId], state)
};
this.stateBuffer.push(update);
if (this.stateBuffer.length > this.bufferSize) {
this.stateBuffer.shift();
}
this.lastState[playerId] = state;
}
calculateDelta(lastState, currentState) {
if (!lastState) return currentState;
const delta = {};
for (const key in currentState) {
if (lastState[key] !== currentState[key]) {
delta[key] = currentState[key];
}
}
return delta;
}
getInterpolatedState(playerId, targetTime) {
const updates = this.stateBuffer.filter(u => u.playerId === playerId);
if (updates.length < 2) return updates[updates.length - 1]?.state;
// Find surrounding updates
let before = updates[0];
let after = updates[updates.length - 1];
for (let i = 0; i < updates.length - 1; i++) {
if (updates[i].timestamp <= targetTime && updates[i + 1].timestamp > targetTime) {
before = updates[i];
after = updates[i + 1];
break;
}
}
// Linear interpolation
const factor = (targetTime - before.timestamp) / (after.timestamp - before.timestamp);
return this.interpolateStates(before.state, after.state, factor);
}
interpolateStates(state1, state2, factor) {
const result = {};
for (const key in state1) {
if (typeof state1[key] === 'number' && typeof state2[key] === 'number') {
result[key] = state1[key] + (state2[key] - state1[key]) * factor;
} else {
result[key] = factor < 0.5 ? state1[key] : state2[key];
}
}
return result;
}
}
Security Considerations
Input Validation
Always validate incoming messages:
class MessageValidator {
constructor() {
this.maxMessageSize = 1024;
this.allowedMessageTypes = ['position', 'action', 'chat'];
}
validate(message) {
// Check message size
if (message.byteLength > this.maxMessageSize) {
console.warn('Message too large:', message.byteLength);
return false;
}
// Parse and validate structure
try {
const parsed = this.parseMessage(message);
if (!this.allowedMessageTypes.includes(parsed.type)) {
console.warn('Invalid message type:', parsed.type);
return false;
}
// Validate specific fields
return this.validateMessageContent(parsed);
} catch (error) {
console.error('Message validation failed:', error);
return false;
}
}
validateMessageContent(message) {
switch (message.type) {
case 'position':
return this.validatePosition(message.data);
case 'action':
return this.validateAction(message.data);
case 'chat':
return this.validateChat(message.data);
default:
return false;
}
}
validatePosition(data) {
return typeof data.x === 'number' &&
typeof data.y === 'number' &&
typeof data.z === 'number' &&
!isNaN(data.x) && !isNaN(data.y) && !isNaN(data.z);
}
validateAction(data) {
return typeof data.actionId === 'number' &&
data.actionId >= 0 &&
data.actionId < 100;
}
validateChat(data) {
return typeof data.message === 'string' &&
data.message.length <= 200;
}
}
Debugging and Monitoring
Performance Metrics
Track key performance indicators:
class PerformanceMonitor {
constructor() {
this.metrics = {
messagesSent: 0,
messagesReceived: 0,
bytesTransferred: 0,
latency: [],
fps: [],
memoryUsage: []
};
this.startTime = Date.now();
}
recordMessageSent(size) {
this.metrics.messagesSent++;
this.metrics.bytesTransferred += size;
}
recordMessageReceived(size, latency) {
this.metrics.messagesReceived++;
this.metrics.bytesTransferred += size;
this.metrics.latency.push(latency);
// Keep only last 100 latency measurements
if (this.metrics.latency.length > 100) {
this.metrics.latency.shift();
}
}
recordFPS(fps) {
this.metrics.fps.push(fps);
if (this.metrics.fps.length > 60) {
this.metrics.fps.shift();
}
}
getStats() {
const runtime = (Date.now() - this.startTime) / 1000;
const avgLatency = this.metrics.latency.reduce((a, b) => a + b, 0) / this.metrics.latency.length;
const avgFPS = this.metrics.fps.reduce((a, b) => a + b, 0) / this.metrics.fps.length;
return {
runtime,
messagesPerSecond: this.metrics.messagesSent / runtime,
bytesPerSecond: this.metrics.bytesTransferred / runtime,
averageLatency: avgLatency,
averageFPS: avgFPS,
totalMessages: this.metrics.messagesSent + this.metrics.messagesReceived
};
}
logStats() {
const stats = this.getStats();
console.log('Performance Stats:', {
'Runtime': `${stats.runtime.toFixed(1)}s`,
'Msg/s': stats.messagesPerSecond.toFixed(1),
'KB/s': (stats.bytesPerSecond / 1024).toFixed(1),
'Latency': `${stats.averageLatency.toFixed(1)}ms`,
'FPS': stats.averageFPS.toFixed(1)
});
}
}
Common Pitfalls to Avoid
1. Sending Too Much Data
- Problem: Sending entire game state every frame
- Solution: Send only changes, use delta compression
2. Ignoring Network Conditions
- Problem: Fixed update rates regardless of connection quality
- Solution: Implement adaptive quality and rate limiting
3. No Client-Side Prediction
- Problem: Game feels laggy and unresponsive
- Solution: Predict movement and reconcile with server state
4. Poor Error Handling
- Problem: Game crashes on network issues
- Solution: Robust error handling and reconnection logic
5. Memory Leaks
- Problem: Performance degrades over time
- Solution: Object pooling and proper cleanup
Next Steps
Now that you understand best practices, explore:
- Standard Rooms - Fixed player count matches
- Endless Rooms - Dynamic player management
- Message Format - Message reliability and encoding
- Tournaments - Competitive events
Start with simple implementations and gradually add optimizations based on performance testing and user feedback.