C++/EasyX面向對象編程實現-簡單雙人對戰五子棋

1、開始

爲了熟悉EasyX的API,我花了一天多一點的時間寫了一個簡單的雙人對戰五子棋遊戲,有開始界面、下棋界面和暫停界面,隨機選擇音樂並循環播放,使用鼠標點擊下棋。

之所以會花一天的時間,主要原因是對面向對象的編程方式不熟悉,在類設計上面權衡了蠻久,然後將就着寫出來了…

這些功能不多,和EasyX能夠玩出的花樣比起來還遠遠不夠。比如動作遊戲中實現的人物的移動、複雜動畫效果的實現等,都需要一些時間,尤其是準備合適的美術素材和進行處理。

2、類結構

下棋遊戲可以抽象出屏幕、棋盤、棋手、棋子等對象。我實現了screen、board、chess這三個對象,並用game對象進行統一管理。

chess中包含棋子半徑radius和繪製棋子的方法,是最基礎的類。screen管理各種界面的繪製,board管理棋盤的繪製和棋局的整體繪製。game是上層對象,調度這三個對象。

之所以沒有實現player,是因爲這裏沒有打算統計player的姓名、分數、下棋的棋數等信息…

在這裏插入圖片描述
遊戲的主文件代碼:

#include "game.h" 
using namespace std; 
int main() { 
	game my_game; 
	my_game.Load(); 
	return 0;
}

幾個界面:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

3、具體實現

就不說明具體細節了,註釋都是按照EasyX文檔上面寫的。

//screen.h
#pragma once
#include <graphics.h>
#include <string>
#include <conio.h>
#include <windows.h> 
#include <cstdlib>
#include <ctime>
#include <string>
//#include<mmsystem.h>
#pragma comment(lib,"winmm.lib")
using namespace std; 

class screen {
private:  
	int unit = 30;   //一個單元格大小
	int width = 20 * unit + 150, height = 21 * unit + 40;
	string startPath = "開始界面.jpg";
	string path = "五子棋背景.jpg"; 
	string tpath = "停止界面.jpg";

	IMAGE startBk; // 開始界面背景圖
	IMAGE boardBk;  // 棋盤背景圖對象
	IMAGE pauseBk; // 暫停界面背景圖

	int bkcolor = YELLOW;  // 屏幕背景顏色
public:  
	screen(); // 初始化繪圖設備
	~screen(); // 關閉繪畫設備 
	int getWidth() { return width; }
	int getHeight() { return height; }

	void printScreen();  // 繪製屏幕(使用背景圖路徑並繪製背景圖(使用部分圖片))
	void repeatPlayMusic();  // 循環播放背景音樂

	void startMenu();
	void pauseMenu();
};
//screen.cpp
#include "screen.h"

screen::screen()
{
	initgraph(width, height);
	// 用於開始批量繪圖,執行後,任何繪圖操作都將暫時不輸出到屏幕上,
	// 直到執行 FlushBatchDraw 或 EndBatchDraw 纔將之前的繪圖輸出
	HWND hwnd = GetHWnd(); // 獲取窗口句柄 
	SetWindowText(hwnd, "雙人五子棋 初版"); // 設置窗口標題

	BeginBatchDraw();

	loadimage(&startBk, startPath.c_str(), width, height);  // 加載開始界面的背景圖,進行圖片拉伸
	loadimage(&boardBk, path.c_str(), 1400, 1400); // 加載背景圖,圖片進行拉伸 
	loadimage(&pauseBk, tpath.c_str(), width, height); // 加載背景圖,拉伸
	srand((unsigned int)time(NULL)); // 隨機播放音樂
}

screen::~screen()
{
	EndBatchDraw();
	closegraph();  // 關閉繪畫設備
}

