.tutorials/tutorial/t13/cannonfield.h
.tutorials/tutorial/t13/gameboard.cpp
.tutorials/tutorial/t13/gameboard.h
.tutorials/tutorial/t13/lcdrange.cpp
.tutorials/tutorial/t13/lcdrange.h
.tutorials/tutorial/t13/main.cpp
.tutorials/tutorial/t13/t13.pro
在這個範例,我們開始接近一個擁有分數、真正可以玩的遊戲。我們給了 MyWidget 一個新名字(GameBoard)並加入一些 slot。
我們把定義放在 gameboard.h 裡,並把實現放在 gameboard.cpp 中。
CannonField 現在有一個遊戲結束的狀態。
LCDRange 的版面配置問題被解決了。
Line by Line Walkthrough
t13/lcdrange.cpp
label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
我們 QLabel 的大小策略(policy)設為 (Preferred, Fixed)。垂直分量確保標籤不會垂直地伸展或收縮;它將會保持在最理想的大小(它的 sizeHint())。這裡解決的版面配置的問題請看第 12 章。
t13/cannonfield.h
CannonField 現在有一個遊戲結束狀態,和一些新函數。
bool gameOver() const { return gameEnded; }
假如遊戲結束,這個函式會返回 true。若是遊戲仍在進行,則返回 false。
void setGameOver(); void restartGame();
這裡有兩個新的 slot:setGameOver() 與 restartGame()。
void canShoot(bool can);
這個新的 signal 表示 CannonField 在一個 shoot() slot 生效的狀態。我們將在下面使用它讓 Shoot 按鈕生效(enable)與失效(disable)。
bool gameEnded;
這個私有變數包含了遊戲狀態;true 代表遊戲結束,而 false 代表遊戲還在進行。
t13/cannonfield.cpp
gameEnded = false;
這一行被加入到建構子中。最初,遊戲還沒有結束(這對玩家來說是幸運的 :-)。
void CannonField::shoot() { if (isShooting()) return; timerCount = 0; shootAngle = currentAngle; shootForce = currentForce; autoShootTimer->start(5); emit canShoot(false); }
我們加入了一個新的 isShooting() 函式,所以 shoot() 使用它以取代直接進行測試。shoot() 也會告訴外界:CannonField 現在無法進行射擊。
void CannonField::setGameOver() { if (gameEnded) return; if (isShooting()) autoShootTimer->stop(); gameEnded = true; update(); }
這個 slot 終止遊戲。它必須從 CannonField 以外的地方被呼叫,因為這個元件並不知道要何時終止遊戲。這在組件程式設計中是一個重要的設計原則。我們使組件盡可能地有彈性,讓它在不同規則中都是可以使用的(舉例來說,一個第一個射中十次的玩家將獲得勝利的多玩家版本可以使用相同的 CannonField)。
假如這個遊戲已經被終止,我們立即返回。假如遊戲仍在繼續,我們則停止砲彈,設定遊戲結束的旗標(flag),並重繪整個元件。
void CannonField::restartGame() { if (isShooting()) autoShootTimer->stop(); gameEnded = false; update(); emit canShoot(true); }
這個 slot 開始一場新遊戲。假如一個砲彈在空中,我們停止射擊。然後我們重設了 gameEnded 變數並重繪整個元件。
就像 hit() 或是 miss(),moveShot() 也在同一時間發出新的 canShoot(true) signal。
在 CannonField::paintEvent() 中的修改:
void CannonField::paintEvent(QPaintEvent * /* event */) { QPainter painter(this); if (gameEnded) { painter.setPen(Qt::black); painter.setFont(QFont("Courier", 48, QFont::Bold)); painter.drawText(rect(), Qt::AlignCenter, tr("Game Over")); }
繪圖事件被擴大成:假如遊戲結束,也就是 gameEnded 為 true 時,顯示 "Game Over" 這段文字。這裡我們並不費心於檢查矩形的更新,因為在遊戲結束的時候,速度不是必要的。
為了描繪出文字,我們首先設定了一個黑色的畫筆;畫筆的顏色是描繪文字時所使用的。接下來,我們選擇 Courier 字型 48 字級的粗體字。最後,我們將文字畫在元件矩形的中央。不幸的,在某些系統(尤其是使用 Unicode 字集的 X 伺服器)會花上一段時間來讀取如此大的字體。因為 Qt 暫存了這些字體,所以你只有在這個字體第一次被使用的時候才會注意到。
paintCannon(painter); if (isShooting()) paintShot(painter); if (!gameEnded) paintTarget(painter); }
我們只有在射擊時會畫出砲彈,並只在遊戲時(這是指,遊戲還沒有結束的時候)畫出攻擊目標。
t13/gameboard.h
這個檔案是新的。它包含了 GameBoard 類別的定義,也就是我們上次看到的 MyWidget。
class CannonField; class GameBoard : public QWidget { Q_OBJECT public: GameBoard(QWidget *parent = 0); protected slots: void fire(); void hit(); void missed(); void newGame(); private: QLCDNumber *hits; QLCDNumber *shotsLeft; CannonField *cannonField; };
現在我們增加了四個 slot。它們是保護屬性且於內部被使用。我們也加入了兩個顯示遊戲狀態的 QLCDNumber(hits 與 shotsLeft)。
t13/gameboard.cpp
這個檔案是新的,它包含了 GameBoard 類別的實現,也就是我們上次看到的 MyWidget。
我們在 GameBoard 建構子中做了一些改變。
cannonField = new CannonField;
cannonField 現在是一個成員變數,所以我們小心地改變建構子以使用它。
connect(cannonField, SIGNAL(hit()), this, SLOT(hit())); connect(cannonField, SIGNAL(missed()), this, SLOT(missed()));
當砲彈擊中或未擊中目標時,我們想做某些事。於是我們連接 CannonField 的 hit() 與 missed() signal 到這個類別的兩個同名的保護屬性 slot。
connect(shoot, SIGNAL(clicked()), this, SLOT(fire()));
以前我們直接連接 Shoot 按鈕的 clicked() signal 到 CannonField 的 shoot() slot。這次我們想要紀錄大砲開火的次數,所以取而代之的,我們將它連接到這個類別裡的一個保護屬性 slot。
注意到,當你使用獨立的組件操作時,去改變一支程式的行為有多麼容易。
connect(cannonField, SIGNAL(canShoot(bool)), shoot, SLOT(setEnabled(bool)));
我們同樣使用 cannonField 的 canShoot() signal 使 Shoot 按鈕適當地生效與失效。
QPushButton *restart = new QPushButton(tr("&New Game")); restart->setFont(QFont("Times", 18, QFont::Bold)); connect(restart, SIGNAL(clicked()), this, SLOT(newGame()));
我們建立、設定、並連接 New Game 按鈕,跟我們為其他按鈕所做的相同。按下這個按鈕將會使這個元件的 newGame() slot 運作。
hits = new QLCDNumber(2); hits->setSegmentStyle(QLCDNumber::Filled); shotsLeft = new QLCDNumber(2); shotsLeft->setSegmentStyle(QLCDNumber::Filled); QLabel *hitsLabel = new QLabel(tr("HITS")); QLabel *shotsLeftLabel = new QLabel(tr("SHOTS LEFT"));
我們建立我們的新元件。注意,我們並不費心將 QLabel 元件的指標保留在 GameBoard 類別裡,因為我們沒有什麼想為它做的了。Qt 將會在 GameBoard 元件被毀滅時刪除它們,而版面配置類別則會適當地改變它們的大小。
QHBoxLayout *topLayout = new QHBoxLayout; topLayout->addWidget(shoot); topLayout->addWidget(hits); topLayout->addWidget(hitsLabel); topLayout->addWidget(shotsLeft); topLayout->addWidget(shotsLeftLabel); topLayout->addStretch(1); topLayout->addWidget(restart);
QGridLayout 的右上格開始變擠了。我們將一個伸展空間(stretch)放置在 New Game 按鈕的左邊,確保這個按鈕總是出現在視窗的右邊。
newGame();
我們做完了所有建構 GameBoard 的動作,所以我們使用 newGame() 來開始。雖然 newGame() 是一個 slot,但是它也可以像一個普通的函式一樣使用。
void GameBoard::fire() { if (cannonField->gameOver() || cannonField->isShooting()) return; shotsLeft->display(shotsLeft->intValue() - 1); cannonField->shoot(); }
這個函式發射出一個砲彈。假如遊戲結束或是砲彈還在空中,我們立即返回。否則我們減少砲彈的數量,並告知加農砲射擊。
void GameBoard::hit() { hits->display(hits->intValue() + 1); if (shotsLeft->intValue() == 0) cannonField->setGameOver(); else cannonField->newTarget(); }
這個 slot 會在砲彈打中目標時被啟動。我們遞增擊中的次數。假如沒有剩下的砲彈,遊戲就結束了。否則,我們讓 CannonField 產生一個新目標。
void GameBoard::missed() { if (shotsLeft->intValue() == 0) cannonField->setGameOver(); }
這個 slot 會在砲彈沒打中目標時被啟動。假如沒有剩下的砲彈,遊戲就結束了。
void GameBoard::newGame() { shotsLeft->display(15); hits->display(0); cannonField->restartGame(); cannonField->newTarget(); }
這個 slot 會在使用者按下 New Game 按鈕時被啟動。它也會被建構子呼叫。首先它將砲彈數設為 15。注意,這是我們在程式中唯一設定砲彈數量的地方。接下來我們重設擊中次數、重新開始遊戲,並產生一個新的攻擊目標。
t13/main.cpp
這個檔案僅僅被減少了一部份。MyWidget 不見了,唯一留下的是 main() 函式,它除了名稱以外沒有其它改變。
Running the Application
加農砲現在可以射中目標了;當一個目標被擊中,一個新的目標會自動被建立。
擊中次數與剩餘砲彈會被顯示,而程式會紀錄它們。遊戲可以終止了,而且有一個按鈕可以開始新遊戲。
Exercises
加入一個隨機的風的因素,並把它顯示給使用者。
當砲彈擊中目標時,做一些飛濺(splatter)的效果。
實現多個目標。
來源:Qt Tutorial 13 - Game Over
版本:4.4.3
0 回覆:
張貼留言