Porady dotyczące architektury / wzorców projektowych gry

16

Od jakiegoś czasu pracuję nad 2d RPG i zdałem sobie sprawę, że podjąłem złe decyzje projektowe. W szczególności jest kilka rzeczy, które powodują mi problemy, więc zastanawiałem się, jakie projekty zastosowali inni ludzie.

Dla małego tła zacząłem nad tym pracować w wolnym czasie zeszłego lata. Początkowo tworzyłem grę w C #, ale około 3 miesiące temu zdecydowałem się przejść na C ++. Chciałem dobrze opanować C ++, ponieważ minęło trochę czasu, odkąd intensywnie go użyłem, i pomyślałem, że taki ciekawy projekt byłby dobrym motywatorem. Używam biblioteki doładowań intensywnie i używam SFML do grafiki i FMOD do audio.

Napisałem sporo kodu, ale rozważam złomowanie go i zacząć od nowa.

Oto główne obszary moich obaw i chciałem uzyskać opinie na temat tego, w jaki sposób inni je rozwiązali lub rozwiązali.

1. Zależności cykliczne Kiedy grałem w C #, tak naprawdę nie musiałem się tym przejmować, ponieważ nie stanowi to problemu. Przejście na C ++ stało się dość poważnym problemem i sprawiło, że pomyślałem, że mogłem źle zaprojektować. Naprawdę nie wyobrażam sobie, jak rozdzielić moje klasy i nadal pozwolić im robić, co chcę. Oto kilka przykładów łańcucha zależności:

Mam klasę efektu statusu. Klasa ma wiele metod (Zastosuj / Odrzuć, Zaznacz itp.), Aby zastosować swoje efekty przeciwko postaci. Na przykład,

virtual void TickCharacter(Character::BaseCharacter* character, Battles::BattleField *field, int ticks = 1);

Funkcje te będą wywoływane za każdym razem, gdy postać zadająca efekt statusu skręci. Byłby użyteczny do zaimplementowania efektów takich jak Regen, Trucizna itp. Jednak wprowadza również zależności od klasy BaseCharacter i klasy BattleField. Oczywiście klasa BaseCharacter musi śledzić, jakie efekty statusu są na nich obecnie aktywne, więc jest to zależność cykliczna. Pole bitwy musi śledzić walczące drużyny, a klasa drużyn ma listę postaci bazowych, które wprowadzają kolejną cykliczną zależność.

2 - Wydarzenia

W C # obszernie korzystałem z delegatów, aby dołączyć do wydarzeń na postaciach, polach bitewnych itp. (Na przykład, był delegat, kiedy zmienia się zdrowie postaci, kiedy zmienia się statystyka, kiedy dodaje się / usuwa efekt statusu itp. .), a pole bitwy / elementy graficzne zaczepiłyby tych delegatów, aby wymusić ich efekty. W C ++ zrobiłem coś podobnego. Oczywiście nie ma bezpośredniego odpowiednika dla delegatów C #, więc zamiast tego stworzyłem coś takiego:

typedef boost::function<void(BaseCharacter*, int oldvalue, int newvalue)> StatChangeFunction;

i w mojej klasie postaci

std::map<std::string, StatChangeFunction> StatChangeEventHandlers;

za każdym razem, gdy zmienia się statystyka postaci, powtarzam i wywołuję każdą funkcję StatChangeFunction na mapie. Chociaż działa, martwię się, że jest to złe podejście do robienia rzeczy.

3 - Grafika

To jest wielka sprawa. Nie jest związany z biblioteką graficzną, której używam, ale jest bardziej konceptualny. W języku C # połączyłem grafikę z dużą ilością moich zajęć, co jest okropnym pomysłem. Chcąc to zrobić, tym razem odsprzęgłem się i spróbowałem innego podejścia.

Aby zaimplementować moją grafikę, wyobrażałem sobie wszystkie grafiki związane z grą jako serię ekranów. Tj. Jest ekran tytułowy, ekran statusu postaci, ekran mapy, ekran ekwipunku, ekran bitwy, ekran GUI bitwy, i zasadniczo mogłem układać te ekrany jeden na drugim, jeśli to konieczne, aby stworzyć grafikę gry. Niezależnie od tego, który ekran jest aktywny, posiada dane wejściowe do gry.

Zaprojektowałem menedżera ekranu, który przesuwałby i popowywał ekrany na podstawie danych wprowadzonych przez użytkownika.

Na przykład, jeśli byłeś na ekranie mapy (moduł obsługi / wizualizatora mapy kafelkowej) i nacisnąłeś przycisk Start, wywołałoby to menedżera ekranu, aby przesunąć ekran Menu głównego na ekran mapy i zaznaczyć mapę ekran, który nie będzie rysowany / aktualizowany. Odtwarzacz będzie poruszał się po menu, które wydawałoby więcej poleceń menedżerowi ekranu, stosownie do potrzeb, wsuwając nowe ekrany na stos ekranów, a następnie wstawiając je, gdy użytkownik zmienia ekrany / anuluje. W końcu, gdy gracz wyjdzie z menu głównego, odsunę go i wrócę do ekranu mapy, zaznaczę, że należy go narysować / zaktualizować i stamtąd.

Ekrany bitewne byłyby bardziej złożone. Miałbym ekran działający jako tło, ekran do wizualizacji każdej ze stron w bitwie oraz ekran do wizualizacji interfejsu użytkownika dla bitwy. Interfejs użytkownika łączyłby się ze zdarzeniami postaci i używał ich do określania, kiedy należy zaktualizować / przerysować składniki interfejsu użytkownika. Wreszcie każdy atak, który ma dostępny skrypt animacji, wywoływałby dodatkową warstwę w celu animacji przed wyskoczeniem ze stosu ekranu. W takim przypadku każda warstwa jest konsekwentnie oznaczana jako ciągniona i aktualizowana, a ja otrzymuję stos ekranów obsługujących moją grafikę bitewną.

Chociaż nie udało mi się jeszcze sprawić, by menedżer ekranu działał idealnie, myślę, że mogę z czasem. Moje pytanie brzmi: czy jest to w ogóle opłacalne podejście? Jeśli to zły projekt, chcę wiedzieć teraz, zanim zainwestuję zbyt dużo czasu w tworzenie wszystkich ekranów, których będę potrzebować. Jak tworzysz grafikę dla swojej gry?

użytkownik127817
źródło

Odpowiedzi:

15

Ogólnie rzecz biorąc, nie powiedziałbym, że nic, co wymieniłeś, powinno spowodować, że złomujesz system i zaczniesz od nowa. Jest to coś, co każdy programista chce zrobić w około 50-75% przez każdy projekt, nad którym pracują, ale prowadzi to do niekończącego się cyklu rozwoju i niczego nie kończącego. W tym celu niektóre informacje zwrotne na temat każdej sekcji.

  1. Może to stanowić problem, ale zwykle jest bardziej irytujące niż cokolwiek innego. Czy używasz #pragmy raz lub #ifndef MY_HEADER_FILE_H # zdefiniować MY_HEADER_FILE_H ... #endif odpowiednio u góry lub otaczając pliki .h? W ten sposób plik .h istnieje tylko raz w ramach każdego zakresu? Jeśli tak, moim zaleceniem jest usunięcie wszystkich instrukcji #include i kompilacja, dodanie tych, które są potrzebne do ponownej kompilacji gry.

  2. Jestem fanem tego typu systemów i nie widzę w tym nic złego. Co to jest zdarzenie w C #, zwykle jest zastępowane przez system zdarzeń lub system przesyłania wiadomości (można wyszukiwać tutaj pytania, aby znaleźć więcej informacji). Kluczem tutaj jest ograniczenie ich do minimum, gdy coś musi się wydarzyć, co już brzmi, jakbyś to robił, nie powinno to oznaczać minimalnych zmartwień.

  3. Wydaje mi się to również na dobrej drodze i to właśnie robię dla własnych silników, zarówno osobiście, jak i zawodowo. To sprawia, że ​​system menu staje się systemem stanu, który albo ma menu główne (przed rozpoczęciem gry), albo HUD odtwarzacza jako wyświetlany ekran „root”, w zależności od konfiguracji.

Podsumowując, nie widzę niczego, co mogłoby się wznowić w tym, na co się natrafisz. Możesz chcieć bardziej formalnej wymiany systemu wydarzeń na drodze, ale to przyjdzie z czasem. Cykliczne dołączanie jest przeszkodą dla wszystkich programistów C / C ++, którzy muszą nieustannie przeskakiwać, a praca nad rozdzieleniem grafiki wydaje się być logicznym „kolejnym krokiem”.

Mam nadzieję że to pomoże!

James
źródło
#ifdef nie pomaga w dołączaniu problemów w okólniku.
Kaczka komunistyczna
Właśnie obejmowałem moją podstawę oczekiwaniem, że będę tam, zanim wyśledzę cykliczne dołączenia. Może być zupełnie innym kotłem ryb, gdy masz wiele definicji symboli, w przeciwieństwie do pliku, który musi zawierać plik, który zawiera siebie. (choć z tego, co opisał, jeśli dołączenia znajdują się w plikach .CPP, a nie w plikach .H, powinien być w porządku z dwoma obiektami podstawowymi znającymi się nawzajem)
James
Dzięki za radę :) Cieszę się, że jestem na dobrej drodze
użytkownik127817
4

Twoje cykliczne zależności nie powinny stanowić problemu, dopóki deklarujesz klasy, w których możesz to zrobić w plikach nagłówkowych i faktycznie #włączasz je do plików .cpp (lub cokolwiek innego).

W przypadku systemu wydarzeń dwie sugestie:

1) Jeśli chcesz zachować wzorzec, którego teraz używasz, rozważ zmianę na boost :: unordered_map zamiast std :: map. Mapowanie za pomocą ciągów jako kluczy jest wolne, zwłaszcza że .NET robi fajne rzeczy pod maską, aby przyspieszyć. Używanie haszowania nieuporządkowanej ciągów powoduje, że porównania są na ogół szybsze.

2) Zastanów się nad przejściem na coś mocniejszego, na przykład boost :: sygnały. Jeśli to zrobisz, możesz zrobić coś fajnego, na przykład sprawić, by śledzić obiekty w grze, czerpiąc z boost :: signal :: trackable, i pozwolić destruktorowi zająć się czyszczeniem wszystkiego, zamiast konieczności ręcznego wyrejestrowania się z systemu zdarzeń. Możesz również mieć wiele sygnałów wskazujących na każde gniazdo (lub odwrotnie, nie pamiętam dokładnej nomenklatury), więc jest to bardzo podobne do działania +=na delegateC #. Największy problem z boost :: signal polega na tym, że należy go skompilować, to nie są tylko nagłówki, więc w zależności od platformy uruchomienie i uruchomienie może być uciążliwe.

Tetrad
źródło