Предисловие


Привет Хабр! Меня зовут Евгений «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.
Всем удачи!

Комментарии (32)


  1. Shtucer
    04.02.2018 18:10
    +2

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

    Ну, как же так? Просто перечисляете все файлы для компиляции в одной команде. Или делаете вот так, чтобы скомпилировать все файлы в текущей директории:


    csc -out:%name%.exe *.cs


    Первая же страница документации.


    1. GNage Автор
      04.02.2018 21:28

      спасибо :) буду знать


  1. ewgeniy2004
    04.02.2018 21:27

    В данном случае предлагаю вместо

    ConsoleKeyInfo key = Console.ReadKey();
    написать
    ConsoleKeyInfo key = Console.ReadKey(true);
    это устранит отображение в консоли нажатой пользователем клавиши из-за чего иногда часть змея пропадает.
    А проблему поворота на 180 градусов двумя нажатиями — поставив «переключатель»

    двумя быстрыми нажатиями змейку можно повернуть на 180 градусов


    1. ewgeniy2004
      04.02.2018 21:38

      «переключатель» — работает просто его почему то нет в коде в начале статьи


      1. GNage Автор
        04.02.2018 21:44

        да, каюсь, забыл, сейчас исправим)


    1. YaNeTu
      05.02.2018 11:40

      Чтобы на 180 не поворачивалась

      // Запрет разворота на 180
      // Если попытка поворота на 180 градусов
      if ((_direction == Directions.Right && DirectionControl == Directions.Left) ||
          (_direction == Directions.Left && DirectionControl == Directions.Right) ||
          (_direction == Directions.Up && DirectionControl == Directions.Down) ||
          (_direction == Directions.Down && DirectionControl == Directions.Up))
      {
      	//пропускаем изменение направления;
      }
      else
      {
      	_direction = DirectionControl;
      }


      1. ewgeniy2004
        05.02.2018 20:48

        мой вариант решения

                    Direction _direction = direction;
                 
                    if (key == ConsoleKey.DownArrow)
                        direction = Direction.DOWN;
                    else if (key == ConsoleKey.UpArrow)
                        direction = Direction.UP;
                    else if(key == ConsoleKey.LeftArrow)
                        direction = Direction.LEFT;
                    else if (key == ConsoleKey.RightArrow)
                        direction = Direction.RIGHT;
                   
        
                    Point p1 = snake.ElementAt(snake.Count - 2);
                    Point p2 = GetNextPoint();
        
                    if (p1.Equals(p2)) direction = _direction;


  1. FFoxDiArt
    04.02.2018 21:27

    Я конечно понимаю, что Вы — начинающий программист, но использование в одном месте лямбд и таких вот конструций вызывает улыбку :)
    (a.x == b.x && a.y == b.y) ? true : false


    1. GNage Автор
      04.02.2018 21:29

      Почему? По моему это классный способ написать метод в одну строчку.


      1. FFoxDiArt
        04.02.2018 21:35

        Не в лямбдах дело :) А в тавтологии вышеуказанного выражения. Достаточно было написать (a.x == b.x && a.y == b.y)


        1. GNage Автор
          04.02.2018 21:51

          Да, я понял :) мы можем вернуть true/false и без оператора ?:


          1. Habra-Mikhail
            04.02.2018 22:04

            Логические выражения при их вычисление возвращают истинность (логическое значение true / false).

            Так что без ?: будет даже лучше.


    1. Zam_dev
      05.02.2018 16:59

      Не сразу то и заметно в чем подвох))


  1. Neiro-Neko
    04.02.2018 22:36
    +1

    Больше всего меня интересуют две вещи:


    1. Почему было принято решение использовать блокнот и консоль?
    2. Почему бы просто в начале игрового цикла (функции Loop) не считать все нажатия которые были и повернуть змейку в последнем направлении? Сразу отпадет необходимость в переключателе.


    1. GNage Автор
      04.02.2018 22:38

      1. Ну что бы показать, что можно компилировать и без IDE, что сделать игру можно уже сейчас нечего не устанавливая (хотя сам и использовал возможности языка, которые можно скомпилировать только компилятором при VS).
      2. Интересное предложение, думаю можно и так сделать.


      1. aquamakc
        05.02.2018 11:19

        1. Ну что бы показать, что можно компилировать и без IDE, что сделать игру можно уже сейчас нечего не устанавливая

        Вспоминается бородатый анекдот:
        лежит на диком пляже молодая изнывающая от скуки девушка. Тут из кустов выходит мужик в ластах, фраке и противогазе.
        Девушка: мужчина, давайте займёмся любовью?
        Мужик: хорошо, но только в одежде, стоя и в гамаке.
        Девушка: о_О почему?
        Мужик: Люблю преодолевать трудности.


    1. ewgeniy2004
      05.02.2018 03:26

      1. В этом случае надо будет делать защиту от залипания клавиш


    1. OlegKozlov
      06.02.2018 10:05

      2. Если рассматривать это именно как игру «Змейку», а не упражнение по лямбдам, то нажатия игрока пропускать нельзя. Поворот змейки в обратную сторону как раз делается двумя быстрыми нажатиями двух стрелок на одном шаге, иначе повороты и развороты начинают страшно бесить из-за сложности попадания в тайминг (шаги игры очень мелкие). Короче, для комфортной игры нужен буферизованый ввод. При противоположных командах нужно брать последнюю, игнорируя попытки включить змее реверс.


  1. nkozhevnikov
    05.02.2018 07:06

    Выложите полный исходный код на Github или сюда под спойлер.
    Уж очень интересно это собрать и сыграть.


    1. GNage Автор
      05.02.2018 11:41

      Так в первой главе под спойлером полный код и выложен


    1. YaNeTu
      05.02.2018 13:24

      Может и на мой код змейки взглянете.
      Змейка отлажена. Новый функционал придумывается и дорабатывается
      Особенность в том, что можно включить автопилот и смотреть как она сама еду ищет и ест.
      github.com/Cuprumbur/Snake

      image

      Присоеденяйтесь
      Автопилот включается кнопкой 'A'


  1. impwx
    05.02.2018 10:46
    +2

    Когда статья пишется «новичком для новичков», в нее неизбежно проникают «вредные советы» и прочие плохие практики. Разберем по пунктам:

    1. Зачем использовать блокнот и консольный компилятор? Слава богу, в 2018 году живем, а не в каменном веке. Есть Visual Studio и VS Code — оба бесплатные, есть отличные автокомплит и отладчик.

    2. Лексема => в данном случае не является «лямбда-оператором» — это просто синтаксический сахар для return. А «expression-bodied member» это не «определение текста выражения» (это что, гугл-транслейт?), а «тело, состоящее из одного выражения».

    3. Кортежи в данном случае вообще не нужно использовать. Вместо Point p = (1, 2, 'x') можно писать var p = new Point(1, 2, 'x') — получаем более осмысленные подсказки и ошибки.

    4. Вместо того, чтобы хранить step и direction, можно было бы хранить вторую переменную типа Point и складывать их, как вектора. Тогда функция GetNextPoint сократилась бы до одной строки.

    5. Коллекция List в данном случае не подходит: удаление первого элемента приводит к тому, что все остальные приходится «сдвинуть» на один шаг назад. Для данного случая есть Queue, поддерживающая как раз добавление с одной стороны и удаление с другой.

    6. Как уже было сказано, не нужно писать (a == b) ? true : false: это эквивалентно просто a == b.


    1. GNage Автор
      05.02.2018 12:22

      Вот ради таких комментов и стоило писать, спасибо :)
      1. Все ради того, что бы показать, что можно взять файл с текстом и скомпилировать его без всяких сторонних программ, как по мне это полезно знать, может появится желание у людей поподробнее разобраться с этой темой, и в том как устроен компилятор.
      2. Да это просто машинный перевод с сайта майкросовтов, поправлю в статье.
      3. Я хотел что бы объявление класса выглядело как обычный тип данных, как int i = 1 например, и показать как этого можно добиться.
      С остальным полностью согласен!


      1. impwx
        05.02.2018 13:40

        1. Инструменты разработчика — сильная сторона дотнета. Ими не стоит пренебрегать из аскетических соображений. Если хочется «быстро написать и сразу проверить» код — попробуйте LINQPad. Автокомплит там включается только в платной версии, но он точно стоит своих денег. Консольный же компилятор за ~7 лет работы с платформой еще ни разу не пригодился.

        3. Похоже на то, что вы до этого работали со старыми стандартами C / C++ и пытаетесь перенести стиль кода в C#. Это плохая идея по двум причинам. Во-первых, такой код делает двойную работу: вместо того, чтобы передать аргументы сразу в конструктор нужного класса, вы сначала передаете их в конструктор кортежа, а потом забираете из него. Во-вторых, неявное приведение типов считается плохим тоном, поскольку затрудняет понимание кода, и допустимо только в очень редких случаях.

        Советую поставить Resharper — там есть огромное количество правил, обнаруживающих «неопрятный» код и позволяющих автоматически его исправить. Со временем эти правила входят в привычку и код становится чище.


        1. GNage Автор
          05.02.2018 14:36

          Я ни в коем случае не хотел принизить значимость IDE, но полезно знать что там под капотом:)
          С# мой первый язык, с С я знакомился только когда пытался смотреть гарвардский курс cs50. С точки зрения производительности, да лучше сразу в конструктор передавать значения, это я понимаю.
          Resharper посмотрю, интересно.


  1. hd_keeper
    05.02.2018 11:16

    Почему Nage, а не Uke?


    1. GNage Автор
      05.02.2018 11:46

      Странный вопрос, ник у меня такой


      1. Sinatr
        05.02.2018 12:24

        Похоже на отсылку к айкидо: «Uke (literally „one who receives“, the one who takes the fall) and nage (the thrower)», скорее всего имелось в виду, что как новичок, вы скорее всего будете Uke, когда Nage придут читать статью ;)


        1. GNage Автор
          05.02.2018 12:32

          как ни странно айкидо я занимался пару лет, но ник не имеет к нему отношения:)


  1. SphinxKingStone
    05.02.2018 11:45

    Не хватает гифки или видео с геймплеем, а так хорошо


  1. EndUser
    06.02.2018 05:51

    Microsoft ® Visual C# Compiler version 2.6.0.62329 (5429b35d) //Это Cummunity 2017
    Copyright © Microsoft Corporation. All rights reserved.

    Snake.cs(51,12): warning CS0660: 'Point' defines operator == or operator != but does not override Object.Equals(object o)
    Snake.cs(51,12): warning CS0661: 'Point' defines operator == or operator != but does not override Object.GetHashCode()

    Что я делаю не так? :-)


    1. GNage Автор
      06.02.2018 10:34

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