Piszę strzelankę (jak 1942, klasyczna grafika 2D) i chciałbym użyć aplikacji opartej na komponentach. Do tej pory myślałem o następującym projekcie:
Każdy element gry (sterowiec, pocisk, wzmocnienie, wróg) jest bytem
Każda jednostka to zestaw komponentów, które można dodawać lub usuwać w czasie wykonywania. Przykłady to Pozycja, Sprite, Zdrowie, IA, Obrażenia, BoundingBox itp.
Chodzi o to, że Sterowiec, Pocisk, Wróg, Wzmocnienie NIE są klasami gier. Jednostka jest definiowana tylko przez składniki, które posiada (i które mogą się zmieniać w czasie). Tak więc sterowiec gracza zaczyna się od komponentów Sprite, Position, Health i Input. Powerup ma Sprite, Position, BoundingBox. I tak dalej.
Główna pętla zarządza „fizyką” gry, tj. Wzajemnym oddziaływaniem komponentów:
foreach(entity (let it be entity1) with a Damage component)
foreach(entity (let it be entity2) with a Health component)
if(the entity1.BoundingBox collides with entity2.BoundingBox)
{
entity2.Health.decrease(entity1.Damage.amount());
}
foreach(entity with a IA component)
entity.IA.update();
foreach(entity with a Sprite component)
draw(entity.Sprite.surface());
...
Komponenty są zakodowane na stałe w głównej aplikacji C ++. Jednostki można zdefiniować w pliku XML (część IA w pliku lua lub python).
Główna pętla nie dba zbytnio o byty: zarządza tylko komponentami. Projekt oprogramowania powinien umożliwiać:
Biorąc pod uwagę komponent, pobierz encję, do której on należy
Biorąc pod uwagę jednostkę, pobierz komponent typu „typ”
Zrób dla wszystkich bytów
Zrób coś dla wszystkich elementów encji (np .: serializuj)
Myślałem o następujących kwestiach:
class Entity;
class Component { Entity* entity; ... virtual void serialize(filestream, op) = 0; ...}
class Sprite : public Component {...};
class Position : public Component {...};
class IA : public Component {... virtual void update() = 0; };
// I don't remember exactly the boost::fusion map syntax right now, sorry.
class Entity
{
int id; // entity id
boost::fusion::map< pair<Sprite, Sprite*>, pair<Position, Position*> > components;
template <class C> bool has_component() { return components.at<C>() != 0; }
template <class C> C* get_component() { return components.at<C>(); }
template <class C> void add_component(C* c) { components.at<C>() = c; }
template <class C> void remove_component(C* c) { components.at<C>() = 0; }
void serialize(filestream, op) { /* Serialize all componets*/ }
...
};
std::list<Entity*> entity_list;
Dzięki temu projektowi mogę uzyskać # 1, # 2, # 3 (dzięki algorytmom boost :: fusion :: map) i # 4. Także wszystko jest O (1) (ok, niezupełnie, ale wciąż jest bardzo szybkie).
Istnieje również bardziej „powszechna” metoda:
class Entity;
class Component { Entity* entity; ... virtual void serialize(filestream, op) = 0; ...}
class Sprite : public Component { static const int type_id = 0; };
class Position : public Component { static const int type_id = 1; };
class Entity
{
int id; // entity id
std::vector<Component*> components;
bool has_component() { return components[i] != 0; }
template <class C> C* get_component() { return dynamic_cast<C> components[C::id](); } // It's actually quite safe
...
};
Innym podejściem jest pozbycie się klasy Entity: każdy typ komponentu żyje na własnej liście. Istnieje więc lista duszków, lista zdrowia, lista obrażeń itp. Wiem, że należą do tej samej jednostki logicznej z powodu identyfikatora jednostki. Jest to prostsze, ale wolniejsze: komponenty IA potrzebują dostępu w zasadzie do wszystkich innych komponentów encji, a to wymagałoby przeszukiwania listy komponentów na każdym etapie.
Który według Ciebie jest lepszy? czy mapa boost :: fusion nadaje się do takiego zastosowania?
źródło
Odpowiedzi:
Przekonałem się, że projektowanie oparte na komponentach i projektowanie zorientowane na dane idą w parze. Mówisz, że posiadanie jednorodnych list komponentów i eliminowanie obiektu encji pierwszej klasy (zamiast wybierania identyfikatora encji na samych komponentach) będzie „wolniejsze”, ale to nie jest ani tu, ani tam, ponieważ tak naprawdę nie profilowałeś żadnego prawdziwego kodu, który wdraża oba podejścia, aby dojść do tego wniosku. W rzeczywistości mogę prawie zagwarantować, że ujednolicenie komponentów i uniknięcie tradycyjnej silnej wirtualizacji będzie szybsze ze względu na różne zalety projektowania zorientowanego na dane - łatwiejszą równoległość, wykorzystanie pamięci podręcznej, modułowość itp.
Nie twierdzę, że takie podejście jest idealne do wszystkiego, ale systemy składowe, które są w zasadzie kolekcjami danych, które wymagają tych samych przekształceń przeprowadzanych na każdej ramce, po prostu krzyczą, aby były zorientowane na dane. Będą chwile, kiedy komponenty będą musiały komunikować się z innymi komponentami różnych typów, ale i tak będzie to konieczne zło. Nie powinno to jednak napędzać projektu, ponieważ istnieją sposoby rozwiązania tego problemu, nawet w skrajnym przypadku, gdy wszystkie komponenty są przetwarzane równolegle, takie jak kolejki komunikatów i futures .
Zdecydowanie Google do projektowania zorientowanego na dane, ponieważ odnosi się do systemów opartych na komponentach, ponieważ ten temat pojawia się dużo i jest sporo dyskusji i niepotwierdzonych danych.
źródło
gdybym miał napisać taki kod, wolałbym raczej używać tej approch (i nie używam żadnego dopalacza, jeśli jest to dla ciebie ważne), ponieważ może zrobić wszystko, co chcesz, ale problem polega na tym, że jest zbyt wiele entetów które nie dzielą jakiegoś komponentu, znalezienie tych, które go mają, zajmie trochę czasu. poza tym nie ma innego problemu, z którym mogę:
w tym podejściu każdy komponent jest bazą dla encji, więc biorąc pod uwagę komponent, jego wskaźnik również jest encją! drugą rzeczą, o którą prosisz, jest bezpośredni dostęp do komponentów jakiejś jednostki, np. kiedy muszę uzyskać dostęp do obrażeń w jednym z moich podmiotów, z których korzystam
dynamic_cast<damage*>(entity)->value
, więc jeślientity
ma element powodujący uszkodzenie, zwróci wartość. jeśli nie jesteś pewien, czyentity
ma uszkodzenie komponentu, czy nie, możesz łatwo sprawdzić, czyif (dynamic_cast<damage*> (entity))
wartość zwracanadynamic_cast
jest zawsze NULL, jeśli rzut nie jest prawidłowy i ten sam wskaźnik, ale z żądanym typem, jeśli jest prawidłowy. więc aby zrobić coś ze wszystkim,entities
co macomponent
, możesz to zrobić jak poniżejjeśli będą jakieś inne pytania, chętnie odpowiem.
źródło
bool isActive
podstawową klasę komponentu. tam jeszcze jest potrzebne do wprowadzenia użytecznych elementów podczas definiowania enteties ale nie uważam, że jako problem, a nadal masz seprate aktualizacje componnent (pamiętaj Somthing jakdynamic_cast<componnet*>(entity)->update()
.