昔懐かし電流イライラ棒系びっくりFlashゲームをReactで作る

# はじめに

皆さんこんにちは。y_masuyamaと申します。

最近、Reactを学び始めたばかりですが、何かアプリを作ってみたいという思いが強くなってきました。
そこで、昔懐かし電流イライラ棒系びっくりFlashゲームを再現してみました。

# 作成したもの

![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3860403/56e3aad0-bc38-fce8-c359-601be3cae514.gif)

- *ソースコード（StackBlitzで公開しています）*
  - https://stackblitz.com/edit/maze-app-gf5ict?file=README.md

:::note info
- 📱スマホでは遊べません（💻PCでお楽しみください）
- 🖥️ブラウザ全画面表示でのみ遊べます
    - ゲーム推奨画面サイズは1920 × 1080です(Canvasサイズを固定値1280 × 720で設定してるため)
- 🎵音や驚き要素が含まれているのでご注意ください
- 📚筆者環境（Mac + Chrome）でのみ動作確認をしています
:::

## ゲーム内容

全3面の「電流イライラ棒」系のゲームです。
3面の細い迷路を超える際に驚きの画像と音が表示されます。

## 使用技術

| 使用技術 | 説明 |
|:--|:--|
| StackBlitz | Reactの動作環境として使用しています（コンソールを見る限り、環境はViteっぽいです）。 |
| HTML, CSS | 迷路部分は`canvas`タグで実装しています。 |
| React | JavaScriptのライブラリです。 |
| TypeScript | JavaScriptのスーパーセット言語です。 |
| howler.js | 音声出力ライブラリです。 |

