Consigli sul collegamento tra il sistema di componenti di entità in C ++


10

Dopo aver letto un po 'di documentazione sul sistema di componenti entità, ho deciso di implementare il mio. Finora ho una classe mondiale che contiene le entità e il gestore di sistema (sistemi), la classe Entity che contiene i componenti come std :: map e alcuni sistemi. Sto tenendo entità come std :: vector nel mondo. Nessun problema finora. Ciò che mi confonde è l'iterazione di entità, non posso avere una mente cristallina, quindi non riesco ancora ad implementare quella parte. Ogni sistema dovrebbe avere un elenco locale di entità a cui sono interessati? O dovrei semplicemente scorrere le entità nella classe mondiale e creare un ciclo nidificato per scorrere i sistemi e verificare se l'entità ha i componenti a cui il sistema è interessato? Intendo :

for (entity x : listofentities) {
   for (system y : listofsystems) {
       if ((x.componentBitmask & y.bitmask) == y.bitmask)
             y.update(x, deltatime)
       }
 }

ma penso che un sistema a maschera di bit bloccherà un po 'la flessibilità in caso di incorporamento di un linguaggio di scripting. O avere elenchi locali per ciascun sistema aumenterà l'utilizzo della memoria per le classi. Sono terribilmente confuso.


Perché ti aspetti l'approccio della maschera di bit per ostacolare i binding di script? Per inciso, utilizzare i riferimenti (const, se possibile) nei cicli for-each per evitare di copiare entità e sistemi.
Benjamin Kloster,

usando una maschera di bit, ad esempio un int, conterrà solo 32 componenti diversi. Non sto insinuando che ci saranno più di 32 componenti, ma cosa succede se ho? dovrò creare un altro int o 64 bit int, non sarà dinamico.
deniz

È possibile utilizzare std :: bitset o std :: vector <bool>, a seconda che si desideri o meno che sia dinamico in fase di esecuzione.
Benjamin Kloster,

Risposte:


7

Avere elenchi locali per ciascun sistema aumenterà l'utilizzo della memoria per le classi.

È un tradizionale compromesso spazio-temporale .

Mentre scorrere tutte le entità e controllare le loro firme è direttamente codificato, può diventare inefficiente man mano che il tuo numero di sistemi cresce - immagina un sistema specializzato (lascia che sia input) che cerchi la sua probabilmente singola entità di interesse tra migliaia di entità non correlate .

Detto questo, questo approccio può ancora essere abbastanza buono a seconda dei tuoi obiettivi.

Tuttavia, se sei preoccupato per la velocità, ci sono ovviamente altre soluzioni da considerare.

Ogni sistema dovrebbe contenere un elenco locale di entità a cui sono interessati?

Esattamente. Questo è un approccio standard che dovrebbe offrire prestazioni decenti ed è ragionevolmente facile da implementare. Il sovraccarico di memoria è trascurabile secondo me - stiamo parlando di memorizzare i puntatori.

Ora come mantenere questi "elenchi di interessi" potrebbe non essere così ovvio. Per quanto riguarda il contenitore di dati, std::vector<entity*> targetsall'interno della classe di sistema è perfettamente sufficiente. Ora quello che faccio è questo:

  • L'entità è vuota al momento della creazione e non appartiene a nessun sistema.
  • Ogni volta che aggiungo un componente a un'entità:

    • ottenere la sua firma bit corrente ,
    • mappare le dimensioni del componente al pool mondiale di dimensioni del blocco adeguate (personalmente uso boost :: pool) e allocare il componente lì
    • ottenere la nuova firma bit dell'entità (che è solo "firma bit corrente" più il nuovo componente)
    • scorrere tutti i sistemi di tutto il mondo e se c'è un sistema la cui firma non corrispondente alla firma corrente dell'entità e non corrispondere alla nuova firma, diventa evidente che dovremmo push_back il puntatore alla nostra entità lì.

          for(auto sys = owner_world.systems.begin(); sys != owner_world.systems.end(); ++sys)
                  if((*sys)->components_signature.matches(new_signature) && !(*sys)->components_signature.matches(old_signature)) 
                          (*sys)->add(this);

La rimozione di un'entità è del tutto analoga, con l'unica differenza che rimuoviamo se un sistema corrisponde alla nostra firma corrente (il che significa che l'entità era lì) e non corrisponde alla nuova firma (il che significa che l'entità non dovrebbe più essere lì ).

Ora potresti prendere in considerazione l'utilizzo di std :: list perché la rimozione dal vettore è O (n), senza menzionare che dovresti spostare grandi quantità di dati ogni volta che rimuovi dal centro. In realtà, non è necessario, poiché non ci interessa elaborare l'ordine a questo livello, possiamo semplicemente chiamare std :: remove e vivere con il fatto che ad ogni eliminazione dobbiamo solo eseguire la ricerca O (n) per il nostro entità da rimuovere.

std :: list ti darebbe O (1) rimuovere ma dall'altra parte hai un po 'di memoria aggiuntiva. Ricorda anche che la maggior parte delle volte elaborerai le entità e non le rimuoverai - e questo sicuramente sarà fatto più velocemente usando std :: vector.

Se sei molto critico in termini di prestazioni, puoi prendere in considerazione anche un altro modello di accesso ai dati , ma in entrambi i casi mantieni un qualche tipo di "elenco di interessi". Ricorda, tuttavia, che se l'API di Entity System viene mantenuta abbastanza astratta, non dovrebbe essere un problema migliorare i metodi di elaborazione delle entità dei sistemi se il tuo framerate diminuisce a causa loro - quindi per ora, scegli il metodo più facile da codificare per te - solo quindi profilare e migliorare se necessario.


5

C'è un approccio che vale la pena considerare dove ogni sistema possiede i componenti associati a se stesso e le entità si riferiscono solo a loro. Fondamentalmente, la tua Entityclasse (semplificata) si presenta così:

class Entity {
  std::map<ComponentType, Component*> components;
};

Quando dici un RigidBodycomponente collegato a un Entity, lo richiedi dal tuo Physicssistema. Il sistema crea il componente e consente all'entità di mantenerlo puntato. Il tuo sistema sarà quindi simile a:

class PhysicsSystem {
  std::vector<RigidBodyComponent> rigidBodyComponents;
};

Ora, questo potrebbe sembrare inizialmente un po 'contro intuitivo, ma il vantaggio sta nel modo in cui i sistemi di entità componente aggiornano il loro stato. Spesso, ripeterai i tuoi sistemi e chiederai loro di aggiornare i componenti associati

for(auto it = systems.begin(); it != systems.end(); ++it) {
  it->update();
}

Il punto di forza di avere tutti i componenti di proprietà del sistema nella memoria contigua è che quando il sistema scorre su ogni componente e lo aggiorna, in pratica deve solo fare

for(auto it = rigidBodyComponents.begin(); it != rigidBodyComponents.end(); ++it) {
  it->update();
}

Non deve iterare su tutte le entità che potenzialmente non hanno un componente che devono aggiornare e ha anche un potenziale per ottime prestazioni della cache perché i componenti verranno tutti archiviati in modo contiguo. Questo è uno, se non il più grande vantaggio di questo metodo. Avrai spesso centinaia e migliaia di componenti in un dato momento, potresti anche provare ad essere il più performante possibile.

A quel punto i tuoi Worldunici cicli nei sistemi e updateli chiama senza bisogno di iterare anche le entità. È (imho) un design migliore perché le responsabilità dei sistemi sono molto più chiare.

Naturalmente, ci sono una miriade di tali disegni, quindi devi valutare attentamente le esigenze del tuo gioco e scegliere il più appropriato, ma come possiamo vedere qui a volte sono i piccoli dettagli di design che possono fare la differenza.


buona risposta, grazie. ma i componenti non hanno funzioni (come update ()), solo dati. e il sistema elabora tali dati. quindi, secondo il tuo esempio, dovrei aggiungere un aggiornamento virtuale per la classe del componente e un puntatore di entità per ciascun componente, giusto?
deniz

@deniz Tutto dipende dal tuo design. Se i componenti non hanno metodi ma solo dati, il sistema può ancora scorrere su di essi ed eseguire le azioni necessarie. Per quanto riguarda il collegamento alle entità, sì, è possibile memorizzare un puntatore all'entità proprietario nel componente stesso o fare in modo che il sistema mantenga una mappa tra gli handle e le entità del componente. In genere, tuttavia, si desidera che i componenti siano il più autonomi possibile. Un componente che non conosce affatto la sua entità padre è l'ideale. Se hai bisogno di comunicazione in quella direzione, preferisci eventi e simili.
pwny

Se dici che sarà meglio per l'efficienza, userò il tuo modello.
deniz

@deniz Assicurati di profilare il tuo codice in anticipo e spesso per identificare ciò che funziona e non per il tuo particolare
motore

ok :) farò un po 'di stress test
deniz

1

Secondo me, una buona architettura è quella di creare un livello componenti nelle entità e separare la gestione di ciascun sistema in questo livello componenti. Ad esempio, il sistema logico ha alcuni componenti logici che influiscono sulla loro entità e memorizza gli attributi comuni che sono condivisi con tutti i componenti nell'entità.

Successivamente, se si desidera gestire gli oggetti di ciascun sistema in punti diversi o in un ordine particolare, è meglio creare un elenco di componenti attivi in ​​ciascun sistema. Tutti gli elenchi di puntatori che è possibile creare e gestire nei sistemi sono meno di una risorsa caricata.

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.