Перевод SDL Game Framework Series. Часть 2 — SDL Coordinates and Bliting

в 2:50, , рубрики: c++, game development, Gamedev, sdl

Взяв за основу первый урок, мы будем углубляться в мир поверхностей SDL. Как я уже говорил, SDL поверхности, в основном, это изображения, сохраненные в памяти. Представьте себе, что у нас есть пустое окно размером 320x240 пикселей. В системе координат SDL, окно представлено следующим образом:

Перевод SDL Game Framework Series. Часть 2 — SDL Coordinates and Bliting

Эта система координат отличается от той к которой вы привыкли (я про декартову). Но основное отличие между этими системами в том, что координата Y «растет» вниз. Понимание системы SDL координат важно, чтобы правильно рисовать изображения на экране, так что уж вникните хорошенько.

Так как у нас уже есть подготовленная и настроенная поверхность (Surf_Display), нам осталось только найти способ отрисовки изображений. Этот способ называется блитированием (от англ. blitting — перемещение группы бит из одного места в другое, в нашем случае подразумевается перенос изображения (или его части) поверх другого), т.е. своего рода наложение. Но прежде чем мы сможем это сделать, мы должны найти ещё и способ загрузить эти изображения в память. SDL предлагает простую функцию, чтобы осуществить задуманное — SDL_LoadBMP (примечание: SDL_LoadBMP предоставляет возможность загрузки изображений только в формате *.BMP, как видно из её названия. Чтобы загружать изображения других форматов, к проекту нужно подключить библиотеку SDL_image, как справедливо заметил в комментариях товарищ alrusdi в первом уроке, и использовать функцию IMG_Load). Пример кода может выглядеть следующим образом:

Пример

SDL_Surface* Surf_Temp;
 
if((Surf_Temp = SDL_LoadBMP("mypicture.bmp")) == NULL) {
    //Обшибка!
}

Здесь всё довольно просто, SDL_LoadBMP принимает в качестве параметра всего один аргумент — путь до файла, который вы хотите загрузить, а возвращает она поверхность, содержащую указанное изображение. Если функция возвращает NULL, то либо файл не найден, либо поврежден, либо возникли другие, более сложные ошибки. К сожалению, в ущерб эффективности, этот метод не обеспечивает полное покрытие всевозможных ошибок загрузки. Очень часто загруженное изображение не соответствует попиксельному формату той поверхности, в которую мы его загружаем. Таким образом во время отображения возможна потеря производительности, цветов изображения, и т.д. (важно чтобы подготавливаемая поверхность и загружаемое изображение подходили друг другу по всем параметрам, т.е. (утрируя) размер коробки подходил бы размерам груза). К счастью в SDL существует быстрый и безболезненный обход этой проблемы — SDL_DisplayFormat. Эта функция настраивает уже загруженное изображение, и возвращает новую поверхность, подходящую под формат отображаемой.
Теперь вам необходимо открыть проект, созданный в предыдущем уроке и добавить два файла: CSurface.h и CSurface.cpp. Откройте CSurface.h и добавьте следующее:

CSurface.h

#ifndef _CSURFACE_H_
    #define _CSURFACE_H_
 
#include <SDL/SDL.h>
 
class CSurface {
    public:
        CSurface();
 
    public:
        static SDL_Surface* OnLoad(char* File);
};
 
#endif

Тем самым мы создали простую функцию OnLoad, которая будет загружать для нас поверхность. Теперь откройте CSurface.cpp и добавьте:

CSurface.cpp

#include "CSurface.h"
 
CSurface::CSurface() {
}
 
SDL_Surface* CSurface::OnLoad(char* File) {
    SDL_Surface* Surf_Temp = NULL;
    SDL_Surface* Surf_Return = NULL;
 
    if((Surf_Temp = SDL_LoadBMP(File)) == NULL) {
        return NULL;
    }
 
    Surf_Return = SDL_DisplayFormat(Surf_Temp);
    SDL_FreeSurface(Surf_Temp);
 
    return Surf_Return;
}

Итак, парочка вещей, на которые стоит обратить внимание:
1. Всегда обнуляйте свои указатели прежде чем как-либо их использовать (NULL или «0» неважно). Это поможет избежать туевой хучи самых разных проблем и ошибок;
2. Помните, что SDL_DisplayFormat возвращает новую поверхность на основе старой, поэтому не стоит забывать освободить ресурсы, занимаемые той старой поверхностью. В противном случае мы будем наблюдать поверхность «блуждающую» в памяти так как ей заблагорассудится.
Теперь у нас есть способ загрузки поверхностей в память, но нам также нужен способ, чтобы отобразить их на другие поверхности. Так же как и для загрузки изображений, у SDL есть функция и для этого: SDL_BlitSurface. Возможно её будет не так просто использовать как SDL_LoadBMP, но не стоит пугаться. Откройте CSurface.h и добавьте следующий прототип функции:

CSurface.h

#ifndef _CSURFACE_H_
    #define _CSURFACE_H_
 
#include <SDL/SDL.h>
 
class CSurface {
    public:
        CSurface();
 
    public:
        static SDL_Surface* OnLoad(char* File);
 
        static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y);
};
 
#endif

Снова откройте CSurface.cpp и добавьте следующее:

CSurface.cpp

#include "CSurface.h"
 
CSurface::CSurface() {
}
 
SDL_Surface* CSurface::OnLoad(char* File) {
    SDL_Surface* Surf_Temp = NULL;
    SDL_Surface* Surf_Return = NULL;
 
    if((Surf_Temp = SDL_LoadBMP(File)) == NULL) {
        return NULL;
    }
 
    Surf_Return = SDL_DisplayFormat(Surf_Temp);
    SDL_FreeSurface(Surf_Temp);
 
    return Surf_Return;
}
 
bool CSurface::OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y) {
    if(Surf_Dest == NULL || Surf_Src == NULL) {
        return false;
    }
 
    SDL_Rect DestR;
 
    DestR.x = X;
    DestR.y = Y;
 
    SDL_BlitSurface(Surf_Src, NULL, Surf_Dest, &DestR);
 
    return true;
}

Прежде всего, давайте взглянем на аргументы, которые передаются в функцию OnDraw. Мы видим две поверхности, и две переменные типа int. Первая поверхность берется в качестве базовой (помните доску в первом уроке?), т.е. той на которую мы и будем всё отображать в дальнейшем. Соответственно вторая поверхность — та, которую мы будем накладывать на базовую (а вот и наши стикеры). В принципе, мы просто размещаем Surf_Src поверх Surf_Dest, вот и весь секрет. X и Y — переменные, которые обозначают координаты места на поверхности Surf_Dest, в которое мы будем отображать Surf_Src.
В начале функции мы должны убедиться, что у нас есть поверхности, в противном случае мы возвращаем false. Далее, мы создаем переменную типа SDL_Rect. Это структура SDL, которая состоит из четырех свойств: X, Y, W, H. Вы уже конечно догадались что она то как раз и задает параметры отображаемого региона поверхности. Пока нас интересуют только координаты места в которое мы будем отображать прямоугольник, и нам наплевать на его размер. Итак, далее мы присваиваем переданные в функцию X, Y координаты структуре отображаемого региона. Если вам интересно, что же за параметр NULL затесался в нашей SDL_BlitSurface (да, автор, нам интересно!), это еще один параметр типа SDL_Rect. Мы вернемся к этому чуть позже.
Позже наступило! Думаю что никто не обидится если мы разберем сигнатуру SDL_BlitSurface чуть раньше. Вкратце поясню: нам не всегда нужно отображать всю поверхность поверх другой, есть много случаев, когда требуется выбрать какую-то часть изображения (например у нас есть тайлсет (от англ. tileset — набор изображений, попросту множество картинок, собранных в одном изображении) и нужно выбрать из него определённый квадратик с текстурой или персонажем, и т.д.). Так вот

int SDL_BlitSurface(SDL_Surface *src, SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect);

принимает в качестве параметров по порядку, слева направо:

  • поверхность, которую будем накладывать;
  • параметры региона отображения накладываемой поверхности (т.е. какую её часть мы будем отображать);
  • поверхность, на которую будем накладывать;
  • ну и, соответственно, параметры региона базовой поверхности, в который будем накладывать.

Думаю теперь всё стало более-менее прозрачно и понятно.
В завершении функции мы отрисовываем настроенные поверхности и возвращаем true.

Теперь, чтобы убедиться, что все работает, давайте создадим тестовую поверхность. Откройте CApp.h, и добавьте новую поверхность, и включите созданный нами заголовочный файл CSurface.h:

CApp.h

#ifndef _CAPP_H_
    #define _CAPP_H_
 
#include <SDL/SDL.h>
 
#include "CSurface.h"
 
class CApp {
    private:
        bool            Running;
 
        SDL_Surface*    Surf_Display;
 
        SDL_Surface*    Surf_Test;
 
    public:
        CApp();
 
        int OnExecute();
 
    public:
        bool OnInit();
 
        void OnEvent(SDL_Event* Event);
 
        void OnLoop();
 
        void OnRender();
 
        void OnCleanup();
};
 
#endif

Также в конструкторе не забудьте сначала обнулить наши поверхности:

CApp.cpp

CApp::CApp() {
    Surf_Test = NULL;
    Surf_Display = NULL;
 
    Running = true;
}

И помните об очистке!

CApp_OnCleanup.cpp

#include "CApp.h"
 
void CApp::OnCleanup() {
    SDL_FreeSurface(Surf_Test);
    SDL_FreeSurface(Surf_Display);
    SDL_Quit();
}

Настало время уже что-то загрузить. Откройте CApp_OnInit.cpp и приведите его к такому виду:

CApp_OnInit.cpp

#include "CApp.h"
 
bool CApp::OnInit() {
    if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        return false;
    }
 
    if((Surf_Display = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
        return false;
    }
 
    if((Surf_Test = CSurface::OnLoad("myimage.bmp")) == NULL) {
        return false;
    }
 
    return true;
}

