
Screenshot: Game Over
文件:
•. cannon.rb
•. t13.rb
•. gamebrd.rb
•. lcdrange.rb
从这个示例开始,我们实现了一个带有分数的真正可玩的游戏。
我们将MyWidget改成一个新名字(GameBoard),给它加上一些信号槽,再将它移动到gamebrd.rb 文件中
CannonField现在拥有一个表示游戏结束的状态。
LCDRange 中的布局问题得到解决。
@label.setSizePolicy(Qt::SizePolicy::Preferred, Qt::SizePolicy::Fixed)
我们将Qt::Label的尺寸变更策略设置成(Qt::SizePolicy::Preferred, Qt::SizePolicy::Fixed)。竖直方向的策略分量确保,这个标签不会在竖直方向上被拉伸或压缩;它会保持自己的最佳尺寸(即为它的QWidget::sizeHint()返回的结果)。这就能够解决第12章中发现的布局问题。
CannonField现在拥有一个表示游戏结束的状态,以及一些新函数。
signals 'canShoot(bool)'
这个新的信号的意义就是,表示CannonField已经进入了能让shoot()信号槽起作用的状态。我们稍后将利用这个信号来启用或禁用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()会使用这个新函数来判断,而不是亲自来直接判断。同时,开始打炮时也会向外界告知CannonField目前不可以再打炮了。
def setGameOver()
if @gameEnded
return
end
if isShooting()
@autoShootTimer.stop()
end
@gameEnded = true
update()
end
这个信号槽会使游戏结束。这个信号槽必须由 CannonField 之外的代码来调用,因为这个部件本身并不知道什么时候游戏会结束。这是组件式编程中的一种重要的设计原则。我们将这个组件实现得尽可能地灵活,以便在不同的游戏规则下重复使用(例如,如果将这个游戏改装成多人对战的,第一个命中10次的玩家就赢得游戏,那种情况下也应当能够在不改变代码的情况下直接使用 CannonField)。
如果游戏已经处于结束状态了,那么我们立即返回。如果游戏正处于进行中状态,那么,我们停掉子弹,设置游戏的结束状态,并且重绘整个部件。
def restartGame()
if isShooting()
@autoShootTimer.stop()
end
@gameEnded = false
update()
emit canShoot(true)
end
这个信号槽会启动一个新游戏。如果有子弹正在空中飞,那么我们停掉子弹。然后,我们重置gameEnded变量,重绘整个部件。
moveShot()会在发射hit()或miss()的同时发射新的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
绘图事件函数被增强了,当游戏已经结束(也就是说,gameEnded为true)时,显示出文字"Game Over"。这里,我们不费心去仔细需要更新的区域,因为,当游戏已经结束时,绘图速度已经不重要了。
为了绘制这个文字内容,我们首先设置一个黑色画笔;在绘制文字内容时会使用画笔的颜色。接下来,我们从Courier 字体族中选择一个尺寸为48点的粗体字体。最后,我们绘制文字内容,在部件本身的矩形区域里居中对齐。不幸的是,在某些系统中(尤其是在X服务器中使用Unicode字体),载入这种大字体狠要花些时间。因为Qt会对字体进行缓存,所以,妳只会在第一次使用该字体时察觉到延迟。
paintCannon(painter)
if isShooting()
paintShot(painter)
end
unless @gameEnded
paintTarget(painter)
end
painter.end()
end
我们只会在有子弹在飞的时候才绘制子弹,只会在游戏正在进行中(也就是说,游戏未结束)的状态下才绘制射击目标。
这是个新文件。它包含着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()'))
这次,当子弹命中或者未命中目标时,我们要做点动作。因此,我们将CannonField 中的hit()和missed()信号连接到这个类中两个具有相同名字的受保护的(protected)信号槽。
connect(shoot, SIGNAL('clicked()'), self, SLOT('fire()') )
之前,我们将Shoot按钮的clicked()信号直接连接到CannonField的shoot()信号槽。这次,我们想要跟踪被打出去的炮的数量,所以,我们将这个信号连接到这个类中的某个信号槽。
体会一下,当妳使用了自包含的组件时,要改变一个程序的行为是多么的容易。
connect(@cannonField, SIGNAL('canShoot(bool)'),
shoot, SLOT('setEnabled(bool)'))
我们还使用cannonField的canShoot()信号来相应地启用或禁用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按钮。单击这个按钮就会触发本部件中的newGame()信号槽。
@hits = Qt::LCDNumber.new(2)
@shotsLeft = Qt::LCDNumber.new(2)
hitsLabel = Qt::Label.new(tr('HITS'))
shotsLeftLabel = Qt::Label.new(tr('SHOTS LEFT'))
我们创建4个新的部件,用于显示命中数目及剩余子弹数目。
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里的右上角单元格开始变得拥挤了。我们在New Game 按钮的左边放置一个拉伸因子,这样可以确保这个按钮一直处于窗口的右侧。
newGame()
现在GameBoard 已经构造完成了,于是我们使用newGame()来启动整个游戏。尽管newGame()是一个信号槽,它也一样可以作为一个普通函数来使用。
def fire()
if @cannonField.gameOver() || @cannonField.isShooting()
return
end
@shotsLeft.display(@shotsLeft.intValue() - 1)
@cannonField.shoot()
en
这个函数用于打出一炮。如果游戏已经结束,或者空中还有子弹在飞,那么我们立即返回。我们减少剩余子弹数量,并且告之加农炮开炮。
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。注意,这是程序中唯一一处设置子弹数量的地方。妳可以随意修改这个数量,以影响到游戏的规则难易程度。然后,我们重置命中数目计数,重新启动游戏,并且生成一个新的射击目标。
这个文件就狠直观了。MyWidget已经移走了,唯一剩下的东西就是main()函数,其内容保持不变,只有文件名发生了改变。
加农炮可以对着目标打炮了;每当有一个目标被命中时,会自动生成一个新的目标。
命中数量和剩余子弹数量都会显示在界面上,程序会跟踪它们的变化。游戏可以结束,并且有一个按钮可以用于启动一个新游戏。
添加一个随机的风力因素,并且将它显示给用户。
当子弹命中目标时,显示出一个溅射效果。
实现多个射击目标。
灰度化
自适应阈值化
HxLauncher: Launch Android applications by voice commands