はじめに
この記事では、Python の Flask と WebSocket を使って、簡単なリアルタイムゲームサーバーを作成します。
作るものは、ブラウザ上でプレイヤーを動かすだけのシンプルなゲームです。
複数人が同じページを開くと、それぞれのプレイヤーの位置がリアルタイムに共有されます。
今回は WebSocket 通信を簡単に扱うために、Flask-SocketIO を使います。
厳密には、Flask-SocketIO は生の WebSocket ではなく、Socket.IO という仕組みを使います。
ただし、リアルタイム通信の入門としては非常に扱いやすいです。
完成イメージ
このゲームでは、次のような動きを実装します。
- ユーザーがページにアクセスする
- サーバーがプレイヤーを登録する
- 矢印キーでプレイヤーを移動する
- 移動情報をサーバーへ送信する
- サーバーが他のプレイヤーへ位置情報を配信する
- 接続が切れたプレイヤーを削除する
使用する技術
今回使う技術は次の通りです。
- Python
- Flask
- Flask-SocketIO
- JavaScript
- HTML Canvas
Flask は通常の Web ページ表示を担当します。
Flask-SocketIO はリアルタイム通信を担当します。
Canvas はブラウザ上でプレイヤーを描画するために使います。
ディレクトリ構成
まず、次のような構成にします。
game-server/
├── app.py
└── templates/
└── index.html
ライブラリのインストール
仮想環境を作成します。
python -m venv venv
有効化します。
Windows の場合です。
venv\Scripts\activate
macOS / Linux の場合です。
source venv/bin/activate
次に、必要なライブラリをインストールします。
pip install flask flask-socketio
サーバー側の実装
app.py を作成します。
from flask import Flask, render_template, request
from flask_socketio import SocketIO, emit
import random
app = Flask(__name__)
app.config["SECRET_KEY"] = "dev"
socketio = SocketIO(app, cors_allowed_origins="*")
players = {}
@app.route("/")
def index():
return render_template("index.html")
@socketio.on("connect")
def handle_connect():
player_id = request.sid
players[player_id] = {
"x": random.randint(50, 350),
"y": random.randint(50, 350)
}
emit("init", {
"id": player_id,
"players": players
})
emit("player_joined", {
"id": player_id,
"player": players[player_id]
}, broadcast=True, include_self=False)
print(f"connect: {player_id}")
@socketio.on("move")
def handle_move(data):
player_id = request.sid
if player_id not in players:
return
dx = int(data.get("dx", 0))
dy = int(data.get("dy", 0))
players[player_id]["x"] += dx
players[player_id]["y"] += dy
players[player_id]["x"] = max(0, min(500, players[player_id]["x"]))
players[player_id]["y"] = max(0, min(500, players[player_id]["y"]))
emit("player_moved", {
"id": player_id,
"player": players[player_id]
}, broadcast=True)
@socketio.on("disconnect")
def handle_disconnect():
player_id = request.sid
if player_id in players:
del players[player_id]
emit("player_left", {
"id": player_id
}, broadcast=True)
print(f"disconnect: {player_id}")
if __name__ == "__main__":
socketio.run(app, host="0.0.0.0", port=5000, debug=True)
サーバー側のポイント
players という辞書で、接続中のプレイヤー情報を管理しています。
players = {}
プレイヤーが接続すると、connect イベントが発生します。
@socketio.on("connect")
def handle_connect():
ここで、プレイヤーIDとして request.sid を使っています。
これは接続ごとに割り当てられるIDです。
移動時は、クライアントから move イベントを受け取ります。
@socketio.on("move")
def handle_move(data):
その後、サーバー側で座標を更新し、全クライアントへ player_moved イベントを送信します。
emit("player_moved", {
"id": player_id,
"player": players[player_id]
}, broadcast=True)
これにより、他のブラウザにも移動結果が反映されます。
クライアント側の実装
次に、templates/index.html を作成します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Simple Game Server</title>
<style>
body {
font-family: sans-serif;
text-align: center;
}
canvas {
border: 1px solid #333;
background: #f5f5f5;
}
</style>
</head>
<body>
<h1>Flask + WebSocket ゲームサーバー</h1>
<p>矢印キーで自分のプレイヤーを動かします。</p>
<canvas id="game" width="500" height="500"></canvas>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<script>
const socket = io();
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
let myId = null;
let players = {};
socket.on("init", (data) => {
myId = data.id;
players = data.players;
draw();
});
socket.on("player_joined", (data) => {
players[data.id] = data.player;
draw();
});
socket.on("player_moved", (data) => {
players[data.id] = data.player;
draw();
});
socket.on("player_left", (data) => {
delete players[data.id];
draw();
});
document.addEventListener("keydown", (event) => {
const speed = 10;
let dx = 0;
let dy = 0;
if (event.key === "ArrowUp") {
dy = -speed;
} else if (event.key === "ArrowDown") {
dy = speed;
} else if (event.key === "ArrowLeft") {
dx = -speed;
} else if (event.key === "ArrowRight") {
dx = speed;
} else {
return;
}
socket.emit("move", {
dx: dx,
dy: dy
});
});
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const id in players) {
const player = players[id];
if (id === myId) {
ctx.fillStyle = "blue";
} else {
ctx.fillStyle = "red";
}
ctx.beginPath();
ctx.arc(player.x, player.y, 10, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "black";
ctx.fillText(id.slice(0, 4), player.x + 12, player.y + 4);
}
}
</script>
</body>
</html>
実行方法
次のコマンドでサーバーを起動します。
python app.py
ブラウザで次のURLを開きます。
http://localhost:5000
複数のブラウザやタブで開くと、複数プレイヤーの動きを確認できます。
この構成の仕組み
通常のHTTP通信では、ブラウザからサーバーへリクエストを送ったときだけレスポンスが返ります。
一方、リアルタイムゲームでは、サーバーからブラウザへ即座に情報を送る必要があります。
そこで WebSocket を使います。
WebSocket を使うと、クライアントとサーバーの間で接続を維持したまま、双方向にデータを送れます。
今回の構成では、次のような流れになります。
ブラウザ
↓ moveイベント
Flask-SocketIOサーバー
↓ player_movedイベント
全ブラウザ
これにより、あるプレイヤーの移動が他のプレイヤーにもすぐ反映されます。
発展させるなら
このサンプルはかなり簡単な実装です。
本格的なゲームサーバーにするなら、次のような機能を追加できます。
- プレイヤー名の設定
- ルーム機能
- 当たり判定
- HPやスコア
- チャット機能
- ログイン機能
- データベース保存
- 不正な移動の検出
- サーバー側でのゲームループ
- Nginxを使った公開
- HTTPS対応
特に重要なのは、サーバー側で状態を管理することです。
クライアント側の情報をそのまま信じると、不正な移動やチートが可能になります。
例えば、今回のコードではクライアントが dx や dy を送っています。
本格的に作るなら、移動量の上限チェックや、移動速度の検証をサーバー側で行うべきです。
まとめ
今回は、Flask と Flask-SocketIO を使って、簡単なゲームサーバーを作成しました。
Flask は通常のWebページ配信を担当し、Flask-SocketIO はリアルタイム通信を担当します。
この構成を使えば、簡単なオンラインゲーム、チャット、共同編集ツール、リアルタイム通知などを作ることができます。
小規模なリアルタイムアプリの学習用としては、Flask + Socket.IO はかなり扱いやすい構成です。