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度(正为顺时针旋转)得到目标图像dstBmpvoid 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的宽就不为0return rotaryRect;   //返回旋转过的图形矩阵信息.elsereturn 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 deletionhookRect = mHook->GetRect();//一旦发生碰撞,钩子与矿物共同返回if ((hookRect.IntersectRect(myObject->GetRect(), hookRect))){//如果碰到了TNTif (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));// 删除该TNTmObjects[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;elsememScore = 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°时,自动出钩  抓猪的话只能随缘了hhint 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++;}//forif (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_ACTIVITYinitGame();       //初始化游戏break;}file.SeekToBegin();int Rev;file.Read(&Rev, sizeof(int));score->SetGameLevel(Rev-1);        //在initLevel时会+1,所以此处-1file.Read(&Rev, sizeof(int));score->SetMyScore(Rev);file.Read(&Rev, sizeof(int));score->SetTotalScore(Rev);ifReadSave = true;}elseifReadSave = false;activityMode = GAME_ACTIVITY; //变更活动模式为GAME_ACTIVITYinitGame();       //初始化游戏
}

总结

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

参考资料

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

MFC版 黄金矿工 游戏开发记录相关推荐

  1. C语言实现基于easyx的(低配版)黄金矿工游戏

    目录 一.项目环境 二.游戏说明 三.运行演示 四.代码 总结 一.项目环境 Visual Studio 2019+EasyX 20220116 二.游戏说明 与黄金矿工游戏类似,每关限时90s(可自 ...

  2. 【java项目】仅需俩小时教你学会自己用java做出自己的“黄金矿工’’游戏

    游戏介绍: 黄金矿工是我们童年都玩过的游戏,非常的好玩,可以单人或者双人一起利用钩爪来获得地下的金子,但是难度也是相当的大.前几关看似非常的简单,但是却机关重重,此款小游戏可以在网站上打开,也可以下载 ...

  3. 2021.03.17 pokémon小游戏开发记录与周总结

    2021.03.17 pokémon小游戏开发记录与周总结 此篇仅包含部分项目代码,只是个人的学习总结. 文章目录 2021.03.17 pokémon小游戏开发记录与周总结 前言 一.前期准备 二. ...

  4. 视频教程 | 3D版切水果游戏开发实战5:加载美术资源

    在我们的前4期开发实战中,我们写代码使用的切割物体都是Egret Pro自带的实体,如:球体.椎体.立方体等,今天我们就来讲讲如何将游戏场景中的切割对象换成美术提供的水果和切刀素材. 核心内容比较简单 ...

  5. Unity源码分享-黄金矿工游戏完整版

    Unity源码分享-黄金矿工游戏完整版 项目地址:https://download.csdn.net/download/Highning0007/88118933

  6. 黄金矿工游戏demo

    试玩地址:  http://www.adanghome.com/js_demo/13/ 请使用chrome.firefox或safari.ie的话,请大于ie8. 按空格键扔出钩子,按左右键控制矿工移 ...

  7. 《Happy Birthday》游戏开发记录(送给朋友的小礼物)

    游戏开发的学习记录⑦ 项目:Happy Birthday (一个小小小游戏,基于unity给朋友做的一个生日小礼物

  8. 《射击游戏》游戏开发记录

    游戏开发的学习记录⑥ 项目:射击游戏 开始时间:2022.9.29 (已经结束很久了,一直忘了写,补一下) 新学到的: 做小地图,用来看自己和敌人的位置 //获得屏幕分辨率比率 float ratio ...

  9. 《捕鱼达人》游戏开发记录

    游戏开发的学习记录④ 项目:捕鱼达人 开始时间:2022.09.01 (新的学期,好好学习!!) 新学到的: 水波荡漾效果的制作 Legacy Shaders->Particles->Ad ...

  10. 视频教程| 3D版切水果游戏开发实战:认识水果

    本周,很多Egret 老铁看到了我们的3D实战内容并积极给予了回应:要与我们一起实战开发3D版切水果游戏!看完后很是感动,在此谢谢老铁们对我们的支持,你们的认可与鼓励是我们不断前行的动力! Egret ...

最新文章

  1. 防止ASP.NET按钮多次提交代码
  2. 【Android RTMP】音频数据采集编码 ( FAAC 头文件与静态库拷贝到 AS | CMakeList.txt 配置 FAAC | AudioRecord 音频采样 PCM 格式 )
  3. window上安装elasticserach
  4. 与毒”共舞30年!清华美女研究生为何放弃高薪,选择特招入伍?背后的原因令人泪崩......
  5. python写数据到mysql_使用python3 实现插入数据到mysql
  6. Qt_加速编译 快速编译 make -j4
  7. 测试用例的定义、内容、作用
  8. 【ICEPAK】手把手教你热仿真
  9. python开发web靠谱吗_Python用来做Web开发的优缺点,你心里必须要记得这些
  10. 《AI进化论:解码人工智能商业场景与案例》----读书笔记
  11. Hbuilder连接NOX夜神模拟器
  12. 手机wifi延迟测试软件,测网速延迟(如何测试wifi延迟)
  13. Java的抗辩本位制
  14. php 用隐藏姓名第一个字,PHP用*号替代姓名除第一个字之外的字符
  15. linux-xsell、xftp连接虚拟机
  16. 智联物联路由器openvpn客户端连接linux服务器通讯基本配置
  17. 1千用户与1千万用户的网站系统架构区别?
  18. qnx 开发十步_十步实现应用程序本地化
  19. 【创利树】电商的用户流失率是80%,你的用户流失率是多少呢?
  20. 超验骇客1280高清科幻大片

热门文章

  1. 由于授权协议中一个错误,远程计算机中断了会话。请重新跟远程计算机连接;或者跟服务器管理员联系。
  2. linux 使用c语言如何获取网关地址
  3. 字体 流光css,实例详解CSS3制作文字流光渐变特效
  4. 深信服SSL远程接入与深信服行为审计同步登陆用户信息
  5. Error response from daemon: Pool overlaps with other one on this address space
  6. 软考之---软件设计师考试经验与笔记分享
  7. spyder python使用技巧大全_spyder常用功能
  8. python runtime错误_使用Django框架遇到RuntimeError: populate() isn't reentrant错误
  9. 什么是MAC多播地址
  10. java自行车 one_小白装车宝典——JAVA ONE七步装车法