Найти тему
ZDG

Пишем Сапёра на C + SDL #9: Полиморфизм структур и рефакторинг

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

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

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

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

Однако, если углубляться дальше, можно заметить, как одни условия наслаиваются на другие, а те на другие, и так далее.

Например, кнопка мыши нажата. Она нажата на клетке? На закрытой или открытой клетке? Это левая или правая кнопка мыши? На клетке стоит флажок? В клетке есть бомба? А мы вообще должны сейчас проверять клетки? Может быть, игра уже закончена?

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

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

Криминала тут, конечно, нет, можно всё передавать. Но это тоже загромождает код и кроме того, порождает много болтающихся вокруг переменных вроде width, height.

Чтобы избавиться от передач множества параметров, можно было бы сделать их глобальными. Честно говоря, для Сапёра это был бы идеальный вариант. Это маленькая программа с простой логикой, и десяток глобальных переменных ей бы не навредил.

Но использование глобальных переменных довольно быстро формирует вредную привычку. Если потом писать на объектно-ориентированных языках, эта привычка начнёт причинять боль. Глобальные переменные создают зависимости. Как это понять? Очень просто. Возьмите свою функцию, которая использует глобальную переменную, и перенесите её в другой проект. И вот функция уже сломалась. Почему? Потому что глобальная переменная, с которой она работает, больше не существует.

Поэтому не будем идти таким путём, даже если сейчас это желательно.

Уменьшить количество болтающихся переменных и передаваемых в функции параметров можно путём их группировки. Иначе говоря, мы сейчас переходим в объектно-ориентированное программирование на коленке.

Я попробую реализовать шаблон проектирования MVC в зачаточном виде.

Представлением (view) будет служить структура, описывающая игровой экран:

Разберём, что тут сделано.

Ключевое слово struct описывает структуру с именем FieldView. Описание заключается в перечислении полей структуры. Ключевое слово typedef говорит, что мы сейчас не создаём структуру, а только объявляем тип FieldView. Так как это представление, то логично, что оно имеет доступ к поверхностям для рисования, к модели поля (fieldModel), и имеет собственные свойства: ширину, высоту, размер клетки, и цвета для рисования. Раньше всё это валялось где-то вокруг.

Если рассуждать в понятиях ООП, то с помощью typedef struct мы описали класс объекта. Сам объект мы создаём как обычную переменную:

FieldView fieldView;

Аналогичным образом задаём модель типа FieldModel:

-2

Теперь, чтобы нарисовать поле, достаточно передать в функцию только указатель на представление (fieldView):

draw_field(&fieldView);

Всё остальное уже есть в самом представлении.

То, что произошло, называется рефакторинг. Иначе говоря, мы посмотрели на код, решили, что так дело не пойдёт, и переписали какие-то места заново, используя новый подход.

Это позволило логически разделить функции модели и представления, разгрузить minesweeper.c от лишнего кода и сократить код в основном цикле опроса, и теперь он находится на грани терпимости. Надо ли будет его разгружать дальше – посмотрим, но по факту в игре осталось сделать совсем немного.

Установка флажков

В обработку нажатия кнопки мыши добавлено дополнительное условие: нажатие сделано левой или правой кнопкой? Если правой, то мы ставим флажок на клетку (делая логическое "или" с маской F_FLAG), но тут не всё так просто. Если на клетке уже стоял флажок, то вместо флажка мы ставим вопрос (F_QUESTION), а если стоял вопрос, то снимаем пометку.

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

Также, левый клик не работает, если на клетке стоит флажок или вопрос.

Объединение

Можно заметить, что мы используем одну структуру event типа SDL_Event для всех событий. События бывают очень разные, и естественно, свойства у них тоже разные. Например, при движении мыши мы получаем координаты мыши из свойства motion:

event.motion.x, event.motion.y

А при нажатии кнопки получаем те же координаты из свойства button:

event.button.x, event.button.y

И motion, и button это в свою очередь разные структуры разного типа. Но неужели они все находятся внутри структуры типа SDL_Event?

То есть каким-то образом, имея лишь одну структуру event, мы в одном случае работаем с event.motion, а в другом случае с event.button?

Если тип SDL_Event описывает все структуры и подструктуры для всех типов событий, то какого же размера он должен быть? Ответ: 56 байт.

Почему так мало (учитывая, что одно поле int это сразу 4 байта)?

Здесь мы имеем дело с объединением, или union. Это специальный метод описания структур.

Например, мы можем задать такое объединение:

typedef union {
int a;
char b;
} MyUnion;

Если бы это была структура, то у неё было бы два поля: int a и char b. Но так как это объединение, в результате получается структура с одним полем. Но это поле может называться и "a", и "b". Если мы напишем так:

MyUnion test;
test.a = 5;

То test.a будет трактоваться как целочисленное (int) поле длиной 4 байта, которому присвоится значение 5.

Если же мы напишем:

test.b = 'A';

То test.b будет трактоваться как символьное (char) поле длиной 1 байт, которому присвоится значение 'A'.

Но и в том и в другом случае эти значения попадут в одну и ту же область памяти. То есть, записывая значение test.a, мы затираем значение test.b, и наоборот. Нам также ничто не мешает записать, например, значение сначала как test.b, а потом прочитать его как test.a. Мы можем трактовать их как хотим.

Собственно, таким образом SDL_Event объединяет много разных описаний структур в одной области памяти. И когда мы пишем event.motion.x или event.button.x, то скорее всего обращаемся к одному и тому же адресу памяти внутри структуры.

В этом можно увидеть проявление полиморфизма.

Рекурсивное открытие клеток

Функция open_cell() открывает одну клетку. То есть просто убирает с неё маску F_CLOSED. Но есть нюанс. Если вокруг этой клетки 0 мин, то нужно открыть все соседние клетки. Далее, если вокруг одной из соседних клеток тоже 0 мин, нужно открыть её соседние клетки, и т.д.

Таким образом, мы имеем типичную рекурсию. Функция вызывает сама себя. Происходит это так:

Функция на вход получает указатель на поле и координаты клетки. Если клетка уже открыта, она ничего не делает. Иначе она открывает клетку. Если вокруг открытой клетки находятся мины (число окружающих мин хранится в самой клетке), то функция больше ничего не делает. Если же мин вокруг нет, то функция по очереди берёт окружающие клетки: слева сверху, сверху, справа сверху, слева, справа и т.д. На каждой клетке функция вызывает сама себя и передаёт туда координаты этой клетки. Стало быть, новый вызов функции будет иметь дело уже с этой клеткой: он также откроет её, проверит, и если что запустит сам себя на окружающих клетках.

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

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

Ну а в юбилейном 10-м выпуске попробуем финализировать игру.

Читайте дальше: Меню и провал планирования

Читайте также: