同時に実行するということ

このエントリーは、KLab Advent Calendar 2015 の12/13の記事です。
KLabGames事業部のyasui-sです。よろしくお願いします。

目次

はじめに

最近、サーバとクライアントが常時接続状態で多数の端末を同期させるゲームが増えてきています。
その中で一番の核であり、キモである同時という部分についての考え方を書こうと思います。
蛇足ですが、今回の説明の内容は、要件によって必要度合いが大きく異なります。
要件によっては基本形で十分であることもありますので適時ご判断ください。
本稿ではわかりやすさを重視し、MMOを題材として解説を行い、各通信時間は1秒単位でかかると設定しています。
また、サーバを含めた各端末上での時刻はすべて統一されているとします。

登場端末

  • サーバ
    • 当たり判定ロジック等々殆どの判定を行っているゲームサーバ
      今回の説明ではコマンドの仲介程度しかやっていない
  • クライアントA
    • コマンドの実行者
      ここで言うコマンドとは、移動や攻撃などありとあらゆる行動のことを言う
  • クライアントB
    • クライアントAを見ている第三者
      主にこの端末と先述のクライアントAとで「同時」というものについて考える
    • もちろん、このクライアントがクライアントAの立場になることもあります
    • ちなみにクライアントCなど他の端末が増えても考え方は同じなのでそれについては端折っています

一番の基本形

次のフローをご覧ください。

case1

これは、単純にコマンドの実行を送るフローです。
誰しもが一番初めに作成する形ではないでしょうか。

今回仮定したようなクライアントAからサーバには1秒、サーバからクライアントBには2秒かかるとした場合、
クライアントB上で実行されるのが3秒遅延してしまうということになります。
この3秒が致命的である場合もあり、この3秒をどのようにごまかすかが重要になってきます。

メリット

  • 構造が単純でわかりやすい
  • コーディングが最も簡単

デメリット

  • クライアントAとサーバで実行されるタイミングがずれる
  • クライアントAとクライアントBで実行されるタイミングが大きくずれる

サーバがコマンドを受け取ったことを返した後、実行する

次のフローをご覧ください。

case2

クライアントから送られたコマンドをサーバが全クライアントへ送信しています。

しかし、サーバからクライアントBへの通信時間がサーバからクライアントAへの通信時間より長いため、
結果的にコマンドの実行が1秒ずれてしまっています。
一番の基本形と比べ、クライアントAとクライアントBとのずれが小さくはなっていますが、
それでも要件として満たさない場合も多いはずです。

また、この方法だと致命的な問題があります。
それは、ユーザの操作に対するレスポンスが遅いということです。
上記フローによると、ユーザの入力からコマンドの反映まで2秒かかってしまっており、
その2秒がユーザにとってストレスになってしまう可能性が大いにあります

メリット

  • 構造が単純でわかりやすい
  • コーディングが簡単
  • サーバ上では完全に同期されているため、サーバでの処理が書きやすい

デメリット

  • ユーザレスポンスが悪い
  • サーバと各クライアントで実行されるタイミングが多少ずれる
  • クライアントAとクライアントBで実行されるタイミングが多少ずれる

方向性:実行時刻を送る

ここでコマンドだけの送信・仲介方法を考えることを諦め、
何らかのデータを付け加えて、このずれをどうにかするという方向へ転換してみたいと思います。
今回は実行時刻を加えてみた場合について考えてみます。
しかし、この時刻がくせ者で、サーバを含めたどの端末のどの時点の時刻かによって状況が変わってくるでしょう。
では、それぞれのケースについて詳しく考えてみましょう。

サーバがコマンドの受け取り時刻を加えて返した後、実行する

次のフローをご覧ください。

case3

サーバがコマンドの実行を受け取った際に、サーバ基準の時刻を付け加えて全クライアントへ送信しています。
そして、受け取ったクライアントではそのサーバ基準の時刻でコマンドが実行されたものとして実行します。

  • ここで言うサーバ基準(過去)の時刻に実行されたものとして扱うという言葉は、
    モーションの途中再生を行うなどの表示上のものを指しています。
    これは見た目上不整合が起こることを意味していますが、自分のキャラクタはともかく、
    そもそも他の人のキャラクタをそこまで凝視することは少ないのではないでしょうか。
    そう考えるとこの補間処理は他の人のキャラクタに限定すると問題ないと私は考えています。

これなら各クライアント上で実行開始された(とされる)時刻が同じであるため、
各端末上でのずれはそれほど発生しないでしょう。
しかし、クライアントA上では依然としてユーザの入力からの遅延が発生しており、ストレスが発生する可能性をはらんでいます。

メリット

  • サーバでコマンドを受け取った時間を正としているため、実行タイミングの改ざんを行いにくい
  • 各端末間でずれが生じにくい(ただし、多少のラグは発生する)
  • サーバ上では完全に同期されているため、サーバでの処理が書きやすい

