Jak mogę współdziałać i komunikować się między obiektami bez wymuszania hierarchii?

9

Mam nadzieję, że te kłótnie wyjaśnią moje pytanie - jednak całkowicie zrozumiem, jeśli nie, więc daj mi znać, jeśli tak jest, a ja postaram się wyjaśnić.

Poznaj BoxPong , bardzo prostą grę, którą stworzyłem, aby zapoznać się z tworzeniem gier obiektowych. Przeciągnij pole, aby kontrolować piłkę i zbierać żółte rzeczy.
Tworzenie BoxPong pomogło mi sformułować między innymi podstawowe pytanie: w jaki sposób mogę mieć obiekty, które współdziałają ze sobą bez konieczności „przynależenia” do siebie? Innymi słowy, czy istnieje sposób, aby obiekty nie były hierarchiczne, ale współistniały? (Przejdę do dalszych szczegółów poniżej.)

Podejrzewam, że problem współistnienia obiektów jest powszechny, więc mam nadzieję, że istnieje ustalony sposób jego rozwiązania. Nie chcę wymyślać kwadratowego koła, więc wydaje mi się, że idealną odpowiedzią, której szukam, jest „oto wzór projektowy, który jest często używany do rozwiązania twojego rodzaju problemu”.

Zwłaszcza w prostych grach, takich jak BoxPong, jasne jest, że istnieje lub powinna istnieć garść przedmiotów współistniejących na tym samym poziomie. Jest pudełko, piłka, jest przedmiot kolekcjonerski. Wszystko, co mogę wyrazić w językach obiektowych, chociaż - a przynajmniej tak się wydaje - to ścisłe relacje HAS-A . Odbywa się to poprzez zmienne składowe. Nie mogę tak po prostu zacząć balli pozwolić, by zrobiła to samo, potrzebuję, aby na stałe przynależeć do innego obiektu. Mam go skonfigurować tak, że głównym celem gry jest okno, a okno z kolei ma piłkę, a ma licznik wynik. Każdy obiekt ma równieżupdate()metoda, która oblicza pozycję, kierunek itp., i idę tam podobną drogą: wywołuję metodę aktualizacji głównego obiektu gry, która wywołuje metody aktualizacji wszystkich jej dzieci, a one z kolei wywołują metody aktualizacji wszystkich swoich dzieci. To jedyny sposób, w jaki mogę stworzyć grę obiektową, ale uważam, że nie jest to idealny sposób. W końcu nie uważałbym piłki za należącą do pudełka, ale raczej za bycie na tym samym poziomie i interakcję z nią. Przypuszczam, że można to osiągnąć, zamieniając wszystkie obiekty gry w zmienne składowe głównego obiektu gry, ale nie widzę, żeby to rozwiązywało. Mam na myśli ... pomijając oczywisty bałagan, w jaki sposób piłka i pudełko mogłyby się poznać , to znaczy wchodzić w interakcje?

Istnieje również problem obiektów, które muszą przekazywać między sobą informacje. Mam spore doświadczenie w pisaniu kodu do SNES, gdzie masz dostęp do praktycznie całej pamięci RAM przez cały czas. Załóżmy, że tworzysz niestandardowego wroga dla Super Mario World , i chcesz, aby usunął wszystkie monety Mario, a następnie po prostu zapisz zero, aby rozwiązać problem 0 USD bez problemu. Nie ma żadnych ograniczeń, że wrogowie nie mogą uzyskać dostępu do statusu gracza. Wydaje mi się, że ta wolność mnie rozpieszcza, ponieważ w C ++ i podobnych często zastanawiam się, jak udostępnić wartość innym obiektom (a nawet globalnym).
Na przykładzie BoxPong, co jeśli chciałbym, aby piłka odbiła się od krawędzi ekranu? widthi heightsą właściwościami Gameklasy,ballmieć do nich dostęp. Mógłbym przekazywać tego rodzaju wartości (za pomocą konstruktorów lub metod, w których są potrzebne), ale to po prostu krzyczy mi złe praktyki.

Wydaje mi się, że moim głównym problemem jest to, że potrzebuję przedmiotów, aby się poznać, ale jedynym sposobem, w jaki to widzę, jest ścisła hierarchia, która jest brzydka i niepraktyczna.

Słyszałem o „klasach przyjaciół” w C ++ i trochę wiem, jak one działają, ale jeśli są one rozwiązaniem całościowym, to dlaczego nie widzę friendsłów kluczowych wylanych w każdym projekcie C ++ i dlaczego koncepcja nie istnieje w każdym języku OOP? (To samo dotyczy wskaźników funkcji, o których niedawno się dowiedziałem.)

Z góry dziękuję za odpowiedzi dowolnego rodzaju - i ponownie, jeśli jest jakaś część, która nie ma dla ciebie sensu, daj mi znać.

vvye
źródło
2
Znaczna część branży gier przeszła na architekturę Entity-Component-System i jej odmiany. Jest to inny sposób myślenia niż tradycyjne podejście OO, ale działa dobrze i ma sens, gdy koncepcja się zapadnie. Unity go używa. W rzeczywistości Unity po prostu korzysta z części Entity-Component, ale opiera się na ECS.
Dunk
Problem zezwalania klasom na współpracę bez wiedzy między sobą rozwiązuje wzór projektowy Mediator. Patrzyłeś na to?
Fuhrmanator

Odpowiedzi:

13

Zasadniczo okazuje się, że obiekty tego samego poziomu znają się bardzo źle. Gdy obiekty się dowiedzą, zostaną ze sobą powiązane lub połączone . To sprawia, że ​​są trudne do zmiany, trudne do przetestowania, trudne do utrzymania.

Działa to znacznie lepiej, jeśli istnieje jakiś obiekt „powyżej”, który wie o tych dwóch i może ustawić interakcje między nimi. Obiekt, który wie o dwóch elementach równorzędnych, może powiązać je ze sobą poprzez wstrzyknięcie zależności lub zdarzenia lub przekazanie wiadomości (lub dowolny inny mechanizm odsprzęgający). Tak, to prowadzi do trochę sztucznej hierarchii, ale jest o wiele lepsze niż bałagan spaghetti, który pojawia się, gdy rzeczy po prostu chcą się rozproszyć. Jest to tylko ważniejsze w C ++, ponieważ potrzebujesz czegoś, aby mieć również żywotność obiektów.

Krótko mówiąc, możesz to zrobić, łącząc obiekty obok siebie przez dostęp ad hoc, ale to zły pomysł. Hierarchia zapewnia porządek i przejrzystą własność. Najważniejszą rzeczą do zapamiętania jest to, że obiekty w kodzie niekoniecznie są obiektami w prawdziwym życiu (a nawet w grze). Jeśli obiekty w grze nie tworzą dobrej hierarchii, lepsza może być inna abstrakcja.

Telastyn
źródło
2

Na przykładzie BoxPong, co jeśli chciałbym, aby piłka odbiła się od krawędzi ekranu? szerokość i wysokość są właściwościami klasy gry i potrzebuję piłki, aby mieć do nich dostęp.

Nie!

Myślę, że głównym problemem, jaki masz, jest zbyt „dosłowne” podejście do programowania obiektowego. W OOP obiekt nie reprezentuje „rzeczy”, ale „ideę”, co oznacza, że ​​„piłka”, „gra”, „fizyka”, „matematyka”, „data” itd. Wszystkie są prawidłowymi obiektami. Nie ma również wymogu, aby obiekty „wiedziały” o czymkolwiek. Na przykład Date.Now().getTommorrow()zapytałby komputer, który jest dzień, zastosował tajemne reguły dotyczące dat, aby ustalić datę jutra i zwrócił ją dzwoniącemu. DateObiekt nie wie o cokolwiek innego, potrzebuje tylko informacji żądania, ile potrzeba z systemu. Ponadto, Math.SquareRoot(number)nie trzeba nic wiedzieć poza logiką jak obliczyć pierwiastek kwadratowy.

W twoim przykładzie, który zacytowałem, „Piłka” nie powinna wiedzieć nic o „Boksie”. Pudełka i kulki to zupełnie inne pomysły i nie mają prawa ze sobą rozmawiać. Ale silnik fizyki wie, co to jest Box and Ball (a przynajmniej ThreeDShape) i wie, gdzie się znajdują i co się z nimi dzieje. Więc jeśli piłka kurczy się, ponieważ jest zimna, silnik fizyki powiedziałby instancji piłki, że jest teraz mniejsza.

