Как написать бродилку на c

О чем статья?

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

Оглавление

  • Мотивация

    • Зачем так делать?

    • Почему C++?

    • Что нужно знать, чтобы понять статью?

  • Обработка действий игрока в реальном времени

    • Windows API

    • conio.h + getch

  • Запуск периодических событий по таймеру

    • Windows API

    • std::chrono

  • Генератор случайных чисел

  • Изменение размера экрана консоли

  • Ускорение вывода текста

    • Перестановка курсора

    • Вывод готовых фрагментов текста

    • Настройка буферизации stdio.h

    • Измерение производительности

Мотивация

Зачем так делать?

Чтобы научиться программировать. А точнее — чтобы научиться переносить человеческие мысли в код. Обычные задания по программированию плохо тренируют этот навык потому что они скучные. Признаюсь честно, я намеренно готовил скучные задания для своих студентов. Нудные задания на превращение двух чисел в третье неизбежны, чтобы освоить базовые концепции. Такие задания помогут вам спуститься по горной реке, заполненной острыми камнями синтаксиса и водоворотами новых концепций. Проблема в том, что простые понятные задания однажды бросят вас в бескрайнем океане возможностей. Тогда вы обнаружите, что можете плыть куда угодно. Однако никто не подскажет вам верное направление. Ни одна платформа с автоматической проверкой решений не сможет проверить творческую работу и поставить оценку. Это может сделать либо живой преподаватель, либо живой напарник.

В статье ничего не будет про создание конкретной игры. Потому что конкретная игра — конкретные правила. А правила вы должны придумать сами. Дайте волю своему воображению, вспомните свою любимый жанр и сделайте хорошую попытку. Единственное ограничение — мы не будем рисовать красивую картинку. Потому что красивая картинка займет слишком много сил и будет только отвлекать нас от упоения кодом. Скажу по секрету, как только вы сделаете свою игру с помощью символов в черном окошечке, то сможете воплотить ваш замысел на любой другой платформе. Интересная игра будет интересна даже в консоли.

Почему C++?

Главная причина в том, что я сейчас веду индивидуальные занятия по C++ у одного талантливого студента. Его успехи вдохновили меня, а его вопросы показали о чем вообще нужно написать. C++ до сих пор рекомендуют как «язык для обучения». Это вселяет надежду, что статья будет полезна многим. Может быть когда-нибудь я напишу такую же статью и для других языков или для Linux, но не рассчитывайте на это. Если вы напишете сами подобный сборник советов для другого языка, то сообщите мне личным сообщением. Я добавлю ссылку на ваш труд.

Что нужно знать, чтобы понять статью?

Если вы понимаете концепцию циклов, массивов и функций, то вам должно быть достаточно. Предупреждаю сразу, в статье будут магические конструкции, которые я не буду объяснять. У меня нет цели сделать всеобъемлющий курс по C++. Цель — писать код и радоваться тому, как оно почти магически заработает. Когда закончите с основной целью или когда встретитесь с непреодолимыми проблемами, тогда и углубляйтесь.

Обработка действий игрока в реальном времени

Проходя стандартную пачку учебных задач вы скорее всего использовали std::cin или scanf. Программа ждала от пользователя каких-то данных и нажатия кнопки Enter. Вся осмысленная работа происходила уже после того, как исходные данные получены и проверены. Но в настоящих играх специальную кнопку нужно нажимать только в пошаговых стратегиях. Остальные игры реагируют сразу при нажатии нужных кнопок. Поэтому главный прием для написания игры в реальном времени — моментальная обработка нажатия клавиш. Программа не должна ждать нажатия кнопки Enter. Игра должна работать и параллельно обрабатывать нажатие на клавиатуру. В профессиональном сообществе говорят, что когда нужно нажимать кнопку Enter, то это блокирующий ввод. Соответственно, когда программа работает и при этом готова обрабатывать команды — неблокирующий ввод.

Windows API

Когда я погуглил «non-blocking console input», то SO услужливо выдал мне вот такой вопрос https://stackoverflow.com/questions/6171132/non-blocking-console-input-c К сожалению, у него нет того ответа, который нужен нам. Вариант с фоновым потоком я считаю слишком сложным для того, чтобы кодить для своего удовольствия. Вариант с дополнительной нестандартной библиотекой — тоже. Вся остальная выдача не сильно отличалась по смыслу.

Существует ли такая возможность вообще? Наверняка да. Например в SDL.dll мы делаем графическое приложение и основной способ реагировать на клавиатуру с мышью — обрабатывать системные события. Вот есть целая глава руководства https://www.willusher.io/sdl2%20tutorials/2013/08/20/lesson-4-handling-events При этом тащить всю библиотеку SDL мне совсем не хотелось. Но раз нестандартная библиотека может обрабатывать события, значит любой сможет. Таким образом, чтобы программа работала и при этом моментально обрабатывала нажатия на клавиши, нужно отказаться от scanf и std::cin. Нам нужно пойти глубже — на уровень событий.

Поиск по ключевым словам «handle C++ event» выдало целую кучу бесполезной информации по рисованию окон в каком-то из фреймворков windows. Я все еще намерен писать простую игру в консоли, а не оконное приложение, поэтому игнорирую результаты.

Следующая попытка была «cpp event loop with console input» и мне попалась статья https://docs.microsoft.com/en-us/windows/console/reading-input-buffer-events из которой я и взял решение. Считаю это удачной находкой, потому что в процессе написания этой статьи пытался вспомнить свои запросы и даже слегка видоизмененный «event loop c++ with console application» уже не давал нужной информации. Пришлось смотреть историю поиска. Для будущих поколений я добавил сюда явный текст своих запросов, в надежде что будущие поиски будут более результативными.

Подстава в том, что даже эта статья на самом деле показывает пример блокирующего чтения «The function [ReadConsoleInput] does not return until at least one record is available to be read.» Поэтому я посмотрел на соседние статьи и нашел обзор низкоуровневых функций чтения https://docs.microsoft.com/en-us/windows/console/low-level-console-input-functions

В ней описана функция PeekConsoleInput, которая «If no records are available, the function returns immediately.», но которая при этом «Reads without removing the pending input records in an input buffer.». То есть если ее вызвать несколько раз подряд, то она получит информацию об одних и тех же событиях. К счастью там же еще описана функция FlushConsoleInputBuffer, которая удаляет все непрочитанные накопленные события. Сочетание этих двух функций позволит добиться нужного эффекта.

Я видоизменил код из найденной статьи https://docs.microsoft.com/en-us/windows/console/reading-input-buffer-events а именно:

  1. Удалил обработку событий мыши. Если понадобится, спишите ее сами из оригинала.

  2. Удалил колдовство над режимом консоли (функции GetConsoleMode/SetConsoleMode). Для обработки клавиатуры подходит и стандартный режим. Это позволило упростить обработку ошибок в функции ErrorExit.

  3. Добавил сквозной счетчик итераций внешнего цикла. Чтобы было видно работу программы при отсутствии событий.

Получилось вот так:

Обработка событий клавиатуры в консоли Windows API

Обработка событий клавиатуры в консоли Windows API

Я знаю, что публиковать код картинкой — ужасно. Признаюсь, что это намеренно. Ленивые и недостаточно целеустремленные не смогут просто скопировать решение.

Результат запуска обработки событий клавиатуры в консоли Windows API

Результат запуска обработки событий клавиатуры в консоли Windows API

При запуске будет повторяться одна и та же фраза. Например «iteration 468811 total 106 current 2» где значение после iteration — счетчик, который увеличивается при каждой итерации цикла. Даже если мы ничего не будем нажимать. значение после total — количество событий, которые мы обработали значение после current — количество событий, которые нужно обработать в этой итерации цикла

Допустим мы увидели как обрабатывать события клавиатуры не останавливая работу программы. Что можно с этим сделать?

Чтобы собрать больше информации, я поставил точку останова в функции обработки события и понажимал разные кнопки.

Анализ результатов в отладчике

Например вот так выглядит событие при нажатии «ж».

Вот так — нажатие шифта

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

Вот так — символ точка с запятой на английской раскладке. Это та же кнопка, что и «ж».

Вот так — символ точка с запятой на русской раскладке. Это уже другая кнопка — с цифрой «4».

Вот так — доллар. Он тоже на цифре «4», но на английской раскладке.

Обратите внимание, что во всех вариантах нажатия на кнопку «ж», «Ж», «;» в поле wVirtualKeyCode находится одно и то же число. Если использовать это поле, то управление не будет зависеть от раскладки и даже от зажатого шифта и капслока.

Еще важный момент, что wVirtualKeyCode для точки с запятой «;» на разных клавишах разный, но uChar.UnicodeChar у них одинаковый.

Итого с помощью Windows API мы можем:

  1. Различать нажатые клавиши по коду клавиши независимо от раскладки

  2. Различать нажатые клавиши по коду из Юникода независимо от их расположения на клавиатуре.

  3. Обрабатывать нажатия на shift, ctrl, alt.

  4. Понимать, нажат ли сейчас shift, ctrl, alt.

  5. Отличать нажатие клавиши и ее отпускание (буду благодарен, если подберете слово получше).

  6. Прикрутить обработку событий мыши

Если мы можем отличать нажатые кнопки, то можем и по-разному на них реагировать. Например менять координаты персонажа при нажатии на стрелки. Буду использовать обычную для программ координатную сетку. Ноль в ней находится слева сверху. Ось X увеличивается вправо, а ось Y — вниз. Для обозначения координат персонажа объявил две переменные: x и y с начальным значением 10.

В обработчик нажатия клавиш добавил ветвление. В нем проверяется код клавиши и увеличивается соответствующая переменная.

Для проверки запустил и нажал вниз 1 раз, вправо 2 раза, вверх 3 раза, влево 4 раза. Получился вывод как на картинке. Чтобы сделать скриншот, пришлось подключить отладчик.

conio.h + getch

Мой студент параллельно мне нашел способ обрабатывать нажатия на кнопки с помощью комбинации функций kbhit и getch из conio.h. С помощью getch можно получить код нажатого символа, но эта функция ждет следующего нажатия. Чтобы программа при этом продолжала работать, нужно сначала вызвать kbhit. Эта функция вернет true, если нажата хотя бы одна клавиша, но при следующего нажатия ждать не будет. Если ничего не нажато, то kbhit возвращает false и программа работает дальше.

К сожалению, я сходу не разобрался, в какой кодировке этот код для кириллических символов. Я реализовал прототип с помощью conio и поэкспериментировал с нажатием клавиш. Эта библиотека игнорирует нажатие shift, ctrl, alt, caps lock. Точка с запятой «;» на клавише с «ж» вернет код «59», на клавише с «4» тоже получился код 59. У символов в разных регистрах, например «ж» и «Ж» будут разные коды. У меня получились 166 и 134 соответственно. Интересно, что стандартное преобразование «(char)key» превратило эти коды в совершенно другие символы.

Обработка событий клавиатуры conio.h

Обработка событий клавиатуры conio.h
Результат запуска обработки событий с помощью conio.h
Результат запуска обработки событий с помощью conio.h

Получается, что conio проще чем winapi по написанию, но беднее по возможностям. Какой именно способ применить для вашей игры — выбирать только вам.

Запуск периодических событий по таймеру

