SDL遊戲開發之六-簡單的SDL程序

1.最簡單的SDL程序

一般的遊戲在運行過程中的大部分操作都是在一個大循環裏,在這個循環裏進行着事件監聽、繪製以及邏輯處理等。而像網絡通信或者是文件讀取等這些比較耗時或者堵塞的操作一般會放到子線程裏面。流程圖如下:

圖1-遊戲流程圖
  1.  先創建窗口和渲染器;如果創建成功,則進入大循環裏;否則則直接退出;
  2. 進行邏輯處理、繪製、事件處理等(注意:以上三個是不分先後的);
  3. 如果發現有退出信號,則退出大循環,然後釋放內存;遊戲結束。否則則重複步驟2。

每一次循環都會進行繪製,所以一次循環也可以廣義地稱爲一幀。另外,由於人眼的視覺殘留,遊戲的幀數需要在一秒超過24幀纔會感覺到流暢,而一般的遊戲的幀數是30或者是60幀(Frame Per Second,FPS)時,就會感覺到比較流暢;除此之外,幀數穩定也是重中之重,比如這一秒有30幀,下一秒是60幀,如果沒有考慮到幀數,無論是精靈(Sprite)的位移或者是動畫,都會有一種不流暢的感覺。解決辦法如下:

  • 不要把耗時處理放在主循環中;
  • 依賴於每一幀的持續時間。

遊戲在運行過程中,每一幀的持續時間多多少少會有些變化,當位移的時候如果加上幀的持續時間,會讓人有着流暢的感覺。

#include "SDL.h"
 
int main(int argc, char** argv)
{
    //創建窗口和渲染器
        SDL_Init(SDL_INIT_EVERYTHING);
	SDL_Window* win = SDL_CreateWindow("FirstProject", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 600, 480, SDL_WINDOW_SHOWN);
	SDL_Renderer* ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_ACCELERATED);
	bool running = true;
	SDL_Event event = {};
        //大循環
	while (running)
	{
                //繪製
		SDL_RenderClear(ren);
		//draw here
		SDL_RenderPresent(ren);
	        //事件處理
		while (SDL_PollEvent(&event))
		{
			switch (event.type)
			{
			case SDL_QUIT:
				running = false;
				break;
			}
		}
	}
        //釋放內存
	SDL_DestroyRenderer(ren);
	SDL_DestroyWindow(win);
 
	return 0;
}

注:SDL的main函數必須爲int main(int argc, char* argv[]) 。原因:SDL作爲一個跨平臺的開發庫,上述的main是SDL提供的統一的入口。

1.1 創建窗口和渲染器

在使用SDL所提供的大部分函數之前,需要先調用SDL_Init函數來初始化SDL庫。官方例子如下:

#include "SDL.h"

int main(int argc, char* argv[])
{
    if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) != 0) {
        SDL_Log("Unable to initialize SDL: %s", SDL_GetError());
        return 1;
    }

    /* ... */

    SDL_Quit();

    return 0;
}

SDL_Init的參數可以是表1中的一個或幾個。

表1 SDL_Init的參數

SDL_INIT_TIMER

定時器子系統

SDL_INIT_AUDIO

音頻子系統

SDL_INIT_VIDEO

視頻子系統,會自動初始化事件子系統

SDL_INIT_JOYSTICK

搖桿子系統,會自動初始化事件子系統

SDL_INIT_HAPTIC

觸摸屏子系統

SDL_INIT_GAMECONTROLLER

控制器子系統,會自動初始化搖桿子系統

SDL_INIT_EVENTS

事件子系統

SDL_INIT_EVERYTHING

以上的所有子系統的和

SDL_INIT_NOPARACHUTE

忽略初始化錯誤

 除了SDL_INIT_EVERYTHING之外,其餘的是可以自由組合的,組合使用的爲二元操作符 | 位或。一般情況下爲了方便,也可以直接使用SDL_INIT_EVERYTHING。

SDL_Init的返回值如果爲0表示操作成功,否則爲失敗。

 1.2 繪製

爲了避免畫面產生閃爍或者撕裂感,SDL提供了雙緩衝機制。

