Cele lekcji
Po zakończeniu tej lekcji…
- Będziesz potrafić zaplanować funkcję.
- Dowiesz się, jak utworzyć kod/logikę funkcji.
- Poznasz kilka najbardziej popularnych poleceń w języku C#.
Sporo już czasu poświęciliśmy na tworzenie elementów składowych naszej gry. Teraz czas wprawić ją w ruch. Najpierw dodamy kod obsługujący przechodzenie gracza do nowej lokacji.
Powiem tylko, że ta lekcja jest długa i dowiesz się w niej wielu nowych rzeczy, więc przygotuj się.
Opis funkcji
Na początek musimy określić, co dzieje się podczas ruchu.
Pisząc funkcję, która wykonuje wiele operacji (tak jak ta), najpierw planuję i przygotowuję wszystkie opcje.
Gdy gracz przechodzi do nowej lokacji, jego zdrowie jest w całości odnawiane, gra sprawdza, czy w lokacji są zadania do wykonania (i czy gracz jest je w stanie wykonać) i czy znajdują się w niej potwory.
Tak wygląda logika funkcji poruszania się gracza. Taki opis często nazywa się „pseudo kodem”. Nie jest to kod C#, ale opis obrazujący, co kod będzie wykonywał.
Każdy poziom wcięcia oznacza obsługę innego warunku, np. operacje do wykonania w przypadku napotkania potwora czy operacje wykonywane, kiedy w lokacji nie ma potwora.
- Czy wejście do lokacji wymaga przedmiotu?
- Czy gracz nie ma przedmiotu?
- Wyświetl komunikat
- Nie pozwalaj wejść graczowi (zatrzymaj przetwarzanie ruchu)
- Czy gracz nie ma przedmiotu?
- Nie aktualizuj bieżącego położenia gracza
- Wyświetl nazwę lokacji i opis
- Pokaż/ukryj dostępne przyciski ruchów
- W pełni ulecz gracza (zakładamy, że gracz odpoczął/uzdrowił się podczas ruchu)
- Zaktualizuj punkty wytrzymałości w interfejsie użytkownika
- Czy lokacja zawiera zadanie?
- Jeśli tak, to czy gracz już przyjął zadanie?
- Jeśli tak, to czy zadanie zostało ukończone?
- Jeśli nie, czy gracz ma przedmioty potrzebne do wykonania zadania?
- Jeśli tak, wykonaj zadanie
- Wyświetl komunikaty
- Usuń przedmioty potrzebne do wykonania zadania z ekwipunku
- Daj nagrodę za zadanie
- Zaznacz zadanie jako wykonane
- Jeśli tak, wykonaj zadanie
- Jeśli nie, czy gracz ma przedmioty potrzebne do wykonania zadania?
- Jeśli nie, daj graczowi zadanie
- Wyświetl komunikat
- Dodaj zadanie do listy zadań gracza
- Jeśli tak, to czy zadanie zostało ukończone?
- Jeśli tak, to czy gracz już przyjął zadanie?
- Czy w lokacji znajduje się potwór?
- Jeśli tak,
- Wyświetl komunikat
- Wygeneruj nowego potwora gotowego do walki
- Wyświetl formanty combobox i przyciski walki
- Ponownie uzupełnij formanty combobox, jeśli zawartość ekwipunku uległa zmianie
- Jeśli nie
- Ukryj formanty combobox i przyciski walki
- Jeśli tak,
- Odśwież zawartość ekwipunku gracza w interfejsie użytkownika, jeśli uległ zmianie
- Odśwież listę zadań gracza w interfejsie użytkownika, jeśli uległa zmianie
- Odśwież formant ComboBox cboWeapons w interfejsie użytkownika
- Odśwież formant ComboBox cboPotions w interfejsie użytkownika
Tworzenie funkcji współdzielonej
Mamy cztery różne funkcje dotyczące ruchu – każda odpowiada za ruch w jednym kierunku – i musimy wykonać poniższe operacje, aby poruszać się w każdym kierunku.
Można to zrobić, pisząc taki sam kod w każdej funkcji, ale jeśli będziemy kiedyś chcieli zmienić tę logikę, zmiany trzeba będzie wprowadzać w czterech miejscach, co często prowadzi do błędów.
Stworzymy więc nową współdzieloną funkcję MoveTo obsługującą ruch do dowolnej lokacji. Każda z czterech funkcji ruchu będzie wywoływać tę nową funkcję.
Przechowywanie lokalizacji gracza
Aktualną lokalizację gracza również musimy gdzieś zapisać, a skoro ta wartość będzie się zmieniać, musimy przechowywać ją w zmiennej lub we właściwości.
W tym przypadku stworzymy właściwość w klasie Player. Ma to swój sens, ponieważ lokacja gracza jest „właściwością” gracza (w sensie ogólnym).
Przechowywanie aktualnego potwora
Jeśli gracz przemieści się do lokacji z potworem, to aktualny potwór również musi być gdzieś przechowywany.
Nasza klasa World zawiera listę wszystkich potworów w grze, ale z tej klasy nie można ich przenieść do walki. W tej klasie istnieje tylko jedna „instancja” każdego potwora, więc, gdyby gracz odbył walkę ze szczurem z listy World.Monsters i go zabił, kolejna walka ze szczurem już nie mogłaby się odbyć, bo został on zabity wcześniej.
Po przejściu do nowej lokacji, w której znajduje się potwór, zostanie utworzona nowa instancja tego typu potwora, która następnie zostanie zapisana w zmiennej. Gracz będzie walczył z tą instancją potwora.
Wiemy już, co chcemy osiągnąć, czas zacząć pisać kod.
Tworzenie funkcji pozwalającej na poruszanie się gracza
Etap 1: Uruchom aplikację Visual Studio i otwórz swoje rozwiązanie.
Etap 2: Najpierw utworzymy nową właściwość do przechowywania aktualnego położenia gracza.
Kliknij dwukrotnie klasę Player w projekcie Engine. Dodaj nową właściwość o nazwie CurrentLocation, której typem danych jest Location.
public Location CurrentLocation { get; set; }
UWAGA: Właściwości nie trzeba podawać w żadnej konkretnej kolejności. Ja po prostu lubię wstawiać właściwości listy na końcu właściwości. Łatwiej mi odczytać taki zapis, kiedy są one pogrupowane w tym samym miejscu w każdej klasie.
Etap 3: Kliknij prawym przyciskiem myszy formularz SuperAdventure.cs w projekcie SuperAdventure, a następnie wybierz pozycję View Code.
Etap 4: Ponieważ musimy napisać dużo kodu, a o pomyłkę bardzo łatwo, warto skopiować nowy kod do formularza SuperAdventure spod tego adresu (https://gist.github.com/ScottLilly/208630cfcdded1cbfdc0) i zastąpić cały aktualny kod w formularzu SuperAdventure.cs.
Co znajduje się w dodanym kodzie?
W wierszu 18. dodaliśmy nową zmienną do przechowywania potwora, z którym gracz walczy w aktualnej lokacji.
W konstruktorze formularza wprowadziliśmy kilka operacji pozwalających rozpocząć grę.
W wierszu 25. gracz jest „przenoszony” do domu. Funkcja MoveTo oczekuje lokacji jako parametru, więc za pomocą funkcji World.GetLocationByID() musimy uzyskać właściwą lokację. Tutaj używamy stałej World.LOCATION_ID_HOME, zamiast wartości 1. O wiele łatwiej jest zrozumieć kod ze stałą, wiedząc, do czego ona służy, gdy używamy tej jednoznacznie nazwanej stałej.
W wierszu 26. dodaliśmy do ekwipunku gracza przedmiot – zardzewiały miecz. Potrzebna jest jakaś broń do walki z pierwszym potworem.
Dodaliśmy też nową funkcję MoveTo służącą do obsługi wszystkich ruchów gracza.
Zagłębiliśmy się również w każdą z czterech funkcji obsługujących ruchy w różnych kierunkach i określiliśmy wywoływanie przez nie funkcji MoveTo.
Funkcja MoveTo
Na początek w tej funkcji rzucają się w oczy wiersze zawierające znaki „//”. To komentarze, które są ignorowane przez komputer. Potrzebne są one programistom, aby wiedzieli, co dany kod robi. Teksty zapisane po podwójnym ukośniku (aż do końca wiersza) są całkowicie ignorowane przez komputer.
W tej funkcji wprowadziłem znacznie więcej komentarzy niż robi się to standardowo. Ma to na celu ułatwienie Ci śledzenia tego, co się dzieje i zrozumienie, w jaki sposób kod jest powiązany z powyższym pseudokodem.
Drugi ciekawy aspekt to długość funkcji – ma ona ponad 300 wierszy.
To bardzo długa funkcja.
W przypadku rozwlekłych funkcji bardzo ciężko jest określić jej zadanie. W kolejnej lekcji postaramy się ją skrócić. Osobiście staram się pisać funkcje składające się z 10-40 wierszy,
więc podczas następnej lekcji podzielimy tę funkcję na mniejsze elementy.
Co dzieje się w funkcji MoveTo()?
W wierszu 57. pojawia się pierwsza instrukcja „if”.
Pozwala ona sprawdzić, czy wejście do nowej lokacji wymaga podsiadania przez gracza jakichś przedmiotów.
Zapis „!=” w języku C# oznacza „nie równa się”. Wykrzyknik zastosowany w dowolnym porównaniu w języku C# stanowi negację, a „null” oznacza nic/zbiór pusty.
Więc, jeśli właściwość lokalizacji ItemRequiredToEnter nie jest pusta, musimy sprawdzić, czy w ekwipunku gracza znajduje się wymagany przedmiot. Jeśli jest pusta, nie trzeba nic robić – żaden przedmiot nie jest wymagany, więc gracz może zawsze przenieść się do nowej lokacji.
W wierszu 72. sprawdzamy, czy w ekwipunku gracza znajduje się wymagany przedmiot. Jeśli nie znaleźliśmy przedmiotu o wymaganym ID, wartością zmiennej playerHasRequiredItem nadal będzie false.
Przed zmienną playerHasRequiredItem wstawiony jest wykrzyknik.
Załóżmy, że gracz nie posiada wymaganego przedmiotu w swoim ekwipunku. Wtedy zmienna playerHasRequiredItem będzie miała wartość false. Wykonanie negacji na zmiennej logicznej powoduje odwrócenie jej wartości: !true (nie prawda) równa się false (fałsz), a !false (nie fałsz) oznacza true (prawda).
Myślenie na zasadzie „nie jest fałszem” nie jest tak jednoznaczne jak określenie „jest prawdą”, niemniej obydwa te stwierdzenia znaczą to samo.
W wierszu 75. wyświetlany jest komunikat o braku przedmiotu pozwalającego na wejście do danej lokacji. Ten wiersz jest oznaczony nowym symbolem „+=”.
Jego znaczenie jest następujące: pobierz wartość ze zmiennej/właściwości po lewej stronie, dodaj do niej wartość po prawej stronie i przypisz wyniki z powrotem do zmiennej/właściwości po lewej stronie.
Użycie symboli „+=” w przypadku ciągu oznacza: dodaj wartość ciągu po prawej stronie do końca istniejącego ciągu. Jeśli używasz ich w połączeniu z liczbą, ich znaczenie jest następujące: dodaj wartość po prawej stronie do wartości po lewej.
My na końcu tekstu w polu rtbMessages RichTextBox dodamy nowy komunikat. W ten sposób gracz może przeglądać stare komunikaty. Jeśli zamiast tego użyjemy znaku „=”, zastąpi on istniejącą wartość Text z użyciem naszego nowego komunikatu.
Mamy też polecenie Environment.NewLine, które dodajne znak końca wiersza do tekstu, więc następny dodany tekst będzie wyświetlany w kolejnym wierszu, a nie na końcu bieżącego wiersza.
Wiersz 76. kończy się poleceniem return, które mówi programowi „wyjdź z tej funkcji”.
Ponieważ ta funkcja jest funkcją void (patrz wiersz 54), nie zwraca ona wartości. Możemy więc tu „wrócić” i nie wykonywać reszty operacji w funkcji. Jest to ważne w przypadku, gdy gracz nie posiada przedmiotu wymaganego do przejścia do lokacji. Wtedy więc, nie chcemy wykonywać pozostałych operacji w funkcji, które powodują przeniesienie gracza do tej lokacji.
Operacje w wierszach 84-87 powodują pokazanie lub schowanie przycisków ruchu, w zależności od tego, czy w nowej lokacji można się poruszać w danych kierunkach. Robimy to poprzez sprawdzenie, czy właściwość dla danej lokacji jest pusta czy nie.
Właściwość Visible przycisków oczekuje wartości logicznej: true lub false.
Więc w wierszu 84., jeśli właściwość LocationToNorth nie jest pusta, wartość po prawej stronie znaku równości jest określana jako true, a przycisk jest widoczny. Jeśli właściwość LocationToNorth jest pusta, zostanie ona określona jako false, a przycisk nie będzie widoczny.
W wierszu 100. sprawdzamy, czy w danej lokalizacji dostępne jest zadanie. Jeśli tak, to czeka nas więcej pracy.
W wierszach 106-117 przeglądamy listę zadań gracza, aby sprawdzić, czy rozpoczął on już zadanie w tej lokacji i czy zostało ono wykonane.
Wiersze 128-163 pozwalają przejrzeć przedmioty potrzebne do wykonania zadania, a następnie sprawdzić każdy przedmiot w ekwipunku gracza, aby określić, czy gracz go posiada i czy ilość danego przedmiotu jest wystarczająca do wykonania zadania.
W pętlach foreach znajdują się instrukcje break służące do zatrzymywania wykonywania przeglądu przedmiotów i wyjścia z foreach. Jeśli program określi, że gracz nie ma jednego przedmiotu lub wystarczającej ilości przedmiotów pozwalającej ukończyć zadanie, można zatrzymać wyszukiwanie innych przedmiotów.
Wiersz 180 zawiera znaki „-=”. Znaki „+=” mówią: dodaj wartość z prawej strony do zmiennej/właściwości po lewej stronie; więc znaki „-=” muszą mówić: odejmij wartość z prawej strony od zmiennej/właściwości po lewej stronie. Tych znaków można używać tylko z liczbami i nie można ich stosować z ciągami (w przeciwieństwie do znaków „+=”).
W tym przypadku używamy ich do usuwania przedmiotów z ekwipunku gracza, kiedy gracz oddaje przedmioty w celu zakończenia zadania.
W wierszu 204. znajdują się znaki „+++”. Wstawienie ich po zmiennej lub właściwości mówi: dodaj 1 do tej zmiennej lub właściwości. Można również stosować znak „–” pozwalający na odjęcie 1 od zmiennej lub właściwości.
W wierszach 264-273 tworzymy nowego potwora, określając nowy obiekt potwora z użyciem wartości pobranych ze standardowego potwora w naszej klasie World.
Aktualizacja formantów DataGridView w interfejsie użytkownika
W wierszach 290-321 aktualizujemy formanty DataGridView w interfejsie użytkownika.
Zawartość ekwipunku ulegnie zmianie po zakończeniu zadania przez gracza. Przedmioty, które gracz musi przekazać, zostaną usunięte z ekwipunku, natomiast nagroda zostanie dodana. Musimy więc zaktualizować interfejs użytkownika, aby pokazywał on rzeczywistą zawartość ekwipunku.
Ponadto jeśli gracz rozpoczął lub ukończył zadanie, lista zadań również ulegnie zmianie.
Aktualizacja formantów ComboBox w interfejsie użytkownika
W przypadku formantów ComboBox w interfejsie użytkownika utworzymy nowe listy zawierające określone typy danych przedmiotów, które są wyświetlane na liście (wiersze 324 i 353). Następnie przechodzimy przez wszystkie przedmioty w ekwipunku gracza i dodajemy je do tych list, jeśli ich typ danych jest prawidłowy (wiersze 326-335 i 355-364).
W wierszach 328 i 357 pojawia się nowe porównanie – is – pozwalające sprawdzić, czy obiekt stanowi określony typ danych.
Pamiętasz, jak tworzyliśmy podklasy Weapon i HealingPotion dla klasy Item? Typem danych obiektu Weapon jest jednocześnie Weapon i Item, a typem danych obiektu HealingPotion jest zarówno HealingPotion, jak i Item.
Jeśli listy są puste (weapons.Count lub healingPotions.Count są równe 0), ukrywamy formant ComboBox i przyciski „używania”, ponieważ gracz nie ma broni ani mikstury, którą można użyć.
Jeśli lista zawiera pozycje, „wiążemy” ją z formantami combobox (wiersze 345-349 i 374-378). Polecenie DisplayMember określa właściwość wyświetlaną w formantach combobox. W tym przypadku chcemy wyświetlić wartość właściwości Name. ValueMember to wartość „zakulisowa”, której użyjemy później. Pozwala ona określić, która pozycja została wybrana.
UWAGA: Listy właściwości z formantami DataGridView i ComboBox można łączyć w sprytniejszy sposób, ale w naszych lekcjach skupimy się tylko na podstawowych metodach.
UWAGA: Mam nadzieję, że coś w nazwach zmiennych Cię zaciekawiło. Są one zazwyczaj długie i opisowe, a dzięki temu łatwo zrozumieć, jakie wartości powinny one zawierać. Opisowe nazw zmiennych ułatwiają tworzenie programu, zwłaszcza na etapie usuwania błędów lub wprowadzania zmian w przyszłości.
W kilku przypadkach pętli foreach używam krótkich nazw zmiennych takich jak qci oraz ii,
ponieważ pętla foreach składa się tylko z kilku wierszy. Zmienna działa przez bardzo krótki czas – jej działanie obowiązuje tylko w tych kilku wierszach pętli. Całą pętlę można swobodnie wyświetlić na ekranie bez przewijania, więc łatwo określić, gdzie dana zmienna jest wypełniana i używana. Gdyby pętla była dłuższa, użyłbym bardziej opisowej dłuższej nazwy.
Podsumowanie
Wiesz już, jak zaplanować logikę w pseudokodzie i utworzyć w programie funkcję wykonującą tę logikę.
Rozumiesz już, w jaki sposób pętle if, else i foreach są używane w funkcji.
A ponadto wiesz, jak wygląda ogromna funkcja i jak trudno pracuje się z takimi funkcjami. W następnej lekcji postaramy się skrócić taką funkcję i lepiej zrozumieć jej działanie.
W tej lekcji wprowadziliśmy dużo nowego materiału i jeśli coś nie jest jasne, napisz komentarz, a ja postaram się dodać więcej informacji.
Łącza do tej lekcji