SDLЭто вольный перевод и переосмысление статьи, SDL Coordinates and Bliting за авторством Tim Jones. Готовый код я буду выкладывать на Github, откуда вы сможете скачать так же ZIP-архив. Если вы увидите ошибку, неточность, или у вас возникнут проблемы - обращайтесь: вы можете оставить комментарий или написать мне на почту, она указана внизу страницы.

Используя первую статью, как базу, углубимся в мир поверхностей SDL. Я пытался донести до вас мысль, что SDL поверхности - это в основном изображения, хранящиеся в памяти. Представьте что, у нас есть пустая поверхность размером 320x240 пикселей. Если добавить к ней систему SDL координат, то получится что-то вроде этого:

Эта система координат отличается, от обычной. Обратите внимание, что координата Y увеличивается вниз, а координата X увеличивается вправо. Это важно понимать, чтобы правильно рисовать изображения на экране.

Поскольку у нас уже есть основная поверхность (Screen), нам нужен способ, чтобы рисовать на ней. Этот процесс называется блиттинг [Blitting - Перенос битовых блоков], когда мы в общем переносим одно изображение на другое. Но прежде чем мы сможем это сделать, мы должны загрузить эти изображения в память. SDL предлагает простую функцию для этого - SDL_LoadBMP. Примерный код может выглядеть следующим образом:

SDL_Surface* TmpSurface;

if((TmpSurface = SDL_LoadBMP("mypicture.bmp")) == NULL) {
    //Error!
}

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

Давайте этот процесс завернем класс. Возьмем код из предыдущей статьи за основу и добавим два новых файла: Sprite.h и Sprite.cpp. Откройте Sprite.h и добавьте следующее:

#ifndef _SPRITE_H_
#define _SPRITE_H_

#include <SDL.h>

class Sprite {
    public:
        Sprite();
    public:
        static SDL_Surface* Load(const char* File);
};

#endif

Мы создали простую статическую функцию Load, которая будет загружать файл в поверхность. Теперь откроем Sprite.cpp:

#include "Sprite.h"

Sprite::Sprite() {}

SDL_Surface* Sprite::Load(const char* File) {
    SDL_Surface* Temp = NULL;
    SDL_Surface* Result = NULL;

    if((Temp = SDL_LoadBMP(File)) == NULL) {
        return NULL;
    }

    Result = SDL_DisplayFormat(Temp);
    SDL_FreeSurface(Temp);

    return Result;
}

Надо отменить несколько важных вещей. Во-первых, всегда помните, что когда вы создаете указатель, установите его в значение NULL или 0. Вы можете столкнуться с проблемами, если вы это не сделаете. Во-вторых, обратите внимание, что SDL_DisplayFormat возвращает новую поверхность, а не перезаписывает оригинальную. Это важно помнить, потому что, поскольку он создает новую поверхность, мы должны освободить старую. В противном случае, у она так и будет занимать память.

Теперь у нас есть способ загрузки поверхностей в памяти, теперь же нужен способ отображать их поверх других поверхностей. Как и в случае с загрузкой изображений, SDL предлагает функцию для блиттинга: SDL_BlitSurface. Она не такая простая как SDL_LoadBMP, но все же достаточно простая. Откроем Sprite.h и добавим следующие прототипы функций:

#ifndef _SPRITE_H_
#define _SPRITE_H_

#include <SDL.h>

class Sprite {
    public:
        Sprite();

        static SDL_Surface* Load(const char* File);
        static bool Draw(SDL_Surface* Dest, SDL_Surface* Src, int X, int Y);
};

#endif

Теперь откройте Sprite.cpp, и добавьте следующее:

#include "Sprite.h"

Sprite::Sprite() {
}

SDL_Surface* Sprite::Load(const char* File) {
    SDL_Surface* Temp = NULL;
    SDL_Surface* Result = NULL;

    if((Temp = SDL_LoadBMP(File)) == NULL) {
        return NULL;
    }

    Result = SDL_DisplayFormat(Temp);
    SDL_FreeSurface(Temp);

    return Result;
}

bool Sprite::Draw(SDL_Surface* Dest, SDL_Surface* Src, int X, int Y) {
    if(Dest == NULL || Src == NULL) {
        return false;
    }

    SDL_Rect Area;
    Area.x = X;
    Area.y = Y;

    SDL_BlitSurface(Src, NULL, Dest, &Area);

    return true;
}

Прежде всего, посмотрим на аргументы, которые передаются функции Draw. У нас есть две поверхности, и две переменные типа int. Первая поверхность Dest - на ней мы будем рисовавать, вторая поверхность Src - из нее будут браться данные для отображения. Можно сказать, что мы "наклеиваем" (помните аналогию со стикерами?) Src на Dest. X, Y - это координаты на поверхности Dest где будет находиться верхний левый угол изображения из Src.

Теперь к содержимому функции. Сначала мы проверяем, что у нас есть действительная поверхность, иначе возвращаем false. Далее, определяем SDL_Rect. Это SDL структура, которая состоит из четырех элементов: X, Y, W, H. Как вы, возможно, догадались, это задает размеры прямоугольника. В нашем случае, мы заботимся только о том, где мы рисуем, а не сколько. Поэтому, мы назначаем X, Y координаты на поверхности Dest. Если вам интересно, что означает NULL среди параметров функции SDL_BlitSurface, то это еще один параметр SDL_Rect. К нему мы вернемся позже. Наконец, мы вызваем функцию переноса изображения, и возвращаем true.

Теперь, чтобы убедиться, что все это работает, давайте создадим тестовую поверхность. Откройте App.h, и создайте новую поверхность, и подключите недавно созданный Sprite.h:

#ifndef _APP_H_
#define _APP_H_

#include <SDL.h>
#include "Sprite.h"

class App {
    private:
        bool            Running;
        SDL_Surface*    Screen;
        SDL_Surface*    Test;

    public:
        App();
        int Execute();

    public:
        bool Init();
        void Event(SDL_Event* Event);
        void Loop();
        void Render();
        void Cleanup();
};

#endif

Не забудьте инициализировать поверхность значением NULL в конструкторе:

App::App() {
    Test = NULL;
    Screen = NULL;
    Running = true;
}

И так же не забудьте "прибраться" за собой:

void App::Cleanup() {
    SDL_FreeSurface(Test);
    SDL_FreeSurface(Screen);
    SDL_Quit();
}

Давайте теперь загрузим изображение. Перейдите к функции App::Init(), добавьте код:


bool App::Init() { if(SDL_Init(SDL_INIT_EVERYTHING) < 0) { return false; } if((Screen = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) { return false; } if((Test = Sprite::Load("image.bmp")) == NULL) { return false; } return true; }

Не забудьте заменить "image.bmp" реальным изображением, которое у вас есть. Если нету - откройте MSPaint, что-нибудь нарисуйте, и сохраните в той же папке, где будет ваш исполняемый файл. Теперь у нас есть загруженное изображение, которое можно нарисовать. Найдите App::Render и добавьте следующее:

void App::Render() {
    Sprite::Draw(Screen, Test, 0, 0);

    SDL_Flip(Screen);
}

Обратите внимание на новую функцию SDL_Flip. Это просто обновление буфера и вывод Screen на экран. Это называется двойной буферизацией. Это процесс рисования всего в памяти, и, потом вывод на экран. Если этого не делать, получилось бы мерцающее изображение. Помните флаг SDL_DOUBLEBUF? Это он и включает двойную буферизацию.

Скомпилируйте код и убедитесь, что все работает правильно. Вы должны увидеть изображение в верхнем левом углу экрана. Если да, то поздравляю, вы на один шаг ближе к самой игре. Если нет, убедитесь, что ваш image.bmp находится в той же папке, что и исполняемый файл. Кроме того, убедитесь, что это действительно bitmap файл.

Теперь давайте продвинемся немного дальше. Хотя это круто уметь рисовать изображение на экране, часто нам нужна только его часть. Возьмем, к примеру, набор тайлов (tileset):

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

#ifndef _SPRITE_H_
#define _SPRITE_H_

#include <SDL.h>

class Sprite {
    public:
        Sprite();

        static SDL_Surface* Load(const char* File);
        static bool Draw(SDL_Surface* Dest, SDL_Surface* Src, int X, int Y);
        static bool Draw(SDL_Surface* Dest, SDL_Surface* Src, int X, int Y, int X2, int Y2, int W, int H);
};

#endif

Теперь в Sprite.cpp (Внимание, мы добавляем еще одну функцию Draw, мы не заменяем первую):

bool Sprite::Draw(SDL_Surface* Dest, SDL_Surface* Src, int X, int Y, int X2, int Y2, int W, int H) {
    if(Dest == NULL || Src == NULL) {
        return false;
    }

    SDL_Rect DestArea;

    DestArea.x = X;
    DestArea.y = Y;

    SDL_Rect SrcArea;

    SrcArea.x = X2;
    SrcArea.y = Y2;
    SrcArea.w = W;
    SrcArea.h = H;

    SDL_BlitSurface(Src, &SrcArea, Dest, &DestArea);

    return true;
}

Обратите внимание, что это в основном точно такая же функция, как и первыая, за исключением того, мы добавили еще один SDL_Rect. Этот SDL_Rect прямоугольник определяет, какие пикселы из исходного изображения скопировать в итоговое. Если, например, указаны параметры 0, 0, 50, 50, то отрисовано было бы лишь левая верхняя часть поверхности (квадрат размером 50х50).

Давайте проверим как это работает, найдите App::Render(), и добавьте следующее:

void App::Render() {
    Sprite::Draw(Screen, Test, 0, 0);
    Sprite::Draw(Screen, Test, 70, 110, 90, 90, 50, 50);

    SDL_Flip(Screen);
}

coordinates

Должно получиться, что ваше изображение отображается по координатам 70, 110 и видна только его часть. Надо обратить особое внимание на то, как эти функции работают, и как устроена система SDL координат, мы будем еще не раз это использовать.