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

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

  • автор:

How I made a game engine from scratch?

Twarit Waikar

The text below was originally posted on SDSLabs’ blog site which is a group of college students that like to make digital products. I should mention that I was a complete beginner when I started. I only had a handful of gamedev experiences before starting and I built away from that. Also, I will be referring to SDSLabs whenever I say ‘we’ or ‘us’.

Today we are proud to announce ‘Rubeus’! Rubeus is an open-source 2D game engine written purely in C++17 and is designed with a vision to inculcate the spirit of game development amongst the general public (specifically the IIT Roorkee junta). You can check out Rubeus at https://github.com/sdslabs/Rubeus.

What is a game engine? How is it different from a game?

A game engine, in its most mature form, is a platform that provides tools and utilities to game developers for designing and developing games based on their ideas.

To provide some comparisons, some of the most well-received games in the market have been Ubisoft games from the likes of the Assassin’s Creed franchise and the Watch_Dogs franchise. These games look and feel very similar to each other as both of these franchises have a stealth mechanic deeply rooted at their heart.

The stealth mode mechanic in Watch_Dogs 1 takes a whopping 100,000 lines of code in the game. It is not only economically impossible to regenerate and reintegrate that much amount of code for each and every game that gets released under the franchise, but would also take up a lot of the developers’ time. This is why Ubisoft has engraved the stealth mechanic in their game engine and they keep reusing it in their newer games with appropriate modifications.

Why make an entire game engine and not a game?

In reality, the development of most games at game studios starts with developing a game engine first. Creating a game without a game engine can be considered as hard-coding functionalities in a crude form. This means that if you happen to work on another project (perhaps a sequel of the previous game), you will have to again work on laying down the basic layer of functionalities that all types of games work on. It is likely that whenever you start working on a new project, a significant part of your code will be repeated every time. This practice of writing the same code every time you work on a new game is incredibly inefficient and this is a problem that we, at SDSLabs, intend to solve.

Making a game engine is a complex task and this is why most casual developers overlook the possibility of using a game engine to realize their game ideas. However, there are a lot of free alternatives out in the market. For example, you might have heard about Fortnite and the latest Tekken 7, both of which run on Epic Games Studio’s Unreal Engine 4. It has, in fact, made it possible for the Tekken franchise to finally hit the PC market. There are plenty of game engines out there but we decided to put ourselves up to the challenge of creating one for ourselves and releasing it to the public for everyone to use.

How to even start with making a game engine?

The starting days of Rubeus in the month of May 2018 were full of reading sessions. The best websites to look up information on topics that we found were related to game development are probably Gamedev.net and Gamasutra. We also recommend following r/gamedev on Reddit, which is a booming community of game developers from both indie and AAA studios.

The beginning of something amazing

One of the first hurdles that we faced was how we should implement the Rubeus engine’s architecture. We had zero levels of abstraction in our codebase and we were trying to create an API out of it that should be useful to even a newbie. This made us take a step back and we started to work on the individual modules of the engine rather than the API.

Any game engine works on a concept of what is known as the ‘Game Loop’. In the most general sense, games are recurrent programs. They do not normally shut down after their execution is complete. Instead, they often keep on repeating a particular set of instructions over and over unless the game shuts itself down or when the player has pressed the QUIT button. This is the concept of an ‘Application Loop’.

Now, let us see how game engines modify this concept so as to suit most closely to an actual game. A general game engine loop looks a bit like this:

The engine update function above consists mainly of the physics engine update and the calling all the tick functions.

A tick function is some user-defined logic that the user wants to run at every frame in the game. It may contain all of the game logic or just a frame counter to measure the FPS. Coming to the physics engine update, it is also just a function that gets called once every frame but it checks for collisions every frame, and if it finds any collisions happening then it defines what change of velocities of the colliding game objects take place in that frame.

The most important parts of making a game engine are implementing the physics update function and the render function (also known as the drawing step). These two functions are implemented independently inside the physics engine and the rendering engine. The render function just renders the scene. It applies no game logic to the scene. All logic is covered inside the update functions. Render functions tend to take care of what visual effects the user requires at a certain moment in the game.

A typical single-threaded game engine update loop should look like this:

Taking all these bits of information together, we started building Rubeus function by function. We first listed down the basic components of a game engine. To give a gist of how and what we started to work on, below is some documentation on our process of building a game engine.

Graphics Components:

A window module that talks to the OS and generates a window which allows OpenGL drawings to be rendered on the screen

This had to be done in an OS-independent way because to keep the engine cross-platform. One such library that eases this task is GLFW(“OpenGL Framework”). GLFW makes the task of drawing to the screen independent of the OS using OpenGL completely hassle-free.

