TOC

This article is currently in the process of being translated into Polish (~99% done).

Tworzenie Gry: SnakeWPF:
Chapter introduction:

In this article series, we're building a complete Snake game from scratch. It makes sense to start with the Introduction and then work your way through the articles one by one, to get the full understanding.

If you want to get the complete source code for the game at once, to get started modifying and learning from it right now, consider downloading all our samples!

Collision Detection

Teraz skoro zaimplementowaliśmy przestrzeń gry, jedzenie i węża jak i jego ciągłe poruszanie się, potrzebujemy jeszcze jednej, finalnej rzeczy aby to wyglądało i działało jak prawdziwa gra: detekcji kolizji . Koncept opiera się na ciągłym sprawdzaniu czy wąż w coś uderzył i potrzebujemy tego z dwóch powodów: aby zobaczyć czy wąż właśnie zjadł jedzenie czy też uderzył w przeszkodę (ścianę albo własny ogon)

Metoda DoCollisionCheck()

Detekcja kolizji zostanie wykorzystania w metodzie nazwanej DoCollisionCheck() więc musimy ją zaimplementować. Oto jak powinna obecnie wyglądać:

private void DoCollisionCheck()
{
    SnakePart snakeHead = snakeParts[snakeParts.Count - 1];
   
    if((snakeHead.Position.X == Canvas.GetLeft(snakeFood)) && (snakeHead.Position.Y == Canvas.GetTop(snakeFood)))
    {        
EatSnakeFood();
return;
    }

    if((snakeHead.Position.Y < 0) || (snakeHead.Position.Y >= GameArea.ActualHeight) ||
(snakeHead.Position.X < 0) || (snakeHead.Position.X >= GameArea.ActualWidth))
    {
EndGame();
    }

    foreach(SnakePart snakeBodyPart in snakeParts.Take(snakeParts.Count - 1))
    {
if((snakeHead.Position.X == snakeBodyPart.Position.X) && (snakeHead.Position.Y == snakeBodyPart.Position.Y))
    EndGame();
    }
}

Jak jak wspominałem, wykonujemy dwa sprawdzenia: pierwsze aby zobaczyć czy obecna pozycja głowy węża pasuje do pozycji obecnego kawałka jedzenia. Jeżeli tak to wywołujemy metodę EatSnakeFood() (więcej o niej później). Wtedy sprawdzamy czy pozycja głowy węża wykracza poza granice przestrzeni gry aby zobaczyć czy wąż jest w drodze wyjścia przez jedną z granic. Jeżeli tak jest, to wywołujemy metodę EndGame(). Na końcu sprawdzamy czy głowa węża styka się z jedną z części ciała - jeżeli tak, to znaczy że wąż uderzył w swój własny ogon, co również kończy grę wraz z wywołaniem EndGame()

Metoda EatSnakeFood()

Metoda EatSnakeFood() jest odpowiedzialna za wykonanie kilku rzeczy, ponieważ od razu jak wąż zje kawałek jedzenia, musimy dodać nowy kawałek, w nowej lokacji, jak i dostosować wynik, długość węża i obecną prędkość gry. Dla wyniku musimy zadeklarować nową lokalną zmienną nazwaną currentScore

public partial class SnakeWPFSample : Window  
{  
    ....  
    private int snakeLength;  
    private int currentScore = 0;  
    ....

Mając to na miejscu, dodajmy metodę EatSnakeFood():

private void EatSnakeFood()
{
    snakeLength++;
    currentScore++;
    int timerInterval = Math.Max(SnakeSpeedThreshold, (int)gameTickTimer.Interval.TotalMilliseconds - (currentScore * 2));
    gameTickTimer.Interval = TimeSpan.FromMilliseconds(timerInterval);    
    GameArea.Children.Remove(snakeFood);
    DrawSnakeFood();
    UpdateGameStatus();
}

Jak wspomnieliśmy, dzieje się tutaj kilka rzeczy:

  • Zwiększamy wartości zmennych snakeLength i currentScore o 1 aby pokazać fakt, że wąż właśnie zjadł kawałek jedzenia
  • Ustawiamy Interval gameTickTimer z wykorzystaniem następującej zasady: obecny currentScore jest mnożony przez 2 a następnej odejmowany od obecnego intervalu (prędkości). To sprawia, że prędkość rośnie wykładniczo wraz z długością węża, co sprawia że gra staje się coraz trudniejsza. Wcześniej zdefiniowaliśmy dolną granicę prędkości w stałej SnakeSpeedThreshold co oznacza że prędkość gry nigdy nie spadnie poniżej prędkości 100 ms
  • Wtedy usuwamy właśnie zjedzony przez węża kawałek jedzenia i wywołujemy metodę DeawSnakeFood() która to dodaje nowy kawałek jedzenia w nowym miejscu.
  • Na końcu wywołujemy metodę UpdateGameStatus(), która wygląda w następujący sposób:
private void UpdateGameStatus()
{
    this.Title = "SnakeWPF - Score: " + currentScore + " - Game speed: " + gameTickTimer.Interval.TotalMilliseconds;
}

Ta metoda po prostu będzie aktualizowała właściwość Title należąca do Window aby odzwierciedlić obecny wynik i prędkość gry. Jest to łatwy sposób na pokazanie obecnego stanu, który to może być rozszerzany w przyszłości, jeśli zajdzie taka potrzeba.

Metoda EndGame()

Potrzebujemy też kawałka kodu który będzie wywoływany kiedy gra powinna się skończyć. Zrobimy to z wykorzystaniem metody EndGame() , która to jest obecnie wywoływana w metodzie DoCollisionCheck(). Jak możesz zobaczyć, jest narazie bardzo prosta:

private void EndGame()
{
    gameTickTimer.IsEnabled = false;
    MessageBox.Show("Oooops, you died!\n\nTo start a new game, just press the Space bar...", "SnakeWPF");
}

Oprócz pokazania wiadomości do użytkownika o niefortunnym odejściu naszego ukochanego węża, zatrzymamy też gameTickTimer. Ponieważ ten timer jest przyczyną działania wszystkich rzeczy w grze, to od razu jak będzie zatrzymany to cały ruch i tworzenie również się zatrzyma.

Ostatnie poprawki

Jesteśmy już prawie gotowi z pierwszym wydaniem w pełni działającej gry w Węża - w zasadzie to musimy zrobić dwie pomniejsze poprawki. Po pierwsze, musimy się upewnić że metoda DoCollisionCheck() jest wywoływana - to powinna być ostania akcja wykonywana w metodze MoveSnake() , którą zaimplementowaliśmy wcześniej:

private void MoveSnake()
{
    .....
   
    //... and then have it drawn!
    DrawSnake();
    // Finally: Check if it just hit something!
    DoCollisionCheck();    
}

Teraz detekcja kolizji jest wykonywana od razu jak wąż się poruszył! Pamiętasz jak mówiłem ci że zaimplementowaliśmy prostą wersję metody StartNewGame() . Musimy ją trochę rozbudować aby upewnić się że resetujemy wynik za każdym razem kiedy gra jest (re)startowana, a także kilka innych rzeczy. Więc zamieńmy metodę StartNewGame() na tą nieco rozbudowaną wersję:

private void StartNewGame()
{
    // Remove potential dead snake parts and leftover food...
    foreach(SnakePart snakeBodyPart in snakeParts)
    {
if(snakeBodyPart.UiElement != null)
    GameArea.Children.Remove(snakeBodyPart.UiElement);
    }
    snakeParts.Clear();
    if(snakeFood != null)
GameArea.Children.Remove(snakeFood);

    // Reset stuff
    currentScore = 0;
    snakeLength = SnakeStartLength;
    snakeDirection = SnakeDirection.Right;
    snakeParts.Add(new SnakePart() { Position = new Point(SnakeSquareSize * 5, SnakeSquareSize * 5) });
    gameTickTimer.Interval = TimeSpan.FromMilliseconds(SnakeStartSpeed);

    // Draw the snake again and some new food...
    DrawSnake();
    DrawSnakeFood();

    // Update status
    UpdateGameStatus();

    // Go!    
    gameTickTimer.IsEnabled = true;
}

Kiedy rozpoczyna się nowa gra, teraz dzieją się następujące rzeczy:

  • Ponieważ to może nie być nasza pierwsza gra, to musimy się upewnić że wszystkie pozostałe rzeczy z poprzedniej gry są usunięte, w tym wszystkie istniejące części węża jak i pozostałe jedzenie.
  • Musimy także zresetować niektóre zmienne to ich oryginalnych wartości np. wynik, długość węża, kierunek jak i szybkość . Musimy też dodać nową głowę węża (która będzie automatycznie rozszerzenie przez metodę MoveSnake()
  • Wtedy wywołujemy metody DrawSnake() i DrawSnakeFood() aby wizualnie pokazać że zaczęła się nowa gra.
  • Wtedy wywołujemy metodę UpdateGameStatus()
  • I w końcu jesteśmy gotowi aby wystartować gameTickTimer - od razu znacznie tykać, co w zasadzie wprawi grę w ruch

Podsumowanie

Jeżeli przeszłeś przez wszystkie artykuły to gratulacje! - właśnie zbudowałeś swoją pierwszą grę w WPF-ie! . Naciesz się swoją ciężką pracą poprzez uruchomienie projektu, wciśnięcie Spacji i rozpoczęcie gry - nawet w tej prostej implementacji, Wąż jest fajną i uzależniającą grą!


This article has been fully translated into the following languages: Is your preferred language not on the list? Click here to help us translate this article into your language!