Solusi arsitektur untuk gim seluler. Bagian 2: Perintah dan antriannya



Di bagian pertama artikel, kami memeriksa bagaimana model harus diatur sehingga mudah digunakan, tetapi debugging dan menautkan antarmuka itu sederhana. Pada bagian ini kita akan mempertimbangkan kembalinya perintah untuk perubahan dalam model, dalam semua keindahan dan keragamannya. Seperti sebelumnya, prioritas bagi kami adalah kenyamanan debugging, meminimalkan gerakan yang perlu dilakukan oleh seorang programmer untuk membuat fitur baru, serta keterbacaan kode untuk seseorang.

Solusi arsitektur untuk gim seluler. Bagian 1: Model
Solusi arsitektur untuk gim seluler. Bagian 3: Lihat di dorongan jet

Kenapa Memerintah


Pola Perintah terdengar keras, tetapi sebenarnya itu hanya objek di mana segala sesuatu yang diperlukan untuk operasi yang diminta ditambahkan dan disimpan di sana. Kami memilih pendekatan ini, setidaknya karena tim kami akan dikirim melalui jaringan, dan bahkan kami akan mendapatkan beberapa salinan dari kondisi permainan untuk penggunaan resmi. Jadi ketika pengguna mengklik tombol, sebuah instance dari kelas perintah dibuat dan dikirim ke penerima. Arti huruf C dalam singkatan MVC agak berbeda.

Prediksi hasil dan verifikasi perintah melalui jaringan


Dalam hal ini, kode spesifik kurang penting daripada idenya. Dan inilah idenya:

Gim yang menghargai diri sendiri tidak bisa menunggu respons dari server sebelum bereaksi terhadap tombol. Tentu saja, Internet semakin baik dan Anda dapat memiliki banyak server di seluruh dunia, dan saya tahu bahkan beberapa gim yang sukses menunggu tanggapan dari server, salah satunya adalah Summoning Wars, tetapi Anda tetap tidak perlu melakukan itu. Karena untuk kelambatan Internet seluler 5-15 detik lebih cenderung menjadi norma daripada pengecualian, di Moskow setidaknya permainan harus benar-benar hebat sehingga para pemain tidak memperhatikannya.

Oleh karena itu, kami memiliki status permainan yang mewakili semua informasi yang diperlukan untuk antarmuka, dan perintah diterapkan segera, dan hanya setelah itu mereka dikirim ke server. Biasanya, pemrogram java pekerja keras duduk di server menduplikasi semua fungsi baru satu ke satu dalam bahasa lain. Pada proyek "rusa" kami, jumlah mereka mencapai 3 orang, dan kesalahan yang dilakukan ketika porting adalah sumber kegembiraan yang sulit ditangkap. Sebaliknya, kita dapat melakukannya secara berbeda. Kami berjalan di .Net server dan berjalan di sisi server kode perintah yang sama seperti pada klien.

Model yang dijelaskan dalam artikel terakhir memberi kita peluang baru yang menarik untuk menguji diri. Setelah mengeksekusi perintah pada klien, kami akan menghitung hash dari perubahan yang terjadi di pohon GameState, dan menerapkannya pada tim. Jika server mengeksekusi kode perintah yang sama, dan hash dari perubahan tidak cocok, maka ada yang tidak beres.

Keuntungan pertama:

  • Solusi ini sangat mempercepat pengembangan dan meminimalkan jumlah programmer server.
  • Jika programmer membuat kesalahan yang mengarah pada perilaku non-deterministik, misalnya, ia mendapat nilai pertama dari Kamus, atau menggunakan DateTime. kita akan mengetahuinya.
  • Pengembangan klien untuk saat ini dapat dilakukan tanpa server sama sekali. Anda bahkan dapat masuk ke alpha friendly tanpa memiliki server. Ini bermanfaat tidak hanya bagi pengembang indie yang melewatkan permainan impian mereka di malam hari. Ketika saya berada di Piksonik ada kasus ketika programmer server kehilangan semua polimer, dan permainan kami dipaksa untuk menjalani moderasi, alih-alih server dummy yang secara bodoh membela seluruh keadaan game sesekali.

Kerugian yang karena alasan tertentu diremehkan secara sistematis:

  • Jika programmer klien melakukan sesuatu yang salah dan itu tidak terlihat selama pengujian, misalnya, kemungkinan barang di kotak misterius, maka tidak ada orang yang menulis hal yang sama untuk kedua kalinya dan menemukan kesalahan. Kode autoportable membutuhkan sikap yang jauh lebih bertanggung jawab terhadap pengujian.

Informasi debugging terperinci


Salah satu prioritas kami adalah kenyamanan debugging. Jika selama eksekusi tim kami menangkap eksekusi - semuanya jelas, kami memutar kembali kondisi permainan, mengirim status penuh ke log dan membuat serial perintah yang menjatuhkannya, semuanya nyaman dan indah. Situasinya lebih rumit jika kita memiliki desync dengan server. Karena klien telah menyelesaikan beberapa perintah lain sejak saat itu, dan ternyata tidak hanya untuk mengetahui keadaan model sebelum menjalankan perintah yang menyebabkan bencana, tetapi saya benar-benar menginginkannya. Mengkloning gamestate di depan setiap tim terlalu rumit dan mahal. Untuk mengatasi masalah tersebut, kami menyulitkan skema yang dijahit di bawah kap mesin.

Di klien kita tidak akan memiliki satu gamestate, tetapi dua. Yang pertama berfungsi sebagai antarmuka utama untuk rendering, perintah diterapkan segera. Setelah itu, perintah yang diterapkan diantrikan untuk mengirim ke server. Server melakukan tindakan yang sama di sisinya, dan mengkonfirmasi bahwa semuanya baik-baik saja dan benar. Setelah menerima konfirmasi, klien mengambil perintah yang sama dan menerapkannya pada gamestate kedua, membawanya ke status yang sudah dikonfirmasi oleh server sebagai benar. Pada saat yang sama, kami juga memiliki kesempatan untuk membandingkan hash dari perubahan yang dibuat agar aman, dan kami juga dapat membandingkan hash penuh dari seluruh pohon pada klien, yang dapat kami hitung setelah perintah dijalankan, beratnya sedikit dan dianggap cukup cepat. Jika server tidak mengatakan bahwa semuanya baik-baik saja, ia meminta klien untuk detail tentang apa yang terjadi, dan klien dapat mengirimnya gamestate kedua berseragam persis seperti yang terlihat sebelum perintah berhasil dieksekusi pada klien.
Solusinya terlihat sangat menarik, tetapi memunculkan dua masalah yang perlu dipecahkan pada level kode:

  • Di antara parameter perintah, tidak hanya ada tipe sederhana, tetapi juga tautan ke model. Di gamestate lain, di tempat yang sama persis adalah objek lain dari model. Kami memecahkan masalah ini dengan cara berikut: Sebelum perintah dijalankan pada klien, kami membuat serialisasi semua datanya. Di antara mereka mungkin ada tautan ke model, yang akan kami tulis dalam bentuk Path ke model dari akar status permainan. Kami melakukan ini di depan tim, karena setelah eksekusi, jalur mungkin berubah. Lalu kami mengirimkan jalur ini ke server, dan server gamestate akan bisa mendapatkan tautan ke modelnya di sepanjang jalan. Demikian pula, ketika sebuah tim diterapkan ke keadaan gim kedua, model dapat diperoleh dari keadaan gim kedua.
  • Selain tipe dan model dasar, tim mungkin memiliki tautan ke koleksi. Kamus <kunci, Model>, Kamus <Model, kunci>, Daftar <Model>, Daftar <Value>. Untuk mereka semua, mereka harus menulis serialis. Benar, Anda tidak bisa terburu-buru dalam hal ini, dalam proyek nyata bidang seperti itu jarang muncul.
  • Mengirim perintah ke server satu per satu bukanlah ide yang baik, karena pengguna dapat memproduksinya lebih cepat daripada Internet dapat menyeretnya bolak-balik, di Internet yang buruk kumpulan perintah yang tidak dikerjakan oleh server akan tumbuh. Alih-alih mengirim perintah satu per satu, kami akan mengirimkannya dalam beberapa bagian. Dalam hal ini, setelah menerima respons dari server bahwa ada sesuatu yang salah, Anda harus terlebih dahulu menerapkan ke status kedua semua perintah sebelumnya dari paket yang sama yang dikonfirmasi oleh server, dan hanya kemudian menghapus dan mengirim status kontrol ke server.

Kenyamanan dan kemudahan menulis perintah


Kode eksekusi perintah adalah kode terbesar kedua dan paling bertanggung jawab pertama dalam game. Semakin sederhana dan jelas akan, dan semakin sedikit programmer perlu melakukan ekstra dengan tangannya untuk menulisnya, semakin cepat kode akan ditulis, semakin sedikit kesalahan akan dibuat dan, sangat tak terduga, semakin senang programmer akan. Saya menempatkan kode eksekusi langsung di perintah itu sendiri, di samping potongan-potongan umum dan fungsi yang terletak di kelas aturan statis yang terpisah, paling sering dalam bentuk ekstensi ke kelas model dengan mana mereka bekerja. Saya akan menunjukkan kepada Anda beberapa contoh perintah dari proyek peliharaan saya, yang satu sangat sederhana dan yang lain sedikit lebih rumit:

namespace HexKingdoms { public class FCSetSideCostCommand : HexKingdomsCommand { //              protected override bool DetaliedLog { get { return true; } } public FCMatchModel match; public int newCost; protected override void HexApply(HexKingdomsRoot root) { match.sideCost = newCost; match.CalculateAssignments(); match.CalculateNextUnassignedPlayer(); } } } 

Dan di sini adalah log yang ditinggalkan oleh perintah ini, jika log ini tidak dinonaktifkan untuk itu.

 [FCSetSideCostCommand id=1 match=FCMatchModel[0] newCost=260] Execute:00:00:00.0027546 Apply:00:00:00.0008689 { "LOCAL_PERSISTENTS":{ "@changed":{ "0":{"SIDE_COST":260}, "1":{"POSSIBLE_COST":260}, "2":{"POSSIBLE_COST":260}}}} 

Pertama kali ditunjukkan dalam log adalah waktu di mana semua perubahan yang diperlukan dalam model dibuat, dan yang kedua adalah waktu di mana semua perubahan dilakukan oleh pengontrol antarmuka. Ini harus ditunjukkan dalam log agar tidak sengaja melakukan sesuatu yang sangat lambat, atau untuk memperhatikan waktu jika operasi mulai memakan terlalu banyak waktu hanya karena ukuran model itu sendiri.

Terlepas dari panggilan ke objek Persistent pada Id-shniks, yang sangat mengurangi keterbacaan log, yang, dengan cara, bisa dihindari di sini, kode perintah itu sendiri dan log yang dia lakukan dengan keadaan gim sangat jelas. Harap dicatat bahwa dalam teks perintah, programmer tidak membuat satu gerakan tambahan. Semua yang Anda butuhkan dikerjakan oleh mesin di bawah kap.

Sekarang mari kita lihat contoh tim yang lebih besar

 namespace HexKingdoms { public class FCSetUnitForPlayerCommand : HexKingdomsCommand { //            protected override bool DetaliedLog { get { return true; } } public FCSelectArmyScreenModel screen; public string unit; public int count; protected override void HexApply(HexKingdomsRoot root) { if (count == 0 && screen.player.units.ContainsKey(unit)) { screen.player.units.Remove(unit); screen.selectedUnits.Remove(unit); } else if (count != 0) { if (screen.player.units.ContainsKey(unit)) { screen.player.units[unit] = count; screen.selectedUnits[unit].count = count; } else { screen.player.units.Add(unit, count); screen.selectedUnits[unit] = new ReferenceUnitModel() { type = unit, count = count }; } } screen.SetSelectedReferenceUnits(); screen.player.CalculateUnitsCost(); var side = screen.match.sides[screen.side]; screen.match.CalculatePlayerAssignmentsAcceptablity(side); screen.match.CalculateNextUnassignedPlayer(screen.player); } } } 

Dan inilah log yang ditinggalkan oleh tim:

 [FCSetUnitForPlayerCommand id=3 screen=/UI_SCREENS[main] unit=militia count=1] Execute:00:00:00.0065625 Apply:00:00:00.0004573 { "LOCAL_PERSISTENTS":{ "@changed":{ "2":{ "UNITS":{ "@set":{"militia":1}}, "ASSIGNED":7}}}, "UI_SCREENS":{ "@changed":{ "main":{ "SELECTED_UNITS":{ "@set":{ "militia":{"@new":null, "TYPE":"militia", "REMARK":null, "COUNT":1, "SELECTED":false, "DISABLED":false, "HIGHLIGHT_GREEN":false, "HIGHLIGHT_RED":false, "BUTTON_ENABLED":false}}}}}}} 

Seperti kata pepatah, jauh lebih dimengerti. Luangkan waktu untuk melengkapi tim dengan log yang nyaman, ringkas dan informatif. Inilah kunci menuju kebahagiaan Anda. Model harus bekerja dengan sangat cepat, jadi di sana kami menggunakan berbagai trik dengan metode penyimpanan dan akses ke ladang. Perintah dieksekusi dalam kasus terburuk satu kali per frame, pada kenyataannya, beberapa kali lebih jarang, jadi kami akan melakukan serialisasi dan deserialisasi bidang perintah tanpa fantasi, hanya melalui refleksi. Kami hanya mengurutkan bidang berdasarkan nama sehingga pesanan tetap, baik, kami akan mengkompilasi daftar bidang sekali selama masa hidup perintah, dan membaca-menulis menggunakan metode asli C #.

Model informasi untuk antarmuka.


Mari kita mengambil langkah selanjutnya dalam mempersulit mesin kita, langkah yang terlihat menakutkan, tetapi sangat menyederhanakan penulisan dan debugging antarmuka. Sangat sering, terutama dalam pola MVP terkait, model hanya berisi logika bisnis yang dikendalikan server, dan informasi tentang keadaan antarmuka disimpan di dalam presenter. Misalnya, Anda ingin memesan lima tiket. Anda telah memilih nomor mereka, tetapi belum mengklik tombol "pesanan". Informasi tentang berapa banyak tiket yang telah Anda pilih dalam formulir dapat disimpan di suatu tempat di sudut-sudut rahasia kelas, yang berfungsi sebagai gasket antara model dan layarnya. Atau, misalnya, pemain beralih dari satu layar ke yang lain, tetapi tidak ada yang berubah dalam model, dan di mana ia berada ketika tragedi itu terjadi, programmer debugging hanya tahu dari kata-kata penguji yang sangat disiplin. Pendekatannya sederhana, bisa dimengerti, hampir selalu digunakan dan sedikit berbahaya, menurut saya. Karena jika terjadi kesalahan, keadaan Presenter ini, yang menyebabkan kesalahan, sama sekali tidak mungkin untuk mengetahuinya. Terutama jika kesalahan terjadi pada server pertempuran selama operasi sebesar $ 1000, dan tidak di tester dalam lingkungan yang terkontrol dan dapat direproduksi.

Alih-alih pendekatan yang biasa ini, kami melarang siapa pun kecuali model untuk memuat informasi tentang keadaan antarmuka. Ini memiliki, seperti biasa, kelebihan dan kekurangan yang harus diperangi.

  • (+1) Keuntungan paling penting, menghemat berbulan-bulan pekerjaan pemrograman - jika terjadi kesalahan, programmer hanya memuat keadaan permainan sebelum kecelakaan dan menerima kondisi yang persis sama tidak hanya dari model bisnis, tetapi dari seluruh antarmuka hingga tombol terakhir di layar.
  • (+2) Jika beberapa tim mengubah sesuatu di antarmuka, programmer dapat dengan mudah pergi ke log dan melihat apa yang sebenarnya telah berubah dalam bentuk json yang mudah digunakan, seperti pada bagian sebelumnya.
  • (-1) Banyak informasi yang berlebihan muncul dalam model yang tidak diperlukan untuk memahami logika bisnis permainan dan tidak diperlukan dua kali oleh server.

Untuk mengatasi masalah ini, kami akan menandai beberapa bidang sebagai notServerVerified, sepertinya ini, misalnya, seperti ini:

 public EDictionary<string, UIStateModel> uiScreens { get { return UI_SCREENS.Get(this); } } public static PDictionaryModel<string, UIStateModel> UI_SCREENS = new PDictionaryModel<string, UIStateModel>() { notServerVerified = true }; 

Bagian dari model ini dan semua yang ada di bawahnya akan berhubungan secara eksklusif dengan klien.

Jika Anda masih ingat, bendera apa yang perlu Anda ekspor dan apa yang tidak terlihat seperti ini:

 [Flags] public enum ExportMode { all = 0x0, changes = 0x1, serverVerified = 0x2 } 

Dengan demikian, saat mengekspor atau menghitung hash, Anda dapat menentukan apakah akan mengekspor seluruh pohon atau hanya bagian dari itu yang diperiksa oleh server.

Komplikasi jelas pertama yang muncul dari sini adalah kebutuhan untuk membuat perintah terpisah yang perlu diperiksa oleh server dan yang tidak diperlukan, tetapi ada juga yang perlu diperiksa tidak sepenuhnya. Agar tidak memuat pemrogram dengan operasi yang tidak perlu untuk mengatur perintah, kami akan kembali mencoba melakukan semua yang diperlukan dengan kap mesin.

