Come posso supportare la comunicazione da componente a oggetto in modo sicuro e con un'archiviazione dei componenti compatibile con la cache?


9

Sto realizzando un gioco che utilizza oggetti di gioco basati su componenti e non riesco a implementare un modo in cui ciascun componente può comunicare con il suo oggetto di gioco. Piuttosto che spiegare tutto in una volta, spiegherò ogni parte del codice di esempio pertinente:

class GameObjectManager {
    public:
        //Updates all the game objects
        void update(Time dt);

        //Sends a message to all game objects
        void sendMessage(Message m);

    private:
        //Vector of all the game objects
        std::vector<GameObject> gameObjects;

        //vectors of the different types of components
        std::vector<InputComponent> input;
        std::vector<PhysicsComponent> ai;
        ...
        std::vector<RenderComponent> render;
}

La GameObjectManagertiene tutti gli oggetti del gioco e dei loro componenti. È anche responsabile dell'aggiornamento degli oggetti di gioco. Lo fa aggiornando i vettori dei componenti in un ordine specifico. Uso i vettori anziché gli array in modo che non vi sia praticamente alcun limite al numero di oggetti di gioco che possono esistere contemporaneamente.

class GameObject {
    public:
        //Sends a message to the components in this game object
        void sendMessage(Message m);

    private:
        //id to keep track of components in the manager
        const int id;

        //Pointers to components in the game object manager
        std::vector<Component*> components;
}

La GameObjectclasse sa quali sono i suoi componenti e può inviargli messaggi.

class Component {
    public:
        //Receives messages and acts accordingly
        virtual void handleMessage(Message m) = 0;

        virtual void update(Time dt) = 0;

    protected:
        //Calls GameObject's sendMessage
        void sendMessageToObject(Message m);

        //Calls GameObjectManager's sendMessage
        void sendMessageToWorld(Message m);
}

La Componentclasse è pura virtuale in modo che le classi per i diversi tipi di componenti possano implementare come gestire i messaggi e aggiornare. Inoltre è in grado di inviare messaggi.

Ora sorge il problema su come i componenti possono chiamare le sendMessagefunzioni in GameObjecte GameObjectManager. Ho trovato due possibili soluzioni:

  1. Dai Componentun puntatore al suo GameObject.

Tuttavia, poiché gli oggetti di gioco sono in un vettore, i puntatori potrebbero diventare rapidamente invalidati (lo stesso si potrebbe dire del vettore in GameObject, ma si spera che la soluzione a questo problema possa anche risolverlo). Potrei mettere gli oggetti di gioco in un array, ma poi dovrei passare un numero arbitrario per la dimensione, che potrebbe facilmente essere inutilmente alta e perdere memoria.

  1. Dai Componentun puntatore a GameObjectManager.

Tuttavia, non desidero che i componenti siano in grado di chiamare la funzione di aggiornamento del gestore. Sono l'unica persona che lavora a questo progetto, ma non voglio prendere l'abitudine di scrivere codice potenzialmente pericoloso.

Come posso risolvere questo problema mantenendo il mio codice sicuro e compatibile con la cache?

Risposte:


6

Il tuo modello di comunicazione sembra a posto e l'opzione uno funzionerebbe bene se solo tu potessi conservare quei puntatori in modo sicuro. È possibile risolvere il problema selezionando una struttura dati diversa per l'archiviazione dei componenti.

A è std::vector<T>stata una prima scelta ragionevole. Tuttavia, il comportamento di invalidazione dell'iteratore del contenitore è un problema. Quello che vuoi è una struttura di dati che sia veloce e coerente con la cache per iterare, e che preservi anche la stabilità dell'iteratore durante l'inserimento o la rimozione di elementi.

È possibile creare una tale struttura di dati. Consiste in un elenco di pagine collegate . Ogni pagina ha una capacità fissa e contiene tutti i suoi elementi in un array. Un conteggio viene utilizzato per indicare quanti elementi in quell'array sono attivi. Una pagina ha anche una lista libera (che consente il riutilizzo delle voci eliminato) e una skip list (che consente di saltare sopra le voci eliminato durante l'iterazione.

In altre parole, concettualmente qualcosa del genere:

struct Page {
   int count;
   int capacity;           // Optional if every page is a fixed size.
   T * m_storage;
   bool * m_skip;          // Skip list; can be bit-compressed.
   std::stack<int> m_free; // Can be replaced with a specialized stack.

   Page * next;
   Page * prior;           // Optional, allows reverse iteration
};

Chiaramente definisco questa struttura di dati un libro (perché è una raccolta di pagine che iterate comunque), ma la struttura ha vari altri nomi.

Matthew Bentley lo definisce una "colonia". L'implementazione di Matthew utilizza un campo di salto per il conteggio dei salti (si scusa per il collegamento MediaFire, ma è il modo in cui Bentley stesso ospita il documento) che è superiore al più tipico elenco di salto basato su booleane in questo tipo di strutture. La libreria di Bentley è solo di intestazione e facile da inserire in qualsiasi progetto C ++, quindi ti consiglio di usare semplicemente questo invece di farne uno tuo. Ci sono molte sottigliezze e ottimizzazioni che sto esaminando qui.

Poiché questa struttura di dati non sposta mai gli elementi una volta aggiunti, i puntatori e gli iteratori a quell'elemento rimangono validi fino a quando l'elemento stesso non viene eliminato (o il contenitore stesso viene cancellato). Poiché memorizza blocchi di elementi allocati in modo contiguo, l'iterazione è veloce e per lo più coerente con la cache. Inserimento e rimozione sono entrambi ragionevoli.

Non è perfetto; è possibile rovinare la coerenza della cache con un modello di utilizzo che prevede l'eliminazione pesantemente da punti effettivamente casuali nel contenitore e quindi iterazione su quel contenitore prima che gli inserti successivi abbiano riempito nuovamente gli articoli. Se ti trovi spesso in quello scenario, salterai regioni di memoria potenzialmente grandi alla volta. Tuttavia, in pratica, penso che questo contenitore sia una scelta ragionevole per il tuo scenario.

Altri approcci, che lascerò per coprire altre risposte, potrebbero includere un approccio basato sull'handle o una sorta di struttura a mappa di slot (dove si dispone di una matrice associativa di "chiavi" intere per valori "interi", i valori essendo indici in un array di supporto, che consente di scorrere su un vettore continuando ad accedere tramite "indice" con qualche ulteriore riferimento indiretto).


Ciao! C'è qualche risorsa in cui posso saperne di più sulle alternative alla "colonia" che hai citato nell'ultimo paragrafo? Sono implementati ovunque? Sto studiando questo argomento da un po 'di tempo e sono davvero interessato.
Rinat Veliakhmedov,

5

Essere "cache friendly" è una preoccupazione che i grandi giochi hanno. Questa sembra essere un'ottimizzazione prematura per me.


Un modo per risolverlo senza essere "cache friendly" sarebbe quello di creare il tuo oggetto nell'heap anziché nello stack: usa newe (smart) puntatori per i tuoi oggetti. In questo modo, sarai in grado di fare riferimento ai tuoi oggetti e il loro riferimento non sarà invalidato.

Per una soluzione più adatta alla cache, è possibile gestire la de / allocazione degli oggetti e utilizzare le maniglie per questi oggetti.

Fondamentalmente, all'inizializzazione del tuo programma, un oggetto riserva un pezzo di memoria sull'heap (chiamiamolo MemMan), quindi, quando vuoi creare un componente, dici a MemMan che hai bisogno di un componente di dimensione X, ' Lo riserverò per te, crei un handle e tieni internamente dove nella sua allocazione è l'oggetto per quel handle. Restituirà la maniglia e quell'unica cosa che manterrai sull'oggetto, mai un puntatore alla sua posizione in memoria.

Se hai bisogno del componente, chiederai a MemMan di accedere a questo oggetto, cosa che farà volentieri. Ma non tenere il riferimento ad esso perché ...

Uno dei compiti di MemMan è di tenere gli oggetti vicini l'uno all'altro in memoria. Una volta ogni pochi frame di gioco, puoi dire a MemMan di riorganizzare gli oggetti in memoria (o potrebbe farlo automaticamente quando crei / elimini oggetti). Aggiornerà la sua mappa della posizione da gestire a memoria. I tuoi handle saranno sempre validi, ma se hai mantenuto un riferimento allo spazio di memoria (un puntatore o un riferimento ), troverai solo la disperazione e la desolazione.

I libri di testo affermano che questo modo di gestire la memoria presenta almeno 2 vantaggi:

  1. meno cache manca perché gli oggetti sono vicini l'uno all'altro in memoria e
  2. riduce il numero della memoria de chiamate / allocazione farete al sistema operativo, che si dice di prendere un po ' di tempo.

Tieni presente che il modo in cui usi MemMan e il modo in cui organizzerai la memoria internamente dipende in realtà da come utilizzerai i tuoi componenti. Se eseguirai l'iterazione in base al loro tipo, ti consigliamo di mantenere i componenti per tipo, se esegui l'iterazione in base al loro oggetto di gioco, dovrai trovare un modo per assicurarti che siano vicini a un altro basato su quello, ecc ...

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.