MARIO CODE INSIDE
超级玛丽制作揭秘
制作 programking 博客 http://blog.csdn.net/programking
目 录
五、 超级玛丽制作揭秘5魔法攻击 类MYANIMAGIC. 13
八、 超级玛丽制作揭秘8跟踪打印 类FILEREPORT. 22
九、 超级玛丽制作揭秘9精灵结构struct ROLE. 24
十二、 超级玛丽制作揭秘12旋风攻击,小怪运动,火圈... 29
二十二、 超级玛丽制作揭秘22玩家角色类MYROLE. 58
二十五、 超级玛丽制作揭秘25类GAMEMAP 全局变量... 72
二十六、 超级玛丽制作揭秘26菜单控制 窗口缩放... 76
二十七、 超级玛丽制作揭秘27程序框架WinProc. 80
图像层:
图像基类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
怎样把所有东西都显示在窗口中,并随着玩家移动呢?
思路:玩家看到的区域称为视图,即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 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()
代码:
//判断是否播放动画,即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
代码:
//默认窗口大小
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);
初始化工作完成。
程序,乐趣无穷。这是我大学时的看法。论坛里有很多介绍程序员七年经历、十年经历的帖子,结果都不好。我虽然只有两年经验,也感觉是这一类的人,况且已经不是程序员。程序员的话题,我现在下结论,还为时尚早。
看着屋里的书籍,有VC的教程,3D MAX的教程,C++编程思想,还有云风的编程感悟。我忽然想起大学时的事情,想起了程序的乐趣。
学校里还没有开C语言,图书馆里有大量的C语言教程。偏偏只能阅览,不准外借。那就直接抄,抄下大段似懂非懂的代码,再到机房调试,这个getch什么意思,读取按键,太简单了……
大四,终于放弃了DOS下的Turbo C,决定做一个Windows下能运行的程序。怎么做呢?图书馆里有VC教程,第一章,点击“默认生成MFC程序”,点击Next,咦,怎么这么多代码?
许多天过去了,小时候玩的“俄罗斯方块”,从一堆代码中诞生。
如今,我有了电脑,有了各种教程。为什么不像大学时那样,继续练习呢?于是,有了超级玛丽,有了这篇文档。
通过这张星际2截图,感受一下程序的乐趣: