Raggruppamento di entità dello stesso componente impostato nella memoria lineare


11

Partiamo dall'approccio di base sistemi-componenti-entità .

Creiamo assemblaggi (termine derivato da questo articolo) semplicemente per informazioni sui tipi di componenti . Viene eseguito in modo dinamico in fase di runtime, proprio come aggiungeremmo / rimuovere componenti a un'entità uno per uno, ma chiamiamolo più precisamente poiché si tratta solo di informazioni sul tipo.

Quindi costruiamo entità specificando l' assemblaggio per ognuna di esse. Una volta creata l'entità, il suo assemblaggio è immutabile, il che significa che non possiamo modificarla direttamente sul posto, ma possiamo comunque ottenere la firma dell'entità esistente su una copia locale (insieme al contenuto), apportare le modifiche appropriate e creare una nuova entità di esso.

Ora per il concetto chiave: ogni volta che un'entità viene creata, viene assegnata a un oggetto chiamato bucket di assemblaggio , il che significa che tutte le entità della stessa firma saranno nello stesso contenitore (ad es. In std :: vector).

Ora i sistemi passano in rassegna ogni secchio di loro interesse e fanno il loro lavoro.

Questo approccio presenta alcuni vantaggi:

  • i componenti sono memorizzati in pochi (precisamente: numero di bucket) blocchi di memoria contigui - questo migliora la compatibilità della memoria ed è più facile scaricare l'intero stato del gioco
  • i sistemi elaborano i componenti in modo lineare, il che significa una migliore coerenza della cache - ciao ciao dizionari e salti di memoria casuali
  • creare una nuova entità è facile come mappare un assemblaggio su un bucket e riportare i componenti necessari al suo vettore
  • cancellare un'entità è facile come una chiamata a std :: move per scambiare l'ultimo elemento con quello cancellato, perché l'ordine non ha importanza in questo momento

inserisci qui la descrizione dell'immagine

Se disponiamo di molte entità con firme completamente diverse, i vantaggi della coerenza della cache diminuiscono, ma non penso che accadrà nella maggior parte delle applicazioni.

C'è anche un problema con l'invalidazione del puntatore dopo che i vettori sono stati riallocati - questo potrebbe essere risolto introducendo una struttura come:

struct assemblage_bucket {
    struct entity_watcher {
        assemblage_bucket* owner;
        entity_id real_index_in_vector;
    };

    std::unordered_map<entity_id, std::vector<entity_watcher*>> subscribers;

    //...
};

Quindi ogni volta che per qualche ragione nella nostra logica di gioco vogliamo tenere traccia di un'entità appena creata, all'interno del bucket registriamo un entity_watcher e una volta che l'entità deve essere std :: spostata durante la rimozione, cerchiamo i suoi watcher e aggiorniamo loro real_index_in_vectora nuovi valori. Il più delle volte questo impone una sola ricerca nel dizionario per ogni eliminazione di entità.

Ci sono altri svantaggi di questo approccio?

Perché la soluzione non è menzionata da nessuna parte, nonostante sia abbastanza ovvia?

EDIT : sto modificando la domanda per "rispondere alle risposte", poiché i commenti non sono sufficienti.

perdi la natura dinamica dei componenti innestabili, creati appositamente per allontanarti dalla costruzione di classi statiche.

Io non. Forse non l'ho spiegato abbastanza chiaramente:

auto signature = world.get_signature(entity_id); // this would just return entity_id.bucket_owner->bucket_signature or so
signature.add(foo_component);
signature.remove(bar_component);
world.delete_entity(entity_id); // entity_id would hold information about its bucket owner
world.create_entity(signature); // automatically assigns new entity to an existing or a new bucket

È semplice come prendere la firma dell'entità esistente, modificarla e caricarla di nuovo come nuova entità. Natura innestabile e dinamica ? Ovviamente. Qui vorrei sottolineare che esiste solo un "assemblaggio" e una classe "secchio". I bucket sono guidati dai dati e creati in fase di esecuzione in quantità ottimale.

dovresti esaminare tutti i bucket che potrebbero contenere un target valido. Senza una struttura di dati esterna, il rilevamento delle collisioni potrebbe essere altrettanto difficile.

Bene, questo è il motivo per cui abbiamo le strutture di dati esterne sopra menzionate . La soluzione alternativa è semplice come l'introduzione di un iteratore nella classe System che rileva quando passare al bucket successivo. Il salto sarebbe puramente trasparente alla logica.


