C++ 飞机大战 是一个基于命令行操作的飞机大战游戏

游戏设计

与之前的2048文章相同,这篇文章会介绍如何使用 C++ 控制台应用程序实现 飞机大战 这款经典游戏。

首先,作为一个 飞机大战 游戏,我们需要有如下的设计&功能

  • 接受用户W(上)、A(左)、S(下)、D(右)键、X(发射子弹)、ESC键的操作【多线程】
  • 将操作反馈到命令行界面中
  • 有我方和敌方飞机、有子弹
  • 敌方飞机可以自动向下移动、我放子弹自动向上移动【多线程】

但是像之前提到的,跟所有的控制台程序一样,C++ 控制台也是无法只修改特定部分的内容的。换句话说,每一次检测到用户按键并且游戏执行完内部的操作之后,我们都需要清空命令行,并且重新输出所有的数据。

代码架构

首先,这个项目可以被拆解成两个对象:一个是棋盘、一个是飞机。我们需要实现的就是构建出棋盘与飞机的类。

为了实现如上的设计,我们需要创建以下文件:

main.cpp
plane.h
plane.cpp
PW grid.h
PW grid.cpp

其中 main.cpp 起到一个调用游戏关键文件的作用,并且会调用函数检测用户输入,从而执行游戏内的特定操作。

PW Grid.h 为 PW Grid.cpp 的头文件。我们会在这里定义整个游戏的关键的函数,然后这些函数会在 Grid.cpp 里面进行实现。而 plane.h 为 plane.cpp 的头文件,在这个里面我们会定义所有与飞行有关的函数,比如说上下左右移动等等。

下面,我会仔细介绍一下这些类。

PW Grid

PW Grid.h 可以从存储的数据、支持的方法来进行拆解。

首先存储的数据包含了如下几个内容:

  • vector<vector<int>> grid —— grid 是一个 2D vector,它会被用来存储一个 n*n 的棋盘;
  • set<GridLocation> enemyList —— 存储敌方飞机的坐标
  • set<GridLocation> bulletList —— 存储子弹的坐标
  • GridLocation myPlane —— 存储我方飞机的位置
  • int numberOfEnemy —— 存储敌人的数量,达到一定数量之后不再生成新的飞机
  • int numberOfBullet —— 存储子弹的数量

注:GridLocation是一个自定义的结构体;其顾名思义,存储了一个x,y坐标,通过这个x,y坐标就可以帮我们找到一个 grid 中的任意一个点;

struct GridLocation {
	int x;
	int y;
	bool operator<(const GridLocation& g) const
	{
		return g.x < x || (g.x == x && g.y < y);
	}
};

接下来我们看一下 Grid 类的方法,以及如何使用以上定义的数据:

Grid()

顾名思义,这是整个类的构造函数,其作用呢就是初始化 grid ,将所有位置的值设置为0;接着,它会初始化我方飞机的位置,位置为 grid 最后一行居中的位置,并将这个位置的值设置为 2【2代表我方飞机】。

除此之外,enemyList 和 bulletList 等其它的初始变量都会被初始化成默认值。

bool move(GridLocation gl)

接受一个地址 gl,将我方飞机移动到该位置。【这个地址 gl 会来自于我们的 plane 类】。

void clear()

这个函数,主要是对程序接收到用户按下ESC键后所进行的操作,他会初始化整个棋盘,并且初始化所有变量。

string structure() const

因为命令行的特性,我们每次都需要重新输出棋盘,子弹以及我方和敌方的飞机。这个函数就是提供了把棋盘按格式化输出的方法。

注:子弹○ = 3;我方飞机▲ = 2;敌方飞机▽ = 1;默认值 = 0;

buildVector()

这个 private 函数提供了构建 grid 的方法,在 Grid(); 以及 void clear(); 函数中,它们都会调用 buildVector(); 来具体构建好 grid

void createEnemy()

创建敌方飞机,如果敌方飞机的数量大于等于棋盘大小 n/2,则停止创建敌方飞机。

void addBullet(GridLocation)

接受一个坐标,向其位置添加子弹。【坐标来自于 plane.h】

bool threadEnemyDown()

将所有的敌军飞机下移一格。

bool threadBulletUp()

将所有的子弹都上移一格。

getNumberOfEnemy() & getNumberOfBullet()

