小游戏超级玛丽制作毕业论文设计,app,java,android,ios,课程,精灵

4004
    


来源:
Licence:
联系:
分类:
平台:
环境:
大小:
更新:
标签:
联系方式 :
免费下载 ×

下载APP,支持永久资源免费下载

限免产品服务请联系qq:1585269081

下载APP
免费下载 ×

下载APP,支持永久资源免费下载

下载APP 免费下载
下载 ×

下载APP,资源永久免费


如果出现不能下载的情况,请联系站长,联系方式在下方。

免费下载 ×

下载论文助手APP,资源永久免费

免费获取

如果你已经登录仍然出现不能下载的情况,请【点击刷新】本页面或者联系站长


工程目录

目  录

一、	超级玛丽制作揭秘1工程开始	2
二、	超级玛丽制作揭秘2图片基类MYBITMAP	4
三、	超级玛丽制作揭秘3游戏背景 类MYBKSKY	7
四、	超级玛丽制作揭秘4图片显示 类MYANIOBJ	9
五、	超级玛丽制作揭秘5魔法攻击 类MYANIMAGIC	13
六、	超级玛丽制作揭秘6时钟控制 类MYCLOCK	14
七、	超级玛丽制作揭秘7字体管理 类MYFONT	19
八、	超级玛丽制作揭秘8跟踪打印 类FILEREPORT	22
九、	超级玛丽制作揭秘9精灵结构struct ROLE	24
十、	超级玛丽制作揭秘10子弹的显示和帧的刷新	26
十一、	超级玛丽制作揭秘11子弹运动和打怪	27
十二、	超级玛丽制作揭秘12旋风攻击,小怪运动,火圈	29
十三、	超级玛丽制作揭秘13小怪和火圈,模板	34
十四、	超级玛丽制作揭秘14爆炸效果,金币	37
十五、	超级玛丽制作揭秘15金币提示,攻击提示	41
十六、	超级玛丽制作揭秘16攻击方式切换	43
十七、	超级玛丽制作揭秘17地图物品	44
十八、	超级玛丽制作揭秘18背景物品	47
十九、	超级玛丽制作揭秘19视图	48
二十、	超级玛丽制作揭秘20地图切换	50
二十一、	超级玛丽制作揭秘21游戏数据管理	53
二十二、	超级玛丽制作揭秘22玩家角色类MYROLE	58
二十三、	超级玛丽制作揭秘23玩家动作控制	63
二十四、	超级玛丽制作揭秘24角色动画	69
二十五、	超级玛丽制作揭秘25类GAMEMAP 全局变量	72
二十六、	超级玛丽制作揭秘26菜单控制 窗口缩放	76
二十七、	超级玛丽制作揭秘27程序框架WinProc	80
二十八、	InitInstance函数说明	85
二十九、	后记	87
 

类结构


图像层:

图像基类MYBITMAP

游戏背景MYBKSKYàMYBITMAP

游戏图片MYANIOBJàMYBITMAP

魔法攻击MYANIMAGICàMYBITMAP


逻辑层:

游戏逻辑GAMEMAP

时钟处理MYCLOCK

字体处理MYFONT

跟踪打印FILEREPORT

玩家控制MYROLEàMYBITMAP


结构和表:

精灵结构ROLE

物品结构MapObject

地图信息表MAPINFO



一、         超级玛丽制作揭秘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;


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


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


二、         超级玛丽制作揭秘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游戏背景 类MYBKSKY

类说明:这是一个专门处理游戏背景的类。在横版游戏或射击游戏中,都有一个背景画面,如山、天空、云、星空等等。这些图片一般只有1到2倍屏幕宽度,然后像一个卷轴一样循环移动,连成一片,感觉上像一张很长的图片。这个类就是专门处理这个背景的。在超级玛丽增强版中,主要关卡是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图片显示 类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魔法攻击 类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时钟控制 类MYCLOCK

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


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


控制事件频率的方法:

1.一秒内发生多次

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

2.一秒内发生一次

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

3.多秒内发生一次

需要一个辅助变量iNumShow,统计时间过了几秒。每隔一秒iNumShow减1,当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字体管理 类MYFONT

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

菜单中的文字包括:

"制作: programking 2008年8月",

"操作:   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跟踪打印 类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精灵结构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;


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

子弹是在哪里发出的呢?

思路:用一个函数不停地检测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子弹的显示和帧的刷新

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

