Zamiast moich zwykłych, ciężkich silników gier, bawię się podejściem opartym na komponentach. Mam jednak trudności z uzasadnieniem, gdzie pozwolić komponentom na wykonanie swoich zadań.
Powiedzmy, że mam prosty byt, który ma listę komponentów. Oczywiście jednostka nie wie, jakie są te elementy. Może być obecny element, który nadaje bytowi pozycję na ekranie, inny może być tam, aby narysować byt na ekranie.
Aby te komponenty działały, muszą aktualizować każdą klatkę, najłatwiej to zrobić, przechodząc przez drzewo sceny, a następnie dla każdej encji aktualizować każdy komponent. Ale niektóre komponenty mogą wymagać nieco więcej zarządzania. Na przykład komponentem, który powoduje, że jednostka jest kolidowalna, musi zarządzać coś, co może nadzorować wszystkie kolidujące elementy. Składnik, który sprawia, że element jest rysowalny, potrzebuje kogoś, kto nadzoruje wszystkie inne elementy do rysowania, aby ustalić kolejność rysowania itp.
Więc moje pytanie brzmi: gdzie mam zaktualizować komponenty, jaki jest czysty sposób na przekazanie ich menedżerom?
Zastanawiałem się nad użyciem obiektu menedżera singletonów dla każdego typu komponentów, ale ma to zwykle wady związane z używaniem singletonu, sposobem na złagodzenie tego problemu jest zastosowanie wstrzykiwania zależności, ale dla tego problemu brzmi to jak przesada. Mógłbym także przejść po drzewie sceny, a następnie zebrać różne komponenty w listy, używając jakiegoś wzorca obserwatora, ale wydaje się to trochę marnotrawstwem w przypadku każdej klatki.
źródło
Odpowiedzi:
Proponuję zacząć od przeczytania 3 wielkich kłamstw Mike'a Actona, ponieważ naruszysz dwa z nich. Mówię poważnie, to zmieni sposób projektowania kodu: http://cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html
Więc co naruszasz?
Kłamstwo 3 - Kod jest ważniejszy niż dane
Mówisz o iniekcji zależności, która może być przydatna w niektórych (i tylko niektórych) przypadkach, ale zawsze powinnaś zadzwonić wielkim wielkim dzwonkiem alarmowym, jeśli go użyjesz, szczególnie przy tworzeniu gier! Dlaczego? Ponieważ jest to często niepotrzebna abstrakcja. A abstrakcje w niewłaściwych miejscach są okropne. Więc masz grę. Gra ma menedżerów różnych komponentów. Wszystkie komponenty są zdefiniowane. Stwórz więc klasę gdzieś w głównym kodzie pętli gry, która „ma” menedżerów. Lubić:
Daj mu kilka funkcji pobierających, aby uzyskać każdą klasę menedżera (getBulletManager ()). Może ta klasa sama w sobie jest singletonem lub jest osiągalna z jednego (prawdopodobnie masz gdzieś centralny singlet gry). Nie ma nic złego w dobrze określonych, zakodowanych danych i zachowaniu.
Nie twórz menedżera ManagerManager, który pozwala rejestrować menedżerów za pomocą klucza, który można odzyskać za pomocą tego klucza przez inne klasy, które chcą korzystać z menedżera. To świetny system i bardzo elastyczny, ale tutaj mowa o grze. Wiesz dokładnie, jakie systemy są w grze. Po co udawać, że nie? Ponieważ jest to system dla osób, które uważają, że kod jest ważniejszy niż dane. Powiedzą: „Kod jest elastyczny, dane go wypełniają”. Ale kod to tylko dane. System, który opisałem, jest znacznie łatwiejszy, bardziej niezawodny, łatwiejszy w utrzymaniu i dużo bardziej elastyczny (na przykład, jeśli zachowanie jednego menedżera różni się od innych menedżerów, wystarczy zmienić tylko kilka wierszy zamiast przerabiać cały system)
Kłamstwo # 2 - Kod powinien być zaprojektowany wokół modelu świata
Więc masz byt w świecie gry. Jednostka ma wiele składników określających jej zachowanie. Tworzysz więc klasę Entity z listą obiektów Component i funkcją Update (), która wywołuje funkcję Update () każdego Component. Dobrze?
Nie :) To projektowanie wokół modelu świata: masz kulę w swojej grze, więc dodajesz klasę Bullet. Następnie aktualizujesz każdy pocisk i przechodzisz do następnego. To absolutnie zabije twoją wydajność i da ci strasznie zawiłą bazę kodów ze zduplikowanym kodem wszędzie i bez logicznej struktury podobnego kodu. (Sprawdź moją odpowiedź tutaj, aby uzyskać bardziej szczegółowe wyjaśnienie, dlaczego tradycyjny projekt OO jest do bani, lub sprawdź Projekt zorientowany na dane)
Spójrzmy na sytuację bez naszego uprzedzenia OO. Chcemy, co następuje, nie mniej więcej (pamiętaj, że nie ma wymogu tworzenia klasy dla encji lub obiektu):
I spójrzmy na sytuację. Twój komponent będzie aktualizował zachowanie każdego obiektu w grze w każdej klatce. To zdecydowanie krytyczny system twojego silnika. Wydajność jest tutaj ważna!
Jeśli znasz zarówno architekturę komputerową, jak i projektowanie zorientowane na dane, wiesz, jak osiągnąć najlepszą wydajność: ciasno upakowana pamięć i grupowanie wykonywania kodu. Jeśli wykonasz fragmenty kodu A, B i C w następujący sposób: ABCABCABC, nie uzyskasz takiej samej wydajności, jak w przypadku wykonania w następujący sposób: AAABBBCCC. Nie tylko dlatego, że pamięć podręczna instrukcji i danych będzie efektywniej wykorzystywana, ale także dlatego, że wykonując wszystkie „A” jeden po drugim, istnieje wiele miejsca na optymalizację: usunięcie duplikatu kodu, wstępne obliczenie danych używanych przez wszystkie „A” itp.
Więc jeśli chcemy zaktualizować wszystkie komponenty, nie róbmy z nich klas / obiektów z funkcją aktualizacji. Nie nazywajmy tej funkcji aktualizacji dla każdego komponentu w każdej jednostce. To rozwiązanie „ABCABCABC”. Zgrupujmy razem wszystkie identyczne aktualizacje komponentów. Następnie możemy zaktualizować wszystkie komponenty A, a następnie B, itd. Czego potrzebujemy, aby to zrobić?
Po pierwsze potrzebujemy menedżerów komponentów. Do każdego rodzaju elementu w grze potrzebujemy klasy menedżerskiej. Posiada funkcję aktualizacji, która aktualizuje wszystkie komponenty tego typu. Ma funkcję tworzenia, która doda nowy komponent tego typu oraz funkcję usuwania, która zniszczy określony komponent. Mogą istnieć inne funkcje pomocnicze do pobierania i ustawiania danych specyficznych dla tego komponentu (np .: ustaw model 3D dla komponentu modelu). Pamiętaj, że menedżer jest w pewnym sensie czarną skrzynką dla świata zewnętrznego. Nie wiemy, jak przechowywane są dane każdego komponentu. Nie wiemy, jak każdy komponent jest aktualizowany. Nie obchodzi nas to, dopóki komponenty zachowują się tak, jak powinny.
Następnie potrzebujemy bytu. Możesz zrobić z tego zajęcia, ale nie jest to konieczne. Jednostka może być niczym więcej niż unikalnym identyfikatorem całkowitym lub ciągiem mieszanym (a więc także liczbą całkowitą). Podczas tworzenia komponentu dla encji przekazujesz identyfikator jako argument do menedżera. Kiedy chcesz usunąć komponent, ponownie przekazujesz identyfikator. Dodanie nieco więcej danych do encji może być zaletą, zamiast uczynienia jej identyfikatorem, ale będą to tylko funkcje pomocnicze, ponieważ jak wymieniłem w wymaganiach, wszystkie zachowania encji są definiowane przez same komponenty. To twój silnik, więc rób to, co ma dla ciebie sens.
Potrzebujemy menedżera jednostek. Ta klasa będzie generować unikalne identyfikatory, jeśli użyjesz rozwiązania opartego tylko na ID, lub może zostać użyta do tworzenia / zarządzania obiektami Entity. Może także przechowywać listę wszystkich bytów w grze, jeśli jest to potrzebne. Entity Manager może być centralną klasą systemu komponentów, przechowując odniesienia do wszystkich menedżerów komponentów w grze i wywołując ich funkcje aktualizacji w odpowiedniej kolejności. W ten sposób wszystko, co musi zrobić pętla, to wywołanie EntityManager.update (), a cały system jest ładnie oddzielony od reszty silnika.
To widok z lotu ptaka, spójrzmy na działanie menedżerów komponentów. Oto czego potrzebujesz:
Ostatni dotyczy definiowania zachowania / logiki komponentów i zależy całkowicie od rodzaju komponentu, który piszesz. AnimationComponent zaktualizuje dane animacji na podstawie klatki, w której się znajduje. DragableComponent zaktualizuje tylko składnik przeciągany myszą. PhysicsComponent zaktualizuje dane w systemie fizyki. Ponieważ jednak aktualizujesz wszystkie komponenty tego samego typu za jednym razem, możesz dokonać optymalizacji, które nie są możliwe, gdy każdy komponent jest osobnym obiektem z funkcją aktualizacji, którą można wywołać w dowolnym momencie.
Zauważ, że wciąż nigdy nie apelowałem o utworzenie klasy XxxComponent do przechowywania danych komponentów. To zależy od Ciebie. Czy lubisz projektowanie zorientowane na dane? Następnie uporządkuj dane w osobnych tablicach dla każdej zmiennej. Czy lubisz projektowanie obiektowe? (Nie poleciłbym tego, wciąż zabije twoją wydajność w wielu miejscach). Następnie utwórz obiekt XxxComponent, który będzie przechowywał dane każdego komponentu.
Wspaniałą rzeczą w menedżerach jest enkapsulacja. Teraz enkapsulacja jest jedną z najstraszniej nadużywanych filozofii w świecie programowania. Tak należy go używać. Tylko menedżer wie, gdzie przechowywane są dane komponentu, jak działa logika komponentu. Istnieje kilka funkcji pobierania / ustawiania danych, ale to wszystko. Możesz przepisać cały menedżer i jego podstawowe klasy, a jeśli nie zmienisz publicznego interfejsu, nikt nawet tego nie zauważy. Zmieniłeś silnik fizyki? Po prostu przepisz PhysicsComponentManager i gotowe.
Jest jeszcze jedna ostatnia rzecz: komunikacja i wymiana danych między komponentami. Teraz jest to trudne i nie ma jednego uniwersalnego rozwiązania. Można utworzyć funkcje get / set w menedżerach, aby na przykład umożliwić komponentowi kolizji uzyskanie pozycji z komponentu pozycji (tj. PositionManager.getPosition (entityID)). Możesz użyć systemu zdarzeń. Możesz przechowywać niektóre wspólne dane w encji (moim zdaniem najbrzydsze rozwiązanie). Możesz użyć (często używanego) systemu przesyłania wiadomości. Lub użyj kombinacji wielu systemów! Nie mam czasu ani doświadczenia, aby wejść do każdego z tych systemów, ale google i wyszukiwarka stosów są Twoimi przyjaciółmi.
źródło
Jest to typowe naiwne podejście do aktualizacji komponentów (i nie ma nic złego w tym, że jest naiwne, jeśli działa dla ciebie). Jeden z dużych problemów, z którymi się zetknął - działasz przez interfejs komponentu (na przykład
IComponent
), więc nie wiesz nic o tym, co właśnie zaktualizowałeś. Prawdopodobnie nie wiesz też nic na temat zamawiania komponentów w ramach jednostki, więcSingleton nie jest tu tak naprawdę potrzebny, dlatego powinieneś go unikać, ponieważ ma w sobie wspomniane wady. Wstrzykiwanie zależności nie jest przesadą - sedno koncepcji polega na tym, że przekazujesz do obiektu rzeczy, których potrzebuje obiekt, najlepiej w konstruktorze. Nie potrzebujesz do tego ciężkiego frameworka DI (takiego jak Ninject ) - po prostu przekaż gdzieś dodatkowy parametr konstruktorowi.
Renderowanie jest podstawowym systemem i prawdopodobnie wspiera tworzenie i zarządzanie czasem życia renderowanych obiektów, które odpowiadają elementom wizualnym w grze (prawdopodobnie sprite'om lub modelom). Podobnie silnik fizyki prawdopodobnie ma dożywotnią kontrolę nad rzeczami reprezentującymi byty, które mogą się poruszać w symulacji fizyki (ciała sztywne). Każdy z tych odpowiednich systemów powinien posiadać, w pewnym zakresie, te obiekty i być odpowiedzialny za ich aktualizację.
Komponenty używane w systemie tworzenia elementów gry powinny być owijane wokół instancji z tych systemów niższego poziomu - twój element pozycji może po prostu owinąć sztywne ciało, twój element wizualny po prostu owinie dający się renderować duszek lub model, i tak dalej.
Następnie sam system, który jest właścicielem obiektów niższego poziomu, jest odpowiedzialny za ich aktualizację i może to zrobić zbiorczo oraz w sposób, który pozwala na wielowątkowość tej aktualizacji, jeśli to konieczne. Główna pętla gry kontroluje prostą kolejność aktualizacji tych systemów (najpierw fizyka, potem renderer lub cokolwiek innego). Jeśli masz podsystem, który nie ma żywotności ani kontroli aktualizacji nad instancjami, które rozdaje, możesz zbudować proste opakowanie do obsługi aktualizacji wszystkich komponentów istotnych dla tego systemu, a także zdecydować, gdzie umieścić to aktualizacja w stosunku do reszty aktualizacji systemu (zdarza się, jak się zdarza, z komponentami „skryptowymi”).
Takie podejście jest czasami znane jako podejście do komponentów zaburtowych , jeśli potrzebujesz więcej szczegółów.
źródło