1、课程大纲
2、第二部分第九课: 实战"悬挂小人"游戏
3、第二部分第十课预告: 安全的文本输入
我们的课程分为四大部分,每一个部分结束后都会有练习题,并会公布答案。还会带大家用C语言编写三个游戏。
C语言编程基础知识
什么是编程?
工欲善其事,必先利其器
你的第一个程序
变量的世界
运算那点事
条件表达式
循环语句
实战:第一个C语言小游戏
函数
练习题
习作:完善第一个C语言小游戏
C语言高级技术
模块化编程
进击的指针,C语言王牌
数组
字符串
预处理
创建你自己的变量类型
文件读写
动态分配
实战:“悬挂小人”游戏
练习题
习作:用自己的语言解释指针
用基于C语言的SDL库开发2D游戏
安装SDL
创建窗口和画布
显示图像
事件处理
实战:“超级玛丽推箱子”游戏
掌握时间的使用
用SDL_ttf编辑文字
用FMOD控制声音
实战:可视化的声音谱线
练习题
数据结构
链表
堆,栈和队列
哈希表
练习题
这一课我们来实战一下,要实现的游戏叫“悬挂小人”。
这个“小人”,不是“君子和小人”的小人。是little man(小小的人)的意思。
小编你有必要这么强调吗?... 简直无聊嘛。
好了,话休絮烦...
俗语说得好:“实践是必要的!”
对于大家来说这又尤为重要,因为我们刚刚结束了一轮C语言的高级技术的“猛烈进攻”,需要好好复习一下,消化消化。
不论你多厉害,在编程领域,不实践是永远不行的。尽管你可能读懂了之前的所有课程,但是如果不配合一定的实践,是不能深刻理解的。
这次的实战练习,我们一起来实现一个小游戏:“悬挂小人”,或叫 “上吊游戏”。英语叫 HangMan,是挺著名的一个休闲益智游戏。
虽说是游戏,但是比较可惜的是还不能有图形界面 (不过课程后面会说怎么实现在控制台绘制小人,其实也可以实现简陋的“图形化”): 因为C语言本身不具备绘制UI的能力,需要引入第三方的库。
而我们真正开始图形编程,要到第三部分:【用基于C语言的SDL库开发2D游戏】
不过我们第二部分的课程已经接近尾声了,不要急,马上我们就可以开始用C语言来做真正的图形界面游戏了。
悬挂小人游戏是一个经典的字母游戏,在规定步数内一个字母一个字母地猜单词,直到猜出整个单词。
所以我们的游戏暂时还是以控制台的形式(黑框框)与大家见面,当然如果你会图形编程,这个游戏也可以自己扩展成图形界面的。相信不少读者应该见过这个游戏的图形界面版本,就是每猜错一个字母画一笔,直到用完规定次数,小人被“吊死”,就真的变成“�潘俊绷恕�
这个实战的目的是让我们可以复习之前学过的所以C语言知识:指针,字符串,文件读写,结构体,数组,… 等,都是好家伙!
题目规定
既然是出题目的实战,那么就需要委屈大家按照我们的题目要求来编写这个游戏啦。
好,就来公布我们的题目要求:
游戏每一轮有7次(次数可以设置,不一定要7次)猜测的机会,用完则此轮失败
每轮会从字典中随机抽取一个单词供玩家猜,初始时单词是以若干个星号(*)的方式来表示。说明所有字母都还隐藏着
字典的所有单词储存在一个文本文件中(在Windows下通常是txt文件,在unix/linux下一般可以是任意后缀名的文件)
每猜错一个字母就扣掉一次机会,猜对一个字母不扣除机会数,猜对的字母会显示在屏幕上的单词中,替换掉星号
一个回合的运作机制
假设要猜的单词是OSCAR
假设我们给程序输入一个字母B(猜的第一个字母),程序会验证字母是否在这个单词里。
有两种情况:
所猜的字母在单词中,此时程序会显示这个单词,不是全部显示,而是显示猜到的那些字母,其他的还未猜到的字母用*表示
所猜的字母不在单词中(目前的情况,因为字母B不在单词OSCAR中),此时程序会告诉玩家“你猜错了”,剩余的机会数会被扣除一个。如果剩余机会数变为0,游戏结束
在图形化的“悬挂小人”(Hangman)游戏中,每猜一次会有一个小人被画出来。我们的游戏,虽然还不能真正实现图形化,但是如果优化一下,也可以在控制台实现类似这样的效果:
假设玩家输入一个C,因为C在单词OSCAR中,那么程序不会扣除玩家的剩余机会数,而且会显示已猜到的字母,如下:
单词:**C**
如果玩家继续输入,这回输入的是O,那么程序会显示如下:
单词:O*C**
多个相同字母的情况
有一些单词中,同一个字母会出现多次。比如在APPLE(苹果)中,P这个字母就出现了2次;在ELEGANCE(优雅)中,E这个字母出现了3次。
“Hangman”(悬挂小人)游戏对此的规则很简单:
只要猜出一个字母,其他重复的字母会同时显示:
假如要猜的单词是ELEGANCE,用户输入了一个E,那么会如下显示:
单词:E*E****E
一个回合的例子
***********************************************
欢迎来到悬挂小人游戏!
您还剩7次机会
神秘单词是什么呢?*****
输入一个字母:E
您还剩6次机会
神秘单词是什么呢?*****
输入一个字母:S
您还剩6次机会
神秘单词是什么呢?*S***
输入一个字母:R
您还剩6次机会
神秘单词是什么呢?*S**R
输入一个字母:
***********************************************
游戏就会这样进行下去,直到玩家在7个机会用完前猜到单词,或者用完7个机会还没猜到单词,游戏结束。
例如:
***********************************************
您还剩2次机会
神秘单词是什么呢?OS*AR
输入一个字母:C
胜利了!神秘单词是:OSCAR
***********************************************
在控制台输入一个字母
在控制台中让程序读入一个字母,看起来简单,但其实暗藏玄机。
不信我们来试一下:
要输入一个字母,一般大家会认为是这样做:
scanf("%c", &myLetter);
确实是不错的,因为 %c 标明了等待用户输入一个字符。输入的字符会储存在myLetter这个变量(类型是char)中。
只要我们只写一个scanf,那是没问题的,但是假如有好几个scanf,会怎么样呢?我们来测试一下:
int main(int argc, char* argv[]) { char myLetter = 0; scanf("%c", &myLetter); printf("%c", myLetter); scanf("%c", &myLetter); printf("%c", myLetter); return 0; }
照我们的设想,上述程序应该会请求用户输入一个字符,再打印出来: 进行两次
测试一下,实际情况是怎么样的呢?你输入了一个字符,没错,然后呢... 程序为你打印出来了你输入的那个字符,假如你输入的是a,那么程序输出
a
然后程序就退出了,没有下文了,为什么不提示我输入第二个字符了呢?就好像它忽略了第二个scanf一样
到底发生了什么呢?
事实上,当你在控制台(console)里面输入时,你输入的内容都被记录到内存的某处,当然也包括按下Enter键(回车键)时产生的输入: \n
因此,你先输入了一个字符(例如a),然后你按了一下回车键: 字符a就被第一个scanf取走了,第二个scanf则把你的回车键(\n)取走了。
为了避免这个问题,我们写一个函数readCharacter()来处理:
char readCharacter() { char character = 0; character = getchar(); // 读取输入的第一个字母 character = toupper(character); // 把这个字母转成大写 // 读取其他的字符,直到 \n (为了忽略它们) while(getchar() != '\n') ; return character; // 返回读到的第一个字母 }
可以看到,以上程序中,我们使用了getchar函数,这个函数是在标准库的stdio.h中,用于读取一个用户输入的字符,效果相当于:
scanf("%c", &letter);
然后,我们又用到了一个在本课程中还没学习过的函数: toupper,根据字面意思to+upper简单来说就是“转换为大写”,所以这个函数就是用于把一个字母转成大写字母。
看到了吧,如果函数名起得好,几乎就不需要注释,看名字就知道大致是干什么的(论编程命名的重要性)。
借着toupper这个函数,玩家就可以输入小写字母或者大写字母了,因为在“悬挂小人”游戏中,我们显示的单词中的字母都是大写的。
toupper这个函数定义在ctype.h这个标准库的头文件中,所以需要
#include <ctype.h>
但是我在Fedora 21(一种Linux发行版)中测试,不加引入ctype.h的这行代码也是可以编译运行的,不过保险起见还是加上吧。
继续看我们的函数,可以看到其中最关键的地方是:
while(getchar() != '\n') ;
这一小段代码使得我们可以清除第一个输入的字母外的其他字符,直到遇见 \n (回车符)。
函数返回的就是第一个输入的字母,这样可以保证不再受回车符的影响了。
我们用了一个while循环,而循环体部分只有一个分号(;),很简洁吧。
也许你会问,之前的课程中while循环的循环体不是由大括号围起来的么,怎么这里只有一个分号呢?
事实上,这个分号就相当于
{ }
就是空循环体,什么都不做,所以其实以上的代码相当于:
while(getchar() != '\n'){ }
但是分号比大括号写起来更简单么,不要忘了程序员是懂得如何偷懒的一群人!
此while循环一直执行,直到用户输入回车符,其他的字符都被从内存中清除了,我们称其为 “清空缓冲区”
总结:
为了在我们的程序中每次读取用户输入的一个字母,我们不要使用
scanf("%c", &myLetter);
而须要借助我们写的函数:
myLetter = readCharacter();
于是,我们的测试程序变成这样:
#include <stdio.h> #include <ctype.h> char readCharacter(){ char character = 0; character = getchar(); // 读取一个字母 character = toupper(character); // 把这个字母转成大写 // 读取其他的字符,直到 \n (为了忽略它) while(getchar() != '\n') ; return character; // 返回读到的第一个字母 } int main(int argc, char* argv[]) { char myLetter = 0; myLetter = readCharacter(); printf("%c\n", myLetter); myLetter = readCharacter(); printf("%c\n", myLetter); return 0; }
运行,输出类似如下(假如用户输入o,回车;输入k,回车)
o
O
k
K
字典 / 词库
因为我们的游戏是一步步写成的,所以一开始,肯定先写简单的,再逐步完善游戏
因此,猜测的单词一开始我们只用一个,
所以,我们一开始会这么写:
char secretWord[] = "BOTTLE";
你会说:“这样不是很无聊嘛,猜测的单词总是这一个”
是的,但之后我们肯定会扩展,一开始这样做是为了不把问题复杂化,一次做一件事情,慢慢来么
之后如果猜测一个单词的代码可以运行了,我们再用一个文件来储存所有可能的单词,这个文件可以起名为dictionary(英语 “字典”的意思)
那什么是字典或词库呢?
在我们的游戏里,就是一个文件,文件中的每一行存放了一个单词,之后我们的程序会随机从此文件中抽取一个单词来作为每一轮的猜测单词
词库是类似这样的:
YOU
MOTHER
LOVE
PANDA
BOTTLE
FUNNY
HONEY
LIKE
JAZZ
MUSIC
BREAD
APPLE
WATER
PEOPLE
对于这个文件里有多少单词,因为我们的词库是可扩展的(之后肯定可以添加新的单词),所以其实只要统计回车符('\n')的数目就可以,因为是每行一个单词
好了,游戏的基本点我们介绍到这里,其实有了前面所有课程的基础,你已经有能力来完成这个看似有点复杂的游戏了,不过要组织得好还是不那么容易的,你可以用多个函数来实现不同的功能
加油,坚持不懈就是胜利,期待你的成果!
点击左下角“阅读原文”查看解题思路和答案。但是我们希望你先自己做,实在做不出来才看答案,不然就没什么收获了。
优化建议
因为我们的项目是在Linux下用gcc来编译的,如果你是在Windows下用CodeBlocks等IDE来编译的,那么请将字典文件dictionary改成dictionary.txt
因为Windows的文件储存形式和Linux(或Unix)有些不一样。
改进游戏
目前来说,我们只让玩家玩一轮,如果能加一个循环,使得游戏每次询问玩家是否要再玩一次,那“真真是极好的”
目前还是单机模式,可以创建一个二人模式,就是一个玩家输入一个单词,第二个玩家来猜
为什么不用printf函数来打印(绘制)一个悬挂小人呢?在每次我们猜错的时候,就把它画出来,每错一个,多画一笔,这样可以增加乐趣,可以用如下的代码:
if (猜错1个字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf("__|__\n");
}
else if (猜错2个字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | |\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf("__|__\n");
}
else if (猜错3个字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf("__|__\n");
}
else if (猜错4个字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|/\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf("__|__\n");
}
else if (猜错5个字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|/\n");
printf(" | |\n");
printf(" |\n");
printf(" |\n");
printf("__|__\n");
}
else if (猜错6个字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|/\n");
printf(" | |\n");
printf(" | /\n");
printf(" |\n");
printf("__|__\n");
}
else if (猜错7个字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|/\n");
printf(" | |\n");
printf(" | / \\\n");
printf(" |\n");
printf("__|__\n");
}
上面代码中的空格也许不同平台的显示不一样,我这里是5个空格。可能需要大家自行调整。
如果7次机会全部用完,则小人挂掉,游戏结束。
请大家花点时间,好好理解这个游戏,并且尽可能地改进它。如果你可以不看我们的答案,而自己完成游戏和改进,那么你会收获很多的!
完成或优化的代码可以打包成zip或rar文件发送给小编,欢迎讨论和交流,联系邮箱在下面。
也可以上传到我们的百度云盘或QQ群文件里,有问题可以在群里讨论。
今天的课就到这里,一起加油吧。
下一次我们学习: SDL开发游戏之安装SDL