В программировании игры плохие глобальные переменные? - PullRequest
24 голосов
/ 14 мая 2010

Я знаю, что моя внутренняя реакция на глобальные переменные "плохая!" но на двух курсах по разработке игр, которые я проходил в моем колледже, глобальные использовались широко, и теперь я использую учебник по программированию игр DirectX 9 (www.directxtutorial.com). ..? Сайт также рекомендует использовать только структуры, если вы можете при программировании игр, чтобы упростить задачу.

Я действительно запутался в этом вопросе, и все исследования, которые я пытался сделать, очень запутаны. Я понимаю, что существуют проблемы при использовании глобальных переменных (проблемы с многопоточностью, они усложняют обслуживание кода, их состояние сложно отслеживать и т. Д.), Но есть также и издержки, связанные с неиспользованием глобальных переменных, мне придется пройти через loooot очень часто информации, которая может сбивать с толку, и я думаю, что стоит времени, хотя я думаю, что указатели ускорили бы процесс (это мой первый раз, когда я пишу игру на C ++.) В любом случае, я понимаю, что, вероятно, нет «правильного» здесь «неправильный» ответ, поскольку оба способа работают , но я хочу, чтобы мой код был как можно более корректным, чтобы любой ввод был хорошим, большое спасибо!

Ответы [ 12 ]

37 голосов
/ 14 мая 2010

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

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

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

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

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

Сказав все это, я согласен с "Скажи глобалам" пункт, сделанный ниже.

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

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

24 голосов
/ 14 мая 2010

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

Скажи нет глобалам. Всегда.

7 голосов
/ 14 мая 2010

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

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

5 голосов
/ 14 мая 2010

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

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

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

4 голосов
/ 14 мая 2010

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

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

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

3 голосов
/ 14 мая 2010

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

Если у вас есть функция / метод / что-либо, что называется 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 голосов
/ 14 мая 2010

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

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

2 голосов
/ 15 мая 2010

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

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

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

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

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

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

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

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

2 голосов
/ 14 мая 2010

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

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

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

2 голосов
/ 14 мая 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...