Piszę grę przy użyciu C ++ i OpenGL 2.1. Zastanawiałem się, jak oddzielić dane / logikę od renderowania. W tej chwili używam klasy bazowej „Renderable”, która daje czystą wirtualną metodę implementacji rysunku. Ale każdy obiekt ma tak wyspecjalizowany kod, tylko obiekt wie, jak prawidłowo ustawić mundury modułu cieniującego i organizować dane bufora tablicy wierzchołków. W moim kodzie pojawia się wiele wywołań funkcji gl *. Czy istnieje jakiś ogólny sposób rysowania obiektów?
21
m_renderable
. W ten sposób możesz lepiej oddzielić swoją logikę. Nie wymuszaj renderowania „interfejsu” na obiektach ogólnych, które również mają fizykę, ai i tak dalej. Następnie możesz zarządzać renderowanymi osobno. Potrzebujesz warstwy abstrakcji nad wywołaniami funkcji OpenGL, aby jeszcze bardziej oddzielić rzeczy. Nie spodziewaj się więc, że dobry silnik będzie zawierał wywołania GL API w swoich różnych możliwych do renderowania implementacjach. To wszystko w mikro-pigułce.Odpowiedzi:
Pomysł polega na użyciu wzorca projektu Visitor. Potrzebujesz implementacji Renderer, która umie renderować rekwizyty. Każdy obiekt może wywołać instancję modułu renderującego w celu obsługi zadania renderowania.
W kilku wierszach pseudokodu:
Pliki gl * są implementowane metodami renderera, a obiekty przechowują tylko dane potrzebne do renderowania, pozycję, typ tekstury, rozmiar ... itd.
Ponadto można skonfigurować różne rendery (debugRenderer, hqRenderer, ... itd.) I używać ich dynamicznie, bez zmiany obiektów.
Można to również łatwo połączyć z systemami Entity / Component.
źródło
Entity/Component
nieco bardziej podkreślić alternatywę, ponieważ może ona pomóc w oddzieleniu dostawców geometrii od innych części silnika (AI, fizyka, sieć lub ogólna rozgrywka). +1!ObjectA
iObjectB
za pomocąDrawableComponentA
orazDrawableComponentB
, i w wewnętrznych metodach renderowania, użyć innych komponentów, jeśli są potrzebne, takich jak:position = component->getComponent("Position");
A w głównej pętli masz listę rysowalnych komponentów, z którymi można wywołać draw.Renderable
), który madraw(Renderer&)
funkcję i wszystkie obiekty, które można renderować, implementują je? W którym przypadkuRenderer
potrzebna jest tylko jedna funkcja, która akceptuje dowolny obiekt implementujący wspólny interfejs i wywołanierenderable.draw(*this);
?gl_*
funkcje do renderera (oddzielając logikę od renderowania), ale twoje rozwiązanie przenosigl_*
wywołania do obiektów.Wiem, że już zaakceptowałeś odpowiedź Zhena, ale chciałbym opublikować inną, na wypadek, gdyby pomógł komukolwiek innemu.
Aby powtórzyć problem, OP chce mieć możliwość oddzielenia kodu renderującego od logiki i danych.
Moim rozwiązaniem jest użycie innej klasy razem do renderowania komponentu, który jest oddzielny od
Renderer
klasy logicznej i. Najpierw musi istniećRenderable
interfejs, który ma funkcję,bool render(Renderer& renderer);
aRenderer
klasa używa wzorca gościa do pobierania wszystkichRenderable
instancji, biorąc pod uwagę listęGameObject
s i renderuje te obiekty, które mająRenderable
instancję. W ten sposób Renderer nie musi wiedzieć o każdym typie obiektu i nadal obowiązkiem każdego typu obiektu jest poinformowanie goRenderable
za pomocągetRenderable()
funkcji. Lub alternatywnie, możesz stworzyćRenderableVisitor
klasę, która odwiedza wszystkie GameObjects i na podstawie indywidualnychGameObject
warunków, które mogą wybrać, aby dodać / nie dodawać swojego renderowania do odwiedzającego. Tak czy inaczej, główną istotą jest to, żegl_*
wszystkie wywołania znajdują się poza samym obiektem i znajdują się w klasie, która zna intymne szczegóły samego obiektu, a nie jego częśćRenderer
.ZASTRZEŻENIE : Ręcznie napisałem te klasy w edytorze, więc jest duża szansa, że coś przeoczyłem w kodzie, ale mam nadzieję, że wpadniesz na pomysł.
Aby pokazać (częściowy) przykład:
Renderable
berłoGameObject
klasa:(Częściowa)
Renderer
klasa.RenderableObject
klasa:ObjectA
klasa:ObjectARenderable
klasa:źródło
Zbuduj system renderowania-polecenia. Obiekt wysokiego poziomu, który ma dostęp zarówno do
OpenGLRenderer
scenografu, jak i do obiektów gry, wykona iterację wykresu sceny lub obiektów gry i zbuduje partięRenderCmds
, która zostanie następnie przekazana temu,OpenGLRenderer
który losuje kolejno, a tym samym zawierający wszystkie OpenGL powiązany kod w nim.Ma to więcej zalet niż tylko abstrakcja; ostatecznie wraz ze wzrostem złożoności renderowania możesz sortować i grupować każde polecenie renderowania według tekstury lub modułu cieniującego, na przykład w
Render()
celu wyeliminowania wielu wąskich gardeł w wywołaniach losowania, które mogą mieć ogromną różnicę w wydajności.źródło
Zależy to całkowicie od tego, czy możesz założyć, co jest wspólne dla wszystkich jednostek renderowalnych, czy nie. W moim silniku wszystkie obiekty są renderowane w ten sam sposób, więc wystarczy podać vbos, tekstury i transformacje. Następnie renderer pobiera je wszystkie, dlatego żadne wywołania funkcji OpenGL nie są potrzebne w różnych obiektach.
źródło
Zdecydowanie umieść kod renderujący i logikę gry w różnych klasach. Kompozycja (jak sugeruje teodron) jest prawdopodobnie najlepszym sposobem na zrobienie tego; każda Istota w świecie gry będzie miała swój Renderowalny - a może ich zestaw.
Nadal możesz mieć wiele podklas renderowalnych, na przykład do obsługi animacji szkieletowych, emiterów cząstek i złożonych shaderów, oprócz podstawowego shadera z teksturą i podświetleniem. Klasa Renderable i jej podklasy powinny zawierać tylko informacje potrzebne do renderowania: geometrię, tekstury i shadery.
Ponadto należy oddzielić wystąpienie danej siatki od samej siatki. Powiedzmy, że masz sto drzew na ekranie, z których każde używa tej samej siatki. Chcesz zapisać geometrię tylko raz, ale będziesz potrzebować osobnych macierzy lokalizacji i obrotu dla każdego drzewa. Bardziej złożone obiekty, takie jak animowane humanoidy, będą również miały dodatkowe informacje o stanie (takie jak szkielet, zestaw aktualnie stosowanych animacji itp.).
Naiwnym podejściem jest iteracja każdej jednostki gry i nakazanie jej renderowania. Alternatywnie, każdy byt (gdy się spawnuje) może wstawić swój renderowany obiekt (obiekty) do obiektu sceny. Następnie funkcja renderowania informuje scenę do renderowania. Umożliwia to scenie wykonywanie złożonych czynności związanych z renderowaniem bez osadzania tego kodu w jednostkach gry lub w określonej podklasie renderowanej.
źródło
Ta rada nie jest tak naprawdę specyficzna dla renderowania, ale powinna pomóc w opracowaniu systemu, który oddziela rzeczy w dużej mierze od siebie. Najpierw spróbuj oddzielić dane „GameObject” od informacji o pozycji.
Warto zauważyć, że proste informacje o położeniu XYZ mogą nie być takie proste. Jeśli używasz silnika fizyki, dane pozycji mogą być przechowywane w silniku innej firmy. Trzeba będzie albo zsynchronizować między nimi (co wymagałoby dużo bezcelowego kopiowania pamięci) lub zapytać o informacje bezpośrednio z silnika. Ale nie wszystkie obiekty wymagają fizyki, niektóre zostaną ustalone na miejscu, więc prosty zestaw pływaków działa tam dobrze. Niektóre mogą być nawet przymocowane do innych obiektów, więc ich pozycja jest w rzeczywistości przesunięciem innej pozycji. W konfiguracji zaawansowanej pozycja może być przechowywana tylko na GPU, jedynym potrzebnym czasem po stronie komputera jest skrypty, przechowywanie i replikacja sieci. Prawdopodobnie będziesz mieć kilka możliwych wyborów dla swoich danych pozycyjnych. Tutaj warto zastosować dziedziczenie.
Zamiast obiektu posiadającego swoją pozycję, obiekt ten powinien być własnością struktury danych indeksujących. Na przykład „Poziom” może mieć scenę Octree, a może scenę z silnikiem fizyki. Kiedy chcesz wyrenderować (lub skonfigurować scenę renderowania), zapytaj swoją specjalną strukturę o obiekty widoczne dla kamery.
Pomaga to również zapewnić dobre zarządzanie pamięcią. W ten sposób obiekt, który tak naprawdę nie znajduje się w obszarze, nie ma nawet pozycji, która ma sens, zamiast zwracania 0,0 coordów lub coordów, które miał, kiedy był ostatni w obszarze.
Jeśli nie będziesz już utrzymywał współrzędnych w obiekcie, zamiast object.getX () uzyskasz poziom.getX (obiekt). Problem z wyszukiwaniem obiektu na poziomie będzie prawdopodobnie powolną operacją, ponieważ będzie musiał przejrzeć wszystkie obiekty i dopasować do tego, którego dotyczy zapytanie.
Aby tego uniknąć, prawdopodobnie stworzyłbym specjalną klasę „link”. Taki, który łączy poziom z obiektem. Nazywam to „Lokalizacja”. Zawierałby współrzędne xyz, a także uchwyt do poziomu i uchwyt do obiektu. Ta klasa łączy byłaby przechowywana w strukturze / poziomie przestrzennym, a obiekt miałby do niej słabe odniesienie (jeśli poziom / lokalizacja zostanie zniszczony, odwołanie do obiektów musi zostać zaktualizowane do wartości zerowej. Może być również warte posiadania klasy Lokalizacja „posiadać” obiekt, w ten sposób, jeśli poziom zostanie usunięty, podobnie jak specjalna struktura indeksu, lokalizacje, które on zawiera i jego obiekty.
Teraz informacje o pozycji są przechowywane tylko w jednym miejscu. Nie powielane między obiektem, strukturą indeksowania Spacial, rendererem i tak dalej.
Przestrzenne struktury danych, takie jak Octrees, często nie muszą nawet mieć współrzędnych przechowywanych obiektów. Pozycja jest zapisywana we względnej lokalizacji węzłów w samej strukturze (można by ją traktować jako rodzaj kompresji stratnej, poświęcającej dokładność dla krótkich czasów wyszukiwania). W przypadku obiektu lokalizacji w Octree po zakończeniu zapytania znajdują się w nim rzeczywiste współrzędne.
Lub jeśli używasz silnika fizyki do zarządzania lokalizacjami twoich obiektów lub ich mieszanką, klasa Location powinna sobie z tym poradzić w sposób transparentny, zachowując cały kod w jednym miejscu.
Kolejną zaletą jest teraz pozycja, a odniesienie do poziomu jest przechowywane w tej samej lokalizacji. Możesz zaimplementować object.TeleportTo (other_object) i pozwolić mu działać na różnych poziomach. Podobnie wyszukiwanie ścieżki AI może podążać za czymś w innym obszarze.
W odniesieniu do renderowania. Twój rendering może mieć podobne powiązanie z lokalizacją. Tyle że miałby tam specyficzne renderowanie. Prawdopodobnie nie potrzebujesz „Object” ani „Level” do przechowywania w tej strukturze. Obiekt może być przydatny, jeśli próbujesz zrobić coś takiego jak wybieranie kolorów lub renderowanie nad nim pływającego paska itp., Ale w przeciwnym razie renderer dba tylko o siatkę i tym podobne. RenderableStuff byłby siatką, mógłby również zawierać ramki ograniczające i tak dalej.
Być może nie będziesz musiał tego robić w każdej klatce, możesz upewnić się, że wybierasz większy region niż aktualnie pokazuje kamera. Buforuj go, śledź ruchy obiektów, aby sprawdzić, czy pole ograniczające znajduje się w zasięgu, śledź ruch kamery i tak dalej. Ale nie zaczynaj grzebać z tego rodzaju rzeczami, dopóki nie przetestujesz tego.
Sam silnik fizyki może mieć podobną abstrakcję, ponieważ nie potrzebuje również danych obiektu, tylko siatkę kolizji i właściwości fizyki.
Wszystkie podstawowe dane obiektu zawierałyby nazwę siatki używanej przez obiekt. Silnik gry może następnie załadować go w dowolnym formacie bez obciążania klasy obiektów mnóstwem rzeczy specyficznych do renderowania (które mogą być specyficzne dla interfejsu API renderowania, tj. DirectX vs OpenGL).
Utrzymuje także oddzielne elementy. Ułatwia to takie czynności, jak wymiana silnika fizyki, ponieważ te rzeczy są głównie samowystarczalne w jednym miejscu. Ułatwia to także nieprzytomność. Możesz testować takie rzeczy, jak zapytania fizyki, bez konieczności konfigurowania fałszywych obiektów, ponieważ wszystko, czego potrzebujesz, to klasa Location. Możesz także łatwiej zoptymalizować rzeczy. Sprawia, że bardziej oczywiste są zapytania, które należy wykonać na jakich klasach i pojedynczych lokalizacjach, aby je zoptymalizować (na przykład na powyższym poziomie .getVisibleObject to miejsce, w którym można buforować rzeczy, jeśli kamera nie porusza się zbyt wiele).
źródło