SDL遊戲開發教程04(C++封裝SDL)

    前面的章節介紹了一個簡單窗口的開發,這節將介紹如何把前面用到的東西用C++封裝起來。

 

    爲什麼用C++封裝起來:

1、C語言沒有異常機制,每次調用一個函數都需要通過檢查返回值來判斷是否成功,比較麻煩。

2、對我個人而言,開發效率上C++要優於C語言,並且C++的代碼要容易組織管理,寫出來的代碼也更容易理解。

 

    封裝的主要部分:

1、將所有的SDL函數都用類包裝起來,對於需要做返回值判斷的函數,在包裝的地方進行判斷,然後決定是否拋出異常。這樣對於調用者來說就不需要再做返回值判斷了。

2、新建一個SDL類。該類用到了單例模式和工廠模式, 提供了訪問所有SDL函數的入口。

3、將創建窗口和消息循環這部分代碼封裝成一個框架類,以後寫代碼時只要繼承這個框架就可以了。

4、包裝SDL_Surface結構體,使它可以像普通對象一樣使用。因爲通過SDL API獲取一個SDL_Surface後需要要手動釋放,否則會造成內存泄漏。

 

    封裝之後的main函數:

#include <string>
#include "lessons/Lesson01.h"
int main( int argc, char* args[] )
{
	Lesson01 frame;

	frame.setSize(800, 600);		//設置窗口大小
	frame.setTitle("Lesson01");		//設置標題

	frame.open();					//打開窗口並開始循環

	return 0;
}

     封裝成這樣子之後,我們可以將不同課程中的例子代碼寫在不同的類中,完全隔離開,到時候想運行哪一課的例子修改一下main中的Lesson01就可以了。

 

    下面根據這份代碼逐步介紹封裝過程。上面main函數中用到了Lesson01類,下面先看看Lesson01裏面有些什麼東西。

    Lesson01.h

#ifndef LESSON01_H_
#define LESSON01_H_
#include "../SDLFrame.h"
class Lesson01 : public SDLFrame
{
public:
	Lesson01();
	virtual ~Lesson01();
protected:
	void onRender();	//渲染窗口
	void onInit();		//初始化
public:
	SDLSurfacePtr message;	//界面要顯示的圖片
};

#endif /* LESSON01_H_ */

    Lesson01.cpp

#include "Lesson01.h"

Lesson01::Lesson01()
{
	// TODO Auto-generated constructor stub

}

Lesson01::~Lesson01()
{
	// TODO Auto-generated destructor stub
}

void Lesson01::onRender()
{
	//將圖片填充到screen
	SDL::video()->BlitSurface(message, NULL, screen, NULL);
}
void Lesson01::onInit()
{
	//加載圖片
	SDLSurfacePtr loadedImage = SDL::video()->LoadBMP("E:\\code_picture\\javaeye.bmp");
	
	//將圖片轉換成適合程序的格式
	message = SDL::video()->DisplayFormat(loadedImage);
}
 

    這裏可以看出Lesson01是繼承自SDLFrame,而它本身只有兩個函數,OnInit負責一些初始化工作,OnRender負責將要顯示的內容填充到screen中去。

    Lesson01.cpp中用到了SDL::video(),這就是前面提到的SDL類,該類提供了所有SDL函數的入口,這裏的SDL::video()->BlitSurface等於SDL_BlitSurface,只是包裝了一下而以。

 

    下面看SDL類

    頭文件

#ifndef SDLCORE_H_
#define SDLCORE_H_
#include "SDLException.h"
#include "SDLVideo.h"
#include "SDLWindow.h"
#include "SDLEvent.h"

class SDL
{
public:
	SDL();
	virtual ~SDL();
public:
	static void Init(Uint32 flags);		//初始化SDL環境,見SDL.h中以SDL_INIT_開頭的宏定義
	static void Quit();					//退出SDL環境
public:
	static SDLVideo * video();			//SDLVideo封裝了video相關的函數
	static SDLWindow * window();		//SDLWindow封裝了窗口相關的函數
	static SDLEvent * event();			//SDLEvent封裝了event相關的函數
};

