Chcę pozbyć się mojego statycznego i globalnego wzorca projektowania, ale jak?

11

Robię małego robota lochowego w kosmosie i chciałbym usłyszeć porady, jak ulepszyć backend silnika. Zasadniczo obecnie wszystko opiera się na crapload menedżerów:

  • BackgroundManager: ma AddBackground(image, parallax)metodę tworzenia fajnych efektów tła.
  • ConfigManager: czyta / tworzy plik konfiguracyjny, a także przechowuje dane odczytane z tego pliku konfiguracyjnego.
  • DrawManager: ma Draw(sprite)metodę rysowania rzeczy na ekranie SetView(view), GetView()itp.
  • EntityManager: przechowuje wszystkie aktywne encje i ma metody dodawania, usuwania i wyszukiwania encji.
  • DungeonManager (w rzeczywistości nazywa się GridManager, ale jest to dla uproszczenia): ma podobne metody GenerateDungeon(), PlaceEnemies(), PlaceFinish()itd.

Teraz nie jestem nawet w połowie listy wszystkich moich menedżerów, więc myślę, że to musi się zmienić. Moja gra jest również oparta na ekranach, co czyni ją bardziej irytującą, ponieważ nie potrzebuję połowy rzeczy, którymi zarządzają moi menedżerowie, na przykład menu głównego (nie potrzebuję fizyki / bytów / lochów w moim menu głównym !)

Teraz pomyślałem o tym, aby menedżerowie nie byli statyczni i aby moja ScreenMainGame była instancją wszystkich menedżerów specyficznych dla gry. Ale to sprawia, że ​​dzwonienie / zdobywanie czegokolwiek związanego z menedżerami jest ogromnym bałaganem ... na przykład, gdybym chciał narysować lochy z dowolnego miejsca, w którym nie ma ScreenMainGame.Draw(), musiałbym to zrobić ...

((ScreenMainGame)ScreenManager.CurrentScreen).GridManager.Draw()

To naprawdę brzydkie.

Zatem, twórcy gier, czy ktoś z was ma pomysł, jak rozwiązać ten bałagan? Byłbym doceniony!

Dlaor
źródło
Nie jestem pewien, jak ci pomóc, ale polecam przeczytanie dobrej książki o wzorach projektowych. Czytałem Head First Design Patterns i bardzo łatwo to zrozumieć. Przykłady są w Javie, ale myślę, że byłoby to wystarczająco blisko C #, aby ci pomóc. Przepraszam, jeśli moja sugestia jest zbyt nowicjuszem.
Amplify91,
Czego używasz do interfejsu z kartą graficzną? XNA? SlimDX?
BlueRaja - Danny Pflughoeft
@BlueRaja: Używam SFML.NET.
Dlaor
W takim przypadku powinieneś po prostu użyć normalnych metod radzenia sobie z tymi problemami w C #: Zobacz mój ostatni link poniżej, a także Co to jest wstrzyknięcie zależności?
BlueRaja - Danny Pflughoeft

Odpowiedzi:

10

Co z silnikiem opartym na komponentach ?

Miałbyś główną klasę o nazwie Engine, która prowadziłaby listę GameScreens, która sama posiadałaby listę Components.

Silnik posiada Updateoraz Drawmetody i zarówno zadzwonić do GameScreen„s Updatei Drawmetod, które same przejść przez każdego komponentu i rozmowy Updatei Draw.

Tak przedstawiony, zgadzam się, że brzmi jak kiepski i powtarzalny projekt. Ale uwierz mi, mój kod stał się znacznie czystszy dzięki zastosowaniu podejścia opartego na komponentach niż w przypadku wszystkich moich starych klas menedżerów .

Utrzymanie takiego kodu również jest o wiele prostsze, ponieważ przechodzisz właśnie przez dużą hierarchię klas i nie musisz przeszukiwać BackgroundManagerwszystkich różnych środowisk. Po prostu mieć ScrollingBackground, ParallaxBackground, StaticBackground, itd., Które wywodzą się z Backgroundklasy.

W końcu zbudujesz całkiem solidny silnik, który możesz ponownie wykorzystać we wszystkich swoich projektach z wieloma często używanymi komponentami i metodami pomocniczymi (np. FrameRateDisplayerJako narzędzie do debugowania,Sprite klasa jako podstawowy duszek z metodami tekstury i rozszerzeń dla wektorów i generowanie liczb losowych).

Nie miałbyś już BackgroundManagerklasy, ale Backgroundklasę, która sama sobie poradziłaby.

Po rozpoczęciu gry wszystko, co musisz zrobić, to w zasadzie:

// when declaring variables:
Engine engine;

// when initializing:
engine = new Engine();
engine.Initialize();
engine.LoadContent();
engine.AddGameScreen(new MainMenuScreen());

// when updating:
engine.Update();

// when drawing:
engine.Draw();

I to wszystko w przypadku kodu początkowego gry.

Następnie na ekranie menu głównego:

class MainMenuScreen : MenuScreen // where MenuScreen derives from the GameScreen class
{
    const int ENEMY_COUNT = 10;

    StaticBackground background;
    Player player;
    List<Enemy> enemies;

    public override void Initialize()
    {
        background = new StaticBackground();
        player = new Player();
        enemies = new List<Enemy>();

        base.AddComponent(background); // defined within the GameScreen class
        base.AddComponent(player);
        for (int i = 0; i < ENEMY_COUNT; ++i)
        {
            Enemy newEnemy = new Enemy();
            enemies.Add(newEnemy);
            base.AddComponent(newEnemy);
        }
    }
}

Masz ogólny pomysł.

Zachowałbyś również odniesienie do Enginewszystkich swoich GameScreenklas, aby móc dodawać nowe ekrany nawet w obrębie GameScreenklasy (np. Kiedy użytkownik kliknie przycisk StartGame w twojej klasie MainMenuScreen, możesz przejść do GameplayScreen).

To samo dotyczy Componentklasy: powinna zawierać referencję swojego rodzica GameScreen, aby mieć zarówno dostęp do Engineklasy, jak i jej rodzica, GameScreenaby dodać nowe komponenty (np. Możesz stworzyć klasę związaną z interfejsem HUD o nazwie, DrawableButtonktóra zawiera DrawableTextkomponent i StaticBackgroundkomponent).

Następnie możesz zastosować inne wzorce projektowe, takie jak „wzorzec projektowy usługi” (nie jestem pewien dokładnej nazwy), w którym możesz przechowywać różne przydatne usługi w swojej Engineklasie (po prostu przechowujesz listę IServices, a inne klasy mogą same dodawać usługi ). np. zachowałbym Camera2Dkomponent w całym moim projekcie jako usługę, aby zastosować jego transformację podczas rysowania innych komponentów. Pozwala to uniknąć konieczności przekazywania go jako parametru wszędzie.

Podsumowując, z pewnością mogą istnieć inne lepsze konstrukcje silnika, ale uznałem, że silnik zaproponowany przez to łącze jest bardzo elegancki, niezwykle łatwy w konserwacji i wielokrotnego użytku. Osobiście poleciłbym przynajmniej spróbować.

Jesse Emond
źródło
1
XNA obsługuje wszystkie te gotowe produkty za pośrednictwem GameComponentusług i usług. Nie ma potrzeby oddzielnej Engineklasy.
BlueRaja - Danny Pflughoeft
+1 za tę odpowiedź. Czy ma sens umieszczanie rysunku w osobnym wątku niż wątek aktualizacji gry?
f20k
1
@BlueRaja - DannyPflughoeft: Rzeczywiście, ale założyłem, że XNA nie był używany, więc wyjaśniłem trochę więcej, jak to zrobić.
Jesse Emond
@ f20k: Tak, prawie w ten sam sposób, w jaki robi to XNA. Nie wiem jednak, jak to się realizuje. = / Musi być gdzieś w Internecie artykuł o tym, jak to zrobić. :)
Jesse Emond
Tak, nie używam XNA, ale wydaje się to miłą alternatywą do tego, co robię. Obecnie czytam książkę „Head First Design Patterns”, aby sprawdzić, czy są jakieś alternatywy. Twoje zdrowie!
Dlaor
1

To, co chcesz zrobić, to podzielić kod na komponenty i usługi. XNA ma już wbudowane wsparcie dla nich.

Zobacz ten artykuł na GameComponentsi usług. Następnie zapoznaj się z tą odpowiedzią na temat korzystania z usług w XNA i bardziej ogólną odpowiedzią na usługi w dowolnym języku.

BlueRaja - Danny Pflughoeft
źródło
-3

To, że są statyczne lub globalne, nie oznacza, że ​​zawsze muszą być ładowane / inicjowane. Użyj wzorca Singleton i pozwól menedżerom być darmowym i ładować je ponownie na żądanie.

Bo Buchanan
źródło
4
Singleton jest tylko fasadą leżącą u podstaw problemu, który OP próbuje tutaj rozwiązać.