Taktyka przenoszenia logiki renderowania poza klasę GameObject

10

Tworząc gry, często tworzysz następujący obiekt gry, z którego dziedziczą wszystkie byty:

public class GameObject{
    abstract void Update(...);
    abstract void Draw(...);
}

Tak więc podczas aktualizacji pętli iterujesz wszystkie obiekty gry i dajesz im szansę na zmianę stanu, a następnie w następnej pętli losowania ponownie iterujesz wszystkie obiekty gry i dajesz im szansę na narysowanie się.

Chociaż działa to dość dobrze w prostej grze z prostym rendererem do przodu, często prowadzi do kilku gigantycznych obiektów gry, które muszą przechowywać swoje modele, wiele tekstur i, co najgorsze, ze wszystkich grubych metod rysowania, które tworzą ścisłe połączenie między obiektem gry, aktualna strategia renderowania i wszelkie klasy związane z renderowaniem.

Gdybym miał zmienić strategię renderowania z naprzód na odroczony, musiałbym zaktualizować wiele obiektów w grze. A obiekty gry, które tworzę, nie nadają się do wielokrotnego użytku. Oczywiście, dziedziczenie i / lub kompozycja może pomóc mi w walce z duplikacją kodu i nieco ułatwić zmianę implementacji, ale nadal wydaje mi się, że jej brakuje.

Być może lepszym sposobem byłoby całkowite usunięcie metody Draw z klasy GameObject i utworzenie klasy Renderer. GameObject nadal musiałby zawierać pewne dane dotyczące jego wyglądu, takie jak model, który ma być reprezentowany i jakie tekstury powinny zostać namalowane na modelu, ale sposób wykonania zostanie pozostawiony do renderowania. Jednak podczas renderowania często występuje wiele przypadków granicznych, więc chociaż usunęłoby to ścisłe powiązanie GameObject z Rendererem, Renderer nadal musiałby wiedzieć wszystko o wszystkich obiektach gry, które sprawiłyby, że byłby gruby, wszystko wiedział i ściśle powiązane. Naruszyłoby to sporo dobrych praktyk. Może projektowanie zorientowane na dane może załatwić sprawę. Obiektami gier z pewnością byłyby dane, ale w jaki sposób mechanizm renderujący byłby przez to sterowany? Nie jestem pewny.

Więc jestem zagubiony i nie mogę wymyślić dobrego rozwiązania. Próbowałem użyć zasad MVC, a kiedyś miałem pewne pomysły, jak to wykorzystać w grach, ale ostatnio nie wydaje się to tak odpowiednie, jak myślałem. Chciałbym wiedzieć, jak wszyscy rozwiązujecie ten problem.

Podsumowując, interesuje mnie, w jaki sposób można osiągnąć następujące cele projektowe.

  • Brak logiki renderowania w obiekcie gry
  • Luźne sprzężenie między obiektami gry a silnikiem renderowania
  • Nie wszyscy znający renderer
  • Najlepiej środowisko przełączające między silnikami renderującymi

Idealna konfiguracja projektu byłaby oddzielną „logiką gry” i renderowała projekt logiki, który nie musi się nawzajem odnosić.

Ten pociąg myślowy zaczął się, gdy usłyszałem na Twitterze, że John Carmack mówi, że ma system tak elastyczny, że może wymieniać silniki renderujące w czasie wykonywania, a nawet może powiedzieć swojemu systemowi, aby używał obu rendererów (renderera oprogramowania i renderera przyspieszanego sprzętowo) jednocześnie, aby mógł sprawdzić różnice. Systemy, które do tej pory programowałem, nie są nawet tak elastyczne

Roy T.
źródło

Odpowiedzi:

7

Szybki pierwszy krok do odłączenia:

Obiekty gry odwołują się do identyfikatora ich wyglądu, ale nie do danych, powiedzmy coś prostego jak ciąg znaków. Przykład: „human_male”

Renderer jest odpowiedzialny za ładowanie i utrzymywanie referencji „human_male” oraz przekazywanie z powrotem uchwytom obiektu do użycia.

Przykład w strasznym pseudokodzie:

GameObject( initialization parameters )
  me.render_handle = Renderer_Create( parameters.render_string )

- elsewhere
Renderer_Create( string )

  new data handle = Resources_Load( string );
  return new data handle

- some time later
GameObject( something happens to me parameters )
  me.state = something.what_happens
  Renderer_ApplyState( me.render_handle, me.state.effect_type )

- some time later
Renderer_Render()
  for each renderable thing
    for each rendering back end
        setup graphics for thing.effect
        render it

