太空入侵者游戏.docx
《太空入侵者游戏.docx》由会员分享,可在线阅读,更多相关《太空入侵者游戏.docx(38页珍藏版)》请在冰豆网上搜索。
![太空入侵者游戏.docx](https://file1.bdocx.com/fileroot1/2023-3/31/3390e9ea-6028-4826-a7ca-133dc52d4f85/3390e9ea-6028-4826-a7ca-133dc52d4f851.gif)
太空入侵者游戏
太空入侵者游戏
入侵者是一个简单的射击游戏。
游戏运行的初始界面如图3.14所示,游戏的战斗场面如图3.15所示。
图3.14入侵者程序初始运行图
图3.15入侵者作战运行图
在游戏中,玩家控制飞船,一方面要向敌机射击,另外还需要躲避敌人的子弹。
直到当前关卡中
所有敌机被消灭,玩家才能进入下一关卡。
要编译这个游戏,需要安装DirectX8.0SDK或以上版本。
在这个例子中要着重强调子弹、奖子(弹药、奖金等)、卷屏等设计技巧,以及射击过程是如何开
展的。
相信通过对这个游戏的理解,读者对射击游戏的构建会有更深刻的认识。
3.7.1奖子的设计
奖子是射击类游戏中必有的精灵,奖子实际就是玩家操作的飞船打掉敌机后,电脑奖赏的奖金、
弹药、防弹服等。
不过要想真正拥有这些奖子,玩家操纵的精灵必须在奖子消失(掉落到屏幕之外或
者在规定时间内消失)之前“吃”掉它。
“吃”实际上是一种碰撞检测行为。
而在吃掉奖子之前,奖子
通常会自动下落。
如图3.16所示是入侵者游戏中的奖子原始图。
Extras.bmp图片的分辨率是125⋅500。
图3.16入侵者中的奖子源视图extras.bmp
从图中不难看出入侵者中有5种奖子,每种奖子有20帧动画。
由于extras的分辨率是125⋅500,
所以平均每个奖子的每一帧是一幅25⋅25分辨率的图像。
在入侵者中,单独设计了一个奖子类Extra:
classExtra
{
private:
intx;//奖子在屏幕中的位置
inty;
RECTrcLastPos;//奖子上次出现在屏幕中的包围盒坐标
intiType;//奖子类型,从extras.bmp中可以看出共有5种类型
Extra*pNext;//奖子链表
Extra*pPrev;
public:
intframe;//帧编号,从extras.bmp中可以看出共有20帧
Extra()
{
x=0;
y=0;
pNext=NULL;
pPrev=NULL;
frame=0;
iType=0;
rcLastPos.left=0;
rcLastPos.top=0;
第3章2D游戏开发121
rcLastPos.right=0;
rcLastPos.bottom=0;
};
intGetType()
{
returniType;
}
voidSetType(intiNewType)
{
iType=iNewType;
}
intGetX()
{
returnx;
}
intGetY()
{
returny;
}
voidMove(intQtde)
{
rcLastPos.left=x;
rcLastPos.top=y;
rcLastPos.right=x+25;
rcLastPos.bottom=y+25;
if(rcLastPos.bottom>455)
rcLastPos.bottom=455;
//Qtde是一个负值,因为这种奖子都是垂直下落的,即y值需要不断增加
y-=Qtde;
}
voidSetXY(intnx,intny)
{
rcLastPos.left=nx;
rcLastPos.top=ny;
rcLastPos.right=nx+25;
rcLastPos.bottom=ny+25;
x=nx;
y=ny;
}
Extra*GetNext()
{
returnpNext;
}
122VisualC++游戏开发技术与实例
Extra*GetPrev()
{
returnpPrev;
}
voidSetNext(Extra*nNext)
{
pNext=nNext;
}
voidSetPrev(Extra*nPrev)
{
pPrev=nPrev;
}
BOOLDraw(LPDIRECTDRAWSURFACE7lpOrigin,LPDIRECTDRAWSURFACE7lpSource)
{
RECTrcRect;
HRESULThRet;
intiClipTop;
intiClipBottom;
//子弹在超出屏幕上方(y值小于0)和跨过屏幕下方(y+255>455)的时候需要进行裁剪
if(y<0)
iClipTop=y*-1;
else
iClipTop=0;
if(y+25>455)
iClipBottom=y+25-455;
else
iClipBottom=0;
rcRect.left=frame*25;
rcRect.top=((iType-1)*25)+iClipTop;
rcRect.right=frame*25+25;
rcRect.bottom=((iType-1)*25)+25-iClipBottom;
//帧数编号加1
frame++;
if(frame==20)
frame=0;
while
(1)
{
hRet=lpOrigin->BltFast(x,y,lpSource,&rcRect,DDBLTFAST_SRCCOLORKEY);
if(hRet==DD_OK)
{
break;
}
if(hRet==DDERR_SURFACELOST)
第3章2D游戏开发123
{
returnFALSE;
}
if(hRet!
=DDERR_WASSTILLDRAWING)
break;
}
returnTRUE;
}
};
很显然,Extra中私有成员变量x、y表示奖子方块左上角在屏幕上的像素坐标(事实上这两个变
量并不是必需的);而矩形结构rcLastPos表示奖子上次出现在屏幕时的坐标;整形的iType表示奖子
类型,这里共有5种类型的奖子。
公有变量frame表示帧编号,每种奖子都有20帧。
在Extra中__________读者
可能有疑惑的地方就是成员变量pNext和pPrev,因为从名称上读者就能猜出Extra变成了一种链表结
构。
这么做的理由很简单,因为屏幕中可能同时出现多个奖子,所以为了奖子管理的需要,不妨将其
设置成链表结构。
Extra中成员函数都比较简单,需要关注的只有Move函数和Draw函数。
Move函数中出现的常数25是由奖子图像的长度、宽度(25⋅25)决定的;而语句rcLastPos.bottom>
455是用来判断奖子是否已经掉出屏幕之外(确切的说是屏幕下限);语句y-=Qtde表示奖子是垂直下
落的,参数Qtde实际是一个负值,后面会看到这个值被设置成–3。
Draw函数中,首先碰到的问题就是对奖子的裁剪。
裁剪解决的方法是当精灵在屏幕之外时,计算
实际需要绘制精灵的部分。
在Draw函数里,裁剪操作如下:
if(y<0)
iClipTop=y*-1;
else
iClipTop=0;
if(y+25>455)
iClipBottom=y+25-455;
else
iClipBottom=0;
rcRect.left=frame*25;
rcRect.top=((iType-1)*25)+iClipTop;
rcRect.right=frame*25+25;
rcRect.bottom=((iType-1)*25)+25-iClipBottom;
当y小于0,表示当前奖子在屏幕上方(但不一定完全在上方),所以需要裁剪掉奖子的上半部分,
iClipTop指定了在25⋅25图像中,实际要绘制的顶部y值。
当y+25>455,表示当前奖子已经掉落到
屏幕的下方(但不一定完全掉落),所以需要裁剪掉奖子的下半部分,iClipBottom指定了在25⋅25图像
中,实际要绘制的底部y值。
如果这两个条件都不满足,则说明奖子完全落在屏幕内,需要完整绘制。
裁剪计算完成后,需要调整帧数,每种奖子都有20帧。
frame++;
if(frame==20)
frame=0;
当然,这段代码可以简化成“(frame++)%20”。
帧调整完成后,绘制奖子。
124VisualC++游戏开发技术与实例
lpOrigin->BltFast(x,y,lpSource,&rcRect,DDBLTFAST_SRCCOLORKEY);
在入侵者的主框架中定义了一个全局奖子对象Extra*pExtra。
因为Extra是一种链表结构,所以在
pExtra中记录了当前屏幕中所有奖子。
而主框架中绘制奖子的函数DrawExtra定义如下。
voidDrawExtra()
{
//绘制屏幕中所有奖子(奖金、弹药等)
Extra*pFirstExtra;
Extra*pNextExtra=NULL;
Extra*pLastExtra=NULL;
Extra*pPrevExtra=NULL;
//保存这个全局指针。
因为后面的操作会改变这个指针,而最后还要恢复它
pFirstExtra=pExtra;
//因为当前屏幕中可能有多个奖子,所以这里使用while语句就是要将所有的奖子都绘制出来
//当前屏幕中的奖子都被保存在同一个奖子链表中,所以绘制所有奖子时要求遍历整个奖子队列
while(pExtra!
=NULL)
{
//传入的是-3,即每次掉落3个像素点
pExtra->Move(-3);
//绘制当前节点奖子,lpExtra是extras.bmp的DDraw的Surface
pExtra->Draw(lpBackBuffer,lpExtra);
//如果当前的奖子已经掉落到屏幕以外,则调整奖子列表,删除当前奖子节点
if(pExtra->GetY()>455)
{
//调整奖子链表节点和相关指针
if(pPrevExtra!
=NULL)
pPrevExtra->SetNext(pExtra->GetNext());
pNextExtra=pExtra->GetNext();
if(pNextExtra!
=NULL)
pNextExtra->SetPrev(pPrevExtra);
//如果当前奖子节点位于链表中的第一位置,则还需要再调整链表
if(pExtra==pFirstExtra)
pFirstExtra=pExtra->GetNext();
//删除当前奖子节点
delete(pExtra);
//重置当前奖子节点
pExtra=pNextExtra;
}
else
{
//遍历奖子链表:
当前奖子节点指向链表中下一个节点
pPrevExtra=pExtra;
第3章2D游戏开发125
pExtra=pExtra->GetNext();
}
}
pExtra=pFirstExtra;
}
事实上,DrawExtra中很大部分的工作都是对Extra链表的操作。
最后的问题就是如何“吃”奖子?
在前面已经提过了,这是一个碰撞检测问题。
在主框架中定义
了“吃”奖子函数CheckHitExtra。
通常“吃”奖子的碰撞检测采用的是最简单的矩形包围盒技术,这
里正是用了这种方法。
当然“吃”完了奖子后,必须把它从奖子链表中删除。
intCheckHitExtra(void)
{
Extra*pFirstExtra,*pPrevExtra,*pNextExtra;
intiReturn=0;
pPrevExtra=NULL;
pNextExtra=NULL;
pFirstExtra=pExtra;
//检测所有奖子是否和飞船发生了碰撞
while(pExtra!
=NULL)
{
//如果奖子在飞船的矩形包围盒内,则说明发生了“吃”的操作
//一旦吃掉,则需要删掉该奖子,同时调整链表
if((pExtra->GetX()>=iShipPos&&
pExtra->GetX()pExtra->GetY()>385&&
pExtra->GetY()<425)&&
(pExtra->GetType()!
=0))
{
if(pPrevExtra!
=NULL)
pPrevExtra->SetNext(pExtra->GetNext());
pNextExtra=pExtra->GetNext();
if(pNextExtra!
=NULL)
pNextExtra->SetPrev(pPrevExtra);
if(pExtra==pFirstExtra)
pFirstExtra=pExtra->GetNext();
iReturn=pExtra->GetType();
delete(pExtra);
pExtra=pFirstExtra;
returniReturn;
}
pPrevExtra=pExtra;
pExtra=pExtra->GetNext();
}
pExtra=pFirstExtra;
returniReturn;
}
126VisualC++游戏开发技术与实例
3.7.2子弹(Bullet)的设计
在入侵者中子弹的设计几乎是和奖子的设计一样的,只是在图像的宽度和高度上需要进行调整。
读者不妨查看类Bullet,然后再和Extra类做个比较。
不过子弹的设计并不是一成不变的。
在游戏设计中,子弹的设计方式和方法很多,这里只是采用
了静态的直线传递方法。
子弹的弹道通常决定了子弹设计的方式。
例如,很多游戏中的追踪弹、散花
弹和激光弹等。
对于追踪弹来说,它会自动锁定目标,当然这并不意味着一定能够击落敌机。
通常情
况下,敌机如果可以将追踪弹引出屏幕之外就算追踪弹无效,这是很公平的决策。
在追踪弹设计中,
弹道的设计需要平滑,因为折线式的追踪轨迹并不现实。
平滑的方法很多,样条曲线就是一个不错的
选择,在第4章中会介绍到一种3次样条曲线的设计方法。
对于散花弹而言,可以考虑成由多个单一
的子弹组成,不过这种方法需要考虑效率。
而激光弹是一种特殊的子弹,它的有效攻击范围实际是会
变化的。
这种子弹的设计需要考虑真实世界的激光。
当打开激光源的时候,光会快速的形成一条射线,
在这条轨迹内的任何物体实际上都会被激光照射,而当关闭激光源的时候,激光总是消失在远处。
根
据这种物理规律,才能够设计出比较合理的激光子弹。
3.7.3卷屏(Scroll)的设计
在入侵者游戏中,为了体现游戏中的飞船处于飞行状态,采用了一种简单的卷屏技术。
这种卷屏
技术是利用背景图片的变化实现的。
如图3.17所示是入侵者的背景图片。
它是640⋅480分辨率的图片。
而该图片中底部的状态栏占据了25个像素的高度。
图3.17背景图片
卷屏函数DrawInvalidBackGround定义如下:
voidDrawInvalidBackGround()
{
//重绘背景
RECTrcRect;
HRESULThRet;
Staticy=0;
constintiFim=455;
//新关卡
第3章2D游戏开发127
if(lastTickCount=0)
{
y=0;
}
//向下卷屏一个像素
y+=1;
//如果已经卷完455个像素(即一个屏幕),则y值重新置0。
if(y>iFim)
y=0;
//上半屏的矩形区域
rcRect.left=0;
rcRect.top=0;
rcRect.right=640;
rcRect.bottom=iFim-y;
while
(1)
{
hRet=lpBackBuffer->BltFast(0,y,lpBkGround,
&rcRect,DDBLTFAST_NOCOLORKEY);
if(hRet==DD_OK)
{
break;
}
if(hRet==DDERR_SURFACELOST)
{
hRet=RestoreSurfaces();
if(hRet!
=DD_OK)
break;
}
if(hRet!
=DDERR_WASSTILLDRAWING)
break;
}
//下半屏的矩形区域
rcRect.left=0;
rcRect.top=iFim-y;
rcRect.right=640;
rcRect.bottom=iFim;
while
(1)
{
hRet=lpBackBuffer->BltFast(0,0,lpBkGround,
&rcRect,DDBLTFAST_NOCOLORKEY);
if(hRet==DD_OK)
{
break;
}
if(hRet==DDERR_SURFACELOST)
{
hRet=RestoreSurfaces();
128VisualC++游戏开发技术与实例
if(hRet!
=DD_OK)
break;
}
if(hRet!
=DDERR_WASSTILLDRAWING)
break;
}
}
DrawInvalidBackGround函数使用的卷屏方法是将背景图片从水平方向分成两个部分,然后分别绘
制上下部分的图片。
背景图片的分割线是从关卡开始后背景下降的y值累加之和。
每次绘制背景的时
候y值都要增加1个像素,直到y值等于455(480中包括了25个像素高度的状态栏)后重置为0。
通过这样的两次背景绘制,屏幕就动了起来。
需要指出的是,对于一些复杂的横板卷屏游戏而言,这种方法就完全行不通了。
因为横板游戏的
背景中包含了复杂的地形,这种地形是影响游戏发展的,而不像在入侵者中背景对整个游戏进程毫无
影响。
上面的DrawInvalidBackGround函数是在UpdateFrame中被调用的,而UpdateFrame函数负责的
是整个游戏状态的刷新。
其中屏幕刷新的时候,遵守一个刷新顺序:
背景刷新、奖子刷新、敌机刷新、
子弹刷新、飞船刷新。
下面来看看UpdateFrame函数是如何工作的。
UpdateFrame函数在开始部分定义了一些局部变量:
voidUpdateFrame(void)
{
intddrval;
DWORDthisTickCount;
RECTrcRect;
DWORDdelay=18;
staticintframe=0;
HBITMAPhbm;
DDBLTFXddbltfx;
HRESULThRet;
接着,它判断飞船是否已经爆炸了一段时间。
如果是这样,则载入游戏结束的画面,重置一些全
局变量。
thisTickCount=GetTickCount();
if(thisTickCount-dwShipExplode>2000&&iShipState==3)
{
Ovni*auxpUFO;
Bullet*auxpBullet;
while(pUFO!
=NULL)
{
auxpUFO=pUFO->GetNext();
delete(pUFO);
pUFO=auxpUFO;
}
while(pBullet!
=NULL)
{
auxpBullet=pBullet->GetNext();
第3章2D游戏开发129
delete(pBullet);
pBullet=auxpBullet;
}
ShowGameOver();
lastTickCount=0;
iAppState=0;
iShipState=0;
}
下面判断当前游戏处于何种状态。
其中APP_MAINMENU标志表示当前游戏处于主菜单状态;
APP_GAMESCREEN标志表示游戏已经开始;APP_CREDITS表示游戏处于显示积分的状态;
APP_HELPSCREEN表示游戏处于帮助屏幕状态。
switch(iAppState)
{
caseAPP_MAINMENU:
//如果时间间隔过短,则返回
if((thisTickCount-lastTickCount)<=delay)
return;
//如果是主菜单界面,则需要做一些初始化工作
if(lastTickCount==0)
{
iOption=0;
frame=0;//设置飞船光标帧标号为0
//载入初始界面的bmp文件
hbm=(HBITMAP)LoadImage(GetModuleHandle(NULL),MAKEINTRESOURCE
(IDB_INVASION),IMAGE_BITMAP,0,0,LR_CREATEDIBSECTION);
if(NULL==hbm)
return;
//将初始界面拷贝到前台缓冲中
ddrval=DDCopyBitmap(lpFrontBuffer,hbm,0,0,640,480);
if(ddrval!
=DD_OK)
{
DeleteObject(hbm);
return;
}
//将初始界面拷贝到后台缓冲中
ddrval=DDCopyBitmap(lpBackBuffer,hbm,0,0,640,480);
if(ddrval!
=DD_OK)
{
DeleteObject(hbm);
return;
}
//画出4个菜单,STARTGAME、CREDITS、HELP和QUIT。
bltText("STARTGAME\0",190,280);
130VisualC++游戏开发技术与实例
bltText("CREDITS\0",190,320);
bltText("HELP\0",190,360);
bltText("QUIT\0",190,400);
}
ddbltfx.dwSize=sizeof(ddbltfx);
ddbltfx.dwFillColor=dwFillColor;
rcRect.left=130;
rcRect.top=270;
rcRect.right=190;
rcRect.bottom=460;
while
(1)
{
//填充后台缓冲的某个矩形区域为黑色
hRet=lpBackBuffer->Blt(&rcRect,NULL,NULL,DDBLT_COLORFILL,&ddbltfx);
if(hRet==DD_OK)
{
break;
}
if(hRe