日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
C++很有趣:編寫一個(gè)井字游戲(TicTacToe)

這個(gè)有趣的C++系列打算展示一下使用C++寫代碼可以和其他主流語言一樣高效而有趣。在第二部分,我將向你展示使用C++從無到有的創(chuàng)建一個(gè)井字游戲。這篇文章,以及整個(gè)系列都是針對那些想學(xué)習(xí)C++或者對這個(gè)語言性能好奇的開發(fā)者。

雁江ssl適用于網(wǎng)站、小程序/APP、API接口等需要進(jìn)行數(shù)據(jù)傳輸應(yīng)用場景,ssl證書未來市場廣闊!成為成都創(chuàng)新互聯(lián)的ssl證書銷售渠道,可以享受市場價(jià)格4-6折優(yōu)惠!如果有意向歡迎電話聯(lián)系或者加微信:028-86922220(備注:SSL證書合作)期待與您的合作!

許多年輕人想學(xué)習(xí)編程來寫游戲。C++是用的最多的用來寫游戲的語言,盡管在寫出下個(gè)憤怒的小鳥之前,需要學(xué)會(huì)很多的編程經(jīng)驗(yàn)。一個(gè)井子游戲是開始學(xué)習(xí)的 一個(gè)好選擇,事實(shí)上,在許多年前我開始學(xué)習(xí)C++后,他是我寫的地一個(gè)游戲。我希望這篇文章可以幫助到那些還不熟悉C++的初學(xué)者和有經(jīng)驗(yàn)的開發(fā)者。

我使用的是Visual Studio 2012來寫這篇文章的源代碼。

游戲介紹

如果你沒有玩過井字游戲或者并不熟悉這個(gè)游戲,下面是來自維基百科的描述.

  井字游戲 (或者"圈圈和叉叉",Xs and Os) 是一個(gè)兩人的紙筆游戲,兩個(gè)人輪流在3X3的網(wǎng)格內(nèi)畫圈和叉. 當(dāng)一名玩家放置的標(biāo)志在水平,垂直或者對角線上成一條線即獲得勝利.

這個(gè)游戲也可以人機(jī)對戰(zhàn),先手不固定.

創(chuàng)建這個(gè)程序的時(shí)候有2個(gè)關(guān)鍵的東西:程序的邏輯和程序的UI界面. 有許多在windows中創(chuàng)建用戶UI的方法, 包括 Win32 API, MFC, ATL, GDI+, DirectX, etc. 在這篇文章中,我將展示使用多種技術(shù)來實(shí)現(xiàn)同一個(gè)程序邏輯. 我們將新建2個(gè)應(yīng)用, 一個(gè)使用 Win32 API 另一個(gè)使用 C++/CX.

游戲邏輯

如果一個(gè)玩家在網(wǎng)格上放下一個(gè)標(biāo)記時(shí),遵循幾個(gè)簡單的規(guī)則,那他就可以玩一個(gè)完美的游戲(意味著贏或者平局)。在Wikipedia上寫有這些規(guī)則,在里面你也可以找到先手玩家的最優(yōu)策略。

在xkcd drawing上 有先手和后手玩家的最優(yōu)策略。盡管有幾個(gè)錯(cuò)誤(在幾種情況下沒有走必勝的步驟,至少在一個(gè)情況下丟失了一個(gè)X標(biāo)記),我將使用這個(gè)版本作為游戲策略(修復(fù) 了那些我能找到的錯(cuò)誤)。記住電腦總是玩一個(gè)完美的游戲。如果你實(shí)現(xiàn)了這樣一個(gè)游戲,你可能也想讓用戶贏,這種情況下你需要一個(gè)不同的方法。當(dāng)對本文的目 的,這個(gè)策略應(yīng)該足夠了。

提出的第一個(gè)問題是在C++程序中用什么數(shù)據(jù)結(jié)構(gòu)來表示圖像的模型。這可以有不同的選擇,比如樹、圖、數(shù)組或者位字段(如果真有人對內(nèi)存消耗很在意)。網(wǎng) 格有9個(gè)單元,我選擇的最簡單的使用對每個(gè)單元使用一個(gè)包含9個(gè)整數(shù)的數(shù)組:0表示空的單元,1表示單元被標(biāo)記為X,2表示單元被標(biāo)記為O。讓我們看下圖 以及它將被如何編碼。

這幅圖可以這么理解:

  • 在單元(0,0)放X。網(wǎng)格可以編碼為:1,0,0,0,0,0,0,0,0
  • 如果對手在單元(0,1)放置O,那么在單元(1,1)放置X?,F(xiàn)在網(wǎng)格編碼為:1,2,0,0,1,0,0,0,0
  • 如果對手在單元(0,2)放置O,那么在單元(2,2)放置X。現(xiàn)在網(wǎng)格編碼為:1,2,2,0,1,0,0,0,1
  • 如果對手在單元(2,2)放置O,那么在單元(2,0)放置X?,F(xiàn)在網(wǎng)格編碼為:1,2,0,0,1,0,1,0,2。這時(shí),無論對手怎么做,X都將贏得比賽。
  • 如果對手在單元(0,2)放置O,那么在單元(1,0)放置X?,F(xiàn)在網(wǎng)格編碼為:1,2,2,1,1,0,1,0,2。這表示的是一個(gè)贏得比賽的一步。

記住這個(gè)我們就可以開始在程序中對其編碼了。我們將使用一個(gè)std::array來表示一個(gè)9格板。這是個(gè)固定大小的容器,在編譯時(shí)就已知的大小,在連續(xù)的內(nèi)存區(qū)域存儲(chǔ)元素。為了避免一遍又一遍的使用相同數(shù)組類型,我將定義一個(gè)別名來簡化。

 
 
 
 
  1. #include 
  2. typedef std::array tictactoe_status;

上面描述的最優(yōu)策略用這樣的數(shù)組隊(duì)列(另一個(gè)數(shù)組)來表示。

 
 
 
 
  1. tictactoe_status const strategy_x[] = 
  2. {
  3.    {1,0,0,0,0,0,0,0,0},
  4.    {1,2,0,0,1,0,0,0,0},
  5.    {1,2,2,0,1,0,0,0,1},
  6.    {1,2,0,2,1,0,0,0,1},
  7.    // ...
  8. };
  9. tictactoe_status const strategy_o[] = 
  10. {
  11.    {2,0,0,0,1,0,0,0,0},
  12.    {2,2,1,0,1,0,0,0,0},
  13.    {2,2,1,2,1,0,1,0,0},
  14.    {2,2,1,0,1,2,1,0,0},
  15.    // ...
  16. };

#p#

strategy_x是先手玩家的最優(yōu)策略,strategy_o是后手玩家的最優(yōu)策略。如果你看了文中的源代碼,你將注意到這兩個(gè)數(shù)組的真實(shí)定義和我前面展示的不同。

 
 
 
 
  1. tictactoe_status const strategy_x[] = 
  2. {
  3. #include "strategy_x.h"
  4. };
  5. tictactoe_status const strategy_o[] = 
  6. {
  7. #include "strategy_o.h"
  8. };

這是個(gè)小技巧,我的理由是,它允許我們把真實(shí)的很長的數(shù)組內(nèi)容放在分開的文件中(這些文件的擴(kuò)展性不重要,它可以不僅僅是C++頭文件,也可以是其他任何 文件),保證源碼文件和定義簡單干凈。strategy_x.h和strategy_o.h文件在編譯的預(yù)處理階段就被插入到源碼文件中,如同正常的頭文 件一樣。下面是strategy_x.h文件的片斷。

 
 
 
 
  1. // http://imgs.xkcd.com/comics/tic_tac_toe_large.png
  2. // similar version on http://upload.wikimedia.org/wikipedia/commons/d/de/Tictactoe-X.svg
  3. // 1 = X, 2 = O, 0 = unoccupied
  4. 1,0,0,0,0,0,0,0,0,
  5. 1,2,0,0,1,0,0,0,0,
  6. 1,2,2,0,1,0,0,0,1,
  7. 1,2,0,2,1,0,0,0,1,
  8. 1,2,0,0,1,2,0,0,1,

你應(yīng)該注意到,如果你使用支持C++11的編譯器,你可以使用一個(gè)std::vector而不是C類型的數(shù)組。Visual Studio 2012不支持這么做,但在Visual Studio 2013中支持。

 
 
 
 
  1. std::vector strategy_o = 
  2. {
  3.    {2, 0, 0, 0, 1, 0, 0, 0, 0},
  4.    {2, 2, 1, 0, 1, 0, 0, 0, 0},
  5.    {2, 2, 1, 2, 1, 0, 1, 0, 0},
  6.    {2, 2, 1, 0, 1, 2, 1, 0, 0},
  7.    {2, 2, 1, 1, 1, 0, 2, 0, 0},
  8. };

為了定義這些數(shù)字表示的對應(yīng)玩家,我定義了一個(gè)叫做tictactoe_player的枚舉類型變量。

 
 
 
 
  1. enum class tictactoe_player : char
  2. {
  3.    none = 0,
  4.    computer = 1,
  5.    user = 2,
  6. };

游戲的邏輯部分將會(huì)在被稱之為tictactoe_game 的類中實(shí)現(xiàn)。最基本的,這個(gè) class 應(yīng)該有下面的狀態(tài):

  • 一個(gè)布爾值用來表示游戲是否開始了,命名為 started 。
  • 游戲的當(dāng)前狀態(tài)(網(wǎng)格上的標(biāo)記), 命名為 status 。
  • 根據(jù)當(dāng)前的狀態(tài)得到的之后可以進(jìn)行的下法的集合,命名為strateg
 
 
 
 
  1. class tictactoe_game
  2. {
  3.    bool started;
  4.    tictactoe_status status;
  5.    std::set strategy;
  6.    
  7.    // ...
  8. };

在游戲的過程中,我們需要知道游戲是否開始了、結(jié)束了,如果結(jié)束了,需要判定是否有哪個(gè)玩家贏了或者最終兩個(gè)人打平。為此,tictactoe_game類提供了三個(gè)方法:

  • is_started()來表示游戲是否開始了
  • is_victory()來檢查是否有哪位玩家在游戲中獲勝
  • is_finished()來檢查游戲是否結(jié)束。當(dāng)其中某位玩家在游戲中獲勝或者當(dāng)網(wǎng)格被填滿玩家不能再下任何的棋子的時(shí)候,游戲結(jié)束。
 
 
 
 
  1. bool is_started() const {return started;}
  2. bool is_victory(tictactoe_player const player) const {return is_winning(status, player);}
  3. bool is_finished() const 
  4. {

 對于方法is_victory()和is_finished(),實(shí)際上是依賴于兩個(gè)私有的方法,is_full(), 用來表示網(wǎng)格是否被填滿并且不能再放下任何的棋子,以及方法is_winning, 用來表示在該網(wǎng)格上是否有某玩家勝出。它們的實(shí)現(xiàn)將會(huì)很容易被讀懂。is_full 通過計(jì)算網(wǎng)格中空的(在表示網(wǎng)格的數(shù)組中值為0)格子的數(shù)量,如果沒有這樣的格子那么將返回true。is_winning將會(huì)檢查這些連線,網(wǎng)格的行、列、以及對角線,依此來查看是否有哪位玩家已經(jīng)獲勝。

 
 
 
 
  1. bool is_winning(tictactoe_status const & status, tictactoe_player const player) const
  2. {
  3.    auto mark = static_cast(player);
  4.    return 
  5.       (status[0] == mark && status[1] == mark && status[2] == mark) ||
  6.       (status[3] == mark && status[4] == mark && status[5] == mark) ||
  7.       (status[6] == mark && status[7] == mark && status[8] == mark) ||
  8.       (status[0] == mark && status[4] == mark && status[8] == mark) ||
  9.       (status[2] == mark && status[4] == mark && status[6] == mark) ||
  10.       (status[0] == mark && status[3] == mark && status[6] == mark) ||
  11.       (status[1] == mark && status[4] == mark && status[7] == mark) ||
  12.       (status[2] == mark && status[5] == mark && status[8] == mark);
  13. }
  14. bool is_full(tictactoe_status const & status) const 
  15. {
  16.    return 0 == std::count_if(std::begin(status), std::end(status), [](int const mark){return mark == 0;});
  17. }

當(dāng)一個(gè)玩家獲勝的時(shí)候,我們想給他所連成的線(行、列、或者對角線)上畫一條醒目的線段。因此首先我們得知道那條線使得玩家獲勝。我們使用了方法get_winning_line()來返回一對 tictactoe_cell,用來表示線段的兩端。它的實(shí)現(xiàn)和is_winning很相似,它檢查行、列和對角線上的狀態(tài)。它可能會(huì)看起來有點(diǎn)冗長,但是我相信這個(gè)方法比使用循環(huán)來遍歷行、列、對角線更加簡單。

 
 
 
 
  1. struct tictactoe_cell
  2. {
  3.    int row;
  4.    int col;
  5.    tictactoe_cell(int r = INT_MAX, int c = INT_MAX):row(r), col(c)
  6.    {}
  7.    bool is_valid() const {return row != INT_MAX && col != INT_MAX;}
  8. };
  9. std::pair const get_winning_line() const
  10. {
  11.    auto mark = static_cast(tictactoe_player::none);
  12.    if(is_victory(tictactoe_player::computer))
  13.       mark = static_cast(tictactoe_player::computer);
  14.    else if(is_victory(tictactoe_player::user))
  15.       mark = static_cast(tictactoe_player::user);
  16.    if(mark != 0)
  17.    {
  18.       if(status[0] == mark && status[1] == mark && status[2] == mark) 
  19.          return std::make_pair(tictactoe_cell(0,0), tictactoe_cell(0,2));
  20.       if(status[3] == mark && status[4] == mark && status[5] == mark)
  21.          return std::make_pair(tictactoe_cell(1,0), tictactoe_cell(1,2));
  22.       if(status[6] == mark && status[7] == mark && status[8] == mark)
  23.          return std::make_pair(tictactoe_cell(2,0), tictactoe_cell(2,2));
  24.       if(status[0] == mark && status[4] == mark && status[8] == mark)
  25.          return std::make_pair(tictactoe_cell(0,0), tictactoe_cell(2,2));
  26.       if(status[2] == mark && status[4] == mark && status[6] == mark)
  27.          return std::make_pair(tictactoe_cell(0,2), tictactoe_cell(2,0));
  28.       if(status[0] == mark && status[3] == mark && status[6] == mark)
  29.          return std::make_pair(tictactoe_cell(0,0), tictactoe_cell(2,0));
  30.       if(status[1] == mark && status[4] == mark && status[7] == mark)
  31.          return std::make_pair(tictactoe_cell(0,1), tictactoe_cell(2,1));
  32.       if(status[2] == mark && status[5] == mark && status[8] == mark)
  33.          return std::make_pair(tictactoe_cell(0,2), tictactoe_cell(2,2));
  34.    }
  35.    return std::make_pair(tictactoe_cell(), tictactoe_cell());
  36. }

#p#

現(xiàn)在我們只剩下添加開始游戲功能和為網(wǎng)格放上棋子功能(電腦和玩家兩者).

對于開始游戲,我們需要知道,由誰開始下第一個(gè)棋子,因此我們可以采取比較合適的策略(兩種方式都需要提供,電腦先手或者玩家先手都要被支持)。同時(shí),我 們也需要重置表示網(wǎng)格的數(shù)組。方法start()對開始新游戲進(jìn)行初始化??梢韵碌钠宓牟呗缘募媳辉僖淮蔚某跏蓟? 從stategy_x 或者strategy_o進(jìn)行拷貝。從下面的代碼可以注意到,strategy是一個(gè)std::set, 并且strategy_x或者strategy_o都是有重復(fù)單元的數(shù)組,因?yàn)樵趖ictoctoe表里面的一些位置是重復(fù)的。這個(gè)std::set 是一個(gè)只包含唯一值的容器并且它保證了唯一的可能的位置(例如對于strategy_o來說,有一半是重復(fù)的)。 中的std::copy算法在這里被用來進(jìn)行數(shù)據(jù)單元的拷貝,將當(dāng)前的內(nèi)容拷貝到std::set中,并且使用方法assign()來將 std::array的所有的元素重置為0。

 
 
 
 
  1. void start(tictactoe_player const player)
  2. {
  3.    strategy.clear();
  4.    if(player == tictactoe_player::computer)
  5.       std::copy(std::begin(strategy_x), std::end(strategy_x), 
  6.                 std::inserter(strategy, std::begin(strategy)));
  7.    else if(player == tictactoe_player::user)
  8.       std::copy(std::begin(strategy_o), std::end(strategy_o), 
  9.                 std::inserter(strategy, std::begin(strategy)));
  10.                 
  11.    status.assign(0);
  12.    
  13.    started = true;
  14. }

當(dāng)玩家走一步時(shí),我們需要做的是確保選擇的網(wǎng)格是空的,并放置合適的標(biāo)記。move()方法的接收參數(shù)是網(wǎng)格的坐標(biāo)、玩家的記號,如果這一步有效時(shí)返回真,否則返回假。

 
 
 
 
  1. bool move(tictactoe_cell const cell, tictactoe_player const player)
  2. {
  3.    if(status[cell.row*3 + cell.col] == 0)
  4.    {
  5.       status[cell.row*3 + cell.col] = static_cast(player);
  6.       
  7.       if(is_victory(player))
  8.       {
  9.          started = false;
  10.       }
  11.       
  12.       return true;
  13.    }
  14.    return false;
  15. }

電腦走一步時(shí)需要更多的工作,因?yàn)槲覀冃枰业诫娔X應(yīng)該走的最好的下一步。重載的move()方法在可能的步驟(策略)集合中查詢,然后從中選擇最佳的一步。在走完這步后,會(huì)檢查電腦是否贏得這場游戲,如果是的話標(biāo)記游戲結(jié)束。這個(gè)方法返回電腦走下一步的位置。

 
 
 
 
  1. tictactoe_cell move(tictactoe_player const player)
  2. {
  3.    tictactoe_cell cell;
  4.    strategy = lookup_strategy();
  5.    if(!strategy.empty())
  6.    {
  7.       auto newstatus = lookup_move();
  8.       for(int i = 0; i < 9; ++i)
  9.       {
  10.          if(status[i] == 0 && newstatus[i]==static_cast(player))
  11.          {
  12.             cell.row = i/3;
  13.             cell.col = i%3;
  14.             break;
  15.          }
  16.       }
  17.       status = newstatus;
  18.       if(is_victory(player))
  19.       {
  20.          started = false;
  21.       }
  22.    }
  23.    return cell;
  24. }

lookup_strategy()方法在當(dāng)前可能的移動(dòng)位置中迭代,來找到從當(dāng)前位置往哪里移動(dòng)是可行的。它利用了這樣的一種事實(shí),空的網(wǎng)格以0來表 示,任何已經(jīng)填過的網(wǎng)格,不是用1就是用2表示,而這兩個(gè)值都大于0。一個(gè)網(wǎng)格的值只能從0變?yōu)?或者2。不可能從1變?yōu)?或從2變?yōu)?。

