C与C++游戏项目练习9:接金币游戏简易版

本文记录了一位开发者使用C++编写接金币游戏的过程,包括改进挡板类设计、实现鼠标控制、使用继承与多态以及增加ESC退出功能。开发者反思了使用EasyX库的依赖,并尝试摆脱它,过程中遇到了问题,最终实现了一个基本健全的游戏版本。文章还分享了游戏开发中的一些技巧和经验。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

## 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
在这里插入图片描述
在这里插入图片描述
费了老大力气,终于把这一节的课后题写完了,一个弹跳的小球居然能联系到接金币、快速敲字母游戏,神奇的相似相通。
看看下一期的主角是什么:
在这里插入图片描述
感谢您能看到这里,一起成为更好的自己~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值