Hvernig við bættum aflfræði ballistískra útreikninga fyrir farsímaskotleik með reiknirit fyrir uppbót fyrir netleynd

Hvernig við bættum aflfræði ballistískra útreikninga fyrir farsímaskotleik með reiknirit fyrir uppbót fyrir netleynd

Hæ, ég er Nikita Brizhak, netþjónaframleiðandi frá Pixonic. Í dag langar mig að tala um að bæta upp töf í fjölspilun farsíma.

Margar greinar hafa verið skrifaðar um biðbætur fyrir netþjóna, þar á meðal á rússnesku. Þetta kemur ekki á óvart, þar sem þessi tækni hefur verið virkan notuð við gerð fjölspilunar FPS síðan seint á tíunda áratugnum. Til dæmis er hægt að muna eftir QuakeWorld modinu, sem var eitt af þeim fyrstu til að nota það.

Við notum það líka í farsíma fjölspilunarskyttunni Dino Squad.

Í þessari grein er markmið mitt ekki að endurtaka það sem þegar hefur verið skrifað þúsund sinnum, heldur að segja hvernig við innleiddum töf bætur í leiknum okkar, að teknu tilliti til tæknistafla okkar og kjarna leikjaeiginleika.

Nokkur orð um heilaberki okkar og tækni.

Dino Squad er net farsíma PvP skotleikur. Leikmenn stjórna risaeðlum sem eru búnar ýmsum vopnum og berjast hver við annan í 6v6 liðum.

Bæði viðskiptavinurinn og þjónninn eru byggðir á Unity. Arkitektúrinn er frekar klassískur fyrir skotmenn: þjónninn er auðvaldslegur og spá viðskiptavinarins virkar á viðskiptavinina. Leikjahermingin er skrifuð með ECS innanhúss og er notuð bæði á þjóninum og viðskiptavininum.

Ef þetta er í fyrsta skipti sem þú heyrir um töfabætur, þá er hér stutt skoðunarferð um málið.

Í fjölspilunar FPS leikjum er leikurinn venjulega hermdur á ytri netþjóni. Spilarar senda inntak sitt (upplýsingar um takkana sem ýtt er á) til netþjónsins og sem svar sendir þjónninn þeim uppfærða leikstöðu sem tekur tillit til móttekinna gagna. Með þessu víxlverkunarkerfi verður töfin frá því að ýta á áfram takkann og þess augnabliks sem spilarapersónan hreyfist á skjánum alltaf meiri en pingið.

Þó að á staðarnetum sé þessi töf (almennt kölluð innsláttartöf) kannski ómerkjanleg, þegar spilað er í gegnum internetið skapar það tilfinningu um að „renna á ís“ þegar maður stjórnar persónu. Þetta vandamál er tvöfalt viðeigandi fyrir farsímakerfi, þar sem tilvikið þegar ping leikmanns er 200 ms er enn talið frábær tenging. Oft getur pingið verið 350, 500 eða 1000 ms. Þá verður næstum ómögulegt að spila hraða skyttu með input lag.

Lausnin á þessu vandamáli er uppgerðarspá viðskiptavinarhliðar. Hér beitir viðskiptavinurinn sjálfur inntakinu á leikmanninn, án þess að bíða eftir svari frá þjóninum. Og þegar svarið berst ber það einfaldlega saman úrslitin og uppfærir stöðu andstæðinganna. Töfin milli þess að ýta á takka og birta niðurstöðuna á skjánum í þessu tilfelli er lítil.

Það er mikilvægt að skilja blæbrigðin hér: viðskiptavinurinn dregur sig alltaf í samræmi við síðasta inntak hans og óvinir - með nettöf, samkvæmt fyrra ástandi frá gögnum frá þjóninum. Það er að segja að þegar hann skýtur á óvin sér leikmaðurinn hann í fortíðinni miðað við sjálfan sig. Meira um spá viðskiptavina við skrifuðum áðan.

Þannig leysir spá viðskiptavinar eitt vandamál, en skapar annað: ef leikmaður skýtur á þeim stað þar sem óvinurinn var í fortíðinni, á þjóninum þegar hann skýtur á sama stað, getur verið að óvinurinn sé ekki lengur á þeim stað. Töf bætur fyrir netþjóna reyna að leysa þetta vandamál. Þegar vopni er hleypt af, endurheimtir þjónninn leiksástandið sem leikmaðurinn sá á staðnum þegar skotið var, og athugar hvort hann hafi raunverulega getað lent í óvininum. Ef svarið er „já“ er höggið talið, jafnvel þó að óvinurinn sé ekki lengur á þjóninum á þeim tímapunkti.

