Referenz auf temporäre Spielobjekte

Design Patterns, Erklärungen zu Algorithmen, Optimierung, Softwarearchitektur
Forumsregeln
Wenn das Problem mit einer Programmiersprache direkt zusammenhängt, bitte HIER posten.
Benutzeravatar
Jonathan
Establishment
Beiträge: 2353
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Referenz auf temporäre Spielobjekte

Beitrag von Jonathan »

Wie verwaltet man am besten Referenzen auf Spielobjekte, die gelöscht werden können?

Nehmen wir beispielsweise einmal an ich hätte folgendes, vereinfachtest Szenario: Mein Spiel hat eine Reihe von Avatar-Objekten. Einen steuert der Spieler, die anderen übernimmt die KI. Die KI kann Gegner finden und beschließen anzugreifen, um zu speichern, wen sie angreift, hält sie eine Referenz auf den Gegner. Wenn der Gegner aber aus einem anderen Grund stirbt, wird sein Objekt gelöscht und die KI löst im nächsten Durchlauf eine Zugriffsverletzung aus. Wie verhindert man das jetzt? (Ich gehe davon aus, dass alle Avatar-Objekte von einem Game-Objekt verwaltet werden, welches damit auch für das Löschen zuständig ist.)

Ich habe mir dazu ein paar Gedanken gemacht, und das sind meine Alternativen. Grundsätzlich muss der Benutzer halt wissen, ob die Referenz noch gültig ist.

1. Jedes Objekt weiß, wer eine Referenz auf das Objekt hält und informiert alle, wenn es gelöscht wird. Sicher zu wissen, wer dich kennt klingt schon nicht trivial, außerdem musst du auch wissen, ob die Objekte die dich kennen noch existieren. Also braucht man auch eine Rückreferenz und spätestens hier wird es unübersichtlich.

2. Objekte werden nicht von einer zentralen Klasse besessen, sondern haben (per shared_ptr) mehrere Besitzer. Ein Objekt kann also mehrere Besitzer haben und wird erst gelöscht, wenn der letzte Besitzer gelöscht wird. Dadurch kann es keine Zugriffsverletzungen geben.
Leider funktioniert das überhaupt nicht. Beispielsweise muss das Spiel Objekte kennen um sie zeichnen zu können. Generell darf es einfach nicht vorkommen, dass irgendwo ein Schatten-Objekt existiert, dass vom Rest der Spiellogik nicht mehr beachtet wird. Wenn ein Objekt gelöscht werden soll, dann muss es auch vollständig gelöscht werden.

3. Der Benutzer kann abfragen, ob das Objekt noch existiert. Die Spielklasse, die alle Objekte verleitet, könnte für jeden Pointer einen Tabelleneintrag haben, in dem steht, ob das Objekt noch existiert. Statt direkte Referenzen hätte man eine Referenz auf den Eintrag, kann prüfen ob der Zeiger noch gültig ist, und falls ja auf das Objekt zugreifen. Diese Tabelle dürfte allerdings nie aufgeräumt werden, was nicht so schlimm ist, da sie verhältnismäßig klein sein sollte.
Alternativ durchsucht man die Liste der vorhandenen Objekte, ob das angeforderte noch dort ist und spart sich so die Tabelle. Wie dem auch sei, der Zugriff wird auf jeden Fall teurer, weil der Test jedesmal durchgeführt werden muss.

4. Spielobjekte werden nicht gelöscht, sondern auf 'ungültig' gesetzt. Das wäre nützlich, wenn ein Objekt zerstört wird und man noch die Überreste anzeigen möchte. Aber halbe Objekte möchte man ja eigentlich auch nicht haben und irgendwann sollen ja vielleicht auch mal die Überreste verschwinden.

5. Nur temporäre Referenzen, die man sich am Beginn einer Funktion holt und von denen man weiß, dass sie bis zum Ende gültig sein werden. Die Gültigkeit zu garantieren klingt nicht in allen Fällen trivial und das Objekt immer neu zu suchen klingt auch nicht lustig.

Insgesamt sehe ich noch keine wirklich elegante Lösung. Als Beispiel wo es gerade für mich relevant wurde: In meinem Spiel können Fahrzeuge ein Radarsystem haben (modulare Fahrzeuge, verschiedene System-Konfigurationen möglich), welches einmal pro Frame schaut, was sich in der Nähe befindet. Die GUI-Klasse vom Spielerfahrzeug kann die Daten nehmen und auf einem Radarschirm darstellen, die KI kann damit Gegner zum angreifen finden. Das Laserobjekt kann Fahrzeuge beschießen und zerstören. Die 3 Objekte sind relativ unabhängig voneinander und in meinem Fall wurde eben erst das Radar, dann der Laser und dann die GUI aktualisiert. Das Radar sieht etwas, der Laser zerstört es und die GUI erzeugt beim Darstellen einen Zugriffsfehler.
Klar, ich könnte die Reihenfolge ändern. Die in allen Kombinationen richtig zu halten muss aber auch erstmal sichergestellt werden. Klar, ich könnte Objekte markieren und erst am Ende eines Frames löschen. Würde hier auch helfen, aber die KI will sich später ja nicht in jedem Frame überlegen, welchen Gegner sie angreifen will.

Das Problem ansich scheint mir sehr generell zu sein, eigentlich geht es ja nur darum, dass Objekte die ich kenne, verschwinden können. Wie geht ihr damit im Allgemeinen um?
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Benutzeravatar
Sternmull
Establishment
Beiträge: 264
Registriert: 27.04.2007, 00:30
Echter Name: Til
Wohnort: Dresden

Re: Referenz auf temporäre Spielobjekte

Beitrag von Sternmull »

Du hast das Problem ja eigentlich schon ganz gut analysiert. Da du shared_ptr ja schon erwähnt hast, könntest du mit weak_ptr glücklich werden. Das würde dann so laufen das die Eigentümer-Hierarchie der Objekte per shared_ptr abgebebildet wird, die problematischen Querverweise aber durch weak_ptr dargestellt werden. Das kommt deiner Tabellen-Idee sehr nahe.

Die Objekte können sich dann also darauf verlassen das ihre Bestandteile existieren, denn die halten sie ja per shared_ptr. Verweise auf "Fremdobjekte" werden per weak_ptr verwaltet und werden somit automatisch NULL wenn das Zielobjekt nicht mehr existiert.
Benutzeravatar
Jonathan
Establishment
Beiträge: 2353
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von Jonathan »

Ok das klingt in der Tat hilfreich. Ich habe shared_ptr nie benutzt, weil ich es dämlich fand, nicht zu wissen, wem ein Objekt gehört. Aber in diesem Zusammenhang hört es sich nach einer einfachen und guten Lösung an.

Aber wie ist es intern Implementiert? Wie hoch sind die Kosten? Die meisten Artikel die ich finden konnte, beschrieben nur wie man damit zirkuläre Referenzen aufbricht, aber darum geht es hier ja gar nicht.

Meine Vorstellung ist: Ein shared_ptr speichert den Zeiger auf das Objekt und einen Zeiger auf einen Counter (also ein int* z.B.). Wird der shared_ptr kopiert, wird der Counter inkrementiert und beide Zeiger kopiert. Wird der letzte shared_ptr gelöscht, wird sowohl das Objekt als auch der Counter freigegeben.Initialisiert man zwei shared_ptr mit dem selben Objekt-Pointer, knallt es, sobald der zweite gelöscht wird (d.h. es gibt keine globale Datenbank Objekt/Benutzer sondern nur einen Counter, den die Pointer sich teilen).
Erwartete Kosten: Kopieren von zwei Zeigern statt einem, Inkrementieren / Dekrementieren eines Counters und Zugriff auf zwei Entfernte Speicherbereiche anstatt einem (die ersten 2 Punkte sind lächerlich, der letzte könnte aus Cache-Gründen ärgerlich sein).

Sehe ich das so in etwa richtig? Wenn ja, sehe ich kaum, wie man das Problem billiger lösen könnte, ich hätte es mit Bordmitteln und äußerst geringem Aufwand gelöst und wenn ich nur weak_ptr beim Abfragen zurückgebe sollte das Programm auch robust sein. Das ich bei jedem Zugriff noch eine Behandlung einbauen muss, was passiert, wenn es das Objekt nicht mehr gibt, kann man ja ohnehin prinzipiell nicht vermeiden, aber derartige Fehler sollten sich sehr leicht finden lassen, wenn der Debugger mir einen weak_ptr mit 0 als Inhalt anzeigt.
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Helmut
Establishment
Beiträge: 237
Registriert: 11.07.2002, 15:49
Wohnort: Bonn
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von Helmut »

Ein shared_ptr hat den weiteren Nachteil, dass er pro Instanz auf die er zeigt eine kleine Heapallokation machen muss.
Auch musst du gut darauf achten, dass keine zirkulären Referenzen entstehen und die Dinger zu debuggen macht auch keinen Spaß.
Ich würde dir die Nummer 4 empfehlen:
4. Spielobjekte werden nicht gelöscht, sondern auf 'ungültig' gesetzt. Das wäre nützlich, wenn ein Objekt zerstört wird und man noch die Überreste anzeigen möchte. Aber halbe Objekte möchte man ja eigentlich auch nicht haben und irgendwann sollen ja vielleicht auch mal die Überreste verschwinden.
Du musst einfach nur garantieren, dass du jeden Frame in jedem Objekt alle Zeiger, die auf "tote" Objekte zeigen, auf null setzt. Da man jedes Objekt pro Frame sowieso irgendwie berührt sollte das ja kein Problem sein. Am Ende des Frames kannst du dann alle als tot markieten Objekte tatsächlich löschen. Oder, falls Objekte auch mitten im Frame als tot markiert werden können, löscht du nur die Objekte die seit mindestens 2 Frames als tot markiert sind.
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: Referenz auf temporäre Spielobjekte

Beitrag von Spiele Programmierer »

Auf dieses Problem bin ich auch bereits des Öfteren gestoßen. Und ich kann gleich sagen, eine wirklich gute Lösung, außer der simplen Vermeidung solcher Situationen, habe ich bisher nicht gefunden. Man kann auch noch versuchen das Problem so zu umgehen, in dem ich nach Möglichkeit keine Zeiger auf andere Objekte speichere, sondern einen anderen eindeutigen Wert(je nach dem zb. Name, 64Bit ID oder Hash). Wenn man das Objekt dann nochmal in einem späteren Spielzyklus braucht, kann man es damit suchen. Von den von dir genannten Vorschlägen finde ich eigentlich nur den 1. Vorschlag vertretbar. Die anderen laufen eigentlich alle auf ein mehr oder weniger großes Speicherleck hinaus, mit dem das Problem scheinbar gelöst wird.
Sehe ich das so in etwa richtig?
Ja, ungefähr, allerdings hast du einige Dinge ausgelassen. Zum Beispiel ist ein "std::shared_ptr" doppelt so groß, was auch beim Verwenden ohne Kopieren mit dem erwähnten Inkrementieren/Dekrementieren den Cache belastet. Zu beachten ist auch noch, dass für die Funktionsweise von "std::waek_ptr" ein weiterer Zähler existiert der die Anzahl verbleibender "std::waek_ptr" speichert. Der Speicherbereich auf dem der "std::waek_ptr" zeigt, darf ja erst freigegeben werden, wenn der letzte seiner Art gelöscht ist. (Auch wenn die owner als "std::shared_ptr" alse schon längst zerstört sind.) Sonst hätte der Zeiger ja das selbe Problem, dass du auch gerade hast. Das ist schonmal schlecht. Wenn man aber auch noch die eigentlich effizientere "std::make_shared"-Methode nutzt(sonst verdoppelt man zusätzlich auch noch die Anzahl Allokationen), dann kann der Speicher der gesamten Daten(also +sizeof(T)) erst freigegeben werden, wenn der letzte std::waek_ptr" zerstört wurde, weil sowohl die Zähler als auch die Daten in einer Allokation liegen. Solange also noch irgend ein "std::waek_ptr" existiert, kann die gesamte Allokation nicht aufgeräumt werden.

Im Prinzip würde ich ein "std::waek_ptr" von den praktischen Auswirkungen ohne "std::make_shared" mir etwa so vorstellen:

Code: Alles auswählen

class Data;
class Owner
{
   std::unique_ptr<Data> MyData;
   std::shared_ptr<Data*> MyDataRef; //Alle teilen sich einen gemeinsamen Zeiger auf die tatsächlichen Daten. Dieser zentrale Zeiger wird genullt, wenn die Daten entfernt wurden.
};
class User
{
   std::shared_ptr<Data*> MyDataRef; //Der Zeiger im Zeiger wird vom "Owner" auf 0 gesetzt, wenn es ungültig wird.
};
EDIT:
Den Vorschlag von Helmut finde ich auch sehr sinnvoll.
Benutzeravatar
Jonathan
Establishment
Beiträge: 2353
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von Jonathan »

