Jak poprawnie używać singletonów w programowaniu silnika C ++?

16

Wiem, że singletony są złe, mój stary silnik gry używał singletonowego obiektu „Game”, który obsługuje wszystko, od przechowywania wszystkich danych do rzeczywistej pętli gry. Teraz robię nowy.

Problem polega na tym, aby narysować coś w SFML, którego używasz window.draw(sprite)tam, gdzie jest okno sf::RenderWindow. Widzę tutaj 2 opcje:

  1. Zrób singletonowy obiekt gry, który pobiera każdy byt w grze (co użyłem wcześniej)
  2. Uczyń to konstruktorem dla encji: Entity(x, y, window, view, ...etc)(to jest po prostu śmieszne i denerwujące)

Jaki byłby właściwy sposób, aby to zrobić, utrzymując konstruktor encji tylko na x i y?

Mógłbym spróbować śledzić wszystko, co robię w głównej pętli gry, i po prostu ręcznie narysować ich duszka w pętli gry, ale to też wydaje się niechlujne, a także chcę absolutnej pełnej kontroli nad całą funkcją losowania dla bytu.

Akumulator
źródło
1
Możesz przekazać okno jako argument funkcji renderowania.
Dari
25
Singletony nie są złe! mogą być przydatne, a czasem konieczne (oczywiście jest to dyskusyjne).
ExOfDe,
3
Zastąp singletony zwykłymi globalsami. Nie ma sensu tworzyć globalnie wymaganych zasobów „na żądanie” ani przekazywać ich. W przypadku encji możesz jednak użyć klasy „level”, aby przechowywać pewne rzeczy, które są istotne dla nich wszystkich.
snake5
Deklaruję moje okno i inne zależności w moim głównym, a następnie mam wskaźniki w innych klasach.
KaareZ,
1
@JAB Łatwo naprawiony dzięki ręcznej inicjalizacji z main (). Leniwa inicjalizacja sprawia, że ​​dzieje się to w nieznanym momencie, co nigdy nie jest dobrym pomysłem dla systemów podstawowych.
snake5

Odpowiedzi:

3

Przechowuj tylko dane potrzebne do wyrenderowania duszka wewnątrz każdej encji, a następnie pobierz je z encji i przekaż do okna do renderowania. Nie trzeba przechowywać żadnych okien ani przeglądać danych w jednostkach.

Można mieć najwyższego poziomu gry lub silnika klasy, która trzyma poziom klasy (posiada wszystkie podmioty obecnie stosowane), a Renderer klasa (zawiera okna widok i niczego innego do renderowania).

Tak więc pętla aktualizacji gry w klasie najwyższego poziomu może wyglądać następująco:

EntityList entities = mCurrentLevel.getEntities();
for(auto& i : entities){
  // Run game logic...
  i->update(...);
}
// Render all the entities
for(auto& i : entities){
  mRenderer->draw(i->getSprite());
}
Blue Wizard
źródło
3
W singletonie nie ma nic idealnego. Po co upubliczniać wewnętrzne implementacje, jeśli nie trzeba? Po co pisać Logger::getInstance().Log(...)zamiast po prostu Log(...)? Po co inicjować klasę losowo, gdy zapyta się, czy można to zrobić ręcznie tylko raz? Globalna funkcja odwołująca się do globalnych danych statycznych jest o wiele prostsza do utworzenia i użycia.
snake5
@ snake5 Usprawiedliwienie singletonów na Stack Exchange przypomina sympatię Hitlera.
Willy Goat,
30

Proste podejście polega na stworzeniu czegoś, co kiedyś było Singleton<T>globalne T. Globały też mają problemy, ale nie stanowią one dodatkowej pracy i kodu, aby wymusić trywialne ograniczenie. Jest to w zasadzie jedyne rozwiązanie, które nie będzie wymagało (potencjalnie) dotykania konstruktora encji.

Trudniejszym, ale być może lepszym podejściem jest przekazanie swoich zależności tam, gdzie ich potrzebujesz . Tak, może to obejmować przekazanie Window *zbioru obiektów (takich jak Twoja istota) w sposób, który wygląda obrzydliwie. Fakt, że wygląda obrzydliwie, powinien ci coś powiedzieć: twój projekt może być obrzydliwy.

Powodem jest to, że jest to trudniejsze (poza włączeniem większego pisania), ponieważ często prowadzi to do refaktoryzacji interfejsów, tak że to, co „musisz” przekazać, jest potrzebne mniejszej liczbie klas na poziomie liści. Powoduje to, że wiele brzydoty nieodłącznie związanej z przekazywaniem renderera do wszystkiego zniknie, a także poprawia ogólną łatwość konserwacji twojego kodu poprzez zmniejszenie ilości zależności i sprzężeń, których zakres stałeś się bardzo oczywisty, przyjmując zależności jako parametry . Kiedy zależności były singletonami lub globałami, mniej oczywiste było, w jaki sposób wzajemnie powiązane były twoje systemy.

Ale jest to potencjalnie duże przedsięwzięcie. Robienie tego do systemu po fakcie może być wręcz bolesne. Może być o wiele bardziej pragmatyczne, aby po prostu zostawić swój system samemu sobie, z singletonem, na razie (szczególnie jeśli próbujesz faktycznie wysłać grę, która w przeciwnym razie działa dobrze; gracze na ogół nie będą się przejmować, jeśli masz singleton lub cztery tam).

Jeśli chcesz spróbować zrobić to z istniejącym projektem, może być konieczne opublikowanie o wiele więcej szczegółów na temat bieżącej implementacji, ponieważ tak naprawdę nie ma ogólnej listy kontrolnej do wprowadzenia tych zmian. Lub przyjdź i porozmawiaj o tym na czacie .

Z tego, co napisałeś, uważam, że dużym krokiem w kierunku „bez singletonu” byłoby uniknięcie konieczności dostępu twoich bytów do okna lub widoku. Sugeruje to, że same się rysują, a ty nie musisz, by istoty same się rysowały . Możesz przyjąć metodologię, w której podmioty zawierają po prostu informacje, które by na to pozwoliłydo narysowania przez jakiś system zewnętrzny (który ma okno i odnośniki do widoku). Istota po prostu ujawnia swoją pozycję i duszka, którego powinien użyć (lub jakieś odniesienie do tego duszka, jeśli chcesz buforować rzeczywiste duszki w samym module renderującym, aby uniknąć powielania instancji). Mechanizm renderujący jest po prostu proszony o narysowanie konkretnej listy encji, przez które przechodzi przez pętlę, odczytuje dane i używa wewnętrznego obiektu okna do wywołania drawduszka wyszukanego encji.

Społeczność
źródło
3
Nie znam C ++, ale czy nie istnieją wygodne ramy wstrzykiwania zależności dla tego języka?
bgusach
1
Nie opisałbym żadnego z nich jako „wygodnego” i ogólnie nie uważam ich za szczególnie przydatne, ale inni mogą mieć z nimi różne doświadczenia, więc warto je wychować.
1
Metoda, którą opisuje jako zmusza ją do tego, by istoty nie rysowały ich same, lecz przechowują informacje, a jeden system obsługuje rysowanie wszystkich bytów, jest dziś bardzo popularny w najpopularniejszych silnikach gier.
Patrick W. McMahon,
1
+1 za „Fakt, że wygląda to obrzydliwie, powinien ci coś powiedzieć: twój projekt może być obrzydliwy”.
Shadow503
+1 za podanie zarówno idealnego przypadku, jak i pragmatycznej odpowiedzi.
6

Dziedzicz z sf :: RenderWindow

SFML faktycznie zachęca do dziedziczenia po klasach.

class GameWindow: public sf::RenderWindow{};

Stąd tworzysz funkcje rysowania elementów dla elementów rysujących.

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity);
};

Teraz możesz to zrobić:

GameWindow window;
Entity entity;

window.draw(entity);

