Найти тему
ZDG

Пишем Сапёра на C + SDL #12: Финализация и феншуй

Предыдущие части: Шрифт и стек меню, Меню и провал планирования, Рефакторинг, Мышь, Подготовка графики, Графический интерфейс, Указатели и графика, Делаем поле, Какую память выделить, Структура программы, Пишем Сапёра на С + SDL

Код для этой части находится в ветке final на github. Вы можете смотреть там все файлы онлайн и также скачать зип-архив всей ветки.

В этом выпуске Сапёр будет полностью доделан. Перечислю оставшиеся задачи:

  1. Сделать два вида окончания игры: победу и поражение
  2. Сделать отложенную инициализацию поля
  3. Сделать таймер
  4. Феншуй

1. Окончание игры

Игра может закончиться только тогда, когда игрок открывает клетку поля. Есть два финала:

Игрок открыл клетку с миной

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

  • Если в клетке есть мина и стоит флаг, с ней ничего не делаем
  • Если в клетке есть мина и нет флага, открываем её
  • Если в клетке нет мины, но есть флаг, открываем её. В дальнейшем в функции draw_field() на этом месте будет рисоваться перечёркнутая мина.

Всё это делается в функции finalize_field().

Далее, устанавливаем в fieldView режим игры game_mode = GAME_STOP, и текущий смайлик меняем на "дохлый". Теперь поле будет перерисовываться с этим смайликом. Также в fieldView.explosion_addr записываем адрес, где была взорвавшаяся мина – эта клетка должна рисоваться красной.

Игрок открыл пустую клетку

В модели fieldModel есть общее количество клеток (total) и есть количество мин (mine_count). Каждая открытая клетка добавляется к счетчику открытых клеток в fieldView.open_cells. Игрок выигрывает, когда открыл total - mine_count клеток.

В этом случае поле также финализируется:

  • все клетки, где есть бомбы, помечаются флажками

Всё это делается в функции finalize_field_win().

Далее устанавливаем в fieldView режим игры game_mode = GAME_STOP, и текущий смайлик меняем на "крутой". Теперь поле будет перерисовываться с этим смайликом.

-2

При старте новой игры смайлик всегда меняется на обычный.

2. Отложенная инициализация поля

Когда всё поле закрыто и игрок нажимает на клетку в первый раз, есть шанс, что он сразу же наткнётся на мину. Очевидно, что это бессмысленно и бесит игроков. Поэтому делается хитрость: поле вначале не содержит мин. Только после того, как игрок кликнет в первый раз, оно заполняется минами. Но клетка, в которую он кликнул, всегда будет пустой.

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

Я разделил эту функцию на две части. Теперь init_field() просто очищает поле, а init_mines() добавляет мины.

Расстановка мин происходит в обработчике отжатия на клетке поля. Если текущий режим игры fieldView.game_mode == GAME_INIT, значит игрок ещё ничего не делал. В этом случае вызываем функцию init_mines(), передавая туда адрес клетки поля, которую нужно исключить. И меняем режим игры на GAME_PLAY.

3. Таймер

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

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

Вместо SDL_WaitEvent() будем использовать SDL_PollEvent(). Правда, теперь наш цикл начинает крутиться безостановочно, что повышает нагрузку на процессор (у меня до 30%). Вставляем в него небольшую задержку SDL_Delay(10) – 10 миллисекунд. Этого достаточно, чтобы разгрузить процессор до 0%.

В этом же цикле ставим вызов update_timer(), но только тогда, когда игрок уже начал играть (game_mode == GAME_PLAY)

Я добавил в fieldView ещё два параметра – start_ticks и timer. В начале игры я присваиваю start_ticks = SDL_GetTicks() – это количество миллисекунд, которое прошло с момента инициализации SDL. Само количество неинтересно, надо только запомнить эту стартовую точку.

Каждый раз при вызове update_timer() она получает новое значение SDL_GetTicks() и вычисляет разницу между ним и start_ticks. Если эта разница равна или больше 1000, значит прошла одна секунда. В этом случае увеличиваем timer на 1 и перерисовываем табло. А start_ticks устанавливаем в новое значение.

4. Доработки и феншуй

Чтобы меньше ворошить стек активных зон при вызове меню, я сделал другую обработку стека: с конца. То есть добавленные последними зоны проверяются первыми (как, собственно, и положено в стеке). Теперь я могу при открытии меню добавить в стек три элемента-опции, но при этом не убирать элемент-поле. Поле будет просто находиться под опциями. Если игрок ткнет в опцию, то в поле он всё равно не попадёт. А если ткнёт куда-то между опциями, то уже попадёт в поле, но оно будет неактивно – я добавил проверку игрового режима в обработчик.

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

-3

И они довольно неплохо смотрятся. Но задавать размер в меню мне уже лень, просто уже не буду делать новые выпуски на эту тему, а буду обновлять главную ветку (main) на гитхабе. На данный момент я воспользовался параметрами командной строки (argc, argv):

-4

Короче говоря, если программу запустить так:

a.exe 24

То клетки будут со стороной 24 пиксела. Я сделал ограничение от 16 до 32.

Далее обсудим небольшие изменения в применении констант. В программе они используются довольно широко. Скажем, режимы игры заданы числами 0, 1, 2, 3, но чтобы не пользоваться непонятными конструкциями типа game_mode = 1, мы даём этим значениям собственные имена. Теперь 0 это GAME_INIT, 1 это GAME_PLAY и т.д., и написав game_mode = GAME_PLAY, мы уже понимаем, о чём речь.

То же самое относится к типам зон: BOX_MENUBUTTON, BOX_FIELD, и т.д., к номерам изображений в массиве: ICON_QUESTION, ICON_FLAG и т.д.

Всё это было задано с помощью define.

Есть более правильный способ задавать такие константы – перечисляемый тип. Как мы знаем, каждый тип описывает данные. Целый тип это число от 0 до... , символьный тип это байт от 0 до 255, строковый тип это указатель на байтовый массив, и т.д. Перечисляемый тип в C задаёт строго ограниченное количество конкретных значений. То есть переменной этого типа можно присвоить только эти значения. Например:

-5

Синтаксис работает так: конструкция enum {} перечисляет значения. typedef ассоциирует эту конструкцию с именем типа EnumGameMode (в названии не обязательно должно быть Enum, это я написал, чтобы отличать enum-типы от других)

Теперь объявим переменную этого типа:

EnumGameMode game_mode;

И теперь этой переменной мы не можем присвоить 0 или 1 или что-то ещё. Ей мы можем присвоить только то, что перечислено в enum: GAME_INIT, GAME_PLAY и т.д.

А что же это за значение, которое выглядит как GAME_INIT? Это вообще что, число, строка? Каждое такое значение пронумеровано по порядку. То есть фактически enum выглядит так:

enum { GAME_INIT = 0, GAME_PLAY = 1, ... }

И присваивая game_mode = GAME_INIT, мы фактически присваиваем game_mode = 0. То есть значения перечисляемого типа – просто последовательные целые числа, которым присвоены имена. То же самое мы делали с помощью define, но есть отличия.

Например, раньше переменная fieldView.game_mode имела тип int и ей можно было присвоить любое целое число. Теперь она имеет тип EnumGameMode и ей можно присвоить только одно из значений перечисляемого типа. Хотя в базе своей всё это остаётся просто типом int и на конечную программу никак не влияет, это помогает компилятору на этапе проверки синтаксиса определить, правильно ли мы используем типы. То есть повышает надёжность кода.

Ещё было бы хорошо разобраться с включаемыми (#include ) файлами. Где-то содержатся определения, где-то функции, какие-то нужно включать раньше, какие-то позже (чтобы они видели друг друга). Сейчас в программе получился небольшой винегрет. Всё это также можно окультурить, но я про это напишу в отдельном выпуске, посвященном общим проблемам и решениям языка C.

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