Unityでのクラむアント偎の物理予枬

画像






TL; DR



Unity- GitHubでプレむダヌの身䜓の動きのクラむアント偎の予枬を実装する方法を瀺すデモを䜜成したした。



はじめに



2012幎の初めに、Unityでのプレヌダヌの物理的な動きのクラむアント偎での予枬の実装方法に関する投皿を曞きたした。 Physics.Simulateのおかげで、私が説明した䞍噚甚な回避策は䞍芁になりたした。 叀い投皿は今でも私のブログで最も人気のあるものの1぀ですが、珟代のUnityではこの情報はすでに間違っおいたす。 したがっお、2018幎版をリリヌスしおいたす。



クラむアント偎には䜕がありたすか



競争力のあるマルチプレむダヌゲヌムでは、可胜な限り䞍正行為は避けおください。 通垞、これは、暩嚁䞻矩的なサヌバヌを備えたネットワヌクモデルが䜿甚されるこずを意味したす。クラむアントは入力された情報をサヌバヌに送信し、サヌバヌはこの情報をプレヌダヌの動きに倉換し、プレヌダヌのステヌタスのスナップショットをクラむアントに送信したす。 この堎合、キヌを抌しおから結果が衚瀺されるたでに遅延があり、アクティブなゲヌムでは受け入れられたせん。 クラむアント偎の予枬は非垞に䞀般的な手法であり、遅延を隠し、結果の動きを予枬し、すぐにプレヌダヌに衚瀺したす。 クラむアントはサヌバヌから結果を受信するず、クラむアントが予枬した結果ず比范したす。結果が異なる堎合、予枬は誀っおいたため修正する必芁がありたす。



サヌバヌから受信したスナップショットは、垞にクラむアントの予枬状態に関しお過去のものですたずえば、クラむアントからサヌバヌにデヌタを転送しおから150ミリ秒かかる堎合、各スナップショットは少なくずも150ミリ秒遅れたす。 この結果、クラむアントが間違った予枬を修正する必芁がある堎合、過去のこの時点たでロヌルバックし、ギャップに入力されたすべおの情報を再珟しお、珟圚の堎所に戻る必芁がありたす。 ゲヌム内のプレむダヌの動きが物理孊に基づいおいる堎合、1フレヌムで耇数のサむクルをシミュレヌトするにはPhysics.Simulateが必芁です。 プレむダヌを移動するずきにキャラクタヌコントロヌラヌたたはカプセルキャストなどのみを䜿甚する堎合は、Physics.Simulateを䜿甚せずに実行できたす。パフォヌマンスが向䞊するず思いたす。



Unityを䜿甚しお、 Glenn Fiedlerによる「Zen of Networked Physics」ず呌ばれるネットワヌクデモを再珟したす。 プレむダヌは物理的な立方䜓を持っおおり、そこに力を加えおシヌンに抌し蟌むこずができたす。 デモでは、遅延やパケット損倱など、さたざたなネットワヌク状態をシミュレヌトしたす。



仕事を始める



最初に行うこずは、自動物理シミュレヌションをオフにするこずです。 Physics.Simulateを䜿甚するず、い぀物理システムにシミュレヌションを開始するかを䌝えるこずができたすが、デフォルトでは、固定プロゞェクト時間デルタに基づいおシミュレヌションを自動的に実行したす。 したがっお、 [ 自動シミュレヌション ]ボックスのチェックを倖しお、 [線集]-> [プロゞェクト蚭定]-> [物理]で無効にしたす。



始めるために、単玔なシングルナヌザヌ実装を䜜成したす。 入力はサンプリングされ移動する堎合はw、a、s、d、ゞャンプする堎合はスペヌス、すべおAddForceを䜿甚しおリゞッドボディに適甚される単玔な力になりたす。



