Skip to content

lesson 2 dont put everything in main

Zhao Yipeng edited this page Dec 7, 2017 · 5 revisions

Lesson 2: 不要所有代码都放在主函数当中

原文地址:http://www.willusher.io/sdl2%20tutorials/2013/08/17/lesson-2-dont-put-everything-in-main

在本课中,我们将从上一课开始,组织纹理加载和渲染代码,将它们从主函数中移到一些有用的函数中。我们还将编写一个简单的通用SDL错误记录器,并学习在使用SDL进行渲染时如何定位和缩放图像。

让我们先声明一些窗口宽度和高度的常量。稍后将需要这些常量决定如何定位图像。

const
  SCREEN_WIDTH: Integer = 640;
  SCREEN_HEIGHT: Integer = 480;

The SDL Error Logger

Throughout lesson 1 we had a lot of repeated code used to print out error messages that was almost the same for each error, except for some different information about which function went wrong. We can improve on this with a more generic error logging function that can take any std::ostream to write to, a message to print and will write out the message along with the error message from SDL_GetError to the stream.

/// <summary>
/// Log an SDL error with some error message to the output stream of our choice
/// </summary>
/// <param name="msg">The error message to write, format will be msg error: SDL_GetError()</param>
procedure logSDLError(const msg: string);
begin
  Writeln(msg, ' error: ', SDL_GetError);
end;

The Texture Loading Function

To wrap up the texture loading from lesson 1 we’ll create a function that takes a filepath to a BMP to load and the renderer to load the texture onto and returns the loaded SDL_Texture*. It’s also important that this function perform the same error checking we had previously and still return a nullptr in case of an error so that we know something went wrong. We’ll define the function loadTexture to do this for us.

First we initialize an SDL_Texture* to nullptr so that in case of an error a valid nullptr is returned instead of a dangling pointer. Next we’ll load up the BMP as before and check for errors, using our new logSDLError function to print out any errors that occurred. If the surface loads ok we then create the texture from the surface and perform an error check on that. If everything goes ok we get back a valid pointer, if not we’ll get back a nullptr and the error messages will show up in our log.

/// <summary>
/// Loads a BMP image into a texture on the rendering device
/// </summary>
/// <param name="AFile">The BMP image file to load</param>
/// <param name="ren">The renderer to load the texture onto</param>
/// <returns>the loaded texture, or nil if something went wrong.</returns>
function loadTexture(const AFile: string; ren: PSDL_Renderer): PSDL_Texture;
var
  texture: PSDL_Texture;
  loadedImage: PSDL_Surface;
begin
  // Initialize to nullptr to avoid dangling pointer issues
  texture := nil;
  // Load the image
  loadedImage := SDL_LoadBMP(PAnsiChar(AnsiString(AFile)));
  // If the loading went ok, convert to texture and return the texture
  if loadedImage <> nil then
  begin
    texture := SDL_CreateTextureFromSurface(ren, loadedImage);
    SDL_FreeSurface(loadedImage);
    // Make sure converting went ok too
    if texture = nil then
    begin
      logSDLError('CreateTextureFromSurface');
    end;
  end
  else
  begin
    logSDLError('LoadBMP');
  end;
  Result := texture;
end;

The Texture Rendering Function

In this lesson we’re going to be drawing textures at some x,y coordinate while preserving their initial width and height. To do this we’ll need to create a destination rectangle to pass to SDL_RenderCopy and get the texture’s width and height with SDL_QueryTexture so that its size will be preserved when rendering. This is a lot to do each time we want to draw, so we’ll create a function, renderTexture, that will take the x and y coordinates to draw to, the texture and the renderer and will setup the destination rectangle correctly and draw the texture.

The destination rectangle is a SDL_Rect with the x and y set to the pixel location on the screen we want the texture’s top left corner to be at and the width and height set to the texture’s width and height. The width and height values are retrieved through SDL_QueryTexture. We then render the texture at the destination rectangle and pass NULL as the source rectangle since we still want to draw the entire texture. You can also set your own width and height values to shrink or stretch the texture as desired.

/// <summary>
/// Draw an SDL_Texture to an SDL_Renderer at position x, y, preserving
/// the texture's width and height
/// </summary>
/// <param name="tex">The source texture we want to draw</param>
/// <param name="ren">The renderer we want to draw to</param>
/// <param name="x">The x coordinate to draw to</param>
/// <param name="y">The y coordinate to draw to</param>
procedure renderTexture(tex: PSDL_Texture; ren: PSDL_Renderer; x, y: Integer);
var
  dst: TSDL_Rect;
begin
  // Setup the destination rectangle to be at the position we want
  dst.x := x;
  dst.y := y;
  // Query the texture to get its width and height to use
  SDL_QueryTexture(tex, nil, nil, @dst.w, @dst.h);
  SDL_RenderCopy(ren, tex, nil, @dst);
end;

Creating the Window and Renderer

We initialize SDL and create our window and renderer the same as in lesson 1 but now we use our logSDLError function to print out any errors that occurred and use the constants we defined earlier as the screen width and height.

  if SDL_Init(SDL_INIT_EVERYTHING) <> 0 then
  begin
    logSDLError('SDL_Init');
    Exit(1);
  end;

  window := SDL_CreateWindow('Lesson 2', 100, 100, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
  if window = nil then
  begin
    logSDLError('CreateWindow');
    SDL_Quit();
    Exit(1);
  end;
  renderer := SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED or SDL_RENDERER_PRESENTVSYNC);
  if renderer = nil then
  begin
    logSDLError('CreateRenderer');
    cleanup(window);
    SDL_Quit();
    Exit(1);
  end;

Loading Our Textures

In this lesson we’ll be drawing a tiled background and a centered foreground image, grab both of them below, or use your own BMP images.

Background Tile

Foreground

We’ll load the textures using our loadTexture function and exit if either fails to load. You should update the filepaths to match your project structure.

  resPath := getResourcePath('Lesson2');
  background := loadTexture((resPath) + 'background.bmp', renderer);
  image := loadTexture(resPath + 'image.bmp', renderer);
  //Make sure they both loaded ok
  if (background = nil)  or  (image = nil) then
  begin
    cleanup(background);
    cleanup(image);
    cleanup(renderer);
    cleanup(window);
    SDL_Quit();
    Exit(1);
  end;

The SDL Coordinate System and Drawing Order

The coordinate system used by SDL to place images on the screen has 0,0 at the top-left corner of the window and SCREEN_WIDTH,SCREEN_HEIGHT at the bottom-right corner. Textures are drawn back to front, with each call to SDL_RenderCopy drawing the new texture on top of the current scene, so we’ll want to draw our background tiles first and then draw our foreground image.

Drawing a Tiled Background

Our background image is 320x240 pixels and we’d like to tile it so that it fills the entire window, which is 640x480 pixels so we’ll need to draw the image four times. Each tile will be scooted over by either the texture width, height or both depending on the location we want it at so that the tile edges all line up. We can retrieve the texture’s width through SDL_QueryTexture like we did in renderTexture and then draw each tile, adjusting each draw over and down as needed.

Exercise Problem: While it’s not so bad to type out the draw positions for just four tiles it would be ridiculous to do so if we wanted to put down a large number of tiles. How could we compute the tile positions to fill the screen completely?

Note: All of this rendering code will be placed within our main loop, similar to lesson 1.

SDL_RenderClear(renderer);
//Get the width and height from the texture so we know how much to move x,y by
//to tile it correctly
SDL_QueryTexture(background, nil, nil, @bW, @bH);
//We want to tile our background so draw it 4 times
renderTexture(background, renderer, 0, 0);
renderTexture(background, renderer, bW, 0);
renderTexture(background, renderer, 0, bH);
renderTexture(background, renderer, bW, bH);

Drawing the Foreground Image

The foreground image will be drawn centered in the window, but since we specify the draw position for the top-left corner of the texture we’ll need to offset it some to put the center of the image in the center of the screen. This offset is computed by moving the x draw position left by half the texture width and the y position up by half the image width from the center of the screen. If we didn’t do this offset the top-left corner of the image would be drawn at the center of the screen instead.

After drawing our textures we’ll present the render and give ourselves a few seconds to admire our work.

SDL_QueryTexture(image, nil, nil, @iW, @iH);
x := SCREEN_WIDTH div 2 - iW div 2;
y := SCREEN_HEIGHT div 2 - iH div 2;
renderTexture(image, renderer, x, y);
SDL_RenderPresent(renderer);
SDL_Delay(1000);

Cleaning Up

Before we exit we’ve got to free our textures, renderer and window and quit SDL.

cleanup(background);
cleanup(image);
cleanup(renderer);
cleanup(window);
SDL_Quit();

End of Lesson

If everything went well and you used the images provided you should see this draw to your window.