Autor Wątek: Data-driven programming - źródła  (Przeczytany 6642 razy)

Offline Esidar

  • Użytkownik

# Grudzień 09, 2011, 21:57:07
Data-driven to sposób projektowania, a data-oriented to sposób programowania.
To sztuczny podział. I moim zdaniem głównie wyrobiony przez teoretyków, bo ktoś kto ma swój kod oparty o tą filozofię nie widzi takich "podziałów".

Nie ma różnicy pomiędzy tym:
Component* o = new Component(); // alokuj z heap'a
a tym:
Component* o = new Component(); // przeciążone new alokuje z liniowo ułożonego poola obiektów - cache-friendly.

Natomiast różnica pomiędzy OOP a data-oriented jest diametralna, bo nie wystarczy stwierdzić "dane umieszczone cache-friendly" albo "wczytam klasę z pliku" (jakiego ? cpp ?). Zmienia się za to cała konstrukcja silnika, to jak są tworzone obiekty growe, jak są przesyłane dane w obrębie obiektu, jak są zmieniane właściwości oraz działanie obiektów itd.

Kod jest stały. Właściwie ilość kodu w OOP a data-oriented jest bardzo podobna. Zmienia się za to sposób powiązania. To czy konstrukcja obiektu (np. componenty) jest wczytywana z pliku .txt z definicją "klasy obiektu" czy jest hardcodowana w .cpp nie zmienia filozofii działania całego kodu ani konstrukcji/organizacji danych. To tylko detal implementacyjny.

Offline Mr. Spam

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

Offline Dab

  • Redaktor
    • blog

# Grudzień 09, 2011, 22:31:21
Cytuj
To sztuczny podział. I moim zdaniem głównie wyrobiony przez teoretyków, bo ktoś kto ma swój kod oparty o tą filozofię nie widzi takich "podziałów".

Po co teoretyzować? Ściągnij sobie Warsztatową Grę: http://www.musztardasarepska.pl/wgdown/

Jest totalnie data-driven, każdy czar/potwór/mapka to złożony XML w którym są zagnieżdżane węzły do odpalania i kontroli kolejnych efektów. Bez żadnych modyfikacji w źródłach można stworzyć masę rzeczy od Arkanoida po symulator lotu. A jednocześnie obok data-oriented kod WG nawet nie leżał, wszystko opiera się na setkach klas i niejasnym relacjom między nimi.

Offline Esidar

  • Użytkownik

# Grudzień 09, 2011, 23:14:05
Jest totalnie data-driven, każdy czar/potwór/mapka to złożony XML w którym są zagnieżdżane węzły do odpalania i kontroli kolejnych efektów.
:) To że dane są wczytywane z XML nie oznacza że silnik jest "data-driven" :) Równie dobrze mogą się wczytywać z <tu wstaw dowolne rozszerzenie używane w tysiącach gier od czasu DOS'a>. W ten sposób to gra która wczytuje mapę BSP jest data-driven bo w mapie ma zapisany int "czy ten trójkąt to jest CPortal czy COccluder a może CMesh". Z taką definicją to OOP polegałoby na "nie wczytywaniu żadnych plików" bo jakikolwiek input z "po za kodu" byłby oznaką "data-driven".

To że w pliku .xml jest napisane "ma 10 HP i pluje ogniem" to też nie jest żaden "sposób programowania/projektowania". Tak się robi od wieków i w zwykłym OOP.

Nie dorabiajmy filozofii i nazewnictwa do wczytywania zwykłych zasobów gry.  Jeżeli kod to setki klas z niejasnymi powiązaniami to jest to zwykłe OOP.

Offline Dab

  • Redaktor
    • blog

# Grudzień 09, 2011, 23:25:29
Cytuj
To że w pliku .xml jest napisane "ma 10 HP i pluje ogniem" to też nie jest żaden "sposób programowania/projektowania". Tak się robi od wieków i w zwykłym OOP.

Ale rzecz w tym, że data-driven nie ma (bezpośrednio) nic wspólnego ani z OOP ani z DOD. Data-driven po prostu znaczy "nie hardkoduj danych". W kodzie gry nie ma "CKotoszczur" ani "CGraczMag". Właściwości obiektów (w tym postaci gracza) są zdefiniowane na podstawie danych. Przy czym raczej nie istnieje jakaś sztywna granica podziału co może być w kodzie a co w danych, żeby określić, że jakaś gra jest data-driven a inna nie.
« Ostatnia zmiana: Grudzień 09, 2011, 23:31:27 wysłana przez Dab »

Offline Kos

  • Użytkownik
    • kos.gd

# Grudzień 10, 2011, 11:43:43
@up - jak masz klasy COrk, COrkMag i CKtoszczurMag, to nawet według samego OOP dobrze jest to rozbić, by mieć klasę CStwór i własności Rasa, Magowość (abstrakcyjnie jako interfejsy lub nieabstrakcyjne jako zwykłe struktury). Możemy sobie z taką infrastrukturą zrobić dowolnego TrollaMaga w kodzie lub plikach z danymi.
Jest to na pewno jedna z rzeczy, do których pił Kyle Wilson.

Pytanie: ile (czy cokolwiek) ma to wspólnego z opisem Wilkersona i Wirfs-Brocka z 1989 (patrz wiki)?

Offline Kos

  • Użytkownik
    • kos.gd

# Grudzień 10, 2011, 11:51:49
Tu ciekawy opis:

http://www.faqs.org/docs/artu/ch09s01.html

Cytuj
When doing data-driven programming, one clearly distinguishes code from the data structures on which it acts, and designs both so that one can make changes to the logic of the program by editing not the code but the data structure.

Data-driven programming is sometimes confused with object orientation, another style in which data organization is supposed to be central. There are at least two differences. One is that in data-driven programming, the data is not merely the state of some object, but actually defines the control flow of the program.

Niby to, co u Wilsona "hardcode nothing", ale też wyraźnie napisane, że to jest zupełnie co innego, niż OOP. Nie wiem, co o tym myśleć.

Offline Esidar

  • Użytkownik

# Grudzień 10, 2011, 16:26:33
Data-driven po prostu znaczy "nie hardkoduj danych".
Gdyby przyjąć taką definicję, to nie byłoby ani jednej gry która by nie była data-driven :) ba, nawet ani jednego programu użytkowego. Wszelkie prezentacje na ten temat by nie miały sensu bo każda gra by wczytywała jakieś "nie hardkodowane dane" z plików i byłaby data-driven. Ale tak nie jest :) O Data-driven się pisze w kontekście sposobu tworzenia kodu, a nie wczytywania danych. Nie jest to kwestia semantyki zapisu danych (czy wczytujemy z XML, LUA, CPP, INI), ale sposobu działania programu.

Niby to, co u Wilsona "hardcode nothing", ale też wyraźnie napisane, że to jest zupełnie co innego, niż OOP. Nie wiem, co o tym myśleć.
To co podałeś z linku, jest dość wystarczające. Szczególnie np. "Programming data-driven style is also sometimes confused with writing state machines" czyli to co pisałem powyżej. To nie jest kwestia wczytywania z XML'a danych do maszyny stanów która przełącza się że "obiekt teraz jest czołgiem, a teraz jest biedronką".


Ponownie odwołam się do prezentacji z przed paru lat które już podawałem. Scott Bilas - Data Driven Game Object System (to podstawa), Rob Fermier - Creating a Data Driven Engine, Duran Alex - Building Object System. Każda z tych prezentacji opisuje różne sposoby implementacji, bo nie ma "jedynej słusznej". Ale każda z nich przedstawia podstawy data-driven, czym się różni od oop, jakie są cele, jakie problemy rozwiązuje itd.

Gdybym miał polecać, to zacząłbym od stworzenia systemu opartego o componenty. To dobry wstęp do zrozumienia działania data-driven w grach. Potem to już kwestie różnych implementacji: dodawanie skryptów (albo nie), organizacja pamięci komponentów, sposoby komunikacji między komponentami itd. IMO Duran Alex - Building Object System się do tego najlepiej nadaje i najprościej to opisuje. On co prawda nie nazywa tego data-driven (w ogóle tego nie nazywa), ale podaje odnośniki do dokumentów o data-driven. Wszystkie te dokumenty opisują to samo (data-driven), ale imo Duran Alex opisuje to najbardziej przystępnie.
« Ostatnia zmiana: Grudzień 10, 2011, 16:28:25 wysłana przez Esidar »

