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

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

Ну что, все готовы? Начинаем!


Эта статья будет короткой, но информативной (для новичков, как я).

На момент написания статьи я знала всего несколько языков и рассуждала о выборе каждого из них и насколько он подходит для разработки этих самых танчиков. Но опираясь на знания я решила распределить всё так:

C# — клиент (так как самый лёгкий в изучении язык)
Rust — сервер (так как самый безопасный и быстрый)
Php/html/css/javascript — сайт (который мы ВОЗМОЖНО будем делать)

Часть первая: постановка задачи


Главное что я должна была сделать, дабы доказать правоту — это простые танчики, но я решила сделать универсальный клиент. Как это? — это когда сервер одинаково оптимизирован как и для WinForm, так и для консоли (потому что я хочу хорошие танчики в винформ).

Так что наша задача звучит так: Необходимо разработать три приложения, первое — для WinForm (стандартное окошко виндовс), второе — консольное (эмулятор денди) и третье — сам сервер.

Часть вторая: идеи и огрехи...


Что необходимо делать хорошему приложению? — этот вопрос я задала при проектировании и в моей голове прозвучал ответ:«Быть быстрым».

Что это значит? — то, что нам придётся работать с несколькими потоками приёма/передачи данных. Снаряд не может ждать, пока отрисуется танчик, танк не может ждать пока рисуется стена, чтобы пошевелиться.

Мы все знаем как это обидно, когда у тебя падает фпс или не успевает что-то отрисоваться и тебя убивают. Таких ситуаций быть не должно!

Подумав, я решила распределить их именно так:

1-й поток (мэйн) — должен отправлять нажатую клавишу на сервер.
2-й поток должен принимать координаты танков.
3-й координаты стен.
4-й координаты снарядов.

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

Теперь немного конкретики с потоками, первый должен быть простым отправителем (то есть организовывать минимальны вычисления и отправлять их на сервер), остальные же должны вечно принимать и отрисовывать всё в нашем приложении. В виде кода всё будет примерно так:

static async void Tank_coordinate()
        {
            //Приём координат танков
            await Task.Run(
              () =>
              {


              });
        }

        static async void Coordinate_wall()
        {
            //Приём координат стен
            await Task.Run(
             () =>
             {


             });
        }

        static async void Shot()
        {
            //Приём координат снарядов
            await Task.Run(
            () =>
            {
            /*   ДЛЯ СПРАВКИ: ТУТ МЫ ПИШЕМ СВОЙ КОД, КОТОРЫЙ БУДЕТ ВЫПОЛНЕН АСИНХРОННО  */

            });
        }

        static void To_key()
        {
            //Приём нажатой клавиши           
        }

После всего этого у меня возникли несколько вариантов организации данных, для их отправки на сервер, и тут в бой пошли лекции. Выбор стоял великий: или всё организовать в интовских/стринговских переменных и рисовать через них, или создать структуру для данных, объекты я не рассматривала т.к. не хотела возится с ссылками. Спустя часик гугляния на форумах я остановилась на втором, так как организация данных в виде структуры гораздо легче, да и писать документацию будет удобнее (совет: если есть группы данных, которые схожи по назначению — лучше будет объединить их в структуру, ибо так гораздо легче читать код и искать переменные). Наша новая задача звучит так: создать структуру в которой будут поля отвечающие за нажатую клавишу, координату игрока, угол поворота танка и (для консоли) — позицию последнего символа и желательно какой-то регулятор отрисовки. Для чего нам последнее? — чтобы не использовались координаты с которыми мы работаем (увеличиваем/уменьшаем/отправляем на сервер)

В чём же были ошибки? — спросите меня вы.

Первоначально я начала лезть в дебри http модели (хотелось сделать на http), но спустя количество времени n мне стало ясно что лучше сделать на tcp (и проблем поменьше и с растом возится легче будет).

Мы определились с потоками и с идеей, что же дальше?

А дальше, друзья, будет самое интересное.

Часть третья: создание структуры и метода Main(). Конец первого этапа разработки


Сразу кину код, чтобы нетерпеливые читатели сразу его скопипастили:

//структура наша будет приватной
//и через методы мы присваиваем ей значения
/// <summary>
       /// Координаты игрока(численные значения)
       /// </summary>
        public struct player_coor
        {
            
            public static void new_player_coor (int x_, int y_, string dir_, ConsoleKey key_, int last_x_, int last_y_)
            {
                 x = x_; y = y_; dir = dir_; key = key_; last_x = last_x_; last_y = last_y_;
            }

            static int x = 2;//стартовые координаты
            static int y = 2;//
            static string dir;//стартовое положене

            static ConsoleKey key;//нажатая клавиша

            static int last_x;//
            static int last_y;//последний 'y' и 'x'

        }
static void Main(string[] args)
        {

        }


Но это не самый удобный код, наилучший вариант был предложен: norver

и выглядит так:

// Класс для удобного представления координат
        public class Position
        {
            // Публичные свойства класса
            public int X { get; set; }
            public int Y { get; set; }

            public Position(int x, int y)
            {
                X = x;
                Y = y;
            }
        }

        // Структура состояние игрока, бывшая "player_coor"     
        public struct PlayerState
        {
            // Метод с названием идентичным названию класса или структуры
            // называется конструктор, он позволяет инициализировать свойства
            // и поля структуры или класса

                /// <summary>
                /// Конструктор структуры
                /// </summary>
                /// <param name="startPosition">Экземпляр позиции</param>
                /// <param name="dir_">Направление корпуса игрока (градусы)</param>
                /// <param name="key_">Нажатая клавиша</param>
                /// <param name="lastPosition">Предыдущая позиция игрока</param>
            public PlayerState(Position startPosition, int dir_, ConsoleKey key_, Position lastPosition)
            {
                StartPosition = startPosition;
                LastPosition = lastPosition;
                dir = dir_;
                key = key_;
            }

            private Position StartPosition { get; set; }
            private Position LastPosition { get; set; }

            //стартовое положение
            static int dir;
            //нажатая клавиша
            static ConsoleKey key;

        }

        static void Main(string[] args)
        {
            // Создаем экземпляр класса Position, с координатами 2, 2
            var startPosition = new Position(2, 2);

            // Создаем экземпляр класса Position, с координатами 5, 10
            var currentPosition = new Position(5, 10);

            // Создаем экземпляр структуры PlayerState
            var currentState = new PlayerState(startPosition, int, 
                ConsoleKey.UpArrow, currentPosition);

            // А вот так можно получить доступ к свойствам класса Position
            Console.WriteLine("X={0}, Y={1}", startPosition.X, startPosition.Y);           
        }


Небольшая ремарка: в ходе написания продолжения этой статьи я выяснила что гораздо легче будет хранить числовую переменную, чем текстовую, именно поэтому в переменной dir (то есть позиция) мы будем хранить целочисленное значение.

Это самый простецкий код, но он делает огромную работу: он распоточивает наше будущее приложение.

Небольшое описание потоков: любой поток создаётся через пространство System.Threading. Создаём мы его так же, как и экземпляр класса, но в аргументе потока указываем void функцию.
После создания потока его можно запустить методом .Start() и отключить (вызвать исключение) методом .Abort(), но это есть синхронная модель (то есть едим и ножом и вилкой, но не можем резать, пока не возьмём вилкой), но есть асинхронная (мы едим и пылесосим, а ноги наши при этом делают жим лёжа по +100500 подходов), которую мы и взяли к использованию.

Вот мы написали свой первый «псевдокод» и основные положения/идеи нашего проекта. Первая стадия подошла к концу, а так же подошло к концу наше бездумство, далее мы будем разрабатывать функции и нам придётся попотеть.

Огромное спасибо:


lair, unsafePtr, vlreshet, domix32, vadimturkov, myxo, norver за идеи и правки статьи.

