Pull to refresh

Танчики в консоли, статья первая: «От спора к написанию кода»

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

Так как компьютерных сетей у нас ещё не было, мне пришлось самой учить всё с нуля. Прочитав, пожалуй, страниц 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 за идеи и правки статьи.

Жду ваших пожеланий и идей, да и прибудет с вами сила!
Tags:
Hubs:
+7
Comments38

Articles

Change theme settings