1、课程大纲
2、第二部分第九课: 实战"悬挂小人"游戏 答案
3、第二部分第十课预告: 安全的文本输入
我们的课程分为四大部分,每一个部分结束后都会有练习题,并会公布答案。还会带大家用C语言编写三个游戏。
C语言编程基础知识
什么是编程?
工欲善其事,必先利其器
你的第一个程序
变量的世界
运算那点事
条件表达式
循环语句
实战:第一个C语言小游戏
函数
练习题
习作:完善第一个C语言小游戏
C语言高级技术
模块化编程
进击的指针,C语言王牌
数组
字符串
预处理
创建你自己的变量类型
文件读写
动态分配
实战:“悬挂小人”游戏
练习题
习作:用自己的语言解释指针
用基于C语言的SDL库开发2D游戏
安装SDL
创建窗口和画布
显示图像
事件处理
实战:“超级玛丽推箱子”游戏
掌握时间的使用
用SDL_ttf编辑文字
用FMOD控制声音
实战:可视化的声音谱线
练习题
数据结构
链表
堆,栈和队列
哈希表
练习题
解方(1. 游戏的代码)
如果你开始阅读这里,说明:
或者你写完了游戏,想来看看我们怎么写
或者你没完成这个游戏,想来看看怎么写
不管您是哪种情况,小编都会介绍一下如何来完成这个游戏
“说不说在我,听不听在您”
事实上,小编自己花了比想象中更多的时间来完成这游戏。 人生总是这样的,“理想丰满,现实骨感,看似美满,人艰不拆”。
但是,我还是坚信大家是有能力独自完成这个小游戏的(如果你认真学习了之前的C语言课程),可以去查阅网上资料,花点时间(几十分钟,几小时,几天?),这并不是一次竞赛,所以不用着急.。
我更希望您花了不少时间,最终实现了这个游戏; 比之您只花5分钟,然后就来看答案要好很多。
千万不要觉得小编是一蹴而就写成这个游戏的,这个游戏虽小,但也还没简单到可以在脑中构思好一切,然后“下笔如有神”: 我也是一步步写出来的。
我们将会分2步来介绍我们的解方:
首先我们会演示如何一步步写游戏的主体部分,一开始我们会只有一个猜测的单词,而且是固定的; 我选了BOTTLE(英语 “瓶子”的意思),因为我们要测试对于单词中有大于等于两个相同字母的情况是否处理正确了(BOTTLE中有2个T)
然后我们会演示如何加入词库的处理程序,以便每一轮游戏可以从词库中随机抽取一个单词
牢记: 重要的不是结果,而是我们思考的方式和过程。
分析main函数
大家都知道,我们的C语言程序都是由main函数作为入口的。我们也不要忘了引入一些标准库的头文件: stdio.h, stdlib.h, ctype.h (为了toupper函数)。
因此,我们的程序一开始会是这样的:
#include <stdio.h> #include <stdlib.h> #include <ctype.h> int main(int argc, char* argv[]) { return 0; }
是不是很简单啊,慢慢来么
我们的main函数将控制游戏的大部分运作,并且调用我们将要写的不少函数
我们来声明一些必要的变量吧。这些变量也不是一次就能全部想到的,都是写一点,想到一些。“罗马不是一日建成的”
#include <stdio.h> #include <stdlib.h> #include <ctype.h> int main(int argc, char* argv[]) { char letter = 0; // 存储用户输入的字母 char secretWord[] = "BOTTLE"; // 要猜测的单词 int letterFound[6] = {0}; // 布尔值的数组. 数组的每一个元素对应猜测单词的一个字母。 0 = 还没猜到此字母, 1 = 已猜到字母 int leftTimes = 7; // 剩余猜测次数 (0 = 失败) int i = 0; // 为了遍历数组,需要一个下标 return 0; }
上述的变量中,起到关键作用的就是letterFound这个int型数组了。这个数组用于表示猜测的单词中哪些字母已经猜到,哪些还没猜到。一开始,我们实现得简单些: 我们的单词BOTTLE有6个字母,因此我们的数组就固定是6个元素的数组。
如果元素为0,表示对应的那个字母还没猜到;如果为1,则表示已猜到。随着游戏的进行,这个数组的元素值会被修改。
例如,如果当下我们玩游戏直到
B*TT*E
那么,letterFound这个数组的值应该是这样
101101
之后我们要测试游戏的一轮是否已经胜利也就比较简单了: 只需要测试letterFound数组的所有元素是否都等于1。
我们就来写判断一轮是否胜利的函数吧,取名为win(英语 “胜利”的意思)好了
int win(int letterFound[]) { int i = 0; int win = 1; // 1为胜利,0为失败 for (i = 0 ; i < 6 ; i++) { if (letterFound[i] == 0) win = 0; } return win; }
可以看到,我们的win函数的参数是一个int型数组,我们在main函数中调用win函数时,会将我们的letterFound数组传给它。
这个函数很简单: 遍历数组,只要还有一个元素为0,那游戏还没胜利; 如果所有元素都为1,则游戏胜利。
为了与此函数搭配,我们还需要写一个函数,起名叫researchLetter,这个函数将有两个功能:
返回一个布尔值(在C语言里用int型表示),用于表示所猜的字母是否存在与单词中
更新letterFound数组的元素,如果所猜的字母在单词中,那么就把对应的元素值修改为1
int researchLetter(char letter, char secretWord[], int letterFound[]) { int i = 0; int correctLetter = 0; // 0表示字母不在单词里,1表示字母在单词里 // 遍历单词数组secretWord,以判断所猜字母是否在单词中 for (i = 0 ; secretWord[i] != '\0' ; i++) { if (letter == secretWord[i]) // 如果字母在单词中 { correctLetter = 1; // 表示猜对了一个字母 letterFound[i] = 1; // 对于所有等于所猜字母的数组位置,都将其数值变为1 } } return correctLetter; }
researchLetter这个函数的好处还在于: 不会在找到第一个存在的字母后就停止,而会继续查找,所以对于像BOTTLE这样有两个字母相同的单词就可以一次揭示两个T了
好,写完这两个函数(放在main函数后面),我们继续写我们的main函数。我们添加一句欢迎词:
printf("欢迎来到悬挂小人游戏!\n");
然后添加一个主循环,是一个while循环:
while (leftTimes > 0 && !win(letterFound)) { }
每轮游戏在leftTimes(剩余猜测机会)大于0并且还没胜利的情况下,是不会停止的
如果剩余次数为0,则本轮游戏失败; 如果胜利,那本轮就赢了
在这两种情况下,都要停止游戏
我们在while循环里添加如下代码:
printf("\n\n您还剩 %d 次机会", leftTimes); printf("\n神秘单词是什么呢 ? "); /* 我们显示猜测的单词,将还没猜到的字母用*表示 例如 : *O**LE */ for (i = 0 ; i < 6 ; i++){ if (letterFound[i]) // 如果第i+1个字母已经猜到 printf("%c", secretWord[i]); // 打印出来 else printf("*"); // 还没猜到,打印一个* }
上面的代码用于:
打印剩余机会数
打印单词(其中还没猜到的字母用星号*表示)
接下来,我们写请求用户输入一个字母的代码:
printf("\n输入一个字母 : "); letter = readCharacter();
还记得我们之前写的函数 readCharacter 吗?它用于读取用户的第一个输入的字母,读到回车符结束,而且它会把该字母转成大写
// 如果用户输入的字母不存在于单词中 if (!researchLetter(letter, secretWord, letterFound)) { leftTimes--; // 将剩余猜测机会数减1 }
以上代码调用researchLetter函数在单词中查找用户输入的字母,如果没找到,则剩余猜测机会数扣除一次。
如果字母存在于单词中,则researchLetter函数还会更新letterFound数组(每个元素对应了神秘单词的每一个字母的猜测情况),将其中对应的0(还没猜到)改为1(已经猜到),这样,win函数在判断的时候,如果letterFound数组的每一个元素都为1,则返回1,表示本轮胜利,猜到单词的全部字母了
暂时,while循环体的内容就到这里了,然后我们还要写跳出while循环之后的代码(或者胜利或者失败):
if (win(letterFound)) printf("\n\n胜利了! 神秘单词是 : %s\n", secretWord); else printf("\n\n失败了! 神秘单词是 : %s\n", secretWord);
游戏主体部分的代码就到这里了,给出我们到目前为止的完整程序:
#include <stdio.h> #include <stdlib.h> #include <ctype.h> int win(int letterFound[]); int researchLetter(char letter, char secretWord[], int letterFound[]); char readCharacter(); int main(int argc, char* argv[]) { char letter = 0; // 存储用户输入的字母 char secretWord[] = "BOTTLE"; // 要猜测的单词 int letterFound[6] = {0}; // 布尔值的数组. 数组的每一个元素对应猜测单词的一个字母。 0 = 还没猜到此字母, 1 = 已猜到字母 int leftTimes = 7; // 剩余猜测次数 (0 = 失败) int i = 0; // 为了遍历数组,需要一个下标 printf("欢迎来到悬挂小人游戏!\n"); while(leftTimes > 0 && !win(letterFound)){ printf("\n\n您还剩 %d 次机会", leftTimes); printf("\n神秘单词是什么呢 ? "); /* 我们显示猜测的单词,将还没猜到的字母用*表示 例如 : *O**LE */ for (i = 0 ; i < 6 ; i++) { if (letterFound[i]) // 如果第i+1个字母已经猜到 printf("%c", secretWord[i]); // 打印出来 else printf("*"); // 还没猜到,打印一个* } printf("\n输入一个字母 : "); letter = readCharacter(); // 如果用户输入的字母不存在于单词中 if (!researchLetter(letter, secretWord, letterFound)) { leftTimes--; // 将剩余猜测机会数减1 } } if (win(letterFound)) printf("\n\n胜利了! 神秘单词是 : %s\n", secretWord); else printf("\n\n失败了! 神秘单词是 : %s\n", secretWord); return 0; } int win(int letterFound[]) { int i = 0; int win = 1; // 1为胜利,0为失败 for (i = 0 ; i < 6 ; i++) { if (letterFound[i] == 0) win = 0; } return win; } int researchLetter(char letter, char secretWord[], int letterFound[]) { int i = 0; int correctLetter = 0; // 0表示字母不在单词里,1表示字母在单词里 // 遍历单词数组secretWord,以判断所猜字母是否在单词中 for (i = 0 ; secretWord[i] != '\0' ; i++) { if (letter == secretWord[i]) // 如果字母在单词中 { correctLetter = 1; // 表示猜对了一个字母 letterFound[i] = 1; // 对于所有等于所猜字母的数组位置,都将其数值变为1 } } return correctLetter; } char readCharacter() { char character = 0; character = getchar(); // 读取一个字母 character = toupper(character); // 把这个字母转成大写 // 读取其他的字符,直到 \n (为了忽略它) while(getchar() != '\n') ; return character; // 返回读到的第一个字母 }
这一部分的程序,你可以将其存放在一个.c文件,例如叫hangman.c
然后用gcc编译(如果是在IDE里面,例如CodeBlocks,那直接点击编译运行):
gcc hangman.c -o hangman
运行:
./hangman
接下来我们要开始第二部分:
词库的代码
根据这部分的代码,我们还会接着修改和添加main函数的内容,好吧,稍作休息,继续前进!
解方(2. 词库的代码)
我们已经编写了游戏主体部分的基本代码,但是我们的游戏目前还不能做到每轮随机抽取一个单词,因此,接下来我们就带大家编写处理词库的代码。
首先,我们需要创建一个文件,用于存放所有的单词。
在Linux或Unix或Mac电脑下,我们都可以直接创建一个不带后缀名的文件,在Windows下可以创建.txt结尾的文本文件。
小编写这个游戏是在Linux系统下,所以直接用VIM或Emacs或其他编辑器创建一个文件, 位于我们源文件的相同目录下:
dictionary
在里面写入以下单词(每行一个,用回车符隔开):
YOU
MOTHER
LOVE
PANDA
BOTTLE
FUNNY
HONEY
LIKE
JAZZ
MUSIC
BREAD
APPLE
WATER
PEOPLE
DOG
CAT
GLASS
SKY
GOD
ZERO
当然了,我这里只是举个例子,你可以创建属于你的词库。
新建两个文件
处理这个文件的代码将会不少(至少,我是这么预感的),因此,我们新建一个.c源文件,可以命名为dictionary.c
顺便,我们也创建dictionary.h这个头文件,其中存放dictionary.c中的函数的原型,这样我们在main函数里就可以通过
#include "dictionary.h"
来引入这些函数的定义了。
在dictionary.c中,首先我们引入一些头文件:
#include <stdio.h> #include <stdlib.h> #include <time.h> //我们需要这里面的随机数函数,还记得我们的第一个小游戏“或多或少”吗? #include <string.h> //我们需要strlen这个计算字符串长度的函数 #include "dictionary.h"
chooseWord函数
这个函数用于从文件dictionary中随机选取一个单词,此函数只有一个参数: 指向内存中可以写入单词的地址的指针,这个指针实参将由main函数提供。
函数返回值是int变量: 1表示一切顺利; 0表示出现错误
此函数的开头是这样:
int chooseWord(char *wordChosen) { FILE* dictionary = NULL; // 指向我们的文件dictionary(词库)的文件指针 int wordNum = 0; // 词库中单词总数 int chosenWordNum = 0; // 选中的单词编号 int i = 0; // 下标 int characterRead = 0; // 读入的字符 }
声明了一些变量,我们接着写:
dictionary = fopen("dictionary", "r"); // 以只读模式打开词库(dictionary文件) if (dictionary == NULL) // 如果打开文件不成功 { printf("\n无法装载词库\n"); return 0; // 返回0表示出错 }
这段代码不难吧,就是尝试打开词库(dictionary文件),并检测dictionary文件指针是否为NULL,如果为NULL,表示打开失败。如果打开文件失败,则程序中止,因为没有进行下去的必要了。
// 统计词库中的单词总数,也就是统计回车符 \n 的数目 do{ characterRead = fgetc(dictionary); if (characterRead == '\n') wordNum++; } while(characterRead != EOF);
上面这段代码中,我们借助fgetc函数遍历整个文件(一个字符一个字符读取)
我们统计读到的回车符(\n)的数目,每读到一个\n,我们对wordNum(单词总数)的值加1
我们通过以上代码,就可以知道词库中的单词总数了,就是wordNum的值
然后,我们需要一个函数,根据wordNum的值计算一个伪随机数出来,作为随机选取的单词编号,我们就来写一个函数,命名为: randomNum
randomNum函数
此函数里的代码我们之前编写第一个C语言小游戏: “或多或少” 时已经用过了,就是简单的伪随机数生成。
作用: 用于返回一个介于 0~单词总数-1 之间的随机数
int randomNum(int maxNum) { srand(time(NULL)); return (rand() % maxNum); }
写好了randomNum函数,我们立即来使用它:
chosenWordNum = randomNum(wordNum); // 随机选取一个单词(编号)
接着,我们需要重新回到文件开始处来进行读取,为了回到文件开始处,可以调用函数 rewind
// 我们重新从文件开始处读取(rewind函数),直到遇到选中的那个单词 rewind(dictionary); while (chosenWordNum > 0) { characterRead = fgetc(dictionary); if (characterRead == '\n') chosenWordNum--;} /* 文件指针已经指向正确位置,我们就用fgets来读取那一行(也就是那个选中的单词)*/ fgets(wordChosen, 100, dictionary); // 放置 \0 字符用于表示字符串结束 wordChosen[strlen(wordChosen) - 1] = '\0'; fclose(dictionary); return 1; // 一切顺利,返回1
dictionary.h文件
其中包含我们的dictionary.c中的函数原型,内容如下:
#ifndef DICTIONARY_H #define DICTIONARY_H int chooseWord(char *wordChosen); int randomNum(int maxNum); #endif
完整的dictionary.c文件
/* 悬挂小人游戏 dictionary.c ------------ 这里定义了两个函数: 1. chooseWord 用于每轮从dictionary文件中随机抽取一个单词 2. randomNum 用于返回一个介于 0~单词总数-1 之间的随机数 */ #include <stdio.h> #include <stdlib.h> #include <time.h> #include <string.h> #include "dictionary.h" int chooseWord(char *wordChosen) { FILE* dictionary = NULL; // 指向我们的文件dictionary的文件指针 int wordNum = 0; // 单词总数 int chosenWordNum = 0; // 选中的单词编号 int i = 0; // 下标 int characterRead = 0; // 读入的字符 dictionary = fopen("dictionary", "r"); // 以只读模式打开词库(dictionary文件) if (dictionary == NULL) // 如果打开文件不成功 { printf("\n无法装载词库\n"); return 0; // 返回0表示出错 } // 统计词库中的单词总数,也就是统计回车符 \n 的数目 do { characterRead = fgetc(dictionary); if (characterRead == '\n') wordNum++; } while(characterRead != EOF); chosenWordNum = randomNum(wordNum); // 随机选取一个单词(编号) // 我们重新从文件开始处读取(rewind函数),直到遇到选中的那个单词 rewind(dictionary); while (chosenWordNum > 0) { characterRead = fgetc(dictionary); if (characterRead == '\n') chosenWordNum--; } /* 文件指针已经指向正确位置,我们就用fgets来读取那一行(也就是那个选中的单词)*/ fgets(wordChosen, 100, dictionary); // 放置 \0 字符用于表示字符串结束 wordChosen[strlen(wordChosen) - 1] = '\0'; fclose(dictionary); return 1; // 一切顺利,返回1 } int randomNum(int maxNum) { srand(time(NULL)); return (rand() % maxNum); }
修改hangman.c文件
现在,既然我们的处理词库的函数已经写完了,也就是在dictionary.c中,那么我们需要相应地修改我们的hangman.c文件中的main函数和其他几个子函数:
有了之前所有课程的知识,靠着注释,应该不难看懂。
完整的hangman.c文件
/* 悬挂小人游戏 main.c ------------ 游戏的主体代码 */ #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <string.h> #include "dictionary.h" int win(int letterFound[], long wordSize); int researchLetter(char letter, char secretWord[], int letterFound[]); char readCharacter(); int main(int argc, char* argv[]) { char letter = 0; // 存储用户输入的字母 char secretWord[100] = {0}; // 要猜测的单词 int *letterFound = NULL; // 布尔值的数组. 数组的每一个元素对应猜测单词的一个字母。 0 = 还没猜到此字母, 1 = 已猜到字母 int leftTimes = 7; // 剩余猜测次数 (0 = 失败) int i = 0; // 下标 long wordSize = 0; // 单词的长度(字母数目) printf("欢迎来到悬挂小人游戏!\n"); // 从词库(文件dictionary)中随机选取一个单词 if (!chooseWord(secretWord)) exit(0); // 退出游戏 // 获取单词的长度 wordSize = strlen(secretWord); letterFound = malloc(wordSize * sizeof(int)); // 动态分配数组的大小,因为我们一开始不知道单词长度 if (letterFound == NULL) exit(0); // 初始化布尔值数组,都置为0,表示还没有字母被猜到 for (i = 0 ; i < wordSize ; i++) letterFound[i] = 0; // 主while循环,如果还有猜测机会并且还没胜利,继续 while(leftTimes > 0 && !win(letterFound, wordSize)){ printf("\n\n您还剩 %d 次机会", leftTimes); printf("\n神秘单词是什么呢 ? "); /* 我们显示猜测的单词,将还没猜到的字母用*表示 例如 : *O**LE */ for (i = 0 ; i < wordSize ; i++) { if (letterFound[i]) // 如果第i+1个字母已经猜到 printf("%c", secretWord[i]); // 打印出来 else printf("*"); // 还没猜到,打印一个* } printf("\n输入一个字母 : "); letter = readCharacter(); // 如果用户输入的字母不存在于单词中 if (!researchLetter(letter, secretWord, letterFound)) { leftTimes--; // 将剩余猜测机会数减1 } } if (win(letterFound, wordSize)) printf("\n\n胜利了! 神秘单词是 : %s\n", secretWord); else printf("\n\n失败了! 神秘单词是 : %s\n", secretWord); return 0; } // 判断是否胜利 int win(int letterFound[], long wordSize) { int i = 0; int win = 1; // 1为胜利,0为失败 for (i = 0 ; i < wordSize ; i++) { if (letterFound[i] == 0) win = 0; } return win; } // 在所要猜的单词中查找用户输入的字母 int researchLetter(char letter, char secretWord[], int letterFound[]) { int i = 0; int correctLetter = 0; // 0表示字母不在单词里,1表示字母在单词里 // 遍历单词数组secretWord,以判断所猜字母是否在单词中 for (i = 0 ; secretWord[i] != '\0' ; i++) { if (letter == secretWord[i]) // 如果字母在单词中 { correctLetter = 1; // 表示猜对了一个字母 letterFound[i] = 1; // 对于所有等于所猜字母的数组位置,都将其数值变为1 } } return correctLetter; } char readCharacter() { char character = 0; character = getchar(); // 读取一个字母 character = toupper(character); // 把这个字母转成大写 // 读取其他的字符,直到 \n (为了忽略它) while(getchar() != '\n') ; return character; // 返回读到的第一个字母 }
好了,这个小游戏已经写完了,编译运行看看吧!
gcc dictionary.c hangman.c -o hangman ./hangman
优化建议
因为我们的项目是在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