该程序为可视化界面下的飞行射击游戏。程序中,下方的 *
符号代表玩家的飞机,上方的 +
符号代表敌方飞机,o
代表射出的子弹,使用a
键和d
键控制飞机向左向右移动,使用空格键退出游戏。当子弹击中敌方飞机后,得一分,当玩家被击中后,屏幕上会显示You are hit!
。
fsPoint
。该程序在设计上包括逻辑层和显示层两层,目前显示层使用控制台有关函数实现。逻辑层不依赖于现实层,从而保证了一定的扩展性。
逻辑层的定义和实现在fsObjects.h
及fsObjects.cpp
中。下面是对逻辑层的介绍。
存储程序中所需的点的坐标。这是一个模板类
template T>
class fsPoint {
public:
T x, y; // 归一化之后的坐标位置,左上角为0,0 右下角为1,1
fsPoint() {};
fsPoint(T xval, T yval) :x(xval), y(yval) {};
fsPoint<T> & operator = (const fsPoint<T> & rval);
};
// 飞机子弹来源
enum bulletSource {
player = 1, // 子弹来自玩家
enemy = 2, // 子弹来自敌人
playerFriend = 3 // 子弹来自玩家的友军
};
// 子弹射击方向
enum Direction
{
up = 1, // 子弹向上飞行
down = 2, // 子弹向下飞行
right = 3, // 子弹向左飞行
left = 4 // 子弹向右飞行
};
创建fsObject时所给出的点不满足左上点和右下点之间的坐标关系。
class fsInvalidInitializePointValException : public std::logic_error {
public:
fsInvalidInitializePointValException() ;
};
如果在规定的两次开火间隔时间内执行fsAirCraft.fire()
就会抛出此异常。提示需要等待足够的时间后才能执行上述方法。
class fsInvalidFireTimeException :public std::logic_error {
public:
fsInvalidFireTimeException() ;
};
输入的子弹速度无效。
class fsInvalidBulletSpeedInException : public std::logic_error {
public:
fsInvalidBulletSpeedInException();
};
在moveToPoint()
方法中,输入的目标点超出了屏幕显示范围(也即目标点的坐标之一不在0到1之间)。
class fsOutRangeMoveToPointException : public std::logic_error {
public:
fsOutRangeMoveToPointException();
};
所有的物体,包括玩家飞机,敌方飞机和子弹,以及分数显示,都是一个类,其基类为fsObject Class
每一个物体都通过两个点,即两个fsPoint
类型的变量来确定所处位置,两个点分别为左上角的点和右下角的点。程序中将所处的空间进行了归一化,所有的物体的横纵坐标都只能在0到1之间。之后再通过显示层将其绘制到屏幕上。
游戏中所用物体的基类,该类的定义如下:
class fsObject {
public:
fsObject() {};
fsObject(const fsPoint<double> & upperleft, // 指定物体左上角的位置
const fsPoint<double> & lowerright, // 指定物体右下角的位置,这两个点确定后,一个物体的位置和大小也就完全确定了
const fsColor & fgc, // 指定物体的显示颜色,如果使用控制台进行输出,这一参数将会被忽略
char sym = 0 // 指定物体的符号,主要用于在控制台中显示该物体。
);
// 取得物体左上角的位置
fsPoint<double> getUpperLeftCorner const();
// 取得物体右下角的位置
fsPoint<double> getLowerRightCorner const();
// 取得该物体的符号
char getSymbol() const ;
// 为物体设置新的符号
void setSymbol(char c) ;
// 得到或设置物体的可见性,返回0为不可见,返回1为完全可见,如果物体不可见,其也就不能击中飞机
int getVisible() const { return this->visible; }
void setVisible(int newVisible) { this->visible = newVisible; return; }
// 移动位置,向左移动一个单位(步长为FS_DEFAULT_MOVE_STEP) 移动成功则返回0 移动失败返回1
int moveleft() ;
// 移动位置,向右移动一个单位(步长为FS_DEFAULT_MOVE_STEP) 移动成功则返回0 移动失败返回1
int moveright();
//在竖直方向上移动位置,如果输入的delta导致物体的左上角超出屏幕,就将其置为最近的FS_DEFAULT_COORD_UPPER_LIMIT_Y或FS_DEFAULT_COORD_UPPER_LIMIT_Y
int moveVertically(double delta) ;
//在水平方向上移动位置,如果输入的delta导致物体的左上角超出屏幕,就将其置为最近的FS_DEFAULT_COORD_UPPER_LIMIT_X或FS_DEFAULT_COORD_UPPER_LIMIT_X
int moveHorizontally(double delta);
// 直接将物体移到左上角与某一点重合的位置,成功则返回1,失败返回0
int moveToPoint(const fsPoint<double> & dest);
// 打印出物体左上角的位置
void printpos() const ;
};
用于表示游戏中飞行的子弹,其定义及有关方法如下:
// 子弹
class fsBullet : public fsObject (
public:
fsBullet() {};
fsBullet(bulletSource sourceOfBullet, // 子弹的来源
Direction directionOfBullet, // 子弹的飞行方向
int damageOfBullet, // 子弹的威力,也即子弹击中后玩家获得多少分
fsPoint<double> sourceulc, // 发出子弹物体的左上角坐标,通过该坐标可以确定子弹的初始位置
double bulletVec = FS_DEFAULT_BULLET_FLY_SPEED // 子弹的飞行速度
) ;
// 更新子弹的位置,每次循环中都要执行此函数以更新子弹的位置,如果返回0,则表示子弹已经飞行到边缘。
int updataPosition();
};
游戏中所有飞机的基类,玩家的飞机和敌人的飞机均由此派生。
class fsAircraft : public fsObject {
public:
fsAircraft() {};
fsAircraft(fsPoint<double> upperleft, // 飞机左上角的位置
fsPoint<double> lowerright, // 飞机右下角的位置
fsColor fgc, // 飞机的显示颜色,在控制台中此参数无效
std::chrono::milliseconds fireinterval = FS_DEFAULT_FIRE_INTERVAL // 飞机的子弹发射间隔,在经过此段时间后飞机才能发射子弹
) ;
// 发射子弹的相关逻辑,判断能否发射子弹,也即自上一次发射子弹之后,是否经历了足够的时间
// 如果可以发射子弹则返回1,不能则返回0
inline int fireReady();
// 如果能够发射子弹,则执行此函数,以返回fsBullet的对象,即该飞机所发射的子弹
fsBullet fire();
// 设置新的飞机子弹伤害
int setBullteDamage(int newBulletDamage);
// 设置飞机是玩家的飞机还是敌人的飞机
void setSource(bulletSource newSource);
// 设置新的子弹飞行方向
void setFireDirection(Direction newDirection);
// 返回剩余生命值,当返回0或者负数时说明死亡(本程序中没有使用)
int getLifeLeft() const;
// 当被击中后调用此函数,返回剩余生命值 当返回0或者负数时说明死亡 子弹威力为负数说明为生命补给包
int gotHit(int bulletDamage = 1);
}
玩家的飞机,结合fsObject
中的moveleft()
和moveright()
方法玩家可以通过键盘操纵其位置。
class fsMyAircraft : public fsAircraft {
public:
fsMyAircraft(fsPoint<double> upperleft, // 飞机左上角的位置
fsPoint<double> lowerright, // 飞机右下角的位置
fsColor fgc, // 飞机的显示颜色,在控制台中此参数无效
std::chrono::milliseconds fireinterval = FS_DEFAULT_FIRE_INTERVAL // 飞机的子弹发射间隔,发射子弹后,再经过此段时间后才能再次发射子弹
) ;
};
敌人的飞机,飞行方向随机决定。
class fsEnemyAircraft : public fsAircraft {
public:
fsEnemyAircraft(fsPoint<double> upperleft, // 飞机左上角的位置
fsPoint<double> lowerright, // 飞机右下角的位置
fsColor fgc, // 飞机的显示颜色,在控制台中此参数无效
std::chrono::milliseconds fireinterval = FS_DEFAULT_FIRE_INTERVAL // 飞机的子弹发射间隔,在经过此段时间后飞机才能发射子弹
);
// 更新敌机的位置,敌机随机移动,每隔一段时间更换一个方向,移动会在一个矩形框中进行
int updatePosition();
计分版 用于记录玩家的分数
class fsScoreBoard : public fsObject {
public:
fsScoreBoard() {};
fsScoreBoard(const fsPoint<double> & upperleft, // 计分版左上角的位置 推荐将左上角的横坐标设置为大于1的数,避免分数的显示与游戏画面重叠
const fsPoint<double> & lowerright, // 计分版右下角的位置
const fsColor & fgc = FS_DEFAULT_SCOREBOARD_COLOR // 计分版颜色,在控制台情况下无效
) ;
// 获取计分版分数
int getCurScore() const ;
// 增加得分
void addScore(int delta = 1) ;
};
// 判断两个物体是否重叠 如果重叠将返回1 否则返回0,主要用来判定子弹是否击中了飞机
int fsOverlap(const fsObject & obj1, const fsObject & obj2);
// 判断一个点是否在一个矩形中,包括在矩形边上的情况,主要用来实现fsOverlap
template <typename T>
bool fsPointInRect(fsPoint point, fsPoint RectUpperLeft, fsPoint RectLowerRight);
#define FS_DEFAULT_FIRE_INTERVAL std::chrono::milliseconds(500) // 默认的开火间隔
#define FS_DEFAULT_FRAME_INTERVAL 10 // 默认的刷新频率,即每隔多少毫秒刷新一次
#define FS_DEFAULT_MOVE_STEP 0.01 // 在moveleft()方法和moveright()方法中默认的每次移动距离
#define FS_MOVE_LEFT_KEY 'a' // 控制玩家飞机向左移动一个单位所对应的按键
#define FS_MOVE_RIGHT_KEY 'd' // 控制玩家飞机向右移动一个单位所对应的按键
#define FS_ESCAPE_KEY ' ' // 退出游戏所使用的按键
#define FS_DEFAULT_BULLET_FLY_SPEED 3.0 // 默认子弹飞行速度,3代表每秒飞行三个屏幕
#define FS_DEFAULT_AIRCRAFT_FLY_SPEED 0.1 // 默认的飞机飞行速度
#define FS_DEFAULT_BULLET_COLOR fsColor(255,0,0,0) // 默认子弹颜色
#define FS_DEFAULT_BULLET_SIZE 0.1 //默认的子弹正方形边长
#define FS_DEFAULT_BULLET_SIZE_X 0.05 //默认的子弹长方形在x轴方向的长度
#define FS_DEFAULT_BULLET_SIZE_Y 0.1 //默认的子弹长方形在y轴方向的长度
#define FS_DEFAULT_PLAYER_SYMBOL '*' // 默认的玩家的飞机的符号
#define FS_DEFAULT_ENEMY_SYMBOL '+' // 默认的敌人的飞机的符号
#define FS_DEFAULT_BULLET_SYMBOL 'o' // 默认的子弹的符号
#define FS_DEFAULT_SCOREBOARD_COLOR fsColor(0,255,0,0) // 默认的计分版颜色
// 默认的边界位置,这些位置主要在move*方法中限制物体不要超出屏幕边界
#define FS_DEFAULT_COORD_LOWER_LIMIT 0.05
#define FS_DEFAULT_COORD_UPPER_LIMIT 0.95
#define FS_DEFAULT_COORD_LOWER_LIMIT_X 0.05
#define FS_DEFAULT_COORD_UPPER_LIMIT_X 0.95
#define FS_DEFAULT_COORD_LOWER_LIMIT_Y 0.05
#define FS_DEFAULT_COORD_UPPER_LIMIT_Y 0.95
fsDraw.h
)该层主要提供了将物体显示在控制台中的函数,如下:
//设置控制台光标位置(x,y)
void gotoxy(int x, int y) {
COORD pos;
pos.X = x;
pos.Y = y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}
// 设置控制台颜色
void setcolor(WORD color) {
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color);
}
// 画出对象,对象的位置由其左上角位置决定,对象的符号由getSymbol()方法返回的字符决定,可以通过
// setSymbol()方法改变这一符号
int fsDraw(const fsObject & fso) {
gotoxy((fso.getUpperLeftCorner()).x * FS_DEFAULT_CONSOLE_BUFFER_SCALE,
(fso.getUpperLeftCorner()).y * FS_DEFAULT_CONSOLE_BUFFER_SCALE);
setcolor(FOREGROUND_BLUE|FOREGROUND_GREEN|FOREGROUND_RED);
cout << fso.getSymbol();
return 0;
}
// 画出计分版并显示分数
int fsDrawScoreBoard(const fsScoreBoard & fsb) {
gotoxy((fsb.getUpperLeftCorner()).x * FS_DEFAULT_CONSOLE_BUFFER_SCALE,
(fsb.getUpperLeftCorner()).y * FS_DEFAULT_CONSOLE_BUFFER_SCALE);
setcolor(FOREGROUND_GREEN);
cout << "SCORE: " << fsb.getCurScore();
return 0;
}
fsTest.h
) 在包含的程序提供的类文件(fsObjects.h, fsDraw.h
)后,简单的射击小游戏可以如下操作(例子请见fsTest.h
中的gameTestOneBuf()
函数):
(1)初始化有关物体,主要是玩家飞机和敌方飞机、计分版,以及有关的子弹
// 初始化玩家飞机
fsMyAircraft playerAircraft = fsMyAircraft(fsPoint<double>(0.5, 0.99),
fsPoint<double>(0.51, 1.00),
fsColor(255, 255, 255, 0));
// 初始化敌方飞机
fsEnemyAircraft enemy1 = fsEnemyAircraft(fsPoint<double>(0.5, 0.10),
fsPoint<double>(0.51, 0.11),
fsColor(255, 0, 0, 0));
// 初始化计分板
fsScoreBoard scoreBoard = fsScoreBoard(fsPoint<double>(1.1, 0.3), fsPoint<double>(1.11, 0.31));
// 等待1s以便能够通过fire函数成功初始化子弹
Sleep(1000);
fsBullet mybullet;
if (playerAircraft.fireReady()) {
mybullet = playerAircraft.fire();
}
fsBullet enemybullet;
if (enemy1.fireReady()) {
enemybullet = enemy1.fire();
}
// 初始化键盘按键存储变量
char ch = '/0';
(2) 进入游戏循环,在游戏循环中,需要依次完成读取按键,更新各物体状态,绘出物体,子弹击中判断等过程
// 游戏循环
while (1) {
// 获取按键
if (_kbhit())
{
ch = getche();
}
// 依据不同的按键做出对应的反应
switch (ch)
{
case FS_MOVE_LEFT_KEY:
playerAircraft.moveleft();
break;
case FS_MOVE_RIGHT_KEY:
playerAircraft.moveright();
break;
case FS_ESCAPE_KEY:
cout << "You ended this game!";
return 0;
default:
break;
}
ch = 0;
// 更新各对象的状态
if (playerAircraft.fireReady()) {
mybullet = playerAircraft.fire();
}
if (enemy1.fireReady()) {
enemybullet = enemy1.fire();
}
mybullet.updataPosition();
enemybullet.updataPosition();
enemy1.updatePosition();
// 呈现图像
system("cls");
fsDraw(playerAircraft);
fsDraw(mybullet);
fsDraw(enemy1);
fsDraw(enemybullet);
fsDrawScoreBoard(scoreBoard);
// 子弹击中的判定,如果子弹击中,则将原来的子弹设为不可见
if (fsOverlap(mybullet, enemy1)) {
scoreBoard.addScore();
mybullet.setVisible(0);
}
if (fsOverlap(enemybullet, playerAircraft)) {
cout << "you are hit!";
enemybullet.setVisible(0);
Sleep(500);
}
Sleep(FS_DEFAULT_FRAME_INTERVAL);
}
这次大作业大概花了三四天时间,这个过程中,我在可视化界面的选择上比较纠结,本想用QT,但感觉学QT也要花不少时间,而且画面也不是这门课程的重点,所以最后选择了控制台来做可视化界面,虽然效果不太理想,但至少可以运行。
在大作业的过程中,我不仅运用了课上学习的关于模板和类的知识,还自学了有关控制台绘制的控制等技能,让我初步体验了完成一个项目是什么样的经历,很有意义。在写说明文档的过程中,我发现Word在处理代码的时候非常麻烦,又学习了Markdown的使用,也算是另外一个小小的收获吧。
这次的作业虽然已经可以运行,但离真正的飞行射击游戏还有很大的差距,比如,画面不够精美,没有升级制度,没有设置多个敌机,没有Boss,不支持PVP(玩家VS玩家)等,以后我还会再抽时间不断完善。值得一提的是,本来想要使用双缓冲技术避免屏幕闪烁,但这样就难以显示出完整的画面,所以最后我搁置了这个方案,这点可能是我首先需要完善的地方。
请访问我的github:https://github.com/archimekai/flightShooting/
这里包含了VS2015的解决方案,可以直接编译运行。