Cara kami mempertingkatkan mekanik pengiraan balistik untuk penembak mudah alih dengan algoritma pampasan kependaman rangkaian

Cara kami mempertingkatkan mekanik pengiraan balistik untuk penembak mudah alih dengan algoritma pampasan kependaman rangkaian

Hai, saya Nikita Brizhak, pembangun pelayan daripada Pixonic. Hari ini saya ingin bercakap tentang mengimbangi ketinggalan dalam berbilang pemain mudah alih.

Banyak artikel telah ditulis mengenai pampasan lag pelayan, termasuk dalam bahasa Rusia. Ini tidak menghairankan, kerana teknologi ini telah digunakan secara aktif dalam penciptaan FPS berbilang pemain sejak lewat 90-an. Sebagai contoh, anda boleh mengingati mod QuakeWorld, yang merupakan salah satu yang pertama menggunakannya.

Kami juga menggunakannya dalam penembak berbilang pemain mudah alih kami Dino Squad.

Dalam artikel ini, matlamat saya bukan untuk mengulangi perkara yang telah ditulis seribu kali, tetapi untuk memberitahu cara kami melaksanakan pampasan lag dalam permainan kami, dengan mengambil kira tindanan teknologi dan ciri permainan teras kami.

Sedikit perkataan tentang korteks dan teknologi kami.

Dino Squad ialah penembak PvP mudah alih rangkaian. Pemain mengawal dinosaur yang dilengkapi dengan pelbagai senjata dan bertarung antara satu sama lain dalam pasukan 6v6.

Kedua-dua pelanggan dan pelayan adalah berdasarkan Unity. Seni bina agak klasik untuk penembak: pelayan adalah autoritarian, dan ramalan pelanggan berfungsi pada pelanggan. Simulasi permainan ditulis menggunakan ECS dalaman dan digunakan pada kedua-dua pelayan dan pelanggan.

Jika ini kali pertama anda mendengar tentang pampasan lag, berikut ialah lawatan ringkas mengenai isu tersebut.

Dalam permainan FPS berbilang pemain, perlawanan biasanya disimulasikan pada pelayan jauh. Pemain menghantar input mereka (maklumat tentang kekunci yang ditekan) ke pelayan, dan sebagai tindak balas pelayan menghantar mereka keadaan permainan yang dikemas kini dengan mengambil kira data yang diterima. Dengan skema interaksi ini, kelewatan antara menekan kekunci ke hadapan dan saat watak pemain bergerak pada skrin akan sentiasa lebih besar daripada ping.

Semasa di rangkaian tempatan kelewatan ini (biasa dipanggil lag input) mungkin tidak dapat dilihat, apabila bermain melalui Internet ia mewujudkan perasaan "meluncur di atas ais" apabila mengawal watak. Masalah ini berganda relevan untuk rangkaian mudah alih, di mana kes apabila ping pemain ialah 200 ms masih dianggap sambungan yang sangat baik. Selalunya ping boleh 350, 500, atau 1000 ms. Kemudian menjadi hampir mustahil untuk memainkan penembak pantas dengan ketinggalan input.

Penyelesaian kepada masalah ini ialah ramalan simulasi sisi klien. Di sini pelanggan sendiri menggunakan input kepada watak pemain, tanpa menunggu respons daripada pelayan. Dan apabila jawapan diterima, ia hanya membandingkan keputusan dan mengemas kini kedudukan lawan. Kelewatan antara menekan kekunci dan memaparkan hasil pada skrin dalam kes ini adalah minimum.

Adalah penting untuk memahami nuansa di sini: pelanggan sentiasa menarik dirinya mengikut input terakhirnya, dan musuh - dengan kelewatan rangkaian, mengikut keadaan sebelumnya dari data dari pelayan. Iaitu, apabila menembak musuh, pemain melihatnya pada masa lalu berbanding dirinya sendiri. Lebih lanjut mengenai ramalan pelanggan kita tulis tadi.

