本文由BlueCoder编写 转载请说明出处:
http://blog.csdn.net/crocodile__/article/details/13505997
我的邮箱:bluecoder@yeah.net 欢迎大家和我交流编程心得
我的微博:BlueCoder_黎小华 欢迎光临^_^
前面两篇回顾了一些MFC编程要领(后期还会随带着讲一点儿,不过不是重点了),今天就进入游戏主题,搞一个小游戏
前段时间,看见Yorhom朋友用html5+Qt开发了一款游戏——SpaceWar,感觉挺有意思的——刚好最近一直在研究了MFC以及GDI+,可以着手将这款游戏移植到MFC中(本来可以提前几天发布的,但是前两天参加了一个竞赛,也就耽搁了一下)——这里还是很感谢Yorhom分享的素材,这位朋友很不错,html5游戏开发爱好者
接下来,我就一步一步介绍这款MFC版游戏的实现流程
————————————————————————————————————————————————————————————
————————————————————————————————————————————————————————————
一、游戏界面以及玩法的介绍
(1). 游戏开始界面
(2). 游戏界面
(3). 游戏结束界面
二、主要的实现细节分析
这里,我将详细讲解我在写这个游戏中用到的主要原理、思想
(1). 首先是游戏背景的移动(这个就是类似于幻灯片播放的原理), 我做了一个示意图,先来看看:
(ps: 红框区域的1、2分别表示在内存中绘制的两张连续的背景 , 蓝色区域表示窗口客户区)
如此循环,给人视角效果就是这两张背景在连续的变换
(2). 子弹的移动——要保证在不同方向上的移动速度相同, 因此需要对移动速度在水平方向(x轴)和垂直方向(y轴)求分量,这个是简单的物理问题,大家应该能明白:
(下图是示意图)
(3). 音效播放的处理
1>. 点击子弹以及爆炸音效都使用PlaySound,但是必须采用异步播放模式,以免游戏界面出现卡顿现象。因为如果采用同步播放,会播放完后才会返回;异步播放会直接返回,不会耽搁时间
2>. 游戏背景音乐要重复播放,除非玩家不再玩了
(4). 碰撞检测
首先要使用一个函数:PtInRect(CPoint pt)-->检测点pt是否在一个矩形区域Rect中
而对于子弹与怪物机的碰撞检测,就需要检测子弹的矩形区域的四个点(左上、下,右上、下),只要有一个点在怪物机的矩形区域内,就表示子弹射击到了一个怪物机
三、代码详解
(1). 类视图
(2). 先来浏览一下头文件SpaceWarView.h
//计时器ID #define ID_BULLET 100//发子弹 #define ID_MONSTER 101//处理怪物 #define ID_MOUVEBK 102//移动背景 //英雄机和怪物机 typedef struct img { CImage png; CRect rect; int speed; bool isOut; }IMG; //子弹 typedef struct blt { CImage png; CRect rect; int speed; bool isOut; int ix;//x移动坐标 int iy;//y移动坐标 }BULLET; //显示状态: 开始 游戏中 结束 enum STATE{START, RUNNING, END}; class CSpaceWarView : public CView { protected: // 仅从序列化创建 CSpaceWarView(); DECLARE_DYNCREATE(CSpaceWarView) // 属性 public: CSpaceWarDoc* GetDocument() const; // 操作 public: // 重写 public: virtual void OnDraw(CDC* pDC); // 重写以绘制该视图 virtual BOOL PreCreateWindow(CREATESTRUCT& cs); protected: // 实现 public: virtual ~CSpaceWarView(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: //数据成员 private: //显示状态 STATE m_state; CDC m_bufferDC;//缓冲DC CBitmap m_bufferBmp;//缓冲Bmp CSize m_sClient;//客户区大小 CPoint m_ptBltOrg;//子弹的起点 //开始背景 struct sbk { CImage bk; CImage normal; CImage selected; CRect rect; bool isSelected; }m_startBk; //游戏背景 struct gbk { CImage fir;//第一张背景 CImage sec;//第二张背景 BOOL isFir;//标记左边是否是第一张 int curx;//左边背景的x坐标 }m_gameBk; //结束背景 struct ebk { CImage bk; CImage gameover; CImage againNor;//重新开始 CImage againSel; CImage exitNor;//退出 CImage exitSel; CRect rGo;//gameover的rect CRect rRes;//restart的rect CRect rExit;//exit的rect bool isExitSel; bool isResSel; }m_endBk; //暂停与继续 struct sg { CImage img; BOOL isStop; CRect rect; }m_stopGoOn; IMG m_hero;//英雄机 vector<BULLET> m_bullet;//子弹 vector<IMG> m_vecMons;//怪物机 size_t m_score;//存储分数-每射击成功一次, 加5分 const int m_max;//标记怪物最多个数 //成员函数 private: //游戏的三种状态 void StartUI();//开始 void RunningUI();//游戏中 void EndUI();//结束 //开始游戏 void StartGame(); //结束游戏 void EndGame(); //暂停游戏 void PauseGame(); //继续游戏 void GoOnGame(); //由于调用次数很多, 设为内联函数 inline void StickBk();//贴背景 inline void StickBullet();//贴子弹 inline void BulletOut();//排除出界的子弹 inline void MoveMonster();//移动怪物机 inline void MoveBullet();//移动子弹 inline int Beat(int i);//返回子弹i射击到的怪物机j //重新启动怪物机 inline void RestartMonster(bool isStart, int i); //为索引为i的子弹求水平、垂直速度 inline void GetXY(CPoint org, CPoint end, int i); // 生成的消息映射函数 protected: DECLARE_MESSAGE_MAP() public: afx_msg void OnTimer(UINT_PTR nIDEvent); afx_msg void OnDestroy(); afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); };
(3). 通过头文件可以看见,我定义了一个枚举变量,用来控制游戏界面的状态
//显示状态: 开始 游戏中 结束 enum STATE{START, RUNNING, END};
根据游戏状态绘制游戏界面void CSpaceWarView::OnDraw(CDC* pDC) { CSpaceWarDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if (!pDoc) return; // TODO: 在此处为本机数据添加绘制代码 //创建内存位图、内存DC以便在内存中绘图, 实现双缓冲 m_bufferDC.CreateCompatibleDC(NULL); m_bufferBmp.CreateCompatibleBitmap(pDC, m_gameBk.fir.GetWidth() * 2, m_gameBk.fir.GetHeight()); m_bufferDC.SelectObject(m_bufferBmp); switch(m_state) { //绘制开始界面 case START: StartUI(); break; //绘制游戏界面 case RUNNING: RunningUI(); break; //绘制结束界面 case END: EndUI(); break; } //将内存中的图贴到客户区中 pDC->BitBlt(0, 0, m_sClient.cx, m_sClient.cy, &m_bufferDC, 0, 0, SRCCOPY); //释放内存位图和内存DC m_bufferBmp.DeleteObject(); m_bufferDC.DeleteDC(); }
(4). 动态贴游戏背景图片
//贴背景 void CSpaceWarView::StickBk() { //如果到了左边界,就换图 if(m_gameBk.curx == -m_gameBk.fir.GetWidth()) { m_gameBk.curx = 0; m_gameBk.isFir = m_gameBk.isFir? 0 : 1; } int width = m_gameBk.fir.GetWidth(); //如果第一张图在前面,就先绘制第一张 if(m_gameBk.isFir) { m_gameBk.fir.BitBlt(m_bufferDC, m_gameBk.curx, 0, SRCCOPY); m_gameBk.sec.BitBlt(m_bufferDC, m_gameBk.curx + width, 0, SRCCOPY); } //第二张图在前面,先绘制第二张 else { m_gameBk.sec.BitBlt(m_bufferDC, m_gameBk.curx, 0, SRCCOPY); m_gameBk.fir.BitBlt(m_bufferDC, m_gameBk.curx + width, 0, SRCCOPY); } }
(5). 对速度求解x、y轴方向的速度分量
//为索引为i的子弹求水平、垂直速度 void CSpaceWarView::GetXY(CPoint org, CPoint end, int i) { //求得子弹的中心位置 org.x += m_bullet[i].png.GetWidth(); org.y += m_bullet[i].png.GetHeight(); //如果两点在一条垂直直线上 if(org.x == end.x) { m_bullet[i].ix = 0; m_bullet[i].iy = m_bullet[i].speed; if(org.y < end.y) m_bullet[i].iy = - m_bullet[i].iy; } //如果两点在一条水平直线上 else if(org.y == end.y) { m_bullet[i].iy = 0; m_bullet[i].ix = m_bullet[i].speed; if(org.x > end.x) m_bullet[i].ix = - m_bullet[i].ix; } else { //求斜率k(正切)、倾斜角angle、cos(余弦)、sin(正弦) double k = (org.y - end.y) * 1.0 / (org.x - end.x); double angle = atan(k); double cosine = cos(angle); double sine = sin(angle); /* 将直线上的速度speed分解为x、y轴上的分量 分别表示x上的移动速度、y上的移动速度 */ m_bullet[i].ix = (int)fabs(m_bullet[i].speed * cosine); if(org.x > end.x) m_bullet[i].ix = - m_bullet[i].ix; m_bullet[i].iy = (int)fabs(m_bullet[i].speed * sine); if(org.y > end.y) m_bullet[i].iy = - m_bullet[i].iy; } }
(6). 点击鼠标左键发送子弹
//如果没有暂停游戏 if(!m_stopGoOn.isStop) { //按下鼠标左键, 发送子弹 for(size_t i=0; i<m_bullet.size(); i++) { if(m_bullet[i].isOut) { m_bullet[i].speed = 20; int x = m_ptBltOrg.x; int y = m_ptBltOrg.y; m_bullet[i].rect.SetRect(x, y, m_bullet[i].png.GetWidth() + x, m_bullet[i].png.GetHeight() + y); GetXY(m_ptBltOrg, point, i); m_bullet[i].isOut = false; break; } } //异步播放射击音乐 PlaySound (TEXT ("res\\music\\attack.wav"), NULL, SND_FILENAME | SND_ASYNC); }
(7). 在移动怪物机时检测怪物机是否出了边界
//在移动怪物机过程中判断怪物机是否出了边界 void CSpaceWarView::MoveMonster() { for(size_t i=0; i<m_vecMons.size(); i++) { //水平移动怪物机只需要变化rect的left和right m_vecMons[i].rect.left -= m_vecMons[i].speed; //如果怪物机出了边界, 进入游戏结束状态 if(m_vecMons[i].rect.left < -m_vecMons[i].rect.Width()) { m_state = END; EndGame(); InvalidateRect(NULL, FALSE); break; } m_vecMons[i].rect.right -= m_vecMons[i].speed; } }
(8). 检测子弹是否射击到了怪物机
//返回子弹i射击到的怪物机j(-1表示未射击到) int CSpaceWarView::Beat(int i) { CRect r = m_bullet[i].rect; for(size_t j=0; j<m_vecMons.size(); j++) { if(m_vecMons[j].rect.PtInRect(CPoint(r.left, r.top)) || m_vecMons[j].rect.PtInRect(CPoint(r.right, r.top)) || m_vecMons[j].rect.PtInRect(CPoint(r.left, r.bottom)) || m_vecMons[j].rect.PtInRect(CPoint(r.right, r.bottom)) ) { return j;//射击到了索引为j的怪物机 } } //未射击到怪物机 return -1; }
(9). 在移动子弹的时检测是否射击到了怪物机
//移动子弹 void CSpaceWarView::MoveBullet() { for(size_t i=0; i<m_bullet.size(); i++) { if(!m_bullet[i].isOut) { //返回当前子弹射击到的怪物机索引 int index_beat = Beat(i); //如果子弹射击到了怪物机 if(index_beat != -1) { //计分 m_score += 5; //重新启动当前怪物机 RestartMonster(false, index_beat); //标记当前子弹出界了 m_bullet[i].isOut = true; //异步播放爆炸音乐 PlaySound (TEXT ("res\\music\\die.wav"), NULL, SND_FILENAME | SND_ASYNC); continue; } //移动子弹 m_bullet[i].rect.left += m_bullet[i].ix; m_bullet[i].rect.right += m_bullet[i].ix; m_bullet[i].rect.top += m_bullet[i].iy; m_bullet[i].rect.bottom += m_bullet[i].iy; } } }
(10). 释放游戏所使用的所有资源
//善后工作 void CSpaceWarView::OnDestroy() { CView::OnDestroy(); /* 程序退出之前, 释放所有内存资源 */ EndGame(); //释放开始界面资源 m_startBk.bk.ReleaseGDIPlus(); m_startBk.normal.ReleaseGDIPlus(); m_startBk.selected.ReleaseGDIPlus(); //释放游戏界面资源 m_gameBk.fir.ReleaseGDIPlus(); m_gameBk.sec.ReleaseGDIPlus(); m_hero.png.ReleaseGDIPlus(); for(size_t i=0; i<m_bullet.size(); i++) { m_bullet[i].png.ReleaseGDIPlus(); } for(size_t i=0; i<m_vecMons.size(); i++) { m_vecMons[i].png.ReleaseGDIPlus(); } //释放结束界面资源 m_endBk.bk.ReleaseGDIPlus(); m_endBk.gameover.ReleaseGDIPlus(); }
四、免费资源下载
【VC++游戏开发#三】2D篇 —— 游戏之一:空中大战(SpaceWar)
最新推荐文章于 2022-10-06 22:00:31 发布