Quando diverse classi devono accedere agli stessi dati, dove devono essere dichiarati i dati?


39

Ho un gioco 2D di difesa della torre di base in C ++.

Ogni mappa è una classe separata che eredita da GameState. La mappa delega la logica e il codice di disegno a ciascun oggetto nel gioco e imposta i dati come il percorso della mappa. Nello pseudo-codice la sezione logica potrebbe assomigliare a questa:

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

Gli oggetti (brividi, torri e missili) sono memorizzati in vettori di puntatori. Le torri devono avere accesso al vettore dei brividi e al vettore dei missili per creare nuovi missili e identificare obiettivi.

La domanda è: dove devo dichiarare i vettori? Dovrebbero essere membri della classe Map e passati come argomenti alla funzione tower.update ()? O dichiarato a livello globale? O ci sono altre soluzioni che mi mancano del tutto?

Quando diverse classi devono accedere agli stessi dati, dove devono essere dichiarati i dati?


1
I membri globali sono considerati "brutti" ma sono rapidi e facilitano lo sviluppo, se si tratta di un gioco piccolo, non c'è problema (IMHO). Potresti anche creare una classe esterna che gestisca la logica ( perché le torri hanno bisogno di questi vettori) e ha accesso a tutti i vettori.
Jonathan Connell,

-1 se questo è legato alla programmazione del gioco, anche mangiare la pizza lo è. Prendi dei buoni libri di progettazione software
Maik Semder,

9
@Maik: in che modo la progettazione del software non è correlata alla programmazione del gioco? Solo perché si applica anche ad altri campi della programmazione non lo rende fuori tema.
BlueRaja - Danny Pflughoeft,

Gli elenchi @BlueRaja dei modelli di progettazione software sono più adatti su SO, questo è ciò per cui è lì dopo tutto. GD.SE è per la programmazione di giochi, non per la progettazione di software
Maik Semder,

Risposte:


53

Quando hai bisogno di una singola istanza di una classe durante il tuo programma, chiamiamo quella classe un servizio . Esistono diversi metodi standard per implementare i servizi nei programmi:

  • Variabili globali . Questi sono i più facili da implementare, ma il peggior design. Se usi troppe variabili globali, ti ritroverai rapidamente a scrivere moduli che fanno troppo affidamento l'uno sull'altro ( accoppiamento forte ), rendendo molto difficile seguire il flusso della logica. Le variabili globali non sono compatibili con il multithreading. Le variabili globali rendono più difficile il monitoraggio della durata degli oggetti e ingombrano lo spazio dei nomi. Sono, tuttavia, l'opzione più performante, quindi ci sono momenti in cui possono e devono essere usati, ma li usano in modo parsimonioso.
  • Singletons . Circa 10-15 anni fa, i singoli erano il grande modello di design da conoscere. Tuttavia, al giorno d'oggi sono guardati dall'alto in basso. Sono molto più facili da utilizzare per il multi-thread, ma è necessario limitarne l'utilizzo a un thread alla volta, che non è sempre quello che si desidera. Tracciare le vite è difficile quanto con le variabili globali.
    Una tipica classe singleton sarà simile a questa:

    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!
    }
  • Iniezione delle dipendenze (DI) . Questo significa semplicemente passare il servizio come parametro del costruttore. Un servizio deve già esistere per passare in una classe, quindi non c'è modo per due servizi di fare affidamento l'uno sull'altro; nel 98% dei casi, questo è ciò che desideri (e per l'altro 2%, puoi sempre creare un setWhatever()metodo e passare il servizio in un secondo momento) . Per questo motivo, DI non ha gli stessi problemi di accoppiamento delle altre opzioni. Può essere utilizzato con il multithreading, poiché ogni thread può semplicemente avere la propria istanza di ogni servizio (e condividere solo quelli di cui ha assolutamente bisogno). Rende anche testabile l'unità di codice, se ti interessa.

    Il problema con l'iniezione di dipendenza è che occupa più memoria; ora ogni istanza di una classe necessita di riferimenti a ogni servizio che utilizzerà. Inoltre, diventa fastidioso usare quando hai troppi servizi; ci sono framework che mitigano questo problema in altri linguaggi, ma a causa della mancanza di riflessione del C ++, i framework DI in C ++ tendono ad essere ancora più lavoro che semplicemente farlo manualmente.

    //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.

    Vedi questa pagina (dalla documentazione per Ninject, un framework C # DI) per un altro esempio.

    L'iniezione di dipendenza è la solita soluzione a questo problema ed è la risposta che vedrai più votata a domande come questa su StackOverflow.com. DI è un tipo di Inversion of Control (IoC).

  • Localizzatore di servizi . Fondamentalmente, solo una classe che contiene un'istanza di ogni servizio. Puoi farlo usando reflection , oppure puoi semplicemente aggiungere una nuova istanza ad esso ogni volta che vuoi creare un nuovo servizio. Hai ancora lo stesso problema di prima - In che modo le classi accedono a questo localizzatore? - che può essere risolto in uno dei modi sopra indicati, ma ora devi farlo solo per la tua ServiceLocatorclasse, piuttosto che per dozzine di servizi. Questo metodo è anche testabile in unità, se ti interessa quel genere di cose.

    I Service Locator sono un'altra forma di Inversion of Control (IoC). Di solito, i framework che eseguono l'iniezione automatica delle dipendenze avranno anche un localizzatore di servizi.

    XNA (framework di programmazione di giochi C # di Microsoft) include un localizzatore di servizi; per saperne di più, vedi questa risposta .


A proposito, IMHO le torri non dovrebbero sapere dei brividi. A meno che tu non stia pianificando di semplicemente passare in rassegna l'elenco dei brividi per ogni torre, probabilmente vorrai implementare un partizionamento dello spazio non banale ; e quel tipo di logica non appartiene alla classe delle torri.


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Josh

Una delle risposte migliori e più chiare che abbia mai letto. Molto bene. Ho pensato che un servizio dovesse sempre essere condiviso però.
Nikos,

5

Personalmente userei il polimorfismo qui. Perché avere un missilevettore, un towervettore e un creepvettore ... quando tutti chiamano la stessa funzione; update? Perché non avere un vettore di puntatori ad una classe base Entityo GameObject?

Trovo che un buon modo di progettare sia pensare "ha senso in termini di proprietà"? Ovviamente una torre possiede un modo per aggiornarsi, ma una mappa possiede tutti gli oggetti su di essa? Se vai per il globale, stai dicendo che nulla possiede le torri e si insinua? Il globale è di solito una cattiva soluzione: promuove cattivi modelli di progettazione, ma è molto più facile lavorare con. Valuta di soppesare "voglio finire questo?" e "voglio qualcosa che posso riutilizzare"?

Un modo per aggirare questo è una qualche forma di sistema di messaggistica. La towerpuò inviare un messaggio al map(che ha accesso a, forse un riferimento al suo proprietario?) Che ha colpito un creep, e il mappoi racconta lacreep è stato colpito. Questo è molto pulito e segrega i dati.

Un altro modo è cercare nella mappa stessa ciò che vuole. Tuttavia, potrebbero esserci problemi con l'ordine di aggiornamento qui.


1
Il tuo suggerimento sul polimorfismo non è molto rilevante. Li ho memorizzati in vettori separati in modo da poter iterare su ogni tipo singolarmente, come nel codice di disegno (dove voglio prima disegnare alcuni oggetti) o nel codice di collisione.
Succoso

Per i miei scopi la mappa possiede le entità, poiché la mappa qui è analoga a "livello". Prenderò in considerazione la tua idea sui messaggi, grazie.
Succoso

1
In un gioco le prestazioni contano. Quindi i vettori dello stesso oggetto hanno una migliore località di riferimento. Inoltre, gli oggetti polimorfici con puntatori virtuali hanno prestazioni terribili perché non possono essere incorporati nel ciclo di aggiornamento.
Zan Lynx,

0

Questo è un caso in cui la programmazione orientata agli oggetti (OOP) rigorosa si interrompe.

Secondo i principi di OOP, è necessario raggruppare i dati con comportamenti correlati utilizzando le classi. Ma hai un comportamento (targeting) che necessita di dati non correlati tra loro (torri e brividi). In questa situazione, molti programmatori cercheranno di associare il comportamento a parte dei dati di cui ha bisogno (ad esempio, le torri gestiscono il targeting, ma non conoscono i brividi), ma c'è un'altra opzione: non raggruppare il comportamento con i dati.

Invece di rendere il comportamento di targeting un metodo della classe della torre, rendilo una funzione libera che accetta torri e brividi come argomenti. Ciò potrebbe richiedere di rendere pubblici un numero maggiore di membri rimasti nella torre e classi di creep, e va bene. Nascondere i dati è utile, ma è un mezzo, non un fine in sé, e non dovresti esserne schiavo. Inoltre, i membri privati ​​non sono l'unico modo per controllare l'accesso ai dati: se i dati non vengono passati a una funzione e non sono globali, vengono effettivamente nascosti da quella funzione. Se l'utilizzo di questa tecnica ti consente di evitare dati globali, potresti effettivamente migliorare l' incapsulamento.

Un esempio estremo di questo approccio è l' architettura del sistema di entità .

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.