首先感谢顽石老师的讲解,十分清晰易懂,附上b站视频链接【C/C++游戏项目教程】趣味编程—黑客帝国数字雨,侵权请联系我删除。
以下为效果图:
效果视频如链接视频所示黑客帝国数字雨效果视频。
——————————————————
我用的VS2022写的,也遇到了一些问题,在后续代码中会尽量标注出来。
全部代码如下所示:
#define _CRT_SECURE_NO_WARNINGS 1#undef UNICODE //用于防止outtextxy报错#undef _UNICODE //用于防止outtextxy报错/*总步骤:1、数字雨在下落,雨是字符串,用rand()随机生成字符串2、用数组保存字符串*/#include <stdio.h>#include <stdlib.h>#include <time.h>#include <graphics.h> //非标准头文件,需要安装EasyX图形库才能使用#define STR_SIZE 20 //字符串大小(默认20)#define STR_NUM 512 //字符串条数(默认128)#define STR_WIDTH 15 //字符之间间隔(默认15)#define WIN_WIDTH 3120//窗口宽度(默认1024,此电脑为3120)#define WIN_HEIGHT 2080 //窗口高度(默认640,此电脑为2080)//用结构体存储字符串最合适struct Rain {int x, y; //雨的坐标int speed; //下落速度char str[STR_SIZE]; //每一串数字雨}rain[STR_NUM];//随机生成字符char CreateCh() {char temp = 0;int flag = rand() % 3; //0 1 2if (flag == 0) {temp = rand() % 26 + 'A'; //0-25}else if(flag == 1) {temp = rand() % 26 + 'a';}else {temp = rand() % 10 + '0';}return temp; //返回一个字符}//初始化游戏各个参数void GameInit() {//创建一个窗口initgraph(WIN_WIDTH, WIN_HEIGHT);//设置随机数种子,语句要放在函数里面srand((unsigned)time(NULL)); //初始化字符串for (int i = 0; i < STR_NUM; i++) {for (int k = 0; k < STR_SIZE; k++) {rain[i].str[k] = CreateCh(); //对数字雨逐个赋值}}//初始化坐标和速度for (int i = 0; i < STR_NUM; i++) {rain[i].x = i * STR_WIDTH; //0 15 30 45......rain[i].y = rand() % WIN_HEIGHT;rain[i].speed = rand() % 5 + 2; //防止为0,要加一个数}}//绘制画面void GameDraw() {for (int i = 0; i < STR_NUM; i++) {for (int k = 0; k < STR_SIZE; k++) {//设置背景颜色setbkmode(TRANSPARENT);//通过计算255/STR_WIDTH=13,即每个字符颜色相差13settextcolor(RGB(0, 255 - 13 * k, 0));//竖着打印出每条字符串,并且控制好间隔outtextxy(rain[i].x, rain[i].y - STR_WIDTH * k, rain[i].str[k]);}}}//随机改变已出现的字符串中的字符void ChangeCh() {for (int i = 0; i < STR_NUM; i++) {rain[i].str[rand() % STR_SIZE] = CreateCh();}}//使数字雨移动起来void RainMove() {for (int i = 0; i < STR_NUM; i++) {rain[i].y += rain[i].speed;if (rain[i].y - STR_WIDTH * STR_SIZE > WIN_HEIGHT) {rain[i].y = 0;}}}int main() {GameInit();BeginBatchDraw(); //开始双缓冲绘图//调用死循环让画面一直更新while (1) {cleardevice(); //每次更新要清除之前的画面GameDraw();FlushBatchDraw(); //绘制ChangeCh();RainMove();}EndBatchDraw(); //getchar(); //防止程序闪退return 0;}
———————————分割线———————————
接下来我将详细描述代码作用:
首先第一部分
#define _CRT_SECURE_NO_WARNINGS 1#undef UNICODE //用于防止outtextxy报错#undef _UNICODE //用于防止outtextxy报错
第一句是针对VS2022以及之前的一些版本都有一些情况,即遇到我们C/C++中常用语句都会报错,在.cpp文件开头加上这一句话可以消除警告并正常运行。
后两句是为了防止后续会用到的outtextxy()函数的报错,后续会详细介绍。
——————————————
#include <stdio.h>#include <stdlib.h>#include <time.h>#include <graphics.h> //非标准头文件,需要安装EasyX图形库才能使用
这些是需要用到的头文件,其中stdlib.h是用到了srand()来生成随机数种子,time.h也是帮助生成随机数种子,而graphics.h需要我们电脑安装EasyX,非常简单,进入EasyX官网,如下图:
点右边的圈直接下载,点左边的圈有安装教程,基本上支持全部版本的VS,连我的目前最新的VS2022也支持自动安装,十分方便。
其可以自动识别你的电脑里的VS版本,直接一键安装上去,就完成了。
————————————
#define STR_SIZE 20 //字符串大小(默认20)#define STR_NUM 512 //字符串条数(默认128)#define STR_WIDTH 15 //字符之间间隔(默认15)#define WIN_WIDTH 3120//窗口宽度(默认1024,此电脑为3120)#define WIN_HEIGHT 2080 //窗口高度(默认640,此电脑为2080)
用宏定义来定义一些后续代码经常用的一些固定量,你后续修改成自己喜欢的参数也可以直接在这里改。
我们的每条数字雨其实就是一个字符串,其中:
STR_SIZE为字符串大小,表示每条数字雨有多少个字符;
STR_NUM为字符串条数,即为全部一共有多少条数字雨在屏幕上;
STR_WIDTH为字符之间间隔,表示字符串中每个字符之间的间距,调整好后可以使数字雨更美观;
WIN_WIDTH和WIN_HEIGHT用来调整显示数字雨窗口的大小,我调成了我电脑屏幕分辨率的大小,你可以任意该,我是想把这个当屏保,但是最顶上的一条程序的白边还是无法去掉,若有可以去掉白边实现全屏的办法,欢迎分享。
————————————
//用结构体存储字符串最合适struct Rain {int x, y; //雨的坐标int speed; //下落速度char str[STR_SIZE]; //每一串数字雨}rain[STR_NUM];
前面我们说了每条数字雨就是一个字符串,但每条数字雨除了它所展示的字符,还有诸如这条雨的x,y坐标,以及其下落速度这些属性,所以我们用结构体struct来存储数字雨。然后便定义一个结构体变量rain[],其大小为数字雨的全部条数,即前面宏定义过的STR_NUM。
————————————
int main() {GameInit();BeginBatchDraw(); //开始双缓冲绘图//调用死循环让画面一直更新while (1) {cleardevice(); //每次更新要清除之前的画面GameDraw();FlushBatchDraw(); //绘制ChangeCh();RainMove();}EndBatchDraw(); //getchar(); //防止程序闪退return 0;}
先来看我们的主函数,我们依然把游戏的各个功能都封装为单独的函数,在主函数中直接调用即可,这样更简洁,而且后续要修改某一功能也更容易,这种思想在实际项目中很常见而且实用,在学习用其它语言例如Python写类似的游戏和项目时也会大量的运用这种思想。
接下来我们来看主函数中调用的各个函数的写法:
——————————————
//随机生成字符char CreateCh() {char temp = 0;int flag = rand() % 3; //0 1 2if (flag == 0) {temp = rand() % 26 + 'A'; //0-25}else if(flag == 1) {temp = rand() % 26 + 'a';}else {temp = rand() % 10 + '0';}return temp; //返回一个字符}
首先是最主要的生成字符的函数CreateCh(),其返回的就是字符char。
数字雨中的字符是随机的,所以我们用rand()模上3,会有0、1、2三种情况,分别生成大写字母、小写字母、数字,这样数字雨更加多样化,看起来也更炫酷。
——————————————
//初始化游戏各个参数void GameInit() {//创建一个窗口initgraph(WIN_WIDTH, WIN_HEIGHT);//设置随机数种子,语句要放在函数里面srand((unsigned)time(NULL)); //初始化字符串for (int i = 0; i < STR_NUM; i++) {for (int k = 0; k < STR_SIZE; k++) {rain[i].str[k] = CreateCh(); //对数字雨逐个赋值}}//初始化坐标和速度for (int i = 0; i < STR_NUM; i++) {rain[i].x = i * STR_WIDTH; //0 15 30 45......rain[i].y = rand() % WIN_HEIGHT;rain[i].speed = rand() % 5 + 2; //防止为0,要加一个数}}
游戏初始化函数GameInit(),用于在游戏每次开始时设定好一些参数,跟前面的宏定义有点类似,但不一样,因为各个地方都要用到的一些常量必须要先定义,它们便放在最一开始的宏定义中。
initgraph()用于创建一个你指定大小的窗口,我们指定的大小就已在宏定义中实现了。这个函数是EasyX中的。
srand()用来设置随机数种子,根据电脑的时间来变化,以保证每次程序运行时所产生的伪随机数是不一样的,让我们的数字雨看上去变幻莫测,不然每次打开程序都是一样的字符,没啥意思。
接下来的嵌套循环就是给每条数字雨的每个字符逐个赋值。
然后是初始化坐标和速度,电脑屏幕向右为x轴正方向,向下为y轴正方向。x坐标要保证相邻的数字雨保持一定间隔;y坐标要在窗口里随机,以此来营造出下雨的感觉;每条数字雨下落的速度也要随机,因为好看,但要防止速度为0,所以要加上一个大于0的数,这里取的模和加的数你可以自己调整,来适应你的窗口大小,以及你自己感觉看的舒服。
——————————————
//绘制画面void GameDraw() {for (int i = 0; i < STR_NUM; i++) {for (int k = 0; k < STR_SIZE; k++) {//设置背景颜色setbkmode(TRANSPARENT);//通过计算255/STR_WIDTH=13,即每个字符颜色相差13settextcolor(RGB(0, 255 - 13 * k, 0));//竖着打印出每条字符串,并且控制好间隔outtextxy(rain[i].x, rain[i].y - STR_WIDTH * k, rain[i].str[k]);}}}
这个绘制画面函数GameDraw()主要用来设置颜色,并通过EasyX中的outtextxy()来打印出每条数字雨。
我们对每条字符串中的绿色设置的是渐变暗的颜色。
这里尤其要注意这个outtextxy(),直接调用都会报错,详细见outtextxy编译错误,感谢分享。不过我试了好多次第三种方法,我的VS就是持续报错,所以我用的方法二,即在代码最前面加上如下代码便可解决问题:
#undef UNICODE //用于防止outtextxy报错#undef _UNICODE //用于防止outtextxy报错
接着,我们要解决要竖着打印字符串的问题,即在打印时让第k个字符y坐标距离字符串首个y坐标k倍的间隔长度,即第一个字符在下面,第二个在第一个的上面1倍的STR_WIDTH距离处,第三个在第一个上面2倍的STR_WIDTH距离处,以此类推。
————————————————
//随机改变已出现的字符串中的字符void ChangeCh() {for (int i = 0; i < STR_NUM; i++) {rain[i].str[rand() % STR_SIZE] = CreateCh();}}
若每条数字雨中的字符在下落时都是实时在变化的,那就更酷炫了,ChangeCh()让每条数字雨中的随机字符再赋一遍值,这里便可以直接调用前面写好的CreateCh()函数。
————————————————
//使数字雨移动起来void RainMove() {for (int i = 0; i < STR_NUM; i++) {rain[i].y += rain[i].speed;if (rain[i].y - STR_WIDTH * STR_SIZE > WIN_HEIGHT) {rain[i].y = 0;}}}
接着我们要让每条字符串向下移动,主函数每调用一次该RainMove()函数,每条数字雨的y坐标便加上speed即下落速度。
但要注意,当每条字符串的最后一个字符掉到窗口最下端之后,重置该字符串首元素的y值,要不然所有字符串掉出屏幕后就没东西在屏幕上了。
——————————————
最后,再来回到主函数上:
int main() {GameInit();BeginBatchDraw(); //开始双缓冲绘图//调用死循环让画面一直更新while (1) {cleardevice(); //每次更新要清除之前的画面GameDraw();FlushBatchDraw(); //绘制ChangeCh();RainMove();}EndBatchDraw(); getchar(); //防止程序闪退return 0;}
首先,调用GameInit(),初始化参数。
接着,调用BeginBatchDraw(),它是EasyX中的函数,作用是解决屏幕不停更新导致的频闪问题,需要结合后面的FlushBatchDraw()以及EndBatchDraw()一起使用,具体原理可以参考防闪屏批量绘图。
然后,使用while (1)死循环让画面一直更新,循环中每次先清除之前的画面,再调用GameDraw()绘制画面,然后用FlushBatchDraw()绘制,接着调用ChangeCh()来改变数字雨中字符,然后用RainMove()使数字雨动起来。
最后加上程序结尾,全部大功告成。
————————————————
在开头提到的b站视频教程中,老师在此之后还加上了背景音乐和其它特效,音乐这块我只能提醒用Apple Music导出的.m4p文件貌似不能成功运行,一般的正常的.mp3文件应该可以。不过由于我没有找到我认为适合这个屏保风格的音乐,我就没做进去背景音乐,包括一些其它特效,有需要的可以自己看视频后面部分加进去。
————————————————
遗憾的是,我想达到的屏保效果一直没有实现,由于上方的程序的白条无法去除,我找不到EasyX中有实现全屏的方法,也试过调整分辨率到很大,均无法解决,如果有兄弟做出来了欢迎分享,十分感谢。
强迫症很难受,上面白边一直去不掉。。。。。。
————————————————
结束,谢谢