 public partial class Command { /** <summary>    ,      </summary> */ public virtual void Apply(ModelRoot root) {} /** <summary>         </summary> */ public virtual void ApplyClientSide(ModelRoot root) {} } 

Programmer yang membuat perintah dapat mengganti satu atau kedua fungsi ini. Semua ini, tentu saja, luar biasa, tetapi bagaimana saya bisa memastikan bahwa programmer tidak mengacaukan sesuatu, dan jika dia mengacaukan sesuatu - bagaimana dia bisa membantunya dengan cepat dan mudah memperbaikinya? Ada dua cara. Saya menerapkan yang pertama, tetapi Anda mungkin lebih suka yang kedua.

Cara pertama


Kami menggunakan fitur keren dari model kami:

  1. Mesin memanggil fungsi pertama, setelah itu menerima hash perubahan di bagian server-check dari kondisi permainan. Jika tidak ada perubahan, maka kami berurusan secara eksklusif dengan tim klien.
  2. Kami mendapatkan hash model perubahan di seluruh model, tidak hanya yang diverifikasi server. Jika berbeda dari hash sebelumnya, maka programmer mengacaukan dan mengubah sesuatu di bagian model yang tidak diperiksa oleh server. Kami berkeliling pohon negara dan membuang programmer sebagai eksekusi daftar lengkap bidang notServerVerified = true dan yang terletak di bawah pohon yang ia ubah.
  3. Kami memanggil fungsi kedua. Kami mendapatkan dari hash model perubahan yang terjadi di bagian yang diperiksa. Jika tidak bertepatan dengan hash setelah panggilan pertama, maka pada fungsi kedua programmer telah melakukan apa saja. Jika kita ingin mendapatkan log yang sangat informatif dalam kasus ini, kita memutar kembali seluruh model ke keadaan semula, membuat serial menjadi file, maka programmer akan berguna untuk debugging, maka kita mengkloningnya secara keseluruhan (dua baris - serialisasi-deserialisasi), dan sekarang kita pertama-tama menerapkan yang pertama fungsi, lalu kita komit perubahan sehingga model terlihat tidak berubah, setelah itu kita menerapkan fungsi kedua. Dan kemudian kami mengekspor semua perubahan di bagian yang diperiksa server dalam bentuk JSON dan memasukkannya ke dalam eksekusi kasar, sehingga programmer yang malu dapat segera melihat apa dan di mana ia berubah, apa yang tidak boleh diubah.

Tampaknya, tentu saja, menakutkan, tetapi sebenarnya itu adalah 7 baris, karena fungsi yang melakukan ini semua (kecuali melintasi pohon dari paragraf kedua) kami siap. Dan karena ini adalah penerimaan, kita dapat membiarkan diri kita bertindak tidak secara optimal.

Cara kedua


Sedikit lebih brutal, sekarang di ModelRoot kita memiliki satu bidang kunci, tetapi kita dapat membaginya menjadi dua, satu hanya akan mengunci bidang yang diperiksa ke server, yang lain hanya bidang yang diperiksa. Dalam hal ini, programmer yang melakukan kesalahan akan mendapatkan penjelasan tentang hal itu segera dengan dasi ke tempat di mana dia melakukannya. Satu-satunya kelemahan dari pendekatan ini adalah bahwa jika di pohon kami satu model-properti ditandai sebagai tidak dicentang, maka segala sesuatu yang terletak di pohon di bawahnya mengenai perhitungan hash dan kontrol perubahan tidak akan diperiksa, bahkan jika setiap bidang tidak ditandai. Kunci, tentu saja, tidak akan melihat ke hierarki, yang berarti bahwa semua bidang bagian pohon yang tidak dicentang harus ditandai, dan itu tidak akan berfungsi di beberapa tempat untuk menggunakan kelas yang sama di UI dan bagian pohon yang biasa. Sebagai pilihan, konstruksi seperti itu dimungkinkan (saya akan menulisnya disederhanakan):

 public class GameState : Model { public RootModelData data; public RootModelLocal local; } public class RootModel { public bool locked { get; } } 

Kemudian ternyata setiap subtree memiliki kunci sendiri. GameState mewarisi model, karena lebih mudah daripada membuat implementasi terpisah dari semua fungsi yang sama untuk itu.

Perbaikan yang diperlukan


Tentu saja, manajer yang bertanggung jawab untuk memproses tim harus menambahkan fungsionalitas baru. Inti dari perubahan adalah bahwa tidak semua perintah akan dikirim ke server, tetapi hanya mereka yang membuat perubahan yang dicentang. Server di sisinya tidak akan menaikkan seluruh pohon status permainan, tetapi hanya bagian yang diperiksa, dan sesuai hash akan bertepatan hanya untuk bagian yang diperiksa. Ketika perintah dieksekusi, hanya yang pertama dari dua fungsi perintah yang akan diluncurkan di server, dan ketika menyelesaikan referensi ke model di gamestate, jika jalur mengarah ke bagian pohon yang tidak dapat diverifikasi, nol akan ditempatkan di variabel perintah alih-alih model. Semua tim yang tidak mengirim akan secara jujur โ€‹โ€‹sejalan dengan yang biasa, tetapi dianggap sudah dikonfirmasi. Begitu mereka mencapai garis dan tidak ada yang belum dikonfirmasi sebelum mereka, mereka akan segera diterapkan ke negara bagian kedua.

Tidak ada yang rumit secara mendasar dalam implementasi. Hanya saja properti dari masing-masing bidang model memiliki satu kondisi lagi, traversal pohon.

Penyempurnaan lain yang diperlukan - Anda akan membutuhkan Pabrik terpisah untuk ParsistentModel di bagian pohon yang diperiksa dan tidak diperiksa dan NextFreeId untuknya akan berbeda.

Perintah diprakarsai oleh server


Ada beberapa masalah jika server ingin mendorong perintahnya ke klien, karena status klien relatif terhadap satu server sudah bisa melompat beberapa langkah ke depan. Gagasan utama adalah bahwa jika server perlu mengirim perintahnya, ia mengirimkan pemberitahuan server ke klien dengan respons berikutnya, dan menulisnya sendiri di bidang untuk pemberitahuan yang dikirim ke klien ini. Klien menerima pemberitahuan, membentuk perintah atas dasar dan meletakkannya di akhir antriannya, setelah yang selesai pada klien tetapi belum mencapai server. Setelah beberapa waktu, perintah dikirim ke server sebagai bagian dari proses normal bekerja dengan model. Setelah menerima perintah ini untuk diproses, server melempar notifikasi keluar dari antrian keluar. Jika klien tidak menanggapi pemberitahuan dalam waktu yang ditentukan dengan paket berikutnya, perintah reboot dikirim ke sana. Jika klien yang menerima notifikasi telah jatuh, terhubung kemudian, atau karena suatu alasan memuat permainan, maka server akan mengubah semua notifikasi menjadi perintah sebelum mengeksekusi negara, mengeksekusi mereka di sisinya, dan hanya setelah itu akan memberikan klien yang baru bergabung status baru. Harap dicatat bahwa pemain mungkin memiliki keadaan yang bertentangan dengan sumber daya negatif ketika pemain berhasil menghabiskan uang tepat pada saat server mengambilnya darinya. Kebetulan tidak mungkin, tetapi dengan DAU besar, hampir tidak bisa dihindari. Oleh karena itu, aturan antarmuka dan permainan tidak boleh mati dalam situasi seperti itu.

Perintah untuk mengeksekusi yang perlu Anda ketahui respons server


Kesalahan umum adalah berpikir bahwa nomor acak hanya dapat diperoleh dari server. Tidak ada yang mencegah Anda membuat generator nomor pseudo-acak yang sama berjalan secara simultan dari klien dan server, dimulai dari sisi umum. Selain itu, benih saat ini dapat disimpan langsung di gamestate. Beberapa mungkin merasa sulit untuk menyinkronkan respons generator ini. Bahkan, untuk ini cukup memiliki satu nomor lagi di artikel yang sama - persis berapa banyak angka yang diterima dari generator hingga saat ini. Jika generator Anda karena suatu alasan tidak konvergen, maka Anda memiliki kesalahan di suatu tempat dan kode tidak bekerja secara deterministik. Dan fakta ini seharusnya tidak disembunyikan di bawah karpet, tetapi disortir dan mencari kesalahan. Untuk sebagian besar kasus, termasuk bahkan kotak-kotak misterius, pendekatan ini sudah cukup.

Namun, ada kalanya opsi ini tidak cocok. Misalnya, Anda memainkan hadiah yang sangat mahal dan tidak ingin kawan yang cerdik itu menguraikan permainan, dan menulis bot yang memberi tahu Anda terlebih dahulu apa yang akan jatuh dari kotak berlian jika Anda membukanya sekarang, dan bagaimana jika Anda memutar drum di tempat lain sebelumnya. Anda dapat menyimpan benih untuk setiap variabel acak secara terpisah, ini akan melindungi terhadap peretasan frontal, tetapi itu tidak akan membantu dengan cara apa pun dari bot yang memberi tahu Anda berapa banyak kotak produk yang Anda butuhkan saat ini berbohong. Nah, kasus yang paling jelas adalah bahwa Anda mungkin tidak ingin bersinar dalam konfigurasi klien dengan informasi tentang kemungkinan beberapa peristiwa langka. Singkatnya, terkadang perlu menunggu respons server.
Situasi seperti itu harus diselesaikan bukan melalui kemampuan mesin tambahan, tetapi dengan memecah tim menjadi dua - yang pertama mempersiapkan situasi dan menempatkan antarmuka dalam keadaan menunggu untuk pemberitahuan, yang kedua sebenarnya pemberitahuan, dengan jawaban yang Anda butuhkan. Bahkan jika Anda benar-benar memblokir antarmuka di antara mereka pada klien, perintah lain mungkin lolos - misalnya, unit energi akan dikembalikan tepat waktu.

Penting untuk dipahami bahwa situasi seperti itu bukanlah aturan, tetapi pengecualian. Faktanya, sebagian besar game hanya membutuhkan satu tim menunggu jawaban - GetInitialGameState. Paket lain dari perintah tersebut adalah interaksi antar pemain dalam permainan-meta, GetLeaderboard, misalnya. Semua dua ratus potongan lainnya bersifat deterministik.

Penyimpanan data server dan topik berlumpur optimasi server


Saya langsung mengakui bahwa saya adalah klien, dan kadang-kadang saya mendengar ide dan algoritme seperti itu dari pelayan server yang akrab sehingga mereka bahkan tidak akan merangkak ke dalam kepala saya. Dari berkomunikasi dengan kolega saya, saya entah bagaimana mengembangkan gambaran tentang bagaimana arsitektur saya harus bekerja di sisi server dalam kasus yang ideal. Namun: Ada kontraindikasi, perlu berkonsultasi dengan server spesialis.

Pertama tentang penyimpanan data. Sisi server Anda yang mungkin memiliki batasan tambahan. Misalnya, Anda mungkin dilarang menggunakan bidang statis. Lebih lanjut, kode perintah dan model autoportable, tetapi kode Properti pada klien dan di server tidak harus bertepatan sama sekali. Apa pun dapat disembunyikan di sana, hingga inisialisasi malas nilai bidang dari memcache, misalnya. Bidang properti juga dapat menerima parameter tambahan yang digunakan oleh server, tetapi tidak memengaruhi pekerjaan klien.

Perbedaan kardinal pertama dari server: di mana field-field diserialisasi dan diserialisasi. Solusi yang masuk akal adalah bahwa sebagian besar pohon negara diserialisasi menjadi satu bidang biner atau json besar. Pada saat yang sama, beberapa bidang diambil dari tabel. Ini diperlukan karena nilai-nilai dari beberapa bidang akan selalu diperlukan untuk layanan interaksi antara pemain untuk bekerja. Sebagai contoh, ikon dan level terus bergerak-gerak oleh berbagai orang. Mereka paling baik disimpan dalam database reguler. Kondisi seseorang yang lengkap atau parsial tetapi terperinci akan sangat dibutuhkan oleh orang lain selain dirinya, ketika seseorang memutuskan untuk memeriksa wilayahnya.

Lebih jauh, menarik bidang dari basis satu per satu tidak nyaman, dan mungkin akan menyeret semuanya untuk waktu yang lama. Solusi yang sangat non-standar, hanya tersedia untuk arsitektur kami, dapat terdiri dari fakta bahwa klien, ketika menjalankan perintah, mengumpulkan informasi tentang semua bidang yang disimpan secara terpisah dalam tabel yang getternya berhasil disentuh, dan menambahkan informasi ini ke perintah sehingga server dapat meningkatkan kelompok bidang ini satu permintaan ke database. Tentu saja, dengan batasan yang masuk akal, agar tidak mengemis untuk DDOS yang disebabkan oleh programer tangan melengkung yang tanpa sadar menyentuh semuanya.

Dengan penyimpanan terpisah seperti itu, seseorang harus mempertimbangkan mekanisme transaksionalitas ketika satu pemain merangkak ke dalam data pemain lain, misalnya, mencuri uang darinya. Namun dalam kasus umum, kami melakukan ini dengan pemberitahuan. Artinya, pencuri segera menerima uangnya, dan orang yang dirampok menerima pemberitahuan dengan instruksi untuk menghapus uang ketika tiba saatnya.

Bagaimana tim dibagi antara server


Sekarang momen penting kedua bagi server. Ada dua pendekatan. Pada awalnya, untuk memproses permintaan apa pun (atau paket permintaan), seluruh status dinaikkan dari database atau cache ke memori, diproses, dan kemudian dikembalikan ke database. Operasi dilakukan secara atom pada sekelompok server pelaksana yang berbeda, dan mereka hanya memiliki basis yang sama, dan itupun tidak selalu. Sebagai klien, menaikkan status keseluruhan ke setiap tim mengejutkan, tetapi saya melihat cara kerjanya, dan bekerja dengan sangat andal dan terukur. Opsi kedua adalah bahwa keadaan pernah naik dalam memori dan tinggal di sana sampai klien jatuh hanya sesekali menambahkan keadaannya saat ini ke database.Saya tidak kompeten untuk memberi tahu Anda kelebihan dan kekurangan metode ini atau itu. Akan bagus jika seseorang dalam komentar menjelaskan kepada saya mengapa yang pertama memiliki hak untuk hidup secara umum. Opsi kedua menimbulkan pertanyaan tentang bagaimana berinteraksi antara pemain yang kebetulan muncul di server yang berbeda. Ini bisa menjadi kritis, misalnya, jika beberapa anggota klan bersiap untuk meluncurkan serangan gabungan. Anda tidak dapat menunjukkan kepada orang lain keadaan anggota partainya dengan penundaan 10 penyelamatan. Sayangnya, saya tidak akan membukanya di sini, berinteraksi melalui notifikasi yang dijelaskan di atas, perintah dari satu server ke server lain - saat ini tidak dapat dilakukan untuk menyelamatkan keadaan pemain saat ini yang dibesarkan di sana. Jika server memiliki tingkat ketersediaan yang sama dari tempat yang berbeda,dan Anda dapat mengelola penyeimbang, Anda dapat mencoba dengan diam-diam mentransfer pemain dari satu server ke yang lain. Jika Anda tahu solusinya lebih baik - pastikan untuk menjelaskan dalam komentar.

Menari dengan waktu


Mari kita mulai dengan pertanyaan, yang sangat saya sukai untuk menjatuhkan orang-orang saat wawancara: Di sini Anda memiliki klien dan server, masing-masing memiliki jamnya sendiri yang cukup akurat. Cara mengetahui seberapa besar perbedaannya. Upaya untuk memecahkan masalah ini di sebuah kedai kopi di atas serbet menunjukkan kualitas terbaik dan terburuk dari seorang programmer. Faktanya adalah bahwa masalah tidak memiliki solusi formal yang benar secara matematis. Tetapi orang yang diwawancarai menyadari hal ini, sebagai aturan, luangkan waktu satu menit pada yang kelima dan hanya setelah pertanyaan terkemuka. Dan cara dia memenuhi wawasan ini dan apa yang dia lakukan selanjutnya - mengatakan banyak tentang hal terpenting dalam karakternya - apa yang akan dilakukan orang ini ketika masalah nyata dimulai dalam proyek Anda.

Solusi terbaik yang saya tahu memungkinkan saya untuk menemukan bukan perbedaan yang tepat, tetapi untuk memperjelas kisaran yang masuk melalui banyak permintaan-jawaban hingga saat paket terbaik yang berjalan dari klien ke server, ditambah waktu paket terbaik yang berjalan dari server ke klien. Secara total, ini akan memberi Anda beberapa puluh milidetik akurasi. Ini berkali-kali lebih baik daripada yang diperlukan untuk meta-game dari game mobile, di sini kami tidak memiliki VR multiplayer atau CS, tetapi masih baik bagi programmer untuk mewakili skala dan sifat kesulitan dengan sinkronisasi jam. Kemungkinan besar, itu akan cukup bagi Anda untuk mengetahui kelambatan rata-rata diambil sebagai ping di setengah, untuk waktu yang lama dengan cut-off penyimpangan lebih dari 30%.

Situasi keren kedua yang mungkin Anda temui adalah menggelar permainan slip, dan mentransfer jam di ponsel Anda. Dalam kedua kasus, waktu dalam aplikasi akan berubah secara dramatis dan tiba-tiba, dan ini harus dikerjakan dengan benar. Setidaknya buat game dimulai ulang, Tapi itu lebih baik, tentu saja, untuk tidak memulai kembali setelah setiap slip, sehingga Anda tidak dapat menggunakan waktu yang telah berlalu dalam aplikasi sejak diluncurkan.

Ketiga, situasi, untuk beberapa alasan, adalah masalah bagi beberapa programmer untuk memahami, meskipun ada solusi yang tepat untuk itu: Operasi sama sekali tidak dapat dilakukan pada waktu server. Misalnya, mulai produksi barang ketika permintaan untuk produksi tiba di server. Kalau tidak, cium selamat tinggal determinisme Anda, dan tangkap 35 ribu desync per hari yang disebabkan oleh berbagai pendapat klien dan server tentang apakah sudah mungkin untuk mengklik pada penghargaan. Keputusan yang tepat adalah bahwa tim mencatat informasi tentang waktu ketika dieksekusi. Server, pada gilirannya, memeriksa apakah perbedaan waktu antara waktu server saat ini dan waktu dalam perintah berada dalam interval yang diizinkan, dan jika itu terjadi, ia mengeksekusi perintah pada bagiannya menggunakan waktu yang dinyatakan oleh klien.
Tugas lain untuk wawancara: Batas waktu setelah itu klien akan mencoba untuk reboot - 30 detik. Apa batas perbedaan waktu yang dapat diterima untuk server? Kiat # 1: Intervalnya tidak simetris. Kiat # 2: Baca kembali paragraf pertama bagian ini lagi, tentukan bagaimana memperpanjang interval agar tidak menangkap 3000 kesalahan per hari pada efek tepi.

Agar ini berfungsi dengan indah dan benar, lebih baik menambahkan parameter tambahan ke parameter panggilan perintah - waktu panggilan. Sesuatu seperti ini:

