Development/Tutorials/Qt4 Ruby Tutorial/Chapter 13: Difference between revisions

From KDE TechBase
No edit summary
No edit summary
 
(4 intermediate revisions by 3 users not shown)
Line 1: Line 1:
{{Template:I18n/Language Navigation Bar|Development/Tutorials/Qt4 Ruby Tutorial/Chapter 13}}
<languages />
{{TutorialBrowser|


series=[[Development/Tutorials/Qt4_Ruby_Tutorial|Qt4 Ruby Tutorial]]|


name=Game Over|


pre=[[Development/Tutorials/Qt4_Ruby_Tutorial/Chapter_12|Tutorial 12 - Hanging in the Air the Way Bricks Don't]]|
{{<translate><!--T:1-->
 
TutorialBrowser</translate>|
next=[[Development/Tutorials/Qt4_Ruby_Tutorial/Chapter_14|Tutorial 14 - Facing the Wall]]
series=[[Special:myLanguage/Development/Tutorials/Qt4_Ruby_Tutorial|<translate><!--T:2-->
Qt4 Ruby Tutorial</translate>]]|
name=<translate><!--T:3-->
Game Over</translate>|
pre=[[Special:myLanguage/Development/Tutorials/Qt4_Ruby_Tutorial/Chapter_12|<translate><!--T:4-->
Tutorial 12 - Hanging in the Air the Way Bricks Don't</translate>]]|
next=[[Special:myLanguage/Development/Tutorials/Qt4_Ruby_Tutorial/Chapter_14|<translate><!--T:5-->
Tutorial 14 - Facing the Wall</translate>]]
}}
}}
== Game Over ==
<translate>
== Game Over == <!--T:6-->
</translate>
<translate>
<!--T:7-->
[[Image:Qt4_Ruby_Tutorial_Screenshot_13.png|center]]
[[Image:Qt4_Ruby_Tutorial_Screenshot_13.png|center]]
</translate>
<translate>
<!--T:8-->
Files:
Files:
</translate>
* [http://www.darshancomputing.com/qt4-qtruby-tutorial/tutorial/t13/lcdrange.rb lcdrange.rb]
* [http://www.darshancomputing.com/qt4-qtruby-tutorial/tutorial/t13/lcdrange.rb lcdrange.rb]
* [http://www.darshancomputing.com/qt4-qtruby-tutorial/tutorial/t13/gamebrd.rb gamebrd.rb]
* [http://www.darshancomputing.com/qt4-qtruby-tutorial/tutorial/t13/gamebrd.rb gamebrd.rb]
Line 18: Line 30:
* [http://www.darshancomputing.com/qt4-qtruby-tutorial/tutorial/t13/cannon.rb cannon.rb]
* [http://www.darshancomputing.com/qt4-qtruby-tutorial/tutorial/t13/cannon.rb cannon.rb]


=== Overview ===
<translate>
=== Overview === <!--T:9-->
 
<!--T:10-->
In this example we start to approach a real playable game with a score.
In this example we start to approach a real playable game with a score.


<!--T:11-->
We give '''<tt>MyWidget</tt>''' a new name ('''<tt>GameBoard</tt>'''), add some slots, and move it to '''<tt>gamebrd.rb</tt>'''
We give '''<tt>MyWidget</tt>''' a new name ('''<tt>GameBoard</tt>'''), add some slots, and move it to '''<tt>gamebrd.rb</tt>'''


<!--T:12-->
The '''<tt>CannonField</tt>''' now has a game over state.
The '''<tt>CannonField</tt>''' now has a game over state.


<!--T:13-->
The layout problems in '''<tt>LCDRange</tt>''' are fixed.  
The layout problems in '''<tt>LCDRange</tt>''' are fixed.  


=== Line by Line Walkthrough ===
=== Line by Line Walkthrough === <!--T:14-->
</translate>
'''[http://www.darshancomputing.com/qt4-qtruby-tutorial/tutorial/t13/lcdrange.rb lcdrange.rb]'''
'''[http://www.darshancomputing.com/qt4-qtruby-tutorial/tutorial/t13/lcdrange.rb lcdrange.rb]'''


<code ruby>
<syntaxhighlight lang="ruby">
     @label.setSizePolicy(Qt::SizePolicy::Preferred, Qt::SizePolicy::Fixed)
     @label.setSizePolicy(Qt::SizePolicy::Preferred, Qt::SizePolicy::Fixed)
</code>
</syntaxhighlight>


<translate>
<!--T:15-->
We set the size policy of the [http://doc.qt.nokia.com/latest/qlabel.html Qt::Label] to ([http://doc.qt.nokia.com/latest/qsizepolicy.html#Policy-enum Qt::SizePolicy::Preferred], [http://doc.qt.nokia.com/latest/qsizepolicy.html#Policy-enum Qt::SizePolicy::Fixed]). The vertical component ensures that the label won't stretch or shrink vertically; it will stay at its optimal size (its [http://doc.qt.nokia.com/latest/qwidget.html#sizeHint-prop QWidget::sizeHint()]). This solves the layout problems observed in Chapter 12.
We set the size policy of the [http://doc.qt.nokia.com/latest/qlabel.html Qt::Label] to ([http://doc.qt.nokia.com/latest/qsizepolicy.html#Policy-enum Qt::SizePolicy::Preferred], [http://doc.qt.nokia.com/latest/qsizepolicy.html#Policy-enum Qt::SizePolicy::Fixed]). The vertical component ensures that the label won't stretch or shrink vertically; it will stay at its optimal size (its [http://doc.qt.nokia.com/latest/qwidget.html#sizeHint-prop QWidget::sizeHint()]). This solves the layout problems observed in Chapter 12.
</translate>


'''[http://www.darshancomputing.com/qt4-qtruby-tutorial/tutorial/t13/cannon.rb cannon.rb]'''
'''[http://www.darshancomputing.com/qt4-qtruby-tutorial/tutorial/t13/cannon.rb cannon.rb]'''


<translate>
<!--T:16-->
The '''<tt>CannonField</tt>''' now has a game over state and a few new functions.
The '''<tt>CannonField</tt>''' now has a game over state and a few new functions.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
   signals 'canShoot(bool)'
   signals 'canShoot(bool)'
</code>
</syntaxhighlight>


<translate>
<!--T:17-->
This new signal indicates that the '''<tt>CannonField</tt>''' is in a state where the '''<tt>shoot()</tt>''' slot makes sense. We'll use it below to enable or disable the <strong>Shoot</strong> button.
This new signal indicates that the '''<tt>CannonField</tt>''' is in a state where the '''<tt>shoot()</tt>''' slot makes sense. We'll use it below to enable or disable the <strong>Shoot</strong> button.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
     @gameEnded = false
     @gameEnded = false
</code>
</syntaxhighlight>


<translate>
<!--T:18-->
This variable contains the game state; '''<tt>true</tt>''' means that the game is over, and '''<tt>false</tt>''' means that a game is going on. Initially, the game is not over (luckily for the player :-).
This variable contains the game state; '''<tt>true</tt>''' means that the game is over, and '''<tt>false</tt>''' means that a game is going on. Initially, the game is not over (luckily for the player :-).
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
def shoot()
def shoot()
   if isShooting()
   if isShooting()
Line 64: Line 95:
   emit canShoot(false)
   emit canShoot(false)
end
end
</code>
</syntaxhighlight>


<translate>
<!--T:19-->
We added a new '''<tt>isShooting()</tt>''' function, so '''<tt>shoot()</tt>''' uses it instead of testing directly. Also, shoot tells the world that the '''<tt>CannonField</tt>''' cannot shoot now.
We added a new '''<tt>isShooting()</tt>''' function, so '''<tt>shoot()</tt>''' uses it instead of testing directly. Also, shoot tells the world that the '''<tt>CannonField</tt>''' cannot shoot now.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
def setGameOver()
def setGameOver()
   if @gameEnded
   if @gameEnded
Line 81: Line 115:
   update()
   update()
end
end
</code>
</syntaxhighlight>


<translate>
<!--T:20-->
This slot ends the game. It must be called from outside '''<tt>CannonField</tt>''', because this widget does not know when to end the game. This is an important design principle in component programming. We choose to make the component as flexible as possible to make it usable with different rules (for example, a multi-player version of this in which the first player to hit ten times wins could use the '''<tt>CannonField</tt>''' unchanged).
This slot ends the game. It must be called from outside '''<tt>CannonField</tt>''', because this widget does not know when to end the game. This is an important design principle in component programming. We choose to make the component as flexible as possible to make it usable with different rules (for example, a multi-player version of this in which the first player to hit ten times wins could use the '''<tt>CannonField</tt>''' unchanged).


<!--T:21-->
If the game has already been ended we return immediately. If a game is going on we stop the shot, set the game over flag, and repaint the entire widget.
If the game has already been ended we return immediately. If a game is going on we stop the shot, set the game over flag, and repaint the entire widget.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
def restartGame()
def restartGame()
   if isShooting()
   if isShooting()
Line 98: Line 136:
   emit canShoot(true)
   emit canShoot(true)
end
end
</code>
</syntaxhighlight>


<translate>
<!--T:22-->
This slot starts a new game. If a shot is in the air, we stop shooting. We then reset the '''<tt>gameEnded</tt>''' variable and repaint the widget.
This slot starts a new game. If a shot is in the air, we stop shooting. We then reset the '''<tt>gameEnded</tt>''' variable and repaint the widget.


<!--T:23-->
'''<tt>moveShot()</tt>''' too emits the new '''<tt>canShoot(true)</tt>''' signal at the same time as either '''<tt>hit()</tt>''' or '''<tt>miss()</tt>'''.
'''<tt>moveShot()</tt>''' too emits the new '''<tt>canShoot(true)</tt>''' signal at the same time as either '''<tt>hit()</tt>''' or '''<tt>miss()</tt>'''.


<!--T:24-->
Modifications in CannonField::paintEvent():
Modifications in CannonField::paintEvent():
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
def paintEvent(event)
def paintEvent(event)
   painter = Qt::Painter.new(self)
   painter = Qt::Painter.new(self)
Line 115: Line 158:
     painter.drawText(rect(), Qt::AlignCenter, tr("Game Over"))
     painter.drawText(rect(), Qt::AlignCenter, tr("Game Over"))
   end
   end
</code>
</syntaxhighlight>


<translate>
<!--T:25-->
The paint event has been enhanced to display the text "Game Over" if the game is over, i.e., '''<tt>gameEnded</tt>''' is '''<tt>true</tt>'''. We don't bother to check the update rectangle here because speed is not critical when the game is over.
The paint event has been enhanced to display the text "Game Over" if the game is over, i.e., '''<tt>gameEnded</tt>''' is '''<tt>true</tt>'''. We don't bother to check the update rectangle here because speed is not critical when the game is over.


<!--T:26-->
To draw the text we first set a black pen; the pen color is used when drawing text. Next we choose a 48 point bold font from the Courier family. Finally we draw the text centered in the widget's rectangle. Unfortunately, on some systems (especially X servers with Unicode fonts) it can take a while to load such a large font. Because Qt caches fonts, you will notice this only the first time the font is used.
To draw the text we first set a black pen; the pen color is used when drawing text. Next we choose a 48 point bold font from the Courier family. Finally we draw the text centered in the widget's rectangle. Unfortunately, on some systems (especially X servers with Unicode fonts) it can take a while to load such a large font. Because Qt caches fonts, you will notice this only the first time the font is used.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
   paintCannon(painter)
   paintCannon(painter)


Line 134: Line 181:
   painter.end()
   painter.end()
end
end
</code>
</syntaxhighlight>


<translate>
<!--T:27-->
We draw the shot only when shooting and the target only when playing (that is, when the game is not ended).
We draw the shot only when shooting and the target only when playing (that is, when the game is not ended).
</translate>


'''[http://www.darshancomputing.com/qt4-qtruby-tutorial/tutorial/t13/gamebrd.rb gamebrd.rb]'''
'''[http://www.darshancomputing.com/qt4-qtruby-tutorial/tutorial/t13/gamebrd.rb gamebrd.rb]'''


<translate>
<!--T:28-->
This file is new. It contains the '''<tt>GameBoard</tt>''' class, which was last seen as '''<tt>MyWidget</tt>'''.
This file is new. It contains the '''<tt>GameBoard</tt>''' class, which was last seen as '''<tt>MyWidget</tt>'''.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
   slots 'fire()', 'hit()', 'missed()', 'newGame()'
   slots 'fire()', 'hit()', 'missed()', 'newGame()'
</code>
</syntaxhighlight>


<translate>
<!--T:29-->
We have now added four slots.
We have now added four slots.


<!--T:30-->
We have also made some changes in the '''<tt>GameBoard</tt>''' constructor.
We have also made some changes in the '''<tt>GameBoard</tt>''' constructor.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
     @cannonField = CannonField.new()
     @cannonField = CannonField.new()
</code>
</syntaxhighlight>


<translate>
<!--T:31-->
'''<tt>@cannonField</tt>''' is now a member variable, so we carefully change the constructor to use it.
'''<tt>@cannonField</tt>''' is now a member variable, so we carefully change the constructor to use it.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
connect(@cannonField, SIGNAL('hit()'),
connect(@cannonField, SIGNAL('hit()'),
         self, SLOT('hit()'))
         self, SLOT('hit()'))
connect(@cannonField, SIGNAL('missed()'),
connect(@cannonField, SIGNAL('missed()'),
         self, SLOT('missed()'))
         self, SLOT('missed()'))
</code>
</syntaxhighlight>


<translate>
<!--T:32-->
This time we want to do something when the shot has hit or missed the target. Thus we connect the '''<tt>hit()</tt>''' and '''<tt>missed()</tt>''' signals of the '''<tt>CannonField</tt>''' to two protected slots with the same names in this class.
This time we want to do something when the shot has hit or missed the target. Thus we connect the '''<tt>hit()</tt>''' and '''<tt>missed()</tt>''' signals of the '''<tt>CannonField</tt>''' to two protected slots with the same names in this class.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
     connect(shoot, SIGNAL('clicked()'), self, SLOT('fire()') )
     connect(shoot, SIGNAL('clicked()'), self, SLOT('fire()') )
</code>
</syntaxhighlight>


<translate>
<!--T:33-->
Previously we connected the <strong>Shoot</strong> button's '''<tt>clicked()</tt>''' signal directly to the '''<tt>CannonField</tt>''''s '''<tt>shoot()</tt>''' slot. This time we want to keep track of the number of shots fired, so we connect it to a slot in this class instead.
Previously we connected the <strong>Shoot</strong> button's '''<tt>clicked()</tt>''' signal directly to the '''<tt>CannonField</tt>''''s '''<tt>shoot()</tt>''' slot. This time we want to keep track of the number of shots fired, so we connect it to a slot in this class instead.


<!--T:34-->
Notice how easy it is to change the behavior of a program when you are working with self-contained components.
Notice how easy it is to change the behavior of a program when you are working with self-contained components.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
connect(@cannonField, SIGNAL('canShoot(bool)'),
connect(@cannonField, SIGNAL('canShoot(bool)'),
         shoot, SLOT('setEnabled(bool)'))
         shoot, SLOT('setEnabled(bool)'))
</code>
</syntaxhighlight>


<translate>
<!--T:35-->
We also use the '''<tt>CannonField</tt>''''s '''<tt>canShoot()</tt>''' signal to enable or disable the <strong>Shoot</strong> button appropriately.
We also use the '''<tt>CannonField</tt>''''s '''<tt>canShoot()</tt>''' signal to enable or disable the <strong>Shoot</strong> button appropriately.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
restart = Qt::PushButton.new(tr('&New Game'))
restart = Qt::PushButton.new(tr('&New Game'))
restart.setFont(Qt::Font.new('Times', 18, Qt::Font::Bold))
restart.setFont(Qt::Font.new('Times', 18, Qt::Font::Bold))


connect(restart, SIGNAL('clicked()'), self, SLOT('newGame()'))
connect(restart, SIGNAL('clicked()'), self, SLOT('newGame()'))
</code>
</syntaxhighlight>


<translate>
<!--T:36-->
We create, set up, and connect the <strong>New Game</strong> button as we have done with the other buttons. Clicking this button will activate the '''<tt>newGame()</tt>''' slot in this widget.
We create, set up, and connect the <strong>New Game</strong> button as we have done with the other buttons. Clicking this button will activate the '''<tt>newGame()</tt>''' slot in this widget.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
@hits = Qt::LCDNumber.new(2)
@hits = Qt::LCDNumber.new(2)
@shotsLeft = Qt::LCDNumber.new(2)
@shotsLeft = Qt::LCDNumber.new(2)
hitsLabel = Qt::Label.new(tr('HITS'))
hitsLabel = Qt::Label.new(tr('HITS'))
shotsLeftLabel = Qt::Label.new(tr('SHOTS LEFT'))
shotsLeftLabel = Qt::Label.new(tr('SHOTS LEFT'))
</code>
</syntaxhighlight>


<translate>
<!--T:37-->
We create four new widgets, to display the number of hits and shots left.
We create four new widgets, to display the number of hits and shots left.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
topLayout = Qt::HBoxLayout.new()
topLayout = Qt::HBoxLayout.new()
topLayout.addWidget(shoot)
topLayout.addWidget(shoot)
Line 207: Line 283:
topLayout.addStretch(1)
topLayout.addStretch(1)
topLayout.addWidget(restart)
topLayout.addWidget(restart)
</code>
</syntaxhighlight>


<translate>
<!--T:38-->
The top-right cell of the [http://doc.qt.nokia.com/latest/qgridlayout.html Qt::GridLayout] is starting to get crowded. We put a stretch just to the left of the <strong>New Game</strong> button to ensure that this button will always appear on the right side of the window.
The top-right cell of the [http://doc.qt.nokia.com/latest/qgridlayout.html Qt::GridLayout] is starting to get crowded. We put a stretch just to the left of the <strong>New Game</strong> button to ensure that this button will always appear on the right side of the window.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
     newGame()
     newGame()
</code>
</syntaxhighlight>


<translate>
<!--T:39-->
We're all done constructing the '''<tt>GameBoard</tt>''', so we start it all using '''<tt>newGame()</tt>'''. Although '''<tt>newGame()</tt>''' is a slot, it can also be used as an ordinary function.
We're all done constructing the '''<tt>GameBoard</tt>''', so we start it all using '''<tt>newGame()</tt>'''. Although '''<tt>newGame()</tt>''' is a slot, it can also be used as an ordinary function.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
def fire()
def fire()
   if @cannonField.gameOver() || @cannonField.isShooting()
   if @cannonField.gameOver() || @cannonField.isShooting()
Line 226: Line 308:
   @cannonField.shoot()
   @cannonField.shoot()
end
end
</code>
</syntaxhighlight>


<translate>
<!--T:40-->
This function fires a shot. If the game is over or if there is a shot in the air, we return immediately. We decrement the number of shots left and tell the cannon to shoot.
This function fires a shot. If the game is over or if there is a shot in the air, we return immediately. We decrement the number of shots left and tell the cannon to shoot.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
def hit()
def hit()
   @hits.display(@hits.intValue() + 1)
   @hits.display(@hits.intValue() + 1)
Line 240: Line 325:
   end
   end
end
end
</code>
</syntaxhighlight>


<translate>
<!--T:41-->
This slot is activated when a shot has hit the target. We increment the number of hits. If there are no shots left, the game is over. Otherwise, we make the '''<tt>CannonField</tt>''' generate a new target.
This slot is activated when a shot has hit the target. We increment the number of hits. If there are no shots left, the game is over. Otherwise, we make the '''<tt>CannonField</tt>''' generate a new target.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
def missed()
def missed()
   if @shotsLeft.intValue() == 0
   if @shotsLeft.intValue() == 0
Line 250: Line 338:
   end
   end
end
end
</code>
</syntaxhighlight>


<translate>
<!--T:42-->
This slot is activated when a shot has missed the target. If there are no shots left, the game is over.
This slot is activated when a shot has missed the target. If there are no shots left, the game is over.
</translate>


<code ruby>
<syntaxhighlight lang="ruby">
def newGame()
def newGame()
   @shotsLeft.display(15)
   @shotsLeft.display(15)
Line 261: Line 352:
   @cannonField.newTarget()
   @cannonField.newTarget()
end
end
</code>
</syntaxhighlight>


<translate>
<!--T:43-->
This slot is activated when the user clicks the <strong>New Game</strong> button. It is also called from the constructor. First it sets the number of shots to 15. Note that this is the only place in the program where we set the number of shots. Change it to whatever you like to change the game rules. Next we reset the number of hits, restart the game, and generate a new target.
This slot is activated when the user clicks the <strong>New Game</strong> button. It is also called from the constructor. First it sets the number of shots to 15. Note that this is the only place in the program where we set the number of shots. Change it to whatever you like to change the game rules. Next we reset the number of hits, restart the game, and generate a new target.
</translate>


'''[http://www.darshancomputing.com/qt4-qtruby-tutorial/tutorial/t13/t13.rb t13.rb]'''
'''[http://www.darshancomputing.com/qt4-qtruby-tutorial/tutorial/t13/t13.rb t13.rb]'''


<translate>
<!--T:44-->
This file has just been on a diet. '''<tt>MyWidget</tt>''' is gone, and the only thing left is the '''<tt>main()</tt>''' function, unchanged except for the name change.
This file has just been on a diet. '''<tt>MyWidget</tt>''' is gone, and the only thing left is the '''<tt>main()</tt>''' function, unchanged except for the name change.
</translate>
<translate>
=== Running the Application === <!--T:45-->


=== Running the Application ===
<!--T:46-->
The cannon can shoot at a target; a new target is automatically created when one has been hit.
The cannon can shoot at a target; a new target is automatically created when one has been hit.


<!--T:47-->
Hits and shots left are displayed and the program keeps track of them. The game can end, and there's a button to start a new game.
Hits and shots left are displayed and the program keeps track of them. The game can end, and there's a button to start a new game.


=== Exercises ===
=== Exercises === <!--T:48-->
 
<!--T:49-->
Add a random wind factor and show it to the user.
Add a random wind factor and show it to the user.


<!--T:50-->
Make some splatter effects when the shot hits the target.
Make some splatter effects when the shot hits the target.


<!--T:51-->
Implement multiple targets.
Implement multiple targets.


<!--T:52-->
[[Category:Ruby]]
[[Category:Ruby]]
</translate>

Latest revision as of 14:25, 18 July 2012

Other languages:


Game Over
Tutorial Series   Qt4 Ruby Tutorial
Previous   Tutorial 12 - Hanging in the Air the Way Bricks Don't
What's Next   Tutorial 14 - Facing the Wall
Further Reading   n/a

Game Over

Files:

Overview

In this example we start to approach a real playable game with a score.

We give MyWidget a new name (GameBoard), add some slots, and move it to gamebrd.rb

The CannonField now has a game over state.

The layout problems in LCDRange are fixed.

Line by Line Walkthrough

lcdrange.rb

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

We set the size policy of the Qt::Label to (Qt::SizePolicy::Preferred, Qt::SizePolicy::Fixed). The vertical component ensures that the label won't stretch or shrink vertically; it will stay at its optimal size (its QWidget::sizeHint()). This solves the layout problems observed in Chapter 12.

cannon.rb

The CannonField now has a game over state and a few new functions.

  signals 'canShoot(bool)'

This new signal indicates that the CannonField is in a state where the shoot() slot makes sense. We'll use it below to enable or disable the Shoot button.

    @gameEnded = false

This variable contains the game state; true means that the game is over, and false means that a game is going on. Initially, the game is not over (luckily for the player :-).

def shoot()
  if isShooting()
    return
  end

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

We added a new isShooting() function, so shoot() uses it instead of testing directly. Also, shoot tells the world that the CannonField cannot shoot now.

def setGameOver()
  if @gameEnded
    return
  end

  if isShooting()
    @autoShootTimer.stop()
  end

  @gameEnded = true
  update()
end

This slot ends the game. It must be called from outside CannonField, because this widget does not know when to end the game. This is an important design principle in component programming. We choose to make the component as flexible as possible to make it usable with different rules (for example, a multi-player version of this in which the first player to hit ten times wins could use the CannonField unchanged).

If the game has already been ended we return immediately. If a game is going on we stop the shot, set the game over flag, and repaint the entire widget.

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

  @gameEnded = false

  update()
  emit canShoot(true)
end

This slot starts a new game. If a shot is in the air, we stop shooting. We then reset the gameEnded variable and repaint the widget.

moveShot() too emits the new canShoot(true) signal at the same time as either hit() or miss().

Modifications in 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

The paint event has been enhanced to display the text "Game Over" if the game is over, i.e., gameEnded is true. We don't bother to check the update rectangle here because speed is not critical when the game is over.

To draw the text we first set a black pen; the pen color is used when drawing text. Next we choose a 48 point bold font from the Courier family. Finally we draw the text centered in the widget's rectangle. Unfortunately, on some systems (especially X servers with Unicode fonts) it can take a while to load such a large font. Because Qt caches fonts, you will notice this only the first time the font is used.

  paintCannon(painter)

  if isShooting()
    paintShot(painter)
  end        

  unless @gameEnded
    paintTarget(painter)
  end

  painter.end()
end

We draw the shot only when shooting and the target only when playing (that is, when the game is not ended).

gamebrd.rb

This file is new. It contains the GameBoard class, which was last seen as MyWidget.

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

We have now added four slots.

We have also made some changes in the GameBoard constructor.

    @cannonField = CannonField.new()

@cannonField is now a member variable, so we carefully change the constructor to use it.

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

This time we want to do something when the shot has hit or missed the target. Thus we connect the hit() and missed() signals of the CannonField to two protected slots with the same names in this class.

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

Previously we connected the Shoot button's clicked() signal directly to the CannonField's shoot() slot. This time we want to keep track of the number of shots fired, so we connect it to a slot in this class instead.

Notice how easy it is to change the behavior of a program when you are working with self-contained components.

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

We also use the CannonField's canShoot() signal to enable or disable the Shoot button appropriately.

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

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

We create, set up, and connect the New Game button as we have done with the other buttons. Clicking this button will activate the newGame() slot in this widget.

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

We create four new widgets, to display the number of hits and 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)

The top-right cell of the Qt::GridLayout is starting to get crowded. We put a stretch just to the left of the New Game button to ensure that this button will always appear on the right side of the window.

    newGame()

We're all done constructing the GameBoard, so we start it all using newGame(). Although newGame() is a slot, it can also be used as an ordinary function.

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

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

This function fires a shot. If the game is over or if there is a shot in the air, we return immediately. We decrement the number of shots left and tell the cannon to shoot.

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

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

This slot is activated when a shot has hit the target. We increment the number of hits. If there are no shots left, the game is over. Otherwise, we make the CannonField generate a new target.

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

This slot is activated when a shot has missed the target. If there are no shots left, the game is over.

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

This slot is activated when the user clicks the New Game button. It is also called from the constructor. First it sets the number of shots to 15. Note that this is the only place in the program where we set the number of shots. Change it to whatever you like to change the game rules. Next we reset the number of hits, restart the game, and generate a new target.

t13.rb

This file has just been on a diet. MyWidget is gone, and the only thing left is the main() function, unchanged except for the name change.

Running the Application

The cannon can shoot at a target; a new target is automatically created when one has been hit.

Hits and shots left are displayed and the program keeps track of them. The game can end, and there's a button to start a new game.

Exercises

Add a random wind factor and show it to the user.

Make some splatter effects when the shot hits the target.

Implement multiple targets.