Bagaimana Kami Datang untuk Menghubungkan Reaktif di Unity3D

gambar



Hari ini saya akan berbicara tentang bagaimana beberapa proyek di Pixonic menjadi apa yang telah lama menjadi norma untuk seluruh front-end global - penautan reaktif.



Sebagian besar proyek kami ditulis dalam Unity 3D. Dan, jika teknologi klien lain dengan reaktif bekerja dengan baik (MVVM, Qt, jutaan kerangka kerja JS), dan diterima begitu saja, Unity tidak memiliki binding bawaan atau yang diterima secara umum.



Saat ini, seseorang mungkin memiliki pertanyaan: โ€œMengapa? Kami tidak menggunakannya dan kami hidup dengan baik. "



Ada alasannya. Lebih tepatnya, ada masalah, salah satu solusi yang dapat digunakan pendekatan seperti itu. Hasilnya, menjadi satu. Dan detailnya ada di bawah potongan.



Pertama, tentang proyek, masalah yang membutuhkan solusi seperti itu. Tentu saja, kita berbicara tentang War Robots - sebuah proyek raksasa dengan banyak tim pengembangan, dukungan, pemasaran, dll. Sekarang kita hanya tertarik pada dua di antaranya: tim programmer klien dan tim antarmuka pengguna. Berikut ini, untuk kesederhanaan, kami akan menyebutnya "kode" dan "tata letak". Kebetulan beberapa orang terlibat dalam desain dan tata letak UI, sementara yang lain melakukan "revitalisasi" semua ini. Ini logis, dan menurut pengalaman saya, saya telah menemukan banyak contoh organisasi tim yang serupa.



Kami memperhatikan bahwa dengan bertambahnya aliran fitur pada proyek, interaksi antara kode dan tata letak menjadi tempat kebuntuan dan kemacetan. Pemrogram menunggu widget siap pakai untuk bekerja, desainer tata letak - untuk beberapa modifikasi dari kode. Ya, banyak hal terjadi selama interaksi ini. Singkatnya, terkadang itu berubah menjadi kekacauan dan penundaan.



Biar saya jelaskan sekarang. Lihat contoh widget sederhana klasik - terutama metode RefreshData. Sisa dari boilerplate saya baru saja menambahkan agar masuk akal, dan itu tidak terlalu menarik perhatian.



public class PlayerProfileWidget : WidgetBehaviour
{
  [SerializeField] private Text nickname;
  [SerializeField] private Image avatar;
  [SerializeField] private Text level;
  [SerializeField] private GameObject hasUpgradeMark;
  [SerializeField] private Button upgradeButton;

  public void Initialize(ProfileService profileService)
  {
 	RefreshData(profileService.Player);

 	upgradeButton.onClick
    	.Subscribe(profileService.UpgradePlayer)
    	.DisposeWith(Lifetime);

 	profileService.PlayerUpgraded
    	.Subscribe(RefreshData)
    	.DisposeWith(Lifetime);
  }

  private void RefreshData(in PlayerModel player)
  {
 	nickname.text = player.Id;
 	avatar.overrideSprite = Resources.Load<Sprite>($"Avatars/{player.Avatar}_Small");
 	level.text = player.Level.ToString();
 	hasUpgradeMark.SetActive(player.HasUpgrade);
  }
}


Ini adalah contoh penautan atas-bawah statis. Dalam komponen GameObject atas (dalam hierarki), Anda menautkan komponen dari jenis objek yang lebih rendah yang sesuai. Semua yang ada di sini sangat sederhana, tetapi tidak terlalu fleksibel.



Fungsionalitas widget terus berkembang dengan munculnya fitur-fitur baru. Bayangkan. Sekarang seharusnya ada batas di sekitar avatar, yang tampilannya bergantung pada level pemain. Oke, mari tambahkan tautan ke Gambar bingkai dan rendam sprite yang sesuai dengan level di sana, lalu tambahkan pengaturan untuk mencocokkan level dan bingkai dan berikan semuanya ke tata letak. Selesai.



Sebulan telah berlalu. Sekarang ikon klan muncul di widget pemain, jika dia adalah anggota. Dan Anda juga perlu mendaftarkan gelar yang dia miliki di sana. Dan nickname perlu dicat hijau jika ada upgrade. Selain itu, kami sekarang menggunakan TextMeshPro. Dan juga ...



Nah, Anda mengerti. Kode menjadi semakin banyak, menjadi semakin rumit, ditumbuhi berbagai kondisi.



Ada beberapa opsi untuk bekerja di sini. Misalnya, programmer memodifikasi kode widget, memberikan perubahan pada tata letak. Mereka menambahkan dan menautkan komponen ke bidang baru. Atau sebaliknya: tata letak mungkin tiba lebih awal, programmer sendiri akan menghubungkan semua yang dibutuhkan. Biasanya, ada beberapa iterasi perbaikan lagi. Bagaimanapun, proses ini tidak paralel. Kedua kontributor sedang mengerjakan resource yang sama. Dan menggabungkan prefab atau adegan masih menyenangkan.



