piątek, 7 sierpnia 2009

Resource Manager


Witam, witam, witam jestem Higgold, wielki, znany w calym Faerunie rezyser. Jestem po prostu maniakiem BG2 i Fallouta. Nigdy nie przeszlem BG2 do konca i musze skonczyc ta gre bo mi nie da zyc tak jak Cywilizacja4.

Dobra mial byc manager obiektów, mamy widze maly poslizg, ale w koncu to nadrobimy. Musze o tym napisac bo ciagle zapominam dlaczego to tak wlasnie dziala. Art jest streszczeniem rozdzialu „The Beauty of Weak References and Null Objects” z perelek chyba 5-tych. Perelki lubie za takie wlasnie rozdzialy o programowaniu ogólnym, o rozwiazaniach programistycznych, których was nie naucza na studiach, pierdole studia, to byla kupa.

Po pierwsze manager obiektów to kontener wskazników do obiektów, który trzyma obiekty, udostepnia je po unikalnym kluczu(String , IDek, Guid) i zarzadza nimi, dzieki czemu mamy w jednym miejscu kod obslugujacy obiekty(tworzenie, uaktualnianie, zwalnianie etc..) no i nasze obiekty.

Moze wystepowac w róznych rolach:
- Manager pluginów – musi wtedy byc przygotowany na obsluge typów obiektów, które nie sa mu znane w momencie kompilacji. W perelkach to jest kolejny rozdzial, moze sie kiedys o tym napisze.

- Manager obiektów bazy danych – czyli w momencie startu aplikacji(serwera, ten manager siedzi na serwerze i posredniczy miedzy klientem a baza) laduje wszystkie obiekty z bazy tworzac odpowiadajace im interfejsy i dba o ich uaktualnianie, tworzenie nowych obiektów, usuwanie istniejacych. Dzieki temu klient proszac o
interfejs do obiektu dostaje aktualny z managera, bez potrzeby ladowania z bazy co by bylo czasochlonne(utwórz polaczenie, wykonaj query etc).

- Prosty manager po stronie klienta. Po prostu chcemy trzymac wszystkie obiekty np. gry w kontenerku, który bedac singletonem umozliwia nam dostep do obiektu z dowolnego miejsca w kodzie po kluczu np.: nazwie. Obiekty mozemy np.; zdefiniowac sobie w XML-ku lub jakims skrypcie. Nawiasem mówiac wszystkie managery to singletony.

- Manager zasobów – o i oto nam sie rozchodzi, po to jest ten art. A wiec czy nie móglby to byc ten manager poprzedni, czyli bezposrednio pointery do obiektów zasobów? Jest problem, te pointery moga przestac byc aktualne i wtedy w kodzie mamy odwolanie do Null-a lub do nieaktualnego adresu alc. Leo why? Jesli mamy doczynienia z obiektami graficznymi i nastapi uniewaznienie kontekstu graficznego(Hdc), lub bedziemy manipulowac oknem renderingu(directx), lub stracimy polaczenie sieciowe, z baza etc to nasze obiekty przestaja sie nadawac do czegokolwiek i trzeba je utworzyc od nowa dla nowego kontekstu etc.

Sa dwa rozwiazania problemu:
1) Trzymamy idki do obiektów i za kazdym razem odwolujemy sie do managera zeby dał nam aktualny obiekt przez idka, uchwyt. To jest dobre acz nienaturalne podejscie, i troche kosztuje nas szukanie w mapie obiektu( kilka tysiecy obiektów na klatke, jakis algorytm n kwadrat i jest problem). Ale na domowe programowanie zupelnie wystarcza.

2) Trzymamy obiekt opakowujacy pointer do zasobu(smart pointer) nazwijmy go ResPtr, który trzyma obiekt przechowujacy wskaznik PtrHolder, który to opakowuje nasz wskaznik, który sie moze zmieniac do woli. Kontener managera przechowuje obiekty PtrHolder, a te z kolei opakowane sa przez ResPtr(smart pointer). Po co nam jeszcze ten ResPtr, nie moglibysmy uzywac bezposrednio PtrHoldera? Otóz chcemy uniknac sytuacji, w której uzytkownik zrobi jakas glupia rzecz z bezposrednim wpisem w mapie, bedzie próbowal usunac przez delete itd., oczywiscie mozemy sie chronic robiac protected i frienda do managera w klasie PtrHolder. Otóz manager potrzebuje tylko prostej logiki opakowujacej pointer, natomiast klient potrzebuje czegos wiecej,
klasa ResPtr posiada przeciazone operatory ->, *, pozwala na wymienianie PtrHoldera trzymanego w mapie na inny w czytelny sposób, bezpieczny sposób. Mozemy tez potrzebowac specjalizowac szablon ResPtr-a dla konkretnych typów. Jak widac tej logiki troche jest.

Na poczatek pierwsze rozwiazanie, czyli dostajemy obiekty przez idki.



