🕹️

ReactでrequestAnimationFrameを使ってゲームループを実装してみる

に公開

ゲームループの処理をReact Hooks化する

ブラウザでゲーム開発やアニメーションの実装をする時にゲームループがあると何かと便利です。setTimeout や setInterval を使うと簡単に実装できるのですが、パフォーマンスを考慮すると requestAnimationFrame を使うのが良さそうです。React で requestAnimationFrame を使おうとしたら少し癖があったので requestAnimationFrame を React Hooks 化してみました。

実装

ページ遷移したときにゲームループの処理がきちんと止まるように useRef を使って requestId の管理をしています。あと、delay を指定すると setInterval を使う時のようにコールバック関数の呼び出しタイミングを制御できるようにしています。

import { useState, useRef, useEffect, useCallback } from 'react'

export const useAnimationFrame = (
  callback: (ts: number) => void,
  delay: number = 0
) => {
  const reqIdRef = useRef<number>(0)
  const startTime = useRef<number>(0);
  const prevTime = useRef<number>(Date.now());

  const loop = useCallback(
    (ts: number) => {
      let st = startTime.current;
      reqIdRef.current = requestAnimationFrame(loop);
      if (st === 0) {
        st = ts;
        startTime.current = st;
      }
      // ループタイミングの計算
      const now = Date.now();
      const delta = now - prevTime.current;
      if (delta >= delay) {
        // 経過時間の計算
        const elapsed = ts - st;
        // コールバック関数の呼び出し
        callback(elapsed);
        prevTime.current = now;
      }
    },
    [callback, delay, startTime, prevTime]
  )
  
  // ゲームループをスタートさせる
  useEffect(() => {
    reqIdRef.current = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(reqIdRef.current);
  }, [loop])
  
  // ゲームループをストップさせる
  const stop = useCallback(() => {
    if (!reqIdRef.current) return;
    cancelAnimationFrame(reqIdRef.current);
    reqIdRef.current = 0;
  }, [reqIdRef]);
  
  // ストップしたゲームループを再スタートする
  const restart = useCallback(() => {
    if (reqIdRef.current) return;
    reqIdRef.current = requestAnimationFrame(loop);
  }, [reqIdRef, loop]);

  return { stop, restart }
}

stop, restart 関数を使って Hooks の外からゲームループの開始と終了を制御できるようにしています。

使い方

useAnimationFrame を使ってタイマー表示をする例です。

import { useState } from 'react'

const GameElem = () => {
  const [time, setTime] = useState<number>(0)
  // ゲームループ
  const loop = (elapsed: number) => {
    setTime(elapsed)
  }
  // 第2引数に数値を指定することによりsetInterval的な使い方ができる
  const { stop, restart } = useAnimationFrame(loop, 80)
  stop()
  
  const onClick = (e: React.MouseEvent<HTMLElement>) => {
    e.preventDefault()
    // アニメーションをスタートする
    restart()
  }
  return (
    <div>{time}</div>
    <button onClick={onClick}>開始</button>
  )
}

useAnimationFrameを使って実装したゲーム

実際に useAnimationFrame を使って実装したゲームです。

useAnimationFrameを拡張してゲームエンジンを作ってみました

参考

Discussion