基于WIN32 API实现的超级玛丽游戏

YoungTime

发布日期: 2018-10-04 21:31:36 浏览量: 610
评分:
star star star star star star star star star star_border
*转载请注明来自write-bug.com

游戏截图

游戏中用到的类结构介绍

图像层

  • 图像基类MYBITMAP

  • 游戏背景MYBKSKY—>MYBITMAP

  • 游戏图片MYANIOBJ—>MYBITMAP

  • 魔法攻击MYANIMAGIC—>MYBITMAP

逻辑层

  • 游戏逻辑GAMEMAP

  • 时钟处理MYCLOCK

  • 字体处理MYFONT

  • 跟踪打印FILEREPORT

  • 玩家控制MYROLE—>MYBITMAP

结构和表

  • 精灵结构ROLE

  • 物品结构MapObject

  • 地图信息表MAPINFO

一、工程开始

介绍下准备工作,也就是所需要的开发工具。代码编写调试:VC 6.0,美术工具:Windows自带的画图(开始-程序-附件-画图)。这是最简陋的开发工具,但已足够。最好再有Photoshop,记事本或UltraEdit等等你喜欢的文本编辑工具。

游戏代码分两部分,图像部分和逻辑部分。

先说图像部分:图像分两种,矩形图片和不规则图片。工程中的PIC文件夹下,可以看到所有图像资源。

矩形图片有:地面、砖块、水管、血条、血条背景。

不规则图片有:蘑菇(玩家,敌人1,敌人2),子弹、旋风、爆炸效果、金币、撞击金币后的得分、攻击武器(那个从魂斗罗里抠来的东东)、火圈1、火圈2、箭头(用于开始菜单选择)、树木、河流、WIN标志、背景图片(游戏背景和菜单背景)。

所有图片都分成几个位图BMP文件存储。一个文件中,每种图片,都纵向排列。每种图片可能有多帧。比如,金币需要4帧图像,才能构成一个旋转的动画效果,那么,各帧图像横向排列。

图像层的结构就这样简单,逻辑层只需要确定“哪个图像,哪一帧”这两个参数,就能在屏幕上绘制出所有图片。

图像层的基类是:

  1. class MYBITMAP
  2. {
  3. public:
  4. MYBITMAP();
  5. ~MYBITMAP();
  6. // 初始化
  7. void Init(HINSTANCE hInstance,int iResource,int row,int col);
  8. void SetDevice(HDC hdest,HDC hsrc,int wwin,int hwin);
  9. void SetPos(int istyle,int x,int y);
  10. // 显示
  11. void Draw(DWORD dwRop);
  12. void Stretch(int x,int y);
  13. void Stretch(int x,int y,int id);
  14. void Show(int x,int y);
  15. void ShowCenter(int y);
  16. void ShowLoop(int left,int top,int right,int bottom,int iframe);
  17. void ShowNoBack(int x,int y,int iFrame);
  18. void ShowNoBackLoop(int x,int y,int iFrame,int iNum);
  19. // 动画播放
  20. void ShowAni();
  21. void SetAni(int x,int y);
  22. HBITMAP hBm;
  23. public:
  24. // 按照行列平均分成几个
  25. int inum;
  26. int jnum;
  27. int width;
  28. int height;
  29. int screenwidth;
  30. int screenheight;
  31. HDC hdcdest;
  32. HDC hdcsrc;
  33. // 当前位置
  34. int xpos;
  35. int ypos;
  36. int iStartAni;
  37. };

这只是一个基类,上面是几个重要的数据成员和函数。它所描述的图片,是一个m行n列构成的m*n个图片,每个图片大小一致,都是矩形。显然,这并不能满足上面的设计要求,怎么解决呢?派生,提供更多的功能。但是,这个基类封装了足够的物理层信息:设备上下文HDC,和位图句柄HBITMAP。矩形图片的显示、不规则图片的显示、图片组织排列信息,这些功能交给它的派生类MYANIOBJ。

还有,我们最关心的问题是图片坐标,比如,不同位置的砖块、精灵、金币,这些由逻辑层处理。

二、图片基类MYBITMAP

先说一下代码风格,大家都说看不懂,这就对了。整套代码约有3000行,并不都是针对这个游戏写的。我想把代码写成一个容易扩展、容易维护、功能全面的“框架”,需要什么功能,就从这个框架中取出相应功能,如果是一个新的功能,比如新的图像显示、新的运动控制,我也能方便地实现。所以,这个游戏的代码,是在前几个游戏的基础上扩充起来的。部分函数,部分变量在这款游戏中,根本不用,但要保留,要为下一款游戏作准备。只要理解了各个类,就理解了整个框架。

今天先讲最基础的图像类MYBITMAP,先说一下代码风格,大家都说看不懂,这就对了。整套代码约有3000行,并不都是针对这个游戏写的。我想把代码写成一个容易扩展、容易维护、功能全面的“框架”,需要什么功能,就从这个框架中取出相应功能,如果是一个新的功能,比如新的图像显示、新的运动控制,我也能方便地实现。所以,这个游戏的代码,是在前几个游戏的基础上扩充起来的。部分函数,部分变量在这款游戏中,根本不用,但要保留,要为下一款游戏作准备。只要理解了各个类,就理解了整个框架。

今天先讲最基础的图像类MYBITMAP,成员函数功能列表:

  1. // 功能 根据一个位图文件,初始化图像
  2. // 入参 应用程序实例句柄 资源ID 横向位图个数 纵向位图个数
  3. void Init(HINSTANCE hInstance,int iResource,int row,int col);
  4. // 功能 设置环境信息
  5. // 入参 目的DC(要绘制图像的DC),临时DC,要绘制区域的宽 高
  6. void SetDevice(HDC hdest,HDC hsrc,int wwin,int hwin);
  7. // 功能 设置图片位置
  8. // 入参 设置方法 横纵坐标
  9. void SetPos(int istyle,int x,int y);
  10. // 功能 图片显示
  11. // 入参 图片显示方式
  12. void Draw(DWORD dwRop);
  13. // 功能 图片缩放显示
  14. // 入参 横纵方向缩放比例
  15. void Stretch(int x,int y);
  16. // 功能 图片缩放显示
  17. // 入参 横纵方向缩放比例 缩放图像ID(纵向第几个)
  18. void Stretch(int x,int y,int id);
  19. // 功能 在指定位置显示图片
  20. // 入参 横纵坐标
  21. void Show(int x,int y);
  22. // 功能 横向居中显示图片
  23. // 入参 纵坐标
  24. void ShowCenter(int y);
  25. // 功能 将某个图片平铺在一个区域内
  26. // 入参 左上右下边界的坐标 图片ID(横向第几个)
  27. void ShowLoop(int left,int top,int right,int bottom,int iframe);
  28. // 功能 不规则图片显示
  29. // 入参 横纵坐标 图片ID(横向第几个)
  30. void ShowNoBack(int x,int y,int iFrame);
  31. // 功能 不规则图片横向平铺
  32. // 入参 横纵坐标 图片ID(横向第几个) 平铺个数
  33. void ShowNoBackLoop(int x,int y,int iFrame,int iNum);
  34. // 动画播放
  35. // 功能 自动播放该图片的所有帧,函数没有实现,但以后肯定要用:)
  36. // 入参 无
  37. void ShowAni();
  38. // 功能 设置动画坐标
  39. // 入参 横纵坐标
  40. void SetAni(int x,int y);

成员数据:

  1. // 图像句柄
  2. HBITMAP hBm;
  3. // 按照行列平均分成几个
  4. int inum;
  5. int jnum;
  6. // 按行列分割后,每个图片的宽高(显然各个图片大小一致,派生后,这里的宽高已没有使用意义)
  7. int width;
  8. int height;
  9. // 屏幕宽高
  10. int screenwidth;
  11. int screenheight;
  12. // 要绘制图片的dc
  13. HDC hdcdest;
  14. // 用来选择图片的临时dc
  15. HDC hdcsrc;
  16. // 当前位置
  17. int xpos;
  18. int ypos;
  19. // 是否处于动画播放中(功能没有实现)
  20. int iStartAni;

这个基类的部分函数和变量,在这个游戏中没有使用,是从前几个游戏中保留下来的,所以看起来有些零乱。这个游戏的主要图像功能,由它的派生类完成。由于基类封装了物理层信息(dc和句柄),派生类的编写就容易一些,可以让我专注于逻辑含义。

基类的函数实现上,很简单,主要是以下几点:

1.图片初始化

  1. // 根据程序实例句柄,位图文件的资源ID,导入该位图,得到位图句柄
  2. hBm=LoadBitmap(hInstance,MAKEINTRESOURCE(iResource));
  3. // 获取该位图文件的相关信息
  4. GetObject(hBm,sizeof(BITMAP),&bm);
  5. // 根据横纵方向的图片个数,计算出每个图片的宽高(对于超级玛丽,宽高信息由派生类处理)
  6. width=bm.bmWidth/inum;
  7. height=bm.bmHeight/jnum;

2.图片显示

各个图片的显示函数,大同小异,都要先选入一个临时DC,再bitblt到要绘制的dc上。矩形图片,可以直接用SRCCOPY的方式绘制;不规则图片,需要先用黑白图与目的区域相”与”(SRCAND),再用”或”的方法显示图像(SRCPAINT),这是一种简单的”绘制透明位图”的方法。

  1. void MYBITMAP::ShowNoBack(int x,int y,int iFrame)
  2. {
  3. xpos=x;
  4. ypos=y;
  5. SelectObject(hdcsrc,hBm);
  6. BitBlt(hdcdest,xpos,ypos,width,height/2,hdcsrc,iFrame*width,height/2,SRCAND);
  7. BitBlt(hdcdest,xpos,ypos,width,height/2,hdcsrc,iFrame*width,0,SRCPAINT);
  8. }

3.图片缩放

用StretchBlt的方法实现。

  1. void MYBITMAP::Stretch(int x,int y,int id)
  2. {
  3. SelectObject(hdcsrc,hBm);
  4. StretchBlt(hdcdest,xpos,ypos,width*x,height*y,
  5. hdcsrc,0,id*height,
  6. width,height,
  7. SRCCOPY);
  8. }

在超级玛丽这个游戏中,哪些图像的处理是通关这个基类呢?只有一个:MYBITMAP bmPre;由于这个基类只能处理几个大小均等的图片,只有这些图片大小一致,且都是矩形:游戏开始前的菜单背景,操作信息的背景,每一关开始前的背景(此时显示LIFE x WORLD x),通关或游戏结束时显示的图片,共5个,将这5个图片,放在一个位图文件中,于是,这些图片的操作就做完了,代码如下:

  1. // 初始设置,在InitInstance函数中
  2. bmPre.Init(hInstance,IDB_BITMAP_PRE1,1,5);
  3. bmPre.SetDevice(hscreen,hmem,GAMEW*32,GAMEH*32);
  4. bmPre.SetPos(BM_USER,0,0);
  5. // 图片绘制,在WndProc中,前两个参数指横纵方向扩大2倍显示.
  6. bmPre.Stretch(2,2,0);
  7. bmPre.Stretch(2,2,4);
  8. bmPre.Stretch(2,2,2);
  9. bmPre.Stretch(2,2,1);
  10. bmPre.Stretch(2,2,3);

三、游戏背景类MYBKSKY

类说明

这是一个专门处理游戏背景的类。在横版游戏或射击游戏中,都有一个背景画面,如山、天空、云、星空等等。这些图片一般只有1到2倍屏幕宽度,然后像一个卷轴一样循环移动,连成一片,感觉上像一张很长的图片。这个类就是专门处理这个背景的。在超级玛丽增强版中,主要关卡是3关,各有一张背景图片;从水管进去,有两关,都用一张全黑图片。共四张图。这四张图大小一致,纵向排列在一个位图文件中。MYBKSKY这个类,派生于MYBITMAP。由于背景图片只需要完成循环移动的效果,只需要实现一个功能,而无需关心其他任何问题(例如句柄、dc)。编码起来很简单,再次反映出面向对象的好处。

技术原理

怎样让一张图片像卷轴一样不停移动呢?很简单,假设有一条垂直分割线,把图片分成左右两部分。先显示右边部分,再把左边部分接到图片末尾。不停移动向右移动分割线,图片就会循环地显示。

MYBKSKY类定义如下所示:

  1. class MYBKSKY:public MYBITMAP
  2. {
  3. public:
  4. MYBKSKY();
  5. ~MYBKSKY();
  6. // show
  7. // 功能 显示一个背景.
  8. // 入参 无
  9. void DrawRoll(); // 循环补空
  10. // 功能 显示一个背景,并缩放图片
  11. // 入参 横纵方向缩放比例
  12. void DrawRollStretch(int x,int y);
  13. // 功能 指定显示某一个背景,并缩放图片,游戏中用的就是这个函数
  14. // 入参 横纵方向缩放比例 背景图片ID(纵向第几个)
  15. void DrawRollStretch(int x,int y,int id);
  16. // 功能 设置图片位置
  17. // 入参 新的横纵坐标
  18. void MoveTo(int x,int y);
  19. // 功能 循环移动分割线
  20. // 入参 分割线移动的距离
  21. void MoveRoll(int x);
  22. // data
  23. // 分割线横坐标
  24. int xseparate;
  25. };

函数具体实现都很简单,例如:

  1. void MYBKSKY::DrawRollStretch(int x,int y, int id)
  2. {
  3. // 选入句柄
  4. SelectObject(hdcsrc,hBm);
  5. // 将分割线右边部分显示在当前位置
  6. StretchBlt(hdcdest,
  7. xpos,ypos, // 当前位置
  8. (width-xseparate)*x,height*y, // 缩放比例
  9. hdcsrc,
  10. xseparate,id*height, // 右边部分的坐标
  11. width-xseparate,height, // 右边部分的宽高
  12. SRCCOPY);
  13. // 将分割线左边部分接在图片末尾
  14. StretchBlt(hdcdest,xpos+(width-xseparate)*x,ypos,
  15. xseparate*x,height*y,
  16. hdcsrc,0,id*height,
  17. xseparate,height,
  18. SRCCOPY);
  19. }

使用举例:

  1. // 定义
  2. MYBKSKY bmSky;
  3. // 初始化
  4. bmSky.Init(hInstance,IDB_BITMAP_MAP_SKY,1,4);
  5. bmSky.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);
  6. bmSky.SetPos(BM_USER,0,0);
  7. // 游戏过程中显示
  8. bmSky.DrawRollStretch(2,2,gamemap.mapinfo.iBackBmp);
  9. // 每隔一定时间,移动分割线
  10. bmSky.MoveRoll(SKY_SPEED);//云彩移动
  11. // 以下两处与玩家角色有关:
  12. // 当玩家切换到一张新地图时,刷新背景图片的坐标
  13. bmSky.SetPos(BM_USER,viewx,0);
  14. // 当玩家向右移动时,刷新背景图片的坐标
  15. bmSky.SetPos(BM_USER,viewx,0);

至此,游戏背景图片的功能就做完了。

四、图片显示类MYANIOBJ

类说明

这个类负责游戏中的图片显示。菜单背景、通关和游戏结束的提示图片,由MYBITMAP处理(大小一致的静态图片)。游戏背景由MYBKSKY处理。其余图片,也就是游戏过程中的所有图片,都是MYANIOBJ处理。

技术原理

游戏中的图片大小不一致,具体在超级玛丽中,可以分成两类:矩形图片和不规则图片。在位图文件中,都是纵向排列各个图片,横向排列各帧。用两个数组存储各个图片的宽和高。为了方便显示某一个图片,用一个数组存储各个图片的纵坐标(即位图文件中左上角的位置)。使用时,由逻辑层指定“哪个图片”的“哪一帧”,显示在“什么位置”。这样图片的显示功能就实现了。

MYANIOBJ类定义如下所示:

  1. class MYANIOBJ:public MYBITMAP
  2. {
  3. public:
  4. MYANIOBJ();
  5. ~MYANIOBJ();
  6. // init list
  7. // 功能 初始化宽度数组 高度数组 纵坐标数组 是否有黑白图
  8. // 入参 宽度数组地址 高度数组地址 图片数量 是否有黑白图(0 没有, 1 有)
  9. // (图片纵坐标信息由函数计算得出)
  10. void InitAniList(int *pw,int *ph,int inum,int ismask);
  11. // 功能 初始化一些特殊的位图,例如各图片大小一致,或者有其他规律
  12. // 入参 初始化方式 参数1 参数2
  13. // (留作以后扩展, 目的是为了省去宽高数组的麻烦)
  14. void InitAniList(int style,int a,int b);
  15. // show
  16. // 功能 显示图片(不规则图片)
  17. // 入参 横纵坐标(要显示的位置) 图片id(纵向第几个), 图片帧(横向第几个)
  18. void DrawItem(int x,int y,int id,int iframe);
  19. // 功能 显示图片(矩形图片)
  20. // 入参 横纵坐标(要显示的位置) 图片id(纵向第几个), 图片帧(横向第几个)
  21. void DrawItemNoMask(int x,int y,int id,int iframe);
  22. // 功能 指定宽度, 显示图片的一部分(矩形图片)
  23. // 入参 横纵坐标(要显示的位置) 图片id(纵向第几个), 显示宽度 图片帧(横向第几个)
  24. void DrawItemNoMaskWidth(int x,int y,int id,int w,int iframe);
  25. // 功能 播放一个动画 即循环显示各帧
  26. // 入参 横纵坐标(要显示的位置) 图片id(纵向第几个)
  27. void PlayItem(int x,int y,int id);
  28. // 宽度数组 最多支持20个图片
  29. int wlist[20];
  30. // 高度数组 最多支持20个图片
  31. int hlist[20];
  32. // 纵坐标数组 最多支持20个图片
  33. int ylist[20];
  34. // 动画播放时的当前帧
  35. int iframeplay;
  36. };

函数实现上也很简单。构造函数中,所有成员数据清零;初始化时,将各图片的高度累加,即得到各图片的纵坐标。显示图片的方法如前所述。

使用举例:

游戏图片分成三类:地图物品、地图背景物体、精灵(即所有不规则图片)。

  1. MYANIOBJ bmMap;
  2. MYANIOBJ bmMapBkObj;
  3. MYANIOBJ bmAniObj;

初始化宽高信息,程序中定义一个二维数组,例如:

  1. int mapani[2][10]={
  2. {32,32,64,32,32,52,64,32,64,32},
  3. {32,32,64,32,32,25,64,32,64,32},
  4. };

第一维mapani[0]存储10个图片的宽度,第二维mapani[1]存储10个图片的高度,初始化时,将mapani[0],mapani[1]传给初始化函数即可。

1.地图物品的显示

  1. // 定义
  2. MYANIOBJ bmMap;
  3. // 初始化
  4. // 这一步加载位图
  5. bmMap.Init(hInstance,IDB_BITMAP_MAP,1,1);
  6. // 这一步初始化DC
  7. bmMap.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);
  8. // 这一步设置宽高信息, 图片为矩形
  9. bmMap.InitAniList(mapsolid[0],mapsolid[1], sizeof(mapsolid[0])/sizeof(int),0);
  10. // 对象作为参数传给逻辑层, 显示地图物品
  11. gamemap.Show(bmMap);

2.血条的显示

打怪时,屏幕上方要显示血条。由于同样是矩形图片,也一并放在了地图物品的位图中。

  1. // 变量声明
  2. extern MYANIOBJ bmMap;
  3. // 显示血条背景,指定图片宽度:最大生命值*单位生命值对应血条宽度
  4. bmMap.DrawItemNoMaskWidth(xstart-1, ATTACK_TO_Y-1, ID_MAP_HEALTH_BK, iAttackMaxLife*BMP_WIDTH_HEALTH, 0);
  5. // 显示怪物血条,指定图片宽度:当前生命值*单位生命值对应血条宽度
  6. bmMap.DrawItemNoMaskWidth(xstart, ATTACK_TO_Y, ID_MAP_HEALTH, iAttackLife*BMP_WIDTH_HEALTH, 0);

3.地图背景物体的显示

背景物体包括草、河流、树木、目的地标志。这些物体都不参与任何逻辑处理,只需要显示到屏幕上。图片放在一个位图文件中,都是不规则形状。

  1. // 定义
  2. MYANIOBJ bmMapBkObj;
  3. // 初始化并加载位图
  4. bmMapBkObj.Init(hInstance,IDB_BITMAP_MAP_BK,1,1);
  5. // 设置dc
  6. bmMapBkObj.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);
  7. // 设置各图片宽高信息
  8. bmMapBkObj.InitAniList(mapanibk[0],mapanibk[1],sizeof(mapanibk[0])/sizeof(int),1);
  9. // 对象作为参数传给逻辑层, 显示地图背景物体
  10. gamemap.ShowBkObj(bmMapBkObj);

4.精灵的显示

精灵包括:蘑菇(玩家,敌人1,敌人2),子弹、旋风、爆炸效果、金币、撞击金币后的得分、攻击武器(那个从魂斗罗里抠来的东东)、火圈1、火圈2、箭头(用于开始菜单选择)。

  1. // 定义
  2. MYANIOBJ bmAniObj;
  3. // 初始化加载位图
  4. bmAniObj.Init(hInstance,IDB_BITMAP_ANI,1,1);
  5. // 设置dc
  6. bmAniObj.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);
  7. // 设置宽高信息
  8. bmAniObj.InitAniList(mapani[0],mapani[1],sizeof(mapani[0])/sizeof(int),1);
  9. // 菜单显示(即菜单文字左边的箭头)
  10. gamemap.ShowMenu(bmAniObj);
  11. // 对象作为参数传给逻辑层, 显示各个精灵
  12. gamemap.ShowAniObj(bmAniObj);

五、魔法攻击类MYANIMAGIC

类说明:玩家有两种攻击方式:普通攻击(子弹),魔法攻击(旋风)。这个类是专门处理旋风的。我最初的想法是用一些特殊的bitblt方法制造特效,例如或、与、异或。试了几次,都失败了。最后只能用“先与后或”的老方法。这个类可看成MYANIOBJ的一个简化版,只支持不规则图片的显示。

MYANIMAGIC类定义如下所示:

  1. class MYANIMAGIC:public MYBITMAP
  2. {
  3. public:
  4. MYANIMAGIC();
  5. ~MYANIMAGIC();
  6. // init list
  7. // 功能 初始化宽度数组 高度数组 纵坐标数组(必须有黑白图)
  8. // 入参 宽度数组地址 高度数组地址 图片数量
  9. // (图片纵坐标信息由函数计算得出)
  10. void InitAniList(int *pw,int *ph,int inum);
  11. // 功能 设置dc
  12. // 入参 显示dc 临时dc(用于图片句柄选择) 临时dc(用于特效实现)
  13. void SetDevice(HDC hdest,HDC hsrc,HDC htemp);
  14. // show
  15. // 功能 显示某个图片的某帧
  16. // 入参 横纵坐标(显示位置) 图片id(纵向第几个) 帧(横向第几个)
  17. void DrawItem(int x,int y,int id,int iframe);
  18. // 宽度数组
  19. int wlist[20];
  20. // 高度数组
  21. int hlist[20];
  22. // 纵坐标数组
  23. int ylist[20];
  24. // 用于特效的临时dc, 功能没有实现
  25. HDC hdctemp;
  26. };

函数具体实现很简单,可参照MYANIOBJ类。

使用举例

  1. // 定义
  2. MYANIMAGIC bmMagic;
  3. // 初始化加载位图
  4. bmMagic.Init(hInstance,IDB_BITMAP_MAGIC,1,1);
  5. // 设置dc
  6. bmMagic.SetDevice(hscreen,hmem, hmem2);
  7. // 初始化宽高信息
  8. bmMagic.InitAniList(mapanimagic[0],mapanimagic[1],sizeof(mapanimagic[0])/sizeof(int));
  9. // 变量声明
  10. extern MYANIMAGIC bmMagic;
  11. // 在逻辑层中, 显示旋风图片
  12. bmMagic.DrawItem(xstart,ystart, 0, FireArray[i].iframe);

六、时钟控制类MYCLOCK

类说明

时间就是生命。这对于游戏来说,最为准确。游戏程序只做两件事:显示图片、处理逻辑。更准确的说法是:每隔一段时间显示图片并处理逻辑。程序中,要设置一个定时器。这个定时器会每隔一段时间发出一个WM_TIMER消息。在该消息的处理中,先逻辑处理。逻辑处理完毕,通过InvalidateRect函数发出WM_PAINT消息,显示各种图片。游戏就不停地运行下去,直至程序结束。

时间表示

用一个整数iNum表示当前时间,游戏中的时间是1,2,3, … , n, 1,2,3, …,n 不停循环.假设1秒内需要25个WM_TIMER消息(每40毫秒1次),则n=25。也可以用一个变量,统计过了几秒。

控制事件频率的方法

  • 一秒内发生多次

    以游戏背景图片为例, 每秒移动5下, 可以在iNum为5,10,15,20,25这5个时间点上移动.即iNum可以被5整除时,修改背景图片的坐标.

  • 一秒内发生一次

    例如火圈, 每秒产生一个新的蘑菇兵. 可以随便指定一个时间点,如20. 当iNum等于20时,生成一个蘑菇兵。

  • 多秒内发生一次

    需要一个辅助变量iNumShow,统计时间过了几秒。每隔一秒iNumShow减1,当iNumShow等于0时处理逻辑。

MYCLOCK类定义如下所示:(所有函数都是内联函数)

  1. class MYCLOCK
  2. {
  3. public:
  4. // 构造函数 初始化所有变量
  5. MYCLOCK()
  6. {
  7. iNum=0; // 时间点
  8. iIsActive=0; // 是否已经开始计时
  9. iNumShow=0; // 计时秒数
  10. iElapse=100; // 默认每100ms发一个WM_TIMER消息
  11. ishow=0; // 是否显示时间
  12. }
  13. // 析构函数 销毁计时器
  14. ~MYCLOCK()
  15. {
  16. Destroy();
  17. }
  18. // 功能 开始计时, 产生WM_TIEMR消息的时间间隔为elapse.
  19. // 设置计时秒数(timetotal).
  20. // 入参 窗口句柄 时间间隔 计时秒数
  21. void Begin(HWND hw,int elapse,int timetotal)
  22. {
  23. if(iIsActive)
  24. return;//已经启动了,直接返回
  25. hWnd=hw;
  26. iElapse=elapse;
  27. SetTimer(hWnd,1,iElapse,NULL);
  28. iNum=1000/iElapse;//一秒钟的时间消息数量
  29. iNumShow=timetotal;
  30. iIsActive=1;
  31. }
  32. // 功能 销毁计时器.
  33. // 入参 无
  34. void Destroy()
  35. {
  36. if(iIsActive)
  37. {
  38. iIsActive=0;
  39. KillTimer(hWnd,1);
  40. }
  41. }
  42. // 功能 重置计时秒数
  43. // 入参 秒数
  44. void ReStart(int timetotal)
  45. {
  46. iNumShow=timetotal;
  47. iNum=1000/iElapse;
  48. ishow=1;
  49. }
  50. //////////////////////////// 显示部分
  51. // 功能 设置显示dc (在超级玛丽增强版中不显示时间)
  52. // 入参 显示dc
  53. void SetDevice(HDC h)
  54. {
  55. hDC=h;
  56. }
  57. // 功能 显示时间, TIME 秒数
  58. // 入参 显示坐标
  59. void Show(int x,int y)
  60. {
  61. char temp[20]={0};
  62. if(!ishow)
  63. return;
  64. // 设置显示文本
  65. sprintf(temp,"TIME: %d ",iNumShow);
  66. TextOut(hDC,x, y, temp,strlen(temp));
  67. }
  68. // 功能 时间点减一
  69. // 如果到了计时秒数, 函数返回1, 否则返回0.
  70. // 入参 无
  71. int DecCount()
  72. {
  73. iNum--;
  74. if(iNum==0)
  75. {
  76. // 过了一秒
  77. iNum=1000/iElapse;
  78. iNumShow--;
  79. if(iNumShow<=0)
  80. {
  81. // 不销毁计时器
  82. return 1;
  83. }
  84. }
  85. return 0;
  86. }
  87. // 功能 时间点减一
  88. // 如果到了计时秒数, 函数返回1并销毁计时器, 否则返回0.
  89. // 入参 无
  90. int Dec()
  91. {
  92. iNum--;
  93. if(iNum<=0)
  94. {
  95. //过了一秒
  96. iNum=1000/iElapse;
  97. iNumShow--;
  98. if(iNumShow<=0)
  99. {
  100. iNumShow=0;
  101. Destroy();
  102. return 1;
  103. }
  104. }
  105. return 0;
  106. }
  107. // 功能 设置是否显示
  108. // 入参 1,显示; 0, 不显示
  109. void SetShow(int i)
  110. {
  111. ishow=i;
  112. }
  113. public:
  114. // 窗口句柄
  115. HWND hWnd;
  116. // 显示dc
  117. HDC hDC;
  118. // 时间点
  119. int iNum;
  120. // 计时秒数
  121. int iNumShow;
  122. // 消息时间间隔
  123. int iElapse;
  124. // 是否开始计时
  125. int iIsActive;
  126. // 是否显示
  127. int ishow;
  128. };

具体函数实现很简单,如上所述。

使用举例

  1. // 定义
  2. MYCLOCK c1;
  3. // 设置显示dc
  4. c1.SetDevice(hscreen);
  5. // 开始计时(计时秒数无效)
  6. c1.Begin(hWnd, GAME_TIME_CLIP ,-1);
  7. // 选择游戏菜单,每隔一定时间,重绘屏幕,实现箭头闪烁
  8. c1.DecCount();
  9. if(0 == c1.iNum%MENU_ARROW_TIME)
  10. // 屏幕提示LIFE,WORLD,如果达到计时秒数,进入游戏。
  11. if(c1.DecCount())
  12. // 进入游戏,计时300秒(无意义,在超级玛丽增强版中取消时间限制)
  13. c1.ReStart(TIME_GAME_IN);
  14. // 在游戏过程中,每隔一定时间,处理游戏逻辑
  15. c1.DecCount();
  16. if(0 == c1.iNum%SKY_TIME)
  17. gamemap.ChangeFrame(c1.iNum); // 帧控制
  18. gamemap.CheckAni(c1.iNum); // 逻辑数据检测
  19. // 玩家过关后,等待一定时间。
  20. if(c1.DecCount())
  21. // 玩家进入水管,等待一定时间。
  22. if(c1.DecCount())
  23. c1.ReStart(TIME_GAME_IN);
  24. // 玩家失败后,等待一定时间。
  25. if(c1.DecCount())
  26. // 玩家通关后,等待一定时间。
  27. if(c1.DecCount())
  28. // 玩家生命值为0,游戏结束,等待一定时间。
  29. if(c1.DecCount())
  30. // 程序结束(窗口关闭),销毁计时器
  31. c1.Destroy();
  32. // 变量声明
  33. extern MYCLOCK c1;
  34. // 游戏菜单中,选择“开始游戏”,显示LIFE,WORLD提示,计时两秒
  35. c1.ReStart(TIME_GAME_IN_PRE); // 停顿两秒
  36. // 进入水管,等待,计时两秒
  37. c1.ReStart(TIME_GAME_PUMP_WAIT);
  38. // 玩家过关,等待,计时两秒
  39. c1.ReStart(TIME_GAME_WIN_WAIT);
  40. // 生命值为0,游戏结束,等待,计时三秒
  41. c1.ReStart(TIME_GAME_END);
  42. // 玩家失败,显示LIFE,WORLD提示,计时两秒
  43. c1.ReStart(TIME_GAME_IN_PRE);
  44. // 玩家失败,等待,计时两秒
  45. c1.ReStart(TIME_GAME_FAIL_WAIT);

至此,所有的时间消息控制、时间计时都已处理完毕。

七、字体管理类MYFONT

类说明

游戏当然少不了文字。在超级玛丽中,文字内容是比较少的,分两类:游戏菜单中的文字,游戏过程中的文字。菜单中的文字包括:

  • “操作: Z:子弹 X:跳 方向键移动 W:默认窗口大小”,
  • “地图文件错误,请修正错误后重新启动程序。”,
  • “(上下键选择菜单,回车键确认)”,
  • “开始游戏”,
  • “操作说明”,
  • “(回车键返回主菜单)”

这几个字符串存储在一个指针数组中(全局变量),通关数组下标使用各个字符串。

游戏中的文字只有两个:’LIFE’,’WORLD’。

其他的文字其实都是位图,例如“通关”、“gameover”以及碰到金币后的“+10”。这些都是位图图片,在pic文件夹里一看便知。

技术原理

要在屏幕上显示一个字符串,分以下几步:将字体句柄选入dc,设置文字背景色,设置文字颜色,最后用TextOut完成显示。这个类就是将整个过程封装了一下。显示dc,背景色,文字颜色,字体句柄都对应各个成员数据。函数具体实现很简单,一看便知。

MYFONT类定义如下所示:

  1. class MYFONT
  2. {
  3. public:
  4. // 构造函数,初始化”字体表”,即5个字体句柄构成的数组,字体大小依次递增.
  5. MYFONT();
  6. ~MYFONT();
  7. // 功能 设置显示文字的dc
  8. // 入参 显示文字的dc句柄
  9. void SetDevice(HDC h);
  10. // 功能 设置当前显示的字体
  11. // 入参 字体表下标
  12. void SelectFont(int i);
  13. // 功能 设置当前字体为默认字体
  14. // 入参 无
  15. void SelectOldFont();
  16. // 功能 在指定坐标显示字符串
  17. // 入参 横纵坐标 字符串指针
  18. void ShowText(int x,int y,char *p);
  19. // 功能 设置文字背景颜色,文字颜色
  20. // 入参 文字背景颜色 文字颜色
  21. void SetColor(COLORREF cbk, COLORREF ctext);
  22. // 功能 设置文字背景颜色,文字颜色
  23. // 入参 文字背景颜色 文字颜色
  24. void SelectColor(COLORREF cbk, COLORREF ctext);
  25. // 显示文字的dc
  26. HDC hdc;
  27. // 字体表,包含5个字体句柄,字体大小依次是0,10,20,30,40
  28. HFONT hf[5];
  29. // 默认字体
  30. HFONT oldhf;
  31. // color
  32. COLORREF c1; // 字体背景色
  33. COLORREF c2; // 字体颜色
  34. };

使用举例

  1. // 定义
  2. MYFONT myfont;
  3. // 初始化设置显示dc
  4. myfont.SetDevice(hscreen);
  5. // 地图文件错误:设置颜色,设置字体,显示提示文字
  6. myfont.SelectColor(TC_WHITE,TC_BLACK);
  7. myfont.SelectFont(0);
  8. myfont.ShowText(150,290,pPreText[3]);
  9. // 游戏开始菜单:设置字体,设置颜色,显示三行菜单文字
  10. myfont.SelectFont(0);
  11. myfont.SelectColor(TC_BLACK, TC_YELLOW_0);
  12. myfont.ShowText(150,260,pPreText[4]);
  13. myfont.ShowText(150,290,pPreText[5]);
  14. myfont.ShowText(150,320,pPreText[6]);
  15. // 游戏操作说明菜单:设置字体,设置颜色,显示四行说明文字
  16. myfont.SelectFont(0);
  17. myfont.SelectColor(TC_BLACK, TC_YELLOW_0);
  18. myfont.ShowText(150,230,pPreText[8]);
  19. myfont.ShowText(50,260,pPreText[1]);
  20. myfont.ShowText(50,290,pPreText[0]);
  21. myfont.ShowText(50,320,pPreText[7]);

这个类的使用就这些。这个类只是负责菜单文字的显示,那么,游戏中的LIFE,WORLD的提示,是在哪里完成的呢?函数如下:

  1. void GAMEMAP::ShowInfo(HDC h)
  2. {
  3. char temp[50]={0};
  4. SetTextColor(h, TC_WHITE);
  5. SetBkColor(h, TC_BLACK);
  6. sprintf(temp, "LIFE : %d",iLife);
  7. TextOut(h, 220,100,temp,strlen(temp));
  8. sprintf(temp, "WORLD : %d",iMatch+1);
  9. TextOut(h, 220,130,temp,strlen(temp));
  10. }

