C 语言课程设计四 坦克大战
一、游戏介绍
玩家坦克与敌方坦克在街道中进行巷战,玩家坦克被击中、玩家指挥部被击中或游戏时
间到,一局游戏结束。
二、实验目的
综合应用 C 语言知识和设计知识开发一款小游戏。
三、实验内容
初始界面如下图。
按下空格键后游戏开始,“空格开始”消失,载入地图,并把玩家坦克设置在指挥部左
侧。
游戏时间到,比如 30 秒,玩家坦克被敌方坦克摧毁,或者玩家指挥部被摧毁,一局游
戏结束,游戏回到初始界面,并显示上一局的分数。
游戏区域为下图中最内部的黑色区域,左上角坐标[-26, -22],右下角坐标为[26, 22]。墙
为正方形,边长为 4,坦克也是正方形,比墙略小一点。
玩家用 WASD 键控制坦克上、下、左、右运行,按 J 键开炮。玩家坦克碰到墙就停下来,
需要调转方向才能继续前进。玩家坦克开炮,一炮就能摧毁一块墙,或者一辆敌方坦克。玩
家没摧毁一辆敌方坦克,加 1 分。
玩家指挥部被坦克或者炮弹(不管玩家还是敌方)碰上,都会被摧毁。
每隔几秒钟,比如 3 秒,就会产生一辆敌方坦克。敌方坦克每隔一段时间,比如 1 秒,
就自动开炮。敌方坦克遇到墙就会停下来。停下来的坦克,前方的墙如果被摧毁了,又能继
续前进。每隔几秒钟,比如 2 秒,敌方坦克就会顺时针变换一个方向前进。
四、实验准备
本实验中可能用到的 C 语言标准库函数和 FunCode API
Stdio.h
函数原型
int sprintf( char *buffer,
const char *format,
[ argument] … ) ;
功能与返回值
把格式化的数组写入某个字
符串。
返回值:字符串长度
参数说明与应用举例
char szName[128];
int i=0;
sprintf(szName, ”feichong_%d”,
i);
将 字 符 串 ”feichong_0” 写 入 到
szName 中
Math.h
函数原型
double atan2( double y,
double x );
String.h
函数原型
extern char *strstr(char
*str1, char *str2);
功能与返回值
计算 y/x 的反正切值。
返回值:以弧度表示并介于
-pi 到 pi 之 间 ( 不 包 括
-pi)。如需使用角度,需要
转换。
=
atan2(
ftan
参数说明与应用举例
float
(x1-x0) );
计算通过点(x1,y1)到点(x0,y0)的
连成的直线与 X 轴之间的夹角。
(y1-y0),
功能与返回值
找出 str2 字符串在 str1 字
符 串 中 第 一 次 出 现 的 位 置
(不包括 str2 的串结束符)。
返回值:返回该位置的指针,
如找不到,返回空指针。
参数说明与应用举例
strstr(szName,
NULL
说明 szName 中包含 feichong
“feichong”)
!=
extern int strcmp(const
char *s1,const char * s2);
比较字符串 s1 和 s2。
当 s1s2 时,返回值>0
“feichong_0”)
strcmp(szName,
== 0
说明 szName 与 feichong_0 相等
FunCode API
函数原型
float dGetScreenLeft();
float dGetScreenRight();
float dGetScreenTop();
float dGetScreenBottom();
float dGetSpritePositionX(const
char* szName);
float dGetSpritePositionY(const
char* szName);
float dSetSpritePositionX(const
char* szName);
float dSetSpritePositionY(const
char* szName);
void
char*
const
fPosX, const float fPosY);
dSetSpritePosition(const
szName,
float
szName,
void
dSetSpriteLinearVelocityX(cons
t char* szName, const
float
fVelX);
void
dSetSpriteLinearVelocityY(const
char*
float
fVelY);
void
dSetSpriteLinearVelocity(const
char*
float
const
fVelX, const float fVelY);
float dGetSpriteRotation(const
char* szName);
const
szName,
功能与返回值
参数说明与应用举例
获取屏幕左边界值
获取屏幕右边界值
获取屏幕上边界值
获取屏幕下边界值
获 取 精 灵 中 心 点 的 X
坐标值
获 取 精 灵 中 心 点 的 Y
坐标值
设 置 精 灵 中 心 点 的 X
坐标值
设 置 精 灵 中 心 点 的 Y
坐标值
设 置 精 灵 中 心 点 的 X
和 Y 坐标值,用来将精
灵放置在某个指定位
置。
设置精灵 X 轴方向速
度
设置精灵 Y 轴方向速
度
设置精灵 X 轴和 Y 轴方
向速度
获取精灵的旋转角度
szName – 精 灵 名 称 。所 有 API
均相同。
游 戏 中 的 精 灵 的 名 称 不 能 相
同。
dSetSpritePosition(“feichong_0”,
0, 0);
将名称为”feichong_0”的精灵的
中心点设置在坐标(0, 0)上
原图的角度
调整后的角度
获得的旋转角度即为两张图片的
角度差
设置文字精灵的整数
数值
设置图片的旋转角度 fRot>0,图片顺时针旋转;
fRot<0,图片逆时针旋转。
dSetTextValue(“score”, 100);
名 称 为 score 的 文 字 精 灵 显 示
100
bVisible 为 true,可见;
为 false,不可见。
bShow 为 true,可见;
为 false,不可见。
设置精灵可见或不可
见
设置鼠标可见或不可
见
删除精灵
判 断 某 个 坐 标 点
(fPosX, fPosY) 是 否
在精灵内部
复制一个精灵。
返回值:1 – 复制成
功;
0 – 复制失败。地图
中没有找到对应名称
的精灵用于复制。
设置精灵的世界边界,
精灵碰到边界时,会激
发精灵与边界的碰撞
事件。因此,设置精灵
位置时,考虑到精灵自
身大小,最好离开边界
一段距离。
让精灵从当前位置飞
向另外一点
常用 于判 断一 个物 体是 不是 碰
到另外一个物体
做法 :一 般在 地图 上摆 放一 个
精灵 作为 模板 ,并 设置 好各 种
属性 。不 仅复 制图 片, 还复 制
属性。
szSrcName – 作为模板的精灵
szTarName – 新的精灵名称
- 左边界值
fLeft
- 上边界值
fTop
- 右边界值
fRight
fBottom- 下边界值
Limit
- 统一使用
WORLD_LIMIT_NULL
fPosX:目标点的 X 坐标值
fPosY:目标点的 Y 坐标值
fSpeed:移动速度
iAutoStop: 移 动 到 终 点 之 后 是
否 自 动 停 止 , true 停 止 false
不停止
int d = dRandomRange[0,3]
d 值可能为 0, 1, 2 或 3
float dSetSpriteRotation(const
char* szName, float fRot);
void dSetTextValue(const char*
szName, int iVal);
dSetSpriteVisible(const
void
char* szName, bool bVisible);
void dShowCursor(const bool
bShow);
void dDeleteSprite(const char*
szName);
bool
const
char*
fPosX, const float fPosY);
bool dCloneSprite(const char*
szSrcName,
char*
szTarName);
dIsPointInSprite(const
float
szName,
const
float
void
dSetSpriteWorldLimit(const
const
char*szName,
EWorldLimit Limit, const float
fTop, const
fLeft, const
float
float
fBottom)
void dSpriteMoveTo(const char
*szName,
fPosX,
const
float
fSpeed, const bool iAutoStop );
const
fPosY, const
fRight,
const
float
float
dRandomRange(const
int
iMin, const int iMax);
int
获取 一个 位于 [iMin,
iMax]之间的随机整数
五、程序初步设计
如果程序规模比较小的时候,我们习惯一上手就写代码,边写边调整。但是当程序越来
越大,代码越来越多的时候,如果我们还用这种方式编程,程序写到一半的时候,你可能会
恨不得重写一遍。
此,我们在写代码之前,先把程序功能细化一下,并初步设计好程序结构,包括数据结
构和自定义函数。有了比较清晰的思路以后,再开始开发程序。
在本项目中,我们要操作的对象有 6 个:玩家坦克、敌方坦克、玩家子弹、敌方子弹、
墙、玩家指挥部。
其中,墙和指挥部都比较简单,主要是前 4 种,而且它们有共通性:名称、速度、位置,
因此,可以考虑用一个结构体来表示。此外,我们需要用一种数据结构来管理它们。由于敌
方坦克、子弹的数量都无法事先确定,所以我们选择链表而不是数组来管理它们。
struct Weapon{
char
float
float
float
int
int
int
fSpeedY;
szName[128];
fPosX,
fPosY;
fSpeedX,
fFireTime;
iHp;
iDir;
iType;
Weapon* pNext;
// 精灵名称
// 精灵坐标
// X 和 Y 方向上速度
// 敌方坦克距下一次开炮的剩余时间
// 生命值
// 朝向:0 - 上方;1 - 右方;2 - 下方;3 - 左方
// 类型: 0 - 我方坦克;1 - 敌方坦克;2 - 我方
// 子弹; 3 - 敌方子弹
// 指向下一个节点的指针
};
其中,iDir 和 iType 用不同整数表示不同含义。如果在小程序中,我们可以在代码里直
接调用这些整数,但是想象一下下面情况:
如果你连续写了三小时代码,你还能清晰记得 1 表示什么含义吗?
你时不时需要找到 Weapon 结构体定义查看这些数字的含义,然后再引用,出错的概率
有多大?
如果你一不小心,在某处搞错了,比如要处理的是敌方坦克,你却引用 2,需要多少时
间才能把错误找出来?
因此,在做一个比较大的程序时,我们强烈建议用定义一个枚举类型,用我们熟悉的单
词来表示这种数字的含义。
enum Direction{
=
UP
RIGHT
=
DOWN =
LEFT
=
0,
1,
2,
3
// 上方
// 右方
// 下方
// 左方
};
enum Role
{
MYTANK
=
ENEMYTANK =
MYBULLET
=
ENEMYBULLET
0,
1,
2,
= 3
// 我方坦克
// 敌方坦克
// 我方子弹
// 敌方子弹
};
除此之外,我们还需要定义一些全局变量来控制程序,根据程序需求,我们目前能考虑
到的:
bool
int
float
g_bStart;
g_iScore;
g_fGameTime;
// 标识一局游戏开始还是结束
// 一局游戏得分
// 一局游戏的剩余时间
g_fCreateTank;
// 距离生成下一批敌方坦克的剩余时间
float
// 游戏地图,0 表示此处为空,1 表示此处有墙。根据游戏空间大小、墙以及坦克大小,
// 我们把地图分成 11 行,13 列,每格大小刚好放一块墙。
i nt
iMap[11][13];
正如前面所示,我们用枚举类型来表示一些数字的含义。出于同样的原因,我们也定义
一些全局常量来存储某些数值。
const
const
const
const
const
float GAME_TIME
float CREATE_TANK_TIME =
float TANK_SPEED
float BULLET_SPEED
float
FIRE_TIME
= 30.f;
5.f;
= 5.f;
= 8.f;
= 2.f;
// 一局游戏时间
// 每批次生成坦克的时间间隔
// 坦克速度
// 子弹速度
// 坦克开炮时间间隔
-26.f;
=
=
-22.f;
= 26.f;
22.f;
float WORLD_LEFT
float WORLD_TOP
float WORLD_RIGHT
float WORLD_BOTTOM =
const
const
const
const
好处有两点:第一、跟枚举类型一样,用有具体含义的单词,在具体调用时容易记住,
不会搞错;第二、如果我们需要调整这些数值,只需在全局常量初始化这里调整就可以了。
比如我们要调整坦克速度,没有定义全局常量的话,我们就要找到各处代码用到坦克速度赋
值的地方修改。这样,既麻烦又容易出错。
// 游戏场景边界左值
// 游戏场景边界左值
// 游戏场景边界左值
// 游戏场景边界左值
程序本身由一个 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、 打开 FunCode,在菜单“文件”->“打开项目…”中找到项目“坦克运动”的工程
文件,打开项目;
2、 点击按钮“设置启动 VC 工程”,设置相关版本 VC 作为启动工具;
3、 点击按钮“启动 VC 工程”,打开 VC++工程;
4、 在 List.h 中定义 Weapon 结构体和 Role、Direction 两个枚举类型。参考“程序初步
设计”。
5、 定义一个全局变量和全局常量,用来标识游戏的开始和结束。
Weapon*
bool
float
int
const float
const float
=
g_pMyTank
g_bStart
=
g_fGameTime =
g_iScore
=
GAME_TIME =
TANK_SPEED =
NULL;
false;
0.f;
0;
30.f;
5.f;
// 我方坦克
// 控制一局游戏开始 true 与结束 false
// 一局游戏的剩余时间
// 一局游戏得分
// 一局游戏的时间
// 坦克速度
6、 dOnKeyDown 函数是处理键盘按下事件的。一局游戏还未开始,按下的空格键,游
戏开始。在该函数中完成步骤一的代码。注意两点:
if(iKey == KEY_SPACE && g_bStart == false) // 游戏未开始,按下空格键
{
g_bStart = true;
g_fGameTime = GAME_TIME;
dSetSpriteVisible("kaishi", false);
dSetTextValue("time", (int)g_fGameTime);
dSetTextValue("score", g_iScore);
}
7、 运行程序,按下空格键,看看游戏是否按要求运行。
8、 游戏开始后,开始计时。游戏时间到,一局游戏结束,游戏恢复到初始界面。要判
断游戏时间,我们需要在 WinMain 的主循环中进行处理。
// 游戏主循环
while( dEngineMainLoop() )
{
// 获取游戏屏幕刷新一次的时间间距
floatfTimeDelta
dGetTimeDelta();
=
if(g_bStart)
{
g_fGameTime -= fTimeDelta;
if(g_fGameTime > 0.f)
// 一局游戏进行中