This article has been localized into Ukrainian 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!
Виявлення зіткнень
Тепер, коли ми реалізували ігрову зону, їжу та змію, а також безперервний рух змії, нам потрібна лише одна річ, щоб це виглядало та діяло як справжня гра: Виявлення зіткнень. Концепція розвивається навколо постійної перевірки, чи щойно наша Змія вдарилася об щось, і наразі нам це потрібно для двох цілей: щоб побачити, чи Змія щойно з'їла трохи їжі, чи вона вдарилася об перешкоду (стіну чи власний хвіст).
Метод DoCollisionCheck()
Виявлення колізій буде виконуватися за допомогою методу під назвою DoCollisionCheck(), тому нам потрібно його реалізувати. Ось як це має виглядати зараз:
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();
}
}Як і обіцяли, ми виконуємо дві перевірки: спочатку ми перевіряємо, чи збігається поточне положення голови змії з положенням поточного шматка їжі. Якщо так, ми викликаємо метод EatSnakeFood() (про це пізніше). Потім ми перевіряємо, чи виходить положення голови змії за межі GameArea, щоб побачити, чи не виходить змія за одну з меж. Якщо так, ми викликаємо метод EndGame(). Нарешті, ми перевіряємо, чи збігається голова змії з одним із положень частин тіла — якщо так, то змія щойно зіткнулася з власним хвостом, що також завершить гру, викликаючи метод EndGame().
Метод EatSnakeFood()
Метод EatSnakeFood() відповідає за кілька речей: щойно змія з'їдає поточний шматочок їжі, нам потрібно додати новий у новому місці, а також налаштувати рахунок, довжину змії та поточну швидкість гри. Для рахунку нам потрібно оголосити нову локальну змінну під назвою currentScore:
public partial class SnakeWPFSample : Window
{
....
private int snakeLength;
private int currentScore = 0;
....Після цього додайте метод 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();
}Як уже згадувалося, тут відбувається кілька речей:
- Ми збільшуємо змінні snakeLength та currentScore на одиницю, щоб відобразити той факт, що змія щойно спіймала шматочок їжі.
- Ми налаштовуємо Interval gameTickTimer, використовуючи таке правило: значення currentScore множиться на 2, а потім віднімається від поточного інтервалу (швидкості). Це призведе до експоненціального зростання швидкості разом із довжиною змії, що ускладнить гру. Раніше ми визначили нижню межу для швидкості з константою SnakeSpeedThreshold, що означає, що швидкість гри ніколи не падає нижче інтервалу 100 мс.
- Потім ми видаляємо шматочок їжі, який щойно з'їла змія, і викликаємо метод DrawSnakeFood(), який додасть новий шматочок їжі в нове місце.
- Нарешті, ми викликаємо метод UpdateGameStatus(), який виглядає так:
private void UpdateGameStatus()
{
this.Title = "SnakeWPF - Score: " + currentScore + " - Game speed: " + gameTickTimer.Interval.TotalMilliseconds;
}Цей метод просто оновить властивість Title елемента Window, щоб відобразити поточний рахунок та швидкість гри. Це простий спосіб відображення поточного стану, який за бажанням можна легко розширити пізніше.
Метод EndGame()
Нам також потрібен невеликий код для виконання, коли гра має закінчитися. Ми зробимо це за допомогою методу EndGame(), який наразі викликається з методу DoCollisionCheck(). Як бачите, наразі це дуже просто:
private void EndGame()
{
gameTickTimer.IsEnabled = false;
MessageBox.Show("Oooops, you died!\n\nTo start a new game, just press the Space bar...", "SnakeWPF");
}Окрім показу користувачеві повідомлення про нещасливу смерть нашої улюбленої змії, ми просто зупиняємо gameTickTimer. Оскільки саме цей таймер відповідає за всі події в грі, щойно його зупиняють, весь рух і малювання також зупиняються.
Остаточні коригування
Ми майже готові з першим варіантом повнофункціональної гри про змійок — насправді нам потрібно лише внести два незначні корективи. По-перше, нам потрібно переконатися, що викликається DoCollisionCheck() — це має статися як остання дія, виконана в методі MoveSnake(), який ми реалізували раніше:
private void MoveSnake()
{
.....
//... and then have it drawn!
DrawSnake();
// Finally: Check if it just hit something!
DoCollisionCheck();
}Тепер виявлення зіткнень виконується, щойно змія рухається! Пам'ятаєте, як я розповідав вам, що ми реалізували простий варіант методу StartNewGame()? Нам потрібно трохи розширити його, щоб переконатися, що ми скидаємо рахунок щоразу, коли гра (пере)запускається, а також кілька інших речей. Отже, замініть метод StartNewGame() цією трохи розширеною версією:
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;
}Коли починається нова гра, тепер відбуваються такі речі:
- Оскільки це може бути не перша гра, нам потрібно переконатися, що всі потенційні залишки з попередньої гри видалено: це включає всі існуючі частини змії, а також залишки їжі.
- Нам також потрібно скинути деякі змінні до їхніх початкових значень, таких як score, length, direction та speed таймера. Ми також додаємо початкову голову змії (яка буде автоматично розгорнута методом MoveSnake()).
- Потім ми викликаємо методи DrawSnake() та DrawSnakeFood(), щоб візуально відобразити початок нової гри.
- Далі ми викликаємо метод UpdateGameStatus().
- І нарешті, ми готові запустити gameTickTimer — він одразу почне цокати, фактично запускаючи гру.
Короткий зміст
Якщо ви прочитали всю цю серію статей: вітаємо – ви щойно створили свою першу гру WPF! Насолоджуйтесь своєю важкою працею, запустивши свій проект, натиснувши клавішу Пробіл та почніть грати – навіть у цій дуже простій реалізації, Snake – це весела та захоплива гра!