【项目实战】C/C++语言带你实现:围棋游戏丨详细逻辑+核心源码

2023-11-08

每天一个编程小项目,提升你的编程能力!

游戏介绍

下围棋的程序,实现了界面切换,选择路数,和围棋规则,也实现了点目功能,不过只有当所有死子都被提走才能点目,不然不准确

操作方法

鼠标操作

游戏截图

编译环境

VisualStudio2019,EasyX_20211109

文件描述

用广度寻路寻找周围所有相同棋子,直到四处碰壁了,得到包围住自己的所有点,看看这些点是空地的数量,空地的数量就是气的数量,气为 0 这些子全部提掉,设为空地。每下一步棋记录下这步棋的位置,悔棋时把这些点提掉。打劫时在存悔棋的点的地方找到劫争的地方,只吃了一颗子并且落在劫争的地方就不能下。

点目就是找到一块空地,看看围住它的是不是都是同一个颜色的子,是的话这块空地有多大这个颜色的子的目数就加多大。

如果围住空地的是不同颜色的子,那么这块空地多大,这两个子的气就加这块空地的目数的一半。

其他功能就是设计模式的问题了,我借鉴了 cocos2d 的设计模式,在 director 里运行整个程序,在 scene 里写这个层运行的功能,director 的 runwithscene 写切换场景。

详解:

1、公共工具

MyTool

这个文件里包含了广度寻路和围棋地图的类,其中围棋地图通过广度寻路实现了吃子,提子,点目的功能。还有一些之前做七巧板的项目时保存下来的类,暂且用不到,但是也许能方便未来开发,所以放到一起。

DealArray处理数组的头文件,目前有三个函数,作用分别是:将 vector 首尾颠倒、判断一个元素是否在 vector 里面,判断两个 vector 是否相等(每个元素都相等就是两个 vector 相等),函数实现为

// 这个实现 vector 首尾颠倒
template<typename Vector_Reverse>// 这个是函数模板
void Reserve_Vector(vector<Vector_Reverse>& arr)
{
	for (int i = 0;i < (arr.size() >> 1);i++)
	{
		Vector_Reverse temp = arr[i];
		arr[i] = arr[arr.size() - i-1];
		arr[arr.size() - i - 1] = temp;
	}
}
// 这个实现判断一个元素是否在 vector 里面
template<typename VectorInclude>
bool ifNotInVector(vector<VectorInclude>arr, VectorInclude num)
{
	for (VectorInclude i : arr)
	{
		if (i == num)return false;
	}
	return true;
}
// 这个实现判断两个 vector 是否相等
template<typename VectorEqual>
bool ifTwoVectorEqual(vector<VectorEqual> arr, vector<VectorEqual>ano)
{
	if (arr.size() != ano.size())return false;
	for (int i = 0;i < arr.size();i++)
	{
		if (arr[i] != ano[i])return false;
	}
	return true;
}

MapPoint地图点的类,由 indexX 存放列数,indexY 存放行数,有 PathDir 枚举类型枚举四个方向,能通过 MapPoint getDirPoint(PathDir turn) 这个函数获得四个方向的点,这个函数长这样

MapPoint getDirPoint(PathDir turn)
{

	switch (turn)
	{
	case path_up:
		return MapPoint(this->indexX,this->indexY-1);
		break;
	case path_down:
		return MapPoint(this->indexX, this->indexY + 1);
		break;
	case path_left:
		return MapPoint(this->indexX-1, this->indexY);
		break;
	case path_right:
		return MapPoint(this->indexX+1, this->indexY);
		break;
	default:
	break;
	}
}

同时这个类也用于保存 BoundingBox 类的坐标,因为 easyx 里的每个点都是整型,所以保存的坐标也是整型。

PathNode广度寻路的节点类,也就是树的数据结构,一个父节点,多个子节点由

MapPoint pos;
PathNode* parent;
vector<PathNode*> child;

这三个数据组成,pos 是这个节点所在的位置,parent 是这个节点的父节点,child 是这个节点的子节点们

为方便清理 PathNode 节点,这个类里还提供了静态函数

static void clearNode(PathNode* p_head)
{
	if (p_head->child.empty())
	{
		if (p_head->parent != nullptr)
		{	
			PathNode* temp = p_head->parent;
			for (int i = 0;i < temp->child.size();i++)
			{
				if (temp->child[i] == p_head)
				{
					temp->child.erase(temp->child.begin() + i);
					break;
				}
			}
		}
		delete p_head;
	}
	else
	{
		// vector 的遍历不够好,直接这么清除子节点
		while (!p_head->child.empty())
		{
			clearNode(p_head->child[0]);
		}
	}
}

要清理掉一整个 PathNode* phead 树只需

PathNode::clearNode(phead);

BFS广度寻路的类,包含

function<bool(MapPoint)> ifCanThrough;// 判断是否可以通行的函数指针
unsigned int size_Width, size_Height;// 地图尺寸,width 为宽度,height 为高度
bool* AuxiliaryMaps;// 辅助地图,判断某个点是否走过

四个数据,用

void BFS::setMap(unsigned int size_Width, unsigned int size_Height, function<bool(MapPoint)> CallBack)
{
	this->size_Height = size_Height;
	this->size_Width = size_Width;
	ifCanThrough = CallBack;
}

设置地图尺寸和判断是否可以通行的函数指针,setMap 用法如下

int map[3][3] =
{
	0,1,0,
	0,1,0,
	0,0,0
};
BFS bfs;
bfs.setMap(3, 3, [&](MapPoint num)
{
	if (map[num.indexY][num.indexX] == 0)return true;
	return false;
});// 这是用 lambda 表达式写的
// 也可以用函数指针如
bool ifCanThrough(MapPoint num)
{
	if (map[num.indexY][num.indexX] == 0)return true;
	return false;
}
bfs.setMap(3, 3, ifCanThrough);
// 或者
bool (*p)(MapPoint)=ifCanThrough;
bfs.setMap(3,3,p);

初始化 AuxiliaryMap 用

void initAuxiliaryMaps()
{
	AuxiliaryMaps = new bool[size_Height * size_Width];
	memset(AuxiliaryMaps, false, sizeof(bool)*size_Height*size_Width);
}

清理 AuxiliaryMap 用

void clearAuxiliaryMaps()
{
	if (AuxiliaryMaps != nullptr)delete AuxiliaryMaps;
	AuxiliaryMaps = nullptr;
}

AuxiliaryMap(辅助地图)的作用是每次遍历一个广度寻路的节点就把该节点的位置的 bool 值设为 true 表示这个点寻找过了,避免重复寻找同一个位置,寻路完就把辅助地图清理掉。

由于不知道 ifCanThrough 是否判断点是否在地图内,所以要多写一个判断点是否在地图内的函数,避免访问 AuxiliaryMap 时数组越界,这个函数为

bool ifInMap(MapPoint num)
{
	if (num.indexX >= 0 && num.indexX < size_Width && num.indexY >= 0 && num.indexY<size_Height)return true;
	return false;
}

现在辅助地图有了,广度寻路的节点有了,是否可以通行的判断也有了,可以根据广度寻路的算法用起点和终点的值找到可以通行的路径了,寻找路径的函数为

vector<MapPoint> getThroughPath(MapPoint star, MapPoint end);

函数过长,就不贴出来了,广度寻路的步骤是

1、将起点放进 PathNode* phead

2、将 phead->pos 在 AuxiliaryMap 对应的点的 bool 设为 true,即 AuxiliaryMap[phead->pos.indexY*size_Width+phead->pos.indexX]=true;

3、判断 phead->pos 上下左右四个方向的点是否找寻过,是否可以通行,未找寻过可以通行则把这个点放入 phead 的子节点,phead->addchild(new PathNode(MapPoint(phead->pos.getDirPoint(path_up /* 或者 path_down path_left path_rght */)))); 并且放进 vector<PathNode*>child; 里

4、遍历 child,看看有没有点到达终点,没有进入步骤 5,有进入步骤 8

5、令 vector<PathNode*>parent=child;child.clear(); 遍历 parnet 里的每个 PathNode,对每个 PathNode* 单独执行步骤 3

6、如果 child 为空,进入步骤 7,如果 child 不为空,进入步骤 4

7、返回空的 vector<MapPoint>result;

8、把找到的 PathNode 节点保存下来,不停找 pathNode 的父节点,把每个父节点的 pos 值 push_back 进 vector<MapPoint> result; 里面返回 result.

具体函数实现看 BFS 里的 vector<MapPoint> getThroughPath(MapPoint star, MapPoint end);

实现这个功能其实对围棋这个项目没有帮助,但是都封装出了这个类,不实现一下这个功能总归有点缺憾,围棋要判断所有能走的点,只需要在广度寻路的八个步骤中去掉对是否到达终点的判断就行了,得到包围这块区域的点只需要在寻找所有能走的点时遇到 ifCanThrough 为 false 的点时把该点所在 AuxiliaryMap 的 bool 值设为 true 并存进 vector<MapPoint> result; 里就行,最终返回的就是遇到的所有不能走的点,在 BFS 的函数实现为

vector<MapPoint> getAllCanThrough(MapPoint star);
vector<MapPoint> getEnclosedPoint(MapPoint star);

BFS 中还实现了单步寻路的功能

vector<MapPoint> SingleSearch(PathNode* begin);

这个的用法是

int map[3][3] =
{
	0,1,0,
	0,1,0,
	0,0,0
};
bool ifCanThrough(MapPoint num)
{
	if (map[num.indexY][num.indexX] == 0)return true;
	return false;
}
BFS bfs;
PathNode* begin = new PathNode(MapPoint(0, 0));
bfs.setMap(3, 3, ifCanThrough);
bfs.initAuxiliaryMaps();
vector<MapPoint> reslt=bfs.SingleSearch(begin);
while(!reslt.empty())
{
	// .....这里写每步寻路后的操作
	reslt = bfs.SingleSearch(begin);
}
bfs.clearAuxiliaryMaps();
PathNode::clearNode(begin);

MapNode地图节点,我试图用图的数据结构来写围棋的地图,这样地图上的每个点都是指针,加上 Map 是个单例模式,得到的每个点,点每个点的处理都会反应到真实的地图上,不用重复传参。

这个头文件有 Piece 枚举类型

enum Piece {Black,White,Space};

表示围棋的黑子,白子,空地三种类型

这个类有

// 上下左右四个节点
MapNode* Node_Left;
MapNode* Node_Right;
MapNode* Node_Up;
MapNode* Node_Down;
// 这个点的棋子
Piece Node_Piece;
// 这个点原来的棋子
Piece original_Piece;
// 这个点的坐标
int indexX, indexY;
// 这个点棋子被改变的次数
unsigned int changeTimes;

9 个数据

要清理整个地图调用

// 在图中的任何一个点都可以用于清除整个图
void MapNode::DeleteChild()
{
	// 从父节点到子节点疯狂扩散来清理子节点
	vector<MapNode*> parent;
	vector<MapNode*> child;
	if (this->Node_Down)
	{
	child.push_back(this->Node_Down);
	this->Node_Down->Node_Up = nullptr;
	this->Node_Down = nullptr;
	}
	if (this->Node_Up)
	{
		child.push_back(this->Node_Up);
		this->Node_Up->Node_Down = nullptr;
		this->Node_Up = nullptr;
	}
	if (this->Node_Left)
	{
		child.push_back(this->Node_Left);
		this->Node_Left->Node_Right = nullptr;
		this->Node_Left = nullptr;
	}
	if (this->Node_Right)
	{
		child.push_back(this->Node_Right);
		this->Node_Right->Node_Left = nullptr;
		this->Node_Right = nullptr;
	}
	while (!child.empty())
	{
		parent = child;
		child.clear();
		for (MapNode* parent_Node : parent)
		{
			if (parent_Node->Node_Down)
			{
				if(ifNotInVector(child, parent_Node->Node_Down))
				child.push_back(parent_Node->Node_Down);
				parent_Node->Node_Down->Node_Up = nullptr;
				parent_Node->Node_Down = nullptr;
			}
			if (parent_Node->Node_Up)
			{
				if(ifNotInVector(child, parent_Node->Node_Up))
				child.push_back(parent_Node->Node_Up);
				parent_Node->Node_Up->Node_Down = nullptr;
				parent_Node->Node_Up = nullptr;
			}
			if (parent_Node->Node_Left)
			{
				if(ifNotInVector(child, parent_Node->Node_Left))
				child.push_back(parent_Node->Node_Left);
				parent_Node->Node_Left->Node_Right = nullptr;
				parent_Node->Node_Left = nullptr;
			}	
			if (parent_Node->Node_Right)
			{
				if(ifNotInVector(child, parent_Node->Node_Right))
				child.push_back(parent_Node->Node_Right);
				parent_Node->Node_Right->Node_Left = nullptr;
				parent_Node->Node_Right = nullptr;
			}
			delete parent_Node;
		}
	}
}

这个函数。这个函数不会把自己清理掉,只会把自己周围的所有节点设为 nullptr,所以可以放心在析构函数里用它。

悔棋时把这个点设为某个棋子用

void MapNode::UndoSetPiece(Piece num)
{
	changeTimes--;
	if (changeTimes == 1)original_Piece = Space;
	else if (num == original_Piece)
	{
		switch (num)
		{
		case White:original_Piece = Black;break;
		case Black:original_Piece = White;break;
		default:
		break;
		}
	}
	Node_Piece =num;
}

悔棋时这个点如果棋子改变次数大于 2,设为与原先相同的子时原先的子就要设为的这个子的相反面,这点有一点小逻辑在里面,当然如果改变次数为 2,要设为任何子,原来的子都会是空地。有闲心的可以自己推一下。

不悔了和落子时把这个点设为某个棋子时用

void MapNode::setPiece(Piece num)
{
	if(num==Space&&Node_Piece!=Space)original_Piece = Node_Piece;
	Node_Piece = num;
	changeTimes++;
}

StepPoint每一步的点,用于存每一步落子的地方和每一步悔棋的地方,还有每一步劫争的 MapNode,用于实现悔棋和不悔了的功能,共有

int indexX, indexY;		// 下的位置
bool ifUpBeEated;		// 上边有没有被吃
bool ifDownBeEated;		// 下边有没有被吃
bool ifLeftBeEated;		// 左边有没有被吃
bool ifRightBeEated;	// 右边有没有被吃
Piece Step_Piece;		// 这一步是什么棋子
MapNode* kozai;			// 这一步劫争的地方

八个数据,如果上边有被吃,就把上边的所有空地找到,设为与这一步棋子相反的棋子,下,左,右亦然,四个方向判断完后再把这颗子提掉,这就是悔棋的逻辑,不用存下被吃掉的所有点,用四个 bool 值就省去了很多内存。

Map,地图的所有数据及数据的处理都在 Map 这个类里。

这是个单例模式的类,单例模式就是任何人不能 new 出一个对象,只有这个类自己才能给出自己的模样,具体写法为

class A
{
public:
	~A() {}// 析构函数一定要是公有的
	static A* getInstance()// getInstance 一定要是静态的
	{
		if (p_Ins == nullptr)p_Ins = new A;
		return p_Ins;
	}
private:
	A() {};// 构造函数一定要是私有的
	static A* p_Ins;// 这个不能在构造函数里初始化
};
A* A::p_Ins = nullptr;// 这个不能漏

具体用法你得多多实践才能理解透彻,例如写一个回合制对战游戏,一个英雄一个怪物,一回合轮一个人发动攻击或者防御什么的,调整每个人的攻击力,防御力,暴击率,看看最后是谁赢了这个小项目,你用单例模式试着做一下差不多就能理解了。之后要说的模拟 cocos 就用到了一个单例模式,也是至关重要的单例模式。

Map 共有

MapNode* Entity;// 实体
int sizeX, sizeY;
stack<StepPoint> everyStep;
stack<StepPoint> everyUndoStep;
function<void(int indexX, int indexY, Piece num)> drawPiece;

这六个数据,且这六个数据都是私有的

drawPiece 是个函数指针,由于地图的不同,drawPiece 函数也会不同,所以具体情况具体赋值,这个 drawPiece 相当于一个虚函数。

为 drawPiece 赋值的接口为

void setDrawPiece(function<void(int indexX, int indexY, Piece num)> num)
{
	drawPiece = num;
}

Entity 是地图数据的实体,通过不断地访问

MapNode* Node_Left;
MapNode* Node_Right;
MapNode* Node_Up;
MapNode* Node_Down;

这四个节点来到达地图上的任何一个地方。具体函数为

MapNode* Map::getMapNode(int indexX, int indexY)
{
	if (!ifInMap(indexX, indexY))return nullptr;
	MapNode* result=Entity;
	for (int xx = 0;xx < indexX;xx++)result = result->Node_Right;
	for (int yy = 0;yy < indexY;yy++)result = result->Node_Down;
	return result;
}

sizeX,sizeY 是地图尺寸,用于广度寻路。

everyStep 储存每一步子落在的地方,everyUndoStep 储存每一步悔棋提掉的子所在的地方,都是 stack 结构来存的。

一开始棋盘是空的,所以通过

void Map::setBlankMap(int width, int height)
{
	sizeX = width;
	sizeY = height;
	if (Entity != nullptr)
	{
		Entity->DeleteChild();
		delete Entity;
		Entity = nullptr;
	}
	Entity = new MapNode;
	Entity->indexX = 0;
	Entity->indexY = 0;
	MapNode* currentY = Entity;
	MapNode* currentX = Entity;
	for (int indexY = 0;indexY < height;indexY++)
	{
		currentX = currentY;
		if (indexY != height - 1)
		{
			currentY->Node_Down = new MapNode;
			currentY->Node_Down->Node_Up = currentY;
			currentY = currentY->Node_Down;
			currentY->indexX = 0;
			currentY->indexY = indexY + 1;
		}
		for (int indexX = 0;indexX < width-1;indexX++)
		{
			currentX->Node_Right = new MapNode;
			currentX->Node_Right->Node_Left = currentX;
			if (currentX->Node_Up && currentX->Node_Up->Node_Right)
			{
				currentX->Node_Right->Node_Up = currentX->Node_Up->Node_Right;
				currentX->Node_Up->Node_Right->Node_Down = currentX->Node_Right;
			}
			currentX = currentX->Node_Right;
			currentX->indexX = indexX + 1;
			currentX->indexY = indexY;
		}
	}
	while (!everyStep.empty())everyStep.pop();
	while (!everyUndoStep.empty())everyUndoStep.pop();
}

来初始化 Entity,sizeX,sizeY。

围棋的流程为一个人下一颗子,判断这颗子吃了几颗子,把吃掉的子提掉,判断能不能下在这里(提掉的子大于一或提掉的子为一且不在 everyStep.top().kozai 的地方,没有提掉的子且自身的气不为 0),能下在这里就下在这里,不能下在这里就重新下,下完轮到另一个人。吃掉子,判断在不在劫争的位置,判断自身的气是否为 0 都要判断气,所以首先要实现判断一个区域的气的功能。

在 Map 里判断一个区域气的功能我写为两个函数

vector<MapNode*> Map::getEnclosedPiece(int indexX, int indexY)
{
	vector<MapNode*> result;
	MapNode* num = getMapNode(indexX, indexY);
	BFS calc;
	calc.setMap(sizeX, sizeY, [&](MapPoint val)
	{
		if (getMapNode(val.indexX, val.indexY)->Node_Piece != num->Node_Piece)return false;
		return true;
	});
	vector<MapPoint>enclose_point=calc.getEnclosedPoint(MapPoint(indexX, indexY));
	for (MapPoint i : enclose_point)
	{
		result.push_back(getMapNode(i.indexX, i.indexY));
	}
	return result;
}
int Map::getZoneQi(int indexX, int indexY)
{
	int result = 0;
	vector<MapNode*> enclose_point = getEnclosedPiece(indexX, indexY);
	for (MapNode* i : enclose_point)
	{
		if (i->Node_Piece == Space)result++;
	}
	return result;
}

getZoneQi 就是判断一个区域气的函数。

判断一个区域的气为 0,那就要把这块区域设为空地,这个需要得到这块区域所有的点,然后把这块区域所有点设为空地,实现这个功能需要两个函数

vector<MapNode*> Map::getAllSimplePiece(int indexX, int indexY)
{
	vector<MapNode*> result;
	MapNode* num = getMapNode(indexX, indexY);
	BFS calc;
	calc.setMap(sizeX, sizeY, [&](MapPoint val)
	{
		if (getMapNode(val.indexX, val.indexY)->Node_Piece != num->Node_Piece)return false;
		return true;
	});
	vector<MapPoint>next_point = calc.getAllCanThrough(MapPoint(indexX, indexY));
	for (MapPoint i : next_point)
	{
		result.push_back(getMapNode(i.indexX, i.indexY));
	}
	return result;
}
 
void Map::setZoneSpace(int indexX, int indexY)
{
	vector<MapNode*> next_point = getAllSimplePiece(indexX, indexY);
	for (MapNode* i : next_point)
	{
		i->setPiece(Space);
	}
}

能吃子,能提子,然后才能落子,落子的功能比较复杂,函数也比较长,总的来说就是

bool putOnePiece(int indexX, int indexY,Piece num);

这个函数,如果这个点能落子返回 true,不能落子返回 false。具体实现看 gitee 上的源码

悔棋功能写在

bool Undo();

不悔了的功能写在

bool UnUnDo();

之所以有返回值是因为有可能没落子就有人按悔棋,或者没悔过棋就有人按不悔了,返回的 bool 值是悔棋和不悔了是否成功。

代码没什么好说的,看源码就是了,有点长。

点目功能写在

double getMesh(Piece num);

里,有点长,看源码去。

至此围棋这个游戏的逻辑已经全部实现了,接着就是界面的切换

2、SimulationCocos(模拟 Cocos)

模拟 Cocos 有三个模块,Menu,Scene,Director

Menu 菜单,用于保存每个按钮的类,每个场景里只有一个菜单,菜单里有 MenuItem (菜单项)

