Autor Wątek: Zawieranie się obiektów  (Przeczytany 10337 razy)

.

  • Gość
# Sierpień 03, 2007, 20:37:19
Hehe... sprawdzałęm te wszystkie kosmiczne pomysły. Z elegancją kodu nie mają nic wspólnego. :)
W kazdym razie moim ulubionym przykładem kłopotów z filtrowaniem obiektów było... "Jak przejechać po wszystkich obiektach w kolekcji, wykonać pętle for, jesli wszystkie mają tylko identyfikator string".
W HLSL można danego pass'a wezwać nazwą (string) lub indeksem. I czegoś takiego należy szukać, sobie organizować. Ja znalazłem enum'y... wygodne i w sumie ciężko się pomylić przy nich (przy string'ach pomyłki to standard). Jak ktoś jest "super, hiper" to może sobie jakiś system enumeracji spróbować sam stworzyć, vide .net'owy. Ja nie chce / dziękuje bardzo.

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Reg

  • Administrator
    • Adam Sawicki - Home Page

# Sierpień 04, 2007, 11:38:18
Eee, gadacie od rzeczy. Nasze pytanie to problem na wyższym poziomie - projektowym, jak sama nazwa działu forum wskazuje. To czy kontener będzie sobie obiekty trzymał w std::set, std::map, stdext::hash_map czy std::vector to jest szczegół. Nam chodzi o to jak ma sprawa wyglądać od zewnątrz dla użytkownika tego kontenera i jego elementów.

Cytuj
Nie wiem czy zbliżę się do sedna sprawy, ale czy nie mógłbyś za pomocą uniwersalnego Add() dodawać wskaźników bazowego typu, a potem używać dynamic_cast<>?
Na tym właśnie polega rozwiązanie oznaczone przez Xiona numerem 2. Czy dynamic_cast czy nie, to już mniejsza z tym - nie widzę związku z tematem.

Cytuj
Jeżeli nie chcesz trzymać dużych ilości elementów, identyfikacja może być rozwiązana przez łańcuchy znaków... ewentualnie potem, pod koniec testów gry/programu - kiedy urośnie Ci ilość elementów - możesz wygenerować unikalne identyfikatory liczbowe dla każdego z nich.
Całkowicie zmieniać sposób oznaczania obiektów, odnoszenia się do nich itd. na końcu projektu to chyba nienajlepszy pomysł.

Cytuj
W kazdym razie moim ulubionym przykładem kłopotów z filtrowaniem obiektów było... "Jak przejechać po wszystkich obiektach w kolekcji, wykonać pętle for, jesli wszystkie mają tylko identyfikator string".
Tego nie rozumiem. Przecież niezależnie czy obiekty są ponazywane stringami, liczbami czy w ogóle niczym, to koniec końców wewnątrz kontenera siedzą w jakimś std::map czy czymś takim i zawsze można po nich wszystkich po kolei przejść.

Cytuj
Ja znalazłem enum'y... wygodne i w sumie ciężko się pomylić przy nich (przy string'ach pomyłki to standard).
Tego też nie rozumiem. Jeśli mówisz o zdefiniowaniu takiego zwykłego C++-owego enuma, to przecież kompletnie nie pasuje do problemu, bo statyczne określa ile będzie i jakie będą elementy.

EDIT: Właśnie sobie uświadomiłem, że podejście nr 3 (element sam dodaje się do kolekcji w konstruktorze, a usuwa w destruktorze) ma jeszcze jedną wielką zaletę - można do niego stosować normalne inteligentne wskaźniki, które zwolnią go po prostu wywołując delete i wszystko będzie OK.
« Ostatnia zmiana: Sierpień 04, 2007, 12:37:06 wysłana przez Reg »

Offline orzech

  • Użytkownik
    • homepage

# Sierpień 04, 2007, 12:41:43
Witam,

Tak czytam sobie ten wątek i chciałem coś wnieść do dyskusji, ale mam jedno pytanie. Nie do końca rozumiem zastosowanie omawianej konstrukcji. Czy moglibyście podać jakiś prosty przykład z życia wzięty, gdzie elementy powinny w swoim stanie przechowywać powiązanie z kontenerem? Byłbym wdzięczny za jakiś przykład, bo ciężko mi przypomnieć sobie, kiedy czegoś takiego używałem (a być może używałem).

Dzięki

Offline Rolnix

  • Użytkownik

# Sierpień 04, 2007, 13:36:48
Znalazłem coś dość ciekawego z wykorzystaniem boosta, spójrzcie:

http://www.boost.org/libs/multi_index/doc/index.html

