Как написать свой графический движок
Перейти к содержимому

Как написать свой графический движок

  • автор:

Создаем игровой движок с видом от первого лица за 265 строк кода на JavaScript

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

Рейкастинг — один из методов рендеринга в компьютерной графике, при котором сцена строится на основе замеров пересечения лучей с визуализируемой поверхностью.

Игрок

Логично, что если мы создаем движок от первого лица, то наш игрок — это и есть точка, из которой будут выходить лучи. Для начала нам понадобится всего три свойства: координата x , координата y и направление:

Карта

Будем хранить карту с помощью двумерного массива. В нем 0 будет обозначать отсутствие стены, а 1 — её наличие. Для нашей реализации такой простой схемы будет достаточно.

Бросаем луч

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

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

Найдем угол каждого луча

Угол зависит от трех параметров: направления, в котором смотрит игрок, фокусного расстояния камеры и колонки, которую мы в данный момент рисуем.

Проследим за каждым лучом на сетке

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

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

Обнаружить пересечения на сетке легко: нужно просто найти все целочисленные x (1, 2, 3…). А потом найти соответствующие y с помощью умножения x на коэффициент угла наклона rise / run .

Прелесть этой части алгоритма в том, что размер карты не имеет значения. Мы рассматриваем только определенный набор точек на сетке — каждый раз примерно одно и то же количество. В нашем примере размер карты 32×32, но если бы он был 32 000×32 000, скорость загрузки была бы такой же.

Рисуем колонку

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

Мы определяем высоту каждой стены, деля её максимальную высоту на z . Чем дальше стена, тем короче мы её рисуем.

Откуда взялся косинус? Если мы будем использовать «чистое» расстояние от игрока до стены, то в итоге получим эффект «рыбьего глаза». Представьте, что вы стоите лицом к стене. Края стены слева и справа находятся от вас дальше, чем центр стены. Но мы же не хотим, чтобы при отрисовке стена выпирала посередине? Для того, чтобы визуализировать плоскую стену так, как мы её видим в реальной жизни, мы строим треугольник из каждого луча и находим перпендикуляр к стене с помощью косинуса. Вот так:

Слева — расстояние, справа — расстояние, умноженное на косинус угла.

В нашей статье это — самая сложная математика, с которой придется столкнуться ��

Визуализируем

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

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

Самые важные свойства камеры — разрешение, фокусное расстояние и диапазон.

  • Разрешение определяет, сколько колонок мы рисуем (сколько лучей бросаем);
  • Фокусное расстояние определяет ширину линзы, через которую мы смотрим (углы лучей);
  • Диапазон определяет дальность обзора (максимальная длина каждого луча).

Собираем воедино

Используем объект Controls для снятия данных с клавиш-стрелок и сенсорной панели, а также объект GameLoop для вызова requestAnimationFrame . Цикл игры прописываем всего тремя строками:

Детали

Дождь

Дождь симулируем с помощью нескольких очень коротких стен, разбросанных произвольно:

Задаем ширину стены в 1 пиксель.

Освещение и молнии

Освещение — это, вообще-то, работа с тенями: все стены рисуются со 100% яркостью, а потом покрываются черным прямоугольником какой-либо прозрачности. Прозрачность определяется как расстоянием до стены, так и её ориентацией (север / юг / запад / восток).

Для симуляции молний, map.light случайным образом совершает резкий скачок до значения 2 и потом так же быстро гаснет.

Предупреждение столкновений

Для того, чтобы игрок не натыкался на стены, мы просто проверяем его следующую локацию по карте. Координаты x и y проверяем по отдельности, чтобы игрок мог идти вдоль стены:

Текстура стен

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

Например, для пересечения в точке (10, 8,2) остаток равен 0,2. Это значит, что пересечение находится в 20% от левого края стены (8) и 80% от правого края (9) . Поэтому мы умножаем 0,2 на texture.width чтобы найти x -координату для изображения текстуры.

Можно посмотреть результат на сайте автора, а также изучить код на GitHub.

Часть 1: точки, векторы и базовые принципы

image

Современные трёхмерные игровые движки, используемые в крупнейших проектах — это тонкая смесь математики и программирования. Многие программисты игр признают, что всецело понять их очень непросто. Если вам не хватает опыта (или профессионального образования, как мне), эта задача становится ещё более сложной. Я хочу познакомить вас с основами графических систем 3D-движков.

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

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

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


Проклятье, отравляющее жизнь многим школьникам

