Waterdrop game, the third version¶
We are going to study how to use Qt to implement a graphical user interface for the waterdrop game. The game logic is nearly the same as in the previous version.
Before studying the program code located in the directory
examples/01-16/waterdrop_game_v3
closer, it is a good idea to first
execute the program at least once.
If you see only a grid without water drops, do as follows.
On the left in Qt, select Projects and uncheck the choice Shadow build.
Responsibilities¶
The program consists of eight classes and the main program. First, we will explain the purpose of each class on a general level.
Data content and game logic¶
The data content and game logic are located in the class GameEngine
.
An attribute of this class is vector< vector< shared_ptr<Drop> > >
board_
, which is the exact replica of the data structure implemented in
the previous version of the game.
So, the class GameEngine
stores the data using the classes
Drop
and Splash
.
This combination of three classes works similarly to the previous version
of this program.
Therefore, a Drop
object contains the information on the water amount
in the drop, and you can add water to it.
When the amount of water reaches its maximum, the drop will pop and
create four Splash
objects.
The Splash
objects know their location and direction, and they are able
to move one square in the right direction.
The most relevant difference to the previous version is that the
GameEngine
object
also includes the pointer to the object GameBoard
, which is included in
the subject coming up next, i.e. the graphical representation of the game board.
Graphical representation¶
In the previous version, the drop and splash objects stored their own
print characters, which means they took care of the representation.
The print characters are in Drop
and Splash
objects
also in this version.
The board of ascii characters from the previous version has been used in
debug printing of this version.
If you wish to view the debug prints, you can do it by removing the
comment character on line 125 of the method GameEngine::addWater
.
This line calls the method print
to the object itself.
See what happens in the console when you execute the program
with the debug prints.
In graphical user interfaces, the stored data is often separate from the representation. For example, on the previous programming course, we talked about the separation of data from its representation when we tried to find new ways to represent the points of a player in a graphical user interface instead of using a label object that contains the amount of points as a number.
The graphical representations of the objects Drop
and Splash
are in the classes DropItem
and SplashItem
.
Both of these classes have been inherited from the class
QGraphicsPixmapItem
.
Using the class QGraphicsPixmapItem
, you can show a picture in
the user interface and then, for example, easily move it around.
The moving properties are implemented in the base class.
The inherited class has all the properties of the base class.
This way, you can move SplashItem
objects on the screen when
splashes fly around the game board.
The DropItem
object including the graphical representation has
a pointer to the object Drop
, which contains similar data.
The Drop
object contains the information on the state of the drop,
which means, for example, the amount of water in the drop.
The DropItem
object is able to show a picture of a correct-sized
drop because it asks the object Drop
for its amount of water
through a pointer.
SplashItem
objects work similarly.
The drawing area that represents the game board is implemented
in the class GameBoard
.
This is, therefore, a graphical representation of the data stored
in GameEngine
.
The class GameBoard
has been inherited from the class QGraphicsScene
,
where you can show graphical drawing elements, such as objects of the type
QGraphicsPixmapItem
.
The objects DropItem
and SplashItem
have no knowledge
of the drawing area or the game board, which means they only
show themselves in the user interface and do nothing else.
The class SplashAnimation
is tightly connected with the
classes Splash
and SplashItem
.
We will get back to this later when we study the animation closer.
Big picture¶
As mentioned, the class GameEngine
contains the game logic.
In addition to that, GameEngine
contains the state of the program as
a whole.
The GameEngine
object tells all the other objects what to do during
the execution of the program.
First, the main program creates the GameBoard
object, which is a
drawing area representing the game board.
Then, it creates the GameEngine
object and the
main window, and gives them the drawing area as a parameter.
The main program also contains the line
QObject::connect(&scene, SIGNAL(mouseClick(int, int)), &engine, SLOT(addWater(int, int)));
This line connects the signal mouseClick
from the drawing
area to the slot addWater
of the game engine.
This means that when you click (x, y) in the graphical user interface,
the drawing area object sends a signal that is received by the game engine.
After all, the idea of the signal was that its sender does not need to know who or what receives it. The drawing area object does not know anything about the game engine object.
Communication between objects¶
In the Plussa material of the previous waterdrop game version, we explained
how the program works with the seed value 7 of the random number
generator when we want to pop a drop located in the coordinates (4, 11).
We will not go through the communication between the Drop
and
Splash
objects here because it is exactly the same as their communication
in the previous version.
If you wish, you can now revise the materials of the previous version.
If you want to set the seed number in a program with a graphical user
interface, edit line 20 of the file gameengine.hh
to set the desired
seed number.
When the user clicks mouse somewhere in the graphical user interface, the
object GameBoard
sends the signal mouseClick
.
The signal activates the slot addWater
of the object GameEngine
.
Let us now look at the program code and see how the program works in this
situation. (Open the file gameengine.cpp
and look at the function
addWater
.)
First, the game engine decreases the amount of water in the tank.
Depending on whether the water was added into an empty square or into a
square already containing a drop, the game engine either creates a new
drop object or adds water into an existing drop object.
The game engine asks the object GameBoard
to complete a corresponding
change in the drawing area, i.e. to execute either the method
removeDrop
or addDrop
.
Next, there is a do-while loop that contains the most essential
functionalities of the graphical user interface.
The loop structure calls in turn the method moveSplashes
of the
game engine itself and the method animate
of
GameBoard
.
This is very similar to the moving of splashes and printing of
the game board in the previous version of the game.
In the graphical user interface, the flight of splashes has been implemented
so that even though a splash can fly through several squares, its flight is
implemented as several one-square-long animations presented after one another.
The method moveSplashes
returned pointers to the drops that grew as
a consequence of the splash movement.
The same do-while loop also goes through all of the drops that are growing.
This can cause new pops, which will create new splashes, and so on.
Two separate coordinate systems¶
As an attribute of the class GameEngine
, board_
is a similar data
structure to what it was in the previous versions of the game.
Here, each coordinate (x, y) corresponds to exactly one element of
vector
, i.e. one square on the game board.
In other words, the smallest unit to be pointed to on the board is one square.
The game always uses the square coordinates when you edit the data content
of the game.
The smallest possible unit of the graphical representation is a pixel you see on the screen. Showing one square as one pixel is not reasonable, which is why we chose to show one square as a 50x50 pixel area. For example, the square (0,0) is represented here as the area marked as px=[0,50], py=[0,50].
Since the graphical representation will be done by the object GameBoard
,
it must be able to convert the square coordinates (x, y) into pixel
coordinates of the screen (px, py) and back.
In the GameBoard
object, the values of the pixel coordinate system are
always handled by objects of the type QPoint
that represent a single
pixel coordinate (px, py).
The calculations use the definition const int GRID_SIDE = 50
.
For example, the function mousePressEvent
in the file gameboard.cpp
handles such a situation that the user has clicked the
mouse on the graphical representation of the game board on the screen,
and the coordinates of that action – which we can access with the method
calls clickPosition.x()
and clickPosition.y()
– are given in pixel
coordinates (px, py).
The GameBoard
object makes the
division clickPosition.x() / GRID_SIDE
.
In C++, its result is an integer (integer division) that gives
the X coordinate of the clicked square.
The calculations are not difficult, but the only way to place all the elements in the right place on the screen is to draw a picture and calculate the locations of each of them. Therefore, a programmer needs mathematics. Especially discrete mathematics is useful.
Animation¶
Animation is started in the method GameEngine::moveSplashes
when the
object GameEngine
uses the method addSplash
to ask the
object GameBoard
to add an animation from point x to point y.
The GameEngine
object does not know how to do this,
but it is the graphical presentation’s job alone.
One attribute of the GameBoard
object is a group of animations
called animations_
, of the type QParallelAnimationGroup
.
It is a group of animations that are executed parallel to each other, i.e.
at the same time.
This could be compared to the previous version of the program, where all
existing splash objects were stored in a vector, and each of them was moved
forward by one step, all at the same time.
The method GameBoard::addSplash
creates a new
SplashAnimation
object that we add into the group animations_
.
The method
GameBoard::animate
then starts all the animations at the same time.
After that, there is the while loop, which waits for the
animations to finish.
The animations proceed as a series of events handled by QEventLoop
.
(We discussed the event handlers shortly on the previous programming course
in connection with event-based programming, which was
introduced after sequential programming.)
Summary¶
We did not get into all the parts of the program code in this material, and some Qt-themed parts of the program cannot be fully understood with the knowledge you have after this course. Nevertheless, we can see similarities between the implementation of the graphical user interface and the earlier waterdrop game versions. The functionality of the program has many features familiar from them.
Qt is a very large library that enables even complicated programs to be implemented efficiently. We will get into it in more detail on the next programming course, but using only the learning material of these courses, you can only get a superficial understanding of Qt. This course has offered you the foundation for starting to use Qt independently. You already know the waterdrop game from the earlier versions. We hope this makes examining it with Qt a little easier.
Implementing the waterdrop game in three stages also gave you an example of how to develop programs in stages. You have now the possibility to develop the even better fourth version yourself.