Жду ваших пожеланий и идей, да и прибудет с вами сила!

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


  1. lair
    08.12.2017 15:36
    +1

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

    … а у вас рисовалка умеет много потоков?


    1-й поток (мэйн) — должен отправлять нажатую клавишу на сервер.
    2-й поток должен принимать координаты танков.
    3-й координаты стен.
    4-й координаты снарядов.

    А почему выделенные потоки, а не событийная модель?


    Небольшое описание потоков: любой поток создаётся через пространство System.Threading.

    А почему не тредпул? Почему не таски с cooperative cancellation?


    1. koito_tyan Автор
      08.12.2017 21:41

      Хорошо, я учту это.
      Перед написанием следующей статьи я сделаю небольшую ремарку


  1. unsafePtr
    08.12.2017 15:41

    Замечания:
    1. Зачем вам класс Thread? Используйте Task. И почитайте про асинхронность. Судя по вашему методу мейн, если при открытии конзоли вы не нажмете клавишу, то мы понапрасну будем есть очень много ресурсов.
    2. Не знаю в какие дебри вы залезли с http, но его использовать будет гораздо проще(потому что это текстовый протокол, а не бинарный коим являеться tcp). Особенно если вы планируете веб приложение, вам надо делать ставку на http.
    3. Было бы не плохо выложить ссылку на репозиторий с кодом. В таком соятоянии, я вообще не вижу кода на расте, а по c# вам надо прочитать style guide. Именование методов с подчеркиванием разве что в WinForms я и видел, которые уже устарели.


    1. vlreshet
      08.12.2017 17:12

      2. Не знаю в какие дебри вы залезли с http, но его использовать будет гораздо проще(потому что это текстовый протокол, а не бинарный коим являеться tcp). Особенно если вы планируете веб приложение, вам надо делать ставку на http.

      Если на то пошло — то лучше вскочить в WebSockets. Его и использовать легко (он текстовый), и в вебе он нативный, и нет проблемы двунаправленности сообщений которая есть в http.


    1. koito_tyan Автор
      08.12.2017 21:42

      Хорошо, спасибо огромное)

      (пока что с http сложно, ибо сервер будет на Раст, а я пока не сильно разобралась с http сервером в растбуке, но спасибо за пожелание)


  1. lair
    08.12.2017 15:52

    Наша новая задача звучит так: создать структуру
    public struct player_coor { public static

    Вот только у вас не структура, а пространство имен для бедных. Структура, в которой все поля статические — это боль, а не структура. И в многопоточности вы об нее убъетесь. И use вам низачем не нужен, понятное дело.


    А вообще, C# — объектно-ориентированный язык, какая религия вам помешала объекты использовать?


    1. koito_tyan Автор
      08.12.2017 21:44

      Религия коллекций и прочитанной литературы по низкоуровневым языкам, спасибо за критику и я изменю код в следующей статье)


      1. lair
        08.12.2017 22:36

        А зачем вы пишете на высокоуровневом языке, как на низкоуровневом? Не надо так делать. В C# и коллекция — объект.


  1. domix32
    08.12.2017 16:05

    Termion в руки и станет безопасно и легко и в терминале и на сервере


    1. koito_tyan Автор
      08.12.2017 21:53

      Спасибо большое)


      1. domix32
        08.12.2017 22:53

        Ждем публичную репу параллельно с продолжением


  1. vadimturkov
    08.12.2017 17:06

    А чем вам дженерики не угодили, раз вы ArrayList используете?


    1. koito_tyan Автор
      08.12.2017 21:45

      Дженерики изучены только в расте, простите :p

      Учту при создании следующей статьи )


  1. myxo
    08.12.2017 21:27

    Странная статья, конечно. Но куда более странные комментарии. Автор же ясно дал понять, что он новичок. А вы просите объяснения почему сделано так, а не иначе. На этом этапе
    пробуют, а не принимают решения.
    Ну то есть теоретически, конечно, важно указать на ошибки, но практически опыта чтобы вообще эти ошибки понять ещё нет (я, конечно, могу ошибаться).


    1. koito_tyan Автор
      08.12.2017 21:46

      Спасибо большое)

      Да, я ещё не опытный программист, но стараюсь им стать и именно поэтому выложила эту статью. Где ещё можно услышать критику и советы, кроме как хабра и киберфорума?
      (если есть ресурсы — дайте пожалуйста, буду очень благодарна за развитие моей грамотности)


    1. lair
      08.12.2017 22:37

      А вы просите объяснения почему сделано так, а не иначе. На этом этапе пробуют, а не принимают решения.

      Вопросы "почему так сделано" очень полезны как раз, они заставляют задуматься. Если взято произвольное первое попавшееся решение (как здесь с потоками), то лучше вернуться на шаг назад.


      1. myxo
        08.12.2017 23:22

        Опять же теоретически. На практике когда ты только начинаешь, то пробуешь то, что работает. И не сильно важно что это — потоки, асинхронка, тред пул, между ними просто не видишь разницы. На этом этапе задавать вопрос «почему так сделано» просто бессмысленно, так как предметная область ещё не осела в голове. Гораздо полезнее просто писать рабочий (возможно через задницу) код.


        1. lair
          08.12.2017 23:41

          И не сильно важно что это — потоки, асинхронка, тред пул, между ними просто не видишь разницы.

          Так чтобы увидеть разницу, надо все это попробовать как раз.


          Гораздо полезнее просто писать рабочий (возможно через задницу) код.

          Зачем привыкать писать через задницу?


  1. koito_tyan Автор
    09.12.2017 15:18

    Огромное спасибо всем за правку меня, напомню что я постоянно улучшаю код, а значит что и постоянно меняется код в статье (старые варианты я оставляю).


    1. lair
      09.12.2017 15:32

      Зачем, зачем у вас внутри async-методов await Task.Run?


      1. koito_tyan Автор
        09.12.2017 15:50

        Ибо там мы и запускаем асинхронный код


      1. koito_tyan Автор
        09.12.2017 16:00

        Если вкратце, то этот тестовый код:

        class Program
            {
                public async void MyMethodAsync()
                {
                    Console.WriteLine("Метод MyMethodAsync - до запуска задачи");
                    await Task.Run(
                     () =>
                     {
                         Console.WriteLine("Метод MyMethodAsync - задача запущена.");
                         for (int i = 0; i < 50;)
                             Console.WriteLine("Мы все дружно ждём [" + i++.ToString() + "] раз от потока Асинхронности");
        
                         Console.WriteLine("Метод MyMethodAsync - завершение выполнения задачи");
                     });
                    Console.WriteLine("Метод MyMethodAsync - выполнение задачи завершено");
        
                }
                static void Main(string[] args)
                {
                    Program mc = new Program();
                    Console.WriteLine("Метод Main - до запуска задачи");
                    mc.MyMethodAsync();            
                    Console.WriteLine("Метод Main - после запуска задачи");
                    for (int i = 0; i < 50;)
                        Console.WriteLine("Мы все дружно ждём ["+i++.ToString()+"] раз от потока Main");
                    Console.ReadKey();
                }
            }
        


        Дал такой результат:
        image

        На его основе я думаю, что смогу принимать данные из веб-сокета независимо от ввода клавиши из потока мэйн, если я не права: поправьте, буду благодарна!


        1. lair
          10.12.2017 10:23

          … а теперь уберите await перед Task.Run. Что изменится? Правильно, одна строчка в консоли, и эта строчка не имеет никакого отношения к выполняемой задаче. После этого со спокойной душой можно переделывать MyMethodAsync из async void в просто void, и наблюдать, что тоже ничего не меняется.


          В вашем случае вся конкурентность обеспечена Task.Run, и в этом смысле он ничем не отличается от ThreadPool.QueueUserWorkitem. Ровно то же самое можно написать на чистых тредах с явным управлением, и опять ничего не изменится.


          Так зачем вам нужна эта пара async-await, если вы ей вообще не пользуетесь?


          1. koito_tyan Автор
            10.12.2017 10:25

            Программа приостанавливается на методе ReadKey() и именно в этот момент должна не прекращаться отрисовка и передача данных с сервера.

            Предложите свой вариант, я буду очень признательна.


            1. lair
              10.12.2017 10:31

              Говорю же: убираете из вашего кода async и await и наблюдаете за результатом.


              (вы понимаете, что делает await?)


              1. koito_tyan Автор
                10.12.2017 10:56

                Хорошо, спасибо)

                Не совсем


                1. lair
                  10.12.2017 10:57

                  Ну так может надо сначала разобраться, чем в код запихивать?


                  1. koito_tyan Автор
                    10.12.2017 11:01

                    Хорошо, я верну старое распоточивание, пока не разберусь с асинхронностью в концепции c#


                    1. lair
                      10.12.2017 11:06

                      Тогда придется разобраться, как обойтись без Thread.Abort. Кстати, треды — это, очевидно, не синхронная модель (как вы пишете в посте).


      1. koito_tyan Автор
        09.12.2017 16:25

        Я поняла вас, спасибо за указание на ошибку.

        Для тех кто минусует, прочтите: habrahabr.ru/post/109345


        1. koito_tyan Автор
          09.12.2017 16:31

          А нет, не поняла…


        1. koito_tyan Автор
          09.12.2017 16:42

          А нет, разобралась


  1. Jmann
    09.12.2017 18:36

    С# легкий для изучения язык и вы не знакомы с дженерикамм, делегатами, TPL, пишите в процедурном стиле. Я два года рытаюсь писать и чем дальше, тем больше вопросов, а ещё ведь он обновляется. А комментарии очень конструктивны, особенно по соккетам.


    1. koito_tyan Автор
      10.12.2017 10:15

      Я понимаю что это, но в силу того что мы ещё не прошли по программе я учу это сама и пока что не разобралась полностью. Моя тактика: писать сразу в бой.

      Да, я понимаю что эта тактика очень и очень плоха, но именно это и подталкивает новичков на каких-либо ошибках лезть в оф. документацию и узнавать что-то новое.

      И да, без ошибок не бывает годных проектов, поэтому я и выкладываю сюда, чтобы услышать конструктив.