小游戏和GUI编程(4) | 基于 SFML 的黑客帝国字符雨

小游戏和GUI编程(4) | 基于 SFML 的黑客帝国字符雨

文章目录

  • 小游戏和GUI编程(4) | 基于 SFML 的黑客帝国字符雨
    • 1. 简介
    • 2. 规划
    • 3. 一个字符的下落
    • 3. 一个雨滴的下落
    • 4. 每个雨滴都是独一无二的
    • 5. 重构后, 降落多个雨滴
    • 6. 总结
    • 7. 参考

1. 简介

使用 SFML 实现黑客帝国字符雨的动态下落效果。

2. 规划

  • 一个字符下落
  • 一个雨滴的下落
  • 每个雨滴都是独一无二的
  • 重构后, 降落多个雨滴

3. 一个字符的下落

小游戏和GUI编程(4) | 基于 SFML 的黑客帝国字符雨_第1张图片

显示 SFML 窗口, 在窗口的中轴线上绘制一个降落的 H 字母, 字母是绿色的, 背景是黑色的。 每更新一帧, H 的位置就下降 16 个像素。 H 字母的大小也是 16 像素。

由于降落的比较快, 手动控制帧率为 10 FPS.

关键代码:

window.setFramerateLimit(10);

...

window.clear();

sf::Text text;
text.setFont(font);
text.setFillColor(sf::Color::Green);
text.setCharacterSize(16);
text.setString(std::string(1, 'H'));
text.setPosition(x, y);
y = (y + 16) % win_height;
window.draw(text);

window.display();

完整代码:

#include 

int main()
{
    constexpr int win_width = 640;
    constexpr int win_height = 480;
    sf::VideoMode videomode(win_width, win_height);
    const std::string title = "Matrix Rain SFML";
    sf::RenderWindow window(videomode, title);
    window.setFramerateLimit(10);

    sf::Font font;
    const std::string asset_dir = "../Resources";
    if (!font.loadFromFile(asset_dir + "/Arial.ttf"))
    {
        printf("Error: font not found\n");
        return 1;
    }

    int x = win_width / 2;
    int y = 0;
    while (window.isOpen())
    {
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }
        }
        
        window.clear();

        sf::Text text;
        text.setFont(font);
        text.setFillColor(sf::Color::Green);
        text.setCharacterSize(16);
        text.setString(std::string(1, 'H'));
        text.setPosition(x, y);
        y = (y + 16) % win_height;
        window.draw(text);

        window.display();
    }

    return 0;
}

3. 一个雨滴的下落

一个雨滴的下落, 说的是一个动态的过程, 由于它下落的很快, 可以同时显示它的多个位置, 因此纵向方向绘制多个字符, 就表示出了一个时间间隔内的雨滴降落轨迹。

    std::string seq = "abcdefghijklmnopqrstuvwxyz";
    for (int i = 0; i < seq.size(); i++)
    {
        sf::Text text;
        text.setFont(font);
        text.setFillColor(sf::Color::Green);
        text.setCharacterSize(16);
        text.setString(std::string(1, seq[i]));
        y = i * 16;
        text.setPosition(x, y);
        window.draw(text);
    }

雨滴轨迹中最下方的是现在的位置, 颜色最深, 最上面的颜色最浅。 因此可以根据雨滴的的长度和位置, 使用渐变的颜色, 来表达雨滴的轨迹:

    std::string seq = "abcdefghijklmnopqrstuvwxyz";
    for (int i = 0; i < seq.size(); i++)
    {
        sf::Text text;
        text.setFont(font);
        text.setFillColor(sf::Color(0, ((i+1) * 1.0/seq.size())*255, 0));
        text.setCharacterSize(16);
        text.setString(std::string(1, seq[i]));
        y = i * 16;
        text.setPosition(x, y);
        window.draw(text);
    }

雨滴的轨迹, 是从上往下降落的, 因此当我们只关注单个雨滴的颜色最深的绿色字符时, 它的位置应该是从上往下的:

int base_y = 0;

