超级玛丽

MARIO CODE INSIDE html

超级玛丽制做揭秘 c++

制做 programking  博客 http://blog.csdn.net/programking程序员

 

 

  算法

 

1、       超级玛丽制做揭秘1工程开始... 2 编程

2、       超级玛丽制做揭秘2图片基类MYBITMAP. 4 api

3、       超级玛丽制做揭秘3游戏背景 MYBKSKY. 7 数组

4、       超级玛丽制做揭秘4图片显示 MYANIOBJ. 9 框架

5、       超级玛丽制做揭秘5魔法攻击 MYANIMAGIC. 13 ide

6、       超级玛丽制做揭秘6时钟控制 MYCLOCK. 14 函数

7、       超级玛丽制做揭秘7字体管理 MYFONT. 19

8、       超级玛丽制做揭秘8跟踪打印 FILEREPORT. 22

9、       超级玛丽制做揭秘9精灵结构struct ROLE. 24

10、       超级玛丽制做揭秘10子弹的显示和帧的刷新... 26

11、       超级玛丽制做揭秘11子弹运动和打怪... 27

12、       超级玛丽制做揭秘12旋风攻击,小怪运动,火圈... 29

十3、       超级玛丽制做揭秘13小怪和火圈,模板... 34

十4、       超级玛丽制做揭秘14爆炸效果,金币... 37

十5、       超级玛丽制做揭秘15金币提示,攻击提示... 41

十6、       超级玛丽制做揭秘16攻击方式切换... 43

十7、       超级玛丽制做揭秘17地图物品... 44

十8、       超级玛丽制做揭秘18背景物品... 47

十9、       超级玛丽制做揭秘19视图... 48

二10、       超级玛丽制做揭秘20地图切换... 50

二11、       超级玛丽制做揭秘21游戏数据管理... 53

二12、       超级玛丽制做揭秘22玩家角色类MYROLE. 58

二十3、       超级玛丽制做揭秘23玩家动做控制... 63

二十4、       超级玛丽制做揭秘24角色动画... 69

二十5、       超级玛丽制做揭秘25GAMEMAP 全局变量... 72

二十6、       超级玛丽制做揭秘26菜单控制 窗口缩放... 76

二十7、       超级玛丽制做揭秘27程序框架WinProc. 80

二十8、       InitInstance函数说明... 85

二十9、       后记... 87

 


 

类结构

 

图像层:

图像基类MYBITMAP

       游戏背景MYBKSKYàMYBITMAP

       游戏图片MYANIOBJàMYBITMAP

       魔法攻击MYANIMAGICàMYBITMAP

 

逻辑层:

游戏逻辑GAMEMAP

时钟处理MYCLOCK

字体处理MYFONT

跟踪打印FILEREPORT

玩家控制MYROLEàMYBITMAP

 

结构和表:

精灵结构ROLE

物品结构MapObject

地图信息表MAPINFO

 

 

1、            超级玛丽制做揭秘1工程开始

两个版本的超级玛丽下载量已超过5000次,谢谢你们支持。谁没法下载,请告诉我邮箱,我直接发。如今反映两个问题,一没有帮助文档,二代码注释太少。今天起,我揭秘制做过程,保证讲解到每一行代码,每个变量。

 

代码我已经发布,可见作这样一个游戏并不难。今天讲准备工做,也就是所须要的开发工具。代码编写调试:VC 6.0,美术工具:Windows自带的画图(开始-程序-附件-画图)。这是最简陋的开发工具,但已足够。最好再有Photoshop,记事本或UltraEdit等等你喜欢的文本编辑工具。

 

游戏代码分两部分,图像部分,逻辑部分。先说图像部分:图像分两种,矩形图片和不规则图片。工程中的PIC文件夹下,能够看到全部图像资源

 

矩形图片有:

地面、砖块、水管、血条、血条背景。

 

不规则图片有:

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

 

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

 

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

 

图像层的基类是:

 

class MYBITMAP

 

 void Init(HINSTANCE hInstance,int iResource,int row,int col);

 void SetDevice(HDC hdest,HDC hsrc,int wwin,int hwin);

 void Draw(DWORD dwRop);

 

 HBITMAP hBm;

//按照行列平均分红几个

 int inum;

 int jnum;

 

 int width;

 int height;

 

 HDC hdcdest;

 HDC hdcsrc;

 

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

 

还有,咱们最关心的问题是图片坐标,好比,不一样位置的砖块、精灵、金币,这些由逻辑层处理,之后再讲,先到这里吧。

 

2、            超级玛丽制做揭秘2图片基类MYBITMAP

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

 

今天先讲最基础的图像类 MYBITMAP

成员函数功能列表:

//功能 根据一个位图文件,初始化图像

//入参 应用程序实例句柄 资源ID 横向位图个数 纵向位图个数

void Init(HINSTANCE hInstance,int iResource,int row,int col);

//功能 设置环境信息

//入参 目的DC(要绘制图像的DC),临时DC,要绘制区域的宽

void SetDevice(HDC hdest,HDC hsrc,int wwin,int hwin);

//功能 设置图片位置

//入参 设置方法 横纵坐标

void SetPos(int istyle,int x,int y);

//功能 图片显示

//入参 图片显示方式

void Draw(DWORD dwRop);

//功能 图片缩放显示

//入参 横纵方向缩放比例

void Stretch(int x,int y);

//功能 图片缩放显示

//入参 横纵方向缩放比例 缩放图像ID(纵向第几个)

void Stretch(int x,int y,int id);

//功能 在指定位置显示图片

//入参 横纵坐标

void Show(int x,int y);

//功能 横向居中显示图片

//入参 纵坐标

void ShowCenter(int y);

//功能 将某个图片平铺在一个区域内

//入参 左上右下边界的坐标 图片ID(横向第几个)

void ShowLoop(int left,int top,int right,int bottom,int iframe);

//功能 不规则图片显示

//入参 横纵坐标 图片ID(横向第几个)

void ShowNoBack(int x,int y,int iFrame);

//功能 不规则图片横向平铺

//入参 横纵坐标 图片ID(横向第几个) 平铺个数

void ShowNoBackLoop(int x,int y,int iFrame,int iNum);

 

//动画播放

//功能 自动播放该图片的全部帧,函数没有实现,但之后确定要用:)

//入参

void ShowAni();

//功能 设置动画坐标

//入参 横纵坐标

void SetAni(int x,int y);

 

成员数据

//跟踪打印类

//     FILEREPORT f;

 

//图像句柄

       HBITMAP hBm;

 

//按照行列平均分红几个

       int inum;

       int jnum;

 

//按行列分割后,每一个图片的宽高(显然各个图片大小一致,派生后,这里的宽高已没有使用意义)

       int width;

       int height;

 

//屏幕宽高

       int screenwidth;

       int screenheight;

 

//要绘制图片的dc

       HDC hdcdest;

 

//用来选择图片的临时dc

       HDC hdcsrc; 

 

//当前位置

       int xpos;

       int ypos;

 

//是否处于动画播放中(功能没有实现)

       int iStartAni;

 

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

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

1.图片初始化:

//根据程序实例句柄,位图文件的资源ID,导入该位图,获得位图句柄

       hBm=LoadBitmap(hInstance,MAKEINTRESOURCE(iResource));

//获取该位图文件的相关信息

       GetObject(hBm,sizeof(BITMAP),&bm);

//根据横纵方向的图片个数,计算出每一个图片的宽高(对于超级玛丽,宽高信息由派生类处理)

       width=bm.bmWidth/inum;

       height=bm.bmHeight/jnum;

 

2.图片显示

各个图片的显示函数,大同小异,都要先选入一个临时DC,bitblt到要绘制的dc.矩形图片,能够直接用SRCCOPY的方式绘制.不规则图片,须要先用黑白图与目的区域相""(SRCAND),再用""的方法显示图像(SRCPAINT),这是一种简单的"去背"方法.

例以下面这个函数:

void MYBITMAP::ShowNoBack(int x,int y,int iFrame)

{

       xpos=x;

       ypos=y;

       SelectObject(hdcsrc,hBm);

       BitBlt(hdcdest,xpos,ypos,width,height/2,hdcsrc,iFrame*width,height/2,SRCAND); 

       BitBlt(hdcdest,xpos,ypos,width,height/2,hdcsrc,iFrame*width,0,SRCPAINT);        

}

 

 

3.图片缩放

StretchBlt的方法实现

void MYBITMAP::Stretch(int x,int y,int id)

{

       SelectObject(hdcsrc,hBm);

       StretchBlt(hdcdest,xpos,ypos,width*x,height*y,

              hdcsrc,0,id*height,

              width,height,

              SRCCOPY);  

}

 

在超级玛丽中的使用

在这个游戏中,哪些图像的处理是通关这个基类呢?只有一个:

MYBITMAP bmPre;

因为这个基类只能处理几个大小均等的图片,只有这些图片大小一致,且都是矩形:游戏开始前的菜单背景,操做信息的背景,每一关开始前的背景(此时显示LIFE x WORLD x),通关或游戏结束时显示的图片.5,将这5个图片,放在一个位图文件中,因而,这些图片的操做就作完了,代码以下:

 

//初始设置,InitInstance函数中

bmPre.Init(hInstance,IDB_BITMAP_PRE1,1,5);

bmPre.SetDevice(hscreen,hmem,GAMEW*32,GAMEH*32);

bmPre.SetPos(BM_USER,0,0);

 

//图片绘制,WndProc,前两个参数指横纵方向扩大2倍显示.

bmPre.Stretch(2,2,0);

bmPre.Stretch(2,2,4);

bmPre.Stretch(2,2,2);   

bmPre.Stretch(2,2,1);   

bmPre.Stretch(2,2,3);

 

图像控制部分,基类就讲到这里,欲知后事,下回分解.

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

3、            超级玛丽制做揭秘3游戏背景 MYBKSKY

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

 

技术原理:

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

 

成员函数功能列表:

class MYBKSKY:public MYBITMAP

{

public:

       MYBKSKY();

       ~MYBKSKY();

 

       //show

       //功能 显示一个背景.

       //入参

       void DrawRoll(); //循环补空

       //功能 显示一个背景,并缩放图片

       //入参 横纵方向缩放比例

       void DrawRollStretch(int x,int y);

       //功能 指定显示某一个背景,并缩放图片,游戏中用的就是这个函数

       //入参 横纵方向缩放比例 背景图片ID(纵向第几个)

       void DrawRollStretch(int x,int y,int id);

       //功能 设置图片位置

       //入参 新的横纵坐标

       void MoveTo(int x,int y);

       //功能 循环移动分割线

       //入参 分割线移动的距离

       void MoveRoll(int x);

 

       //data

       //分割线横坐标

       int xseparate;

};

 

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

void MYBKSKY::DrawRollStretch(int x,int y, int id)

{

       //选入句柄

       SelectObject(hdcsrc,hBm);

      

       //将分割线右边部分显示在当前位置

       StretchBlt(hdcdest,

              xpos,ypos,      //当前位置

              (width-xseparate)*x,height*y,              //缩放比例

              hdcsrc,

              xseparate,id*height,       //右边部分的坐标

              width-xseparate,height,  //右边部分的宽高

              SRCCOPY);

      

       //将分割线左边部分接在图片末尾

       StretchBlt(hdcdest,xpos+(width-xseparate)*x,ypos,

              xseparate*x,height*y,

              hdcsrc,0,id*height,

              xseparate,height,

              SRCCOPY);  

}

 

使用举例:

定义 MYBKSKY bmSky;

初始化

mario01/mario01.cpp(234):   bmSky.Init(hInstance,IDB_BITMAP_MAP_SKY,1,4);

mario01/mario01.cpp(235):   bmSky.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);

mario01/mario01.cpp(236):   bmSky.SetPos(BM_USER,0,0);

游戏过程当中显示

mario01/mario01.cpp(366):                        bmSky.DrawRollStretch(2,2,gamemap.mapinfo.iBackBmp);

每隔必定时间,移动分割线

mario01/mario01.cpp(428):                               bmSky.MoveRoll(SKY_SPEED);//云彩移动

 

如下两处与玩家角色有关:

当玩家切换到一张新地图时,刷新背景图片的坐标

mario01/gamemap.cpp(314):        bmSky.SetPos(BM_USER,viewx,0);

当玩家向右移动时,刷新背景图片的坐标

mario01/gamemap.cpp(473): bmSky.SetPos(BM_USER,viewx,0);

 

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

 

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

