Skip to content

JavaScript で MMOG をつくってみよう その3

こんにちは、中嶋 (@ringo) です。

この連載では、MMOG開発ではまだ実績が少ないJavaScriptを使っていますが、
拙著「オンラインゲームを支える技術」という本では実績のある制作技法を紹介しています。そちらもどうぞ!

さて前回はUnityを使ってFPSの操作系を実装してみたのでした。
Unityの処理系が、コントロールキーやスペースキーといった
操作ボタンの種類の情報を隠して、Fireとか 垂直移動とかジャンプ(!)
といった意味のある値に変換してくれるので、非常に実装しやすいです。
まあこれも世界標準のFPS操作系を使うことにしてるからですが。

操作系の実装はかなりやりやすいことがわかりました。

今回はクライアント側で必要な描画量が実現できるか調べてみます。

お手本のminecraftで、フォグをOFFにすると、かなり遠く(200セル先)まで見通すことができます。

次の動画を、しばらくじっと見てみてください。

プレイ開始した後、画面の手前から、だんだん見える範囲が広がっていきます。
見える範囲が最大まで広がるには、20秒ぐらいかかるようです。

その後、突然左を向くと、また同じように見える範囲がだんだん広がっていきます。
さらにもとの向きに戻したら、今後はすぐに遠くまで見えます。

「すぐにプレイ開始できるが、完全な状態になるまで数十秒かかる」
という非同期処理は、MMOGではいつも使われます。

minecraftの地形は、ちいさな「箱」の集まりで表現されてます。
箱の数が、かなり多いですが。。大丈夫かな?

描画の限界性能は、毎フレーム描画するポリゴンの数に比例します。
ひとつの箱を描画するのに、三角形のポリゴンは、2枚から12枚必要です。
2枚とは1面だけが見えてる状態で、12枚は全部の面が見えてる状態です。
もし、箱がきっちり地面の上に平らに並んでいたら、上面の1面だけが見えてるので、
三角形ポリゴンは2枚あればOKです。

今後のために箱1個のサイズを「1m」と言ってしまいます。
ポリゴンの座標はfloatで、1.0fが1mです。
また、「箱」はふつう「ボクセル(Voxel)」と言います。

minecraftみたいに200向こうまで見えたらきっと楽しいです。
遠くに見える陣営の様子をうかがいながら作戦を立てるとかできれば最高です。

さて200m向こう(高さも)まで全部箱で埋まっていたら、

Far: 200 x 200 x 200 = 800万個 !?

むー。多い。。 ポリゴン数でいうともっと増えます。
Nの3乗に比例する世界はスゴイ。

でも実際には、地面はほとんど平面だし、
実際にはこれほど大きな数にならないはず。
もし全部平面だったら200×200 = 4万しかありません。
自然界のフラクタル次元はそんなに高くない。

また、カメラがうつしているのは全体ではなくて、台形(視錐台といいます)の領域で、
その角度は60度ぐらいしかありません。なので本当に描画されるボクセルは、
ざっくり半分ぐらいの数になります。そしたら2万。

そこでてきとうに計算をします。

もし地面が完全に平坦だとしたら、 200 x 200 = 4万。
立体交差や急な崖とかが多少あるとしても、4万の2〜5倍にはおさまるでしょう。
視錐台のかたちで、半分。だから、 4万 x 5 / 2 = 10万。

ボクセルの数で、10万個が、目標ですね。

必要なポリゴン数は、ボクセルの2倍から12倍です。
仮に6倍とすると 60万ポリゴン。これを秒間30回更新したら、1800万ポリゴン/秒。
1秒に30回(FPS=30)は操作のレスポンスを保つための死守ラインです。

この数値を念頭に置きながら、Unity3Dの限界性能を探るプログラムを書きます。

で、作ってみたのがこれ。テクスチャを貼った大量の立方体を表示してみます。

