Как и зачем создавать собственный игровой движок / Хабр
Игра с нуля — интересный челлендж для разработчика. Но если хотите пройти его на сложности Nightmare, можно еще и сделать собственный игровой движок, заточенный специально под проект. Подводных камней много, рассказываем, что важно знать при разработке такого гейм-дизайнерского софта, и что в него добавить.
Итак, вы задумались о создании собственного игрового движка. Отлично! У этого варианта множество плюсов в сравнении с использованием коммерческого — такого как Unity или Unreal. В этой статье разберемся, зачем разрабатывать свой движок, какие системы необходимо предусмотреть и как правильно подойти к процессу.
Зачем?
Начнем с главного вопроса, который стоит задать себе, если вы решили разработать собственный движок: зачем это вам?
Резонными причинами могут быть, например, такие:
Хотите создать игру с использованием новой технологии, которую не поддерживают другие движки. Или поддерживают, но реализация слишком сложна и костыльна. Это может быть масштабная симуляция (Factorio), нестандартный проект, который не вписывается в готовые шаблоны (Noita, Miegakure), и множество других идей. В таких случаях нет иного выхода кроме как писать собственный движок под проект.
Хотите оптимизировать рабочий процесс под игры, которые создаете. Если для проекта не нужен полный объем возможностей коммерческого движка, есть смысл создать кастомизированный вариант с подходящими для конкретной игры редакторами и функциями. Если движок не затачивается под конкретный проект, стоит задуматься, так ли нужно самописное решение или все-таки достаточно готовых вариантов?
Не хотите в долгосрочной перспективе зависеть от чужих технологий. Если вы хотите иметь полный контроль над проектом, готовы самостоятельно исправлять ошибки (а не сидеть в ожидании багфикса от создателей) и не бояться, что очередное обновление сломает игру, то собственный движок — подходящий вариант! И не придется зависеть от прихотей крупных корпораций
Вам интересно разобраться, как устроены и работают игровые движки. Это отличная причина — по правде говоря, самый веский повод заняться разработкой собственного движка.
Раз уж начали составлять списки, то вот несколько неудачных причин браться за разработку движка. Если найдете свою среди этих пунктов — притормозите и подумайте еще раз.
Вам кажется, что вы придумаете движок покруче, чем Unity или Unreal (или Godot, или GameMaker). Не выйдет. Разработать подходящий для специфических нужд софт можно (см. предыдущий список), но в одиночку или маленькой командой невозможно создать универсальный движок, который будет конкурировать с известным универсальным ПО. Особенно при первой попытке.
Думаете, что иначе вы «ненастоящий программист»? Использование готового движка не делает гейм-разработчика хуже. Для того они и придуманы! Это просто инструмент для создания игр. 99% проектов можно разработать при помощи уже существующего софта — в этом нет ничего постыдного. Ведь главное — это сама игра!
Если вы хотите таким образом сэкономить время или деньги — забудьте! Создавать движок с нуля долго, а время = деньги. Использовать готовый софт выгоднее, чем пытаться разработать собственный. В долгосрочной перспективе это может стать выигрышной стратегией, но только если движок будет основой для нескольких прибыльных проектов, и при этом значительно удобнее в работе, чем коммерческие. Такое ПО разработать сложно, особенно если это первый опыт (и почти невозможно, если речь о 3D).
При принятии решения учитывайте свой опыт и цели. Чем меньше практики в создании игр, тем сложнее окажется разработка движка — обязательно потренируйтесь прежде чем браться за игровое ПО.
Я начинал с флэш-игр в 90-00х, и ни один движок того времени не поддерживал импорт флэш-анимаций. Единственным выходом было создать собственный софт. Намного приятнее и быстрее закидывать swf-файлы в папку с ресурсами и сразу использовать анимации в игре без промежуточных шагов типа экспорта в списки спрайтов.
Конечно,целесообразность создания собственного движка во многом зависит от количества опыта как в геймдеве, так и в программировании. Приятно сделать кастомный софт, не гуглить постоянно туториалы, и самостоятельно дебажить возникающие ошибки. В то же время, достаточно допустить пару оплошностей, и проект развалится, а обратиться за советом будет некуда. Собственный движок — это полный контроль, но и полная ответственность за продукт.
Что?
Игровой движок — это рабочая среда, в которой создают игры. Он состоит из базы, на которой строится проект, и деталей, из которых, словно из деталей лего, состоит сама игра. Это золотая середина между «логикой игры» и «скучными техническими штуками»: благодаря движку в игровом коде не приходится вручную прописывать, как отобразить на экране условный треугольник, можно сразу заняться взаимодействием элементов.
Разные движки выполняют за вас разное количество работы. Некоторые просто отображают графику на экране (Flash, Pico-8). Другие сами по себе — целая игра с возможностью кастомизации или узко заточены под определенный жанр (RPGMaker, Ren’Py). А между ними — бесчисленное количество вариантов.
Игровые движки обычно основываются на простых фреймворках типа SDL и OpenGL, и включают в себя специализированные библиотеки для аудио, видео, физических и математических вычислений и чего угодно еще. При создании движка нет необходимости каждую мелочь прописывать вручную, практически на каждую потенциально полезную опцию доступна соответствующая библиотека.
Базовые функции движка.
Это основы, необходимые для того, чтобы начать создавать игры.
Инициализация системы.
Приводит программу в боевую готовность после запуска — открывает окно, загружает данные. С этим (и не только!) справится стандартная библиотека SDL, проще ее и использовать.
Контроль частоты кадров
Ограничивает частоту сменяемости кадров для плавного изображения и оптимизации работы.
Ввод
Существует много способов реагирования на нажатия кнопок или движения джойстика, и обычно с этим справляется ранее упомянутый SDL. Так что если он используется для инициализации, больше ничего дополнительно ставить не надо. Поверх него можно построить мощную и гибкую систему ввода, но для начала хватит и дефолтной.
Рендеринг
Большинство (ну как минимум 75%) игр так или иначе используют графику, и за нее отвечает как раз движок. В 2D-игре минимальному рендеру достаточно отображать на экране текстурированные четырехугольники. Шейдеры, буферы вершин, однобуферная прорисовка, меши, материалы и так далее — это прекрасные опции, которые можно добавить позднее, если понадобится. Если хочется заморочиться с OpenGL или Vulkan и кастомизировать рендерер — на здоровье! Но помните, нет ничего постыдного в том, чтобы использовать для рендеринга готовые библиотеки типа Ogre3D. Выбор зависит от целей и потребностей разработчика, а также от того, какие задачи интереснее решать самостоятельно.
Математические и прочие утилиты
Желательно, чтобы к этим библиотекам имели доступ как игровой код, так и движок. Плюс — ко всем иным полезным функциям и формулам, которые вы найдете в процессе разработки. STB — отличный ресурс для поиска всевозможных утилит, которые могут пригодиться при создании движка.
Дополнительные функции
Еще несколько систем, которые лучше добавить в движок ближе к делу, когда они непосредственно понадобятся для игры:
Управление игровыми объектами и сценами
Можно кодить и вручную, но практичнее предусмотреть систему для обработки отдельных игровых объектов и коллекций. Это один из ключевых механизмов в движке, ведь он управляет логикой игры. Из каких компонентов состоят объекты, на какие типы событий реагируют, как происходит взаимодействие, что со структурой памяти, используется ECS? (Кстати, «чистый» неадаптированный ECS лучше применять только для специфических кейсов.) Эти и не только вопросы должна покрывать система управления объектами и сценами. Для таких задач доступны готовые библиотеки (особенно для чистого ECS), но, поскольку эта структура сильнее остальных влияет на игровой код, я склоняюсь к принципу «сделай сам». Использование существующего решения вынудит постоянно думать о том, как вписать логику игры в рамки. А надо наоборот — адаптировать движок под выражение задуманной игровой логики.
Аудио
Звуковые эффекты и музыка здесь разделены, хотя и прячутся под одним названием. Основные необходимые функции — это запуск и остановка звуковых циклов и воспроизведение звуковых эффектов от начала до конца. Этим аудио не ограничивается, но даже с двумя базовыми опциями можно далеко продвинуться. Минус в том, что стандартные звуковые фреймворки (FMod and Wwise) — коммерческие и с кучей лицензионных ограничений. Однако большинство ресурсов с открытым кодом раздражают неудобством (передаю привет OpenAL). Сам я использую FAudio — на мой вкус, простая и комфортная в использовании база для построения сложных звуковых механик.
Загрузка и управление файлами
Файлы загружают все игры. Вряд ли вам захочется вручную повторно загружать и декодировать уже добавленные файлы, так что понадобится система, которая этим займется. В будущем загрузчику файлов можно добавить и другие функции — например, поддержку модов или динамическую выгрузку. Это не срочно — поначалу можно использовать встроенный менеджер, но со временем файлов станет так много, что понадобится удобная система управления файлами и ресурсами.
Нетворкинг
Окей, нетворкинг (онлайн-мультиплеер) — это ОЧЕНЬ опционально. Если не планируется режим p2p, то и не заморачивайтесь. Однако эту систему чрезвычайно сложно встроить в движок, который разработан не с расчетом на многопользовательские игры. Поэтому, если вы планируете или допускаете добавление мультиплеера, подготовьте почву заранее, потому что иначе придется переделывать все системы.
Это базовый набор систем, которые входят в игровой движок. Другие варианты типа обнаружения столкновений, физики, сериализации, анимации и UI уже опциональны. Они распространены, поэтому входят в большинство движков, но для создания игр не обязательны. Например, предотвращение столкновений можно обеспечить при помощи математических утилит и прописать алгоритм в коде игры. Простейшую гравитацию и ускорение можно настроить без физических движков типа Box2D or Bullet. А полная сериализация вообще лишняя, если нужно попросту сохранить чекпойнт.
В самописном движке однозначно будет меньше систем и функций, чем в универсальном коммерческом. Такова цель! Unity и Unreal — огромные монолиты, и каждая отдельная игра использует лишь малую часть предложенных опций. Добавляйте только то, что нужно для вашего конкретного кейса и сосредоточьтесь на том, чтобы сделать инструменты разработки лучше и комфортнее в использовании.
Озанкомьтесь с тем, как работают другие игровые движки, прежде чем браться за собственный. Разберитесь, какие парадигмы и алгоритмы они используют, что реализовано классно, а что раздражает. Попробуйте создать мини-игру на нескольких движках, чтобы понять, как они устроены.
Как?
Итак, вы взвесили за и против, поняли, чего хотите, и решили все-таки взяться за создание движка. И как же это сделать?
Сразу к делу: делайте игру параллельно с разработкой движка. Это правило нельзя нарушать. Изучите основы как можно скорее и сразу же начинайте создавать на этой базе игру. Движок — ничто без игры.
Это необходимо, потому что функционал движка должен соответствовать потребностям сделанных на нем игр. Нельзя понять, как построить хорошую анимационную систему, если проект не требует сложной анимации. Слабые места движка проявляются в процессе написания игры. Может быть, нужна древовидная система, благодаря которой дальние объекты не будут рендериться, пока не приблизятся на определенное расстояние? Я не знаю, и вы не узнаете, пока не соберете игровой уровень, который будет страшно зависать. И даже тогда проблема может оказаться не в обновлении объектов — чтобы понять, надо проверить.
Не программируйте того, что не нужно. Если единственный UI в игре — кнопка Play в главном меню, поздравляю! Не придется создавать мудреный пользовательский интерфейс. В The End Is Nigh нет ни физического движка, ни детектора столкновений. Там даже нет камеры, потому что она там не нужна. Я использовал электронную таблицу .csv, чтобы собрать карту мира, вместо всяких сложных редакторов. Делается легко и нормально работает.
Не буду вдаваться в подробности реализации — способов слишком много, каждый подходит для определенных случаев. Нет «наилучшего варианта рендеринга» или «самого правильного способа управления объектами». Все зависит от игры. Начинайте с основ и расширяйтесь по мере необходимости.
Что касается языков программирования — выбирайте, каким лучше владеете. Разработка движка — сама по себе непроста, а если делать это параллельно с изучением С++, обе эти задачи станут в два раза сложнее. C# идеально подойдет для создания движка. Медленнее, чем на С++, но не критично. Более медленный язык типа Python может вызвать затруднения, если в игре много движущихся объектов… но для некоторых игр пойдет. Используйте, что удобно.
И еще — с первой попытки идеально не получится. Моей первой игрой на самописном движке стала Closure, и в ней полный бардак (забавно, что ее номинировали на награду «Техническое совершенство» на фестивале независимых игр в 2010 году). Системы рендеринга и обновления вдвоем обрабатывали всю игру. Добавлять новые объекты было крайне трудоемко, приходилось дописывать кучу кода и работать с кривыми редакторами анимации, так что в итоге осталось с дюжину интерактивных предметов. У некоторых из них было несколько вариаций, кардинально менявших поведение объекта — это было проще, чем добавлять новые. Так что прожекторы, зеркала и турели по сути один и тот же объект!
Но с ошибками приходит и опыт. Движок Closure написан кое-как, но оказался достаточно хорош, чтобы запустить игру на PS3. Идея переписать некоторые части движка была заманчивой, но это лишь отложило бы выход игры. Вместо этого я писал заметки о том, что получилось плохо, чтобы учесть ошибки в следующий раз. Особенно о том, что мешало непосредственно созданию игры. То же и с The End is Nigh. В ее движке (который, кстати, НАМНОГО лучше, чем в Closure) все равно была куча ошибок, которые я решал, стиснув зубы. Как только игра вышла, я сразу начал улучшать движок для следующего проекта, исправлять раздражающие баги и добавлять новые функции.
И так раз за разом: учишься, создаешь игру, запускаешь, и все по новой. До тех пор, пока движок не станет действительно хорош.
Не стал вдаваться в технические подробности, как внедрить в движок каждую отдельную систему. Это зависит от конкретных вариантов использования, есть сотни способов — и каждый из них правильный. Понять, что вам подходит — ВОТ в чем суть разработки движка, с таким настроем стоит браться за создание собственных проектов.
Вот и все, что я хотел рассказать в этой статье. Скорее всего, она вас либо мотивировала на разработку собственного движка, либо окончательно отпугнула от этой идеи. Оба варианта хороши, если вы поняли, чего хотите сами.
Как написать собственный игровой движок на C++ / Хабр
Перевод статьи Джеффа Прешинга (Jeff Preshing) How to Write Your Own C++ Game Engine.
Как написать собственный игровой движок на C++
В последнее время я занят тем, что пишу игровой движок на C++. Я пользуюсь им для создания небольшой мобильной игры Hop Out. Вот ролик, записанный с моего iPhone 6. (Можете включить звук!)
Your browser does not support HTML5 video.
Hop Out — та игра, в которую мне хочется играть самому: ретро-аркада с мультяшной 3D-графикой. Цель игры — перекрасить каждую из платформ, как в Q*Bert.
Hop Out всё ещё в разработке, но движок, который приводит её в действие, начинает принимать зрелые очертания, так что я решил поделиться здесь несколькими советами о разработке движка.
С чего бы кому-то хотеть написать игровой движок? Возможных причин много:
- Вы — ремесленник. Вам нравится строить системы с нуля и видеть, как они оживают.
- Вы хотите узнать больше о разработке игр. Я в игровой индустрии 14 лет и всё ещё пытаюсь в ней разобраться. Я даже не был уверен, что смогу написать движок с чистого листа, ведь это так сильно отличается от повседневных рабочих обязанностей программиста в большой студии. Я хотел проверить.
- Вам нравится ощущение контроля. Организовать код именно так, как вам хочется, и всегда знать, где что находится — это приносит удовольствие.
- Вас вдохновляют классические игровые движки, такие как AGI (1984), id Tech 1 (1993), Build (1995), и гиганты индустрии вроде Unity и Unreal.
- Вы верите, что мы, индустрия игр, должны сбросить покров таинственности с процесса разработки движков. Мы пока не очень-то освоили искусство разработки игр — куда там! Чем тщательнее мы рассмотрим этот процесс, тем выше наши шансы усовершенствовать его.
Игровые платформы в 2017-ом — мобильные, консоли и ПК — очень мощные и во многом похожи друг на друга. Разработка игрового движка перестала быть борьбой со слабым и редким железом, как это было в прошлом. По-моему, теперь это скорее борьба со сложностью вашего собственного произведения. Запросто можно сотворить монстра! Вот почему все советы в этой статье вращаются вокруг того, как сохранить код управляемым. Я объединил их в три группы:
- Используйте итеративный подход
- Дважды подумайте, прежде чем слишком обобщать
- Осознайте, что сериализация — обширная тема.
Эти советы применимы к любому игровому движку. Я не собираюсь рассказывать, как написать шейдер, что такое октодерево или как добавить физику. Я полагаю, вы и так в курсе, что должны это знать — и во многом эти темы зависят от типа игры, которую вы хотите сделать. Вместо этого я сознательно выбрал темы, которые не освещаются широко — темы, которые я нахожу наиболее интересными, когда пытаюсь развеять завесу тайны над чем-либо.
Используйте итеративный подход
Мой первый совет — не задерживаясь заставьте что-нибудь (что угодно!) работать, затем повторите.
По возможности, начните с образца приложения, которое инициализирует устройство и рисует что-нибудь на экране. В данном случае я скачал SDL, открыл Xcode-iOS/Test/TestiPhoneOS.xcodeproj
, затем запустил на своём iPhone пример testgles2
.
Вуаля! У меня появился замечательный вращающийся кубик, использующий OpenGL ES 2.0.
Моим следующим шагом было скачивание сделанной кем-то 3D-модели Марио. Я быстро написал черновой загрузчик OBJ-файлов — этот формат не так уж сложен — и подправил пример, чтобы он отрисовывал Марио вместо кубика. Ещё я интегрировал SDL_Image, чтобы загружать текстуры.
Затем я реализовал управление двумя стиками, чтобы перемещать Марио. (Поначалу я рассматривал идею создания dual-stick шутера. Впрочем, не с Марио).
Следующим делом я хотел познакомиться со скелетной анимацией, так что открыл Blender, создал модель щупальца и привязал к нему скелет из двух костей, которые колебались туда-сюда.
К тому моменту я отказался от формата OBJ и написал скрипт на Python для экспорта собственных JSON-файлов из Blender. Эти JSON-файлы описывали заскиненный меш, скелет и данные анимации. Я загружал эти файлы в игру с помощью библиотеки C++ JSON.
Как только всё заработало, я вернулся в Blender и создал более проработанного персонажа (Это был первый сделанный и зариганный мной трёхмерный человек. Я им весьма гордился.)
В течение следующих нескольких месяцев я сделал такие шаги:
- Начал выделять функции работы с векторами и матрицами в собственную библиотеку трёхмерной математики.
- Заменил
.xcodeproj
на проект CMake - Заставил движок запускаться и на Windows, и на iOS, потому что мне нравится работать в Visual Studio.
- Начал перемещать код в отдельные библиотеки «engine» и «game». Со временем, я разделил их на ещё более мелкие библиотеки.
- Написал отдельное приложение, чтобы конвертировать мои JSON-файлы в бинарные данные, которые игра может загружать напрямую.
- В какой-то момент убрал все библиотеки SDL из iOS-сборки. (Cборка для Windows всё ещё использует SDL.)
Ключевой момент в следующем: я не планировал архитектуру движка до того как начал программировать. Это был осознанный выбор. Вместо этого я всего лишь писал максимально простой код, реализующий следующую часть функционала, затем смотрел на него, чтобы увидеть, какая архитектура возникла естественным образом. Под «архитектурой движка» я понимаю набор модулей, которые составляют игровой движок, зависимости между этими модулями и API для взаимодействия с каждым модулем.
Этот подход итеративен, потому что фокусируется на небольших практических результатах. Он хорошо работает при написании игрового движка, потому что на каждом шаге у вас есть работающая программа. Если что-то идёт не так, когда вы выделяете код в новый модуль, вы всегда можете сравнить изменения с кодом, который раньше работал. Разумеется, я предполагаю, что вы пользуетесь какой-нибудь системой контроля версий.
Может показаться, что при таком подходе много времени теряется впустую, ведь вы всегда пишете плохой код, который потом требуется переписывать начисто. Но большая часть изменений представляет собой перемещение кода из одного .cpp
-файла в другой, извлечение определений функций в
-файлы или другие не менее простые действия. Определить, где что должно лежать — сложная задача, и решить её проще, когда код уже существует.
Готов поспорить, что больше времени тратится при противоположном подходе: пытаться заранее продумать архитектуру, которая будет делать всё, что вам понадобится. Две моих любимых статьи про опасности чрезмерной инженерии — The Vicious Circle of Generalization Томаша Дабровски и Don’t Let Architecture Astronauts Scare You Джоэла Спольски.
Я не говорю, что вы не должны решать проблемы на бумаге до того, как столкнётесь с ними в коде. Я также не утверждаю, что вам не следует заранее решить, какой функционал вам нужен. Например, я знал с самого начала, что хочу, чтобы движок загружал все ресурсы в фоновом потоке. Просто я не пытался спроектировать или реализовать этот функционал до тех пор, пока мой движок не начал загружать хоть какие-то ресурсы.
Итеративный подход дал мне куда более элегантную архитектуру, чем я мог бы вообразить, глядя на чистый лист бумаги. iOS-сборка моего движка сегодня на 100% состоит из оригинального кода, включая собственную математическую библиотеку, шаблоны контейнеров, систему рефлексии/сериализации, фреймворк рендеринга, физику и аудио микшер. У меня были причины писать каждый из этих модулей самостоятельно, но для вас это может быть необязательным. Вместо этого есть множество отличных библиотек с открытым исходным кодом и разрешительной лицензией, которые могут оказаться подходящими вашему движку. GLM, Bullet Physics и STB headers — лишь некоторые из интересных примеров.
Дважды подумайте, прежде чем слишком обобщать
Как программисты, мы стремимся избегать дублирования кода, и нам нравится, когда код следует единому стилю. Тем не менее, я думаю, что полезно не давать этим инстинктам управлять всеми решениями.
Время от времени нарушайте принцип DRY
Приведу пример: мой движок содержит несколько шаблонных классов умных указателей, близких по духу к std::shared_ptr
. Каждый из них помогает избежать утечек памяти, выступая обёрткой вокруг сырого указателя.
Owned<>
для динамически выделяемых объектов, имеющих единственного владельца.Reference<>
использует подсчёт ссылок чтобы позволить объекту иметь несколько владельцев.audio::AppOwned<>
используется кодом за пределами аудио микшера. Это позволяет игровым системам владеть объектами, которые аудио микшер использует, такими как голос, который в данный момент воспроизводится.audio::AudioHandle<>
использует систему подсчёта ссылок, внутреннюю для аудио микшера.
Может показаться, что некоторые из этих классов дублируют функциональность других, нарушая принцип DRY. В самом деле, в начале разработки я пытался повторно использовать существующий класс Reference<>
как можно больше. Однако, я выяснил, что время жизни аудио-объекта подчиняется особым правилам: если объект закончил воспроизведение фрагмента, и игра не владеет указателем на этот объект, его можно сразу же поместить в очередь на удаление. Если игра захватила указатель, тогда аудио-объект не должен быть удалён. А если игра захватила указатель, но владелец указателя уничтожен до того, как воспроизведение закончилось, оно должно быть отменено. Вместо того чтобы усложнять Reference<>
, я решил, что будет практичнее ввести отдельные классы шаблонов.
95% времени повторное использование существующего кода — верный путь. Но если оно начинает вас сковывать или вы обнаруживаете, что усложняете что-то, однажды бывшее простым, спросите себя: не должна ли эта часть кодовой базы в действительности быть разделена надвое.
Использовать разные соглашения о вызове — это нормально
Одна из вещей, которая мне не нравится в Java — то, что она заставляет вас определять каждую функцию внутри класса. По-моему, это бессмысленно. Это может придать вашему коду более единообразный вид, но также поощряет переусложнение и не поддерживает итеративный подход, описанный мной ранее.
В моём C++ движке некоторые функции принадлежат классами, а некоторые — нет. Например, каждый противник в игре — это класс, и бо́льшая часть поведения противника реализована в этом классе, как и следовало ожидать. С другой стороны, sphere casts в моём движке выполняются вызовом sphereCast()
, функции в пространстве имён physics
. sphereCast()
не принадлежит какому-либо классу — это просто часть модуля physics
. У меня есть система сборки, которая управляет зависимостями между модулями, что сохраняет код достаточно (для меня) хорошо организованным. Заворачивание этой функции в произвольный класс никоим образом не улучшит организацию кода.
А ещё есть динамическая диспетчеризация, которая является формой полиморфизма. Часто нам нужно вызвать функцию объекта, не зная точного типа этого объекта. Первый порыв программиста на C++ — определить абстрактный базовый класс с виртуальными функциями, затем перегрузить эти функции в производном классе. Работает, но это лишь одна из техник. Существуют и другие методы динамической диспетчеризации, которые не привносят так много дополнительного кода, или имеют другие преимущества:
- С++11 ввел
std::function
, и это удобный способ хранить функции обратного вызова. Также можно написать собственную версиюstd::function
, не вызывающую столько боли, когда заходишь в неё в отладчике. - Многие функции обратного вызова могут быть реализованы с помощью пары указателей: указателя на функцию и непрозрачного аргумента. Требуется только явное приведение внутри функции обратного вызова. Это часто встречается в библиотеках на чистом C.
- Иногда базовый тип известен во время компиляции, и можно привязать вызов функции вообще без накладных расходов времени выполнения. Turf — библиотека, которой я пользуюсь в своём игровом движке, сильно полагается на этот способ. Взгляните на
turf::Mutex
для примера. Это простоtypedef
над платформо-специфичными классами. - Иногда самый прямой путь — создать и поддерживать таблицу сырых указателей на функцию своими силами. Я использовал этот подход в своих аудио микшере и системе сериализации. Интерпретатор Python также на полную использует эту технику, как будет показано ниже.
- Вы можете даже хранить указатели на функцию в хэш-таблице, используя имена функций как ключи. Я пользуюсь этой техникой для диспетчеризации событий ввода, таких как события мультитача. Это часть стратегии по записи ввода игры и воспроизведения его в системе реплеев.
Динамическая диспетчеризация — обширная тема. Я лишь поверхностно рассказал о ней, чтобы показать как много способов реализации существует. Чем больше растяжимого низкоуровневого кода вы пишите — что не редкость для игрового движка — тем чаще обнаруживаете себя за изучением альтернатив. Если вы не привыкли к программированию в таком виде, интерпретатор Python, написанный на C — отличный пример для изучения. Он реализует мощную объектную модель: каждый PyObject
указывает на PyTypeObject
, а каждый PyTypeObjeсt
содержит таблицу указателей на функцию для динамической диспетчеризации. Документ Defining New Types — хорошая начальная точка, если вы хотите сразу погрузиться в детали.
Осознайте, что сериализация — обширная тема
Сериализация — это преобразование объектов времени выполнения в последовательность байтов и обратно. Другими словами, сохранение и загрузка данных.
Для многих, если не большинства, движков игровой контент создаётся в разных редактируемых, таких как .png
, .json
, .blend
или проприетарных форматах, затем в конце концов конвертируется в платформо-специфичные форматы игры, которые движок может быстро загрузить. Последнее приложение в этом процессе часто называют «cooker». Cooker может быть интегрирован в другой инструмент или даже распределяться между несколькими машинами. Обычно, cooker и некоторое количество инструментов разрабатываются и поддерживаются в тандеме с самим игровым движком.
При подготовке такого пайплайна выбор форматов файлов на каждой из стадий остаётся за вами. Вы можете определить несколько собственных форматов, и они могут эволюционировать в процессе того как вы добавляете функциональность в движок. В то время как они эволюционируют, у вас может возникнуть необходимость сохранить совместимость некоторых программ с ранее сохранёнными файлами. Не важно в каком формате, в конце концов вам придётся сериализовать их в C++.
В C++ есть бесчисленное множество способов организовать сериализацию. Один из довольно очевидных заключается в добавлении функций save
и load
классам, которые вы хотите сериализовать. Вы можете добиться обратной совместимости, храня номер версии в заголовке файла, затем передавая это число в каждую функцию load
. Это работает, хотя код может стать громоздким.
void load(InStream& in, u32 fileVersion) { // Загрузить ожидаемые переменные-члены in >> m_position; in >> m_direction; // Загрузить более новую переменную только если версия загружаемого файла больше 2. if (fileVersion >= 2) { in >> m_velocity; } }
Можно писать более гибкий, менее подверженный ошибкам код сериализации, пользуясь преимуществом рефлексии — а именно, созданием данных времени выполнения, описывающих расположение ваших C++ типов. Чтобы получить краткое представление о том, как рефлексия может помочь с сериализацией, взглянем на то, как это делает Blender, проект с открытым исходным кодом.
Когда вы собираете Blender из исходников, выполняется много шагов. Во-первых, компилируется и запускается собственная утилита makesdna
. Эта утилита парсит набор заголовочных файлов C в дереве исходников Blender, а затем выводит краткую сводку со всеми определёнными типами в собственном формате, известном как SDNA. Эти SDNA-данные служат данными рефлексии. SDNA затем компонуется с самим Blender, и сохраняется с каждым .blend
-файлом, который Blender записывает. С этого момента, каждый раз когда Blender загружает .blend
-файл, он сравнивает SDNA .blend
-файла cо SDNA, скомпонованной с текущей версией во время исполнения и использует общий код сериализации для обработки всех различий. Эта стратегия даёт Blender впечатляющий диапазон обратной и прямой совместимости. Вы всё ещё можете загрузить файлы версии 1.0 в последней версии Blender, а новые .blend
-файлы могут быть загружены в старых версиях.
Как и Blender, многие игровые движки — и связанные с ними инструменты — создают и используют собственные данные рефлексии. Есть много способов делать это: вы можете разбирать собственный исходный код на C/C++, чтобы извлечь информацию о типах, как это делает Blender. Можете создать отдельный язык описания данных и написать инструмент для генерации описаний типов и данных рефлексии C++ из этого языка. Можете использовать макросы препроцессора и шаблоны C++ для генерации данных рефлексии во время выполнения. И как только у вас под рукой появятся данные рефлексии, открываются бесчисленные способы написать общий сериализатор поверх всего этого.
Несомненно, я упускаю множество деталей. В этой статье я хотел только показать, что есть много способов сериализовать данные, некоторые из которых очень сложны. Программисты просто не обсуждают сериализацию столько же, сколько другие системы движка, даже несмотря на то, что большинство других систем зависят от неё. Например, из 96 программистских докладов GDC 2017, я насчитал 31 доклад о графике, 11 об онлайне, 10 об инструментах, 3 о физике, 2 об аудио — и только один, касающийся непосредственно сериализации.
Как минимум, постарайтесь представить, насколько сложными будут ваши требования. Если вы делаете маленькую игру вроде Flappy Bird, с несколькими ассетами, вам скорее всего не придётся много думать о сериализации. Вероятно, вы можете загружать текстуры напрямую из PNG и этого будет достаточно. Если вам нужен компактный бинарный формат с обратной совместимостью, но вы не хотите разрабатывать свой — взгляните на сторонние библиотеки, такие как Cereal или Boost.Serialization. Не думаю, что Google Protocol Buffers идеально подходят для сериализации игровых ресурсов, но они всё равно стоят изучения.
Написание игрового движка — даже маленького — большое предприятие. Я мог бы сказать намного больше, но, если честно, самый полезный совет, который я могу придумать для статьи такой длины: работайте итеративно, слегка сопротивляйтесь тяге к обобщению кода, и помните, что сериализация — обширная тема, так что понадобится выбрать подходящую стратегию. Мой опыт показывает, что каждый из этих пунктов может стать камнем преткновения, если его игнорировать.
Я люблю сравнивать наблюдения по этой теме, так что мне очень интересно услышать мнение других разработчиков. Если вы писали движок, привел ли ваш опыт к тем же выводам? А если не писали или ещё только собираетесь, ваши мысли мне тоже интересны. Что вы считаете хорошим ресурсом для обучения? Какие аспекты ещё кажутся вам загадочными? Не стесняйтесь оставлять комментарии ниже или свяжитесь со мной через Twitter.
Как построить двигатель
| Практическое руководство — Двигатель и трансмиссия
В основе любой мощной уличной машины лежит мощный двигатель, созданный для работы.
Сердцем любой мощной уличной машины является мощный двигатель. Без мощной мельницы ваша машина будет показываться и никуда не денется, а это не самая лучшая репутация в местном круизном центре. Но, поскольку многие автомобили поставлялись с завода с тусклым двигателем, вам решать превратить свой уличный крейсер в уличный хулиган.
За прошедшие годы Car Craft написала бесчисленное количество историй о о том, как построить двигатель мощностью в миллион лошадиных сил и как заставить свой V8 выдавать невероятный крутящий момент. Иногда, однако, эти истории слишком технологичны и/или требуют модов, которые слишком дороги для среднего автопроизводителя. Таким образом, эта техническая функция возвращается к основам. В нем содержится широкий спектр процедур сборки двигателя, советы и рекомендации по сборке. Данный совет является общей информацией и применим к большинству двигателей V8 американского производства, выпускаемых крупными производителями автомобилей, такими как Chevrolet, Chrysler/Dodge, Ford, Buick, Olds и Pontiac.
Важно помнить, что успешная сборка вашего первого двигателя — это не ракетостроение — это просто вопрос тщательной работы и пристального внимания к деталям. Просто помните, что если у вас есть вопрос по сборке или вы не уверены в спецификации крутящего момента, не гадайте, найдите правильный ответ. Разнообразные источники могут дать ответы на ваши вопросы по двигателестроению. Их можно найти в таких местах, как страницы журнала Car Craft, руководство по двигателю (например, Chilton) для автомобиля того года/типа, которым вы владеете, или связавшись с производителем соответствующей детали. Например, если вы не знаете, как отрегулировать зазор клапана на вашем новом уличном/полосном распределительном валу, позвоните в службу технической поддержки компании-производителя кулачков и спросите у компании из первых рук. Задавание вопросов первым помогает исключить ошибки, потраченное впустую время и потраченные впустую деньги.
Суть сборки вашего первого двигателя в том, чтобы сделать это правильно . Помните, что если вы не собираете двигатели каждый день, чтобы зарабатывать на жизнь, вам, вероятно, потребуется больше времени, чтобы собрать двигатель, чем в гоночной мастерской. Тем не менее, приз самому быстрому моторостроителю не присуждается, так что не торопитесь. Выделите один день на построение нижней части. Затем вернитесь на следующий день (с ясным умом и новым энтузиазмом), чтобы установить кулачок, головки и коромысла. Разделение процесса сборки двигателя делает проект (и весь ваш проект по сборке дорожных машин) простым и приятным. В конце концов, вся цель проекта маслкара — развлекаться.
А пока ознакомьтесь с сопровождающими фотографиями и подписями, а также A-B-C сборки двигателя в списке и галерее изображений ниже.
- Шаг 1: Выбирайте бюджетные, надежные модификации, обеспечивающие отличные дорожные характеристики.
- Шаг 2: Решите, как ваш автомобиль будет управляться большую часть времени, и соответственно выберите компоненты двигателя.
- Шаг 3: Проведите хонингование блока цилиндров с установленной нажимной пластиной, если это возможно.
- Шаг 4: Простой процесс механической обработки в домашних условиях включает в себя нарезание резьбы во всех отверстиях под болты на блоке цилиндров.
- Этап 5. Декинг блока цилиндров позволяет получить более ровную и плоскую поверхность деки, что способствует лучшей герметизации цилиндра
- Шаг 6: Промойте блок цилиндров, коленчатый вал и шатуны мыльной водой.
- Шаг 7: Покрасьте блок цилиндров снаружи высокотемпературной краской.
- Шаг 8: Выровняйте-расточите главный блок цилиндров.
- Шаг 9: Затяните болты основной крышки постепенно в правильной последовательности с помощью динамометрического ключа профессионального качества.
- Шаг 10: Установите коренной подшипник в блок цилиндров насухо.
- Шаг 11: Осторожно установите коленчатый вал на место, стараясь не повредить коренные подшипники.
- Шаг 12: Чтобы правильно совместить распределительный вал с коленчатым валом, совместите шестерни цепи привода ГРМ так, чтобы две маленькие точки находились рядом друг с другом.
- Шаг 13: При использовании гидравлического распределительного вала затяните гайку коромысла до нулевого зазора, а затем затяните гайку еще на один оборот.
- Шаг 14: Установите циферблатный индикатор в отверстие подъемника (этот циферблатный индикатор плотно удерживается в отверстии подъемника с помощью уплотнительных колец, установленных на валу индикатора).
- Шаг 15: Для регулировки распределительного вала начните с использования циферблатного индикатора (стрелка A), чтобы определить, когда поршень № 1 находится в верхней мертвой точке (ВМТ). Затем установите градусное колесо (стрелка B) на конец коленчатого вала. Установите градусный указатель кулачка (стрелка C) так, чтобы он совпал с нулевой отметкой на градусном колесе.
- Шаг 16: Всегда покупайте высококачественные прокладки от известного производителя.
- Шаг 17: Чтобы убедиться, что датчик остается надежно прикрепленным к масляному насосу (и в надлежащей фазе с ним), приварите их прихваточным швом.
- Шаг 18: Первые 10 минут обкатки двигателя являются самыми важными. Поддерживайте обороты двигателя в диапазоне 2000–2500 об/мин и постоянно контролируйте состояние двигателя (например, давление топлива и масла, а также угол опережения зажигания).
- Шаг 19: Настоятельно рекомендуется отбалансировать детали двигателя, составляющие вращающийся узел.
Страницы трендов
Лучшие электромобили — самые популярные модели электромобилей
Сколько стоит Tesla? Вот разбивка цен
Лучшие гибридные автомобили — модели гибридных автомобилей с самым высоким рейтингом
Все электрические внедорожники, которые можно купить в США в 2022 году
Это самые экономичные пикапы 90 90 82
Это внедорожники с лучшим расходом топлива
Популярные страницы
Лучшие электромобили — модели электромобилей с самым высоким рейтингом
Сколько стоит Tesla? Вот разбивка цен
Лучшие гибридные автомобили — модели гибридных автомобилей с самым высоким рейтингом
Все электрические внедорожники, которые можно купить в США в 2022 году
Это самые экономичные пикапы 90 90 82
Это внедорожники с лучшим расходом топлива
Как собрать автомобильный двигатель с нуля
by Cayden Conor
Создание автомобильного двигателя с нуля требует времени и терпения, и вам также понадобится опытный механик, который поможет вам в этом предприятии. Сборка автомобильного двигателя — это больше, чем просто сборка деталей. В зависимости от типа двигателя существуют различные допуски и настройки момента затяжки гаек и болтов, которые необходимо соблюдать. Существует множество различных типов двигателей, хотя наиболее популярными являются 4-цилиндровые, 6-цилиндровые и 8-цилиндровые двигатели. Двигатели также подразделяются на типы, например, карбюраторные или инжекторные, с одним верхним распредвалом, с двойным верхним распредвалом или обычные распредвалы. Независимо от того, какой у вас двигатель, все основные компоненты одинаковы.
Шаг 1
Замочите новые подъемники в моторном масле не менее чем на пять или шесть часов, а лучше на ночь. Пока подъемники замачиваются, выровняйте все новые детали, гайки и болты и возьмите все инструменты, которые вам понадобятся, чтобы собрать двигатель.
Шаг 2
Установите поршневые кольца на поршень. Убедитесь, что вы расположили три кольца в шахматном порядке. Если разрыв колец выровнен, у вас будет прорыв газов при запуске двигателя, и двигатель будет постоянно жечь масло. Нанесите на стенки цилиндра средство для обработки масла STP. Хотя можно использовать и другие масла, STP является густым и вязким и прилипает к стенкам не только во время работы, но и остается там во время запуска, защищая новый двигатель до тех пор, пока масляный насос не сможет все правильно смазать.
Шаг 3
Переверните блок и установите верхнюю половину коренных и шатунных подшипников. Покройте видимую сторону всех подшипников средством для обработки масла STP. Установите кривошип на место, затем установите нижнюю половину коренных подшипников на крышки подшипников. Установите крышки коренных подшипников. Это удержит кривошип. Обратитесь к руководству по эксплуатации двигателя, чтобы узнать расстояние между коренными подшипниками и значения крутящего момента.
Шаг 4
Переверните блок обратно. Поместите шатунные подшипники в крышки подшипников на одном из узлов поршень-шток. Обратите внимание на маркировку на верхней части поршня. Это скажет вам, в какую сторону поршень входит в блок. С помощью компрессора для колец вдавите кольца в поршень и установите поршень в отверстие цилиндра. Аккуратно постучите по поршню резиновым молотком, пока он не войдет в отверстие. Направляйте шток на шейку кривошипа, постукивая по поршню. Повторите этот шаг для семи других поршней.
Шаг 5
Переверните блок. Поместите подшипники в крышки подшипников, нанесите на них масло для обработки STP и установите в соответствии со спецификациями двигателя по зазору и крутящему моменту. Установите распределительный вал. Покройте кулачок пропиткой маслом STP, затем осторожно вставьте распределительный вал в блок. Обязательно установите кнопку кулачка. Установите цепь привода ГРМ и крышку привода ГРМ. Перед установкой крышки убедитесь, что установочные метки правильно совмещены. Установите масляный насос и масляный поддон.
Шаг 6
Установите подъемники. Они уже должны быть покрыты маслом, так как к этому моменту должны были впитаться. Покрасьте впускной камбуз. Это поможет маслу легче скользить обратно в блок. Установите головы. Убедитесь, что прокладки головки находятся на месте и не закрывают ни одно из отверстий в водяной рубашке. Они работают только в одном направлении, но могут подходить для нескольких способов. Затяните головки, используя спецификации для двигателя конкретного года, над которым вы работаете.
Шаг 7
Установите толкатели и коромысла. Обратитесь к руководству по эксплуатации двигателя, чтобы узнать характеристики крутящего момента на коромыслах. Если вы строите двигатель с верхним расположением распредвала, вам не нужно будет устанавливать толкатели. Кулачок кулачка давит прямо на подъемник. Установите впускной коллектор. Вы можете нанести немного RTV на прокладки, чтобы удерживать их на месте.
Шаг 8
Установите крышки клапанов. Теперь вы готовы установить двигатель в машину. Остальные аксессуары (топливный насос, карбюратор и распределитель или система впрыска топлива) могут быть установлены после того, как двигатель будет надежно закреплен болтами в моторном отсеке.
Вещи, которые вам понадобятся
- Набор 1/4-дюймовых головок (метрических и стандартных)
- 1/4-дюймовый храповик
- 1/4-дюймовый пневматический пистолет
- 1/4-дюймовые удлинители различных длины
- Набор головок 1/2 дюйма (метрических и стандартных)
- Трещотка 1/2 дюйма
- Пневматический пистолет 1/2 дюйма
- Удлинители 1/2 дюйма различной длины
- Набор из 3 шт. 8-дюймовые торцевые головки (метрические и стандартные)
- 3/8-дюймовая трещотка
- 3/8-дюймовый воздушный пистолет
- 3/8-дюймовый удлинитель различной длины
- Установка поршня
- Обычная отвертка
- Phillips Outdriver
- Набор стандартных рычагов
- Set of Metriver
- 70028
- Set of Metriver
- 77778
- Set of Metriver
- 77778
- Set of Metriver
- 70028 70028
- Set of Metriver
- 70028 70028
- . плоскогубцы
- Разводной ключ
- Подъемник двигателя
- Стенд двигателя
Writer Bio
Cayden Conor пишет с 1996 года.