DTF.RU - Статьи [http://dailytelefrag.ru/articles/]

Обзор лучших
agile-практик применительно к разработке игр

Михаил Дубаков из TargetProcess и Вадим Гайдукевич из Wargaming.net рассказывают о применении в геймдеве практик, связанных с гибкой разработкой ПО. В обзорной статье рассматриваются следующие методики: итеративная разработка, юнит тестирование, разработка через тестирование, рефакторинг, парное программирование, постоянная интеграция.

http://dailytelefrag.ru/articles/read.php?id=47509


Михаил Дубаков
Основатель и CEO TargetProcess, Inc., известной своими разработками по управлению проектами с использованием XP, SCRUM и других Agile методологий.

Вадим Гайдукевич
Разработчик и руководитель проектов в компании Wargaming.net, Inc.

Предисловие

Прежде чем начать читать эту статью, задайте себе всего один вопрос: "Хочется ли мне, чтобы новый проект прошел так же, как предыдущий?"


function bool ShouldIReadTheArticle(bool previousProjectSucceed)
{
	return !previousProjectSucceed;
}

Если входное условие пройдено, начнем.

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

В последнее время разработчики игр все чаще обсуждают и применяют практики, связанные с гибкой разработкой ПО (agile development). Среди наиболее проверенных и работающих практик можно выделить следующие:

  • Итеративная разработка / Iterative Development
  • Юнит тестирование / Unit Testing
  • Разработка через тестирование / Test Driven Development (TDD)
  • Рефакторинг / Refactoring
  • Парное программирование / Pair Programming
  • Постоянная интеграция / Continuous Integration

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

Итеративная разработка и планирование

Цикл разработки приложений известен любому курсанту:

  1. Сбор требований
  2. Планирование
  3. Разработка
  4. Тестирование
  5. Поставка

Типичный проект выглядит примерно так:

  • Сбор требований. Группа заинтересованных лиц собирается и обсуждает проект. Например, пошаговую стратегию. После горячих споров, дюжины ящиков пива и нескольких блоков сигарет рождается диздок, который в какой-то мере описывает будущую игру.
  • Планирование. На работу в срочном порядке нанимается человек, в совершенстве владеющий знаниями о таком сакральном инструменте как MS Project (в игровой компании такого человека обычно нет). В MS Project составляется план проекта с красивой диаграммой Гантта. Такой план очень сильно влияет на инвесторов и помогает выбить деньги под проект. Инвесторы любят красивые диаграммы и толстые документы с описанием требований.
  • Разработка. Когда деньги получены можно заниматься самым интересным - разработкой. Через несколько дней выясняется, что план недостаточно детален для текущих задач и его надо постоянно уточнять и дополнять. План проекта устаревает примерно через полтора месяца, когда желание переделывать его окончательно растворяется в рутине управления проектом. К середине проекта план заменяется документом в Excel, который просто содержит все необходимые требования с оценкой, датой выпуска и ответственными за исполнение. К концу разработки Excel документ также исчезает из процесса и заменяется феноменальной памятью управляющего проектом и прочих руководящих работников.
  • Тестирование. Если в компании есть здравомыслящие люди, то к великому счастью конечных пользователей у нее есть отдел контроля качества (QA - quality assurance). Когда большинство руководящих работников соглашается, что вроде бы все требования реализованы, проект отдается в отдел QA для тщательного тестирования и исправления ошибок. Тестирование и исправление ошибок занимает времени столько же, сколько разработка, потому что именно сейчас выясняется и уточняется огромное количество деталей.
  • Поставка. Альфа-версия вылетает не оттестированой. Бета-версия выбегает не оттестированной. Демо-версии выходят не оттестированными. Версия к выставке выходит не оттестированной. Голд-мастеру везет больше, и он выползает оттестированным и в принципе довольно стабильным, если конечно QA отдел в компании работает. Голд-мастер никогда не выходит вовремя, особенно если эта дата отражена в первоначальном плане в MS Project.

Вот типичная схема "водопадного" процесса (Waterfall).

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

  1. Короткий цикл разработки / Short development life-cycle
  2. Фиксированные интервалы / Time Boxing

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

Второй принцип требует, чтобы итерации были одинаковой длины. Например, итерации могут длиться один месяц. Первая итерация - 1 месяц, вторая итерация - 1 месяц и так далее. Совершенно необходимо через месяц выпустить продукт, который можно проинсталлировать и показать конечному пользователю. Фиксирование длины итерации дает огромное преимущество для контроля за ходом проекта. Если в первой итерации команда выполнила объем работ в 200 идеальных дней, то во второй итерации объем работ будет примерно таким же. Так что уже после первых двух итераций есть представление о том, какая скорость разработки и когда продукт будет выпущен.

Типичный проект с использованием итеративной разработки выглядит примерно так:

  • Сбор требований. Группа заинтересованных лиц собирается и обсуждает проект. Например, пошаговую стратегию. После горячих споров, дюжины ящиков пива и нескольких блоков сигарет рождается диздок, который максимально кратко описывает будущую игру.
  • Планирование. Из требований составляется список. Каждое требование оценивается и устанавливается его полезность (Business Value). Самые полезные требования планируются на первый релиз, из них выбирается некоторое количество требований на первую итерацию. Таким образом, за несколько часов составляется план релиза и план первой итерации. Все остальные требования лежат мертвым грузом и терпеливо ждут. Каждое требование в первой итерации разбивается на задачи. Команда собирается и быстренько расхватывает самые интересные задачи как горячие пирожки. Неинтересные задачи достаются самым безынициативным и неопытным программистам.
  • Разработка. Каждое утро команда собирается вместе и каждый разработчик рассказывает, что он сделал вчера, какие были проблемы, и что он будет делать сегодня. Лидер проекта помечает все сделанные задачи и отражает прогресс команды с помощью Burn down chart, который висит в красном углу поверх пыльной диаграммы Гантта из предыдущего проекта. Все четко знают, какая ситуация в текущей итерации. Все знают скорость команды и представляют, что она может сделать за одну итерацию, а что нет.
  • Тестирование. Тестирование начинается одновременно с началом первой итерации. Вместо привычного разноса кофе и закупки булочек тестировщики уже пишут тест кейсы для требований из первой итерации. Как только разработка требования закончена, QA с радостью берут промежуточный билд и тестируют его на предмет соответствия тест кейсам. Найденные дефекты тут же исправляются разработчиками по горячим следам. Поставка. Альфа версия вылетает оттестированной и вовремя. Бета версия выбегает оттестированной и вовремя. Демо версии выходят оттестированными и вовремя, за редким исключением. Версия к выставке выходит оттестированной и практически со всем запланированным к выставке функционалом. Голд мастер выходит оттестированным и стабильным. Голд мастер запаздывает редко.

У итеративного процесса несколько преимуществ по сравнению с традиционным водопадом:

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

У игры есть жестко зафиксированные даты релизов (демо-версии, выставки, голд-мастер). Для итеративного процесса это не проблема. Нужно просто правильно разбить релизы на итерации.

Рекомендуемая длина итерации при разработке игр - 2-4 недели, но это сильно зависит от команды. При разработке игр есть вероятность, что за первую/ вторую итерацию ничего работающего выпустить не получится. Но надо стремиться к тому, чтобы запускающаяся игра с минимальным функционалом появилась как можно раньше.

Разбиение требований

Специфика разработки игр в том, что обычно программистов в составе команды всего около 20-25%. Остальная команда состоит из художников, 3d-моделлеров, тестировщиков и еще многих других людей занятных профессий. Важно научиться не просто выделять отдельные фичи (Feature), но и уметь эти фичи разбивать на пользовательские истории (User Story) и задачи (Task).

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

Из всего этого инфантильного монолога можно выделить всего одно требование: мальчик хочет авианосец с самолетиками.

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

Фича: Сделать авианосец.
Пользовательские истории (с точки зрения игрока):

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

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

  1. Обсудить на предмет здравого смысла ("Эта идея с летающей подводной лодкой совершенно идиотская").
  2. Выбрать те истории, которые в принципе должны быть реализованы в игре.
  3. Грубо оценить трудозатраты на реализацию этих историй.
  4. Расставить приоритеты ("Какая история важная?", "Какая не очень важная?").

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

Ниже в качестве примера приведены Истории пользователя/Задачи (с точки зрения разработчика) для авианосца.

  • Создать юнит авианосца:
    • Набросок авианосца (художник)
    • 3D-модель авианосца (3d-моделлер)
    • Текстура авианосца (художник)
    • Анимация (аниматор)
    • Спецэффекты (художник по спецэффектам)
    • Озвучивание авианосца (звукорежиссер)
  • Вставить авианосец в игру:
    • Создать набор тест кейсов для авианосца (QA)
    • Программирование логики (плавает по морю) (программист)
    • Программирование логики (не может заходить в порты) (программист)
    • Программирование логики (перевозка самолетов, посадка и взлет самолетов) (программист)
  • Научить AI пользоваться авианосцем:
    • Программирование стратегии (программист АИ)
    • Программирование тактики (программист АИ)

Более подробно прочитать про Agile-планирование можно здесь[3].

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

Я не случайно упомянул слова "регулярно и систематично". Как правило, у начинающих команд процесс тестирования построен по принципу "тестируем от забора и до обеда". Случаи спонтанного тестирования игры начинаются после прочтения статьи про процессы разработки и учащаются ближе к дате релиза ("Так, через неделю у нас выставка. Давайте-ка быстренько поищем баги, а то на выставке облажаемся по полной программе"). Хорошо, если есть возможность выделить отдел тестирования (пусть даже из одного-двух человек). Если такой возможности нет, тоже не беда. Можно выделить один день в неделю (или лучше несколько последних дней итерации), когда практически ВСЯ команда тестирует игру.

Про систематичность стоит поговорить подробнее. Важно, чтобы игра делала все, что ожидают от нее игроки и не делала того, чего от нее не ожидают. Так как это практически невозможно, поступают проще - игра должна делать все то, что в нее закладывают разработчики. Учитывая, что разработчики хотят заложить всего и много (особенно за месяц до релиза), очень важно иметь возможность оперативно проверять, не сломалось ли что-нибудь в игре. Это называется регрессионным тестированием. Можно посадить 10 тестировщиков и надеяться, что один из них случайно вызовет всплывающую подсказку над только что разбомбленной базой противника, которая ведет к крашу игры только из-за того, что был изменен алгоритм вывода текста на экран в заданном прямоугольнике, который протестировали на различных тестах, но совсем не учли того, что символ одинарных кавычек после редактирования в MS Word может стать вовсе не тем, чем кажется на самом деле. Причем игра падать будет только в этом месте и больше нигде. В более простом варианте - нет гарантии того, что перед релизом не забудут протестировать маленькую фичу, которая всегда работала, но вот именно вчера ее совершенно случайно сломал нерадивый программист Сережа, который практически не спал два дня.

Тест Кейсы/Приемочные тесты (Test Cases/Acceptance Tests)
Человечество давно научилось бороться с забывчивостью отдельных индивидуумов. Для этого оно придумало чек-листы (checklist). Разработчики в качестве чек-листов могут использовать Тест Кейсы или Приемочные Тесты (Test Cases/Acceptance Tests). Тест кейс в общем случае состоит из:

  • описания начального состояния системы
  • действий пользователя
  • ожидаемого результата этих действий

Обычно тест кейс имеет всего два состояния: Прошел/Упал. Если игра аккуратно покрыта тест кейсами, то перед релизом трудно будет что-нибудь "забыть" протестировать (но легко можно не успеть).

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

Для игры тест кейсы обычно пишутся методом copy-past из диздока с небольшими изменениями. Писать тест кейсы можно геймдизайнеру (помощнику геймдизайнера) или QA. Есть простое правило, которым можно руководствоваться при написании тест кейсов: тест кейсы - это точные критерии приемки выполненной задачи.

Пример: В диздоке написано: "После окончания игры игрок вводит свое имя в таблицу рекордов".

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

Дополним задание несколькими тест кейсами:

Тест 1
Действие: Нажать кнопку "Достижения" в главном меню.
Результат: Открывается таблица рекордов, которая состоит из 10 записей.

Тест 2 Действие: Удалить файл "topscore.sav", хранящий рекорды. Перезапустить игру.
Результат: В таблицу занесены имена игроков со следующими результатами: "1. Коля 100000 .... 10. Вася 100."

Тест 3 Начальное состояние: Выполнить Тест 2.
Действие: Набрать в игре ровно 100 очков и проиграть.
Результат: Игра предложит ввести имя в таблицу рекордов.

Тест 4 Начальное состояние: Выполнить Тест 3.
Действие: Ввести имя состоящее из одних "W" на максимальную длину. Нажать ввод.
Результат: Имя помещается в отведенное для него место на экране. Результат находится на последней (10-й) строке вместо "Вася".

Плюсы Тест Кейсов:

  • Нельзя "забыть" протестировать какую-то функциональность.
  • Новые тестировщики в проект вводятся практически без временных затрат.
  • Сокращается общее время на полное тестирование игры.
  • Облегчают написание кода программистам.

Минусы:

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

    Юнит Тесты (Unit Tests)

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

    Основных целей у юнит тестов две:

    1. помочь программисту написать код
    2. внести изменения в существующий код без ошибок.

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

    1. Вначале он пишет кусочек кода.
    2. Затем запускает программу и
      1. Проверяет путем создания подходящих условий, как написанный код работает.
      2. Или пишет небольшой кусочек кода, который создает нужные условия и запускает свеженаписанный код.
      3. Или запускает программу под отладчиком и искусственно создает подходящие условия для тестирования.

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

    Ленивый программист поступает иначе:

    1. Вначале он пишет юнит тест.
    2. Затем пишет кусок кода и запускает юнит тест. "Зеленый" тест говорит о том, что кусок кода работает правильно и работа сделана.

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

    Пример: Допустим у юнита есть метод Distance(x, y), который возвращает количество move points от текущего положения до некоторой точки на карте. Также есть тестовая карта. В этом случае мы можем написать несколько тестов, которые проверят правильность работы метода. Выглядеть тест мог бы, например, так:

    
    	
    UNIT_TEST DistanceTest()
    {
    	LoadTestScene("TestScene1.scn");
    	Unit * tank = PutUnit(TANK, 10, 10); //ставим танк на сушу
    	Unit * boat = PutUnit(BOAT, 20, 20); //а корабль в море
    	
    	TEST_EQUAL(tank->Distance(11, 11), 1); //на соседнюю клетку за ход
    TEST_EQUAL(tank->Distance(20, 20), -1); //в море - никогда
    TEST_EQUAL(tank->Distance(15, 15), 6); //а по лесу едем очень медленно
    TEST_EQUAL(boat->Distance(30, 20), 10); // Корабль плавает по морю
    TEST_EQUAL(boat->Distance(10, 10), -1); // а по суше плыть не хочет
    
    tank->SetXY(1, 1);
    TEST_EQUAL(tank->Distance(127, 127), 283); //зато танк может проехать всю карту
    по диагонали, но не быстро TEST_EQUAL(tank->Distance(-1, -1), -1); //Интересно, упадет ли тест если отправить
    танк за границу... }

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

    
    
    UNIT_TEST ExperienceTest()
    {
    	Creature * wolf = Create(WOLF, LEVEL5);
    	Wolf.expirience_base = 33; //независимо, что введут геймдизайнеры потом для волка,
    мы проверяем алгоритм, т.е. переопределяем значения от которых зависит начисление опыта. Creature * hero = Create(HERO, LEVEL1); TEST_EQUAL(hero->GetExperience(Wolf), 165); //алгоритм прост: разница уровней + 1
    умноженная на базовую величину. }

    Вообще юнит тесты должны как можно меньше зависеть от данных. Хорошей иллюстрацией может служить боец, сраженный вражеской пулей. Неправильный юнит тест обязательно проверит, куда тело упало и как оно там лежит ("ах, у вас правая рука загибается под неестественным углом в 123 градуса"). Правильный Юнит Тест просто проверит, что тело "упокоилось" в течение разумного периода времени. В результате неправильный тест придется переписывать всякий раз, когда 3D-моделлер уберет пару полигонов или добавит/поменяет кости.

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

    Плюсы Юнит Тестов:

    • Сокращается количество ошибок в коде.
    • Упрощается проведение рефакторинга кода.
    • Упрощает (и ускоряет) отладку.

    Минусы:

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

    Рефакторинг (Refactoring)

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

    Не знаю как вас, а меня с детства учили не трогать то, что работает. А тут предлагают что-то менять в уже работающем коде и тратить на это драгоценное время. При этом игра ни на шаг не приблизится к релизу. Другими словами: полезность рефакторинга не очевидна.

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

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

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

    Дальше начинается этап поддержки кода. В нем исправляют ошибки, в него вносят ошибки, добавляют функциональность, убирают функциональность, оптимизируют, просматривают на предмет ошибок. Для нас важно минимизировать время, которое будет отнимать поддержка кода. Именно с этой целью проводят рефакторинг. Рефакторинг устраняет все неточные первоначальные предположения и оставляет после себя код, который делает только то, что надо и не больше. О том, как правильно проводить рефакторинг, написана большая и толстая книга[1]. Мы же ограничимся примером.

    
    
    bool Unit::CanMove(int x, int y)
    {
    	if (x < 0 || x > map->GetWidth() || y < 0 || y > map->GetHeight())
    		return false;
    
    	Unit * u = map->GetUnit(x, y);
    	if (u && (u->type != TRANSPORT || !u->CanLoad(this)))
    		return false;
    
    	if (u)
    		return true;
    
    	TERRAIN t = map->GetTerrain(x, y);
    	if ((type == TANK || type == ROBOT) && t == GROUND)
    		return true;
    	if (type == BATTLESHIP && t == SEA)
    		return true;
    	if (type == HELICOPTER && t != MOUNTAINS)
    		return true;
    }
    

    В общем-то обыкновенный кусочек кода. Понять, что он делает, не составляет никакого труда даже для нерадивого программиста Сережи. Тем не менее, этот код - потенциальный источник проблем в будущем. Собственно, он уже источник проблем. Я только что добавил подводную лодку, и чуда не случилось - она у меня никуда не поплыла. Решить вопрос с подводной лодкой можно очень быстро, добавив одно условие в строку

    
    
    if ((type == BATTLESHIP || type == SUBMARINE) && t == SEA)
    

    Это решит проблему сейчас, но в будущем создаст еще 20-30 небольших проблемок (столько юнитов планируется добавить в игру). Так что попробуем улучшить код.

    Для разминки вынесем код, проверяющий попадание координат в границы карты, в класс Map, где ему самое место:

    
    
    bool Map::isValidCoords(int x, int y)
    {
    	return (x >= 0 && x < width && y >= 0 && y < height);
    }
    

    Соответственно проверка превратится в достаточно понятный кусочек кода:

    
    
    	if (!map->isValidCoords(x, y))
    		return false;
    

    Следующим шагом вынесем проверку, данный юнит это транспорт или нет, в метод CanLoad. В результате и этот код упростится:

    
    
    	Unit * unit = map->GetUnit(x, y);
    	if (unit && !unit->CanLoad(this)
    		return false;
    

    И, наконец, мы преобразуем проверку, может ли данный юнит двигаться по данному типу поверхности. Для этого мы зададим типы повверхности в виде флагов, чтобы над ними можно было проводить бинарные логические операции (&|^). В результате метод стал совсем простой для поддержки и понимания. Кроме того, в будущем в него уже не понадобится лазить всякий раз при добавлении юнита.

    
    
    bool Unit::CanMove(int x, int y)
    {
    	if (!map->isValidCoords(x, y))
    		return false;
    
    	Unit * unit = map->GetUnit(x, y);
    	if (unit && !unit->CanLoad(this)
    		return false;
    
    	return isUnitCanMoveOnTerrain(map->GetTerrainType(x, y));
    }				
    
    bool Unit::isUnitCanMoveOnTerrain(TERRAIN_TYPE terrain_type)
    {
    	return terrain_type & supported_terrain_types;
    }
    

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

    Плюсы:

    • Простой и легко читаемый код.
    • Упрощается внесение изменений в код и поддержка кода.

    Минусы:

    • Время надо тратить здесь и сейчас для получения результата когда-то потом.
    • Без юнит тестирования рефакторинг имеет крайне ограниченное применение.

    Объеденив Тест Кейсы, Юнит Тесты и Рефакторинг мы получим простую, но очень мощную методологию - Разработка через тестирование (Test Driven Development / TDD). TDD улучшает качество кода, позволяет создавать более простую и изящную архитектуру, а также уменьшает количество дефектов в коде.

  • Парное программирование (Pair Programming)

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

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

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

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

    Ошибочно считается, что два программиста, работающих в паре, напишут в два раза меньше двух программистов, работающих по отдельности. Это верно только для тривиальных случаев: пока два программиста в паре пишут цикл от 1 до 10. Как показывает мировая практика, на длительных задачах два программиста по отдельности пишут всего на 15% кода больше, чем пара. При этом они совершают большее количество ошибок и пишут менее понятный код[2].

    Плюсы:

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

    Минусы:

    • Занимает немного больше времени (на 15%) при написании кода.
    • Иногда появляется социальное противодействие со стороны сотрудников.

    Постоянная интеграция (Continuous Integration)

    Рассмотрим типичный случай разработки игры. Программисты Юра и Артем работают над ядром игры. Программисты Олег и Андрей делают "искусственный интеллект" (AI). Очевидно, что ядро и AI должны взаимодействовать друг с другом. Что будет, если эти два модуля начнут интегрировать через месяц после начала разработки? А через полгода? Правильно, если интегрировать их через полгода, то заставить их подружиться займет еще пару недель. Весь проект, все его модули должны интегрироваться как можно чаще. Оптимально - каждый день. AI должен начать работать с ядром как можно раньше. Это первоочередная задача. И развиваться он должен совместно с ядром. Каждый день. ВСЕ разработчики должны работать с самой свежей версией кода, даже если их модуль более-менее независим.

    Continuous Integration (CI) дает постоянную обратную связь. Любой процесс, который дает мгновенную обратную связь при разработке игры, позволяет делать ее быстрее и лучше.

    Ничего сложного в CI нет совершенно. Для осуществления CI необходимо несколько условий:

    • При разработке должна использоваться система контроля версий. Например CVS, Subversion, Visual Source Safe.
    • Разработчики должны чекинить код как можно чаще. Не реже одного раза в день.
    • Билд должен собираться автоматически (bat-файл, что угодно).

    Обеспечив эти три условия уже можно использовать CI, но для эффективного его использования еще нужно:

    • Набор юнит тестов.
    • Набор автоматических приемочных тестов.

    Работа CI показана на рисунке:

    Существуют системы, которые автоматизируют процесс CI. Самая известная среди них - Cruise Control.

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

    Преимущества CI:

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

    Agile Development и Обратная Связь

    Обратная связь при создании любого продукта - это ключ к успеху. Чем раньше вы получите ответ на поставленный вопрос, тем скорее сможете что-либо поправить и улучшить. Простой пример - компиляция кода. Представьте, что компиляторов не изобрели. Как проверить бедным программистам синтаксическую правильность кода? Правильно, запустить приложение. Именно так поступают трудолюбивые PHP программисты (ленивые, как вы знаете, пишут юнит тесты). Компилятор дает немедленный ответ на вопрос "Не ошибся ли я где-то в коде?"

    Если оглянуться назад, то можно установить любопытную связь между лучшими практиками и обратной связью:

    Практика Обратная связь Пример
    Итеративния разработка / Iterative Development Какая скорость нашей работы? Успеем ли мы закончить релиз вовремя? За итерацию 2 мы сделали 59 пунктов. На эту будет столько же. Значит релиз запаздывает на 6 дней
    Юнит тестирование / Unit Testing Не поломал я что-нибудь полезное, улучшая код в этом классе? Упс!, поломался тест CanMove. Что-то я напортачил в последнем изменении...
    Парное программирование / Pair Programming Нельзя ли тут чего-нибудь улучшить? Действительно ли это оптимальное решение? (постоянный код ревью) - Этот алгоритм мне совсем не нравится.- А мне нравится. - А мне, нет, давай упростим (долгая дискуссия по алгоритму).
    Постоянная интеграция / Continuous Integration Все ли хорошо с интеграцией? Собирается ли проект? Не поломал ли нерадивый программист Саша функциональность, изменив пару строк в важном классе? Черт, билд поломали! Саша, это ты последний чекинил код?

    Заключение

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

    Гибкие процессы - это не панацея от всех проблем разработки. Это не готовое решение, которое можно применить, и все закрутится и заработает само по себе. Это всего лишь философия ориентации на результат путем:

    1. Устранения препятствий.
    2. Отбрасывания всего лишнего.
    3. Упорядочивания всего необходимого.

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

    С чего начать? Прежде всего, задайте себе вопрос: "Хочется ли мне, чтобы новый проект прошел так же, как предыдущий?" Если ответ положительный, смело идите пить пиво и забудьте про эту статью. Если вас начинают грызть сомнения, смело идите пить пиво со своей командой, захватив стопку чистых листиков и карандашей. Проблемы и решения лучше обсуждать в непринужденной обстановке на свежем воздухе.


    Литература и полезные ссылки:

    [1] Refactoring: Improving the Design of Existing Code (Hardcover) by Martin Fowler, Kent Beck, John Brant, William Opdyke, Don Roberts Addison-Wesley Professional; 1st edition (June 28, 1999), ISBN-13: 978-0201485677

    [2] Costs and benefits of pair programming Alistair Cockbrun

    [3] Agile Estimating and Planning (Robert C. Martin Series) (Paperback) by Mike Cohn, Prentice Hall PTR (November 1, 2005), ISBN-13: 978-0131479418

    [4] SCRUM for Video Game Development, Mike Cohn

    [5] http://en.wikipedia.org/wiki/Agile_software_development

    [6] http://agilealliance.org/

    [15.08.2007]

    Copyright © 1999-2019 DTF Ltd. Все права защищены.
    Замечания и предложения отправляйте по адресу team@dtf.ru