This article has been localized into Czech by the community.
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!
Detekce kolize
Nyní, když jsme implementovali herní plochu, jídlo a hada, stejně jako neustálý pohyb hada, potřebujeme pouze jednu poslední věc, aby to vypadalo a chovalo se jako skutečná hra: Detekci srážek. Koncept spočívá v neustálém kontrolování, zda náš had právě nenarazil na něco a momentálně detekci potřebujeme pro dva účely: Zjistit, zda had právě snědl nějaké jídlo, nebo zda nenarazil na překážku (stěnu nebo svůj vlastní ocas).
DoCollisionCheck() metoda
Detekce srážek bude prováděna v metodě nazvané DoCollisionCheck(), takže ji musíme implementovat. Zde je, jak by momentálně měla vypadat:
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 bylo slíbeno, provedeme dvě kontroly: Nejprve zkontrolujeme, zda současná pozice hlavy hada odpovídá pozici aktuálního kusu jídla. Pokud ano, zavoláme metodu EatSnakeFood() (o tom později). Poté zkontrolujeme, zda se pozice hlavy hada nachází mimo hranice herní oblasti, abychom zjistili, zda se had chystá opustit některou z hranic. Pokud ano, zavoláme metodu EndGame(). Nakonec zkontrolujeme, zda se pozice hlavy hada shoduje s pozicí některého z částí jeho těla - pokud ano, had právě narazil do vlastního ocasu, což také ukončí hru, s voláním metody EndGame().
The EatSnakeFood() metoda
Metoda EatSnakeFood() je zodpovědná za provedení několika věcí, protože jakmile had sežere aktuální kus jídla, musíme přidat nový kus na nové pozici, stejně jako upravit skóre, délku hada a aktuální rychlost hry. Pro skóre musíme deklarovat novou lokální proměnnou nazvanou currentScore:
public partial class SnakeWPFSample : Window
{
....
private int snakeLength;
private int currentScore = 0;
....
S tím na místě přidejte metodu 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 bylo zmíněno, zde se děje několik věcí:
- Inkrementujeme proměnné snakeLength a currentScore o jedničku, aby odrážely skutečnost, že had právě sežral kousek potravy.
- Upravujeme Interval gameTickTimer pomocí následujícího pravidla: currentScore se vynásobí dvěma a poté se odečte od aktuálního intervalu (rychlosti). To způsobí, že rychlost bude exponenciálně růst společně s délkou hada, což hru činí stále obtížnější. Dříve jsme definovali dolní hranici pro rychlost, s konstantou SnakeSpeedThreshold, což znamená, že rychlost hry nikdy nespadne pod interval 100 ms.
- Poté odstraníme kousek jídla, který právě had snědl, a poté zavoláme metodu DrawSnakeFood(), která přidá nový kousek jídla na nové místo.
- Nakonec zavoláme metodu UpdateGameStatus(), která vypadá takto:
private void UpdateGameStatus()
{
this.Title = "SnakeWPF - Score: " + currentScore + " - Game speed: " + gameTickTimer.Interval.TotalMilliseconds;
}
Tato metoda jednoduše aktualizuje vlastnost Title okna Window, aby odrážela aktuální skóre a rychlost hry. To je snadný způsob, jak zobrazit aktuální stav, který lze později snadno rozšířit, pokud je to žádoucí.
EndGame() metoda
Také potřebujeme trochu kódu, který se má spustit, když by hra měla skončit. Uděláme to pomocí metody EndGame(), která je v současnosti volána z metody DoCollisionCheck(). Jak vidíte, v současné době je to velmi jednoduché:
private void EndGame()
{
gameTickTimer.IsEnabled = false;
MessageBox.Show("Oooops, you died!\n\nTo start a new game, just press the Space bar...", "SnakeWPF");
}
Kromě zobrazení zprávy uživateli o nešťastném konci našeho milovaného hada jednoduše zastavíme gameTickTimer. Jelikož tento časovač způsobuje všechny události ve hře, jakmile je zastaven, všechny pohyby a kreslení také přestanou.
Finální úpravy
Nyní jsme téměř připraveni s prvním návrhem plně funkční hry Snake - ve skutečnosti potřebujeme udělat jen dvě menší úpravy. Nejprve musíme zajistit, aby byla volána metoda DoCollisionCheck() - to by mělo proběhnout jako poslední akce provedená v metodě MoveSnake(), kterou jsme implementovali dříve:
private void MoveSnake()
{
.....
//... and then have it drawn!
DrawSnake();
// Finally: Check if it just hit something!
DoCollisionCheck();
}
Detekce kolize je nyní prováděna hned, jakmile se had pohne! Nyní si vzpomeňte, jak jsem vám řekl, že jsme implementovali jednoduchou variantu metody StartNewGame()? Potřebujeme ji trochu rozšířit, abychom zajistili, že každé spuštění (nebo restartování) hry resetuje skóre, stejně jako několik dalších věcí. Takže nahraďte metodu StartNewGame() touto mírně rozšířenou verzí:
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;
}
Když se hra nově spustí, stane se nyní následující:
- Protože to nemusí být první hra, musíme se ujistit, že jsou odstraněny veškeré zbytky z předchozí hry: To zahrnuje všechny existující části hada, stejně jako zbytky jídla.
- Také potřebujeme resetovat některé proměnné na jejich počáteční nastavení, jako je skóre, délka, směr a rychlost časovače. Přidáme také počáteční hlavu hada (která bude automaticky rozšířena metodou MoveSnake()).
- Poté zavoláme metody DrawSnake() a DrawSnakeFood(), aby se vizuálně odrazilo, že byla spuštěna nová hra.
- Nakonec zavoláme metodu UpdateGameStatus().
- A konečně, jsme připraveni spustit gameTickTimer - okamžitě začne tikat, čímž v podstatě uvede hru do pohybu.
Shrnutí
Pokud jste došli až na konec této série článků: gratuluji - právě jste postavili svou první hru WPF! Užijte si všechnu svou tvrdou práci spuštěním vašeho projektu, stisknutím klávesy Space a začněte hrát - i v této velmi jednoduché implementaci je Snake zábavná a návyková hra!