Skip to content

JavaScriptでMMOGを作ってみよう その6 minecraftプロトコル調査

今回は、お手本のminecraftの通信プロトコルを調べます。プロトコルが決まったら、必要なプログラムの内容は、だいたい決まります。

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

 

既存のゲームのプロトコルを調べる普通の方法は、tcpdump等のツールを使ってパケット解析をすることですが、これはなかなか面倒な作業です。

幸い、minecraft には mineserver プロジェクトという、ファンが勝手に作っている互換サーバソフトウェアがあります。このソースを読むのが手っ取り早い。

https://github.com/fador/mineserver

 

mineserverはC++で書かれたminecraftのマルチプレイサーバで、バージョン1.3までの機能をひととおり備えています。ただJava製のMODが使えないので、あまり利用されてはいないようです。プロトコル研究用には役立ちますが。

また現在はminecraft本家が 1.4に上がっていて、通信ができなくなっているので注意が必要です。通信の内容は基本的には変わっていないようです。

mineserverは、拙著でも紹介している、libeventを用いる枯れた方法で実装されています。

main()関数から読み進めて、イベントコールバックの解説をし、C++でのパケットシリアライザの実装詳細を解説し・・・・とやっていると、いくらスペースがあっても足りないので、ここでは、ポイントだけを紹介します。

minecraftの通信は大体こういう感じになっています。

  • マップデータはサーバで生成して、16x16x16ボクセルの単位でクライアントに送信
  • マイキャラクターや地形に対する操作は、クライアントで動かしてから、結果をサーバに送信。
  • サーバは受信した座標をそのまま受け入れてメモリ内容を上書き。(フラグでOFFにできる)
  • サーバで衝突判定はしないが、墜落のダメージ計算などは行う
  • 動物(mob)の出現位置や動きはサーバで決定して結果をクライアントに送信
  • 動物に対する攻撃も、クライアントで成功したらその結果をサーバに送信

ポイントは、「クライアントで動かしてから、結果をサーバに送信し、サーバがそれを無条件に受け入れる」ということです。

通常、webサービスでは「クライアントから送られて来るデータはすべて汚染されてる」と仮定して、条件を満たす場合以外は書き込みを行いません。しかし、minecraftのプロトコルでは、サーバは、クライアントから送られて来るプレイヤーの座標値をすべて信用して受け入れます。

この結果、通常ではないバージョンのクライアントプログラムを勝手に作成して、高速に移動できるバージョンのクライアントを作れば、ほかのプレイヤーよりも速く移動できるスーパーキャラを作れてしまいます。ほかにも「武器の届く範囲を遠くする」とかもできてしまいます。これではMMOGは作れません。

というわけで、minecraftは、MMOGとしてつくられていない、ということがわかりました。プレイヤー全員で1個の世界を共有して育てていくゲームではなく、プレイヤー一人一人が自分の世界を育てていくゲームである、ということです。

一般的にMMOGは、通常のWebサービスと同じく、「クライアントからのデータは全て故意に修正されてる可能性がある」という前提で作ります。

minecraftをお手本にしてMMOGを作るにはどういう修正が必要でしょうか。

minecraftの公式版のマルチプレイサーバでは、マップを破壊できないようにするオプションがあり、このオプションが有効になっていると、一旦壊れたように見えた地形が、一瞬の後、もとに戻ります。

ここでの考え方の基本は、「クライアントでとりあえずプレイヤー操作を受け入れ、その結果をサーバに送信し、それを受け入れられないならエラーを返し、エラーが返ってきたら、元に戻す」ということです。

こうすることで、ほとんどの状況で、なめらかな操作感が実現できます。

プレイヤーキャラが移動したときも、壁を越えて移動したり、速く移動しすぎたりしたときは、エラーを返せばいいでしょう。そのときは、キャラクターの位置を元に戻すようにサーバから指示します。

ただし、「操作のなめらかさのためのマージン」が必要です。

クライアント側もサーバ側も30fpsで同じように処理ループを回したとしても、その間にはインターネットがあるので、10ms〜100ms程度の、予測できない遅延が発生します。

そのため「1フレームあたり0.1mしか移動できない」という制約を設定しても、パケットがまとめて届いたら、エラーになってしまいます。このときキャラクターの位置が戻されたら不快感になります。

なので、新しい位置を通知するRPCを送信する際、クライアント側の現在時刻を入れておき、時間に対して多く移動し過ぎてるかどうかを判定します。 コードで比較するとこんな感じです。

send_updatePlayerPosition( Vector3 pos );  // minecraft の位置更新

send_updatePlayerPosition( Vector3 pos, int currentTimer ); // 時刻情報つき位置更新

それでも時刻についてはクライアントはウソをつくことができるので、その時間を累積して、インターネットの通信遅延より十分長い時間に対して、クライアントの時間がサーバの時間より多く経過し過ぎてるかどうかをチェックして、エラーを判定します。例えばサーバが5秒進む間にクライアントが8秒進んでいたらエラーとするといった感じです。これがなめらかさのためのマージンになります。

攻撃者は、このマージンの一杯までを駆使する専用のクライアントプログラムを使うことで、移動に関しては通常版のクライアントを使っている人よりも速く移動することはできます。

この方法の良いところは単純なことですが、弱点もあります。

格闘系のゲームのような、停止と移動を頻繁に繰り返すようなキャラクターの動きが主の場合は、停止している間には時刻が進まず、移動してるときだけ時間が進むようなクライアントプログラムを改変すれば、移動に関しては極端に高速化することが可能です。格闘系ゲームだとこのチートは決定的に有利になってしまうでしょう。

「minecraft立体陣取り」は、走り回るゲームになるでしょうから、この単純な方法でいいでしょう。

