Wie entwerfe ich einen AssetManager?

26

Was ist der beste Ansatz zum Entwerfen eines AssestManager, der Verweise auf Grafiken, Sounds usw. eines Spiels enthält?

Sollten diese Assets in einem Schlüssel / Wert-Map-Paar gespeichert werden? Dh ich frage nach "Hintergrund" -Asset und die Map gibt die zugehörige Bitmap zurück? Gibt es einen noch besseren Weg?

Insbesondere schreibe ich ein Android / Java-Spiel, aber die Antworten können allgemeiner Natur sein.

Bryan Denny
quelle

Antworten:

16

Das hängt vom Umfang Ihres Spiels ab. Ein Vermögensverwalter ist für größere Titel unabdingbar, weniger für kleinere Spiele.

Bei größeren Titeln müssen Sie Probleme wie die folgenden bewältigen:

  • Geteilte Assets - Wird diese Ziegelstruktur von mehreren Modellen verwendet?
  • Lebensdauer des Assets - Wird dieses Asset, das Sie vor 15 Minuten geladen haben, nicht mehr benötigt? Zählen Sie Ihr Vermögen nach, um sicherzustellen, dass Sie wissen, wann etwas fertig ist usw
  • Wenn in DirectX 9 bestimmte Asset-Typen geladen sind und Ihr Grafikgerät verloren geht (dies passiert unter anderem, wenn Sie Strg + Alt + Entf drücken), muss Ihr Spiel sie neu erstellen
  • Laden Sie Assets, bevor Sie sie benötigen - ohne diese können Sie keine großen Open-World-Spiele erstellen
  • Massenladen von Assets - Wir packen oft viele Assets in eine einzelne Datei, um die Ladezeiten zu verkürzen. Das Durchsuchen der Disc ist sehr zeitaufwändig

Bei kleineren Titeln sind diese Dinge weniger problematisch, Frameworks wie XNA haben Asset Manager in sich - es macht wenig Sinn, sie neu zu erfinden.

Wenn Sie feststellen, dass Sie einen Asset Manager benötigen, gibt es eigentlich keine einheitliche Lösung, aber ich habe festgestellt, dass eine Hash-Map mit dem Schlüssel als Hash * des Dateinamens (gesenkt und Trennzeichen alle "fest") funktioniert gut für die Projekte, an denen ich gearbeitet habe.

In der Regel ist es nicht ratsam, Dateinamen in Ihrer App fest zu codieren. In der Regel ist es besser, wenn ein anderes Datenformat (z. B. XML) Dateinamen in "IDs" darstellt.

  • Als amüsante Randnotiz erhalten Sie normalerweise eine Hash-Kollision pro Projekt.
icStatic
quelle
Nur weil Sie Assets verwalten müssen, sind keine AssetManager erforderlich, ein wichtiges Substantiv mit Großbuchstaben, das wahrscheinlich über zu viele Methoden, eine schlechte Leistung und eine unsaubere Speichersemantik verfügt. Für einen Vergleich, darüber nachzudenken , was passiert , wenn Sie eine Menge von Projekt - Management ( in der Regel gut), und dann , wenn Sie eine Menge von Projekt Managern ( in der Regel schlecht).
2
@Joe Wreschnig - Wie würden Sie die fünf von icStatic genannten Anforderungen ohne Einsatz eines Asset Managers angehen?
Antinome
8

(Ich versuche, die "Don't use a Asset Manager" -Diskussion hier zu vermeiden, da ich sie für offtopisch halte.)

Eine Key / Value-Map ist ein sehr brauchbarer Ansatz.

Wir haben eine ResourceManager-Implementierung, in der Fabriken für verschiedene Ressourcentypen registriert werden können.

Die Methode "getResource" verwendet Vorlagen, um die richtige Factory für den gewünschten Ressourcentyp zu finden, und gibt ein bestimmtes ResourceHandle zurück (ebenfalls mithilfe der Vorlage, um ein bestimmtes ResourceHandle zurückzugeben).

Die Ressourcen werden vom ResourceManager (im ResourceHandle) neu gezählt und freigegeben, wenn sie nicht mehr benötigt werden.

Das erste Addon, das wir geschrieben haben, war die "reload (XYZ)" - Methode, mit der wir Ressourcen von außerhalb der laufenden Engine ändern können, ohne Code zu ändern oder das Spiel neu zu laden. (Dies ist wichtig, wenn Künstler an Konsolen arbeiten;))

Meistens haben wir nur eine Instanz des ResourceManagers, aber manchmal erstellen wir eine neue Instanz nur für eine Ebene oder eine Karte. Auf diese Weise können wir im levelResourceManager einfach "shutdown" aufrufen und sicherstellen, dass nichts ausläuft.

(kurzes) Beispiel

// very abbreviated!
// this code would never survive our coding guidelines ;)

ResourceManager* pRm = new ResourceManager;
pRm->initialize( );
pRm->registerFactory( new TextureFactory );
// [...]
TextureHandle tex = pRm->getResource<Texture>( "test.otx" ); // in real code we use some macro magic here to use CRCs for filenames
tex->storeToHardware( 0 ); // channel 0

pRm->releaseResource( pRm );

// [...]
pRm->shutdown(); // will log any leaked resource
Andreas
quelle
6

Engagierte Manager-Klassen sind so gut wie nie das richtige Engineering-Tool. Wenn Sie das Asset nur einmal benötigen (z. B. einen Hintergrund oder eine Karte), sollten Sie es nur einmal anfordern und es nach Abschluss des Vorgangs normal sterben lassen. Wenn Sie eine bestimmte Art von Objekt zwischenspeichern müssen, sollten Sie eine Factory verwenden, die zuerst einen Cache überprüft und andernfalls etwas lädt, es in den Cache stellt und es dann zurückgibt - und diese Factory kann einfach eine statische Funktion sein, die auf eine statische Variable zugreift , keine eigene Art.

Steve Yegge (unter vielen, vielen anderen) hat eine gute Geschichte darüber geschrieben, wie nutzlose Manager-Klassen aufgrund des Singleton-Musters am Ende sein können. http://sites.google.com/site/steveyegge2/singleton-considered-stupid


quelle
2
Okay, sicher. In Fällen wie Android (oder anderen Spielen) müssen Sie jedoch viele Grafiken / Sounds in den Speicher laden, bevor Sie das Spiel starten, nicht während. Wie kann ich das, was Sie sagen (Fabriken), verwenden, um dies während eines Ladebildschirms zu tun? Schlagen Sie einfach jedes Objekt in der Fabrik auf den Ladebildschirm, damit es sie zwischenspeichert.
Bryan Denny
Ich bin mit Android-Details nicht vertraut, aber ich habe keine Ahnung, was Sie mit "bevor Sie das Spiel starten" meinen. Ist es wirklich unmöglich, eine Ressource zu laden, wenn Sie sie benötigen (oder wenn Sie sie "bald" benötigen), anstatt wenn Sie das Programm starten? Ich finde das extrem unwahrscheinlich, sonst könnte man zB nie mehr Texturen haben, als in das magere RAM von Android passen.
@Joe werfen Sie einen Blick auf meine andere Frage zum Thema "Ladebildschirme": gamedev.stackexchange.com/questions/1171/… Wenn Sie einen leeren Cache öffnen, dauert es lange, bis Sie auf die Festplatte zugreifen. Dies kann bei diesen ersten Aufrufen zu Leistungseinbußen bei FPS führen . Wenn Sie bereits wissen, was Sie vorzeitig treffen werden, können Sie es auch während des Ladens treffen, um es vorab im Cache zu speichern, oder?
Bryan Denny
Wieder kann ich nicht mit Android sprechen, aber normalerweise können Sie genau das tun, was Sie tun können, ohne FPS-Treffer zu erhalten, da der Thread, der auf die Festplatte geht, überhaupt keine CPU verbraucht. Sie müssen es nur so weit im Voraus budgetieren, dass Sie kein Pop-In erhalten. Wenn Sie alles vorab zwischenspeichern möchten, weil Sie im Voraus wissen, was Sie benötigen, benötigen Sie wirklich keinen AssetManager, da Sie die Assets überhaupt nicht verwalten müssen - sie sind bereits alle verfügbar.
1
@ Joe, ist eine Fabrik nicht auch ein "engagierter Manager"?
MSN
2

Ich habe immer gedacht, dass ein guter Vermögensverwalter mehrere Betriebsmodi haben sollte. Diese Modi wären höchstwahrscheinlich separate Quellmodule, die einer gemeinsamen Schnittstelle angehören. Die zwei grundlegenden Betriebsarten wären:

  • Produktionsmodus - Alle Assets sind lokal und werden von allen Metadaten befreit
  • Entwicklungsmodus - Tests werden in einer Datenbank (z. B. MySQL usw.) mit zusätzlichen Metadaten gespeichert. Die Datenbank wäre ein zweistufiges System mit einer lokalen Datenbank, die eine gemeinsam genutzte Datenbank zwischenspeichert. Ersteller von Inhalten können die gemeinsam genutzte Datenbank bearbeiten und aktualisieren. Aktualisierungen werden automatisch an Entwickler- / QS-Systeme weitergeleitet. Es sollte auch möglich sein, Platzhalterinhalte zu erstellen. Da sich alles in einer Datenbank befindet, können Abfragen in der Datenbank durchgeführt und Berichte erstellt werden, um den Status der Produktion zu analysieren.

Sie benötigen ein Tool, mit dem Sie alle Assests aus der freigegebenen Datenbank abrufen und den Produktionsdatensatz erstellen können.

In meinen Jahren als Entwickler habe ich so etwas noch nie gesehen, obwohl ich nur für eine Handvoll Unternehmen gearbeitet habe, so dass meine Ansicht nicht wirklich repräsentativ ist.

Aktualisieren

OK, einige negative Stimmen. Ich werde diesen Entwurf erweitern.

Erstens brauchen Sie keine Factory-Klassen, denn wenn Sie haben:

TextureHandle tex = pRm->getResource<Texture>( "test.otx" );

Sie kennen den Typ, machen Sie also einfach:

TextureHandle tex = new TextureHandle ("test.otx");

Aber was ich oben versucht habe zu sagen, ist, dass Sie sowieso keine expliziten Dateinamen verwenden würden. Die zu ladende Textur wird durch das Modell bestimmt, auf dem die Textur verwendet wird. Es könnte sich um einen 32-Bit-Integer-Wert handeln, der für die CPU viel einfacher zu handhaben ist. Im Konstruktor für TextureHandle hätten Sie also:

if (texture already loaded)
  update texture reference count
else
  asset_stream = new AssetStream (resource_id)
  asset_stream->ReadBytes
  create texture
  set texture ref count to 1

AssetStream verwendet den Parameter resource_id, um den Speicherort der Daten zu ermitteln. Die Vorgehensweise hängt von der Umgebung ab, in der Sie ausgeführt werden:

In der Entwicklung: Der Stream sucht in einer Datenbank nach der ID (z. B. mit SQL), um einen Dateinamen zu erhalten, und öffnet die Datei. Die Datei kann lokal zwischengespeichert oder von einem Server abgerufen werden, wenn die lokale Datei nicht vorhanden ist oder vorhanden ist veraltet.

In Release: Der Stream sucht die ID in einer Schlüssel- / Wertetabelle, um einen Versatz / eine Größe in einer großen, gepackten Datei (wie der WAD-Datei von Doom) zu erhalten.

Skizz
quelle
Ich habe Sie abgelehnt, weil Sie vorgeschlagen haben, alles in eine SQL-Tabelle mit Primärschlüsseln zu integrieren, anstatt ein echtes VCS zu verwenden. Ich denke auch darüber nach, undurchsichtige IDs anstelle einer vorzeitigen Optimierung von String-Namen zu verwenden. Ich habe bei zwei großen Projekten Zeichenfolgen für alle Assets außer Übersetzungsschlüsseln verwendet, von denen wir Hunderttausende sehr langer Zeichenfolgenschlüssel hatten (und dann nur zum Portieren auf Konsolen). Sie wurden normalerweise normalisiert, sodass wir Zeigervergleiche anstelle von Zeichenfolgenvergleichen verwenden konnten. Zeichenfolgenvergleiche werden jedoch häufig von den Kosten des Speicherabrufs und nicht von den tatsächlichen Vergleichskosten bestimmt.
@Joe: Ich habe nur SQL als Beispiel angegeben und dann konnte man nur in einer Entwicklungsumgebung ebenfalls ein VCS verwenden. Ich habe nur die SQL-Datenbank vorgeschlagen, da Sie dann den gespeicherten Objekten zusätzliche Informationen hinzufügen und mithilfe der SQL-Funktionen Informationen aus der Datenbank abfragen können (dies ist mehr ein Verwaltungsgewinn als alles andere). Undurchsichtige IDs als vorzeitige Optimierung - manche sehen das vielleicht so, aber ich denke, es wäre einfacher, damit anzufangen, als es zu einem späteren Zeitpunkt in der Entwicklung zu zeigen. Ich denke nicht, dass es die Entwicklung stark beeinträchtigen würde, wenn Sie ID oder Zeichenfolgen verwenden.
Skizz
2

Was ich gerne für Assets mache, ist die Einrichtung eines Pauschalmanagers . Von der Doom-Engine inspiriert, sind Klumpen Datenelemente, die Assets enthalten und in einer Klumpendatei gespeichert sind , in der die Namen, Längen, der Typ (Bitmap, Sound, Shader usw.) und der Inhaltstyp (Datei, ein weiterer Klumpen usw.) angegeben sind die Klumpendatei selbst). Beim Start werden diese Klumpen in einen Binärbaum eingetragen, aber noch nicht geladen. Jede Karte (die auch ein Knoten ist) hat eine Liste von Abhängigkeiten, die einfach die Namen der Knoten sind, die die Karte benötigt, um zu funktionieren. Diese Klumpen werden, sofern sie nicht bereits geladen wurden, zum Zeitpunkt des Ladens der Karte geladen. Darüber hinaus werden die Klumpen der angrenzenden Karten geladen, und zwar nicht gleichzeitig, sondern wenn der Motor aus irgendeinem Grund im Leerlauf läuft. Dies kann die Karten nahtlos machen und es gibt keinen Ladebildschirm.

Meine Methode ist perfekt für Karten mit offener Welt, aber ein Level-basiertes Spiel wird nicht von der Nahtlosigkeit profitieren, die diese Methode für Sie mit sich bringt. Hoffe das hilft!

Marcus Cramer
quelle