Читать книгу Programowanie gier - Robert Nystrom - Страница 27
ОглавлениеKapsułkuje żądanie w formie obiektu. Umożliwia to parametryzację klienta przy użyciu różnych żądań oraz umieszczanie żądań w kolejkach i dziennikach, a także zapewnia obsługę cofania operacji5.
Polecenie to jeden z moich ulubionych wzorców. W przypadku większości pisanych przeze mnie programów – gier, ale nie tylko – koniec końców znajduje on gdzieś zastosowanie. Jeśli wykorzystuję go we właściwym miejscu, udaje się rozplątać trochę naprawdę sękatego kodu. Jak na tak elegancki wzorzec, Banda czterech – jak można się było spodziewać – stworzyła jego zawiły opis. Spójrzcie tylko wyżej.
Myślę, że wszyscy możemy się zgodzić, że jest to okropne zdanie. Po pierwsze, zniekształca metaforę, którą próbuje stworzyć – jakakolwiek by ona nie była. Na zewnątrz dziwnego świata oprogramowania, w którym słowa mogą znaczyć wszystko, „klient” to osoba – ktoś, z kim robi się interesy. Ostatnim razem, gdy sprawdzałem, istoty ludzkie nie mogły zostać „sparametryzowane”.
Następnie reszta tego zdania to po prostu lista rzeczy, do których być może moglibyśmy wykorzystać ten wzorzec. Nie oświeci nas to, chyba że nasz przypadek użycia znajdzie się akurat na tej liście. Mój zwięzły slogan opisujący wzorzec Polecenie jest następujący:
Polecenie to zreifikowane wywołanie metody.
Oczywiście „zwięzły” często znaczy tyle co „mętnie zdawkowy”, więc może to wcale nie oznaczać poprawy. Rozwijając to nieco: „reifikować” – na wypadek, gdybyście nigdy nie słyszeli tego słowa – znaczy „uczynić rzeczywistym”. Inny termin na reifikowanie to uczynienie czegoś „pierwszoklasowym”.
Słowo „reifikować” pochodzi od łacińskiego słowa „res”, które znaczy „rzecz” i końcówki „-ować”. Zasadniczo znaczy więc ono tyle, co „urzeczowić”, którego użycie, szczerze mówiąc, byłoby znacznie lepsze.
Oba terminy oznaczają wzięcie jakiegoś pojęcia i przemienienie go we fragment danych – jakiś obiekt – który można umieścić w zmiennej, przekazać do funkcji itp. Tak więc, mówiąc, że wzorzec Polecenie to „zreifikowane wywołanie metody”, mam na myśli to, że jest to wywołanie metody opakowane w obiekt.
Systemy odbicia w niektórych językach pozwalają nam w naszym programie na imperatywną pracę z typami w czasie wykonania. Możemy dostać obiekt, który reprezentuje klasę jakichś innych obiektów i możemy się z nim bawić, obserwując, co ten typ jest w stanie zrobić. Innymi słowy, refleksja to zreifikowany system typów.
Przypomina to w dużym stopniu „wywołanie zwrotne”, „funkcję pierwszoklasową”, „wskaźnik do funkcji”, „domknięcie” lub „funkcję częściowo aplikowaną”, w zależności od tego, z jakiego języka się wywodzimy. I rzeczywiście, wszystkie te kwestie są do siebie zbliżone. Banda czterech stwierdza następnie: „Polecenia to zorientowane obiektowo zamienniki wywołania zwrotnego”.
Byłoby to lepszym sloganem dla tego wzorca niż ten, który wybrała Banda czterech. Wszystko to jednak jest abstrakcyjne i mgliste. Chciałbym rozpoczynać rozdziały od czegoś konkretnego i pod tym względem zawaliłem. Aby to wynagrodzić, odtąd będę posługiwał się już wyłącznie przykładami, do których polecenia pasują znakomicie.
Konfigurowanie poleceń użytkownika
W każdej grze gdzieś znajduje się fragment kodu, który wczytuje surowe polecenia użytkownika – wciśnięte przyciski, zdarzenia w obrębie klawiatury, kliknięcia myszki – cokolwiek. Bierze on każdą taką daną wejściową i przekłada ją na sensowne akcje w obrębie gry:
Rysunek 2.1. Przyciski odwzorowane na akcje w grze
Najprostsza implementacja wygląda jakoś tak:
Wskazówka: nie należy za często wciskać B.
Zwykle ta funkcja jest wywoływana raz na każdą klatkę przez pętlę gry (rozdz. „Pętla gry”) i jestem pewny, że łatwo można dojść do tego, co takiego robi. Ten kod działa, jeśli chcemy na sztywno przypisać polecenia użytkownika do akcji w grze, ale wiele gier pozwala użytkownikowi na skonfigurowanie odwzorowania przycisków.
Aby to obsłużyć, musimy zamienić te bezpośrednie odwołania do jump() i fireGun() na coś, co będziemy w stanie podstawić. „Podstawianie” bardzo przypomina przypisywanie wartości zmiennej, potrzebujemy więc obiektu, który może nam posłużyć za reprezentację akcji w grze. Wprowadzamy wzorzec Polecenie.
Definiujemy klasę bazową, która reprezentuje wywoływalne polecenie w grze:
Jeśli mamy interfejs z pojedynczą metodą, która niczego nie zwraca, jest spora szansa, że jest to wzorzec Polecenie.
Następnie tworzymy podklasy dla każdej z różnych akcji w grze:
W naszym kodzie obsługi wejścia przechowujemy wskaźnik do polecenia dla każdego przycisku:
Teraz kod odpowiadający za obsługę wejścia po prostu przekazuje to do:
Zauważyliście, że nie sprawdzamy tu NULL? Oznacza to założenie, że każdy przycisk będzie miał dowiązane do siebie jakieś polecenie.
Jeśli chcemy obsługiwać przyciski, które nic nie robią i nie muszą bezpośrednio sprawdzać NULL, możemy zdefiniować klasę polecenia, którego metoda execute() nie robi nic. Następnie, zamiast ustawiać kod obsługi przycisku na NULL, robimy tak, aby wskazywał on na ten obiekt. Jest to wzorzec nazywany „Pustym obiektem”.
Wcześniej każde polecenie bezpośrednio wywoływało funkcję, a teraz mamy warstwę pośrednią.
Rysunek 2.2. Przyciski odwzorowane na przypisywalne polecenia
Oto wzorzec Polecenie w pigułce. Jeśli już możemy dostrzec jego wartość, resztę tego rozdziału możemy potraktować jako bonus.
Kierowanie aktorami
Klasy poleceń, które dopiero co zdefiniowaliśmy, sprawdzają się w poprzednim przykładzie, ale są dość ograniczone. Problem polega na tym, że zakładają one, iż istnieją funkcje najwyższego poziomu takie jak jump(), fireGun() itd., które w ukryty sposób wiedzą, w jaki sposób odnaleźć awatar gracza i zmusić go do tego, aby tańczył tak, jak mu zagramy.
To założone sprzężenie ogranicza użyteczność tych poleceń. JumpCommand może sprawić, aby skoczył jedynie gracz. Poluzujmy to ograniczenie. Zamiast wywoływać funkcje, które same odnajdują obiekty, których dotyczy polecenie, przekażemy obiekt, któremu chcemy rozkazywać:
W tym przypadku GameActor to nasza klasa „obiektu gry”, która reprezentuje jakąś postać w świecie gry. Przekazujemy ją do execute(), tak by pochodne polecenie mogło wywoływać metody na tych postaciach, które sobie wybierzemy, np. w taki sposób:
Teraz możemy wykorzystać tę jedną klasę, aby sprawić, że dowolna postać w grze będzie skakać. Między kodem obsługi wejścia a poleceniem brakuje nam jedynie jakiegoś kawałka, który przyjmuje polecenie i wywołuje je na właściwym obiekcie.
Najpierw zmieniamy handleInput() tak, by zwracało polecenia:
Nie możemy wykonać polecenia od razu, ponieważ nie wie ono, do którego aktora przekazać. W tym miejscu wykorzystujemy fakt, że polecenie jest zreifikowanym wywołaniem – możemy opóźnić moment wykonania wywołania. Następnie potrzebne nam będzie trochę kodu, który przyjmuje to polecenie i wykonuje je na aktorze reprezentującym gracza. Coś takiego:
Zakładając, że aktor odnosi się do postaci gracza, umożliwi to poprawne kierowanie nim na podstawie poleceń użytkownika. Wracamy więc do tego samego zachowania, jakie mieliśmy w pierwszym przykładzie. Dodanie warstwy pośredniej między poleceniem a wykonującym je aktorem pozwoliło nam uzyskać małą i zgrabną właściwość: możemy teraz pozwolić graczowi na kontrolowanie dowolnego aktora w grze, zmieniając aktora, na którym wykonujemy te polecenia.
W praktyce nie jest to powszechna cecha, istnieje jednak podobny przypadek użycia, który rzeczywiście często się pojawia. Jak dotąd rozważaliśmy jedynie postacie kierowane przez gracza, co jednak z wszystkimi innymi aktorami w świecie gry? Te sterowane są przez sztuczną inteligencję gry. Możemy wykorzystać ten sam wzorzec Polecenie w charakterze interfejsu między silnikiem sztucznej inteligencji i aktorami. Kod sztucznej inteligencji emituje po prostu obiekty Command.
W tym miejscu oddzielenie sztucznej inteligencji wybierającej polecenia oraz kodu aktora, który je wykonuje, daje nam mnóstwo elastyczności. Dla różnych aktorów możemy wykorzystać różne moduły sztucznej inteligencji. Możemy też dobrać sztuczną inteligencję do różnych rodzajów zachowań. Chcemy bardziej agresywnego przeciwnika? Zwyczajnie wpinamy bardziej agresywną sztuczną inteligencją, aby wygenerować dla niego polecenia. W rzeczywistości możemy nawet podpiąć sztuczną inteligencję do postaci gracza, co może się przydać np. w trybie demo, gdy gra musi działć w trybie autopilota.
Tworząc polecenia kontrolujące pierwszoklasowe obiekty aktora, usunęliśmy ścisłą zależność bezpośredniego wywołania metody. Zamiast tego możemy o tym pomyśleć jako o kolejce czy też strumieniu poleceń:
Rysunek 2.3. Kiepsko narysowana analogia
Znacznie więcej informacji na temat tego, do czego może nam się przydać kolejkowanie znaleźć można w rozdziale Kolejka zdarzeń.
Dlaczego odczułem potrzebę stworzenia rysunku przedstawiającego „strumień”? I dlaczego wygląda on jak rura?
Jakiś kod (odpowiadający za obsługę wejścia lub sztuczną inteligencję) tworzy polecenia i umiejscawia je w strumieniu, inny kod (dyspozytor lub sam aktor) konsumuje polecenia i je wywołuje. Wtykając w środek tę kolejkę, oddzieliliśmy producenta z jednej strony i konsumenta z drugiej.
Jeśli sprawimy, aby te polecenia były możliwe do serializacji, możemy przesłać ich strumień przez sieć. Możemy wziąć polecenia gracza, pchnąć je po sieci do innej maszyny, a następnie ponownie je odtworzyć. Jest to istotny element tworzenia wieloosobowych gier sieciowych.