Bagaimana saya bisa mengakses komponen dalam C ++ Entity-Component-Systems saya dengan benar?


18

(Apa yang saya jelaskan didasarkan pada desain ini: Apa itu kerangka kerja sistem entitas ? , gulir ke bawah dan Anda akan menemukannya)

Saya mengalami beberapa masalah dalam menciptakan sistem entitas-komponen dalam C ++. Saya memiliki kelas Komponen saya:

class Component { /* ... */ };

Yang sebenarnya merupakan antarmuka, untuk komponen lain yang akan dibuat. Jadi, untuk membuat komponen khusus, saya hanya mengimplementasikan antarmuka dan menambahkan data yang akan digunakan dalam game:

class SampleComponent : public Component { int foo, float bar ... };

Komponen-komponen ini disimpan di dalam kelas Entity, yang memberi setiap instance Entity ID unik:

class Entity {
     int ID;
     std::unordered_map<string, Component*> components;
     string getName();
     /* ... */
};

Komponen ditambahkan ke entitas dengan hashing nama komponen (ini mungkin bukan ide yang bagus). Ketika saya menambahkan komponen kustom, itu disimpan sebagai tipe Komponen (kelas dasar).

Sekarang, di sisi lain, saya memiliki antarmuka Sistem, yang menggunakan antarmuka Node di dalamnya. Kelas Node digunakan untuk menyimpan beberapa komponen entitas tunggal (karena Sistem tidak tertarik menggunakan semua komponen entitas). Ketika Sistem harus update(), hanya perlu mengulang melalui Node yang disimpannya dibuat dari entitas yang berbeda. Begitu:

/* System and Node implementations: (not the interfaces!) */

class SampleSystem : public System {
        std::list<SampleNode> nodes; //uses SampleNode, not Node
        void update();
        /* ... */
};

class SampleNode : public Node {
        /* Here I define which components SampleNode (and SampleSystem) "needs" */
        SampleComponent* sc;
        PhysicsComponent* pc;
        /* ... more components could go here */
};

Sekarang masalahnya: katakanlah saya membangun SampleNodes dengan mengirimkan entitas ke SampleSystem. SampleNode kemudian "memeriksa" apakah entitas memiliki komponen yang diperlukan untuk digunakan oleh SampleSystem. Masalahnya muncul ketika saya perlu mengakses komponen yang diinginkan di dalam Entity: komponen disimpan dalam Componentkoleksi (kelas dasar), jadi saya tidak dapat mengakses komponen dan menyalinnya ke node baru. Saya telah sementara memecahkan masalah dengan melemparkan Componentke tipe turunan, tapi saya ingin tahu apakah ada cara yang lebih baik untuk melakukan ini. Saya mengerti jika ini berarti mendesain ulang apa yang sudah saya miliki. Terima kasih.

Jawaban:


23

Jika Anda akan menyimpan Components dalam koleksi bersama-sama maka Anda harus menggunakan kelas dasar umum sebagai jenis yang disimpan dalam koleksi, dan dengan demikian Anda harus melemparkan ke tipe yang benar ketika Anda mencoba mengakses Components dalam koleksi. Masalah mencoba melemparkan ke kelas turunan yang salah dapat dihilangkan dengan penggunaan templat dan typeidfungsi yang cerdas , namun:

Dengan peta yang dinyatakan seperti ini:

std::unordered_map<const std::type_info* , Component *> components;

fungsi addComponent seperti:

components[&typeid(*component)] = component;

dan komponen getComponent:

template <typename T>
T* getComponent()
{
    if(components.count(&typeid(T)) != 0)
    {
        return static_cast<T*>(components[&typeid(T)]);
    }
    else 
    {
        return NullComponent;
    }
}

Anda tidak akan salah pilih. Ini karena typeidakan mengembalikan pointer ke info tipe dari tipe runtime (tipe yang paling diturunkan) dari komponen. Karena komponen disimpan dengan info tipe itu sebagai kuncinya, para pemain tidak mungkin menyebabkan masalah karena tipe yang tidak cocok. Anda juga mendapatkan mengkompilasi tipe waktu memeriksa pada tipe templat karena harus jenis yang berasal dari Komponen atau static_cast<T*>akan memiliki jenis yang tidak cocok dengan unordered_map.

Anda tidak perlu menyimpan komponen dari berbagai jenis dalam koleksi umum. Jika Anda meninggalkan ide s Entitymengandung Component, dan alih-alih memiliki setiap Componenttoko Entity(pada kenyataannya, itu mungkin hanya akan menjadi integer ID), maka Anda dapat menyimpan setiap tipe komponen turunan dalam koleksi sendiri dari tipe turunan alih-alih sebagai jenis pangkalan umum, dan temukan Component"milik" sebuah Entitymelalui ID itu.

Implementasi kedua ini sedikit lebih tidak intuitif untuk dipikirkan daripada yang pertama, tetapi mungkin bisa disembunyikan sebagai detail implementasi di belakang antarmuka sehingga pengguna sistem tidak perlu peduli. Saya tidak akan mengomentari mana yang lebih baik karena saya belum benar-benar menggunakan yang kedua, tapi saya tidak melihat menggunakan static_cast sebagai masalah dengan jaminan yang kuat pada jenis sebagai implementasi pertama menyediakan. Perhatikan bahwa itu memerlukan RTTI yang mungkin atau mungkin tidak menjadi masalah tergantung pada platform dan / atau keyakinan filosofis.


3
Saya telah menggunakan C ++ selama hampir 6 tahun sekarang, namun setiap minggu saya belajar beberapa trik baru.
knight666

Terimakasih telah menjawab. Saya akan mencoba menggunakan metode pertama terlebih dahulu, dan jika saya mungkin nanti saya akan memikirkan cara menggunakan yang lain. Tetapi, bukankah addComponent()metode ini perlu menjadi metode templat juga? Jika saya mendefinisikan a addComponent(Component* c), setiap sub-komponen yang saya tambahkan akan disimpan dalam sebuah Componentpointer, dan typeidakan selalu merujuk ke Componentkelas dasar.
Federico

2
Typeid akan memberi Anda tipe sebenarnya dari objek yang sedang diarahkan bahkan jika pointernya adalah kelas dasar
Chewy Gumball

Saya sangat menyukai jawaban chewy, jadi saya mencoba implementasi di mingw32. Saya mengalami masalah yang disebutkan oleh fede rico di mana addComponent () menyimpan semuanya sebagai komponen karena typeid mengembalikan komponen sebagai tipe untuk semuanya. Seseorang di sini menyebutkan bahwa typeid harus memberikan tipe aktual dari objek yang sedang diarahkan bahkan jika pointer ke kelas dasar, tetapi saya pikir itu mungkin berbeda berdasarkan kompiler, dll. Adakah yang bisa mengkonfirmasi hal ini? Saya menggunakan g ++ std = c ++ 11 mingw32 di windows 7. Saya akhirnya hanya memodifikasi getComponent () menjadi templat, kemudian menyimpan tipe dari itu ke th
shwoseph

Ini bukan kompiler khusus. Anda mungkin tidak memiliki ekspresi yang tepat sebagai argumen untuk fungsi typeid.
Chewy Gumball

17

Chewy benar, tetapi jika Anda menggunakan C ++ 11, Anda memiliki beberapa tipe baru yang dapat Anda gunakan.

Alih-alih menggunakan const std::type_info*sebagai kunci di peta Anda, Anda bisa menggunakan std::type_index( lihat cppreference.com ), yang merupakan pembungkus di sekitar std::type_info. Mengapa Anda menggunakannya? The std::type_indexsebenarnya menyimpan hubungan dengan std::type_infosebagai pointer, tapi itu salah satu pointer kurang bagi Anda untuk khawatir tentang.

Jika Anda memang menggunakan C ++ 11, saya akan merekomendasikan menyimpan Componentreferensi di dalam smart pointer. Jadi petanya bisa seperti:

std::map<std::type_index, std::shared_ptr<Component> > components

Menambahkan entri baru dapat dilakukan:

components[std::type_index(typeid(*component))] = component

dimana componenttipe std::shared_ptr<Component>. Mengambil referensi ke jenis tertentu Componentdapat terlihat seperti:

template <typename T>
std::shared_ptr<T> getComponent()
{
    std::type_index index(typeid(T));
    if(components.count(std::type_index(typeid(T)) != 0)
    {
        return static_pointer_cast<T>(components[index]);
    }
    else
    {
        return NullComponent
    }
}

Perhatikan juga penggunaan static_pointer_castbukan static_cast.


1
Saya sebenarnya menggunakan pendekatan semacam ini dalam proyek saya sendiri.
vijoc

Ini sebenarnya cukup nyaman, karena saya telah belajar C ++ menggunakan standar C ++ 11 sebagai referensi. Satu hal yang saya perhatikan, adalah bahwa semua sistem entitas-komponen yang saya temukan di web menggunakan semacam itu cast. Saya mulai berpikir bahwa tidak mungkin untuk mengimplementasikan ini, atau desain sistem serupa tanpa gips.
Federico

@Fede Menyimpan Componentpointer dalam satu wadah tentu perlu melemparkannya ke tipe turunan. Tapi, seperti yang ditunjukkan Chewy, Anda memiliki opsi lain yang tersedia untuk Anda, yang tidak memerlukan casting. Saya sendiri tidak melihat ada yang "buruk" dalam desain jenis ini, karena mereka relatif aman.
vijoc

@vijoc Terkadang mereka dianggap buruk karena masalah koherensi memori yang mungkin mereka perkenalkan.
akaltar
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.