FastAPIでブロック崩しゲームを作ってみた...が失敗した話

## はじめに

こんにちは、みなさん。今回は、私が「FastAPIでブロック崩しゲームを作る」という少し変わったチャレンジをしてみた経験を共有したいと思います。結論から言うと、このプロジェクトは技術的には成功しましたが、実用面では大きな失敗でした。この記事を通じて、私の失敗から得られた教訓をみなさんと共有できればと思います。

FastAPIでhtmlを出力するからのながれ、ゲームもできるかなと甘い考えで取り組んだ失敗談です。

## はじめに：なぜFastAPIでゲームを？

私がFastAPIでゲームを作ろうと思い立ったのには、いくつかの理由がありました：

1. リアルタイム通信の学習：WebSocketを使ったリアルタイム通信の実装を学ぶ良い機会になると考えました。
2. サーバーサイドゲームロジック：チート防止やマルチプレイヤー機能の実装を容易にできるのではないかと期待しました。
3. 新しい可能性の探求：Web技術の新しい使い方を模索することで、何か面白い発見があるかもしれないと思いました。

## 実装

### ファイル構成

```
fastapi-breakout/
│
├── main.py          # FastAPIのメインアプリケーションファイル
├── index.html       # ゲームのフロントエンドHTML/JavaScriptファイル
└── requirements.txt # プロジェクトの依存関係リスト
```

### サーバーサイド（FastAPI）

```python
from fastapi import FastAPI, WebSocket
from fastapi.responses import HTMLResponse
import json

app = FastAPI()

class Game:
    def __init__(self):
        self.paddle_x = 350
        self.ball_x = 400
        self.ball_y = 300
        self.ball_dx = 2
        self.ball_dy = -2
        self.blocks = [[1 for _ in range(10)] for _ in range(5)]

    def update(self):
        # ボールの移動
        self.ball_x += self.ball_dx
        self.ball_y += self.ball_dy

        # 壁との衝突判定
        if self.ball_x <= 0 or self.ball_x >= 800:
            self.ball_dx *= -1
        if self.ball_y <= 0:
            self.ball_dy *= -1

        # パドルとの衝突判定
        if self.ball_y >= 580 and self.paddle_x <= self.ball_x <= self.paddle_x + 100:
            self.ball_dy *= -1

        # ブロックとの衝突判定
        for i in range(5):
            for j in range(10):
                if self.blocks[i][j] == 1:
                    if (i * 30 <= self.ball_y <= (i + 1) * 30 and
                        j * 80 <= self.ball_x <= (j + 1) * 80):
                        self.blocks[i][j] = 0
                        self.ball_dy *= -1
                        break

game = Game()

@app.get("/")
async def get():
    with open("index.html", "r") as f:
        return HTMLResponse(content=f.read())

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    while True:
        data = await websocket.receive_text()
        game.paddle_x = int(data)
        game.update()
        await websocket.send_text(json.dumps({
            "paddle_x": game.paddle_x,
            "ball_x": game.ball_x,
            "ball_y": game.ball_y,
            "blocks": game.blocks
        }))
```

### クライアントサイド（HTML/JavaScript）

```html
<!DOCTYPE html>
<html>
<head>
    <title>ブロック崩しゲーム</title>
    <style>
        #gameCanvas {
            border: 1px solid black;
        }
    </style>
</head>
<body>
    <canvas id="gameCanvas" width="800" height="600"></canvas>
    <script>
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const socket = new WebSocket('ws://localhost:8000/ws');

        let gameState = {
            paddle_x: 350,
            ball_x: 400,
            ball_y: 300,
            blocks: []
        };

        socket.onmessage = function(event) {
            gameState = JSON.parse(event.data);
            drawGame();
        };

        function drawGame() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            // パドルを描画
            ctx.fillStyle = 'blue';
            ctx.fillRect(gameState.paddle_x, 580, 100, 10);

            // ボールを描画
            ctx.beginPath();
            ctx.arc(gameState.ball_x, gameState.ball_y, 10, 0, Math.PI * 2);
            ctx.fillStyle = 'red';
            ctx.fill();
            ctx.closePath();

            // ブロックを描画
            ctx.fillStyle = 'green';
            for (let i = 0; i < gameState.blocks.length; i++) {
                for (let j = 0; j < gameState.blocks[i].length; j++) {
                    if (gameState.blocks[i][j] === 1) {
                        ctx.fillRect(j * 80, i * 30, 75, 25);
                    }
                }
            }
        }

        canvas.addEventListener('mousemove', function(event) {
            const rect = canvas.getBoundingClientRect();
            const mouseX = event.clientX - rect.left;
            socket.send(mouseX.toString());
        });

        function gameLoop() {
            requestAnimationFrame(gameLoop);
        }

        gameLoop();
    </script>
</body>
</html>
```

## 失敗の内容

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/d08f8210-7d91-0f0a-b08c-5f10fdfc5429.png)
マウスでバーを操作する

コードを書き、ゲームを動かすところまでは順調に進みました。しかし、実際にプレイしてみると、以下の問題が明らかになりました：

1. 遅延：サーバーとクライアント間の通信に時間がかかり、ゲームの動きがぎこちなくなりました。
2. 同期の問題：クライアントとサーバーの状態が完全に同期せず、時々ボールの位置が飛び飛びに移動しているように見えました。

結果として、ゲームは技術的には「動く」ものの、実用的な面では使い物にならないものとなってしまいました。

## 得られた教訓

この失敗から、以下の教訓を得ることができました：

1. 適切な技術の選択：ゲーム開発には、リアルタイム性能に優れた専用のゲームエンジンやフレームワークを使用するべきです。
2. ネットワーク遅延の考慮：WebベースのリアルタイムアプリケーションでCJは、ネットワーク遅延を常に考慮に入れる必要があります。
3. クライアントサイド予測：スムーズなゲーム体験を提供するためには、クライアントサイドでの状態予測とサーバーサイドでの検証の組み合わせが重要です。
4. スケーラビリティの考慮：マルチプレイヤーゲームを開発する際は、初期段階からスケーラビリティを考慮に入れる必要があります。

## まとめ

このプロジェクトは失敗に終わりましたが、非常に貴重な学びの機会となりました。時には「やってはいけないこと」を実際にやってみることで、より深い理解が得られることがあります。

この経験から、私は以下のことを学びました：

1. 実験的なアプローチは価値があるが、プロダクション用途には確立された手法を使うべき。
2. Webテクノロジーは多様な用途に使えるが、それぞれの技術には適切な使用領域がある。
3. 失敗は学びの機会であり、それを他者と共有することで、コミュニティ全体の知識を向上させることができる。

みなさんも、新しいアイデアに挑戦する際は、その技術の限界と可能性をよく考慮してみてください。そして、たとえ失敗したとしても、そこから学んだことを大切にしてください。

最後に、この記事を読んでくださったみなさん、ありがとうございます。私の失敗談が、少しでもみなさんの助けになれば幸いです。Happy coding!