Come posso usare correttamente i singoli nella programmazione del motore C ++?


16

So che i singleton sono cattivi, il mio vecchio motore di gioco utilizzava un oggetto "Game" singleton che gestisce tutto, dalla conservazione di tutti i dati al loop di gioco effettivo. Ora ne sto facendo uno nuovo.

Il problema è che per disegnare qualcosa in SFML si usa window.draw(sprite)dove è una finestra sf::RenderWindow. Ci sono 2 opzioni che vedo qui:

  1. Crea un oggetto di gioco singleton che recupera ogni entità nel gioco (quello che ho usato prima)
  2. Rendi questo il costruttore per le entità: Entity(x, y, window, view, ...etc)(questo è solo ridicolo e fastidioso)

Quale sarebbe il modo corretto per farlo mantenendo il costruttore di un'entità solo su xey?

Potrei provare a tenere traccia di tutto ciò che faccio nel ciclo di gioco principale, e semplicemente disegnare manualmente il loro sprite nel circuito di gioco, ma anche quello sembra disordinato e voglio anche il pieno controllo assoluto su un'intera funzione di disegno per l'entità.


1
Potresti passare la finestra come argomento della funzione 'render'.
dari,

25
I single non sono male! possono essere utili e talvolta necessari (ovviamente è discutibile).
ExOfDe,

3
Sentiti libero di sostituire i singoli con globi semplici. Inutile creare risorse richieste a livello globale "on demand", inutile farle passare. Per le entità, tuttavia, è possibile utilizzare una classe di "livello" per contenere determinate cose che sono rilevanti per tutte.
snake5,

Dichiaro la mia finestra e altre dipendenze nella mia principale, e poi ho i puntatori nelle altre mie classi.
KaareZ

1
@JAB Facilmente risolto con l'inizializzazione manuale da main (). L'inizializzazione lenta lo fa accadere in un momento sconosciuto, che non è mai una buona idea per i sistemi core.
snake5,

Risposte:


3

Memorizza solo i dati necessari per eseguire il rendering dello sprite all'interno di ciascuna entità, quindi recuperali dall'entità e passali alla finestra per il rendering. Non è necessario archiviare alcuna finestra o visualizzare i dati all'interno di entità.

Potresti avere una classe di gioco o motore di livello superiore che contiene una classe di livello (contiene tutte le entità attualmente in uso) e una classe di rendering (contiene la finestra, la vista e qualsiasi altra cosa per il rendering).

Quindi il ciclo di aggiornamento del gioco nella tua classe di livello superiore potrebbe apparire come:

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());
}

3
Non c'è niente di ideale in un singleton. Perché rendere pubblici gli interni dell'implementazione quando non è necessario? Perché scrivere Logger::getInstance().Log(...)invece che solo Log(...)? Perché inizializzare la classe in modo casuale quando viene chiesto se è possibile farlo manualmente una sola volta? Una funzione globale che fa riferimento a globi statici è molto più semplice da creare e utilizzare.
snake5,

@ snake5 Giustificare i singoli su Stack Exchange è come simpatizzare con Hitler.
Willy Goat,

30

L'approccio semplice è quello di rendere la cosa che invece era Singleton<T>globale T. Anche i globali hanno problemi, ma non rappresentano un sacco di lavoro extra e codice boilerplate per imporre un banale vincolo. Questa è sostanzialmente l'unica soluzione che non coinvolgerà (potenzialmente) il contatto con il costruttore dell'entità.

L'approccio più difficile, ma forse migliore, è passare le tue dipendenze dove ne hai bisogno . Sì, ciò potrebbe comportare il passaggio Window *di un gruppo di oggetti (come la tua entità) in un modo che sembra grossolano. Il fatto che appaia disgustoso dovrebbe dirti qualcosa: il tuo design potrebbe essere disgustoso.

Il motivo per cui ciò è più difficile (oltre a comportare una maggiore digitazione) è che questo spesso porta a refactoring delle tue interfacce in modo che la cosa che "hai bisogno" di passare sia necessaria da un numero inferiore di classi a livello foglia. Questo rende un sacco di bruttezza insita nel passare il tuo renderer a tutto ciò che va via, e migliora anche la manutenibilità generale del tuo codice riducendo la quantità di dipendenze e accoppiamento, la misura di cui hai reso molto ovvio prendendo le dipendenze come parametri . Quando le dipendenze erano singole o globali, era meno ovvio quanto fossero interconnessi i tuoi sistemi.

Ma è potenzialmente un'impresa importante . Farlo su un sistema dopo il fatto può essere decisamente doloroso. Potrebbe essere molto più pragmatico per te lasciare semplicemente il tuo sistema da solo, con il singleton, per ora (specialmente se stai cercando di spedire un gioco che altrimenti funziona bene, i giocatori non si preoccuperanno in genere se hai un singleton o quattro in là).

Se vuoi provare a farlo con il tuo progetto esistente, potresti dover pubblicare molti più dettagli sulla tua attuale implementazione in quanto non esiste davvero un elenco di controllo generale per apportare queste modifiche. O vieni a discuterne chat .

Da quello che hai pubblicato, penso che un grande passo nella direzione "no singleton" sarebbe quello di evitare la necessità per le tue entità di avere accesso alla finestra o alla vista. Suggerisce che si disegnano da soli, e con lo sprite cercarono l'entità. non è necessario che le entità si disegnino da sole . È possibile adottare una metodologia in cui le entità contengono solo le informazioni che consentirebberoessi devono essere disegnati da un sistema esterno (che ha la finestra e visualizzare i riferimenti). L'entità espone semplicemente la sua posizione e lo sprite che dovrebbe usare (o qualche tipo di riferimento a detto sprite, se si desidera memorizzare nella cache gli sprite effettivi nel renderer stesso per evitare di avere istanze duplicate). Al renderer viene semplicemente detto di disegnare un particolare elenco di entità, attraverso il quale scorre, legge i dati e usa il suo oggetto finestra tenuto internamente per chiamaredraw


3
Non ho familiarità con C ++, ma non ci sono strutture di iniezione di dipendenza confortevoli per questo linguaggio?
bgusach,

1
Non descriverei nessuno di loro come "comodo" e non li trovo particolarmente utili in generale, ma altri potrebbero avere un'esperienza diversa con loro, quindi è un buon punto per farli apparire.

1
Il metodo che descrive come farlo in modo che le entità non le disegnino da sole ma mantengano le informazioni e un singolo sistema gestisce tutte le entità è oggi utilizzato molto nei motori di gioco più popolari.
Patrick W. McMahon il

1
+1 per "Il fatto che appaia disgustoso dovrebbe dirti qualcosa: il tuo design potrebbe essere disgustoso".
Shadow503

+1 per dare sia il caso ideale sia la risposta pragmatica.

6

Eredita da sf :: RenderWindow

SFML in realtà ti incoraggia a ereditare dalle sue classi.

class GameWindow: public sf::RenderWindow{};

Da qui, si creano funzioni di disegno membro per le entità di disegno.

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

Ora puoi farlo:

GameWindow window;
Entity entity;

window.draw(entity);

Puoi anche fare un ulteriore passo avanti se le tue Entità terranno i loro sprite unici facendo sì che l'Entità erediti da sf :: Sprite.

class Entity: public sf::Sprite{};

Ora sf::RenderWindowpuò semplicemente disegnare Entità e le entità ora hanno funzioni come setTexture()e setColor(). L'Entità potrebbe persino usare la posizione dello sprite come propria posizione, permettendoti di usare la setPosition()funzione sia per spostare l'Entità che il suo sprite.


Alla fine , è carino se hai solo:

window.draw(game);

Di seguito sono riportati alcuni esempi di implementazioni veloci

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);
 }
};

O

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity){
  sf::RenderWindow::draw(entity.getSprite()); //assuming Entities hold their own sprite.
 }
};

3

Eviti i singoli nello sviluppo del gioco nello stesso modo in cui li eviti in ogni altro tipo di sviluppo del software: passi le dipendenze .

Con quella di mezzo, è possibile scegliere di passare le dipendenze direttamente come tipi nude (come int, Window*, ecc) oppure si può scegliere di passare loro in uno o più personalizzato involucro tipi (come EntityInitializationOptions).

Il primo modo può diventare fastidioso (come hai scoperto), mentre il secondo ti permetterà di passare tutto in un oggetto e modificare i campi (e persino specializzare il tipo di opzioni) senza andare in giro e cambiare ogni costruttore di entità. Penso che il secondo modo sia migliore.


3

I single non sono male. Invece sono facili da abusare. D'altra parte, i globi sono ancora più facili da abusare e hanno molti più problemi.

L'unico motivo valido per sostituire un singleton con un globale è pacificare gli odiatori religiosi del singleton.

Il problema è avere un design che includa classi di cui esiste una sola istanza globale e che devono essere accessibili ovunque. Questo si rompe non appena si finiscono per avere più istanze del singleton, ad esempio in un gioco quando si implementa lo schermo diviso, o in un'applicazione aziendale sufficientemente grande quando si nota che un singolo logger non è sempre una grande idea .

In conclusione, se hai davvero una classe in cui hai una singola istanza globale che non puoi ragionevolmente passare per riferimento , singleton è spesso una delle soluzioni migliori in un pool di soluzioni non ottimali.


1
Sono un odio religioso singleton e non considero neanche una soluzione globale. : S
Dan Pantry,

1

Inietti dipendenze. Un vantaggio nel fare questo è ora che puoi creare vari tipi di queste dipendenze tramite una fabbrica. Sfortunatamente, strappare i singoli da una classe che li usa è come tirare un gatto per le zampe posteriori su un tappeto. Ma se le inietti, puoi scambiare le implementazioni, forse al volo.

RenderSystem(IWindow* window);

Ora puoi iniettare vari tipi di finestre. Ciò consente di scrivere test su RenderSystem con vari tipi di finestre in modo da poter vedere come si romperà o funzionerà il tuo RenderSystem. Questo non è possibile, o più difficile, se si utilizzano singleton direttamente all'interno di "RenderSystem".

Ora è più testabile, modulare ed è anche disaccoppiato da un'implementazione specifica. Dipende solo da un'interfaccia, non da un'implementazione concreta.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.