Dzisiaj zajmiemy się napisaniem kolejnej gry - znanego wszystkim z komórek Nokii, Snake'a. Wykorzystamy kod, który napisaliśmy już przy okazji Tetrisa.
Nauczymy się także korzystać z przestrzeni nazw System.Generics, która
zawiera typowe struktury danych, takie jak stos, kolejka czy lista. My
wykorzystamy listę dwukierunkową (LinkedList<>).
Na początku trochę o tej właśnie liście. Jej deklaracja wygląda następująco:
System.Generics.LinkedList<T> nazwa_zmiennej;
T jest typem przechowywanych wartości. W tej strukturze danych
przechowywane są elementy LinkdeListNode, które zawierają właściwe
zmienne, które chcemy pamiętać (właściwość Value - jest ona typu T).
Dane są ułożone w określonej kolejności. Każdy element wie jaki jest jego
poprzednik i następnik (właściwości Next, Previous). Sama lista
posiada z góry zdefiniowane właściwości, z których możemy swobodnie korzystać. Przechowuje
ona np. swój początek oraz koniec (właściwości First oraz Last).
W tej strukturze danych dzięki połączeniu pomiędzy sąsiednimi elementami, możemy dostać się do dowolnej
wartości przesuwając się po elementach w prawo lub w lewo. Mamy do dyspozycji szereg metod do obsługi listy. Między innymi
AddFirst, AddNext, Find, Remove. Po szczegóły odsyłam
do dokumentacji
MSDN. LinkedList będziemy używać do przechowywania węża w
naszej grze. Jest to dość intuicyjne. Pierwszy element odpowiada głowie, a
kolejne - kolejnym segmentom gada. Ostatni z elementów to końcówka ogona.
Zajmijmy się teraz grą. Tworzymy nowy projekt, nazywamy go Snake.
Zmieniamy nazwę formy na frmSnake, we właściwości Text wpisujemy
Snake. Rozmiary ustawiamy na 371x200, AutoSizeMode na
GrowAndShrink, a MaximizeBox na false - nie chcemy, aby można
było zmieniać rozmiar lub maksymalizować okno aplikacji. Do formy dodajemy 5
etykiet. Dwie z nich nazywamy lblPoziom i lblPunkty, nazwy
pozostałych są nieistotne. Zmieniamy położenie lblPoziom na 325x9 i
wpisujemy tekst 9. Umiejscowienie lblPunkty to 325x32, tekst to
0 (zero). Do dwóch z pozostałych etykiet wpisujemy Poziom i Punkty,
a lokacje ustawiamy odpowiednio na 281x9 oraz 281x32. Do ostatniej kontrolki
wpisujemy:
Enter - Start
Spacja - Pauza
Escape - Koniec
1...9 - Poziom
Posłuży nam ona ze legendę. Jej położenie to 381x84. Teraz dodajemy
kontrolkę Timer i nadajemy jej nazwę licznik - będzie ona
licznikiem czasu. Na końcu ustawiamy jeszcze właściwość formy
KeyPreview na true. Dzięki temu forma będzie przechwycać zdarzenia
powiązane z klawiaturą od wszystkich kontrolek w niej zawartych. To na razie tyle jeśli
chodzi o wygląd aplikacji. Przejdźmy teraz do stworzenia klasy reprezentującej
planszę do gry.

