用C++編寫一個井字游戲 (Tic Tac Toe)
這個有趣的C++系列打算展示一下使用C++寫代碼可以和其他主流語言一樣高效而有趣。在第二部分,我將向你展示使用C++從無到有的創建一個井字游戲。這篇文章,以及整個系列都是針對那些想學習C++或者對這個語言性能好奇的開發者。
許多年輕人想學習編程來寫游戲。C++是用的最多的用來寫游戲的語言,盡管在寫出下個憤怒的小鳥之前,需要學會很多的編程經驗。一個井子游戲是開始學習的一個好選擇,事實上,在許多年前我開始學習C++后,他是我寫的地一個游戲。我希望這篇文章可以幫助到那些還不熟悉C++的初學者和有經驗的開發者。
我使用的是Visual Studio 2012來寫這篇文章的源代碼。
游戲介紹
如果你沒有玩過井字游戲或者并不熟悉這個游戲,下面是來自維基百科的描述.
井字游戲 (或者"圈圈和叉叉",Xs and Os) 是一個兩人的紙筆游戲,兩個人輪流在3X3的網格內畫圈和叉. 當一名玩家放置的標志在水平,垂直或者對角線上成一條線即獲得勝利.

這個游戲也可以人機對戰,先手不固定.
創建這個程序的時候有2個關鍵的東西:程序的邏輯和程序的UI界面. 有許多在windows中創建用戶UI的方法, 包括 Win32 API, MFC, ATL, GDI+, DirectX, etc. 在這篇文章中,我將展示使用多種技術來實現同一個程序邏輯. 我們將新建2個應用, 一個使用 Win32 API 另一個使用 C++/CX.
游戲邏輯
如果一個玩家在網格上放下一個標記時,遵循幾個簡單的規則,那他就可以玩一個完美的游戲(意味著贏或者平局)。在Wikipedia上寫有這些規則,在里面你也可以找到先手玩家的最優策略。
在xkcd drawing上有先手和后手玩家的最優策略。盡管有幾個錯誤(在幾種情況下沒有走必勝的步驟,至少在一個情況下丟失了一個X標記),我將使用這個版本作為游戲策略(修復了那些我能找到的錯誤)。記住電腦總是玩一個完美的游戲。如果你實現了這樣一個游戲,你可能也想讓用戶贏,這種情況下你需要一個不同的方法。當對本文的目的,這個策略應該足夠了。
提出的第一個問題是在C++程序中用什么數據結構來表示圖像的模型。這可以有不同的選擇,比如樹、圖、數組或者位字段(如果真有人對內存消耗很在意)。網格有9個單元,我選擇的最簡單的使用對每個單元使用一個包含9個整數的數組:0表示空的單元,1表示單元被標記為X,2表示單元被標記為O。讓我們看下圖以及它將被如何編碼。
這幅圖可以這么理解:
- 在單元(0,0)放X。網格可以編碼為:1,0,0,0,0,0,0,0,0
- 如果對手在單元(0,1)放置O,那么在單元(1,1)放置X。現在網格編碼為:1,2,0,0,1,0,0,0,0
- 如果對手在單元(0,2)放置O,那么在單元(2,2)放置X。現在網格編碼為:1,2,2,0,1,0,0,0,1
- ...
- 如果對手在單元(2,2)放置O,那么在單元(2,0)放置X。現在網格編碼為:1,2,0,0,1,0,1,0,2。這時,無論對手怎么做,X都將贏得比賽。
- 如果對手在單元(0,2)放置O,那么在單元(1,0)放置X。現在網格編碼為:1,2,2,1,1,0,1,0,2。這表示的是一個贏得比賽的一步。
- ... </ul>
記住這個我們就可以開始在程序中對其編碼了。我們將使用一個std::array來表示一個9格板。這是個固定大小的容器,在編譯時就已知的大小,在連續的內存區域存儲元素。為了避免一遍又一遍的使用相同數組類型,我將定義一個別名來簡化。
#include上面描述的最優策略用這樣的數組隊列(另一個數組)來表示。typedef std::array tictactoe_status;
tictactoe_status const strategy_x[] = { {1,0,0,0,0,0,0,0,0}, {1,2,0,0,1,0,0,0,0}, {1,2,2,0,1,0,0,0,1}, {1,2,0,2,1,0,0,0,1}, // ... };tictactoe_status const strategy_o[] = { {2,0,0,0,1,0,0,0,0}, {2,2,1,0,1,0,0,0,0}, {2,2,1,2,1,0,1,0,0}, {2,2,1,0,1,2,1,0,0}, // ... };</pre>strategy_x是先手玩家的最優策略,strategy_o是后手玩家的最優策略。如果你看了文中的源代碼,你將注意到這兩個數組的真實定義和我前面展示的不同。
tictactoe_status const strategy_x[] = {include "strategy_x.h"
};
tictactoe_status const strategy_o[] = {
include "strategy_o.h"
};</pre>
這是個小技巧,我的理由是,它允許我們把真實的很長的數組內容放在分開的文件中(這些文件的擴展性不重要,它可以不僅僅是C++頭文件,也可以是其他任何文件),保證源碼文件和定義簡單干凈。strategy_x.h和strategy_o.h文件在編譯的預處理階段就被插入到源碼文件中,如同正常的頭文件一樣。下面是strategy_x.h文件的片斷。
// http://imgs.xkcd.com/comics/tic_tac_toe_large.png // similar version on http://upload.wikimedia.org/wikipedia/commons/d/de/Tictactoe-X.svg // 1 = X, 2 = O, 0 = unoccupied1,0,0,0,0,0,0,0,0,
1,2,0,0,1,0,0,0,0, 1,2,2,0,1,0,0,0,1, 1,2,0,2,1,0,0,0,1, 1,2,0,0,1,2,0,0,1,</pre>你應該注意到,如果你使用支持C++11的編譯器,你可以使用一個std::vector而不是C類型的數組。Visual Studio 2012不支持這么做,但在Visual Studio 2013中支持。
std::vector為了定義這些數字表示的對應玩家,我定義了一個叫做tictactoe_player的枚舉類型變量。strategy_o = { {2, 0, 0, 0, 1, 0, 0, 0, 0}, {2, 2, 1, 0, 1, 0, 0, 0, 0}, {2, 2, 1, 2, 1, 0, 1, 0, 0}, {2, 2, 1, 0, 1, 2, 1, 0, 0}, {2, 2, 1, 1, 1, 0, 2, 0, 0}, }; enum class tictactoe_player : char { none = 0, computer = 1, user = 2, };游戲的邏輯部分將會在被稱之為tictactoe_game 的類中實現。最基本的,這個 class 應該有下面的狀態:
- 一個布爾值用來表示游戲是否開始了,命名為 started 。
- 游戲的當前狀態(網格上的標記), 命名為 status 。
- 根據當前的狀態得到的之后可以進行的下法的集合,命名為strategy </ul>
- is_started()來表示游戲是否開始了
- is_victory()來檢查是否有哪位玩家在游戲中獲勝
- is_finished()來檢查游戲是否結束。當其中某位玩家在游戲中獲勝或者當網格被填滿玩家不能再下任何的棋子的時候,游戲結束。 </ul>
- 一個入口點,通常來說是WinMain,而不是main。它需要一些參數例如當前應用實例的句柄,命令行和指示窗口如何展示的標志。
- 一個窗口類,代表了創建一個窗口的模板。一個窗口類包含了一個為系統所用的屬性集合,例如類名,class style(不同于窗口的風格),圖標,菜單,背景刷,窗口的指針等。一個窗口類是進程專用的并且必須要注冊到系統優先級中來創建一個窗口。使用RegisterClassEx來注冊一個窗口類。
- 一個主窗口,基于一個窗口類來創建。使用CreateWindowEx可以創建一個窗口。
- 一個窗口過程函數,它是一個處理所有基于窗口類創建的窗口的消息的方法。一個窗口過程函數與窗口相聯,但是它不是窗口。
- 一個消息循環。一個窗口通過兩種方式來接受消息:通過SendMessage,直接調用窗口過程函數直到窗口過程函數處理完消息之后才返回,或者通過PostMessage (或 PostThreadMessage)把一個消息投送到創建窗口的線程的消息隊列中并且不用等待線程處理直接返回。因此線程必須一直運行一個從消息隊列接收消息和把消息發送給窗口過程函數的循環 </ul>
- WindowClass類是對窗口類相關資源初始化的封裝,在構造函數中,它初始化了WNDCLASSEX 的結構并且調用 RegisterClassEx 方法。在析構函數中,它通過調用 UnregisterClass 移除窗口的注冊。
- Window類是通過對HWND封裝一些諸如Create,ShowWindow 和Invalidate的函數(它們的名字已經告訴了你他們是做什么的)。它還有幾個虛成員代表消息句柄,它們會被窗口過程調用 (OnPaint,OnMenuItemClicked,OnLeftButtonDown) 。這個window類將會被繼承來并提供具體的實現。
- DeviceContex類是對設備描述表(HDC)的封裝。在構造函數中它調用 BeginPaint 函數并且在析構函數中調用 EndPaint 函數。 </ul>
- 處理“Start user”按鈕的theClickedevent事件的handler
- 處理“Start computer”按鈕的theClickedevent事件的handler
- 處理面板網格的thePointerReleasedevent事件的handler,當指針(鼠標或者手勢)從網格釋放時被調用。 </ul> 對這兩個按鈕點擊的handler,在邏輯上與我們在Win32桌面應用中實現的類似。首先,我們必須要重置游戲(一會會看到這代表什么意思)。如果玩家先開始,那么我們僅僅只需要用正確的策略來初始化游戲對象。如果是電腦先開始,那我們除了要初始化策略,還要讓電腦呈現出真正走了一步并且在電腦走的那一步的單元格上做上標記。
class tictactoe_game { bool started; tictactoe_status status; std::setstrategy; // ... };</tictactoe_status></pre>
在游戲的過程中,我們需要知道游戲是否開始了、結束了,如果結束了,需要判定是否有哪個玩家贏了或者最終兩個人打平。為此,tictactoe_game類提供了三個方法:
bool is_started() const {return started;} bool is_victory(tictactoe_player const player) const {return is_winning(status, player);} bool is_finished() const {</span>對于方法is_victory()和is_finished(),實際上是依賴于兩個私有的方法,is_full(), 用來表示網格是否被填滿并且不能再放下任何的棋子,以及方法is_winning, 用來表示在該網格上是否有某玩家勝出。它們的實現將會很容易被讀懂。is_full 通過計算網格中空的(在表示網格的數組中值為0)格子的數量,如果沒有這樣的格子那么將返回true。is_winning將會檢查這些連線,網格的行、列、以及對角線,依此來查看是否有哪位玩家已經獲勝。
bool is_winning(tictactoe_status const & status, tictactoe_player const player) const { auto mark = static_cast(player); return (status[0] == mark && status[1] == mark && status[2] == mark) || (status[3] == mark && status[4] == mark && status[5] == mark) || (status[6] == mark && status[7] == mark && status[8] == mark) || (status[0] == mark && status[4] == mark && status[8] == mark) || (status[2] == mark && status[4] == mark && status[6] == mark) || (status[0] == mark && status[3] == mark && status[6] == mark) || (status[1] == mark && status[4] == mark && status[7] == mark) || (status[2] == mark && status[5] == mark && status[8] == mark); } bool is_full(tictactoe_status const & status) const { return 0 == std::count_if(std::begin(status), std::end(status), {return mark == 0;}); }</char></pre>當一個玩家獲勝的時候,我們想給他所連成的線(行、列、或者對角線)上畫一條醒目的線段。因此首先我們得知道那條線使得玩家獲勝。我們使用了方法get_winning_line()來返回一對 tictactoe_cell,用來表示線段的兩端。它的實現和is_winning很相似,它檢查行、列和對角線上的狀態。它可能會看起來有點冗長,但是我相信這個方法比使用循環來遍歷行、列、對角線更加簡單。</span>
struct tictactoe_cell { int row; int col;tictactoe_cell(int r = INT_MAX, int c = INT_MAX):row(r), col(c) {}
bool is_valid() const {return row != INT_MAX && col != INT_MAX;} };
std::pair<TICTACTOE_CELL, tictactoe_cell=""> const get_winning_line() const { auto mark = static_cast
(tictactoe_player::none); if(is_victory(tictactoe_player::computer)) mark = static_cast (tictactoe_player::computer); else if(is_victory(tictactoe_player::user)) mark = static_cast (tictactoe_player::user); if(mark != 0) { if(status[0] == mark && status[1] == mark && status[2] == mark) return std::make_pair(tictactoe_cell(0,0), tictactoe_cell(0,2)); if(status[3] == mark && status[4] == mark && status[5] == mark) return std::make_pair(tictactoe_cell(1,0), tictactoe_cell(1,2)); if(status[6] == mark && status[7] == mark && status[8] == mark) return std::make_pair(tictactoe_cell(2,0), tictactoe_cell(2,2)); if(status[0] == mark && status[4] == mark && status[8] == mark) return std::make_pair(tictactoe_cell(0,0), tictactoe_cell(2,2)); if(status[2] == mark && status[4] == mark && status[6] == mark) return std::make_pair(tictactoe_cell(0,2), tictactoe_cell(2,0)); if(status[0] == mark && status[3] == mark && status[6] == mark) return std::make_pair(tictactoe_cell(0,0), tictactoe_cell(2,0)); if(status[1] == mark && status[4] == mark && status[7] == mark) return std::make_pair(tictactoe_cell(0,1), tictactoe_cell(2,1)); if(status[2] == mark && status[5] == mark && status[8] == mark) return std::make_pair(tictactoe_cell(0,2), tictactoe_cell(2,2)); }
return std::make_pair(tictactoe_cell(), tictactoe_cell()); }</char></char></char></TICTACTOE_CELL,></pre>
現在我們只剩下添加開始游戲功能和為網格放上棋子功能(電腦和玩家兩者).
對于開始游戲,我們需要知道,由誰開始下第一個棋子,因此我們可以采取比較合適的策略(兩種方式都需要提供,電腦先手或者玩家先手都要被支持)。同時,我們也需要重置表示網格的數組。方法start()對開始新游戲進行初始化。可以下的棋的策略的集合被再一次的初始化, 從stategy_x 或者strategy_o進行拷貝。從下面的代碼可以注意到,strategy是一個std::set, 并且strategy_x或者strategy_o都是有重復單元的數組,因為在tictoctoe表里面的一些位置是重復的。這個std::set 是一個只包含唯一值的容器并且它保證了唯一的可能的位置(例如對于strategy_o來說,有一半是重復的)。
中的std::copy算法在這里被用來進行數據單元的拷貝,將當前的內容拷貝到std::set中,并且使用方法assign()來將std::array的所有的元素重置為0。 void start(tictactoe_player const player) { strategy.clear(); if(player == tictactoe_player::computer) std::copy(std::begin(strategy_x), std::end(strategy_x), std::inserter(strategy, std::begin(strategy))); else if(player == tictactoe_player::user) std::copy(std::begin(strategy_o), std::end(strategy_o), std::inserter(strategy, std::begin(strategy)));status.assign(0);
started = true; }</pre>
當玩家走一步時,我們需要做的是確保選擇的網格是空的,并放置合適的標記。move()方法的接收參數是網格的坐標、玩家的記號,如果這一步有效時返回真,否則返回假。
bool move(tictactoe_cell const cell, tictactoe_player const player) { if(status[cell.row3 + cell.col] == 0) { status[cell.row3 + cell.col] = static_cast(player); if(is_victory(player)) { started = false; } return true;
}
return false; }</char></pre>電腦走一步時需要更多的工作,因為我們需要找到電腦應該走的最好的下一步。重載的move()方法在可能的步驟(策略)集合中查詢,然后從中選擇最佳的一步。在走完這步后,會檢查電腦是否贏得這場游戲,如果是的話標記游戲結束。這個方法返回電腦走下一步的位置。
tictactoe_cell move(tictactoe_player const player) { tictactoe_cell cell;strategy = lookup_strategy();
if(!strategy.empty()) { auto newstatus = lookup_move();
for(int i = 0; i < 9; ++i) { if(status[i] == 0 && newstatus[i]==static_cast<char>(player)) { cell.row = i/3; cell.col = i%3; break; } } status = newstatus; if(is_victory(player)) { started = false; }
}
return cell; }</char></pre>lookup_strategy()方法在當前可能的移動位置中迭代,來找到從當前位置往哪里移動是可行的。它利用了這樣的一種事實,空的網格以0來表示,任何已經填過的網格,不是用1就是用2表示,而這兩個值都大于0。一個網格的值只能從0變為1或者2。不可能從1變為2或從2變為1。
當游戲開始時的網格編碼為0,0,0,0,0,0,0,0,0來表示并且當前情況下任何的走法都是可能的。這也是為什么我們要在thestart()方法里把整個步數都拷貝出來的原因。一旦玩家走了一步,那能走的步數的set便會減少。舉個例子,玩家在第一個格子里走了一步。此時網格編碼為1,0,0,0,0,0,0,0,0。這時在數組的第一個位置不可能再有0或者2的走法因此需要被過濾掉。
std::settictactoe_game::lookup_strategy() const { std::set nextsubstrategy; for(auto const & s : strategy) { bool match = true; for(int i = 0; i < 9 && match; ++i) { if(s[i] < status[i]) match = false; }
if(match) { nextsubstrategy.insert(s); }
}
return nextsubstrategy; }</tictactoe_status></tictactoe_status></pre>
在選擇下一步時我們需要確保我們選擇的走法必須與當前的標記不同,如果當前的狀態是1,2,0,0,0,0,0,0,0而我們現在要為玩家1選擇走法那么我們可以從余下的7個數組單元中選擇一個,可以是:1,2,1,0,0,0,0,0,0或1,2,0,1,0,0,0,0,0... 或1,2,0,0,0,0,0,0,1。然而我們需要選擇最優的走法而不是僅僅只隨便走一步,通常最優的走法也是贏得比賽的關鍵。因此我們需要找一步能使我們走向勝利,如果沒有這樣的一步,那就隨便走吧。
tictactoe_status tictactoe_game::lookup_move() const { tictactoe_status newbest = {0}; for(auto const & s : strategy) { int diff = 0; for(int i = 0; i < 9; ++i) { if(s[i] > status[i]) diff++; }if(diff == 1) { newbest = s; if(is_winning(newbest, tictactoe_player::computer)) { break; } }
}
assert(newbest != empty_board);
return newbest; }</pre>
做完了這一步,我們的游戲的邏輯部分就完成了。更多細節請閱讀game.h和game.cpp中的代碼
一個用Win32 API實現的游戲
我將用Win32 API做用戶界面來創建第一個應用程序。如果你不是很熟悉Win32 編程那么現在已經有大量的資源你可以利用學習。為了使大家理解我們如何創建一個最終的應用,我將只講述一些必要的方面。另外,我不會把每一行代碼都展現并解釋給大家,但是你可以通過下載這些代碼來閱讀瀏覽它。
一個最基本的Win32應用需要的一些內容:
你可以在 MSDN 中找到關于Win 32 應用程序如何注冊窗口類、創建一個窗口、運行消息循環的例子。一個Win32的應用程序看起來是這樣的:
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASS wc; // set the window class attributes // including pointer to a window procedureif (!::RegisterClass(&wc)) return FALSE;
HWND wnd = ::CreateWindowEx(...); if(!wnd) return FALSE;
::ShowWindow(wnd, nCmdShow);
MSG msg; while(::GetMessage(&msg, nullptr, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); }
return msg.wParam;
}</pre>當然,這還不夠,我們還需要一個窗口過程函數來處理發送給窗口的消息,比如PAINT消息,DESTORY 消息,菜單消息和其它的一些必要的消息。一個窗口過程函數看起來是這樣的:
LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch(message) { case WM_PAINT: { PAINTSTRUCT ps; HDC dc = ::BeginPaint(hWnd, &ps); // paint ::EndPaint(hWnd, &ps); } break;case WM_DESTROY: ::PostQuitMessage(0); return 0;
case WM_COMMAND: { ... } break; }
return ::DefWindowProc(hWnd, message, wParam, lParam); }</pre>
我更喜歡寫面向對象的代碼,不喜歡面向過程,所以我用幾個類封裝了窗口類、窗口和設備描述表。你可以在附件的代碼framework.h 和framework.cpp 找到這些類的實現(它們非常小巧 )。
這個游戲的主要窗口是TicTacToeWindow類,它是從Window類繼承而來,它重載了虛擬方法來處理消息,該類的聲明是這樣的:
class TicTacToeWindow : public Window { HANDLE hBmp0; HANDLE hBmpX; BITMAP bmp0; BITMAP bmpX;tictactoe_game game;
void DrawBackground(HDC dc, RECT rc); void DrawGrid(HDC dc, RECT rc); void DrawMarks(HDC dc, RECT rc); void DrawCut(HDC dc, RECT rc);
virtual void OnPaint(DeviceContext* dc) override; virtual void OnLeftButtonUp(int x, int y, WPARAM params) override; virtual void OnMenuItemClicked(int menuId) override;
public: TicTacToeWindow(); virtual ~TicTacToeWindow() override; };</pre>MethodOnPaint()函數用來繪制窗口,它用來繪制窗口背景,網格線,填充的單元格(如果有的話),如果在游戲結束,玩家贏了,一條紅線在獲勝行,列或對角線的 標記。為了避免閃爍,我們使用了雙緩沖技術:創建一個內存設備文本(通過調用toBeginPaint函數準備窗口的設備文本來匹配),一個內存中的位圖匹配內存設備文本,繪制該位圖,然后用窗口設備文本來復制內存設備文本。
void TicTacToeWindow::OnPaint(DeviceContext* dc) { RECT rcClient; ::GetClientRect(hWnd, &rcClient);auto memdc = ::CreateCompatibleDC(dc); auto membmp = ::CreateCompatibleBitmap(dc, rcClient.right - rcClient.left, rcClient.bottom-rcClient.top); auto bmpOld = ::SelectObject(memdc, membmp);
DrawBackground(memdc, rcClient);
DrawGrid(memdc, rcClient);
DrawMarks(memdc, rcClient);
DrawCut(memdc, rcClient);
::BitBlt(*dc, rcClient.left, rcClient.top, rcClient.right - rcClient.left, rcClient.bottom-rcClient.top, memdc, 0, 0, SRCCOPY);
::SelectObject(memdc, bmpOld); ::DeleteObject(membmp); ::DeleteDC(memdc); }</pre>
![]()
我不會在這里列出DrawBackground,DrawGridand和 DrawMarksfunctions的內容。他們不是很復雜,你可以閱讀源代碼。DrawMarksfunction使用兩個位圖,ttt0.bmp和tttx.bmp,繪制網格的痕跡。
![]()
我將只顯示如何在獲勝行,列或對角線繪制紅線。首先,我們要檢查游戲是否結束,如果結束那么檢索獲勝線。如果兩端都有效,然后計算該兩個小區的中心,創建和選擇一個畫筆(實心,15像素寬的紅色線)并且繪制兩個小區的中間之間的線。
void TicTacToeWindow::DrawCut(HDC dc, RECT rc) { if(game.is_finished()) { auto streak = game.get_winning_line();if(streak.first.is_valid() && streak.second.is_valid()) { int cellw = (rc.right - rc.left) / 3; int cellh = (rc.bottom - rc.top) / 3; auto penLine = ::CreatePen(PS_SOLID, 15, COLORREF(0x2222ff)); auto penOld = ::SelectObject(dc, static_cast<hpen>(penLine)); ::MoveToEx( dc, rc.left + streak.first.col * cellw + cellw/2, rc.top + streak.first.row * cellh + cellh/2, nullptr); ::LineTo(dc, rc.left + streak.second.col * cellw + cellw/2, rc.top + streak.second.row * cellh + cellh/2); ::SelectObject(dc, penOld); }
} }</hpen></pre>主窗口有三個項目菜單, ID_GAME_STARTUSER在用戶先移動時啟動一個游戲, ID_GAME_STARTCOMPUTER在當電腦先移動時啟動一個游戲, ID_GAME_EXIT用來關閉應用。當用戶點擊兩個啟動中的任何一個,我們就必須開始一個游戲任務。如果電腦先移動,那么我們應該是否移動,并且,在所有情況中,都要重新繪制窗口。
void TicTacToeWindow::OnMenuItemClicked(int menuId) { switch(menuId) { case ID_GAME_EXIT: ::PostMessage(hWnd, WM_CLOSE, 0, 0); break;case ID_GAME_STARTUSER: game.start(tictactoe_player::user); Invalidate(FALSE); break;
case ID_GAME_STARTCOMPUTER: game.start(tictactoe_player::computer); game.move(tictactoe_player::computer); Invalidate(FALSE); break; } }</pre>
現在只剩下一件事了,就是留意在我們的窗口中處理用戶單擊鼠標的行為。當用戶在我們的窗口客戶區內按下鼠標時,我們要去檢查是鼠標按下的地方是在哪一個網格內,如果這個網格是空的,那我們就把用戶的標記填充上去。之后,如果游戲沒有結束,就讓電腦進行下一步的移動。
void TicTacToeWindow::OnLeftButtonUp(int x, int y, WPARAM params) { if(game.is_started() && !game.is_finished()) { RECT rcClient; ::GetClientRect(hWnd, &rcClient);int cellw = (rcClient.right - rcClient.left) / 3; int cellh = (rcClient.bottom - rcClient.top) / 3; int col = x / cellw; int row = y / cellh; if(game.move(tictactoe_cell(row, col), tictactoe_player::user)) { if(!game.is_finished()) game.move(tictactoe_player::computer); Invalidate(FALSE); }
} }</pre>最后,我們需要實現WinMain函數,這是整個程序的入口點。下面的代碼與這部分開始我給出的代碼非常相似,不同的之處是它使用了我對窗口和窗口類進行封裝的一些類。
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WindowClass wndcls(hInstance, L"TicTacToeWindowClass", MAKEINTRESOURCE(IDR_MENU_TTT), CallWinProc);TicTacToeWindow wnd; if(wnd.Create( wndcls.Name(), L"Fun C++: TicTacToe", WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, 300, 300, hInstance)) { wnd.ShowWindow(nCmdShow);
MSG msg; while(::GetMessage(&msg, nullptr, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } return msg.wParam;
}
return 0; }</pre>雖然我覺得我放在這里的代碼是相當的短小精焊,但如果你不熟悉Win32 API程序設計,你仍然可能會覺得這些代碼有點復雜。無論如何,你都一定要清楚的了解對象的初始化、如何創建一個窗口、如何處理窗口消息等。但愿你會覺得下一部分更有趣。
一個Windows Runtime的游戲app
Windows Runtime是Windows 8引入的一個新的Windows運行時引擎. 它依附于Win32并且有一套基于COM的API. 為Windows Runtime創建的app通常很糟糕,被人稱為"Windows商店" 應用. 它們運行在Windows Runtime上, 而不是Windows商店里, 但是微軟的市場營銷人員可能已經沒什么創造力了. Windows Runtime 應用和組件可以用C++實現,不管是用Windows Runtime C++ Template Library (WTL) 或者用 C++ Component Extensions (C++/CX)都可以. 在這里我將使用XAML和C++/CX來創建一個功能上和我們之前實現的桌面版應用類似的應用。
當你創建一個空的Windows Store XAML應用時向導創建的項目實際上并不是空的, 它包含了所有的Windows Store應用構建和運行所需要的文件和配置。但是這個應用的main page是空的。
我們要關心對這篇文章的目的,唯一的就是主界面。 XAML代碼可以在應用在文件MainPage.xaml中,和背后的MainPage.xaml.h MainPage.xaml.cpp的代碼。,我想建立簡單的應用程序如下圖。
![]()
下面是XAML的頁面可能看起來的樣子(在一個真實的應用中,你可能要使用應用程序欄來操作,如啟動一個新的游戲,主頁上沒有按鍵,但為了簡單起見,我把它們在頁面上)
<?XML:NAMESPACE PREFIX = [default] http://schemas.microsoft.com/winfx/2006/xaml/presentation NS = "
<grid.rowdefinitions> </rowdefinition></rowdefinition></rowdefinition></grid.rowdefinitions> <textblock verticalalignment="Center" horizontalalignment="Center" margin="10" fontfamily="Segoe UI" fontsize="42" foreground="White" text="Fun C++: Tic Tac Toe" grid.row="0"> <textblock name="txtStatus" verticalalignment="Center" horizontalalignment="Center" margin="10" fontfamily="Segoe UI" fontsize="42" foreground="LightGoldenrodYellow" text="Computer wins!" grid.row="1"> <grid name="board" background="White" height="400" margin="50" grid.row="2" pointerreleased="board_PointerReleased" width="400"> <grid.columndefinitions> <columndefinition width="1*"> <columndefinition width="1*"> <columndefinition width="1*"> </columndefinition></columndefinition></columndefinition></grid.columndefinitions> <grid.rowdefinitions> <rowdefinition height="1*"> <rowdefinition height="1*"> <rowdefinition height="1*"> </rowdefinition></rowdefinition></rowdefinition></grid.rowdefinitions> <!-- Horizontal Lines --> <rectangle height="1" verticalalignment="Bottom" grid.row="0" fill="Black" grid.columnspan="3"> <rectangle height="1" verticalalignment="Bottom" grid.row="1" fill="Black" grid.columnspan="3"> <rectangle height="1" verticalalignment="Bottom" grid.row="2" fill="Black" grid.columnspan="3"> <!-- Vertical Lines --> <rectangle horizontalalignment="Right" width="1" fill="Black" grid.rowspan="3" grid.column="0"> <rectangle horizontalalignment="Right" width="1" fill="Black" grid.rowspan="3" grid.column="1"> <rectangle horizontalalignment="Right" width="1" fill="Black" grid.rowspan="3" grid.column="2"> </rectangle></rectangle></rectangle></rectangle></rectangle></rectangle></grid> <stackpanel horizontalalignment="Center" grid.row="3" orientation="Horizontal"> <button name="btnStartUser" content="Start user" click="btnStartUser_Click"> </button><button name="btnStartComputer" content="Start computer" click="btnStartComputer_Click">
</button></stackpanel></textblock></textblock></grid></page></pre>
與win32桌面版的游戲不同,在Windows Runtime的程序中,我們不必關心用戶界面的繪制,但我們還得創建UI元素。比如,當用戶在玩游戲的時候,在其中一個格子里單擊了鼠標,我們就必須創建一個UI元素來表示一個標記。為此,我會用在桌面版(too.bmp and ttx.bmp)中用過的位圖,并且在圖像控件中顯示它們.。我還會在獲勝的行、列、或對角線上畫一個紅色的線,為此,我會用到Lineshape類。
我們可以直接把tictactoe_game的源代碼(game.h, game.cpp, strategy_x.h and strategy_o.h)添加到工程里。或者我們可以把它們導出成一個單獨的DLL。為了方便,我使用了相同的源文件。然后我們必須添加一個tictactoe_game對象到MainPage類中。
#pragma onceinclude "MainPage.g.h"
include "..\Common\game.h"
namespace TicTacToeWinRT { public ref class MainPage sealed { private: tictactoe_game game;
// ...
}; } </pre>
這里有3類基本的事件處理handler需要我們自己實現:
void TicTacToeWinRT::MainPage::btnStartUser_Click(Object^ sender, RoutedEventArgs^ e) { ResetGame();game.start(tictactoe_player::user); }
void TicTacToeWinRT::MainPage::btnStartComputer_Click(Object^ sender, RoutedEventArgs^ e) { ResetGame();
game.start(tictactoe_player::computer); auto cell = game.move(tictactoe_player::computer);
PlaceMark(cell, tictactoe_player::computer); }</pre>
PlaceMark()方法創建了一個newImagecontrol控件,設定它的Source是tttx.bmp或者ttt0.bmp,并且把它添加到所走的那一步的面板網格上。
void TicTacToeWinRT::MainPage::PlaceMark(tictactoe_cell const cell, tictactoe_player const player) { auto image = ref new Image(); auto bitmap = ref new BitmapImage( ref new Uri(player == tictactoe_player::computer ? "ms-appx:///Assets/tttx.bmp" : "ms-appx:///Assets/ttt0.bmp")); bitmap->ImageOpened += ref new RoutedEventHandler( this, image, bitmap, cell { image->Width = bitmap->PixelWidth; image->Height = bitmap->PixelHeight; image->Visibility = Windows::UI::Xaml::Visibility::Visible; });image->Source = bitmap;
image->Visibility = Windows::UI::Xaml::Visibility::Collapsed; image->HorizontalAlignment = Windows::UI::Xaml::HorizontalAlignment::Center; image->VerticalAlignment = Windows::UI::Xaml::VerticalAlignment::Center;
Grid::SetRow(image, cell.row); Grid::SetColumn(image, cell.col);
board->Children->Append(image); }</pre>
當開始一場新游戲時,這些在游戲過程中被添加到網格上的Imagecontrol控件需要被移除掉。這正是theResetGame()method方法所做的事情。此外,它還移除了游戲勝利時顯示的紅線和顯示游戲結果的文字。
void TicTacToeWinRT::MainPage::ResetGame() { std::vector<?xml:namespace prefix = windows />windows::ui::xaml::uielement^ children;for(auto const & child : board->Children) { auto typeName = child->GetType()->FullName; if(typeName == "Windows.UI.Xaml.Controls.Image" || typeName == "Windows.UI.Xaml.Shapes.Line") { children.push_back(child); } }
for(auto const & child : children) { unsigned int index; if(board->Children->IndexOf(child, &index)) { board->Children->RemoveAt(index); } }
txtStatus->Text = nullptr; }/windows::ui::xaml::uielement^</pre>當玩家在一個單元格上點擊了一下指針,并且這個單元格是沒有被占據的,那我們就讓他走這一步。如果這時游戲還沒有結束,那我們也讓電腦走一步。當游戲在玩家或者電腦走過一步之后結束,我們會在一個text box中顯示結果并且如果有一方勝利,會在勝利的行,列或對角上劃上紅線。
void TicTacToeWinRT::MainPage::board_PointerReleased(Platform::Object^ sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs^ e) { if(game.is_started() && ! game.is_finished()) { auto cellw = board->ActualWidth / 3; auto cellh = board->ActualHeight / 3;auto point = e->GetCurrentPoint(board); auto row = static_cast<int>(point->Position.Y / cellh); auto col = static_cast<int>(point->Position.X / cellw); game.move(tictactoe_cell(row, col), tictactoe_player::user); PlaceMark(tictactoe_cell(row, col), tictactoe_player::user); if(!game.is_finished()) { auto cell = game.move(tictactoe_player::computer); PlaceMark(cell, tictactoe_player::computer); if(game.is_finished()) { DisplayResult( game.is_victory(tictactoe_player::computer) ? tictactoe_player::computer : tictactoe_player::none); } } else { DisplayResult( game.is_victory(tictactoe_player::user) ? tictactoe_player::user : tictactoe_player::none); }
} }
void TicTacToeWinRT::MainPage::DisplayResult(tictactoe_player const player) { Platform::String^ text = nullptr; switch (player) { case tictactoe_player::none: text = "It's a draw!"; break; case tictactoe_player::computer: text = "Computer wins!"; break; case tictactoe_player::user: text = "User wins!"; break; }
txtStatus->Text = text;
if(player != tictactoe_player::none) { auto coordinates = game.get_winning_line(); if(coordinates.first.is_valid() && coordinates.second.is_valid()) { PlaceCut(coordinates.first, coordinates.second); } } }
void TicTacToeWinRT::MainPage::PlaceCut(tictactoe_cell const start, tictactoe_cell const end) { auto cellw = board->ActualWidth / 3; auto cellh = board->ActualHeight / 3;
auto line = ref new Line(); line->X1 = start.col cellw + cellw / 2; line->Y1 = start.row cellh + cellh / 2;
line->X2 = end.col cellw + cellw / 2; line->Y2 = end.row cellh + cellh / 2;
line->StrokeStartLineCap = Windows::UI::Xaml::Media::PenLineCap::Round; line->StrokeEndLineCap = Windows::UI::Xaml::Media::PenLineCap::Round; line->StrokeThickness = 15; line->Stroke = ref new SolidColorBrush(Windows::UI::Colors::Red);
line->Visibility = Windows::UI::Xaml::Visibility::Visible;
Grid::SetRow(line, 0); Grid::SetColumn(line, 0); Grid::SetRowSpan(line, 3); Grid::SetColumnSpan(line, 3);
board->Children->Append(line); }</int></int></pre>到這里就全部結束了,你可以build它了,啟動它然后玩吧。它看起來像這樣:
結語
通過這篇文章我們能看到如何用C++語言實現一個簡單的游戲,并且利用不同的技術在不同的用戶界面上實現它。游戲的邏輯部分我們只用標準C++寫了一次,并且使用了兩種截然不同的技術用它來構建了2個應用:Win32 API,我們不得不明確的指定一些工作例如創建一個窗口并且繪制它,還有就是使用XAML的Windows Runtime,這里framework為我們做了大量的工作,是我們能夠專注于游戲邏輯的實現(并且當我們需要去設計UI的時候,我們不僅僅只有手動聲明這一種方法,不僅可以在XAML中,也可以在code behind里)。其中我們也看到了如何使用標準的容器例如std::array和std::set,并且無縫的在C++/CX中使用純C++邏輯代碼創建一個Windows Runtime應用。