Podczas ćwiczeń z Programowania 2 motywem przewodnim zadań będzie pisanie prostej gry RPG w trybie tekstowym. Mam nadzieję, że każdy co najmniej raz w życiu widział jakąkolwiek grę komputerową, a może nawet grę RPG typu Baldur's Gate czy Diablo (w tym miejscu purystów gatunku RPG przepraszam :) ). Naszym celem będzie stworzenie czegoś na kształt Diablo - jedna postać przeciwko hordzie potworów czyhających w lochach. Temat jest na tyle szeroki że pozwoli wykorzystać wszystkie dostępne w OOP możliwości, dodatkowo jest na tyle naturalny, że łatwo budować model problemu - hierarchię dziedziczenia, itp.
W trakcie trwania semestru będę podawał listę czynności, które należny w grze zaimplementować, będę też na zajęciach pokazywał przykładowy kod napisany przezemnie. Kod zostanie potem opublikowany na stronie (przed kolejnymi ćwiczeniami). Proszę jednak starać się pisać samodzielnie - kod nie musi wyglądać i działać dokładnie tak jak mój, można wykorzystywać własną inwencję twórczą.
Na koniec semestru będzie można oddać stworzoną w trakcie zajęć grę jako projekt zaliczeniowy.
Zakładam, że do pisania będziemy używć standardowo Dev-C++. Jeśli ktoś ma inne preferencje np. Microsoft Visual Studio, pisanie w konsoli z makefilem, nie ma problemu, jeśli będzie miał do tych narzędzi dostęp (make jest standardowo instalowany, gorzej z VStudio).
Jeśli ktoś korzysta z projektu Dev-C++ to powinien po otworzeniu projektu dodać we właściwościach (prawym przyciskiem na nazwie projektu w okienku nawigatora -> właściwości (properties)) ustawić w zakładce directories odpowiednią ścieżkę w kategorii include directories. W plikach zip jest tam ustawiona ścieżka do mojego położenia projektu.
Jeżli ktoś korzysta z make to powinien także ustawić tą ścieżkę w opcjach kompilatora. Tutorial jak to zrobić wkrótce
Te ścieżki są po to, aby można było w projekcie korzystać z #inlcude < _nazwa_lokalnego_pliku_.h > zamiast z cudzysłowu #inlcude " ścieżka/względna/w/projekcie/_nazwa_lokalnego_pliku_.h " . Eliminuje to czasem konieczność użycia niepotrzebnych i brzydko wyglądających ../../ w ścieżce w includzie. Chciałem państwu pokazać jak można to zrobić.
Omówienie regulaminu / Wstęp do OOP (C++) - dziedziczenie, hierarchia dziedziczenia, typy dziedziczenia, enkapsulacja.
Zadania:
Przed przystąpieniem do pracy stwórz następujące klasy:
attack(Creature &c)
Stwórz funkcję main i utwórz tam kilka instancji klasy Creature. Utwórz obiekt klasy Adventurer. Wykonaj kilka razy funkcję attack() na stworzonych potworach.
Gratulacje, napisałeś swoją pierwszą gre RPG! ;)
Jesli pisałeś wszystko w jednym pliku podziel projekt na pliki, jeden z klasą adventurer, jeden z klasą creature, jeden z zawartością main. Zadbaj o zabezpieczenie przed wielokrotnym włączeniem.
OOP (C++) - konstruktory, destruktory, rzutowanie, wskaźniki, referencje, dynamic_cast, wstęp do wirtualności.
Zadania:
OOP (C++) - wirtualność cd., klasy abstrakcyjne, wyodrębnianie wielu poziomów abstrakcji
Zadania:
W klasie Monster (reprezentującej potwory) dodaj wirtualną metodę attack, która będzie służyła do atakowania graczy jednym z dostępnych dla potwora ataków. Funkcja ta powinna być czysto wirtualna - to sam potwór powinien zdecydować jaki jest jego ulubiony atak! Np. Goblin powinien atakować w zwarciu, Goblin łucznik z dystansu a mag swoimi czarami.
Zmodyfikuj swój kod, tak aby możliwa była interakcja z graczem, np. przed każdym atakiem mógł wybrać jaki atak stosuje przeciwko potworowi (Hint: stwórz w klasie Adventurer funkcję attack, która za każdym razem na początku zapyta, który atak wybrać, po czym wywoła odpowiednią funkcję (magicAttack, normalAttack, rangedAttack)
Zmodyfikuj kod przykładowej gry tak aby korzystał z nowych funkcji - gra zaczyna już być interaktywna!
Gra robi się też coraz bardziej zagmatwana... Spróbuj dodać do możliwości gracza umiejętność leczenia. Dodaj umiejętność leczenia się niektórym potworom. Co się stanie jeśli będziesz musiał dodać wiele takich specjalnych umiejętności?
Jak widać, za każdym razem musisz zmodyfikować kod w wielu miejscach, np. w klasie Bazowej i we wszystkich klasach pochodnych. Czy nie da się tego zrobić lepiej? Zastanów się czy wszystkie umiejętności specjalne nie mają pewnych wspólnych cech? Czy wszystkie ataki nie mają wspólnych cech? Czy nie da się tych wspólnych cech przekształcić we wspólną abstrakcyjną klasę bazową (albo interface)?
Stwórz klasę Attack, która pozwala atakować potwory, niech posiada jedną funkcję wirtualną doAttack(Monster& m). Wywiedź z niej klasy MagicAttack, NormalAttack, RangedAttack. W konstruktorach, klasy te powinny w jakiś sposób mieć możliwość otrzymania parametrów, które zdecydują jak dużo obrażeń zadają. Możesz w tym celu w konstruktorze przekazać referencję do obiektu klasy Adventurer.
Usuń z klasy Adventurer (i pochodnych) specjalistyczne funkcje [yyy]Attack(..), zostaw tylko funkcję attack(...). W klasie adveturer stwórz pole currentAttack. Niech każda z klas pochodnych: Thief, Mage i Warrior zainicjalizuje to pole odpowiednią wartością w swoim konstruktorze.
W klasie Attack dodaj wirtualną metodę const char* getDescription(); która zwraca nazwę ataku. Rozszerz przykład z poprzedniego podpunktu: w klasie Adventurer dodaj tablicę (albo vector z stla - dla wygody), która będzie trzymać wszystkie dostępne dla postaci ataki. Pobaw się i stwórz kilka podklas MagicAttack, RangedAttack, NormalAttack (np. HolySmiteAttack, którzy zadaje dwa razy więcej obrażeń, ale zużywa manę :) ). Niech każda nowa klasa w metodzie getDescription() zwraca krótką nazwę np. poprzez return "Mój super wyjątkowy atak!";
W klasie Attack znów dodaj opcję wyboru ataku, przy każdym wywołaniu niech funkcja wylistuje tablicę ataków wykorzystując funkcję getDescription(), po czym zapyta a numer ataku.
Teraz nasuwa się pytanie: czy możesz hierarchię klasy ataków wykorzystać dla potworów? Póki co one dalej korzystają z gotowych funkcji, co nie jest elastycznym rozwiązaniem? Co zrobić, aby klasy ataków były równie dobre przeciwko poszukiwaczowi przygód? Hint: stwórz nową (lub wykorzystaj istniejącą) abstrakcję (klasę nadrzędną), która pozwoli traktować w pewnych przypadkach klasy Adventurer i Monster tak samo. Hint 2: uzyj klasy Creature.
Spraw aby klasa Adventurer dziedziczyła po creature. Teraz możesz np. usunąć metody getHitPoints(), setHitPoints(), oraz odpowiadające im pola z klasy Adventurer! Dzięki abstrakcji, kod staje się krótszy i mniej podatny na błędy.
Zmień metodę doAttack() tak aby przyjmowała referencję do Creature. W konstruktorach klas pochodnych zmień przyjmowane parametry tak, aby i potwory mogły korzystać ze stworzonych ataków (albo stwórz nowe konstruktory!).
W klasie monster usuń metody [yyy]Attack, zmodyfikuj klasę Goblin tak aby wykonywała zawsze atak normalny. Klasę maga zmodyfikuj tak, aby mag pamiętał 3 rózne ataki i wykonywał je na zmianę (w kółko zapętlając modulo 3).
Pobaw się teraz kodem, stwórz nowe rodzaje ataków, stwórz ataki dedykowane tylko dla konkretnego rodzaju potwora, dla konkretnego rodzaju postaci itp. Przy tworzeniu postaci dodaj możliwość wyboru posiadanych ataków. Podczas awansu dodaj możliwość wyboru kolejnych rodzajów ataków.
Rozwiążemy teraz kolejny problem: umiejętności specjalne, inne niż ataki. Do tej pory były to funkcje w klasie adventurer. Czy nie dałoby się stworzyć podobnej struktury dla umiejętności, jak dla ataków? Co więcej, czy nie dałoby się ich jakoś podłączyć do hierchii ataków?
Wykonaj eksperyment. Skoro teraz atak przyjmuje jako cel obiekt klasy Creature, z której wywodzi się również Adventurer, spróbuj atakiem zaatakować samego bohatera (player.attack(player)). Udało się? Co jeśli stworzyłbyś klasę HealingAttack : publick Attack, która zamiast zadawać rany leczyłaby? Co taka sytuacja znaczy z punktu widzenia abstrakcji? Czy klasy reprezentujące umiejętności specjalne i ataki mają jakieś wspólne cechy? Jak należny wyodrębnić z nich wspólną abstrakcję?
Stwórz klasę Action reprezentującą dowolną akcję podejmowaną przez istoty w świecie gry. Dla ciekawostki stworzymy w niej funkcję-operator: virtual void operator(Creature& target) = 0;. Tak, takie rzeczy też są możliwe w C++! :)
Stórz przykładową akcję: ExamineAction : public Action; której zadaniem będzie wypisanie na ekran liczby punktów życia celu. Jak wygląda wywołanie takiej akcji? ExamineAction a; a(player); Można by powiedzieć, że Action jest obiektem funkcyjnym - abstrakcją funkcji!
Dodaj Action do hierarchii dziedziczenia attack. Jak to zrobić szybko i bezboleśnie, nie zmieniając wiele kodu? Rozszerz Attack : public Action; W klasie Attack zaimplementuj teraz operator (): void operator(Creature& t) { this->doAttack(t); }
Plusami takiego rozwiązania są:
Teraz dodaj kilka akcji nie związanych z atakami, np. HealingAction, która zabiera manę a przywraca punkty życia. Możesz spróbować dodać odwrotną umiejętność, np. SacrificeAction, która zabiera życie a dodaje manę, itp
Zmodyfikuj kod głównej pętli programu, tak aby od teraz działał na akcjach, a nie na atakach. Dodaj w konstruktorach klas postaci przydzielanie umiejętności, w awansie także. Od teraz gra powinna stać się dużo ciekawsza.
Spraw aby potwory też korzystały z systemu akcji
Zauważ, że gracze inaczej korzystają z akcji niż potwory - potwory algorytmicznie wybierają swoją następną akcję (są sterowane przez komputer), dla gracza musimy napisać jakiś mechanizm komunikacji z wejściem/wyjściem od gracza. Mimo to, obie te funkcje mają robić w zasadzie to samo, tylko w inny sposób. Mogą mieć wspólną nazwę itp. Wszystko to sprawia że można umieścić wspólny interface tej operacji (funkcję wirtualną) we wspólnej klasie bazowej Creature. Od teraz wszystkie istoty w świecie mogą działać!
OOP (C++) - dziedziczenie wielokrotne
Zadania:
Przed przystąpieniem do wykonywania ćwiczeń upewnij się że masz co najmniej stworzone klasy: Adventurer, Warrior : public Adventurer, Mage : public Adventurer
Wymagane minimum kodu do rozwiązania ćwiczeńOOP (C++) - polimorfizm statyczny (szablony)
Zadania:
Ponieważ szablony są dość skomplikowanym pojęciem, szczególnie, jeśli używa się ich w dużych projektach, przykłady które zrealizujemy będą raczej klasyczne. Zadaniem jest utworzenie klasy Pojemnika (Container). Klasy Container będziemy mogli potem użyć w naszym projekcie do przechowywania kolekcji rozmaitych obiektów.OOP (C++) - wyjątki
Na początek weźmy naszą klasę Container z poprzednich ćwiczeń. Może być wersja bez template'ów - dla uproszczenia. Potem i tak proszę zrobić wersję z szablonem.
Do tej pory używaliśmy (i tak się zazwyczaj robi) wyjątków do obsługi sytuacji błędnych - odniesienie poza obszar tablicy, dodanie elementu ponad stan, itp. Wyjątki jednak, jak sama nazwa wskazuje nie służą tylko do obsługi błędów! (inaczej mielibyśmy mechanizm obsługi błędów). Do czego więc można jeszcze uzyć wyjątków?
Wróćmy teraz do samej gry RPG. Napiszmy mechanizm umozliwiający awans postaci. Gdzie umieścilibyśmy kod takiego awansu? Do tej pory awans polegał tylko na zwiększeniu poziomu (zmienna level). Jak dodać mozliwość wyboru nowych umiejętności, jak zmieniać maksymalną liczbę punktów wytrzymałości, itp? Jak zrobić, aby każda z klas miała własną obsługę awansu (różne listy umiejętności, inne atrybuty się zwiększają, itp.)?
Teoretycznie możemy to dodać właśnie w funkcji nextLevel(), w niej pytać uzytkownika o wybór umiejętności, itp. Funkcja mogłaby być wirtualna i moglibyśmy ją przeładowywać w podklasach...
... jednak jest pewien problem: zamykamy na przyszłość drogę do zmiany zasad w sposobie awansu, oraz zamykamy duża funkcjonalność związaną z interakcją z graczem w klasie, która powinna mieć z tym jak najmniej do czynienia (dlaczego?).
W tym miejscu możemy się zastanowić nad wyborem wyjątków do obsługi tego mechanizmu - w końcu awans postaci to bardzo wyjątkowa sytuacja. Możemy z klasy gracza wyrzucić wyjątek informujący o tym, że nastąpił awans. W ten sposób przekażemy sterowanie programu do nadrzędnego mechanizmu - w naszym przypadku będzie to RPGEngine. I właśnie on powinien się zając awansem (jesli ktoś grał kiedyś w papierową wersję RPG to wie, że wszystkie zasady związane z awansem są opisane właśnie w silniku gry - Zasadach:) )!
W ten sposób awansem postaci zajmie się podsystem, który jest do tego najlepiej przygotowany. W przyszłości, możemy wydać naszą grę RPG (pewne jej komponenty) jako bibliotekę, a innym programistom zostawić mozliwość zaimplementowania mechaniki awansu, itp. lub sami będziemy mogli tworzyć wiele różnych gier RPG, nie pisząc wiele kodu (złota zasada DRY - don't repeat yourself).
OOP (C++) - STL i Pojemniki (Containers)
Programowanie orientowane zdarzeniami na przykładzie Listener'ów Javy
PERL Składnia
PERL - wyrażenia regularne
Poniżej znajdują się zadania na wyrażenia regularne. Rozwiązanie każdego z nich można wysłać do mnie na mail (najlepiej na robsontpm[wiadomo]gmail[co]com), temat powinien być następujący: [dwucyfrowy numer zadania]. [Imię i nazwisko], np. 01. Robert Szczelina.
Spośród osób, które wyślą rozwiązania danego zadania jeden punkt z aktywności otrzyma osoba, która poda najkrótsze poprawne rozwiązanie. Jeśli będzie kilka najkrótszych rozwiązań, punkt otrzyma nadesłane najwcześniej.
Liczą się tylko rozwiązania wysłane w trakcie ćwiczeń (14:00 - 16:00).
Osoby, które wyślą co najmniej 5 poprawnych rozwiązań, bez względu na długość otrzymają dodatkowe 3 pkt. z aktywności.
Uwaga 1: w rozwiązaniach należy podać cały regex w notacji perlowej, tzn z / lub |, np. m/^[A-Z][a-z]*$/, czy s/^[A-Z][a-z]*$/to jest imie/g. Proszę pamiętać, że regexy mają przełączniki (np. /g) które zmieniają zachowanie i wolno, a nawet czasem trzeba je stosować.
Uwaga 2: Jeśli w którymś zadaniu jest napisane, że należy podać program w perlu, to do liczenia najkrótszego kodu liczy się suma długości użytych regexów.
Uwaga 3: Długością regexu "s/^[A-Z][a-z]*$/to jest imie/g" jest długość napisu pomiędzy " i ", w tym przypadku 30. W szczególności wliczają się parametry typu /g /i itp.
Tutaj można poczytać co nieco o regex'ach. Jako pomoc polecam także google.
Perl - programowanie obiektowe
Jeśli nie wykonałeś zadań z poprzedniego tygodnia, możesz to zrobić, jeśli skończysz pracować nad poniższymi zadaniami.
Podsumowanie - przykładowy test