Найти в Дзене
ZDG

Пишем Сапёра на C + SDL #10: Меню и провал планирования

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

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

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

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

Я думал сэкономить на высоте окна за счёт свободного пространства верхней панели, но не учёл, что при размере поля 9*9 клеток свободного пространства-то и не остаётся! Потом пришёл к компромиссу:

-2

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

  1. Игровое поле
  2. Кнопка со смайликом (новая игра)
  3. Кнопка меню (показать/спрятать меню)
  4. Область с опциями меню (при открытом меню)

И естественно, клик в каждой зоне имеет свою, независимую логику.

Чтобы привести это всё в порядок, я внедрил тип структуры MouseBox, который описывает зону перехвата мыши. МouseBox состоит из трёх полей: type – чтобы знать, за что отвечает данная зона, data – чтобы при необходимости сохранить временные данные, и rect – ссылка на прямоугольник SDL_Rect.

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

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

Размеры и расположение зон зависят от размера игрового поля. Если бы в зонах хранились непосредственно структуры SDL_Rect, то при изменении размеров поля пришлось бы обновлять их. Но тут я прикинул, что представление FieldView и так вычисляет эти прямоугольники для рисования, а поэтому может просто хранить их у себя. Итого в структуре FieldView будут:

-3

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

Создаём переменные для зон:

-4

Ну и конечно типы-константы BOX_FIELD, BOX_MENU и т.д. были добавлены через #define .

Список зон в начале игры выглядит так:

MouseBox *box_list[] = {&startBox, &menuButtonBox, &fieldBox};

Мы готовы проверять зоны. Переделаем опрос мыши в главном цикле:

-5

Общая структура условий осталась, но существенно похудела. Теперь мы здесь занимаемся только фильтрацией событий перед отправкой их в обработчики. Если мышь двигалась и есть активная зона (activeBox), то мышь могла выйти за пределы активной зоны. Тогда вызывается функция process_mouse_out(), чтобы активная зона могла что-то у себя сделать по этому поводу. Если же мышь всё ещё в активной зоне, вызывается функция process_mouse_move(), чтобы зона опять же у себя отработала движение мыши (если ей это вообще надо).

Для события нажатия перебираем в цикле все зоны, находим, в какой из них произошло нажатие, и запускаем функцию process_mouse_down() с этой зоной и событием. Функция должна вернуть результат в виде указателя на активную зону или NULL. Мы записываем этот результат в activeBox для дальнейших применений.

Обработка отжатия происходит только когда есть активная зона (то есть чтобы было отжатие, должно быть сначала нажатие). Здесь запускается функция process_mouse_up().

Теперь надо написать сами функции process_mouse_up(), process_mouse_down(), process_mouse_move() и process_mouse_out().

-6

Здесь по сути ещё ничего не происходит. Функция работает просто как маршрутизатор: проверяет тип зоны и вызывает соответствующий обработчик. Значит, для нажатия/отжатия кнопки мыши нужно написать ещё обработчики process_field_up(), process_field_down(), process_menu_up(), process_menu_down(), process_startbutton_up(), process_startbutton_down(), process_menubutton_up(), process_menubutton_down().

Что-то очень много, да? И ведь это только кнопки мыши. А ещё то же самое нужно сделать для движения и потери фокуса. Но давайте посмотрим на это так: это реально нужные действия. Когда мы нажимаем на кнопку, надо нарисовать её нажатой. Когда отпускаем – надо нарисовать отжатой, и т.д. От этого кода всё равно никуда не деться, он должен быть. И разнося его по обработчикам конкретных элементов интерфейса, мы инкапсулируем логику обработки в небольшие, понятные функции, а не навешиваем гроздья условий в одном месте.

Посмотрим на обработчик поля для нажатой кнопки.

-7

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

Теперь обработчик поля для отжатой кнопки:

-8

Это тоже перешло из основного цикла и стало проще. Адрес открываемой клетки вычисляется из box->data, сохранённого ранее при нажатии.

Теперь обработчики поля для движения и потери фокуса мыши:

-9

Ещё один кусочек логики, ранее существовавший в главном цикле. На этом с полем покончено, и можно заняться кнопкой со смайликом (это зона startBox).

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

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

-10

Обработка движения мыши внутри зоны отсутствует, так как не нужна. Обработка потери фокуса просто восстанавливает исходный вид кнопки:

-11

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

-12

Абсолютно аналогично сделаны функции обработки кнопки меню. Все эти функции собраны в файле menu.c.

Пока всё. Мы получили в принципе тот же самый функционал, что и в предыдущем выпуске, но теперь более-менее удобно расширяемый. Ещё не сделано меню. Поэтому будут ещё два выпуска: добавление меню и полная финализация.

Читайте дальше: Шрифт и стек меню

Читайте также: С чего начинать писать игру?

Наука
7 млн интересуются