Tidak ada satu pun monolit. Pendekatan modular dalam Unity

gambar


Artikel ini akan membahas pendekatan modular untuk desain dan implementasi game lebih lanjut di mesin Unity. Pro, kontra, dan masalah utama yang harus Anda hadapi dijelaskan.



Istilah "pendekatan modular" berarti organisasi perangkat lunak yang menggunakan rakitan akhir yang independen, dapat dicolokkan secara internal yang dapat dikembangkan secara paralel, diubah dengan cepat, dan mencapai perilaku perangkat lunak yang berbeda tergantung pada konfigurasinya.



Struktur modul



Penting untuk terlebih dahulu menentukan apa itu modul, struktur apa yang dimilikinya, bagian mana dari sistem yang bertanggung jawab atas apa dan bagaimana modul tersebut harus digunakan.



Modul adalah rakitan yang relatif independen yang tidak bergantung pada proyek. Ini dapat digunakan dalam proyek yang sangat berbeda dengan konfigurasi yang tepat dan adanya inti yang sama dalam proyek. Kondisi wajib untuk implementasi modul adalah adanya jejak. bagian:



Perakitan infrastruktur


Rakitan ini berisi model dan kontrak yang dapat digunakan oleh rakitan lain. Penting untuk dipahami bahwa bagian modul ini tidak boleh memiliki tautan ke implementasi fitur tertentu. Idealnya, kerangka kerja hanya dapat merujuk pada inti proyek.

Struktur perakitan terlihat seperti berikut ini. cara:



gambar


  • Entitas - entitas yang digunakan di dalam modul.
  • Pesan - model permintaan / sinyal. Anda dapat membacanya nanti.
  • Kontrak adalah tempat untuk menyimpan antarmuka.


Penting untuk diingat bahwa Anda disarankan untuk meminimalkan penggunaan referensi di antara rakitan infrastruktur.



Dibangun dengan fitur


Implementasi khusus dari fitur tersebut. Ia dapat menggunakan di dalam dirinya sendiri salah satu pola arsitektur, tetapi dengan amandemen bahwa sistem harus modular.

Arsitektur internal dapat terlihat seperti ini:



gambar


  • Entitas - entitas yang digunakan di dalam modul.
  • Pemasang - Kelas untuk mendaftarkan kontrak untuk DI.
  • Layanan adalah lapisan bisnis.
  • Manajer - tugas manajer adalah menarik data yang diperlukan dari layanan, membuat ViewEntity, dan mengembalikan ViewManager.
  • ViewManagers - Menerima ViewEntity dari Manajer, membuat Tampilan yang diperlukan, meneruskan data yang diperlukan.
  • View - Menampilkan data yang diteruskan dari ViewManager.


Menerapkan pendekatan modular



Untuk menerapkan pendekatan ini, setidaknya dua mekanisme mungkin diperlukan. Kami membutuhkan pendekatan untuk membagi kode menjadi rakitan dan kerangka DI. Contoh ini menggunakan mekanisme File Definisi Majelis dan Zenject.



Penggunaan mekanisme spesifik di atas adalah opsional. Hal utama adalah memahami untuk apa mereka digunakan. Anda dapat mengganti Zenject dengan kerangka DI apa pun dengan wadah IoC atau yang lain, dan File Definisi Majelis - dengan sistem lain yang memungkinkan Anda menggabungkan kode menjadi rakitan atau membuatnya independen (Misalnya, Anda dapat menggunakan repositori berbeda untuk modul berbeda yang dapat dihubungkan sebagai peckages, submodul gita atau yang lainnya).



Fitur dari pendekatan modular adalah tidak ada referensi eksplisit dari perakitan satu fitur ke fitur lainnya, dengan pengecualian referensi ke rakitan infrastruktur di mana model dapat disimpan. Interaksi antara modul diimplementasikan menggunakan pembungkus sinyal dari kerangka kerja Zenject. Wrapper memungkinkan Anda mengirim sinyal dan permintaan ke modul yang berbeda. Perlu dicatat bahwa sinyal berarti pemberitahuan oleh modul saat ini dari modul lain, dan permintaan berarti permintaan untuk modul lain yang dapat mengembalikan data.



Sinyal


Sinyal - mekanisme untuk memberi tahu sistem tentang beberapa perubahan. Dan cara termudah untuk membongkarnya adalah dengan berlatih.



Katakanlah kita memiliki 2 modul. Foo dan Foo2. Modul Foo2 harus menanggapi beberapa perubahan dalam modul Foo. Untuk menghilangkan ketergantungan pada modul, 2 sinyal diimplementasikan. Satu sinyal di dalam modul Foo, yang akan menginformasikan sistem tentang perubahan status, dan sinyal kedua di dalam modul Foo2. Modul Foo2 akan bereaksi terhadap sinyal ini. Perutean sinyal OnFooSignal di OnFoo2Signal akan berada di modul perutean.

Secara skematis akan terlihat seperti ini:



gambar




Pertanyaan


Query memungkinkan penyelesaian masalah komunikasi penerimaan / pengiriman data oleh satu modul dari modul lain (lainnya).



Mari kita pertimbangkan contoh serupa yang diberikan di atas untuk sinyal.

Katakanlah kita memiliki 2 modul. Foo dan Foo2. Modul Foo membutuhkan beberapa data dari modul Foo2. Pada saat yang sama, modul Foo seharusnya tidak mengetahui apa-apa tentang modul Foo2. Sebenarnya, masalah ini dapat diselesaikan dengan menggunakan sinyal tambahan, tetapi solusi dengan kueri terlihat lebih sederhana dan lebih indah.



Ini akan terlihat seperti ini secara skematis:



gambar


Komunikasi antar modul



Untuk meminimalkan tautan antara modul dengan fitur (termasuk tautan Infrastruktur-Infrastruktur), diputuskan untuk menulis pembungkus di atas sinyal yang disediakan oleh kerangka kerja Zenject dan membuat modul yang tugasnya adalah untuk merutekan sinyal dan data peta yang berbeda.



PS Sebenarnya, modul ini memiliki link ke semua rakitan Infrastruktur yang tidak bagus. Namun masalah ini bisa diselesaikan melalui IoC.



Contoh interaksi modul



Katakanlah ada dua modul. LoginModule dan RewardModule. RewardModule harus memberikan hadiah kepada pengguna setelah login FB.



namespace RewardModule.src.Infrastructure.Messaging.Signals
{
    public class OnLoginSignal : SignalBase
    {
        public bool IsFirstLogin { get; set; }
    }
}


namespace RewardModule.src.Infrastructure.Messaging.RequestResponse.Produce
{
    public class GainRewardRequest : EventBusRequest<ProduceResponse>
    {
        public bool IsFirstLogin { get; set; }
    }
}


namespace MessagingModule.src.Feature.Proxy
{
    public class LoginModuleProxy
    {
        [Inject]
        private IEventBus eventBus;
        
        public override async void Subscribe()
        {            
            eventBus.Subscribe<OnLoginSignal>((loginSignal) =>
            {
                var request = new GainRewardRequest()
                {
                    IsFirstLogin = loginSignal.IsFirstLogin;
                }

                var result = await eventBus.FireRequestAsync<GainRewardRequest, GainRewardResponse>(request);
                var analyticsEvent = new OnAnalyticsShouldBeTracked()
                {
                   AnalyticsPayload = new Dictionary<string, string>
                    {
                      {
                        "IsFirstLogin", "false"
                      },
                    },
                  };
                eventBus.Fire<OnAnalyticsShouldBeTrackedSignal>(analyticsEvent);
            });


Pada contoh di atas, tidak ada tautan langsung antar modul. Tapi mereka terhubung melalui MessagingModule. Sangat penting untuk diingat bahwa seharusnya tidak ada dalam routing selain sinyal / permintaan routing dan pemetaan.



Substitusi implementasi



Dengan menggunakan pendekatan modular dan pola beralih Fitur, Anda dapat mencapai hasil yang luar biasa dalam hal dampak pada aplikasi Anda. Memiliki konfigurasi tertentu di server, Anda dapat memanipulasi pengaktifan / penonaktifan modul yang berbeda di awal aplikasi, mengubahnya selama permainan.



Hal ini dicapai dengan fakta bahwa selama pengikatan modul di Zenject (pada kenyataannya, ke dalam wadah), bendera ketersediaan modul diperiksa dan, berdasarkan ini, modul diikat ke dalam wadah atau tidak. Untuk mencapai perubahan perilaku selama sesi permainan (katakanlah Anda perlu mengubah mekanisme selama sesi permainan. Ada modul Solitaire dan modul Klondike. Dan untuk 50 persen pengguna, modul kerchief harus berfungsi), sebuah mekanisme dikembangkan yang, ketika beralih dari satu adegan ke adegan lain membersihkan wadah modul tertentu dan mengikat dependensi baru.



Dia bekerja di jalan setapak. prinsip: jika sebuah fitur diaktifkan, dan kemudian selama sesi dinonaktifkan, maka perlu untuk mengosongkan penampung. Jika fitur ini diaktifkan, Anda perlu melakukan semua perubahan pada penampung. Hal ini penting dilakukan pada tahap "kosong" agar tidak melanggar integritas data dan koneksi. Perilaku ini dapat diterapkan, tetapi sebagai fitur produksi, tidak disarankan untuk menggunakan fungsionalitas tersebut, karena hal itu berisiko lebih besar untuk merusak sesuatu.



Di bawah ini adalah pseudocode dari kelas dasar, yang turunannya diperlukan untuk mendaftarkan sesuatu di container.



    public abstract class GlobalInstallerBase<TGlobalInstaller, TModuleInstaller> : MonoInstaller<TGlobalInstaller>
        where TGlobalInstaller : MonoInstaller<TGlobalInstaller>
        where TModuleInstaller : Installer
    {
        protected abstract string SubContainerName { get; }
        
        protected abstract bool IsFeatureEnabled { get; }
        
        public override void InstallBindings()
        {
            if (!IsFeatureEnabled)
            {
                return;
            }
            
            var subcontainer = Container.CreateSubContainer();
            subcontainer.Install<TModuleInstaller>();
            
            Container.Bind<DiContainer>()
                .WithId(SubContainerName)
                .FromInstance(subcontainer)
                .AsCached();
        }
        
        protected virtual void SubContainerCleaner(DiContainer subContainer)
        {
            subContainer.UnbindAll();
        }

        protected virtual DiContainer SubContainerInstanceGetter(InjectContext containerContext)
        {
            return containerContext.Container.ResolveId<DiContainer>(SubContainerName);
        }
    }


Contoh modul primitif



Mari kita lihat contoh sederhana bagaimana modul dapat diimplementasikan.



Misalkan Anda perlu mengimplementasikan modul yang akan membatasi pergerakan kamera sehingga pengguna tidak dapat membawanya melampaui "batas" layar.



Modul akan berisi rakitan Infrastruktur dengan sinyal yang akan memberi tahu bahwa kamera telah mencoba beralih ke sistem dari layar.



Fitur - implementasi fitur. Ini akan menjadi logika untuk memeriksa apakah kamera di luar jangkauan, memberi tahu modul lain tentangnya, dll.



gambar


  • BorderConfig adalah entitas yang menggambarkan batas-batas layar.
  • BorderViewEntity adalah entitas yang akan diteruskan ke ViewManager dan View.
  • BoundingBoxManager - mendapatkan BorderConfig dari server, membuat BorderViewEntity.
  • BoundingBoxViewManager โ€” MonoBehaviour'a. , .
  • BoundingBoxView โ€” , ยซยป .




  • . , , .
  • .
  • EventHell, , .
  • โ€” , . , , โ€” .
  • .
  • .
  • - , . , MVC, โ€” ECS.
  • , .
  • , .



All Articles