當(dāng)游戲開始時(shí)的網(wǎng)格編碼為0,0,0,0,0,0,0,0,0來表示并且當(dāng)前情況下任何的走法都是可能的。這也是為什么我們要在thestart()方法 里把整個(gè)步數(shù)都拷貝出來的原因。一旦玩家走了一步,那能走的步數(shù)的set便會(huì)減少。舉個(gè)例子,玩家在第一個(gè)格子里走了一步。此時(shí)網(wǎng)格編碼為 1,0,0,0,0,0,0,0,0。這時(shí)在數(shù)組的第一個(gè)位置不可能再有0或者2的走法因此需要被過濾掉。

 
 
 
 
  1. std::set tictactoe_game::lookup_strategy() const
  2. {
  3.    std::set nextsubstrategy;
  4.    for(auto const & s : strategy)
  5.    {
  6.       bool match = true;
  7.       for(int i = 0; i < 9 && match; ++i)
  8.       {
  9.          if(s[i] < status[i])
  10.             match = false;
  11.       }
  12.       if(match)
  13.       {
  14.          nextsubstrategy.insert(s);
  15.       }
  16.    }
  17.    return nextsubstrategy;
  18. }

在選擇下一步時(shí)我們需要確保我們選擇的走法必須與當(dāng)前的標(biāo)記不同,如果當(dāng)前的狀態(tài)是1,2,0,0,0,0,0,0,0而我們現(xiàn)在要為玩家1選擇走法那么 我們可以從余下的7個(gè)數(shù)組單元中選擇一個(gè),可以是: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。然而我們需要選擇最優(yōu)的走法而不是僅僅只隨便走一步,通常最優(yōu)的走法也是贏得比賽的關(guān)鍵。因此我們需要找一步能使 我們走向勝利,如果沒有這樣的一步,那就隨便走吧。

 
 
 
 
  1. tictactoe_status tictactoe_game::lookup_move() const
  2. {
  3.    tictactoe_status newbest = {0};
  4.    for(auto const & s : strategy)
  5.    {
  6.       int diff = 0;
  7.       for(int i = 0; i < 9; ++i)
  8.       {
  9.          if(s[i] > status[i])
  10.             diff++;
  11.       }
  12.       if(diff == 1)
  13.       {
  14.          newbest = s;
  15.          if(is_winning(newbest, tictactoe_player::computer))
  16.          {
  17.             break;
  18.          }
  19.       }
  20.    }
  21.    assert(newbest != empty_board);
  22.    return newbest;
  23. }

做完了這一步,我們的游戲的邏輯部分就完成了。更多細(xì)節(jié)請閱讀game.hgame.cpp中的代碼

#p#

一個(gè)用Win32 API實(shí)現(xiàn)的游戲

我將用Win32 API做用戶界面來創(chuàng)建第一個(gè)應(yīng)用程序。如果你不是很熟悉Win32 編程那么現(xiàn)在已經(jīng)有大量的資源你可以利用學(xué)習(xí)。為了使大家理解我們?nèi)绾蝿?chuàng)建一個(gè)最終的應(yīng)用,我將只講述一些必要的方面。另外,我不會(huì)把每一行代碼都展現(xiàn)并 解釋給大家,但是你可以通過下載這些代碼來閱讀瀏覽它。

一個(gè)最基本的Win32應(yīng)用需要的一些內(nèi)容:

  • 一個(gè)入口點(diǎn),通常來說是WinMain,而不是main。它需要一些參數(shù)例如當(dāng)前應(yīng)用實(shí)例的句柄,命令行和指示窗口如何展示的標(biāo)志。
  • 一個(gè)窗口類,代表了創(chuàng)建一個(gè)窗口的模板。一個(gè)窗口類包含了一個(gè)為系統(tǒng)所用的屬性集合,例如類名,class style(不同于窗口的風(fēng)格),圖標(biāo),菜單,背景刷,窗口的指針等。一個(gè)窗口類是進(jìn)程專用的并且必須要注冊到系統(tǒng)優(yōu)先級中來創(chuàng)建一個(gè)窗口。使用RegisterClassEx來注冊一個(gè)窗口類。
  • 一個(gè)主窗口,基于一個(gè)窗口類來創(chuàng)建。使用CreateWindowEx可以創(chuàng)建一個(gè)窗口。
  • 一個(gè)窗口過程函數(shù),它是一個(gè)處理所有基于窗口類創(chuàng)建的窗口的消息的方法。一個(gè)窗口過程函數(shù)與窗口相聯(lián),但是它不是窗口。
  • 一個(gè)消息循環(huán)。一個(gè)窗口通過兩種方式來接受消息:通過SendMessage,直接調(diào)用窗口過程函數(shù)直到窗口過程函數(shù)處理完消息之后才返回,或者通過PostMessage (或 PostThreadMessage)把一個(gè)消息投送到創(chuàng)建窗口的線程的消息隊(duì)列中并且不用等待線程處理直接返回。因此線程必須一直運(yùn)行一個(gè)從消息隊(duì)列接收消息和把消息發(fā)送給窗口過程函數(shù)的循環(huán)

