Speichern von Spielobjekten in mehreren Containern

7

Angesichts von DRY erscheint es wünschenswert, eine Sammlung verwandter Spielobjekte nur in einem Container zu speichern. Möglicherweise sind jedoch Untersammlungen dieser Objekte in verschiedenen Kontexten erforderlich. Es kann sinnvoll sein, diese spezifischen Untergruppen in bestimmten, besser geeigneten Behältern aufzubewahren. Dies erhöht den Aufwand, Objekte über Container hinweg zu verfolgen, beispielsweise wenn Objekte aus der Spielwelt entfernt werden.

Welche Möglichkeiten gibt es, um ein solches Design zu vereinfachen, und was sind die typischen Kompromisse?

Um zu zeigen:

In einem Multiplayer-Rollenspiel enthält der Server möglicherweise eine Sammlung von Spielcharakteren in einer Karte, die zum Nachschlagen nach ID geeignet ist.

world
    map<id, Character> allCharacters

Ein Charakter kann sich auch in einem bestimmten Spiellevel befinden. Um alle Zeichen zu identifizieren, die in einer Ebene vorhanden sind, kann es angebracht erscheinen, für jede Ebene einen Container einzuführen, der die aktuell vorhandenen Zeichen enthält. Auf diese Weise können Sie eine gemeinsame Logik für alle Zeichen in dieser Ebene ausführen.

world
    map<id, Character> allCharacters

    [levels]
        level1
           vector<Character> charactersOnPlayfield
        level2
           vector<Character> charactersOnPlayfield
        ...

Wenn ein Charakter mit der Welt interagiert, sollten Nachrichten nur an Charaktere in Reichweite weitergeleitet werden. Dieses Interessenmanagement könnte erreicht werden, indem jede Ebene in ein Raster von Zellen unterteilt wird, in denen jeweils die aktuell darauf stehenden Zeichen gespeichert sind.

world
    map<id, Character> allCharacters

    [levels]
        level1
           vector<Character> charactersOnPlayfield
           [cells]
               cell1
                   vector<Character> charactersOnCell
               cell2
                   vector<Character> charactersOnCell
               ...
        level2
           vector<Character> charactersOnPlayfield
           [cells]
               cell1
                   vector<Character> charactersOnCell
               cell2
                   vector<Character> charactersOnCell
               ...
        ...

Die Charakterobjekte auf verschiedenen Abstraktionsebenen führen dazu, dass über Objektbesitz und Lebensdauer sorgfältig nachgedacht werden muss.

Beachten Sie, dass die in den Containern gespeicherten Zeichenobjekte natürlich Referenzen und keine Kopien sind. Außerdem gehe ich davon aus, dass keine Speicherbereinigung vorhanden ist.

jmp97
quelle

Antworten:

6

Ich würde davon abraten, tatsächlich mehrere Listen wie diese zu haben, und stattdessen eine Art Signal- / Slot-Bibliothek verwenden (was im Grunde das Beobachtermuster ist), um das Muster "Aufrufen einer Funktion für alle Objekte in diesem Bereich / dieser Zone" zu vereinfachen.

Objekte können ziemlich einfach verwalten, welches Signal sie mit sich selbst verbinden, dh jedes Mal, wenn sie sich bewegen, trennen sie sich von dem Signal, das ihren alten Steckplatz darstellt, und stellen eine Verbindung zum neuen her, wenn Sie so feinkörnig sind.

Tetrad
quelle
Das oder durch Erstellen schreibgeschützter beobachtbarer / gefilterter Sammlungen, deren Inhalt auf einer anderen Sammlung basiert. Alternativ können Sie partitionierte Sammlungen erstellen, die in Bezug auf die untergeordneten Sammlungen (eine für jede 'Partition') oder die gesamte (aggregierte) Sammlung angezeigt werden können. Ich gehe im Allgemeinen mit einem dieser Ansätze vor, es sei denn, es gibt einen zwingenden Grund für ein Objekt, eine diskrete Mitgliedschaft in separaten Sammlungen zu haben (normalerweise gibt es keinen). In beiden Fällen müssen Sie den gleichzeitigen Zugriff berücksichtigen, und Sie möchten wahrscheinlich eine Snapshot- / Versionierungsfunktion.
Mike Strobel
3

