ИгрыХабрахабр

[Из песочницы] Своя змейка, или пишем первый проект. Часть 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 ireadonly 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; 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(); } 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) { 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; } } 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); }
}

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

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.
Всем удачи!

Теги
Показать больше

Похожие статьи

Кнопка «Наверх»
Закрыть