Interaksi klien-server dalam perangkat PvP shooter dan server game baru: masalah dan solusi

Dalam artikel seri sebelumnya (semua tautan di akhir artikel) tentang pengembangan penembak cepat baru, kami memeriksa mekanisme arsitektur utama logika game berdasarkan ECS, dan fitur-fitur bekerja dengan penembak pada klien, khususnya, implementasi sistem untuk memprediksi tindakan pemain lokal untuk meningkatkan respons pemain terhadap permainan. . Kali ini kita akan membahas lebih rinci tentang masalah interaksi klien-server dalam kondisi koneksi jaringan seluler yang buruk dan cara-cara untuk meningkatkan kualitas permainan bagi pengguna akhir. Saya juga akan menjelaskan secara singkat arsitektur server game.




Selama pengembangan PvP sinkron baru untuk perangkat seluler, kami mengalami masalah khas genre:

  1. Kualitas koneksi klien seluler buruk. Ini adalah ping rata-rata yang relatif tinggi di wilayah 200-250 ms, dan distribusi waktu ping yang tidak stabil dengan mempertimbangkan perubahan titik akses (walaupun, bertentangan dengan kepercayaan umum, persentase kehilangan paket di jaringan seluler 3G + cukup rendah - sekitar 1%).
  2. Solusi teknis yang ada adalah kerangka kerja mengerikan yang mendorong pengembang ke kerangka kerja ketat.

Kami membuat prototipe pertama di UNet, meskipun memberlakukan pembatasan skalabilitas, kontrol atas komponen jaringan dan menambahkan ketergantungan pada koneksi yang berubah-ubah dari klien master. Kemudian kami beralih ke netcode yang ditulis sendiri di atas Server Photon , tetapi lebih lanjut tentang itu nanti.

Pertimbangkan mekanisme untuk mengatur interaksi antara klien dalam game PvP sinkron. Yang paling populer di antaranya:

  • P2P atau peer-to-peer . Semua logika pertandingan di-host di salah satu klien dan hampir tidak memerlukan biaya lalu lintas dari kami. Tetapi ruang lingkup untuk curang dan persyaratan tinggi untuk klien yang menjadi tuan rumah pertandingan, serta keterbatasan NAT tidak memungkinkan kami untuk mengambil solusi ini untuk permainan seluler.
  • Server klien . Sebaliknya, dedicated server memungkinkan Anda untuk sepenuhnya mengendalikan segala sesuatu yang terjadi dalam pertandingan (selamat tinggal, curang), dan kinerjanya memungkinkan Anda untuk menghitung beberapa hal khusus untuk proyek kami. Juga, banyak penyedia hosting besar memiliki struktur subnet mereka sendiri, yang memberikan penundaan minimal bagi pengguna akhir.

Diputuskan untuk menulis server otoriter.


Jaringan dengan peer-to-peer (kiri) dan client-server (kanan)

Transfer data antara klien dan server


Kami menggunakan Server Photon - ini memungkinkan kami untuk dengan cepat menyebarkan infrastruktur yang diperlukan untuk proyek berdasarkan skema yang sudah berjalan selama bertahun-tahun (dalam Robot Perang kami menggunakannya).

Server Photon secara eksklusif merupakan solusi transportasi bagi kami, tanpa desain tingkat tinggi yang sangat terkait dengan mesin permainan tertentu. Yang memberikan beberapa keuntungan, karena perpustakaan transfer data dapat diganti kapan saja.

Server game adalah aplikasi multi-utas dalam wadah Foton. Aliran terpisah dibuat untuk setiap kecocokan, yang merangkum seluruh logika kerja dan mencegah pengaruh satu kecocokan pada kecocokan lainnya. Semua koneksi server dikendalikan oleh Photon, dan data yang datang dari klien ditambahkan ke antrian, yang kemudian diurai menjadi ECS.


Skema umum aliran pertandingan dalam wadah Server Photon

Setiap pertandingan terdiri dari beberapa tahap:

  1. Antrean klien game dalam layanan yang disebut pencocokan. Segera setelah jumlah pemain yang memenuhi persyaratan tertentu dikumpulkan di dalamnya, ia melaporkan hal ini ke server permainan menggunakan gRPC. Pada saat yang sama, semua data yang diperlukan untuk membuat game dikirimkan.


    Skema umum untuk membuat kecocokan
  2. Di server permainan, inisialisasi pertandingan dimulai. Semua parameter kecocokan diproses dan disiapkan, termasuk data peta, serta semua data pelanggan yang diterima dari layanan pembuatan kecocokan. Memproses dan menyiapkan data menyiratkan bahwa kami mem-parsing semua data yang diperlukan dan menulisnya ke subset entitas khusus yang kami sebut RuleBook. Ini menyimpan statistik pertandingan (yang tidak berubah selama kursus) dan akan dikirimkan ke semua klien selama koneksi dan otorisasi pada server game sekali atau ketika menghubungkan kembali setelah kehilangan koneksi. Data pertandingan statis termasuk konfigurasi peta (penyajian peta oleh komponen ECS yang menghubungkannya dengan mesin fisik), data pelanggan (nama panggilan, seperangkat senjata yang mereka miliki dan tidak berubah selama pertempuran, dll.).
  3. Menjalankan pertandingan. Sistem ECS yang membentuk gim di server mulai bekerja. Semua sistem berdetak 30 frame per detik.
  4. Setiap frame membaca dan membongkar input atau salinan pemain jika pemain tidak mengirim input mereka dalam interval tertentu.
  5. Kemudian, dalam bingkai yang sama, input diproses dalam sistem ECS, yaitu: perubahan status pemain; dunia yang dia pengaruhi dengan masukannya; dan status pemain lain.
  6. Pada akhir frame, negara dunia yang dihasilkan dikemas untuk pemain dan dikirim melalui jaringan.
  7. Pada akhir pertandingan, hasilnya dikirim ke klien dan ke layanan mikro, yang memproses hadiah untuk pertempuran menggunakan gRPC, serta analis untuk pertandingan.
  8. Setelah itu, aliran korek terjepit dan aliran menutup.


Urutan tindakan di server dalam satu bingkai

Di sisi klien, proses menghubungkan ke pertandingan adalah sebagai berikut:

  1. Pertama, permintaan dibuat untuk antri dalam layanan untuk membuat pertandingan melalui websocket dengan serialisasi melalui protobuf.
  2. Saat membuat pertandingan, layanan ini memberi tahu klien tentang alamat server game dan mentransfer payload tambahan yang diperlukan oleh klien sebelum pertandingan. Sekarang klien siap untuk memulai proses otorisasi di server game.
  3. Klien membuat soket UDP dan mulai mengirim permintaan ke server game untuk terhubung ke pertandingan bersama dengan beberapa kredensial. Server sudah menunggu klien ini. Saat terhubung, ia memberinya semua data yang diperlukan untuk memulai permainan dan menampilkan dunia untuk pertama kalinya. Ini termasuk: RuleBook (daftar data statis untuk pertandingan), serta StringIntMap, yang kami sebut sebagai data tentang garis yang digunakan dalam gameplay yang akan diidentifikasi oleh bilangan bulat selama pertandingan). Ini diperlukan untuk menghemat lalu lintas, karena garis yang lewat setiap frame menciptakan beban signifikan pada jaringan. Misalnya, semua nama pemain, nama kelas, pengidentifikasi senjata, akun, dan sejenisnya, semua informasi ditulis ke StringIntMap, di mana ia dikodekan menggunakan data integer sederhana.

Ketika seorang pemain secara langsung mempengaruhi pengguna lain (menyebabkan kerusakan, memaksakan efek, dll.), Sejarah negara dicari di server untuk membandingkan dunia game yang benar-benar dilihat oleh klien dalam centang simulasi spesifik dengan apa yang terjadi di server dengan yang lain pada saat itu entitas game.

Misalnya, Anda menembak klien Anda. Bagi Anda, ini terjadi secara instan, tetapi klien telah "melarikan diri" untuk beberapa waktu ke depan dibandingkan dengan dunia sekitarnya, yang ia tampilkan. Oleh karena itu, karena prediksi lokal mengenai perilaku pemain, server perlu memahami di mana dan dalam kondisi apa lawan berada pada saat tembakan (mungkin mereka sudah mati atau, sebaliknya, kebal). Server memeriksa semua faktor dan memberikan putusannya atas kerusakan yang terjadi.


Permintaan untuk membuat pertandingan, menghubungkan ke server game dan otorisasi

Serialisasi dan deserialisasi, pengemasan, dan pembongkaran byte pertama pertandingan


Kami memiliki serialisasi data biner yang dipatenkan, dan untuk transfer data kami menggunakan UDP.

UDP adalah opsi yang paling jelas untuk mengirim pesan dengan cepat antara klien dan server, di mana biasanya jauh lebih penting untuk menampilkan data sesegera mungkin daripada menampilkannya secara prinsip. Paket yang hilang membuat penyesuaian, tetapi masalah diselesaikan untuk setiap kasus secara individual, seperti Karena data terus-menerus berasal dari klien ke server dan kembali, Anda dapat memasukkan konsep koneksi antara klien dan server.