Unity3Dには”Cube”という物体が内蔵されていて、関数1つ呼べば生成できます。
CONTROLキーを押すごとに 256個作ります。

for(t = 0;t<16;t++){
for(s = 0; s<16; s++){
var cu = Instantiate( prefabVoxel, Vector3( t*1.2 , tmpcounter0 *1.2, s*1.2 ), Quaternion.identity );
cubes.Push(cu);
}
}

うーーむ。4000個を越えただけでもうFPS30を切ってしまいました。。。
桁がひとつ足りないですね。簡単にはいかないようです。

ハードウェアを酷使するプログラムを速くする常套手段は、
「データはまとめて送れ」です。3Dプログラミングなら、
GPUにまとめてどかっとデータを送ると速くなります。

minecraftで、 だんだん遠くが見えるようになるのも実は、16x16x16ボクセルごとに、
データをまとめてプログラムで生成して、その単位で渡しています。

Unityでも同じことができます。

以下のように深いループをまわして、頂点データを作って、

var mesh : Mesh = GetComponent(MeshFilter).mesh;

mesh.Clear();

var n:int = 4;
var vertices : Vector3[] = new Vector3[ n*n*n *8 ]; // 頂点座標の配列
var uv : Vector2[] = new Vector2[ n*n*n *8 ]; // UV座標の配列
var triangles : int[] = new int[ n*n*n * 36 ]; // 三角形の頂点の設定

var vi:int=0;
var ti:int=0;

for( var z:int = 0; z < n; z++ ){
for( var y:int = 0; y < n; y++ ){
for( var x:int = 0; x < n; x++ ){
// makeCube( Vector3(x*1.3,y*1.3,z*1.3));
makeCube( Vector3(x*1.3,y*1.3,z*1.3 ),   // Cubeを作る
vertices,
uv,
vi,
triangles,
ti );
vi += 8;
ti += 36;
}
}
}

mesh.vertices = vertices; //  6x6x6 = 108個のcubeを一括で設定。
mesh.uv = uv;
mesh.triangles = triangles;

mesh.RecalculateNormals();

makeCubeの定義は以下のような感じです。

// 指定した位置に頂点をもつ立方体を返す
function makeCube( basepos : Vector3, vertices : Vector3[], uv : Vector2[], vi : int, triangles : int[], ti : int )
{
vertices[vi+0] = basepos + Vector3( 0,0,0 ); // Z=0
vertices[vi+1] = basepos + Vector3( 0,1,0 ); //
vertices[vi+2] = basepos + Vector3( 1,0,0 ); //
vertices[vi+3] = basepos + Vector3( 1,1,0 ); //

vertices[vi+4] = basepos + Vector3( 0,0,1 ); // Z=1
vertices[vi+5] = basepos + Vector3( 0,1,1 ); //
vertices[vi+6] = basepos + Vector3( 1,0,1 ); //
vertices[vi+7] = basepos + Vector3( 1,1,1 ); //

uv[vi+0] = Vector2(0,0); // Z=0
uv[vi+1] = Vector2(0,1);
uv[vi+2] = Vector2(1,0);
uv[vi+3] = Vector2(1,1);

uv[vi+4] = Vector2(1,1); // Z=1
uv[vi+5] = Vector2(1,0);
uv[vi+6] = Vector2(0,0);
uv[vi+7] = Vector2(0,1);

triangles[ti+0] = vi+0; // Z=0平面の三角0
triangles[ti+1] = vi+1; //
triangles[ti+2] = vi+2; //
triangles[ti+3] = vi+1; // Z=0平面の三角1
triangles[ti+4] = vi+3; //
triangles[ti+5] = vi+2; //

triangles[ti+6] = vi+4; // Z=1平面の三角0
triangles[ti+7] = vi+6;
triangles[ti+8] = vi+5;
triangles[ti+9] = vi+5; // Z=1平面の三角1
triangles[ti+10]= vi+6;
triangles[ti+11]= vi+7;

triangles[ti+12] = vi+5; // X=0平面の三角0
triangles[ti+13] = vi+1;
triangles[ti+14] = vi+0;
triangles[ti+15] = vi+0; // X=0平面の三角1
triangles[ti+16] = vi+4;
triangles[ti+17] = vi+5;

triangles[ti+18] = vi+2; // X=1平面の三角0
triangles[ti+19] = vi+3;
triangles[ti+20] = vi+6;
triangles[ti+21] = vi+3; // X=1平面の三角1
triangles[ti+22] = vi+7;
triangles[ti+23] = vi+6;

triangles[ti+24] = vi+0; // Y=0平面の三角0
triangles[ti+25] = vi+2;
triangles[ti+26] = vi+6;
triangles[ti+27] = vi+0; // Y=1平面の三角1
triangles[ti+28] = vi+6;
triangles[ti+29] = vi+4;

triangles[ti+30] = vi+1; // Y=1平面の三角0
triangles[ti+31] = vi+5;
triangles[ti+32] = vi+3;
triangles[ti+33] = vi+5; // Y=1平面の三角1
triangles[ti+34] = vi+7;
triangles[ti+35] = vi+3;
}

