Autor Wątek: Zarządzanie obiektami  (Przeczytany 3296 razy)

Offline Fedake

  • Użytkownik

# Wrzesień 14, 2012, 18:00:21
Witam, pisząc obiektowo grę natknąłem się na pewien problem. Wszystkie obiekty w grze dziedziczą z klasy bazowej Entity. Dodatkowo mam inną klasę która przechowuje vector wskaźników na obiekty typu Entity, posiada dodatkowo metody do zarządzania jak usuwanie i dodawanie co nie jest ważne. Problem powstaje gdy różne obiekty dziedziczące z klasy Entity potrzebują do updatu różnych danych. Dla przykładu Enemy potrzebuje czasu który minął od ostatniej klatki oraz pozycji gracza, a Button jedynie informacji czy ma być wciśnięty czy nie.
Jak wiadomo posiadając wskaźniki na Entity nie mogę wywoływać funkcji specyficznych dla danego typu obiektu.

Pierwszy pomysł na jaki wpadłem(i używam aktualnie) to zadeklarować wszystkie potrzebne przyszłym obiektom metody jako wirtualne w klasie Entity oraz zdefiniowanie odpowiednich metod w klasach pochodnych. Następnie przy iteracji przez wszystkie obiekty, sprawdzanie ich typu i dzięki polimorfizmowi użycie odpowiednich metod. Niestety, problem powstaje gdy dodaję kolejne rodzaje obiektów muszę deklarować kolejne metody update z różnymi argumentami.

Kolejny pomysł to sprawdzanie typu w czasie iteracji(tak samo jak ostatnio), z tą różnicą że tym razem zamiast używać odpowiedniej, jednej z kilkunastu przeładowanych metod update, rzutować wskaźnik Entity na odpowiedni typ zyskując tym samym dostęp do wszystkich metod klasy pochodnej. Eliminuje to problem dodawania do klasy bazowej coraz to nowych metod ale wydaje mi się że ten sposób jest trochę "brzydki".

Następny pomysł to jedna metoda update przyjmująca kilkanaście argumentów, z których klasy pochodne będą używać tylko tych które są im potrzebne.

I tutaj moje pytanie, czy któraś z tych metod jest popularna/powszechnie uznana za dobrą, czy może istnieje jakiś sposób bez wad którego nie udało mi się wymyślić.

Pozdrawiam.

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Charibo

  • Redaktor

# Wrzesień 14, 2012, 18:14:19
Nie wiem na ile to jest "powszechne i dobre", ale ja zwykłem ograniczać hierarchię do minimum. Zasadniczo klasa Wróg nie ma nic wspólnego z klasą Przycisk, może poza tym, że obie mogą zawierać komponent Renderable. Wtedy po prostu jeśli już trzymam listy, to osobnych rzeczy. Osobna lista na wrogów (to już nie kłopot, żeby Gnom dziedziczył z Wroga), osobna na buttony/elementy GUI, itd, itd. :)

Offline JasonVoorhees

  • Użytkownik
    • FotoGry

# Wrzesień 14, 2012, 20:13:11
Zasadniczo klasa Wróg nie ma nic wspólnego z klasą Przycisk, może poza tym, że obie mogą zawierać komponent Renderable.
Jak to nic? A pozycja, X, Y :D ?

Offline Xender

  • Użytkownik

# Wrzesień 14, 2012, 20:29:05
Takiego gwałtu na OOPie dawno nie widziałem :P.
1. Klasa bazowa nie może zawierać więcej funkcjonalności niż część wspólna wszystkich klas pochodnych; inaczej mówiąc, nieabstrakcyjna klasa pochodna nie może nie implementować jakiejś funkcjonalności klasy bazowej. Każda klasa bazowa musi dostarczać pełnego interfejsu klasy bazowej.

Po ludzku oznacza to, że klasa bazowa musi tworzyć interfejs, który ma sens we wszytkich klasach pochodnych

2. Nie ma sensu wyprowadzać wszystkiego od jednej klasy bazowej bez powodu. Jednostki i elementy GUI mają się do siebie nijak.

(to już nie kłopot, żeby Gnom dziedziczył z Wroga)
Ajć, to powinno być już definiowane danymi - klasa Enemy (a jeszcze lepiej Entity (w sensie jednostka, nie "cokolwiek" jak wymyślił autor tematu)) powinna IMHO trzymać wskaźnik na wzorzec gnoma (obiekt, nie klasę) i dane konkretnej instancji gnoma.

Offline dynax

  • Użytkownik

# Wrzesień 14, 2012, 20:59:18
Nie wiem na ile to jest "powszechne i dobre", ale ja zwykłem ograniczać hierarchię do minimum. Zasadniczo klasa Wróg nie ma nic wspólnego z klasą Przycisk, może poza tym, że obie mogą zawierać komponent Renderable.

Ja tam lubię design jaki ma flixel. Jest główna klasa Base która jest tylko bazą do dziedziczenia dla innych komponentów. Ma pola typu "isActive" i deklaracje funkcji typu "update()", "render()" czy "collide()". Z tej klasy dziedziczą 2 inne - Object i Group. Group jest kontenerem mogącym przechowywać obiekty Base i za jednym wywołaniem wyżej wymienionych funkcji odwołać się do każdego przechowywanego przez siebie obiektu. Object zaś reprezentuje typ do wyświetlania na ekran. Posiada pola "x", "y", "velocity", "width", "height". Tutaj rozwijane są funkcje odpowiedzialne za kolizje. Z Object dziedziczą klasy typu Sprite i Text które już się specjalizują w wyświetlaniu konkretnego contentu. Z Group dziedziczą klasy w stylu ParticleEmitter i Button (bo button to tak naprawdę kontener złożony ze sprite'a z tłem i tekstu na wierzchu).

Offline Shusty

  • Użytkownik

# Wrzesień 14, 2012, 21:19:33
Nad OOP lepiej dobrze pomyśleć, bo potem same kłopoty. Lepiej nie robić takiej dużej hierarchii, jeśli się nie ma tego dobrze przemyślanego.

Co do Twojego pytania poszukaj o: dynamic_cast i typeid

Offline Xender

  • Użytkownik

# Wrzesień 14, 2012, 21:40:05
@up - OOP to sposób a nie temat do myślenia. Lepiej w ogóle nie robić dużej hierarchii, a małą tylko wtedy, gdy to konieczne. Nieuzasadnionego RTTI unikać bardziej niż goto - metody wirtualne są po to, aby wszelkie zachowania zależne od właściwego typu obiektu definiować w jednym miejscu - w definicji klasy.

Offline Fedake

  • Użytkownik

# Wrzesień 14, 2012, 22:39:12
Widze że mój przykład z buttonem troche namieszał więc wyjaśnie. Button w moim aktualnym projekcie jest oczywiście jednostką poziomu, dlatego dziedziczy z Entity, jest to przycisk na którym można stanąć i zostaje wciśnięty.

Po ludzku oznacza to, że klasa bazowa musi tworzyć interfejs, który ma sens we wszytkich klasach pochodnych
I właśnie na tym polega mój problem, nie wiem jak stworzyć taki interfejs który miałby sens we wszystkich klasach pochodnych, które potrzebują różnych danych co klatkę aby mogły poprawnie działać.

Offline Shusty

  • Użytkownik

# Wrzesień 14, 2012, 23:08:13
@up: w takim przypadku robienie polimorfizmu dla metody update po prostu nie ma sensu.
Ew. na siłę możesz przekazywać zawsze np. 6argumentów i wtedy każdy skorzysta z parametru jaki potrzebuje, albo w te co nie potrzebne dla danego obiektu jako parametr dajesz null.

Moim zdaniem powinieneś mieć na obiekty więcej kontenerów, mniej będziesz miał bałaganu.

Offline Veldrin

  • Użytkownik

  • +2
# Wrzesień 15, 2012, 00:15:27
Nie wystarczy tradycyjna (i powszechnie spotykana w wieeeeeeeeeeeeelu silnikach i projektach) klasa bazowa i virtual update?

Ja żeby użytkownik miał wybór to w swoim silniku zrobiłem bazowe SceneEntity, które składa się z kilku komponentów/struktur odpowiedziealnych za konkretne rzeczy. Aktualizacja stanu jest możliwa na trzy sposoby: dziedziczenie i wołanie update, podpięty skrypt Lua, który dostaje API lub podpinamy funktor.

Jak chcemy robić grę korzystając z OOP to dziedziczymy i ładujemy tam dane/wskaźniki do wszelkich danych potrzebnych danemu bytowi, a update dostaje tylko const dt. Aktualizując korzystamy z danych.

Jak chcemy mieć dowolność i nie robić dużej hierarchii to podpinamy obiekt funkcyjny. Metoda do klasy gdzie są dane i inne funkcje i świat zaczyna być piękny.

PS. Dawanie N argumentów i rzucanie nullptr tam gdzie nie będzie wykorzystywany to pachnie mi strasznym(brrr) stylem WinAPI. Funkcja przyjmuje 10 argumentów przy czym 6 ma być nullem, w trzech ma być -1 (bo tak) a pierwszym przyjmuje jedyną istotną wartość... 

Offline Xender

  • Użytkownik

# Wrzesień 15, 2012, 00:17:14
Ew. na siłę możesz przekazywać zawsze np. 6argumentów i wtedy każdy skorzysta z parametru jaki potrzebuje, albo w te co nie potrzebne dla danego obiektu jako parametr dajesz null.
No bez jaj...
@Veldrin - to gorsze niż WinAPI, bo tam te argumenty coś znaczą (chyba, że są reserved ;->) a null jest tylko default... Tutaj to "dynamicznie przydzielane reserved"...

Moim zdaniem powinieneś mieć na obiekty więcej kontenerów, mniej będziesz miał bałaganu.
Dokładnie.

Offline Shusty

  • Użytkownik

# Wrzesień 15, 2012, 00:52:37
Veldrin ma w sumie rację. update() powinien przyjmować jako parametr tylko long deltaTime

Offline Kurak

  • Użytkownik

# Wrzesień 15, 2012, 01:10:53
Na wstępie: nie ma jednej idealnej drogi. Nie próbuję niczego udowadniać, raczej chcę pokazać inne możliwości. OP najlepiej żeby sam coś wymyślił ;)

Cytuj
Dla przykładu Enemy potrzebuje czasu który minął od ostatniej klatki oraz pozycji gracza, a Button jedynie informacji czy ma być wciśnięty czy nie.
Opcja 1: dajesz każdemu obiektowi możliwość zarejestrowania się pod konkretny event (np. OnFrame, OnCollision, OnTakeDamage, OnDeactivate). Handlery przyjmują właściwe sobie argumenty, nie ma wołania niepotrzebnych handlerów, a jakieś eventy i tak są zawsze potrzebne (i lepsze to niż sprawdzanie stanu co ramkę w każdym obiekcie).
Opcja 2: wołasz jeden generyczny, bezargumentowy update i potrzebne dane wyciągasz przez jakieś gettery. To jest najprostsze ale też niezbyt sprytne (mało które obiekty naprawdę potrzebują updatu w każdej ramce, wystarczą odpowiedzi na eventy).

Cytuj
2. Nie ma sensu wyprowadzać wszystkiego od jednej klasy bazowej bez powodu. Jednostki i elementy GUI mają się do siebie nijak.
Akurat jeśli mówimy o C++ to taki powód łatwo znaleźć - sensowne, silne RTTI. Może wciąganie do tego GUI jest faktycznie trochę przesadne, ale już uwspólnienie wszystkich serializowanych/replikowanych/whatever klas jest wygodne. A już zwłaszcza obiekty gameplayowe na levelu które i tak pewnie nie są tworzone zwykle z kodu tylko deserializowane przy ładowaniu mapy :)

Cytuj
Ajć, to powinno być już definiowane danymi - klasa Enemy (a jeszcze lepiej Entity (w sensie jednostka, nie "cokolwiek" jak wymyślił autor tematu)) powinna IMHO trzymać wskaźnik na wzorzec gnoma (obiekt, nie klasę) i dane konkretnej instancji gnoma.
Różny zestaw danych to jedno, gorzej z zachowaniami. Definiowanie zachowania przez dane to już raczej inna dyskusja i dotyczy raczej AI niż organizacji całości kodu gameplayowego :) A tam to co "zachowuje się prawie tak samo tylko ma różne parametry i parę special case'ów" pewnie szybko się rozjedzie i zamiast na siłę trzymać to razem łatwiej i czytelniej rozdzielić na osobne klasy. Jednym z plusów jest też to, że trudniej coś wtedy zjebać w jednym zmieniając drugie :)

Disclaimer: pisane z perspektywy człowieka który nie hejtuje OOPu i uważa hierarchię polimorficznych klas w kodzie gameplayowym za coś zupełnie normalnego i, o ile to możliwe, wygodnego. Większość przykładów które piętnują OOP w grach na necie to kod typowo enginowy co jest trochę inną bajką i raczej nie dotyczy tego tematu.

Offline Xender

  • Użytkownik

# Wrzesień 15, 2012, 12:55:33
@up - definiowanie zachowań przez dane to skrypty ;-). Chodzi mi o to, że jeśli mamy kilka ogólnych typów wrogów i nie chcemy tego rozszerzać, to piszmy klasę. Ale jak dla każdego gnoma/orka/goblina/krasnoluda iitd. mamy mieć oddzielną klasę, to coś jest źle :P.

Co do rozwiązania, to można wyjść krok dalej niż eventy - system komunikatów (wg. idei opisanej w artykułach - http://warsztat.gd/wiki/Komunikaty). Czyli każdy komunikat ma id, odbiorcę (obiekt, którego dotyczy), czas doręczenia itd.
« Ostatnia zmiana: Wrzesień 15, 2012, 13:01:21 wysłana przez olo16 »

Offline Nsuidara

  • Użytkownik
    • Site

# Wrzesień 16, 2012, 02:34:39
Ale jak dla każdego gnoma/orka/goblina/krasnoluda iitd. mamy mieć oddzielną klasę, to coś jest źle :P.
Zgadzam się totalnie.

Zasadniczo jak jest robione AI dla kupców, nie jest robiony skrypt Kowal / Alchemik itp tylko i wyłącznie Vendor
a gdzieś tam w pliku gdzie definiuje się jednostki daje się Imie, Typ, Lista Sprzedwania / Skupowania, AI ...

Tak samo tutaj nie robi się Ork, Kowal, Ptak... można nazwać npc, "NPC" ... itp i tam odpowiednio wszystko zamieszczać