＊ ReactやTypeScriptなどのバージョンについてはソースコードの[package.json](https://stackblitz.com/edit/maze-app-gf5ict?file=package.json)に記載してますのでこちらご確認ください。

# コードの解説

簡単にコードの解説を行います。
なお、ここでは大幅に省略しているので、詳細は上記リンクのソースコードを参照してください。

## ゲーム画面の遷移

Reactの[useState](https://ja.react.dev/reference/react/useState)フックを使って実装しています。

`App.tsx`に以下のような`state`と`setState`関数を作成し、ステートが変わることで画面が切り替わるようにしています。

### state

```typescript
  const [scene, setScene] = useState<'title' | 'game' | 'ending'>('title');
```
### stateのset関数

```typescript
  const gameStart = () => setScene('game');
  const gameOver = () => setScene('title');
  const viewEnding = () => setScene('ending');
```

## タイトル画面

Playボタンを押すとgameStart関数が発火し、迷路ゲーム画面が表示されるように実装しています。

```typescript
export const Title = ({ handleClick }: Props) => {
  return (
     <!-- 省略 -->
      <button onClick={handleClick} className="title__button">
        <span className="title__buttonText">play</span>
      </button>
  );
};
```

## 迷路ゲーム画面

以下のコンポーネントで実装しています。

- Mazeコンポーネント
  - 迷路切り替え用のコンポーネント
- Level1, Level2, Level3コンポーネント
  - 迷路ゲームのコンポーネントで、ステージごとに存在します

## 迷路の切り替え

タイトル画面と同じようにReactの[useState](https://ja.react.dev/reference/react/useState)フックを使って実装しています。

Maze.tsxに以下のようなstateを作成し、ステートが変わることで画面が切り替わるようにしています。

### state

```typescript
const [level, setLevel] = useState(1);
```

## 迷路の描画

迷路は[canvas](https://developer.mozilla.org/ja/docs/Web/HTML/Element/canvas)要素で描画してます。

### Canvas要素の取得

canvas要素のメソッドを使いたいため、React公式ドキュメント([ref で DOM を操作する](https://ja.react.dev/reference/react/useRef#manipulating-the-dom-with-a-ref))を参考にcanvasのDOMを取得しております。

```typescript
const canvasRef = useRef<HTMLCanvasElement>(null);

useEffect(() => {
    const ctx = canvasRef.current?.getContext('2d');
    
    // canvasがなければ早期return
    if (!ctx) return;

    // 以降canvasを使いたい場合の処理
}, []);
```

[useEffect](https://ja.react.dev/reference/react/useEffect)フックはReact外部のシステムと同期させるフックで、上記のようにDOMを操作したい場合も使えます。

また、上記で取得している[getContext('2d')](https://developer.mozilla.org/ja/docs/Web/API/HTMLCanvasElement/getContext)はcanvas要素の2次元の描画コンテキストオブジェクトを作成するメソッドです。

### Canvas要素での画面描画

迷路の作成はcanvasの[fillRect](https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/fillRect)メソッドを使って描画してます。

```typescript
const ctx = canvasRef.current?.getContext('2d');

// canvasがなければ早期return
if (!ctx) return;

// 背景部分
ctx.fillStyle = 'black'; // 色の指定
ctx.fillRect(0, 0, width, height); // x, y, 横幅, 縦幅を指定して染め上げる
```

迷路の描画の部分に関してあまり上手いやり方ではないのですが長方形を重ねることで迷路を作成してます。

```typescript
// 迷路部分
ctx.fillStyle = 'aqua'; // 色の指定
ctx.fillRect(500, 40, 610, 60);　// x, y, 横幅, 縦幅を指定して染め上げる
ctx.fillRect(500, 40, 300, 670);　// x, y, 横幅, 縦幅を指定して染め上げる

// ゴール部分
ctx.fillStyle = 'red';　// 色の指定
ctx.fillRect(1050, 40, 80, 60);　// x, y, 横幅, 縦幅を指定して染め上げる
```

また文字の挿入は[fillText](https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/fillText)メソッドで実装してます。

```typescript
// 文字追加
ctx.font = 'bold 24px verdana, sans-serif ';
const welcomeMessage = 'Level1';
ctx.textAlign = 'start';
ctx.textBaseline = 'bottom';
ctx.fillStyle = 'white';
ctx.fillText(welcomeMessage, 1050, 130);
```

## 迷路にロジックを追加

迷路が描画できたら、次にゲームオーバーとクリアの判定ロジックを追加します。

ロジックの仕組みはシンプルで、以下の流れで実装しています。

- マウスポインタの位置を取得
- 取得した位置にあるcanvas要素の色を取得
- 取得した色に応じて処理を追加
  - 黒ならゲームオーバー
  - 赤ならクリア

### マウスポインタの位置を取得

マウスの動き自体は[pointermove](https://developer.mozilla.org/ja/docs/Web/API/Element/pointermove_event)イベントで取得してます。

Reactで元のブラウザイベントはnativeEventで取得できるので、canvas内でのポインターの位置は以下のコードで取得できます。

```typescript
<canvas
    onPointerMove={(e) => {
        // マウスポインタの位置取得
        const x = e.nativeEvent.offsetX;
        const y = e.nativeEvent.offsetY;
    }
/>
```

:::note warn
今回のゲームでは実装してないのですが、pointermoveイベントは頻繁に発生するイベントになります。
複雑な処理を追加する場合、処理頻度を制御（スロットリング）するなどパフォーマンス対策が必要です。
:::


### 取得した位置にあるcanvas要素の色を取得

ポインターの位置が分かればcanvasでその箇所のイメージデータを[getImageData](https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/getImageData)メソッドで取得できます。

1ピクセル分取り出したいので以下のコードとなります。

```typescript
const [r, g, b] = ctx.getImageData(ポインタのx座標, ポインタのy座標, 1, 1).data;
```

### 取得した色に応じて処理を追加

上記で取得した色情報に応じて、処理を追加しています。

ゲームオーバーの処理は以下のように実装します。

```typescript
if (r === 0 && g === 0 && b === 0) {
  gameOver();
}
```

また、ゴールの処理は以下のように実装します。

```typescript
if (r === 255 && g === 0 && b === 0) {
  goNextLevel();
}
```

### 最後の迷路だけびっくり要素（見えないゴール）を追加

最後の迷路だけ他とは違い見えないゴールを設置する必要があります。
ちょっとここの実装は無理矢理にはなりますがcanvasの上部に見えないDOM(cssでz-indexを調整)を置くことで実現してます。

```typescript
<>
  // 見えないDOM
  <div
    className="gameOver"
    onMouseEnter={() => {
      viewEnding();
    }}
  ></div>
  <canvas
    onPointerMove={(e) => {
      const ctx = canvasRef.current?.getContext('2d');
      if (!ctx) return;
      const x = e.nativeEvent.offsetX;
      const y = e.nativeEvent.offsetY;
      const [r, g, b] = ctx.getImageData(x, y, 1, 1).data;

      if (r === 0 && g === 0 && b === 0) {
        gameOver();
      }
    }}
    ref={canvasRef}
    width={width}
    height={height}
  />
</>
```


## エンディング画面

エンディング画面は[howler.js](https://howlerjs.com/)で音声が鳴るように実装してます。

```typescript
// セットアップ
const sound = new Howl({
src: ['./sound.wav'],
});

// 音声の再生
sound.play();

// 音量の変更
Howler.volume(1);
```


# 終わりに

今回はReactとcanvasを使って昔懐かしのFlashゲームを再現しました。

技術的にはシンプルですが、Reactの基本を学ぶアプリとしては良いものになったのかと思います。

最後までお読みいただき、ありがとうございました！


# おまけ：作成したゲームの元ネタ

以下の動画が元ネタとなっています。（びっくり要素が含まれているのでご注意ください）

https://youtu.be/uBIIVz_xp8I?si=dGirpjBgIc4wRZyl

~~また、上記のFlashゲームを体験した男性の動画もあります。~~

https://www.nicovideo.jp/watch/sm14261501


:::note info
株式会社BTMではエンジニアの採用をしております。
ご興味がある方はぜひ[コチラ](https://www.b-tm.co.jp/recruit/mid-career.html)をご覧ください
:::