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 AnimationXml
będzie klucz, pod którym się zarejestrowałeś AnimationXmlLoader
, który implementuje IAssetLoader
. Oczywiście PlayerWalkCycle
okreś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 type
i ładował surowe bajty pliku o nazwie name
do 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 IDisposable
i 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 object
wyż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 .