Взяв за основу первый урок, мы будем углубляться в мир поверхностей SDL. Как я уже говорил, SDL поверхности, в основном, это изображения, сохраненные в памяти. Представьте себе, что у нас есть пустое окно размером 320x240 пикселей. В системе координат SDL, окно представлено следующим образом:
Эта система координат отличается от той к которой вы привыкли (я про декартову). Но основное отличие между этими системами в том, что координата Y «растет» вниз. Понимание системы SDL координат важно, чтобы правильно рисовать изображения на экране, так что уж вникните хорошенько.
Так как у нас уже есть подготовленная и настроенная поверхность (Surf_Display), нам осталось только найти способ отрисовки изображений. Этот способ называется блитированием (от англ. blitting — перемещение группы бит из одного места в другое, в нашем случае подразумевается перенос изображения (или его части) поверх другого), т.е. своего рода наложение. Но прежде чем мы сможем это сделать, мы должны найти ещё и способ загрузить эти изображения в память. SDL предлагает простую функцию, чтобы осуществить задуманное — SDL_LoadBMP (примечание: SDL_LoadBMP предоставляет возможность загрузки изображений только в формате *.BMP, как видно из её названия. Чтобы загружать изображения других форматов, к проекту нужно подключить библиотеку SDL_image, как справедливо заметил в комментариях товарищ alrusdi в первом уроке, и использовать функцию IMG_Load). Пример кода может выглядеть следующим образом:
SDL_Surface* Surf_Temp;
if((Surf_Temp = SDL_LoadBMP("mypicture.bmp")) == NULL) {
//Обшибка!
}
Здесь всё довольно просто, SDL_LoadBMP принимает в качестве параметра всего один аргумент — путь до файла, который вы хотите загрузить, а возвращает она поверхность, содержащую указанное изображение. Если функция возвращает NULL, то либо файл не найден, либо поврежден, либо возникли другие, более сложные ошибки. К сожалению, в ущерб эффективности, этот метод не обеспечивает полное покрытие всевозможных ошибок загрузки. Очень часто загруженное изображение не соответствует попиксельному формату той поверхности, в которую мы его загружаем. Таким образом во время отображения возможна потеря производительности, цветов изображения, и т.д. (важно чтобы подготавливаемая поверхность и загружаемое изображение подходили друг другу по всем параметрам, т.е. (утрируя) размер коробки подходил бы размерам груза). К счастью в SDL существует быстрый и безболезненный обход этой проблемы — SDL_DisplayFormat. Эта функция настраивает уже загруженное изображение, и возвращает новую поверхность, подходящую под формат отображаемой.
Теперь вам необходимо открыть проект, созданный в предыдущем уроке и добавить два файла: CSurface.h и CSurface.cpp. Откройте CSurface.h и добавьте следующее:
#ifndef _CSURFACE_H_
#define _CSURFACE_H_
#include <SDL/SDL.h>
class CSurface {
public:
CSurface();
public:
static SDL_Surface* OnLoad(char* File);
};
#endif
Тем самым мы создали простую функцию OnLoad, которая будет загружать для нас поверхность. Теперь откройте CSurface.cpp и добавьте:
#include "CSurface.h"
CSurface::CSurface() {
}
SDL_Surface* CSurface::OnLoad(char* File) {
SDL_Surface* Surf_Temp = NULL;
SDL_Surface* Surf_Return = NULL;
if((Surf_Temp = SDL_LoadBMP(File)) == NULL) {
return NULL;
}
Surf_Return = SDL_DisplayFormat(Surf_Temp);
SDL_FreeSurface(Surf_Temp);
return Surf_Return;
}
Итак, парочка вещей, на которые стоит обратить внимание:
1. Всегда обнуляйте свои указатели прежде чем как-либо их использовать (NULL или «0» неважно). Это поможет избежать туевой хучи самых разных проблем и ошибок;
2. Помните, что SDL_DisplayFormat возвращает новую поверхность на основе старой, поэтому не стоит забывать освободить ресурсы, занимаемые той старой поверхностью. В противном случае мы будем наблюдать поверхность «блуждающую» в памяти так как ей заблагорассудится.
Теперь у нас есть способ загрузки поверхностей в память, но нам также нужен способ, чтобы отобразить их на другие поверхности. Так же как и для загрузки изображений, у SDL есть функция и для этого: SDL_BlitSurface. Возможно её будет не так просто использовать как SDL_LoadBMP, но не стоит пугаться. Откройте CSurface.h и добавьте следующий прототип функции:
#ifndef _CSURFACE_H_
#define _CSURFACE_H_
#include <SDL/SDL.h>
class CSurface {
public:
CSurface();
public:
static SDL_Surface* OnLoad(char* File);
static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y);
};
#endif
Снова откройте CSurface.cpp и добавьте следующее:
#include "CSurface.h"
CSurface::CSurface() {
}
SDL_Surface* CSurface::OnLoad(char* File) {
SDL_Surface* Surf_Temp = NULL;
SDL_Surface* Surf_Return = NULL;
if((Surf_Temp = SDL_LoadBMP(File)) == NULL) {
return NULL;
}
Surf_Return = SDL_DisplayFormat(Surf_Temp);
SDL_FreeSurface(Surf_Temp);
return Surf_Return;
}
bool CSurface::OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y) {
if(Surf_Dest == NULL || Surf_Src == NULL) {
return false;
}
SDL_Rect DestR;
DestR.x = X;
DestR.y = Y;
SDL_BlitSurface(Surf_Src, NULL, Surf_Dest, &DestR);
return true;
}
Прежде всего, давайте взглянем на аргументы, которые передаются в функцию OnDraw. Мы видим две поверхности, и две переменные типа int. Первая поверхность берется в качестве базовой (помните доску в первом уроке?), т.е. той на которую мы и будем всё отображать в дальнейшем. Соответственно вторая поверхность — та, которую мы будем накладывать на базовую (а вот и наши стикеры). В принципе, мы просто размещаем Surf_Src поверх Surf_Dest, вот и весь секрет. X и Y — переменные, которые обозначают координаты места на поверхности Surf_Dest, в которое мы будем отображать Surf_Src.
В начале функции мы должны убедиться, что у нас есть поверхности, в противном случае мы возвращаем false. Далее, мы создаем переменную типа SDL_Rect. Это структура SDL, которая состоит из четырех свойств: X, Y, W, H. Вы уже конечно догадались что она то как раз и задает параметры отображаемого региона поверхности. Пока нас интересуют только координаты места в которое мы будем отображать прямоугольник, и нам наплевать на его размер. Итак, далее мы присваиваем переданные в функцию X, Y координаты структуре отображаемого региона. Если вам интересно, что же за параметр NULL затесался в нашей SDL_BlitSurface (да, автор, нам интересно!), это еще один параметр типа SDL_Rect. Мы вернемся к этому чуть позже.
Позже наступило! Думаю что никто не обидится если мы разберем сигнатуру SDL_BlitSurface чуть раньше. Вкратце поясню: нам не всегда нужно отображать всю поверхность поверх другой, есть много случаев, когда требуется выбрать какую-то часть изображения (например у нас есть тайлсет (от англ. tileset — набор изображений, попросту множество картинок, собранных в одном изображении) и нужно выбрать из него определённый квадратик с текстурой или персонажем, и т.д.). Так вот
int SDL_BlitSurface(SDL_Surface *src, SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect);
принимает в качестве параметров по порядку, слева направо:
- поверхность, которую будем накладывать;
- параметры региона отображения накладываемой поверхности (т.е. какую её часть мы будем отображать);
- поверхность, на которую будем накладывать;
- ну и, соответственно, параметры региона базовой поверхности, в который будем накладывать.
Думаю теперь всё стало более-менее прозрачно и понятно.
В завершении функции мы отрисовываем настроенные поверхности и возвращаем true.
Теперь, чтобы убедиться, что все работает, давайте создадим тестовую поверхность. Откройте CApp.h, и добавьте новую поверхность, и включите созданный нами заголовочный файл CSurface.h:
#ifndef _CAPP_H_
#define _CAPP_H_
#include <SDL/SDL.h>
#include "CSurface.h"
class CApp {
private:
bool Running;
SDL_Surface* Surf_Display;
SDL_Surface* Surf_Test;
public:
CApp();
int OnExecute();
public:
bool OnInit();
void OnEvent(SDL_Event* Event);
void OnLoop();
void OnRender();
void OnCleanup();
};
#endif
Также в конструкторе не забудьте сначала обнулить наши поверхности:
CApp::CApp() {
Surf_Test = NULL;
Surf_Display = NULL;
Running = true;
}
И помните об очистке!
#include "CApp.h"
void CApp::OnCleanup() {
SDL_FreeSurface(Surf_Test);
SDL_FreeSurface(Surf_Display);
SDL_Quit();
}
Настало время уже что-то загрузить. Откройте CApp_OnInit.cpp и приведите его к такому виду:
#include "CApp.h"
bool CApp::OnInit() {
if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
return false;
}
if((Surf_Display = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
return false;
}
if((Surf_Test = CSurface::OnLoad("myimage.bmp")) == NULL) {
return false;
}
return true;
}
Убедитесь в том что у вас действительно имеется файл myimage.bmp. Если нет — скачайте или нарисуйте сами и положите его в каталог с исполняемым файлом вашей игры. Откройте CApp_OnRender.cpp и добавьте следующее:
#include "CApp.h"
void CApp::OnRender() {
CSurface::OnDraw(Surf_Display, Surf_Test, 0, 0);
SDL_Flip(Surf_Display);
}
Обратите внимание на новую функцию SDL_Flip. Она обновляет буфер и отображает Surf_Display на экран. Это называется двойной буферизацией. Она подготавливает созданные поверхности сначала в памяти, а затем отображает подготовленное на экран. Если бы мы не использовали её, то наблюдали бы мерцающий экран. Помните флаг SDL_DOUBLEBUF, который мы указывали при создании поверхности? Он-то как раз и включает режим двойной буферизации.
Теперь вы можете откомпилировать проект, и убедиться, что все работает правильно. Вы должны увидеть изображение в верхнем левом углу окна. Если да, то поздравляю, вы еще на один шаг ближе к реальной игре. Если нет, то убедитесь в том, что у вас myimage.bmp лежит в той же папке, что и исполняемый файл, а также в том, что он нормально открывается в просмотрщике графики. Вот что получилось у меня:
(И да, я немного схитрил видоизменил код и загрузил свой аватар в формате *.PNG, используя IMG_Load. Советую вам тоже поэкпериментировать с этой функцией, да и с другими тоже. Дерзайте и у вас всё получится!). Если у вас появляется сообщение deprecated conversion from string constant to ‘char*’ -wwrite-strings
необходимо изменить сигнатуру функции OnLoad(char* File) на OnLoad(const char* File) в CSurface.h и соответственно в CSurface.cpp.
Двинемся дальше! Мы потешили себя тем, что отобразили, наконец, в окошке наше первое изображение, но очень часто нам необходимо отобразить всего лишь его часть, как пример — тайлсеты, указанные ниже:
Т.е. имея всего одно изображение, нам нужно нарисовать только его часть. Откройте CSurface.h, и добавьте следующий код:
#ifndef _CSURFACE_H_
#define _CSURFACE_H_
#include <SDL/SDL.h>
class CSurface {
public:
CSurface();
public:
static SDL_Surface* OnLoad(char* File);
static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y);
static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y, int X2, int Y2, int W, int H);
};
#endif
Откройте CSurface.cpp, и добавьте следующую функцию (Важно, мы добавляем вторую функцию OnDraw, а не заменяем уже имеющуюся! Вы же в курсе про перегрузку функций?):
bool CSurface::OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y, int X2, int Y2, int W, int H) {
if(Surf_Dest == NULL || Surf_Src == NULL) {
return false;
}
SDL_Rect DestR;
DestR.x = X;
DestR.y = Y;
SDL_Rect SrcR;
SrcR.x = X2;
SrcR.y = Y2;
SrcR.w = W;
SrcR.h = H;
SDL_BlitSurface(Surf_Src, &SrcR, Surf_Dest, &DestR);
return true;
}
Видите, это в основном та же функция, как и раньше, за исключением того, мы добавили еще один SDL_Rect. Этот регион позволяет указать, какие пиксели из накладываемой поверхности нужно скопировать на основную. Теперь вкупе с координатами мы указываем ещё и оставшиеся два параметра — ширину и высоту 0, 0, 50, 50 и в итоге получаем отображаемый регион в виде квадрата 50x50 пикселей.
#include "CApp.h"
void CApp::OnRender() {
CSurface::OnDraw(Surf_Display, Surf_Test, 0, 0);
CSurface::OnDraw(Surf_Display, Surf_Test, 100, 100, 0, 0, 50, 50);
SDL_Flip(Surf_Display);
}
А вот частичка моего аватара с отступом в 100 пикселей от верха и левого края экрана:
Ссылка на исходный код:
Ссылки на все уроки:
- Разработка игрового фрэймворка. Часть 1 — Основы SDL
- Разработка игрового фрэймворка. Часть 2 — Координаты и отображение
Автор: m0sk1t