Warsztat.GDCompo!ProjektyMediaArtykułyQ&AForumOferty pracyPobieranie

Opisz napotkaną sytuację, a redakcja niezwłocznie znajdzie rozwiązanie!

wyślij anuluj

Przechowywanie zasobów

Tekst został importowany z Warsztatowych artykułów. Jego oryginalnym autorem jest MoN. Jeżeli został importowany poprawnie, usuń ten szablon!

Przechowywanie Zasobów


Wczytywanie i przechowywanie danych jest bardzo ważne w grach. W małych produkcjach typu snake czy saper można pozwolić sobie na trzymanie wszystkiego "luzem", ale i większy projekt, tym więcej komplikacji powoduje takie rozwiązanie. W pewnym momencie musimy napisać porządny system zarządzający potrzebnymi nam zasobami - będzie je za nas przechowywał, zwalniał itd.

Takie systemy zarządzające zwane są zazwyczaj z angielskiego managerami, czyli zarządcami. Natchniony artykułem w "Perełkach programowania gier cz. 1" napisałem własną, uniwersalną i zdatna do ponownego użycia klasę hermetyzującą podstawową funkcjonalność managera. Właściwości, których potrzebowałem są następujące:

  1. Musi być prawdziwie uniwersalny - nadawać się do przechowywania różnorodnych danych oraz umożliwiać bezproblemowe wielokrotne używanie.
  2. Musi hermetyzować całą operację - nie ma mowy, by dawał użytkownikowi wskaźnik jako uchwyt do zasobu
  3. Musi (w zależności od typu zasobu) unikać duplikacji danych - jeśli np. dwukrotnie wczytywany jest ten sam dźwięk, manager nie powinien przechowywać w pamięci kopii tych samych danych
  4. W związku z powyższym powinien również posiadać licznik referencji do zasobów, by unikać zwalniania ciągle używanych danych
  5. Powinien zawierać funkcje do zwalniania i ponownego wczytywania wszystkich danych (np. gdy gra jest pauzowana lub przestaje być aktywna - dzięki temu pamięć nie jest marnowana)

Dołączona klasa IManager realizuje wszystkie powyższe wymagania (choć cechę 5. można udoskonalić). Jest ona gotowa do użycia. Poniżej znajduje się dokładny jej opis i wytłumaczenie, w jaki sposób wdrożyłem założenia w życie.

System uchwytów, który zastosowałem, jest prawie identyczny jak w "Perełkach...", jest jednak parę różnic:

template < typename T> struct Uchwyt { unsigned short Indeks, MagicznaLiczba; bool Poprawny() { return Indeks == (unsigned short)(~MagicznaLiczba); } void Ustaw(unsigned short nIndeks) { Indeks = nIndeks, MagicznaLiczba = (~Indeks); } void UstawNaNiepoprawny() { Indeks = 0; MagicznaLiczba = 0; } };

Struktura Uchwyt jest parametryzowana w sumie na wszelki wypadek, dzięki temu:

struct TexID{ }; struct FontID { }; typedef Uchwyt< TexID> HTekstura; typedef Uchwyt< FontID> HCzcionka;

typy Htekstura i HCzcionka są (przynajmniej teoretycznie) różnymi typami - unikniemy dzięki temu odwoływania się do np. do czcionek za pomocą uchwytu do tekstury itp.

MagicznaLiczba służy do szybkiego ustalenia, czy uchwyt jest poprawny. Dzięki temu unikamy odwoływania się do nieistniejących zasobów (przez np. uchwyt z losowymi wartościami) oraz mamy łatwą furtkę do komunikowania błędu przy wczytywaniu - zwracamy błędny uchwyt.

Czas na klasę managera:

template < typename TypUchwytu, typename TypPrzechowywany, typename TypWejscia> class IManager { protected: bool Zaladuj(TypWejscia & Nazwa, TypPrzechowywany * Dane); void Usun(TypPrzechowywany Dane);

Idea jest następująca: tworzymy klasę parametryczną IManager, zawierającą wszystko oprócz dwóch metod: zaladuj() i usun() - będzie się je definiować później. Parametry szablonu to: uchwyty, jakich używamy dla tej kopii managera (np. HTekstura), typ, jaki będzie przechowywany (np. LPDIRECT3DTEXTURE9) oraz typ parametru używanego do wczytywania i identyfikowania danych (np. std::string - nie char*! - jako nazwa pliku). Tworząc np. manager tekstur wystarczy odziedziczyć klasę IManager< HTekstura, LPDIRECT3DTEXTURE9, std::string> i dodać w niej metody: bool Zaladuj(std::string & Nazwa, LPDIRECT3DTEXTURE9 * Dane) (zwracającą informację o powodzeniu lub klęsce wczytywania) i void Usun(LPDIRECT3DTEXTURE9 Dane) usuwającą niepotrzebne zasoby, by stworzyć manager tekstur. Reszta klasy IManager zajmie się pozostałą robotą.

Czas na resztę:

private: struct ElementTablicy { TypPrzechowywany Dane; unsigned short LicznikReferencji; TypWejscia Nazwa; }; std::vector< ElementTablicy> PrzechowywaneDane;

W wektorze PrzechowywaneDane zawarte będą wczytane zasoby wraz z ilością referencji do nich i kopią parametrów użytych do wczytania - dzięki temu, zanim manager wczyta dane z dysku, będzie mógł sprawdzić, czy aby nie przechowuje już w pamięci żądanego zasobu.

std::vector< unsigned short> WolneMiejsca; unsigned short ZnajdzMiejsce() { if (WolneMiejsca.size() > 0) { unsigned short Indeks = WolneMiejsca.back(); WolneMiejsca.pop_back(); return Indeks; } else { ElementTablicy el; PrzechowywaneDane.push_back(el); return PrzechowywaneDane.size()-1; } }

Jeśli jakiś zasób zostanie ostatecznie usunięty, to żeby nie robić gulaszu z wektora, usuwając dane ze środka, informacja o pustym miejscu zostaje dopchnięta do wektora WolneMiejsca. Aby więc uprościć znajdowanie miejsca dla nowych zasobów, dodałem funkcję ZnajdzMiejsce() - jeśli są jakieś znane wolne miejsca to są one użyte, jeśli nie, dodawane jest nowe w wektorze z danymi.

public: TypUchwytu WczytajZParametrow(TypWejscia & Wejscie) { for (unsigned short i = 0; i < PrzechowywaneDane.size(); i++) { if (PrzechowywaneDane[i].Nazwa == Wejscie) { ++PrzechowywaneDane[i].LicznikReferencji; TypUchwytu u; u.Ustaw(i); return u; } } TypPrzechowywany Dane; if (!Zaladuj(Wejscie, &Dane)) { TypUchwytu u; u.UstawNaNiepoprawny(); return u; } unsigned short Indeks = ZnajdzMiejsce(); PrzechowywaneDane[Indeks].Dane = Dane; PrzechowywaneDane[Indeks].LicznikReferencji = 1; PrzechowywaneDane[Indeks].Nazwa = Wejscie; TypUchwytu u; u.Ustaw(Indeks); return u; }

Metoda WczytajZParametrow() jest fasadową metodą ładującą dane. Najpierw sprawdza czy nie posiada aby takiego samego zasobu już w pamięci. Jeśli nie, to próbuje wczytać dane na podstawie otrzymanych parametrów, znajduje dla nich wolne miejsce, ustawia ilość referencji na 1 i zwraca uchwyt (w razie błędu przy wczytywaniu zwraca uchwyt niepoprawny).

Porównywanie służące szukaniu wczytanych zasobów może wymagać zdefiniowania własnego operatora ==. Jest to nieco niewygodne, ale dzięki temu możemy wymusić każdorazowe ładowanie zasobu od nowa - wystarczy żeby nasz operator zawsze zwracał fałsz.

// tutaj sie zaklada ze dostarczane dane sa juz w porzadku TypUchwytu WczytajDane(TypPrzechowywany & Dane) { unsigned short Indeks = ZnajdzMiejsce(); PrzechowywaneDane[Indeks].Dane = Dane; PrzechowywaneDane[Indeks].LicznikReferencji = 1; #ifdef _DEBUG memset( (void*)&PrzechowywaneDane[Indeks].Nazwa, 0xCC, sizeof(TypWejscia) ); #endif TypUchwytu u; u.Ustaw(Indeks); return u; }

To dodatkowa metoda do wczytywania już istniejących danych. Tracimy wtedy całkowicie możliwość unikania duplikacji zasobów, ale czasem jest to zbędne. W trybie debug dodatkowo pole wykorzystywane na przechowywanie kopii parametrów jest wypełniane 0xCC - może być przydatne.

void Zwolnij(TypUchwytu & Uchwyt) { assert(Uchwyt.Poprawny(), "Zwolnienie niepoprawnego uchwytu"); assert( PrzechowywaneDane.size(), "Próba usunięcia zasobu z pustej tablicy"); if ( (--PrzechowywaneDane[Uchwyt.Indeks].LicznikReferencji) == 0 ) { Usun( PrzechowywaneDane[Uchwyt.Indeks].Dane ); WolneMiejsca.push_back(Uchwyt.Indeks); } Uchwyt.UstawNaNiepoprawny(); }

Ta metoda zwalnia dane, po upewnieniu się że dostała poprawny uchwyt. Jeśli po zmniejszeniu ilość referencji do zasobu jest równa 0, to jest on usuwany metodą Usun(), a jego miejsce oznaczone jako wolne. Na koniec uchwyt jest psuty, by nie został ponownie użyty.

TypPrzechowywany & Pobierz(TypUchwytu Uchwyt) { assert(Uchwyt,Poprawny(), "Próba dereferencji niepoprawnego uchwytu"); return PrzechowywaneDane[Uchwyt.Indeks].Dane; }

To chyba mówi samo za siebie :)

void ZwolnijPozostale() { while (PrzechowywaneDane.size()) { if (PrzechowywaneDane.back().LicznikReferencji == 0) { PrzechowywaneDane.pop_back(); continue; } Usun(PrzechowywaneDane.back().Dane); PrzechowywaneDane.pop_back(); } }

Ta metoda sprząta po całej klasie, usuwając niezwolnione dane, które zostały do końca programu.

void Wstrzymaj() { for (unsigned int i = 0; i < PrzechowywaneDane.size(); i++) Usun(PrzechowywaneDane[i].Dane); } bool Wznow() { for (unsigned int i = 0; i < PrzechowywaneDane.size(); i++) { bool res = Zaladuj(PrzechowywaneDane[i].Nazwa, &PrzechowywaneDane[i].Dane); if (!res) { // obsluz blad } } }

Te dwie metody służą do zwalniania i przywracania zasobów w miarę potrzeby. Nie można z tego korzystać, jeśli "ręcznie" dodawaliśmy zasoby - można sobie z tym oczywiście poradzić, ale wymaga to większej integracji z systemem odpowiedzialnym za wczytanie zasobu.

Użycie tak napisanego szablonu jest niezwykle proste. Oto kompletny przykład:

// używany typ uchwytów struct TexID { }; typedef Uchwyt< TexID> HTekstura; class CManagerTekstur : public IManager< HTekstura, LPDIRECT3DTEXTURE9, std::string> { private: LPDIRECT3DDEVICE9 m_d3ddev; public: CManagerTekstur(void); ~CManagerTekstur(void); // nagłówki funkcji abstrakcyjnych - w tym przypadku nie musimy dodawać nic innego bool Zaladuj(std::string Sciezka, LPDIRECT3DTEXTURE9 * Dane); void Usun(LPDIRECT3DTEXTURE9 & Dane); }; bool CManagerTekstur::Zaladuj(std::string Sciezka, LPDIRECT3DTEXTURE9 * Dane) { HRESULT res = D3DXCreateTextureFromFile(m_d3ddev, Sciezka, Dane); return (SUCCEEDED(res)); } void CManagerTekstur::Usun(LPDIRECT3DTEXTURE9 & Dane) { Dane->Release(); }

Kod jest oczywiście dostępny i zachęcam do jego używania :) Uwagi końcowe: użyłem własnego makra assert, stąd 2 parametry. Idea uchwytów w tej formie jest natchniona "Perełkami...", jak również część innych rozwiązań użytych w IManagerze. Resztę wymyśliłem ja :)

Pozdrawiam

MoN

Dołączony plik: manager.h (3.4 kB)
Niestety plik gdzieś zaginął w akcji. Ktokolwiek go widział, proszę o kontakt z redakcją.

Tekst dodał:
Adam Sawicki
09.08.2006 03:51

Ostatnia edycja:
Adam Sawicki
09.08.2006 03:51

Kategorie:

Aby edytować tekst, musisz się zalogować.

# Edytuj Porównaj Czas Autor Rozmiar
#1 edytuj 09.08.2006 03:51 Adam Sawicki 11.39 KB
Zwykły
Do sprawdzenia
Do akceptacji
  • SirMike (@SirMike) 15 sierpnia 2008 19:40
    O ile temat jest bardzo ciekawy to sposob w jaki przedstawia to artykul juz nie. Nic z niego nie wiadomo i jesli ktos nie czytal "Perelek" to kompletnie nic nie zajarzy.
  • Napisz komentarz:
    Aby dodać swój komentarz, musisz się zalogować.
Licencja Creative Commons

Warsztat używa plików cookies. | Copyright © 2006-2017 Warsztat · Kontakt · Regulamin i polityka prywatności
build #ff080b4740 (Tue Mar 25 11:39:28 CET 2014)