Jak powinienem zbudować rozszerzalny system ładowania zasobów?

19

Dla silnika gry hobby w Javie chcę napisać prosty, ale elastyczny menedżer zasobów / zasobów. Aktywa to dźwięki, obrazy, animacje, modele, tekstury i tak dalej. Po kilku godzinach przeglądania i eksperymentach z kodem wciąż nie jestem pewien, jak to zaprojektować.

W szczególności szukam sposobu, w jaki mogę zaprojektować menedżera w taki sposób, aby streścił sposób w jaki określone typy zasobów są ładowane i skąd są ładowane. Chciałbym móc obsługiwać zarówno system plików, jak i pamięć masową RDBMS, bez konieczności reszty programu o tym wiedzieć. Podobnie chciałbym dodać zasób opisu animacji (FPS, ramki do renderowania, odniesienie do obrazu duszka, itd.), Który jest XML. Powinienem być w stanie napisać do tego klasę z funkcją wyszukiwania i odczytu pliku XML oraz tworzenia i zwracania AnimationAssetklasy z tymi informacjami. Szukam projektu opartego na danych .

Mogę znaleźć wiele informacji o tym, co powinien zrobić zarządca aktywów, ale nie o tym, jak to zrobić. Wydaje się, że generycy biorący udział prowadzą do jakiejś formy kaskadowania klas lub innej klasy klas pomocniczych. Jednak nie widziałem wyraźnego przykładu, który nie wyglądałby jak osobisty hack lub punkt konsensusu.

użytkownik8363
źródło

Odpowiedzi:

23

Zacznę od braku myślenia o zarządzającym aktywami . Myślenie o twojej architekturze w luźno określonych terminach (np. „Manager”) pozwala mentalnie zmieść wiele szczegółów pod dywan, w związku z czym trudniej jest znaleźć rozwiązanie.

Skoncentruj się na swoich specyficznych potrzebach, które wydają się mieć związek z tworzeniem mechanizmu ładowania zasobów, który wyodrębnia bazową pamięć źródłową i umożliwia rozszerzanie obsługiwanego zestawu typów. W twoim pytaniu nie ma nic takiego, na przykład, buforowanie już załadowanych zasobów - co jest w porządku, ponieważ zgodnie z zasadą pojedynczej odpowiedzialności prawdopodobnie powinieneś zbudować pamięć podręczną zasobów jako oddzielną jednostkę i agregować dwa interfejsy gdzie indziej , odpowiednio.

Aby rozwiązać konkretny problem, należy zaprojektować moduł ładujący tak, aby sam nie ładował żadnych zasobów, ale raczej przekazał tę odpowiedzialność interfejsom dostosowanym do ładowania określonych typów zasobów. Na przykład:

interface ITypeLoader {
  object Load (Stream assetStream);
}

Możesz tworzyć nowe klasy, które implementują ten interfejs, a każda nowa klasa jest dostosowana do ładowania określonego typu danych ze strumienia. Korzystając ze strumienia, moduł ładujący może być napisany w oparciu o wspólny interfejs niezależny od pamięci i nie musi być zakodowany na stałe, aby można go było załadować z dysku lub bazy danych; pozwoli to nawet załadować zasoby ze strumieni sieciowych (co może być bardzo przydatne przy wdrażaniu ponownego ładowania zasobów na gorąco, gdy gra jest uruchomiona na konsoli, a narzędzia do edycji na komputerze podłączonym do sieci).

Twój główny moduł ładujący musi być w stanie zarejestrować i śledzić te moduły ładujące:

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

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

„Klucz” użyty tutaj może być czymkolwiek zechcesz - i nie musi to być ciąg znaków, ale łatwo je zacząć. Klucz będzie uwzględniał sposób, w jaki użytkownik powinien zidentyfikować konkretny zasób, i będzie używany do wyszukiwania odpowiedniego modułu ładującego. Ponieważ chcesz ukryć fakt, że implementacja może korzystać z systemu plików lub bazy danych, nie możesz mieć użytkowników odwołujących się do zasobów ścieżką systemu plików lub czymkolwiek podobnym.

Użytkownicy powinni odnosić się do zasobu zawierającego minimum informacji. W niektórych przypadkach wystarczy sama nazwa pliku, ale stwierdziłem, że często pożądane jest użycie pary typu / nazwy, więc wszystko jest bardzo wyraźne. W ten sposób użytkownik może odnosić się do nazwanego wystąpienia jednego z plików XML animacji jako "AnimationXml","PlayerWalkCycle".

