Implementasi Sistem Komponen Entitas Sederhana

Halo semuanya!

Aliran keempat "Pengembang C ++" dimulai di sini, salah satu kursus paling aktif di negara kami, dinilai dari pertemuan nyata, di mana tidak hanya "pejuang" datang untuk berbicara dengan Dima Shebordaev :) Secara umum, kursus telah berkembang menjadi salah satu yang terbesar di negara kami, tetap tidak berubah bahwa Dima melakukan pelajaran terbuka dan kami memilih bahan yang menarik sebelum dimulainya kursus.

Ayo pergi!

Entri


Entity Component System (ECS, "entitas-komponen-sistem") sekarang berada di puncak popularitas sebagai alternatif arsitektur yang menekankan prinsip Komposisi daripada pewarisan. Dalam artikel ini, saya tidak akan masuk ke detail konsep, karena sudah ada sumber daya yang cukup tentang topik ini. Ada banyak cara untuk mengimplementasikan ECS, dan, tetapi saya, paling sering, memilih yang agak rumit yang dapat membingungkan pemula dan membutuhkan banyak waktu.

Dalam posting ini, saya akan menjelaskan cara yang sangat sederhana untuk mengimplementasikan ECS, versi fungsional yang hampir tidak memerlukan kode, tetapi sepenuhnya mengikuti konsep.



ECS


Berbicara tentang ECS, orang sering memaksudkan hal-hal yang berbeda. Ketika saya berbicara tentang ECS, maksud saya adalah sistem yang memungkinkan Anda untuk mendefinisikan entitas yang memiliki nol atau lebih komponen data murni. Komponen-komponen ini diproses secara selektif oleh sistem logika murni. Sebagai contoh, posisi, kecepatan, hitbox, dan kesehatan suatu komponen terkait dengan entitas E. Mereka hanya menyimpan data dalam diri mereka sendiri. Misalnya, komponen kesehatan dapat menyimpan dua bilangan bulat: satu untuk kesehatan saat ini dan satu untuk maksimum. Suatu sistem dapat menjadi sistem regenerasi kesehatan yang menemukan semua instance komponen kesehatan dan meningkatkannya dengan 1 setiap 120 frame.

Implementasi C ++ yang khas


Ada banyak perpustakaan yang menawarkan implementasi ECS. Biasanya, mereka memasukkan satu atau lebih item dari daftar:

  • Warisan Komponen dasar / Sistem kelas GravitySystem : public ecs::System ;
  • Penggunaan templat secara aktif;
  • Baik itu, dan lainnya dalam beberapa tampilan CRTP ;
  • Kelas EntityManager , yang mengontrol pembuatan / penyimpanan entitas secara implisit.

Beberapa contoh google cepat:


Semua metode ini memiliki hak untuk hidup, tetapi ada beberapa kelemahan di dalamnya. Cara mereka memproses data secara buram berarti bahwa akan sulit untuk memahami apa yang terjadi di dalam dan jika perlambatan kinerja telah terjadi. Ini juga berarti bahwa Anda harus mempelajari seluruh lapisan abstraksi dan memastikan bahwa itu cocok dengan kode yang ada. Jangan lupa tentang bug tersembunyi, yang mungkin banyak tersembunyi dalam jumlah kode yang harus Anda debug.

Pendekatan berbasis template dapat sangat memengaruhi waktu kompilasi dan seberapa sering Anda harus membangun kembali build. Sedangkan konsep berbasis pewarisan dapat menurunkan kinerja.

Alasan utama saya pikir pendekatan ini berlebihan adalah bahwa masalah yang mereka pecahkan terlalu sederhana. Pada akhirnya, ini hanyalah komponen data tambahan yang terkait dengan entitas, dan pemrosesan selektif mereka. Di bawah ini saya akan menunjukkan cara yang sangat sederhana tentang bagaimana ini dapat diimplementasikan.

Pendekatan sederhana saya


Esensi

Dalam beberapa pendekatan, kelas Entitas didefinisikan, di lain, mereka bekerja dengan entitas sebagai ID / pegangan. Dalam pendekatan komponen, entitas tidak lain adalah komponen yang terkait dengannya, dan untuk ini kelas tidak diperlukan. Suatu entitas akan secara eksplisit ada berdasarkan komponen terkait. Untuk melakukan ini, tentukan:

 using EntityID = int64_t; //    , int64_t -   

Komponen Entitas

Komponen adalah berbagai jenis data yang terkait dengan entitas yang ada. Kita dapat mengatakan bahwa untuk setiap entitas e, e akan memiliki nol dan lebih banyak jenis komponen yang dapat diakses. Intinya, ini adalah hubungan nilai kunci yang meledak dan, untungnya, ada alat perpustakaan standar dalam bentuk kartu untuk ini.

Jadi, saya mendefinisikan komponen sebagai berikut:

 struct Position { float x; float y; }; struct Velocity { float x; float y; }; struct Health { int max; int current; }; template <typename Type> using ComponentMap = std::unordered_map<EntityID, Type>; using Positions = ComponentMap<Position>; using Velocities = ComponentMap<Velocity>; using Healths = ComponentMap<Health>; struct Components { Positions positions; Velocities velocities; Healths healths; }; 

Ini cukup untuk menunjukkan entitas melalui komponen, seperti yang diharapkan dari ECS. Misalnya, untuk membuat entitas dengan posisi dan kesehatan, tetapi tanpa kecepatan, Anda perlu:

 //given a Components instance c EntityID newID = /*obtain new entity ID*/; c.positions[newID] = Position{0.0f, 0.0f}; c.healths[newID] = Health{100, 100}; 

Untuk menghancurkan entitas dengan ID yang diberikan, kami cukup .erase() dari setiap kartu.

Sistem

Komponen terakhir yang kita butuhkan adalah sistem. Ini adalah logika yang bekerja dengan komponen untuk mencapai perilaku tertentu. Karena saya suka menyederhanakan banyak hal, saya menggunakan fungsi normal. Sistem regenerasi kesehatan yang disebutkan di atas mungkin hanya fungsi berikutnya.

 void updateHealthRegeneration(int64_t currentFrame, Healths& healths) { if(currentFrame % 120 == 0) { for(auto& [id, health] : healths) { if(health.current < health.max) ++health.current; } } } 

Kita dapat menempatkan panggilan ke fungsi ini di tempat yang tepat di loop utama dan mentransfernya ke penyimpanan komponen kesehatan. Karena repositori kesehatan hanya berisi catatan untuk entitas yang memiliki kesehatan, ia dapat memprosesnya secara terpisah. Ini juga berarti bahwa fungsi hanya mengambil data yang diperlukan dan tidak menyentuh yang tidak relevan.

Tetapi bagaimana jika sistem bekerja dengan lebih dari satu komponen? Katakan sistem fisik yang mengubah posisi berdasarkan kecepatan. Untuk melakukan ini, kita perlu memotong semua kunci dari semua tipe komponen yang terlibat dan beralih pada nilainya. Pada titik ini, perpustakaan standar tidak lagi cukup, tetapi menulis pembantu tidak begitu sulit. Sebagai contoh:

 void updatePhysics(Positions& positions, const Velocities& velocities) { //   ,   N   //   ID,    . std::unordered_set<EntityID> targets = mapIntersection(positions, velocities); // target'     ,   //  ,       . for(EntityID id : targets) { Position& pos = positions.at(id); const Velocity& vel = velocities.at(id); pos.x += vel.x; pos.y += vel.y; } } 

Atau Anda dapat menulis pembantu yang lebih ringkas yang memungkinkan akses yang lebih efisien melalui iterasi daripada mencari.

 void updatePhysics(Positions& positions, const Velocities& velocities) { //   ,    //  .        //    . intersectionInvoke<Position, Velocity>(positions, velocities, [] (EntityID id, Position& pos, const Velocity& vel) { pos.x += vel.x; pos.y += vel.y; } ); } 

Dengan demikian, kami membiasakan diri dengan fungsi dasar ECS biasa.

Manfaatnya


Pendekatan ini sangat efektif, karena dibangun dari awal tanpa membatasi abstraksi. Anda tidak perlu mengintegrasikan pustaka eksternal atau mengadaptasi basis kode dengan ide yang telah ditentukan tentang apa yang seharusnya Entitas / Komponen / Sistem.
Dan karena pendekatan ini benar-benar transparan, atas dasar itu Anda dapat membuat utilitas dan bantuan apa pun. Implementasi ini tumbuh dengan kebutuhan proyek Anda. Kemungkinan besar, untuk prototipe sederhana atau game untuk game jam'ov, Anda akan memiliki cukup banyak fungsi yang dijelaskan di atas.

Jadi, jika Anda baru di seluruh bidang ECS ​​ini, pendekatan langsung seperti itu akan membantu untuk memahami ide-ide utama.

Keterbatasan


Tetapi, seperti halnya metode lain, ada beberapa keterbatasan. Dalam pengalaman saya, justru implementasi seperti itu menggunakan unordered_map dalam game non-sepele yang akan menyebabkan masalah kinerja.

Iterasi persimpangan kunci pada banyak instance unordered_map dengan banyak entitas tidak dapat diukur dengan baik karena Anda benar-benar melakukan pencarian N*M , di mana N adalah jumlah komponen yang tumpang tindih, M adalah jumlah entitas yang cocok, dan unordered_map tidak pandai melakukan caching. Masalah ini dapat diperbaiki dengan menggunakan penyimpanan nilai kunci yang lebih cocok untuk iterasi daripada unordered_map .

Keterbatasan lain adalah boilerplating. Tergantung pada apa yang Anda lakukan, mengidentifikasi komponen-komponen baru dapat menjadi membosankan. Anda mungkin perlu menambahkan pengumuman tidak hanya dalam struktur Komponen, tetapi juga dalam fungsi spawn, serialisasi, utilitas debugging, dll. Saya berlari ke ini sendiri dan memecahkan masalah dengan menghasilkan kode - Saya mendefinisikan komponen dalam file json eksternal, dan kemudian menghasilkan komponen C ++ dan fungsi pembantu pada tahap build. Saya yakin Anda dapat menemukan metode lain berdasarkan template untuk memperbaiki masalah boilerplate yang Anda temui.

AKHIR

Jika Anda memiliki pertanyaan dan komentar, Anda dapat meninggalkannya di sini atau pergi ke pelajaran terbuka dengan Dima , dengarkan dia dan tanyakan sekitar.

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


All Articles