Multiplayer Game Server

← Back to Projects

Multiplayer Game Server in Python

About the project: Here, we are building a multiplayer game server.

A proper multiplayer server requires a persistent connection protocol like WebSockets, which is handled beautifully in Python using the asyncio and websockets libraries.

    Here, two files are being created.
  1. server.py: The Python backend that manages all player connections, game state, and broadcasting.
  2. client.html: A simple HTML/JavaScript frontend that connects to the server and visualizes the movement.

Setup Instructions

  1. Install Dependencies:Open your terminal and install the required library:
  2. 
      pip install websockets
      
  3. Save Files: Save the following two code blocks into the same directory:
    • The first block as server.py
    • The second block as client.html
  4. Run the Server: Execute the Python script:
  5. 
        python server.py
        
  6. Test: Open the client.html file in your web browser. To test the multiplayer functionality, open the same file in a second browser tab or window. Use the arrow keys or W/A/S/D to move your player's dot.


Project Level: Advance

1. Python WebSocket Game Server

This server handles new connections, registers player IDs, processes input commands (like UP/DOWN/LEFT/RIGHT), and broadcasts the updated state to all connected clients 20 times per second.


import asyncio
import websockets
import json
import time
import random
import os

# --- Global Game State Management ---

# Tracks connected client WebSockets
CONNECTIONS = set() 
# Simple game state: {player_id: {x: int, y: int, color: str}}
GAME_STATE = {} 
NEXT_PLAYER_ID = 1

# --- Server Logic ---

async def register(websocket):
    """Assigns a new ID and registers the connection."""
    global NEXT_PLAYER_ID, GAME_STATE
    
    # Assign new player ID
    player_id = str(NEXT_PLAYER_ID)
    NEXT_PLAYER_ID += 1
    
    # Initialize position and color for the new player (500x500 map)
    GAME_STATE[player_id] = {
        "id": player_id,
        "x": random.randint(50, 450),
        "y": random.randint(50, 450),
        "color": f"#{random.randint(0, 0xFFFFFF):06x}",
    }
    
    # Store the connection and send the player their assigned ID
    CONNECTIONS.add(websocket)
    print(f"Player {player_id} connected. Total players: {len(CONNECTIONS)}")
    await websocket.send(json.dumps({"type": "init", "id": player_id}))

    return player_id

async def unregister(websocket, player_id):
    """Removes the connection and player from the state."""
    global GAME_STATE
    
    CONNECTIONS.remove(websocket)
    if player_id in GAME_STATE:
        del GAME_STATE[player_id]
        print(f"Player {player_id} disconnected. Total players: {len(CONNECTIONS)}")

async def broadcast_state():
    """Sends the current GAME_STATE to all connected clients."""
    if CONNECTIONS:
        # Prepare the state for broadcast
        state_message = json.dumps({
            "type": "state_update",
            "players": list(GAME_STATE.values())
        })
        
        # websockets.broadcast sends the message simultaneously to all connections
        await websockets.broadcast(CONNECTIONS, state_message)

async def process_player_input(player_id, message):
    """Updates the player's position based on their command."""
    if player_id not in GAME_STATE:
        return

    player = GAME_STATE[player_id]
    
    # Simple movement logic
    speed = 10 # Speed boost for continuous presses
    
    # Min/Max bounds keep players inside the 500x500 game area (with a 10px margin)
    if message == "UP": player["y"] = max(10, player["y"] - speed)
    elif message == "DOWN": player["y"] = min(490, player["y"] + speed)
    elif message == "LEFT": player["x"] = max(10, player["x"] - speed)
    elif message == "RIGHT": player["x"] = min(490, player["x"] + speed)
    

async def handler(websocket, path):
    """Handles the lifecycle of a single client connection."""
    
    player_id = None
    try:
        # 1. Registration
        player_id = await register(websocket)
        
        # 2. Input Loop: Listens for client messages (movement commands)
        async for message in websocket:
            try:
                data = json.loads(message)
                if data.get("type") == "move" and "direction" in data:
                    # Process the movement command immediately
                    await process_player_input(player_id, data["direction"])
                
            except json.JSONDecodeError:
                print(f"Invalid JSON received from {player_id}: {message}")
            
    except websockets.exceptions.ConnectionClosed:
        # Connection closed normally or due to an error
        pass
    finally:
        # 3. Unregistration
        if player_id:
            await unregister(websocket, player_id)

async def game_tick_loop():
    """Periodically broadcasts the game state."""
    while True:
        # Send the state 20 times per second (50ms interval) for smooth updates
        await asyncio.sleep(0.05) 
        await broadcast_state()

async def main():
    """The main entry point for the server application."""
    
    # Start the state broadcast loop as a separate background task
    asyncio.create_task(game_tick_loop())
    
    # Start the WebSocket server
    host = "0.0.0.0" # Listen on all interfaces
    port = int(os.environ.get("PORT", 8765)) # Use environment port or default to 8765
    
    # The 'with' statement ensures the server stops gracefully
    async with websockets.serve(handler, host, port):
        print("--------------------------------------------------")
        print(f"WebSocket Server running on ws://{host}:{port}")
        print(f"Local access URL: ws://127.0.0.1:{port}")
        print("To test, save the client code below as 'client.html' and open it in your browser.")
        print("--------------------------------------------------")
        
        # Keeps the main task running indefinitely until interrupted
        await asyncio.Future() 

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\nServer manually stopped.")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")

  


2. HTML/JavaScript Game Client

This client connects to the server, uses keyboard and touch inputs to send movement commands, and uses the received state to render player dots on the screen.

 
  <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Multiplayer Game Client</title>
    
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        body { font-family: 'Inter', sans-serif; background-color: #f0f4f8; }
        .game-container { 
            position: relative; 
            width: 500px; 
            height: 500px; 
            margin: 20px auto; 
            background-color: #ffffff; 
            border: 5px solid #3b82f6; 
            box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1); 
            border-radius: 8px;
            /* Responsive adjustments for mobile */
            max-width: 95vw;
            max-height: 95vw;
        }
        .player-dot {
            position: absolute;
            width: 20px;
            height: 20px;
            border-radius: 50%;
            /* Smooth movement transition (must match server tick rate) */
            transition: transform 0.05s linear; 
            box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
            border: 2px solid white;
        }
    </style>
