Cumu avemu fattu a meccanica di calculu balisticu per un shooter mobile cù un algoritmu di compensazione di ritardu di rete

Cumu avemu fattu a meccanica di calculu balisticu per un shooter mobile cù un algoritmu di compensazione di ritardu di rete

Hola, sò Nikita Brizhak, un sviluppatore di servitori di Pixonic. Oghje vogliu parlà di cumpensà u lag in u multiplayer mobile.

Parechji articuli sò stati scritti nantu à a compensazione di u lag di u servitore, ancu in russo. Questu ùn hè micca surprisante, postu chì sta tecnulugia hè stata aduprata attivamente in a creazione di FPS multiplayer da a fine di l'anni 90. Per esempiu, pudete ricurdà u mod QuakeWorld, chì era unu di i primi à aduprà.

Avemu ancu aduprà in u nostru shooter multiplayer mobile Dino Squad.

In questu articulu, u mo scopu ùn hè micca di ripetiri ciò chì hè digià scrittu mille volte, ma di dì cumu avemu implementatu a compensazione di lag in u nostru ghjocu, tenendu in contu a nostra pila di tecnulugia è e funzioni di ghjocu core.

Uni pochi parolle nantu à a nostra corteccia è tecnulugia.

Dino Squad hè un shooter PvP mobile in rete. I ghjucatori cuntrullanu i dinosauri equipati cù una varietà d'arme è si cumbattenu in squadre 6v6.

Sia u cliente è u servitore sò basati in Unity. L'architettura hè abbastanza classica per i tiratori: u servitore hè autoritariu, è a predizione di u cliente travaglia nantu à i clienti. A simulazione di ghjocu hè scritta cù l'ECS in-house è hè aduprata in u servitore è in u cliente.

Se questa hè a prima volta chì avete intesu parlà di a compensazione di lag, eccu una breve escursione in u prublema.

In i ghjochi FPS multiplayer, a partita hè generalmente simulata nantu à un servitore remoto. I ghjucatori mandanu a so input (infurmazione nantu à i chjavi pressati) à u servitore, è in risposta u servitore li manda un statu di ghjocu aghjurnatu in cunsiderà i dati ricevuti. Cù stu schema d'interazzione, u ritardu trà a pressione di a chjave avanti è u mumentu chì u caratteru di u ghjucatore si move nantu à u screnu serà sempre più grande di u ping.

Mentre nantu à e rete lucali, stu ritardu (pupularmente chjamatu input lag) pò esse imperceptible, quandu ghjucate via Internet crea una sensazione di "sliding on ice" quandu u cuntrollu di un caratteru. Stu prublema hè doppiamente pertinenti per e rete mobile, induve u casu quandu u ping di un ghjucatore hè 200 ms hè sempre cunsideratu una cunnessione eccellente. Spessu u ping pò esse 350, 500, o 1000 ms. Allora diventa quasi impussibile di ghjucà un shooter veloce cù input lag.

A suluzione à stu prublema hè a prediczione di simulazione di u cliente. Quì u cliente stessu applica l'input à u caratteru di u ghjucatore, senza aspittà una risposta da u servitore. È quandu a risposta hè ricevuta, simpricimenti paragunate i risultati è aghjurnà e pusizioni di l'avversari. U ritardu trà a pressione di una chjave è a visualizazione di u risultatu nantu à u screnu in questu casu hè minimu.

Hè impurtante di capiscenu a sfumatura quì: u cliente sempre sguassate secondu u so ultimu input, è i nemichi - cù ritardu di rete, secondu u statu precedente da i dati da u servitore. Questu hè, quandu si spara à un nemicu, u ghjucatore u vede in u passatu relative à ellu stessu. Più nantu à a previsione di u cliente avemu scrittu prima.

Cusì, a previsione di u cliente risolve un prublema, ma crea un altru: se un ghjucatore tira à u puntu induve l'inimicu era in u passatu, nantu à u servitore quandu si spara in u stessu puntu, u nemicu ùn pò più esse in quellu locu. A compensazione di lag di u servitore prova di risolve stu prublema. Quandu un'arma hè sparata, u servitore restaurà u statu di ghjocu chì u ghjucatore hà vistu in u locu à u mumentu di u tiru, è verifica s'ellu puderia veramente chjappà u nemicu. Se a risposta hè "sì", u colpu hè cuntatu, ancu s'ellu ùn hè più in u servitore in quellu puntu.

Armati di sta cunniscenza, avemu cuminciatu à implementà a compensazione di lag di u servitore in Dino Squad. Prima di tuttu, avemu avutu a capiscenu cumu per restaurà in u servitore ciò chì u cliente hà vistu? È ciò chì esattamente deve esse restauratu? In u nostru ghjocu, i colpi di l'arme è l'abilità sò calculati per via di raycasts è overlays - vale à dì per interazzione cù i colliders fisici di u nemicu. Per quessa, avemu bisognu di ripruduce a pusizione di questi colliders, chì u ghjucatore "vidia" in u locu, in u servitore. À quellu tempu avemu usatu a versione Unity 2018.x. L'API di fisica ci hè statica, u mondu fisicu esiste in una sola copia. Ùn ci hè manera di salvà u so statu è poi risturà da a scatula. Allora chì fà ?

A suluzione era nantu à a superficia; tutti i so elementi eranu digià utilizati da noi per risolve altri prublemi:

  1. Per ogni cliente, avemu bisognu di sapè à quale tempu hà vistu l'avversari quandu hà pressu i chjavi. Avemu digià scrittu sta informazione in u pacchettu di input è l'hà utilizatu per aghjustà a predizione di u cliente.
  2. Avemu bisognu di pudè almacenà a storia di i stati di u ghjocu. Hè in questu chì tenemu e pusizioni di i nostri avversari (è dunque i so colliders). Avemu digià avutu una storia statale nantu à u servitore, avemu usatu per custruisce delta. Sapendu u tempu ghjustu, pudemu truvà facilmente u statu ghjustu in a storia.
  3. Avà chì avemu u statu di ghjocu da a storia in manu, avemu bisognu di pudè sincronizà i dati di u ghjucatore cù u statu di u mondu fisicu. I colliders esistenti - move, mancanti - creanu, inutili - distrugge. Sta logica era ancu digià scritta è custituita da parechji sistemi ECS. L'avemu utilizatu per tene parechje sale di ghjocu in un prucessu Unity. E postu chì u mondu fisicu hè unu per prucessu, hà da esse riutilizatu trà e camere. Prima di ogni tick di a simulazione, avemu "resettatu" u statu di u mondu fisicu è reinitialized cù dati per a stanza attuale, circannu a riutilizà l'uggetti di ghjocu Unity quant'è pussibule attraversu un sistema di pooling intelligente. Tuttu ciò chì restava era invucà a stessa logica per u statu di u ghjocu da u passatu.

Mettendu tutti questi elementi inseme, avemu avutu una "macchina di u tempu" chì puderia retrocede u statu di u mondu fisicu à u mumentu ghjustu. U codice hè diventatu simplice:

public class TimeMachine : ITimeMachine
{
     //История игровых состояний
     private readonly IGameStateHistory _history;

     //Текущее игровое состояние на сервере
     private readonly ExecutableSystem[] _systems;

     //Набор систем, расставляющих коллайдеры в физическом мире 
     //по данным из игрового состояния
     private readonly GameState _presentState;

     public TimeMachine(IGameStateHistory history, GameState presentState, ExecutableSystem[] timeInitSystems)
     {
         _history = history; 
         _presentState = presentState;
         _systems = timeInitSystems;  
     }

     public GameState TravelToTime(int tick)
     {
         var pastState = tick == _presentState.Time ? _presentState : _history.Get(tick);
         foreach (var system in _systems)
         {
             system.Execute(pastState);
         }
         return pastState;
     }
}

Tuttu ciò chì restava era di capisce cumu utilizà sta macchina per cumpensà facilmente i colpi è l'abilità.

In u casu più simplice, quandu a meccanica hè basatu annantu à un unicu hitscan, tuttu pare esse chjaru: prima chì u ghjucatore spara, hà bisognu di rinvià u mondu fisicu à u statu desideratu, fà un raycast, cuntà u hit o miss, è torna u mondu à u statu iniziale.

Ma ci sò assai pochi tali meccanichi in Dino Squad! A maiò parte di l'armi in u ghjocu creanu prughjetti - bullets longu chì volanu per parechji ticks di simulazione (in certi casi, decine di ticks). Chì fà cun elli, chì ora duveranu vola ?

В articulu anticu nantu à a pila di rete Half-Life, i ragazzi di Valve anu dumandatu a listessa quistione, è a so risposta hè stata questa: a compensazione di u lag di u projectile hè problematicu, è hè megliu per evitari.

Ùn avemu micca avutu sta opzione: l'armi basati in projectile eranu una caratteristica chjave di u disignu di u ghjocu. Allora avemu avutu à vene cun qualcosa. Dopu qualchì brainstorming, avemu formulatu duie opzioni chì parevanu travaglià:

1. Lighemu u projectile à u tempu di u ghjucatore chì hà creatu. Ogni tick di a simulazione di u servitore, per ogni balla di ogni ghjucatore, rinviamu u mondu fisicu à u statu di u cliente è eseguite i calculi necessarii. Stu approcciu hà permessu di avè una carica distribuita nantu à u servitore è u tempu di volu prevedibile di i prughjetti. A prevedibilità era particularmente impurtante per noi, postu chì avemu tutti i prughjetti, cumpresi i prughjetti nemici, previsti nantu à u cliente.

Cumu avemu fattu a meccanica di calculu balisticu per un shooter mobile cù un algoritmu di compensazione di ritardu di rete
In a stampa, u ghjucatore à tick 30 spara un missile in anticipazione: vede in quale direzzione corre u nemicu è cunnosce a velocità apprussimativa di u missile. Localmente vede ch'ellu hà culpitu u mira à u 33e tick. Grazie à a compensazione di lag, apparirà ancu nantu à u servitore

2. Facemu tuttu u listessu cum'è in a prima opzione, ma, dopu avè cuntatu un tick di a simulazione di bullet, ùn avemu micca firmatu, ma cuntinuemu à simule u so volu in u stessu tick di u servitore, ogni volta chì avvicina u so tempu più vicinu à u servitore. unu per unu tick è aghjurnà pusizioni collider. Facemu questu finu à chì una di duie cose succede:

  • U bullet hè scadutu. Questu significa chì i calculi sò finiti, pudemu cuntà un miss o un hit. È questu hè à u stessu tick in quale u colpo hè statu sparatu! Per noi questu era un plus è un minus. Un plus - perchè per u ghjucatore di sparà, questu hà riduciutu significativamente u ritardu trà u colpu è a diminuzione di a salute di u nemicu. U svantaghju hè chì u listessu effettu hè statu osservatu quandu l'avversari sparanu à u ghjucatore: l'inimicu, pare, solu spara un cohettu lento, è u dannu hè digià cuntatu.
  • U bullet hà righjuntu u tempu di u servitore. In questu casu, a so simulazione continuarà in u prossimu tick di u servitore senza alcuna compensazione di lag. Per i prughjetti lenti, questu puderia teoricamente riduce u numeru di rollbacks di fisica cumparatu cù a prima opzione. À u listessu tempu, a carica irregolare nantu à a simulazione aumentava: u servitore era o inattivu, o in un tick di u servitore calculava una decina di ticks di simulazione per parechje balle.

Cumu avemu fattu a meccanica di calculu balisticu per un shooter mobile cù un algoritmu di compensazione di ritardu di rete
U stessu scenariu cum'è in a stampa precedente, ma calculatu secondu u secondu schema. U missile "catturatu" cù u tempu di u servitore à u listessu tick chì u colpu hè accadutu, è u colpu pò esse cuntatu cum'è u prossimu tick. À u 31 tick, in questu casu, a compensazione di lag ùn hè più appiicata

In a nostra implementazione, sti dui approcci sò diffirenti in solu un paru di linee di codice, cusì avemu creatu i dui, è per un bellu pezzu esistevanu in parallelu. Sicondu a meccanica di l'arma è a velocità di a bala, avemu sceltu una o una altra opzione per ogni dinosauru. U puntu di svolta quì hè stata l'apparizione in u ghjocu di meccanica cum'è "s'è tù chjappà u nemicu tante volte in un tali tempu, uttene un tali bonus". Qualchese meccanicu induve u tempu in u quale u ghjucatore hà colpitu u nemicu hà ghjucatu un rolu impurtante hà rifiutatu di travaglià cù u sicondu approcciu. Allora avemu finitu per andà cù a prima opzione, è avà s'applica à tutte l'arme è tutte e capacità attive in u ghjocu.

Separatamente, vale a pena suscitarà u prublema di u rendiment. Se pensate chì tuttu questu rallentarà e cose, rispondu : hè. L'unità hè abbastanza lenta in u muvimentu di i colliders è li accende è spegne. In Dino Squad, in u "peghju" casu, ci ponu esse parechje centinaie di prughjetti esistenti simultaneamente in cumbattimentu. Spostà i colliders per cuntà ogni projectile individualmente hè un lussu inaccessibile. Dunque, era assolutamente necessariu per noi di minimizzà u numeru di "rollbacks" di fisica. Per fà questu, avemu creatu un cumpunente separatu in ECS in quale avemu registratu u tempu di u ghjucatore. Avemu aghjustatu à tutte e entità chì necessitanu una compensazione di lag (prughjetti, capacità, etc.). Prima di cumincià à trasfurmà tali entità, li raggruppemu da questu tempu è li processemu inseme, rinviendu u mondu fisicu una volta per ogni cluster.

In questu stadiu avemu un sistema di travagliu generale. U so codice in una forma un pocu simplificata:

public sealed class LagCompensationSystemGroup : ExecutableSystem
{
     //Машина времени
     private readonly ITimeMachine _timeMachine;

     //Набор систем лагкомпенсации
     private readonly LagCompensationSystem[] _systems;
     
     //Наша реализация кластеризатора
     private readonly TimeTravelMap _travelMap = new TimeTravelMap();

    public LagCompensationSystemGroup(ITimeMachine timeMachine, 
        LagCompensationSystem[] lagCompensationSystems)
     {
         _timeMachine = timeMachine;
         _systems = lagCompensationSystems;
     }

     public override void Execute(GameState gs)
     {
         //На вход кластеризатор принимает текущее игровое состояние,
         //а на выход выдает набор «корзин». В каждой корзине лежат энтити,
         //которым для лагкомпенсации нужно одно и то же время из истории.
         var buckets = _travelMap.RefillBuckets(gs);

         for (int bucketIndex = 0; bucketIndex < buckets.Count; bucketIndex++)
         {
             ProcessBucket(gs, buckets[bucketIndex]);
         }

         //В конце лагкомпенсации мы восстанавливаем физический мир 
         //в исходное состояние
         _timeMachine.TravelToTime(gs.Time);
     }

     private void ProcessBucket(GameState presentState, TimeTravelMap.Bucket bucket)
     {
         //Откатываем время один раз для каждой корзины
         var pastState = _timeMachine.TravelToTime(bucket.Time);

         foreach (var system in _systems)
         {
               system.PastState = pastState;
               system.PresentState = presentState;

               foreach (var entity in bucket)
               {
                   system.Execute(entity);
               }
          }
     }
}

Il ne restait plus qu'à configurer les détails :

1. Capisce quantu à limità a distanza massima di u muvimentu in u tempu.

Era impurtante per noi di fà u ghjocu u più accessibile pussibule in cundizioni di e rete mobili poveri, cusì avemu limitatu a storia cù un margine di 30 ticks (cù un tick rate di 20 Hz). Questu permette à i ghjucatori di chjappà avversari ancu à pings assai alti.

2. Determinà quale ogetti ponu esse spustati in u tempu è quale ùn pò micca.

Di sicuru, movemu i nostri avversari. Ma i scudi di energia installable, per esempiu, ùn sò micca. Avemu decisu chì era megliu dà priorità à l'abilità difensiva, cum'è spessu si faci in i tiratori in linea. Se u ghjucatore hà digià postu un scudo in u presente, i balli compensati da u lag da u passatu ùn deve micca vola per ellu.

3. Decide s'ellu hè necessariu di cumpensà l'abilità di i dinosauri: muzzicu, colpu di coda, etc. Avemu decisu ciò chì era necessariu è processà secondu e listessi regule cum'è bullets.

4. Determina ciò chì fà cù i colliders di u ghjucatore per quale a compensazione di lag hè esse realizatu. In una bona manera, a so pusizioni ùn deve micca trasfurmà in u passatu: u ghjucatore deve vede in u stessu tempu in u quale hè avà nantu à u servitore. In ogni casu, avemu ancu retrocede i colliders di u ghjucatore di sparà, è ci sò parechje ragioni per questu.

Prima, migliura u clustering: pudemu usà u listessu statu fisicu per tutti i ghjucatori cù ping stretti.

Siconda, in tutti i raycasts è overlaps sempre escludemu i colliders di u ghjucatore chì pussede l'abilità o projectiles. In Dino Squad, i ghjucatori cuntrolanu i dinosauri, chì anu una geometria piuttostu non standard per i standard di tiratore. Ancu s'è u ghjucatore tira in un angulu inusual è a trajectoria di a balla passa per u collider di dinosauri di u ghjucatore, a balla l'ignorarà.

In terzu, calculemu e pusizioni di l'arma di u dinosauru o u puntu di applicazione di l'abilità utilizendu dati da l'ECS ancu prima di l'iniziu di a compensazione di lag.

In u risultatu, a pusizione reale di i colliders di u lag-compensated player ùn hè micca impurtante per noi, cusì avemu pigliatu una strada più produtiva è à u stessu tempu più simplice.

A latenza di a rete ùn pò esse solu eliminata, pò esse solu mascherata. Cum'è qualsiasi altru mètudu di disguise, a compensazione di u lag di u servitore hà i so cummerci. Migliura l'esperienza di ghjocu di u ghjucatore chì spara à a spesa di u ghjucatore chì hè sparatu. Per Dino Squad, però, a scelta quì era ovvia.

Di sicuru, tuttu questu hà avutu ancu esse pagatu da a cumplessità aumentata di u codice di u servitore in tuttu - sia per i programatori è i diseggiani di ghjocu. Se prima a simulazione era una semplice chjama sequenziale di sistemi, allora cù una compensazione di lag, i loops nidificati è i rami apparsu in questu. Avemu ancu passatu assai sforzu per fà cunvene u travagliu.

In a versione 2019 (è forse un pocu prima), Unity hà aghjuntu un supportu tutale per scene fisiche indipendenti. L'avemu implementatu nantu à u servitore quasi subitu dopu à l'aghjurnamentu, perchè vulemu sbarazzà rapidamente di u mondu fisicu cumunu à tutte e stanze.

Avemu datu à ogni sala di ghjocu a so propria scena fisica è cusì eliminata a necessità di "pulizziari" a scena da i dati di a stanza vicina prima di calculà a simulazione. Prima, hà datu un aumentu significativu di a produtividade. Siconda, hà permessu di sbarazzarsi di una classa sana di bugs chì si sò sviluppati se u programatore hà fattu un errore in u codice di pulizia di scena quandu aghjunghje novi elementi di ghjocu. Tali errori eranu difficiuli di debug, è spessu risultatu in u statu di l'uggetti fisichi in a scena di una stanza "fluendu" in una altra stanza.

In più, avemu fattu una ricerca in se i sceni fisichi puderanu esse aduprati per almacenà a storia di u mondu fisicu. Questu hè, in cundizzioni, ùn assignate micca una scena à ogni stanza, ma 30 sceni, è fate un buffer ciclicu fora di elli, in quale guardà a storia. In generale, l'opzione hè stata di travagliu, ma ùn l'avemu micca implementata: ùn hà micca dimustratu alcuna crescita di produtividade, ma hà bisognu di cambiamenti piuttostu risicatu. Era difficiuli di predichendu cumu si cumportanu u servitore quandu travaglia per un bellu pezzu cù tante scene. Dunque, avemu seguitu a regula: "S'ellu ùn hè micca rottu, ùn risolve micca».

Source: www.habr.com

Add a comment