Win32 游戏开发:TicTacToe(井字游戏) 下篇

Win32游戏开发 同时被 2 个专栏收录
4 篇文章 0 订阅
4 篇文章 0 订阅

上篇进行讲解了游戏的规则、界面设计、游戏流程、······

下面我们继续码代码吧,(#^.^#)

 

在写代码前先说之前所漏掉的两个枚举,分别是:

① ClickPlayer (枚举点击玩家)

/* 点击玩家枚举 */
typedef enum _ClickPlayer {
	ClickPlayer_Player1 = 0x00000001,	// 玩家1
	ClickPlayer_Player2 = 0x00000002	// 玩家2
}ClickPlayer;

② GameOverType (枚举游戏结束类型)

/* 游戏结局 */
typedef enum _GameOverType {
	GameOverType_Tie     = 0x00000001,	// 平局
	GameOverType_Player1 = 0x00000002,	// 玩家1
	GameOverType_Player2 = 0x00000004	// 玩家2
}GameOverType;

 

下面开始继续代码的讲解:

2)游戏初始化(TicTacToe_Init)

①TicTacToe_Init

VOID TicTacToe_Init(HWND hWnd)
{
	/* 游戏初始化 */
	g_game.Init(hWnd);
	/* 创建兼容DC和兼容位图 */
	Util::CreateDoubleBuffer(hWnd, g_mdc, g_bitmap);
	::SelectObject(g_mdc, g_bitmap);
}

g_game.Init(hWnd),进行初始化游戏中的游戏对象

Util::CreateDoubleBuffer进行创建双缓冲(Util在后面进行讲解,此处先为了解)

::SelectObject将兼容位图放入兼容DC中

 

②Game::Init(游戏初始化)

void Game::Init(HWND hWnd)
{
	/* 游戏初始化 */
	m_bIsGameOver = false;
	m_hWnd = hWnd;
	/* 初始化棋盘 */
	m_board.Init(hWnd);
}

m_bIsGameOver = false,游戏设定为未结束

m_hWnd = hWnd,保存窗口句柄为后面使用

m_board.Init(hWnd),初始化棋盘

 

③Board::Init(棋盘初始化)

void Board::Init(HWND hWnd)
{
	/* 设定玩家1为开始玩家*/
	m_cur_click = ClickPlayer::ClickPlayer_Player1;
	::GetClientRect(hWnd, &m_rect);

	/* 初始化九个格子 m_ppPreces不为NULL则为上局棋子 */
	if (m_ppPreces != NULL) {
		delete m_ppPreces;
		m_ppPreces = NULL;
	}
	m_ppPreces = new Prece*[9];

	/* 两边余量 */
	int x_margin = 20;
	int y_margin = 20;

        /* 设定棋盘矩形大小 */
	m_rect.left += x_margin;
	m_rect.top += y_margin;
	m_rect.right -= x_margin;
	m_rect.bottom -= y_margin;

	/* 棋盘宽高 */
	int width = m_rect.right - m_rect.left;
	int height = m_rect.bottom - m_rect.top;

        /* 棋盘左上角(x, y) 以及棋子的宽和高 */
	int x_start = m_rect.left;
	int y_start = m_rect.top;
	int w_distance = width / 3;
	int h_distance = height / 3;

	for (int c = 0, col = 3; c < col; ++c)
	{
		for (int r = 0, row = 3; r < row; ++r)
		{
                        /* 创建棋盘格子,并保存到棋盘中 */
			m_ppPreces[c * 3 + r] = new Prece(x_start + (r * w_distance), y_start + (c * h_distance), w_distance, h_distance, c * 3 + r);
		}
	}
}

拆分成小部分讲解:

/* 设定玩家1为开始玩家 */
m_cur_click = ClickPlayer::ClickPlayer_Player1;

设定玩家1为先手(ClickPlayer为点击玩家枚举,后面进行讲解)

/* 初始化九个格子 m_ppPreces不为NULL则为上局棋子 */
if (m_ppPreces != NULL) {
	delete m_ppPreces;
	m_ppPreces = NULL;
}
m_ppPreces = new Prece*[9];

如果已经创建了9个棋子,则释放此9个棋子。(如果不为NULL则是上一局的棋子)

然后分配9个格子空间进行保存格子

/* 两边余量 */
int x_margin = 20;
int y_margin = 20;

/* 设定棋盘矩形大小 */
m_rect.left += x_margin;
m_rect.top += y_margin;
m_rect.right -= x_margin;
m_rect.bottom -= y_margin;

