Главная » Хабрахабр » [Перевод] Уроки, полученные при создании первой игры, и почему я хочу написать свой движок

[Перевод] Уроки, полученные при создании первой игры, и почему я хочу написать свой движок

image

Недавно я выпустил свою первую игру BYTEPATH и мне показалось, что будет полезно записать свои мысли о том, чему я научился в процессе её создания. Я разделю эти уроки на «мягкие» и «жёсткие»: под мягкими я подразумеваю идеи, связанные с разработкой ПО, жёсткие — это более технические аспекты программирования. Кроме того, я расскажу о том, почему хочу написать собственный движок.
Сообщу ради контекста, что начал делать собственные игры примерно 5-6 лет назад и у меня есть 3 «серьёзных» проекта, над которыми я работал до выпуска первой игры. Два из эти проектов мертвы и полностью провалились, а последний я временно приостановил, чтобы поработать над BYTEPATH.

Вот gif-анимации из этих проектов

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

Преждевременное обобщение

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

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

Поначалу мы кодируем ABC непосредственно в сущности, потому что нет никаких причин делать иначе. Рассмотрим пример сущности, которая выполняет действия ABC. Если новые сущности используют AB таким же образом, каким они определены, то это не проблема. Но когда дело доходит до другой сущности, выполняющей ABD, мы анализируем всё и думаем «давай-ка возьмём AB из этих двух сущностней, и тогда каждая из них будет обрабатывать самостоятельно только C и D», что кажется вполне логичным, потому что мы абстрагируем 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 становится лучшим вложением средств, никогда не наступает.

ECS очень полезен, я выпустил уже несколько AAA-игр, заработавших миллионы долларов, в которые играли миллиарды людей по всему миру!!! К слову о несоответствии контекста: если эта статья наберёт популярность, то в комментариях обязательно появится какой-нибудь AAA-разработчик и скажет что-то вроде «Я уже 20 лет работаю в этой индустрии, и этот несмышлёныш несёт полную ЧУШЬ!!! Прекратите нести этот бред!!!»

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

Можно управлять им так, чтобы он работал с определёнными ограничениями (например, чтобы не запускался в течение n мс), поэтому в этом проблем нет. Стоит заметить, что с точки зрения скорости проблем со сборщиком мусора Lua у меня нет. Поэтому желателен максимальный контроль над количеством размещённой памяти. Проблема только в том, что можно сказать ему не выполняться более n мс, а он не сможет собрать весь мусор, который вы сгенерировали за кадр. Есть очень хорошая статья по этой теме: http://bitsquid.blogspot.com.br/2011/08/fixing-memory-issues-in-lua.html, и я расскажу о ней подробнее, когда доберусь в этой статье до движка.

Таймеры, ввод и камера

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

timer:after(2, function() self.dead = true end)

Этот код убивает текущий объект (self) через 2 секунды. Также эта библиотека позволяет очень удобно реализовывать переходы tween:

timer:tween(2, self, , '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 под новый функционал.

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

snake_case и camelCase

Сейчас я использую snake_case для имён переменных и camelCase для названий функций. В будущем я собираюсь использовать snake_case везде, кроме имён классов/модулей, которые по-прежнему останутся CamelCase. Причина этого очень проста: в camelCase очень сложно читать длинные названия функций. Возможность перепутать имена переменных и функций в snake_case обычно не является проблемой благодаря контексту использования имени, поэтому всё будет в порядке.
Основной причиной того, что я хочу написать после завершения этой игры собственный движок, является контроль. LÖVE — это отличный фреймворк, но когда дело доходит до выпуска игры, он становится слишком грубоват. Такие вещи, как поддержка Steamworks, поддержка HTTPS, тестирование других физических движков наподобие Chipmunk, использование библиотек C/C++, упаковка своей игры для распространения на Linux и куча других вещей, которые я вскоре упомяну, оказываются слишком сложными.

Я программирую на C, поэтому с этим у меня нет никаких проблем, но изначально я решил использовать фреймворк потому, что хотел пользоваться Lua и не беспокоиться ни о чём другом, а такая работа противоречит моим стремлениям. Это не значит, что задача нерешаема, но для её решения мне пришлось бы спуститься до уровня C/C++ и работать там. Поэтому если мне в любом случае придётся работать на низком уровне, то я лучше буду владеть этой частью кодовой базы, написав её самостоятельно.

Есть игра, которая мне нравится и в которую я довольно долго играл — Throne of Lies: Однако здесь я хочу изложить более общую точку зрения на движки, и для этого мне придётся вместо LÖVE начать ругать Unity.

Это клон «Мафии», у которого было (а возможно, и сейчас есть) очень здоровое и хорошее сообщество. Я узнал о ней от стримера, которого смотрю, поэтому в игре есть много людей с похожим образом мышления, что очень здорово. В целом игра мне очень нравилась. Однажды я нашёл на /r/gamedev постмортем этой игры от одного из её разработчиков. Этот парень был одним из программистов, и он написал один комментарий, привлёкший моё внимание:

Я начал вести журнал багов, потому что они настолько плохи и мне приходится записывать скриншоты и скринкасты, чтобы доказать, что я не сумасшедший. У меня есть множество страниц багов, с которыми я встречаюсь ежедневно. Я больше не буду сообщать о багах, если только за это не будут платить, потому что за все эти годы я не увидел, чтобы исправили хотя бы один из них. Просто потому, что их очень много и их никто не исправляет. Unity кажется таким потрясающим, поэтому нас обманывает превосходный маркетинг. Два года баг-репортов, и они всё ещё существуют; разработчики просто продолжают добавлять фичи, и это даже не смешно. Поэтому они делаю новую фичу, выпускают её в альфаподобном состоянии и забывают о ней навечно, переходя к следующей фиче, которая заработает им деньги. Но спустя два года я уже вижу шаблон их работы: высшее руководство очевидно не разрабатывало и не создавало игры ни разу за всю свою жизнь. Потом они постоянно помечают висящие баги как исправленные, как будто никто ничего не заметит. С каждой версией происходит одна и та же история. Unity замечателен для минипроектов, но попробуйте сделать что-то более продвинутое, и быстро начнёте обнаруживать баги. В последнее время это активно начали обсуждать.

В число багов входят постоянные тормоза, проблемы с Async, добавленная и заброшенная поддержка Vulkan, они полностью сломали FB standalone для всей версии и притворились, что ничего не было, перейдя к новым фичам (нам пришлось убрать логин через FB и т.п.), глитчи UI наподобие искажённого текста, баг с окрашиванием всего в розовый цвет, баг с чёрным экраном, баг с исчезающим текстом, Scroll Rect'ы содержат больше багов, чем я могу перечислить (даже улучшающие их плагины терпят неудачу, потому что ставятся поверх Unity).

Если изменить их положение, то иногда элемент UI сворачивается и становится отрицательным. Например, скроллеры… В случайные моменты времени они могут начать смещаться влево, хотя ты ничего не делал. Приходится нажимать на Play, а потом Stop, чтобы увидеть это снова, если повезёт, или перезагружать систему.

Потом и эту фичу забросили. Ещё баги… У их фичи совместной работы (collab) были потрясающие разработчики, но снова та же беда — высшее руководство, принимающее плохие решения, выпустило её в почти в состоянии альфы, просто чтобы заработать денег. Всё настолько дико. Мы отказались от collab в пользу gitlab CE и половина наших проблем просто исчезла. Комбо-баг… Блокируется кнопка Play, а перезагрузка затормаживает всё на 2 секунды. Один из самых крупных багов — через каждые 2-3 запуска кнопка запуска блокировалась (патч так и не вышел, отчёт отправлен в ПРОШЛОМ ГОДУ), и запуск Unity блокирует все потоки на две минуты. Теперь представьте работу в таком режиме в течение 10 часов. Через каждые 2-3 запуска игры.

Всё, что делаешь с фичами буфера обмена, блокирует весь буфер обмена ЗА ПРЕДЕЛАМИ Unity, пока не перезагрузишь компьютер. Ещё баги… Unity крашится, если вы выполняете выход, когда предварительно загружается новая сцена — выглядит непрофессионально.

Это тоже отдельная история. Ещё нарушенные обещания… UNET? В результате оказалась только для двух людей, со сломанной архитектурой, по-прежнему не работает уже в течение полутора лет, нет ни документации, ни туториала, ни поддержки. Они заявляли, что это будет фича корпоративного уровня. Мы снова купились на маркетинг и потеряли три месяца, а потом перешли на Photon и то, что заняло у меня три месяца, я сделал за три дня. Похоже, её полностью отдали на аутсорс, потому что никто ничего не знает. Я делал уморительные скриншоты. Даже модераторы на их собственном форуме говорят, что она совершенно не работает. Снова тот же шаблон: реализуем, выпускаем в состоянии альфы, забываем навечно. Приходилось смеяться, чтобы не заплакать… Сколько времени было потрачено… Так много нарушенных обещаний.

Вот, что я могу сказать: если делаете 3D, то переходите на Unreal. И такого было очень много. Раньше я был гордым Unity-разработчиком, пока не увидел за маской ужасную правду. Я даже не могу начать описывать своё разочарование. Так много вложено, а я даже не могу рекомендовать Unity другим разработчикам. Нам было так стыдно, что мы даже убрали со своего веб-сайта логотип Unity.

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

А потом он сказал такое:

И такое:

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

По крайней мере, с LÖVE я заметил одну важную вещь — многие проблем фреймворка могли бы быть решены, если бы разработчики активно делали на нём инди-игры. По моему мнению, одним из самых убедительных аргументов в его постах является то, что применимо и другим движкам, а не только к Unity: разработчики движка сами не делают на нём игры. 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:

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

Если я использую чей-то чужой движок на Lua, то выбор сделан за меня и этим выбором я могу и не быть полностью довольным, например, как это случилось с LÖVE. Смысл написания своего собственного движка в том, что у меня будет полный контроль над тем, как происходит привязка C/Lua и над выбором компромиссов, которые при этом должны возникать. Поэтому это основной способ, которым я могу получить больше контроля над памятью и создавать быстрые и надёжные игры.

Внешняя интеграция

Такие вещи, как 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 (их можно найти в моём репозитории), потому что в противном случае ими бы никто не пользовался.

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

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


Оставить комментарий

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

*

x

Ещё Hi-Tech Интересное!

Как Яндекс применил компьютерное зрение для повышения качества видеотрансляций. Технология DeepHD

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

Security Week 36: Telnet должен быть закрыт

Telnet — это очень старый протокол. Википедия сообщает, что он был разработан в 1969 году, много лет активно использовался для удаленного доступа к компьютерам и серверам, причем как под управлением Unix/Linux, так и для систем под Windows (telnet можно было ...