Snake w C#

Snake w C#

Autor: Marcin Hałaczkiewicz

Opublikowano: 4/28/2007, 12:00 AM

Liczba odsłon: 77798

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ę.

Jak wykorzystać Copilot w codziennej pracy? Kurs w przedsprzedaży
Jak wykorzystać Copilot w codziennej pracy? Kurs w przedsprzedaży

Wydarzenia