SDL 2: Основы

в 12:42, , рубрики: c++, game development, Gamedev, gamedevelopment, graphics, sdl, sdl2, tutorial, Программирование, метки: , , , , , ,

К сожалению, даже на официальной вики почти не возможно найти каких либо примеров использования SDL2.x, что уж говорить о рунете. Пытаясь разобраться, я нашел всего лишь пару статей, которые не покрыли и трети моих вопросов.
SDL 2.x существенно отличается от 1.x и даже, если в прошлом вам приходилось с ним работать — теперь вы рискуете ничего не понять.

Сегодня мы напишем простенькую программу выводящую на экран фон и зумируемый спрайт персонажа перемещающегося с помощью WASD и стрелок. + разберемся как в SDL работать с мышкой.

Для работы нам понадобится:

  • SDL2.h
  • SDL2_image.h>
  • SDL2_mixer.h>

Все это можно легко найти на просторах интернета.

Начнем с самого начала:

Инициализация SDL

Начнем, пожалуй, с создания объекта класса SDL_DisplayMode.
Он нам очень пригодится, если мы хотим иметь приложение на весь экран.
Этот объект нужно создать до инициализации самого SDL.

SDL_DisplayMode displayMode;

После этого нужно проинициализировать сам SDL:

if (SDL_Init(SDL_INIT_EVERYTHING) != 0){
	std::cout << "SDL_Init Error: " << SDL_GetError() << std::endl;
	return 1;
}

Флаг SDL_INIT_EVERYTHING инициализирует все подсистемы SDL. Если вам нужно только что то конкретное, то на вики можно найти их полный перечень.

Теперь нам нужно получить параметры монитора с которым мы работаем.
Для этого мы создаем интовую переменную, в которую будет возвращен 0, если все прошло успешно и приравниваем ее функции SDL_GetDesktopDisplayMode(*int displayIndex, SDL_DisplayMode* mode).
Если в первый аргумент записать 0, то функция обратиться к главному монитору. Все полученные параметры мы сможем считать с объекта displayMode.

	int request = SDL_GetDesktopDisplayMode(0,&displayMode);

Пришло время заняться нашим окном!
Тут все предельно просто, создаем указатель на объект класса SDL_Window и вызываем функцию

SDL_Window* SDL_CreateWindow(const char* title, int x, int y, int w, int h, Uint32 flags) 

Тут все конечно и так ясно, но на всякий случай объясню что к чему.

  • title — имя окна.
  • x,y — координаты окна. Если хотим открыть на весь экран, то нужно ставить 0,0
  • w,h — размеры окна. Что бы открыть на весть экран обращаемся к объекту displayMode.
  • flags — тут выставляем флаги инициализации окна. Вы можете сказать, что я тупой не прав и существует флаг SDL_WINDOW_FULLSCREEN, и я тут изобретаю велосипед своим DisplayMode, но нет!
    Путем научного тыка продуктивных экспериментов, я заметил что такой способ гораздо быстрее и на него не реагируют антивирусники. На тот же SDL_WINDOW_FULLSCREEN аваст кричал, что меня пытаются взломать.

SDL_WINDOW_SHOWN — делает окно видимым.

В итоге на выходе получаем такой код:

SDL_Window *win = SDL_CreateWindow("Hello World!", 0, 0, displayMode.w, displayMode.h, SDL_WINDOW_SHOWN);
if (win == nullptr){
	std::cout << "SDL_CreateWindow Error: " << SDL_GetError() << std::endl;
	return 1;
}

Теперь нам нужно создать рендер:

SDL_Renderer* SDL_CreateRenderer(SDL_Window* window,
                                 int         index,
                                 Uint32      flags)

  • window — окно в котором мы будем работать.
  • index — индекс драйвера который будет использовать рендер. Если поставить -1, то рендер будет использовать первый подходящий драйвер.
  • flags — флаги рендера. Полный список как всегда на вики.
    Я буду использовать SDL_RENDERER_ACCELERATED отвечающий за аппаратное ускорение и SDL_RENDERER_PRESENTVSYNC отвечающий за вертикальную синхронизацию.

