Найти тему
ZDG

Пишем Сапёра на C + SDL #11: Шрифт и стек меню

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

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

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

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

SDL_ttf

Первоначально я хотел использовать библиотеку SDL_ttf для вывода шрифтов. Но с ней оказалось не всё так просто. Во-первых, по умолчанию она отсутствует в SDL2, и её нужно ставить отдельно. Это прошло не безболезненно, но после некоторых мук у меня всё получилось. Во-вторых, эта библиотека загружает шрифт из внешнего ttf-файла. Это значит, что данный файл придётся включить в поставку Сапёра, и он уже не будет автономной программой. В-третьих, городить столько сложностей ради трёх строчек меню как-то смешно.

Поэтому работе с SDL_ttf я лучше посвящу отдельный выпуск, где все вопросы будут рассмотрены подробно.

И снова растр

А в Сапёре мы будем использовать старый добрый растровый шрифт. Технология будет такая же, как для вывода цифр в 5-килобайтном Тетрисе.

Я раскопал в своих архивах один шрифт, который раньше использовал везде как стандартный. Ему уже 20 лет, и он так и называется ZDG-шрифт. В нём каждый символ имеет размер 8*16 пикселов и занимает 16 байт. Каждый байт это 8 пикселов по горизонтали.

Шрифт описан как массив байтов, и я статически включил его в программу, так же как до этого включал изображения.

Далее была написана функция для вывода текста.

Порядок действий таков. В языке C строка это последовательность байтов. В конце строки находится нулевой байт. Поэтому мы берём указатель на строку (c) и в цикле увеличиваем его, пока не наткнёмся на нулевой байт. Это внешний цикл перебора.

-2

Получив из строки очередной символ, мы вычисляем смещение для этого символа в массиве шрифта (letter). Каждый символ это 16 байт плюс 2 каких-то служебных байта (смешно, я не помню, зачем они там), итого код (он же порядковый номер) символа умножаем на 18 и получаем указатель на байты символа в массиве.

Теперь циклом от 0 до 16 перебираем 16 байт данных. Это соответствует изменению вертикальной координаты. По горизонтали мы перебираем биты внутри байта (byte). Начиная с младшего бита: если он равен 1, то рисуем точку (прямоугольник 1*1), если 0, то не рисуем. Чтобы получить следующий бит, сдвигаем байт вправо 1 раз. И так пока в байте не останется ненулевых битов (т.е. когда будут одни нулевые, байт будет равен нулю).

Стек меню

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

-3

Само меню состоит из трёх опций. Это три зоны для клика мышью (структуры типа MouseBox). Их надо добавить в список зон, чтобы они начали обрабатываться. Как мы помним, в списке зон уже находятся: кнопка старта, кнопка меню и зона игрового поля. При переходе в меню нужно убрать из списка зону игрового поля и добавить три зоны опций меню.

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

Иначе говоря, мы имеем дело с добавлением и удалением элементов из списка в режиме стека (push / pop). Это меняет длину списка, но мы не можем её менять в языке C.

Поэтому задаём фиксированную длину списка (10 элементов хватит), и делаем дополнительный параметр: box_list_size. Он по сути является смещением в стеке. Первоначально он равен нулю. Чтобы добавить элемент в список, мы записываем этот элемент в текущее смещение (0) и увеличиваем box_list_size, т.е. теперь текущее смещение стало 1. При записи следующего элемента смещение станет 2, и т.д. Чтобы удалить элемент из списка, просто уменьшаем текущее смещение.

Перебор списка зон мы делаем от нуля до текущего смещения.

Первоначально я сделал структуру, которая хранила и список зон, и смещение вместе.

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

Решением стало переместить и список, и его смещение, и три опции меню в структуру FieldView. Она передаётся практически во все функции, и значит, доступ ко всему будет. Эта структура всё пухнет и пухнет, приобретая дополнительные поля. Что немного тревожит. Но FieldView является представлением, то есть отвечает за интерфейс и рисование элементов. И я в общем-то не смог найти в ней чего-то несвойственного для представления. Да, полей много, но все они получаются по делу.

Далее рутинно пишем обработчик нажатия и отжатия на опции меню. У них у всех один обработчик, так как все три опции работают одинаково. Рисуем нажатый вид, отжатый вид. Текст у опций разный. Какой именно текст писать, мы выясняем, проверяя личные данные зоны (свойство data).

-4

Наконец, при отжатии опции происходит следующее:

Мы опять же с помощью свойства data выясняем, что это была за опция. И делаем новую инициализацию поля с нужными размерами и количеством мин. После инициализации мы меняем размер окна, чтобы он соответствовал размерам поля. При изменении размеров окна полученная ранее от окна поверхность screenSurface перестаёт работать, поэтому мы сначала освобождаем текущую поверхность (SDL_FreeSurface()), затем меняем размер окна, и повторно получаем от окна указатель на поверхность.

-5

Ну и конечно, мы удаляем из стека зон три опции меню и возвращаем назад зону игрового поля. Меню исчезает, поле появляется.

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

-6

И так выглядит меню в самом маленьком окне:

-7

Обратите внимание на кнопку меню – у неё разный вид, когда меню включено, и когда нет.

Всё готово, и осталось в последнем выпуске оформить конец игры и добавить таймер.

Читайте дальше: Финализация и феншуй

Читайте также: 5-килобайтный Тетрис на JavaScript

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