今回は二次元物理エンジンBox2DJSとWebGLライブラリ、three.jsを組み合わせて、カーレースゲームのサンプルを作成しました。
Box2DJS
Box2DJSはC++で書かれた物理演算エンジンをJavaScriptに移植したものです。質量・速度・摩擦といった、古典力学的な法則をシミュレーションするゲーム用の2D物理演算エンジンとして、現在ではActionScript、Java、C#、Pythonにも移植されています。
前回の記事ではenchant.jsとthree.jsを併用しましたが、今回は3D表現としてthree.jsを使う事は変わりませんが、メインのロジック部分にBox2DJSを使用しました。
ソースコード
※操作方法 : 前進「↑」キー、後退「↓」キー、旋回「←→」キー、カメラアングル切り替え「スペース」キー
※WebGLを使用したデモなのでChromeかFireFoxで閲覧してください。
ゲームロジックは左上の2Dマップ部分で、ホイールを回転させ、地面との摩擦を生じさせる事で車を走らせたり、障害物に衝突する事で生まれる物理法則をBox2DJSで再現しています。表示部分には、three.jsを利用して、コースを作成し、2Dマップ部分の車の位置と角度に応じて、車の位置と角度、カメラの位置と角度を変更し、臨場感のある3D表現を実現しています。Box2DJSはあくまで二次元物理エンジンなので、このゲームには高さの概念がありませんが、表現力自体はまるで3Dレースゲームで、ゲームとして十分に成り立つ印象はあります。
まずはBox2DJSにてカーレースゲームを作りました。ゲームロジックは全てここに集約されるので、ここでゲームとして面白いものにしなくてはなりません。Box2DJSは多くの方がブログで多くサンプルを公開していますので、探せば面白い使い方が見つかるかもしれません。JavaScript版のドキュメントは多くありませんが、元々移植されたものですので、本家サイトやActionScript3.0版である程度予測する事は可能です。
Box2DJSでのゲームが出来たら、マップやゲーム状況を共有します。マップは壁と障害物の情報を配列に格納して共有しています。この配列を元にBox2DJSでは壁と障害物を作成、three.jsでは3Dの壁を大量に生成します。この時にジオメトリを結合することによって描画付加を少なくすることができます。Three.jsでは、THREE.GeometryUtils.merge()メソッドでジオメトリを結合できます。こちらサイトを参考にさせていただきました。WebGL対応のライブラリThree.jsを爆速にする方法
main.js(321行目~)
// 壁(高)
geometry = new THREE.Geometry();
texture = new THREE.ImageUtils.loadTexture("block2.jpg");
material = new THREE.MeshPhongMaterial({map: texture, bumpMap: texture, bumpScale: 0.1, specular: 0xFFFFFF, shininess: 1000});
for (i = 0, max = MAP.length; i < max; i = i + 1) {
for (j = 0, max2 = MAP[i].length; j < max2; j = j + 1) {
if (MAP[i][j] == 1) {
mesh = new THREE.Mesh(new THREE.CubeGeometry(MAP_BLOCK_SIZE, MAP_BLOCK_SIZE, MAP_BLOCK_SIZE), new THREE.MeshBasicMaterial());
mesh.position.set(MAP_BLOCK_SIZE * j + (MAP_BLOCK_SIZE / 2), MAP_BLOCK_SIZE / 2, MAP_BLOCK_SIZE * i + (MAP_BLOCK_SIZE / 2));
THREE.GeometryUtils.merge(geometry, mesh);
}
}
}
mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
_scene.add(mesh);
Box2DJSの車と障害物の二次元物理演算結果をthree.jsの3Dオブジェクトに反映させます。レースゲームだとスピート感が重要ですが、requestAnimationFrameだと付加状況に合わせてフレームレートを上げ下げし、付加状況によって車のスピード感が変わってしまいますのでsetIntervalを使用しています。ちなみにFPSは30に設定しています。
main.js(177行目~)
setInterval(function () {
// Box2Dレンダリング
box2dCar.upDate();
box2dWorld.Step(1 / FPS, 8, 3);
box2dWorld.ClearForces();
box2dWorld.DrawDebugData();
// Box2Dの車の位置情報取得してThreeJSの車情報に変換
var tagX = box2dCar.getPosition().x * SCALE;
var tagZ = box2dCar.getPosition().y * SCALE;
var tagAngle = -box2dCar.getAngle();
var steeingAngle = box2dCar.getSteeringAngle() + getAngleByRotation(90);
// ThreeJSレンダリング
threeJsCar.upDate({
x: tagX,
z: tagZ,
angle: tagAngle,
steeringAngle: steeingAngle
});
threeJsCamera.upDate({
x: tagX,
z: tagZ,
angle: tagAngle
});
var i = 0, max;
for (i = 0, max = threeJsBoxes.length; i < max; i = i + 1) {
threeJsBoxes[i].upDate({
x: box2dObstacles.getBoxPosition(i).x * SCALE,
z: box2dObstacles.getBoxPosition(i).y * SCALE,
angle: -box2dObstacles.getBoxAngle(i)
});
}
threeJsRenderer.upDate({
scene: threeJsScene.getScene(),
camera: threeJsCamera.getCamera()
});
if (threeJsControls) threeJsControls.update();
}, 1000 / FPS);
このように、ゲームロジックとしてBox2DJSの二次元物理演算を利用し3D表現としてthree.jsを利用して、必要なデータを共有するだけで、あたかも3Dゲーム風な表現力とゲーム性を得ることが出来ました。ゲームロジックと3D表現をいかに分離するかが、コードをシンプルにするコツかと思います。
次は三次元物理演算と3D表現を使って、ゲームを組んで見たいと思います。