</head>
<body class="p-4">
    <div class="max-w-xl mx-auto bg-white p-6 rounded-xl shadow-2xl">
        <h1 class="text-3xl font-bold text-center text-blue-600 mb-4">WebSocket Multiplayer Demo</h1>
        <p class="text-center text-gray-500 mb-6">
            Your ID: <span id="player-id" class="font-mono font-semibold text-red-500">Connecting...</span> | 
            Status: <span id="status" class="font-semibold text-yellow-600">Disconnected</span>
        </p>

        
        <div id="game-area" class="game-container">
            
        </div>

        
        <div class="mt-8 text-center">
            <h2 class="text-xl font-semibold mb-3 text-gray-700">Movement Controls (Arrow Keys / W A S D)</h2>
            <div class="grid grid-cols-3 gap-2 w-48 mx-auto">
                <div></div>
                <button id="up-btn" class="p-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md shadow-md focus:outline-none transition">UP</button>
                <div></div>
                <button id="left-btn" class="p-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md shadow-md focus:outline-none transition">LEFT</button>
                <button id="down-btn" class="p-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md shadow-md focus:outline-none transition">DOWN</button>
                <button id="right-btn" class="p-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md shadow-md focus:outline-none transition">RIGHT</button>
            </div>
            <p class="text-xs text-gray-400 mt-3">Try opening this page in multiple browser tabs. </p>
        </div>
    </div>

    <script>
        const GAME_AREA = document.getElementById('game-area');
        const STATUS_SPAN = document.getElementById('status');
        const PLAYER_ID_SPAN = document.getElementById('player-id');
        let socket;
        let myPlayerId = null;
        
        // --- IMPORTANT: Update the port if you changed it in server.py ---
        const WS_URL = 'ws://' + (window.location.hostname || '127.0.0.1') + ':8765'; 

        // --- Connection Setup ---
        function connect() {
            if (socket) { socket.close(); }
            
            setStatus('Connecting...', 'text-yellow-600');

            try {
                // Initialize WebSocket connection
                socket = new WebSocket(WS_URL);
            } catch (error) {
                console.error("WebSocket connection failed:", error);
                setStatus('Failed to connect', 'text-red-600');
                setTimeout(connect, 5000); 
                return;
            }

            socket.onopen = () => {
                setStatus('Connected', 'text-green-600');
                console.log('WebSocket connected.');
            };

            socket.onmessage = (event) => {
                const data = JSON.parse(event.data);
                
                if (data.type === 'init') {
                    // Store player's assigned ID
                    myPlayerId = data.id;
                    PLAYER_ID_SPAN.textContent = myPlayerId;
                } else if (data.type === 'state_update') {
                    // Render the latest game state
                    renderGameState(data.players);
                }
            };

            socket.onclose = () => {
                setStatus('Disconnected', 'text-red-600');
                console.log('WebSocket disconnected. Reconnecting in 5s...');
                myPlayerId = null;
                PLAYER_ID_SPAN.textContent = 'Disconnected';
                setTimeout(connect, 5000); 
            };

            socket.onerror = (error) => {
                console.error('WebSocket error:', error);
                setStatus('Error', 'text-red-600');
            };
        }
        
        function setStatus(text, colorClass) {
            STATUS_SPAN.textContent = text;
            STATUS_SPAN.className = '';
            STATUS_SPAN.classList.add('font-semibold', colorClass);
        }

        // --- Game Rendering ---

        function renderGameState(players) {
            const dotElements = {};
            
            // Collect existing dots for fast lookup
            document.querySelectorAll('.player-dot').forEach(dot => {
                dotElements[dot.dataset.id] = dot;
            });

            // 1. Update/Create Players
            players.forEach(player => {
                let dot = dotElements[player.id];

                if (!dot) {
                    // Create new dot element
                    dot = document.createElement('div');
                    dot.classList.add('player-dot');
                    dot.dataset.id = player.id;
                    GAME_AREA.appendChild(dot);
                }
                
                // Update styling and position
                dot.style.backgroundColor = player.color;
                dot.style.zIndex = (player.id === myPlayerId) ? 10 : 5; 
                
                // Apply the CSS transform for smooth, efficient movement updates
                // Subtract 10 to center the 20x20px dot on the X/Y coordinates
                dot.style.transform = `translate(${player.x - 10}px, ${player.y - 10}px)`;
                
                // Mark element as processed
                delete dotElements[player.id];
            });

            // 2. Remove Disconnected Players
            // Any remaining elements in dotElements need to be removed
            for (const id in dotElements) {
                dotElements[id].remove();
            }
        }

        // --- Input Handling ---
        
        let moveInterval = null;
        let currentDirection = null;

        function sendMove(direction) {
            if (socket && socket.readyState === WebSocket.OPEN && myPlayerId) {
                // Send the move command to the server
                socket.send(JSON.stringify({
                    type: "move",
                    direction: direction
                }));
            }
        }
        
        // Starts sending the move command repeatedly while the key/button is held down
        function startContinuousMove(direction) {
            // Only start if the direction has changed or is null
            if (currentDirection === direction && moveInterval !== null) return; 

            // Clear any existing interval
            clearInterval(moveInterval);
            currentDirection = direction;
            
            // Send the command immediately
            sendMove(direction);

            // Set up a loop to send the command 20 times per second
            moveInterval = setInterval(() => {
                sendMove(currentDirection);
            }, 50); 
        }

        function stopContinuousMove() {
            // Stop the continuous loop
            clearInterval(moveInterval);
            moveInterval = null;
            currentDirection = null;
        }

        // --- Keyboard Event Listeners ---
        const activeKeys = new Set();
        const keyMap = {
            'ArrowUp': 'UP', 'w': 'UP',
            'ArrowDown': 'DOWN', 's': 'DOWN',
            'ArrowLeft': 'LEFT', 'a': 'LEFT',
            'ArrowRight': 'RIGHT', 'd': 'RIGHT'
        };

        document.addEventListener('keydown', (e) => {
            const direction = keyMap[e.key.toLowerCase()];
            if (direction) {
                e.preventDefault(); // Prevent scrolling
                if (!activeKeys.has(direction)) {
                    activeKeys.add(direction);
                }
                // Always use the last key pressed to set the movement direction
                startContinuousMove(direction);
            }
        });

        document.addEventListener('keyup', (e) => {
            const direction = keyMap[e.key.toLowerCase()];
            if (direction) {
                activeKeys.delete(direction);
                
                if (activeKeys.size > 0) {
                    // If another key is still down, switch to the most recently pressed one
                    const remainingDirections = Array.from(activeKeys);
                    startContinuousMove(remainingDirections[remainingDirections.length - 1]);
                } else {
                    // Stop movement completely if no keys are down
                    stopContinuousMove();
                }
            }
        });
        
        // --- Touch/Mouse Event Listeners ---

        function setupButtonListeners(buttonId, direction) {
            const btn = document.getElementById(buttonId);
            
            // Mouse events
            btn.addEventListener('mousedown', () => startContinuousMove(direction));
            btn.addEventListener('mouseup', stopContinuousMove);
            btn.addEventListener('mouseleave', stopContinuousMove);
            
            // Touch events
            btn.addEventListener('touchstart', (e) => { e.preventDefault(); startContinuousMove(direction); }, false);
            btn.addEventListener('touchend', stopContinuousMove, false);
            btn.addEventListener('touchcancel', stopContinuousMove, false);
        }

        setupButtonListeners('up-btn', 'UP');
        setupButtonListeners('down-btn', 'DOWN');
        setupButtonListeners('left-btn', 'LEFT');
        setupButtonListeners('right-btn', 'RIGHT');
        
        // Start the connection when the page loads
        document.addEventListener('DOMContentLoaded', connect);

    </script>
</body>
</html>


You can directly copy the code using green 📋 Copy Code button and directly paste the same into a file and save it.



← Back to Projects