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和上下左右,支持雙人輸入,而不是共用一個鼠標🙃;實現人機對戰;實現聯網對戰等等。