Nola hobetu genuen jaurtitzaile mugikor baten kalkulu balistikoen mekanika sareko latentzia konpentsatzeko algoritmo batekin

Nola hobetu genuen jaurtitzaile mugikor baten kalkulu balistikoen mekanika sareko latentzia konpentsatzeko algoritmo batekin

Kaixo, Nikita Brizhak naiz, Pixonic-eko zerbitzari garatzailea. Gaurkoan mugikorreko jokalari anitzeko lag-a konpentsatzeari buruz hitz egin nahiko nuke.

Artikulu asko idatzi dira zerbitzariaren atzerapenaren konpentsazioari buruz, errusieraz barne. Ez da harritzekoa, teknologia hau 90eko hamarkadaren amaieratik jokalari anitzeko FPS sortzeko modu aktiboan erabili baita. Esate baterako, QuakeWorld mod-a gogoratu dezakezu, hura erabiltzen lehenetariko bat izan zena.

Dino Squad gure mugikorreko jokalari anitzeko jaurtitzailean ere erabiltzen dugu.

Artikulu honetan, nire helburua ez da jada idatzitakoa mila aldiz errepikatzea, baizik eta gure jokoan lag-konpentsazioa nola ezarri genuen kontatzea, gure teknologia pila eta jokatzeko oinarrizko ezaugarriak kontuan hartuta.

Gure kortexari eta teknologiari buruzko hitz batzuk.

Dino Squad sare mugikorreko PvP jaurtitzailea da. Jokalariek hainbat armaz hornitutako dinosauroak kontrolatzen dituzte eta elkarren aurka borrokatzen dute 6v6 taldeetan.

Bezeroa eta zerbitzaria Unity-n oinarritzen dira. Arkitektura nahiko klasikoa da jaurtitzaileentzat: zerbitzaria autoritarioa da eta bezeroen iragarpena bezeroekin funtzionatzen du. Joko-simulazioa barneko ECS erabiliz idatzita dago eta zerbitzarian zein bezeroan erabiltzen da.

Lag-konpentsazioari buruz entzuten duzun lehen aldia bada, hona hemen gaiari buruzko txango labur bat.

Jokalari anitzeko FPS jokoetan, partida urruneko zerbitzari batean simulatu ohi da. Jokalariek zerbitzariari bidaltzen diote sarrera (sakatutako teklei buruzko informazioa), eta erantzunez zerbitzariak jokoaren egoera eguneratua bidaltzen die jasotako datuak kontuan hartuta. Interakzio-eskema honekin, aurrera egiteko tekla sakatzearen eta jokalariaren pertsonaia pantailan mugitzen den momentuaren arteko atzerapena ping-a baino handiagoa izango da beti.

Sare lokaletan atzerapen hori (sarrerako atzerapena deitzen dena) nabariezina izan daitekeen arren, Internet bidez jokatzean, pertsonaia bat kontrolatzerakoan "izotz gainean irristatzearen" sentsazioa sortzen du. Arazo hau bikoitza da garrantzitsua sare mugikorretarako, non jokalari baten ping-a 200 ms-koa denean oraindik konexio bikaintzat hartzen den. Askotan ping-a 350, 500 edo 1000 ms izan daiteke. Orduan, ia ezinezkoa bihurtzen da sarrerako atzerapenarekin jaurtitzaile azkar batean jokatzea.

Arazo honen irtenbidea bezeroaren aldetik simulazio-iragarpena da. Hemen bezeroak berak aplikatzen dio sarrera jokalariaren pertsonaiari, zerbitzariaren erantzunaren zain egon gabe. Eta erantzuna jasotzen denean, emaitzak alderatu eta aurkarien posizioak eguneratu besterik ez du egiten. Kasu honetan, tekla bat sakatu eta emaitza pantailan bistaratzeko arteko atzerapena gutxienekoa da.

Garrantzitsua da Γ±abardura ulertzea hemen: bezeroak bere azken sarreraren arabera marrazten du beti, eta etsaiak - sareko atzerapenarekin, zerbitzariaren datuen aurreko egoeraren arabera. Hau da, etsai bati tiro egiten duenean, jokalariak iraganean ikusten du bere buruarekiko. Bezeroen iragarpenari buruz gehiago lehenago idatzi genuen.

Horrela, bezeroen iragarpenak arazo bat konpontzen du, baina beste bat sortzen du: jokalari batek iraganean etsaia zegoen puntuan tiro egiten badu, zerbitzarian puntu berean tiro egitean, baliteke etsaia gehiago leku horretan ez egotea. Zerbitzariaren atzerapenaren konpentsazioa arazo hau konpontzen saiatzen da. Arma bat tiro egiten denean, zerbitzariak jokalariak tiroaren unean lokalean ikusi zuen joko-egoera berrezartzen du eta etsaia benetan jo zezakeen ala ez egiaztatzen du. Erantzuna "bai" bada, kolpea zenbatuko da, nahiz eta une horretan etsaia zerbitzarian ez egon.

Ezagutza horrekin hornituta, zerbitzariaren atzerapenaren konpentsazioa ezartzen hasi ginen Dino Squad-en. Lehenik eta behin, bezeroak ikusitakoa zerbitzarian nola berreskuratu ulertu behar genuen? Eta zehazki zer berreskuratu behar da? Gure jokoan, armen eta gaitasunen kolpeak raycast eta gainjartzeen bidez kalkulatzen dira, hau da, etsaien talkatzaile fisikoekin elkarrekintzen bidez. Horren arabera, jokalariak lokalean "ikusten" zuen talkagailu horien posizioa erreproduzitu behar genuen zerbitzarian. Garai hartan Unity 2018.x bertsioa erabiltzen ari ginen. Bertan fisikako APIa estatikoa da, mundu fisikoa kopia bakarrean dago. Ez dago modurik bere egoera gorde eta gero kutxatik leheneratu. Beraz, zer egin?

Irtenbidea azalean zegoen; bere elementu guztiak jada erabili genituen beste arazo batzuk konpontzeko:

  1. Bezero bakoitzeko, teklak sakatzean aurkariak zein ordutan ikusi zituen jakin behar dugu. Dagoeneko informazio hau sarrerako paketean idatzi dugu eta bezeroaren iragarpena doitzeko erabili dugu.
  2. Joko-egoeren historia gordetzeko gai izan behar dugu. Bertan eutsiko diegu aurkarien (eta, beraz, talkatzaileen) posizioei. Dagoeneko zerbitzarian egoera-historia genuen, eraikitzeko erabili genuen deltak. Garai egokia ezagututa, erraz aurki genezake historian egoera egokia.
  3. Orain historiako joko-egoera esku artean dugula, jokalariaren datuak mundu fisikoaren egoerarekin sinkronizatzeko gai izan behar dugu. Dauden talkatzaileak - mugitu, falta direnak - sortu, beharrezkoak ez direnak - suntsitu. Logika hori ere jada idatzita zegoen eta hainbat ECS sistemak osatzen zuten. Hainbat joko-gela edukitzeko erabili genuen Unity prozesu batean. Eta mundu fisikoa prozesu bakoitzeko bat denez, gelen artean berrerabili behar zen. Simulazioaren marka bakoitzaren aurretik, mundu fisikoaren egoera "berrezarri" dugu eta egungo gelako datuekin berrezarri dugu, Unity joko-objektuak ahalik eta gehien berrerabili nahian bilketa-sistema adimentsu baten bidez. Iraganeko joko egoerarako logika bera deitzea besterik ez zen geratzen.

Elementu horiek guztiak elkartuz, mundu fisikoaren egoera momentu egokira itzul zezakeen "denboraren makina" bat lortu genuen. Kodea sinplea izan zen:

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

Makina hau nola erabili jaurtiketak eta trebetasunak erraz konpentsatzeko asmatzea besterik ez zen geratzen.

Kasurik errazenean, mekanika hitscan bakar batean oinarritzen denean, dena argi dagoela dirudi: jokalariak tiro egin baino lehen, mundu fisikoa nahi duen egoerara itzuli behar du, raycast bat egin, hit edo falta zenbatu behar du, eta mundua hasierako egoerara itzuli.

Baina horrelako mekanika gutxi daude Dino Squad-en! Jokoaren arma gehienek proiektilak sortzen dituzte: hainbat simulazio-tick (kasu batzuetan, dozenaka tick) hegan egiten duten iraupen luzeko balak. Zer egin haiekin, zein ordutan egin behar dute hegan?

Π’ antzinako artikulua Half-Life sareko pilari buruz, Valve-ko ​​mutilek galdera bera egin zuten, eta hau izan zen erantzuna: proiektilaren atzerapenaren konpentsazioa arazotsua da, eta hobe da hori saihestea.

Ez genuen aukera hau: proiektiletan oinarritutako armak ziren jokoaren diseinuaren funtsezko ezaugarria. Beraz, zerbait asmatu behar genuen. Zenbait ideia-jasa egin ondoren, funtzionatzen zirudien bi aukera planteatu genituen:

1. Proyectila sortu duen jokalariaren denborarekin lotzen dugu. Zerbitzariaren simulazioaren marka bakoitza, jokalari bakoitzaren bala bakoitzeko, mundu fisikoa bezeroaren egoerara itzultzen dugu eta beharrezko kalkuluak egiten ditugu. Planteamendu honek zerbitzarian karga banatua eta proiektilen hegaldi denbora aurreikusgarria izatea ahalbidetu zuen. Aurreikusgarritasuna bereziki garrantzitsua zen guretzat, proiektil guztiak, etsaien jaurtiketak barne, bezeroari aurreikusten baititugu.

Nola hobetu genuen jaurtitzaile mugikor baten kalkulu balistikoen mekanika sareko latentzia konpentsatzeko algoritmo batekin
Irudian, 30. tick-eko jokalariak misil bat botatzen du aurreikuspenean: etsaia zein norabidetan dabilen ikusten du eta misilaren gutxi gorabeherako abiadura ezagutzen du. Lokalean ikusten du 33. tickean jo zuela diana. Lag-konpentsazioari esker, zerbitzarian ere agertuko da

2. Lehenengo aukeran bezala egiten dugu dena, baina, bala simulazioaren tick bat zenbatuta, ez dugu gelditzen, zerbitzariaren tick berdinaren barruan bere hegaldia simulatzen jarraitzen dugu, aldi bakoitzean bere denbora zerbitzarira hurbilduz. banan-banan markatu eta talkagailuen posizioak eguneratzen. Hau egiten dugu bi gauza hauetako bat gertatu arte:

  • Bala iraungi da. Horrek esan nahi du kalkuluak amaitu direla, huts edo hit bat zenbatu dezakegula. Eta hau tiroa egin zen tick berean dago! Guretzat hau plus bat eta minus bat izan zen. Plus bat - tiro jokalariarentzat horrek nabarmen murriztu zuelako kolpearen eta etsaiaren osasunaren gutxitzearen arteko atzerapena. Alde txarra da aurkariek jokalariari tiro egiten ziotenean efektu bera antzematen zela: etsaiak, antza denez, suziri motel bat besterik ez zuen jaurti, eta kalteak zenbatuta zeuden jada.
  • Bala zerbitzariaren ordura iritsi da. Kasu honetan, bere simulazioa hurrengo zerbitzariaren markan jarraituko du atzerapen-konpentsaziorik gabe. Proyectil moteletarako, teorikoki, fisikako atzerapenen kopurua murriztu liteke lehen aukerarekin alderatuta. Aldi berean, simulazioaren karga irregularra handitu egin zen: zerbitzaria inaktibo zegoen, edo zerbitzari bateko tick batean simulazioko hamaika tick kalkulatzen zituen hainbat baletarako.

Nola hobetu genuen jaurtitzaile mugikor baten kalkulu balistikoen mekanika sareko latentzia konpentsatzeko algoritmo batekin
Aurreko irudiko eszenatoki bera, baina bigarren eskemaren arabera kalkulatua. Misilak zerbitzariaren denborarekin "harrapatu" zuen tiroa gertatu zen tick berean, eta kolpea hurrengo tick bezain goiz zenbatu daiteke. 31. tick-ean, kasu honetan, atzerapenaren konpentsazioa ez da aplikatzen

Gure inplementazioan, bi ikuspegi hauek kode-lerro pare batean desberdinak ziren, beraz, biak sortu genituen, eta denbora luzez paraleloan existitu ziren. Armaren mekanikaren eta balaren abiaduraren arabera, dinosauro bakoitzarentzat aukera bat edo beste aukeratu genuen. Hemen inflexio-puntua mekanika jokoan agertzea izan zen: "Halako eta halako garai batean etsaia hainbeste aldiz jotzen baduzu, lortu halako eta halako bonus bat". Jokalariak etsaia jo zuen denborak paper garrantzitsua betetzen zuen edozein mekanikari uko egin zion bigarren hurbilketarekin lan egiteari. Beraz, azkenean lehen aukerarekin joan ginen, eta orain jokoan dauden arma eta gaitasun aktibo guztiei aplikatzen zaie.

Bereiz, merezi du errendimenduaren gaia planteatzea. Horrek guztiak gauzak motelduko zituela uste bazenuen, erantzuten dizut: hala da. Unity nahiko motela da talkatzaileak mugitzen eta pizten eta itzaltzen. Dino Squad-en, kasu "txarrenean", hainbat ehunka proiektil egon daitezke aldi berean borrokan. Talkagailuak mugitzea, proiektil bakoitza banaka zenbatzeko, luxu ezinezkoa da. Hori dela eta, guztiz beharrezkoa zen guretzat fisikako "erretorren" kopurua gutxitzea. Horretarako, osagai bereizi bat sortu dugu ECSn, eta bertan erreproduzitzailearen denbora grabatzen dugu. Lag-konpentsazioa eskatzen duten entitate guztietan gehitu dugu (proiektilak, gaitasunak, etab.). Horrelako entitateak prozesatzen hasi baino lehen, ordurako multzokatzen ditugu eta elkarrekin prozesatzen ditugu, mundu fisikoa kluster bakoitzeko behin atzera botaz.

Fase honetan, orokorrean funtzionatzen duen sistema dugu. Bere kodea forma zertxobait sinplifikatu batean:

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

Xehetasunak konfiguratzea besterik ez zen falta:

1. Mugimenduaren distantzia maximoa denboran zenbat mugatu behar den ulertzea.

Garrantzitsua zen guretzat jokua ahalik eta eskuragarriena izatea sare mugikor eskasetako baldintzetan, beraz istorioa 30 tick-ko marjina batekin mugatu genuen (20 Hz-ko tick-tasa batekin). Horri esker, jokalariek aurkariak jo ditzakete ping oso altuetan ere.

2. Zehaztu zein objektu mugitu daitezkeen denboran eta zeintzuk ez.

Gu, noski, aurkariak mugitzen ari gara. Baina instalagarriak diren energia-ezkutuak, adibidez, ez dira. Erabaki genuen hobe zela defentsarako gaitasunari lehentasuna ematea, sareko jaurtiketetan egin ohi den bezala. Jokalariak jada ezkutu bat jarri badu orainaldian, iraganeko lag-konpentsatutako balek ez lukete bertatik igaroko.

3. Dinosauroen gaitasunak konpentsatzea beharrezkoa den erabakitzea: ziztada, buztan kolpea, etab. Behar zena erabaki genuen eta balen arau berberen arabera prozesatu genuen.

4. Zehaztu zer egin behar den desfasearen konpentsazioa egiten ari den jokalariaren talkagailuekin. Modu onean, haien posizioak ez luke iraganera aldatu behar: jokalariak bere burua ikusi behar du orain zerbitzarian dagoen denbora berean. Hala ere, jaurtitzailearen talkagailuak ere atzera botatzen ditugu, eta hainbat arrazoi daude horretarako.

Lehenik eta behin, clustering hobetzen du: egoera fisiko bera erabil dezakegu ping itxiak dituzten jokalari guztientzat.

Bigarrenik, raycast eta gainjartze guztietan beti baztertzen ditugu gaitasunak edo proiektilak dituen jokalariaren talkatzaileak. Dino Squad-en, jokalariek dinosauroak kontrolatzen dituzte, jaurtitzaileen estandarren arabera geometria ez-estandar samarra dutenak. Jokalariak ezohiko angelu batean tiro egiten badu eta balaren ibilbidea jokalariaren dinosauroen talkagailutik igarotzen bada ere, balak ez du aintzat hartuko.

Hirugarrenik, dinosauroaren armaren posizioak edo gaitasunaren aplikazio-puntua kalkulatzen ditugu ECSko datuak erabiliz, atzerapenaren konpentsazioa hasi aurretik ere.

Ondorioz, lag-konpentsatutako jokalariaren talkatzaileen benetako posizioak ez du garrantzirik guretzat, beraz, bide emankorragoa eta aldi berean sinpleagoa hartu genuen.

Sareko latentzia ezin da kendu besterik gabe, ezkutatu besterik ez dago. Mozorrotzeko beste edozein metodo bezala, zerbitzariaren atzerapenaren konpentsazioak bere aldeak ditu. Tiroa egiten ari den jokalariaren joko-esperientzia hobetzen du tirokatzen ari den jokalariaren kontura. Dino Squadrentzat, ordea, hemen hautua begi-bistakoa zen.

Jakina, hori guztia zerbitzariaren kodearen konplexutasun handiagoarekin ere ordaindu behar izan zen, bai programatzaileentzat bai joko-diseinatzaileentzat. Lehenago simulazioa sistemen dei sekuentzial soil bat bazen, orduan atzerapenaren konpentsazioarekin, begiztak eta adarrak agertu ziren bertan. Gainera, ahalegin handia egin dugu lan egiteko erosoa izan dadin.

2019ko bertsioan (eta agian pixka bat lehenago), Unity-k eszena fisiko independenteetarako laguntza osoa gehitu zuen. Eguneratu eta berehala zerbitzarian ezarri genituen, gela guztietan ohikoa den mundu fisikoa azkar kendu nahi genuelako.

Jolas-gela bakoitzari bere eszena fisikoa eman genion eta horrela simulazioa kalkulatu aurretik aldameneko gelako datuetatik eszena "garbitzeko" beharra ezabatu genuen. Lehenik eta behin, produktibitatearen igoera nabarmena eman zuen. Bigarrenik, programatzaileak eszena garbitzeko kodean akats bat egiten bazuen joko-elementu berriak gehitzean sortzen ziren akatsen klase osoa kentzea ahalbidetu zuen. Horrelako akatsak arazketa zailak ziren, eta askotan gela bateko eszenako objektu fisikoen egoera beste gela batera "isaria" egiten zuten.

Horrez gain, ikerketa batzuk egin genituen mundu fisikoaren historia gordetzeko eszena fisikoak erabil zitezkeen ala ez. Hau da, baldintzapean, gela bakoitzari ez eszena bat esleitu, 30 eszena baizik, eta horiekin buffer zikliko bat egin, bertan istorioa gordetzeko. Oro har, aukerak funtzionatzen zuen, baina ez genuen inplementatu: ez zuen produktibitatearen igoera zororik erakutsi, aldaketa arriskutsu samarrak behar zituen. Zaila zen aurreikustea zerbitzariak nola jokatuko zuen denbora luzez hainbeste eszenarekin lan egitean. Horregatik, araua jarraitu genuen: "Ez bada hautsi, ez konpondu'.

Iturria: www.habr.com

Gehitu iruzkin berria