Создаем игровой движок с видом от первого лица за 265 строк кода на JavaScript
В этой статье мы создадим небольшой игровой движок с видом от первого лица без сложной математики и техник 3D-визуализации, используя метод рейкастинга (трассировки, или «бросания», лучей).
Рейкастинг — один из методов рендеринга в компьютерной графике, при котором сцена строится на основе замеров пересечения лучей с визуализируемой поверхностью.
Игрок
Логично, что если мы создаем движок от первого лица, то наш игрок — это и есть точка, из которой будут выходить лучи. Для начала нам понадобится всего три свойства: координата x , координата y и направление:
Карта
Будем хранить карту с помощью двумерного массива. В нем 0 будет обозначать отсутствие стены, а 1 — её наличие. Для нашей реализации такой простой схемы будет достаточно.
Бросаем луч
Фишка в том, что при рейкастинге движок не рисует пространство целиком. Вместо этого он делит его на отдельные колонки и воспроизводит одну за одной. Каждая колонка представляет собой один брошенный под определенным углом луч. Если луч встречает на пути стену, он измеряет расстояние до нее и рисует прямоугольник в колонке. Высота прямоугольника определяется пройденным расстоянием — чем дальше стена, тем короче колонка.

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

Начинаем с того, что находим ближайшую к игроку горизонтальную stepX и вертикальную stepY линию сетки. Перемещаемся к той, что ближе, и проверяем на наличие стены с помощью inspect . Повторяем шаги до тех пор, пока не отследим до конца траекторию каждого луча.
Обнаружить пересечения на сетке легко: нужно просто найти все целочисленные x (1, 2, 3…). А потом найти соответствующие y с помощью умножения x на коэффициент угла наклона rise / run .
Прелесть этой части алгоритма в том, что размер карты не имеет значения. Мы рассматриваем только определенный набор точек на сетке — каждый раз примерно одно и то же количество. В нашем примере размер карты 32×32, но если бы он был 32 000×32 000, скорость загрузки была бы такой же.
Рисуем колонку
После того как мы отследили луч, нам нужно нарисовать все стены, которые встречаются ему на пути.
Мы определяем высоту каждой стены, деля её максимальную высоту на z . Чем дальше стена, тем короче мы её рисуем.
Откуда взялся косинус? Если мы будем использовать «чистое» расстояние от игрока до стены, то в итоге получим эффект «рыбьего глаза». Представьте, что вы стоите лицом к стене. Края стены слева и справа находятся от вас дальше, чем центр стены. Но мы же не хотим, чтобы при отрисовке стена выпирала посередине? Для того, чтобы визуализировать плоскую стену так, как мы её видим в реальной жизни, мы строим треугольник из каждого луча и находим перпендикуляр к стене с помощью косинуса. Вот так:

Слева — расстояние, справа — расстояние, умноженное на косинус угла.
В нашей статье это — самая сложная математика, с которой придется столкнуться
Визуализируем
Используем объект Camera, чтобы отрисовать карту с точки зрения игрока. Объект будет отвечать за визуализацию каждой колонки в процессе движения слева направо.
Прежде чем он отрисует стены, мы зададим skybox — большое изображение для фона с горизонтом и звездами. После того, как закончим со стенами, добавим оружие на передний план.
Самые важные свойства камеры — разрешение, фокусное расстояние и диапазон.
- Разрешение определяет, сколько колонок мы рисуем (сколько лучей бросаем);
- Фокусное расстояние определяет ширину линзы, через которую мы смотрим (углы лучей);
- Диапазон определяет дальность обзора (максимальная длина каждого луча).
Собираем воедино
Используем объект Controls для снятия данных с клавиш-стрелок и сенсорной панели, а также объект GameLoop для вызова requestAnimationFrame . Цикл игры прописываем всего тремя строками:
Детали
Дождь
Дождь симулируем с помощью нескольких очень коротких стен, разбросанных произвольно:
Задаем ширину стены в 1 пиксель.
Освещение и молнии
Освещение — это, вообще-то, работа с тенями: все стены рисуются со 100% яркостью, а потом покрываются черным прямоугольником какой-либо прозрачности. Прозрачность определяется как расстоянием до стены, так и её ориентацией (север / юг / запад / восток).
Для симуляции молний, map.light случайным образом совершает резкий скачок до значения 2 и потом так же быстро гаснет.
Предупреждение столкновений
Для того, чтобы игрок не натыкался на стены, мы просто проверяем его следующую локацию по карте. Координаты x и y проверяем по отдельности, чтобы игрок мог идти вдоль стены:
Текстура стен
Без текстуры стена выглядела бы довольно скучно. Для каждой колонки мы определяем текстуру посредством взятия остатка в точке пересечения луча со стеной.
Например, для пересечения в точке (10, 8,2) остаток равен 0,2. Это значит, что пересечение находится в 20% от левого края стены (8) и 80% от правого края (9) . Поэтому мы умножаем 0,2 на texture.width чтобы найти x -координату для изображения текстуры.
Можно посмотреть результат на сайте автора, а также изучить код на GitHub.
How I made a game engine from scratch?
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.
Как и зачем создавать собственный игровой движок
Игра с нуля — интересный челлендж для разработчика. Но если хотите пройти его на сложности Nightmare, можно еще и сделать собственный игровой движок, заточенный специально под проект. Подводных камней много, рассказываем, что важно знать при разработке такого гейм-дизайнерского софта, и что в него добавить.

Итак, вы задумались о создании собственного игрового движка. Отлично! У этого варианта множество плюсов в сравнении с использованием коммерческого — такого как Unity или Unreal. В этой статье разберемся, зачем разрабатывать свой движок, какие системы необходимо предусмотреть и как правильно подойти к процессу.
Зачем?
Начнем с главного вопроса, который стоит задать себе, если вы решили разработать собственный движок: зачем это вам?
Резонными причинами могут быть, например, такие:
Хотите создать игру с использованием новой технологии, которую не поддерживают другие движки. Или поддерживают, но реализация слишком сложна и костыльна. Это может быть масштабная симуляция (Factorio), нестандартный проект, который не вписывается в готовые шаблоны (Noita, Miegakure), и множество других идей. В таких случаях нет иного выхода кроме как писать собственный движок под проект.
Хотите оптимизировать рабочий процесс под игры, которые создаете. Если для проекта не нужен полный объем возможностей коммерческого движка, есть смысл создать кастомизированный вариант с подходящими для конкретной игры редакторами и функциями. Если движок не затачивается под конкретный проект, стоит задуматься, так ли нужно самописное решение или все-таки достаточно готовых вариантов?
Не хотите в долгосрочной перспективе зависеть от чужих технологий. Если вы хотите иметь полный контроль над проектом, готовы самостоятельно исправлять ошибки (а не сидеть в ожидании багфикса от создателей) и не бояться, что очередное обновление сломает игру, то собственный движок — подходящий вариант! И не придется зависеть от прихотей крупных корпораций
Вам интересно разобраться, как устроены и работают игровые движки. Это отличная причина — по правде говоря, самый веский повод заняться разработкой собственного движка.
Раз уж начали составлять списки, то вот несколько неудачных причин браться за разработку движка. Если найдете свою среди этих пунктов — притормозите и подумайте еще раз.
Вам кажется, что вы придумаете движок покруче, чем Unity или Unreal (или Godot, или GameMaker). Не выйдет. Разработать подходящий для специфических нужд софт можно (см. предыдущий список), но в одиночку или маленькой командой невозможно создать универсальный движок, который будет конкурировать с известным универсальным ПО. Особенно при первой попытке.
Думаете, что иначе вы «ненастоящий программист»? Использование готового движка не делает гейм-разработчика хуже. Для того они и придуманы! Это просто инструмент для создания игр. 99% проектов можно разработать при помощи уже существующего софта — в этом нет ничего постыдного. Ведь главное — это сама игра!
Если вы хотите таким образом сэкономить время или деньги — забудьте! Создавать движок с нуля долго, а время = деньги. Использовать готовый софт выгоднее, чем пытаться разработать собственный. В долгосрочной перспективе это может стать выигрышной стратегией, но только если движок будет основой для нескольких прибыльных проектов, и при этом значительно удобнее в работе, чем коммерческие. Такое ПО разработать сложно, особенно если это первый опыт (и почти невозможно, если речь о 3D).
При принятии решения учитывайте свой опыт и цели. Чем меньше практики в создании игр, тем сложнее окажется разработка движка — обязательно потренируйтесь прежде чем браться за игровое ПО.
Я начинал с флэш-игр в 90-00х, и ни один движок того времени не поддерживал импорт флэш-анимаций. Единственным выходом было создать собственный софт. Намного приятнее и быстрее закидывать swf-файлы в папку с ресурсами и сразу использовать анимации в игре без промежуточных шагов типа экспорта в списки спрайтов.
Конечно,целесообразность создания собственного движка во многом зависит от количества опыта как в геймдеве, так и в программировании. Приятно сделать кастомный софт, не гуглить постоянно туториалы, и самостоятельно дебажить возникающие ошибки. В то же время, достаточно допустить пару оплошностей, и проект развалится, а обратиться за советом будет некуда. Собственный движок — это полный контроль, но и полная ответственность за продукт.
Игровой движок — это рабочая среда, в которой создают игры. Он состоит из базы, на которой строится проект, и деталей, из которых, словно из деталей лего, состоит сама игра. Это золотая середина между «логикой игры» и «скучными техническими штуками»: благодаря движку в игровом коде не приходится вручную прописывать, как отобразить на экране условный треугольник, можно сразу заняться взаимодействием элементов.
Разные движки выполняют за вас разное количество работы. Некоторые просто отображают графику на экране (Flash, Pico-8). Другие сами по себе — целая игра с возможностью кастомизации или узко заточены под определенный жанр (RPGMaker, Ren’Py). А между ними — бесчисленное количество вариантов.
Игровые движки обычно основываются на простых фреймворках типа SDL и OpenGL, и включают в себя специализированные библиотеки для аудио, видео, физических и математических вычислений и чего угодно еще. При создании движка нет необходимости каждую мелочь прописывать вручную, практически на каждую потенциально полезную опцию доступна соответствующая библиотека.
Базовые функции движка.
Это основы, необходимые для того, чтобы начать создавать игры.
Инициализация системы.
Приводит программу в боевую готовность после запуска — открывает окно, загружает данные. С этим (и не только!) справится стандартная библиотека SDL, проще ее и использовать.
Контроль частоты кадров
Ограничивает частоту сменяемости кадров для плавного изображения и оптимизации работы.
Существует много способов реагирования на нажатия кнопок или движения джойстика, и обычно с этим справляется ранее упомянутый SDL. Так что если он используется для инициализации, больше ничего дополнительно ставить не надо. Поверх него можно построить мощную и гибкую систему ввода, но для начала хватит и дефолтной.
Рендеринг
Большинство (ну как минимум 75%) игр так или иначе используют графику, и за нее отвечает как раз движок. В 2D-игре минимальному рендеру достаточно отображать на экране текстурированные четырехугольники. Шейдеры, буферы вершин, однобуферная прорисовка, меши, материалы и так далее — это прекрасные опции, которые можно добавить позднее, если понадобится. Если хочется заморочиться с OpenGL или Vulkan и кастомизировать рендерер — на здоровье! Но помните, нет ничего постыдного в том, чтобы использовать для рендеринга готовые библиотеки типа Ogre3D. Выбор зависит от целей и потребностей разработчика, а также от того, какие задачи интереснее решать самостоятельно.
Математические и прочие утилиты
Желательно, чтобы к этим библиотекам имели доступ как игровой код, так и движок. Плюс — ко всем иным полезным функциям и формулам, которые вы найдете в процессе разработки. STB — отличный ресурс для поиска всевозможных утилит, которые могут пригодиться при создании движка.
Дополнительные функции
Еще несколько систем, которые лучше добавить в движок ближе к делу, когда они непосредственно понадобятся для игры:
Управление игровыми объектами и сценами
Можно кодить и вручную, но практичнее предусмотреть систему для обработки отдельных игровых объектов и коллекций. Это один из ключевых механизмов в движке, ведь он управляет логикой игры. Из каких компонентов состоят объекты, на какие типы событий реагируют, как происходит взаимодействие, что со структурой памяти, используется ECS? (Кстати, «чистый» неадаптированный ECS лучше применять только для специфических кейсов.) Эти и не только вопросы должна покрывать система управления объектами и сценами. Для таких задач доступны готовые библиотеки (особенно для чистого ECS), но, поскольку эта структура сильнее остальных влияет на игровой код, я склоняюсь к принципу «сделай сам». Использование существующего решения вынудит постоянно думать о том, как вписать логику игры в рамки. А надо наоборот — адаптировать движок под выражение задуманной игровой логики.
Аудио
Звуковые эффекты и музыка здесь разделены, хотя и прячутся под одним названием. Основные необходимые функции — это запуск и остановка звуковых циклов и воспроизведение звуковых эффектов от начала до конца. Этим аудио не ограничивается, но даже с двумя базовыми опциями можно далеко продвинуться. Минус в том, что стандартные звуковые фреймворки (FMod and Wwise) — коммерческие и с кучей лицензионных ограничений. Однако большинство ресурсов с открытым кодом раздражают неудобством (передаю привет OpenAL). Сам я использую FAudio — на мой вкус, простая и комфортная в использовании база для построения сложных звуковых механик.
Загрузка и управление файлами
Файлы загружают все игры. Вряд ли вам захочется вручную повторно загружать и декодировать уже добавленные файлы, так что понадобится система, которая этим займется. В будущем загрузчику файлов можно добавить и другие функции — например, поддержку модов или динамическую выгрузку. Это не срочно — поначалу можно использовать встроенный менеджер, но со временем файлов станет так много, что понадобится удобная система управления файлами и ресурсами.
Нетворкинг
Окей, нетворкинг (онлайн-мультиплеер) — это ОЧЕНЬ опционально. Если не планируется режим p2p, то и не заморачивайтесь. Однако эту систему чрезвычайно сложно встроить в движок, который разработан не с расчетом на многопользовательские игры. Поэтому, если вы планируете или допускаете добавление мультиплеера, подготовьте почву заранее, потому что иначе придется переделывать все системы.
Это базовый набор систем, которые входят в игровой движок. Другие варианты типа обнаружения столкновений, физики, сериализации, анимации и UI уже опциональны. Они распространены, поэтому входят в большинство движков, но для создания игр не обязательны. Например, предотвращение столкновений можно обеспечить при помощи математических утилит и прописать алгоритм в коде игры. Простейшую гравитацию и ускорение можно настроить без физических движков типа Box2D or Bullet. А полная сериализация вообще лишняя, если нужно попросту сохранить чекпойнт.
В самописном движке однозначно будет меньше систем и функций, чем в универсальном коммерческом. Такова цель! Unity и Unreal — огромные монолиты, и каждая отдельная игра использует лишь малую часть предложенных опций. Добавляйте только то, что нужно для вашего конкретного кейса и сосредоточьтесь на том, чтобы сделать инструменты разработки лучше и комфортнее в использовании.
Озанкомьтесь с тем, как работают другие игровые движки, прежде чем браться за собственный. Разберитесь, какие парадигмы и алгоритмы они используют, что реализовано классно, а что раздражает. Попробуйте создать мини-игру на нескольких движках, чтобы понять, как они устроены.
Итак, вы взвесили за и против, поняли, чего хотите, и решили все-таки взяться за создание движка. И как же это сделать?
Сразу к делу: делайте игру параллельно с разработкой движка. Это правило нельзя нарушать. Изучите основы как можно скорее и сразу же начинайте создавать на этой базе игру. Движок — ничто без игры.
Это необходимо, потому что функционал движка должен соответствовать потребностям сделанных на нем игр. Нельзя понять, как построить хорошую анимационную систему, если проект не требует сложной анимации. Слабые места движка проявляются в процессе написания игры. Может быть, нужна древовидная система, благодаря которой дальние объекты не будут рендериться, пока не приблизятся на определенное расстояние? Я не знаю, и вы не узнаете, пока не соберете игровой уровень, который будет страшно зависать. И даже тогда проблема может оказаться не в обновлении объектов — чтобы понять, надо проверить.
Не программируйте того, что не нужно. Если единственный UI в игре — кнопка Play в главном меню, поздравляю! Не придется создавать мудреный пользовательский интерфейс. В The End Is Nigh нет ни физического движка, ни детектора столкновений. Там даже нет камеры, потому что она там не нужна. Я использовал электронную таблицу .csv, чтобы собрать карту мира, вместо всяких сложных редакторов. Делается легко и нормально работает.
Не буду вдаваться в подробности реализации — способов слишком много, каждый подходит для определенных случаев. Нет «наилучшего варианта рендеринга» или «самого правильного способа управления объектами». Все зависит от игры. Начинайте с основ и расширяйтесь по мере необходимости.
Что касается языков программирования — выбирайте, каким лучше владеете. Разработка движка — сама по себе непроста, а если делать это параллельно с изучением С++, обе эти задачи станут в два раза сложнее. C# идеально подойдет для создания движка. Медленнее, чем на С++, но не критично. Более медленный язык типа Python может вызвать затруднения, если в игре много движущихся объектов… но для некоторых игр пойдет. Используйте, что удобно.
И еще — с первой попытки идеально не получится. Моей первой игрой на самописном движке стала Closure, и в ней полный бардак (забавно, что ее номинировали на награду «Техническое совершенство» на фестивале независимых игр в 2010 году). Системы рендеринга и обновления вдвоем обрабатывали всю игру. Добавлять новые объекты было крайне трудоемко, приходилось дописывать кучу кода и работать с кривыми редакторами анимации, так что в итоге осталось с дюжину интерактивных предметов. У некоторых из них было несколько вариаций, кардинально менявших поведение объекта — это было проще, чем добавлять новые. Так что прожекторы, зеркала и турели по сути один и тот же объект!
Но с ошибками приходит и опыт. Движок Closure написан кое-как, но оказался достаточно хорош, чтобы запустить игру на PS3. Идея переписать некоторые части движка была заманчивой, но это лишь отложило бы выход игры. Вместо этого я писал заметки о том, что получилось плохо, чтобы учесть ошибки в следующий раз. Особенно о том, что мешало непосредственно созданию игры. То же и с The End is Nigh. В ее движке (который, кстати, НАМНОГО лучше, чем в Closure) все равно была куча ошибок, которые я решал, стиснув зубы. Как только игра вышла, я сразу начал улучшать движок для следующего проекта, исправлять раздражающие баги и добавлять новые функции.
И так раз за разом: учишься, создаешь игру, запускаешь, и все по новой. До тех пор, пока движок не станет действительно хорош.
Не стал вдаваться в технические подробности, как внедрить в движок каждую отдельную систему. Это зависит от конкретных вариантов использования, есть сотни способов — и каждый из них правильный. Понять, что вам подходит — ВОТ в чем суть разработки движка, с таким настроем стоит браться за создание собственных проектов.
Вот и все, что я хотел рассказать в этой статье. Скорее всего, она вас либо мотивировала на разработку собственного движка, либо окончательно отпугнула от этой идеи. Оба варианта хороши, если вы поняли, чего хотите сами.
Разработка собственного движка, напутствие
Это не тутор, а скорее моё виденье проблемы «для чайников». Ниже я лишь опишу самые вводные в тему, и наведу на мысли что и как может делаться. На полноту не претендую, поэтому самых умных жду в комментариях с пожеланиями и угрозами.
АТЕНШОН, МНОГА ТЕКСТА НЕТ ПИКЧ
Реклама своей игры как «написанная на собственном движке» это интересная затея, но явно не оправдывает себя, если твоя цель именно сделать игру, а не развлечься.
Разработка полноценного движка, о котором так мечтают люди начавшие свой путь в гейдеве, это время затратная вещь. Любой игровой движок состоит из компонентов основных на каких-то абстракциях, и разница между «сильным» и «слабым» инструментом — в количестве этих компонентов.
Поясню — что бы написать минимально рабочую игру, тебе нужно: обработка ввода, вывод графики и игровой цикл с логикой. Можно сказать — «всего 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 — «Статья слишкам длинная устал читать, пиши кароче»