Projektowanie oparte na komponentach: obsługa interakcji między obiektami

9

Nie jestem pewien, jak dokładnie obiekty robią rzeczy z innymi obiektami w projekcie opartym na komponentach.

Powiedz, że mam Objklasę. Ja robię:

Obj obj;
obj.add(new Position());
obj.add(new Physics());

Jak więc mógłbym mieć inny przedmiot, który nie tylko poruszy piłkę, ale zastosuje fizykę. Nie szukam szczegółów implementacji, ale raczej abstrakcyjny sposób komunikowania się obiektów. W projekcie opartym na jednostce możesz mieć po prostu:

obj1.emitForceOn(obj2,5.0,0.0,0.0);

Każdy artykuł lub wyjaśnienie, aby lepiej zrozumieć projekt oparty na komponentach i sposób wykonywania podstawowych czynności, byłoby naprawdę pomocne.

jmasterx
źródło

Odpowiedzi:

10

Zwykle odbywa się to za pomocą wiadomości. Możesz znaleźć wiele szczegółów w innych pytaniach na tej stronie, takich jak tutaj lub tam .

Aby odpowiedzieć na twój konkretny przykład, sposobem jest zdefiniowanie małej Messageklasy, którą Twoje obiekty mogą przetwarzać, np .:

struct Message
{
    Message(const Objt& sender, const std::string& msg)
        : m_sender(&sender)
        , m_msg(msg) {}
    const Obj* m_sender;
    std::string m_msg;
};

void Obj::Process(const Message& msg)
{
    for (int i=0; i<m_components.size(); ++i)
    {
        // let components do some stuff with msg
        m_components[i].Process(msg);
    }
}

W ten sposób nie „zanieczyszczasz” Objswojego interfejsu klasami metodami związanymi z komponentami. Niektóre komponenty mogą przetworzyć wiadomość, niektóre mogą ją po prostu zignorować.

Możesz zacząć od wywołania tej metody bezpośrednio z innego obiektu:

Message msg(obj1, "EmitForce(5.0,0.0,0.0)");
obj2.ProcessMessage(msg);

W takim przypadku obj2„s” Physicswybierze wiadomość i zrobi wszystko, czego potrzebuje. Po zakończeniu będzie albo:

  • Wyślij do siebie komunikat „SetPosition”, który Positionwybierze komponent;
  • Lub bezpośrednio uzyskaj dostęp do Positionkomponentu w celu modyfikacji (całkiem źle w przypadku projektu opartego wyłącznie na komponentach, ponieważ nie możesz założyć, że każdy obiekt ma Positionkomponent, ale Positionkomponent może być wymagany Physics).

Zasadniczo dobrym pomysłem jest opóźnienie faktycznego przetworzenia wiadomości na aktualizację następnego komponentu. Natychmiastowe przetworzenie może oznaczać wysyłanie wiadomości do innych elementów innych obiektów, więc wysłanie tylko jednej wiadomości może szybko oznaczać nierozerwalny stos spaghetti.

Prawdopodobnie będziesz musiał później wybrać bardziej zaawansowany system: asynchroniczne kolejki komunikatów, wysyłanie wiadomości do grupy obiektów, rejestrowanie / wyrejestrowywanie poszczególnych komponentów z wiadomości itp.

MessageKlasa może być ogólna pojemnik z prostego łańcucha, jak pokazano powyżej, ale przetwarzania ciągi przy starcie nie jest naprawdę skuteczny. Możesz wybrać kontener wartości ogólnych: ciągi, liczby całkowite, liczby zmiennoprzecinkowe ... Z nazwą lub jeszcze lepszą, z identyfikatorem, aby rozróżnić różne typy wiadomości. Możesz też uzyskać klasę podstawową, która będzie pasować do konkretnych potrzeb. W twoim przypadku możesz sobie wyobrazić, EmitForceMessageże wywodzi się Messagei dodaje pożądany wektor siły - ale uważaj, jeśli to zrobisz, koszt czasu wykonania RTTI .

Laurent Couvidou
źródło
3
Nie martwiłbym się „nieczystością” bezpośredniego dostępu do komponentów. Komponenty służą do zaspokajania potrzeb funkcjonalnych i projektowych, a nie środowiska akademickiego. Chcesz sprawdzić, czy komponent istnieje (np. Sprawdź, czy wartość zwracana nie jest pusta dla wywołania komponentu get).
Sean Middleditch
Zawsze myślałem o tym, jak ostatnio powiedziałeś, używając RTTI, ale tak wiele osób mówiło tak wiele złych rzeczy o RTTI
jmasterx
@SeanMiddleditch Pewnie, zrobiłbym to w ten sposób, tylko wspominając, że aby było jasne, że zawsze powinieneś dokładnie sprawdzać, co robisz, uzyskując dostęp do innych komponentów tego samego bytu.
Laurent Couvidou
@Milo RTTI zaimplementowane przez kompilator i dynamic_cast może stać się wąskim gardłem, ale na razie nie będę się tym martwić. Nadal możesz to zoptymalizować później, jeśli stanie się to problemem. Identyfikatory klas oparte na CRC działają jak urok.
Laurent Couvidou
´template <typename T> uint32_t class_id () {static uint32_t v; return (uint32_t) & v; } ´ - nie wymaga RTTI.
arul
3

To, co zrobiłem, aby rozwiązać problem podobny do tego, co pokazujesz, to dodanie pewnych procedur obsługi komponentów i dodanie pewnego rodzaju systemu rozwiązywania zdarzeń.

Tak więc w przypadku obiektu „Fizyka” po zainicjowaniu dodawałby się do centralnego menedżera obiektów fizyki. W pętli gry menedżerowie tego rodzaju mają swoje własne etapy aktualizacji, więc gdy ten program PhysicsManager jest aktualizowany, oblicza wszystkie interakcje fizyki i dodaje je do kolejki zdarzeń.

Po wygenerowaniu wszystkich zdarzeń możesz rozwiązać kolejkę zdarzeń, po prostu sprawdzając, co się stało i wykonując odpowiednie działania, w twoim przypadku powinno być zdarzenie, w którym obiekt A i B w jakiś sposób wchodzą w interakcję, więc wywołujesz metodę emitForceOn.

Zalety tej metody:

  • Pod względem koncepcyjnym jest bardzo prosty do naśladowania.
  • Daje ci miejsce na konkretne optymalizacje, takie jak korzystanie z quadtress lub cokolwiek, czego potrzebujesz.
  • Ostatecznie jest to naprawdę „plug and play”. Obiekty z fizyką nie wchodzą w interakcje z obiektami niefizycznymi, ponieważ nie istnieją dla menedżera.

Cons:

  • W efekcie pojawia się wiele referencji, więc poprawne wyczyszczenie wszystkiego może być nieco nieporządne, jeśli nie jesteś ostrożny (od komponentu do właściciela komponentu, od menedżera do komponentu, od wydarzenia do uczestników itp. ).
  • Musisz zwrócić szczególną uwagę na kolejność rozwiązywania wszystkiego. Wydaje mi się, że to nie twój przypadek, ale napotkałem więcej niż jedną nieskończoną pętlę, w której wydarzenie stworzyło kolejne zdarzenie i właśnie dodawałem je bezpośrednio do kolejki zdarzeń.

Mam nadzieję, że to pomoże.

PS: Jeśli ktoś ma czystszy / lepszy sposób na rozwiązanie tego, naprawdę chciałbym to usłyszeć.

Carlos
źródło
1
obj->Message( "Physics.EmitForce 0.0 1.1 2.2" );
// and some variations such as...
obj->Message( "Physics.EmitForce", "0.0 1.1 2.2" );
obj->Message( "Physics", "EmitForce", "0.0 1.1 2.2" );

Kilka rzeczy do zapamiętania na temat tego projektu:

  • Nazwa komponentu jest pierwszym parametrem - ma to na celu uniknięcie nadmiernej pracy kodu nad komunikatem - nie wiemy, jakie komponenty może wywołać dowolny komunikat - i nie chcemy, aby wszystkie z nich przeżuwały komunikat z 90% błędem stopa, która konwertuje na wiele niepotrzebnych gałęzi i strcmp .
  • Nazwa wiadomości to drugi parametr.
  • Pierwsza kropka (w punktach 1 i 2) nie jest konieczna, po prostu ułatwia czytanie (dla ludzi, a nie komputerów).
  • Jest kompatybilny z sscanf, iostream, you-name-it. Brak cukru syntaktycznego, który nie upraszcza przetwarzania wiadomości.
  • Jeden parametr łańcuchowy: przekazywanie typów rodzimych nie jest tańsze pod względem wymagań dotyczących pamięci, ponieważ musisz obsługiwać nieznaną liczbę parametrów stosunkowo nieznanego typu.
snake5
źródło