Как реализовать модули C ++ с возможностью горячей замены?

39

Быстрое итерационное время - ключ к разработке игр, гораздо больше, чем, на мой взгляд, прикольная графика и движки с множеством функций. Неудивительно, что многие маленькие разработчики выбирают языки сценариев.

Способ Unity 3D, позволяющий приостановить игру и изменить ресурсы и код, а затем продолжить и сразу же вступить в силу, абсолютно подходит для этого. Мой вопрос: кто-нибудь реализовывал подобную систему на игровых движках C ++?

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

Очевидно, что будут компромиссы, и я не могу себе представить, что вы можете просто перекомпилировать свой код, пока игра находится в режиме паузы, перезагружать его и работать при любых обстоятельствах или на любой платформе.

Но, возможно, это возможно для программирования ИИ и простых логических модулей уровня. Это не так, как если бы я хотел сделать это в любом проекте в краткосрочной (или долгосрочной) перспективе, но мне было любопытно.

Злой Энгель
источник

Ответы:

26

Это определенно возможно сделать, но не с вашим типичным кодом C ++; вам нужно создать библиотеку в стиле C, которую можно динамически связывать и перезагружать во время выполнения. Чтобы сделать это возможным, библиотека должна содержать все состояния в непрозрачном указателе, которые могут быть предоставлены библиотеке после перезагрузки.

Тимоти Фаррар обсуждает свой подход:

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

Джейсон Козак
источник
Я предпочитаю это ответу Блэр, так как вы на самом деле отвечаете на вопрос.
Джонатан Дикинсон
15

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

Волосатые, стеснительные вещи. Лучше использовать язык сценариев для кода, который нуждается в быстрой итерации.

На работе наша текущая кодовая база представляет собой смесь C ++ и Lua. Lua тоже не маленькая часть нашего проекта - это почти 50/50 раскол. Мы осуществили перезагрузку Lua на лету, так что вы можете изменить строку кода, перезагрузить и продолжать работу. Фактически, вы можете сделать это, чтобы исправить ошибки сбоев, возникающие в коде Lua, без перезапуска игры!

Блэр Холлоуэй
источник
3
Другим важным моментом является то, что даже если вы не намерены держать систему в скрипте, если вы ожидаете многократного повторения на раннем этапе, все же может быть вполне разумно начать с Lua, а затем перейти к C ++ в будущем, когда вам понадобится дополнительная производительность и скорость итераций замедлились. Очевидным недостатком здесь является то, что вы в конечном итоге портируете код, но часто правильная логика - сложная часть - код почти записывается сам, как только вы разберетесь с этой частью.
Логан Кинкейд
15

(Возможно, вы захотите узнать о термине «исправление обезьяны» или «штамповка утки», если не по каким-либо иным причинам, кроме юмористического образа мыслей.)

И это в стороне: если ваша цель - уменьшить время итерации для изменений «поведения», попробуйте несколько подходов, которые помогут вам в этом, и хорошо сочетаются, чтобы в будущем это стало возможным.

(Это будет немного касательно, но я обещаю, что это вернется!)

  • Начните с данных и начните с малого: перезагрузите на границах («уровнях» и т. П.), Затем перейдите к использованию функциональности ОС для получения уведомлений об изменениях файлов или просто регулярно проводите опросы .
  • (Для получения бонусных баллов и меньшего времени загрузки (опять же, уменьшая время итерации) посмотрите на запекание данных .)
  • Скрипты являются данными и позволяют вам повторять поведение. Если вы используете язык сценариев, у вас теперь есть уведомления / возможность перезагрузить эти сценарии, интерпретированные или скомпилированные. Вы также можете подключить ваш переводчик к игровой консоли, сетевому сокету и т. П. Для повышения гибкости во время выполнения.
  • Код также может быть данными : ваш компилятор может поддерживать оверлеи , общие библиотеки, библиотеки DLL и т.п. Таким образом, теперь вы можете выбрать «безопасное» время для выгрузки и перезагрузки оверлея или DLL, как ручного, так и автоматического. Другие ответы подробно здесь. Обратите внимание, что некоторые варианты этого могут мешать криптографической подписи подписи, биту NX (без выполнения) или подобным механизмам безопасности.
  • Рассмотрим глубокую версионную систему сохранения / загрузки . Если вы можете надежно сохранять и восстанавливать свое состояние даже перед лицом изменений кода, вы можете закрыть свою игру и перезапустить ее с новой логикой в ​​той же точке. Проще сказать, чем сделать, но это выполнимо, и это заметно проще и более портативно, чем перетаскивание памяти для изменения инструкций.
  • В зависимости от структуры и детерминизма вашей игры вы можете выполнять запись и воспроизведение . Если эта запись находится только над «игровыми командами» (например, карточная игра), вы можете изменить весь код рендеринга и воспроизвести запись, чтобы увидеть ваши изменения. Для некоторых игр это так же просто, как записать некоторые начальные параметры (например, случайное начальное число), а затем действия пользователя. Для некоторых это намного сложнее.
  • Приложите усилия, чтобы сократить время компиляции . В сочетании с вышеупомянутыми системами сохранения / загрузки или записи / воспроизведения, или даже с оверлеями или библиотеками DLL, это может уменьшить ваш оборот больше, чем любая другая вещь.

Многие из этих моментов полезны, даже если вы не полностью перезагрузите данные или код.

Вспомогательные анекдоты:

На большом ПК RTS (команда ~ 120 человек, в основном C ++) существовала невероятно глубокая система сохранения состояния, которая использовалась как минимум для трех целей:

  • «Неглубокое» сохранение было передано не на диск, а на механизм CRC, чтобы многопользовательские игры оставались в режиме симуляции с блокировкой по одному CRC каждые 10-30 кадров; это гарантировало, что никто не изменял и через несколько кадров обнаружил ошибки рассинхронизации
  • Если и когда произошла ошибка рассинхронизации в многопользовательском режиме, было выполнено сверхглубокое сохранение в каждом кадре, которое снова передавалось в механизм CRC, но на этот раз механизм CRC генерировал бы много CRC, каждый для меньших пакетов байтов. Таким образом, он может точно сказать, какая часть состояния начала расходиться в последнем кадре. Мы обнаружили неприятную разницу в «режиме с плавающей запятой по умолчанию» между процессорами AMD и Intel, использующими это.
  • Обычное сохранение глубины может не сохранять, например, точный кадр анимации, в котором играл ваш юнит, но оно получит положение, здоровье и т. Д. Всех ваших юнитов, что позволит вам сохранять и возобновлять игру в любое время в течение игрового процесса.

С тех пор я использовал детерминированную запись / воспроизведение в C ++ и Lua карточной игре для DS. Мы подключились к API, который мы разработали для AI (на стороне C ++), и записали все действия пользователя и AI. Мы использовали эту функциональность в игре (чтобы обеспечить воспроизведение для игрока), но также и для диагностики проблем: когда происходил сбой или странное поведение, все, что нам нужно было сделать, это получить файл сохранения и воспроизвести его в отладочной сборке.

С тех пор я также использовал оверлеи несколько раз, и мы объединили их с нашей системой «автоматически разбрасывать этот каталог и загружать новый контент в карманный компьютер». Все, что нам нужно сделать, это оставить кат-сцену / уровень / что угодно и вернуться обратно, и будут загружены не только новые данные (спрайты, разметка уровней и т. Д.), Но и любой новый код в оверлее. К сожалению, с более новыми карманными компьютерами становится все труднее из-за механизмов защиты от копирования и защиты от взлома, которые обрабатывают код специально. Мы все еще делаем это для сценариев Lua, хотя.

И последнее, но не менее важное: вы можете (и я имею в виду, в различных очень маленьких особых обстоятельствах) сделать небольшую пробивку утки, непосредственно исправляя коды операций. Это работает лучше всего, если вы работаете на фиксированной платформе и компиляторе, и, поскольку он почти не поддерживается, очень подвержен ошибкам и ограничен в том, что вы можете быстро выполнить, я в основном использую его только для перенаправления кода во время отладки. Тем не менее, он очень быстро научит вас многому об архитектуре набора команд.

Leander
источник
6

Вы можете сделать что-то вроде горячей замены модулей во время выполнения, реализуя модули в виде динамических библиотек (или общих библиотек в UNIX), и используя dlopen () и dlsym () для динамической загрузки функций из библиотеки.

Для Windows эквивалентами являются LoadLibrary и GetProcAddress.

Это метод C, и у него есть некоторые подводные камни при использовании его в C ++, вы можете прочитать об этом здесь .

Матиас Вальденегро
источник
это руководство - ключ к созданию библиотек с возможностью горячей замены, спасибо
JqueryToAddNumbers
4

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

  1. Изменение функций обратного вызова не будет работать должным образом. E & C работает, добавляя новый фрагмент кода и изменяя таблицу вызовов, чтобы указывать на новый блок. Для обратных вызовов и всего, что использует указатели функций, все равно будут выполняться старые, неизмененные вызовы. Чтобы это исправить, вы можете использовать функцию обратного вызова оболочки, которая вызывает статическую функцию.
  2. Изменение заголовочных файлов почти никогда не будет работать. Это предназначено для изменения фактических вызовов функций.
  3. Различные языковые конструкции будут вызывать загадочные ошибки. По моему личному опыту, предварительные объявления таких вещей, как enums, часто делают это.

Я использовал Edit и Continue, чтобы значительно улучшить скорость таких вещей, как итерация пользовательского интерфейса. Допустим, у вас есть работающий пользовательский интерфейс, полностью построенный на коде, за исключением того, что вы случайно поменяли порядок прорисовки двух блоков, чтобы вы ничего не видели. Изменяя 2 строки кода в реальном времени, вы можете сэкономить себе 20-минутный цикл компиляции / сборки / тестирования, чтобы проверить тривиальное исправление пользовательского интерфейса.

Это НЕ полное решение для производственной среды, и я обнаружил, что лучшее решение - это перенести как можно большую часть вашей логики в файлы данных, а затем сделать эти файлы данных перезагружаемыми.

Бен Зейглер
источник
какой цикл компиляции занимает 20 минут?!? я бы лучше застрелился
JqueryToAddNumbers
3

Как уже говорили другие, это сложная проблема, динамически связывая C ++. Но это решенная проблема - вы, возможно, слышали о COM или одном из маркетинговых имен, которые применялись к нему на протяжении многих лет: ActiveX.

С точки зрения разработчика, COM имеет немного дурную славу, потому что может потребоваться много усилий для реализации компонентов C ++, которые раскрывают их функциональность, используя его (хотя это облегчается с помощью ATL - библиотеки шаблонов ActiveX). С точки зрения потребителя у него плохое имя, потому что приложения, использующие его, например, для встраивания электронной таблицы Excel в документ Word или диаграммы Visio в электронную таблицу Excel, имели тенденцию довольно часто падать в тот же день. И это сводится к тем же проблемам - даже несмотря на все рекомендации, которые предлагает Microsoft, COM / ActiveX / OLE было / трудно сделать правильно.

Подчеркну, что технология COM сама по себе не является плохой по своей сути. Прежде всего, DirectX использует COM-интерфейсы для демонстрации его функциональности, и это работает достаточно хорошо, как и множество приложений, которые встраивают Internet Explorer, используя его элемент управления ActiveX. Во-вторых, это один из самых простых способов динамического связывания кода C ++ - интерфейс COM по сути является просто виртуальным классом. Хотя у него есть IDL, такой как CORBA, вы не обязаны его использовать, особенно если определенные вами интерфейсы используются только в вашем проекте.

Если вы не пишете для Windows, не думайте, что COM не стоит рассматривать. Mozilla повторно реализовала его в своей кодовой базе (используемой в браузере Firefox), потому что им нужен был способ компонентизировать свой код C ++.

U62
источник
2

Там же реализация выполнения скомпилированных C ++ для игровой код здесь . Кроме того, я знаю, что есть по крайней мере один проприетарный игровой движок, который делает то же самое. Это сложно, но выполнимо.

Лоран Кувиду
источник