4、            超级玛丽制做揭秘4图片显示 MYANIOBJ

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

 

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

 

成员函数功能列表:

class MYANIOBJ:public MYBITMAP

{

public:

       MYANIOBJ();

       ~MYANIOBJ();

 

       //init list

       //功能 初始化宽度数组 高度数组 纵坐标数组 是否有黑白图

       //入参 宽度数组地址 高度数组地址 图片数量 是否有黑白图(0 没有, 1 )

       //(图片纵坐标信息由函数计算得出)

       void InitAniList(int *pw,int *ph,int inum,int ismask);

 

       //功能 初始化一些特殊的位图,例如各图片大小一致,或者有其余规律

       //入参 初始化方式 参数1 参数2

       //(留做之后扩展, 目的是为了省去宽高数组的麻烦)

       void InitAniList(int style,int a,int b);

 

       //show

       //功能 显示图片(不规则图片)

       //入参 横纵坐标(要显示的位置) 图片id(纵向第几个), 图片帧(横向第几个)

       void DrawItem(int x,int y,int id,int iframe);

       //功能 显示图片(矩形图片)

       //入参 横纵坐标(要显示的位置) 图片id(纵向第几个), 图片帧(横向第几个)

       void DrawItemNoMask(int x,int y,int id,int iframe);

       //功能 指定宽度, 显示图片的一部分(矩形图片)

       //入参 横纵坐标(要显示的位置) 图片id(纵向第几个), 显示宽度 图片帧(横向第几个)

       void DrawItemNoMaskWidth(int x,int y,int id,int w,int iframe);

       //功能 播放一个动画 即循环显示各帧

       //入参 横纵坐标(要显示的位置) 图片id(纵向第几个)

       void PlayItem(int x,int y,int id);

 

       //宽度数组 最多支持20个图片

       int wlist[20];

       //高度数组 最多支持20个图片

       int hlist[20];

       //纵坐标数组 最多支持20个图片

       int ylist[20];

 

       //动画播放时的当前帧

       int iframeplay;

};

 

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

使用举例:

游戏图片分红三类:地图物品、地图背景物体、精灵(即全部不规则图片)

MYANIOBJ bmMap;

MYANIOBJ bmMapBkObj;

MYANIOBJ bmAniObj;

初始化宽高信息

程序中定义一个二维数组,例如:

int mapani[2][10]={

{32,32,64,32,32,52,64,32,64,32},

{32,32,64,32,32,25,64,32,64,32},

};

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

 

1.     地图物品的显示:

定义

mario01/mario01.cpp(82):MYANIOBJ bmMap;

初始化

这一步加载位图

mario01/mario01.cpp(238):   bmMap.Init(hInstance,IDB_BITMAP_MAP,1,1);

这一步初始化DC

mario01/mario01.cpp(239):   bmMap.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);

这一步设置宽高信息, 图片为矩形

mario01/mario01.cpp(240):   bmMap.InitAniList(mapsolid[0],mapsolid[1], sizeof(mapsolid[0])/sizeof(int),0);

对象做为参数传给逻辑层, 显示地图物品

mario01/mario01.cpp(368):                        gamemap.Show(bmMap);

2.     血条的显示:

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

变量声明

mario01/gamemap.cpp(11):extern MYANIOBJ bmMap;

显示血条背景,指定图片宽度:最大生命值*单位生命值对应血条宽度

mario01/gamemap.cpp(522):        bmMap.DrawItemNoMaskWidth(xstart-1, ATTACK_TO_Y-1,ID_MAP_HEALTH_BK,

显示怪物血条,指定图片宽度:当前生命值*单位生命值对应血条宽度

mario01/gamemap.cpp(525):        bmMap.DrawItemNoMaskWidth(xstart, ATTACK_TO_Y,ID_MAP_HEALTH,

 

3.     地图背景物体的显示

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

定义

mario01.cpp(83):MYANIOBJ bmMapBkObj;

初始化并加载位图

mario01/mario01.cpp(242):   bmMapBkObj.Init(hInstance,IDB_BITMAP_MAP_BK,1,1);

设置dc

mario01/mario01.cpp(243):   bmMapBkObj.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);

设置各图片宽高信息

mario01/mario01.cpp(244):   bmMapBkObj.InitAniList(mapanibk[0],mapanibk[1],sizeof(mapanibk[0])/sizeof(int),1);

对象做为参数传给逻辑层, 显示地图背景物体

mario01/mario01.cpp(367):                        gamemap.ShowBkObj(bmMapBkObj);

4.     精灵的显示

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

定义

mario01.cpp(84):MYANIOBJ bmAniObj;

初始化加载位图

mario01/mario01.cpp(246):   bmAniObj.Init(hInstance,IDB_BITMAP_ANI,1,1);

设置dc

mario01/mario01.cpp(247):   bmAniObj.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);

设置宽高信息

mario01/mario01.cpp(248):   bmAniObj.InitAniList(mapani[0],mapani[1],sizeof(mapani[0])/sizeof(int),1);

菜单显示(即菜单文字左边的箭头)

mario01/mario01.cpp(341):                        gamemap.ShowMenu(bmAniObj);

对象做为参数传给逻辑层, 显示各个精灵

mario01/mario01.cpp(369):                        gamemap.ShowAniObj(bmAniObj);

 

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

 

5、            超级玛丽制做揭秘5魔法攻击 MYANIMAGIC

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

 

成员函数功能列表:

class MYANIMAGIC:public MYBITMAP

{

public:

       MYANIMAGIC();

       ~MYANIMAGIC();

 

       //init list

       //功能 初始化宽度数组 高度数组 纵坐标数组(必须有黑白图)

       //入参 宽度数组地址 高度数组地址 图片数量

       //(图片纵坐标信息由函数计算得出)

       void InitAniList(int *pw,int *ph,int inum);

       //功能 设置dc

       //入参 显示dc 临时dc(用于图片句柄选择) 临时dc(用于特效实现)

       void SetDevice(HDC hdest,HDC hsrc,HDC htemp);

 

       //show

       //功能 显示某个图片的某帧

       //入参 横纵坐标(显示位置) 图片id(纵向第几个) (横向第几个)

       void DrawItem(int x,int y,int id,int iframe);

 

       //宽度数组

       int wlist[20];

       //高度数组

       int hlist[20];

       //纵坐标数组

       int ylist[20];

 

       //用于特效的临时dc, 功能没有实现L

       HDC hdctemp;

};

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

 

使用举例

定义

mario01/mario01.cpp(87):MYANIMAGIC bmMagic;

初始化加载位图

mario01/mario01.cpp(250):   bmMagic.Init(hInstance,IDB_BITMAP_MAGIC,1,1);

设置dc

mario01/mario01.cpp(251):   bmMagic.SetDevice(hscreen,hmem, hmem2);

初始化宽高信息

mario01/mario01.cpp(252):   bmMagic.InitAniList(mapanimagic[0],mapanimagic[1],sizeof(mapanimagic[0])/sizeof(int));

 

变量声明

gamemap.cpp(22):extern MYANIMAGIC bmMagic;

在逻辑层中, 显示旋风图片

mario01/gamemap.cpp(568):                      bmMagic.DrawItem(xstart,ystart, 0, FireArray[i].iframe);

 

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

6、            超级玛丽制做揭秘6时钟控制 MYCLOCK

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

 

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

 

控制事件频率的方法:

1. 一秒内发生屡次

以游戏背景图片为例, 每秒移动5, 能够在iNum5,10,15,20,255个时间点上移动.iNum能够被5整除时,修改背景图片的坐标.

2. 一秒内发生一次

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

3. 多秒内发生一次

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

 

成员函数功能列表:(全部函数都是内联函数)

class MYCLOCK

{

public:

       //构造函数 初始化全部变量

       MYCLOCK()

       {

              iNum=0;//时间点

              iIsActive=0;//是否已经开始计时

              iNumShow=0;//计时秒数

              iElapse=100;//默认每100ms发一个WM_TIMER消息

              ishow=0; //是否显示时间

       }

       //析构函数 销毁计时器

       ~MYCLOCK()

       {

              Destroy();

       }

 

       //功能 开始计时, 产生WM_TIEMR消息的时间间隔为elapse.

       //     设置计时秒数(timetotal).

       //入参 窗口句柄 时间间隔 计时秒数

       void Begin(HWND hw,int elapse,int timetotal)

       {

              if(iIsActive)

                     return;//已经启动了,直接返回

 

              hWnd=hw;

              iElapse=elapse;

 

              SetTimer(hWnd,1,iElapse,NULL);

              iNum=1000/iElapse;//一秒钟的时间消息数量

              iNumShow=timetotal;

              iIsActive=1;

       }

       //功能 销毁计时器.

       //入参

       void Destroy()

       {

              if(iIsActive)

              {

                     iIsActive=0;

                     KillTimer(hWnd,1);

              }

       }

 

       //功能 重置计时秒数

       //入参 秒数

       void ReStart(int timetotal)

       {

              iNumShow=timetotal;   

              iNum=1000/iElapse;

              ishow=1;

       }

 

       //////////////////////////// 显示部分

       //功能 设置显示dc (在超级玛丽加强版中不显示时间)

       //入参 显示dc

       void SetDevice(HDC h)

       {

              hDC=h;

       }

       //功能 显示时间, TIME 秒数

       //入参 显示坐标

       void Show(int x,int y)

       {

              char temp[20]={0};

 

              if(!ishow)

                     return;

 

              //设置显示文本

              sprintf(temp,"TIME: %d  ",iNumShow);

              TextOut(hDC,x, y, temp,strlen(temp));

       }

 

       //功能 时间点减一

       //     若是到了计时秒数, 函数返回1, 不然返回0.

       //入参

       int DecCount()

       {

              iNum--;

              if(iNum==0)

              {

                     //过了一秒

                     iNum=1000/iElapse;

                     iNumShow--;

                     if(iNumShow<=0)

                     {

                            //不销毁计时器

                            return 1;

                     }

              }

              return 0;

       }

 

       //功能 时间点减一

       //     若是到了计时秒数, 函数返回1并销毁计时器, 不然返回0.

       //入参

       int Dec()

       {

              iNum--;

              if(iNum<=0)

              {

                     //过了一秒

                     iNum=1000/iElapse;

                     iNumShow--;

                     if(iNumShow<=0)

                     {

                            iNumShow=0;

                            Destroy();

                            return 1;

                     }

              }

              return 0;

       }

      

       //功能 设置是否显示

       //入参 1,显示; 0, 不显示

       void SetShow(int i)

       {

              ishow=i;

       }

 

public:

       //窗口句柄

       HWND hWnd;

       //显示dc

       HDC hDC;

 

       //时间点

       int iNum;

       //计时秒数

       int iNumShow;

       //消息时间间隔

       int iElapse;

       //是否开始计时

       int iIsActive;

       //是否显示

       int ishow;

};

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

使用举例:

定义

mario01.cpp(75):MYCLOCK c1;

设置显示dc

mario01/mario01.cpp(270):   c1.SetDevice(hscreen);

开始计时(计时秒数无效)

mario01/mario01.cpp(271):   c1.Begin(hWnd, GAME_TIME_CLIP ,-1);

选择游戏菜单,每隔必定时间,重绘屏幕,实现箭头闪烁

mario01/mario01.cpp(407):                        c1.DecCount();

mario01/mario01.cpp(408):                        if(0 == c1.iNum%MENU_ARROW_TIME)

屏幕提示LIFE,WORLD,若是达到计时秒数,进入游戏。

mario01/mario01.cpp(415):                        if(c1.DecCount())

进入游戏,计时300秒(无心义,在超级玛丽加强版中取消时间限制)

mario01/mario01.cpp(418):                               c1.ReStart(TIME_GAME_IN);                  

在游戏过程当中,每隔必定时间,处理游戏逻辑

mario01/mario01.cpp(425):                        c1.DecCount();

mario01/mario01.cpp(426):                        if(0 == c1.iNum%SKY_TIME)

mario01/mario01.cpp(430):                        gamemap.ChangeFrame(c1.iNum);//帧控制

mario01/mario01.cpp(434):                        gamemap.CheckAni(c1.iNum);//逻辑数据检测

玩家过关后,等待必定时间。

mario01/mario01.cpp(440):                        if(c1.DecCount())

玩家进入水管,等待必定时间。

mario01/mario01.cpp(448):                        if(c1.DecCount())

