Jak mogę prawidłowo uzyskać dostęp do komponentów w moich systemach C ++ Entity-Component-Systems?

18

(To, co opisuję, opiera się na tym projekcie: Co to jest struktura systemu encji? Przewiń w dół, a znajdziesz ją)

Mam problemy z tworzeniem systemu encji-komponentów w C ++. Mam swoją klasę Component:

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

Który jest w rzeczywistości interfejsem do tworzenia innych komponentów. Aby stworzyć niestandardowy komponent, po prostu implementuję interfejs i dodam dane, które będą używane w grze:

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

Te komponenty są przechowywane w klasie Entity, co nadaje każdej instancji Entity unikalny identyfikator:

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

Komponenty są dodawane do encji, mieszając nazwę komponentu (prawdopodobnie nie jest to świetny pomysł). Kiedy dodam niestandardowy komponent, jest on zapisywany jako typ komponentu (klasa bazowa).

Z drugiej strony mam interfejs systemu, który korzysta z interfejsu węzła. Klasa Node służy do przechowywania niektórych komponentów pojedynczej encji (ponieważ System nie jest zainteresowany wykorzystaniem wszystkich komponentów encji). Gdy system musi update(), musi tylko iterować przechowywane w nim węzły utworzone z różnych podmiotów. Więc:

/* 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 */
};

Teraz problem: powiedzmy, że buduję SampleNodes, przekazując encję do SampleSystem. SampleNode następnie „sprawdza”, czy jednostka ma wymagane komponenty do użycia przez SampleSystem. Problem pojawia się, gdy potrzebuję dostępu do żądanego komponentu wewnątrz Entity: komponent jest przechowywany w Componentkolekcji (klasy podstawowej), więc nie mogę uzyskać dostępu do komponentu i skopiować go do nowego węzła. Tymczasowo rozwiązałem problem, przypisując Componenttyp pochodny, ale chciałem wiedzieć, czy istnieje lepszy sposób na zrobienie tego. Rozumiem, czy oznaczałoby to przeprojektowanie tego, co już mam. Dzięki.

Federico
źródło

Odpowiedzi:

23

Jeśli zamierzasz przechowywać Componentwszystkie razem w kolekcji, musisz użyć wspólnej klasy podstawowej jako typu przechowywanego w kolekcji, a zatem musisz rzutować na poprawny typ, gdy próbujesz uzyskać dostęp do nich Componentw kolekcji. Problemy z próbą rzutowania na niewłaściwą klasę pochodną można wyeliminować dzięki sprytnemu użyciu szablonów i typeidfunkcji:

Przy takiej deklaracji mapy:

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

funkcja addComponent, taka jak:

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

i getComponent:

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

Nie dostaniesz błędnego komunikatu. Wynika to z tego, typeidże zwraca wskaźnik do informacji o typie środowiska wykonawczego (najbardziej pochodnego typu) komponentu. Ponieważ komponent jest przechowywany z kluczowymi informacjami tego typu, rzutowanie nie może powodować problemów z powodu niedopasowania typów. Otrzymasz również sprawdzanie typu czasu kompilacji na typie szablonu, ponieważ musi to być typ pochodzący z Component, w przeciwnym razie static_cast<T*>będą mieć niedopasowane typy z unordered_map.

Nie trzeba jednak przechowywać elementów różnych typów we wspólnej kolekcji. Jeśli porzucisz ideę Entityzawierających Components i zamiast tego będziesz mieć w każdym Componentmagazynie Entity(w rzeczywistości prawdopodobnie będzie to tylko liczba całkowita), możesz zapisać każdy typ komponentu pochodnego we własnej kolekcji typu pochodnego zamiast jako wspólny typ podstawowy i znajdź Component„należące do” za Entitypomocą tego identyfikatora.

Ta druga implementacja jest nieco bardziej intuicyjna do myślenia niż pierwsza, ale prawdopodobnie może być ukryta jako szczegóły implementacji za interfejsem, więc użytkownicy systemu nie muszą się tym przejmować. Nie będę komentować, które jest lepsze, ponieważ tak naprawdę nie użyłem drugiego, ale nie widzę używania static_cast jako problemu z tak silną gwarancją na typy, jak zapewnia pierwsza implementacja. Należy pamiętać, że wymaga RTTI, co może, ale nie musi być problemem, w zależności od platformy i / lub przekonań filozoficznych.

Chewy Gumball
źródło
3
Używam C ++ od prawie 6 lat, ale co tydzień uczę się nowych sztuczek.
knight666
Dzięki za odpowiedź. Najpierw spróbuję użyć pierwszej metody, a jeśli później, wymyślę inny sposób. Ale czy ta addComponent()metoda nie musi być również metodą szablonową? Jeśli zdefiniuję a addComponent(Component* c), dowolny dodany podskładnik będzie przechowywany we Componentwskaźniku i typeidzawsze będzie odnosił się do Componentklasy podstawowej.
Federico
2
Typeid poda rzeczywisty typ wskazywanego obiektu, nawet jeśli wskaźnik jest klasy podstawowej
Chewy Gumball
Naprawdę podobała mi się odpowiedź Chewy, więc wypróbowałem jej implementację na mingw32. Natknąłem się na problem wspomniany przez fede rico, w którym addComponent () przechowuje wszystko jako składnik, ponieważ typeid zwraca składnik jako typ wszystkiego. Ktoś tutaj wspomniał, że typeid powinien podawać rzeczywisty typ wskazywanego obiektu, nawet jeśli wskaźnik jest klasą bazową, ale myślę, że może się różnić w zależności od kompilatora itp. Czy ktoś może to potwierdzić? Używałem g ++ std = c ++ 11 mingw32 na Windows 7. Skończyłem po prostu modyfikując getComponent (), aby był szablonem, a następnie zapisałem ten typ w th
shwoseph
To nie jest specyficzne dla kompilatora. Prawdopodobnie nie masz poprawnego wyrażenia jako argumentu funkcji typeid.
Chewy Gumball,
17

Chewy ma rację, ale jeśli używasz C ++ 11, masz kilka nowych typów, których możesz użyć.

Zamiast używać const std::type_info*jako klucza na mapie, możesz użyć std::type_index( patrz cppreference.com ), który jest opakowaniem wokół std::type_info. Dlaczego miałbyś to wykorzystać? std::type_indexFaktycznie przechowuje relacji z std::type_infojako wskaźnik, ale to jeden wskaźnik mniej dla ciebie martwić.

Jeśli rzeczywiście używasz C ++ 11, polecam przechowywanie Componentreferencji wewnątrz inteligentnych wskaźników. Mapa może więc wyglądać następująco:

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

Można dodać nowy wpis, aby:

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

gdzie componentjest typu std::shared_ptr<Component>. Pobieranie odniesienia do danego typu Componentmoże wyglądać następująco:

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
    }
}

Zwróć też uwagę na użycie static_pointer_castzamiast static_cast.

vijoc
źródło
1
Właściwie stosuję tego rodzaju podejście w swoim własnym projekcie.
vijoc
Jest to w rzeczywistości całkiem wygodne, ponieważ uczę się C ++ przy użyciu standardu C ++ 11 jako odniesienia. Jedną rzeczą, którą zauważyłem, jest to, że wszystkie systemy elementów-bytów, które znalazłem w Internecie, używają pewnego rodzaju cast. Zaczynam myśleć, że wdrożenie tego lub podobnego projektu systemu byłoby niemożliwe bez rzutów.
Federico
@Fede Przechowywanie Componentwskaźników w jednym pojemniku niekoniecznie wymaga rzutowania ich na typ pochodny. Ale, jak zauważył Chewy, masz do dyspozycji inne opcje, które nie wymagają rzutowania. Sam nie widzę nic „złego” w tego rodzaju rzutach w projekcie, ponieważ są one względnie bezpieczne.
vijoc
@vijoc Są czasami uważane za złe ze względu na problem ze spójnością pamięci, który mogą wprowadzić.
akaltar