设置棋盘的矩形大小,也就是给窗口四边留有相应的余量

/* 棋盘宽高 */
int width = m_rect.right - m_rect.left;
int height = m_rect.bottom - m_rect.top;

/* 棋盘左上角(x, y) 以及棋子的宽和高 */
int x_start = m_rect.left;
int y_start = m_rect.top;
int w_distance = width / 3;
int h_distance = height / 3;

width和height,分别为棋盘的宽度和高度

x_left_top和y_left_top,分别为棋盘的左上角(x, y)

w_distance和h_distance,分别为棋子的宽度和高度

然后进行设置棋子相应的位置

for (int c = 0, col = 3; c < col; ++c)
{
	for (int r = 0, row = 3; r < row; ++r)
	{
		/* 创建棋盘格子,并保存到棋盘中 */
		m_ppPreces[c * 3 + r] = new Prece(x_start + (r * w_distance), y_start + (c * h_distance), w_distance, h_distance, c * 3 + r);
	}
}

两个for循环为创建3x3的棋子,col(列) row(行)

m_ppPreces[c * 3 + r]为将棋子放入数组对应的下标中,也就是1对应棋子1、2对应棋子2···

new Prece(x_start + (r * w_distance), y_start + (c * h_distance), w_distance, h_distance, c * 3 + r)
Prece::Prece(int x, int y, int w, int h, int index)
{
	/* 格子初始化 */
	m_x = x;
	m_y = y;
	m_w = w;
	m_h = h;
	m_index = index;
	m_bIsClick = false;
}

new 棋子,创建棋子,并且初始化位置和大小以及棋子的下标(也就是1,2,3,···标识棋子位置)

和点击状态为未点击

PS:所以在Board::Init中为初始化棋盘大小和初始化棋盘上的9个棋子的位置等信息

 

2) 游戏鼠标点击事件处理(TicTacToe_MouseDown)

① case WM_LBUTTONDOWN

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
        ······
	case WM_LBUTTONDOWN:
		/* 井字游戏鼠标点击消息处理 */
		TicTacToe_MouseDown(LOWORD(lParam), HIWORD(lParam));
		break;
	······
	}

	return ((LRESULT)0);
}

lParam为附加消息(不同消息的附加消息都不同)

低位保存x坐标(使用LOWORD获取低位数据)

高位保存y坐标(使用HIWORD获取低位数据)

最后将(x, y)传入TicTacToe_MouseDown

 

② TicTacToe_MouseDown(点击事件处理)

VOID TicTacToe_MouseDown(int x, int y)
{
	/* 游戏鼠标处理 */
	g_game.MouseDown(x, y);
}

传递鼠标消息给游戏对象

 

③Game::MouseDown(游戏点击事件处理)

void Game::MouseDown(int x, int y)
{
	if (m_bIsGameOver)
	{
		/* 如果游戏结束,点击重新开始 */
		Init(m_hWnd);
		return;
	}
	/* 游戏鼠标点击消息处理 */
	m_board.MouseDown(x, y);
    /* 检测是否游戏结束 */
	CheckGameOver();
}

拆成小部分进行讲解:

if (m_bIsGameOver)
{
	/* 如果游戏结束,点击重新开始 */
	Init(m_hWnd);
	return;
}

如果游戏已经结束,则点击为重新开始游戏

此处的Init(m_hWnd)为上面所讲的初始化游戏

/* 游戏鼠标点击消息处理 */
m_board.MouseDown(x, y);

讲鼠标消息传递到棋盘中

/* 检测是否游戏结束 */
CheckGameOver();

检测是否游戏结束(判断条件为是否三个点连成一线,或9个格子都填满了)

(CheckGameOver方法后面进行讲解,此处先了解)

 

④ Board::MouseDown(棋盘点击事件处理)

void Board::MouseDown(int x, int y)
{
	/* 检测是否点击到格子 */
	for (int i = 0, count = 9; i < 9; ++i)
	{
		if (m_ppPreces[i]->CheckClick(x, y))
		{
			/* 设定棋子被当前落棋玩家点击 */
			m_ppPreces[i]->Click(m_cur_click);
			/* 点击到格子,则切换玩家下棋 */
			m_cur_click = (m_cur_click == ClickPlayer::ClickPlayer_Player1 ? 
				ClickPlayer::ClickPlayer_Player2 : 
				ClickPlayer::ClickPlayer_Player1);
		}
	}
}

for循环为遍历9个棋子

if (m_ppPreces[i]->CheckClick(x, y))

通过鼠标点击的位置(x, y)进行判断是否点击到棋子(Prece::CheckClick方法下面进行讲解)

/* 设定棋子被当前落棋玩家点击 */
m_ppPreces[i]->Click(m_cur_click);

设定棋子被当前落棋玩家点击

/* 点击到格子,则切换玩家下棋 */
m_cur_click = (m_cur_click == ClickPlayer::ClickPlayer_Player1 ? 
	ClickPlayer::ClickPlayer_Player2 : 
	ClickPlayer::ClickPlayer_Player1);

切换落棋玩家

 

⑤Prece::CheckClick(判断棋子是否被点击)

bool Prece::CheckClick(int x, int y)
{
	/* 判断鼠标点击的位置是否在格子内 */
	return (!m_bIsClick) && (x <= m_x + m_w && y <= m_y + m_h && x >= m_x && y >= m_y);
}

通过判断棋子是否已被点击,并且鼠标点击的(x, y)是否在棋子的矩形中

 

⑥Prece::Click(设定棋子被玩家点击)

void Prece::Click(ClickPlayer sender)
{
	/* 格子被点击 */
	m_bIsClick = true;
	/* 设定点击玩家 */
	m_click_player = sender;
}

设定棋子被点击,设定点击玩家

 

3) 游戏渲染 (TicTacToe_Render)

① TicTacToe_Render(游戏渲染)

VOID TicTacToe_Render(HWND hWnd)
{
	if (g_mdc == NULL)
		return;

	HDC hdc = ::GetDC(hWnd);
	RECT clientRect;
	::GetClientRect(hWnd, &clientRect);
	int width = clientRect.right - clientRect.left;
	int height = clientRect.bottom - clientRect.top;

	/* 游戏绘制 */
	g_game.Render(g_mdc);

	/* 将兼容DC绘制到设备DC中 */
	::BitBlt(hdc, 0, 0, width, height, g_mdc, 0, 0, SRCCOPY);
	::ReleaseDC(hWnd, hdc);
}

GetDC()获取设备DC,GetClientRect()获取客户端区域矩形

获取客户端的宽和高,主要为了将兼容DC绘制到设备DC中(使用了BitBlt函数)

用了GetDC(),不要忘了使用ReleaseDC()进行释放设备DC

/* 游戏绘制 */
g_game.Render(g_mdc);

绘制游戏中的游戏对象,将其绘制到兼容DC中

 

② Game::Render(绘制游戏中的游戏对象)

void Game::Render(HDC hdc)
{
	/* 绘制游戏背景 */
	DrawBackground(hdc);
	/* 绘制棋盘 */
	m_board.Render(hdc);
	/* 绘制游戏结束 */
	DrawGameOver(hdc);
}

在游戏绘制中分别有:绘制游戏背景、绘制画板、绘制游戏结束

 

③ Game::DrawBackground(绘制游戏背景)


void Game::DrawBackground(HDC hdc)
{
	/* 创建背景颜色画刷 */
	HBRUSH brush = ::CreateSolidBrush(RGB(22, 22, 22));
	RECT rect;
	::GetClientRect(m_hWnd, &rect);
	/* 填充背景颜色 */
	::FillRect(hdc, &rect, brush);
	::DeleteObject(brush); brush = NULL;
}

1. 首先创建背景颜色画刷

2. 获取窗口客户端矩形

3. 填充背景画刷颜色到窗口客户端矩形

4. 最后释放画刷对象

 

④  Game::DrawGameOver(绘制游戏结束)

void Game::DrawGameOver(HDC hdc)
{
	/* 绘制游戏结束信息 */
	if (m_bIsGameOver)
	{
		LPCWSTR lpszTitle = _T("游戏结束");
		LPCWSTR lpszBody = NULL;
		LPCWSTR lpszTips = _T("点击屏幕重新开始游戏");

		/* 设置显示消息 */
		if (m_over_type == GameOverType::GameOverType_Tie)
			lpszBody = _T("  平局");
		else if (m_over_type == GameOverType::GameOverType_Player1)
			lpszBody = _T("玩家1获胜");
		else
			lpszBody = _T("玩家2获胜");

		// 设置绘制的文字字体
		HFONT hFont, hOldFont;
		Util::CreateLogFont(hFont, 45);
		hOldFont = (HFONT)SelectObject(hdc, hFont);

		/* 文字背景为透明 */
		::SetBkMode(hdc, TRANSPARENT);

		/* 绘制标题 */
		::SetTextColor(hdc, RGB(197, 31, 31));
		::TextOut(hdc, 150, 100, lpszTitle, lstrlen(lpszTitle));

		/* 绘制信息 */
		::SetTextColor(hdc, RGB(87, 105, 60));
		::TextOut(hdc, 150, 225, lpszBody, lstrlen(lpszBody));

		/* 绘制提示消息 */
		::SetTextColor(hdc, RGB(91, 74, 66));
		::TextOut(hdc, 0, 350, lpszTips, lstrlen(lpszTips));

		::SelectObject(hdc, hOldFont);
		::DeleteObject(hFont); hFont = NULL;
	}
}

