Как избежать жесткого кодирования в игровых движках

22

Мой вопрос не является вопросом кодирования; это относится ко всему дизайну игрового движка в целом.

Как избежать жесткого кодирования?

Этот вопрос намного глубже, чем кажется. Скажем, если вы хотите запустить игру, которая загружает файлы, необходимые для работы, как вы избегаете говорить что-то подобное load specificfile.wadв коде движка? Кроме того, когда файл загружен, как вы не говорите load aspecificmap in specificfile.wad?

Этот вопрос относится практически ко всей конструкции двигателя, и как можно меньше кода должно быть жестко закодировано. Каков наилучший способ достичь этого?

Маркус Крамер
источник

Ответы:

42

Управляемое данными кодирование

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

Как ты находишь aspecificmap? Потому что он находится в файле данных, в котором перечислены идентификаторы карт и их ресурсы на диске.

Должен быть только очень небольшой набор «основных» ресурсов, которые по закону трудно или невозможно избежать жесткого кодирования. Немного поработав, это может быть ограничено одним жестко заданным именем актива по умолчанию, например main.wadили тому подобным. Этот файл потенциально может быть изменен во время выполнения путем передачи аргумента командной строки в игру, иначе game.exe -wad mymain.wad.

Написание кода, управляемого данными, основывается на нескольких других принципах. Например, можно избежать того, чтобы системы или модули запрашивали конкретный ресурс и вместо этого инвертировали эти зависимости. То есть не DebugDrawerзагружайте debug.fontего в код инициализации; вместо этого нужно DebugDrawerвзять дескриптор ресурса в своем коде инициализации. Этот дескриптор может быть загружен из основного файла конфигурации игры.

В качестве конкретных примеров из нашей кодовой базы у нас есть объект «глобальных данных», который загружается из базы данных ресурсов (которая сама по умолчанию является ./resourcesпапкой, но может быть перегружена аргументом командной строки). Идентификатор базы данных ресурсов этих глобальных данных является единственным необходимым жестко запрограммированным именем ресурса в кодовой базе (у нас есть другие, потому что иногда программисты становятся ленивыми, но мы обычно в конечном итоге исправляем / удаляем их). Этот глобальный объект данных полон компонентов, единственной целью которых является предоставление данных конфигурации. Одним из компонентов является компонент глобальных данных пользовательского интерфейса, который содержит дескрипторы ресурсов для всех основных ресурсов пользовательского интерфейса (шрифты, файлы Flash, значки, данные о локализации и т. Д.), А также ряд других элементов конфигурации. Когда разработчик UI решает переименовать главный актив UI от /ui/mainmenu.swfдо/ui/lobby.swfони просто обновляют эту глобальную ссылку на данные; код двигателя не нужно менять вообще.

Мы используем эти глобальные данные для всего. Все игровые персонажи, все уровни, пользовательский интерфейс, аудио, основные ресурсы, конфигурация сети, все. (ну, не все , но эти другие ошибки исправляются.)

Этот подход имеет много других преимуществ. С одной стороны, это делает упаковку и объединение ресурсов неотъемлемой частью всего процесса. Твердо заданные пути в движке также означают, что эти же пути должны быть жестко закодированы в любых сценариях или инструментах, упаковывающих игровые ресурсы, и эти пути затем могут быть не синхронизированы. Вместо этого, опираясь на один основной актив и цепочки ссылок, мы можем создать пакет активов с помощью одной команды, например, bundle.exe -root config.data -out main.wadи знать, что она будет включать все необходимые нам активы. Кроме того, поскольку упаковщик будет просто следовать ссылкам на ресурсы, мы знаем, что он будет включать только те ресурсы, которые нам требуются, и пропустит весь оставшийся пух, который неизбежно накапливается в течение жизни проекта (плюс мы можем автоматически создавать списки этих ресурсов). пух для обрезки).

Хитрый угловой случай всего этого в сценариях. Концептуально создать движок, управляемый данными, легко, но я видел очень много проектов (хобби для AAA), где сценарии считаются данными и, следовательно, «разрешено» просто использовать пути к ресурсам без разбора. Не делай этого. Если файлу Lua нужен ресурс, и он просто вызывает такую ​​функцию, как textures.lua("/path/to/texture.png")у конвейера ресурсов, будет много проблем, зная, что скрипт /path/to/texture.pngдолжен работать правильно, и он может счесть эту текстуру неиспользованной и ненужной. Сценарии должны обрабатываться как любой другой код: любые данные, которые им нужны, включая ресурсы или таблицы, должны быть указаны в записи конфигурации, которую механизм и конвейер ресурсов могут проверять на наличие зависимостей. Данные, которые говорят "загрузить скрипт foo.lua", вместо этого должны сказать "foo.luaи задайте ему эти параметры ", где параметры включают любые необходимые ресурсы. Если сценарий случайным образом порождает врагов, например, передайте список возможных врагов в сценарий из этого файла конфигурации. Затем механизм может предварительно загрузить врагов с уровнем ( так как он знает полный список возможных порождений) и конвейер ресурсов знает, как связать всех врагов с игрой (так как на них окончательно ссылаются данные конфигурации). Если сценарии генерируют строки с именами путей и просто вызывают loadфункцию, то ни движок и конвейер ресурсов не могут каким-либо образом точно знать, какие ресурсы может попытаться загрузить скрипт.

Шон Миддледич
источник
Хороший ответ, очень практичный, а также объясняет подводные камни и ошибки, которые люди делают при реализации этого! +1
WHN
+1. Добавлю, что следование шаблону указания на ресурсы, которые содержат данные конфигурации, также очень полезно, если вы хотите включить моддинг. Так гораздо сложнее и рискованнее модифицировать игры, которые требуют от вас изменения исходных файлов данных, а не создания собственных и указания на них. Еще лучше, если вы можете указать на несколько файлов с определенным порядком приоритета.
Jeutnarg
12

Точно так же вы избегаете жесткого кодирования в общих функциях.

Вы передаете параметры и сохраняете свою информацию в конфигурационных файлах.

В этой ситуации нет абсолютно никакой разницы в разработке программного обеспечения между написанием движка и написанием класса.

MgrAssets
public:
  errorCode loadAssetFromDisk( filePath )
  errorCode getMap( mapName, map& )

private:
  maps[name, map]

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

Оттуда все управляется "главным" файлом конфигурации.

Vaillancourt
источник
1
Да, это плюс какой-то механизм, чтобы ввести пользовательскую логику. Это может быть сделано путем встраивания таких языков, как C #, python и т. Д., Чтобы расширить основные возможности движка с помощью пользовательских функций
qCring
3

Мне нравятся другие ответы, поэтому я буду немного противоречить. ;)

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

«Чистый» подход, основанный на данных, позволил бы запустить исполняемый файл с параметрами (параметрами) командной строки, необходимыми для загрузки исходной конфигурации, но для того, чтобы понять, как интерпретировать эту информацию, необходимо будет кодировать механизм. Например , если ваши файлы конфигурации в формате JSON, вы должны жестко закодировать переменные , искать, например , двигатель должен знать , искать "intro_movies"и "level_list"и так далее.

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

Таким образом, мантра не столько избегает жесткого кодирования, сколько гарантирует, что вы можете вносить изменения с наименьшими усилиями.

В отличие от подхода к файлам данных (который я искренне поддерживаю), может случиться так, что вы можете компилировать данные в свой движок. Если «стоимость» этого ниже, тогда нет никакого реального вреда; если вы работаете над этим только тогда, вы можете отложить обработку файла на более поздний срок и не обязательно испортить себя. Мои первые несколько игровых проектов имели большие таблицы данных, жестко запрограммированные в самой игре, например, список оружия и их различные данные:

struct Weapon
{
    enum IconID icon;
    enum ModelID model;
    int damage;
    int rateOfFire;
    // etc...
};

const struct Weapon g_weapons[] =
{
    { ICON_PISTOL, MODEL_PISTOL, 5, 6 },
    { ICON_RIFLE, MODEL_RIFLE, 10, 20 },
    // etc...
};

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

штрих-кот-бэнг
источник
Не очень сложно разобрать JSON. Единственная вовлеченная «стоимость» - это обучение. (В частности, обучение использованию соответствующего модуля или библиотеки. Например, в Go есть хорошая поддержка json.)
Wildcard
Это не очень сложно, но это требует не только обучения. Например, я знаю, как технически анализировать JSON, я написал парсеры для многих других форматов файлов, но мне нужно было бы либо найти и установить стороннее решение (и выяснить зависимости и как его построить), либо свернуть свое собственное. Занимает больше времени, чем не делает.
Даш-Том-
4
Все занимает больше времени, чем не делать этого. Но необходимые вам инструменты уже написаны. Точно так же, как вам не нужно разрабатывать компилятор для написания игры или возиться с машинным кодом, но вы должны изучать язык для платформы, с которой вы работаете. Итак, научитесь также использовать парсер json.
Wildcard
Я не уверен, каков твой аргумент. В этом ответе я защищаю YAGNI; если вам не нужно тратить / тратить время на то, что вам не поможет, не делайте этого. Если вы хотите потратить время на это, тогда отлично. Может быть, вам придется провести время позже, может быть, вы не будете, но делать это заранее, только отвлекает вас от задачи создания игры. Разработка игр тривиальна; каждая задача, которая входит в создание игры, проста. Просто у большинства игр есть миллион простых задач, и ответственный разработчик выбирает те, которые быстрее всего достигают этой цели.
dash-tom-bang
2
На самом деле, я проголосовал за ваш ответ; нет реального аргумента как такового. Я просто хотел отметить, что JSON не сложно разобрать. Читая еще раз, я полагаю, что я в основном отвечал на фрагмент "но тогда вам нужно выполнить синтаксический анализ и перевод и весь этот джаз". Но я согласен, что для личного проекта игр и подобных, YAGNI. :)
Wildcard