返回敌人与子弹的个数。

plane

plane.h 同样也可以从存储的数据、支持的方法来进行拆解。

plane.h 存储的数据相比 grid 来说就简单了许多,只包含了一条内容:

  • GridLocation currentPlace —— 存储了当前飞机的位置

接着是它的方法:

void left() & void right() & void up() & void down()

顾名思义,这四个函数为用户的左、右、上、下按键提供了具体地操作方法。这几个方法会针对 currentPlace 变量进行修改

GridLocation returnPlace()

前面 grid 提到的方法 move 需要接受坐标值。这个方法就是返回的是 currenPlace 这个变量,也就是飞机在移动过后的坐标,也即 move 所需要的坐标值。因为 move 方法最后把移动的坐标反映到了 grid 上。

GridLocation shoot()

前面 grid 提到的方法 addBullet 需要接受坐标值。这个方法就是返回的是 子弹的坐标【即为当前飞机坐标的 x 坐标 - 1】。

buildPlane()

初始化我方飞机坐标。

核心技术及问题

多线程

可以看到,我在开头的位置在两个地方标注了【多线程】这个标记。

为什么需要使用多线程是因为我们需要每隔一段时间调用 threadBulletUp 来将子弹上移,调用 threadEnemyDown 来将敌机下移。

这里就需要使用两个线程——一个移动子弹,一个移动飞机。

除此之外,我们还需要另外一个线程来接受我们的 Input,也就是键盘的按键。这样,我方的飞机才能够移动。

具体如何使用代码调用呢:

thread task01(functionName);
task01.detach();

线程同步

可以注意到,我们 threadBulletUpthreadEnemyDown 方法都涉及修改我们定义的 vector<vector<int>> grid 变量。这样会导致一个问题:如果线程一修改了 grid 变量的值,而线程二想同时读取并且修改 grid 变量内容,我们不能保证读取到的数据是经过写线程修改后的。

为了确保读线程读取到的是经过修改的变量,就必须在向变量写入数据时禁止其他线程对其的任何访问,直至赋值过程结束后再解除对其他线程的访问限制。这种保证线程能了解其他线程任务处理结束后的处理结果而采取的保护措施即为线程同步。 —— 《VISUAL C 同步技术》

在 C++ 里,想要线程同步,我们可以引入 #include <mutex> 库。只要在修改数据的地方加入如下的语句我们就可以实现线程同步:

mu.lock();
# Some operation here
mu.unlock();

减少CPU消耗

在编译完成程序之后,我发现了一个问题:即 CPU 调用非常高,一直保持百分之十几。显然,这对一个命令行程序是非常不正常的。

最后,我发现是我在 main() 函数结尾之后使用了一个 while(true) 来保证线程不会因为进程的结束而被强行终止。但是 while(true) 最大的问题就是他不会释放 CPU 核心,而是会一直占用。

所以这里有两种解决方案:

  • 在调用第三个线程的时候,不采用 detach() 方法,而是用 join()。这样会让进程等到线程三结束之后再退出。但是线程三并不会退出。
  • while(true) 里面执行 sleep(1)。这样会让 CPU 在这段睡眠时间将资源分配给其他软件。从而让 CPU 使用量降低。

代码实现

main.cpp

#include <iostream>
#include <conio.h>
#include <string>
#include "PW grid.h"
#include "plane.h"
#include <thread>
#include <Windows.h>
#include <mutex>

using namespace std;

mutex mu;
Grid gd;
Plane myPlane;

void cls(HANDLE hConsole)
{
	CONSOLE_SCREEN_BUFFER_INFO csbi;
	SMALL_RECT scrollRect;
	COORD scrollTarget;
	CHAR_INFO fill;

	// Get the number of character cells in the current buffer.
	if (!GetConsoleScreenBufferInfo(hConsole, &csbi))
	{
		return;
	}

	// Scroll the rectangle of the entire buffer.
	scrollRect.Left = 0;
	scrollRect.Top = 0;
	scrollRect.Right = csbi.dwSize.X;
	scrollRect.Bottom = csbi.dwSize.Y;

	// Scroll it upwards off the top of the buffer with a magnitude of the entire height.
	scrollTarget.X = 0;
	scrollTarget.Y = (SHORT)(0 - csbi.dwSize.Y);

	// Fill with empty spaces with the buffer's default text attribute.
	fill.Char.UnicodeChar = TEXT(' ');
	fill.Attributes = csbi.wAttributes;

	// Do the scroll
	ScrollConsoleScreenBuffer(hConsole, &scrollRect, NULL, scrollTarget, &fill);

	// Move the cursor to the top left corner too.
	csbi.dwCursorPosition.X = 0;
	csbi.dwCursorPosition.Y = 0;

	SetConsoleCursorPosition(hConsole, csbi.dwCursorPosition);
}

