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
Ok, I hit the unique_ptr part and I just want to make sure I got right what was written up there.
ReplyDeleteSo, whenever those 2 pointers go out of scope they are destroyed not by the unique_ptr default destructor but by the SDL_DestroyWindow and SDL_DestroyRenderer?
The syntax is a bit intimidating, and I still don't get WHEN these pointers go out of scope, I take it's often? I read somewhere one of the cool things is that these kinds of pointers have only self-ownership so nobody (no other classes and stuff) can mess with it.
Lemme continue my reading now... heh
The unique ptr will use whatever the default deleter is for the type, so if it were a class it'd use the destructor, or for a primitive it'd just call delete. However because SDL needs to do some extra stuff to clean up the objects it provides the SDL_DestroyX set of functions for properly freeing objects. These wouldn't be recognized as the default deleters by unique ptr since SDL is a C library and C has no destructors, so we need to tell the unique ptr specifically to use those functions to clean up the objects.
DeleteAs for when they go out of scope, static members go out of scope after main exits, this calls the unique ptr destructor which will then free the managed object by calling the deleter function (SDL_DestroyX) on the object.
Correct on the ownership, only one unique ptr may point to the object at once. There's also the new shared ptr which can have multiple ptrs to one object and will free it when the last on goes out of scope. Although now that I'm revisiting this I think that the renderer and window should be freed when calling Window::Quit() before exiting SDL. I'll have to update this heh.
If you want to do some more reading on the unique and shared ptrs, check out http://en.cppreference.com/w/cpp/memory/unique_ptr
and
http://en.cppreference.com/w/cpp/memory/shared_ptr
They can be really handy.
Why would you free them in Window::Quit? Is it because they have to be freed before SDL_Quit()?
DeleteThanks for those links, I'll check them after reading this tutorial. Actually there is something odd going on, the image is drawn in the center and it's shrinked, and so is the rendered TTF message, is this supposed to happen?
This comment has been removed by the author.
DeleteTinypic.com sucks; so here's a screenshot showing the app: http://i.imgur.com/bCWhuei.png
DeleteYea, I think for them to be properly cleaned up they should be freed before calling SDL_Quit.
DeleteYea that's the correct look for this project, since we draw to a 150x150 size box, so the image and text will be all scrunched up. The drawing in this tutorial was to show the capability for image scaling, rotation and flipping and how to use them.
Ahhh... I get it now, you centered the 150x150 and 'image.png' shrinked into it because of its size being smaller than the texture's (is it possible to prevent shrinking/enlarging by rendering a 150x150 upper-left part of the texture?); and the TTF message is upside down because of the flip?
DeleteHopefully I got it right! I'm going easy on SDL this time because I had frustrating times 2 years ago, and raged a lot, now I'm taking the slow route till I'm able to make the so desired 2D RPG. :P
Yea, the image is shrunk because the destination rect is smaller than the image rect, similarly if the destination rect is bigger the image will be stretched. If you want to only draw a sub-section of an image you can take clips of an image to be drawn, it's gone over in lesson 5. And yep the text is flipped because we specify SDL_FLIP_VERTICAL when drawing it.
DeleteGood read: http://ootips.org/yonat/4dev/smart-pointers.html
ReplyDeleteLittle advice for new C++ programmers on Linux (without using any IDE):
ReplyDeleteIf you are using terminal for compiling (g++/clang/whatever), to compile this code properly you need to add every *.cpp file to the command. For example instead of using:
clang++ -Wall -g main.cpp -o program.o -std=c++11 -lSDL2 -lSDL2_image -lSDL2_ttf
You should use:
clang++ -Wall -g main.cpp window.cpp -o program.o -std=c++11 -lSDL2 -lSDL2_image -lSDL2_ttf
In the future, if you will have many *.cpp files in yours project it could be annoying to add every single file to the command, so I recommend you to learn how to use makefile and make command.
Or you can just start using some IDE ;)
I'm somewhat of a noob so wouldn't know how to do this but wouldn't typedefs and templates help clean the syntax up?
ReplyDeleteDid a quick search on C++14 and SDL2 and came across this article which seems to fit in with this discussion.
ReplyDeletehttp://ericscottbarr.com/blog/2014/04/c-plus-plus-14-and-sdl2-managing-resources/
Although it uses C++14 features (I use Visual Studio 2013 so these features are not available), the author does follow up with a workaround using C++11 features. I did get his example compiling and working for the SDL_Window case in my app class.
I'm not experienced enough to fully understand his discussion though and get lost in the arcane trickery going on in the make_resource template and specializations. When I tried to apply this to take care of the SDL_Renderer pointer I hit template parameter matching issues. Maybe someone on here more experienced can clarify?
I also have a question about the reason to use std::unique_ptr to manage scope of SDL resources in this context. Given that the resources in question are static members of this Window class, wouldn't it be sufficient to simply put the corresponding SDL_DestroyX functions in the Window class destructor to achieve the same RAII goal that unique_ptr provides? I suspect I'm missing something :)
ReplyDeleteThe post you linked using unique_ptr is the right way to manage the SDL resources. This post is kind of old and the code isn't really that good, I've been working on a re-write over here: http://www.willusher.io/pages/sdl2/ but haven't gotten to this lesson yet unfortunately.
DeleteSince no Window instance is every really instantiated the destructor wouldn't be called, since we just use it as this terrible singleton. Again the code in this lesson is pretty bad.
Since SDL2 lets you have multiple windows you'd actually want a Window class with non-static members that manages their lifetime with unique_ptrs.
Thanks. Look forward to the re-write.
DeleteI have everything working using std::unique taking care of the window and renderer pointers but I have to say I don't quite uinderstand screen drawing. Right now I'm using the following in the render method:
ReplyDeleteSDL_SetRenderDrawColor(m_renderer.get(), 0, 0, 255, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(m_renderer.get(), NULL);
SDL_RenderPresent(m_renderer.get());
When running and resizing the screen, the screen only clears to blue when I release the mouse. I suspect this is a function of the event loop blocking. Is there an established way for the window to be a little more 'real-time' during resizing?
It's a windows thing, when resizing the window it doesn't repaint it iirc. I could be wrong about this though (or if there's a solution I'm unaware of). I'd recommend heading to the SDL IRC channel on freenode and asking around.
DeleteInteresting. I'm checking out SDL alongside my attempts to learn Direct2D under Win32 and I don't see this 'render only after releasing mouse button when resizing' when handling basic win32 WM_RESIZE and WM_PAINT messages. I suspect this is because I've basically told Windows (when filling out WNDCLASS) to not bother using a default brush to clear the window during WM_PAINT messages and instead leave invalidating and clearing the window to me and my calls to the Direct2D API.
DeleteI'm guessing considering SDL shields me from many platform-specific details such as Window rendering means I'll not easily find it possible tailor how it handles window painting messages from the system?I do have Direct2D integrated into my SDL class and seemingly working fine and rendering to the window sop far though.
You are able to get access to the underlying platform window handle through SDL, see: https://wiki.libsdl.org/SDL_GetWindowWMInfo?highlight=%28\bCategoryVideo\b%29|%28CategoryEnum%29|%28CategoryStruct%29
DeleteMaybe with that you can set it up to behave that way? I'm not very familiar with this area :P
I have the HWND handle via the code below. Nice easy access. I'm hoping there's similar easy exposure of other DirectX data structures.
Delete// Retrieve HWND for Direct2D function calls.
SDL_SysWMinfo sysinfo = {}; // Holds Window information
SDL_GetWindowWMInfo(m_window.get(), &sysinfo);
SDL_VERSION(&sysinfo.version); // SDL version
m_windowHandle = sysinfo.info.win.window; //Gets the HWND
Direct2D requires the handle for factory and render target creation. I suspect I need better control over the actual WNDCLASS structure settings. Later version of Direct2D provide even more powerful features if you go with a Direct2D context arrangement with the Direct3D swap chain rather than hooking up with the window HWND. I feel I'll be foregoing SDLs video wrapper facilities and instead using it for non-video event handling and its other services.
On a unrelated matter. Is there some way to stop the annoying behavior of Blogger sending email notifications letting me know that I've commented? I'd rather just be notified when someone else comments :)
ReplyDeleteI don't think so, it's part of why I moved away from blogspot :P. That and the poor formatting and code styling
Delete