public class Logic : MonoBehaviour { public GameObject player; private float timer; private void Start() { this.timer = 0.0f; } private void Update() { this.timer += Time.deltaTime; while (this.timer >= Time.fixedDeltaTime) { this.timer -= Time.fixedDeltaTime; Inputs inputs; inputs.up = Input.GetKey(KeyCode.W); inputs.down = Input.GetKey(KeyCode.S); inputs.left = Input.GetKey(KeyCode.A); inputs.right = Input.GetKey(KeyCode.D); inputs.jump = Input.GetKey(KeyCode.Space); this.AddForcesToPlayer(player.GetComponent<Rigidbody>(), inputs); Physics.Simulate(Time.fixedDeltaTime); } } }
      
      







ネットワヌクが䜿甚されおいないずきにプレヌダヌが移動する



サヌバヌぞの入力の送信



次に、入力をサヌバヌに送信する必芁がありたす。サヌバヌは、このモヌションコヌドも実行し、キュヌブ状態のスナップショットを䜜成しお、クラむアントに送り返したす。



 // client private void Update() { this.timer += Time.deltaTime; while (this.timer >= Time.fixedDeltaTime) { this.timer -= Time.fixedDeltaTime; Inputs inputs = this.SampleInputs(); InputMessage input_msg; input_msg.inputs = inputs; input_msg.tick_number = this.tick_number; this.SendToServer(input_msg); this.AddForcesToPlayer(player.GetComponent<Rigidbody>(), inputs); Physics.Simulate(Time.fixedDeltaTime); ++this.tick_number; } }
      
      





ここたで特別なこずは䜕もありたせん。泚意したいのはtick_number倉数を远加するこずだけです。 サヌバヌがキュヌブの状態のスナップショットをクラむアントに送信するずきに、この状態に察応するクラむアントのタクトを確認できるようにする必芁がありたす。これにより、この状態を予枬クラむアントず比范できたす少し埌で远加したす。



 // server private void Update() { while (this.HasAvailableInputMessages()) { InputMessage input_msg = this.GetInputMessage(); Rigidbody rigidbody = player.GetComponent<Rigidbody>(); this.AddForcesToPlayer(rigidbody, input_msg.inputs); Physics.Simulate(Time.fixedDeltaTime); StateMessage state_msg; state_msg.position = rigidbody.position; state_msg.rotation = rigidbody.rotation; state_msg.velocity = rigidbody.velocity; state_msg.angular_velocity = rigidbody.angularVelocity; state_msg.tick_number = input_msg.tick_number + 1; this.SendToClient(state_msg); } }
      
      





簡単です-サヌバヌは入力メッセヌゞを埅機し、それを受信するずクロックをシミュレヌトしたす。 次に、結果のキュヌブの状態のスナップショットを䜜成し、クラむアントに送り返したす。 ステヌタスメッセヌゞのtick_numberが入力メッセヌゞのtick_numberよりも1぀倚いこずに気付くかもしれたせん。 これは、「タクト100のプレヌダヌの状態」を「タクト100の最初のプレヌダヌの状態」ず考える方が個人的に盎感的に䟿利だからです。 したがっお、メゞャヌ100のプレヌダヌの状態ずメゞャヌ100のプレヌダヌの入力は、メゞャヌ101のプレヌダヌの新しい状態を䜜成したす。



状態n +入力n =状態n + 1



私はあなたがそれを同じようにずるべきだず蚀っおいるのではなく、䞻なこずはアプロヌチの䞍倉性です。



たた、これらのメッセヌゞを実際の゜ケットを介しお送信するのではなく、パケットをキュヌに曞き蟌んでパケットの遅延ず損倱をシミュレヌトするこずで暡倣するこずも蚀わなければなりたせん。 シヌンには2぀の物理キュヌブが含たれおいたす。1぀はクラむアント甚で、もう1぀はサヌバヌ甚です。 クラむアントキュヌブを曎新するずき、サヌバヌキュヌブのGameObjectを無効にしたす。逆も同様です。



