Come evitare che gli oggetti di gioco si cancellino accidentalmente in C ++


20

Diciamo che il mio gioco ha un mostro che può esplodere kamikaze sul giocatore. Scegliamo un nome per questo mostro a caso: un Creeper. Quindi, la Creeperclasse ha un metodo che assomiglia a questo:

void Creeper::kamikaze() {
    EventSystem::postEvent(ENTITY_DEATH, this);

    Explosion* e = new Explosion;
    e->setLocation(this->location());
    this->world->addEntity(e);
}

Gli eventi non sono in coda, vengono spediti immediatamente. Questo fa sì che l' Creeperoggetto venga eliminato da qualche parte all'interno della chiamata postEvent. Qualcosa come questo:

void World::handleEvent(int type, void* context) {
    if(type == ENTITY_DEATH){
        Entity* ent = dynamic_cast<Entity*>(context);
        removeEntity(ent);
        delete ent;
    }
}

Poiché l' Creeperoggetto viene eliminato mentre il kamikazemetodo è ancora in esecuzione, si arresta in modo anomalo quando tenta di accedere this->location().

Una soluzione è accodare gli eventi in un buffer e inviarli in un secondo momento. È quella la soluzione comune nei giochi C ++? Sembra un po 'un trucco, ma potrebbe essere solo a causa della mia esperienza con altre lingue con diverse pratiche di gestione della memoria.

In C ++, esiste una soluzione generale migliore a questo problema in cui un oggetto si elimina accidentalmente dall'interno di uno dei suoi metodi?


6
che ne dici di chiamare postEvent alla fine del metodo kamikaze invece che all'inizio?
Hackworth,

@Hackworth che avrebbe funzionato per questo esempio specifico, ma sto cercando una soluzione più generale. Voglio essere in grado di pubblicare eventi da qualsiasi luogo e non aver paura di causare arresti anomali.
Tom Dalling,

Potresti anche dare un'occhiata all'implementazione di autoreleasein Objective-C, dove le eliminazioni vengono trattenute fino a "solo un po '".
Chris Burt-Brown,

Risposte:


40

Non cancellare this

Anche implicitamente.

- Mai -

L'eliminazione di un oggetto mentre una delle sue funzioni membro è ancora nello stack sta implorando problemi. Qualsiasi architettura di codice che si traduca in ciò che accade ("accidentalmente" o no) è oggettivamente cattiva , è pericolosa e dovrebbe essere immediatamente riformulata . In questo caso, se al tuo mostro sarà permesso di chiamare "World :: handleEvent", non eliminare in nessun caso il mostro all'interno di quella funzione!

(In questa situazione specifica, il mio approccio abituale è di far mettere il mostro su una bandiera "morta" su se stesso e di far testare l'oggetto del mondo - o qualcosa del genere - per quella bandiera "morta" una volta per fotogramma, rimuovendo quegli oggetti dal suo elenco di oggetti nel mondo, o eliminandolo o restituendolo a un pool di mostri o qualsiasi cosa sia appropriata. In questo momento, il mondo invia anche notifiche sulla cancellazione, quindi altri oggetti nel mondo sanno che il mostro ha ha smesso di esistere e può rilasciare qualsiasi puntatore che potrebbe trattenere. Il mondo lo fa in un momento sicuro, quando sa che nessun oggetto è in fase di elaborazione, quindi non devi preoccuparti che lo stack si svolga fino a un certo punto dove il puntatore 'this' punta alla memoria liberata.)


6
La seconda metà della tua risposta tra parentesi è stata utile. Il polling per una bandiera è una buona soluzione. Ma stavo chiedendo come evitare di farlo, non se fosse una cosa buona o cattiva da fare. Se una domanda chiede " Come posso evitare di fare X accidentalmente? ", E la tua risposta è " Mai e poi mai X, mai accidentalmente " in grassetto, ciò non risponde effettivamente alla domanda.
Tom Dalling,

7
Sono dietro i commenti che ho fatto nella prima metà della mia risposta, e sento che rispondono pienamente alla domanda come originariamente formulata. Il punto importante che ripeterò qui è che un oggetto non si elimina da solo. Mai . Non chiama qualcun altro per eliminarlo. Mai . Invece, devi avere qualcos'altro, al di fuori dell'oggetto, che possiede l'oggetto ed è responsabile di notare quando l'oggetto deve essere distrutto. Questa non è una cosa "proprio quando muore un mostro"; questo è per tutto il codice C ++ sempre, ovunque, per sempre. Nessuna eccezione.
Trevor Powell,

3
@TrevorPowell Non sto dicendo che ti sbagli. In effetti, sono d'accordo con te. Sto solo dicendo che in realtà non risponde alla domanda che è stata posta. È come se mi chiedessi " Come posso ottenere l'audio nel mio gioco? " E la mia risposta è stata " Non riesco a credere che tu non abbia l'audio. Metti l'audio nel tuo gioco in questo momento. " Poi tra parentesi in fondo a me metti " (Puoi usare FMOD) ", che è una risposta effettiva.
Tom Dalling,

6
@TrevorPowell Questo è dove ti sbagli. Non è "solo disciplina" se non conosco alternative. Il codice di esempio che ho fornito è puramente teorico. So già che è un cattivo design, ma il mio C ++ è arrugginito, quindi ho pensato di chiedere qui dei migliori progetti prima di programmare effettivamente ciò che voglio. Quindi sono venuto a chiedere progetti alternativi. " Aggiungi un flag di eliminazione " è un'alternativa. " Non farlo mai " non è un'alternativa. Mi sta solo dicendo quello che già so. Sembra che tu abbia scritto la risposta senza leggere correttamente la domanda.
Tom Dalling,

4
@Bobby La domanda è "Come NON fare X". Dire semplicemente "NON fare X" è una risposta senza valore. Se la domanda fosse stata "Ho fatto X" o "Stavo pensando di fare X" o qualsiasi altra variante di questo, allora avrebbe incontrato i parametri della meta discussione, ma non nella sua forma attuale.
Joshua Drake,

21

Invece di mettere in coda gli eventi in un buffer, accodare le eliminazioni in un buffer. La cancellazione ritardata ha il potenziale per semplificare enormemente la logica; puoi effettivamente liberare memoria alla fine o all'inizio di un fotogramma quando sai che non sta accadendo nulla di interessante ai tuoi oggetti ed eliminarlo da qualsiasi luogo.


1
È divertente che tu lo abbia menzionato, perché stavo pensando a quanto NSAutoreleasePoolsarebbe bello un Objective-C in questa situazione. Potrebbe essere necessario creare un DeletionPoolmodello C ++ o qualcosa del genere.
Tom Dalling,

@TomDalling L'unica cosa a cui prestare attenzione se si rende il buffer esterno all'oggetto è che un oggetto potrebbe voler essere eliminato per più motivi su un singolo fotogramma e sarebbe possibile provare a eliminarlo più volte.
John Calsbeek,

Verissimo. Dovrò mantenere i puntatori in uno std :: set.
Tom Dalling,

5
Invece di un buffer di oggetti da eliminare, è anche possibile impostare un flag nell'oggetto. Una volta che inizi a capire quanto vuoi evitare di chiamare nuovo o eliminare durante il runtime e passare a un pool di oggetti, sarà sia più semplice che più veloce.
Sean Middleditch il

4

Invece di consentire al mondo di gestire l'eliminazione, è possibile consentire all'istanza di un'altra classe di fungere da bucket per archiviare tutte le entità eliminate. Questa particolare istanza dovrebbe ascoltare gli ENTITY_DEATHeventi e gestirli in modo tale da metterli in coda. L' Worldpuò quindi iterare in questi casi ed eseguire operazioni di post-morte dopo che il telaio è stato reso e 'chiaro' questo secchio, che a sua volta eseguire l'eliminazione effettiva delle istanze entità.

Un esempio di tale classe sarebbe il seguente: http://ideone.com/7Upza


+1, è un'alternativa alla segnalazione diretta delle entità. Più direttamente, basta avere una lista viva e una lista morta di entità direttamente nella Worldclasse.
Laurent Couvidou,

2

Suggerirei di implementare una fabbrica utilizzata per tutte le allocazioni di oggetti di gioco nel gioco. Quindi, invece di chiamarti nuovo, diresti alla fabbrica di creare qualcosa per te.

Per esempio

Object* o = gFactory->Create("Explosion");

Ogni volta che si desidera eliminare un oggetto, la factory inserisce l'oggetto in un buffer che viene cancellato dal fotogramma successivo. La distruzione ritardata è molto importante nella maggior parte degli scenari.

Considera anche di inviare tutti i messaggi con un ritardo di un frame. Esistono solo un paio di eccezioni a cui è necessario inviare immediatamente, la stragrande maggioranza dei casi, tuttavia, è sufficiente


2

Puoi implementare tu stesso la memoria gestita in C ++, così che quando ENTITY_DEATHviene chiamato, tutto ciò che accade è che il numero dei suoi riferimenti viene ridotto di uno.

Successivamente, come suggerito da @John all'inizio di ogni frame, è possibile verificare quali entità sono inutili (quelle con zero riferimenti) ed eliminarle. Ad esempio puoi usare boost::shared_ptr<T>( documentato qui ) o se stai usando C ++ 11 (VC2010)std::tr1::shared_ptr<T>


Solo std::shared_ptr<T>, non relazioni tecniche! - Dovrai specificare un deleter personalizzato, altrimenti eliminerà immediatamente l'oggetto quando il conteggio dei riferimenti raggiunge lo zero.
leftaroundabout

1
@leftaroundabout dipende davvero, almeno dovevo usare tr1 in gcc. ma in VC non ce n'era bisogno.
Ali1S232,

2

Usa il pooling e non eliminare effettivamente gli oggetti. Invece, cambia la struttura dei dati in cui sono registrati. Ad esempio per il rendering di oggetti c'è un oggetto scena e tutte le entità in qualche modo registrate su di esso per il rendering, il rilevamento delle collisioni ecc. Invece di eliminare l'oggetto, staccarlo dalla scena e inserirlo in un pool di oggetti morti. Questo metodo non solo previene i problemi di memoria (come un oggetto che si elimina da solo) ma può anche velocizzare il gioco se si utilizza correttamente il pool.


1

Quello che abbiamo fatto in un gioco è stato utilizzare il posizionamento nuovo

SomeEvent* obj = new(eventPool.alloc()) new SomeEvent();

eventPool era solo una grande quantità di memoria che era stata ricavata e il puntatore a ciascun segmento veniva archiviato. Allocare () restituirebbe l'indirizzo di un blocco di memoria libero. Nel nostro eventPool la memoria è stata trattata come uno stack, quindi dopo che tutti gli eventi sono stati inviati, abbiamo semplicemente reimpostato il puntatore dello stack all'inizio dell'array.

A causa del modo in cui il nostro sistema di eventi ha funzionato, non abbiamo avuto bisogno di chiamare i distruttori sugli eventi. Quindi il pool registra semplicemente il blocco di memoria come libero e lo alloca.

Questo ci ha dato un'enorme velocità.

Inoltre ... Abbiamo effettivamente utilizzato i pool di memoria per tutti gli oggetti allocati dinamicamente nello sviluppo, in quanto è stato un ottimo modo per trovare perdite di memoria, se c'erano oggetti lasciati nei pool quando il gioco è uscito (normalmente), allora era probabile che ci fosse una perdita di mem.

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.