ほとんどデータのようなコードですが。。
3Dプログラミングでは、原始的な形状を生成するときは、
よくこういうコードが出てきます。
こういうコードは、きっちりサブルーチンの中に閉じ込めておけば、
コード全体が混乱することはありません。

ちなみに、配列の書式がJavaScriptとちょっと違っています。

var a : int[] = new int[100]; // Unity組み込み配列
var a : Array = new Array( ... ) // JavaScript配列

Unityの組み込み配列は、JavaScriptの配列と違って、型を固定することで、
非常に速くなります。

これをくみこむとこんな感じになりました:

10万個ぐらい出してもFPS=25以上出ています。

注意点が2つ。

おなじPCで、FPS30で動画キャプチャーをしているので、
その影響で25とかになってしまってますが、キャプチャーしないときは、
10万ボクセルでFPS40ぐらい出ています。

もうひとつ重要なのは、プログラムがボクセルデータを生成する瞬間、
「かくかく」止まってる瞬間があることです。
TABキーをおすごとに、 4x4x4=64個の塊を 6×6=36個、計2304個生成しますが、
そのために大体 50ミリ秒ぐらい止まるようです。

なので、minecraftと同じように「だんだん遠くに広げる」という方法で、
順次ポリゴンのデータを生成する工夫も必要になりそうです。

10万ボクセルをポリゴン12枚でテストしたので、
120万ポリゴン x FPS30 = 3600万ポリゴン/秒

ということで、かなりいい成績がでました!

というわけで描画速度については、大丈夫そうです。

あと1つだけ。
Unity Web Playerを使う場合も大体同じ性能が出るのですが、
Chromiumよりも Safariのほうがフレームスキップが起きにくくて、
描画がなめらかでした。バージョンによって違うかも。

Webプレイヤーのプレイアブルはこちら:

fpstest2_wp.html

画面左上のClearボタンを押すとボクセルを全部消せます。

プロジェクト全体をソースも込みで、ここにアップロードしておきます:

wise9_20110402.zip

Unityフリー版は、バージョン管理システムを使うのがほとんど不可能なほど、
めちゃくちゃにディレクトリを壊しまくるので、zipでまとめてありあす。
それだけのために13万も払えないよ。。

ほんとはまだ、キャラクター描画とかあるけど、何かのついでにやるとして、
クライアントは、だいたい大丈夫になったので、
次回からは通信部分とサーバーの調査に入ります。

(続く)

このエントリーをはてなブックマークに追加
はてなブックマーク - JavaScript で MMOG をつくってみよう その3
Post to Google Buzz
Share on GREE

Related posts:

  1. JavaScriptで MMOG をつくってみよう その1
  2. JavaScriptで MMOG をつくってみよう その2

Facebook comments:

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*