Reactでマインスウィーパーを作ったらカオスが見えた
こちらで実際に作ったゲームが遊べます。
はじめに
Minesweeperは私が3番目に作成したゲームです。空白マスをクリックした時の処理を再現するのに何日も悩み、『空白マスを開く処理を空白マスを開く処理の中で呼び出す』という手法を思いつき、実装できた時の喜びは今でも鮮明に覚えています。後に再帰処理と呼ばれ、ファイルシステムの走査などごくごく普通に使われる手法と知ることになるのですが、、
何はともあれ、私がプログラミングにハマるきっかけになったことは間違いなく、pythonのtkinter、pygame等、新しい言語やパッケージや構文を試す時に、Minesweeperを作りながら使い方を覚える、というのが私の習慣になっています。
このサイトのNext.jsへの移行に合わせて、Reactのclassコンポーネントを全てReact Hookに書き換えています。なのでReactでMinesweeperを作成することは、私の中では極めて自然な行為なのであります。
前提
- React Hookを利用
- ゲーム部分はcanvas要素を利用
どう構築するか
canvasなら普通getContext("2d")で描写すると思います。
const cvs = document.createElement("canvas");
const ctx = cvs.getContext("2d");
ctx.fillRect(10,10,20,20);
Minesweeperのゲーム進行で考えると、、
- マスの状態を配列に保持(初期処理)
- クリック
- クリックの座標(x,y)に対応する配列を更新
- 配列の状態を下にcanvasを描写
とし、2~4を繰り返して実装することになります。
Reactでは、レンダリング部分はDOMでなく、なるべくReactに任せるのがグッド・プラクティス(?)とされているようです。しかし、Reactにはcanvasへのレンダリングを代わりにやってくれる機能はありません。canvasを使う以上、従来通りgetContext("2d")でレンダリングを行う必要がありそうです。
Reactでは定義したstateが変更された時に差分のレンダリングが行われます。上に当てはめると、マスの状態の配列をstateにして、クリックした時に配列のstateを更新し、ctxによるレンダリングはuseEffect内でやれば、実装できそうです。
しかしここで、1つひっかかることがあります。Reactのドキュメントを見ると、useStateの例のほとんどは、numberのようにprimitiveな型で 初期化されています。配列のような参照型は、更新処理が効かないように思われます。
実際に以下のような使い方だと、再レンダリングは行われませんでした。いくらboardの中身を更新したところで、boardの参照先は変わらないため、 Reactは"変更なし"と判断するのでしょう。
import {useState} from "react";
export function default Game(){
const [board,seBoard] = ([0,0,0,0,0,0]);
const click = (e)=>{
// 何かboardを更新する処理。board[y][x] = 9 とか
setBoard(board);
};
return <canvas onClick={click}></canvas>
}
まぁこれはさほど不思議ではありません。しかし、これだとクリック⇒boardのstate更新は出来ません。Reactっぽくはありませんが、canvasの再レンダリングは、再プレイやレベル変更等で、完全にゲームをやりなおす時に実施することにします。つまり、
const [board,setBoard] = useState([]);
のseBoardでboardの参照値が変わる場合にのみ、reactの再レンダリングが行われるということです。
結果
あまり多くは語りません。。。ロジック部分は別ファイルに分けてたのですが、やたらとboardとctxを引数に取る関数が出来上がってしまったのと、 canvasの大きさ等を保持するために、boardにやたらと多くのプロパティを追加する羽目になりました。githubにソースはあります。
解決策
全部作った後に知りましたが、useStateで配列を扱う場合、
const [board,setBoard] = useState([]);
~~
setBoard([...board])
のように新しい配列に入れてあげればReactも"更新あり"と判断してくれるようです。もう少し調べていれば、、、
ドキュメントにも、useStateにオブジェクトも使えるよ、とは記載がされていた気もしますが、複雑な構造のデータを1つ入れるより、細かくして複数のuseStateをしたほうが良い、って記載してあった(と思う)ので、しょっぱなからあきらめてしまっていました。。
※Reactっぽくするならcanvasを使わないのも手だったかも。Spriteシートをimgタグでどう分割するのか、あまりイメージできませんが。。。