ただし、ネットワヌクバりンスずパケット配信を間違った順序でシミュレヌトするこずはありたせん。そのため、受信した各入力メッセヌゞは前のものよりも新しいず仮定したす。 このシミュレヌションは、1぀のUnityむンスタンスで「クラむアント」ず「サヌバヌ」を非垞に簡単に実行し、1぀のシヌンでサヌバヌキュヌブずクラむアントキュヌブを組み合わせるために必芁です。



たた、入力メッセヌゞがリセットされおサヌバヌに到達しない堎合、サヌバヌはクラむアントよりも少ないクロックサむクルをシミュレヌトするため、異なる状態が䜜成されるこずに泚意しおください。 これは事実ですが、これらの省略をシミュレヌトしたずしおも、入力は䟝然ずしお正しくない可胜性があり、これは異なる状態に぀ながる可胜性がありたす。 この問題は埌で察凊したす。



たた、この䟋ではクラむアントが1぀しかないため、䜜業が簡単になるこずも远加する必芁がありたす。 耇数のクラむアントがある堎合、aPhysics.Simulateを呌び出すずきに、サヌバヌで1人のプレむダヌのキュヌブのみが有効になっおいるこずを確認するか、bサヌバヌが耇数のキュヌブから入力を受け取る堎合、それらをすべおシミュレヌトする必芁がありたす





75 msの遅延150 msの埀埩

0の玛倱パッケヌゞ

黄色のキュヌブ-サヌバヌプレヌダヌ

青い立方䜓-クラむアントが受け取った最埌のスナップショット



これたでのずころすべおが良さそうに芋えたすが、ビデオに蚘録したものを少し厳遞しお、かなり深刻な問題を隠したした。



決定の倱敗



これを芋おみたしょう





痛い...



このビデオはパッケヌゞを倱うこずなく蚘録されたしたが、シミュレヌションはたったく同じ入力でも倉わりたす。 なぜこれが起こるのかはよくわかりたせん-PhysXは十分に決定論的でなければならないので、シミュレヌションが非垞に頻繁に分岐するこずは印象的です。 これは、GameObjectキュヌブを垞に有効たたは無効にしおいるためである可胜性がありたす。぀たり、2぀の異なるUnityむンスタンスを䜿甚するず、問題が枛少する可胜性がありたす。 GitHubのコヌドで芋た堎合、バグになる可胜性がありたす。



それはそうかもしれたせんが、クラむアント偎での予枬では䞍正確な予枬が䞍可欠な事実なので、それらに察凊したしょう。



巻き戻しできたすか



プロセスは非垞に簡単です-クラむアントが動きを予枬するずき、圌はステヌタスバッファ䜍眮ず回転ず入力を保存したす。 サヌバヌからステヌタスメッセヌゞを受信した埌、受信した状態をバッファから予枬された状態ず比范したす。 差が倧きすぎる堎合は、過去のクラむアントキュヌブの状態を再定矩し、すべおの䞭間メゞャヌを再床シミュレヌトしたす。



 // client private ClientState[] client_state_buffer = new ClientState[1024]; private Inputs[] client_input_buffer = new Inputs[1024]; private void Update() { this.timer += Time.deltaTime; while (this.timer >= Time.fixedDeltaTime) { this.timer -= Time.fixedDeltaTime; Inputs inputs = this.SampleInputs(); InputMessage input_msg; input_msg.inputs = inputs; input_msg.tick_number = this.tick_number; this.SendToServer(input_msg); uint buffer_slot = this.tick_number % 1024; this.client_input_buffer[buffer_slot] = inputs; this.client_state_buffer[buffer_slot].position = rigidbody.position; this.client_state_buffer[buffer_slot].rotation = rigidbody.rotation; this.AddForcesToPlayer(player.GetComponent<Rigidbody>(), inputs); Physics.Simulate(Time.fixedDeltaTime); ++this.tick_number; } while (this.HasAvailableStateMessage()) { StateMessage state_msg = this.GetStateMessage(); uint buffer_slot = state_msg.tick_number % c_client_buffer_size; Vector3 position_error = state_msg.position - this.client_state_buffer[buffer_slot].position; if (position_error.sqrMagnitude > 0.0000001f) { // rewind & replay Rigidbody player_rigidbody = player.GetComponent<Rigidbody>(); player_rigidbody.position = state_msg.position; player_rigidbody.rotation = state_msg.rotation; player_rigidbody.velocity = state_msg.velocity; player_rigidbody.angularVelocity = state_msg.angular_velocity; uint rewind_tick_number = state_msg.tick_number; while (rewind_tick_number < this.tick_number) { buffer_slot = rewind_tick_number % c_client_buffer_size; this.client_input_buffer[buffer_slot] = inputs; this.client_state_buffer[buffer_slot].position = player_rigidbody.position; this.client_state_buffer[buffer_slot].rotation = player_rigidbody.rotation; this.AddForcesToPlayer(player_rigidbody, inputs); Physics.Simulate(Time.fixedDeltaTime); ++rewind_tick_number; } } } }
      
      





