はじめに
WebGLの3Dブラウザゲームを作りたいと考えた時、どのようなフレームワークを選択すればよいのでしょうか?three.jsは一番メジャーなWebGLの3DCGライブラリですが、基本的に3DCGを描画する機能しかないので、これ単体でゲームを作るには少し苦しい局面もあります。そこで、Reactとreduxに、UIやゲームの進行管理を任せることでそこそこの規模のゲーム開発ができるのでは?と思ったのでthree.js x React x redux で3Dオセロゲームを作ってみました。
作った物
こちらでプレイできます。
https://arihide.github.io/reversi3d/
ソースコードはこちら
UI・3D描画周り: https://github.com/Arihide/reversi3d
オセロ のルール・AI周り: https://github.com/Arihide/reversi.js
実装時に行ったこと
three.jsのオブジェクトもコンポーネントとして扱う
Reactでは React.Component を継承することでコンポーネントを作成しますが、Canvas内に描画される3Dオブジェクトも THREE.Mesh を継承してコンポーネントっぽくしてみました。
以下はオセロ盤のオブジェクトの実装を簡素にした例です。
import store from 'js/store'
import { Mesh } from 'three'
export default class BoardMesh extends Mesh {
constructor(geometry, material) {
store.subscribe(this.onChange.bind(this))
}
onChange() {
// 石がひっくり返ったりしたときの処理などゲームが進んだときの処理を書く…
}
}
流石にReactのコンポーネントほど便利なライフサイクルはないのですが、
reduxのstoreも購読することであたかもView Treeの一部に組み込まれているかのように扱えているのでとても便利になりました。
three.jsは一つのファイルにオブジェクト追加処理を書いていくとどんどん複雑になってしまうので、こういった形で分離できるのはとても良いですね。
AI・ゲームロジック部分の別モジュール化
今回実装するにあたって、UI表示とゲームロジックの部分を別リポジトリに分けました。
UI・3D描画周り: https://github.com/Arihide/reversi3d
オセロ のロジック・AI周り: https://github.com/Arihide/reversi.js
そして、以下のようにUI側のpackage.jsonにパッケージとして追加しました。
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-redux": "^7.2.0",
"react-transition-group": "^4.4.1",
"redux": "^4.0.5",
"reversi.js": "git+https://github.com/Arihide/reversi.js.git",
"three": "^0.118.3"
}
このようにした1番の理由は、ゲームロジックの部分はUIと比較して複雑なので、パッケージとして分離した上で単体テストなども用意して動作確認をしたかったからです。
とにかくコアな部分は分離して粒度を小さくして信頼性をあげるのが良いと思います。
Redux DevToolsの活用
ゲーム開発においてデバッグは必要不可欠です!
デバッグはゲーム中で起こりうる全ての状態に対して行われるべきですが、ゲームによっては再現が難しい状態があったりします。
例えば今回のオセロの例だと、
- パスの局面
- 全ての石が同じ色になる局面
- 引き分け
などが挙げられます。(物凄い再現が難しいというわけではないのですが)
こういった特定の状況再現のためにデバッグツールを使用することはよくありますが、特に個人開発だとツールの用意までには手が回らないことも多いです。
なので、今回はRedux DevToolsを利用をしてみました。このツールにはStoreの履歴をJson形式でインポート・エクスポートできるので容易に状態の再現ができるようになります。
こういったツールが利用できるのもまたフレームワークを利用するメリットですね!
Web Workerの活用
オセロの最善手計算は物凄い数ゲーム木のノードを探索するので、とても重い処理です。これが行われる最中は3Dの描画がカクついてしまってよくないので、Web Workerを使用して処理を別スレッドに移すことで、カクツキを防ぎました。
Workerの呼び出し側と受けてのコードは以下です。
export async function computeMove(state) {
return new Promise(resolve => {
worker.addEventListener("message", e => {
resolve(placeDisc(state, e.data))
}, { once: true })
worker.postMessage(state.playedMoves)
})
}
self.addEventListener('message', e => {
board.initialize()
for (let move of e.data) {
board.pushMove(move.place)
}
alphaBetaPlayer.computeMove(board, bestMove => {
self.postMessage(bestMove)
})
})
どうしてもゲームなどは負荷対策が必要になってくるので、マルチスレッドなどの知識も必要になってきます。
まとめ
今回はthree.js x React x redux で3Dオセロゲームを作ってみました。
そのままthree.jsでオブジェクトを量産していくとゴチャっとしてしまう場合もありますが、コンポーネント化しreduxと結び付けることですっきりして結構大規模なゲームも作れるのでは?という感じもしました。