「Charisma The Chameleon」というタイトルのゲームを開発しています。このゲームは、Three.js、ReactJSおよびWebGLを使って開発しています。本記事ではこれらの技術がreact-three-renderer(R3R)を使ってどのように動作するかを説明します。
ReactおよびWebGLの紹介については、SitePointの『A Beginner’s Guide to WebGL』や『Getting Started with React and JSX』をチェックしてください。本記事および添付のコードはES6 Syntaxを使用しています。
はじめに
しばらく前にPete Huntが#reactjs IRC channelでReactを使ったゲーム開発に関するジョークを口にしていました。
「きっとReactでファーストパーソン・シューティングゲーム(日本版編注:本人視点のシューティングゲームのこと)が作れるよ!
敵には<Head />や<Body>や<Legs>があるんだ」
私も彼も笑いました。みんな楽しいひとときを過ごしました。「いったい誰がそんなことをするのだろう?」と私は首を傾げました。
数年後、なんと私がやっています。
Charisma The Chameleonは主人公を小さくするパワーアップアイテムを集めて無限のフラクタル迷路を解き進むゲームです。私は数年前からReactの開発者をやっていて、Reactを使ってThree.jsを動かす方法があるかどうか興味を持ちました。R3Rに関心を持ったのはそのときです。
なぜReactなのか?
みなさんが「どうして?」と思っていることは分かっていますが、少しだけおつき合いください。3Dシーンを動かすのにReactを検討する理由は以下のとおりです。
- 「宣言型の」ビューによってシーンの描画とゲームロジックを明確に分離する
- <Player />、<Wall />、<Level />など、識別しやすいコンポーネントの設計
- ゲームアセットの「ホット」(ライブ)リローディング(テクスチャとモデルを変更し、シーンでそれらがリアルタイムに更新されるのを見ると分かります)
- Chrome inspectorなどネイティブのブラウザーツールを使って、マークアップとして3Dシーンを検査およびデバッグ
- Webpackを使用して従属グラフでゲームアセットを管理。例:<Texture src={require('../assets/image.png')}/>
では、シーンをセットアップして、どのように動作するのか説明していきます。
ReactとWebGL
記事に添付するためにサンプルGithubリポジトリを作成しました。リポジトリをコピーしてREADMEの説明に従ってコードを実行し、理解を深めてください。3DロボットのSitePointyが主役です!
注記:R3Rはまだベータ版です。R3RのAPIは不安定で、将来変更される可能性があります。現在のところThree.jsのサブセットのみが扱えます。ゲーム全体をビルドするのに十分であることが分かっていますが、ケースによってはそうではない場合もあります。
ビューコードの構築
WebGLを動かすのにReactを使用する主なメリットは、ビューコードがゲームロジックから切り離されるということです。つまり、描画されたエンティティは識別が容易な小さなコンポーネントだということです。
R3RではThree.jsをラップする宣言的なAPIに着目します。たとえば、以下のように記述できます。
<scene>
<perspectiveCamera
position={ new THREE.Vector3( 1, 1, 1 )
/>
</scene>
すでに、カメラを使った空っぽの3Dシーンが用意されています。シーンにメッシュを加えるには、ただ単純にシーンに<mesh />コンポーネントを入れ、そこに<geometry />、<material />を追加してください。
<scene>
...
<mesh>
<boxGeometry
width={ 1 }
height={ 1 }
depth={ 1 }
/>
<meshBasicMaterial
color={ 0x00ff00 }
/>
</mesh>
これでTHREE.Sceneが内部で生成され、さらにTHREE.BoxGeometryを使用して自動的にメッシュが追加されます。そして、R3Rで古いシーンと変更箇所を比較します。シーンに新しいメッシュを追加した場合は、元のメッシュは再生成されません。vanilla ReactやDOMと同じく、3Dシーンは変更部分しか更新されません。
Reactを使って作業しているため、ゲームエンティティをコンポーネントファイルへ分離可能です。リポジトリのサンプル内にあるRobot.jsファイルで、純粋なReactビューコードを使用してメインキャラクターを生成する方法が明示されています。それは「Stateless functional」コンポーネントであり、ローカルの状態を保持しないことです。
const Robot = ({ position, rotation }) => <group
position={ position }
rotation={ rotation }
>
<mesh rotation={ localRotation }>
<geometryResource
resourceId="robotGeometry"
/>
<materialResource
resourceId="robotTexture"
/>
</mesh>
</group>;
そしてここで3Dシーンに<Robot />を組み込みます!
<scene>
...
<mesh>...</mesh>
<Robot
position={...}
rotation={...}
/>
</scene>
R3R Githubリポジトリで多くのAPIのサンプルを参照できます。また、the accompanying projectで完成したサンプルセットアップを閲覧できます。
ゲームロジックの構築
問題の後半はゲームロジックの取り扱いについてです。ロボットのSitePointyに単純なアニメーションを少し加えます。
ゲームループは元来どのように動作するのでしょうか? ユーザーの入力を受け取り、元の「世界の状態」を解析し、描画用に新しい「世界の状態」を返します。便宜上、「game state」オブジェクトをstateコンポーネント内に保存します。さらに発達したプロジェクトでは、game stateをReduxおよびFlux storeに移動可能です。
ブラウザーのrequestAnimationFrame APIコールバックを使ってゲームループを操作し、GameContainer.js内でループを実行します。またロボットをアニメーション化するには、requestAnimationFrameへ渡されたタイムスタンプに基づいて新しい位置を計算し、そして新しい位置をstateに保存します。
//...
gameLoop( time ) {
this.setState({
robotPosition: new THREE.Vector3(
Math.sin( time * 0.01 ), 0, 0
)
});
}
setState()を呼び出すと子コンポーネントの再描画が開始され、3Dシーンが更新されます。そして、コンテナーコンポーネントからプレゼンテーショナルコンポーネントの<Game />へstateを渡します。
render() {
const { robotPosition } = this.state;
return <Game
robotPosition={ robotPosition }
/>;
}
このコードの構築をサポートする適用可能で便利なパターンがあります。単純な時間ベースの計算によってロボットの位置を更新します。また、将来的に、前回のゲームステートより、前回のロボットの位置を斟酌する必要があるかもしれません。データをいくつか取得して処理し、新しいデータを返す関数はレデューサーと呼ばれることがありますが、動作コードをレデューサー関数にまとめられます。
結果として、関数呼び出しのみを内部に持つ明快でシンプルなゲームループが記述できます。
import robotMovementReducer from './game-reducers/robotMovementReducer.js';
//...
gameLoop() {
const oldState = this.state;
const newState = robotMovementReducer( oldState );
this.setState( newState );
}
ゲームループにさらにロジックを追加するには、以下の物理処理の例のようにもう1つレデューサー関数を生成して、前のレデューサーの結果を渡します。
const newState = physicsReducer( robotMovementReducer( oldState ) );
ゲームエンジンが進化するにつれて、ゲームロジックを別々の関数内に構築することはより重要な意味を持ちます。ここでは単純にレデューサーパターンを使用して構築します。
アセットの管理
これはまだR3Rの発展領域です。テクスチャ用にJSXタグでurl属性を指定します。Webpackを使用すれば画像へのローカルパスが要求できます。
<texture url={ require( '../local/image/path.png' ) } />
このセットアップを用いると、ディスク上で画像を変更すれば3Dシーンがリアルタイムに更新されます! ゲームデザインやコンテンツを素早く繰り返し更新していくのに非常に有効です。
3Dモデルなどほかのアセットについては、さらにJSONLoaderなどThree.jsのビルトインローダーを用いて処理する必要があります。3DモデルファイルをロードするためにカスタムWebpackローダーを使用した経験がありますが、結局のところなんのメリットもない面倒な作業でした。モデルをバイナリデータとして扱い、file-loaderを用いてデータを読み込むほうが簡単です。モデルデータのライブリローディングをすることもできます。この動作はサンプルコードで参照できます。
デバッグ
R3Rでは、ChromeおよびFirefox向けにReact開発者ツールの拡張機能をサポートしています。これを使うと、まるでvanilla DOMであるかのようにシーンを検査できます! このインスペクター内で要素上にマウスポインターを合わせると、シーン内にバウンディングボックスが表示されます。また、テクスチャ定義の上にマウスポインターを合わせると、シーン内のどのオブジェクトがそれらのテクスチャを用いているかが分かります。
また、react-three-renderer Gitterチャットルームに参加すると、アプリケーションのデバッグに関するサポートが得られます。
パフォーマンスの考察
Charisma The Chameleonの開発中に、このワークフロー特有のパフォーマンス問題にいくつか直面してきました。
- Webpackを使用したホットリローディングにかかる時間は30秒程度もあった。リロードのたびに巨大なアセットをバンドルに上書きする必要があったためで、解決策はWebpackのDLLPluginを実行し、リロード時間を5秒以下に削減することだった
- シーンではフレームレンダーごとに1度だけsetState()を呼び出すのが理想だ。ゲームを分析したあと、React自身が大きな障害になる。フレームごとにsetState()を複数回呼び出してしまうと、二重レンダリングやパフォーマンスの低下の原因になる
- オブジェクトが一定数を超えると、R3Rのパフォーマンスは素のThree.jsコードよりも劣るようになる。私の場合は約1000個のオブジェクトでそうなった。サンプルの「Benchmarks」の下の項目でR3RとThree.jsを比較できる
ChromeデベロッパーツールのTimeline機能はパフォーマンスをデバッグするための素晴らしいツールです。簡単にゲームループを視覚的に分析でき、さらにデベロッパーツールの「Profile」機能よりも解読しやすいです。
最後に
Charisma The Chameleonをチェックして、セットアップを使ってなにができるか確かめてください。この一連のツールはまだとても新しいですが、WebGLのゲームコードを手際よく構築するには、ReactとR3Rの組み合わせが不可欠であると分かりました。また、小規模ながら成長中のR3Rサンプルのページをチェックすると、よく整理されたコードサンプルを参照できます。
※本記事はMark Brown、Kev Zettlerが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:Building a Game with Three.js, ReactJS and WebGL)
[翻訳:市川千枝/編集:Livit]