Как вы можете заметить, если просто отпустить программу в свободный полет, то она будет очень часто обращаться к списку событий. На моем компьютере получается несколько десятков раз за каждую миллисекунду. Само по себе это хорошо, т.к. позволяет оперативно реагировать на все действия игрока. Однако если мир будет меняться с такой же скоростью, то игрок никак не сможет поспеть за ним. Есть несколько способов контролировать период обновления мира. Я выбрал вариант без подключения дополнительных библиотек. Основная идея в том, чтобы постоянно смотреть на время, а обновление мира запускать каждые несколько секунд. Раз уж я докопал до ручной реализации event-loop, то изобрести велосипед с реализацией задержки в рамках выбранной архитектуры не составит труда.

Конкретная реализация нагуглилась с первого запроса «windows.h time milliseconds». https://stackoverflow.com/questions/17008026/windows-how-to-get-the-current-time-in-milliseconds-in-c Для очистки совести я еще попробовал поискать «cpp thread sleep» и «cpp sleep». Первый вариант получился слишком сложный, а второй недостаточно точный.

Поэтому я предлагаю свой вариант. Цель — вписать ожидание в существующий цикл обработки событий. Для этого в начале программы я получаю текущее время. Если бы я работал с обычными часами, то это было бы 17:32:44. Затем я вычисляю время следующего события. Допустим оно должно произойти через 40 минут. Для этого к текущему времени прибавляю длительность ожидания. В моем примере событие произойдет в 18:12:44. Время второго события получается к времени первого события прибавляю длительность ожидания. 18:52:44 Врем третьего события 19:32:44 и так далее.

Существует несколько способов получить текущее время:

  1. С помощью GetSystemTime из windows.h

  2. С помощью std::chrono

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

Получение текущего времени Windows API

Функция GetSystemTime находится в библиотеке . Она записывает структуру с несколькими полями. Каждое поле отвечает за свою компоненту времени: год, месяц, день и так далее включая миллисекунды. Называются они довольно очевидно, так что я думаю вы догадаетесь или воспользуетесь переводчиком. В примере ниже структура со временем лежит в переменной tempTime. В отладчике она выглядит примерно так:

Обратите внимание, что все компоненты времени меняются от 0 до значения не превышающего следующую компоненту. Ну то есть часы от 0 до 23, минуты от 0 до 59. Миллисекунды от 0 до 999. Если сравнивать «в лоб», то возникнет проблема когда последнее обновление было в конце прошлой секунды, а текущее время — в текущей. У этого варианта есть два решения:

  1. Конвертировать полученную структуру в что-то вроде unix timestamp — одно число, начиная с «начала эпохи».

  2. Сделать хитрое сравнение.

Первый вариант может решить проблему только если я воспроизведу вычисление настоящего unix-timestamp, а мне лень. Например там будет сложность с количеством дней в месяце. Поэтому я сделаю менее точную версию, где «начало эпохи» будет в начале текущего месяца.

Код получается вот таким

Получение текущей даты Windows API

Получение текущей даты Windows API

При запуске получается примерно такой вывод:

Результат получения времени из Windows API

Результат получения времени из Windows API

Теперь добавляю дополнительную переменную для хранения времени предыдущего обновления (prevTime) и периода между обновлениями (delay).

Каждую итерацию цикла текущее время сравнивается со временем последнего обновления. Если разница больше задержки, то выполняется обновление. Дополнительно реализована функция для предупреждения перехода через ноль.

При запуске получается примерно такой вывод.

Таким образом команда вывода слова «tick» будет выполняться раз в 300 миллисекунд. После нее нужно помещать логику будущей игры.

Получение текущего времени std::chrono

Реализация с помощью std::chrono получилась значительно проще. Но в ней используется непривычное для обычных людей запись времени. Там нет количества часов с начала дня, минут с начала часа и секунд с начала минуты. С помощью std::chrono можно получить количество миллисекунд с «начала эпохи». То есть с 1 января 1970 года. Это получается огромное целое число. Например 1610827417491. В отличие от «количества миллисекунд с начала секунды», это число всегда увеличивается. Поэтому не возникнет необходимости беспокоиться о переполнении часов.

В этом варианте я также использовал другой способ сравнения времени. На одно арифметическое действие в цикле меньше. Мелочь, а приятно.

Получение текущего времени std::chrono

Получение текущего времени std::chrono

Константа PRIu64 нужна для форматированного вывода значения типа uint64_t с помощью printf. Для её использования нужно подключить inttypes.h Хотелось бы, конечно, не подключать лишних библиотек, но времени на обход именно этой библиотеки у меня не было.

Вывод получился вот таким

Вывод от получения текущего времени std::chrono

Вывод от получения текущего времени std::chrono

Что с этим можно сделать? Навскидку мне в голову пришло:

  1. Симуляция гравитации в платформере. Каждые Х миллисекунд персонаж должен падать на 1 символ.

  2. Полет пуль. Каждые Х миллисекунд передвинь пулю по направлению выстрела.

  3. Движение змейки. Каждые Х миллисекунд передвинь сегменты змейки на один символ от головы.

Что самое важное, у разных событий может быть разный период обновления. Вот прототип, где каждые 300 миллисекунд у персонажа уменьшается координата Х на 1, а каждые 225 миллисекунд — координата Y. При этом игрок может на WASD влиять на эти координаты гораздо чаще.

На картинке ниже вывод, который получается в такой программе время. Обратите внимание на чередование фраз «Each 225» и «Each 300».

Генератор случайных чисел

Если немного копнуть в алгоритм генераторов случайных чисел, то можно обнаружить, что они не такие уж случайные. Поэтому используют более точный термин Генератор псевдо-случайных чисел или ГПСЧ.

По запросу «с++ rand» и «cpp rand» можно найти довольно много материалов. Например обширную статью на русском https://en.cppreference.com/w/cpp/numeric/random/rand и чуть более сухую на английском https://en.cppreference.com/w/cpp/numeric/random/rand

Вкратце перескажу основные тезисы:

  1. Функция rand возвращает случайное число от 0 до RAND_MAX. Значение RAND_MAX зависит от библиотеки, но должно быть не менее 32767.

  2. Если просто вызывать функцию rand, то при разных запусках, последовательность случайных чисел будет одинаковой.

  3. Чтобы последовательность была каждый раз разной, нужно применить функцию srand.

Я от себя добавлю, что аргумент функции srand еще называют зерном (или «сидом» от слова seed). Вы подобное видели во многих играх при создании мира.

Примеры начальных значений для ГПСЧ

Если зерно мира заполнять одним и тем же значением, то получатся одинаковые последовательности случайных значений. В играх получатся одинаковые миры. Вот пример использования ГСПЧ с постоянным значением зерна.

При запуске у меня появляется фраза «first is 440 second is 19053». Независимо от количества запусков получаются одни и те же числа. У вас могут получиться другие числа, но от запуска к запуску они должны быть одинаковы.

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

Ответ — время запуска. Простейший способ задания зерна случайности — взять текущее время. Обычно во всяких примерах предлагают подключить библиотеку ctime и вызывать функцию std::time(nullptr). Этот вариант не устраивает меня потому что библиотека ctime больше ни для чего не используется. Зато при реализации периодических событий была подключена библиотека chrono. В примере ниже зерно мира задается текущим временем.

Инициализация зерна случайности с помощью std::chrono

Инициализация зерна случайности с помощью std::chrono

Я варварски сконвертировал uion64_t в int, но этого оказалось достаточно.

Следующая проблема в том, что числа получаются огромные. Повторюсь, что rand возвращает число от 0 до RAND_MAX, который у меня равен 0x7fff. В десятичной системе счисления диапазон случайных чисел получается от 0 до 32767.

Классический способ уменьшить этот диапазон — применить операцию «остаток от деления». Выражение выглядит как «std::rand() % boundary», где boundary — новое ограничение диапазона.

Вы когда-нибудь задумывались, а почему остаток от деления нам действительно помогает? Не получится ли такого, что какое-то число будет встречаться чаще других?

Строгое математическое доказательство будет чрезмерным для этой статьи. Поэтому постараюсь объяснить «на пальцах».

Посмотрите на два столбца чисел. Левый столбец — просто числа по порядку от 0 до 14. Правый столбец — остаток от деления числа из левого столбца на 4.

0 0

1 1

2 2

3 3

4 0

5 1

6 2

7 3

8 0

9 1

10 2

11 3

12 0

13 1

14 2

Как вы можете заметить, диапазон от 0 до 14 разделился на несколько отрезков от 0 до 3. Числа в правом столбце возрастают так же равномерно, как и в левом. Точно такая же закономерность прослеживается и на диапазоне от 0 до 32767. Функция rand() может вернуть каждое число «из левого столбца» с равной вероятностью. После нахождения остатка от деления мы получим соответствующее число из правого столбца.

Обратите внимание, что последний диапазон «от 0 до 3» в правом столбце не успел закончиться. Если числа в левом диапазоне будут выбираться с равной вероятностью, то числа 0, 1, 2 из правого столбца будут появляться чаще, чем число 3. То же самое будет и на полном масштабе, если 32767 не будет делиться нацело на выбранное вами ограничение. Впрочем, исходный диапазон рандома достаточно велик, чтобы мы не заметили этот небольшой недостаток. В документации на cppreference в формуле выбора числа есть попытка компенсировать его делением на «((RAND_MAX + 1u)/6)».

Как уже говорилось выше, рандом возвращает числа от 0 до ограничения. Проблема в том, что при написании игры нужно использовать числа не от 0, а например от 10 до 20. В реализации std::rand приходится упражняться со сложением и вычитанием. К счастью, я нашел ссылку на статью с описанием синтаксиса, который был добавлен в стандарт C++ от 2011 года. https://en.cppreference.com/w/cpp/numeric/random/uniformintdistribution В нем есть красивый способ описать рандом в нужном диапазоне. Немножко оформления и можно просто получать случайные числа в нужном диапазоне.

#include <random>
#include <stdio.h>

int main()
{
	std::random_device rd;//Источник зерна для рандома
	std::mt19937 gen(rd());//Вихрь Мерсенна (Mersenne Twister) 
	std::uniform_int_distribution<> oneToSix(1, 6);//функция распределения от 1 до 6
	std::uniform_int_distribution<> twentyToForty(20, 40);//функция распределения от 20 до 40
  //Два числа в диапазоне от 1 до 6
  int first = oneToSix(gen);
  int second = oneToSix(gen);

  printf("%d %d ", first, second);
  
  //два числа в диапазоне от 20 до 40
  int third = twentyToForty(gen);
  int fourth = twentyToForty(gen);

  printf("%d %d", third, fourth);
 
  return 0;
}

Подробнее о Вихре Мерсенна можно почитать на Википедии https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D1%85%D1%80%D1%8C_%D0%9C%D0%B5%D1%80%D1%81%D0%B5%D0%BD%D0%BD%D0%B0

Меня такая находка очень порадовала. Пора переходить в туториалах для новичков на стандарт C++ хотя бы десятилетней давности. Этот фрагмент кода я специально для вас оформил текстом, который можно скопировать.

Изменение размера консоли

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

Изменение размера окна консоли тоже оказалось задачкой с подвохом. Первый запрос был тривиальным «c++ change size of console window». Первый ответ на него подробно объяснял как сделать это с помощью настроек окна консоли на уровне операционной системы. То есть не из самой игры, а со стороны пользователя. Прикладывать эту инструкцию к игре я посчитал неправильным. Нужен способ сделать это из самой программы. Второй и последующие ответы описывали изменение размера окна консоли с помощью функции MoveWindow. Фактическое количество текста при этом не менялось. Если окно становилось слишком маленьким, то появлялись полосы прокрутки.