拆分进行讲解:

if (m_bIsGameOver)

当游戏结束才进行绘制

LPCWSTR lpszTitle = _T("游戏结束");
LPCWSTR lpszBody = NULL;
LPCWSTR lpszTips = _T("点击屏幕重新开始游戏");

游戏结束页面显示:标题(lpszTitle)、获胜内容(lpszBody)、重新开始提示(lpszTips)

/* 设置显示消息 */
if (m_over_type == GameOverType::GameOverType_Tie)
	lpszBody = _T("  平局");
else if (m_over_type == GameOverType::GameOverType_Player1)
	lpszBody = _T("玩家1获胜");
else
	lpszBody = _T("玩家2获胜");

根据结局,进行显示不同的获胜内容

// 设置绘制的文字字体
HFONT hFont, hOldFont;
Util::CreateLogFont(hFont, 45);
hOldFont = (HFONT)SelectObject(hdc, hFont);

设置文字的字体

/* 文字背景为透明 */
::SetBkMode(hdc, TRANSPARENT);

设置文字背景颜色为透明

/* 绘制标题 */
::SetTextColor(hdc, RGB(197, 31, 31));
::TextOut(hdc, 150, 100, lpszTitle, lstrlen(lpszTitle));

/* 绘制信息 */
::SetTextColor(hdc, RGB(87, 105, 60));
::TextOut(hdc, 150, 225, lpszBody, lstrlen(lpszBody));

/* 绘制提示消息 */
::SetTextColor(hdc, RGB(91, 74, 66));
::TextOut(hdc, 0, 350, lpszTips, lstrlen(lpszTips));

将标题、获胜内容、重新开始提示显示到屏幕

SetTextColor(设置显示文字的颜色)

TextOut(输出文字到指定位置)

::SelectObject(hdc, hOldFont);
::DeleteObject(hFont); hFont = NULL;

恢复到之前的状态,销毁字体对象

PS:说了这么多其实就是显示(标题、获胜内容、重新开始)

 

⑤ Board::Render (绘制棋盘)

void Board::Render(HDC hdc)
{
	/* 绘制"井" */
	DrawBoard(hdc);

	/* 绘制棋子 */
	for (int i = 0, count = 9; i < 9; ++i)
	{
		m_ppPreces[i]->Render(hdc);
	}
}

在棋盘绘制中分别是绘制棋盘和绘制9个棋子

这里的棋盘是指棋盘上面的"井"

 

⑥ Board::DrawBoard (绘制棋盘)

void Board::DrawBoard(HDC hdc)
{
	/* 创建画笔 */
	HPEN hPen = ::CreatePen(PS_SOLID, 3, RGB(0, 0, 0));
	HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);

	/* 棋盘宽高 */
	int width = m_rect.right - m_rect.left;
	int height = m_rect.bottom - m_rect.top;
	int x_left_top = m_rect.left;
	int y_left_top = m_rect.top;
	int w_distance = width / 3;
	int h_distance = height / 3;

	/* "井"四标边 */
	int points[4][4];

	/* 竖线第一条 */
	points[0][0] = x_left_top + w_distance;
	points[0][1] = y_left_top;
	points[0][2] = x_left_top + w_distance;
	points[0][3] = y_left_top + height;

	/* 竖线第二条 */
	points[1][0] = x_left_top + 2 * w_distance;
	points[1][1] = y_left_top;
	points[1][2] = x_left_top + 2 * w_distance;
	points[1][3] = y_left_top + height;

	/* 横线第一条 */
	points[2][0] = x_left_top;
	points[2][1] = y_left_top + h_distance;
	points[2][2] = x_left_top + width;
	points[2][3] = y_left_top + h_distance;

	/* 横线第二条 */
	points[3][0] = x_left_top;
	points[3][1] = y_left_top + 2 * h_distance;
	points[3][2] = x_left_top + width;
	points[3][3] = y_left_top + 2 * h_distance;

	Util::DrawLine(hdc, points[0]);
	Util::DrawLine(hdc, points[1]);
	Util::DrawLine(hdc, points[2]);
	Util::DrawLine(hdc, points[3]);

	::SelectObject(hdc, hOldPen);
	::DeleteObject(hPen); hPen = NULL;
}

拆分进行讲解:

/* 创建画笔 */
HPEN hPen = ::CreatePen(PS_SOLID, 3, RGB(0, 0, 0));
HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);

创建颜色画笔,使用此颜色画笔进行绘制线条

/* 棋盘宽高 */
int width = m_rect.right - m_rect.left;
int height = m_rect.bottom - m_rect.top;
int x_left_top = m_rect.left;
int y_left_top = m_rect.top;
int w_distance = width / 3;
int h_distance = height / 3;

width和height,分别为棋盘的宽度和高度

x_left_top和y_left_top,分别为棋盘的左上角(x, y)

w_distance和h_distance,分别为棋子的宽度和高度

/* "井"四标边 */
int points[4][4];

定义一个4x4的数组,用于保存四条直线的起点和终点坐标(x, y)

/* 竖线第一条 */
points[0][0] = x_left_top + w_distance;
points[0][1] = y_left_top;
points[0][2] = x_left_top + w_distance;
points[0][3] = y_left_top + height;

/* 竖线第二条 */
points[1][0] = x_left_top + 2 * w_distance;
points[1][1] = y_left_top;
points[1][2] = x_left_top + 2 * w_distance;
points[1][3] = y_left_top + height;

/* 横线第一条 */
points[2][0] = x_left_top;
points[2][1] = y_left_top + h_distance;
points[2][2] = x_left_top + width;
points[2][3] = y_left_top + h_distance;

/* 横线第二条 */
points[3][0] = x_left_top;
points[3][1] = y_left_top + 2 * h_distance;
points[3][2] = x_left_top + width;
points[3][3] = y_left_top + 2 * h_distance;

"井"字四条边的起点和终点坐标(x, y)

Util::DrawLine(hdc, points[0]);
Util::DrawLine(hdc, points[1]);
Util::DrawLine(hdc, points[2]);
Util::DrawLine(hdc, points[3]);

调用Util的绘制线段(Util在后面进行讲解,此处先为了解)

::SelectObject(hdc, hOldPen);
::DeleteObject(hPen); hPen = NULL;

恢复为先前状态,销毁画笔对象

 

⑦ Prece::Render (绘制棋子)

void Prece::Render(HDC hdc)
{
	/* 绘制标记 */
	DrawGraphics(hdc);
}

在绘制棋子中为绘制标记

 

⑧ Prece::DrawGraphics(绘制棋子标记)

void Prece::DrawGraphics(HDC hdc)
{
	/* 判断棋子是否被玩家点击 */
	if (!m_bIsClick)
		return;

	if (m_click_player == ClickPlayer::ClickPlayer_Player1)
	{
		/* 绘制玩家1图形 */
		DrawPlayer1Graphics(hdc);
	}
	else
	{
		/* 绘制玩家2图形 */
		DrawPlayer2Graphics(hdc);
	}
}

拆分进行讲解:

/* 判断棋子是否被玩家点击 */
	if (!m_bIsClick)
		return;

当棋子没有被玩家点击,不进行绘制标记(因为只有点击了才会有玩家的标记)

if (m_click_player == ClickPlayer::ClickPlayer_Player1)
{
	/* 绘制玩家1图形 */
	DrawPlayer1Graphics(hdc);
}

如果棋子为玩家1点击,则绘制玩家1的标记

else
{
	/* 绘制玩家2图形 */
	DrawPlayer2Graphics(hdc);
}

否则为玩家2点击,则绘制玩家2的标记

 

⑨ Prece::DrawPlayer1Graphics (绘制玩家1标记)

void Prece::DrawPlayer1Graphics(HDC hdc)
{
	// 棋子中心点坐标
	int x_center = m_x + (m_w / 2);
	int y_center = m_y + (m_h / 2);

	/* 绘制 "×" */
	double len = m_w / 3.0;
	float angles[] = {
		45, 135, 225, 315
	};

	int points[2][4];

	float rad = 3.1415926f / 180.0f;
	/* 第一条 */
	int x_lt = (int)(x_center + len * cos(angles[0] * rad));
	int y_lt = (int)(y_center + len * sin(angles[0] * rad));
	int x_rd = (int)(x_center + len * cos(angles[2] * rad));
	int y_rd = (int)(y_center + len * sin(angles[2] * rad));

	/* 第二条 */
	int x_rt = (int)(x_center + len * cos(angles[1] * rad));
	int y_rt = (int)(y_center + len * sin(angles[1] * rad));
	int x_ld = (int)(x_center + len * cos(angles[3] * rad));
	int y_ld = (int)(y_center + len * sin(angles[3] * rad));

	points[0][0] = x_lt;
	points[0][1] = y_lt;
	points[0][2] = x_rd;
	points[0][3] = y_rd;

	points[1][0] = x_rt;
	points[1][1] = y_rt;
	points[1][2] = x_ld;
	points[1][3] = y_ld;

	HPEN hPen = ::CreatePen(PS_SOLID, 3, RGB(153, 77, 82));
	HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);
	/* 绘制 */
	Util::DrawLine(hdc, points[0]);
	Util::DrawLine(hdc, points[1]);

	::SelectObject(hdc, hOldPen);
	::DeleteObject(hPen); hPen = NULL;
}

拆分进行讲解:

// 棋子中心点坐标
int x_center = m_x + (m_w / 2);
int y_center = m_y + (m_h / 2);

计算出棋子的中心点坐标(x, y)

double len = m_w / 3.0;

表示"×"两条线段的长度的一半

也就是比如这个"\"为两条线段其中一条,则此len变量表示这条线段的一半

float angles[] = {
	45, 135, 225, 315
};

四个点的角度

int points[2][4];

两条线段的起点和终点的坐标(x, y)

float rad = 3.1415926f / 180.0f;

计算1°相对的弧度

/* 第一条 */
int x_lt = (int)(x_center + len * cos(angles[0] * rad));
int y_lt = (int)(y_center + len * sin(angles[0] * rad));
int x_rd = (int)(x_center + len * cos(angles[2] * rad));
int y_rd = (int)(y_center + len * sin(angles[2] * rad));

/* 第二条 */
int x_rt = (int)(x_center + len * cos(angles[1] * rad));
int y_rt = (int)(y_center + len * sin(angles[1] * rad));
int x_ld = (int)(x_center + len * cos(angles[3] * rad));
int y_ld = (int)(y_center + len * sin(angles[3] * rad));

points[0][0] = x_lt;
points[0][1] = y_lt;
points[0][2] = x_rd;
points[0][3] = y_rd;

points[1][0] = x_rt;
points[1][1] = y_rt;
points[1][2] = x_ld;
points[1][3] = y_ld;

通过三角函数计算出四个点的坐标(x, y)

其中lt(left-top)、rt(right-top)、ld(left-down)、rd(right-down)

HPEN hPen = ::CreatePen(PS_SOLID, 3, RGB(153, 77, 82));
HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);

创建颜色画笔,并且使用此画笔

/* 绘制 */
Util::DrawLine(hdc, points[0]);
Util::DrawLine(hdc, points[1]);

进行绘制两条线段线段

::SelectObject(hdc, hOldPen);
::DeleteObject(hPen); hPen = NULL;

恢复到先前的状态,并且销毁画笔对象

 

⑩ Prece::DrawPlayer2Graphics (绘制玩家2标记)

void Prece::DrawPlayer2Graphics(HDC hdc)
{
	/* 棋子中心点坐标 */
	int x_center = m_x + (m_w / 2);
	int y_center = m_y + (m_h / 2);

	/* "○"半径 */
	int r = m_w / 3;
	/* 绘制 "○" */
	HPEN hPen = ::CreatePen(PS_SOLID, 3, RGB(64, 116, 52));
	HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);
	HBRUSH hBrush = (HBRUSH)::GetStockObject(NULL_BRUSH);
	HBRUSH bOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);

	::Ellipse(hdc, x_center - r, y_center - r, x_center + r, y_center + r);

	::SelectObject(hdc, bOldBrush);
	::SelectObject(hdc, hOldPen);
	::DeleteObject(hBrush); hBrush = NULL;
	::DeleteObject(hPen); hPen = NULL;
}

拆分进行讲解:

/* 棋子中心点坐标 */
int x_center = m_x + (m_w / 2);
int y_center = m_y + (m_h / 2);

计算出棋子的中心坐标(x, y)

/* "○"半径 */
int r = m_w / 3;

定义圆的半径

HPEN hPen = ::CreatePen(PS_SOLID, 3, RGB(64, 116, 52));
HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);
HBRUSH hBrush = (HBRUSH)::GetStockObject(NULL_BRUSH);
HBRUSH bOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);

创建颜色画笔,并选用此画笔

创建空心画刷,并选用此画刷(选用空心画刷就是让圆的中心为透明,不填充颜色)

