Hoe ons die meganika van ballistiese berekening vir 'n mobiele skut gemaak het met 'n netwerkvertragingsvergoedingsalgoritme

Hoe ons die meganika van ballistiese berekening vir 'n mobiele skut gemaak het met 'n netwerkvertragingsvergoedingsalgoritme

Hallo, ek is Nikita Brizhak, 'n bedienerontwikkelaar van Pixonic. Vandag wil ek praat oor die kompensasie vir vertraging in mobiele multiplayer.

Baie artikels is oor bedienerlagvergoeding geskryf, insluitend in Russies. Dit is nie verbasend nie, aangesien hierdie tegnologie sedert die laat 90's aktief gebruik is in die skepping van multiplayer FPS. Byvoorbeeld, jy kan die QuakeWorld-mod onthou, wat een van die eerstes was wat dit gebruik het.

Ons gebruik dit ook in ons mobiele multispeler-skieter Dino Squad.

In hierdie artikel is my doel nie om te herhaal wat reeds duisend keer geskryf is nie, maar om te vertel hoe ons vertragingsvergoeding in ons spel geïmplementeer het, met inagneming van ons tegnologiestapel en kernspelkenmerke.

'n Paar woorde oor ons korteks en tegnologie.

Dino Squad is 'n netwerk mobiele PvP-skieter. Spelers beheer dinosourusse wat toegerus is met 'n verskeidenheid wapens en veg teen mekaar in 6v6-spanne.

Beide die kliënt en die bediener is gebaseer op Unity. Die argitektuur is redelik klassiek vir shooters: die bediener is outoritêr, en kliëntvoorspelling werk op die kliënte. Die spelsimulasie word geskryf deur gebruik te maak van interne ECS en word op beide die bediener en kliënt gebruik.

As dit die eerste keer is dat jy van vertragingsvergoeding hoor, hier is 'n kort uitstappie na die kwessie.

In multiplayer FPS-speletjies word die wedstryd gewoonlik op 'n afgeleë bediener gesimuleer. Spelers stuur hul insette (inligting oor die sleutels wat gedruk is) na die bediener, en in reaksie stuur die bediener vir hulle 'n opgedateerde speltoestand met inagneming van die ontvangde data. Met hierdie interaksieskema sal die vertraging tussen die druk van die vorentoe-sleutel en die oomblik wat die spelerkarakter op die skerm beweeg, altyd groter wees as die ping.

Terwyl op plaaslike netwerke hierdie vertraging (wat in die volksmond insetvertraging genoem word) onopvallend kan wees, skep dit 'n gevoel van "gly op ys" wanneer 'n karakter beheer word wanneer u via die internet speel. Hierdie probleem is dubbel relevant vir mobiele netwerke, waar die geval wanneer 'n speler se ping 200 ms is, steeds as 'n uitstekende verbinding beskou word. Dikwels kan die ping 350, 500 of 1000 ms wees. Dan word dit amper onmoontlik om 'n vinnige skut met insetlag te speel.

Die oplossing vir hierdie probleem is kliënt-kant simulasie voorspelling. Hier pas die kliënt self die insette op die spelerkarakter toe, sonder om te wag vir 'n reaksie van die bediener. En wanneer die antwoord ontvang word, vergelyk dit eenvoudig die resultate en werk die opponente se posisies op. Die vertraging tussen die druk van 'n sleutel en die vertoon van die resultaat op die skerm in hierdie geval is minimaal.

Dit is belangrik om die nuanse hier te verstaan: die kliënt trek homself altyd volgens sy laaste insette, en vyande - met netwerkvertraging, volgens die vorige toestand vanaf die data vanaf die bediener. Dit wil sê, wanneer hy op 'n vyand skiet, sien die speler hom in die verlede relatief tot homself. Meer oor kliënt voorspelling ons het vroeër geskryf.

Kliëntvoorspelling los dus een probleem op, maar skep 'n ander: as 'n speler skiet op die punt waar die vyand in die verlede was, op die bediener wanneer hy op dieselfde punt skiet, is die vyand dalk nie meer op daardie plek nie. Bedienerlagvergoeding poog om hierdie probleem op te los. Wanneer 'n wapen afgevuur word, herstel die bediener die speltoestand wat die speler plaaslik gesien het ten tyde van die skoot, en kyk of hy werklik die vyand kon getref het. As die antwoord "ja" is, word die treffer getel, selfs al is die vyand nie meer op die bediener op daardie stadium nie.