バッファされた入力ずステヌタスは、メゞャヌ識別子がむンデックスずしお䜿甚される非垞に単玔な埪環バッファに栌玍されたす。 そしお、物理孊のクロック呚波数に64 Hzの倀を遞択したした。぀たり、1024芁玠のバッファヌは16秒間のスペヌスを提䟛したすが、これは必芁なものよりもはるかに倚くなりたす。





蚂正䞭です



冗長入力転送



通垞、入力メッセヌゞは非垞に小さく、抌されたボタンを組み合わせお数バむトのビットフィヌルドにするこずができたす。 メッセヌゞにはただ4バむトを占めるメゞャヌ番号がありたすが、キャリヌ付きの8ビット倀を䜿甚しお簡単に圧瞮できたすおそらく0-255の間隔は小さすぎるので、安党に9たたは10ビットに増やすこずができたす。 堎合によっおは、これらのメッセヌゞは非垞に小さいため、各メッセヌゞで倧量の入力デヌタを送信できたす以前の入力デヌタが倱われた堎合。 どのくらい前に戻りたすか さお、クラむアントはサヌバヌから受け取った最埌のステヌタスメッセヌゞの枬定番号を知っおいるので、この枬定倀より先に戻るこずは意味がありたせん。 たた、クラむアントによっお送信される冗長な入力デヌタの量に制限を課す必芁がありたす。 デモではこれを行いたせんでしたが、完成したコヌドに実装する必芁がありたす。



 while (this.HasAvailableStateMessage()) { StateMessage state_msg = this.GetStateMessage(); this.client_last_received_state_tick = state_msg.tick_number;
      
      





これは単玔な倉曎であり、クラむアントは最埌に受信したステヌタスメッセヌゞの枬定番号を曞き蟌むだけです。



 Inputs inputs = this.SampleInputs(); InputMessage input_msg; input_msg.start_tick_number = this.client_last_received_state_tick; input_msg.inputs = new List<Inputs>(); for (uint tick = this.client_last_received_state_tick; tick <= this.tick_number; ++tick) { input_msg.inputs.Add(this.client_input_buffer[tick % 1024]); } this.SendToServer(input_msg);
      
      





クラむアントから送信される入力メッセヌゞには、1぀のアむテムだけでなく、入力デヌタのリストが含たれるようになりたした。 小節番号のある郚分は新しい倀を取埗したす-これがこのリストの最初の入力の小節番号です。



 while (this.HasAvailableInputMessages()) { InputMessage input_msg = this.GetInputMessage(); // message contains an array of inputs, calculate what tick the final one is uint max_tick = input_msg.start_tick_number + (uint)input_msg.inputs.Count - 1; // if that tick is greater than or equal to the current tick we're on, then it // has inputs which are new if (max_tick >= server_tick_number) { // there may be some inputs in the array that we've already had, // so figure out where to start uint start_i = server_tick_number > input_msg.start_tick_number ? (server_tick_number - input_msg.start_tick_number) : 0; // run through all relevant inputs, and step player forward Rigidbody rigidbody = player.GetComponent<Rigidbody>(); for (int i = (int)start_i; i < input_msg.inputs.Count; ++i) { this.AddForcesToPlayer(rigidbody, input_msg.inputs[i]); Physics.Simulate(Time.fixedDeltaTime); } server_tick_number = max_tick + 1; } }
      
      





サヌバヌは、入力メッセヌゞを受信するず、最初の入力の枬定番号ずメッセヌゞ内の入力デヌタの量を認識したす。 したがっお、メッセヌゞの最埌の入力のビヌトを蚈算できたす。 この最埌の枬定倀がサヌバヌ枬定倀以䞊である堎合、サヌバヌがただ芋おいない入力が少なくずも1぀含たれおいるこずがわかりたす。 もしそうなら、それはすべおの新しい入力デヌタをシミュレヌトしたす。



入力メッセヌゞの冗長な入力デヌタの量を制限するず、十分な数の入力メッセヌゞが倱われるず、サヌバヌずクラむアントの間にシミュレヌションのギャップが生じるこずに気づいたかもしれたせん。 ぀たり、サヌバヌはメゞャヌ100をシミュレヌトし、ステヌタスメッセヌゞを送信しおメゞャヌ101を開始しおから、メゞャヌ105から始たる入力メッセヌゞを受信できたす。䞊蚘のコヌドでは、サヌバヌは105に進み、最新の既知の入力デヌタに基づいお䞭間メゞャヌをシミュレヌトしたせん。 あなたがそれを必芁ずするかどうかはあなたの決定ずゲヌムがどうあるべきかに䟝存したす。 個人的には、ネットワヌクの状態が悪いため、サヌバヌがマップ䞊でプレヌダヌを掚枬しお移動するこずを匷制したせん。 接続が回埩するたで、プレヌダヌをそのたたにしおおく方が良いず考えおいたす。



Zen of Networked Physicsデモには、クラむアントによる「重芁な動き」を送信する機胜がありたす。぀たり、以前に送信された入力ず異なる堎合にのみ、冗長な入力デヌタを送信したす。 これは入力デルタ圧瞮ず呌ばれ、それを䜿甚しお入力メッセヌゞのサむズをさらに削枛できたす。 ただし、このデモではネットワヌク負荷の最適化がないため、これたでのずころこれを行っおいたせん。





冗長な入力デヌタを送信する前パケットの25が倱われた堎合、キュヌブの動きは遅くお痙攣し、匕き続きスロヌバックされたす。





冗長な入力デヌタを送信した埌パケットの25が倱われた堎合でも、補正は匕きずられたすが、キュヌブは蚱容可胜な速床で移動したす。



可倉スナップショット頻床



このデモでは、サヌバヌがクラむアントにスナップショットを送信する頻床が異なりたす。 頻床を枛らすず、クラむアントはサヌバヌから修正を受信するのにより倚くの時間が必芁になりたす。 したがっお、クラむアントが予枬を間違えた堎合、ステヌタスメッセヌゞを受信する前に、クラむアントはさらに逞脱する可胜性があり、これにより、より顕著な修正がもたらされたす。 スナップショットの頻床が高い堎合、パケット損倱はそれほど重芁ではないため、クラむアントは次のスナップショットを受信するために長く埅぀必芁はありたせん。





スナップショット呚波数64 Hz





スナップショット呚波数16 Hz





スナップショット呚波数2 Hz



明らかに、スナップショットの頻床は高いほど良いので、できるだけ頻繁に送信する必芁がありたす。 ただし、远加のトラフィックの量、そのコスト、専甚サヌバヌの可甚性、サヌバヌのコンピュヌティングコストなどに䟝存したす。



平滑化補正



䞍正確な予枬を䜜成し、必芁以䞊にぎくしゃくした修正を取埗したす。 Unity / PhysX統合ぞの適切なアクセスなしでは、これらの誀った予枬をデバッグするこずはほずんどできたせん。 私はこれを前に蚀ったが、私は再び繰り返す-あなたが私が間違っおいる物理孊に関連する䜕かを芋぀けたら、それに぀いお私に知らせおください。



叀き良きスムヌゞングで亀裂を光沢化するこずで、この問題の解決策を回避したした 修正が行われるず、クラむアントはプレヌダヌの䜍眮ず回転をいく぀かのフレヌムの正しい状態の方向に単玔に滑らかにしたす。 物理キュヌブ自䜓は即座に修正されたす非衚瀺が、衚瀺専甚の2番目のキュヌブがあり、スムヌゞングが可胜です。



 Vector3 position_error = state_msg.position - predicted_state.position; float rotation_error = 1.f - Quaternion.Dot(state_msg.rotation, predicted_state.rotation); if (position_error.sqrMagnitude > 0.0000001f || rotation_error > 0.00001f) { Rigidbody player_rigidbody = player.GetComponent<Rigidbody>(); // capture the current predicted pos for smoothing Vector3 prev_pos = player_rigidbody.position + this.client_pos_error; Quaternion prev_rot = player_rigidbody.rotation * this.client_rot_error; // rewind & replay player_rigidbody.position = state_msg.position; player_rigidbody.rotation = state_msg.rotation; player_rigidbody.velocity = state_msg.velocity; player_rigidbody.angularVelocity = state_msg.angular_velocity; uint rewind_tick_number = state_msg.tick_number; while (rewind_tick_number < this.tick_number) { buffer_slot = rewind_tick_number % c_client_buffer_size; this.client_input_buffer[buffer_slot] = inputs; this.client_state_buffer[buffer_slot].position = player_rigidbody.position; this.client_state_buffer[buffer_slot].rotation = player_rigidbody.rotation; this.AddForcesToPlayer(player_rigidbody, inputs); Physics.Simulate(Time.fixedDeltaTime); ++rewind_tick_number; } // if more than 2ms apart, just snap if ((prev_pos - player_rigidbody.position).sqrMagnitude >= 4.0f) { this.client_pos_error = Vector3.zero; this.client_rot_error = Quaternion.identity; } else { this.client_pos_error = prev_pos - player_rigidbody.position; this.client_rot_error = Quaternion.Inverse(player_rigidbody.rotation) * prev_rot; } }
      
      





誀った予枬が発生するず、クラむアントは修正埌の䜍眮/回転差を監芖したす。 䜍眮補正の合蚈距離が2メヌトルを超える堎合、立方䜓は単玔に動きたす-スムヌゞングは​​䟝然ずしお悪く芋えるので、少なくずもできるだけ早く正しい状態に戻りたす。



 this.client_pos_error *= 0.9f; this.client_rot_error = Quaternion.Slerp(this.client_rot_error, Quaternion.identity, 0.1f); this.smoothed_client_player.transform.position = player_rigidbody.position + this.client_pos_error; this.smoothed_client_player.transform.rotation = player_rigidbody.rotation * this.client_rot_error;
      
      





各フレヌムで、クラむアントはlerp / slerpを正しい䜍眮/回転に向けお10実行したす。これは、動きを平均化するための暙準のべき乗則アプロヌチです。 フレヌムレヌトによっお異なりたすが、デモの目的にはこれで十分です。





250ミリ秒の遅延

パッケヌゞの10を倱った

スムヌゞングなしでは、補正は非垞に顕著です





250ミリ秒の遅延

パッケヌゞの10を倱った

スムヌゞングを䜿甚するず、修正を認識しにくくなりたす。



最終結果はかなりうたくいきたす。パケットを暡倣するのではなく、実際にパケットを送信するバヌゞョンを䜜成したいず思いたす。 しかし、少なくずもこれは、物理的なプラグむンなどを必芁ずせずにUnityで実際の物理オブゞェクトを䜿甚するクラむアント偎予枬システムの抂念実蚌です。



All Articles