#endif /* SDLCORE_H_ */

   CPP文件

#include "SDLCore.h"

SDL::SDL()
{
	// TODO Auto-generated constructor stub

}

SDL::~SDL()
{
	// TODO Auto-generated destructor stub
}

void SDL::Init(Uint32 flags)
{
	int ret =  SDL_Init(flags);
	if(ret == -1)
	{
		throw SDLException(std::string("初始化SDL錯誤:") + SDL_GetError());
	}
}
void SDL::Quit()
{
	SDL_Quit();
}

SDLVideo * SDL::video()
{
	static SDLVideo video;
	return &video;
}

SDLWindow * SDL::window()
{
	static SDLWindow window;
	return &window;
}

SDLEvent * SDL::event()
{
	static SDLEvent event;
	return &event;
}

   從上面的代碼可以看出,SDL類只負責初始化和退出SDL環境,同時創建SDL相關的封裝類對象,這裏用到了C++靜態成員變量的特性:全局生命週期且只被初始化一次。從而保證SDLVideo、SDLWindow、SDLEvent的對象全局唯一。

   SDLException是程序定義的一個異常類,由於很普通,所以在這裏不再進行解釋。

 

   SDLVideo、SDLWindow、SDLEvent都是SDL API函數的封裝類,原理幾乎是一樣的,這裏取其中的一個進行分析。

SDLVideo.h

#ifndef SDLVIDEO_H_
#define SDLVIDEO_H_
#include "SDLException.h"
#include "SDL/SDL.h"
#include "SDLSurface.h"

class SDLVideo
{
	friend class SDL;


private:
	SDLVideo();
public:
	virtual ~SDLVideo();
public:
	/**
	 * 設置窗口模式
	 * width	寬
	 * height	高
	 * bpp		顏色位數
	 * flags	SDL.h中以SDL_INIT_開頭的宏定義
	 * return	窗口對應的內存塊
	 */
	SDLSurfacePtr SetVideoMode(int width, int height, int bpp, Uint32 flags);

	/*
	 * 將內存中的內容顯示到屏幕上
	 * screen	內存塊
	 */
	void Flip(SDLSurfacePtr screen);

	/**
	 * 將圖片轉換成程序需要的格式(源圖片和轉換後的圖片在不同的內存中)
	 * surface	源圖片
	 * return	轉換後的圖片
	 */
	SDLSurfacePtr DisplayFormat(SDLSurfacePtr surface);

	/*
	 * 將硬盤上的圖片加載到內存中(只支持BMP格式)
	 * file		圖片文件路徑
	 * return	加載後內存中的圖片區域
	 */
	SDLSurfacePtr	LoadBMP(std::string file);

	/**
	 * 將源圖片覆蓋到目的圖片區域上
	 * src		源圖片
	 * srcrect	將要覆蓋過去的源圖片區域,NULL表示全部
	 * dst		目的圖片
	 * dstrect	源圖片要覆蓋到目的圖片的哪個地方,NULL表示左上角
	 */
	void BlitSurface(SDLSurfacePtr src, SDL_Rect *srcrect, SDLSurfacePtr dst, SDL_Rect *dstrect);
};

#endif /* SDLVIDEO_H_ */

 SDLVideo.cpp

#include "SDLVideo.h"

SDLVideo::SDLVideo() {
	// TODO Auto-generated constructor stub

}

SDLVideo::~SDLVideo() {
	// TODO Auto-generated destructor stub
}

SDLSurfacePtr SDLVideo::SetVideoMode(int width, int height, int bpp, Uint32 flags)
{
	SDL_Surface * surface = SDL_SetVideoMode(width, height, bpp, flags);
	if(NULL == surface)
	{
		throw SDLException(std::string("SDL_SetVideoMode初始化視頻模式時發生錯誤:") + SDL_GetError());
	}

	return SDLSurfacePtr(new SDLSurface(surface));
}

void SDLVideo::Flip(SDLSurfacePtr screen)
{
	int ret = SDL_Flip(screen->value());
	if(ret == -1)
	{
		throw SDLException(std::string("SDL_Flip內存內容顯示到屏幕時發生錯誤:") + SDL_GetError());
	}
}

SDLSurfacePtr SDLVideo::DisplayFormat(SDLSurfacePtr surface)
{
	SDL_Surface *newSurface = SDL_DisplayFormat(surface->value());
	if(NULL == newSurface)
	{
		throw SDLException(std::string("SDL_DisplayFormat轉換圖片格式爲程序格式時發生錯誤:") + SDL_GetError());
	}

	return SDLSurfacePtr(new SDLSurface(newSurface));
}

SDLSurfacePtr	SDLVideo::LoadBMP(std::string file)
{
	SDL_Surface *surface = SDL_LoadBMP(file.c_str());
	if(NULL == surface)
	{
		throw SDLException(std::string("SDL_LoadBMP加載BMP圖片時發生錯誤:") + SDL_GetError());
	}

	return SDLSurfacePtr(new SDLSurface(surface));
}

void SDLVideo::BlitSurface(SDLSurfacePtr src, SDL_Rect *srcrect, SDLSurfacePtr dst, SDL_Rect *dstrect)
{
	int ret = SDL_BlitSurface(src->value(), srcrect, dst->value(), dstrect);
	if(ret == -1)
	{
		throw SDLException(std::string("SDL_BlitSurface重疊圖片時發生錯誤:") + SDL_GetError());
	}

}

     從上面的代碼可以看出SDLVideo只是簡單的將SDL中video相關的函數做一下包裝,檢查SDL函數的返回值,如果有錯誤就拋出異常。在頭文件中,將SDL類聲明成友元類並且將構造函數設置爲private是爲了避免在除SDL類以外的地方實例化該類的對象。

    這裏用到了SDLSurfacePtr和SDLSurface。SDLSurfacePtr定義:

typedef boost::shared_ptr<SDLSurface> SDLSurfacePtr;

    構造SDLSurfacePtr的代碼爲:

SDLSurfacePtr(new SDLSurface(surface));

    可以看出,SDLSurfacePtr中有SDLSurface,SDLSurface中有SDL_Surface*。

    boost庫的shared_ptr是一種帶引用計數的智能指針,當shared_ptr對象的引用計數變成0的時候,會自動delete它裏面保存的對象,所以當最後一個SDLSurfacePtr對象析構的時候,SDLSurfacePtr會調用delete SDLSurface。關於shared_ptr的詳細介紹,可以通過GOOGLE搜到很多資料。

    SDLSurface的析構函數如下:

SDLSurface::~SDLSurface()
{
	if(surface != NULL)//surface是SDL_Surface *類型
	{
		SDL_FreeSurface(surface);
	}
}

    由於SDLSurface的析構函數中會調用SDL_Surface*的釋放操作。從而使得內存中的SDL_Surface*被自動釋放。這樣就省去了手動釋放SDL_Surface的麻煩。

 

    最後來看看SDLFrame類

頭文件:

#ifndef SDLFRAME_H_
#define SDLFRAME_H_

#include "SDL/SDLCore.h"
class SDLFrame
{
public:
	static const std::string DEFAULT_TITLE;			//默認窗口標題
	static const int DEFAULT_SCREEN_WIDTH = 800;	//默認窗口寬
	static const int DEFAULT_SCREEN_HEIGHT = 600;	//默認窗口高
public:
	SDLFrame();
	virtual ~SDLFrame();
public:
	/*
	 * 打開窗口
	 * flags	窗口模式,見SDL_video.h中的宏定義
	 */
	void 	open(Uint32 flags = SDL_HWSURFACE | SDL_DOUBLEBUF);

