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ć:
private CollissionManager _collissionManager;
private BulletManager _bulletManager;
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):
- Masz grupę bytów
- Jednostki składają się z szeregu składników, które określają zachowanie jednostki
- Chcesz aktualizować każdy element gry w każdej klatce, najlepiej w kontrolowany sposób
- Poza identyfikacją komponentów jako należących do siebie, sam byt nie musi nic robić. Jest to link / ID dla kilku komponentów.
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:
- Utwórz dane komponentu po wywołaniu funkcji create (entityID)
- Usuń dane komponentu po wywołaniu metody remove (entityID)
- Zaktualizuj wszystkie (odpowiednie) dane komponentu po wywołaniu update () (tzn. Nie wszystkie komponenty muszą aktualizować każdą ramkę)
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.