ホーム / ブログ

Flask + WebSocketで簡単なゲームサーバーを作る方法

はじめに

この記事では、Python の Flask と WebSocket を使って、簡単なリアルタイムゲームサーバーを作成します。

作るものは、ブラウザ上でプレイヤーを動かすだけのシンプルなゲームです。

複数人が同じページを開くと、それぞれのプレイヤーの位置がリアルタイムに共有されます。

今回は WebSocket 通信を簡単に扱うために、Flask-SocketIO を使います。

厳密には、Flask-SocketIO は生の WebSocket ではなく、Socket.IO という仕組みを使います。
ただし、リアルタイム通信の入門としては非常に扱いやすいです。


完成イメージ

このゲームでは、次のような動きを実装します。


使用する技術

今回使う技術は次の通りです。

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イベント
全ブラウザ

これにより、あるプレイヤーの移動が他のプレイヤーにもすぐ反映されます。


発展させるなら

このサンプルはかなり簡単な実装です。

本格的なゲームサーバーにするなら、次のような機能を追加できます。

特に重要なのは、サーバー側で状態を管理することです。

クライアント側の情報をそのまま信じると、不正な移動やチートが可能になります。

例えば、今回のコードではクライアントが dxdy を送っています。
本格的に作るなら、移動量の上限チェックや、移動速度の検証をサーバー側で行うべきです。


まとめ

今回は、Flask と Flask-SocketIO を使って、簡単なゲームサーバーを作成しました。

Flask は通常のWebページ配信を担当し、Flask-SocketIO はリアルタイム通信を担当します。

この構成を使えば、簡単なオンラインゲーム、チャット、共同編集ツール、リアルタイム通知などを作ることができます。

小規模なリアルタイムアプリの学習用としては、Flask + Socket.IO はかなり扱いやすい構成です。

🐦 ポストする ← 記事一覧へ