Helmut hat geschrieben:Ein shared_ptr hat den weiteren Nachteil, dass er pro Instanz auf die er zeigt eine kleine Heapallokation machen muss.
Moment, was genau heißt das? Wie kann ein shared_ptr denn auf mehr als eine Instanz zeigen? Oder meinst du, dass alle shared_ptr die auf ein Objekt zeigen, jeweils eine eigene kleine Heapallokation machen? Wofür wäre die?
Spiele Programmierer hat geschrieben:Zum Beispiel ist ein "std::shared_ptr" doppelt so groß, was auch beim Verwenden ohne Kopieren mit dem erwähnten Inkrementieren/Dekrementieren den Cache belastet.
Naja, um ehrlich zu sein springe ich auch so schon eine Menge rum. Ich speicher halt unique_ptr auf meine Objekte, einfach weil es bequemer ist, das drüberiterieren wird dadurch aber natürlich für den Cache nicht gerade angenehm. Aber wenigstens bin ich mir da der Sache bewusst, was ich schlimmer finde sind Stellen, die langsam sind, ohne das man es ahnt, weswegen ich nochmal genau nachfragen wollte.
Spiele Programmierer hat geschrieben:Zu beachten ist auch noch, dass für die Funktionsweise von "std::waek_ptr" ein weiterer Zähler existiert der die Anzahl verbleibender "std::waek_ptr" speichert.
Ok, dass man zwei Zähler braucht klingt einleuchtend. Aber: Wo kommt der Speicher für den zweiten her? Er muss ja auch von allen weap_ptr geteilt werden, müssen ihn dann nicht auch schon alle shared_ptr kennen? (Weil man von denen ja die weak_ptr erstellen muss). D.h. jedesmal wenn man einen shared_ptr benutzt bezahlt man schon ein wenig dafür mit, dass man evtl. davon einen weak_ptr erstellen kann?
Aber wie auch immer, dann hat man halt zwei Zeiger, das macht die Sache auch nicht so viel schlimmer. Sie sollten dann ja auch direkt beisammen liegen.

zu make_shared: Ist die Idee, dass man dann nur einen Speicherbereich allokiert, in dem dann Objekt, shared-Zähler und Weak-Zähler liegen? Somit spart man sich eine Allokation und die Daten liegen auch ein wenig besser beisammen? Klingt gut und ich finde es auch nicht schlimm, wenn das komplette Objekt dann erst später gelöscht wird. Man sollte ja relativ schnell an allen Stellen merken, dass das Objekt nicht mehr existiert.
Apropo Freigeben: Ginge das dann so?: p=weak_ptr<t>(); Den Zeiger als solchen kann ich ja als Stackobjekt / Teil eines anderen Objektes nicht löschen, also weise ich ihn einfach einen leeren Zeiger zu, wodurch der geteilte Speicherbereich freigegeben wird?

Helmut hat geschrieben:Du musst einfach nur garantieren, dass du jeden Frame in jedem Objekt alle Zeiger, die auf "tote" Objekte zeigen, auf null setzt. Da man jedes Objekt pro Frame sowieso irgendwie berührt sollte das ja kein Problem sein. Am Ende des Frames kannst du dann alle als tot markieten Objekte tatsächlich löschen. Oder, falls Objekte auch mitten im Frame als tot markiert werden können, löscht du nur die Objekte die seit mindestens 2 Frames als tot markiert sind.
Joah, dann brauch ich nur zum einen einen sehr besonderen "ich bin tot" Zustand, wo ein Objekt überall ignoriert werden muss (wenn ich einen toten weak_ptr normal benutze merk ich das sofort) und ich muss sicherstellen, dass jedes Objekt jede Referenz in jedem Frame checkt und nicht nur, wenn es sie benutzt.


Insgesamt tendiere ich jetzt also dazu, in der Objektverwaltung unique_ptr durch shared_ptr zu ersetzen (wobei es dann immer nur EINEN shared_ptr für jedes Objekt geben wird) und nach außen nur weak_ptr statt normaler Zeiger rauszugeben. Und dann halt vor jeder Benutzung prüfen, ob das Objekt noch existiert. Vielleicht bau ich mir noch ein kleines Makro dass ich dann bei jeder Benutzung verwende und das mich zwingt, eine Behandlung für den Fall, dass das Objekt nicht existiert zu implementieren. Dann dürften eigentlich keine Lecks auftreten können und das ganze sollte nur minimal aufwändiger sein, als bisher.
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: Referenz auf temporäre Spielobjekte

Beitrag von Spiele Programmierer »

Moment, was genau heißt das? Wie kann ein shared_ptr denn auf mehr als eine Instanz zeigen? Oder meinst du, dass alle shared_ptr die auf ein Objekt zeigen, jeweils eine eigene kleine Heapallokation machen? Wofür wäre die?
Seine Aussage ist nicht allgemeingültig. Ein "shared_ptr" besteht aus mindestens zwei Zeigern. Einer zeigt auf das Programmobjekt und einer auf die Referenzzähler. Wenn du "std::make_shared" verwendest, wird eine Allokation durchgeführt und beide Zeiger zeigen dort hinein. Das führt eben zu dem zusätzlichen Problem, dass der gesamte Speicher vorerst nicht freigegeben werden kann. Erzeugst du ein "shared_ptr" per Konstruktur, übergibst du ihm einen Zeiger der mit "new" von dir allokiert wurde. Referenzzähler finden da natürlich keinen Platz, deshalb muss "std::hared_ptr" eine zweite Allokation machen.
das drüberiterieren wird dadurch aber natürlich für den Cache nicht gerade angenehm.
Naja, es ist halt der beste Ansatz den wir haben, wenn du tatsächlich die alte klassische Polymorphie verwendest. Und von den Allokationen sehr solide. "shared_ptr" ist da doch behäbiger und die
Ok, dass man zwei Zähler braucht klingt einleuchtend. Aber: Wo kommt der Speicher für den zweiten her?
Für den zweiten Zähler? Beide Zähler liegen im gleichen Block der extra allokiert wurde oder bei "std::make_shared" mit dem Rest geteilt.
Er muss ja auch von allen weap_ptr geteilt werden, müssen ihn dann nicht auch schon alle shared_ptr kennen? (Weil man von denen ja die weak_ptr erstellen muss)
Ja. Das ist der "zweite Zeiger" in dem "std::shared_ptr"
das macht die Sache auch nicht so viel schlimmer.
Es verdoppelt die Cachebelastung beim Zeiger nachschlagen.

Ich betrachte "std::shared_ptr" sehr skeptisch. Besonders weil der Zeiger sehr viel macht, was du gar nicht willst und du alle Resourcen explizit freigeben musst, sogar an den Stellen n denen eigentlich keine Besitzbeziehung besteht. Das ist eigentlich schlimmer als "new & delete per Hand" von der Zuverlässigkeit und ein einziges Memory (& Cache) Leak.

Um die Deallokation sicherzustellen, muss außerdem auch sicherstellt sein, dass alle Objekte regelmäßig angefasst werden. Also ganz ähnlich zu Helmuts-Vorschlag, nur dass ein Leck still ignoriert wird. Seine Idee würde sich zudem recht einfach kapseln lassen, in dem du eine kleine Smart-Pointer Klasse schreibst, die im "operator *" etc. halt noch automatisch prüft, ob das Dead-Flag gesetzt ist. Das ist dann eigentlich sehr viel sicherer, sehr einfach und wenig Overhead. Speziell im Vergleich zur "std::weak_ptr"-Methode sehe ich das eigentlich nur Vorteile.
Helmut
Establishment
Beiträge: 237
Registriert: 11.07.2002, 15:49
Wohnort: Bonn
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von Helmut »

Spiele Programmierer hat geschrieben:
Moment, was genau heißt das? Wie kann ein shared_ptr denn auf mehr als eine Instanz zeigen? Oder meinst du, dass alle shared_ptr die auf ein Objekt zeigen, jeweils eine eigene kleine Heapallokation machen? Wofür wäre die?
Seine Aussage ist nicht allgemeingültig. Ein "shared_ptr" besteht aus mindestens zwei Zeigern. Einer zeigt auf das Programmobjekt und einer auf die Referenzzähler. Wenn du "std::make_shared" verwendest, wird eine Allokation durchgeführt und beide Zeiger zeigen dort hinein.
Huch, das wusste ich nicht. :) Dann ist meine Aussage natürlich falsch, zumindest bezüglich der doppelten Heapallokation.
Jonathan hat geschrieben:Joah, dann brauch ich nur zum einen einen sehr besonderen "ich bin tot" Zustand, wo ein Objekt überall ignoriert werden muss (wenn ich einen toten weak_ptr normal benutze merk ich das sofort) und ich muss sicherstellen, dass jedes Objekt jede Referenz in jedem Frame checkt und nicht nur, wenn es sie benutzt.
Ersteres passiert ganz aufomatisch, wenn du letzteres tust.
Letztlich sind beide Lösungen identisch. Nur ist bei der shared_ptr Variante viel Funktionalität in einem nicht-debugbaren, komplizierten und potentiell langsamem Konstrukt versteckt, während bei meiner Variante relativ einfache Regeln eingehalten werden müssen.
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von dot »

Hab nicht den ganzen Thread gelesen, aber auf den ersten Blick würde ich mal meine, dass dein Problem daher kommt, dass es keine klare Trennung zwischen Spiellogik und Grafik gibt. Wenn die Grafik nicht die Logikobjekte verwenden würde, sondern Logikobjekte Grafikobjekte steuern würden, gäbe es kein Problem. Wenn das Logikobjekt stirbt, wird das Grafikobjekt einfach ebenfalls zerstört...

shared_ptr ist hier imo keine saubere Lösung; damit würdest du nur das Symptom der ungeklärten Besitzverhältnisse behandeln...
Jonathan hat geschrieben:Ich habe shared_ptr nie benutzt, weil ich es dämlich fand, nicht zu wissen, wem ein Objekt gehört.
Das ist eine sehr gesunde Lebenseinstellung, die du nicht so einfach aufgeben solltest. ;)
Benutzeravatar
Sternmull
Establishment
Beiträge: 264
Registriert: 27.04.2007, 00:30
Echter Name: Til
Wohnort: Dresden

Re: Referenz auf temporäre Spielobjekte

Beitrag von Sternmull »

Sein Problem ist doch aber das seine Spiellogik Verweise von einem Objekt auf ein anderes braucht, und das diese Verweise irgendwie vernünftig behandelt werden müssen wenn das Zielobjekt verschwindet. Das ist also zwangsweise auf einer Ebene.

Die Eigentums-Beziehung muss durch die Verwendung von shared_ptr auch nicht kaputt gehen. Statt dessen sollte man tunlichst darauf achten das diese immer eine Hierarchie darstellen, und die Bildung von zyklischen Referenzen möglichst auf Design-Ebebene ausschließen (z.B. festlegen das ein Objekt eine Waffe haben kann, eine Waffe aber kein Objekt haben kann, also niemals beide aufeinander verweisen können).

Von der Performance her sind shared_ptr und weak_ptr natürlich ein bisschen langsamer als rohe Zeiger. Allerdings erfüllen sie damit ja auch einen Zweck. Wenn man statt dessen rohe Zeiger nimmt, aber dann alle Objekte ständig nach toten Zeigern durchsuchen muss, hat man womöglich auch keine bessere Performance... dafür aber einen Haufen kryptischen Code.
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: Referenz auf temporäre Spielobjekte

Beitrag von Spiele Programmierer »

Von der Performance her sind shared_ptr und weak_ptr natürlich ein bisschen langsamer als rohe Zeiger.
Für sich alleine Betrachtet alles andere als ein bisschen. Über die gesamte Anwendung betrachtet, ist es je nach dem natürlich möglicherweise vernachlässigbar.
Wenn man statt dessen rohe Zeiger nimmt, aber dann alle Objekte ständig nach toten Zeigern durchsuchen muss, hat man womöglich auch keine bessere Performance...
Der Punkt ist: Ziemlich genau das musst du auch bei "std::weak_ptr" tun, ansonsten werden die Objekte still und heimlich nie freigegeben. Somit sehe ich keinen einzigen Vorteil außer die "Vertuschung".
dafür aber einen Haufen kryptischen Code.
Ähm, ne. Jedenfalls nicht, wenn du es sauber umsetzt.
Wenn die Funktionalität gut in Klassen gekapselt ist, sollte es mindestens genau so einfach sein.
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von Krishty »

Sternmull hat geschrieben:Wenn man statt dessen rohe Zeiger nimmt, aber dann alle Objekte ständig nach toten Zeigern durchsuchen muss, hat man womöglich auch keine bessere Performance... dafür aber einen Haufen kryptischen Code.
Lustigerweise ist das Vermerken nutzloser Einträge mit finaler Konsolidierung des Arrays mein neues Code-Hobby. So lange man nicht in jedem Frame die Hälfte der Objekte löscht (so dass die Heap-Verwaltungsstrukturen immer im Cache liegen) ist es so ziemlich optimal. Dabei kann man direkt eine Tabelle erzeugen, die alte Zeiger zu neuen übersetzt, und hat dann eine sehr effiziente Übersetzung des alten Zustands zum Neuen.

Aber es irritiert schon, dass im Jahre 2014 eine Makroarchitektur vor allem im Hinblick auf Leistung (eine Allokation mehr oder weniger?) diskutiert wird. Da hat mein Compiler-Genörgel wohl viel FUD gestreut …
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Sternmull
Establishment
Beiträge: 264
Registriert: 27.04.2007, 00:30
Echter Name: Til
Wohnort: Dresden

Re: Referenz auf temporäre Spielobjekte

Beitrag von Sternmull »

Spiele Programmierer hat geschrieben: Für sich alleine Betrachtet alles andere als ein bisschen. Über die gesamte Anwendung betrachtet, ist es je nach dem natürlich möglicherweise vernachlässigbar.
Ich hab vorhin mal ein Beispiel zusammengeschossen um die die Performance der beiden Methoden zu vergleichen. Dabei hat shared_ptr/weak_ptr knapp doppelt so lange gebraucht wie die rohen Zeiger. In dem Beispiel hab ich aber auch quasi nichts anderes gemacht als unmengen Pointer zu dereferenzieren, ein paar virtualle Funktionen aufzurufen (weil ich glaube das das bei den Spiele-Objekten auch häufig passieren wird) und ein paar Integer hoch und runter zu zählen. Sobald man noch ein paar mal std::string zuweist verschwindet die Relevanz dieses Performance-Unterschieds recht schnell. Und an stellen an denen man einen shared_ptr mehrfach hintereinander derefenziert, wird man sich in der Regel schon aus Bequemlichkeit einmal eine Referenz holen und dann auf die Zugreifen, da fällt der shared_ptr-Dereferenzierungs-Overhead dann also nur einmal an.
Spiele Programmierer hat geschrieben:
Wenn man statt dessen rohe Zeiger nimmt, aber dann alle Objekte ständig nach toten Zeigern durchsuchen muss, hat man womöglich auch keine bessere Performance...
Der Punkt ist: Ziemlich genau das musst du auch bei "std::weak_ptr" tun, ansonsten werden die Objekte still und heimlich nie freigegeben. Somit sehe ich keinen einzigen Vorteil außer die "Vertuschung".
Nein, bei weak_ptr bleiben doch nur die kleinen indirektions-Objekte übrig. Und ein paar hundert davon tun nun wirklich nicht weh. Und dafür entledigt man sich dem Aufwand der anfällt wenn man alle schwachen Referenzen selber invalidieren will. Wenn man da eine vergisst, dann knallt es irgendwann mal (wenn das Objekt eben doch mal zerstört wurde und man vergessen hat den Zeiger in der letzten Ecke ganz hinten der drauf zeigt auf NULL zu setzen).
Was du da als "Vertuschung" bezeichnest ist in meinen Augen ein gewünschter Automatismus der es einem erlaubt sich wesentlicheren Dingen zuzuwenden statt sich mit einem selbst-implentierten weak_ptr-Äquivalent zu quälen.
Spiele Programmierer hat geschrieben: Ähm, ne. Jedenfalls nicht, wenn du es sauber umsetzt.
Wenn die Funktionalität gut in Klassen gekapselt ist, sollte es mindestens genau so einfach sein.
Wie würde das denn aussehen? Mir fällt da auf Anhieb erst mal kein Ansatz ein den ich reinen Gewissens empfehlen könnte. Man würde ja manuell GC spielen und müsste über alle Referenzen in einer Suppe von heterogenen Objekten iterieren.
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: Referenz auf temporäre Spielobjekte

Beitrag von Spiele Programmierer »

Nein, bei weak_ptr bleiben doch nur die kleinen indirektions-Objekte übrig.
Das ist ja schon schlimm genug, aber na gut.
Wenn man aber "std::make_shared" nutzt, wird es mehr.
Und dafür entledigt man sich dem Aufwand der anfällt wenn man alle schwachen Referenzen selber invalidieren will.
Worauf ich hinaus wollte: genau das musst du bei "std::weak_ptr" auch machen, sonst verbleibt Müll. Müll der leicht nie mehr aufgeräumt wird und im Falle von "std::make_shared" sogar potentiell ziemlich viel.
Wenn man da eine vergisst, dann knallt es irgendwann mal.
It's not a bug, it's a feature...
Ich sehe als einen Vorteil, wenn vergessene tote Objektreste einen Fehler auslösen, als wenn sie einfach weiter Speicher verwaisen.
Das ist das was ich mit "Vertuschung" bezeichnet habe. Die Tatsache eine 50 zeilige Klasse zu schreiben, als "quälen" zu bezeichnen, ist wohl leicht übertrieben.

Wenn es wirklich bloß ein paar hundert oder auch tausend sind, es auch dabei bleibt und nicht doch "std::make_shared" mit großen Objekten genutzt wird, wäre es natürlich nicht so schlimm.
Guten Gewissens allgemein empfehlen kann ich das aber leider irgendwie einfach nicht.
Helmut
Establishment
Beiträge: 237
Registriert: 11.07.2002, 15:49
Wohnort: Bonn
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von Helmut »

@Sternmull
Du tust jetzt so, als ob es tausende solcher Pointer gibt, die auch schwer zu finden sind. Ich habe mal gerade in den Source von einen tatsächlichen Spiel geschaut, das den Ansatz verwirklicht, den ich hier vorgestellt habe. Und es gab genau eine Stelle im ganzen Spiel, in dem ein Objekt auf ein anderes verwies. In 99% der Fälle gibt es klare Besitzverhältnisse. Aber selbst wenn sowas häufiger vorkommt und man eine Stelle vergisst, dann crasht das Spiel beim Zugriff über den Pointer halt. Dann debuggt man das und fixt das Problem in 5 Sekunden.
Wenn das Spiel leaked wird man bei dem shared_ptr und weak_ptr Wirrwar schon auf externe Tools zurückgreifen müssen, um den Fehler finden zu können.
Sternmull hat geschrieben:Nein, bei weak_ptr bleiben doch nur die kleinen indirektions-Objekte übrig.
Nicht, wenn du std::make_shared benutzt
Benutzeravatar
Sternmull
Establishment
Beiträge: 264
Registriert: 27.04.2007, 00:30
Echter Name: Til
Wohnort: Dresden

Re: Referenz auf temporäre Spielobjekte

Beitrag von Sternmull »

@Spiele Programmierer:
Dann bleibt aber meine Frage: Wie würde denn das aussehen was in der Praxis besser ist als weak_ptr?

@Helmut:
Das mag ja sein, kommt aber ganz auf das Spiel an. z.B. wird man in einem Strategiespiel vielleicht so was haben wie "Einheit X: Beschütze Einheit Y". Das könnte man realisieren indem man X einen weak_ptr auf Y gibt. Oder wenn eine Einheit mehrere Einheiten nacheinander besuchen/angreifen soll, könnte man das über eine Queue mit weak_ptr machen. Und wenn man will dann findet man bestimmt noch ganz viele andere Szenarien :) An den Stellen hat man mit weak_ptr ein einfaches Leben, auch wenn halt hier und da mal einer rumgammelt der eigentlich bereits NULL geworden ist.

Aber es stimmt schon das ich mir die Querverweise vielleicht etwas extremer ausmale als sie in der Realität normalerweise sind. Aber ich bin vom Prinzip einer manuell gepflegten clearPointersToDeadObjects() Funktion auch einfach nicht wirklich angetan. Und vom Prinzip die Eigentums-Beziehungen per shared_ptr abzubilden bin ich recht überzeugt (weil es mich in der Praxis bisher auch immer glücklich gemacht hat). Deshalb ist in meinen Augen der weak_ptr die naheliegende Lösung für das ursprüngliche Problem.

So ganz nebenbei sollte man grundsätzlich bereit sein Memory-Leaks zu debuggen. Nach meiner Erfahrung tauchen die aber am häufigsten auf wenn man den Speicher manuell verwaltet (vor allem wenn es sich um komplexe Regeln für Allokation und Freigabe handelt), oder Referenzzähler explizit bedienen muss und sich irgendwo vertut. Seit ich UMDH kenne, sind solche Probleme aber meist schnell aus der Welt geschafft. Ohne externes Tool geht es dann auch einfach nicht.
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: Referenz auf temporäre Spielobjekte

Beitrag von Spiele Programmierer »

Wie würde denn das aussehen was in der Praxis besser ist als weak_ptr?
Eine Klasse mit einer Funktion "UpdatePointer" die das Flag prüft und gegebenenfalls auf "nullptr" setzt. Dazu noch den "->" und "*" überladen um den Pointer zugreifen zu können. Wahlweise kann man dort auch das Flag erkennen und direkt "nullptr" zurückgeben, oder eine Debugmessage einbauen, dass man wohl vergessen hat "UpdatePointer" vorher aufzurufen.

Sollte eine sehr einfache kleine Sache sein. Codeoverhead am Einsatzort besteht nur aus dem Flag(das man als OO Freund auch in eine Basisklasse schieben könnte) und dem "UpdatePointer"-Aufruf.
Benutzeravatar
Jonathan
Establishment
Beiträge: 2353
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von Jonathan »

Naja, wenn ich den Objekten ein 'dead'-Flag hinzufüge habe ich halt immer noch das Problem, dass ich nicht genau weiß, wann ich sie denn löschen kann. Wenn ich garantieren kann, das alle Objekte, die eine Referenz halten, sie einmal pro Frame überprüfen und gegebenenfalls löschen, werde ich auch mit weak_ptr niemals leaks haben. Aber passiert das an einer Stelle mal nicht, habe ich in einem Fall einen Absturz und im anderen Fall ein Leak bis das Spiel aufgeräumt wird.

Allerdings würde ich auch für die 'dead'-Flag-Lösung eine Proxyklasse brauchen, einfach damit ich sichergehen kann, dass es auch immer abgefragt wird. Und naja, irgendwie ist ja tot-sein auch eine Metainformation die ansich nicht in das Objekt selber reingehört.

Aber: Ich baue vermutlich erstmal einen typedef ein, dann kann man ja bei Bedarf zwischen beiden Lösungen hin und herswitchen. Denn in beiden Fällen muss der Benutzer ja abfragen, ob das Objekt noch lebt und das möglichst regelmäßig, solange er es benutzt.

Und um ehrlich zu sein: Mich interessiert es auch nicht, wenn ein paar Objekte mal länger im Speicher bleiben. So groß sind die in meinem Fall auch nicht. Und was viel wichtiger ist, ist dass ja die Lecks nicht unbegrenzt wachsen können. Man hat zu jedem Zeitpunkt ja nur eine endliche Anzahl an Objekten, die endlich viele Referenzen halten können. Es ist also eigentlich unmöglich, dass man nach einigen Stunden keinen Hauptspeicher mehr übrig hat, im schlimmsten Fall verschwendet man um einen konstanten Faktor mehr Speicher als man eigentlich benötigen würde. Wichtig ist, dass das Spiel nicht abstürzt und das Objekte nicht auf anderen Objekten operieren können, die nicht mehr im Spiel sind.
Und bezüglich der Geschwindigkeit: Eine Designentscheidung war von Anfang an, wenige aber komplexe Objekte zu haben. Meine Spielobjekte sind aus verschiedenen Komponenten zusammengebaut, da sind also eh schon eine ganze Reihe an Zeigern drin. Damit werde ich keine Massenschlachten wie in Total War simulieren können (vermute ich), aber dafür sind die Objekte flexibel und vielseitig. Unter all diesen Gesichtspunkten halte ich weak_ptr für eine gute Lösung meines Problems.

Interessant wird es noch, solche Referenzen auch zu speichern. Irgendetwas mit "alle Objekte haben eindeutige Identifier, beim Speichern werden IDs statt Pointer geschrieben und beim Laden eine ID->Pointer Tabelle aufgestellt, durch die die Referenzen wieder hergestellt werden" wird es wohl werden. Mal schaun.
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Benutzeravatar
xq
Establishment
Beiträge: 1581
Registriert: 07.10.2012, 14:56
Alter Benutzername: MasterQ32
Echter Name: Felix Queißner
Wohnort: Stuttgart & Region
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von xq »

Sehr spannende Diskussion!

Zu der Referenz-Geschichte:
Wir müssen doch gar nicht den Counteralloc so lange halten bis jeder weakpointer tot ist, sondern nur solange bis jeder weakpointer mitbekommen hat, dass das Objekt tot ist. Dann kann der Weakpointer sein Referenzcounting lösen und sich quasi in den "Idle" schalten und nur noch nullptr zurückgeben.

Da wir in einem Spiel sind gehe ich davon aus, dass die meisten der Weak Pointer so oder so jedes Frame abgefragt werden, von daher dürfte der memory leak faktor stark sinken...

Just my two cents
War mal MasterQ32, findet den Namen aber mittlerweile ziemlich albern…

Programmiert viel in ⚡️Zig⚡️ und nervt Leute damit.
Benutzeravatar
Jonathan
Establishment
Beiträge: 2353
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von Jonathan »

Spiele Programmierer hat geschrieben:
Wie würde denn das aussehen was in der Praxis besser ist als weak_ptr?
Eine Klasse mit einer Funktion "UpdatePointer" die das Flag prüft und gegebenenfalls auf "nullptr" setzt. Dazu noch den "->" und "*" überladen um den Pointer zugreifen zu können. Wahlweise kann man dort auch das Flag erkennen und direkt "nullptr" zurückgeben, oder eine Debugmessage einbauen, dass man wohl vergessen hat "UpdatePointer" vorher aufzurufen.

Sollte eine sehr einfache kleine Sache sein. Codeoverhead am Einsatzort besteht nur aus dem Flag(das man als OO Freund auch in eine Basisklasse schieben könnte) und dem "UpdatePointer"-Aufruf.
Schön und gut, aber wie stellst du sicher, dass alle Referenzen mindestens einmal pro Frame überprüft werden? Außerdem brauchst du noch ein wenig mehr Logik, weil du tot-markierte Objekte erst im übernächsten Frame entfernen darfst (weil sie ja getötet werden können, nachdem jemand schon die Referenz für diesen Frame überprüft hat).

Und manchmal will man ja vielleicht auch gar nicht in jedem Frame alle Referenzen überprüfen müssen. Vielleicht will die KI ja bevor sie einen Gegner weiter angreift noch kurz ein Item aufsammeln. Es kann halt echt leicht Situationen geben, wo man eine Referenz nicht immer benutzen würde, wenn man es nicht muss. Und so gesehen kostet das Überprüfen ja auch minimal Zeit, gratis ist es jedenfalls nicht. Und wenn die Kosten dafür "Speicher für 2 ints wird etwas später freigegeben" sind, dann hört sich das für mich gar nicht so grausam an. (man kann ja immer noch abwägen, ob man make_shared benutzt, oder nicht).
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: Referenz auf temporäre Spielobjekte

Beitrag von Spiele Programmierer »

Wir müssen doch gar nicht den Counteralloc so lange halten bis jeder weakpointer tot ist, sondern nur solange bis jeder weakpointer mitbekommen hat, dass das Objekt tot ist. Dann kann der Weakpointer sein Referenzcounting lösen und sich quasi in den "Idle" schalten und nur noch nullptr zurückgeben.
Ja, das stimmt. Mitbekommen und Zerstören ist jedoch praktisch in dem Fall das selbe. Zerstört wird es, wenn der schache Zeiger es mitbekommen hat.
Da wir in einem Spiel sind gehe ich davon aus, dass die meisten der Weak Pointer so oder so jedes Frame abgefragt werden, von daher dürfte der memory leak faktor stark sinken...
Zu beachten ist jedoch: Einer reicht, dass es nicht funktioniert
Und: Abfragen alleine reicht, soweit ich weiß, bei der Implementierung der Standardbibliothek nicht aus. Es muss explizit zurückgesetzt werden. Außerdem wer denkt daran, wenn es nicht direkt auffällt wenn man es vergisst?
Schön und gut, aber wie stellst du sicher, dass alle Referenzen mindestens einmal pro Frame überprüft werden?
In den aller meisten Spielen wird ohnehin für jedes Objekt eine (oder mehrere) "Update"-Methode(n) aufgerufen. Dort kannst du die Funktionalität unterbringen.
Und wenn die Kosten dafür "Speicher für 2 ints wird etwas später freigegeben" sind, dann hört sich das für mich gar nicht so grausam an
Jede Allokation bringt von sich aus weiteren Overhead mit und der Verbrauch wird in der Praxis deutlich höher liegen. Mindestens Faktor 2, wahrscheinlich aber noch mehr.
Und naja, irgendwie ist ja tot-sein auch eine Metainformation die ansich nicht in das Objekt selber reingehört.
Es wäre auch eine Möglichkeit, den "std::unique_ptr" mit einer entsprechenden Klasse zu ersetzen, die zum einen aus dem Objekt und aus dem Flag besteht. Wer sich etwas mehr Mühe geben möchte, könnte das auch wieder vollkommen transparent gestalten.
Benutzeravatar
Jonathan
Establishment
Beiträge: 2353
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von Jonathan »

Ja, das mit den Metainformationen ist ein schwaches Argument.

Ich fasse jetzt einfach mal zusammen, was ich glaube, was dein Punkt ist. Einerseits sagst du ja, es ist ansich kein großes Problem, die Referenz ständig abzufragen, andererseits warnst du vor Leaks, wenn weak_ptr benutzt werden. Das passt so direkt nicht zusammen, aber was du glaube ich wirklich sagen willst ist: Wenn es abstürzt findet man den Bug und kann ihn beheben, wenn es läuft merkt man es nicht und der Bug bleibt da.
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Benutzeravatar
Schrompf
Moderator
Beiträge: 4838
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von Schrompf »

Bei Splatter habe ich alle Objekte anhand einer ID in einem Container. Meine Objektreferenz ist dann eine kleine struct { Zeiger* z; ID id; }, die beim Zugriff das aktuelle Objekt aus dem Container holt und den Zeiger vergleicht, ob es noch dasselbe Objekt ist. Ich habe nämlich festgestellt, dass Zugriffe auf solche Verweise an allen möglichen Stellen der Spiellogik passieren, und ich mich eben nicht darauf verlassen kann, dass auf jeden Fall vorher ein Refresh durchkam und alle Referenzen auf tote Objekte genullt hat.

Macht auch die Cache-Kohärenz noch etwas kaputter, aber das ist eh wurscht, wenn wir von heap-allokierten Objekten mit Virtual Function Table reden. Da streuen die Speicherzugriffe so weit, dass eine weitere Indirektion in der Praxis irrelevant ist.

Wenn ich das heutzutage schreiben würde, würde ich einfach shared_ptr und weak_ptr nehmen. Funktioniert, ist schnell gemacht, und der Performance-Unterschied dürfte für ein paar tausend Objekte nicht messbar sein, wenn es echte Objekte mit echter Spiellogik sind. Die doppelte Allokation kann man mit make_shared() vermeiden und bekommt außerdem die Referenzzähler schon nah ans Objekt. Und - was hier bisher vernachlässigt wurde - die idiomatischen C++-Lösungen auf Destruktor-Basis funktionieren trotzdem noch sauber. Was man von Lösungen mit dead-Flag nicht behaupten kann.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: Referenz auf temporäre Spielobjekte

Beitrag von Spiele Programmierer »

aber was du glaube ich wirklich sagen willst ist: Wenn es abstürzt findet man den Bug und kann ihn beheben, wenn es läuft merkt man es nicht und der Bug bleibt da.
Ja, das war auf jeden Fall mein Hauptargument. Deshalb kann man den Speicheroverhead besser abschätzen.
Helmut
Establishment
Beiträge: 237
Registriert: 11.07.2002, 15:49
Wohnort: Bonn
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von Helmut »

Schrompf hat geschrieben:Bei Splatter habe ich alle Objekte anhand einer ID in einem Container. Meine Objektreferenz ist dann eine kleine struct { Zeiger* z; ID id; }, die beim Zugriff das aktuelle Objekt aus dem Container holt und den Zeiger vergleicht, ob es noch dasselbe Objekt ist. Ich habe nämlich festgestellt, dass Zugriffe auf solche Verweise an allen möglichen Stellen der Spiellogik passieren, und ich mich eben nicht darauf verlassen kann, dass auf jeden Fall vorher ein Refresh durchkam und alle Referenzen auf tote Objekte genullt hat.
Aber wieso speicherst du dann überhaupt den Zeiger? Du kannst ja nicht if(z->id == id) schreiben, weil das Objekt ja schon potentiell gelöscht ist. So müsste man bei jedem Zugriff das Objekt mit der ID suchen.
Schrompf hat geschrieben:Und - was hier bisher vernachlässigt wurde - die idiomatischen C++-Lösungen auf Destruktor-Basis funktionieren trotzdem noch sauber. Was man von Lösungen mit dead-Flag nicht behaupten kann.
Das ist in der Tat ein gutes Argument.
Benutzeravatar
Schrompf
Moderator
Beiträge: 4838
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von Schrompf »

Helmut hat geschrieben:Aber wieso speicherst du dann überhaupt den Zeiger? Du kannst ja nicht if(z->id == id) schreiben, weil das Objekt ja schon potentiell gelöscht ist. So müsste man bei jedem Zugriff das Objekt mit der ID suchen.
Ich speichere sowohl Zeiger als auch ID als Referenz. Die ID ist so ne Art Array-Index, also schnell aufzulösen. Wenn die Objekte gelöscht werden, wird auch der Platz unter deren ID frei. Und wenn dann ein neues Objekt gespawnt wird, bekommt es eine der freien IDs. Wenn dann später ein Objekt eine Referenz auflösen will, gibt es also drei Möglichkeiten.

- das Objekt zur ID existiert und der Zeiger ist identisch - prima
- das Objekt zur ID existiert nicht, Zeiger ist also Null - Ziel ist weg
- das Objekt zur ID existiert, hat aber anderen Zeiger - Ziel ist weg, unter der ID hockt ein neues Objekt.

Ich mache dann zur Sicherheit noch einen dynamic_cast zur erwarteten Klasse, aber bisher war das nie ein Problem. Theoretisch könnte es aber auch ein Objekt geben, was an der selben Adresse wie der Vorgänger landet. Keine Ahnung, wie ich das mit dem System abfangen würde.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: Referenz auf temporäre Spielobjekte

Beitrag von Spiele Programmierer »

Dann gibt es doch aber noch die Möglichkeit, dass ein neues Objekt nach dem Löschen, zufälligerweise den gleichen Speicherplatz zugewiesen bekommt und die ID sowieso übereinstimmt. Die Möglichkeit besteht und ist je nach dem wie der Allokator arbeitet, möglicherweise gar nicht soooo unwahrscheinlich.

Damit das System sicher ist, dürften IDs niemals mehrmals vergeben werden.
Benutzeravatar
Sternmull
Establishment
Beiträge: 264
Registriert: 27.04.2007, 00:30
Echter Name: Til
Wohnort: Dresden

Re: Referenz auf temporäre Spielobjekte

Beitrag von Sternmull »

Das klingt echt abenteuerlich. Wenn ein Objekt freigegeben wird und sofort wieder ein Objekt gleichen Typs alloziert wird, dann ist es sehr wahrscheinlich das das neue Objekt auch wieder die gleiche Adresse bekommt. Ein kleiner Test dazu sagt das es bei mir zu 100% wieder an der gleichen Stelle alloziert wird... also verwendest du wahrscheinlich deine IDs (die für mich eher nach "Slots" klingen) nicht so schnell wieder.
Benutzeravatar
xq
Establishment
Beiträge: 1581
Registriert: 07.10.2012, 14:56
Alter Benutzername: MasterQ32
Echter Name: Felix Queißner
Wohnort: Stuttgart & Region
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von xq »

Man könnte ja auch dann eine Art Objekt-Referenz machen, welche ein ID-System mit Hashmap nutzt, dass die De-/Referenzierung via Hashmap+ID übernimmt. Wenn man dann *, -> überschreibt, sollte das ja doch relativ fix gehen (gucken ob ID in hashmap/array/whatever, wenn nicht, nullptr)
Dadurch hätte man eine schöne sharedpointerfreie geschichte
War mal MasterQ32, findet den Namen aber mittlerweile ziemlich albern…

Programmiert viel in ⚡️Zig⚡️ und nervt Leute damit.
Benutzeravatar
Schrompf
Moderator
Beiträge: 4838
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: Referenz auf temporäre Spielobjekte

Beitrag von Schrompf »

Das stimmt wohl. Man hätte für die Wiedererkennung auch einfach eine zweite ID benutzen können. Aber wie gesagt: heutzutage würde ich einfach shared_ptr und weak_ptr benutzen und die ganze Diskussion wäre überflüssig.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Antworten