Предыдущие части: Подготовка графики, Графический интерфейс, Указатели и графика, Делаем поле, Какую память выделить, Структура программы, Пишем Сапёра на С + SDL
Код для этой части находится в ветке mouse на github. Вы можете смотреть там все файлы онлайн и также скачать зип-архив всей ветки.
В этом выпуске игра приобретает уже очерченный характер. Довольно странное ощущение, когда видишь точную копию Сапёра, возникшую из написанного кода:
Я подогнал размеры, сделал правильные цвета и т.д. Это всё чисто механическая работа, которая не затрагивает логику игры, так что говорить о ней нечего. Я также разнёс разные функции по разным файлам, чтобы с ними было удобнее работать.
В предыдущем выпуске я подготовил необходимую графику в виде одной картинки:
А на поле, как видно, уже выводятся отдельные фрагменты этой картинки. Давайте ещё раз повторим, как это работает.
↑ Эта общая картинка существует в программе как поверхность (SDL_Surface). Поверхность представляет собой массив пикселов. А каждый пиксел – три байта (R,G,B). Пиксельные данные можно и читать, и записывать.
Далее, есть ещё одна поверхность. Это наше игровое окно, в котором мы хотим что-то нарисовать.
Мы можем копировать часть одной поверхности в другую, задав нужную прямоугольную область. Операции рисования в SDL2 используют структуры типа SDL_Rect, которые описывают прямоугольные области.
Допустим, нужно нарисовать весёлый смайлик в окне игры. Наша консолидированная картинка хранится в поверхности, заданной переменной iconsSurface. Смайлик расположен внутри этой поверхности по координатам (110, 0), и имеет размер (17, 17). Значит, мы задаём исходную прямоугольную область как
SDL_Rect src_rect = {110, 0, 17, 17};
На экране, то есть внутри поверхности окна (переменная screenSurface) эта картинка должна быть расположена по координатам (245, 20). Размер у неё, естественно, тот же. Значит, задаём целевую прямоугольную область как
SDL_Rect dst_rect = {245, 20, 17, 17};
Осталось перебросить данные из одной поверхности в другую.
SDL_LowerBlit(iconsSurface, &src_rect, screenSurface, &dst_rect);
В функцию SDL_LowerBlit() мы передали четыре параметра: указатели на исходную поверхность и на исходную прямоугольную область, и указатели на целевую поверхность и на целевую прямоугольную область. Функция осуществляет перенос данных из одной области в другую.
Таким образом, для каждой картинки или иконки, которая содержится в объединённой картинке, нужно знать её исходную прямоугольную область. Я сделал следующие массивы:
В массиве field_icons содержатся прямоугольники для иконок "вопрос", "флаг", "мина", "красная мина", "ошибка". Стало быть, если я хочу нарисовать мину, то её прямоугольник это элемент массива с индексом 2:
field_icons[2]
Чтобы было проще ориентироваться, я также ввёл с помощью #define константы ICON_QUESTION, ICON_FLAG, ICON_MINE и т.д., то есть теперь можно писать так:
field_icons[ICON_MINE]
Аналогично я поступил с массивом смайликов. Честно говоря, не знаю, почему они в отдельном массиве, ведь можно было сделать всё в одном, но ладно, пусть будет так.
Кроме того, в картинке есть большие красные цифры 0-9 для таймера и счётчиков и разноцветные цифры 1-9 для количества мин на поле. Для них можно было бы тоже создать массивы, но вместо массива я задаю только первый прямоугольник:
SDL_Rect red_digits = {0,0,11,21};
SDL_Rect field_digits = {20,21,10,10};
Так как цифры идут друг за другом, я могу добраться до N-й цифры, просто взяв первый прямоугольник и прибавив к его координате "x" его собственную ширину, умноженную на N.
В файле draw_image.c содержатся функции для рисования картинки/иконки, одной цифры и трёхзначного числа.
Вся текущая логика рисования клеток поля заключена в draw_field.c и выражается в проверке битовых масок: закрыта клетка или открыта, есть в ней мина или нет, есть ли окружающие мины или нет. Это буквально по одной-две строчки на условие, так что комментировать не буду.
Перейдём к обработке действий игрока.
Большинство игр (и эта не исключение) основаны на бесконечном цикле:
- Получить события
- Изменить состояние
- Перейти на пункт 1
В нашем случае (minesweeper.c) это цикл while(1), единица в скобках это условие повторения цикла, и так как это не 0, то цикл будет повторяться вечно.
Внутри цикла мы вызываем функцию SDL_WaitEvent(), передавая в неё указатель на структуру event типа SDL_Event. Эта функция дожидается события. То есть пока игрок ничего не делает, событий не происходит и программа стоит на месте, ожидая возврата из этой функции. Как только случилось событие, структура event оказывается заполненной параметрами этого события и программа продолжается.
В первую очередь нас интересует тип события (event.type). События могут не относиться к игровой деятельности. Например, окно получило или потеряло фокус – это событие. Окно спряталось или показалось – это тоже событие, и т.д. И всё это мы будем получать.
Перво-наперво нужно выяснить, не пора ли заканчивать программу. Если тип события SDL_QUIT, то это значит, что от системы поступила команда на закрытие (игрок нажал на крестик), и мы выходим из цикла с помощью break. Программа заканчивается.
Если это был не SDL_QUIT, далее мы обрабатываем только те типы событий, которые нас интересуют. Это: игрок нажал кнопку мыши (SDL_MOUSEBUTTONDOWN), игрок отпустил кнопку мыши (SDL_MOUSEBUTTONUP), и игрок переместил мышь (SDL_MOUSEMOTION).
Обработка событий вместе с игровой логикой заключена в основном цикле while(), и это не совсем хорошо для организации кода, но пока терпимо. Вы можете легко различить каждый блок проверок по условиям типа
if (event.type == SDL_MOUSEMOTION)...
Начнём с самого очевидного, то есть с нажатия кнопки мыши. Итак, мы получили событие с типом SDL_MOUSEBUTTONDOWN. У этого события (точнее, у структуры event, в которой оно хранится) есть поле button, которое в свою очередь тоже структура, у которой есть поля "x" и "y" – это координаты курсора мыши в тот момент, когда была нажата кнопка.
Если эти координаты не попадают на игровое поле, нас это событие не интересует. Если же попадают, то вычисляем, на какую конкретно клетку поля. Если это открытая клетка, она нас опять же не интересует – открытые клетки уже неактивны.
Если же это закрытая клетка, то мы перерисовываем её как открытую. Не делаем её открытой, а только перерисовываем. Это создаёт эффект нажатой кнопки – выпуклый рельеф клетки как бы вдавливается внутрь.
И запоминаем указатель на эту клетку в переменной current_cell. На этом обработка события нажатия окончена.
Далее нас интересует событие с типом SDL_MOUSEMOTION – движение мыши. И вот здесь мы проверяем, чему равен указатель current_cell. Если он NULL, то мы ничего не делаем – клетка не нажималась. А вот если нажималась, нам нужно выяснить: не сдвинул ли игрок мышь с нажатой клетки? Мы так же получаем координаты курсора мыши из event.motion.x и event.motion.y, так же вычисляем, попадают ли они на поле, и так же вычисляем указатель на текущую клетку (new_cell).
Теперь, если new_cell не равно current_cell, значит, игрок нажал на кнопку на одной клетке, а потом сдвинул курсор на другую клетку. Почему это важно? Потому что кликом считается действие, когда кнопка была нажата и отжата на одной и той клетке. Если между нажатием и отжатием произошел сдвиг на другую клетку, нажатие должно быть отменено.
В этом случае, а также в случае, когда курсор вне поля, мы отменяем нажатие: рисуем обратно закрытую клетку и обнуляем current_cell. Всё, нажатой клетки больше нет. Обработка этого события закончена.
Наконец, событие отжатия кнопки: SDL_MOUSEBUTTONUP. Здесь уже всё просто: если указатель current_cell не NULL, значит клетка была нажата и "дожила" до отжатия. Мы можем смело ставить ей статус "открыта" (убирая битовую маску F_CLOSED), и вызываем функцию draw_field(), чтобы перерисовать всё поле (это избыточно, но так проще всего). Готово.
Сейчас есть уникальная возможность поиграть в Сапёра, открывая клетки с минами и не взрываясь на них. Потому что в этом выпуске мы посвятили время обработке событий мыши, а не логике игры.
В следующем выпуске сделаем:
- открытие пустых клеток рекурсивным способом
начало и конец игры по всем правиламрефакторинг
Читайте дальше: Полиморфизм и рефакторинг