今天我们尝试用Qt + C++实现以前玩过的一个小游戏,规则很简单,在一个圈上均匀分布着八盏灯,点击其中一盏就会同时切换它与它对面两盏灯(即它正对面灯的左右相邻两盏)的状态,将所有灯点亮即为游戏胜利。使用VS2022 + Qt5.15.2开发。
本文不光是为了过一遍开发流程,让读者(也让笔者)熟悉一下Qt VS开发的一些流程操作,更重要的是笔者想表达整个开发过程中一定要模拟用户(玩家)视角来设计软件的流程,因为操作界面讲究的就是用户的交互性,如果只站在开发者便利的角度设计软件,最终的成品会出现很多反直觉的操作方式和逻辑。
下面开始我们的开发过程。
首先创建Qt工程,使用designer设计界面,注意善用布局,这样既能保证界面窗口缩放不影响控件显示,还能适应不同分辨率的屏幕。我们设计如下的界面,尽量使用栅格布局,以及水平垂直布局加弹簧来调整间隔。
使用layoutStretch参数来调整某布局里面的子布局之间所占的空间比例,默认0,0就是一比一。
注意对需要的控件或窗口进行尺寸(或最小尺寸)的固定,以保证其维持完整的形态,比如我们最关键的放灯和操作按钮的widget,我们固定尺寸并使用打破布局,来自由移动调整其中控件的位置,使其看起来位于一个圆上。
接着是载入资源文件的部分,我们先收集好需要的素材,这里共一张gif(作为logo),以及表示灯开关两种状态的两个图标,logo我们直接在网上找合适的gif使用,灯的图标这里选择用电脑画图自己画了两个,大家可以自行决定。我们把所有的资源放在同一个目录下,在VS解决方案资源管理器中找到资源文件.qrc打开,向其中add我们全部的资源。
我们可以选择在designer中直接为控件加载资源,但这只能影响软件开启时的外观,因为游戏进行中我们的代码会实时更新每个控件显示的图片(logo除外),我们可以在designer中为每个控件加载资源如下
要注意gif图像在designer中以pixmap形式加载的话并不能动起来,所以我们需要在代码的初始化部分使用QMovie加载让它动起来,详见代码部分。
至此界面基本完成。
下面开始代码部分,首先捋一遍逻辑:我们的思路是初始将所有八个操作按钮禁用,按下start按钮后游戏开始,此时随机点亮八个灯中的若干盏,并解禁所有的按钮,初始化倒计时的定时器,初始化步数,接下来开始玩家的操作时间。
玩家可在八个按钮中一次选择一个点击,点击后会同时切换本身所在与对面两盏共三盏灯的亮灭,并使步数+1,要注意需要一个state数组记录所有灯的状态,每次点击后需要判断是否满足胜利条件。
倒计时用一个定时器实现,归零时判定为游戏失败,游戏失败和胜利都需要弹出提示,并重新禁用所有操作按钮,知道玩家下一次点击start开始一局游戏。
下面展示全部头文件(LightGame.h)内容,我们所需的变量及函数的声明都在其中。
#pragma once
#include <QtWidgets/QMainWindow>
#include "ui_LightGame.h"
#include <vector>
#include <qmovie.h>
#include <qtimer.h>
#include <time.h>
#include <qmessagebox.h>
using namespace std;
class LightGame : public QMainWindow
{
Q_OBJECT
public:
LightGame(QWidget *parent = Q_NULLPTR);
private:
Ui::LightGameClass ui;
QTimer* timer_countDown;
int g_count = 0;//记录倒计时
int g_steps = 0;//记录步数
vector<QPushButton*> buttons;//按钮组,总共八个按钮,放到数组里方便循环操作,减少冗余代码量
vector<QLabel*> labels;//图标组,同上
vector<int> state;//记录状态判定游戏胜利
QImage* on;//加载亮的灯资源
QImage* off;//加载灭的灯资源
void rev(int n0, int n1, int n2);//每次点击按钮后,调用该函数对点击处的灯及对面两个灯进行反转
bool checkWin();//每次点击按钮后,调用该函数检查是否满足胜利条件
void showWin();//胜利后调用该函数,提示用户胜利并禁用操作按钮,直到点击start后开始一局新的游戏
void showLost();//超时失败后调用该函数,同上
void lightOP(int index, bool turnOn);//具体对一盏灯的操作,第一个参数为灯的序号,第二个参数为亮或者灭
private slots:
void countDown();//计时器槽函数
//按钮槽函数,新版qt支持将函数名定义为如下的格式,省去connect操作
void on_button_start_clicked();
void on_pushButton_1_clicked();
void on_pushButton_2_clicked();
void on_pushButton_3_clicked();
void on_pushButton_4_clicked();
void on_pushButton_5_clicked();
void on_pushButton_6_clicked();
void on_pushButton_7_clicked();
void on_pushButton_8_clicked();
};
下面是源文件部分,初始化加载资源部分的代码如下:
//加载logo
QMovie* logo = new QMovie(QString::fromUtf8(":/LightGame/x64/Debug/qrc/fire.gif"));
ui.label_logo->setMovie(logo);
logo->start();
ui.label_logo->show();
//加载图标
on = new QImage(QString::fromUtf8(":/LightGame/x64/Debug/qrc/on.jpg"));
off = new QImage(QString::fromUtf8(":/LightGame/x64/Debug/qrc/off.jpg"));
当按下start按钮开始游戏后,我们需要做的事包括:重置倒计时和步数,解禁按钮,随机点亮几个灯。其中随机点亮灯我们要使用随机数实现。
//随机点灯的思路是生成一个二进制八位随机数,其每一位对应一盏灯
srand(time(nullptr));
int randoxNumber = rand() % 256;
for (int i = 0; i < 8; ++i) {
if ((randoxNumber >> i) & 1) {
lightOP(i, true);
state[i] = 1;
}
else {
lightOP(i, false);//注意不点亮的灯也要初始化
state[i] = 0;
}
}
下面直接放出源文件(LightGame.cpp)全部内容,相关函数的作用及调用逻辑在头文件的注释中已说清楚。
#include "LightGame.h"
#define COUNTDOWN 60
//using namespace std;
LightGame::LightGame(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
//加载logo
QMovie* logo = new QMovie(QString::fromUtf8(":/LightGame/x64/Debug/qrc/fire.gif"));
ui.label_logo->setMovie(logo);
logo->start();
ui.label_logo->show();
//加载图标
on = new QImage(QString::fromUtf8(":/LightGame/x64/Debug/qrc/on.jpg"));
off = new QImage(QString::fromUtf8(":/LightGame/x64/Debug/qrc/off.jpg"));
timer_countDown = new QTimer();
connect(timer_countDown, SIGNAL(timeout()), this, SLOT(countDown()));
g_count = COUNTDOWN;
g_steps = 0;
buttons = { ui.pushButton_1, ui.pushButton_2, ui.pushButton_3, ui.pushButton_4, ui.pushButton_5, ui.pushButton_6, ui.pushButton_7, ui.pushButton_8 };
labels = { ui.label_1, ui.label_2, ui.label_3, ui.label_4, ui.label_5, ui.label_6, ui.label_7, ui.label_8 };
state = { 0, 0, 0, 0, 0, 0, 0, 0 };
}
//定时器timeout槽函数,实现倒计时
void LightGame::countDown() {
g_count--;
if (g_count == 0) {
timer_countDown->stop();
showLost();//超时结束
}
ui.label_time->setText(QStringLiteral("剩余") + QString::number(g_count) + QStringLiteral("秒"));
}
//按下开始按钮,要做的事包括:重置倒计时和步数,解禁按钮,随机点亮几个灯
void LightGame::on_button_start_clicked() {
g_count = COUNTDOWN;
timer_countDown->start(1000);
g_steps = 0;
ui.label_steps->setText(QStringLiteral("已用") + QString::number(g_steps) + QStringLiteral("步"));
for (int i = 0; i < 8; ++i) {
buttons[i]->setEnabled(true);
}
//随机点灯的思路是生成一个二进制八位随机数,其每一位对应一盏灯
srand(time(nullptr));
int randoxNumber = rand() % 256;
for (int i = 0; i < 8; ++i) {
if ((randoxNumber >> i) & 1) {
lightOP(i, true);
state[i] = 1;
}
else {
lightOP(i, false);//注意不点亮的灯也要初始化
state[i] = 0;
}
}
}
void LightGame::rev(int n0, int n1, int n2) {
ui.label_steps->setText(QStringLiteral("已用") + QString::number(++g_steps) + QStringLiteral("步"));
state[n0] = state[n0] ^ 1;
lightOP(n0, state[n0]);
state[n1] = state[n1] ^ 1;
lightOP(n1, state[n1]);
state[n2] = state[n2] ^ 1;
lightOP(n2, state[n2]);
}
bool LightGame::checkWin() {
bool res = true;
for (int i = 0; i < 8; ++i) {
if (!state[i]) res = false;
}
return res;
}
void LightGame::showWin() {
for (int i = 0; i < 8; ++i) {
buttons[i]->setEnabled(false);
}
timer_countDown->stop();
//使用QStringLiteral代替QString以保证中文显示不是乱码
QString dlgTitle = QStringLiteral("Win!");
//QTextCodec::setCodecForCStrings(QTextCodec::codecForName(“GB2312”));
QString strInfo = QStringLiteral("游戏获胜,用时") + QString::number(60 - g_count) + QStringLiteral("秒,花费") + QString::number(g_steps) + QStringLiteral("步");
QMessageBox::about(this, dlgTitle, strInfo);
}
void LightGame::showLost() {
for (int i = 1; i < 9; ++i) {
buttons[i]->setEnabled(false);
}
QString dlgTitle = "Lost!";
QString strInfo = QStringLiteral("游戏失败,时间耗尽!");
QMessageBox::about(this, dlgTitle, strInfo);
}
void LightGame::lightOP(int index, bool turnOn) {
if (turnOn) labels[index]->setPixmap(QPixmap::fromImage(*on));
else labels[index]->setPixmap(QPixmap::fromImage(*off));
labels[index]->setScaledContents(true);
}
//pushButtons
void LightGame::on_pushButton_1_clicked() {
rev(0, 3, 5);
if (checkWin()) showWin();
}
void LightGame::on_pushButton_2_clicked() {
rev(1, 4, 6);
if (checkWin()) showWin();
}
void LightGame::on_pushButton_3_clicked() {
rev(2, 5, 7);
if (checkWin()) showWin();
}
void LightGame::on_pushButton_4_clicked() {
rev(3, 6, 0);
if (checkWin()) showWin();
}
void LightGame::on_pushButton_5_clicked() {
rev(4, 7, 1);
if (checkWin()) showWin();
}
void LightGame::on_pushButton_6_clicked() {
rev(5, 0, 2);
if (checkWin()) showWin();
}
void LightGame::on_pushButton_7_clicked() {
rev(6, 1, 3);
if (checkWin()) showWin();
}
void LightGame::on_pushButton_8_clicked() {
rev(7, 2, 4);
if (checkWin()) showWin();
}
本文主要内容基本结束,后续我们会尝试对软件进行打包以及生成安装包操作。
用到的资源文件的ui文件链接如下:提取码:0ruj