Najpierw dodajemy do projektu pliki, których używaliśmy przy okazji tworzenia
Tetrisa - kratka.cs i Plansza.cs, artykuł ten znajdziemy tutaj. Dokonujemy niewielkiej modyfikacji w kratka
- zmieniamy stałą wymiar na 10. Następnie tworzymy nową klasę i nazywamy ją
Plansza_Snake. Klasa ta będzie dziedziczyć po klasie Plansza
(podobnie jak Plansza_Tetris w Tetrisie). Plansza reprezentuje
podstawowe okno gry. Można w niej rysować poszczególne kratki, odświeżyć lub
wyczyścić ekran. Dopiero Plansza_Snake będzie zawierać węża, metody
służące do poruszania nim i pilnujące zasad gry. Jednak zanim się nią
zajmiemy, stwórzmy klasę Waz. Oto jej pełna treść:
using System.Collections.Generic;
using System.Drawing;
public class Waz
{
public LinkedList<Point> punkty;
public Color kolor;
public Waz() : this(Color.Azure) { }
public Waz(Color k)
{
punkty = new LinkedList<Point>();
kolor = k;
for (int i = 0; i < 5; i++)
{
punkty.AddLast(new Point(10 + i, 6));
}
}
public void rysuj(Graphics g)
{
foreach (Point buf in punkty)
{
(new
kratka(buf.X, buf.Y, Color.Red)).rysuj(g);
}
}
}
Jak widać zawiera ona element LinkedList<Point>, który posłuży nam do
zapamiętania położenia poszczególnych segmentów węża oraz zmienną, w której
pamiętamy kolor. Ponadto potrafi się narysować. Możemy już zamknąć ten plik.
Teraz wracamy do Plansza_Snake.cs Wewnątrz klasy deklarujemy wyliczenie
public enum kierunki { gora, dol, lewo, prawo }; Przyda nam się
przy oprogramowywaniu ruchu węża. Dodajemy następujące zmienne:
private Waz figura;
public kierunki kierunek = kierunki.lewo;
public bool gra = false;
public bool pauza = false;
public bool przedluz = false;
public bool nowy_element = false;
private Color kolor_nowych = Color.MediumSeaGreen;
private Random losuj;
Reprezentują one kolejno: węża, kierunek aktualnego ruchu, informację czy odbywa
się jakaś gra. Następnie: czy jest włączona pauza, czy w kolejnym ruchu wąż się
przedłuży, czy w kolejnym ruchu pojawi się nowa kratka do zebrania oraz kolor
tych właśnie kratek. Ostatnia zmienna losuj posłuży nam do wybierania
losowych współrzędnych dla "pożywienia" dla węża. Nie licząc konstruktorów klasa
Plansza_Snake zawiera tylko 3 metody. pierwsza z nich -
nowywaz
tworzy nowego węża, zapamiętuje jego pozycję oraz rysuje go. Ponadto wyświetla
pierwsze jedzenie dla gada w postaci kratki o innym niż on sam kolorze. Kolejna
funkcja - ruch - jest niezmiernie ważna. Odpowiada ona za poruszanie się
węża. Sprawdza również, czy wąż nie zjadł czegoś i wtedy przedłuża go, w
przeciwnym wypadku jedynie go przesuwa. Ponadto kontroluje czy wąż w coś nie
uderzył, a jeśli tak - gra jest kończona. Metoda
losowewspolrzedne zwraca
punkt o losowych współrzędnych mieszczących się na ekranie gry oraz nie
kolidujących z już zarysowanymi kratkami. Możemy już zamknąć ten plik.
Zajmiemy się teraz oprogramowaniem formy frmSnake. Otwieramy plik
frmSnake.cs z widokiem na kod. Do klasy dodajemy zmienne:
private Plansza_Snake okno;
int Poziom;
int Punkty;
Oczywistym jest do czego służą. Na końcu konstruktora dopisujemy:
okno = new Plansza_Snake(24, 13);
this.okno.Location = new System.Drawing.Point(15, 15);
this.okno.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.okno.Name = "okno";
this.okno.TabIndex = 0;
Controls.Add(okno);
Poziom = int.Parse(lblPoziom.Text);
Punkty = int.Parse(lblPunkty.Text);
W ten sposób dodajemy do formy naszą własną kontrolkę - Plansza_Snake
oraz inicjujemy zmienne Poziom i Punkty wartościami wpisanymi w etykietach
im odpowiadających. Następnie tworzymy 3 zdarzenia. Pierwsze to Paint dla
naszej formy. W jego wnętrzu znajduje się tylko jedna komenda:
okno.odswiez();
Jej zadaniem jest odświeżenia okna z grą. Kolejne zdarzenie to Tick dla
licznika. Zajmuje się ono podliczaniem punktów oraz przesuwaniem węża w
kolejnych odcinkach czasu. Jeśli gra się zakończyła, to wyświetlany jest
odpowiedni komunikat. Ostatnie zdarzenie KeyDown dla formy reaguje na
naciśnięcia klawiszy. Strzałki oczywiście odpowiadają zmianie kierunku ruchu
węża i pozostałym opcjom opisanym w legendzie. Oprócz tego sprawdzane jest czy gra się
w ogóle odbywa tak, aby np. po wciśnięciu klawisza enter podczas gry, ta nie zaczęła się
od nowa.
I tak oto ukończyliśmy kolejną grę - Snake'a. Zrobiliśmy to tak szybko i
łatwo, gdyż większość pracy wykonaliśmy podczas pisania Tetrisa. Tam właśnie
powstała klasa Plansza, która posłużyła jako prototyp dla planszy do gry
dla tych dwóch gier. Jedyne czym musieliśmy się zająć to oprogramowanie
algorytmów kontrolujących zasady gry, poruszanie się węża i pozostałych
elementów charakterystycznych tylko dla danej gry. Podobnie było przy Tetrisie. Na tym
właśnie polega moc dziedziczenia w językach obiektowych ;) - raz napisana klasa
niejednokrotnie przydaje się w różnych projektach, co znacznie oszczędza nasz
czas i ułatwia pracę.