Убедитесь в том что у вас действительно имеется файл myimage.bmp. Если нет — скачайте или нарисуйте сами и положите его в каталог с исполняемым файлом вашей игры. Откройте CApp_OnRender.cpp и добавьте следующее:

CApp_OnRender.cpp

#include "CApp.h"
 
void CApp::OnRender() {
    CSurface::OnDraw(Surf_Display, Surf_Test, 0, 0);
 
    SDL_Flip(Surf_Display);
}

Обратите внимание на новую функцию SDL_Flip. Она обновляет буфер и отображает Surf_Display на экран. Это называется двойной буферизацией. Она подготавливает созданные поверхности сначала в памяти, а затем отображает подготовленное на экран. Если бы мы не использовали её, то наблюдали бы мерцающий экран. Помните флаг SDL_DOUBLEBUF, который мы указывали при создании поверхности? Он-то как раз и включает режим двойной буферизации.
Теперь вы можете откомпилировать проект, и убедиться, что все работает правильно. Вы должны увидеть изображение в верхнем левом углу окна. Если да, то поздравляю, вы еще на один шаг ближе к реальной игре. Если нет, то убедитесь в том, что у вас myimage.bmp лежит в той же папке, что и исполняемый файл, а также в том, что он нормально открывается в просмотрщике графики. Вот что получилось у меня:

Перевод SDL Game Framework Series. Часть 2 — SDL Coordinates and Bliting

(И да, я немного схитрил видоизменил код и загрузил свой аватар в формате *.PNG, используя IMG_Load. Советую вам тоже поэкпериментировать с этой функцией, да и с другими тоже. Дерзайте и у вас всё получится!). Если у вас появляется сообщение deprecated conversion from string constant to ‘char*’ -wwrite-strings необходимо изменить сигнатуру функции OnLoad(char* File) на OnLoad(const char* File) в CSurface.h и соответственно в CSurface.cpp.

Двинемся дальше! Мы потешили себя тем, что отобразили, наконец, в окошке наше первое изображение, но очень часто нам необходимо отобразить всего лишь его часть, как пример — тайлсеты, указанные ниже:

Tilesets

Перевод SDL Game Framework Series. Часть 2 — SDL Coordinates and Bliting

Перевод SDL Game Framework Series. Часть 2 — SDL Coordinates and Bliting

Перевод SDL Game Framework Series. Часть 2 — SDL Coordinates and Bliting

Перевод SDL Game Framework Series. Часть 2 — SDL Coordinates and Bliting

Т.е. имея всего одно изображение, нам нужно нарисовать только его часть. Откройте CSurface.h, и добавьте следующий код:

CSurface.h

#ifndef _CSURFACE_H_
    #define _CSURFACE_H_
 
#include <SDL/SDL.h>
 
class CSurface {
    public:
        CSurface();
 
    public:
        static SDL_Surface* OnLoad(char* File);
 
        static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y);
 
        static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y, int X2, int Y2, int W, int H);
};
 
#endif

Откройте CSurface.cpp, и добавьте следующую функцию (Важно, мы добавляем вторую функцию OnDraw, а не заменяем уже имеющуюся! Вы же в курсе про перегрузку функций?):

CSurface.cpp

bool CSurface::OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y, int X2, int Y2, int W, int H) {
    if(Surf_Dest == NULL || Surf_Src == NULL) {
        return false;
    }
 
    SDL_Rect DestR;
 
    DestR.x = X;
    DestR.y = Y;
 
    SDL_Rect SrcR;
 
    SrcR.x = X2;
    SrcR.y = Y2;
    SrcR.w = W;
    SrcR.h = H;
 
    SDL_BlitSurface(Surf_Src, &SrcR, Surf_Dest, &DestR);
 
    return true;
}

Видите, это в основном та же функция, как и раньше, за исключением того, мы добавили еще один SDL_Rect. Этот регион позволяет указать, какие пиксели из накладываемой поверхности нужно скопировать на основную. Теперь вкупе с координатами мы указываем ещё и оставшиеся два параметра — ширину и высоту 0, 0, 50, 50 и в итоге получаем отображаемый регион в виде квадрата 50x50 пикселей.

Перевод SDL Game Framework Series. Часть 2 — SDL Coordinates and Bliting

CApp_OnRender.cpp

#include "CApp.h"
 
void CApp::OnRender() {
    CSurface::OnDraw(Surf_Display, Surf_Test, 0, 0);
    CSurface::OnDraw(Surf_Display, Surf_Test, 100, 100, 0, 0, 50, 50);
 
    SDL_Flip(Surf_Display);
}

А вот частичка моего аватара с отступом в 100 пикселей от верха и левого края экрана:

Перевод SDL Game Framework Series. Часть 2 — SDL Coordinates and Bliting

Ссылка на исходный код:

Ссылки на все уроки:

Автор: m0sk1t

Источник


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


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