c++求出时间间隔_第13章 祖玛(《C和C++游戏趣味编程》配套教学视频)

b061bb166c3c094ae27d8b15c4626ec1.png

(图书介绍:童晶:《C和C++游戏趣味编程》新书预告)

本章我们将编写祖玛游戏,各种颜色的小球沿着轨道移动,玩家必须阻止小球进入轨道终点的城堡。鼠标可以移动控制炮台旋转、鼠标右键更换小球颜色、按下鼠标左键发射小球。发射的小球进入轨道,如果周围有连续三个相同颜色的小球即可消除,效果如图所示。

为了实现动态数据结构,首先学习了链表和C++标准模板库。然后利用面向对象知识和STL的vector,依次实现了顶点类、轨迹类和小球类;接着实现了炮台类,完成炮台旋转、发射小球和胜负判断的功能。

知乎视频​www.zhihu.com
zhihu-card-default.svg

讲解视频:

知乎视频​www.zhihu.com
zhihu-card-default.svg

最终代码:

#include <graphics.h>  
#include <conio.h>
#include <time.h>
#include <vector>
#include <algorithm>
#pragma comment(lib,"Winmm.lib")
using namespace std;
#define  WIDTH 1000 // 窗口宽度
#define  HEIGHT 700 // 窗口高度
#define  Radius 25 //  小球半径
#define  ColorNum 5 //  小球颜色种类数目
COLORREF  colors[ColorNum] = {RED,BLUE,GREEN,YELLOW,MAGENTA}; // 定义数组保存所有的颜色

// 求两点之间的距离函数
float Distance(float x1,float y1,float x2,float y2)
{
	float xd = x1 - x2;
	float yd = y1 - y2;
	float length = sqrt(xd*xd+yd*yd); 
	return length;
}

void sleep(DWORD ms)  // 精确延时函数
{
	static DWORD oldtime = GetTickCount();
	while(GetTickCount() - oldtime < ms)
		Sleep(1);
	oldtime = GetTickCount();
}

void PlayMusicOnce(TCHAR fileName[80]) // 播放一次音乐函数
{
	TCHAR cmdString1[50];
	_stprintf(cmdString1, _T("open %s alias tmpmusic"), fileName); // 生成命令字符串
	mciSendString(_T("close tmpmusic"), NULL, 0, NULL); // 先把前面一次的音乐关闭
	mciSendString(cmdString1, NULL, 0, NULL); // 打开音乐
	mciSendString(_T("play tmpmusic"), NULL, 0, NULL); // 仅播放一次
}

class Point // 定义顶点类
{
public:
	float x,y; // 记录(x,y)坐标
	Point() // 无参数的构造函数
	{
	}
	Point (float ix,float iy) // 有参数的构造函数
	{
		x = ix;
		y = iy;
	}
};

class Path  // 定义轨迹类
{
public:
	vector<Point> keyPoints; //  记录轨迹上的一些关键点,关键点之间以直线相连
	float sampleInterval; // 对特征点连成的轨迹线,进行均匀采样的间隔
	vector<Point> allPoints; //  所有以采样间隔得到的采样点

	void getAllPoints() // 以采样间隔进行采样,得到所有的采样点
	{
		int i;
		// 对关键点依次连接形成的多条线段进行遍历
		for (i=0;i<keyPoints.size()-1;i++)
		{
			float xd = keyPoints[i+1].x - keyPoints[i].x;
			float yd = keyPoints[i+1].y - keyPoints[i].y;
			float length = sqrt(xd*xd+yd*yd); // 这一段直线段的长度

			int num = length/sampleInterval; // 这一段直线段要被采样的个数
			for (int j=0;j<num;j++)
			{
				float x_sample = keyPoints[i].x + j*xd/num;
				float y_sample = keyPoints[i].y + j*yd/num;
				allPoints.push_back(Point(x_sample,y_sample)); // 添加进去所有的采样点
			}
		}
		// 还有最后一个关键点
		allPoints.push_back(Point(keyPoints[i].x,keyPoints[i].y));
	}

	void draw() // 画出轨迹
	{
		setlinecolor(RGB(0,0,0)); // 设置线条颜色
		setfillcolor(RGB(0,0,0)); // 设置填充颜色
		// 画出关键点依次连接形成的多条线段
		for (int i=0;i<keyPoints.size()-1;i++)
			line(keyPoints[i].x,keyPoints[i].y,keyPoints[i+1].x,keyPoints[i+1].y); 
		// 所有采样点处,分别画一个小圆点
		for (int i=0;i<allPoints.size();i++)
			fillcircle(allPoints[i].x,allPoints[i].y,2); 
	}

	~Path()  // 析构函数
	{
		keyPoints.clear(); // 清除vector的内存空间
		allPoints.clear();
	}
};

class Ball // 定义小球类
{
public:	
	Point center; // 圆心坐标
	float radius; // 半径
	int colorId;  // 小球的颜色序号,具体颜色在colors数组中取
	int indexInPath; // 小球位置在Path的allPoints中的对应序号
	int direction; // 小球移动方向,1向终点,-1向起点,0暂停

	void draw() // 画出小球
	{
		setlinecolor(colors[colorId]);
		setfillcolor(colors[colorId]);
		fillcircle(center.x,center.y,radius); 
	}

	void movetoIndexInPath(Path path)
	{
		// 让小球移动到 Path的allPoints中的indexInPath序号位置
		center = path.allPoints[indexInPath];
	}

	void initiate(Path path) // 初始化小球到path最开始的位置上
	{
		radius = Radius; //  半径
		indexInPath = 0; // 初始化序号
		direction = 0; // 初始静止
		movetoIndexInPath(path); // 移动到Path上面的对应序号采样点位置		
		colorId = rand() % ColorNum; // 随机颜色序号
	}

	// 让小球沿着轨迹Path移动,注意不要越界
	// direction为0暂时不动,direction为1向着终点移动,direction为-1向着起点移动
	void changeIndexbyDirection(Path path) 
	{
		if (direction==1 && indexInPath+1<path.allPoints.size())
			indexInPath++;
		else if (direction==-1 && indexInPath-1>=0)
			indexInPath--;
	}
};

class Cannon // 炮台类,包括角色图片,还有一个小球
{
public:
	IMAGE im; // 角色图片
	IMAGE im_rotate; // 角色旋转后的图片
	float x,y; // 中心坐标
	Ball ball;  // 一个可以绕着中心旋转,变颜色的小球
	float angle; // 旋转角度

	void draw() // 一些绘制函数
	{
		rotateimage(&im_rotate,&im,angle,RGB(160,211,255),false,false);//旋转角色图片
		putimage(x-im.getwidth()/2,y-im.getheight()/2,&im_rotate); // 显示旋转后角色图片
		ball.draw(); // 绘制这个待发射的小球
	}

	void setBallPosition() // 生成炮台小球的坐标
	{
		ball.center.x = x + 100 * cos(angle);
		ball.center.y = y + 100 * sin(angle);
	}	

	void updateWithMouseMOVE(int mx,int my) // 根据鼠标的移动位置来更新
	{
		// 求出炮台到鼠标的角度
		float xs = mx - x;
		float ys = my - y;
		float length = sqrt(xs*xs+ys*ys);
		if (length>4) // 鼠标距离中心位置过近,不处理
		{
			angle = atan2(-ys,xs); // 求出炮台旋转角度

			// 也顺便求出炮台附带的球的位置
			ball.center.x = x + 100 * xs/length;
			ball.center.y = y + 100 * ys/length;
		}
	}	

	void updateWithRButtonDown() // 当鼠标右键点击时,改变小球的颜色
	{
		// 更改炮台要发射的小球的颜色
		ball.colorId +=1;
		if (ball.colorId==ColorNum)
			ball.colorId =0;
	}
};

// 在Balls中序号i位置球,寻找其前后有没有和他颜色一样,且多余个连续靠近的球
// 如果有的话,就删除掉,返回的结果是删除掉的小球的个数
// 如果一个没有删除,就返回0
int eraseSameColorBalls(int i,Ball fireball,Path &path,vector <Ball> &balls)
{
	// 记录下前后和插入的小球颜色一样的序号,后面去重复,得到对应的要删除的序号
	vector<int> sameColorIndexes;  
	int forward = i; 
	int backward = i; 
	sameColorIndexes.push_back(i); // 首先把i添加到vector中

	// 向Path终点方向寻找,也就是向最开始加入的球方向寻找
	while(forward>0 &&  balls[forward].colorId==fireball.colorId)
	{
		sameColorIndexes.push_back(forward);
		if (balls[forward-1].indexInPath - balls[forward].indexInPath>2*Radius/path.sampleInterval)
			break; // 前面一个球和这个球间距过大,跳出循环
		forward--;
	}
	if (forward==0 && balls[0].colorId==fireball.colorId) // 处理特殊情况,最接近终点的那个球
		sameColorIndexes.push_back(forward);

	// 向Path起点方向寻找,也就是最后加入的球的方向寻找
	while (backward<balls.size()-1 && balls[backward].colorId==fireball.colorId) // 还没有找到最后一个加入的球
	{
		sameColorIndexes.push_back(backward);
		if (balls[backward].indexInPath - balls[backward+1].indexInPath>2*Radius/path.sampleInterval)
			break; // 前面一个球和这个球间距过大,跳出循环
		backward++;
	}
	if (backward==balls.size()-1 && balls[balls.size()-1].colorId==fireball.colorId) // 处理特殊情况,最接近起点的那个球
		sameColorIndexes.push_back(backward);

	// 去除同样颜色小球中重复的序号
	sort(sameColorIndexes.begin(), sameColorIndexes.end());
	vector<int>::iterator ite = unique(sameColorIndexes.begin(), sameColorIndexes.end());
	sameColorIndexes.erase(ite, sameColorIndexes.end());

	int NumSameColors = sameColorIndexes.size();
	if (NumSameColors>=3) // 相同颜色的球达到3个或以上
	{
		int minIndex = sameColorIndexes[0];
		int maxIndex = sameColorIndexes[NumSameColors-1];
		// 把这些球给删掉						
		balls.erase(balls.begin()+minIndex,balls.begin()+maxIndex+1);
		return NumSameColors; // 消除了,返回消除小球数目
	}
	return 0; // 没有消除,返回0
}

// 以下定义一些全局变量
Path path; // 定义轨迹对象
vector <Ball> balls; // 记录多个小球
IMAGE im_role,im_house,im_bk; // 一些图片
Cannon cannon;  // 定义炮台对象
int gameStatus = 0;  // 游戏状态,-1失败,0正常,1胜利

void startup()  // 初始化函数
{	
	mciSendString(_T("open game_music.mp3 alias bkmusic"), NULL, 0, NULL);//打开背景音乐
	mciSendString(_T("play bkmusic repeat"), NULL, 0, NULL);  // 循环播放
	srand(time(0)); // 随机初始化种子
	initgraph(WIDTH,HEIGHT); // 新开一个画面
	cleardevice(); // 清屏
	loadimage(&im_bk, _T("bk.jpg")); // 导入背景图片
	loadimage(&im_role, _T("role.jpg")); // 导入角色图片
	loadimage(&im_house, _T("house.jpg")); // 导入家图片

	// 为轨迹类添加一些关键点
	path.keyPoints.push_back(Point(50, 300));
	path.keyPoints.push_back(Point(50, 600));
	path.keyPoints.push_back(Point(100, 650));
	path.keyPoints.push_back(Point(700, 650));
	path.keyPoints.push_back(Point(700, 550));
	path.keyPoints.push_back(Point(250, 550));
	path.keyPoints.push_back(Point(200, 500));
	path.keyPoints.push_back(Point(200, 200));
	path.keyPoints.push_back(Point(250, 150));
	path.keyPoints.push_back(Point(800, 150));
	path.keyPoints.push_back(Point(850, 200));
	path.keyPoints.push_back(Point(850, 650));
	path.keyPoints.push_back(Point(950, 650));
	path.keyPoints.push_back(Point(950, 100));
	path.keyPoints.push_back(Point(900, 50));
	path.keyPoints.push_back(Point(150, 50));

	path.sampleInterval = Radius/5; // 设置轨迹线的采样间隔,需被Radius整除以便处理
	path.getAllPoints();    // 获得轨迹上的所有采样点

	// 炮台做一些初始化
	cannon.im = im_role; // 炮台角色图片
	cannon.angle = 0; // 初始角度
	cannon.x = 500;  // 中心坐标
	cannon.y = 350;
	cannon.ball.radius = Radius; // 炮台带的小球的半径
	cannon.ball.colorId = rand()%ColorNum; // 炮台小球颜色
	cannon.setBallPosition(); // 设置炮台小球的坐标

	// 先添加一些小球
	for (int i=0;i<15;i++)
	{
		Ball ball;  // 定义一个小球对象
		ball.initiate(path); // 初始化小球到path最开始的位置上	
		balls.push_back(ball); // 把小球ball添加到balls中
	}

	BeginBatchDraw(); // 开始批量绘制
}

