Game 模块是游戏的运行逻辑模块。由于这一部分和 Qt界面的代码完全独立,大家可以完全可以自己采用自己喜欢的算法重新实现。
我实现的时候是这样设计的(请不必拘泥于此)
-
我希望有一个4x4的数组用于存放每一轮游戏中小方块的值,
int board[4][4]; -
游戏初始化的时候需要随机将两个小方块赋值为 2
-
设计一个
move函数,接收一个表示方向的参数(用于指示上下左右),然后更新 4x4 小方块数组的值。-
有一个成员
changed, 用于判断move前后4x4表盘是否变化。 -
设计一个
getEmptyPos函数,这样move结束后,如果表盘发生变化,可以随机获取一个空方块,将其赋值为 2。 -
有一个成员
total_score,每次move后都会更新分数。
-
类似前两步骤,我们添加一个 Game 类。配置时啥也不需要勾选。
然后我们复制粘贴如下代码(点开小三角形可以查看代码)。
game.h
#ifndef GAME_H
#define GAME_H
#include <array>
const int NCells = 4;
using Pos = std::array<int, 2>;
class Game
{
public:
int board[NCells][NCells];
int total_score = 0;
bool changed;
Game();
void move(bool horizonal, bool reverse);
void resetGame();
Pos getEmptyPos();
bool isfull();
bool wonGame();
};
#endif // GAME_Hgame.cpp
#include <algorithm>
#include <cstring>
#include <vector>
#include "game.h"
Game::Game()
{
resetGame();
}
void Game::resetGame()
{
memset(board, 0, sizeof board);
board[rand()%NCells][rand()%NCells] = 2;
auto pos = getEmptyPos();
board[pos[0]][pos[1]] = 2;
total_score = 0; changed = true;
}
Pos Game::getEmptyPos()
{
int i,j;
do {
i = rand() % NCells;
j = rand() % NCells;
} while (board[i][j]);
return {i, j};
}
bool Game::isfull()
{
return std::all_of(*board, *board + NCells * NCells, [](int x) { return x != 0; } );
}
void Game::move(bool horizonal, bool reverse)
{
static int board_prev[NCells][NCells];
memcpy(board_prev, board, sizeof board); //backup
using Vec = std::vector<int> ;
auto squeezeVec = [&] (Vec v, bool reverse) -> Vec {
if (reverse) v = Vec(v.rbegin(), v.rend());
Vec ans; size_t first = 0;
while (first+1 < v.size()) {
if (v[first] == v[first+1]) {
total_score += v[first] * 2;
ans.push_back(v[first] * 2), first += 2;
}
else ans.push_back(v[first]), first++;
}
if (first+1 == v.size()) ans.push_back(v[first]);
while (ans.size() < NCells) ans.push_back(0);
return reverse ? Vec(ans.rbegin(), ans.rend()) : ans;
};
auto getVal = [&] (size_t i, size_t j) -> int& {
return horizonal ? board[i][j] : board[j][i];
};
for (size_t i=0; i < NCells; ++i) {
Vec v;
for (size_t j=0; j < NCells; ++j) {
if (getVal(i, j)) v.push_back(getVal(i, j));
}
auto ans = squeezeVec(v, reverse);
for (size_t j=0; j < NCells; ++j) {
getVal(i, j) = ans[j];
}
}
changed = !std::equal(*board, *board + NCells * NCells, *board_prev );
}以上是 Game 模块-游戏逻辑模块的源代码,由于我用到了很多小技巧和C++11的知识,因此我们不会讲解其中的代码细节。你完全可以根据自己的想法重新写一遍。
我们在 GameBoard 中增加一个 Game 对象,Game 对象可以提供如下操作
- 每一轮的 4x4 小方块 的值
board[4][4] - 每一轮的总分
total_score - 一个
move函数,接收一个指示上下左右方向的参数,调用后会更新 4x4 小方块 值 - 检测每一轮后表盘是否变化了
changed - 返回一个空方块的位置
getEmptyPos()
此外我们在 GameBoard 还增加了 Cell* cells[4][4] {}; 用于存放 4x4 小方块的界面的指针。
我们还增加一个 keyPressEvent 函数,用于监听键盘按键事件,然后触发不同的游戏更新方式。
gameboard.h
#ifndef GAMEBOARD_H
#define GAMEBOARD_H
#include <QLabel>
#include <QVBoxLayout>
#include <QGridLayout>
#include <QKeyEvent>
#include "game.h"
#include "cell.h"
class GameBoard : public QWidget
{
Q_OBJECT
public:
explicit GameBoard(QWidget *parent = 0);
private:
// main game logic
Game game;
Cell* cells[NCells][NCells] {};
// main layout
QVBoxLayout *mainLayout;
// grid layout of board
QGridLayout *boardLayout;
// score widget
QLabel *score;
void drawBoard();
protected:
void keyPressEvent(QKeyEvent *event);
};
#endif // GAMEBOARD_Hgameboard.cpp
#include "gameboard.h"
#include "cell.h"
#include <cstdlib>
GameBoard::GameBoard(QWidget *parent) :
QWidget(parent)
{
// set default size
resize(650, 450);
// create the main layout
mainLayout = new QVBoxLayout();
setLayout(mainLayout);
// create the board layout
boardLayout = new QGridLayout();
drawBoard();
mainLayout->insertLayout(0, boardLayout);
// create the score widget and add it to the board
score = new QLabel(QString("SCORE: %1").arg(0));
score->setStyleSheet("QLabel { color: rgb(235,224,214); font: 16pt; }");
score->setFixedHeight(50);
mainLayout->insertWidget(1, score, 0, Qt::AlignRight);
// style sheet of the board
setStyleSheet("GameBoard { background-color: rgb(187,173,160) }");
}
void GameBoard::drawBoard()
{
delete boardLayout;
boardLayout = new QGridLayout();
for (int i = 0; i < NCells; ++i) {
for (int j = 0; j < NCells; ++j) {
delete cells[i][j];
cells[i][j] = new Cell(game.board[i][j]);
boardLayout->addWidget(cells[i][j], i, j);
cells[i][j]->draw();
}
}
mainLayout->insertLayout(0, boardLayout);
}
void GameBoard::keyPressEvent(QKeyEvent *event)
{
switch (event->key()) {
case Qt::Key_Up:
game.move(false, false);
break;
case Qt::Key_Down:
game.move(false, true);
break;
case Qt::Key_Left:
game.move(true, false);
break;
case Qt::Key_Right:
game.move(true, true);
break;
}
if (game.changed) {
if (!game.isfull()) {
auto pos = game.getEmptyPos();
game.board[pos[0]][pos[1]] = 2;
}
score->setText(QString("SCORE: %1").arg(game.total_score));
drawBoard();
}
}
GameBoard 继承自 QWidget,因此可以覆写 keyPressEvent 函数, 我们希望根据键盘按键的上下左右触发 不同的 game.move(bool, bool) 函数。
// gameboard.cpp
...
void GameBoard::keyPressEvent(QKeyEvent *event)
{
switch (event->key()) {
case Qt::Key_Up:
game.move(false, false);
break;
case Qt::Key_Down:
game.move(false, true);
break;
case Qt::Key_Left:
game.move(true, false);
break;
case Qt::Key_Right:
game.move(true, true);
break;
}
...
}最终效果如下
你可以敲击键盘上的上下左右键来控制方块移动。不妨自己试着玩一会吧。
我没有设计游戏最终失败后的逻辑,也没有设计游戏成功达到 2048 后的逻辑,你可以自己尝试一下。
至此,教程就基本结束了。你已经初步了解了 Qt 设计界面的方式和模块化开发的大体流程。
Qt 在网上的教程还有很多,开源的 demo 项目也是不胜枚举。接下来你可以拉上小伙伴一起玩,共同讨论、尝试设计不一样的 Qt 界面或者游戏。

