Kiedy kilka klas potrzebuje dostępu do tych samych danych, gdzie należy je zadeklarować?

39

Mam podstawową grę obrony wieży 2D w C ++.

Każda mapa jest oddzielną klasą, która dziedziczy po GameState. Mapa deleguje logikę i kod rysunkowy do każdego obiektu w grze i ustawia dane, takie jak ścieżka mapy. W pseudokodzie sekcja logiki może wyglądać mniej więcej tak:

update():
  for each creep in creeps:
    creep.update()
  for each tower in towers:
    tower.update()
  for each missile in missiles:
    missile.update()

Obiekty (skrada się, wieże i pociski) są przechowywane w wektorze wskaźników. Wieże muszą mieć dostęp do wektora pełzania i wektora pocisków, aby tworzyć nowe pociski i identyfikować cele.

Pytanie brzmi: gdzie mam zadeklarować wektory? Czy powinni należeć do klasy Map i przekazywani jako argumenty do funkcji tower.update ()? Czy zadeklarowany globalnie? Czy są też inne rozwiązania, których zupełnie mi brakuje?

Kiedy kilka klas potrzebuje dostępu do tych samych danych, gdzie należy je zadeklarować?

Soczysty
źródło
1
Członkowie globalni są uważani za „brzydkich”, ale są szybcy i ułatwiają rozwój, jeśli jest to mała gra, to nie jest problem (IMHO). Możesz także stworzyć klasę zewnętrzną, która obsługuje logikę ( dlaczego wieże potrzebują tych wektorów) i ma dostęp do wszystkich wektorów.
Jonathan Connell
-1, jeśli jest to związane z programowaniem gier, to również jedzenie pizzy. Zdobądź kilka dobrych książek o projektowaniu oprogramowania
Maik Semder
9
@ Maik: W jaki sposób projektowanie oprogramowania nie ma związku z programowaniem gier? To, że dotyczy to również innych dziedzin programowania, nie czyni z niego tematu.
BlueRaja - Danny Pflughoeft
@BlueRaja listy wzorców projektowych oprogramowania lepiej pasują do SO, po to właśnie jest. GD.SE służy do programowania gier, a nie do projektowania oprogramowania
Maik Semder

Odpowiedzi:

53

Gdy potrzebujesz jednego wystąpienia klasy w całym programie, nazywamy tę klasę usługą . Istnieje kilka standardowych metod wdrażania usług w programach:

  • Zmienne globalne . Są to najłatwiejsze do wdrożenia, ale najgorsze wzornictwo. Jeśli użyjesz zbyt wielu zmiennych globalnych, szybko odkryjesz, że piszesz moduły, które zbyt mocno na sobie polegają ( silne sprzężenie ), co bardzo utrudnia przepływ logiki. Zmienne globalne nie są przyjazne dla wielowątkowości. Zmienne globalne utrudniają śledzenie żywotności obiektów i zaśmiecają przestrzeń nazw. Są jednak najbardziej wydajną opcją, więc są chwile, kiedy mogą i powinny być używane, ale używaj ich oszczędnie.
  • Singletony . Około 10-15 lat temu, singletons były duży projekt, wzór wiedzieć. Jednak w dzisiejszych czasach patrzy się na nie z góry. Są one znacznie łatwiejsze do wielowątkowego, ale musisz ograniczyć ich użycie do jednego wątku na raz, co nie zawsze jest tym, czego chcesz. Śledzenie żywotności jest tak samo trudne, jak w przypadku zmiennych globalnych. Typowa klasa singleton będzie wyglądać mniej więcej tak:

    class MyClass
    {
    private:
        static MyClass* _instance;
        MyClass() {} //private constructor
    
    public:
        static MyClass* getInstance();
        void method();
    };
    
    ...
    
    MyClass* MyClass::_instance = NULL;
    MyClass* MyClass::getInstance()
    {
        if(_instance == NULL)
            _instance = new MyClass(); //Not thread-safe version
        return _instance;
    
        //Note that _instance is *never* deleted - 
        //it exists for the entire lifetime of the program!
    }
  • Wstrzykiwanie zależności (DI) . Oznacza to po prostu przekazanie usługi jako parametru konstruktora. Usługa musi już istnieć, aby przenieść ją do klasy, więc nie można polegać na dwóch usługach; w 98% przypadków tego właśnie chcesz (a dla pozostałych 2% zawsze możesz utworzyć setWhatever()metodę i przekazać ją później) . Z tego powodu DI nie ma takich samych problemów ze sprzężeniem, jak inne opcje. Można go używać z wielowątkowością, ponieważ każdy wątek może po prostu mieć własną instancję każdej usługi (i udostępniać tylko te, których absolutnie potrzebuje). Umożliwia to również testowanie jednostki kodu, jeśli na tym zależy.

    Problem z wstrzykiwaniem zależności polega na tym, że zajmuje więcej pamięci; teraz każda instancja klasy potrzebuje referencji do każdej usługi, z której będzie korzystać. Korzystanie z zbyt wielu usług jest denerwujące. istnieją frameworki, które łagodzą ten problem w innych językach, ale z powodu braku refleksji w C ++, frameworki DI w C ++ wydają się być jeszcze więcej pracy niż tylko ręczne tworzenie.

    //Example of dependency injection
    class Tower
    {
    private:
        MissileCreationService* _missileCreator;
        CreepLocatorService* _creepLocator;
    public:
        Tower(MissileCreationService*, CreepLocatorService*);
    }
    
    //In order to create a tower, the creating-class must also have instances of
    // MissileCreationService and CreepLocatorService; thus, if we want to 
    // add a new service to the Tower constructor, we must add it to the
    // constructor of every class which creates a Tower as well!
    //This is not a problem in languages like C# and Java, where you can use
    // a framework to create an instance and inject automatically.

    Zobacz tę stronę (z dokumentacji Ninject, frameworka C # DI) dla innego przykładu.

    Wstrzykiwanie zależności jest typowym rozwiązaniem tego problemu i jest odpowiedzią, którą zobaczysz najbardziej wysoko postawiony na takie pytania na StackOverflow.com. DI jest rodzajem Inwersji Kontroli (IoC).

  • Lokalizator usług . Zasadniczo, tylko klasa, która przechowuje instancję każdej usługi. Możesz to zrobić za pomocą refleksji lub możesz po prostu dodać do niej nową instancję za każdym razem, gdy chcesz utworzyć nową usługę. Nadal masz ten sam problem, co wcześniej - w jaki sposób klasy uzyskują dostęp do tego lokalizatora? - które można rozwiązać na jeden z powyższych sposobów, ale teraz musisz to zrobić tylko dla swojej ServiceLocatorklasy, a nie dla dziesiątek usług. Ta metoda jest również testowalna jednostkowo, jeśli zależy Ci na tego rodzaju rzeczach.

    Lokalizatory usług to kolejna forma Inwersji Kontroli (IoC). Zazwyczaj środowiska wykonujące automatyczne wstrzykiwanie zależności będą miały także lokalizator usług.

    XNA (platforma programowania gier C # firmy Microsoft) zawiera lokalizator usług; Aby dowiedzieć się więcej na ten temat, zobacz tę odpowiedź .


Nawiasem mówiąc, wieże IMHO nie powinny wiedzieć o stworach. O ile nie planujesz po prostu przewijać listy pełzaczy dla każdej wieży, prawdopodobnie będziesz chciał wdrożyć nietrywialne partycjonowanie przestrzeni ; i tego rodzaju logika nie należy do klasy wież.

BlueRaja - Danny Pflughoeft
źródło
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
Josh
Jedna z najlepszych, najczystszych odpowiedzi, jakie kiedykolwiek przeczytałem. Dobra robota. Myślałem, że usługa powinna być zawsze udostępniana.
Nikos,
5

Osobiście użyłbym tutaj polimorfizmu. Po co missilewektor, towerwektor i creepwektor ... kiedy wszystkie wywołują tę samą funkcję; update? Dlaczego nie mieć wektora wskaźników do jakiejś klasy bazowej Entitylub GameObject?

Uważam, że dobrym sposobem na projektowanie jest myślenie „czy to ma sens pod względem własności”? Oczywiście wieża ma sposób na aktualizację, ale czy mapa posiada wszystkie obiekty na niej? Jeśli wybierasz globalny, to czy mówisz, że nic nie jest właścicielem wież i skrada się? Globalne jest zwykle złym rozwiązaniem - promuje złe wzorce projektowe, ale znacznie łatwiej jest z nim pracować. Zastanów się nad ważeniem „czy chcę to zakończyć?” i „czy chcę czegoś, co mogę ponownie wykorzystać”?

Jednym ze sposobów obejścia tego jest jakaś forma systemu przesyłania wiadomości. towerMoże wysłać wiadomość do map(których ma dostęp, być może odniesienie do jego właściciela?), Że to hit creep, a mapnastępnie opowiada creepto był hit. Jest to bardzo czyste i segreguje dane.

Innym sposobem jest po prostu przeszukiwanie samej mapy w poszukiwaniu tego, czego chce. Mogą tu jednak występować problemy z kolejnością aktualizacji.

Kaczka komunistyczna
źródło
1
Twoja sugestia na temat polimorfizmu nie jest tak naprawdę istotna. Przechowuję je w osobnych wektorach, dzięki czemu mogę iterować osobno po każdym typie, na przykład w kodzie rysunkowym (gdzie najpierw chcę narysować określone obiekty) lub w kodzie kolizyjnym.
Juicy
Dla moich celów mapa jest właścicielem encji, ponieważ mapa tutaj jest analogiczna do „poziomu”. Rozważę twój pomysł na wiadomości, dzięki.
Juicy
1
W grze liczy się wydajność. Zatem wektory tego samego czasu obiektu mają lepszą lokalizację odniesienia. Ponadto obiekty polimorficzne z wirtualnymi wskaźnikami mają niesamowitą wydajność, ponieważ nie można ich wprowadzić do pętli aktualizacji.
Zan Lynx
0

Jest to przypadek, w którym rozpada się ścisłe programowanie obiektowe (OOP).

Zgodnie z zasadami OOP należy grupować dane z powiązanymi zachowaniami za pomocą klas. Ale masz zachowanie (targetowanie), które potrzebuje danych, które nie są ze sobą powiązane (wieże i stwory). W tej sytuacji wielu programistów będzie próbowało powiązać zachowanie z częścią potrzebnych danych (np. Wieże obsługują celowanie, ale nie wiedzą o pełzaniu), ale jest też inna opcja: nie grupuj zachowania z danymi.

Zamiast uczynić zachowanie targetowania metodą klasy tower, uczyń z niego bezpłatną funkcję, która przyjmuje wieże i pełznie jako argumenty. Może to wymagać upublicznienia większej liczby członków, którzy pozostali w wieży i klas pełzania . Ukrywanie danych jest przydatne, ale jest środkiem, a nie celem samym w sobie i nie powinieneś być do niego niewolnikiem. Poza tym prywatni członkowie nie są jedynym sposobem kontrolowania dostępu do danych - jeśli dane nie są przekazywane do funkcji i nie są globalne, są skutecznie ukryte przed tą funkcją. Jeśli użycie tej techniki pozwala uniknąć globalnych danych, być może poprawia się enkapsulacja.

Skrajnym przykładem tego podejścia jest architektura systemu encji .

Steve S.
źródło