Reactではじめるキノコ狩り

こんにちは、ちいかわです。
こちらは [Catallaxy Advent Calendar 2021](https://qiita.com/advent-calendar/2021/catallaxy) の第9日目の記事となります。8日目はエンジニア2号さんによる[お料理コラム](https://qiita.com/catallaxy/items/1dfb5313fc57a6dbecc1)でした。
本日もよろしくお願いいたします。

以前 Rails ゴリゴリマンだった私は、Catallaxy に入社後初めて React に触れ、それからフロントエンドの開発はもっぱら React ゴリゴリラです。もともとプログラマになりたいと思ったきっかけが子どもの頃に出会った HTML / CSS で、知恵と工夫次第でWeb上でなんでも表現できる────とえらく感動したものでした。 React は自分にとって言わばその拡張パックのようなもので、無限だと思っていた表現の幅がさらに拡がり、日々の開発で情緒がどうにかなってしまっています。

本記事ではクリスマスも近いことですし、 React を使ってキノコ狩りをしたいと思います。

## 採取スペースを用意する

キノコ狩りにはなんといっても専用の採取場所が必要です。
Component 力をふんだんに使い、サクッと用意してみます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2345141/96fdfc23-8924-d82d-a525-54837a530475.png" width="500" alt="採取スペース" />
[CodeSandbox で見る](https://codesandbox.io/embed/kinoko-01-0qrc4?fontsize=14&hidenavigation=1&theme=dark&view=preview)
キノコを採るためとしか思えない舞台ができました！


## 動きたい
自由意志で動けないことにはキノコ狩りは始まりません。左右移動と、ついでにジャンプもしたいですね。
安定した動きにするため、 `requestAnimationFrame` を使った hooks を作り、1フレームごとの計算をしていきます。 Rx を使えば美しくイカしたコードが書けるのでしょうが、慣れていないので力技でいきます。
力技も楽しいものです。
※ 記事中のソースコードは部分的なものです。ステップごとに CodeSandbox のリンクを貼っていますので、実際に動くコードはそちらを参照ください。

```ts
/**
 1フレームごとに frameCount をインクリメントして返す hooks
 */
export const useAnimationFrameCount = () => {
  const [frameCount, setFrameCount] = useState(0);

  useEffect(() => {
    const loop = () => {
      setFrameCount((prevFrameCount) => prevFrameCount + 1);
      requestAnimationFrame(loop);
    };
    const request = requestAnimationFrame(loop);

    return () => cancelAnimationFrame(request);
  }, []);

  return frameCount;
};
```
```ts
/**
 フレームカウントごとにキャラクターの位置を計算する hooks
*/
export const useMoveUnit = () => {
  const frameCount = useAnimationFrameCount();

  const pressLeft = useKeyPress("ArrowLeft");
  const pressRight = useKeyPress("ArrowRight");
  const pressTop = useKeyPress("ArrowUp");

  const [position, setPosition] = useState({ left: 0, bottom: 0 });

  const move = () => {
    // 左右移動の処理
    if (pressLeft) {
      // 「←」キーを押したとき
    }
    if (pressRight) {
      // 「→」キーを押したとき
    }
  };

  const jump = (frame: number) => {
    // 上下移動の処理
    if (pressTop) {
      // 「↑」キーを押したとき
    }
  };

  useEffect(() => {
    // フレームカウントごとに左右・上下移動の処理を実行する
    move();
    jump();
  }, [frameCount]);

  return { position };
};
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2345141/1438b3d0-41c2-1479-3189-9ef0d4564040.gif" width="500" alt="動いたぞ" />
[CodeSandbox で遊ぶ](https://codesandbox.io/embed/kinoko-02-7cf3d?fontsize=14&hidenavigation=1&theme=dark&view=preview)
左右移動とジャンプができるようになりました。十字キーで操作できます。遊んでみてください。
CodeSandbox が Qiita に埋め込めないことを今知り、泣いています。

上下の移動は重力計算のため、なつかしい物理の公式を使っています。
地味だけど大切なポイントは、オブジェクト位置の指定を `{ left, bottom }` にしたことです。始点が `(0, 0)` である方が圧倒的にわかりやすいです。


## 当たり判定
キノコを採るにはなんとなく、宙に浮いた「？」のブロックに衝突したい気持ちになります。というわけでキャラクターとブロックとの当たり判定を作ります。
物体と物体が衝突したかどうか（重なっているかどうか）を判定する関数を泥くさく書いていきます。今回は四角と四角の衝突のため単純な計算ですみますが、形状が複雑になるなら別の方法をとったほうがいいと思います。

```ts
/**
 unit と blocks[] が衝突したら、衝突した block を返す関数。
 やっていることは単純
 */
const collision = (
  unit: { position: Position; size: Size }, // キャラクター
  blocks: { position: Position; size: Size }[] // ブロックの配列
) => {
  return blocks.find((block) => {
    const horizontal =
      block.position.left < unit.position.left + unit.size.width &&
      unit.position.left < block.position.left + block.size.width;
    const vertical =
      block.position.bottom < unit.position.bottom + unit.size.height &&
      unit.position.bottom < block.position.bottom + block.size.height;

    return horizontal && vertical;
  });
};
```
この当たり判定の処理を、さきほどのキャラクターの位置計算をする `useMoveUnit` の中でいい感じに活用します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2345141/a570499d-83ad-96fa-8e93-d51947fe6e58.gif" width="500" alt="ぶつかるぞ" />
[CodeSandbox でぶつかる](https://codesandbox.io/embed/kinoko-03-p5bm6?fontsize=14&hidenavigation=1&theme=dark&view=preview)
ブロックにぶつかることができました。壁も突き抜けないように処理しました。
キノコが近づいています。


## キノコ
あとはキノコに出てきてもらうだけです。ついでに勝手に動いてもらいます。
`useMoveUnit` を再利用し、フレームカウントごとに自動で移動させるキノコ専用の hooks を作ります。
キノコ自身の物体への当たり判定は、ブロックの上部だけなので楽チンです。

```ts
/**
 キノコ専用 hooks
 */
export const useMoveKinoko = (params: {
  initialPosition: Position;
  size: Size;
  blocks: { position: Position; size: Size }[];
}) => {
  const frameCount = useAnimationFrameCount();

  const [position, setPosition] = useState(params.initialPosition);

  const [horizontalSpeed] = useState(2);
  const [startFallFrame, setStartFallFrame] = useState(0);
  const [gravity] = useState(9.8);

  const move = () => {
    // 右に動き続ける
    setPosition((pos) => ({
      left: pos.left + horizontalSpeed,
      bottom: pos.bottom
    }));
  };

  const fall = (frame: number) => {
    if (position.bottom === 0) return;

    if (startFallFrame === 0) {
      // キノコは常に落ちようとする
      setStartFallFrame(frame);
    } else {
      const t = (frame - startFallFrame) / 10;
      const speed = -gravity * t;

      // 当たり判定
      const block = collision(
        {
          position: { ...position, bottom: position.bottom + speed },
          size: params.size
        },
        params.blocks
      );
      if (block) {
        setStartFallFrame(0);
      } else {
        setPosition((pos) => ({
          left: pos.left,
          bottom: Math.max(pos.bottom + speed, 0)
        }));
      }
    }
  };

  useEffect(() => {
    move();
    fall(frameCount);
  }, [frameCount]);

  return { position };
};
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2345141/f659c25e-67f2-2031-4093-008f91d8c429.gif" width="500" alt="キノコが出たぞ" />
キノコが出現し、落ちてきました！
さらにさきほど作った当たり判定をキャラクター vs キノコ用に作り直し、キノコめがけてぶつかっていけばフィニッシュです :tada: 
[CodeSandbox でキノコ狩りしてみてね](https://codesandbox.io/embed/kinoko-04-rujw4?fontsize=14&hidenavigation=1&theme=dark&view=preview)


## おわりに
JavaScript（TypeScript）でゲームを開発するためのライブラリはたくさん存在します。よりゲームらしいゲームを手早く、高品質に作るためには、世に出ているライブラリをお借りした方が適している場合が多いです。物理演算とかめんどうだし。
ですが、たまにはひとつひとつの小さな処理から自分の手で書いていくことも大事な体験だと思います。
ちなみにReactゴリラはたまにと言わずなんでも自作するのが大好きです。

Catallaxyにはエンジニアに限らずさまざまなゴリラが所属していて、それぞれの分野で毎日活躍しています。読んでくださったあなたにも、もしかしたらぴったりハマるポジションが弊社にあるかもしれません。現在募集している職種は下記のページでご確認できます。よろしくお願いいたします。

[採用募集ページ / 株式会社Catallaxy](https://open.talentio.com/r/1/c/catallaxy/homes/2947)