Dlaczego powinienem oddzielać obiekty od renderowania?

11

Disclamer: Wiem, co to jest wzorzec systemu encji i nie używam go.

Dużo czytałem o rozdzielaniu obiektów i renderowaniu. O tym, że logika gry powinna być niezależna od silnika renderującego. To wszystko jest w porządku i eleganckie i ma doskonały sens, ale powoduje również wiele innych bólów:

  • potrzeba synchronizacji między obiektem logicznym a obiektem renderującym (tym, który utrzymuje stan animacji, duszków itp.)
  • trzeba otworzyć obiekt logiczny publicznie, aby obiekt renderujący mógł odczytać rzeczywisty stan obiektu logicznego (często doprowadzając obiekt logiczny do łatwej transformacji w głupi obiekt pobierający i ustawiający)

Nie wydaje mi się to dobrym rozwiązaniem. Z drugiej strony bardzo intuicyjne jest wyobrażanie sobie obiektu jako jego reprezentacji 3d (lub 2d), a także bardzo łatwego w utrzymaniu (i być może również o wiele bardziej enkapsulowanym).

Czy istnieje sposób na utrzymanie reprezentacji graficznej i logiki gry w połączeniu (unikanie problemów z synchronizacją), ale oderwanie silnika renderowania? Czy istnieje sposób na oddzielenie logiki gry i renderowania, która nie powoduje powyższych wad?

(być może z przykładami, nie jestem zbyt dobry w rozumieniu abstrakcyjnych rozmów)

But
źródło
1
Byłoby również pomocne, jeśli podasz przykład tego, co masz na myśli, mówiąc, że nie używasz wzorca systemu encji, oraz sposób, w jaki myślisz, że odnosi się to do tego, czy powinieneś oddzielić problem renderowania od encji logika gry.
michael.bartnett,
@ michael.bartnett, nie dzielę obiektów na małe komponenty wielokrotnego użytku obsługiwane przez systemy, tak jak robi to większość implementacji wzorców. Zamiast tego mój kod jest raczej próbą wzorca MVC. Ale to tak naprawdę nie ma znaczenia, ponieważ pytanie nie jest zależne od żadnego kodu (nawet języka). Umieściłem dysklamator, ponieważ wiedziałem, że niektórzy próbowaliby mnie przekonać do korzystania z ECS, które wydaje się leczyć raka. I, jak widać, i tak się stało.
But

Odpowiedzi:

13

Załóżmy, że masz scenę złożoną ze świata , gracza i bossa. Aha, to jest gra dla trzeciej osoby, więc masz też aparat .

Twoja scena wygląda następująco:

class Scene {
    World* world
    Player* player
    Enemy* boss
    Camera* camera
}

(Przynajmniej są to podstawowe dane . To, jak je przechowujesz, zależy od ciebie.)

Chcesz aktualizować i renderować scenę tylko podczas gry, a nie w trybie pauzy lub w menu głównym ... więc dołącz ją do stanu gry!

State* gameState = new State();
gameState->addScene(scene);

Teraz twój stan gry ma scenę. Następnie chcesz uruchomić logikę na scenie i renderować scenę. Dla logiki wystarczy uruchomić funkcję aktualizacji.

State::update(double delta) {
    scene->update(delta);
}

W ten sposób możesz zachować całą logikę gry w Sceneklasie. I tylko dla odniesienia, system komponentu encji może to zrobić w ten sposób:

State::update(double delta) {
    physicsSystem->applyPhysics(scene);
}

W każdym razie udało Ci się zaktualizować scenę. Teraz chcesz to wyświetlić! W tym celu wykonujemy coś podobnego do powyższego:

State::render() {
    renderSystem->render(scene);
}

Proszę bardzo. RenderSystem odczytuje informacje ze sceny i wyświetla odpowiedni obraz. Uproszczona metoda renderowania sceny może wyglądać następująco:

RenderSystem::renderScene(Scene* scene) {
    Camera* camera = scene->camera;
    lookAt(camera); // Set up the appropriate viewing matrices based on 
                    // the camera location and direction

    renderHeightmap(scene->getWorld()->getHeightMap()); // Just as an example, you might
                                                        // use a height map as your world
                                                        // representation.
    renderModel(scene->getPlayer()->getType()); // getType() will return, for example "orc"
                                                // or "human"

    renderModel(scene->getBoss()->getType());
}

Naprawdę uproszczone, nadal musisz na przykład zastosować rotację i tłumaczenie w zależności od tego, gdzie jest twój gracz i gdzie szuka. (Mój przykład to gra 3D, jeśli wybierzesz 2D, będzie to spacer po parku).

Mam nadzieję, że tego właśnie szukałeś? Jak miejmy nadzieję, możesz przypomnieć sobie z powyższego, system renderowania nie dba o logikę gry . Używa tylko bieżącego stanu sceny do renderowania, tj. Pobiera z niej niezbędne informacje w celu renderowania. A logika gry? Nie ma znaczenia, co robi moduł renderujący. Cholera, nie ma znaczenia, czy w ogóle jest wyświetlany!

Nie musisz też dołączać informacji o renderowaniu do sceny. Powinno wystarczyć, aby mechanizm renderujący wiedział, że musi wyrenderować orka. Załadujesz już model orka, który renderer wie, aby wyświetlić.

To powinno spełnić twoje wymagania. Reprezentacja graficzna i logika są połączone , ponieważ oba wykorzystują te same dane. Jednak są one oddzielne , ponieważ żadne z nich nie polega na drugim!

EDYCJA: I tylko po to, żeby odpowiedzieć, dlaczego tak się dzieje? Ponieważ to łatwiejsze jest najprostszym powodem. Nie musisz myśleć o „tak i tak się stało, powinienem teraz zaktualizować grafikę”. Zamiast tego sprawiasz, że coś się dzieje, a każda klatka gra patrzy na to, co się obecnie dzieje, i interpretuje to w jakiś sposób, dając ci wynik na ekranie.

Wina
źródło
7

W tytule zadajesz inne pytanie niż treść ciała. W tytule pytasz, dlaczego logika i rendering powinny być oddzielone, ale w treści pytasz o implementacje systemów logiki / grafiki / renderowania.

Drugie pytanie zostało już poruszone , więc skupię się na pierwszym pytaniu.

Powody oddzielenia logiki i renderowania:

  1. Powszechnie panuje przekonanie, że przedmioty powinny zrobić jedną rzecz
  2. Co jeśli chcesz przejść z 2D na 3D? Co się stanie, jeśli zdecydujesz się na zmianę z jednego systemu renderującego na inny w środku projektu? Nie chcesz indeksować całego kodu i wprowadzać wielkich zmian w środku logiki gry.
  3. Prawdopodobnie powinieneś powtarzać sekcje kodu, co jest ogólnie uważane za zły pomysł.
  4. Możesz budować systemy kontrolujące potencjalnie ogromne obszary renderowania lub logiki bez indywidualnej komunikacji z małymi elementami.
  5. Co jeśli chcesz przypisać klejnot do gracza, ale system jest spowolniony o ile aspektów ma klejnot? Jeśli odpowiednio wyodrębniłeś swój system renderowania, możesz go aktualizować z różną szybkością, aby uwzględnić kosztowne operacje renderowania.
  6. Pozwala myśleć o rzeczach, które naprawdę mają znaczenie dla tego, co robisz. Po co owijać mózg wokół transformacji macierzy i przesuwania sprite'ów i współrzędnych ekranu, gdy wszystko, co chcesz zrobić, to zastosować mechanikę podwójnego skoku, dobrać kartę lub wyposażyć miecz? Nie chcesz, aby duszek reprezentujący twój wyposażony miecz renderował na jasno różowy tylko dlatego, że chciałeś go przenieść z prawej ręki na lewą.

W ustawieniach OOP tworzenie nowych obiektów wiąże się z pewnym kosztem, ale z mojego doświadczenia wynika, że ​​koszt zasobów systemowych jest niewielką ceną, jaką należy zapłacić za zdolność do zastanowienia się i wdrożenia konkretnych rzeczy, które muszę wykonać.

Dragonsdoom
źródło
6

Ta odpowiedź ma jedynie na celu zbudowanie intuicji, dlaczego oddzielenie renderowania i logiki jest ważne, zamiast bezpośredniego sugerowania praktycznych przykładów.

Załóżmy, że mamy dużego słonia , nikt w pokoju nie widzi całego słonia. może nawet wszyscy nie zgadzają się co do tego, co to właściwie jest. Ponieważ każdy widzi inną część słonia i może sobie z tym poradzić. Ale ostatecznie nie zmienia to faktu, że jest to duży słoń.

Słoń reprezentuje obiekt gry ze wszystkimi szczegółami. Ale nikt tak naprawdę nie musi wiedzieć wszystkiego o słoniu (obiekcie gry), aby móc wykonywać swoją funkcjonalność.

Połączenie logiki gry i renderowania jest tak, jakby wszyscy widzieli całego słonia. Jeśli coś się zmieniło, wszyscy muszą o tym wiedzieć. Podczas gdy w większości przypadków muszą zobaczyć tylko tę część, którą są zainteresowani. Jeśli coś zmieniło osobę, która o tym wie, wystarczy tylko powiedzieć drugiej osobie o wyniku tej zmiany, to jest dla niego ważne (traktuj to jako komunikację za pośrednictwem wiadomości lub interfejsów).

wprowadź opis zdjęcia tutaj

Punkty, o których wspomniałeś, nie są wadami, są tylko wadami, jeśli było więcej zależności, niż powinno być w silniku, innymi słowy, systemy widzą części słonia bardziej niż powinny. A to oznacza, że ​​silnik nie został „poprawnie” zaprojektowany.

Synchronizacja z jej formalną definicją jest potrzebna tylko wtedy, gdy używasz silnika wielowątkowego, w którym logika i rendering znajdują się w dwóch różnych wątkach, a nawet silnik, który wymaga dużej synchronizacji między systemami, nie jest specjalnie zaprojektowany.

W przeciwnym razie naturalnym sposobem radzenia sobie z takim przypadkiem jest zaprojektowanie systemu jako wejścia / wyjścia. Aktualizacja wykonuje logikę i wyświetla wynik. Renderowanie jest tylko kanałem z wynikami aktualizacji. Naprawdę nie musisz ujawniać wszystkiego. Udostępniasz tylko interfejs, który komunikuje się między dwoma etapami. Komunikacja między różnymi częściami silnika powinna odbywać się za pomocą abstrakcji (interfejsów) i / lub komunikatów. Nie należy ujawniać wewnętrznej logiki ani stanów.

Weźmy prosty przykład wykresu sceny, aby wyjaśnić ten pomysł.

Aktualizacja odbywa się zwykle za pośrednictwem pojedynczej pętli zwanej pętlą gry (lub ewentualnie za pośrednictwem wielu pętli gier, z których każda działa w osobnym wątku). Po pętli zaktualizowano kiedykolwiek obiekt gry. Musi jedynie stwierdzić za pośrednictwem wiadomości lub interfejsów, że obiekty 1 i 2 zostały zaktualizowane, i przekazać je z ostateczną transformacją.

System renderujący dokonuje tylko ostatecznej transformacji i nie wie, co faktycznie zmieniło się w obiekcie (na przykład nastąpiła konkretna kolizja itp.). Teraz, aby wyrenderować ten obiekt, potrzebuje tylko identyfikatora tego obiektu i ostatecznej transformacji. Następnie renderer będzie zasilał interfejs API renderowania siatką i ostateczną transformacją, nie wiedząc nic więcej.

concept3d
źródło