//prosty manager trzymajacy obiekty w mapie
template <class T>
class DLLEXPORT BasicManager
{
protected:
std::map<std::string, long> mapNameToID;
std::map<long, T*> mapObject;
public:

//wrzuc do mapy i zwróc id
long AddObject(std::string _name, T* _object);
void Remove(std::string _name);
long GetID(std::string _name);
T* GetObject(std::string _name);
T* GetObject(long _id);
void RestoreAll(){};
void Clear();
};



Oczywiscie wszystkie managery stoja na szablonach, aczkolwiek moglibysmy uniknac tego trzymajac pointery do wspólnej klasy bazowej, potem trzeba by rzutowac na wlasciwy typ, mozna by dodac tylko jedna funkcje szablonowa T* GetObject(long id) i byloby ok. Nie ma tutaj o czym pisac, wyszukiwanie po long jest duuuzo szybsze niz po String, ale czasami w inicjalizacji wygodniej uzyc stringa, natomiast w reszcie kodu uzywamy longa.

Rozwiazanie drugie:
Na poczatek PtrHolder, czyli proste opakowanie wskaznika. Ale zesmy nafrendowali, po prostu nie chcemy zeby user zrobil z ta klasa cokolwiek, to jest klasa dla managera.



template <class T>
class DLLEXPORT PtrHolder
{
template <class T>
friend class BaseManager;
template <class T>
friend class FileManager;
template <class T>
friend class ResPtr;
protected:
T* pointer;
~PtrHolder(){RemovePointer();}
void RemovePointer()
{
if(pointer)
delete pointer;
pointer = NULL;
}
PtrHolder(T* _ptr) {pointer = _ptr; }

T* GetPointer(){return pointer; }
};



Teraz nasz smart pointer ResPtr uzywany przez klientów. Dzieki przeciazonym operatorom mozemy uzywac tego obiektu tak samo jak wskaznika.



template <class ResType>
class DLLEXPORT ResPtr
{
protected:
PtrHolder<ResType>* ptr;
public:
ResPtr(){ptr = NULL; }
ResPtr(PtrHolder<ResType>* _ptr) { ptr = _ptr; }

ResType* operator->(){return ptr ? ptr->GetPointer() : 0; }

ResType& operator*(){return *(ptr->GetPointer());}

void SetPtr(PtrHolder<ResType>* _ptr) {ptr = _ptr;}

ResType* GetPtr(){ return ptr ? ptr->GetPointer() : 0; }
};



Przyszedl czas na managery. Zrobilem bazowa.



//bazowy manager, trzeba wyprowadzac klasy pochodne i pisac ladowanie zasobu, zwracamy wrapper do zasobu i nic wiecej.
template <class T>
class DLLEXPORT BaseManager
{
protected:
PtrHolder<T>* defaultObject;
std::map<std::string, pad::PtrHolder<T>* > mapRemovedHolders;
std::map<std::string, pad::PtrHolder<T>* > mapObject;
public:
~BaseManager();
bool AddObject(std::string _name, T* _object, ResPtr<T>& _outPtr);
virtual bool GetObject(std::string _name, ResPtr<T>& _outPtr);
virtual bool Restore(std::string _name);
void GetDefaultObject(ResPtr<T>& outPtr);
void SetDefaultObject(T* _object);
void RestoreAll();
void Dispose(std::string _name);
void DisposeAll();
void Remove(std::string _name);
void Clear();
};

//bazowy manager dla zasobów plikowych
template <class Res>
class DLLEXPORT FileManager : public BaseManager<Res>
{
protected:
std::string path;
virtual bool LoadRes(std::string _fileName, Res** _res);
public:
FileManager(std::string _folderPath);
~FileManager(){};
void SetResFolderPath(std::string _path);
virtual bool GetObject(std::string _name, ResPtr<Res>& _outPtr);
virtual bool Restore(std::string _name);
};



O a tu juz mamy klase której uzywam w kodzie, jakis manager do obslugi bitmap, przeslaniam metode ladowania tego konkretnego zasobu(LoadRes), móglbym jeszcze w imoplementacji obsluzyc rózne typy tekstur na podstawie rozszerzen np.: jpg, png. Singleton w postaci statycznej metody i statycznego pola w metodzie, a nie jako membera, co powoduje bklopoty z inicjalizacja tego membera w róznych przestrzeniach adresowych(dll-ka a exe), no i jakas konkretna dla tego managera metoda do tworzenia recznego teksturki.



class DLLEXPORT Image2DMgr : public FileManager<Image2D>
{
protected:
bool LoadRes(std::string _fileName, Image2D** _image);
Image2DMgr();
public:
static Image2DMgr* GetInst();
Image2D* CreateImage(std::string name, int width, int height);
};


Podobnie mam zrobiony shader manager.


class DLLEXPORT ShaderMgr : public FileManager<Shader>
{
protected:
BasicManager<ShaderProgram> m_Programs;
bool LoadRes(std::string _fileName, Shader** _shader);
ShaderMgr();
public:
static ShaderMgr* GetInst();
bool CreateShader(std::string _name, std::string _program, EShader _type, PadPtr<Shader>& _res);
};


A juz manager programów nie dziedziczy po FileManager, bo obsluguje nie plikowe zasoby.


class DLLEXPORT ProgramMgr : public BaseManager<ShaderProgram>
{
protected:
ProgramMgr();
public:
static ProgramMgr* GetInst();
bool CreateShaderProgram(std::string _name, std::string _vertexShader, std::string _pixelShader, PadPtr<ShaderProgram>& _outPtr);
};



No i jak uzyc tego w kodzie:


void QuadricScene::Init(int _width, int _height)
{
MyScene::Init(_width, _height);

ResPtr<Shader> shaderVert;
ResPtr<Shader> shaderFrag;
ResPtr<ShaderProgram> program;

ShaderMgr::GetInst()->SetResFolderPath("D://media//shader//");
ShaderMgr::GetInst()->GetObject("diffuse.vert", shaderVert);
ShaderMgr::GetInst()->GetObject("diffuse.frag", shaderFrag);
ProgramMgr::GetInst()->CreateShaderProgram("program0", "diffuse.vert", "diffuse.frag", program);

ShaderMgr::GetInst()->GetObject("PerPixel.vert", shaderVert);
ShaderMgr::GetInst()->GetObject("PerPixel.frag", shaderFrag);
ProgramMgr::GetInst()->CreateShaderProgram("program1", "PerPixel.vert", "PerPixel.frag", program);


sphereGeom = new GeomSphereQuadric(2, 20, 20);
cylinderGeom = new GeomCylinderQuadric(2, 2, 2, 20, 20);
coneGeom = new GeomCylinderQuadric(0, 3, 2, 20, 20);
discGeom = new GeomDiskQuadric(1, 3, 30, 30);
partialDiscGeom = new GeomCylinderQuadric(0, 3, 2, 50, 50);

meshSphere = new Mesh(sphereGeom);
meshCylinder = new Mesh(cylinderGeom);
meshCone = new Mesh(coneGeom);
meshDisc = new Mesh(discGeom);
meshPartialDisc = new Mesh(partialDiscGeom);
meshSphere2 = new Mesh(sphereGeom);
meshCylinder2 = new Mesh(cylinderGeom);

meshSphere->SetPosition( 5, 1, -3);
meshCylinder->SetPosition( 5, 1, -10);
meshSphere2->SetPosition( 12, 1, -3);
meshCylinder2->SetPosition( 12, 1, -10);
meshCone->SetPosition( 17, 1, -3);
meshDisc->SetPosition( 17, 1, -10);
meshPartialDisc->SetPosition( 17, 1, -15);

meshCylinder->GetMaterial().SetBase("Copper");
meshCylinder->GetMaterial().SetShaderProgram("program1");
meshCylinder2->GetMaterial().SetBase("Copper");
meshCylinder2->GetMaterial().SetShaderProgram("program1");

meshCone->GetMaterial().SetBase("PolishedGold");

meshDisc->GetMaterial().SetBase("Ruby");
meshDisc->GetMaterial().SetShaderProgram("program1");
meshPartialDisc->GetMaterial().SetBase("Ruby");
meshPartialDisc->GetMaterial().SetShaderProgram("program1");

meshSphere->GetMaterial().SetBase("Ruby");
meshSphere->GetMaterial().SetShaderProgram("program0");
meshSphere2->GetMaterial().SetBase("Ruby");
meshSphere2->GetMaterial().SetShaderProgram("program1");

meshCone->Rotate(90, 0, 0);
meshPartialDisc->Rotate(90, 0, 0);

scene3D->AddObject(meshSphere);
scene3D->AddObject(meshCylinder);
scene3D->AddObject(meshSphere2);
scene3D->AddObject(meshCylinder2);
scene3D->AddObject(meshCone);
scene3D->AddObject(meshDisc);
scene3D->AddObject(meshPartialDisc);
}



Zostala ostatnia kwestia, napisalem metody w managerku GetDefaultObject, SetDefaultObject. Jesli dostajemy z managera wskaznik do zasobu, to trzeba by sprawdzic czy jest NULL i dopiero wtedy bezpiecznie uzyc go, fuck, pieprzone NULLe.
Moga sie nam te NULLe pojawic, nie pytajcie jak, takie jest zycie, jak uniknac obslugi tych nieszczesnych przypadków? Otóz zapewniajac jakis defaultowy zasób np.: dla bitmap moze to byc bitmapa 1x1 w bialym kolorze, dla dzwieków, 1 sekundowy
pusty dzwiek lub cokolwiek. Cos co nie zalamie programu, trzeba utworzyc taki zasób i wrzucic go na samym poczatku do managera, np. w konstruktorze przez SetDefaultObject. Trzeba jeszcze wyspecjalizowac nasze ResPtr-y dla takich zasobów, czyli operatory wyluskania.



//specjalizacja operatorów dla zasobu typu Text
template<>
Text* ResPtr<Text>::operator->()
{
return ptr && ptr->GetPointer() ? ptr->GetPointer() : TextManager::GetInst()->GetDefaultRes();
};

template<>
Text& ResPtr<Text>::operator*()
{
return ptr && ptr->GetPointer() ? *ptr->GetPointer() : *TextManager::GetInst()->GetDefaultRes();
};


Dziekuje i do uslyszenia.

Brak komentarzy: