Şəbəkə gecikmə kompensasiyası alqoritmi ilə mobil atıcı üçün ballistik hesablama mexanikasını necə etdik

Şəbəkə gecikmə kompensasiyası alqoritmi ilə mobil atıcı üçün ballistik hesablama mexanikasını necə etdik

Salam, mən Nikita Brizhak, Pixonic-dən server tərtibatçısı. Bu gün mobil multiplayerdə gecikmənin kompensasiyası haqqında danışmaq istərdim.

Rus dilində də daxil olmaqla server gecikməsinin kompensasiyası haqqında çoxlu məqalələr yazılmışdır. Bu təəccüblü deyil, çünki bu texnologiya 90-cı illərin sonlarından çox oyunçu FPS-nin yaradılmasında fəal şəkildə istifadə olunur. Məsələn, onu ilk istifadə edənlərdən biri olan QuakeWorld modunu xatırlaya bilərsiniz.

Biz onu mobil multiplayer atıcı Dino Squad-da da istifadə edirik.

Bu yazıda məqsədim artıq yazılanları min dəfə təkrarlamaq deyil, texnologiya yığınımızı və əsas oyun xüsusiyyətlərimizi nəzərə alaraq oyunumuzda gecikmə kompensasiyasını necə tətbiq etdiyimizi izah etməkdir.

Korteksimiz və texnologiyamız haqqında bir neçə kəlmə.

Dino Squad şəbəkə mobil PvP atıcısıdır. Oyunçular müxtəlif silahlarla təchiz edilmiş dinozavrları idarə edir və 6v6 komandalarında bir-biri ilə vuruşurlar.

Həm müştəri, həm də server Birliyə əsaslanır. Memarlıq atıcılar üçün olduqca klassikdir: server avtoritardır və müştəri proqnozu müştərilər üzərində işləyir. Oyun simulyasiyası daxili ECS istifadə edərək yazılmışdır və həm serverdə, həm də müştəridə istifadə olunur.

Əgər gecikmə kompensasiyası haqqında ilk dəfə eşidirsinizsə, bu məsələyə qısa bir ekskursiya təqdim edirik.

Çox oyunçulu FPS oyunlarında matç adətən uzaq serverdə simulyasiya edilir. Oyunçular öz girişlərini (basılan düymələr haqqında məlumatı) serverə göndərirlər və cavab olaraq server alınan məlumatları nəzərə alaraq onlara yenilənmiş oyun vəziyyətini göndərir. Bu qarşılıqlı əlaqə sxemi ilə irəli düyməsinin basılması ilə oyunçu personajının ekranda hərəkət etdiyi an arasındakı gecikmə həmişə pingdən daha çox olacaq.

Yerli şəbəkələrdə bu gecikmə (xalq arasında giriş gecikməsi adlanır) görünməz ola bilər, İnternet vasitəsilə oynayarkən simvolu idarə edərkən "buz üzərində sürüşmə" hissi yaradır. Bu problem mobil şəbəkələr üçün ikiqat aktualdır, burada oyunçunun pinginin 200 ms olduğu hal hələ də əla əlaqə hesab olunur. Çox vaxt ping 350, 500 və ya 1000 ms ola bilər. Sonra giriş gecikməsi ilə sürətli atıcı oynamaq demək olar ki, qeyri-mümkün olur.

Bu problemin həlli müştəri tərəfi simulyasiyanın proqnozlaşdırılmasıdır. Burada müştəri özü serverdən cavab gözləmədən girişi oyunçu xarakterinə tətbiq edir. Cavab alındıqda isə sadəcə nəticələri müqayisə edir və rəqiblərin mövqelərini yeniləyir. Bu halda düyməyə basmaqla nəticənin ekranda göstərilməsi arasındakı gecikmə minimaldır.

Buradakı nüansı başa düşmək vacibdir: müştəri həmişə özünü son girişinə görə çəkir, düşmənlər isə serverdən alınan məlumatlardan əvvəlki vəziyyətə görə şəbəkə gecikməsi ilə. Yəni, düşmənə atəş açan zaman oyunçu onu özünə nisbətən keçmişdə görür. Müştəri proqnozu haqqında daha çox əvvəllər yazdıq.

Beləliklə, müştəri proqnozu bir problemi həll edir, digərini yaradır: əgər oyunçu düşmənin keçmişdə olduğu nöqtədə, eyni nöqtədə atəş açarkən serverdə atəş alarsa, düşmən artıq həmin yerdə olmaya bilər. Server gecikmə kompensasiyası bu problemi həll etməyə çalışır. Silah atılan zaman server oyunçunun atəş zamanı yerli olaraq gördüyü oyun vəziyyətini bərpa edir və onun həqiqətən də düşməni vura bilib-bilmədiyini yoxlayır. Cavab "bəli" olarsa, düşmən həmin anda serverdə olmasa belə, vuruş sayılır.

Bu biliklə silahlanaraq, Dino Squad-da server gecikmə kompensasiyasını tətbiq etməyə başladıq. Əvvəla, müştərinin gördüklərini serverdə necə bərpa edəcəyimizi başa düşmək lazım idi? Və tam olaraq nəyi bərpa etmək lazımdır? Oyunumuzda silahlardan və qabiliyyətlərdən gələn zərbələr şüalar və örtüklər vasitəsilə, yəni düşmənin fiziki toqquşdurucuları ilə qarşılıqlı əlaqə vasitəsilə hesablanır. Müvafiq olaraq, oyunçunun yerli olaraq "gördüyü" bu toqquşdurucuların yerini serverdə təkrarlamaq lazım idi. O zaman biz Unity 2018.x versiyasından istifadə edirdik. Oradakı fizika API statikdir, fiziki dünya bir nüsxədə mövcuddur. Onun vəziyyətini saxlamaq və sonra qutudan bərpa etmək üçün heç bir yol yoxdur. Bəs nə etməli?

Həll səthdə idi, onun bütün elementləri bizim tərəfimizdən digər problemləri həll etmək üçün artıq istifadə edilmişdir:

  1. Hər bir müştəri üçün biz bilməliyik ki, o, düymələri basdıqda rəqibləri nə vaxt görüb. Biz artıq bu məlumatı giriş paketinə yazmışıq və ondan müştəri proqnozunu tənzimləmək üçün istifadə etmişik.
  2. Biz oyun vəziyyətlərinin tarixini saxlamağı bacarmalıyıq. Biz rəqiblərimizin (və buna görə də onların toqquşdurucularının) mövqelərini tutacağıq. Serverdə artıq dövlət tarixçəmiz var idi, onu qurmaq üçün istifadə etdik deltalar. Doğru zamanı bilməklə tarixdə düzgün dövləti asanlıqla tapa bilərdik.
  3. İndi əlimizdə tarixdən oyun vəziyyətinə sahib olduğumuz üçün oyunçu məlumatlarını fiziki dünyanın vəziyyəti ilə sinxronizasiya etməyi bacarmalıyıq. Mövcud toqquşdurucular - hərəkət edin, itkin olanlar - yaradın, lazımsızlar - məhv edin. Bu məntiq də artıq yazılmışdı və bir neçə ECS sistemindən ibarət idi. Biz ondan bir Unity prosesində bir neçə oyun otağı saxlamaq üçün istifadə etdik. Fiziki dünya hər bir proses üçün bir olduğu üçün otaqlar arasında təkrar istifadə edilməli idi. Simulyasiyanın hər işarəsindən əvvəl biz fiziki dünyanın vəziyyətini "sıfırladıq" və onu cari otaq üçün məlumatlarla yenidən başladır, ağıllı birləşdirmə sistemi vasitəsilə Unity oyun obyektlərini mümkün qədər təkrar istifadə etməyə çalışırıq. Qalan şey oyun vəziyyəti üçün keçmişdən eyni məntiqə müraciət etmək idi.

Bütün bu elementləri bir araya gətirməklə, fiziki dünyanın vəziyyətini lazımi məqama qaytara bilən “zaman maşını” əldə etdik. Kodun sadə olduğu ortaya çıxdı:

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

Qalan yalnız atışları və bacarıqları asanlıqla kompensasiya etmək üçün bu maşından necə istifadə edəcəyini anlamaq idi.

Ən sadə halda, mexanika bir hitscan üzərində qurulduqda, hər şey aydın görünür: oyunçu atış etməzdən əvvəl fiziki dünyanı istədiyi vəziyyətə qaytarmalı, raycast etməli, vuruşu və ya qaçırmağı saymalı və dünyanı ilkin vəziyyətinə qaytarın.

Ancaq Dino Squad-da belə mexaniklər çox azdır! Oyundakı silahların əksəriyyəti mərmilər yaradır - bir neçə simulyasiya gənəsi (bəzi hallarda onlarla gənə) üçün uçan uzunömürlü güllələr. Onlarla nə etməli, saat neçədə uçmalıdırlar?

В qədim məqalə Half-Life şəbəkə yığını haqqında, Valve-dən olan uşaqlar eyni sualı verdilər və cavabları belə oldu: mərmi gecikməsinin kompensasiyası problemlidir və bundan qaçmaq daha yaxşıdır.

Bizim belə seçimimiz yox idi: mərmi əsaslı silahlar oyun dizaynının əsas xüsusiyyəti idi. Ona görə də bir şey tapmalı olduq. Bir az beyin fırtınası apardıqdan sonra, işə yarayan iki variant hazırladıq:

1. Biz mərmi onu yaradan oyunçunun vaxtı ilə bağlayırıq. Server simulyasiyasının hər işarəsi, hər oyunçunun hər gülləsi üçün biz fiziki dünyanı müştəri vəziyyətinə qaytarırıq və lazımi hesablamaları həyata keçiririk. Bu yanaşma serverdə paylanmış yükə və mərmilərin proqnozlaşdırıla bilən uçuş müddətinə malik olmağa imkan verdi. Proqnozlaşdırma bizim üçün xüsusi əhəmiyyət kəsb edirdi, çünki bizdə bütün mərmilər, o cümlədən düşmən mərmiləri müştəridə proqnozlaşdırılır.

Şəbəkə gecikmə kompensasiyası alqoritmi ilə mobil atıcı üçün ballistik hesablama mexanikasını necə etdik
Şəkildə 30-cu gənədəki oyunçu intizarla raket atır: o, düşmənin hansı istiqamətdə qaçdığını görür və raketin təxmini sürətini bilir. Yerli olaraq 33-cü gənədə hədəfi vurduğunu görür. Gecikmə kompensasiyası sayəsində o, serverdə də görünəcək

2. Biz hər şeyi birinci variantda olduğu kimi edirik, lakin güllə simulyasiyasının bir işarəsini sayaraq dayanmırıq, lakin hər dəfə vaxtını serverə yaxınlaşdıraraq eyni server işarəsi daxilində onun uçuşunu simulyasiya etməyə davam edirik. bir-bir qeyd edin və kollayder mövqelərini yeniləyin. Bunu iki şeydən biri baş verənə qədər edirik:

  • Güllənin müddəti bitib. Bu o deməkdir ki, hesablamalar başa çatıb, biz bir miss və ya hiti saya bilərik. Və bu, atəşin açıldığı eyni gənədir! Bizim üçün bu həm müsbət, həm də mənfi idi. Bir artı - çünki atıcı oyunçu üçün bu, vuruş və düşmənin sağlamlığının azalması arasındakı gecikməni əhəmiyyətli dərəcədə azaltdı. İşin mənfi tərəfi odur ki, rəqiblər oyunçuya atəş açanda da eyni effekt müşahidə olunurdu: düşmən, deyəsən, yalnız yavaş bir raket atıb və zərər artıq hesablanıb.
  • Güllə server vaxtına çatdı. Bu halda, onun simulyasiyası heç bir gecikmə kompensasiyası olmadan növbəti server işarəsində davam edəcəkdir. Yavaş mərmilər üçün bu, nəzəri olaraq birinci variantla müqayisədə fizikadan geri dönmələrin sayını azalda bilər. Eyni zamanda, simulyasiyada qeyri-bərabər yük artdı: server ya boş idi, ya da bir server işarəsində bir neçə güllə üçün onlarla simulyasiya işarəsini hesablayırdı.

Şəbəkə gecikmə kompensasiyası alqoritmi ilə mobil atıcı üçün ballistik hesablama mexanikasını necə etdik
Əvvəlki şəkildəki kimi eyni ssenari, lakin ikinci sxemə görə hesablanır. Raket atışın baş verdiyi eyni işarədə server vaxtı ilə "tutdu" və zərbə növbəti gənə kimi erkən hesablana bilər. 31-ci gənədə bu halda gecikmə kompensasiyası artıq tətbiq edilmir

Tətbiqimizdə bu iki yanaşma cəmi bir neçə kod sətirində fərqlənirdi, ona görə də biz hər ikisini yaratdıq və uzun müddət onlar paralel olaraq mövcud idi. Silahın mexanikasından və güllənin sürətindən asılı olaraq hər bir dinozavr üçün bu və ya digər variantı seçdik. Burada dönüş nöqtəsi mexanika oyununda “düşməni filan vaxtda bu qədər vursan, filan bonus qazan” kimi görünüşü oldu. Oyunçunun düşməni vurduğu vaxtın mühüm rol oynadığı hər hansı bir mexanik ikinci yanaşma ilə işləməkdən imtina etdi. Beləliklə, biz birinci seçimlə davam etdik və bu, indi bütün silahlara və oyundakı bütün aktiv qabiliyyətlərə aiddir.

Ayrı-ayrılıqda, performans məsələsini qaldırmağa dəyər. Bütün bunların işləri ləngitəcəyini düşünürsənsə, cavab verirəm: belədir. Birlik toqquşdurucuları hərəkət etdirməkdə və onları işə salmaqda və söndürməkdə olduqca yavaşdır. Dino Squad-da, "ən pis halda" ssenaridə, döyüşdə eyni vaxtda mövcud olan bir neçə yüz mərmi ola bilər. Hər bir mərmi ayrı-ayrılıqda saymaq üçün kollayderlərin hərəkət etdirilməsi əlçatmaz bir lüksdür. Buna görə də, fizika "geri dönmələrin" sayını minimuma endirməyimiz tamamilə zəruri idi. Bunun üçün biz ECS-də oyunçunun vaxtını qeyd etdiyimiz ayrıca komponent yaratdıq. Biz onu gecikmə kompensasiyası tələb edən bütün obyektlərə əlavə etdik (mərmilər, qabiliyyətlər və s.). Bu cür obyektləri emal etməyə başlamazdan əvvəl biz onları bu vaxta qədər klasterləşdiririk və hər klaster üçün fiziki dünyanı bir dəfə geri çəkərək birlikdə emal edirik.

Bu mərhələdə bizdə ümumi işləyən bir sistem var. Onun kodu bir qədər sadələşdirilmiş formada:

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

Qalan yalnız detalları konfiqurasiya etmək idi:

1. Hərəkətin maksimum məsafəsini vaxtında nə qədər məhdudlaşdıracağını anlayın.

Zəif mobil şəbəkələr şəraitində oyunu mümkün qədər əlçatan etmək bizim üçün vacib idi, buna görə də hekayəni 30 gənə marjası ilə məhdudlaşdırdıq (20 Hz tezliyi ilə). Bu, oyunçulara hətta çox yüksək pinglərdə də rəqiblərini vurmağa imkan verir.

2. Hansı obyektlərin vaxtında köçürülə biləcəyini və hansının hərəkət edə bilməyəcəyini müəyyənləşdirin.

Biz, əlbəttə ki, rəqiblərimizi hərəkətə gətiririk. Ancaq quraşdırıla bilən enerji qalxanları, məsələn, deyil. Biz qərara gəldik ki, tez-tez onlayn atıcılarda olduğu kimi müdafiə qabiliyyətinə üstünlük vermək daha yaxşıdır. Əgər oyunçu indiki vaxtda artıq qalxan qoyubsa, keçmişin lag kompensasiya edilmiş güllələri onun içindən keçməməlidir.

3. Dinozavrların qabiliyyətlərini kompensasiya etmək lazım olub-olmamasına qərar verin: dişləmə, quyruq vurma və s. Biz nə lazım olduğuna qərar verdik və onları güllələrlə eyni qaydalara uyğun olaraq emal etdik.

4. Gecikmə kompensasiyası həyata keçirilən oyunçunun toqquşdurucuları ilə nə edəcəyini müəyyənləşdirin. Yaxşı mənada, onların mövqeyi keçmişə keçməməlidir: oyunçu özünü indi serverdə olduğu vaxtda görməlidir. Bununla belə, biz atıcı oyunçunun toqquşdurucularını da geri çəkirik və bunun bir neçə səbəbi var.

Birincisi, klasterləşməni yaxşılaşdırır: biz yaxın pingləri olan bütün oyunçular üçün eyni fiziki vəziyyətdən istifadə edə bilərik.

İkincisi, bütün şüalanmalarda və üst-üstə düşmələrdə biz həmişə bacarıqlara və ya mərmilərə sahib olan oyunçunun toqquşdurucularını istisna edirik. Dino Squad-da oyunçular atıcı standartlarına görə olduqca qeyri-standart həndəsə malik dinozavrları idarə edirlər. Oyunçu qeyri-adi bucaq altında atsa və güllənin trayektoriyası oyunçunun dinozavr toqquşdurucusundan keçsə belə, güllə buna məhəl qoymayacaq.

Üçüncüsü, biz dinozavrın silahının mövqelərini və ya ECS-dən alınan məlumatlardan istifadə edərək qabiliyyətin tətbiqi nöqtəsini, hətta gecikmə kompensasiyası başlamazdan əvvəl hesablayırıq.

Nəticədə, lag kompensasiya edən oyunçunun toqquşdurucularının real mövqeyi bizim üçün əhəmiyyətsiz olduğundan daha məhsuldar və eyni zamanda daha sadə yol tutduq.

Şəbəkə gecikməsi sadəcə aradan qaldırıla bilməz, yalnız maskalana bilər. Hər hansı digər maskalanma üsulu kimi, server gecikməsinin kompensasiyasının da öz üstünlükləri var. Bu, vurulan oyunçunun hesabına atəş edən oyunçunun oyun təcrübəsini yaxşılaşdırır. Dino Squad üçün isə burada seçim göz qabağında idi.

Əlbəttə ki, bütün bunlar həm də bütövlükdə server kodunun artan mürəkkəbliyi ilə ödənilməli idi - həm proqramçılar, həm də oyun dizaynerləri üçün. Əgər əvvəllər simulyasiya sistemlərin sadə ardıcıl çağırışı idisə, onda gecikmə kompensasiyası ilə içərisində iç-içə döngələr və budaqlar meydana çıxdı. Biz də onunla işləməyi rahat etmək üçün çox səy sərf etdik.

2019 versiyasında (və bəlkə də bir az əvvəl), Unity müstəqil fiziki səhnələr üçün tam dəstək əlavə etdi. Biz onları yeniləmədən dərhal sonra serverdə tətbiq etdik, çünki bütün otaqlar üçün ümumi olan fiziki dünyadan tez bir zamanda xilas olmaq istəyirdik.

Biz hər bir oyun otağına öz fiziki səhnəsini verdik və beləliklə, simulyasiyanı hesablamazdan əvvəl səhnəni qonşu otağın məlumatlarından “təmizləmək” ehtiyacını aradan qaldırdıq. Birincisi, məhsuldarlığın əhəmiyyətli dərəcədə artmasına səbəb oldu. İkincisi, proqramçı yeni oyun elementləri əlavə edərkən səhnə təmizləmə kodunda səhv edərsə, yaranan bütün səhvlər sinfindən qurtulmağa imkan verdi. Bu cür səhvləri aradan qaldırmaq çətin idi və onlar tez-tez bir otağın səhnəsindəki fiziki obyektlərin başqa bir otağa "axması" ilə nəticələnirdi.

Bundan əlavə, fiziki səhnələrin fiziki dünyanın tarixini saxlamaq üçün istifadə oluna biləcəyi ilə bağlı bəzi araşdırmalar apardıq. Yəni, şərti olaraq, hər otağa bir səhnə deyil, 30 səhnə ayırın və onlardan hekayəni saxlamaq üçün dövri bufer hazırlayın. Ümumiyyətlə, seçim işə yaradı, amma biz onu həyata keçirmədik: məhsuldarlıqda heç bir çılğın artım göstərmədi, əksinə riskli dəyişikliklər tələb etdi. Bu qədər səhnə ilə uzun müddət işləyərkən serverin necə davranacağını təxmin etmək çətin idi. Buna görə də biz qaydaya əməl etdik: “Qırılmayıbsa, düzəltməyin.

Mənbə: www.habr.com

Добавить комментарий