Шрифты True Type

Пришло время научится рендерить текст. `SDL` не поддерживает `*.ttf` нативно, так что нам понадобится расширение `SDL_ttf`. Оно позволяет создавать поверхности из шрифтов True Type.Вы можете скачать `SDL_ttf` [здесь](http://www.libsdl.org/projects/SDL_ttf/). Для установки расширения, вы можете воспользоваться моей [инструкцией](http://tatrix.org/sdl-tutorial/3/). Установка `SDL_ttf` выполняется очень похоже на установку `SDL_image`. Пользователем \*nix систем возможно придется линковать приложение с freetype. Этот туториал раскрывает основы использования `SDL_ttf`.
//Поверхности
SDL_Surface *background = NULL;
SDL_Surface *message = NULL;
SDL_Surface *screen = NULL;
//Структура события
SDL_Event event;
//Шрифт, который мы будем использовать
TTF_Font *font = NULL;
//Цвет шрифта
SDL_Color textColor = { 255, 255, 255 };

Объявляем переменные. Тут у нас поверхности фона и экрана, а так же структура событий, как и раньше. Так же у нас есть поверхность message, которая будет содержать поверхность с текстом. Новый тип данных TTF_Font определяет шрифт, который мы будем использовать. Плюс структура SDL_Color определяющая цвет текста для рендеринга. В нашем случае цвет — белый. Если вы хотите больше узнать про тип данных SDL_Color, можете посмотреть о нем в документации SDL.

bool init() {
    //Инициализировать все подсистемы SDL
    if( SDL_Init( SDL_INIT_EVERYTHING ) == -1 ) {
        return false;
    }
    //Подготовить экран
    screen = SDL_SetVideoMode(
        SCREEN_WIDTH,
        SCREEN_HEIGHT,
        SCREEN_BPP,
        SDL_SWSURFACE
    );
    //Если при подготовке экрана произошла ошибка
    if( screen == NULL ) {
        return false;
    }
    //Инициализировать SDL_ttf
    if( TTF_Init() == -1 ) {
        return false;
    }
    //Установить заголовок окна
    SDL_WM_SetCaption( "TTF Test", NULL );
    //Если все прошло хорошо
    return true;
}

Это наша функция инициализации. Все тоже самое что и раньше, только в этот раз мы дополнительно инициализируем SDL_ttf``SDL_ttf инициализируется вызовом TTF_Init(). TTF_Init() возвращает -1 в случае ошибки.TTF_Init() должна быть вызвана до использованию любой другой функции SDL_ttf.

bool load_files() {
    //Загрузить фоновое изображение
    background = load_image( "background.png" );
    //Открыть шрифт
    font = TTF_OpenFont( "lazy.ttf", 28 );
    //В случае проблем с загрузкой фона
    if( background == NULL ) {
        return false;
    }
    //При ошибке загрузки шрифта
    if( font == NULL ) {
        return false;
    }
    //Если все загрузилось
    return true;
}

Это наша функция загрузки файлов. Для загрузки *.ttf шрифта нужно вызвать TTF_OpenFont(). Первый аргумент TTF_OpenFont() это имя *.ttf файла, который вы хотите открыть. Второй аргумент — размер который вы хотите установить открываемому шрифту. В случае ошибки при загрузки шрифта, TTF_OpenFont() вернет NULL.

    //Отрендерить текст
    message = TTF_RenderText_Solid(
        font,
        "The quick brown fox jumps over the lazy dog",
        textColor
    );
    //При ошибке рендера
    if( message == NULL ) {
        return 1;
    }
    //Скопировать изображения на экран
    apply_surface( 0, 0, background, screen );
    apply_surface( 0, 150, message, screen );
    //Обновить экран
    if( SDL_Flip( screen ) == -1 ) {
       return 1;
    }

Это код рендеринга внутри функции main(). Быстрейший путь отрендерить текст это использовать функцию TTF_RenderText_Solid(). TTF_RenderText_Solid() принимает шрифт в качестве первого аргумента и создает поверхность с текстом из второго аргумента и цветом из третьего. TTF_RenderText_Solid() возвращает NULL в случае ошибки. Существуют и другие способы рендеринга текста, проверьте их в документации SDL_ttf. Для некоторых пользователей Линукса TTF_RenderText_Solid() не сработает. В этом случае убедитесь что у вас используется последняя версия библиотеки freetype (библиотека на которой основана SDL_ttf) и сама SDL_ttf. Если это не помогло, попробуйте использовать TTF_RenderText_Shaded() вместо TTF_RenderText_Solid().

void clean_up() {
    //Освободить поверхности
    SDL_FreeSurface( background );
    SDL_FreeSurface( message );
    //Закрыть использованный шрифт
    TTF_CloseFont( font );
    //Завершить SDL_ttf
    TTF_Quit();
    //Завершить SDL
    SDL_Quit();
}

Это наша функция очистки. Сначала мы освобождаем поверхность экрана, а затем избавляемся от сгенерированной поверхности с текстом. Мы также закрываем открытый шрифт при помощи TTF_CloseFont() и затем завершаем работу SDL_ttf при помощи TTF_Quit(). После этого мы как обычно завершаем работу SDL.

Скачать исходники и материалы

Копирование с обрезкой и листы спрайтов

Листы спрайтов это коллекции картинок, хранящиеся в одном файле изображения. Они полезны когда у вас есть большое количество изображений, но вы не хотите иметь дело с большим количеством файлов. Для того, чтоы получить индивидуальное изображение, вы должны быть в состоянии вырезать нужную часть при копировании. В этом уроке у нас есть лист спрайтов с четырьмя разными спрайтами с изображением точки на них. Далее мы рассмотрим как вырезать отдельные спрайты из листа.
//Поверхности
SDL_Surface *dots = NULL;
SDL_Surface *screen = NULL;
//Структура события
SDL_Event event;
//Части карты спрайтов для копирования
SDL_Rect clip[ 4 ];

Тут у нас несколько глобальных переменных. Поверхность экрана и структура для событий с которой мы недавно знакомились. Также у нас есть поверхность dots, которая является листом спрайтов, содержащим четыре спрайта точек. Также там у нас есть массив из четырех SDL_Rect. Они хранят смещения и измерения спрайтов с точками.

void apply_surface(
    int x,
    int y,
    SDL_Surface* source,
    SDL_Surface* destination,
    SDL_Rect* clip = NULL
) {
    //Смещение
    SDL_Rect offset;
    //Получить смещение
    offset.x = x;
    offset.y = y;
    //Скопировать
    SDL_BlitSurface( source, clip, destination, &offset );
}

Это наша функция копирования, но с небольшими корректировками. Новый аргумент типа SDL_Rect по имени clip определяет прямоугольный кусок поверхности, который мы хотим скопировать. Значени аргумента по умолчанию устанавливается в NULL, что означает, что

apply_surface( 0, 0, image, screen, NULL );

и

apply_surface( 0, 0, image, screen );

делают одно и тоже. Мы также изменяем способ вызова SDL_BlitSurface(). Мы больше не устанавливаем второй аргумент в NULL, теперь мы передаем туда аргумент clip. Теперь SDL_BlitSurface() будет копировать область поверхности источника, определенную в прямоугольнике clip. Если clip равен NULL, значит будет скопирована вся поверхность источника.

    //Clip range for the top left
    clip[ 0 ].x = 0;
    clip[ 0 ].y = 0;
    clip[ 0 ].w = 100;
    clip[ 0 ].h = 100;

    //Clip range for the top right
    clip[ 1 ].x = 100;
    clip[ 1 ].y = 0;
    clip[ 1 ].w = 100;
    clip[ 1 ].h = 100;

    //Clip range for the bottom left
    clip[ 2 ].x = 0;
    clip[ 2 ].y = 100;
    clip[ 2 ].w = 100;
    clip[ 2 ].h = 100;

    //Clip range for the bottom right
    clip[ 3 ].x = 100;
    clip[ 3 ].y = 100;
    clip[ 3 ].w = 100;
    clip[ 3 ].h = 100;

В функции main после того, как все инициализировано и файлы загружены, мы устанавливаем координаты прямоугольников для вырезания. Мы собираемся взять этот спрайт: и установить прямоугольники для вырезания по определенным областям: Как-то так. Теперь мы готовы копировать отдельные спрайты с листа.

    //Залить экран белым цветом
    SDL_FillRect(
        screen,
        &screen->clip_rect,
        SDL_MapRGB( screen->format, 0xFF, 0xFF, 0xFF )
);

Тут мы заполняем экран белым цветом, при помощи SDL_FillRect(). SDL_FillRect() принимает поверхность первым аргументом и заполняет область указанную во втором аргументе цветом, указанным в третьем. Область во втором аргументе это прямоугольник самой поверхности или, проще говоря, вся поверхность целиком.

    //Скопировать поверхности на экран
    apply_surface( 0, 0, dots, screen, &clip[ 0 ] );
    apply_surface( 540, 0, dots, screen, &clip[ 1 ] );
    apply_surface( 0, 380, dots, screen, &clip[ 2 ] );
    apply_surface( 540, 380, dots, screen, &clip[ 3 ] );
    //Обновить экран
    if( SDL_Flip( screen ) == -1 ) {
        return 1;
    }

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

Скачать исходники и материалы

Хромакей

Сегодня мы будем учится применять хромакей (color key). По\-русски это означает, что этот урок научит вас удалять цвет фона при копировании поверхности.Структура `SDL_Surface` имеет элемент под названием `color key`. Хромакей это цвет который вы не хотите копировать при копировании поверхности. Это то, что используется для получения прозрачного фона. Скажем вы хотите скопировать фигуру из линий по имени Foo на такой фон: Но вы не хотите, чтобы вылезал голубой фон с изображения фигуры: Для того чтобы голубой цвет фона не появился нужно сделать хромакей равным цвету фона (в данном случае RGB #00FFFF). Обычно хромакей устанавливается при загрузке изображения.
SDL_Surface *load_image( std::string filename ) {
    //Временное хранилище для загружаемого изображения
    SDL_Surface* loadedImage = NULL;
    //Оптимизированное изображение, которое и будет использоваться
    SDL_Surface* optimizedImage = NULL;
    //Загрузить изображение
    loadedImage = IMG_Load( filename.c_str() );
    //Если изображение загружено
    if( loadedImage != NULL ) {
        //Создать оптимизированное изображение
        optimizedImage = SDL_DisplayFormat( loadedImage );
        //Освободить ресурсы из-под старого изображения
        SDL_FreeSurface( loadedImage );

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

        //Если изображение успешно оптимизировано
        if( optimizedImage != NULL ) {
            //Отобразить хромакей
            Uint32 colorkey = SDL_MapRGB( optimizedImage->format, 0, 0xFF, 0xFF );

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

            //Сделать все пиксели с цветом R 0, G 0xFF, B 0xFF прозрачными
            SDL_SetColorKey( optimizedImage, SDL_SRCCOLORKEY, colorkey );
        }

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

    //Вернуть оптимизированное изображение
    return optimizedImage;
}

После этого функция загрузки изображений возвращает оптимизированную с примененным хромакеем поверхность.

    //Скопировать поверхность на экран
    apply_surface( 0, 0, background, screen );
    apply_surface( 240, 190, foo, screen );
    //Обновить экран
    if( SDL_Flip( screen ) == -1 ) {
        return 1;
    }

Теперь фоновое изображение скопировано, изображение фигуры с хромакеем также скопировано. И теперь нет голубого фона вокруг изображения человечка. Для тех кто использует PNG с прозрачностью: IMG_Load() самостоятельно позаботится о прозрачности. Попытка применить хромакей к изображению, которое уже содержит прозрачный фон, приведет к пугающим результатам. Также вы потеряете альфа канал, если будете использовать SDL_DisplayFormat() вместо SDL_DisplayFormatAlpha(). Для того чтобы у изображений в PNG формате осталась прозрачность, просто не применяйте к ним хромакей. Также, IMG_load умеет обрабатывать прозрачность изображений в формате TGA. Загляните в документацию по SDL за дополнительной информацией по данной технике.

Скачать исходники

Событийно-ориентированное программирование

До этого момента, вы, вероятно, писали программы в "командно-ориентированном" стиле, используя cin и cout. Эта руководство научит вас как проверять и обрабатывать события.

В оригинале автор использует термин "X out". Как я понял под этим подразумевается закрытие приложение, либо при помощи нажатия на значок крестика в заголовке окна, либо при помощи соответствующего клавиатурного сочетания, например, Alt + F4. Я буду называть это действие "закрытие приложения"

Событие это просто что-то, что произошло. Это может быть нажатие клавиши, движение мыши, изменение размера окна или в нашем случае, желание пользователя закрыть окно.

//Заголовки
#include
#include
#include
//Аттрибуты экрана
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int SCREEN_BPP = 32;
//Поверхности, которые будут использоваться
SDL_Surface *image = NULL;
SDL_Surface *screen = NULL;

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

//Структура для событий, которые будут использоваться
SDL_Event event;

А вот это уже кое-что новенькое. Структура SDL_Event хранит данные события, которые мы будем обрабатывать.

SDL_Surface *load_image( std::string filename ) {
    //Временное хранилище для загружаемого изображения
    SDL_Surface* loadedImage = NULL;
    //Оптимизированное изображение, которое и будет использоваться
    SDL_Surface* optimizedImage = NULL;
    //Загрузить изображение
    loadedImage = IMG_Load( filename.c_str() );
    //Если изображение загружено
    if( loadedImage != NULL ) {
        //Создать оптимизированное изображение
        optimizedImage = SDL_DisplayFormat( loadedImage );
        //Освободить ресурсы из-под старого изображения
        SDL_FreeSurface( loadedImage );
    }
    //Вернуть оптимизированное изображение
    return optimizedImage;
}
void apply_surface( int x, int y, SDL_Surface* source, SDL_Surface* destination ) {
    //Делаем прямоугольник, для временного хранения смещений
    SDL_Rect offset;
    //Кладем смещения в прямоугольник
    offset.x = x;
    offset.y = y;
    //Копировать поверхность
    SDL_BlitSurface( source, NULL, destination, &offset );
}

Это наши функции для загрузки и копирования поверхностей. По сравнению с прошлой частью руководства ничего не поменялось.

bool init() {
    //Инициализировать все подсистемы SDL
    if( SDL_Init( SDL_INIT_EVERYTHING ) == -1 ) {
        return false;
    }
    //Подготовить экран
    screen = SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT,
        SCREEN_BPP, SDL_SWSURFACE );
    //Если при подготовке экрана произошла ошибка
    if( screen == NULL ) {
        return false;
    }
    //Установить заголовок окна
    SDL_WM_SetCaption( "Event test", NULL );
    //Если все прошло хорошо
    return true;
}

Это наша функция инициализации. Она запускает SDL, подготавливает окно, устанавливает заголовок и возвращает false если происходит какая-нибудь ошибка.

bool load_files() {
    //Загрузить изображение
    image = load_image( "x.png" );
    //Если при загрузке произошла ошибка
    if( image == NULL ) {
        return false;
    }
    //Если все загрузилось
    return true;
}

Это функция загружающая файлы. Она загружает изображения и возвращает false если возникли какие-то проблемы.

void clean_up() {
    //Освободить поверхность
    SDL_FreeSurface( image );
    //Завершаем SDL
    SDL_Quit();
}

Здесь у нас функция очистки, вызываемая при завершении программы. Она освобождает нашу поверхность и завершает работу SDL.

int main( int argc, char* args[] ) {
    //Подтверждение того, что программа ожидает выхода
    bool quit = false;

Теперь мы дошли до нашей функции main Здесь мы объявляем переменную quit, которая следит за тем, желает ли пользователь покинуть программу. Так как приложение только что запустилось, мы устанавливаем ее в false, иначе завершение работы произойдет немедленно.

    //Инициализация
    if( init() == false ) {
        return 1;
    }
    //Загрузить файлы
    if( load_files() == false ) {
        return 1;
    }

Теперь мы вызываем функции инициализации и загрузки файлов, сделанные ранее.

    //Скопировать поверхность на экран
    apply_surface( 0, 0, image, screen );
    //Обновить экран
    if( SDL_Flip( screen ) == -1 ) {
        return 1;
    }

Затем мы показываем изображение на экране.

//До тех пор, пока пользователь не захотел закрыть приложение
    while( quit == false ) {

Теперь мы запускаем главный цикл программы. Этот цикл выполняется до тех пор, пока пользователь не установит quit в true.

        //Пока есть события для обработки
        while( SDL_PollEvent( &event ) ) {

В SDL, когда происходит событие, она попадает в "очередь событий". Очередь событий хранит данные для каждого произошедшего события. Поэтому если вы нажали кнопку мыши, передвинули мышь, а затем нажали на клавишу на клавиатуре, очередь событий будет выглядеть как-то так: SDL_PollEvent() занимается тем, что берет событие из очереди и помещает его данные в нашу структуру: В результате этот код продолжает получать данные событий, пока очередь не опустеет.

             //Если пользователь попытался закрыть окно
            if( event.type == SDL_QUIT ) {
                //Закрываем приложение
                quit = true;
            }
        }
    }

Когда пользователь попытается закрыть приложение, тип полученного события будет SDL_QUIT. Но когда пользователь делает это, программа не закрывается; это только сообщает нам, что пользователь хочет выйти из программы. А поскольку мы хотим чтобы программа завершилась, когда пользователь попытается её закрыть, мы устанавливаем quit в true и это прерывает цикл, в котором мы находимся.

    //Освободить поверхность и завершить работу SDL
    clean_up();
    return 0;
}

В конце концов, мы выполняем функцию очистки. Есть и другие способы обработки событий, например, SDL_WaitEvent() и SDL_PeepEvents(). Вы можете узнать о них в документации.

Скачать исходники

Примечание: теперь самое время узнать побольше о функциях SDL по обработке ошибок. У меня нет руководства по ним, но я затрагиваю их в статье 5 (англ.). В документация по SDL объясняется функция SDL_GetError(), а документация по SDL_image объясняет IMG_GetError(). SDL_ttf и SDL_mixer так же имеют свои функции обработки ошибок, так что не забудьте посмотреть и их.

Допольнительные материалы:
Cобытийно-ориентированное программирование (Event Driven Programming)

Установка библиотек расширений

SDL изначально поддерживает только файлы .bmp, но при использовании библиотеки SDL_image, вы сможете загружать BMP, PNM, XPM, LBM, PCX, GIF, JPEG, TGA и PNG. Библиотеки расширений позволяют вам использовать функции, которые SDL не поддерживает изначально. Установка расширений SDL совсем не сложна. Я бы даже сказал, что это проще чем базовая настройка SDL. Это часть руководства научит вас тому, как установить и настроить библиотек SDL_image. Как только вы узнаете как установить это расширение, вы сможете установить и настроить любое другое.

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

  1. Пользователи Ubuntu могут воспользоваться пакетным менеджером. Открыв его ищите пакет libsdl-image1.2-dev, который является пакетом для разработки под SDL. Как только вы нашли его, нажимайте установить. Если вы устанавливаете SDL_ttf, ищите libsdl-ttf2.0-dev. Если SDL_mixed, ищите libsdl-mixer1.2-dev. И т.д. и т.д.
  2. Для дистрибутивов основанных на RPM, вам нужны пакеты SDL_image для разбаботчиков. Пакет SDL_image можно найти на этой странице. Прокрутите окно до секции "Binary" и скачайте нужный пакет: Теперь запустите RPM и дайте ей сделать свое дело.
  3. Если в первой части руководства вы использовали apt-get, aptitude или yum для установки SDL, то у вас уже должны быть установлены пакеты SDL_image, SDL_ttf и SDL_mixer.

Теперь, когда вы установили библиотеки разработчика, самое время научится компилировать приложения с использованием библиотек. Когда вы компилируете приложение, просто добавьте -lSDL_image после g++ -o myprogram mysource.cpp -lSDL Если вы связываете с SDL_ttf, добавляйте -lSDL_ttf. Если SDL_mixer, то -lSDL_mixer. И т.д. и т.д. Для использования SDL_image убедитесь что вы включили заголовочный файл.

#include

Для SDL_ttf:

#include

Для SDL_mixer:

#include

И т.д. и т.д. Теперь библиотека готова к использованию. И можно использовать функции SDL_image. Главная функция, о которой вы бы хотели узнать это IMG_Load().

SDL_Surface *load_image( std::string filename ) {
    //Временное хранилище для загружаемого изображения
    SDL_Surface* loadedImage = NULL;
     //Оптимизированное изображение, которое и будет использоваться
    SDL_Surface* optimizedImage = NULL;
    //Загружаем изображение при помощи SDL_image
    loadedImage = IMG_Load( filename.c_str() );
    //Если изображение загружено
    if( loadedImage != NULL ) {
        //Создать оптимизированное изображение
        optimizedImage = SDL_DisplayFormat( loadedImage );
        //Освободить ресурсы из-под старого изображения
        SDL_FreeSurface( loadedImage );
    }
    //Вернуть оптимизированное изображение
    return optimizedImage;
}

Перед вами пересмотренная версия функции загрузки изображений из предыдущей части руководства. Как вы можете видеть, IMG_Load() работает так же как и SDL_LoadBMP(), но с одни большим исключением: IMG_Load() может загружать файлы BMP, PNM, XPM, LBM, PCX, GIF, JPEG, TGA и PNG Начиная с этого момента, в руководствах будут использоваться изображения в формате PNG. Изображения в этом формате используют великолепный алгоритм сжатия без потерь.

Скачать исходники. Рекомендую держать под рукой документацию по SDL_image. Попоробуйте ввести в терминале

man IMG_Load

Если документация не установилась при установке пакетов, вы можете найти ее на этой странице.