Admittedly something small.
2016年12月24日

モナドのまほう 第13話『ネオアームストロングCannon.js砲じゃねえか完成度高けーなオイ』

ゲーム開発


第一話 / 前回

ゲームを作る日記です。古代の宗教家の誕生日に世間は沸いているようですが、私は無宗教なので特に感慨はないです。

PureScript By Exampleスペイン語版

我々PureScripterの聖典たる"PureScript By Example"に、スペイン語版が登場したようです。スペイン語は四億人以上の話者がいて、割合ではインターネット全体の8%。英語、中国語に次いで第3位だそうです。これを機にもっとユーザが増えるといいですね。

Cannon.jsを導入しました

衝突判定にCannon.jsを導入しました。今まではテキトーに実装したガバガバ判定だったので、簡単に地面にめり込んだりジャンプもせずに段差にぴょんと飛び乗ったりしていましたが、これでキャラクターが地形にめり込んだりしなくなりました。地形にめり込むと上方に弾き飛ばすという実装になっていたので、以前は地下に潜ると壁にぶつかっただけで地上にはじき出されたりしていたんですが、これでちゃんと地底探検ができるようになりました。

一方で高いところから飛び降りて着地すると反発係数がゼロなのにぴょんと跳ね返ったりとか、自分がいる場所にブロックを置くとキャラクターが弾き飛ばされて上空高く吹っ飛んだり、Havok神ならぬCannon神の威光を感じさせます。見た目に変化はないのでスクリーンショットはありません。

Cannon.jsのサンプルにちょうどボクセル形状のフィールドを歩きまわるサンプルがあったので、これをそっくり借りて改造して使っています。1チャンクが161616ですから、それぞれのボクセルについて一対一で衝突形状を生成してしまうと、たった1チャンクで最大で4096個もの立方体形状を使う可能性があります。平均的には半分の2000程度でしょうか。これはあまりに多すぎますが、このサンプルでは近傍のボクセル同士を連結して形状を減らす仕組みが使われていて、1チャンクあたり30~50個程度と衝突形状の数を大きく削減できます。それでも333個のチャンクについて物理シミュレーションを行うので、500個程度の衝突形状が衝突判定に使われ、かなり重いというのは確かです。もう少しチューニングを加えないと快適にプレイできないと思います。cannon本体側でボトルネックになっていそうな部分も見かけたので、もしかしたらcannon.js本体に手を入れてチューニングする必要があるかもしれません。それでも効率面は今のところなんとかなりそうな気がします。ゲーム開発は本当にチューニングがつらいです。

Cannon.jsの使いかたは難しくありません。まずはWorldオブジェクトとBodyオブジェクトを作り、BodyWorldに追加します。massプロパティは文字通り質量ですが、デフォルト値の0にすると一切移動しない静的な物体になり、ゼロより大きい値にすると重力に引かれてたりして力を受けると動くようになります。適当にサンプルコードっぽいもので示せばこんなかんじです。

// ワールドを作る
var world = new CANNON.World();

// 物体を作る
var shape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5));
var body = new CANNON.Body({ mass: 1.0 });
body.addShape(shape);

// ワールドに追加
world.addBody(body);

ワールドが作成できたら、キー入力などに応じて物体に位置や速度、力などをプロパティで設定し、それからWorld.stepを呼び出すと、シミュレーションが指定した時間のぶんだけ進みます。それが終わると物体の位置や速度などが更新されているので、これを読み取って描画に反映させます。

// プレイヤーキャラクターの状態
var playerPosition, playerVelocity;

// ゲームループ的なものがあるとして
gameloop(function(){

    // キー入力などに合わせてプレイヤーの次の状態を決める
    if(isKeyPressed("w")){
        playerVelocity.z = 1.0;   // 進む
    }else{
        playerVelocity.z = 0.0;   // 止まる
    }

    // 現在の状態をbodyに書き込む
    body.position = playerPosition;
    body.velocity = playerVelocity;

    // 時間を進める
    world.step(1 / 60);

    // 状態が変わっているので取り出す
    playerPosition = body.position;
    playerVelocity = body.velocity;

    // キャラクターのメッシュをその位置に移動する
    playerMesh.position = playerPosition;
})

基本的にはこれだけです。さほど難しくはないので、ゲームを作りたい人は使ってみるといいと思います。僅かなコーディングで複雑な動きが生まれるので、ゲームのプレイにも一気に幅が生まれます。

オプションを直しました

今までオプション設定のボリュームが壊れていたんですが直しました。BGMを切り替えた時に、自然にフェードアウトしてから切り替わるようにもなっています。再生中のサウンドをいきなり切るとプチッというノイズになるので、サウンドを切るときは必ずフェードアウトさせましょう。

影の設定とかも再読み込みしないと反映されなかったのですが、設定を変えるとすぐに反映されるように直しました。シャドウはほんとに重いんですが、見た目だけでなくプレイアビリティに影響してくるので省略するわけにいきません。ゲームの技術レポートとかに載っていそうな比較画像:

シャドウと頂点カラーなし

頂点カラーあり

両方あり

頂点カラーなしだと平面が同じ色でベターッとなってしまい、凹凸がよくわからなくなってしまいます(これはテクスチャをちゃんと描いていないせいでもありますが)。頂点カラーで凹凸を強調すると形状がだいぶわかりやすくなりますし、シャドウはキャラクターの足元の位置を示すという役割もあります。ジャンプした時の着地点の目安なんかにもなるので必要です。

ツクールMV下調べメモ

RPGアツマールでの公開を検討するためのメモ:

たとえば、メッセージを表示するには$gameMessage.addでいいみたいです。公式のAPIドキュメントが見当たらないのですが、どうも本体にも付属していないらしく、有志がコードを直接読んで解読しているようです。公式ドキュメントくださいよ!

純粋関数型プログラミング言語の効率について

PureScriptはすべてのデータ型が不変な純粋関数型プログラミング言語であり、オブジェクトの状態を変更したいときは、オブジェクトを直接変更するのではなく新しい状態のオブジェクトを新たに作成します。これを聞くとかなり効率面が心配になる人も多いと思います。

でも、今回PureScriptの効率できつかったのは、データ型の不変性が原因ではなく、PureScriptの関数がすべてカリー化されているせいで大量の関数オブジェクトが作成されてガーベージコレクタが動いてカクつく、というものでした。具体的には、地形を生成する関数、地形のボクセルデータから3Dメッシュの形状を計算する関数のふたつは、個々のボクセルにアクセスするので大量のループ処理が入るのですが、ここで大量のクロージャが作成されてどうにもならず、おとなしくJavaScriptで書きなおしました。この大量のEffのクロージャは、もしコンパイラがインライン化をするようになれば将来的には解消されるかもしれません。

現状の実装のCPUプロファイルをとって見ると、一番支配的なのが(program)で、これのほとんどはwebglによるレンダリングでしょう。それからArrayCollesionMatrix.resetなどのcannon.jsの関数や、MaterialDefines.isEqualのようなbabylonjsの関数が並びます。

ある程度の最適化を加えたら、どんなゲームであろうと効率はグラフィックスと物理シミュレーションにほとんど支配されてしまい、プログラミング言語が純粋であろうがなかろうがあまり関係なくなってしまいます。もちろん一部では純粋関数型言語の特性によってボトルネックが生じた部分の最適化は必要なものの、今回の例でいえば2つの独立した純粋な関数をJavaScriptに書きなおしただけで、PureScriptが原因のボトルネックは解消されました。純粋関数型プログラミング言語ではデータ型が不変なので効率が心配というのはわかるんですが、実際のところあまり心配はいらないでしょう。ちゃんとプロファイルを取ってボトルネックをひとつひとつ潰していくという、どんなプログラミング言語にでもいえることをちゃんと丁寧に実施することが大切なんだと思います。

次のお話


このエントリーをはてなブックマークに追加