В программировании игры плохие глобальные переменные?

24
задан Joe.F 14 May 2010 в 06:31
поделиться

11 ответов

Проблема с играми и глобальными объектами заключается в том, что игры (в настоящее время) распределяются по потокам на уровне движка. Разработчики игр, использующие движок, используют абстракции движка, а не прямое программирование параллелизма (IIRC). Во многих языках высокого уровня, таких как C ++, состояние совместного использования потоков является сложным.Когда многие параллельные процессы совместно используют общий ресурс, они должны убедиться, что не наступают друг другу на ноги.

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

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

Есть исключения в API программирования игр, таких как OpenGL и DX. Если ваши общие данные / глобальные объекты являются указателями на графические контексты DX или OpenGL, то обычно это относится к операциям графического процессора, которые не страдают от тех же проблем.

Просто будь осторожен. Хранить объекты, представляющие «игрока», «зомби» или что-то еще, и делить их между потоками может быть непросто. Вместо этого создайте потоки «player» и потоки «zombie group» и получите надежную абстракцию параллелизма между ними, основанную на передаче сообщений, а не на доступе к состоянию этих объектов через границу потока / критического раздела.

Сказав все это, я согласен с «Скажи нет глобальным объектам» , изложенным ниже.

Подробнее о сложности потоков и общего состояния см .:

1 POSIX Threads API - я знаю, что это POSIX, но дает хорошую идею, которая переводится в другой API
2 Википедия отличный набор статей о механизмах управления параллелизмом
3 Проблема обеденного философа (и многие другие)
4 Руководства и статьи ThreadMentor по многопоточности
5 Еще одна статья Intel, но более того маркетинговой вещи.
6 Статья ACM о создании многопоточных игровых движков

37
ответ дан 28 November 2019 в 22:14
поделиться

Все ответы до сих пор касаются проблемы глобальных / потоков, но я добавлю свои 2 цента в структуру / обсуждение класса (понимается как все публичные атрибуты / частные атрибуты + методы). Предпочтение структур над классами на основании того, что они более простые, соответствует тому же принципу мышления, когда предпочтение отдается ассемблеру над C ++ на том основании, что это более простой язык.

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

Другое распространенное обсуждение это производительность: если мне просто нужно обновить данные, то необходимость вызова метода повлияет на мою производительность. Не совсем. Если методы просты и вы предоставляете их в заголовке как встроенные (внутри тела класса или снаружи с помощью ключевого слова inline ), компилятор сможет скопировать эти инструкции в каждое место использования. Вы получаете гарантию, что компилятор не оставит никаких проверок по ошибке и не повлияет на производительность.

6
ответ дан 28 November 2019 в 22:14
поделиться

Метапроблема здесь - это состояние. От какого состояния зависит та или иная функция и насколько оно изменяется? Затем подумайте, сколько состояний является неявным по сравнению с явным, и сравните это с проверкой и изменением.

Если у вас есть функция / метод / то, что называется DoStuff (), вы не знаете извне, от чего это зависит, что ему нужно и что произойдет с общим состоянием. Если это член класса, вы также не знаете, как изменится состояние этого объекта. Это плохо.

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

У функций-членов класса появляется возможность усугубить некоторые проблемы. Существует огромная разница между

myObject.hitpoints -= 4;
myObject.UpdateHealth();

и

myObject.TakeDamage(4);

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

extern float value_to_sqrt;
value_to_sqrt = 2.0f;
sqrt();  // reads the global value_to_sqrt
extern float sqrt_value; // has the results of the sqrt.

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

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

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

3
ответ дан 28 November 2019 в 22:14
поделиться

Прочитав немного больше того, что вы написали:

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

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

4
ответ дан 28 November 2019 в 22:14
поделиться

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

Скажите "нет" глобальным файлам. Всегда.

24
ответ дан 28 November 2019 в 22:14
поделиться

Если в классе A вам нужен доступ к данным D, вместо того, чтобы устанавливать D глобально, вам лучше поместить в A ссылку на D.

2
ответ дан 28 November 2019 в 22:14
поделиться

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

2
ответ дан 28 November 2019 в 22:14
поделиться

Две конкретные проблемы, с которыми я сталкивался в прошлом:

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

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

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

Если вы попадаете в свое состояние через одну «struct GlobalTable», которая содержит глобальные переменные, или набор методов для объекта или тому подобное, ваш литеральный пул имеет тенденцию быть намного меньше, уменьшая размер Раздел .text в исполняемом файле.

Это в основном касается архитектур с наборами инструкций, которые не могут встраивать цели загрузки непосредственно в инструкции (см., Например, кодирование инструкций ARM фиксированной ширины или Thumb версии 1 на процессорах ARM). Держу пари, даже на современных процессорах вы получите немного меньший кодогенератор.

Кроме того, вдвойне больно, когда кеши инструкций и данных разделены (опять же, как на некоторых процессорах ARM); вы, скорее всего, получите две загруженные строки кэша, тогда как вам может понадобиться только одна с унифицированным кешем. Поскольку литеральный пул может считаться данными и не всегда запускаться на границе строки кэша, одну и ту же строку кэша, возможно, придется загрузить как в i-cache, так и в d-cache.

Это может считаться «микрооптимизацией», но это преобразование, которое относительно легко применить ко всей кодовой базе (например, extern struct GlobalTable {/ * ... * /} the; , а затем заменить все g_foo с the.foo ) ... И мы видели, что размер кода уменьшился от 5 до 10 процентов в некоторых случаях, с соответствующим повышением производительности за счет сохранения чистоты кешей. .

2
ответ дан 28 November 2019 в 22:14
поделиться

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

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

7
ответ дан 28 November 2019 в 22:14
поделиться

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

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

3
ответ дан 28 November 2019 в 22:14
поделиться

Глобалы НЕ являются плохими по своей сути. В C, например, они являются частью нормального использования языка... и поскольку C++ построен на C, им все еще есть место.

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

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

2
ответ дан 28 November 2019 в 22:14
поделиться
Другие вопросы по тегам:

Похожие вопросы: