终于等到今天了。在《21天C语言代码训练营》中,我就想讲这个项目了,只是用C语言写会比较麻烦,我怕自己水平有限讲不清楚砸了自己的招牌,不得已就放弃了。如今到了C++,我想是时候拿出来讲一讲了。
对坦克大战情有独钟是因为大学时候第一次参加程序设计比赛就做的这个游戏。当时用的语言是Java,那个比赛让我悟出了面向对象的强大之处,我也是从那时开始接触设计模式的。对我而言,坦克大战有着非同寻常的意义,所以一定要带大家用C++实现一下。
知识准备
前面我们用一个星空的项目给大家介绍面向对象编程的三个重要特性。没有看过的同学建议你看了这些文章之后再来学习后面的内容。
- 封装
- 继承
- 多态
代码分享
这个项目中的代码会在GitHub上发布,里面的每一个分支对应着简书中的每一篇文章。
比如,今天是第一篇文章,这一篇里完成的代码会上传在day1这个分支中,以此类推。
坦克大战
我们依然使用EasyX在控制台程序中制作这个游戏程序。这一篇的主要任务是在屏幕上画出一个白色的主战坦克,可以通过方向键控制它的前进方向。效果如下:
下面我们正式开始。
画布类
在这个工程中,我们将EasyX画布相关的功能封装在一个Graphic类中,创建两个文件:Graphic.h和Graphic.cpp。
Graphic.h
#ifndef __GRAPHIC_H__
#define __GRAPHIC_H__
#include <graphics.h>
#define SCREEN_WIDTH 1024
#define SCREEN_HEIGHT 768
class Graphic
{
public:
static void Create();
static void Destroy();
static int GetScreenWidth();
static int GetScreenHeight();
private:
static int m_screen_width;
static int m_screen_height;
};
#endif
Graphic.cpp
#include "Graphic.h"
int Graphic::m_screen_width = SCREEN_WIDTH;
int Graphic::m_screen_height = SCREEN_HEIGHT;
void Graphic::Create()
{
initgraph(m_screen_width, m_screen_height);
}
void Graphic::Destroy()
{
closegraph();
}
int Graphic::GetScreenWidth()
{
return m_screen_width;
}
int Graphic::GetScreenHeight()
{
return m_screen_height;
}
这个类使用的是静态成员变量和静态成员函数,要注意静态成员变量的初始化时在类外进行的。通过这个类可以实现创建和销毁画布的功能,还能够让其他代码随时通过这个类拿到画布的尺寸。
坦克抽象类
由于我们的程序是要通过EasyX画在屏幕上,各种元素都需要统一放在可以遍历的数据结构中方便操作,所以我们在实现坦克代码时会用到多态。这里先创建一个坦克的抽象类。新建文件Tank.h,加入下面代码:
#ifndef __TANK_H__
#define __TANK_H__
#include "Graphic.h"
enum Dir { UP, DOWN, LEFT, RIGHT };
class Tank
{
public:
virtual void Display() = 0;
virtual void Move() = 0;
protected:
int m_x;
int m_y;
COLORREF m_color;
Dir m_dir;
int m_step;
};
#endif
所有坦克都需要引用的东西会定义在这个文件中。这里定义了一个枚举类型,表示方向用的。我们的程序只考虑四个方向,如果需要让坦克可以有八个前进方向后面可以在这里扩充其他方向。
坦克抽象类中,我们定义了两个函数,Display()和Move()大家很熟悉了,在星空项目里用的很多,主要是负责将自己画在屏幕上和移动自己。
属性中m_dir保存坦克的行驶方向,Display和Move都需要使用它。
主战坦克
所谓主战坦克就是玩家控制的坦克,所有的坦克中,只有这个一个是可以控制的。这一点它比较特殊。
创建文件MainTank.h,写入下面代码:
#ifndef __MAIN_TANK__
#define __MAIN_TANK__
#include "Tank.h"
class MainTank : public Tank
{
public:
MainTank()
{
m_x = 400;
m_y = 300;
m_color = WHITE;
m_dir = Dir::UP;
m_step = 2;
}
~MainTank(){}
// 设置行驶方向
void SetDir(Dir dir);
void Display();
void Move();
protected:
// 绘制坦克主体
void DrawTankBody(int style);
};
#endif
这个类继承了Tank类,在初始化时给各个属性赋初值。我们默认主战坦克一开始在屏幕中间,行驶方向向上,颜色为白色。
我们主战坦克的形状如下:
我们来看看怎么实现它。创建文件MainTank.cpp代码如下:
#include "MainTank.h"
void MainTank::SetDir(Dir dir)
{
m_dir = dir;
}
void MainTank::DrawTankBody(int style)
{
fillrectangle(m_x - 4, m_y - 4, m_x + 4, m_y + 4);
if (style == 1)
{
fillrectangle(m_x - 8, m_y - 6, m_x - 6, m_y + 6);
fillrectangle(m_x + 6, m_y - 6, m_x + 8, m_y + 6);
}
else
{
fillrectangle(m_x - 6, m_y - 8, m_x + 6, m_y - 6);
fillrectangle(m_x - 6, m_y + 6, m_x + 6, m_y + 8);
}
}
void MainTank::Display()
{
COLORREF color_save = getfillcolor();
setfillcolor(m_color);
switch (m_dir)
{
case UP:
DrawTankBody(1);
line(m_x, m_y, m_x, m_y - 10);
break;
case DOWN:
DrawTankBody(1);
line(m_x, m_y, m_x, m_y + 10);
break;
case LEFT:
DrawTankBody(0);
line(m_x, m_y, m_x - 10, m_y);
break;
case RIGHT:
DrawTankBody(0);
line(m_x, m_y, m_x + 10, m_y);
break;
default:
break;
}
setfillcolor(color_save);
}
void MainTank::Move()
{
switch (m_dir)
{
case UP:
m_y -= m_step;
if (m_y < 0)
m_y = Graphic::GetScreenHeight() - 1;
break;
case DOWN:
m_y += m_step;
if (m_y > Graphic::GetScreenHeight())
m_y = 1;
break;
case LEFT:
m_x -= m_step;
if (m_x < 0)
m_x = Graphic::GetScreenWidth() - 1;
break;
case RIGHT:
m_x += m_step;
if (m_x > Graphic::GetScreenWidth())
m_x = 1;
break;
default:
break;
}
}
SetDir()
这个很简单,就是修改成员变量的值。通过这个函数能够改变坦克的行驶方向。
DrawTankBody()
这个函数负责画坦克的主题部分,一个正方形的坦克身和两个矩形的履带。由于坦克上下行驶和左右行驶形状不同,因此通过一个参数负责绘制不同的形状。
Display()
这个是核心的绘制方法,提供给外部调用的。这里主要是两部分工作:
- 判断坦克的行驶方向,之后调用DrawTankBody绘制出坦克身
- 根据行驶方向画上炮管
Move()
这个函数每执行一次,坦克向前移动m_step长度。当超出屏幕边沿时,从另一侧重新出现,行驶方向不变。是不是很简单。
键盘事件监听
最后到了我们的主程序了,新建文件main.cpp,加入下面代码:
#pragma warning(disable:4996)
#include <iostream>
#include <conio.h>
#include <time.h>
#include "Graphic.h"
#include "MainTank.h"
using namespace std;
void main()
{
Graphic::Create();
MainTank mainTank;
bool loop = true;
bool skip = false;
while (loop)
{
if (kbhit())
{
int key = getch();
switch (key)
{
// Up
case 72:
mainTank.SetDir(Dir::UP);
break;
// Down
case 80:
mainTank.SetDir(Dir::DOWN);
break;
// Left
case 75:
mainTank.SetDir(Dir::LEFT);
break;
// Right
case 77:
mainTank.SetDir(Dir::RIGHT);
break;
case 224: // 方向键高8位
break;
// Esc
case 27:
loop = false;
break;
// Space
case 32:
break;
// Enter
case 13:
if (skip)
skip = false;
else
skip = true;
break;
default:
break;
}
}
if (!skip)
{
cleardevice();
mainTank.Move();
mainTank.Display();
}
Sleep(200);
}
Graphic::Destroy();
}
这里需要说明的是,我们通过kbhit()捕捉键盘动作,之后再通过getch()得到按下键的码值。有了码值,我们就可以做相应的操作了。这里主要实现了下面几个功能:
- 方向键
按了上下左右方向键之后,坦克相应地进行转向。
- Esc键
按了Esc键之后,程序退出。
- Enter键
按了Enter键之后,程序暂停,再按一下程序继续。
好了,我们现在来运行一下程序。是不是看到了一辆神奇的白色坦克呢?
我是天花板,让我们一起在软件开发中自我迭代。
如有任何问题,欢迎与我联系。