这个函数很简单。要说明的是,它并没有设置字体,因为在显示菜单的时候已经设置过了。

至此,所有文字的处理全部实现。

八、跟踪打印类FILEREPORT

前面介绍了图片显示、时钟控制、字体管理几项基本技术。这是所有游戏都通用的基本技术。剩下的问题就是游戏逻辑,例如益智类、运动类、射击类、格斗类等等。当然,不同的游戏需要针对自身做一些优化,比如益智类游戏的时钟控制、画面刷新都更简单,而格斗游戏,画面的质量要更酷、更炫。下面要介绍整个游戏的核心层:逻辑控制。地图怎样绘制的?物品的坐标怎么存储?人物怎样移动?游戏流程是什么样的?

在介绍这些内容前,先打断一下思路,说程序是怎样写出来的,即“调试”。

程序就是一堆代码,了无秘密。初学时,dos下一个猜数字的程序,只需要十几行。一个纸牌游戏,一千多行,而超级玛丽增强版,近三千行。怎样让这么一堆程序从无到有而且运行正确?开发不是靠设计的巧妙或者笨拙,而是靠反复调试。在三千行的代码中,增加一千行,仍然运行正确,这是编程的基本要求。这个最基本的要求,靠设计做不到,只能靠调试。正如公司里的测试部,人力规模,工作压力,丝毫不比开发部差。即使如此,还是能让一些简单bug流入最终产品。老板只能先问测试部:“这么简单的bug,怎么没测出来?”再问开发部:“这么明显的错误,你怎么写出来的?”总之,程序是调出来的。

怎么调?vc提供了很全面的调试方法,打断点、单步跟踪、看变量。这些方法对游戏不适用。一个bug,通常发生在某种情况下,比如超级玛丽,玩家在水管上,按方向键“下”,新的地图显示不出来,屏幕上乱七八糟。请问,bug在哪里?玩家坐标出问题、按键响应出问题、地图加载出问题、图片显示出问题?打断点,无处下手。

解决方法是:程序中,创建一个文本文件,在“可能有问题”的地方,添加代码,向这个文件写入提示信息或变量内容(称为跟踪打印)。这个文本文件,就成了代码运行的日志。看日志,就知道代码中发生了什么事情。最终,找到bug。

FILEREPORT,就是对日志文件创建、写入等操作的封装。

FILEREPORT类定义如下所示:

  1. class FILEREPORT
  2. {
  3. public:
  4. // 功能 默认构造函数,创建日志trace.txt
  5. // 入参 无
  6. FILEREPORT();
  7. // 功能 指定日志文件名称
  8. // 入参 日志文件名称
  9. FILEREPORT(char *p);
  10. // 功能 析构函数,关闭文件
  11. // 入参 无
  12. ~FILEREPORT();
  13. // 功能 向日志文件写入字符串
  14. // 入参 要写入的字符串
  15. void put(char *p);
  16. // 功能 向日志文件写入一个字符串,两个整数
  17. // 入参 字符串 整数a 整数b
  18. void put(char *p,int a,int b);
  19. // 功能 计数器计数, 并写入一个提示字符串
  20. // 入参 计时器id 字符串
  21. void putnum(int i,char *p);
  22. // 功能 判断一个dc是否为null, 如果是,写入提示信息
  23. // 入参 dc句柄 字符串
  24. void CheckDC(HDC h,char *p);
  25. // 功能 设置显示跟踪信息的dc和文本坐标
  26. // 入参 显示dc 横纵坐标
  27. void SetDevice(HDC h,int x,int y);
  28. // 功能 设置要显示的跟踪信息
  29. // 功能 提示字符串 整数a 整数b
  30. void Output(char *p,int a,int b);
  31. // 功能 在屏幕上显示当前的跟踪信息
  32. void Show();
  33. private:
  34. // 跟踪文件指针
  35. FILE *fp;
  36. // 计数器组
  37. int num[5];
  38. // 显示dc
  39. HDC hshow;
  40. // 跟踪文本显示坐标
  41. int xpos;
  42. int ypos;
  43. // 当前跟踪信息
  44. char info[50];
  45. };

函数具体实现很简单,只是简单的文件写入。要说明的是两部分,

  • 一:计数功能,有时要统计某个事情发生多少次,所以用一个整数数组,通过putnum让指定数字累加。
  • 二:显示功能,让跟踪信息,立刻显示在屏幕上。

使用举例:没有使用。程序最终完成,所有的跟踪打印都已删除。

九、精灵结构struct ROLE

这个结构用来存储两种精灵:敌人(各种小怪)和子弹(攻击方式)。敌人包括两种蘑菇兵和两种火圈。子弹包括火球和旋风。游戏中,精灵的结构很简单:

  1. struct ROLE
  2. {
  3. int x; // 横坐标
  4. int y; // 纵坐标
  5. int w; // 图片宽度
  6. int h; // 图片高度
  7. int id; // 精灵id
  8. int iframe; // 图片当前帧
  9. int iframemax; // 图片最大帧数
  10. // 移动部分
  11. int xleft; // 水平运动的左界限
  12. int xright; // 水平运动的右界限
  13. int movex; // 水平运动的速度
  14. // 人物属性
  15. int health; // 精灵的生命值
  16. int show; // 精灵是否显示
  17. };

游戏中的子弹处理非常简单,包括存储、生成、销毁。子弹的存储:所有的子弹存储在一个数组中,如下:

  1. struct ROLE FireArray[MAX_MAP_OBJECT];

其实,所有的动态元素都有从生成到销毁的过程。看一下子弹是怎样产生的。

首先,玩家按下z键:发出子弹,调用函数:

  1. int GAMEMAP::KeyProc(int iKey)
  2. case KEY_Z: // FIRE
  3. if(iBeginFire)
  4. break;
  5. iTimeFire=0;
  6. iBeginFire=1;
  7. break;

这段代码的意思是:如果正在发子弹,代码结束。否则,设置iBeginFire为1,表示开始发子弹。

子弹是在哪里发出的呢?

思路:用一个函数不停地检测iBeginFire,如果它为1,则生成一个子弹。函数如下:

  1. int GAMEMAP::CheckAni(int itimeclip)

发子弹的部分:

  1. // 发子弹
  2. if(iBeginFire)
  3. {
  4. // 发子弹的时间到了(连续两个子弹要间隔一定时间)
  5. if(0 == iTimeFire )
  6. {
  7. // 设置子弹属性: 可见, 动画起始帧:第0帧
  8. FireArray[iFireNum].show=1;
  9. FireArray[iFireNum].iframe = 0;
  10. // 子弹方向
  11. // 如果人物朝右
  12. if(0==rmain.idirec)
  13. {
  14. // 子弹向右
  15. FireArray[iFireNum].movex=1;
  16. }
  17. else
  18. {
  19. // 子弹向左
  20. FireArray[iFireNum].movex=-1;
  21. }
  22. // 区分攻击种类: 子弹,旋风
  23. switch(iAttack)
  24. {
  25. // 普通攻击: 子弹
  26. case ATTACK_NORMAL:
  27. // 精灵ID: 子弹
  28. FireArray[iFireNum].id=ID_ANI_FIRE;
  29. // 设置子弹坐标
  30. FireArray[iFireNum].x=rmain.xpos;
  31. FireArray[iFireNum].y=rmain.ypos;
  32. // 设置子弹宽高
  33. FireArray[iFireNum].w=FIREW;
  34. FireArray[iFireNum].h=FIREH;
  35. // 设置子弹速度: 方向向量乘以移动速度
  36. FireArray[iFireNum].movex*=FIRE_SPEED;
  37. break;

最后,移动数组的游标iFireNum.这个名字没起好, 应该写成cursor.游标表示当前往数组中存储元素的位置。

  1. // 移动数组游标
  2. iFireNum=(iFireNum+1)%MAX_MAP_OBJECT;

至此,游戏中已经生成了一个子弹。 由图像层,通过子弹的id,坐标在屏幕上绘制出来。

子弹已经显示在屏幕上,接下来,就是让它移动、碰撞、销毁。

十、子弹的显示和帧的刷新

继续介绍子弹的显示和动画帧的刷新,这个思路,可以应用的其他精灵上。

上次讲所有的子弹存储到一个数组里,用一个游标(数组下标)表示新生产的子弹存储的位置。设数组为a,长度为n。游戏开始,一个子弹存储在a0,然后是a1,a2,…,a(n-1)。然后游标又回到0,继续从a0位置存储。数组长度30,保存屏幕上所有的子弹足够了。

子弹的显示功能由图像层完成,如同图像处理中讲的,显示一个子弹(所有图片都是如此),只需要子弹坐标,子弹图片id,图片帧。函数如下:

  1. void GAMEMAP::ShowAniObj(MYANIOBJ & bmobj)

代码部分:

  1. // 显示子弹,魔法攻击
  2. for(i=0;i<MAX_MAP_OBJECT;i++)
  3. {
  4. if (FireArray[i].show)
  5. {
  6. ystart=FireArray[i].y;
  7. xstart=FireArray[i].x;
  8. switch(FireArray[i].id)
  9. {
  10. case ID_ANI_FIRE:
  11. bmobj.DrawItem(xstart,ystart,FireArray[i].id,FireArray[i].iframe);
  12. break;

子弹图片显示完成。游戏中,子弹是两帧图片构成的动画。动画帧是哪里改变的呢?

刷新帧的函数是:

  1. voidGAMEMAP::ChangeFrame(int itimeclip)

游戏中,不停地调用这个函数,刷新各种动画的当前帧。其中子弹部分的代码:

  1. // 子弹,攻击控制
  2. for(i=0;i<MAX_MAP_OBJECT;i++)
  3. {
  4. if(FireArray[i].show)
  5. {
  6. switch(FireArray[i].id)
  7. {
  8. default:
  9. FireArray[i].iframe=1-FireArray[i].iframe;
  10. break;
  11. }
  12. }
  13. }

子弹的动画只有两帧,所以iframe只是0,1交替变化。至此,子弹在屏幕上显示,并且两帧图片不停播放。

子弹和小怪碰撞,是游戏中的关键逻辑。网游里也是主要日常工作,打怪。消灭小怪,也是这个游戏的全部乐趣。那么, 这个关键的碰撞检测,以及碰撞检测后的逻辑处理,是怎样的呢?

十一、子弹运动和打怪

玩家按攻击键,生成子弹,存储在数组中,显示,接下来:子弹运动,打怪。先说子弹是怎样运动的。思路:用一个函数不停地检测子弹数组,如果子弹可见,刷新子弹的坐标。

实现如下:

  1. int GAMEMAP::CheckAni(int itimeclip)
  2. {
  3. // 子弹移动
  4. for(i=0;i<MAX_MAP_OBJECT;i++)
  5. {
  6. // 判断子弹是否可见
  7. if (FireArray[i].show)
  8. {
  9. // 根据子弹的移动速度movex,修改子弹坐标.
  10. // (movex为正,向右移动;为负,向左移动,).
  11. FireArray[i].x+=FireArray[i].movex;
  12. // 判断子弹是否超出了屏幕范围,如果超出,子弹消失(设置为不可见)
  13. if( FireArray[i].x > viewx+VIEWW || FireArray[i].x<viewx-FIRE_MAGIC_MAX_W)
  14. {
  15. FireArray[i].show = 0;
  16. }
  17. }
  18. }
  19. }

至此,子弹在屏幕上不停地运动。

打怪是怎样实现的呢:碰撞检测的思路:用一个函数不停地检测所有子弹,如果某个子弹碰到了小怪,小怪消失,子弹消失。

实现如下:

  1. int GAMEMAP::CheckAni(int itimeclip)
  2. {
  3. // 检测子弹和敌人的碰撞(包括魔法攻击)
  4. for(i=0;i<MAX_MAP_OBJECT;i++)
  5. {
  6. // 判断小怪是否可见
  7. if(MapEnemyArray[i].show)
  8. {
  9. // 检测所有子弹
  10. for(j=0;j<MAX_MAP_OBJECT;j++)
  11. {
  12. // 判断子弹是否可见
  13. if (FireArray[j].show)
  14. {
  15. // 判断子弹和小怪是否"碰撞"
  16. if(RECT_HIT_RECT(FireArray[j].x+FIRE_XOFF,
  17. FireArray[j].y,
  18. FireArray[j].w,
  19. FireArray[j].h,
  20. MapEnemyArray[i].x,
  21. MapEnemyArray[i].y,
  22. MapEnemyArray[i].w,
  23. MapEnemyArray[i].h)
  24. )
  25. {
  26. // 如果碰撞,小怪消灭
  27. ClearEnemy(i);
  28. switch(iAttack)
  29. {
  30. case ATTACK_NORMAL:
  31. // 子弹消失
  32. FireArray[j].show=0;

如果是旋风,在旋风动画帧结束后消失。

碰撞检测说明

子弹和小怪,都被看作是矩形,检测碰撞就是判断两个矩形是否相交。以前,有网友说,碰撞检测有很多优化算法。我还是想不出来,只写成了这样:

  1. // 矩形与矩形碰撞
  2. #define RECT_HIT_RECT(x,y,w,h,x1,y1,w1,h1) ( (y)+(h)>(y1) && (y)<(y1)+(h1) && (x)+(w)>(x1) && (x)<(x1)+(w1) )

小怪的消失,代码如下所示:

  1. void GAMEMAP::ClearEnemy(int i)
  2. {
  3. // 小怪的生命值减一
  4. MapEnemyArray[i].health--;
  5. // 如果小怪的生命值减到0, 小怪消失(设置为不可见)
  6. if(MapEnemyArray[i].health<=0)
  7. {
  8. MapEnemyArray[i].show=0;
  9. }
  10. }

至此,玩家按下攻击键,子弹生成、显示、运动,碰到小怪,子弹消失,小怪消失。这些功能全部完成。如果只做成这样,不算本事。

攻击方式分两种:子弹和旋风。小怪包括:两种蘑菇兵和两种火圈。同时,火圈能产生两种蘑菇兵,而旋风的攻击效果明显高于普通子弹。这是不是很复杂?怎样做到的呢?

十二、旋风攻击、小怪运动、火圈

前面介绍了子弹的生成、显示、运动、碰撞、消失的过程。这个过程可以推广到其他精灵上。继续介绍旋风、蘑菇兵、火圈。

作为魔法攻击方式的旋风,和子弹大同小异。旋风的存储与子弹同存储在一个数组中,如下:

  1. struct ROLE FireArray[MAX_MAP_OBJECT];

使用时,用id区分。旋风生成函数如下所示:

  1. int GAMEMAP::CheckAni(int itimeclip)
  2. {
  3. // 发子弹
  4. if(iBeginFire)
  5. {
  6. if(0 == iTimeFire )
  7. {
  8. FireArray[iFireNum].show=1;
  9. FireArray[iFireNum].iframe = 0;
  10. // 子弹方向
  11. if(0==rmain.idirec)
  12. {
  13. FireArray[iFireNum].movex=1;
  14. }
  15. else
  16. {
  17. FireArray[iFireNum].movex=-1;
  18. }
  19. switch(iAttack)
  20. {
  21. case ATTACK_MAGIC:
  22. FireArray[iFireNum].id=ID_ANI_FIRE_MAGIC;
  23. FireArray[iFireNum].x=rmain.xpos-ID_ANI_FIRE_MAGIC_XOFF;
  24. FireArray[iFireNum].y=rmain.ypos-ID_ANI_FIRE_MAGIC_YOFF;
  25. FireArray[iFireNum].w=FIRE_MAGIC_W;
  26. FireArray[iFireNum].h=FIRE_MAGIC_H;
  27. FireArray[iFireNum].movex=0;
  28. break;
  29. }
  30. // 移动数组游标
  31. iFireNum=(iFireNum+1)%MAX_MAP_OBJECT;
  32. }
  33. iTimeFire=(iTimeFire+1)%TIME_FIRE_BETWEEN;
  34. }
  35. }

这和子弹生成的处理相同。唯一区别是旋风不移动,所以movex属性最后设置为0。

旋风的显示原理

旋风在屏幕上的绘制和子弹相同,代码部分和子弹相同。但是旋风的帧刷新有些特殊处理:

  1. void GAMEMAP::ChangeFrame(int itimeclip)
  2. {
  3. // 子弹,攻击控制
  4. for(i=0;i<MAX_MAP_OBJECT;i++)
  5. {
  6. // 如果攻击(子弹、旋风)可见
  7. if(FireArray[i].show)
  8. {
  9. switch(FireArray[i].id)
  10. {
  11. case ID_ANI_FIRE_MAGIC:
  12. // 旋风当前帧加一
  13. FireArray[i].iframe++;
  14. // 如果帧为2(即第三张图片) ,图片坐标修正,向右移
  15. if(FireArray[i].iframe == 2)
  16. {
  17. FireArray[i].x+=FIRE_MAGIC_W;
  18. }
  19. // 如果帧号大于3,即四张图片播放完,旋风消失,设置为不可见
  20. if(FireArray[i].iframe>3)
  21. {
  22. FireArray[i].show=0;
  23. }
  24. break;
  25. }
  26. }

至此,旋风显示,动画播放结束后消失。旋风不涉及运动。碰撞检测的处理和子弹相同,唯一区别是:当旋风和小怪碰撞,旋风不消失。

  1. int GAMEMAP::CheckAni(int itimeclip)
  2. {
  3. switch(iAttack)
  4. {
  5. case ATTACK_NORMAL:
  6. // 子弹消失
  7. FireArray[j].show=0;
  8. break;
  9. // 旋风不消失
  10. default:
  11. break;
  12. }

那么,再看小怪消失的函数:

  1. void GAMEMAP::ClearEnemy(int i)
  2. {
  3. MapEnemyArray[i].health--;
  4. if(MapEnemyArray[i].health<=0)
  5. {
  6. MapEnemyArray[i].show=0;
  7. }

可以看到,此时并不区分攻击方式。但旋风存在的时间长(动画结束后消失),相当于多次调用了这个函数,间接提高了杀伤力。至此,两种攻击方式都已实现。

再看小怪,分蘑菇兵和火圈两种。

存储问题和攻击方式处理相同,用数组加游标的方法,蘑菇兵和火圈存储在同一数组中,如下:

  1. struct ROLE MapEnemyArray[MAX_MAP_OBJECT];
  2. int iMapEnemyCursor;

小怪是由地图文件设定好的,以第二关的地图文件为例,其中小怪部分如下:

  1. ;enemy
  2. 21 6 1 1 0 15 24
  3. 23 6 1 1 0 15 24
  4. 48 7 2 2 6 0 0
  5. 68 5 2 2 8 0 0

各个参数是什么意义呢?看一下加载函数就全明白了。函数如下所示:

  1. int GAMEMAP::LoadMap()
  2. {
  3. // 如果文件没有结束后
  4. while(temp[0]!='#' && !feof(fp))
  5. {
  6. // 读入小怪数据 横坐标 纵坐标 宽 高 id 运动范围左边界 右边界
  7. sscanf(temp,"%d %d %d %d %d %d %d",
  8. &MapEnemyArray[i].x,
  9. &MapEnemyArray[i].y,
  10. &MapEnemyArray[i].w,
  11. &MapEnemyArray[i].h,
  12. &MapEnemyArray[i].id,
  13. &MapEnemyArray[i].xleft,
  14. &MapEnemyArray[i].xright);
  15. // 坐标转换.乘以32
  16. MapEnemyArray[i].x*=32;
  17. MapEnemyArray[i].y*=32;
  18. MapEnemyArray[i].w*=32;
  19. MapEnemyArray[i].h*=32;
  20. MapEnemyArray[i].xleft*=32;
  21. MapEnemyArray[i].xright*=32;
  22. MapEnemyArray[i].show=1;
  23. // 设置移动速度(负,表示向左)
  24. MapEnemyArray[i].movex=-ENEMY_STEP_X;
  25. // 动画帧
  26. MapEnemyArray[i].iframe=0;
  27. // 动画最大帧
  28. MapEnemyArray[i].iframemax=2;
  29. // 设置生命值
  30. switch(MapEnemyArray[i].id)
  31. {
  32. case ID_ANI_BOSS_HOUSE:
  33. MapEnemyArray[i].health=BOSS_HEALTH;
  34. break;
  35. case ID_ANI_BOSS_HOUSE_A:
  36. MapEnemyArray[i].health=BOSS_A_HEALTH;
  37. break;
  38. default:
  39. MapEnemyArray[i].health=1;
  40. break;
  41. }
  42. // 将火圈存储在数组的后半段,数值长30, BOSS_CURSOR为15
  43. if ( i<BOSS_CURSOR
  44. && ( MapEnemyArray[i].id == ID_ANI_BOSS_HOUSE
  45. || MapEnemyArray[i].id == ID_ANI_BOSS_HOUSE_A) )
  46. {
  47. // move data to BOSS_CURSOR
  48. MapEnemyArray[BOSS_CURSOR]=MapEnemyArray[i];
  49. memset(&MapEnemyArray[i],0,sizeof(MapEnemyArray[i]));
  50. i=BOSS_CURSOR;
  51. }
  52. i++;
  53. // 读取下一行地图数据
  54. FGetLineJumpCom(temp,fp);
  55. }

看来比生成子弹要复杂一些,尤其是火圈,为什么要从第15个元素上存储?因为,火圈要不停地生成蘑菇兵,所以”分区管理”,数值前一半存储蘑菇兵,后一半存储火圈。那么,小怪和火圈是怎样显示和运动的呢?火圈怎样不断产生新的小怪?

十三、小怪和火圈

小怪的显示问题,蘑菇兵和火圈处于同一个数组,很简单:

  1. void GAMEMAP::ShowAniObj(MYANIOBJ & bmobj)
  2. {
  3. // 显示敌人
  4. for(i=0;i<MAX_MAP_OBJECT;i++)
  5. {
  6. if (MapEnemyArray[i].show)
  7. {
  8. bmobj.DrawItem(MapEnemyArray[i].x,MapEnemyArray[i].y,
  9. MapEnemyArray[i].id,MapEnemyArray[i].iframe);
  10. }
  11. }

同样,如同图片处理所讲,显示一个图片,只需要坐标、id、帧。

帧刷新和小怪运动的代码如下所示:

  1. void GAMEMAP::ChangeFrame(int itimeclip)
  2. {
  3. // 移动时间:每隔一段时间ENEMY_SPEED,移动一下
  4. if(0 == itimeclip% ENEMY_SPEED)
  5. {
  6. for(i=0;i<MAX_MAP_OBJECT;i++)
  7. {
  8. // 如果小怪可见
  9. if(MapEnemyArray[i].show)
  10. {
  11. // 帧刷新
  12. MapEnemyArray[i].iframe=(MapEnemyArray[i].iframe+1)%MapEnemyArray[i].iframemax;
  13. switch(MapEnemyArray[i].id)
  14. {
  15. case ID_ANI_ENEMY_NORMAL:
  16. case ID_ANI_ENEMY_SWORD:
  17. // 蘑菇兵移动(士兵,刺客)
  18. MapEnemyArray[i].x+=MapEnemyArray[i].movex;
  19. // 控制敌人移动:向左移动到左边界后,移动速度movex改为向右。移动到右边界后,改为向左。
  20. if(MapEnemyArray[i].movex<0)
  21. {
  22. if(MapEnemyArray[i].x<=MapEnemyArray[i].xleft)
  23. {
  24. MapEnemyArray[i].movex=ENEMY_STEP_X;
  25. }
  26. }
  27. else
  28. {
  29. if(MapEnemyArray[i].x>=MapEnemyArray[i].xright)
  30. {
  31. MapEnemyArray[i].movex=-ENEMY_STEP_X;
  32. }
  33. }
  34. break;
  35. }

至此,所有小怪不停移动。(火圈的movex为0,不会移动)

在前面的子弹、旋风的碰撞处理中已讲过。碰撞后,生命值减少,减为0后,消失。火圈会产生新的蘑菇兵,怎样实现的呢?思路:不断地检测火圈是否出现在屏幕中,出现后,生成蘑菇兵。

  1. int GAMEMAP::CheckAni(int itimeclip)
  2. {
  3. // 如果在显示范围之内,则设置显示属性
  4. for(i=0;i<MAX_MAP_OBJECT;i++)
  5. {
  6. // 判断是否在屏幕范围内
  7. if ( IN_AREA(MapEnemyArray[i].x, viewx, VIEWW) )
  8. {
  9. // 如果有生命值,设置为可见
  10. if(MapEnemyArray[i].health)
  11. {
  12. MapEnemyArray[i].show=1;
  13. switch(MapEnemyArray[i].id)
  14. {
  15. // 普通级火圈
  16. case ID_ANI_BOSS_HOUSE:
  17. // 每隔一段时间, 产生新的敌人
  18. if(itimeclip == TIME_CREATE_ENEMY)
  19. {
  20. MapEnemyArray[iMapEnemyCursor]=gl_enemy_normal;
  21. MapEnemyArray[iMapEnemyCursor].x=MapEnemyArray[i].x;
  22. MapEnemyArray[iMapEnemyCursor].y=MapEnemyArray[i].y+32;
  23. // 移动游标
  24. iMapEnemyCursor=(iMapEnemyCursor+1)%BOSS_CURSOR;
  25. }
  26. break;
  27. // 下面是战斗级火圈,处理相似
  28. }
  29. }
  30. }
  31. else
  32. {
  33. // 不在显示范围内,设置为不可见
  34. MapEnemyArray[i].show=0;
  35. }
  36. }

这样,火圈就不断地产生蘑菇兵。

再说一下模板,这里的模板不是C++的模板。据说template技术已发展到艺术的境界,游戏中用到的和template无关,而是全局变量。如下:

  1. // 普通蘑菇兵
  2. struct ROLE gl_enemy_normal=
  3. {
  4. 0,
  5. 0,
  6. 32,
  7. 32,
  8. ID_ANI_ENEMY_NORMAL,
  9. 0,
  10. 2,
  11. 0,
  12. 0,
  13. -ENEMY_STEP_X, // speed
  14. 1,
  15. 1
  16. };

当火圈不断产生新的蘑菇兵时,直接把这个小怪模板放到数组中,再修改一下坐标即可。(对于蘑菇刺客,还要修改id和生命值)

游戏的主要逻辑完成。此外,还有金币,爆炸效果等其他动态元素,它们是怎么实现的?

十四、爆炸效果和金币

子弹每次攻击到效果,都会显示一个爆炸效果。由于只涉及图片显示,它的结构很简单。如下:

  1. struct MapObject
  2. {
  3. int x;
  4. int y;
  5. int w;
  6. int h;
  7. int id;
  8. int iframe;
  9. int iframemax; // 最大帧数
  10. int show; // 是否显示
  11. };

存储问题,爆炸效果仍然使用数组加游标的方法,如下:

  1. struct MapObject BombArray[MAX_MAP_OBJECT];
  2. int iBombNum;

当子弹和小怪碰撞后,生成。

  1. void GAMEMAP::ClearEnemy(int i)
  2. {
  3. // 生成
  4. BombArray[iBombNum].show=1;
  5. BombArray[iBombNum].id=ID_ANI_BOMB;
  6. BombArray[iBombNum].iframe=0;
  7. BombArray[iBombNum].x=MapEnemyArray[i].x-BOMB_XOFF;
  8. BombArray[iBombNum].y=MapEnemyArray[i].y-BOMB_YOFF;
  9. // 修改数组游标
  10. iBombNum=(iBombNum+1)%MAX_MAP_OBJECT;

和子弹、小怪的显示方法相同。

  1. void GAMEMAP::ShowAniObj(MYANIOBJ & bmobj)
  2. {
  3. for(i=0;i<MAX_MAP_OBJECT;i++)
  4. {
  5. if (BombArray[i].show)
  6. {
  7. ystart=BombArray[i].y;
  8. xstart=BombArray[i].x;
  9. bmobj.DrawItem(xstart,ystart,BombArray[i].id, BombArray[i].iframe);
  10. }
  11. }

和子弹、小怪的帧刷新方法相同。

  1. void GAMEMAP::ChangeFrame(int itimeclip)
  2. {
  3. for(i=0;i<MAX_MAP_OBJECT;i++)
  4. {
  5. if(BombArray[i].show)
  6. {
  7. BombArray[i].iframe++;
  8. // 当第四张图片显示完毕,设置为不可见。
  9. if(BombArray[i].iframe>3)
  10. {
  11. BombArray[i].show=0;
  12. }
  13. }
  14. }

碰撞检测:爆炸效果不涉及碰撞检测。

消失:如上所述,爆炸效果在动画结束后消失。

金币的处理比小怪更简单。当玩家和金币碰撞后,金币消失,增加金钱数量。用数组加游标的方法存储,如下:

  1. struct MapObject MapCoinArray[MAX_MAP_OBJECT];
  2. int iCoinNum;

金币的生成,和小怪相似,从地图文件中加载。以第二关为例,地图文件中的金币数据是:

  1. 6 5 32 32 3
  2. 7 5 32 32 3
  3. 8 5 32 32 3
  4. 9 5 32 32 3
  5. 18 4 32 32 3
  6. 19 4 32 32 3
  7. 20 4 32 32 3

数据依次表示横坐标、纵坐标、宽、高、图片id。

  1. int GAMEMAP::LoadMap()
  2. {
  3. while(temp[0]!='#' && !feof(fp))
  4. {
  5. sscanf(temp,"%d %d %d %d %d",
  6. &MapCoinArray[i].x,
  7. &MapCoinArray[i].y,
  8. &MapCoinArray[i].w,
  9. &MapCoinArray[i].h,
  10. &MapCoinArray[i].id);
  11. MapCoinArray[i].show=1;
  12. MapCoinArray[i].iframe=0;
  13. // 坐标转换,乘以32
  14. MapCoinArray[i].x*=32;
  15. MapCoinArray[i].y*=32;
  16. // 设置这个动画元件的最大帧
  17. switch(MapCoinArray[i].id)
  18. {
  19. case ID_ANI_COIN:
  20. MapCoinArray[i].iframemax=4;
  21. break;
  22. }
  23. i++;
  24. iCoinNum++;
  25. // 读取下一行数据
  26. FGetLineJumpCom(temp,fp);
  27. }

金币显示和小怪的显示方法相同:

  1. void GAMEMAP::ShowAniObj(MYANIOBJ & bmobj)
  2. {
  3. // 显示金币,和其他物品
  4. for(i=0;i<iCoinNum;i++)
  5. {
  6. ystart=MapCoinArray[i].y;
  7. xstart=MapCoinArray[i].x;
  8. bmobj.DrawItem(xstart,ystart,MapCoinArray[i].id, MapCoinArray[i].iframe);
  9. }

金币帧刷新和小怪的帧刷新方法相同:

  1. void GAMEMAP::ChangeFrame(int itimeclip)
  2. {
  3. for(i=0;i<MAX_MAP_OBJECT;i++)
  4. {
  5. // 如果金币可见,帧加一
  6. if(MapCoinArray[i].show)
  7. { MapCoinArray[i].iframe=(MapCoinArray[i].iframe+1)%MapCoinArray[i].iframemax;
  8. }
  9. }

金币碰撞检测和小怪的碰撞检测方法相似,区别在于:金币的碰撞检测没有判断是否可见,只要金币位于屏幕中,和玩家碰撞,则金币消失,金钱数量iMoney增加。

  1. int GAMEMAP::CheckAni(int itimeclip)
  2. {
  3. for(i=0;i<iCoinNum;i++)
  4. {
  5. tempx=MapCoinArray[i].x;
  6. tempy=MapCoinArray[i].y;
  7. if ( IN_AREA(tempx, viewx-32, VIEWW) )
  8. {
  9. // 玩家坐标是rmain.xpos rmain.ypos
  10. if( RECT_HIT_RECT(rmain.xpos,
  11. rmain.ypos,
  12. 32,32,
  13. tempx,
  14. tempy,
  15. MapCoinArray[i].w,MapCoinArray[i].h)
  16. )
  17. {
  18. switch(MapCoinArray[i].id)
  19. {
  20. case ID_ANI_COIN:
  21. // 增加金钱数量
  22. iMoney+=10;
  23. // 金币消失
  24. ClearCoin(i);
  25. break;
  26. }
  27. return 0;
  28. }
  29. }
  30. } // end of for

金币消失和小怪的消失不一样,不需要设置show为0,而是直接删除元素,即数组移动的方法:

  1. void GAMEMAP::ClearCoin(int i)
  2. {
  3. // 检查合法性
  4. if(i<0 || i>=iCoinNum)
  5. return;
  6. // 减少一个金币,或者减少一个其他物品
  7. for(;i<iCoinNum;i++)
  8. {
  9. MapCoinArray[i]=MapCoinArray[i+1];
  10. }
  11. // 修改数量
  12. iCoinNum--;

由此可见,直接删除元素,省去了是否可见的判断。但凡事都有两面性,移动数组显然比单个元素的设置要慢(实际上不一定,可以优化)。方法多种多样,这就是程序的好处,永远有更好的答案。

所有的动态元素都介绍完了。所谓动态元素,就是有一个生成、运行、销毁的过程。只不过,有的复杂一些,如子弹、旋风、蘑菇兵、火圈,有些元素简单一些,如爆炸效果、金币。方法都大同小异,要强调的是,这不是最好的方法。碰到金币后,会出现‘+10’的字样,怎么做呢?

十五、金币提示和攻击提示

提示信息,是玩家得到的反馈。比如,碰到金币,金币消失,此时就要显示“+10”;攻击小怪,小怪却没有消失,这时要显示血条,告知玩家小怪的生命值。下面讲提示信息。

金币提示+10的字样,并没有用文字处理,而是用图片(4帧的动画)。这样,实现起来很简单,和爆炸效果用同一个数组存储,处理方法相同。

金币的碰撞检测函数如下所示:

  1. int GAMEMAP::CheckAni(int itimeclip)
  2. {
  3. for(i=0;i<iCoinNum;i++)
  4. {
  5. // 判断玩家是否碰到金币
  6. switch(MapCoinArray[i].id)
  7. {
  8. case ID_ANI_COIN:
  9. // 碰到金币
  10. iMoney+=10;
  11. // 金币消失,显示+10字样
  12. ClearCoin(i);
  13. break;

金币消失函数如下所示:

  1. void GAMEMAP::ClearCoin(int i)
  2. {
  3. switch(MapCoinArray[i].id)
  4. {
  5. case ID_ANI_COIN:
  6. // 碰到了金币,显示+10字样. 和爆炸效果的处理一样, 只是图片id不同
  7. BombArray[iBombNum].show=1;
  8. BombArray[iBombNum].id=ID_ANI_COIN_SCORE;
  9. BombArray[iBombNum].iframe=0;
  10. BombArray[iBombNum].x=MapCoinArray[i].x-COIN_XOFF;
  11. BombArray[iBombNum].y=MapCoinArray[i].y-COIN_YOFF;
  12. iBombNum=(iBombNum+1)%MAX_MAP_OBJECT;
  13. break;
  14. }

攻击提示需要给出攻击对象名称,血条。存储:

  1. // 攻击对象提示
  2. char AttackName[20]; // 攻击对象名称
  3. int iAttackLife; // 攻击对象当前生命值
  4. int iAttackMaxLife; // 攻击对象最大生命值

提示信息设置:在小怪被攻击的时候,设置提示信息。其他攻击对象处理相似。

  1. void GAMEMAP::ClearEnemy(int i)
  2. {
  3. // 设置攻击对象生命值
  4. iAttackLife=MapEnemyArray[i].health;
  5. switch(MapEnemyArray[i].id)
  6. {
  7. case ID_ANI_BOSS_HOUSE:
  8. // 设置名称
  9. strcpy(AttackName,"普通级火圈");
  10. // 设置最大生命值
  11. iAttackMaxLife=BOSS_HEALTH;

提示信息显示:

  1. void GAMEMAP::ShowOther(HDC h)
  2. {
  3. // 如果攻击对象生命值不为0, 显示提示信息
  4. if(iAttackLife)
  5. {
  6. // 输出名称
  7. TextOut(h,viewx+ATTACK_TO_TEXT_X,
  8. ATTACK_TO_TEXT_Y,AttackName,strlen(AttackName));
  9. // 显示血条
  10. xstart=viewx+ATTACK_TO_X-iAttackMaxLife*10;
  11. // 按最大生命值显示一个矩形, 作为背景
  12. bmMap.DrawItemNoMaskWidth(xstart-1, ATTACK_TO_Y-1,ID_MAP_HEALTH_BK,
  13. iAttackMaxLife*BMP_WIDTH_HEALTH, 0);
  14. // 按当前生命值对应的宽度, 显示一个红色矩形
  15. bmMap.DrawItemNoMaskWidth(xstart, ATTACK_TO_Y,ID_MAP_HEALTH,
  16. iAttackLife*BMP_WIDTH_HEALTH, 0);
  17. }

金钱数量显示和攻击提示位于同一个函数:

  1. void GAMEMAP::ShowOther(HDC h)
  2. {
  3. sprintf(temp,"MONEY: %d",iMoney);
  4. TextOut(h,viewx+20,20,temp,strlen(temp));

至此,攻击系统(子弹、旋风、蘑菇兵,火圈),金币(金币,金钱数量),提示信息(金币提示,攻击提示),这几类元素都介绍过了,还有一个,武器切换,就是从魂斗罗里抠来的那个东西。

十六、攻击方式切换

当玩家碰到武器包(就是魂斗罗里那个东西),攻击方式切换。

  • 思路:把它放到存储金币的数组中,用id区别。碰撞检测时,如果是金币,金币消失,如果是武器包,攻击方式切换。
  • 存储:和金币位于同一个数组MapCoinArray。
  • 生成:由地图文件加载。比如第一关的地图文件数据:
  1. 25 4 52 25 5

各参数含义:横坐标、纵坐标、宽、高、图片id。

和金币的加载相同,唯一区别是金币图片有4帧,武器包只有2帧,加载函数如下所示:

  1. int GAMEMAP::LoadMap()
  2. {
  3. MapCoinArray[i].iframemax=2;

显示和金币的处理相同,相同函数,相同代码。(再次显示出图像层的好处)

帧刷新和金币的处理相同,相同函数,相同代码。(再次显示出图像层的好处)

碰撞检测和金币的处理相同,如果是武器包,设置新的攻击方式,武器包消失。

  1. int GAMEMAP::CheckAni(int itimeclip)
  2. {
  3. switch(MapCoinArray[i].id)
  4. {
  5. case ID_ANI_ATTACK:
  6. // 设置新的攻击方式
  7. iAttack=ATTACK_MAGIC;
  8. // 武器包消失
  9. ClearCoin(i);
  10. break;
  11. }

武器包的消失和金币的处理相同,相同函数,相同代码,这是逻辑层的好处(放在同一个数组中,处理简单)。

至此,攻击系统,金币系统,提示信息,武器切换,全部完成。只需要一个地图把所有的物品组织起来,构成一个虚拟世界,呈现在玩家眼前。

十七、地图物品

自从游戏机发明以来,地图是什么样的呢?打蜜蜂,吃豆,地图是一个矩形,玩家在这个矩形框内活动。后来,地图得到扩展,可以纵向移动,比如打飞机;可以横向移动,比如超级玛丽、魂斗罗等等横板过关游戏。再后来,横向纵向都可以移动,后来又有45度地图,3D技术后终于实现了高度拟真的虚拟世界。

  • 超级玛丽的地图可以看成是一个二维的格子。每个格子的大小是32x32像素。
  • 游戏窗口大小为12个格子高,16个格子宽。
  • 游戏地图宽度是游戏窗口的5倍,即12个格子高,5x16个格子宽。

地图物品有哪些呢?地面,砖块,水管。先看一下存储结构:

  1. struct MapObject
  2. {
  3. int x;
  4. int y;
  5. int w;
  6. int h;
  7. int id;
  8. int iframe;
  9. int iframemax; // 最大帧数
  10. int show; // 是否显示
  11. };

各个成员含义是横坐标、纵坐标、宽、高、id、当前帧、最大帧、是否可见。用第一关地图文件的地图物品举例:(只包含5个参数)

  1. 0 9 10 3 0

这个物品是什么呢?横向第0个格子,纵向第9个格子,宽度10个格子,高度3个格子,id为0,表示地面。

在显示的时候,只要把坐标、宽高乘以32,即可正确显示。

地图所有物品仍然用数组+游标的方法存储,如下:

  1. struct MapObject MapArray[MAX_MAP_OBJECT];
  2. int iMapObjNum;

从地图文件中加载并生成地图。

  1. int GAMEMAP::LoadMap()
  2. {
  3. while(temp[0]!='#' && !feof(fp))
  4. {
  5. // 读取一个物品
  6. sscanf(temp,"%d %d %d %d %d",
  7. &MapArray[i].x,
  8. &MapArray[i].y,
  9. &MapArray[i].w,
  10. &MapArray[i].h,
  11. &MapArray[i].id);
  12. MapArray[i].show=0;
  13. iMapObjNum++;
  14. i++;
  15. // 读取下一个物品
  16. FGetLineJumpCom(temp,fp);
  17. }

地图显示和物品显示一样,只是地面和砖块需要双重循环。对于每个宽w格,高h格的地面、砖块,需要把单个地面砖块平铺w*h次,所以用双重循环。

  1. void GAMEMAP::Show(MYANIOBJ & bmobj)
  2. {
  3. for(i=0;i<iMapObjNum;i++)
  4. {
  5. ystart=MapArray[i].y*32;
  6. switch(MapArray[i].id)
  7. {
  8. //进出水管
  9. case ID_MAP_PUMP_IN:
  10. case ID_MAP_PUMP_OUT:
  11. xstart=MapArray[i].x*32;
  12. bmobj.DrawItemNoMask(xstart, ystart, MapArray[i].id, 0);
  13. break;
  14. default:
  15. for(j=0;j<MapArray[i].h;j++)
  16. {
  17. xstart=MapArray[i].x*32;
  18. for(k=0;k<MapArray[i].w;k++)
  19. {
  20. bmobj.DrawItemNoMask(xstart, ystart, MapArray[i].id, 0);
  21. xstart+=32;
  22. }
  23. ystart+=32;
  24. } // end of for
  25. break;

其中,水管是一个单独完整的图片,直接显示,不需要循环。

地面、砖块、水管都是静态图片,不涉及帧刷新。保证玩家顺利地行走,如果玩家不踩在物品上,则不停地下落。

  1. int GAMEMAP::CheckRole()
  2. {
  3. // 检测角色是否站在某个物体上
  4. for(i=0;i<iMapObjNum;i++)
  5. {
  6. // 玩家的下边线,是否和物品的上边线重叠
  7. if( LINE_ON_LINE(rmain.xpos,
  8. rmain.ypos+32,
  9. 32,
  10. MapArray[i].x*32,
  11. MapArray[i].y*32,
  12. MapArray[i].w*32)
  13. )
  14. {
  15. // 返回1,表示玩家踩在这个物品上
  16. return 1;
  17. }
  18. }
  19. // 角色开始下落
  20. rmain.movey=1;
  21. rmain.jumpx=0; // 此时要清除跳跃速度,否则将变成跳跃,而不是落体
  22. return 0;

十八、背景物品

背景物品更简单,包括草丛,树木,河流,win标志。这些背景物品只需要显示,不涉及逻辑处理。用数组+游标的方法存储,如下:

  1. struct MapObject MapBkArray[MAX_MAP_OBJECT];
  2. int iMapBkObjNum;

第一关的背景物品数据(含义和地图物品相同):

  1. 17 5 3 2 0 (草丛)
  2. 76 7 3 2 1 win标志)
  3. 10 10 3 2 2 (河流)

背景物品加载和地图物品加载方法相同。

  1. int GAMEMAP::LoadMap()
  2. {
  3. while(temp[0]!='#' && !feof(fp))
  4. {
  5. sscanf(temp,"%d %d %d %d %d",
  6. &MapBkArray[i].x,
  7. &MapBkArray[i].y,
  8. …...
  9. MapBkArray[i].iframe=0;
  10. iMapBkObjNum++;
  11. i++;
  12. // 下一个物品
  13. FGetLineJumpCom(temp,fp);
  14. }

背景物品的显示:

  1. void GAMEMAP::ShowBkObj(MYANIOBJ & bmobj)
  2. {
  3. for(i=0;i<iMapBkObjNum;i++)
  4. {
  5. bmobj.DrawItem(xstart,ystart,MapBkArray[i].id,ibkobjframe);
  6. }

帧刷新:背景物品都是2帧动画。所有背景物品当前帧用ibkobjframe控制。

  1. void GAMEMAP::ChangeFrame(int itimeclip)
  2. {
  3. if(0 == itimeclip% WATER_SPEED)
  4. {
  5. ibkobjframe=1-ibkobjframe;

十九、视图

怎样把所有东西都显示在窗口中,并随着玩家移动呢?

思路:玩家看到的区域称为视图,即12格高,16格宽的窗口(每格32*32像素)。先把整个地图则绘制在一个DC上,然后从这个地图DC中,截取当前视图区域的图像,绘制到窗口中。修改视图区域的坐标(横坐标增加),就实现了地图的移动。

初始化:

  1. BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
  2. {
  3. // hwindow是游戏窗口的DC句柄
  4. hwindow=GetDC(hWnd);
  5. // hscreen是整个地图对应的DC
  6. hscreen=CreateCompatibleDC(hwindow);
  7. // 建立一个整个地图大小(5倍窗口宽)的空位图,选入hscreen
  8. hmapnull=CreateCompatibleBitmap(hwindow,GAMEW*32*5,GAMEH*32);
  9. SelectObject(hscreen,hmapnull);

视图的显示:

  1. LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM)
  2. {
  3. case WM_PAINT:
  4. // hwindow是游戏窗口的DC句柄
  5. hwindow = BeginPaint(hWnd, &ps);
  6. SelectObject(hscreen,hmapnull);
  7. case GAME_IN:
  8. // 显示天空
  9. bmSky.DrawRollStretch(2,2,gamemap.mapinfo.iBackBmp);
  10. // 显示背景物品
  11. gamemap.ShowBkObj(bmMapBkObj);
  12. // 显示地图物品
  13. gamemap.Show(bmMap);
  14. // 显示动态元素
  15. gamemap.ShowAniObj(bmAniObj);
  16. // 显示提示信息
  17. gamemap.ShowOther(hscreen);
  18. // 显示玩家
  19. rmain.Draw();
  20. break;
  21. if(gamemap.iScreenScale)
  22. {
  23. // 窗口大小调整功能,代码略
  24. }
  25. else
  26. {
  27. // 从整个地图的DC中, 截取当前视图区域的图像,绘制到窗口
  28. BitBlt(hwindow, 0, 0, GAMEW*32, GAMEH*32, hscreen, gamemap.viewx, 0, SRCCOPY);
  29. }

可以看到,视图的左上角横坐标是viewx,只需要刷新这个坐标,就实现了地图移动。

视图坐标刷新思路:用一个函数不停地检测,玩家角色和视图左边界的距离,超过特定值,把视图向右移。如果玩家坐标和视图左边界的距离大于150,移动视图。

  1. void GAMEMAP::MoveView()
  2. {
  3. if(rmain.xpos - viewx > 150)
  4. {
  5. viewx+=ROLE_STEP;
  6. //判断视图坐标是否达到最大值(地图宽度减去一个窗口宽度)
  7. if(viewx>(mapinfo.viewmax-1)*GAMEW*32)
  8. viewx=(mapinfo.viewmax-1)*GAMEW*32;
  9. }

二十、地图切换

地图分两种,普通地图和隐藏地图(指通过水管进入的地图)。先讲普图地图的切换,再讲隐藏地图的切换。

普通地图的切换思路:很简单,用一个数字iMatch表示当前是第几关。每过一关,iMatch+1,加载下一张地图。

过关检测:用一个函数不停地检测玩家是否到了地图终点,如果是,加载下一关的地图。

  1. int GAMEMAP::IsWin()
  2. {
  3. // 判断玩家的坐标是否到达地图终点(横坐标大于等于地图宽度)
  4. if(rmain.xpos >= MAX_PAGE*GAMEW*32 )
  5. {
  6. // iMatch增加
  7. iMatch=mapinfo.iNextMap;
  8. if(iMatch>=MAX_MATCH)
  9. {
  10. // 如果iMatch大于关卡数量(即通过最后一关),加载第一关的数据,代码略
  11. }
  12. else
  13. {
  14. // 没有通关
  15. InitMatch();//初始化游戏数据
  16. // 设置玩家角色坐标,初始化玩家角色
  17. rmain.SetPos(BM_USER,3*32,8*32);
  18. rmain.InitRole(0,GAMEW*32*MAX_PAGE-32);
  19. // 加载下一关的地图
  20. LoadMap();
  21. }

函数LoadMap()根据iMatch的值加载某一关的地图。而iMatch的修改代码是:

对于普通地图iMatch取值为0,1,2,…,只需要+1即可,为什么要有一个复杂的赋值过程呢?是为了实现隐藏地图的切换。

隐藏地图的切换,先看一下LoadMap加载的地图文件是什么样子?超级玛丽增强版的地图存储在一个文本文件中,结构为:

  1. *0
  2. // 第0关的地图数据
  3. *1
  4. // 第1关的地图数据
  5. *4
  6. // 第4关的地图数据

其中,编号0,1,2表示前三关的普图地图,编号3,4是隐藏地图(3是第0关的隐藏地图,4是第1关的隐藏地图)。怎样表示地图之间的关系呢?

思路:设计一张“地图信息表”,格式如下:

  1. 0关:下一关编号,隐藏地图编号
  2. 1关:下一关编号,隐藏地图编号
  3. 4关:下一关编号,隐藏地图编号

这样就形成一个地图信息的处理:

  • 从“地图信息表”中读取当前关卡的的地图信息。
  • 当玩家到达地图终点,读取“下一关”编号;玩家进入水管,读取“隐藏地图编号”。

游戏的地图信息结构:

  1. struct MAPINFO
  2. {
  3. int iNextMap;
  4. int iSubMap;
  5. };

地图信息表(全局变量): (数组的第i个元素,表示第i关的地图信息)

  1. struct MAPINFO allmapinfo[]={
  2. {1,3},
  3. {2,4},
  4. {MAX_MATCH,-1, },
  5. {-1,0},
  6. {-1,1}
  7. };

对应的逻辑信息为:

  • 第0关的下一关是第1关,从水管进入第3关。
  • 第1关的下一关是第2关,从水管进入第4关。
  • 第2关(最后一关)没有下一关(MAX),没有从水管进入的地图。
  • 第3关没有下一关,从水管进入第0关。
  • 第4关没有下一关,从水管进入第1关。

这样,实现了从水管进入隐藏关,又从水管返回的功能。

地图信息的存储在 struct MAPINFO mapinfo; 结构体变量中,每一关的游戏开始前,都要用这个函数初始化游戏数据。包括读取地图信息:

  1. void GAMEMAP::InitMatch()
  2. {
  3. mapinfo=allmapinfo[iMatch];

玩家到达地图终点的检测:

  1. int GAMEMAP::IsWin()
  2. {
  3. iMatch=mapinfo.iNextMap;

切换到下一关的地图编号。

玩家进入水管的检测思路:当玩家按下方向键“下”,判断是否站在水管上(当然进入地图的水管),如果是,切换地图。

  1. int GAMEMAP::KeyProc(int iKey)
  2. {
  3. case VK_DOWN:
  4. for(i=0;i<iMapObjNum;i++)
  5. {
  6. // 判断玩家是否站在一个地图物品上
  7. if( LINE_IN_LINE(玩家坐标,地图物品坐标))
  8. {
  9. // 这个物品是水管
  10. if(MapArray[i].id == ID_MAP_PUMP_IN)
  11. {
  12. // 设置游戏状态:进入水管
  13. iGameState=GAME_PUMP_IN;

函数WndProc中,不断检测GAME_PUMP_IN状态,代码如下:

  1. case WM_TIMER:
  2. switch(gamemap.iGameState)
  3. {
  4. case GAME_PUMP_IN:
  5. if(c1.DecCount())
  6. {
  7. // 如果GAME_PUMP_IN状态结束,加载隐藏地图。
  8. gamemap.ChangeMap();:

是不是复杂一些?确实,它可以简化。我想这还是有好处,它容易扩展。这仍然是我最初的构思,这是一个代码框架。看一下ChangeMap的处理:

  1. void GAMEMAP::ChangeMap()
  2. {
  3. //读取隐藏地图编号
  4. iMatch=mapinfo.iSubMap;
  5. //游戏初始化
  6. InitMatch();
  7. //加载地图
  8. LoadMap();

可见,ChangeMap的简单很简单。因为,LoadMap的接口只是iMatch,我只要保证iMatch在不同情况下设置正确,地图就会正确地加载。

至此,地图切换实现。但是,地图切换中,还有其它的游戏数据要刷新,怎样处理呢?

二十一、游戏数据管理

进入每一关之前,需要对所有游戏数据初始化。进入隐藏地图,同样需要初始化。而且,从隐藏地图返回上层地图,还要保证玩家出现在“出水管”处。地图数据、玩家数据、视图数据,都要设置正确。

所有的游戏数据,即封装在gamemap中的数据,分成如下几种:

  • 场景数据:包含当前关卡的地图,所有精灵,金币,提示信息。
  • 视图数据:视图窗口坐标。
  • 玩家数据:玩家角色的个人信息,例如金钱数量,攻击方式,游戏次数。

1.场景数据

  1. int iGameState; // 当前游戏状态
  2. int iMatch; // 当前关卡
  3. // 各种精灵的数组:
  4. struct MapObject MapArray[MAX_MAP_OBJECT]; // 地图物品
  5. struct MapObject MapBkArray[MAX_MAP_OBJECT]; // 地图背景物品
  6. struct ROLE MapEnemyArray[MAX_MAP_OBJECT]; // 小怪
  7. struct MapObject MapCoinArray[MAX_MAP_OBJECT]; // 金币
  8. struct ROLE FireArray[MAX_MAP_OBJECT]; // 子弹
  9. struct MapObject BombArray[MAX_MAP_OBJECT]; // 爆炸效果
  10. // 当前关卡的地图信息
  11. struct MAPINFO mapinfo;
  12. // 图片帧
  13. int ienemyframe; // 小怪图片帧
  14. int ibkobjframe; // 背景图片帧
  15. // 玩家攻击
  16. int iTimeFire; // 两个子弹的间隔时间
  17. int iBeginFire; // 是否正在发子弹
  18. // 攻击对象提示
  19. char AttackName[20]; // 攻击对象名称
  20. int iAttackLife; // 攻击对象生命值
  21. int iAttackMaxLife; // 攻击对象最大生命值

2.视图数据

  1. int viewx; // 视图起始坐标

3.玩家数据

  1. int iMoney; // 金钱数量
  2. int iAttack; // 攻击方式
  3. int iLife; // 玩家游戏次数

可见,每次加载地图前,要初始化场景数据和视图数据,而玩家数据不变,如金钱数量。

游戏数据处理,假设没有隐藏地图的功能,游戏数据只需要完成初始化的功能,分别位于以下三个地方:

  • 程序运行前,初始化;
  • 过关后,初始化,再加载下一关地图;
  • 失败后,初始化,再加载当前地图;

1.游戏程序运行,所有游戏数据初始化

  1. BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
  2. {
  3. gamemap.Init();
  4. void GAMEMAP::Init()
  5. {
  6. // 设置游戏初始状态
  7. iGameState=GAME_PRE;
  8. // 设置当前关卡
  9. iMatch=0;
  10. // 设置玩家数据 玩家游戏次数,金钱数量,攻击种类
  11. iLife=3;
  12. iMoney=0;
  13. iAttack=ATTACK_NORMAL;
  14. // 设置视图坐标
  15. viewx=0;
  16. // 初始化场景数据
  17. InitMatch();
  18. void GAMEMAP::InitMatch()
  19. {
  20. memset(MapArray,0,sizeof(MapArray));
  21. memset(BombArray,0,sizeof(BombArray));
  22. ienemyframe=0;
  23. iFireNum=0;
  24. ……

这样,程序启动,InitInstance中完成第一次初始化。

2.过关后,游戏数据初始化,加载下一关地图

  1. int GAMEMAP::IsWin()
  2. {
  3. // 判断玩家是否到达地图终点
  4. if(rmain.xpos >= MAX_PAGE*GAMEW*32 )
  5. {
  6. // 读取下一关地图编号
  7. iMatch=mapinfo.iNextMap;
  8. if(iMatch>=MAX_MATCH)
  9. {
  10. // 如果全部通过
  11. Init(); // 初始化所有数据
  12. LoadMap(); // 加载地图
  13. }
  14. else
  15. {
  16. InitMatch(); // 初始化场景数据
  17. // 设置玩家坐标
  18. rmain.SetPos(BM_USER,3*32,8*32);
  19. rmain.InitRole(0,GAMEW*32*MAX_PAGE-32);
  20. // 加载下一关的地图
  21. LoadMap();
  22. }

3.如果玩家失败,重新加载当前地图

  1. int GAMEMAP::IsWin()
  2. {
  3. // 检测角色和敌人的碰撞
  4. for(i=0;i<MAX_MAP_OBJECT;i++)
  5. {
  6. if(MapEnemyArray[i].show)
  7. {
  8. if(HLINE_ON_RECT(玩家坐标 小怪坐标))
  9. {
  10. if(0 == rmain.movey)
  11. {
  12. // 玩家在行走过程中,碰到小怪,游戏失败
  13. Fail();
  14. }
  15. else
  16. {
  17. // 玩家在下落过程中,碰到火圈,游戏失败
  18. switch(MapEnemyArray[i].id)
  19. {
  20. case ID_ANI_BOSS_HOUSE:
  21. case ID_ANI_BOSS_HOUSE_A:
  22. Fail();
  23. ……
  24. // 玩家到达地图底端(掉入小河),游戏失败
  25. if(rmain.ypos > GAMEH*32)
  26. {
  27. Fail();
  28. return 0;
  29. }
  30. void GAMEMAP::Fail()
  31. {
  32. // 玩家游戏次数减1
  33. iLife--;
  34. // 设置游戏状态
  35. iGameState=GAME_FAIL_WAIT;
  36. // GAME_FAIL_WAIT状态结束后,调用函数void GAMEMAP::Fail_Wait()加载地图。
  37. void GAMEMAP::Fail_Wait()
  38. {
  39. if( iLife <=0)
  40. {
  41. // 游戏次数为0,重新开始,初始化所有数据
  42. Init();
  43. }
  44. else
  45. {
  46. // 还能继续游戏
  47. }
  48. // 设置玩家坐标
  49. rmain.SetPos(BM_USER,3*32,8*32);
  50. rmain.InitRole(0,GAMEW*32*MAX_PAGE-32);
  51. // 加载当前地图
  52. LoadMap();

至此,在没有隐藏地图的情况下,游戏数据管理(只有初始化)介绍完了。

增加了隐藏地图的功能,游戏数据管理包括:初始化,数据刷新。哪些数据需要刷新呢?

  • 刷新玩家坐标

    例如,从第一关(地图编号为0)进入隐藏地图,玩家出现在(3,8),即横向第3格,纵向第8格。玩家返回第一关后,要出现在“出水管”的位置(66,7)。

  • 刷新视图坐标

    例如,从第一关进入隐藏地图,玩家出现在(3,8),视图对应地图最左边,玩家返回第一关后,视图要移动到“出水管”的位置。

  • 刷新背景图片的坐标

    例如,从第一关进入隐藏地图,玩家出现在(3,8),天空背景对应地图最左边,玩家返回第一关后,背景图片要移动到“出水管”的位置。

  1. void GAMEMAP::ChangeMap()
  2. {
  3. // 初始化视图坐标
  4. viewx=0;
  5. // 获取隐藏地图编号
  6. iMatch=mapinfo.iSubMap;
  7. // 初始化场景数据
  8. InitMatch();
  9. // 设置玩家坐标
  10. rmain.SetPos(BM_USER,mapinfo.xReturnPoint*32,mapinfo.yReturnPoint*32);
  11. // 玩家角色初始化
  12. rmain.InitRole(0,GAMEW*32*MAX_PAGE-32);
  13. // 设定视图位置
  14. if(rmain.xpos - viewx > 150)
  15. {
  16. SetView(mapinfo.xReturnPoint*32-32); // 往左让一格
  17. if(viewx>(mapinfo.viewmax-1)*GAMEW*32)
  18. viewx=(mapinfo.viewmax-1)*GAMEW*32;
  19. }
  20. // 设定人物活动范围
  21. rmain.SetLimit(viewx, GAMEW*32*MAX_PAGE);
  22. // 设定背景图片坐标
  23. bmSky.SetPos(BM_USER,viewx,0);
  24. // 加载地图
  25. LoadMap();
  26. }

所以,地图信息表中,要包含“出水管”的坐标。完整的地图信息表如下:

  1. struct MAPINFO
  2. {
  3. int iNextMap; // 过关后的下一关编号
  4. int iSubMap; // 进入水管后的地图编号
  5. int xReturnPoint; // 出水管的横坐标
  6. int yReturnPoint; // 出水管的纵坐标
  7. int iBackBmp; // 背景图片ID
  8. int viewmax; // 视图最大宽度
  9. };
  10. struct MAPINFO allmapinfo[]={
  11. {1,3,66,7,0,5},
  12. {2,4,25,4,1,5},
  13. {MAX_MATCH,-1,-1,-1,2,5},
  14. {-1,0,3,8,3,1},
  15. {-1,1,3,8,3,2}
  16. };

第0关

{1,3,66,7,0,5},表示第0关的下一关是第1关,从水管进入第3关,出水管位于(66,7),天空背景id为0,视图最大宽度为5倍窗口宽度。

第3关

{-1,0,3,8,3,1},表示第3关没有下一关,从水管进入第0关,出水管位于(3,8),天空背景id为3,视图最大宽度为1倍窗口宽度。

这样,隐藏地图切换的同时,视图数据,玩家数据均正确。

各个动态元素,地图的各种处理都已完成,只需要让玩家控制的小人,走路,跳跃,攻击,进出水管。玩家的动作控制怎样实现?

二十二、玩家角色类MYROLE

玩家控制的小人,和各种小怪基本一致。没什么神秘的。主要有三个功能要实现:键盘响应,动作控制,图片显示。

为了方便图片显示,玩家角色类MYROLE直接派生自图片类MYBITMAP。

MYROLE类定义如下所示:

  1. class MYROLE:public MYBITMAP
  2. {
  3. public:
  4. // 构造函数,析构函数
  5. MYROLE();
  6. ~MYROLE();
  7. // 初始化部分
  8. // 功能 初始化玩家信息
  9. // 入参 玩家运动范围的左边界 右边界()
  10. void InitRole(int xleft, int xright);
  11. // 功能 设置玩家运动范围
  12. // 入参 玩家运动范围的左边界 右边界()
  13. void SetLimit(int xleft, int xright);
  14. // 图片显示部分
  15. // 功能 显示玩家角色图片(当前坐标 当前帧)
  16. // 入参 指定的横坐标 纵坐标 帧
  17. void Draw(int x,int y,int iframe);
  18. // 功能 刷新帧,该函数没有使用, 帧刷新的功能在其它地方完成
  19. // 入参 无
  20. void ChangeFrame();
  21. // 功能 设置玩家状态. 该函数没有使用
  22. // 入参 玩家状态
  23. void SetState(int i);
  24. // 动作部分
  25. // 功能 玩家角色移动
  26. // 入参 无
  27. void Move();
  28. // 功能 玩家角色跳跃. 该函数没有使用
  29. // 入参 指定地点横坐标 纵坐标
  30. void MoveTo(int x,int y);
  31. // 功能 从当前位置移动一个增量
  32. // 入参 横坐标增量 纵坐标增量
  33. void MoveOffset(int x,int y);
  34. // 功能 向指定地点移动一段距离(移动增量是固定的)
  35. // 入参 指定地点横坐标 纵坐标
  36. void MoveStepTo(int x,int y);
  37. // 动画部分
  38. // 功能 播放动画
  39. // 入参 无
  40. void PlayAni();
  41. // 功能 设置动画方式
  42. // 入参 动画方式
  43. void SetAni(int istyle);
  44. // 功能 判断是否正在播放动画, 如果正在播放动画,返回1.否则,返回0
  45. // 入参 无
  46. int IsInAni();
  47. // 数据部分
  48. // 玩家状态, 该变量没有使用
  49. int iState;
  50. // 图片数据
  51. // 玩家当前帧
  52. int iFrame;
  53. // 动作控制数据
  54. // 玩家活动范围: 左边界 右边界(只有横坐标)
  55. int minx;
  56. int maxx;
  57. // 运动速度
  58. int movex; // 正值,向右移动
  59. int movey; // 正值,向下移动
  60. // 跳跃
  61. int jumpheight; // 跳跃高度
  62. int jumpx; // 跳跃时, 横向速度(正值,向右移动)
  63. // 玩家运动方向
  64. int idirec;
  65. // 动画数据
  66. int iAniBegin; // 动画是否开始播放
  67. int iparam1; // 动画参数
  68. int iAniStyle; // 动画方式
  69. };

各个功能的实现:

键盘响应

玩家通过按键,控制人物移动。

  1. LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM)
  2. {
  3. case WM_KEYDOWN:
  4. if(gamemap.KeyProc(wParam))
  5. InvalidateRect(hWnd,NULL,false);
  6. break;
  7. case WM_KEYUP:
  8. gamemap.KeyUpProc(wParam);
  9. break;

按键消息包括“按下”和“抬起”两种方式:

  1. int GAMEMAP::KeyProc(int iKey)
  2. {
  3. switch(iGameState)
  4. {
  5. case GAME_PRE: // 选择游戏菜单
  6. switch(iKey)
  7. {
  8. case 0xd: // 按下回车键
  9. switch(iMenu)
  10. {
  11. case 0: // 菜单项0“开始游戏”
  12. c1.ReStart(TIME_GAME_IN_PRE); // 计时两秒
  13. iGameState=GAME_IN_PRE; // 进入游戏LIFE/WORLD提示状态
  14. break;
  15. case 1: // 菜单项1“操作说明”
  16. SetGameState(GAME_HELP); // 进入游戏状态“操作说明”,显示帮助信息
  17. break;
  18. }
  19. break;
  20. case VK_UP: // 按方向键“上”,切换菜单项
  21. iMenu=(iMenu+1)%2;
  22. break;
  23. case VK_DOWN: // 按方向键“下”,切换菜单项
  24. iMenu=(iMenu+1)%2;
  25. break;
  26. }
  27. return 1;
  28. case GAME_HELP: // 游戏菜单项“操作说明”打开
  29. switch(iKey)
  30. {
  31. case 0xd: // 按回车键,返回游戏菜单
  32. SetGameState(GAME_PRE); // 设置游戏状态:选择菜单
  33. break;
  34. }
  35. return 1;
  36. case GAME_IN: // 游戏进行中
  37. // 如果人物正在播放动画,拒绝键盘响应
  38. if(rmain.IsInAni())
  39. {
  40. break;
  41. }
  42. // 根据方向键, X, Z, 触发移动,跳跃,攻击等功能
  43. switch(iKey)
  44. {
  45. case VK_RIGHT:
  46. case VK_LEFT:
  47. case VK_DOWN:
  48. case KEY_X: // 跳
  49. case KEY_Z: // FIRE
  50. // 秘籍
  51. case 0x7a: // 按键F11, 直接切换攻击方式
  52. iAttack=(iAttack+1)%ATTACK_MAX_TYPE;
  53. break;
  54. case 0x7b: // 按键F12 直接通关(游戏进行中才可以,即游戏状态GAME_IN)
  55. rmain.xpos = MAX_PAGE*GAMEW*32;
  56. break;
  57. }
  58. break;
  59. }
  60. return 0;
  61. }

可见,按键响应只需要处理三个状态:

  • 菜单选择GAME_PRE
  • 操作说明菜单打开GAME_HELP
  • 游戏进行中GAME_IN

说明前两个状态属于菜单控制,函数返回1,表示立即刷新屏幕。对于状态GAME_IN,返回0。游戏过程中,屏幕刷新由其它地方控制。

按键“抬起”的处理:

  1. void GAMEMAP::KeyUpProc(int iKey)
  2. {
  3. switch(iKey)
  4. {
  5. // 松开方向键“左右”,清除横向移动速度
  6. case VK_RIGHT:
  7. rmain.movex=0;
  8. break;
  9. case VK_LEFT:
  10. rmain.movex=0;
  11. break;
  12. case KEY_X: // 松开跳跃键,无处理
  13. break;
  14. case KEY_Z: // 松开攻击键,清除变量iBeginFire,表示停止攻击
  15. iBeginFire=0;
  16. break;
  17. case KEY_W: // 按W,调整窗口为默认大小
  18. MoveWindow(hWndMain,
  19. (wwin-GAMEW*32)/2,
  20. (hwin-GAMEH*32)/2,
  21. GAMEW*32,
  22. GAMEH*32+32,
  23. true);
  24. break;
  25. }

显示问题:

  1. void MYROLE::Draw()
  2. {
  3. // 判断是否播放动画,即iAniBegin为1
  4. if(iAniBegin)
  5. {
  6. // 显示动画帧
  7. PlayAni();
  8. }
  9. else
  10. {
  11. // 显示当前图片
  12. SelectObject(hdcsrc,hBm);
  13. BitBlt(hdcdest,xpos,ypos,
  14. width,height/2,
  15. hdcsrc,iFrame*width,height/2,SRCAND);
  16. BitBlt(hdcdest,xpos,ypos,
  17. width,height/2,
  18. hdcsrc,iFrame*width,0,SRCPAINT);
  19. }

二十三、玩家动作控制

玩家移动:把行走和跳跃看成两个状态,各自用不同的变量表示横纵方向的速度。

相关属性:

  • 行走:横向速度为movex,纵向不移动
  • 跳跃:横向速度为jumpx,纵向速度为movey。当前跳跃高度jumpheight
  • 运动方向:idirec

思路:

  • 第一步:玩家按键,按键处理函数设置这些属性。按键松开,清除动作属性。
  • 第二步:用一个函数不停检测这些变量,控制玩家移动。

按键触发

  1. int GAMEMAP::KeyProc(int iKey)
  2. {
  3. switch(iKey)
  4. {
  5. case VK_RIGHT: // 按右
  6. // 判断是否正在跳跃, 即纵向速度不为0
  7. if(rmain.movey!=0)
  8. {
  9. // 跳跃过程中, 设置横向速度, 方向向右, 大小为4像素
  10. rmain.jumpx=4;
  11. }
  12. rmain.movex=4; // 设置横向速度, 方向向右, 大小为4像素
  13. rmain.idirec=0; // 设置玩家方向, 向右
  14. break;
  15. case VK_LEFT: // 按左
  16. // 如果是跳跃过程中, 设置横向速度, 方向向左, 大小为4像素
  17. if(rmain.movey!=0)
  18. {
  19. rmain.jumpx=-4;
  20. }
  21. rmain.movex=-4; // 设置横向速度, 方向向左, 大小为4像素
  22. rmain.idirec=1; // 设置玩家方向, 向左
  23. break;
  24. case KEY_X: // X键跳
  25. // 如果已经是跳跃状态,不作处理,代码中断
  26. if(rmain.movey!=0)
  27. break;
  28. // 设置纵向速度,方向向上(负值),大小为13
  29. rmain.movey=-SPEED_JUMP;
  30. // 将当前的横向速度,赋值给“跳跃”中的横向速度
  31. rmain.jumpx=rmain.movex;
  32. break;
  33. case KEY_Z: // FIRE
  34. if(iBeginFire)
  35. break; // 如果已经开始攻击,代码中断
  36. iTimeFire=0; // 初始化子弹间隔时间
  37. iBeginFire=1; // 置1,表示开始攻击
  38. break;

按键松开

  1. void GAMEMAP::KeyUpProc(int iKey)
  2. {
  3. // 松开左右键,清除横向速度
  4. case VK_RIGHT:
  5. rmain.movex=0;
  6. break;
  7. case VK_LEFT:
  8. rmain.movex=0;
  9. break;
  10. case KEY_X: // 跳
  11. // 不能清除跳跃的横向速度jumpx
  12. // 例如,移动过程中起跳,整个跳跃过程中都要有横向速度
  13. break;
  14. case KEY_Z: // FIRE
  15. iBeginFire=0; // 停止攻击
  16. break;

控制移动

  1. LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM)
  2. {
  3. case WM_TIMER:
  4. switch(gamemap.iGameState)
  5. {
  6. case GAME_IN:
  7. rmain.Move();//人物移动
  8. ……
  9. break;

每45毫秒产生一个WM_TIMER消息,在GAME_IN状态下,调用各种检测函数。其中rmain.Move()就是不断检测玩家动作属性,实现移动。

  1. void MYROLE::Move()
  2. {
  3. if(0 == movey)
  4. {
  5. // 如果不是跳跃, 横向移动
  6. MoveOffset(movex, 0);
  7. }
  8. else
  9. {
  10. // 跳跃, 先横向移动, 再纵向移动
  11. MoveOffset(jumpx, 0);
  12. MoveOffset(0, movey);
  13. }
  14. // 玩家帧控制 ”纠错法”
  15. if(movex<0 && iFrame<3)
  16. {
  17. iFrame=3; // 如果玩家向左移动, 而图片向右, 则设置为3(第4张图片)
  18. }
  19. if(movex>0 && iFrame>=3)
  20. {
  21. iFrame=0; // 如果玩家向右移动, 而图片向右, 则设置为0(第1张图片)
  22. }
  23. // 帧刷新
  24. if(movex!=0)
  25. {
  26. if(0==idirec)
  27. iFrame=1-iFrame; // 如果方向向右, 图片循环播放0,1帧
  28. else
  29. iFrame=7-iFrame; // 如果方向向左, 图片循环播放3,4帧
  30. }
  31. if(movey!=0)
  32. {
  33. // 跳跃过程中, 帧设置为0(向右),3(向左)
  34. // 帧刷新后, 重新设置帧, 就实现了跳跃过程中, 图片静止
  35. iFrame=idirec*3;
  36. }
  37. // 跳跃控制
  38. if(movey<0)
  39. {
  40. // 向上运动(纵向速度movey为负值)
  41. jumpheight+=(-movey); // 增加跳跃高度
  42. // 重力影响,速度减慢
  43. if(movey<-1)
  44. {
  45. movey++;
  46. }
  47. // 到达顶点后向下落, 最大跳跃高度为JUMP_HEIGHT * 32, 即3个格子的高度
  48. if(jumpheight >= JUMP_HEIGHT * 32)
  49. {
  50. jumpheight = JUMP_HEIGHT * 32; // 跳跃高度置为最大
  51. movey=4; // 纵向速度置为4, 表示开始下落
  52. }
  53. }
  54. else if(movey>0)
  55. {
  56. // 下落过程, 跳跃高度减少
  57. jumpheight -= movey;
  58. // 重力影响,速度增大
  59. movey++;
  60. }

玩家移动

  1. void MYROLE::MoveOffset(int x,int y)
  2. {
  3. // 横纵增量为0,不移动,代码结束
  4. if(x==0 && y==0)
  5. return;
  6. // 如果碰到物体,不移动,代码结束
  7. if(!gamemap.RoleCanMove(x,y))
  8. return;
  9. // 修改玩家坐标
  10. xpos+=x;
  11. ypos+=y;
  12. // 判断是否超出左边界
  13. if(xpos<minx)
  14. xpos=minx; // 设置玩家坐标为左边界
  15. // 判断是否超出右边界
  16. if(xpos>maxx)
  17. xpos=maxx;

碰撞检测

无论行走,跳跃,都是用函数MoveOffset操纵玩家坐标。这时,就要判断是否碰到物体。如果正在行走,则不能前进;如果是跳跃上升,则开始下落。

  1. int GAMEMAP::RoleCanMove(int xoff, int yoff)
  2. {
  3. int canmove=1;// 初始化, 1表示能移动
  4. for(i=0;i<iMapObjNum;i++)
  5. {
  6. if( RECT_HIT_RECT(玩家坐标加增量,地图物品坐标))
  7. {
  8. // 碰到物体,不能移动
  9. canmove=0;
  10. if(yoff<0)
  11. {
  12. // 纵向增量为负(即上升运动), 碰到物体开始下落
  13. rmain.movey=1;
  14. }
  15. if(yoff>0)
  16. {
  17. // 纵向增量为正(即下落运动), 碰到物体, 停止下落
  18. rmain.jumpheight=0; // 清除跳跃高度
  19. rmain.movey=0; // 清除纵向速度
  20. rmain.ypos=MapArray[i].y*32-32;// 纵坐标刷新,保证玩家站在物品上
  21. }
  22. break;
  23. }
  24. }
  25. return canmove;

玩家移动的过程中,要不断检测是否站在地图物品上。如果在行走过程中,且没有站在任何物品上,则开始下落。

  1. int GAMEMAP::CheckRole()
  2. {
  3. if(rmain.movey == 0 )
  4. {
  5. // 检测角色是否站在某个物体上
  6. for(i=0;i<iMapObjNum;i++)
  7. {
  8. // 玩家的下边线,是否和物品的上边线重叠
  9. if( LINE_ON_LINE(rmain.xpos,
  10. rmain.ypos+32,
  11. 32,
  12. MapArray[i].x*32,
  13. MapArray[i].y*32,
  14. MapArray[i].w*32)
  15. )
  16. {
  17. // 返回1,表示玩家踩在这个物品上
  18. return 1;
  19. }
  20. }
  21. // 角色开始下落
  22. rmain.movey=1;
  23. rmain.jumpx=0;// 此时要清除跳跃速度,否则将变成跳跃,而不是落体
  24. return 0;

至此,玩家在这个虚拟世界可以做出各种动作,跳跃,行走,攻击。增强版中,加入了水管,玩家在进出水管,就需要动画。

二十四、角色动画

玩家在进出水管的时候,需要进入水管、从水管中升起两个动画。当动画播放结束后,切换到新的地图。动画播放过程中,禁止键盘响应,即玩家不能控制移动。

玩家进水管

地图物品中,水管分两个,进水管(玩家进入地图)和出水管(从别的地图返回)。两种水管对应不同的图片ID:

  1. #define ID_MAP_PUMP_IN 9
  2. #define ID_MAP_PUMP_OUT 10

玩家进入水管的检测:

  1. int GAMEMAP::KeyProc(int iKey)
  2. {
  3. // 检测玩家按“下”,如果玩家站在进水管上,开始播放动画
  4. case VK_DOWN:
  5. for(i=0;i<iMapObjNum;i++)
  6. {
  7. if( LINE_IN_LINE(玩家坐标的下边界,地图物品的上边界))
  8. {
  9. // 判断是否站在进水管上
  10. if(MapArray[i].id == ID_MAP_PUMP_IN)
  11. {
  12. // 如果站在设置角色动画方式,向下移动
  13. rmain.SetAni(ROLE_ANI_DOWN);
  14. iGameState=GAME_PUMP_IN; // 设置游戏状态:进水管
  15. c1.ReStart(TIME_GAME_PUMP_WAIT);// 计时2秒
  16. }
  17. }
  18. }
  19. break;

动画设置函数:

  1. void MYROLE::SetAni(int istyle)
  2. {
  3. iAniStyle=istyle; // 设置动画方式
  4. iparam1=0; // 参数初始化为0
  5. iAniBegin=1; // 表示动画开始播放

iparam1是动画播放中的一个参数,根据动画方式不同,可以有不同的含义。

动画播放

玩家角色显示函数:

  1. void MYROLE::Draw()
  2. {
  3. //判断是否播放动画,即iAniBegin为1
  4. if(iAniBegin)
  5. {
  6. PlayAni(); //播放当前动画
  7. }

动画播放函数:

  1. void MYROLE::PlayAni()
  2. {
  3. // 根据不同的动画方式,播放动画
  4. switch(iAniStyle)
  5. {
  6. case ROLE_ANI_DOWN:
  7. // 玩家进入水管的动画,iparam1表示下降的距离
  8. if(iparam1>31)
  9. {
  10. // 下降距离超过31(即图片高度),玩家完全进入水管,无需图片显示
  11. break;
  12. }
  13. // 玩家没有完全进入水管,截取图片上半部分,显示到当前的坐标处
  14. SelectObject(hdcsrc,hBm);
  15. BitBlt(hdcdest,
  16. xpos,ypos+iparam1,
  17. width,height/2-iparam1,
  18. hdcsrc,
  19. iFrame*width,height/2,SRCAND);
  20. BitBlt(hdcdest,
  21. xpos,ypos+iparam1,
  22. width,height/2-iparam1,
  23. hdcsrc,
  24. iFrame*width,0,SRCPAINT);
  25. // 增加下降高度
  26. iparam1++;
  27. break;

玩家进入水管后,切换地图

在时间片的处理中,当GAME_PUMP_IN状态结束,切换地图,并设置玩家动画:

  1. LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM)
  2. {
  3. case GAME_PUMP_IN:
  4. if(c1.DecCount())
  5. {
  6. gamemap.ChangeMap(); // 切换地图
  7. gamemap.SetGameState(GAME_IN); // 设置游戏状态
  8. c1.ReStart(TIME_GAME_IN); // 计时300秒
  9. rmain.SetAni(ROLE_ANI_UP); // 设置动画,图片上升
  10. }
  11. InvalidateRect(hWnd,NULL,false);
  12. break;

从水管中上升

根据不同的动画方式,播放动画:

  1. void MYROLE::PlayAni()
  2. {
  3. switch(iAniStyle)
  4. {
  5. case ROLE_ANI_UP:
  6. if(iparam1>31)
  7. {
  8. // 如果上升距离超过31(图片高度),动画结束
  9. break;
  10. }
  11. // 人物上升动画,截取图片上部,显示到当前坐标
  12. SelectObject(hdcsrc,hBm);
  13. BitBlt(hdcdest,
  14. xpos,ypos+32-iparam1,
  15. width,iparam1,
  16. hdcsrc,
  17. iFrame*width,height/2,SRCAND);
  18. BitBlt(hdcdest,
  19. xpos,ypos+32-iparam1,
  20. width,iparam1,
  21. hdcsrc,
  22. iFrame*width,0,SRCPAINT);
  23. // 增加上升距离
  24. iparam1++;
  25. // 如果上升距离超过31(图片高度)
  26. if(iparam1>31)
  27. {
  28. iAniBegin=0; // 动画结束,清除动画播放状态
  29. }

至此,两个动画方式都实现了。但是,如果在动画播放过程中,玩家按左右键,移动,就会出现,角色一边上升,一边行走,甚至跳跃。怎样解决?如果播放动画,屏蔽键盘响应。

  1. int GAMEMAP::KeyProc(int iKey)
  2. {
  3. case GAME_IN:
  4. // 如果人物正在播放动画,拒绝键盘响应
  5. if(rmain.IsInAni())
  6. {
  7. break;
  8. }

这样,在播放过程中,不受玩家按键影响。玩家所有功能全部实现,接下来看一下整个游戏逻辑。

二十五、GAMEMAP全局变量类

所有游戏数据都需要封装到实际的变量中。整个游戏,就是用类GAMEMAP表示的。

GAMEMAP类定义如下所示:

  1. class GAMEMAP
  2. {
  3. public:
  4. // 加载地图
  5. int LoadMap();
  6. // 初始化所有游戏数据
  7. void Init();
  8. // 初始化场景数据
  9. void InitMatch();
  10. // 显示地图物品
  11. void Show(MYANIOBJ & bmobj);
  12. // 显示地图背景物品,河流,树木
  13. void ShowBkObj(MYANIOBJ & bmobj);
  14. // 显示所有动态元素,金币,小怪等
  15. void ShowAniObj(MYANIOBJ & bmobj);
  16. // 显示LIFE, WORLD提示
  17. void ShowInfo(HDC h);
  18. // 显示金钱, 攻击提示信息
  19. void ShowOther(HDC h);
  20. // 键盘处理
  21. int KeyProc(int iKey);
  22. // 按键抬起处理
  23. void KeyUpProc(int iKey);
  24. // 移动视图
  25. void MoveView();
  26. // 设置视图起始坐标
  27. void SetView(int x);
  28. // 设置视图状态, 函数没有使用
  29. void SetViewState(int i);
  30. // 设置游戏状态
  31. void SetGameState(int i);
  32. // 碰撞检测
  33. // 判断人物能否移动
  34. int RoleCanMove(int xoff, int yoff);
  35. // 检测人物是否站在物品上
  36. int CheckRole();
  37. // 检测所有动态元素之间的碰撞, 子弹和蘑菇兵的生成
  38. int CheckAni(int itimeclip);
  39. // 清除一个小怪
  40. void ClearEnemy(int i);
  41. // 清除一个金币
  42. void ClearCoin(int i);
  43. // 帧刷新
  44. void ChangeFrame(int itimeclip);
  45. // 逻辑检测
  46. int IsWin(); // 胜负检测
  47. void Fail(); // 失败处理
  48. void Fail_Wait(); //失败后, 加载地图
  49. // 地图切换
  50. void ChangeMap();
  51. // 错误检查
  52. void CodeErr(int i);
  53. // 菜单控制
  54. void ShowMenu(MYANIOBJ & bmobj);
  55. // 构造和析构函数
  56. GAMEMAP();
  57. ~GAMEMAP();
  58. // 数据部分
  59. int iMatch; // 当前关卡
  60. int iLife; // 游戏次数
  61. int iGameState; // 游戏状态
  62. // 地图物品数组 游标
  63. struct MapObject MapArray[MAX_MAP_OBJECT];
  64. int iMapObjNum;
  65. // 地图背景物品数组 游标
  66. struct MapObject MapBkArray[MAX_MAP_OBJECT];
  67. int iMapBkObjNum;
  68. // 小怪火圈数组 游标
  69. struct ROLE MapEnemyArray[MAX_MAP_OBJECT];
  70. int iMapEnemyCursor;
  71. // 金币武器包 数组 游标
  72. struct MapObject MapCoinArray[MAX_MAP_OBJECT];
  73. int iCoinNum;
  74. // 下一个地图编号, 变量没有使用
  75. int iNextMap;
  76. // 玩家数据
  77. int iMoney; // 金钱数量
  78. int iAttack; // 攻击方式
  79. // 视图数据
  80. int viewx; // 视图横坐标
  81. int viewy; // 视图纵坐标
  82. int iViewState; // 视图状态
  83. // 地图信息
  84. struct MAPINFO mapinfo;
  85. // frame control
  86. int ienemyframe; // 小怪帧
  87. int ibkobjframe; // 背景物品帧
  88. // 子弹数组 游标
  89. struct ROLE FireArray[MAX_MAP_OBJECT];
  90. int iFireNum;
  91. int iTimeFire; // 两个子弹的时间间隔
  92. int iBeginFire; // 是否开始攻击
  93. // 爆炸效果,+10字样 数组 游标
  94. struct MapObject BombArray[MAX_MAP_OBJECT];
  95. int iBombNum;
  96. // 攻击对象提示
  97. char AttackName[20]; // 名称
  98. int iAttackLife; // 生命值
  99. int iAttackMaxLife; // 最大生命值
  100. // 菜单部分
  101. int iMenu; // 当前菜单项编号
  102. // 屏幕缩放
  103. int iScreenScale; // 是否是默认窗口大小
  104. };

所有的数据都存储到一系列全局变量中:

  1. // 所有菜单文字
  2. char *pPreText[]={
  3. "操作: Z:子弹 X:跳 方向键移动 W:默认窗口大小",
  4. };
  5. // 所有动态元素的图片宽 高
  6. int mapani[2][10]={
  7. {32,32,64,32,32,52,64,32,64,32},
  8. {32,32,64,32,32,25,64,32,64,32},
  9. };
  10. // 所有地图物品的图片宽 高
  11. int mapsolid[2][13]={
  12. {32,32,32,32,32,32,32,32,32,64,64,20,100},
  13. {32,32,32,32,32,32,32,32,32,64,64,10,12}
  14. };
  15. // 所有背景物品的图片宽 高
  16. int mapanibk[2][4]={
  17. {96,96,96,96},
  18. {64,64,64,64},
  19. };
  20. // 旋风的宽 高
  21. int mapanimagic[2][1]={
  22. {192},
  23. {128}
  24. };
  25. // 所有地图信息
  26. struct MAPINFO allmapinfo[]={
  27. {1,3,66,7,0,5},
  28. {2,4,25,4,1,5},
  29. {MAX_MATCH,-1,-1,-1,2,5},
  30. {-1,0,3,8,3,1},
  31. {-1,1,3,8,3,2}
  32. };
  33. // 普通蘑菇兵模板
  34. struct ROLE gl_enemy_normal=
  35. {
  36. 0,
  37. 0,
  38. 32,
  39. 32,
  40. ID_ANI_ENEMY_NORMAL,
  41. };
  42. // 跟踪打印
  43. // FILEREPORT f1;
  44. // 计时器
  45. MYCLOCK c1;
  46. // 游戏全部逻辑
  47. GAMEMAP gamemap;
  48. //各种图片
  49. MYBITMAP bmPre; // 菜单背景,通关,GAMEOVER
  50. MYBKSKY bmSky; // 天空背景
  51. MYANIOBJ bmMap; // 地图物品
  52. MYANIOBJ bmMapBkObj; // 地图背景物品
  53. MYANIOBJ bmAniObj; // 所有动态元素
  54. MYROLE rmain; // 玩家角色
  55. MYANIMAGIC bmMagic; // 旋风
  56. // 字体管理
  57. MYFONT myfont; // 字体
  58. // DC句柄
  59. HDC hwindow,hscreen,hmem,hmem2;// 窗口DC, 地图DC, 临时DC,临时DC2
  60. // 空位图
  61. HBITMAP hmapnull;
  62. // 窗口大小
  63. int wwin,hwin; // 显示器屏幕宽 高
  64. int wwingame,hwingame; // 当前窗口宽 高
  65. HWND hWndMain; // 窗口句柄

二十六、菜单控制 窗口缩放

菜单控制:开始菜单只有两项:0项“开始游戏”,1项“操作说明”,菜单编号用iMenu表示。

菜单文字显示:

  1. LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM)
  2. {
  3. // 在WM_PAINT绘制消息中:
  4. case GAME_PRE:
  5. gamemap.viewx=0; // 设置视图坐标
  6. bmPre.Stretch(2,2,0); // 菜单背景图片
  7. myfont.SelectFont(0); // 设置文字字体
  8. myfont.SelectColor(TC_BLACK, TC_YELLOW_0);// 设置文字颜色
  9. // 显示3行文字
  10. myfont.ShowText(150,260,pPreText[4]);
  11. myfont.ShowText(150,290,pPreText[5]);
  12. myfont.ShowText(150,320,pPreText[6]);
  13. // 显示箭头
  14. gamemap.ShowMenu(bmAniObj);
  15. break;

菜单箭头显示:

  1. void GAMEMAP::ShowMenu(MYANIOBJ & bmobj)
  2. {
  3. // 根据当前菜单编号,决定箭头的纵坐标
  4. bmobj.PlayItem(115,280+iMenu*30, ID_ANI_MENU_ARROW);

箭头会不停闪烁,怎样刷新帧?就在显示函数PlayItem中,如下

  1. void MYANIOBJ::PlayItem(int x,int y,int id)
  2. {
  3. // 按照坐标,ID,显示图片
  4. ……
  5. // 切换当前帧
  6. iframeplay=(iframeplay+1)%2;
  7. }

菜单的按键响应:

  1. int GAMEMAP::KeyProc(int iKey)
  2. {
  3. switch(iGameState)
  4. {
  5. case GAME_PRE:// 选择游戏菜单
  6. switch(iKey)
  7. {
  8. case 0xd:// 按下回车键
  9. switch(iMenu)
  10. {
  11. case 0: // 菜单项0“开始游戏”
  12. c1.ReStart(TIME_GAME_IN_PRE); // 计时两秒
  13. iGameState=GAME_IN_PRE;// 进入游戏LIFE WORLD提示状态
  14. break;
  15. case 1: // 菜单项1“操作说明”
  16. SetGameState(GAME_HELP); // 进入游戏状态“操作说明”,显示帮助信息
  17. break;
  18. }
  19. break;
  20. case VK_UP: // 按方向键“上”,切换菜单项
  21. iMenu=(iMenu+1)%2;
  22. break;
  23. case VK_DOWN: // 按方向键“下”,切换菜单项
  24. iMenu=(iMenu+1)%2;
  25. break;
  26. }
  27. return 1; // 表示立即刷新画面

窗口缩放功能的实现

窗口是否为默认大小,用iScreenScale表示。iScreenScale为1,表示窗口被放大,将视图区域缩放到当前的窗口大小。

初始化由构造函数完成,窗口大小检测,用户拉动窗口,触发WM_SIZE消息。

  1. LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM)
  2. {
  3. case WM_SIZE:
  4. // 获取当前窗口宽 高
  5. wwingame=LOWORD(lParam);
  6. hwingame=HIWORD(lParam);
  7. // 如果窗口小于默认大小,仍然设置为默认数值,图像不缩放
  8. if( wwingame <= GAMEW*32 || hwingame <= GAMEH*32)
  9. {
  10. wwingame = GAMEW*32;
  11. hwingame = GAMEH*32;
  12. gamemap.iScreenScale = 0;
  13. }
  14. else
  15. {
  16. // 宽度大于高度的4/3
  17. if(wwingame*3 > hwingame*4)
  18. {
  19. wwingame = hwingame*4/3; // 重新设置宽度
  20. }
  21. else
  22. {
  23. hwingame = wwingame*3/4; // 重新设置高度
  24. }
  25. gamemap.iScreenScale =1; // 表示图像需要缩放
  26. }
  27. break;

图像缩放,在WM_PAINT消息处理中,绘制完所有图片后,根据iScreenScale缩放视图区域的图像。

  1. LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM)
  2. {
  3. // 判断是否缩放图像
  4. if(gamemap.iScreenScale)
  5. {
  6. // 缩放视图区域图像
  7. StretchBlt(hwindow,0,0,
  8. wwingame,hwingame,
  9. hscreen,
  10. gamemap.viewx,0,
  11. GAMEW*32,GAMEH*32,
  12. SRCCOPY);
  13. }
  14. else
  15. {
  16. // 不缩放,视图区域拷贝到窗口
  17. BitBlt(hwindow, 0, 0, GAMEW*32, GAMEH*32, hscreen, gamemap.viewx, 0, SRCCOPY);
  18. }

二十七、程序框架WinProc

怎样把所有的功能组织起来,形成一个完整的游戏呢?游戏状态。不同的游戏状态下,对应不同的图片显示、逻辑处理、按键响应。这样就形成了一个结构清晰的框架。各个模块相对独立,也方便扩展。

由于是消息处理机制,所有功能对应到消息处理函数WndProc,程序框架如下:

  1. LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM)
  2. {
  3. 绘图消息WM_PAINT:
  4. 状态1:状态1绘图。
  5. 状态2:状态2绘图。
  6. ……
  7. 计时消息WM_TIMER:
  8. 状态1:状态1逻辑处理。发WM_PAINT消息,通知绘图。
  9. 状态2:状态2逻辑处理。发WM_PAINT消息,通知绘图。
  10. ……
  11. 按键消息WM_KEYDOWN WM_KEYUP:
  12. 状态1:状态1逻辑处理。发WM_PAINT消息,通知绘图。
  13. 状态2:状态2逻辑处理。发WM_PAINT消息,通知绘图。
  14. ……
  15. }

程序入口:

  1. int APIENTRY WinMain(HINSTANCE hInstance,
  2. HINSTANCE hPrevInstance,
  3. LPSTR lpCmdLine,
  4. int nCmdShow)
  5. {
  6. MyRegisterClass(hInstance); // 类注册
  7. // 初始化
  8. if (!InitInstance (hInstance, nCmdShow))
  9. {
  10. return FALSE;
  11. }
  12. // 消息循环
  13. while (GetMessage(&msg, NULL, 0, 0))
  14. {
  15. if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
  16. {
  17. TranslateMessage(&msg);
  18. DispatchMessage(&msg);
  19. }
  20. }
  21. return msg.wParam;
  22. }

整个消息处理循环,是默认的结构。InitInstance函数复杂初始化。类注册函数MyRegisterClass中,把菜单栏取消了,即wcex.lpszMenuName=NULL,其它不变。

消息处理函数:

  1. LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM)
  2. {
  3. switch (message)
  4. {
  5. case WM_PAINT:
  6. // 窗口DC
  7. hwindow = BeginPaint(hWnd, &ps);
  8. // 初始化空图
  9. SelectObject(hscreen,hmapnull);
  10. switch(gamemap.iGameState)
  11. {
  12. case GAME_ERR:
  13. // 地图文件加载错误
  14. gamemap.viewx=0; // 视图坐标
  15. // 显示错误信息
  16. bmPre.Stretch(2,2,0); // 背景图片
  17. myfont.SelectColor(TC_WHITE,TC_BLACK);// 文字颜色
  18. myfont.SelectFont(0); // 字体
  19. myfont.ShowText(150,290,pPreText[3]); // 显示文字
  20. break;
  21. case GAME_PRE:
  22. // 菜单显示
  23. (代码略)
  24. break;
  25. case GAME_HELP:
  26. // 菜单项“操作说明”
  27. (代码略)
  28. break;
  29. case GAME_IN_PRE:
  30. // 游戏LIFE,WORLD提示
  31. gamemap.viewx=0; // 视图坐标
  32. bmPre.Stretch(2,2,2); // 背景图片
  33. gamemap.ShowInfo(hscreen); // 显示LIFE,WORLD
  34. break;
  35. case GAME_IN: // 游戏进行中
  36. case GAME_WIN: // 游戏进行中,过关
  37. case GAME_FAIL_WAIT: // 游戏进行中,失败
  38. case GAME_PUMP_IN: // 游戏进行中,进入水管
  39. bmSky.DrawRollStretch(2,2,gamemap.mapinfo.iBackBmp);// 背景图片
  40. gamemap.ShowBkObj(bmMapBkObj); // 地图背景物品
  41. gamemap.Show(bmMap); // 地图物品
  42. gamemap.ShowAniObj(bmAniObj); // 动态元素
  43. gamemap.ShowOther(hscreen); // 金钱数量,攻击提示
  44. rmain.Draw(); // 玩家角色
  45. break;
  46. case GAME_OVER:
  47. // 游戏结束
  48. gamemap.viewx=0;
  49. bmPre.Stretch(2,2,1); // 输出图片GAME OVER
  50. break;
  51. case GAME_PASS:
  52. // 游戏通关
  53. gamemap.viewx=0;
  54. bmPre.Stretch(2,2,3); // 输出图片通关
  55. break;
  56. }
  57. if(gamemap.iScreenScale)
  58. { // 窗口缩放,放大视图区域
  59. StretchBlt(hwindow,0,0,wwingame,hwingame,hscreen, gamemap.viewx,0,GAMEW*32,GAMEH*32,SRCCOPY);
  60. }
  61. else
  62. { // 拷贝视图区域
  63. BitBlt(hwindow,0,0,GAMEW*32,GAMEH*32,hscreen, gamemap.viewx, 0, SRCCOPY);
  64. }
  65. EndPaint(hWnd, &ps); // 绘图结束
  66. break;
  67. case WM_TIMER:
  68. switch(gamemap.iGameState)
  69. {
  70. case GAME_PRE: // 游戏菜单
  71. c1.DecCount();// 计时器减1
  72. if(0 == c1.iNum%MENU_ARROW_TIME)
  73. { // 每隔10个时间片(即箭头闪烁的时间),刷新屏幕
  74. InvalidateRect(hWnd,NULL,false);
  75. }
  76. break;
  77. case GAME_IN_PRE: // 游戏LIFE,WORLD提示
  78. if(c1.DecCount())
  79. {
  80. // 计时结束,进入游戏。
  81. gamemap.SetGameState(GAME_IN);
  82. c1.ReStart(TIME_GAME_IN); // 启动计时300秒
  83. }
  84. InvalidateRect(hWnd,NULL,false); // 刷新屏幕
  85. break;
  86. case GAME_IN: // 游戏进行中
  87. case GAME_WIN: // 游戏进行中,过关
  88. c1.DecCount();// 计时器计时
  89. if(0 == c1.iNum%SKY_TIME)
  90. {
  91. bmSky.MoveRoll(SKY_SPEED);// 云彩移动
  92. }
  93. gamemap.ChangeFrame(c1.iNum);// 帧控制
  94. rmain.Move();// 人物移动
  95. gamemap.MoveView();// 视图移动
  96. gamemap.CheckRole();// 角色检测
  97. gamemap.CheckAni(c1.iNum);// 逻辑数据检测
  98. gamemap.IsWin(); // 胜负检测
  99. InvalidateRect(hWnd,NULL,false); // 刷新屏幕
  100. break;
  101. case GAME_WIN_WAIT: // 游戏进行中,过关,停顿2秒
  102. if(c1.DecCount())
  103. {
  104. // 计时结束,进入游戏LIFE,WORLD提示
  105. gamemap.SetGameState(GAME_IN_PRE);
  106. InvalidateRect(hWnd,NULL,false); // 刷新屏幕
  107. }
  108. break;
  109. case GAME_PUMP_IN: // 游戏进行中,进入水管,停顿2秒
  110. if(c1.DecCount())
  111. {
  112. // 计时结束,切换地图
  113. gamemap.ChangeMap();
  114. gamemap.SetGameState(GAME_IN); // 进入游戏
  115. c1.ReStart(TIME_GAME_IN); // 启动计时300秒
  116. rmain.SetAni(ROLE_ANI_UP); // 设置玩家出水管动画
  117. }
  118. InvalidateRect(hWnd,NULL,false); // 刷新屏幕
  119. break;
  120. case GAME_FAIL_WAIT: // 游戏进行中,失败,停顿2秒
  121. if(c1.DecCount())
  122. {
  123. // 计时结束,加载地图
  124. gamemap.Fail_Wait();
  125. }
  126. break;
  127. case GAME_PASS: //全部通关,停顿2秒
  128. if(c1.DecCount())
  129. {
  130. // 计时结束,设置游戏状态:游戏菜单
  131. gamemap.SetGameState(GAME_PRE);
  132. }
  133. InvalidateRect(hWnd,NULL,false); // 刷新屏幕
  134. break;
  135. case GAME_OVER: // 游戏结束,停顿3秒
  136. if(c1.DecCount())
  137. {
  138. // 计时结束,设置游戏状态:游戏菜单
  139. gamemap.SetGameState(GAME_PRE);
  140. }
  141. InvalidateRect(hWnd,NULL,false); // 刷新屏幕
  142. break;
  143. }
  144. break;
  145. case WM_KEYDOWN: // 按键处理
  146. if(gamemap.KeyProc(wParam))
  147. InvalidateRect(hWnd,NULL,false);
  148. break;
  149. case WM_KEYUP: // 按键“抬起”处理
  150. gamemap.KeyUpProc(wParam);
  151. break;
  152. case WM_SIZE: // 窗口大小调整,代码略
  153. break;
  154. case WM_DESTROY: // 窗口销毁,释放DC, 代码略
  155. break;

终于,所有模块全部完成,游戏制作完成。整个工程差不多3000行代码。第一个制作超级玛丽的程序员,是否用了这么多代码,肯定没有。当时,应该是汇编。3000行C++代码,还达不到汇编程序下的地图规模、图片特效、游戏流畅度。可见,程序的乐趣无穷。

二十八、InitInstance函数说明

  1. BOOL InitInstance(HINSTANCE, int)
  2. {
  3. // 默认窗口大小
  4. wwingame=GAMEW*32;
  5. hwingame=GAMEH*32;
  6. // 显示器屏幕大小
  7. wwin=GetSystemMetrics(SM_CXSCREEN);
  8. hwin=GetSystemMetrics(SM_CYSCREEN);
  9. // 创建窗口
  10. hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
  11. (wwin-wwingame)/2, (hwin-hwingame)/2,
  12. wwingame, hwingame+32, NULL, NULL, hInstance, NULL);
  13. // 设置窗口句柄
  14. hWndMain=hWnd;
  15. //DC
  16. hwindow=GetDC(hWnd); // 窗口DC
  17. hscreen=CreateCompatibleDC(hwindow); // 地图绘制DC
  18. hmem=CreateCompatibleDC(hwindow); // 临时DC
  19. hmem2=CreateCompatibleDC(hwindow); // 临时DC
  20. // 用空位图初始化各个DC
  21. hmapnull=CreateCompatibleBitmap(hwindow,GAMEW*32*5,GAMEH*32);
  22. SelectObject(hscreen,hmapnull);
  23. SelectObject(hmem,hmapnull);
  24. SelectObject(hmem2,hmapnull);
  25. // 释放窗口DC
  26. ReleaseDC(hWnd, hwindow);
  27. // 位图初始化
  28. // 菜单背景图片,通关,GAMEOVER
  29. bmPre.Init(hInstance,IDB_BITMAP_PRE1,1,5);
  30. bmPre.SetDevice(hscreen,hmem,GAMEW*32,GAMEH*32);
  31. bmPre.SetPos(BM_USER,0,0);
  32. // 天空背景图片
  33. bmSky.Init(hInstance,IDB_BITMAP_MAP_SKY,1,4);
  34. bmSky.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);
  35. bmSky.SetPos(BM_USER,0,0);
  36. // 地图物品图片
  37. bmMap.Init(hInstance,IDB_BITMAP_MAP,1,1);
  38. bmMap.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);
  39. bmMap.InitAniList(mapsolid[0],mapsolid[1], sizeof(mapsolid[0])/sizeof(int),0);
  40. // (其它位图代码略)
  41. // 玩家图片初始化
  42. rmain.Init(hInstance,IDB_BITMAP_ROLE,5,1);
  43. rmain.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);
  44. // 字体初始化
  45. myfont.SetDevice(hscreen);
  46. // 游戏数据初始化
  47. gamemap.Init();
  48. // 玩家角色初始化坐标,数据初始化
  49. rmain.SetPos(BM_USER,3*32,8*32);
  50. rmain.InitRole(0,GAMEW*32*MAX_PAGE-32);
  51. // 文件检查
  52. if(!gamemap.LoadMap())
  53. {
  54. // 文件加载失败,设置游戏状态:文件错误
  55. gamemap.CodeErr(ERR_MAP_FILE);
  56. }
  57. // 计时器初始化
  58. c1.SetDevice(hscreen);
  59. // 计时器启动,每40毫秒一次WM_TIMER消息
  60. c1.Begin(hWnd, GAME_TIME_CLIP ,-1);
  61. // 设置显示方式,显示窗口
  62. ShowWindow(hWnd, nCmdShow);
  63. UpdateWindow(hWnd);
  64. }
上传的附件 cloud_download 基于WIN32 API实现的超级玛丽游戏-文档及源码.7z ( 556.59kb, 31次下载 )
error_outline 下载需要6点积分

发送私信

最合适你的人,是不需要奔跑着去追赶,拼了命去靠近的

5
文章数
9
评论数
eject