::Ellipse(hdc, x_center - r, y_center - r, x_center + r, y_center + r);

绘制圆

::SelectObject(hdc, bOldBrush);
::SelectObject(hdc, hOldPen);
::DeleteObject(hBrush); hBrush = NULL;
::DeleteObject(hPen); hPen = NULL;

恢复先前的状态,并销毁画笔和画刷对象

 

4) Game::CheckGameOver (检测游戏是否结束)

void Game::CheckGameOver()
{
	/* 获取棋盘格子 */
	Prece** ppPreces = m_board.GetPreces();

	/* 获取玩家点击了的格子 */
	int p1[9];
	int p2[9];
	memset((void*)p1, -1, sizeof(p1));
	memset((void*)p2, -1, sizeof(p2));

	int index1 = 0;
	int index2 = 0;
	for (int i = 0, count = 9; i < count; ++i)
	{
		if (ppPreces[i] != NULL && ppPreces[i]->IsClick())
		{
			ppPreces[i]->GetClickPlayer() == ClickPlayer::ClickPlayer_Player1 ?
				p1[index1++] = ppPreces[i]->GetIndex() :
				p2[index2++] = ppPreces[i]->GetIndex();
		}
	}

	/* 不足3个取消比较 */
	if (index1 < 3 && index2 < 3)
		return;
	
	/* 8种获胜结果集合 */
	int win_set[8][3] = {
		{0, 1, 2},
		{3, 4, 5},
		{6, 7, 8},

		{0, 3, 6},
		{1, 4, 7},
		{2, 5, 8},

		{0, 4, 8},
		{2, 4, 6}
	};

	/* 进行比较 */
	int nP1Match = 0;
	int nP2Match = 0;
	for (int i = 0; i < 8; ++i)
	{
		nP1Match = 0;
		nP2Match = 0;
		for (int j = 0; j < 3; ++j)
		{
			for (int k = 0; k < index1; ++k)
			{
				if (p1[k] == win_set[i][j])
					++nP1Match;
				else if (p2[k] == win_set[i][j])
					++nP2Match;

				if (nP1Match == 3)
				{
					m_over_type = GameOverType::GameOverType_Player1;
					m_bIsGameOver = true;
					return;
				}
				else if (nP2Match == 3)
				{
					m_over_type = GameOverType::GameOverType_Player2;
					m_bIsGameOver = true;
					return;
				}
			}
		}
	}

	/* 9个为平局 */
	if (index1 + index2 >= 9)
	{
		m_over_type = GameOverType::GameOverType_Tie;
		m_bIsGameOver = true;
	}

}

拆分进行讲解:

/* 获取棋盘格子 */
Prece** ppPreces = m_board.GetPreces();

获取棋盘上的9个棋子

/* 获取玩家点击了的格子 */
int p1[9];
int p2[9];
memset((void*)p1, -1, sizeof(p1));
memset((void*)p2, -1, sizeof(p2));

定义p1、p2两个数组进行保存 玩家1 玩家2 点击的棋子下标,并设置数组中的元素为-1

int index1 = 0;
int index2 = 0;
for (int i = 0, count = 9; i < count; ++i)
{
	if (ppPreces[i] != NULL && ppPreces[i]->IsClick())
	{
		ppPreces[i]->GetClickPlayer() == ClickPlayer::ClickPlayer_Player1 ?
			p1[index1++] = ppPreces[i]->GetIndex() :
			p2[index2++] = ppPreces[i]->GetIndex();
	}
}

遍历9个棋子,将点击的棋子保存到对应的数组中

/* 不足3个取消比较 */
if (index1 < 3 && index2 < 3)
	return;

如果棋子点击数量不足三个,则不进行比较(因为最低也要三个棋子连成一线)

/* 8种获胜结果集合 */
int win_set[8][3] = {
	{0, 1, 2},
	{3, 4, 5},
	{6, 7, 8},

	{0, 3, 6},
	{1, 4, 7},
	{2, 5, 8},

	{0, 4, 8},
	{2, 4, 6}
};

列举八种胜利方式

int nP1Match = 0;
int nP2Match = 0;

定义玩家1玩家2匹配的数量

for (int i = 0; i < 8; ++i)
{
    nP1Match = 0;
    nP2Match = 0;
    for (int j = 0; j < 3; ++j)
    {
        for (int k = 0; k < index1; ++k)
        {
	       ......
        }
    }
}

每次进行一种胜利方式匹配前,都重新设置玩家1玩家2的匹配数为0

