Как создать игровой движок
Перейти к содержимому

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

  • автор:

Разработка игрового движка

В данный момент существует множество игровых движков под разные типы игр. Можно взять любой и при наличии опыта программирования или огромного желания, начинать реализовывать проект своей мечты. Разные движки требуют различного уровня знаний и опыта. Если ориентироваться на платформу iOS можно выбрать SpriteKit или Cocos2D, если выбрать Android — выбор будет LibGDX или AndEngine. Но часто берут кроссплатформенные движки — код пишется один раз и с небольшими доработками или без них компилируется под разные платформы.

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

Выбор движка по бэкграунду знаний:

  • Cocos2Dx, Unreal Engine — C++
  • LOVE, Corona SDK, Defold — Lua
  • Unity3D — C#
  • SpriteKit — Swift \ ObjectiveC
  • AndEngine, LibGDX — Java

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

А не написать ли мне свой движок?

Вводная часть закончена, теперь можно и представиться. Меня зовут Надтока Антон, я мобильный разработчик для iOS и Android. В свободное от работы время я пишу мобильный движок для игр. Будучи студентом как и многие другие (думаю были и такие) хотел создать свой Counter Strike. Еще был OpenGL 1.1 и в то время я реализовывал небольшой движок для Windows, который позволял отображать SkyBox, оружие, уровень и эффект стрельбы. Чтобы скрыть недостатки графики использовался туман. Позже работая по разным направлениям сконцентрировался на мобильной разработке в начале 2012. Удалось пощупать и поработать с AndEngine, Cocos2Dx и Unity.

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

Причины написать сейчас свой движок:

  • Узнать как все работает под капотом
  • Полный контроль над кодом и поведением движка
  • Компактный размер финального результата
  • Добавление новых функций в конкретные сроки
  • Получить кайф от разработки

Посколько я начал работать с С++ с 2005 года выбор языка отпал сам собой. За основу был взят стандарт C++11.

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

Разработка собственного движка, напутствие

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

АТЕНШОН, МНОГА ТЕКСТА НЕТ ПИКЧ

Предостережение

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

Поясню — что бы написать минимально рабочую игру, тебе нужно: обработка ввода, вывод графики и игровой цикл с логикой. Можно сказать — «всего 3 компоненты и ты уже можешь делать свои крестики-нолики или тетрисы!». Но для звания «великого игрового движка», при мысли о котором ты возбуждаешься, явно не хватает всего 3х компонент. Различные механизмы подгрузки данных, гибкие системы рендера, система скриптов, встроенные редакторы уровней — всё это требует времени на реализацию, и чем больше ты хочешь, тем больше времени у тебя уйдёт на разработку «движка с 0», а сколько времени потратится на написание самой игры?

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

А как писать?

Для начала нужно подвести черту между двумя кардинально разными подходами в написании этих ваших «движков». Я обозвал их так.

  • «Динамические движи», это те в которых вся игровая логика содержится внутри объектов, которые можно передать в другие бинарные модули. (Прим — .dll библиотеки)
  • «Статические движки», это те в которых вся логика содержится внутри самого движка и определяется на этапе компиляции.

Какие нюансы я обнаружил при такой классификации?
«Статические движки»

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

«Динамические движки»

  • Требуют глубокого понимания матчасти, из-за виртуализации объектов.
  • Код можно разбить на независимые модули и подключать буквально на лету. В некоторых играх есть моды которые реализуют «движок в движке», которые используют другие подключаемые модули.
  • Большинство инструментов для отладки придётся писать самому, если это не встроено в сам язык. То есть, если ты не пишешь на .NET/JAVA/Lua?
  • «Скрипты» могут быть реализованы в виде скомпилированных бинарников обёрнутого в объекты, от чего «потанцевал» при добавлении контента может быть больше (в плане производительности).

Большинство «индюшачьих» игровых движков описаны как «статические». Их гораздо проще писать и ещё проще поддерживать. Использование «динамического» подхода, даст тебе ненужную модульность. Возможно написание своего крутого интерфейса для аллокатора памяти хорошая затея, но ты бы мог потратить это время делая игру, а не натирать цепь разобранного велосипеда.
*** Рекомендация — пиши «статику»
*** Самый наивный способ сделать «динамику» — писать классы с виртуальными методами или их аналогами.

Кроссплатформенность

Это боль. (вся глава моё нытьё. )
Первая мысль которая должна посетить твою голову — как распространить игру на максимально широкую аудиторию.
Ты можешь использовать кроссплатформенные библиотеки, но их функционал крайне ограничен, так как они пытаются создать абстракцию которая «будет жить на любой платформе». Ты конечно можете написать свою библиотеку. Но вот в чомъ мем
Проблема в том, что платформы достаточно сильно отличаются даже в самых базовых вещах. Как пример — Windows при старте программы не имеет потоков ввода-вывода и для этого ей нужно создавать консоль, но консоль можно создать 2 различными способами и у каждого способа есть куча параметров, для лююююбых потребностей. В Linux же всё проще, у тебя со старта приложения есть потоки ввода-вывода и что бы «увидеть их» нужно просто перенаправить их в файл или создать окно-терминал. КАК ты даже ТАКУЮ простую проблему будешь решать, я не представляю. (я это уже сделал)
Чего уже говорить о создании графического контекста.

Вот ты пытался в OpenGL на Windows? Знаешь что на официальном сайте написано использовать GetDC() при связывании контекста окна и OpenGL? А ты знал что это DC на самом деле может быть принтером? Может быть целым монитором? Видеокартой? Просто окном? «Ресурсом менеджера окон»? А то что в Linux ты должен ручками выбрать нужный тебе монитор для запуска? Всякие драйверы-сервера запускать? И не один. И это надо как-то собрать в одну единую абстракцию и непринуждённо дёргать по воле случая.

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

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

Делаем игры, наивно

В начале статьи я ввёл понятие «компонента». На самом деле я называю так любую штуку «которая делает что-то другое» )))

Любой код можно разбить на некое число компонент-модулей. Самое примитивное приложение состоит всего из 3 модулей

  • Обработка ввода
  • Вывод изображения
  • Логика (зачастую на стейт машинах)

Уже это позволяет писать тебе довольно простые игры. Скажем крестики нолики или «давилку мух»? Давай вместе представим как оно может работать.

  • Загружаем нужные ресурсы
  • — В цикле.
  • Рисуем: фон > картиночки
  • Обрабатываем ввод
    — Если нажата мышка, проверяем хитбоксы объектов
    — Если прошли проверку, меняем «игровое состояние»
  • (для «давилки мух») Раз в Х циклов запускаем новую муху по некоторой траектории. И обрабатываем перемещение живых мух.

И это уже можно назвать игрой. «Погоди» — скажешь ты — «Но фактически тут 4 модуля, загрузка ресурсов!!». Хах, но если ты чуть-чуть разбираешься в линковке приложения, то ты можешь вшить данные в бинарник) И тебе не нужно будет загружать никаких данных. Поведение мух, также, вшито в движок и относится к логике, потому фактически тут именно 3 модуля.

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

Я бы сказал так — «движок занимается обработкой пользовательских данных и может иметь изменяемую логику»

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

  • Конкретная загрузка ресурсов
  • Обработка ввода, который конвертируется в абстракции
  • Вывод изображения, который обходит множество объектов
  • Игровая логика, которая уже разбита на несколько частей
    — Обработка загрузки-переходов уровней
    — Информация о сцене
    — Нахождение коллизий и проверки на триггеры
    — Обработка поведения ИИ?
    — Обработка ввода от игрока
    — Скриптовый процессор

Теперь, как прошлый раз, давай разберём поэтапно, что и как движок должен делать.
— Для начала опишем «минимальную» логику.

  • Информация о сцене (уровне)
    — Какой фон
    — Набор тайлсетов (2D матрица)
    — Предметы
    — Юниты
    *** Всё кроме фона должно иметь информацию о себе, типа — «текстура», «какие-то свойства», есть ли «триггер», «скрит» или «поведение»
  • Вшитые в движок «примитивы»
    — Триггеры (какие бывают, что проверяют, что вызывают. прим — если хибоксы пересекаются — запускает смену уровня)
    — Поведение (хардкодное поведение, что делает объект. прим — триггернутый предмет увеличивает свойство Х, у объекта который триггернул. Объект Х движется в одну сторону и случайное время меняет направление. Это можно оформить в виде «функций» для скриптового языка. )
    — Свойства (Что за свойства, определяемые скриптами свойства)
    — Процессор скриптов
  • Обработка передвижений-триггеров-физики?

Ну и сам игровой цикл будет состоять примерно из такой логики

  • Загружаем информацию об уровне (описание уровня)
  • Загружаем нужные ресурсы (текстуры и скрипты)
  • — В цикле.
  • Рисуем: фон > тайлест > предметы > юниты
  • Обрабатываем ввод
    — Абстрагируемся от кнопок и просто передаём «Идти влево»
  • Обрабатываем логику
    — Проходимся по всем единицам со скриптами
    — Проходимся по тем, у кого базовое поведение
    — Что-то делаем с игроком?
    — Проверяем коллизии-физику? (вызываем триггеры если что-то нашли)
    — Двигаем юнитов-предметы

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

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

Поясню за скрипты — для многих это будет открытием, но скрипты можно делать и в виде виртуальной машины-процессора. Твоя задача будет — написать парсер текста, описать виртуальную машину и придумать как именно получать информацию о сцене-объектах. В качестве примера, можно использовать многострадальные стековые процессоры, по типу forth. Они очень легко делаются, но к их логике нужно привыкать. По сути это единственный выход написать «быстро» ваш собственный «скриптовый процессор».
*** Обязательно сделай вывод логов при сборке скрипта, или при его работе.
*** Старайся избегать логики типа [INT a += «Lord»]. Писать нетипизрованный код опасно, но можно выделить отдельные команды для работы с конкретными типами.
*** ДА, ТЫ БУДЕШЬ ПИСАТЬ СКРИПТЫ НА АССЕМБЛЕРЕ.
*** Для написания простого ассемблера, хватит и знаний типа — Sting.indexof(«ADD»); и подобного говнокода. Но что бы написать нормально, или хотя бы простенький язык, вам нужны знания о «регулярных выражениях» или «парсерных комбинаторах».
*** Не надо упарываться в «полноценный язык», посмотрите как писались языки программирования в бородатых 80х, даже тот же Pascal. Они работают просто и честно, такие реализации займут у вас в разы меньше времени, чем описание «очередного» . C\Rust\Haskell.

Типа канец?

В целом написать некий «движок в вакууме» не такая сложная задача, как кажется. На ранних этапах большинство поведений-абстракций можно вшивать в движок. Да и в целом, большинство вещей работают довольно просто, и требуют от вас лишь знаний и понимания. Но я напомню, написать свой движок для игры — плохо. Это отнимает огромное количество времени, которое вы можете потенциально потратить на разработку самой игры. А любая гордость проходит, после осознания того, что ты делал свою игру примерно 20% времени пока писал код.

Изначально, решился написать эту статью, ради привлечения инвестиций, на время разработки своей «базовой +18 новеллы». (инициатива друга)
Так что ты это, кнопочку то нажми, а?

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

  • Как лучше работать с памятью-данными.
  • Проблемы STL библиотек. (C++)
  • Детали виртуализации объектов, микро рекомендации.
  • Как можно делать «моды».
  • Профилирование и Логирование, почему это важно.
  • Что нужно знать о многопотоке, вводные-подводные.
  • Асинхронно или параллельно? Как это работает?
  • Работа с текстом-кодировками, и почему это настоящий ад.

На счёт моего кода — не будет опенсорса. Когда я релизнусь, я выложу лишь API к бинарнику. По сути, я использую «динамический» подход, который описывал выше. Потому запустить свою игру, можно будет просто собрав .dll и запустить бинарник запихнув либу в аргументы строки. Но вот когда это случится. Когда я перестану морить себя голодом? Кто знает

PS — Под конец получилось немного сумбурно, ибо я писал всё за один заход. В целом я описал лишь поверхностно многие вещи. Если будет спрос, могу углубится.
PPS — Мне тут говорят что бы я сделал бусти и публиковался впредь там, раз в месяцок публикуя «фри контент». Может в следующий раз? Я просто не знаю о чём там можно писать, лол.
PPPS — «Статья слишкам длинная устал читать, пиши кароче»

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

Моя игра Speebot выпущена в Steam! Бесплатная demo версия прилагается.

Я разрабатывал эту игру с января 2016 года в своё свободное время в одиночку. Мною выполнено всё программирование, дизайн игрового процесса, создание графики и музыки. Кроме того, я написал собственный игровой движок с нуля.

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

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

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

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

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

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

Ну и наконец: написание собственного движка — это очень интересно.

Разработка движка началась в январе 2016 года. Я назвал его YUME («мечта» по-японски). Он написан на Haxe, C++ и OpenGL. Я использую модифицированную версию библиотеки Lime, которая включает в себя несколько полезных функций, например, позволяет загружать ресурсы и открывает доступ к OpenGL.

Вот так движок выглядел после первой недели разработки.

Вот так движок выглядел после первой недели разработки.

До начала создания движка я почти ничего не знал о разработке 3D игр. Нужно было разбираться в OpenGL читая документацию, форумы и уроки, предназначенные для других языков (Java и C++). Через пару месяцев у меня получился довольно стабильный 3D-визуализатор.

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

В конце концов, получилась 3D библиотека, которую можно использовать для чего-то конкретного. Многое из того, что я перечислил, присутствует и в других движках, но в YUME есть несколько отличий. Одно из них — система временных шагов.

Система временных шагов гарантирует, что движок обрабатывает и показывает кадры игры с определённым интервалом. В YUME нет ограничения по количеству кадров в секунду, т.е. нет привязанности к 30 или 60 кадрам в секунду. Частота кадров может быть любая, а игра всё равно будет работать с одной и той же скоростью, потому что частота обновления логики не связана с частотой обновления экрана. На компьютерах разных мощностей может быть разная производительность, и время для показа одного кадра может быть любым. А логика всегда привязана к частоте 62.5 циклов в секунду (16 миллисекунд на каждый цикл). В этом основной принцип системы временных шагов, хотя есть некоторые крайние случаи.

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

Ранняя версия YUME.

Ранняя версия YUME.

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

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

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

Уровни можно редактировать и тестировать сразу. Для продуктивности — то что нужно.

Я написал простую систему игровой физики и начал экспериментировать с игровым процессом. Через несколько недель был готов первый прототип игры, в котором игрок мог перемещать цилиндр по 3D уровню.

Прототип Speebot в июле 2016 года.

Прототип Speebot в июле 2016 года.

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

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

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

В конце 2016 года я сделал паузу от разработки игры, и посвятил несколько месяцев изучению и практике написания музыки. У меня нет музыкального образования, но это — моё хобби. У меня уже был небольшой опыт в композиции музыки для моей предыдущей игры Hypnorain, но я хотел улучшить свои навыки, поэтому снова погрузился в изучение музыкальной теории. Для композиции музыки я использовал программу SunVox, которая является виртуальным модульным синтезатором. В финальной версии игры Speebot всего 23 трека, которые вместе составляют более часа оригинальной музыки. Я удовлетворён результатом, и продолжу улучшать свои навыки для следующей игры.

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

После выпуска нескольких демо версий, обработки отзывов от игроков и улучшения игры, Speebot был выпущен в октябре 2017 года. В финальной версии игры 200 уровней, 4 мира, редактор пользовательских уровней, несколько дополнительных режимов игры, и много другого дополнительного контента.

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

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

Следующая статья

Hypnorain, Speebot со скидкой! Прогресс по движку YUME 2

15 Января 2018

Подписаться

Получайте уведомления о новых статьях, чтобы:

Следить за процессом разработки моей следующей игры.

Читать статьи об искусстве и технологиях создания игр и движков.

Получать новости об изменениях в моих играх.

Подписка бесплатная, просто нажмите на кнопку!

О себе

Я — Кирилл Полетаев, ака KEYREAL, индивидуальный разработчик компьютерных игр.

Этот сайт посвящён моим проектам.

Подписаться

Получайте уведомления о новых статьях, чтобы:

Следить за процессом разработки моей следующей игры.

Читать статьи об искусстве и технологиях создания игр и движков.

Получать новости об изменениях в моих играх.

Подписка бесплатная, просто нажмите на кнопку!

Избранное

  • Игра Citadelic теперь доступна
  • Модификация базы для разных стилей игры в Citadelic
  • Добавление разнообразия с помощью игровых модификаторов
  • Генерация растровых шрифтов в играх
  • Как я создаю игры на своём 3D движке
  • Почему я использую собственный движок 3D игр
  • Создание собственного сценарного языка для моего игрового движка
  • Как я создал собственный 3D движок и игру на нём за 20 месяцев

Написать игровой движок на первом курсе: легко! (ну почти)

Привет! Меня зовут Глеб Марьин, я учусь на первом курсе бакалавриата «Прикладная математика и информатика» в Питерской Вышке. Во втором семестре все первокурсники нашей программы делают командные проекты по С++. Мы с моими партнерами по команде решили написать игровой движок.

О том, что у нас получается, читайте под катом.

Всего нас в команде трое: я, Алексей Лучинин и Илья Онофрийчук. Никто из нас не является экспертом в разработке игр, а тем более в создании игровых движков. Для нас это первый большой проект: до него мы выполняли только домашние задания и лабораторные работы, так что едва ли профессионалы в области компьютерной графики найдут здесь новую для себя информацию. Мы будем рады, если наши идеи помогут тем, кто тоже хочет создать свой движок. Но тема эта сложна и многогранна, и статья ни в коем случае не претендует на полноту специализированной литературы.

Всем остальным, кому интересно узнать о нашей реализации, — приятного чтения!

Графика

Первое окно, мышь и клавиатура

Для создания окон, обработки ввода с мыши и клавиатуры мы выбрали библиотеку SDL2. Это был случайный выбор, но мы о нем пока что не пожалели.

Важно было на самом первом этапе написать удобную обертку над библиотекой, чтобы можно было парой строчек создавать окно, проделывать с ним манипуляции вроде перемещения курсора и входа в полноэкранный режим и обрабатывать события: нажатия клавиш, перемещения курсора. Задача оказалось несложной: мы быстро сделали программу, которая умеет закрывать и открывать окно, а при нажатии на ПКМ выводить «Hello, World!».

Тут появился главный игровой цикл:

Event ev; bool running = true; while (running): ev = pullEvent(); for handler in handlers[ev.type]: handler.handleEvent(ev);

К каждому событию привязаны обработчики — handlers , например, handlers[QUIT] = . Их задача — обрабатывать соответствующее событие. QuitHandler в примере будет выставлять running = false , тем самым останавливая игру.

Hello World

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

glBegin(GL_QUADS); glVertex2f(-1.0f, 1.0f); glVertex2f(1.0f, 1.0f); glVertex2f(1.0f, -1.0f); glVertex2f(-1.0f, -1.0f); glEnd();

Затем мы научились рисовать двумерный многоугольник и вынесли фигуры в отдельный класс GraphicalObject2d , который умеет поворачиваться с помощью glRotate , перемещаться с glTranslate и растягиваться с glScale . Цвет мы задаем по четырем каналам, используя glColor4f(r, g, b, a) .

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

Камера

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

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

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

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

Model = Translate * Scale * Rotate. 

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

 glm::mat4 View = glm::lookAt(cameraPosition, objectPosition, up);

Дословно: посмотри на objectPosition с позиции cameraPosition , причем направление вверх — это «up». Зачем нужно это направление? Представьте, что вы фотографируете чайник. Вы направляете на него камеру и располагаете чайник в кадре. В этот момент вы можете точно сказать, где у кадра верх (скорее всего там, где у чайника крышка). Программа не может за нас додумать, как расположить кадр, и именно поэтому нужно указывать вектор «up».

Мы получили координаты в базисе камеры, осталось спроецировать полученные координаты на плоскость камеры. Этим занимается матрица Projection , которая создает эффект уменьшения объекта при его отдалении от нас.

Чтобы получить координаты вершины на экране, нужно перемножить вектор на матрицу по крайней мере пять раз. Все матрицы имеют размер 4 на 4, так что придется проделать довольно много операций умножения. Мы не хотим нагружать ядра процессора большим количеством простых задач. Для этого лучше подойдет видеокарта, у которой есть необходимые ресурсы. Значит, нужно написать шейдер: небольшую инструкцию для видеокарты. В OpenGL есть специальный шейдерный язык GLSL, похожий на C, который поможет нам это сделать. Не будем вдаваться в подробности написания шейдера, лучше наконец-то посмотрим на то, что вышло:

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

Физика

Какая же игра без физики? Для обработки физического взаимодействия мы решили использовать библиотеку Box2d и создали класс WorldObject2d , который наследовался от GraphicalObject2d . К сожалению, не получилось использовать Box2d «из коробки», поэтому отважный Илья написал обертку к b2Body и всем физическим соединениям, которые есть в этой библиотеке.

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

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

Текстуры

Для загрузки изображений мы использовали библиотеку DevIL. Каждому GraphicalObject2d стал соответствовать один экземпляр класса GraphicalPolygon — лицевая часть объекта — и GraphicalEdge — боковая часть. На каждую можно натянуть свою текстуру. Первый результат:

Все основное, что требуется от графики, уже готово: отрисовка, один источник освещения и текстуры. Графика — на данном этапе все.

Машина состояний, задание поведений объектов

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

Объекты, которые умеют обновляться, наследуются от созданного нами класса Behavior. У него есть функции onStart, onActive, onStop , которые позволяют переопределить поведение наследника при запуске, при жизни и при завершении его активности. Теперь нужно создать верховный объект Activity , который бы вызывал эти функции от всех объектов. Функция loop, которая это делает, выглядит следующим образом:

void loop(): onAwake(); awake = true; while (awake): onStart(); running = true while (running): onActive(); onStop(); onDestroy();

Пока running == true , кто-нибудь может вызвать функцию pause() , которая сделает running = false . Если же кто-то вызовет kill() , то awake и running обратятся в false , и активность остановится полностью.

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

Решение: у каждого Behavior пусть будет массив subBehaviors , которые он будет обновлять, то есть:

void onStart(): onStart() // не забыть стартовать себя самого for sb in subBehaviors: sb.onStart() // а только после этого все дочерние Behavior void onActive(): onActive() for sb in subBehaviors: sb.onActive()

И так далее, для каждой функции.

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

bool isTransitionActivated(): // для idle_walk->attack return canSee(enemy);

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

У состояния есть три стадии: оно началось, оно тикает, оно остановлено. К каждой из стадий можно добавлять какие-то действия, например, прикрепить к объекту текстуру, применить к нему импульс, установить скорость и так далее.

Сохранения

Создавая уровень в редакторе, хочется иметь возможность сохранить его, а сама игра должна уметь загружать уровень из сохраненных данных. Поэтому все объекты, которые нужно сохранять, наследуются от класса NamedStoredObject . Он хранит строку с именем, названием класса и обладает функцией dump() , которая сбрасывает данные об объекте в строку.

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

На самом деле, игра и редактор — это почти один и тот же класс, только в игре уровень загружается в режиме чтения, а в редакторе — в режиме записи. Для записи и чтения объектов из json-а движок использует библиотеку rapidjson.

Графический интерфейс

В какой-то момент перед нами встал вопрос: пусть уже написана графика, машина состояний и все прочее. Как пользователь сможет написать игру, используя это?

В первоначальном варианте ему пришлось бы отнаследоваться от Game2d и переопределить onActive , а в полях класса создавать объекты. Но во время создания он не может видеть того, что создает, и нужно было бы еще и скомпилировать его программу и прилинковать к нашей библиотеке. Ужас! Были бы и плюсы — можно было бы задавать столь сложные поведения, на какие только хватило бы фантазии: например, передвинуть блок земли на столько, сколько жизней у игрока, и делать это при условии, что Уран в созвездии Тельца, а курс евро не превышает 40 рублей. Однако мы все-таки решили сделать графический интерфейс.

В графическом интерфейсе количество действий, которые можно произвести с объектом, будет ограничено: перелистнуть слайд анимации, применить силу, установить определенную скорость и так далее. Та же ситуация с переходами в машине состояний. В больших движках проблему ограниченного количества действий решают связыванием текущей программы с другой — например, в Unity и Godot используется связывание с C#. Уже из этого скрипта можно будет сделать что угодно: и посмотреть, в каком созвездии Уран, и какой сейчас курс евро. У нас такой функциональности на данный момент нет, но в наши планы входит связать движок с Python 3.

Для реализации графического интерфейса мы решили использовать Dear ImGui, потому что она очень маленькая (по сравнению с широко известным Qt) и писать на ней очень просто. ImGui — парадигма создания графического интерфейса. В ней каждую итерацию главного цикла все виджеты и окна отрисовываются заново только если это нужно. С одной стороны, это уменьшает объем потребляемой памяти, но с другой, скорее всего, занимает больше времени, чем одно выполнение сложной функции создания и сохранение нужной информации для последующего рисования. Тут уже осталось только реализовать интерфейсы для создания и редактирования.

Вот как в момент выхода статьи выглядит графический интерфейс:

Редактор уровня

Редактор машины состояний

Заключение

Мы создали только основу, на которую можно вешать что-то более интересное. Иными словами, есть куда расти: можно реализовать отрисовку теней, возможность создания более чем одного источника освещения, можно связать движок с интерпретатором Python 3, чтобы писать скрипты для игры. Хотелось бы доработать интерфейс: сделать его красивее, добавить больше различных объектов, поддержку горячих клавиш…

Работы еще предстоит много, но мы довольны тем, что имеем на данный момент.

За время создания проекта мы получили много разнообразного опыта: работы с графикой, создания графических интерфейсов, работы с json файлами, обертки многочисленных C библиотек. А еще опыт написания первого большого проекта в команде. Надеемся, что нам удалось рассказать о нем так же интересно, как было интересно им заниматься 🙂

  • вшэ
  • hse
  • питерская вышка
  • обучение программированию
  • прикладная математика и информатика
  • бакалавриат
  • c++
  • проект
  • игровой движок
  • Блог компании Питерская Вышка
  • Программирование
  • C++
  • Учебный процесс в IT
  • Дизайн игр

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

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