Untuk membuat kode yang optimal dan nyaman berdasarkan deskripsi deklaratif dari struktur ECS kami, kami menggunakan pembuatan kode. Saat membuat komponen, aturan serialisasi dan deserialisasi juga dibuat untuknya. Serialisasi didasarkan pada paket biner khusus yang memungkinkan Anda mengemas data dengan cara yang paling ekonomis. Kumpulan byte yang diperoleh selama operasinya bukan yang paling optimal, tetapi memungkinkan Anda untuk membuat aliran dari mana Anda dapat membaca beberapa paket data tanpa perlu deserialisasi lengkap.

Batas transfer data sebesar 1500 byte (alias MTU), pada kenyataannya, adalah ukuran paket maksimum yang dapat ditransfer melalui Ethernet. Properti ini dapat dikonfigurasi pada setiap hop jaringan dan seringkali bahkan di bawah 1500 byte. Apa yang terjadi jika saya mengirim paket yang lebih besar dari 1500 byte? Fragmentasi paket dimulai. Yaitu setiap paket akan secara paksa dibagi menjadi beberapa bagian, yang akan dikirim secara terpisah dari satu antarmuka ke antarmuka lainnya. Mereka dapat dikirim dengan rute yang sangat berbeda, dan waktu untuk menerima paket tersebut dapat meningkat secara signifikan sebelum lapisan jaringan mengeluarkan paket terpaku ke aplikasi Anda.

Dalam hal Photon, perpustakaan secara paksa mulai mengirim paket-paket seperti itu dalam mode UDP yang andal. Yaitu Foton akan menunggu setiap fragmen paket, serta meneruskan fragmen yang hilang jika hilang selama penerusan. Tetapi pekerjaan seperti itu dari bagian jaringan tidak dapat diterima dalam permainan di mana penundaan jaringan minimum diperlukan. Oleh karena itu, disarankan untuk mengurangi ukuran paket yang diteruskan ke minimum dan tidak melebihi 1500 byte yang direkomendasikan (dalam permainan kami, ukuran satu negara penuh dunia tidak melebihi 1000 byte; ukuran paket dengan kompresi delta adalah 200 byte).

Setiap paket dari server memiliki header pendek yang berisi beberapa byte yang menggambarkan jenis paket. Klien pertama-tama membongkar set byte ini dan menentukan paket mana yang kita hadapi. Kami sangat bergantung pada properti ini dari mekanisme deserialisasi kami selama otorisasi: agar tidak melebihi ukuran paket yang disarankan 1500 byte, kami memecah paket RuleBook dan StringIntMap ke dalam beberapa tahap; dan untuk memahami apa yang sebenarnya kita dapatkan dari server - aturan main atau status itu sendiri - kita menggunakan header paket.

Ketika mengembangkan fitur-fitur baru proyek, ukuran paket terus berkembang. Ketika kami mengalami masalah ini, diputuskan untuk menulis sistem kompresi delta kami sendiri, serta kliping data kontekstual yang tidak dibutuhkan klien.

Optimasi lalu lintas jaringan yang sensitif terhadap konteks. Kompresi Delta


Kliping data kontekstual ditulis secara manual berdasarkan data apa yang dibutuhkan klien untuk menampilkan dunia dan prediksi lokal dari data mereka sendiri agar berfungsi dengan benar. Kemudian, kompresi delta diterapkan pada data yang tersisa.

Permainan kami setiap kutu menghasilkan keadaan dunia baru, yang harus dikemas dan diteruskan ke pelanggan. Biasanya, kompresi delta adalah untuk pertama mengirim keadaan penuh dengan semua data yang diperlukan kepada klien, dan kemudian hanya mengirim perubahan pada data ini. Ini dapat direpresentasikan sebagai berikut:

deltaGameState = newGameState - prevGameState

Tetapi untuk setiap klien, data yang berbeda dikirim dan hilangnya hanya satu paket dapat mengarah pada kenyataan bahwa Anda harus meneruskan keadaan penuh dunia.

Meneruskan status penuh dunia adalah tugas yang agak mahal untuk jaringan. Oleh karena itu, kami memodifikasi pendekatan dan mengirimkan perbedaan antara kondisi dunia saat ini yang diproses dan yang diterima secara tepat oleh klien. Untuk melakukan ini, klien dalam paketnya dengan input juga mengirimkan nomor kutu, yang merupakan pengidentifikasi unik dari status permainan yang telah ia terima dengan tepat. Sekarang server tahu berdasarkan kondisi apa yang diperlukan untuk membangun kompresi delta. Klien biasanya tidak punya waktu untuk mengirimi server nomor kutu yang dimilikinya sebelum server menyiapkan bingkai berikutnya dengan data. Oleh karena itu, pada klien ada sejarah negara server di dunia, yang diterapkan patch deltaGameState oleh server.


Ilustrasi frekuensi interaksi klien-server dalam proyek

Mari kita membahas lebih detail tentang apa yang dikirim klien. Dalam penembak klasik, paket seperti itu disebut ClientCmd dan berisi informasi tentang tombol yang ditekan pemain dan waktu tim dibuat. Di dalam paket input, kami mengirim lebih banyak data:

