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表現という意味でもチャレンジングなことができて満足しています。こちらはもう少し練度をあげたいと思ったので、やっていきを継続します。

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

B!
dev

jsx-presentationを使ってJSXでpptxファイルを生成する

週末にpptxファイルが生成できるスライド作成ツールのプロトタイプを作りました。 スライドエディタで作成したスライドをPowerPoint形式で出力できるようにした。 まだまだサイズやアスペクト

poem

欲しいものリスト(2020年版)

欲しいものはAmazonに売ってないので、独自にリストを作ることにしました。 - いい感じの動画を作るセンス 今年に入ってAfter Effectsを使ったアニメーションを作ったりしています。Tw

dev

esaをより便利に使うための拡張機能「Refined esa」をつくった

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

Copyright © 2019 @mottox2 All Rights Reserved.