Skip to content

第4章 UX、Canvas、スマートフォン ​

Web技術で2Dボードゲームを作る ​

自分はフロントエンドに詳しいわけではなく、この領域の実装はかなりAIエージェントへ任せた。そもそもWebで作ろうとしたのは、自分の技術スタックと相性がよく、使ったことのないゲームエンジンを新しく導入しなくても、2DボードゲームであればDOMとCanvasで十分に実現できるだろうと考えたためである。

ロビー、情報パネル、ボタンなどは通常のDOMで作れる。ただ、六角形の土地が連なる盤面と、その上に置かれる道や建物については、一つの座標空間へ自由に描くイメージがあった。そのためCanvas APIを使うことを先に決め、Reactと組み合わせるならどのライブラリがよいかをAIに調べてもらった。その中で有力だったReact Konvaを採用した、というのが当時の経緯である。

DOM、Canvas、Reactを理解し直す ​

この記事を書き始めた時点では、DOMを「HTMLのタグで情報を構造化する仕組み」、Canvasを「複雑な図形をピクセル単位で自由に描く仕組み」、Reactを「仮想DOMとの差分だけを反映するため再レンダーが軽い仕組み」と捉えていた。大まかな方向は合っていたと思う。ただ、実際のコードを理解するには、それぞれをもう少し分けて考える必要があった。

HTMLはタグを使って文書の構造を記述する。DOMは、HTMLなどをブラウザがノードの木として表現し、JavaScriptから参照や変更をできるようにしたオブジェクトモデルである。ボタンや文章は意味を持った要素として残るため、ブラウザのレイアウト、フォーカス、キーボード操作、アクセシビリティの仕組みを利用できる。

canvasもDOM上では一つのHTML要素である。ただし、その中に描いた六角形、道路、文字、画像は個別のDOM要素にはならない。JavaScriptから描画コンテキストへ命令を送り、一枚のビットマップを更新する。MDNのCanvas APIの説明にもあるように、Canvas内の図形はセマンティックHTMLのように支援技術へ公開されない。重要な情報や確定操作をDOM側に残す意味はここにある。

最初は、表現が複雑ならCanvasを使う、という程度に考えていた。実際には、単純か複雑かだけで決まる話ではない。文章、フォーム、情報パネルのように意味とレイアウトが重要なものはDOMに向いている。多数の図形を同じ座標系で扱い、拡大、縮小、パン、重なりを管理したい場合にはCanvasが候補になる。個々の図形をDOM要素として残したいなら、SVGという選択肢もある。

Reactについても、「仮想DOMが差分だけを更新するから速い」とだけ理解していたが、それでは少し粗かった。stateが更新されると、まずコンポーネントが再レンダーされ、次に表示したい構造が計算される。その後のcommitで、必要な変更だけが実際のDOMへ反映される。React公式が説明するrenderとcommitは別の段階であり、再レンダーが起きてもDOM全体を書き換えるわけではない。ただし、コンポーネント関数を再評価する処理自体は発生する。「Reactだから常に安い」わけでもなければ、「再レンダーはすべて止めるべき」という話でもない。

では、React Konvaはこの間で何をしているのか。ReactのpropsやstateからKonvaの図形ノードを更新し、Konvaが必要なCanvasレイヤーを再描画する。Reactコンポーネントの再レンダー、Konvaノードの更新、Canvasレイヤーの再描画、ブラウザによる表示は、それぞれ別の段階である。Canvas自体が状態を管理したり、変更を自動で検出したりするわけではない。

自分は当初、Konvaを「Canvasの中に作られる世界」と捉えていた。より正確には、KonvaはJavaScriptのメモリ上にStage、Layer、Group、Line、CircleなどのScene Graphを持ち、Canvasを描画先として使う。図形の親子関係、位置、拡大率、重なり順をノードとして管理し、表示用とは別のCanvasを使ったヒット判定によって、各図形のクリックやタッチを処理する。描画後のCanvasに残るのはあくまでピクセルであり、土地や道路がCanvas内のオブジェクトとして残るわけではない。

Konvaを使えば、盤面固有の座標計算まで自動で解決されるのかとも考えたが、そうではなかった。GeoGainerでは、ゲーム状態が持つ六角形のキューブ座標を画面上のx、yへ変換し、交点や道の両端も独自に計算している。Konvaが担当するのは、計算済みの位置へ図形を置き、グループとして移動や拡大を行い、重なりと入力を管理するところである。

生のCanvas APIだけでも同じ盤面は実装できる。その場合は、図形の一覧、描画順、カメラ座標をアプリケーション側で持ち、変更があるたびにCanvasを消して順番に描き直す。ブラウザから届くクリックイベントの対象は、個々の土地ではなくcanvas要素一つである。クリックされた画面座標を盤面座標へ戻し、どの六角形、交点、道に当たったのかを幾何計算しなければならない。Path2DとCanvasの判定APIを使う場合でも、パスとゲーム上の対象との対応は自分で持つ必要がある。

複数のCanvasを重ねればレイヤーも再現できる。ただ、その構成、イベントの振り分け、再描画のタイミング、アニメーションまで自分で管理することになる。React Konvaは、Canvasでは不可能だったことを可能にしたというより、Canvas上で繰り返し必要になるオブジェクト管理を引き受けてくれるライブラリだった、という理解に変わった。

SVGを選択肢に入れ直す ​

そもそも、盤面はCanvasでなければならなかったのか。今回の記事を書く中で、そこから考え直した。

結論としては、GeoGainerくらいの規模であれば、CanvasでもSVGでもよかったのだと思う。当時の自分には、ゲーム盤はCanvasの中に描くものだというイメージがあった。そのためSVGは比較対象に入っておらず、React KonvaもSVGより優れていると判断して選んだわけではない。Canvasを使うという前提でAIにライブラリを調べてもらい、その中から安定していそうなものを選んだ、というのが実際のところである。

では、SVGを使うと何が変わるのか。自分にとって大きかったのは、土地や道、交点をDOM要素として残せることだった。言うなれば、React KonvaがJavaScript上に盤面のオブジェクトを持つのに対して、SVGではブラウザから見えるDOMの中に盤面の構造が残る。

各要素にゲーム上のID、役割、名前を持たせれば、PlaywrightやAIエージェントから、画面上の座標ではなく「この交点」「この道」と対象を指定できる。PlaywrightのLocatorはroleやアクセシブルな名前から要素を探せる。ARIA snapshotを使えば、画面の意味構造を読み取ることもできる。Konvaの図形はJavaScript上のノードなので、DOMやアクセシビリティツリーには個別の要素として現れない。

ただし、六角形を一つのpolygon要素で描けば、その6頂点が自動で別々のDOM要素になるわけではない。交点を選択できるようにするなら、透明なcircleなどの十分な大きさのヒット領域を交点ごとに置き、安定した交点IDと操作上の意味を与える必要がある。道についても、表示用の線とは別に、指で押しやすい太い透明な入力領域を置ける。SVGを使うだけではなく、ゲーム上の意味と図形要素を対応させるところまで設計して、初めて自動テストのしやすさにつながる。

AIエージェントを使った動作確認まで考えると、この違いはかなり効いてくる。Canvasでも、座標を指定したクリックやスクリーンショット比較はできる。テスト用にScene Graphへ接続したり、Canvasと同期するDOMの入力層を用意したりする方法もある。ただ、座標による操作は画面サイズ、拡大率、端末のピクセル比の影響を受けやすい。UIの統合テストのしやすさという点では、SVGに軍配が上がるのではないかと思う。

もちろん、ゲームルール、状態遷移、座標計算の正しさは、ブラウザの表現と切り離してテストできる。SVGの利点が特に出るのは、候補が画面に表示され、その対象を操作でき、結果が反映されるというUI全体の経路である。

とはいえ、アニメーションや描画する要素が大幅に増えれば、Canvasの方が有利になる可能性はある。SVGでも親のg要素を変形すれば、複数の図形をまとめて動かせる。このあたりは、実際の盤面を両方で作って計測したわけではない。少なくとも現在のGeoGainerについて、性能面でもCanvasが正解だったと言い切れるほどの材料はないと思っている。

GeoGainerはすでにReact Konvaで問題なく動いているので、今からSVGへ作り直す話ではない。ただ、UIの操作まで含めて自動検証できることにはかなり価値を感じた。次に同じくらいの規模の2Dゲームを作るなら、SVGで盤面を作り、Playwrightで一連の操作を検証する構成を試してみたいと思う。

ドラッグ中の性能改善を理解する ​

盤面をドラッグするたびに座標が変わるので、その座標を毎回React stateへ戻す必要があるのか。当初は、このあたりをReactとKonvaの双方向データバインディングのように捉えていた。

実際の実装は少し違う。通常時はReact stateから表示へ流れる宣言的な経路を保ち、操作頻度が高いドラッグ中だけ、Konvaノードとrefに一時的な座標を持たせる。指を動かしている間はKonvaの盤面グループを直接移動し、レイヤーの描画をまとめて行う。指を離したときにだけ最終座標をReact stateへ戻す。

つまり、常に双方向で同期しているわけではない。高頻度な操作中だけReactの通常経路から一時的に外れ、操作が終わったところで戻ってくる、という理解の方が近い。ドラッグ中に別のReact更新が起きても盤面が古い位置へ戻らないよう、表示時には一時座標を優先する補正も入っている。

この性能改善で重要だったのは、再レンダーを無条件に減らすことではなく、頻度の高い処理だけを分けたことだと思う。通常時の分かりやすい状態管理を保ちながら、ドラッグ中の滑らかさも確保する。そのための実装だった。

実装を任せ、操作体験を評価する ​

ここまで仕組みを掘り下げてきたが、盤面のパン、ズーム、ドラッグ、対象選択といった操作の実装に、自分が直接携わったわけではない。当時はかなりAI任せで、実際に操作して問題がなければよい、という判定をしていた。

ただ、任せたままで何も判断していなかったわけではない。自分が決めたのは、どのような体験を守りたいかという部分である。盤面を違和感なく動かせること、対象を迷わず選べること、友人がストレスなく遊べることを確認し、問題があればAIに改善を依頼した。実装方法を指定するのではなく、操作した結果を評価する役割だった。

スマートフォン対応も、具体的な実装はAIへ任せた。PC版をそのまま縮小すれば、盤面も操作領域も小さくなり、使いづらくなることは目に見えていた。そこでスマートフォン向けの画面を別に考え、ゲームの操作性とも相性がよかった横向きを正式な対応方向にした。盤面と現在の操作を優先し、それ以外の情報は必要なときに開く構成である。

モバイルの性能改善では、AIが画像の転送量、Reactの定期更新、ドラッグ中の描画経路を調査した。自分は、スマートフォンだけ画像を軽くすること、PC版の画質は維持すること、実機で感じたドラッグの引っかかりを改善することを判断した。具体的な最適化と検証はAIが担当している。

この章は、自分がフロントエンドの実装を手書きしたことを示すものではない。専門外の領域であっても、目的と制約を決め、AIへ調査と実装を任せ、最後は自分で操作して判断する。その役割分担でプロダクトを作った記録である。

人間の判断とAIエージェントの実装による開発記録