基于VC++的MFC类库的飞机大战游戏的设计与实现

FullHouse

发布日期: 2018-11-05 15:24:49 浏览量: 2238
评分:
star star star star star star star star star star
*转载请注明来自write-bug.com

1 概述

1.1 项目简介

本次实训项目是做一个飞机大战的游戏,应用MFC编程,完成一个界面简洁流畅、游戏方式简单,玩起来易于上手的桌面游戏。该飞机大战项目运用的主要技术即是MFC编程中的一些函数、链表思想以及贴图技术。

1.2 实训功能说明

1.2.1 基本功能

  • 通过键盘,方向键和ASWD键可控制战机的位置,空格键和鼠标左键发射子弹

  • 界面中敌机出现的位置,以及敌机和Boss炸弹的发射均为随机的,敌机与敌机炸弹、Boss炸弹均具有一定的速度,且随着关卡难度的增大,数量和速度均随着关卡数增加而增加

  • 对于随机产生的敌机和敌机炸弹,若超过矩形区域,则释放该对象

  • 添加碰撞效果,包括战机子弹打中敌机爆炸、敌机炸弹打中战机爆炸、战机与敌机相撞爆炸、战机子弹与敌机炸弹相撞爆炸、战机子弹打中Boss、战机与Boss碰撞以及战机吃到血包七种碰撞效果。且碰撞发生后子弹、炸弹、血包均消失,战机生命值减一,敌机和Boss生命值减少当前战机炮弹威力的生命值,若敌机或Boss生命值归零,则删除敌机或Boss

  • 血包:随着关卡游戏进程的进行,会出现一定量的血包供战机补给生命值,血包会在客户区矩形框内运动,10秒后消失;若战机在10秒内吃到血包,则会增加5点生命值知道生命值上限

  • 每关中战机有三条命,每条命10点生命值,生命使用完后,会进入GameOver界面显示得分数,并提供重新开始游戏和退出功能

  • 游戏提供10个关卡,每个关卡需要打死相应关卡的敌机数量才能进入Boss模式,打败Boss之后将会进入下一关。10关通关后,显示通关界面,并提供重新开始游戏和退出游戏的功能选项

  • 暂停功能:游戏进行过程中按下Z键可进入暂停模式,再按Z则返回游戏

  • 无敌模式:游戏进行过程中按下Y键可进入无敌模式,再按Y则返回正常游戏。该模式下战机生命值不会减少,可供测试使用

  • 魔法值:游戏进行过程中,战机魔法值会随着时间递增到上限10,魔法值供战机道具功能的使用,过一个关卡魔法值不清零

  • 战机大招:当战机魔法值为10满状态时,按下X键消耗所有魔法值可发动大招,对屏幕中的敌机进行清屏,Boss扣50点血量

  • 防护罩:当魔法值不为0时,按下C键可打开防护罩道具,该状态下战机处于无敌状态,不会损失生命值,但魔法值会随着防护罩开启慢慢降低

  • 战机升级功能:战机子弹单个威力为1,在魔法值不为0时,按下V键开启升级战机模式,战机图标变为动画,子弹威力变成两倍。(若同时开启防护罩和战机升级,则魔法值递减速度翻倍)

1.2.2 附加功能

  • 为游戏界面每个关卡添加了滚动背景图片和背景音乐,并在敌机发送炮弹、战机发射子弹、战机击中敌机、敌机击中战机、战机敌机相撞、敌机战机子弹相撞、战机吃到血包、战机大招、战机升级、战机防护罩、游戏结束时均添加了音效

  • 为美化游戏界面,采用了一部分全民飞机大战图标,并添加了爆炸动画和升级战机动画特效,背景音乐采用微信飞机大战背景音乐和相关特效音效

  • 为游戏设置了不同的关卡,每个关卡难度不同,敌机与敌机炸弹的速度随着关卡增大而加快,进入第五关以后敌机从上下方均会随机出现,且随机发射炸弹

  • 前五关卡敌机从上方飞出,速度一定,战机每打掉一架敌机则增加一分,当战机得分超过该关卡所需分数(和关卡数相关)则可进入Boss模式,打败Boss进入下一关;进入第六关以后,敌机分别从上下两方飞出。随着关卡数增加,敌机数量增加,速度增快,敌机炮弹数量和速度也相应增加,进入Boss所需分数增加,Boss生命值和火力也随着关卡数的增加而增加,游戏难度陡然直升

  • 游戏界面中显示当前状态下的关卡数、当前命数、当前得分、战机血条、战机魔法条、无敌模式提醒和战机道具提醒,Boss模式下还有Boss血条

  • 增加了鼠标控制战机位置这一效果,战绩的位置随着鼠标的移动而移动,并且点击鼠标左键可使得战机发射子弹

  • 进入游戏先进入欢迎界面,欢迎界面中显示游戏使用说明,点击鼠标左键和空格键开始游戏。游戏过程中战机命数使用完、通关均有相应界面进行提醒,用户可选择重新开始游戏或退出游戏

2 相关技术

2.1 Windows定时器技术

Windows定时器是一种输入设备,它周期性地在每经过一个指定的时间间隔后就通知应用程序一次。程序将时间间隔告诉Windows,然后Windows给您的程序发送周期性发生的WM_TIMER消息以表示时间到了。本程序中使用多个定时器,分别控制不同的功能。在MFC的API函数中使用SetTimer()函数设置定时器,设置系统间隔时间,在OnTimer()函数中实现响应定时器的程序。

2.2 透明贴图实现技术

绘制透明位图的关键就是创建一个“掩码”位图(mask bitmap),这个“掩码”位图是一个单色位图,它是位图中图像的一个单色剪影。

在详细介绍实现过程之前先介绍下所使用的画图函数以及函数参数所代表的功能;整个绘制过程需要使用到BitBlt()函数。整个功能的实现过程如下:

  1. 创建一张大小与需要绘制图像相同的位图作为“掩码”位图

  2. 将新创建的“掩码”位图存储至掩码位图的设备描述表中

  3. 把位图设备描述表的背景设置成“透明色”,不需要显示的颜色

  4. 复制粘贴位图到“掩码”位图的设备描述表中,这个时候“掩码”位图设备描述表中存放的位图与位图设备描述表中的位图一样

  5. 把需要透明绘制的位图与对话框绘图相应区域的背景进行逻辑异或操作绘制到对话框上

  6. 把“掩码”位图与这个时候对话框相应区域的背景进行逻辑与的操作

  7. 重复步骤5的操作,把需要透明绘制的位图与对话框绘图相应区域的背景进行逻辑异或操作绘制到对话框上

  8. 最后把系统的画笔还给系统,删除使用过的GDIObject,释放非空的指针,最后把新建的设备描述表也删除

2.3 CObList链表

MFC类库中提供了丰富的CObList类的成员函数,此程序主要用到的成员函数如下:

  • 构造函数,为CObject指针构造一个空的列表

  • GetHead(),访问链表首部,返回列表中的首元素(列表不能为空)

  • AddTail(),在列表尾增加一个元素或另一个列表的所有元素

  • RemoveAll(),删除列表中所有的元素

  • GetNext(),返回列表中尾元素的位置

  • GetHeadPosition(),返回列表中首元素的位置

  • RemoveAt(),从列表中删除指定位置的元素

  • GetCount(),返回列表中的元素数

在CPlaneGameView.h文件中声明各游戏对象与游戏对象链表:

  • 滚动背景模块

    1. CScenescene;//游戏背景对象
    2. CImageList startIMG;欢迎界面图像列表
  • 各游戏对象

    1. CMyPlane *myplane;
    2. CEnemy *enemy;
    3. CBoss *boss;
    4. CBomb *bomb;
    5. CBall *ball;
    6. CExplosion *explosion;
    7. CBlood *blood;
  • 存储游戏对象的对象链表

    1. CObList enemyList;
    2. CObList meList;
    3. CObList bombList;
    4. CObList ballList;
    5. CObList explosionList;
    6. CObList bloodList;
  • 客户区窗口矩形(用来动态获取客户区矩形大小)

    1. CRect rect;//窗口屏幕矩形
  • 游戏运行相关参数:

    1. int speed;//战机的速度,方向键控制
    2. int myLife;//为战机设置生命值
    3. int lifeNum;//战机命条数
    4. int myScore;//战机的得分
    5. int passScore;//当前关卡得分数
    6. int lifeCount;//血包产生控制参数
    7. int magicCount;//魔法值,控制能否发大招
    8. int bossBlood;//Boss血量
    9. int passNum;//记录当前关卡
  • 游戏运行相关标志位

    1. BOOL bloodExist;//标记屏幕中是否存在血包
    2. int isPass;//是否通关的标志
    3. int isPause;//是否暂停
    4. BOOL isBoss;//标记是否进入Boss
    5. BOOL bossLoaded;//标记Boss出场完成
    6. BOOL isProtect;//标记是否开启防护罩
    7. BOOL isUpdate;//标记战机是否升级
    8. BOOL test;//无敌模式标志位
    9. BOOL isStop;//标记游戏停止
    10. BOOL isStarted;//标记欢迎界面是否加载完成

2.4 获取矩形区域

首先,使用CRect定义一个对象,然后使用GetClientRect(&对象名)函数,获取界面的矩形区域rect.Width() 为矩形区域的宽度,rect.Height()为矩形区域的高度。

使用IntersectRect(&,&))函数来判断两个源矩形是否有重合的部分。如果有不为空,则返回非零值;否则,返回0。

2.5内存释放

在VC/MFC用CDC绘图时,频繁的刷新,屏幕会出现闪烁的现象,CPU时间占用率相当高,绘图效率极低,很容易出现程序崩溃。及时的释放程序所占用的内存资源是非常重要的。

在程序中使用到的链表、刷子等占用内存资源的对象都要及时的删除。Delete Brush,List.removeall()等。

2.6 CImageList处理爆炸效果

爆炸效果是连续的显示一系列的图片。如果把每一张图片都显示出来的话,占用的时间是非常多的,必然后导致程序的可行性下降。CImageList是一个“图象列表”是相同大小图象的集合,每个图象都可由其基于零的索引来参考。可以用来存放爆炸效果的一张图片,使用Draw()函数来绘制在某拖拉操作中正被拖动的图象,即可通过Timer消息连续绘制出多张图片做成的爆炸效果。

3 总体设计与详细设计

3.1 系统模块划分

该飞机大战游戏程序分为游戏滚动背景绘制模块、各游戏对象绘制模块、游戏对象之间的碰撞模块、爆炸效果产生模块、游戏界面输出玩家得分关卡信息模块、战机道具技能维护模块、消息处理模块、视图生命周期维护模块。

其中在游戏对象绘制模块中,战机是唯一对象,在游戏开始时产生该对象,赋予其固定的生命值,当其与敌机对象、敌机炸弹碰撞时使其生命值减一,直至生命值为零,便删除战机对象。敌机对象与敌机炸弹对象的绘制中采用定时器技术,定时产生。爆炸对象初始化为空,当游戏过程中即时发生碰撞时,在碰撞位置产生爆炸对象,添加到爆炸链表中,并根据爆炸素材图像分八帧进行输出,达到动画特效。

3.2 主要功能模块

3.2.1 系统对象类图

CGameObject是各个游戏对象的抽象父类,继承自CObject类,其他的类:战机类、敌机类、爆炸类、子弹类、炸弹类、血包类、文字类都继承了此类,CBoss类继承敌机类。
每个游戏对象类中既继承了来自父类CGameObject的属性,又有自己的特有属性和方法。

3.2.2 系统主程序活动图

3.2.3 系统部分流程图

飞机大战游戏执行流程图

定时器产生敌机和炸弹流程图

血包执行流程图

4 编码实现

4.1 双缓冲

  1. //双缓冲
  2. CDC *pDC = GetDC();
  3. //获得客户区矩形区域
  4. GetClientRect(&rect);
  5. //内存缓冲CDC
  6. CDC cdc;
  7. //内存中承载临时图像的缓冲位图
  8. CBitmap* cacheBitmap = new CBitmap;
  9. //用当前设备CDC初始化缓冲CDC
  10. cdc.CreateCompatibleDC(pDC);
  11. //绑定pDC和缓冲位图的关系,cdc先输出到缓冲位图中,输出完毕之后再一次性将缓冲位图输出到屏幕
  12. cacheBitmap->CreateCompatibleBitmap(pDC, rect.Width(), rect.Height());
  13. //替换cdc原本的缓冲区为缓冲位图,这样cdc输出的内容就写到了缓冲位图中
  14. CBitmap *pOldBit = cdc.SelectObject(cacheBitmap);
  15. //将二级缓冲cdc中的数据推送到一级级缓冲pDC中,即输出到屏幕中
  16. pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &cdc, 0, 0, SRCCOPY);
  17. //释放二级cdc
  18. cdc.DeleteDC();
  19. //释放缓冲位图
  20. cacheBitmap->DeleteObject();
  21. //释放一级pDC
  22. ReleaseDC(pDC);