mario01/mario01.cpp(452):                               c1.ReStart(TIME_GAME_IN);                  

玩家失败后,等待必定时间。

mario01/mario01.cpp(459):                        if(c1.DecCount())

玩家通关后,等待必定时间。

mario01/mario01.cpp(466):                        if(c1.DecCount())

玩家生命值为0,游戏结束,等待必定时间。

mario01/mario01.cpp(474):                        if(c1.DecCount())

程序结束(窗口关闭),销毁计时器

mario01/mario01.cpp(518):                 c1.Destroy();

变量声明

gamemap.cpp(20):extern MYCLOCK c1;

游戏菜单中,选择“开始游戏”,显示LIFE,WORLD提示,计时两秒

mario01/gamemap.cpp(333):                      c1.ReStart(TIME_GAME_IN_PRE); //停顿两秒

进入水管,等待,计时两秒

mario01/gamemap.cpp(407):                                    c1.ReStart(TIME_GAME_PUMP_WAIT);

玩家过关,等待,计时两秒

mario01/gamemap.cpp(1083):              c1.ReStart(TIME_GAME_WIN_WAIT);

生命值为0,游戏结束,等待,计时三秒

mario01/gamemap.cpp(1116):              c1.ReStart(TIME_GAME_END); 

玩家失败,显示LIFE,WORLD提示,计时两秒

mario01/gamemap.cpp(1121):              c1.ReStart(TIME_GAME_IN_PRE);   

玩家失败,等待,计时两秒

mario01/gamemap.cpp(1140):       c1.ReStart(TIME_GAME_FAIL_WAIT);

 

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

 

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

7、            超级玛丽制做揭秘7字体管理 MYFONT

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

菜单中的文字包括:

       "制做: programking 20088",

       "操做:   Z:子弹   X:   方向键移动  W:默认窗口大小",

       "地图文件错误,请修正错误后从新启动程序。",

       "(上下键选择菜单,回车键确认)",

       "开始游戏",

       "操做说明",

       "博客: http://blog.csdn.net/programking",

       "(回车键返回主菜单)"

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

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

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

 

成员函数功能列表:

class MYFONT

{

public:

       //构造函数,初始化字体表”,5个字体句柄构成的数组,字体大小依次递增.

       MYFONT();

       ~MYFONT();

 

       //功能 设置显示文字的dc

       //入参 显示文字的dc句柄

       void SetDevice(HDC h);

       //功能 设置当前显示的字体

       //入参 字体表下标

       void SelectFont(int i);

       //功能 设置当前字体为默认字体

       //入参

       void SelectOldFont();

       //功能 在指定坐标显示字符串

       //入参 横纵坐标 字符串指针

       void ShowText(int x,int y,char *p);

       //功能 设置文字背景颜色,文字颜色

       //入参 文字背景颜色 文字颜色

       void SetColor(COLORREF cbk, COLORREF ctext);

       //功能 设置文字背景颜色,文字颜色

       //入参 文字背景颜色 文字颜色

       void SelectColor(COLORREF cbk, COLORREF ctext);

 

       //显示文字的dc

       HDC hdc;

       //字体表,包含5个字体句柄,字体大小依次是0,10,20,30,40

       HFONT hf[5];

       //默认字体

       HFONT oldhf;

       //color

       COLORREF c1;//字体背景色

       COLORREF c2;//字体颜色

};

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

 

使用举例:

定义

mario01/mario01.cpp(89):MYFONT myfont;

初始化设置显示dc

mario01/mario01.cpp(258):   myfont.SetDevice(hscreen);

地图文件错误:设置颜色,设置字体,显示提示文字

mario01/mario01.cpp(327):                        myfont.SelectColor(TC_WHITE,TC_BLACK);

mario01/mario01.cpp(328):                        myfont.SelectFont(0);

mario01/mario01.cpp(329):                        myfont.ShowText(150,290,pPreText[3]);

游戏开始菜单:设置字体,设置颜色,显示三行菜单文字

mario01/mario01.cpp(336):                        myfont.SelectFont(0);

mario01/mario01.cpp(337):                        myfont.SelectColor(TC_BLACK, TC_YELLOW_0);

mario01/mario01.cpp(338):                        myfont.ShowText(150,260,pPreText[4]);

mario01/mario01.cpp(339):                        myfont.ShowText(150,290,pPreText[5]);

mario01/mario01.cpp(340):                        myfont.ShowText(150,320,pPreText[6]);

游戏操做说明菜单:设置字体,设置颜色,显示四行说明文字

mario01/mario01.cpp(348):                        myfont.SelectFont(0);

mario01/mario01.cpp(349):                        myfont.SelectColor(TC_BLACK, TC_YELLOW_0);

mario01/mario01.cpp(350):                        myfont.ShowText(150,230,pPreText[8]);

mario01/mario01.cpp(351):                        myfont.ShowText(50,260,pPreText[1]);

mario01/mario01.cpp(352):                        myfont.ShowText(50,290,pPreText[0]);

mario01/mario01.cpp(353):                        myfont.ShowText(50,320,pPreText[7]);

 

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

void GAMEMAP::ShowInfo(HDC h)

{

       char temp[50]={0};

 

       SetTextColor(h, TC_WHITE);

       SetBkColor(h, TC_BLACK);

 

       sprintf(temp, "LIFE  : %d",iLife);

       TextOut(h, 220,100,temp,strlen(temp));

 

       sprintf(temp, "WORLD : %d",iMatch+1);

       TextOut(h, 220,130,temp,strlen(temp));

}

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

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

 

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

8、            超级玛丽制做揭秘8跟踪打印 FILEREPORT

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

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

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

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

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

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

成员函数功能列表:

class FILEREPORT

{

public:

       //功能 默认构造函数,建立日志trace.txt

       //入参

       FILEREPORT();

       //功能 指定日志文件名称

       //入参 日志文件名称

       FILEREPORT(char *p);

       //功能 析构函数,关闭文件

       //入参

       ~FILEREPORT();

      

       //功能 向日志文件写入字符串

       //入参 要写入的字符串

       void put(char *p);

       //功能 向日志文件写入一个字符串,两个整数

       //入参 字符串 整数a 整数b

       void put(char *p,int a,int b);

       //功能 计数器计数, 并写入一个提示字符串

       //入参 计时器id 字符串

       void putnum(int i,char *p);

      

       //功能 判断一个dc是否为null, 若是是,写入提示信息

       //入参 dc句柄 字符串

       void CheckDC(HDC h,char *p);

 

       //功能 设置显示跟踪信息的dc和文本坐标

       //入参 显示dc 横纵坐标

       void SetDevice(HDC h,int x,int y);

       //功能 设置要显示的跟踪信息

       //功能 提示字符串 整数a 整数b

       void Output(char *p,int a,int b);

       //功能 在屏幕上显示当前的跟踪信息

       void Show();

 

 

private:

       //跟踪文件指针

       FILE *fp;

 

       //计数器组

       int num[5];

 

       //显示dc

       HDC hshow;

       //跟踪文本显示坐标

       int xpos;

       int ypos;

       //当前跟踪信息

       char info[50];

 

};

函数具体实现很简单,只是简单的文件写入。要说明的是两部分,一:计数功能,有时要统计某个事情发生多少次,因此用一个整数数组,经过putnum让指定数字累加。二:显示功能,让跟踪信息,马上显示在屏幕上。

使用举例:

没有使用。程序最终完成,全部的跟踪打印都已删除。

 

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

9、            超级玛丽制做揭秘9精灵结构struct ROLE

今天开始讲逻辑层:struct ROLE

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

struct ROLE

{

 int x;//横坐标

 int y;//纵坐标

 int w;//图片宽度

 int h;//图片高度

 int id;//精灵id

 int iframe;//图片当前帧

 int iframemax;//图片最大帧数

 

 //移动部分

 int xleft;//水平运动的左界限

 int xright;//水平运动的右界限

 int movex;//水平运动的速度

 

 //人物属性

 int health;//精灵的生命值

 

 int show; //精灵是否显示

};

 

游戏中的子弹处理很是简单,包括存储、生成、销毁。

子弹的存储:全部的子弹存储在一个数组中,以下:

 struct ROLE FireArray[MAX_MAP_OBJECT];

 

其实,全部的动态元素都有从生成到销毁的过程。看一会儿弹是怎样产生的。

 

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

 

int GAMEMAP::KeyProc(int iKey)

 

  case KEY_Z: //FIRE

   if(iBeginFire)

    break;

   iTimeFire=0;

   iBeginFire=1; 

 

break;

 

这段代码的意思是:若是正在发子弹,代码结束。不然,设置iBeginFire1,表示开始发子弹。

子弹是在哪里发出的呢?

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

int GAMEMAP::CheckAni(int itimeclip)

发子弹的部分:

