Анимированная кнопка на SDL

Речь пойдет о создании такого распространенного элемента управления, как кнопка. Расписывать преимущества этого компонента в графическом интерфейсе игры или какого-либо другого приложения не имеет смысла. Среди решаемых в статье задач: обработка событий нажатия кнопки и наведения курсора мыши; анимация; использование CALLBACK функций.
Описание
Кнопка — это прямоугольный элемент управления, реагирующий на щелчки по нему мышью.
Если данный элемент разрабатывается для игры, то его художественное оформление имеет значение. Если кнопка будет представлять из себя статичное изображение, то пользователя это мало порадует. Следовательно нужно организовать визуальную реакцию на наведение и щелчок мыши.
Кроме оформления важно влияние кнопки на события в игре. Поэтому нужно учесть возможность вызова внешних пользовательских функций (callback).
Вспомогательные инструменты
Что мы будем использовать для более продуктивной работы над проектом? Так как кнопка имеет много настраиваемых параметров, то сосредоточим данные о них в конфигурационном файле. Для чтения таких файлов используем ConfReader.h.
Для работы с набором изображений подключим модуль SpriteBank, описанный в статье об игре Arkanoid.
Графические заготовки
Много интересных вариантов GUI можно обнаружить на сайте opengameart.org. Для нашего проекта возьмем такой вариант и доработаем его в GIMP. В результате получим следующее изображение:

Верхний вариант существует для обычного состояния кнопки, средний — для случая наведения курсора, нижний — для нажатия. Конечно, сам момент щелчка занимает очень мало времени. Но внимательный человек заметит, что реакция на щелчок происходит в момент отпускания кнопки мыши. Таким образом, пока кнопка нажата и удерживается, мы будем отображать нижний вариант.
В качестве фона задействуем картинку звездного неба, что была использована в Arkanoid.
Конфигурация
config.cfg
# Config file for SDLButton sample # https://tripledistillation.ru # [screen] whb = 320, 240, 32 backid = 0 [spritebank] file = ./bank.set [button] xywh = 25, 95, 270, 50 setid = 1
Все настройки будем хранить в файле config.cfg. Кроме параметров конструируемого объекта я включил настройки некоторых других частей приложения. В секции screen расположены размер окна и количество бит на пиксель, а также указан идентификатор для фона. Секция spritebank содержит имя файла, описывающего множество спрайтов. И, наконец, секция button задает параметры кнопки.
Поскольку масштабирование исходных изображений не рассматривается, у читателя могут возникнуть вопросы по поводу разных размеров самого объекта и картинки для него. Неужели размеры кнопки нельзя определить по размерам картинки?! Дело в том, что приведенная выше заготовка содержит тень. Следовательно само изображение больше, чем активная часть кнопки. А в случае наведения курсора на область тени кнопка реагировать не должна.
bank.set
В нашем банке спрайтов будут две картинки. Одна из них — фон, другая — изображения кнопки. Всего четыре спрайта и два подмножества. Одно подмножество для фона, другое для кнопки.
0 2 0 ./bg_1_1.png 1 ./button1.png 4 1 0 0 0 640 480 2 1 15 19 277 57 3 1 15 119 277 57 4 1 15 219 277 57 2 0 1 1 0 1 3 2 1 3 1 4 1
О формате файла bank.set рассказано в этой статье.
Код
Класс Button
Исходя из поставленных задач, описание класса Button будет следующим:
/* Header file for Button class */ #include "SpriteBank.h" #ifndef BUTTON_H #define BUTTON_H class Button { public: Button(int x, int y, int w, int h, SpriteBank* sb, int setId); Button(); ~Button(); int x; int y; int w; int h; SpriteSet* ss; SpriteBank* sb; void (*onClick)(int, int, void*); void *userData; void draw(); void handle_events(SDL_Event& event); private: enum { D_NORM = 0, D_HOVER, D_CLICK }; int d_state; }; #endif
Итак, среди параметров — координаты и габариты кнопки, указатели на банк спрайтов и необходимое их подмножество, указатель на пользовательскую функцию-обработчик щелчка, указатель на пользовательские данные. Среди публичных методов — draw, отвечающий за отображение объекта на экране, и обработчик событий sdl. В приватных данных — перечисление состояний кнопки и хранящая текущее состояние переменная d_state. Последняя введена для выбора подходящего спрайта процедурой draw.
Рассмотрим реализацию описанных методов.
void Button::draw() { switch(d_state) { case D_NORM: SpriteBankDrawSetId(sb, ss, 0, x, y); break; case D_HOVER: SpriteBankDrawSetId(sb, ss, 1, x, y); break; case D_CLICK: SpriteBankDrawSetId(sb, ss, 2, x, y); break; default: break; }; };
Будем считать, что подмножество спрайтов, отведенное нашему объекту содержит рисунки всех состояний кнопки в следующем порядке: нормальное, с наведенным курсором, при щелчке.
Рассмотрим теперь процедуру обработки событий sdl.
void Button::handle_events(SDL_Event& event) { int mx, my; mx = event.button.x; my = event.button.y; if( (mx > x) && (mx < x + w) && (my > y) && (my < y + h) ) { if(event.type == SDL_MOUSEBUTTONDOWN) { d_state = D_CLICK; } else if(event.type == SDL_MOUSEMOTION) { if(d_state != D_CLICK) d_state = D_HOVER; } else if(event.type == SDL_MOUSEBUTTONUP) { if(onClick) onClick(mx, my, userData); d_state = D_NORM; }; } else d_state = D_NORM; };
Порядок действий таков. Сперва получаем положение курсора мыши, затем определяем — попал ли он в прямоугольник, соответствующий нашей кнопке. Если это так, то определяем — какое из событий произошло: нажатие кнопки мыши, движение курсора или отпускание кнопки. Для каждого случая задаем соответствующее состояние в d_state. Кроме того, при отпускании кнопки вызываем пользовательскую функцию, указатель на которую хранится в переменной onClick. В качестве аргументов передаем координаты щелчка и указатель на пользовательские данные.
Конечно, в определении формата пользовательской функции можно было указать только один аргумент, не передавая в нее координаты курсора. Всё зависит от спектра решаемых задач.
Конструктор и деструктор класса Button не имеют никаких особенностей, кроме вызова функции SpriteBankSetMakeFast. Последняя нужна для создания копии подмножества спрайтов для «быстрого» доступа к ним. Соответственно в деструкторе мы освобождаем память, выделенную для копии подмножества.
Button::Button(int x, int y, int w, int h, SpriteBank* sb, int setId) : x(x), y(y), w(w), h(h) { this->sb = sb; this->ss = SpriteBankSetMakeFast(sb, setId); this->onClick = NULL; this->d_state = D_NORM; }; Button::Button() { this->x = 0; this->y = 0; this->w = 10; this->h = 10; this->sb = NULL; this->ss = NULL; this->onClick = NULL; this->d_state = D_NORM; }; Button::~Button() { SpriteSetFree(this->ss); };
Если конструктор не имеет аргументов, то дальнейшая инициализация должна включать указание банка спрайтов и нужного их подмножества.
Код приложения
Часть кода взята с ресурса Lazy Foo’ Productions, и определенную долю разъяснений можно получить там.
Рассмотрим поэтапно работу программы.
В первую очередь определяются переменные для организации нужных пауз в игровом цикле. Это необходимо для того, чтобы добиться фиксированного FPS в 30 фреймов.
Uint32 start, end; Uint32 cdelay = 1000/FRAME_PER_SEC;
Затем определяется класс для чтения конфигурации и задаются параметры для основной поверхности (screen).
ConfReader cr; cr.read_config("config.cfg"); int sw, sh, bits; getWindowParams(cr, sw, sh, bits); SDL_Init(SDL_INIT_EVERYTHING); screen = SDL_SetVideoMode(sw, sh, bits, SDL_HWSURFACE);
В данном приложении я вынес процесс чтения данных из config.cfg в отдельные процедуры. getWindowParams — одна из них.
void getWindowParams(ConfReader& cr, int& sw, int& sh, int& bits) { string key_whb = "screen_whb"; std::vector<int> nums; cr.getInts(key_whb, nums); sw = nums[0]; sh = nums[1]; bits = nums[2]; };
Далее загружаем банк спрайтов по указанному в конфигурации файлу.
SpriteBank* sb = getSpriteBank(cr); if(!sb) return 0;
Короче говоря, все процедуры или функции, содержащие аргумент с именем cr, предназначены для получения параметров конфигурации.
SpriteBank* getSpriteBank(ConfReader& cr) { string key_file = "spritebank_file"; string f; cr.getString(key_file, f); return SpriteBankLoad(f.c_str()); };
После загрузки банка, определяется подмножество, содержащее фон.
SpriteSet* ssBack = getBackground(cr, sb); if(!ssBack) return 0;
Вот теперь мы подошли к самому главному — определению кнопки.
Button btnQuit; btnQuit.sb = sb; btnQuit.onClick = my_click_quit; setButton(cr, btnQuit); SDL_Event event; bool working = true; btnQuit.userData = &working;
Так как класс создан без аргументов, то следует инициализировать кнопку. Мы задаем указатель на банк спрайтов и на пользовательскую процедуру my_click_quit, которая должна вызываться по щелчку мыши. Как видно из названия процедуры, клик должен спровоцировать закрытие приложения. Это можно сделать, прекратив работу игрового цикла, указав значение false в переменной working. Значит, в качестве пользовательских данных кнопки будет выступать указатель на переменную working.
void my_click_quit(int x, int y, void* b) { bool* w = (bool*)b; *w = false; };
Сам игровой цикл выглядит следующим образом:
while(working) { start = SDL_GetTicks(); while(SDL_PollEvent(&event)) { if(event.type == SDL_QUIT) { working = false; }; btnQuit.handle_events(event); }; SpriteBankDrawSetId(sb, ssBack, 0, 0, 0); btnQuit.draw(); SDL_Flip(screen); end = SDL_GetTicks(); if(end - start < cdelay) SDL_Delay(cdelay - end + start); }; SpriteSetFree(ssBack); SpriteBankFree(sb); SDL_Quit(); return 0;
Поэтапно происходит следующее. Запоминаем текущее состояние таймера, затем обрабатываем события, передавая информацию о них в нашу кнопку через метод handle_events. Отображаем фон с помощью метода SpriteBankDrawSetId, а затем и саму кнопку. С помощью процедуры SDL_Flip делаем видимым результат рисования. В конце тела цикла получаем текущее состояние таймера и рассчитываем необходимую пауза для фиксированных 30 FPS.
После завершения цикла освобождаем память, отводимую для банка спрайтов и подмножества с фоном, закрываем контекст sdl.
Исходники к статье здесь.
Добавить комментарий