1.最简单的SDL程序
一般的游戏在运行过程中的大部分操作都是在一个大循环里,在这个循环里进行着事件监听、绘制以及逻辑处理等。而像网络通信或者是文件读取等这些比较耗时或者堵塞的操作一般会放到子线程里面。流程图如下:

- 先创建窗口和渲染器;如果创建成功,则进入大循环里;否则则直接退出;
- 进行逻辑处理、绘制、事件处理等(注意:以上三个是不分先后的);
- 如果发现有退出信号,则退出大循环,然后释放内存;游戏结束。否则则重复步骤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中的一个或几个。
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 扩展
- SDL_RenderCopy 把纹理绘制到当前的渲染目标中。
- SDL_RenderCopyEx SDL_RenderCopy的扩展。
- 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函数的功能是释放内存。
运行结果: