NJU南京大学高程课设:坦克大战(BattleCity)

高程课设:坦克大战

代码已上传至Github, 请关注Github账号:SH_Hattrick
代码传送门

.
.
.


2020年4月,大一时编写,现在看来代码还有很高的改进空间(高情商说法)

.
.
.

本次课设完成了对4399小游戏:坦克大战的低配模拟。

接下来我将从以下几个方面介绍本次课设的内容。

  1. 设计内容和程序功能
  2. 模块划分和数据结构安排
  3. 设计收获

设计内容与程序功能

本次坦克大战项目的大体功能如下:

  1. 实现三种类型的坦克:重型(HeavyTank),装甲(ArmoredTank),轻型(LightTank).
  2. 实现三种地形(基地本身算一种特殊地形,因此应是四种),砖墙,铁墙,草地
  3. 实现三种buff:黄色:回复全部生命值。绿色:提升攻击力。紫色:暂时无敌
  4. 右侧状态栏打印总用时;已摧毁敌军数,以及剩余生命值(上图中没有体现剩余生命值,因截图在实现该功能之前)。
  5. 三种地形的效果基本与经典游戏相同:砖墙(BrickWall)可被一枚炮弹摧毁,铁墙(IronWall)不可摧毁,但可以被吃到绿色道具的玩家摧毁,草地(Grass)可穿过,炮弹可穿过,不可摧毁。
  6. 三种坦克生命值分别为5,4,3

这就是本次课设的具体设计内容,接下来时具体模块划分。

模块划分与数据结构安排

本次课程设计有以下头文件:

Tank.h Tool.h AITank.h Game.h Control.h。

Class Widget:

所有类的基类。坦克(Tank),炮弹(Shell),地形(Block),加成(Buff)全部由它继承而来。

该类设计如下

protected:
	int _x = 0;//横坐标
	int _y = 0;//纵坐标
	int x_size = 0;//横轴大小
	int y_size = 0;//纵轴大小
	int health = 0;//血量
	int maxhealth = 0;//最大血量
	IMAGE rim;//图像
Public:
	bool existence = false;
	bool penetrable = false;
	Widget(int x, int y, int h) :_x(x), _y(y), health(h) {
    
     
		existence = true;
		loadimage(&rim, _T("../images/black.jpg"), XSIZE, YSIZE);
	}
	void undrawWidget();
	virtual void drawWidget() = 0;
	int giveHealth();
	void reduceHealth(int r);
	pair<int,int> giveCoordinate();

显然,不管是tank,buff,block,初始化时都必须有一个坐标值使它在地图上有所显示。大部分block,tank都需要生命值变量。而采用easyx绘图库的IMAGE类,在初始化时为每一种基类链接一个图像,便于绘图。基类本身的IMAGE为纯黑图像。

existence和penetrable用于判断该对象是否已经“死亡”或它是可穿越的。

Widget类实现的方法有: 给出坐标,给出生命值,undraw即以背景色重绘,相当于擦去图像,纯虚函数由各类实现,因并非所有对象都是简单的正方形贴图。

各个基类(Tank单独介绍):

下列类在Tool.h中实现

//炮弹类
class Shell : public Widget
{
    
    
protected:
	int damage = 0;
	int x_direction = 1;
	int y_direction = 0;
	int speed = 8;
	Widget* towner;
public:
	friend class Tank;
	Shell(){
    
    }
	Shell(int x, int y, int dam, int sp, int xdir,int ydir) :
		Widget(x, y, 4), damage(dam), x_direction(xdir),y_direction(ydir) {
    
     x_size = 5; y_size = 5; }
	void undrawWidget();
	void drawWidget();
	void owner(Widget*);
	string getDir();
	int collide(Widget*);
	void Move();
};



//地形基类
class Block : public Widget
{
    
    
protected:
public:
	Block(int x, int y, int h) :Widget(x, y, h) {
    
     x_size = XSIZE, y_size = XSIZE; };
	void drawWidget();
};



//加成道具基类
class Buff : public Widget
{
    
    
protected:
public:
	friend class Tank;
	Buff(int x, int y) :Widget(x, y, 1) {
    
     penetrable = true; isbuff = true; }
	virtual void buffTank(Tank*) = 0;
};

坦克类

坦克类在tank.h中实现

class Tank : public Widget
{
    
    
protected:
	
	int power;
	int speed;
	int x_dir = 0;
	int y_dir = -1;