while(window.isOpen())
{
    std::string seq = "hello world";
    for (int i = 0; i < seq.size(); i++)
    {
        sf::Text text;
        text.setFont(font);
        text.setFillColor(sf::Color(0, ((i+1) * 1.0/seq.size())*255, 0));
        text.setCharacterSize(16);
        text.setString(std::string(1, seq[i]));
        y = i * 16 + base_y;
        text.setPosition(x, y);
        window.draw(text);
    }
    base_y = (base_y + 16) % win_height;
}

效果:
小游戏和GUI编程(4) | 基于 SFML 的黑客帝国字符雨_第2张图片

4. 每个雨滴都是独一无二的

每个雨滴对应的字符串是不同的(字符数量, 字符内容), 出现位置也不相同。

对于单个雨滴, 首先随机化它的出现位置, 也就是 x 坐标; 而在这个雨滴降落到窗口最下方之前, x 坐标需要保持不变; 当降落到窗口最下方时, 重新随机化 x 坐标:

x = rand() % (win_width / 16) * 16;

while (window.isOpen())
{
    std::string seq = "hello world";
    for (int i = 0; i < seq.size(); i++)
    {
        sf::Text text;
        text.setFont(font);
        text.setFillColor(sf::Color(0, ((i+1) * 1.0/seq.size())*255, 0));
        text.setCharacterSize(16);
        text.setString(std::string(1, seq[i]));
        y = i * 16 + base_y;
        text.setPosition(x, y);
        window.draw(text);
    }
    base_y = base_y + 16;
    if (base_y >= win_height)
    {
        base_y = 0;
        x = rand() % (win_width / 16) * 16;
    }
}

小游戏和GUI编程(4) | 基于 SFML 的黑客帝国字符雨_第3张图片

对于字符内容、 字符长度, 也需要在每次雨滴到达窗口最下方的时候更新:

static std::string get_random_seq(int& seqlen)
{
    seqlen = rand() % 10 + 5;
    std::string seq(seqlen, ' ');
    for (int i = 0; i < seq.size(); i++)
    {
        seq[i] = (rand() % 26) + 'a';
    }
    return seq;
}

std::string seq = get_random_seq(seqlen);
int base_y = 0;
while (window.isOpen())
{
    window.clear();
    for (int i = 0; i < seq.size(); i++)
    {
        sf::Text text;
        text.setFont(font);
        text.setFillColor(sf::Color(0, ((i+1) * 1.0/seq.size())*255, 0));
        text.setCharacterSize(16);
        text.setString(std::string(1, seq[i]));
        y = i * 16 + base_y;
        text.setPosition(x, y);
        window.draw(text);
    }
    base_y = base_y + 16;
    if (base_y >= win_height)
    {
        base_y = 0;
        x = rand() % (win_width / 16) * 16;
        seq = get_random_seq(seqlen);
    }
    window.display();
}

5. 重构后, 降落多个雨滴

首先, 针对单个雨滴的降落, 把之前的面向过程的写法重构为 OOP 写法. 通过封装为 class Rain, 提供的 drop() 方法完成了渲染, 使得 main loop 代码变得清爽:

#include 

static std::string get_random_seq(int& seqlen)
{
    seqlen = rand() % 10 + 5;
    std::string seq(seqlen, ' ');
    for (int i = 0; i < seq.size(); i++)
    {
        seq[i] = (rand() % 26) + 'a';
    }
    return seq;
}

class Rain
{
public:
    Rain(int a_win_width, int a_win_height, sf::Font& a_font)
    {
        seqlen = rand() % 10 + 5;
        seq = get_random_seq(seqlen);
        x = rand() % (win_width / 16) * 16;
        font = a_font;
        base_y = 0;
        win_width = a_win_width;
        win_height = a_win_height;
    }

    std::string seq;
    int seqlen;
    int x;
    sf::Font font;
    int base_y;
    int y = 0;
    int win_width;
    int win_height;

    void drop(sf::RenderWindow& window)
    {
        for (int i = 0; i < seq.size(); i++)
        {
            sf::Text text;
            text.setFont(font);
            text.setFillColor(sf::Color(0, ((i+1) * 1.0/seq.size())*255, 0));
            text.setCharacterSize(16);
            text.setString(std::string(1, seq[i]));
            y = i * 16 + base_y;
            text.setPosition(x, y);
            window.draw(text);
        }
        base_y = base_y + 16;
        if (base_y >= win_height)
        {
            base_y = 0;
            x = rand() % (win_width / 16) * 16;
            seq = get_random_seq(seqlen);
        }
    }
};

int main()
{
    srand((unsigned)time(NULL));
    constexpr int win_width = 640;
    constexpr int win_height = 480;
    sf::VideoMode videomode(win_width, win_height);
    const std::string title = "Matrix Rain SFML";
    sf::RenderWindow window(videomode, title);
    window.setFramerateLimit(10);

    sf::Font font;
    const std::string asset_dir = "../Resources";
    if (!font.loadFromFile(asset_dir + "/Arial.ttf"))
    {
        printf("Error: font not found\n");
        return 1;
    }

    Rain rain(win_width, win_height, font);
    while (window.isOpen())
    {
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }
        }
        
        window.clear(sf::Color::Black);

        rain.drop(window);

        window.display();
    }

    return 0;
}

其次, 为了渲染多个雨滴, 每个雨滴的都需要执行 drop() 函数。 由于 drop() 函数里的最后判断重置的代码, 和构造函数里重复了, 因此进一步提取出 reset() 成员函数。

最终代码

#include 

static std::string get_random_seq(int& seqlen)
{
    seqlen = rand() % 20 + 8;
    std::string seq(seqlen, ' ');
    for (int i = 0; i < seq.size(); i++)
    {
        seq[i] = (rand() % 26) + 'a';
    }
    return seq;
}

class Rain
{
public:
    Rain(int a_win_width, int a_win_height, sf::Font& a_font)
    {
        srand((unsigned)time(NULL));
        font = a_font;
        win_width = a_win_width;
        win_height = a_win_height;
        reset();
    }

    void reset()
    {
        seq = get_random_seq(seqlen);
        x = rand() % (win_width / 16) * 16;
        base_y = 0;
    }

    std::string seq;
    int seqlen;
    int x;
    sf::Font font;
    int base_y;
    int y = 0;
    int win_width;
    int win_height;

    void drop(sf::RenderWindow& window)
    {
        for (int i = 0; i < seq.size(); i++)
        {
            sf::Text text;
            text.setFont(font);
            text.setFillColor(sf::Color(0, ((i+1) * 1.0/seq.size())*255, 0));
            text.setCharacterSize(16);
            text.setString(std::string(1, seq[i]));
            y = i * 16 + base_y;
            text.setPosition(x, y);
            window.draw(text);

            if (y >= win_height)
            {
                reset();
                break;
            }
        }
        base_y += 16;
    }
};

int main()
{
    constexpr int win_width = 800;
    constexpr int win_height = 600;
    sf::VideoMode videomode(win_width, win_height);
    const std::string title = "Matrix Rain SFML";
    sf::RenderWindow window(videomode, title);
    window.setFramerateLimit(10);

    sf::Font font;
    const std::string asset_dir = "../Resources";
    if (!font.loadFromFile(asset_dir + "/Arial.ttf"))
    {
        printf("Error: font not found\n");
        return 1;
    }

    std::vector<Rain> rains(40, Rain(win_width, win_height, font));
    for (int i = 0; i < rains.size(); i++)
    {
        rains[i].reset();
    }
    while (window.isOpen())
    {
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }
        }
        
        window.clear(sf::Color::Black);

        for (auto& rain: rains)
        {
            rain.drop(window);
        }

        window.display();
    }

    return 0;
}

最终效果:

6. 总结

本文提供了基于 SFML 的仿黑客帝国的雨滴下落效果的的动态窗口显示, 从最简单的单个字符下落显示, 到最终的多雨滴、随机出现位置、序列长度随机化, 每一步都给出了代码和基本的解释。 在简介阶段只做了初步的任务划分, 在每一小节则进一步细化需求, 通过一步或多步实现了效果, 并在必要的时候(单个雨滴到多个雨滴)时, 通过重构代码, 降低了代码调用的复杂度, 从而为多个雨滴的降落效果提供了便利。

7. 参考

  • https://www.cnblogs.com/zjutzz/p/17067234.html

你可能感兴趣的:(SFML,字符雨,黑客帝国,EasyX)