Quantcast
Channel: GameDev.net
Viewing all articles
Browse latest Browse all 17825

Stretching Your Game To Fit The Screen Without Letterboxing - SDL2

$
0
0
If you're making a game that you want to be used on a wide variety of systems, your game has to be scalable, especially if it is in a very small resolution. This is usually pretty straightforward. Most graphics libraries and the like have things built in for doing the scaling for you. SDL2 does, but only to a certain extent. If you want your game to be scaled without letter-boxing, you have to write this yourself. It took me a while to figure out, so I though I would spread the knowledge.

The code included is written in C++ and uses SDL2, which can be found here: http://libsdl.org/. The documentation which describes the functions that I use is located here: http://wiki.libsdl.org/CategoryRender. And to set up SDL2, some easy instructions can be found at the awesome site LazyFoo

Scaling On Up


The theory here is this: You don't want letterboxing. You want the game to be taken from its original size and stretched to fit the window no matter what. So you just render everything to a back buffer by setting the target to it, instead of to the window's buffer, then at the end of your rendering cycle, switch the target back to the screen and RenderCopy the back buffer onto the screen, rescaling if neccessary, then set the target back to your back buffer and repeat. It's slightly trickier than it sounds.

Enter The Code


So in order to accomplish scaling without letter-boxing, we will need some SDL rectangles variables.

SDL_Rect nativeSize;
SDL_Rect newWindowSize;

One is for storing the original size of the game, nativeSize, and one is for storing the size of the window when it changes size, newWindowSize.

float scaleRatioW;
float scaleRatioH;

SDL_Window * window;
SDL_Renderer * renderer;
SDL_Texture * backBuffer;

SDL_Texture * ballImage;

bool resize;

We also will need an SDL_Window, an SDL_Renderer for that window, and last but not least a backBuffer for rendering everything to before copying it, properly scaled, to the window's buffer. Also there is ballImage which will be used to demonstrate the actual scaling. The two floats, scaleRatioW and scaleRatioH are optional. They are used if you need to scale the values of certain things that rely on the coordinates of the mouse, e.g. a button. You would multiply the x coord and width of its bounding-box by scaleRatioW and the y coord and height by scaleRatioH. resize is used to tell the render function to call the resize function. It is neccessary to do it this way because there is a stall in the program after resizing the window that can sometimes cause problems if you call the resize functions immediately upon receiving a resize event.

So I have set up a fairly simple main function to run the program.

int main(int argc, char * argv[])
{
    InitValues();
    InitSDL();

    bool quit = false;

    while(!quit)
    {
        quit = HandleEvents();
        Render();
    }

    return 0;
}

InitValues does what you might expect it to do, gives all the variables their starting values. InitSDL does most of the importnat work, setting up the window and renderer as well as set up the back buffer and load the ball image. HandleEvents returns false if the user clicks the x button on the window, however it also captures the window resize event and resizes the screen using a function called Resize. Render handles a very important part of the process, changing from the back buffer to the window buffer. I'll explain each of the functions in turn.

void InitValues()
{ 
    nativeSize.x = 0;
    nativeSize.y = 0;
    nativeSize.w = 256;
    nativeSize.h = 224;

    newWindowSize.x = 0;
    newWindowSize.y = 0;
    newWindowSize.w = nativeSize.w;
    newWindowSize.h = nativeSize.h;
    
    scaleRatioW = 1.0f;
    scaleRatioH = 1.0f;

    window = NULL;
    renderer = NULL;
    backBuffer = NULL;
    ballImage = NULL;
}

I set the native size to that of a GameBoy, 256 by 224.

void InitSDL()
{
    if(SDL_Init(SDL_INIT_EVERYTHING) < 0)
    {
        cout << "Failed to initialize SDL" << endl;
    }

    //Set the scaling quality to nearest-pixel
    if(SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0") < 0)
    {
        cout << "Failed to set Render Scale Quality" << endl;
    }

    //Window needs to be resizable
    window = SDL_CreateWindow("Rescaling Windows!",
                              SDL_WINDOWPOS_CENTERED,
                              SDL_WINDOWPOS_CENTERED,
                              nativeSize.w,
                              nativeSize.h,
                              SDL_WINDOW_RESIZABLE);

    //You must use the SDL_RENDERER_TARGETTEXTURE flag in order to target the backbuffer
    renderer = SDL_CreateRenderer(window,
                                  -1,
                                  SDL_RENDERER_ACCELERATED |
                                  SDL_RENDERER_TARGETTEXTURE);

    //Set to blue so it's noticeable if it doesn't do right.
    SDL_SetRenderDrawColor(renderer, 0, 0, 200, 255);

    //Similarly, you must use SDL_TEXTUREACCESS_TARGET when you create the texture
    backBuffer = SDL_CreateTexture(renderer,
                                   SDL_GetWindowPixelFormat(window),
                                   SDL_TEXTUREACCESS_TARGET,
                                   nativeSize.w,
                                   nativeSize.h);

    //IMPORTANT Set the back buffer as the target
    SDL_SetRenderTarget(renderer, backBuffer);

    //Load an image yay
    SDL_Surface * image = SDL_LoadBMP("Ball.bmp");

    ballImage = SDL_CreateTextureFromSurface(renderer, image);

    SDL_FreeSurface(image);
}

This is a fairly large chunk of code. First SDL_Init is called to set up all the SDL subsystems. Next, something that one might find important, SDL_SetHint is called to set the render scale quality to nearest-pixel. Linear quality makes scaling very small images to very large images hazy, and this is unwanted behavior.

Next the window is created with the SDL_WINDOW_RESIZABLE flag to allow it to be resized. Then, very important, the renderer is created with the SDL_RENDERER_TARGETTEXTURE flag which allows a texture to be targeted, and backBuffer must be created using the SDL_TEXTUREACCESS_TARGET flag for it to be used as a back buffer. The render target is then set to backBuffer so that all drawing will happen on it, not on the window.

bool HandleEvents()
{
    while(SDL_PollEvent(&event) )
    {
        if(event.type == SDL_QUIT)
        {
            return true;
        }
        else if(event.type == SDL_WINDOWEVENT)
        {
            if(event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
            {
                resize = true;     
            }
        }
    }
    
    return false;
}

The HandleEvents function for clarification. Resize is not called all the time, just when the window's size changes.

void Resize()
{
    int w, h;

    SDL_GetWindowSize(window, &w, &h);

    scaleRatioW = w / nativeSize.w;
    scaleRatioH = h / nativeSize.h;  //The ratio from the native size to the new size

    newWindowSize.w = w;
    newWindowSize.h = h;

    //In order to do a resize, you must destroy the back buffer. Try without it, it doesn't work
    SDL_DestroyTexture(backBuffer);
    backBuffer = SDL_CreateTexture(renderer,
                                   SDL_GetWindowPixelFormat(window), 
                                   SDL_TEXTUREACCESS_TARGET, //Again, must be created using this
                                   nativeSize.w, 
                                   nativeSize.h);

    SDL_Rect viewPort;
    SDL_RenderGetViewport(renderer, &viewPort);

    if(viewPort.w != newWindowSize.w || viewPort.h != newWindowSize.h)
    {
        //VERY IMPORTANT - Change the viewport over to the new size. It doesn't do this for you.
        SDL_RenderSetViewport(renderer, &newWindowSize);
    }
}

So here is the most important part of scaling and where it became tricky. There are quite a few things that happen here. First, get the new window size (using GetWindowSize for clarity). Then calculate the new ratio based on the new size and the native size. Then, Very Important you must destory and recreate your back buffer. I honestly don't know why, but if you try without doing this it will not work. Then, also Very Important, you must set the view port to the new window size because it will not do it for you, or do it right anyways.

void Render()
{
    SDL_RenderCopy(renderer, ballImage, NULL, NULL); //Render the entire ballImage to the backBuffer at (0, 0)

    SDL_SetRenderTarget(renderer, NULL); //Set the target back to the window
    
    	if(resize) //If a resize is neccessary, do so.
        {
            Resize();
        }

        SDL_RenderCopy(renderer, backBuffer, &nativeSize, &newWindowSize); //Render the backBuffer onto the
                                                                           //screen at (0,0)    
        SDL_RenderPresent(renderer);
        SDL_RenderClear(renderer); //Clear the window buffer

    SDL_SetRenderTarget(renderer, backBuffer); //Set the target back to the back buffer
    SDL_RenderClear(renderer); //Clear the back buffer
}

And last but not least the Render function. The comments explain this one pretty well.

Note:  For some reason, whenever you first resize the window, there is about a three-second delay and after that it resizes without a hitch. Not sure why on that one.


Conclusion


SDL2 is a pretty straight-forward API for doing just about anything. However, getting around letter-boxing was slightly tricky. Some of the things that occur in the background of SDL aren't obvious which led to a sort of trial and error process to figure out the way around. I hope this helps if anyone has the same problem with letter-boxing that I did.

Here is the source. You can run it from inside the bin folder: Attached File  SDL2StretchToWindow.zip   965.39KB   15 downloads

Viewing all articles
Browse latest Browse all 17825

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>