Jak zaprojektować AssetManager?

26

Jakie jest najlepsze podejście do projektowania AssestManager, który będzie zawierał odniesienia do grafiki, dźwięków itp. Gry?

Czy te zasoby powinny być przechowywane w parze Mapa / klucz? Czy pytam o zasób „w tle”, a mapa zwraca powiązaną bitmapę? Czy jest jeszcze lepszy sposób?

W szczególności piszę grę na Androida / Java, ale odpowiedzi mogą być ogólne.

Bryan Denny
źródło

Odpowiedzi:

16

To zależy od zakresu twojej gry. Menedżer zasobów jest absolutnie niezbędny w przypadku większych tytułów, a mniej w przypadku mniejszych gier.

W przypadku większych tytułów musisz poradzić sobie z takimi problemami:

  • Wspólne zasoby - czy ta tekstura cegły jest używana przez wiele modeli?
  • Żywotność zasobu - czy zasób załadowany 15 minut temu nie jest już potrzebny? Odnośnik zliczający Twoje zasoby, aby upewnić się, że wiesz, kiedy coś jest zakończone itp
  • W DirectX 9, jeśli niektóre typy zasobów są załadowane, a urządzenie graficzne „zgubiło się” (dzieje się tak, jeśli naciśniesz Ctrl + Alt + Del między innymi) - gra będzie musiała je odtworzyć
  • Ładowanie zasobów przed ich potrzebą - bez tego nie można tworzyć dużych gier z otwartym światem
  • Zbiorcze ładowanie zasobów - często pakujemy wiele zasobów w jeden plik, aby skrócić czas ładowania - wyszukiwanie wokół dysku jest bardzo czasochłonne

W przypadku mniejszych tytułów jest to mniejszy problem, frameworki takie jak XNA mają w sobie menedżerów zasobów - ponowne wymyślenie tego nie ma większego sensu.

Jeśli uważasz, że potrzebujesz menedżera zasobów, naprawdę nie ma jednego uniwersalnego rozwiązania, ale zauważyłem, że mapa hash z kluczem jako hash * nazwy pliku (obniżona, a separatory „naprawione”) działa dobrze dla projektów, nad którymi pracowałem.

Zazwyczaj nie zaleca się wpisywania nazw plików w kodzie na stałe, zwykle lepiej jest, aby inny format danych (np. Xml) przedstawiał nazwy plików jako „identyfikatory”.

  • Jako zabawną notatkę dodatkową, zwykle dostajesz jedną kolizję skrótu na projekt.
icStatic
źródło
Tylko dlatego, że musisz zarządzać zasobami, nie wymaga AssetManagers, ważnego ważnego rzeczownika, który prawdopodobnie ma zbyt wiele metod, słabą wydajność i mętną semantykę pamięci. Dla porównania zastanów się, co się stanie, jeśli masz dużo zarządzania projektami (zwykle dobrze), a następnie, gdy masz wielu kierowników projektów (zwykle źle).
2
@Joe Wreschnig - jak spełniłbyś pięć wymagań wspomnianych przez icStatic bez korzystania z menedżera aktywów?
antinome
8

(Próbuję uniknąć omawiania „nie używaj menedżera zasobów”, ponieważ uważam to za nietypowe.)

Mapa klucz / wartość jest bardzo użytecznym podejściem.

Mamy jedną implementację ResourceManager, w której można rejestrować fabryki dla różnych typów zasobów.

Metoda „getResource” używa szablonów, aby znaleźć poprawną Fabrykę dla pożądanego typu zasobu i zwraca określony ResourceHandle (ponownie używając szablonu do zwrócenia określonegoResourceHandle).

Zasoby są ponownie liczone przez ResourceManager (wewnątrz ResourceHandle) i zwalniane, gdy nie są już potrzebne.

Pierwszym dodatkiem, który napisaliśmy, była metoda „reload (XYZ)”, która pozwala nam zmieniać zasoby spoza działającego silnika bez zmiany kodu lub ponownego ładowania gry. (Jest to niezbędne, gdy artyści pracują na konsolach;))

W większości przypadków mamy tylko instancję menedżera zasobów, ale czasami tworzymy nową instancję tylko dla poziomu lub mapy. W ten sposób możemy po prostu wywołać „zamknięcie” na levelResourceManager i upewnić się, że nic nie wycieka.

(krótki) przykład

// 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
źródło
6

Dedykowane klasy menedżerskie prawie nigdy nie są odpowiednim narzędziem inżynieryjnym. Jeśli potrzebujesz zasobu tylko raz (jak tło lub mapa), powinieneś zażądać go tylko raz i pozwolić, aby umarł normalnie, gdy skończysz. Jeśli potrzebujesz buforować określony rodzaj obiektu, powinieneś użyć fabryki, która najpierw sprawdza pamięć podręczną, a w inny sposób ładuje coś, umieszcza ją w pamięci podręcznej, a następnie zwraca - i ta fabryka może być po prostu funkcją statyczną uzyskującą dostęp do zmiennej statycznej , nie jest rodzajem własnego.

Steve Yegge (pośród wielu, wielu innych) napisał dobrą historię o tym, jak ostatecznie bezużyteczne klasy menedżerskie, na podstawie singletonu, są. http://sites.google.com/site/steveyegge2/singleton-consanted-stupid


źródło
2
Ok pewnie. Ale w przypadkach takich jak Android (lub inne gry) musisz załadować dużo grafiki / dźwięków do pamięci przed uruchomieniem gry, a nie podczas. Jak mogę użyć tego, co mówisz (fabryki), aby to zrobić podczas ekranu ładowania? Wystarczy nacisnąć każdy obiekt w fabryce na ekranie ładowania, aby je buforował?
Bryan Denny
Nie znam szczegółów Androida, ale nie mam pojęcia, co rozumiesz przez „przed rozpoczęciem gry”. Czy naprawdę niemożliwe jest załadowanie zasobu, gdy jest on potrzebny (lub gdy będzie potrzebny „wkrótce”), a nie po uruchomieniu programu? Uważam to za bardzo mało prawdopodobne, w przeciwnym razie np. Nigdy nie będziesz mieć więcej tekstur niż mieści się w skromnej pamięci RAM Androida.
@Joe spójrz na moje inne pytanie dotyczące „ładowania ekranów”: gamedev.stackexchange.com/questions/1171/... Uderzenie w pustą pamięć podręczną oznacza długi czas na przejście na dysk i może spowodować pewne pogorszenie wydajności FPS podczas tych pierwszych połączeń . Jeśli już wiesz, co trafisz z wyprzedzeniem, równie dobrze możesz trafić go podczas ładowania, aby wstępnie buforować, prawda?
Bryan Denny
Znowu nie mogę rozmawiać z Androidem, ale zwykle przechodzenie na dysk jest dokładnie tym, co możesz zrobić bez przyjmowania trafień FPS, ponieważ wątek przechodzący na dysk w ogóle nie zużywa żadnego procesora. Musisz tylko zrobić to z odpowiednim wyprzedzeniem, aby nie dostać się do pop-inu. Jeśli zamierzasz wstępnie buforować wszystko, ponieważ wiesz z wyprzedzeniem, czego potrzebujesz, naprawdę nie potrzebujesz AssetManager, ponieważ nie musisz w ogóle zarządzać zasobami - są już dostępne.
1
@Joe, czy fabryka nie jest także „Dedicated Manager”?
MSN
2

Zawsze uważałem, że dobry zarządca aktywów powinien mieć kilka trybów działania. Te tryby najprawdopodobniej będą oddzielnymi modułami źródłowymi przylegającymi do wspólnego interfejsu. Dwa podstawowe tryby działania to:

  • Tryb produkcji - wszystkie zasoby są lokalne i pozbawione wszystkich metadanych
  • Tryb programowania - assesty są przechowywane w bazie danych (np. MySQL itp.) Z dodatkowymi metadanymi. Baza danych byłaby dwupoziomowym systemem z lokalną bazą danych buforującą współużytkowaną bazę danych. Twórcy treści mogliby edytować i aktualizować współużytkowaną bazę danych oraz aktualizacje automatycznie propagowane do systemów dla programistów / QA. Powinna również istnieć możliwość tworzenia treści zastępczych. Ponieważ wszystko znajduje się w bazie danych, w bazie danych można generować zapytania i generować raporty w celu analizy stanu produkcji.

Potrzebujesz narzędzia, które może pobrać wszystkie assesty ze współużytkowanej bazy danych i utworzyć produkcyjny zestaw danych.

Przez lata jako programista nigdy nie widziałem czegoś takiego, chociaż pracowałem tylko dla kilku firm, więc mój pogląd nie jest tak naprawdę reprezentatywny.

Aktualizacja

OK, trochę głosów negatywnych. Rozwinę ten projekt.

Po pierwsze, tak naprawdę nie potrzebujesz klas fabrycznych, ponieważ jeśli masz:

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

znasz typ, więc po prostu:

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

ale potem próbowałem powiedzieć powyżej, że i tak nie używałbyś jawnych nazw plików, tekstura do załadowania byłaby określona przez model, na którym tekstura jest używana, więc tak naprawdę nie potrzebujesz czytelnej dla człowieka nazwy, może to być 32-bitowa wartość całkowita, co znacznie ułatwia obsługę procesora. Tak więc w konstruktorze TextHandle miałbyś:

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 używa parametru resource_id do znalezienia lokalizacji danych. Sposób, w jaki to zrobił, byłby zależny od środowiska, w którym działasz:

W fazie rozwoju: strumień wyszukuje identyfikator w bazie danych (na przykład za pomocą SQL), aby uzyskać nazwę pliku, a następnie otwiera plik, plik może zostać buforowany lokalnie lub pobrany z serwera, jeśli plik lokalny nie istnieje lub jest przeterminowany.

W wersji: strumień wyszukuje identyfikator w tabeli klucz / wartość, aby uzyskać przesunięcie / rozmiar dużego, spakowanego pliku (takiego jak plik WAD Dooma).

Skizz
źródło
Głosowałem za odrzuceniem, ponieważ zasugerowałeś przywrócenie wszystkiego do tabeli SQL z kluczami głównymi zamiast używania prawdziwego VCS. Zastanawiam się także nad przedwczesną optymalizacją używania nieprzejrzystych identyfikatorów zamiast nazw ciągów. Użyłem ciągów w dwóch dużych projektach dla wszystkich zasobów innych niż klucze tłumaczeń, z których mieliśmy setki tysięcy bardzo długich kluczy ciągów (a następnie tylko do przeniesienia na konsole). Zazwyczaj były one znormalizowane, więc moglibyśmy używać porównań wskaźników zamiast porównań ciągów, ale porównania ciągów są często zdominowane przez koszt pobierania pamięci, a nie rzeczywiste porównanie.
@Joe: Podałem tylko SQL jako przykład, a potem tylko w środowisku programistycznym, możesz równie dobrze użyć VCS. Zasugerowałem tylko bazę danych SQL, ponieważ możesz następnie dodać dodatkowe informacje do przechowywanych obiektów i użyć funkcji SQL do wyszukiwania informacji z bazy danych (więcej korzyści z zarządzania niż cokolwiek innego). Jeśli chodzi o nieprzejrzyste identyfikatory jako przedwczesną optymalizację - wydaje mi się, że niektórzy mogą to postrzegać w ten sposób, ale myślę, że łatwiej byłoby zacząć od tego, niż pokazywać to później. Nie sądzę, aby miało to duży wpływ na rozwój, gdybyś użył identyfikatora lub ciągów.
Skizz
2

To, co lubię robić dla zasobów, to skonfigurować menedżera brył . Zainspirowane silnikiem Doom, bryły to fragmenty danych, które zawierają zasoby, przechowywane w pliku bryły, który deklaruje nazwy brył, długości, typ (mapa bitowa, dźwięk, moduł cieniujący itp.) Oraz typ zawartości (plik, kolejna bryła, wewnątrz plik bryłowy). Po uruchomieniu te bryły są wprowadzane do drzewa binarnego, ale nie są jeszcze ładowane. Każda mapa (która jest również bryłą) ma listę zależności, które są po prostu nazwami brył, które musi działać mapa. Te grudki, o ile nie zostały już załadowane, są ładowane podczas ładowania mapy. Dodatkowo ładowane są sąsiednie mapy, nie tylko w tym samym czasie, ale z jakiegoś powodu, gdy silnik pracuje na biegu jałowym. Może to sprawić, że mapy będą płynne i nie będzie ekranu ładowania.

Moja metoda jest idealna do map w otwartym świecie, ale gra oparta na poziomie nie odniesie korzyści z płynności, jaką zapewnia ta metoda. Mam nadzieję że to pomoże!

Marcus Cramer
źródło