Dalam
artikel sebelumnya, saya menjelaskan teknologi dan pendekatan yang kami gunakan saat mengembangkan penembak cepat seluler baru. Karena itu adalah ulasan dan bahkan artikel yang dangkal - hari ini saya akan menggali lebih dalam dan menjelaskan secara rinci mengapa kami memutuskan untuk menulis kerangka kerja ECS kami sendiri dan tidak menggunakan yang sudah ada. Akan ada contoh kode dan bonus kecil di akhir.

Apa itu ECS sebagai contoh
Saya sudah menjelaskan secara singkat apa itu Entity Component System, dan ada artikel tentang Habrรฉ tentang ECS โโ(pada dasarnya, terjemahan artikel - lihat ulasan saya tentang yang paling menarik di bagian akhir artikel, sebagai bonus). Dan hari ini saya akan memberi tahu Anda bagaimana kami menggunakan ECS - menggunakan contoh kode kami.
Diagram di atas menjelaskan esensi dari
Player , komponen-komponennya dan data mereka, dan sistem yang bekerja dengan pemain dan komponen-komponennya. Objek utama dalam diagram adalah pemain:
- dapat bergerak di luar angkasa - Komponen Transform and Movement , MoveSystem ;
- memiliki beberapa kesehatan dan dapat mati - komponen Kesehatan , Kerusakan , KerusakanSistem ;
- setelah kematian muncul di titik respawn - komponen Transform untuk posisi itu, Sistem RespawnS ;
- mungkin kebal - komponen Invincible .
Kami menggambarkan ini dengan kode. Pertama, mari kita dapatkan antarmuka untuk komponen dan sistem. Komponen dapat memiliki metode bantu umum, sistem hanya memiliki satu metode
Jalankan , yang menerima keadaan dunia pada input untuk diproses:
public interface IComponent {
Untuk komponen, kami membuat kelas rintisan yang digunakan oleh pembuat kode kami untuk mengubahnya menjadi kode komponen yang benar-benar digunakan. Mari kita cari beberapa blanko untuk
Health ,
Damage , dan
Invincible (untuk komponen lainnya akan serupa).
[Component] public class Health { [Max(1000)]
Komponen menentukan keadaan dunia, oleh karena itu mereka hanya berisi data, tanpa metode. Pada saat yang sama, tidak ada data dalam
Invincible , digunakan dalam logika sebagai tanda kebal - jika esensi pemain memiliki komponen ini, maka pemain sekarang kebal.
Atribut
Komponen digunakan oleh generator untuk menemukan kelas kosong untuk komponen.
Atribut Max dan
DontSend diperlukan sebagai petunjuk saat membuat cerita bersambung dan mengurangi ukuran keadaan dunia yang ditransmisikan melalui jaringan atau disimpan ke disk. Dalam hal ini, server tidak akan membuat serial bidang
Jumlah dan mengirimkannya melalui jaringan (karena klien tidak menggunakan parameter ini, diperlukan hanya di server). Dan bidang
Hp dapat dikemas dengan baik dalam beberapa bit, mengingat nilai kesehatan maksimum.
Kami juga memiliki kelas
prefab Entitas , tempat kami menambahkan informasi tentang semua komponen yang mungkin dari entitas apa pun, dan generator sudah akan membuat kelas nyata darinya:
public class Entity { public Health Health; public Damage Damage; public Invincible Invincible;
Setelah itu, generator kami akan membuat kode kelas komponen
Kesehatan ,
Kerusakan, dan
Tak Terkalahkan , yang sudah akan digunakan dalam logika permainan:
public sealed class Health : IComponent { public int Hp; public void Reset() { Hp = default(int); }
Seperti yang Anda lihat, data tetap ada di kelas dan metode ditambahkan, misalnya,
Atur Ulang . Diperlukan untuk mengoptimalkan dan menggunakan kembali komponen dalam kumpulan. Metode tambahan lainnya tidak mengandung logika bisnis - saya tidak akan memberikannya untuk singkatnya.
Kelas juga akan dihasilkan untuk keadaan dunia, yang berisi daftar semua komponen dan entitas:
public sealed class GameState {
Dan akhirnya, kode yang dihasilkan untuk
Entity :
public sealed class Entity { public uint Id;
Kelas
Entity pada dasarnya hanya pengenal komponen. Referensi ke objek dunia
GameState hanya digunakan dalam metode tambahan untuk kenyamanan menulis kode logika bisnis. Mengetahui pengenal suatu komponen, kita dapat menggunakannya untuk membuat serial hubungan di antara entitas, mengimplementasikan tautan dalam komponen ke entitas lain. Sebagai contoh, komponen
Kerusakan berisi referensi ke entitas
Korban untuk menentukan siapa yang rusak.
Ini mengakhiri kode yang dihasilkan. Secara umum, kita membutuhkan generator agar tidak menulis metode bantu setiap saat. Kami hanya menggambarkan komponen sebagai data, kemudian generator melakukan semua pekerjaan. Contoh metode pembantu:
- membuat / menghapus entitas;
- tambah / hapus / salin komponen, akses jika ada;
- bandingkan dua negara di dunia;
- cerita bersambung dari keadaan dunia;
- kompresi delta;
- kode halaman web atau jendela Persatuan untuk menampilkan keadaan dunia, entitas, komponen (lihat detail di bawah);
- dan lainnya
Mari kita beralih ke kode sistem. Mereka mendefinisikan logika bisnis. Misalnya, mari kita menulis kode sistem yang menghitung kerusakan pemain:
public sealed class DamageSystem : ISystem { void ISystem.Execute(GameState gs) { foreach (var damage in gs.Damages) { var invincible = damage.Victim.Invincible; if (invincible != null) continue; var health = damage.Victim.Health; if (health == null) continue; health.Hp -= damage.Amount; } } }
Sistem melewati semua komponen
Kerusakan di dunia dan mencari tahu apakah ada komponen
Invincible pada pemain yang berpotensi rusak (
Korban ). Jika dia, pemain kebal, kerusakan tidak bertambah. Selanjutnya, kami mendapatkan komponen
Kesehatan korban dan mengurangi kesehatan pemain dengan jumlah kerusakan.
Pertimbangkan fitur utama dari sistem:
- Suatu sistem biasanya adalah kelas tanpa kewarganegaraan, tidak mengandung data internal apa pun, tidak mencoba menyimpannya di suatu tempat, kecuali data tentang dunia yang ditransmisikan dari luar.
- Sistem biasanya melalui semua komponen dari tipe tertentu dan bekerja dengannya. Mereka biasanya dipanggil oleh tipe komponen ( Damage โ DamageSystem ) atau oleh tindakan yang mereka lakukan ( RespawnSystem ).
- Sistem mengimplementasikan fungsionalitas minimal. Sebagai contoh, jika kita melangkah lebih jauh, setelah DamageSystem dijalankan, RemoveDamageSystem lain akan menghapus semua komponen Kerusakan . Pada centang selanjutnya, ApplyDamageSystem lain yang didasarkan pada pemotretan pemain dapat kembali menggantung komponen Kerusakan dengan kerusakan baru. Dan kemudian PlayerDeathSystem akan memeriksa kesehatan pemain ( Health.Hp ) dan, jika kurang dari atau sama dengan 0, akan menghancurkan semua komponen pemain kecuali Transform dan tambahkan komponen Bendera Mati .
Total, kami mendapatkan kelas-kelas berikut dan hubungan di antara mereka:

Beberapa fakta tentang ECS
ECS memiliki pro dan kontra sebagai pendekatan untuk pengembangan dan cara mewakili dunia permainan, sehingga semua orang memutuskan sendiri apakah akan menggunakannya atau tidak. Mari kita mulai dengan pro:
- Komposisi versus pewarisan berganda. Dalam kasus pewarisan berganda, banyak fungsi yang tidak perlu dapat diwarisi. Dalam kasus ECS, fungsionalitas muncul / menghilang ketika komponen ditambahkan / dihapus.
- Pemisahan logika dan data. Kemampuan untuk mengubah logika (mengubah sistem, menghapus / menambah komponen) tanpa merusak data. Yaitu Anda dapat menonaktifkan grup sistem yang bertanggung jawab atas fungsi tertentu kapan saja, semua yang lain akan terus berfungsi dan ini tidak akan memengaruhi data.
- Siklus permainan disederhanakan. Satu Pembaruan muncul, dan seluruh siklus dibagi ke dalam sistem. Data diproses oleh "aliran" dalam sistem, terlepas dari mesinnya (tidak ada jutaan panggilan Perbarui , seperti dalam Unity).
- Entitas tidak tahu kelas mana yang mempengaruhinya (dan seharusnya tidak tahu).
- Penggunaan memori yang efisien . Itu tergantung pada implementasi ECS. Anda bisa menggunakan kembali objek dan komponen entitas yang dibuat menggunakan kumpulan; Anda dapat menggunakan tipe nilai untuk data dan menyimpannya dalam memori berdampingan ( Lokalitas data ).
- Lebih mudah untuk menguji ketika data dipisahkan dari logika. Terutama ketika Anda menganggap bahwa logika adalah sistem kecil dengan beberapa baris kode.
- Lihat dan edit keadaan dunia secara real time . Karena keadaan dunia hanyalah data, kami menulis alat yang menampilkan di halaman web seluruh negara di pertandingan di server (serta adegan pertandingan dalam 3D). Komponen apa pun dari entitas apa pun dapat dilihat, diubah, dihapus. Hal yang sama dapat dilakukan di editor Unity untuk klien.

Dan sekarang kontra:
- Anda perlu belajar berpikir, mendesain, dan menulis kode secara berbeda . Pikirkan dalam hal entitas, komponen, dan sistem. Banyak pola desain dalam ECS diimplementasikan dengan cara yang sangat berbeda (lihat contoh implementasi pola Negara di salah satu artikel ulasan di akhir).
- Lebih banyak kode . Luar biasa. Di satu sisi, karena fakta bahwa kita memecah logika menjadi sistem kecil, alih-alih menggambarkan semua fungsi dalam satu kelas, ada lebih banyak kelas, tetapi tidak ada lebih banyak kode.
- Urutan sistem disebut memengaruhi operasi seluruh game . Biasanya, sistem saling tergantung satu sama lain, urutan eksekusi diatur oleh daftar dan dieksekusi dalam urutan ini. Sebagai contoh, DamageSystem pertama menganggap kerusakan, kemudian RemoveDamageSystem menghapus komponen Kerusakan . Jika Anda secara tidak sengaja mengubah urutannya, maka semuanya akan bekerja secara berbeda. Secara umum, ini juga berlaku untuk kasus OOP biasa, jika Anda mengubah urutan pemanggilan metode, tetapi dalam ECS lebih mudah untuk membuat kesalahan. Misalnya, jika bagian dari logika berjalan pada klien untuk prediksi, maka urutannya harus sama dengan di server.
- Kita perlu menghubungkan data dan peristiwa logika dengan tampilan . Dalam kasus Unity, kami memiliki MVP:
- Model - GameState dari ECS;
- Lihat - bersama kami, ini adalah kelas MonoBehavior Unity ( Renderer , Teks , dll.) Dan standar secara eksklusif;
- Presenter menggunakan GameState untuk menentukan peristiwa penampilan / hilangnya entitas, komponen, dll., Menciptakan objek Persatuan dari prefab dan mengubahnya sesuai dengan perubahan di negara dunia.
Tahukah Anda bahwa:- ECS bukan hanya tentang lokalitas data . Bagi saya, ini lebih merupakan paradigma pemrograman, sebuah pola, cara lain untuk merancang dunia game - sebut saja apa pun yang Anda suka. Lokalitas data hanyalah sebuah optimasi.
- Unity tidak memiliki ECS! Seringkali Anda bertanya kepada kandidat dalam wawancara tim - apa yang Anda ketahui tentang ECS? Jika Anda belum pernah mendengar, Anda memberi tahu mereka, dan mereka menjawab: "Ah, jadi seperti di Unity, maka saya tahu!". Tapi tidak, tidak seperti di mesin Unity. Di sana, data dan logika digabungkan dalam komponen MonoBehaviour , dan GameObject (jika dibandingkan dengan entitas di ECS) memiliki data tambahan - nama, tempat dalam hierarki, dll. Pengembang persatuan saat ini sedang mengerjakan implementasi ECS yang normal di mesin, dan sejauh ini tampaknya akan baik. Mereka merekrut spesialis di bidang ini - saya harap hasilnya keren.
Kriteria seleksi kami untuk kerangka ECS
Ketika kami memutuskan untuk membuat game di ECS, kami mulai mencari solusi yang siap pakai dan menuliskan persyaratan untuknya berdasarkan pengalaman salah satu pengembang. Dan mereka melukis bagaimana solusi yang ada memenuhi persyaratan kami. Itu setahun yang lalu, saat ini, sesuatu bisa saja berubah. Sebagai solusi, kami mempertimbangkan:
- Entitas
- Artemis C #
- Ash.net
- ECS adalah solusi kami sendiri pada saat kami menyusunnya. Yaitu asumsi dan keinginan kita, apa yang bisa kita lakukan sendiri.
Kami menyusun tabel untuk perbandingan, di mana saya juga memasukkan solusi kami saat ini (menyebutnya sebagai
ECS (sekarang) ):
Warna merah - solusinya tidak mendukung persyaratan kami, oranye - sebagian mendukung, hijau - sepenuhnya mendukung.Bagi kami, analogi operasi untuk mengakses komponen dan mencari entitas dalam ECS adalah operasi dalam database sql. Oleh karena itu, kami menggunakan konsep seperti tabel (tabel), gabung (gabung operasi), indeks (indeks), dll.
Kami akan menjelaskan persyaratan kami dan sejauh mana perpustakaan dan kerangka kerja pihak ketiga sesuai dengan mereka:
- set data terpisah (histori, saat ini, visual, statis) - kemampuan untuk secara terpisah memperoleh dan menyimpan status dunia (misalnya, status saat ini untuk diproses, untuk rendering, riwayat negara, dll.). Semua keputusan yang dipertimbangkan mendukung persyaratan ini .
- ID entitas sebagai bilangan bulat - dukungan untuk mewakili entitas dengan nomor pengenalnya. Hal ini diperlukan untuk transmisi melalui jaringan dan kemampuan untuk menghubungkan entitas dalam sejarah negara. Tidak ada solusi yang dianggap didukung. Misalnya, dalam Entitas, entitas diwakili oleh objek penuh (seperti GameObject di Unity).
- bergabung dengan ID O (N + M) - dukungan untuk pengambilan sampel yang relatif cepat dari dua jenis komponen. Misalnya, ketika Anda perlu mendapatkan semua entitas dengan komponen dari tipe Kerusakan (misalnya, potongan N mereka) dan Kesehatan (potongan M) untuk menghitung dan menyebabkan kerusakan. Ada dukungan penuh di Artemis; di Entitas dan Ash.NET lebih cepat dari O (Nยฒ), tetapi lebih lambat dari O (N + M). Saya tidak ingat penilaiannya sekarang.
- bergabung dengan referensi ID O (N + M) - sama seperti di atas hanya ketika komponen dari satu entitas memiliki tautan ke yang lain, dan yang terakhir perlu mendapatkan komponen lain (dalam contoh kami, komponen Kerusakan pada entitas pembantu mengacu pada entitas pemain. Korban dan dari sana Anda perlu mendapatkan komponen Kesehatan ). Tidak didukung oleh solusi yang dipertimbangkan.
- tidak ada alokasi kueri - tidak ada alokasi memori tambahan saat menanyakan komponen dan entitas dari negara dunia. Dalam Entitas, itu dalam kasus-kasus tertentu, tetapi tidak berarti bagi kami.
- tabel pool - penyimpanan data dunia dalam pool, kemampuan untuk menggunakan kembali memori, alokasi hanya ketika pool kosong. Ada "beberapa" dukungan di Entitas dan Artemis, tidak ada sama sekali di Ash.NET.
- bandingkan dengan ID (tambah, del) - dukungan bawaan untuk peristiwa penciptaan / penghancuran entitas dan komponen oleh ID. Tingkat tampilan (Tampilan) diperlukan untuk menampilkan / menyembunyikan objek, memutar animasi, efek. Tidak didukung oleh solusi yang dipertimbangkan.
- ฮ serialisasi (quantisation, skip) - kompresi delta bawaan untuk membuat serialisasi keadaan dunia (misalnya, untuk mengurangi ukuran data yang dikirim melalui jaringan). Out of the Box tidak didukung dalam solusi apa pun.
- Interpolasi adalah mekanisme interpolasi built-in antara negara-negara dunia. Tidak ada solusi yang didukung.
- reuse type type - kemampuan untuk menggunakan tipe komponen yang pernah ditulis dalam berbagai jenis entitas. Hanya mendukung Entitas .
- urutan sistem eksplisit - kemampuan untuk mengatur sistem pesanan panggilan Anda sendiri. Semua keputusan didukung.
- editor (unity / server) - dukungan untuk melihat dan mengedit entitas secara real time, baik untuk klien maupun untuk server. Entitas hanya mendukung kemampuan untuk melihat dan mengedit entitas dan komponen di editor Unity.
- fast copy / replace - kemampuan untuk dengan murah menyalin / mengganti data. Tidak ada solusi yang didukung.
- komponen sebagai tipe nilai (struct) - komponen sebagai tipe nilai. Pada prinsipnya, saya ingin mencapai kinerja yang baik berdasarkan ini. Tidak ada satu pun sistem yang didukung, kelas komponen ada di mana-mana.
Persyaratan opsional (
tidak ada solusi pada saat itu yang mendukungnya ):
- indeks - mengindeks data seperti dalam database.
- kunci komposit - kunci kompleks untuk akses cepat ke data (seperti dalam database).
- pemeriksaan integritas - kemampuan untuk memverifikasi integritas data dalam keadaan dunia. Berguna untuk debugging.
- kompresi konten-sadar adalah kompresi data terbaik berdasarkan pengetahuan tentang sifat data. Misalnya, jika kita tahu ukuran maksimum peta atau jumlah objek maksimum di dunia.
- jenis / batas sistem - pembatasan jumlah jenis komponen atau sistem. Di Artemis pada waktu itu tidak mungkin membuat lebih dari 32 atau 64 jenis komponen dan sistem .
Seperti dapat dilihat dari tabel, kita sendiri ingin menerapkan semua persyaratan, kecuali yang opsional. Bahkan, saat ini kami
belum melakukan:
- bergabung dengan ID O (N + M) dan bergabung dengan referensi ID O (N + M) - pemilihan untuk dua komponen yang berbeda masih menempati O (Nยฒ) (pada kenyataannya, bersarang untuk loop). Di sisi lain, tidak ada begitu banyak entitas dan komponen untuk pertandingan.
- bandingkan dengan ID (add, del) - tidak diperlukan di level framework. Kami menerapkan ini pada level yang lebih tinggi di MVP.
- salin / ganti cepat dan komponen sebagai tipe nilai (struct) - pada titik tertentu kami menyadari bahwa bekerja dengan struktur tidak akan senyaman dengan kelas, dan menetap di kelas - kami lebih memilih kenyamanan pengembangan daripada kinerja yang lebih baik. Omong-omong, para pengembang Entitas melakukan hal yang sama pada akhirnya .
Pada saat yang sama, kami menyadari salah satu persyaratan yang awalnya opsional menurut kami:
- kompresi konten-sadar - karena itu kami dapat secara signifikan (puluhan kali) mengurangi ukuran paket yang dikirimkan melalui jaringan. Untuk jaringan data seluler, sangat penting untuk menyesuaikan ukuran paket di MTU sehingga tidak "dipecah" menjadi bagian-bagian kecil yang mungkin hilang, masuk dalam urutan yang berbeda, dan kemudian perlu dirakit di bagian-bagian. Misalnya, di Photon, jika ukuran data tidak cocok di pustaka MTU, itu membagi data menjadi paket dan mengirimkannya sebagai dapat diandalkan (dengan pengiriman yang dijamin), bahkan jika Anda mengirimnya sebagai "tidak dapat diandalkan" dari atas. Diuji dengan rasa sakit tangan pertama.
Fitur pengembangan kami di ECS
- Kami di ECS menulis logika bisnis secara eksklusif . Tidak ada pekerjaan dengan sumber daya, pandangan, dll. Karena kode logika ECS secara bersamaan berjalan pada klien di Unity dan di server, kode tersebut harus sebebas mungkin dari tingkat dan modul lain.
- Kami mencoba meminimalkan komponen dan sistem . Biasanya, untuk setiap tugas baru, kami memulai komponen dan sistem baru. Tetapi kadang-kadang terjadi bahwa kita memodifikasi yang lama, menambahkan data baru ke komponen, dan "mengembang" sistem.
- Dalam implementasi ECS kami, Anda tidak dapat menambahkan beberapa komponen dari jenis yang sama ke satu entitas . Karena itu, jika seorang pemain dipukul beberapa kali dalam satu centang (misalnya, beberapa lawan), maka kami biasanya membuat entitas baru untuk setiap kerusakan dan menambahkan komponen Kerusakan padanya.
- Terkadang, presentasi tidak cukup dengan informasi yang ada di GameState . Maka Anda harus menambahkan komponen khusus atau data tambahan yang tidak terlibat dalam logika, tetapi yang dibutuhkan tampilan. Misalnya, bidikan adalah instan di server, satu tick hidup, dan secara visual, itu lebih lama pada klien. Oleh karena itu, untuk klien, shot ditambahkan ke parameter "shot lifetime".
- Kami mengimplementasikan acara / permintaan dengan membuat komponen khusus . Misalnya, jika seorang pemain mati, kami menggantungnya komponen tanpa data Mati , yang merupakan acara untuk sistem lain dan tingkat tampilan bahwa pemain telah meninggal. Atau jika kita perlu menghidupkan kembali pemain di titik lagi, kami membuat entitas terpisah dengan komponen Respawn dengan informasi tambahan tentang siapa yang akan dihidupkan kembali. Sistem RespawnS terpisah pada awal siklus permainan melewati komponen-komponen ini dan sudah menciptakan esensi pemain. Yaitu sebenarnya, entitas pertama adalah permintaan untuk membuat entitas kedua.
- Kami memiliki komponen / entitas "tunggal" khusus . Misalnya, kami memiliki entitas dengan ID = 1, di mana komponen khusus digantung - pengaturan permainan.
Bonus
โ ECS โ . , , , :
- Unity, ECS -- โ ECS . mopsicus , ECS, . : Unity ECS , . . ยซยป ECS Unity. ECS-, : LeoECS , BrokenBricksECS , Svelto.ECS .
- Unity3D ECS Job System โ , ECS Unity. fstleo , Unity ECS, , - , JobSystem.
- Entity System Framework ? โ Ash- ActionScript. , , OOP- ECS-.
- Ash Entity System โ , FSM State ECS โ , .
- Entity-Component-System โ โ ECS C++.