Bagaimana kami meningkatkan mekanisme penghitungan balistik untuk penembak seluler dengan algoritme kompensasi latensi jaringan

Bagaimana kami meningkatkan mekanisme penghitungan balistik untuk penembak seluler dengan algoritme kompensasi latensi jaringan

Hai, saya Nikita Brizhak, pengembang server dari Pixonic. Hari ini saya ingin berbicara tentang mengkompensasi kelambatan dalam multipemain seluler.

Banyak artikel telah ditulis tentang kompensasi lag server, termasuk dalam bahasa Rusia. Hal ini tidak mengherankan, karena teknologi ini telah aktif digunakan dalam pembuatan FPS multipemain sejak akhir tahun 90an. Misalnya, Anda dapat mengingat mod QuakeWorld, yang merupakan salah satu mod pertama yang menggunakannya.

Kami juga menggunakannya di penembak multipemain seluler Dino Squad.

Dalam artikel ini, tujuan saya bukan untuk mengulangi apa yang telah ditulis ribuan kali, tetapi untuk memberi tahu bagaimana kami menerapkan kompensasi lag dalam game kami, dengan mempertimbangkan tumpukan teknologi dan fitur gameplay inti kami.

Sedikit penjelasan tentang korteks dan teknologi kita.

Dino Squad adalah penembak PvP seluler jaringan. Pemain mengontrol dinosaurus yang dilengkapi dengan berbagai senjata dan bertarung satu sama lain dalam tim 6v6.

Baik klien dan server didasarkan pada Unity. Arsitekturnya cukup klasik untuk penembak: servernya otoriter, dan prediksi klien berfungsi pada klien. Simulasi permainan ditulis menggunakan ECS internal dan digunakan di server dan klien.

Jika ini pertama kalinya Anda mendengar tentang kompensasi lag, berikut adalah penjelasan singkat mengenai masalah tersebut.

Dalam game FPS multipemain, pertandingan biasanya disimulasikan di server jarak jauh. Pemain mengirimkan masukan mereka (informasi tentang tombol yang ditekan) ke server, dan sebagai tanggapan, server mengirimi mereka status permainan yang diperbarui dengan mempertimbangkan data yang diterima. Dengan skema interaksi ini, jeda antara menekan tombol maju dan saat karakter pemain bergerak di layar akan selalu lebih besar daripada ping.

Meskipun di jaringan lokal penundaan ini (populer disebut input lag) mungkin tidak terlalu terasa, saat bermain melalui Internet akan menimbulkan perasaan β€œmeluncur di atas es” saat mengontrol karakter. Masalah ini sangat relevan untuk jaringan seluler, di mana kasus ketika ping pemain mencapai 200 ms masih dianggap sebagai koneksi yang sangat baik. Seringkali pingnya bisa 350, 500, atau 1000 ms. Maka menjadi hampir mustahil untuk memainkan penembak cepat dengan input lag.

Solusi untuk masalah ini adalah prediksi simulasi sisi klien. Di sini klien sendiri yang menerapkan input ke karakter pemain, tanpa menunggu respons dari server. Dan ketika jawabannya diterima, ia hanya membandingkan hasilnya dan memperbarui posisi lawannya. Penundaan antara menekan tombol dan menampilkan hasilnya di layar dalam hal ini minimal.

Penting untuk memahami nuansanya di sini: klien selalu menggambar dirinya sendiri sesuai dengan input terakhirnya, dan musuh - dengan penundaan jaringan, sesuai dengan keadaan sebelumnya dari data dari server. Artinya, saat menembaki musuh, pemain melihatnya di masa lalu dibandingkan dirinya sendiri. Lebih lanjut tentang prediksi klien kami menulis sebelumnya.

Jadi, prediksi klien memecahkan satu masalah, tetapi menciptakan masalah lain: jika seorang pemain menembak pada titik di mana musuh berada di masa lalu, di server ketika menembak pada titik yang sama, musuh mungkin tidak lagi berada di tempat itu. Kompensasi kelambatan server berupaya mengatasi masalah ini. Ketika senjata ditembakkan, server mengembalikan status permainan yang dilihat pemain secara lokal pada saat tembakan, dan memeriksa apakah dia benar-benar dapat mengenai musuh. Jika jawabannya β€œya”, pukulan akan dihitung, meskipun musuh tidak lagi berada di server pada saat itu.

Berbekal pengetahuan tersebut, kami mulai menerapkan kompensasi lag server di Dino Squad. Pertama-tama, kita harus memahami cara memulihkan di server apa yang dilihat klien? Dan apa sebenarnya yang perlu dipulihkan? Dalam game kami, serangan dari senjata dan kemampuan dihitung melalui pancaran sinar dan overlay - yaitu, melalui interaksi dengan penumbuk fisik musuh. Oleh karena itu, kami perlu mereproduksi posisi collider ini, yang β€œdilihat” oleh pemain secara lokal, di server. Saat itu kami menggunakan Unity versi 2018.x. API fisika di sana bersifat statis, dunia fisik ada dalam satu salinan. Tidak ada cara untuk menyimpan statusnya dan memulihkannya dari kotak. Jadi apa yang harus dilakukan?

Solusinya ada di permukaan; semua elemennya telah kami gunakan untuk memecahkan masalah lain:

  1. Untuk setiap klien, kita perlu mengetahui jam berapa dia melihat lawan ketika dia menekan tombol. Kami telah menulis informasi ini ke dalam paket input dan menggunakannya untuk menyesuaikan prediksi klien.
  2. Kita harus bisa menyimpan sejarah status permainan. Di situlah kita akan mempertahankan posisi lawan kita (dan juga penumbuk mereka). Kami sudah memiliki riwayat status di server, kami menggunakannya untuk membangun delta. Mengetahui waktu yang tepat, kita dapat dengan mudah menemukan keadaan yang tepat dalam sejarah.
  3. Sekarang kita sudah memiliki keadaan permainan dari sejarah, kita harus bisa menyinkronkan data pemain dengan keadaan dunia fisik. Penumbuk yang ada - pindahkan, yang hilang - buat, yang tidak perlu - hancurkan. Logika ini juga sudah ditulis dan terdiri dari beberapa sistem ECS. Kami menggunakannya untuk mengadakan beberapa ruang permainan dalam satu proses Unity. Dan karena dunia fisik adalah satu per proses, maka harus digunakan kembali antar ruangan. Sebelum simulasi dimulai, kami "mengatur ulang" keadaan dunia fisik dan menginisialisasinya kembali dengan data untuk ruangan saat ini, mencoba menggunakan kembali objek permainan Unity sebanyak mungkin melalui sistem pengumpulan yang cerdas. Yang tersisa hanyalah menggunakan logika yang sama untuk keadaan permainan di masa lalu.

Dengan menggabungkan semua elemen ini, kita mendapatkan β€œmesin waktu” yang dapat mengembalikan keadaan dunia fisik ke momen yang tepat. Kodenya ternyata sederhana:

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 tersisa hanyalah mencari cara menggunakan mesin ini untuk mengimbangi tembakan dan kemampuan dengan mudah.

Dalam kasus yang paling sederhana, ketika mekanismenya didasarkan pada satu hitscan, semuanya tampak jelas: sebelum pemain menembak, ia perlu mengembalikan dunia fisik ke keadaan yang diinginkan, melakukan raycast, menghitung hit atau miss, dan mengembalikan dunia ke keadaan awal.

Tapi mekanik seperti itu di Dino Squad sangat sedikit! Sebagian besar senjata dalam game ini membuat proyektil - peluru berumur panjang yang terbang selama beberapa tick simulasi (dalam beberapa kasus, puluhan tick). Apa yang harus dilakukan dengan mereka, jam berapa mereka harus terbang?

Π’ artikel kuno tentang tumpukan jaringan Half-Life, orang-orang dari Valve menanyakan pertanyaan yang sama, dan jawaban mereka adalah ini: kompensasi kelambatan proyektil bermasalah, dan lebih baik menghindarinya.

Kami tidak memiliki opsi ini: senjata berbasis proyektil adalah fitur utama desain game. Jadi kami harus memikirkan sesuatu. Setelah sedikit bertukar pikiran, kami merumuskan dua opsi yang tampaknya berhasil:

1. Kami mengikat proyektil dengan waktu pemain yang membuatnya. Setiap detak simulasi server, untuk setiap peluru dari setiap pemain, kami mengembalikan dunia fisik ke status klien dan melakukan perhitungan yang diperlukan. Pendekatan ini memungkinkan untuk memiliki beban terdistribusi di server dan waktu penerbangan proyektil yang dapat diprediksi. Prediktabilitas sangat penting bagi kami, karena kami memiliki semua proyektil, termasuk proyektil musuh, yang diprediksi pada klien.

Bagaimana kami meningkatkan mekanisme penghitungan balistik untuk penembak seluler dengan algoritme kompensasi latensi jaringan
Dalam gambar, pemain di centang 30 menembakkan rudal sebagai antisipasi: dia melihat ke arah mana musuh berlari dan mengetahui perkiraan kecepatan rudal. Secara lokal dia melihat bahwa dia mencapai target pada tick ke-33. Berkat kompensasi lag, itu juga akan muncul di server

2. Kami melakukan semuanya sama seperti pada opsi pertama, tetapi, setelah menghitung satu tick dari simulasi peluru, kami tidak berhenti, tetapi terus mensimulasikan penerbangannya dalam tick server yang sama, setiap kali mendekatkan waktunya ke server centang satu per satu dan perbarui posisi collider. Kami melakukan ini sampai salah satu dari dua hal terjadi:

  • Pelurunya sudah habis masa berlakunya. Artinya perhitungan sudah selesai, kita bisa menghitung meleset atau mengenai sasaran. Dan ini pada saat yang sama saat tembakan dilepaskan! Bagi kami ini merupakan plus dan minus. Nilai tambah - karena bagi pemain yang menembak, hal ini secara signifikan mengurangi jeda antara pukulan dan penurunan kesehatan musuh. Sisi negatifnya adalah efek yang sama terjadi ketika lawan menembaki pemain: musuh tampaknya hanya menembakkan roket lambat, dan kerusakan sudah dihitung.
  • Peluru telah mencapai waktu server. Dalam hal ini, simulasinya akan berlanjut di server berikutnya tanpa kompensasi lag apa pun. Untuk proyektil lambat, ini secara teoritis dapat mengurangi jumlah kemunduran fisika dibandingkan dengan opsi pertama. Pada saat yang sama, beban simulasi yang tidak merata meningkat: server dalam keadaan menganggur, atau dalam satu tick server menghitung selusin tick simulasi untuk beberapa poin.

Bagaimana kami meningkatkan mekanisme penghitungan balistik untuk penembak seluler dengan algoritme kompensasi latensi jaringan
Skenarionya sama seperti pada gambar sebelumnya, tetapi dihitung berdasarkan skema kedua. Rudal β€œmengejar” waktu server pada waktu yang sama dengan saat tembakan terjadi, dan pukulannya dapat dihitung paling awal pada waktu berikutnya. Pada tick ke-31, dalam hal ini, kompensasi lag tidak lagi diterapkan

Dalam implementasi kami, kedua pendekatan ini hanya berbeda dalam beberapa baris kode, jadi kami membuat keduanya, dan untuk waktu yang lama keduanya ada secara paralel. Bergantung pada mekanisme senjata dan kecepatan peluru, kami memilih satu atau beberapa opsi untuk setiap dinosaurus. Titik balik disini adalah munculnya mekanisme dalam permainan seperti β€œkalau kamu memukul musuh berkali-kali dalam waktu ini dan itu, dapatkan bonus ini dan itu”. Mekanik mana pun yang waktu pemain memukul musuh memainkan peran penting menolak bekerja dengan pendekatan kedua. Jadi kami akhirnya memilih opsi pertama, dan sekarang ini berlaku untuk semua senjata dan semua kemampuan aktif dalam game.

Secara terpisah, ada baiknya mengangkat masalah kinerja. Jika Anda berpikir bahwa semua ini akan memperlambat segalanya, saya menjawab: ya. Unity cukup lambat dalam menggerakkan collider dan menyalakan dan mematikannya. Di Pasukan Dino, dalam kasus "terburuk", mungkin ada beberapa ratus proyektil yang muncul secara bersamaan dalam pertempuran. Memindahkan collider untuk menghitung setiap proyektil satu per satu adalah kemewahan yang tidak terjangkau. Oleh karena itu, sangatlah penting bagi kita untuk meminimalkan jumlah β€œkemunduran” fisika. Untuk melakukan ini, kami membuat komponen terpisah di ECS tempat kami mencatat waktu pemain. Kami menambahkannya ke semua entitas yang memerlukan kompensasi lag (proyektil, kemampuan, dll.). Sebelum kami mulai memproses entitas tersebut, kami mengelompokkannya pada saat ini dan memprosesnya bersama-sama, mengembalikan dunia fisik satu kali untuk setiap cluster.

Pada tahap ini kami memiliki sistem yang berfungsi secara umum. Kodenya dalam bentuk yang agak disederhanakan:

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 tersisa hanyalah mengonfigurasi detailnya:

1. Pahami seberapa besar batasan jarak pergerakan maksimum dalam waktu.

Penting bagi kami untuk membuat game ini dapat diakses semaksimal mungkin dalam kondisi jaringan seluler yang buruk, jadi kami membatasi cerita dengan margin 30 tick (dengan tick rate 20 Hz). Hal ini memungkinkan pemain untuk memukul lawan bahkan pada ping yang sangat tinggi.

2. Menentukan benda mana yang dapat dipindahkan dalam waktu dan mana yang tidak.

Kami, tentu saja, menggerakkan lawan kami. Namun perisai energi yang dapat dipasang, misalnya, tidak demikian. Kami memutuskan bahwa lebih baik memberi prioritas pada kemampuan bertahan, seperti yang sering dilakukan pada penembak online. Jika pemain telah memasang perisai di masa sekarang, peluru dengan kompensasi lag dari masa lalu tidak akan terbang melewatinya.

3. Putuskan apakah perlu untuk mengimbangi kemampuan dinosaurus: gigitan, serangan ekor, dll. Kami memutuskan apa yang diperlukan dan memprosesnya sesuai dengan aturan yang sama seperti peluru.

4. Tentukan apa yang harus dilakukan dengan collider pemain yang melakukan kompensasi lag. Dalam cara yang baik, posisi mereka tidak boleh bergeser ke masa lalu: pemain harus melihat dirinya dalam waktu yang sama saat dia berada di server. Namun, kami juga mengembalikan tabrakan pemain penembak, dan ada beberapa alasan untuk ini.

Pertama, ini meningkatkan pengelompokan: kita dapat menggunakan kondisi fisik yang sama untuk semua pemain dengan ping yang dekat.

Kedua, dalam semua raycast dan overlap kami selalu mengecualikan collider dari pemain yang memiliki kemampuan atau proyektil. Di Dino Squad, pemain mengontrol dinosaurus, yang memiliki geometri non-standar menurut standar penembak. Sekalipun pemain menembak pada sudut yang tidak biasa dan lintasan peluru melewati penumbuk dinosaurus milik pemain, peluru akan mengabaikannya.

Ketiga, kami menghitung posisi senjata dinosaurus atau titik penerapan kemampuan menggunakan data dari ECS bahkan sebelum dimulainya kompensasi lag.

Akibatnya, posisi sebenarnya dari pemain yang mendapat kompensasi lag tidak penting bagi kami, jadi kami mengambil jalur yang lebih produktif dan sekaligus lebih sederhana.

Latensi jaringan tidak dapat dihilangkan begitu saja, melainkan hanya dapat ditutupi. Seperti metode penyamaran lainnya, kompensasi kelambatan server memiliki konsekuensi tersendiri. Ini meningkatkan pengalaman bermain game pemain yang menembak dengan mengorbankan pemain yang ditembak. Namun, bagi Dino Squad, pilihannya sudah jelas.

Tentu saja, semua ini juga harus dibayar dengan meningkatnya kompleksitas kode server secara keseluruhan - baik untuk programmer maupun desainer game. Jika sebelumnya simulasi adalah panggilan sistem sekuensial sederhana, maka dengan kompensasi lag, loop dan cabang bersarang muncul di dalamnya. Kami juga menghabiskan banyak upaya untuk membuatnya nyaman untuk digunakan.

Pada versi 2019 (dan mungkin sedikit lebih awal), Unity menambahkan dukungan penuh untuk adegan fisik independen. Kami menerapkannya di server segera setelah pembaruan, karena kami ingin segera menyingkirkan dunia fisik yang umum di semua ruangan.

Kami memberikan setiap ruang permainan adegan fisiknya sendiri dan dengan demikian menghilangkan kebutuhan untuk β€œmenghapus” adegan dari data ruang tetangga sebelum menghitung simulasi. Pertama, hal ini memberikan peningkatan produktivitas yang signifikan. Kedua, ini memungkinkan untuk menghilangkan seluruh kelas bug yang muncul jika programmer membuat kesalahan dalam kode pembersihan adegan saat menambahkan elemen game baru. Kesalahan seperti itu sulit untuk di-debug, dan sering kali mengakibatkan keadaan objek fisik dalam adegan satu ruangan "mengalir" ke ruangan lain.

Selain itu, kami melakukan penelitian apakah pemandangan fisik dapat digunakan untuk menyimpan sejarah dunia fisik. Yaitu, secara kondisional, mengalokasikan bukan hanya satu adegan ke setiap ruangan, tetapi 30 adegan, dan membuat buffer siklik darinya, untuk menyimpan cerita. Secara umum, opsi tersebut ternyata berhasil, tetapi kami tidak menerapkannya: opsi tersebut tidak menunjukkan peningkatan produktivitas yang gila-gilaan, namun memerlukan perubahan yang agak berisiko. Sulit untuk memprediksi bagaimana server akan berperilaku ketika bekerja dalam waktu lama dengan begitu banyak adegan. Oleh karena itu, kami mengikuti aturan: β€œJika tidak rusak, jangan perbaiki'.

Sumber: www.habr.com

Tambah komentar