Oleh itu, ramalan pelanggan menyelesaikan satu masalah, tetapi mencipta masalah lain: jika pemain menembak pada titik di mana musuh berada pada masa lalu, pada pelayan apabila menembak pada titik yang sama, musuh mungkin tidak lagi berada di tempat itu. Pampasan lag pelayan cuba menyelesaikan masalah ini. Apabila senjata dilepaskan, pelayan memulihkan keadaan permainan yang pemain lihat secara tempatan pada masa tembakan, dan memeriksa sama ada dia benar-benar boleh mengenai musuh. Jika jawapannya "ya," pukulan dikira, walaupun musuh tidak lagi berada di pelayan pada ketika itu.

Berbekalkan pengetahuan ini, kami mula melaksanakan pampasan lag pelayan dalam Skuad Dino. Pertama sekali, kita perlu memahami bagaimana untuk memulihkan pada pelayan apa yang dilihat oleh pelanggan? Dan apa sebenarnya yang perlu dipulihkan? Dalam permainan kami, pukulan daripada senjata dan kebolehan dikira melalui siaran sinar dan tindanan - iaitu, melalui interaksi dengan penyerang fizikal musuh. Sehubungan itu, kami perlu menghasilkan semula kedudukan pelanggar ini, yang pemain "lihat" secara tempatan, pada pelayan. Pada masa itu kami menggunakan Unity versi 2018.x. API fizik di sana adalah statik, dunia fizikal wujud dalam satu salinan. Tiada cara untuk menyimpan keadaannya dan kemudian memulihkannya dari kotak. Jadi apa yang perlu dilakukan?

Penyelesaiannya ada di permukaan; semua elemennya telah digunakan oleh kami untuk menyelesaikan masalah lain:

  1. Bagi setiap pelanggan, kita perlu tahu pada pukul berapa dia melihat lawan apabila dia menekan kekunci. Kami telah pun menulis maklumat ini ke dalam pakej input dan menggunakannya untuk melaraskan ramalan pelanggan.
  2. Kita perlu dapat menyimpan sejarah keadaan permainan. Di dalamnya kita akan memegang jawatan lawan kita (dan oleh itu pelanggar mereka). Kami sudah mempunyai sejarah keadaan pada pelayan, kami menggunakannya untuk membina delta. Mengetahui masa yang sesuai, kita boleh mencari keadaan yang betul dalam sejarah dengan mudah.
  3. Memandangkan kita mempunyai keadaan permainan daripada sejarah dalam tangan, kita perlu dapat menyegerakkan data pemain dengan keadaan dunia fizikal. Pelanggar sedia ada - bergerak, yang hilang - cipta, yang tidak perlu - musnahkan. Logik ini juga telah ditulis dan terdiri daripada beberapa sistem ECS. Kami menggunakannya untuk memegang beberapa bilik permainan dalam satu proses Perpaduan. Dan kerana dunia fizikal adalah satu setiap proses, ia terpaksa digunakan semula antara bilik. Sebelum setiap tanda pada simulasi, kami "menetapkan semula" keadaan dunia fizikal dan memulakan semula dengan data untuk bilik semasa, cuba menggunakan semula objek permainan Unity sebanyak mungkin melalui sistem pengumpulan yang bijak. Yang tinggal hanyalah menggunakan logik yang sama untuk keadaan permainan dari masa lalu.

Dengan menggabungkan semua elemen ini, kami mendapat "mesin masa" yang boleh melancarkan keadaan dunia fizikal ke masa yang tepat. Kod itu ternyata mudah:

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

Yang tinggal hanyalah memikirkan cara menggunakan mesin ini untuk mengimbangi pukulan dan kebolehan dengan mudah.

Dalam kes paling mudah, apabila mekanik didasarkan pada satu hitscan, segala-galanya nampak jelas: sebelum pemain menembak, dia perlu memusingkan dunia fizikal ke keadaan yang dikehendaki, melakukan siaran sinar, mengira pukulan atau tersasar, dan mengembalikan dunia kepada keadaan asal.