void refresh(Grid& grid) {
	HANDLE hStdout;
	hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
	
	cls(hStdout);
	cout << grid.structure();
}

void refreshEnemyLocation()
{
	while (true) {
		mu.lock();
		bool success = gd.threadEnemyDown();
		gd.createEnemy();
		if (!success) {
			gd.clear();
			myPlane.reset();
			cout << "You Fail! Press any key to continue!";
			int ch = _getch();
		}
		mu.unlock();

		refresh(gd);
		Sleep(1500);
	}
}

void getUserInput()
{
	bool success = true;

	while (true) {
		int ch = _getch();

		while (ch != 27 && ch != 87 && ch != 119 && ch != 65 && ch != 97 && ch != 83 && ch != 115 && ch != 68 && ch != 100 && ch != 81 && ch != 113) {
			ch = _getch();
		}

		mu.lock();
		if (ch == 27) {
			gd.clear();
			myPlane.reset();
		}
		else if (ch == 87 || ch == 119) {
			myPlane.up();
			success = gd.move(myPlane.returnPlace());
		}
		else if (ch == 65 || ch == 97) {
			myPlane.left();
			success = gd.move(myPlane.returnPlace());
		}
		else if (ch == 83 || ch == 115) {
			myPlane.down();
			success = gd.move(myPlane.returnPlace());
		}
		else if (ch == 68 || ch == 100) {
			myPlane.right();
			success = gd.move(myPlane.returnPlace());
		}
		else if (ch == 81 || ch == 113) {
			GridLocation bullet = myPlane.shoot();
			if (bullet.x != -1 && bullet.y != -1) {
				gd.addBullet(bullet);
			}
		}
		
		if (!success) {
			gd.clear();
			myPlane.reset();
			success = true;
			cout << "You Fail! Press any key to continue!";
			int ch = _getch();
		}
		mu.unlock();

		refresh(gd);
	}
}

void moveBullet()
{
	while (true) {
		mu.lock();
		bool success = gd.threadBulletUp();
		if (!success) {
			gd.clear();
			myPlane.reset();
			cout << "You blew yourself up! Press any key to continue!";
			int ch = _getch();
		}
		mu.unlock();
		refresh(gd);
		
		Sleep(1000);
	}
}

int main()
{

	cout << "Welcome to Plane Wars, a game based on cpp console. Hope you can have an enjoyable experience here.\n" <<
		"You can press WASD to control, Q to shot and ESC to restart the game. Press any key to Continue!\n";
	int ch = _getch();
	
	thread task01(refreshEnemyLocation);
	task01.detach();
	thread task02(getUserInput);
	task02.detach();
	thread task03(moveBullet);
	task03.join();

	return 0;
}

plane.cpp

#include "plane.h"
#include "PW grid.h"

Plane::Plane() {
	buildPlane();
}

void Plane::buildPlane() {
	currentPlace.x = sizeOfGrid - 1;
	currentPlace.y = sizeOfGrid / 2;
}

void Plane::reset() {
	buildPlane();
}

GridLocation Plane::returnPlace() {
	return currentPlace;
}

void Plane::left() {
	if (currentPlace.y != 0) {
		currentPlace.y -= 1;
	}
}

void Plane::up() {
	if (currentPlace.x != 0) {
		currentPlace.x -= 1;
	}
}

void Plane::down() {
	if (currentPlace.x != sizeOfGrid - 1) {
		currentPlace.x += 1;
	}
}

void Plane::right() {
	if (currentPlace.y != sizeOfGrid - 1) {
		currentPlace.y += 1;
	}
}

GridLocation Plane::shoot() {
	GridLocation bullet = {-1,-1};
	if(currentPlace.x != 0){
		bullet.x = currentPlace.x - 1;
		bullet.y = currentPlace.y;
	}

	return bullet;
}

PW Grid.cpp

#include "PW grid.h"
#include <random>
#include <string>
#include <time.h>
#include <conio.h>
#include <set>

using namespace std;

int sizeOfGrid = 16;

void Grid::createEnemy() {
	if (numberOfEnemy < sizeOfGrid / 2) {
		srand((unsigned)time(NULL));
		int y = rand() % sizeOfGrid;
		if (grid[0][y] == 0) {
			grid[0][y] = 1;
			numberOfEnemy += 1;
			GridLocation tmp = { 0,y };
			enemyList.insert(tmp);
		}
	}
}

void Grid::addBullet(GridLocation gd) {
	bulletList.insert(gd);
	numberOfBullet += 1;
}

bool Grid::threadEnemyDown() {
	set<GridLocation> temp;

	for (GridLocation genemy : enemyList) {
		grid[genemy.x][genemy.y] = 0;
		if (genemy.x != sizeOfGrid - 1) {
			genemy.x += 1;
			if (grid[genemy.x][genemy.y] == 2) {
				return false;
			}else if(grid[genemy.x][genemy.y] == 3){
				grid[genemy.x][genemy.y] = 0;
				bulletList.erase(genemy);
				numberOfEnemy -= 1;
				numberOfBullet -= 1;
			}
			else {
				grid[genemy.x][genemy.y] = 1;
				temp.insert(genemy);
			}
		}
		else {
			numberOfEnemy -= 1;
		}
	}

	enemyList = temp;

	return true;
}

bool Grid::threadBulletUp() {
	set<GridLocation> temp;

	for (GridLocation gbullet : bulletList) {
		grid[gbullet.x][gbullet.y] = 0;
		if (gbullet.x != 0) {
			gbullet.x -= 1;
			if (grid[gbullet.x][gbullet.y] == 1) {
				grid[gbullet.x][gbullet.y] = 0;
				enemyList.erase(gbullet);
				numberOfBullet -= 1;
				numberOfEnemy -= 1;
			}
			else if (grid[gbullet.x][gbullet.y] == 2) {
				return false;
			}
			else {
				grid[gbullet.x][gbullet.y] = 3;
				temp.insert(gbullet);
			}
		}
		else {
			numberOfBullet -= 1;
		}
	}

	bulletList = temp;

	return true;
}

int Grid::getNumberOfEnemy() {
	return numberOfEnemy;
}

int Grid::getNumberOfBullet() {
	return numberOfBullet;
}

void Grid::buildVector() {
	vector<int> temp = {};
	for (int i = 0; i < sizeOfGrid; i++) {
		temp.push_back(0);
	}
	
	grid = {};
	for (int i = 0; i < sizeOfGrid; i++) {
		grid.push_back(temp);
	}

	grid[sizeOfGrid - 1][sizeOfGrid / 2] = 2;
	numberOfEnemy = 0;
	numberOfBullet = 0;
	myPlane.x = sizeOfGrid - 1;
	myPlane.y = sizeOfGrid / 2;
	enemyList = {};
	bulletList = {};
}

Grid::Grid() {
	buildVector();
}

string Grid::structure() const{
	string s = "";

	for (int i = 0; i < sizeOfGrid; ++i) {
		s += "|";
		for (int j = 0; j < sizeOfGrid; ++j) {
			if (grid[i][j] == 0) {
				s += "  ";
			}
			else if(grid[i][j] == 1) {
				s += "▽";
			}
			else if(grid[i][j] == 2) {
				s += "▲";
			}
			else {
				s += "○";
			}
		}
		s += "|";
		s += "\n";
	}

	return s;
}

void Grid::clear() {
	buildVector();
}

bool Grid::move(GridLocation gl) {
	grid[myPlane.x][myPlane.y] = 0;

	if (grid[gl.x][gl.y] != 0) {
		return false;
	}
	else {
		myPlane.x = gl.x;
		myPlane.y = gl.y;
		grid[myPlane.x][myPlane.y] = 2;
	}

	return true;
}

注:头文件并没有提供,如有需要可以自己实现!

下载链接

如果想要下载编译好的文件,链接在此:
https://pan.baidu.com/s/1R5EUl9uoFBWXG4x25si78Q
提取码: 0118