Собираем все вместе и получаем:

SDL_Renderer *ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (ren == nullptr){
	std::cout << "SDL_CreateRenderer Error: " << SDL_GetError() << std::endl;
	return 1;
}

Теперь SDL готов с нами сотрудничать!

Вывод на экран

Пришло время заняться изображениями.
Для начала нам нужно создать 2 объекта класса SDL_Rect.
Этот объект будет содержать физические параметры наших текстур, таких как ширину, высоту и положение в окне.

SDL_Rect player_RECT;
		player_RECT.x = 0;   //Смещение полотна по Х
		player_RECT.y = 0;   //Смещение полотна по Y
		player_RECT.w = 333; //Ширина полотна
		player_RECT.h = 227; //Высота полотна

SDL_Rect background_RECT;
		background_RECT.x = 0;
		background_RECT.y = 0;
		background_RECT.w = displayMode.w;
		background_RECT.h = displayMode.h;

И еще пару строк, чтобы чуть позже мы смогли зумировать нашего персонажа:

const int player_WIGHT = 333;   //Ширина исходнго изображения
const int player_HEIGH = 227;   //Высота исходного изображения
double TESTtexture_SCALE = 1.0; //Множетель для зумирования

И вот мы добрались до загрузки текстур.
Я покажу 2 способа:

Но для начала небольшое отступление!
Думаю те кто раньше работали с SDL1.x в объяснениях не нуждаются, но я все же расскажу от том как устроен SDL, вдруг (ну мало ли) тут кто то с ним не знаком.
В SDL есть 4 основных класса/структуры участвующих в выводе изображения на экран: SDL_Texture, SDL_Surface, SDL_Rect, SDL_Render.

Про последние 2 мы уже поговорили, давайте теперь вкратце обсудим оставшиеся.

  • SDL_Surface — работая с SDL_mixer.h о нем вы можете забыть. Но глупо с чем то работать не имея ни малейшего представления о том как оно устроено.
    Подробности

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

    Как пример: чистый SDL работает только с BMP, которое поддерживает альфа-канал, только в 32-битном цвете, а он поддерживается для этого формата далеко не на каждой ОС. А тут уже теряется вся польза от кроссплатформенности SDL.

  • SDL_Texture — создав объект структуры SDL_Surface, мы должны превратить его в текстуру, что бы рендер смог ей заняться.

После этого этот объект отправляется в рендер:

SDL_RenderCopy(SDL_Renderer* renderer,  SDL_Texture* texture,  const SDL_Rect* srcrect,  const SDL_Rect* dstrect)
SDL_RenderPresent(SDL_Renderer* renderer)

Ну вот, с теорией разобрались, пора к практике!

Вариант номер РАЗ:
Этот метод завязан на библиотеках SDL_mixer.h и SDL_Image.h, так что не орите на меня удивляйтесь, когда на вас польются ошибки подключив только SDL2.

Его особенность в том, что он без велосипедов передает альфа-канал.

SDL_Texture *player =  IMG_LoadTexture(ren,"..\res\player.png");

Теперь у нас есть текстура персонажа готовая показаться на экране. Но перед этим нам нужно еще создать фон.

Вариант номер ДВА:
Фон мы создадим с использованием чистого SDL. Просто потому, что мы можем!

Код ТУТа!

SDL_Surface *BMP_background = SDL_LoadBMP("..\res\background.bmp");
if (BMP_background == nullptr){
	std::cout << "SDL_LoadBMP Error: " << SDL_GetError() << std::endl;
	return 1;
}

SDL_Texture *background = SDL_CreateTextureFromSurface(ren, BMP_background);
SDL_FreeSurface(BMP_background); //Очищение памяти поверхности
if (player == nullptr){
	std::cout << "SDL_CreateTextureFromSurface Error: " << SDL_GetError() << std::endl;
	return 1;
}