Jedes Zeichen sollte auf die eine oder andere Weise alle Container kennen, in denen es enthalten ist. Dies kann explizit (eine tatsächliche Liste der Container, die darauf verweisen) und berechenbar sein (die Zeichenkoordinaten werden gespeichert, aus denen die tatsächlichen Zellvektoren abgeleitet werden können) ) oder unausgesprochen ("Zeichen können auch in einer dieser sechs Listen gespeichert sein, und ich werde mich nicht darum kümmern, welche zu verfolgen"). Oder eine beliebige Kombination der drei. Der wichtige Teil ist, dass es einfach und effizient ist, jeden Ort zu finden, der einen Charakter betrachtet.

Es sollte auch einen kanonischen Speicher geben, in dem alle Charaktere im Spiel gespeichert sind, z. B. Ihre allCharacters-Karte dort oben.

Ein Charakter lebt genau dann, wenn er in allCharacters enthalten ist. Wenn Sie es aus allCharacters entfernen möchten, entfernen Sie es aus jedem Container, der es ebenfalls enthält (was gemäß dem ersten Absatz einigermaßen effizient sein sollte), und Sie sind festgelegt.

Dies setzt voraus, dass Sie keine Speicherbereinigung haben. Wenn Sie dies tun, können Sie meistens davonkommen, dass der GC sich darum kümmert. Wenn Sie am Ende Lecks haben, erstellen Sie einfach einen Container mit schwachen Referenzen, der alle Entitäten enthält, und verwenden Sie diesen, um zu analysieren, welche Entitäten undicht sind.

ZorbaTHut
quelle
Danke das ist hilfreich. Ich gehe davon aus, dass es keine Speicherbereinigung gibt, fügte das meiner Frage hinzu.
jmp97
Ich habe noch nie eine Plattform verwendet, auf der ich mich für die Spiellogik auf den GC verlassen kann. Die meisten Plattformen haben eine Verzögerung - variable Länge für GCs der Generation -, die Dutzende von Frames lang sein kann, bevor sie wirklich etwas sammeln. Einige Sprachen wie Python stellen sicher, dass nicht kreisförmige Verweise sofort finalisiert werden, aber es kommt sehr selten vor, dass sie nicht in Spielen enthalten sind, insbesondere beim Tod (A und B schießen sich gegenseitig; einer stirbt).
@ Joe, ich spreche nicht wirklich davon, es für die Gameplay-Logik zu verwenden, aber Sie können es leicht für die schwierigen Teile der Cruft-Sammlung verwenden. Mit der Hypothese von jmp97 können Sie ein "Spieler lebt noch" -Flag haben und dann dem "Interaktions-Broadcast" -System eine einfache Bedingung hinzufügen, um sicherzustellen, dass ein Spieler lebt, bevor Sie etwas an ihn senden. (Oder backen Sie das in die Datenstruktur-Durchquerung.) Zu diesem Zeitpunkt ist Ihre Gameplay-Logik in diesem einfachen Flag zentralisiert, und Sie können sich auch darauf verlassen, dass der GC den Speicher später bereinigt.
ZorbaTHut
2

Bei der Beantwortung meiner Frage aus den bisherigen Erfahrungen sehe ich grundsätzlich folgende Optionen:

Extrahieren Sie Ihre spezifischere Sammlung von Objekten jedes Mal aus der allgemeineren Sammlung, wenn Sie sie benötigen

  • pro: zentrales Objekt-Repository
  • con: Rechenaufwand

Bewahren Sie Ihre spezifischere Sammlung in speziellen Behältern auf, wie im Beispiel für diese Frage angegeben

  • Pro: Schnelle Suche / vorsortierte Daten
  • con: mehr involvierte Verfolgung von Objekten

Verwenden Sie das Caching für bestimmte Perspektiven auf / Teilmengen der Spielobjekte

  • Pro: Mittelweg zwischen 1 und 2
  • con: muss eine Caching-Strategie entwickeln, die die Sache komplizieren kann

Zu den Faktoren für die Auswahl einer dieser Optionen gehören

  • Wie oft ist die spezifische Sammlung von Objekten erforderlich?
  • Wie hoch sind die Kosten für die Verfolgung der Objekte über mehrere Container hinweg (dies kann nur im Fall der Objektentfernung relevant sein)?

Das Beobachtermuster kann dazu beitragen, die Notwendigkeit zu verbreiten, die anderen Teilmengen von Objekten zu ändern.

jmp97
quelle