Si e kemi përmirësuar mekanikën e llogaritjeve balistike për një gjuajtës celular me një algoritëm të kompensimit të vonesës së rrjetit

Si e kemi përmirësuar mekanikën e llogaritjeve balistike për një gjuajtës celular me një algoritëm të kompensimit të vonesës së rrjetit

Përshëndetje, unë jam Nikita Brizhak, një zhvillues serveri nga Pixonic. Sot do të doja të flisja për kompensimin e vonesës në multiplayer celular.

Janë shkruar shumë artikuj në lidhje me kompensimin e vonesës së serverit, përfshirë në Rusisht. Kjo nuk është për t'u habitur, pasi kjo teknologji është përdorur në mënyrë aktive në krijimin e FPS me shumë lojtarë që nga fundi i viteve '90. Për shembull, ju mund të mbani mend modin QuakeWorld, i cili ishte një nga të parët që e përdori atë.

Ne e përdorim atë gjithashtu në Dino Squad-in tonë celular për gjuajtës me shumë lojtarë.

Në këtë artikull, qëllimi im nuk është të përsëris atë që është shkruar tashmë një mijë herë, por të tregoj se si kemi zbatuar kompensimin e vonesave në lojën tonë, duke marrë parasysh grupin tonë të teknologjisë dhe veçoritë kryesore të lojës.

Disa fjalë për korteksin dhe teknologjinë tonë.

Dino Squad është një qitës PvP celular në rrjet. Lojtarët kontrollojnë dinosaurët e pajisur me një shumëllojshmëri armësh dhe luftojnë me njëri-tjetrin në ekipe 6v6.

Si klienti ashtu edhe serveri bazohen në Unity. Arkitektura është mjaft klasike për gjuajtësit: serveri është autoritar dhe parashikimi i klientit funksionon tek klientët. Simulimi i lojës është shkruar duke përdorur ECS të brendshme dhe përdoret si në server ashtu edhe në klient.

Nëse kjo është hera e parë që keni dëgjuar për kompensimin e vonesës, këtu është një ekskursion i shkurtër në këtë çështje.

Në lojërat FPS me shumë lojtarë, ndeshja zakonisht simulohet në një server të largët. Lojtarët dërgojnë të dhënat e tyre (informacionet për çelësat e shtypur) në server, dhe si përgjigje serveri u dërgon atyre një gjendje të përditësuar të lojës duke marrë parasysh të dhënat e marra. Me këtë skemë ndërveprimi, vonesa ndërmjet shtypjes së tastit përpara dhe momentit kur personazhi i luajtësit lëviz në ekran do të jetë gjithmonë më i madh se ping.

Ndërsa në rrjetet lokale kjo vonesë (e quajtur gjerësisht vonesa në hyrje) mund të jetë e padukshme, kur luani nëpërmjet internetit krijon një ndjenjë "rrëshqitjeje mbi akull" kur kontrolloni një personazh. Ky problem është dyfish i rëndësishëm për rrjetet celulare, ku rasti kur ping-u i një lojtari është 200 ms konsiderohet ende një lidhje e shkëlqyer. Shpesh ping mund të jetë 350, 500 ose 1000 ms. Atëherë bëhet pothuajse e pamundur të luash një gjuajtës të shpejtë me vonesë në hyrje.

Zgjidhja për këtë problem është parashikimi i simulimit nga ana e klientit. Këtu vetë klienti aplikon të dhëna për karakterin e luajtësit, pa pritur një përgjigje nga serveri. Dhe kur merret përgjigja, ajo thjesht krahason rezultatet dhe përditëson pozicionet e kundërshtarëve. Vonesa ndërmjet shtypjes së një tasti dhe shfaqjes së rezultatit në ekran në këtë rast është minimale.

Është e rëndësishme të kuptohet nuanca këtu: klienti gjithmonë tërheq veten sipas hyrjes së tij të fundit, dhe armiqtë - me vonesë në rrjet, sipas gjendjes së mëparshme nga të dhënat nga serveri. Kjo do të thotë, kur gjuan në një armik, lojtari e sheh atë në të kaluarën në lidhje me veten e tij. Më shumë rreth parashikimit të klientit kemi shkruar më herët.

Kështu, parashikimi i klientit zgjidh një problem, por krijon një tjetër: nëse një lojtar qëllon në pikën ku armiku ishte në të kaluarën, në server kur gjuan në të njëjtën pikë, armiku mund të mos jetë më në atë vend. Kompensimi i vonesës së serverit përpiqet të zgjidhë këtë problem. Kur gjuhet një armë, serveri rikthen gjendjen e lojës që lojtari pa lokalisht në momentin e gjuajtjes dhe kontrollon nëse ai vërtet mund të kishte goditur armikun. Nëse përgjigja është "po", goditja llogaritet, edhe nëse armiku nuk është më në server në atë pikë.

Të armatosur me këtë njohuri, ne filluam të zbatojmë kompensimin e vonesës së serverit në Dino Squad. Para së gjithash, duhej të kuptonim se si të rivendosnim në server atë që pa klienti? Dhe çfarë saktësisht duhet të restaurohet? Në lojën tonë, goditjet nga armët dhe aftësitë llogariten përmes rrezeve dhe mbivendosjeve - domethënë përmes ndërveprimeve me përplasësit fizikë të armikut. Prandaj, na duhej të riprodhonim pozicionin e këtyre përplasësve, të cilat lojtari "i pa" në vend, në server. Në atë kohë ne përdornim versionin Unity 2018.x. API i fizikës atje është statik, bota fizike ekziston në një kopje të vetme. Nuk ka asnjë mënyrë për të ruajtur gjendjen e tij dhe më pas për ta rivendosur atë nga kutia. Pra, çfarë të bëni?

Zgjidhja ishte në sipërfaqe; të gjithë elementët e saj ishin përdorur tashmë nga ne për të zgjidhur probleme të tjera:

  1. Për secilin klient, ne duhet të dimë se në cilën orë ai pa kundërshtarët kur shtypte tastet. Ne e kemi shkruar tashmë këtë informacion në paketën hyrëse dhe e kemi përdorur për të rregulluar parashikimin e klientit.
  2. Ne duhet të jemi në gjendje të ruajmë historinë e gjendjeve të lojës. Është në të që ne do të mbajmë pozicionet e kundërshtarëve tanë (dhe rrjedhimisht përplasësve të tyre). Tashmë kishim një histori shtetërore në server, e përdorëm për ta ndërtuar deltat. Duke ditur kohën e duhur, ne mund të gjenim lehtësisht gjendjen e duhur në histori.
  3. Tani që kemi në dorë gjendjen e lojës nga historia, duhet të jemi në gjendje të sinkronizojmë të dhënat e lojtarëve me gjendjen e botës fizike. Përplasësit ekzistues - lëvizni, ata që mungojnë - krijoni, ata të panevojshëm - shkatërroni. Kjo logjikë gjithashtu ishte shkruar tashmë dhe përbëhej nga disa sisteme ECS. Ne e përdorëm atë për të mbajtur disa dhoma lojërash në një proces Unity. Dhe meqenëse bota fizike është një për proces, ajo duhej të ripërdorej midis dhomave. Para secilës shenjë të simulimit, ne "rivendosnim" gjendjen e botës fizike dhe e rifilluam atë me të dhëna për dhomën aktuale, duke u përpjekur të ripërdorim objektet e lojës Unity sa më shumë që të jetë e mundur përmes një sistemi të zgjuar bashkimi. Ajo që mbeti ishte të thirrej e njëjta logjikë për gjendjen e lojës nga e kaluara.

Duke i bashkuar të gjithë këta elementë, ne morëm një "makinë kohe" që mund të kthente gjendjen e botës fizike në momentin e duhur. Kodi doli të ishte i thjeshtë:

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

E tëra që mbetej ishte të kuptonim se si ta përdorni këtë makinë për të kompensuar lehtësisht goditjet dhe aftësitë.

Në rastin më të thjeshtë, kur mekanika bazohet në një skanim të vetëm, gjithçka duket se është e qartë: përpara se lojtari të gjuajë, ai duhet të kthejë botën fizike në gjendjen e dëshiruar, të bëjë një transmetim me rreze, të numërojë goditjen ose humbjen dhe ktheni botën në gjendjen fillestare.

Por ka shumë pak mekanikë të tillë në Dino Squad! Shumica e armëve në lojë krijojnë predha - plumba jetëgjatë që fluturojnë për disa rriqra simulimi (në disa raste, dhjetëra rriqra). Çfarë të bëni me ta, në çfarë ore duhet të fluturojnë?

В artikull i lashtë në lidhje me pirgun e rrjetit Half-Life, djemtë nga Valve bënë të njëjtën pyetje, dhe përgjigja e tyre ishte kjo: kompensimi i vonesës së predhës është problematik dhe është më mirë ta shmangni atë.

Ne nuk e kishim këtë opsion: armët me bazë predha ishin një tipar kryesor i dizajnit të lojës. Kështu që ne duhej të dilnim me diçka. Pas disa idesh, ne formuluam dy opsione që dukej se funksiononin:

1. E lidhim predhën me kohën e lojtarit që e ka krijuar. Çdo shenjë e simulimit të serverit, për çdo plumb të çdo lojtari, ne e kthejmë botën fizike në gjendjen e klientit dhe kryejmë llogaritjet e nevojshme. Kjo qasje bëri të mundur që të ketë një ngarkesë të shpërndarë në server dhe kohë të parashikueshme fluturimi të predhave. Parashikueshmëria ishte veçanërisht e rëndësishme për ne, pasi ne kemi të gjitha predhat, duke përfshirë predha armike, të parashikuara për klientin.

Si e kemi përmirësuar mekanikën e llogaritjeve balistike për një gjuajtës celular me një algoritëm të kompensimit të vonesës së rrjetit
Në foto, lojtari në shenjën 30 qëllon një raketë në pritje: ai sheh se në cilin drejtim po vrapon armiku dhe e di shpejtësinë e përafërt të raketës. Lokalisht sheh se ka goditur objektivin në shenjën e 33-të. Falë kompensimit të vonesës, do të shfaqet edhe në server

2. Ne bëjmë gjithçka njësoj si në opsionin e parë, por, pasi kemi numëruar një shenjë të simulimit të plumbit, ne nuk ndalemi, por vazhdojmë të simulojmë fluturimin e tij brenda të njëjtit shenjë të serverit, çdo herë duke e afruar kohën e tij me serverin një nga një shënoni dhe përditësoni pozicionet e përplasësve. Ne e bëjmë këtë derisa të ndodhë një nga dy gjërat:

  • Plumbi ka skaduar. Kjo do të thotë që llogaritjet kanë mbaruar, mund të numërojmë një humbje ose një goditje. Dhe kjo është në të njëjtin tik-tak në të cilin u qëllua! Për ne kjo ishte një plus dhe një minus. Një plus - sepse për lojtarin e gjuajtjes kjo zvogëloi ndjeshëm vonesën midis goditjes dhe uljes së shëndetit të armikut. E keqja është se i njëjti efekt u vërejt kur kundërshtarët qëlluan kundër lojtarit: armiku, me sa duket, gjuajti vetëm një raketë të ngadaltë dhe dëmi tashmë ishte numëruar.
  • Plumbi ka arritur në kohën e serverit. Në këtë rast, simulimi i tij do të vazhdojë në shenjën tjetër të serverit pa asnjë kompensim vonese. Për predha të ngadalta, kjo teorikisht mund të zvogëlojë numrin e kthimeve të fizikës në krahasim me opsionin e parë. Në të njëjtën kohë, ngarkesa e pabarabartë në simulim u rrit: serveri ishte ose i papunë, ose në një tik-tak të serverit po llogaritte një duzinë rriqrash simulimi për disa plumba.

