MFC版 黃金礦工 遊戲開發報告
- 目錄
-
- 前言
- 實作功能
- 工程目錄結構
- 界面設計
-
- 主界面
- 遊戲界面
- 設定界面
- 遊戲說明界面
- 遊戲資源的擷取
- 遊戲基類(MyObject)實作
- 靜态礦物類
- 鈎子類
- 遊戲主要功能的實作
-
- 鈎子收發
- 礦物生成
- 碰撞檢測與處理
- 跳關
- 積分與倒計時
- 自動挖礦
- 龍寶大招(大威天龍!)
- 存/讀檔
- 總結
- 參考資料
目錄
前言
龍寶礦工是本人在"遊戲開發"課程中的大作業,斷斷續續用了兩個星期完成。期間參考了課程中的案例代碼,也查了挺多資料,感覺遊戲開發的過程非常有趣,因為可以實時地感受自己努力的結果,很有成就感。但遊戲開發也很燒腦,有時候晚上會想着明天要做哪一塊的内容然後睡不着覺。特此記錄一下開發的過程,希望能幫助到後來者。
實作功能
- 包括 原版黃金礦工 除商店外的絕大部分功能
- 提供 自動挖礦 功能,供玩家在手動挖累了時使用
- 增加 跳關 功能,避免地圖上沒有礦物時還需等待到倒計時結束.
- 提供 大招 功能,一鍵清圖并獲得積分,屬于娛樂功能
- 增加 收鈎 功能,當鈎子上沒有礦物時可以按“上”鍵收鈎。
- 增加 存/讀檔 功能,玩家在進入遊戲與退出時可選擇存/讀檔
- 提供 設定 功能,玩家可以控制音樂音效,大招,自動挖礦的開關
工程目錄結構
界面設計
一共設計了四個界面:主界面,遊戲界面,設定界面,遊戲說明界面
界面如下:
主界面
這裡其實可以做三個按鈕搞定的,做的時候沒有想到這一點,就自己畫了張圖當做主界面,再通過監聽滑鼠點選的位置判斷點到了哪個按鈕,進而進入相應界面。不過這樣也挺好的,算是多一種思路吧。
遊戲界面
遊戲界面就跟原版黃金礦工差不多了。中間的龍寶就是我們的“礦工”,它正在用它的舌頭搖着拉杆。龍寶下方有着大大小小的金礦和石頭,還有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.com/longjie1107/DragonMiner
參考資料
如何錄制電腦内部聲音?
大威天龍世尊地藏般若諸佛 原聲版片段_哔哩哔哩 (゜-゜)つロ …
黃金礦工
圖示下載下傳,ICON(SVG/PNG/ICO/ICNS)圖示搜尋下載下傳 - Easyicon
二維圖形旋轉公式的推導
MFC下對位圖旋轉
COblist 類 | Microsoft Docs
CFile 類 | Microsoft Docs