Caint: рисовалка на канвасе

Простенкая рисовалка (Canvas pAINT) на канвасе:<img src="https:&#x2F;&#x2F;tatrix.org&#x2F;processed_images&#x2F;ac84259c8322921400.jpg" />(в новом окне)Чуть позже я собираюсь написать статью о том, как я её писал.

var iframe = document.getElementById("caint"); iframe.onload = function() { var page = document.getElementById("page"); var close = document.createElement("a"); close.innerHTML = "x"; close.href = "javascript://"; close.style.position = "absolute"; close.style.right = "0.5em"; close.style.top = "0.5em"; close.style.color = "white"; var hide = function() { page.style.display = "block"; iframe.style.display = "none"; } close.onclick = hide; window.onpopstate = hide; iframe.contentDocument.body.appendChild(close); } function caintShow() { iframe.src="/public/misc/caint/"; var wp = document.getElementById("wpadminbar"); if(wp) wp.style.display = "none"; document.location.hash = "caint"; page.style.display = "none"; iframe.style.display = "block"; iframe.style.position = "fixed"; iframe.style.top = 0; iframe.style.left = 0; iframe.style.bottom = 0; iframe.style.right = 0; document.body.appendChild(iframe); return false; } document.getElementById("caintShow").onclick = caintShow;

Продвинутый таймер

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

//Таймер
class Timer
{
    private:
    //Сколько было времени при запуске таймера
    int startTicks;

    //Сколько было времени, когда таймер поставили на паузу
    int pausedTicks;

    //Статус таймера
    bool paused;
    bool started;

    public:
    //Инициализируем переменные
    Timer();

    //Действия с часами
    void start();
    void stop();
    void pause();
    void unpause();

    //Получить время таймера
    int get_ticks();

    //Проверки статуса таймера
    bool is_started();
    bool is_paused();
};

Здесь мы можем наблюдать краткое описание нашего класса. startTicks — точка отсчета времени, а pausedTicks — время, когда таймер поставили на паузу. Конструктор инициализирует переменные, и я вполне уверен, что вы прекрасно представляете что делают start(), stop(), pause() и unpause(). Функция get_ticks() возвращает время таймера в миллисекундах. is_started() проверяет запущен ли таймер, а is_paused() поставлен ли он на паузу.

Timer::Timer()
{
    //Инициализировать переменные
    startTicks = 0;
    pausedTicks = 0;
    paused = false;
    started = false;
}

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

void Timer::start()
{
    //Запустить таймер
    started = true;

    //Снять с паузы
    paused = false;

    //Запомнить текущее время
    startTicks = SDL_GetTicks();
}

Теперь когда мы запускаем таймер, мы выставляем статус started в true, снимаем его с паузы и делаем время запуска равным текущему времени при помощи SDL_GetTicks().

void Timer::stop()
{
    //Остановить таймер
    started = false;

    //Снять таймер с паузы
    paused = false;
}

Когда мы останавливаем его, мы выставляем статус started в false и, опять-таки, снимаем с паузы.

int Timer::get_ticks()
{
    //Если таймер запущен
    if( started == true )
    {
        //Если таймер на паузе
        if( paused == true )
        {
            //Возвращаем время работы таймера до паузы
            return pausedTicks;
        }
        else
        {
            //Возвращаем текущее время минус время запуска
            return SDL_GetTicks() - startTicks;
        }
    }

    //Таймер не запущен
    return 0;
}

Итак, функция возвращающая значение таймера. Для начала проверяем, запущен ли таймер. Если да, проверяем не стоит ли он на паузе. Если он на паузе, возвращаем время работы до паузы. Мы поговорим о паузах позже. Если таймер не на паузе, вы возвращаем разницу во времени между запуском и текущим моментом времени. Как вы можете заметить, формула такая же, как и в предыдущем уроке. Если таймер не запускался возвращаем ноль.

void Timer::pause()
{
    //Если таймер запущен и еще на паузе
    if( ( started == true ) && ( paused == false ) )
    {
        //Поставить на паузу
        paused = true;

        //Вычислить время работы таймера до паузы
        pausedTicks = SDL_GetTicks() - startTicks;
    }
}

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

void Timer::unpause()
{
    //Если таймер на паузе
    if( paused == true )
    {
        //Снять с паузы
        paused = false;

        //Скинуть время запуска
        startTicks = SDL_GetTicks() - pausedTicks;

        //Обнулить время работы до паузы
        pausedTicks = 0;
    }
}