MenuItem菜单项,是一个双向链表,每个菜单里只有一个 MenuItem 链表,每个 MenuItem 里包含一个 Button

Button包含三个函数指针

function<void(BoundingBox num)> ResponseFunction;	// 响应
function<bool(BoundingBox num)> Call_Back;			// 回调
function<void(BoundingBox num)> Restore;			// 恢复

和一个 BoundingBox 类。

BoundingBox边框,包含

int size_width, size_height;	// 尺寸
MapPoint Place;					// 左上角位置

三个数据,判断某个点是否在 BoundingBox 里面调用

bool BoundingBox::ifInBoundingBox(MapPoint num)
{
	int heightest, lowest, leftest, rightest;
	heightest = Place.indexY;
	lowest = Place.indexY + size_height;
	leftest = Place.indexX;
	rightest = Place.indexX + size_width;
	if (num.indexX >= leftest && num.indexX <= rightest && num.indexY >= heightest && num.indexY <= lowest)return true;
	return false;
}

当一个场景里发生了点击反应,只需在场景的 Menu 里调用

MenuItem* Menu::IfHappendEvent(int xx, int yy)
{
	MenuItem* current = head;
	bool ifFind = false;
	while (current != nullptr)
	{
		if (current->ifThisIsCalling(xx, yy))
		{
			ifFind = true;
			break;
		}
		current = current->child;
	}
	if(ifFind)
	return current;
	return nullptr;
}

就能判断是否按到了某个按钮以及得到那个按钮的 MenuItem 值,然后调用 MenuItem 的按钮的 ResponseFunc

当点击反应结束时调用响应中的按钮的 Restore 然后判断鼠标所在的位置还在不在按钮里面,在的话调用按钮的 Call_Back 函数,函数里面传的参是按钮的边框,用于绘制按钮。

Scene场景,继承自 GameNode 类,

GameNode是一个双向链表,有

virtual bool initMySelf() { return true; }
virtual bool operation() { return true; }
virtual void EndOperation(){}

三个虚函数,operation 是场景运行时的函数,EndOperation 是令场景结束运行的函数,initMySelf 是初始化场景的函数

同时还有

bool GameNode::ifInRace(GameNode* num)
{
	GameNode* current = this;
	while (current!=nullptr)
	{
		if (current == num)return true;
		current = current->child;
	}
	current = this;
	while (current!=nullptr)
	{
		if (current == num)return true;
		current = current->parent;
	}
	return false;
}

判断某个场景是否和自己有血缘关系。有血缘关系返回 true,无血缘关系返回 false

在 Scene 里有

function<void()> Operat_Func;

这个函数指针,也算是个虚函数,交由子类实现,子类必须实现这个函数指针,不然一定会报错,所以也可以称作不会报错的纯虚函数吧。

还有

bool ifExit;

是否退出场景的判断

在 Scene 里实现了

bool Scene::operation()
{
	ifExit = false;
	while (true)
	{
		Operat_Func();
		if (ifExit)break;
	}
	return true;
}
void EndOperation() { ifExit = true; };

这两个函数,operation 里面真正的精华是 Operat_Func(); 这个函数,这个函数交由 Scene 的子类实现。Scene 的子类可以通过调用 this->EndOperation(); 这个函数退出场景。

Director,单例模式,程序运行的核心,每个 Scene 都在 Director 里运行。只有两个数据

bool ifExit; 			// 是否退出的判断
GameNode* IsRunning;	// 当前运行的场景

Director 里主要通过两个函数来实现 Scene 的运行和场景的切换

void Director::RunWithScene(GameNode* scene)
{
	if (IsRunning != nullptr)
	{
		IsRunning->EndOperation();
	}
	IsRunning = scene;
}
void Director::Operation()
{
	ifExit = false;
	GameNode* temp = IsRunning;
	while (true)
	{
		if (temp == nullptr)break;
		temp->initMySelf();
		if (temp->operation())// 场景一律在这个判断里运行,退出场景时进入判断
		{
			if(!IsRunning->ifInRace(temp))// 此时 IsRunning 已经通过 Director::getInstance()->RunWithScene(...); 改变了自己
			delete temp;
			temp = IsRunning;
		}
		if (ifExit)break;
	}
}

 

IsRunning 变了,temp 不变,原来的场景能运行至结束然后才跳出,释放掉原来场景的内存接着才运行新的场景,这就是 Director 的核心逻辑,Director 需要和 Scene 互相引用,Scene 通过访问 Director 类直接访问当前正在运行的程序,如果 Director 不是单例模式,那么 Scene 就不能通过直接访问类的方式访问到当前的 Director,Director 还得传参给 Scene,这就造成了 Scene 和 Director 互相引用,也就是未定义类型的问题。所以 Director 用单例模式会很方便。

当然,这只是我使用 Cocos2d-x 根据 Cocos 的特性推测着写的,Cocos2d-x 里有自动释放池,写起来估计比我这种山寨版的要好,但是我这个在 Scene 里引用了 graphics.h 头文件,也就是可以在 Scene 里重新定义图形界面的大小,某种意义上会比 Cocos2d 方便。

3、GameScene,LoadScene

这两个类都继承自 Scene,都需要实现 initMySelf 函数,不过如果要实现两个场景之间的切换不能通过互相引用的方法或者分成两个文件,一个头文件,一个 .cpp 文件来实现,头一种会造成发现一个多次重定义的标识符,和未定义标识符的报错,后一种会多出 140 个报错说是什么什么字符已经定义了。总之两个文件不能互相引用,那么就是一个知道另一个,一个不知道另一个,在这种情况下要实现场景的切换就用到了 GameNode 的特性双向链表,比如我是让 LoadScene 文件里引用了 GameScene 的头文件,然后在 LoadScene 的类里包含了 GameScene* scene; 在构造函数的时候

scene = new GameScene;
scene->addChild(this);

把自己设为 scene 的子节点,开始游戏时

Director::getInstance()->RunWithScene(scene);

进入 GameScene

在 GameScene 里要变回 LoadScene 只需

Director::getInstance()->RunWithScene(this->getChild());

就行了。Director 里要是 IsRunning 和 temp 有血缘关系它是不会 delete 掉 temp 的。所以切换场景时这两个场景都不会被清理掉。

以上就是围棋的所有逻辑了,至于代码部分,很长,逻辑都有了就剩搬砖把大楼盖起来,看不下去我的源码也可以根据我的描述写一份自己的了,我相信我描述的够清楚了。

此外,我也给大家分享我收集的其他

编程学习书籍分享:

编程学习视频分享:

整理分享(多年学习的源码、项目实战视频、项目笔记,基础入门教程)最重要的是你可以在群里面交流提问编程问题哦!

欢迎转行和学习编程的伙伴,利用更多的资料学习成长比自己琢磨更快哦!(↓↓↓↓↓↓

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【项目实战】C/C++语言带你实现:围棋游戏丨详细逻辑+核心源码 的相关文章

随机推荐

  • 区块链技术系列(1) - 数字签名

    导读 在现实社会中 签名作为签名者身份的一种证明 签名代表对签名文件的认可 不可抵赖 理论上签名是可信 不可伪造的 现在网络环境越来越广泛 有大量的信息通过网络传播 并且会保存在上面 这些电子数据显然无法人工签名 数字签名就孕育而生 本文将
  • SpringBoot常用注解

    前言 Spring Boot是由Pivotal团队提供的全新框架 其设计目的是用来简化新Spring应用的初始搭建以及开发过程 应用于快速应用开发领域 所需支持 Maven构建提供支持或Gradle 不懂Maven或者Gradle 的建议还
  • 基于全连接孪生网络的目标跟踪(siamese-fc)

    Fully Convolutional Siamese Networks for Object Tracking 这两年可以说deeplearning已经占领了visual object tracking这个领域 但是对于跟踪问题来说 这些
  • 在.NET Framework中的连接字符串ConnectionStrings属性

    在 NET Framework中 ConfigurationManager ConnectionStrings属性是用来访问在Visual Studio IDE应用程序配置文件中配置的数据库连接字符串的 每个连接字符串在Visual Stu
  • 建标库标准怎么导出pdf_保存和导出PDF文档,这款OCR文字识别软件能做到

    ABBYY FineReader 作为一款强大的OCR文字识别软件 如果能运用到大家的办公中 将能帮助大家将各种格式的PDF文档保存为新的PDF文档 PDF A格式文档 以及Microsoft Word Excel PPT等格式 在保存与导
  • spring 的@componentscan, @import, @configuration总结

    缝合怪 本人百度总结了几篇文章 对几篇文章进行汇总缝合总结 configuration 这个注解用来代替spring容器的xml配置文件 具体就是配置文件中的
  • VM VirtualBox 全屏模式 && 自动缩放模式 相互切换

    1 自动缩放模式 热键Host C 偶然一次机会 把虚拟机切换为了自动缩放模式 如下图 想要再切换为全屏模式 发现不知如何操作 后来折腾了一会儿 切换成功 以此备录一下 2 切换为全屏模式 热键Host F 切换为全屏模式的快捷键为Host
  • liveshare开发体验 vs_imgcook体验

    D2今年收费了 我所在创业公司没有报销 当然门票也不是什么大钱 无奈忙成狗错过了早鸟票 指望后面看看分享ppt 无意中看到D2官方流出的一个感兴趣的网址 说是 可以由视觉稿一键生成代码 https imgcook taobao org 创业
  • Kafka、RabbitMQ、RocketMQ 消息中间件的对比

    什么是消息队列 消息队列是在消息的传输过程中保存消息的容器 包含以下 3 元素 Producer 消息生产者 负责产生和发送消息到 Broker Broker 消息处理中心 负责消息存储 确认 重试等 一般其中会包含多个 Queue Con
  • 3DCAT实时云渲染助力VR虚拟现实迈向成熟

    近年来 虚拟现实 Virtual Reality VR 技术在市场上的应用越来越广泛 虚拟现实已成为一个热门的科技话题 相关数据显示 2019年至2021年 我国虚拟现实市场规模不断扩大 从2019年的282 8亿元增长至2021年的583
  • qt Model_View_Delegate 模型_视图_代理

    QT当中model view delegate 模型 视图 代理 此结构实现数据和界面的分离 Qt的模型 视图结构分为三部分 模型 model 视图 view 代理 Delegate 其中模型与数据源通信 并为其它部件提供接口 视图从模型中
  • CSS动画-Animation

    一 动画介绍 动画 animation 是CSS3中具有颠覆性的特征之 可通过设置多个节点来精确控制一个或一组动画常用来实现复杂的动画效果 相比较过渡 动画可以实现更多变化 更多控制的效果 二 动画组成 制作动画分为两个部分 keyfram
  • 立体匹配中的NCC,SAD,SSD算法

    常用的基于区域的局部匹配准则主要有图像序列中对应像素差的绝对值 SAD Sum of Absolute Differences 图像序列中对应像素差的平方和 SSD Sum of Squared Differences 图像的相关性 NCC
  • HSqlDB(java内置数据库)

    1 HSqlDB简介 HSQLDB是一款Java内置的数据库 非常适合在用于快速的测试和演示的Java程序中 无需独立安装数据库 HSQLDB有三种模式 1 Server 就像Mysql那样 2 In Process 又叫做 Standal
  • OpenCV颜色查找表

    Mat color imread flover jpeg Mat lut Mat zeros 256 1 CV 8UC3 for int i 0 i lt 256 i lut at
  • 《每日一题》NO.18:哪些因素会影响标准单元的延迟?

    芯司机 每日一题 会每天更新一道IC面试笔试题 其中有些题目已经被很多企业参考采用了哦 聪明的你快来挑战一下吧 今天是第18题 标准单元是RTL2GDS流程的基础 哪些因素会影响到标准单元的延迟呢 我们在工程项目中应该如何处理这些因素呢 快
  • springboot2

    springboot2 springboot2 核心功能 配置文件 web开发 数据访问 Junit5测试 actutor生产指标监控 springboot核心原理解析 springboot2场景整合 虚拟化技术 安全控制 缓存技术 消息中
  • 什么是SQL注入式攻击,如何去防范SQL注入式攻击

    一 SQL注入式攻击 1 所谓SQL注入式攻击 就是攻击者把SQL命令插入到Web表单的输入域或页面请求的查询字符串 欺骗服务器执行恶意的SQL命令 2 在某些表单中 用户输入的内容直接用来构造 或者影响 动态SQL命令 或作为存储过程的输
  • 测试用例、缺陷报告示例子

    测试用例 用例标题的作用 让人更清晰直观的查看 前置条件和测试步骤 测试步骤是在前置条件的基础上进行的 合格测试用例标题 缺陷 缺陷的介绍 需求 规格 说明书中明确要求的功能 缺失 少功能 需求 规格 说明书中致命不应该出现的错误 功能错误
  • 【项目实战】C/C++语言带你实现:围棋游戏丨详细逻辑+核心源码

    每天一个编程小项目 提升你的编程能力 游戏介绍 下围棋的程序 实现了界面切换 选择路数 和围棋规则 也实现了点目功能 不过只有当所有死子都被提走才能点目 不然不准确 操作方法 鼠标操作 游戏截图 编译环境 VisualStudio2019