一般に、MMOGで「生身の人間が気持ちよく殴り合えるようなゲーム」を作るのは、極めて難しい課題になります。決定版の解決策はまだないです。

プレイヤーの移動についてはだいたいOKになりました。

 

次に、MOB(徘徊してる生物たち)とプレイヤーの関係はどうでしょうか。

一般論は無いので、MOBごとに考える必要があります。

 

お手本のminecraftで、「お邪魔キャラ」っぽいMOBは3種類。「スケルトン」、「クリーパー」、「ゾンビ」です。地獄エリアにはもっと強力なのがいます。

minecraftのMOBは「弓矢や火の玉など、飛び道具を使うものと、使わないもの」「歩いてくるもの、ふわふわ飛んでいるもの」「プレイヤーに近づいてくるもの、こないもの」の3軸で分類できます。 あとは自爆してしまうものなどちょっとした応用があるだけです。

それぞれのMOBに攻撃されるとプレイヤーはダメージを受け、体力が減少し、0になると死にます。死ぬとスタート地点にもどるので、立体陣取りでは大きな時間ロスになります。

ただ立体陣取りで大事なことは、主眼は、MOBからうけるダメージではなく、陣取りの作戦に与える全体的な影響です。

MOBはもちろん全部実装する必要はないです。良さそうなのについて考えておきましょう。

スケルトンは、一定の距離まで近づいてから、弓矢で攻撃してきます。弓矢を避けるには、スキマをあけた柵を建設するなどします。迅速に柵を作れるかはプレイヤーの腕の見せ所で、陣取りが面白くなりそうです。

minecraftでは弓矢はクライアント側でプレイヤーに当たったかどうか衝突判定をしています。専用クライアントを作ればこれはキャンセルできてしまうので、この判定をサーバでやるよう、変える必要があります。

この動画で、左側のMOBがスケルトン、右側がゾンビです。夜にしか登場しないので見にくいですが。。外れた弓矢が、地面に刺さっているのが確認できます。ゾンビはどんどん近づいてきますが、スケルトンは一定の距離を保ちます。

 

サーバで弓矢を動かし、その座標をクライアントに送ると、ネットワークの遅延が発生します。そのため、弓矢が自分に当たった場合でも、画面ではまだ弓矢が当たっていないように見える場合があります。

一瞬の操作で避けたつもりなのにやられる。

これでは納得がいきません。予測し慣れている場合の人間の反応速度は0.05秒よりもかなり小さく、ネットワークの遅延よりも小さい場合が多いのでこれはだめです。

ここでも「画面の納得感を向上させるほどチートの余地が増える」という厳しいトレードオフがあります。これは複雑なアルゴリズムなどを入れても大して改善しません。

ここは、「一発で受けるダメージを小さくする」「弓矢を遅くする」などでごまかします。幸い、FPS視点なので、「矢があたる場面」をまじまじと見ることは無いので、まあ大丈夫でしょう。 商用のゲーム開発だったらこれで何日か悩んだりするところですがここはあっさりいきましょう。

この基準で、飛び道具を使うMOBについては問題なさそうです。

次、殴ってくる敵、ゾンビはどうでしょうか。ゾンビは、高い段差を越えられないので、ゾンビが近づけないような段差を速く作れるかどうかにプレイヤーの巧拙が出ます。 あらかじめゾンビに強いアクセス通路を造るなどの戦略がチームプレイの成果を左右するでしょう。これは面白いかもしれません。

minecraftでは、ゾンビを殴る判定、殴られる判定ともにクライアントで行い、結果をサーバに送信しています。これも容易にチートされます。決して殴られないバージョンのクライアントを作るのは簡単です。

この処理もサーバに移転します。

そうすると「この間合いだと殴られてないはずなのに殴られて納得いかない。逆に自分が殴ったつもりなのにかすりもしない」という問題がでます。

定番の考え方は、納得度を増すには、「敵には厳しく、自分には甘くせよ」です。殴り合う場合は、自分の腕は長く、敵の腕は短くすれば納得しやすくなります。当たり前ですが。あと、ゾンビの歩く速度を遅くすればいいでしょう。それも大幅に。 これでゲームがつまらなくなるでしょうか?

通路作りの巧拙がチームプレイの成果に影響を与えなくなるなら問題ですが、「そもそも殴り合いにならないようにする」という計画を楽しむのがポイントなので、きっと大丈夫です。

 

MMOGのプログラミングで最も悩ましい「リアルタイム性の高い部分の判定方法」について、だいたい見通しが立ちました。 あとは、ばりばりとコードを書いていけば機械的にできあがる。はず??

 

そう簡単にいくものでしょうか。

 

次回からはコードをかいていきますが、最初にやることは、

  1. サーバでボクセルマップを保持
  2. クライアントでマップ描画
  3. クライアントから接続してサーバ内のキャラを移動
  4. それを各クライアントに同報
  5. 各クライアントでキャラを更新

これをフィールドテスト版とでも呼びましょう。コード量の予想は、サーバに300行、クライアントで1000行ぐらいかな? Unity3Dの場合「行数」にあんまり意味ないけど。

 

(続く)

 

このエントリーをはてなブックマークに追加
はてなブックマーク - JavaScriptでMMOGを作ってみよう その6 minecraftプロトコル調査
Post to Google Buzz
Share on GREE

Related posts:

  1. JavaScriptでMMOGを作ってみよう その5 RPCとORM
  2. JavaScript で MMOG をつくってみよう その3
  3. JavaScriptで MMOG をつくってみよう その1
  4. JavaScriptで MMOGをつくってみよう その4

Facebook comments:

Post a Comment

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