admin管理员组

文章数量:1530266

MFC版 黄金矿工 游戏开发记录

  • 目录
    • 前言
    • 实现功能
    • 工程目录结构
    • 界面设计
      • 主界面
      • 游戏界面
      • 设置界面
      • 游戏说明界面
    • 游戏资源的获取
    • 游戏基类(MyObject)实现
    • 静态矿物类
    • 钩子类
    • 游戏主要功能的实现
      • 钩子收发
      • 矿物生成
      • 碰撞检测与处理
      • 跳关
      • 积分与倒计时
      • 自动挖矿
      • 龙宝大招(大威天龙!)
      • 存/读档
    • 总结
    • 参考资料

目录

前言

龙宝矿工是本人在"游戏开发"课程中的最终项目,断断续续用了两个星期完成。期间参考了课程中的案例代码,也查了挺多资料,感觉游戏开发的过程非常有趣,因为可以实时地感受自己努力的结果,很有成就感。但游戏开发也很烧脑,有时候晚上会想着明天要做哪一块的内容然后睡不着觉。特此记录一下开发的过程,希望能帮助到后来者。

实现功能

  1. 包括 原版黄金矿工 除商店外的绝大部分功能
  2. 提供 自动挖矿 功能,供玩家在手动挖累了时使用
  3. 增加 跳关 功能,避免地图上没有矿物时还需等待到倒计时结束.
  4. 提供 大招 功能,一键清图并获得积分,属于娱乐功能
  5. 增加 收钩 功能,当钩子上没有矿物时可以按“上”键收钩。
  6. 增加 存/读档 功能,玩家在进入游戏与退出时可选择存/读档
  7. 提供 设置 功能,玩家可以控制音乐音效,大招,自动挖矿的开关

工程目录结构

界面设计

一共设计了四个界面:主界面游戏界面设置界面游戏说明界面
界面如下:

主界面

这里其实可以做三个按钮搞定的,做的时候没有想到这一点,就自己画了张图当做主界面,再通过监听鼠标点击的位置判断点到了哪个按钮,从而进入相应界面。不过这样也挺好的,算是多一种思路吧。

游戏界面

游戏界面就跟原版黄金矿工差不多了。中间的龙宝就是我们的“矿工”,它正在用它的舌头摇着拉杆。龙宝下方有着大大小小的金矿和石头,还有TNT,小猪,神秘宝藏。界面左上方是关卡和积分信息,右上方是剩余时间。

设置界面

此处可以控制音乐,音效,自动挖矿,大招的开关。

游戏说明界面

包含了游戏的简介和操作指南。

游戏资源的获取

说实话,游戏资源的获取才是做龙宝矿工时最耗费我时间的一块地方。有时间一定要学学画画!

龙宝矿工的资源有位图资源和音频资源。

位图资源的话这里给大家推荐一个网站,easyicon,里面有很多透明的位图可以直接拿过来用,或者自己稍微修改一下用也可以。龙宝矿工的“矿物”,“小龙”,都是出自这里。主游戏界面的背景图和矿车是我在4399黄金矿工游戏里用Photoshop扣下来的。小龙用矿车的四帧动画和小猪行走的两帧动画也是在原图的基础上做了一些修改做出来的。(PS:Photoshop强无敌)

音频资源中背景音乐算是比较好弄的。而wav格式的音效的获取就比较折磨人了。当时去各种音效网站找感觉效果都不对,自己录总觉得没原版的感觉。干脆去获取原版黄金矿工的音效了。步骤:首先开启电脑录制内部声音功能,然后打开录音机,打开游戏,录制游戏内音效。此时获得到的音效是m4a格式,我们需要的是wav格式,所以此时需要进行格式转换,此处推荐几个网站能进行m4a转wav,wav时长裁剪与wav音量调整,通过这几个步骤应该就能得到比较满意的音效了。

以下是获得到的资源截图:

游戏基类(MyObject)实现

游戏基类的编写是很重要的,它能够为我们接下来要写的其他类(钩子类,矿物类等)提供一些通用基础的参数与函数。

通用的参数有:物体的坐标矿物是否被抓取旋转圆心坐标(钩具中心)

功能有:设置与获取物体坐标设置与获取矿物是否被抓取的值获取物体的包围盒(判断碰撞用)获取矿物的质量,图像与分数绘制图片旋转绘制旋转后的图片。最后两个函数用于处理钩子和矿物的旋转与绘制。

以下是MyObject的头文件:

#define PI acos(-1.0)		//arccos(-1) = π
class MyObject: public CObject
{
public:
	CPoint GetPos() { return mPointPos; }     //位置
	void SetIsCatch(int value) { isCatch = value; }
	int GetIsCatch() { return isCatch; }
	void SetX(int x) { mPointPos.x = x; }
	void SetY(int y) { mPointPos.y = y; }
	virtual CRect GetRect() = 0;                    //包围盒
	virtual void Draw(CDC* pDC) = 0;   //绘制函数
	virtual int GetWeight() = 0;   //获取矿物质量
	virtual int GetScore() = 0;   //获取矿物分数
	virtual CBitmap * GetMyBmp() = 0;   //获取矿物图像
	//旋转原始图像orgBmp,Angle度(正为顺时针旋转)得到目标图像dstBmp
	void BmpRotate(CBitmap * orgBmp, CBitmap * dstBmp, double Angle); 
	//绘制函数 将orgBmp绕旋转圆心(钩具中心)旋转angle度后,再平移(offsetx,offsety)之后所得到的图像.originx,y为将orgBmp旋转至钩具中心正下方时的图形中心点坐标
	void DrawRotateBmp(CDC * pDC, CBitmap *orgBmp, int angle, int originx, int originy, int offsetx, int offsety);
	MyObject();
	~MyObject();

protected:
	CPoint mPointPos;
	int isCatch;
	int centerx, centery;	//所有矿物和钩子的旋转圆心(钩具中心)
	CRect rotaryRect;	//储存图片旋转后的矩形信息.
};

BmpRotate 和 DrawRotateBmp 这两个函数原本是钩子类里面的,后来我发现矿物也需要旋转,就从钩子类移植到基类了。这两个函数的编写也是废了我很大功夫。当时上网想找现成的函数,但放到自己的代码里效果又不对。后来又找到了两篇关于图形旋转原理和MFC下对位图旋转的博客,自己改进了一下才写出这两个函数。
这里贴一下图形绕某点旋转的公式

其中 P(x,y)为图形原先的位置,O(x0,y0)是旋转圆心的位置,b为旋转的角度,P’(x‘,y‘)为图形旋转后的位置。这里说一下b,该公式推导时,y轴是朝上的,这样得出的b若为正指图形绕逆时针旋转b角度。而MFC中y轴默认朝下,所以b为正时指图形绕顺时针旋转b角度,b是负数就是逆时针旋转,这一点是要注意的。

如果要实现黄金矿工中钩子的效果,除了要将钩子绕钩具中心旋转 b 角度,还要将钩子自身的图形绕自身中心旋转 b 角度,再进行平移才能实现。而旋转图形自身就是由BmpRotate 函数实现的, DrawRotateBmp 函数则负责将通过 BmpRotate 函数得到的图形 绘制在通过上面的公式与平移后得到的坐标上。

画张草图,展示一下原始的钩子经过这两个函数的处理后呈现的样子,希望有助理解:

以下是两个函数的具体实现:

//旋转原始图像orgBmp,Angle度(正为顺时针旋转)得到目标图像dstBmp
void MyObject::BmpRotate(CBitmap* orgBmp, CBitmap* dstBmp, double Angle)
{
	BITMAP bmp;
	orgBmp->GetBitmap(&bmp);	//获取位图信息
	BYTE *pBits = new BYTE[bmp.bmWidthBytes*bmp.bmHeight];
	orgBmp->GetBitmapBits(bmp.bmWidthBytes*bmp.bmHeight, pBits);	//原始信息存储至pBits中
	Angle = Angle * PI / 180;	//角度转换为弧度制
	int interval = bmp.bmWidthBytes / bmp.bmWidth;	//每像素所需字节数

	int newWidth, newHeight, newbmWidthBytes;	//新图的高宽与每行字节数
												//得到cos和sin的绝对值以计算高宽.
	double abscos, abssin;
	abscos = cos(Angle) > 0 ? cos(Angle) : -cos(Angle);
	abssin = sin(Angle) > 0 ? sin(Angle) : -sin(Angle);
	newWidth = (int)(bmp.bmWidth * abscos + bmp.bmHeight * abssin);
	newHeight = (int)(bmp.bmWidth * abssin + bmp.bmHeight * abscos);
	newbmWidthBytes = newWidth * interval;
	BYTE *TempBits = new BYTE[newWidth * newHeight * interval];	//新图的信息存储至TempBits中
																//初始化新图信息,全部涂为白色.
	for (int j = 0; j < newHeight; j++) {
		for (int i = 0; i < newWidth; i++) {
			for (int k = 0; k < interval; k++) {
				TempBits[i*interval + j * newbmWidthBytes + k] = 0xff;
			}
		}
	}
	double newrx0 = newWidth * 0.5, rx0 = bmp.bmWidth * 0.5;	//变换后的中心点
	double newry0 = newHeight * 0.5, ry0 = bmp.bmHeight * 0.5;	//变换前的中心点
																//遍历新图的每一个像素点
	for (int j = 0; j < newHeight; j++) {
		for (int i = 0; i< newWidth; i++) {
			int tempI, tempJ;	//原图对应点
								//首先要明确:新图和原图的左上方坐标都为(0,0).在此情况下,下式可以这样理解:
								//对于新图的每一个点,让其跟随新图中心点平移至中心点为(0,0),然后旋转-Angle度,
								//再让该点跟随中心点平移,当中心点平移至原图的中心点.该点就回到了旋转前的位置.
			tempI = (int)((i - newrx0)*cos(Angle) + (j - newry0)*sin(Angle) + rx0);
			tempJ = (int)(-(i - newrx0)*sin(Angle) + (j - newry0)*cos(Angle) + ry0);
			//如果该点在原图中找到了对应点
			if (tempI >= 0 && tempI<bmp.bmWidth)
				if (tempJ >= 0 && tempJ < bmp.bmHeight)
				{
					//将原图的对应点信息赋给该点
					for (int m = 0; m < interval; m++)
						TempBits[i*interval + j * newbmWidthBytes + m] = pBits[tempI*interval + bmp.bmWidthBytes * tempJ + m];
				}
		}
	}
	//更新位图信息
	bmp.bmWidth = newWidth;
	bmp.bmHeight = newHeight;
	bmp.bmWidthBytes = newbmWidthBytes;
	//创建位图
	dstBmp->CreateBitmapIndirect(&bmp);
	//将位图信息传入位图
	dstBmp->SetBitmapBits(bmp.bmWidthBytes*bmp.bmHeight, TempBits);
	delete pBits;
	delete TempBits;	//释放内存
}

//绘制函数 将orgBmp(图形中心点在旋转圆心下方)绕旋转圆心(钩具中心)旋转angle度后,再平移(offsetx,offsety)之后所得到的图像
void MyObject::DrawRotateBmp(CDC * pDC, CBitmap* orgBmp, int angle, int originx, int originy, int offsetx, int offsety) {
	double newx, newy;
	CDC memDC;
	//计算图像中心点经角度变换后的坐标
	newx = (originx - centerx) *  cos(angle / 180.0 * PI) - (originy - centery)  * sin(angle / 180.0 * PI) + centerx;
	newy = (originx - centerx) *  sin(angle / 180.0 * PI) + (originy - centery)  * cos(angle / 180.0 * PI) + centery;
	memDC.CreateCompatibleDC(pDC);
	CBitmap *tmpBmp = new CBitmap;
	BITMAP tmpBmpInfo;
	BmpRotate(orgBmp, tmpBmp, angle);
	tmpBmp->GetBitmap(&tmpBmpInfo);
	//由中心点坐标与新图形的宽高与平移的量得出左上角坐标
	mPointPos.x = (int)(newx - 0.5*tmpBmpInfo.bmWidth + offsetx);
	mPointPos.y = (int)(newy - 0.5*tmpBmpInfo.bmHeight + offsety);
	//更新旋转矩形信息
	rotaryRect.left = mPointPos.x;
	rotaryRect.right = mPointPos.x + tmpBmpInfo.bmWidth;
	rotaryRect.top = mPointPos.y;
	rotaryRect.bottom = mPointPos.y + tmpBmpInfo.bmHeight;
	CBitmap* old = memDC.SelectObject(tmpBmp);
	pDC->TransparentBlt(mPointPos.x, mPointPos.y, tmpBmpInfo.bmWidth, tmpBmpInfo.bmHeight, &memDC, 0, 0, tmpBmpInfo.bmWidth, tmpBmpInfo.bmHeight, RGB(255, 255, 255));
	memDC.SelectObject(old);
	tmpBmp->DeleteObject();	//释放内存
}

静态矿物类

静态矿物类指的是静止的普通矿物,即钻石,大中小金块,大小石头共六种矿物。每种矿物的特点是具有固定的分数、重量(影响速度)和图像信息。据此我们可以构造静态矿物类的结构体:

typedef struct staticMine {
	CBitmap bmp;	//储存矿物图片
	int score;		//矿物分数
	int weight;		//矿物重量(影响速度)
	int width;		//矿物宽
	int height;		//矿物长
}staticMine;

而后在头文件声明静态变量与初始化函数,准备在进程开始时初始化获得所有静态矿物的信息。

static staticMine mStaticMine[6];
static int score[6];//分别代表钻石,大中小金矿,大小石头的分数与重量
static int weight[6];
static void LoadImage();	//初始化函数,进程开始时调用。

初始化操作如下:

//加载每种静态矿物的图片与其他属性
void StaticMine::LoadImage()
{
	BITMAP mineBMP;
	mStaticMine[0].bmp.LoadBitmap(IDB_DIAMOND);
	mStaticMine[1].bmp.LoadBitmap(IDB_LARGEGOLD);
	mStaticMine[2].bmp.LoadBitmap(IDB_MIDGOLD);
	mStaticMine[3].bmp.LoadBitmap(IDB_LITTLEGOLD);
	mStaticMine[4].bmp.LoadBitmap(IDB_BIGSTONE);
	mStaticMine[5].bmp.LoadBitmap(IDB_STONE);

	for (int i = 0; i < 6; i++) {
		mStaticMine[i].bmp.GetBitmap(&mineBMP);
		mStaticMine[i].height = mineBMP.bmHeight;
		mStaticMine[i].width = mineBMP.bmWidth;
		mStaticMine[i].score = score[i];
		mStaticMine[i].weight = weight[i];
	}
}

类初始化就到此为止。而对每一个静态矿物实体,由于他们坐标不同,类别不同,仍需初始化:

StaticMine::StaticMine(int startX, int startY)
{
	mPointPos.x = startX;
	mPointPos.y = startY;
	mType = rand() % 6;	//六种矿物中的随机一种
}

而后还有静态矿物的绘制函数,这里采用了双缓冲绘制,避免了闪屏:

void StaticMine::Draw(CDC * pDC)
{
	CDC memDC;

	memDC.CreateCompatibleDC(pDC);
	CBitmap* old = memDC.SelectObject(&mStaticMine[mType].bmp);

	pDC->TransparentBlt(mPointPos.x, mPointPos.y, mStaticMine[mType].width, mStaticMine[mType].height, &memDC, 0, 0, mStaticMine[mType].width, mStaticMine[mType].height, RGB(255, 255, 255));

	memDC.SelectObject(old);

}

最后还要实现父类的虚函数与类在结束时的内存回收功能。

int GetType() { return mType; }
int GetScore() { return score[mType]; }
int GetWeight() { return weight[mType]; }
CBitmap * GetMyBmp() { return &mStaticMine[mType].bmp; }	
CRect StaticMine::GetRect()
{
	if (rotaryRect.Width())	//如果已经旋转过了,rotaryRect的宽就不为0
		return rotaryRect;	//返回旋转过的图形矩阵信息.
	else
		return CRect(mPointPos.x, mPointPos.y, mPointPos.x + mStaticMine[mType].width, mPointPos.y + mStaticMine[mType].height);
}
void StaticMine::DeleteImage()
{
	for(int i=0;i<6;i++)
		mStaticMine[i].bmp.DeleteObject();
}

至此,静态矿物类需要的功能就全部实现了。而龙宝类与其他的矿物类(小猪类,宝藏类,TNT类)的实现思路其实跟静态矿物类是差不多的,这里不赘述了。

钩子类

钩子类是这个游戏中最核心的类,写它的时候遇到了很多搞笑的bug,像是钩子突然消失(offset精度问题),钩子可以向天上发射(没有限制钩子在HOOK_KEEP状态时只能出钩不能收钩)。
钩子有三种状态:1.HOOK_KEEP:绕钩具中心来回旋转。2.HOOK_OUT:出钩。3.HOOK_IN收钩。
钩子处在HOOK_KEEP状态时,会来回的旋转,设旋转最大角度为α,我们注意角度在边界值的处理就好。

if (angle == 80)	orient = -1;	//当顺时针旋转80度后变换旋转方向
if (angle == -80)	orient = 1;	//当逆时针旋转80度后变换旋转方向
angle += orient * vangle;
DrawRotateBmp(pDC, &hookBmp[0], angle, originx,originy, 0, 0);	//将出钩图片绕旋转圆心(钩具中心)旋转angle度后,再平移(0,0)之后所得到的图像

当钩子出钩时,钩子的旋转角度恒定,关于旋转圆心的偏移量不断变化,据此得出处理方法:

offsetx -= sin(angle*PI / 180)*vgo;
offsety += cos(angle*PI / 180)*vgo;
//(471,110)为初始情况左上角的坐标,对于超出边界的钩子,直接收回
if (offsetx <= -471 || offsetx >= (WIN_WIDTH - 471) || offsety >= (WIN_HEIGHT-110)) {
	status = HOOK_IN;
}
else {	//若未超出边界则绘制钩子
	DrawRotateBmp(pDC, &hookBmp[0], angle, originx, originy, (int)offsetx, (int)offsety);
}

收钩也是差不多的情况,但要注意由于offset的值使双精度浮点数,处理时要小心,不能直接判断其等于0。还要注意钩子收回的速度会根据矿物的重量改变。

//如果已经很接近初始位置时,就当做已经到了初始位置,调整偏移量为0,设置钩子状态为HOOK_KEEP
if ( (offsetx < 1.1 * vback && offsetx > -1.1 * vback) && (offsety < 1.1 * vback &&offsety > -1.1 * vback)) {
	offsetx = 0;
	offsety = 0;
	status = HOOK_KEEP;
}

钩子回收速度的处理函数

void Hook::SetVback(int weight)
{
	if (weight == 0) vback = vgo;
	vback = vgo * 100 / (100+weight);
}

钩子类的逻辑到这就差不多了

游戏主要功能的实现

钩子收发

在MFC中使用键盘消息处理函数(OnKeyDown)即可。

void CDragonMinerView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
	......
	......
	case VK_UP:
		myObject = (MyObject*)mObjects[HOOK].GetHead();
		//收钩(只能在出钩时收钩,否则钩子会往天上飞hhh)
		if (((Hook*)myObject)->GetStatus() == HOOK_OUT) {
			((Hook*)myObject)->SetStatus(HOOK_IN);
		}
		break;
	case VK_DOWN:
		myObject = (MyObject*)mObjects[HOOK].GetHead();
		//只能在等待时出钩
		if (((Hook*)myObject)->GetStatus() == HOOK_KEEP) {
			((Hook*)myObject)->SetStatus(HOOK_OUT);
			if(isSoundEffectsOn)
				PlaySound((LPCWSTR)IDR_HOOKOUT, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
		}
		break;
}

矿物生成

矿物的生成是比较需要考虑的,毕竟要让关卡随机生成矿物,总不能生成到天上去吧,也不能让矿物重叠在一起,也不能全是钻石,全是碎石头。于是在生成时就需要随机函数。比如说我们需要生成猪,那就让电脑去决定它的数量与方位

// 猪/钻石猪,0-2只
count = rand() % 3;
while(count--){
	x = rand() % 551;	// 550 + PigWidth + 400(小猪向右移动最远距离) = 屏幕宽
	y = rand() % 594 + 128;	//593 + PigHeight +128 = 屏幕高    128为矿洞的topY
}

但仅仅这样是不够的,因为随机生成的小猪可能与其他矿物重叠,所以需要判断它是否与原有的其他矿物重叠。怎么判断呢?可以使用IntersectRect函数判断矿物矩形是否相交来判断。那么当我们判断出小猪与其他矿物重叠,就需要重新生成小猪,在循环内 count++ 即可。如果没有重叠,就直接将小猪加入列表尾部 mObjects[PIGS].AddTail(myObj); mObjects是COblist类的实体列表,里面存放了所有的矿物信息,龙宝与钩子,爆炸等等,具体使用推荐查看微软文档

碰撞检测与处理

碰撞的检测上还是利用IntersectRect函数。当钩子撞上矿物后,钩子状态从出钩变为收钩。矿物的isCatch值在撞上时设置为1.这样在绘制时通过isCatch的值就能将矿物与钩子一同回来的画面画出来。
由于矿物跟钩子一同返回时,二者始终都在碰撞,那么就需要判断终止的条件。很明显当钩子的状态变为HOOK_KEEP时,矿物已经到了终点,这时就需要删除矿物,并增加玩家积分。
而当处理特殊的矿物,如TNT时,碰到时就需要直接删除矿物,并生成爆炸效果类的实体。当挖到上宝藏时,要根据其类型进行不同的判断,如果是普通矿物,就只加分数;是大力水,则设置收钩速度在该关卡恒定;是幸运草,则设置分数在该关卡翻倍。

