[C++]RTTI

0

W moim programie stosuje polimorfizm. Wywoływane są różne funkcje różnych klas poprzez jedną tablicę Obiektów CObject. Mam jednak kilka klas, które mają dane typu życia itp. Umieściłęm więc te obiekty w klasie i dałem wskaźnik do tych obiektów CObject ( normalnie korzystam z new ). No ale ten sposób wymaga kilakkrotnego powtarzania prawie identycznych fragmentów kodu w kolizji. Prostym sposobem rozwiązania tego problemu jest RTTI ( rzutuje w dół ). Ale może jest szybszy sposób? dynamic_cast jest stosunkowo dość powolny. Co zrobić?

PS: Jak wygląda to w większych projektach? Jak wykonuje się operacje na zmiennych danej klasy pochodnej, które nie występują w klasie podstawowej, bo są specyficzne tylko dla jednego, czy kilku obiektów?

0

Analiza <-> projekt <-> implementacja
Lepiej najpierw dobrze zaprojektować klasy (ze zwróceniem uwagi na to CO chce się osiągnąc i gdzie mogą pojawić się zmiany/rozszerzenia) niż później się męczyć...

PS: Jak wygląda to w większych projektach? Jak wykonuje się operacje na zmiennych danej klasy pochodnej, które nie występują w klasie podstawowej, bo są specyficzne tylko dla jednego, czy kilku obiektów?

Ale o co Ci chodzi?

Operujesz na zmiennej klasy pochodnej przez wskaźnik/referencję do klasy bazowej, czyli sprawdzasz typ i robisz downcast?? Błąd projektu!

0

Mam klasę CObject po której dziedziczą inne klasy ( np. CMonster ). Wczytuje dane z plików i zapisuje do tablicy objects ( typu CObject ). W zależności od typu używam. objects[i] = new CObject, object[i] = new CMonster, itp. W czasie działąnia programu wywołuje funkcje animate tablicy objects ( polimorfizm ustala czy wyołać CObject::Animate, czy CMonster::Animate. ). Sprawdza kolizje, itp. No ale, gdy wiem, że wystąpiła kolizja np. strzały z obiektem CMonster, to muszę jakoś zmniejszyć punkty życia potwora. Mam do dyspozycji tylko element tablicy objects ( typu CObject po którym dziedziczy CMonster ). Nie chce umieszczać żyć i tego typu rzeczy w klasie CObject, bo z tego korzysta tylko kilka obiektów. Byłoby by to marnotrawienie miejsca. Do tej pory dokładnie sprawdzałem stan zmiennej type i na tej podstawie zmniejszałem punkty życia obiektów CMonster zapisanych w tablicy wskaźników od których były wskaźniki do ytanblicy objects. Teraz znając typ rzutuje na typ np. CMonster i moge zmniejszać hp, a kodu jest trzy razy mniej.

0

A słyszałeś o interfejsach? Po to się je projektuje, żeby rodzina klas mogła realizować to samo zachowanie ale nie znaczy, że tak samo...

class CObject {
public:
    virtual HitMe() {}
};

class CMonster : public CObject {
    int points;
public:
    HitMe() { points--; }
};
0

Przy czymś takim to jest proste, ale co zrobić kiedy chce kolizje, naciśniećie myszką zrobić dla każdego obiektu jedną pętlą, a obiekty różnią częściowo. Musiałby stworzyć z 50 funkcji wirtualnych, a to chyba nie jest najlepsze rozwiązanie ...

0

Może troszke za mało opisałem.
Fragent klasy CObject:

class CObject{
protected:
...
public:
virtual void Draw();
virtual void Animate();
virtual void Init();
virtual void Load(ifstream& plik, int i);
...
};

Obiekty klasy CObject i klas od niej pochodnych zapisuje w tablicy CObject* object[60].
W funkcji render wywołuje funkcje virtualne Draw obiektów, a w funkcji Colizion rozpatruje kolizje i wywołuje funkcje virtualne animate obiektów. ( wszystko w oparciu o tablice CObject* object[60] ).
Dzięki temu cześć obiektów będzie nieruchomych ( void Animate(){} ) inne będą miały złożone funkcje animate ( CPlayer np. - związek przyczynowo-skutkowy naciśnięcia klawiszu, CMonster np. - SI, oba np.- poruszanie się i walka ).

Teraz muszę jakoś rozpatrzyć specyficzne kolizje ( gracza/stwora z mieczem - naciśnięcia klawisza/działanie SI, zmienia texture gracza/stwora na atakującego i tworzony jest niewidzialny obiekt, z którym można "kolidować" ). Muszę jakoś zmniejszyć punkty życia obiektów CMonster lub CPlayer. Nie chcę wstawiać funkcji virtualnej do klasy CObject, bo wtedy funkcja będzie strasznie zapchana ( będą dochodziły nowe fumkcje i nowe, aż będzie ich z 50 ). Oto moje dwa pomysły na rozwiązanie problemu żyć.

Sposób I ( stary ).
Wczytywanie:

if(name==3){
        control->object = new CPlayer; // control jest typu CControlMove (np.kolizje)
        layer->objects[i] = control->object;
        layer->objects[i]->Init();
        layer->objects[i]->Load(plik,i);
}
...

Kolizja:

void CControlMove::Colizion(CObject* obiekt1, CObject* obiekt2){

if(obiekt1->desc->type==4 && obiekt2->desc->type==3 ){
obiekt1->enabled=false;
player->hp-=1;  // obiekt 2   
}

....

Około 4,5 takich instrukcji warunkowych ( musze rozpatrzyć przypadek gracza i przypadek stwora oraz to, że obiekt1 może być graczem/stworem, albo obiekt 2 ).

Sposób II ( Nowy, z wykorzystaniem mechanizmu RTTI )

Wczytywanie odbywa się normalnie: object[i] = new CPlayer ( bez tych różnych kombinacji ).

Kolizje [ wprowadziłem typ CInfo ( o1, o2 ) posiadający paramtery takie jak hp, itp. Klasa ta dziedziczy po CObject, a po niej dziedziczą CPlayer i CMonster ]:

void CControlMove::Colizion(CObject* obiekt1, CObject* obiekt2){

if(obiekt1->desc->type==4 && (obiekt2->desc->type==3 ||obiekt2->desc->type==2 )){
 o1 = dynamic_cast<CInfo*>(obiekt2);
 o2 = obiekt1;
 DoColizion();
}
if(obiekt2->desc->type==4 && (obiekt1->desc->type==3 ||obiekt2->desc->type==2  )){
 o1 = dynamic_cast<CInfo*>(obiekt1);
 o2 = obiekt2;
 DoColizion();
}

}

void CControlMove::DoColizion(){
 o2->enabled=false;
 o1->hp -= o2->bonus->damage;
 if(o1->hp<1){
  o1->enabled=false;
  o1->enabledDraw=false;
 }
}

Jestem raczej początkującym programistą - ucze się głównie z książek. Nie wiem jak powinny wyglądać prawdziwe projekty. Jeśli moglibyście pomóc mi w znalezieniu lepszej konstrukcji programu/enginu to byłbym bardzo wdzięczny.

PS: Pokazałem tu tylko najważniejsze części programu. Oczywiście wszystko jest bardziej rozwinięte.

0

[Jeżeli jeszcze jesteś zainteresowany...]

Posłuchaj, pozbywasz się całej informacji o typie zmiennej poprzez to, że wrzucasz wszystkie obiekty gry do jednego worka - zostaje Ci jedynie CObject, a Ty męczysz się później z typowaniem tego...

Ponieważ piszesz program sam to nie musisz używać takich masakrycznych sposobów, szczególnie że to jest bardzo prosty problem.

Masz źle zaprojektowanę hierachię klas, to po pierwsze; po drugie do tego dochodzi to co się DZIEJE w programie, a tu już logika działania zależy od przeznaczenia programu. U ciebie też nie jest to zbyt dobrze zrobione w odniesieniu do klas jakie masz w projekcie.

Obiekt (raczej) powinien w pełni być odpowiedzialny za swoje zachowanie, i wyrażać je poprzez metody. Nikt nie karze Ci umieścić całej logiki działania wszystkich obiektów w klasie CObject!

Proponuję rozdzielić nieco klasy tematycznie (np. CPlayer; CItem<-CBox,CSword,...;CStill<-CWall,CDoor,.. itd.), i zaprojektować powiązania między klasami (lub rodzinami, np. CPlayer między CMonster, CItem, itd. ale już CItem raczej nie ma wiele wspólnego ze ścianą... zastanów się nad tym).

0

Kod jest moim zdaniem nie jest aż tak źle zaprojektowany, a Ty stanąłeś po prostu przed problemem wielowyboru ^^. Przyznam, nie ma jednego dobrego rozwiązania w tej sytuacji. RTTI w grach zazwyczaj się nie używa, gdyż jest za wolne; kiedy pojawia się konieczność jego użycia, to albo przeprojektowuje się wszystko (ale to zły znak, jeżeli trzeba coś przeprojektować już podczas pisania), albo robi się własne. W tym drugim przypadku nie musi to być nic trudniejszego, niż jakaś cyfra, ale lepiej zrobić identyfikację typu po nazwie. Ale po kolei:

MarcinEC, napisałeś: "Posłuchaj, pozbywasz się całej informacji o typie zmiennej poprzez to, że wrzucasz wszystkie obiekty gry do jednego worka - zostaje Ci jedynie CObject, a Ty męczysz się później z typowaniem tego...". Tak się akurat składa, że robi się tak [prawie] zawsze. Tj. przechowujesz listę na wskaźniki do CObject, i w pętli głównej, tudzież innych miejscach gry, robisz update za pomocą funkcji wirtualnych. Oczywiście, w niektórych miejscach warto też prowadzić inne, równoległe listy - np. lista wskaźników na CItem, żeby sprawdzać tylko te obiekty, czy gracz może je podnieść. Generalnie jednak największa, główna lista powinna zawierać obiekty, które mogą indywidualnie przemieszczać się po świecie gry, czyli CObject, CActor, czy jak się tam je nazwie. Chodzi o to, żeby udostępniały jakiś prosty interfejs do uaktualniania fizyki i rysowania.

Teraz musisz dokładnie przemyśleć hierarchię klas w swojej grze (czy co tam piszesz ^^) - np. klasy CPlayer, CMonster, CFlyingMonster, itp. to klasy istot obdarzonych jakąś tam inteligencją, która pozwala im na chodzenie, podnoszenie przedmiotów, atakowanie, etc. [przy czym CPlayer jest oczywiście kierowany przez gracza) - już masz dobre miejsce na wspólną klasę bazową, np. CPawn (pawn - ang. pionek w szachach). Z tej klasy będą dziedziczyć się wszystkie postacie. Wszystkie przedmioty dziedziczyć będą po CItem, jakieś kamienie, etc. po CDecoration, a wszystko to razem po CActor / CObject [jak tam sobie nazwiesz]. Teraz co to dało - załóżmy, że chcesz zaimplementować sprawdzanie zderzenia pomiędzy CPlayer a CACID - gdzie ta druga klasa to bajoro z kwasem ^^. Wiesz, że kwas zadaje uszkodzenia wszystkim postaciom. Z użyciem własnego RTTI mogłoby to wyglądać tak:

void CACID::Kolizja(CActor * z_czym)
{
       if(z_czym.IsA("Pawn"))
       {
              z_czym.ZadajUszkodzenia(this, USZKODZENIE_POPARZENIE, ile_uszkodzen);
       }
}

Idea tego jest taka, że sprawdzasz, czy przekazany obiekt jest postacią (potworem, graczem). Jeżeli tak, to zadajesz mu uszkodzenia (funkcja wirtualna z klasy CActor). Oczywiście zostanie też wywołana prawdopodobnie druga funkcja - CPlayer::Kolizja, z przekazanym do niej bajorkiem z kwasem, ale ta funkcja już nie musi sprawdzać, czy obiekt jest bajorkiem [w tym miejscu wkracza dobry projekt!!]. Tak na marginesie dodam, że taki właśnie kod steruje grą Unreal Tournament ^^. Jak działa takie RTTI? Masz następującą funkcję: bool IsA(std::string nazwa);. Jeżeli obiekt jest klasą o podanej nazwie, lub dziedziczy po czymś o tej nazwie (np. CACID dziedziczy po CDecoration, która dziedziczy po CActor), to zwraca prawdę. W przeciwnym wypadku zwraca fałsz. Jeszcze się wytłumaczę z tej funkcji ZadajUszkodzenia - dla przykładu przyjmuje ona trzy parametry - wskaźnik do obiektu, który zadał uszkodzenia (CActor*), typ uszkodzeń (int, stała tekstowa, jak tam chcesz), oraz ile uszkodzeń zadano (int, float). Jakbyś chciał mieć potwora odpornego na ataki gracza, to sprawdzasz w jego implementacji tej funkcji

void CIndestructableMonster::ZadajUszkodzenia(CActor* od_kogo, int typ, int ile)
{
         if(od_kogo.IsA("Player"))
         {
                return;     //nie zadajemy uszkodzen
         }
         //...
}

W innym przypadku będziesz chciał, żeby potwór był odporny na kwas; to piszesz linijkę:

if(typ == USZKODZENIE_KWAS) return;

I tak dalej. Wszystko zależy od dobrego projektu. RTTI używasz własnego, i tylko po to, żeby sprawdzić, czy dany obiekt jest graczem, potworem, etc. NIE używasz go natomiast przy rysowaniu na ekran, czy sprawdzaniu fizyki - od tego masz klasę podstawową CActor, funkcje wirtualne, i garść zmiennych - każde RTTI jest zbyt wolne, żeby się go opłacało wywoływać co klatkę x razy dla wszystkich obiektów. Jeśli wydaje się, że jednak trzeba, to przeprojektuj ^^.

Teraz - jak napisać takie RTTI? No, z tym jest więcej zabawy. Dobry kod był w II tomie Perełek Programowania Gier (jak będziesz w pobliżu księgarni, to przeglądnij); wymaga lekkich modyfikacji i okrojenia. Nie wiem, czy nie zamieszczę przerobionej wersji gdzieś na sieci, ale jak chcesz, to Ci mogę wysłać mailem.

Ok, rozpisałem się, mam nadzieje, że chociaż zrozumiale :)
Pozdrawiam wszystkich,
TeMPOraL

