こんにちは。中嶋( @ringo ) です。本業の開発が佳境に入っている影響で、前回から間が空いてしまいましたが、今回は、本格的なゲーム内容実装の第一段階として、サーバ側にキャラクターやMOBなどの物理挙動を実装しました。これで、本当の意味でのMMOGになりました。
(拙著「オンラインゲームを支える技術」ではJavaScriptよりも実績の多い制作技法を紹介しています。そちらもどうぞ!)
いつもどおり、サーバソースとUnity3Dのスナップショットがgithubに上げてあります: https://github.com/kengonakajima/wise9mmo
さて今回の修正のメインは、物理挙動をサーバにも実装したところです。
前回までのシステムは、プレイヤーキャラクターの動きは、以下のようなしくみになっていました。
- クライアント側で動かし、座標を更新する
- 更新された新しい座標をサーバへ送信する
- サーバはそれを無条件に受け入れる
このままだとクライアントからウソの座標を送れば不正行為が好きなだけできます。
数分で終わる少人数対戦ゲームならともかく、永続的な世界をもつMMOGでは、これではだめです。
そこで次のように修正しました。
- クライアント側で動かし、座標を更新する
- 更新された新しい座標をサーバへ送信する
- サーバでは、クライアントとは別に1体のキャラクター(ghost)を初期化する。
- サーバ側で、地形とghostの衝突判定や、重力による落下などを実装する。
- サーバ側のghostは、クライアントから受信した座標を「目的地」に設定して、その地点に向かって、サーバ側の物理法則にしたがって移動する。(移動できない場合がある)
- サーバ側のghostの座標をクライアントに通知し続ける
- クライアントでは、サーバ側のghostとの位置の齟齬が大きくなりすぎたら、強制的にghostの位置にワープする。
箇条書きにすると長いですが、ようするに「サーバ側にゴーストを立てて、それをリモコン操作する」というモデルです。
ゴーストはクライアントの動きにできるかぎり付いていきますが、ゴーストができるのはジャンプと水平移動と落下だけなので、不正な値(空中の座標や地中の座標)を指定しても、ゴーストはそこにはたどり着けず、不正行為はできません。
クライアント側とサーバ側の両方でキャラクターを独立に動かすことで、ほとんどの状況において、すぐに反応するなめらかなプレイ感覚を実現しつつ、不正行為を無くすことができます。
実際の動きは、次の動画で見れます:
動画では、ついてくる立方体が2つあります。
大きいほうの立方体が「ゾンビ」で、これは敵(MOB)です。速度は2m/secです。
小さいほうの立方体がプレイヤーキャラクターのゴーストで、この速度は5m/secです。1mあたり 200ミリ秒かかる計算になります。
動画では、ゴーストが正確についてきているのがわかります。
また、動画の末尾で、高いところから落ちてダメージを2受けているログが画面に出ていますが、このダメージ計算は、ゴーストが落ちて地面に激突したことを、サーバで判定して、その結果HPが減ったことをstatusChangeプロトコルで通知しています。
ゴースト方式を用いる場合、サーバ側の「移動後座標受信関数」は、このようになります:
function move(x,y,z,sp,pitch,yaw,dy,dt){
this.pc.setMove( x, y, z, pitch, yaw, dy, dt );
}
// 以下、actor.jsの setMoveの実体
// args: すべて float
Actor.prototype.setMove = function( x, y, z, pitch, yaw, dy, dt ) {
this.yaw = yaw;
this.pitch = pitch;
this.toPos = new g.Vector3(x,y,z); // ここに行きたいという座標だけ更新
};
それぞれのActor(サーバで物理挙動するものの名前) が持っているtoPosという変数に座標を入れているだけです。
それを、それぞれのActorが動くときに毎回調べて、その座標に近づくように動かします:
// 目的地が設定されてる場合は
var toV = this.pos.diff(this.toPos);
if( toV.length() < 0.01 ){
this.toPos = null;
} else {
toV = toV.normalized().mul(this.speedPerSec);
nextpos = new g.Vector3( this.pos.x + toV.mul(dTime).x,
this.pos.y + this.dy * dTime,
this.pos.z + toV.mul(dTime).z );
}
}
向いてる方向を正規化して、1秒あたりの速度をかけて、実際に経過した時間をさらにかけているだけです。正規化しているので、極端に遠い座標が無効化されます。
物理挙動自体は、クライアントとできるだけ同じになるように、クライアントのコードをほぼそのまま移植しました。JSからJSへの移植なのでコピペして変数名を変えるぐらいのものでとても簡単です。
通信環境が劣悪なときなど、サーバにいるゴーストとクライアントの表示が大きく違うときに、強制的にワープさせるのはクライアント側のこのコードです:
// 目的地が設定されてる場合は
var toV = this.pos.diff(this.toPos);
if( toV.length() 5 ){
hero.transform.position = pos;
}
これが動作するとこんな感じの画面になります:
この動画では、サーバ側のゴーストが目的地座標の更新を行わないようにして、
かならず5m離れるごとにワープさせられるようにしています。
実際の環境に近づけて、node.jsを数十ミリ秒ランダムにsleepさせて試したところ、 minecraftの世界では、PCの環境で遊ぶ分には全然問題ないレベルになるようです。
これは、minecraftでは世界がかならず1mの立方体からできていて、その移動に200msかかるので、PCインターネットでの数十msの遅延では、このブロックの範囲を大きく越えるようなズレが起きにくいことがあります。ごく薄いカベのような物体が無いということです。
今回は、サーバでゴーストやMOBを動かすために、サーバに「タスクシステム」を追加しました。1ミリ秒ごとに全部のゴーストやMOBのすべてをスキャンして、設定されている時間がきたら、設定されてるコールバック関数を呼び出します。
まず node.jsのsetIntervalを1ミリ秒に設定して、1ミリ秒ごとに全部の動くものに対してpoll()します。
以下はmain.js のトップレベルの呼び出しです。
var loopCounter = 0;
setInterval( function() {
var d = new Date();
var curTime = d.getTime();
fld.poll(curTime );
loopCounter++;
}, 1 );
setInterval( function() {
sys.puts( "loop:" + loopCounter );
}, 1000 );
サーバ内のMOBやゴーストの数が増えすぎて、サーバの負荷が高すぎるかどうか判定するために、loopCounterを pollするたびに加算して、1秒に1回、その回数を出力しています。
1ミリ秒に1回の呼び出しなので、平均的にはほぼ毎秒1000回呼び出されるはずですが、その処理に1ミリ秒以上かかるようになると、1000より小さくなり、サーバの負荷が高すぎることがわかります。
ゾンビなどの「よく動くもの」は30ミリ秒ごとに1回、挙動関数を呼び出すようにしています。 たとえば、ゾンビの場合は、以下のような挙動関数を持っています。
// pcと距離が2以内だったら攻撃
// 常にpcの方にむく
// hateはたまに更新
// まずxyから pitchを決めて進もうとし、前にブロックがあったらジャンプ、し続ける
// 穴があったら素直に落ちる
// 経路探索しない
function zombieMove( curTime ) {
if( this.hate == undefined ) this.hate = null;
if( ( this.counter % 10 ) == 0 ){
var pcs = this.field.searchLatestNearPC( this.pos, 10, curTime - 1000 );
if( pcs.length > 0 ){
this.hate = pcs[0];
} else {
this.hate = null;
}
}
var targetPos ;
if( this.hate ){
targetPos = new g.Vector3( this.hate.pos.x, this.hate.pos.y, this.hate.pos.z );
} else {
targetPos = new g.Vector3(2,2,2);
}
var diff = this.pos.diff( targetPos ) ;
this.pitch = this.pos.getPitch( diff );
if( this.pos.hDistance( targetPos ) < 1.0 ){
this.vVel = 0;
} else {
this.vVel = 1.0;
}
// 障害物あったらジャンプ
if( ( this.counter % 50 ) == 0 ){
sys.puts("z: lastxyz:" + this.lastXOK + "," + this.lastZOK + " dy:" + this.dy );
if( ( this.lastXOK == false || this.lastZOK == false ) && this.dy == 0 ){
this.dy = 4.0;
this.falling = true;
sys.puts( "zombie jump!");
main.nearcast( this.pos,
"jumpNotify",
this.id,// TODO
this.dy );
}
}
}
ゾンビの動きの特徴は、「近くにプレイヤーを発見したら、ターゲットとして記憶し、遠く離れるまでは追いかけ続ける。 見失ったら、初期位置(2,2,2)に戻る。目の前に壁があったら、とりあえずジャンプする」 です。
これだけでも、立方体がおいかけてくる様は、けっこう恐ろしいものです。
さて、今回はゲーム内容の実装第一段階ということで、サーバ側での物理挙動と敵の動きを実装しました。
次は、通信プログラミングではないけど、動画でも登場していた、人間の形をしたキャラクターが歩き回るようにして、よりゲームらしい画面にしていきます。
Related posts:
Post a Comment