你可以在 MSDN 中找到關(guān)于Win 32 應(yīng)用程序如何注冊窗口類、創(chuàng)建一個(gè)窗口、運(yùn)行消息循環(huán)的例子。一個(gè)Win32的應(yīng)用程序看起來是這樣的:

 
 
 
 
  1. int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
  2. {
  3.    WNDCLASS wc; 
  4.    // set the window class attributes
  5.    // including pointer to a window procedure
  6.    
  7.    if (!::RegisterClass(&wc))
  8.       return FALSE;
  9.       
  10.    HWND wnd = ::CreateWindowEx(...);
  11.    if(!wnd)
  12.       return FALSE;
  13.       
  14.    ::ShowWindow(wnd, nCmdShow); 
  15.    
  16.    MSG msg;
  17.    while(::GetMessage(&msg, nullptr, 0, 0))
  18.    {
  19.       ::TranslateMessage(&msg);
  20.       ::DispatchMessage(&msg);
  21.    }
  22.    return msg.wParam;   
  23. }

當(dāng)然,這還不夠,我們還需要一個(gè)窗口過程函數(shù)來處理發(fā)送給窗口的消息,比如PAINT消息,DESTORY 消息,菜單消息和其它的一些必要的消息。一個(gè)窗口過程函數(shù)看起來是這樣的:

 
 
 
 
  1. LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
  2. {
  3.    switch(message)
  4.    {
  5.    case WM_PAINT:
  6.       {
  7.          PAINTSTRUCT ps;
  8.          HDC dc = ::BeginPaint(hWnd, &ps);
  9.          // paint
  10.          ::EndPaint(hWnd, &ps);
  11.       }
  12.       break;
  13.    case WM_DESTROY:
  14.       ::PostQuitMessage(0);
  15.       return 0;
  16.    case WM_COMMAND:
  17.       {
  18.          ...
  19.       }
  20.       break;
  21.    }
  22.    return ::DefWindowProc(hWnd, message, wParam, lParam);
  23. }

我更喜歡寫面向?qū)ο蟮拇a,不喜歡面向過程,所以我用幾個(gè)類封裝了窗口類、窗口和設(shè)備描述表。你可以在附件的代碼framework.h framework.cpp  找到這些類的實(shí)現(xiàn)(它們非常小巧 )。

  • WindowClass類是對窗口類相關(guān)資源初始化的封裝,在構(gòu)造函數(shù)中,它初始化了WNDCLASSEX 的結(jié)構(gòu)并且調(diào)用 RegisterClassEx 方法。在析構(gòu)函數(shù)中,它通過調(diào)用 UnregisterClass 移除窗口的注冊。
  • Window類是通過對HWND封裝一些諸如Create,ShowWindow 和Invalidate的函數(shù)(它們的名字已經(jīng)告訴了你他們是做什么的)。它還有幾個(gè)虛成員代表消息句柄,它們會(huì)被窗口過程調(diào) 用 (OnPaint,OnMenuItemClicked,OnLeftButtonDown) 。這個(gè)window類將會(huì)被繼承來并提供具體的實(shí)現(xiàn)。
  • DeviceContex類是對設(shè)備描述表(HDC)的封裝。在構(gòu)造函數(shù)中它調(diào)用 BeginPaint 函數(shù)并且在析構(gòu)函數(shù)中調(diào)用  EndPaint 函數(shù)。

這個(gè)游戲的主要窗口是TicTacToeWindow類,它是從Window類繼承而來,它重載了虛擬方法來處理消息,該類的聲明是這樣的:

 
 
 
 
  1. class TicTacToeWindow : public Window
  2. {
  3.    HANDLE hBmp0;
  4.    HANDLE hBmpX;
  5.    BITMAP bmp0;
  6.    BITMAP bmpX;
  7.    tictactoe_game game;
  8.    void DrawBackground(HDC dc, RECT rc);
  9.    void DrawGrid(HDC dc, RECT rc);
  10.    void DrawMarks(HDC dc, RECT rc);
  11.    void DrawCut(HDC dc, RECT rc);
  12.    virtual void OnPaint(DeviceContext* dc) override;
  13.    virtual void OnLeftButtonUp(int x, int y, WPARAM params) override;
  14.    virtual void OnMenuItemClicked(int menuId) override;
  15. public:
  16.    TicTacToeWindow();
  17.    virtual ~TicTacToeWindow() override;
  18. };

#p#

MethodOnPaint()函數(shù)用來繪制窗口,它用來繪制窗口背景,網(wǎng)格線,填充的單元格(如果有的話),如果在游戲結(jié)束,玩家贏了,一條紅線在獲勝 行,列或?qū)蔷€的 標(biāo)記。為了避免閃爍,我們使用了雙緩沖技術(shù):創(chuàng)建一個(gè)內(nèi)存設(shè)備文本(通過調(diào)用toBeginPaint函數(shù)準(zhǔn)備窗口的設(shè)備文本來匹配),一個(gè)內(nèi)存中的位圖 匹配內(nèi)存設(shè)備文本,繪制該位圖,然后用窗口設(shè)備文本來復(fù)制內(nèi)存設(shè)備文本。

 
 
 
 
  1. void TicTacToeWindow::OnPaint(DeviceContext* dc)
  2. {
  3.    RECT rcClient;
  4.    ::GetClientRect(hWnd, &rcClient);
  5.    auto memdc = ::CreateCompatibleDC(*dc);
  6.    auto membmp = ::CreateCompatibleBitmap(*dc, rcClient.right - rcClient.left, rcClient.bottom-rcClient.top);
  7.    auto bmpOld = ::SelectObject(memdc, membmp);
  8.    
  9.    DrawBackground(memdc, rcClient);
  10.    DrawGrid(memdc, rcClient);
  11.    DrawMarks(memdc, rcClient);
  12.    DrawCut(memdc, rcClient);
  13.    ::BitBlt(*dc, 
  14.       rcClient.left, 
  15.       rcClient.top, 
  16.       rcClient.right - rcClient.left, 
  17.       rcClient.bottom-rcClient.top,
  18.       memdc, 
  19.       0, 
  20.       0, 
  21.       SRCCOPY);
  22.    ::SelectObject(memdc, bmpOld);
  23.    ::DeleteObject(membmp);
  24.    ::DeleteDC(memdc);
  25. }

我不會(huì)在這里列出DrawBackground,DrawGridand和 DrawMarksfunctions的內(nèi)容。他們不是很復(fù)雜,你可以閱讀源代碼。DrawMarksfunction使用兩個(gè)位圖,ttt0.bmp和tttx.bmp,繪制網(wǎng)格的痕跡。

我將只顯示如何在獲勝行,列或?qū)蔷€繪制紅線。首先,我們要檢查游戲是否結(jié)束,如果結(jié)束那么檢索獲勝線。如果兩端都有效,然后計(jì)算該兩個(gè)小區(qū)的中心,創(chuàng)建和選擇一個(gè)畫筆(實(shí)心,15像素寬的紅色線)并且繪制兩個(gè)小區(qū)的中間之間的線。

 
 
 
 
  1. void TicTacToeWindow::DrawCut(HDC dc, RECT rc)
  2. {
  3.    if(game.is_finished())
  4.    {
  5.       auto streak = game.get_winning_line();
  6.       if(streak.first.is_valid() && streak.second.is_valid())
  7.       {
  8.          int cellw = (rc.right - rc.left) / 3;
  9.          int cellh = (rc.bottom - rc.top) / 3;
  10.          auto penLine = ::CreatePen(PS_SOLID, 15, COLORREF(0x2222ff));
  11.          auto penOld = ::SelectObject(dc, static_cast(penLine));
  12.          ::MoveToEx(
  13.             dc, 
  14.             rc.left + streak.first.col * cellw + cellw/2, 
  15.             rc.top + streak.first.row * cellh + cellh/2,
  16.             nullptr);
  17.          ::LineTo(dc,
  18.             rc.left + streak.second.col * cellw + cellw/2,
  19.             rc.top + streak.second.row * cellh + cellh/2);
  20.          ::SelectObject(dc, penOld);
  21.       }
  22.    }
  23. }

主窗口有三個(gè)項(xiàng)目菜單, ID_GAME_STARTUSER在用戶先移動(dòng)時(shí)啟動(dòng)一個(gè)游戲, ID_GAME_STARTCOMPUTER在當(dāng)電腦先移動(dòng)時(shí)啟動(dòng)一個(gè)游戲, ID_GAME_EXIT用來關(guān)閉應(yīng)用。當(dāng)用戶點(diǎn)擊兩個(gè)啟動(dòng)中的任何一個(gè),我們就必須開始一個(gè)游戲任務(wù)。如果電腦先移動(dòng),那么我們應(yīng)該是否移動(dòng),并且,在所有情況中,都要重新繪制窗口。

 
 
 
 
  1. void TicTacToeWindow::OnMenuItemClicked(int menuId)
  2. {
  3.    switch(menuId)
  4.    {
  5.    case ID_GAME_EXIT:
  6.       ::PostMessage(hWnd, WM_CLOSE, 0, 0);
  7.       break;
  8.    case ID_GAME_STARTUSER:
  9.       game.start(tictactoe_player::user);
  10.       Invalidate(FALSE);
  11.       break;
  12.    case ID_GAME_STARTCOMPUTER:
  13.       game.start(tictactoe_player::computer);
  14.       game.move(tictactoe_player::computer);
  15.       Invalidate(FALSE);
  16.       break;
  17.    }
  18. }

#p#

現(xiàn)在只剩下一件事了,就是留意在我們的窗口中處理用戶單擊鼠標(biāo)的行為。當(dāng)用戶在我們的窗口客戶區(qū)內(nèi)按下鼠標(biāo)時(shí),我們要去檢查是鼠標(biāo)按下的地方是在哪一個(gè)網(wǎng)格內(nèi),如果這個(gè)網(wǎng)格是空的,那我們就把用戶的標(biāo)記填充上去。之后,如果游戲沒有結(jié)束,就讓電腦進(jìn)行下一步的移動(dòng)。

 
 
 
 
  1. void TicTacToeWindow::OnLeftButtonUp(int x, int y, WPARAM params)
  2. {
  3.    if(game.is_started() && !game.is_finished())
  4.    {
  5.       RECT rcClient;
  6.       ::GetClientRect(hWnd, &rcClient);
  7.       int cellw = (rcClient.right - rcClient.left) / 3;
  8.       int cellh = (rcClient.bottom - rcClient.top) / 3;
  9.       int col = x / cellw;
  10.       int row = y / cellh;
  11.       if(game.move(tictactoe_cell(row, col), tictactoe_player::user))
  12.       {
  13.          if(!game.is_finished())
  14.             game.move(tictactoe_player::computer);
  15.          Invalidate(FALSE);
  16.       }
  17.    }
  18. }

最后,我們需要實(shí)現(xiàn)WinMain函數(shù),這是整個(gè)程序的入口點(diǎn)。下面的代碼與這部分開始我給出的代碼非常相似,不同的之處是它使用了我對窗口和窗口類進(jìn)行封裝的一些類。

 
 
 
 
  1. int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
  2. {
  3.    WindowClass wndcls(hInstance, L"TicTacToeWindowClass", MAKEINTRESOURCE(IDR_MENU_TTT), CallWinProc);   
  4.    TicTacToeWindow wnd;
  5.    if(wnd.Create(
  6.       wndcls.Name(), 
  7.       L"Fun C++: TicTacToe", 
  8.       WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, 
  9.       CW_USEDEFAULT, 
  10.       CW_USEDEFAULT, 
  11.       300, 
  12.       300, 
  13.       hInstance))
  14.    {
  15.       wnd.ShowWindow(nCmdShow);
  16.       MSG msg;
  17.       while(::GetMessage(&msg, nullptr, 0, 0))
  18.       {
  19.          ::TranslateMessage(&msg);
  20.          ::DispatchMessage(&msg);
  21.       }
  22.       return msg.wParam;
  23.    }
  24.    return 0;
  25. }

雖然我覺得我放在這里的代碼是相當(dāng)?shù)亩绦【?,但如果你不熟悉Win32 API程序設(shè)計(jì),你仍然可能會(huì)覺得這些代碼有點(diǎn)復(fù)雜。無論如何,你都一定要清楚的了解對象的初始化、如何創(chuàng)建一個(gè)窗口、如何處理窗口消息等。但愿你會(huì)覺得下一部分更有趣。

一個(gè)Windows Runtime的游戲app

Windows Runtime是Windows 8引入的一個(gè)新的Windows運(yùn)行時(shí)引擎. 它依附于Win32并且有一套基于COM的API. 為Windows Runtime創(chuàng)建的app通常很糟糕,被人稱為"Windows商店" 應(yīng)用. 它們運(yùn)行在Windows Runtime上, 而不是Windows商店里, 但是微軟的市場營銷人員可能已經(jīng)沒什么創(chuàng)造力了. Windows Runtime 應(yīng)用和組件可以用C++實(shí)現(xiàn),不管是用Windows Runtime C++ Template Library (WTL) 或者用 C++ Component Extensions (C++/CX)都可以. 在這里我將使用XAML和C++/CX來創(chuàng)建一個(gè)功能上和我們之前實(shí)現(xiàn)的桌面版應(yīng)用類似的應(yīng)用。

當(dāng)你創(chuàng)建一個(gè)空的Windows Store XAML應(yīng)用時(shí)向?qū)?chuàng)建的項(xiàng)目實(shí)際上并不是空的, 它包含了所有的Windows Store應(yīng)用構(gòu)建和運(yùn)行所需要的文件和配置。但是這個(gè)應(yīng)用的main page是空的。

我們要關(guān)心對這篇文章的目的,唯一的就是主界面。 XAML代碼可以在應(yīng)用在文件MainPage.xaml中,和背后的MainPage.xaml.h MainPage.xaml.cpp的代碼。,我想建立簡單的應(yīng)用程序如下圖。

下面是XAML的頁面可能看起來的樣子(在一個(gè)真實(shí)的應(yīng)用中,你可能要使用應(yīng)用程序欄來操作,如啟動(dòng)一個(gè)新的游戲,主頁上沒有按鍵,但為了簡單起見,我把它們在頁面上)

 
 
 
 
  1.     x:Class="TicTacToeWinRT.MainPage"
  2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.     xmlns:local="using:TicTacToeWinRT"
  5.     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  6.     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  7.     mc:Ignorable="d">   
  8.    
  9.    
  10.       
  11.          
  12.          < 當(dāng)前名稱:C++很有趣:編寫一個(gè)井字游戲(TicTacToe)
    轉(zhuǎn)載來于:http://www.5511xx.com/article/dhocshc.html