Bagi para insinyur, semuanya sederhana: jika Anda melihat masalah, Anda mencoba menyelesaikannya. Jadi kami mencoba. Akibatnya, kami sampai pada gagasan bahwa perlu mempersempit kontak depan antara kedua tim. Dan pola reaktif mempersempit bagian depan ini ke satu titik - yang biasa disebut Model Tampilan. Bagi kami, ini bertindak sebagai kontrak antara kode dan tata letak. Ketika saya turun ke detailnya, arti kontrak akan menjadi jelas, dan mengapa itu tidak menghalangi operasi paralel dua tim.



Pada saat kami baru saja memikirkan semua ini, ada beberapa solusi pihak ketiga. Kami mencari Unity Weld, Peppermint Data Binding, DisplayFab. Mereka semua memiliki pro dan kontra. Tetapi salah satu kekurangan fatal bagi kami adalah hal biasa - kinerja yang buruk untuk tujuan kami. Mereka mungkin bekerja dengan baik pada antarmuka sederhana, tetapi pada saat itu kami tidak dapat menghindari kerumitan antarmuka.



Karena tugas tersebut tampaknya tidak terlalu sulit, dan bahkan pengalaman yang relevan tersedia, diputuskan untuk menerapkan sistem pengikatan reaktif di dalam studio.



Tugasnya adalah sebagai berikut:



  • Performa. Mekanisme untuk menyebarkan perubahan itu sendiri harus cepat. Juga diinginkan untuk mengurangi beban pada GC sehingga Anda dapat menggunakan semua ini bahkan dalam gameplay, di mana freeze sama sekali tidak menyenangkan.
  • Penulisan yang nyaman. Ini diperlukan agar orang-orang dari tim UI dapat bekerja dengan sistem.
  • API yang nyaman.
  • Kemungkinan diperpanjang.




Atas ke bawah, atau deskripsi umum



Tugasnya jelas, tujuannya jelas. Mari kita mulai dengan "kontrak" - ViewModel. Setiap orang harus dapat membentuknya, yang berarti penerapan ViewModel harus sesederhana mungkin. Ini pada dasarnya hanya sekumpulan properti yang menentukan status tampilan saat ini.



Untuk mempermudah, kami telah membatasi set tipe properti dengan nilai bool, int, float dan string sebanyak mungkin. Ini ditentukan oleh beberapa pertimbangan sekaligus:



  • Membuat serialisasi jenis ini dalam Unity sangatlah mudah;
  • , -, . , Sprite -, PlayerModel , ;
  • , .


Semua properti aktif dan memberi tahu pelanggan tentang perubahan pada nilainya. Nilai-nilai ini tidak selalu ada - hanya ada peristiwa dalam logika bisnis yang perlu divisualisasikan. Dalam kasus ini, ada tipe properti tanpa nilai - peristiwa.



Tentu saja, Anda juga tidak dapat melakukannya tanpa koleksi di antarmuka. Oleh karena itu, ada juga tipe properti collection. Koleksi ini memberi tahu pelanggan tentang perubahan apa pun dalam komposisinya. Elemen koleksi juga merupakan ViewModels dari struktur atau skema tertentu. Skema ini juga dijelaskan dalam kontrak saat mengedit.



Dalam editor ViewModel terlihat seperti ini:







Perlu dicatat bahwa properti dapat diedit langsung di inspektur dan dengan cepat. Hal ini memungkinkan Anda untuk melihat bagaimana widget (atau jendela, atau pemandangan, atau apa pun) akan berperilaku saat runtime bahkan tanpa kode, yang sangat nyaman dalam praktiknya.



Jika ViewModel adalah bagian atas dari sistem penjilidan kami, maka bagian bawah adalah yang disebut aplikator. Ini adalah pelanggan terakhir dari properti ViewModel yang melakukan semua pekerjaan:



  • Mengaktifkan / menonaktifkan GameObject atau komponen individu dengan mengubah nilai properti boolean;
  • Ubah teks di bidang tergantung pada nilai properti string;
  • Animator diluncurkan, parameternya diubah;
  • Gantikan sprite yang diinginkan dari koleksi dengan kunci indeks atau string.


Saya akan berhenti di sini, karena jumlah aplikasi hanya dibatasi oleh imajinasi dan rentang tugas yang Anda selesaikan.



Beginilah tampilan beberapa aplikator di editor:









Untuk fleksibilitas lebih, adaptor dapat digunakan antara properti dan aplikator. Ini adalah entitas untuk mengubah properti sebelum diterapkan. Ada juga banyak yang berbeda:



  • Boolean - misalnya, ketika Anda perlu membalikkan properti boolean atau mengembalikan true atau false tergantung pada nilai jenis yang berbeda (saya ingin batas emas saat level di atas 15).
  • Aritmatika . Tidak ada komentar di sini.
  • Operasi pada koleksi : membalikkan, mengambil hanya sebagian dari koleksi, mengurutkan berdasarkan kunci, dan banyak lagi.


Sekali lagi, mungkin ada berbagai macam opsi adaptor yang berbeda, jadi saya tidak akan melanjutkan.











Faktanya, meskipun jumlah total aplikator dan adaptor yang berbeda besar, perangkat dasar yang digunakan di mana-mana sangat terbatas. Seseorang yang bekerja dengan konten perlu mempelajari set ini terlebih dahulu, yang sedikit menambah waktu pelatihan. Namun, Anda perlu mencurahkan waktu untuk ini sekali, agar lebih jauh tidak ada masalah besar di sini. Apalagi kami memiliki buku resep dan dokumentasi tentang hal ini.



Ketika layout kekurangan sesuatu, programmer menambahkan komponen yang diperlukan. Pada saat yang sama, sebagian besar aplikator dan adaptor bersifat universal dan digunakan kembali secara aktif. Secara terpisah, perlu dicatat bahwa kami masih memiliki aplikator yang mengerjakan refleksi melalui UnityEvent. Mereka berlaku dalam kasus di mana aplikator yang diperlukan belum diterapkan atau implementasinya tidak praktis.



Ini tentu saja menambah pekerjaan tim tata letak. Namun dalam kasus kami, mereka bahkan senang dengan tingkat kebebasan dan kemandirian dari programmer yang mereka dapatkan. Dan jika pekerjaan telah meningkat dari sisi tata letak, maka dari sisi kode semuanya sekarang jauh lebih mudah.



Mari kembali ke contoh PlayerProfileWidget. Ini adalah tampilannya sekarang dalam proyek hipotetis kami sebagai presenter, karena kami tidak lagi membutuhkan Widget sebagai komponen, dan kami bisa mendapatkan semuanya dari ViewModel daripada menautkan semuanya secara langsung:



public class PlayerProfilePresenter : Presenter
{
  private readonly IMutableProperty<string> _playerId;
  private readonly IMutableProperty<string> _playerAvatar;
  private readonly IMutableProperty<int> _playerLevel;
  private readonly IMutableProperty<bool> _playerHasUpgrade;

  public PlayerProfilePresenter(ProfileService profileService, IViewModel viewModel)
  {
 	_playerId = viewModel.GetString("player/id");
 	_playerAvatar = viewModel.GetString("player/avatar");
 	_playerLevel = viewModel.GetInteger("player/level");
 	_playerHasUpgrade = viewModel.GetBoolean("player/has-upgrade");

 	RefreshData(profileService.Player);

 	viewModel.GetEvent("player/upgrade")
    	.Subscribe(profileService.UpgradePlayer)
    	.DisposeWith(Lifetime);

 	profileService.PlayerUpgraded
    	.Subscribe(RefreshData)
    	.DisposeWith(Lifetime);
  }

  private void RefreshData(in PlayerModel player)
  {
 	_playerId.Value = player.Id;
 	_playerAvatar.Value = player.Avatar;
 	_playerLevel.Value = player.Level;
 	_playerHasUpgrade.Value = player.HasUpgrade;
  }
}


Dalam konstruktor, Anda dapat melihat kode mendapatkan properti dari ViewModel. Ya, dalam kode ini, pemeriksaan dihilangkan untuk kesederhanaan, tetapi ada metode yang akan memunculkan pengecualian jika mereka tidak menemukan properti yang diinginkan. Selain itu, kami memiliki beberapa alat yang memberikan jaminan yang cukup kuat bahwa bidang yang diperlukan tersedia. Mereka didasarkan pada validasi aset, yang dapat Anda baca di sini .



Saya tidak akan membahas detail implementasi, karena akan memakan banyak teks dan waktu Anda. Jika ada penyelidikan publik, sebaiknya diterbitkan di artikel tersendiri. Saya hanya akan mengatakan bahwa implementasinya tidak jauh berbeda dari Rx yang sama, hanya saja semuanya sedikit lebih sederhana.



Tabel menunjukkan hasil benchmark yang membuat 500 formulir dengan InputField, Teks dan Tombol, terkait dengan satu properti model dan satu fungsi tindakan.







Sebagai kesimpulan, saya dapat melaporkan bahwa tujuan di atas telah tercapai. Tolok ukur komparatif menunjukkan keuntungan baik dalam memori dan dalam waktu relatif terhadap opsi yang disebutkan. Saat tim tata letak dan orang-orang dari departemen lain yang menangani konten menjadi lebih sadar, gesekan dan pemblokiran menjadi semakin berkurang. Efisiensi dan kualitas kode telah meningkat, dan sekarang banyak hal tidak memerlukan campur tangan programmer.



All Articles