用C++和SFML写游戏-纹理及精灵(5.2)

上一篇文章介绍了纹理的使用。这一节将会介绍 精灵 以及资源管理。

一、什么是精灵

在这之前你可能听说过精灵的概念。实际上,精灵是贴有纹理的“画布”。说到这里,有人可能会说,前面的 Shpae类不是也一样的吗?答案是的,但是精灵有一点不同。

最重要的不同点是,精灵总是绘制在纹理矩形中,而 Shape 可以不必这样,精灵总是与一个纹理关联的,它不能单独存在。除此之外, Sprite类有一个类似与 Shape::setFillColor()的函数 Sprite::setColor()。当它们都与一个纹理(Texture)相关联的时候,效果是一样的,但是如果 Sprite 没有与纹理(Texture)关联的话,它不会被指定的颜色绘制,相反的是 Shape 是可以被指定颜色绘制的。

那么问题来了?为什么我们要使用精灵而不是 Shape呢?从上面的描述来看,精灵似乎是弱化版的Shape。真正的原因是因为精灵简单有用。

观察一下代码:

1
2
3
4
5
6
// 创建一个带纹理的shape
sf::RectangleShape rectShape(sf::Vector2f(100, 100));
rectShape.setTexture(&texture);

// 创建精灵
sf::Sprite sp(texture);

可以很明显的看到创建一个精灵是一个更加简单的过程。实际上,这也是 Sprite类在 SFML 中被使用的主要目的——尽可能快速,轻松地在屏幕上渲染纹理。

</br>

二、Transformables 和 Drawables

Sprite 类起源于这两个类 —— TransformableDrawable

Drawable类是一个接口,它有一个抽象方法 Drawable::deaw(),它的子类必须实现这个方法,进而使得子类是可以被绘制到画布上的(例如 RenderWindow

Transformable 拥有位置、旋转、缩放以及原点等属性。可以通过Transformable::setPosition()Transformable::getPosition()Transformable::move() 等方法去访问这些属性。

这些方法我们在 Shape 类中也使用过,因为 Shape 继承了这两个类。类似的,我们可以通过继承这两个类来实现自己的精灵。

</br>

三、Sprite 类的其他属性

在实现碰撞检测(Collision Detection)时,不可避免地需要知道场景中物体的尺寸或者边界。我们称之为“包围盒技术”(Bounding Box)。常见的包围盒有:Sphere、AABB、OBB、FDH等

Sprite 使用了AABB包围盒(Axis-aligned bounding box)的方法。Sprite::getLocalBounds()Sprite::getGlobalBounds() 都能够计算出 Sprite 的 AABB 属性。区别是 Sprite::getLocalBounds() 忽略了实体的变换属性(平移、旋转、缩放。。。),而 Sprite::getGlobalBounds() 考虑了这些属性。通过这两个方法返回的 FloatRect 可以用于碰撞检测。

</br>

四、资源管理

无论是做游戏还是多媒体程序,准确、高效的管理好资源是很重要的。确保好资源不被意外的销毁以及不重复读取同一资源是我们构建一份健壮的代码的基本需求。

需要明白一个事实,C++里栈中的对象在不需要的会被自动清除。但是在某些编程语言中,对象存储在堆中(实现了内存管理)。因此,我们的程序要确保对象在被使用的时候是存活的。下面的例子展示了当函数退出的时候,纹理也被销毁了。

1
2
3
4
5
6
sf::Sprite createSprite(std::string const& filename) {
sf::Texture texture;
texture.loadFromFile(filename);
// 函数返回时,texture 已经被销毁了
return sf::Sprite(texture);
}

如果我们使用上面的方法创建 Sprite,那么在绘制 Sprite 的时候将会是一片空白,texture不会被显示出来。

创建一个资源管理器去管理资源的生命周期是很有用的,下面试资源管理器的头文件定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef ASSET_MANAGER_H
#define ASSET_MANAGER_H

#include <SFML/Graphics.hpp>
#include <map>

class AssetManager
{
public:
AssetManager();

static sf::Texture& GetTexture(std::string const& filename);

private:
std::map<std::string, sf::Texture> m_Textures;

static AssetManager* sInstance;
};

#endif

可以看到,我们的 AssetManager 是一个单例。这样子的话我们可以在程序的任何地方通过静态方法 GetTexture() 访问资源而不用先去获取 AssetManager 对象的引用。

里面的 map 类型变量存储了资源是否读取过,方便后面判断是否需要再次读取资源。

现在来看看实现代码:

1
2
3
4
5
6
7
8
9
#include "AssetManager.h"
#include <assert.h>

AssetManager* AssetManager::sInstance = nullptr;

AssetManager::AssetManager() {
assert(sInstance == nullptr);
sInstance = this;
}

assert 确保了我们的资源管理器只有一个实例。

再来看看 AssetManager::GetTexture() 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
sf::Texture& AssetManager::GetTexture(std::string const& filename) {
auto& texMap = sInstance->m_Textures;

auto pairFound = texMap.find(filename);
if (pairFound != texMap.end()) {
return pairFound->second;
}

auto& texture = texMap[filename];
texture.loadFromFile(filename);
return texture;
}

代码很容易懂,如果资源已经被读取过了,直接返回,否则从给定路径读取。

最后,看看我们的资源管理器如何使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <SFML/Graphics.hpp>
#include "AssetManager.h"

int main() {
sf::RenderWindow window(sf::VideoMode(640, 480), "AssetManager");
AssetManager manager;

sf::Sprite sprite1 = sf::Sprite(AssetManager::GetTexture("1.png"));
sf::Sprite sprite2 = sf::Sprite(AssetManager::GetTexture("2.png"));
sf::Sprite sprite3 = sf::Sprite(AssetManager::GetTexture("1.png"));

while (window.isOpen()) {
// Game loop
}

// main 函数返回时,manager 被销毁
return 0;
}

sprite3 不会从磁盘读取,而是从 texture 缓存中读取,提高了效率。

</br>

下一节将会介绍动画精灵😃。

文章作者: Sshpark
文章链接: http://sshpark.com.cn/2019/08/28/用C-和SFML写游戏-纹理及精灵(5-2)/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Sshpark