game-networking-js
v1.0.5
Published
A library for handling real-time multiplayer game state.
Downloads
4
Readme
game-networking-js
Introduction
game-networking-js is a client-side JavaScript library designed to simplify the addition of real-time, peer-to-peer (P2P) multiplayer functionality to web-based games. It leverages PeerJS for WebRTC connections and Supabase for robust, serverless matchmaking.
This library abstracts away the complexities of WebRTC and room management, providing a high-level, event-driven API so you can focus on your game's logic.
Key features include:
- Hosting game sessions with automatic room creation and cleanup.
- Finding and joining random games via a matchmaking queue.
- Joining specific games using a unique room ID or a shareable link/QR code.
- An event-driven API for handling player connections, disconnections, and data messages.
- A clear, host-authoritative architectural pattern to prevent common P2P synchronization issues.
Architectural Model: Host-Authoritative
This library enforces a host-authoritative networking model. This is crucial for preventing desynchronization bugs.
- The Host is the Source of Truth: One player (the "host") is responsible for managing the definitive game state. The host runs the full game logic, processes all player inputs, and resolves all outcomes.
- Clients are "Dumb" Renderers: All other players (the "clients") do not run their own game logic. They send their inputs to the host and then simply render the updated game state they receive back from the host.
Adhering to this model is the key to building a stable multiplayer experience with this library.
Prerequisites
Before using the library, your HTML page must include the PeerJS and Supabase client libraries.
<script src="https://unpkg.com/[email protected]/dist/peerjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>Installation
The recommended way to use the library is by importing it as an ES6 module directly in your project.
// main.js
import * as GameNetworking from './src/index.js';Service Worker Configuration (IMPORTANT)
If your application uses a Service Worker (sw.js) for caching and offline capabilities, you must add all the library's source files to your cache list. Forgetting this step will cause the library to fail to load on subsequent visits, leading to application-breaking errors.
Example sw.js urlsToCache array:
const CACHE_NAME = 'your-game-cache-v1';
const urlsToCache = [
// ... your other app files (index.html, style.css, main.js)
// Add all game-networking-js library files
'./src/index.js',
'./src/core/GameNetworking.js',
'./src/core/PeerManager.js',
'./src/core/StateManager.js',
'./src/core/SupabaseManager.js',
'./src/utils/ErrorCodes.js',
'./src/utils/MessageTypes.js'
];Note: Always remember to increment your
CACHE_NAMEversion whenever you update theurlsToCachelist to ensure the service worker updates correctly.
Supabase Setup
1. Create the Matchmaking Table
In your Supabase project's SQL editor, create the game_rooms table. This table will be used to list available game sessions for matchmaking.
CREATE TABLE game_rooms (
room_id TEXT PRIMARY KEY,
host_peer_id TEXT NOT NULL,
game_type_identifier TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'waiting', -- 'waiting', 'full', 'in_game'
max_players INT NOT NULL DEFAULT 2,
current_players INT NOT NULL DEFAULT 1,
game_settings JSONB,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Create an index for faster room queries
CREATE INDEX idx_available_game_rooms ON game_rooms (
game_type_identifier,
status,
expires_at,
current_players,
max_players
);2. Create the Required RPC Function
This database function is essential for the findRandomGame feature. It efficiently finds an open room that matches the game criteria.
CREATE OR REPLACE FUNCTION find_available_room(
p_game_type_identifier TEXT,
p_game_settings_filter JSONB DEFAULT NULL
)
RETURNS TABLE(
room_id TEXT,
host_peer_id TEXT,
game_type_identifier TEXT,
status TEXT,
max_players INT,
current_players INT,
game_settings JSONB,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ
) AS $$
BEGIN
RETURN QUERY
SELECT
gr.room_id,
gr.host_peer_id,
gr.game_type_identifier,
gr.status,
gr.max_players,
gr.current_players,
gr.game_settings,
gr.expires_at,
gr.created_at
FROM game_rooms gr
WHERE gr.game_type_identifier = p_game_type_identifier
AND gr.status = 'waiting'
AND gr.current_players < gr.max_players
AND gr.expires_at > now()
AND (
p_game_settings_filter IS NULL
OR gr.game_settings @> p_game_settings_filter
)
ORDER BY gr.created_at ASC
LIMIT 1;
END;
$$ LANGUAGE plpgsql;3. Set Up Row Level Security (RLS)
Enable RLS and create policies to allow public access for creating, finding, and updating rooms.
-- 1. Enable RLS on the table
ALTER TABLE game_rooms ENABLE ROW LEVEL SECURITY;
-- 2. Create policies for public access
CREATE POLICY "Allow public read access" ON game_rooms FOR SELECT USING (true);
CREATE POLICY "Allow public insert" ON game_rooms FOR INSERT WITH CHECK (true);
CREATE POLICY "Allow public update" ON game_rooms FOR UPDATE USING (true);
CREATE POLICY "Allow public delete" ON game_rooms FOR DELETE USING (true);4. Optional: Auto-Update updated_at Timestamp
This trigger automatically updates the updated_at column whenever a room's details change.
CREATE OR REPLACE FUNCTION handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER on_game_rooms_update
BEFORE UPDATE ON game_rooms
FOR EACH ROW
EXECUTE PROCEDURE handle_updated_at();Quick Start
1. Initialize the Library
This must be done once when your application starts.
import * as GameNetworking from './src/index.js';
const config = {
supabaseUrl: 'YOUR_SUPABASE_URL',
supabaseKey: 'YOUR_SUPABASE_ANON_KEY',
tableName: 'game_rooms', // Must match your Supabase table name
gameTypeIdentifier: 'dots-and-boxes-v1', // A unique ID for your game
qrCodeBaseUrl: 'https://your-game-url.com' // For QR code generation
};
// Initialize the library when your app loads
await GameNetworking.initialize(config);
console.log('Game networking is ready!');2. Host a Game
const hostPlayerData = { name: "Alice", icon: "👑", color: "#FF6B6B" };
const gameSettings = { maxPlayers: 2, boardSize: "4x4" };
const hostCallbacks = {
onPlayerJoined: (player) => { console.log(`${player.name} joined!`); },
onPlayerLeft: (playerId) => { console.log(`Player ${playerId} left.`); },
onDataReceived: (data, fromPlayerId) => {
// Host processes client inputs here
handleClientMove(fromPlayerId, data);
},
onError: (error) => { console.error('Host error:', error); }
};
try {
// hostGame returns the host's complete player object
const { roomId, qrCodeData, hostPlayer } = await GameNetworking.hostGame(
hostPlayerData,
gameSettings,
hostCallbacks
);
console.log('Room created! ID:', roomId);
console.log('Host player object:', hostPlayer);
// Now, display the QR code or link for others to join
displayQRCode(qrCodeData);
} catch (error) {
console.error('Failed to host game:', error);
}3. Join a Game
const clientPlayerData = { name: "Bob", icon: "🚀", color: "#4ECDC4" };
const clientCallbacks = {
onSearching: () => { console.log('Looking for a game...'); },
onGameFound: (room) => { console.log('Game found! Connecting...'); },
onJoinSuccess: (details) => {
console.log('Successfully joined room!', details);
// details.yourData contains your final player info from the host
},
onJoinFailed: (error) => { console.error('Could not join:', error.message); },
onDataReceived: (data, hostPeerId) => {
// Client renders authoritative state from host
handleHostUpdate(data);
},
onHostLeft: () => { console.log('The host has left the game.'); },
onError: (error) => { console.error('Client error:', error); }
};
// To join a random game
const gameFilter = { boardSize: "4x4" }; // Optional filter
GameNetworking.findRandomGame(clientPlayerData, gameFilter, clientCallbacks);
// To join a specific game by ID (e.g., from a URL)
const roomId = "dots-and-boxes-v1-some-peer-id";
GameNetworking.joinGameById(roomId, clientPlayerData, clientCallbacks);State Synchronization Guide
To prevent bugs, the host must be the single source of truth. Here is a recommended implementation pattern based on a dots-and-boxes game.
Host-Side Logic (hostCallbacks.onDataReceived)
When a client sends a move, the host validates it, processes it, and broadcasts the complete result to all players.
// A simplified example of the host's move handler
function handleClientMove(fromPlayerId, data) {
if (data.type !== 'SUBMIT_MOVE') return;
// 1. Validate the move (e.g., is it this player's turn?)
if (state.currentPlayerId !== fromPlayerId) {
return; // Ignore out-of-turn move
}
// 2. Process the move and get all outcomes
// processMove should return the coordinates of any completed boxes
const completedBoxes = gameLogic.processMove(data.move, fromPlayerId);
// 3. Broadcast the authoritative result to ALL clients
GameNetworking.broadcastData({
type: 'GAME_STATE_UPDATE',
lastMove: { ...data.move, playerIndex: fromPlayerId },
completedBoxes: completedBoxes, // Array of {r, c, owner}
nextPlayerId: state.currentPlayerId, // The new current player
updatedScores: state.playerScores // The new scores
});
// 4. If game is over, broadcast that too
if (isGameOver()) {
GameNetworking.broadcastData({ type: 'GAME_OVER', ... });
}
}Client-Side Logic (clientCallbacks.onDataReceived)
The client does not run game logic. It only receives GAME_STATE_UPDATE messages and renders the result.
// A simplified example of the client's update handler
function handleHostUpdate(data) {
if (data.type !== 'GAME_STATE_UPDATE') return;
// 1. Render the line that was drawn
ui.drawVisualLine(data.lastMove);
// 2. Render all completed boxes sent by the host
data.completedBoxes.forEach(box => {
ui.fillBoxOnBoard(box.r, box.c, box.owner);
});
// 3. Update scores and turn display with data from the host
ui.updateScores(data.updatedScores);
ui.updatePlayerTurn(data.nextPlayerId);
}API Reference
Core Functions
initialize(config): Initializes the library. Must be called first.hostGame(hostPlayerData, gameSettings, callbacks): Creates a new game session. Returns aPromisethat resolves with{ roomId, qrCodeData, hostPlayer }.findRandomGame(playerData, filter, callbacks): Finds and joins an available game.joinGameById(roomId, playerData, callbacks): Joins a game by its specific ID.leaveGame(): Disconnects from the current session and cleans up resources.sendDataToHost(data): (Client-only) Sends a JSON-serializable object to the host.sendDataToPlayer(playerId, data): (Host-only) Sends data to a specific client by their room-local ID.broadcastData(data, excludePeerId?): (Host-only) Sends data to all connected clients.getLocalPeerId(): Returns the local client's raw PeerJS ID, ornull.
MSG_TYPE
The library exports an enum-like object, MSG_TYPE, containing standardized message types for internal and external communication. It is highly recommended to use these for your own game-specific messages to avoid conflicts.
import { MSG_TYPE } from './src/index.js';
Callbacks
Provide hostCallbacks or clientCallbacks objects to the networking functions. These objects contain functions that are triggered on specific events.
Host Callbacks (hostCallbacks)
onPlayerAttemptingJoin: (data, accept, reject) => {}: (Optional) Intercept join requests to approve or deny them.onPlayerJoined: (player) => {}onPlayerLeft: (playerId) => {}onDataReceived: (data, fromPlayerId) => {}onError: (error) => {}
Client Callbacks (clientCallbacks)
onSearching: () => {}onGameFound: (roomDetails) => {}onNoGameFound: () => {}onJoinSuccess: (details) => {}onJoinFailed: (error) => {}onDataReceived: (data, fromHostPeerId) => {}onHostLeft: () => {}onError: (error) => {}
Troubleshooting
- "Cannot read properties of undefined..." on init: Ensure
sw.jsis caching all files from the/srcdirectory and that you have cleared your browser cache after updating the service worker. - Players can't connect: This is often a NAT or firewall issue. Using a TURN server can significantly improve connection success rates. You can add TURN server credentials to the
peerJsConfigobject during initialization. - Game state desynchronizes: You are likely running game logic on the client. Refactor your code to follow the host-authoritative model strictly. The client should only send its input to the host and render the state it receives back.
