Bagaimana cara menangani netcode?

10

Saya tertarik untuk mengevaluasi berbagai cara yang berbeda bahwa netcode dapat "menghubungkan ke" mesin gim. Saya sedang mendesain permainan multipemain sekarang, dan sejauh ini saya telah menentukan bahwa saya perlu (paling tidak) memiliki utas terpisah untuk menangani soket jaringan, berbeda dari sisa mesin yang menangani loop grafis dan skrip.

Saya memang memiliki satu cara potensial untuk membuat game jaringan sepenuhnya single-threaded, yaitu memeriksanya setelah merender setiap frame, menggunakan soket non-blocking. Namun ini jelas tidak optimal karena waktu yang diperlukan untuk membuat bingkai ditambahkan ke jeda jaringan: Pesan yang tiba melalui jaringan harus menunggu sampai bingkai saat ini membuat (dan logika game) selesai. Tapi, setidaknya dengan cara ini gameplaynya akan tetap lancar, kurang lebih.

Memiliki utas terpisah untuk jaringan memungkinkan game menjadi sepenuhnya responsif terhadap jaringan, misalnya ia dapat mengirim kembali paket ACK secara instan setelah menerima pembaruan status dari server. Tapi saya agak bingung tentang cara terbaik untuk berkomunikasi antara kode permainan dan kode jaringan. Utas jaringan akan mendorong paket yang diterima ke antrian, dan utas permainan akan membaca dari antrian pada waktu yang tepat selama perulangannya, jadi kami belum menyingkirkan keterlambatan bingkai hingga satu ini.

Juga, sepertinya saya ingin utas yang menangani pengiriman paket terpisah dari yang memeriksa paket yang turun, karena tidak akan dapat mengirim satu saat berada di tengah-tengah memeriksa apakah ada pesan masuk. Saya berpikir tentang fungsi selectatau sejenisnya.

Saya kira pertanyaan saya adalah, apa cara terbaik untuk mendesain game untuk respon jaringan yang terbaik? Jelas klien harus mengirim input pengguna sesegera mungkin ke server, jadi saya bisa meminta kode net-send segera setelah loop pemrosesan acara, keduanya di dalam loop game. Apakah ini masuk akal?

Steven Lu
sumber

Jawaban:

13

Abaikan responsif. Pada LAN, ping tidak signifikan. Di internet, 60-100ms pulang pergi adalah berkah. Berdoalah kepada para dewa lag agar Anda tidak mendapatkan paku> 3K. Perangkat lunak Anda harus dijalankan dengan jumlah pembaruan / detik yang sangat rendah untuk ini menjadi masalah. Jika Anda memotret selama 25 pembaruan / detik, maka Anda memiliki waktu maksimum 40ms antara saat Anda menerima paket dan menindaklanjutinya. Dan itu adalah kasus single-threaded ...

Rancang sistem Anda untuk fleksibilitas dan kebenaran. Inilah ide saya tentang cara menghubungkan subsistem jaringan ke dalam kode permainan: Pesan. Solusi untuk banyak masalah bisa berupa "pesan". Saya pikir pesan menyembuhkan kanker pada tikus lab sekali. Pesan menghemat saya $ 200 atau lebih pada asuransi mobil saya. Namun serius, olahpesan mungkin merupakan cara terbaik untuk melampirkan subsistem apa pun ke kode game sambil tetap mempertahankan dua subsistem independen.

Gunakan olahpesan untuk komunikasi apa pun antara subsistem jaringan dan mesin game, dan untuk hal tersebut di antara dua subsistem. Olahpesan antar subsistem bisa sesederhana gumpalan data yang dilewatkan oleh pointer menggunakan std :: list.

Cukup minta antrian pesan keluar dan referensi ke mesin game di subsistem jaringan. Gim ini dapat membuang pesan yang ingin dikirim ke antrian keluar dan mengirimnya secara otomatis, atau mungkin ketika beberapa fungsi seperti "flushMessages ()" dipanggil. Jika mesin permainan memiliki satu antrian pesan bersama yang besar, maka semua subsistem yang diperlukan untuk mengirim pesan (logika, AI, fisika, jaringan, dll.) Semua dapat membuang pesan ke dalamnya di mana lingkaran permainan utama kemudian dapat membaca semua pesan dan kemudian bertindak sesuai.

Saya akan mengatakan bahwa menjalankan soket pada utas lain baik-baik saja, meskipun tidak diperlukan. Satu-satunya masalah dengan desain ini adalah bahwa umumnya asynchronous (Anda tidak tahu persis kapan paket sedang dikirim) dan yang dapat membuatnya sulit untuk debug dan membuat masalah terkait waktu muncul / hilang secara acak. Namun, jika dilakukan dengan benar, tak satu pun dari ini harus menjadi masalah.

Dari level yang lebih tinggi, saya akan mengatakan jaringan yang terpisah dari mesin game itu sendiri. Mesin game tidak peduli tentang soket atau penyangga, ia peduli tentang acara. Peristiwa adalah hal-hal seperti "Pemain X melepaskan tembakan" "Sebuah ledakan di game T terjadi". Ini dapat ditafsirkan oleh mesin game secara langsung. Di mana mereka dihasilkan (skrip, tindakan klien, pemain AI, dll) tidak masalah.

Jika Anda memperlakukan subsistem jaringan Anda sebagai sarana untuk mengirim / menerima acara, maka Anda mendapatkan sejumlah keuntungan dari sekadar memanggil recv () pada soket.

Anda dapat mengoptimalkan bandwidth, misalnya, dengan mengambil 50 pesan kecil (panjang 1-32 byte) dan membuat subsistem jaringan mengemasnya menjadi satu paket besar dan mengirimkannya. Mungkin itu bisa menekan mereka sebelum mengirim jika itu masalah besar. Di sisi lain, kode tersebut dapat mengompres / membongkar paket besar menjadi 50 peristiwa terpisah lagi untuk dibaca oleh mesin game. Ini semua bisa terjadi secara transparan.

Hal-hal keren lainnya termasuk mode permainan pemain tunggal yang menggunakan kembali kode jaringan Anda dengan memiliki klien murni + server murni yang berjalan di mesin yang sama berkomunikasi melalui pesan dalam ruang memori bersama. Kemudian, jika gim pemain tunggal Anda beroperasi dengan benar, klien jarak jauh (yaitu multiplayer sejati) juga akan beroperasi. Selain itu, itu memaksa Anda untuk mempertimbangkan terlebih dahulu data apa yang dibutuhkan oleh klien karena permainan pemain tunggal Anda akan terlihat benar atau benar-benar salah. Padu dan padu, jalankan server DAN menjadi klien dalam game multi pemain - semuanya bekerja dengan mudah.

PatrickB
sumber
Anda menyebutkan menggunakan std :: list sederhana atau semacamnya untuk menyampaikan pesan. Ini mungkin menjadi topik untuk StackOverflow, tetapi apakah benar bahwa semua utas berbagi ruang alamat yang sama, dan selama saya menjaga agar beberapa utas tidak bercinta dengan memori yang termasuk dalam antrian saya secara bersamaan, saya seharusnya baik-baik saja? Saya hanya bisa mengalokasikan data untuk antrian di tumpukan seperti biasa, dan hanya menggunakan beberapa mutex di dalamnya?
Steven Lu
Ya itu benar. Sebuah mutex yang melindungi semua panggilan ke std :: list.
PatrickB
Terima kasih telah membalas! Saya telah membuat banyak kemajuan dengan rutinitas threading saya sejauh ini. Perasaan yang luar biasa, memiliki mesin gim sendiri!
Steven Lu
4
Perasaan itu akan hilang. Namun, kuningan besar yang Anda dapatkan, tetap bersamamu.
ChrisE
@StevenLu Agak [sangat] terlambat, tapi saya ingin menunjukkan bahwa mencegah utas dari bercak dengan memori secara bersamaan bisa sangat sulit, tergantung pada bagaimana Anda mencoba melakukannya dan seberapa efisien yang Anda inginkan. Jika Anda melakukan ini hari ini, saya akan mengarahkan Anda ke salah satu dari banyak implementasi concurrent-queue open source yang sangat baik, sehingga Anda tidak perlu menemukan kembali roda yang rumit.
Dana Gugatan Monica
4

Saya perlu (paling tidak) memiliki utas terpisah untuk menangani soket jaringan

Tidak, kamu tidak.

Saya memang memiliki satu cara potensial untuk membuat game jaringan sepenuhnya single-threaded, yaitu memeriksanya setelah merender setiap frame, menggunakan soket non-blocking. Namun ini jelas tidak optimal karena waktu yang dibutuhkan untuk membuat bingkai ditambahkan ke jeda jaringan:

Tidak penting. Kapan logika Anda diperbarui? Ada gunanya mengambil data dari jaringan jika Anda belum bisa melakukan apa-apa dengannya. Demikian pula ada gunanya merespons jika Anda tidak memiliki sesuatu untuk dikatakan.

misalnya, ia dapat mengirim kembali paket ACK secara instan setelah menerima pembaruan status dari server.

Jika game Anda berjalan sangat cepat sehingga menunggu frame berikutnya yang akan diberikan adalah penundaan yang signifikan, maka itu akan mengirimkan cukup data sehingga Anda tidak perlu mengirim paket ACK terpisah - cukup sertakan nilai ACK di dalam data normal Anda payload, jika Anda membutuhkannya sama sekali.

Untuk sebagian besar game berjejaring, sangat mungkin untuk memiliki loop game seperti ini:

while 1:
    read_network_messages()
    read_local_input()
    update_world()
    send_network_updates()
    render_world()

Anda dapat memisahkan pembaruan dari rendering, yang sangat disarankan, tetapi segala sesuatu yang lain dapat tetap sederhana seperti itu kecuali Anda memiliki kebutuhan khusus. Apa jenis gim yang Anda buat?

Kylotan
sumber
7
Pembaruan decoupling dari rendering tidak hanya sangat dianjurkan, diperlukan juga jika Anda menginginkan mesin non-kotoran.
AttackingHobo
Kebanyakan orang tidak membuat mesin, dan mungkin sebagian besar game masih tidak memisahkan keduanya. Setuju untuk melewatkan nilai waktu yang telah berlalu ke fungsi pembaruan berfungsi dengan baik pada sebagian besar kasus.
Kylotan
2

Namun ini jelas tidak optimal karena waktu yang diperlukan untuk membuat bingkai ditambahkan ke jeda jaringan: Pesan yang tiba melalui jaringan harus menunggu sampai bingkai saat ini membuat (dan logika game) selesai.

Itu tidak benar sama sekali. Pesan melewati jaringan sementara penerima membuat bingkai saat ini. Kelambatan jaringan dijepit ke sejumlah bingkai di sisi klien; ya- tetapi jika klien memiliki FPS yang sangat sedikit sehingga ini adalah masalah besar, maka mereka memiliki masalah yang lebih besar.

DeadMG
sumber
0

Komunikasi jaringan harus dikelompokkan. Anda harus berjuang untuk satu paket yang dikirim setiap tick game (yang seringkali ketika frame diberikan tetapi benar-benar harus independen).

Entitas game Anda berbicara dengan subsistem jaringan (NSS). NSS mengumpulkan pesan, ACK, dll dan mengirimkan beberapa (semoga satu) paket UDP berukuran optimal (biasanya ~ 1500 byte). NSS mengemulasi paket, saluran, prioritas, mengirim ulang, dll, sementara hanya mengirim paket UDP tunggal.

Baca melalui gaffer pada tutorial gim atau gunakan ENet yang mengimplementasikan banyak ide Glenn Fiedler.

Atau Anda bisa menggunakan TCP jika gim Anda tidak membutuhkan reaksi kedutan. Lalu semua masalah batching, kirim ulang, dan ACK hilang. Anda masih menginginkan NSS untuk mengelola bandwidth dan saluran.

deft_code
sumber
0

Jangan sepenuhnya "mengabaikan respons". Ada sedikit keuntungan dari menambahkan latensi 40ms ke paket yang sudah tertunda. Jika Anda menambahkan beberapa bingkai (pada 60fps) maka Anda menunda pemrosesan pembaruan posisi beberapa bingkai. Lebih baik menerima paket dengan cepat dan memprosesnya dengan cepat sehingga Anda meningkatkan akurasi simulasi.

Saya sangat sukses mengoptimalkan bandwidth dengan memikirkan informasi keadaan minimum yang diperlukan untuk merepresentasikan apa yang terlihat di layar. Kemudian melihat setiap bit data dan memilih model untuk itu. Informasi posisi dapat dinyatakan sebagai nilai delta seiring waktu. Anda dapat menggunakan model statistik Anda sendiri untuk ini dan menghabiskan waktu men-debug mereka, atau Anda dapat menggunakan perpustakaan untuk membantu Anda. Saya lebih suka menggunakan model floating point perpustakaan ini DataBlock_Predict_Float Itu membuatnya sangat mudah untuk mengoptimalkan bandwidth yang digunakan untuk grafik adegan permainan.

Justin
sumber