	void 	setTitle(std::string title);
	void 	setSize(int width, int heigth);
protected:
	/**
	 * 消息處理函數,當有用戶輸入的時候,框架會調用此函數
	 * event	待處理的消息
	 * return	如果爲false,則程序退出
	 */
	virtual	bool onEvent(const SDL_Event *event);

	/**
	 * 當需要繪製窗口時,框架會調用此函數
	 */
	virtual void onRender();

	/**
	 * 顯示窗口前,框架會調用此函數
	 */
	virtual void onInit();
protected:
	SDLSurfacePtr screen;
	std::string title;
	int	width;
	int height;
};

#endif /* SDLFRAME_H_ */

 源文件:

#include "SDLFrame.h"

const std::string SDLFrame::DEFAULT_TITLE  = "SDL Tutorial";

SDLFrame::SDLFrame()
{
	title = DEFAULT_TITLE;
	width = DEFAULT_SCREEN_WIDTH;
	height = DEFAULT_SCREEN_HEIGHT;
}

SDLFrame::~SDLFrame()
{
	// TODO Auto-generated destructor stub
}

void 	SDLFrame::open(Uint32 flags)
{
	//初始化SDL環境
	SDL::Init(SDL_INIT_EVERYTHING);

	//設置屏幕模式
	screen = SDL::video()->SetVideoMode(width, height, 32, flags);

	//設置窗口標題
	SDL::window()->SetCaption(title);

	//初始化
	onInit();

	//開始事務循環
	SDL_Event event;
	bool bQuit = false;
	while(!bQuit)
	{
		while( SDL::event()->PollEvent( &event ) )
		{
			if(!onEvent(&event))
			{
				bQuit = true;
			}
		}

		//繪製
		onRender();

		//將在內存中的處理結果顯示到屏幕上
		SDL::video()->Flip(screen);
	}

	//退出SDL環境
	SDL::Quit();
}
void 	SDLFrame::setTitle(std::string title)
{
	this->title = title;
}

void 	SDLFrame::setSize(int width, int heigth)
{
	this->width = width;
	this->height = heigth;
}
bool SDLFrame::onEvent(const SDL_Event *event)
{
	switch(event->type)
	{
	case SDL_KEYDOWN:
		if(event->key.keysym.sym == SDLK_ESCAPE)
		{
			return false;
		}
		break;
	case SDL_QUIT:
		return false;
		break;
	default:
		break;
	}

	return true;
}
void SDLFrame::onRender()
{

}
void SDLFrame::onInit()
{

}

     SDLFrame類封裝了消息循環,通過在循環中調用成員函數的方式將消息循環中公共的部分與特殊的部分分離開,從而可以在基類中重載這些成員函數使不同的基類表現出不通的特性。其中onEvent負責處理用戶輸入,onInit負責窗口創建後的初始化,onRender負責窗口的繪製。

     這裏onEvent只處理了窗口關閉和ESC鍵按下兩個消息,子類可以通過重載來覆蓋默認實現。onInit和onRender都是空實現。需要在子類中去實現具體的操作。

     結合消息循環,現在再回過頭去看Lesson01的代碼,就會發現只要程序一有空閒,就會調用onRender函數,而Lesson1的onRender函數中只有一行代碼:SDL::video()->BlitSurface(message, NULL, screen, NULL);,並且這行代碼中用到的message和screen永遠不會變,你可能會想老這樣調用同樣的代碼是不是很浪費資源,在這裏確實是浪費資源,其實只要將這行代碼放到onInit函數的末尾就可以了。這裏可以這樣做的原因是因爲程序初始化好了之後內存中的內容不再會發生變化,所以每次調用SDL::video()->Flip(screen)都不會改變屏幕顯示的內容。說的通俗點,就是這個程序太簡單了,用不着定時去更新窗口。後面的章節中將會看到定時更新窗口的用處。

     這節的內容就介紹到這裏,在以後的章節中,都將採用同Lesson01一樣的方式來編寫代碼。附件中是本節內容的完整源代碼。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章