Своя змейка, или пишем первый проект. Часть 0

Предисловие

Привет Хабр! Меня зовут Евгений «Nage», и я начал заниматься программированием около года назад, в свободное от работы время. Просмотрев множество различных туториалов по программированию задаешься вопросом «а что же делать дальше?», ведь в основном все рассказывают про самые основы и дальше как правило не заходят. Вот после продолжительного времени за просмотром разных роликов про одно и тоже я решил что стоит двигаться дальше, и браться за первый проект. И так, сейчас мы разберем как можно написать игру «Змейка» в консоли со своими начальными знаниями.

Глава 1. Итак, с чего начнем?

Для начала нам ничего лишнего не понадобится, только блокнот (или ваш любимый редактор), и компилятор C#, он присутствует по умолчанию в Windows, находится он в С:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe. Можно использовать компилятор последней версии который поставляется с visual studio, он находится Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\Roslyn\csc.exe.

Создадим файл для быстрой компиляции нашего кода, сохранил файл с расширением .bat со следующим содержимым:

@echo off :Start set /p name= Enter program name:  echo. С:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe "%name%.cs" echo. goto Start

«@echo off» отключает отображение команд в консоли. С помощью команды goto получаем бесконечный цикл. Задаем переменную name, а с модификатором /p в переменную записывается значение введенное пользователем в консоль. «echo.» просто оставляет пустую строчку в консоли. Далее вызываем компилятор и передаем ему файл нашего кода, который он скомпилирует.

Таким способом мы можем скомпилировать только один файл, поэтому мы будем писать все классы в одном документе (я не разобрался еще как компилировать несколько файлов в один .exe через консоль, да и это не тема нашей статьи, может кто нибудь расскажет в комментариях).

Для тех кто сразу хочет увидеть весь код.

Скрытый текст

using System; using System.Threading; using System.Collections.Generic; using System.Linq;  namespace SnakeGame {     class Game     {         static readonly int x = 80;         static readonly int y = 26;          static Walls walls;         static Snake snake;         static FoodFactory foodFactory;         static Timer time;          static void Main()         {             Console.SetWindowSize(x + 1, y + 1);             Console.SetBufferSize(x + 1, y + 1);             Console.CursorVisible = false;              walls = new Walls(x, y, '#');             snake = new Snake(x / 2, y / 2, 3);              foodFactory = new FoodFactory(x, y, '@');             foodFactory.CreateFood();              time = new Timer(Loop, null, 0, 200);              while (true)             {                 if (Console.KeyAvailable)                 {                     ConsoleKeyInfo key = Console.ReadKey();                     snake.Rotation(key.Key);                 }             }         }// Main()          static void Loop(object obj)         {             if (walls.IsHit(snake.GetHead()) || snake.IsHit(snake.GetHead()))             {                 time.Change(0, Timeout.Infinite);             }             else if (snake.Eat(foodFactory.food))             {                 foodFactory.CreateFood();             }             else             {                 snake.Move();             }         }// Loop()     }// class Game      struct Point     {         public int x { get; set; }         public int y { get; set; }         public char ch { get; set; }                  public static implicit operator Point((int, int, char) value) =>                new Point {x = value.Item1, y = value.Item2, ch = value.Item3};          public static bool operator ==(Point a, Point b) =>                  (a.x == b.x && a.y == b.y) ? true : false;         public static bool operator !=(Point a, Point b) =>                  (a.x != b.x || a.y != b.y) ? true : false;          public void Draw()         {             DrawPoint(ch);         }         public void Clear()         {             DrawPoint(' ');         }          private void DrawPoint(char _ch)         {             Console.SetCursorPosition(x, y);             Console.Write(_ch);         }     }      class Walls     {         private char ch;         private List<Point> wall = new List<Point>();          public Walls(int x, int y, char ch)         {             this.ch = ch;              DrawHorizontal(x, 0);             DrawHorizontal(x, y);             DrawVertical(0, y);             DrawVertical(x, y);         }          private void DrawHorizontal(int x, int y)         {             for (int i = 0; i < x; i++)             {                 Point p = (i, y, ch);                 p.Draw();                 wall.Add(p);             }         }          private void DrawVertical(int x, int y)         {             for (int i = 0; i < y; i++)             {                 Point p = (x, i, ch);                 p.Draw();                 wall.Add(p);             }         }          public bool IsHit(Point p)         {             foreach (var w in wall)             {                 if (p == w)                 {                     return true;                 }             }             return false;         }     }// class Walls      enum Direction     {         LEFT,         RIGHT,         UP,         DOWN     }      class Snake     {         private List<Point> snake;          private Direction direction;         private int step = 1;         private Point tail;         private Point head;          bool rotate = true;          public Snake(int x, int y, int length)         {             direction = Direction.RIGHT;              snake = new List<Point>();             for (int i = x - length; i < x; i++)             {                 Point p = (i, y, '*');                 snake.Add(p);                  p.Draw();             }         }          public Point GetHead() => snake.Last();          public void Move()         {             head = GetNextPoint();             snake.Add(head);              tail = snake.First();             snake.Remove(tail);              tail.Clear();             head.Draw();              rotate = true;         }          public bool Eat(Point p)         {             head = GetNextPoint();             if (head == p)             {                 snake.Add(head);                 head.Draw();                 return true;             }             return false;         }      public Point GetNextPoint ()      {         Point p = GetHead ();          switch (direction)          {         case Direction.LEFT:             p.x -= step;             break;         case Direction.RIGHT:             p.x += step;             break;         case Direction.UP:             p.y -= step;             break;         case Direction.DOWN:             p.y += step;             break;         }         return p;     }      public void Rotation (ConsoleKey key)      {         if (rotate)          {             switch (direction)              {             case Direction.LEFT:             case Direction.RIGHT:                 if (key == ConsoleKey.DownArrow)                     direction = Direction.DOWN;                 else if (key == ConsoleKey.UpArrow)                     direction = Direction.UP;                 break;             case Direction.UP:             case Direction.DOWN:                 if (key == ConsoleKey.LeftArrow)                     direction = Direction.LEFT;                 else if (key == ConsoleKey.RightArrow)                     direction = Direction.RIGHT;                 break;             }             rotate = false;         }      }          public bool IsHit(Point p)         {             for (int i = snake.Count - 2; i > 0; i--)             {                 if (snake[i] == p)                 {                     return true;                 }             }             return false;         }     }//class Snake      class FoodFactory     {         int x;         int y;         char ch;         public Point food { get; private set; }          Random random = new Random();          public FoodFactory(int x, int y, char ch)         {             this.x = x;             this.y = y;             this.ch = ch;         }          public void CreateFood()         {             food = (random.Next(2, x - 2), random.Next(2, y - 2), ch);             food.Draw();         }     } } 

Глава 2. Первые шаги

Подготовим поле нашей игры, начиная с точки входа в нашу программу. Задаем переменные X и Y, размер и буфер окна консоли, и скроем отображение курсора.

using System; using System.Collections.Generic; using System.Linq; class Game{     static readonly int x = 80;     static readonly int y = 26;      static void Main(){         Console.SetWindowSize(x + 1, y + 1);         Console.SetBufferSize(x + 1, y + 1);         Console.CursorVisible = false;     }// Main() }// class Game 

Для вывода на экран нашей «графики» создадим свой тип данных — точка. Он будет содержать координаты и символ, который будет выводится на экран. Также сделаем методы для вывода на экран точки и ее «стирания».

struct Point{     public int x { get; set; }     public int y { get; set; }     public char ch { get; set; }      public static implicit operator Point((int, int, char) value) =>          new Point {x = value.Item1, y = value.Item2, ch = value.Item3};      public void Draw(){         DrawPoint(ch);     }     public void Clear(){         DrawPoint(' ');     }     private void DrawPoint(char _ch){         Console.SetCursorPosition(x, y);         Console.Write(_ch);     } }

Это интересно!
Оператор => называется лямбда-оператор, он используется в качестве определения анонимных лямбда выражений, и в качестве тела, состоящего из одного выражения, синтаксический сахар, заменяющий оператор return. Приведенный выше метод переопределения оператора (про его назначение чуть ниже) можно переписать так:

 public static bool operator ==(Point a, Point b){     if (a.x == b.x && a.y == b.y){         return true;     }     else{         return false;     } }

Создадим класс стен, границы игрового поля. Напишем 2 метода на создание вертикальных и горизонтальных линий, и в конструкторе вызываем отрисовку всех 4х сторон заданным символом. Список всех точек в стенке нам пригодится позже.

class Walls{     private char ch;     private List<Point> wall = new List<Point>();      public Walls(int x, int y, char ch){         this.ch = ch;         DrawHorizontal(x, 0);         DrawHorizontal(x, y);         DrawVertical(0, y);         DrawVertical(x, y);     }      private void DrawHorizontal(int x, int y){         for (int i = 0; i < x; i++){             Point p = (i, y, ch);             p.Draw();             wall.Add(p);         }     }     private void DrawVertical(int x, int y) {         for (int i = 0; i < y; i++) {             Point p = (x, i, ch);             p.Draw();             wall.Add(p);         }     } }// class Walls

Это интересно!

Как вы могли заметить для инициализации типа данных Point используется форма Point p = (x, y, ch); как и у встроенных типов, это становится возможным при переопределении оператора implicit, в котором описывается как задаются переменные.

Важно!

Конструкция (int, int, char) называется кортежем, и работает только с .net 4.7+, по этому если у вас не установлен visual studio, то в вашем распоряжении только компилятор v4.0.30319 и нужно использовать стандартную инициализацию через оператор new.

Вернемся к классу Game и объявим поле walls, а в методе Main инициализируем ее.

class Game{ static Walls walls;     static void Main(){         walls = new Walls(x, y, '#'); ...

Все! Можно скомпилировать код и посмотреть, что наше поле построилось, и самая легкая часть позади.

Глава 3. А что сегодня на завтрак?

Добавим генерацию еды на нашем поле, для этого создадим класс FoodFactory, который и будет заниматься созданием еды внутри границ.

class FoodFactory {     int x;     int y;     char ch;     public Point food { get; private set; }      Random random = new Random();      public FoodFactory(int x, int y, char ch)     {         this.x = x;         this.y = y;         this.ch = ch;     }      public void CreateFood()     {         food = (random.Next(2, x - 2), random.Next(2, y - 2), ch);         food.Draw();     } }

Добавляем инициализацию фабрики и создадим еду на поле

class Game{     static FoodFactory foodFactory;      static void Main(){         foodFactory = new FoodFactory(x, y, '@');         foodFactory.CreateFood(); ...

Кушать подано!

Глава 4. Время главного героя

Перейдем к созданию самой змеи, и для начала определим перечисление направления движения змейки.

enum Direction{     LEFT,     RIGHT,     UP,     DOWN }

Теперь можем создать класс змейки, где опишем как она будет ползать, поворачивать. Определим список точек змеи, наше перечисление, шаг на сколько будет перемещаться за ход, и ссылки на хвостовую и головную точки, и конструктор, в котором рисуем змею в заданных координатах и заданной длинны при старте игры.

class Snake{     private List<Point> snake;     private Direction direction;     private int step = 1;     private Point tail;     private Point head;     bool rotate = true;     public Snake(int x, int y, int length){         direction = Direction.RIGHT;         snake = new List<Point>();         for (int i = x - length; i < x; i++)        {             Point p = (i, y, '*');             snake.Add(p);             p.Draw();         }     } //Методы движения и поворота в зависимости он направления движения змейки.     public Point GetHead() => snake.Last();     public void Move(){         head = GetNextPoint();         snake.Add(head);         tail = snake.First();         snake.Remove(tail);         tail.Clear();         head.Draw();         rotate = true;     }     public Point GetNextPoint() {         Point p = GetHead();         switch (direction) {             case Direction.LEFT:                 p.x -= step;                 break;             case Direction.RIGHT:                 p.x += step;                 break;             case Direction.UP:                 p.y -= step;                 break;             case Direction.DOWN:                 p.y += step;                 break;         }     return p;     }     public void Rotation(ConsoleKey key) {         if (rotate) {             switch (direction) {                 case Direction.LEFT:                 case Direction.RIGHT:                     if (key == ConsoleKey.DownArrow)                         direction = Direction.DOWN;                     else if (key == ConsoleKey.UpArrow)                         direction = Direction.UP;                     break;                 case Direction.UP:                 case Direction.DOWN:                     if (key == ConsoleKey.LeftArrow)                         direction = Direction.LEFT;                     else if (key == ConsoleKey.RightArrow)                         direction = Direction.RIGHT;                     break;             }             rotate = false;         }     } }//class Snake

В методе поворота, что бы избежать возможности повернуть сразу на 180 градусов, просто указываем, что в каждом направлении мы можем повернуть только в 2 стороны. А проблему поворота на 180 градусов двумя нажатиями — поставив «переключатель», отключаем возможность поворачивать после первого нажатия, и включаем после очередного хода.

Осталось вывести ее на экран.

class Game{     static Snake snake;     static void Main(){         snake = new Snake(x / 2, y / 2, 3); ... 

Готово! теперь у нас есть все что нужно, поле огороженное стенами, рандомно появляющаяся еда, и змейка. Пришла пора заставить все это взаимодействовать друг с другом.

Глава 5. Л-логика

Заставим нашу змейку двигаться, напишем бесконечный цикл для считывания клавиш нажатых на клавиатуре, и передаем клавишу в метод поворота змеи

class Game {     static void Main () {         while (true) {             if (Console.KeyAvailable) {                 ConsoleKeyInfo key = Console.ReadKey ();                 snake.Rotation(key.Key);             } ...

для движения змеи воспользуемся классом .net который будет запускать метод Loop через определенные промежутки времени.

using System.Threading; class Game {     static Timer time;     static void Main () {         time = new Timer (Loop, null, 0, 200); ...

Теперь, перед тем как написать метод движения змейки, надо реализовать взаимодействие головы с едой, стенками и хвостом змеи. Для этого надо написать метод, позволяющий сравнивать две точки на совпадение координат. Переопределим оператор равенства и не равенства, их обязательно нужно переопределять в паре.

struct Point {     public static bool operator == (Point a, Point b) =>          (a.x == b.x && a.y == b.y) ? true : false;     public static bool operator != (Point a, Point b) =>          (a.x != b.x || a.y != b.y) ? true : false; ...

Теперь можно написать метод, который будет проверять совпадает ли интересующая нас точка с какой нибудь из массива стен.

class Walls {     public bool IsHit (Point p) {         foreach (var w in wall) {             if (p == w) {                 return true;             }         }         return false;     } ...

И похожий метод проверяющий не совпадает ли точка с хвостом.

class Snake {     public bool IsHit (Point p) {         for (int i = snake.Count - 2; i > 0; i--) {             if (snake[i] == p) {                 return true;             }         }         return false;     } ...

И методом проверки съела ли еду наша змейка, и сразу делаем ее длиннее.

class Snake {     public bool Eat (Point p) {         head = GetNextPoint ();         if (head == p) {             snake.Add (head);             head.Draw ();             return true;         }         return false;     } ...

теперь можно написать метод движения, со всеми нужными проверками.

class Snake {     static void Loop (object obj) {         if (walls.IsHit (snake.GetHead ()) || snake.IsHit (snake.GetHead ())) {             time.Change (0, Timeout.Infinite);         } else if (snake.Eat (foodFactory.food)) {             foodFactory.CreateFood ();         } else {             snake.Move ();         }     } ...

Вот и все! Наша змейка в консоли закончена и можно поиграть.

Заключение

Мы посмотрели как можно реализовать первую простенькую игру с небольшим использованием ООП, научились перегружать операторы, посмотрели на кортежи и лямбда оператор, надеюсь это было полезно!

Это была пилотная статья, и если вам понравилось, я напишу про реализацию змейки на Unity.
Всем удачи!

FavoriteLoadingДобавить в избранное

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *