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



Pada artikel sebelumnya, kami menjelaskan bagaimana model harus diatur dengan nyaman dan dengan kemampuan luas, sistem perintah seperti apa yang cocok untuknya, yang bertindak sebagai pengontrol, saatnya berbicara tentang huruf ketiga dari singkatan MVC alternatif kami.

Sebenarnya, Assetstore memiliki perpustakaan UniRX yang sangat canggih dan siap pakai yang mengimplementasikan reaktivitas dan mengontrol inversi untuk persatuan. Tetapi kita akan membicarakannya di akhir artikel, karena alat yang kuat, besar, dan sesuai dengan RX ini untuk kasus kita cukup berlebihan. Untuk melakukan semua yang kami butuhkan sangat mungkin dilakukan tanpa menarik RX, dan jika Anda memilikinya, Anda dapat dengan mudah melakukan hal yang sama dengannya.

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

Ketika seseorang baru mulai menulis gim pertama, tampaknya logis baginya untuk memiliki fungsi yang akan menggambar seluruh bentuk, atau sebagian darinya, dan menariknya setiap kali ada perubahan penting. Seiring berjalannya waktu, antarmuka tumbuh dalam ukuran, bentuk dan bagian cetakan menjadi seratus, kemudian dua ratus, dan ketika dompet mengubah keadaannya, seperempat dari mereka harus digambar ulang. Dan kemudian manajer datang dan mengatakan bahwa "seperti dalam permainan itu" Anda perlu membuat titik merah kecil pada tombol jika ada bagian di dalam tombol di mana ada subbagian di mana tombol itu berada, dan sekarang Anda memiliki sumber daya yang cukup untuk melakukan sesuatu dengan mengkliknya itu penting. Dan itu saja, berlayar ...

Penyimpangan dari konsep menggambar berlangsung dalam beberapa tahap. Pertama, masalah bidang tunggal diselesaikan. Anda memiliki, misalnya, bidang dalam model, dan bidang teks di mana semua isinya harus ditampilkan. Oke, kami memulai objek yang berlangganan pembaruan bidang ini, dan setiap pembaruan menambahkan hasil ke bidang teks. Dalam kode tersebut, sesuatu seperti ini:

var observable = new ChildControl(FCPlayerModel.ASSIGNED, Player); observable.onChange(i => Assigned.text = i.ToString()) 

Sekarang kita tidak perlu mengikuti redrawing, cukup buat desain ini, dan kemudian semua yang terjadi dalam model akan jatuh ke antarmuka. Bagus, tapi rumit, itu mengandung banyak gerakan jelas yang tidak perlu bahwa seorang programmer harus menulis dengan tangannya 100.500 kali dan kadang-kadang membuat kesalahan. Mari kita bungkus iklan ini dalam fungsi ekstensi yang akan menyembunyikan huruf tambahan di bawah tenda.

 Player.Get(c, FCPlayerModel.ASSIGNED).Action(c, i => Assigned.text = i.ToString()); 

Jauh lebih baik, tapi bukan itu saja. Menggeser bidang model ke dalam bidang teks adalah operasi yang sering dan tipikal sehingga kita akan membuat fungsi wrapper terpisah untuknya. Sekarang ternyata cukup singkat dan baik, menurut saya.

 Player.Get(c, FCPlayerModel.ASSIGNED).SetText(c, Assigned); 

Di sini saya menunjukkan ide utama bahwa saya akan dibimbing oleh ketika membuat antarmuka untuk sisa hidup saya: "Jika seorang programmer harus melakukan sesuatu setidaknya dua kali, bungkus dengan fungsi khusus yang nyaman dan pendek."

Pengumpulan sampah


Efek samping dari rekayasa antarmuka reaktif adalah penciptaan banyak objek yang berlangganan sesuatu dan karenanya tidak akan meninggalkan memori tanpa tendangan khusus. Bagi saya sendiri, pada zaman kuno, saya datang dengan cara yang tidak begitu indah, tetapi sederhana dan terjangkau. Saat membuat formulir apa pun, daftar semua pengontrol dibuat yang dibuat sehubungan dengan formulir ini, untuk singkatnya, itu hanya disebut "c". Semua fungsi pembungkus khusus menerima daftar ini sebagai parameter yang diperlukan pertama dan ketika DisconnectModel formulir, itu melewati daftar semua kontrol dan tanpa belas kasihan gelisah dengan kode dalam leluhur yang sama. Tidak ada keindahan dan keanggunan, tetapi murah, dapat diandalkan dan relatif praktis. Anda dapat memiliki keamanan yang lebih banyak jika, alih-alih lembar kontrol, Anda memerlukan IView untuk masuk dan memberikan ini ke semua tempat ini. Pada dasarnya hal yang sama, lupa untuk mengisi hal yang sama tidak akan berhasil, tetapi lebih sulit untuk diretas. Saya takut lupa, tetapi saya tidak terlalu takut bahwa seseorang akan dengan sengaja merusak sistem, karena orang yang pandai seperti itu harus diperangi dengan ikat pinggang dan metode non-perangkat lunak lainnya, jadi saya hanya membatasi diri untuk c.

Pendekatan alternatif dapat diambil dari UniRX. Setiap bungkus membuat objek baru yang memiliki tautan ke yang sebelumnya ia dengarkan. Dan pada akhirnya, metode AddTo (komponen) dipanggil, yang mengaitkan seluruh rantai kontrol ke beberapa objek yang dapat dirusak. Dalam contoh kita, kode tersebut akan terlihat seperti ini:

 Player.Get(FCPlayerModel.ASSIGNED).SetText(Assigned).AddTo(this); 

Jika pemilik terakhir dari rantai ini memutuskan untuk dihancurkan, ia akan mengirim ke semua kontrol yang ditugaskan kepadanya perintah "bunuh diri tentang membuang jika tidak ada yang mendengarkan Anda kecuali saya". Dan seluruh rantai dengan patuh dibersihkan. Jadi tentu saja ini jauh lebih ringkas, tetapi dari sudut pandang saya ada satu kelemahan penting. AddTo dapat secara tidak sengaja dilupakan dan tidak ada yang akan mengetahuinya sampai semuanya terlambat.

Bahkan, Anda dapat menggunakan hack Unity yang kotor dan melakukannya tanpa kode tambahan di Tampilan:

 public static T AddTo<T>(this T disposable, Component component) where T : IDisposable { var composite = new CompositeDisposable(disposable); Observable .EveryUpdate() .Where(_ => component == null) .Subscribe(_ => composite.Dispose()) .AddTo(composite); return disposable; } 

Seperti yang Anda ketahui, tautan ke Komponen Unicomponent atau GameObject di Unity adalah nol. Tetapi Anda perlu memahami bahwa hakokostyl ini membuat pendengar yang diperbarui untuk setiap rantai kontrol yang dihancurkan, dan ini sudah agak sopan.

Antarmuka independen model


Cita-cita kami, yang, bagaimanapun, dapat kita capai dengan mudah, adalah situasi ketika kita dapat memuat GameState lengkap kapan saja, baik model yang diverifikasi server dan model data untuk UI, dan aplikasi akan berada dalam keadaan yang persis sama, sampai ke keadaan semua tombol. Ada dua alasan untuk ini. Yang pertama adalah bahwa beberapa programmer suka menyimpan di dalam pengontrol bentuk, atau bahkan dalam tampilan itu sendiri, mengutip fakta bahwa siklus hidup mereka persis sama dengan bentuk itu sendiri. Yang kedua adalah bahwa bahkan jika semua data untuk formulir ada dalam modelnya, perintah untuk membuat dan mengisi formulir itu sendiri mengambil bentuk pemanggilan fungsi eksplisit, dengan beberapa parameter tambahan, misalnya, bidang mana dari daftar yang harus difokuskan.

Anda tidak harus berurusan dengan ini jika Anda tidak benar-benar menginginkan kenyamanan debugging. Tapi kami tidak seperti itu, kami ingin men-debug antarmuka semudah operasi dasar dengan model. Untuk melakukan ini, fokus berikut. Di bagian UI model, variabel diatur, misalnya. Utama, dan di dalamnya, sebagai bagian dari perintah, Anda meletakkan model formulir yang ingin Anda lihat. Keadaan variabel ini dipantau oleh pengontrol khusus, jika sebuah model muncul dalam variabel ini, tergantung pada jenisnya, itu memunculkan formulir yang diinginkan, menempatkannya di tempat yang diperlukan, dan mengirimkannya panggilan ke ConnectModel (model). Jika variabel dibebaskan dari model, pengontrol akan menghapus formulir dari kanvas dan menggunakannya. Dengan demikian, tidak ada tindakan untuk memintas model terjadi, dan semua yang Anda lakukan dengan antarmuka terlihat jelas pada model ExportChanges. Dan kemudian kita dibimbing oleh prinsip "semua yang telah dilakukan dua kali bungkus" dan menggunakan pengontrol yang sama persis di semua tingkat antarmuka. Jika cetakan memiliki tempat untuk cetakan lain, maka model UI dibuat untuk itu, dan variabel dibuat dalam model cetakan induk. Persis sama dengan daftar.

Efek samping dari pendekatan ini adalah bahwa dua file ditambahkan ke formulir apa pun, satu dengan model data untuk formulir ini, dan yang lainnya, biasanya monobah yang berisi tautan ke elemen UI, yang, setelah menerima model dalam fungsi ConnectModel, akan membuat semua pengontrol reaktif untuk semua bidang model dan semua elemen UI. Yah, itu bahkan lebih ringkas, sehingga juga nyaman untuk bekerja dengannya, mungkin itu tidak mungkin. Jika memungkinkan, tulis di komentar.

Kontrol daftar


Situasi tipikal adalah ketika model memiliki daftar beberapa elemen. Karena saya ingin semuanya dilakukan dengan sangat mudah, dan lebih disukai pada satu baris, saya juga ingin melakukan sesuatu untuk daftar yang mudah ditangani. Satu baris mungkin, tetapi ternyata panjangnya tidak nyaman. Secara empiris, ternyata hampir seluruh keragaman kasus hanya ditutupi oleh dua jenis kontrol. Yang pertama memonitor keadaan koleksi dan memanggil tiga fungsi lambda, yang pertama dipanggil ketika beberapa elemen ditambahkan ke koleksi, yang kedua ketika elemen meninggalkan koleksi, dan akhirnya yang ketiga dipanggil ketika elemen koleksi mengubah urutan. Jenis kontrol kedua yang paling umum memantau daftar, dan merupakan sumber langganan darinya - halaman dengan nomor tertentu. Misalnya, ia mengikuti Daftar dengan panjang 102 elemen, dan itu sendiri mengembalikan Daftar 10 elemen, dari tanggal 20 hingga 29. Dan peristiwa yang dihasilkan persis sama seolah-olah itu daftar itu sendiri.

Tentu saja, mengikuti prinsip "membuat pembungkus untuk segala sesuatu yang dilakukan dua kali", sejumlah besar pembungkus nyaman muncul, misalnya, yang hanya menerima input Pabrik, membangun korespondensi antara jenis model dan Tampilan mereka, dan tautan ke Kanvas di mana Anda perlu menambahkan elemen. Dan banyak yang serupa lainnya, hanya sekitar selusin pembungkus untuk kasus khas.

Kontrol yang lebih kompleks


Kadang-kadang muncul situasi yang berlebihan untuk diekspresikan melalui model, sebanyak mereka jelas. Di sini, kontrol yang melakukan semacam operasi pada suatu nilai dapat datang ke penyelamatan, serta kontrol yang memantau kontrol lainnya. Misalnya, situasi umum: tindakan memiliki harga, dan tombol hanya aktif jika ada lebih banyak uang di akun daripada harganya.

 item.Get(c, FCUnitItem.COST).Join(c, Player.Get(c, MONEY)).Func(c, (cost, money) => cost <= money).SetActive(c, BuyButton); 

Bahkan, situasinya sangat khas sehingga sesuai dengan prinsip saya ada pembungkus yang sudah jadi untuk itu, tapi kemudian saya menunjukkan isinya.

Kami mengambil barang yang akan dibeli, membuat objek yang berlangganan salah satu bidangnya, dan memiliki nilai tipe panjang. Mereka menambahkan satu kontrol lagi, yang juga bertipe panjang, metode mengembalikan kontrol yang memiliki sepasang nilai, dan acara yang diubah dipecat ketika salah satu dari mereka berubah, kemudian fungsi menciptakan objek untuk setiap perubahan dalam input yang menghitung fungsi, dan acara yang diubah jika nilai akhir dihitung. fungsi telah berubah.

Compiler akan berhasil membangun tipe kontrol yang diperlukan berdasarkan tipe data input dan tipe ekspresi yang dihasilkan. Dalam kasus yang jarang terjadi ketika jenis yang dikembalikan oleh fungsi lambda tidak jelas, kompiler akan meminta Anda untuk mengklarifikasi secara eksplisit. Akhirnya, panggilan terakhir mendengarkan kontrol Boolean, tergantung pada yang menghidupkan atau mematikan tombol.

Bahkan, pembungkus nyata dalam proyek menerima dua tombol untuk input, satu untuk kasus ketika ada uang dan yang lainnya ketika tidak ada cukup uang, dan perintah untuk membuka jendela modal "Beli mata uang" juga tergantung pada tombol kedua. Dan semua ini dalam satu baris sederhana.

Sangat mudah untuk melihat bahwa menggunakan Bergabung dan Berfungsi Anda dapat membangun struktur kompleks sewenang-wenang. Dalam kode saya, ada fungsi yang menghasilkan kontrol kompleks, menghitung berapa banyak pemain dapat membeli dengan memperhitungkan jumlah pemain di sisinya, dan aturan bahwa setiap orang bisa melebihi anggaran sebesar 10% jika semua bersama-sama tidak melebihi total anggaran. Dan ini adalah contoh bagaimana tidak perlu melakukannya, karena seberapa sederhana dan mudah untuk men-debug apa yang terjadi dalam model, sama sulitnya untuk menangkap kesalahan dalam kontrol reaktif. Anda bahkan akan menangkap eksekusi dan menghabiskan banyak waktu untuk memahami apa yang menyebabkannya.

Oleh karena itu, prinsip umum menggunakan kontrol kompleks adalah sebagai berikut: Saat membuat prototipe formulir, Anda dapat menggunakan struktur pada kontrol reaktif, terutama jika Anda tidak yakin bahwa mereka akan menjadi lebih rumit di masa depan, tetapi segera setelah Anda curiga bahwa jika rusak, Anda tidak akan mengerti apa yang terjadi, Anda harus segera mentransfer manipulasi ini ke model, dan memasukkan perhitungan yang sebelumnya dilakukan dalam kontrol ke metode ekstensi di kelas aturan statis.

Ini sangat berbeda dari prinsip “Lakukan segera dengan segera”, yang sangat disukai di kalangan perfeksionis, karena kita hidup di dunia pengembang game, dan ketika Anda mulai melewati formulir, Anda sama sekali tidak dapat memastikan apa yang akan dilakukan dalam tiga hari. Seperti yang dikatakan salah satu kolega saya: "Jika saya mendapat lima sen setiap kali perancang permainan berubah pikiran, saya sudah menjadi orang yang sangat kaya." Sebenarnya, ini tidak buruk, tetapi bahkan sebaliknya itu baik. Gim ini harus berkembang dengan coba-coba, karena jika Anda tidak melakukan kloning bodoh, maka Anda tidak dapat membayangkan apa yang benar-benar dibutuhkan pemain.

Satu sumber data untuk banyak tampilan


Untuk begitu banyak kasus pola dasar yang perlu Anda bicarakan secara terpisah. Kebetulan bahwa model elemen yang sama sebagai bagian dari model antarmuka dirender dalam Tampilan berbeda tergantung di mana dan dalam konteks apa ini terjadi. Dan kami menggunakan prinsip - "satu tipe, satu tampilan." Misalnya, Anda memiliki kartu pembelian senjata yang berisi informasi yang tidak rumit yang sama, tetapi dalam mode toko yang berbeda, kartu itu harus diwakili oleh prefab yang berbeda. Solusinya terdiri dari dua bagian untuk dua situasi yang berbeda.

Yang pertama adalah ketika Tampilan ini ditempatkan di dalam dua Tampilan yang berbeda, misalnya, toko dalam bentuk daftar pendek dan toko dengan gambar besar. Dalam hal ini, dua pabrik terpisah didirikan untuk membantu, membangun pencocokan jenis cetakan. Dalam metode ConnectModel dari satu Tampilan, Anda akan menggunakan satu dan yang lain di yang lain. Ini kasus yang sama sekali berbeda jika Anda perlu menunjukkan kartu dengan informasi yang benar-benar identik di satu tempat sedikit berbeda. Terkadang, dalam kasus ini, model elemen memiliki bidang tambahan yang menunjukkan latar belakang meriah dari elemen tertentu, dan kadang-kadang hanya saja model elemen memiliki ahli waris yang tidak memiliki bidang apa pun dan hanya perlu digambar dengan cetakan lain. Pada prinsipnya, tidak ada yang bertentangan.

Tampaknya ini solusi yang jelas, tetapi saya melihat cukup banyak kode aneh tentang tarian aneh dengan rebana di sekitar situasi ini, dan menganggap perlu untuk menulis tentang itu.

Kasus khusus: kontrol dengan banyak dependensi


