字符雨
2021-12-7 ~ 2022-1-10
(4)
程序说明
- 字符雨绘图小程序,字符随机生成。
- 程序开始后会自动全屏,隐藏窗口边框,看不到右上角关闭窗口的图标,按下任意按键程序结束。修改 useNoBorderWindow 变量可以取消这种默认设定,自由更改窗口大小
- 程序环境: VS 2019、EasyX_20211109、C++11
实现思路
- 字符在屏幕上方随机生成、然后以随机设定的速度(像素/秒)下落。
- 字符的颜色渐变由设定的 Fx() 函数决定,初始使用 Fx() = x * x。
- 每帧字符是否生成基于当前雨滴数量(C)、最大雨滴数量(M)、最小雨滴数量(N)决定。决定函数为 f(random x) <= p, x= [0, 1], p = (M - C) / (M - N),本程序中取 _Fx() = 3x ^ 2 - 2x ^ 3。
执行效果
源代码
/*
* 程序名称:字符雨
* 环 境:VS2019, EasyX 2021 版, C++11
* 作 者:孙小鱼(QQ:1226598193)
* 版 权:作者原创,无抄袭,不涉及版权问题
* 完成时间:2021-12-07
*/
#include <iostream>
#include <graphics.h>
#include <Windows.h>
#include <vector>
#include <string>
#include <map>
#include <cmath>
#include <conio.h>
#include <ctime>
using namespace std;
#pragma region program config
bool useNoBorderWindow = true; // 是否使用无边框窗口(true 为去除边框)
int xScreen = GetSystemMetrics(SM_CXSCREEN); // 屏幕宽度(像素)
int yScreen = GetSystemMetrics(SM_CYSCREEN); // 屏幕高度(像素)
const int rainMaxNum = 100; // 屏幕上随机生成的雨滴的最大数量(太大会影响性能)
const int rainMinNum = 50; // 屏幕上随机生成的雨滴的最小数量
const int characterMaxNum = 25; // 每个雨滴中的字符最大数量(太大会影响性能)
const int characterMinNum = 5; // 每个雨滴中的字符最小数量
const float randomUp = -20.0f; // 随机生成节点的上边界
const float randomLeft = 0; // 随机生成节点的左边界
const float randomBottom = 0; // 随机生成节点的下边界
const float randomRight = (float)xScreen; // 随机生成节点的右边界
const float rainMaxSize = 55; // 字体最大大小
const float rainMinSize = 25; // 字体最小大小
const float rainMaxSpeed = yScreen / 1.2f; // 像素 /(秒)
const float rainMinSpeed = yScreen / 8.0f;
const float ratioWH = 0.5f; // 字符得宽高比
#pragma endregion
#pragma region Graph Function
HWND hWnd;
inline void syOuttextxy(float x, float y, TCHAR ch);
void CreateCanvas();
void DestoryCanvas();
#pragma endregion
struct Rect
{
union
{
struct { float x1, y1, x2, y2; };
struct { float left, up, right, bottom; };
};
};
struct Point
{
float x, y;
Point(float _x = 0.0f, float _y = 0.0f) : x(_x), y(_y) {}
Point(const Point& pos) : x(pos.x), y(pos.y) {}
};
// 每一个可绘制对象都要继承自该类
class DrawAble
{
protected:
Point m_position;
public:
void SetPosition(const Point& pos) { m_position = pos; }
Point GetPosition() { return m_position; }
public:
DrawAble() : m_position({ 0.0f, 0.0f }) {}
virtual void Draw() = 0;
virtual void Update() = 0;
virtual ~DrawAble() {};
};
struct CompareRainNode; // 超前声明
/* 字符结构 */
class RainNode : public DrawAble
{
friend struct CompareRainNode;
private:
vector<TCHAR> m_characters; // 所有的字符
COLORREF m_color; // 字符颜色
float m_size; // 字符大小
float m_speed; // 字符下落速度
bool m_isActive; // 当前字符是否还在屏幕显示范围内
public:
void Draw() override;
void Update() override;
public:
RainNode();
void SetAttributes(const vector<TCHAR>& characters, COLORREF color, float size, float speed, Point pos = { 0.0f, 0.0f });
bool IsActive() { return m_isActive; }
void SetActive(bool active) { m_isActive = active; }
private:
void _UpdateActive(); // 更新是否在屏幕内的标志
float Fx(float x) { return x * x; } // 字符串变色的插值函数
};
/* 使用 Map 之前,我们需要提供比较运算符*/
struct CompareRainNode
{
bool operator()(const RainNode* a,const RainNode* b) const
{
return a < b;
}
};
/* 字符管理 */
class RainManager
{
private:
static RainManager* m_instance;
vector<RainNode*> m_rainPool;
map<RainNode*, DrawAble*, CompareRainNode> m_rains;
public:
static RainManager& GetInstance();
~RainManager();
void Frame();
private:
RainManager();
RainManager(const RainManager& rainManager) = delete;
RainManager& operator=(const RainManager& rainManager) = delete;
void Update(); // 每一帧对所有的字符雨进行更新、删除不在屏幕上的雨滴
void Draw(); // 每一帧进行绘制
private:
void* _GetRainFromPool(); // 从雨滴池子中获取一个新雨滴
void _GenRain(float p); // 以概率函数 Fx(x) <= p 产生一个新雨滴, p = [0, 1]
float _Fx(float x); // 定义域为 x = [0, 1], 值域为 Fx = [0, 1] 的函数
void _RandomInitRainNode(const Rect& rec, RainNode & rainNode); // 在 rec 区域内随机产生一个字符雨节点
};
/* 帧率计算 */
class Time
{
private:
static Time* m_instance;
public:
static Time& GetInstance();
private:
double m_deltaTime;
double m_time;
LARGE_INTEGER m_tickPerSecond;
LARGE_INTEGER m_curTick;
LARGE_INTEGER m_lastTick;
LARGE_INTEGER m_startTick;
public:
void Update(); // 每帧更新时间
double deltaTime();
double time();
private:
Time();
Time(const Time& t) = delete;
Time& operator=(const Time& t) = delete;
};
int main()
{
float nextTime = 0;
double frame;
TCHAR tcFrame[16];
CreateCanvas();
while (!_kbhit())
{
cleardevice(); // 清屏
Time::GetInstance().Update(); // 更新时间
RainManager::GetInstance().Frame(); // 字符每帧的更新、绘制
if (Time::GetInstance().time() > nextTime) // 绘制 FPS
{
nextTime += 1.0f;
frame = 1.0 / Time::GetInstance().deltaTime();
wsprintf(tcFrame, L"FPS:%d\n", (int)(frame + 0.5));
}
settextcolor(GREEN);
settextstyle(15, 0, L"宋体");
outtextxy(1, 1, tcFrame);
FlushBatchDraw();
}
DestoryCanvas();
return 0;
}
#pragma region Graph Function
inline void syOuttextxy(float x, float y, TCHAR ch)
{
outtextxy((int)(x + 0.5f), (int)(y + 0.5f), ch);
}
void CreateCanvas()
{
hWnd = initgraph(xScreen, yScreen);
if (useNoBorderWindow)
{
// 设置窗口没有边界、最大化窗口
int screenWidth = GetSystemMetrics(SM_CXSCREEN); // 屏幕宽度(像素)
int screenHeight = GetSystemMetrics(SM_CYSCREEN); // 屏幕高度(像素)
SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) & (~WS_CAPTION));
SetWindowPos(hWnd, HWND_TOP, (screenWidth - xScreen) >> 1, (screenHeight - yScreen) >> 1, xScreen, yScreen, SWP_SHOWWINDOW);
}
BeginBatchDraw();
setbkmode(TRANSPARENT);
srand((unsigned int)time(nullptr));
Time::GetInstance();
}
void DestoryCanvas()
{
EndBatchDraw();
closegraph();
}
#pragma endregion
void RainNode::Draw()
{
settextstyle((int)(m_size + 0.5f), 0, L"新宋体");
for (int i = 0; i < m_characters.size(); ++i)
{
float u = Fx( (m_characters.size() - i - 1.0f) / (m_characters.size() - 1.0f));
settextcolor(RGB(GetRValue(m_color) * u, GetGValue(m_color) * u, GetBValue(m_color) * u));
syOuttextxy(m_position.x, m_position.y - (i + 1) * m_size, m_characters[i]);
}
}
void RainNode::Update()
{
// 更新坐标信息
this->m_position.y += this->m_speed * (float)Time::GetInstance().deltaTime();
// 更新是否在屏幕内的标记
_UpdateActive();
}
RainNode::RainNode() : m_characters(vector<TCHAR>(0)), m_color(WHITE), m_size(0.0f), m_speed(0.0f), m_isActive(false){ }
void RainNode::SetAttributes(const vector<TCHAR>& characters, COLORREF color, float size, float speed, Point pos)
{
this->m_position = pos;
this->m_characters.resize(0);
this->m_characters.insert(m_characters.end(), characters.begin(), characters.end());
this->m_color = color;
this->m_speed = speed;
this->m_size = size;
_UpdateActive();
}
void RainNode::_UpdateActive()
{
// 字符最下面的那个字的左下角坐标为 m_position
if (m_position.x >= max(xScreen, randomRight) || m_position.y <= min(0, randomUp)) // 上边界、右边界
{
m_isActive = false;
return;
}
if (m_position.x + m_size <= min(randomLeft, 0) || m_position.y - m_size * m_characters.size() >= max(yScreen, randomBottom)) // 左边界、下边界
{
m_isActive = false;
return;
}
m_isActive = true;
}
RainManager* RainManager::m_instance = nullptr;
RainManager::RainManager()
{
m_rainPool.resize(rainMaxNum);
for (auto it = m_rainPool.begin(); it != m_rainPool.end(); it++)
{
(*it) = new RainNode;
(*it)->SetActive(false);
}
}
RainManager& RainManager::GetInstance()
{
if (m_instance == nullptr)
{
m_instance = new RainManager;
}
return *m_instance;
}
RainManager::~RainManager()
{
for (auto it = m_rainPool.begin(); it != m_rainPool.end(); ++it)
{
delete (*it);
}
}
void RainManager::Frame()
{
/* 绘制字符 */
Draw();
/* 更新字符 */
Update();
/* 决定是否需要产生新字符 */
float p = (rainMaxNum - m_rains.size() ) / (float)(rainMaxNum - rainMinNum);
if (p > 1.0f)
{
p = 1.0f;
}
if (p < 0.0f)
{
p = 0.0f;
}
_GenRain(p);
}
void RainManager::Update()
{
for (auto it = m_rains.begin(); it != m_rains.end(); )
{
it->second->Update();
if (! ((RainNode*)it->second)->IsActive())
{
it = m_rains.erase(it); // 这里删除,只是把雨滴放入了我们的 rainPool 中,并没有释放内存
}
else
{
it++;
}
}
}
void RainManager::Draw()
{
for (auto it = m_rains.begin(); it != m_rains.end(); ++it)
{
it->second->Draw();
}
}
void* RainManager::_GetRainFromPool()
{
for (auto it = m_rainPool.begin(); it != m_rainPool.end(); ++it)
{
if (!(*it)->IsActive())
{
return (*it);
}
}
return nullptr;
}
void RainManager::_GenRain(float p)
{
float x = (rand() % 10001) / 10000.0f; // tmp = [0.0f, 1.0f]
if (_Fx(x) > p)
{
return;
}
RainNode* tmp = (RainNode*)_GetRainFromPool();
if (tmp == nullptr)
{
return;
}
_RandomInitRainNode({randomLeft, randomUp, randomRight, randomBottom}, *tmp);
auto debug = m_rains.insert(make_pair(tmp, (DrawAble*)tmp));
}
float RainManager::_Fx(float x)
{
return (float)((3.0 - 2 * (double)x) * (double)x * (double)x);
}
void RainManager::_RandomInitRainNode(const Rect& rec, RainNode& rainNode)
{
float x = (rand() % (int)(rec.right - rec.left)) + rec.left;
float y = (rand() % (int)(rec.bottom - rec.up)) + rec.up;
static const TCHAR wcharTable[53] = L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
int len = rand() % (characterMaxNum - characterMinNum + 1) + characterMinNum;
vector<TCHAR> tmpChars(len);
for (int i = 0; i < len; ++i)
{
tmpChars[i] = wcharTable[rand() % 52];
}
COLORREF tmpColor = RGB(rand() % 56 + 200, rand() % 56 + 200, rand() % 56 + 200);
float tmpSize = rand() % (int)(rainMaxSize - rainMinSize + 1) + rainMinSize;
float tmpSpeed = rand() % (int)(rainMaxSpeed - rainMinSpeed + 1) + rainMinSpeed;
rainNode.SetAttributes(tmpChars, tmpColor,tmpSize, tmpSpeed, Point(x, y));
rainNode.SetActive(true);
}
Time& Time::GetInstance()
{
if (m_instance == nullptr)
{
m_instance = new Time;
}
return *m_instance;
}
void Time::Update()
{
QueryPerformanceCounter(&m_curTick);
m_time = (m_curTick.QuadPart - m_startTick.QuadPart) / (double)m_tickPerSecond.QuadPart;
m_deltaTime = (m_curTick.QuadPart - m_lastTick.QuadPart) / (double)m_tickPerSecond.QuadPart;
m_lastTick = m_curTick;
}
double Time::deltaTime()
{
return m_deltaTime;
}
double Time::time()
{
return m_time;
}
Time::Time()
{
QueryPerformanceFrequency(&m_tickPerSecond);
QueryPerformanceCounter(&m_startTick);
m_curTick.QuadPart = m_startTick.QuadPart;
m_lastTick.QuadPart = m_startTick.QuadPart - 1; // 计算第一帧时,防止除零错误
Update();
}
Time* Time::m_instance = nullptr;
class DrawAble
{
protected:
Point m_position;
public:
void SetPosition(const Point& pos) { m_position = pos; }
Point GetPosition() { return m_position; }
public:
DrawAble() : m_position({ 0.0f, 0.0f }) {} //-----------------------------此行报错 ,不明所以
virtual void Draw() = 0;
virtual void Update() = 0;
virtual ~DrawAble() {};
};
我是在 VS2019 默认的 C++ 14 的环境上,可以编译通过。