Wednesday, September 5, 2012

Lesson 7: Taking Advantage of Classes

Deprecation Notice:

I've been updating the lessons now that SDL2 is officially released, please visit my new site on Github that has updated lessons and improved code.


If you recall from the previous lessons we've used a global SDL_Window and SDL_Renderer so that we could access them from within our various functions throughout main.cpp, however using non-constant globals is a very bad idea, and when we start wanting to write more complicated programs this method will become unusable.

In this lesson we will look to object oriented programming to resolve these issues and put together a useful class to represent our Window that we can use for loading and drawing images and text. In addition to this we will discover SDL's more powerful rendering function, SDL_RenderCopyEx, which will allow us to specify rotations about points and flips to apply to the texture when it's drawn.

For this lesson you'll need to have at least some knowledge of C++ classes along with the C++11 std::unique_ptr, as we will be using both throughout the lesson.

Before we jump into coding we'll need to have a plan of how our class looks and how we want it to be used. For simplicity I've chosen to make all members of the class static, which enables us to call the functions on our window from anywhere in the program that includes its header file. It would be undesirable to do this if you plan to use SDL 2.0's new ability to support multiple windows, but for now this method will be acceptable.

Now we can move on to planning out individual functions for our class. Obviously we'll want to bring in our ApplySurface, LoadImage and RenderText functions from the previous lessons. In addition to this we will need some sort of initialization and quitting functions to create and close the window and start/exit SDL and SDL_ttf. We'll also need to be able to tell the window when to clear and present, and it would be useful to get the window's width and height, so we'll make a function for that as well.

So here's what we've got planned out for the Window class:

Where I've included the appropriate header guards and renamed ApplySurface to Draw and omitted most of its parameters as they'll be a good bit longer now that we also need to pass a rotation angle, point of rotation and flip to SDL_RenderCopyEx.

This looks pretty good for a simple Window class, so lets start turning it into real code.

The first thing I want to cover is how we'll be handling our SDL_Window and SDL_Renderer. Instead of using a raw pointer and calling SDL_DestroyX in the Quit function, we'll be using a new C++11 feature known as the std::unique_ptr. This pointer is included in the <memory> header and allows for use of lifetime managed objects. In the case of the unique_ptr it allows for only one pointer to use the object at a time and once that pointer goes out of scope it will call the destructor function chosen for the pointer, freeing the memory automatically.

It's easy to see how this unique_ptr can be useful, we don't need to worry about managing the objects memory and we don't need to worry about it not being freed in the case that an error arises. Suppose in our program we were to hit some runtime error and crash out of a function or the whole program without calling the necessary memory releasing functions. The memory would never be freed! However with a unique_ptr it would go out of scope, calling either the default destructor of the object, or a specified function passed to it to clean up the memory. Very handy indeed!

This is not to say that the new unique_ptr, shared_ptr and weak_ptr are a magical band-aid to resolve any memory issues in C++11 as they most certainly are not. Raw pointers still have their place and misuse of the new types will still result in memory issues. It's important to use the appropriate type for the application. In this case the unique_ptr is an excellent choice for our use.

Since we want to use the SDL_DestroyX function instead of the object's destructor to free the managed object, we need to specify the function signature of  the desired destruction function to our unique_ptr, which in this case is the signature for SDL_DestroyX, which is void(*)(SDL_X*). So mWindow and mRenderer will end up being:

Now that we know how mWindow and mRenderer will be implemented we can move on to writing our functions, and declaring the static member variables in window.cpp.

First, the declaration of our variables: mWindow, mRenderer and mBox.

It looks a bit messy, but it really isn't so bad. We simply set our unique_ptrs to be pointing to the appropriate destructor function and set the data to nullptr.

As mentioned earlier, our constructor and destructor do nothing, so we'll skip covering writing those, as there's nothing to write and jump into putting together our Init function.

For Init we just want to start up SDL and TTF and create our SDL_Window and SDL_Renderer, so we just want to take lines 75-97 of Lesson6 main.cpp and put them into their own function, and allow for a string to be passed that will be set as the window title. Simple enough! So our function definition will look like:

Where I have used doxygen style comments, to enable usage of doxygen to generate easy to understand documentation for the code. Our implementation of the function is simply lines 75-97 of the previous lesson:

This function should look very familiar, the only difference being that we use the unique_ptr reset function to change the data that it points to, instead of the previous methods used. In this case we simply change the nullptr they're currently managing to the created SDL_Window or SDL_Renderer that they should be managing.

The Quit function is very simple, we just take the SDL_Quit and TTF_Quit calls and put them into the function.

The most changed function is our former ApplySurface function (now named Draw), which we'll need to rework to enable us to pass the extra parameters needed by SDL_RenderCopyEx. If we take a look at the documentation for the function we can see that in addition to a texture pointer, destination rect and clip rect we need an angle (in degrees), a pivot point and a flip value.

So now we know what we need, and can write the function, however I've chosen to pass the pivot point values as separate ints instead of an SDL_Point, in the future we'll create a 2d vector class and use that instead, which will also enable us to perform more advanced vector math. So our function will look like so:

Be sure to note the default parameters we've chosen, if we pass just a texture and destination, we'll get an upright drawing of the texture within the destination rectangle, with the rest of the parameters defaulting to no change.

Our function is quite easy to write as it's simply serving as a wrapper around SDL_RenderCopyEx, with one extra thing. For simplicity we want the pivot point we pass to be relative to the center of the dstRect, while SDL will consider the point relative to the texture's X and Y coordinates, so we must add an offset to center it. Alternatively, if NULL is passed as the SDL_Point* parameter it will set the rotation about the destination rect's center.

There's one other new thing tucked away in the call to SDL_RenderCopyEx, which is that in order to access the raw SDL_Renderer pointer that the function expects we use mRenderer.get() to get it from the unique_ptr.

Next we define our LoadImage and RenderText functions:

I leave writing the function to you as it's the same as the definitions in Lesson 7 with the small change to passing mRenderer.get() in place of renderer.

Finally we define our Clear, Present and Box functions. Clear simply calls SDL_RenderClear on mRenderer and Present does the same for SDL_RenderPresent. Box lets us return an SDL_Rect containing the window's width and height, which we can query using SDL_GetWindowSize. Our functions will look like:

If you have issues working out the class, my implementation can be found at the Github repo, window.h and window.cpp. Now that we've got our Window class written, it's time to try it out in an application and make sure it works.

In main.cpp we now will also need to include our window header, "window.h" in order to use the class. Instead of calling SDL_Init and etc. as done previously we can now call Window::Init and catch any errors it may throw to create our window and make sure it went ok.

We can then load images and render text using the LoadImage and RenderText functions from our class.

Here we also perform error catching to respond appropriately to any errors that may be thrown by the functions. Now we can make use of our Box function to find the center of the window and set a position rect to draw our image and text too.

In addition we create a variable called angle so that we can test out drawing things with some rotation. To change the variable we add some keydown checks to increase or decrease the value of angle.

Finally we can use the new Window functions to Clear, Draw and Present our screen with a simple call:

And before exiting the program, we simply call Destroy on the textures and Window::Quit to exit SDL and TTF.

Lesson 7 Extra Challenge!

Make it possible to set the window width and height when calling Init
Hint

End of Lesson 7

Thanks for joining me! See you again soon in Lesson 8: Timers.