//发子弹

 if(iBeginFire)

 {

    //发子弹的时间到了(连续两个子弹要间隔必定时间)

  if(0 == iTimeFire )

  {

    //设置子弹属性:  可见, 动画起始帧:0

   FireArray[iFireNum].show=1;

   FireArray[iFireNum].iframe = 0;

   //子弹方向

//若是人物朝右

   if(0==rmain.idirec)

   {

//子弹向右

    FireArray[iFireNum].movex=1;

   }

   else

   {

//子弹向左

    FireArray[iFireNum].movex=-1;

   }

//区分攻击种类: 子弹,旋风

   switch(iAttack)

   {

//普通攻击: 子弹

   case ATTACK_NORMAL:

//精灵ID: 子弹

    FireArray[iFireNum].id=ID_ANI_FIRE;

//设置子弹坐标

    FireArray[iFireNum].x=rmain.xpos;

    FireArray[iFireNum].y=rmain.ypos;

 

//设置子弹宽高

    FireArray[iFireNum].w=FIREW;

    FireArray[iFireNum].h=FIREH;

 

//设置子弹速度: 方向向量乘以移动速度

    FireArray[iFireNum].movex*=FIRE_SPEED;

    break;

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

//移动数组游标

   iFireNum=(iFireNum+1)%MAX_MAP_OBJECT;

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

子弹已经显示在屏幕上, 接下来, 就是让它移动, 碰撞, 销毁. 且听下会分解.

 

10、            超级玛丽制做揭秘10子弹的显示和帧的刷新

感谢你们的支持,这些代码有很大优化的余地,有些代码甚至笨拙。我尽可能讲清楚我写时的思路。今天讲子弹的显示和动画帧的刷新,这个思路,能够应用的其余精灵上。

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

 

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

void GAMEMAP::ShowAniObj(MYANIOBJ & bmobj)

代码部分:

//显示子弹,魔法攻击

       for(i=0;i<MAX_MAP_OBJECT;i++)

       {

              if (FireArray[i].show)

              {

                     ystart=FireArray[i].y;

                     xstart=FireArray[i].x;

                    

                     switch(FireArray[i].id)

                     {

                     case ID_ANI_FIRE:

                            bmobj.DrawItem(xstart,ystart,FireArray[i].id,FireArray[i].iframe);

                            break;

子弹图片显示完成.

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

刷新帧的函数是void GAMEMAP::ChangeFrame(int itimeclip)

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

//子弹,攻击控制

       for(i=0;i<MAX_MAP_OBJECT;i++)

       {

              if(FireArray[i].show)

              {

                     switch(FireArray[i].id)

                     {

              default:

                            FireArray[i].iframe=1-FireArray[i].iframe;

                            break;

                     }

              }    

       }

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

子弹和小怪碰撞, 是游戏中的关键逻辑.网游里也是主要平常工做, 打怪. 消灭小怪, 也是这个游戏的所有乐趣, 固然,这是在我下一个版本的游戏没有开发出来的时候.我预计要加入更多的动态元素, 更大的地图. 只是这个计划被如今的工做搁置了. 那么, 这个关键的碰撞检测, 以及碰撞检测后的逻辑处理, 是怎样的呢? 且听下回分解.

 

11、           超级玛丽制做揭秘11子弹运动和打怪

感谢你们支持。书接上回。玩家按攻击键,生成子弹,存储在数组中,显示,接下来:

子弹运动,打怪。先说子弹是怎样运动的。思路:用一个函数不停地检测子弹数组,若是子弹可见,刷新子弹的坐标。

实现以下:

函数:int GAMEMAP::CheckAni(int itimeclip)

代码部分:

       //子弹移动

       for(i=0;i<MAX_MAP_OBJECT;i++)

       {

       //判断子弹是否可见

              if (FireArray[i].show)

              {

       //根据子弹的移动速度movex,修改子弹坐标.

       //(movex为正,向右移动;为负,向左移动,).

                     FireArray[i].x+=FireArray[i].movex;

                    

       //判断子弹是否超出了屏幕范围,若是超出,子弹消失(设置为不可见)

                     if( FireArray[i].x > viewx+VIEWW || FireArray[i].x<viewx-FIRE_MAGIC_MAX_W)

                     {

                            FireArray[i].show = 0;

                     }

              }

       }

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

打怪是怎样实现的呢?碰撞检测.思路:用一个函数不停地检测全部子弹,若是某个子弹碰到了小怪,小怪消失,子弹消失.

实现以下:

函数: int GAMEMAP::CheckAni(int itimeclip)

代码部分:

       //检测子弹和敌人的碰撞(包括魔法攻击)

       for(i=0;i<MAX_MAP_OBJECT;i++)

       {

       //判断小怪是否可见

              if(MapEnemyArray[i].show)

              {

              //检测全部子弹

                     for(j=0;j<MAX_MAP_OBJECT;j++)

                     {

                     //判断子弹是否可见

                            if (FireArray[j].show)

                            {

                            //判断子弹和小怪是否"碰撞"

                                   if(RECT_HIT_RECT(FireArray[j].x+FIRE_XOFF,

                                          FireArray[j].y,

                                          FireArray[j].w,

                                          FireArray[j].h,

                                          MapEnemyArray[i].x,

                                          MapEnemyArray[i].y,

                                          MapEnemyArray[i].w,

                                          MapEnemyArray[i].h)

                                          )

                                   {

                                   //若是碰撞,小怪消灭

                                          ClearEnemy(i);

 

                                          switch(iAttack)

                                          {

                                          case ATTACK_NORMAL:

                                                 //子弹消失

                                                 FireArray[j].show=0;

(说明:若是是旋风,在旋风动画帧结束后消失)

碰撞检测说明:

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

//矩形与矩形碰撞

#define RECT_HIT_RECT(x,y,w,h,x1,y1,w1,h1) ( (y)+(h)>(y1) && (y)<(y1)+(h1) && (x)+(w)>(x1) && (x)<(x1)+(w1) )

小怪的消失

函数: void GAMEMAP::ClearEnemy(int i)

代码部分:

       //小怪的生命值减一

       MapEnemyArray[i].health--;

 

       //若是小怪的生命值减到0, 小怪消失(设置为不可见)

       if(MapEnemyArray[i].health<=0)

       {

              MapEnemyArray[i].show=0;

       }

 

至此, 玩家按下攻击键,子弹生成,显示,运动,碰到小怪,子弹消失,小怪消失.这些功能所有完成.若是只作成这样,不算本事.攻击方式分两种:子弹,旋风.小怪包括:两种蘑菇兵,两种火圈.同时,火圈能产生两种蘑菇兵.而旋风的攻击效果明显高于普通子弹.是否是很复杂?其实,了无秘密.这是怎样作到的呢?且听下回分解.

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

12、           超级玛丽制做揭秘12旋风攻击,小怪运动,火圈

接上回。前面介绍了子弹的生成、显示、运动、碰撞、消失的过程。这个过程能够推广到其余精灵上。今天讲旋风、蘑菇兵、火圈。

做为魔法攻击方式的旋风,和子弹大同小异。

旋风的存储与子弹同存储在一个数组中,以下:

struct ROLE FireArray[MAX_MAP_OBJECT];

使用时,用id区分。

旋风生成函数:int GAMEMAP::CheckAni(int itimeclip)

代码部分:

       //发子弹

       if(iBeginFire)

       {

              if(0 == iTimeFire )

              {

                     FireArray[iFireNum].show=1;

                     FireArray[iFireNum].iframe = 0;

 

                     //子弹方向

                     if(0==rmain.idirec)

                     {

                            FireArray[iFireNum].movex=1;

                     }

                     else

                     {

                            FireArray[iFireNum].movex=-1;

                     }

 

                     switch(iAttack)

                     {

                     case ATTACK_MAGIC:

                            FireArray[iFireNum].id=ID_ANI_FIRE_MAGIC;

                            FireArray[iFireNum].x=rmain.xpos-ID_ANI_FIRE_MAGIC_XOFF;

                            FireArray[iFireNum].y=rmain.ypos-ID_ANI_FIRE_MAGIC_YOFF;

                            FireArray[iFireNum].w=FIRE_MAGIC_W;

                            FireArray[iFireNum].h=FIRE_MAGIC_H;

                            FireArray[iFireNum].movex=0;

                            break;    

                     }

                     //移动数组游标

                     iFireNum=(iFireNum+1)%MAX_MAP_OBJECT;

              }

              iTimeFire=(iTimeFire+1)%TIME_FIRE_BETWEEN;

       }

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

旋风的显示:

旋风在屏幕上的绘制和子弹相同,函数void GAMEMAP::ShowAniObj(MYANIOBJ & bmobj)。代码部分和子弹相同。

可是旋风的帧刷新有些特殊处理,函数void GAMEMAP::ChangeFrame(int itimeclip)

代码部分:

       //子弹,攻击控制

       for(i=0;i<MAX_MAP_OBJECT;i++)

       {

              //若是攻击(子弹、旋风)可见 

              if(FireArray[i].show)

              {

                     switch(FireArray[i].id)

                     {

                     case ID_ANI_FIRE_MAGIC:

                            //旋风当前帧加一

                            FireArray[i].iframe++;

                            //若是帧为2(即第三张图片)       ,图片坐标修正,向右移

                            if(FireArray[i].iframe == 2)

                            {

                                   FireArray[i].x+=FIRE_MAGIC_W;                                                             

                            }

                            //若是帧号大于3,即四张图片播放完,旋风消失,设置为不可见

                            if(FireArray[i].iframe>3)

                            {

                                   FireArray[i].show=0;

                            }

                            break;

                     }

              }    

至此,旋风显示,动画播放结束后消失.

旋风不涉及运动。碰撞检测的处理和子弹相同,惟一区别是:当旋风和小怪碰撞,旋风不消失。

函数为:int GAMEMAP::CheckAni(int itimeclip)

代码以下:

                                          switch(iAttack)

                                          {

                                          case ATTACK_NORMAL:

                                                 //子弹消失

                                                 FireArray[j].show=0;

                                                 break;

                                                 //旋风不消失

                                          default:

                                                 break;

                                          }

那么,再看小怪消失的函数void GAMEMAP::ClearEnemy(int i)

代码部分:     

       MapEnemyArray[i].health--;

       if(MapEnemyArray[i].health<=0)

       {

              MapEnemyArray[i].show=0;

       }

能够看到, 此时并不区分攻击方式. 但旋风存在的时间长(动画结束后消失),至关于屡次调用了这个函数,间接提升了杀伤力.

至此,两种攻击方式都已实现.

 

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

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

struct ROLE MapEnemyArray[MAX_MAP_OBJECT];

       int iMapEnemyCursor;

小怪生成:

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

;enemy

21 6 1 1 0 15 24

23 6 1 1 0 15 24

48 7 2 2 6 0 0

68 5 2 2 8 0 0

各个参数是什么意义呢?看一下加载函数就全明白了.函数:int GAMEMAP::LoadMap()

代码部分:

//若是文件没有结束后

while(temp[0]!='#' && !feof(fp))

       {

       //读入小怪数据 横坐标 纵坐标 id 运动范围左边界 右边界

              sscanf(temp,"%d %d %d %d %d %d %d",

                     &MapEnemyArray[i].x,

                     &MapEnemyArray[i].y,

                     &MapEnemyArray[i].w,

                     &MapEnemyArray[i].h,

                     &MapEnemyArray[i].id,

                     &MapEnemyArray[i].xleft,

                     &MapEnemyArray[i].xright);

             

              //坐标转换.乘以32

              MapEnemyArray[i].x*=32;

              MapEnemyArray[i].y*=32;

              MapEnemyArray[i].w*=32;

              MapEnemyArray[i].h*=32;

              MapEnemyArray[i].xleft*=32;

              MapEnemyArray[i].xright*=32;          

              MapEnemyArray[i].show=1;

              //设置移动速度(,表示向左)

              MapEnemyArray[i].movex=-ENEMY_STEP_X;

              //动画帧

              MapEnemyArray[i].iframe=0;

              //动画最大帧

              MapEnemyArray[i].iframemax=2;

             

              //设置生命值

              switch(MapEnemyArray[i].id)

              {

              case ID_ANI_BOSS_HOUSE:

                     MapEnemyArray[i].health=BOSS_HEALTH;

                     break;

 

              case ID_ANI_BOSS_HOUSE_A:

                     MapEnemyArray[i].health=BOSS_A_HEALTH;

                     break;

 

              default:

                     MapEnemyArray[i].health=1;

                     break;

              }

 

              //将火圈存储在数组的后半段,数值长30, BOSS_CURSOR15

              if ( i<BOSS_CURSOR

                      && (  MapEnemyArray[i].id == ID_ANI_BOSS_HOUSE

                         || MapEnemyArray[i].id == ID_ANI_BOSS_HOUSE_A) )

              {

                     //move data to BOSS_CURSOR

                     MapEnemyArray[BOSS_CURSOR]=MapEnemyArray[i];

                     memset(&MapEnemyArray[i],0,sizeof(MapEnemyArray[i]));                  

                     i=BOSS_CURSOR;

              }

 

              i++;

              //读取下一行地图数据

              FGetLineJumpCom(temp,fp);      

       }

看来比生成子弹要复杂一些.尤为是火圈, 为何要从第15个元素上存储?由于,火圈要不停地生成蘑菇兵,因此"分区管理",数值前一半存储蘑菇兵,后一半存储火圈.

小怪和火圈是怎样显示、运动的呢?火圈怎样不断产生新的小怪?且听下回分解。

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

十3、           超级玛丽制做揭秘13小怪和火圈,模板

小怪的显示问题.

蘑菇兵和火圈处于同一个数组,很简单.

函数:void GAMEMAP::ShowAniObj(MYANIOBJ & bmobj)

代码:

       //显示敌人

       for(i=0;i<MAX_MAP_OBJECT;i++)

       {

              if (MapEnemyArray[i].show)

              {

                     bmobj.DrawItem(MapEnemyArray[i].x,MapEnemyArray[i].y,

                            MapEnemyArray[i].id,MapEnemyArray[i].iframe);     

              }

       }

一样,如同图片处理所讲,显示一个图片,只须要坐标,id,.

 

帧刷新和小怪运动.

函数:void GAMEMAP::ChangeFrame(int itimeclip)

代码:

       //移动时间:每隔一段时间ENEMY_SPEED,移动一下

       if(0 == itimeclip% ENEMY_SPEED)

       {

              for(i=0;i<MAX_MAP_OBJECT;i++)

              {

                     //若是小怪可见

                     if(MapEnemyArray[i].show)

                     {

                            //帧刷新

                            MapEnemyArray[i].iframe=(MapEnemyArray[i].iframe+1)%MapEnemyArray[i].iframemax;

                           

                            switch(MapEnemyArray[i].id)

                            {

                            case ID_ANI_ENEMY_NORMAL:

                            case ID_ANI_ENEMY_SWORD:

                                   //蘑菇兵移动(士兵,刺客)

                                   MapEnemyArray[i].x+=MapEnemyArray[i].movex;

                                  

                                   //控制敌人移动:向左移动到左边界后,移动速度movex改成向右。移动到右边界后,改成向左。

                                   if(MapEnemyArray[i].movex<0)

                                   {

                                          if(MapEnemyArray[i].x<=MapEnemyArray[i].xleft)

                                          {

                                                 MapEnemyArray[i].movex=ENEMY_STEP_X;                                     

                                          }

                                   }

                                   else

                                   {

                                          if(MapEnemyArray[i].x>=MapEnemyArray[i].xright)

                                          {

                                                 MapEnemyArray[i].movex=-ENEMY_STEP_X;                      

                                          }

                                   }

                                   break;

                            }

至此,全部小怪不停移动。(火圈的movex0,不会移动)

 

碰撞检测和消失。

在前面的子弹、旋风的碰撞处理中已讲过。碰撞后,生命值减小,减为0后,消失。

 

火圈.

火圈会产生新的蘑菇兵,怎样实现的呢?思路:不断地检测火圈是否出如今屏幕中,出现后,生成蘑菇兵。

函数:int GAMEMAP::CheckAni(int itimeclip)

代码部分:

       //若是在显示范围以内,则设置显示属性

       for(i=0;i<MAX_MAP_OBJECT;i++)

       {

              //判断是否在屏幕范围内

              if ( IN_AREA(MapEnemyArray[i].x, viewx, VIEWW) )

              {

                     //若是有生命值,设置为可见

                     if(MapEnemyArray[i].health)

                     {

                            MapEnemyArray[i].show=1;

 

                            switch(MapEnemyArray[i].id)

                            {

                            //普通级火圈

                            case ID_ANI_BOSS_HOUSE:

                                   //每隔一段时间, 产生新的敌人

                                   if(itimeclip == TIME_CREATE_ENEMY)

                                   {

                                          MapEnemyArray[iMapEnemyCursor]=gl_enemy_normal;

                                          MapEnemyArray[iMapEnemyCursor].x=MapEnemyArray[i].x;

                                          MapEnemyArray[iMapEnemyCursor].y=MapEnemyArray[i].y+32;

 

                                          //移动游标

                                          iMapEnemyCursor=(iMapEnemyCursor+1)%BOSS_CURSOR;                             

                                   }

                                   break;

                                   //下面是战斗级火圈,处理类似

                            }

                     }

              }

              else

              {

                     //不在显示范围内,设置为不可见

                     MapEnemyArray[i].show=0;

              }           

       }

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

 

再说一下模板.

这里的模板不是c++的模板.听说template技术已发展到艺术的境界.游戏中用到的和template无关,而是全局变量.以下:

//普通蘑菇兵

struct ROLE gl_enemy_normal=

{

       0,

       0,

       32,

       32,

       ID_ANI_ENEMY_NORMAL,

       0,

       2,

       0,

       0,

       -ENEMY_STEP_X,//speed

       1,

       1

};

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

游戏的主要逻辑完成.此外,还有金币,爆炸效果等其余动态元素,它们是怎么实现的?且听下回分解。

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

十4、           超级玛丽制做揭秘14爆炸效果,金币

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

struct MapObject

{

       int x;

       int y;

       int w;

       int h;

       int id;

       int iframe;

       int iframemax;//最大帧数

       int show; //是否显示

};

存储问题。

爆炸效果仍然使用数组加游标的方法,以下:

       struct MapObject BombArray[MAX_MAP_OBJECT];

       int iBombNum;

 

生成。

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

函数:void GAMEMAP::ClearEnemy(int i)

代码部分:

       //生成

BombArray[iBombNum].show=1;

       BombArray[iBombNum].id=ID_ANI_BOMB;

       BombArray[iBombNum].iframe=0;

       BombArray[iBombNum].x=MapEnemyArray[i].x-BOMB_XOFF;

       BombArray[iBombNum].y=MapEnemyArray[i].y-BOMB_YOFF;

       //修改数组游标

       iBombNum=(iBombNum+1)%MAX_MAP_OBJECT;

 

显示。

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

函数:void GAMEMAP::ShowAniObj(MYANIOBJ & bmobj)

代码部分:

       for(i=0;i<MAX_MAP_OBJECT;i++)

       {

              if (BombArray[i].show)

              {

                     ystart=BombArray[i].y;

                     xstart=BombArray[i].x;

                     bmobj.DrawItem(xstart,ystart,BombArray[i].id, BombArray[i].iframe);    

              }

       }

 

帧刷新。

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

函数:void GAMEMAP::ChangeFrame(int itimeclip)

代码部分:

for(i=0;i<MAX_MAP_OBJECT;i++)

       {

              if(BombArray[i].show)

              {

                     BombArray[i].iframe++;

//当第四张图片显示完毕,设置为不可见。

                     if(BombArray[i].iframe>3)

                     {

                            BombArray[i].show=0;

                     }

              }    

       }

 

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

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

 

金币。

金币的处理比小怪更简单。当玩家和金币碰撞后,金币消失,增长金钱数量。

存储问题。

用数组加游标的方法(下一个版本中封装起来),以下:

       struct MapObject MapCoinArray[MAX_MAP_OBJECT];

       int iCoinNum;

 

金币的生成。

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

6 5 32 32 3    

7 5 32 32 3    

8 5 32 32 3    

9 5 32 32 3    

18 4 32 32 3   

19 4 32 32 3   

20 4 32 32 3

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

函数:int GAMEMAP::LoadMap()

代码部分:

       while(temp[0]!='#' && !feof(fp))

       {

              sscanf(temp,"%d %d %d %d %d",

                     &MapCoinArray[i].x,

                     &MapCoinArray[i].y,

                     &MapCoinArray[i].w,

                     &MapCoinArray[i].h,

                     &MapCoinArray[i].id);                

              MapCoinArray[i].show=1;

              MapCoinArray[i].iframe=0;

              //坐标转换,乘以32

              MapCoinArray[i].x*=32;

              MapCoinArray[i].y*=32;

              //设置这个动画元件的最大帧

              switch(MapCoinArray[i].id)

              {

              case ID_ANI_COIN:

                     MapCoinArray[i].iframemax=4;

                     break;

              }

              i++;

              iCoinNum++;

              //读取下一行数据

              FGetLineJumpCom(temp,fp);      

       }

 

金币显示:

和小怪的显示方法相同.

函数:void GAMEMAP::ShowAniObj(MYANIOBJ & bmobj)

代码:

       //显示金币,和其余物品

       for(i=0;i<iCoinNum;i++)

       {

              ystart=MapCoinArray[i].y;

              xstart=MapCoinArray[i].x;

              bmobj.DrawItem(xstart,ystart,MapCoinArray[i].id, MapCoinArray[i].iframe);  

       }

 

金币帧刷新:

和小怪的帧刷新方法相同.

函数:void GAMEMAP::ChangeFrame(int itimeclip)

代码:

       for(i=0;i<MAX_MAP_OBJECT;i++)

              {

                     //若是金币可见,帧加一

                     if(MapCoinArray[i].show)

                     {                                               MapCoinArray[i].iframe=(MapCoinArray[i].iframe+1)%MapCoinArray[i].iframemax;

                     }

              }

 

金币碰撞检测:

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

函数:int GAMEMAP::CheckAni(int itimeclip)

代码:

       for(i=0;i<iCoinNum;i++)

       {

              tempx=MapCoinArray[i].x;

              tempy=MapCoinArray[i].y;

             

              if ( IN_AREA(tempx, viewx-32, VIEWW) )

              {

                     //玩家坐标是rmain.xpos rmain.ypos

                     if(    RECT_HIT_RECT(rmain.xpos,

                            rmain.ypos,

                            32,32,

                            tempx,

                            tempy,

                            MapCoinArray[i].w,MapCoinArray[i].h)

                            )

                     {

                            switch(MapCoinArray[i].id)

                            {

                            case ID_ANI_COIN:

                                   //增长金钱数量

                                   iMoney+=10; 

                                   //金币消失

                                   ClearCoin(i);

                                   break;

                            }                         

                            return 0;

                     }           

              }

       } // end of for

 

金币消失:

和小怪的消失不同.不须要设置show0, 而是直接删除元素.即数组移动的方法.

函数:void GAMEMAP::ClearCoin(int i)

代码:

       //检查合法性

       if(i<0 || i>=iCoinNum)

              return;

       //减小一个金币,或者减小一个其余物品

       for(;i<iCoinNum;i++)

       {

              MapCoinArray[i]=MapCoinArray[i+1];

       }

       //修改数量

       iCoinNum--;

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

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

附:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

十5、           超级玛丽制做揭秘15金币提示,攻击提示

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

金币提示:

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

金币的碰撞检测函数:int GAMEMAP::CheckAni(int itimeclip)

代码:

       for(i=0;i<iCoinNum;i++)

       {

              //判断玩家是否碰到金币

                            switch(MapCoinArray[i].id)

                            {

                            case ID_ANI_COIN:

                                   //碰到金币

                                   iMoney+=10; 

                                   //金币消失,显示+10字样

                                   ClearCoin(i);

                                   break;

金币消失函数:void GAMEMAP::ClearCoin(int i)

代码:

       switch(MapCoinArray[i].id)

       {

       case ID_ANI_COIN:

              //碰到了金币,显示+10字样. 和爆炸效果的处理同样, 只是图片id不一样

              BombArray[iBombNum].show=1;

              BombArray[iBombNum].id=ID_ANI_COIN_SCORE;

              BombArray[iBombNum].iframe=0;

              BombArray[iBombNum].x=MapCoinArray[i].x-COIN_XOFF;

              BombArray[iBombNum].y=MapCoinArray[i].y-COIN_YOFF;

              iBombNum=(iBombNum+1)%MAX_MAP_OBJECT;

              break;

       }

 

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

存储:

       //攻击对象提示

       char AttackName[20];//攻击对象名称

       int iAttackLife;//攻击对象当前生命值

       int iAttackMaxLife;//攻击对象最大生命值

 

提示信息设置:在小怪被攻击的时候,设置提示信息。

函数:void GAMEMAP::ClearEnemy(int i)

代码:

//设置攻击对象生命值

       iAttackLife=MapEnemyArray[i].health;

       switch(MapEnemyArray[i].id)

       {

       case ID_ANI_BOSS_HOUSE:

              //设置名称

              strcpy(AttackName,"普通级火圈");

              //设置最大生命值

              iAttackMaxLife=BOSS_HEALTH;

其余攻击对象处理类似。

 

显示:

函数: void GAMEMAP::ShowOther(HDC h)

代码:

//若是攻击对象生命值不为0, 显示提示信息

       if(iAttackLife)

       {

              //输出名称

              TextOut(h,viewx+ATTACK_TO_TEXT_X,

                     ATTACK_TO_TEXT_Y,AttackName,strlen(AttackName));

             

              //显示血条

              xstart=viewx+ATTACK_TO_X-iAttackMaxLife*10;

              //按最大生命值显示一个矩形, 做为背景

              bmMap.DrawItemNoMaskWidth(xstart-1, ATTACK_TO_Y-1,ID_MAP_HEALTH_BK,

                     iAttackMaxLife*BMP_WIDTH_HEALTH, 0);

              //按当前生命值对应的宽度, 显示一个红色矩形

              bmMap.DrawItemNoMaskWidth(xstart, ATTACK_TO_Y,ID_MAP_HEALTH,

                     iAttackLife*BMP_WIDTH_HEALTH, 0);

       }

提示信息功能完成。

 

金钱数量显示:和攻击提示位于同一个函数void GAMEMAP::ShowOther(HDC h)

代码:

       sprintf(temp,"MONEY: %d",iMoney);

       TextOut(h,viewx+20,20,temp,strlen(temp));

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

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

十6、           超级玛丽制做揭秘16攻击方式切换

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

思路:把它放到存储金币的数组中,用id区别。碰撞检测时,若是是金币,金币消失,若是是武器包,攻击方式切换。

存储:和金币位于同一个数组MapCoinArray

生成:由地图文件加载。好比第一关的地图文件数据:

25 4 52 25 5

各参数含义:横坐标 纵坐标 图片id

加载函数:int GAMEMAP::LoadMap()

代码:和金币的加载相同,惟一区别是金币图片有4帧,武器包只有2帧,设置以下:

MapCoinArray[i].iframemax=2;

 

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

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

 

碰撞检测:和金币的处理相同。

函数:int GAMEMAP::CheckAni(int itimeclip)

代码:若是是武器包,设置新的攻击方式,武器包消失。

                            switch(MapCoinArray[i].id)

                            {

                            case ID_ANI_ATTACK:       

                                   //设置新的攻击方式

                                   iAttack=ATTACK_MAGIC;

                                   //武器包消失

                                   ClearCoin(i);

                                   break;

                            }                         

 

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

至此,攻击系统,金币系统,提示信息,武器切换,所有完成。只须要一个地图把全部的物品组织起来,构成一个虚拟世界,呈如今玩家眼前。地图怎样处理?且听下回分解。

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

十7、           超级玛丽制做揭秘17地图物品

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

超级玛丽的地图能够当作是一个二维的格子。每一个格子的大小是32x32像素。

游戏窗口大小为12个格子高,16个格子宽。

游戏地图宽度是游戏窗口的5倍,即12个格子高,5x16个格子宽。

地图物品有哪些呢?地面,砖块,水管。

 

存储问题:

先看一下存储结构:

struct MapObject

{

       int x;

       int y;

       int w;

       int h;

       int id;

       int iframe;

       int iframemax;//最大帧数

       int show; //是否显示

};

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

0 9 10 3 0

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

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

地图全部物品仍然用数组+游标的方法存储,以下:

       struct MapObject MapArray[MAX_MAP_OBJECT];

       int iMapObjNum;

 

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

加载函数:int GAMEMAP::LoadMap()

代码:

       while(temp[0]!='#' && !feof(fp))

       {

              //读取一个物品

              sscanf(temp,"%d %d %d %d %d",

                     &MapArray[i].x,

                     &MapArray[i].y,

                     &MapArray[i].w,

                     &MapArray[i].h,

                     &MapArray[i].id);               

              MapArray[i].show=0;

              iMapObjNum++;

              i++;

              //读取下一个物品

              FGetLineJumpCom(temp,fp);      

       }

 

地图显示:和物品显示同样,只是地面和砖块须要双重循环。

函数:void GAMEMAP::Show(MYANIOBJ & bmobj)

代码:对于每一个宽w格,高h格的地面、砖块,须要把单个地面砖块平铺w*h次,因此用双重循环。

       for(i=0;i<iMapObjNum;i++)

       {

              ystart=MapArray[i].y*32;

              switch(MapArray[i].id)

              {

              //进出水管

              case ID_MAP_PUMP_IN:

              case ID_MAP_PUMP_OUT:

                     xstart=MapArray[i].x*32;

                     bmobj.DrawItemNoMask(xstart, ystart, MapArray[i].id, 0);                    

                     break;

      

              default:                 

                     for(j=0;j<MapArray[i].h;j++)

                     {

                            xstart=MapArray[i].x*32;

                            for(k=0;k<MapArray[i].w;k++)

                            {

                                   bmobj.DrawItemNoMask(xstart, ystart, MapArray[i].id, 0);                    

                                   xstart+=32;

                            }

                            ystart+=32;                         

                     } // end of for

                     break;

说明:水管是一个单独完整的图片,直接显示,不须要循环。

 

帧刷新:地面、砖块、水管都是静态图片,不涉及。

 

碰撞检测:保证玩家顺利地行走。若是玩家不踩在物品上,则不停地下落。

函数:int GAMEMAP::CheckRole()

代码:

              //检测角色是否站在某个物体上

              for(i=0;i<iMapObjNum;i++)

              {

                     //玩家的下边线,是否和物品的上边线重叠

                     if( LINE_ON_LINE(rmain.xpos,

                            rmain.ypos+32,

                            32,

                            MapArray[i].x*32,

                            MapArray[i].y*32,

                            MapArray[i].w*32)

                            )

                     {

                            //返回1,表示玩家踩在这个物品上

                            return 1;

                     }

              }

              //角色开始下落

              rmain.movey=1;    

              rmain.jumpx=0;//此时要清除跳跃速度,不然将变成跳跃,而不是落体

              return 0;

至此,地图物品的功能完成,且听下回分解。

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

十8、           超级玛丽制做揭秘18背景物品

背景物品更简单,包括草丛,树木,河流,win标志。这些背景物品只须要显示,不涉及逻辑处理。

存储:用数组+游标的方法,以下:

       struct MapObject MapBkArray[MAX_MAP_OBJECT];

       int iMapBkObjNum;

第一关的背景物品数据:

17 5 3 2 0(草丛)

76 7 3 2 1win标志)

10 10 3 2 2(河流)

含义和地图物品相同。

 

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

加载函数:int GAMEMAP::LoadMap()

代码:    while(temp[0]!='#' && !feof(fp))

       {

              sscanf(temp,"%d %d %d %d %d",

                     &MapBkArray[i].x,

                     &MapBkArray[i].y,

…...

              MapBkArray[i].iframe=0;

              iMapBkObjNum++;

              i++;

              //下一个物品

              FGetLineJumpCom(temp,fp);      

       }

 

显示:

函数:void GAMEMAP::ShowBkObj(MYANIOBJ & bmobj)

代码:

       for(i=0;i<iMapBkObjNum;i++)

       {

              bmobj.DrawItem(xstart,ystart,MapBkArray[i].id,ibkobjframe);  

       }

 

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

函数:void GAMEMAP::ChangeFrame(int itimeclip)

代码:

       if(0 == itimeclip% WATER_SPEED)

       {

              ibkobjframe=1-ibkobjframe;  

至此,背景物品的功能完成。地图怎样跟随玩家移动呢? 且听下回分解。

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

十9、           超级玛丽制做揭秘19视图

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

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

 

初始化:

函数:BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)

代码:

// hwindow是游戏窗口的DC句柄

       hwindow=GetDC(hWnd);

       // hscreen是整个地图对应的DC

       hscreen=CreateCompatibleDC(hwindow);

       //创建一个整个地图大小(5倍窗口宽)的空位图,选入hscreen

       hmapnull=CreateCompatibleBitmap(hwindow,GAMEW*32*5,GAMEH*32);

       SelectObject(hscreen,hmapnull);

 

显示。

函数:WndProc

代码:

              case WM_PAINT:

                     // hwindow是游戏窗口的DC句柄

                     hwindow = BeginPaint(hWnd, &ps);

                     SelectObject(hscreen,hmapnull);

 

                     case GAME_IN:

                            //显示天空

                            bmSky.DrawRollStretch(2,2,gamemap.mapinfo.iBackBmp);

                            //显示背景物品

                            gamemap.ShowBkObj(bmMapBkObj);

                            //显示地图物品

                            gamemap.Show(bmMap);

                            //显示动态元素

                            gamemap.ShowAniObj(bmAniObj);

                            //显示提示信息

                            gamemap.ShowOther(hscreen);

                            //显示玩家

                            rmain.Draw();

                            break;    

                    

if(gamemap.iScreenScale)

                     {

                            //窗口大小调整功能,代码略

                     }

                     else

                     {                  

       //从整个地图的DC, 截取当前视图区域的图像,绘制到窗口

       BitBlt(hwindow,0,0,GAMEW*32,GAMEH*32,hscreen,gamemap.viewx,0,SRCCOPY);

                     }

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

 

视图坐标刷新:

思路:用一个函数不停地检测,玩家角色和视图左边界的距离,超过特定值,把视图向右移。

函数:void GAMEMAP::MoveView()

代码:若是玩家坐标和视图左边界的距离大于150,移动视图。

       if(rmain.xpos - viewx > 150)

       {

              viewx+=ROLE_STEP;  

              //判断视图坐标是否达到最大值(地图宽度减去一个窗口宽度)

              if(viewx>(mapinfo.viewmax-1)*GAMEW*32)

                     viewx=(mapinfo.viewmax-1)*GAMEW*32;       

       }

至此,地图跟随玩家移动。每一关的地图是怎样切换的呢?且听下回分解。

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

二10、           超级玛丽制做揭秘20地图切换

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

普通地图的切换:

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

存储:    int iMatch;

初始化:       iMatch=0;0表示第一关)

 

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

函数:int GAMEMAP::IsWin()

代码:

       //判断玩家的坐标是否到达地图终点(横坐标大于等于地图宽度)

if(rmain.xpos >= MAX_PAGE*GAMEW*32 )

       {           

              // iMatch增长

              iMatch=mapinfo.iNextMap;

 

              if(iMatch>=MAX_MATCH)

              {

                     //若是iMatch大于关卡数量(即经过最后一关),加载第一关的数据,代码略

              }

              else

              {

                     //没有通关

                     InitMatch();//初始化游戏数据

                     //设置玩家角色坐标,初始化玩家角色

                     rmain.SetPos(BM_USER,3*32,8*32);

                     rmain.InitRole(0,GAMEW*32*MAX_PAGE-32);              

                     //加载下一关的地图

                     LoadMap();  

              }

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

              iMatch=mapinfo.iNextMap;

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

 

隐藏地图的切换:

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

*0

//0关的地图数据

*1

//1关的地图数据

*4

//4关的地图数据

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

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

0关:下一关编号,隐藏地图编号

1关:下一关编号,隐藏地图编号

4关:下一关编号,隐藏地图编号

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

(1)从“地图信息表”中读取当前关卡的的地图信息。

(2)当玩家到达地图终点,读取“下一关”编号;玩家进入水管,读取“隐藏地图编号”。

 

游戏的地图信息结构:

struct MAPINFO

{

       int iNextMap;

       int iSubMap;

};

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

struct MAPINFO allmapinfo[]={

{1,3},

{2,4},

{MAX_MATCH,-1, },

{-1,0},

{-1,1}

};

对应的逻辑信息为:

0关的下一关是第1关,从水管进入第3关。

1关的下一关是第2关,从水管进入第4关。

2关(最后一关)没有下一关(MAX),没有从水管进入的地图。

3关没有下一关,从水管进入第0关。

4关没有下一关,从水管进入第1关。

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

 

地图信息的存储:       struct MAPINFO mapinfo;

地图信息的读取:

函数:void GAMEMAP::InitMatch()

代码:每一关的游戏开始前,都要用这个函数初始化游戏数据。包括读取地图信息,以下:

       mapinfo=allmapinfo[iMatch];

 

玩家到达地图终点的检测:即int GAMEMAP::IsWin(),经过代码:

              iMatch=mapinfo.iNextMap;

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

 

玩家进入水管的检测:

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

函数:int GAMEMAP::KeyProc(int iKey)

代码:

              case VK_DOWN:

                     for(i=0;i<iMapObjNum;i++)

                     {

                            //判断玩家是否站在一个地图物品上

                            if( LINE_IN_LINE(玩家坐标,地图物品坐标))

                            {                         

                                   //这个物品是水管

                                   if(MapArray[i].id == ID_MAP_PUMP_IN)

                                   {

                                          //设置游戏状态:进入水管

                                          iGameState=GAME_PUMP_IN;

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

              case WM_TIMER:

                     switch(gamemap.iGameState)

                     {

                            case GAME_PUMP_IN:

                            if(c1.DecCount())

                            {

                                   //若是GAME_PUMP_IN状态结束,加载隐藏地图。

                                   gamemap.ChangeMap();

                                  

是否是复杂一些?确实,它能够简化。我想这仍是有好处,它容易扩展。这仍然是我最初的构思,这是一个代码框架。

看一下ChangeMap的处理:

函数:void GAMEMAP::ChangeMap()

代码:

       //读取隐藏地图编号

       iMatch=mapinfo.iSubMap;

       //游戏初始化

       InitMatch();                                      

       //加载地图

       LoadMap();

可见,ChangeMap的简单很简单。由于,LoadMap的接口只是iMatch,我只要保证iMatch在不一样状况下设置正确,地图就会正确地加载。我把iMatch从初版游戏中的++,改为从“地图信息表”中读取,这样,隐藏地图的功能实现了。

举例:若是下一个版本中,一个地图,有多个水管包含隐藏地图?怎样实现呢?很简单,把地图信息表改为“水管编号-地图信息”的对应结构,功能实现。

至此,地图切换实现。可是,地图切换中,还有其它的游戏数据要刷新,怎样处理呢?且听下回分解。

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

二11、          超级玛丽制做揭秘21游戏数据管理

进入每一关以前,须要对全部游戏数据初始化。进入隐藏地图,一样须要初始化。并且,从隐藏地图返回上层地图,还要保证玩家出如今“出水管”处。地图数据、玩家数据、视图数据,都要设置正确。

 

全部的游戏数据,即封装在gamemap中的数据,分红以下几种:

场景数据:包含当前关卡的地图,全部精灵,金币,提示信息。

视图数据:视图窗口坐标。

玩家数据:玩家角色的我的信息,例如金钱数量,攻击方式,游戏次数。

1.       场景数据:

       int iGameState;//当前游戏状态

       int iMatch;      //当前关卡

各类精灵的数组:

       struct MapObject MapArray[MAX_MAP_OBJECT]; //地图物品

       struct MapObject MapBkArray[MAX_MAP_OBJECT];     //地图背景物品

       struct ROLE MapEnemyArray[MAX_MAP_OBJECT];      //小怪

       struct MapObject MapCoinArray[MAX_MAP_OBJECT]; //金币

       struct ROLE FireArray[MAX_MAP_OBJECT]; //子弹

       struct MapObject BombArray[MAX_MAP_OBJECT];       //爆炸效果

       //当前关卡的地图信息

       struct MAPINFO mapinfo;

       //图片帧

int ienemyframe;    //小怪图片帧

       int ibkobjframe;     //背景图片帧

       //玩家攻击

       int iTimeFire;//两个子弹的间隔时间

       int iBeginFire;//是否正在发子弹

       //攻击对象提示

       char AttackName[20];//攻击对象名称

       int iAttackLife;//攻击对象生命值

       int iAttackMaxLife;// 攻击对象最大生命值

2. 视图数据:

       int viewx;//视图起始坐标

3. 玩家数据:

       int iMoney;     //金钱数量

       int iAttack;      //攻击方式

       int iLife;         //玩家游戏次数

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

 

游戏数据处理:

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

程序运行前,初始化;

过关后,初始化,再加载下一关地图;

失败后,初始化,再加载当前地图;

 

1.       游戏程序运行,全部游戏数据初始化。

函数:BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)

代码:把全部游戏数据初始化

gamemap.Init();

 

游戏初始化函数:void GAMEMAP::Init()

代码:

       //设置游戏初始状态

       iGameState=GAME_PRE;

       //设置当前关卡

       iMatch=0;

//设置玩家数据 玩家游戏次数,金钱数量,攻击种类

       iLife=3;

       iMoney=0;

       iAttack=ATTACK_NORMAL;

       //设置视图坐标

       viewx=0;

       //初始化场景数据

       InitMatch();

 

场景数据初始化函数:void GAMEMAP::InitMatch()

代码:把全部游戏数据清除(置0

       memset(MapArray,0,sizeof(MapArray));

       memset(BombArray,0,sizeof(BombArray));

       ienemyframe=0;

       iFireNum=0;

       ……

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

 

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

过关检测函数:int GAMEMAP::IsWin()

代码:

       //判断玩家是否到达地图终点

if(rmain.xpos >= MAX_PAGE*GAMEW*32 )

       {

              //读取下一关地图编号

              iMatch=mapinfo.iNextMap;

              if(iMatch>=MAX_MATCH)

              {

                     //若是所有经过

                     Init();      //初始化全部数据

                     LoadMap();   //加载地图

              }

              else

              {

                     InitMatch();    //初始化场景数据

                     //设置玩家坐标

                     rmain.SetPos(BM_USER,3*32,8*32);

                     rmain.InitRole(0,GAMEW*32*MAX_PAGE-32);              

                     //加载下一关的地图

                     LoadMap();  

              }

 

3.       若是玩家失败,从新加载当前地图。

失败检测函数:int GAMEMAP::IsWin()

代码:若是玩家碰到了小怪,或者踩到火圈,游戏失败,调用Fail()进一步处理。

       //检测角色和敌人的碰撞

       for(i=0;i<MAX_MAP_OBJECT;i++)

       {

              if(MapEnemyArray[i].show)

              {

                     if(HLINE_ON_RECT(玩家坐标 小怪坐标))

                     {

                            if(0 == rmain.movey)

                            {

                                   //玩家在行走过程当中,碰到小怪,游戏失败

                                   Fail();

                            }

                            else

                            {

                                   //玩家在下落过程当中,碰到火圈,游戏失败

                                   switch(MapEnemyArray[i].id)

                                   {

                                   case ID_ANI_BOSS_HOUSE:

                                   case ID_ANI_BOSS_HOUSE_A:

                                          Fail();

       ……

//玩家到达地图底端(掉入小河),游戏失败

if(rmain.ypos > GAMEH*32)

       {

      

              Fail();

              return 0;

       }                                       

 

失败处理函数:void GAMEMAP::Fail()

代码:

       //玩家游戏次数减1

       iLife--;

       //设置游戏状态

       iGameState=GAME_FAIL_WAIT;

 

GAME_FAIL_WAIT状态结束后,调用函数void GAMEMAP::Fail_Wait()加载地图。

函数:void GAMEMAP::Fail_Wait()

代码:

       if(    iLife <=0)

       {

              //游戏次数为0,从新开始,初始化全部数据

              Init();

       }

       else

       {

              //还能继续游戏

       }

       //设置玩家坐标

       rmain.SetPos(BM_USER,3*32,8*32);

       rmain.InitRole(0,GAMEW*32*MAX_PAGE-32);

       //加载当前地图

       LoadMap();  

 

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

 

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

1. 刷新玩家坐标。

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

2. 刷新视图坐标。

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

3. 刷新背景图片的坐标。

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

隐藏地图加载函数:void GAMEMAP::ChangeMap()

代码:

       //初始化视图坐标

       viewx=0;

       //获取隐藏地图编号

       iMatch=mapinfo.iSubMap;

       //初始化场景数据

       InitMatch();

       //设置玩家坐标                                       

       rmain.SetPos(BM_USER,mapinfo.xReturnPoint*32,mapinfo.yReturnPoint*32);

       //玩家角色初始化

rmain.InitRole(0,GAMEW*32*MAX_PAGE-32);

       //设定视图位置

       if(rmain.xpos - viewx > 150)

       {

              SetView(mapinfo.xReturnPoint*32-32);//往左让一格

              if(viewx>(mapinfo.viewmax-1)*GAMEW*32)

                     viewx=(mapinfo.viewmax-1)*GAMEW*32;

       }

       //设定人物活动范围

       rmain.SetLimit(viewx, GAMEW*32*MAX_PAGE);

       //设定背景图片坐标

       bmSky.SetPos(BM_USER,viewx,0);

       //加载地图

       LoadMap();

}

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

struct MAPINFO

{

       int iNextMap;  //过关后的下一关编号

       int iSubMap;   //进入水管后的地图编号

       int xReturnPoint;    //出水管的横坐标

       int yReturnPoint;    //出水管的纵坐标

       int iBackBmp;        //背景图片ID

       int viewmax;          //视图最大宽度

};

struct MAPINFO allmapinfo[]={

{1,3,66,7,0,5},

{2,4,25,4,1,5},

{MAX_MATCH,-1,-1,-1,2,5},

{-1,0,3,8,3,1},

{-1,1,3,8,3,2}

};

说明:

0关:

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

3关:

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

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

 

各个动态元素,地图的各类处理都已完成,只须要让玩家控制的小人,走路,跳跃,攻击,进出水管。玩家的动做控制怎样实现?且听下回分解。

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

二12、          超级玛丽制做揭秘22玩家角色类MYROLE

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

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

 

成员函数功能列表:

class MYROLE:public MYBITMAP

{

public:

       //构造函数,析构函数

       MYROLE();

       ~MYROLE();

       //初始化部分

       //功能 初始化玩家信息

       //入参 玩家运动范围的左边界 右边界()

       void InitRole(int xleft, int xright);

       //功能 设置玩家运动范围

       //入参 玩家运动范围的左边界 右边界()

       void SetLimit(int xleft, int xright);

 

       //图片显示部分

       //功能 显示玩家角色图片(当前坐标 当前帧)

       //入参

       void Draw();

       //功能 显示玩家角色图片

       //入参 指定的横坐标 纵坐标

       void Draw(int x,int y,int iframe);

       //功能 刷新帧,该函数没有使用, 帧刷新的功能在其它地方完成

       //入参

       void ChangeFrame();

       //功能 设置玩家状态. 该函数没有使用

       //入参 玩家状态

       void SetState(int i);

       //动做部分

       //功能 玩家角色移动

       //入参

       void Move();

       //功能 玩家角色跳跃. 该函数没有使用

       //入参

       void Jump();

       //功能 移动到指定地点

       //入参 指定地点横坐标 纵坐标

       void MoveTo(int x,int y);

       //功能 从当前位置移动一个增量

       //入参 横坐标增量 纵坐标增量

       void MoveOffset(int x,int y);

       //功能 向指定地点移动一段距离(移动增量是固定的)

       //入参 指定地点横坐标 纵坐标

       void MoveStepTo(int x,int y);

       //动画部分

       //功能 播放动画

       //入参

       void PlayAni();

       //功能 设置动画方式

       //入参 动画方式

       void SetAni(int istyle);

       //功能 判断是否正在播放动画, 若是正在播放动画,返回1.不然,返回0

       //入参

       int IsInAni();

 

       //数据部分

       //玩家状态, 该变量没有使用

       int iState;

       //图片数据

//玩家当前帧

       int iFrame;

       //动做控制数据

       //玩家活动范围: 左边界 右边界(只有横坐标)

       int minx;

       int maxx;

       //运动速度

       int movex;//正值,向右移动

       int movey;//正值,向下移动

       //跳跃

       int jumpheight;//跳跃高度

       int jumpx;//跳跃时, 横向速度(正值,向右移动)

       //玩家运动方向

       int idirec;

       //动画数据

       int iAniBegin;//动画是否开始播放

       int iparam1;//动画参数

       int iAniStyle;//动画方式

};

 

各个功能的实现:

键盘响应。

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

消息处理函数函数:WndProc

代码:

              case WM_KEYDOWN:

                     if(gamemap.KeyProc(wParam))   

                            InvalidateRect(hWnd,NULL,false);

                     break;

              case WM_KEYUP:

                     gamemap.KeyUpProc(wParam);                                     

                     break;

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

KEYDOWN处理函数:int GAMEMAP::KeyProc(int iKey)

代码:不一样游戏状态下,按键功能不一样。

       switch(iGameState)

       {

       case GAME_PRE://选择游戏菜单

              switch(iKey)

              {

              case 0xd://按下回车键

                     switch(iMenu)

                     {

                     case 0:    //菜单项0“开始游戏”

                            c1.ReStart(TIME_GAME_IN_PRE); //计时两秒

                            iGameState=GAME_IN_PRE;//进入游戏LIFE/WORLD提示状态

                            break;

                           

                     case 1:    //菜单项1“操做说明”

                            SetGameState(GAME_HELP); //进入游戏状态“操做说明”,显示帮助信息

                            break;

                     }

                     break;

                    

              case VK_UP:          //按方向键“上”,切换菜单项

                     iMenu=(iMenu+1)%2;

                     break;

              case VK_DOWN:         //按方向键“下”,切换菜单项

                     iMenu=(iMenu+1)%2;

                     break;

              }

              return 1;

 

       case GAME_HELP: //游戏菜单项“操做说明”打开

              switch(iKey)

              {

              case 0xd:        //按回车键,返回游戏菜单

                     SetGameState(GAME_PRE);        //设置游戏状态:选择菜单

                     break;                  

              }

              return 1;

 

       case GAME_IN: //游戏进行中

              //若是人物正在播放动画,拒绝键盘响应

              if(rmain.IsInAni())

              {

                     break;

              }

              //根据方向键, X, Z, 触发移动,跳跃,攻击等功能

              switch(iKey)

              {

              case VK_RIGHT:                        

              case VK_LEFT:

              case VK_DOWN:

              case KEY_X: //                

              case KEY_Z: //FIRE

 

              //秘籍J

case 0x 7a ://按键F11, 直接切换攻击方式

                     iAttack=(iAttack+1)%ATTACK_MAX_TYPE;

                     break;

              case 0x7b://按键F12 直接通关(游戏进行中才能够,即游戏状态GAME_IN

                     rmain.xpos = MAX_PAGE*GAMEW*32;

                     break;

              }

              break;

       }

       return 0;

}

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

菜单选择GAME_PRE

操做说明菜单打开GAME_HELP

游戏进行中GAME_IN

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

 

按键“抬起”的处理:

函数:void GAMEMAP::KeyUpProc(int iKey)

代码:按键抬起,只须要清除一些变量。

       switch(iKey)

       {

       //松开方向键“左右”,清除横向移动速度

       case VK_RIGHT:   

              rmain.movex=0;

              break;

       case VK_LEFT:

              rmain.movex=0;

              break;

       case KEY_X: //松开跳跃键,无处理

              break;

 

       case KEY_Z: //松开攻击键,清除变量iBeginFire,表示中止攻击

              iBeginFire=0;

              break;

 

       case KEY_W: //W,调整窗口为默认大小

              MoveWindow(hWndMain,

                     (wwin-GAMEW*32)/2,

                     (hwin-GAMEH*32)/2,

                     GAMEW*32,

                     GAMEH*32+32,

                     true);                   

 

              break;

       }

这就是游戏的全部按键处理。

 

显示问题:

函数:void MYROLE::Draw()

代码:

//判断是否播放动画,即iAniBegin1

if(iAniBegin)

       {

              //显示动画帧

              PlayAni();      

       }

       else

       {

              //显示当前图片

              SelectObject(hdcsrc,hBm);

              BitBlt(hdcdest,xpos,ypos,

                     width,height/2,

                     hdcsrc,iFrame*width,height/2,SRCAND);          

              BitBlt(hdcdest,xpos,ypos,

                     width,height/2,

                     hdcsrc,iFrame*width,0,SRCPAINT);  

       }

 

玩家角色是怎样行走和跳跃的呢?动画播放怎样实现?且听下回分解。

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

二十3、          超级玛丽制做揭秘23玩家动做控制

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

相关属性:

行走:横向速度为movex,纵向不移动。

跳跃:横向速度为jumpx,纵向速度为movey。当前跳跃高度jumpheight

运动方向:idirec

 

思路:

第一步:玩家按键,按键处理函数设置这些属性。按键松开,清除动做属性。

第二步:用一个函数不停检测这些变量,控制玩家移动。

 

1. 按键触发:

按键处理函数:int GAMEMAP::KeyProc(int iKey)

代码:

              switch(iKey)

              {

              case VK_RIGHT:    //按右

                     //判断是否正在跳跃, 即纵向速度不为0

                     if(rmain.movey!=0)

                     {

                            //跳跃过程当中, 设置横向速度, 方向向右, 大小为4像素

                            rmain.jumpx=4;

                     }

                     rmain.movex=4;     //设置横向速度, 方向向右, 大小为4像素

                     rmain.idirec=0;       //设置玩家方向, 向右

                     break;

                    

              case VK_LEFT:  //按左

                     //若是是跳跃过程当中, 设置横向速度, 方向向左, 大小为4像素

                     if(rmain.movey!=0)

                     {

                            rmain.jumpx=-4;

                     }

                     rmain.movex=-4;           //设置横向速度, 方向向左, 大小为4像素

                     rmain.idirec=1;              //设置玩家方向, 向左

                     break;

             

              case KEY_X: //X键跳

                     //若是已是跳跃状态,不做处理,代码中断

                     if(rmain.movey!=0)

                            break;

                     //设置纵向速度,方向向上(负值),大小为13

                     rmain.movey=-SPEED_JUMP;

                     //将当前的横向速度,赋值给“跳跃”中的横向速度

                     rmain.jumpx=rmain.movex;

                     break;

                    

              case KEY_Z: //FIRE

                     if(iBeginFire)

                            break;     //若是已经开始攻击,代码中断

                     iTimeFire=0;   //初始化子弹间隔时间

                     iBeginFire=1;  //1,表示开始攻击

                     break;

按键松开处理函数:void GAMEMAP::KeyUpProc(int iKey)

代码:

       //松开左右键,清除横向速度

       case VK_RIGHT:   

              rmain.movex=0;

              break;

       case VK_LEFT:

              rmain.movex=0;

              break;

       case KEY_X: //

//不能清除跳跃的横向速度jumpx

//例如,移动过程当中起跳,整个跳跃过程当中都要有横向速度

              break;

       case KEY_Z: //FIRE

              iBeginFire=0;         //中止攻击

              break;

 

2.       控制移动。

动做检测函数:WndProc

代码:时间片的处理中,根据不一样状态,调用各类检测函数。

              case WM_TIMER:

                     switch(gamemap.iGameState)

                     {

                     case GAME_IN:

                            rmain.Move();//人物移动

                            ……

                            break;

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

函数:void MYROLE::Move()

代码:

       if(0 == movey)

       {

              //若是不是跳跃, 横向移动

              MoveOffset(movex, 0);

       }

       else

       {

              //跳跃, 先横向移动, 再纵向移动

              MoveOffset(jumpx, 0);

              MoveOffset(0, movey);

       }

 

       //玩家帧控制 纠错法

       if(movex<0 && iFrame<3)

       {

              iFrame=3;       //若是玩家向左移动, 而图片向右, 则设置为3(4张图片)

       }

       if(movex>0 && iFrame>=3)

       {

              iFrame=0;       //若是玩家向右移动, 而图片向右, 则设置为0(1张图片)

       }

       //帧刷新

       if(movex!=0)

       {

              if(0==idirec)

                     iFrame=1-iFrame;   //若是方向向右, 图片循环播放0,1

              else

                     iFrame=7-iFrame; //若是方向向左, 图片循环播放3,4

       }

       if(movey!=0)

       {

//跳跃过程当中, 帧设置为0(向右),3(向左)

//帧刷新后, 从新设置帧, 就实现了跳跃过程当中, 图片静止

              iFrame=idirec*3;   

       }

 

       //跳跃控制

       if(movey<0)

       {

              //向上运动(纵向速度movey为负值)

              jumpheight+=(-movey);        //增长跳跃高度

             

              //重力影响,速度减慢

              if(movey<-1)

              {

                     movey++;

              }

 

              //到达顶点后向下落, 最大跳跃高度为JUMP_HEIGHT * 32, 3个格子的高度

              if(jumpheight >= JUMP_HEIGHT * 32)

              {    

                     jumpheight =  JUMP_HEIGHT * 32;   //跳跃高度置为最大

                     movey=4;       //纵向速度置为4, 表示开始下落

              }

       }

       else if(movey>0)

       {

              //下落过程, 跳跃高度减小

              jumpheight -= movey;

              //重力影响,速度增大

              movey++;                    

       }

玩家移动函数:void MYROLE::MoveOffset(int x,int y)

代码:根据增量设置坐标

       //横纵增量为0,不移动,代码结束

       if(x==0 && y==0)

              return;

 

       //若是碰到物体,不移动,代码结束

       if(!gamemap.RoleCanMove(x,y))

              return;

       //修改玩家坐标

       xpos+=x;

       ypos+=y;

       //判断是否超出左边界

       if(xpos<minx)

              xpos=minx;    //设置玩家坐标为左边界

       //判断是否超出右边界

       if(xpos>maxx)

              xpos=maxx;   

 

3.       碰撞检测

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

函数:int GAMEMAP::RoleCanMove(int xoff, int yoff)

代码:

       int canmove=1;//初始化, 1表示能移动

for(i=0;i<iMapObjNum;i++)

       {

              if( RECT_HIT_RECT(玩家坐标加增量,地图物品坐标))

              {

                     //碰到物体,不能移动

                     canmove=0;

                     if(yoff<0)

                     {

                            //纵向增量为负(即上升运动), 碰到物体开始下落

                            rmain.movey=1;

                     }

                     if(yoff>0)

                     {

//纵向增量为正(即下落运动), 碰到物体, 中止下落

                            rmain.jumpheight=0;//清除跳跃高度

                            rmain.movey=0;//清除纵向速度

                            rmain.ypos=MapArray[i].y*32-32;//纵坐标刷新,保证玩家站在物品上

                     }

                     break;

              }

       }

       return canmove;

 

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

函数:int GAMEMAP::CheckRole()

代码:

       if(rmain.movey == 0 )

       {

              //检测角色是否站在某个物体上

              for(i=0;i<iMapObjNum;i++)

              {

                     //玩家的下边线,是否和物品的上边线重叠

                     if( LINE_ON_LINE(rmain.xpos,

                            rmain.ypos+32,

                            32,

                            MapArray[i].x*32,

                            MapArray[i].y*32,

                            MapArray[i].w*32)

                            )

                     {

                            //返回1,表示玩家踩在这个物品上

                            return 1;

                     }

              }

              //角色开始下落

              rmain.movey=1;    

              rmain.jumpx=0;//此时要清除跳跃速度,不然将变成跳跃,而不是落体

              return 0;

 

至此,玩家在这个虚拟世界能够作出各类动做,跳跃,行走,攻击。加强版中,加入了水管,玩家在进出水管,就须要动画。怎么实现,且听下回分解。

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

二十4、          超级玛丽制做揭秘24角色动画

玩家在进出水管的时候,须要进入水管、从水管中升起两个动画。当动画播放结束后,切换到新的地图。

动画播放过程当中,禁止键盘响应,即玩家不能控制移动。

 

1.       玩家进水管。

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

#define ID_MAP_PUMP_IN 9

#define ID_MAP_PUMP_OUT 10

玩家进入水管的检测:

函数:int GAMEMAP::KeyProc(int iKey)

代码:检测玩家按“下”,若是玩家站在进水管上,开始播放动画

              case VK_DOWN:

                     for(i=0;i<iMapObjNum;i++)

                     {

                            if( LINE_IN_LINE(玩家坐标的下边界,地图物品的上边界))

                            {                         

                                   //判断是否站在进水管上

                                   if(MapArray[i].id == ID_MAP_PUMP_IN)

                                   {

                                          //若是站在设置角色动画方式,向下移动

                                          rmain.SetAni(ROLE_ANI_DOWN);

                                          iGameState=GAME_PUMP_IN;//设置游戏状态:进水管

                                          c1.ReStart(TIME_GAME_PUMP_WAIT);//计时2

                                   }

                            }

                     }

                     break;

动画设置函数:void MYROLE::SetAni(int istyle)

代码:

       iAniStyle=istyle;     //设置动画方式

       iparam1=0;     //参数初始化为0

       iAniBegin=1;   //表示动画开始播放

说明: iparam1是动画播放中的一个参数, 根据动画方式不一样,能够有不一样的含义.

 

2.       动画播放

玩家角色显示函数:void MYROLE::Draw()

代码:

//判断是否播放动画,即iAniBegin1

       if(iAniBegin)

       {

              PlayAni();       //播放当前动画

       }

动画播放函数:void MYROLE::PlayAni()

代码:根据不一样的动画方式,播放动画

       switch(iAniStyle)

       {

       case ROLE_ANI_DOWN:

              //玩家进入水管的动画,iparam1表示降低的距离

              if(iparam1>31)

              {

                     //降低距离超过31(即图片高度),玩家彻底进入水管,无需图片显示

                     break;                  

              }

              //玩家没有彻底进入水管,截取图片上半部分,显示到当前的坐标处

              SelectObject(hdcsrc,hBm);

              BitBlt(hdcdest,

                     xpos,ypos+iparam1,

                     width,height/2-iparam1,

                     hdcsrc,

                     iFrame*width,height/2,SRCAND);             

              BitBlt(hdcdest,

                     xpos,ypos+iparam1,

                     width,height/2-iparam1,

                     hdcsrc,

                     iFrame*width,0,SRCPAINT);      

              //增长降低高度

              iparam1++;           

              break;

      

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

函数:WndProc

代码:在时间片的处理中,当GAME_PUMP_IN状态结束,切换地图,并设置玩家动画:从水管中上升。

                     case GAME_PUMP_IN:

                            if(c1.DecCount())

                            {

                                   gamemap.ChangeMap();//切换地图

                                   gamemap.SetGameState(GAME_IN);   //设置游戏状态

                                   c1.ReStart(TIME_GAME_IN);            //计时300

                                   rmain.SetAni(ROLE_ANI_UP);            //设置动画,图片上升

                            }

                            InvalidateRect(hWnd,NULL,false);

                            break;

4.       从水管中上升

动画播放函数:void MYROLE::PlayAni()

代码:根据不一样的动画方式,播放动画

       switch(iAniStyle)

       {

       case ROLE_ANI_UP:

              if(iparam1>31)

              {

                     //若是上升距离超过31(图片高度),动画结束

                     break;                  

              }

              //人物上升动画,截取图片上部,显示到当前坐标

              SelectObject(hdcsrc,hBm);

              BitBlt(hdcdest,

                     xpos,ypos+32-iparam1,

                     width,iparam1,

                     hdcsrc,

                     iFrame*width,height/2,SRCAND);             

              BitBlt(hdcdest,

                     xpos,ypos+32-iparam1,

                     width,iparam1,

                     hdcsrc,

                     iFrame*width,0,SRCPAINT);      

              //增长上升距离

              iparam1++;

//若是上升距离超过31(图片高度)

              if(iparam1>31)

              {

                     iAniBegin=0;   //动画结束,清除动画播放状态

              }

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

按键响应函数:int GAMEMAP::KeyProc(int iKey)

代码:

       case GAME_IN:

              //若是人物正在播放动画,拒绝键盘响应

              if(rmain.IsInAni())

              {

                     break;

              }

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

:

超级玛丽初版源码连接:http://download.csdn.net/source/497676

超级玛丽加强版源码连接:http://download.csdn.net/source/584350

 

 

二十5、          超级玛丽制做揭秘25GAMEMAP 全局变量

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

成员函数功能列表:

class GAMEMAP

{

public:

       //加载地图

       int LoadMap();

       //初始化全部游戏数据

void Init();

相关文章
相关标签/搜索