void screen::printScreen()
{
	setbkcolor(bkcolor);  // 設置屏幕背景顏色
	cleardevice(); //用當前背景色清空屏幕,並將當前點移至 (0, 0)
	
	putimage(0, 0, width, height, &boardBk, 0, 0); // 繪製圖像(指定寬高和起始位置)

	setbkmode(0);
	settextcolor(BLACK);
	int x = width - 4 * unit, y = height - 8 * unit;
	settextstyle(20, 0, _T("黑體"));
	outtextxy(x, y, "五子棋規則:");

	settextstyle(16, 0, _T("宋體"));
	outtextxy(x, y + unit, "1. 五子一線贏");
	outtextxy(x, y + 2 * unit, "2. 黑子先行");
	outtextxy(x, y + 3 * unit, "3. 按esc暫停");  
	outtextxy(x, y + 4 * unit, "4. 按s存檔");
	outtextxy(x, y + 5 * unit, "5. 按r悔棋");

	FlushBatchDraw();  
}

void screen::repeatPlayMusic()
{
	/* 
	0 千年幻想鄉
	1 竹取飛翔
	2 碎月
	3 幽雅に咲かせ 墨染の桜
	4 東方妖々夢~広有射怪鳥事
	*/
	string t1 = "open music", t2 = ".mp3 alias bkmusic";
	char kth = '0' + rand() % 5;  // 隨機播放音樂
	string sound = t1 + kth + t2; 
	/*
	1. 打開音樂 open後面的music.mp3是音樂文件的相對路徑,使用絕對路徑也可以
	2. alias bgm 是給音樂取個別名  下面就可以直接open bgm了,別名可以自取
	3. 播放音樂中 repeat表示重複播放,如果只想播放一次,可以去掉repeat
	4. mciSendString這個函數可以播放mp3,wav格式的音樂,如果代碼無誤但是沒法播放音樂,嘗試換一首
	*/
	mciSendString(sound.c_str(), NULL, 0, NULL); // 打開背景音樂
	mciSendString("play bkmusic repeat", NULL, 0, NULL);  // 循環播放音樂 	
}

void screen::startMenu()
{ 
	putimage(0, 0, width, height, &startBk, 0, 0);
	setbkmode(0);
	settextcolor(BLACK);
	settextstyle(50, 0, _T("隸書")); 
	outtextxy(width * 0.35, height * 0.7, "a 進入遊戲");
	outtextxy(width * 0.35, height * 0.76, "b 退出遊戲");
	FlushBatchDraw();

	settextcolor(LIGHTGRAY);
	settextstyle(30, 0, _T("黑體")); 
	outtextxy(15, height * 0.83, "雙人五子棋,進入遊戲後點擊鼠標落子,按esc鍵暫停");
	FlushBatchDraw();
}

void screen::pauseMenu()
{
	putimage(0, 0, width, height, &pauseBk, 0, 0);
	setbkmode(0);
	settextcolor(BLACK);
	settextstyle(50, 0, _T("隸書"));
	outtextxy(width * 0.35, height * 0.4, "1 繼續遊戲");
	outtextxy(width * 0.35, height * 0.46, "2 退出遊戲");
	FlushBatchDraw();
}
//board.h
#pragma once 
#include <graphics.h>
#include <windows.h>
#include <cstdio>

const int black = 1, white = 2;
// 提供棋子的半徑和打印棋子的靜態方法
class chess {
public:
	const static int radius = 8;  // 棋子的半徑
	friend class board;
	static void printChess(int x, int y, int color) { // 畫棋子 	// 畫面座標,非棋盤座標
		setlinestyle(PS_SOLID, 2); // 這裏設置當前邊框樣式
		setlinecolor(color);

		setfillstyle(BS_SOLID); // 設置當前填充樣式
		setfillcolor(color);  // 設置填充顏色
		fillcircle(x, y, radius); // 畫有邊框的填充圓
	}
};

// 實現二維數組運算的重載
// 實現雙下標的方式對元素進行賦值和讀取的操作
class row {
private:
	const static int size = 20; 
	int *p; // 一維數組
public:
	row() { p = new int[size]; } // 申請行內存
	~row() { delete[] p; }           // 釋放內存
	int &operator[](int idx) { 
		if (idx >= size) exit(-1); // 若下標值大於元素個數,則終止程序

		return p[idx];  // 返回該元素本身
	}
};

class board
{
private:
	int unit = 30; // 單元格大小
	const static int width = 20; // 棋盤大小
	row *used_board;  // 棋盤狀態 
public:     
	board() {
		used_board = new row[width];

		for (int i = 0; i < width; ++i) // 初始全部爲空
			for (int j = 0; j < width; ++j)
				used_board[i][j] = 0; // 0爲空; 1爲黑棋; 2爲白棋(black = 1, white = 2) 
	}
	~board() { delete [] used_board; }

	int getWidth() { return width;  }
	int getUnit() { return unit; }

	void printBoard();  // 打印棋盤
	void printChesses(chess *temp); // 根據used_board情況打印所有棋子, 用作暫停後重繪整體棋局

	bool whoWin();  // 判斷輸贏 
	row &operator[](int idx) {
		if (idx >= width) exit(-1);  // 若下標值大於元素個數,則終止程序
		 //返回一個row類型的對象,用row類的對象調用第二個下標值,再返回row類中的元素
		return used_board[idx]; 
	} 
};
//board.cpp
#include "board.h" 

char buffer[5];
char* change(int x) { // 轉換數字爲字符串
	sprintf_s(buffer, "%d", x);
	return buffer;
}

void board::printBoard()
{
	setcolor(BLACK); // 設置畫線顏色和文本顏色,相當於同時使用setlinecolor和settextcolor
	setlinestyle(PS_SOLID, 1); // 設置畫線樣式,實線,寬2
	for (int i = 2; i < width; ++i) { // 繪製棋盤
		line(unit, i * unit, width * unit, i * unit); // 橫線
		line(i * unit, unit, i * unit, width * unit); // 豎線
	} 

	setlinestyle(PS_SOLID, 2); // 設置畫線樣式,實線,寬2
	line(1 * unit, 1 * unit, width * unit, 1 * unit);  // 繪製外面的四條線
	line(width * unit, 1 * unit, width * unit, width * unit);
	line(1 * unit, 1 * unit, 1 * unit, width * unit);
	line(1 * unit, width * unit, width * unit, width * unit);
	
	setbkmode(0);  // 設置圖案填充和文字輸出時的背景模式,默認爲當前背景色
	//settextcolor(BLACK);  // 設置文字顏色 //前面已經設置過了
	settextstyle(16, 0, _T("宋體")); // 設置文字高度和字體
	// 對棋盤線標上下標
	for (int i = 1; i <= width; ++i)  // 縱座標
		outtextxy(unit - 25, i * unit - 8, change(i));
	for (int j = 1; j <= width; ++j)
		outtextxy(j * unit, width * unit + 10, 'A' + j - 1);

	// 五子棋/圍棋的5個黑點
	setfillcolor(BLACK); // 設置當前的填充顏色
	fillcircle(4 * unit, 4 * unit, 3); // 左上 // 使用當前線形和當前填充樣式繪製有外框的填充圓
	fillcircle(17 * unit, 4 * unit, 3); // 右上
	fillcircle(4 * unit, 17 * unit, 3); // 左下
	fillcircle(17 * unit, 17 * unit, 3); // 右下

	//fillcircle(width / 2 * unit, width / 2 * unit, 3); // 中間
	//setbkmode(0);
	//settextstyle(16, 0, _T("宋體"));
	//settextcolor(BLACK);
	//outtextxy(510, 40, "棋子座標爲:");
	//outtextxy(520, 60, "  行  列");
	//outtextxy(510, 100, "該:");
	//outtextxy(510, 130, "白棋步數:");
	//outtextxy(510, 160, "黑棋步數:");
}

void board::printChesses(chess *temp)  // 根據used_board的情況打印所有出現的棋子
{
	for (int i = 1; i <= width; ++i) {
		for (int j = 1; j <= width; ++j) {
			if (used_board[i - 1][j - 1] == 1) temp->printChess(i * unit, j * unit, BLACK);
			else if (used_board[i - 1][j - 1] == 2) temp->printChess(i * unit, j * unit, WHITE);
		}
	}
	FlushBatchDraw();
}
 
bool board::whoWin()
{		
	HWND hwnd = GetHWnd(); // windows窗口句柄
	for (int i = 0; i < width; ++i) {
		for (int j = 0; j < width; ++j) { //黑棋判斷贏了
			if (
				(used_board[i][j] == 1 && used_board[i + 1][j] == 1 && used_board[i + 2][j] == 1 && used_board[i + 3][j] == 1 && used_board[i + 4][j] == 1) || // 橫向
				(used_board[i][j] == 1 && used_board[i][j + 1] == 1 && used_board[i][j + 2] == 1 && used_board[i][j + 3] == 1 && used_board[i][j + 4] == 1) || // 縱向
				(used_board[i][j] == 1 && used_board[i + 1][j + 1] == 1 && used_board[i + 2][j + 2] == 1 && used_board[i + 3][j + 3] == 1 && used_board[i + 4][j + 4] == 1) || // 左上到右下
				(used_board[i][j] == 1 && used_board[i - 1][j + 1] == 1 && used_board[i - 2][j + 2] == 1 && used_board[i - 3][j + 3] == 1 && used_board[i - 4][j + 4] == 1)  // 右上到左下
			) {
				MessageBox(hwnd, TEXT("黑棋贏!"), "勝利:", MB_OK);
				system("pause");
				return true;
			} else if ( 	//白棋判斷贏了
				(used_board[i][j] == 2 && used_board[i + 1][j] == 2 && used_board[i + 2][j] == 2 && used_board[i + 3][j] == 2 && used_board[i + 4][j] == 2) || // 橫向
				(used_board[i][j] == 2 && used_board[i][j + 1] == 2 && used_board[i][j + 2] == 2 && used_board[i][j + 3] == 2 && used_board[i][j + 4] == 2) || // 縱向
				(used_board[i][j] == 2 && used_board[i + 1][j + 1] == 2 && used_board[i + 2][j + 2] == 2 && used_board[i + 3][j + 3] == 2 && used_board[i + 4][j + 4] == 2) || // 左上到右下
				(used_board[i][j] == 2 && used_board[i - 1][j + 1] == 2 && used_board[i - 2][j + 2] == 2 && used_board[i - 3][j + 3] == 2 && used_board[i - 4][j + 4] == 2)  // 右上到左下
			) {
				MessageBox(hwnd, TEXT("白棋贏!"), "勝利:", MB_OK);
				system("pause");
				return true;
			}
		}
	}
	return false;
}

下面的玩家信息輸入界面和退出界面沒有實現…

//game.h
#pragma once
#include "screen.h"
#include "board.h" 

#include <cmath>
#include <graphics.h>
// 開始界面;玩家信息輸入界面;下棋界面;暫停界面;退出 
enum status {START, RAWINPUT, PLAY, PAUSE, EXIT}; 

class game {
private:
	screen *my_screen;
	board  my_board;
	chess  *my_chess;
	status game_status, old_game_status;  // 遊戲狀態	
	int player_exchange;

	void StartOrNot();	 // 開始或退出
	void PauseOrNot(); // 暫停或退出
	void End() {
		delete my_screen;
		delete my_chess;
	};

	void BasicShow();  // 不會變的(棋盤和部分屏幕)
	void UpdateWithInput(); // 用戶點擊棋盤更新used_board狀態
	void Circle();  // 遊戲主循環

	void WriteBoardRecord();  // 存檔,棋盤數據
	void WritePlayRecord();  // 存檔,玩家數據
	void ReadBoardRecord(); // 讀檔,棋盤數據
	void ReadPlayRecord(); // 讀檔,玩家數據

public:
	game() {
		my_screen = new screen; // 先有屏幕
		my_board = *new board; // 棋盤在屏幕上面
		my_chess = new chess; 
		old_game_status = game_status = START;  // 先是開始界面
		player_exchange = 1;  // 開始是黑棋先行
	} 
	void Load() { // 唯一的公共接口,加載遊戲
		StartOrNot();
		Circle();
		End();
	}
}; 

沒有玩家信息,讀檔和存檔函數都懶得實現。

//game.cpp
#include "game.h"

void game::StartOrNot()
{
	my_screen->repeatPlayMusic();  // 循環播放音樂 
	if (game_status == START) {
		my_screen->startMenu(); // 開始界面 

		char input;
		do {
			input = _getch(); // 非功能鍵、控制鍵、數字鍵盤的碼值
			if (input == 'a') {
				game_status = PLAY; // 開始新遊戲
				break;
			}
			else if (input == 'b') {
				game_status = EXIT; // 直接退出
				exit(0);
			}
		} while (input != 'a' || input != 'b'); // 其他鍵繼續
	}
}

void game::BasicShow()  // 打印遊戲全部基本不變的要素
{
	my_screen->printScreen();
	my_board.printBoard();
	my_board.printChesses(my_chess);
	FlushBatchDraw();
}

void game::PauseOrNot()
{
	if (game_status == PAUSE) {
		my_screen->pauseMenu(); // 暫停界面
		old_game_status = game_status; // 記錄之前的狀態
		 
		char input;
		do {
			input = _getch(); // 阻塞進程
			if (input == '1') { // 非功能鍵、控制鍵、數字鍵盤的碼值
				game_status = PLAY; 
				return;
			} else if (input == '2') {
				game_status = EXIT;
				exit(0);
			}
		} while (input != '1' || input != '2');
	} 
	old_game_status = PLAY; 
}

void game::UpdateWithInput()
{
	if (game_status == PLAY) {
		MOUSEMSG m;   // 定義鼠標信息
		while (MouseHit()) { // 鼠標發生消息
			m = GetMouseMsg();  // 得到鼠標消息
			int a = 0, b = 0, x = 0, y = 0;  // 交叉點的棋盤座標和屏幕座標
			int bw = my_board.getWidth(), u = my_board.getUnit();
			
			int flag = 0;
			if (m.uMsg == WM_LBUTTONDOWN) {  // 單擊鼠標左鍵
				// 得到棋子的位置
				for (int i = 1; i <= bw; ++i) {
					for (int j = 1; j <= bw; ++j) {
						if (abs(m.x - i * u) <= 12 && abs(m.y - j * u <= 12)) {
							a = i - 1, b = j - 1;
							x = i * u, y = j * u;
							flag = 1;  break;
						}
					}
					if (flag) break;
				}
				if ((x >= 30 && y >= 30) && (x <= bw * u && y <= bw * u) && my_board[a][b] == 0) {
					if (player_exchange % 2 == 1) my_board[a][b] = 1;  // 下黑棋(奇數)
					else my_board[a][b] = 2; // 下白棋(偶數)
					
					my_chess->printChess(x, y, my_board[a][b] == 1 ? BLACK : WHITE); // 即時更新,繪製棋子
					FlushBatchDraw();
					player_exchange++; // 換棋手
					if (my_board.whoWin()) exit(0);
				}
			}
		}

		char input;
		if (_kbhit()) {
			input = _getch();
			if (input == 27) game_status = PAUSE;  // 用戶輸入esc,暫停
			else if (input == 's') { }  // 存檔
			else if (input == 'r') { }  // 悔棋
		}
	}
}

void game::Circle() // 實現遊戲主循環
{
	if (game_status == PLAY) BasicShow(); // 如果進入遊戲,顯示屏幕和棋盤
	while (true) {
		if (old_game_status == PAUSE) BasicShow(); // 從暫停界面恢復到棋盤
		UpdateWithInput(); // 等待輸入
		PauseOrNot();  // 判斷是否進入暫停狀態
	}
}

400多行,不大。就是這樣了。

不過五子棋還是可以繼續做下去,把上面的功能都實現。然後,還可以做:比如使用不同的按鍵asdw和上下左右,支持雙人輸入,而不是共用一個鼠標🙃;實現人機對戰;實現聯網對戰等等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章