Следующая попытка была «c++ set console size». Два первых ответа вели на известные советы с функцией MoveWindow. Зато дальше пошли ссылки на документацию. А именно — на функцию SetConsoleScreenBufferSize. Судя по описанию, она меняет не размер видимого окна, а внутренний размер буфера. В качестве аргументов она принимает поток вывода и структуру с желаемыми размерами буфера.

На тот момент я не знал точно, какие размеры стандартные и что я могу туда поставить. Поэтому указал 20 на 20. Для проверки размеров окна я также вывел прямоугольник из цифр от 0 до 9 шириной 20 на 20. Получился вот такой код:

Заполнение экрана символами и изменение размера консоли

Заполнение экрана символами и изменение размера консоли

Вывод получился вот таким

Поскольку это работа на уровне WinAPI, в результате получился код ошибки. Я в основном работаю с java стеком и обычно вижу стектрейсы и тексты исключений. Несмотря на это, принцип решения проблемы не изменился. Для расшифровки кода ошибки нужно воспользоваться официальной документацией. Она легко ищется запросом «getlasterror error codes». Кодов ошибок описано около девяти тысяч на нескольких страницах. Для моего случая подойдет первая страница https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes—0-499-

Ошибка гласит ERROR-INVALID-PARAMETER 87 (0x57) The parameter is incorrect.

Маловато объяснений. Тогда я проверил как другие пишут этот код. Запрос «SetConsoleScreenBufferSize incorrect argument» привел меня вот на этот вопрос на SO https://stackoverflow.com/questions/12900713/reducing-console-size

В ключевых аспектах код ответа был похож на мой. Но в нем содержалось важное дополнение «If you call SetConsoleScreenBufferSize with illegal value in COORDS (e.g. too little height/width) then you get an error, usually 87 ‘invalid argument’.»

Потом я посмотрел в документацию к функции SetConsoleScreenBufferSize https://docs.microsoft.com/en-us/windows/console/setconsolescreenbuffersize и увидел что на размеры буфера наложены ограничения. Получается, что я передал слишком маленькие значения. У меня не было необходимости перебирать значения для получения точных минимальных размеров. В конце концов цель — увеличить размеры буфера, а не уменьшить. Поэтому показалось логичным отталкиваться от текущих размеров окна. Раз у нас есть функция SetЧтототам, значит должна быть и функция GetЧтототам. GetConsoleScreenBufferInfo действительно нашлась https://docs.microsoft.com/en-us/windows/console/getconsolescreenbufferinfo С помощью неё и отладчика MSVS я выяснил, что размеры буфера на моей машине по умолчанию 80 на 50. Ширину я увеличил примерно в три раза, а высоту в полтора. При инициализации структуры size значением X = 200 и Y = 80 в высоту появились полосы прокрутки. Здесь и пригодилась функция MoveWindow.

Исходный код был видоизменен вот так:

Вывод при этом получился таким

Ускорение вывода текста

Запустив программу для заполнения большого экрана символами, я обнаружил, что текст пишется в консоль очень медленно. Заполнение символами экрана 200 на 80 заметно человеческому глазу. За одну секунду получится обновить экран лишь 1-2 раза. Это вряд ли связано с производительностью компьютера. Интуиция подсказывает, что это искусственное ограничение. При решении этой проблемы у меня было два направления поиска:

  1. Как быстро написать много текста?

  2. Как обновить только тот фрагмент экрана, который действительно менялся?

Сначала я поискал «cpp console reduce delay between screen updates». Практически все ссылки вели на советы по добавлению паузы, что мне совершенно не интересно. Только один ответ в выдаче говорил что-либо об ускорении вывода https://stackoverflow.com/questions/26376094/c-writing-to-console-without-delays. В нем предлагается подготовить большой буфер в памяти и вывести его одной командой.

Затем я поискал «windows.h write lot of text without animation» и нашел вот такой вопрос с очень любопытным ответом. https://stackoverflow.com/questions/34842526/update-console-without-flickering-c

Автор вопроса и автор ответа разговаривают в контексте создания консольной игры. Вместо полной очистки и полного заполнения экрана в ответе предлагается:

  1. Переставить курсор и писать новый текст поверх старого без предварительной очистки экрана.

  2. Переписывать фрагменты, а не отдельные буквы.

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

  4. Использовать два буфера и писать на экран только разницу.

Обратите внимание, в ответе на вопрос есть еще пример настройки цвета текста в консоли. У меня, к сожалению, не хватило времени воспроизвести этот прием.

Перестановка курсора

Простой перенос курсора в верхний левый угол экрана значительно улучшил ситуацию на windows 7. Я все еще видел процесс заполнения экрана, но текст на экране не исчезал два раза в секунду. У меня пропадали некоторые линии. У моего студента была windows 10 и без дополнительных ухищрений было видно только мигание самого курсора в разных частях экрана. Пропадания линий замечено не было.

За основу был взят код из главы «Изменение размера консоли».

Для своего удобства я реализовал функцию заполнения структуры нужными координатами.

Функция подготовки структуры с координатами

Функция подготовки структуры с координатами

Для перестановки курсора достаточно вызвать функцию SetConsoleCursorPosition и передать ей в качестве аргументов поток вывода и структуру с координатами желаемой позиции курсора.

Вызов функции SetConsoleCursorPosition для перестановки курсора

Вызов функции SetConsoleCursorPosition для перестановки курсора

Как я уже говорил, это улучшило ситуацию, но проблему полностью не решило. Поэтому я решил раскопать поглубже.

Вывод готовых фрагментов текста

Следующая попытка — писать символы не по одному, а строчками. Для этого нужно подготовить массив символов в памяти, который я далее буду писать как целую строку. Длина этого массива совпадает с шириной окна консоли. Во внутреннем цикле массив будет заполняться, а во внешнем — выводиться на экран. Ранее я выводил цифры с помощью форматированного вывода. Теперь нужно писать именно цифры. Для простой конвертации числа в символ нужно знать коды этих символов. У цифры «0» код символа 48, у цифры «9» — 57. То есть к числу от 0 до 9 включительно достаточно просто прибавить 48. Напомню, что последний символ в буфере обязательно должен быть «», чтобы не ловить баги, связанные с выводом нежелательных символов. За основу я взял код из раздела про изменение размера консоли.

Вывод текста фрагментами

Вывод текста фрагментами

В примерах еще часто использовался std::cout.flush(), который тащит за собой подключение iostream. Мне не хотелось использовать дополнительную библиотеку. Наверняка аналог есть и в stdio, который уже подключен. Мой запрос для поиска был «stdio flush output». Две ссылки на Stackoverflow указывают на fflush

  • https://stackoverflow.com/questions/12450066/flushing-buffers-in-c/12450125

  • https://stackoverflow.com/questions/1716296/why-does-printf-not-flush-after-the-call-unless-a-newline-is-in-the-format-strin

Функция fflush вызывается с аргументом stdout. Я минут 10 искал как правильно заполнить переменную stdout, а оказалось она просто доступна из глобальной области видимости.

Настройка буферизации stdio.h

Подозреваю, что в stdio.h буферизация вывода уже реализована, поэтому мой код скорее всего оказался велосипедом. На момент оформления этой статьи я уже забыл как искал информацию. Главное — для настройки буфера с помощью stdio.h нужно воспользоваться функцией setvbuf. Она принимает stdout, буфер, какие-то флаги и число — размер буфера.

Настройка буфера вывода с помощью stdio.h

Настройка буфера вывода с помощью stdio.h

Измерение производительности

Тут я осознал, что не в состоянии сравнить производительность разных вариантов «на глаз». Поэтому вкрутил измерение времени до и после обновления экрана. Напишу тут порядок цифр, потому что значения с точностью до микросекунд не существенны для сравнения. Код практически полностью списал из ответа к этому вопросу https://stackoverflow.com/a/21856299

Функция получения времени с точностью до микросекунд

Функция получения времени с точностью до микросекунд

Само измерение — тривиально. Получаю текущее время до и после отрисовки экрана. Затем вычитаю и вывожу на экран. Есть способы лучше, но для моей задачи этого оказалось достаточно. Код ниже — развитие варианта с настройкой буфера с помощью stdio.h Код варианта с велосипедным буфером принципиально не отличался, поэтому не публикую его.

Измерил количество микросекунд без явного указания буфера. Три запуска, три числа: 8930000 8880000 9220000.

с размером буфера 1/16 от 740000 до 750000 микросекунд

с размером буфера 1/8 от 40000 до 39000 микросекунд

с размером буфера 1/4 от 18000 до 19000 микросекунд

с размером буфера 1/2 от 12000 до 13000 микросекунд

с размером буфера равным ширине консоли от 90000 до 10000.

Во всех этих случаях наблюдается мигание строк на экране размером с буфер. То есть при буфере 1/4, мигают фрагменты в четверть строки. С буфером равным ширине консоли получается очень большой разброс, причем без промежуточных значений. либо за 10000, либо за 90000. При этом мигает так, как будто буфер половина. redraw настройка буфера библиотеки.PNG

Велосипедный вариант с заполнением массива и выводом его на экран получился такой же, как с размером буфера равным ширине консоли. Были цифры 90000 и 10000 без промежуточных значений. При этом для создания буфера меньшего размера пришлось бы значительно усложнить реализацию. redraw велосипедный буфер.PNG

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

Заключение

К сожалению, новогодние праздники кончились и у меня сильно сократилось время на написание статьи. Вряд ли я когда либо еще плотно займусь этой темой. Поэтому я остановлюсь на достигнутом, чтобы не закопаться в перфекционизме.

В качестве бонуса — ссылка на подробный разбор вывода русского текста в консоль. https://ru.stackoverflow.com/questions/459154/%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9-%D1%8F%D0%B7%D1%8B%D0%BA-%D0%B2-%D0%BA%D0%BE%D0%BD%D1%81%D0%BE%D0%BB%D0%B8

P.S. Если вы нашли опечатки или ошибки в тексте, пожалуйста, сообщите мне. Это можно сделать выделив часть текста и нажав «Ctrl / ⌘ + Enter», если у вас есть Ctrl / ⌘, либо через личные сообщения. Если же оба варианта недоступны, напишите об ошибках в комментариях. Спасибо!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Почему начали изучать программирование?


7.62%
Чтобы получать большую зарплату
8


37.14%
Чтобы разрабатывать игры
39


12.38%
Чтобы оживлять железки
13

Проголосовали 105 пользователей.

Воздержался 21 пользователь.

Извиняюсь за не столь содержательный заголовок

Весьма странно начинать лог разработки не рассказав об игре хотя бы двух слов, да ещё и издалека — предметы и инвентарь.
Будет затронута только программная часть т.е. без графики, а всю информацию будем выводить в консоль. В конце лог-статьи будет приведён код на языке С++.

А теперь приступим :)

Для начала определить структуру предмета необходимо… или же нет?
На самом деле, следует знать зачем эти всякие предметы нужны и на что они влияют, но тут ответ более-менее ясен — характеристики персонажа, но вот что это за характеристики? Это и следует обозначить.

// Характеристики
struct sAttributes
{
	short health;		// Здоровье
	short shield;		// Щит
	short defence;	// Оборона (глушитель урона)
	short damage;	// Урон
};

Если Вам знаком синтаксис языка Cи, то вопросов возникнуть не должно, а для тех кто «не в теме», следует пояснить кое-что, но не углубляться в подробности: (лучше найти информацию в интернете, а то я плохо объясняю)

