フレーム同期の最適化における難点及びソリューション

フレーム同期という部分はやや複雑なので、細部にも多くの最適化ポイントがあり、いくつかの異なる最適化の方向もあります。プロジェクトの種類、操作感への要求、オンラインプレーヤーの数などに応じて、さまざまな問題が発生します。最適化の方向性や最適化手法の違いにより、異論が生じるかもしれません。さらに、フレーム同期自体には、さまざまなニーズを満たすための多くのバリエーションがあります。したがって、この記事のすべては、作成者のプロジェクトタイプ(ACT)に基づいて解決策と最適化を行うため、フレーム同期を必要とする他のゲームには必ずしも適していません。余計な誤解を引き起こさないように事前にそれを知っておきたいのです。


フレーム同期のいくつかの難点

フレーム同期の基礎的な原理及び状態同期との違いについての記事はたくさん載っており、グーグルで簡単に検索できるため、ここではいくつかの難点だけを説明します。


クライアント独自計算の正しさを保つ、即ち一致性。

フレーム同期の基礎は、異なるクライアントが、同じ操作コマンドの順序によって、それぞれロジックを実行して同じ効果を得ることができるということです。皆が知っているように、Unityエンジンでは、異なるコールの順、タイミング、浮動小数点計算の偏差、コンテナーソートの不確実性、Coroutineでのロジックの記述によって引き起こされる不確実性、物理浮動小数点数、ランダム値による不確実性など。

ランダム値など、ランダムシードを作成することだけで簡単に解決できるものがあります。

コード仕様に注意を払う必要があるものもあります。たとえば、フレーム同期のバトルでは、ロジック部分はCoroutineを使用せず、Dictionaryなどの順序が不確実なコンテナのサイクルに依存しません。

さらに一番基本的なことは、各ロジックを単独でUpdateするのではなく、統合されたロジックのTickエントリを介して、バトルロジック全体を更新することです。各Tickが上から下まで、実行の順序が毎回同じであることを確認してください。

物理に関しては、バトルロジックに必要がないため、衝突は自らで衝突ロジックを作成するため、これを飛ばして説明を進めます。

最後に、浮動小数点数の計算では一致性を保証できないため、固定小数点数に変換する必要があります。固定小数点数の実現に関しては、元の浮動小数点数に上に1000または10000を乗算し、対応する場所を1000または10000で除算するのが最も簡単な方法です。また三角関数でテーブルを検索して、いくつかの問題を解決できて不一致の確率を減らせます。しかし、この根本から解決できない方法は、いくつかのおそれが潜んでいます(たとえば、intとfloatを乗算した場合、元の値が* 1000の場合、最終的に計算された値は非常に大きくなって、境を越えるリスクがあります。)

最善の解決策は:より正確で厳密な固定小数点数の数学ライブラリを使用することです。C#には、固定小数点数の実装があります。Photonネットワークの初期バージョンでは、Truesyncに非常に優れた固定小数点数の実装があります。

固定小数点数の実現

その中に、FPはFloatを完全に置き換えることができ、独自のロジック部分であるFloatなどをFPに変換するだけで、簡単に解決できます。さらに、Protobufシリアル化メソッド(下の図のように、コード内のAttributeに注意)とうまく統合して、配置ファイルも固定小数点になるようにすることができます。

TSVectorはVector3に対応し、FPに基づいている限り、独自のデータ構造を拡張できます。(もちろん、複雑なプラグインが使用されていて、それらがオープンソースでない場合、固定小数点数の変換ははるかに難しくなります)

三角関数はテーブルを検索する方法で実装されるため、固定小数点数の正確さが確保される

個人的には、この方法は、単純な10,000で乗算する方法、10,000で除算する方ほより優れていると思います。不利な点は、計算パフォーマンスが少し悪いことかもしれませんが、多くのテストの後、計算パフォーマンスへの影響は非常に小さく、ほとんどのプロジェクトのニーズを満たすことができるはずです。

計算の不確実性に対しても、いくつかの恐れもあります。Physics.Raycastを使用して地面と壁を検出して、キャラクターに坂を上させたり下らせたりして、階段などのでこぼこの道で歩かせて、形が規則していない壁でもあります。ここでは、不確実性が生じる可能性のある浮動小数点数の位置を取得します。ここでは、可能な限り不確実性を避けるために、数値の切り捨てなどの方法を使用しました。繰り返しテストを行った後、不一致は起こされませんでした。しかし、結局のところ、ロジックの側にこの方法はリスクがあります。より良い方法は、固定小数点数に基づいた一連のraycastメカニズムを実装することです。人員の限り、それを暫時的にしません。他の詳しい記事がありますので、ぜひ参照してください。

フレーム同期:浮動小数点精度テスト(中国語注意)https://zhuanlan.zhihu.com/p/30422277


フレーム同期ネットワークプロトコルの実現

計算の一致性の問題を解決した後、ネットワークがどのように通信するかを検討する必要があります。ここでは、p2pメソッドについては説明しません。説明したいのは、複数のclientと1つのserverのモードです。サーバーは、tickを統合し、clientの命令を転送してから他のclientへ通知することを担当します。以下の記事を参照してください。

オンラインゲームのスムーズな基盤:フレーム同期ゲームの開発

http://www.10tiao.com/html/255/201609/2650586281/4.html

まず、ネットワークプロトコルの選択について話しましょう。TCPかUDPかについては、言うまでもなくUDPを選択するに違いありません。遅延を短縮できますから。UDPの選択については、インターネットでの記事を読んだことがありますが、信頼性の高い送信に基づくUDPを使用するのか、冗長な情報に基づくUDPを使用するのかという誤解が生じやすいです。

信頼性の高い送信に基づくUDPとは、UDPの上にカプセル化を加え、自らでパケット損失処理をし、メッセージシーケンス、再送信などを実装するTCPに似ているメッセージ処理方法です。従って、上位層ロジックがデータパケットを処理するときに、パケットのじょんを考慮する必要がないようにします。パケット損失など。類似な実装は、Enet、KCPなどがあります。

冗長情報UDPとは、上位層のロジックが自らパケット損失、順不同、再送信などを処理する必要があり、下位層が元のUDPを直接使用するか、Enetの Unsequencedのようなモードを使用するかのことを指します。一般的な処理方法は、両端のメッセージにフレームの確認情報が含まれていることです。たとえば、クライアント(C)がサーバー(S)に100フレームのデータを通知し、Sがデータを受信した後にCに「100フレームを受け取りました」と通知します。その知らせはずっとCに届かない場合(パケット損失、順不同などの原因で)、確認メッセージを受信するまで、100フレームのデータをSに送信し続けます。

この両者の相違を明確にしない記事はいくつかありますが、実に違いが大きいと言えます。個人的には、信頼性の高い送信のUDPはフレーム同期に相応しくないと思います。何故ならば、パケットの順序を確保し、パケットの損失や再送信などに対処するために、ネットワークが貧弱な場合、Delayが非常に大きくなるためです。これにより、パケットの送受信を、より良いと言えども、やはりTCPのように処理します。だから、良い効果を得るために、冗長情報のUDPを使用しなければなりません。それに、フレーム確認と再送信方法をサーバーとネゴシエートする限り、実装はそれほど複雑ではありません。自分で実装したら、最適化の余地がたくさんあります。たとえば、プロトコルの定義は次のようになります。 

双方も、お互いにどのフレームまでに受け取ったのかを通知し、またcmdlistを介して受信されなかったコマンドを再送信する。

このような頻繁に送受信されるメッセージに対し、Protobufを使用すると、論理フレームごとにGCが発生します。これは非常に悪いことです。解決策は、ProtobufにGCなしの変換をするか、自分で簡単なbyteの読み取り・書き込みを実装します。実装することです。GCなしの変換は重過ぎて必要が無いと感じます。戦闘中に頻繁に送信されるメッセージはごくわずかであり、byte の読み取り・書き込みを自分で処理するだけで十分です。

また、KCPの作者であるWeiYixiaoさんは、KCP + FECのモードは、冗長モードよりも優れた結果が得られるかもしれないと述べています。まだ深く調べたことがありませんが、一応皆さんに推します。

プロジェクトの初期に、サーバーはEnetを使用することを決定し、どうせ冗長パッケージを使用したので、EnetとKCPを考えませんでした。後はKCPに変更しようと思って、サーバーは変更したくなかったので、そのままになっています。

Enetの問題は、Enetのipv6バージョンが未成熟なPull Requestであるということです。Enetの作成者はMergeをしません(それにいくつかのipv6のPull Requestを保存しています)。安定性について確認できませんから、いくつかのCommitを確認しました。またテストして来て、大きな問題はありません。 KCP ipv6の問題をまだ評価していませんが、GithubにC#バージョンがあルため、ipv6サポートを簡単に変更できるはずです。


ロジックと表示の分離

この部分は、フレーム同期に関する多くの記事で言及されています。配置されたデータはディスプレイから分離するなら、戦闘では、戦闘のロジックもディスプレイから分離する必要があります。

たとえば、アクションスイッチングのロジックは、AnimatorでのClipの再生ではなく、独自の抽象的な論理フレームに基づいています。たとえば、攻撃アクションの場合、10フレームが表示され始め、攻撃ブロックが出現します。さらに敵の受撃ブロックとの衝突を検出し始めます。このときの10フレームは独立したロジックでなけれならなく、Animatorの再生時間またはAnimatorStateInfoのNormalizedTimeに依存することはできません。さらに、キャラクターのモデルをロードしなくても、バトルのロジックを実行できます。十分に分離されている限り、戦闘の検証プログラムとしてサーバー上で実行することもできます。「Honor of Kings」もこの方法でします。


オンラインでスムーズな戦いを実現する方法

これまでのすべての準備、最終的な目標は、戦闘をスムーズにすることです。特に、ACTゲームや格闘ゲームでは、ボタンを押した直後に操作をフィードバックする必要があり、少し遅れるとプレイヤーの操作感に影響を与え、コンボ操作が中断され、体験に悪影響を与えます。遅延に対する感度はMOBAゲームよりもさらに高く、優れた操作感とオンライン戦闘(PVP、チームPVE)を実現するために、遅延でフリーズしたまたは操作フィードバックで変化が起こったことを避け、究極のフレーム同期を実現する必要があります。

このため、Lockstepの方法は使用できません。Lockstepは、ネットワーク環境が良好なイントラネットや、操作遅延の影響を受けにくいタイプに適しています(カードゲームのフレーム同期に使用されるプロジェクトがあると聞いたことがあります)。

一部のゲームで作成された命令Bufferである操作をキャッシュサーバーで確認することはできません。「Honor of Kings」の分析記事での説明は、非常に具体的です。これはモードと呼ばれるものでもあります。このモードは、ネットワークの小さな変動や、操作に対してあまり高いフィードバックを必要としないゲームを解決できます。たとえば、攻撃する前に比較的長い前進スイングがあるゲームもあります。このタイプのゲームはこれを使用して、ほとんどの問題を解決できるはずです。ただし、この方法には依然としてリスクがあります。戦略を通じてBufferを動的に調整できたとしても、高遅延でフリーズしたことやスムーズでない問題を依然として解決し難いです。「Honor of Kings」はうまく最適化されました。Bufferの長さは0にすることができると言われました。この記事では、スムーズな補間と論理的な表示の分離による最適化についてのみ言及していて、詳細については触れませんでした。この方法でしか最適化するかどうかはまだ不明でしたが、これ以上の具体的な分析はまだないそうです。

Bufferを命令する方法も私たちのニーズを満たすことができません。言い換えれば、この方法で最適化して「Honor of Kings」のような効果が得られる方法を見つけていません。他のMOBA、ACT、ARPGゲームのテザリングもテストしましたが、高遅延、ネットワークの変動が大きい場合、「Honor of Kings」よりも優れたパフォーマンスはありません。

最後に、私たちのニーズを注意深く研究した後、自分に非常に適した有益な記事を見つけました:

Understanding Fighting Game Networking

http://mauve.mizuumi.net/2012/07/05/understanding-fighting-game-networking/

この記事では、さまざまな方法を詳細に紹介します。最終的なロールバックロジック(Rollback)は、究極のソリューションです。国内の記事でも言及されています。

http://www.skywind.me/blog/archives/1343#more-1343

記事の中にTime Warpと呼ばれる方法は、私的理解では、ロールバックロジックと同じようなコンセプトです。


ゲームロジックのロールバック