变量八种胜利方式,然后遍历每种里面的三个棋子下标,

遍历玩家1和玩家2所下的所有棋子,如果存在此种组合的三个棋子下标则获胜

if (p1[k] == win_set[i][j])
    ++nP1Match;

如果玩家1存在此种组合的其中一个下标,则对匹配变量(nP1Match)进行++

else if (p2[k] == win_set[i][j])
    ++nP2Match;

如果玩家2存在此种组合的其中一个下标,则对匹配变量(nP2Match)进行++

if (nP1Match == 3)
{
    m_over_type = GameOverType::GameOverType_Player1;
    m_bIsGameOver = true;
    return;
}
else if (nP2Match == 3)
{
    m_over_type = GameOverType::GameOverType_Player2;
    m_bIsGameOver = true;
    return;
}

如果匹配数量为3,则为八种获胜方式中的其中一种方式中的下标全部匹配,那么此玩家获胜

设置游戏结束类型(m_over_type = ···)(在绘制游戏结束中使用)

并且设置游戏结束(m_bIsGameOver = true)

/* 9个为平局 */
if (index1 + index2 >= 9)
{
	m_over_type = GameOverType::GameOverType_Tie;
	m_bIsGameOver = true;
}

如果上面两个玩家都没有匹配成功,并且九个棋子都被玩家点击,那么为平局

设置游戏结束类型(m_over_type = ···)(在绘制游戏结束中使用)

并且设置游戏结束(m_bIsGameOver = true)

 

5) Util (常用工具)类

是否发现在上面经常使用到了Util这个类呢。

正是因为经常使用,所以避免太多重复代码,故添加此类

PS:以后游戏中会继续使用此Util,并且不断进行添加常用的方法

在Util中定义了三个方法,分别是:

static void DrawLine(HDC, int[4]);                          // 绘制一条直线
static void CreateDoubleBuffer(HWND, HDC &, HBITMAP &);     // 创建创缓冲
static void CreateLogFont(HFONT &, int);                    // 创建逻辑字体

 

① Util::DrawLine (绘制线段)

void Util::DrawLine(HDC hdc, int points[4])
{
	/* *
	 * int[4] 表示两个点的 (x, y) 
	 * 第一个点为 (points[0], points[1])
	 * 第二个点为 (points[2], points[3])
	 * */

	::MoveToEx(hdc, points[0], points[1], NULL);
	::LineTo(hdc, points[2], points[3]);
}

通过MoveToEx移动到线段起点,在通过LineTo绘制一条从起点到终点的线段

 

②  Util::CreateDoubleBuffer (创建双缓冲)

void Util::CreateDoubleBuffer(HWND hWnd, HDC &mdc, HBITMAP &bitmap)
{
	/* * 
	 * 创建双缓冲
	 * 也就是: 兼容DC和兼容位图
	 * */
	HDC hdc = ::GetDC(hWnd);
	RECT clientRect;
	::GetClientRect(hWnd, &clientRect);
	mdc = ::CreateCompatibleDC(hdc);
	bitmap = ::CreateCompatibleBitmap(hdc, clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);

	::ReleaseDC(hWnd, hdc);
}

1. 通过GetDC获取设备DC

2. 获取客户端矩形大小

3. 通过CreateCompatibleDC创建兼容DC

4. 通过CreateCompatibleBitmap创建一张跟客户端矩形大小的兼容位图

5. 最后不要忘了ReleaseDC,释放获取的设备DC

 

③ Util::CreateLogFont (创建逻辑字体)

void Util::CreateLogFont(HFONT &hFont, int nFontHeight)
{
	/* * 
	 * 创建逻辑字体
	 * */
	LOGFONT logfont;
	ZeroMemory(&logfont, sizeof(LOGFONT));
	logfont.lfCharSet = GB2312_CHARSET;
	logfont.lfHeight = nFontHeight;
	hFont = ::CreateFontIndirect(&logfont);
}

1. 定义一个逻辑字体

2. 设置逻辑字体的字符集为GB_2312(详细见GB_2312百度百科)

3. 设置逻辑字体的高度为参数的高度

4. 创建此逻辑字体

 

ヽ( ̄▽ ̄)و,井字游戏就此介绍完毕啦,下面贴游戏图吧,(#^.^#)

井字游戏运行图井字游戏结束图

 

 

源代码:

避免有些没有C币的小伙伴(就像我),所以将源代码放到Github啦~

Github地址: Github

 

 

 

 

 

  • 2
    点赞
  • 5
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 终极编程指南 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值