struct название_структуры
{
	тип переменная;	// комментарий
};

Теперь у нас есть структура определяющая характеристики.
Можно завести структуру и для персонажа, затем для предмета, но лучше не спешить и немного подумать.

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

// Качество
enum eQuality
{
	EQ_COMMON   =0,	// Обычный
	EQ_STANDARD =1,	// Стандартный (в чём отличие от обычного?)
	EQ_HANDMADE,	// Самопальный
	EQ_LEGENDARY,	// Легендарный
	EQ_EPIC,		// Эпичный

	Count_Quiality	// так узнаем кол-во
};

// Тип предмета, если установлен в OTHER, то может быть произвольный
enum eKind // TYPE
{
	Kind_WEAPON = 0,	// Оружие
	Kind_ARMOR,			// Броня
	//Kind_POTION,
	Kind_OTHER
};

// Бит-флаги предмета
enum eItemFlag
{
	IF_CLEAR			= 0x0000,	

	IF_EQUIP			= 0x0001,	// Можно экипировать
	IF_COLLECTABLE	= 0x0002,	// Занимает один слот, записывая кол-во
	IF_USABLE			= 0x0004,	// Можно использовать
	IF_QUEST			= 0x0008	// Предмет, необходимый по сюжетуквесту (нельзя выбросить, слот не учитывается)
};

// Описание предмета для глобального списка
struct sItemDescription
{
	// Уникальный ID
	unsigned long	ID;

	string		 name;	// название предмета для отображения
	string		 desc;	// описание
	sAttributes	 attrib;

	eQuality	qual;	// качество
	eKind		kind;	// тип
	long		 falgs;	// бит-флаги
	
	// Визуальная информация
	int			modelID;	// номер 3Д модели
	int			textureID;	// номер текстуры для 3Д модели
	int			pictureID;	// номер картинкитекстурыиконки для отображения в инвентаре
	// Цена предмета, можно задать вручную или же воспользоваться специальной функцией
	int	cost; 
};

— Эй! Что такое unsigned long, string и eQuality ?
unsigned означает, что наша переменная имеет исключительно положительное значение (для целочисленных типов).
long — аналогично как и short, однако, занимает больше памяти и следовательно может иметь больше значений.
string — тип описывающий строку текста (из стандартной C++ библиотеки).
eQuality и eKind — перечисления.

Код, возможно, кого-то испугает, но на самом деле всё очень просто.
Мы имеем структуру описания предмета — это означает, что используя этот «бланк» можно будет обозначить любой предмет в игре.
Вот пример такого определения:

sItemDescription SuperSword;
SuperSword.ID = CurID++; // Для каждого предмета считается свой ID
SuperSword.name 	= 	"Супер меч";
SuperSword.desc 	= 	"Лишь избранный может нести его";

SuperSword.attrib.health		= 0;
SuperSword.attrib.shield		= 0;
SuperSword.attrib.defence	= 0;
SuperSword.attrib.damage	= 20;

SuperSword.kind		= Kind_WEAPON;	 // Тип "оружие"
SuperSword.qual		= EQ_LEGENDARY;	 // Качество предмета как "Легендарный"
SuperSword.falgs	= IF_EQUIP;			 // можно экипировать

SuperSword.cost = ItemCost( SuperSword ); // Вычислим стоимость предмета

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

Это функция, принимающая в качестве аргумента требования к процедурному предмету (смотри ниже).

sItemDescription GenerateItem( const sItemGenerationDesc &gendesc )
{
	sItemDescription desc;

	desc.ID = CurID++;

	desc.kind = gendesc.kind;
	desc.qual = gendesc.quality;

	desc.attrib.health = 0;
	desc.attrib.defence = 0;
	desc.attrib.damage = 0;
	desc.attrib.shield = 0;

	if( desc.kind == Kind_WEAPON )
	{
		desc.name = QualityToString(desc.qual) + " " + weapons_names[ RandomRange(0,weapons_names_count) ];
		// Лихо рассчитываем параметр используя качество, начальное значение и великий рандом 
		desc.attrib.damage = gendesc.attribs_ranges.damage + ((int)desc.qual) * RandomRange( 0, 4 );
	
		desc.falgs = IF_EQUIP;

	}else
	if( desc.kind == Kind_ARMOR )
	{
		desc.name = QualityToString(desc.qual) + " " + armor_names[ RandomRange(0,armor_names_count) ];
		desc.attrib.shield = gendesc.attribs_ranges.shield + ((int)desc.qual) * RandomRange( 0, 2 );
		desc.attrib.defence = gendesc.attribs_ranges.defence + RandomRange( 0, 2 );
		desc.falgs = IF_EQUIP;
	}

	desc.cost = ItemCost( desc ); // Вычисляем цену
	desc.desc = "Этот предмет сгенерирован программой"; // описание описания, извините за тавтологию 
	
	return desc;
}

А что-то за структура sItemGenerationDesc ?

struct sItemGenerationDesc
{
	eQuality		quality;			// Качество, которое мы хотим дать предмету
	eKind		kind;			// Тип предмета - оружие, броня или может ещё что
	sAttributes	attribs_ranges; 	// Разброс по атрибутам или их начальное значение
};

Таким образом, можно манипулируя тремя параметрами получить различные предметы.
Должен заметить, что с названием предметов есть неприятный момент, например, такой — «ЭПИЧНЫЙ труба» … такое можно исправить, но это уже на будущее :)

Вот скриншот с консоли программы, демонстрирующие генератор предметов:

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

Это первая статья из цикла, поэтому принимаю различные предложения по улучшению подачи контента )
Хотелось бы разрабатывать игру не просто для себя, а совместно с сообществом.

На радаре концепт арты врагов! Если не терпится посмотреть на них, то можете разыскать их на сайте стопгейм.

Бродилка в консоли, мигает герой

18.05.2017, 17:56. Показов 1389. Ответов 0


Здравствуйте. Пишу обычную «бродилку» для консоли. Суть: герой может со стрелок ходить по «полю» и стрелять в направлении последнего движения. Также есть рандомно двигающийся враг (пока что один). Поражение врага пока не реализовано.
Собственно вопрос: как по-умному реализовать все так, чтобы герой и противник могли двигаться одновременно? Сейчас это выглядит так: враг двигается нормально, герой при нажатии стрелки пропадает и появляется там, где должен. Это выглядит плохо и неудобно. Как я понимаю, это нужно делать через System.Threading.Thread.Sleep(), но как именно?

Program.cs:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace _20170512_ExamTask
{
    class Program
    {
       public static bool wasLeft,
                          wasUp,
                          wasDown = false;
       public static bool wasRight = true;
        static void Main(string[] args)
        {            
            Hero h3;
            Bullet b3;
            Enemy e3;
            UI.GetHero(out h3);
            UI.GetEnemy(out e3);
            Console.CursorVisible = false;      
            ConsoleKey key = ConsoleKey.Spacebar;
 
            do            
            {
                UI.ShowHero(h3);
 
                if (Console.KeyAvailable)
                {
                    key = Console.ReadKey(true).Key;
 
                    switch (key)
                    {
                        case ConsoleKey.LeftArrow:
                            Hero.MoveHero(-1, 0, ref h3);
                            Hero.ClearHero(h3, ConsoleKey.LeftArrow);
                            wasLeft = true;
                            wasRight = false;
                            wasUp = false;
                            wasDown = false;
                            break;
                        case ConsoleKey.RightArrow:
                            Hero.MoveHero(1, 0, ref h3);
                            Hero.ClearHero(h3, ConsoleKey.RightArrow);
                            wasLeft = false;
                            wasRight = true;
                            wasUp = false;
                            wasDown = false;
                            break;
                        case ConsoleKey.UpArrow:
                            Hero.MoveHero(0, -1, ref h3);
                            Hero.ClearHero(h3, ConsoleKey.UpArrow);
                            wasLeft = false;
                            wasRight = false;
                            wasUp = true;
                            wasDown = false;
                            break;
                        case ConsoleKey.DownArrow:
                            Hero.MoveHero(0, 1, ref h3);
                            Hero.ClearHero(h3, ConsoleKey.DownArrow);
                            wasLeft = false;
                            wasRight = false;
                            wasUp = false;
                            wasDown = true;
                            break;
                        case ConsoleKey.E:
                            if (wasLeft)
                            {
                                b3.x = h3.x - 1;
                                b3.y = h3.y;
                                UI.ShowBullet(b3);
                                Bullet.BulletWay(b3.x, b3.y, ref b3);
                            }
                            if (wasRight)
                            {
                                b3.x = h3.x + 1;
                                b3.y = h3.y;
                                UI.ShowBullet(b3);
                                Bullet.BulletWay(b3.x, b3.y, ref b3);
                            }
                            if (wasUp)
                            {
                                b3.x = h3.x;
                                b3.y = h3.y - 1;
                                UI.ShowBullet(b3);
                                Bullet.BulletWay(b3.x, b3.y, ref b3);
                            }
                            if (wasDown)
                            {
                                b3.x = h3.x;
                                b3.y = h3.y + 1;
                                UI.ShowBullet(b3);
                                Bullet.BulletWay(b3.x, b3.y, ref b3);
                            }
 
                            break;
                        default:
                            break;
                    }
                    
                }
 
                UI.HideEnemy(e3);
                Enemy.EnemyMovement(e3.x, e3.y, Randomizer.GetRandomDirection(), ref e3);
                UI.ShowEnemy(e3);
                System.Threading.Thread.Sleep(100);
            } while (key != ConsoleKey.Escape);
            Console.ReadKey();
        }
 
    }
}

Hero.cs — структура, реализующая героя:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace _20170512_ExamTask
{
 
    struct Hero
    {
        public int x;
        public int y;
        public char symbol;
 
        public static void MoveHero(int dx, int dy, ref Hero h)
        {
            h.x += dx;
            h.y += dy;
        }
        public static void ClearHero(Hero h, ConsoleKey keyPressed)
        {
            switch (keyPressed)
            {
                case ConsoleKey.LeftArrow:
                    Console.SetCursorPosition(h.x + 1, h.y);
                    Console.Write(" ");
                    break;
                case ConsoleKey.RightArrow:
                    Console.SetCursorPosition(h.x - 1, h.y);
                    Console.Write(" ");
                    break;
                case ConsoleKey.UpArrow:
                    Console.SetCursorPosition(h.x, h.y + 1);
                    Console.Write(" ");
                    break;
                case ConsoleKey.DownArrow:
                    Console.SetCursorPosition(h.x, h.y - 1);
                    Console.Write(" ");
                    break;
                default:
                    break;
            }
        }
    }
}

Enemy.cs — структура, реализующая врага:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace _20170512_ExamTask
{
    enum Direction
    {
        NoDirection,
        Left,
        Right,
        Up,
        Down
    }
 
    struct Enemy
    {
        public int x;
        public int y;
 
        public static void MoveEnemy(int dx, int dy, ref Enemy e)
        {
            e.x += dx;
            e.y += dy;
        }
        public static void EnemyMovement(int x, int y, Direction direction, ref Enemy e)
        {
            int oldX = x;
            int oldY = y;
 
            switch (direction)
            { 
                case Direction.Right:
                    MoveEnemy(1, 0, ref e);
                    break;
                case Direction.Left:
                    MoveEnemy(-1, 0, ref e);
                    break;
                case Direction.Up:
                    MoveEnemy(0, -1, ref e);
                    break;
                case Direction.Down:
                    MoveEnemy(0, 1, ref e);
                    break;
                default:
                    break;
            }
        }
    }
}

