Game Programming using Qt 5 Beginner's Guide
上QQ阅读APP看书,第一时间看更新

Time for action – Functionality of a tic-tac-toe board

We need to implement a function that will be called upon by clicking on any of the nine buttons on the board. It has to change the text of the button that was clicked on—either "X" or "O"—based on which player made the move. It then has to check whether the move resulted in the game being won by the player (or a draw if no more moves are possible), and if the game ended, it should emit an appropriate signal, informing the environment about the event.

When the user clicks on a button, the clicked() signal is emitted. Connecting this signal to a custom slot lets us implement the mentioned functionality, but since the signal doesn't carry any parameters, how do we tell which button caused the slot to be triggered? We could connect each button to a separate slot, but that's an ugly solution. Fortunately, there are two ways of working around this problem. When a slot is invoked, a pointer to the object that caused the signal to be sent is accessible through a special method in QObject, called sender(). We can use that pointer to find out which of the nine buttons stored in the board list is the one that caused the signal to fire:

void TicTacToeWidget::someSlot() {
    QPushButton *button = static_cast<QPushButton*>(sender());
    int buttonIndex = m_board.indexOf(button);
    // ...
} 

While sender() is a useful call, we should try to avoid it in our own code as it breaks some principles of object-oriented programming. Moreover, there are situations where calling this function is not safe. A better way is to use a dedicated class called QSignalMapper, which lets us achieve a similar result without using sender() directly. Modify the constructor of TicTacToeWidget, as follows:

QGridLayout *gridLayout = new QGridLayout(this);
QSignalMapper *mapper = new QSignalMapper(this);
for(int row = 0; row < 3; ++row) {
    for(int column = 0; column < 3; ++column) {
        QPushButton *button = new QPushButton(" ");
        gridLayout->addWidget(button, row, column);
        m_board.append(button);
        mapper->setMapping(button, m_board.count() - 1);
        connect(button, SIGNAL(clicked()), mapper, SLOT(map()));
    }
}
connect(mapper, SIGNAL(mapped(int)),
        this,   SLOT(handleButtonClick(int)));

Here, we first created an instance of QSignalMapper and passed a pointer to the board widget as its parent so that the mapper is deleted when the widget is deleted.

Almost all subclasses of QObject can receive a pointer to the parent object in the constructor. In fact, our MainWindow and TicTacToeWidget classes can also do that, thanks to the code Qt Creator generated in their constructors. Following this rule in custom QObject-based classes is recommended. While the parent argument is often optional, it's a good idea to pass it when possible, because objects will be automatically deleted when the parent is deleted. However, there are a few cases where this is redundant, for example, when you add a widget to a layout, the layout will automatically set the parent widget for it.

Then, when we create buttons, we "teach" the mapper that each of the buttons has a number associated with it—the first button will have the number 0, the second one will be bound to the number 1, and so on. By connecting the clicked() signal from the button to the mapper's map() slot, we tell the mapper to process that signal. When the mapper receives the signal from any of the buttons, it will find the mapping of the sender of the signal and emit another signal—mapped()—with the mapped number as its parameter. This allows us to connect to that signal with a new slot (handleButtonClick()) that takes the index of the button in the board list.

Before we create and implement the slot, we need to create a useful enum type and a few helper methods. First, add the following code to the public section of the class declaration in the tictactoewidget.h file:

enum class Player {
    Invalid, Player1, Player2, Draw
};
Q_ENUM(Player)

This enum lets us specify information about players in the game. The Q_ENUM macro will make Qt recognize the enum (for example, it will allow you to pass the values of this type to qDebug() and also make serialization easier). Generally, it's a good idea to use Q_ENUM for any enum in a QObject-based class.

We can use the Player enum immediately to mark whose move it is now. To do so, add a private field to the class:

Player m_currentPlayer; 

Don't forget to give the new field an initial value in the constructor:

m_currentPlayer = Player::Invalid;

Then, add the two public methods to manipulate the value of this field:

Player currentPlayer() const 
{
return m_currentPlayer;
}
void setCurrentPlayer(Player p) { if(m_currentPlayer == p) { return; } m_currentPlayer = p; emit currentPlayerChanged(p); }

The last method emits a signal, so we have to add the signal declaration to the class definition along with another signal that we will use:

signals:
    void currentPlayerChanged(Player);
    void gameOver(Player); 
We only emit the currentPlayerChanged signal when the current player really changes. You always have to pay attention that you don't emit a "changed" signal when you set a value to a field to the same value that it had before the function was called. Users of your classes expect that if a signal is called changed, it is emitted when the value really changes. Otherwise, this can lead to an infinite loop in signal emissions if you have two objects that connect their value setters to the other object's changed signal.

Now it is time to implement the slot itself. First, declare it in the header file:

private slots:
    void handleButtonClick(int index);

Use  Alt Enter to quickly generate a definition for the new method, as we did earlier.

When any of the buttons is pressed, the handleButtonClick() slot will be called. The index of the button clicked on will be received as the argument. We can now implement the slot in the .cpp file:

void TicTacToeWidget::handleButtonClick(int index)
{
    if (m_currentPlayer == Player::Invalid) {
        return; // game is not started
    }
    if(index < 0 || index >= m_board.size()) {
        return; // out of bounds check
    }
    QPushButton *button = m_board[index];
    if(button->text() != " ") return; // invalid move
    button->setText(currentPlayer() == Player::Player1 ? "X" : "O");
    Player winner = checkWinCondition();
    if(winner == Player::Invalid) {
        setCurrentPlayer(currentPlayer() == Player::Player1 ?
                         Player::Player2 : Player::Player1);
        return;
    } else {
        emit gameOver(winner);
    }
}

Here, we first retrieve a pointer to the button based on its index. Then, we check whether the button contains an empty space—if not, then it's already occupied, so we return from the method so that the player can pick another field in the board. Next, we set the current player's mark on the button. Then, we check whether the player has won the game. If the game didn't end, we switch the current player and return; otherwise, we emit a gameOver() signal, telling our environment who won the game. The checkWinCondition() method returns Player1, Player2, or Draw if the game has ended, and Invalid otherwise. We will not show the implementation of this method here, as it is quite lengthy. Try implementing it on your own, and if you encounter problems, you can see the solution in the code bundle that accompanies this book.

The last thing we need to do in this class is to add another public method for starting a new game. It will clear the board and set the current player:

void TicTacToeWidget::initNewGame() {
    for(QPushButton *button: m_board) {
        button->setText(" ");
    }
    setCurrentPlayer(Player::Player1);
}

Now we only need to call this method in the MainWindow::startNewGame method:

void MainWindow::startNewGame()
{
    ui->player1Name->setText(tr("Alice"));
    ui->player2Name->setText(tr("Bob"));
    ui->gameBoard->initNewGame();
}

Note that ui->gameBoard actually has a TicTacToeWidget * type, and we can call its methods even though the form editor doesn't know anything specific about our custom class. This is the result of the promoting that we did earlier.

It's time to see how all this works together! Run the application, click on the Start new game button, and you should be able to play some tic-tac-toe.