To trochę jak budowanie samochodu. Chip komputerowy nic nie wie o silniku samochodowym, ale samochód może użyć chipu komputerowego do sterowania silnikiem. Prosty pomysł użycia małych, prostych rzeczy razem, aby stworzyć nieco większą, bardziej złożoną rzecz, która sama w sobie może być używana jako komponent innych, bardziej złożonych części.

A w przykładzie Mario, co jeśli jesteś w pokoju wyzwań, w którym dotknięcie wroga nie powoduje marnowania monet Marios, a jedynie wyrzuca go z tego pokoju? Jest poza obszarem idei Mario lub wroga, że ​​Mario powinien stracić monety, dotykając wroga (w rzeczywistości, jeśli Mario ma gwiazdę niewrażliwości, zabija wroga zamiast tego). Tak więc każdy obiekt (dziedzina / idea), który jest odpowiedzialny za to, co dzieje się, gdy Mario dotyka wroga, jest jedynym, który musi o nim wiedzieć i powinien zrobić cokolwiek z nim (w zakresie, w jakim obiekt ten pozwala na zewnętrzne zmiany sterowane ).

Co więcej, w twoich wypowiedziach na temat obiektów wywołujących dzieci Update(), jest to bardzo podatne na błędy, jak gdyby Updatenazywało się wiele razy na ramkę od różnych rodziców? (nawet jeśli złapiesz to, to zmarnowany czas procesora, który może spowolnić twoją grę) Każdy powinien dotykać tylko tego, czego potrzebuje, kiedy jest to konieczne. Jeśli używasz Update (), powinieneś użyć jakiejś formy wzorca subskrypcji, aby upewnić się, że wszystkie Aktualizacje są wywoływane raz na ramkę (jeśli nie jest to obsługiwane w Unity)

Nauka definiowania pomysłów domeny w jasne, izolowane, dobrze zdefiniowane i łatwe w użyciu bloki będzie największym czynnikiem wpływającym na efektywność wykorzystania OOP.

Tezra
źródło
1

Poznaj BoxPong, bardzo prostą grę, którą stworzyłem, aby zapoznać się z tworzeniem gier obiektowych.

Tworzenie BoxPong pomogło mi sformułować między innymi podstawowe pytanie: w jaki sposób mogę mieć obiekty, które współdziałają ze sobą bez konieczności „przynależenia” do siebie?

Mam spore doświadczenie w pisaniu kodu do SNES, gdzie masz dostęp do praktycznie całej pamięci RAM przez cały czas. Załóżmy, że tworzysz niestandardowego wroga dla Super Mario World, i chcesz, aby usunął wszystkie monety Mario, a następnie po prostu zapisz zero, aby rozwiązać problem 0 USD bez problemu.

Wygląda na to, że brakuje ci programowania obiektowego.

Programowanie obiektowe polega na zarządzaniu zależnościami poprzez selektywne odwracanie niektórych kluczowych zależności w architekturze, aby zapobiec sztywności, kruchości i niemożliwości ponownego użycia.

Czym jest zależność? Zależność polega na czymś innym. Kiedy przechowujesz zero, aby zaadresować 0DBF, polegasz na tym, że pod tym adresem znajdują się monety Mario i że monety są reprezentowane jako liczba całkowita. Twój niestandardowy kod wroga zależy od kodu implementującego Mario i jego monety. Jeśli zmienisz miejsce, w którym Mario przechowuje swoje monety w pamięci, musisz ręcznie zaktualizować cały kod odnoszący się do lokalizacji w pamięci.

Kod zorientowany obiektowo polega na uzależnieniu kodu od abstrakcji, a nie od szczegółów. Więc zamiast

class Mario
{
    public:
        int coins;
}

piszesz

class Mario
{
    public:
        void LoseCoins();

    private:
        int coins;
}

Teraz, jeśli chcesz zmienić sposób, w jaki Mario przechowuje swoje monety z długiego lub podwójnego, lub przechowuje je w sieci, przechowuje w bazie danych lub rozpoczyna inny długi proces, dokonujesz zmiany w jednym miejscu: Klasa Mario i cały pozostały kod działa bez żadnych zmian.

Dlatego kiedy pytasz

jak mogę mieć obiekty, które współdziałają ze sobą bez konieczności „przynależenia” do siebie?

naprawdę pytasz:

jak mogę mieć kod, który zależy bezpośrednio od siebie bez żadnych abstrakcji?

który nie jest programowaniem obiektowym.

Sugeruję, aby zacząć od przeczytania wszystkiego tutaj: http://objectmentor.com/omSolutions/oops_what.html, a następnie wyszukania wszystkiego na YouTubie przez Roberta Martina i obejrzenia wszystkiego.

Moje odpowiedzi pochodzą od niego, a niektóre z nich są bezpośrednio od niego cytowane.

Mark Murfin
źródło
Dzięki za odpowiedź (i stronę, do której linkujesz; wygląda interesująco). Właściwie wiem o abstrakcji i możliwości ponownego użycia, ale chyba nie ułożyłem tego zbyt dobrze w mojej odpowiedzi. Jednak z przykładowego kodu, który podałeś, mogę teraz lepiej zilustrować mój punkt widzenia! Mówisz w zasadzie, że obiekt wroga nie powinien tego robić mario.coins = 0;, ale mario.loseCoins();co jest w porządku i prawda - ale chodzi mi o to, w jaki sposób wróg może mariomimo to uzyskać dostęp do obiektu? mariobycie zmienną członkowską enemynie wydaje mi się właściwe.
vvye
Cóż, najprostszą odpowiedzią jest przekazanie Mario jako argumentu funkcji w Enemy. Możesz mieć funkcję taką jak marioNearby () lub attackMario (), która wziąłaby Mario za argument. Tak więc za każdym razem, gdy pojawia się logika, kiedy wróg i Mario powinni wchodzić w interakcje, wywołujemy wroga.marioNearby (mario), który wywołałby mario.loseCoins (); W dalszej części drogi możesz zdecydować, że istnieje klasa wrogów, którzy powodują, że mario traci tylko jedną monetę, a nawet zdobywa monety. Masz teraz jedno miejsce na dokonanie tej zmiany, która nie powoduje zmian skutków ubocznych w innym kodzie.
Mark Murfin
Przekazując Mario wrogowi, właśnie go połączyłeś. Mario i Wróg nie powinni wiedzieć, że ten drugi to nawet coś. Dlatego tworzymy obiekty wyższego rzędu, aby zarządzać parowaniem prostych obiektów.
Tezra
@Tezra Ale czy te obiekty wyższego rzędu nie są w ogóle wielokrotnego użytku? Wydaje się, że te obiekty działają jak funkcje, istnieją tylko jako procedura, którą wykazują.
Steve Chamaillard,
@SteveChamaillard Każdy program będzie miał co najmniej odrobinę specyficznej logiki, która nie ma sensu w żadnym innym programie, ale pomysł polega na utrzymaniu tej logiki w izolacji dla kilku klas wysokiego rzędu. Jeśli masz mario, wroga i klasę poziomu, możesz ponownie użyć mario i wroga w innych grach. Jeśli zwiążesz wroga i Mario bezpośrednio ze sobą, każda gra, która potrzebuje jednej, musi przyciągnąć drugą.
Tezra
0

Możesz włączyć luźne sprzęganie, stosując wzór mediatora. Jego wdrożenie wymaga odniesienia do komponentu mediatora, który zna wszystkie komponenty odbierające.

Uświadamia sobie „myśl mistrza gry, proszę, niech tak się stanie”.

Ogólny wzorzec to wzorzec publikowania-subskrybowania. Właściwe jest, aby mediator nie zawierał dużej logiki. W przeciwnym razie użyj ręcznie spreparowanego mediatora, który wie, gdzie kierować wszystkie połączenia, a może nawet je modyfikować.

Istnieją warianty synchroniczne i asynchroniczne, zwykle nazywane magistralą zdarzeń, magistralą komunikatów lub kolejką komunikatów. Sprawdź je, aby ustalić, czy są odpowiednie w danym przypadku.

Hero Wanders
źródło