Tutaj AnimationXmlbędzie klucz, pod którym się zarejestrowałeś AnimationXmlLoader, który implementuje IAssetLoader. Oczywiście PlayerWalkCycleokreśla konkretny zasób. Biorąc pod uwagę nazwę typu i nazwę zasobu, moduł ładujący zasoby może wysłać zapytanie do swojego trwałego magazynu o surowe bajty tego zasobu. Ponieważ dążymy do maksymalnej ogólności tutaj, możesz to zaimplementować, przekazując modułowi ładującemu dostęp do magazynu podczas jego tworzenia, umożliwiając zastąpienie nośnika pamięci wszystkim, co może zapewnić strumień później:

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>();
}

Bardzo prosty dostawca strumienia po prostu szukałby określonego katalogu głównego zasobu w poszukiwaniu podkatalogu o nazwie typei ładował surowe bajty pliku o nazwie namedo strumienia i zwracał go.

Krótko mówiąc, masz tutaj system, w którym:

  • Istnieje klasa, która umie czytać nieprzetworzone bajty z pewnego rodzaju pamięci wewnętrznej (dysku, bazy danych, strumienia sieciowego, cokolwiek innego).
  • Istnieją klasy, które wiedzą, jak przekształcić strumień surowych bajtów w określony rodzaj zasobu i zwrócić go.
  • Twój „moduł ładujący zasoby” ma po prostu kolekcję powyższych elementów i wie, jak połączyć wyjście dostawcy strumienia z modułem ładującym specyficznym dla typu, a tym samym stworzyć konkretny zasób. Udostępniając sposoby konfiguracji dostawcy strumienia i programów ładujących specyficznych dla typu, masz system, który może być rozszerzony przez klientów (lub Ciebie) bez konieczności modyfikowania faktycznego kodu modułu ładującego zasoby.

Niektóre zastrzeżenia i uwagi końcowe:

  • Powyższy kod to w zasadzie C #, ale powinien być tłumaczony na dowolny język przy minimalnym wysiłku. Aby to ułatwić, pominąłem wiele rzeczy, takich jak sprawdzanie błędów lub prawidłowe używanie IDisposablei inne idiomy, które mogą nie mieć zastosowania bezpośrednio w innych językach. Zostały one jako zadanie domowe dla czytelnika.

  • Podobnie zwracam konkretny zasób jak objectwyżej, ale możesz użyć ogólnych lub szablonów lub cokolwiek innego, aby stworzyć bardziej szczegółowy typ obiektu, jeśli chcesz (powinieneś, z przyjemnością z nim pracować).

  • Jak wyżej, w ogóle nie mam tu do czynienia z buforowaniem. Można jednak łatwo dodawać buforowanie, zachowując ogólność i konfigurowalność. Wypróbuj i przekonaj się!

  • Istnieje wiele sposobów, aby to zrobić, a na pewno nie ma jednego sposobu ani konsensusu, dlatego nie udało się go znaleźć. Próbowałem podać wystarczającą ilość kodu, aby uzyskać dostęp do konkretnych punktów, nie zmieniając tej odpowiedzi w boleśnie długą ścianę kodu. Jest już bardzo długa. Jeśli masz pytania wyjaśniające, możesz skomentować lub znaleźć mnie na czacie .


źródło
1
Dobre pytanie i dobra odpowiedź, która popycha rozwiązanie nie tylko w kierunku projektowania opartego na danych, ale także jak zacząć myśleć w oparciu o dane.
Patrick Hughes,
Bardzo miła i dogłębna odpowiedź. Uwielbiam sposób, w jaki interpretowałeś moje pytanie i powiedziałeś mi dokładnie to, co musiałem wiedzieć, gdy tak słabo je sformułowałem. Dzięki! Czy mógłbyś wskazać mi jakieś zasoby dotyczące strumieni?
user8363,
„Strumień” to po prostu sekwencja (potencjalnie bez określonego końca) bajtów lub danych. Myślałem w szczególności o strumieniu C # , ale prawdopodobnie bardziej interesują Cię klasy strumienia Java - chociaż ostrzegam, że nie znam zbyt wiele Java, więc może nie być idealną klasą do użycia.
Strumienie są zazwyczaj stanowe, ponieważ dany obiekt strumienia ma zwykle bieżącą pozycję odczytu lub zapisu w strumieniu, a wszelkie operacje wejścia / wyjścia wykonywane na nim występują z tej pozycji - dlatego użyłem ich jako danych wejściowych do interfejsów zasobów powyżej, ponieważ w gruncie rzeczy mówią „oto surowe dane i od czego zacząć czytać, czytać z nich i robić swoje”.
Takie podejście uwzględnia niektóre z podstawowych zasad zarówno SOLID, jak i OOP . Brawo.
Adam Naylor,