0

Dobra, piszę drugiego posta, bo widzę, że starego jakoś tak dziwnie wysłało :) Zamieszczę kod przykładowej klasy DTI (Dynamic Type Information), trochę poobcinany z komentarzy. Potrzebujesz dwóch plików - DTI.h:

#include <string>

class CDTIClass
{
private:
	std::string name;	//class name
	CDTIClass * parentClass;	//parent class
	
public:

	CDTIClass();	//default constructor
	CDTIClass( std::string newName, CDTIClass* newParent );	//constructor
	virtual ~CDTIClass();	//destructor

	std::string GetName();	//get class name
	void SetName( std::string newName );	//set class name

	CDTIClass* GetParent();	//get parent class
	void SetParent( CDTIClass* newParent );	//set new parent class

	bool IsA( std::string testedName );	//is this class a...

};

//expose type to DTI
#define EXPOSE_TYPE \
	public: \
		static CDTIClass Class;
//typedef superior class as 'super'
#define DECLARE_SUPER(SuperClass) \
	public: \
		typedef SuperClass super;

i DTI.cpp

#include <DTI.h>

CDTIClass::CDTIClass()
{
}


CDTIClass::CDTIClass( std::string newName, CDTIClass* newParent )
{
	SetName( newName );
	
	SetParent( newParent );
}


CDTIClass::~CDTIClass()
{
}