Ho anche letto l'articolo di Randy Gaul sulla memorizzazione di tutti i componenti in vettori e ho lasciato che i loro sistemi li elaborassero. Lì vedo due grossi problemi: cosa succede se voglio aggiornare solo un sottoinsieme di entità (ad esempio pensare all'abbattimento). Per questo motivo i componenti verranno nuovamente accoppiati con le entità. Per ogni passaggio dell'iterazione del componente, devo verificare se l'entità a cui appartiene è stata selezionata per un aggiornamento. L'altro problema è che alcuni sistemi devono elaborare più tipi di componenti diversi eliminando nuovamente il vantaggio della coerenza della cache. Qualche idea su come affrontare questi problemi?
Tiguchi,

Risposte:


7

In sostanza, è stato progettato un sistema a oggetti statici con un allocatore di pool e con classi dinamiche.

Ho scritto un sistema a oggetti che funziona in modo quasi identico al tuo sistema di "assemblaggi" ai miei tempi di scuola, anche se tendo sempre a chiamare "assemblaggi" o "progetti" o "archetipi" nei miei progetti. L'architettura era più una seccatura nei sistemi di oggetti ingenui e non aveva vantaggi misurabili in termini di prestazioni rispetto ad alcuni dei progetti più flessibili con cui l'ho confrontata. La capacità di modificare dinamicamente un oggetto senza la necessità di modificarlo o riallocarlo è estremamente importante quando si lavora su un editor di giochi. I designer vorranno trascinare i componenti sulle definizioni degli oggetti. Il codice di runtime potrebbe anche aver bisogno di modificare i componenti in modo efficiente in alcuni progetti, anche se personalmente non mi piace. A seconda di come colleghi i riferimenti agli oggetti nel tuo editor,

Otterrai una coerenza della cache peggiore di quanto pensi nella maggior parte dei casi non banali. Il tuo sistema di intelligenza artificiale, ad esempio, non si preoccupa dei Rendercomponenti ma finisce per essere bloccato su di essi come parte di ogni entità. Gli oggetti su cui si esegue l'iterazione sono più grandi e le richieste di cacheline finiscono per raccogliere dati non necessari e vengono restituiti meno oggetti interi con ogni richiesta). Sarà comunque migliore del metodo ingenuo e la composizione dell'oggetto metodo ingenuo viene utilizzata anche nei grandi motori AAA, quindi probabilmente non hai bisogno di meglio, ma almeno non pensare di non poterlo migliorare ulteriormente.

Il tuo approccio ha più senso per i alcunicomponenti, ma non tutti. Non mi piace fortemente ECS perché raccomanda di mettere sempre ogni componente in un contenitore separato, il che ha senso per la fisica o la grafica o quant'altro, ma non ha alcun senso se si consentono più componenti di script o AI componibile. Se lasci che il sistema componente sia usato non solo per gli oggetti incorporati ma anche come modo per progettisti e programmatori di gameplay di comporre il comportamento degli oggetti, può avere senso raggruppare tutti i componenti AI (che interagiranno spesso) o tutti gli script componenti (dal momento che si desidera aggiornarli tutti in un batch). Se desideri il sistema più performante, avrai bisogno di un mix di allocazione dei componenti e schemi di archiviazione e mettiti il ​​tempo per capire in modo conclusivo quale sia la migliore per ogni particolare tipo di componente.


Ho detto: non possiamo cambiare la firma dell'entità, e intendevo dire che non possiamo modificarla direttamente sul posto, ma possiamo comunque ottenere un assemblaggio esistente in una copia locale, apportare modifiche e caricarlo nuovamente come nuova entità - e questi le operazioni sono piuttosto economiche, come ho mostrato nella domanda. Ancora una volta - esiste solo UNA classe "bucket". "Assemblaggi" / "Firme" / "chiamiamolo come vogliamo" possono essere creati dinamicamente in fase di esecuzione come in un approccio standard, arriverei persino a pensare a un'entità come a "firma".
Patryk Czachurski,

E ho detto che non vuoi necessariamente affrontare la reificazione. "Creare una nuova entità" potrebbe potenzialmente significare suddividere tutti gli handle esistenti nell'entità, a seconda di come funziona il sistema di handle. La tua chiamata se è abbastanza economica o no. Ho trovato solo un dolore nel sedere con cui avere a che fare.
Sean Middleditch,

Bene, ora ho il tuo punto su questo. Ad ogni modo, penso che anche se l'aggiunta / rimozione fosse un po 'più costosa, ciò accade in modo così ocassionalico che vale comunque la pena semplificare notevolmente il processo di accesso ai componenti, che avviene in tempo reale. Quindi, il sovraccarico del "cambiamento" è trascurabile. A proposito del tuo esempio di IA, non vale comunque la pena di questi pochi sistemi che necessitano comunque di dati provenienti da più componenti?
Patryk Czachurski,

Il mio punto era che l'IA era un luogo in cui il tuo approccio è migliore, ma per altri componenti non è necessariamente così.
Sean Middleditch il

4

Quello che hai fatto sono stati reingegnerizzati oggetti C ++. Il motivo per cui questo sembra ovvio è che se si sostituisce la parola "entità" con "classe" e "componente" con "membro", questo è un progetto OOP standard che utilizza mixin.

1) perdi la natura dinamica dei componenti innestabili, creati appositamente per allontanarti dalla costruzione di classi statiche.

2) la coerenza della memoria è molto importante all'interno di un tipo di dati, non all'interno di un oggetto che unifica più tipi di dati in un unico posto. Questo è uno dei motivi per cui sono stati creati i componenti + sistemi, per allontanarsi dal frammento di memoria di classe + oggetto.

3) questo design ritorna anche allo stile di classe C ++ perché stai pensando all'entità come ad un oggetto coerente quando, in una progettazione componente + sistema, l'entità è semplicemente un tag / ID per rendere comprensibili i meccanismi interni agli umani.

4) è altrettanto facile serializzare se stesso un componente rispetto a un oggetto complesso serializzare più componenti al suo interno, se non addirittura più semplice tenerne traccia come programmatore.

5) il prossimo passo logico lungo questo percorso è rimuovere i sistemi e inserire quel codice direttamente nell'entità, dove ha tutti i dati necessari per funzionare. Tutti possiamo vedere cosa implica =)


2) forse non capisco completamente la memorizzazione nella cache, ma diciamo che esiste un sistema che funziona con diciamo 10 componenti. In un approccio standard, elaborare ciascuna entità significa accedere alla RAM 10 volte, poiché i componenti sono sparsi in posizioni casuali nella memoria, anche se vengono utilizzati pool, poiché componenti diversi appartengono a pool diversi. Non sarebbe "importante" memorizzare l'intera entità in una sola volta ed elaborare tutti i componenti senza perdere una sola cache, senza nemmeno dover effettuare ricerche nel dizionario? Inoltre, ho apportato una modifica per coprire il punto 1)
Patryk Czachurski il

@Sean Middleditch ha una buona descrizione di questa scomposizione nella cache nella sua risposta.
Patrick Hughes,

3) Non sono oggetti coerenti in alcun modo. A proposito del componente A che si trova proprio accanto al componente B in memoria, è solo "coerenza della memoria", non "coerenza logica", come ha sottolineato John. I secchi, al momento della loro creazione, potevano persino mescolare i componenti in firma per qualsiasi ordine desiderato e i principi sarebbero ancora mantenuti. 4) può essere altrettanto facile "tenere traccia" se abbiamo abbastanza astrazione - quello di cui stiamo parlando è semplicemente uno schema di archiviazione che fornisce iteratori e forse una mappa di offset byte può rendere l'elaborazione semplice come in un approccio standard.
Patryk Czachurski,

5) E non credo che nulla in questa idea indichi questa direzione. Non è che non voglio essere d'accordo con te, sono solo curioso di sapere dove può condurre questa discussione, anche se probabilmente porterà a una sorta di "misura" o alla ben nota "ottimizzazione prematura". :)
Patryk Czachurski il

@PatrykCzachurski ma i tuoi sistemi non funzionano con 10 componenti.
user253751

3

Tenere insieme entità simili non è così importante come si potrebbe pensare, motivo per cui è difficile pensare a un motivo valido diverso da "perché è un'unità". Ma dal momento che lo stai davvero facendo per coerenza della cache in contrapposizione alla coerenza logica, potrebbe avere senso.

Una difficoltà che potresti avere è l'interazione tra i componenti in diversi bucket. Non è terribilmente semplice trovare qualcosa a cui la tua IA può sparare, ad esempio, dovresti esaminare tutti i secchi che potrebbero contenere un bersaglio valido. Senza una struttura di dati esterna, il rilevamento delle collisioni potrebbe essere altrettanto difficile.

Per continuare a organizzare le entità insieme per coerenza logica, l'unica ragione per cui avrei potuto tenere le entità unite è a scopo di identificazione nelle mie missioni. Devo sapere se hai appena creato un'entità di tipo A o di tipo B, e riesco a ovviare a questo ... lo hai indovinato: aggiungendo un nuovo componente che identifica l'assemblaggio che unisce questa entità. Anche allora, non sto riunendo tutti i componenti per un grande compito, ho solo bisogno di sapere di cosa si tratta. Quindi non penso che questa parte sia terribilmente utile.


Devo ammettere che non capisco bene la tua risposta. Cosa intendi per "coerenza logica"? A proposito di difficoltà nelle interazioni, ho fatto una modifica.
Patryk Czachurski,

"Coerenza logica", come in: Ha "senso logico" mantenere vicini tutti i componenti che compongono un'entità Albero.
John McDonald,
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.