Pracuję nad grą 2D, w której możesz poruszać się w górę, w dół, w lewo i w prawo. Mam zasadniczo dwa obiekty logiki gry:
- Gracz: ma pozycję względem świata
- Świat: Rysuje mapę i gracza
Jak dotąd świat zależy od gracza (tzn. Ma odniesienie do niego), potrzebuje swojej pozycji, aby dowiedzieć się, gdzie narysować postać gracza i jaką część mapy narysować.
Teraz chcę dodać wykrywanie kolizji, aby uniemożliwić graczowi poruszanie się przez ściany.
Najprostszym sposobem, jaki mogę wymyślić, jest poproszenie gracza o spytanie świata, czy zamierzony ruch jest możliwy. Wprowadziłoby to jednak cykliczną zależność między Graczem a Światem (tzn. Każda zawiera odniesienie do drugiej), co wydaje się warte uniknięcia. Jedynym sposobem, w jaki wpadłem na pomysł, jest to, żeby Świat poruszył Graczem , ale wydaje mi się to nieco nieintuicyjne.
Jaka jest moja najlepsza opcja? A może unikanie cyklicznej zależności nie jest tego warte?
Odpowiedzi:
Świat nie powinien się rysować; Renderujący powinien narysować Świat. Gracz nie powinien się losować; Renderujący powinien narysować Gracza względem Świata.
Gracz powinien zapytać Świat o wykrycie kolizji; a może kolizje powinny być obsługiwane przez osobną klasę, która sprawdzałaby wykrywanie kolizji nie tylko ze statycznym światem, ale także z innymi aktorami.
Myślę, że Świat prawdopodobnie nie powinien być świadomy Gracza; powinien to być prymityw na niskim poziomie, a nie obiekt boga. Gracz prawdopodobnie będzie musiał wywołać niektóre metody świata, być może pośrednio (wykrywanie kolizji lub sprawdzanie interaktywnych obiektów itp.).
źródło
Renderer
wymaga pewnego rodzaju, ale to nie znaczy, że logika, w jaki sposób każda rzecz jest renderowana, jest obsługiwana przezRenderer
, każda rzecz, którą należy narysować, powinna prawdopodobnie odziedziczyć po wspólnym interfejsie, takim jakIDrawable
lubIRenderable
(lub równoważny interfejs w dowolnym języku). Świat może byćRenderer
, jak sądzę, ale wygląda na to, że przekroczyłby swoją odpowiedzialność, zwłaszcza gdyby był jużIRenderable
sobą.Oto jak typowy silnik renderujący obsługuje te rzeczy:
Istnieje zasadnicza różnica między miejscem, w którym obiekt znajduje się w przestrzeni, a sposobem jego rysowania.
Rysowanie obiektu
Zwykle masz klasę renderującą , która to robi. Po prostu bierze obiekt (Model) i rysuje na ekranie. Może mieć metody takie jak drawSprite (Sprite), drawLine (..), drawModel (Model), co tylko chcesz. Jest to Renderer, więc powinien robić wszystkie te rzeczy. Używa również dowolnego interfejsu API, który masz pod spodem, dzięki czemu możesz na przykład mieć renderer korzystający z OpenGL i taki, który korzysta z DirectX. Jeśli chcesz przenieść swoją grę na inną platformę, po prostu napisz nowy renderer i użyj go. To jest takie proste.
Przenoszenie obiektu
Każdy obiekt jest dołączony do czegoś, co lubimy nazywać Sceną . Osiągasz to poprzez kompozycję. SceneNode zawiera obiekt. Otóż to. Co to jest SceneNode? Jest to prosta klasa zawierająca wszystkie przekształcenia (położenie, obrót, skalę) obiektu (zwykle względem innego obiektu SceneNode) wraz z rzeczywistym obiektem.
Zarządzanie obiektami
Jak zarządzane są SceneNodes? Za pomocą menedżera scen . Ta klasa tworzy i śledzi każdy SceneNode w twojej scenie. Możesz poprosić o konkretny SceneNode (zwykle identyfikowany przez ciąg znaków, taki jak „Player” lub „Table”) lub listę wszystkich węzłów.
Rysowanie świata
To powinno być już dość oczywiste. Po prostu przejdź przez każdy węzeł SceneNode w scenie i poproś Renderer, aby narysował go we właściwym miejscu. Możesz narysować go we właściwym miejscu, ponieważ renderer przechowuje przekształcenia obiektu przed jego renderowaniem.
Wykrywanie kolizji
To nie zawsze jest banalne. Zwykle można zapytać scenę o to, jaki obiekt znajduje się w pewnym punkcie przestrzeni lub jakie obiekty przecinają promienie. W ten sposób możesz stworzyć promień z odtwarzacza w kierunku ruchu i zapytać kierownika sceny, jaki jest pierwszy obiekt, który przecina promień. Następnie możesz przesunąć gracza na nową pozycję, przesunąć go o mniejszą ilość (aby zbliżyć go do kolidującego obiektu) lub nie ruszać go wcale. Upewnij się, że te zapytania są obsługiwane przez osobne klasy. Powinni poprosić menedżera SceneManager o listę węzłów SceneNodes, ale kolejnym zadaniem jest ustalenie, czy ten węzeł SceneNode pokrywa punkt w przestrzeni, czy przecina się z promieniem. Pamiętaj, że SceneManager tworzy i zapisuje tylko węzły.
Czym więc jest gracz i jaki jest świat?
Gracz może być klasą zawierającą węzeł SceneNode, który z kolei zawiera model do renderowania. Poruszasz odtwarzaczem, zmieniając pozycję węzła sceny. Świat jest po prostu instancją programu SceneManager. Zawiera wszystkie obiekty (poprzez SceneNodes). Wykrywasz kolizje, wykonując zapytania o bieżący stan sceny.
Jest to dalekie od pełnego lub dokładnego opisu tego, co dzieje się w większości silników, ale powinno pomóc ci zrozumieć podstawy i dlaczego ważne jest przestrzeganie zasad OOP podkreślonych przez SOLID . Nie pogódź się z myślą, że zbyt trudno jest zrestrukturyzować kod lub że tak naprawdę nie pomoże. W przyszłości zyskasz znacznie więcej, starannie projektując swój kod.
źródło
Dlaczego miałbyś tego unikać? Jeśli chcesz stworzyć klasę wielokrotnego użytku, należy unikać zależności cyklicznych. Ale Gracz nie jest klasą, która w ogóle musi być wielokrotnego użytku. Czy kiedykolwiek chciałbyś korzystać z odtwarzacza bez świata? Prawdopodobnie nie.
Pamiętaj, że klasy to nic innego jak kolekcje funkcjonalności. Pytanie brzmi, jak podzielić funkcjonalność. Rób wszystko, co musisz zrobić. Jeśli potrzebujesz dekadencji kołowej, niech tak będzie. (Nawiasem mówiąc, to samo dotyczy wszystkich funkcji OOP. Koduj rzeczy w taki sposób, aby służyły celowi, a nie ślepo przestrzegaj paradygmatów.)
Edytuj
OK, aby odpowiedzieć na pytanie: możesz uniknąć sytuacji, w której gracz musi znać świat w celu sprawdzenia kolizji, korzystając z oddzwaniania:
Świat, który opisałeś w pytaniu, może być obsługiwany przez świat, jeśli ujawnisz prędkość bytów:
Pamiętaj jednak, że prędzej czy później będziesz potrzebować zależności od świata, to znaczy za każdym razem, gdy potrzebujesz funkcjonalności Świata: chcesz wiedzieć, gdzie jest najbliższy wróg? Chcesz wiedzieć, jak daleko jest następna półka? To jest zależność.
źródło
render(World)
. Debata dotyczy tego, czy cały kod powinien być wciśnięty w jedną klasę, czy też kod powinien być podzielony na jednostki logiczne i funkcjonalne, które są łatwiejsze w utrzymaniu, rozszerzaniu i zarządzaniu. BTW, powodzenia w ponownym użyciu tych menedżerów komponentów, silników fizyki i menedżerów danych wejściowych, wszystkie sprytnie niezróżnicowane i całkowicie powiązane.Twój obecny projekt wydaje się być sprzeczny z pierwszą zasadą SOLID .
Ta pierwsza zasada, zwana „zasadą pojedynczej odpowiedzialności”, jest ogólnie dobrą wskazówką, której należy przestrzegać, aby nie tworzyć monolitycznych obiektów typu „wszystko do zrobienia”, które zawsze zaszkodzą Twojemu projektowi.
Konkretując, twój
World
obiekt jest odpowiedzialny zarówno za aktualizację i utrzymanie stanu gry, jak i za rysowanie wszystkiego.Co się stanie, jeśli Twój kod renderowania zmieni się / będzie musiał się zmienić? Dlaczego warto aktualizować obie klasy, które w rzeczywistości nie mają nic wspólnego z renderowaniem? Jak już powiedział Liosan, powinieneś mieć
Renderer
.Teraz, aby odpowiedzieć na twoje aktualne pytanie ...
Można to zrobić na wiele sposobów, a to tylko jeden sposób na oddzielenie płatności:
Object
s, w których znajduje się gracz, ale nie zależy to od klasy gracza (aby to osiągnąć, użyj dziedziczenia).InputManager
.Renderer
Zwraca wszystkie obiekty.źródło
health
jakie ma tylko ta instancjaPlayer
).Gracz powinien zapytać świat o takie rzeczy, jak wykrywanie kolizji. Sposobem na uniknięcie okrągłej zależności jest brak uzależnienia świata od Gracza. Świat musi wiedzieć, gdzie się rysuje: prawdopodobnie chcesz, aby był on bardziej abstrakcyjny, być może z odniesieniem do obiektu Camera, który z kolei może zawierać odniesienie do jakiejś Istoty do śledzenia.
To, czego chcesz uniknąć w odniesieniu do cyklicznych odniesień, to nie tyle trzymanie odniesień do siebie, ale raczej bezpośrednie nawiązywanie do siebie w kodzie.
źródło
Ilekroć dwa różne typy obiektów mogą zadawać sobie pytania. Będą od siebie zależeć, ponieważ muszą mieć odniesienie do drugiego, aby wywołać jego metody.
Możesz uniknąć okrągłej zależności, każąc Światowi pytać Gracza, ale Gracz nie może pytać Świata lub odwrotnie. W ten sposób Świat ma odniesienia do Graczy, ale gracze nie potrzebują odniesienia do Świata. Lub odwrotnie. Ale to nie rozwiąże problemu, ponieważ świat będzie musiał zapytać graczy, czy mają coś o co zapytać, i powiedzieć im w następnym zaproszeniu ...
Tak więc nie można naprawdę obejść tego „problemu” i myślę, że nie trzeba się tym martwić. Niech projekt będzie głupi prosty tak długo, jak to możliwe.
źródło
Po usunięciu szczegółów dotyczących gracza i świata masz prosty przypadek, że nie chcesz wprowadzać okrągłej zależności między dwoma obiektami (które w zależności od twojego języka mogą nawet nie mieć znaczenia, zobacz link w komentarzu Fuhrmanatora). Istnieją co najmniej dwa bardzo proste rozwiązania strukturalne, które można zastosować do tego i podobnych problemów:
1) Wprowadź wzór singletonu do swojej światowej klasy . Umożliwi to graczowi (i każdemu innemu obiektowi) łatwe znalezienie obiektu świata bez kosztownych wyszukiwań lub trwałych łączy. Istotą tego wzorca jest to, że klasa ma statyczne odniesienie do jedynego wystąpienia tej klasy, które jest ustawiane na podstawie wystąpienia obiektu i usuwane po jego usunięciu.
W zależności od języka programowania i złożoności, którą chcesz, możesz łatwo wdrożyć go jako nadklasę lub interfejs i ponownie użyć go dla wielu głównych klas, w których nie spodziewasz się, że będziesz mieć więcej niż jedną w swoim projekcie.
2) Jeśli język, w którym się rozwijasz, obsługuje go (wiele osób to robi), użyj Słabego odniesienia . Jest to odwołanie, które nie wpływa na takie rzeczy, jak zbieranie śmieci. Jest to przydatne w tych właśnie przypadkach, pamiętaj jednak, aby nie przyjmować żadnych założeń dotyczących tego, czy obiekt, do którego odwołujesz się słabo, nadal istnieje.
W twoim szczególnym przypadku, Twoi Gracze mogą mieć słabe odniesienie do świata. Zaletą tego (jak w przypadku singletonu) jest to, że nie trzeba szukać obiektu świata w jakiś sposób w każdej ramce ani mieć stałego odwołania, które utrudni procesy związane z odwołaniami cyklicznymi, takimi jak zbieranie śmieci.
źródło
Jak mówili inni, myślę, że
World
robi jedną rzecz za dużo: to stara się zarówno zawierać gręMap
(która powinna być odrębną jednostką) i byćRenderer
w tym samym czasie.Utwórz nowy obiekt (zwany
GameMap
prawdopodobnie) i zapisz w nim dane na poziomie mapy. Napisz w nim funkcje, które współdziałają z bieżącą mapą.Wtedy też potrzebują
Renderer
obiektu. Państwo mogłoby uczynić tenRenderer
przedmiot rzecz, która zarówno zawieraGameMap
iPlayer
(takżeEnemies
), a także przyciąga je.źródło
Można uniknąć zależności cyklicznych, nie dodając zmiennych jako członków. Użyj statycznej funkcji CurrentWorld () dla odtwarzacza lub czegoś podobnego. Nie należy jednak wymyślać interfejsu innego niż już zaimplementowany w świecie, jest to całkowicie niepotrzebne.
Możliwe jest również zniszczenie referencji przed / podczas niszczenia obiektu gracza, aby skutecznie zatrzymać problemy spowodowane przez cykliczne referencje.
źródło