std::string CDTIClass::GetName()
{
	return name;
}


void CDTIClass::SetName( std::string newName )
{
	name = newName;
}


CDTIClass* CDTIClass::GetParent()
{
	return parentClass;
}


void CDTIClass::SetParent( CDTIClass *newParent )
{
	parentClass = newParent;
}


bool CDTIClass::IsA( std::string testedName)
{
	
	CDTIClass* startClass = this;

	while( startClass )
	{
		if ( startClass->name == testedName )
		{
			return true;
		}
		else
		{
			startClass = startClass->GetParent();
		}
	}

	return false;
}

Teraz jak się tym posługiwać - jeżeli chcesz w jakiejś klasie wyróżnić jej typ, wystawić na widok, czy jakby tam nie przetłumaczyć słowa Expose, to używasz makra EXPOSE_TYPE. Weźmy dla przykładu klasę CActor:

 //actor.h
class CActor : public CObject
{
public:
	EXPOSE_TYPE;
	DECLARE_SUPER(CObject);
//........

Dięki makru EXPOSE_TYPE klasa Actor będzie udostępniała swój typ przy DTI, a dzięki DECLARE_SUPER będziemy mogli pisać super::JakasFunkcja, zamiast CObiect::JakasFunkcja (jeżeli chcemy, żeby wykonała się funkcja rodzica). W actor.cpp musimy dać jednak linijkę:

CDTIClass CActor::Class("CActor", &CObject::Class);

Dzięki temu ustalamy, że nazwą klasy jest "CActor", i że dziedziczy ona po CObject (jeżeli żaden z rodziców nie ma EXPOSE_TYPE, lub chcemy celowo zerwać chierarchię, to podajemy NULL w drugim parametrze). Działania funkcji DTI nie trzeba chyba tłumaczyć. Posługujemy się tym tak, jak pokazałem w poprzednim poście, czyli np.


TU MAŁA UWAGA
srx, bo w kilku miejscach pisałem np. CActor* costam, a potem costam.IsA(); oczywiście chodziło mi o costam->IsA(); odruchowo pisałem dla referencji :)


CActor *costam;

costam = new CMonster;    //gdziestam indziej w kodzie
//...
if(costam->IsA("CMonster")) //przetworz potwora

WAŻNA UWAGA - to, że mamy takie ładne DTI nie upoważnia nas do robienia downcastów. Na prawdę, nie ma nigdzie potrzeby rzutowania z CActor na np. CPlayer. Jeżeli potrzebujesz wiedzieć, który obiekt jest CPlayer, bo musisz np. wprowadzać sterowanie myszką, to przechowujesz sobie dodatkowy wskaźnik na CPlayer, i ustawiasz go w odpowiednim momencie. I nie martw się o to, że w CObject rośnie Ci ilość funkcji wirtualnych. W Unreal Tournament w klasie CActor było chyba ponad 100. Nie musisz i tak wszystkich później zmieniać w klasach pochodnych; wystarczy, że CActor zapewni jakieś tam podstawowe zachowanie, a większość obiektów potem z niego korzysta.

Dobra, znowu się rozpisałem, i pewno narobiłem koleje błędy :)
Pozdrawiam,
TeMPOraL [może wreszcie post wyśle się z mojego profilu].

0

Wielkie dzięki za pomoc! To jest naprawdę świetne!
Mam kilka pytań.

  1. Czy wszystkie obiekty umieścić w jednej tablicy/kontenerze i potem wywoływać dla nich funkcje Draw(), Animate(), czy lepiej zrobić kilka tablic/kontenerów?
  2. Do tej pory aby wykonać kolizje musiałem umieścić w sekcji public klasy CObject x, y, width, height. Potem na podstawie tych danych i jeszcze angel w funkcji wykobywałem pętle dla każdej pary obiektów, obliczałem cztery wierzchołki i wtedy sprawdzałem kolizje. Da się to wykonać lepiej?

1 użytkowników online, w tym zalogowanych: 0, gości: 1