Загрузка и копирование оптимизированных поверхностей

Оригинальный заголовок: Optimized Surface Loading and Blitting.
В английском варианте используется термин BLIT — BLock Image Transfer. Дословно его можно перевести как "перенос блоков изображения". В сущности "блитинг" заключается в копировании прямоугольника с одной поверхности в такой же или меньший прямоугольник другой поверхности. Поэтому в переводе я буду использовать термин "копирование", как перевод для "блитинга"

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

//Заголовки
#include
#include

Тут мы подключаем заголовочые файлы для нашей программы.
SDL.h подключен потому, что мы, очевидно, будем использовать функции SDL.
Заголовок string используется потому, что ... эмм, я просто больше предпочитаю std::string чем char*.

//Аттрибуты экрана
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int SCREEN_BPP = 32;

Тут мы определяем различные аттрибуты для экрана.
Я вполне уверен что вы и сами понимаете зачем нужны SCREEN_WIDTH и SCREEN_HEIGHT. SCREEN_BP это количество битов на пиксель. Во всех частях руководства будет использоваться значение в 32 бита на пиксель.

//Поверхности, которые будут использоваться
SDL_Surface *message = NULL;
SDL_Surface *background = NULL;
SDL_Surface *screen = NULL;

Это три изображения, которые мы будем использовать. "background" очевидно соответствует фоновому изображению, "message" это растровое изображение говорящее "Hello", ну и "screen" — очевидно экран. Помните: хорошей практикой будет всегда устанавливать ваши указатели в NULL, если они пока что никуда не указывают.

SDL_Surface *load_image( std::string filename ) {
    //Временное хранилище для загружаемого изображения
    SDL_Surface* loadedImage = NULL;
    //Оптимизированное изображение, которое и будет использоваться
    SDL_Surface* optimizedImage = NULL;

Это наша функция, загружающая изображения.
Она загружает изображение и возвращает указатель на оптимизированную версию загруженной картинки. Аргумент "filename" это путь к изображению, которое нужно загрузить. "loadedImage" — поверхность в которую изначально загружается изображение. "optimizedImage" — поверхность, которая будет использоваться впоследствии.

    //Загрузить изображение
    loadedImage = SDL_LoadBMP( filename.c_str() );

Перво наперво загружаем изображение при помощи SDL_LoadBMP() Но не следует использовать его сразу, потому что оно 24-битное. Экран 32-битный и не самой лучшей идеей будет копировать одну поверхность на другую, отличающуюся форматом, потому что SDL будет вынуждена изменять формат на лету, что вызовет падение производительности.

    //Если при загрузке изображения не произошло ничего непредвиденного
    if( loadedImage != NULL ) {
        //Создать оптимизированное изображение
        optimizedImage = SDL_DisplayFormat( loadedImage );
        //Освободить ресурсы из-под старого изображения
        SDL_FreeSurface( loadedImage );
    }

Дальше мы проверяем успешно ли было загружено изображение. Если произошла ошибка, loadedImage будет равно NULL Если изображение загружено правильно, мы вызываем SDL_DisplayFormat(), которая создаст новую версию "loadedImage" в формате экрана. Причина по которой мы это делаем, заключается в том, что когда вы попытаетесь поместить одну поверхность на другую с отличающимся форматом, SDL преобразует поверхности так, чтобы они были в одном формате. Создание преобразованных поверхностей каждый раз, когда вы их копируете, в пустую тратит процессорное время, что выливается в пониженную скорость выполнения. Так как мы преобразуем поверхность в момент загрузки, то когда вы захотите перенести ее на экран, они уже будут в одинаковых форматах. И тогда SDL не придется преобразовывать их на лету. И так, теперь у нас есть две поверхности: старое загруженное изображение и новое оптимизированное. SDL_DisplayFormat() создала новое оптимизированное изображение, но не избавилась от старого. Поэтому мы вызываем SDL_FreeSurface() чтобы избавится от старого загруженного изображения.

    //Вернуть оптимизированное изображение
    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_Rect. Мы делаем это потому, что SDL_BlitSurface() принимает смещения только внутри SDL_Rect. SDL_Rect — тип данных, представляющий прямоугольник. Он содержит четыре поля, представляющих смещения по X, по Y, а также ширину и высоту прямоугольника. В данном случае нас интересуют только поля x и y.

    //Копировать поверхность
    SDL_BlitSurface( source, NULL, destination, &offset );
}

Теперь мы на самом деле копируем поверхность при помощи SDL_BlitSurface(). Первый аргумент это поверхность, которую мы используем. Не волнуйтесь по поводу второго аргумента, мы просто пока что присваиваем ему NULL. Третий аргумент это поверхность на которую мы собираемся копировать. Четвертый аргумент хранит смещения, по которым наша поверхность (source) будет скопирована на целевую (destination).

int main( int argc, char* args[] ) {

Теперь мы напишем нашу главную функцию — main При использовании SDL, вам всегда следует использовать:

int main( int argc, char* args[] )

или

int main( int argc, char** args )

Использование int main(), void main() или любого другого типа не сработает.

    //Инициализировать все подсистемы SDL
    if( SDL_Init( SDL_INIT_EVERYTHING ) == -1 ) {
        return 1;
    }

Тут мы запускаем SDL при помощи SDL_Init() Мы передаем в SDL_Init() флаг SDL_INIT_EVERYTHING, который запускает все подсистемы SDL. Подсистемы SDL это сущности типа видео, аудио, таймеров и т.д., которые являются отдельными компонентам движка, используемыми для создания игры. Мы не собираемся использовать все подсистемы, но не будет ничего страшного если мы тем не менее инициализируем их. Если SDL не удается запуститься, она возвращает -1. В этом случае мы обрабатываем ошибку, возвращая 1, тем самым завершая выполнение приложения.

    //Подготовить экран
    screen = SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,
        SDL_SWSURFACE );

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

    //Если при подготовке экрана произошла ошибка
    if( screen == NULL ) {
        return 1;
    }

Если при создании экрана что-то пошло не так, screen будет равно NULL

    //Установить заголовок окна
    SDL_WM_SetCaption( "Hello World", NULL );

Здесь мы устанавливаем текст заголовка окна: "Hello World" Заголовок это часть окна:

    //Загрузить изображения
    message = load_image( "hello.bmp" );
    background = load_image( "background.bmp" );

Теперь изображения загружены при помощи функции которую мы сделали.

    //Копировать фоновое изображение на экран
    apply_surface( 0, 0, background, screen );

Теперь пора скопировать фоновое изображение при помощи нашей функции. До того как мы скопировали фон, экран выглядит так: А теперь, когда мы скопировали фоновое изображение, экран в памяти выглядит так: Когда вы копируете поверхности, вы копируете пиксели с одной поверхности на другую. Поэтому теперь наш экран содержит фоновое изображение в верхнем левом углу, но мы хотим заполнить им все доступное место. Значит ли это что мы должны загрузить фоновое изображение еще три раза?

    apply_surface( 320, 0, background, screen );
    apply_surface( 0, 240, background, screen );
    apply_surface( 320, 240, background, screen );

Нет. Мы просто напросто можем скопировать ту же самую поверхность еще три раза.

    //Скопировать сообщение на экран
    apply_surface( 180, 140, message, screen );

Теперь мы собираемся скопировать поверхность с сообщением на экран со смещениями 180 по оси x и 140 по оси y. Суть в том, что координатная система в SDL не выглядит как то так: Система координат SDL выглядит так: Поэтому начало координат (0, 0) в левом верхнем углу, вместо левого нижнего. И поэтому когда вы копируете поверхность сообщения, она скопируется на 180 пикселей правее и на 140 пикселей ниже относительно начала координат в левом верхнем углу: Система координат SDL по началу кажется неудобной, но вы привыкнете к ней.

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

Хотя мы и скопировали наши поверхности, экран по прежнему пуст. Теперь мы должны обновить экран при помоще SDL_Flip(), чтобы поверхность экрана в памяти совпадала с тем, что изображено на мониторе. Если при этом произошла ошибка, мы вернем 1;

    //Подождать две секунды
    SDL_Delay( 2000 );

Мы вызываем SDL_Delay() для того, чтобы окно не исчезло через долю секунды, сразу после появления. SDL_Delay() принимает время в миллисекундах, тоесть 1/1000 секунды. И так, окно будет будет показываться в течение 2000/1000 секунды, проще говоря две секунды.

    //Освободить поверхности
    SDL_FreeSurface( message );
    SDL_FreeSurface( background );
    //Завершаем SDL
    SDL_Quit();
    //Возвращаем 0
    return 0;
}

И наконец мы выполняем очищающие процедуры.SDL_FreeSurface() используется для того чтобы избавится от загруженных изображений, так как мы больше ими не пользуемся. Если мы не будет освобождать память которую мы использовали, это приведет к утечкам памяти. Потом SDL_Quit() вызывается для завершения работы SDL. Затем мы возвращаем 0, завершая выполнение программы. Вы возможно спрашиваете себя, "почему мы не освобождаем поверхность экрана (screen). Не волнуйтесь. SDL_Quit() позаботится об этом для нас.

Если при запуске приложения, изображения не появляются или окно появляется и тут же исчезает, и вы видите в консоли (или в файле stderr.txt):
Ошибка сегментирования или Fatal signal: Segmentation Fault
То это потому, что программа пытается обратится по адресу памяти, по которому ей не следует этого делать. Скорее всего это происходит потому, что apply_surface() попыталась обратится по нулевому указателю (NULL). Это означает что вам нужно убедится что файлы с изображениями находятся в той же директории что и сама программа. Если окно появляется, но изображений не видно, опять таки, убедитесь что файлы изображений в той же директории что и сама программа или в директории проекта.

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

Вывод на экран первого изображения

Это часть руководства о том, как написать Hello World в стиле SDL. Теперь, когда вы установили SDL, самое время написать простейшее графическое приложение, которое загружает и отображает изображение на экране.

//Подключить описания функция и типов данных SDL
#include

В самом верху исходного файла мы подключаем заголовочный файл SDL, для того чтобы мы могли использовать функции и типы данных SDL. Обращаю ваше внимание на то, что некоторым из вас (например тем кто использует Visual Studio) необходимо подключать заголовок SDL. Например так:

#include "SDL.h"

Так что если компилятор жалуется на то, что он не может найти <SDL/SDL.h>, то либо вы подключаете файл по неправильному пути, либо вы забыли положить SDL.h в нужное место.

int main( int argc, char* args[] )
{
    //Изображения
    SDL_Surface* hello = NULL;
    SDL_Surface* screen = NULL;

В начале функции main(), мы объявляем два указателя на структуры SDL_Surface. SDL_Surface это изображение, и в этом приложении мы собираемся иметь дело с двумя изображениями. Поверхность(surface) "hello" это изображение, которое мы собираемся загружать и отображать. "screen" это то, что будет видно на экране. Когда вы имеете дело с указателями, вы всегда должны их инициализировать. Также, когда вы работаете с SDL, вы должны объявлять вашу функцию main() таким же образом как в коде выше. Вы не можете использовать void main()target="_blank" или что-то подобное.

    //Инициализировать SDL
    SDL_Init( SDL_INIT_EVERYTHING );
    //Настроить экран
    screen = SDL_SetVideoMode( 640, 480, 32, SDL_SWSURFACE );
    //Загрузить изображение
    hello = SDL_LoadBMP( "hello.bmp" );

Первая функция, которую мы вызываем в main это SDL_Init(). Вызов SDL_Init() инициализирует все подсистемы SDL, поэтому мы можем начать использовать графические функции SDL. Дальше мы вызываем SDL_SetVideoMode(), которая устанавливает ширину окна в 640 пикселей, высоту в 480 и глубину цвета в 32 бита на пиксель. Последний аргумент(SDL_SWSURFACE) располагает поверхность в программной памяти. Вызов SDL_SetVideoMode() возвращает указатель на поверхность окна, так что мы теперь можем ее использовать. После того как окно настроено, мы загружаем наше изображений при помощи SDL_LoadBMP(). SDL_LoadBMP() принимает в качестве аргумента путь к растровому изображению (bitmap) и возвращает указатель на загруженную SDL_Surface. В случае ошибки загрузки эта функци вернет NULL.

    //Вывести изображение на экран
    SDL_BlitSurface( hello, NULL, screen, NULL );
    //Обновить экран
    SDL_Flip( screen );
    //Пауза
    SDL_Delay( 2000 );

Теперь, когда мы настроили наше окно и загрузили изображение, мы хотим вывести загруженное изображение на экран. Мы делаем это при помощи функции SDL_BlitSurface(). Первый аргумент SDL_BlitSurface() это поверхность, которую мы хотим вывести (source, источник). Третий аргумент это поверхность, на которую мы хотим вывести наше изображение (destination, назначение). SDL_BlitSurface() помещает поверхность источника на поверхность назначения. В нашем случае она собирается вывести наше загруженное изображение на экран. О втором аргументе будет рассказано в последующих частях руководства. Теперь, когда наше изображение расположено на экране, нам нужно обновить экран, чтобы мы могли его увидеть. Мы делаем это используя SDL_Flip(). Если вы не вызовите SDL_Flip(), все что вы увидете это черный не обновленный экран. Теперь когда изображение выведено на экран и сделано видимым, нам нужно сделать так, чтобы окно оставалось видимым некоторое время, а не просто моргнуло на мгновение и исчезло. Мы добьемся этого вызовом SDL_Delay(). Здесь мы делаем задержку на 2000 миллисекунд(2 секунды). Более хороший способ заставить окно оставаться на месте мы узнаем в четвертой части руководства.

    //Освободить память из-под загруженного изображения
    SDL_FreeSurface( hello );
    //Освободить ресурсы занятые SDL
    SDL_Quit();
    return 0;
}

Теперь, когда нам больше не нужно загруженное изображение в нашей программе, нам надо удалить его из памяти. Вы не можете просто удалить указатель, вы должны использовать SDL_FreeSurface() для того чтобы удалить изображение из памяти. В конце мы вызываем SDL_Quit() для того чтобы завершить работу SDL. Вы возможно хотите узнать, почему мы не удаляем поверхность экрана(screen). Не волнуйтесь. SDL_Quit() удалит ее за нас. Поздравляю! Вы только написали ваше первое графическое приложение.

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

Установка SDL

Оригинал: Lazy Foo SDL Tutorial Представляю мой пробный перевод первой части руководства по SDL от Lazy Foo. Так как перевод пробный, я для начала переведу только ту часть руководства, которую я непосредственно проходил и соответственно тестировал. Это относится к тем частям, которые зависят от операционной системы и используемого ПО. В частности в этой статье я переведу установку SDL только под Linux, а сборку приложения только из командной строки. Так как SDL это сторонняя библиотека, вам придется устанавливать ее самостоятельно. Здесь я приведу пошаговое руководство по её установке. Если у вас возникли какие то проблемы, попробуйте почитать FAQ по разработке на SDL. Как только вы установите SDL, вы можете переходить ко второй половине руководства и научитесь загружать и отображать изображения на экране.

Так как существует огромное количество дистрибутивов, это руководство, возможно, не сработает на вашем дистрибутиве. Попробуйте почитать SDL Linux FAQ. Если вы все перепробовали и все еще ничего не работает, свяжитесь со мной и я попробую добавить инструкции по конкретному дистрибутиву.

Для дистрибутивов использующих APT (Debian, Ubuntu, Mint, etc) введите в терминале эти команды:

$ sudo aptitude install libsdl1.2-dev libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev

Или если aptitude не установлен:

$ sudo apt-get install libsdl1.2-dev libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev

Для дистрибутивов использующих yum(Yellow dog Updater) используйте следующее:

$ yum install SDL-devel SDL_mixer-devel SDL_image-devel SDL_ttf-devel

Так же как для aptitude или apt-get для установки вам нужны административные привилегии. (Запуск через sudo или от имени root'а) Для дистрибутивов основанных на RPM вы найдете эти пакеты на сайте SDL, в особенности на этой странице. Теперь, когда вы установили SDL, самое время открыть вашу любимую IDE или текстовый редактор.

Создайте текстовый файл(тоесть исходник) со следущим содержимым:

#include
int main( int argc, char* args[] )
{
   //Инициализировать SDL
   SDL_Init( SDL_INIT_EVERYTHING );
   //Освободить ресурсы занятые SDL
   SDL_Quit();
   return 0;
}

Теперь введите в командной строке:

g++ -o myprogram mysource.cpp -lSDL

и мы закончили.

Xneur: задержка восстановления раскладки

Пользуясь тайловым оконным менеджером(awesome) и не используя окружение рабочего стола, я предпочитаю чтобы каждое приложение помнило свою раскладку клавиатуры. То есть если я в консоли напечатал команду, потом переключился на IM-клиент, включил русскую раскладку, напечатал сообщение и переключился обратно в консоль, то раскладка в консоли должна быть латинской, а в IM-клиенте — русской. Попробовав несколько вариантов реализации этой возможности, мой выбор пал на xneur.

  • Во-первых он умеет как раз то, что мне нужно, т.е. сохранять для каждого приложения свою раскладку.
  • Во-вторых он имеет удобную мне функцию ручной изменения раскладки выделенного текста.
  • Ну и в-третьих он исправляет ДВе больших буквы, которые постоянно возникают в переписке.

Есть только одна неприятная проблема, особенно заметная при использовании тайлового WM: полусекундная задержка восстановления раскладки при получении окном фокуса. Из-за чего я много раз пытался открыть сайт www.google.ru или написать что-нибудь в стиле "Gривет!". Чтобы поделится рецептом как исправить эту проблему и написан этот пост. Рецепт для Ubuntu/Mint. В остальных дистрибутивах все тоже самое, за исключением инструментов для установки приложений и названия пакетов. Установим xneur, если он еще не стоит:

$ sudo aptitude install xneur

Будем патчить исходники и пересобирать xneur, так что для начала поставим пакет dpkg-dev, который так же должен автоматически поставить пакет build-essential, нужный нам для сборки xneur.

$ sudo aptitude install dpkg-dev

Дальше скачаем исходники:

$ mkdir xneur && cd xneur
$ apt-get source xneur
$ cd xneur-0.15.0/

Теперь скачаем и распакуем патч:

$ mkdir patch && cd path
$ wget tatrix.org/misc/patch.tar.bz
$ tar -xjvf patch.tar.bz

А теперь применим его и соберем пакет:

$ for i in *; do find ../ -name ${i:0:-5} -exec patch {} -i $i \; ; done
$ cd ..
$ sudo aptitude install libpcre3-dev libenchant-dev
libxosd-dev libgstreamer0.10-dev libnotify-dev
$ ./configure

Впрочем, если вам не нужен звук или другие возможности, вы можете собрать xneur без них:

$ sudo aptitude install libpcre3-dev libenchant-dev libxosd-dev libnotify-dev
$ ./configure --with-sound=no

В моем случае сборка падала из-за использования устаревшей функции (XKeycodeToKeysym). Отключим это предупреждение и запустим компиляцию(есть ли более правильный путь?):

$ CFLAGS=-Wno-error make -e

И последний штрих. Сделаем ссылку на свежесобранный xneur:

$ x=`which xneur`; sudo mv -v $x ${x}_orig
$ sudo ln -vs `pwd`/src/xneur $x

Вот и все. Теперь задержка равна нулю. При желании ее можно изменить в конфигурационном файле (обычно это ~/.xneur/xneurrc):

#Задержка в миллисекундах
FocusDelay 100000

Я писал Андрею, автору xneur по этому поводу, и возможно в новой версии появится это настройка. Спасибо за внимание.

Bomberman!

Простая реализиция на canvas'е моего видения бомбермена, в которого я правда особо не играл. Управление: стрелочки — движение, пробел или ctrl — поставить бомбу. Играть