 public interface Command { void Apply(ModelRoot root, long time); } 

Dan saran saya kepada Anda, omong-omong, jangan gunakan jenis Unity asli untuk waktu dalam model - Anda akan tersedak. Lebih baik menyimpan UnixTime di waktu server, kapan pun Anda perlu memiliki metode konversi yang praktis, dan menyimpannya dalam model di bidang PTime khusus yang berbeda dari PValue <long> hanya ketika mengekspor ke JSON, itu menambahkan informasi yang berlebihan dalam tanda kurung yang tidak dapat dibaca saat Impor: Waktu dalam format yang dapat dibaca manusia. Anda tidak bisa mematuhiku. Aku sudah memperingatkanmu.

Keempat situasi: Dalam keadaan permainan, ada situasi di mana tim harus dimulai tanpa partisipasi pemain, dalam waktu, misalnya, pemulihan energi. Situasi yang sangat umum. Saya ingin memiliki lapangan, ia mudah berlatih. Misalnya PTimeOut, di mana dimungkinkan untuk merekam titik waktu setelah mana perintah harus dibuat dan dieksekusi. Dalam kode, ini mungkin terlihat seperti ini:

 public class MyModel : Model { public static PTimeOut RESTORE_ENERGY = new PTimeOut() {command = (model, property) => new RestoreEnergyCommand() { model = model}} public long restoreEnergy { get { return RESTORE_ENERGY.Get(this); } set { RESTORE_ENERGY.Set(this, value); }} } 

Tentu saja, selama pemuatan awal pemain, server harus terlebih dahulu memprovokasi pembuatan dan pelaksanaan semua perintah ini, dan hanya kemudian memberikan status kepada pemain. Jebakan di sini adalah bahwa semua ini terkenal mengganggu notifikasi yang bisa didapatkan pemain selama waktu ini. Dengan demikian, akan perlu untuk terlebih dahulu melepaskan waktu sebelum waktu pemberitahuan pertama, jika Anda perlu menarik banyak perintah pada saat yang sama, kemudian membuat perintah dari pemberitahuan itu sendiri, kemudian membuka waktu sampai pemberitahuan berikutnya, kemudian kerjakan dan seterusnya. Jika seluruh masa liburan ini tidak sesuai dengan batas waktu server, dan ini dimungkinkan jika pemain sering dikecam dengan pemberitahuan, kami menulis status saat ini dari memori ke database dan sebagai gantinya merespons dengan perintah untuk menyambung kembali ke klien.

Semua perintah ini entah bagaimana harus belajar tentang apa yang mereka butuhkan untuk membuat dan menjalankan eksekusi. Solusi saya yang agak kasar, tetapi nyaman adalah bahwa model memiliki satu panggilan lagi, yang bergulir di seluruh hierarki model, yang berkedut setelah setiap perintah dieksekusi, dan juga pada timer. Tentu saja, ini adalah overhead tambahan untuk berjalan di sekitar pohon hampir dalam pembaruan, alih-alih Anda dapat berlangganan atau berhenti berlangganan dari event CurrentTime mencuat dari status permainan dengan setiap perubahan di bidang ini:

 public partial class Model { public void SetCurrentTime(long time); } vs public partial class RootModel { public event Action<long> setCurrentTime; } 

Ini bagus, tetapi masalahnya adalah bahwa model yang dihapus dari pohon model selamanya dan mengandung bidang seperti itu akan tetap berlangganan acara ini, dan harus menyelesaikannya dengan benar. Sebelum mengirim perintah, periksa apakah mereka masih di pohon dan memiliki tautan lemah ke acara ini atau inversi kontrol, sehingga karena ini mereka tidak tetap tidak dapat diakses oleh GC.

Lampiran 1, kasus khas yang diambil dari kehidupan nyata


Saya mengambil dari komentar ke bagian pertama. Dalam mainan, sangat sering beberapa tindakan tidak terjadi segera setelah perintah ke model, tetapi pada akhir beberapa jenis animasi. Dalam latihan kami, ada kasus seperti itu ketika kotak misteri terbuka, dan tentu saja uang akan berubah hanya ketika animasi diputar hingga akhir. Salah satu pengembang kami memutuskan untuk menyederhanakan hidupnya, dan tidak mengubah nilai pada perintah, tetapi memberi tahu server bahwa ia berubah, dan pada akhir animasi jalankan callback, yang akan mengoreksi nilai dalam model ke yang diinginkan. Singkatnya, dilakukan dengan baik. Dia membuat kotak-kotak misterius ini selama dua minggu, dan kemudian tiga kesalahan yang sangat sulit ditangkap yang muncul sebagai akibat dari aktivitasnya muncul dan kami harus menghabiskan tiga minggu lagi untuk menangkapnya, terlepas dari kenyataan bahwa waktu untuk "menulis ulang seperti biasa" tentu saja, tidak ada yang bisa menyorot. Dari mana ia mengikuti dengan jelasSaya pikir kesimpulannya adalah bahwa lebih baik melakukan semuanya dari awal dengan reaktivitas normal.

Jadi, keputusan saya adalah sesuatu seperti ini. Tentu saja, uang tidak terletak di bidang yang terpisah, tetapi merupakan salah satu objek dalam kamus inventaris, tetapi ini tidak begitu penting saat ini. Model memiliki satu bagian yang diperiksa oleh server, dan atas dasar mana logika bisnis bekerja, dan yang lain hanya ada pada klien. Uang dalam model utama diperoleh segera setelah keputusan dibuat, dan pada bagian kedua dari daftar "ditangguhkan", sebuah elemen dibuat dengan jumlah yang sama yang memulai animasi berdasarkan fakta kemunculannya, dan ketika animasi berakhir, sebuah perintah diluncurkan yang menghilangkan elemen ini. Catatan klien yang murni "belum menunjukkan jumlah ini." Dan dalam bidang nyata, bukan hanya nilai bidang ditampilkan, tetapi nilai bidang minus semua penundaan klien. Pembagian menjadi dua tim seperti itu dilakukan karenabagaimana jika klien reboot setelah tim pertama, tetapi sebelum yang kedua semua uang yang diterima oleh pemain akan berada di akunnya tanpa tanda dan pengecualian. Dalam kode tersebut, akan ada sesuatu seperti ini:

 public class OpenMisterBox : Command { public BoxItemModel item; public int slot; //        ,  . public override void Apply(GameState state) { state.inventory[item.revardKey] += item.revardCount; } //       . public override void Apply(GameState state) { var cause = state.NewPersistent<WaitForCommand>(); cause.key = item.key; cause.value = item.value; state.ui.delayedInventoryVisualization.Add(cause); state.ui.mysteryBoxScreen.animations.Add(new Animation() {cause = item, slot = slot})); } } public class MysteryBoxView : View { /* ... */ public override void ConnectModel(MysteryBoxScreenModel model, List<Control> c) { model.Get(c, MysteryBoxScreenModel.ANIMATIONS) .Control(c, onAdd = item => animationFactory(item, OnComleteOrAbort => { AsincQueue(new RemoveAnimation() {cause = item.cause, animation = item}) }), onRemove = item => {} ) } } public class InventoryView : View<InventoryItem> { public Text text; public override void ConnectModel(InventoryItem model, List<Control> c) { model.GameState.ui.Get(c, UIModel.DELAYED_INVENTORY_VISUALIZATION). .Where(c, item => item.key == model.key) .Expression(c, onChange = (IList<InventoryItem> list) => { int sum = 0; for (int i = 0; i < list.Count; i++) sum += list[i].value; return sum; }, onAdd = null, onRemove = null ) //      .Join (c, model.GameState.Get(GameState.INVENTORY).ItemByKey(model.key)) .Expression(c, (delay, count) => count - delay) .SetText(c, text); //     ,      ,   ,  ,   ,       ,     : model.inventory.CreateVisibleInventoryItemCount(c, model.key).SetText(c, text); } } public class RemoveDelayedInventoryVisualization : Command { public DelayCauseModel cause; public override void Apply(GameState state) { state.ui.delayedInventoryVisualization.Remove(cause); } } public class RemoveAnimation : RemoveDelayedInventoryVisualization { public Animation animation public override void Apply(GameState state) { base.Apply(state); state.ui.mysteryBoxScreen.animations.Remove(animation); } } 

Apa yang kita miliki pada akhirnya?Ada dua View, di salah satu dari mereka beberapa animasi dimainkan, yang akhirnya menunggu uang ditampilkan dalam tampilan yang sama sekali berbeda, yang tidak tahu siapa dan mengapa ingin menunjukkan makna yang berbeda. Semuanya reaktif. Kapan saja, Anda dapat memuat status penuh GameState ke dalam game dan itu akan mulai diputar tepat dari tempat kami tinggalkan, termasuk animasi yang dimulai. Kebenaran akan dimulai dari awal, karena kami tidak menghapus tahap animasi, tetapi jika kami benar-benar membutuhkannya, kami dapat menghapusnya bahkan.

Total


Merancang logika bisnis game melalui model, tim, dan file statis dengan aturan, membungkusnya di semua sisi dengan log yang terperinci dan dibuat secara otomatis dan melampirkan eksekusi informatif dari banyak kesalahan khas yang dibuat oleh seorang programmer yang melihat fitur-fitur baru, ini, menurut saya, adalah cara yang tepat untuk hidup cahaya putih. Dan bukan hanya karena Anda dapat mengajukan fungsionalitas baru beberapa kali lebih cepat. Ini masih sangat penting, karena jika mudah bagi Anda untuk mengunduh dan men-debug fitur baru, maka perancang game akan memiliki waktu untuk melakukan beberapa kali percobaan lebih banyak pada desain gamed dengan programmer yang sama. Dengan segala hormat terhadap pekerjaan kita, itu tergantung pada kita apakah permainan gagal atau tidak, tetapi apakah menembak atau tidak tergantung pada gamedisme, dan mereka perlu diberi ruang untuk eksperimen.
Dan sekarang saya meminta Anda untuk menjawab pertanyaan yang sangat penting bagi saya. Jika Anda memiliki gagasan tentang cara melakukan apa yang saya lakukan dengan buruk, atau hanya ingin mengomentari jawaban saya, saya menunggu Anda di komentar. Proposal untuk kerja sama dan instruksi tentang berbagai kesalahan sintaksis, silakan di PM.

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


All Articles