Si e kemi përmirësuar mekanikën e llogaritjeve balistike për një gjuajtës celular me një algoritëm të kompensimit të vonesës së rrjetit
I njëjti skenar si në foton e mëparshme, por i llogaritur sipas skemës së dytë. Raketa "kapi" kohën e serverit në të njëjtin tik-tak që ndodhi dhe goditja mund të numërohet që në shenjën tjetër. Në pikën e 31-të, në këtë rast, kompensimi i vonesës nuk zbatohet më

Në zbatimin tonë, këto dy qasje ndryshonin në vetëm disa rreshta kodi, kështu që ne i krijuam të dyja, dhe për një kohë të gjatë ato ekzistonin paralelisht. Në varësi të mekanikës së armës dhe shpejtësisë së plumbit, ne zgjodhëm një ose një opsion tjetër për secilin dinosaur. Pika e kthesës këtu ishte shfaqja në lojën e mekanikës si "nëse e godet armikun kaq shumë herë në një kohë të tillë, merr një bonus". Çdo mekanik ku koha në të cilën lojtari goditi armikun luajti një rol të rëndësishëm, refuzoi të punonte me qasjen e dytë. Kështu që ne përfunduam duke shkuar me opsionin e parë, dhe tani ai vlen për të gjitha armët dhe të gjitha aftësitë aktive në lojë.

Më vete, ia vlen të ngrihet çështja e performancës. Nëse keni menduar se e gjithë kjo do të ngadalësojë gjërat, unë përgjigjem: është. Uniteti është mjaft i ngadalshëm në lëvizjen e përplasësve dhe ndezjen dhe fikjen e tyre. Në Skuadrën Dino, në rastin "më të keq", mund të ketë disa qindra predha që ekzistojnë njëkohësisht në luftim. Lëvizja e përplasësve për të numëruar çdo predhë individualisht është një luks i papërballueshëm. Prandaj, ishte absolutisht e nevojshme që ne të minimizonim numrin e "kthimeve" të fizikës. Për ta bërë këtë, ne krijuam një komponent të veçantë në ECS në të cilin regjistrojmë kohën e luajtësit. E kemi shtuar në të gjitha subjektet që kërkojnë kompensim vonese (predha, aftësi, etj.). Përpara se të fillojmë përpunimin e entiteteve të tilla, ne i grupojmë ato deri në këtë kohë dhe i përpunojmë së bashku, duke e kthyer botën fizike një herë për çdo grup.

Në këtë fazë kemi një sistem përgjithësisht funksional. Kodi i tij në një formë disi të thjeshtuar:

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

E tëra që mbetej ishte të konfiguroni detajet:

1. Kuptoni sa të kufizoni distancën maksimale të lëvizjes në kohë.

Ishte e rëndësishme për ne që ta bënim lojën sa më të aksesueshme në kushtet e rrjeteve të dobëta celulare, kështu që e kufizuam historinë me një diferencë prej 30 tik-takimesh (me një shpejtësi tik-takimi 20 Hz). Kjo i lejon lojtarët të godasin kundërshtarët edhe në ping shumë të lartë.

2. Përcaktoni se cilat objekte mund të zhvendosen në kohë dhe cilat jo.

Ne, sigurisht, po lëvizim kundërshtarët tanë. Por mburojat energjetike të instalueshme, për shembull, nuk janë. Ne vendosëm që ishte më mirë t'i jepnim përparësi aftësisë mbrojtëse, siç bëhet shpesh në gjuajtësit online. Nëse lojtari ka vendosur tashmë një mburojë në të tashmen, plumbat e kompensuar me vonesë nga e kaluara nuk duhet të kalojnë nëpër të.

3. Vendosni nëse është e nevojshme të kompensohen aftësitë e dinosaurëve: kafshimi, goditja e bishtit, etj. Ne vendosëm se çfarë duhej dhe i përpunonim sipas të njëjtave rregulla si plumbat.

4. Përcaktoni se çfarë të bëni me përplasësit e lojtarit për të cilin po kryhet kompensimi i vonesës. Në një mënyrë të mirë, pozicioni i tyre nuk duhet të zhvendoset në të kaluarën: lojtari duhet ta shohë veten në të njëjtën kohë në të cilën është tani në server. Sidoqoftë, ne gjithashtu i kthejmë mbrapsht përplasësit e lojtarit që gjuan, dhe ka disa arsye për këtë.

Së pari, përmirëson grupimin: ne mund të përdorim të njëjtën gjendje fizike për të gjithë lojtarët me ping të ngushtë.

Së dyti, në të gjitha transmetimet me rreze dhe mbivendosjet ne gjithmonë përjashtojmë përplasësit e lojtarit që zotëron aftësitë ose predha. Në Dino Squad, lojtarët kontrollojnë dinosaurët, të cilët kanë gjeometri mjaft jo standarde sipas standardeve të gjuajtjes. Edhe nëse lojtari gjuan në një kënd të pazakontë dhe trajektorja e plumbit kalon përmes përplasësit të dinosaurëve të lojtarit, plumbi do ta injorojë atë.

Së treti, ne llogarisim pozicionet e armës së dinosaurëve ose pikën e aplikimit të aftësisë duke përdorur të dhëna nga ECS edhe para fillimit të kompensimit të vonesës.

Si rezultat, pozicioni real i përplasësve të lojtarit të kompensuar me vonesë është i parëndësishëm për ne, kështu që ne morëm një rrugë më produktive dhe në të njëjtën kohë më të thjeshtë.

Vonesa e rrjetit nuk mund të hiqet thjesht, ajo vetëm mund të maskohet. Ashtu si çdo metodë tjetër maskimi, kompensimi i vonesës së serverit ka kompromiset e veta. Ai përmirëson përvojën e lojës së lojtarit që gjuan në kurriz të lojtarit që goditet. Për Skuadrën Dino, megjithatë, zgjedhja këtu ishte e qartë.

Sigurisht, e gjithë kjo duhej të paguhej edhe nga kompleksiteti i shtuar i kodit të serverit në tërësi - si për programuesit ashtu edhe për projektuesit e lojërave. Nëse më parë simulimi ishte një thirrje e thjeshtë sekuenciale e sistemeve, atëherë me kompensimin e vonesës, sythe dhe degë të mbivendosura u shfaqën në të. Ne gjithashtu shpenzuam shumë përpjekje për ta bërë të përshtatshme për të punuar me të.

Në versionin 2019 (dhe ndoshta pak më herët), Unity shtoi mbështetje të plotë për skenat e pavarura fizike. Ne i zbatuam ato në server pothuajse menjëherë pas përditësimit, sepse donim të shpëtonim shpejt nga bota fizike e zakonshme për të gjitha dhomat.

Ne i dhamë secilës dhomë të lojës skenën e saj fizike dhe kështu eliminuam nevojën për të "pastruar" skenën nga të dhënat e dhomës fqinje përpara se të llogarisim simulimin. Së pari, ai dha një rritje të konsiderueshme të produktivitetit. Së dyti, bëri të mundur heqjen e një klase të tërë gabimesh që lindën nëse programuesi bën një gabim në kodin e pastrimit të skenës kur shtoi elementë të rinj të lojës. Gabime të tilla ishin të vështira për t'u korrigjuar, dhe ato shpesh rezultuan në gjendjen e objekteve fizike në skenën e një dhome "duke rrjedhur" në një dhomë tjetër.

Përveç kësaj, ne bëmë disa kërkime nëse skenat fizike mund të përdoren për të ruajtur historinë e botës fizike. Kjo do të thotë, me kusht, ndani jo një skenë në secilën dhomë, por 30 skena dhe bëni një tampon ciklik prej tyre, në të cilin do të ruani historinë. Në përgjithësi, opsioni doli të funksiononte, por ne nuk e zbatuam atë: nuk tregoi ndonjë rritje të çmendur të produktivitetit, por kërkonte ndryshime mjaft të rrezikshme. Ishte e vështirë të parashikohej se si do të sillej serveri kur punonte për një kohë të gjatë me kaq shumë skena. Prandaj, ne kemi ndjekur rregullin: "Nëse nuk është thyer, mos e rregulloni'.

Burimi: www.habr.com

Shto një koment