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?
źródło
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
+=
nadelegate
C #. 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.źródło