Multiplayer game Example in Javascript & Python
A simple example. An game that has a backend in Python and frontend in Javascript, using pixi.js.
➡ View demo online
➡ View full source on GitHub
➡ Run the example
Running the example
You can run this self-contained example using Docker.
git clone https://github.com/scalesocket/scalesocket
cd scalesocket/examples/
docker compose up --build multiplayer
Then open http://localhost:5000/
in your browser.
Frontend code
The frontend consists of three files index.html
, client.js
and bunny.png
. Pixi.js is used to simplify drawing and managing sprites.
The index.html
file loads the game and connects to the server using websockets.
<!doctype html>
<html lang="en">
<head>
<title>Multiplayer Game Example using ScaleSocket</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://pixijs.download/v7.0.0/pixi.min.js"></script>
<script type="module" src="./client.js"></script>
<style>
body { background-color: #e2e8f0; font-family: Arial, Helvetica, sans-serif; margin: 0; text-align: center; }
canvas { max-width: 100vw; height: auto; }
</style>
</head>
<body>
<div>
<h1>Multiplayer Game Example</h1>
<p><select
onchange="let value = this.options[this.selectedIndex].value || (Math.random() + 1).toString(36).substring(7); window.location.search = `?room=${value}`;">
<option disabled selected>Change Room</option>
<option value="default">Room 0 (default)</option>
<option value="room1">Room 1</option>
<option value="room2">Room 2</option>
<option value="room3">Room 3</option>
<option value="">Create new room</option>
</select></p>
</div>
<p>Multiplayer game, try tapping around and opening multiple instances. Built with <a
href="https://www.scalesocket.org/man/examples/multiplayer" target="_blank">ScaleSocket</a>.</p>
<canvas id="canvas"></canvas>
<script type="module">
import { Game } from "./client.js";
const $ = document.querySelector.bind(document);
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
const params = new URLSearchParams(document.location.search);
const room = params.get("room") ?? 'default';
// create a new PIXI application with resources
const app = new PIXI.Application({
width: 480, height: 480, autoDensity: true,
view: $('#canvas'),
backgroundColor: 0xffffff
});
PIXI.Assets.addBundle('default', { 'bunny': 'bunny.png' });
let load = PIXI.Assets.loadBundle('default');
// connect to websocket server and room based on URL
const ws = new WebSocket(`${protocol}:${window.location.host}/${room}`);
let connect = new Promise((resolve, reject) => {
ws.addEventListener('open', (event) => resolve(), { once: true });
ws.addEventListener('error', (event) => reject(), { once: true });
});
// start the game
void Promise.all([load, connect]).then(async ([resources, _]) => {
const game = new Game(app, ws, resources);
await game.init();
});
</script>
</body>
</html>
The actual frontend logic is in client.js
. We receive message from the websocket and update sprites (players) based on it. When the player clicks on the screen, we send the input to the server.
// client.js
export class Game {
constructor(app, ws, resources) {
this.app = app;
this.ws = ws;
this.resources = resources;
this.state = new GameState(this.app.stage, resources);
}
sendMessage(t, data) {
// send outgoing message
this.ws.send(JSON.stringify({ t, data }));
}
recvMessage(t, data) {
// handle incoming message
if (t == "State") {
this.state.updatePlayers(data.players);
} else if (t == "Leave") {
this.state.removePlayer(data.id);
}
}
async init() {
// set websocket listener
this.ws.addEventListener('message', e => {
const { t, data } = JSON.parse(e.data);
this.recvMessage(t, data);
});
// set click listener
this.app.stage.interactive = true;
this.app.stage.hitArea = this.app.screen;
this.app.stage.on('pointerdown', (e) => {
this.sendMessage('Input', { x: e.global.x, y: e.global.y });
});
// set interpolation to run on every tick
PIXI.Ticker.shared.add(dt => this.state.interpolate(dt));
}
}
class GameState {
/** @type {Record<number, {sprite: any, pos: any}>} */
players = {}
constructor(stage, resources) {
this.stage = stage;
this.resources = resources;
}
updatePlayers(players) {
for (const [id, pos] of Object.entries(players)) {
this.updatePlayer(id, pos);
}
}
updatePlayer(id, pos) {
if (id in this.players) {
this.players[id].pos = pos;
} else {
this.addPlayer(id, pos);
}
}
addPlayer(id, pos) {
const [x, y] = pos;
const sprite = new PIXI.Sprite(new PIXI.Texture(this.resources.bunny.baseTexture));
sprite.anchor.set(0.5);
sprite.position.set(x, y);
this.players[id] = { pos, sprite };
this.stage.addChild(sprite);
}
removePlayer(id) {
if (id in this.players) {
console.log('removing player');
const sprite = this.players[id].sprite;
this.stage.removeChild(sprite);
sprite.destroy();
delete this.players[id];
}
}
interpolate(_dt) {
for (const { pos, sprite } of Object.values(this.players)) {
const [x, y] = pos;
const dx = (x - sprite.x) / 3;
const dy = (y - sprite.y) / 3;
sprite.x += dx;
sprite.y += dy;
}
}
}
Backend code
The backend is a single file, server.py
. It reads stdin in a loop, and updates state based on incoming messages.
#!/usr/bin/python3
from contextlib import suppress
from json import JSONDecodeError, loads, dumps
from sys import stdin, stdout, stderr
def main():
print("game server started", file=stderr)
players = {}
# receiving data is as easy as reading stdin
stdin_events = map(parse_json, stdin)
for event in stdin_events:
t, id, data = parse_event(event)
if t == "Join": # default join event for new clients
players[id] = (150, 150)
send_event("State", {"players": players}, to_id=id)
elif t == "Leave": # default leave event for disconnected clients
del players[id]
send_event("Leave", {"id": id})
elif t == "Input": # our custom event for player input
players[id] = (data.get("x", 0), data.get("y", 0))
send_event("State", {"players": players})
def send_event(t: str, data: dict, to_id: int = None):
# sending data is as easy as printing
print(dumps({"t": t, "data": data, "_to": to_id}))
def parse_json(data: str):
with suppress(JSONDecodeError):
return loads(data)
return None
def parse_event(event: dict):
with suppress(KeyError):
return event["t"], int(event["_from"]), event.get("data")
return None, None, None
if __name__ == "__main__":
# ensure python output is not buffered
stdout.reconfigure(line_buffering=True)
main()
Backend server
The backend is the ScaleSocket server.
We want to:
- let players join rooms based on URL
- start a new
server.py
process when a new user connects - send client connect and disconnect events to the server
- host the static files
To do this, start ScaleSocket using:
scalesocket --addr 0.0.0.0:5000\
--staticdir /var/www/public/ \
--frame=json \
--joinmsg '{"t":"Join","_from":#ID}'\
--leavemsg '{"t":"Leave","_from":#ID}'\
server.py
How does it work?
The frontend connects to the server using websockets. The server spins up a new server.py
process.
When the frontend sends a input, ScaleSocket passes it directly to the stdin of server.py
.
The input is read in a loop by mapping json parser over the stdin
which is a generator in Python.
# excerpt from server.py
from sys import stdin
def parse_json(data: str):
with suppress(JSONDecodeError):
return loads(data)
return None
stdin_events = map(parse_json, stdin)
for event in stdin_events:
# do something with the event
# in our case the frontend sends
# {"t": "Input", "data": {"x": 123, "y": 456}}
# ...
The events update the local state on the backend, and the backend sends the new state to stdout, by printing the state.
# excerpt from server.py
from json import dumps
# ...
# sending is as easy as printing
print(dumps({"t": t, "data": data}))
# optionally, we may specify a receiver by using "_to"
print(dumps({"t": t, "data": data, "_to": to_id}))
Was this page helpful?