Предыдущие части: Графический интерфейс, Указатели и графика, Делаем поле, Какую память выделить, Структура программы, Пишем Сапёра на С + SDL
Код для этой части находится в ветке images на github. Вы можете смотреть там все файлы онлайн и также скачать зип-архив всей ветки.
Я взял скриншоты Сапёра и перерисовал графику с них, чтобы у меня были чистые векторные исходники. Да, векторные, просто вместо пикселов я нарисовал прямоугольники:
Какой смысл рисовать пиксельную графику в векторе? Да никакого, мне просто было так удобнее, и времени почти не заняло.
Далее нужно поговорить о том, что и как работает в SDL2.
Поверхности и текстуры
В SDL2 есть два типа графических контекстов: поверхность (surface) и текстура. И тот и другой очень похожи друг на друга, и там и там можно рисовать. Однако принципиальное отличие заключается в том, что поверхность рисуется обычным способом, то есть задействуется оперативная память и процессор. А текстура рисуется в графическом ускорителе, то есть задействуется память видеокарты и процессор видеокарты.
Архитектура графического ускорителя в целом такова, что текстуру для рисования нужно в него загрузить. То есть даже когда мы имеем готовую к употреблению картинку в памяти, графический ускоритель её не видит, она не в той памяти. Далее, чтобы рисовать, или, как говорят, рендерить эту картинку на экран, её нужно установить в качестве текущей текстуры.
Допустим, у вас есть несколько разных картинок. Чтобы все их нарисовать через графический ускоритель, нужно:
- Перенести все картинки из оперативной памяти в графическую память как отдельные текстуры
- Перед рендерингом каждой картинки установить текущую текстуру (текстура это сама картинка)
И как раз эти операции – перенос в графическую память и назначение текущей текстуры – очень медленные. Поэтому, когда нужно рисовать много разных картинок много раз, их объединяют в одну (как на примере выше). Тогда у нас получается одна текстура, которую нужно загрузить в графическую память один раз и назначить текущей тоже один раз.
Чтобы рендерить разные картинки, используя одну текстуру, нужно просто указать, какой кусочек этой текстуры мы хотим взять. Например, нам надо нарисовать зелёную цифру "2" отсюда:
Относительно левого верхнего угла текстуры картинка с цифрой "2" находится по координатам x=30, y=21, и имеет размер w=10, h=10.
Следовательно, мы сообщаем рендереру, "сделай рендер кусочка с координатами (30,21) размером (10,10)"
То же самое можно делать и с поверхностью (уже вне графического ускорителя). Только разница в том, что её никуда не надо загружать и назначать, а значит, каждая отдельная картинка может быть отдельной поверхностью. Ну и конечно, всё это будет работать медленнее, так как графический ускоритель в этом не участвует.
Собственно, к чему это я. Мы будет использовать обычное рисование с поверхностями, потому что скорости нам хватает с головой, но картинки будем держать все вместе, как будто рисуем на ускорителе. Потому что большой разницы, как их хранить, нет.
Загрузка картинок
Традиционно ресурсы игры подгружаются из сопутствующих файлов, но Сапёр – игра из одного файла, поэтому вся графика находится сразу внутри неё. Следовательно, задача – сделать так, чтобы экспортированная из редактора картинка оказалась в программе.
Для этого мне пришлось написать ещё одну программу – image_convert.c. Она загружает из файла картинку в формате BMP. Как выяснилось, для других форматов одной SDL2 мало, нужно еще добавить библиотеку SDL_image. Я не стал возиться, BMP так BMP.
Так вот, она загружает картинку из файла с помощью функции SDL_LoadBMP(), и в результате получается поверхность, заполненная цветными RGB-пикселами. (Это не на экране. Поверхность просто в памяти.)
Получив параметры поверхности (ширина, высота, цветность и т.д.), я перебираю её пикселы и просто печатаю их значения. В результате получается такой текст:
Да, это тупо значения всех байтов поверхности, идущие через запятую. Да, их много (весь текст – около 50 килобайт), но в скомпилированном виде это займёт 5 килобайт (на самом деле 16, так как на 1 пиксел нужно 3 байта. Можно было бы хранить не сами байты цвета, а только номер цвета, но тогда пришлось бы дополнительно преобразовывать их потом). Я просто дописал к ним статическое объявление массива unsigned char icons[] и поместил это всё в отдельный файл icons.h. Теперь у меня есть массив, состоящий из всех байтов картинки, и он уже включён в программу с помощью #include "icons.h".
Конвертация поверхностей
Далее, уже в программе minesweeper.c, с помощью функции SDL_CreateRGBSurfaceFrom() я воссоздаю поверхность из байтового массива с теми же самыми параметрами длины, ширины, цветности и т.д., с которыми был сохранён массив.
Остаётся нарисовать эту поверхность на экранной поверхности. То есть да, поверхностей для рисования может быть много. И когда мы рисуем что-то на экране, мы переносим данные из какой-либо поверхности на экранную поверхность (по факту мы просто копируем блоки памяти из одного места в другое).
Но перед тем, как это сделать, необходимо переконвертировать нашу исходную поверхность с картинкой. Дело в том, что мы заранее не знаем, в каком видеорежиме будем работать, и какой формат будет у экранной поверхности. Поэтому нужно сделать так, чтобы формат нашей поверхности совпадал с экранным форматом.
Для этого вызывается функция SDL_ConvertSurface(), куда мы передаём указатель на свою поверхность и указатель на формат той поверхности, под которую нужно подстроиться. В ответ мы получаем указатель на новую, переконвертированную поверхность, и вот ей уже можно пользоваться.
Рисование
Чтобы скопировать кусок поверхности на другую поверхность, обычно используется функция SDL_BlitSurface(), но я использовал SDL_LowerBlit(). Она отличается лишь тем, что не делает дополнительных проверок. У нас нет таких ситуаций, что, допустим, картинка выходит за край поверхности, поэтому и проверки по идее не нужны. Посмотрим. Если что, можно переделать.
Я просто вывел всю поверхность в левый верхний угол, и можно убедиться, что всё работает:
К сожалению, сделать остальные доработки пока не хватает времени, так что подождём до следующего выпуска. План на него такой:
- Каждой картинке внутри поверхности назначить координаты, ширину и высоту, чтобы знать, откуда её брать.
- Подогнать все размеры и цвета как в оригинальном Сапёре.
- Сделать вывод поля с картинками.
- Освоить указание и клики мышью.
Читайте дальше: Библиотека картинок и мышь