目录
更新说明
前言
1.相关函数的简单介绍(预备知识)
(1)关于函数getch()
(2)关于函数system()
(3)关于函数sleep()
(4)关于函数kbhit()
(5)关于随机数函数
(6)关于函数gotoxy()
(7)关于函数HideCursor()
2.几款小游戏的思路与实现
(1)一些通用思路
【1】模拟坐标位置
【2】坐标位置移动
【3】模块化设计
(2)几个基础小游戏的实现
写在前面
【1】最简单的飞机游戏
【2】函数封装的飞机游戏(进阶版)
【3】飞机游戏+数组(更新中~)
【4】贪吃蛇
【5】反弹球消砖块
未完待续~~
更新说明
本文三月中旬就已经发布,迄今为止一字未更,主要原因是博主前段时间比较焦虑,对这篇博客三分钟热度后摆烂了,尤其是之前最后一次写完博客后第二天发现没保存到直接心态爆炸,如今思之,觉得是时候再捡拾起来了。
预计本月内继续更新本文至完结,还打算更新几个小游戏项目之类的,喜欢的可别错过了哦,点赞收藏关注一波博主,防止迷路。
前言
学习编程的过程难免枯燥,初学者的热情估计也没几天就到了瓶颈期,那么还不赶紧看过来!!
学c语言怎样才能学的废寝忘食通宵达旦而不是枯燥乏味昏昏欲睡呢?
那就让编程加上一点游戏的元素,提高其趣味性,在实现一个个小游戏的同时提升代码思维与写代码的能力。
建议直接看游戏思路,遇到不熟悉的函数才返回到相关函数介绍那里查阅。
(本文的小游戏是基础向的,只在控制台上简单模拟实现,若觉得过于简单,可以略过此文)
1.相关函数的简单介绍(预备知识)
(1)关于函数getch()
函数原型:int getch(void)
函数用途:从控制台读取一个字符,但不显示在屏幕上(不回显)
所在头文件:#include(非标准库要注意其移植性)
使用getch()会等待你按下任意键,再继续执行下面的语句,相当于一个暂停程序的作用。
使用ch=getch()会等待你按下任意键之后,把该键字符所对应的ASCII码赋给ch,再执行下面语句。
getch()接收任意按键,包括回车,空格,但它直接从键盘读入而不是从缓冲区。
关于这个缓冲区是getchar()里的概念
我们常用的是getchar()这个函数它所在文件是stdio.h 也就是标准C库函数。当程序调用getchar时,程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。
当用户键入回车之后,getchar()才开始从stdio流中每次读入一个字符,getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。(如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。)
意思就是getch()接收你键入的任意单字符,不用回车来继续程序,也不会让该字符显示在屏幕上。
(2)关于函数system()
函数原型:int system(const char *command) //参数为操作系统命令
函数用途:是C语言提供的与操作系统衔接的函数,执行一个操作系统命令
所在头文件:#include
本文用到了几个命令:
system(“pause”) //让程序暂停一下,然后按任意键继续,初学的时候最多见于程序的末尾处,用于看运行结果,避免程序一闪而过,还可以用来作为游戏的暂停使用
system(“cls”) //执行清屏操作,清除之前所打印的所有内容
(3)关于函数sleep()
函数原型:void Sleep(DWORD ms)
函数用途:使计算机程序进入休眠,使其在一段时间内处于非活动状态
所在头文件:#include
在VC中Sleep中的第一个英文字符为大写的”S”。
在标准C中是sleep(S不要大写)。
具体用什么看你用什么编译器,简单的说VC用Sleep,别的一律使用sleep。
在Windows操作系统中,sleep()函数需要一个以毫秒为单位的参数代表程序挂起时长。
比如我想让程序滞留一秒的话,就可以使用sleep(1000);。
(4)关于函数kbhit()
函数原型:int kbhit(void)
函数用途:检查当前是否有键盘输入,若有则返回一个非0值,否则返回0
所在头文件: #include
(5)关于随机数函数
函数原型:int rand(void) 和void srand (unsigned int n)
函数用途:rand()会根据srand()设置的种子返回随机数
所在头文件:#include
rand()函数返回0到RAND_MAX之间的伪随机数(pseudorandom)。RAND_MAX常量被定义在stdlib.h头文件中。其值等于32767,或者更大。
srand(n)函数使用自变量n作为种子,用来初始化随机数产生器rand()。若把相同的种子传入srand(),调用rand()时,会产生相同的随机数序列。
可我们需要的是不同的随机数,要想实现生成不同的随机数,就需要使srand()函数设置不同的种子,就需要给srand()时刻变化的种子,而我们知道时间是一直在改变的,所以我们可以通过利用计算机不同的时间来获得不同的种子。
time(NULL)返回的是系统的时间,从1970.1.1零点零分算起,单位为秒。调用时需要用#include头文件。srand((unsigned)time(NULL))则使用系统定时/计数器的值作为随机种子,不用时间得到的种子是不同的,这样我们就可以通过rand()函数得到不同的随机数了。如果仍然觉得时间间隔太小,可以在(unsigned)time(NULL)后面乘上某个合适的整数。
例如,srand((unsigned)time(NULL)*10)。
求一定范围内的随机数
如要取[0,10)之间的随机整数,需将rand()的返回值与10求模。
randnumber = rand() % 10;
那么,如果取的值不是从0开始呢?只需要记住一个通用的公式。
要取[a,b)之间的随机整数(包括a,但不包括b),使用:
(rand() % (b – a)) + a
求伪随机浮点数
要取得0~1之间的浮点数,可以用:
rand() / (double)(RAND_MAX)
如果想取更大范围的随机浮点数,比如0~100,可以采用如下方法:
rand() /((double)(RAND_MAX)/100)
(6)关于函数gotoxy()
所在头文件:#include
void gotoxy(int x, int y) { COORD pos = {x,y}; HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(handle, pos);/}
函数用途:使光标移动到(x,y)位置,这里用来实现“清屏”而不会使得打印的游戏画面闪烁(system(“cls”)清屏后打印画面会持续闪烁)。
把上面显示的函数定义写在调用函数前,再调用gotoxy函数,在()中输入目标坐标位置如gotoxy(0,0),就会使得光标移动到控制台屏幕(0,0)处。
( 一般要用到直接拷贝代码即可,有兴趣的可以自行深入了解。)
(7)关于函数HideCursor()
所在头文件:#include
函数用途:隐藏光标
void HideCursor() {CONSOLE_CURSOR_INFO cursor_info = {1,0};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cursor_info);}
(一般要用到直接拷贝代码即可,有兴趣的可以自行深入了解。)
2.几款小游戏的思路与实现
(1)一些通用思路
【1】模拟坐标位置
起步阶段的小游戏得在控制台上显示运行,总会需要确定目标位置以实现某些判断和游戏功能,这时候就需要拟定一个正交模拟坐标轴xoy了。
这里考虑以X为横坐标,Y为纵坐标。
试试来个 *
int x, y;//x为左右方向上的坐标,y为上下方向上的坐标x = 10;y = 20;int i, j;//循环变量 for(i = 0;i < y;i++){printf("\n");}for(j = 0;j < x;j++){printf(" ");}printf("*");printf("\n");
可能有人会有好奇,要是我先确定横坐标X行不行呢?
答案是不行!
要是颠倒顺序就会成这样:
注意:一旦换行就变成从行首开始了,相当于X清零。
【2】坐标位置移动
在【1】所讲述的基础上给定一个while(1)无限循环,使得状态可以持续刷新。
目前给定的X,Y是确定的值,所以坐标是固定的,那要是每次循环时X和Y的值都不一样呢?
比如第一次循环时Y的值是20,我在while循环内的末尾处加上一个Y++的话,下一次循环时Y的值就是21了,相应的,打印出来的目标Y坐标就是21,也就是向下移动了一个单位。以此类推,我们就有了让目标动起来的办法。
在while循环内,通过改变X和Y的值来改变坐标位置。
案例:跳动的小球
我想在控制台打球,咋搞?一步步来嘛,至少先让球能动起来。
这里用o字母当作球,欲实现小球按照所给轨迹自行循环跳动。
球的输出
for (i = 0; i < y; i++)printf("\n");for (j = 0; j < x; j++)printf(" ");printf("o");
我想先让小球上下跳动,就是Y坐标不断变化,定义一个速率变量velocity,值越大坐标变动越快。
小球不断向下移动:
int i, j;int y = 1, x = 50;int velocity = 1;while(1){ y = y + velocity; system("cls"); for (i = 0; i < y; i++)printf("\n"); for (j = 0; j < x; j++)printf(" ");printf("o");}
要让小球下落到一定程度后回弹,向上反弹到一定程度后又再次向下回弹,定义顶端变量int top,底端变量bottom。if语句判断一下是否触顶或触底,是的话就让velocity取反。
int i, j;int y = 1, x = 50;int velocity = 1;int top = 1;int bottom = 20;while(1){if (y > bottom || y < top)velocity *= -1;y = y + velocity;system("cls");for (i = 0; i < y; i++)printf("\n");for (j = 0; j < x; j++)printf(" ");printf("o");}
左右移动同理就不赘述了。为了区分,我设了两个velocity,分别对应X和Y方向。
int i, j;int y = 1, x = 1;int velocity_x = 1;int velocity_y = 1;int top = 1;int bottom = 20;int left = 1;int right = 40; while(1){if (y > bottom || y right || x < left)velocity_x *= -1;y = y + velocity_y;x = x + velocity_x;Sleep(50); //减缓小球移动速度system("cls");for (i = 0; i < y; i++)printf("\n");for (j = 0; j < x; j++)printf(" ");printf("o");}system("pause");
这样小球就上下左右地自己跳动起来了!
(Sleep函数不清楚可以回看上文相关函数介绍内容)
更为复杂深入的内容后面会单独讲,这里先了解这么多就够了。
【3】模块化设计
使用函数后,本文可能会用到的模板如下:
//全局变量的定义 int main(){startup();//游戏数据初始化 while(1)//游戏状态刷新 {show();//显示画面 updateWithoutIn();//与用户输入无关的数据更新 updateWithIn();//与用户输入有关的数据更新 }system("pause");}
(2)几个基础小游戏的实现
写在前面
建议读者按顺序一个一个看,有些思路要是在前面的游戏里讲过了一般就不会再重复提,默认读者已经有看完了前面游戏思路的基础。
【1】最简单的飞机游戏
所需知识:
选择语句,循环语句
何为最简单?在黑漆漆的控制台上一架小飞机只能上下左右移动加上个简单射击算不算简单?
思路分析:
确定好飞机的坐标位置:建立模拟坐标轴,循环输出空格与换行移动坐标。
还得有个飞机图案,考虑用输出的 * 字符简单组装。
int x, y;//x为左右方向上的坐标,y为上下方向上的坐标x = 10;y = 20;int i, j;//循环变量 for(i = 0;i < y;i++){printf("\n");}for(j = 0;j < x;j++){printf(" ");}printf(" *\n");for(j = 0;j < x;j++){printf(" ");}printf(" ***\n");for(j = 0;j < x;j++){printf(" ");}printf("*****\n");for(j = 0;j < x;j++){printf(" ");}printf(" * *\n");
如何让飞机动起来呢?
给定一个while(1)无限循环,使得状态可以持续刷新。
目前给定的X,Y是确定的值,所以飞机坐标是固定的,那要是每次循环时X和Y的值都不一样呢?
比如第一次循环时Y的值是20,我在while循环内的末尾处加上一个Y++的话,下一次循环时Y的值就是21了,相应的,打印出来的飞机Y坐标就是21,也就是向下移动了一个单位。按这个思路,我们就有了让飞机动起来的办法。
如何让飞机听从指令指哪飞哪呢?
小游戏一般用什么控制目标移动?对,就是WASD对应上左下右。那我们如何让电脑get到我们的指令?
可以用键盘键入对应字符作为移动按键,使用if语句判断区别指令。
Y++和Y–分别对应下移和上移,X++和X–分别对应右移和左移。
这里定义一个char变量input来接收指令,再在原while循环末尾处加上这段代码:
input = getch();if(input == ‘w’)y--;if(input == ‘s’)y++;if(input == ‘a’)x--;if(input == ‘d’)x++;
关于getch()请移步本文的函数相关介绍模块查看相关内容。
尝试着按下移动键,你的飞机动起来了吗?
要是想要改变移动速度,可以让X或Y的值不只是自增1,增加的越多速度越快。
如何发射激光biu~biu~biu~?
这里简单地用竖线‘ | ’组成激光,意思就是要在飞机尖端前呈现一段连续的竖线呗,而且还要跟随着飞机移动。
int x, y;//x为左右方向上的坐标,y为上下方向上的坐标x = 10;y = 20;int i, j;//循环变量 char input;while(1){system("cls");//不断清屏,实现图案变化的前提 for(i = 0;i < y;i++) //这里就是激光的实现,用两重循环先把激光打出来{for(j = 0;j < x;j++){ printf(" ");}printf(" |");printf("\n");}/*for(i = 0;i < y;i++) //注意一下这里我把它注释掉了,为神马呢??{printf("\n");}*/for(j = 0;j < x;j++){printf(" ");}printf(" *\n");for(j = 0;j < x;j++){printf(" ");}printf(" ***\n");for(j = 0;j < x;j++){printf(" ");}printf("*****\n");for(j = 0;j < x;j++){printf(" ");}printf(" * *\n");input = getch();if(input == 'w')y -= 2;if(input == 's')y += 2;if(input == 'a')x -= 2;if(input == 'd')x += 2;}
打完激光后飞机坐标也确定下来了,不需要重复打坐标所以要注释掉那段代码。
那要是我想啥时候发激光就发,啥时候不发就不发该咋整捏?
简单,两种状态的选择呗,if语句走起!
定义一个int变量isfired来控制激光的开与关,isfired初始值设为0,即默认为关闭状态,值为1就输出激光,然后将值重置为0;当值为0时单纯打出飞机图案,不发射激光。
if(isfired == 1){for(i = 0;i < y;i++){for(j = 0;j < x;j++){printf(" ");}printf(" |");printf("\n");}for(j = 0;j < x;j++) //这个for循环是给飞机的第一个*确定X坐标的,可别忘了加上{printf(" ");}isfired = 0;}else{for(i = 0;i < y;i++)printf("\n");for(j = 0;j < x;j++)printf(" ");}
那么isfired怎么由自己来确定是1还是0呢?跟控制飞机移动同理,我这里用空格字符作为射击键,若是input接收到空格,就让isfired值为1。
input = getch();if(input == 'w')y -= 2;if(input == 's')y += 2;if(input == 'a')x -= 2;if(input == 'd')x += 2;if(input == ' ')isfired = 1;
要是我这样还不过瘾,还想有个打击对象咋整呢?
那就在第一行打印一个靶子呗,要是激光和靶子X坐标相同,就清除掉靶子呗。我们定义一个int变量iskilled来作为靶子的判断开关,起始值为0,默认开启,要是满足激光与靶子的X坐标相同就让值变为1,清除靶子。
对靶子重新定义一个横坐标nx(不然靶子就和飞机一起移动了),这里我让nx=10了,当然靶子位置可以自定义,要注意激光的X坐标相对于x向右偏移了两个单位即x+2(两个空格的缘故),那么只要以x+2==nx为判断条件即可,满足的话将iskilled的值转为1,同时别忘了要在飞机已经发射激光的前提下才判断是否击中,把if(x+2 == nx) iskilled =1;放入if(isfired == 1)的语句内。
if(iskilled == 0){for(j = 0;j < nx;j++){printf(" ");}printf("+\n");//靶子 }if(isfired == 1){for(i =0;i < y;i++){for(j = 0;j < x;j++){printf(" ");}printf(" |");printf("\n");}if(x + 2 == nx)iskilled = 1;for(j = 0;j < x;j++){ printf(" ");}isfired = 0;}
好了,这下就实现了飞机的简单打靶了。什么?你说靶子打完了就没了想让它再出现?
没问题!
类比一下之前的isfired值的控制,我们只要在靶子被打完后立刻将iskilled的值复原为0即可。
if(iskilled == 0){for(j = 0;j < nx;j++){printf(" ");}printf("+\n");//靶子 }elseiskilled = 0;
加上一个else语句即可。
下面就是全部代码:
#include#include#includeint main(){int x, y;//x为左右方向上的坐标,y为上下方向上的坐标x = 10;y = 20;int i, j;//循环变量 char input;int isfired = 0;int nx = 10;int iskilled = 0;while(1){system("cls");//不断清屏,实现图案变化的前提 if(iskilled == 0){for(j = 0;j < nx;j++){printf(" ");}printf("+\n");//靶子 }elseiskilled = 0;if(isfired == 1) //激光{for(i =0;i < y;i++){for(j = 0;j < x;j++){printf(" ");}printf(" |");printf("\n");}if(x + 2 == nx)iskilled = 1;for(j = 0;j < x;j++){ printf(" ");}isfired = 0;}else{for(i = 0;i < y;i++)printf("\n");for(j = 0;j < x;j++)printf(" ");}printf(" *\n"); //飞机for(j = 0;j < x;j++){printf(" ");}printf(" ***\n");for(j = 0;j < x;j++){printf(" ");}printf("*****\n");for(j = 0;j < x;j++){printf(" ");}printf(" * *\n");input = getch(); //键入字符交互if(input == 'w')y -= 2;if(input == 's')y += 2;if(input == 'a')x -= 2;if(input == 'd')x += 2;if(input == ' ')isfired = 1;}printf("\n");system("pause");}
【2】函数封装的飞机游戏(进阶版)
所需新知识:
函数的定义与调用
虽然已经实现了最简单的飞机游戏,但总觉得这样的一整坨代码全部一股脑地堆砌在main函数中不仅看的难受,改起代码来也有点痛苦。(基于上一篇的内容进行更进)
我们使用函数来分别执行不同的功能,将游戏模块化。
#include#include#include#include#includeint main(){startup();//游戏数据初始化 while(1)//游戏状态刷新 {show();//显示画面 updateWithoutIn();//与用户输入无关的更新 updateWithIn();//与用户输入有关的更新 }system("pause");}
记得在写别的函数前先把这两个函数定义写上,后面要用到
void HideCursor() //隐藏光标的函数的定义 {CONSOLE_CURSOR_INFO cursor_info = {1,0};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cursor_info);}void gotoxy(int x, int y) //用以清屏的函数的定义 { COORD pos = {x,y}; HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(handle, pos);}
先给出所需的所有全局变量
//全局变量的定义 int position_x, position_y; //飞机坐标位置 int height, width; //游戏画面尺寸 int bullet_x, bullet_y; //子弹坐标位置 int enemy_x, enemy_y; //敌人坐标位置 int score; // 游戏得分 int enemy_speed_limit; //下落速度的控制变量 int bullet_speed; //子弹速度的控制变量
我们首先想想怎么把一个基本的游戏画面打印出来
用两个for循环遍历坐标,如果飞机/子弹/敌人/边界的坐标对应上了就打印。值还没确定没关系,我们先把大体框架建立起来。
void show() //显示画面 {int i, j;int m, n;gotoxy(0,0); //使光标移动到(X,Y)位置,清屏作用 for(i = 0;i < height;i++){for(j = 0;j < width;j++){if((i == position_y)&&(j == position_x))printf("*");//输出飞机 else if((i == bullet_y)&&(j == bullet_x))printf("|"); //输出子弹 else if((i == enemy_y)&&(j == enemy_x))printf("#"); //输出敌人 else if(j == width - 1) //显示右边界 printf("@");else if(i == height -1) //显示下边界 printf("@");elseprintf(" ");}printf("\n");}}
根据需要初始化游戏数据
void startup() //游戏数据初始化 {height = 28;width = 50;//控制画面尺寸 position_x = width / 2; //这里我选定飞机初始位置为坐标系中心position_y = height /2;bullet_y = -1;//为了让子弹一开始未发射时不显示bullet_x = position_x;//子弹X坐标上与飞机相同bullet_speed = 1;enemy_x = width / 2; //敌人初始位置enemy_y = 0;enemy_speed_limit = 5;score = 0; //得分初始值HideCursor();//隐藏光标 }
子弹发射与移动
之前我们飞机发射的是激光对吧,这次我们改为发射子弹,并且子弹发射出去后会向上飞行。
这个子弹发射出去后的坐标变化是不是与用户的输入无关呀,所以在函数void updateWithoutIn()里写上:
if(bullet_y > -1) bullet_y -= bullet_speed; //子弹向上飞行(速度可控)
子弹坐标不断减去bullet_speed,不断减小而实现向上移动,并且由bullet_speed的变化而改变速度。
发射的话还是用空格控制,在void updateWithIn() 中
if(input == ' '){bullet_y = position_y - 1;//子弹从飞机头部上一行发射 bullet_x = position_x;//让子弹始终从飞机头部发射,也就是飞机左右移动时子弹X坐标也会跟随变化 }
敌人的随机出现与下落
我们在前面show()打印画面的时候有打印敌人的代码,但是怎么让敌人动起来呢?
直接enemy_y++不就行了嘛,但是这样以来敌人一旦掉出边界就没了,怎样让敌人再次出现呢?
在void updateWithoutIn()中
if(enemy_y == height){enemy_y = 0; //敌人反复下落 }else{enemy_y++; //敌人下落 }
欸!这敌人下落的也太快了吧??哦对了,用Sleep()函数不就行了嘛(●ˇ∀ˇ●),但是!!
一旦用Sleep()整个程序的运行速度都会变慢,你飞机和子弹的移动速度也变慢了!!
那咋整??有一种办法,设置速度限制变量enemy_speed_limit和speed,speed用来表示while()已循环轮数,我让enemy_speed_limit作为speed的上限,假设enemy_speed_limit为5,只要speed < 5的话就让speed自增1(初值为0),一旦speed == 5了就让enemy_y++,这样一来,只有while()循环了5次才让敌人下落一个单位,比起原来的循环一次下落一个单位是不是就慢了一些呢。
敌人位置始终是同一个,不得劲啊,想让敌人重新生成时随机生成,那就用rand()呗!
static int speed = 0; //静态变量,这里设置成全局变量也行,只要不会随函数的栈帧释放而释放即可(不懂可以搜索相关资料)if(speed < enemy_speed_limit) //减缓敌人下落速度 speed++;if(enemy_y == height){enemy_y = 0; //敌人反复下落 enemy_x = rand() % width;//在0~width范围内,随机X坐标位置下落 }else{if(speed == enemy_speed_limit){enemy_y++; //敌人下落 speed = 0; // 别忘了将speed重置为0,不然无法实现减缓效果 }}
敌人与子弹的碰撞响应
敌人此时刻的Y坐标不小于子弹此时刻的Y坐标并且敌人前一时刻的Y坐标小于子弹此时刻的Y坐标,用以确保子弹命中敌人。
一旦命中敌人,得分增加,敌人Y坐标重置为0,敌人X坐标随机重置,子弹的Y坐标也重置(或者你想子弹具有穿透力也可以不重置)。
static int last_enemy_y; //敌人前一时刻的Y坐标if((bullet_x == enemy_x)&&(enemy_y >= bullet_y)&&(last_enemy_y < bullet_y)) //击中敌人的响应 {score++;enemy_y = 0;//让下一个敌人从最顶端下落 bullet_y = -1;//重置子弹 enemy_x = rand() % width;//让下一个敌人随机X坐标位置下落 }
last_enemy_y我把它放这儿了。
if(enemy_y == height){enemy_y = 0; //敌人反复下落 enemy_x = rand() % width;//随机X坐标位置下落 }else{if(speed == enemy_speed_limit){last_enemy_y = enemy_y; //<------放这里了enemy_y++; //敌人下落 speed = 0; // 别忘了将speed重置为0,不然无法实现减缓效果 }}
用户有关的输入与更新
这里用了kbhit()(使得只有用户按下按键后才执行if语句里的代码)和getch()(注意:有些编译器里要加上_,即_kbhit和_getch)
void updateWithIn() //与用户输入有关的更新(针对于飞机) {char input;if(kbhit()) //当用户按下按键时才执行 {input = getch();if(input == 'a')position_x--;if(input == 'd')position_x++;if(input == 'w')position_y--;if(input == 's')position_y++;if(input == ' ')//发射子弹{bullet_y = position_y - 1;//子弹从飞机头部上一行发射 bullet_x = position_x;//让子弹始终从飞机头部发射,也就是飞机左右移动时子弹X坐标也会跟随变化 }if(input == 27)system("pause");//按ESC建暂停游戏 }}
全部代码
#include#include#include#include#include//全局变量的定义 int position_x, position_y; //飞机坐标位置 int height, width; //游戏画面尺寸 int bullet_x, bullet_y; //子弹坐标位置 int enemy_x, enemy_y; //敌人坐标位置 int score; // 游戏得分 int enemy_speed_limit; //下落速度的控制变量 int bullet_speed; //子弹速度的控制变量 void HideCursor() //隐藏光标的函数的定义 {CONSOLE_CURSOR_INFO cursor_info = {1,0};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cursor_info);}void gotoxy(int x, int y) //用以清屏的函数的定义 { COORD pos = {x,y}; HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(handle, pos);}void startup() //游戏数据初始化 {height = 28;width = 50;//控制画面尺寸 position_x = width / 2;position_y = height /2;bullet_y = -1;bullet_x = position_x;bullet_speed = 1;enemy_x = width / 2;enemy_y = 0;enemy_speed_limit = 5;score = 0; HideCursor();//隐藏光标 }void show() //显示画面 {int i, j;int m, n;gotoxy(0,0); //使光标移动到(X,Y)位置 for(i = 0;i < height;i++){for(j = 0;j = bullet_y)&&(last_enemy_y < bullet_y)) //击中敌人的响应 {score++;enemy_y = 0;//让下一个敌人从最顶端下落 bullet_y = -1;//重置子弹 enemy_x = rand() % width;//让下一个敌人随机X坐标位置下落 }static int speed = 0; //静态变量,speed其实就是已循环轮数 if(speed -1) bullet_y -= bullet_speed; //子弹向上飞行(速度可控) if(enemy_y == height){enemy_y = 0; //敌人反复下落 enemy_x = rand() % width;//随机X坐标位置下落 }else{if(speed == enemy_speed_limit){last_enemy_y = enemy_y;enemy_y++; //敌人下落 speed = 0; // 别忘了将speed重置为0,不然无法实现减缓效果 }}}void updateWithIn() //与用户输入有关的更新(针对于飞机) {char input;if(kbhit()) //当用户按下按键时才执行 {input = getch();if(input == 'a')position_x--;if(input == 'd')position_x++;if(input == 'w')position_y--;if(input == 's')position_y++;if(input == ' '){bullet_y = position_y - 1;//子弹从飞机头部上一行发射 bullet_x = position_x;//让子弹始终从飞机头部发射,也就是飞机左右移动时子弹X坐标也会跟随变化 }if(input == 27)system("pause");//按ESC建暂停游戏 }}int main(){startup();//游戏数据初始化 while(1)//游戏状态刷新 {show();//显示画面 updateWithoutIn();//与用户输入无关的更新 updateWithIn();//与用户输入有关的更新 }system("pause");}
【3】飞机游戏+数组(更新中~)
【4】贪吃蛇
所需新知识:
数组
童年回忆了属于是,还记得以前玩贪吃蛇用的都是“老人机”,想自己动手实现贪吃蛇吗?来,咱们干就完了!
既要我写的清晰,又要读者读的清楚明白,于是乎我就简单画了份思维导图:
建议先把这份思维导图看一遍,把握一下大致框架!!
头文件和模版框架
#include#include#include#include#includeint main(){ startup();//游戏数据初始化 while(1)//游戏状态刷新 {show();//显示画面 moveSnake_ByDirection ()//控制小蛇移动updateWithIn();//与用户输入有关的数据更新 }system("pause");}
游戏画面输出
int canvas [HEIGHT][WIDTH] = {0};//二维数组记录游戏画面中对应元素 ,二维数组建立坐标系 //0输出空格,-1输出边框#,1输出蛇头@ ,>1输出蛇身 *,-2输出食物$ //这样决定元素的值是为了将蛇头蛇身有关量和无关量区分开来 //到后面设计蛇移动的时候就有大用了 #define HEIGHT 25 //总的画面尺寸大小 #define WIDTH 50void show(){gotoxy(0,0);int i, j;for(i = 0;i < HEIGHT ;i++){for(j = 0; j 1) //输出蛇身 printf("*"); else if(canvas[i][j] == 0) //输出空格printf(" ");else if(canvas[i][j] == -2) //输出食物 printf("$");}printf("\n");}printf("\n");printf("得分:%d ",score);Sleep(50);//减缓程序速度}
游戏数据初始化
#define start_length 4 //蛇身初始长度 int move_Direction; //小蛇移动方向,1234分别对应上下左右 int newHead_i,newHead_j; //蛇头的移动后的新位置 int food_i,food_j; //食物位置int score; //得分int restart; //重开判断变量 void startup() //游戏数据初始化 {HideCursor(); //清除光标int i, j;//初始化食物的位置 (随机坐标)food_i = rand() % (HEIGHT - 5) + 2;food_j = rand() % (WIDTH - 5) + 2;canvas[food_i][food_j] = -2;//初始化边框 for(i = 0; i < HEIGHT; i++){canvas[i][0] = -1;canvas[i][WIDTH - 1] = -1;}for(j = 0; j < WIDTH; j++){canvas[0][j] = -1;canvas[HEIGHT - 1][j] = -1;}//初始化蛇头蛇身 canvas[HEIGHT / 2][WIDTH / 2] = 1; //初始化蛇头(游戏画面中间位置) for(i = 1; i<= start_length; i++) //根据宏定义的初始蛇身长度初始化蛇身 canvas[HEIGHT / 2][WIDTH / 2 - i] = i + 1;//从蛇头开始,从右往左初始化生成蛇身 score = 0; restart = 0;move_Direction = 4;//初始时向右移动 }
蛇移动(重点)
回想一下我们玩过的贪吃蛇游戏,里面的蛇从一开始就一直移动,直到碰撞到自身或者边框为止(这里考虑的是有边框的情况),所以我们要使蛇不断自己移动。
那又怎么控制方向呢?当我们按下方向键的时候,是不是蛇头马上就向着我们按的方向转头了呢,这样我们就知道:可以通过控制蛇头的方向来控制蛇的移动方向。
我们先看张图(结合下文的解释一起看):
暗藏玄机:
在贪吃蛇的设计中,可以看出它具有“滞后性”,即蛇头与蛇身的移动并不同拍,靠蛇头的摆动来控制方向。
在初始化蛇身的时候为什么要赋值i + 1呢?为什么不沿着前面思路干脆就让数组值为2就表示蛇身呢?
其实蕴藏着巧妙设计,思考一下上图的蛇移动思路,你会豁然开朗。
这里赋值i + 1,沿着蛇身伸展方向上每小段蛇身对应数组值递增,这样就让方向性代数化了!!
怎么说呢,你看呀,假设蛇头蛇身水平方向上共线,数组值为54321,那么数组值递减方向就是蛇头朝向,递增方向就是蛇尾朝向,那数组大于0的值中最大值为蛇尾,最小值为蛇头,一下子就把蛇头蛇尾定位下来了嘛。结合一下移动思路,比如所有大于0元素+1,再使原最大值为0,丢弃蛇尾旧位置,蛇身就向着蛇头方向移动了一格嘛,此时旧蛇头位置原本数组值为1变为2了就变成了蛇身(根据前面举的例子54321),再就可以根据旧蛇头位置和移动方向变量moveDirection的值,确定新的蛇头位置了呗。
(注意:这里还只是将新蛇头的位置定了下来,而新蛇头位置的数组值还没赋上,至于为啥,后面会讲。)
int newHead_i,newHead_j; //蛇头的移动后的新位置void moveSnake_ByDirection ()//控制小蛇移动 {int max = 0;int i, j;int oldTail_i,oldTail_j;//记录旧的蛇尾位置 int oldHead_i,oldHead_j;//记录旧的蛇头位置 for(i = 1; i < HEIGHT - 1; i++)//循环遍历数组,不包括边框 {for(j =1 ; j 0){//对所有大于0的元素加1 canvas[i][j]++;if(max < canvas[i][j]) //找出最大值{//记录最大值所在位置 (旧的蛇尾位置) max = canvas[i][j];oldTail_i = i;oldTail_j = j;}if(canvas[i][j] == 2){//记录最小值所在位置(旧的蛇头位置) oldHead_i = i;oldHead_j = j;}}}} canvas[oldTail_i][oldTail_j] = 0;//清除旧蛇尾if(move_Direction == 1){newHead_j = oldHead_j; //新的蛇头位置定位 newHead_i = oldHead_i - 1; //1对应向上移动,新蛇头位置与旧蛇头位置相比} //Y坐标-1,X坐标不变if(move_Direction == 2){newHead_j = oldHead_j; //新的蛇头位置定位 newHead_i = oldHead_i + 1; //2对应向下移动,新蛇头位置与旧蛇头位置相比} //Y坐标+1,X坐标不变if(move_Direction == 3){newHead_j = oldHead_j - 1; //新的蛇头位置定位 newHead_i = oldHead_i; //3对应向左移动,新蛇头位置与旧蛇头位置相比} //Y坐标不变,X坐标-1if(move_Direction == 4){newHead_j = oldHead_j + 1; //新的蛇头位置定位 newHead_i = oldHead_i; //4对应向右移动,新蛇头位置与旧蛇头位置相比} //Y坐标不变,X坐标+1}
我们在初始化数据的时候写了一句move_Direction = 4; 对吧,这样的话蛇一开始就会向右一直移动,直到发生碰撞或用户键入值进行操作(上下左右移动或暂停或重开)。
吃食物
要注意的点就是旧蛇尾的值什么时候为0的问题,这个问题就和上面讲的蛇移动有关系了。
旧蛇尾为0蛇就向着蛇身沿蛇头的方向移动一格嘛。
这里要对吃食物进行判断嘛,而且吃完食物还要增长蛇身,可以考虑在新蛇尾往后一格的位置加一格蛇身对吧,欸,新蛇尾往后一格是哪?这不刚好就是旧蛇尾的位置嘛!
那干脆就用if…else语句嘛,如果蛇吃到食物,旧蛇尾就不变成0,保留旧蛇尾为新蛇尾,蛇身长度就+1了;如果蛇没有吃到食物,那旧蛇尾变为0,蛇照常移动。
(要是用下面的代码的话,上面那段代码中的 “canvas[oldTail_i][oldTail_j] = 0; //清除旧蛇尾” 记得清除掉)
//判断小蛇新蛇头是否碰到食物 if(canvas[newHead_i][newHead_j] == -2){//食物重新生成 canvas[food_i][food_j] = 0;food_i = rand() % (HEIGHT - 5) + 2;food_j = rand() % (WIDTH - 5) + 2;canvas[food_i][food_j] = -2;//吃到食物,旧蛇尾不用变为0,使得蛇身长度+1 //得分+1score++; }else{//没吃到食物时蛇身长度不变canvas[oldTail_i][oldTail_j] = 0;//旧蛇尾所在位置的元素变为0}
蛇碰撞后的响应
碰撞后游戏失败,可以选择是否重开。
//新蛇头位置数组值的赋值放到下面语句里,是为了判断新蛇头位置是否已经是蛇身的位置或边框的位置 //小蛇碰到自身或边框游戏失败 if(canvas[newHead_i][newHead_j] > 0 || canvas[newHead_i][newHead_j] == -1) {int k; //相撞则游戏失败,看看是否需要重来 printf("Game Over!\n是否重来? Y or N (连按两下)\n");//连按(小写的y或n)两下有一下是为了跳过pause指令,还有一下是给getch()接收的 //暂停程序system("pause");k = getch();if(k == 'y') //用户想要重开的话restart = 1;else //否则结束游戏,退出程序exit(0);}else //未相撞则继续前进 {canvas[newHead_i][newHead_j] = 1;//新蛇头位置数组值赋值}
用户输入相关
所谓的蛇头不会反噬蛇身的限制是什么意思呢?
比如蛇现在向右移动,我这时候按下a键,是想命令蛇向左移动对吧,如果没有限制的话,这时蛇头就会调头向左移动,会“吞噬”掉蛇身!
所以我们要判断一下蛇头要转动的地方是不是空的,比如向左移动要判断一下蛇头左边是不是空的,也就是 canvas[newHead_i][newHead_j – 1] == 0 是否成立,其他的限制条件以此类推。
void updateWithIn() {char input;if(kbhit()) //当用户按下建时执行 {input = getch();//要记得加蛇头不会反噬蛇身的限制 if(input == 'w' && (canvas[newHead_i - 1][newHead_j] == 0))move_Direction = 1;if(input == 's' && (canvas[newHead_i + 1][newHead_j] == 0))move_Direction = 2;if(input == 'a' && (canvas[newHead_i][newHead_j - 1] == 0)) move_Direction = 3;if(input == 'd' && (canvas[newHead_i][newHead_j + 1] == 0))move_Direction = 4;if(input == 27){system("pause");}if(input == 'r'){restart = 1;}}}
重新开始咋写
这里给出的一个思路是用goto语句从while循环中出来再回到main函数最开始的地方,然后全部再来一次。设置一个restart变量初值为0,一旦用户键入r键或者是游戏失败了选择重开时,restart的值就变为1,而如果restart的值变为1了,就用goto语句跳出去。
是不是感觉这样就了?
请看下图:
不,其实还不够,你会发现上一局的数组中一些元素的值残留了下来。
所以我们要顺便把它们清除掉。
int main(){int i, j;flag:startup();//游戏数据初始化 while(1)//游戏状态刷新 {show();//显示画面 updateWithoutIn();//与用户输入无关的更新 updateWithIn();//与用户输入有关的更新 if(restart == 1)//是否重新游戏的判断 {restart = 0; //别忘了重置restart的值!!for(i = 0;i < HEIGHT ;i++) //清除残留数据{for(j = 0; j < WIDTH ;j++){if(canvas[i][j] != 0){canvas[i][j] = 0;}}}system("cls");//清除失败后的选项文字 goto flag;}}system("pause");}
好了,以上就是贪吃蛇的所有思路分析了。
完整代码:
#include#include#include#include#include#define HEIGHT 25 //总的画面尺寸大小 #define WIDTH 50#define start_length 4 //蛇身初始长度 //全局变量的定义 int canvas [HEIGHT][WIDTH] = {0};//二维数组记录游戏画面中对应元素 ,二维数组建立坐标系 //0输出空格,-1输出边框#,1输出蛇头@ ,>1输出蛇身 *,-2输出食物$ int score;int restart;//重开变量 int move_Direction; //小蛇移动方向,1234分别对应上下左右 int newHead_i,newHead_j; //蛇头的移动后的新位置 int food_i,food_j;void HideCursor() //隐藏光标的函数的定义 {CONSOLE_CURSOR_INFO cursor_info = {1,0};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cursor_info);}void gotoxy(int x, int y) //用以清屏的函数的定义 { COORD pos = {x,y}; HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(handle, pos);}void startup() //游戏数据初始化 {HideCursor();int i, j;//初始化食物的位置 food_i = rand() % (HEIGHT - 5) + 2;food_j = rand() % (WIDTH - 5) + 2;canvas[food_i][food_j] = -2;//初始化边框 for(i = 0;i < HEIGHT;i++){canvas[i][0] = -1;canvas[i][WIDTH - 1] = -1;}for(j = 0;j < WIDTH;j++){canvas[0][j] = -1;canvas[HEIGHT - 1][j] = -1;}//初始化蛇头蛇身 canvas[HEIGHT / 2][WIDTH / 2] = 1; for(i = 1; i<= start_length;i++) canvas[HEIGHT / 2][WIDTH / 2 - i] = i + 1;//从右往左生成蛇身 score = 0; restart = 0;move_Direction = 4;//初始时向右移动 }void moveSnake_ByDirection ()//控制小蛇移动 {int max = 0;int i, j;int oldTail_i,oldTail_j;//记录旧的蛇尾位置 int oldHead_i,oldHead_j;//记录旧的蛇头位置 for(i = 1;i < HEIGHT - 1;i++)//不包括边框 {for(j =1 ;j 0){//对所有大于0的元素加1 canvas[i][j]++;if(max 0 || canvas[newHead_i][newHead_j] == -1) {int k;printf("Game Over!\n是否重来? Y or N (连按两下)\n");//相撞则游戏失败,看看是否需要重来 system("pause");k = getch();if(k == 'y')restart = 1;elseexit(0);}else//未相撞则继续前进 {canvas[newHead_i][newHead_j] = 1;}}void show(){gotoxy(0,0);int i, j;for(i = 0;i < HEIGHT ;i++){for(j = 0; j 1)//输出蛇身 printf("*"); else if(canvas[i][j] == 0)printf(" ");else if(canvas[i][j] == -2)//输出食物 printf("$");}printf("\n");}printf("\n");printf("得分:%d ",score);Sleep(50);} void updateWithoutIn(){moveSnake_ByDirection ();//小蛇移动相关 }void updateWithIn() {char input;if(kbhit()) //当用户按下建时执行 {input = getch();//要记得加蛇头不会反噬蛇身的限制 if(input == 'w' && (canvas[newHead_i - 1][newHead_j] == 0))move_Direction = 1;if(input == 's' && (canvas[newHead_i + 1][newHead_j] == 0))move_Direction = 2;if(input == 'a' && (canvas[newHead_i][newHead_j - 1] == 0)) move_Direction = 3;if(input == 'd' && (canvas[newHead_i][newHead_j + 1] == 0))move_Direction = 4;if(input == 27){system("pause");}if(input == 'r'){restart = 1;}}}int main(){int i, j;flag:startup();//游戏数据初始化 while(1)//游戏状态刷新 {show();//显示画面 updateWithoutIn();//与用户输入无关的更新 updateWithIn();//与用户输入有关的更新 if(restart == 1)//是否重新游戏的判断 {restart = 0;for(i = 0;i < HEIGHT ;i++){for(j = 0; j < WIDTH ;j++){if(canvas[i][j] != 0){canvas[i][j] = 0;}}}system("cls");//清除失败后的选项文字 goto flag;}}system("pause");}
【5】反弹球消砖块
还记得我们上文中举过的跳动的小球的例子吗?想让小球四处弹跳并且能反弹吗?我们接下来就来实现一下反弹球消砖块的小游戏。
先看看思维导图:
头文件与模板框架
#include#include#include#include#includeint main(){startup();//游戏数据初始化 while(1)//游戏状态刷新 {show();//显示画面 updateWithoutIn();//与用户输入无关的更新 updateWithIn();//与用户输入有关的更新 }system("pause");}
全局变量
//全局变量的定义 int canvas[HEIGHT][WIDTH];//0为空格,1为小球O,2为挡板*,3为砖块# int ball_x, ball_y;//小球坐标 int ball_vx, ball_vy;//小球速度 int pos_x, pos_y;//挡板中心坐标 int radius;//挡板半径 int left, right;//挡板左右端X坐标 int thickness = 3;//砖块厚度 int score; //得分
二维数组的行和列分别充当逻辑坐标轴Y和X,二维数组不同坐标对应数值对应不同画面元素,0为空格,1为小球O,2为挡板*,3为砖块#
感谢观看,你的支持就是对我最大的鼓励,如果觉得本文还算有用,不妨动动手指点个赞收个藏再关注一波~