Offline lmmilewski

  • Użytkownik
    • Łukasz Milewski - devblog

# Grudzień 10, 2011, 19:20:05
@Esidar dzięki za wartościowe źródła ;-)

Ale jeżeli chodzi o hardcodowanie danych to IMO z tym "ani jednego" trochę przesadzasz.

@Dab myślę, że w prawidłowo zaprojektowanym OOP też nie będzie "CKotoszczur" ani "CGraczMag".


Offline Paweł

  • Użytkownik

# Grudzień 10, 2011, 19:51:32
Pytanie co jeszcze rozumiecie poprzez komponent. Do tej pory spotkałem się z dwoma podejściami:
1.struct Object{
map<int,Component> components;
};
2.
int object = createObject();
component1.add(object);
Obydwa podejścia mają wady i zalety.
Teraz robie tak że obiekt to poprostu mapa uchwytów do składników obiektów. Jak widać narzut jest dość spory. Kolejny problem to odpowiedni podział danych w komponentach: jak struktury będą małe to będzie duża 'elastyczność' ale będzie trzeba trzymać więcej uchwytów, map, ogólnie więcej odpytywania o komponenty. Jak struktury będą duże to znowu sytem będzie bardziej sztywny. I co gdy jeden komponent wymaga innego? Użuwac zaślepki, czy może dodawac niezbędny składnik? Jak to rozwiązujecie?

Offline koirat

  • Użytkownik

# Grudzień 11, 2011, 02:04:17
Mi sie nie podoba ani 1 ani 2 :P.
1. mi sie nie podoba bo mamy wolny dostęp do "map" i możemy tam mieszać do woli. Dodatkowo nie wiem jak jest komponent tworzony wiec nie wiem jak sie komunikuje z innymi komponentami w obiekcie.
2. dodajemy do komponentu obiekt, wydaje mi sie to malo logiczne. Czy komponent bedzie mogl miec wielu rodzicow ? W ten czas rezygnujemy z jego stanu.

3. Ja bym to chyba zrobil jakos tak (na codzien nie programuje w c++ (to kod pogladowy) wiec prosze o wyrozumiałość)

class Object{
  private:
        map<int,Component> components;

   public:
         void AddComponent(Component* component)
          {
                component.parent=this;
                map.add(component.id , component)
           }

          Component* GetComponent(int id);
};

Jak brakuje komponentu który jest potrzebny niech wywala błąd natomiast unikał bym zautomatyzowanego dodawania komponentów.

Na koniec destruktor obiektu niszczy wszystkie komponenty.

Moje 3 grosze.
« Ostatnia zmiana: Grudzień 11, 2011, 02:09:22 wysłana przez koirat »

Offline Esidar

  • Użytkownik

# Grudzień 11, 2011, 14:45:37
1. mi sie nie podoba bo mamy wolny dostęp do "map" i możemy tam mieszać do woli.
Tak, bo każdy programista marzy o mieszaniu w kodzie. Ja np. dzisiaj odpaliłem Visuala, wybrałem sobie losowo klasę z publicznym kontenerem i wpisałem 4 do szóstego elementu. Nie wiem po co i do czego ten kontener służy, ale to nie jest istotne bo lubię "mieszać do woli". Dodatkowo lubię od czasu do czasu zrobić ((uint*)this)[0] = 4; więc też warto się przed tym zabezpieczyć.

at.2 : to kwestia implementacji, bo nie jest powiedziane że obiekt będzie rodzicem componentu. Jeżeli obiekt typu component jest tylko JEDEN to może on przechowywać listę wszystkich obiektów które z niego korzystają (patrz: Duran Alex - Building Object System) np:
class Component
{
    struct map<int,ComponentData>         daneDlaObiektow;
    void add( int objectId )
    {
         daneDlaObiektow.insert( objectId )
    }
};
Oczywiście kwestią pozostaje wymiana informacji między komponentami i obiektem, ale w tym przykładzie, nie ma sensu tworzenie specjalnego obiektu Object który przechowuje komponenty, bo czemu to ma służyć ? Istotą jest to że komponenty "nie wiedzą" o istnieniu innych komponentów ani jaki mają typ, więc metoda GetComponent zupełnie niczemu nie służy (co potem zrobisz z tym wskaźnikiem ?).

