«Я знаю, что ничего не знаю» Сократ
Для кого: для IT-шников, которые плевали на всех разработчиков и хотят поиграть в свои игры!
О чем: о том, как начать писать игры на C/C++, если вдруг вам это надо!
Зачем вам это читать: разработка приложений — это не моя рабочая специализация, но я стараюсь каждую неделю программировать. Потому что люблю игры!
Привет, меня зовут Андрей Гранкин, я DevOps в компании Luxoft. Разработка приложений — это не моя рабочая специализация, но я стараюсь каждую неделю программировать. Потому что люблю игры!
Индустрия компьютерных игр огромна, по слухам, сегодня даже больше, чем индустрия кино. Игры писали с начала развития компьютеров, используя, по современным меркам, сложные и базовые методы разработки. Со временем стали появляться игровые движки с уже запрограммированной графикой, физикой, звуком. Они позволяют сосредоточиться на разработке самой игры и не заморачиваться по поводу ее основания. Но вместе с ними, с движками, разработчики «слепнут» и деградируют. Само производство игр ставится на конвейер. А количество продукции начинает преобладать над ее качеством.
В то же время, играя в чужие игры, мы постоянно ограничены локациями, сюжетом, персонажами, игровой механикой, которую придумали другие люди. Так что я понял, что…
… настало время создавать свои миры, подвластные только мне. Миры, где я есть и Отец, и Сын, и Святой Дух!
И я искренне верю, что, написав свой собственный игровой движок и игру на нем, получится разуть глаза, протереть форточки и прокачать свою кабину, став более опытным и цельным программистом.
В этой статье попробую рассказать, как начал писать небольшие игры на C/C++, каков процесс разработки и где нахожу время на хобби в условиях большой загруженности. Она субъективна и описывает процесс индивидуального старта. Материал о невежестве и вере, о моей личной картине мира в данный момент. Другими словами, «Администрация не несет ответственности за ваши личные мозги!».
Практика
«Знания без практики бесполезны, практика без знаний опасна» Конфуций
Мой блокнот — моя жизнь!
Итак, на практике могу сказать, что все для меня начинается с блокнота. Туда записываю не только свои повседневные задачи, в нем еще и рисую, программирую, конструирую блок-схемы и решаю задачи, в том числе математические. Всегда используйте блокнот и пишите только карандашом. Это чисто, удобно и надежно, ИМХО.
![image](https://habrastorage.org/webt/12/ro/s2/12ros2fqdnxuhll3f_cr3ajplfe.jpeg)
Мой (уже исписанный) блокнот. Вот так он выглядит. В нем повседневные задачи, идеи, рисунки, схемы, решения,
На данном этапе я успел закончить три проекта (это в моем понимании «законченности», ведь любой продукт можно развивать относительно бесконечно).
- 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
![image](https://habrastorage.org/webt/m5/ys/jj/m5ysjjfpakqcssddgeg2qjnyedc.jpeg)
3D-сцена Architect Demo
Первый проект реализован не на C/C++, а на C# с помощью игрового движка Unity. Этот движок был не так требователен к «железу», как Unreal Engine, а также мне показался легче в установке и использовании. Другие движки я не рассматривал.
Целью в Unity для меня не была разработка какой-то игры. Я хотел создать 3D-сцену с каким-то персонажем. Он, а точнее Она (я смоделировал девушку, в которую был влюблен =) должна была двигаться и взаимодействовать с окружающим миром. Важно было только понять, что такое Unity, какой процесс разработки и сколько усилий требуется для создания чего-либо. Так родился проект Architect Demo (название придумано почти от балды). Программирование, моделирование, анимирование, текстурирование заняло у меня, наверное, два месяца ежедневной работы.
Стартовал я с обучающих видео на YouTube по созданию 3D-моделей в Blender. Blender — это отличный бесплатный тул для 3D-моделирования (и не только), не требующий установки. И здесь меня ждало потрясение… Оказывается, моделирование, анимирование, текстурирование — это огромные отдельные темы, на которые можно книжки писать. Особенно это касается персонажей. Чтобы моделировать пальцы, зубы, глаза и другие части тела, вам потребуются знания анатомии. Как устроены мышцы лица? Как люди двигаются? Мне пришлось «вставлять» кости в каждую руку, ногу, палец, фаланги пальцев!
Моделировать ключицы, дополнительные кости-рычаги, для того чтобы анимация выглядела естественно. После таких уроков осознаешь, какой огромный труд проделывают создатели анимационных фильмов, лишь бы сотворить 30 секунд видео. А ведь 3D-фильмы длятся часами! А мы потом выходим из кинотеатров и говорим что-то вроде: «Та, дерьмовый мультик/фильм! Могли сделать и получше...» Глупцы!
И еще, что касается программирования в этом проекте. Как оказалось, самая интересная для меня часть была математическая. Если вы запустите сцену (ссылка на репозиторий в описании проекта), то заметите, что камера вращается вокруг персонажа-девочки по сфере. Чтобы запрограммировать такое вращение камеры, мне пришлось сначала высчитывать координаты точки положения на круге (2D), а потом и на сфере (3D). Самое смешное, что я ненавидел математику в школе и знал ее на три с минусом. Отчасти, наверное, потому что в школе тебе попросту не объясняют, как, черт возьми, эта математика применяется в жизни. Но когда ты одержим своей целью, мечтой, то разум очищается, раскрывается! И ты начинаешь воспринимать сложные задачи как увлекательное приключение. А потом думаешь: «Ну чего же *любимая* математичка не могла нормально рассказать, где эти формулы прислонить можно?».
![image](https://habrastorage.org/webt/9v/6b/wv/9v6bwvuhg9nrp-i1u45ay28bxri.jpeg)
Просчет формул для вычисления координат точки на круге и на сфере (из моего блокнота)
Game 1. Simple Snake
- Платформа: Windows (тестировал на Windows 7, 10)
- Язык: по-моему, писал на чистом C
- Игровой движок: консоль Windows
- Вдохновение: javidx9
- Репозиторий: GitHub
![image](https://habrastorage.org/webt/ip/ut/rs/iputrshwcsilxsxw4_sjuhb4evm.jpeg)
Игра 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, который и определяет эту скорость.
![image](https://habrastorage.org/webt/8_/mv/k4/8_mvk40o8rrmisgyu_glibssot8.jpeg)
Игровой цикл. Программирование «змейки» в блокноте
Если разрабатывать символьную консольную игру, то выводить данные на экран с помощью обычного потокового вывода ‘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
![image](https://habrastorage.org/webt/se/r0/ww/ser0wwynoy5pinfe6g8lh152fma.jpeg)
Игра Crazy Tanks
Выводить символы в консоль — это, наверное, самое простое, что вы можете превратить в игру. Но тогда появляется одна неприятность: символы имеют разную высоту и ширину (высота больше, чем ширина). Таким образом, все будет выглядеть непропорционально, а движение вниз или вверх казаться гораздо быстрее, чем влево или вправо. Этот эффект очень заметен в «Змейке» (Game 1). «Танчики» (Game 2) лишены такого недостатка, так как вывод там организован через закрашивание экранных пикселей разными цветами. Можно сказать, я написал рендерер. Правда, это уже чуть сложнее, хотя и гораздо интереснее.
По данной игре будет достаточно описать мою систему вывода пикселей на экран. Считаю это основной частью игры. А все остальное можно придумать самостоятельно.
Итак, то, что вы видите на экране — это просто набор движущихся разноцветных прямоугольников.
![image](https://habrastorage.org/webt/l0/pd/0e/l0pd0evwmxonaoar8zkahzk6oi8.jpeg)
Набор прямоугольников
Каждый прямоугольник представлен матрицей, заполненной числами. Кстати, могу выделить один интересный нюанс — все матрицы в игре запрограммированы как одномерный массив. Не двумерный, а одномерный! С одномерными массивами гораздо проще и быстрее работать.
![image](https://habrastorage.org/webt/ic/hw/ax/ichwax5bee3v8cmagwngucydcew.jpeg)
Пример матрицы игрового танка
![image](https://habrastorage.org/webt/pj/p7/1n/pjp71n_lodzkzrus7wzbibwbhzc.jpeg)
Представление матрицы игрового танка одномерным массивом
![image](https://habrastorage.org/webt/h1/9r/tn/h19rtn2gcadv0jg_o2xcjdkqnqe.jpeg)
Более наглядный пример представления матрицы одномерным массивом
Но доступ к элементам массива происходит в двойном цикле, как будто это не одномерный, а двумерный массив. Так сделано потому, что мы все-таки работаем с матрицами.
![image](https://habrastorage.org/webt/5j/wm/2c/5jwm2cxqnxodkoqfhghqmj2rcq4.jpeg)
Обход одномерного массива в двойном цикле. 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 — темно-серый цвет и так далее. Таким образом, можно сказать, у нас получилась матрица игрового поля, где каждое число — это какой-то цвет.
Итак, мы имеем числовую матрицу всего игрового поля с одной стороны и битовую карту для вывода изображения с другой. Пока что битовая карта «пустая» — в ней еще нет информации о пикселях нужного цвета. Это значит, что последним этапом будет заполнение битовой карты информацией о каждом пикселе на основе числовой матрицы игрового поля. Наглядный пример такого преобразования на картинке ниже.
![image](https://habrastorage.org/webt/v8/-e/0k/v8-e0kxpr7voobupng6zam5qi9s.jpeg)
Пример заполнения битовой карты (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++. Она небольшая, интересная и неплохо организована.
На разработку этой игры ушло где-то полгода. Писал, в основном, во время обеда и перекусов на работе. Садился на офисной кухне, топтал харчи и писал код. Или же за ужином дома. Вот и получились у меня такие «кухонные войны». Как всегда, я активно использовал блокнот, и все концептуальные штуки рождались именно в нем.
В завершение практической части пульну несколько сканов моего блокнота. Чтобы показать, что именно я записывал, рисовал, считал, проектировал…
![image](https://habrastorage.org/webt/hn/6d/b1/hn6db1ojueu7bpxwcqbhpb3sipa.jpeg)
Проектирование изображения танков. И отпределенеи того, сколько пикселей каждый танк долже занимать на экране
![image](https://habrastorage.org/webt/hd/v0/6w/hdv06wsgmwmwoln2kiyqleykgi0.jpeg)
Просчет алгоритма и формул по обороту танка вокруг своей оси
![image](https://habrastorage.org/webt/dx/c7/2a/dxc72advweforpufurwhm_lei2e.jpeg)
Схема моей коллекции (той самой, в которой есть memory leak, скорее всего). Коллегкция создана по типу Linked List
![image](https://habrastorage.org/webt/q3/uq/qa/q3uqqalgnxygn5on5ydojkrzsyi.jpeg)
А это тщетные попытки прикрутить к игре искусственный интеллект
Теория
«Даже путь в тысячу миль начинается с первого шага» (Древнекитайская мудрость)
Перейдем от практики к теории! Как же найти время на свое хобби?
- Определить, чего вы действительно хотите (увы, это самое сложное).
- Расставить приоритеты.
- Пожертвовать всем «лишним» в угоду высшим приоритетам.
- Двигаться к целям каждый день.
- Не ждать, что появится два-три часа свободного времени на хобби.
С одной стороны, вам нужно определить, чего вы хотите и расставить приоритеты. А с другой, возможно, отказаться от каких-то дел/проектов в пользу этих приоритетов. Другими словами, придется пожертвовать всем «лишним». Я где-то слышал, что в жизни должно быть максимум три основных занятия. Тогда вы сможете ими заниматься наиболее качественно. А дополнительные проекты/направления начнут банально перегружать. Но это все, наверное, субъективно и индивидуально.
Существует некое золотое правило: never have a 0% day! О нем я узнал в статье одного indie-разработчика. Если работаете над каким-то проектом, то делайте по нему что-то каждый день. И неважно, сколько вы сделаете. Напишите одно слово или одну строчку кода, посмотрите одно обучающее видео или забейте один гвоздь в доску — просто сделайте хоть что-то. Самое сложное — это начать. Как только начнете, то наверняка сделаете немного больше, чем хотели. Так вы будете постоянно двигаться к своей цели и, поверьте, очень быстро. Ведь основной тормоз всех дел — это прокрастинация.
И важно помнить, что не стоит недооценивать и игнорировать свободные «опилки» времени в 5, 10, 15 минут, ждать каких-то больших «бревен» длительностью в час или два. Стоите в очереди? Продумайте что-то по вашему проекту. Поднимаетесь по эскалатору? Запишите что-то в блокнот. Едите в автобусе? Отлично, прочтите какую-то статью. Используйте каждую возможность. Хватит смотреть котиков и собачек на YouTube! Не засоряйте себе мозг!
И последнее. Если, прочитав эту статью, вам понравилась идея создания игр без использования игровых движков, то запомните имя Casey Muratori. У этого парня есть сайт. В секции «watch -> PREVIOUS EPISODES» вы найдете чудесные бесплатные видеоуроки по созданию профессиональной игры с полнейшего нуля. За пять уроков Intro to C for Windows вы, возможно, узнаете больше, чем за пять лет обучения в университете (об этом кто-то написал в комментариях под видео).
Также Кейси объясняет, что, разработав свой собственный игровой движок, вы будете лучше понимать любые существующие движки. В мире фреймворков, где все пытаются автоматизировать, вы научитесь создавать, а не использовать. Осознаете саму природу компьютеров. А также станете гораздо более толковым и зрелым программистом — профи.
Удачи вам на выбранном пути! И давайте сделаем мир профессиональней.
dominigato
8ll
Потому что девопсам платят, в отличие от
dominigato
Да ладно, неужели геймдев настолько отстает? На говноиграх на андроиде целые компании вырастают.
8ll
На глассдоре devops engineer vs game engineer средняя зп 99000 USD в год vs 50900 USD в год. Практически в два раза. Плюс хороших студий днем с огнем если не хочешь пилить всякие гемблинги и браузерки или тетрисы на андроиды те же самые
dominigato
Ну то что выскакивает это и есть говноигры на андроиде, Гемблинги вообще параллельная планета с дикими зарплатами, которые не снились даже.
Если немного снизить свои моральные принципы, можно делать больше чем девопс в бодишопе, наверное.
Brightori
эмм, я там (на глассдоре)видел Middle Unity Developer = 125k в год. Lead 120 — 150. Но это про США. В Англии как то рассматривал вакансию за 80к фунтов в год. Я конечно хз сколько девопсы получают. Но в геймдеве не 50к )
ЗЫ только что посмотрел там по юнитистам 50-180 разброс в год ), вакансий куча
много вакансий 100+
8ll
Я не знаю где ты увидел 125.
![image](https://previews.dropbox.com/p/thumb/AA--8v8hiDcJVhEQL3WQRyn8lpqWpro8LcF8KiKyJLxAdpQvix6O3RZ4GcbfaTme3yauQrHJbZAwRmwmM9tBGIShV4SbiX5-ZcSIFcDpaU0U90iYWAWxw3IjMolh9BIVLIEMHvGaO8DZ9xSBDifa_Wr7j7F6aObBN-486gF05JiYf1gXj_InsKKuCIWDi_z5k8A0n0fR4EAipGuPN5fSEt5XHs9Ffd8clYUdWiNU85x6WHLlxzjuMz6_2OpZqoNXdd-26yaOi2CvLRlAZ0atfrNy_h_NR7Dl5AzPd8KE1N0xGpO1stUrAaa6clXFYMqgXwWA_-6NF1lYKV37dGmiB8sZk0-pU9bHLLPbS-owE-PCWw/p.png?fv_content=true&size_mode=5)