上一篇:Xcode与C++之游戏开发:游戏对象
预备动作:请按照第一篇,搭好一个黑框窗口,然后按照第二篇完成基本渲染器,按第三篇完成增量时间。本篇有大量代码,请有一定的耐心。
精灵
精灵是2D游戏中的视觉对象,通常用来表示角色、背景或者其它动态对象。对于移动游戏而言,它占了大部分的游戏大小。大多数2D游戏具有数十个甚至数百个精灵。
每个精灵都有一个或多个与之关联的图像文件。有许多不同的图像文件格式,游戏使用基于平台和其它约束使用不同格式。例如,PNG 是压缩图像格式,使用这个格式可以减少一部分存储空间。但是,硬件不能原生地支持绘制 PNG 文件,所以加载时间会长一点。一些平台会使用一些对显卡(准确地说,图形硬件)特别优化过格式,比如PVR(ios)、DXT(PC和Xbox)。
因为它在游戏中大量的存在,因此高效地运用它们就显得非常重要了。
加载图像文件
仅仅使用 SDL 库的2D游戏,最简单的加载图像文件的方式是使用 SDL Image 库,因为 SDL 本身只支持 .bmp 格式。第一步就是用 IMG_Init
初始化 SDL 图像,并且用一个标志位参数来表明所需的文件格式。在 Game::Inititalize()
中加入:
if (IMG_Init(IMG_INIT_PNG) == 0)
{
SDL_Log("不能初始化SDL_image: %s", SDL_GetError());
return false;
}
除了 PNG,还有其它的格式:
标志 | 格式 |
---|---|
IMG_INIT_JPG | JPEG |
IMG_INIT_PNG | PNG |
IMG_INIT_TIF | TIFF |
完成 SDL Image 的初始化后,就可以使用 IMG_Load
加载图像文件到 SDL_Surface
。在这之后,使用 SDL_CreateTextureFromSurface
将 SDL_Surface
转换到 SDL_Texture
(这是 SDL 要求的)。将上述过程封装一下:
SDL_Texture* Game::LoadTexture(const char* fileName)
{
// 从文件中加载
SDL_Surface* surf = IMG_Load(fileName);
if (!surf)
{
SDL_Log("加载图像文件 %s 失败", fileName);
return nullptr;
}
// 从 surface 创建 texture
SDL_Texture* tex = SDL_CreateTextureFromSurface(mRenderer, surf);
SDL_FreeSurface(surf);
if (!tex)
{
SDL_Log("%s surface 转换到 texture 失败!", fileName);
return nullptr;
}
return tex;
}
现在尝试加载一张图片试试,在 Game::Inititalize()
中加入下面的代码(注意,将Game::GenerateOutput()
函数清空,否则你会有惊喜)。
SDL_Texture* tex = LoadTexture("/XXX/XXX/.png");
SDL_RenderCopy(mRenderer, tex, 0, 0);
SDL_RenderPresent(mRenderer);
嗯,你应该猜得出来,其实我按图片大小修改了初始化时候的窗体大小。(上述代码只是演示一下,不出现在后文的代码中,换句话说,本文最终产品可能不包含上述代码,请酌情处理。)
texture的存储与加载
接下来,又有一个很有意思的问题,怎么存储这些 texture(可以翻译成纹理,但感觉中文有点小歧义,纹理总是让我想到那些木纹、石头纹路之类的。图片属于纹理,有点小奇怪)。使用相同的图片在游戏中非常普遍,假如有20个行星,每个行星都用相同的行星图片,重复加载20次,显然是不能接受的(I/O开销很昂贵的)。
一个最简单的方式就是在游戏中创建一个文件名到 SDL_Texture
指针的映射。C++11 引入的 unordered_map
是一个很不错的选择。我们可以创建一个 GetTexture
函数,它通过 texture 名字返回对应的 SDL_Texture
指针。这个函数首先应该检查 texture 是否已经存在,如果不存在,那就从文件中加载 texture。
先在 Game.hpp
中声明函数还有映射 mTextures
:
// ....
#include <string>
#include <unordered_map>
class Game
{
public:
//...
SDL_Texture* GetTexture(const std::string& fileName);
private:
// ...
// 已加载的 textures
std::unordered_map<std::string, SDL_Texture*> mTextures;
};
再到 Game.cpp
实现:
SDL_Texture* Game::GetTexture(const std::string& fileName)
{
SDL_Texture* tex = nullptr;
// texture是否已经存在?
auto iter = mTextures.find(fileName);
if (iter != mTextures.end())
{
tex = iter->second;
}
else
{
// 从文件中加载
SDL_Surface* surf = IMG_Load(fileName.c_str());
if (!surf)
{
SDL_Log("加载texture文件%s失败", fileName.c_str());
return nullptr;
}
// 从 surface 中创建 textures
tex = SDL_CreateTextureFromSurface(mRenderer, surf);
SDL_FreeSurface(surf);
if (!tex)
{
SDL_Log("无法把%s从surface转化到texture", fileName.c_str());
return nullptr;
}
mTextures.emplace(fileName.c_str(), tex);
}
return tex;
}
简单情况下将文件名映射到 SDL_Texture 指针是有意义的,但考虑到实际的游戏可能具有许多不同类型的 textures,比如声音效果,3D模型,字体等。因此,编写一个更健壮的系统来一般处理所有类型的 textures 是有必要的,但为了简单起见,就不打算开发什么资产管理系统了。
为了更清楚的划分责任,不妨创建一个 Load_data()
函数来加载游戏世界中的演员。好吧,暂时留白,待会等演员上场,再回过头来补充这个函数。老规矩,先头文件(private,放在私有里),然后.cpp文件。之后在 Initialize()
的最后调用这个函数。
// ...
LoadData();
mTicksCount = SDL_GetTicks();
return true;
}
void Game::LoadData()
{
}
画家算法
假设游戏只有背景图片和游戏角色,那么绘制的顺序是先背景再角色。这很类似画家的绘图时候的方式,因此这也被称作画家算法(painter’s algorithm)。也就说,先画最底层,再一层层往上渲染。
混合游戏对象模型的实现
上一篇,已经介绍过游戏对象了,接下来会采用Actor/Component混合式的游戏对象模型。但由于上一篇没有上下文,没有完整的具体代码,但对游戏对象模型的实现已经给出了介绍,下面直接贴出代码(不清楚的,请参考上一篇)。
Component.hpp:
#ifndef Component_hpp
#define Component_hpp
class Component
{
public:
// 构造函数
// (值越低的更新顺序,则组件越早更新)
Component(class Actor* owner, int updateOrder = 100);
// 析构函数
virtual ~Component();
// 通过增量时间更新组件
virtual void Update(float deltaTime);
int GetUpdateOrder() const {
return mUpdateOrder; }
protected:
// 所属的角色
class Actor* mOwner;
// 组件的更新顺序
int mUpdateOrder;
};
#endif /* Component_hpp */
Component.cpp:
#include "Component.hpp"
#include "Actor.hpp"
Component::Component(Actor* owner, int updateOrder)
:mOwner(owner)
,mUpdateOrder(updateOrder)
{
// 添加到actor的组件向量
mOwner->AddComponent(this);
}
Component::~Component()
{
mOwner->RemoveComponent(this);
}
void Component::Update(float deltaTime)
{
}
Actor.hpp:
#ifndef Actor_hpp
#define Actor_hpp
#include <vector>
#include "Math.hpp"
class Actor
{
public:
enum State
{
EActive,
EPaused,
EDead
};
Actor(class Game* game);
virtual ~Actor();
// 从 Game 调用 Update 函数 (不被继承重写)
void Update(float deltaTime);
// 更新属于该actor的所有组件 (不被继承重写)
void UpdateComponents(float deltaTime);
// actor特有更新代码 (可重写)
virtual void UpdateActor(float deltaTime);
// Getters/setters
const Vector2& GetPosition() const {
return mPosition; }
void SetPosition(const Vector2& pos) {
mPosition = pos; }
float GetScale() const {
return mScale; }
void SetScale(float scale) {
mScale = scale; }
float GetRotation() const {
return mRotation; }
void SetRotation(float rotation) {
mRotation = rotation; }
State GetState() const {
return mState;