Archive:Development/Tutorials/Qt4 Ruby Tutorial/Chapter 13 (zh TW)

From KDE TechBase
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

Template:I18n/Language Navigation Bar (zh TW) Template:TutorialBrowser (zh TW)

Game Over

檔案:

概覽

在這個範例中,我們開始接近一個有分數、真正可以玩的遊戲。

我們給 MyWidget 一個新名字(GameBoard),再加上一些槽。並且把它移動到 gamebrd.rb

CannonField 現在有一個遊戲結束狀態。

LCDRange 的佈局問題被修正了。

一行一行的瀏覽

lcdrange.rb

    @label.setSizePolicy(Qt::SizePolicy::Preferred, Qt::SizePolicy::Fixed)

我們設定 Qt::Label 的大小政策為(Qt::SizePolicy::PreferredQt::SizePolicy::Fixed)。垂直組件確保標籤將不會垂直伸展或收縮,它會保持在最佳大小(它的 QWidget::sizeHint())。這解決了在第12章發現的佈局問題。

cannon.rb

CannonField 現在有遊戲結束狀態和一些新功能。

signals 'canShoot(bool)'

CannonField 的這個新訊號表示 shoot() 槽的有效狀態。我們將使用它啟用(enable)或禁用(disable)下面的 Shoot 按鈕。

@gameEnded = false

這個變數包含了遊戲狀態;true 代表遊戲結束,而 false 代表遊戲還沒結束。(幸運的玩家:-)。

def shoot()
  if isShooting()
    return
  end

  @timerCount = 0
  @shootAngle = @currentAngle
  @shootForce = @currentForce
  @autoShootTimer.start(5)
  emit canShoot(false)
end

我們增加了一個新的 isShooting() 函式,所以 shoot() 使用它以取代直接測試。此外,shoot 也會告訴外界,CannonField 現在無法進行射擊。

def setGameOver()
  if @gameEnded
    return
  end

  if isShooting()
    @autoShootTimer.stop()
  end

  @gameEnded = true
  update()
end

這個槽終止遊戲。它必須要在 CannonField 外部呼叫,因為這個 widget 不知道什麼時候終止遊戲。這在組件程式設計中是一項重要的設計原則。我們使組件盡可能靈活,使其在不同規則都可使用(例如,由第一位命中十次的玩家獲勝的多人版本,可以使用相同的 CannonField)。

如果遊戲已經終止,我們立即返回。如果遊戲繼續,我們停止射擊、設定遊戲結束的標誌,並重繪整個 widget。

def restartGame()
  if isShooting()
    @autoShootTimer.stop()
  end

  @gameEnded = false

  update()
  emit canShoot(true)
end

這槽開始一場新遊戲。如果有砲彈在空中,我們停止射擊。然後我們重設 gameEnded 變數並重繪 widget。

就像 hit()miss()moveShot() 也同時發出新的 canShoot(true) 訊號。

在 CannonField::paintEvent() 中的修改:

def paintEvent(event)
  painter = Qt::Painter.new(self)

  if @gameEnded
    painter.setPen(Qt::black)
    painter.setFont(Qt::Font.new( "Courier", 48, Qt::Font::Bold))
    painter.drawText(rect(), Qt::AlignCenter, tr("Game Over"))
  end

繪圖事件已經增強:如果遊戲結束時,換句話說 gameEndedtrue,顯示文字「Game Over」。這裡我們並不費心去檢查矩形的更新。因為遊戲結束了,速度並不重要。

為了繪製文字,我們首先設定了一支黑色的筆觸,畫筆的顏色是繪製文字時使用。接下來,我們選擇48號粗體 的 Courier 字型。最後,我們繪製文字在 widget 矩形的中心。不幸的是,在某些系統上(尤其是使用 Unicode 字集的 X 伺服器),它可能需要一段時間來載入這麼大的字體。因為 Qt 會暫存字體,所以你只有在字體第一次使用時會注意。

  paintCannon(painter)

  if isShooting()
    paintShot(painter)
  end        

  unless @gameEnded
    paintTarget(painter)
  end

  painter.end()
end

我們只有在射擊時會畫出砲彈,並且只在遊戲時(這是指遊戲還沒有結束的時候)畫出目標。

gamebrd.rb

這個檔案是新加的。它包含 GameBoard 類別,也就是上次看到的 MyWidget

slots 'fire()', 'hit()', 'missed()', 'newGame()'

我們現在又增加了4個槽。

我們還對 GameBoard 建構子作了一些修改。

@cannonField = CannonField.new()

@cannonField 現在是一個成員變數,所以我們小心地修改建構子以使用它。

connect(@cannonField, SIGNAL('hit()'),
        self, SLOT('hit()'))
connect(@cannonField, SIGNAL('missed()'),
        self, SLOT('missed()'))

當砲彈已經擊中或未擊中目標時,我們想要做些事。因此,我們連接 CannonFieldhit()missed() 訊號到這個類別中兩個同名的保護槽(protected slots)。

connect(shoot, SIGNAL('clicked()'), self, SLOT('fire()') )

之前,我們直接連接 Shoot 按鈕的 clicked() 訊號到CannonFieldshoot() 槽。這次我們想要紀錄發射的數目,因此我們將它連接到這個類別中的一個槽。

請注意,當你使用獨立組件運作時,改變一個程式的行為是多麼地容易。

connect(@cannonField, SIGNAL('canShoot(bool)'),
        shoot, SLOT('setEnabled(bool)'))

我們還使用 CannonFieldcanShoot() 訊號來適當地啟用或禁用 Shoot 按鈕。

restart = Qt::PushButton.new(tr('&New Game'))
restart.setFont(Qt::Font.new('Times', 18, Qt::Font::Bold))

connect(restart, SIGNAL('clicked()'), self, SLOT('newGame()'))

我們建立、設定,並連接 New Game 按鈕,就像我們為其他按鈕所做的。按下這個按鈕將會啟動這個 widget 的 newGame() 槽。

@hits = Qt::LCDNumber.new(2)
@shotsLeft = Qt::LCDNumber.new(2)
hitsLabel = Qt::Label.new(tr('HITS'))
shotsLeftLabel = Qt::Label.new(tr('SHOTS LEFT'))

我們建立四個新的 widget,以顯示擊中(hits)次數,和剩餘砲彈(shots left)。

topLayout = Qt::HBoxLayout.new()
topLayout.addWidget(shoot)
topLayout.addWidget(@hits)
topLayout.addWidget(hitsLabel)
topLayout.addWidget(@shotsLeft)
topLayout.addWidget(shotsLeftLabel)
topLayout.addStretch(1)
topLayout.addWidget(restart)

Qt::GridLayout 的右上格開始變擠了。我們把一個 stretch 放在 New Game 按鈕的左邊,以確保此按鈕總是出現在視窗的右側。

newGame()

我們完成所有的 GameBoard 建設,所以我們使用 newGame() 來開始。雖然 newGame() 是一個槽,它也可以當作普通的函式。

def fire()
  if @cannonField.gameOver() || @cannonField.isShooting()
    return
  end

  @shotsLeft.display(@shotsLeft.intValue() - 1)
  @cannonField.shoot()
end

這個函式會發射一顆砲彈。如果遊戲結束,或有一顆砲彈在空中,我們立即返回。否則我們減少剩餘砲彈數,並告訴加農砲射擊。

def hit()
  @hits.display(@hits.intValue() + 1)

  if @shotsLeft.intValue() == 0
    @cannonField.setGameOver()
  else
    @cannonField.newTarget()
  end
end

這個槽會在砲彈擊中目標時啟動。我們增加擊中次數。如果沒有剩餘砲彈,遊戲結束。否則,我們讓 CannonField 產生一個新的目標。

def missed()
  if @shotsLeft.intValue() == 0
    @cannonField.setGameOver()
  end
end

這個槽會在砲彈沒打中目標時啟動。如果沒有剩餘砲彈,遊戲結束。

def newGame()
  @shotsLeft.display(15)
  @hits.display(0)
  @cannonField.restartGame()
  @cannonField.newTarget()
end

這個槽會在使用者按下 New Game 按鈕時啟動。它也會被建構子呼叫。首先,它設定砲彈數為15。請注意,這是程式中我們唯一設定砲彈數的地方。可以更改它為任何您想要的遊戲規則。接下來,我們重設擊中次數、重新啟動遊戲,並產生一個新的目標。

t13.rb

這個檔案剛剛減肥。MyWidget 離開了,唯一留下的是 main() 函式,它除了名稱以外沒有其它改變。

執行應用程式

加農砲可以射擊目標了。一個新的目標會在舊的被擊中後自動建立。

顯示擊中和剩餘砲彈,並且程式也會紀錄他們。遊戲可以終止,而且有一個按鈕可以開始新遊戲。

練習

加入一個隨機的因子:風。並顯示給使用者。

當砲彈擊中目標時,做出一些飛濺(splatter)的效果。

實現多個目標。