Okey, co wiem do tej pory; Jednostka zawiera komponent (przechowywanie danych), który przechowuje takie informacje; - Tekstura / sprite - Shader - itp
A potem mam system renderujący, który rysuje to wszystko. Ale nie rozumiem, w jaki sposób należy zaprojektować renderer. Czy powinienem mieć jeden komponent dla każdego „typu wizualnego”. Jeden komponent bez modułu cieniującego, jeden z modułem cieniującym itp.?
Potrzebuję tylko trochę informacji na temat „właściwego sposobu”, aby to zrobić. Wskazówki i pułapki, na które należy uważać.
Odpowiedzi:
Trudno odpowiedzieć na to pytanie, ponieważ każdy ma własne wyobrażenie o tym, jak powinien wyglądać system komponentu encji. Najlepsze, co mogę zrobić, to podzielić się z Tobą niektórymi rzeczami, które uważam za najbardziej przydatne dla mnie.
Jednostka
Podchodzę do ECS klasami tłuszczowymi, prawdopodobnie dlatego, że uważam, że ekstremalne metody programowania są wysoce nieefektywne (pod względem produktywności człowieka). W tym celu byt jest dla mnie abstrakcyjną klasą, którą odziedziczą bardziej wyspecjalizowane klasy. Jednostka ma wiele wirtualnych właściwości i prostą flagę, która mówi mi, czy ta jednostka powinna istnieć. W związku z tym pytanie dotyczące systemu renderowania
Entity
wygląda tak:składniki
Składniki są „głupie”, ponieważ nic nie robią ani nic nie wiedzą . Nie mają żadnych odniesień do innych komponentów i zazwyczaj nie mają żadnych funkcji (pracuję w języku C #, więc używam właściwości do obsługi funkcji pobierających / ustawiających - jeśli mają funkcje, opierają się na odzyskiwaniu przechowywanych danych).
Systemy
Systemy są mniej „głupie”, ale wciąż są głupimi automatami. Nie mają one kontekstu dla całego systemu, nie mają odniesień do innych systemów i nie przechowują żadnych danych poza kilkoma buforami, które mogą być potrzebne do ich indywidualnego przetwarzania. W zależności od systemu może mieć specjalistyczną metodę
Update
lubDraw
metodę, aw niektórych przypadkach obie te metody.Interfejsy
Interfejsy są kluczową strukturą w moim systemie. Służą do określania, co
System
może przetwarzać, a coEntity
potrafi. Interfejsami istotnymi do renderowania są:IRenderable
iIAnimatable
.Interfejsy po prostu informują system, które komponenty są dostępne. Na przykład system renderujący musi znać obwiednię elementu i rysowany obraz. W moim przypadku byłoby to
SpatialComponent
iImageComponent
. Wygląda to tak:RenderingSystem
Jak więc system renderujący rysuje byt? To jest naprawdę dość proste, więc pokażę ci tylko uproszczoną klasę, aby dać ci pomysł:
Patrząc na klasę, system renderowania nawet nie wie, co to
Entity
jest. Wszystko, o czym wieIRenderable
, to po prostu podano listę do narysowania.Jak to wszystko działa
Pomoże to również zrozumieć, w jaki sposób tworzę nowe obiekty gry i jak je karmię do systemów.
Tworzenie jednostek
Wszystkie obiekty gry dziedziczą od Entity oraz wszelkie odpowiednie interfejsy opisujące możliwości tego obiektu gry. Prawie wszystko, co jest animowane na ekranie, wygląda następująco:
Karmienie systemów
Trzymam listę wszystkich bytów, które istnieją w świecie gry, na jednej liście o nazwie
List<Entity> gameObjects
. Następnie każdą ramkę przeglądam tę listę i kopiuję odwołania do obiektów do większej liczby list na podstawie typu interfejsu, takiego jakList<IRenderable> renderableObjects
iList<IAnimatable> animatableObjects
. W ten sposób, jeśli różne systemy muszą przetwarzać ten sam byt, mogą to zrobić. Następnie po prostu przekazuję te listy każdemu z systemówUpdate
lubDraw
metod i pozwalam systemom wykonywać swoją pracę.Animacja
Możesz być ciekawy, jak działa system animacji. W moim przypadku możesz zobaczyć interfejs IAnimatable:
Kluczową rzeczą, na którą należy zwrócić uwagę, jest to, że
ImageComponent
aspektIAnimatable
interfejsu nie jest tylko do odczytu; ma setera .Jak można się domyślać, komponent animacji po prostu przechowuje dane dotyczące animacji; lista ramek (które są składnikami obrazu), bieżąca ramka, liczba ramek na sekundę do narysowania, czas, jaki upłynął od przyrostu ostatniej klatki, oraz inne opcje.
System animacji korzysta z systemu renderowania i relacji komponentu obrazu. Po prostu zmienia komponent obrazu elementu, zwiększając klatkę animacji. W ten sposób animacja jest renderowana pośrednio przez system renderowania.
źródło
Zobacz tę odpowiedź, aby zobaczyć rodzaj systemu, o którym mówię.
Komponent powinien zawierać szczegóły dotyczące tego, co narysować i jak go narysować. System renderowania weźmie te szczegóły i narysuje obiekt w sposób określony przez komponent. Tylko jeśli użyjesz znacznie różnych technologii rysowania, będziesz mieć osobne komponenty dla osobnych stylów.
źródło
Kluczowym powodem rozdzielenia logiki na komponenty jest utworzenie zestawu danych, które po połączeniu w całość dają użyteczne zachowanie wielokrotnego użytku. Na przykład rozdzielenie jednostki na element PhysicsComponent i RenderComponent ma sens, ponieważ prawdopodobne jest, że nie wszystkie jednostki będą miały fizykę, a niektóre jednostki mogą nie mieć Sprite.
Aby odpowiedzieć na twoje pytanie, musisz spojrzeć na swoją architekturę i zadać sobie dwa pytania:
Przy dzieleniu komponentu ważne jest, aby zadać to pytanie, jeśli odpowiedź na 1. brzmi „tak”, prawdopodobnie masz dobrego kandydata do utworzenia dwóch oddzielnych komponentów, jednego z modułem cieniującym i jednego z teksturą. Odpowiedź na 2. jest zwykle tak dla komponentów takich jak Pozycja, w których wiele elementów może używać pozycji.
Na przykład, zarówno fizyka, jak i dźwięk mogą używać tej samej pozycji, zamiast obu komponentów przechowujących zduplikowane pozycje refaktoryzujesz je do jednego PositionComponent i wymagać, aby podmioty korzystające z PhysicsComponent / AudioComponent również miały PositionComponent.
W oparciu o informacje, które nam przekazałeś, nie wydaje się, aby Twój RenderComponent był dobrym kandydatem do podziału na TextureComponent i ShaderComponent, ponieważ shader są całkowicie zależne od Texture i nic więcej.
Zakładając, że używasz czegoś podobnego do T-Machine: Entity Systems przykładowa implementacja RenderComponent & RenderSystem w C ++ wyglądałaby mniej więcej tak:
źródło
Pitfall # 1: przesadny kod. Zastanów się, czy naprawdę potrzebujesz każdej rzeczy, którą wdrożysz, ponieważ będziesz musiał z tym żyć przez dłuższy czas.
Pitfall # 2: zbyt wiele obiektów. Nie użyłbym systemu z zbyt dużą liczbą obiektów (po jednym dla każdego typu, podtypu itp.), Ponieważ utrudnia to automatyczne przetwarzanie. Moim zdaniem o wiele przyjemniej jest kontrolować każdy obiekt określonym zestawem funkcji (w przeciwieństwie do jednego elementu). Na przykład tworzenie komponentów dla każdego bitu danych zawartych w renderowaniu (komponent tekstury, komponent modułu cieniującego) jest zbyt podzielony - zwykle i tak trzeba mieć wszystkie te komponenty razem, prawda?
Pitfall # 3: zbyt ścisła kontrola zewnętrzna. Wolę zmieniać nazwy na obiekty cieniujące / tekstury, ponieważ obiekty mogą się zmieniać za pomocą renderera / typu tekstury / formatu cieniującego / cokolwiek innego. Nazwy są prostymi identyfikatorami - to renderer decyduje, co z nich zrobić. Pewnego dnia możesz chcieć mieć materiały zamiast zwykłych shaderów (np. Dodaj shadery, tekstury i tryby mieszania z danych). Dzięki interfejsowi tekstowemu znacznie łatwiej jest to zaimplementować.
Jeśli chodzi o renderer, może to być prosty interfejs, który tworzy / niszczy / utrzymuje / renderuje obiekty utworzone przez komponenty. Najbardziej prymitywną reprezentacją może być coś takiego:
Umożliwiłoby to zarządzanie tymi obiektami ze składników i utrzymanie ich na tyle daleko, aby można je było renderować w dowolny sposób.
źródło