Так как компьютерных сетей у нас ещё не было, мне пришлось самой учить всё с нуля. Прочитав, пожалуй, страниц 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)
unsafePtr
08.12.2017 15:41Замечания:
1. Зачем вам класс Thread? Используйте Task. И почитайте про асинхронность. Судя по вашему методу мейн, если при открытии конзоли вы не нажмете клавишу, то мы понапрасну будем есть очень много ресурсов.
2. Не знаю в какие дебри вы залезли с http, но его использовать будет гораздо проще(потому что это текстовый протокол, а не бинарный коим являеться tcp). Особенно если вы планируете веб приложение, вам надо делать ставку на http.
3. Было бы не плохо выложить ссылку на репозиторий с кодом. В таком соятоянии, я вообще не вижу кода на расте, а по c# вам надо прочитать style guide. Именование методов с подчеркиванием разве что в WinForms я и видел, которые уже устарели.vlreshet
08.12.2017 17:122. Не знаю в какие дебри вы залезли с http, но его использовать будет гораздо проще(потому что это текстовый протокол, а не бинарный коим являеться tcp). Особенно если вы планируете веб приложение, вам надо делать ставку на http.
Если на то пошло — то лучше вскочить в WebSockets. Его и использовать легко (он текстовый), и в вебе он нативный, и нет проблемы двунаправленности сообщений которая есть в http.
koito_tyan Автор
08.12.2017 21:42Хорошо, спасибо огромное)
(пока что с http сложно, ибо сервер будет на Раст, а я пока не сильно разобралась с http сервером в растбуке, но спасибо за пожелание)
lair
08.12.2017 15:52Наша новая задача звучит так: создать структуру
public struct player_coor { public static
Вот только у вас не структура, а пространство имен для бедных. Структура, в которой все поля статические — это боль, а не структура. И в многопоточности вы об нее убъетесь. И
use
вам низачем не нужен, понятное дело.
А вообще, C# — объектно-ориентированный язык, какая религия вам помешала объекты использовать?
koito_tyan Автор
08.12.2017 21:44Религия коллекций и прочитанной литературы по низкоуровневым языкам, спасибо за критику и я изменю код в следующей статье)
lair
08.12.2017 22:36А зачем вы пишете на высокоуровневом языке, как на низкоуровневом? Не надо так делать. В C# и коллекция — объект.
vadimturkov
08.12.2017 17:06А чем вам дженерики не угодили, раз вы ArrayList используете?
koito_tyan Автор
08.12.2017 21:45Дженерики изучены только в расте, простите :p
Учту при создании следующей статьи )
myxo
08.12.2017 21:27Странная статья, конечно. Но куда более странные комментарии. Автор же ясно дал понять, что он новичок. А вы просите объяснения почему сделано так, а не иначе. На этом этапе
пробуют, а не принимают решения.
Ну то есть теоретически, конечно, важно указать на ошибки, но практически опыта чтобы вообще эти ошибки понять ещё нет (я, конечно, могу ошибаться).koito_tyan Автор
08.12.2017 21:46Спасибо большое)
Да, я ещё не опытный программист, но стараюсь им стать и именно поэтому выложила эту статью. Где ещё можно услышать критику и советы, кроме как хабра и киберфорума?
(если есть ресурсы — дайте пожалуйста, буду очень благодарна за развитие моей грамотности)
lair
08.12.2017 22:37А вы просите объяснения почему сделано так, а не иначе. На этом этапе пробуют, а не принимают решения.
Вопросы "почему так сделано" очень полезны как раз, они заставляют задуматься. Если взято произвольное первое попавшееся решение (как здесь с потоками), то лучше вернуться на шаг назад.
myxo
08.12.2017 23:22Опять же теоретически. На практике когда ты только начинаешь, то пробуешь то, что работает. И не сильно важно что это — потоки, асинхронка, тред пул, между ними просто не видишь разницы. На этом этапе задавать вопрос «почему так сделано» просто бессмысленно, так как предметная область ещё не осела в голове. Гораздо полезнее просто писать рабочий (возможно через задницу) код.
lair
08.12.2017 23:41И не сильно важно что это — потоки, асинхронка, тред пул, между ними просто не видишь разницы.
Так чтобы увидеть разницу, надо все это попробовать как раз.
Гораздо полезнее просто писать рабочий (возможно через задницу) код.
Зачем привыкать писать через задницу?
koito_tyan Автор
09.12.2017 15:18Огромное спасибо всем за правку меня, напомню что я постоянно улучшаю код, а значит что и постоянно меняется код в статье (старые варианты я оставляю).
lair
09.12.2017 15:32Зачем, зачем у вас внутри
async
-методовawait Task.Run
?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(); } }
Дал такой результат:
На его основе я думаю, что смогу принимать данные из веб-сокета независимо от ввода клавиши из потока мэйн, если я не права: поправьте, буду благодарна!lair
10.12.2017 10:23… а теперь уберите
await
передTask.Run
. Что изменится? Правильно, одна строчка в консоли, и эта строчка не имеет никакого отношения к выполняемой задаче. После этого со спокойной душой можно переделыватьMyMethodAsync
изasync void
в простоvoid
, и наблюдать, что тоже ничего не меняется.
В вашем случае вся конкурентность обеспечена
Task.Run
, и в этом смысле он ничем не отличается от ThreadPool.QueueUserWorkitem. Ровно то же самое можно написать на чистых тредах с явным управлением, и опять ничего не изменится.
Так зачем вам нужна эта пара
async-await
, если вы ей вообще не пользуетесь?koito_tyan Автор
10.12.2017 10:25Программа приостанавливается на методе ReadKey() и именно в этот момент должна не прекращаться отрисовка и передача данных с сервера.
Предложите свой вариант, я буду очень признательна.lair
10.12.2017 10:31Говорю же: убираете из вашего кода
async
иawait
и наблюдаете за результатом.
(вы понимаете, что делает
await
?)koito_tyan Автор
10.12.2017 10:56Хорошо, спасибо)
Не совсемlair
10.12.2017 10:57Ну так может надо сначала разобраться, чем в код запихивать?
koito_tyan Автор
10.12.2017 11:01Хорошо, я верну старое распоточивание, пока не разберусь с асинхронностью в концепции c#
lair
10.12.2017 11:06Тогда придется разобраться, как обойтись без
Thread.Abort
. Кстати, треды — это, очевидно, не синхронная модель (как вы пишете в посте).
koito_tyan Автор
09.12.2017 16:25Я поняла вас, спасибо за указание на ошибку.
Для тех кто минусует, прочтите: habrahabr.ru/post/109345
Jmann
09.12.2017 18:36С# легкий для изучения язык и вы не знакомы с дженерикамм, делегатами, TPL, пишите в процедурном стиле. Я два года рытаюсь писать и чем дальше, тем больше вопросов, а ещё ведь он обновляется. А комментарии очень конструктивны, особенно по соккетам.
koito_tyan Автор
10.12.2017 10:15Я понимаю что это, но в силу того что мы ещё не прошли по программе я учу это сама и пока что не разобралась полностью. Моя тактика: писать сразу в бой.
Да, я понимаю что эта тактика очень и очень плоха, но именно это и подталкивает новичков на каких-либо ошибках лезть в оф. документацию и узнавать что-то новое.
И да, без ошибок не бывает годных проектов, поэтому я и выкладываю сюда, чтобы услышать конструктив.
lair
… а у вас рисовалка умеет много потоков?
А почему выделенные потоки, а не событийная модель?
А почему не тредпул? Почему не таски с cooperative cancellation?
koito_tyan Автор
Хорошо, я учту это.
Перед написанием следующей статьи я сделаю небольшую ремарку