- finally
GameObject_Destroy()
  Renderer_Destroy( me.render_handle )

Przepraszam za ten bałagan, w każdym razie twoje warunki są spełnione przez tę prostą zmianę z czystego OOP opartego na patrzeniu na takie rzeczy jak rzeczywiste obiekty i na OOP w oparciu o obowiązki.

  • Brak logiki renderowania w obiekcie gry (zrobione, cały obiekt wie, że jest uchwytem, ​​dzięki czemu może zastosować efekty do siebie)
  • Luźne sprzężenie między obiektami gry a silnikiem renderowania (gotowe, cały kontakt odbywa się poprzez abstrakcyjny uchwyt, stany, które można zastosować, a nie to, co zrobić z tymi stanami)
  • Nie wszyscy znający renderer (gotowe, tylko wiedzą o sobie)
  • Najlepiej, jeśli środowisko wykonawcze przełącza się między silnikami renderującymi (odbywa się to na etapie Renderer_Render (), jednak musisz napisać oba tylne końce)

Słowami kluczowymi, które możesz wykroczyć poza zwykłe refaktoryzowanie klas, byłyby „system encji / komponentów” i „wstrzykiwanie zależności” oraz potencjalnie „MVC” wzorce GUI tylko po to, aby uruchomić stare koła mózgowe.

Patrick Hughes
źródło
To bardzo różni się od tego, co zrobiłem wcześniej, wydaje się, że ma całkiem spory potencjał. Na szczęście nie ogranicza mnie żaden istniejący silnik, więc mogę po prostu majstrować. Sprawdzę także terminy, o których wspomniałeś, chociaż zastrzyk uzależnienia zawsze powoduje ból mojego mózgu: P.
Roy T.
2

To, co zrobiłem dla własnego silnika, to zgrupowanie wszystkiego w moduły. Mam więc swoją GameObjectklasę i ma ona uchwyt do:

  • ModuleSprite - rysowanie duszków
  • ModuleWeapon - pistolety
  • ModuleScriptingBase - skryptowanie
  • Moduł Cząstek - efekty cząsteczkowe
  • ModuleCollision - wykrywanie kolizji i reagowanie

Więc mam Playerklasę i Bulletklasę. Oba pochodzą z GameObjecti są dodawane do Scene. Ale Playerma następujące moduły:

  • ModuleSprite
  • ModuleWeapon
  • Moduł Cząstek
  • ModuleCollision

I Bulletma te moduły:

  • ModuleSprite
  • ModuleCollision

Ten sposób organizowania rzeczy pozwala uniknąć „Diamentu śmierci”, w którym masz A Vehicle, A VehicleLandi A, VehicleWatera teraz chcesz VehicleAmphibious. Zamiast tego masz a Vehiclei może mieć ModuleWatera ModuleLand.

Dodano bonus: możesz tworzyć obiekty za pomocą zestawu właściwości. Wszystko, co musisz wiedzieć, to typ podstawowy (Gracz, Wróg, Pocisk itp.), A następnie utwórz uchwyty do modułów potrzebnych dla tego typu.

W mojej scenie wykonuję następujące czynności:

  • Zadzwoń Updatedo wszystkich GameObjectuchwytów.
  • Wykonaj sprawdzanie kolizji i reagowanie na kolizję dla tych, którzy mają ModuleCollisionuchwyt.
  • Zadzwoń UpdatePostdo wszystkich GameObjectuchwytów, aby poinformować o ich ostatecznej pozycji po fizyce.
  • Zniszcz obiekty z ustawioną flagą.
  • Dodaj nowe obiekty z m_ObjectsCreatedlisty do m_Objectslisty.

I mógłbym to zorganizować dalej: według modułów zamiast według obiektu. Następnie renderowałbym listę ModuleSprite, aktualizowałem kilka ModuleScriptingBasei kolizji z listą ModuleCollision.

knight666
źródło
Brzmi jak kompozycja na maksa! Bardzo dobrze. Nie widzę tu jednak wielu wskazówek dotyczących renderowania. Jak sobie z tym poradzić, dodając różne moduły?
Roy T.
O tak. Jest to wadą tego systemu: jeśli masz określone wymagania dotyczące GameObject(na przykład sposobu renderowania „węża” duszków), musisz albo utworzyć element podrzędny ModuleSpritedla tej określonej funkcji ( ModuleSpriteSnake), albo całkowicie dodać nowy moduł ( ModuleSnake). Na szczęście są to tylko wskaźniki, ale widziałem kod, w którym GameObjectdosłownie wszystko, co mógł zrobić obiekt.
knight666