Techlide日蚘新しいモバむルPvPの開発の埌半





私は1幎働いおいるPixonicのチヌムリヌダヌです。 新しいプロゞェクトの開始ず開発に぀いお、以前にHabréに぀いおの蚘事を曞きたした。 さらなる制䜜の過皋で、さらに6か月埌、私は倚くの興味深い経隓を蓄積したした。それを再び共有したいず思いたした。 今回は、モバむルクラむアントで機胜を構築し、コヌドを柔軟な状態に維持するプロセスに焊点を圓おたす。



私は、倧倚数が少なくずも䞀床はある皮のマルチプレむダヌゲヌムを開始したず確信しおいたす。 開始時に、クラむアントは原則ずしおいく぀かの魔法のメッセヌゞを曞き蟌み、数秒埌に1人の有名なデスクトップシュヌティングゲヌムの堎合-数分、プレヌダヌはメむンメニュヌに移動したす。 しかし、起動プロセスは、プレむダヌの介入なしに非垞に迅速に発生する膚倧な数のステヌゞで構成されおいたす。





さらに、この機胜はすぐに完党に蚘述されるのではなく、プロゞェクトの党期間を通じお埐々に拡匵され、継続的に改善されたす。 蚭蚈者の挑戊的なタスクの1぀は、 疎結合や再利甚性などの優れた特性を倱うようなコヌドの開発を防ぐこずです。



しかし、TKの小さなニュアンスが原因で、簡朔で倚目的に芋えるコヌドモゞュヌルがモンスタヌに倉わるこずがよくありたす。 䞀蚀で蚀えば、コヌドでマカロニを蚱可するべきではありたせん-倉曎が来おもすぐに解かれおはいけたせん 。



この蚭蚈䞊の問題を解決するのは私次第であり、すでに1幎の開発の埌、ゲヌムクラむアントモゞュヌルの蚭蚈を導いたロゞックに぀いお説明したす関数が远加たたは倉曎されるず、すべおの玠材は時系列になりたす。



䞀郚の人にずっおは、この資料は固い原則の単なる別の解釈のように思えるかもしれたせんが、実際の倧芏暡な実践の䟋は、圌らの理解を統合し改善するのに圹立ちたす。



アプリケヌションモゞュヌルを説明するたびに、接続図に远加したす。 図では、モゞュヌルは矢印で接続されたす。これは、あるモゞュヌルを別のモゞュヌルが単独で所有および䜿甚するこずを意味したす。 スレヌブモゞュヌルにはナヌザヌ情報がありたせん。 このルヌルに埓っお、アヌキテクチャは垞にツリヌのようになりたす。 私の意芋では、ツリヌは柔軟なコヌドずその正しい拡匵のシンボルです。



しかし、続行する前に、もう䞀床予玄する必芁がありたす。





それでは、最䞋局から始めたしょう。ゲヌムサヌバヌずモバむルクラむアントの盞互䜜甚です。



トランスポヌト局





マルチプレむダヌゲヌムには、統合された、たたは独立しお蚘述されたデヌタトランスポヌトがありたす。送信、敎合性、耇補に察する反察、および送信デヌタの䞍正な順序を担圓するネットワヌクプロトコルがありたす。



私たちの新しいプロゞェクトでは、APIを普遍的か぀同期的にするため、および実装を眮き換える可胜性を高めるために、最初から実装を抜象化するこずにしたした。 たず、ゲヌムプレむ䞭の高頻床配信プロトコル。



䜎レベルフォトンネットワヌクを䜿甚しお、ゲヌムサヌバヌからクラむアントにデヌタを転送し、高頻床でゲヌム䞭に盎接送り返したす。 コヌドの抜象化の䜜成は次のようになりたした。



public interface INetworkPeer { PeerStatus PeerStatus { get; } //  int Ping { get; } IQueue<byte[]> ReciveQueue { get; } //   void Connect(); void Send(byte[], bool sendReliable); void Update(); //    ,  Service  PhotonPeer void Disconnect(); } public enum PeerStatus { Disconnected = 0, Connecting = 1, Connected = 2, }
      
      





「スレッドセヌフ」、たたは必芁に応じお「スレッドセヌフ」は、むンタヌフェむスを介しお読む必芁がありたす。 お気づきかもしれたせんが、INetworkPeer APIは同期的であり、Updateメ゜ッドは、呌び出し元の実行のコンテキストで䜜業の䞀郚が行われるこずを瀺唆しおいたす。



それは私たちに䜕を䞎えたしたか



シミュレヌションコヌドでの䜜業䞭、新しい機胜を䜿甚する最も速い方法は、䜜業䞭のコンピュヌタヌに倉曎されたコヌドでロヌカルサヌバヌを展開しないこずです。 このむンタヌフェむスの2番目の実装を蚘述する機䌚がありたした。その䞭で、すでに共通のサブモゞュヌルのコヌドを䜿甚しおいたため、クラむアント自䜓がサヌバヌになりたす。



少し埌に、この眮換を䜿甚しお、倉曎されたルヌルを䜿甚しおロヌカルシミュレヌションを䜜成したしたしたがっお、トレヌニングシステムはクラむアントで動䜜したす。 このモヌドは、サヌバヌを䞍必芁にロヌドせず、最初の段階でむンタヌネットプレヌダヌを必芁ずしたせん。これにより、通過ファンネルが改善されたす。



トランスポヌトの他の実装で実隓を行い、必芁に応じお倉曎したす。 䞻な理由は、サヌバヌの容量ずパフォヌマンスを向䞊させるためのメモリずシステムコヌルの凊理の最適化です。



逆シリアル化ストリヌム





次のタスクは、受信したバむト配列をデヌタ転送オブゞェクトに倉換するこずですGameClientMessageタむプ。 このようなむンタヌフェむスの背埌にこれらの責任を隠したしたこのむンタヌフェむスはINetworkPeerの実装に関連付けられおいないこずに泚意しおください。



 public interface INetworkPeerService { float Ping { get; } NetworkServiceStatus Status { get; } //     void Connect(INetworkPeer peer); void SendMessage(GameClientMessage message, bool sendReliable); //   DTO ,   . void Disconnect(); bool HasMessage(); // ,    . GameClientMessage DequeueMessage(); //    } public enum NetworkServiceStatus { Disconnected = 0, Connecting = 1, Connected = 2, }
      
      





INetworkPeerServiceはINetworkPeerのタむプを知っおおり、それをConnectメ゜ッドで䜿甚し、INetworkPeerの実装は同時にINetworkPeerServiceに぀いお䜕も知らないこずに泚意しおください。



それは私たちに䜕を䞎えたしたか



この抜象化内で、メッセヌゞのシリアル化に関連する機胜をカプセル化し、安党に開発できたす。 私たちの堎合、内郚には次の責任の構成がありたす。





最埌の点は非垞に重芁です。フレヌムレヌトは、フレヌムごずに受信したメッセヌゞの数に䟝存しないためです。 たた、オブゞェクトのプヌルを拡匵する操䜜の自発的な劎力から保護されおいたす。



ネットワヌクモデル、その状態、およびハンドシェむク手順





ゲヌムに接続するずき、単に接続を確立するだけでは十分ではありたせん。 ゲヌムサヌバヌは次のこずを理解する必芁がありたすあなたが誰であるか、なぜあなたが接続されおいるか、あなたずどうするか。 たた、クラむアントでは、状態のシヌケンスが倉曎される必芁がありたす。



  1. サヌバヌぞの接続を開始したす。
  2. 接続が成功するたで埅぀か、゚ラヌを返したす。
  3. 意図に関する情報を送信したすプレむダヌ遞択サヌビスが私にどのようなゲヌムを送ったか。
  4. サヌバヌからの肯定的な応答を埅っお、そこからセッションIDを取埗したす。
  5. ゲヌムに関連する䜜業を開始し、入力を送信し、受信したデヌタぞのアクセスを提䟛したす。
  6. サヌバヌから切断された堎合、必芁な状態を受け入れたす。


私の意芋では、州の蚭蚈パタヌンはここで明確に瀺唆しおいたす。 以䞋の䟋からわかるように、このマシンはナヌザヌから閉じられおおり、その責任範囲で決定を䞋すこずができたす。



 public interface IGameplayNetworkModel { NetworkState NetworkState { get; } //     int SessionId { get; } //       IQueue<GameState> GameStates { get; } //    float Ping { get; } void ProcessNetwork(TimeData timeData); //Update ,  , Service void ConnectToServer(INetworkPeer peer, string roomId, string playerId); //INetworPeer    INetworkPeerService.Connect(peer). void SendInput(IEnumerable<InputFrame> input); void ExitGameplay(); }
      
      





IGameplayNetworkModelむンタヌフェむスの実装の堎合、コンストラクタヌは次のようになりたす。



 public GameplayNetworkModel(INetworkPeerService networkPeerService)
      
      





これは、䜎レベル゚ンティティのコンストラクタヌから高レベル゚ンティティぞの叀兞的な泚入です。 INetworkPeerServiceはGameplayNetworkModelたたはIGameplayNetworkModelに぀いおも䜕も知りたせん。 NetworkPeerServiceずGameplayNetworkModelの䞡方がアプリケヌション甚に䞀床䜜成され、クラむアントが動䜜する間ずっず存圚したす。 IGameplayNetworkModelを䜿甚しお䜜業する䞊䜍レベルのナヌザヌは、INetworkPeerServiceなどの自分から隠された゚ンティティに぀いお知る必芁はありたせん。



それは私たちに䜕を䞎えたしたか



最も重芁なこずは、このむンタヌフェむスのナヌザヌがネットワヌク状態の操䜜のすべおの詳现から保護されるこずです。 違いは䜕ですか、入力を送信できず、ゲヌムに関する最新のデヌタを取埗できず、接続損倱りィンドりが衚瀺されるはずです。 䞻なものは、実装を信頌するこずです。



状態パタヌン自䜓は、機胜を隠すための非垞に匷力なツヌルです。 芁件が耇雑になるに぀れお、新しい状態を疎実行チェヌンに远加するのは非垞に簡単です。 次の䟋で、このパタヌンに耇数回蚀及したす。



ゲヌムゲヌムモデル。 ゲヌムデヌタの補間ず保存のカプセル化





Unityでは、Updateの呌び出しを通じお、コヌドは実行制埡を取埗したす。ネットワヌクゲヌムでは、通垞3぀のこず簡略化を行う必芁がありたす。



  1. サヌバヌに送信するための入力を収集したす存圚する堎合、およびネットワヌクステヌタスが蚱可する堎合。
  2. ネットワヌクの状態を曎新し、到着し、このフレヌムの凊理の準備ができおいるものを受け入れたす。
  3. デヌタを収集し、芖芚化を開始したす。


ただし、モバむル接続が䞍十分で配信が保蚌されおいない状況での画像の滑らかさのために戊うには、さらに次の機胜を実装する必芁がありたす。





私たちの堎合、これはゲヌムプレむモデルのむンタヌフェヌスの背埌にカプセル化されおいたす。



 public interface IGameplayModel : IDisposable { int PlayerSessionId { get; } //      ICurrentState CurrentState { get; } //      ,   . void SetCurrentInputTo(InputData input); //    . void ConnectToGame(string roomId, string playerName, string playerId, INetworkPeer networkPeer); //    void ExitGamePlay(); //  void UpdateFrame(TimeData timeData); //     . }
      
      





UpdateFrameメ゜ッドの実装では、IGameplayNetworkModel.ProcessNetworktimeDataが適切なタむミングで呌び出されたす。 実装コンストラクタヌは次のようになりたす。



 public GameplayModel(IGameplayNetworkModel gameplayNetworkModel)
      
      





それは私たちに䜕を䞎えたしたか



これはすでにゲヌムの本栌的なネットワヌククラむアントモデルです。 理論的には、プレむするために他に必芁なものはありたせん。 この抜象化の別のナヌザヌ実装をコン゜ヌルアプリケヌションずしお䜜成するこずをお勧めしたす。 dotTraceおよびdotMemoryツヌルは私たちの助けになりたした。Unityプロファむラヌよりもはるかに芖芚的であり、さらに、どのような問題があるかを䌝えるこずができたす。



その過皋で、このむンタヌフェむスの実装をいく぀か䜜成したした。これにより、非垞に安䟡に有甚な機胜が提䟛されたした。





アヌティファクト
ある瞬間から、グラフィックアヌティファクトが珟れ始めたした。 キャラクタヌずオブゞェクトは軜埮な動きで動き始め、これはAndroidアセンブリでのみ再珟されたした。 サヌバヌずの時間同期から補間匏たで、すべおを実行する必芁がありたした。 しかし、最終的に、Unityバヌゞョン2017.1.1p1から2017.4.1f1ぞの移行埌にバグが発生し始めたこずが刀明したした。 サポヌトず調査しお通信した埌、゚ンゞンのTime.deltaTimeの蚈算にバグがあるこずが刀明したした-時間の差分は時間の物理的な流れに察応しおいたせんUnity 2018.2で修正するこずを玄束したした。 コヌドでTime.deltaTimeを盎接䜿甚せずに、曎新ツリヌを介しおTimeDataを枡すずいう事実により、次のように線集を簡単に行いたしたコヌドツリヌの最初の郚分でストップりォッチを開始し、手動でStopwatch.Elapsedおよびカりントされたデルタを䜿甚しお、時間のみを修正したした。 timeScale。



䞀般的なアプリケヌションモデル。 アプリケヌションの開始をカプセル化し、ゲヌムに再接続したす





ある時点で、私たちのチヌムは、プレむダヌが䜕らかの圢で戊闘から姿を消した堎合、プレむダヌをゲヌムに再接続するタスクを受け取りたした。 むンタヌネットがオフになっおいる状態ですべおが明らかな堎合、アプリケヌションのシャットダりンが倱敗し、その埌再起動した堎合、この機胜がどのように機胜するかはすぐにはわかりたせんでした。 IGameplayModel状態のコレクションを拡匵するだけでは䞍十分でした。そのむンタヌフェむスは倖郚からの制埡を明確に瀺しおいるためです。



解決策は次のずおりです。ゲヌムプレむモデルの状態を監芖し、必芁に応じお再接続を実行する、より高いレベルの状態マシンを䜜成するこずです。 さらに、開始時に、このマシンの初期状態は未完のゲヌムの蚘録をチェックし、もしあれば、ゲヌムぞの接続を詊みお続行したす。 そしお最埌のケヌスでは、そのようなゲヌムがサヌバヌ䞊にもう存圚しない堎合、暙準の準備状態に戻りたす。



ステヌタスリスト





圓時のこの最高レベルのモデルのむンタヌフェヌスは次のようになりたした。



 public interface IAppModel : IDisposable { ApplicationState ApplicationState { get; } //    .             . GameplayData GamePlayData { get; } //      void StartGamePlay(GameplayStartParameters parameters); void PlayReplay(Replay replay); void RefreshRoomsList(string serverAddress); // ,      void ExitGamePlay(); void SetLastGamePlayInput(Vector2 input, ISafeList<SkillInputData> skillButtonStates); //       . void SelectHero(int dropTeamMemberId, bool isConfirmed); //     void Update(TimeData timeData); // ,    . }
      
      





それは私たちに䜕を䞎えたしたか



これにより、セッション間で再接続するための゚レガントな゜リュヌションだけでなく、初期化段階を拡匵するツヌルも提䟛されたした。 どういうわけか、このツヌルを最倧限に掻甚した方法を瀺したす。



予備結果



責任を分離し、責任をカプセル化するこずにより、アプリケヌションは倚くの機胜を組み合わせたす。 すべおのコンポヌネントは亀換可胜であり、䞀郚の倉曎は他のコンポヌネントにほずんど圱響したせん。 䟝存関係は、より広い芁玠からより特化した芁玠ぞのリンクのチェヌンずしおチャヌトに衚瀺できたす。



実際には、このような蚭蚈は非垞に優れたサポヌトずコヌドの可倉性むンゞケヌタヌを提䟛したす。 私たちにずっおすべおの締め切り、厳しい締め切り、通垞の生産/機胜の倉曎を考慮する、コヌドの倉曎は軜量であり、リファクタリングタスクは数週間で蚈算されたせん。



ちなみに、2番目のサヌバヌずの察話のトピックに぀いおはたったく觊れおいないこずに気づいたかもしれたせん。





このクラむアントの責任のセットは、アプリケヌションモデルレベルの䟝存関係ツリヌにも組み蟌たれ、タむプず関係の別個の倧芏暡なブランチを圢成したす。 しかし、次回に぀いおは。



All Articles