课程设计三 太空战机
一、游戏介绍
太空战机是玩家用键盘控制战机移动并发射子弹,消灭敌方的战机。敌方战机从右到左移动,同时上下浮动。同时隔一定的时间发射子弹,我方战机在受到敌方战机子弹攻击时,战机的颜色会发生变化,生命值也在减少,当我方战机的生命值减少到0时,我方战机消失,同时产生一架我方的新的战机,游戏重新开始。
二、实验目的
综合应用C语言的知识开发一款小游戏。
三、实验内容
在外星球上,玩家通过键盘WSAD键控制己方战机,消灭外星球的邪恶战机。
要求如下:
游戏运行时,初始界面如下图。
按下空格键,游戏开始,玩家通过WSAD键控制己方战机移动;己方战机不能超出世界边界。
玩家战机每隔0.3秒发射一发子弹;
添加敌方战机,每隔2秒创建一架敌方战机;
敌方战机每隔0.3秒发射一发子弹;
记录游戏的最高分。
游戏初始界面
实验指南
实验一 游戏框架的搭建
【实验内容】
由于本实验比较复杂,所以我们使用多文件实现
添加文件
搭建游戏平台
还没有用到的函数可以先声明,在定义,函数体为空
【实验思路】
为了让游戏的代码更加清晰,我们使用多文件,一般的代码编写都是一个源程序文件对应一个头文件,所以我们增加一个源文件程序,一个头文件程序。在本实验中,主程序Main.cpp主要是来显示游戏的大体框架,我们将在LessonX.cpp中去具体实现不同的函数,由主程序调用。
【实验指导】
导入模板“AirPlane”,将太空战机的地图初始化;
由于我们这个游戏稍稍有些复杂,所以我们采用多文件的形式,那么我们先添加一个LessonX.cpp和LessonX.h文件:
首先在VC6.0当中,点击新建按钮,如图红色区域
然后会出现一个空白文件,然后再空白文件中点击一下,选中空白文件,使用快捷 方式Ctrl+s,或者使用File菜单栏下的Save选项,如下图
这样会弹出一个对话框,如下图
之后点击红色区域,返回到上一个目录:
选中Src目录,双击进入Src并将Text2.txt命名为LessonX.cpp点击保存即可。
同样的方式建立LessonX.h文件,在保存的时候放在Hearder目录下,并将**.txt 文件更名为LessonX.h;
下面我们将新建的两个文件添加到我们的工程当中,
首先在VC中右击Source Files,并且选中Add Files to Folder选项,如下图
然后弹出对话框,如下图
点击返回上一目录,得到如下对话框
双击Src进入到目录里面,选中LessonX.cpp文件,点击OK即可;
添加LessonX.h文件的步骤,右击Header Files,然后找到Header文件夹,双击进 入并选中LessonX.h文件,左击OK即可;
我们来搭建游戏的框架,我们在主函数中的while循环的最后添加一个函数GameMainLoop(fDeltaTime)的调用,该函数是游戏的中心部分,游戏就是通过它不断的刷新数据;
现在我们在LessonX.cpp中来实现这个GameMainLoop函数,游戏主循环,此函数将被不停的调用,引擎每刷新一次屏幕,此函数即被调用一次用以处理游戏的开始、进行中、结束等各种状态. 函数参数fDeltaTime : 上次调用本函数到此次调用本函数的时间间隔,单位:秒
void GameMainLoop( float fDeltaTime )
{
switch( g_iGameState )
{
// 初始化游戏,清空上一局相关数据
case 1:
{
g_iGameState = 2; // 初始化之后,将游戏状态设置为进行中
GameInit();
}
break;
// 游戏进行中,处理各种游戏逻辑
case 2:
{
// 判断输赢
if( true)
{
//游戏结束。调用游戏结算函数,并把游戏状态修改为结束状态
g_iGameState = 0;
GameEnd();
}
else // 游戏未结束,继续游戏
{
GameRun( fDeltaTime );
}
}
break;
// 游戏结束/等待按空格键开始
case 0:
default:
break;
};
}
以上游戏框架中GameInit、GameRun、GameEnd函数我们还没有定义,那么我们就在LessonX.cpp中定义这三个函数;
#include
#include “LessonX.h” // 注意<>和“”的区别
// 游戏状态的定义
g_iGameState = 0;
// 函数声明
void GameInit();
void GameRun(float);
void GameEnd();
// 函数定义
Void GameInit()
{
}
Void GameRun(float fDeltaTime)
{
}
Void GameEnd()
{
}
这样我们的游戏框架就搭建好了,编译一下就可以运行了,虽然有什么效果。
实验二 游戏需要的实物及分析
【实验内容】
分析游戏中需要的对实物
分析这些实物需要的变量
这些变量进行初始化
【实验思路】
对于本游戏,我们至少需要以下几个实物,我方战机、敌方战机、我方战机发射的子弹、敌方战机发射的子弹、当前分数、最高分数等,关于战机我们还需要考虑到生命值、发射子弹的时间、敌机上下浮动等变量,关于子弹我们需要考虑子弹的生命值、对敌方或者是我方的伤害值、是由谁发射的等问题。
【实验指导】
为了便于对战机和子弹的管理,我们使用结构体来将战机和子弹的变量存放在一起。战机和子弹的管理我们都放在链表中进行管理, 我们首先增加ListX.cpp和List.h这两个文件,并且将这两个文件添加到工程当中;
在LessonX.h中添加防止头文件包含的语句:
#ifndef _LESSONX_H_
#define _LESSONX_H_
… …
#endif //_LESSONX_H_
我们将代码写入到#define和#endif的中间
然后我们加入头文件#include “CommonAPI.h”
接着我们用宏定义定义一些极限值
#define MAX_NAME_LEN 128 // 名字的最大长度
#define CONTROL_MAX_HP 1000 // 我方战机的最大生命值是1000
#define BULLET_DAMAGE_1 100 // 子弹1的伤害值是100
#define VER_MIN_CREATE 1
#define VER_MAX_CREATE 6
之后我们用枚举类型列出本游戏需要用的三种类型:
enum ESpriteType
{
SPRITE_CONTROL, // 我们控制的战机
SPRITE_VER, // 敌方战机
SPRITE_BULLET1 // 朝前方发射的子弹,且可以被销毁
};
再接下来就是我们的结构体了
struct SGameSprite
{
char szName[MAX_NAME_LEN]; // 精灵的名字
int iHp; // Health Point生命值
int iScore; // 击毁本精灵可以获得的分数值
int iDamage; // 自身碰到敌方可以给敌方造成的伤害
ESpriteType eSpriteType; // 精灵的类型
int iBulletType; // 该子弹类型,通过该类型判断是不是受到伤害
float fFireAffterCreate; // 精灵被创建多长时间可以发射子弹
float fBulletTime; // 子弹1被发射的时间间隔
float fFloatTime; // 上下浮动的时间
bool bFloatUp; // 是否上下浮动
};
这样我们的LessonX.h文件就已经初始化好了,接下来使用就比较方便了;
实验三 对游戏中的精灵进行链表管理
【实验内容】
精灵是个体,将这些个体联系在一起便于管理
我们要根据链表由名字获得精灵,遍历精灵链表,添加精灵到链表,根据名字删除 链表,删除所有精灵
【实验思路】
如果通过个体去处理问题,在不断有其他精灵创建的前提下,是相当麻烦的,比如战机撞到世界边界的问题,如果不用链表遍历操作的话,通过if-else判断是无法想象的工作量,其实也是无法完成的,因为你不知道到底会有多少架战机被创造出来,所以为了便于对这些个体进行管理和操作,我们有必要为这些精灵个体建立一个链表,这样对于精灵的遍历、插入、删除等操作就非常遍历了。
【实验指导】
在List.h文件中,首先要防止头文件重复包含:
#ifndef _LISTX_H_
#define _LISTX_H_
… …
#endif //__LISTX_H_
以后所有代码都写在#define 和#endif之间;
接着要将头文件包含进来
#include
#include “CommonAPI.h”
#include “LessonX.h”
建立一个结构体的结构体
struct SpriteStruct
{
SGameSprite *pSprite;
SpriteStruct *pNext;
SpriteStruct *pPrev;
};
之后就是声明一下操作链表的函数,具体用到的函数如下
extern int GList_GetListSize();
// 根据名字获取Sprite
extern SGameSprite *GList_GetSpriteByName( const char *szName );
// 根据索引获取Sprite,如果要遍历链表并删除其中的某个元素,请从后面往前面遍历(即索引初始化为链表大小然后递减),否则必然出错
extern SGameSprite *GList_GetSpriteByIndex( const int iIndex );
// 添加一个Sprite到链表里
extern SpriteStruct *GList_AddSprite( SGameSprite *pSprite );
// 根据名字删除Sprite. bDeleteImage : 是否删除该Sprite在地图上的图片显示
extern void GList_DeleteSprite( const char *szName, bool bDeleteImage = true );
// 根据指针删除Sprite. bDeleteImage : 是否删除该Sprite在地图上的图片显示
extern void GList_DeleteSprite( SGameSprite *pSprite, bool bDeleteImage = true );
// 删除所有Sprite. bDeleteImage : 是否删除该Sprite在地图上的图片显示
extern void GList_DeleteAllSprite( bool bDeleteImage = true );
有了上面的函数声明,我们在ListX.cpp中给出函数的实现,在函数实现中都有注释,我们就不在做详解,并且这部分比较难,同学可以直接抄在文件当中,之后做一下分析就可以了
SpriteStruct *g_pListHeader = NULL;
int g_iListSize = 0;
// 链表元素个数
/////////////////////////////////////////////////////////////////////////////////
int GList_GetListSize()
{
return g_iListSize;
}
//=========================================================================
// 根据名字获取Sprite
SGameSprite* GList_GetSpriteByName( const char *szName )
{
SpriteStruct *pPtr = g_pListHeader;
while( NULL != pPtr )
{
if( strcmp( pPtr->pSprite->szName, szName ) == 0 )
return pPtr->pSprite;
pPtr = pPtr->pNext;
}
return NULL;
}
//=========================================================================
// 任务:根据索引获取Sprite
// 提示:参考“根据名字获取Sprite”
// 提示:如果要遍历链表并删除其中的某个元素,请从后面往前面遍历(即索引初始化为链表大小然后递减),否则必然出错
SGameSprite *GList_GetSpriteByIndex( const int iIndex )
{
// 通过循环从链表头开始查找,当循环次数达到iIndex次时,得到该索引对应的元素
int iLoop = 0;
SpriteStruct *pPtr = g_pListHeader;
while( NULL != pPtr )
{
if( iLoop == iIndex )
return pPtr->pSprite;
iLoop++;
pPtr = pPtr->pNext;
}
return NULL;
}
//=========================================================================
// 添加一个Sprite到链表里
SpriteStruct *GList_AddSprite( SGameSprite *pSprite )
{
if( NULL == pSprite )
return NULL;
SpriteStruct *pPtr = (SpriteStruct*)malloc( sizeof(SpriteStruct) );
pPtr->pSprite = pSprite;
pPtr->pNext = NULL;
pPtr->pPrev = NULL;
// 插入链表表尾
if( NULL == g_pListHeader )
g_pListHeader = pPtr;
else
{
SpriteStruct *pTemp = g_pListHeader;
while( NULL != pTemp->pNext )
pTemp = pTemp->pNext;
pPtr->pPrev = pTemp;
pTemp->pNext = pPtr;
}
g_iListSize++;
return pPtr;
}
//=========================================================================
// 根据名字删除Sprite
void GList_DeleteSprite( const char *szName, bool bDeleteImage )
{
SpriteStruct *pPtr = NULL;
for( pPtr = g_pListHeader; NULL != pPtr; pPtr = pPtr->pNext )
{
if( strcmp( szName, pPtr->pSprite->szName ) == 0 )
{
// 将本指针从链表中取出(即将链表中的前后指针重新指定)
// 假设目前链表如下:有ABC三个值,A <-> B <-> C,需要删除B
// 则需要将A的Next指向C,C的Prev指向A,删除之后结果为 A <-> C
if( NULL != pPtr->pNext )
{
pPtr->pNext->pPrev = pPtr->pPrev;
}
if( NULL != pPtr->pPrev )
{
pPtr->pPrev->pNext = pPtr->pNext;
}
// 如果是表头
if( pPtr == g_pListHeader )
{
g_pListHeader = g_pListHeader->pNext;
}
// 删除Sprite
if( bDeleteImage )
dDeleteSprite(pPtr->pSprite->szName);
// 释放内存
free( pPtr );
//
g_iListSize–;
return;
}
}
}
//=========================================================================
// 任务:根据指针删除Sprite
// 提示:参考“根据名字删除Sprite”
void GList_DeleteSprite( SGameSprite *pSprite, bool bDeleteImage )
{
SpriteStruct *pPtr = NULL;
for( pPtr = g_pListHeader; NULL != pPtr; pPtr = pPtr->pNext )
{
if( pPtr->pSprite == pSprite )
{
// 将本指针从链表中取出(即将链表中的前后指针重新指定)
// 假设目前链表如下:有ABC三个值,A <-> B <-> C,需要删除B
// 则需要将A的Next指向C,C的Prev指向A,删除之后结果为 A <-> C
if( NULL != pPtr->pNext )
{
pPtr->pNext->pPrev = pPtr->pPrev;
}
if( NULL != pPtr->pPrev )
{
pPtr->pPrev->pNext = pPtr->pNext;
}
// 如果是表头
if( pPtr == g_pListHeader )
{
g_pListHeader = g_pListHeader->pNext;
}
// 删除Sprite
if( bDeleteImage )
dDeleteSprite(pPtr->pSprite->szName);
// 释放内存
free( pPtr );
g_iListSize–;
return;
}
}
}
//=========================================================================
// 删除所有Sprite
void GList_DeleteAllSprite( bool bDeleteImage )
{
SpriteStruct *pPtr = NULL;
SpriteStruct *pPtrhNext = g_pListHeader;
while( NULL != pPtrhNext )
{
pPtr = pPtrhNext;
pPtrhNext = pPtrhNext->pNext;
// 删除Sprite显示
if( bDeleteImage )
dDeleteSprite(pPtr->pSprite->szName);
// 释放内存
free( pPtr );
};
g_pListHeader = NULL;
g_iListSize = 0;
}
至此 我们的准备工作就完全结束了,下面的实验就可以看到实实在在的效果了。
实验四 游戏开始和控制我方战机移动
【实验内容】
按空格键,游戏开始,“空格开始”字样消失。
创建我方控制的战机。
战机碰到世界边界时,静止。
游戏开始后,通过键盘WSAD键控制战机移动。
战机左右运动的速度为30。上下运动的速度为15。
在游戏中显示游戏的当前积分和最高积分。
【实验思路】
按空格键开始游戏,属于键盘按下事件,我们在dOnKeyDown函数中编写代码。在游戏中,按下空格键,将“空格开始”隐藏,并改变游戏的状态,游戏就可以开始了。要让战机在四个方向上能够移动,必须在四个方向上给战机一个速度,通过判断键盘按下事件,可以给战机在不同方向上一个速度,当键盘抬起的时候,就在dOnKeyUp事件函数中将速度归0,这样就可以实现键盘按下,战机在相应的方向上移动,键盘抬起,战机在相应的方向上速度归0。
【实验指导】
在进行具体操作实验以前,先在LessonX.cpp中初始化我方、敌方、子弹三个精灵:
// 初始化各种模板数据
void InitTemplateData()
{
// 我们控制的Control Sprite
strcpy( g_ControlSprite.szName, “ControlSprite” );
g_ControlSprite.iHp = CONTROL_MAX_HP;
g_ControlSprite.iScore = 0;
g_ControlSprite.iDamage = 500; // 敌机碰到我,也会被击伤…
g_ControlSprite.eSpriteType = SPRITE_CONTROL;
g_ControlSprite.iBulletType = 0; // Unused
g_ControlSprite.fFireAfterCreate = 0.f; // Unused
g_ControlSprite.fBulletTime = 0.3f; // 0.3秒发射一次子弹
g_ControlSprite.fBulletTime2 = 0.f; // Unused
g_ControlSprite.fFloatTime = 0.f; // 飞行中上下浮动控制
g_ControlSprite.bFloatUp = true;
// 敌方战机
g_VerTemplate.szName[0] = ‘\0’;
g_VerTemplate.iHp = 100; // 生命值100
g_VerTemplate.iScore = 50; // 积分50
g_VerTemplate.iDamage = 500; // 碰到自身,伤害500
g_VerTemplate.eSpriteType = SPRITE_VER;
g_VerTemplate.iBulletType = 0; // Unused
g_VerTemplate.fFireAfterCreate = 1.f; // 出生1s后开始发射子弹
g_VerTemplate.fBulletTime = 1.f; // 1秒发射一次子弹
g_VerTemplate.fBulletTime2 = 0.f;
g_VerTemplate.fFloatTime = 0.f; // 飞行中上下浮动控制
g_VerTemplate.bFloatUp = true;
// 子弹
g_Bullet1Template.szName[0] = ‘\0’;
g_Bullet1Template.iHp = 1; // 生命值1
g_Bullet1Template.iScore = 10; // 积分10
g_Bullet1Template.iDamage = 0; // 伤害值由发射的Sprite决定
g_Bullet1Template.eSpriteType = SPRITE_BULLET1;
g_Bullet1Template.iBulletType = 0; // 类型由发射的Sprite决定
g_Bullet1Template.fFireAfterCreate = 0.f; // Unused
g_Bullet1Template.fBulletTime = 0.0f; // Unused
g_Bullet1Template.fBulletTime2 = 0.f; // Unused
g_Bullet1Template.fFloatTime = 0.f; // 飞行中上下浮动控制
g_Bullet1Template.bFloatUp = true;
}
给出以下全局变量:g_iGameState:游戏进行的状态:0表示结束或者等待开始;1 表示初始化;2表示游戏进行中;g_fWorldLeft,g_fWorldRight,g_fWorldTop, g_fWorldBottom这四个变量分别表示战机的左右上下四个世界边界值;g_fVelocityLeft, g_fVelocityRight,g_fVelocityTop,g_fVelocityBottom这四个变量分别表示战机在左右上 下的四个方向的速度
Main.cpp为游戏的只要框架,我的实现代码主要要在LessonX.cpp中完成,而 LessonX.h主要是用来声明变量和函数;
现在我们就在LessonX.cpp中定义以下全局变量
float g_fWorldLeft = 0.f; // 世界上下左右边界
float g_fWorldRight = 10.f;
float g_fWorldTop = 0.f;
float g_fWorldBottom = 10.f;
//
float g_fVelocityLeft = 0.f; // 控制飞机的上下左右速度
float g_fVelocityRight = 0.f;
float g_fVelocityUp = 0.f;
float g_fVelocityDown = 0.f;
定义好以上变量以后我们在LessonX.cpp中定义键盘按下事件函数OnKeyDown函数
// 函数声明
void OnKeyDown( const int iKey, const int bAltPress, const int bShiftPress, const int bCtrlPress );
// 函数实现
void OnKeyDown( const int iKey, const int bAltPress, const int bShiftPress, const int bCtrlPress )
{
// 按下空格,游戏开始
if( KEY_SPACE == iKey && 0 == g_iGameState )
{
g_iGameState = 1;
}
else if( 2 == g_iGameState )
{
// W A S D键控制移动。按键按下,给某方向的速度变量赋值
if( KEY_A == iKey )
g_fVelocityLeft = 30.f; // 左
else if( KEY_D == iKey )
g_fVelocityRight = 30.f; // 右
else if( KEY_W == iKey )
g_fVelocityUp = 15.f; // 上
else if( KEY_S == iKey )
g_fVelocityDown = 15.f; // 下
// 更新移动
UpdateMovement();
}
实现好之后要在Main.cpp的dOnKeyDown函数中调用一下,在调用之前,需要经OnKeyDown函数在LessonX.h中声明一下,形如:
extern void OnKeyDown( const int iKey, const int bAltPress, const int bShiftPress, const int bCtrlPress );
我们再在LessonX.cpp中添加鼠标抬起事件OnKeyUp的定义
// 函数声明
void OnKeyUp( const int iKey );
// 函数定义
void OnKeyUp( const int iKey )
{
if( 2 == g_iGameState )
{
// W A S D键松开,清零某方向的速度变量
if( KEY_A == iKey )
{
// 左
g_fVelocityLeft = 0.f;
}
else if( KEY_D == iKey )
{
g_fVelocityRight = 0.f;
}
else if( KEY_W == iKey )
{
g_fVelocityUp = 0.f;
}
else if( KEY_S == iKey )
{
g_fVelocityDown = 0.f;
}
// 更新移动
UpdateMovement();
}
同理,该函数也要在LessonX.h中先声明,然后到Main.cpp中的dOnKeyDown函数中调用;
其中UpdateMovement()是更新速度的函数,其实现方式如下:
void UpdateMovement()
{
// 相减原理:当键按下的时候,给变量赋值,弹起的时候,给变量清0。
// 这样当左右键同时按下的时候,两者相减为0,所以静止不动
// 松开其中的左键或者右键,该方向的变量被清0,另一方向未松开,值未被 清零,所以相减之后速度非0,就朝还没释放的按键方向移动
float fVelX = g_fVelocityRight – g_fVelocityLeft;
float fVelY = g_fVelocityDown – g_fVelocityUp;
dSetSpriteLinearVelocityX( g_ControlSprite.szName, fVelX );
dSetSpriteLinearVelocityY( g_ControlSprite.szName, fVelY );
}
由于用到了g_ControlSprite,所以我们定义结构体全局变量g_ControlSprite,
SGameSprite g_ControlSprite;
这样通过我们按下空格键,游戏的状态就变成1,然后再switch中调用相应的case1语句,进行初始化,那么我们在初始化时应该去初始化一些全局变量还有精灵的一些属性:
void GameInit()
{
g_fVelocityLeft = 0.f;
g_fVelocityRight = 0.f;
g_fVelocityUp = 0.f;
g_fVelocityDown = 0.f;
g_fVerCreateTime = 2.f;
// 初始化Control Sprite的数值
g_ControlSprite.iHp = CONTROL_MAX_HP;
g_ControlSprite.iScore = 0;
g_ControlSprite.fBulletTime = 0.3f;
InitTemplateData();
// 隐藏 “按空格开始游戏” 这个提示图片
dSetSpriteVisible( “GameBegin”, false );
}
然后进行编译就可以运行结构,查看战机是否随着键盘的按下运动。
实验五 添加子弹类,实现战机开炮
【实验内容】
写一个创建子弹的函数;
通过空格键控制飞机发射子弹;
当空格键按下时,飞机每隔0.3秒发射一发子弹;
【实验思路】
我们通过对创建子弹函数的调用,来发射子弹。当我们按下空格的时候,就去改变子弹是否可以发射的状态,如果键盘按下,则可以发射子弹,当键盘抬起就不可以在发射子弹,子弹的发射不光要看是否按下空格键,还要看游戏是否处于运行状态,如果游戏不处于运行状态,那么子弹依旧不可以发射。
【实验指导】
写出创建子弹的函数:
// 创建普通类型子弹,iBulletType:0为自己发射的子弹,1为敌方发射的子弹; Pos:发射坐标
SGameSprite *CreateBullet1( const int iBulletType, const float fPosX, const float fPosY )
{
SGameSprite *pSprite = (SGameSprite*)malloc( sizeof(SGameSprite) );
// 拷贝子弹数据模板内容,这样不需要一个一个单独赋值
memcpy( pSprite, &g_Bullet1Template, sizeof(SGameSprite) );
sprintf( pSprite->szName, “Bullet1_%d”, g_iCreatedSpriteCount++ );
pSprite->iDamage = BULLET_DAMAGE_1;
pSprite->iBulletType = iBulletType;
// 添加到列表管理
GList_AddSprite( pSprite );
// 克隆精灵的显示图片
dCloneSprite( “Bullet1_Template”, pSprite->szName );
// 给予坐标及速度
dSetSpritePosition( pSprite->szName, fPosX, fPosY );
// 敌方的子弹,方向是反的(由右向左),所以需要翻转
if( 1 == iBulletType )
{
// 固定速度
dSetSpriteLinearVelocityX( pSprite->szName, -30.f );
}
else
{
dSetSpriteFlipX( pSprite->szName, true );
// 固定速度
dSetSpriteLinearVelocityX( pSprite->szName, 60.f );
}
// 设置世界边界限制,及碰撞模式为NULL,即自行处理
dSetSpriteWorldLimit( pSprite->szName, WORLD_LIMIT_NULL, g_fWorldLeft, g_fWorldTop, g_fWorldRight, g_fWorldBottom );
return pSprite;
}
定义全局变量g_bControlCanFire,并在GameInit中初始化为false,因为在没有按下空格键的时候,是不能发射子弹的。
在OnKeyDown中的else if的最后添加:
// 游戏进行中,按下空格发射子弹
if( KEY_SPACE == iKey )
g_bControlCanFire = true;
在OnKeyUp中的if语句的最后添加:
// 游戏进行中,松开空格,停止发射子弹
if( KEY_SPACE == iKey )
g_bControlCanFire = false;
最后在GameRun中添加如下代码,发射子弹
g_ControlSprite.fBulletTime -= fDeltaTime;
if(g_ControlSprite.fBulletTime < 0 && g_bControlCanFire)
{
g_ControlSprite.fBulletTime = 0.3;
CreateBullet1( 0, dGetSpritePositionX( g_ControlSprite.szName ), dGetSpritePositionY( g_ControlSprite.szName ) );
}
这样通过编译就可以看到我方战机不但可以上下左右的飞,还可以发射子弹。
实验六 敌方战机
【实验内容】
写一个创建敌方战机的函数;
战机以编辑器中VerticalSprite_Template精灵为模板;
每隔2秒创建一架敌方战机;
敌方战机每隔1秒发射一发子弹;
敌方战机飞行中上下浮动;
【实验思路】
敌方战机的出现是有规律的,它不受我方的控制,他的子弹发射也是有规律的,到了一定的时间他就会发射,为了让敌方战机更加具有攻击性,我们使战机能够上下浮动。
【实验指导】
创建敌方战机,我们用函数CreateVerTick来实现
void CreateVerTick( float fDeltaTime )
{
// 是否到时间创建
g_fVerCreateTime -= fDeltaTime;
if( g_fVerCreateTime <= 0.f )
{
// 随机一个时间,作为下次出生的时间
g_fVerCreateTime = (float)dRandomRange( 5, 10 ) ;
// 飞机群的第一个飞机的起始Y坐标
int iPosBase = RandomRange((int)g_fWorldTop + 5, (int)g_fWorldTop + 25);
// 随机数量
int iCount = dRandomRange( VER_MIN_CREATE, VER_MAX_CREATE );
for( int iLoop = 0; iLoop < iCount; iLoop++ )
{
// 创建Sprite
SGameSprite *pSprite=(SGameSprite*)malloc( sizeof(SGameSprite) );
memcpy( pSprite, &g_VerTemplate, sizeof(SGameSprite) );
sprintf(pSprite->szName,”VerticalSprite_%d”, g_iCreatedSpriteCount++ );
pSprite->fFloatTime=(float)dRandomRange(0, 10) / 10.f; // 上升和下降都取随机值
pSprite->bFloatUp=dRandomRange(0,1) == 1 ? true : false;
dCloneSprite( “VerticalSprite_Template”, pSprite->szName );
// 添加到链表中
GList_AddSprite( pSprite );
// 坐标生成:
int iRandom =dRandomRange((int)g_fWorldRight+20, (int)g_fWorldRight + 40 );
float fPosY = g_fWorldTop + iPosBase + 10.f * iLoop;
dSetSpritePosition( pSprite->szName, (float)iRandom, fPosY );
// 固定速度
dSetSpriteLinearVelocityX( pSprite->szName, -10.f );
// 设置世界边界限制,及碰撞模式为NULL,即自行处理
dSetSpriteWorldLimit(pSprite->szName,WORLD_LIMIT_NULL, g_fWorldLeft-10.f,g_fWorldTop,g_fWorldRight+ 100.f, g_fWorldBottom );
}
}
}
检查敌机是否到时间创建子弹
//=====================================================================
// 已经创建的Ver Sprite,每个游戏循环都执行此函数。进行子弹的发射等
void VerLoopTick( SGameSprite *pSprite, float fDeltaTime )
{
pSprite->fFireAfterCreate -= fDeltaTime;
if( pSprite->fFireAfterCreate <= 0.f )
{
// 子弹1的创建
pSprite->fBulletTime -= fDeltaTime;
if( pSprite->fBulletTime <= 0.f )
{
// 子弹时间固定,不需要随机
pSprite->fBulletTime = g_VerTemplate.fBulletTime;
CreateBullet1(1,dGetSpritePositionX(pSprite->szName), dGetSpritePositionY(pSprite->szName) );
}
}
}
为了增加敌机的攻击性,我们使敌机上下浮动,所以在// 飞行中上下浮动
if( pSprite->bFloatUp )
{
// 上漂
pSprite->fFloatTime += fDeltaTime;
if( pSprite->fFloatTime >= 1.f )
{
pSprite->bFloatUp = false;
}
float fPosY = dGetSpritePositionY(pSprite->szName);
fPosY += 6.f * fDeltaTime;
dSetSpritePositionY( pSprite->szName, fPosY );
}
else
{
// 下漂
pSprite->fFloatTime -= fDeltaTime;
if( pSprite->fFloatTime <= 0.f )
{
pSprite->bFloatUp = true;
}
float fPosY = dGetSpritePositionY(pSprite->szName);
fPosY -= 6.f * fDeltaTime;
dSetSpritePositionY( pSprite->szName, fPosY );
}
这样编译运行的话,敌机依旧没有出现,因为我们还没有调用相应的函数,因为敌机是一直出现的,所以我们在GameRun函数中调用CreateVerTick函数。
并且在现在我们创建的战机中进行遍历,让每个敌机都发射自己的子弹,这样就在GameRun中添加如下代码
// 遍历当前已经创建的Sprite,执行各自的循环功能(各自的子弹发射等)
int iListSize = GList_GetListSize();
int iLoop = 0;
for( iLoop = 0; iLoop < iListSize; iLoop++ )
{
SGameSprite *pSprite = GList_GetSpriteByIndex( iLoop );
if( NULL != pSprite )
{
switch( pSprite->eSpriteType )
{
case SPRITE_VER:
VerLoopTick( pSprite, fDeltaTime );
break;
};
}
}
这样通过编译就可以得到我方战机当按下空格可以发射子弹,敌方战机上下浮动,也可以按固定的时间发射子弹。
实验七 添加碰撞检测
【实验内容】
解决;
将战机类和子弹类改为继承于CGameSprite类;
为游戏中的精灵添加HP属性,并为子弹添加破坏力的属性。
CGameSprite类添加检测碰撞的虚函数,并在各子类中实现;
当玩家战机的HP值小于0是游戏结束。
【实验思路】
当发现精灵与精灵发生碰撞的时候,我们可以判断是那种精灵碰撞,现在我们可以有四种形式的碰撞,即我方战机与敌方战机的碰撞,我方战机与敌方子弹的碰撞,我放子弹与敌方子弹的碰撞,我方子弹与敌方战机的碰撞,分别处理这些碰撞,并且加上相应的特效。还要检测相应精灵的生命值,如果生命值小于0,则相应的精灵被删除。
【实验指导】
首先在LessonX.cpp中声明函数OnSpriteColSprite;
在LessonX.cpp中定义该函数
// 精灵与精灵碰撞之后,调用此函数
void OnSpriteColSprite( const char *szSrcName, const char *szTarName )
{
if( 2 != g_iGameState )
return;
SGameSprite *pSrcSprite = IsControlSprite( szSrcName ) ? &g_ControlSprite : GList_GetSpriteByName( szSrcName );
SGameSprite *pTarSprite = IsControlSprite( szTarName ) ? &g_ControlSprite : GList_GetSpriteByName( szTarName );
// 只要有一个为空,我们就认为此碰撞无效
if( NULL == pSrcSprite || NULL == pTarSprite )
return;
// 碰撞是双方的,所以双方都处理自己的碰撞,所以Src和Tar碰撞时,需要处 // 理Src->Tar,也要处理Tar->Src
// Src 碰撞–> Tar. 4种敌机共用一个碰撞处理函数,其它都是单独专用的
if( SPRITE_CONTROL == pSrcSprite->eSpriteType )
ControlColOther( pSrcSprite, pTarSprite );
else if( SPRITE_BULLET1 == pSrcSprite->eSpriteType )
Bullet1ColOther( pSrcSprite, pTarSprite );
else
EnemyColOther( pSrcSprite, pTarSprite );
// Tar 碰撞–> Src
if( SPRITE_CONTROL == pTarSprite->eSpriteType )
ControlColOther( pTarSprite, pSrcSprite );
else if( SPRITE_BULLET1 == pTarSprite->eSpriteType )
Bullet1ColOther( pTarSprite, pSrcSprite );
else
EnemyColOther( pTarSprite, pSrcSprite );
// 死亡判断,死亡之后删除该Sprite。
// 我们控制的ControlSprite的死亡判断在GameMainLoop里进行,不在这里
if( SPRITE_CONTROL != pSrcSprite->eSpriteType )
{
if( IsDead( pSrcSprite ) )
GList_DeleteSprite( pSrcSprite, true );
}
//
if( SPRITE_CONTROL != pTarSprite->eSpriteType )
{
if( IsDead( pTarSprite ) )
GList_DeleteSprite( pTarSprite, true );
}
}
在上面的函数实现过程当中,调用了IsDead函数,该函数就是用来检测目标精灵是否还有生命值,该函数的实现代码为:
// 判断是否死亡(生命值HP小于等于0)
bool IsDead( SGameSprite *pSprite )
{
return ( NULL != pSprite && pSprite->iHp <= 0 );
}
在上面的函数实现过程当中,调用了IsControlSprite函数,该函数就是用来检测目标精灵是否为我方战机,该函数的实现代码为:
// 判断是否是我们控制的Sprite的类型
bool IsControlSprite( const char *szName )
{
return ( strcmp( g_ControlSprite.szName, szName ) == 0 );
}
在上面的函数实现过程当中,调用了ControlColOther函数,该函数就是用来处理我方战机与其他精灵相碰撞,该函数的实现代码为
// ControlSprite与其它Sprite碰撞之后的数值处理
void ControlColOther( SGameSprite *pControl, SGameSprite *pOther )
{
// 永远要注意指针使用之前的NULL判断。
if( NULL == pOther || NULL == pControl )
return;
// 碰撞到自己的子弹不处理
if( IsControlSpriteBullet( pOther ) )
return;
int iHp = 0;
// 碰撞到加血精灵,增加Hp.
if( SPRITE_HEALTH == pOther->eSpriteType )
{
// 加血精灵的Damage作为增加的Hp。且不能超过最大HP
iHp = pControl->iHp + pOther->iDamage;
if( iHp > CONTROL_MAX_HP )
iHp = CONTROL_MAX_HP;
pControl->iHp = iHp;
// 加上加血精灵的积分,如果不想让加血精灵有积分,将其积分设置为0即可
pControl->iScore += pOther->iScore;
}
// 碰撞到敌军、敌军子弹.
else
{
// 自己被对方打伤,先扣自己HP(如果自己死亡了,在下个循环里游戏即刻结束)
pControl->iHp -= pOther->iDamage;
// 如果我方的伤害大于对方的HP,则在对方的碰撞函数里,对方将死亡,在此增加积分。
if( pControl->iDamage >= pOther->iHp )
{
// 增加我方积分(只要该敌人死亡,即使是与敌人同归于尽,也是有积分的)
pControl->iScore += pOther->iScore;
}
dPlayEffect( “playerExplode”, 3.0f, dGetSpritePositionX(pControl->szName), dGetSpritePositionY(pControl->szName), 0.f );
}
// 根据各hp等级,显示不同颜色
if( pControl->iHp <= 200 )
{
// 绿蓝为0,纯红
dSetSpriteColorGreen( pControl->szName, 0 );
dSetSpriteColorBlue( pControl->szName, 0 );
}
else if( pControl->iHp <= 500 )
{
// 半红
dSetSpriteColorGreen( pControl->szName, 128 );
dSetSpriteColorBlue( pControl->szName, 128 );
}
else
{
// 正常
dSetSpriteColorGreen( pControl->szName, 255 );
dSetSpriteColorBlue( pControl->szName, 255 );
}
//更新积分显示
dSetTextValue( “CurScoreText”, pControl->iScore );
}
在上面的函数实现过程当中,调用了Bullet1ColOther函数,该函数就是用来处理普通子弹与其他精灵碰撞,该函数的实现代码为:
//Bullet1 普通子弹与其它Sprite碰撞之后的处理
void Bullet1ColOther( SGameSprite *pBullet, SGameSprite *pOther )
{
// 永远要注意指针使用之前的NULL判断。
if( NULL == pOther || NULL == pBullet )
return;
// 子弹为我方子弹
if( IsControlSpriteBullet( pBullet ) )
{
// 我方子弹碰撞到加血精灵Health、特殊子弹Bullet2、ControlSprite的时 // 候,不进行处理
if( SPRITE_HEALTH == pOther->eSpriteType || SPRITE_BULLET2 == pOther->eSpriteType || SPRITE_CONTROL == pOther->eSpriteType )
return;
// 碰到对方,自己掉血
pBullet->iHp -= pOther->iDamage;
// 子弹爆炸的特效,选用smallExplosion
dPlayEffect( “smallExplosion”, 1.0, dGetSpritePositionX(pBullet->szName), dGetSpritePositionY(pBullet->szName), 0.f );
// 我方子弹击中敌人,增加积分
g_ControlSprite.iScore += pOther->iScore;
//更新积分显示
dSetTextValue( “CurScoreText”, g_ControlSprite.iScore );
}
// 敌方子弹
else
{
// 只有在碰撞到ControlSprite及其子弹的情况才进行处理。
if(SPRITE_CONTROL==pOther->eSpriteType|| IsControlSpriteBullet( pOther ) )
{
// 碰到对方,自己掉血
pBullet->iHp -= pOther->iDamage;
// 特效
dPlayEffect(“smallExplosion”, 1.0, dGetSpritePositionX(pBullet->szName), dGetSpritePositionY(pBullet->szName), 0.f );
}
}
}
在上面的函数实现过程当中,调用了EnemyColOther函数,该函数就是用来处理普通子弹与其他精灵碰撞,该函数的实现代码为:
// 任务:Ver Sprite这种敌机与其它Sprite碰撞之后的处理
void EnemyColOther( SGameSprite *pEnemy, SGameSprite *pOther )
{
// 永远要注意指针使用之前的NULL判断。
if( NULL == pOther || NULL == pEnemy )
return;
// 只有碰撞到ControlSprite、我方子弹这两种情况才进行处理。
// 其它的,比如碰撞到敌方自己的飞机、敌方子弹、等都不做处理
if( IsControlSpriteBullet( pOther ) || SPRITE_CONTROL == pOther->eSpriteType )
{
// 自己被扣血
pEnemy->iHp -= pOther->iDamage;
// 碰撞死亡后播特效
if( IsDead( pEnemy ) )
{
if( SPRITE_BIG_BOSS == pEnemy->eSpriteType )
dPlayEffect(“bigExplode”,3.f, dGetSpritePositionX(pEnemy->szName), dGetSpritePositionY(pEnemy->szName), 0.f );
else
dPlayEffect(“enemyExplode”,3.f, dGetSpritePositionX(pEnemy->szName), dGetSpritePositionY(pEnemy->szName), 0.f );
}
}
}
调用IsControlSpriteBullet函数,该函数就是用来判断是否是我方战机,该函数的实现代码为:
// 判断是否是我们控制的Sprite的类型
bool IsControlSprite( const char *szName )
{
return ( strcmp( g_ControlSprite.szName, szName ) == 0 );
}
调用IsControlSpriteBullet函数,该函数就是用来判断是否是我方战机发出的子弹,该函数的实现代码为:
// 判断是否是ControlSprite发射的子弹
bool IsControlSpriteBullet( SGameSprite *pSprite )
{
if( NULL != pSprite )
{
// 先判断是否是子弹类型的Sprite
if( SPRITE_BULLET1 == pSprite->eSpriteType || SPRITE_BULLET2 == pSprite->eSpriteType )
return ( 0 == pSprite->iBulletType );
}
return false;
}
编译运行就可以发现,现在的我方战机可以发射子弹,并且可以摧毁敌方战机,但是我方战机即使多次受到敌方的子弹攻击,我没有死亡,这是因为我们认为游戏是一直进行的,在case2的if判断条件一直为真,为了解决这个问题,我们来改变一下case2;
case 2:
{
// 判断输赢
if( IsGameLost() )
{
// 游戏结束。调用游戏结算函数并把游戏状态修改为结束状态
g_iGameState = 0;
GameEnd();
}
else // 游戏未结束,继续游戏
{
GameRun( fDeltaTime );
}
}
其中IsGameLost的实现为:
// 是否输了 — ControlSprite Dead
bool IsGameLost()
{
return IsDead( &g_ControlSprite );
}
现在编译,我方战机可以被摧毁,但是分数没有归零,战机的位置也是随意,为了能够明显的看出是新的一局开始,我们在GameEnd函数里添加一些代码。
void GameEnd()
{
// 将控制的Sprite速度归零,坐标归于屏幕左边
dSetSpriteLinearVelocity( g_ControlSprite.szName, 0.f, 0.f );
dSetSpritePosition( g_ControlSprite.szName, g_fWorldLeft + 5.f, 0.f );
// 恢复正常颜色
dSetSpriteColorGreen( g_ControlSprite.szName, 255 );
dSetSpriteColorBlue( g_ControlSprite.szName, 255 );
// 删除本局的所有Sprite
GList_DeleteAllSprite();
// 显示 “按空格开始游戏” 这个提示图片
dSetTextValue(“CurScoreText”, 0);
dSetSpriteVisible( “GameBegin”, true );
}
实验六 读写游戏记录
【实验内容】
读取游戏最高分记录;
写入游戏最高分记录;
【实验思路】
使用文件将最高分写入到文件当中,每句打完之后可以将现在的分数和文件中的分数比较,如果本局中的分数比最高分数要高,那么就更新最高分数,如果不如原来的最高分数高,就保持原来的最高分数。
【实验指导】
因为最高分要与本局最后的分数进行比较,所以一下代码要加到GameEnd中,添加代码如下:
if( g_iMaxScore < g_ControlSprite.iScore )
{
g_iMaxScore = g_ControlSprite.iScore;
// 写文件
// [Your Code]
FILE *pfp = fopen( "Score.dat", "wb" );
if( NULL != pfp )
{
fwrite( &g_iMaxScore, sizeof(g_iMaxScore), 1, pfp );
fclose( pfp );
}
// [End Your Code]
//更新最大积分
dSetTextValue( "MaxScoreText", g_iMaxScore );
}
这样编译之后,能够保存本局的最高分,如何去保存以前的最高分呢,我们在GameInit中添加代码
static bool bInited = false;
if( !bInited )
{
// 从文件里读取历史最高积分
// 本案例只记录单一积分值,有兴趣的话,可以做多个积分的排名然后进行存储与读取
// 后面游戏结束那里,文件的存储将作为教学任务
FILE *pfp = fopen( "Score.dat", "rb" );
if( NULL != pfp )
{
fread( &g_iMaxScore, sizeof(g_iMaxScore), 1, pfp );
fclose( pfp );
//更新最大积分
dSetTextValue( "MaxScoreText", g_iMaxScore );
}
}
然后编译运行就可以看到游戏可以记录最高分数,并且每次开始游戏,都会显示以前游戏的最高分数。
上海锐格软件有限公司
PAGE \* MERGEFORMAT 16
PAGE \* MERGEFORMAT 17