DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды

«Я знаю, что ничего не знаю» Сократ

Для кого: для IT-шников, которые плевали на всех разработчиков и хотят поиграть в свои игры!

О чем: о том, как начать писать игры на C/C++, если вдруг вам это надо!

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

Привет, меня зовут Андрей Гранкин, я DevOps в компании Luxoft. Разработка приложений — это не моя рабочая специализация, но я стараюсь каждую неделю программировать. Потому что люблю игры!

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

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

… настало время создавать свои миры, подвластные только мне. Миры, где я есть и Отец, и Сын, и Святой Дух!

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

В этой статье попробую рассказать, как начал писать небольшие игры на C/C++, каков процесс разработки и где нахожу время на хобби в условиях большой загруженности. Она субъективна и описывает процесс индивидуального старта. Материал о невежестве и вере, о моей личной картине мира в данный момент. Другими словами, «Администрация не несет ответственности за ваши личные мозги!».

Практика

«Знания без практики бесполезны, практика без знаний опасна» Конфуций

Мой блокнот — моя жизнь!


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

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
Мой (уже исписанный) блокнот. Вот так он выглядит. В нем повседневные задачи, идеи, рисунки, схемы, решения, черная бухгалтерия, код и так далее

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

  • Project 0: это 3D-сцена Architect Demo, написанная на C# с использованием игрового движка Unity. Для платформ macOS и Windows.
  • Game 1: консольная игра Simple Snake (всем известная как «Змейка») под Windows. Написанная на C.
  • Game 2: консольная игра Crazy Tanks (всем известная как «Танчики»), написанная уже на C++ (с использованием классов) и тоже под Windows.

Project 0. Architect Demo

  • Платформа: Windows (Windows 7, 10), Mac OS (OS X El Capitan v. 10.11.6)
  • Язык: C#
  • Игровой движок: Unity
  • Вдохновение: Darrin Lile
  • Репозиторий: GitHub

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
3D-сцена Architect Demo

Первый проект реализован не на C/C++, а на C# с помощью игрового движка Unity. Этот движок был не так требователен к «железу», как Unreal Engine, а также мне показался легче в установке и использовании. Другие движки я не рассматривал.

Целью в Unity для меня не была разработка какой-то игры. Я хотел создать 3D-сцену с каким-то персонажем. Он, а точнее Она (я смоделировал девушку, в которую был влюблен =) должна была двигаться и взаимодействовать с окружающим миром. Важно было только понять, что такое Unity, какой процесс разработки и сколько усилий требуется для создания чего-либо. Так родился проект Architect Demo (название придумано почти от балды). Программирование, моделирование, анимирование, текстурирование заняло у меня, наверное, два месяца ежедневной работы.

Стартовал я с обучающих видео на YouTube по созданию 3D-моделей в Blender. Blender — это отличный бесплатный тул для 3D-моделирования (и не только), не требующий установки. И здесь меня ждало потрясение… Оказывается, моделирование, анимирование, текстурирование — это огромные отдельные темы, на которые можно книжки писать. Особенно это касается персонажей. Чтобы моделировать пальцы, зубы, глаза и другие части тела, вам потребуются знания анатомии. Как устроены мышцы лица? Как люди двигаются? Мне пришлось «вставлять» кости в каждую руку, ногу, палец, фаланги пальцев!

Моделировать ключицы, дополнительные кости-рычаги, для того чтобы анимация выглядела естественно. После таких уроков осознаешь, какой огромный труд проделывают создатели анимационных фильмов, лишь бы сотворить 30 секунд видео. А ведь 3D-фильмы длятся часами! А мы потом выходим из кинотеатров и говорим что-то вроде: «Та, дерьмовый мультик/фильм! Могли сделать и получше…» Глупцы!

И еще, что касается программирования в этом проекте. Как оказалось, самая интересная для меня часть была математическая. Если вы запустите сцену (ссылка на репозиторий в описании проекта), то заметите, что камера вращается вокруг персонажа-девочки по сфере. Чтобы запрограммировать такое вращение камеры, мне пришлось сначала высчитывать координаты точки положения на круге (2D), а потом и на сфере (3D). Самое смешное, что я ненавидел математику в школе и знал ее на три с минусом. Отчасти, наверное, потому что в школе тебе попросту не объясняют, как, черт возьми, эта математика применяется в жизни. Но когда ты одержим своей целью, мечтой, то разум очищается, раскрывается! И ты начинаешь воспринимать сложные задачи как увлекательное приключение. А потом думаешь: «Ну чего же *любимая* математичка не могла нормально рассказать, где эти формулы прислонить можно?».

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
Просчет формул для вычисления координат точки на круге и на сфере (из моего блокнота)

Game 1. Simple Snake

  • Платформа: Windows (тестировал на Windows 7, 10)
  • Язык: по-моему, писал на чистом C
  • Игровой движок: консоль Windows
  • Вдохновение: javidx9
  • Репозиторий: GitHub

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
Игра Simple Snake

3D-сцена — это не игра. К тому же моделировать и анимировать 3D-объекты (особенно персонажи) долго и сложно. Поигравшись с Unity, ко мне пришло осознание, что нужно было продолжать, а точнее начинать, с основ. Чего-то простого и быстрого, но в то же время глобального, чтобы понять само устройство игр.

А что у нас простое и быстрое? Правильно, консоль и 2D. Точнее даже консоль и символы. Опять полез искать вдохновение в интернет (вообще, считаю интернет наиболее революционным и опасным изобретением XXI века). Нарыл видео одного программиста, который делал консольный тетрис. И по подобию его игры решил запилить «змейку». С видео я узнал про две фундаментальные вещи — игровой цикл (с тремя базовыми функциями/частями) и вывод в буфер.

Игровой цикл может выглядеть примерно так:

int main()
   {
      Setup();
      // a game loop
      while (!quit)
      {
          Input();
          Logic();
          Draw();
          Sleep(gameSpeed);  // game timing
      }
      return 0;
   }

В коде представлена сразу вся функция main(). А игровой цикл начинается после соответствующего комментария. В цикле три базовые функции: Input(), Logic(), Draw(). Сначала ввод данных Input (в основном контроль нажатия клавиш), потом обработка введенных данных Logic, затем вывод на экран — Draw. И так каждый кадр. В такой способ создается анимация. Это как в рисованных мультиках. Обычно обработка введенных данных отбирает больше всего времени и, насколько я знаю, определяет фреймрейт игры. Но тут функция Logic() выполняется очень быстро. Поэтому контролировать скорость смены кадров приходится функцией Sleep() с параметром gameSpeed, который и определяет эту скорость.

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
Игровой цикл. Программирование «змейки» в блокноте

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

Получение буфера экрана (если можно так сказать):

// create screen buffer for drawings
   HANDLE hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0,
 							   NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
   DWORD dwBytesWritten = 0;
   SetConsoleActiveScreenBuffer(hConsole);

Непосредственный вывод на экран некой строки scoreLine (строка отображения очков):

// draw the score
   WriteConsoleOutputCharacter(hConsole, scoreLine, GAME_WIDTH, {2,3}, &dwBytesWritten);

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

Game 2. Crazy Tanks

  • Платформа: Windows (тестировал на Windows 7, 10)
  • Язык: C++
  • Игровой движок: консоль Windows
  • Вдохновение: книга Beginning C++ Through Game Programming
  • Репозиторий: GitHub

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
Игра Crazy Tanks

Выводить символы в консоль — это, наверное, самое простое, что вы можете превратить в игру. Но тогда появляется одна неприятность: символы имеют разную высоту и ширину (высота больше, чем ширина). Таким образом, все будет выглядеть непропорционально, а движение вниз или вверх казаться гораздо быстрее, чем влево или вправо. Этот эффект очень заметен в «Змейке» (Game 1). «Танчики» (Game 2) лишены такого недостатка, так как вывод там организован через закрашивание экранных пикселей разными цветами. Можно сказать, я написал рендерер. Правда, это уже чуть сложнее, хотя и гораздо интереснее.

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

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

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
Набор прямоугольников

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

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
Пример матрицы игрового танка

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
Представление матрицы игрового танка одномерным массивом

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
Более наглядный пример представления матрицы одномерным массивом

Но доступ к элементам массива происходит в двойном цикле, как будто это не одномерный, а двумерный массив. Так сделано потому, что мы все-таки работаем с матрицами.

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
Обход одномерного массива в двойном цикле. Y — идентификатор строк, X — идентификатор столбцов

Обратите внимание: вместо привычных матричных идентификаторов i, j я использую идентификаторы x и y. Так, мне кажется, приятней для глаз и понятней для мозга. К тому же такая запись позволяет удобно проецировать используемые матрицы на оси координат двухмерного изображения.

Теперь о пикселях, цвете и выводе на экран. Для вывода используется функция StretchDIBits (Header: windows.h; Library: gdi32.lib). В эту функцию, помимо всего прочего, передается следующее: устройство, на которое выводится изображение (в моем случае это консоль Windows), координаты старта отображения картинки, ее ширина/высота и сама картинка в виде битовой карты (bitmap), представленной массивом байтов. Битовая карта в виде массива байт!

Функция StretchDIBits() в работе:

// screen output for game field
   StretchDIBits(
               deviceContext,
               OFFSET_LEFT, OFFSET_TOP,
               PMATRIX_WIDTH, PMATRIX_HEIGHT,
               0, 0,
               PMATRIX_WIDTH, PMATRIX_HEIGHT,
               m_p_bitmapMemory, &bitmapInfo,
               DIB_RGB_COLORS,
               SRCCOPY
               );

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

Создание битовой карты m_p_bitmapMemory:

// create bitmap
   int bitmapMemorySize = (PMATRIX_WIDTH * PMATRIX_HEIGHT) * BYTES_PER_PIXEL;
   void* m_p_bitmapMemory = VirtualAlloc(0, bitmapMemorySize, MEM_COMMIT, PAGE_READWRITE);

Грубо говоря, битовая карта состоит из набора пикселей. Каждые четыре байта в массиве — это пиксель в формате RGB. Один байт на значение красного цвета, один байт на значение зеленого цвета (G) и один байт на синий цвет (B). Плюс остается один байт на отступ. Эти три цвета — Red/Green/Blue (RGB) — смешиваются друг с другом в разных пропорциях — и получается результирующий цвет пикселя.

Теперь, повторюсь, каждый прямоугольник, или игровой объект, представлен числовой матрицей. Все эти игровые объекты помещаются в коллекцию. И затем расставляются по игровому полю, формируя одну большую числовую матрицу. Каждое число в матрице я сопоставил с определенным цветом. Например, числу 8 соответствует синий цвет, числу 9 — желтый, числу 10 — темно-серый цвет и так далее. Таким образом, можно сказать, у нас получилась матрица игрового поля, где каждое число — это какой-то цвет.

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

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
Пример заполнения битовой карты (Pixel matrix) информацией на основе числовой матрицы (Digital matrix) игрового поля (индексы цветов не совпадают с индексами в игре)

Также представлю кусок реального кода из игры. Переменной colorIndex на каждой итерации цикла присваивается значение (индекс цвета) из числовой матрицы игрового поля (mainDigitalMatrix). Затем в переменную color записывается сам цвет на основе индекса. Дальше полученный цвет разделяется на соотношение красного, зеленого и синего оттенка (RGB). И вместе с отступом (pixelPadding) раз за разом эта информация записывается в пиксель, формируя цветное изображение в битовой карте.

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

Заполнение битовой карты информацией на основе числовой матрицы игрового поля:

// set pixel map variables
   int colorIndex;
   COLORREF color;
   int pitch;
   uint8_t* p_row;
 
   // arrange pixels for game field
   pitch = PMATRIX_WIDTH * BYTES_PER_PIXEL;     // row size in bytes
   p_row = (uint8_t*)m_p_bitmapMemory;       //cast to uint8 for valid pointer arithmetic
   							(to add by 1 byte (8 bits) at a time)   
   for (int y = 0; y < PMATRIX_HEIGHT; ++y)
   {
       uint32_t* p_pixel = (uint32_t*)p_row;
       for (int x = 0; x < PMATRIX_WIDTH; ++x)
       {
           colorIndex = mainDigitalMatrix[y * PMATRIX_WIDTH + x];
           color = Utils::GetColor(colorIndex);
           uint8_t blue = GetBValue(color);
           uint8_t green = GetGValue(color);
           uint8_t red = GetRValue(color);
           uint8_t pixelPadding = 0;
 
           *p_pixel = ((pixelPadding << 24) | (red << 16) | (green << 8) | blue);
           ++p_pixel;
       }
       p_row += pitch;
   }

По описанному выше методу в игре Crazy Tanks формируется одна картинка (кадр) и выводится на экран в функции Draw(). После регистрации нажатия клавиш в функции Input() и последующей их обработки в функции Logic() формируется новая картинка (кадр). Правда, игровые объекты уже могут иметь другое положение на игровом поле и, соответственно, отрисовываются в другом месте. Так и происходит анимация (движение).

По идее (если ничего не забыл), понимание игрового цикла из первой игры («Змейка») и системы вывода пикселей на экран из второй игры («Танчики») — это все, что вам нужно, чтобы написать любую свою 2D-игру под Windows. Без звука! 😉 Остальные же части — просто полет фантазии.

Конечно, игра «Танчики» сконструирована гораздо сложнее, чем «Змейка». Я использовал уже язык C++, то есть описывал разные игровые объекты классами. Создал собственную коллекцию — код можно посмотреть в headers/Box.h. Кстати, в коллекции, скорее всего, есть утечка памяти (memory leak). Использовал указатели. Работал с памятью. Должен сказать, что мне очень помогла книга Beginning C++ Through Game Programming. Это отличный старт для начинающих в C++. Она небольшая, интересная и неплохо организована.

На разработку этой игры ушло где-то полгода. Писал, в основном, во время обеда и перекусов на работе. Садился на офисной кухне, топтал харчи и писал код. Или же за ужином дома. Вот и получились у меня такие «кухонные войны». Как всегда, я активно использовал блокнот, и все концептуальные штуки рождались именно в нем.

В завершение практической части пульну несколько сканов моего блокнота. Чтобы показать, что именно я записывал, рисовал, считал, проектировал…

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
Проектирование изображения танков. И отпределенеи того, сколько пикселей каждый танк долже занимать на экране

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
Просчет алгоритма и формул по обороту танка вокруг своей оси

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
Схема моей коллекции (той самой, в которой есть memory leak, скорее всего). Коллегкция создана по типу Linked List

DevOps’ный C++ и «кухонные войны», или Как я начал писать игры во время еды
А это тщетные попытки прикрутить к игре искусственный интеллект

Теория

«Даже путь в тысячу миль начинается с первого шага» (Древнекитайская мудрость)

Перейдем от практики к теории! Как же найти время на свое хобби?

  1. Определить, чего вы действительно хотите (увы, это самое сложное).
  2. Расставить приоритеты.
  3. Пожертвовать всем «лишним» в угоду высшим приоритетам.
  4. Двигаться к целям каждый день.
  5. Не ждать, что появится два-три часа свободного времени на хобби.

С одной стороны, вам нужно определить, чего вы хотите и расставить приоритеты. А с другой, возможно, отказаться от каких-то дел/проектов в пользу этих приоритетов. Другими словами, придется пожертвовать всем «лишним». Я где-то слышал, что в жизни должно быть максимум три основных занятия. Тогда вы сможете ими заниматься наиболее качественно. А дополнительные проекты/направления начнут банально перегружать. Но это все, наверное, субъективно и индивидуально.

Существует некое золотое правило: never have a 0% day! О нем я узнал в статье одного indie-разработчика. Если работаете над каким-то проектом, то делайте по нему что-то каждый день. И неважно, сколько вы сделаете. Напишите одно слово или одну строчку кода, посмотрите одно обучающее видео или забейте один гвоздь в доску — просто сделайте хоть что-то. Самое сложное — это начать. Как только начнете, то наверняка сделаете немного больше, чем хотели. Так вы будете постоянно двигаться к своей цели и, поверьте, очень быстро. Ведь основной тормоз всех дел — это прокрастинация.

И важно помнить, что не стоит недооценивать и игнорировать свободные «опилки» времени в 5, 10, 15 минут, ждать каких-то больших «бревен» длительностью в час или два. Стоите в очереди? Продумайте что-то по вашему проекту. Поднимаетесь по эскалатору? Запишите что-то в блокнот. Едите в автобусе? Отлично, прочтите какую-то статью. Используйте каждую возможность. Хватит смотреть котиков и собачек на YouTube! Не засоряйте себе мозг!

И последнее. Если, прочитав эту статью, вам понравилась идея создания игр без использования игровых движков, то запомните имя Casey Muratori. У этого парня есть сайт. В секции «watch -> PREVIOUS EPISODES» вы найдете чудесные бесплатные видеоуроки по созданию профессиональной игры с полнейшего нуля. За пять уроков Intro to C for Windows вы, возможно, узнаете больше, чем за пять лет обучения в университете (об этом кто-то написал в комментариях под видео).

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

Удачи вам на выбранном пути! И давайте сделаем мир профессиональней.

Автор: Гранкин Андрей, DevOps



Источник: habr.com