Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Для создания игры наподобие Destiny требуется много командной работы и мастерства. У нас есть талантливые люди во всех областях знаний, однако было непросто достичь уровня координации, необходимого для создания игры масштаба Destiny.
Это похоже на то, как если бы группе людей выдали кисть и один холст, надеясь получить в результате качественный портрет. Чтобы создать нечто, отличное от хаоса, необходимо договориться о правилах. Например, выбрать палитру, размеры кистей, используемые в разных ситуациях, да и само содержимое картины. Достичь такой согласованности в команде невероятно важно.
В сфере разработки одним из способов достижения согласованности являются инструкции (гайдлайны) по кодингу: правила, которые соблюдают наши инженеры, чтобы обеспечивать возможность поддержки кодовой базы. Сегодня я расскажу о том, как мы определились с тем, какие гайдлайны нам нужны и как они помогают в решении проблем, с которыми сталкивается крупная студия.
В этой статье мы делаем упор на разработке игр и языке программирования C++, но даже если вы не знаете C++ и не работаете инженером, она всё равно будет для вас интересной.
Что такое гайдлайн по кодингу?
Гайдлайны — это правила, которые соблюдают инженеры при написании кода. Обычно они применяются для задания определённого стиля форматирования, для обеспечения правильного использования системы и чтобы не возникали распространённые проблемы. Хорошо написанный гайдлайн даёт чётко прописанные инструкции в стиле «Сделайте X» или «Не делайте Y», а также объясняет причины включения этого правила в гайдлайн. Вот пара примеров из наших гайдлайнов по C++:
Не используйте ключевое слово static напрямую
Ключевое слово «static» выполняет множество различных задач в C++, в том числе объявляет невероятно опасные статические переменные, локальные для функций.
Вам следует использовать более специфичные ключевые слова-обёртки в cseries_declarations.h, например, static_global, static_local, и т.п. Это позволяет нам эффективно выполнять аудит опасных статических локальных переменных функций.
Фигурные скобки на отдельных строках
Фигурные скобки всегда располагаются на отдельных строках.
Допустимо исключение для однострочных определений встраиваемых функций.
Обратили внимание, что во втором гайдлайне указано исключение? Обычно разработчики должны следовать гайдлайнам, но всегда есть место для исключений, если они приводят к повышению качества кода. Однако основания для таких исключений должны быть достаточно убедительными, например, создание объективно более чистого кода или обход конкретного пограничного случая системы, который иным способом не устранить. Если такое встречается часто и ситуация чётко определена, то мы добавляем в гайдлайн официальное исключение.
Давайте воспользуемся примером из повседневной жизни. В США самое частое правило, которое нужно соблюдать при вождении — ехать по правой стороне дороги. Люди ездят так почти всегда. Но на узкой сельской дороге, где мало машин, чаще всего нанесена пунктирная разделительная полоса, означающая, что разрешено переместиться на левую сторону дороги, чтобы обогнать медленную машину. Исключение из правила!
Но даже если у вас есть множество хорошо написанных и продуманных гайдлайнов, как сделать так, чтобы люди им следовали? В компании Bungie основным инструментом для применения гайдлайнов является анализ кода (code review). При анализе кода ты показываешь изменения в своём коде коллегам-инженерам, и они дают отзывы о нём, прежде чем ты поделишься им с остальной частью команды. Это похоже на то, как мою статью перед публикацией вычитывали другие люди в поисках грамматических ошибок или неуклюжих предложений. Анализ кода — прекрасный инструмент обеспечения соответствия гайдлайнам, распространения знаний о системе и предоставления проверяющим и проверяемым возможности находить баги до того, как они произойдут, благодаря чему они не влияют на здоровье кодовой базы и команды.
Также можно проверять свой код инструментами или автоматически исправлять его при наличии любых легко выявляемых нарушений гайдлайнов, которые обычно связаны с форматированием или правильным использованием языка программирования. К сожалению, эта система для нашей кодовой базы на C++ пока не готова, поскольку у нас есть специальная разметка, которую мы используем для рефлексии типов и аннотирования метаданных, а их готовый инструмент распознать не может. Но мы работаем в этом направлении!
В принципе, мы вкратце описали механику написания гайдлайнов и работы с ними. Однако мы не рассмотрели самый важный аспект: проверку того, что гайдлайны представляют ценность для команды и кодовой базы. Как же мы решаем, что ценно? Давайте для начала рассмотрим некоторые из трудностей, способных усложнить разработку, и будем отталкиваться от них.
Трудности, говорите?
Первая трудность — это язык программирования, который мы используем для разработки игр: C++. Это мощный высокопроизводительный язык, соединяющий в себе современные концепции с олдскульными принципами. Он является одним из самых популярных вариантов для разработки AAA-игр, требующих выполнять максимальное количество вычислений за наименьший объём времени. Такая производительность в основном достигается благодаря тому, что разработчикам предоставляется больше контроля над низкоуровневыми ресурсами, которыми им нужно управлять вручную. И эта великая сила требует от инженеров великой ответственности: ресурсами нужно управлять правильно, а запутанные части языка применять соответствующим образом.
К тому же сейчас наша кодовая база довольно велика, примерно 5,1 миллиона строк кода на C++ для каждой игры. Часть из него — это недавно написанный код, например, код поддержки Cross Play в Destiny. Части этого кода уже двадцать лет, например, коду, проверяющему нажатия клавиш. Часть кода платформозависима для поддержки всех устройств, для которых мы выпускаем игру. А часть кода — это мусор, который необходимо удалить.
Изменения в давно устоявшихся гайдлайнах могут внести расхождения между старым и новым кодом (если, конечно же, мы не можем вложить ресурсы в глобальное переписывание кода), поэтому необходимо соблюдать баланс между любыми изменениями в гайдлайнах и весом уже написанного кода.
У нас ведь не только есть весь этот код, но мы и работаем над несколькими версиями этого кода параллельно! Например, ветвь разработки Season of the Splicer называется v520, а один из наших последних сезонов называется v530. В версии v600 произошли серьёзные изменения для поддержки следующего крупного расширения The Witch Queen. Изменения, сделанные в v520, автоматически интегрируются во все дальнейшие ветви, в v530, а потом и в v600, чтобы разработчики этих ветвей работали с самыми актуальными версиями файлов. Однако такой процесс интеграции может вызывать проблемы, когда один участок кода изменяется в нескольких ветвях и конфликт необходимо разрешать вручную. Или хуже того, часть кода может обеспечивать беспроблемное слияние, но вызывать изменение логики, приводящее к возникновению бага. В гайдлайнах должны присутствовать инструкции, снижающие вероятность возникновения таких проблем.
Кроме того, Bungie — это крупная компания; гораздо крупнее, чем пара студентов колледжа, пищущих игры в комнате общежития в 1991 году. Сейчас на нас работает больше 150 инженеров, и примерно 75 из них регулярно работают над игровым клиентом на C++. Каждый из них — умный, трудолюбивый человек, имеющий собственный опыт и мнение. Такое разнообразие — наша основная сила, и нам нужно пользоваться ею полностью, обеспечив доступность и понятность кода, написанного каждым из разработчиков.
Теперь, когда мы знаем, с какими трудностями сталкивается компания, можно выработать принципы, на которые стоит делать упор в гайдлайнах. Мы в компании Bungie называем эти принципы «бритвами гайдлайнов кодинга на C++» (C++ Coding Guideline Razors).
Бритвами? Теми, которыми бреются?
Вообще да, но не совсем. Понятие бритвы введено потому, что мы используем их для «сбривания» сложности и обеспечения чёткого фокуса на наших задачах (на решении трудностей, о которых мы говорили выше). Все создаваемые нами гайдлайны должны соответствовать одной или нескольким таким бритвам, а если какие-то из них бритвам не соответствуют, то они или вредны, или представляют собой лишнюю умственную нагрузку на команду.
Я расскажу о каждой из таких бритв, к введению которых пришла Bungie, и объясню логику каждой, а также приведу несколько примеров гайдлайнов, обеспечивающих соответствие бритве.
№1 Отдавайте предпочтение понятности ценой времени написания
Каждая строка кода будет прочитана множество раз многими людьми с разным опытом при редактировании её специалистом, поэтому отдавайте предпочтение явному, но подробному, а не краткому, но подразумеваемому.
При внесении изменений в кодовую базу нам чаще всего нужно тратить время на разбор окружающих систем, чтобы убедиться, что наше изменение будет соответствовать им, а уже потом писать новый код или вносить модификацию. Автором окружающего кода может быть член команды, бывший коллега или вы сами три года назад, уже забывший весь контекст. Кем бы он ни был, лучше всего помочь в продуктивности всем будущим читателям кода, сделав его чётким и понятным на момент его первоначального написания, даже если потребуется больше времени на ввод или поиск подходящих слов.
Примеры гайдлайнов Bungie в поддержку этой бритвы:
- Использование стандарта наименования snake_case.
- Избегание аббревиатур (например,
screen_manager
вместоscrn_mngr
) - Поощрение добавления полезных внутристрочных комментариев.
Ниже представлен фрагмент кода UI, демонстрирующий работу этих гайдлайнов. Даже не видя окружающего кода, вы, вероятно, сможете получить представление о том, что он должен делать.
int32 new_held_milliseconds= update_context->get_timestamp_milliseconds() - m_start_hold_timestamp_milliseconds;
set_output_property_value_and_accumulate(
&m_current_held_milliseconds,
new_held_milliseconds,
&change_flags,
FLAG(_input_event_listener_change_flag_current_held_milliseconds));
bool should_trigger_hold_event= m_total_hold_milliseconds > NONE &&
m_current_held_milliseconds > m_total_hold_milliseconds &&
!m_flags.test(_flag_hold_event_triggered);
if (should_trigger_hold_event)
{
// Raise a flag to emit the hold event during event processing, and another
// to prevent emitting more events until the hold is released
m_flags.set(_flag_hold_event_desired, true);
m_flags.set(_flag_hold_event_triggered, true);
}
№2 Избегайте различий, не имеющих разницы
По мере возможности и без потери обобщённости снижайте мыслительную нагрузку, устраняя избыточные и произвольные альтернативы.
Эта и последующая бритва идут рука об руку; обе они связаны с нашей способностью выявлять различия. Можно написать определённое поведение в коде множеством разных способов, и иногда разница между ними не важна. Когда такое происходит, лучше устранить из кодовой базы возможность такого различия, чтобы читателям не нужно было определять её. Чтобы связать несколько фрагментов с одной концепцией, требуются мыслительные усилия, поэтому устранив эти ненужные различия, мы можем упростить читателю выявление паттернов кода и понимание кода с первого взгляда.
Печальный пример такого различия — использование для отступов табулатур или пробелов. В конечном итоге, не важно, что выберете вы, но этот выбор нужно сделать, чтобы избежать кода со смешанным форматированием, который очень быстро может стать нечитаемым.
Примеры гайдлайнов Bungie по кодингу, поддерживающих эту бритву:
- Используйте грамматику американского английского (например, «color» вместо «colour»).
- В общем случае используйте постинкремент (
index++
, а не++index
). *
и&
должны располагаться рядом с именем переменной, а не именем типа (int32 *my_pointer
, а неint32* my_pointer
).- Различные правила о пробелах и высокоуровневом упорядочивании кода в пределах файла.
№3 Практикуйте визуальную целостность
Используйте визуально отличающиеся паттерны, чтобы передать сложность и указать на опасности.
Это обратная сторона предыдущей бритвы, в ней мы хотим, чтобы различия, указывающие на важную концепцию, сильно выделялись. Это помогает читателям кода при отладке, чтобы они могли видеть элементы, стоящие их внимания при выявлении проблем.
Вот пример того, как мы делаем что-то реально заметным. В языке C++ можно использовать препроцессор для устранения частей кода из компиляции в зависимости от того, собираем ли мы версию игры для внутреннего использования, или нет. Обычно в игру встроено множество отладочных средств, которые необязательно оставлять в выпускаемой версии, поэтому они удаляются, когда выполняется компиляция игры для розничной продажи. Однако мы хотим гарантировать, чтобы код выпускаемой игры случайно не был помечен как код внутренней версии, в противном случае возникнут баги, проявляющиеся только в среде розничной версии. А их устранять не очень весело.
Мы справляемся с этим, делая директивы препроцессора C++ по-настоящему очевидными. Мы используем для нужных переключателей имена в верхнем регистре и выравниваем по левому краю все команды препроцессора, чтобы они выделялись на фоне остального кода. Вот пример того, как это выглядит:
void c_screen_manager::render()
{
bool ui_rendering_enabled= true;
#ifdef UI_DEBUG_ENABLED
const c_ui_debug_globals *debug_globals= ui::get_debug_globals();
if (debug_globals != nullptr && debug_globals->render.disabled)
{
ui_rendering_enabled= false;
}
#endif // UI_DEBUG_ENABLED
if (ui_rendering_enabled)
{
// ...
}
}
Примеры гайдлайнов Bungie по кодингу, поддерживающие эту бритву:
- Фигурные скобки всегда должны находиться в отдельной строке, чётко обозначая вложенную логику.
- Верхний регистр для символов препроцессора (например,
#ifdef PLATFORM_WIN64
). - Отсутствие пробела слева от оператора присваивания, чтобы отличить его от оператора сравнения (например,
my_number= 42
иmy_number == 42
). - Использование операторов указателей (
*
/&
/->
) вместо ссылок для указания на косвенную адресацию памяти.
№4 Избегайте сбивающих с толку абстракций.
При сокрытии сложности выделяйте характеристики, которые важно понимать заказчику.
В жизни мы постоянно используем абстракции, чтобы снижать сложность при донесении каких-то концепций. Вместо того, чтобы сказать: «Мне нужна тарелка с двумя кусками хлеба один поверх другого, между которыми находятся несколько слоёв ветчины и сыра», мы обычно говорим «Я хочу сэндвич с ветчиной и сыром». Сэндвич — это абстракция распространённого вида еды.
Естественно, что мы активно используем абстракции в коде. Функции оборачивают набор команд именем, параметрами и выходными значениями, чтобы их можно было легко многократно использовать в кодовой базе. Операторы позволяют нам выполнять работу в сжатом и хорошо читаемом виде. Классы объединяют данные и функциональность в модульный блок. Именно благодаря абстракциям мы пользуемся сегодня языками программирования, а не пишем приложения в сырых машинных опкодах.
Однако иногда абстракция может сбивать с толку. Если попросить у кого-нибудь сэндвич, существует вероятность того, что вы получите хот-дог или кесадилью, в зависимости от того, как человек интерпретирует понятие сэндвича. Аналогично и абстракции в коде можно использовать неправильно, что приводит к путанице. Например, операторы с классами можно переопределять и связывать с любой функциональностью, но считаете ли вы ясным, что
m_game_simulation++
соответствует вызову покадровой функции обновления для состояния симуляции? Нет! Это сбивающая с толку абстракция и в правильном виде она должна выглядеть примерно как m_game_simulation.update()
, чтобы чётко сообщать о своём предназначении.Цель этой бритвы — избегать использования нестандартных абстракций, чтобы назначение создаваемых абстракций было чётким и понятным. Это реализуется при помощи подобных гайдлайнов:
- Используйте стандартизированные префиксы переменных и типов для их быстрого распознавания.
- например:
c_
для обозначения классовых типов иe_
для перечислений (enum). - например:
m_
для переменных-членов,k_
для констант.
- например:
- Никакой перегрузки операторов для нестандартной функциональности.
- Имена функций должны иметь очевидный смысл.
- например:
get_blank()
должна иметь тривиальные затраты. - например:
try_to_get_blank()
может завершиться неудачно, но это поведение должно быть корректно. - например: ожидается, что
compute_blank()
илиquery_blank()
должны иметь нетривиальные затраты.
- например:
№5 Отдавайте предпочтение паттернам, повышающим надёжность кода.
Желательно снизить вероятность того, что изменения в будущем (или конфликтующее изменение в другой ветви) приведут к появлению неочевидного бага. Желательно упрощать поиск багов, так как мы тратим гораздо больше времени на расширение и отладку, чем на реализацию.
Просто пиши совершенно логичный код, и багов не возникнет. Всё просто, так ведь?
Ну… на самом деле, не совсем. Многие трудности, о которых мы говорили ранее, сильно повышают вероятность возникновения бага, и иногда во время разработки просто можно что-то проглядеть. Ошибки случаются, и это нормально. К счастью, есть различные способы создания кода, которыми мы рекомендуем пользоваться для снижения вероятности появления багов.
Один из способов — увеличение количества валидации состояний, выполняемого во время выполнения; это гарантирует, что предложения инженера о поведении системы окажутся истинными. Мы в компании Bungie предпочитаем использовать для этого assert. Assert — это функция, которая просто проверяет истинность определённого условия, и если оно неистинно, то игра совершает контролируемый аварийный выход. Этот сбой можно немедленно отладить на рабочей станции инженера или загрузить в нашу систему TicketTrack с описанием assert, стеком вызова функций и файлом дампа для дальнейшего исследования. Кроме того, большинство assert вырезаются из розничной версии игры, потому что работа с игрой внутри компании и тестирование отдела QA подтвердило, что сбои по assert не происходят, то есть продаваемая игра не обязана тратить вычислительные ресурсы на эти проверки.
Ещё один способ — внедрение практик, способных снизить потенциальный эффект, которое будет иметь изменение в коде. Например, в одном из наших гайдлайнов по C++ допускается только один оператор
return
на каждую функцию. Опасность наличия нескольких операторов return
заключается в том, что при добавлении новых операторов return
в уже имеющуюся функцию вероятна утеря обязательного элемента логики, который расположен ниже в функции. Также это означает, что в будущем разработчики будут вынуждены разбираться во всех точках выхода из функции вместо использования вложенных условных операторов с отступами для визуализации потока выполнения функции. Благодаря тому, что гайдлайн допускает только один оператор return
в конце функции, инженеру приходится создавать условные конструкции для демонстрации ветвления логики внутри функции, что повышает заметность кода, обёрнутого в условные конструкции, и его воздействия.Вот некоторые из гайдлайнов Bungie по кодингу, поддерживающие эту бритву:
- Инициализируйте переменные во время объявления.
- Следуйте принципам правильности const для интерфейсов классов.
- Единственный оператор
return
в конце функции. - Использование assert для валидации состояний.
- Избегайте нативных массивов и используйте наши собственные контейнеры.
№6 Централизуйте управление жизненным циклом.
Распределение управления жизненным циклом по разным системам с разными политиками усложняет правильный выбор при связывании систем и поведений. Вместо этого используйте общий набор инструментов и идиомы, а также по возможности избегайте управления жизненным циклом вашей системы.
Под управлением жизненным циклом в этой бритве в первую очередь подразумевается распределение памяти внутри игры. Один из обоюдоострых мечей языка C++ заключается в том, что управление этой памятью в основном отдаётся в руки инженера. Это означает, что мы можем разрабатывать наиболее эффективные для нас стратегии распределения и использования памяти, но в то же время подразумевает, что мы берём на себя все риски возникновения багов. Неправильное использование памяти может привести к багам, воспроизводимым время от времени и неочевидным образом, и их очень сложно отследить и устранить.
Вместо того, чтобы заставлять каждого инженера искать собственный способ управления памятью в его системе, мы используем уже написанный набор инструментов, для применения которого достаточно просто добавить его в систему. Эти инструменты не только проверены в бою и стабильны, но и содержат в себе функции слежения, чтобы мы могли видеть всю картину использования памяти нашим приложением и обнаруживать проблемные распределения.
Некоторые примеры гайдлайнов Bungie по кодингу, поддерживающие эту бритву:
- Используйте заданные в движке паттерны распределения памяти.
- Не распределяйте память напрямую из операционной системы.
- Избегайте применения Standard Template Library для кода игры.
Подведём итог
Бритвы гайдлайнов помогают выполнять оценку гайдлайнов, чтобы убедиться, что они позволяют нам справляться с трудностями, с которыми мы сталкиваемся при написании кода крупных систем. Наши бритвы:
- Отдавайте предпочтение понятности ценой времени написания
- Избегайте различий, не имеющих разницы
- Практикуйте визуальную целостность
- Избегайте сбивающих с толку абстракций
- Отдавайте предпочтение паттернам, повышающим надёжность кода
- Централизуйте управление жизненным циклом
Кроме того, как вы могли заметить, в формулировках бритв ничего не говорится о специфике C++, и это сделано намеренно. Такие формулировки хороши тем, что в целом они нацелены на создание общей философии написания удобного в поддержке кода. По большей мере они применимы и к другим языкам и фреймворкам, а уже созданные на их основе гайдлайны относятся к конкретным языкам, проектам и культурам команд разработчиков. Если вы инженер, то при выборе гайдлайнов для следующего проекта они будут вам полезны.
Кто отвечает за гайдлайны?
К слову об оценке: кто в компании Bungie отвечает за проверку и утверждение гайдлайнов? Этим занимается наш собственный комитет по гайдлайнам кодинга на C++ (C++ Coding Guidelines Committee). Именно комитет добавляет, изменяет и устраняет гайдлайны в процессе разработки новых паттернов кодинга и функций языков. Комитет состоит из четырёх человек, регулярно обсуждающих изменения, которые утверждаются большинством голосов.
Кроме того, комитет используется в качестве «громоотвода» в случае возникновения споров. Написание кода может быть очень личным процессом с субъективными мнениями, основанными на стилистическом самовыражении или стратегических практиках, что может приводить к достаточно серьёзным спорам о том, что же лучше для кодовой базы. Вместо того, чтобы позволять всему отделу разработки увязать в спорах, теряя в них время и энергию, отправляется запрос комитету, члены которого проверяют и обсуждают его, а затем принимают окончательное авторитетное решение.
Разумеется, иногда даже четырём людям сложно прийти к общему мнению, и именно поэтому бритвы так важны: они дают членам комитета общий фундамент, позволяющий при оценке таких запросов определять, в чём заключается основная ценность гайдлайна.
Достижение согласованности
Как говорилось в начале статьи, согласованность в команде чрезвычайно важна для её эффективности. У нас существуют гайдлайны по кодингу, стимулирующие к согласованности наших инженеров, а также есть бритвы гайдлайнов, помогающие определить, действительно ли гайдлайны направлены на решение проблем, с которыми мы сталкиваемся в студии. Потребность в согласованности масштабируется с ростом студии и кодовой базы, и непохоже, что в обозримом будущем этот рост закончится. Поэтому мы будем итеративно работать над нашими гайдлайнами в процессе возникновения новых трудностей и изменений.