21 марта 2011

Дневник разработки: main_loop

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

Давайте напиши этот тест вместе. Будем считать что это мои рассуждения в слух. Что уж тут поделать, но в слух мне думается гораздо лучше, терпите.

Начнем с самого теста, а именно с функции run. Назовем ее run_begin_and_end, так как в этом тесте это будет не единственный тест. Глупо звучит. Это потому что до этого у нас один тест компилировался в один исполняемый файл, тут же я считаю удобным выполнить сразу несколько тестов. Получается что до этого наши тестовые наборы содержали исключительно по одному тестовому случаю.

void
run_begin_and_end()
{
  begin_assertions();

  assert(game->status == WORK);

  game->status = DONE;

  main_loop(game);

  assert(game->status == END);
  assert(game->error == NO_ERROR);

  end_assertions();
}

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

WORK
означает что игра работает.
DONE
означает что игре необходимо закончиться.
END
собственно это значение означает игра закончена.

Зачем вообще нужен этот END? Дело в том что DONE, не будет означает завершение игры, это сигнал к смене декораций. Об этом чуть позже, а сейчас нужно добиться от теста положительного результата.

Что бы тест прошел успешно достаточно было написать такой код:

void
main_loop(struct context *game)
{
  game->status = END;
}

Правда ведь что-то не то. Где циклы, где условия, где хоть что-нибудь кроме смены значения переменной. Ну не бывает главных циклов в одну строчку (хотя конечно бывают, но явно не из одного присваивание значения переменной (тут надо заметить что сделать так все же можно, если перегрузить оператор = в языке который это делать позволяет)). Если у вас возникло желание это исправить сразу же, то наберитесь мужества и остановите себя. Мы пишем код который позволяет тесту пройти корректно. Если код нас не устраивает и мы понимаем что он не делает что мы хотим, то нужно писать еще тест, который более детально расскажет что же он должен делать.

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

Давайте подумаем как мы можем проверить то, что внутри функции main_loop действительно есть цикл? Все очень просто. Что в этом самом цикле будет происходить на что мы сможем повлиять?

Что вообще происходит в главном цикле игры?

  1. Обработка ввод с внешних устройств
  2. Обновление мира
  3. Отрисовка мира

Ну с обработкой событий вроде понятно, это в основном ввод пользователя с клавиатуры или возня мышью. Отрисовка мира это render. А вот на обновления мира, мы и будем влиять. Что же я имел ввиду?

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

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

Почему я разделил игровую логику и AI. Игровая логика это то, что и есть игра, она работает всегда, AI же можно не тревожить каждую секунду.

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

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

Для это есть lua.

Продолжение следует...