Трёхмерное декартово пространство даёт нам оси x, y и z (описывающие положение по горизонтали, вертикали и в глубину). Координаты любой точки в этом пространстве обозначаются как несколько чисел (в нашем случае три числа, потому что у нас три оси). На двухмерной плоскости запись обозначается как , а в трёхмерном пространстве — как . Эта запись (кортеж) показывает положение точки относительно исходной точки пространства (которая обычно обозначается как .

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

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

Кроме задания точки нам нужно определить её части.

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

Мы определим три базисных вектора в нашем пространстве:


Источник: http://www.thefullwiki.org/Arithmetics/Cartesian_Coordinate.

Система координат

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

Обозначение точек

Точку начала координат системы координат можно обозначить точкой , которая описывается кортежем из трёх элементов (0,0,0). Это значит, что математическое представление системы координат можно изобразить так:

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

С этого момента мы будем обозначать скалярные значения строчными буквами, а векторы — прописными, то есть , и — это скаляры, а , и — векторы. (На самом деле это базисные векторы, определение которым мы дали выше.)

Это значит, что точка, записываемая кортежем (2,3,4), может быть представлена так:

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

Взаимная перпендикулярность

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

Нашу систему координат можно также назвать «правой»:


Источник: http://viz.aset.psu.edu/gho/sem_notes/3d_fundamentals/html/3d_coordinates.html.

На языке математики это значит, что:

где обозначает оператор векторного произведения.

Векторное произведение можно определить следующим уравнением (при условии, что у нас есть два кортежа из трёх элементов):

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

Точки и векторы

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

Чтобы не перепутать эти два типа объектов, я буду записывать точки курсивными прописными буквами, например, , а векторы — полужирными прописными, например, .

При работе с точками и векторами мы будем использовать две основные аксиомы. Вот они:

  • Аксиома 1: разность между двумя точками — это вектор, то есть
  • Аксиома 2: сумма точки и вектора — это точка, то есть

Создание движка

Благодаря этим двум аксиомам у нас есть достаточно информации для создания классов-«кирпичиков», которые являются сердцем любого трёхмерного игрового движка: класса Point и класса Vector . Если бы мы собирались создавать свой движок на основе этой информации, то нам бы нужно было сделать и другие важные шаги при создании этих классов (в основном связанных с оптимизацией и работой с существующими API), но мы опустим это ради упрощения.

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

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

Заключение

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

Часть 2: линейные преобразования

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

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

Основы линейных преобразований

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

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

Все линейные преобразования имеют следующую форму:

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

Каждую из этих частей — два вектора и функцию — можно представить в виде матрицы: вектор — как матрицу 1×3, вектор — как ещё одну матрицу 1×3, а линейное преобразование — как матрицу 3×3 (матрицу преобразований).

Это значит, что развернув уравнение, мы получим следующее:

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

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

Повороты

Поворот, по самому определению — это круговое движение объекта вокруг точки поворота. Точка поворота в нашем пространстве может принадлежать плоскости XY, плоскости XZ или плоскости YZ (где каждая плоскость составлена из двух базисных векторов, которые мы обсуждали в первой части).

Три точки поворота означают, что у нас есть три отдельных матрицы вращения:

Матрица поворота по XY:

Матрица поворота по XZ:

Матрица поворота по YZ:

То есть для поворота точки вокруг плоскости XY на 90 градусов ( радиан — в большинстве математических библиотек есть функция преобразования градусов в радианы), нужно выполнить следующие действия:

То есть если начальная точка имела координаты , то выходная точка будет иметь координаты .

Упражнение: функции поворота

В качестве упражнения попробуйте создать три новые функции для класса Vector . Одна должна поворачивать вектор вокруг плоскости XY, другая — вокруг YZ, а третья — вокруг XZ. На входе функции должны получать нужное число градусов поворота, а на выходе возвращать вектор.

В целом функции должны работать следующим образом:

  1. Создание выходного вектора.
  2. Преобразование входа в градусах в радианы.
  3. Решение каждого из элементов кортежа выходного вектора с помощью приведённых выше уравнений.
  4. Возврат выходного вектора.

Масштабирование

Масштабирование — это преобразование, увеличивающее или уменьшающее объект в соответствии с заданным масштабом.

Это преобразование выполнить довольно просто (по крайней мере, по сравнению с поворотами). Преобразование масштабирования требует двух типов входных данных: входного вектора и кортежа масштабирования из трёх элементов, который определяет масштаб входного вектора по каждой из осей пространства.

Например, в кортеже масштабирования величина представляет собой масштаб по оси X, — по оси Y, а — по оси Z.

Матрица масштабного преобразования имеет следующий вид (где , и — это элементы кортежа масштабирования):

Чтобы сделать входной вектор A вдвое больше по оси X (то есть использовать кортеж ), вычисления должны иметь следующий вид:

То есть при входном векторе выходной вектор будет равен .

Упражнение: функции масштабирования

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

В целом функция должна работать следующим образом:

  1. Создание выходного вектора.
  2. Решение каждого элемента кортежа выходного вектора с помощью приведённого выше уравнения (которое можно упростить до y0 = x0 * s0; y1 = x1*s1; y2 = x2*s2 ).
  3. Возврат выходного вектора.

Давайте что-нибудь создадим!

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

Прежде чем начать, нам нужно добавить ещё одну функцию в класс Point . Мы назовём её setPointToPoint() , она будет просто задавать положение текущей точки в точке, которая ей передаётся. На входе она будет получать точку и ничего не возвращать.

Вот краткие характеристики программы:

  • Программа будет хранить в массиве 100 точек.
  • При нажатии клавиши D программа будет очищать текущий экран и перерисовывать точки.
  • При нажатии клавиши A программа будет масштабировать положения всех точек на 0,5.
  • При нажатии клавиши S программа будет масштабировать положения всех точек на 2,0.
  • При нажатии клавиши R будет поворачивать положение всех точек на 15 градусов на плоскости XY.
  • При нажатии клавиши Escape программа закрывается (если вы не пишете её на JavaScript или другом веб-ориентированном языке).

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

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

Заключение

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

Как обычно, для упрощения я убрал из движка некоторые вещи (а именно перемещение и отражения). Если вы хотите больше узнать об этих двух типах линейных преобразований, то можете почитать в статье Википедии и по ссылкам в статье.

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

Часть 3: пространства и отсечение

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

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

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

Отсечение

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

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

Пространство обзора будет задаваться по всем трём нашим традиционным осям: x, y и z. Её границы по x состоят из всего между левой и правой границами окна, границы по y — из всего между верхней и нижней границами окна, а границы по z находятся в пределах от 0 (куда установлена камера) до расстояния видимости игрока (в нашем демо мы будем использовать произвольно выбранное значение 100 ).

Перед отрисовкой точки класс камеры будет проверять, находится ли точка в пространстве обзора. Если находится, то точка отрисовывается, в противном случае — не отрисовывается.

Может, пора добавить камеру?

Поняв основы отсечения, мы можем создать класс камеры:

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

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


Источник: http://en.wikipedia.org/wiki/File:ViewFrustum.svg

Подсказка: если вы хотите отделить систему камер от рендерера, то можно просто создать класс Renderer , отсечь системой камер точки, сохранить в массиве те из них, которые нужно отрисовывать, а затем отправить массив в функцию draw() рендерера.

Управление точками

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

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

Внеся все эти дополнения, давайте немного улучшим программу, написанную в прошлой части.

Больше и лучше

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

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

Давайте взглянем на код:

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

Заключение

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

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

Часть 4: растеризация отрезков прямых и окружностей

Растеризация

Растеризация — это процесс преобразования формы, описанной в векторном графическом формате (или в нашем случае математически) в растровое изображение (в котором форма вписывается в пиксельную структуру).

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

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

Отрезки прямых


Источник: http://en.wikipedia.org/wiki/File:Bresenham.svg

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

Поэтапно алгоритм Брезенхэма выглядит так:

  1. Получение на входе начальной и конечной точек отрезка прямой.
  2. Определение направления отрезка прямой вычислением его свойств и (, ).
  3. Определение свойств sx , sy и обнаружения ошибки (математическое определение приведено ниже).
  4. Округление каждой точки в отрезке до пикселя выше/ниже.

Если нужно выполнить преобразование с новым классом LineSegment , то для этого достаточно применить соответствующее преобразование к начальной и конечной точкам LineSegment , а затем передать их обратно в класс. Все точки между нии будут обработаны при отрисовке самого LineSegment , потому что для поиска всех последующих точек алгоритму Брезенхэма требуются только начальная и конечная точки.

Чтобы встроить класс LineSegment в уже существующий движок, нам нужно добавить в класс функцию draw() , поэтому я отказался от использования функции returnPointsInSegment . Эта функция будет возвращать массив всех точек, находящихся в отрезке прямой, что позволит нам удобно отрисовывать и отсекать отрезок.

Функция returnPointsInSegment() будет выглядеть примерно так (в JavaScript):

Простейший способ добавить рендеринг отрезков прямых в класс камеры — добавление простой структуры if , например, вот такой:

И это всё, что понадобится для работы нашего первого класса формы! Если вы хотите подробнее узнать технические аспекты алгоритма Брезенхэма (в особенности об ошибках), то можно прочитать о них в статье в Википедии.

Окружности


Источник: http://en.wikipedia.org/wiki/File:Bresenham_circle.svg

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

Новый алгоритм будет работать так:

  1. Получение центральной точки и радиуса окружности.
  2. Принудительное задание точек в каждом основном направлении
  3. Циклический обход каждого из квадрантов, отрисовка их дуг

Функция returnPointsInCircle() будет вести себя так же, как функция класса LineSegment , возвращая массив точек, чтобы камера могла отрендерить и отсечь их. Это позволяет движку обрабатывать множество разных форм, в каждую из которых нужно вносить только незначительные изменения.

Вот как будет выглядеть функция returnPointsInCircle() (в JavaScript):

Теперь мы просто добавим ещё одну конструкцию if в основной цикл отрисовки, и эти окружности будут полностью интегрированы в код!

Вот как может выглядеть обновлённый цикл отрисовки:

Теперь, когда у нас есть два новых класса, давайте сделаем что-нибудь!

Мастер растеризации

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

Давайте посмотрим на код:

Если всё получится успешно, то вы сможете отрисовывать с помощью движка отличные окружности.

Заключение

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

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

Часть 5: растеризация треугольников и четырёхугольников

Для создания классов Triangle и Quad мы будем активно использовать класс LineSegment .

Растеризация треугольников

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

Набросок класса будет выглядеть так:

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

Тогда с помощью класса LineSegment можно написать следующую функцию returnPointsInTriangle() :

Неплохо, правда? Мы уже проделали большую работу в классе LineSegment , поэтому нам просто последовательно соединить отрезки вместе для создания более сложных форм. Это позволяет нам с лёгкостью создавать на экране гораздо более сложные многоугольники (полигоны) простым добавлением новых LineSegment (и хранением большего количества точек в самом классе).

Теперь давайте посмотрим, как добавить в эту систему ещё точек, создав класс квадрата.

Работаем с квадратами

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

Теперь нам нужно просто добавить ещё один отрезок прямой в функцию returnPointsInQuad , вот так:

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

Работаем с полигонами

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

Второй — использование цикла для обхода всего неопределённого количества отрезков прямых в функции returnPointsInPolygon() , которая может выглядеть примерно так:

Добавив в движок этот класс, мы можем одной строкой кода создавать что угодно — от треугольника до 39-стороннего чудовища.

Создатель полигонов

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

Спецификации программы можно разбить на следующие части:

  • Первоначальная отрисовка полигона на экране.
  • При нажатии клавиши A снижать количество сторон полигона на 1.
  • При нажатии клавиши S увеличивать количество сторон полигона на 1.
  • Количество сторон полигона не должно быть меньше 3.
  • Количество сторон полигона не должно быть больше 10.

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

Заключение

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

Часть 5: цвета

Наш теоретический движок уже содержит почти всё необходимое, а именно:

  • Классы Point и Vector (строительные кирпичики движка).
  • Функции преобразований точек.
  • Класс Camera (задаёт область видимости и отсекает точки за пределами экрана).
  • Три класса для растеризации (отрезков прямых, окружностей и полигонов).

Цвет для всех!

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

Один из самых стандартных способов хранения цвета в программировании — использование красного, зелёного и синего значений (обычно это называется аддитивным смешением цветов). Сохраняя значение от 0 до 255 каждого из компонентов цвета, можно создать большую палитру цветов. (Таким образом определяют цвет большинство API, поэтому для совместимости логично использовать этот способ).

В зависимости от используемого графического API эти значения можно передавать или в десятеричном ( 255,0,0 ), или в шестнадцатеричном виде ( 0xFF0000 или #FF0000 ). Мы будем использовать десятеричный формат, потому что с ним гораздо проще работать. Кроме того, если ваш графический API использует шестнадцатеричные значения, то в нём скорее всего есть функция для преобразования десятеричных значений в шестнадцатеричные, то есть это не будет проблемой.

Чтобы начать реализацию цветовой модели, мы добавим три новых переменных в класс Point: red , blue и green . Пока не происходит ничего непонятного, но вот как должен теперь выглядеть набросок нашего класса Point :

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

Вид функции очень сильно зависит от используемого графического API, но во всех интерфейсах обычно есть похожая функция:

Если в вашем графическом API используются шестнадцатеричные, а не десятеричные значения цвета, то функция будет выглядеть примерно так:

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

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

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

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

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

Давайте используем наши новые возможности, написав программу.

Экспериментируем с 16,7 миллиона цветов

С помощью аддитивного смешения цветов мы с лёгкостью можем создать больше 16,7 миллиона цветов, используя простую запись ( r,g,b ). Мы создадим программу, которая пользуется всем этим огромным количеством цветов.

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

Программа будет иметь следующие спецификации:

  • Отрисовка объекта на экране.
  • При нажатии клавиши A значение красного компонента снижается, при нажатии Q — увеличивается.
  • При нажатии клавиши S значение зелёного компонента снижается, при нажатии W — увеличивается.
  • При нажатии клавиши D значение синего компонента снижается, при нажатии E — увеличивается.
  • Перерисовка объекта после обновления цвета.
  • Необходимо ограничивать значения компонентов и не давать им уходить за пределы от 0 до 255.

Теперь мы можем поэкспериментировать с объектом и придать ему любой цвет!

Заключение

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

Часть 7: динамическое освещение

В этой части мы рассмотрим только самые основы динамического освещения, так что не слишком пугайтесь (вся эта тема очень обширна и о ней пишут целые книги).

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

Повторение

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

Давайте создадим на основе этой информации простой класс освещения.

Класс освещения


Пример динамического освещения. Источник: http://redeyeware.zxq.net

Для работы класс освещения потребуется некоторая информация, а именно положение, цвет, тип и интенсивность (или радиус освещения).

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

С учётом всего этого класс будет выглядеть примерно так:

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

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

Свет? Камера? Мотор.


Ещё один пример динамического освещения. Источник: http://blog.illuminatelabs.com/2010/04/hdr-and-baked-lighting.html

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

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

С учётом всего этого мы можем добавить код, похожий на код из функции камеры drawScene() :

Как вы видите, наш способ изменения цвета точки пока не слишком сложен (однако существуют и многие другие, которые можно при желании использовать). Мы меняем цвет точки на процент, зависящий от расстояния до центра источника освещения. Наш способ освещения совершенно не учитывает затенения, поэтому далёкие от источника света точки не становятся темнее, а объекты не перекрывают свет от других объектов, которые могут находиться за ними.

Следуй за светом

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

Вот как может выглядеть программа:

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

Заключение

Хотя наше динамическое освещение и простое, его можно при желании запросто расширить. Некоторые довольно простые, но интересные дополнения:

Создание игрового движка с нуля на C++

Итак, вы хотите узнать больше об игровых движках и написать их самостоятельно? Это потрясающе! Чтобы помочь вам в вашем путешествии, вот несколько рекомендаций по библиотекам C++ и зависимостям, которые помогут вам взяться за дело.

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

Один из моих наставников, доктор Сепи, однажды сказал:

«Некоторые думают, что игры — это детское развлечение, но разработка игр — одна из немногих областей, в которой используются почти все предметы стандартной учебной программы по CS». Сепиде Чакаве] (https://www.conted.ox.ac.uk/profiles/sepideh-chakaveh)

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

В зависимости от характера вашей игры вам может понадобиться еще глубже погрузиться в более специализированные области, такие как распределенные системы или взаимодействие человека с компьютером. Разработка игр — серьезное дело, и она может стать мощным инструментом для изучения серьезных концепций CS.

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

При этом это будет не учебник по программированию. Я не буду вдаваться в технические подробности или объяснять, как все эти элементы склеены вместе с помощью кода. Если вы ищете исчерпывающую видеокнигу о том, как написать игровой движок на C++, это отличная отправная точка: [Создание 2D-игрового движка с C++ и Lua] (https://pikuma.com/courses/cpp-2d). -игровой-движок-разработка).

Что такое игровой движок?

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

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

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

Чтобы действительно понять, как работает этот API, давайте рассмотрим его в контексте. Например, нередко API игрового движка предоставляет функцию под названием «IsColliding()», которую разработчики могут вызывать, чтобы проверить, сталкиваются ли два игровых объекта или нет. Программисту не нужно знать, как реализована эта функция или каков алгоритм, необходимый для правильного определения того, перекрываются ли две фигуры. Насколько нам известно, функция IsColliding представляет собой черный ящик, который творит чудеса и корректно возвращает true или false, если эти объекты сталкиваются друг с другом или нет. Это пример функции, которую большинство игровых движков предоставляют своим пользователям.

если (IsColliding(игрок, пуля)) <

![Большинство движков абстрагируются от обнаружения столкновений и просто отображают его как функцию true/false.]

Помимо API для программирования, еще одной большой обязанностью игрового движка является аппаратная абстракция. Например, 3D-движки обычно строятся на специальном графическом API, таком как [OpenGL] (https://www.opengl.org/), [Vulkan] (https://www.vulkan.org/) или [Direct3D]. (https://docs.microsoft.com/en-us/windows/win32/direct3d). Эти API обеспечивают программную абстракцию для графического процессора (GPU).

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

Восстание игровых движков

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

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

Некоторые популярные классические движки: id Tech, Build и AGI . Эти движки были созданы для помощи в разработке конкретных игр, и они позволяли другим членам команды быстро разрабатывать новые уровни, добавлять пользовательские ресурсы и настраивать карты на лету. Эти пользовательские движки также использовались для [модификации] (https://en.wikipedia.org/wiki/Video_game_modding) или создания пакетов расширения для своих оригинальных игр.

Id Software разработала id Tech. id Tech — это набор различных движков, где каждая итерация связана с отдельной игрой. Часто можно услышать, как разработчики описывают id Tech 0 как «движок Wolfenstein3D», id Tech 1 как «движок Doom», а id Tech 2 как «движок Quake».

Сборка — еще один пример движка, который помог сформировать историю игр 90-х. Он был создан [Кеном Сильверманом] (http://advsys.net/ken/), чтобы облегчить настройку шутеров от первого лица. Подобно тому, что случилось с id Tech, Build развивался со временем, и его различные версии помогли программистам разрабатывать такие игры, как Duke Nukem 3D, Shadow Warrior и Кровь. Это, пожалуй, самые популярные игры, созданные с использованием движка Build, и их часто называют «большой тройкой».

Еще одним примером игрового движка из 90-х была «Утилита создания сценариев для Manic Mansion» (SCUMM). SCUMM — это движок, разработанный LucasArts, и он лежит в основе многих классических игр Point-and-Click, таких как Monkey Island и Full Дроссель.

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

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

Зачем делать игровой движок?

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

Существует множество бесплатных, мощных и профессиональных [коммерческих движков] (https://www.incredibuild.com/blog/top-7-gaming-engines-you-should-consider-for-2020), которые разработчики могут использовать для создания и развертывать свои собственные игры. С таким большим выбором игровых движков, зачем кому-то создавать игровой движок с нуля?

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

  • Возможность обучения: низкоуровневое понимание того, как работают игровые движки внутри, может помочь вам вырасти как разработчику.
  • Управление рабочим процессом: у вас будет больше контроля над особыми аспектами вашей игры, и вы сможете настроить решение в соответствии с вашими потребностями рабочего процесса.
  • Настройка: вы сможете адаптировать решение под уникальные игровые требования.
  • Минимализм: меньшая кодовая база может уменьшить накладные расходы, связанные с большими игровыми движками.
  • Инновация: вам может понадобиться внедрить что-то совершенно новое или настроить таргетинг на нетрадиционное оборудование, которое не поддерживает ни один другой движок.

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

Как сделать игровой движок

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

1. Выбор языка программирования

Одним из первых решений, с которыми мы сталкиваемся, является выбор языка программирования, который мы будем использовать для разработки основного кода движка. Я видел, как движки разрабатывались на необработанном ассемблере, C, C++ и даже на таких высокоуровневых языках, как C#, Java, Lua и даже JavaScript!

Одним из самых популярных языков для написания игровых движков является C++. Язык программирования C++ сочетает в себе скорость с возможностью использования объектно-ориентированного программирования (ООП) и других парадигм программирования, которые помогают разработчикам организовывать и разрабатывать большие программные проекты.

Поскольку при разработке игр обычно очень важна производительность, C++ имеет то преимущество, что является компилируемым языком. Компилируемый язык означает, что окончательные исполняемые файлы будут запускаться на процессоре целевой машины. Существует также множество специализированных библиотек C++ и наборов средств разработки для большинства современных консолей, таких как PlayStation или Xbox.

![Разработчики могут получить доступ к контроллеру Xbox с помощью библиотек C++, предоставленных Microsoft.]

Говоря о производительности, лично я не рекомендую языки, использующие виртуальные машины, байт-код или любой другой промежуточный уровень. Помимо C++, некоторыми современными альтернативами, которые подходят для написания основного кода игрового движка, являются [Rust] (https://www.rust-lang.org/), [Odin] (https://odin-lang.org/), и [Zig] (https://ziglang.org/).

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

2. Доступ к оборудованию

В более старых операционных системах, таких как MS-DOS, мы обычно могли использовать адреса памяти и получать доступ к специальным ячейкам, которые были сопоставлены с различными аппаратными компонентами. Например, все, что мне нужно было сделать, чтобы «раскрасить» пиксель определенным цветом, — это загрузить в специальный адрес памяти число, представляющее правильный цвет моей палитры VGA, и драйвер дисплея преобразовал это изменение в физический пиксель в ЭЛТ-монитор.

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

Например, если вы используете Windows, macOS, Linux или *BSD, вам необходимо запросить у ОС правильные разрешения для рисования и рисования пикселей на экране или взаимодействия с любым другим аппаратным компонентом. Даже простая задача открытия окна на рабочем столе ОС должна выполняться через API операционной системы.

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

Одной из очень популярных библиотек, которая помогает с абстракцией оборудования для разных платформ, является SDL. Лично мне нравится использовать SDL, когда я преподаю классы разработчиков игр, потому что с SDL мне не нужно создавать одну версию моего кода для Windows, другую версию для macOS и еще одну для студентов Linux. SDL работает как мост не только для разных операционных систем, но и для разных архитектур ЦП (Intel, ARM, Apple M1 и т. д.). Библиотека SDL абстрагирует низкоуровневый доступ к оборудованию и «переводит» наш код для правильной работы на этих разных платформах.

Вот минимальный фрагмент кода, который использует SDL для открытия окна в операционной системе. Я не обрабатываю ошибки для простоты, но приведенный ниже код будет одинаковым для Windows, macOS, Linux, BSD и даже RaspberryPi.

include

SDL_Window* window = SDL_CreateWindow(«Мое окно», 0, 0, 800, 600, 0);

SDL_Renderer* renderer = SDL_CreateRenderer(окно, -1, 0);

Но SDL — это всего лишь один пример библиотеки, которую мы можем использовать для достижения этого многоплатформенного доступа к оборудованию. SDL — популярный выбор для 2D-игр и переноса существующего кода на разные платформы и консоли. Другой популярный вариант многоплатформенной библиотеки, которая используется в основном с 3D-играми и 3D-движками, — GLFW. Библиотека GLFW очень хорошо взаимодействует с ускоренными 3D API, такими как OpenGL и Vulkan.

3. Игровой цикл

Когда у нас открыто окно ОС, нам нужно создать контролируемый [игровой цикл] (https://gameprogrammingpatterns.com/game-loop.html).

Проще говоря, мы обычно хотим, чтобы наши игры работали со скоростью 60 кадров в секунду. Частота кадров может различаться в зависимости от игры, но для сравнения: фильмы, снятые на пленку, воспроизводятся со скоростью 24 кадра в секунду (каждую секунду перед вашими глазами мелькают 24 изображения).

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

  • Обработка событий ввода без блокировки
  • Обновить все игровые объекты и их свойства для текущего кадра
  • Визуализировать все игровые объекты и другую важную информацию на экране

Это милый цикл while. Мы все? Точно нет!

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

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

4. Ввод

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

Для обработки пользовательского ввода мы должны запросить доступ к аппаратным событиям, и это должно быть выполнено через API операционной системы. Хорошая новость заключается в том, что мы можем использовать мультиплатформенную библиотеку аппаратных абстракций (SDL, GLFW, SFML и т. д.) для обработки пользовательского ввода.

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

в то время как (SDL_PollEvent(&event)) <

если (event.key.keysym.sym == SDLK_SPACE) <

Опять же, если мы используем кросс-платформенную библиотеку, такую ​​как SDL, для обработки ввода, нам не нужно слишком беспокоиться о реализации для конкретной ОС. Наш код C++ должен быть одинаковым независимо от платформы, на которую мы ориентируемся.

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

5. Представление игровых объектов в памяти

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

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

Если одна из ваших целей — узнать больше об алгоритмах и структурах данных, я рекомендую вам попробовать реализовать эти структуры данных самостоятельно. Если вы используете C++, одним из вариантов является использование STL (стандартная библиотека шаблонов) и использование многих структур данных, которые поставляются с ней (векторы , списки, очереди, стеки, карты, наборы и т. д.). C++ STL сильно зависит от шаблонов, так что это может быть хорошей возможностью попрактиковаться в работе с шаблонами и увидеть их в действии в настоящий проект.

Когда вы начнете больше читать об архитектуре игрового движка, вы увидите, что один из самых популярных шаблонов проектирования, используемых в играх, основан на сущностях и компонентах. entity-component организует объекты нашей игровой сцены как сущности (то, что Unity называет «игровыми объектами», а Unreal называет «актерами») и компоненты (данные, которые мы можем добавить или прикрепить к нашим сущностям).

Чтобы понять, как сущности и компоненты работают вместе, представьте себе простую игровую сцену. Объекты будут нашим основным игроком, враги, пол, снаряды, а компоненты будут важными блоками данных, которые мы «прикрепляем» к нашим объектам, например, положение, скорость, коллайдер твердого тела и т. д.

![Популярным шаблоном проектирования игрового движка является организация игровых элементов в виде сущностей и компонентов.]

Некоторые примеры компонентов, которые мы можем прикрепить к нашим объектам:

  • Компонент положения: Отслеживает координаты положения нашего объекта в мире (или x-y-z в 3D).
  • Компонент скорости: отслеживает скорость движения объекта по оси x-y (или x-y-z в 3D).
  • Компонент Sprite: Обычно он хранит PNG-изображение, которое мы должны визуализировать для определенного объекта.
  • Компонент анимации: отслеживает скорость анимации объекта и то, как кадры анимации меняются с течением времени.
  • Компонент коллайдера: Обычно он связан с физическими характеристиками твердого тела и определяет сталкивающуюся форму объекта (ограничивающий прямоугольник, ограничивающий круг, сетчатый коллайдер и т. д.).
  • Компонент здоровья: сохраняет текущее значение здоровья объекта. Обычно это просто число или, в некоторых случаях, процентное значение (например, полоса здоровья).
  • Компонент сценария: Иногда к нашей сущности может быть прикреплен компонент сценария, который может быть внешним файлом сценария (Lua, Python и т. д.), который наши механизмы должны интерпретировать и выполнять за кулисами.

Это очень популярный способ представления игровых объектов и важных игровых данных. У нас есть сущности, и мы «подключаем» к нашим сущностям разные компоненты.

Существует множество книг и статей, в которых исследуется, как мы должны реализовывать дизайн сущностных компонентов, а также какие структуры данных мы должны использовать в этой реализации. Структуры данных, которые мы используем, и то, как мы получаем к ним доступ, напрямую влияют на производительность нашей игры, и вы наверняка слышали, как разработчики упоминают такие вещи, как [дизайн, ориентированный на данные] (https://en.wikipedia.org/wiki/Data-Oriented_design). ), Entity-Component-System (ECS), локальность данных и многие другие. другие идеи, которые полностью связаны с тем, как наши игровые данные хранятся в памяти и как мы можем эффективно получить доступ к этим данным.

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

Есть несколько популярных вариантов готовых к использованию библиотек ECS, которые мы можем включить в наш проект C++ и начать создавать сущности и присоединять компоненты, не беспокоясь о том, как они реализованы внутри. Некоторыми примерами библиотек C++ ECS являются [EnTT] (https://github.com/skypjack/entt/wiki) и [Flecs] (https://github.com/SanderMertens/flecs).

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

А теперь серьезный разговор! После того, как вы закончите свою специальную реализацию ECS, я бы посоветовал вам просто использовать некоторые из популярных сторонних библиотек ECS (EnTT, Flecs и т. д.). Это профессиональные библиотеки, которые разрабатывались и тестировались индустрией в течение нескольких лет. Они, вероятно, намного лучше, чем все, что мы могли бы придумать с нуля сами.

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

6. Рендеринг

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

Первый шаг — рассмотреть характер игр, которые мы будем создавать с помощью нашего движка. Мы создаем игровой движок для разработки только 2D-игр? Если это так, нам нужно подумать о рендеринге спрайтов, текстур, управлении слоями и, возможно, воспользоваться ускорением видеокарты. Хорошая новость заключается в том, что 2D-игры обычно проще, чем 3D, а 2D-математика значительно проще, чем 3D-математика.

Если вашей целью является разработка 2D-движка, вы можете использовать SDL для облегчения многоплатформенного рендеринга. SDL абстрагирует аппаратное ускорение графического процессора, может декодировать и отображать изображения PNG, рисовать спрайты и отображать текстуры в нашем игровом окне.

Теперь, если вашей целью является разработка 3D-движка, нам нужно определить, как мы будем отправлять некоторую дополнительную 3D-информацию (вершины, текстуры, шейдеры и т. д.) на графический процессор. Вы, вероятно, захотите использовать программную абстракцию графического оборудования, и наиболее популярными вариантами являются [OpenGL] (https://www.opengl.org/), [Direct3D] (https://en.wikipedia.org). /wiki/Direct3D), Вулкан и Металл. Решение о том, какой API использовать, может зависеть от вашей целевой платформы. Например, Direct3D будет поддерживать приложения Microsoft, а Metal будет работать исключительно с продуктами Apple.

3D-приложения работают, обрабатывая 3D-данные через графический конвейер. Этот конвейер будет определять, как ваш движок должен отправлять графическую информацию на графический процессор (вершины, координаты текстуры, нормали и т. д.). Графический API и конвейер также определяют, как мы должны писать программируемые [шейдеры] (https://en.wikipedia.org/wiki/Shader) для преобразования и изменения вершин и пикселей нашей 3D-сцены.

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

Говоря о трехмерных объектах и ​​вершинах, хорошей идеей будет делегировать библиотеке задачу чтения и декодирования различных форматов сетки. Существует множество популярных форматов 3D-моделей, о которых должны знать большинство сторонних 3D-движков. Некоторые примеры файлов: .OBJ, Collada, FBX и DAE. Я рекомендую начать с файлов .OBJ. Существуют хорошо протестированные и хорошо поддерживаемые библиотеки, которые обрабатывают загрузку OBJ с помощью C++. TinyOBJLoader и AssImp — отличные варианты, которые используются многими игровыми движками.

7. Физика

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

Здесь нам также необходимо рассмотреть, какой тип физики мы хотим смоделировать. 2D-физика обычно проще, чем 3D, но базовые части физического моделирования очень похожи на 2D- и 3D-движки.

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

Для 2D-физики я рекомендую посмотреть [Box2D] (https://box2d.org/) и [Chipmunk2D] (https://chipmunk-physics.net/). Для профессионального и стабильного трехмерного физического моделирования хорошими названиями являются такие библиотеки, как [PhysX] (https://developer.nvidia.com/physx-sdk) и [Bullet] (https://pybullet.org/). Использование стороннего физического движка всегда полезно, если стабильность физики и скорость разработки имеют решающее значение для вашего проекта.

![Box2D — это очень популярный вариант физической библиотеки, которую вы можете использовать с вашим игровым движком.]

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

Если вы хотите узнать больше о физических движках, вы можете воспользоваться несколькими хорошими книгами и онлайн-ресурсами. Для 2D-физики твердого тела вы можете посмотреть [исходный код Box2D] (https://github.com/erincatto/box2d) и [слайды] (https://box2d.org/publications/) от Эрин Катто. . Но если вы ищете исчерпывающий курс по игровой физике, то [2D Game Physics from Scratch] (https://pikuma.com/courses/game-physics-engine-programming), вероятно, будет хорошим началом.

Если вы хотите узнать о трехмерной физике и о том, как реализовать надежную симуляцию физики, другим отличным ресурсом является книга «[Игровая физика] (https://www.amazon.co.uk/Game-Physics-David-H-Eberly). /dp/0123749034)» Дэвида Эберли.

8. Пользовательский интерфейс

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

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

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

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

Если вашей целью является создание инструментов пользовательского интерфейса для вашего движка, я рекомендую использовать существующую стороннюю библиотеку пользовательского интерфейса. Быстрый поиск в Google покажет вам, что наиболее популярными вариантами являются [Уважаемый ImGui] (https://github.com/ocornut/imgui), [Qt] (https://github.com/qt) и [Nuklear]. (https://github.com/vurtun/nuklear).

![ImGui — это мощная библиотека пользовательского интерфейса, которая используется многими игровыми движками в качестве инструмента редактирования.]

Уважаемый ImGui — один из моих любимых, поскольку он позволяет нам быстро настраивать пользовательские интерфейсы для инструментов движка. В проекте ImGui используется шаблон проектирования под названием «[немедленный режим пользовательского интерфейса] (https://en.wikipedia.org/wiki/Immediate_mode_GUI)», и он широко используется с игровыми движками, поскольку он хорошо взаимодействует с 3D-приложениями, используя преимущества ускоренный рендеринг GPU.

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

9. Сценарии

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

Идея проста; мы встраиваем язык сценариев в наше родное приложение C++, и этот более простой язык сценариев может использоваться непрофессиональными программистами для описания поведения объектов, логики ИИ, анимации и других важных аспектов нашей игры.

Некоторые из популярных языков сценариев для игр: [Lua] (https://www.lua.org/), [Wren] (https://wren.io/), [C#] (https://en.wikipedia). .org/wiki/C_Sharp_(язык_программирования)), Python и JavaScript. Все эти языки работают на значительно более высоком уровне, чем наш родной код C++. Тот, кто пишет сценарии игрового поведения с использованием языка сценариев, не должен беспокоиться о таких вещах, как управление памятью или другие низкоуровневые детали работы основного движка. Все, что им нужно сделать, это запрограммировать уровни, а наш движок знает, как интерпретировать сценарии и выполнять сложные задачи за кулисами.

Мой любимый скриптовый язык — Lua. Lua небольшой, быстрый и очень легко интегрируется с собственным кодом C и C++. Кроме того, если я работаю с Lua и «современным» C++, мне нравится использовать библиотеку-оболочку под названием [Sol] (https://github.com/ThePhD/sol2). Библиотека Sol помогает мне начать работу с Lua и предлагает множество вспомогательных функций для улучшения традиционного Lua C-API.

Если мы включим скрипты, мы почти достигнем точки, когда сможем начать говорить о более сложных темах в нашем игровом движке. Скрипты помогают нам определять логику ИИ, настраивать кадры и движения анимации, а также другое игровое поведение, которое не должно жить внутри нашего собственного кода C++ и которым можно легко управлять с помощью внешних скриптов.

10. Аудио

Еще один элемент, который вы могли бы добавить в игровой движок, — это звук.

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

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

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

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

Вот несколько хороших библиотек и инструментов для аудио, которые вы можете интегрировать с игровым движком: SDL_Mixer, SoLoud и FMOD.

![Tiny Combat Arena использует библиотеку FMOD для звуковых эффектов, таких как допплер и сжатие.]

11. Искусственный интеллект

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

В играх ИИ используется для создания отзывчивого, адаптивного или интеллектуального поведения игровых объектов. Большая часть логики ИИ добавляется к неигровым персонажам (NPC, врагам), чтобы имитировать человеческий интеллект.

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

Подробная книга о теории и реализации искусственного интеллекта для игр называется [ИИ для игр] (https://www.amazon.co.uk/AI-Games-Third-Ian-Millington/dp/1138483974) Яна Миллингтона. .

Не пытайтесь сделать все сразу

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

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

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

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

Не торопитесь и сосредоточьтесь на основах

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

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

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

Сосредоточьтесь на основах и владейте этими знаниями. Неважно, насколько мала или проста концепция, владейте ею. Все остальное — эго.

How to make your own game engine (and why)

Tyler Glaiel

So you’re thinking about making your own game engine. Great! There’s lots of reasons to want to make one yourself instead of using a commercial one like Unity or Unreal. In this post I will go over why you might want to, what systems are needed in a game engine, and how you should approach development of it. I won’t be going into any deep technical details here, this is about why and how to develop a game engine, not a tutorial for how to write the code.

Lets start with the absolute first question you should be asking yourself if you want to make your own game engine: Why?

Here’s a few good reasons why you might want to:

  • Novel Tech: You want to make a game that uses a piece of novel tech that no other engines out there currently support, or can’t be easily made to support in their current state. This can mean some kind of massive-scale simulation that requires some to-the-metal coding to make performant (Factorio) or some custom thing that doesn’t fit into any existing molds (Noita, Miegakure), or wishing to target an odd piece of hardware that current engines don’t support (Playdate), or any number of other things really. It’s a good reason to make your own engine, cause there isn’t any other option in cases like this.
  • Specialization: You want to optimize your workflow for the kind of games you make. You don’t need all the features included in a commercial game engine, and you can make your asset pipeline / level editor / whatever way smoother to use when considering your specific use cases instead of needing it to be general purpose. Specialization is almost a requirement of making your own engine, if you aren’t specializing it and catering it towards your exact use case, you should rethink why you’re making an engine in the first place.
  • Independence: You don’t want to be dependent on someone else’s technology in the long term. The incentives and values of a company like Unity or Epic are not always going to align with your own, and you want control over your own tech, the ability to fix bugs yourself instead of “waiting and hoping”, and comfort knowing that an update won’t completely break your current project. You are willing to eat the cost of developing your own tech, because in the long run its good to not have to constantly change stuff depending on the whims of giant companies.
  • Curiosity/Learning: You’re just curious about how it works and why other engines have made certain decisions they did. This is a great reason, one of the best reasons actually, to make your own game engine.

Also while I’m making lists, here’s a couple of bad reasons for why you would want to make your own game engine. If any of these are your (only) motivation, you should back up and reconsider:

  • I can do it better: You think you can just make something better than Unity or Unreal (or Godot or GameMaker) in general. You can’t. It’s possible to make something that is better than these for specific use cases (see: Specialization above), but you, as an individual or a tiny team, are not going to compete with these for general purpose stuff. Especially if you have never made your own game engine before.
  • It’s the what real programmers do: There is no “right way” to make a game, you don’t win programmer points for making your own engine. If your game is a good fit for an existing engine (and I’d argue like, 99% of games are), there’s no shame in using one. After all, a game engine is simply a tool for which to make a game, its not and should never be the goal by itself. The games you make with it are all that matters.
  • Saving Money/Time: You most likely won’t. Making an engine takes time, and time=money. Unity’s cost is negligible compared to the time it takes to make your own tech. Unreal’s rev share is negligible unless you’re selling 10 million copies of a AAA game. Using your own engine won’t make you sell more copies of your game automatically. And while you *can* save time in the long run, this usually means having your engine be good enough to carry you across multiple projects, while also providing you with significant workflow improvements compared to commercial engines. It’s not easy to get this right, and you definitely won’t if its your first try at it (and extremely unlikely if you’re doing 3D instead of 2D).

Also, you should absolutely consider your own experience and goals when weighing all of this. If you don’t have a lot of experience making games, you will have a much harder time making an engine (and you should definitely go get some game making experience before trying to make an engine anyway). If you want to do 3D, you will have a much harder time than just doing 2D.

For me, the motivations to making my own engine are mostly about novel tech and specialization. I come from a flash game background, having made quite a lot of them in the mid/late 00s, and its a workflow I am really comfortable with and enjoy. None of the existing engines out there supported importing flash animations, so my only option was to do it myself. It’s extremely nice to just be able to plop .swf files into my resources folder and instantly have the animations available for use in my game code, without needing any middle steps to export them to sprite sheets or whatever.

These aren’t the only reasons why you’d want to make your own game engine. There’s a ton of advantages (and disadvantages) to it, and whether or not the pros outweigh the cons depends a whole lot on how much experience you have with both game dev in general and lower level coding. Like, it’s nice to just have your own tech, and its nice to not have to constantly google for tutorials that might be outdated for how to do something in your engine. It’s nice to be able to actually debug the internals of your game if something goes wrong. But it can also suck if you made a couple of bad design choices and everything falls apart entirely, and there’s no resources online to help you. You have full control and full responsibility, and all the pros and cons that come with that.

What?

Before we get into how to make your own game engine, lets take a step back and define what a game engine actually is, and the types of problems a game engine is supposed to solve.

A game engine is the framework upon which you make a game. It provides you a foundation on which to build on top of, and all the building blocks and lego pieces you need to assemble a game out of. It provides a boundary between “game logic” and “boring technical stuff” so that your game logic code (the stuff that actually describes what your game is, how it controls, the interactions between objects, and the rules), doesn’t need to actually care about stuff like how to send a triangle to the graphics card.

There’s actually a lot of variance in how much game engines actually do for you. Some of them are barely more than just a framework for displaying graphics, doing very little for you beyond that (Flash, Pico-8). Some of them are basically an entire game by themselves, or at least hyper-specialized for a specific genre, putting a ton of common game logic in the engine itself (RPGMaker, Ren’Py). And everything in-between.

Game engines themselves also tend to be built on top of even lower level frameworks like SDL and OpenGL, and include many special purpose libraries for things like audio, physics, math, and whatever else is out there that you find useful. Making a custom game engine does not mean writing every little piece of it yourself, especially something as standard and useful as SDL. Assembling the right set of existing libraries for your use case is part of making an engine as well, and there do exist libraries out there for nearly all of the systems you could want in your engine.

Anyway with that in mind, lets talk a bit about what I consider the most basic feature set you need for your game engine. The bare essentials. The minimum that you need before you can start making a game.

  • System Initialization: opening a window, obtaining an OpenGL/DirectX/Vulkan context, initializing audio. SDL will handle all of this for you, so just use SDL. Like really, SDL is industry standard at this point for custom engines, there’s no reason to do this part yourself.
  • Frame Timing Control: You want your game to run 60fps, so you need some kind of timer and loop that controls when updates and renders happen.
  • Input: You need to be able to respond to button presses. There’s a lot of different ways to respond to button presses, maybe you just want to be able to query the current state of a button, or maybe you want to register events, it doesn’t really matter. SDL will report button inputs to you, if you’re already using that for system initialization you should be using it here as well. You can build a really powerful and flexible input system on top of this, but its not necessary to do anything more than the basics at first.
  • Rendering: Most, probably at least like 75% of games, use graphics in some way, and this is absolutely something that should be your engine’s responsibility. If you’re doing a 2D game, the bare minimum renderer just needs to be able to display simple textured quads on the screen. Shaders, vertex buffers, render targets, meshes, materials, and whatnot are all great, but they can come later as you need them. If you want to get your hands dirty with OpenGL or Vulkan or whatever you can absolutely go ham with your custom renderer, but again there’s no shame in using an existing library like Ogre3D to cover rendering. Entirely depends on your goals and your needs and what problems you actually find interesting to solve.
  • Math & Misc. Utilities: I don’t really consider “vectors and matrix math” a full on “System” but it is something you should probably have as a utility library that engine and game code both have access to. Plus any other random useful functions and formulas you find as you dev. STB is an incredibly great resource for random utility stuff you might need.

Also here’s a few systems that are bare essentials as well, but you don’t need to add them until later on when you actually need to start using them.

  • Game Object / Scene Management: Coding everything in one big update function isn’t actually as bad as it seems, but if your game starts getting even a little bit complex you’re going to want some kind of system to handle individual game objects and collections of game objects. In a way this ends up being the most important system in your engine, as it is what drives what your game logic looks like. Are your objects big inheritance trees? Are they made up of smaller components? Are you using ECS? (sidenote: you probably shouldn’t use “pure” ECS unless you have a really specific use case for it) What types of events do they respond to? How do you query for other objects to interact with? Do you care about memory layout? These are all things that your game object / scene management system should cover. There are some existing libraries for this, especially for pure ECS, however since this is the system that has the most impact on what your game code looks like, I heavily lean towards “do it yourself” here. Relying on an existing solution will force you to constantly think about how to fit your game logic into that mold. Exactly backwards from what you’d usually want. You want your engine to be designed to support the way you want to express game logic, not the other way around.
  • Audio: Sound Effects and Music are kind of separate systems here, though both lumped under “Audio”. Basic functionally you need is the ability to start and stop audio loops (music) and the ability to play one off sound effects in their entirety. There’s a whole lot more to audio than just this, but you can get very far with just the basics here. The annoying thing about audio is that the industry standard audio frameworks (FMod and Wwise) are both commercial and come with a bunch of licensing restrictions, but a lot of the open source ones are just really annoying to use (shoutout to OpenAL). Personally I find FAudio pretty simple and pretty great, and I use that as a base to build more complicated behavior off of.
  • File Loading & Management: All games need to load files. You need a way to load files. You probably don’t want to redundantly reload/decode files that have already been loaded in, so you need some kind of basic manager that handles all of this. You might want to be able to build mod support or dynamic loading/unloading or whatever off of this, as long as you have the basics here and only ever load files through the file manager, you can easily add whatever other functionality you want into this later. You don’t need this immediately, since you can just use built in file loading as a placeholder, but you will want to eventually have a file / resource manager system in place once you start using files more often.
  • Networking: Ok, networking (online multiplayer) is VERY optional (if you don’t need online multiplayer, do not bother), however its an exceptionally difficult system to staple onto an engine that was not designed with multiplayer in mind, so if you are making a multiplayer game you need to consider this early because its mere existence influences the design of literally every single other system.

That is the basic set of systems that you’d need to have what I consider a game engine. Other systems like Collision and Physics and Serialization and Animation and UI and whatnot are actually optional. Most engines have them because they’re common enough that its worth including, but you don’t need those to make a game. You can get away with simple collision checks in your math utility library, and just let game logic do that all manually. You can do basic gravity and acceleration without a physics engine like Box2D or Bullet. And full serialization can often be huge overkill if all you need to do is save which checkpoint you spawn at.

If you’re writing your own engine, you will necessarily have less systems and features in it than the big general purpose commercial ones have. That should be the goal! Unity and Unreal are bloated monoliths, any individual game is only using a small slice of what they have to offer. Add only what you need, for your specific use case, and focus on making what’s important to you and your game extra good.

I very strongly recommend becoming familiar with how a bunch of different game engines work before starting out making your own. Learn what kinds of paradigms and structures they use, what’s cool about them and what’s annoying about them. Make a small game in a bunch of different engines if you can, just to obtain that knowledge.

Ok so you’ve weighed the whys and explored the whats, and decided that you want to make your own game engine. How do you go about doing this?

I’ll get straight to the point: Make a game at the same time as you’re making the engine. This is an unbreakable rule. The only unbreakable rule. Get the basics in as fast as you possibly can and then immediately start making a game on top of it. An engine is nothing without a game.

You need to do this, because the features of an engine should be informed by the needs of the games made in it. You will not understand how to architect a good animation system if you don’t have a game that needs complicated animation flow. You will not know what your performance bottlenecks are if you don’t have a game. Do you need some kind of tree based world structure that will avoid updating or rendering objects that are too far away from the camera? I don’t know, and you don’t know either, until you have a large level that runs like crap. Even then, updating the objects might not be the actual bottleneck and you wont know until you profile it.

Don’t code something until you need it. If the only UI in your game is a play button on the main menu, congrats! You don’t need to code a fancy UI system for your engine. The End Is Nigh shipped without a physics engine, or even a collision broadphase. Hell it didn’t even have a camera system, since the game just didn’t need it. I used a .csv spreadsheet to arrange the world map instead of some complicated editor. It was easy and worked fine.

I’m not going deep into implementation details for these systems because there’s just way too many ways to do it that all work just fine for different purposes. There is no “best way to do rendering” or “best way to do game object management”. It all depends on your game. Start with the basics and expand them as you find need to.

For programming languages, use what you’re comfortable with. Making an engine is a big involved task, if you also try to learn C++ at the same time as trying to learn how to make a game engine you’re just doubling the difficulty of learning either of those tasks. C# is perfectly fine for making a game engine. Slower than C++, but often not slow enough to matter. Something really slow like python might be a bit of a stretch if your game has a lot of moving parts… but for some kinds of games it’s still fine. Use what you’re comfortable with.

Also, you will never get this right on your first try. My first game with a custom engine was Closure, and it was an absolute mess (despite, kind of hilariously, being nominated for a Technical Excellence award at the 2010 IGF). One big update function and one big render function handled basically the entirety of the game. Adding new kinds of game objects was extremely annoying and required adding code in a ton of different places, and working with some janky ass custom editors for animations, so we ended up with only about a dozen different kinds of interactive objects in the end, though some of them had some variants (bool toggles) that changed their behavior significantly because it was easier to add variants than it was to add new objects. The spotlights, mirrors, and turret are all the same object!

But you learn from this experience. Closure’s engine was a mess, but it still shipped in that state, and it was “good enough” that I could run it on the PS3 without that much hassle. It was tempting to rewrite the engine at points, but rather than do that (which would have only served to delay the game), I just kept notes of all the things about it that sucked so I could do things better the next time. Especially things that got in the way of actually making the game. I did the same thing with The End is Nigh. Its engine (while much, MUCH better than Closure’s) still had a bunch of issues with it that I just grit my teeth and dealt with during dev. As soon as the game came out, I immediately started updating the engine for use in our next project, fixing all the annoyances with it and adding in the new features we needed.

It’s an iterative process through and through. You learn, make a game, iterate and repeat. Over and over until eventually your engine is good.

Hopefully you should understand why I didn’t bother giving any technical details for how to implement all those individual systems. It just depends way too much on your specific use cases, and there’s hundreds of different, completely valid ways, to approach each of them. Figuring out what works for you IS what making your own game engine is about, and that’s the mindset you should be in when writing your own stuff.

Anyway that’s about all I wanted to cover in this post, hopefully you’re either motivated to write your own game engine now, or scared away from the thought of it entirely. Both are good outcomes! If you have any actual questions about technical details or opinions on game dev or game engine development, feel free to ask me about it on Twitter (publicly) and I’ll do my best to respond. Thanks for reading!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *