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

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 CN) Template:TutorialBrowser (zh CN)

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)的效果。

实现多个目标。