Możesz nawet zrobić to o krok dalej, jeśli Twoje Entity będą przechowywać własne unikalne duszki, sprawiając, że Entity będą dziedziczyć po sf :: Sprite.

class Entity: public sf::Sprite{};

Teraz sf::RenderWindowmogą po prostu rysować encje, a encje mają teraz funkcje takie jak setTexture()i setColor(). Istota może nawet używać pozycji duszka jako własnej pozycji, co pozwala używać setPosition()funkcji zarówno do poruszania Jednostki, jak i jej duszka.


W końcu jest całkiem fajnie, jeśli masz tylko:

window.draw(game);

Poniżej kilka szybkich przykładowych implementacji

class GameWindow: public sf::RenderWindow{
 sf::Sprite entitySprite; //assuming your Entities don't need unique sprites.
public:
 void draw(const Entity& entity){
  entitySprite.setPosition(entity.getPosition());
  sf::RenderWindow::draw(entitySprite);
 }
};

LUB

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity){
  sf::RenderWindow::draw(entity.getSprite()); //assuming Entities hold their own sprite.
 }
};
Willy Goat
źródło
3

Unikasz singletonów w rozwoju gier w taki sam sposób, w jaki unikasz ich w każdym innym oprogramowaniu: przechodzisz zależności .

Z tym z drogi, można wybrać przekazać zależności bezpośrednio jako nagie typów (jak int, Window*itp) lub możesz przekazać je w jednym lub więcej typów niestandardowych otoki (jak EntityInitializationOptions).

Ten pierwszy sposób może być irytujący (jak się dowiedziałeś), podczas gdy ten drugi pozwoli ci przekazać wszystko w jednym obiekcie i zmodyfikować pola (a nawet specjalizować typ opcji) bez konieczności zmieniania konstruktora encji. Myślę, że ten drugi sposób jest lepszy.

TC
źródło
3

Singletony nie są złe. Zamiast tego są łatwe do nadużyć. Z drugiej strony globale są jeszcze łatwiejsze do nadużyć i mają wiele innych problemów.

Jedynym uzasadnionym powodem zastąpienia singletona globalnym jest uspokojenie religijnych hejterów singletonów.

Problemem jest projekt, który obejmuje klasy, w których istnieje tylko jedna globalna instancja, i które muszą być dostępne z każdego miejsca. To się rozpada, gdy tylko pojawi się wiele wystąpień singletonu, na przykład w grze, gdy wdrażasz podzielony ekran, lub w wystarczająco dużej aplikacji dla przedsiębiorstw, gdy zauważysz, że pojedynczy rejestrator nie zawsze jest tak świetnym pomysłem .

Podsumowując, jeśli naprawdę masz klasę, w której masz jedną globalną instancję, której nie można rozsądnie przekazać przez odniesienie , singleton jest często jednym z lepszych rozwiązań w puli rozwiązań nieoptymalnych.

Piotr
źródło
1
Jestem osobą nienawidzącą singli religijnych i nie uważam też globalnego rozwiązania za rozwiązanie. : S
Dan Pantry
1

Wstrzykiwać zależności. Zaletą tego jest to, że możesz teraz tworzyć różne typy tych zależności za pośrednictwem fabryki. Niestety wyrywanie singletonów z klasy, która ich używa, przypomina ciągnięcie kota za tylne nogi po dywanie. Ale jeśli je wstrzykujesz, możesz wymieniać implementacje, być może w locie.

RenderSystem(IWindow* window);

Teraz możesz wstrzykiwać różne rodzaje okien. Umożliwia to pisanie testów na RenderSystem z różnymi typami okien, dzięki czemu można zobaczyć, jak RenderSystem się zepsuje lub wykona. Nie jest to możliwe ani trudniejsze, jeśli używasz singletonów bezpośrednio w „RenderSystem”.

Teraz jest bardziej testowalny, modułowy, a także oddzielony od konkretnej implementacji. To zależy tylko od interfejsu, a nie konkretnej implementacji.

Todd
źródło