字符雨 铜牌收录

程序说明

  • 字符雨绘图小程序,字符随机生成。
  • 程序开始后会自动全屏,隐藏窗口边框,看不到右上角关闭窗口的图标,按下任意按键程序结束。修改 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;

评论 (3) -

  • 复制粘贴,运行了下,
    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() {};
    };
    • 这行只是想初始化一下 m_position 变量的值为 {0.0f, 0.0f };
      我是在 VS2019 默认的 C++ 14 的环境上,可以编译通过。

添加评论