Bullet.cs — структура, реализующая полет снаряда:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace _20170512_ExamTask
{
    struct Bullet
    {
        public int x;
        public int y;
        // + Direction
 
        public static void MoveBullet(int dx, int dy, ref Bullet b)
        {
            b.x += dx;
            b.y += dy;
        }
        public static void BulletWay(int x, int y, ref Bullet b)
        {
            if (Program.wasRight)
            {
                for (int i = x; i < Console.BufferWidth; i += 3)
                {
                    Console.SetCursorPosition(i, y);
                    Console.Write("*");
                    System.Threading.Thread.Sleep(25);
                    Console.SetCursorPosition(i, y);
                    Console.Write(" ");
                    System.Threading.Thread.Sleep(25);
                }
            }
            if (Program.wasLeft)
            {
                for (int i = x; i > 0; i -= 3)
                {
                    Console.SetCursorPosition(i, y);
                    Console.Write("*");
                    System.Threading.Thread.Sleep(25);
                    Console.SetCursorPosition(i, y);
                    Console.Write(" ");
                    System.Threading.Thread.Sleep(25);
                }
            }
            if (Program.wasUp)
            {
                for (int i = y; i > 0; i -= 3)
                {
                    Console.SetCursorPosition(x, i);
                    Console.Write("*");
                    System.Threading.Thread.Sleep(25);
                    Console.SetCursorPosition(x, i);
                    Console.Write(" ");
                    System.Threading.Thread.Sleep(25);
                }
            }
            if (Program.wasDown)
            {
                for (int i = y; i < Console.BufferHeight; i += 3)
                {
                    Console.SetCursorPosition(x, i);
                    Console.Write("*");
                    System.Threading.Thread.Sleep(25);
                    Console.SetCursorPosition(x, i);
                    Console.Write(" ");
                    System.Threading.Thread.Sleep(25);
                }
            }
        }
    }
}

UI.cs — класс, реализующий отображение всех элементов игры:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace _20170512_ExamTask
{
    class UI
    {
        //const int minWidth = 15;
        //const int maxWidth = 50;
        //const int minHeight = 10;
        //const int maxHeight = 50;
 
        public static void GetHero(out Hero h)
        {
            h.x = 10;
            h.y = 10;
            h.symbol = '0';
        }
 
        static int pointPosTop;
        static int pointPosLeft;
 
        public static void ShowHero(Hero h)
        {
            Console.SetCursorPosition(h.x, h.y);
            Console.Write(h.symbol);
            pointPosTop = Console.CursorTop;    // сохранение позиции по вертикали
            pointPosLeft = Console.CursorLeft;
        }
        public static void ShowBullet(Bullet b)
        {
 
            Console.SetCursorPosition(b.x, b.y);
            Console.Write(".", b.x, b.y);
        }
        public static void GetEnemy(out Enemy e)
        {
            e.x = Randomizer.GetRandomCoord(10, 70);
            e.y = Randomizer.GetRandomCoord(10, 50);
           
        }
        public static void ShowEnemy(Enemy e)
        {
            Console.SetCursorPosition(e.x, e.y);
            Console.Write("E", e.x, e.y);
            Console.SetCursorPosition(pointPosLeft, pointPosTop);
        }
 
        public static void HideEnemy(Enemy e)
        {
            Console.SetCursorPosition(e.x, e.y);
            Console.Write(' ');
            Console.SetCursorPosition(pointPosLeft, pointPosTop);
        }
    }
}

Randomizer.cs — класс для получения необходимых рандомных значений:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace _20170512_ExamTask
{
    class Randomizer
    {
        public static Random Rnd = new Random();
 
        public static Direction GetRandomDirection()
        {
            Direction retDirection = Direction.NoDirection;
 
            //[PointColors.NoColor .. PointColors.Blue
            //[                  0 .. 3]
 
            retDirection = (Direction)(Rnd.Next((int)Direction.NoDirection) + Rnd.Next(1, 4));
 
            return retDirection;
        }
 
        public static int GetRandomCoord(int min, int max)
        {
            return Rnd.Next(min, max);
        }
 
        //public static Point PointGetRandomPoint()
        //{ 
        //}
 
    }
}

Если нужно, могу прикрепить весь проект, не знаю, разрешено ли это правилами.

__________________
Помощь в написании контрольных, курсовых и дипломных работ, диссертаций здесь



0



Привет, Вектозавры! Сегодня я расскажу о том, как за 15 дней я написал свой онлайн шутер от первого лица. В этом ролике я с самого начала продемонстрирую, как создать свою псевдо 3D игру в стиле DOOM или Wolfenstein 3D.

Мы начнем с установки необходимой библиотеки, рисования объектов и управления камерой с клавиатуры.
После этого мы научимся строить 3D изображение, добавим освещение и управление мышью.
Далее мы реализуем текстурирование и сделаем нашу игру светлой и красивой. В такую игру уже захочется поиграть.
Мы добавим объекты разной высоты, скины, оружия и врагов, а также зеркала, в которых будет видно отражение объектов. А потом посмотрим, что будет, если поставить два зеркала напротив друг друга.
Ну и в конце концов, мы добавим онлайн в игру, чтобы можно было играть с другом. В общем, вас ждет большая и очень интересная статья, приятного чтения, вектозавры!

Первая часть статьи.

В предыдущем ролике я показал, как можно с помощью алгоритма ray-cast и консольной графики сделать простую бродилку. Много кому ролик понравился, некоторые даже сами пытались реализовать свою версию и, как по мне, получилось очень здорово!

В этот раз я захотел написать полноценную игру.
Конечно, можно писать игру на Unity или каком-нибудь другом движке, который предоставляет огромные возможности, но я захотел сделать всё сам и самостоятельно написать движок для игры.

В качестве основы я выбрал библиотеку SFML, которая позволяет рисовать линии, многоугольники и окружности. Также она дает возможность удобного использования клавиатуры, добавления звуков и загрузки изображений. В общем все, что нужно для того, чтобы писать игру и не заморачиваться над не существенными деталями на низком уровне.

Подключение SFML

Первое, что нужно сделать — это подключить эту библиотеку. Это не самая простая задача — пришлось хорошенько покопаться в мануалах и исходниках.
Естественно, подключилась SFML далеко не с первого раза, но меня это не испугало и в конце концов я смог разобраться.

Для проверки корректности работы попробуем нарисовать зелёную окружность.

Ура! Как видно, всё отлично работает, а значит можно приступать к проектированию каркаса проекта и реализации первых и самых важных интерфейсов.

Формирование каркаса игры

Основой будет является класс «Мир», в котором будут храниться все объекты на карте. Важно отметить тот факт, что все объекты представляют из себя набор из точек в двумерном пространстве. То есть реально никакого 3D не будет, но далее я создам иллюзию трёхмерного изображения и игроку будет казаться, что он бегает по трёхмерной карте.

Половину дня я потратил на проверку и отладку кода, но в итоге всё-таки смог нарисовать первый объект на карте.

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

По сути класс «Мир» готов и теперь можно создать небольшую 2D сцену из нескольких объектов. Хорошо бы также добавить на сцену камеру и прописать управление с клавиатуры.

2D карта, камера и управление

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

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

Ray-cast и получение 3D изображения

Теперь самое главное – движок игры. Нужно сделать так, чтобы камера могла рисовать изображение на экран, и чтобы это выглядело, как настоящее 3D. Для этого, мы будем использовать алгоритм ray-cast.
Его суть заключается в следующем: Игрок пускает луч до стены. Если мы найдем точку пересечения луча и препятствия, то сможем определить расстояние от камеры до стенки. Так вот если стенка близко, то мы в этом направлении нарисуем большую полоску, а если стенка далеко, то и полоска должна быть маленькой.

Так мы делаем для всех направлений в пределах угла обзора. Пускаем лучи во все стороны и мерим расстояния. Вот и всё.

Настолько простой алгоритм дает возможность создать иллюзию реального 3D. Ну то есть если не разбираешься, то с ходу и не отличишь от реального трехмерного изображения.
Так я и написал предыдущую консольную версию.

Встаёт проблема определения расстояния до двумерное объекта. На Хабре я нашел мощный и простой алгоритм обнаружения пересечения отрезков. Это, по сути, главный алгоритм во всей игре. Им я буду пользоваться буквально постоянно.

Задача состояла только в его адаптации под мои структуры. После того, как реализован алгоритм, остаётся только пустить лучики по всей зоне видимости и определить расстояния.
Как оказалось, это не самая простая задача: поначалу всё очень тормозило, а камера неправильно обнаруживала расстояния. В итоге получалось, что я как будто вижу невидимую стену прямо перед собой.

Естественно, это было связано с тем, что я ошибся при реализации алгоритма. Итерационно, исправляя ошибки и отлаживая результат, я пришел к тому, что всё работает как мне нужно.

Видно, что лучи огибают двумерные объекты и камера может понять на каком расстоянии от неё находится стена. Чудненько! Ведь это всё значит, что можно переходить к построению изображения с камеры.
Перед тем, как переходить к псевдо 3D я решил сделать небольшую тестовую комнату, по которой будут разбросаны препятствия.

Сейчас можно убедиться, что всё работает правильно и лучи испытывают коллизию со всеми объектами на сцене.
Сделаем масштаб немного поменьше и белый задний фон. Мы получили небольшую карту, которую уберём в самом конце, а сейчас будем использовать для отладки.

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

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

Видно, что результат почти правильный, но что-то явно не так. Оказалось, проблема не в математике, а в том, что я неправильно использовал библиотеку. Только минут через 30 понял, что неправильно рисую полоску.
Исправил и всё сразу заработало.

Отлично – выглядит очень круто. Единственное, можно более далёкие полоски делать более темными чтобы создать иллюзию освещения и придать объем изображению. Поигравшись с углом обзора, я получил довольно-таки красивый результат.

Всё плавно и красиво – можно идти дальше!

Текстурирование

Хорошо бы теперь сделать так, чтобы стенки могли иметь какую-нибудь текстуру. Я полез в поисковик и нашел подходящую для кирпичной стены.

Теперь немного отвлечёмся на то, как именно я придумал реализовать текстурирование. Я не видел этого метода в интернете и придумал это сам, поэтому вкратце расскажу, как это происходит.
Допустим мы имеем текстуру стены. Теперь мы кромсаем её на вертикальные полоски и каждую из этих полосок накладываем на соответствующую полоску стены.

В принципе, это и есть весь алгоритм. Очень простой и понятный.
Теперь каждый объект имеет свою текстуру в качестве свойства и каждая точка на поверхности объекта теперь соответствует конкретному сдвигу в текстуре.

Естественно, более далёкие участки текстуры мы будем рисовать более темными.
Теперь можно найти, например, текстуру колонны и применить её для боковых колонн.
Здесь нужно обратить внимание на то, что при моём подходе, рисуя вертикальные полоски, мы не сможем сделать красивый пол. Небо можно сделать, просто рисуя текстуру, которую мы будем сдвигать при повороте камеры. В итоге получится эффект параллакса и объёма, а пол я решил пока просто сделать однотонным.
Важно, чтобы текстура неба была бесшовная, то есть при её склейке не должно быть разрывов. Тогда начало и конец без видимых полос соединяться друг с другом.

Теперь игра стала выглядеть светлой и смотрится намного лучше. И весь этот результат я добился всего за 5 дней. Но нужно понимать, что я программировал практически в режиме «нон-стоп» по 6-8 часов каждый день.
Мне и вправду стало очень интересно, что может получится и я решил продолжить, хотя изначально я задумывал остановиться на этом.

Оружие

Какой шутер может обойтись без оружия? Нужно скорее добавлять! Сначала я написал класс “Weapon”, содержащий всю необходимую логику, вроде стрельбы, подсчета количества патронов и скорости перезарядки. Класс «Camera» (то есть игрок) будет содержать в себе массив оружий и индекс выбранного оружия. Да, сначала оружие будет только одно, но я делаю задел на будущее.
Каждое оружие имеет свою текстуру. Я полез в поисковик и нашел вот такой вот дробовик.

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

Стоило добавить одну текстуру, а выглядеть стало в тысячи раз круче!
Можно добавить реализма, смещая текстуру оружия по синусу с постоянной скоростью. Получится эффект дыхания.
Чтобы выстрел выглядел красиво, нужно добавить анимацию перезарядки. Для этого я в Фотошопе разрезал текстуру оружия на три составляющие, одну из которых немного дорисовал. Таким образом, рука будет всегда находится над стволом, а приклад и ружье над рукой. Перемещая руку, можно создать иллюзию перезарядки. Скорость стрельбы оружия можно будет менять (например, прокачивая ловкость игрока) и поэтому скорость анимации тоже будет меняться.

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

Коллизия камеры со стенками

Раз уж делать полноценную игру, так делать по совести – нужно заняться коллизией камеры со стенами, чтобы человек не мог ходить сквозь препятствия.
Теперь вместо того, чтобы кидать лучи только в направлении взгляда я кидаю их вообще во все стороны. Зачем это нужно? Если я обнаруживаю объекты, которые потенциально могут испытать коллизию с игроком, то я добавляю их в массив «collision» потом при смещении я прохожусь по всем потенциальным коллизиям.

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

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

То есть объекты дают возможность выходить из текстуры, но входить внутрь запрещается.

Меню игры

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

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

Зеркала и стены разной высоты

Этот проект я начал показывать своим знакомым и один из них предложил добавить зеркала и стены разной высоты. В принципе, ray-cast позволяет запросто сделать зеркало. Если стенка является зеркалом, то нужно просто отразить пришедший луч и рекурсивно продолжить вычисления.

Единственный нюанс, который нужно понять – это как правильно отразить луч от данной поверхности с известным вектором нормали. Это чисто математическая задача, которую я быстро решил.

Кстати, у меня есть замечательная школа математики для программистов – Академия вектозавров. Первую, бесплатную, главу пройти обязательно всем! :)

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

Обрисовывать препятствия можно с помощью алгоритма художника, начиная с самых дальних и заканчивая самыми ближними. Алгоритм художника заключается в том, что объекты, которые должны находится ближе и перекрывать собой дальние, рисуются самыми последними на экране.
После долгих часов отладки и доработки движка я все-таки смог реализовать зеркала. Теперь можно увидеть самого себя. Для персонажа я выбрал первую попавшуюся мне текстуру.

Видно, что, как и следует, изображение в зеркале симметрично отображается и если за зеркалом текстура Анны смотрит на зеркало, то из зазеркалья Анна смотрит на нас. Надписи и текст тоже отлично отражаются.
А сейчас читатель, наверное, уже задался вопросом: «А что будет, если поставить два зеркала напротив друг друга?». Давайте проверим, как движок с этим справляется.

Очень круто! Все работает как надо.
Чтобы добиться такого результата нужно только не забыть ограничить глубину рекурсии лучей. Когда я этого не сделал и подходил к зеркалам, игра сразу падала, ведь стек быстро переполнялся.
Можно увеличить радиус обзора, но тогда сразу будет заметно, что игра начала тормозить.

Вот ради такой красоты я и занимаюсь программированием. Простая идея, а столько всего красивого можно получить.
Движок, по сути, в каком-то виде уже готов. Нужно только добавить врагов и обработку столкновения пули с противником.

Противник и обработка выстрелов

Чтобы добавить противника достаточно просто добавить еще одну камеру на карту.

И вот, противник готов.
Но как сделать обработку выстрелов? Я сделал так: после выстрела я пускаю луч в направлении взгляда. Если этот луч пересекает какую-нибудь камеру, то мы уменьшаем у неё количество здоровья.

Естественно, я использую тот же самый алгоритм бросания лучей, что и при измерении расстояний. Фактически ничего нового я вообще не писал, а просто использовал уже готовую функцию.
Но как мы помним, при встрече на пути зеркала луч рекурсивно отражается. Так как я использовал ту же функцию, то и пули так-же будут отражаться от зеркал.

Это вводит новую интересную механику в игру, ведь теперь если умеешь пользоваться зеркалами, можно убивать врагов, стреляя прямо в зазеркалье. Урон от пуль зависит от расстояния до цели. То есть если стоять вплотную, то враг сразу погибает, а если стрелять издалека или, например, через зеркало, то урон будет куда меньше.

Multiplayer

По сути, сейчас уже можно играть, но проблема в том, что не с кем. Что же, придётся решать эту проблему.
Давайте добавим возможность играть нескольким игрокам на одной карте. В SFML есть встроенные сокеты, позволяющие обмениваться сообщениями между клиентами по протоколу TCP или UDP. Я решил выбрать UDP, так как моя домашняя сеть достаточно надёжная и ошибки доставки встречаются редко, а если и встречаются, то это не особо скажется на геймплее.
Сначала задача была написать простое клиент-серверное приложение. На стороне сервера перемещается окружность, а клиент должен показывать, где сейчас эта окружность находится.

Возникла такая проблема: я изменяю координату кружка на сервере, а кружок на клиенте сдвигается с огромной задержкой. Практически сразу же я понял, что сервер затапливает UDP пакетами клиента. Поэтому клиенту надо читать все сообщения из буфера до тех пор, пока не будет прочитано последнее.
Несколько часов и кривая версия онлайн готова: игроки видят друг друга и могут стрелять в друг друга. Если запас жизней кончается, то игрока телепортирует на место старта.

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

Проектирование карты для сражений

Теперь нужно сделать интересную карту для deathmatch’a и можно будет звать друзей играть.
Сначала я набросал будущую карту на листе, а потом расставил колонны по углам, чтобы потом соединить их кирпичной стенкой.

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

Результаты

Можно наконец начать тест игры. Урон по противнику наносится, а значит, всё работает правильно.

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

То есть если тебя подбили, то ты некоторое время медленно двигаешься и должен отсидеться в укрытии. Потом здоровье восстановится, и ты снова готов играть дальше.

В общем, пока как-то так. Этот проект я не забрасываю и буду развивать его дальше. Подпишись на канал, если хочешь первым узнать о выходе новой части. Делать такие большие и информационно объемные ролики очень сложно, особенно учитывая, что я работаю один, так что ваша подписка и будет мотивировать меня делать видео дальше.
Огромное спасибо моим пятерым спонсорам на patreon. Сумма выходит не большая, но тот факт, что вы поддерживаете меня и следите за каналом намного важнее любых денег.

GitHub & Как запустить игру?

А теперь для тех, кто хочет попробовать запустить эту игру или покопаться в коде. На своём GitHub’е я создал репозиторий с проектом, откуда его можно скачать. Тут же будет список того, над чем я работаю в данный момент и что уже сделано.

Для того, чтобы всё заработало, нужно поставить библиотеку OpenAL. Заходим в папку “Release” и запускаем .exe файл. Если вы увидели эту менюшку, то поздравляю, всё заработало.

Планы на будущее

Вы можете предлагать всевозможные дополнения в игру, и я с радостью добавлю понравившиеся мне фитчи.
По мере возможности я стараюсь отвечать на каждый ваш вопрос, но зато я точно читаю абсолютно все ваши комментарии. Так что добро пожаловать, делитесь мнением или темой, которая вам показалось интересной или непонятной. Тут рады каждому новому Вектозаврику.


Друзья! Я очень благодарен вам за то, что вы интересуетесь моими работами, ведь каждый пост на сайте даётся очень непросто. Я буду рад любому отклику и поддержке с вашей стороны.

Если у вас остались вопросы или пожелания, то вы можете оставить комментарий (регистрироваться не нужно)

Ivan Ilin:

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

Дата: 15-03-2020 в 12:47

Анонимно:

молодец хороший контент делаешь

Дата: 24-03-2020 в 13:10

Dat boi:

Круто! Отличная работа. Надеюсь, это перерастет во что-то большее

Дата: 26-03-2020 в 08:39

Анонимно:

Предложение! Можно добавить физику, если это возможно! Сделать карту по красивее и объемнее (аналог того же De_DUst) ! Попробовать это все перевести на API Vulkan и DX12, если это возможно! Конечно я понимаю, что не все сразу! Можно еще и с шейдерами поиграться

Дата: 26-03-2020 в 09:36

Cat-code:

Я бы хотел увидеть игру не на райкастинге а игру где мир описан обьектами с точками и текстурами .Я пытался что то сделать но фпс падал из за того что я поворачивал всю карту вокруг игрока а не взгляд -я делал массив точек относительно игрока потом умнажал на матрицу поворота затем рисовал текструры по растоянию .(формула перспективы и перевода в 2д по X «Z это кордината дальности»-(точкаX+игрокX)*(fov/(точкаZ+игрокZ))

Дата: 30-03-2020 в 02:13

Анонимно:

Предложение! Можно сделать игру на одного игрока или мультиплеер, где надо справляться с «волнами» врагов» К примеру они могут выходить из какой-нибуть двери, А после убийства определенного количества врагов может выходить бос(он будет с большим количеством HP и моделька к примеру будет по больше)

Дата: 05-04-2020 в 10:26

Назар Ус:

гле можно скачять игру

Дата: 09-04-2020 в 09:54

Анонимно:

Не могли бы вы пояснить начинающему программисту, что делать после скачивания с репозитория, чтобы можно было самому менять что-то в игре, ибо visual studio отказывается что-либо билдить?

Дата: 12-04-2020 в 17:39

Анонимно:

Используй gpu и твоя игра похожа на Team Fortres 2

Дата: 13-04-2020 в 10:32

Кашпировский:

Хотел денюшку перевести, но карту заблокировало

Дата: 09-07-2020 в 19:53

Анонимно:

А почему не Simple Directmedia Layer (SDL) http://libsdl.org ?

Так игра бы собиралась под многие платформы.

Удачи.

Дата: 15-10-2020 в 16:14

Дмитрий:

Здравствуйте,
На github нет текстур , а очень хотелось бы попробовать разобраться. Без них не работает.
Закиньте, пожалуйста, текстуры куда-нибудь для скачивания, хоть на тот же гитхаб!!

Дата: 31-10-2020 в 14:21

Анонимно:

а.и.твия.ватжчваьтжьвпждлрояжвдла

Дата: 02-11-2020 в 18:07

лявоамдфокруькумьжьщу:

юалтюМЛЫОы

Дата: 02-11-2020 в 18:08

Анонимно:

Круто продолжай дальше, в том-же духе на игрой. Желаю успехов с игрой в дальнейшем.

Дата: 07-12-2020 в 13:18

Анонимно:

Как ты реализовал коллизию?

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

Сижу голову ломаю, а всё равно стенку либо насквозь проходит, либо прям в ней застревает :(
————
tg @nekotorio

Дата: 03-02-2021 в 13:23

vladislaw2020:

Памагите!Как сделать текстуры вообще не понимаю.Я тоже делаю шутер на C++.

Дата: 31-03-2021 в 08:27

Анонимно:

чел а код где, самый сок решил себе оставить?

Дата: 20-06-2021 в 19:11

Анонимно:

Visual studio ничего не билдит.

Дата: 25-06-2021 в 13:02

Aleksey:

Иван, Вы большой молодец! Делайте телеграм-канал, а пока Ваш сайт в закладках)
Успехов Вам!

Дата: 17-01-2022 в 23:47

CD-DWD:

а где код?

Дата: 11-06-2022 в 09:26

Анонимно:

прикольно автор харош

Дата: 07-01-2023 в 17:46

  • Subject:
    C# Tutorials
  • Learning Time: 2 hours

Hi Welcome to this tutorial. This tutorial is an updated version of the C# Snakes game tutorial we have done before on the website.  This video tutorial will explain all of the core components needed to make this classic game in windows form in visual studio with C# programming language. We wont be using any oher game frameworks to make this work however we will be using some OOP programming practices in this tutorial. Everything you need to know and how to use them inside of the game effectively is explained in the tutorial.

Lesson objectives –

  1. Make a full snakes game in visual studio
  2. Using custom classes to load and reload default settings
  3. Using custom classes to draw the snake and food to the game
  4. Working with Keyboard controls
  5. Keeping score and high score in the current game session
  6. Able to take snap of the game when it ends
  7. Starting and Restarting the game to replay

Full Video Tutorial on How to make the classic snake game in Visual Studio with C#

 
Download Snake Game Project Tutorial on MOOICT GitHub
 

Source Code –

Circle Class –

This class will help us draw circles to the form, we will use this one to determine the X and Y position of the circles in the screen. This will be used for both the SNAKE and the Food objects inside of the game.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Classic_Snakes_Game_Tutorial___MOO_ICT
{
    class Circle
    {
        public int X { get; set; }
        public int Y { get; set; }

        public Circle()
        {
            X = 0;
            Y = 0;
        }
    }
}

Settings Class

This is the settings class for the game. This class will be used to load up the default settings for the game objects only. It will load the size in height and width of the circles that will be drawn to the project also the direction the snake should be moving when the game initially loads.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Classic_Snakes_Game_Tutorial___MOO_ICT
{
    class Settings
    {

        public static int Width { get; set; }
        public static int Height { get; set; }

        public static string directions;

        public Settings()
        {
            Width = 16;
            Height = 16;
            directions = "left";
        }
    }
}

Main Form C# Scripts

This is the main form1.cs code. In this script we have given all of the instructions necessary for the game and how it will behave when the game loads up. We have several events in the code below for example the Key Down, Key Up, Timer,  2 Click Events and a Paint Event. Also we have 3 custom functions such as Restart Game, Game Over and Eat Food function for the snake.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.Drawing.Imaging; // add this for the JPG compressor

namespace Classic_Snakes_Game_Tutorial___MOO_ICT
{
    public partial class Form1 : Form
    {

        private List<Circle> Snake = new List<Circle>();
        private Circle food = new Circle();

        int maxWidth;
        int maxHeight;

        int score;
        int highScore;

        Random rand = new Random();

        bool goLeft, goRight, goDown, goUp;


        public Form1()
        {
            InitializeComponent();

            new Settings();
        }

        private void KeyIsDown(object sender, KeyEventArgs e)
        {

            if (e.KeyCode == Keys.Left && Settings.directions != "right")
            {
                goLeft = true;
            }
            if (e.KeyCode == Keys.Right && Settings.directions != "left")
            {
                goRight = true;
            }
            if (e.KeyCode == Keys.Up && Settings.directions != "down")
            {
                goUp = true;
            }
            if (e.KeyCode == Keys.Down && Settings.directions != "up")
            {
                goDown = true;
            }



        }

        private void KeyIsUp(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Left)
            {
                goLeft = false;
            }
            if (e.KeyCode == Keys.Right)
            {
                goRight = false;
            }
            if (e.KeyCode == Keys.Up)
            {
                goUp = false;
            }
            if (e.KeyCode == Keys.Down)
            {
                goDown = false;
            }
        }

        private void StartGame(object sender, EventArgs e)
        {
            RestartGame();
        }

        private void TakeSnapShot(object sender, EventArgs e)
        {
            Label caption = new Label();
            caption.Text = "I scored: " + score + " and my Highscore is " + highScore + " on the Snake Game from MOO ICT";
            caption.Font = new Font("Ariel", 12, FontStyle.Bold);
            caption.ForeColor = Color.Purple;
            caption.AutoSize = false;
            caption.Width = picCanvas.Width;
            caption.Height = 30;
            caption.TextAlign = ContentAlignment.MiddleCenter;
            picCanvas.Controls.Add(caption);

            SaveFileDialog dialog = new SaveFileDialog();
            dialog.FileName = "Snake Game SnapShot MOO ICT";
            dialog.DefaultExt = "jpg";
            dialog.Filter = "JPG Image File | *.jpg";
            dialog.ValidateNames = true;

            if (dialog.ShowDialog() == DialogResult.OK)
            {
                int width = Convert.ToInt32(picCanvas.Width);
                int height = Convert.ToInt32(picCanvas.Height);
                Bitmap bmp = new Bitmap(width, height);
                picCanvas.DrawToBitmap(bmp, new Rectangle(0,0, width, height));
                bmp.Save(dialog.FileName, ImageFormat.Jpeg);
                picCanvas.Controls.Remove(caption);
            }





        }

        private void GameTimerEvent(object sender, EventArgs e)
        {
            // setting the directions

            if (goLeft)
            {
                Settings.directions = "left";
            }
            if (goRight)
            {
                Settings.directions = "right";
            }
            if (goDown)
            {
                Settings.directions = "down";
            }
            if (goUp)
            {
                Settings.directions = "up";
            }
            // end of directions

            for (int i = Snake.Count - 1; i >= 0; i--)
            {
                if (i == 0)
                {

                    switch (Settings.directions)
                    {
                        case "left":
                            Snake[i].X--;
                            break;
                        case "right":
                            Snake[i].X++;
                            break;
                        case "down":
                            Snake[i].Y++;
                            break;
                        case "up":
                            Snake[i].Y--;
                            break;
                    }

                    if (Snake[i].X < 0)
                    {
                        Snake[i].X = maxWidth;
                    }
                    if (Snake[i].X > maxWidth)
                    {
                        Snake[i].X = 0;
                    }
                    if (Snake[i].Y < 0)
                    {
                        Snake[i].Y = maxHeight;
                    }
                    if (Snake[i].Y > maxHeight)
                    {
                        Snake[i].Y = 0;
                    }


                    if (Snake[i].X == food.X && Snake[i].Y == food.Y)
                    {
                        EatFood();
                    }

                    for (int j = 1; j < Snake.Count; j++)
                    {

                        if (Snake[i].X == Snake[j].X && Snake[i].Y == Snake[j].Y)
                        {
                            GameOver();
                        }

                    }


                }
                else
                {
                    Snake[i].X = Snake[i - 1].X;
                    Snake[i].Y = Snake[i - 1].Y;
                }
            }
            picCanvas.Invalidate();
        }

        private void UpdatePictureBoxGraphics(object sender, PaintEventArgs e)
        {
            Graphics canvas = e.Graphics;

            Brush snakeColour;

            for (int i = 0; i < Snake.Count; i++)
            {
                if (i == 0)
                {
                    snakeColour = Brushes.Black;
                }
                else
                {
                    snakeColour = Brushes.DarkGreen;
                }

                canvas.FillEllipse(snakeColour, new Rectangle
                    (
                    Snake[i].X * Settings.Width,
                    Snake[i].Y * Settings.Height,
                    Settings.Width, Settings.Height
                    ));
            }


            canvas.FillEllipse(Brushes.DarkRed, new Rectangle
            (
            food.X * Settings.Width,
            food.Y * Settings.Height,
            Settings.Width, Settings.Height
            ));
        }

        private void RestartGame()
        {
            maxWidth = picCanvas.Width / Settings.Width - 1;
            maxHeight = picCanvas.Height / Settings.Height - 1;

            Snake.Clear();

            startButton.Enabled = false;
            snapButton.Enabled = false;
            score = 0;
            txtScore.Text = "Score: " + score;

            Circle head = new Circle { X = 10, Y = 5 };
            Snake.Add(head); // adding the head part of the snake to the list

            for (int i = 0; i < 100; i++)
            {
                Circle body = new Circle();
                Snake.Add(body);
            }

            food = new Circle { X = rand.Next(2, maxWidth), Y = rand.Next(2, maxHeight)};

            gameTimer.Start();

        }

        private void EatFood()
        {
            score += 1;

            txtScore.Text = "Score: " + score;

            Circle body = new Circle
            {
                X = Snake[Snake.Count - 1].X,
                Y = Snake[Snake.Count - 1].Y
            };

            Snake.Add(body);

            food = new Circle { X = rand.Next(2, maxWidth), Y = rand.Next(2, maxHeight) };
        }

        private void GameOver()
        {
            gameTimer.Stop();
            startButton.Enabled = true;
            snapButton.Enabled = true;

            if (score > highScore)
            {
                highScore = score;

                txtHighScore.Text = "High Score: " + Environment.NewLine + highScore;
                txtHighScore.ForeColor = Color.Maroon;
                txtHighScore.TextAlign = ContentAlignment.MiddleCenter;
            }
        }
    }
}

Нашли для вас серию видео, в которой автор по шагам рассказывает, как написать свою игру на C++. Эти уроки не требуют каких-то продвинутых знаний, но базовые представления о программировании приветствуются.

Урок 0 — Введение и подготовка

В этом уроке автор расскажет, как пишутся игры. Также вы загрузите компилятор и напишите простой helloworld в Visual Studio, чтобы всё было готово для разработки игры.

Смотреть урок 0

Урок 1 — Окна и указатели

Здесь вы перейдёте к первому шагу на пути к игре: созданию окон. К концу урока у вас всё будет готово для отображения графики в окне.

Смотреть урок 1

Урок 2 — Графика

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

Смотреть урок 2

Урок 3 — Ввод, движение и время

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

Смотреть урок 3

Урок 4 — Геймплей, столкновения, улучшение передвижения

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

Смотреть урок 4

Урок 5 — Вражеский ИИ, подсчёт очков, завершаем геймплей

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

Смотреть урок 5

Урок 6 — Завершаем игру

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

Смотреть урок 6

Игровая индустрия стремительно развивается. Одним из самых распространенных языков разработки игр и иного развлекательного контента является C++. Он достаточно сложный, особенно на первых порах. Также популярностью пользуется его предшественник – C.

Если верить Google, написать собственную игру может каждый. Для этого предстоит выбрать язык разработки, а затем выучить его. Далее – определиться с типом приложения, реализовать в виде кода.

В данной статье будет рассказано о том, как написать собственную игру на C++. Рассмотрим ключевые моменты процесса, разберемся с мотивацией. Информация пригодится и новичкам, и продвинутым разработчикам.

Мотивационный вопрос

Перед тем, как программировать, нужно задуматься над истинной мотивацией идеи. Google указывает на то, что создать собственную игру «просто так» не получится. Что-то должно двигать человеком.

Игровой код на C++ — это отличный способ потренироваться в разработке, изучить ее азы. А еще – создать проект, который может принести прибыль при грамотной реализации.

Почему C++

Игра – это простой программный код. Но на C++ можно создавать совершенно разные приложения. Игры – лишь начало. Данный язык программирования является универсальным и самым популярным.