Gewapen met hierdie kennis, het ons begin om bedienerlagvergoeding in Dino Squad te implementeer. Eerstens moes ons verstaan ​​hoe om op die bediener te herstel wat die kliënt gesien het? En wat presies moet herstel word? In ons spel word treffers van wapens en vermoëns bereken deur strale en oorlegsels - dit wil sê deur interaksies met die vyand se fisiese botsers. Gevolglik moes ons die posisie van hierdie botsers, wat die speler plaaslik "gesien" het, op die bediener weergee. Op daardie stadium het ons Unity weergawe 2018.x gebruik. Die fisika API daar is staties, die fisiese wêreld bestaan ​​in 'n enkele kopie. Daar is geen manier om sy toestand te stoor en dit dan uit die boks te herstel nie. So wat om te doen?

Die oplossing was op die oppervlak; al die elemente daarvan is reeds deur ons gebruik om ander probleme op te los:

  1. Vir elke kliënt moet ons weet op watter tydstip hy teenstanders gesien het toe hy die sleutels gedruk het. Ons het reeds hierdie inligting in die invoerpakket geskryf en dit gebruik om die kliëntvoorspelling aan te pas.
  2. Ons moet die geskiedenis van spelstate kan stoor. Dit is daarin dat ons die posisies van ons teenstanders (en dus hul botsers) sal beklee. Ons het reeds 'n staatsgeskiedenis op die bediener gehad, ons het dit gebruik om te bou deltas. As ons die regte tyd ken, kan ons maklik die regte toestand in die geskiedenis vind.
  3. Noudat ons die speltoestand uit die geskiedenis in die hand het, moet ons spelerdata met die toestand van die fisiese wêreld kan sinchroniseer. Bestaande botsers - beweeg, ontbrekende - skep, onnodige - vernietig. Hierdie logika is ook reeds geskryf en het uit verskeie ECS-stelsels bestaan. Ons het dit gebruik om verskeie speletjieskamers in een Unity-proses te hou. En aangesien die fisiese wêreld een per proses is, moes dit tussen kamers hergebruik word. Voor elke tik van die simulasie het ons die toestand van die fisiese wêreld "teruggestel" en dit herinitialiseer met data vir die huidige kamer, en probeer om Unity-speletjievoorwerpe soveel as moontlik te hergebruik deur 'n slim poelstelsel. Al wat oorgebly het, was om dieselfde logika vir die spelstaat uit die verlede aan te roep.

Deur al hierdie elemente saam te voeg, het ons 'n "tydmasjien" gekry wat die toestand van die fisiese wêreld na die regte oomblik kon terugrol. Die kode blyk eenvoudig te wees:

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;
     }
}

Al wat oorgebly het, was om uit te vind hoe om hierdie masjien te gebruik om maklik vir skote en vermoëns te vergoed.

In die eenvoudigste geval, wanneer die meganika op 'n enkele trefferskandering gebaseer is, blyk alles duidelik te wees: voordat die speler skiet, moet hy die fisiese wêreld terugrol na die verlangde toestand, 'n uitsending doen, die treffer of mis tel, en die wêreld terug te bring na die aanvanklike toestand.

Maar daar is baie min sulke meganika in Dino Squad! Die meeste van die wapens in die speletjie skep projektiele – langlewende koeëls wat vir verskeie simulasiebosluise vlieg (in sommige gevalle dosyne bosluise). Wat om met hulle te doen, hoe laat moet hulle vlieg?

В antieke artikel oor die Half-Life-netwerkstapel het die ouens van Valve dieselfde vraag gevra, en hul antwoord was dit: projektielvertragingsvergoeding is problematies, en dit is beter om dit te vermy.

Ons het nie hierdie opsie gehad nie: projektiel-gebaseerde wapens was 'n sleutelkenmerk van die spelontwerp. So ons moes met iets vorendag kom. Na 'n paar dinkskrums het ons twee opsies geformuleer wat blykbaar werk:

1. Ons bind die projektiel aan die tyd van die speler wat dit geskep het. Elke regmerkie van die bedienersimulasie, vir elke koeël van elke speler, rol ons die fisiese wêreld terug na die kliënttoestand en voer die nodige berekeninge uit. Hierdie benadering het dit moontlik gemaak om 'n verspreide las op die bediener en voorspelbare vlugtyd van projektiele te hê. Voorspelbaarheid was veral vir ons belangrik, aangesien ons alle projektiele, insluitend vyandelike projektiele, op die kliënt voorspel het.

Hoe ons die meganika van ballistiese berekening vir 'n mobiele skut gemaak het met 'n netwerkvertragingsvergoedingsalgoritme
Op die prent skiet die speler by regmerkie 30 'n missiel in afwagting: hy sien in watter rigting die vyand hardloop en weet die benaderde spoed van die missiel. Plaaslik sien hy dat hy die teiken by die 33ste regmerkie getref het. Danksy vertragingsvergoeding sal dit ook op die bediener verskyn

2. Ons doen alles dieselfde as in die eerste opsie, maar nadat ons een regmerkie van die koeëlsimulasie getel het, stop ons nie, maar gaan voort om sy vlug binne dieselfde bedienermerk te simuleer, en bring elke keer sy tyd nader aan die bediener een vir een regmerkie en die opdatering van botsingsposisies. Ons doen dit totdat een van twee dinge gebeur:

  • Die koeël het verval. Dit beteken dat die berekeninge verby is, ons kan 'n mis of 'n treffer tel. En dit is by dieselfde regmerkie waarin die skoot afgevuur is! Vir ons was dit beide 'n plus en 'n minus. 'N Pluspunt - want vir die skietspeler het dit die vertraging tussen die treffer en die afname in die vyand se gesondheid aansienlik verminder. Die nadeel is dat dieselfde effek waargeneem is wanneer teenstanders op die speler geskiet het: die vyand, wil dit voorkom, het net 'n stadige vuurpyl afgevuur, en die skade was reeds getel.
  • Die koeël het bedienertyd bereik. In hierdie geval sal die simulasie daarvan voortgaan in die volgende bedienermerk sonder enige vertragingsvergoeding. Vir stadige projektiele kan dit teoreties die aantal fisika-terugskrywings verminder in vergelyking met die eerste opsie. Terselfdertyd het die ongelyke las op die simulasie toegeneem: die bediener was óf ledig, óf in een bedienermerk het dit 'n dosyn simulasiebosluise vir verskeie koeëls bereken.

Hoe ons die meganika van ballistiese berekening vir 'n mobiele skut gemaak het met 'n netwerkvertragingsvergoedingsalgoritme
Dieselfde scenario as in die vorige prent, maar bereken volgens die tweede skema. Die missiel het die bedienertyd “ingehaal” met dieselfde tik wat die skoot plaasgevind het, en die treffer kan so vroeg as die volgende tik getel word. By die 31ste regmerkie, in hierdie geval, word vertragingsvergoeding nie meer toegepas nie

In ons implementering het hierdie twee benaderings verskil in net 'n paar reëls kode, so ons het albei geskep, en vir 'n lang tyd het hulle parallel bestaan. Afhangende van die meganika van die wapen en die spoed van die koeël, het ons een of ander opsie vir elke dinosourus gekies. Die keerpunt hier was die verskyning in die spel van meganika soos "as jy die vyand soveel keer in so en so 'n tyd tref, kry so en so 'n bonus." Enige werktuigkundige waar die tydstip waarop die speler die vyand getref het 'n belangrike rol gespeel het, het geweier om met die tweede benadering te werk. So ons het uiteindelik met die eerste opsie gegaan, en dit is nou van toepassing op alle wapens en alle aktiewe vermoëns in die spel.

Afsonderlik is dit die moeite werd om die kwessie van prestasie te opper. As jy gedink het dit alles sou dinge vertraag, antwoord ek: dit is so. Unity is redelik stadig om botsers te beweeg en hulle aan en af ​​te skakel. In Dino Squad, in die "ergste" geval, kan daar 'n paar honderd projektiele gelyktydig in 'n geveg bestaan. Om botsers te beweeg om elke projektiel individueel te tel, is 'n onbekostigbare luukse. Daarom was dit absoluut noodsaaklik vir ons om die aantal fisika "terugskrywings" te minimaliseer. Om dit te doen, het ons 'n aparte komponent in ECS geskep waarin ons die speler se tyd opneem. Ons het dit bygevoeg by alle entiteite wat vertragingsvergoeding benodig (projektiele, vermoëns, ens.). Voordat ons sulke entiteite begin verwerk, groepeer ons hulle teen hierdie tyd en verwerk hulle saam, en rol die fisiese wêreld een keer terug vir elke groepering.

Op hierdie stadium het ons 'n algemeen werkende stelsel. Sy kode in 'n ietwat vereenvoudigde vorm:

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);
               }
          }
     }
}

Al wat oorgebly het, was om die besonderhede op te stel:

1. Verstaan ​​hoeveel om die maksimum afstand van beweging in tyd te beperk.

Dit was vir ons belangrik om die speletjie so toeganklik as moontlik te maak in toestande van swak mobiele netwerke, so ons het die storie beperk met 'n marge van 30 regmerkies (met 'n tiktempo van 20 Hz). Dit laat spelers toe om teenstanders selfs met baie hoë pings te slaan.

2. Bepaal watter voorwerpe betyds verskuif kan word en watter nie.

Ons beweeg natuurlik ons ​​teenstanders. Maar installeerbare energieskerms is byvoorbeeld nie. Ons het besluit dat dit beter is om prioriteit te gee aan die verdedigingsvermoë, soos dikwels in aanlyn skuts gedoen word. As die speler reeds 'n skild in die hede geplaas het, moet lag-kompenseerde koeëls uit die verlede nie daardeur vlieg nie.

3. Besluit of dit nodig is om te vergoed vir die dinosourusse se vermoëns: byt, stertslaan, ens. Ons het besluit wat nodig is en verwerk dit volgens dieselfde reëls as koeëls.

4. Bepaal wat om te doen met die botsers van die speler vir wie vertragingsvergoeding uitgevoer word. Op 'n goeie manier moet hul posisie nie na die verlede verskuif nie: die speler moet homself sien in dieselfde tyd waarin hy nou op die bediener is. Ons rol egter ook die botsers van die skietspeler terug, en daar is verskeie redes hiervoor.

Eerstens verbeter dit groepering: ons kan dieselfde fisiese toestand gebruik vir alle spelers met noue pings.

Tweedens, in alle strale en oorvleuelings sluit ons altyd die botsers uit van die speler wat die vermoëns of projektiele besit. In Dino Squad beheer spelers dinosourusse, wat volgens skutstandaarde taamlik nie-standaard meetkunde het. Selfs as die speler teen 'n ongewone hoek skiet en die koeël se trajek gaan deur die speler se dinosourusbotser, sal die koeël dit ignoreer.

Derdens bereken ons die posisies van die dinosourus se wapen of die punt van toepassing van die vermoë deur data van die ECS te gebruik selfs voor die aanvang van vertragingsvergoeding.

Gevolglik is die werklike posisie van die botsers van die lag-kompenseerde speler vir ons onbelangrik, so ons het 'n meer produktiewe en terselfdertyd eenvoudiger pad geneem.

Netwerkvertraging kan nie eenvoudig verwyder word nie, dit kan net gemasker word. Soos enige ander metode van vermomming, het vergoeding vir bedienervertraging sy nadele. Dit verbeter die spelervaring van die speler wat skiet ten koste van die speler op wie geskiet word. Vir Dino Squad was die keuse hier egter voor die hand liggend.

Dit alles moes natuurlik ook betaal word deur die verhoogde kompleksiteit van die bedienerkode as geheel – beide vir programmeerders en speletjie-ontwerpers. As die simulasie vroeër 'n eenvoudige opeenvolgende oproep van stelsels was, dan het geneste lusse en takke met vertragingsvergoeding daarin verskyn. Ons het ook baie moeite gedoen om dit gerieflik te maak om mee te werk.

In die 2019-weergawe (en miskien 'n bietjie vroeër), het Unity volle ondersteuning vir onafhanklike fisieke tonele bygevoeg. Ons het dit byna onmiddellik na die opdatering op die bediener geïmplementeer, want ons wou vinnig ontslae raak van die fisiese wêreld wat algemeen in alle kamers is.

Ons het elke speelkamer sy eie fisiese toneel gegee en sodoende die behoefte uitgeskakel om die toneel uit die data van die naburige kamer te "vee" voordat die simulasie bereken word. Eerstens het dit 'n aansienlike toename in produktiwiteit gegee. Tweedens het dit dit moontlik gemaak om ontslae te raak van 'n hele klas foute wat ontstaan ​​het as die programmeerder 'n fout in die toneelskoonmaakkode gemaak het wanneer nuwe speletjie-elemente bygevoeg is. Sulke foute was moeilik om te ontfout, en dit het dikwels daartoe gelei dat die toestand van fisiese voorwerpe in een kamer se toneel na 'n ander kamer "vloei".

Daarbenewens het ons navorsing gedoen oor of fisiese tonele gebruik kan word om die geskiedenis van die fisiese wêreld te stoor. Dit wil sê, ken voorwaardelik nie een toneel aan elke kamer toe nie, maar 30 tonele, en maak 'n sikliese buffer daarvan om die storie te stoor. Oor die algemeen het die opsie geblyk te werk, maar ons het dit nie geïmplementeer nie: dit het geen gekke toename in produktiwiteit getoon nie, maar het taamlik riskante veranderinge vereis. Dit was moeilik om te voorspel hoe die bediener sou optree as jy vir 'n lang tyd met soveel tonele werk. Daarom het ons die reël gevolg: "Moet dit nie regmaak as dit nie stukkend is nie".

Bron: will.com

Voeg 'n opmerking