.

  • Gość
# Sierpień 04, 2007, 17:32:30
Reg: W takim razie, nie wiem, pewnie źle cię zrozumiałem, gdzie ty masz problem z identyfikacją obiektów... do czego ci potrzebna, jeśli nie do filtrowania/iteracji? Może chcesz rozpoznawać jaki typ został podany na wejściu do Add() który przyjmie każdy typ? Chyba nie oto ci chodzi.
Co do opisuje mojej implementacji problemu filtrowania - poniosło mnie.



Offline Asmodeusz

  • Użytkownik
    • Bogumił Wiatrowski: Blog

# Sierpień 04, 2007, 17:49:31
To ja może zaproponuję rozwiązanie 2.1 odnośnie radzenia sobie z kontenerami. Otóż zamiast dodawać obiekt do kontenera w konstruktorze a kasować w destruktorze można to sobie znacznie ułatwić. Jak wiadomo, tymczasowe obiekty danego typu raczej nie muszą trafiać do kontenera - po pierwsze to za długo trwa, po drugie - po co? Dlatego też dodawanie obiektu sugeruję dodać na poziomie operatora new. Natomiast sam fakt identyfikacji typu: dynamic_cast, drzewo dziedziczenia na wzór .NET (dziedziczenie z jednej klasy i dowolnej liczby interfejsów) i problem jest rozwiązany. Unikalne identyfikatory: coby nie operować na całych łańcuchach, używam 64-bitowego całkowitego hasha dla łańcucha nazwy - dzięki szablonom liczony jest w czasie kompilacji a jednocześnie przekazanie runtime nazwy obiektu umożliwi jego znalezienie. Natomiast wyliczanie obiektów rozwiązane jest w podobny sposób: każdy obiekt ma ID (64-bitowy również) przypisywany w momencie dodawania go do któregokolwiek z globalnych kontenerów składający się z 8 bitów identyfikatora kontenera, 48 bitów właściwego ID i 8 bitów sumy kontrolnej dla ID kontenera i właściwego ID. Metoda wymaga produkcji znacznej ilości kodu (około 1500 linii w moim przypadku), ale działa dość szybko a do tego bardzo przyjemnie. Ponadto przeciążony new może być łatwo lączony z własną alokacją pamięci.

Offline Reg

  • Administrator
    • Adam Sawicki - Home Page

# Sierpień 05, 2007, 13:35:10
orzech: Typowe przykłady tego problemu spotykane w programowaniu gier:

- Manager zasobów przechowujący Zasoby, z czego rodzajami zasobów są Tekstura, Bufor, Render Target, Siatka
- Scena przechowująca Encje, z czego rodzajami encji są Potworek, Efekt cząsteczkowy

Offline orzech

  • Użytkownik
    • homepage

# Sierpień 05, 2007, 15:48:20
Ok, dzięki za przykłady.

Nie mniej jednak nie do końca widzę potrzebę tworzenia powiązania między kontenerem, a obiektami, które mają być w nim przechowywane. Może faktycznie jest to wygodne, aby każdy obiekt wiedział, w czym się zawiera, ale to jest właśnie odpowiedzialność pojemnika.

Pytanie jest - cóż takiego oferuje kontener (powiedzmy sobie SceneManager), aby przechowywane wewnątrz obiekty miały mieć do niego dostęp? Czy chodzi o to, aby obiekt (powiedzmy Potworek) sam mógł się usunąć z pojemnika? Jeśli tak, to można to zrobić bez przechowywania jakiejkolwiek referencji do kontenera. Jeśli chodzi o dodawanie do kontenera różnych Encji z poziomu innych Encji, to prowadziłoby to do dużego zamieszania w kodzie (z poziomu potworka dodawane są nowe potworki - czy ma to sens?). W takim przypadku można by wykorzystać zewnętrzny system poleceń, gdzie polecenia byłyby niezależne od logiki Encji. Przykład :

class Monster : public IEntity
{
public:
virtual void Update(float dt);
};

void Monster::Update(float dt)
{
    // Potworek nie wie nic o kontenerze, w ktorym sie znajduje
    // Odpowiedzialnosc za tego typu sprawy przenosimy na system polecen

    ResourceMgr *pResourceMgr = Systems.getResourceMgr();
    CommandMgr *pCommandMgr = Systems.getCommandMgr();
   
    ICommand *pAddNewMonster = pResourceMgr->GetResource( ID_COMMAND_ADD_NEW_MONSTER );
    pCommandMgr->PostCommand( pAddNewMonster );
}

// Sęk w tym, aby za obsługę kontenera nie odpowiadał bezpośrednio przechowywany obiekt.
// Jest to rozwiązanie pośrednio dobre, bo ktoś i tak musi trzymać informację o kontenerze, ale
// lepiej żeby robił to wyodrębniony system poleceń.

Inne, znacznie prostsze rozwiązanie to przekazywać kontener jako parametr metod, które mogłyby ewentualnie z niego skorzystać. Wtedy mielibyśmy :

void Monster::Update(IContainer *pContainer, float dt)
{
    pContainter->remove(this);
}

Zakładając, że to kontener wywołuje Update każdej z Encji, problem się rozwiązuje bez tworzenia jakiegokolwiek sztywnego powiązania (na etapie tworzenia instancji klasy). Jest to dużo wygodniejsze, bo kontenery mogą się podmieniać, a przechowywane obiekty zawsze będą otrzymywać w parametrze swoich metod kontener, do którego należą.

Być może przeoczyłem coś bardzo istotnego, co przekreśla moją koncepcję, więc jeśli jestem w błędzie, to proszę mnie wyprowadźcie. :)

Pozdrawiam!

Offline yarpen

  • Użytkownik

# Sierpień 05, 2007, 21:35:25
Wybor metody tak naprawde zalezny jest od jednego prostego pytania: czy moga istniec elementy nie nalezace do zadnego kontenera. Jezeli tak -> rozwiazanie 2, jezeli nie - to najlepiej przekazywac kontener do konstruktora, wyrzucac w destruktorze, bo wtedy nie ma mozliwosci "przypadkowego" uzyskania osieroconego obiektu. Rozwiazanie z konstrukcja w kontenerze nie podoba mi sie o tyle, ze miesza 2 funkcje: skladowanie i tworzenie (wiec obiekt jest jednoczesnie i fabryka i kontenerem, co nie w kazdym przypadku musi byc wada, ale jest mniej uniwersalne i te odpowiedzialnosci sa trudniejsze do rozbicia).

Offline Reg

  • Administrator
    • Adam Sawicki - Home Page

# Sierpień 06, 2007, 11:38:05
yarpen: Zgadzam się z tym co napisałeś.

orzech: To czy elementy mają trzymać wskaźnik do kontenera czy tylko kontener do elementów to też w tym problemie jest tylko szczegół implementacyjny, a nie istota sprawy.

Offline Xion

  • Redaktor
    • xion.log

# Sierpień 07, 2007, 13:14:10
yarpen: To prawda, zależy to głównie od dopuszczania lub niedopuszczania istnienia wolnych obiektów i generalnie od tego, czy istnienie danego elementu utożsamiamy z jego zawieraniem się w jakimś kontenerze. A czy tak jest, pozostaje kwestią projektową wyższego rzędu niż poziom, na którym mówimy o tym ogólnym związku klas.


A co do dylematu z początku topica, to chyba czas podjąć w końcu jakąś decyzję. Jak to bowiem twierdzi pewien dziennikarz w reklamie: "w życiu jest czas poszukiwań i czas kiedy trzeba w końcu coś odnaleźć" :) Postanowiłem zatem zdecydować się na rozwiązanie w pewien sposób kompromisowe, zależne od okoliczności.

W ogólności jest to oczywiście rozwiązanie 2, rozdzielające istnienie obiektu od jego przynależności. To sprawia też, że mogą istnieć obiekty wolne, niezależne od jakiegokolwiek kontenera.
Wspomniana "różnica zależna od okoliczności" dotyczy tego, kto zarządzą przynależnością obiektu do kontenera. W przypadku struktur hierarchicznych (jak np. węzły w drzewie DOM XML czy kontrolki GUI) obiekty same są kontenerami i dlatego są równorzędne; sądzę więc, że powinny posiadać interfejs służący zmianie kontenera na poziomie obiektu. Nie chodzi o żadną skomplikowaną konstrukcję, ale o zwykłą metodę SetParent(), która usunie obiekt ze starego pojemnika i wstawi do nowego.
W pozostałych przypadkach - jak choćby bytach w scenie 3D - kontener jest czymś innym i wyraźnie nadrzędnym wobec obiektu. Dlatego też ewentualna zmiana przynależności elementu (już z przykładu widać, że nie byłoby to częste) wymagać powinna świadomego usunięcia obiektu z jednego i wstawienia do drugiego pojemnika.

Zatem.. taka jest moja opinia i ja ją całkowicie podzielam ;)

.

  • Gość
# Sierpień 07, 2007, 15:26:01
Xion: Jeśli analizowałeś implementację kontenerów pod GUI.
to mam dla Ciebie (Was) kilka rad (może komuś się przydadzą):

1.)
Box& SetParent(Box*), Box& SetChildren(Box*, ...),
Box& operator<<(Box&),
Box& UnParent(Box*), Box& UnChildren(Box*, ...).

Gdzie Box jest JEDN¡ Z podstawowych klas (w miarę budowy coraz to bardziej zaawansowanych systemów GUI, do "podstawowych klas" pewnie także dołączą klasy warunków i skryptów (oskryptowane animacje GUI itp).

2.) Jeśli Button i Layer'ka dziedziczą z Box'a, to w Buttonie bez problemu mogą być inne Buttony(!) (zezwólaj na tego typu dowolność -- kiedyś docenisz).

3.) Oczywiście GUI może być używane jako okienka 2D jak i 3D: gdzieś wewnątrz budynku 3D jest komputer itp. (Dla 3D nie odczytujesz x = Stream::mouse->x, tylko projekcją (ale to każdy wie)).

4.) Możesz się zastanawiać jak zrobić, żeby jedno okienko przysłaniało inne i blokowało komunikaty myszki dla przysłoniętych okienek itp.
Masz dwukierunkową listę cykliczną.
Idąc w lewo rysujesz (począwszy od pierwszego, czyli "najbardziej przysłoniętego") potomków.
Idąc w prawo analizujesz nad którym potomkiem jest kursor myszy i który przysłania pozostałe.
Operacje powtarzasz rekurencyjnie przechodząc w głąb drzewa potomków.

Pomyślcie nad tym :)

Offline Reg

  • Administrator
    • Adam Sawicki - Home Page

# Sierpień 10, 2007, 16:03:47
Właśnie natrafiłem na argument przeciwko rozwiązaniu nr 3, który objawił mi się w postaci błędu "Pure virtual function call" czy jakoś tak.

class Entity
{
public:
  Entity(Kontener *k) { k->DodajEntity(this); }
  virtual BOX GetAABB() = 0;
};

class Kontener
{
public:
  void DodajEntity(Entity *e)
  {
    BOX AABB = e->GetAABB();
    ...
  }
};

class KonkretnaEntity : public Entity
{
public:
  KonkretnaEntity(Kontener *k) : Entity(k) { }
  virtual BOX GetAABB() { return ... }
};

Kontener K;
new KonkretnaEntity(&K); // Błąd!

Klasa bazowa ma funkcję abstrakcyjną (czysto wirtualną) mającą zwracać dane na temat tego obiektu. Dane te potrzebne są podczas dodawania obiektu do kontenera. Np. kontener realizuje podział przestrzeni BSP, Octree itp. i musi znać bryłę otaczającą dodawany obiekt. Tymczasem w konstruktorach i destruktorach metody wirtualne nie działają, bo wówczas istnieje jakoby tylko instancja klasy bazowej, jeszcze nie podrzędnej.

Rozwiązanie: Albo wybrać wariant oznaczony przez Xiona numerem 2, albo zrobić tak jak ja zrobiłem:

class Entity
{
public:
  Entity(Kontener *k) : m_k(k) { }
  void CtorHack() { m_k->DodajEntity(this); }
  virtual BOX GetAABB() = 0;

private:
  Kontener m_k;
};

class Kontener
{
public:
  void DodajEntity(Entity *e)
  {
    BOX AABB = e->GetAABB();
    ...
  }
};

class KonkretnaEntity : public Entity
{
public:
  KonkretnaEntity(Kontener *k) : Entity(k) { }
  virtual BOX GetAABB() { return ... }
};

Kontener K;
Entity *e = new KonkretnaEntity(&K);
e->CtorHack(); // Działa dobrze

Offline Hadrian W.

  • Użytkownik
    • Homepage

# Sierpień 10, 2007, 17:51:08
Regedit: tylko, czy w ten sposób cała ta zabawa nie traci sensu? Całość przestaje być automatyczna.

Offline krajek

  • Użytkownik

# Sierpień 10, 2007, 18:07:52
Queight : wcale nie trzeba rezygnować z 'automatyki', można np takie macro HaX!0rskie
#define MAKRO_HACK(entity,concrete,kontener) \
    Entity * entity = new concrete(&kontener);\
    entity->CtorHack();

Przykladowe wywołanie odpowiadające temu drugiiemu kodowi Rega.
MAKRO_HACK(e,KonkretnaEntity,K);
To tylko troszke wiecej roboty ale jest jako taka automatyka:P. Ważne ,że działa.