Wie sollte ich ein erweiterbares System zum Laden von Assets strukturieren?

19

Für eine Hobby-Game-Engine in Java möchte ich einen einfachen, aber flexiblen Asset- / Ressourcen-Manager programmieren. Assets sind Sounds, Bilder, Animationen, Modelle, Texturen usw. Nach ein paar Stunden Browsen und einigen Codeexperimenten bin ich mir immer noch nicht sicher, wie ich dieses Ding entwerfen soll.

Insbesondere möchte ich wissen, wie ich den Manager so gestalten kann, dass er abstrahiert, wie bestimmte Asset-Typen geladen werden und woher die Assets geladen werden. Ich möchte in der Lage sein, sowohl das Dateisystem als auch den RDBMS-Speicher zu unterstützen, ohne dass der Rest des Programms davon Kenntnis haben muss. Ebenso möchte ich ein Animationsbeschreibungsasset (FPS, zu rendernde Frames, Verweis auf das Sprite-Bild usw.) hinzufügen, das XML ist. Ich sollte in der Lage sein, eine Klasse dafür mit der Funktionalität zu schreiben, eine XML-Datei zu finden und zu lesen und eine AnimationAssetKlasse mit diesen Informationen zu erstellen und zurückzugeben . Ich suche ein datengetriebenes Design.

Ich finde viele Informationen darüber, was ein Vermögensverwalter tun sollte, aber nicht darüber, wie er es tun soll. Die beteiligten Generika scheinen zu einer Form der Kaskadierung von Klassen oder zu einer Form von Hilfsklassen zu führen. Ich habe jedoch kein klares Beispiel gesehen, das nicht wie ein persönlicher Hack oder ein Konsenspunkt aussah.

user8363
quelle

Antworten:

23

Ich würde von nicht denken über einen Vermögenswert beginnen Manager . Wenn Sie über Ihre Architektur in groben Zügen nachdenken (wie "Manager"), können Sie im Kopf viele Details unter den Teppich kehren, was es schwieriger macht, sich für eine Lösung zu entscheiden.

Konzentrieren Sie sich auf Ihre spezifischen Anforderungen. Dies scheint mit der Erstellung eines Mechanismus zum Laden von Ressourcen zu tun zu haben, der den zugrunde liegenden Ursprungsspeicher abstrahiert und die Erweiterbarkeit des unterstützten Typensatzes ermöglicht. In Ihrer Frage geht es zum Beispiel nicht wirklich um das Zwischenspeichern bereits geladener Ressourcen. Dies ist in Ordnung, da Sie nach dem Prinzip der einmaligen Verantwortung wahrscheinlich einen Asset-Cache als separate Entität erstellen und die beiden Schnittstellen an anderer Stelle zusammenfassen sollten , wie angemessen.

Um Ihr spezielles Anliegen anzugehen, sollten Sie Ihren Loader so gestalten, dass er keine Assets selbst lädt, sondern diese Verantwortung auf Schnittstellen delegiert, die auf das Laden bestimmter Arten von Assets zugeschnitten sind. Beispielsweise:

interface ITypeLoader {
  object Load (Stream assetStream);
}

Sie können neue Klassen erstellen, die diese Schnittstelle implementieren, wobei jede neue Klasse darauf zugeschnitten ist, einen bestimmten Datentyp aus einem Stream zu laden. Durch die Verwendung eines Streams kann der Type Loader gegen eine gemeinsame, speicherunabhängige Schnittstelle geschrieben werden und muss nicht fest codiert sein, um von der Festplatte oder einer Datenbank geladen zu werden. Dies würde es Ihnen sogar ermöglichen, Ihre Assets aus Netzwerkströmen zu laden (was sehr nützlich sein kann, wenn Sie ein Hot-Reload von Assets durchführen, während Ihr Spiel auf einer Konsole und Ihre Bearbeitungstools auf einem mit dem Netzwerk verbundenen PC ausgeführt werden).

Ihr Haupt-Asset-Loader muss in der Lage sein, diese typspezifischen Loader zu registrieren und zu verfolgen:

class AssetLoader {
  public void RegisterType (string key, ITypeLoader loader) {
    loaders[key] = loader;
  }

  Dictionary<string, ITypeLoader> loaders = new Dictionary<string, ITypeLoader>();
}

Der "Schlüssel", der hier verwendet wird, kann beliebig sein - und es muss keine Zeichenfolge sein, aber diese sind einfach zu beginnen. Der Schlüssel hängt davon ab, wie Sie von einem Benutzer erwarten, dass er ein bestimmtes Asset identifiziert, und wird zum Nachschlagen des entsprechenden Loaders verwendet. Da Sie die Tatsache ausblenden möchten, dass die Implementierung möglicherweise ein Dateisystem oder eine Datenbank verwendet, dürfen Benutzer nicht über einen Dateisystempfad oder ähnliches auf Assets verweisen.

Benutzer sollten mit einem Minimum an Informationen auf ein Asset verweisen. In einigen Fällen würde nur ein Dateiname ausreichen, aber ich habe festgestellt, dass es oft wünschenswert ist, ein Typ / Name-Paar zu verwenden, damit alles sehr eindeutig ist. Daher kann ein Benutzer auf eine benannte Instanz einer Ihrer Animations-XML-Dateien verweisen als "AnimationXml","PlayerWalkCycle".

Hier AnimationXmlwäre der Schlüssel, unter dem Sie sich registriert haben AnimationXmlLoader, der implementiert IAssetLoader. PlayerWalkCycleIdentifiziert offensichtlich den spezifischen Vermögenswert. Mit einem Typnamen und einem Ressourcennamen kann Ihr Asset Loader den persistenten Speicher nach den unformatierten Bytes dieses Assets abfragen. Da wir hier ein Höchstmaß an Allgemeingültigkeit anstreben, können Sie dies implementieren, indem Sie dem Loader beim Erstellen einen Speicherzugriff übermitteln, sodass Sie das Speichermedium durch alles ersetzen können, was später einen Stream bereitstellen kann:

interface IAssetStreamProvider {
  Stream GetStream (string type, string name);
}

class AssetLoader {
  public AssetLoader (IAssetStreamProvider streamProvider) {
    provider = streamProvider;
  }

  object LoadAsset (string type, string name) {
    var loader = loaders[type];
    var stream = provider.GetStream(type, name);

    return loader.Load(stream);
  }

  public void RegisterType (string type, ITypeLoader loader) {
    loaders[type] = loader;
  }

  IAssetStreamProvider provider;
  Dictionary<string, ITypeLoader> loaders = new Dictionary<string, ITypeLoader>();
}

Ein sehr einfacher Stream-Provider sucht einfach in einem angegebenen Asset-Stammverzeichnis nach einem Unterverzeichnis mit dem Namen typeund lädt die unformatierten Bytes der genannten Datei namein einen Stream und gibt sie zurück.

Kurz gesagt, was Sie hier haben, ist ein System, in dem:

  • Es gibt eine Klasse, die weiß, wie rohe Bytes aus einem Back-End-Speicher gelesen werden (eine Festplatte, eine Datenbank, ein Netzwerk-Stream, was auch immer).
  • Es gibt Klassen, die wissen, wie ein Raw-Byte-Stream in eine bestimmte Art von Ressource umgewandelt und zurückgegeben wird.
  • Ihr tatsächlicher "Asset Loader" verfügt lediglich über eine Sammlung der oben genannten Elemente und kann die Ausgabe des Stream-Providers in den typspezifischen Loader leiten und so ein konkretes Asset erstellen. Indem Sie Möglichkeiten zur Konfiguration des Stream-Providers und der typspezifischen Loader bereitstellen, können Sie das System durch Clients (oder Sie selbst) erweitern, ohne den tatsächlichen Code des Asset Loader ändern zu müssen.

Einige Vorsichtsmaßnahmen und Schlussbemerkungen:

  • Der obige Code ist im Grunde C #, sollte aber mit minimalem Aufwand in nahezu jede Sprache übersetzt werden können. Um dies zu vereinfachen, habe ich viele Dinge weggelassen, wie z. B. die Fehlerprüfung oder die korrekte Verwendung von IDisposableund anderer Redewendungen, die in anderen Sprachen möglicherweise nicht direkt zutreffen. Diese bleiben dem Leser als Hausaufgabe.

  • Ebenso gebe ich das konkrete Asset wie objectoben zurück, aber Sie können Generika oder Vorlagen oder was auch immer verwenden, um einen spezifischeren Objekttyp zu erzeugen, wenn Sie möchten (Sie sollten, es ist schön, mit zu arbeiten).

  • Wie oben beschäftige ich mich hier überhaupt nicht mit Caching. Sie können das Zwischenspeichern jedoch einfach und mit der gleichen Allgemeinheit und Konfigurierbarkeit hinzufügen. Probieren Sie es aus und sehen Sie!

  • Es gibt viele, viele und viele Möglichkeiten, dies zu tun, und es gibt sicherlich keinen einzigen Weg oder Konsens, weshalb Sie keinen gefunden haben. Ich habe versucht, genügend Code bereitzustellen, um die spezifischen Punkte zu vermitteln, ohne diese Antwort in eine schmerzhaft lange Code-Wand zu verwandeln. Es ist schon außerordentlich lange so. Wenn Sie Fragen haben, können Sie diese gerne kommentieren oder mich im Chat finden .


quelle
1
Gute Frage und gute Antwort, die die Lösung nicht nur in Richtung eines datengetriebenen Designs treibt, sondern auch in Richtung eines datengetriebenen Denkens.
Patrick Hughes
Sehr nette und ausführliche Antwort. Ich finde es toll, wie Sie meine Frage interpretiert und mir genau gesagt haben, was ich wissen musste, während ich sie so schlecht formuliert habe. Vielen Dank! Könnten Sie mich vielleicht auf einige Ressourcen zu Streams hinweisen?
User8363
Ein "Stream" ist nur eine Sequenz (möglicherweise ohne bestimmbares Ende) von Bytes oder Daten. Ich habe speziell an C # 's Stream gedacht , aber Sie interessieren sich wahrscheinlich mehr für Javas Stream- Klassen - obwohl ich gewarnt sein muss, dass ich nicht allzu viel Java kenne, sodass dies möglicherweise keine ideale Klasse für die Verwendung ist.
Streams sind in der Regel zustandsbehaftet, da ein bestimmtes Stream-Objekt normalerweise eine aktuelle Lese- oder Schreibposition innerhalb des Streams hat und alle E / A-Vorgänge, die Sie darauf ausführen, von dieser Position aus erfolgen. Deshalb habe ich sie als Eingaben für die obigen Asset-Schnittstellen verwendet. weil sie im Wesentlichen sagen "hier sind einige Rohdaten und wo man anfängt zu lesen, daraus zu lesen und dein Ding zu machen."
Mit diesem Ansatz werden einige der Kernprinzipien von SOLID und OOP gewürdigt . Bravo.
Adam Naylor