4.2 滚动背景

在滚动背景的初始化函数和释放函数添加背景音乐播放和释放。

  1. class CScene
  2. {
  3. //成员变量
  4. private:
  5. CImage images[8];//滚动背景,0位为开始图片,1-7为七张不同的背景
  6. int beginY;//背景的Y坐标
  7. bool isStart;//是否开始
  8. //成员函数
  9. public:
  10. bool InitScene();//初始化场景
  11. void MoveBg();//移动背景
  12. ////绘制场景(注:这里bufferDC是引用参数)
  13. void StickScene(CDC* pDC, int index, CRect rClient);//传入index-1表示输出开始图片
  14. void ReleaseScene();//释放内存资源
  15. //构造与析构
  16. public:
  17. CScene(void);
  18. ~CScene(void);
  19. };
  20. //初始化场景
  21. bool CScene::InitScene()
  22. {
  23. //加载开始图片
  24. this->images[0].Load(_T("image\\start.bmp"));
  25. CString str;
  26. //如果加载失败, 返回false
  27. for (int i = 1; i <= 7; i++) {
  28. str.Format(_T("image\\background%d.bmp"), i);
  29. this->images[i].Load(str);
  30. if (images[i].IsNull())
  31. return false;
  32. }
  33. //开始为真, 背景起始坐标为0
  34. this->isStart = true;
  35. this->beginY = 0;
  36. //播放背景音乐
  37. mciSendString(L"open sound\\background.mp3 alias bgm", NULL, 0, NULL);
  38. mciSendString(L"play bgm repeat", NULL, 0, NULL);
  39. return true;
  40. }
  41. //绘制场景
  42. void CScene::StickScene(CDC* pDC,int index, CRect rClient)
  43. {
  44. if (index == -1)
  45. index = 0;
  46. else
  47. index = index % 7 + 1;
  48. //设置缩放图片的模式为:COLORONCOLOR, 以消除像素重叠
  49. pDC->SetStretchBltMode(COLORONCOLOR);
  50. //如果到了下边界, 回到起点
  51. if (beginY >= rClient.Height())
  52. {
  53. beginY = 0;
  54. if (isStart)
  55. isStart = false;
  56. }
  57. //客户区高度
  58. int cltHeight = rClient.Height();
  59. rClient.bottom = cltHeight + beginY;
  60. rClient.top = beginY;
  61. //如果是开始就绘制起始背景
  62. if (isStart)
  63. {
  64. this->images[index].StretchBlt(*pDC, rClient, SRCCOPY);
  65. }
  66. //将下一张背景作为起始背景
  67. else
  68. {
  69. this->images[index].StretchBlt(*pDC, rClient, SRCCOPY);
  70. }
  71. //绘制下一张背景
  72. rClient.top -= cltHeight;
  73. rClient.bottom -= cltHeight;
  74. images[index].StretchBlt(*pDC, rClient, SRCCOPY);
  75. }
  76. //移动背景
  77. void CScene::MoveBg()
  78. {
  79. //移动背景
  80. beginY += 1;
  81. }
  82. //释放内存资源
  83. void CScene::ReleaseScene()
  84. {
  85. for (int i = 0; i <8; i++)
  86. if (!images[i].IsNull())
  87. images[i].Destroy();
  88. mciSendString(L"close bgm", NULL, 0, NULL);
  89. }

onTimer中控制背景滚动:

  1. //输出背景
  2. if (isStarted == FALSE)
  3. scene.StickScene(&cdc, -1, rect);
  4. else
  5. scene.StickScene(&cdc, passNum, rect);
  6. if (nIDEvent == 4) {
  7. //滚动背景
  8. scene.MoveBg();
  9. }

4.3 显示战机

  1. if (myplane != NULL)
  2. {
  3. myplane->Draw(&cdc,FALSE,isProtect);
  4. }

