Czy jestem na dobrej drodze z tą architekturą komponentów?

9

Niedawno postanowiłem odnowić architekturę gry, aby pozbyć się głębokich hierarchii klas i zastąpić je konfigurowalnymi komponentami. Pierwszą hierarchią, którą zastępuję, jest hierarchia przedmiotów i chciałbym uzyskać porady, czy jestem na dobrej drodze.

Wcześniej miałem hierarchię, która wyglądała mniej więcej tak:

Item -> Equipment -> Weapon
                  -> Armor
                  -> Accessory
     -> SyntehsisItem
     -> BattleUseItem -> HealingItem
                      -> ThrowingItem -> ThrowsAsAttackItem

Nie trzeba dodawać, że zaczynał się robić bałagan i nie było to łatwe rozwiązanie dla przedmiotów, które musiały być różnego rodzaju (tj. Niektóre urządzenia są używane do syntezy przedmiotów, niektóre mogą być rzucane itp.)

Następnie spróbowałem przefakturować i umieścić funkcjonalność w podstawowej klasie przedmiotów. Ale potem zauważyłem, że Przedmiot miał dużo nieużywanych / zbędnych danych. Teraz próbuję stworzyć komponent taki jak architektura, przynajmniej dla moich przedmiotów, zanim spróbuję zrobić to z innymi klasami gier.

Oto, co obecnie myślę o konfiguracji składników:

Mam podstawową klasę przedmiotów, która ma miejsca na różne elementy (tj. Miejsce na wyposażenie, miejsce na leczenie, itp., A także mapę na dowolne elementy), więc coś takiego:

class Item
{
    //Basic item properties (name, ID, etc.) excluded
    EquipmentComponent* equipmentComponent;
    HealingComponent* healingComponent;
    SynthesisComponent* synthesisComponent;
    ThrowComponent* throwComponent;
    boost::unordered_map<std::string, std::pair<bool, ItemComponent*> > AdditionalComponents;
} 

Wszystkie elementy elementu odziedziczą po podstawowej klasie ItemComponent, a każdy typ elementu odpowiada za poinformowanie silnika, jak zaimplementować tę funkcję. tzn. HealingComponent mówi mechanikom bitewnym, jak spożywać przedmiot jako przedmiot leczący, podczas gdy ThrowComponent mówi silnikowi bitewnemu, jak traktować przedmiot jako przedmiot rzucany.

Mapa służy do przechowywania dowolnych składników, które nie są podstawowymi elementami. Łączę go z boolem, aby wskazać, czy pojemnik na przedmioty powinien zarządzać ItemComponent, czy też jest zarządzany przez zewnętrzne źródło.

Mój pomysł polegał na tym, że zdefiniowałem podstawowe komponenty używane przez mój silnik gry, a moja fabryka przedmiotów przypisałaby komponenty, które faktycznie mają przedmiot, w przeciwnym razie są one zerowe. Mapa zawierałaby dowolne komponenty, które generalnie byłyby dodawane / używane przez pliki skryptów.

Moje pytanie brzmi: czy to dobry projekt? Jeśli nie, jak można to poprawić? Rozważałem grupowanie wszystkich komponentów na mapie, ale użycie indeksowania ciągów wydawało się niepotrzebne w przypadku podstawowych elementów

użytkownik127817
źródło

Odpowiedzi:

8

To wydaje się bardzo rozsądnym pierwszym krokiem.

Opowiadasz się za kombinacją ogólności (mapa „dodatkowych komponentów”) i wydajności wyszukiwania (elementy zakodowane na sztywno), co może być trochę wstępną optymalizacją - twoje zdanie na temat ogólnej nieefektywności opartej na łańcuchach Wyszukiwanie jest dobrze wykonane, ale można to złagodzić, wybierając indeksowanie składników przez coś szybciej niż skrót. Jednym z podejść może być nadanie każdemu typowi komponentu unikalnego identyfikatora typu (zasadniczo implementujesz lekki niestandardowy kod RTTI ) i indeksowanie na jego podstawie.

Niezależnie od tego, ostrzegam cię przed ujawnieniem publicznego API dla obiektu Item, który pozwala poprosić o dowolny komponent - na stałe i dodatkowe - w jednolity sposób. Ułatwiłoby to zmianę podstawowej reprezentacji lub bilansu składników zakodowanych na stałe / nie zakodowanych na stałe bez konieczności refaktoryzacji wszystkich klientów elementów składowych.

Możesz również rozważyć udostępnienie „fałszywych” wersji bez oporu każdego z zakodowanych na stałe komponentów i upewnić się, że są one zawsze przypisywane - możesz wtedy używać elementów referencyjnych zamiast wskaźników i nigdy nie będziesz musiał sprawdzać wskaźnika NULL przed interakcją z jedną z klas komponentów zakodowanych na stałe. Nadal będziesz ponosić koszty wysyłki dynamicznej za interakcję z elementami tego komponentu, ale tak by się stało nawet z elementami wskaźnika. Jest to bardziej kwestia czystości kodu, ponieważ wpływ na wydajność będzie prawdopodobnie znikomy.

Nie sądzę, że to świetny pomysł, aby mieć dwa różne rodzaje zakresów życia (innymi słowy, nie sądzę, że wartość bool, którą masz na dodatkowej mapie komponentów, to świetny pomysł). Komplikuje system i sugeruje, że zniszczenie i uwolnienie zasobów nie będzie strasznie deterministyczne. Interfejs API twoich komponentów byłby znacznie jaśniejszy, gdybyś wybrał jedną strategię zarządzania przez całe życie lub drugą - albo jednostka zarządza czasem życia komponentu, albo podsystemem, który realizuje komponenty (wolę ten drugi, ponieważ lepiej paruje się z komponentem zaburtowym podejście, które omówię dalej).

Dużym minusem, jaki widzę w twoim podejściu, jest to, że zbijasz wszystkie komponenty razem w obiekt „bytu”, co w rzeczywistości nie zawsze jest najlepszym projektem. Z mojej powiązanej odpowiedzi na inne pytanie oparte na komponentach:

Twoje podejście do używania dużej mapy komponentów i wywołania update () w obiekcie gry jest dość nieoptymalne (i częsta pułapka dla tych, którzy pierwszy budują tego rodzaju systemy). Powoduje to bardzo słabą spójność pamięci podręcznej podczas aktualizacji i nie pozwala korzystać z współbieżności i trendu w kierunku procesu dużych partii danych lub zachowania w stylu SIMD jednocześnie. Często lepiej jest użyć projektu, w którym obiekt gry nie aktualizuje swoich komponentów, a raczej podsystem odpowiedzialny za sam komponent aktualizuje je wszystkie naraz.

Zasadniczo przyjmujesz to samo podejście, przechowując komponenty w elemencie przedmiotu (co jest znowu całkowicie akceptowalnym pierwszym krokiem). To, co możesz odkryć, to fakt, że większość dostępu do komponentów, o które martwisz się wydajnością, to po prostu ich aktualizacja, a jeśli zdecydujesz się zastosować bardziej zewnętrzne podejście do organizacji komponentów, gdzie komponenty są przechowywane w spójnej pamięci podręcznej , wydajna (dla swojej domeny) struktura danych według podsystemu, który najlepiej rozumie ich potrzeby, można osiągnąć znacznie lepszą, bardziej równoległą wydajność aktualizacji.

Ale wskazuję to tylko jako coś, co należy uznać za przyszły kierunek - na pewno nie chcesz przesadzić z nadmierną inżynierią; możesz dokonać stopniowego przejścia przez ciągłe refaktoryzowanie lub odkryć, że Twoja obecna implementacja doskonale spełnia Twoje potrzeby i nie ma potrzeby jej powtarzania.

Społeczność
źródło
1
+1 za sugerowanie pozbycia się przedmiotu Przedmiot. Chociaż będzie to więcej pracy z góry, ostatecznie będzie produkować lepszy system komponentów.
James
Miałem jeszcze kilka pytań, nie jestem pewien, czy powinienem zacząć nowy topiuc, więc najpierw spróbuję tutaj: w mojej klasie przedmiotów nie ma metod, które nazywam evert frame (lub nawet zamykam). W przypadku moich podsystemów graficznych posłucham twojej rady i zachowam wszystkie obiekty do aktualizacji w systemie. Drugie pytanie, jakie miałem, dotyczy sposobu obsługi kontroli komponentów? Na przykład chcę sprawdzić, czy mogę użyć elementu jako X, więc naturalnie sprawdziłbym, czy element ma komponent potrzebny do wykonania X. Czy to właściwy sposób, aby to zrobić?
Jeszcze