StupidBeauty
Read times:934Posted at:Fri Jan 31 02:58:36 2014
- no title specified

Qt®4的Ruby教程翻译:第13章:完蛋了,Chapter 13: Game Over

Screenshot: Game OverScreenshot: Game Over


文件:

概述

从这个示例开始,我们实现了一个带有分数的真正可玩的游戏。

我们将MyWidget改成一个新名字(GameBoard),给它加上一些信号槽,再将它移动到gamebrd.rb 文件中

CannonField现在拥有一个表示游戏结束的状态。

LCDRange 中的布局问题得到解决。

一行行地研究

lcdrange.rb

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

我们将Qt::Label的尺寸变更策略设置成(Qt::SizePolicy::Preferred, Qt::SizePolicy::Fixed)。竖直方向的策略分量确保,这个标签不会在竖直方向上被拉伸或压缩;它会保持自己的最佳尺寸(即为它的QWidget::sizeHint()返回的结果)。这就能够解决第12章中发现的布局问题。

cannon.rb

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

绘图事件函数被增强了,当游戏已经结束(也就是说,gameEndedtrue)时,显示出文字"Game Over"。这里,我们不费心去仔细需要更新的区域,因为,当游戏已经结束时,绘图速度已经不重要了。

为了绘制这个文字内容,我们首先设置一个黑色画笔;在绘制文字内容时会使用画笔的颜色。接下来,我们从Courier 字体族中选择一个尺寸为48点的粗体字体。最后,我们绘制文字内容,在部件本身的矩形区域里居中对齐。不幸的是,在某些系统中(尤其是在X服务器中使用Unicode字体),载入这种大字体狠要花些时间。因为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()'))

这次,当子弹命中或者未命中目标时,我们要做点动作。因此,我们将CannonField 中的hit()missed()信号连接到这个类中两个具有相同名字的受保护的(protected)信号槽。

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按钮。单击这个按钮就会触发本部件中的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。注意,这是程序中唯一一处设置子弹数量的地方。妳可以随意修改这个数量,以影响到游戏的规则难易程度。然后,我们重置命中数目计数,重新启动游戏,并且生成一个新的射击目标。

t13.rb

这个文件就狠直观了。MyWidget已经移走了,唯一剩下的东西就是main()函数,其内容保持不变,只有文件名发生了改变。

运行程序

加农炮可以对着目标打炮了;每当有一个目标被命中时,会自动生成一个新的目标。

命中数量和剩余子弹数量都会显示在界面上,程序会跟踪它们的变化。游戏可以结束,并且有一个按钮可以用于启动一个新游戏。

练习

添加一个随机的风力因素,并且将它显示给用户。

当子弹命中目标时,显示出一个溅射效果。

实现多个射击目标。

[下一篇: 14章 ]

灰度化

自适应阈值化

Your opinions
Your name:Email:Website url:Opinion content:
- no title specified

HxLauncher: Launch Android applications by voice commands