4.4 随机产生敌机和敌机炮弹、Boss炮弹

  1. //随机添加敌机,敌机随机发射炸弹,此时敌机速度与数量和关卡有关
  2. if (myplane != NULL && isPause == 0&&isBoss==FALSE)
  3. {
  4. //敌机产生定时器触发
  5. if (nIDEvent == 2) {
  6. //根据关卡数产生敌机
  7. if (passNum <=5) {
  8. //前五关只有一个方向的敌机
  9. int direction = 1;//设置敌机的方向,从上方飞出
  10. CEnemy *enemy = new CEnemy(direction, rect.right, rect.bottom);
  11. enemyList.AddTail(enemy);//随机产生敌机
  12. }
  13. else if (passNum >5) {//第五关之后,两个方向的敌机
  14. int direction1 = 1; //设置敌机的方向,从上方飞出
  15. CEnemy *enemy1 = new CEnemy(direction1,rect.right,rect.bottom);
  16. enemy1->SetSpeed(ENEMY_SPEED+(rand() % 2 + passNum-1));
  17. enemyList.AddTail(enemy1);//随机产生敌机
  18. int direction2 = -1;//设置敌机的方向,从下方飞出
  19. CEnemy *enemy2 = new CEnemy(direction2, rect.right, rect.bottom);
  20. enemy2->SetSpeed(ENEMY_SPEED + (rand() % 2 + passNum - 1));
  21. enemyList.AddTail(enemy2);//随机产生敌机
  22. }
  23. }
  24. //超出边界的敌机进行销毁
  25. POSITION stPos = NULL, tPos = NULL;
  26. stPos = enemyList.GetHeadPosition();
  27. int direction = 1;
  28. while (stPos != NULL)
  29. {
  30. tPos = stPos;
  31. CEnemy *enemy = (CEnemy *)enemyList.GetNext(stPos);
  32. //判断敌机是否出界
  33. if (enemy->GetPoint().x<rect.left || enemy->GetPoint().x>rect.right
  34. || enemy->GetPoint().y<rect.top || enemy->GetPoint().y>rect.bottom)
  35. {
  36. enemyList.RemoveAt(tPos);
  37. delete enemy;
  38. }//if
  39. else
  40. {
  41. //没出界,绘制
  42. enemy->Draw(&cdc,passNum, FALSE);
  43. //敌机炸弹产生定时器触发
  44. if (nIDEvent == 3) {
  45. //设置定时器产生敌机炸弹
  46. PlaySound((LPCTSTR)IDR_WAV_BALL, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
  47. CBall * ball = new CBall(enemy->GetPoint().x + ENEMY_HEIGHT / 2, enemy->GetPoint().y + ENEMY_HEIGHT, enemy->GetDirection());
  48. ball->SetBallSpeed(BALL_SPEED + (passNum - 1) );
  49. ballList.AddTail(ball);
  50. }
  51. }//else
  52. }//while
  53. //判断产生的敌机炸弹是否出界,若已经出界,则删除该敌机炸弹
  54. POSITION stBallPos=NULL, tBallPos=NULL;
  55. stBallPos = ballList.GetHeadPosition();
  56. while (stBallPos != NULL)
  57. {
  58. tBallPos = stBallPos;
  59. ball = (CBall *)ballList.GetNext(stBallPos);
  60. if (ball->GetPoint().x<rect.left || ball->GetPoint().x>rect.right
  61. || ball->GetPoint().y<rect.top || ball->GetPoint().y>rect.bottom)
  62. {
  63. ballList.RemoveAt(tBallPos);
  64. delete ball;
  65. }//if
  66. else
  67. {
  68. ball->Draw(&cdc,passNum, FALSE);
  69. }//else
  70. }//while
  71. }
  72. //Boss产生炮弹,一次五发炮弹
  73. if (myplane != NULL && isPause == 0 && isBoss == TRUE) {
  74. //Boss发射子弹
  75. //敌机炸弹产生定时器触发
  76. if (nIDEvent == 3) {
  77. //设置定时器产生敌机炸弹
  78. PlaySound((LPCTSTR)IDR_WAV_BALL, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
  79. CBall * ball1 = new CBall(boss->GetPoint().x + BOSS_WIDTH / 2, boss->GetPoint().y + BOSS_HEIGHT, 1);
  80. ball1->SetBallSpeed(BALL_SPEED + (passNum - 1) * 2);
  81. ballList.AddTail(ball1);
  82. CBall * ball2 = new CBall(boss->GetPoint().x +5, boss->GetPoint().y + BOSS_HEIGHT, 1);
  83. ball2->SetBallSpeed(BALL_SPEED + (passNum - 1) * 2);
  84. ballList.AddTail(ball2);
  85. CBall * ball3 = new CBall(boss->GetPoint().x + BOSS_WIDTH -5, boss->GetPoint().y + BOSS_HEIGHT,1);
  86. ball3->SetBallSpeed(BALL_SPEED + (passNum - 1) * 2);
  87. ballList.AddTail(ball3);
  88. CBall * ball4 = new CBall(boss->GetPoint().x + BOSS_WIDTH / 2+85, boss->GetPoint().y + BOSS_HEIGHT, 1);
  89. ball4->SetBallSpeed(BALL_SPEED + (passNum - 1) * 2);
  90. ballList.AddTail(ball4);
  91. CBall * ball5 = new CBall(boss->GetPoint().x + BOSS_WIDTH / 2-85, boss->GetPoint().y + BOSS_HEIGHT, 1);
  92. ball5->SetBallSpeed(BALL_SPEED + (passNum - 1) * 2);
  93. ballList.AddTail(ball5);
  94. }
  95. //显示Boss炸弹
  96. //判断产生的敌机炸弹是否出界,若已经出界,则删除该敌机炸弹
  97. POSITION stBallPos = NULL, tBallPos = NULL;
  98. stBallPos = ballList.GetHeadPosition();
  99. while (stBallPos != NULL)
  100. {
  101. tBallPos = stBallPos;
  102. ball = (CBall *)ballList.GetNext(stBallPos);
  103. if (ball->GetPoint().x<rect.left || ball->GetPoint().x>rect.right
  104. || ball->GetPoint().y<rect.top || ball->GetPoint().y>rect.bottom)
  105. {
  106. ballList.RemoveAt(tBallPos);
  107. delete ball;
  108. }//if
  109. else
  110. {
  111. ball->Draw(&cdc, FALSE);
  112. }//else
  113. }//while
  114. }

4.5 显示战机发射子弹

  1. if (myplane != NULL&& isPause == 0)
  2. {
  3. //声明战机子弹位置
  4. POSITION posBomb = NULL, tBomb = NULL;
  5. posBomb = bombList.GetHeadPosition();
  6. while (posBomb != NULL)
  7. {
  8. tBomb = posBomb;
  9. bomb = (CBomb *)bombList.GetNext(posBomb);
  10. if (bomb->GetPoint().x<rect.left || bomb->GetPoint().x>rect.right
  11. || bomb->GetPoint().y<rect.top || bomb->GetPoint().y>rect.bottom)
  12. {
  13. //删除越界的子弹
  14. bombList.RemoveAt(tBomb);
  15. delete bomb;
  16. }
  17. else
  18. bomb->Draw(&cdc, FALSE);
  19. }
  20. }

4.6 碰撞检测

以战机子弹集中敌机为例。

  1. if (myplane != NULL&& isPause == 0)
  2. {
  3. //声明战机子弹位置,敌机位置
  4. POSITION bombPos, bombTemp, enemyPos, enemyTemp;
  5. for (bombPos = bombList.GetHeadPosition(); (bombTemp = bombPos) != NULL;)
  6. {
  7. bomb = (CBomb *)bombList.GetNext(bombPos);
  8. for (enemyPos = enemyList.GetHeadPosition(); (enemyTemp = enemyPos) != NULL;)
  9. {
  10. enemy = (CEnemy *)enemyList.GetNext(enemyPos);
  11. //获得战机子弹的矩形区域
  12. CRect bombRect = bomb->GetRect();
  13. //获得敌机的矩形区域
  14. CRect enemyRect = enemy->GetRect();
  15. //判断两个矩形区域是否有交接
  16. CRect tempRect;
  17. if (tempRect.IntersectRect(&bombRect, enemyRect))
  18. {
  19. //将爆炸对象添加到爆炸链表中
  20. CExplosion *explosion = new CExplosion((bomb->GetPoint().x + BOMB_WIDTH / 2 - EXPLOSION_WIDTH / 2), (bomb->GetPoint().y + BOMB_HEIGHT / 2 - EXPLOSION_WIDTH / 2));
  21. explosionList.AddTail(explosion);
  22. PlaySound((LPCTSTR)IDR_WAV_EXPLOSION, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
  23. //爆炸后删除子弹
  24. bombList.RemoveAt(bombTemp);
  25. delete bomb;
  26. //指向下一个
  27. bomb = NULL;
  28. //敌机生命值减少
  29. enemy->life -= (1 + isUpdate);
  30. if (enemy->life <= 0) {
  31. //增加得分
  32. passScore++;
  33. //删除敌机
  34. enemyList.RemoveAt(enemyTemp);
  35. delete enemy;
  36. }
  37. //炮弹已删除,直接跳出本循环
  38. break;
  39. }
  40. }
  41. if (isBoss == TRUE&&bomb != NULL) {
  42. //获得战机子弹的矩形区域
  43. CRect bombRect = bomb->GetRect();
  44. //获得Boss的矩形区域
  45. CRect bossRect = boss->GetRect();
  46. //判断两个矩形区域是否有交接
  47. CRect tempRect;
  48. if (tempRect.IntersectRect(&bombRect, bossRect))
  49. {
  50. //将爆炸对象添加到爆炸链表中
  51. CExplosion *explosion = new CExplosion((bomb->GetPoint().x + BOMB_WIDTH / 2 - EXPLOSION_WIDTH / 2), (bomb->GetPoint().y + BOMB_HEIGHT / 2 - EXPLOSION_WIDTH / 2));
  52. explosionList.AddTail(explosion);
  53. PlaySound((LPCTSTR)IDR_WAV_EXPLOSION, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
  54. //爆炸后删除子弹
  55. bombList.RemoveAt(bombTemp);
  56. delete bomb;
  57. bomb = NULL;
  58. //是Boss,不删除敌机,只扣血
  59. bossBlood -= (1 + isUpdate);
  60. if (bossBlood <= 0) {
  61. delete boss;
  62. boss = NULL;
  63. //过关的标志变量
  64. delete myplane;
  65. myplane = NULL;
  66. isPass = 1;
  67. isBoss = FALSE;
  68. }
  69. }
  70. }
  71. }
  72. }

4.7 显示爆炸效果

  1. if (myplane != NULL&&isPause == FALSE)
  2. {
  3. POSITION explosionPos, explosionTemp;
  4. explosionPos = explosionList.GetHeadPosition();
  5. //检索爆炸链表,非空时在所在位置显示
  6. while (explosionPos != NULL)
  7. {
  8. explosionTemp = explosionPos;
  9. explosion = (CExplosion *)explosionList.GetNext(explosionPos);
  10. BOOL flag = explosion->Draw(&cdc, FALSE);
  11. //爆炸8帧结束,删除爆炸对象
  12. if (flag == EXPLOSION_STATUS_STOP)
  13. {
  14. explosionList.RemoveAt(explosionTemp);
  15. delete explosion;
  16. }
  17. }//while
  18. }

4.8 血包功能

游戏三分之一和三分之二进程时刻出现血包。

  1. //开启血包
  2. if (myplane != NULL && myLife > 0)
  3. {
  4. //关卡打了三分之一三分之二处出现血包
  5. if (passScore > (PASS_SCORE + passNum * 5)*lifeCount/3)
  6. {
  7. //若屏幕中有未吃掉的血包,这次不产生血包
  8. if (bloodExist == FALSE) {
  9. lifeCount++;
  10. //产生血包
  11. blood = new CBlood(rect.right,rect.bottom);
  12. bloodList.AddTail(blood);
  13. bloodExist = TRUE;
  14. SetTimer(6, 10000, NULL);
  15. }else lifeCount++;
  16. }
  17. }
  18. //血包定时器,10秒后血包消失
  19. if (nIDEvent == 6&&isPause==0) {
  20. KillTimer(6);
  21. bloodExist = FALSE;
  22. //声明血包位置
  23. POSITION bloodPos, bloodTemp;
  24. for (bloodPos = bloodList.GetHeadPosition(); (bloodTemp = bloodPos) != NULL;)
  25. {
  26. blood = (CBlood *)bloodList.GetNext(bloodPos);
  27. bloodList.RemoveAt(bloodTemp);
  28. delete blood;
  29. }
  30. }
  31. //显示血包
  32. if (myplane != NULL&&isPause == FALSE)
  33. {
  34. POSITION bloodPos;
  35. bloodPos = bloodList.GetHeadPosition();
  36. //检索血包链表,非空时在所在位置显示
  37. while (bloodPos != NULL)
  38. {
  39. blood = (CBlood *)bloodList.GetNext(bloodPos);
  40. blood->Draw(&cdc, FALSE);
  41. }//while
  42. }
  43. //血包碰撞检测
  44. if (myplane != NULL&& isPause == 0)
  45. {
  46. //声明血包位置
  47. POSITION bloodPos, bloodTemp;
  48. for (bloodPos = bloodList.GetHeadPosition(); (bloodTemp = bloodPos) != NULL;)
  49. {
  50. blood = (CBlood *)bloodList.GetNext(bloodPos);
  51. //获得血包矩形
  52. CRect bloodbRect = blood->GetRect();
  53. //获得战机矩形
  54. CRect planeRect = myplane->GetRect();
  55. //判断两个矩形区域是否有交接
  56. CRect tempRect;
  57. if (tempRect.IntersectRect(&bloodbRect, planeRect))
  58. {
  59. //加血效果
  60. myLife += 5;
  61. if (myLife > DEFAULT_LIFE)
  62. myLife = DEFAULT_LIFE;
  63. // TODO 声音
  64. PlaySound((LPCTSTR)IDR_WAV_BLOOD, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
  65. //加血后血包删除
  66. bloodList.RemoveAt(bloodTemp);
  67. delete blood;
  68. break;
  69. }
  70. }
  71. }

4.9 通关和死亡消息页面

  1. if (isStop == FLAG_RESTART)
  2. {
  3. HFONT textFont;
  4. textFont = CreateFont(18, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100, 10, 0);
  5. cdc.SelectObject(textFont);
  6. //设置透明背景
  7. cdc.SetBkMode(TRANSPARENT);
  8. cdc.SetTextColor(RGB(255, 255, 255));
  9. cdc.TextOutW(rect.right/2-150, rect.bottom/2-30, _T("哇,恭喜你已通关!是否重新开始?Y/N"));
  10. }
  11. else if (isStop == FLAG_STOP)
  12. {
  13. HFONT textFont;
  14. textFont = CreateFont(18 , 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100, 10, 0);
  15. cdc.SelectObject(textFont);
  16. //设置透明背景
  17. cdc.SetBkMode(TRANSPARENT);
  18. cdc.SetTextColor(RGB(255, 255, 255));
  19. //显示最后结果
  20. CString str;
  21. cdc.TextOutW(rect.right / 2 - 100, rect.bottom / 2 - 30, _T("GAME OVER!"));
  22. str.Format(_T("您的得分为:%d"), myScore);
  23. cdc.TextOutW(rect.right / 2 - 100, rect.bottom / 2 - 10, str);
  24. cdc.TextOutW(rect.right / 2 - 100, rect.bottom / 2 +10, _T("COME ON !重新开始?Y/N"));
  25. }

4.10 魔法值控制维护

  1. //控制魔法值
  2. if (nIDEvent == 5) {
  3. if (myplane != NULL&&isPause == 0) {
  4. //防护罩和战机升级没打开,魔法值递增
  5. if (isProtect==FALSE&&isUpdate==FALSE) {
  6. magicCount++;
  7. if (magicCount > 10)
  8. magicCount = 10;
  9. }
  10. //判断是否打开防护罩
  11. if (isProtect == TRUE) {
  12. //开启防护罩魔法值递减
  13. magicCount --;
  14. if (magicCount <= 0) {
  15. magicCount = 0;
  16. isProtect = FALSE;
  17. }
  18. }
  19. //判断是否升级战机
  20. if (isUpdate == TRUE) {
  21. //战机升级,魔法值递减
  22. magicCount--;
  23. if (magicCount <= 0) {
  24. magicCount = 0;
  25. isUpdate = FALSE;
  26. myplane->SetIsUpdate(isUpdate);
  27. }
  28. }
  29. }
  30. }

4.11 得分到达关卡需求,进入Boss

  1. // 进入Boss
  2. int pScore = PASS_SCORE + passNum * 5;
  3. // TODO调试条件
  4. //if (myplane != NULL && passScore >= 3 && isPause == 0&&isBoss==FALSE)
  5. if (myplane != NULL && passScore >= pScore && isPause == 0 && isBoss == FALSE)
  6. {
  7. //进入Boss
  8. isBoss = TRUE;
  9. boss = new CBoss(1, rect.right, rect.bottom);
  10. boss->SetSpeed(BOSS_SPEED+passNum-1);
  11. boss->life = BOSS_LIFE + passNum * 50;//Boss总血量
  12. bossBlood= BOSS_LIFE + passNum * 50;//当前Boss血量
  13. //Boss出场,暂停游戏
  14. bossLoaded = FALSE;
  15. //重新设置Boss的子弹产生频率,增强Boss子弹发射频率
  16. KillTimer(3);
  17. SetTimer(3, 2000 - passNum * 180, NULL);
  18. }
  19. //显示Boss
  20. if (myplane != NULL &&boss!=NULL&& isPause == 0 && isBoss == TRUE) {
  21. BOOL status = boss->Draw(&cdc,passNum, FALSE);
  22. if (status == TRUE)
  23. bossLoaded = TRUE;
  24. }

4.12 检测标记位isPass,判断打赢Boss,进入下一关

  1. if (isPass == 1)
  2. {
  3. isPass = FALSE;
  4. if (passNum ==10)//10关
  5. {
  6. //重新初始化数据
  7. KillTimer(1);
  8. KillTimer(2);
  9. KillTimer(3);
  10. //KillTimer(4);
  11. KillTimer(5);
  12. myplane = new CMyPlane(FALSE);
  13. isPause = TRUE;
  14. isStop = FLAG_RESTART;
  15. //清屏
  16. CBitmap* tCache = cacheBitmap;
  17. cacheBitmap = new CBitmap;
  18. cacheBitmap->CreateCompatibleBitmap(pDC, rect.Width(), rect.Height());
  19. cdc.SelectObject(cacheBitmap);
  20. delete tCache;
  21. }//if
  22. else
  23. {
  24. KillTimer(1);
  25. KillTimer(2);
  26. KillTimer(3);
  27. //KillTimer(4);
  28. KillTimer(5);
  29. isPause = TRUE;
  30. //保存所需数据
  31. int tScore = myScore + passScore;
  32. int tPassNum = passNum + 1;
  33. int tTest = test;
  34. int magic = magicCount;
  35. //重新开始游戏
  36. Restart();
  37. myScore = tScore;
  38. passNum = tPassNum;
  39. magicCount = magic;
  40. test = tTest;
  41. }
  42. }

4.13 按键消息响应

  1. // 方向上键或W键
  2. if (myplane != NULL && (GetKeyState(VK_UP) < 0 || GetKeyState('W') < 0) && isPause == 0)
  3. // 方向下键或S键
  4. if (myplane != NULL && (GetKeyState(VK_DOWN) < 0 || GetKeyState('S') < 0) && isPause == 0)
  5. // 方向左键或A键
  6. if (myplane != NULL && (GetKeyState(VK_LEFT) < 0 || GetKeyState('A') < 0) && isPause == 0 )
  7. // 方向右键或D键
  8. if (myplane != NULL && (GetKeyState(VK_RIGHT) < 0 || GetKeyState('D') < 0) && isPause == 0 )
  9. // 空格键发射子弹
  10. if (myplane != NULL && (GetKeyState(VK_SPACE) < 0) && isPause == 0 && bossLoaded == TRUE) {
  11. //空格键发射子弹
  12. CBomb *Bomb1 = new CBomb(myplane->GetPoint().x+20, myplane->GetPoint().y, 1,isUpdate);
  13. PlaySound((LPCTSTR)IDR_WAV_BOMB, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
  14. bombList.AddTail(Bomb1);
  15. CBomb *Bomb2 = new CBomb(myplane->GetPoint().x + PLANE_WIDTH-50, myplane->GetPoint().y, 1, isUpdate);
  16. bombList.AddTail(Bomb2);
  17. //子弹自动飞行,在Timer中绘制
  18. }
  19. // Z键暂停
  20. if (myplane != NULL&&GetKeyState('Z') < 0)
  21. // X键发大招清屏
  22. if (myplane != NULL&&GetKeyState('X') < 0 && isPause == 0 && bossLoaded == TRUE)
  23. {
  24. //战机发大招
  25. if (magicCount >= 10) {
  26. magicCount -= 10;
  27. //清空敌机
  28. POSITION enemyPos, enemyTemp;
  29. for (enemyPos = enemyList.GetHeadPosition(); (enemyTemp = enemyPos) != NULL;)
  30. {
  31. enemy = (CEnemy *)enemyList.GetNext(enemyPos);
  32. //将爆炸对象添加到爆炸链表中
  33. CExplosion *explosion = new CExplosion((enemy->GetPoint().x + ENEMY_WIDTH / 2), (enemy->GetPoint().y + ENEMY_HEIGHT / 2));
  34. explosionList.AddTail(explosion);
  35. PlaySound((LPCTSTR)IDR_WAV_DAZHAO, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
  36. //删除敌机
  37. enemyList.RemoveAt(enemyTemp);
  38. delete enemy;
  39. //增加得分
  40. passScore++;
  41. }//for
  42. if(isBoss==TRUE) {
  43. //将爆炸对象添加到爆炸链表中
  44. CExplosion *explosion = new CExplosion(boss->GetPoint().x + BOSS_WIDTH / 2, boss->GetPoint().y + BOSS_HEIGHT / 2);
  45. explosionList.AddTail(explosion);
  46. PlaySound((LPCTSTR)IDR_WAV_DAZHAO, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
  47. bossBlood -= 50;
  48. if (bossBlood <= 0) {
  49. //boss死,过关
  50. //过关的标志变量
  51. delete boss;
  52. boss = NULL;
  53. //过关的标志变量
  54. isPause = TRUE;
  55. CMyPlane* temp = myplane;
  56. myplane = new CMyPlane(FALSE);
  57. delete temp;
  58. temp = NULL;
  59. isPass = 1;
  60. isBoss = FALSE;
  61. }
  62. }
  63. //清空敌机炮弹
  64. POSITION ballPos, ballTemp;
  65. for (ballPos = ballList.GetHeadPosition(); (ballTemp = ballPos) != NULL;)
  66. {
  67. ball = (CBall *)ballList.GetNext(ballPos);
  68. //删除敌机炮弹
  69. ballList.RemoveAt(ballTemp);
  70. delete ball;
  71. }//for
  72. }
  73. }//if
  74. // C键开启防护罩
  75. if (myplane != NULL&&GetKeyState('C') < 0 && isPause == 0)
  76. // V键战机升级
  77. if (myplane != NULL&&GetKeyState('V') < 0 && isPause == 0)
  78. // Y键开启无敌模式或响应死亡、重开游戏界面操作:
  79. if (GetKeyState('Y') < 0 )
  80. {
  81. if (isStop == FALSE) {
  82. //无敌模式开关
  83. if (test == FALSE)
  84. test = TRUE;
  85. else test = FALSE;
  86. }
  87. else {
  88. isStop = FALSE;
  89. Restart();
  90. }
  91. }
  92. // N键响应结束、重开游戏界面操作
  93. if (GetKeyState('N') < 0) {
  94. if (isStop!=FALSE) {
  95. MyDialog dialog;
  96. dialog.DoModal();
  97. }
  98. }
  99. // 按空格进入游戏
  100. if (isStarted == FALSE && (GetKeyState(VK_SPACE) < 0)) {
  101. isStarted = TRUE;
  102. }

4.14 鼠标消息响应

  1. // 鼠标移动
  2. void CPlaneWarView::OnMouseMove(UINT nFlags, CPoint point)
  3. {
  4. // TODO: 在此添加消息处理程序代码和/或调用默认值
  5. if (myplane!=NULL && isPause == 0 ) {
  6. //绘制新游戏对象
  7. myplane->SetPoint(point.x,point.y);
  8. }
  9. CView::OnMouseMove(nFlags, point);
  10. }
  11. // 鼠标左键发射子弹和开始界面进入游戏
  12. void CPlaneWarView::OnLButtonDown(UINT nFlags, CPoint point)
  13. {
  14. // TODO: 在此添加消息处理程序代码和/或调用默认值
  15. if (myplane!=NULL&&isPause == 0 && bossLoaded == TRUE)
  16. {
  17. CBomb *Bomb1 = new CBomb(myplane->GetPoint().x, myplane->GetPoint().y, 1, isUpdate);
  18. PlaySound((LPCTSTR)IDR_WAV_BOMB, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
  19. bombList.AddTail(Bomb1);
  20. CBomb *Bomb2 = new CBomb(myplane->GetPoint().x + BOMB_DISTANCE, myplane->GetPoint().y, 1, isUpdate);
  21. bombList.AddTail(Bomb2);
  22. //子弹自动飞行,在Timer中绘制
  23. }
  24. if (isStarted == FALSE) {
  25. isStarted = TRUE;
  26. }
  27. CView::OnLButtonDown(nFlags, point);
  28. }

4.15 生命周期

4.15.1 窗口销毁

  1. void CPlaneWarView::OnDestroy()
  2. {
  3. // 销毁指针资源
  4. if (myplane != NULL)
  5. delete myplane;
  6. if (boss != NULL)
  7. delete boss;
  8. //释放内存资源
  9. scene.ReleaseScene();
  10. CView::OnDestroy();
  11. }

4.15.2 游戏重新开始

  1. void CPlaneWarView::Restart()
  2. {
  3. // TODO: 在此处添加游戏重新开始初始化参数
  4. //战机重新加载
  5. myplane = new CMyPlane(FALSE);
  6. //清空敌机链表
  7. if (enemyList.GetCount() > 0)
  8. enemyList.RemoveAll();
  9. //清空战机链表
  10. if (meList.GetCount() > 0)
  11. meList.RemoveAll();
  12. //清空战机子弹链表
  13. if (bombList.GetCount() > 0)
  14. bombList.RemoveAll();
  15. //清空敌机炸弹链表
  16. if (ballList.GetCount() > 0)
  17. ballList.RemoveAll();
  18. //清空爆炸链表
  19. if (explosionList.GetCount() > 0)
  20. explosionList.RemoveAll();
  21. //清空血包列表
  22. if (bloodList.GetCount() > 0)
  23. bloodList.RemoveAll();
  24. //参数重新初始化
  25. myLife = DEFAULT_LIFE;
  26. lifeNum = DEFAULT_LIFE_COUNT;
  27. myScore = 0;
  28. passScore = 0;
  29. passNum = DEFAULT_PASS;
  30. isPass = 0;
  31. isPause = 0;
  32. lifeCount = 1;
  33. magicCount = 0;
  34. bloodExist = FALSE;
  35. bossBlood = BOSS_LIFE;
  36. isBoss = FALSE;
  37. bossLoaded = TRUE;
  38. isProtect = FALSE;
  39. isUpdate = FALSE;
  40. test = FALSE;
  41. boss = NULL;
  42. SetMyTimer();
  43. }

4.15.3 游戏暂停

  1. void CPlaneWarView::Pause()
  2. {
  3. // TODO: 在此处添加游戏暂停操作
  4. isPause = TRUE;
  5. Sleep(1000);
  6. }

4.15.4 生命值归零,游戏结束

  1. void CPlaneWarView::gameOver(CDC* pDC,CDC& cdc,CBitmap* cacheBitmap)
  2. {
  3. //结束游戏界面
  4. //释放计时器
  5. KillTimer(1);
  6. KillTimer(2);
  7. KillTimer(3);
  8. //KillTimer(4);
  9. KillTimer(5);
  10. //计算最后得分
  11. myScore += passScore;
  12. //播放游戏结束音乐
  13. PlaySound((LPCTSTR)IDR_WAV_GAMEOVER, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
  14. //清屏
  15. CBitmap* tCache = cacheBitmap;
  16. cacheBitmap = new CBitmap;
  17. cacheBitmap->CreateCompatibleBitmap(pDC, rect.Width(), rect.Height());
  18. cdc.SelectObject(cacheBitmap);
  19. //内存中承载临时图像的缓冲位图
  20. delete tCache;
  21. isStop = FLAG_STOP;
  22. }

5 游戏演示

游戏初始界面

游戏画面1

游戏画面2

游戏画面3

游戏结束

6 实训中遇到的主要问题及解决方法

6.1 滚动背景实现问题

要实现滚动背景,需要在背景图上取出客户区矩形大小的区域,并按照Timer进行递进,并且要在背景图边界处衔接好返回背景图开头位置,实现一张背景图的循环反复,达到滚动背景图的目的,刚开始由于不懂得如何实现开头结尾处的衔接问题,导致滚动背景图实现难度大,后来经由网上查阅的资料得知要把背景图加载两份,第二份背景图开头衔接在第一份结尾处,让客户区矩形在该两份背景图上进行滑动,当滑动到第二份背景图时,再把第一份接到第二份结尾处,从而实现循环反复滚动背景。其中实现的难点在于控制好客户区矩形坐标和背景图上要显示的图片块位于图片上的坐标的对应关系。

6.2 背景音乐循环播放和游戏音效同时输出问题

由于平时接触MFC太少,对MFC操作多媒体文件颇为陌生,刚开始使用的PlaySound可以简单的实现音频的输出,但是该函数只支持单个音轨的播放,不支持背景音乐和游戏操作音效的同时播放,后来经过大量的查找资料和代码样例,终于找到了使用mciSendString进行背景音乐的播放,而使用原来的PlaySound播放操作音效,达到了背景音乐和操作音效的同时播放的目的。

6.3 多帧位图素材的动画特效实现问题

飞机大战中的爆炸显示是通过一张带有八个帧的位图进行连续输出实现爆炸特效,刚开始只是简单的在一个while循环中循环八次,结果不尽如人意,后来联想到帧和时间的对应关系,在每一次onTimer调用时输出一帧,并在爆炸对象中用progress标记位记录播放位置,等到八个帧播放结束时,返回播放结束标记位,在onTimer中检测并删除播放完成的该爆炸对象。

6.4 游戏结束和游戏重新开始的游戏资源维护问题

该游戏由于实现了多个额外功能,且都是带有全局意义的,因此放置了较多标记位来标记每个状态,通过这些标记位来控制游戏进程中的各个状态,因此在游戏结束和重新开始游戏时,各个标记位的正确重置将会决定重新开始游戏之后的游戏状态。还由于这些操作可能会在onTimer函数调用过程中进行中断,这和onTimer中使用的双缓冲的pDC和cdc的申请和释放息息相关,中断操作必须对pDC和cdc进行正确的处理才会防止游戏过程中的意外崩溃出现。

7 实训体会

由于对MFC接触不多,那次本次实训开发飞机大战相当于是零基础,因此在开发过程中走了很多弯路,但是也是这次的实训,让我了解了MFC的事件处理机制,了解了如何在屏幕中绘制需要向用户展示的图形信息,学会了使用基本的操作对屏幕中绘制的图形进行控制,也了解了Windows的屏幕定时刷新的机制,学习了微软封装好的一些C++的类库,学会遇到问题到MSDN中进行API查询,并对VS的使用和编程也进行了提高。

在开发过程中遇到了大量的异常和中断,通过此次开发,也学会了遇到错误如何慢慢通过IDE的错误信息进行错误查找和代码排错更正,通过添加断点调试程序一步步监视程序运行的具体过程,从而找到程序运行出错的原因。

通过此次的实训,对MFC入了门,真正实现了图形界面的底层操作,消除了以前对于MFC的一些恐惧心理,让我也对MFC产生了兴趣,在之后的学习中,我会多找机会使用MFC进行一些开发,做一些实用的程序出来。

上传的附件 cloud_download 基于VC++实现的飞机大战游戏.7z ( 21.13mb, 207次下载 )
error_outline 下载需要11点积分

发送私信

人生百年,转眼成空。幸福靠的不是缘分,而是珍惜

7
文章数
8
评论数
最近文章
eject