Halo, Habr! Saya mempersembahkan terjemahan wiki dari proyek
Svelto.ECS yang ditulis oleh Sebastiano Mandalร .
Svelto.ECS adalah hasil dari penelitian dan penerapan prinsip SOLID selama bertahun-tahun dalam pengembangan game di Unity. Ini adalah salah satu dari banyak implementasi pola ECS yang tersedia untuk C # dengan berbagai fitur unik yang diperkenalkan untuk mengatasi kekurangan dari pola itu sendiri.
Penampilan pertama
Cara termudah untuk melihat fitur-fitur dasar Svelto.ECS adalah dengan mengunduh
Contoh Vanilla . Jika Anda ingin memastikan kemudahan penggunaannya, saya akan menunjukkan kepada Anda sebuah contoh:
Sayangnya, tidak mungkin untuk dengan cepat memahami teori di balik kode ini, yang mungkin terlihat sederhana tetapi membingungkan pada saat bersamaan. Untuk memahami hal ini, Anda perlu menghabiskan waktu membaca "dinding teks" dan mencoba contoh-contoh di atas.
Pendahuluan
Baru-baru ini, saya telah banyak membahas
Svelto.ECS dengan beberapa programmer yang kurang lebih berpengalaman. Saya mengumpulkan banyak umpan balik dan membuat banyak catatan yang akan saya gunakan sebagai titik awal untuk artikel saya berikutnya, di mana saya akan berbicara lebih banyak tentang teori dan praktik yang baik. Spoiler kecil: Saya menyadari bahwa ketika Anda mulai menggunakan Svelto.ECS, rintangan terbesar adalah
mengubah paradigma pemrograman . Sungguh menakjubkan betapa banyak yang harus saya tulis untuk menjelaskan konsep-konsep baru yang diperkenalkan oleh Svelto.ECS, dibandingkan dengan sejumlah kecil kode yang ditulis untuk mengembangkan kerangka kerja. Bahkan, sementara kerangka itu sendiri sangat sederhana dan ringan, transisi dari OOP dengan penggunaan pewarisan aktif atau komponen Unity biasa ke desain "baru" modular dan longgar digabungkan bahwa Svelto.ECS menawarkan untuk menggunakan mencegah orang beradaptasi dengan kerangka kerja.
Svelto.ECS aktif digunakan dalam
Freejam (catatan penerjemah - Penulis adalah direktur teknis di perusahaan ini). Karena saya selalu dapat menjelaskan kepada teman-teman saya konsep dasar kerangka kerja, mereka membutuhkan waktu lebih sedikit untuk memahami cara bekerja dengannya. Meskipun Svelto.ECS sekuat mungkin, kebiasaan buruk sulit diatasi, sehingga pengguna cenderung menyalahgunakan beberapa fleksibilitas yang memungkinkan mereka untuk menyesuaikan kerangka kerja dengan paradigma "lama" yang membuat mereka nyaman. Hal ini dapat menyebabkan bencana karena kesalahpahaman atau distorsi konsep yang mendasari kerangka logika. Itulah sebabnya saya bermaksud untuk menulis artikel sebanyak mungkin, terutama karena saya yakin bahwa paradigma ECS adalah solusi terbaik saat ini untuk menulis kode yang efektif dan didukung untuk proyek-proyek besar yang berubah dan dikerjakan ulang berkali-kali selama beberapa tahun.
Robocraft dan
Cardlife adalah buktinya.
Saya tidak akan berbicara banyak tentang teori yang mendasari artikel ini. Saya hanya akan mengingatkan Anda mengapa saya menolak untuk menggunakan
wadah IoC dan mulai menggunakan kerangka ECS secara eksklusif: Kontainer IoC adalah alat yang sangat berbahaya jika digunakan tanpa memahami esensi dari inversi kontrol. Seperti yang dapat Anda lihat dari artikel saya sebelumnya, saya membedakan antara inversi kontrol penciptaan (Inversion of Creation Control) dan inversi kontrol aliran (Inversion of Flow Control). Pembalikan kontrol aliran seperti prinsip Hollywood: "Jangan panggil kami, kami akan menghubungi Anda." Ini berarti bahwa ketergantungan yang disuntikkan tidak boleh digunakan secara langsung melalui metode publik, karena dengan melakukannya Anda cukup menggunakan wadah IoC sebagai pengganti untuk bentuk injeksi global lain, seperti singleton. Namun, jika wadah IoC digunakan atas dasar Inversion of Management (IoC), maka pada dasarnya semuanya bermula untuk menggunakan kembali pola "Metode Templat" untuk menerapkan manajer yang hanya digunakan untuk mendaftarkan objek yang mereka kelola. Dalam konteks nyata dari inversi kontrol aliran, manajer selalu bertanggung jawab untuk mengelola entitas. Apakah ini terlihat seperti pola ECS? Tentu saja Berdasarkan alasan ini, saya mengambil pola ECS dan mengembangkan kerangka kerja kaku berdasarkannya, dan penggunaannya sama saja dengan menerapkan paradigma pemrograman baru.
Root Komposisi dan MesinRoot
Kelas utama adalah Komposisi Root aplikasi. Akar komposisi adalah tempat di mana dependensi dibuat dan diimplementasikan (saya berbicara banyak tentang ini di artikel saya). Root komposisi berasal dari sebuah konteks, tetapi sebuah konteks dapat memiliki lebih dari satu root komposisi. Misalnya, Pabrik adalah akar komposisi. Aplikasi mungkin memiliki lebih dari satu konteks, tetapi ini adalah skenario lanjutan, dan dalam contoh ini kami tidak akan mempertimbangkannya.
Sebelum menyelam ke dalam kode, mari kita berkenalan dengan aturan pertama dari bahasa Svelto.ECS. ECS adalah singkatan Sistem Entitas Komponen. Infrastruktur ECS telah dianalisis dengan baik dalam artikel oleh banyak penulis, tetapi sementara konsep dasarnya umum, implementasinya sangat bervariasi. Pertama-tama, tidak ada cara standar untuk menyelesaikan beberapa masalah yang muncul saat menggunakan kode berorientasi ECS. Sehubungan dengan masalah ini saya melakukan sebagian besar upaya saya, tetapi saya akan membicarakannya nanti atau dalam artikel berikut. Teori ini didasarkan pada konsep Essence, Components (entitas) dan Systems. Walaupun saya mengerti mengapa kata System secara historis digunakan, sejak awal saya tidak menemukan itu cukup intuitif untuk tujuan ini, jadi saya menggunakan Engine sebagai sinonim untuk System, dan Anda, tergantung pada preferensi Anda, dapat menggunakan salah satu dari istilah ini.
Kelas EnginesRoot adalah inti dari Svelto.ECS. Dengan bantuannya, Anda dapat mendaftarkan mesin dan merancang semua esensi permainan. Membuat mesin secara dinamis tidak masuk akal, jadi mereka semua harus ditambahkan ke instance EnginesRoot dari akar yang sama dari komposisi di mana ia dibuat. Untuk alasan yang serupa, mesin virtual EnginesRoot tidak boleh dipasang, dan mesin tidak boleh dihapus setelah ditambahkan.
Untuk membuat dan menerapkan dependensi, kita memerlukan setidaknya satu root komposisi. Ya, dalam satu aplikasi lebih dari satu EnginesRoot mungkin ada, tetapi kami tidak akan menyinggung ini dalam artikel saat ini, yang saya coba sederhanakan sebanyak mungkin. Inilah yang terlihat seperti akar komposisi dengan pembuatan mesin dan injeksi ketergantungan:
void SetupEnginesAndEntities() {
Kode ini dari contoh Survival, yang sekarang dikomentari dan mematuhi hampir semua aturan praktik baik yang saya usulkan untuk diterapkan, termasuk penggunaan platform-independen dan logika mesin yang diuji. Komentar akan membantu Anda memahami sebagian besar dari mereka, tetapi proyek sebesar ini mungkin sulit dipahami jika Anda baru mengenal Svelto.
Entitas
Langkah pertama setelah membuat root kosong komposisi dan turunan dari kelas EnginesRoot adalah mengidentifikasi objek yang ingin Anda kerjakan terlebih dahulu. Adalah logis untuk memulai dengan Entity Player. Inti dari Svelto.ECS tidak boleh disamakan dengan Obyek Game Persatuan (GameObject). Jika Anda membaca artikel lain yang terkait dengan ECS, Anda dapat melihat bahwa di banyak dari mereka, entitas sering digambarkan sebagai indeks. Ini mungkin cara terburuk untuk memperkenalkan konsep ECS. Meskipun berlaku untuk Svelto.ECS, ini tersembunyi di dalamnya. Saya ingin pengguna Svelto.ECS untuk mewakili, menjelaskan, dan mengidentifikasi setiap entitas dalam hal bahasa Domain Desain Game. Entitas dalam kode harus objek yang dijelaskan dalam dokumen desain game. Segala bentuk definisi entitas lainnya akan mengarah pada cara yang jauh mengadaptasi pandangan lama Anda dengan prinsip-prinsip Svelto.ECS. Ikuti aturan mendasar ini dan Anda tidak akan salah. Kelas entitas itu sendiri tidak ada dalam kode, tetapi Anda tetap tidak harus mendefinisikannya secara abstrak.
Mesin
Langkah selanjutnya adalah berpikir tentang perilaku apa yang diminta Entitas. Setiap perilaku selalu dimodelkan di dalam Engine, Anda tidak dapat menambahkan logika ke kelas lain di dalam aplikasi Svelto.ECS. Kita bisa mulai dengan memindahkan karakter pemain dan menentukan kelas
PlayerMovementEngine . Nama mesin harus difokuskan dengan sangat sempit, karena semakin spesifik, semakin besar kemungkinan mesin akan mengikuti Aturan Tanggung Jawab Tunggal. Penamaan kelas yang tepat di Svelto.ECS sangat mendasar. Dan tujuannya tidak hanya untuk menunjukkan niat Anda dengan jelas, tetapi juga untuk membantu Anda "melihat" mereka sendiri.
Untuk alasan yang sama, penting bahwa mesin Anda berada dalam ruang nama yang sangat khusus. Jika Anda menentukan ruang nama sesuai dengan struktur folder, beradaptasi dengan konsep Svelto.ECS. Penggunaan ruang nama tertentu membantu mendeteksi kesalahan desain saat entitas digunakan di dalam ruang nama yang tidak kompatibel. Misalnya, tidak diasumsikan bahwa objek musuh akan digunakan di dalam ruang nama pemain, kecuali tujuannya adalah untuk melanggar aturan yang terkait dengan modularitas dan kopling objek yang lemah. Idenya adalah bahwa objek namespace tertentu hanya dapat digunakan di dalamnya atau namespace induk. Menggunakan Svelto.ECS jauh lebih sulit untuk mengubah kode Anda menjadi spaghetti, di mana dependensi disuntikkan ke kanan dan kiri, dan aturan ini akan membantu Anda meningkatkan standar kualitas kode bahkan lebih tinggi ketika dependensi diabstraksi dengan benar di antara kelas.
Dalam Svelto.ECS, abstraksi bergerak maju beberapa baris, tetapi ECS pada dasarnya membantu untuk mengabstraksi data dari logika yang seharusnya memproses data. Entitas ditentukan oleh data mereka, bukan perilaku mereka. Dalam hal ini, engine adalah tempat di mana Anda dapat menempatkan perilaku gabungan entitas identik sehingga engine selalu dapat bekerja dengan sekumpulan entitas.
Svelto.ECS dan paradigma ECS memungkinkan encoder untuk mencapai salah satu grails suci pemrograman murni, yang merupakan enkapsulasi logika yang ideal. Mesin tidak boleh memiliki fungsi publik. Satu-satunya fungsi publik yang harus ada adalah fungsi yang diperlukan untuk mengimplementasikan antarmuka kerangka kerja. Ini mengarah pada melupakan injeksi dependensi dan membantu menghindari kode buruk yang terjadi ketika menggunakan injeksi dependensi tanpa inversi kontrol. Mesin harus TIDAK PERNAH tertanam di mesin lain atau jenis kelas apa pun. Jika Anda berpikir ingin menerapkan mesin, Anda cukup membuat kesalahan mendasar dalam desain kode.
Dibandingkan dengan Unity MonoBehaviour, engine sudah menunjukkan keunggulan besar pertama, yaitu kemampuan untuk mengakses semua status entitas jenis ini dari area kode yang sama. Ini berarti bahwa kode dapat dengan mudah menggunakan keadaan semua objek langsung dari tempat yang sama di mana logika dari objek umum akan dieksekusi. Selain itu, mesin individual dapat memproses objek yang sama sehingga mesin dapat mengubah keadaan objek, sementara mesin lain dapat membacanya, secara efektif menggunakan dua mesin untuk komunikasi melalui data entitas yang sama. Contohnya dapat dilihat dengan melihat
mesin PlayerGunShootingEngine dan
PlayerGunShootingFxsEngine . Dalam hal ini, dua mesin berada di namespace yang sama, sehingga mereka dapat berbagi data entitas yang sama.
PlayerGunShootingEngine menentukan apakah seorang pemain (musuh) telah rusak, dan menulis nilai
lastTargetPosition dari komponen
IGunAttributesComponent (yang merupakan komponen
PlayerGunEntity ).
PlayerGunShootFxsEngine memproses efek grafis senjata dan membaca posisi target yang dipilih oleh pemain. Ini adalah contoh interaksi antara mesin melalui polling data. Nanti dalam artikel ini saya akan menunjukkan bagaimana memungkinkan mekanisme untuk berkomunikasi di antara mereka dengan
mendorong data (Data mendorong) atau
data mengikat (Data binding) . Logikanya, mesin tidak boleh menyimpan status.
Mesin tidak perlu tahu cara berinteraksi dengan mesin lain. Komunikasi eksternal terjadi melalui abstraksi, dan Svelto.ECS memecahkan hubungan antara mesin dalam tiga cara resmi yang berbeda, tetapi saya akan membicarakan hal ini nanti. Mesin terbaik adalah yang tidak memerlukan komunikasi eksternal. Mesin ini mencerminkan perilaku yang dienkapsulasi dengan baik dan biasanya bekerja melalui loop logis. Loop selalu dimodelkan menggunakan tugas Svelto.Task di dalam aplikasi Svelto.ECS. Karena gerakan pemain perlu diperbarui setiap kutu fisik, adalah wajar untuk membuat tugas untuk dilakukan pada setiap kutu fisik. Svelto.Tasks memungkinkan Anda untuk menjalankan setiap jenis
IEnumerator pada beberapa jenis penjadwal. Dalam hal ini, kami memutuskan untuk membuat tugas di
PhysicScheduler , yang memungkinkan Anda untuk memperbarui posisi pemain:
public PlayerMovementEngine(IRayCaster raycaster, ITime time) { _rayCaster = raycaster; _time = time; _taskRoutine = TaskRunner.Instance.AllocateNewTaskRoutine() .SetEnumerator(PhysicsTick()).SetScheduler(StandardSchedulers.physicScheduler); } protected override void Add(PlayerEntityView entityView) { _taskRoutine.Start(); } protected override void Remove(PlayerEntityView entityView) { _taskRoutine.Stop(); } IEnumerator PhysicsTick() {
Tugas Svelto.Tasks dapat dilakukan secara langsung atau melalui objek
ITaskRoutine . Saya tidak akan banyak bicara tentang Svelto.Tasks di sini, karena saya menulis artikel lain untuk itu. Alasan saya memutuskan untuk menggunakan tugas rutin alih-alih meluncurkan implementasi IEnumerator secara langsung adalah sangat diskresioner. Saya ingin menunjukkan bahwa Anda dapat memulai siklus ketika objek pemain ditambahkan ke mesin dan menghentikannya saat dihapus.
Namun, untuk ini Anda perlu tahu kapan suatu objek ditambahkan dan dihapus.Svelto.ECS memperkenalkan menambah dan menghapus callback untuk mengetahui kapan entitas tertentu ditambahkan atau dihapus. Ini adalah sesuatu yang unik di Svelto.ECS, tetapi pendekatan ini harus digunakan dengan bijak. Saya sering melihat bahwa callback ini disalahgunakan, seperti dalam banyak kasus mereka cukup untuk meminta entitas. Bahkan memiliki referensi entitas sebagai bidang mesin harus dianggap lebih sebagai pengecualian daripada aturan.Hanya ketika panggilan balik ini digunakan, mesin harus diwarisi baik dari SingleEntityViewEngine atau dari MultiEntitiesViewEngine <EntityView1, ..., EntityViewN>. Sekali lagi, penggunaan data ini harus jarang, dan mereka sama sekali tidak bermaksud melaporkan objek yang akan diproses oleh mesin.Mesin paling sering mengimplementasikan antarmuka IQueryingEntityViewEngine . Ini memungkinkan Anda untuk mengakses dan mengekstrak data dari basis data entitas. Ingatlah bahwa Anda selalu dapat meminta objek dari dalam mesin, tetapi saat Anda meminta entitas yang tidak kompatibel dengan namespace di mana mesin berada, Anda harus memahami bahwa Anda sudah melakukan sesuatu yang salah. Mesin tidak boleh berasumsi bahwa entitas dapat diakses, dan harus bekerja pada sekumpulan objek. Seharusnya tidak diasumsikan bahwa akan selalu ada hanya satu pemain dalam permainan, seperti yang saya lakukan dalam contoh kode. Di EnemyMovementEngine ada pendekatan yang sangat umum tentang cara meminta objek: public void Ready() { Tick().Run(); } IEnumerator Tick() { while (true) { var enemyTargetEntityViews = entityViewsDB.QueryEntityViews<EnemyTargetEntityView>(); if (enemyTargetEntityViews.Count > 0) { var targetEntityView = enemyTargetEntityViews[0]; var enemies = entityViewsDB.QueryEntityViews<EnemyEntityView>(); for (var i = 0; i < enemies.Count; i++) { var component = enemies[i].movementComponent; component.navMeshDestination = targetEntityView.targetPositionComponent.position; } } yield return null; } }
Dalam hal ini, siklus mesin utama dimulai langsung pada penjadwal yang telah ditentukan. Centang () .Jalankan ()menunjukkan cara terpendek untuk memulai IEnumerator dengan Svelto.Tasks. IEnumerator akan terus menghasilkan ke frame berikutnya sampai setidaknya satu target Musuh ditemukan. Karena kita tahu bahwa akan selalu ada hanya satu tujuan (asumsi buruk lain), saya memilih yang pertama tersedia. Sementara tujuan dari Target Musuh hanya bisa satu (meskipun mungkin ada lebih banyak!), Ada banyak musuh, dan mesin tetap menjaga logika pergerakan untuk semua orang. Dalam hal ini, saya curang, karena saya benar-benar menggunakan Unity Nav Mesh System, jadi yang harus saya lakukan hanyalah mengatur tujuan ke NavMesh. Jujur, saya tidak pernah menggunakan kode Unity NavMesh, jadi saya bahkan tidak yakin cara kerjanya, kode ini hanya diwarisi dari demo Survival asli.Perhatikan bahwa komponen tidak pernah secara langsung menyediakan dependensi Navmesh Unity. Komponen Entity, seperti yang akan saya bahas nanti, harus selalu memaparkan tipe nilai. Dalam hal ini, aturan ini juga memungkinkan Anda untuk menjaga kode di bawah kendali, karena tipe nilai bidang navMeshDestination dapat diimplementasikan kemudian dengan menggunakan Unity Nav Mesh.Untuk menyelesaikan paragraf di mesin, perhatikan bahwa tidak ada yang namanya mesin terlalu kecil. Karena itu, jangan takut untuk menulis mesin yang berisi beberapa baris kode, karena Anda tidak dapat menulis logika di tempat lain, dan Anda perlu mesin Anda untuk mengikuti aturan tanggung jawab yang seragam.Representasi entitas
Sebelum itu, kami memperkenalkan konsep Mesin dan definisi abstrak dari Essence, mari sekarang mendefinisikan apa Representasi esensi itu. Saya harus mengakui bahwa dari 5 konsep tempat Svelto.ECS dibangun, Entity Views mungkin adalah yang paling membingungkan. Sebelumnya bernama Node (nama yang diambil dari kerangka Ash ECS ), saya menyadari bahwa nama "Node" tidak berarti apa-apa. EntityView mungkin juga menyesatkan karena programmer biasanya terkait dengan representasi konsep yang berasal dari template model view controller(Model View Controller), bagaimanapun, Svelto.ECS menggunakan View, karena EntityView adalah bagaimana Engine melihat Entity. Saya suka menggambarkannya seperti ini karena tampaknya paling alami, tetapi saya juga bisa menyebutnya EntityMap, karena EntityView menampilkan komponen entitas yang harus diakses oleh mesin. Skema konsep Svelto.ECS ini seharusnya sedikit membantu:
Saya sarankan mulai dengan Engine, dan sekarang kita berada di sisi kanan skema ini. Setiap mesin memiliki set sendiri EntityViews. Mesin dapat menggunakan kembali EntityViews yang kompatibel dengan namespace, tetapi paling sering Engine mendefinisikan EntityViews-nya. Mesin tidak peduli apakah entitas Player benar-benar didefinisikan, ia menyatakan fakta bahwa ia membutuhkan PlayerEntityViewuntuk bekerja. Menulis kode tergantung pada kebutuhan Mesin, Anda tidak harus membuat entitas dan bidangnya sebelum Anda memahami cara menggunakannya. Dalam skenario yang lebih kompleks, nama EntityView bisa lebih spesifik. Sebagai contoh, jika kita harus menulis mesin yang rumit untuk menangani logika pemain dan membuat grafik pemain (atau animasi, dll.), Kita bisa memiliki PlayerPhysicEngine dengan PlayerPhysicEntityView , serta PlayerGraphicEngine dengan PlayerGraphicEntityView atau PlayerAnimationEngine dengan PlayerAnimationEntityView . Nama yang lebih spesifik dapat digunakan, seperti PlayerPhysicMovementEngine atau PlayerPhysicJumpEngine (dll.).Komponen
Kami menyadari bahwa mesin memodelkan perilaku untuk sekumpulan data entitas, dan kami memahami bahwa engine tidak menggunakan entitas secara langsung, tetapi menggunakan komponen entitas melalui representasi entitas. Kami menyadari bahwa EntityView adalah kelas yang dapat berisi HANYA komponen publik entitas. Saya juga mengisyaratkan bahwa komponen entitas selalu merupakan antarmuka, jadi mari kita berikan definisi yang lebih baik:Entitas adalah kumpulan data, dan komponen entitas adalah cara untuk mengakses data itu. Jika Anda belum memperhatikan hal ini, mendefinisikan komponen entitas sebagai antarmuka adalah fitur lain yang cukup unik dari Svelto.ECS. Biasanya, komponen dalam kerangka kerja lain adalah objek. Menggunakan antarmuka sebagai gantinya dapat secara signifikan mengurangi kode. Jika Anda mengikuti prinsipnya" Prinsip Segregasi Antarmuka", setelah menulis antarmuka komponen kecil, bahkan dengan masing-masing properti, Anda akan melihat bahwa Anda telah mulai menggunakan kembali antarmuka komponen dalam entitas yang berbeda. Dalam contoh kami, ITransformComponent digunakan kembali dalam banyak representasi entitas. Menggunakan komponen sebagai antarmuka juga memungkinkan mereka untuk mengimplementasikan objek yang sama, yang dalam banyak kasus menyederhanakan hubungan antara entitas yang melihat entitas yang sama menggunakan representasi entitas yang berbeda (atau sama, jika mungkin).Oleh karena itu, di Svelto.ECS, komponen entitas selalu merupakan antarmuka, dan antarmuka ini hanya digunakan melalui bidang EntityView di dalam Engine. Antarmuka komponen entitas kemudian diimplementasikan oleh yang disebutยซยป. , .Komponen harus selalu menyimpan tipe yang bermakna, dan bidang selalu properti. Pengecualian hanya dapat dibuat untuk menulis setter dan getter sebagai metode untuk menggunakan kata kunci ref ketika optimasi diperlukan. Ini tidak berarti bahwa kode tersebut berorientasi data, tetapi akan memungkinkan Anda membuat kode untuk pengujian, karena logika mesin tidak boleh memproses tautan ke dependensi eksternal. Selain itu, ini mencegah coders dari kecurangan pada framework dan menggunakan fungsi publik (yang mungkin termasuk logika!) Objek acak. Satu-satunya alasan Anda bisa merasakan kebutuhan untuk menggunakan tautan di dalam antarmuka komponen entitas adalah untuk berurusan dengan dependensi pihak ketiga, seperti objek Unity. Namun, contoh Kelangsungan Hidup menunjukkan bagaimana menangani ini,meninggalkan kode tes mesin tanpa harus khawatir tentang dependensi Unity.Di sinilah Penjelas Entitas datang untuk menyelamatkan untuk mengumpulkan semuanya. Kita tahu bahwa engine dapat mengakses data Entity melalui Komponen yang disimpan di Entity Views. Kita tahu bahwa engine adalah kelas, EntityView adalah kelas yang hanya berisi entitas Komponen dan Komponen adalah antarmuka. Meskipun saya memberikan definisi abstrak dari Essence, kami belum melihat satu pun kelas yang benar-benar mewakili Essence. Ini sesuai dengan konsep objek yang merupakan pengidentifikasi dalam sistem ECS modern. Namun, tanpa definisi Entitas yang benar, ini akan memaksa pembuat kode untuk mengidentifikasi Entitas dengan Representasi entitas, yang akan menjadi bencana besar. Representasi entitas adalah cara di mana beberapa Mesin dapat melihat Entitas yang sama,tetapi mereka bukan Entitas. Entitas itu sendiri harus selalu dianggap sebagai set data yang didefinisikan melalui Komponen entitas, tetapi bahkan ini adalah definisi yang lemah. Contoh EntityDescriptor memungkinkan encoder untuk menentukan Entitasnya dengan benar, terlepas dari mesin yang akan memprosesnya. Oleh karena itu, dalam hal Entity Player, kita perluPlayerEntityDescriptor . Kelas ini akan digunakan untuk membuat Entitas, dan meskipun apa yang sebenarnya dilakukannya adalah sesuatu yang sama sekali berbeda, fakta bahwa pengguna dapat menulis BuildEntity <PlayerEntityDescriptor> () membantu dengan mudah memvisualisasikan Entitas untuk membangun dan mengomunikasikan niat kepada orang lain. encoders.Namun, apa yang sebenarnya dilakukan EntityDescriptor adalah membuat daftar EntityViews !!! Pada tahap awal pengembangan kerangka kerja, saya mengizinkan pembuat kode untuk membuat daftar EntityViews ini secara manual, yang menyebabkan kode yang sangat jelek karena tidak bisa lagi memvisualisasikan apa yang sebenarnya terjadi.Beginilah bentuk PlayerEntityDescriptor : using Svelto.ECS.Example.Survive.Camera; using Svelto.ECS.Example.Survive.HUD; using Svelto.ECS.Example.Survive.Enemies; using Svelto.ECS.Example.Survive.Sound; namespace Svelto.ECS.Example.Survive.Player { public class PlayerEntityDescriptor : GenericEntityDescriptor<HUDDamageEntityView, PlayerEntityView, EnemyTargetEntityView, DamageSoundEntityView, HealthEntityView, CameraTargetEntityView> { } }
Penjelas entitas (dan Pelaksana) adalah satu-satunya kelas yang dapat menggunakan pengidentifikasi dari beberapa ruang nama. Dalam hal ini, PlayerEntityDescriptor menentukan daftar EntityViews untuk instantiate dan menyuntikkan ke mesin ketika membuat PlayerEntity.Pemegang EntityDescriptor
EntityDescriptorHolder adalah ekstensi untuk Unity dan hanya boleh digunakan dalam kasus-kasus tertentu. Yang paling umum adalah penciptaan semacam polimorfisme yang menyimpan informasi tentang Entitas untuk membangun GameObject Persatuan. Dengan demikian, kode yang sama dapat digunakan untuk membuat beberapa jenis Entitas. Sebagai contoh, di Robocraft, kami menggunakan pabrik kubus tunggal yang membangun semua kubus yang membentuk mesin. Jenis kubus untuk rakitan disimpan dalam cetakan kubus itu sendiri. Ini bagus asalkan implementernya sama antara kubus atau ditemukan di GameObject sebagai MonoBehaviour. Membuat Entitas secara langsung lebih disukai, jadi gunakan EntityDescriptorHolders hanya jika Anda benar memahami prinsip-prinsip Svelto.ECS, jika tidak ada risiko penyalahgunaan. Fungsi dari contoh ini menunjukkan cara menggunakan kelas: void BuildEntitiesFromScene(UnityContext contextHolder) {
Perhatikan bahwa dalam contoh ini saya menggunakan fungsi BuildEntity non-generik . Saya akan menjelaskan ini. Dalam hal ini, pelaksana adalah kelas MonoBehaviour yang dilampirkan ke GameObject. Ini bukan praktik yang baik. Saya seharusnya menghapus kode ini dari contoh, tetapi meninggalkan untuk menunjukkan kepada Anda kasus khusus ini. Implementer, seperti yang akan kita lihat nanti, harus kelas MonoBehaviour hanya jika diperlukan!Peniru
Sebelum membuat esensi kami, mari kita mendefinisikan konsep terakhir di Svelto.ECS, yang merupakan Impaler . Seperti yang kita ketahui, Komponen Entitas selalu merupakan antarmuka, dan antarmuka C # harus diimplementasikan. Objek yang mengimplementasikan antarmuka ini disebut "implementor". Pelaksana memiliki beberapa karakteristik penting:- Kemampuan untuk melepaskan jumlah objek yang akan dirakit dari jumlah komponen entitas yang diperlukan untuk menentukan data entitas.
- Kemampuan untuk bertukar data antara Komponen yang berbeda, karena Komponen memberikan data melalui properti, properti yang berbeda dari suatu Komponen dapat mengembalikan bidang implementasi yang sama.
- Kemampuan untuk membuat rintisan komponen antarmuka untuk komponen entitas. Ini penting agar kode mesin tetap diuji.
- Svelto.ECS (third party) . . Unity, , , Monobehaviour . , Unity, OnTriggerEnter / OnTriggerExit , Unity. , . :
public class EnemyTriggerImplementor : MonoBehaviour, IImplementor, IEnemyTriggerComponent, IEnemyTargetComponent { public event Action<int, int, bool> entityInRange; bool IEnemyTriggerComponent.targetInRange { set { _targetInRange = value; } } bool IEnemyTargetComponent.targetInRange { get { return _targetInRange; } } void OnTriggerEnter(Collider other) { if (entityInRange != null) entityInRange(other.gameObject.GetInstanceID(), gameObject.GetInstanceID(), true); } void OnTriggerExit(Collider other) { if (entityInRange != null) entityInRange(other.gameObject.GetInstanceID(), gameObject.GetInstanceID(), false); } bool _targetInRange; }
, , . , .Penciptaan Entitas
Misalkan kita membuat Mesin kami , menambahkannya ke EnginesRoot , menciptakan Tampilan Entitas mereka , yang membutuhkan Komponen sebagai antarmuka yang akan diimplementasikan di dalam Pelaksana . Inilah saatnya untuk menciptakan Essence pertama kami. Entitas selalu dibuat melalui turunan dari Entity Factory yang dibuat oleh EnginesRoot melalui fungsi GenerateEntityFactory . Tidak seperti instance EnginesRoot, instance IEntityFactory dapat digunakan dan ditransfer. Objek dapat dibangun di dalam root komposisi atau secara dinamis di dalam pabrik, jadi untuk kasus yang terakhir, Anda harus melewati IEntityFactory melalui parameter.IEntityFactory hadir dengan beberapa fitur serupa. Dalam artikel ini saya akan melewatkan fungsi penjelasan PreallocateEntitySlots dan BuildMetaEntity , untuk fokus pada fungsi yang paling umum digunakan BuildEntity dan BuildEntityInGroup .Yang terbaik adalah selalu menggunakan BuildEntityInGroup , tetapi untuk contoh Survival Anda tidak perlu, jadi mari kita lihat bagaimana BuildEntity yang biasa digunakan dalam contoh: IEnumerator IntervaledTick() {
Ingatlah untuk membaca semua komentar dalam contoh ini, mereka akan membantu Anda lebih memahami konsep Svelto.ECS. Karena kesederhanaan contohnya, saya tidak menggunakan BuildEntityInGroup , yang digunakan dalam proyek yang lebih kompleks. Dalam Robocraft, setiap mesin yang memproses logika kubus fungsional memproses logika SEMUA kubus fungsional jenis khusus ini dalam game. Namun, seringkali perlu untuk mengetahui kendaraan milik kubus mana, sehingga menggunakan grup untuk setiap mesin akan membantu memecah kubus dari jenis yang sama menjadi mesin, di mana ID mesin adalah ID grup. Ini memungkinkan kita untuk mengimplementasikan hal-hal keren, seperti menjalankan satu tugas Svelto.Tasks pada mesin di dalam mesin yang sama, yang dapat bekerja secara paralel menggunakan multithreading.Sepotong kode ini menunjukkan satu masalah penting yang dapat sayabahas lebih detail dalam artikel berikut ... dari komentar (jika Anda belum membacanya): Jangan pernah membuat MonoBehaviour Imprementors hanya untuk penyimpanan data. Data harus selalu diambil melalui Lapisan Layanan terlepas dari sumber data. Manfaatnya banyak, termasuk fakta bahwa untuk mengubah sumber data Anda hanya perlu mengubah kode layanan. Dalam contoh sederhana ini, saya tidak menggunakan lapisan Layanan, tetapi secara umum idenya jelas. Perhatikan juga bahwa saya hanya mengunggah data satu kali untuk setiap peluncuran aplikasi, di luar loop utama. Anda selalu dapat menggunakan trik ini jika data yang Anda butuhkan tidak pernah berubah.Awalnya, saya membaca data langsung dari MonoBehaviour, seperti yang dilakukan oleh pembuat kode malas. Ini membuat saya membuat implementator serializer baca-saja MonoBehaviore. Ini dapat diterima jika kami tidak ingin mengabstraksi sumber data, tetapi jauh lebih baik untuk membuat serialisasi informasi menjadi file json dan membacanya berdasarkan permintaan ke layanan daripada membaca data ini dari Komponen Entitas.Komunikasi di Svelto.ECS
Satu masalah yang solusinya belum pernah distandarisasi oleh implementasi ECS adalah komunikasi antar sistem. Ini adalah tempat lain di mana saya banyak berpikir, dan Svelto.ECS menyelesaikannya dengan dua cara baru. Cara ketiga adalah menggunakan pola Observer / Observed standar, dapat diterima dalam kasus yang sangat spesifik dan spesifik.DispatchOnSet / DispatchOnChange
Sebelumnya kami telah melihat cara mengizinkan Mesin untuk bertukar data melalui Komponen Entitas menggunakan Polling Data. DispatchOnSet dan DispatchOnChange adalah satu-satunya referensi (tipe tidak signifikan) yang dapat dikembalikan oleh properti Entity Components, tetapi jenis parameter generik T harus merupakan tipe yang bermakna. Nama-nama fungsi terdengar seperti dispatcher acara, tetapi sebaliknya mereka harus dianggap sebagai metode mendorong data, yang bertentangan dengan jajak pendapat data, yang agak mirip dengan pengikatan data. Itu saja, kadang-kadang data polling tidak nyaman, kami tidak ingin polling variabel setiap frame ketika kami tahu bahwa data jarang berubah. DispatchOnSet dan DispatchOnChangetidak dapat dimulai tanpa mengubah data, ini memungkinkan kami untuk menganggapnya sebagai mekanisme pengikatan data dan bukan peristiwa biasa. Juga tidak ada fungsi peluncuran untuk dipanggil, sebagai gantinya, nilai data yang dimiliki oleh kelas-kelas ini harus diatur atau diubah. Tidak ada contoh yang bagus dalam kode Survival, tetapi Anda dapat melihat bagaimana bidang targetHit Boolean dari IGunHitTargetComponent bekerja . Perbedaan antara DispatchOnSet dan DispatchOnChange adalah bahwa yang terakhir memecat acara hanya ketika data benar-benar berubah, dan yang sebelumnya selalu.Sequencer
Mesin Ideal sepenuhnya dienkapsulasi, dan Anda dapat menulis logika mesin ini sebagai urutan instruksi menggunakan Svelto.Tasks dan IEnumerators. Namun, ini tidak selalu memungkinkan, karena dalam beberapa kasus Mesin harus mengirim acara ke Mesin lain. Ini biasanya dilakukan melalui data Entity, terutama menggunakan DispatchOnSet dan DispatchOnChangeNamun, seperti dalam kasus Entitas โrusakโ dalam contoh, serangkaian Mesin independen dan tidak terkait bertindak di atasnya. Dalam kasus lain, Anda ingin urutan menjadi ketat dalam urutan mesin yang dipanggil, seperti dalam contoh di mana saya ingin kematian selalu terjadi untuk yang terakhir. Dalam hal ini, urutannya tidak hanya sangat mudah digunakan, tetapi juga sangat nyaman! Urutan refactoring sangat sederhana. Oleh karena itu, gunakan IEnumerator Svelto Tasks untuk mesin "vertikal" dan urutan untuk logika "horizontal" di antara mesin.Pengamat / Diamati
Saya meninggalkan kesempatan untuk menggunakan pola ini khusus untuk kasus-kasus di mana kode warisan atau kode yang tidak menggunakan Svelto.ECS harus berinteraksi dengan mesin Svelto.ECS. Untuk kasus lain, itu harus digunakan dengan sangat hati-hati, karena ada kemungkinan penyalahgunaan pola, karena itu akrab bagi kebanyakan coders yang baru mengenal Svelto.ECS, dan Sequencer biasanya merupakan pilihan terbaik.