当s1=s2时,返回值=0
当s1>s2时,返回值>0
strcmp(szName,“feichong_0”)==0
说明szName与feichong_0相等
FunCodeAPI
函数原型
功能与返回值
参数说明与应用举例
floatdGetScreenLeft();
获取屏幕左边界值
floatdGetScreenRight();
获取屏幕右边界值
floatdGetScreenTop();
获取屏幕上边界值
floatdGetScreenBottom();
获取屏幕下边界值
floatdGetSpritePositionX(constchar*szName);
获取精灵中心点的X坐标值
szName–精灵名称。
所有API均相同。
游戏中的精灵的名称不能相同。
floatdGetSpritePositionY(constchar*szName);
获取精灵中心点的Y坐标值
floatdSetSpritePositionX(constchar*szName);
设置精灵中心点的X坐标值
floatdSetSpritePositionY(constchar*szName);
设置精灵中心点的Y坐标值
voiddSetSpritePosition(constchar*szName,constfloatfPosX,constfloatfPosY);
设置精灵中心点的X和Y坐标值,用来将精灵放置在某个指定位置。
dSetSpritePosition(“feichong_0”,0,0);
将名称为”feichong_0”的精灵的中心点设置在坐标(0,0)上
voiddSetSpriteLinearVelocityX(constchar*szName,constfloatfVelX);
设置精灵X轴方向速度
voiddSetSpriteLinearVelocityY(constchar*szName,constfloatfVelY);
设置精灵Y轴方向速度
voiddSetSpriteLinearVelocity(constchar*szName,constfloatfVelX,constfloatfVelY);
设置精灵X轴和Y轴方向速度
floatdGetSpriteRotation(constchar*szName);
获取精灵的旋转角度
原图的角度
调整后的角度
获得的旋转角度即为两张图片的角度差
floatdSetSpriteRotation(constchar*szName,floatfRot);
设置图片的旋转角度
fRot>0,图片顺时针旋转;
fRot<0,图片逆时针旋转。
voiddSetTextValue(constchar*szName,intiVal);
设置文字精灵的整数数值
dSetTextValue(“score”,100);
名称为score的文字精灵显示100
voiddSetSpriteVisible(constchar*szName,boolbVisible);
设置精灵可见或不可见
bVisible为true,可见;
为false,不可见。
voiddShowCursor(constboolbShow);
设置鼠标可见或不可见
bShow为true,可见;
为false,不可见。
voiddDeleteSprite(constchar*szName);
删除精灵
booldIsPointInSprite(constchar*szName,constfloatfPosX,constfloatfPosY);
判断某个坐标点(fPosX,fPosY)是否在精灵内部
常用于判断一个物体是不是碰到另外一个物体
booldCloneSprite(constchar*szSrcName,constchar*szTarName);
复制一个精灵。
返回值:
1–复制成功;
0–复制失败。
地图中没有找到对应名称的精灵用于复制。
做法:
一般在地图上摆放一个精灵作为模板,并设置好各种属性。
不仅复制图片,还复制属性。
szSrcName–作为模板的精灵
szTarName–新的精灵名称
voiddSetSpriteWorldLimit(constchar*szName,constEWorldLimitLimit,constfloatfLeft,constfloatfTop,constfloatfRight,constfloatfBottom)
设置精灵的世界边界,精灵碰到边界时,会激发精灵与边界的碰撞事件。
因此,设置精灵位置时,考虑到精灵自身大小,最好离开边界一段距离。
fLeft-左边界值
fTop-上边界值
fRight-右边界值
fBottom-下边界值
Limit-统一使用
WORLD_LIMIT_NULL
voiddSpriteMoveTo(constchar*szName,constfloatfPosX,constfloatfPosY,constfloatfSpeed,constbooliAutoStop);
让精灵从当前位置飞向另外一点
fPosX:
目标点的X坐标值
fPosY:
目标点的Y坐标值
fSpeed:
移动速度
iAutoStop:
移动到终点之后是否自动停止,true停止false不停止
intdRandomRange(constintiMin,constintiMax);
获取一个位于[iMin,iMax]之间的随机整数
intd=dRandomRange[0,3]
d值可能为0,1,2或3
五、程序初步设计
如果程序规模比较小的时候,我们习惯一上手就写代码,边写边调整。
但是当程序越来越大,代码越来越多的时候,如果我们还用这种方式编程,程序写到一半的时候,你可能会恨不得重写一遍。
此,我们在写代码之前,先把程序功能细化一下,并初步设计好程序结构,包括数据结构和自定义函数。
有了比较清晰的思路以后,再开始开发程序。
在本项目中,我们要操作的对象有6个:
玩家坦克、敌方坦克、玩家子弹、敌方子弹、墙、玩家指挥部。
其中,墙和指挥部都比较简单,主要是前4种,而且它们有共通性:
名称、速度、位置,因此,可以考虑用一个结构体来表示。
此外,我们需要用一种数据结构来管理它们。
由于敌方坦克、子弹的数量都无法事先确定,所以我们选择链表而不是数组来管理它们。
structWeapon{
charszName[128];//精灵名称
floatfPosX,fPosY;//精灵坐标
floatfSpeedX,fSpeedY;//X和Y方向上速度
floatfFireTime;//敌方坦克距下一次开炮的剩余时间
intiHp;//生命值
intiDir;//朝向:
0-上方;1-右方;2-下方;3-左方
intiType;//类型:
0-我方坦克;1-敌方坦克;2-我方
//子弹;3-敌方子弹
Weapon*pNext;//指向下一个节点的指针
};
其中,iDir和iType用不同整数表示不同含义。
如果在小程序中,我们可以在代码里直接调用这些整数,但是想象一下下面情况:
如果你连续写了三小时代码,你还能清晰记得1表示什么含义吗?
你时不时需要找到Weapon结构体定义查看这些数字的含义,然后再引用,出错的概率有多大?
如果你一不小心,在某处搞错了,比如要处理的是敌方坦克,你却引用2,需要多少时间才能把错误找出来?
因此,在做一个比较大的程序时,我们强烈建议用定义一个枚举类型,用我们熟悉的单词来表示这种数字的含义。
enumDirection{
UP=0,//上方
RIGHT=1,//右方
DOWN=2,//下方
LEFT=3//左方
};
enumRole
{
MYTANK=0,//我方坦克
ENEMYTANK=1,//敌方坦克
MYBULLET=2,//我方子弹
ENEMYBULLET=3//敌方子弹
};
除此之外,我们还需要定义一些全局变量来控制程序,根据程序需求,我们目前能考虑到表示游戏状态的变量、表示游戏得分的变量、游戏剩余时间变量以及距离下一辆坦克产生的时间等变量;
//游戏地图,0表示此处为空,1表示此处有墙。
根据游戏空间大小、墙以及坦克大小,
//我们把地图分成11行,13列,每格大小刚好放一块墙。
intiMap[11][13];
正如前面所示,我们用枚举类型来表示一些数字的含义。
出于同样的原因,我们也定义一些全局常量来存储某些数值。
constfloatGAME_TIME=30.f;//一局游戏时间
constfloatCREATE_TANK_TIME=5.f;//每批次生成坦克的时间间隔
constfloatTANK_SPEED=5.f;//坦克速度
constfloatBULLET_SPEED=8.f;//子弹速度
constfloatFIRE_TIME=2.f;//坦克开炮时间间隔
constfloatWORLD_LEFT=-26.f;//游戏场景边界左值
constfloatWORLD_TOP=-22.f;//游戏场景边界左值
constfloatWORLD_RIGHT=26.f;//游戏场景边界左值
constfloatWORLD_BOTTOM=22.f;//游戏场景边界左值
好处有两点:
第一、跟枚举类型一样,用有具体含义的单词,在具体调用时容易记住,不会搞错;第二、如果我们需要调整这些数值,只需在全局常量初始化这里调整就可以了。
比如我们要调整坦克速度,没有定义全局常量的话,我们就要找到各处代码用到坦克速度赋值的地方修改。
这样,既麻烦又容易出错。
程序本身由一个main.cpp文件组成,包含7个函数,一个主函数WinMain和6个事件函数(键盘按下、键盘弹起、鼠标移动、鼠标点击、精灵与精灵的碰撞、精灵与世界边界的碰撞)。
我们增加两个文件,List.h和List.cpp,主要用来声明和定义结构体以及链表操作的函数。
链表操作至少包括下面四个操作:
AddToList//往链表里添加一个节点;
DeleteNode//从链表里删除一个节点;
FindeNode//从链表里查找一个节点;
DeleteList//删除整个链表。
一局游戏结束,我们需要把本局游戏中还没删除的精灵全部删除,从而保持下一局游戏的“干净”,所以往往需要删除整个链表。
载入地图、玩家坦克运动、敌方坦克的生成、坦克发射子弹,我们也可以考虑定义单独的函数来完成:
LoadMap//载入地图
MoveMyTank//玩家坦克运动
CreateEnemyTanks//敌方坦克生成
OnFire//坦克发射炮弹
本游戏,大部分功能都是通过碰撞来实现的,比如玩家坦克子弹击中敌方坦克,就是玩家子弹与敌方坦克的碰撞。
子弹到了游戏界面外,就是子弹与世界边界的碰撞。
我们通过下面表格,把整个游戏中各种碰撞整理出来,表格中的响应是反应方的响应。
“无”表示不可能发生碰撞。
我们知道,要发生碰撞,碰在一起的两个精灵,必须一方具有“发送碰撞”的属性,另外一方具有“接受碰撞”的属性。
由于敌方坦克与敌方坦克、敌方子弹与敌方子弹也可能发生碰撞,所以需要同时设置“发送碰撞”和“接受碰撞”的属性
参与方方
反应
玩家坦克
发送
敌方坦克
接受
发送
玩家子弹
发送
敌方子弹
接受
发送
墙
接受
玩家指挥部
接受
世界边界
玩家坦克
发送
无
游戏结束
无
游戏结束
停止
游戏结束
停止
敌方坦克
接受
发送
停止
后面一辆调头;
对撞,都调头
删除
不处理
停止
游戏结束
顺时针调转一个方向
玩家子弹
发送
无
删除
加分
无
删除
删除
游戏结束
删除
敌方子弹
接受
发送
删除
删除
删除
均删除
删除
游戏结束
删除
墙
接受
不处理
不处理
删除
删除
无
无
无
玩家指挥部
接受
游戏结束
游戏结束
游戏结束
游戏结束
无
无
无
根据上面表格,我们可以定义5个碰撞函数
OnMyTankColOther//玩家坦克与其他精灵碰撞
OnEnemyTankColOther//敌方坦克与其他精灵碰撞
OnBulletColOther//子弹与其他精灵碰撞。
子弹碰上其他精灵,本身都是被
//删除,比较简单,因此两种子弹合并起来
OnWallColOther//墙与其他精灵碰撞
OnGoalColOther//玩家指挥部与其他精灵碰撞
其中,col是collision(碰撞)的缩写。
精灵与世界边界的碰撞,比较简单,我们直接在dOnSpriteColWorldLimit函数中完成。
我们现在对整个程序架构有了一定了解。
现在可以开始编程了,在编程的过程,我们还会根据细节进一步完善。
六、实验指南
实验一游戏开始和结束
【实验内容】
步骤一、按空格键,游戏开始,“空格开始”字样消失,设置初始时间为30。
步骤二、按WASD键,控制坦克上下左右运动。
步骤三、游戏开始后,右上角实时显示剩余时间。
步骤四、当超过30秒,游戏结束,重新显示“空格开始“字样,游戏时间设为0,坦克回到初始位置。
【实验思路】
在List.h中定义Weapon结构体和两个枚举类型。
定义全局变量和全局常量。
定义MoveMyTank函数,控制玩家坦克上下左右移动。
【实验指导】
1、创建“TankWar”工程,并且添加List.h文件,并在List.h中定义Weapon结构体和Role、Direction两个枚举类型。
参考“程序初步设计”;
2、接着我们要定义一个游戏状态的变量,依次变量来判断游戏是否开始,并且还要定义我们控制的坦克精灵的对象指针;
3、dOnKeyDown函数是处理键盘按下事件的。
一局游戏还未开始,按下的空格键,游戏开始。
此时应该“空格开始”消失,显示本局的游戏时间以及得分等信息,
4、运行程序,按下空格键,看看游戏是否按要求运行。
5、游戏开始后,开始计时。
游戏时间到,一局游戏结束,游戏恢复到初始界面。
要判断游戏时间,我们需要在WinMain的主循环中进行处理。
//游戏主循环
while(dEngineMainLoop())
{
//获取游戏屏幕刷新一次的时间间距
floatfTimeDelta=dGetTimeDelta();
if(g_bStart)
{
g_fGameTime-=fTimeDelta;
if(g_fGameTime>0.f)//一局游戏进行中
{
}
else//一局游戏结束
{
g_bStart=false;
}
}
else//游戏结束后处理
{
}
}
6、一局游戏开始后,在WinMain函数中的主循环里,通过dSetTextValue把(int)g_fGameTime的数值显示在名为“time”的文字精灵里,从而实时显示剩余时间(显示整秒的时间)。
7、一局游戏进行中,每循环一次,g_fGameTime减去一次刷屏的时间。
当g_fGameTime≤0,说明一局游戏时间已完,游戏停止,重新显示“空格开始“字样。
注意:
g_iStart要改为0,否则下一局就不能正确判断游戏开始或结束。
8、我们定义一个MoveMyTank函数来处理玩家坦克的运动。
因为玩家坦克是用键盘控制的,所以需要传递两个参数,一个用来表示按下或松开了哪个键盘,一个用来表示是按下还是松开。
按下W键,设置玩家坦克向上的速度;按下A键,设置向左速度;按下S键,设置向下的速度,按下D键,设置向右速度。
松开键盘,坦克速度为0。
此外,按下键盘时,还需要相应设置坦克朝向iDir。
注意:
如果函数定义写在函数调用之后,需要在函数调用前面进行声明。
voidMoveMyTank(intiKey,intiPress)
{
}
9、完成函数,我们在dOnKeyDown和dOnKeyUp函数中进行调用。
//按下键盘
if(g_bStart)//一局游戏进行中
{
if(iKey==KEY_W||iKey==KEY_A||iKey==KEY_S||iKey==KEY_D)
{
MoveMyTank(iKey,1);
}
}
//松开键盘
if(g_bStart)
{
if(iKey==KEY_W||iKey==KEY_A||iKey==KEY_S||iKey==KEY_D)
{
MoveMyTank(iKey,0);
}
}
10、玩家坦克开到边界时,让它停止运动。
我们在dOnSpriteColWorldLimit函数中,设设置玩家坦克速度为0。
if(strcmp(szName,g_pMyTank->szName)==0)//玩家坦克
{
dSetSpriteLinearVelocity(g_pMyTank->szName,0.f,0.f);
}
11、因为增加了新功能,我们在一局游戏结束时,就必须考虑到新情况:
游戏结束时,坦克已经不在原来位置了;坦克朝向发生变化;游戏结束的时候,玩家正好按下某个控制键,给坦克设置了速度。
我们该如何处理这三种情况?
实验二玩家坦克在街道中运行
【实验内容】
步骤一、一局游戏开始时载入地图。
步骤二、一局游戏结束后删除地图。
步骤三、玩家坦克碰到墙的话,不能继续前行。
【实验思路】
利用数组的值来载入地图,值为0,说明此处为空;值为1,说明此处有墙。
游戏开始后载入地图,但只能载入一次。
如果不停载入,会占有大量资源。
游戏结束时,需要把地图中墙都删除。
创建一个新的精灵,首先需要给它命名。
有规则地给精灵命名,有利于我们对精灵进行处理。
我们按照墙生成的先后顺序,分别给精灵命名为:
wall0,wall1,wall2…
设置玩家坦克的碰撞属性,并专门定义一个函数处理玩家坦克发生碰撞后,玩家坦克的响应。
【实验指导】
1、增加新的全局变量定义
intg_iWallCount=0;//记录墙的数量
intg_iMap[11][13];//地图数组
一局游戏结束后,我们需要卸载地图,也就是把墙都删除了。
因此,我们需要知道墙的数量。
2、定义LoadMap函数,用来载入地图。
定义一个局部数组并初始化,然后依次赋值给全局数组。
局部数组初始化如下:
intiMap[11][13]=
{
{0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,1,0,1,0,1,0,1,0,1,0,1,0},
{0,1,0,1,0,1,0,1,0,1,0,1,0},
{0,1,0,1,0,1,1,1,0,1,0,1,0},
{0,1,0,1,0,1,0,1,0,1,0,1,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,1,1,1,0,0,0,0,0},
{0,1,0,1,0,0,0,0,0,1,0,1,0},