Wie vermeide ich, dass Spielobjekte sich versehentlich in C ++ selbst löschen?

20

Angenommen, in meinem Spiel befindet sich ein Monster, das Kamikaze auf dem Spieler explodieren lassen kann. Lass uns zufällig einen Namen für dieses Monster auswählen: einen Creeper. Die CreeperKlasse hat also eine Methode, die ungefähr so ​​aussieht:

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

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

Die Ereignisse werden nicht in die Warteschlange gestellt, sondern sofort versendet. Dies bewirkt, dass das CreeperObjekt irgendwo innerhalb des Aufrufs von gelöscht wird postEvent. Etwas wie das:

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

Da das CreeperObjekt gelöscht wird, während die kamikazeMethode noch ausgeführt wird, stürzt es beim Zugriff ab this->location().

Eine Lösung besteht darin, die Ereignisse in einen Puffer zu stellen und sie später zu versenden. Ist das die übliche Lösung in C ++ - Spielen? Es fühlt sich wie ein Hack an, aber das könnte nur an meiner Erfahrung mit anderen Sprachen mit anderen Speicherverwaltungsmethoden liegen.

Gibt es in C ++ eine bessere allgemeine Lösung für dieses Problem, bei der sich ein Objekt versehentlich aus einer seiner Methoden löscht?

Tom Dalling
quelle
6
Wie wäre es, wenn Sie postEvent am ENDE der Kamikaze-Methode anstatt am Anfang aufrufen?
Hackworth
@ Hackworth das würde für dieses spezielle Beispiel funktionieren, aber ich suche nach einer allgemeineren Lösung. Ich möchte Ereignisse von überall aus veröffentlichen können und keine Angst vor Abstürzen haben.
Tom Dalling
Sie können auch einen Blick auf die Implementierung von autoreleasein Objective-C werfen , wo Löschungen "nur für ein bisschen" zurückgehalten werden.
Chris Burt-Brown

Antworten:

40

Nicht löschen this

Selbst implizit.

- Je -

Das Löschen eines Objekts, während sich eine seiner Mitgliedsfunktionen noch auf dem Stapel befindet, ist problematisch. Jede Codearchitektur, die dazu führt ("versehentlich" oder nicht), ist objektiv schlecht , gefährlich und sollte sofort überarbeitet werden . In diesem Fall, wenn dein Monster 'World :: handleEvent' aufrufen darf , lösche auf keinen Fall das Monster in dieser Funktion!

(In dieser speziellen Situation ist es meine übliche Vorgehensweise, das Monster eine "tote" Flagge auf sich selbst setzen zu lassen und das Weltobjekt - oder so ähnlich - einmal pro Frame auf diese "tote" Flagge testen zu lassen und diese Objekte zu entfernen Zu diesem Zeitpunkt sendet die Welt auch Benachrichtigungen über das Löschen, damit andere Objekte auf der Welt wissen, dass das Monster es hat Die Welt tut dies zu einem sicheren Zeitpunkt, wenn sie weiß, dass derzeit keine Objekte verarbeitet werden, sodass Sie sich keine Sorgen mehr machen müssen, dass der Stapel zu einem bestimmten Zeitpunkt abgewickelt wird wo der 'this' Zeiger auf den freigegebenen Speicher zeigt.)