Jak brakuje komponentu który jest potrzebny
A jaki komponent jest potrzebny ? System będzie go rozpoznawać po nazwie ? Po ID ? Sprawdzi if( FindComponent( 0x172376d613 ) == null ) Message( "ten czołg nie ma bakłażana" ); ?

-----

Pierwsza wersja tworzy bez sensu obiekt który nikomu nie jest potrzebny. Obiekt nie ma żadnych danych bo one siedzą w komponentach, nie będzie też nigdzie przechowywany (po za managerem obiektów, a jemu wystarczy zwykły INT, bez wskaźnika) i ma listę componentów której też nikt nie będzie używał.

Z kolei druga metoda ma tą zaletę, że mając tylko jeden obiekt Component (to może też być statyczna klasa), można układać dane dla obiektów, liniowo w pamięci w ten sposób przyśpieszając niektóre obliczenia.
Dobrze jest sobie zrobić jeden Component główny w celach optymalizacyjnych. Większość komponentów będzie korzystać z pozycji obiektu, więc LocationComponent który ma współrzędne, orientację, ew. macierz, może nie mieć std:map tylko std:vector a wszystkie komponenty mogą być świadome istnienia takiego komponentu, wtedy pobieranie pozycji obiektu będzie się odbywać po indeksie do tablicy a nie w mapie, ale to tylko kwestia optymalizacyjna, no i to jest raczej wyjątek, bo inne komponenty raczej się ze sobą nie komunikują bezpośrednio.

Offline koirat

  • Użytkownik

# Grudzień 11, 2011, 20:37:03
Tak, bo każdy programista marzy o mieszaniu w kodzie. Ja np. dzisiaj odpaliłem Visuala, wybrałem sobie losowo klasę z publicznym kontenerem i wpisałem 4 do szóstego elementu. Nie wiem po co i do czego ten kontener służy, ale to nie jest istotne bo lubię "mieszać do woli". Dodatkowo lubię od czasu do czasu zrobić ((uint*)this)[0] = 4; więc też warto się przed tym zabezpieczyć.

Hołduje zasadzie iż program powinien być jak najbardziej odporny na działania szkodliwe, wiele sytuacji szkodliwych tworzy się  przypadkowo przez nieuwage. Równiż nie wiadomo kto w przyszłości będzie korzystał z tego kody. Tak samo jak pracujesz w grupie informatyków doceniasz takie rozwiązania.

at.2 : to kwestia implementacji, bo nie jest powiedziane że obiekt będzie rodzicem componentu. Jeżeli obiekt typu component jest tylko JEDEN to może on przechowywać listę wszystkich obiektów które z niego korzystają (patrz: Duran Alex - Building Object System) np:
class Component
{
    struct map<int,ComponentData>         daneDlaObiektow;
    void add( int objectId )
    {
         daneDlaObiektow.insert( objectId )
    }
};

Po to obiekt powinien być rodzicem komponentu aby obiekt kontrolował długość życia tego komponentu, tzw. kompozycja. Dodatkowo jeśli klasa Obiekt enkapsuluje zbiór komponentów jest dodatkowa jasność w strukturze.

Oczywiście kwestią pozostaje wymiana informacji między komponentami i obiektem, ale w tym przykładzie, nie ma sensu tworzenie specjalnego obiektu Object który przechowuje komponenty, bo czemu to ma służyć ? Istotą jest to że komponenty "nie wiedzą" o istnieniu innych komponentów ani jaki mają typ, więc metoda GetComponent zupełnie niczemu nie służy (co potem zrobisz z tym wskaźnikiem ?).
A jaki komponent jest potrzebny ? System będzie go rozpoznawać po nazwie ? Po ID ? Sprawdzi if( FindComponent( 0x172376d613 ) == null ) Message( "ten czołg nie ma bakłażana" ); ?
Niewiem dlaczego uważasz iż komponenty nie powinny mieć dostępu do innych komponentów ?
A ID oznaczało by pewien bazowy typ componentu  niewiem jak by to można było sensownie rozwiązać w C++ może typeid  albo enum ?

Ta druga metoda o której mówimy jest właśnie data oriented (powiedzmy ze idzie w tym kierunku) i  to rozwiazanie nie jest przezemnie tu opisywane. Jako iż Paweł pisał tu ogólnie, napisałem moją opinie.
« Ostatnia zmiana: Grudzień 11, 2011, 20:41:45 wysłana przez koirat »

Offline Esidar

  • Użytkownik

# Grudzień 12, 2011, 14:22:29
Równiż nie wiadomo kto w przyszłości będzie korzystał z tego kody.
Infoterroryści ? Kodopsuje ? Przez nie uwagę, to można zamazać pamięć, a nie mieć dostęp do mapy. Jeżeli ktoś robi manipulacje na mapie, to wie dlaczego, po co i jak.

Po to obiekt powinien być rodzicem komponentu aby obiekt kontrolował długość życia tego komponentu, tzw. kompozycja.
Obiekt nie kontroluje życia komponentu. Życie komponentu jest kontrolowane przez dane sterujące obiektem (data-driven), czyli różnego rodzaju managery, które tworzą, usuwają i dokonują manipulacji na obiektach. Obiekt sam z siebie ani nie usuwa ani nie dodaje komponentów.

Niewiem dlaczego uważasz iż komponenty nie powinny mieć dostępu do innych komponentów ?
Bo jeśli komponenty będą ze sobą powiązane, to nie będzie się to niczym różniło od OOP.

Programowanie data driven wymaga pewnego specyficznego podejścia do powiązań pomiędzy danymi i kodem który nimi manipuluje. Zamiast:
CollisionComponent::Update()
{
     if( collided )
         soundComponent->PlaySound( collisionSound );
}
masz
CollisionComponent::Update()
{
     if( collided )
         Event( "collision" );
}
To oczywiście tylko jeden przykład, ale takich przykładów są dziesiątki. Pisanie komponentów musi być tak zrobione aby nie było powiązań między nimi. Wtedy dokładanie nowych funkcjonalności albo usuwanie zbędnych, jest bardzo proste (u mnie to jest dołączenie pliku który sam się rejestruje w systemie, albo całego projektu). Nie ma też wtedy takich "kwiatków" że ci wyskakuje "Sorry ale ten czołg nie pojedzie bo komponent Move wymaga komponentu EngineSound".

« Ostatnia zmiana: Grudzień 12, 2011, 14:24:13 wysłana przez Esidar »

Offline koirat

  • Użytkownik

# Grudzień 14, 2011, 15:04:08
Infoterroryści ? Kodopsuje ? Przez nie uwagę, to można zamazać pamięć, a nie mieć dostęp do mapy. Jeżeli ktoś robi manipulacje na mapie, to wie dlaczego, po co i jak.

No to Const pewnie tez nie ma sensu stosować skoro wszyscy wiedza co robią. Biorąc pod uwage iż zasłanianie wewnętrznej struktury class jest częstą praktyką w różnych frameworkach, aby nie być gołosłownym np. w Ogre3d raczej mnie nie przekonałeś. Co do pyskatej formy twojej wypowiedzi to pozostawie ją bez komentarza.

Co do dalszej twojej wypowiedzi, to przecież napisałem że to o czym mówie nie opisuje sytuacji data-oriented.
Tylko odpowiadam na pytanie Pawła:
"Pytanie co jeszcze rozumiecie poprzez komponent. Do tej pory spotkałem się z dwoma podejściami:"

Offline Kos

  • Użytkownik
    • kos.gd

# Grudzień 14, 2011, 15:13:09
Kłócicie się o nie-wiadomo-co. Kwestia się rozbija, czy lista komponentów ma być częścią interfejsu, czy nie ma. Poziom enkapsulacji rzecz względna.