SDL 简介





5.00/5 (3投票s)
了解如何使用 SDL 创建一个简单的可视化应用程序。
引言
本文将引导您使用 SDL 和 C++ 创建一个基本的游戏框架。最终结果很简单,但它为构建更具功能性的可视化应用程序和游戏提供了坚实的基础。
背景
此代码被用作为 SDL 游戏编程竞赛 创建的射击类游戏的基础。
使用代码
我们将从 EngineManager
类开始。该类将负责初始化 SDL,维护构成游戏的各种对象,以及分发事件。
EngineManager.h
#ifndef _ENGINEMANAGER_H__
#define _ENGINEMANAGER_H__
#include <sdl.h>
#include <list>
#define ENGINEMANAGER EngineManager::Instance()
class BaseObject;
typedef std::list<baseobject*> BaseObjectList;
class EngineManager
{
public:
~EngineManager();
static EngineManager& Instance()
{
static EngineManager instance;
return instance;
}
bool Startup();
void Shutdown();
void Stop() {running = false;}
void AddBaseObject(BaseObject* object);
void RemoveBaseObject(BaseObject* object);
protected:
EngineManager();
void AddBaseObjects();
void RemoveBaseObjects();
bool running;
SDL_Surface* surface;
BaseObjectList baseObjects;
BaseObjectList addedBaseObjects;
BaseObjectList removedBaseObjects;
Uint32 lastFrame;
};
#endif
EngineManager.cpp
#include "EngineManager.h"
#include "ApplicationManager.h"
#include "BaseObject.h"
#include "Constants.h"
#include <boost/foreach.hpp>
EngineManager::EngineManager() :
running(true),
surface(NULL)
{
}
EngineManager::~EngineManager()
{
}
bool EngineManager::Startup()
{
if(SDL_Init(SDL_INIT_EVERYTHING) < 0)
return false;
if((surface = SDL_SetVideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, BITS_PER_PIXEL,
SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_ANYFORMAT)) == NULL)
return false;
APPLICATIONMANAGER.Startup();
lastFrame = SDL_GetTicks();
while (running)
{
SDL_Event sdlEvent;
while(SDL_PollEvent(&sdlEvent))
{
if (sdlEvent.type == SDL_QUIT)
running = false;
}
AddBaseObjects();
RemoveBaseObjects();
Uint32 thisFrame = SDL_GetTicks();
float dt = (thisFrame - lastFrame) / 1000.0f;
lastFrame = thisFrame;
BOOST_FOREACH (BaseObject* object, baseObjects)
object->EnterFrame(dt);
SDL_Rect clearRect;
clearRect.x = 0;
clearRect.y = 0;
clearRect.w = SCREEN_WIDTH;
clearRect.h = SCREEN_HEIGHT;
SDL_FillRect(surface, &clearRect, 0);
BOOST_FOREACH (BaseObject* object, baseObjects)
object->Draw(this->surface);
SDL_Flip(surface);
}
return true;
}
void EngineManager::Shutdown()
{
APPLICATIONMANAGER.Shutdown();
surface = NULL;
SDL_Quit();
}
void EngineManager::AddBaseObject(BaseObject* object)
{
addedBaseObjects.push_back(object);
}
void EngineManager::RemoveBaseObject(BaseObject* object)
{
removedBaseObjects.push_back(object);
}
void EngineManager::AddBaseObjects()
{
BOOST_FOREACH (BaseObject* object, addedBaseObjects)
baseObjects.push_back(object);
addedBaseObjects.clear();
}
void EngineManager::RemoveBaseObjects()
{
BOOST_FOREACH (BaseObject* object, removedBaseObjects)
baseObjects.remove(object);
removedBaseObjects.clear();
}
我们需要做的第一件事是调用 SDL_Init
。这会加载 SDL 库,并初始化我们指定的任何子系统。在本例中,我们通过提供 SDL_INIT_EVERYTHING
标志来指定初始化所有内容。您可以选择只初始化您需要的子系统(音频、视频、输入等),但由于随着游戏的进行,我们将使用其中的大部分子系统,因此现在初始化所有内容可以节省一些时间。
if(SDL_Init(SDL_INIT_EVERYTHING) < 0)
return false;
如果 SDL 及其子系统已正确加载和初始化,我们便会创建一个窗口。SCREEN_WIDTH
、SCREEN_HEIGHT
和 BITS_PER_PIXEL
定义了窗口的大小和颜色深度。这些值定义在 Constants.h 文件中。下一个参数是一组选项,用于进一步指定窗口如何工作。
SDL_HWSURFACE
选项告诉 SDL 将视频表面放在视频内存中(即显卡上的内存)。大多数系统都有专用的显卡,拥有充足的内存,足以容纳我们的 2D 游戏。
SDL_DOUBLEBUF
选项告诉 SDL 设置两个视频表面,并通过调用 SDL_Flip()
在两者之间进行切换。这可以防止在监视器刷新时视频内存正在写入时可能发生的视觉撕裂。它比单缓冲渲染方案慢,但同样,大多数系统都足够快,不会对性能产生任何影响。
SDL_ANYFORMAT
选项告诉 SDL,如果它无法以请求的颜色深度设置窗口,则可以使用它可用的最佳颜色深度。我们请求了 32 位颜色深度,但某些桌面可能只运行在 16 位。这意味着我们的应用程序不会因为桌面未设置为 32 位颜色深度而失败。
if((surface = SDL_SetVideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, BITS_PER_PIXEL,
SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_ANYFORMAT)) == NULL)
return false;
然后启动 ApplicationManager
。ApplicationManager
包含定义应用程序如何运行的逻辑。它与 EngineManager
分开,以便将初始化和管理 SDL 所需的代码与管理应用程序本身所需的代码分开。
APPLICATIONMANAGER.Startup();
获取当前系统时间并存储在 lastFrame
变量中。稍后将使用此变量来计算自上一帧渲染以来经过的时间。帧之间的时间用于使游戏对象能够以可预测的方式移动,而与帧速率无关。
lastFrame = SDL_GetTicks();
下一块代码定义了渲染循环。渲染循环是每帧执行一次的循环。
在渲染循环中所做的第一件事是处理上一帧期间可能触发的任何 SDL 事件。目前,我们唯一关心的事件是 SDL_Quit
事件,当窗口关闭时会触发该事件。在此事件中,我们将 running
变量设置为 false
,这将使我们退出渲染循环。
SDL_Event sdlEvent;
while(SDL_PollEvent(&sdlEvent))
{
if (sdlEvent.type == SDL_QUIT)
running = false;
}
接下来,任何新增或移除的 BaseObject
都会与主 baseObjects
集合同步。BaseObject
类是构成游戏的所有对象的基类。当创建或销毁一个新的 BaseObject
类时,它会添加或移除自身到 EngineManager
维护的主集合中。但是,它们不能直接修改 baseObjects
集合 - 任何新增或移除的对象都会被放入名为 addedBaseObjects
和 removedBaseObjects
的临时集合中,这确保了在循环遍历 baseObjects
集合时不会修改它。在循环遍历集合的项时修改集合永远不是一个好主意。调用 AddBaseObjects
和 RemoveBaseObjects
函数允许在我们可以确定我们没有循环遍历它时更新 baseObjects
集合。
AddBaseObjects();
RemoveBaseObjects();
以秒(或秒的一部分)为单位计算自上一帧以来的时间,并将当前系统时间保存在 lastFrame
变量中。
Uint32 thisFrame = SDL_GetTicks();
float dt = (thisFrame - lastFrame) / 1000.0f;
lastFrame = thisFrame;
然后调用每个 BaseObject
的 Update
函数。Update
函数是游戏对象执行任何内部更新的地方,例如移动、旋转或射击武器。将提供之前计算的帧时间,因此游戏对象可以每秒以相同的量更新自身,而与帧速率无关。
BOOST_FOREACH (BaseObject* object, baseObjects)
object->EnterFrame(dt);
然后通过使用 SDL_FillRect
函数将黑色矩形绘制到整个屏幕来清除视频缓冲区。
SDL_Rect clearRect;
clearRect.x = 0;
clearRect.y = 0;
clearRect.w = SCREEN_WIDTH;
clearRect.h = SCREEN_HEIGHT;
SDL_FillRect(surface, &clearRect, 0);
现在通过调用其 Draw
函数,要求游戏对象将自身绘制到视频表面。在这里,游戏对象用来表示自身的任何图形都会被绘制到后缓冲区。
BOOST_FOREACH (BaseObject* object, baseObjects)
object->Draw(this->surface);
最后,翻转后缓冲区,将其显示在屏幕上。
SDL_Flip(surface);
Shutdown
函数清理任何内存。
调用 ApplicationManager
的 shutdown 函数,它将清理它创建的所有对象。
APPLICATIONMANAGER.Shutdown();
然后我们调用 SDL_Quit()
,它会卸载 SDL 库。
surface = NULL;
SDL_Quit();
接下来的四个函数,AddBaseObject
、RemoveBaseObject
、AddBaseObjects
和 RemoveBaseObjects
,都用于将 BaseObject
添加或移除到临时 addedBaseObjects
和 removedBaseObjects
集合中,或将这些临时集合中的对象同步到主 baseObjects
集合。
void EngineManager::AddBaseObject(BaseObject* object)
{
addedBaseObjects.push_back(object);
}
void EngineManager::RemoveBaseObject(BaseObject* object)
{
removedBaseObjects.push_back(object);
}
void EngineManager::AddBaseObjects()
{
BOOST_FOREACH (BaseObject* object, addedBaseObjects)
baseObjects.push_back(object);
addedBaseObjects.clear();
}
void EngineManager::RemoveBaseObjects()
{
BOOST_FOREACH (BaseObject* object, removedBaseObjects)
baseObjects.remove(object);
removedBaseObjects.clear();
}
正如我们之前提到的,ApplicationManager
包含定义应用程序如何运行的代码。在这个非常简单的演示中,我们在 Startup
函数中创建了一个新的 Bounce
对象实例,并在 Shutdown
函数中将其移除。
ApplicationManager.h
#ifndef _APPLICATIONMANAGER_H__
#define _APPLICATIONMANAGER_H__
#define APPLICATIONMANAGER ApplicationManager::Instance()
#include "Bounce.h"
class ApplicationManager
{
public:
~ApplicationManager();
static ApplicationManager& Instance()
{
static ApplicationManager instance;
return instance;
}
void Startup();
void Shutdown();
protected:
ApplicationManager();
Bounce* bounce;
};
#endif
ApplicationManager.cpp
#include "ApplicationManager.h"
ApplicationManager::ApplicationManager() :
bounce(NULL)
{
}
ApplicationManager::~ApplicationManager()
{
}
void ApplicationManager::Startup()
{
try
{
bounce = new Bounce("../media/image.bmp");
}
catch (std::string& ex)
{
}
}
void ApplicationManager::Shutdown()
{
delete bounce;
bounce = NULL;
}
BaseObject
类是所有游戏对象的基类。它定义了 EngineManager
类在渲染循环期间使用的 EnterFrame
和 Draw
函数。除了定义这些函数之外,BaseObject
在创建时通过调用 AddBaseObject
函数向 EngineManager
注册自身,并在销毁时调用 RemoveBaseObject
函数移除自身。
BaseObject.h
#ifndef _BASEOBJECT_H__
#define _BASEOBJECT_H__
#include <SDL.h>
class BaseObject
{
public:
BaseObject();
virtual ~BaseObject();
virtual void EnterFrame(float dt) {}
virtual void Draw(SDL_Surface* const mainSurface) {}
};
#endif
BaseObject.cpp
#include "BaseObject.h"
#include "EngineManager.h"
BaseObject::BaseObject()
{
ENGINEMANAGER.AddBaseObject(this);
}
BaseObject::~BaseObject()
{
ENGINEMANAGER.RemoveBaseObject(this);
}
VisualGameObject
扩展了 BaseObject
类,并增加了将图像显示到屏幕的功能。
VisualGameObject.h
#ifndef _VISUALGAMEOBJECT_H__
#define _VISUALGAMEOBJECT_H__
#include <string>
#include <SDL.h>
#include "BaseObject.h"
class VisualGameObject :
public BaseObject
{
public:
VisualGameObject(const std::string& filename);
virtual ~VisualGameObject();
virtual void Draw(SDL_Surface* const mainSurface);
protected:
SDL_Surface* surface;
float x;
float y;
};
#endif
VisualGameObject.cpp
#include "VisualGameObject.h"
VisualGameObject::VisualGameObject(const std::string& filename) :
BaseObject(),
surface(NULL),
x(0),
y(0)
{
SDL_Surface* temp = NULL;
if((temp = SDL_LoadBMP(filename.c_str())) == NULL)
throw std::string("Failed to load BMP file.");
surface = SDL_DisplayFormat(temp);
SDL_FreeSurface(temp);
}
VisualGameObject::~VisualGameObject()
{
if (surface)
{
SDL_FreeSurface(surface);
surface = NULL;
}
}
void VisualGameObject::Draw(SDL_Surface* const mainSurface)
{
SDL_Rect destRect;
destRect.x = int(x);
destRect.y = int(y);
SDL_BlitSurface(surface, NULL, mainSurface, &destRect);
}
使用 SDL_LoadBMP
函数(SDL_LoadBMP
在技术上是一个宏)从加载的 BMP 文件创建 SDL 表面。
SDL_Surface* temp = NULL;
if((temp = SDL_LoadBMP(filename.c_str())) == NULL)
throw std::string("Failed to load BMP file.");
当表面加载时,它可能与屏幕的颜色深度不同。尝试绘制一个颜色深度不同的表面会消耗大量的 CPU 周期,因此我们使用 SDL_DisplayFormat
函数来创建一个我们刚刚加载的表面副本,该副本与当前屏幕深度匹配。通过一次性完成此操作,而不是每帧都进行,我们可以获得额外的性能。
surface = SDL_DisplayFormat(temp);
在以正确的格式创建新表面后,可以移除通过加载 BMP 文件创建的表面。
SDL_FreeSurface(temp);
VisualGameObject
析构函数清理了构造函数创建的表面。
VisualGameObject::~VisualGameObject()
{
if (surface)
{
SDL_FreeSurface(surface);
surface = NULL;
}
}
最后,覆盖了 Draw
函数,并提供了使用 x 和 y 坐标作为图像左上角位置的代码,将构造函数中加载的表面绘制到屏幕上。
void VisualGameObject::Draw(SDL_Surface* const mainSurface)
{
SDL_Rect destRect;
destRect.x = int(x);
destRect.y = int(y);
SDL_BlitSurface(surface, NULL, mainSurface, &destRect);
}
Bounce
类是如何将所有这些其他类结合在一起的一个示例。
Bounce.h
#ifndef _BOUNCE_H__
#define _BOUNCE_H__
#include <SDL.h>
#include <string>
#include "VisualGameObject.h"
class Bounce :
public VisualGameObject
{
public:
Bounce(const std::string filename);
~Bounce();
void EnterFrame(float dt);
protected:
int xDirection;
int yDirection;
};
#endif
Bounce.cpp
#include "Bounce.h"
#include "Constants.h"
static const float SPEED = 50;
Bounce::Bounce(const std::string filename) :
VisualGameObject(filename),
xDirection(1),
yDirection(1)
{
}
Bounce::~Bounce()
{
}
void Bounce::EnterFrame(float dt)
{
this->x += SPEED * dt * xDirection;
this->y += SPEED * dt * yDirection;
if (this->x < 0)
{
this->x = 0;
xDirection = 1;
}
if (this->x > SCREEN_WIDTH - surface->w)
{
this->x = float(SCREEN_WIDTH - surface->w);
xDirection = -1;
}
if (this->y < 0)
{
this->y = 0;
yDirection = 1;
}
if (this->y > SCREEN_HEIGHT - surface->h)
{
this->y = float(SCREEN_HEIGHT - surface->h);
yDirection = -1;
}
}
覆盖了 EnterFrame
函数,并通过修改基类 VisualGameObject
的 x
和 y
变量来移动图像,当它撞到屏幕边缘时会弹开。
void Bounce::EnterFrame(float dt)
{
this->x += SPEED * dt * xDirection;
this->y += SPEED * dt * yDirection;
if (this->x < 0)
{
this->x = 0;
xDirection = 1;
}
if (this->x > SCREEN_WIDTH - surface->w)
{
this->x = float(SCREEN_WIDTH - surface->w);
xDirection = -1;
}
if (this->y < 0)
{
this->y = 0;
yDirection = 1;
}
if (this->y > SCREEN_HEIGHT - surface->h)
{
this->y = float(SCREEN_HEIGHT - surface->h);
yDirection = -1;
}
}
正如您所见,通过在 EngineManager
、BaseObject
和 VisualGameObject
类中定义使用 SDL 所需的底层逻辑,像 Bounce
这样的类可以专注于定义其行为的代码,而不必担心诸如绘制到屏幕本身之类的低级问题。我们将随着游戏的开发更多地使用这些基类。
现在,我们有了一个对 SDL 的简单介绍,以及一个我们可以继续构建的框架的开端。
历史
- 2009 年 8 月 21 日 - 初始帖子。