Snake Game

Това е една от първите игри, които написах на C# + WinForms. В основата си кодът е от туториал някъде из youtube, но сегашното му състояние е силно модифицирано, с изгледи още да се промени.

Преди всичко трябва да се сдобиете със сорса, защото в статията няма да бъде написано абсолютно всичко. Най-добрият вариант е да клонирате репо-то ми в гитхъб, така ще можете лесно да го синхронизирате с това в моят профил и кодът в статиите ми винаги ще е достъпен и актуален за вас. Другият вариант е да го изтеглите от тук. По време на писане на статията вече направих няколко промени, така че вторият download е възможно да не съответства напълно на кода по-долу, но е работещ. Вече стигнахме до съществената част.

Иб*си змиятааааааа – Неизвестен

Gameplay

Целта е да изядеш колкото се може повече храна, преди да се ухапеш(долу-горе ми напомня на мен, когато съм на шведска маса). Може да се минава през стени. Възможно е след изяждане на храна да се появи бонус храна, която се движи. Тя е с различна продължителност и точките от нея са равни на оставащото й време. Бонус храната също може да минава през стени, но когато се докосне до змията и не е изядена, тя се отблъсква и продължава в противоположна посока.

Design View

DesignView

В дизайна на формата имаме няколко елемента. Добавени са няколко лейбъла, статус бар и таймер. Всички можете да ги намерите, като в Toolbox или с Ctrl+Alt+X. Добавят се с drag’n’drop. Лейбълите се самообясняват, ние само ги скриваме и показваме когато е нужно, статус бар-а служи за отчитане на точките и се ъпдейтва всеки път, когато змията хапне нещо. Това, което е най-важно тук е таймера. Ако сте правили конзолни игри, знаете, че използвахме за забавяне Thread.Sleep(n); тук обаче таймерът е средството, чрез което контролираме кадрите. Още повече – използваме го и като основен цикъл за играта. При всеки tick на таймера се извиква функция.

Snake.cs

Ако още не сте разбрали – това е змията. :D

private Rectangle[] snakeRec;
 private SolidBrush brush;
 private int x, y, width, height;

public Snake()
 {
 snakeRec = new Rectangle[3];
 brush = new SolidBrush(Color.Blue);
 x = 150;
 y = 130;
 width = 10;
 height = 10;

for (int i = 0; i < snakeRec.Length; i++)
 {
 snakeRec[i] = new Rectangle(x, y, width, height);
 x -= 10;
 }
 }

Това, което виждаме в по-горният код е анатомията на една змия. Rectangle е структура идваща от System.Drawing;. В себе си съдържа своите измерения и позиция – Height, Width, X и Y. Тялото на змията се състои от масив от правоъгълници, всеки от които е с размери 10х10 пиксела. SolidBrush.. познахте, пак идва от същия неймспейс. Това е четка и с нея запълваме правоъгълниците, как ще видите след малко. В конструктора виждаме, че змията ни се състои от 3 правоъгълник. Четката е синя, но вие можете да си я промените да съответства на вашите партийни или религиозни виждания. x и y са точките в които правоъгълниците започват или по-разбираемо горен ляв ъгъл. Следва задаване на широчина и дължина на правоъгълниците, и понеже двете са равни, нашите правоъгълници стават квадрати. Цикълът в края създава самите правоъгълници(вече квадрати) като всеки следващ се намира с 10 пиксела по-наляво, за да се образува змията, а не да са един върху друг.

  • drawSnake(Graphics paper) обхожда всички елементи на масива и ги рисува, обяснено е по-подробно във Form1.cs.
  • moveПосока() всички методи започващи с move променят координата в първият правоъгълник от snakeRec.
  • moveSnake() се извиква от всеки метод moveПосока(). Премества правоъгълниците в масива. Последният отива на мястото на предпоследният, предпоследният на пред-предпоследният и тн. След като това се извърши първият и вторият имат еднакви координати. Тогава се връщаме в извикващият метод и той променя първият.
  • growSnake() добавя нов правоъгълник(с размерите на квадрат), когато змията изяде храна и порасне.

Food.cs

Тук е момента да получите Deja-Vu:

</pre>int width, height;
 private SolidBrush brush;
 public Rectangle foodRec;
 public Food(Random randFood)
 {
 brush = new SolidBrush(Color.Red);

width = 10;
 height = 10;
 foodRec = new Rectangle(randFood.Next(0, 29) * 10, randFood.Next(0, 29) * 10, width, height);
 }

В началото на класа имаме същото като при змията. Разликата е, че тук имаме само един правоъгълник(квадрат) и си го рисуваме. Също така за x и y директно задаваме стойности, чрез randFood.Next(0, 29) * 10.

  • FoodLocation(Random randFood, Snake s) в този метод позиционираме храната. Първият параметър е генератор на произволни числа, а вторият е нашата змия. Това, което правим с нея е – когато слагаме нова храна трябва да проверим, дали не е в змията, ако е така избираме нови координати за храна.
  • drawFood(Graphics paper) рисува правоъгълника.

BonusFood.cs

</p>
public bool DrawBonusFood { get; set; }
public bool IsActive { get; set; }
public int DurationInMSec { get; set; }
public int Direction { get; set; }
//if dtimer is < 0, direction can change
public int DirectionTimer { get; set; }

Продължителността за която бонус храната се появява може да бъде различна. Direction е посоката в която ще се движи. Най-интересната променлива тук е DirectionTimer. Понеже бонус храната се движи, тя може да се сблъска със змията, тогава храната тръгва в обратната посока. Това, което този таймер прави е да забрани промяна на посоката за известно време, защото без него бонус храната постоянно ще променя посоката си и ще остане в змията, понеже винаги я пресича.

  • BonusFoodLocationDirection(Random randFood) това е метода, който инициализира променливите на бонус храната – посока, място и продължителност.
  • MoveBonusFood() мести храната по зададената посока.
  • BonusDrawFood(Graphics paper) може и да не го очаквахте, но рисува храната.
  • SetX и SetY – не ме питайте защо съществуват и ги ползвам така…

Form1.cs

Знам, името кърти. Тук се изпълнява цикъла на играта и колизии. В началото имаме някои ключови декларации:


Random randFood = new Random();
Graphics paper;
Snake snake;
Food food;
BonusFood bonusFood;</pre>
bool left = false;
 bool right = false;
 bool down = false;
 bool up = false;

int score = 0;

public Form1()
 {
 InitializeComponent();
 food = new Food(randFood);
 bonusFood = new BonusFood();

start();
 }

randFood дава произволни стойности за позиция на храната. Graphics е клас в неймспейса System.Drawing. Можете да си го представите като повърхността върху която ще рисуваме, и тук името на инстанцията му(обекта) paper е добра метафора. Работи по следният начин – създаваме правоъгълник, четка и извикваме paper.FillRectangle(четка, правоъгълник); В програмата използваме само това, но възможностите му не са ограничени до тук, възможно е да се рисуват много други фигури и изображения.

  • Form1_KeyDown(object sender, KeyEventArgs e) това е event handler(метод, който се извиква при определено събитие), който проверява кое копче сме натиснали и ако то съответства на някоя от стрелките сетва посоката, в която сме натиснали. За да добавите такъв във вашият проект влизате в design view, цъкате на формата с дясно копче(или избирате менюто) и избирате Properties. След това в горната му част трябва да има бутон Events, с дабъл клик избирате евента, който в случая е KeyDown.
  • Form1_Paint(object sender, PaintEventArgs e) друг евент хендлър – рисуваме всички елементи от играта и бонус, ако има.
  • timer1_Tick(object sender, EventArgs e) мда, евент хендлър. Извиква се на всеки тик на таймера, можете да настройвате интервала между тик-овете както през design view, така и в кода. Това е цикълът, който “върти” играта. Първите две операции, които се извършват е да се придвижи змията в текущата посока и след това, ако има бонус храна да се намали продължителността й, ако продължителността е 0, храната изчезва. Следват проверки за колизии:
  1. if(snake.SnakeRec[0].IntersectsWith(food.foodRec))

    Проверяваме дали първият правоъгълник на змията се пресича с нормалната храна. Тук за щастие Rectangle класът има много удобен метод – IntersectsWith(rect). Чрез него можем да видим, дали два правоъгълника се пресичат. При пресичане точките се увеличават, змията расте, пускаме нова храна и има шанс да пуснем бонус храна.

  2. else if (bonusFood.IsActive && snake.SnakeRec[0].IntersectsWith(bonusFood.foodRec))

    Ако има бонус храна проверяваме дали първият правоъгълник на смока се пресича с този на бонуса. Ако е така даваме точки равни на оставащото време на бонуса и го деактивираме. Змията не расте от бонус храната :( .

  3. Ако бонуса е активен го придвижваме, това вече знаем как се случва.
  4. collisionWithSelfAndWindow() Проверяваме дали змията се е ухапала.
    if (snake.SnakeRec[0].IntersectsWith(snake.SnakeRec[i]))
     {
     restart();
     }

    Всеки от правоъгълниците се обхожда и се проверява, дали се пресича с главата.

    if (snake.SnakeRec[0].X <= -10)
     {
     snake.SnakeRec[0].X = 290;
     } else
    
    if (snake.SnakeRec[0].X > 290)
     {
     snake.SnakeRec[0].X = 0;
     }
    
    if (snake.SnakeRec[0].Y < 0)
     {
     snake.SnakeRec[0].Y = 290;
     } else
    
    if (snake.SnakeRec[0].Y > 290)
     {
     snake.SnakeRec[0].Y = 0;
     }

    Ако излизаме от полето се прехвърляме в противоположната стена. Така нашата змия минава през стени. По-надолу правим същото нещо, само, че с бонус храната, като проверяваме и дали се допира до змията, тогава променя посоката си на движение.

Bugs and Improvements

Понеже знам, че най-добре се учи като се пише тук ще напиша проблемите, които съществуват в играта и неща, които могат да се добавят. Така всеки, който желае да допише нещо ще има някаква цел.

bug: Възможно е да се самоубиеш, като промениш посоката много бързо. Пример – движиш се надолу, натискаш бързо надясно и нагоре, и главата на змията експлодира(всъщност правоъгълниците се пресичат, но така звучи по-добре).

bug: Храните не се ресетват правилно, когато змията умре.

bug: Бонус храната не изчезва, когато се вземе нормалната.

improvement: Може да направите меню в което да се избират нива на трудност. Възможно е и трудността да се увеличава постепенно.

improvement: Запазване на най-високият резултат или на последните резултати.

improvement: Методите, в които змията расте и се движи са доста груби. В първият всеки път, когато тя порасне се създава нов масив с нови обекти, което е немислимо в комерсиална игра(не че нашата е такава). Движението е долу-горе същото нещо, правоъгълниците се преместват в масива по стойност, а не референция, което води до излишно копиране. Проблемът идва от това, че Rectangle е структура, а не клас.

improvement: Възможност за оразмеряване на полето. Не е задължително змията да скейлва с него.

improvement: Бонус храната да се отблъсква от нормалната храна, а не да преминава през нея.

Заключителна реч

Никога не съм си представял, че блогването може да е толкова трудоемка задача. Всичко обаче се отплаща, защото светът е по-добър, когато има повече змии. Приятно програмиране!

Posted in C#, Programming Tagged with: , , , , , , ,

Leave a Reply

Your email address will not be published.