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/12/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].

The square (5,4) is represented as the area marked as px=[200,250], py=[150,200]

The square (5,4) is represented as the area marked as px=[200,250], py=[150,200]

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.