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.
- server.py: The Python backend that manages all player connections, game state, and broadcasting.
- client.html: A simple HTML/JavaScript frontend that connects to the server and visualizes the movement.
Setup Instructions
- Install Dependencies:Open your terminal and install the required library:
- Save Files: Save the following two code blocks into the same directory:
- The first block as server.py
- The second block as client.html
- Run the Server: Execute the Python script:
- 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.
pip install websockets
python server.py
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
