Verwalten mehrerer Referenzen derselben Spieleinheit an verschiedenen Orten mithilfe von IDs

8

Ich habe großartige Fragen zu ähnlichen Themen gesehen, aber keine, die sich mit dieser speziellen Methode befassten:

Angesichts der Tatsache, dass ich in meinem [XNA Game Studio] -Spiel mehrere Sammlungen von Spielentitäten habe, wobei viele Entitäten zu mehreren Listen gehören, überlege ich, wie ich verfolgen kann, wann immer eine Entität zerstört wird, und sie aus den Listen entfernen kann, zu denen sie gehört .

Viele potenzielle Methoden scheinen schlampig / verworren zu sein, aber ich erinnere mich an eine Art und Weise, die ich zuvor gesehen habe, bei der Sie statt mehrerer Sammlungen von Spielentitäten Sammlungen von Spielentitäts- IDs haben . Diese IDs werden Spielentitäten über eine zentrale "Datenbank" (möglicherweise nur eine Hash-Tabelle) zugeordnet.

Wenn also ein Teil des Codes auf die Mitglieder einer Spielentität zugreifen möchte, prüft er zunächst, ob er sich noch in der Datenbank befindet. Wenn nicht, kann es entsprechend reagieren.

Ist das ein vernünftiger Ansatz? Es scheint, dass dies viele der Risiken und Probleme beim Speichern mehrerer Listen beseitigen würde, wobei der Kompromiss die Kosten für die Suche jedes Mal sind, wenn Sie auf ein Objekt zugreifen möchten.

vargonian
quelle

Antworten:

3

Kurze Antwort: ja.

Lange Antwort: Eigentlich haben alle Spiele, die ich gesehen habe, auf diese Weise funktioniert. Die Suche nach Hash-Tabellen ist schnell genug. und wenn Sie sich nicht für tatsächliche ID-Werte interessieren (dh sie werden nirgendwo anders verwendet), können Sie einen Vektor anstelle einer Hash-Tabelle verwenden. Welches ist noch schneller.

Als zusätzlichen Bonus ermöglicht dieses Setup die Wiederverwendung Ihrer Spielobjekte, wodurch die Speicherzuweisungsrate verringert wird. Wenn ein Spielobjekt zerstört wird, anstatt es zu "befreien", bereinigen Sie einfach seine Felder und legen einen "Objektpool" ab. Wenn Sie dann ein neues Objekt benötigen, nehmen Sie einfach ein vorhandenes aus dem Pool. Wenn ständig neue Objekte erscheinen, kann dies die Leistung spürbar verbessern.

Keine Ursache
quelle
Im Allgemeinen ist das genau genug, aber wir müssen zuerst andere Dinge berücksichtigen. Wenn viele Objekte schnell erstellt und zerstört werden und unsere Hash-Funktion gut ist, passt die Hashtabelle möglicherweise besser als ein Vektor. Wenn Sie jedoch Zweifel haben, gehen Sie mit dem Vektor.
Hokiecsgrad
Ja, ich muss zustimmen: Der Vektor ist nicht absolut schneller als eine Hash-Tabelle. Aber es ist 99% der Zeit schneller (-8
Nevermind
Wenn Sie Slots wiederverwenden möchten (was Sie definitiv wollen), müssen Sie sicherstellen, dass ungültige IDs erkannt werden. Ex. Das Objekt mit der ID 7 befindet sich in Liste A, B und C. Das Objekt wird zerstört und im selben Steckplatz wird ein anderes Objekt erstellt. Dieses Objekt registriert sich in Liste A und C. Der Eintrag des ersten Objekts in Liste B "zeigt" jetzt auf das neu erstellte Objekt, was in diesem Fall falsch ist. Dies kann mit dem Handle-Manager von Scott Bilas scottbilas.com/publications/gem-resmgr perfekt gelöst werden . Ich habe dies bereits in kommerziellen Spielen verwendet und es funktioniert wie ein Zauber.
DarthCoder
3

Ist das ein vernünftiger Ansatz? Es scheint, dass dies viele der Risiken und Probleme beim Speichern mehrerer Listen beseitigen würde, wobei der Kompromiss die Kosten für die Suche jedes Mal sind, wenn Sie auf ein Objekt zugreifen möchten.

Alles, was Sie getan haben, ist, die Last der Durchführung der Überprüfung vom Zeitpunkt der Zerstörung auf den Zeitpunkt der Verwendung zu verlagern. Ich würde nicht behaupten, dass ich das noch nie zuvor gemacht habe, aber ich halte es nicht für "Klang".

Ich schlage vor, eine von mehreren robusteren Alternativen zu verwenden. Wenn die Anzahl der Listen bekannt ist, können Sie das Objekt einfach aus allen Listen entfernen. Das ist harmlos, wenn sich das Objekt nicht in einer bestimmten Liste befindet und Sie es garantiert korrekt entfernt haben.

Wenn die Anzahl der Listen unbekannt oder unpraktisch ist, speichern Sie Rückverweise auf diese im Objekt. Die typische Implementierung hierfür ist das Beobachtermuster. Sie können jedoch wahrscheinlich einen ähnlichen Effekt mit Delegaten erzielen, die zurückrufen, um das Element bei Bedarf aus den enthaltenen Listen zu entfernen.

Oder manchmal brauchen Sie gar nicht mehrere Listen. Oft können Sie ein Objekt in nur einer Liste behalten und dynamische Abfragen verwenden, um bei Bedarf andere vorübergehende Sammlungen zu generieren. z.B. Anstatt eine separate Liste für das Inventar eines Charakters zu haben, können Sie einfach alle Objekte herausziehen, bei denen "Mitgenommen" dem aktuellen Charakter entspricht. Dies mag ziemlich ineffizient klingen, ist aber für relationale Datenbanken gut genug und kann durch clevere Indizierung optimiert werden.

Kylotan
quelle
Ja, in meiner aktuellen Implementierung muss jede Liste oder jedes einzelne Objekt, das einen Verweis auf eine Spielentität möchte, das zerstörte Ereignis abonnieren und entsprechend reagieren. Dies kann in gewisser Hinsicht genauso gut oder besser sein als die ID-Suche.
Vargonian
1
In meinem Buch ist dies das genaue Gegenteil einer robusten Lösung. Verglichen mit Lookup-by-ID haben Sie MEHR Code, MEHR Stellen für Fehler, MEHR Möglichkeiten, etwas zu vergessen, und die Möglichkeit, Objekte zu zerstören, ohne alle zum Booten zu informieren. Mit Lookup-by-ID haben Sie ein wenig Code, der sich fast nie ändert, einen einzigen Zerstörungspunkt und eine 100% ige Garantie, dass Sie niemals auf ein zerstörtes Objekt zugreifen.
Nevermind
1
Mit Lookup-by-ID haben Sie zusätzlichen Code an jeder Stelle, an der Sie das Objekt verwenden müssen. Bei einem beobachterbasierten Ansatz steht Ihnen kein zusätzlicher Code zur Verfügung, außer beim Hinzufügen und Entfernen von Elementen zu Listen, bei denen es sich wahrscheinlich um eine Handvoll Stellen im gesamten Programm handelt. Wenn Sie mehr auf Elemente verweisen, als Sie erstellen, zerstören oder zwischen Listen verschieben, führt der Beobachteransatz zu weniger Code.
Kylotan
Es gibt wahrscheinlich mehr Orte, die auf Objekte verweisen, als Orte, die Objekte erstellen / zerstören. Für jede Referenz ist jedoch nur ein winziges Stück Code erforderlich, oder gar kein Code, wenn die Suche bereits von einer Eigenschaft umschlossen ist (was die meiste Zeit der Fall sein sollte). Außerdem können Sie vergessen, die Objektzerstörung zu abonnieren, aber Sie können nicht vergessen, ein Objekt nachzuschlagen.
Nevermind
1

Dies scheint nur eine spezifischere Instanz des Problems "So entfernen Sie Objekte aus einer Liste, während Sie sie durchlaufen" zu sein, das leicht zu lösen ist (das Iterieren in umgekehrter Reihenfolge ist wahrscheinlich der einfachste Weg für eine Liste im Array-Stil wie C # List).

Wenn eine Entität von mehreren Stellen aus referenziert werden soll, sollte es sich ohnehin um einen Referenztyp handeln . Sie müssen also kein kompliziertes ID-System durchlaufen - eine Klasse in C # bietet bereits die erforderliche Indirektion.

Und schließlich - warum "die Datenbank überprüfen"? Geben Sie jeder Entität einfach ein IsDeadFlag und überprüfen Sie dies.

Andrew Russell
quelle
Ich kenne C # nicht wirklich, aber diese Architektur wird häufig in C und C ++ verwendet, wo der normale Referenztyp (Zeiger) der Sprache nicht sicher verwendet werden kann, wenn das Objekt zerstört wurde. Es gibt verschiedene Möglichkeiten, damit umzugehen, aber alle beinhalten eine Indirektionsebene, und dies ist eine gängige Lösung. (Eine Liste von Backpointern, wie Kylotan vorschlägt, ist eine andere - die Sie auswählen, hängt davon ab, ob Sie Speicher oder Zeit verbringen möchten.)
C # ist Müll gesammelt, daher spielt es keine Rolle, ob Sie Instanzen abbrechen. Für nicht verwaltete Ressourcen (dh vom Garbage Collector) gibt es das IDisposableMuster, das dem Markieren eines Objekts als sehr ähnlich ist IsDead. Wenn Sie Instanzen bündeln müssen, anstatt sie GC-fähig zu machen, ist natürlich eine kompliziertere Strategie erforderlich.
Andrew Russell
Das IsDead-Flag habe ich in der Vergangenheit mit Erfolg verwendet. Ich mag nur die Idee, dass das Spiel abstürzt, wenn ich nicht nach einem toten Zustand suche, anstatt fröhlich weiterzumachen, mit möglicherweise bizarren und schwer zu debuggenden Ergebnissen. Die Verwendung einer Datenbanksuche wäre die erstere; IsDead markiert letzteres.
Vargonian
3
@vargonian Beim Disposable-Muster wird normalerweise IsDisposed oben in jeder Funktion und Eigenschaft in einer Klasse aktiviert. Wenn die Instanz entsorgt ist, werfen Sie ObjectDisposedException(was im Allgemeinen mit einer unhandlichen Ausnahme "abstürzt"). Indem Sie die Prüfung auf das Objekt selbst verschieben (anstatt das Objekt extern zu prüfen), können Sie dem Objekt erlauben, sein eigenes Verhalten "Bin ich tot" zu definieren und die Last der Prüfung von Anrufern zu entfernen. Für Ihre Zwecke können Sie ein IDisposableeigenes IsDeadFlag mit ähnlichen Funktionen implementieren oder erstellen .
Andrew Russell
1

Ich benutze diesen Ansatz oft in meinem eigenen C # /. NET-Spiel. Abgesehen von den anderen hier beschriebenen Vorteilen (und Gefahren!) Kann dies auch dazu beitragen, Serialisierungsprobleme zu vermeiden.

Wenn Sie die integrierten binären Serialisierungsfunktionen von .NET Framework nutzen möchten, können Sie mithilfe der Entitäts-IDs die Größe des ausgeschriebenen Objektdiagramms minimieren. Standardmäßig serialisiert der Binärformatierer von .NET ein gesamtes Objektdiagramm auf Feldebene. Angenommen, ich möchte eine ShipInstanz serialisieren . Wenn Shipein _ownerFeld auf den PlayerEigentümer verweist , wird diese PlayerInstanz ebenfalls serialisiert. Wenn Playerein _shipsFeld enthält (z. B. ICollection<Ship>), dann alle Schiffe des Spielers zusammen mit allen anderen Objekten, auf die auf Feldebene verwiesen wird, ebenfalls ausgeschrieben (rekursiv). Es ist einfach, versehentlich ein großes Objektdiagramm zu serialisieren, wenn Sie nur einen kleinen Teil davon serialisieren möchten.

Wenn ich stattdessen ein _ownerIdFeld habe, kann ich diesen Wert verwenden, um die PlayerReferenz bei Bedarf aufzulösen . Meine öffentliche API kann mit dem sogar unverändert bleibenOwner Eigenschaft lediglich die Suche durchführt.

Während Hash-basierte Suchvorgänge im Allgemeinen sehr schnell sind, kann der zusätzliche Overhead bei sehr großen Entitätssätzen mit häufigen Suchvorgängen zu einem Problem werden. Wenn es für Sie zu einem Problem wird, können Sie die Referenz mithilfe eines Felds zwischenspeichern, das nicht serialisiert wird. Zum Beispiel könnten Sie so etwas tun:

public class Ship
{
    private int _ownerId;
    [NonSerialized] private Lazy<Player> _owner;

    public Player Owner
    {
        get { return _owner.Value; }
    }

    public Ship(Player owner)
    {
        _ownerId = owner.PlayerID;
        EnsureCache();
    }

    private void EnsureCache()
    {
        if (_owner == null)
            _owner = new Lazy<Player>(() => Game.Current.Players[_ownerId]);
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        EnsureCache();
    }
}

Natürlich kann dieser Ansatz auch die Serialisierung erschweren , wenn Sie tatsächlich ein großes Objektdiagramm serialisieren möchten . In diesen Fällen müssten Sie eine Art 'Container' entwickeln, um sicherzustellen, dass alle erforderlichen Objekte enthalten sind.

Mike Strobel
quelle
0

Eine andere Möglichkeit, Ihre Bereinigung durchzuführen, besteht darin, die Kontrolle umzukehren und ein Einwegobjekt zu verwenden. Sie haben wahrscheinlich einen Faktor, der Ihre Entitäten zuordnet. Übergeben Sie daher alle Listen, zu denen sie hinzugefügt werden sollen, an die Factory. Die Entität behält einen Verweis auf alle Listen bei, zu denen sie gehört, und implementiert IDisposable. Bei der dispose-Methode wird es aus allen Listen entfernt. Jetzt müssen Sie sich nur noch an zwei Stellen mit der Listenverwaltung befassen: Fabrik (Erstellung) und Entsorgung. Das Löschen im Objekt ist so einfach wie entity.Dispose ().

Das Problem von Namen und Referenzen in C # ist wirklich nur für die Verwaltung von Komponenten oder Werttypen wichtig. Wenn Sie Listen von Komponenten haben, die mit einer Entität verknüpft sind, kann die Verwendung eines ID-Felds als Hashmap-Schlüssel hilfreich sein. Wenn Sie jedoch nur Listen mit Entitäten haben, verwenden Sie einfach Listen mit einer Referenz. C # -Referenzen sind sicher und einfach zu bearbeiten.

Um Elemente aus der Liste zu entfernen oder hinzuzufügen, können Sie eine Warteschlange oder eine beliebige Anzahl anderer Lösungen zum Hinzufügen und Löschen von Listen verwenden.

CodexArcanum
quelle