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

Пишем Сапёра на C + SDL #6: Графический интерфейс

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

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

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

Я хочу полностью повторить интерфейс оригинального Сапёра (пробовал играть в несколько других версий, но они не вызывают такого ощущения привычного уюта, как оригинальная версия).

Понадобятся следующие элементы: иконки мины, флажка, смайлик (два или три), цифры для таймера и счётчика мин, цифры от 1 до 8, а также общие строительные блоки с эффектом рельефности.

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

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

Как устроена видеопамять?

Это специальная область памяти, к которой можно получить доступ в виде адреса. Если говорить абстрактно, то скажем, с адреса 100500 начинается область видеопамяти.

Если записать что-то в видеопамять, то эта информация появится на экране в виде цветных пикселов. А вот как вычислить положение и цвет каждого пиксела, зависит от того, какой формат пиксела используется.

Формат зависит от возможностей видеокарты и выбранного видеорежима. Опять же абстрактно, если режим чёрно-белый, то один бит в памяти это один пиксел: 0 это чёрный, 1 это белый. Существовали и другие довольно экзотические режимы, но в наше время используются режимы TrueColor. Это значит, что цвет задаётся 3-мя или 4-мя байтами. Первые три байта это значения R, G, B, а четвёртый или не используется, или хранит прозрачность.

Следовательно, зная адрес начала видеопамяти, формат цвета (RGB 32 бита), и размеры экрана (например 640 * 480), можно получить адрес пиксела с координатами (x = 50, y = 100) так:

  • каждый пиксел это 4 байта, значит смещение по "x" это 4 * 50
  • каждые 4 * 640 байт от начала памяти это начало новой строки, значит смещение по "y" это 4 * 640 * 100
  • итого адрес пиксела это 100500 + 4 * 50 + 4 * 640 * 100

Берём, допустим, зелёный цвет. Это R=0, G=255, B=0. То есть цвет будет представлен как 32-битное число, состоящее из 4-х байт: (0,255,0,0). Это число мы записываем в вычисленный ранее адрес и вот, мы получили пиксел зелёного цвета по координатам (50, 100).

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

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

В языке C есть функция memset(). Она должна использовать машинную инструкцию (не могу сказать про все процессоры, но как минимум про x86) stosb / stosw / stosd, которая в цикле (это не программный, а аппаратный цикл) записывает значение в область памяти. То есть мы говорим: вот тебе значение (размером 1 / 2 / 4 байта), запиши его N раз подряд начиная с этого адреса. (Поправка: я освежил информацию о memset(), и оказалось, что она работает только с байтами.)

В результате мы получим в видеопамяти строку из N повторяющихся значений, а иначе говоря – нарисованную горизонтальную линию.

Горизонтальная линия же – это прямоугольник с высотой 1 пиксел.

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

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

Что насчёт изображений?

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

  • Картинка это по сути своей прямоугольник
  • Есть быстрая машинная инструкция для перемещения данных из памяти в память

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

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

Есть, однако, и другая функция языка C: memcpy(), которая должна использовать машинную инструкцию movsb. Она делает именно то, что требуется. Мы говорим: вот тебе адрес, откуда копировать данные, вот тебе адрес, куда копировать данные. Скопируй оттуда туда N раз по 1 байту.

И так, линия за линией, картинка переносится в видеопамять. При этом, конечно, требуется, чтобы цветовые данные картинки совпадали с цветовым форматом выбранного видеорежима, потому что они копируются как есть. Если мы загружаем, скажем, картинку в формате JPG, то её необходимо предварительно декодировать в последовательность RGB-байт.

Звучит как много работы и сложностей, но на самом деле руками мы фактически ничего не делаем. Библиотека SDL2 предоставляет нам готовую функцию SDL_FillRect() для рисования прямоугольника, которая уже содержит все необходимые оптимизации. Нам не нужно считать адреса и вычислять форматы пикселов. Также и картинки мы можем загружать какие угодно, и не волноваться о том, как перевести их в нужный формат. Для рисования картинки мы будем использовать SDL_BlitSurface(), но об этом немного позже.

Итак, некоторые элементы очевидно будут картинками. Это иконка флажка, смайлик, и стилизованные 7-сегментные цифры. Их мы просто сделаем в графическом редакторе, экспортируем в PNG и загрузим в программу.

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

Рассмотрим элементы панели. Мы можем нарисовать её с помощью пяти прямоугольников (на картинке помечены номерами):

-2

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

-3

Есть, правда, один недостаток. Это обработка углов, где должна быть диагональная граница:

-4

Очевидно, с помощью прямоугольников можно получить только горизонтальные и вертикальные границы. Что делать с диагональной? Её можно сделать, рисуя в цикле горизонтальные линии с последовательно уменьшающейся длиной:

-5

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

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

Отмечу три момента:

  1. Рисование "лесенки" я совместил с рисованием верхней и нижней горизонтальной полоски. Нет смысла рисовать полоску и "лесенку" отдельно, так как можно удлинить край "лесенки" сразу на ширину полоски.
  2. Цвета я передаю в функцию как целые 32-битные числа – это удобнее, чем передавать по отдельности R,G,B, и также можно записывать их в 16-ричном виде, как в HTML. Но так как SDL_MapRGB() требует именно три отдельных компонента, я получаю их путём сдвига и битовых операций.
  3. Я также передаю параметр draw_body. Если он не нулевой, то рисуется центральная часть панели, а если нулевой – только рамка. Это позволяет не рисовать лишнего, например, когда мы рисуем панель поверх панели и они одного цвета.

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

-6

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

В следующем выпуске сделаем картинки и засунем их в программу.

Читайте дальше: Конвертирование изображений

Читайте также: Битовые операции

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