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;	//释放内存