Programowanie zorientowane obiektowo jest niewątpliwie jednym z najpopularniejszych obecnie paradygmatów. Powstało bardzo wiele opracowań na jego temat wprowadzających chociażby techniki znane jako wzorce projektowe. Nie jest to jednak rozwiązanie idealne i w pewnych zastosowaniach wiążą się z nim pewne trudności. Chodzi tutaj o tworzenie gier komputerowych gdzie coraz częściej można się spotkać z programowaniem opartym na komponentach.

Uzasadnienie

Praktycznie w każdej grze występuje główna baza obiektów w niej występujących. Zwykle są to instancje klas ściśle ze sobą powiązanych odpowiednią hierarchią. Wspólna funkcjonalność, zgodnie z duchem OOP, jest w miarę możliwości przenoszona do klas znajdujących się wyżej w hierarchii. Wszelkie odstępstwa od domyślnego zachowania są implementowane przy użyciu polimorfizmu.

Wraz ze wzrostem stopnia skomplikowania projektu coraz trudniej jest unikać dziedziczenia wielobazowego i wszystkich związanych z nim niedogodności, w trakcie pozbywania się duplikowanego kodu. Innym problemem który się pojawia jest powstanie bardzo dużych klas bazowych, które są zwykle wysoce niezalecane. Dochodzi do sytuacji gdzie programowanie zorientowane obiektowo najwyraźniej nie jest najlepszy sposobem na rozwiązanie danego problemu.

Podstawowe założenia

W powyższych sytuacjach bardziej istotnym od zarządzania obiektami jest wygodne zarządzanie logiką kodu. Realizowane jest to za pomocą dwóch podstawowych elementów programowania opartego na komponentach:

  • jednostka (entity) - identyfikuje to co w OOP zostałoby nazwane obiektem. Nie posiada pól ani metod, jest jedynie czymś w rodzaju nazwy. Swoim przeznaczeniem przypomina klasę, ponieważ definiuje konkretne zachowanie, lecz ilościowo odpowiada obiektom.
  • komponent (component) - definiują logikę operacji. Każdy komponent opisuje inne fragmenty zachowania jednostki. Najważniejszą różnicą między klasami a komponentami jest to, że klasy opisują pełną funkcjonalność, podczas gdy pojedynczy komponent tylko jej część. W większości implementacji komponenty są obiektami globalnymi realizowanymi przy pomocy wzorca singleton, jednakże nie jest to sztywną regułą.

Niezwykle istotnym elementem całego systemu jednostek jest przyporządkowywanie im konkretnych komponentów. Może to odbywać się na dwa sposoby, zależnie od implementacji. Także umieszczenie kodu i danych jest sprawą zależną od samej implementacji. Warto jednak pamiętać, że podstawowym założeniem programowania opartego na komponentach jest utrzymanie minimalnego rozmiaru jednostki. W rezultacie jest to najczęściej liczba całkowita.

Dane

W systemach opartych na tym paradygmacie dane są umieszczane w komponentach. W zależności od możliwych zastosowań jednostki, komponenty przechowują dane potrzebne do realizowania przez nie odpowiednich funkcji. Wymaga to dodatkowego mechanizmu komunikowania się pomiędzy komponentami różnych typów w sytuacji gdy potrzebna jest modyfikacja danych innego komponentu. Powinno to być jednak unikane. Główną zaletą jest uproszczenie mechanizmów związanych z synchronizacją w przypadku systemów wielowątkowych. Każdy komponent zajmuje się tylko swoimi danymi dzięki czemu można ograniczyć ilość blokad.

Dodatkowo najczęściej to komponenty przechowują informację o przyporządkowanym im jednostką. Dzięki temu zlokalizowanie wszystkich jednostek jest proste i nie wymaga istnienia dodatkowych struktur danych. W tej kwestii wiele jednak zależy od implementacji, bo nie da się ukryć, że zdarza się także sytuacja w której każda jednostka przechowuje listę przypisanych komponentów.

Kod

W przypadku kodu najpopularniejszym rozwiązaniem jest umieszczenie go także w komponentach. Wtedy co prawda jest on powiązany z danymi, co może przypominać OOP, jednak uzyskuje się duże rozdrobnienie funkcjonalności co ułatwia wiele kwestii. Oprócz wspomnianego ominięcia problemu synchronizacji, możliwa jest dynamiczna zmiana właściwości i metod obiektu (reprezentowanego przez jednostkę) co pozwala na ograniczenie ilości zajmowanej pamięci. Istotny jest także inny sposób zapisu kodu co jest główną cechą odróżniającą od siebie paradygmaty programowania. Znacząco ułatwia to dodanie dodatkowej funkcjonalności poprzez wprowadzenie kolejnego komponentu.

Komponenty ułatwiają także masowe wykonywanie operacji. Jeżeli dana funkcja powinna być wykonana na wszystkich obiektach danego typu, komponent potrafi to zrobić korzystając z posiadanej listy przyporządkowanych mu jednostek. Kluczowe jest tutaj ustalenie odpowiedniej kolejności wykonywania komponentów w odpowiedzi na zdarzenia.

Przykład realizacji

Przykładowe zastosowanie komponentów w grze polega na stworzeniu wystarczającej ich ilości do pokrycia każdego możliwego zachowania obiektów. Oczywiście, z reguły nie stosuje się tutaj żadnej hierarchii komponentów, ponieważ zwykle nie przynosi to żadnych korzyści. Każdy obiekt obecny w grze jest zarejestrowany w komponencie Render który udostępnia procedury związane z jego wyświetlaniem. Oprócz tego łódki mogą być zarejestrowane w komponencie Boat. Po ich zniszczeniu zostają usunięte z komponentu Boat a zarejestrowane w Wreck.

Główna pętla programu w odpowiedniej kolejności, często w odpowiedzi na konkretne zdarzenia wykonuje żądane komponenty. Dla przykładu, jeżeli samochód jest sterowany przez gracza, to należy do komponentu PlayerControl, którego procedury są wywoływane w odpowiedzi na zdarzenia z urządzeń zewnętrznych.

Podsumowanie

Programowanie oparte na komponentach zwane także bardziej szczegółowo Entity System jest bez wątpienia bardzo ciekawym spojrzeniem na sposób rozwiązania problemów związanych z dużą ilością podobnych obiektów, których implementacja zawiera jednak pewne różnice. Pozwalają one w takiej sytuacji na uporządkowanie kodu i co za tym idzie ułatwienia jego dalszej rozbudowy.

Jest to technika stosowana najczęściej w przypadku produkcji gier komputerowych co nie oznacza, że w innych dziedzinach nie może znaleźć ona zastosowania. Istnieje wiele różnych podejść do takich kwestii jak umieszczenie kodu czy sposób kojarzenia komponentów z jednostkami. Dzieje się tak, ponieważ nie ma tutaj uniwersalnego rozwiązania i każdy dostosowuje to do swoich potrzeb. Podobnie rzecz się ma gdy porównać komponenty do OOP. Trudno powiedzieć żeby jeden paradygmat był lepszy od drugiego. Ich zastosowanie jest odmienne i należy tak je używać, aby wykorzystać jak najwięcej ich zalet.

Komentarze do wpisu "Programowanie oparte na komponentach":

1. sprae napisał(a):
18 lutego 2009, 07:25:36

Bardzo ładny tekst. Mimo wszystko zobaczyłbym jakiś prosty przykład implementacji (albo ilustrację). Takie rzeczy pozwalają odbiorcy stwierdzić, czy jego myślenie przebiega w odpowiednim kierunku. ;)

2. sf napisał(a):
18 lutego 2009, 10:45:40

Ciekawy wpis. Mam nadzieję na więcej :)

3. Paweł Dziepak napisał(a):
18 lutego 2009, 20:05:22

Jeżeli chodzi o przykład implementacji to byłby zbyt obszerny, ale rzeczywiście muszę pomyśleć nad wspieraniem się ilustracjami i schematami.

4. stronger napisał(a):
19 lutego 2009, 22:02:17

Interesujące podejście. Czy mógłbyś podać odnośniki do jakichś frameworków lub bibliotek wspierających programowanie ES? Na pierwszy rzut oka przypomina ono nieco konstrukcję Traits uzupełnioną wzorcem Registry. Jestem ciekaw czy tak właśnie jest, bo każdy problem rozwiązywany przez Traitsy da się zaimplementować przy użyciu tradycyjnego OOP (niewiele większym wysiłkiem).

A tu o paradygmatach programowania jako takich: http://steve-yegge.blogspot.com/2008/10/universal-design-pattern.html

5. Paweł Dziepak napisał(a):
19 lutego 2009, 22:39:39

Zacznę od końca. Triatsy rzeczywiście wyglądają bardzo podobnie do komponentów. Może nie zaznaczyłem tego wyraźnie, ale właśnie o to chodzi, że programowanie oparte na komponentach można zrealizować przy użyciu OOP i tak jest to najczęściej robione. Tak więc używając Traitsów tak na prawdę to już nie jest taki "tradycyjny" OOP.

Co do przykładowych implementacji, to przed napisaniem tego artykułu szukałem trochę i nie znalazłem żadnego rozwiązania z udostępnionym kodem źródłowym. Prawie wszystkie przypadki kiedy ktoś wspominał o takim rozwiązaniu dotyczyły programowania gier, a jak wiadomo opensource w tej dziedzinie nie jest zbyt powszechnym rozwiązaniem.

6. Paweł Dziepak napisał(a):
19 lutego 2009, 23:42:22

Wydaje mi się, że potrzeba jeszcze uściślić powyższy komentarz. Nie ukrywam że triatsy nie były mi dość dobrze znane i tamtą opinię wyraziłem po pobieżnym przeczytaniu jednego opracowania.

Z tego co zauważyłem to triatsy są wykorzystywane do utworzenia konkretnej instancji która jest później wykorzystywana tak samo jak każdy inny obiekt. W przypadku komponentów to są one najczęściej singletonami przechowującymi informację o jednostkach.

Nie mogę się zgodzić, że w podobnie prosty sposób da się to zrealizować "czystym" OOP. Wymieniłem przykładowe problemy w moim artykule.

Generalnie żeby dobrze "poczuć" ideę komponentów nie można myśleć o nich jako o czymś do zrealizowania zgodnie z OOP. Myślę, że dobrym (chociaż nie idealnym) porównaniem będzie stwierdzenie, że programowanie komponentowe jest OOP w którym dane i kod zamienimy miejscami.

Dodaj komentarz: