Next.jsとp5.jsを使って簡単にクソゲーをWebアプリにして公開してみる

p5.jsを使うと簡単にゲームが作れることを知ったので、Next.jsで動かしてみたという備忘録です。

Webエディタが用意されているので、簡単に触ってみるといいかも知れません。:point_down:

https://editor.p5js.org/

## 前提条件
- WSL2がインストールされているWindowsまたはLinux環境
- `npm`が使えること（Node.jsがインストールされている）

## 1. Next.js プロジェクトの作成
まず、`create-next-app` を使って新しいNext.jsのプロジェクトを作成します。

```sh
npx create-next-app@latest my-game-app
```

### `create-next-app` のオプションについて
以下のようなオプションが表示されるので、適宜選択します。

1. **Would you like to use TypeScript with this project?** → `Yes`
2. **Would you like to use ESLint with this project?** → `Yes`
3. **Would you like to use Tailwind CSS with this project?** → `No`（今回は不要）
4. **Would you like to use `src/` directory with this project?** → `No`
5. **Would you like to use experimental `app/` directory with this project?** → `Yes`

選択が完了すると、`my-game-app` というディレクトリにNext.jsプロジェクトが作成されます。

```sh
cd my-game-app

npm install react-p5
```

これでNext.jsとp5.jsが入りました
準備完了です。

## 2. `components` フォルダを作成し、`p5.tsx` を作成

```sh
mkdir components
touch components/p5.tsx
```

`p5.tsx` にはp5.jsを使った描画処理を記述します。
今回は下記の記事を参考に作ってみました。ありがとうございます:pray:

https://note.com/rodz/n/nba072e615cc0

```tsx
// components/p5.tsx
"use client";

import dynamic from "next/dynamic";
import p5Types from "p5";

const Sketch = dynamic(() => import('react-p5'), {
    loading: () => null,
    ssr: false
});

const P5Component = () => {
    let basketX: number;
    let basketY: number;
    let basketWidth: number;
    let basketHeight: number;
    let score: number = 0;
    let basketImg: p5Types.Image;
    let acornImg: p5Types.Image;
    let isTouching: boolean = false;
    const acorns: { x: number, y: number }[] = [];

    // 画像などのロードを行う
    const preload = (p5: p5Types) => {
        basketImg = p5.loadImage('/images/basket.png'); // カゴの画像
        acornImg = p5.loadImage('/images/acorn.png'); // 🌰の画像
    };

    // 初期処理
    const setup = (p5: p5Types, canvasParentRef: Element) => {
        p5.createCanvas(p5.windowWidth, p5.windowHeight).parent(canvasParentRef);
        basketWidth = 100;
        basketHeight = 50;
        basketX = p5.windowWidth / 2 - basketWidth / 2;
        basketY = p5.windowHeight - basketHeight - 10;
    };

    // 1フレームごとの処理
    const draw = (p5: p5Types) => {
        p5.background(255, 255, 255);

        // カゴの描画
        p5.image(basketImg, basketX, basketY, basketWidth, basketHeight);

        // 🌰の描画と落下
        for (let i = acorns.length - 1; i >= 0; i--) {
            const acorn = acorns[i];
            acorn.y += 5;
            p5.image(acornImg, acorn.x, acorn.y, 20, 20);

            // カゴに入ったかどうかの判定
            if (acorn.y > basketY && acorn.x > basketX && acorn.x < basketX + basketWidth) {
                acorns.splice(i, 1);
                score++;
            } else if (acorn.y > p5.windowHeight) {
                acorns.splice(i, 1);
            }
        }

        // スコアの表示
        p5.fill(0);
        p5.textSize(32);
        p5.text(`Score: ${score}`, 10, 30);

        // 新しい🌰を追加
        if (p5.frameCount % 15 === 0) {
            acorns.push({ x: p5.random(20, p5.windowWidth - 20), y: 0 });
        }

        // カゴの移動（キーボード操作）
        if (p5.keyIsDown(p5.LEFT_ARROW)) {
            basketX -= 10;
        }
        if (p5.keyIsDown(p5.RIGHT_ARROW)) {
            basketX += 10;
        }

        // カゴの移動（マウス操作）
        if (p5.mouseIsPressed) {
            basketX = p5.mouseX - basketWidth / 2;
        }
    };

    // タッチ操作の開始
    const touchStarted = (p5: p5Types) => {
        isTouching = true;
        return false;
    };

    // タッチ操作の終了
    const touchEnded = (p5: p5Types) => {
        isTouching = false;
        return false;
    };

    // タッチ操作の移動
    const touchMoved = (p5: p5Types) => {
        if (isTouching && p5.touches.length > 0) {
            const touch = p5.touches[0] as Touch;
            basketX = touch.clientX - basketWidth / 2;
        }
        return false;
    };

    // コンポーネントのレスポンシブ化
    const windowResized = (p5: p5Types) => {
        p5.resizeCanvas(p5.windowWidth, p5.windowHeight);
        basketY = p5.windowHeight - basketHeight - 10;
    };

    return (
        <Sketch
            preload={preload}
            setup={setup}
            draw={draw}
            touchStarted={touchStarted}
            touchEnded={touchEnded}
            touchMoved={touchMoved}
            windowResized={windowResized}
        />
    );
};

export default P5Component;
```

## 3. `app/page.tsx` を編集する
次に、`app/page.tsx` を編集して、`p5.tsx` を読み込むようにします。

```tsx
// app/page.tsx
import P5Component from '@/components/p5'

export default function Home() {
  return (
    <>
      <P5Component />
    </>
  )
}
```

これで、`p5.tsx` を `Home` コンポーネントで表示する準備が整いました。

## 4. ローカルで確認する
プロジェクトのルートディレクトリに戻り、開発サーバーを起動します。

```sh
npm run dev
```

ブラウザで `http://localhost:3000` にアクセスし、ページが表示されることを確認しましょう。

## 5. Vercel にデプロイ
Next.jsはVercelとの相性が非常に良いため、簡単にデプロイできます。

### Vercel にデプロイする手順
1. [Vercelの公式サイト](https://vercel.com/) にアクセスします。
2. `Log in` をクリックし、GitHubアカウントなどでログインします。
3. ログイン後、`New Project` をクリックします。
4. GitHubと連携し、デプロイしたいリポジトリを選択します。
5. 設定を確認し、`Deploy` ボタンを押します。
6. 数分でデプロイが完了し、公開URLが発行されます。

このURLを開いて、ゲームがWeb上に公開されたことを確認しましょう。

## まとめ
1. `create-next-app` で Next.js のプロジェクトを作成
2. `components` フォルダを作成し `p5.tsx` を追加
3. `app/page.tsx` を編集して `p5.tsx` を読み込む
4. `npm run dev` でローカル確認
5. Vercelを使ってデプロイ

これで、簡単にクソゲーをWebアプリにして公開できましたー！🚀

今回作ったGitHubリポジトリ

https://github.com/TateruNakayama/next-p5-app

デプロイしたWebアプリ(クソゲー)

https://next-p5-k62xxrg20-taterunakayamas-projects.vercel.app/