Недавно я выпустил свою первую игру BYTEPATH и мне показалось, что будет полезно записать свои мысли о том, чему я научился в процессе её создания. Я разделю эти уроки на «мягкие» и «жёсткие»: под мягкими я подразумеваю идеи, связанные с разработкой ПО, жёсткие — это более технические аспекты программирования. Кроме того, я расскажу о том, почему хочу написать собственный движок.
Мягкие уроки
Сообщу ради контекста, что начал делать собственные игры примерно 5-6 лет назад и у меня есть 3 «серьёзных» проекта, над которыми я работал до выпуска первой игры. Два из эти проектов мертвы и полностью провалились, а последний я временно приостановил, чтобы поработать над BYTEPATH.
Первые два проекта провалились по разным причинам, но с точки зрения программирования они провалились (по крайней мере, как мне это видится) потому, что я слишком часто пытался быть слишком умным и заранее обобщал слишком многое. Большинство мягких уроков связано с этим провалом, поэтому важно было сказать об этом.
Преждевременное обобщение
Пока самый важный урок, который я извлёк из этой игры, заключается в том, что если существует поведение, которое повторяется для нескольких типов сущностей, то лучше по умолчанию копипастить его, а не выполнять слишком рано абстрагирование/обобщение.
На практике этого добиться очень сложно. Мы, программисты, приучены замечать повторения и стремимся как можно быстрее избавляться от них, но я заметил, что обычно этот импульс гораздо чаще создаёт проблемы, чем решает. Главная проблема, которую он создаёт, заключается в том, что обобщения часто неверны, а когда обобщение ошибочно, оно привязывает структуру кода к себе, и это гораздо сложнее исправить и изменить, чем при отсутствии обобщения.
Рассмотрим пример сущности, которая выполняет действия ABC
. Поначалу мы кодируем ABC
непосредственно в сущности, потому что нет никаких причин делать иначе. Но когда дело доходит до другой сущности, выполняющей ABD
, мы анализируем всё и думаем «давай-ка возьмём AB
из этих двух сущностней, и тогда каждая из них будет обрабатывать самостоятельно только C
и D
», что кажется вполне логичным, потому что мы абстрагируем AB
и сможем повторно использовать их в других местах. Если новые сущности используют AB
таким же образом, каким они определены, то это не проблема. Допустим, у нас есть ABE
, ABF
и так далее…
Но рано или поздно (и обычно это происходит раньше) появляется сущность, которой требуется AB*
, почти похожее на AB
, но с маленькой и несовместимой разницей. Тогда мы можем или изменить AB
с учётом AB*
, или создать совершенно новую часть, в которой будет содержаться поведение AB*
. Если мы повторим такое упражнение несколько раз, то в первом случае мы придём к очень сложному AB
со всевозможными переключателями и флагами для различных поведений, а во втором случае мы вернёмся на первую клетку поля, потому что все немного отличающиеся версии AB
всё равно будут содержать кучу повторяющегося кода.
В основе этой проблемы лежит тот факт, что каждый раз, когда мы добавляем что-то новое или изменяем поведение чего-то старого, мы должны делать это с учётом существующих структур. Чтобы изменить что-то, мы должны всегда задумываться «это будет находиться в AB
или в AB*
?», и этот кажущийся простым вопрос является источником всех проблем. Так происходит потому, что мы пытаемся вставить что-то в существующую структуру, а не просто добавить новое и заставить это работать. Невозможно переоценить разницу в том, чтобы просто делать то, что нужно и тем, что приходится учитывать имеющийся код.
Поэтому я осознал, что поначалу гораздо проще по умолчанию выбирать копипастинг кода. В показанном выше примере у нас есть ABC
, и чтобы добавить ABD
мы просто скопипастим ABC
и удалим часть C
, заменив её на D
. То же относится и к ABE
с ABF
, и когда нам нужно добавить AB*
, мы просто снова копипастим AB
и заменяем его на AB*
. Когда мы добавляем в эту схему что-то новое, нам достаточно просто скопировать код оттуда, где уже выполняется похожее действие, и изменить его, не беспокоясь о том, как оно встроится в уже имеющийся код. Оказалось, что такой способ гораздо лучше в реализации и ведёт к меньшему количеству проблем, хотя и выглядит контринтуитивным.
Большинство советов не подходит разработчикам-одиночкам
Между большинством советов программистам из Интернета и тем, что мне на самом деле приходится делать как одиночному разработчику, существует несовпадение контекстов. Причина этого в следующем: во-первых, большинство программистов работает в коллективе с другими людьми, поэтому обычно советы даются с этим предположением; во-вторых, бОльшая часть создаваемого людьми ПО должна существовать в течение очень долгого времени, но это не относится к инди-игре. Это означает, что большинство советов программистам практически бесполезно для области соло-разработки инди-игр, и что благодаря этому я могу делать многое то, что невозможно для других людей.
Например, я могу использовать глобальные значения, потому что очень часто они полезны, и пока я могу удерживать их в голове, они не представляют проблемы (подробнее об этом можно прочитать в части 10 туториала по BYTEPATH). Также я могу не слишком сильно комментировать свой код, потому что бОльшую его часть я держу в голове, ведь кодовая база не очень велика. Я могу создавать скрипты, которые работают только на моей машине, потому что никому не понадобится выполнять сборку игры, то есть сложность этого шага можно сильно снизить и для выполнения работы мне не понадобятся специальные инструменты. У меня могут быть огромные функции и классы, и поскольку я создаю их с нуля и точно знаю, как они работают, их огромный объём не представляет проблемы. И всё это я могу делать потому, что, как оказывается, большинство связанных с ними проблем проявляется только в коллективах, работающих над ПО с длительным сроком жизни.
Из работы над этим проектом я научился тому, что не произошло ничего особо плохого, когда я делал все эти «плохие» вещи. Где-то на границе сознания я всегда помнил, что для создания инди-игр мне не нужен суперкачественный код, учитывая тот факт, что многие разработчики создали отличные игры, используя очень плохие практики написания кода:
casenpai: Меня приводит в ужас то, что у тебя, похоже, в коде 864 конструкций case.
Тоби Фокс (автор Undertale): Я не умею программировать, лол.
И этот проект должен был подтвердить мне это мнение. Стоит заметить — это не значит, что вы можете расслабиться и писать мусорный код. В контексте разработки инди-игр это значит, что скорее всего стоит бороться с этим импульсом большинства программистов, с почти аутистической потребностью делать всё правильным и чистым, потому что это враг, замедляющий вашу работу.
ECS
Паттерн Entity Component System — хороший реальный пример противоречия всему, сказанному в предыдущих двух разделах. После прочтения большинства статей становится понятно, что инди-разработчики считают наследование плохой практикой, и что мы можем использовать компоненты, создавая сущности как из конструктора Lego, и что благодаря им мы можем гораздо проще использовать многократно используемое поведение, а буквально всё в создании игры становится легче.
По своему определению стремление программистов к ECS говорит о преждевременном обобщении, потому что если мы рассматриваем вещи как кирпичики Lego и думаем о том, как из них можно собирать новые вещи, то мы думаем в рамках многократно применяемых фрагментов, которые можно объединить каким-то полезным образом. И по причинам, перечисленным мной в разделе о предварительном обобщении, я считаю, что это СОВЕРШЕННО НЕВЕРНО! Хорошо объясняет мою позицию этот точный научный график:
Как вы видите, защищаемый мной принцип «yolo-кодинга» сначала намного проще и постепенно становится сложнее: сложность проекта увеличивается и yolo-техники начинают демонстрировать свои проблемы. С другой стороны, ECS поначалу гораздо сложнее — вам приходится создавать компоненты, а это по определению более трудно, чем создавать просто работающие элементы. Но со временем полезность ECS становится всё более и более очевидной и в какой-то момент он побеждает yolo-кодинг. Моя точка зрения заключается в том, что в контексте БОЛЬШИНСТВА инди-игр момент, в который ECS становится лучшим вложением средств, никогда не наступает.
К слову о несоответствии контекста: если эта статья наберёт популярность, то в комментариях обязательно появится какой-нибудь AAA-разработчик и скажет что-то вроде «Я уже 20 лет работаю в этой индустрии, и этот несмышлёныш несёт полную ЧУШЬ!!! ECS очень полезен, я выпустил уже несколько AAA-игр, заработавших миллионы долларов, в которые играли миллиарды людей по всему миру!!! Прекратите нести этот бред!!!»
И хотя этот AAA-разработчик будет прав в том, что ECS полезен для него, это не всегда верно для других инди-разработчиков: из-за несовпадения контекстов эти две группы решают очень разные задачи.
Как бы то ни было, мне кажется, я, как мог, донёс свою точку зрения. Значит ли это, что использующие ECS глупы или тупы? Нет. Я думаю, что если вы уже привыкли к использованию ECS и он для вас работает, то можете без размышлений использовать его. Но я считаю, что инди-разработчики в целом должны более критически относиться к таким решениям и их недостаткам. Думаю, здесь очень подходит мысль Джонатана Блоу (я ни в коем случае не считаю, что он согласился бы со мной относительно ECS):
Избегайте разделения поведения на несколько объектов
Один из паттернов, которого, похоже, мне не удалось избежать — это разбиение одного поведения на несколько объектов. В BYTEPATH это в основном проявилось тем, как я создавал комнату «Console», но в Frogfaller (игре, которую я делал ранее) это более очевидно:
Этот объект состоит из основного тела медузы, из отдельных ног медузы и логического объекта, связывающего всё вместе и координирующего поведения тела и ног. Это очень неуклюжий способ кодирования такой сущности, потому что поведение разделено на три разных типа объектов и их координация становится очень сложной, но когда мне приходится кодировать сущность подобным образом (и в игре есть множество многосоставных сущностей), то я естественным образом выбираю такой способ решения.
Одна из причин, по которым я по умолчанию выбираю подобное разделение, заключается в том, что каждый физический объект должен содержаться в коде в одном объекте, то есть когда я хочу создать новый физический объект, мне также нужно создать и новый экземпляр объекта. На самом деле это не является жёстким правилом или ограничением, которое обязательно к исполнению, мне просто очень удобно это из-за того, как я спроектировал архитектуру моего API физики.
На самом деле я долгое время размышлял о том, как можно решить эту проблему, но мне так и не удалось найти хорошее решение. Простое кодирование всего в одном объекте выглядит ужасно, потому что нужно выполнять координацию между различными физическими объектами, но разделение физических объектов на правильные объекты с их последующей координацией тоже кажутся неприемлемыми и неправильными. Я не знаю, как эту проблему решают другие люди, так что жду ваших советов!
Жёсткие уроки
Их контекст заключается в том, что я писал свою игру на Lua и с помощью LÖVE. Я написал 0 строк кода на C и C++, всё писалось на Lua. Поэтому многие из этих уроков связаны с самим Lua, хотя большинство из них применимы и к другим языкам.
nil
90% багов, получаемых от игроков, связаны с доступом к переменным nil
. Я не отслеживал статистику того, какие типы доступа более/менее часты, но чаще всего они связаны со смертью объекта, когда другой объект хранит ссылку на этот умерший объект и пытается с ним что-нибудь сделать. Думаю, это относится к категории проблем «срока жизни».
Решение этой проблемы в каждом случае обычно реализуется очень просто, достаточно проверять, существует ли объект и только после этого выполнять с ним действия:
if self.other_object then
doThing(self.other_object)
end
Однако проблема кодинга таким способом заключается в том, что я ссылаюсь на другой объект слишком уж перестраховываясь, а поскольку Lua является интерпретируемым языком, происходят такие редкие баги с ветвями кода. Но я не могу придумать никакого другого способа решения этой проблемы, и поскольку она является серьёзным источником багов, то мне кажется правильным иметь стратегию для их правильной обработки.
В будущем я подумываю никогда не ссылаться из одного объекта на другой напрямую, а вместо этого ссылаться на них через их id. В такой ситуации, когда я хочу сделать что-то с другим объектом, мне сначала придётся получить его по его id, а затем уже что-то с ним делать:
local other_object = getObjectByID(self.other_id)
if other_object then
doThing(other_object)
end
Преимущество такого подхода в том, что он заставляет меня получать объект каждый раз, когда я хочу с ним что-нибудь сделать. Кроме того, я никогда не сделаю ничего подобного:
self.other_object = getObjectByID(self.other_id)
Это значит, что я никогда не храню постоянную ссылку на другой объект в текущем, то есть не может произойти ошибок из-за смерти другого объекта. Мне это не кажется очень желательным решением, потому что каждый раз, когда я хочу что-то сделать, оно добавляет много излишнего. Языки наподобие MoonScript немного помогают в этом, потому что там можно сделать нечто подобное:
if object = getObjectByID(self.other_id)
doThing(object)
Но так как я не буду использовать MoonScript, то мне, похоже, придётся смириться с этим.
Больший контроль над размещением памяти
Хотя я и не буду утверждать, что сборка мусора плоха, особенно с учётом того, что я собираюсь использовать Lua для моих следующих игр, мне всё равно очень не нравятся некоторые её аспекты. В похожих на C языках возникновение утечки раздражает, но в них мы обычно можем приблизительно понять, где она происходит. Однако в языках наподобие Lua сборщик мусора похож на «чёрный ящик». Можно заглянуть в него, чтобы получить намёки о происходящем, но это неидеальный способ работы. Когда у вас происходит утечка в Lua, то она оказывается гораздо большей проблемой, чем в C. Это дополняется тем, что я использую кодовую базу C++, которой не владею, а именно кодовую базу LÖVE. Я не знаю, как разработчики настроили размещение памяти со своей стороны, поэтому со стороны Lua мне гораздо сложнее добиться предсказуемого поведения памяти.
Стоит заметить, что с точки зрения скорости проблем со сборщиком мусора Lua у меня нет. Можно управлять им так, чтобы он работал с определёнными ограничениями (например, чтобы не запускался в течение n мс), поэтому в этом проблем нет. Проблема только в том, что можно сказать ему не выполняться более n мс, а он не сможет собрать весь мусор, который вы сгенерировали за кадр. Поэтому желателен максимальный контроль над количеством размещённой памяти. Есть очень хорошая статья по этой теме: http://bitsquid.blogspot.com.br/2011/08/fixing-memory-issues-in-lua.html, и я расскажу о ней подробнее, когда доберусь в этой статье до движка.
Таймеры, ввод и камера
Это три области, в которых я очень доволен получившимися у меня решениями. Для этих общих задач я написал три библиотеки:
- https://github.com/SSYGEN/chrono (Timer)
- https://github.com/SSYGEN/boipushy (Input)
- https://github.com/SSYGEN/STALKER-X (Camera)
У всех них есть API, которые кажутся мне очень интуитивно понятными и очень облегчают мою жизнь. Пока самым полезным для меня оказывалась Timer, потому что она позволяет мне реализовывать всевозможные решения простым образом:
timer:after(2, function() self.dead = true end)
Этот код убивает текущий объект (self) через 2 секунды. Также эта библиотека позволяет очень удобно реализовывать переходы tween:
timer:tween(2, self, {alpha = 0}, 'in-out-cubic', function() self.dead = true end)
Эта строка позволяет плавно изменять (tween) атрибут alpha
объекта до 0 в течение 2 секунд с помощью режима tween in-out-cubic
, а затем уничтожать объект. Это позволяет создать эффект постепенного растворения и исчезания. Также его можно использовать, чтобы заставить объекты мерцать при ударе:
timer:every(0.05, function() self.visible = not self.visible, 10)
Этот код 10 раз каждые 0,05 секунды переключает значение self.visible
между true и false. Это значит, что он создаёт эффект мерцания на 0,5 секунды. Как вы видите, библиотеку можно использовать практически безгранично. Это стало возможным благодрая тому, как Lua работает со своими анонимными функциями.
Другие библиотеки имеют столь же тривиальный API, являющийся мощным и полезным. Библиотека камеры — единственная, которая оказалась слишком низкоуровневой, но это можно улучшить в будущем. Смысл её заключается в том, чтобы иметь возможность реализовать нечто похожее на то, что показано в этом видео:
Но в конце концов я создал что-то вроде промежуточного слоя между самыми основами модуля камеры и тем, что показано в видео. Поскольку я хотел, чтобы библиотекой пользовались люди, использующие LÖVE, мне пришлось делать меньше допущений о том, какие типы атрибутов могут быть доступны. То есть некоторые из возможностей, показанных в видео, реализовать невозможно. В будущем, когда я буду создавать собственный движок, я смогу допускать о своих игровых объектах всё, что захочу, то есть буду способен реализовать правильную версию библиотеки, которая умеет всё, что показано в этом видео!
Комнаты и области
Для меня очень подходящим способом работы с объектами оказалась концепция комнат (Room) и областей (Area). Комнаты — это аналог «уровня» или «сцены». В них происходит всё действие, их может быть множество и вы можете переключаться между ними. Область — это тип менеджера объектов, который может находиться внутри комнат. Некоторые называют такие объекты Area «пространствами» (spaces). Area и Room работают вместе примерно таким образом (в реальной версии этих классов будет намного больше функций, например, у Area будут addGameObject
, queryGameObjectsInCircle
, и т.д.):
Area = Class()
function Area:new()
self.game_objects = {}
end
function Area:update(dt)
-- update all game objects
end
Room = Class()
function Room:new()
self.area = Area()
end
function Room:update(dt)
self.area:update(dt)
end
Преимущество разделения этих концепций заключается в том, что в комнатах необязательно должны быть области, то есть способ управления объектами в комнате не является фиксированным. В одной комнате я могу решить, что объекты должны управляться каким-то другим образом, и тогда я смогу просто писать код, вместо того, чтобы приспосабливать свой код Area под новый функционал.
Однако одно из преимуществ такого подхода заключается в том, что легко смешать локальную логику управления объектами с логикой управления объектами Area, если в комнате есть они обе. Это очень легко может запутать и при разработке BYTEPATH это стало серьёзным источником ошибок. Поэтому в будущем я постараюсь сделать так, чтобы в Room использовались строго или Area, или её собственная процедура управления объектами, но никогда обе одновременно.
snake_case и camelCase
Сейчас я использую snake_case для имён переменных и camelCase для названий функций. В будущем я собираюсь использовать snake_case везде, кроме имён классов/модулей, которые по-прежнему останутся CamelCase. Причина этого очень проста: в camelCase очень сложно читать длинные названия функций. Возможность перепутать имена переменных и функций в snake_case обычно не является проблемой благодаря контексту использования имени, поэтому всё будет в порядке.
Движок
Основной причиной того, что я хочу написать после завершения этой игры собственный движок, является контроль. LÖVE — это отличный фреймворк, но когда дело доходит до выпуска игры, он становится слишком грубоват. Такие вещи, как поддержка Steamworks, поддержка HTTPS, тестирование других физических движков наподобие Chipmunk, использование библиотек C/C++, упаковка своей игры для распространения на Linux и куча других вещей, которые я вскоре упомяну, оказываются слишком сложными.
Это не значит, что задача нерешаема, но для её решения мне пришлось бы спуститься до уровня C/C++ и работать там. Я программирую на C, поэтому с этим у меня нет никаких проблем, но изначально я решил использовать фреймворк потому, что хотел пользоваться Lua и не беспокоиться ни о чём другом, а такая работа противоречит моим стремлениям. Поэтому если мне в любом случае придётся работать на низком уровне, то я лучше буду владеть этой частью кодовой базы, написав её самостоятельно.
Однако здесь я хочу изложить более общую точку зрения на движки, и для этого мне придётся вместо LÖVE начать ругать Unity. Есть игра, которая мне нравится и в которую я довольно долго играл — Throne of Lies:
Это клон «Мафии», у которого было (а возможно, и сейчас есть) очень здоровое и хорошее сообщество. Я узнал о ней от стримера, которого смотрю, поэтому в игре есть много людей с похожим образом
У меня есть множество страниц багов, с которыми я встречаюсь ежедневно. Я начал вести журнал багов, потому что они настолько плохи и мне приходится записывать скриншоты и скринкасты, чтобы доказать, что я не сумасшедший. Просто потому, что их очень много и их никто не исправляет. Я больше не буду сообщать о багах, если только за это не будут платить, потому что за все эти годы я не увидел, чтобы исправили хотя бы один из них. Два года баг-репортов, и они всё ещё существуют; разработчики просто продолжают добавлять фичи, и это даже не смешно. Unity кажется таким потрясающим, поэтому нас обманывает превосходный маркетинг. Но спустя два года я уже вижу шаблон их работы: высшее руководство очевидно не разрабатывало и не создавало игры ни разу за всю свою жизнь. Поэтому они делаю новую фичу, выпускают её в альфаподобном состоянии и забывают о ней навечно, переходя к следующей фиче, которая заработает им деньги. С каждой версией происходит одна и та же история. Потом они постоянно помечают висящие баги как исправленные, как будто никто ничего не заметит. В последнее время это активно начали обсуждать. Unity замечателен для минипроектов, но попробуйте сделать что-то более продвинутое, и быстро начнёте обнаруживать баги.
В число багов входят постоянные тормоза, проблемы с Async, добавленная и заброшенная поддержка Vulkan, они полностью сломали FB standalone для всей версии и притворились, что ничего не было, перейдя к новым фичам (нам пришлось убрать логин через FB и т.п.), глитчи UI наподобие искажённого текста, баг с окрашиванием всего в розовый цвет, баг с чёрным экраном, баг с исчезающим текстом, Scroll Rect'ы содержат больше багов, чем я могу перечислить (даже улучшающие их плагины терпят неудачу, потому что ставятся поверх Unity).
Например, скроллеры… В случайные моменты времени они могут начать смещаться влево, хотя ты ничего не делал. Если изменить их положение, то иногда элемент UI сворачивается и становится отрицательным. Приходится нажимать на Play, а потом Stop, чтобы увидеть это снова, если повезёт, или перезагружать систему.
Ещё баги… У их фичи совместной работы (collab) были потрясающие разработчики, но снова та же беда — высшее руководство, принимающее плохие решения, выпустило её в почти в состоянии альфы, просто чтобы заработать денег. Потом и эту фичу забросили. Мы отказались от collab в пользу gitlab CE и половина наших проблем просто исчезла. Всё настолько дико. Один из самых крупных багов — через каждые 2-3 запуска кнопка запуска блокировалась (патч так и не вышел, отчёт отправлен в ПРОШЛОМ ГОДУ), и запуск Unity блокирует все потоки на две минуты. Комбо-баг… Блокируется кнопка Play, а перезагрузка затормаживает всё на 2 секунды. Через каждые 2-3 запуска игры. Теперь представьте работу в таком режиме в течение 10 часов.
Ещё баги… Unity крашится, если вы выполняете выход, когда предварительно загружается новая сцена — выглядит непрофессионально. Всё, что делаешь с фичами буфера обмена, блокирует весь буфер обмена ЗА ПРЕДЕЛАМИ Unity, пока не перезагрузишь компьютер.
Ещё нарушенные обещания… UNET? Это тоже отдельная история. Они заявляли, что это будет фича корпоративного уровня. В результате оказалась только для двух людей, со сломанной архитектурой, по-прежнему не работает уже в течение полутора лет, нет ни документации, ни туториала, ни поддержки. Похоже, её полностью отдали на аутсорс, потому что никто ничего не знает. Мы снова купились на маркетинг и потеряли три месяца, а потом перешли на Photon и то, что заняло у меня три месяца, я сделал за три дня. Даже модераторы на их собственном форуме говорят, что она совершенно не работает. Я делал уморительные скриншоты. Приходилось смеяться, чтобы не заплакать… Сколько времени было потрачено… Так много нарушенных обещаний. Снова тот же шаблон: реализуем, выпускаем в состоянии альфы, забываем навечно.
И такого было очень много. Вот, что я могу сказать: если делаете 3D, то переходите на Unreal. Я даже не могу начать описывать своё разочарование. Раньше я был гордым Unity-разработчиком, пока не увидел за маской ужасную правду. Нам было так стыдно, что мы даже убрали со своего веб-сайта логотип Unity. Так много вложено, а я даже не могу рекомендовать Unity другим разработчикам.
То есть этот человек, создавший очень понравившуюся мне игру, рассказывал ужасные вещи о Unity, о том, что он очень нестабилен, что разработчики постоянно стремятся к новым функциям и никогда не реализуют их правильно, и так далее. Меня очень удивило, что кому-то настолько не нравится Unity, что он пишет такое. Поэтому я решил немного подтолкнуть его, чтобы узнать, что ещё он может сказать о Unity:
А потом он сказал такое:
И такое:
Я никогда не пользовался Unity, поэтому не знаю, правду ли он говорит, но он написал на нём готовую игру и я не вижу причин, по которым он мог бы лгать. Его точка зрения во всех этих постах примерно одинакова: Unity сосредоточен на добавлении новых функций вместо усовершенствования имеющихся и у Unity есть проблемы с поддержанием стабильности множества имеющихся функций между версиями.
По моему мнению, одним из самых убедительных аргументов в его постах является то, что применимо и другим движкам, а не только к Unity: разработчики движка сами не делают на нём игры. По крайней мере, с LÖVE я заметил одну важную вещь — многие проблем фреймворка могли бы быть решены, если бы разработчики активно делали на нём инди-игры. Потому что в таком случае все эти проблемы стали бы для них очевидными, получили бы высочайший приоритет и быстро были исправлены. xblade724 выяснил, что то же самое справедливо и для Unity. А многие другие знакомые мне люди обнаружили подобное и для других движков.
Есть очень малое количество фреймворков/движков, на которых сами разработчики активно делают игры. Первые, которые приходят мне в голову: Unreal, потому что Epic создала кучу суперуспешных игр на собственном движке, последняя из них Fortnite; Monogame, потому что основные разработчики портируют с его помощью игры на разные платформы; и GameMaker, потому что YoYo Games делает мобильные игры на своём движке.
Для всех остальных известных мне движков это условие не выполняется, то есть у всех этих движков есть очень очевидные проблемы и препятствия для создания готовых игр, которые скорее всего никогда не будут исправлены. Потому что нет стимула, так ведь? Если какие-то проблемы воздействуют только на 5% пользователей, потому что они возникают в конце цикла разработки игры, то зачем вообще исправлять их, если ты не делаешь на собственном движке игры и тебе не предстоит столкнуться с этими проблемами самому?
И всё это означает, что если я заинтересован в создании игр надёжным и проверенным способом, не сталкиваясь с кучей неожиданных проблем ближе к завершению игры, то я не будут использовать движок, который усложнит мою жизнь, поэтому я не буду использовать никакой другой движок, кроме перечисленных выше трёх. В моём конкретном случае Unreal не подходит, потому что меня в основном интересуют 2D-игры, а Unreal для них — это перебор, Monogame не работает, потому что я ненавижу C#, а GameMaker не работает, потому что мне не нравится идея визуального кодинга или кодинга на основе интерфейса. То есть у меня остаётся единственный вариант — создать свой собственный движок.
Итак, разобравшись со всеми этими рассуждениями, давайте перейдём к конкретным задачам:
Взаимодействие C/Lua и память
Привязка C/Lua может осуществляться двумя фундаментальными способами (по крайней мере, исходя из моего ограниченного опыта): с полными пользовательскими данными и с ограниченными пользовательскими данными. При использовании полных пользовательских данных когда код на Lua запрашивает размещение чего-то в C, например, физического объекта, мы создаём ссылку на этот объект в Lua и используем её. Таким образом мы можем создать полный объект с метатаблицами и всевозможными параметрами, надёжно описывающими объект C. Одна из проблем такого подхода заключается в том, что это создаёт кучу мусора со стороны Lua, а как я упоминал в предыдущих разделах, я стремлюсь как можно сильнее избегать размещения памяти, или, по крайней мере, хочу иметь полный контроль над ним, когда оно происходит.
Поэтому логичнее мне будет использовать подход с ограниченными пользовательскими данными. Ограниченные пользовательские данные — это просто обычный указатель C. Это означает, что мы не можем получить много информации об объекте, на который указываем, но этот вариант обеспечивает наибольший контроль со стороны Lua. В этой схеме создание и уничтожение объектов должно выполняться вручную и всё не собирается волшебным образом, и именно это мне и нужно. Этой теме посвящён очень интересный доклад разработчика Stingray Engine:
Прочитав документацию, можно увидеть, как описываемое им происходит в движке.
Смысл написания своего собственного движка в том, что у меня будет полный контроль над тем, как происходит привязка C/Lua и над выбором компромиссов, которые при этом должны возникать. Если я использую чей-то чужой движок на Lua, то выбор сделан за меня и этим выбором я могу и не быть полностью довольным, например, как это случилось с LÖVE. Поэтому это основной способ, которым я могу получить больше контроля над памятью и создавать быстрые и надёжные игры.
Внешняя интеграция
Такие вещи, как Steamworks, Twitch, Discord и другие сайты, имеют собственные API, которые нужно интегрировать, чтобы пользоваться их удобными возможностями, а если не владеть кодовой базой на C/C++, то это задача будет намного сложнее. Разумеется, можно выполнить работу для интегрирования их в LÖVE, например, но при этом потребуется больше труда, чем при интеграции в собственный движок.
Если вы используете очень популярные движки наподобие Unity или Unreal, в которых уже есть реализованная другими людьми интеграция с большинством таких сервисов, то это не проблема, но если вы пользуетесь другим движком с меньшей базой пользователей, то вам придётся или интегрировать эти вещи самостоятельно, или использовать чей-то наполовину реализованный и едва работающий код, что является плохим решением.
И снова владение частью кодовой базы на C/C++ делает такие интеграции гораздо проще, потому что вы можете просто реализовать только нужное вам и это точно будет работать.
Другие платформы
Это одно из преимуществ, которые я вижу в движках наподобие Unity или Unreal по сравнению с написанием собственного движка: они поддерживают все наиболее распространённые платформы. Я не знаю, хорошо ли реализована, но меня впечатляет то, что они на это способны, и в одиночку сделать это будет сложно. Хотя я и не супернерд, живущий и дышащий ассемблером, я не думаю, что у меня возникнет куча проблем с портированием моего будущего движка на консоли или другие платформы, но не могу рекомендовать каждому идти таким путём, потому что скорее всего это приведёт к куче трудозатрат.
Одна из платформ, которую я очень хочу поддерживать с самого начала — это веб. Однажды я играл в браузере в игру Bloons TD5, и спустя какое-то время игра предложила мне перейти в Steam и купить её за 10 долларов. Так я и поступил. Поэтому я считаю, что поддержка браузерной версии игры с меньшим количеством функций и предложение перейти в Steam — это хорошая стратегия, которую я тоже хочу реализовать. Предварительно я изучил вопрос о том, что нужно для создания движка на C, и, похоже, удобным для работы SDL будет Emscripten, с помощью которого я смогу рисовать на экране в браузере.
Реплеи, трейлеры
Создание трейлера для этой игры оказалось очень плохим опытом. Мне он совсем не понравился. Я хорошо могу продумывать в голове фильмы/трейлеры/истории (по какой-то причине я постоянно это делаю, когда слушаю музыку), поэтому у меня была очень хорошая идея для трейлера, который я хотел создать для игры. Но результат получился совершенно не тем, потому что я не знал, как использовать необходимые инструменты (например, редактор видео) и не имел особого контроля над записью.
Я надеюсь реализовать в своём движке систему реплеев и систему трейлеров. Система реплеев позволит мне намного проще записывать геймплейные клипы, потому что для записи игрового процесса мне не понадобятся сторонние программы. Кроме того, я считаю, что смогу сделать так, чтобы геймплей постоянно записывался во время разработки, чтобы я программным образом мог просматривать все реплеи и выбирать определённые события или последовательности событий, которые можно использовать в трейлере. Если мне это удастся, то процесс получения нужных мне записей станет намного проще.
Кроме того, после реализации этой системы реплеев я хочу добавить встроенную в движок систему трейлеров, позволяющую мне соединять вместе фрагменты различных реплеев. На самом деле я не вижу в этом никаких технических препятствий, так что вопрос только в реализации.
И причина, по которой мне нужен свой движок для создания системы реплеев, заключается в том, что мне совершенно точно нужно работать на уровне байтов, чтобы реплеи работали управляемым образом и занимали меньше места. Я уже собирал систему реплеев в на Lua в этой статье, но всего 10 секунд реплея создают файл объёмом 10 МБ. В неё можно внести и дополнительные оптимизации, но в конце концов Lua имеет свои пределы, и гораздо удобнее оптимизировать подобные вещи на C.
Целостность конструкции
И последняя причина, по которой я хочу создать собственный движок — это целостность конструкции. Один из принципов, которые я люблю/ненавижу в LÖVE, Lua (то же самое относится и к философии Linux) — это их децентрализованность. В Lua и LÖVE нет стандартных способов реализации, люди делают то, что им кажется правильным, и если вы хотите написать библиотеку, которой будут пользоваться другие люди, то не стоит делать слишком много допущений. Этой идее следовали все библиотеки, созданные мной для LÖVE (их можно найти в моём репозитории), потому что в противном случае ими бы никто не пользовался.
Преимущества такой децентрализации заключаются в том, что я запросто могу взять чью-то библиотеку, использовать её в своей игре, подогнать её под свои нужды и в целом всё будет работать. Недостатки такой децентрализации заключаются в том, что количество времени, которое мне может сэкономить каждая библиотека, ниже, по сравнению с кодом, более централизованным относительно какого-то набора стандартов. Я уже упоминал это в примере с моей собственной библиотекой камеры. Это противоречит тому, чтобы делать всё быстро.
Поэтому одна из вещей, которую я очень хочу сделать в своём движке — это способность централизовать всё точно так, как я хочу и возможность делать множество допущений, что увеличит темп работы (а также, я надеюсь, и повысит продуктивность)!
Автор: PatientZero
А огромные – это сколько? В моём текущем проекте есть заголовочный файл в 60323 строки. И это заголовок только одного файла. И перегруженные функции-члены только с одним именем суммарно занимают 399931 строку. С другим именем тот же класс имеет суммарно 433417 строк перегруженных функций-членов. Думаете, это исходники? Фиг! Я хитрый. И тексты на c++ у меня генерятся в отдельной софтине. По определённым формализованным правилам. Думаете проще было бы написать шаблоны? Шаблон, достаточно чётко ограничивающий фактические типы, но в то же время достаточно гибко учитывающий их характеристики, будет написать не проще и не быстрей.
И пиши по-русски.