原文地址:http://twinklebeardev.blogspot.com/2012/09/lesson-7-taking-advantage-of-classes.html
回头看看之前的课程,你会发现我们使用了SDL_Window和SDL_Renderer的全局变量,以便于在main.cpp的每一个函数处都能访问到它们。但是,其实使用非const的全局变量是个很不好的做法,当我们想写一些稍微复杂点的程序的时候,这个方法就不行了。
本节我们将看看使用面向对象的编程方法来解决这些问题,并且拼凑出一个可以代表Window的类,我们可以用它来加载和绘制图片还有文本。此外,我们还会用到SDL的更强大的绘制函数,SDL_RenderCopyEx, 这个函数允许我们指定在绘制时应用到texture上的对点的旋转还有翻转。
本节你需要至少知道C++中的类以及C++11中的 std::unique_ptr,这两者在本节中都要用到。
在我们开始编码之前,我们需要计划一下我们的类应该是怎样的,它应该怎样用。为简单起见,我选择将这个类中所有的成员都设为了static,这样只要包含了这个头文件,我们可以在程序的任意一处调用这些函数。但如果你想使用SDL2.0的多窗口功能的话,这种方法就不恰当了。但是在这里我们只有一个窗口,所以这个方法是可以的。
现在我们可以继续计划我们的类中的函数了。很显然我们应该把我们之前写的的ApplySurface, LoadImage以及RenderText放进去。此外,我们还需要一些初始化和退出的函数,以创建/关闭窗口,开启/退出SDL和SDL_ttf.我们还需要告诉窗口什么时候清屏什么时候呈现,而且还想要获得窗口的宽度和高度。所以我们为clear和present也分别写一个函数。
所以下面就是我们计划中的Window类的样子:
这里我还加了header guard,把ApplySurface改名为Draw并省略了它的大部分参数——因为这里我们还需要传入旋转角度,旋转的点以及翻转的参数给SDL_RenderCopyEx,所以它的参数列表可能会有点长。
看起来这对一个简单的window类来说已经挺好的了,于是现在我们来实现它吧。
我想要谈到的第一件事是,怎样处理SDL_Window和SDL_Renderer。我们使用C++11中的新特性:std::unique_ptr,来替代使用一个原始的指针(raw pointer),并在Quit函数中使用SDL_DestoyX释放它的方法. 这个指针包含在头文件里面,它允许我们管理对象的生命周期。unique_ptr在同一时间只允许一个指针使用这个对象,一旦这个指针脱离了当前的作用域,它会调用你所指定的析构函数(译注:这里并不一定是类的destructor),自动释放掉内存空间。
unique_ptr的用处显而易见。我们不必担心怎样管理对象的内存,也不必担心对象没有释放所造成的错误了。试想一下吧,当我们命中了某些运行时错误的时候,我们从一个函数里崩溃出来,或者直接整个程序崩溃掉了,我们就没有办法调用必要的内存释放函数了。在这种情况下,这些内存就再也不能用了。但是使用了unique_ptr之后,当它离开作用域时,它会自动调用对象的默认析构函数或者你指定的释放内存的函数。这实在是太方便了!
这也并不是说unique_ptr, shared_ptr以及weak_ptr就是能够治疗C++11中所有的内存问题的万用药,因为其实它们大部分时候都不能。raw pointer仍然有着它们的用武之地,滥用这些新的指针也会导致内存问题。对应用程序来说,选择合适的类型很重要。而在这里, unique_ptr对我们来说就是一个不错的选择。
因为我们想要使用SDL_Destroy函数而不是对象的析构函数来释放对象,我们需要指定想要的销毁函数的函数原型,在这里就是SDL_DestroyX的函数原型,也就是void()(SDL_X)。 所以mWindow和mRender最终会被这样释放。
既然我们知道了怎样实现mWindow和mRender,我们可以继续编写函数了,在window.cpp中定义(原文为declare,疑有误)静态变量。
首先,定义三个变量:mWindow, mRenderer还有mBox.
看起来有点乱,不过这其实还不坏。我只是让unique_ptr指向了对应的的销毁函数并把数据置为nullptr.
之前也提到了,我们的构造函数和析构函数事实上啥也不干,所以我们会跳过它们的编写,直接开始编写Init函数。
在Init函数里,我们只想打开SDL和TTF并且创建我们的SDL_Window还有SDL_Renderer,所以我们只需要把第六节main.cpp中的75-97行代码拿来放在这里,并接受传入的一个字符串,把它设为窗口标题。够简单了吧~所以我们的函数定义是这样的:
当我使用doxygen风格的注释的时候,是为了让doxygen为代码生成易于理解的文档。我们的函数实现就是上一节中的75-97行。
这个函数很眼熟,和上一节唯一的不同是我们使用unique_ptr的reset函数来改变它所指向的内存区域。这里我们仅仅把它们原来管理的nullptr改为了SDL_Window和SDL_Renderer.
quit函数实现起来非常简单,我们只需要把SDL_Quit和TTF_Quit放进去。
改变最大的函数是我们之前的ApplySurface函数(现在被改名为Draw函数了),它需要我们把参数列表改为可以传入额外的SDL_RenderCopyEX的参数。如果看看这个函数的文档,我们可以看到除了texture指针、目标矩形、裁剪矩形之外,还需要提供一个角(以角度为单位),一个旋转轴点和一个翻转值。
所以现在我们知道我们需要什么了,我们可以编写函数了。这里我选择以单独的int值传入旋转中心而不是以SDL_Point结构体传入。以后我们会创建一个2D向量类来代替SDL_Point,这个类会提供更高级的向量运算的功能。总之我们的函数应该是这样的:
注意一下我们所设置的默认参数。如果我们只传入一个texture和一个目标矩形,我们会看到texture使用目标矩形在屏幕右上角绘制的结果。
因为这个函数事实上就是个对SDL_RenderCopyEx的封装,再加上一个额外的东西。为了简单起见我们想要传入的旋转中心点是相对于目标矩形的中心的,但是SDL会把这当作相对于texture的x和y坐标的,所以我们必须增加一个偏移来把它置为中心。另一方面,如果给SDL_Point*参数传入NULL,它会把旋转中心设为目标矩形的中心。
此外,这里还有个隐藏的新知识点,在SDL_RenderCopyEx中,我们使用了mRenderer.get()以从unique_ptr获得SDL_Renderer指针。
下一步我们定义我们的LoadImage和RenderText函数:
我把编写这个函数的任务交给你,它和第七节中的定义相同,除了在传递renderer的指针的时候,写的是mRenderer.get()。
最后我们定义我们的Clear, Present和Box函数。Clear只需用mRenderer调用SDL_RenderClear, Present只需调用SDL_RenderPresent。Box返回一个SDL_Rect,其中包含了窗口的宽度和高度,这可以通过SDL_GetWindowSize获得。我们的函数看起来应该是这样的:
如果你对这个类有问题,我的类实现可以在Github repo上找到,分别是window.h和window.cpp.现在我们写好了Window类,是时候在程序里试试它,看看它究竟能不能正常工作了。
在main.cpp中我们想要使用window类,需要包含它的头文件, “window.h”。作为之前调用SDL_Init等物的方法的替代,我们现在可以调用Window::Init来创建窗口,并catch它可能会抛出的异常以确认它已经正确执行。
然后我们可以使用LoadImage和RenderText加载图像、绘制文本了。
这里我们还添加了异常捕获的代码,以捕获函数所有可能抛出的错误。
现在我们可以利用Box函数来获得窗口的中心点,然后设置一个目标矩形来绘制图像和文本。
此外,我们还创建了一个angle变量,以便于测试一些使用旋转的绘制。我添加了一些按键检查来增加或减少这个变量。
最后我们可以使用新的Window的函数来简单地Clear, Draw 和 Present了。
在退出程序之前,我们只需在texture上调用Destroy函数,然后调用Window::Quit以退出SDL和TTF。
第七节的Extrra Challenge
实现在调用Init函数的时候设置窗口的大小。
【提示】
超级简单的~只需要把窗口的宽高传给Init就够啦。