Lesson 2: 不要把全部东西都放在main函数里面
这节课,我们将处理纹理和渲染的代码移出main方法。我们还将编写一个简单通用的错误记录器,并了解如何在SDL渲染时对图像进行定位和缩放。
我们先声明一些常量,为窗口设置宽度和高度。在定位图片时,我们将用到这些参数。
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
SDL错误记录器
在课程1,我们使用了很多的重复的代码打印错误(除了一些关于函数出错的信息)。我们可以使用一个更加通用的错误记录函数来改进这一点。这个函数采用任何syd::stream来写入要打印的消息,并将要打印的信息和SDL_GetError中的错误信息打印出来。
/**
* Log an SDL error with some error message to the output stream of our choice
* @param os The output stream to write the message to
* @param msg The error message to write, format will be msg error: SDL_GetError()
*/
void logSDLError(std::ostream &os, const std::string &msg){
os << msg << " error: " << SDL_GetError() << std::endl;
}
纹理加载方法
根据课程1,我们将创建一个函数,它需要文件的路径去加载一个bmp文件,并使用渲染器将图片加载纹理返回一个SDL_Texture*指针。这个函数同样执行同样的错误检查,当出错时会返回一个nullptr。我们将这个方法名定义为loadTexture。
首先,我们将SDL_Texture指针初始化为nullptr,以便如果出错可以返回一个nullptr而不是NULL。接下来, 我们将像以前一样加载BMP图片, 并检查错误, 使用我们的新 logSDLError 函数打印出发生的任何错误。如果表面加载确定, 我们然后创建纹理从表面, 并执行错误检查。如果一切顺利, 我们得到了一个有效的指针, 如果不是, 我们将返回一个 nullptr, 错误信息将显示在日志中。
/**
* Loads a BMP image into a texture on the rendering device
* @param file The BMP image file to load
* @param ren The renderer to load the texture onto
* @return the loaded texture, or nullptr if something went wrong.
*/
SDL_Texture* loadTexture(const std::string &file, SDL_Renderer *ren){
//Initialize to nullptr to avoid dangling pointer issues
SDL_Texture *texture = nullptr;
//Load the image
SDL_Surface *loadedImage = SDL_LoadBMP(file.c_str());
//If the loading went ok, convert to texture and return the texture
if (loadedImage != nullptr){
texture = SDL_CreateTextureFromSurface(ren, loadedImage);
SDL_FreeSurface(loadedImage);
//Make sure converting went ok too
if (texture == nullptr){
logSDLError(std::cout, "CreateTextureFromSurface");
}
}
else {
logSDLError(std::cout, "LoadBMP");
}
return texture;
}
纹理渲染方法
这一小节,我们绘制顶点位置为(x, y),宽度和高度不变的纹理(Texture)。为了做到这一点,我们需要创建一个目标矩阵,将它传递到SDL_RenderCopy,获得纹理的宽度和高度与 SDL_QueryTexture,渲染时可以保存纹理的宽度和高度。每次我们想绘制图案,就调用这个函数,函数会使用x和y坐标去绘制图案,纹理和渲染器会正确设置目标矩阵的位置并绘制纹理。
目标矩阵是一个SDL_Rect,纹理的左上角的坐标为(x,y),宽度和高度设置为纹理的宽度和高度。通过 SDL_QueryTexture 检索宽度和高度值。当想要绘制整个纹理, 需要在目标矩形处呈现纹理,将源矩阵设置为NULL。你还可以根据需要设置自己的宽度和高度值以收缩或拉伸纹理。
/**
* Draw an SDL_Texture to an SDL_Renderer at position x, y, preserving
* the texture's width and height
* @param tex The source texture we want to draw
* @param ren The renderer we want to draw to
* @param x The x coordinate to draw to
* @param y The y coordinate to draw to
*/
void renderTexture(SDL_Texture *tex, SDL_Renderer *ren, int x, int y){
//Setup the destination rectangle to be at the position we want
SDL_Rect dst;
dst.x = x;
dst.y = y;
//Query the texture to get its width and height to use
SDL_QueryTexture(tex, NULL, NULL, &dst.w, &dst.h);
SDL_RenderCopy(ren, tex, NULL, &dst);
}
创建窗口和渲染器
我们初始化SDL并创建窗口和渲染器的方式与1课相似, 但现在我们使用 logSDLError 函数打印出发生的任何错误, 并使用我们早先定义的屏幕宽度和高度的常量创建窗口。
if (SDL_Init(SDL_INIT_EVERYTHING) != 0){
logSDLError(std::cout, "SDL_Init");
return 1;
}
SDL_Window *window = SDL_CreateWindow("Lesson 2", 100, 100, SCREEN_WIDTH,
SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
if (window == nullptr){
logSDLError(std::cout, "CreateWindow");
SDL_Quit();
return 1;
}
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (renderer == nullptr){
logSDLError(std::cout, "CreateRenderer");
cleanup(window);
SDL_Quit();
return 1;
}
加载纹理
在本课中, 我们将绘制一个平铺背景和一个居中的前景图像, 现在把两个图案列在下面, 或者使用您自己的 BMP 图像。
背景图
前景图
我们将使用我们的 loadTexture 函数加载纹理和退出, 如果无法加载。您应该更新 filepaths 以匹配您的项目结构。
const std::string resPath = getResourcePath("Lesson2");
SDL_Texture *background = loadTexture(resPath + "background.bmp", renderer);
SDL_Texture *image = loadTexture(resPath + "image.bmp", renderer);
if (background == nullptr || image == nullptr){
cleanup(background, image, renderer, window);
SDL_Quit();
return 1;
}
SDL坐标系和绘图顺序
SDL的坐标系中,左上角坐标为(0,0),右下角的坐标为(SCREEN_WIDTH, SCREEN_HEIGHT)。调用SDL_RenderCopy将新的纹理绘制到当前场景的顶部,我们需要先绘制背景,然后再绘制前景图像。
绘制平铺背景
背景图是320×240像素,如果我们想要将这个图片平铺到整个640*480的屏幕,需要绘制这个图案四次。每个图块都将通过纹理宽度、高度或两者冲, 这取决于我们所需要的位置, 以便平铺边缘全部排成一行(没看懂什么意思)。我们可以像在 renderTexture 中一样, 通过 SDL_QueryTexture 来检索纹理的宽度, 然后绘制每个图块, 根据需要调整每个绘图。
练习问题: 虽然它不是那么糟糕, 绘制出来的画的位置, 只有四个, 这将是荒谬的, 如果我们要在屏幕防止大量的图片。我们如何计算图片的位置来完全填满屏幕?
注意: 所有这些渲染代码将放在我们的主循环中, 类似于课程1。
SDL_RenderClear(renderer);
int bW, bH;
SDL_QueryTexture(background, NULL, NULL, &bW, &bH);
renderTexture(background, renderer, 0, 0);
renderTexture(background, renderer, bW, 0);
renderTexture(background, renderer, 0, bH);
renderTexture(background, renderer, bW, bH);
绘制前景图像
前景图像将在窗口中居中绘制, 但由于我们指定了纹理左上角的绘制位置, 因此需要对其进行偏移, 以将图像的中心放在屏幕中央。此偏移量是通过将 x 绘制位置向左移动一半的纹理宽度和 y 位置由屏幕中央的图像宽度的一半来计算的。如果我们不这样做, 图像的左上角将被绘制在屏幕的中心。
绘制纹理后, 我们将呈现渲染, 并给自己几秒钟的时间来欣赏我们的工作。
int iW, iH;
SDL_QueryTexture(image, NULL, NULL, &iW, &iH);
int x = SCREEN_WIDTH / 2 - iW / 2;
int y = SCREEN_HEIGHT / 2 - iH / 2;
renderTexture(image, renderer, x, y);
SDL_RenderPresent(renderer);
SDL_Delay(1000);
释放内存
在退出之前, 我们必须释放我们的纹理、渲染器和窗口, 然后退出 SDL。
cleanup(background, image, renderer, window);
SDL_Quit();
结束
如果一切顺利, 你使用的图像提供你应该看到绘制到窗口的图像。
如果您有任何问题, 请检查您的错误日志, 查看可能出现问题的位置和/或张贴评论。