Когда мы хотим снять таймер с паузы, мы проверяем был ли таймер на паузе до этого. Если был, меняем статус paused на false, и затем изменяем время запуска на текущее минус время работы до паузы. В конце концов обнуляем pausedTicks, впрочем без особой на то причины, по сути лишь потому что мне не нравятся бессвязные переменные.

bool Timer::is_started()
{
    return started;
}

bool Timer::is_paused()
{
    return paused;
}

Функции проверяющие статус таймера. Я вполне уверен что они очевидны.

    //Создаем таймер
    Timer myTimer;

    //Генерируем сообщения для поверхностей
    startStop = TTF_RenderText_Solid(
                    font,
                    "Press S to start or stop the timer",
                    textColor);
    pauseMessage = TTF_RenderText_Solid(
                       font,
                       "Press P to pause or unpause the timer",
                       textColor);

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

    //Запустить таймер
    myTimer.start();

    //Пока пользователь не захотел выйти
    while( quit == false )
    {

Преждем чем начать главный цикл, запускаем таймер.

        //Пока есть события для обработки
        while( SDL_PollEvent( &event ) )
        {
            //Если нажали клавишу
            if( event.type == SDL_KEYDOWN )
            {
                //Если нажали s
                if( event.key.keysym.sym == SDLK_s )
                {
                    //Если таймер запущен
                    if( myTimer.is_started() == true )
                    {
                        //Остановить таймер
                        myTimer.stop();
                    }
                    else
                    {
                        //Запустить таймер
                        myTimer.start();
                    }
                }

В этом месте мы обрабатываем нажатия клавиш. При нажатии "s", если таймер запущен останавливаем его, а в противном случае запускаем.

                //Если нажали p
                if( event.key.keysym.sym == SDLK_p )
                {
                    //Если таймер на паузе
                    if( myTimer.is_paused() == true )
                    {
                        //Снять с паузы
                        myTimer.unpause();
                    }
                    else
                    {
                        //Поставить на паузу
                        myTimer.pause();
                    }
                }
            }

При нажатии на "p", если таймер на паузе снимаем его с нее, а иначе ставим.

        //Строка для времени таймера
        std::stringstream time;

        //Преобразовываем время таймера в строку
        time << "Timer: " << myTimer.get_ticks() / 1000.f;

        //Рендерим поверхность
        seconds = TTF_RenderText_Solid( font, time.str().c_str(), textColor );

        //Отображаем ее
        apply_surface( ( SCREEN_WIDTH - seconds->w ) / 2, 0, seconds, screen );

        //Освобождаем память
        SDL_FreeSurface( seconds );

        //Обновляем экран
        if( SDL_Flip( screen ) == -1 )
        {
            return 1;
        }
    }

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

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

Время

Мы уже разобрались с событиями. Теперь пришло время разобраться с временем. Знание того, как обращаться с временем — ключевой момент при создании игр. В этом уроке мы создадим простой таймер, который можно запускать и останавливать.Предположим что вам нужно отмерить 30-секундный интервал, но у вас нет таймера. Если бы на стене висели часы с секундной стрелкой, вы бы дождались пока она достигнет положения кратного 15 секундам:и ждали бы до тех пор, пока она не пройдет 30 секунд от точки отсчета.Наш таймер работает по этому же принципу. SDL запускает внутренний таймер в нашей программе, и мы можем получить время в миллисекундах используя SDL_GetTIcks(). Если вам нужно отмерить 1000 миллисекунд, вы сохраняете время когда вы начали ожидание и ждете до тех пор, пока разница между временем начала и текущем не будет равна 1000.

//Заголовки
#include "SDL/SDL.h"
#include "SDL/SDL_image.h"
#include "SDL/SDL_ttf.h"
#include
#include

Наряду со стандартными заголовками, мы подключаем строковые потоки. Я объясню что это такое чуть позже.

int main( int argc, char* args[] )
{
    //Флаг выхода
    bool quit = false;

    //Время начала отсчета таймера
    Uint32 start = 0;

    //Флаг старта/остановки таймера
    bool running = true;

В начале функции main мы объявляем две переменные, которые мы будем использовать для реализации нашего таймера. start будет хранить время запуска таймера, а running — флаг, отслеживающий запущен ли таймер.

    //Инициализируем таймер
    start = SDL_GetTicks();

    //Пока пользователь не решил выйти
    while( quit == false )
    {

После того, как мы инициализировали и загрузили поверхности, пришло время запустить таймер. Мы начинаем с того, что сохраняем текущее время, используя SDL_GetTicks(). После этого мы входим в главный цикл.

        //Пока есть события для обработки
        while( SDL_PollEvent( &event ) )
        {
            //Если нажали клавишу
            if( event.type == SDL_KEYDOWN )
            {
                //Если нажали клавишу 's'
                if( event.key.keysym.sym == SDLK_s )
                {
                    //Если таймер запущен
                    if( running == true )
                    {
                        //Остановить таймер
                        running = false;
                        start = 0;  
                    }
                    else
                    {
                        //Запустить таймер
                        running = true;
                        start = SDL_GetTicks();  
                    }
                }
            }

Здесь мы обрабатываем нажатие клавиши 's', которая запускает и останавливает таймер. Если таймер был запущен, мы выставляем флаг running в false, и обнуляем start, просто потому что я не люблю бродячие значения. Если таймер был остановлен, мы, напротив, присваиваем флагу running значение true и запускаем таймер, так же, как мы сделали это до входа в главный цикл.

        //Если таймер запущен
        if( running == true )
        {
            //Время таймера в виде строки
            std::stringstream time;

            //Преобразовать время в строку
            time << "Timer: " << SDL_GetTicks() - start;

После того, как мы выводим фон и сообщение, мы проверяем запущен ли таймер. Если да, то показываем время. Формула для расчета времени таймера такова:время = текущее время - время запускаТо есть, если вы запустили таймер, когда значение SDL_GetTicks() было равно 10 000 и теперь оно 20 000, формула даст нам цифру 10 000, означающую что прошло 10 секунд с момента запуска таймера. Теперь кладем 'Timer: ' + время таймера в строковой поток. Мы создаем строковой поток по имени time, который хранит сообщение содержащее время таймера. Строковые потоки позволяют создавать строки из нескольких переменных. Как вы могли заметить, они очень похожи на cout, но вместо того, чтобы выводить все в консоль, они просто сохраняют текст в строковом потоке, чтобы мы могли воспользоваться им впоследствии. Тем из вас, кто все еще пользуются VC++ 6.0, нужно будет привести (SDL_GetTicks() - start) к int.

            //Отрисовать время
            seconds = TTF_RenderText_Solid( font, time.str().c_str(), textColor );

            //Вывести время
            apply_surface( ( SCREEN_WIDTH - seconds->w ) / 2, 50, seconds, screen );

            //освободить поверхность
            SDL_FreeSurface( seconds );
        }

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

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

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

Emacs: раскладка на meta-space

Извечная проблема пользователей емакса (и вима) с переключением раскладки и мне изрядно попортила жизнь. Долгое время выходом из положения, если это так можно назвать, для меня было банальное переключение системной раскладки для ввода небольшого количества русского текста и последующее переключение раскладки обратно. А для случаев когда нужно вводить много кириллических символов вполне подходит встроенный в емакс механизм input-method. Но иметь одну комбинацию для переключения системной раскладки, а другую для переключения раскладки в редакторе (ctrl-\) ну ни как не способсвует психическому спокойствию. Перепробывав миллион методов, я пришел к следующему решению. Для начала, чтобы кисти рук не выгибались я перенес ctrl на место caps'a. С переключением раскладки все интересней. На данный момент все устроено так: переключение на правом alt'e и на meta-space. Плюс за неимением лучших вариантов и для того чтобы не было соблазна пользоваться старыми хоткеями, левый ctrl включает латинскую раскладку, а правый кириллическую. (для ubuntu: правим файлик /etc/default/keyboad

XKBOPTIONS="grp_led:scroll,compose:rwin,grp:toggle,grp:alt_space_toggle,grp:lctrl_rctrl_switch,caps:none,ctrl:nocaps"

Основной хоткей для переключения раскладки — meta-space. Теперь для того чтобы пользоваться им же в емаксе добавляем в ~/.emacs следующий код:

;; for this to work kbdd must be installed
(global-set-key (kbd "M-SPC")
		(lambda ()
		  "Toggle input method"
		  (interactive)
		  (toggle-input-method)
		  (start-process "dbus-send"
				 nil
				 "dbus-send"
				 "--dest=ru.gentoo.KbddService"
				 "/ru/gentoo/KbddService"
				 "ru.gentoo.kbdd.set_layout"
				 "uint32:1")))

Решение требует того, чтобы был услановлен kbdd.

sudo aptitude install kbdd

Который кстати умеет сохранять раскладку для каждого приложения, что тоже весьма удобно, особенно для легких окружений типа Awesome. В результате: системная раскладка переключается по meta-space, зажигая при этом лампочку scroll lock'a при включении кириллической раскладки. В емаксе input-method включается точно так же. При этом, правда, системная раскладка остается английской, и поэтому scroll lock не загорается. Что вполне приемлемо на фоне остальных решений этого вопроса.

Воспроизведение звука

Воспроизведение звука — еще одна ключевая составляющая программирования игр. Штатные функции SDL для проигрывания звука довольно запутанные. Поэтому мы будет разбираться в том, как проигрывать звуки при помощи библиотеки SDL_mixer. SDL_mixer — библиотека расширений, которая делает использование звука безумно простым делом. Скачать SDL_mixer можно тут. Для того, чтобы установить ее просто следуйте инструкции. Ставится SDL_mixer так же просто, как и SDL_image: просто замените в тексте инструкции SDL_image на SDL_mixer Этот туториал раскрывает основы проигрыванию музыки и звуков при помощи SDL_mixer на примере "музыки", которую я создал барабаня по моему монитору.

//Музыка, которую будем проигрывать
Mix_Music *music = NULL;
//Звуки, которые будут использоваться
Mix_Chunk *scratch = NULL;
Mix_Chunk *high = NULL;
Mix_Chunk *med = NULL;
Mix_Chunk *low = NULL;

Здесь мы можем видеть новые типы данных, с которыми мы будем работать.Mix_Music тип данные который мы используем для музыки, и Mix_Chunk для звука соответственно.

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_mixer
    if( Mix_OpenAudio( 22050, MIX_DEFAULT_FORMAT, 2, 4096 ) == -1 ) {
        return false;
    }
    //Установить заголовок окна
    SDL_WM_SetCaption( "Monitor Music", NULL );
    //Если все прошло хорошо
    return true;
}

В функции инициализации мы вызываем Mix_OpenAudio() для инициализации аудио функций SDL_mixer Первый аргумент Mix_OpenAudio() это используемая частота звука, в данном случае равная 22050, что является рекомендуемым значением. Второй аргумент это формат звука, который мы выставим в значение по умолчанию. Третий аргумент — сколько использовать каналов. Мы задаем его равным двум, чтобы получить стерео звук. Если бы мы передали единицу, получился бы моно звук. Последний аргумент — размер сэмпла, в данном случае 4096. Если вы используете OGG, MOD или другие форматы, отличные от WAV, взгляните на Mix_Init() для инициализации декодеров и Mix_Quit() для их закрытия. Мы будем использовать только WAV файлы, так что не будем писать код для чего-то, чем мы не будем пользоваться.

{
    //Загрузить фоновое изображение
    background = load_image( "background.png" );

    //Открыть файл шрифта
    font = TTF_OpenFont( "lazy.ttf", 30 );

    //Если не загрузился фон
    if( background == NULL )
    {
        return false;    
    }

    //Если не загрузился шрифт
    if( font == NULL )
    {
        return false;
    }

    //Загружаем музыку
    music = Mix_LoadMUS( "beat.wav" );

    //Если не получилось
    if( music == NULL )
    {
        return false;    
    }

    //Загружаем звуковые эффекты
    scratch = Mix_LoadWAV( "scratch.wav" );
    high = Mix_LoadWAV( "high.wav" );
    med = Mix_LoadWAV( "medium.wav" );
    low = Mix_LoadWAV( "low.wav" );

    //Если что-то пошло не так
    if( ( scratch == NULL ) || ( high == NULL ) || ( med == NULL ) || ( low == NULL ) )
    {
        return false;    
    }

    //Все прошло удачно
    return true;    
}

Тут мы видим нашу функцию загрузки. Для загрузки музыки мы используем Mix_LoadMUS(). Mix_LoadMUS() принимает имя файла с музыкой и возвращает соответствующие данные или NULL в случае ошибки. Для загрузки звуковых эффектов мы применяем Mix_LoadWAV(). Она работает аналогично, возвращая Mix_Chunk в случае успеха и NULL в противном случае.

void clean_up()
{
    //Освобождаем поверхность фона
    SDL_FreeSurface( background );

    //Освобождаем звуковые эффекты
    Mix_FreeChunk( scratch );
    Mix_FreeChunk( high );
    Mix_FreeChunk( med );
    Mix_FreeChunk( low );

    //Освобождаем музыку
    Mix_FreeMusic( music );

    //Закрывает шрифт
    TTF_CloseFont( font );

    //Закрываем SDL_mixer
    Mix_CloseAudio();

    //Закрываем SDL_ttf
    TTF_Quit();

    //Закрываем SDL
    SDL_Quit();
}

В функции очистки мы вызываем Mix_FreeChunk(), чтобы избавится от загруженных звуковых эффектов и Mix_FreeMusic(), чтобы освободить ресурсы занятые музыкой. После того как мы закончили использовать SDL_mixer вызывается Mix_CloseAudio.

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

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

                //Если нажали 1
                if( event.key.keysym.sym == SDLK_1 )
                {
                    //Проиграть эффект скрипа
                    if( Mix_PlayChannel( -1, scratch, 0 ) == -1 )
                    {
                        return 1;    
                    }
                }
                //Если нажали 2
                else if( event.key.keysym.sym == SDLK_2 )
                {
                    //Проиграть звук удара высокой частоты
                    if( Mix_PlayChannel( -1, high, 0 ) == -1 )
                    {
                        return 1;    
                    }
                }
                //Если нажали 3
                else if( event.key.keysym.sym == SDLK_3 )
                {
                    //Проиграть звук удара средней частоты
                    if( Mix_PlayChannel( -1, med, 0 ) == -1 )
                    {
                        return 1;    
                    }
                }
                //Если нажали 4
                else if( event.key.keysym.sym == SDLK_4 )
                {
                    //Проиграть звук удара низкой частоты
                    if( Mix_PlayChannel( -1, low, 0 ) == -1 )
                    {
                        return 1;    
                    }
                }

При проверки нажатий на клавиши, сначала мы проверяем были ли нажаты 1, 2, 3 или 4. Эти клавиши проигрывают звуковые эффекты. Если одна из этих клавиш была нажата, мы вызываем Mix_PlayChannel(), чтобы проиграть звук, ассоциированный с данной клавишей. Первый аргумент Mix_PlayChannel() это канал, в который нужно проиграть звук. Так как мы передаем -1 Mix_PlayChannel() просто берет первый доступный канал и проигрывает звук. Второй аргумент это Mix_Chunk, который нужно проиграть. Третий — сколько раз нужно повторить проигрывание. В случае если он равен 0, звук будет проигран единожды. В случае проблем с проигрыванием звука, Mix_PlayChannel() возвращает -1.

                //Если нажали 9
                else if( event.key.keysym.sym == SDLK_9 )
                {
                    //Если музыка не играет
                    if( Mix_PlayingMusic() == 0 )
                    {
                        //Запускаем проигрываение музыки
                        if( Mix_PlayMusic( music, -1 ) == -1 )
                        {
                            return 1;
                        }    
                    }

Дальше мы обрабатываем нажатие клавиши 9, которая используется для проигрывания или приостановки музыки. Сначала мы проверяем, не играет ли уже музыка при помощи Mix_PlayingMusic(). Если нет, мы вызываем Mix_PlayMusic для запуска проигрывания. Первый аргумент Mix_PlayMusic это музыка, которые мы собираемся проигрывать. Второй — сколько раз ее повторять. В нашем случае, -1 означает играть до тех пор, пока проигрывание не будет остановлено вручную. В случае проблем с проигрыванием музыки, Mix_PlayMusic() возвращает -1.

                    //Если музыка играет
                    else
                    {
                        //Если музыка на паузе
                        if( Mix_PausedMusic() == 1 )
                        {
                            //Продолжить играть
                            Mix_ResumeMusic();
                        }
                        //Если музыка играет
                        else
                        {
                            //Приостановить проигрывание
                            Mix_PauseMusic();
                        }
                    }
                }

Теперь, если музыка играла и пользователь нажимает 9, мы либо приостанавливаем либо возобновляем проигрывание. Сначала мы проверяем на паузе ли музыка при помощи Mix_PausedMusic(). Если это так, то мы возобновляем проигрываение вызывая Mix_ResumeMusic(). В противном случае мы приостанавливаем ее при помощи Mix_PauseMusic()

                //Если нажали 0
                else if( event.key.keysym.sym == SDLK_0 )
                {
                    //Остановить музыку
                    Mix_HaltMusic();
                }
            }

В конце концов, мы проверяем не нажал ли пользователь 0. И в этом случае останавливаем проигрывание музыки вызовом Mix_HaltMusic()

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