Ada satu kasus khusus yang ingin saya bicarakan secara terpisah. Ini adalah kontrol yang memantau sejumlah besar elemen. Misalnya, kontrol yang memantau daftar model dan meringkas konten bidang yang terletak di dalam masing-masing elemen. Dengan overtube besar dalam daftar, misalnya, mengisinya dengan data, kontrol seperti itu berisiko menangkap banyak peristiwa tentang perubahan karena ada plus satu dalam daftar elemen. Menghitung ulang fungsi agregat berkali-kali tentu saja merupakan ide yang buruk. Khusus untuk kasus seperti itu, kami membuat kontrol yang berlangganan acara onTransactionFinished, yang menonjol dari GameState, dan seingat kami, tautan ke GameState tersedia dalam model apa pun. Dan dengan perubahan apa pun pada input, kontrol ini hanya akan memberi tanda pada dirinya sendiri bahwa data asli telah berubah, dan hanya akan diceritakan kembali ketika menerima pesan tentang akhir transaksi, atau ketika menemukan bahwa transaksi tersebut sudah selesai pada saat ketika menerima pesan dari aliran peristiwa input . Jelas bahwa kontrol semacam itu mungkin tidak dilindungi dari pesan yang tidak perlu jika ada dua kontrol seperti itu dalam rantai pemrosesan aliran. Yang pertama akan mengumpulkan awan perubahan, menunggu akhir transaksi, memulai aliran perubahan lebih lanjut, dan ada satu lagi yang telah menangkap banyak perubahan, menerima acara tentang akhir transaksi (ia beruntung berada dalam daftar fungsi yang dilanggankan pada acara sebelumnya), menghitung semuanya, dan kemudian dia bam dan acara perubahan lainnya, dan menceritakan semuanya untuk kedua kalinya. Mungkin, tetapi jarang, dan yang lebih penting, jika kontrol Anda melakukan perhitungan mengerikan seperti lebih dari sekali dalam satu aliran perhitungan, maka Anda melakukan sesuatu yang salah, dan Anda perlu mentransfer semua manipulasi neraka ini ke model dan aturan, di mana mereka , faktanya, tempat itu.

Perpustakaan Siap UniRX


Dan mungkin untuk membatasi diri pada semua hal di atas, dan dengan tenang mulai menulis karya agung Anda, terutama karena dibandingkan dengan model dan tim kontrol itu sangat sederhana dan mereka ditulis dalam waktu kurang dari seminggu, jika gagasan bahwa Anda menciptakan sepeda tidak mengaburkan, dan semuanya sudah dipikirkan dan ditulis sebelum saya didistribusikan secara gratis kepada semua orang.

Mengungkap UniRX, kami menemukan desain yang cantik dan sesuai standar yang dapat membuat utas dari semuanya secara umum, menggabungkannya dengan cerdik, menyaringnya dari utas utama ke utas non-utama, atau mengembalikan kontrol kembali ke utas utama, yang memiliki banyak alat siap pakai untuk dikirim ke tempat yang berbeda, dan seterusnya. selanjutnya. Kami tidak memiliki dua hal di sana: Kesederhanaan dan Kemudahan debugging. Apakah Anda pernah mencoba men-debug beberapa bangunan bertingkat di Linq dalam langkah-langkah di debugger? Jadi di sini masih jauh lebih buruk. Pada saat yang sama, kami benar-benar kekurangan untuk apa semua mesin canggih ini diciptakan. Demi kesederhanaan kondisi debugging dan reproduksi, kami benar-benar tidak memiliki berbagai sumber sinyal, semuanya terjadi di aliran utama, karena menggoda dengan multithreading di permainan-meta sepenuhnya berlebihan, semua asinkronasi pemrosesan perintah tersembunyi di dalam mesin pengirim perintah, dan asinkroninya sendiri mengambil sangat banyak di dalamnya tidak banyak ruang, lebih banyak perhatian diberikan pada semua jenis pemeriksaan, pemeriksaan mandiri, dan kemungkinan pencatatan dan pemutaran.

Secara umum, jika Anda sudah tahu cara menggunakan UniRX, saya akan membuatnya khusus untuk Anda untuk model IObservable, dan Anda dapat menggunakan fitur truf perpustakaan favorit Anda di mana Anda membutuhkannya, tetapi untuk sisanya saya sarankan untuk tidak membangun tank dari mobil berkecepatan tinggi dan mobil dari tank hanya di atas tanah. bahwa keduanya memiliki roda.

Di akhir artikel ini, saya sampaikan kepada Anda, para pembaca yang budiman, pertanyaan-pertanyaan tradisional yang sangat penting bagi saya, ide-ide saya tentang yang indah, dan untuk prospek pengembangan karya ilmiah dan teknis saya.

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


All Articles