🕹️

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

2023/08/15に公開

ゲームループの処理を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 [isRunning, setIsRunning] = useState(false)
  const reqIdRef = useRef<number>(0)
  const [startTime, setStartTime] = useState<number>(0)
  const [prevTime, setPrevTime] = useState<number>(Date.now())

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

  return { start, stop }
}

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

使い方

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

import { useState } from 'react'

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

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

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

参考

Discussion