Работая с чистым SDL никогда нельзя забывать делать проверки на ошибки!

И вот наконец то, УРА, пришло время вывести это на экран!

SDL_RenderClear(ren); //Очистка рендера
SDL_RenderCopy(ren,background,NULL,&background_RECT); //Копируем в рендер фон
SDL_RenderCopy(ren, player, NULL, &player_RECT); //Копируем в рендер персонажа
SDL_RenderPresent(ren); //Погнали!!

События

Думаю не стоит объяснять что из 2-х картинок игры не выйдет.
Время добавить немного динамики к нашему чуду!

Для начало нужно создать парочку бесконечных циклов, которые работают пока есть события и нет выхода:

SDL_Event event;
bool quit = false;
while(!quit)
	while(SDL_PollEvent(&event))
	{
		SDL_PumpEvents(); // обработчик событий.
	}

Пора заняться непосредственно событиями.
В SDL есть 2 способа считывать события с контроллеров:

  • Первым способом мы реализуем работу с мышкой.
    Но для начала добавим пару строк перед циклами:

    SDL_Texture* ARRAY_textures[2] = {background, player};
    SDL_Rect* ARRAY_rect[2] = {&background_RECT, &player_RECT};
    int ARRAY_texturesState[2] = {1,1};

    Это нам пригодится чтобы иметь возможность отображать или не отображать ту или иную текстуру.
    А теперь вставляем этот код во внутренний цикл.

    Код не ТОРТ, он правда тут!

    if(event.type == SDL_QUIT)
    	quit=true;
    				
    if(event.type == SDL_MOUSEBUTTONDOWN)
    {
    	if(event.button.button == SDL_BUTTON_LEFT && event.button.x <=10 && event.button.y <=10)
    		quit = true;
    	if(event.button.button == SDL_BUTTON_RIGHT)
    		ARRAY_texturesState[1] = 1;
    	if((event.button.button == SDL_BUTTON_LEFT) && (event.button.x >= player_RECT.x) &&
    		(event.button.y >= player_RECT.y) &&
    		(event.button.x <= player_RECT.w + player_RECT.x) &&
    		(event.button.y <= player_RECT.h + player_RECT.y))
    		ARRAY_texturesState[1] = 0;
    }

    Думаю это не требует пояснений, если вы внимательно читали и занимаетесь программированием больше 21 дня, но все же уточню, что event.button.button ждет специальный флаг SDLя, который вы сможете легко найти на вики, а event.type ждет флага о типе события, полный список которых находится все там же!

    Мы уже можем закрыть окно кликом по левому верхнему углу экрана! И даже более того, мы можем убрать и вернуть персонажа когда захотим просто кликнув по нему!
    Да, я тоже чувствую, как ощущение власти начинает нас захлестывать, но не время останавливаться, впереди еще клавиатура!

  • Второй способ:
    Для начала перед циклом нам нужно создать одну константу:

    const Uint8 *keyboardState = SDL_GetKeyboardState(NULL);

    Она нужна что бы отслеживать состояния кнопок.

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

    Много-много кода

    void move_UP   (SDL_Renderer* render, SDL_Texture* texture, SDL_Rect &destrect, int offset = 5)
    {
    	destrect.y -= offset;
    	SDL_RenderClear(render);
    	SDL_RenderCopy(render, texture,NULL,&destrect);
    }
    void move_DOWN (SDL_Renderer* render, SDL_Texture* texture, SDL_Rect &destrect, int offset = 5)
    {
    	destrect.y += offset;
    	SDL_RenderClear(render);
    	SDL_RenderCopy(render, texture,NULL,&destrect);
    }
    void move_LEFT (SDL_Renderer* render, SDL_Texture* texture, SDL_Rect &destrect, int offset = 5)
    {
    	destrect.x -= offset;
    	SDL_RenderClear(render);
    	SDL_RenderCopy(render, texture,NULL,&destrect);
    }
    void move_RIGHT(SDL_Renderer* render, SDL_Texture* texture, SDL_Rect &destrect, int offset = 5)
    {
    	destrect.x += offset;
    	SDL_RenderClear(render);
    	SDL_RenderCopy(render, texture,NULL,&destrect);
    }
    
    void render_UPDATE(SDL_Renderer* render, SDL_Texture* texture[], SDL_Rect* destrect[], int states[])
    {
    	SDL_RenderClear(render);
    	if(states[0]) SDL_RenderCopy(render, texture[0],NULL,destrect[0]);
    	if(states[1]) SDL_RenderCopy(render, texture[1],NULL,destrect[1]);
    }

    И теперь возвращаемся во внутренний цикл и добавляем еще много-много кода:

    Много-много кода

    if((keyboardState[SDL_SCANCODE_UP])||(keyboardState[SDL_SCANCODE_W]))
    	move_UP(ren,player,player_RECT);
    				
    if((keyboardState[SDL_SCANCODE_DOWN])||(keyboardState[SDL_SCANCODE_S]))
    	move_DOWN(ren,player,player_RECT);
    
    if((keyboardState[SDL_SCANCODE_LEFT])||(keyboardState[SDL_SCANCODE_A]))
    	move_LEFT(ren,player,player_RECT);
    
    if((keyboardState[SDL_SCANCODE_RIGHT])||(keyboardState[SDL_SCANCODE_D]))
    	move_RIGHT(ren,player,player_RECT);
    
    
    //ZOOM----------------------------------------------------------------
    if(keyboardState[SDL_SCANCODE_KP_PLUS])
    {
    	TESTtexture_SCALE += 0.02;
    	player_RECT.h = player_HEIGH * TESTtexture_SCALE;
    	player_RECT.w = player_WIGHT * TESTtexture_SCALE;
    }
    if(keyboardState[SDL_SCANCODE_KP_MINUS])
    {
    	TESTtexture_SCALE -= 0.02;
    	player_RECT.h = player_HEIGH * TESTtexture_SCALE;
    	player_RECT.w = player_WIGHT * TESTtexture_SCALE;
    }

    Особо нового тут ничего нет. Единственное что добавилось это конструкция keyboardState[flag].
    Такая конструкция возвращает true в случае, если кнопка нажата и false в обратом.
    Список флагов… ТЫ УЖЕ ГОВОРИЛ МНОГО РАЗ!

Осталось вывести полученный результат на экран. Для этого добавляем в цикл:

render_UPDATE(ren, ARRAY_textures, ARRAY_rect, ARRAY_texturesState);	//Написанная нами функция обновления рендера
SDL_RenderPresent(ren);	

Закрываем цикл!
И в итоге нам осталось только завершить нашу программу.

Занавес!

Перед тем как все закончить нам нужно удалить наши текстуры из памяти.

SDL_DestroyTexture(player);
SDL_DestroyTexture(background);

И теперь можно смело завершать работу SDL и программы:

SDL_DestroyRenderer(ren);
SDL_DestroyWindow(win);
SDL_Quit();	
return 1;

Финал, овации! Мы написали первую программу на SDL2! С чем я нас поздравляю!

P.S.

Что бы создать такую элементарную программу у меня ушло 2 дня. В интернете настолько мало мануалов по SDL2, что проще застрелиться чем что то найти.
Очень надеюсь, что Вам эта статья была полезна и этот монстр не отберет у вас так много времени, как у меня.

Автор: Cripos

Источник


  1. Infintiy:

    void move_LEFT (SDL_Renderer* render, SDL_Texture* texture, SDL_Rect &destrect, int offset = 5)
    {
    destrect.x -= offset;
    SDL_RenderClear(render);
    SDL_RenderCopy(render, texture,NULL,&destrect);

    Ты одно и тоже сто раз применяешь – это супертормоза.
    Кусок
    SDL_RenderClear(render);
    SDL_RenderCopy(render, texture,NULL,&destrect);
    вынеси из функций и применяй его 1 раз в самом низу кода.

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js