Vopnaðir þessari þekkingu byrjuðum við að innleiða töf bætur fyrir netþjóna í Dino Squad. Fyrst af öllu þurftum við að skilja hvernig á að endurheimta á þjóninum það sem viðskiptavinurinn sá? Og hvað nákvæmlega þarf að endurheimta? Í leiknum okkar eru högg frá vopnum og hæfileikum reiknuð út með geislavörpum og yfirlagi - það er í gegnum samskipti við líkamlega árekstur óvinarins. Í samræmi við það þurftum við að endurskapa stöðu þessara kollidera, sem spilarinn „sá“ á staðnum, á þjóninum. Á þeim tíma vorum við að nota Unity útgáfu 2018.x. Eðlisfræði-API þar er kyrrstætt, efnisheimurinn er til í einu eintaki. Það er engin leið til að vista ástand þess og endurheimta það síðan úr kassanum. Svo hvað á að gera?

Lausnin var á yfirborðinu; allir þættir hennar höfðu þegar verið notaðir af okkur til að leysa önnur vandamál:

  1. Fyrir hvern viðskiptavin þurfum við að vita hvenær hann sá andstæðinga þegar hann ýtti á takkana. Við höfum þegar skrifað þessar upplýsingar inn í inntakspakkann og notað þær til að stilla spá viðskiptavinarins.
  2. Við þurfum að geta geymt sögu leikríkja. Það er í henni sem við munum halda stöðu andstæðinga okkar (og þar með árekstra þeirra). Við höfðum þegar ástandsferil á þjóninum, við notuðum hann til að byggja upp deltas. Með því að vita rétta tímann gætum við auðveldlega fundið rétta ástandið í sögunni.
  3. Nú þegar við erum með leikjaástandið úr sögunni í höndunum þurfum við að geta samstillt gögn leikmanna við ástand líkamlegs heims. Núverandi árekstrar - hreyfa, vantar - búa til, óþarfa - eyðileggja. Þessi rökfræði var líka þegar skrifuð og samanstóð af nokkrum ECS kerfum. Við notuðum það til að halda nokkrum leikjaherbergjum í einu Unity ferli. Og þar sem efnisheimurinn er einn í hverju ferli, þurfti að endurnýta hann á milli herbergja. Fyrir hverja merkingu í uppgerðinni „endurstillum“ við ástand efnisheimsins og frumstillum það aftur með gögnum fyrir núverandi herbergi og reynum að endurnýta Unity leikjahluti eins mikið og mögulegt er með snjöllu samsetningarkerfi. Það eina sem var eftir var að kalla fram sömu rökfræði fyrir leikjaástandið frá fortíðinni.

Með því að setja alla þessa þætti saman fengum við „tímavél“ sem gæti snúið ástandi efnisheimsins aftur á réttan tíma. Kóðinn reyndist einfaldur:

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

Það eina sem var eftir var að finna út hvernig á að nota þessa vél til að bæta auðveldlega upp skot og hæfileika.

Í einfaldasta tilfellinu, þegar vélfræðin er byggð á einni höggskönnun, virðist allt vera á hreinu: áður en leikmaðurinn skýtur þarf hann að snúa líkamlega heiminum til baka í æskilegt ástand, gera raycast, telja högg eða miss, og koma heiminum aftur í upphafsástand.

En það eru mjög fáir slíkir vélvirkjar í Dino Squad! Flest vopnin í leiknum búa til skotfæri - langlífar byssukúlur sem fljúga fyrir nokkrum eftirlíkingartikkum (í sumum tilfellum heilmikið af merkjum). Hvað á að gera við þá, hvenær ættu þeir að fljúga?

В forn grein um Half-Life netstaflann spurðu krakkar frá Valve sömu spurningu og svarið þeirra var þetta: töf á skotfæri er vandamál og það er betra að forðast það.

Við höfðum ekki þennan valmöguleika: vopn byggð á skotvopnum voru lykilatriði í hönnun leiksins. Svo við urðum að finna upp á einhverju. Eftir smá hugarflug mótuðum við tvo valkosti sem virtust virka:

1. Við bindum skotið við tíma leikmannsins sem bjó það til. Sérhver merking á netþjónsuppgerðinni, fyrir hverja byssukúlu hvers leikmanns, snúum við líkamlegum heimi til baka í biðlarastöðu og framkvæmum nauðsynlega útreikninga. Þessi nálgun gerði það að verkum að hægt var að hafa dreift álag á netþjóninn og fyrirsjáanlegan flugtíma skotvopna. Fyrirsjáanleiki var sérstaklega mikilvægur fyrir okkur, þar sem við erum með öll skot, þar á meðal óvinaskot, spáð á viðskiptavininn.

Hvernig við bættum aflfræði ballistískra útreikninga fyrir farsímaskotleik með reiknirit fyrir uppbót fyrir netleynd
Á myndinni skýtur leikmaðurinn við merkið 30 flugskeyti í eftirvæntingu: hann sér í hvaða átt óvinurinn hleypur og veit áætlaða hraða eldflaugarinnar. Staðbundið sér hann að hann hitti markið á 33. tikkinu. Þökk sé töfabótum mun það einnig birtast á þjóninum

2. Við gerum allt eins og í fyrsta valmöguleikanum, en eftir að hafa talið eitt hak af skothermi, hættum við ekki, heldur höldum áfram að líkja eftir flugi þess innan sama miðlaramerkið, í hvert skipti sem tími hans færumst nær þjóninum einn í einu hak og uppfærsla á riðilstöðvum. Við gerum þetta þar til annað af tvennu gerist:

  • Byssukúlan er útrunnin. Þetta þýðir að útreikningum er lokið, við getum talið miss eða högg. Og þetta er á sama merkinu og skotið var í! Fyrir okkur var þetta bæði plús og mínus. Plús - vegna þess að fyrir skotleikmanninn minnkaði þetta verulega seinkunina á milli höggsins og heilsu óvinarins. Gallinn er sá að sömu áhrif komu fram þegar andstæðingar skutu á leikmanninn: óvinurinn, að því er virðist, skaut aðeins hægfara eldflaug og skaðinn var þegar talinn.
  • Byssukúlan er komin á netþjónstíma. Í þessu tilviki mun eftirlíking þess halda áfram í næsta miðlaramerki án þess að töf sé greidd. Fyrir hægar skotfæri gæti þetta fræðilega fækkað fjölda endurfalla í eðlisfræði miðað við fyrsta valkostinn. Á sama tíma jókst ójafnt álag á uppgerðina: þjónninn var annaðhvort aðgerðalaus eða í einum miðlaratikki var hann að reikna út tugi uppgerðamerkja fyrir nokkrar byssukúlur.

Hvernig við bættum aflfræði ballistískra útreikninga fyrir farsímaskotleik með reiknirit fyrir uppbót fyrir netleynd
Sama atburðarás og á fyrri myndinni, en reiknuð samkvæmt öðru skema. Eldflaugin „náði“ tíma þjónsins á sama merkinu og skotið átti sér stað og hægt er að telja höggið strax í næsta hak. Við 31. hak, í þessu tilviki, er töfabótum ekki lengur beitt

Í útfærslu okkar voru þessar tvær aðferðir ólíkar í aðeins nokkrum línum af kóða, þannig að við bjuggum til báðar, og í langan tíma voru þær til samhliða. Það fer eftir aflfræði vopnsins og hraða skotsins, við völdum einn eða annan kost fyrir hverja risaeðlu. Vendipunkturinn hér var framkoma í vélfræðileiknum eins og "ef þú lendir á óvininum svo oft á svona og svo tíma, fáðu svo og svo bónus." Sérhver vélvirki þar sem tíminn þegar leikmaður sló á óvininn gegndi mikilvægu hlutverki neitaði að vinna með seinni nálguninni. Þannig að við enduðum með fyrsta valmöguleikann og hann á nú við um öll vopn og alla virka hæfileika í leiknum.

Sérstaklega er þess virði að vekja máls á frammistöðu. Ef þú hélst að allt þetta myndi hægja á hlutunum svara ég: það er það. Unity er frekar hægt að færa kollidera og kveikja og slökkva á þeim. Í Dino Squad, í „versta“ tilfelli, geta verið nokkur hundruð skotfæri samtímis í bardaga. Það er lúxus á óviðráðanlegu verði að hreyfa árekstra til að telja hvert skot fyrir sig. Þess vegna var það algjörlega nauðsynlegt fyrir okkur að lágmarka fjölda „tilbaka“ í eðlisfræði. Til að gera þetta bjuggum við til sérstakan íhlut í ECS þar sem við skráum tíma spilarans. Við bættum því við allar einingar sem krefjast töf bóta (skot, hæfileika osfrv.). Áður en við byrjum að vinna úr slíkum einingar klösum við þeim á þessum tíma og vinnum þær saman og snúum efnisheiminum til baka einu sinni fyrir hvern klasa.

Á þessu stigi erum við með almennt starfandi kerfi. Kóði þess í nokkuð einfölduðu formi:

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

Allt sem var eftir var að stilla smáatriðin:

1. Skildu hversu mikið á að takmarka hámarksfjarlægð hreyfingar í tíma.

Það var mikilvægt fyrir okkur að gera leikinn eins aðgengilegan og mögulegt er við aðstæður þar sem farsímakerfi eru léleg, svo við takmörkuðum söguna með 30 mörkum (með 20 Hz tick rate). Þetta gerir leikmönnum kleift að lemja andstæðinga jafnvel við mjög há ping.

2. Ákvarða hvaða hluti er hægt að færa í tíma og hverjir ekki.

Við erum að sjálfsögðu að færa andstæðinga okkar. En uppsettanlegir orkuhlífar eru það til dæmis ekki. Við ákváðum að það væri betra að setja varnargetuna í forgang eins og oft er gert í netskyttum. Ef leikmaðurinn hefur þegar sett skjöld í nútíðinni, ættu byssukúlur úr fortíðinni ekki að fljúga í gegnum hann.

3. Ákveða hvort nauðsynlegt sé að bæta upp fyrir hæfileika risaeðlanna: bit, rófuhögg o.s.frv. Við ákváðum hvað þurfti og unnum eftir sömu reglum og byssukúlur.

4. Ákveða hvað á að gera við árekstur leikmannsins sem verið er að greiða fyrir seinkun. Á góðan hátt ætti staða þeirra ekki að breytast í fortíðina: leikmaðurinn ætti að sjá sjálfan sig á sama tíma og hann er núna á þjóninum. Hins vegar snúum við líka til baka á skotleikmanninum og það eru nokkrar ástæður fyrir því.

Í fyrsta lagi bætir það þyrpinguna: við getum notað sama líkamlega ástandið fyrir alla leikmenn með loka ping.

Í öðru lagi, í öllum geislavörpum og skörun útilokum við alltaf árekstur leikmannsins sem á hæfileikana eða skotfærin. Í Dino Squad stjórna leikmenn risaeðlum, sem hafa frekar óhefðbundna rúmfræði miðað við skotstaðla. Jafnvel þótt leikmaður skýti í óvenjulegu horni og ferill kúlu fer í gegnum risaeðluárekstur leikmannsins, mun kúlan hunsa það.

Í þriðja lagi reiknum við út staðsetningu vopns risaeðlunnar eða beitingarpunkti hæfileikans með því að nota gögn frá ECS jafnvel áður en töf bóta hefst.

Fyrir vikið skiptir raunveruleg staða árekenda leikmannsins, sem er töf, ekki máli, svo við fórum afkastameiri og um leið einfaldari leið.

Ekki er einfaldlega hægt að fjarlægja netleynd, það er aðeins hægt að hylja hana. Eins og hver önnur dulbúningsaðferð, hafa töf bætur á netþjóni sínum málamiðlun. Það bætir leikupplifun leikmannsins sem er að skjóta á kostnað leikmannsins sem skotið er á. Fyrir Dino Squad var valið hér hins vegar augljóst.

Allt þetta þurfti auðvitað líka að borga fyrir aukinn flókið netþjónskóðans í heild - bæði fyrir forritara og leikjahönnuði. Ef fyrr var uppgerðin einfalt raðkall kerfa, þá birtust hreiðraðir lykkjur og greinar í henni með töfuppbót. Við lögðum líka mikið upp úr því að gera það þægilegt að vinna með.

Í 2019 útgáfunni (og kannski aðeins fyrr), bætti Unity við fullum stuðningi við sjálfstæðar líkamlegar senur. Við innleiddum þær á þjóninum nánast strax eftir uppfærsluna, vegna þess að við vildum losna fljótt við líkamlega heiminn sem er sameiginlegur í öllum herbergjum.

Við gáfum hverju leikherbergi sína eigin líkamlegu senu og útilokuðum því þörfina á að „hreinsa“ atriðið úr gögnum nærliggjandi herbergis áður en uppgerðin var reiknuð út. Í fyrsta lagi gaf það verulega aukningu í framleiðni. Í öðru lagi gerði það mögulegt að losna við heilan flokk af villum sem komu upp ef forritarinn gerði villu í senuhreinsunarkóðanum þegar hann bætti við nýjum leikþáttum. Erfitt var að kemba slíkar villur og þær leiddu oft til þess að ástand líkamlegra hluta í vettvangi eins herbergis „flæði“ inn í annað herbergi.

Að auki gerðum við nokkrar rannsóknir á því hvort hægt væri að nota líkamlegar senur til að geyma sögu efnisheimsins. Það er, með skilyrðum, úthlutaðu ekki einu atriði í hvert herbergi, heldur 30 atriði, og búðu til hringlaga biðminni úr þeim, til að geyma söguna. Almennt séð reyndist valmöguleikinn vera að virka, en við útfærðum hann ekki: hann sýndi enga brjálaða framleiðniaukningu heldur krafðist frekar áhættusamra breytinga. Það var erfitt að spá fyrir um hvernig þjónninn myndi haga sér þegar unnið var í langan tíma með svo mörgum senum. Þess vegna fylgdum við reglunni: "Ef það er ekki slitið skaltu ekki laga það'.

Heimild: www.habr.com

Bæta við athugasemd