🎲

React 19 RC でテトリスを作り、ライブラリとして公開しました

2024/06/10に公開

はじめに

この記事では、先日公開したテトリス風のパズルゲーム「TsumiZare」と、React 19 で新しくなった ref を活用した例などを紹介します。

ブラウザゲーム「TsumiZare」

TsumiZare は、テトリス風のパズルゲームです。ブラウザゲームであり、最低限ですがPWAにも対応しているためアプリとしてもインストールできます。この手のパズルゲームが難しい子どもでも楽しめるモードを用意しています。

ゲームプレイは以下のURLにアクセスするだけで可能です。ユーザー登録すら不要です。

https://tsumizare.app/

TsumiZare のプレイ画面

開発の背景などについては note へ投稿しましたので、そちらもご覧いただけると嬉しいです

https://note.com/yoshikouki/n/n82cacdbfe95e

このリポジトリで公開しています

https://github.com/yoshikouki/TsumiZare

技術スタック

興味あるライブラリなどを天こ盛りにしたような構成です。

利用した React 19 RC の新機能

TsumiZare では、以下の新機能と改善を利用しています。

今回は私の観測できる範囲ではあまり関心が集まっていない ref 周りの変更について、その実例と併せて紹介します

ref の実装例

テトリスのアクティブなブロック (テトリミノ) の自然落下を実現するために、ゲームのチック (ゲームの状態を更新するための定期的な処理。もしくはその時間単位) を実装しました。

React 18 までの実現方法の一つは useEffect の利用です。

export const useBlockyGame = (option?: {
  upAction?: UpAction;
}) => {
  const { board, setBoard, hasCollision } = useContext(BlockyGameContext);
  .
  .
  .
  // Game loop
  useEffect(() => {
    if (board.status !== "playing") return;
    const interval = setInterval(runTick, board.config.dropInterval);
    return () => clearInterval(interval);
  }, [board.status, runTick, board.config.dropInterval]);
}

これでも設計によっては問題にならない場合もありますが、TsumiZare では board<Context> で管理しており、この useEffect が定義されるカスタムフック useBlockyGame はコンテキスト内でどこでも呼び出せるようにしました。

そうなると、useBlockyGame を呼び出したそれぞれの場所で useEffect が走ることになり、board はコンテキスト内で共通しているため、チック処理が重複実行されてしまいます。

この状態を回避するために、「チック処理を実行する責務」を hook からコンポーネントへ委譲するように設計しました。

useEffect を ref Callback へ置き換える

コンポーネントの ref Callback でチック処理とクリーナップを定義しておき、

export const useBlockyGame = (option?: {
  upAction?: UpAction;
}) => {
  const { board, setBoard, hasCollision } = useContext(BlockyGameContext);
  .
  .
  .
  // Game loop
  const tickRunnerRef = (_ref: HTMLDivElement) => {
    if (board.status !== "playing") return;
    const interval = setInterval(runTick, board.config.dropInterval);
    return () => clearInterval(interval);
  };
  
  return { tickRunnerRef };
}

コンポーネントに ref を渡すだけです。

export const Game = () => {
  const {
    board,
    tickRunnerRef,
    .
    .
  } = useBlockyGame();

  return <TickRunner ref={tickRunnerRef} /
export const TickRunner = ({ ref }: { ref: Ref<HTMLDivElement> }) => {
  return <div ref={ref} />;
};

このようにすることで、ref を持つコンポーネントがレンダリングされない限りチックは走らなくなります。また、useEffect が定義されたコンポーネントのレンダリングに依存する状態から、特定のコンポーネントの存在に依存するようになり、処理をコントロールしやすくなりました。

ref Callback のクリーナップ追加により、使い心地はそのままに useEffect に頼らくて済む場面もでてきそうです。
(とはいえ useEffect を減らすために無関係な ref をばらまくのは別の意味で複雑になりそうなため、私はあくまで表現方法が増えたくらいに考えています)

リザルト共有機能の紹介

プレイ後にリザルト画面を表示し、その結果をSNSへ投稿したり画像として保存できる機能を提供しています。

リザルト画面のスクリーンショット

以下のように画像として共有できます

https://x.com/yoshikouki_/status/1797999766607212737

この機能は、@vercel/og 内部で使用されている next/satori を使用しています。

Satori: Enlightened library to convert HTML and CSS to SVG.

リザルト画面に表示しているのは React コンポーネントで、ボタン押下時に画像へと変換しています。手順は次の通りです。

  1. 共有ボタン押下
  2. next/satori でリザルトコンポーネントをSVGへ変換
  3. SVG を canvas へ変換
  4. canvas を PNG Blob に変換
  5. 後処理
    • 共有: navigator.share を呼び出し
    • 保存: a タグに埋め込んでクリック処理

https://github.com/yoshikouki/TsumiZare/blob/04691cb234d5c9fdc7fbb7034829e62addba4683/src/futures/tsumizare/result-viewer.tsx#L9-L75

現時点では、実装を簡単にするためにクライアントで実行しています。しかし、next/satori の変換時にフォントを読み込む必要があり、アプリで使用している Web フォント (Inter-Black 317 KB) を改めてリクエストしています。また、next/satori が原因かまでは調べきれていませんが、端末やブラウザによって挙動に差があることを確認しています。

そのため、画像描画は素直にサーバー側で実装するべきだったと反省しており、今後改修する予定です。また、SNSへの共有では、リザルト画像をOGPイメージとして表示する方針も考えています。

blocky-game について

ここまで紹介してきたブロックゲームのコア機能である React カスタムフックを npm で公開しています。なんと React 19 RC にのみ対応しています。

https://github.com/yoshikouki/TsumiZare/tree/main/packages/blocky-game

セットアップは簡単です。

まず React 19 RC をセットアップします。

https://react.dev/blog/2024/04/25/react-19-upgrade-guide

その後、依存関係をインストールして、

npm install blocky-game
# or
bun add blocky-game

コンテキストを定義します。

import { BlockyGameProvider } from "blocky-game";
import { BlockyGame } from "@/components/BlockyGame";

export default function HomePage() {
  return (
    <BlockyGameProvider>
      <BlockyGame />
    </BlockyGameProvider>
  );
}

アプリケーションのコードも同リポジトリで公開しているため、触られる際にはコンポーネントなどの参考にしてください。

https://github.com/yoshikouki/TsumiZare

終わりに

TsumiZare はまだ公開したてのアプリです。今後もより良いゲームを目指して改良を続けていきたいと考えています。

SNSやリポジトリを通してフィードバックいただけたら嬉しいです。ご意見、ご感想をお待ちしております。

Discussion