ロールバックロジックは、即ち私たちの解決策です。クライアントの時間がサーバーより進んでいると理解できます。クライアントは、サーバーがフレームの戻りを確認してからコマンドを実行する必要はありません。代わりに、プレーヤーはそれを入力してすぐに実行してから(他のプレーヤーの入力は最新の入力に基づいて予測、または他のより最適化された予測案)、サーバーにその命令を送信します。サーバーが受け取って確認してから、前の実行と同じようなら(自分や他のプレイヤーによって予測された操作)、何も変更しません。一致していないなら(予測エラー)、ゲームの全体ロジックは最後のサーバーによって確認された正しいフレームにロールバックされ、それから現在のクライアントのフレームに追いつきます。

ここでのロジックはちょっと複雑なので、例を挙げて説明します。

現在のクライアント(A、B)は100フレームまで実行され、サーバーは97フレームまで実行されます。フレーム100で、Aが移動を実行し、Bが攻撃を実行しました。AとBの両方がサーバーに通知しました:100フレームまでに実行しました。操作は移動(A)・攻撃(B)です。サーバーは、自分のフレーム98または99でA・Bのメッセージを受信し、対応するフレームの操作データに格納されます。サーバーが100フレームまで(または事前に)実行すると、このデータをABにブロードキャストします。

次に、AとBはすぐに100フレームの実行を開始し、Aは移動を実行し、Bが操作を実行しないと予測します。 Bが攻撃を実行し、Aが攻撃を実行すると予測します(おそらくAの99フレームも攻撃です)、AとBはそれぞれ互いの操作を予測します。

予測エラーが発生して、ロジックロールバックを実行することを防ぐために、AとBが100フレームを実行した後、それぞれ100フレームの状態スナップショットとそれぞれの操作(予測操作を含む)を保存します。

数フレームを実行した後、AとBはフレーム103に到達し、サーバーはフレーム100に到達し、ABにデータをブロードキャストし始めました。一定の遅延の後、ABはサーバーによって確認された100フレームのデータを受信しました。その時、AB は既に104まで実行しました。AとBはそれぞれ、サーバーのデータが自分で予測したデータと同じであるかどうかを確認します。たとえば、Aは100フレームをチェックした後、その動作は自分の予測と同じく、何の処理も行わずに前進し続けます。それに対し、Bが100フレームをチェックした後、Aに対する予測はサーバーによって確認されたAの操作とは異なり(Bは攻撃を予測したが、実際のAの操作は移動でした)、Bは前の確認が同じフレーム(即ち99フレーム)にロールバックし、確認した100フレームの動作に従って100フレームを実行し、そして101〜103のフレームロジックをすばやく実行してから、104フレームを実行します。(101〜 104)は以前として予測されたロジックフレームです。

クライアントは現在の操作をすぐに実行するため、操作感はPVE(ネットワークに接続されていない)とまったく同じであり、Delayはありません。そのため、優れた操作感が得られます。予測が異なる場合は、論理ロールバックを実行して、現在の操作をすばやく追い戻ります。

このように、ネットワークの良いプレイヤーとネットワークの悪いプレイヤーはお互いに影響を与えません。Lockstepのように、ネットワークの良いプレイヤーはネットワークの悪いプレイヤーによってLockされます。ネットワーク遅延によってロックされることもなく、クライアントは常に前方を予測できます。

ネットワークが良いプレーヤー(A)の場合、(動的Latencyに従って)動的に調整して、クライアントをサーバーより少しの先に進めさせ、予測の量を最小限に抑え、ロールバックを最小限に抑えることができます。たとえば、ネットワークが優れている場合、クライアントは2〜3フレームだけ進んでいる可能性があります。

ネットワークが不良なプレーヤー(B)の場合は、Latencyに従ってに従って動的に調整して、サーバーより遠くの先に進めさせます(例、5フレームを優先的に行う)。

それでは、Aの予測エラーは2〜3フレームのみに残っており、ネットワークが不良なBは、5つのフレームがある可能性があります。最適化された予測技術とメッセージ通知の最適化により、AとBの予測エラー率をさらに減らすことができます。Aの場合、戦闘はスムーズで、手触りもとても良いです。少数のロールバックが最適化されており、フリーズや遅延感が起こされません。

最適化のポイントは、ネットワークが悪いプレーヤーBとその方の操作体験です。クライアントはサーバーの確認を待たずに操作を行うため、Bの操作感はAと同じですが、遅延によりBが予測するフレーム数が多くなり、予測エラーやロールバックが増える可能性があります。Bの予測によれば、100フレームでAをヒットするはずですが、Aの操作を予測し間違えるため、ロールバックが再度実行された後、Bは100フレームでAにヒットしない場合があります。 Bにとって、補間といくつかの平滑化方法を通して、Bの感じたことは大きいな区別がありません。Bは自分自身を見て、操作はタイムリーにフィードバックされ、自分側の操作はスムーズだと感じます。

このように、ネットワークの悪いBの操作感はAと同じであることが保証されます。ロールバックによって引き起こされるわずかなジッターは、Aのジッターだと思われます。最適化(補間、平滑化など)を通じて、これらをさらに減らした後、Bの操作感はとても良くなります。 200〜300ミリ秒のランダムな遅延で、Bの操作感が良好であることをテストしました。

ここで、クライアントはサーバーを前倒し、遅延が増加している場合、高速化されます。「Overwatch」はそれを同じように扱います。

https://mp.weixin.qq.com/s/cOGn8-rHWLIxdDz-R3pXDg

ここでは、一つだけを強調したいのです。予測実行はフレーム同期に関する多くの記事で言及されている予測とは異な理、実際のロジックの予測です。一部の記事で紹介されている予測実行は、前進運動や変位などのViewレベルでの予測だけで、ロジックは事前に実行されず、サーバーは復帰を待つ必要があります。これらの2種類の予測実行(Viewの予測実行と実際のロジック予測実行)はまったく同じ概念ではないため、ここでは注意深く区別する必要があります。


ゲームロジックのスナップショット(snapshot)

ロジックがロールバックできる理由は、各フレームの状態はスナップショットを処理し、各フレームの状態を保存し、任意のフレーム状態にロールバックする機能に基づいています。

Understanding Fighting Game Networking:

http://mauve.mizuumi.net/2012/07/05/understanding-fighting-game-networking/

「Overwatch」:

https://mp.weixin.qq.com/s/cOGn8-rHWLIxdDz-R3pXDg

上記の2つの記事では、スナップショットの説明を触れました。やや違っているかもしれませんが、各フレームの状態を保存するというアイデアは同じです。スナップショットを扱う場合(『Understanding』の記事はエミュレーターゲームで、メモリスナップショットの形で簡単に行うことができます)、それは難点と言えます。

次はこの前の記事で、ECSの適用について述べました。

https://zhuanlan.zhihu.com/p/38280972

https://blog.codingnow.com/2017/06/overwatch_ecs.html

ECSは良い処理法で、以下の記事にオープンソースのdemoをしましたが、まだ成熟したプロジェクトと言えません。

https://www.kisence.com/2017/11/12/guan-yu-zheng-tong-bu-de-xie-xin-de/

この記事のアイデアは非常に明確であり、いくつかの実際の問題点も指摘されており、解決策のアイデアは基本的に正しいです。参照してください。

以前にやったのですが、当時は「Overwatch」の記事が公開されておらず、ECSに基づく戦いでもありませんでした。スナップショットを扱うときは、自分のロジックに従ってやらなければなりませんでした。

私の考えでは、ロールバックインターフェイスを介して、データのロールバックが必要な部分にインターフェイスを実装し、それぞれが独自の保存スナップショットとロールバックを処理します。複雑な配置をシリアル化するのと同じように、各配置は独自の部分をシリアル化し、最終的にシリアル化されたファイルにマージします。

まず、インターフェイス、およびスナップショットデータのReaderとWriterを定義します

次に、各モジュールは独自のTakeSnapshotとRollbackを単独で処理します。以下のように例を示します。

単純な数値ロールバック

リストのロールバックのコピーとサブモジュールのロールバックの呼び出し

アイデアが正しければ、簡単に処理できます。WriteとReadの順序に気を付け、リストの処理に注意を払うと、ほとんどの問題が解決されます。当然ながら、ロジックを実装するプロセスでは、モジュールがどのようにロールバックされるかに常に注意を払ってください(たとえば、乱数の取得もロールバックする必要があります)。

より簡単な方法があります。それは、属性にAttributeと入力してから、一般的なメソッドを作成することです。たとえば、私の初期の実装計画は次のとおりです。

属性にタグする

ラベルによると、リフレクションによる一般的な読み取りと書き込みの方法を通して、各モジュールが独自の方法を実装する必要はありません。

コードの一部

この方法はほとんどの問題をうまく解決でき、前述のTruesyncにも適用します。

しかし、このメソッドには避けられない問題、つまりGCがあります。これは、リフレクションに基づいて、fieldのGetValueとSetValueを呼び出すときに、GCが避けられないためです。また、全自動であるため、特殊なロジックの処理が不便であり、最適化をデバッグするのも不便です。最後に、現在の方法に変更しました。少し面倒に見えますが、より制御しやすくなっています。その後の多くの最適化も便利になっています。

スナップショットに関しては、最適化できる点も多くあります。GCメモリの方でも実行効率の方でも最適化する必要があります。そうしないと、パフォーマンスの問題が発生する可能性があります。この最適化については、時間があれば別の記事で説明しましょう。

スナップショットがあれば、ロールバックやスキップもサポートできます。たとえば、戦闘ビデオを見たい場合、スナップショットがないと1000フレームにスキップし、保存された操作手順に従って最初のフレームから1000フレームまですばやく実行する必要があります。スナップショットを使用すると、 1000フレームに直接ジャンプできます。中間プロセスを実行する必要はありません。異なるフレームを切り替える必要がある場合は、スキップするだけで済みます。これは非常に役立ちます。


自動テスト

フレーム同期には一致性をテストする必要があるため、私たちにとって、ロールバックも多くのテストを必要となります。自動テストは必要なステップです。これについて、操作、スナップショット、logを保存してから、さまざまなクライアントのデータを比較し、さまざまな場所を見つけて、エラーをチェックして修正することです。

現在は、ワンステップ操作、自動ループバトル、各バトルデータをイントラネットログサーバーにアップロードします。

戦闘データが多い場合、ツールで自動的にデータを解析して比較して、同期していないポイントを見つけます。さらに良く最適化することもできますが、今では十分に感じられます。多数の内部自動テストの後、現在の戦闘の一貫性は非常に良好です。


まとめ

要約すると、現在のフレーム同期方法は、予測、スナップショット、およびロールバックです。これらを有機的に組み合わせて最適化すると、非常に優れたフレーム同期ネットワーク効果が得られます。ネットワーク速度に関係なく、遅延が異常すぎない限り、非常に優れた操作感が保証されます。

スナップショットのロールバック方法は、すべてのゲームに適用できるわけではありません。例えば、

Skywind Insideは、オンラインゲーム同期の技術記事でこのモードの欠点(Time warpまたはRollback)について説明しています。

http://www.skywind.me/blog/archives/1343#more-1343

記事に示すように、このモードは、多数のプレーヤーがあるオンラインゲームに適応しません。例えば、MOBAに適していないかもしれません。現在、最大で3人がオンラインで、最適化やテストして、効果上の問題はありません。ただし、オンラインのユーザーが多いほど、予測操作でエラーが発生する可能性が高くなり、ロールバックが多くなります。

一つの記事で、すべてを網羅することは難しく、多くの場所が明確に説明されていない可能性があります。さらに、個人的な能力とチームメンバー(3人のクライアント)が限られているため、設計と実装が十分でない部分がたくさんあるに違い有りません。ご了承願います。


役に立つ参考記事:

1、Understanding Fighting Game Networking
http://mauve.mizuumi.net/2012/07/05/understanding-fighting-game-networking/

2、
http://www.skywind.me/blog/archives/1343#more-1343

3、
https://mp.weixin.qq.com/s/cOGn8-rHWLIxdDz-R3pXDg

4、
http://youxiputao.com/articles/11842

5、
https://zhuanlan.zhihu.com/p/30422277

6、A guide to understanding netcode
https://www.gamereplays.org/overwatch/portals.php?show=page&name=overwatch-a-guide-to-understanding-netcode

7、
http://www.10tiao.com/html/255/201609/2650586281/4.html

最後に、最初に述べたように、フレームの同期にはたくさんの変種があって、実装方法や最適化の方向にもたくさんある為、記事には同じくフレーム同期と呼んでも、区別しています。ぜひ詳しく区別して理解してください。


UWA公式サイト:https://jp.uwa4d.com

UWA公式ブログ:https://blog.jp.uwa4d.com

UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com