void show()  // 绘制函数
{
	putimage(0,0,&im_bk); // 显示背景图片
	putimage(30,10,&im_house); // 显示房子图片
	//path.draw();  // 画出轨迹
	cannon.draw(); // 画出炮台	
	for (int i=0;i<balls.size();i++)
		balls[i].draw();  // 画出所有小球

	// 设置字体显示属性
	setbkmode(TRANSPARENT);
	settextcolor(RGB(255,0,0)); 
	settextstyle(60, 0, _T("宋体"));
	if (gameStatus==1) // 输出游戏胜利
		outtextxy(WIDTH*0.35, HEIGHT*0.35, _T("游戏胜利 :)"));
	else if (gameStatus==-1) // 输出游戏失败
		outtextxy(WIDTH*0.35, HEIGHT*0.35, _T("游戏失败 :("));

	FlushBatchDraw(); // 批量绘制
}

void updateWithoutInput() // 和输入无关的更新
{	
	static clock_t start = clock(); // 记录第一次运行时刻
	clock_t now = clock(); // 获得当前时刻
	// 计算程序目前一共运行了多少秒
	int nowSecond =( int(now - start) / CLOCKS_PER_SEC);
	// 100秒内,时间每过10秒钟,新增一批小球
	if (nowSecond%10==0 && nowSecond<=100 && gameStatus==0) 
	{
		Ball ball;  // 定义一个小球对象
		ball.initiate(path); // 初始化小球到path最开始的位置上
		balls.push_back(ball); // 把小球ball添加到balls中
	}
	if (balls.size()==0) // 小球清空完了
	{
		if (nowSecond>100) // 时间到了,游戏胜利
			gameStatus = 1; // 游戏胜利
		return; // 没有到截止时间,小球清空了,等到到时间后产生新的小球
	}
	// 第一个球跑到终点了,游戏失败
	if (balls[0].indexInPath >= path.allPoints.size()-1)
	{
		gameStatus = -1; // 游戏失败
		return;
	}

	int i;	
	for (i=0;i<balls.size();i++)
		balls[i].direction = 0; // 先让所有小球的速度设为0

	//balls向前移动的源动力来自最后面一个小球,最后一个小球direction=1
	//如果终点方向前面一个小球和这个小球正好相切,则其direction也为1,否则direction为0
	i = balls.size() - 1; // 最后一个小球
	balls[i].direction = 1; // 最后一个小球向前运动

	while (i>0)  // 一直向前判断,还没有遍历到最前面一个小球
	{
		// 如果前后两个小球正好相切
		if (balls[i-1].indexInPath-balls[i].indexInPath <= 2*Radius/path.sampleInterval)
		{
			balls[i-1].direction = 1; // 前一个小球的方向也是向前
			// 对前一个小球的indexInPath进行规则化,确保正好相切
			balls[i-1].indexInPath = balls[i].indexInPath+2*Radius/path.sampleInterval; 
			i--;
		}
		else // 有一个小球不直接接触,就停止向前速度的传递
			break; // 跳出循环
	}

	for (int i=0;i<balls.size();i++)  // 每一个小球根据其direction更新他的位置
	{
		balls[i].movetoIndexInPath(path);
		balls[i].changeIndexbyDirection(path);
	}

	sleep(30); // 暂停若干毫秒
}

void updateWithInput()  // 和输入相关的更新
{
	if (gameStatus!=0) // 游戏胜利或失败,不需要用户再输入,函数直接返回
		return;

	int i,j;
	MOUSEMSG m;		// 定义鼠标消息
	while (MouseHit())  // 检测当前是否有鼠标消息
	{
		m = GetMouseMsg();
		if(m.uMsg == WM_MOUSEMOVE)  // 鼠标移动时			
		{
			cannon.updateWithMouseMOVE(m.x,m.y); // 炮台旋转,小球也移动到对应位置上
		}
		else if(m.uMsg == WM_RBUTTONDOWN)  // 鼠标右键点击时,更改炮台要发射的小球的颜色		
		{
			cannon.updateWithRButtonDown();
		}
		else if(m.uMsg == WM_LBUTTONDOWN)  // 鼠标左键点击时			
		{	
			cannon.updateWithMouseMOVE(m.x,m.y); //  先更新下炮台旋转角度、炮台小球的坐标
			float vx = (cannon.ball.center.x - cannon.x)/5; // 炮台小球移动速度
			float vy = (cannon.ball.center.y - cannon.y)/5;			
			int isCollider = 0; // 假设balls中没有小球和炮台小球碰撞
			// 沿着发射的方向炮台小球逐步移动,判断有balls有没有小球和炮台小球碰撞
			while (isCollider==0 && cannon.ball.center.y>0 && cannon.ball.center.y < HEIGHT
				&& cannon.ball.center.x>0  && cannon.ball.center.x < WIDTH ) // 炮台小球超出边界就不用处理了
			{
				cannon.ball.center.x += vx; // 更新发射小球的位置
				cannon.ball.center.y += vy;
				show(); // 显示下炮台小球的运动轨迹

				// balls中所有小球和炮台小球坐标判断,看看是否有相交的
				for (i=0;i<balls.size();i++)
				{
					float distance = Distance(balls[i].center.x, balls[i].center.y,cannon.ball.center.x,cannon.ball.center.y); 
					if (distance<Radius) // 找到和炮台小球碰撞的小球
					{			
						isCollider = 1; // 设为找到碰撞小球了						
						cannon.updateWithMouseMOVE(m.x,m.y); // 把炮台小球的位置移动回去

						// 下面复制一份小球,插入到这个地方
						Ball fireball = balls[i];
						fireball.colorId = cannon.ball.colorId; // 将插入小球变成炮台小球的颜色
						balls.insert(balls.begin()+i,fireball); // 复制一个小球,插入到vector中

						// 在Balls中序号i位置球,寻找其前后有没有和他颜色一样,且多余3个连续靠近的球
						// 如果是的话,就删除掉,返回的结果是删除掉的小球的个数
						// 如果一个没有删除,就返回0
						int count = eraseSameColorBalls(i,fireball,path,balls);
						if (count>=3)
							PlayMusicOnce(_T("coin.mp3"));  // 播放一次金币音效
						if (count==0)// 如果没有消除的话
						{
							for (j=i;j>=0;j--) // 移动前面的小球,留出空间放下新插入的小球
							{
								if (balls[j].indexInPath - balls[j+1].indexInPath <=0)
									balls[j].indexInPath = balls[j+1].indexInPath + 2*Radius/path.sampleInterval;
								else
									break; // 前面小球间有空隙,不用再处理了
							}
						}
						return; // 找到一个和炮台碰撞的小球了,后面的不用再找了
					}
				}  // for (i=0;i<balls.size();i++)
			} // 炮台小球逐步移动,和balls数组中所有小球进行判断
		} // 鼠标左键点击时	
	} 
}

void gameover() // 游戏结束时的处理
{
	balls.clear(); // 清除vector的内存空间
}

int main() // 主函数
{
	startup();  // 初始化	
	while (1)  // 循环
	{
		show(); // 显示
		updateWithoutInput();  // 和输入无关的更新
		updateWithInput();  // 和输入相关的更新
	}
	gameover(); // 游戏结束时的处理
	return 0;
}

这一章主要讲解了链表、STL、构造函数与析构函数等知识,实现了祖玛游戏。读者可以尝试在本章代码基础上继续改进:

1、实现多种道具,比如炸弹、万能颜色球等;

2、实现祖玛游戏中的小球回吸、连续消除功能;

3、实现一个设计、保存、读取轨迹地图数据的程序。

C++的标准模板库异常强大,读者可以进一步学习,并尝试利用STL改进之前章节实现的游戏。

代码下载地址: https://pan.quark.cn/s/a4b39357ea24 在计算机视觉技术中,数据集扮演着训练评估模型的核心角色。Labelme作为一个广受欢迎的开源工具,能够支持用户以交互方式对图像进行标注,而COCO(Common Objects in Context)则是一种被广泛采纳的数据集标准格式,适用于包括物体检测、图像分割在内的多种任务。本文将详细阐述如何将Labelme生成的标注数据转换为COCO数据集的标准格式。 Labelme标注的图像在输出为JSON格式时,会包含以下核心内容: 1. `version`: 指明JSON文件的版本信息。 2. `flags`: 目前未定义或保持为空,预留用于未来的功能扩展。 3. `shapes`: 列表形式存储对象的形状信息,每个形状项包含`label`(对象类别名称),`points`(构成对象边缘的多边形顶点),以及`shape_type`(通常为“polygon”)。 4. `imagePath``imageData`: 提供原始图像的存储路径二进制数据,便于后续图像的还原。 5. `imageHeight``imageWidth`: 明确标注图像的垂直水平尺寸。 COCO数据集的标准格式中定义了三种主要的标注类型: 1. Object instances(目标实例):主要用于执行物体检测任务。 2. Object keypoints(目标上的关键点):适用于人体姿态估计相关应用。 3. Image captions(看图说话):用于生成图像的文本描述。 COCO的JSON结构中包含以下基本组成部分: 1. `images`:记录图像的基本属性,包括`height`(高度)、`...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值