mottox2 blog

Reactで寿司を回すタイピングゲームを作った

dev
thumbnail

@dala00さんが企画している、1週間でテーマに沿ったWebサービスを作る #web1weekという企画に参加しました。今回は第二回の開催で「Like」というお題でした。

esaをより便利に使うための拡張機能「Refined esa」をつくった に引き続き、Webエンジニアの多くが好きな「寿司」をテーマにしたものを作ろうと思いました。
寿司といいえばタイピングです。また、先日ちょうど寿司を回すデモを作っていたので、タイピングで寿司を回すミニゲームにしました。ゲームを公開するのは人生で始めてです!

ゲームはこちら
ソースはこちら

遊び方

Enterを押すとゲームが開始します。画面下部に文字が表示されるので、20秒の制限時間内にできるだけ入力してください。
入力するとどんどん寿司の回転が早くなるので、できるだけ回してください。制限時間を使い切ると、回した回数が表示されます。できるだけ多く回すように頑張ってください。

技術的な話

寿司の3D空間はreact-three-fiberを使ったWebGL、それ以外は普通のReactアプリの構成で作っています。

react-three-fiberはthreejsをReactで扱うためのライブラリです。threejsはJavaScriptで3D表現を扱うためのライブラリでしたが、命令的な処理を記述する必要があります。これを宣言的な記述で扱えるのがreact-three-fiberの特長です。

例えば、今回のアプリケーションではBackgroundというコンポーネントを作っており、そのコンポーネント内で寿司を回すロジックを書いています。また、Reactで書いているので、他のコンポーネントで生成した値を3D表現にわたすのも非常に簡単です。

次のコードは作成したゲームのコンポーネントです。通常のReactコンポーネントと同じような扱いをしています。

jsx
const App = () => {
  const mounted = useMounted()
  const { mode, setMode } = useMode('title')
  const [ stats, setStats ] = useState<Stats>(initialStats)
  const [ rotation, setRotation ] = useState(0)
  const onTimerEnd = useCallback(() => setMode('result'), [])
  const restart = useCallback(() => setMode('title'), [])

  return <div className={styles.container}>
    {mounted && <Background count={stats.score} mode={mode} setRotation={setRotation} />}
    { mode === 'title' && <Title start={() => setMode('game')}/>}
    { mode === 'result' && <Result score={stats.score} miss={stats.miss} rotation={rotation} restart={restart} />}
    { mode === 'game' &&
      <Game onEnd={onTimerEnd} onReset={() => setMode('title')} setStats={setStats} />
    }
  </div>
}
background.js
import React, { useRef, useState, useMemo, useEffect, Suspense } from 'react';
import { Canvas, useFrame, useThree, useResource } from 'react-three-fiber'
import { Vector3, PerspectiveCamera } from 'three';

const colors = ['#E43327', '#E6E02A']
export function Background({count, mode, setRotation}: any) {
  const color = colors[count % 2]
  return (
    <div className='container'>
      <Canvas shadowMap={true}>
        <ambientLight />
        <pointLight castShadow position={[0, 10, 20]} />
        <gridHelper args={[300, 100, 0x888888, 0x888888]} position={[0, -0.65, 0]}/>
        <mesh receiveShadow position={[0, -0.7, 0]}>
          <boxBufferGeometry attach="geometry" args={[200, 0.1, 200]} />
          <meshStandardMaterial attach="material" color={'#a0a0a0'} roughness={0.0} />
        </mesh>
        <Sushi setRotation={setRotation} mode={mode} position={[0, 0, 0]} speed={0.03 * count} scale={[1 + 0.18 * count,1,1]} color={color} />
        <Camera />
      </Canvas>
    </div>
  );
}

let rotation = 0

const Sushi = ({speed, color, mode, setRotation, ...props}) => {
  const group = useRef<any>()

  useEffect(() => {
    if (mode === 'result') setRotation(rotation / 3.14 / 2)
    rotation = 0
  }, [mode])

  useFrame(() => {
    group.current.rotation.y += speed || 0.01
    if (mode === 'game') rotation += speed || 0.01
  })

  return <group {...props} ref={group}>
      <Box position={[0, 0.7, 0]} geometry={[2, 0.4, 1]} castShadow color={color} />
      <Box position={[0, 0, 0]} geometry={[2, 1, 1]} castShadow color="#CCCCCC" />
    </group>
}

const Box = (props: any) => {
  return (
    <mesh {...props}>
      <boxBufferGeometry attach="geometry" args={props.geometry || [1, 1, 1]} />
      <meshStandardMaterial attach="material" color={props.color || '#666600'} />
    </mesh>
  )
}

const Camera = (props: any) => {
  const ref = useRef<PerspectiveCamera>()
  const { setDefaultCamera } = useThree()
  useEffect(() => {
    if (!ref.current) return
    setDefaultCamera(ref.current)
    ref.current.lookAt(new Vector3(0, 0, 0))
  }, [ref, setDefaultCamera])

  return <perspectiveCamera position={[3, 3,3 ]} args={['90', window.innerWidth / window.innerHeight]} ref={ref} {...props} />
}

export default Background;

その他気にしたこと

デザイン

本当はリッチな背景を用意したり、寿司を流したかったのですが、3D表現を始めて1週間なので難しく諦めました。むしろ寿司ネタ以外をモノクロにすることでそれっぽくなるんじゃないかと思い余計な装飾が取り払いました。結果として、寿司が際立つインパクトのあるビジュアルになったと思います。

回して伸ばす

寿司を1つだけ3D空間に表示して回してみたところインパクトが足りませんでした。そこで(意味がわからないですが)回すついでに伸ばしてみたところ謎のシュールさが生まれたのでこのアイディアを採用しました。
ただ、最初のプレイでシュールさを感じてもらえるようにするのが難しく、ゲームバランスを考えるゲーム制作者の苦労を感じました。

やらなかったこと

途中でめんどくさくなって放棄した要素が結構多い

  • ミスのエフェクト
  • 表示される言葉のバリエーション
  • レスポンシブ対応
  • ちゃんとしたゲームインターフェース
  • 連続正解ボーナス
  • シェア文言を複数パターン用意
    • 特定の点数を超えると文言が変化するやつ

感想

ゲームを作るのは始めてでしたが、気になる箇所を潰す間に飽きてしまいました。めちゃくちゃ高いクオリティでゲームを公開している開発者に敬意を持ちました。
3D表現という意味でもチャレンジングなことができて満足しています。こちらはもう少し練度をあげたいと思ったので、やっていきを継続します。

だらさん、楽しい企画をありがとうございました!

dev

ウェブ技術で縦書きを含む画像を生成したい

ここ最近、Web技術を利用した画像生成に興味があります。本記事では、日本語における表現の一種である縦書きに焦点を当て、Web技術を使った縦書きを含む画像生成方法についての調査をまとめました。 > 現

blog

Netlify Formsで問い合わせフォームを作ったら簡単だった

追記(2022/12/29): 問い合わせに対応する窓口をTwitterに統一したいので、フォームページは削除しました。 当ブログは静的サイトホスティングサービスのNetlifyでホスティングされ

netlify
dev

翻訳でHacktoberfestに参加しました

毎年10月に開催されるHacktoberfestに参加しました。このイベントはOSSへの貢献を行い、期間中に規定数(4つ)の貢献を行った人に特典がプレゼントされるものになっています。 自分はドキュメ

Copyright © 2023 @mottox2 All Rights Reserved.