C与C++游戏项目练习7:快速敲字母游戏简易版(关于解决书上课后题的一个小插曲~~)
学习中的一个小插曲~~
学到童晶老师这本书的用数组实现弹跳的小球时,我被课后题难住了。
“按一个空格就要生成一个小球”,emm我懂想法是用面向对象语言,按一下空格就实例化一个类对象,但是鉴于我自学的C++实在是基础不牢地动山摇,一时半会竟然还把我难住了···········
所幸,我之前大一做工程实践项目时看了一个C++游戏编程课程,上面教的“快速敲字母”游戏的方法我感觉和课后两个题十分相似(比如说按空格生成一个小球——不停自动生成字母;按键移动容器来接金币———按键盘上对应键消除字母),于是我打算回顾一下我学的这门课程,在敲字母游戏的基础上来改进代码。(有句话说得好,“你学过的知识一定在未来的某个地方等着你”,真的是太对了!!!)
这次的代码有亿点点长:
建议使用DEV C++运行!VS2019的话字母落地会报错
建议使用DEV C++运行!VS2019的话字母落地会报错
建议使用DEV C++运行!VS2019的话字母落地会报错
建议使用DEV C++运行!VS2019的话字母落地会报错
#include<iostream>
//#pragma comment(lib,"winmm.lib")//使用lib'文件。使用PlaySound播放音效用
#include<Windows.h>
#include<conio.h>//使用_getch()
#include<vector>//LetterShower类使用
#include<time.h>//使产生字母具有随机性
using namespace std;
HANDLE handle;//获取输出设备句柄
COORD crd;//用于设置输出坐标(因为这两个变量会一直用,不停改变输入位置,所以设为全局变量)
const short GROUND = 27;//地面水平线的纵坐标,字母落到这里判定落地
const short STD_WIDTH = 60;//记录宽度,便于输出一整排的边框等
const short HEALTH = 5;//满血量设置为5
class Vitality//血槽类
{
private:
short vitality;//实时血量
public:
Vitality() { vitality = HEALTH; }//构造函数,将初始血量设置为满血(HEALTH=5)
void ShowVitality()
{
crd.X = STD_WIDTH * 0.6; crd.Y = 0;
SetConsoleCursorPosition(handle, crd);
SetConsoleTextAttribute(handle, FOREGROUND_RED | FOREGROUND_GREEN|FOREGROUND_INTENSITY);//设为黄色字体,高亮
cout << "HP: ";//hit points血量
for (int i = 0; i < HEALTH; i++)//每次都输出五个血量
{
cout << (i < vitality ? "■" : "□");//如果小于实际血量vitality,输出实心方框,反之输出空心方框
}
SetConsoleTextAttribute(handle, FOREGROUND_RED | FOREGROUND_GREEN|FOREGROUND_BLUE);//恢复黑底白字
}
short GetWound(short n = -1)//如果没有传入参数的话,默认扣血量-1
{
vitality += n;
ShowVitality();//及时调用显示函数更新血量
return vitality;
}
short GetRestore()//恢复生命力,便于玩家进行下一轮游戏
{
vitality = HEALTH;
ShowVitality();//实时更新显示
return vitality;
}
};
Vitality vty;//将该Vitality对象定义为全局变量,便于在LetterShower的类函数中访问
struct Letter //字母结构体,用于储存字母的相关信息
{
char letter;//当前字母
short x;//x坐标
short y;//y坐标
};//字母掉落的事件在其他类中实现,此处只需要字母的基本信息
class LetterShower
{
private:
vector<Letter> letters[26];//建立26个成员的向量数组,下标为0的元素存放字母A的出现、下落等情况,以此类推
//新产生一个字母,就在对应的向量数组成员中把对应元素增加一个,把掉落的字母Letter类型的数据放入对应的向量中;
//假如一个字母被玩家消除或者已经掉落到地面上,也要在对应下标的向量中删除那个元素
short score;//保存得分
short delay;//字母掉落时延迟的毫秒数
bool Ground(Letter * l)//判断传入的该字母是否落地
{
return l->y >= GROUND;//l的纵坐标大于地面,说明落地,return true,反之return false
}
public:
LetterShower() { score = 0; }//构造函数,分数初始化为0
void GenerateLetter()//产生并下落字母
{
Letter l = { 'A' + rand() % 26,rand() % STD_WIDTH,1 };//产生0-25的随机数+A的ASCII码,得到随机一个A-Z的字母,屏幕上横坐标任意,纵坐标为1(y=0是显示血槽的)
letters[l.letter -'A'].push_back(l);//新产生的字母l压入向量对应下标,push_back()传值压入,不用考虑l的生命周期
}
short Fall()//管理所有字母往下掉落一次
{
short i;
vector<Letter>::iterator itr;
for (i = 0; i < 26; i++)//A-Z的遍历
{
for (itr = letters[i].begin(); itr != letters[i].end(); /*itr++*/)//挨个遍历每个字母
{
crd.X = itr->x; crd.Y = itr->y;//获得字母原来的位置坐标,准备输出空格将其删除
SetConsoleCursorPosition(handle, crd);
cout << " ";
if (Ground(&(*itr)))//如果触地,需要处理扣血
//if(crd.Y>GROUND)
{
if (vty.GetWound() <= 0)//血量减少到小于等于0,游戏结束
{
crd.X = 13; crd.Y = GROUND + 2;//GROUND+1是输出的边框,所以提示语可以GROUND+2输出
SetConsoleCursorPosition(handle, crd);
SetConsoleTextAttribute(handle,BACKGROUND_BLUE|BACKGROUND_GREEN|BACKGROUND_RED);//白色背景
cout << "Game Over!" << endl;
SetConsoleTextAttribute(handle, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED);//恢复默认黑色背景
//PlaySound(".\\sound\\over.wav",SND_ASYNC|SND_FILENAME);错误写法,参数太少
//PlaySound(TEXT(".\\sound\\over.wav"),NULL, SND_ASYNC | SND_FILENAME);懒得找音频,省略了
//BOOL PlaySound(LPCSTR pszSound, HMODULE hmod,DWORD fdwSound);
//SND_ASYNC用异步方式播放声音,PlaySound函数在开始播放后立即返回执行下一步代码。
//SND_FILENAMEpszSound参数指定了WAVE文件名。
printf("\a");
return -1;//返回-1告知本函数的调用块:游戏已经结束,记得把函数返回值从void改为short
}
//游戏还未结束,只需要把字母消除掉
letters[i].erase(itr);//将已经落地的字母从向量中清除掉(此时迭代器已经指向下一个字母了,所以要注释掉for循环的itr++,不然就重复相加了)
//PlaySound(TEXT(".\\sound\\over.wav"),NULL, SND_ASYNC | SND_FILENAME);懒得找音频,省略了
//break;不可以用break,因为这个循环内除了落地判断,还要处理字母的下落,如果检测到落地就退出循环
//那么其他字母就会停止下落导致悬停在空中
continue;//已经有一个字母落地的话,其他相同的字母肯定是比这个后生成,会更晚掉落
}
itr->y++;//未触地就继续下落,y坐标+1
crd.X = itr->x; crd.Y = itr->y;//更新输出坐标
SetConsoleCursorPosition(handle, crd);
cout << itr->letter;//在新位置输出字母
itr++;//因为未落地不会执行erase(itr)所以要手动让迭代器指向下一个字母
}
}
return 0;//返回0表示游戏还未结束,正常返回
}
void ShowScore()
{
crd.X = 1;
crd.Y = GROUND + 2;
SetConsoleCursorPosition(handle, crd);
cout << " Score: " << score << " ";//多输出几位空格来,防止上一次得分位数较多没有被完全遮挡
}
void ClearAll()//清空游戏数据,便于开始第二关
{
for (int i = 0; i < 26; i++)
{
while (!letters[i].empty())//向量不为空,一直弹出
{
letters[i].pop_back();
}
}
score = 0;//分数置为0
}
void SetDelay(short d)//设置延迟时间
{
delay = d;
}
void Wait()
{
Sleep(delay);//按SetDelay中设定好的时间休眠
}
void Rain()//反复调用Fall()函数来实现字母不断像下掉落
{
char ch;//保存用户按键信息
vector<Letter>::iterator itr;
Repeat:
while (!_kbhit())
{
if (Fall() == -1)//用Fall的返回值来判断,如果=-1说明游戏结束
{
return;//函数唯一出口
}
Wait();
if (rand ()% 3 == 0)//等可能生成0,1,2,如果正好等于0就生成字母,保证每次循环有1/3的几率产生新字母
{
GenerateLetter();
}
}
ch = _getch();
if(ch>='a'&&ch<='z')//因为一般是按键输入小写所以判断a-z
{
//if (isalpha(ch))此处isalpha不区分大小写,只判断ch是不是字母,但因为后面要用ASCII码之差来找下标,所以不难用这个
if (!letters[ch - 'a'].empty())//该字母对应的向量不为空
{
itr = letters[ch - 'a'].begin();//获取第一个字母,因为是最早push_back产生也是最先下落的
crd.X = itr->x; crd.Y = itr->y;//视觉上从屏幕上擦除
SetConsoleCursorPosition(handle, crd);
cout << " ";
letters[ch - 'a'].erase(itr);//逻辑上擦除,从数组中消除
//PlaySound(TEXT(".\\sound\\erase.wav"),NULL,SND_ASYNC|SND_FILENAME);懒得找资源,省去了
printf("\a");
score++;
ShowScore();//实时更新分数
}
}
goto Repeat;//回到while (!_kbhit()),也可用while循环代替Repeat
}
};
void PrintLevel(short lv)//注意此函数并没有封装在LetterShower中
{
crd.X = 4; crd.Y = 0;//最上面一行显示
SetConsoleCursorPosition(handle, crd);
switch (lv)
{
case 0:cout << "Level: EASY"; break;
case 1:cout << "Level: INTERMEDIATE"; break;
case 2:cout << "Level: HARD"; break;
}
}
void Welcome()
{
crd.X = 17; crd.Y = 10;
SetConsoleCursorPosition(handle, crd);
cout << "Welcome to Tyoe Game!" << endl;
cout << "\t Press any key to continue..." << endl;//一个制表符\t+4个空格对齐
}
short ShowMenu()//返回值显示用户所选难度级别,0--容易,1--中级,2--困难
{
system("cls");//清屏
crd.X = 10; crd.Y = 10;
SetConsoleCursorPosition(handle, crd);
cout << "Which level do you want to choose?" << endl;
short result = 0;
char ch = 0;//接受用户按键
do
{
if (ch == 75)//左方向键
{
result = (result + 2) % 3;//0变2,1变0,2变1
}
else if (ch == 77)//右方向键
{
result = (result + 1) % 3;//0变1,1变2,2变0
}
crd.X = 14; crd.Y = 12;
SetConsoleCursorPosition(handle, crd);
if (result == 0)//这段代码的意思是,如果result=0,就把第一项的背景色设置为绿色,否则就设为默认
{
SetConsoleTextAttribute(handle, BACKGROUND_GREEN);//背景色设置为绿色
}
else
{
SetConsoleTextAttribute(handle, FOREGROUND_BLUE|FOREGROUND_RED|FOREGROUND_GREEN);
}
cout << " EASY ";//美观起见,前后加空格,防止E和Y离背景色太近
crd.X = 24; crd.Y = 12;
if (result == 1)
{
SetConsoleTextAttribute(handle, BACKGROUND_GREEN);//背景色设置为绿色
}
else
{
SetConsoleTextAttribute(handle, FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_GREEN);
}
cout << " INTERMEDIATE ";
crd.X = 42; crd.Y = 12;
if (result == 2)
{
SetConsoleTextAttribute(handle, BACKGROUND_GREEN);//背景色设置为绿色
}
else
{
SetConsoleTextAttribute(handle, FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_GREEN);
}
cout << " HARD ";
ch=_getch();//阻塞,注意包含#include<conio.h>
if (ch == 0)//方向键(比如do--while循环开头判断的左方向键75)需要接收两次,第一次接收到的是0
{
ch = _getch();
}
} while (ch!=VK_RETURN);//只有按下回车键才会退出游戏
SetConsoleTextAttribute(handle,FOREGROUND_BLUE|FOREGROUND_GREEN|FOREGROUND_RED);//恢复默认文字和背景色
return result;
}
void DrawGround()//绘制地面
{
crd.X = 0; crd.Y = GROUND + 1;
SetConsoleCursorPosition(handle, crd);
for (short i = 0; i < STD_WIDTH; i++)
{
cout << "=";
}
}
int main()
{
handle = GetStdHandle(STD_OUTPUT_HANDLE);//因为要重复使用句柄,为了防止因为本语句放在一个函数里而这个函数的调用顺序不对(比如说有函数(直接使用句柄)在他之前调用,却以为已经在该函数里获取了句柄(其实该函数没有调用或者调用晚了),就会导致找不到句柄)造成的逻辑错误,把这句话放在主函数中
system("mode con cols=60 lines=31");//设置窗体尺寸
Welcome();//显示欢迎界面
_getch();
srand((unsigned)time(NULL));//产生随机种子,程序时间作为参数传入
char choice;
short level;//接收关卡选择
//Vitality vty;//前面已经定义过全局变量,不能重复定义
LetterShower Is;//用于输出分数,因为分数是该类的一个成员变量
do
{
system("cls");//清除欢迎界面显示文字以及之前游戏遗留的字母
level = ShowMenu();
system("cls");//清除菜单
vty.GetRestore();//确保血量恢复到满(可能上一关有扣血)
vty.ShowVitality();//显示血量
PrintLevel(level);//输出难度
DrawGround();//显示地面
Is.ClearAll();//清空向量中现有字母,重置分数为0
Is.ShowScore();
Is.SetDelay(300 - level * 60);
Is.Rain();//不断循环持续下去,如果离开此函数就表示游戏已经结束
cout << " Try Again? (y or n)";
cin >> choice;
} while (choice=='y'||choice=='Y');
return 0;
}
以下是一些敲代码过程中遇到的一些问题和自己百度查到的解析整理:
老师的代码在VC++6.0可以通过,但是VS2019这会遇到问题。
幸好网上有前辈提出来并且已经解决了这个问题~~
https://bbs.csdn.net/topics/392861905
评论区有位大神也提供了一种思路,貌似还要简单一些
代码里有个PlaySound函数是用来播放声音的,老师那样写的话,在VS2019会报错“PlaySound参数太少”
解决方案在这里:
https://www.thinbug.com/q/20831366
我为了省去找音源的麻烦,换成了printf("\a");
之前项目里面用过这个函数,找了很久这个函数后面类似于SND_ASYNC(用异步方式播放声音,PlaySound函数在开始播放后立即返回执行下一步代码)这样的播放标志及其含义,今天居然百度到了一个大合集,极其快乐~~
https://blog.csdn.net/ccx_john/article/details/12494129
我很想知道下图这能不能换成一个letters->clear
找了自己之前C++入门时看的讲义:
百度查了一下区别,感觉emm看的半懂不懂的,谨慎起见还是按照老师这样写吧,如果有大神明白的话还请不吝赐教~~
https://blog.csdn.net/vieri_ch/article/details/1191390
https://bbs.csdn.net/topics/391052258
不知道为什么。他字母落地有个报错“迭代器不兼容”,我检查了一下感觉并不是我代码敲错了,emm··········如果有大神知道的话还请不吝赐教
评论区见仁见智各持己见
(这个问题的解决方式上面有提到)
以下内容是诚心推荐,并非恰烂钱吃饭,没有收任何广告费!!!
以下内容是诚心推荐,并非恰烂钱吃饭,没有收任何广告费!!!
以下内容是诚心推荐,并非恰烂钱吃饭,没有收任何广告费!!!
安利一下我学过的这门课,是工程实践查资料时偶然发现的网课,当时大一做的是走迷宫小游戏(如果有朋友感兴趣的话我可以把删去个人学校信息的代码发出来共享),里面按钮的做法就是看这个网课学的,最后效果非常之好(虽然写的代码是C和C++混血的奇怪玩意),我自己都佩服自己的那种。
链接:https://www.51zxw.net/List.aspx?cid=717#!fenye=5
付费,但是特别便宜,一节课平均下来才几毛钱,对学生党来说非常友好了。
这个老师的代码功底很好,讲的都是效率高的巧办法,我个人感觉很有帮助。
我参考的主要是这部分的课程教授代码,感兴趣的朋友可以看看:
这个老师教的两门课程我都买了,真的太赞了,而且内容特别良心,就完全是想找但是很难找的内容,包括我后来也是在这个网站(不过不是这个老师)上自学的3dmax建模(说来惭愧,学了一半我太懒,咕咕咕了现在都还没看完网课·········)
感谢你能看到这里,一起努力成为更好的自己~~