

C++开发,使用工具为vs2019的community版本,坦克大战需要借助EasyX库来完成坦克大战的图形绘制。
(1)玩家移动及发射炮弹:
单人版:玩家通过W,S,A,D分别控制坦克进行上、下、左、右的移动,J键表示发射炮弹。
双人版:玩家一通过W,S,A,D分别控制坦克进行上、下、左、右的移动,J键表示发射炮弹;
玩家二通过↑↓←→分别控制坦克进行上、下、左、右的移动,1键表示发射炮弹。
敌方坦克移动及发射炮弹:自动移动,自动发射。
(2)敌方坦克设置有四种不同分值的坦克(从100到400分四种,以下分别称为一、二、三、四级敌方坦克),从一级到四级敌方坦克攻击速度逐渐增长。
(3)玩家从开始游戏时会有三条生命(即三次游戏机会),进入下一关生命值不会重新刷新。
(4)对于第一敌方坦克,玩家炮弹击中一次则死亡。对于四级敌方坦克,玩家炮弹需要击中四次才死亡。同理可得二、三级敌方坦克死亡规则。(但由于游戏目前尚未能解决的bug,有时四级敌方坦克击中一次可会死亡。一、二、三级敌方坦克暂未出现此bug)
若玩家被敌方坦克击中,则玩家死亡,失去一条生命,重新在出生地出生。当玩家生命值全部失去,则玩家失败,游戏结束。
同时,玩家需要守家(即守卫自己的出生地),一旦玩家出生地被攻击也意味着游戏结束。
(5)草地地图可隐藏坦克踪迹;
河流地图坦克无法经过,但炮弹可以经过。
(6)玩家每攻击掉敌方坦克可以进行升级,当玩家升至4级,炮弹可击穿铁块。而当玩家重新出生时,等级会重置。敌方坦克不能打掉铁块。
(7)一旦游戏开始,玩家不能暂停游戏。
(8)玩家出生地固定在出生点(即家),敌方炮弹随机智能地出现在地图上任一出生点。
(9)每个关卡对应不同的地图、背景音乐及难度。
(1)整体布局清新简洁。
(2)色调、背景、图标均仿照或选取于经典坦克大战游戏中,为老玩家找回童年的快乐。
(3)游戏中界面主要显示玩家坦克、敌方坦克、障碍物、玩家出生地等。
(4)游戏界面右侧上方实时显示当前剩余敌方坦克数量。
(5)游戏界面右侧下方实时显示当前玩家所剩生命值和游戏关卡数。
(6)游戏每当击中死亡一个敌方坦克时,在死亡地会短暂出现该敌方坦克所价值分值后消失。
(6)游戏结束后跳出分数面板页面,显示本次玩家所获得分数。包括玩家一、玩家二个人所获得总分数和分别击中一、二、三、四级敌方坦克的个数和获得的单项分数。
创建一个Tankunit类:用来存储所有的坦克都有的属性。共有属性:方向,前进方向和射击方向;装甲等级,不同装甲类型的坦克有不同的特点,区分不同类型坦克等级。
创建一个PlayerUnit类:用来存档玩家的坦克。新增变量lives,用来存放玩家剩余坦克数亮。这个变量将会继承于Tank类。
创建一个ComputerPlayerunit:用来存档电脑玩家,新增静态变量,用来保存敌军剩余坦克数量,Left Ops=20:每一关卡20个敌军坦克。
创建一个map-res类:对地图信息的重新处理,用以存储每关的地图信息。
创建一个Music类:用以存储并处理音乐相关的函数及内容。
创建一个Setting类:用以存储所有相关的宏定义及基础类的初始值。
创建一个Timer类:用以分离时间相关的所有函数。
创建一个GameWindow类:用以窗口类的相关函数以及窗口动画。
//参数及类的设定(部分)
enum BulletShootKind {None, Player_1 = PLAYER_SIGN, Player_2 = PLAYER_SIGN + 1, Camp, Other}; //障碍物标记
enum BlastState { Blasting, BlastEnd, NotBlast };//坦克状态
enum TANK_KIND { PROP, COMMON }; //坦克类型
enum Star_State {Star_Timing,Star_Failed,Star_Out,Star_Showing,Star_Stop,Tank_Out};
struct BoxMarkStruct
{
int box_8[26][26];
int box_4[52][52]; // 墙击中标记
int bullet_4[52][52]; // 子弹标记
};
struct BulletUnit
{
int x, y; // 子弹坐标
int dir; // 子弹方向
int speed[4]; // 子弹速度
int mKillId; // 玩家或者电脑坦克的数字标记
static IMAGE mBulletImage[4]; // 图片
static int mBulletSize[4][2];
static int devto_tank[4][2];
static int devto_head[4][2];
static int bomb_center_dev[4][2];
};
struct BombStruct
{
static IMAGE mBombImage[3]; // 子弹爆炸图
int mBombX, mBombY; // 爆炸点坐标
bool canBomb;
int counter; // 计数器
};
类的继承与关系流程图(逻辑概念)

(1)加载图片
采用嵌套循环加载玩家坦克图片并提前对图片资源进行处理,一开始加载图片的尺寸比原始尺寸大了几倍,所以每次对图片处理需要更多的CPU资源,所以缩小图片加载到内存中的尺寸,并减小占用的空间,这样处理之后可使CPU占用率从24%降到7%左右。


TankUnit::TankUnit(byte player, byte level)
{
switch(player)
{
case 1:
{
for ( int i = 0; i < 4; i++ )
{
_stprintf_s(c, L"./res/%dPlayer/m%d-%d-1.gif", player, level, i);
loadimage(&CPTankPic[i][0], c);
_stprintf_s(c, L"./res/%dPlayer/m%d-%d-2.gif", player, level, i );
loadimage(&CPTankPic[i][1], c);
}
}
break;
default:
throw _T("数值越界, TankUnit.cpp-> TankUnit construct function");
}
}
(2)绘制坦克
采用EaxyX系统函数,绘制图像,需要在绘图界面进行两次绘图。第一次,用遮罩图和背景进行按位与的操作,第二次,用真正坦克图和背景图进行按位或的操作。在移动之前应该把原来的坦克图像去除掉。背景颜色的更换,在EasyX资料中提到需要用cleardevice()函数和setbkcolor()函数进行配合。可通过这两函数对背景颜色进行更换。
void putimage(
int dstX, // 绘制位置的 x 坐标
int dstY, // 绘制位置的 y 坐标
int dstWidth, // 绘制的宽度
int dstHeight, // 绘制的高度
IMAGE *pSrcImg, // 要绘制的 IMAGE 对象指针
int srcX, // 绘制内容在 IMAGE 对象中的左上角 x 坐标
int srcY, // 绘制内容在 IMAGE 对象中的左上角 y 坐标
DWORD dwRop = SRCCOPY // 三元光栅操作码);
地图数据利用map-res.cpp提前存储
//这里仅展示了第1关与第35关的地图信息
//一共设计了35关卡,所以一共有35张地图
class MAP
{
public:
MAP() {}
~MAP() {}
void map_1();
void map_2();
void map_3();
......
}
void MAP::map_1()
{
char s[26][27] = {
"00000000000000000000000000",
"00000000000000000000000000",
"00330033003300330033003300",
"00330033003300330033003300",
"00330033003300330033003300",
"00330033003300330033003300",
"00330033003355330033003300",
"00330033003355330033003300",
"00330033003300330033003300",
"00330033000000000033003300",
"00330033000000000033003300",
"00000000003300330000000000",
"00000000003300330000000000",
"33003333000000000033330033",
"55003333000000000033330055",
"00000000003300330000000000",
"00000000003333330000000000",
"00330033003333330033003300",
"00330033003300330033003300",
"00330033003300330033003300",
"00330033003300330033003300",
"00330033000000000033003300",
"00330033000000000033003300",
"00330033000333300033003300",
"00000000000300300000000000",
"00000000000300300000000000" };
for (int i = 0; i < 26; i++)
{
strcpy_s(buf[i], s[i]);
}
}
void MAP::map_35()
{
char s[26][27] = { "00000000000000000000000000",
"00000000000000000000000000",
"00000000330033000000000000",
"00000000330033000000000000",
"11000011331133110000110000",
"11000011331133110000110000",
"33111133333333331111331100",
"33111133333333331111331100",
"33333333553355333333331100",
"33333333553355333333331100",
"44444433333333334444441100",
"44444433333333334444441100",
"44333333333333333333444411",
"44333333333333333333444411",
"33333344333333443333331111",
"33333344333333443333331111",
"33334444443344444433334444",
"33334444443344444433334444",
"11444411111111114444114411",
"11444411111111114444114411",
"00111100000000001111001100",
"00111100000000001111001100",
"00000000000000000000000000",
"00000000000333300000000000",
"00000000000300300000000000",
"00000000000300300000000000"};
for (int i = 0; i < 26; i++)
{
strcpy_s(buf[i], s[i]);
}
}
初次绘制之后,地图显示的墙、草、水和铁并无区别,在初次绘制的基础上,进行加工,采用多次绘图的方式,使得坦克在穿过草丛时显示在草丛下面,而在经过冰面时显示在冰面上面。
// 森林地图展示
for (int i = 0; i < 26; i++)
{
for (int j = 0; j < 26; j++)
{
x = j * BOX_SIZE;
y = i * BOX_SIZE;
if (CPBoxMarkUnit->box_8[i][j] == _FOREST)
TransparentBlt(CPCenter_HDC, x, y, BOX_SIZE, BOX_SIZE, GetImageHDC(&ForestPic), 0, 0, BOX_SIZE, BOX_SIZE, 0x000000);
}
}
(1)移动
a.怎样将他的坐标设置为持续变化的状态?
先用一个变量保存坦克的坐标,然后对这个变量进行修改,让他的x或者y坐标进行变化,再传输到P1这个对象当中。此时还得防止坦克运动过快的问题,只需加上一个Sleep函数,让每次执行循环一次之后休眠一段时间sleep的休眠时间可以根据想要效果进行更改。
b.初次玩家坦克移动实现了玩家通过WSAD控制玩家坦克的移动,但此时出现了一个问题,在玩家同时按下WA或者WD等时玩家坦克会出现斜着走的问题,为了解决这一问题,增加了键盘上的按键监听,使得每次只能按下一个方向的按键。
if (GetAsyncKeyState('A') & 0x8000)
{
if (PLOn && CPTankDir == DIR_LEFT )
{
PLAiMove = true;
PLAiCount = 0;
PLRandCount = rand() % 8 + 7;
}
PLMoving = true;
Move(DIR_LEFT);
}
else if (GetAsyncKeyState('W') & 0x8000)
{
if (PLOn && CPTankDir == DIR_UP)
{
PLAiMove = true;
PLAiCount = 0;
PLRandCount = rand() % 8 + 7;
}
PLMoving = true;
Move(DIR_UP);
}
else if (GetAsyncKeyState('D') & 0x8000)
{
if (PLOn && CPTankDir == DIR_RIGHT)
{
PLAiMove = true;
PLAiCount = 0;
PLRandCount = rand() % 8 + 7;
}
PLMoving = true;
Move(DIR_RIGHT);
}
else if (GetAsyncKeyState('S') & 0x8000)
{
if (PLOn && CPTankDir == DIR_DOWN)
{
PLAiMove = true;
PLAiCount = 0;
PLRandCount = rand() % 8 + 7;
}
PLMoving = true;
Move(DIR_DOWN);
}
else if (PLMoving)
{
PLMoving = false;
}
c.电脑坦克的AI移动,在电脑坦克移动过程中,需要重新对电脑坦克的方向做出设定。
if (unit1->renewXYPos())
{
ctrl(*unit1, map,*unit2);
}
if (unit2->renewXYPos())
{
static int i= rand() % 4 ;
if (!(rand() % 40)) {
i= rand() % 4 ;
ctrlpc(*unit2, map, i,*unit1);
}
else {
ctrlpc(*unit2, map, i,*unit1);
}
}
d.电脑坦克的AI移动采用随机数控制,但是采用简单的随机数控制时,有时坦克会一直撞墙,而不会产生方向上的改变,在随机数的基础上新增一些函数,使得坦克在碰撞时会增大改变方向的几率,与此同时增加了回头开炮函数,增大了电脑坦克回头开炮的几率。
// 原左右变上下方向
if (CPTankDir == DIR_LEFT || CPTankDir == DIR_RIGHT)
{
if (PLTankX > (PLTankX / BOX_SIZE) * BOX_SIZE + BOX_SIZE / 2 - 1)
PLTankX = (PLTankX / BOX_SIZE + 1) * BOX_SIZE;
else
PLTankX = (PLTankX / BOX_SIZE) * BOX_SIZE;
}
// 上下变左右
else
{
if (PLTankY > (PLTankY / BOX_SIZE) * BOX_SIZE + BOX_SIZE / 2 - 1)
PLTankY = (PLTankY / BOX_SIZE + 1) * BOX_SIZE;
else
PLTankY = (PLTankY / BOX_SIZE) * BOX_SIZE;
}
// 更改方向
CPTankDir = new_dir;
(2)碰撞
a.增加碰撞检测函数,用以检测玩家与地图之间,玩家和电脑坦克之间,电脑坦克和电脑坦克之间的碰撞。
bool TankUnit::ifTouch(const Map& map)const
{
Pos_RC curPos[2] = { GetMapPos(),GetMapPos() };//获取当前行列坐标,有两个碰撞判定点
switch (GetDirection())
{
case DIR_UP:
curPos[0].row--;
curPos[1].row--;
curPos[1].col++;
break;
case DIR_LEFT:
curPos[0].col--;
curPos[1].col--;
curPos[1].row++;
break;
case DIR_DOWN:
curPos[0].row += 2;
curPos[1].row += 2;
curPos[1].col++;
break;
case DIR_RIGHT:
curPos[0].col += 2;
curPos[1].col += 2;
curPos[1].row++;
break;
default:
break;
}
b.在测试中发现,坦克与地图之间的碰撞是正确的,但是如果有两个坦克,他们都是运动的,两个坦克的碰撞会发生偏差,其中一个坦克会嵌入另一个坦克中一部分,这是因为坦克碰撞的坐标计算错误,不能运用中心坐标,应该增加运用多个边缘坐标。
// 四个方向坦克中心点
int dev[4][2][2] = { {{-1,-1},{0,-1}}, {{-1,-1},{-1,0}}, {{-1,1},{0,1}},{ {1,-1},{1,0}} };
//障碍物格子检测
int temp1 = bms->box_8[index_i + dev[CPTankDir][0][0]][index_j + dev[CPTankDir][0][1]];
int temp2 = bms->box_8[index_i + dev[CPTankDir][1][0]][index_j + dev[CPTankDir][1][1]];
//下标偏移量
int dev_4[4][4][2] = { {{-2,-2},{1,-2},{-1,-2},{0,-2}}, {{-2,-2},{-2,1},{-2,-1},{-2,0}},{{-2,2},{1,2},{-1,2},{0,2}}, {{2,-2},{2,1},{2,-1},{2,0}} };


(3)子弹
设置玩家和电脑的子弹等级都分为四种情况,不同等级的坦克子弹的打击效果不同,子弹打击墙块,四级玩家坦克打击铁块,其他等级玩家坦克打击铁块,玩家打击电脑,电脑打击玩家,测试结果均正确。
//子弹类
struct BulletUnit
{
int x, y; // 子弹坐标
int dir; // 子弹方向
int speed[4]; // 子弹速度
int mKillId; // 玩家或者电脑坦克的数字标记
static IMAGE mBulletImage[4]; // 图片
static int mBulletSize[4][2];
static int devto_tank[4][2];
static int devto_head[4][2];
};
//子弹移动与碰撞检测
BulletShootKind ComputerUnit::MoveBullet()
{
if (CPBulletUnit.x == SHOOTABLE_X || !CPBulletTimer.TimeOver() )
return BulletShootKind::None;
BulletShootKind result = CheckBomb();
switch (result)
{
case BulletShootKind::Camp:
case BulletShootKind::Other:
case BulletShootKind::Player_1:
case BulletShootKind::Player_2:
return result;
}
int dir = CPBulletUnit.dir;
SignBullet(CPBulletUnit.x, CPBulletUnit.y, dir, _EMPTY );
CPBulletUnit.x = CPBulletUnit.x + CPBulletUnit.speed[CPLevel] * CPXY[dir][0];
CPBulletUnit.y = CPBulletUnit.y + CPBulletUnit.speed[CPLevel] * CPXY[dir][1];
SignBullet(CPBulletUnit.x, CPBulletUnit.y, dir, CP_BUTTLE_SIGN );
return BulletShootKind::None;
}
在窗口的右侧部分显示剩余敌军坦克数量,并且每当新出现一个电脑坦克,该小图标就减少一个。
int x[2] = {233, 241};
int n, index;
for ( int i = 0; i < RemainCPTankNum; i++ )
{
n = i / 2;
index = i % 2;
TransparentBlt( CPPic_HDC, x[index], 19 + n * 8, ENEMY_TANK_ICO_SIZE, ENEMY_TANK_ICO_SIZE,
GetImageHDC(&CPTankIcoPic), 0, 0, ENEMY_TANK_ICO_SIZE, ENEMY_TANK_ICO_SIZE, 0xffffff );
}

在电脑玩家新生成一个的时候,会先显示1s的出生星星然后再显示电脑坦克。
// 电脑坦克出生并伴有星星
Star_State ComputerUnit::Opening(int& renumber,const HDC& center_hdc)
{
Star_State result = CPStar.EnemyShowStar(center_hdc, CPX, CPY, bms);
switch (result)
{
case Star_State::Star_Out:
Grid_4(CPX, CPY, STAR_SIGN); // 出生星星星开始出现
break;
case Star_State::Star_Showing: // 出生星星正在出现
break;
case Star_State::Star_Stop: // 出生星星消失
CPNumber = TOTAL_ENEMY_NUMBER - renumber;
renumber -= 1;
Grid_4(CPX, CPY, COMPUTER_SIGN + 1000 * CPLevel + 100 * CPKind + CPNumber);
break;
}
return result;
}

要区分每种电脑坦克,记录不同的分数值,且要在每个电脑坦克被玩家击中后实时记录分数请况。
// 如果胜利跳转到分数面板
void Settings::IFWin()
{
if (WWin && WWinCount++ > 210 && !GameOverFlag && ScorePanel == false)
{
//Music::PauseBk(true);
ScorePanel = true;
for (list<PlayerUnit*>::iterator itor = PlayerList.begin(); itor != PlayerList.end(); itor++)
{
(*itor)->ResetScorePanelData(PlayerList.size(), GameNowStage);
}
}
}
// 每行分数依次显示
for (int i = 0; i < 4; i++)
{
// 当前显示的行
if (cur_line == i)
{
int temp = kill_num2[i] + 1;
if (temp <= kill_num[i])
{
kill_num2[i]++;
Music::MicPlay(Mic_SCOREPANEL_DI);
break;
}
else
{
line_done_flag[player_id] = true;
bool temp = true;
for (int m = 0; m < player_num; m++)
{
if (line_done_flag[m] == false)
{
temp = false;
break;
}
}
if (temp)
{
cur_line++;
for (int m = 0; m < player_num; m++)
line_done_flag[m] = false;
}
}
}
}

项目整体采用C++编写,编译器为VS2019,需要下载easyx库:源码(全部代码)