Trevor Powell
quelle
6
Die zweite Hälfte Ihrer Antwort in Klammern war hilfreich. Das Abrufen einer Flagge ist eine gute Lösung. Aber ich habe gefragt, wie ich es vermeiden soll, ob es gut oder schlecht ist. Wenn eine Frage fragt : „ Wie kann ich versehentlich tun X vermeiden? “, Und die Antwort ist „ Nie X jemals tun, auch nicht versehentlich “ in fett, das eigentlich nicht die Frage beantworten.
Tom Dalling
7
Ich stehe hinter den Kommentaren, die ich in der ersten Hälfte meiner Antwort abgegeben habe, und ich habe das Gefühl, dass sie die Frage in ihrer ursprünglichen Formulierung vollständig beantworten. Der wichtige Punkt, den ich hier wiederhole, ist, dass sich ein Objekt nicht selbst löscht. Immer . Es ruft niemanden an, um es zu löschen. Immer . Stattdessen müssen Sie etwas anderes außerhalb des Objekts haben, das das Objekt besitzt und dafür verantwortlich ist, zu bemerken, wann das Objekt zerstört werden muss. Dies ist keine "nur wenn ein Monster stirbt" Sache; Dies gilt für jeden C ++ - Code immer, überall und für alle Zeiten. Keine Ausnahmen.
Trevor Powell
3
@ TrevorPowell Ich sage nicht, dass Sie falsch liegen. In der Tat stimme ich Ihnen zu. Ich sage nur, dass es die gestellte Frage nicht beantwortet. Es ist, als ob Sie mich gefragt hätten, wie ich Audio in mein Spiel bekomme, und meine Antwort lautete: " Ich kann nicht glauben, dass Sie kein Audio haben. Fügen Sie Audio jetzt in Ihr Spiel ein. " Dann in Klammern unten I put " (Sie können FMOD verwenden) ", was eine tatsächliche Antwort ist.
Tom Dalling
6
@ TrevorPowell Hier liegen Sie falsch. Es ist nicht "nur Disziplin", wenn ich keine Alternativen kenne. Der von mir angegebene Beispielcode ist rein theoretisch. Ich weiß bereits, dass es sich um ein schlechtes Design handelt, aber mein C ++ ist verrostet. Deshalb dachte ich, ich würde hier nach besseren Designs fragen, bevor ich tatsächlich codiere, was ich will. Also fragte ich nach alternativen Designs. " Löschvormerkung hinzufügen " ist eine Alternative. " Mach es nie " ist keine Alternative. Es sagt mir nur, was ich bereits weiß. Es fühlt sich an, als hättest du die Antwort geschrieben, ohne die Frage richtig zu lesen.
Tom Dalling
4
@ Bobby Die Frage ist "Wie man kein X macht". Einfach "Do NOT do X" zu sagen, ist eine wertlose Antwort. Wäre die Frage "Ich habe X gemacht" oder "Ich habe überlegt, X zu machen" oder eine andere Variante davon, dann würde sie die Parameter der Metadiskussion erfüllen, aber nicht in der vorliegenden Form.
Joshua Drake
21

Anstatt Ereignisse in einem Puffer in die Warteschlange zu stellen, müssen Sie Löschvorgänge in einem Puffer in die Warteschlange stellen . Delayed-Deletion hat das Potenzial, die Logik massiv zu vereinfachen. Sie können tatsächlich Speicher am Ende oder Anfang eines Frames freigeben, wenn Sie wissen, dass Ihren Objekten nichts Interessantes passiert, und von absolut jedem Ort aus löschen.

John Calsbeek
quelle
1
Lustigerweise erwähnen Sie das, weil ich mir überlegt habe, wie schön ein NSAutoreleasePoolvon Objective-C in dieser Situation wäre. Möglicherweise müssen Sie eine DeletionPoolmit C ++ - Vorlagen oder so etwas machen.
Tom Dalling
@TomDalling Wenn Sie den Puffer außerhalb des Objekts erstellen, sollten Sie darauf achten, dass ein Objekt möglicherweise aus mehreren Gründen in einem einzelnen Frame gelöscht werden soll, und Sie können versuchen, es mehrmals zu löschen.
John Calsbeek
Sehr richtig. Ich werde die Zeiger in einem std :: set behalten müssen.
Tom Dalling
5
Anstelle eines Puffers mit zu löschenden Objekten können Sie auch einfach ein Flag im Objekt setzen. Sobald Sie feststellen, wie viel Sie vermeiden möchten, während der Laufzeit neu aufzurufen oder zu löschen, und in einen Objektpool wechseln, wird dies sowohl einfacher als auch schneller.
Sean Middleditch
4

Anstatt die Welt mit dem Löschen befassen zu müssen, können Sie die Instanz einer anderen Klasse als Bucket zum Speichern aller gelöschten Entitäten verwenden. Diese bestimmte Instanz sollte ENTITY_DEATHEreignisse abhören und sie so behandeln, dass sie in eine Warteschlange eingereiht werden. Das Worldkann dann über diese Instanzen iterieren und Operationen nach dem Tod ausführen, nachdem der Frame gerendert wurde, und diesen Bucket "löschen", was wiederum das tatsächliche Löschen von Instanzen von Entitäten durchführen würde.

Ein Beispiel für eine solche Klasse wäre: http://ideone.com/7Upza

Vite Falcon
quelle
+1, das ist eine Alternative zum direkten Kennzeichnen der Entitäten. Noch direkter: Erstellen Sie direkt in der WorldKlasse eine lebendige Liste und eine tote Liste von Entitäten .
Laurent Couvidou
2

Ich würde vorschlagen, eine Factory zu implementieren, die für alle Zuordnungen von Spielobjekten im Spiel verwendet wird. Anstatt selbst neu anzurufen, würden Sie die Fabrik bitten, etwas für Sie zu kreieren.

Beispielsweise

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

Wann immer Sie ein Objekt löschen möchten, schiebt die Factory das Objekt in einen Puffer, der im nächsten Frame gelöscht wird. Eine verzögerte Zerstörung ist in den meisten Szenarien sehr wichtig.

Denken Sie auch daran, alle Nachrichten mit einer Verzögerung von einem Frame zu senden. Es gibt nur wenige Ausnahmen, bei denen Sie sofort senden müssen, die überwiegende Mehrheit der Fälle jedoch nur

Millianz
quelle
2

Sie können Managed Memory in C ++ selbst implementieren, sodass beim ENTITY_DEATHAufruf lediglich die Anzahl der Referenzen um eins reduziert wird.

Später, wie @John am Anfang jedes Frames vorgeschlagen hat, können Sie überprüfen, welche Entities nutzlos sind (diejenigen mit Nullreferenzen) und sie löschen. Zum Beispiel können Sie verwenden boost::shared_ptr<T>( hier dokumentiert ) oder wenn Sie C ++ 11 (VC2010) verwendenstd::tr1::shared_ptr<T>

Ali1S232
quelle
Nur std::shared_ptr<T>keine technischen Berichte! - Sie müssen einen benutzerdefinierten Löscher angeben. Andernfalls wird das Objekt sofort gelöscht, wenn der Referenzzähler Null erreicht.
Leftaroundabout
1
@leftaroundabout es hängt wirklich davon ab, zumindest musste ich tr1 in gcc verwenden. aber in VC gab es keine Notwendigkeit dafür.
Ali1S232
2

Verwenden Sie Pooling und löschen Sie keine Objekte. Ändern Sie stattdessen die Datenstruktur, in der sie registriert sind. Zum Beispiel für das Rendern von Objekten gibt es ein Szenenobjekt und alle Entitäten, die irgendwie für das Rendern, die Kollisionserkennung usw. registriert sind. Anstatt das Objekt zu löschen, trennen Sie es von der Szene und fügen es in einen Pool toter Objekte ein. Diese Methode verhindert nicht nur Speicherprobleme (wie das Löschen eines Objekts selbst), sondern beschleunigt möglicherweise auch Ihr Spiel, wenn Sie den Pool ordnungsgemäß verwenden.

hevi
quelle
1

Was wir in einem Spiel gemacht haben, war die Verwendung der neuen Platzierung

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

Der eventPool war nur ein großes Array von Speicher, der aufgeschlüsselt wurde, und der Zeiger auf jedes Segment wurde gespeichert. Somit würde alloc () die Adresse eines freien Speicherblocks zurückgeben. In unserem eventPool wurde der Speicher als Stapel behandelt. Nachdem alle Ereignisse gesendet wurden, haben wir den Stapelzeiger einfach auf den Anfang des Arrays zurückgesetzt.

Aufgrund der Funktionsweise unseres Ereignissystems mussten wir die Destruktoren nicht am Abend aufrufen. Der Pool würde also einfach den Speicherblock als frei registrieren und ihn zuweisen.

Dies gab uns eine enorme Geschwindigkeit.

Außerdem ... haben wir tatsächlich Speicherpools für alle dynamisch zugewiesenen Objekte in der Entwicklung verwendet, da dies eine großartige Möglichkeit war, Speicherlecks zu finden. Wenn sich beim Beenden des Spiels (normalerweise) noch Objekte in den Pools befanden, war dies wahrscheinlich der Fall ein mem leck.

PhilCK
quelle