上次讲所有的子弹存储到一个数组里。用一个游标(数组下标)表示新生产的子弹存储的位置。设数组为a,长度为n。游戏开始,一个子弹存储在a0,然后是a1,a2,...,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子弹运动和打怪

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

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

实现如下:

函数: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旋风攻击,小怪运动,火圈

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

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

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

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_CURSOR为15

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


十三、        超级玛丽制作揭秘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;

}

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


碰撞检测和消失。

在前面的子弹、旋风的碰撞处理中已讲过。碰撞后,生命值减少,减为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

十四、        超级玛丽制作揭秘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


金币消失:

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

函数: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


十五、        超级玛丽制作揭秘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


十六、        超级玛丽制作揭秘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


十七、        超级玛丽制作揭秘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个格子,id为0,表示地面。

在显示的时候,只要把坐标、宽高乘以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


十八、        超级玛丽制作揭秘18背景物品

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

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

struct MapObject MapBkArray[MAX_MAP_OBJECT];

int iMapBkObjNum;

第一关的背景物品数据:

17 5 3 2 0(草丛)

76 7 3 2 1(win标志)

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


十九、        超级玛丽制作揭秘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


二十、        超级玛丽制作揭秘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


二十一、        超级玛丽制作揭秘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)进入隐藏地图,玩家出现在(3,8),即横向第3格,纵向第8格。玩家返回第一关后,要出现在“出水管”的位置(66,7)。

2.刷新视图坐标。

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

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

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

隐藏地图加载函数: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关,出水管位于(66,7),天空背景id为0,视图最大宽度为5倍窗口宽度。

第3关:

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

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


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

附:

超级玛丽第一版源码链接:http://download.csdn.net/source/497676

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

二十二、        超级玛丽制作揭秘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 0x7a://按键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()

代码:

//判断是否播放动画,即iAniBegin为1

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


二十三、        超级玛丽制作揭秘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

二十四、        超级玛丽制作揭秘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()

代码:

//判断是否播放动画,即iAniBegin为1

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



二十五、        超级玛丽制作揭秘25类GAMEMAP全局变量

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

成员函数功能列表:

class GAMEMAP

{

public:

//加载地图

int LoadMap();

//初始化所有游戏数据

void Init();

//初始化场景数据

void InitMatch();


//显示地图物品

void Show(MYANIOBJ & bmobj);

//显示地图背景物品,河流,树木

void ShowBkObj(MYANIOBJ & bmobj);

//显示所有动态元素,金币,小怪等

void ShowAniObj(MYANIOBJ & bmobj);

//显示LIFE, WORLD提示

void ShowInfo(HDC h);

//显示金钱,攻击提示信息

void ShowOther(HDC h);


//键盘处理

int KeyProc(int iKey);

//按键抬起处理

void KeyUpProc(int iKey);

//移动视图

void MoveView();

//设置视图起始坐标

void SetView(int x);

//设置视图状态,函数没有使用

void SetViewState(int i);

//设置游戏状态

void SetGameState(int i);


//碰撞检测

//判断人物能否移动

int RoleCanMove(int xoff, int yoff);

//检测人物是否站在物品上

int CheckRole();

//检测所有动态元素之间的碰撞,子弹和蘑菇兵的生成

int CheckAni(int itimeclip);

//清除一个小怪

void ClearEnemy(int i);

//清除一个金币

void ClearCoin(int i);

//帧刷新

void ChangeFrame(int itimeclip);


//逻辑检测

int IsWin();     //胜负检测

void Fail();      //失败处理

void Fail_Wait();     //失败后,加载地图


//地图切换

void ChangeMap();

//错误检查

void CodeErr(int i);

//菜单控制

void ShowMenu(MYANIOBJ & bmobj);


//构造和析构函数

GAMEMAP();

~GAMEMAP();


//数据部分

int iMatch;      //当前关卡

int iLife;  //游戏次数

int iGameState;       //游戏状态


//地图物品数组 游标

struct MapObject MapArray[MAX_MAP_OBJECT];

int iMapObjNum;


//地图背景物品数组 游标

struct MapObject MapBkArray[MAX_MAP_OBJECT];

int iMapBkObjNum;


//小怪火圈数组 游标

struct ROLE MapEnemyArray[MAX_MAP_OBJECT];

int iMapEnemyCursor;


//金币武器包 数组 游标

struct MapObject MapCoinArray[MAX_MAP_OBJECT];

int iCoinNum;


//下一个地图编号,变量没有使用

int iNextMap;


//玩家数据

int iMoney;     //金钱数量

int iAttack;      //攻击方式


//视图数据

int viewx; //视图横坐标

int viewy; //视图纵坐标

int iViewState; //视图状态


//地图信息

struct MAPINFO mapinfo;


//frame control

int ienemyframe;     //小怪帧

int ibkobjframe;      //背景物品帧


//子弹数组 游标

struct ROLE FireArray[MAX_MAP_OBJECT];

int iFireNum;

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

int iBeginFire;//是否开始攻击


//爆炸效果,+10字样 数组 游标

struct MapObject BombArray[MAX_MAP_OBJECT];

int iBombNum;


//攻击对象提示

char AttackName[20];      //名称

int iAttackLife;       //生命值

int iAttackMaxLife; //最大生命值


//菜单部分

int iMenu;       //当前菜单项编号


//屏幕缩放

int iScreenScale;      //是否是默认窗口大小

};

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

//所有菜单文字

char *pPreText[]={

"制作: programking 2008年8月",

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

};

//所有动态元素的图片宽 高

int mapani[2][10]={

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

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

};

//所有地图物品的图片宽 高

int mapsolid[2][13]={

{32,32,32,32,32,32,32,32,32,64,64,20,100},

{32,32,32,32,32,32,32,32,32,64,64,10,12}

};

//所有背景物品的图片宽 高

int mapanibk[2][4]={

{96,96,96,96},

{64,64,64,64},

};

//旋风的宽 高

int mapanimagic[2][1]={

{192},

{128}

};

//所有地图信息

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}

};


//普通蘑菇兵模板

struct ROLE gl_enemy_normal=

{

0,

0,

32,

32,

ID_ANI_ENEMY_NORMAL,

};


//跟踪打印

//FILEREPORT f1;

//计时器

MYCLOCK c1;

//游戏全部逻辑

GAMEMAP gamemap;

//各种图片

MYBITMAP bmPre;      //菜单背景,通关,GAMEOVER

MYBKSKY bmSky;       //天空背景

MYANIOBJ bmMap;      //地图物品

MYANIOBJ bmMapBkObj;    //地图背景物品

MYANIOBJ bmAniObj;         //所有动态元素

MYROLE rmain;    //玩家角色

MYANIMAGIC bmMagic;      //旋风

//字体管理

MYFONT myfont;  //字体

//DC句柄

HDC hwindow,hscreen,hmem,hmem2;//窗口DC,地图DC,临时DC,临时DC2

//空位图

HBITMAP hmapnull;

//窗口大小

int wwin,hwin;//显示器屏幕宽 高

int wwingame,hwingame; //当前窗口宽 高

HWND hWndMain; //窗口句柄

所有功能都分别介绍过了。再讲菜单控制和窗口缩放,且听下回分解。


附:

超级玛丽第一版源码链接:http://download.csdn.net/source/497676

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



二十六、        超级玛丽制作揭秘26菜单控制 窗口缩放

菜单控制:

开始菜单只有两项:0项“开始游戏”,1项“操作说明”

菜单编号用iMenu表示。


初始化:

函数:void GAMEMAP::Init()

代码:iMenu=0;


菜单显示:

菜单文字显示:

函数:WndProc

代码:在WM_PAINT绘制消息中:

case GAME_PRE:

gamemap.viewx=0;         //设置视图坐标

bmPre.Stretch(2,2,0);      //菜单背景图片

myfont.SelectFont(0);     //设置文字字体

myfont.SelectColor(TC_BLACK, TC_YELLOW_0);//设置文字颜色

//显示3行文字

myfont.ShowText(150,260,pPreText[4]);                                   

myfont.ShowText(150,290,pPreText[5]);

myfont.ShowText(150,320,pPreText[6]);

//显示箭头

gamemap.ShowMenu(bmAniObj);

break;

菜单箭头显示:

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

代码:根据当前菜单编号,决定箭头的纵坐标

bmobj.PlayItem(115,280+iMenu*30, ID_ANI_MENU_ARROW);

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

void MYANIOBJ::PlayItem(int x,int y,int id)

{

//按照坐标,ID,显示图片

……

//切换当前帧

iframeplay=(iframeplay+1)%2;

}


菜单的按键响应:

函数: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; //表示立即刷新画面

至此,菜单功能实现。


窗口缩放功能的实现:

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

初始化由构造函数完成:

GAMEMAP::GAMEMAP()

{

iScreenScale=0;

Init();

}


窗口大小检测:用户拉动窗口,触发WM_SIZE消息。

函数:WndProc

代码:

case WM_SIZE:

//获取当前窗口宽 高

wwingame=LOWORD(lParam);

hwingame=HIWORD(lParam);


//如果窗口小于默认大小,仍然设置为默认数值,图像不缩放

if( wwingame <= GAMEW*32 || hwingame <= GAMEH*32)

{

wwingame = GAMEW*32;

hwingame = GAMEH*32;

gamemap.iScreenScale = 0;                         

}

else

{

//宽度大于高度的4/3

if(wwingame*3 > hwingame*4)

{                         

wwingame = hwingame*4/3;   //重新设置宽度

}

else

{

hwingame = wwingame*3/4;   //重新设置高度

}

gamemap.iScreenScale =1;      //表示图像需要缩放

}

break;                   


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

函数:WndProc

代码:

//判断是否缩放图像

if(gamemap.iScreenScale)

{

//缩放视图区域图像

StretchBlt(hwindow,0,0,

wwingame,hwingame,

hscreen,

gamemap.viewx,0,

GAMEW*32,GAMEH*32,

SRCCOPY);  

}

else

{           

//不缩放,视图区域拷贝到窗口

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

}

至此,所有功能实现。游戏流程怎样控制呢?且听下回分解。

附:

超级玛丽第一版源码链接:http://download.csdn.net/source/497676

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



二十七、        超级玛丽制作揭秘27程序框架WinProc

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

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

消息处理函数WndProc

{

绘图消息WM_PAINT:

状态1:状态1绘图。

状态2:状态2绘图。

……

计时消息WM_TIMER:

状态1:状态1逻辑处理。发WM_PAINT消息,通知绘图。

状态2:状态2逻辑处理。发WM_PAINT消息,通知绘图。

……

按键消息WM_KEYDOWN  WM_KEYUP:

状态1:状态1逻辑处理。发WM_PAINT消息,通知绘图。

状态2:状态2逻辑处理。发WM_PAINT消息,通知绘图。

……

}

逻辑处理后是否发WM_PAINT消息,依据具体情况而定。


程序入口:

int APIENTRY WinMain(HINSTANCE hInstance,

HINSTANCE hPrevInstance,

LPSTR     lpCmdLine,

int       nCmdShow)

{

MyRegisterClass(hInstance);    //类注册

//初始化

if (!InitInstance (hInstance, nCmdShow))

{

return FALSE;

}

//消息循环

while (GetMessage(&msg, NULL, 0, 0))

{

if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))

{

TranslateMessage(&msg);

DispatchMessage(&msg);

}

}

return msg.wParam;

}

整个消息处理循环,是默认的结构。

说明:InitInstance函数复杂初始化。类注册函数MyRegisterClass中,把菜单栏取消了,即wcex.lpszMenuName=NULL,其它不变。


消息处理函数WndProc

代码:

switch (message)

{

case WM_PAINT:

//窗口DC

hwindow = BeginPaint(hWnd, &ps);

//初始化空图

SelectObject(hscreen,hmapnull);

switch(gamemap.iGameState)

{

case GAME_ERR:

//地图文件加载错误

gamemap.viewx=0;         //视图坐标

//显示错误信息

bmPre.Stretch(2,2,0);      //背景图片

myfont.SelectColor(TC_WHITE,TC_BLACK);//文字颜色

myfont.SelectFont(0);     //字体

myfont.ShowText(150,290,pPreText[3]); //显示文字

break;

case GAME_PRE:

//菜单显示

(代码略)

break;

case GAME_HELP:

//菜单项“操作说明”

(代码略)

break;


case GAME_IN_PRE:

//游戏LIFE,WORLD提示

gamemap.viewx=0;         //视图坐标

bmPre.Stretch(2,2,2);      //背景图片

gamemap.ShowInfo(hscreen);   //显示LIFE,WORLD

break;

case GAME_IN:             //游戏进行中

case GAME_WIN:                 //游戏进行中,过关

case GAME_FAIL_WAIT:      //游戏进行中,失败

case GAME_PUMP_IN:         //游戏进行中,进入水管

bmSky.DrawRollStretch(2,2,gamemap.mapinfo.iBackBmp);//背景图片

gamemap.ShowBkObj(bmMapBkObj);          //地图背景物品

gamemap.Show(bmMap);                     //地图物品

gamemap.ShowAniObj(bmAniObj);       //动态元素

gamemap.ShowOther(hscreen);               //金钱数量,攻击提示

rmain.Draw();         //玩家角色

break;

case GAME_OVER:

//游戏结束

gamemap.viewx=0;

bmPre.Stretch(2,2,1);                    //输出图片GAME OVER

break;                                 

case GAME_PASS:

//游戏通关

gamemap.viewx=0;

bmPre.Stretch(2,2,3);                    //输出图片通关

break;

}

if(gamemap.iScreenScale)

{     //窗口缩放,放大视图区域

StretchBlt(hwindow,0,0,wwingame,hwingame,hscreen,          gamemap.viewx,0,GAMEW*32,GAMEH*32,SRCCOPY);   

}

else

{     //拷贝视图区域                           BitBlt(hwindow,0,0,GAMEW*32,GAMEH*32,hscreen,gamemap.viewx,0,SRCCOPY);

}

EndPaint(hWnd, &ps);    //绘图结束

break;


case WM_TIMER:

switch(gamemap.iGameState)

{

case GAME_PRE:   //游戏菜单

c1.DecCount();//计时器减1

if(0 == c1.iNum%MENU_ARROW_TIME)

{     //每隔10个时间片(即箭头闪烁的时间),刷新屏幕

InvalidateRect(hWnd,NULL,false);

}                         

break;

case GAME_IN_PRE: //游戏LIFE,WORLD提示

if(c1.DecCount())

{    

//计时结束,进入游戏。

gamemap.SetGameState(GAME_IN);

c1.ReStart(TIME_GAME_IN); //启动计时300秒

}

InvalidateRect(hWnd,NULL,false);  //刷新屏幕

break;                          

case GAME_IN:             //游戏进行中

case GAME_WIN:          //游戏进行中,过关

c1.DecCount();//计时器计时

if(0 == c1.iNum%SKY_TIME)

{

bmSky.MoveRoll(SKY_SPEED);//云彩移动

}

gamemap.ChangeFrame(c1.iNum);//帧控制

rmain.Move();//人物移动

gamemap.MoveView();//视图移动

gamemap.CheckRole();//角色检测

gamemap.CheckAni(c1.iNum);//逻辑数据检测

gamemap.IsWin();          //胜负检测

InvalidateRect(hWnd,NULL,false);  //刷新屏幕

break;

case GAME_WIN_WAIT: //游戏进行中,过关,停顿2秒

if(c1.DecCount())

{

//计时结束,进入游戏LIFE,WORLD提示

gamemap.SetGameState(GAME_IN_PRE);                                

InvalidateRect(hWnd,NULL,false);  //刷新屏幕

}

break;

case GAME_PUMP_IN:         //游戏进行中,进入水管,停顿2秒

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;

case GAME_FAIL_WAIT:             //游戏进行中,失败,停顿2秒

if(c1.DecCount())

{

//计时结束,加载地图

gamemap.Fail_Wait();

}

break;

case GAME_PASS:       //全部通关,停顿2秒

if(c1.DecCount())

{    

//计时结束,设置游戏状态:游戏菜单

gamemap.SetGameState(GAME_PRE);                              

}

InvalidateRect(hWnd,NULL,false);  //刷新屏幕

break;                                 

case GAME_OVER: //游戏结束,停顿3秒

if(c1.DecCount())

{

//计时结束,设置游戏状态:游戏菜单

gamemap.SetGameState(GAME_PRE);                              

}

InvalidateRect(hWnd,NULL,false);  //刷新屏幕

break;

}

break;


case WM_KEYDOWN:   //按键处理

if(gamemap.KeyProc(wParam))

InvalidateRect(hWnd,NULL,false);

break;


case WM_KEYUP: //按键“抬起”处理

gamemap.KeyUpProc(wParam);                                        

break;


case WM_SIZE:      //窗口大小调整,代码略      

break;                   


case WM_DESTROY:            //窗口销毁,释放DC,代码略

break;


消息处理函数完成。


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


附:

超级玛丽第一版源码链接:http://download.csdn.net/source/497676

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


二十八、        InitInstance函数说明

初始化函数:InitInstance

代码:

//默认窗口大小

wwingame=GAMEW*32;

hwingame=GAMEH*32;

//显示器屏幕大小

wwin=GetSystemMetrics(SM_CXSCREEN);

hwin=GetSystemMetrics(SM_CYSCREEN);

//创建窗口

hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,

(wwin-wwingame)/2, (hwin-hwingame)/2,

wwingame, hwingame+32, NULL, NULL, hInstance, NULL);

//设置窗口句柄

hWndMain=hWnd;

//DC

hwindow=GetDC(hWnd); //窗口DC

hscreen=CreateCompatibleDC(hwindow); //地图绘制DC

hmem=CreateCompatibleDC(hwindow);  //临时DC

hmem2=CreateCompatibleDC(hwindow);       //临时DC


//用空位图初始化各个DC

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

SelectObject(hscreen,hmapnull);

SelectObject(hmem,hmapnull);

SelectObject(hmem2,hmapnull);

//释放窗口DC

ReleaseDC(hWnd, hwindow);


//位图初始化

//菜单背景图片,通关,GAMEOVER

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

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

bmPre.SetPos(BM_USER,0,0);

//天空背景图片

bmSky.Init(hInstance,IDB_BITMAP_MAP_SKY,1,4);

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

bmSky.SetPos(BM_USER,0,0);


//地图物品图片

bmMap.Init(hInstance,IDB_BITMAP_MAP,1,1);

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

bmMap.InitAniList(mapsolid[0],mapsolid[1], sizeof(mapsolid[0])/sizeof(int),0);


(其它位图代码略)


//玩家图片初始化

rmain.Init(hInstance,IDB_BITMAP_ROLE,5,1);

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

//字体初始化

myfont.SetDevice(hscreen);


//游戏数据初始化

gamemap.Init();

//玩家角色初始化坐标,数据初始化

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

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

//文件检查

if(!gamemap.LoadMap())

{

//文件加载失败,设置游戏状态:文件错误

gamemap.CodeErr(ERR_MAP_FILE);

}

//计时器初始化

c1.SetDevice(hscreen);

//计时器启动,每40毫秒一次WM_TIMER消息

c1.Begin(hWnd, GAME_TIME_CLIP ,-1);

//设置显示方式,显示窗口

ShowWindow(hWnd, nCmdShow);

UpdateWindow(hWnd);


初始化工作完成。




免费下载 ×

下载APP,支持永久资源免费下载

下载APP 免费下载
温馨提示
请用电脑打开本网页,即可以免费获取你想要的了。
扫描加我微信 ×

演示

×
登录 ×


下载 ×
论文助手网
论文助手,最开放的学术期刊平台
				目  录

一、	超级玛丽制作揭秘1工程开始	2
二、	超级玛丽制作揭秘2图片基类MYBITMAP	4
三、	超级玛丽制作揭秘3游戏背景 类MYBKSKY	7
四、	超级玛丽制作揭秘4图片显示 类MYANIOBJ	9
五、	超级玛丽制作揭秘5魔法攻击 类MYANIMAGIC	13
六、	超级玛丽制作揭秘6时钟控制 类MYCLOCK	14
七、	超级玛丽制作揭秘7字体管理 类MYFONT	19
八、	超级玛丽制作揭秘8跟踪打印 类FILEREPORT	22
九、	超级玛丽制作揭秘9精灵结构struct ROLE	24
十、	超级玛丽制作揭秘10子弹的显示和帧的刷新	26
十一、	超级玛丽制作揭秘11子弹运动和打怪	27
十二、	超级玛丽制作揭秘12旋风攻击,小怪运动,火圈	29
十三、	超级玛丽制作揭秘13小怪和火圈,模板	34
十四、	超级玛丽制作揭秘14爆炸效果,金币	37
十五、	超级玛丽制作揭秘15金币提示,攻击提示	41
十六、	超级玛丽制作揭秘16攻击方式切换	43
十七、	超级玛丽制作揭秘17地图物品	44
十八、	超级玛丽制作揭秘18背景物品	47
十九、	超级玛丽制作揭秘19视图	48
二十、	超级玛丽制作揭秘20地图切换	50
二十一、	超级玛丽制作揭秘21游戏数据管理	53
二十二、	超级玛丽制作揭秘22玩家角色类MYROLE	58
二十三、	超级玛丽制作揭秘23玩家动作控制	63
二十四、	超级玛丽制作揭秘24角色动画	69
二十五、	超级玛丽制作揭秘25类GAMEMAP 全局变量	72
二十六、	超级玛丽制作揭秘26菜单控制 窗口缩放	76
二十七、	超级玛丽制作揭秘27程序框架WinProc	80
二十八、	InitInstance函数说明	85
二十九、	后记	87
			 
回复
来来来,吐槽点啥吧

作者联系方式

×

向作者索要->