デメリット

  • 時刻を同期する必要がある
  • ユーザレスポンスが若干悪い
    • 時刻を返さない場合と比べると、途中から再生される分ユーザレスポンスが多少改善はされている
    • 自分の操作キャラクタが動作を途中から行っているため、見た目が悪い

コマンドを実行したクライアント上では即時実行し、サーバがコマンドの受け取り時刻を加えて他のクライアントへ返した後、実行する

次のフローをご覧ください。

case4

クライアントA上ではコマンドの入力後、すぐに実行しています。
これにより先ほど発生していた入力に対する遅延は起こらなくなっています。
また、クライアントB上ではサーバで受け取った時刻を元に実行しており、
(補間処理などは行いつつ)クライアントAとクライアントBとのずれは1秒で収まっています。

多くの要件はこの構成で十分なのではないでしょうか。
しかし、逆を言えば1秒もずれているとも言えます。
タイミングが重要な要件であればこのズレが致命的になる可能性があります。

メリット

  • ユーザレスポンスが良い
  • クライアントAを除く、サーバを含めたすべての端末でずれが生じにくい(ただし、多少のラグは発生する)

デメリット

  • 時刻を同期する必要がある
  • クライアントAとサーバを含めた他の端末とでずれが生じる

コマンドを実行したクライアントでコマンドとクライアント基準の実行時刻を送り、サーバがコマンドを他のクライアントへ返した後実行する

次のフローをご覧ください。

case5

クライアントAでは自分の時刻を元に実行時刻を決め、コマンドと共に送っています。
サーバではそれを仲介するだけにし、クライアントBではクライアントA上の実行時刻を元に補間し、実行します。
その結果、クライアントAとサーバ、クライアントBでは実行開始されたタイミングまで同期できました。
しかし、クライアントの自己申告による実行時間を信用するということは、改ざんに対して弱くなることと同義であるため、
悪意のあるクライアントからの通信に対応する必要が発生します。

メリット

  • ユーザレスポンスが良い
  • 各クライアントとサーバとでずれが生じにくい(ただし、多少のラグは発生する)

デメリット

  • 時刻を同期する必要がある
  • クライアントからの通信改ざんに対して脆弱になる

まとめ

以下に今回挙げた項目についてサマリを作ってみました。
個々の方法について比較する一助になれば幸いです。

方法 メリット デメリット 用途例
基本形 構造が単純
コーディングが簡単
各端末でタイミングがずれる タイミングが重要でないもの
例:チャットの吹き出し
サーバがコマンドを受け取ったことを返した後、実行する 構造が単純
コーディングが簡単
サーバ基準で完全に同期している
各端末でタイミングがずれる
ユーザレスポンスが悪い
タイミングが重要でないもの
サーバ側で何らかの判定を行う必要があるもの
例:アイテムの使用
サーバがコマンドの受け取り時刻を加えて返した後、実行する 各端末でずれが生じにくい
サーバ基準で完全に同期している
実行タイミングの改ざんを行いにくい
ユーザレスポンスが悪い
操作キャラクタを含めた見た目が悪い
サーバ側で何らかの判定を行う必要があるもの
例:コマンド方式の場合のスキルの先行入力
コマンドを実行したクライアント上では即時実行し、
サーバがコマンドの受け取り時刻を加えて他のクライアントへ返した後、実行する
実行端末以外の各端末でずれが生じにくい
ユーザレスポンスが良い
実行端末とそれ以外の端末でずれが生じる サーバ側で何らかの判定を行う必要があるもの
ユーザの操作に対するレスポンスが早いほうがいいもの
例:エモーション
コマンドを実行したクライアントでコマンドとクライアント基準の実行時刻を送り
、サーバがコマンドを他のクライアントへ返した後実行する
各端末でずれが生じにくい
ユーザレスポンスが良い
クライアントからの通信改ざんに対して脆弱になる ユーザの操作に対するレスポンスが早い必要があるもの
通信が改ざんされても影響が少ないか異常値をフィルタできるもの
例:コマンド入力方式でない場合の移動処理

これらの手法はどれも一長一短であり、何度も言いますが要件によって最適な選択肢が違います
また、行う通信によってそれぞれの手法を選択するということも有力な候補になるでしょう。
(例えば、移動はクライアントから実行時刻を決めて、攻撃はサーバが実行時刻を決めるなど)

更に今回は割愛しましたが、攻撃対象が死亡しているなど、コマンドが実行できないとサーバが判断した際に
どのようにキャンセルを行うかということも含めて考えていく必要があります。
これらの手法を元に、更に効率のよい方法を日々模索していきたいと思います。

予告

明日は hhatto さんです。お楽しみに。

このブログについて

KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。

関連記事

このブログについて

KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。