Drop-in/Drop-out Mechanics
Endless rooms are designed for dynamic player management where players can join and leave at any time without disrupting the ongoing game. This creates flexible multiplayer experiences perfect for casual games and persistent worlds.
Core Concepts
What is Drop-in/Drop-out?
Drop-in/drop-out gameplay allows:
- Flexible Timing: Players join whenever they want
- No Waiting: Games don't wait for full lobbies
- Continuous Play: Game state persists through player changes
- Dynamic Scaling: Game adapts to current player count
Key Differences from Standard Rooms
| Feature | Endless Rooms | Standard Rooms |
|---|---|---|
| Join Timing | Anytime | Only at session start |
| Leave Timing | Anytime | Only at session end |
| Player Count | Variable | Fixed |
| Game Flow | Continuous | Session-based |
| Waiting | None | Required for matchmaking |
Implementation Patterns
Player Join Handling
// Handle new player joining
function handlePlayerJoin(room, participantList) {
const newPlayer = room.participants[room.participants.length - 1];
console.log(`Player ${newPlayer.name} joined the game`);
// Add player to game state
addPlayerToGame(newPlayer);
// Send current game state to new player
sendGameStateToPlayer(newPlayer.participantID);
// Notify other players
broadcastPlayerJoined(newPlayer);
// Update UI
updatePlayerCount(participantList.length);
showJoinNotification(newPlayer.name);
}
// Send current game state to new player
function sendGameStateToPlayer(participantId) {
const gameState = {
type: 'game_state_sync',
data: {
currentScore: getCurrentScore(),
gameTime: getGameTime(),
playerPositions: getAllPlayerPositions(),
activePowerUps: getActivePowerUps(),
level: getCurrentLevel()
},
timestamp: Date.now()
};
const encoder = new TextEncoder();
const messageData = encoder.encode(JSON.stringify(gameState));
MoitribeSDK('my-game', 'endlessmessage', {
messageData: messageData.buffer,
participantIds: [participantId],
isReliable: true
});
}
Player Leave Handling
// Handle player leaving
function handlePlayerLeave(room, participantList) {
const leftPlayer = findLeftPlayer(room);
if (!leftPlayer) return;
console.log(`Player ${leftPlayer.name} left the game`);
// Remove player from game state
removePlayerFromGame(leftPlayer.participantID);
// Handle player's in-game assets
handlePlayerAssets(leftPlayer);
// Notify other players
broadcastPlayerLeft(leftPlayer);
// Update UI
updatePlayerCount(participantList.length);
showLeaveNotification(leftPlayer.name);
// Adjust game difficulty if needed
adjustGameDifficulty(participantList.length);
}
// Handle player's assets when they leave
function handlePlayerAssets(player) {
// Option 1: Remove player's objects
removePlayerObjects(player.participantID);
// Option 2: Transfer ownership to nearest player
transferPlayerAssets(player.participantID);
// Option 3: Keep objects but mark as abandoned
markAssetsAsAbandoned(player.participantID);
}
// Adjust game based on player count
function adjustGameDifficulty(playerCount) {
const difficulty = calculateDifficulty(playerCount);
// Adjust spawn rates
updateSpawnRates(difficulty);
// Modify enemy strength
updateEnemyStrength(difficulty);
// Change resource availability
updateResourceRates(difficulty);
console.log(`Game difficulty adjusted for ${playerCount} players`);
}
Game State Synchronization
// Maintain consistent game state
class GameStateManager {
constructor() {
this.gameState = {
players: new Map(),
world: {},
score: 0,
level: 1,
timestamp: Date.now()
};
}
// Add new player
addPlayer(participant) {
const playerState = {
participantID: participant.participantID,
name: participant.name,
position: this.getSpawnPosition(),
score: 0,
health: 100,
joinedAt: Date.now()
};
this.gameState.players.set(participant.participantID, playerState);
this.broadcastPlayerUpdate(playerState, 'joined');
}
// Remove player
removePlayer(participantId) {
const player = this.gameState.players.get(participantId);
if (player) {
this.gameState.players.delete(participantId);
this.broadcastPlayerUpdate(player, 'left');
}
}
// Get spawn position for new player
getSpawnPosition() {
const positions = [
{ x: 100, y: 100 },
{ x: 500, y: 100 },
{ x: 300, y: 300 },
{ x: 100, y: 500 },
{ x: 500, y: 500 }
];
// Find unused spawn position
for (const pos of positions) {
const isOccupied = Array.from(this.gameState.players.values())
.some(player =>
Math.abs(player.position.x - pos.x) < 50 &&
Math.abs(player.position.y - pos.y) < 50
);
if (!isOccupied) {
return pos;
}
}
// Fallback to random position
return {
x: Math.random() * 600,
y: Math.random() * 600
};
}
// Broadcast player updates
broadcastPlayerUpdate(player, action) {
const update = {
type: 'player_update',
action: action,
player: {
id: player.participantID,
name: player.name,
position: player.position,
score: player.score
},
timestamp: Date.now()
};
const encoder = new TextEncoder();
const messageData = encoder.encode(JSON.stringify(update));
MoitribeSDK('my-game', 'endlessmessagetoall', {
messageData: messageData.buffer,
isReliable: true
});
}
// Get current state for new players
getCurrentState() {
return {
type: 'full_state_sync',
players: Array.from(this.gameState.players.values()),
world: this.gameState.world,
score: this.gameState.score,
level: this.gameState.level,
timestamp: Date.now()
};
}
}
Use Case Examples
Casual Party Game
// Party game that adapts to player count
class PartyGameManager {
constructor() {
this.minPlayers = 1;
this.maxPlayers = 20;
this.currentGame = null;
}
handlePlayerJoin(room, participantList) {
const playerCount = participantList.length;
// Start game if first player joins
if (playerCount === 1 && !this.currentGame) {
this.startGame();
}
// Add player to current round
if (this.currentGame) {
this.addPlayerToCurrentRound(getLastJoinedPlayer(room));
}
// Adjust game for new player
this.adjustGameForPlayerCount(playerCount);
}
handlePlayerLeave(room, participantList) {
const playerCount = participantList.length;
// Remove player from current round
this.removePlayerFromCurrentRound(getLastLeftPlayer(room));
// End game if no players left
if (playerCount === 0 && this.currentGame) {
this.endGame();
}
// Adjust game for remaining players
this.adjustGameForPlayerCount(playerCount);
}
adjustGameForPlayerCount(playerCount) {
// Adjust mini-game selection
this.selectAppropriateMiniGame(playerCount);
// Modify scoring system
this.adjustScoringSystem(playerCount);
// Update UI elements
this.updateGameUI(playerCount);
}
selectAppropriateMiniGame(playerCount) {
const games = {
1: ['solo_challenge', 'endless_runner'],
2: ['duel', 'cooperative'],
'3-4': ['team_battle', 'free_for_all'],
'5+': ['mass_battle', 'team_competition']
};
const suitableGames = games[this.getPlayerCountCategory(playerCount)];
const selectedGame = suitableGames[Math.floor(Math.random() * suitableGames.length)];
this.loadMiniGame(selectedGame);
}
getPlayerCountCategory(count) {
if (count === 1) return 1;
if (count === 2) return 2;
if (count <= 4) return '3-4';
return '5+';
}
}
Persistent World Game
// Persistent world that continues with or without players
class PersistentWorldManager {
constructor() {
this.worldState = {
environment: {},
npcs: [],
resources: [],
buildings: [],
lastUpdate: Date.now()
};
this.activePlayers = new Map();
this.worldLoop = null;
}
startWorldLoop() {
this.worldLoop = setInterval(() => {
this.updateWorld();
this.broadcastWorldUpdates();
}, 1000 / 60); // 60 FPS
}
handlePlayerJoin(room, participantList) {
const newPlayer = room.participants[room.participants.length - 1];
// Add player to active list
this.activePlayers.set(newPlayer.participantID, {
participant: newPlayer,
joinedAt: Date.now(),
lastSeen: Date.now()
});
// Send current world state
this.sendWorldStateToPlayer(newPlayer.participantID);
// Spawn player in world
this.spawnPlayerInWorld(newPlayer);
// Start world loop if not running
if (!this.worldLoop) {
this.startWorldLoop();
}
}
handlePlayerLeave(room, participantList) {
const leftPlayer = this.findLeftPlayer(room);
if (!leftPlayer) return;
// Remove player from active list
this.activePlayers.delete(leftPlayer.participantID);
// Save player state
this.savePlayerState(leftPlayer);
// Remove player from world (gracefully)
this.removePlayerFromWorld(leftPlayer);
// Stop world loop if no players left (optional)
if (this.activePlayers.size === 0) {
this.pauseWorldLoop();
}
}
updateWorld() {
// Update NPCs
this.updateNPCs();
// Regenerate resources
this.regenerateResources();
// Process world events
this.processWorldEvents();
// Update timestamp
this.worldState.lastUpdate = Date.now();
}
broadcastWorldUpdates() {
if (this.activePlayers.size === 0) return;
const update = {
type: 'world_update',
timestamp: this.worldState.lastUpdate,
npcs: this.getChangedNPCs(),
resources: this.getChangedResources(),
events: this.getRecentEvents()
};
const encoder = new TextEncoder();
const messageData = encoder.encode(JSON.stringify(update));
MoitribeSDK('my-game', 'endlessmessagetoall', {
messageData: messageData.buffer,
isReliable: false // World updates can be unreliable
});
}
pauseWorldLoop() {
if (this.worldLoop) {
clearInterval(this.worldLoop);
this.worldLoop = null;
console.log('World loop paused - no active players');
}
}
}
Cooperative Survival Game
// Survival game where players help each other
class SurvivalGameManager {
constructor() {
this.gameState = {
wave: 1,
enemies: [],
resources: [],
baseHealth: 100,
players: new Map(),
difficulty: 1
};
this.waveTimer = null;
this.enemySpawnTimer = null;
}
handlePlayerJoin(room, participantList) {
const newPlayer = room.participants[room.participants.length - 1];
// Add player with starting equipment
this.addPlayer(newPlayer);
// Adjust difficulty based on player count
this.updateDifficulty(participantList.length);
// Send current game state
this.sendGameStateToPlayer(newPlayer.participantID);
// Start wave system if not running
if (!this.waveTimer) {
this.startWaveSystem();
}
}
handlePlayerLeave(room, participantList) {
const leftPlayer = this.findLeftPlayer(room);
if (!leftPlayer) return;
// Remove player but keep their contributions
this.removePlayer(leftPlayer);
// Redistribute player's resources
this.redistributePlayerResources(leftPlayer);
// Adjust difficulty
this.updateDifficulty(participantList.length);
// Check if game should continue
if (participantList.length === 0) {
this.endGame();
}
}
updateDifficulty(playerCount) {
// Base difficulty on player count
const baseDifficulty = Math.max(1, Math.floor(playerCount / 2));
// Adjust current difficulty smoothly
this.gameState.difficulty = baseDifficulty;
// Update enemy spawn rates
this.updateEnemySpawnRate();
// Adjust wave difficulty
this.updateWaveDifficulty();
console.log(`Difficulty updated to ${baseDifficulty} for ${playerCount} players`);
}
redistributePlayerResources(leftPlayer) {
const playerResources = this.getPlayerResources(leftPlayer.participantID);
const activePlayers = Array.from(this.gameState.players.values())
.filter(p => p.participantID !== leftPlayer.participantID);
if (activePlayers.length === 0) return;
// Distribute resources equally
const sharePerPlayer = Math.floor(playerResources / activePlayers.length);
activePlayers.forEach(player => {
this.addResourcesToPlayer(player.participantID, sharePerPlayer);
});
// Notify players about resource redistribution
this.notifyResourceRedistribution(leftPlayer.name, sharePerPlayer);
}
startWaveSystem() {
this.waveTimer = setInterval(() => {
this.startNewWave();
}, 120000); // New wave every 2 minutes
this.enemySpawnTimer = setInterval(() => {
this.spawnEnemies();
}, 5000); // Spawn enemies every 5 seconds
}
}
Best Practices
Smooth Transitions
// Ensure smooth player transitions
function smoothPlayerTransition(player, action) {
switch (action) {
case 'join':
// Fade in player
fadeInPlayer(player);
// Play join sound
playSound('player_join');
// Show welcome message
showWelcomeMessage(player.name);
break;
case 'leave':
// Fade out player
fadeOutPlayer(player);
// Play leave sound
playSound('player_leave');
// Show goodbye message
showGoodbyeMessage(player.name);
break;
}
}
State Consistency
// Maintain consistent state across player changes
function ensureStateConsistency() {
// Validate player positions
validatePlayerPositions();
// Check for orphaned objects
cleanupOrphanedObjects();
// Verify game rules
validateGameRules();
// Sync with server if needed
if (stateInconsistencyDetected()) {
requestStateSync();
}
}
Performance Optimization
// Optimize performance for variable player counts
function optimizeForPlayerCount(playerCount) {
// Adjust update frequencies
const updateRate = Math.max(16, Math.floor(1000 / (playerCount * 10)));
setUpdateRate(updateRate);
// Optimize rendering
if (playerCount > 10) {
enableLODRendering();
} else {
disableLODRendering();
}
// Adjust physics precision
const physicsSteps = Math.max(1, Math.floor(60 / playerCount));
setPhysicsSteps(physicsSteps);
}
Next Steps
- Create Room - Set up endless rooms
- Join Room - Join existing rooms
- Room Callbacks - Handle player events
- Send Messages - Implement communication
Design Tip
Design your game mechanics to work with any number of players, from 1 to the maximum you support. Test edge cases like players joining/leaving rapidly.