// 检测钩子是否钩到矿物
for (int i = PIGS; i <= STATIC_MINE; i++) {
	for (pos1 = mObjects[i].GetHeadPosition(); (pos2 = pos1) != NULL;) //遍历所有矿物
	{
		myObject = (MyObject*)mObjects[i].GetNext(pos1);  // save for deletion
		hookRect = mHook->GetRect();

		//一旦发生碰撞,钩子与矿物共同返回
		if ((hookRect.IntersectRect(myObject->GetRect(), hookRect)))
		{
			//如果碰到了TNT
			if (i == TNT) {
				int x, y;	//设置爆炸效果的位置
				x = (int)((myObject->GetPos().x) - (EXPLOSION_WIDTH - TNT_WIDTH) / 2);
				y = (int)((myObject->GetPos().y) - (EXPLOSION_HEIGHT - TNT_HEIGHT) / 2);
				PlaySound((LPCWSTR)IDR_EXPLODE, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC | SND_NOSTOP);
				mObjects[EXPLOSION].AddTail(new Explosion(x,y));
				// 删除该TNT
				mObjects[i].RemoveAt(pos2);
				delete myObject;
				mHook->SetStatus(HOOK_IN);//钩子回收
				break;
			}
			//矿物回收
			//以下语句只需要在第一次碰撞时调用一次
			if (useForFirstTime == 0) {
				int dStaticMineOffset = 0;	// 不同矿物的中心与钩子中心的相对偏移量不同
				if (i == TREASURE)	dStaticMineOffset = 16;
				if (i == STATIC_MINE) {
					switch (((StaticMine*)myObject)->GetType())
					{
					case DIAMOND:	dStaticMineOffset = 2; break;
					case LARGEGOLD:	dStaticMineOffset = 45; break;
					case MIDGOLD:	dStaticMineOffset = 30; break;
					case LITTLEGOLD:	dStaticMineOffset = 20; break;
					case BIGSTONE:	dStaticMineOffset = 18; break;
					case STONE:	dStaticMineOffset = 10; break;
					}
				}
				dMineHookCenter = (int)((myObject->GetRect().Height() + hookRect.Height())*0.5 - dStaticMineOffset);
				myObject->SetIsCatch(1);	//设置矿物的isCatch值为1表示矿物被抓住了,该参数决定绘制时矿物是否跟随钩子移动
				mHook->SetStatus(HOOK_IN);//钩子回收
				if (!isGetStrenthBuff)	//没有力量buff时
					mHook->SetVback(myObject->GetWeight());	//根据矿物重量设置回收速度
				useForFirstTime = 1;
				if(isSoundEffectsOn)
					PlaySound((LPCWSTR)IDR_CATCH, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
			}
			//当钩子到达原位时,矿物消失转化为分数
			if (mHook->GetStatus() == HOOK_KEEP) {
				if (i == TREASURE) {		//	抓到宝藏
					if (((Treasure*)myObject)->GetType() == STRENTH)	//获得大力水,得到力量buff加成
						isGetStrenthBuff = true;
					else if (((Treasure*)myObject)->GetType() == LUCK)	//获得幸运草,得到金币buff加成
						isGetMoneyBuff = true;
					if (((Treasure*)myObject)->GetType() != MONEY && isSoundEffectsOn)
						PlaySound((LPCWSTR)IDR_GETBUFF, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
				}
				// 删除矿物
				mObjects[i].RemoveAt(pos2);
				mHook->SetVback(0);	//重置钩子速度
				int memScore;
				if (isGetMoneyBuff)	//有金币buff就获得双倍积分
					memScore = myObject->GetScore() * 2;
				else
					memScore = myObject->GetScore();
				Score::AddMyScore(memScore);	//增加分数
				score->SetIfDrawAddScore(1, memScore);	//通知Score绘制加分画面
				if (memScore != 0 && isSoundEffectsOn)
					PlaySound((LPCWSTR)IDR_GETMONEY, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
				delete myObject;
				useForFirstTime = 0;
				break;
			}
			if(isSoundEffectsOn)
				PlaySound((LPCWSTR)IDR_PULLMINE, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC | SND_NOSTOP);
			break;
		}//if
	}//for
}//for

跳关

当玩家分数达到要求时,可以按‘N’键跳关,避免无谓的等待。
在键盘消息函数中进行跳关处理:

case 'n':
case 'N':
	if (Score::GetMyScore() >= int(Score::GetTotalScore() *2 / 3)) {	//如果符合过关条件就去下一关
		isGoNextLevel = 1;
	}
	else {
		isGamePause = true;
		if (AfxMessageBox(_T("分数不够还想蒙混过关?"), MB_OK, 0) == IDOK)
			isGamePause = false;
	}
	
	break;

积分与倒计时

积分与倒计时的实现使用MFC的CRect类与CString类就可以搞定。CRect类负责绘制时间条,CString用于显示关卡数,当前积分,总积分,剩余时间。
积分绘制:

if (ifDrawAddScore == 1) {	//抓到了东西
	if (frame == 0) {		//如果绘制加分画面能用的帧数已经用光
		frame = 30;	//重置绘制时间
		ifDrawAddScore = 0;	
		addScore = 0;
	}
	else {		//可用帧数不为0,表示可以绘制加分画面
		if (addScore) {	//抓到的是有价值的东西
			strScore.Format(_T("当前关卡: %d"), mGameLevel);
			pDC->TextOut(mPointPos.x, mPointPos.y, strScore);
			strScore.Format(_T("当前积分: %d + %d"), mMyScore - addScore, addScore);
			pDC->TextOut(mPointPos.x, mPointPos.y + 24, strScore);
			strScore.Format(_T("目标积分: %d"), (int)(mTotalScore * 2 / 3));
			pDC->TextOut(mPointPos.x, mPointPos.y + 48, strScore);
			frame--;
			pDC->SelectObject(oldFont);//选择回老字体
		}
		else {	//抓到的东西没有价值,即抓到了tnt,大力水之类的东西
			ifDrawAddScore = 0;
		}
		font.DeleteObject();//删除新字体
	}
}
if (ifDrawAddScore == 0) {	//平常情况,没抓到矿物时,直接绘制当前分数与目标分数
	strScore.Format(_T("当前关卡: %d"), mGameLevel);
	pDC->TextOut(mPointPos.x, mPointPos.y, strScore);
	strScore.Format(_T("当前积分: %d"), mMyScore);
	pDC->TextOut(mPointPos.x, mPointPos.y + 24, strScore);
	strScore.Format(_T("目标积分: %d"), (int)mTotalScore * 2/ 3);
	pDC->TextOut(mPointPos.x, mPointPos.y + 48, strScore);
	pDC->SelectObject(oldFont);//选择回老字体
	font.DeleteObject();//删除新字体
}

倒计时绘制

CBrush brush;
CRect bar;	//时间条
CString msg;	//消息
//绘制时间条
brush.CreateSolidBrush(RGB(255, 0, 0));
bar.top = TOP_OFFSET ;
bar.left = LEFT_OFFSET;
bar.right = (int)(LEFT_OFFSET + BAR_LEN * mTimeLeft* 1.0 / TOTAL_TIME);
bar.bottom = bar.top + 20;
memDC.FillRect(bar, &brush);
memDC.SetTextAlign(TA_CENTER);
msg.Format(_T("剩余时间: %d"), mTimeLeft);
memDC.TextOut((int)(bar.left + bar.right) / 2, bar.bottom + 4, msg);

自动挖矿

自动挖矿的原理其实很简单,就是检测矿物中心与钩具中心的角度 和 钩子与钩具中心的角度的差值,当差值很小时就认为三点一线,出钩自动挖矿。

if (isAutoModeOn) {
//原理:检测 矿物中心与钩具(489,97)的角度 与 钩子与钩具的角度之差,当差值<=2°时,自动出钩  抓猪的话只能随缘了hh
	int centerx, centery, angle1, angle2;	//center:矿物中心点 angle1: 钩子与钩具的角度 angle2:矿物与钩具的角度
	angle1 = mHook->GetAngle();	//钩子的角度
	//遍历所有矿物
	for (int i = PIGS; i <= STATIC_MINE; i++) {
		int total = 0;
		for (pos1 = mObjects[i].GetHeadPosition(); (pos2 = pos1) != NULL;)
		{
			myObject = (MyObject*)mObjects[i].GetNext(pos1);	//获取矿物对象
			centerx = myObject->GetRect().left + (int)myObject->GetRect().Width() / 2;
			centery = myObject->GetRect().top + (int)myObject->GetRect().Height() / 2;
			double mcos = (centery - 97) / sqrt((centerx - 489)*(centerx - 489) + (centery - 97)*(centery - 97));
			angle2 = (int)(acos(mcos) / PI * 180);
			angle2 = (centerx - 489) < 0 ? angle2 : -angle2;
			if (abs(angle2 - angle1) <= 2 && myObject->GetIsCatch() == 0) {	//钩具,钩子,矿物在一条直线上,而且该矿物不在钩子上
				if (mHook->GetStatus() == HOOK_KEEP) {
					mHook->SetStatus(HOOK_OUT);
					if (isSoundEffectsOn)
						PlaySound((LPCWSTR)IDR_HOOKOUT, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
				}
				i = STATIC_MINE + 1;
				break;
			}
			total++;
		}//for
		if (total == 0 && Score::GetMyScore() >= int(Score::GetTotalScore() * 2 / 3))		//抓完了前往下一关
			isGoNextLevel = 1;
	}//for
}

龙宝大招(大威天龙!)

这个大招其实蛮水的,属于娱乐功能,蹭蹭我社会法海哥的热度~
大招效果是:消除所有矿物,在地图上造成全图爆炸效果,增加1万分。
效果图:

//大威天龙!
if (isLegendModeOn && useSkill) {	//生成全屏炸弹清屏,并且获取10000分
	int addScore = 10000;
	Score::AddMyScore(addScore);	//增加分数
	score->SetIfDrawAddScore(1, addScore);	//通知Score绘制加分画面
	for(int x = 0;x<1024;x+=256)	//全屏炸弹
		for(int y=128;y<768;y+=160)
			mObjects[EXPLOSION].AddTail(new Explosion(x, y));
	useSkill = 0;
	frame = 50;
	PlaySound((LPCWSTR)IDR_SKILLSOUND, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
}

存/读档

当玩家在游戏界面按’esc’键退出时,会触发存档选择框,询问玩家是否存档。当玩家在主界面进入游戏时,会询问是否读档。通过CFile类编写存档文件实现存/读档功能

存档:

if (nChar == VK_ESCAPE) {
	//在游戏界面按"esc"键会先询问是否存档
	if (activityMode == GAME_ACTIVITY) {
		if (AfxMessageBox(_T("是否存档?"), MB_YESNO, 0) == IDYES)		//选是,写入当前关卡与当前分数
		{
			CFile file;
			file.Open(_T("save.txt"), CFile::modeCreate | CFile::modeWrite, NULL);
			int value;
			value = Score::GetGameLevel();
			file.Write(&value, sizeof(int));
			value = Score::GetMyScore();
			file.Write(&value, sizeof(int));
			value = Score::GetTotalScore();
			file.Write(&value, sizeof(int));
			file.Close();
		}
	}
	activityMode = MAIN_ACTIVITY;	//esc返回主界面
}

读档:

//判断在主界面时鼠标是否点击到了按钮.第一个按钮"开始游戏"左上顶点为(358,259),右下顶点为(680,381)
if (mouseX >= 358 && mouseX <= 680 && mouseY >= 259 && mouseY <= 381) {
	if (AfxMessageBox(_T("是否读档?"), MB_YESNO, 0) == IDYES)		//选是,写入当前关卡与当前分数
	{
		if (!file.Open(_T("save.txt"), CFile::modeRead, NULL)) {	//若打不开存档,重开游戏
			ifReadSave = false;
			activityMode = GAME_ACTIVITY;	//变更活动模式为GAME_ACTIVITY
			initGame();		//初始化游戏
			break;
		}
		file.SeekToBegin();
		int Rev;
		file.Read(&Rev, sizeof(int));
		score->SetGameLevel(Rev-1);		//在initLevel时会+1,所以此处-1
		file.Read(&Rev, sizeof(int));
		score->SetMyScore(Rev);
		file.Read(&Rev, sizeof(int));
		score->SetTotalScore(Rev);
		ifReadSave = true;
	}else
		ifReadSave = false;
	activityMode = GAME_ACTIVITY;	//变更活动模式为GAME_ACTIVITY
	initGame();		//初始化游戏
}

总结

这次的黄金矿工制作我个人的体验很好,既能在制作时体会写游戏的快乐,又能在游戏成品后,在空闲时间玩玩自己的游戏。我在过程中实践了课程中的知识,也学习到了许多课外知识,提升了编程能力。
再说本次游戏开发的成果黄金矿工,有基于原版的突破(存读档,跳关,可以提前回收钩子等),也有许多可以改进与不足之处。改进之处有:可以做个本地的排行榜,,宝藏可以获得炸弹,设置大招cd,改善自动挖矿算法(目前抓猪只能随缘抓)。不足之处有:钩子在回收时会抓到猪(其实也算是游戏特色,愿者上钩),基本没有异常的处理,一旦出错时就会崩溃。
说了这么多难免有所纰漏,希望各位能不吝给予指正。
最后附上项目的github地址,供大家参考。
https://github/longjie1107/DragonMiner

参考资料

如何录制电脑内部声音?
大威天龙世尊地藏般若诸佛 原声版片段_哔哩哔哩 (゜-゜)つロ …
黄金矿工
图标下载,ICON(SVG/PNG/ICO/ICNS)图标搜索下载 - Easyicon
二维图形旋转公式的推导
MFC下对位图旋转
COblist 类 | Microsoft Docs
CFile 类 | Microsoft Docs

本文标签: 游戏开发黄金矿工MFC