	bool existence = false;
	bool isplayer = false;
	IMAGE playertank_up;
	IMAGE playertank_down;
	IMAGE playertank_right;
	IMAGE playertank_left;
public:
	int id;
	int steptimes = 0;
	int shtimes = 0;
    //因tank有转向问题,所以有四个方向的贴图
    //基类中为playertank的贴图,在各个子类中为正常敌军tank的贴图
	Tank(int p, int h, int x, int y, int s) :Widget(x, y, h), power(p), speed(s) {
    
    
		existence = true; 
		isatank = true; 
		loadimage(&playertank_up, _T("../images/player1tank_up.jpg"), XSIZE, YSIZE);
		loadimage(&playertank_down, _T("../images/player1tank_down.jpg"), XSIZE, YSIZE);
		loadimage(&playertank_right, _T("../images/player1tank_right.jpg"), XSIZE, YSIZE);
		loadimage(&playertank_left, _T("../images/player1tank_left.jpg"), XSIZE, YSIZE);
		srand((unsigned int)time(NULL));
		id = rand() % 100;
		maxhealth = health;
	}
	virtual Shell* Attack();
	virtual void Damaged(Shell*);
	void Move(int nxdir,int nydir);
	void changeDir(int nxdir, int nydir);
	void setplayer() {
    
     isplayer = true; }
	void playerDrawWidget();
	void instantKill() {
    
     power = 5000; }
	void setID(int i);
	void restoreHealth() {
    
     health = maxhealth; }
	string giveDirection();
	bool ifDead();
};

上述类衍生的基类将不再一一介绍。

Game.h:游戏主进程安排

欢迎界面和结束界面不再介绍,主要来介绍主进程gameProcess函数。

进入该函数后首先是对各种资源的初始化:

//地图资源
	vector<vector<Widget*>> mapWidgets;
	//炮弹对象
	vector<Shell*> allShells;
	//敌军对象
	vector<Tank*> enemies;
	//可穿越物对象
	vector<Widget*> can_be_penetrated;
	//Buff对象
	vector<Buff*> allBuffs;
	//加载地图
	initMap(mapWidgets, can_be_penetrated,allBuffs);

地图以20*16的二维数组储存,目前看来似乎将数组再扩大一点会更合理,采用40*32的地图会更好。还有一些小变量来记录游戏中的各种数据。

在初始化地图中,数组**vector<vector<Widget*>> **将储存所有的地图信息,在初始化时顺便调用各个对象的绘图函数(drawWidget)。炮弹和buff单独有数组进行储存,可穿越物对象将定时重绘,砖墙铁墙这样的对象在初始化后不会循环进行重绘。

接下进入while循环。该循环会做如下工作:

  1. 查看现存敌军坦克数量,如数量不足,将生成新的敌军tank。
  2. 判断胜利与失败条件。
  3. 绘制可穿越物对象。
  4. 加载线程:while循环中采用多线程(Thread),为每一个敌军tank和玩家坦克,以及炮弹数组allshells分配一个线程,交给线程里的函数控制。

这就是游戏的静态框架,接下来介绍关于动态控制的函数。

Control.h与AITank.h

这两个文件中主要起作用的为以下三个函数:

allTankCtrl AITankCtrl ShellCtrl

  1. allTankCtrl用于控制坦克移动和发射炮弹,接受一个Tank*和地图二维数组的引用作为参数,当Tank*为玩家控制时,将根据玩家按键进行操作,如果发射炮弹,则使用C++11中future+promise的线程返回值机制将发射的炮弹传递给allshells。
  2. AITankCtrl用于电脑坦克控制,allTankCtrl中有一个参数char cmd = 0。玩家调用时不会对其赋值,当AI调用h时,将显式地将AI命令以cmd参数传入,这样,allTankCtrl将不会接受键盘信息。AITankCtrl中是一系列尽可能使得电脑坦克智能的寻路算法。
  3. ShellCtrl将控制炮弹的飞行,再击中不可穿越对象时(penetrable == false)时调用其collide(Widget*)函数,判断此次collide的后果。

以上就是本程序的模块划分与数据结构,以及游戏主框架的搭建。

设计收获

本次设计中,主要用到了一下几种此前没有使用过的机制

  1. 继承概念。虽然早就学过相应的语法,但此次设计首次在大项目中应用。首次尝试,回看代码,确实有很多不够OOP的地方,没有最大化地利用面向对象的优势。
  2. 多线程概念。熟悉了C++11的Thread,future,promise库,不过尚未很好地理解使用线程的意义,这次就算是小小的体验一把。

这次发现自己的代码设计其实问题很大,意识到了***design then code***的重要性,下次在打开编译器之前一定要彻底地对程序进行分析,写一份完整的设计方案。

数据结构安排不太合理,这导致代码的可拓展性比较差,还是好好跟着高年级的学长多学学吧!

以上即本次报告的全部内容。

猜你喜欢

转载自blog.csdn.net/natrick/article/details/113633034