「坦克大战」项目的设计文档
功能设计
游戏部分
在存在 障碍物 的地图上,玩家需要操作一辆绿色的坦克与敌对的灰色坦克作战。 坦克 以及其发射的 炮弹 均会被障碍物所阻挡。玩家用键盘上的 wasd按键 控制坦克移动,用 鼠标 控制炮弹发射方向,按下 空格键 以发射炮弹。而敌方坦克则会自主移动并发射炮弹攻击玩家。炮弹碰到障碍物时会 反弹 ,但是反弹超过一定次数时炮弹就会消失。击毁敌方坦克可以得到一定的分数。每一关均有不同通关条件,分别是一星、二星、三星需要的通关分数。当玩家 用完炮弹 或 操作的坦克被击毁 时,游戏结束。
关卡设计部分
为了让玩家体验更多的快乐,并将快乐分享给其他人 (互相折磨) ,我们开发了 自定义关卡 功能。玩家可以自主设计关卡,将其储存到自己的存档里或生成 分享码 (一个由数字和符号构成的字符串),而其他玩家可以通过输入分享码导入关卡。除了完全自主设计之外,玩家也可以在其他关卡的基础上进行设计 ,让它更加折磨人 。
控制部分
为了便于保存游戏进度,我们提供了 存档功能 ,将游戏自带文件以及玩家存档都放到游戏目录下的 datas 文件夹中。存档中包含了玩家各个关卡的进度,以及玩家自定义的关卡。
整体架构设计
输出图形时所使用的工具
考虑到 C++ 自带的控制台不能绘制出这一游戏的画面,我们上网查阅了资料,决定使用 Easyx 图形库。使用 Easyx 可以便捷地画出直线、圆、矩形等几何图形,输出文本,导入图片,检测鼠标和键盘的状态。基于这些功能我们就能绘制出游戏中的坦克、炮弹、障碍物,以及控制游戏时用到的按钮、文本框、消息窗口了。
控制部分
widget
类有三大功能:初始化, 页面跳转 ,以及绘图。初始化时构造背景数据,并用list容器存储;绘图时迭代器访问并输出;页面跳转:widget
类通过 currentindex
(记录当前页面的编号)这个变量实现功能的分类。在接受鼠标和键盘信息后,通过 button
类改变 currentindex
的值,再调用不同的函数实现对应的功能。点击“开始游戏”则运行 Level_Editor()
函数,点击“设计关卡”则通过 look_up_pages()
函数查看并选择关卡,选择完毕就进入 Game
类的成员函数 run()
中运行游戏。
游戏的运行
通过 Game
类来实现运行功能。首先数据初始化,以及背景图绘制,接着点击“开始游戏”则开始运行游戏,在每次键盘和鼠标操作后,改变对应数据(坦克,炮弹,背景图中的分数等),并通过双缓冲统一绘制出来,以及改变帧率以此改善画质体验感。最后通过 is_win
这个布尔量判断是否结束循环,展示相应的动画的同时保留并改变该用户相应的数据。
自定义关卡 的实现思路与之类似。
存档部分
通过 File_Manager
类来实现文件操作的功能。游戏一开始,先检测游戏文件是否完整,接着读取所有玩家的用户名和密码以完成登录。当玩家在某一关卡创造了新纪录,或保存正在编辑的关卡时,都要将新的玩家数据存档。我们按照一定规律编排数据,使得玩家信息、关卡信息都可以转化成字符串,同时也可以从字符串中读入这些信息。
数据结构设计
存档部分
使用 Level_Data
类储存一个关卡的数据,包含坦克的初始位置、障碍物的位置、通关条件等信息。使用 Player_Data
类储存一个玩家的数据,包括用户名、密码、得分等。
以下展示这两个类的部分定义:
struct d_wall //储存障碍物数据的结构体
{
int stx,sty,edx,edy; // 左上角、右下角的坐标
};
struct d_player //储存我方坦克数据的结构体
{
int dir,x,y; // 初始坦克位置
int bullet_num; // 初始炮弹数量
};
struct d_enemy //储存敌方坦克数据的结构体
{
int dir,x,y,seed,score;
// 朝向( 0 表示横向, 1 表示纵向),初始坦克位置,随机种子,分值
};
class Level_Data
{
public:
string name; // 关卡名
int score3,score2,score1; // 通关的分数要求
list<d_wall> wall; // 障碍物
d_player player; // 我方坦克
list<d_enemy> enemy; // 敌方坦克
}
class Player_Data
{
public:
int file_id; // 玩家存档的编号
string name; // 用户名
string password; // 密码
list<int> score; // 所有关卡的得分(先是固定关卡,再是自定义关卡)
list<string> c_level; // 自定义关卡的关卡分享码
}
游戏运行部分
当玩家点进一个关卡后,我们通过 Game
类完成一个关卡的运行。 Game
类中包含若干个成员,其中 optank
类型的数组 Enermy
储存的是 敌方坦克 的数据, tank
类型的变量 my_tank
储存的是 我方坦克 的数据, Bullet
类型的链表 bullet
和 optank_bullet
分别储存 我方坦克发射的炮弹 和 敌方坦克发射的炮弹 的数据, Wall
类型的链表 wall
储存 障碍物 的数据。
Game
类的定义如下:
class Game{
private:
list<Wall> wall; //障碍物
list<Bullet> my_bullet; //我方坦克发射的炮弹
list<Bullet> optank_bullet[30]; //敌方坦克发射的炮弹
optank Enermy[30]; //敌方坦克
tank my_tank; //我方坦克
Level_Data lvl; //这个关卡的数据
int num; //敌方坦克数量
}
关键算法设计
多线程的实现
在游戏中,我们需要让敌方坦克、炮弹同时移动,同时能够让玩家随时操作我方坦克移动或攻击。但是 C++ 的程序是单线程运行的,这就需要我们把多线程的任务拆分成更小的子任务,每隔 20 毫秒依次执行各个子任务。我们拆分的子任务依次为:
- 检测鼠标左键是否被按下,如果是的话,我方坦克发射一枚炮弹;
- 敌方坦克移动;
- 我方炮弹移动;
- 敌方坦克发射炮弹;
- 敌方坦克炮弹移动;
- 判断我方坦克是否被敌方炮弹击中;
- 判断敌方坦克是否被我方炮弹击中;
- 如果分值发生变化,更新屏幕上显示的分值。
在实现 文本框、 按钮 ,制作 自定义关卡 界面时,也同样采取了这种思路。
图形的绘制
尽管有了 Easyx 这一工具,也要通过拼接各种几何图形完成游戏内容的绘制。
-
文本框:边框为圆角矩形,内部绘制文本;
-
按钮:先绘制有内部填充的圆角矩形,再在内部绘制文本;
-
坦克:先绘制有内部填充的矩形(车身),再绘制由坦克中心指向鼠标的线段,最后绘制有内部填充的圆形(炮塔);
-
子弹:有内部填充的圆形。
这些图形的展示图如下:
后期在对游戏界面进行美化时,我们还制作了启动游戏时的渐变动画。这则是通过每隔40毫秒计算每个像素点的RGB值,并重新输出实现的(后来发现这样对于电脑显卡的要求很高,但是目前也想不到优化的方案)。