A rendering module that abstracts all objects appearing in the game with the specific image/color that they use to get rendered to the screen

It is also the renderer’s job to use the specific shaders that allow the objects to get colored in a way that the shader governs. In a nutshell, shaders are bits of code (written in OpenGL Shading Language a.k.a. GLSL, in our case) that tell the GPU where each vertex is present in the 3D space and what sort of algorithms should be used to color every pixel along with what sort of effects should be applied to the rendered image.

For example, 3D games nowadays have started using a ‘Bloom’ effect to highlight bright objects on the screen.

Bloom effect is a lighting illusion that is used to make objects appear brighter than the maximum brightness of the monitor.

This particular effect i.e. Bloom effect is implemented inside shaders and the renderer uses these shaders to output graphics on the screen.

During the development of Rubeus’ renderer, that we proudly named the ‘Guerrilla Renderer’, we ran a benchmark at rendering 14,560 sprites (a.k.a. 2D objects) at 450 FPS on a GTX 1060 (6GB). We tried to crank the numbers higher and somehow managed to choke our own engine by displaying 122,300 sprites at 4 FPS. A quick reminder: No real-life game ever reaches these numbers of objects being displayed at a time. We had also tested Guerrilla only in Debug mode without any form of inlining of C++ code.

This is a test run from another benchmark that we ran.

Multithreading:

We were aware of the fact that even if we may not ever make Rubeus multithreaded in the first place, we may require the need to perform asynchronous responses such as implementing a console, or a debug menu for the user that use Rubeus to make a game for their players.

Multithreaded programs are exactly what they sound like. They are able to follow more than 1 flow of execution of code at a certain moment in time. Beware that such systems can be incredibly hard to build because improper sharing of resources and also just overuse of threading will also give a negative hit to the performance of the engine.

We have implemented a multithreaded messaging system inside Rubeus that we plan to release in v2.0. More information on this type of architecture can be found in this wonderful article about making different types of game engine architectures.

By the time we were done with the multithreading architecture and the Guerrilla renderer, it was already mid-July and we had started to realize that this project might take a while to get completed. Not because of any lack of development times but the sheer size of this project. It was about time we started to really speed things up or Rubeus would be seeing the light of the day not before 2019 or maybe even not at all.

A Physics Engine:

Rubeus’ physics engine, nicknamed ‘Awerere’ (pronounced as “auror”) is also what it sounds like. Awerere is a physics engine that works inside Rubeus and allows simulation of life-like collisions and physics of game objects.

The physics engine is an essential part of bringing any form of realism in a game. It is responsible for figuring out what objects are colliding with each other, what objects are not colliding with each other and if they are colliding, what velocities do they move away with and if they have collided, should they repel like rigid bodies or do they release some form of energy (likes of what we see in inelastic collisions amongst rigid bodies).

All these questions are what the physics engine has to find the answers for. Sometimes the user can define some customized response to collisions. For example, the user would like to open a door to another doorway if the player shoots a particular switch on the wall.

Currently, Awerere supports shapes such as boxes, circles, and planes. It handles collisions amongst all of the permutations of these objects and assigns them their final physical state after the collision. Designing Awerere and implementing the different collision algorithms was a treat because we personally like studying and realizing rigid body physics with the help of real-world physical laws.

Inputs and Sounds Manager:

In this part of the development cycle, we were slowly approaching the release date and we had already implemented and debugged the hard parts of implementing a physics engine.

We have used the popular ‘Simple and Fast Multimedia Library’, often known as just ‘SFML’ to provide Rubeus with cross-platform access to the sound devices in an organized sound manager. Rubeus now supports loading both long audio tracks like ambient and background music in addition to short pieces of audio like footsteps, gunshots, etcetera.

When we had selected GLFW for generating windows on all OSs, we also had to keep a note of how GLFW handled getting inputs from the input devices. It also provides keyboard presses along with mouse button presses, scrolling, and just plain cursor positions. But all of this is done in an asynchronous response that GLFW provides the engine. We have implemented a system that keeps track of what keys have been pressed and what keys have been released at the starting of every frame. This made up the base of creating the Input manager which we further abstracted to work by creating keybindings for each control. Multiple keys are now assignable to a single keybinding/control.

Bringing everything together

Near the end of November we were ready with all subsystems and modules that were required to make up Rubeus. But we still did not have an API or a gateway that the user could interact with.

This is when we came up with broCLI, which stands for ‘Rubeus on Command Line Interface’. broCLI is a CLI tool implemented in Golang which helps create a project structure for Rubeus. It was the perfect idea for Rubeus to use a CLI tool instead of a GUI so that users get a feeling of doing some heavy work while working with Rubeus. The user persona for Rubeus was always a beginner programmer from the start of May 2018. A CLI is probably something that they will be seeing a lot in their coming days.

Rubeus may have a GUI later but for v1.0, we are focusing only on a CLI.

Fast forward to this day

We are happy that we were able to implement such a complex piece of technology with elegance and now, we invite others to partake in this endeavor into the realms of game development.

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

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

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

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

Поясню — что бы написать минимально рабочую игру, тебе нужно: обработка ввода, вывод графики и игровой цикл с логикой. Можно сказать — «всего 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 — «Статья слишкам длинная устал читать, пиши кароче»

Создание игрового движка с нуля на 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 на экране, насладитесь этим моментом и убедитесь, что вы понимаете, что делаете. Если вам удалось успешно обнаружить столкновение двух тел, наслаждайтесь этим моментом и поразмыслите над только что полученными знаниями.

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

Как создать графический движок с нуля на Python

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

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

Код

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

Модуль дисплея

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

Чтобы справиться со всем этим, мы создали модуль display в нашем проекте, где находятся классы Screen и Bitmap . Screen обрабатывает задачи окна, настраивает и отключает, а также отвечает на вводимые пользователем данные. Bitmap принимает три точки и цвет и рисует соединяющий их треугольник заданного цвета (раньше это был обычный Python с Tkinter, но с тех пор добавили numpy и подушку для ускорения).

Модуль алгебры

Поскольку графический движок по своей сути содержит так много линейной алгебры, мы создали удобный модуль, который имеет несколько классов: Vec3 , Vec2 и Mat3 (матрица 3×3). На самом деле это не более чем оболочки для массива или массива массивов, но они реализуют перегрузку операторов и некоторые вспомогательные методы (подумайте о скалярном произведении, перекрестном произведении и умножении матриц), которые делают остальную часть кода намного чище. .

Модуль двигателя

Решая, как структурировать наш графический движок, я опирался на опыт работы с OpenGL, Apple SceneKit, Unity, Roblox и программу 3D-моделирования Blender. Результат наиболее точно соответствует структуре, которую использует Unity.

Самый фундаментальный объект в нашем движке — это Node . Node имеет три свойства: Mesh , Transform и Shader , а также массив дочерних узлов. Mesh — это объект, который содержит список точек ( Vec3 ) и граней (массив индексов трех точек, которые соединяет это лицо). Transform кодирует, как масштабировать, вращать и перемещать этот узел и все дочерние элементы относительно родительского узла. И Shader в настоящее время просто сохраняет цвет узла, но это в списке улучшений.

От Node идет объект Camera , который кодирует важную информацию о рендеринге сцены. И хотя Camera происходит от Node, он не поддерживает Mesh или Shader — только Transform . Он также имеет несколько других важных свойств: фокусное расстояние, ширину изображения, высоту изображения, ближнюю глубину и большую глубину. Ширина и высота довольно просты, фокусное расстояние мы рассмотрим в линейной алгебре, а ближняя глубина и дальняя глубина — это просто точки отсечки для «слишком близко» и «слишком далеко», в которых мы отбрасываем треугольники от рендеринга. — часть более крупного процесса, называемого отбраковкой.

Объект самого высокого уровня в движке — это Scene . Scene довольно прост — он просто содержит корень Node , дочерним элементом которого должно быть все в сцене, и ссылку на Camera , который следует использовать для рендеринга. У него также есть самый важный метод: render() , который заставляет все отображаться на Bitmap с использованием большого количества линейной алгебры.

Линейная алгебра

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

Чтобы объяснить линейную алгебру, мы рассмотрим один вызов метода render() в нашем Scene объекте. Представим, что наша сцена содержит единственный узел со следующей сеткой:

И чтобы было интересно, почему бы нам не переместить его на (5, 20, 3) , повернуть вокруг оси z на 45 ° и удвоить его размер. Используя стандартную камеру (фокусное расстояние: 2, ширина: 4, высота: 3), мы должны получить следующее изображение:

Шаг 1. Примените преобразование

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

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

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

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

Шаг 2: сортировка по W-индексу

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

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

После того, как все w-индексы вычислены, грани переупорядочиваются в соответствии с убывающим w-индексом и переходят к следующему шагу.

Шаг 3. Рисование в растровое изображение

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

Мы делаем это, отображая каждую вершину из мирового пространства в 2D-пространство, определяемое камерой, которое мы называем экраном. Помогает представить нашу камеру такой:

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

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

Решая для s, получаем:

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

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

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

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

Каэден Уайл — студент Вашингтонского университета, соучредитель Offsite и основатель Kilometer Creative, где он выпустил несколько игр в App Store, включая TileForm и Landr.

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

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