Tetapi terdapat sangat sedikit mekanik seperti itu dalam Skuad Dino! Kebanyakan senjata dalam permainan mencipta peluru - peluru tahan lama yang terbang untuk beberapa kutu simulasi (dalam beberapa kes, berpuluh-puluh kutu). Apa yang perlu dilakukan dengan mereka, pukul berapa mereka harus terbang?

Π’ artikel kuno mengenai timbunan rangkaian Half-Life, lelaki dari Valve bertanya soalan yang sama, dan jawapan mereka adalah ini: pampasan lag peluru bermasalah, dan lebih baik untuk mengelakkannya.

Kami tidak mempunyai pilihan ini: senjata berasaskan peluru merupakan ciri utama reka bentuk permainan. Jadi kami terpaksa membuat sesuatu. Selepas beberapa sumbang saran, kami merumuskan dua pilihan yang nampaknya berkesan:

1. Kami mengikat peluru dengan masa pemain yang menciptanya. Setiap tanda pada simulasi pelayan, untuk setiap peluru setiap pemain, kami melancarkan dunia fizikal kepada keadaan klien dan melakukan pengiraan yang diperlukan. Pendekatan ini memungkinkan untuk mempunyai beban teragih pada pelayan dan masa penerbangan peluru yang boleh diramal. Kebolehramalan amat penting bagi kami, kerana kami mempunyai semua peluru, termasuk peluru musuh, diramalkan pada klien.

Cara kami mempertingkatkan mekanik pengiraan balistik untuk penembak mudah alih dengan algoritma pampasan kependaman rangkaian
Dalam gambar, pemain pada tanda 30 menembak peluru berpandu dalam jangkaan: dia melihat ke arah mana musuh sedang berlari dan mengetahui anggaran kelajuan peluru berpandu. Secara tempatan dia melihat bahawa dia mencapai sasaran pada tanda ke-33. Terima kasih kepada pampasan lag, ia juga akan muncul pada pelayan

2. Kami melakukan semuanya sama seperti dalam pilihan pertama, tetapi, setelah mengira satu tanda pada simulasi peluru, kami tidak berhenti, tetapi terus mensimulasikan penerbangannya dalam tanda pelayan yang sama, setiap kali mendekatkan masanya kepada pelayan satu persatu tandakan dan kemas kini kedudukan collider. Kami melakukan ini sehingga salah satu daripada dua perkara berlaku:

  • Peluru telah tamat tempoh. Ini bermakna bahawa pengiraan telah tamat, kita boleh mengira terlepas atau pukulan. Dan ini adalah pada tanda yang sama di mana tembakan dilepaskan! Bagi kami ini adalah tambah dan tolak. Tambahan - kerana untuk pemain menembak, ini mengurangkan kelewatan antara pukulan dan penurunan kesihatan musuh dengan ketara. Kelemahannya ialah kesan yang sama diperhatikan apabila pihak lawan melepaskan tembakan ke arah pemain: musuh, nampaknya, hanya melepaskan roket perlahan, dan kerosakan sudah dikira.
  • Peluru telah mencapai masa pelayan. Dalam kes ini, simulasinya akan diteruskan dalam tanda pelayan seterusnya tanpa sebarang pampasan lag. Untuk projektil perlahan, ini secara teori boleh mengurangkan bilangan rollback fizik berbanding pilihan pertama. Pada masa yang sama, beban tidak sekata pada simulasi meningkat: pelayan sama ada melahu, atau dalam satu tanda pelayan ia mengira sedozen tanda simulasi untuk beberapa peluru.

Cara kami mempertingkatkan mekanik pengiraan balistik untuk penembak mudah alih dengan algoritma pampasan kependaman rangkaian
Senario yang sama seperti dalam gambar sebelum ini, tetapi dikira mengikut skema kedua. Peluru berpandu "terperangkap" dengan masa pelayan pada tanda yang sama ketika tembakan berlaku, dan pukulan boleh dikira seawal tick seterusnya. Pada tanda ke-31, dalam kes ini, pampasan lag tidak lagi digunakan

Dalam pelaksanaan kami, kedua-dua pendekatan ini berbeza dalam hanya beberapa baris kod, jadi kami mencipta kedua-duanya, dan untuk masa yang lama ia wujud secara selari. Bergantung pada mekanik senjata dan kelajuan peluru, kami memilih satu atau pilihan lain untuk setiap dinosaur. Titik perubahan di sini ialah penampilan dalam permainan mekanik seperti "jika anda memukul musuh berkali-kali dalam masa yang begitu dan ini, dapatkan bonus ini dan ini." Mana-mana mekanik di mana masa di mana pemain memukul musuh memainkan peranan penting enggan bekerja dengan pendekatan kedua. Jadi kami akhirnya menggunakan pilihan pertama, dan ia kini terpakai kepada semua senjata dan semua kebolehan aktif dalam permainan.

Secara berasingan, adalah wajar membangkitkan isu prestasi. Jika anda fikir semua ini akan memperlahankan keadaan, saya jawab: memang betul. Perpaduan agak perlahan dalam menggerakkan pelanggar dan menghidupkan dan mematikannya. Dalam Skuad Dino, dalam kes "terburuk", mungkin terdapat beberapa ratus peluru yang wujud serentak dalam pertempuran. Menggerakkan pelanggar untuk mengira setiap peluru secara individu adalah kemewahan yang tidak mampu dimiliki. Oleh itu, adalah amat perlu bagi kami untuk meminimumkan bilangan "pemulihan" fizik. Untuk melakukan ini, kami mencipta komponen berasingan dalam ECS di mana kami merekodkan masa pemain. Kami menambahkannya pada semua entiti yang memerlukan pampasan lag (projektil, kebolehan, dsb.). Sebelum kami mula memproses entiti tersebut, kami mengumpulkannya pada masa ini dan memprosesnya bersama-sama, melancarkan dunia fizikal sekali untuk setiap gugusan.

Pada peringkat ini kami mempunyai sistem yang berfungsi secara amnya. Kodnya dalam bentuk yang agak ringkas:

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

Yang tinggal hanyalah mengkonfigurasi butiran:

1. Memahami berapa banyak untuk mengehadkan jarak maksimum pergerakan dalam masa.

Adalah penting bagi kami untuk menjadikan permainan itu boleh diakses sebaik mungkin dalam keadaan rangkaian mudah alih yang lemah, jadi kami mengehadkan cerita dengan margin 30 kutu (dengan kadar kutu 20 Hz). Ini membolehkan pemain memukul lawan walaupun pada ping yang sangat tinggi.

2. Tentukan objek yang boleh digerakkan dalam masa dan yang tidak boleh.

Kami, sudah tentu, menggerakkan lawan kami. Tetapi perisai tenaga boleh dipasang, sebagai contoh, tidak. Kami memutuskan bahawa adalah lebih baik untuk memberi keutamaan kepada keupayaan pertahanan, seperti yang sering dilakukan dalam penembak dalam talian. Jika pemain telah meletakkan perisai pada masa ini, peluru pampasan lag dari masa lalu tidak sepatutnya terbang melaluinya.

3. Tentukan sama ada perlu untuk mengimbangi kebolehan dinosaur: gigitan, serangan ekor, dll. Kami memutuskan perkara yang diperlukan dan memprosesnya mengikut peraturan yang sama seperti peluru.

4. Tentukan apa yang perlu dilakukan dengan pelanggar pemain yang pampasan lag dilakukan. Dengan cara yang baik, kedudukan mereka tidak sepatutnya beralih ke masa lalu: pemain harus melihat dirinya pada masa yang sama di mana dia kini berada di pelayan. Walau bagaimanapun, kami juga melancarkan pelanggar pemain menembak, dan terdapat beberapa sebab untuk ini.

Pertama, ia meningkatkan pengelompokan: kita boleh menggunakan keadaan fizikal yang sama untuk semua pemain dengan ping dekat.

Kedua, dalam semua pancaran sinar dan pertindihan kami sentiasa mengecualikan pelanggar pemain yang memiliki kebolehan atau peluru. Dalam Skuad Dino, pemain mengawal dinosaur, yang mempunyai geometri bukan standard mengikut piawaian penembak. Walaupun pemain menembak pada sudut yang luar biasa dan trajektori peluru melepasi pelanggar dinosaur pemain, peluru akan mengabaikannya.

Ketiga, kami mengira kedudukan senjata dinosaur atau titik aplikasi keupayaan menggunakan data daripada ECS walaupun sebelum permulaan pampasan lag.

Akibatnya, kedudukan sebenar pelanggar pemain yang diberi pampasan ketinggalan adalah tidak penting bagi kami, jadi kami mengambil jalan yang lebih produktif dan pada masa yang sama lebih mudah.

Kependaman rangkaian tidak boleh dialih keluar begitu sahaja, ia hanya boleh ditutup. Seperti mana-mana kaedah penyamaran yang lain, pampasan lag pelayan mempunyai kelebihannya. Ia meningkatkan pengalaman permainan pemain yang menembak dengan mengorbankan pemain yang ditembak. Bagi Skuad Dino, bagaimanapun, pilihan di sini adalah jelas.

Sudah tentu, semua ini juga perlu dibayar oleh peningkatan kerumitan kod pelayan secara keseluruhan - baik untuk pengaturcara dan pereka permainan. Jika lebih awal simulasi adalah panggilan berurutan mudah sistem, maka dengan pampasan lag, gelung bersarang dan cawangan muncul di dalamnya. Kami juga menghabiskan banyak usaha untuk menjadikannya mudah untuk bekerja.

Dalam versi 2019 (dan mungkin sedikit lebih awal), Unity menambah sokongan penuh untuk adegan fizikal bebas. Kami melaksanakannya pada pelayan hampir serta-merta selepas kemas kini, kerana kami ingin segera menyingkirkan dunia fizikal yang biasa kepada semua bilik.

Kami memberikan setiap bilik permainan adegan fizikalnya sendiri dan dengan itu menghapuskan keperluan untuk "mengosongkan" adegan daripada data bilik jiran sebelum mengira simulasi. Pertama, ia memberikan peningkatan yang ketara dalam produktiviti. Kedua, ia memungkinkan untuk menyingkirkan seluruh kelas pepijat yang timbul jika pengaturcara membuat ralat dalam kod pembersihan tempat kejadian apabila menambah elemen permainan baharu. Ralat sedemikian sukar untuk dinyahpepijat, dan ia sering mengakibatkan keadaan objek fizikal dalam pemandangan satu bilik "mengalir" ke bilik lain.

Di samping itu, kami melakukan beberapa penyelidikan sama ada adegan fizikal boleh digunakan untuk menyimpan sejarah dunia fizikal. Iaitu, secara bersyarat, peruntukkan bukan satu adegan untuk setiap bilik, tetapi 30 adegan, dan buat penimbal kitaran daripadanya, untuk menyimpan cerita. Secara umum, pilihan itu ternyata berfungsi, tetapi kami tidak melaksanakannya: ia tidak menunjukkan peningkatan produktiviti yang gila, tetapi memerlukan perubahan yang agak berisiko. Sukar untuk meramalkan bagaimana pelayan akan berkelakuan apabila bekerja untuk masa yang lama dengan begitu banyak adegan. Oleh itu, kami mengikuti peraturan: "Sekiranya ia tidak rosak, jangan membetulkannya'.

Sumber: www.habr.com

Tambah komen