使用 SFML 实现黑客帝国字符雨的动态下落效果。
显示 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;
}
一个雨滴的下落, 说的是一个动态的过程, 由于它下落的很快, 可以同时显示它的多个位置, 因此纵向方向绘制多个字符, 就表示出了一个时间间隔内的雨滴降落轨迹。
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;
}
每个雨滴对应的字符串是不同的(字符数量, 字符内容), 出现位置也不相同。
对于单个雨滴, 首先随机化它的出现位置, 也就是 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;
}
}
对于字符内容、 字符长度, 也需要在每次雨滴到达窗口最下方的时候更新:
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();
}
首先, 针对单个雨滴的降落, 把之前的面向过程的写法重构为 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;
}
最终效果:
本文提供了基于 SFML 的仿黑客帝国的雨滴下落效果的的动态窗口显示, 从最简单的单个字符下落显示, 到最终的多雨滴、随机出现位置、序列长度随机化, 每一步都给出了代码和基本的解释。 在简介阶段只做了初步的任务划分, 在每一小节则进一步细化需求, 通过一步或多步实现了效果, 并在必要的时候(单个雨滴到多个雨滴)时, 通过重构代码, 降低了代码调用的复杂度, 从而为多个雨滴的降落效果提供了便利。