16.1: Funkcja umożliwiająca poruszanie się gracza

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)
  • 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 nie, daj graczowi zadanie
        • Wyświetl komunikat
        • Dodaj zadanie do listy zadań gracza
  • 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
  • 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

Kod źródłowy w serwisie GitHub

Kod źródłowy w serwisie Dropbox

Leave a Reply

Your email address will not be published. Required fields are marked *