Come rimuovo al meglio un'entità dal mio ciclo di gioco quando è morta?


16

Ok, quindi ho un grande elenco di tutte le mie entità che cerco e aggiorno. In AS3 posso archiviarlo come un array (lunghezza dinamica, non tipizzato), un vettore (tipizzato) o un elenco collegato (non nativo). Al momento sto usando Array ma ho intenzione di passare a Vector o all'elenco collegato se è più veloce.

Comunque, la mia domanda, quando un'entità viene distrutta, come dovrei rimuoverla dall'elenco? Potrei annullare la sua posizione, separarla o semplicemente apporre una bandiera su di essa per dire "salta su di me, sono morta". Sto unendo le mie entità, quindi è probabile che un'entità morta sia di nuovo viva ad un certo punto. Per ogni tipo di raccolta qual è la mia migliore strategia e quale combinazione di tipo di raccolta e metodo di rimozione funzionerà meglio?


La lunghezza di un vettore non è fissa, ma è digitata e ciò lo rende superiore all'array. Lo svantaggio è che non esiste una sintassi rapida per definire un elenco precompilato, ma non è necessario che io pensi.
Bart van Heukelom,

Risposte:


13

Vorrei archiviare tutti i componenti aggiuntivi / rimossi in elenchi separati e fare quelle operazioni dopo aver ripetuto il ciclo di aggiornamento.


10

Il framework Flixel usa la bandiera morta (in realtà diverse bandiere che determinano se deve essere disegnata, aggiornata e così via). Direi che se vuoi rilanciare le entità e se le prestazioni sono un problema, usi la bandiera morta. Nella mia esperienza, creare un'istanza di nuove entità è l'operazione più costosa nel caso d'uso che descrivi, e la giunzione o l'annullamento di elementi può portare a un gonfiore della memoria data la raccolta dei rifiuti a volte squirrata di Flash.


1
+1 per flixel. Riciclare deadaiuta davvero con le prestazioni.
Snow Blind,

3

Mentre alcune tecniche sono intrinsecamente più efficienti di altre, importa solo se stai esaurendo i cicli sulla tua piattaforma di destinazione. Usa qualunque tecnica ti permetta di fare il tuo gioco più velocemente. Nel frattempo, cerca di non fare affidamento sull'implementazione specifica delle strutture di dati del container e, se necessario, ti aiuterà a ottimizzare in seguito.

Giusto per affrontare alcune delle tecniche già discusse da altri qui. Se l'ordine delle entità è importante, un flag morto può consentire di eseguire la giunzione durante il ciclo di aggiornamento sul frame successivo. per esempio. pseudocodice molto semplice:

void updateGame()
{
  // updateEntities()
  Entity* pSrcEntity = &mEntities[0];
  Entity* pDstEntity = &mEntities[0];
  newNumEntities = 0;
  for (int i = 0; i < numEntities; i++)
  {
    if (!pSrcEntity->isDead)
    {
       // could be inline but whatever.
       updateEntity(pDstEntity, pSrcEntity);
       // if entity just died, don't update the pDstEntity pointer, 
       // and just let the next entity updated overwrite it.
       if (!pDstEntity->isDead)
       {
          pDstEntity++;
          newNumEntities++;
       }
    }
    pSrcEntity++;
  }
}
numEntities = newNumEntities;

Queste sono le caratteristiche di questo schema:

  • naturale compattezza delle entità (anche se con possibilmente 1 latenza di fotogramma prima di poter recuperare uno slot entità).
  • nessun problema di riordino casuale.
  • mentre gli elenchi doppiamente collegati hanno inserzione / eliminazione O (1), ma sono molto difficili da precaricare per nascondere in modo ottimale la latenza della cache. Mantenerli in un array compatto consente alle tecniche di precaricamento dei blocchi di funzionare correttamente.
  • In caso di distruzione di più oggetti, non è necessario eseguire copie a turni ridondanti per mantenere l'ordine e la compattezza (tutto viene eseguito una volta durante il passaggio di aggiornamento)
  • Sfruttate i dati toccanti che dovranno essere già nella cache durante l'aggiornamento.
  • Funziona bene se i poitner delle entità di origine e destinazione devono separare le matrici. È quindi possibile eseguire il doppio buffer degli array di entità per sfruttare il multicore / ad es. un thread aggiorna / scrive le entità per il frame N, mentre un altro thread sta eseguendo il rendering delle entità del frame precedente per il frame N-1.
  • La compattezza significa che è più semplice eseguire il DMA dell'intero lotto su un processore eterogeneo per un ulteriore carico di lavoro della CPU, ad es. SPU o GPU.

+1. Mi piace questo. Anche se non ho quasi mai bisogno di aggiornamenti ordinati all'interno di un pool, lo aggiungerò al sacco di cose da ricordare se mi imbatto nella situazione: o)
Kaj

2

Parlando in termini della mia esperienza di programmazione generale, lo splicing è di solito un'operazione lenta, che comporta lo spostamento di tutti gli elementi esistenti su uno. Penserei che impostarlo su null sarebbe la soluzione migliore qui ; una bandiera morta funzionerebbe ma dovresti fare attenzione a non lasciare che il tuo codice diventi disordinato.

In realtà stavamo solo parlando del pool di risorse nella chatroom, in realtà. È un'ottima pratica, ed è bello sentire che lo stai facendo. :)


1
Se l'ordine di aggiornamento non è importante, la giunzione dovrebbe essere semplice come spostare l'ultima entità nell'indice corrente e ridurre il conteggio del pool e l'indice dell'iteratore.
Kaj,

Caspita, ottimo punto Kaj! :)
Ricket,

2

Personalmente, vorrei utilizzare un elenco collegato. L'iterazione su una lista piaciuta è veloce, così come l'aggiunta e la rimozione di elementi. L'uso di un array o di un vettore sarebbe una buona scelta se si necessita dell'accesso diretto agli elementi nella struttura (ad es. Accesso a un indice), ma non sembra che sia necessario.

Ogni volta che si rimuove un elemento dall'elenco collegato, è possibile aggiungerlo a un pool di oggetti che possono quindi essere riciclati per risparmiare sull'allocazione di memoria.

Ho usato le strutture di dati poligonali in diversi progetti e sono stato molto contento di loro.

Modifica: scusa, penso che la risposta non sia stata molto chiara in termini di strategia di rimozione: suggerirei di rimuovere l'elemento dall'elenco, non appena è morto e aggiungerlo direttamente alla struttura del pool (ricicla). Poiché la rimozione di un elemento da un elenco collegato è molto efficace, non vedo alcun problema nel farlo.


1
Presumo che tu suggerisca un elenco con doppio link qui? (avanti / indietro)? Inoltre: stai suggerendo una sorta di pool sugli elementi di collegamento o stai allocando dinamicamente ciascun porta-puntatore nell'elenco collegato?
Simon,

Sì, dovrebbe essere un elenco a doppio link più adatto a tale compito. Grazie per la segnalazione! Per quanto riguarda il riutilizzo degli elementi: stavo pensando a una classe di pool / datastruttura specializzata, quella che crea nuovi oggetti su richiesta o utilizza istanze esistenti se ce ne sono nel pool. Pertanto sarebbe opportuno rimuovere effettivamente gli elementi "morti" dall'elenco e aggiungerli al pool per un utilizzo successivo.
bummzack,

Un elenco collegato singolarmente andrà bene. Le liste doppiamente collegate danno solo il vantaggio di iterare in entrambe le direzioni. Per scorrere un elenco collegato singolarmente con l'opzione di rimuovere l'elemento corrente, è necessario tenere traccia della voce precedente.
deft_code

@caspin si esattamente. Se si utilizza un elenco a collegamento singolo, è necessario tenere traccia dei nodi precedenti e collegare il loro nextpuntatore al nodo dopo uno eliminato. Se non vuoi il fastidio di farlo da solo, un elenco a doppio link sarebbe la DataStructure preferita.
bummzack,

1

"metti semplicemente una bandiera su di essa per dire" salta su di me, sono morto. "Sto unendo le mie entità, quindi un'Entità che è morta è molto probabile che sia di nuovo viva ad un certo punto"

Penso che tu abbia risposto alla tua domanda riguardo a questa specifica domanda. Mi allontanerei dalle matrici se hai mai intenzione di lavorare su di esse oltre a push e pop. Gli elenchi collegati sarebbero un modo più intelligente di procedere se si prevede di eseguire operazioni pesanti. Detto questo, se prevedi di reintegrare la stessa entità nel gioco, ha senso impostare solo una variabile booleana e verificarla durante i cicli delle operazioni di gioco.


0

Una soluzione pulita e generale che ho trovato su una lib che ho usato era usare una mappa bloccabile.

hai 2 operazioni lock()e unlock(), mentre esegui l'iterazione sulla mappa lock(), ora da questo punto ogni operazione che modifica la mappa non ha effetto, viene semplicemente inserita in una CommandQueueche verrà eseguita una volta chiamataunlock() .

Quindi la rimozione di un'entità avrebbe il seguente pseudo-codice:

void lockableMap::remove(std::string id) {
   if(isLocked) {
       commandQueue.add(new RemoveCommand(id));
   } else {
       //remove element from map
   }

e quando tu unlock()

isLocked = false
commandQueue.execute(this);

L'unica cosa che devi considerare è che rimuoverai l'entità solo dopo il ciclo.

EDIT: questa è la soluzione proposta da Simon.



0

Ho due metodi.

Quando si chiama un oggetto da eliminare, in realtà imposta due flag:

1.Uno per dire al contenitore che un oggetto è stato cancellato

2.Uno per dire al contenitore quali oggetti sono stati richiesti per essere cancellati

void object::deleteObject()
{
    container->objectHasBeenDeleted = true;
    isToDelete = true;
}

Uno Utilizzando un vettore di oggetti

std::vector<object*> objects;

Quindi, nella funzione di aggiornamento, controlla se un oggetto è stato eliminato e in tal caso scorre tutti gli oggetti e rimuovi quelli che hanno un flag di eliminazione

void container::update()
{
    if (objectHasBeenDeleted)
    {
        std::vector<object*>::iterator ListIterator;
        for(ListIterator=objects.begin(); ListIterator!=objects.end();)
        {
            if( (*ListIterator)->isToDelete )
            {
                ListIterator = objects.erase(ListIterator);
                delete *ListIterator;
            }
            else {
                ++ListIterator;
            }
        }
    objectHasBeenDeleted = false;
    }
}

Due Usando un (puntatore a) un vettore di oggetti.

std::vector<object*> *objects;

Nella funzione di aggiornamento, se un oggetto deve essere eliminato, scorrere gli oggetti e aggiungere quelli che non devono essere eliminati in un nuovo vettore. elimina il vettore oggetti e imposta il puntatore sul nuovo vettore

void container::update()
{
    if (objectHasBeenDeleted)
    {
        std::vector<object*> *newVector;
        unsigned long i;
        for (i = 0; i < objects->size(); i++)
        {
            if (!objects->at(i)->isToDelete)
            {
                newVector->push_back(objects->at(i));
            }
        }
        delete objects;
        objects = newVector;
        objectHasBeenDeleted = false;
    }
}
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.