一、设计目的与意义
总所周知,C++由来写游戏是再合适不过了,因为其面向对象的特性使得游戏设计起来更加简单方便快捷,维护起来更加轻松容易,其继承多态的属性也使得代码可扩展性与可复用性大大增强。通过写游戏代码可以加强巩固面向对象知识。我创作此游戏的原因有二,其一,为了加深对面向对象技术的掌握,深入理解其思想。 其二,为了圆自己一个小小的梦想。
二、游戏简介
本游戏为飞机大战游戏,控制台类型游戏,采用C++图形界面库graphics.h来实现界面功能。由于并非采用win32或MFC来实现游戏界面显示以及事件处理功能,所以玩起来有些卡顿,但却另有一番风趣。本游戏为通关类型游戏,玩家可以自己选择出战飞机,以键盘的方向键来控制飞机上下左右移动,飞机会自行发射子弹,无需玩家控制。敌机按照预先设定好的指令进行运动,每架飞机拥有自己的血量,受伤血量减少,当血量减少到0时,飞机被击毁。只有不被敌机击毁并打败最终BOSS就可以赢得游戏(目前只有一关,设定运行轨迹需要精力时间)。用来模拟小时候所玩的飞机大战,回忆童年再合适不过了。
2.1 游戏分析与设计
2.1.1 结构体设计
在计算机里,所有运动的物体都是通过一张张图形图像不断的显示刷新来做到的,而所有图形图像都是由一个一个的像素点组成的。
本游戏中,所有的物体(包括飞机,子弹,背景图等)都本质都是一个矩形。在屏幕上,要想表示一个矩形得知道它的坐标以及长宽。对于一个矩形,坐标我用结构体Point来表示,长宽用结构体Size来表示。
typedef struct point
{
int x; //x轴坐标
int y; //y轴坐标
}Point;
typedef struct size
{
int width; //宽
int height; //高
}Size;
控制敌机移动轨迹的指令结构体
typedef struct order //指令
{
direction dir; //方向枚举
int num; //指令执行步数
}Order;
2.1.2 枚举设计
为了更好的表示一架飞机类型,子弹类型,方向类型。特此设计了一批枚举类型
enum bulletKind { SPOT, ADD, STAR, CHARO, LINE, DOUBLESTAR }; //子弹类型
enum myPlaneKind { myPlane1, myPlane2, myPlane3}; //我机类型
enum enemyPlaneKind { enemyPlane1, enemyPlane2, enemyPlane3, boosPlane }; //敌机类型
enum direction { UP = 72, LEFT = 75, RIGHT = 77, DOWM = 80, STOP = 0 }; //方向
2.1.3 飞机子弹类设计
类是面向对象最基本的东西,如何设计类,是本游戏的重中之重。
2.1.3 PointSize类
单单一矩形是无法表示一个物体的,还得附上一些属性及方法。为此设计一个最基本的基类是必须的,PontSize类因此而生,它是本游戏飞机子弹类的基类。
class PointSize
{
public:
PointSize() = default;
PointSize(const Point& point, const Size& si) //构造函数
:size(si), pos(point), exists(true) {}
Size getSize() const { return size; } //得到大小
Point getPoint() const { return pos; } //得到位置
bool isExists() const { return exists; } //判断是否存在
void Destroyed() { exists = false; } //销毁
virtual void move(int key, int speed); //移动
protected:
bool exists; //存在与否
Point pos; //位置
Size size; //大小
};
它有一个虚函数move,用来改变位置,参数key是方向枚举,speed是速度(即一个单位时间的运动位移)。
基本的属性与方法设计好后,便可以设计之后的类。
2.1.3 Bullet类(子弹基类)
Bullet类继承于PointSize类。子弹类拥有攻击力、速度、自身类型属性。
class Bullet: public PointSize //子弹
{
public:
Bullet(const Point& point, const Size& size, int da, int spe, bulletKind k) :
PointSize(point, size), damage(da), speed(spe), kind(k) {} //构造函数
int getDamage() const { return damage; } //得到攻击力
int getSpeed() const { return speed; } //得到速度
bulletKind getKind() const { return kind; } //得到子弹类型
void move(int dir) { PointSize::move(dir, speed); } //重写基类方法
protected:
int damage; //子弹攻击力
int speed; //子弹速度
bulletKind kind; //子弹类型
};
成员kind是判断某子弹对象所属于的子弹类型,便于刻画其特征。
2.1.3 Airplane类(飞机基类)
每架飞机都拥有子弹夹、血量、移动速度、射击时间等属性。
class Airplane:public PointSize
{
public:
Airplane() = default;
Airplane(int bl, int sp, bulletKind bk, int time, const Point& point, const Size& size, LPCTSTR file)
: blood(bl), speed(sp),bulletkind(bk),
shootIntervalTime(time),times(0), PointSize(point,size),hited(false),boomNum(10)
{
if (file != NULL)
loadimage(&img,file, size.width, size.height);
//图形库API,用来加载图片
}
int getBlood() const { return blood; } //得到剩余血量
IMAGE& getImage() { return img; } //得到图像的引用
int getBulletsNum() const { return bulletsClip.size(); } //得到子弹数量
Bullet* getBullet(int i) const {
if (i >= bulletsClip.size()) return NULL;
return bulletsClip[i];
} //得到第i颗子弹的引用
int getIntervalTime() const { return shootIntervalTime; } //得到射击间隔时间
int getTimes() const { return times; } //得到距离射击时间
int getBoomTimes() const { return boomTimes; } //得到爆炸秒数
void hurted(int num) { blood -= num; } //受伤
void move(int dir) { PointSize::move(dir, speed); } //移动
virtual void bulletsMove(int dir); //子弹移动
virtual void shooting(); //射击
void reduceBoomTimes() { boomTimes--; } //减少爆炸秒数
void addTimes() { times++; } //增加距离射击时间
bool isHited() { return hited; } //是否被击中
void setHited(bool is) { hited = is; } //射击被击中状态
void timesToZero() { times = 0; } //射击时间归0
void clear(); //清楚所有子弹,释放内存
virtual ~Airplane() { clear(); }
protected:
std::vector<Bullet*> bulletsClip; //子弹夹
bulletKind bulletkind; //子弹夹里的子弹类型
IMAGE img; //一个由图像库提供的结构体,用来显示飞机图片
int blood; //剩余血量
int speed; //移动速度
int shootIntervalTime; //射击间隔时间
int times; //离本次射击时间
bool hited; //被击中
int boomTimes; //爆炸秒数
};
成员bulletsClip 采用容器vector来保存每颗子弹的指针,每颗子弹都是动态分配的,clear函数和析构函数用来释放这些资源。
射击子弹即是动态申请一个子弹对象,把其指针添加到子弹夹中。
2.1.3 MyAirplane类(我方飞机类)
主要是重写基类方法,还能添加属性(暂未添加)。
class MyAirplane :public Airplane
{
public:
MyAirplane() = default;
MyAirplane(int bl, int sp, bulletKind bk,int time, const Point& point,const Size& size, LPCTSTR file) : Airplane(bl, sp, bk,time, point, size, file) {}
void move(int key); //重写基类方法
void bulletsMove(int dir = UP); //重写基类方法,默认向上移动
void shooting(); //重写基类方法
};
2.1.3 EnemyAirplane类(敌方飞机类)
增加了指令相关的成员与方法,达到敌机能够按照预先设计好的轨迹行动,还能添加属性(暂未添加)。
class EnemyAirplane : public Airplane
{
public:
EnemyAirplane() = default;
EnemyAirplane(int bl, int sp, bulletKind bk, int time, const Point& point,const Size& size,
LPCTSTR file) :Airplane(bl, sp, bk,time, point, size, file),index(0), iExecuteTimes(0) {}
void shooting(); //重写基类方法
int getIndex() const { return index; } //得到当前指令在指令集的下标
int getIExeTimes() const { return iExecuteTimes; }//得到当前指令以执行步数
int getOrderNum() const { return orders.size(); } //得到指令集数
Order getOrder(int i) const { //得到第i条指令
if (i < orders.size()) {
return orders[i];
}
}
void addOrder(direction dir, int num) { //添加指令
orders.push_back({ dir, num });
}
void addIndex() { index++; } //将下标移动下一条指令
void addIExeTimes() { iExecuteTimes++; } //增加当前指令执行步数
void ExeTimesToZero() { iExecuteTimes = 0; } //将指令执行步数清零
private:
std::vector<Order> orders; //指令集
int index; //执行第i条指令
int iExecuteTimes; //第i条指令以执行数
};
2.1.3 具体飞机,子弹类
每种飞机、子弹对应都有具体的类
class PointBullet : public Bullet //点型子弹
{
public:
PointBullet(Point pos = { 0,0 }) :
Bullet(pos, { 2,2 }, 5, 8, SPOT) {}
};
class AddBullet :public Bullet //+型子弹
{
public:
AddBullet(Point pos = { 0,0 }) :
Bullet(pos, { 4,4 }, 10, 6, ADD) {}
};
class StarBullet :public Bullet //星型子弹
{
public:
StarBullet(Point pos = { 0,0 }) :
Bullet(pos, { 6,6 }, 15, 6, STAR) {}
};
class DoubleStarBullet :public Bullet //双星型子弹
{
public:
DoubleStarBullet(Point pos = { 0,0 }) :
Bullet(pos, { 18,6 }, 30, 8, STAR) {}
};
class MyAirplane1 : public MyAirplane //我方飞机1号
{
public:
MyAirplane1(Point pos = { 0,0 })
: MyAirplane(200, 12, ADD, 12, pos, { 50,50 },
TEXT("image\\myPlane1.PNG")) {}
};
class MyAirplane2 : public MyAirplane //我方飞机2号
{
public:
MyAirplane2(Point pos = { 0,0 })
: MyAirplane(250, 6, STAR, 12, pos, { 50,50 },
TEXT("image\\myPlane2.PNG")) {}
};
class MyAirplane3 : public MyAirplane //我方飞机3号
{
public:
MyAirplane3(Point pos = { 0,0 })
: MyAirplane(300, 6, DOUBLESTAR, 12, pos, { 50,50 },
TEXT("image\\myPlane3.PNG")) {}
};
class EnemyAirplane1 : public EnemyAirplane //敌方飞机1号
{
public:
EnemyAirplane1(Point pos = { 0,0 })
: EnemyAirplane(30, 2, SPOT,160, pos, { 50,50 },
TEXT("image\\enemyPlane1.PNG")) {}
};
class EnemyAirplane2 : public EnemyAirplane //敌方飞机2号
{
public:
EnemyAirplane2(Point pos = { 0,0 })
: EnemyAirplane(50, 2, ADD, 60, pos, { 50,50 },
TEXT("image\\enemyPlane2.PNG")) {}
};
class EnemyAirplane3 : public EnemyAirplane //敌方飞机3号
{
public:
EnemyAirplane3(Point pos = { 0,0 })
: EnemyAirplane(80, 2, STAR, 60, pos, { 50,50 },
TEXT("image\\enemyPlane3.PNG")) {}
};
class BoosAirplane : public EnemyAirplane //BOSS
{
public:
BoosAirplane(Point pos = { 0,0 })
: EnemyAirplane(800, 2, DOUBLESTAR, 120, pos, { 150,150 },
TEXT("image\\boosPlane.PNG")) {}
};
2.1.4 地图玩家敌人类设计
有了飞机和子弹后,剩下的就是如何管理这些飞机子弹以及设计游戏地图与规则,于是引入地图玩家敌人类。
2.1.4 Map类(地图类)
为了使游戏更加生动,加入地图背景是必要的,但一张静态的背景图是没啥用的。于是引入地图类,让背景图自己动起来。背景图向下运动,使得飞机看上去有种向前飞的感觉。
class Map
{
public:
Map(int x = 0, int y = 0) : posX(x), posY(y) {};
void init(LPCTSTR mapFile, int w, int h);
void setPos(int x, int y) { //设置坐标
posX = x;
posY = y;
}
int getPosY() const { return posY; } //得到y轴坐标
void move() { //向下移动
posY += speed;
}
void drawMap() { //显示图片
putimage(posX, posY, &img);
}
private:
IMAGE img; //存储图片
int posX; //图片y轴坐标
int posY; //图片y轴坐标
public:
const static int speed; //运动速度
const static int mapWidth; //图片宽度
const static int mapHeight; //图片高度
};
2.1.4 Player类(玩家类)
用来操作飞机
class Player
{
public:
Player(int kind = myPlane1, Point pos = { 0,0 }) {
pMe = getMyAirplane(kind, pos);
pme = pMe;
}
void controlPlane(); //控制我方飞机
void draw(); //显示飞机和子弹
void shoot(); //发射子弹
MyAirplane& getPlane() { return *pMe; }
void clear() { }//释放资源
~Player() { clear(); }
private:
MyAirplane* pMe; //指向我方飞机的指针
};
2.1.4 Enemy类(敌人类)
用来统一管理敌方的资源
class Enemy
{
public:
Enemy(int num = 0) : planeNum(num) { planes.clear(); }
void init(LPCTSTR planeFile); //初始化敌机
void move(); //敌机的移动
void draw(); //敌机的显示
void shooted(); //敌机被我方机射击情况
void shootMe(); //敌机射击我方机情况
void clear(); //释放资源
bool isShow(); //敌机是否出现
int getEnemyNum() const { return planeNum; } //得到敌机存活数量
~Enemy() { clear(); }
private:
std::vector<EnemyAirplane*> planes; //敌机数组
int planeNum; //敌机存活数量
};
成员planes 保存所有的敌机。
2.1.5 普通函数
除了类方法外。还设计出普通函数来实现一些操作。其中一个很重要的函数void beginPlay();用来控制地图的移动刷新,关卡的刷新。
动态申请飞机、子弹对象并返回指针的函数
MyAirplane* getMyAirplane(int kind, Point pos = { 0,0 }); //获得我方飞机
EnemyAirplane* getEnemyAirplane(int kind, Point pos = { 0,0 });//获得敌方飞机
Bullet* getBullet(int kind, Point pos = { 0,0 }); //获得子弹
参数kind表示飞机或子弹类型,pos表示生成时的坐标。
飞机、子弹互相碰撞的检测。
bool impactText(const PointSize& lhs, const PointSize& rhs); //碰撞检测
各种绘画显示函数
void drawBullet(Bullet& bullet); //刻画子弹
void drawAirplane(Airplane& plane); //刻画飞机
void drawRect(const Point& pos,const Size& size); //刻画矩形
void drawBoom(const Point& pos, const Size& size); //刻画爆炸
还有游戏开始界面选择函数
int gameBeginGUI(); //游戏开始界面
bool choose(int key, POINT& pos, int up, int bottom, int dis); //选择选项
int choosePlane(int op = 1); //选择出战飞机
void showPlane(IMAGE* pimg, int n); //显示所有我方飞机
2.2 图片
开始界面
选择飞机界面
打飞机中
打BOOS中
2.3 设计特色
2.3.1 多线程 :
控制台不像MFC和win32那样本身具备多线程和消息事件功能。对本游戏来说,无论是敌机、我机、子弹还是地图的运动都是通过不断刷新来实现的。如果只有一个主线程,图片刷新帧数为30毫秒一次,玩家按下键盘到飞机真实运动开始之间至少需要30毫秒再加上需要处理很多东西,这便会导致延迟,所以玩家控制的飞机运动将会十分卡顿,体验感极差。但加入多线程的话,情况会有大有改善。地图,敌机,子弹的刷新归一个线程管理,而玩家操作飞机归另一个线程管理。飞机运动起来便会顺畅很多(依旧会卡顿)。
2.3.2 指令控制敌机运动:
飞机运动问题一直困扰着我很久,在计算机组成原理这么课程上,我获得灵感。既然计算机是由指令和数据组成这个,那么运动轨迹不也可以由指令控制吗,运动指令分为两部分一是方向二是运动时间,如上面的Order结构体所示。把指令提前写好在文件中,等玩家到达某一关卡时,将指令从文件中读取出来放到敌机对象的指令集成员中,敌机按照指令进行运动。
2.3.3 易扩展性
敌机、我机、子弹类已经设计好,代码中的飞机、子弹对象都是这些类的实例的指针或引用,要想增加一种物品,只需要在添加一个子类即可。这是多态性的好处。
三、总结与体会
一开始我是想用MFC来重写实现去年的大作业的(也是游戏,但是是控制台文字游戏),但发现自己根本不会这门技术,虽然暑假有学过一些,但终究只是皮毛。在天大的困难面前我还是放弃了。MFC写一下管理系统我到还行,游戏什么我用MFC根本不可能, 但我又不想写什么管理系统的。于是我将目光转向win32,毕竟很多游戏可以用它来实现,可我又不会,又没时间学。犹豫徘徊很久之后决定使用C++一个轻量级图形界面库Graphics.h来实现这个游戏。
刚开始动手时,脑袋乱七八糟的,有种无从下手的感觉。要从哪里开始做起、做什么、怎么做统统不知道。冷静一阵子后,先从构造类开始。飞机、子弹、图片等等所有游戏中出现的物体该如何用类来表示,他们拥有什么属性、什么方法、各类之间关系如何我也不知从何下手。多态性、封装性、复用性等等又该如何体现,也不知道。从一开始的全局乱构乱造,到局部改善,到慢慢发现并使用更好的方法,再到全局整体代码提升的道路上,渐渐找到道路方向,有了整体的布局,一切便清晰了。
代码不算得很长,比去年的算是简洁多了。毕竟去年的是纯控制台、功能太多。吸取去年的教训,这次不做那么复杂(虽然脑袋中有很多想法),毕竟功能越多付出时间精力也就越多,但乐趣却是去年无法比拟的,无论是在敲代码中、测试中都会让我有种成就感,因为这种有图像界面的我是第一次系统做,看着那些运动的东西,会觉得一些新奇(知道的多了,未知与神秘也会随之增多)。其实还有很多更加有趣的功能可以添加,因为时间关系,这个游戏也许就到此为止了吧。
♻️ 资源
大小: 22.8MB
➡️ 资源下载:https://download.csdn.net/download/s1t16/87553498
注:如当前文章或代码侵犯了您的权益,请私信作者删除!