其實對於SDL來說,跟繪製有關的函數並沒有幾個,最常用的有4個。

  1.2.1 SDL_RenderClear()

根據繪製顏色清除當前的渲染目標。可以簡單地認爲該函數是清除畫布,當調用該函數後,畫布則刷新成SDL_SetRenderDrawColor()所設置的顏色,默認爲黑色(0, 0, 0)。

官方示例:SDL_RenderClear

  1.2.2 SDL_RenderPresent()

把後緩衝區的畫面繪製到前緩衝區中。這個函數一般情況下是和SDL_RenderClear()配合使用的。由SDL_RenderClear()清屏,然後SDL_RenderPresent()進行畫面呈現,在這兩個函數之間進行繪製。

官方示例:SDL_RenderPresent

  1.2.3 擴展

  1. SDL_RenderCopy 把紋理繪製到當前的渲染目標中。
  2. SDL_RenderCopyEx SDL_RenderCopy的擴展。
  3. SDL_RenderDraw* 繪製點、線、矩形等。(根據SDL_SetRenderDrawColor所設置的顏色進行繪製,所以如果和清屏的顏色相同的話,那麼應該看不到繪製的點、線或者是矩形)

 以上的幾個函數就是SDL所提供的所有的繪製函數

1.2.4 渲染目標 render target

 SDL的渲染目標一般有兩類,一個是SDL_Renderer渲染器,另一個是SDL_Texture紋理。渲染器是默認的緩衝區,即把後緩衝區繪製到屏幕上;另外一個是把後緩衝區繪製到對應的紋理上。關於渲染器和紋理,目前說的有點模糊,之後會專門說一個SDL_Renderer SDL_Texture SDL_Surface。可以簡單地認爲SDL_Renderer爲渲染器,繪製需要用到它;SDL_Texture紋理是圖片。

 1.3 事件處理

當有事件發生時,SDL會首先把事件放入事件隊列中,等待被處理。用得比較多的就是SDL_PollEvent,一般的用法就是使用一個循環來抓取事件並進行處理:

while (1) {
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        //事件處理
    }
    //邏輯處理
    //繪製處理
}

SDL_PollEvent是非阻塞式的,它會判斷事件隊列中是否存在事件,如果有,則返回true,並把數據寫入到SDL_Event中,否則返回false,事件處理結束。

使用循環的一大部分原因是爲了能在這一幀內處理完事件隊列。

SDL_Event是一個聯合體,它囊括了各種事件,包括鼠標事件、鍵盤事件、搖桿事件等,然後通過event.type來判斷當前的事件是什麼類型的。

注:除了外部發送事件外,還可以在程序中手動發送事件到事件隊列中。

2.對主程序的封裝

可以簡單地根據流程圖把之前的代碼進行一個封裝。

#ifndef __Game_H__
#define __Game_H__

#include <vector>
#include <string>
#include <iostream>
#include <stdexcept>

#include "SDL.h"

class Game
{
private:
	static Game*s_pInstance;
	Game();
public:
     static Game* getInstance();
     //初始化
     bool init(const char *title, int xpos, int ypos, int width, int height, int flags);
     void render();
	 void update();
	 void handleEvents();
	 void clean();

	 SDL_Renderer* getRenderer() const { return m_pRenderer; }
	 bool running() const { return m_bRunning; }

	 int getGameWidth() const { return m_gameWidth; }
	 int getGameHeight() const { return m_gameHeight; }
private:
	//SDL窗口 渲染器
	SDL_Window* m_pWindow;
	SDL_Renderer* m_pRenderer;
	//是否運行
	bool m_bRunning;
	//屏幕大小
	int m_gameWidth;
	int m_gameHeight;
};
typedef Game TheGame;
#endif

Game作爲單例類,方便獲取渲染器和窗口的尺寸。在有了這個類後,main函數就比較簡單了:

#include<iostream>
#include<string>

#include "SDL.h"

#include "Game.h"

int main(int argc,char**argv)
{
	if (TheGame::getInstance()->init("Shot Stick"
		,SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED
		,960,640,SDL_WINDOW_SHOWN))
	{
		while (TheGame::getInstance()->running())
		{
			TheGame::getInstance()->update();
			TheGame::getInstance()->render();
			TheGame::getInstance()->handleEvents();
		}
	}
	else
	{
	    std::cout<<"error:"<<SDL_GetError()<<std::endl;
		return -1;
	}
	TheGame::getInstance()->clean();

	return 0;
}

SDL_WINDOWPOS_CENTERED 是一個宏,表示窗口顯示在屏幕的中央。

在封裝之後,main.cpp中的代碼和流程圖的功能是一致的,接下來則是Game中函數的實現了。

#include "Game.h"

Game*Game::s_pInstance = NULL;

Game::Game()
	:m_pWindow(nullptr)
	,m_pRenderer(nullptr)
	,m_bRunning(true)
	,m_gameWidth(0)
	,m_gameHeight(0)
{
}

Game* Game::getInstance()
{
	if (s_pInstance == NULL)
		s_pInstance = new Game();

	return s_pInstance;
}

所謂單例類,就是隻有一個實例,把構造函數作爲私有函數,並提供一個靜態共有函數來獲取唯一實例。

bool Game::init(const char *title, int xpos, int ypos, int width, int height, int flags)
{
    m_bRunning = false;
	if (SDL_Init(SDL_INIT_EVERYTHING) == 0)
	{
		/// if succeeded create our window
		m_pWindow = SDL_CreateWindow(title, xpos, ypos, width, height, flags);
		if (m_pWindow != NULL)
			m_pRenderer = SDL_CreateRenderer(m_pWindow, -1,SDL_RENDERER_ACCELERATED|SDL_RENDERER_PRESENTVSYNC);
		if (m_pRenderer != NULL)
			SDL_SetRenderDrawColor(m_pRenderer,210,250,255,255);
		else
			return false;
	}
	else
		return false;

	m_bRunning = true;
	std::string platform = SDL_GetPlatform();
	// init
	if (platform == "Android") {
		SDL_GetWindowSize(m_pWindow,&m_gameWidth,&m_gameHeight);
	}
	else {
		m_gameWidth = width;
		m_gameHeight = height;
	}
	SDL_Log("width=%d, height=%d\n", m_gameWidth, m_gameHeight);

	return true;
}

在Game::init函數中進行初始化操作,創建窗口和渲染器。在這之後,判斷當前的平臺,因爲在android等嵌入式設備中,一個應用的窗口一般就是手機分辨率的大小。所以如果是在android下,則獲取實際的窗口大小,而不是預設的窗口大小。

不過需要注意的是,還是可以設置窗口爲我們所設置的大小的,可以調用SDL_RenderSetScale對整個窗口進行拉伸,以鋪滿整個屏幕,這樣做的優點是不用操心分辨率的問題,缺點就是在不同分辨率下的手機上會有不同程度的拉伸,有時候甚至導致變形。

void Game::render()
{
	SDL_SetRenderDrawColor(m_pRenderer,210,250,255,255);
	///clear the renderer to the draw color
	SDL_RenderClear(m_pRenderer);
	///draw
    SDL_SetRenderDrawColor(m_pRenderer,0, 0, 0);
    SDL_Rect rect = { 0, 0, 200, 200 };
	SDL_RenderDrawRect(m_pRenderer, &rect);

	///draw to the screen
	SDL_RenderPresent(m_pRenderer);
}

Game::draw函數進行實際的繪製操作,目前在這裏只是繪製一個黑色的不填充矩形。

void Game::handleEvents()
{
	SDL_Event event;
	while (SDL_PollEvent(&event))
	{
		switch (event.type)
		{
		case SDL_QUIT:
			m_bRunning = false;
			break;
		}
	}
}

Game::handleEvents函數進行事件處理,目前僅僅判斷是否有退出信號,如果有則退出大循環。

void Game::clean()
{
	SDL_DestroyRenderer(m_pRenderer);
	SDL_DestroyWindow(m_pWindow);
	SDL_Quit();
}

void Game::update()
{
}

Game::clean函數的功能是釋放內存。

運行結果:

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