Он выбирается разработчиками по нескольким причинам:

  • широкая распространенность;
  • возможность создавать не только простые проекты, но и сложные;
  • кроссплатформенность;
  • функциональность;
  • удобство работы с кодом.

Google указывает на то, что нынешний рынок труда остро нуждается в программистах на C++. Начиная с мелких игр, человек сможет отработать навыки и знания, дорасти до крупных масштабных работ.

Ключевые компоненты проекта

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

  • графику;
  • логику;
  • интерфейс;
  • звук;
  • историю;
  • игровой процесс (физику).

Каждый компонент требует тщательной проработки. Новички могут использовать для быстрого создания игры готовые движки. Они включают в себя необходимые элементы для разработки развлекательного контента.

Графическая составляющая

Это – картинка на экране. Включает в себя изображения и эффекты. Сюда можно отнести:

  • 3D-компоненты;
  • текстуры;
  • 2D-плитки;
  • 2D-модели;
  • видео с полным движением (FMV).

Над графикой лучше работать дизайнерам. Программистам рекомендуется сконцентрироваться на создании «внутренности» будущей игры. А визуальную составляющую оставить тем, кто занимается этим вопросом профессионально.

Звуковые эффекты

Звук – еще один важный компонент игры. Google подчеркивает, что сюда относят не только музыку, но и эффекты. Они будут воспроизводиться во время работы приложения. Сюда также относят эффекты Фоули («отголоски» окружающей среды) и мод-треки.

История

Предыстория игры, включающая в себя всю информацию, полученную от игрока в процессе работы приложения. Пример – выигрыши и проигрыши. История является игровым элементом. Это – неизменяемая последовательность. Можно охарактеризовать ее «прогрессом».

Физика

То, как будут взаимодействовать объекты на экране. Проработка физической составляющей, согласно Google – это трудный и важный процесс. «С нуля» им занимаются преимущественно опытные разработчики. Остальные предпочитают пользоваться готовыми библиотеками, фреймворками и движками.

Как можно создавать игры

В Google удается отыскать тысячи ответов на вопрос о том, как можно создавать игры на любом выбранном языке. Это относится и к простым программным кодам, и к сложным. Пользователь может воспользоваться:

  1. Написанием кода через библиотеки и фреймворки, с нуля. Неплохой вариант для небольших проектов.
  2. Собственным движком. Такой подход присущ большинству крупных компаний. Пример – ReEngine от Capcom.
  3. Готовым движком. Наиболее подходящий вариант для быстрого старта. Игровые движки включают в себя «все необходимое» для того, чтобы человек мог написать собственную игру без существенных навыков в программировании.

Далее рассмотрим первый вариант. За основу возьмем несколько элементарных игр. Если хочется получить на выходе более сложный и совершенный проект, рекомендуется присмотреться к движкам.

Алгоритм работ

Писать программы, согласно Google, рекомендуется, следуя конкретному алгоритму. В противном случае процесс рискует затянуться на месяцы, а то и годы. Особенно если речь идет о команде новичков.

Программный игровой код лучше составлять так:

  1. Продумать общую концепцию. Составить историю игры.
  2. Собрать команду помощников. Самостоятельно работать над небольшим проектом – это нормально. Но, если на C++ планируется релиз игры не по типу «змейки» или «угадай число», лучше запастись поддержкой. Это поможет ускорить процесс разработки.
  3. Продумать интерфейс, графику и физику. Этот шаг пропускается, если разработчик предпочел пользоваться движками.
  4. Непосредственно создать игровой код. Это – самый сложный и важный этап. Он требует определенных навыков программирования.
  5. Провести тест. Без тестирования и проверок ни один контент не может рассчитывать на успешный релиз.
  6. Исправить обнаруженные ошибки и неполадки. В конце – снова проверить работоспособность кода.

Когда все готово, можно использовать последний шаг – релиз игры. Пример – на своем сайте или в фирменных интернет-магазинчиках.

Данный алгоритм универсален. Он, по Google, подойдет не только для игры на C++, но и для любого другого языка программирования.

Змейка

«Змейка» — популярная и простая игра, которая нравилась всем детям 90-х. Она подходит для любых платформ, включая мобильные операционные системы. Сейчас можно отыскать огромное количество вариантов «Змейки», включая онлайн версии.

За несколько свободных вечеров, обладая минимальными знаниями в C++, удастся написать собственную «Змейку из начала 90-х». Она не выделяется графикой, но общий смысл создания игры на выбранном языке программирования станет понятен. Данный проект назовем Oldschool Snake.

Принцип игры

Для того, чтобы начать процесс, нужно продумать правила и особенности управления:

  1. Движение змейки осуществляется за счет курсора мыши.
  2. При нажатии на Esc происходит выход из игры.
  3. Если система спрашивает «Еще раз?», требуется кликнуть по N на клавиатуре. Это приведет к завершению игрового процесса.
  4. На собственных хвост и стенки змея на экране не должна натыкаться. Это приводит к проигрышу. То есть, к смерти змейки.
  5. Движение «хвостом вперед» не поддерживается. Если так попытаться сделать, последует мгновенная смерть.
  6. Для роста змеи необходимо кормить ее. Для этого – собирать доллары на экране. Чем больше съела змейка, тем крупнее (длиннее) она становится.
  7. Каждая «подкормка» приносит определенное количество баллов. Они помогут сформировать ТОП-10 рейтинг.

Рейтинг, получаемый в игре, прямо пропорционален длине змейки, а также обратно пропорционален времени, потраченному на съедение очередной порции «подпитки».

В Google можно отыскать немало других правил и игровых особенностей для «змеек». Только начинать разработку рекомендуется с такого элементарного алгоритма. Он не позволит пользователю запутаться.

Лицензия

Google указывает на то, что любая игрушка или иной компьютерный софт должны обладать лицензией. Рекомендуется на первых порах отдать предпочтение GNU GPL.

Это – вариант для «свободных программистов». Позволяет спокойно распространять контент, изучать исходные коды и корректировать их под собственные нужды. Но только для некоммерческих целей.

Особенности реализации

Играть в такую игру достаточно легко. Она обладает простой логикой и графикой. Можно написать подобное приложение за несколько вечеров. Здесь стоит запомнить следующее:

  1. Изначально приложение создавалось для Windows 2000 Professional и выше.
  2. Для того, чтобы перенести игру на другие ОС, потребуется переписать класс CScreen и иметь порт библиотеки conio.h.
  3. Компиляция происходила через TDM-GCC 4.8.1. на 64-бит. Можно попробовать запуск через иные компиляторы.

Данное приложение проблематично назвать «современной игрой», но на первых порах ее достаточно. Вот архив с исходным кодом.

Виселица

А вот еще один довольно интересный пример – «Виселица». Игра, которая, согласно Google, известная многим. Здесь она представлена своеобразным механизмом, коим является любое приложение. Обладает:

  • основой – каркасом;
  • дополняющими направлениями – теми, что задают границы возможностей;
  • концом.

С данными компонентами в элементарном приложении проблем не возникнет. Особенно если посмотреть в Google правила «Виселицы». Для крупных проектов над каждой составляющей утилиты предстоит немало потрудиться.

Правила и принципы

«Виселица» — это игра в стиле «угадай слово». Предложенный пример реализовывается в пределах одного диалогового поля. Случайным образом программа загадывает слово. Пользователю предлагаются 10 попыток. Если он ошибается – засчитывается проигрыш.

После запуска система предложит сыграть. Для ответа необходимо нажать на клавиатуре «Д» (да) или «Н» (нет). В первом случае происходит запуск операции. Во втором – выход из системы.

После загадывания слова приложение покажет количество букв в соответствующей «комбинации». Весь процесс происходит на русском языке. При вводе неправильной буквы (не на той раскладке) система должна сообщить о том, что введены неверные данные. Попытка хода не засчитывается.

Ввод данных – с маленькой буквы. При попытке указать заглавную, игра попросит исправится. Если клиент угадал букву, она отобразится в положенных местах вместо «знака вопроса». Повторный ввод ранее указанных букв не отнимает ход. Программа просто просит пользователя заменить ее на другую.

Также стоит обратить внимание на то, что после каждого хода игрока приложение отображает количество попыток, отведенных на угадывание слова. Они снимаются, если допущена ошибка. В противном случае – сохраняются.

Реализация

При написании кода «Виселицы» в приведенном далее примере непосредственный процесс начинается с 84 строки. Тут необходимо запомнить следующее:

  1. Начинает работать бесконечный цикл. Google характеризует его как петлю.
  2. Цикл работает до тех пор, пока игрок не угадает слово или не допустит установленное количество ошибок.
  3. В коде есть проверка на соответствие символов буквам русского алфавита.
  4. В процессе реализации проверяется повторный ввод ранее указанных букв.
  5. При запуске система выдаст сообщение с правилами. Не придется лезть в Google и разбираться с принципами работы «Виселицы».

Выйти можно в любой момент. Здесь – исходный код приложения.

Как быстро научиться программировать

Создание собственной игры – дело не самое трудное, если речь идет о небольших проектах. Когда у пользователя есть заинтересованность в крупных и сложных работах, предстоит многое изучить. Быстрее вникнуть в тему можно, грамотно выбрав концепцию обучения:

  1. Поступление в техникум. Вариант для тех, кто думает развиваться в сфере программирования. В техникумы берут после 9 или 11 классов. Здесь не учат писать игры. Google указывает на то, что в подобных учреждениях дают «базу» для дальнейшего развития. Обучение 3-4 года. В конце выдается диплом о среднем специальном образовании.
  2. ВУЗы. Поступление в университет на «разработку и программирование» – лучшее решение для тех, кто готов полноценно погрузиться в азы соответствующих процессов. ВУЗы изучают различные ЯП, включая C++. Обучение отнимает 4-6 лет. В конце выдается диплом о высшем образовании в выбранном направлении. На 2-3 курс можно зачислиться при предварительном обучении в техникуме «на программиста». Образовательный процесс сопровождается кураторством и практикой.
  3. Самообучение. Неплохой вариант для тех, кто обладает особой усидчивостью. В Google можно отыскать множество туториалов и видео о разработке игр на любом языке, а не только на C++. Скорость обучения контролируется пользователем. Он может сконцентрироваться на более «проблемных» для себя областях и отдать предпочтение практике. Из минусов – отсутствие документального подтверждения навыков. Остается собирать портфолио и участвовать во всевозможных конкурсах.

Если же хочется побыстрее освоить навыки игрового программирования, стоит присмотреться к дистанционным онлайн курсам. Пример – от OTUS. Этот образовательный центр предлагает учиться в режиме онлайн инновационным IT-профессиям. В срок от нескольких месяцев до года пользователя научат с «нуля» не только писать сложные программы, но и создавать собственный развлекательный контент. Образовательный процесс разбит на «блоки» — для новичков и опытных разработчиков. Можно выбрать сразу одно или несколько направлений обучения. Весь процесс сопровождается кураторством, а также многочисленной практикой и интересными домашними заданиями. В конце каждого курса клиенты получают не только собственное портфолио, но и электронный сертификат, подтверждающий знания в выбранной области.

Хотите освоить современную IT-специальность? Огромный выбор курсов по востребованным IT-направлениям есть в Otus! Ниже – один из них:

Понравилась статья? Поделить с друзьями:
  • Как написать бровисту на запись
  • Как написать бриф пример
  • Как написать бриф для копирайтера
  • Как написать бречалову главе удмуртии
  • Как написать брестская область на английском