public sealed class InputSample { //  ,        public uint WorldTick; // ,      ,     public uint PlayerSimulationTick; //   .  (idle, , ) public MovementMagnitude MovementMagnitude; //  ,   public float MovementAngle; //    public AimMagnitude AimMagnitude; //    public float AimAngle; //   ,       public uint ShotTarget; //    ,        public float AimMagnitudeCompressed; } 


Ada beberapa poin menarik. Pertama, klien memberi tahu server tempat centang melihat semua objek dari dunia game di sekitarnya yang tidak dapat diprediksi (WorldTick). Tampaknya klien mampu "menghentikan" waktu untuk dunia, dan berlari serta menembak semua orang sendiri karena prediksi lokal. Ini tidak benar. Kami hanya mempercayai seperangkat nilai terbatas dari klien dan jangan biarkan dia menembak ke masa lalu selama lebih dari 1 detik. Bidang WorldTick juga digunakan sebagai paket pengakuan, atas dasar mana kompresi delta dibangun.

Anda dapat menemukan nomor floating point dalam suatu paket. Biasanya, nilai-nilai tersebut sering digunakan untuk mengambil bacaan dari joystick pemain, tetapi nilai-nilai tersebut tidak ditransmisikan dengan baik melalui jaringan, karena memiliki "bouncing" besar dan biasanya terlalu akurat. Kami menghitung angka-angka tersebut dan mengemasnya menggunakan biner packer sehingga tidak melebihi nilai integer yang dapat ditampung dalam beberapa bit, tergantung pada ukurannya. Dengan demikian, pengemasan input dari joystick pengarah rusak:

 if (Math.Abs(s.AimMagnitudeCompressed) < float.Epsilon) { packer.PackByte(0, 1); } else { packer.PackByte(1, 1); float min = 0; float max = 1; float step = 0.001f; //     1000    , //          //     packer.PackUInt32((uint)((s.AimMagnitudeCompressed - min)/step), CalcFloatRangeBits(min, max, step)); } 


Fitur menarik lainnya saat mengirim input adalah beberapa perintah dapat dikirim beberapa kali. Sangat sering kita ditanya apa yang harus dilakukan jika seseorang telah menekan kemampuan tertinggi, dan paket dengan inputnya telah hilang? Kami hanya mengirim input ini beberapa kali. Ini terlihat seperti pengiriman yang dijamin, tetapi lebih fleksibel dan lebih cepat. Karena ukuran paket input sangat kecil, kita dapat mengemas beberapa input pemain yang berdekatan ke dalam paket yang dihasilkan. Saat ini, ukuran jendela yang menentukan jumlahnya adalah lima.


Paket input yang dihasilkan pada klien di setiap centang dan dikirim ke server

Transmisi data jenis ini adalah yang tercepat dan paling andal untuk menyelesaikan masalah kita tanpa menggunakan UDP yang andal. Kami melanjutkan dari fakta bahwa kemungkinan kehilangan sejumlah paket dalam satu baris sangat rendah dan merupakan indikator penurunan kualitas jaringan secara keseluruhan. Jika ini terjadi, server cukup menyalin input yang terakhir diterima dari pemain dan menerapkannya, berharap itu tidak berubah.

Jika klien menyadari bahwa ia tidak menerima paket melalui jaringan untuk waktu yang sangat lama, proses menghubungkan kembali ke server dimulai. Server, untuk bagiannya, memantau bahwa antrian input dari pemain selesai.

Alih-alih kesimpulan dan referensi


Ada banyak sistem lain di server game yang bertanggung jawab untuk mendeteksi, men-debug, dan mengedit pertandingan "by the gain", desainer game memperbarui konfigurasi tanpa memulai ulang, masuk, dan memantau status server. Kami juga ingin menulis tentang ini secara lebih rinci, tetapi secara terpisah.

Pertama-tama, ketika mengembangkan permainan jaringan pada platform seluler, Anda harus memperhatikan operasi yang benar dari klien Anda dengan ping tinggi (sekitar 200 ms), kehilangan data yang sedikit lebih sering, serta ukuran data yang dikirim. Dan Anda harus secara jelas masuk ke dalam batas paket 1500 byte untuk menghindari fragmentasi dan penundaan lalu lintas.

Tautan yang bermanfaat:


Artikel sebelumnya tentang proyek:

  1. "Bagaimana Kami Mengayun di Mobile Fast Paced Shooter: Teknologi dan Pendekatan . "
  2. "Bagaimana dan mengapa kami menulis ECS kami . "
  3. "Ketika kami menulis kode jaringan penembak PvP seluler: sinkronisasi pemain pada klien . "

Source: https://habr.com/ru/post/id420019/


All Articles