SFML2.6 图形模块--用顶点数组设计实体

介绍

SFML提供了简单的类来表示最常见的2D实体。虽然更复杂的实体可以轻松地从这些基本组件创建,但这并不总是最有效的解决方案。例如,如果绘制大量的精灵,则很快会达到图形卡的限制。原因是性能在很大程度上取决于调用绘制函数的次数。确实,每次调用都涉及设置一组OpenGL状态,重置矩阵,更改纹理等等。即使仅绘制两个三角形(精灵),所有这些都是必需的。这对于您的图形卡来说远非最佳:今天的GPU设计用于处理大量的三角形,通常是几千到数百万个。

为了填补这一差距,SFML提供了一种更低级别的机制来绘制东西:顶点数组。实际上,顶点数组在所有其他SFML类中都被内部使用。它们允许更灵活地定义2D实体,包含您需要的任意数量的三角形。它们甚至允许绘制点或线条。

什么是顶点,为什么它们总是在数组中?

顶点是您可以操作的最小图形实体。简而言之,它是一个图形点:它自然具有2D位置(x,y),但也具有颜色和一对纹理坐标。稍后我们将讨论这些属性的作用。

单独的顶点并不能做太多事情。它们总是被组合成图元:点(1个顶点),线(2个顶点),三角形(3个顶点)或四边形(4个顶点)。然后,您可以将多个图元组合在一起,创建实体的最终几何形状。

现在您理解了为什么我们总是谈论顶点数组,而不是单独的顶点。

一个简单的顶点数组

现在让我们看看sf :: Vertex类。它只是一个容器,包含三个公共成员和除构造函数之外没有任何函数。这些构造函数允许您从您关心的属性集中构造顶点 - 您并不总是需要为实体着色或打上纹理。

// create a new vertex
sf::Vertex vertex;

// set its position
vertex.position = sf::Vector2f(10.f, 50.f);

// set its color
vertex.color = sf::Color::Red;

// set its texture coordinates
vertex.texCoords = sf::Vector2f(100.f, 100.f);

…或者,使用正确的构造函数:

sf::Vertex vertex(sf::Vector2f(10.f, 50.f), sf::Color::Red, sf::Vector2f(100.f, 100.f));

现在,让我们定义一个图元。请记住,一个图元由多个顶点组成,因此我们需要一个顶点数组。SFML为此提供了一个简单的包装器:sf :: VertexArray。它提供了类似于std :: vector的数组语义,并且还存储其顶点定义的图元类型。

// create an array of 3 vertices that define a triangle primitive
sf::VertexArray triangle(sf::Triangles, 3);

// define the position of the triangle's points
triangle[0].position = sf::Vector2f(10.f, 10.f);
triangle[1].position = sf::Vector2f(100.f, 10.f);
triangle[2].position = sf::Vector2f(100.f, 100.f);

// define the color of the triangle's points
triangle[0].color = sf::Color::Red;
triangle[1].color = sf::Color::Blue;
triangle[2].color = sf::Color::Green;

// no texture coordinates here, we'll see that later

你的三角形已经准备好,现在可以开始绘制它了。绘制顶点数组与绘制任何其他SFML实体类似,可以使用draw函数:

window.draw(triangle);

在这里插入图片描述
可以看到,顶点的颜色被插值以填充图元。这是创建渐变的不错方式。

请注意,你不一定要使用sf::VertexArray类,它只是为方便而定义的,它本质上只是一个带有sf::PrimitiveType的std::vector< sf::Vertex >。如果你需要更多的灵活性或者静态数组,你可以使用自己的存储方式。然后,你必须使用draw函数的重载形式,该函数接受指向顶点、顶点数和图元类型的指针。

std::vector<sf::Vertex> vertices;
vertices.push_back(sf::Vertex(...));
...

window.draw(&vertices[0], vertices.size(), sf::Triangles);
sf::Vertex vertices[2] =
{
    sf::Vertex(...),
    sf::Vertex(...)
};

window.draw(vertices, 2, sf::Lines);

图元类型

让我们暂停一下,看看你可以创建哪些类型的基本几何图元。如上所述,你可以定义最基本的2D图元:点、线、三角形和四边形(四边形只是为了方便起见,内部图形卡会将其分成两个三角形)。还有“链式”变体,它们允许在两个连续的图元之间共享顶点。这样做的好处是,连续的图元通常在某种程度上是相互连接的。

让我们来看看完整的列表:

图元类型描述例子
sf::Points一组不相连的点。这些点没有厚度:无论当前的变换和视图如何,它们始终只占用一个像素。在这里插入图片描述
sf::Lines一组不相连的线。这些线没有厚度:无论当前的变换和视图如何,它们始终只有一个像素宽。在这里插入图片描述
sf::LineStrip一组相连的线。一个线段的结束顶点被用作下一个线段的起始顶点。在这里插入图片描述
sf::Triangles一组不相连的三角形。在这里插入图片描述
sf::TriangleStrip一组相连的三角形。每个三角形与下一个三角形共享其最后两个顶点。在这里插入图片描述
sf::TriangleFan一组连接到一个中心点的三角形。第一个顶点是中心,然后每个新顶点定义一个新的三角形,使用中心和前一个顶点。在这里插入图片描述
sf::Quads (已弃用)一组未连接的四边形。每个四边形的4个点必须一致地定义,可以顺时针或逆时针顺序。在这里插入图片描述

纹理

与其他SFML实体一样,顶点数组也可以进行纹理贴图。为此,您需要操作顶点的texCoords属性。该属性定义了哪个纹理像素被映射到顶点。

// create a triangle strip
sf::VertexArray triangleStrip(sf::TriangleStrip, 4);

// define it as a rectangle, located at (10, 10) and with size 100x100
triangleStrip[0].position = sf::Vector2f(10.f, 10.f);
triangleStrip[1].position = sf::Vector2f(10.f, 110.f);
triangleStrip[2].position = sf::Vector2f(110.f, 10.f);
triangleStrip[3].position = sf::Vector2f(110.f, 110.f);

// define its texture area to be a 25x50 rectangle starting at (0, 0)
triangleStrip[0].texCoords = sf::Vector2f(0.f, 0.f);
triangleStrip[1].texCoords = sf::Vector2f(0.f, 50.f);
triangleStrip[2].texCoords = sf::Vector2f(25.f, 0.f);
triangleStrip[3].texCoords = sf::Vector2f(25.f, 50.f);

纹理坐标是以像素为单位定义的(就像精灵和形状的textureRect一样)。它们没有规范化(在0到1之间),这可能会让习惯于OpenGL编程的人感到惊讶。

顶点数组是低级实体,只处理几何数据,不存储像纹理这样的其他属性。要使用纹理绘制一个顶点数组,必须将其直接传递给绘制函数:

sf::VertexArray vertices;
sf::Texture texture;

...

window.draw(vertices, &texture);

这是简短的版本,如果您需要传递其他渲染状态(例如混合模式或变换),则可以使用显式版本,该版本接受一个sf::RenderStates对象:

sf::VertexArray vertices;
sf::Texture texture;

...

sf::RenderStates states;
states.texture = &texture;

window.draw(vertices, states);

变换顶点数组

Transforming(变换)与纹理类似,变换信息并不储存在顶点数组中,你必须将其传递给绘制函数。在使用 sf::Transform 类进行变换时,它所作用的是整个顶点数组,不会修改原始顶点数据。因此,在绘制时,你需要将变换信息通过 sf::RenderStates 对象传递给绘制函数,以实现顶点数组的变换。

sf::VertexArray vertices;
sf::Transform transform;

...

window.draw(vertices, transform);

或者,如果你需要传递其它的渲染状态:

sf::VertexArray vertices;
sf::Transform transform;

...

sf::RenderStates states;
states.transform = transform;

window.draw(vertices, states);

如果想了解更多有关变换和sf::Transform类的内容,你可以阅读有关变换实体的教程。

创建一个类似SFML的实体

现在您知道如何定义自己的纹理/着色/变换实体,那么将其包装在类中不是很好吗?幸运的是,SFML为您提供了sf::Drawable和sf::Transformable基类,使这一点变得容易。这两个类是内置SFML实体sf::Sprite、sf::Text和sf::Shape的基础。

sf::Drawable是一个接口:它声明一个纯虚函数,没有成员或具体函数。继承sf::Drawable允许您以与SFML类相同的方式绘制类的实例:

class MyEntity : public sf::Drawable
{
private:

    virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const;
};

MyEntity entity;
window.draw(entity); // internally calls entity.draw

请注意,这样做并不是强制性的,您也可以在您的类中拥有类似的draw函数,然后简单地使用entity.draw(window)进行调用。但是,使用sf::Drawable作为基类的方式更好、更一致。这也意味着,如果您计划存储一组可绘制的对象,您可以这样做而不需要任何额外的努力,因为所有可绘制的对象(SFML的和您的)都是从相同的类派生的。

另一个基类sf::Transformable没有虚函数。从它继承会自动将与其他SFML类相同的变换函数添加到您的类中(setPosition、setRotation、move、scale等)。您可以在转换实体的教程中了解更多关于这个类的知识。

使用这两个基类和一个顶点数组(在这个例子中,我们也会添加一个纹理),这是一个典型的类似SFML的图形类应该是这样的:

class MyEntity : public sf::Drawable, public sf::Transformable
{
public:

    // add functions to play with the entity's geometry / colors / texturing...

private:

    virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const
    {
        // apply the entity's transform -- combine it with the one that was passed by the caller
        states.transform *= getTransform(); // getTransform() is defined by sf::Transformable

        // apply the texture
        states.texture = &m_texture;

        // you may also override states.shader or states.blendMode if you want

        // draw the vertex array
        target.draw(m_vertices, states);
    }

    sf::VertexArray m_vertices;
    sf::Texture m_texture;
};

然后,您可以像使用内置的SFML类一样使用这个类:

MyEntity entity;

// you can transform it
entity.setPosition(10.f, 50.f);
entity.setRotation(45.f);

// you can draw it
window.draw(entity);

示例:瓷砖地图

有了以上所学的知识,我们可以创建一个封装瓷砖地图的类。整个地图将包含在一个顶点数组中,因此它的绘制速度将非常快。请注意,我们只有在整个瓷砖集合可以适合一个纹理时,才能应用这种策略。否则,我们至少需要使用一个纹理对应一个顶点数组。

以下是瓷砖地图类的声明:

class TileMap : public sf::Drawable, public sf::Transformable
{
public:

    bool load(const std::string& tileset, sf::Vector2u tileSize, const int* tiles, unsigned int width, unsigned int height)
    {
        // load the tileset texture
        if (!m_tileset.loadFromFile(tileset))
            return false;

        // resize the vertex array to fit the level size
        m_vertices.setPrimitiveType(sf::Triangles);
        m_vertices.resize(width * height * 6);

        // populate the vertex array, with two triangles per tile
        for (unsigned int i = 0; i < width; ++i)
            for (unsigned int j = 0; j < height; ++j)
            {
                // get the current tile number
                int tileNumber = tiles[i + j * width];

                // find its position in the tileset texture
                int tu = tileNumber % (m_tileset.getSize().x / tileSize.x);
                int tv = tileNumber / (m_tileset.getSize().x / tileSize.x);

                // get a pointer to the triangles' vertices of the current tile
                sf::Vertex* triangles = &m_vertices[(i + j * width) * 6];

                // define the 6 corners of the two triangles
                triangles[0].position = sf::Vector2f(i * tileSize.x, j * tileSize.y);
                triangles[1].position = sf::Vector2f((i + 1) * tileSize.x, j * tileSize.y);
                triangles[2].position = sf::Vector2f(i * tileSize.x, (j + 1) * tileSize.y);
                triangles[3].position = sf::Vector2f(i * tileSize.x, (j + 1) * tileSize.y);
                triangles[4].position = sf::Vector2f((i + 1) * tileSize.x, j * tileSize.y);
                triangles[5].position = sf::Vector2f((i + 1) * tileSize.x, (j + 1) * tileSize.y);

                // define the 6 matching texture coordinates
                triangles[0].texCoords = sf::Vector2f(tu * tileSize.x, tv * tileSize.y);
                triangles[1].texCoords = sf::Vector2f((tu + 1) * tileSize.x, tv * tileSize.y);
                triangles[2].texCoords = sf::Vector2f(tu * tileSize.x, (tv + 1) * tileSize.y);
                triangles[3].texCoords = sf::Vector2f(tu * tileSize.x, (tv + 1) * tileSize.y);
                triangles[4].texCoords = sf::Vector2f((tu + 1) * tileSize.x, tv * tileSize.y);
                triangles[5].texCoords = sf::Vector2f((tu + 1) * tileSize.x, (tv + 1) * tileSize.y);
            }

        return true;
    }

private:

    virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const
    {
        // apply the transform
        states.transform *= getTransform();

        // apply the tileset texture
        states.texture = &m_tileset;

        // draw the vertex array
        target.draw(m_vertices, states);
    }

    sf::VertexArray m_vertices;
    sf::Texture m_tileset;
};

现在我们来看使用这个类的应用程序:

int main()
{
    // create the window
    sf::RenderWindow window(sf::VideoMode(512, 256), "Tilemap");

    // define the level with an array of tile indices
    const int level[] =
    {
        0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
        0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0,
        1, 1, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3,
        0, 1, 0, 0, 2, 0, 3, 3, 3, 0, 1, 1, 1, 0, 0, 0,
        0, 1, 1, 0, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2, 0, 0,
        0, 0, 1, 0, 3, 0, 2, 2, 0, 0, 1, 1, 1, 1, 2, 0,
        2, 0, 1, 0, 3, 0, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1,
        0, 0, 1, 0, 3, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1,
    };

    // create the tilemap from the level definition
    TileMap map;
    if (!map.load("tileset.png", sf::Vector2u(32, 32), level, 16, 8))
        return -1;

    // run the main loop
    while (window.isOpen())
    {
        // handle events
        sf::Event event;
        while (window.pollEvent(event))
        {
            if(event.type == sf::Event::Closed)
                window.close();
        }

        // draw the map
        window.clear();
        window.draw(map);
        window.display();
    }

    return 0;
}

在这里插入图片描述
你可以在这里下载用于这个tilemap示例的瓷砖集。

例子:粒子系统

这个第二个示例实现了另一个常见实体:粒子系统。这个非常简单,没有纹理,尽可能少的参数。它演示了使用sf::Points原语类型与动态顶点数组,在每个帧中都会发生变化。

class ParticleSystem : public sf::Drawable, public sf::Transformable
{
public:

    ParticleSystem(unsigned int count) :
    m_particles(count),
    m_vertices(sf::Points, count),
    m_lifetime(sf::seconds(3.f)),
    m_emitter(0.f, 0.f)
    {
    }

    void setEmitter(sf::Vector2f position)
    {
        m_emitter = position;
    }

    void update(sf::Time elapsed)
    {
        for (std::size_t i = 0; i < m_particles.size(); ++i)
        {
            // update the particle lifetime
            Particle& p = m_particles[i];
            p.lifetime -= elapsed;

            // if the particle is dead, respawn it
            if (p.lifetime <= sf::Time::Zero)
                resetParticle(i);

            // update the position of the corresponding vertex
            m_vertices[i].position += p.velocity * elapsed.asSeconds();

            // update the alpha (transparency) of the particle according to its lifetime
            float ratio = p.lifetime.asSeconds() / m_lifetime.asSeconds();
            m_vertices[i].color.a = static_cast<sf::Uint8>(ratio * 255);
        }
    }

private:

    virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const
    {
        // apply the transform
        states.transform *= getTransform();

        // our particles don't use a texture
        states.texture = NULL;

        // draw the vertex array
        target.draw(m_vertices, states);
    }

private:

    struct Particle
    {
        sf::Vector2f velocity;
        sf::Time lifetime;
    };

    void resetParticle(std::size_t index)
    {
        // give a random velocity and lifetime to the particle
        float angle = (std::rand() % 360) * 3.14f / 180.f;
        float speed = (std::rand() % 50) + 50.f;
        m_particles[index].velocity = sf::Vector2f(std::cos(angle) * speed, std::sin(angle) * speed);
        m_particles[index].lifetime = sf::milliseconds((std::rand() % 2000) + 1000);

        // reset the position of the corresponding vertex
        m_vertices[index].position = m_emitter;
    }

    std::vector<Particle> m_particles;
    sf::VertexArray m_vertices;
    sf::Time m_lifetime;
    sf::Vector2f m_emitter;
};

还有一个使用它的小演示:

int main()
{
    // create the window
    sf::RenderWindow window(sf::VideoMode(512, 256), "Particles");

    // create the particle system
    ParticleSystem particles(1000);

    // create a clock to track the elapsed time
    sf::Clock clock;

    // run the main loop
    while (window.isOpen())
    {
        // handle events
        sf::Event event;
        while (window.pollEvent(event))
        {
            if(event.type == sf::Event::Closed)
                window.close();
        }

        // make the particle system emitter follow the mouse
        sf::Vector2i mouse = sf::Mouse::getPosition(window);
        particles.setEmitter(window.mapPixelToCoords(mouse));

        // update it
        sf::Time elapsed = clock.restart();
        particles.update(elapsed);

        // draw it
        window.clear();
        window.draw(particles);
        window.display();
    }

    return 0;
}

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面我会给你讲解如何使用SFML来实现n-puzzle游戏的可视化。首先,我们需要了解一下SFML是什么。 SFML是一个跨平台的图形库,它提供了简单易用的API,可以帮助我们快速创建2D游戏和图形应用程序。它支持多种编程语言,包括C++、Python、Java等。 接下来,我们可以开始实现n-puzzle游戏的可视化了。首先,我们需要创建一个窗口来显示游戏界面。以下是创建窗口的代码: ```cpp #include <SFML/Graphics.hpp> int main() { sf::RenderWindow window(sf::VideoMode(800, 600), "N-Puzzle Game"); while (window.isOpen()) { sf::Event event; while (window.pollEvent(event)) { if (event.type == sf::Event::Closed) window.close(); } window.clear(sf::Color::White); // 绘制游戏界面 window.display(); } return 0; } ``` 这段代码创建了一个800x600大小的窗口,并且在每一帧中都会清空窗口并绘制游戏界面。接下来,我们需要实现游戏界面的绘制。 n-puzzle游戏的界面由一个N*N的网格组成,每个格子中都会有一个数字。我们可以使用SFML中的矩形来表示每个格子,并且使用文本来显示数字。以下是绘制游戏界面的代码: ```cpp const int N = 3; const int tileSize = 100; sf::RectangleShape tile(sf::Vector2f(tileSize, tileSize)); tile.setFillColor(sf::Color::White); tile.setOutlineColor(sf::Color::Black); tile.setOutlineThickness(2); sf::Font font; font.loadFromFile("arial.ttf"); sf::Text text("", font, tileSize / 2); text.setFillColor(sf::Color::Black); text.setStyle(sf::Text::Bold); text.setOrigin(text.getGlobalBounds().width / 2, text.getGlobalBounds().height / 2); for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { int number = i * N + j + 1; if (number == N * N) continue; tile.setPosition(j * tileSize, i * tileSize); text.setString(std::to_string(number)); text.setPosition(j * tileSize + tileSize / 2, i * tileSize + tileSize / 2); window.draw(tile); window.draw(text); } } ``` 这段代码首先定义了每个格子的大小为100x100,并且使用白色填充和黑色边框。然后,它加载了一个字体文件,并且创建了一个文本对象来显示数字。最后,它使用两个嵌套循环来绘制整个游戏界面。 这样,我们就完成了n-puzzle游戏的可视化。完整代码如下: ```cpp #include <SFML/Graphics.hpp> const int N = 3; const int tileSize = 100; int main() { sf::RenderWindow window(sf::VideoMode(N * tileSize, N * tileSize), "N-Puzzle Game"); sf::RectangleShape tile(sf::Vector2f(tileSize, tileSize)); tile.setFillColor(sf::Color::White); tile.setOutlineColor(sf::Color::Black); tile.setOutlineThickness(2); sf::Font font; font.loadFromFile("arial.ttf"); sf::Text text("", font, tileSize / 2); text.setFillColor(sf::Color::Black); text.setStyle(sf::Text::Bold); text.setOrigin(text.getGlobalBounds().width / 2, text.getGlobalBounds().height / 2); while (window.isOpen()) { sf::Event event; while (window.pollEvent(event)) { if (event.type == sf::Event::Closed) window.close(); } window.clear(sf::Color::White); for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { int number = i * N + j + 1; if (number == N * N) continue; tile.setPosition(j * tileSize, i * tileSize); text.setString(std::to_string(number)); text.setPosition(j * tileSize + tileSize / 2, i * tileSize + tileSize / 2); window.draw(tile); window.draw(text); } } window.display(); } return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值