События мыши

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

//Кнопка
class Button {
private:
    //Атрибуты кнопки
    SDL_Rect box;
    //Часть спрайт листа кнопки, которая будет отображаться
    SDL_Rect* clip;
public:
    //Инициализация переменных
    Button( int x, int y, int w, int h );
    //Обработка событий и выбор спрайта для отображения
    void handle_events();
    //Вывод кнопки на экран
    void show();
};

Это наш класс для кнопки, с которым мы будем взаимодействовать. В нем есть прямоугольник определяющий позицию и размеры кнопки. Так же у нас есть указатель на спрайт из спрайт листа, которым пользуется кнопка. Затем у нас есть конструктор, устанавливающий атрибуты кнопки в соответствие с переданными аргументами. Далее следует функция handle_events(), которая обрабатывает движение и другие события мыши. И наконец функция show() отображающая кнопку на экран.

void set_clips()
{
    //нарезаем спрайты
    clips[ CLIP_MOUSEOVER ].x = 0;
    clips[ CLIP_MOUSEOVER ].y = 0;
    clips[ CLIP_MOUSEOVER ].w = 320;
    clips[ CLIP_MOUSEOVER ].h = 240;

    clips[ CLIP_MOUSEOUT ].x = 320;
    clips[ CLIP_MOUSEOUT ].y = 0;
    clips[ CLIP_MOUSEOUT ].w = 320;
    clips[ CLIP_MOUSEOUT ].h = 240;

    clips[ CLIP_MOUSEDOWN ].x = 0;
    clips[ CLIP_MOUSEDOWN ].y = 240;
    clips[ CLIP_MOUSEDOWN ].w = 320;
    clips[ CLIP_MOUSEDOWN ].h = 240;

    clips[ CLIP_MOUSEUP ].x = 320;
    clips[ CLIP_MOUSEUP ].y = 240;
    clips[ CLIP_MOUSEUP ].w = 320;
    clips[ CLIP_MOUSEUP ].h = 240;    
}

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

Button::Button( int x, int y, int w, int h )
{
    //Установить атрибуты
    box.x = x;
    box.y = y;
    box.w = w;
    box.h = h;

    //Спрайт по умолчанию
    clip = &clips[ CLIP_MOUSEOUT ];
}

Конструктор класса кнопки довольно прост. Он устанавляет смещения кнопки по x и y, а так же ширину и высоту. Так же он устанавливает спрайт по умолчанию.

void Button::handle_events()
{
    //Смещения мыши
    int x = 0, y = 0;

    //Если мышь сдвинулась
    if( event.type == SDL_MOUSEMOTION )
    {
        //Получить смещения
        x = event.motion.x;
        y = event.motion.y;

        //Если мышь над кнопкой
        if( ( x > box.x ) && ( x < box.x + box.w ) &&
                ( y > box.y ) && ( y < box.y + box.h ) )
        {
            //Установить соответсвующий спрайт
            clip = &clips[ CLIP_MOUSEOVER ];    
        }
        //Если нет
        else
        {
            //Ставим спрайт
            clip = &clips[ CLIP_MOUSEOUT ];
        }    
    }

В обработчике событий, перво-наперво мы проверяем двигалась ли мышь. Когда мышь движется, происходит событие SDL_MOUSEMOTION Если мышь двинулась, мы получаем ее смещения из структуры события, затем проверяем находится ли она над кнопкой. Если она над ней, мы устанавливаем спрайт Mouse over, или спрайт Mouse out в противном случае.

    //Если была нажата кнопка мыши
    if( event.type == SDL_MOUSEBUTTONDOWN )
    {
        //Если нажили левую
        if( event.button.button == SDL_BUTTON_LEFT )
        {
            //Получить смещения
            x = event.button.x;
            y = event.button.y;

            //Если мышь над кнопкой
            if( ( x > box.x ) && ( x < box.x + box.w ) &&
                    ( y > box.y ) && ( y < box.y + box.h ) )
            {
                //Установить спрайт
                clip =&clips[ CLIP_MOUSEDOWN ];
            }
        }
    }

Затем проверяем было ли нажатие кнопок мыши. При нажатии происходит событие SDL_MOUSEBUTTONDOWN. Мы хотим, чтобы кнопка реагировала только на левую кнопку мыши, так что мы проверяем была ли нажата именно левая кнопка мыши. Далее проверяем было ли нажатие над нашей кнопкой. Если это так, то устанавливаем для кнопки спрайт Mouse down

    //Если кнопку мыши отпустили
    if( event.type == SDL_MOUSEBUTTONUP )
    {
        //Если отпустили левую кнопку
        if( event.button.button == SDL_BUTTON_LEFT )
        {
            //получить смещения
            x = event.button.x;
            y = event.button.y;

            //Если мышь над кнопкой
            if( ( x > box.x ) && ( x < box.x + box.w ) &&
                    ( y > box.y ) && ( y < box.y + box.h ) )
            {
                //Установить спрайт
                clip = &clips[ CLIP_MOUSEUP ];
            }
        }
    }
}

Затем мы проверяем была ли кнопка мыши отпущена над кнопкой с событием SDL_MOUSEBUTTONUP В этой программе мы получаем смещения мыши из структуры события. Было бы более целесообразно получать смещения мыши при помощи SDL_GetMouseState(), но… я слишком ленив, чтобы возвращаться назад и переписывать код.

void Button::show()
{
    //Показать кнопку
    apply_surface( box.x, box.y, buttonSheet, screen, clip );
}

Далее в функции show мы отображаем спрайт кнопки на экране.

    //Нарезать спрайт лист
    set_clips();
    //Сделать кнопку
    Button myButton( 170, 120, 320, 240 );

В начале функции main() после инициализации и загрузки всего что нужно, мы нарезаем спрайт лист и настраиваем нашу кнопку.

    //Пока пользователь не захотел выйти
    while( quit == false )
    {
        //Если есть события для обработки
        if( SDL_PollEvent( &event ) )
        {
            //Обработать события мыши
            myButton.handle_events();

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

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

        //Показать кнопку
        myButton.show();

        //Обновить экран
        if( SDL_Flip( screen ) == -1 )
        {
            return 1;    
        }
    }

Здесь мы видим наш класс кнопки в действии внутри главного цикла. В начале мы обрабатываем события. Как вы можете видеть мы используем обработчик событий кнопки, а так же проверяем не хочет ли пользователь завершить приложение. Обычно для обработки событий используется цикл while, но в этом (и в предыдущих) мы используем if. Из-за этого обрабатывается одно событие за кадр — таким образом легче видеть отдельные события. В большинстве настоящих приложений используется while, потому что обычно вы хотите обработать все события в очереди в каждом кадре. После обработки события, мы очищаем экран, заливая его белым. Затем мы отображаем кнопку и обновляем экран. И далее главный цикл повторяется, таким образом мы можем отрисовать следующий кадр, пока пользователь не захочет выйти. Если у вас быстрый компьютер, вы скорее всего не увидите спрайт CLIP_MOUSEUP. Это происходит потому, что приложении работает так быстро, что он отображается только долю секунды. К счастью, далее будет группа туториалов, показывающих как рассчитывать время и регулировать скорость кадров. Если вы замедлите выполнение программы до 20 кадров в секунду у вы хотя бы сможете заметить его.

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

Нажатия клавиш

Этот урок рассказывает о том, как обнаруживать нажатия клавиш. Мы будем писать простую программу, показывающую какая из клавиш-стрелок была нажата. Мы уже делали простую обработку событий (SDL_QUIT). Сегодня мы разберемся с тем, как определять что была нажата клавиша, и что это была за клавиша.

    //Сгенерировать поверхности с сообщениями
    upMessage = TTF_RenderText_Solid( font, "Up was pressed.", textColor );
    downMessage = TTF_RenderText_Solid( font, "Down was pressed.", textColor );
    leftMessage = TTF_RenderText_Solid( font, "Left was pressed", textColor );
    rightMessage = TTF_RenderText_Solid( font, "Right was pressed", textColor );

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

    //Если есть событие для обработки
    if( SDL_PollEvent( &event ) ) {
        //Если была нажата клавиша
        if( event.type == SDL_KEYDOWN ) {

Теперь, когда мы хотим проверить была ли нажата клавиша, мы проверяем равен ли тип события SDL_KEYDOWN.

            //Выбрать правильное сообщение
            switch( event.key.keysym.sym ) {
                case SDLK_UP: message = upMessage; break;
                case SDLK_DOWN: message = downMessage; break;
                case SDLK_LEFT: message = leftMessage; break;
                case SDLK_RIGHT: message = rightMessage; break;
            }
        }
        //Если пользователь хочет выйти
        else if( event.type == SDL_QUIT ) {
            //Выходим из программы
            quit = true;
        }
    }

Теперь, если была нажата клавиша, мы должны проверить что это была за клавиша.SDL_PollEvent() кладет данные типа SDL_KEYDOWN в структуру события как SDL_KeyboardEvent по имени key:и внутри key находится структура keysym:а внутри keysym SDL_Key под названием sym, который хранит информацию о том какая кнопка была нажата. Если была нажата стрелка вверх, sym будет равен SDLK_UP и мы выберем сообщение "вверх", если sym будет равен SDLK_DOWN мы выберем сообщение "вниз" и т.д. Посмотреть все определения SDL_Key вы можете в документации по SDL. Так же мы проверяем не хочет ли пользователь выйти и соответствующим образом обрабатываем это событие. Замечание: Некоторые IDE, типа Code::Blocks, включают флаг -Wall по умолчанию. Из-за этого компилятор может жаловаться на то что у вас нет выражений case для всех возможных значений. Чтобы компилятор перестал жаловаться просто добавьте в конец блока switch:

default : ;

    //Если есть сообщение для отображения
    if( message != NULL ) {
        //Скопировать сообщение на экран
        apply_surface( 0, 0, background, screen );
        apply_surface(
            ( SCREEN_WIDTH - message->w ) / 2,
            ( SCREEN_HEIGHT - message->h ) / 2,
            message,
            screen
        );
        //Обнулить указатель на поверхность сообщения
        message = NULL;
    }
    //Обновить экран
    if( SDL_Flip( screen ) == -1 ) {
        return 1;
    }

Если поверхность сообщения никуда не указывает, ее значение будет NULL и ничего не будет скопировано. В обратном случае мы копируем фон и затем помещаем сообщение по центру экрана. Способ центрирования заключается в вычитании ширины/высоты копируемой поверхности из ширины/высоты поверхности на которую вы копируете. А так как поверхность должна быть отцентрирована, отступ с обеих сторон должен быть одинаковым, поэтому мы делим оставшееся расстояние пополам. После этого мы сбрасываем сообщение в NULL и обновляем экран.

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

Шрифты True Type

Пришло время научится рендерить текст. SDL не поддерживает *.ttf нативно, так что нам понадобится расширение SDL_ttf. Оно позволяет создавать поверхности из шрифтов True Type.Вы можете скачать SDL_ttf здесь. Для установки расширения, вы можете воспользоваться моей инструкцией. Установка 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 за дополнительной информацией по данной технике.

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