## C与C++游戏项目练习9:接金币游戏简易版
还是只能在devC++里面运行,不要用VS!!!
还是只能在devC++里面运行,不要用VS!!!
还是只能在devC++里面运行,不要用VS!!!**
宝书镇楼
与快速敲字母和弹跳的小球3.0的主要区别以及改进:
1.把挡板(这里面是叫盘子)类和血量一样写成了全局变量,并且把ShowPlate、ClearPlate、MovePlate等函数归类到盘子类的成员函数,而不是由游戏控制类GameCtroller类控制,因为盘子的绘制、清除以及移动更符合盘子这个类的行为(就好比你能吃饭,老师喊你吃饭,老师应该是发出指令,但是吃饭这个能力属性是你有的),个人认为这样更加规范。
2.弹跳的小球3.0里面挡板能上下左右移动,这里根据惯常玩法只制作了左右移动功能,,但是,我灵机一动——
通过重载MovePlate成员函数改成捕捉鼠标事件使用Point类和按键都可以来控制盘子的左右移动,
void MovePlate(char input)//根据键盘输入移动盘子(一次一格,好控制)
{
if (input == 'a'&& GetLeft() > 0)//接收到a,并且不抵在左边界,盘子左移
{
x--;
ChangeBound();//更改盘子边界
}
else if (input == 'd'&& GetRight()< STD_WIDTH)//接收到d,并且不抵在右边界,盘子右移
{
x++;
ChangeBound();//更改盘子边界
}
}
void MovePlate(int distance)//重载成员函数,通过鼠标移动距离来移动盘子(一次一段,不好控制)
{
distance %= STD_WIDTH;//防止距离超出范围,控制在游戏界面内
if (GetLeft() > distance && GetRight() + distance < STD_WIDTH)//在可移动范围内,最左边要至少能移动一个distance的距离,右边同理
{
x += distance;
ChangeBound();//更改盘子边界
}
else//避免x卡在边界处无法移动
{
if (x <= 0)
{
x = 1+ridus;//起点加半径
ChangeBound();
}
else
{
x = STD_WIDTH - 1-ridus;
ChangeBound();
}
}
}
*注意,鼠标不能拖拽太快,否则会造成卡顿以及出现多个盘子
**
顺便提一句,最早先让我知道有捕捉鼠标操作也是这个老师的网课教我的哦~~但是他这里面讲的内容需要配合EasyX实现
经过这个网课的培训我大一下工程实践1做走迷宫游戏(当时还是只学了C语言,代码写了1563行,带EasyX图形界面,可把我得瑟坏了)拿了全班第一,真的很感谢这位老师!没有他我做不出来这么好的项目,也就是这个时候我产生了对编程做游戏的浓厚兴趣~~~~~~~
我想摆脱对EasyX的依赖(因为这个好像(听说的)过时了而且我走迷宫里面已经用得差不多了,想试试学点新的东西)
于是,我又百度了一下:
鼠标消息参考:写的很详细,但没有解决我的问题
https://blog.csdn.net/iteye_11539/article/details/82302654?utm_medium=distribute.pc_relevant_bbs_down.none-task-blog-baidujs-1.nonecase&depth_1-utm_source=distribute.pc_relevant_bbs_down.none-task-blog-baidujs-1.nonecase
我又继续搜,emmm··········
再来······
https://blog.csdn.net/qq_31567335/article/details/79674152
代码搬过去了,有点看不懂,于是我搜了一下里面的函数:
https://blog.csdn.net/baidu_38494049/article/details/82930099
这篇真的很赞
3.使用了继承和多态,掉落物类FallThings为父类,三个子类Coin、Boom、Gift分别代表具体的掉落物,各自代表的符号、接到加分扣血情况、名称各不相同
但是,结果————
踩了个大雷:在使用迭代器时创建的是FallThings父类迭代器。但是不能把子类对象往里加
https://www.zhihu.com/question/22585094
下面有人说这位答主的回答有问题,但我觉得是指他示例代码没对(此处略),而不是说我这里截取的理论不对
(此踩雷代码现已加入KFC 上传资源包豪华套餐)早知道我不耍这个小聪明搞什么继承多态了,就普普通通一个FallingThings类多好··········果然还是学艺不精啊
4.构造函数加了默认值,即使在调用构造函数的时候,没有提供实参值,不仅不会出错,而且还确保按照默认的参数值对对象进行初始化
5.新增按ESC退出功能,判断输入字符ASCII码是不是等于27即可
插播一个软件问题,如果遇到Dev-C++调试时提示“项目没有调试信息,您想打开项目调试选项并重新生成吗?”这种报错,可以参考如下解决方案:
https://blog.csdn.net/qq_41112170/article/details/102985428
以下是经过多次修改之后基本健全的代码:
#include<iostream>
#include<time.h>
#include<conio.h>
#include<windows.h>
#include<vector>
HANDLE handle= GetStdHandle(STD_OUTPUT_HANDLE);
COORD crd;
using namespace std;
const short HEALTH = 5;//满血总血量为5
const short STD_WIDTH = 60;//记录宽度,便于输出一整排的边框等
const short GROUND = 27;//地面水平线的纵坐标
void gotoxy(int x, int y)//光标移到(x,y)位置处(封装了SetConsoleCursorPosition(handle, crd)函数,减少代码重复)
{
crd.X = x;
crd.Y = y;
SetConsoleCursorPosition(handle, crd);
}
void ShowBound()//绘制边框
{
//绘制竖边框
for (int i = 0; i < GROUND; i++)
{
gotoxy(STD_WIDTH, i);
printf("|\n");
}
//绘制横边框
for (int j = 0; j < STD_WIDTH; j++)
{
gotoxy(j, GROUND);
printf("-");
}
}
class Vitality//血量
{
private:
int vitality;
public:
Vitality() { vitality = HEALTH; }//每次把血量恢复到满血
void ShowVitality()
{
gotoxy(STD_WIDTH * 0.6, 0);
SetConsoleTextAttribute(handle, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY);//设为黄色字体,高亮
cout << "HP: ";//hit points血量
for (int i = 0; i < HEALTH; i++)
{
cout<<(i < vitality ? "■" : "□");//如果小于实际血量vitality,输出实心方框,反之输出空心方框
}
cout<<"Vitality: "<<vitality;
SetConsoleTextAttribute(handle, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);//恢复黑底白字
}
short GetVitality()//获取当前血量
{
return vitality;
}
short GetThing(short n = 0)//如果没有传入参数,默认每次不加不减血量
{
vitality += n;//接到炸弹-1,接到血包+0-3点血
if(vitality>5)
{
vitality=5;
}
ShowVitality();//更新扣血之后的血量
return vitality;
}
short GetRestore()
{
vitality = HEALTH;
ShowVitality();//更新满血复活之后的血量
return vitality;
}
};
Vitality vitality;//控制血量的全局变量
class FallThings//所有掉落物的基类
{
protected://不能写成private,否则子类无法修改
int x;
int y;
public:
bool active;
string Name;
int grade;
int num;//随机生成掉落物用
char letter;//符号默认是金币
FallThings(int x=rand() % STD_WIDTH - 1,int y=1)//rand() % STD_WIDTH - 1生成0-STD_WIDTH-2的随机数,因为最上面要输出血槽,所以要y=1
{
this->x = x;
this->y = y;
active=true;
grade = 1;
num = rand() % 7;//0-4金币,5炸弹,6礼物,这样金币概率最大
if(num==5)
{
Name = "Boom";
letter = '@';//炸弹
grade=-1;
}
else if(num==6)//血包
{
Name = "Vitality";
letter = '+';
grade=rand()%4;
}
else//金币
{
Name = "Coin";
letter = 'o';
grade=1;
}
}
int GetX()//返回x坐标
{
return x;
}
int GetY()//返回y坐标
{
return y;
}
// 提供接口框架的纯虚函数
void Fall()//实现下落,沿着垂直方向往下掉就行
{
y++;
}
};
class Plate//盘子类,用来接掉落物
{
private:
int x;
int y ;//默认位置是屏幕正中间,离地面一格远
int ridus;//半径
int left;//左边界
int right;//右边界
public:
Plate(int x=STD_WIDTH / 2,int y=GROUND - 1,int r=5)
{
this->x = x;
this->y = y;
this->ridus = r;
left = x - ridus;//左边界
right = x + ridus;//右边界
ChangeBound();//确定左右边界
}
int GetX()//返回x坐标
{
return x;
}
int GetY()//返回y坐标
{
return y;
}
int GetLeft() { return left; }
int GetRight() { return right; }
void ChangeBound()//在圆心坐标改变的时候改变左右边界
{
left = x - ridus;//左边界
right = x + ridus;//右边界
}
void MovePlate(char input)//根据键盘输入移动盘子(一次一格,好控制)
{
if (input == 'a'&& GetLeft() > 0)//接收到a,并且不抵在左边界,盘子左移
{
x--;
ChangeBound();//更改盘子边界
}
else if (input == 'd'&& GetRight()< STD_WIDTH)//接收到d,并且不抵在右边界,盘子右移
{
x++;
ChangeBound();//更改盘子边界
}
}
void MovePlate(int distance)//重载成员函数,通过鼠标移动距离来移动盘子(一次一段,不好控制)
{
distance %= STD_WIDTH;//防止距离超出范围,控制在游戏界面内
if (GetLeft() > distance && GetRight() + distance < STD_WIDTH)//在可移动范围内,最左边要至少能移动一个distance的距离,右边同理
{
x += distance;
ChangeBound();//更改盘子边界
}
else//避免x卡在边界处无法移动
{
if (x <= 0)
{
x = 1+ridus;//起点加半径
ChangeBound();
}
else
{
x = STD_WIDTH - 1-ridus;
ChangeBound();
}
}
}
void ShowPlate()
{
if(GetLeft()>0&&GetY()<STD_WIDTH)//只在规定范围内画盘子 (不然因为鼠标的移动可能盘子会出现在奇奇怪怪的地方哦)
{
gotoxy(GetLeft(), GetY());//注意横坐标从plate左边界开始,纵坐标是plate的y坐标
for (int i = GetLeft(); i != GetRight(); i++)//整个输出的范围是plate从左边界到右边界
{
printf("*");
}
}
}
void ClearPlate()//清除盘子
{
gotoxy(GetLeft(), GetY());//原来是盘子的地方输出空格
for (int i = GetLeft(); i != GetRight(); i++)
{
printf(" ");
}
}
void RestorePlate()
{
x = STD_WIDTH / 2;//恢复盘子默认位置
y = GROUND - 1;
}
};
Plate plate;//全局变量盘子
class GameCtroller//游戏管理类,控制游戏运作
{
public:
vector<FallThings> fallthings;//用于管理掉落物的动态数组
int score;//本轮得分
GameCtroller() { score = 0; }
short delay;//设置休眠时间
void GameRestore()//重复游戏时用,重置游戏初始数据
{
fallthings.clear();//清空动态数组
vitality.GetRestore();//恢复血量
score = 0;//清空分数
}
void GenerateFallingThings()//每隔一段时间调用,自动生成掉落物
{
gotoxy(rand()%STD_WIDTH ,1);//从最顶端随机列(0-STD_WIDTH-1)生成
for (int i = 0; i < rand() % 5; i++)//每次随机生成0-5个掉落物
{
fallthings.push_back(FallThings());//生成一个匿名的FallThings对象加入vector里
}
}
short Fall()
{
vector < FallThings>::iterator itr;//迭代器,遍历掉落物数组fallthings
for (itr = fallthings.begin(); itr != fallthings.end(); itr++)
{
if (itr->active == true)//活跃状态为true才判断执不执行以下代码
{
gotoxy(itr->GetX(), itr->GetY());//光标移动到当前位置,实施擦除
cout << " ";
//当前元素被接到了或者掉到了地平线以下,需要消除
if (itr->GetY() == plate.GetY() - 1)//y高度=盘子y高度-1,表示被接到了
{
if (itr->num>=0&&itr->num<=4)//接到金币
{
score+=itr->grade;//接到一个金币,得分+1
}
else if (itr->num==5)//接到炸弹
{
vitality.GetThing(itr->grade);//把炸弹对应的扣血量(-1)作为参数传入Vitality,实现接到炸弹血量-1
}
else if (itr->num==6)//接到血包
{
vitality.GetThing(itr->grade);//把礼物对应的加血量(+0-3)作为参数传入Vitality,实现接到血包血量+1
}
if (vitality.GetVitality() <= 0)//本轮扣血加分处理完了,如果发现没血了,就输出游戏结束
{
crd.X = 13; crd.Y = GROUND + 2;//GROUND+1是输出的边框,所以提示语可以GROUND+2输出
SetConsoleCursorPosition(handle, crd);
//SetConsoleTextAttribute(handle, BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED);//白色背景 不注释的话最后输出GameOver就会黑底白字和白底黑子鱼龙混杂,不知道为什么会这样
cout << "Game Over!" << endl;
SetConsoleTextAttribute(handle, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED);//恢复默认黑色背景
return -1;
}
itr->active = false;//活跃设置为false,不再掉落
vitality.ShowVitality();//展示血量
ShowScore();//展示分数
}
else if (itr->GetY() >= GROUND)//当前元素落到地平线以下都没有被接到,直接消除但不扣血(这也是为什么不能和被接到的情况合并处理的原因)
{
itr->active = false;//活跃设置为false,不再掉落
}
else//当前元素还在下落阶段,就让其继续下落
{
itr->Fall();//纵坐标继续加1
gotoxy(itr->GetX(), itr->GetY());//光标移到该元素的新位置(x横坐标不变,y纵坐标+1)
cout << itr->letter;//输出该迭代器对应符号,金币--o,炸弹--@,血包—+
//itr++;//因为erase会让迭代器自动移到下一个,这里没有erase,就要手动移动到下一个元素
}
}
}
return 0;//正常下落返回0
}
void SetDelay(int d)
{
delay = d;
}
void Wait()
{
Sleep(delay);
}
void ShowScore()
{
gotoxy(1, GROUND + 2);
cout << " Score: " << score << " ";//多输出几位空格来,防止上一次得分位数较多没有被完全遮挡
}
};
int main()
{
GameCtroller gamectroller;//实例化游戏控制类
char choice = 'n';
char input;
gamectroller.SetDelay(300);
srand((unsigned)time(NULL));//产生随机种子,程序时间作为参数传入
do
{
system("cls");//清屏
plate.RestorePlate();//盘子恢复默认位置
ShowBound();//绘制边框
gamectroller.GameRestore();//重置游戏
vitality.GetRestore();//恢复满血
vitality.ShowVitality();//展示血量
POINT last_p;//每次开始游戏都重新定义,保证从初始位置开始捕捉鼠标
POINT p;
do
{
gamectroller.ShowScore();//输出得分
plate.ShowPlate();//更新盘子位置
GetCursorPos(&last_p);//获取鼠标坐标
last_p.y = GROUND - 1;//防止光标随鼠标移动,影响盘子的位置
Sleep(50); //休息0.05s间隔必须短,因为鼠标移动很快,间隔长了会很卡
GetCursorPos(&p);//再次获取鼠标坐标
p.y = GROUND - 1;
if (p.x != last_p.x)//现在x与之前不一样,说明在0.1s内移动了鼠标,就处理移动挡板
{//鼠标在范围内移动才行
plate.ClearPlate();//清除原来的挡板
plate.MovePlate(int(p.x-last_p.x));//根据按键移动挡板,强制转换为int,不然会报错“对重载调用不明确”
plate.ShowPlate();//展示类中包含的挡板移动之后
}
if (_kbhit())//检测到键盘输入,可能挡板移动(ad)
{
input = _getch();
if (input == 'a' || input == 'd')//根据输入的是a或者d来决定挡板的移动
{
plate.ClearPlate();//清除原来的挡板
plate.MovePlate(input);//根据按键移动挡板
plate.ShowPlate();//展示类中包含的挡板移动之后
}
if (input == 27)
{
break;//按ESC退出
}
}
gamectroller.GenerateFallingThings();
if (gamectroller.Fall() == -1)
{
break;//-1表示游戏结束,跳出内层循环询问玩家是否开始下一局
}
gamectroller.Wait();//每300毫秒自动生成下一轮字母
} while (true);
cout << " Try Again? (y or n)";
cin >> choice;
} while (choice == 'y' || choice == 'Y');
return 0;
}
运行效果:
看,鼠标移动盘子的效果还是有缺陷,有时候鼠标移动速度过快,盘子就找不到了,而且盘子的效果也远没有市面上游戏那么灵活,我觉得是我没有用专门的控件(比如EasyX?)导致的。尝试推卸责任ing
费了老大力气,终于把这一节的课后题写完了,一个弹跳的小球居然能联系到接金币、快速敲字母游戏,神奇的相似相通。
看看下一期的主角是什么:
感谢您能看到这里,一起成为更好的自己~~