Уроки, полученные при создании первой игры, и почему я хочу написать свой движок |
||
МЕНЮ Искусственный интеллект Поиск Регистрация на сайте Помощь проекту ТЕМЫ Новости ИИ Искусственный интеллект Разработка ИИГолосовой помощник Городские сумасшедшие ИИ в медицине ИИ проекты Искусственные нейросети Слежка за людьми Угроза ИИ ИИ теория Внедрение ИИКомпьютерные науки Машинное обуч. (Ошибки) Машинное обучение Машинный перевод Реализация ИИ Реализация нейросетей Создание беспилотных авто Трезво про ИИ Философия ИИ Big data Работа разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика
Генетические алгоритмы Капсульные нейросети Основы нейронных сетей Распознавание лиц Распознавание образов Распознавание речи Техническое зрение Чат-боты Авторизация |
2018-04-27 14:28 Недавно я выпустил свою первую игру BYTEPATH и мне показалось, что будет полезно записать свои мысли о том, чему я научился в процессе её создания. Я разделю эти уроки на «мягкие» и «жёсткие»: под мягкими я подразумеваю идеи, связанные с разработкой ПО, жёсткие — это более технические аспекты программирования. Кроме того, я расскажу о том, почему хочу написать собственный движок. Мягкие уроки Сообщу ради контекста, что начал делать собственные игры примерно 5-6 лет назад и у меня есть 3 «серьёзных» проекта, над которыми я работал до выпуска первой игры. Два из эти проектов мертвы и полностью провалились, а последний я временно приостановил, чтобы поработать над BYTEPATH. Вот gif-анимации из этих проектов Первые два проекта провалились по разным причинам, но с точки зрения программирования они провалились (по крайней мере, как мне это видится) потому, что я слишком часто пытался быть слишком умным и заранее обобщал слишком многое. Большинство мягких уроков связано с этим провалом, поэтому важно было сказать об этом. Преждевременное обобщение Пока самый важный урок, который я извлёк из этой игры, заключается в том, что если существует поведение, которое повторяется для нескольких типов сущностей, то лучше по умолчанию копипастить его, а не выполнять слишком рано абстрагирование/обобщение. AB* , почти похожее на AB , но с маленькой и несовместимой разницей. Тогда мы можем или изменить AB с учётом AB* , или создать совершенно новую часть, в которой будет содержаться поведение AB* . Если мы повторим такое упражнение несколько раз, то в первом случае мы придём к очень сложному AB со всевозможными переключателями и флагами для различных поведений, а во втором случае мы вернёмся на первую клетку поля, потому что все немного отличающиеся версии AB всё равно будут содержать кучу повторяющегося кода.В основе этой проблемы лежит тот факт, что каждый раз, когда мы добавляем что-то новое или изменяем поведение чего-то старого, мы должны делать это с учётом существующих структур. Чтобы изменить что-то, мы должны всегда задумываться «это будет находиться в AB или в AB* ?», и этот кажущийся простым вопрос является источником всех проблем. Так происходит потому, что мы пытаемся вставить что-то в существующую структуру, а не просто добавить новое и заставить это работать. Невозможно переоценить разницу в том, чтобы просто делать то, что нужно и тем, что приходится учитывать имеющийся код.Поэтому я осознал, что поначалу гораздо проще по умолчанию выбирать копипастинг кода. В показанном выше примере у нас есть ABC , и чтобы добавить ABD мы просто скопипастим ABC и удалим часть C , заменив её на D . То же относится и к ABE с ABF , и когда нам нужно добавить AB* , мы просто снова копипастим AB и заменяем его на AB* . Когда мы добавляем в эту схему что-то новое, нам достаточно просто скопировать код оттуда, где уже выполняется похожее действие, и изменить его, не беспокоясь о том, как оно встроится в уже имеющийся код. Оказалось, что такой способ гораздо лучше в реализации и ведёт к меньшему количеству проблем, хотя и выглядит контринтуитивным.Большинство советов не подходит разработчикам-одиночкам Между большинством советов программистам из Интернета и тем, что мне на самом деле приходится делать как одиночному разработчику, существует несовпадение контекстов. Причина этого в следующем: во-первых, большинство программистов работает в коллективе с другими людьми, поэтому обычно советы даются с этим предположением; во-вторых, бОльшая часть создаваемого людьми ПО должна существовать в течение очень долгого времени, но это не относится к инди-игре. Это означает, что большинство советов программистам практически бесполезно для области соло-разработки инди-игр, и что благодаря этому я могу делать многое то, что невозможно для других людей. Тоби Фокс (автор Undertale): Я не умею программировать, лол. И этот проект должен был подтвердить мне это мнение. Стоит заметить — это не значит, что вы можете расслабиться и писать мусорный код. В контексте разработки инди-игр это значит, что скорее всего стоит бороться с этим импульсом большинства программистов, с почти аутистической потребностью делать всё правильным и чистым, потому что это враг, замедляющий вашу работу. ECS Паттерн Entity Component System — хороший реальный пример противоречия всему, сказанному в предыдущих двух разделах. После прочтения большинства статей становится понятно, что инди-разработчики считают наследование плохой практикой, и что мы можем использовать компоненты, создавая сущности как из конструктора Lego, и что благодаря им мы можем гораздо проще использовать многократно используемое поведение, а буквально всё в создании игры становится легче. И хотя этот AAA-разработчик будет прав в том, что ECS полезен для него, это не всегда верно для других инди-разработчиков: из-за несовпадения контекстов эти две группы решают очень разные задачи. Как бы то ни было, мне кажется, я, как мог, донёс свою точку зрения. Значит ли это, что использующие ECS глупы или тупы? Нет. Я думаю, что если вы уже привыкли к использованию ECS и он для вас работает, то можете без размышлений использовать его. Но я считаю, что инди-разработчики в целом должны более критически относиться к таким решениям и их недостаткам. Думаю, здесь очень подходит мысль Джонатана Блоу (я ни в коем случае не считаю, что он согласился бы со мной относительно ECS): Избегайте разделения поведения на несколько объектов Один из паттернов, которого, похоже, мне не удалось избежать — это разбиение одного поведения на несколько объектов. В BYTEPATH это в основном проявилось тем, как я создавал комнату «Console», но в Frogfaller (игре, которую я делал ранее) это более очевидно: Жёсткие уроки Их контекст заключается в том, что я писал свою игру на Lua и с помощью L?VE. Я написал 0 строк кода на C и C++, всё писалось на Lua. Поэтому многие из этих уроков связаны с самим Lua, хотя большинство из них применимы и к другим языкам. nil 90% багов, получаемых от игроков, связаны с доступом к переменным
Однако проблема кодинга таким способом заключается в том, что я ссылаюсь на другой объект слишком уж перестраховываясь, а поскольку Lua является интерпретируемым языком, происходят такие редкие баги с ветвями кода. Но я не могу придумать никакого другого способа решения этой проблемы, и поскольку она является серьёзным источником багов, то мне кажется правильным иметь стратегию для их правильной обработки. В будущем я подумываю никогда не ссылаться из одного объекта на другой напрямую, а вместо этого ссылаться на них через их id. В такой ситуации, когда я хочу сделать что-то с другим объектом, мне сначала придётся получить его по его id, а затем уже что-то с ним делать:
Преимущество такого подхода в том, что он заставляет меня получать объект каждый раз, когда я хочу с ним что-нибудь сделать. Кроме того, я никогда не сделаю ничего подобного:
Это значит, что я никогда не храню постоянную ссылку на другой объект в текущем, то есть не может произойти ошибок из-за смерти другого объекта. Мне это не кажется очень желательным решением, потому что каждый раз, когда я хочу что-то сделать, оно добавляет много излишнего. Языки наподобие MoonScript немного помогают в этом, потому что там можно сделать нечто подобное:
Но так как я не буду использовать MoonScript, то мне, похоже, придётся смириться с этим. Больший контроль над размещением памяти Хотя я и не буду утверждать, что сборка мусора плоха, особенно с учётом того, что я собираюсь использовать Lua для моих следующих игр, мне всё равно очень не нравятся некоторые её аспекты. В похожих на C языках возникновение утечки раздражает, но в них мы обычно можем приблизительно понять, где она происходит. Однако в языках наподобие Lua сборщик мусора похож на «чёрный ящик». Можно заглянуть в него, чтобы получить намёки о происходящем, но это неидеальный способ работы. Когда у вас происходит утечка в Lua, то она оказывается гораздо большей проблемой, чем в C. Это дополняется тем, что я использую кодовую базу C++, которой не владею, а именно кодовую базу L?VE. Я не знаю, как разработчики настроили размещение памяти со своей стороны, поэтому со стороны Lua мне гораздо сложнее добиться предсказуемого поведения памяти. Таймеры, ввод и камера Это три области, в которых я очень доволен получившимися у меня решениями. Для этих общих задач я написал три библиотеки:
У всех них есть API, которые кажутся мне очень интуитивно понятными и очень облегчают мою жизнь. Пока самым полезным для меня оказывалась Timer, потому что она позволяет мне реализовывать всевозможные решения простым образом:
Этот код убивает текущий объект (self) через 2 секунды. Также эта библиотека позволяет очень удобно реализовывать переходы tween:
Эта строка позволяет плавно изменять (tween) атрибут alpha объекта до 0 в течение 2 секунд с помощью режима tween in-out-cubic , а затем уничтожать объект. Это позволяет создать эффект постепенного растворения и исчезания. Также его можно использовать, чтобы заставить объекты мерцать при ударе:
Этот код 10 раз каждые 0,05 секунды переключает значение self.visible между true и false. Это значит, что он создаёт эффект мерцания на 0,5 секунды. Как вы видите, библиотеку можно использовать практически безгранично. Это стало возможным благодрая тому, как Lua работает со своими анонимными функциями.Другие библиотеки имеют столь же тривиальный API, являющийся мощным и полезным. Библиотека камеры — единственная, которая оказалась слишком низкоуровневой, но это можно улучшить в будущем. Смысл её заключается в том, чтобы иметь возможность реализовать нечто похожее на то, что показано в этом видео: Но в конце концов я создал что-то вроде промежуточного слоя между самыми основами модуля камеры и тем, что показано в видео. Поскольку я хотел, чтобы библиотекой пользовались люди, использующие L?VE, мне пришлось делать меньше допущений о том, какие типы атрибутов могут быть доступны. То есть некоторые из возможностей, показанных в видео, реализовать невозможно. В будущем, когда я буду создавать собственный движок, я смогу допускать о своих игровых объектах всё, что захочу, то есть буду способен реализовать правильную версию библиотеки, которая умеет всё, что показано в этом видео! Комнаты и области Для меня очень подходящим способом работы с объектами оказалась концепция комнат (Room) и областей (Area). Комнаты — это аналог «уровня» или «сцены». В них происходит всё действие, их может быть множество и вы можете переключаться между ними. Область — это тип менеджера объектов, который может находиться внутри комнат. Некоторые называют такие объекты Area «пространствами» (spaces). Area и Room работают вместе примерно таким образом (в реальной версии этих классов будет намного больше функций, например, у Area будут
Преимущество разделения этих концепций заключается в том, что в комнатах необязательно должны быть области, то есть способ управления объектами в комнате не является фиксированным. В одной комнате я могу решить, что объекты должны управляться каким-то другим образом, и тогда я смогу просто писать код, вместо того, чтобы приспосабливать свой код 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 и куча других вещей, которые я вскоре упомяну, оказываются слишком сложными. У меня есть множество страниц багов, с которыми я встречаюсь ежедневно. Я начал вести журнал багов, потому что они настолько плохи и мне приходится записывать скриншоты и скринкасты, чтобы доказать, что я не сумасшедший. Просто потому, что их очень много и их никто не исправляет. Я больше не буду сообщать о багах, если только за это не будут платить, потому что за все эти годы я не увидел, чтобы исправили хотя бы один из них. Два года баг-репортов, и они всё ещё существуют; разработчики просто продолжают добавлять фичи, и это даже не смешно. 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, а как я упоминал в предыдущих разделах, я стремлюсь как можно сильнее избегать размещения памяти, или, по крайней мере, хочу иметь полный контроль над ним, когда оно происходит. Внешняя интеграция Такие вещи, как Steamworks, Twitch, Discord и другие сайты, имеют собственные API, которые нужно интегрировать, чтобы пользоваться их удобными возможностями, а если не владеть кодовой базой на C/C++, то это задача будет намного сложнее. Разумеется, можно выполнить работу для интегрирования их в L?VE, например, но при этом потребуется больше труда, чем при интеграции в собственный движок. Другие платформы Это одно из преимуществ, которые я вижу в движках наподобие Unity или Unreal по сравнению с написанием собственного движка: они поддерживают все наиболее распространённые платформы. Я не знаю, хорошо ли реализована, но меня впечатляет то, что они на это способны, и в одиночку сделать это будет сложно. Хотя я и не супернерд, живущий и дышащий ассемблером, я не думаю, что у меня возникнет куча проблем с портированием моего будущего движка на консоли или другие платформы, но не могу рекомендовать каждому идти таким путём, потому что скорее всего это приведёт к куче трудозатрат. Реплеи, трейлеры Создание трейлера для этой игры оказалось очень плохим опытом. Мне он совсем не понравился. Я хорошо могу продумывать в голове фильмы/трейлеры/истории (по какой-то причине я постоянно это делаю, когда слушаю музыку), поэтому у меня была очень хорошая идея для трейлера, который я хотел создать для игры. Но результат получился совершенно не тем, потому что я не знал, как использовать необходимые инструменты (например, редактор видео) и не имел особого контроля над записью. И причина, по которой мне нужен свой движок для создания системы реплеев, заключается в том, что мне совершенно точно нужно работать на уровне байтов, чтобы реплеи работали управляемым образом и занимали меньше места. Я уже собирал систему реплеев в на Lua в этой статье, но всего 10 секунд реплея создают файл объёмом 10 МБ. В неё можно внести и дополнительные оптимизации, но в конце концов Lua имеет свои пределы, и гораздо удобнее оптимизировать подобные вещи на C. Целостность конструкции И последняя причина, по которой я хочу создать собственный движок — это целостность конструкции. Один из принципов, которые я люблю/ненавижу в L?VE, Lua (то же самое относится и к философии Linux) — это их децентрализованность. В Lua и L?VE нет стандартных способов реализации, люди делают то, что им кажется правильным, и если вы хотите написать библиотеку, которой будут пользоваться другие люди, то не стоит делать слишком много допущений. Этой идее следовали все библиотеки, созданные мной для L?VE (их можно найти в моём репозитории), потому что в противном случае ими бы никто не пользовался. Источник: habr.com Комментарии: |
|