用Qt写一个迷宫游戏
一段时间以前,我用c写了个使用随机数生成迷宫的代码,现在在自学学Qt,于是就想到了利用这个代码改一个小游戏。
现在记录一下自己的学习过程,也算抛砖引玉,各位看官请多指教。
话不多说现在开始。
使用的是Qt的IDE:QT Creater
step 1:把迷宫生成的c代码用c++类改写
为了方便,先把代码改写成类,诸位有兴趣可以拿去一玩。
代码丑了点,但是能用就行了。
maze.h
#ifndef MAZE_H
#define MAZE_H
/**************************************************
* -1:边界 0:墙壁
* 1:空单元(初始化后未判定的单元)
* 2:待定墙壁(新建已定单元时建立)
* 3:已定单元(确定迷宫树已延伸到的单元)
* 4:打通墙壁(延伸操作)
* 5.起点
* 6.终点
* 7.已经过路径
***************************************************/
class maze {
private:
int level; //迷宫阶数
int** map; //地图存储空间
int start_x, start_y; //起终点
//生成基础地图(单元格)
void base();
//使点的周围设为待定(2)
void _2(int i, int j);
//设定迷宫开始延伸的起点
void start() ;
//循环停止判定(是否存在未判定的区域)
bool judge();
//操作(如果相邻空单元(1)则打通(变为4),如果不相邻空单元则为墙壁(0))
void op(int i, int j);
//随机选择一个待定墙壁判断并操作
void random2();
public:
//构造函数申请内存空间
maze(int in_level);
~maze();
//获取地图
int getlevel() ;
int** getmap();
int getside();
//生成地图
void makemap();
int p_x, p_y; //当前位置
//重置地图
void rebuildmap();
int* operator[](int index);
};
#endif // MAZE_H
maze.cpp
#include "maze.h"
//生成基础地图(单元格)
void maze::base() {
for (int i = 0; i < level * 2 + 1; i++) {
for (int j = 0; j < level * 2 + 1; j++) {
if (i == 0 || j == 0 || i == level * 2 + 1 - 1 ||
j == level * 2 + 1 - 1) {
map[i][j] = -1;
} else if (i % 2 != 0 && j % 2 != 0) {
map[i][j] = 1;
} else {
map[i][j] = 0;
}
}
}
}
//使点的周围设为待定(2)
void maze:: _2(int i, int j) {
if (map[i - 1][j] == 0) {
map[i - 1][j] = 2;
}
if (map[i + 1][j] == 0) {
map[i + 1][j] = 2;
}
if (map[i][j - 1] == 0) {
map[i][j - 1] = 2;
}
if (map[i][j + 1] == 0) {
map[i][j + 1] = 2;
}
}
//设定迷宫开始延伸的起点
void maze:: start() {
map[start_x][start_y] = 5;
_2(start_x, start_y);
}
//循环停止判定(是否存在未判定的区域)
bool maze:: judge() {
bool flag = 0;
for (int i = 0; i < level * 2 + 1; i++) {
for (int j = 0; j < level * 2 + 1; j++) {
if (map[i][j] == 2) {
flag = 1;
return flag;
}
}
}
return flag;
}
//操作(如果相邻空单元(1)则打通(变为4),如果不相邻空单元则为墙壁(0))
void maze:: op(int i, int j) {
if ((map[i - 1][j] == 3 || map[i - 1][j] == 5) && map[i + 1][j] == 1) {
map[i][j] = 4;
map[i + 1][j] = 3;
_2(i + 1, j);
start_x = i + 1;
start_y = j;
} else if ((map[i][j - 1] == 3 || map[i][j - 1] == 5) &&
map[i][j + 1] == 1) {
map[i][j] = 4;
map[i][j + 1] = 3;
_2(i, j + 1);
start_x = i;
start_y = j + 1;
} else if ((map[i + 1][j] == 3 || map[i + 1][j] == 5) &&
map[i - 1][j] == 1) {
map[i][j] = 4;
map[i - 1][j] = 3;
_2(i - 1, j);
start_x = i - 1;
start_y = j;
} else if ((map[i][j + 1] == 3 || map[i][j + 1] == 5) &&
map[i][j - 1] == 1) {
map[i][j] = 4;
map[i][j - 1] = 3;
_2(i, j - 1);
start_x = i;
start_y = j - 1;
} else {
map[i][j] = 0;
}
}
//随机选择一个待定墙壁判断并操作
void maze:: random2() {
int t = 0;
for (int i = 0; i < level * 2 + 1; i++) {
for (int j = 0; j < level * 2 + 1; j++) {
if (map[i][j] == 2) {
t++;
}
}
}
int k = rand() % t + 1;
t = 0;
for (int i = 0; i < level * 2 + 1; i++) {
for (int j = 0; j < level * 2 + 1; j++) {
if (map[i][j] == 2) {
t++;
if (t == k) {
op(i, j);
goto loopout;
}
}
}
}
loopout:
if (!judge()) {
map[start_x][start_y] = 6;
}
}
//构造函数申请内存空间
maze:: maze(int in_level) : level(in_level) {
map = new int* [level * 2 + 1];
for (int i = 0; i < level * 2 + 1; i++) {
map[i] = new int[level * 2 + 1];
}
start_x = 1, start_y = 1; //起点设置为(1,1)
}
maze::~maze() {
for (int i = 0; i < level * 2 + 1; i++) {
delete [] map[i];
}
delete [] map;
}
//获取地图
int maze:: getlevel() {
return maze::level;
}
int** maze::getmap() {
return map;
}
int maze::getside() {
return level * 2 + 1;
}
//生成地图
void maze:: makemap() {
p_x = start_x;
p_y = start_y;
base();
start();
int a = 0;
while (judge()) {
a++;
random2();
// if (a % 30 == 0) {
// printarr(map, level);
// system("PAUSE");
// }
}
}
int* maze::operator[](int index) {
return map[index];
}
//重置地图
void maze::rebuildmap(){
start_x=1;
start_y=1;
makemap();
}
好现在就可以生成一个迷宫了。
我把一开始的栅格大小叫做迷宫的阶数,现在可以通过构造函数生成特定特定阶数的迷宫,这阶数就可以变相的看作难度。
step 2:创建ui
首先我们创建一个Qt widgets application 项目,叫maze好了。
类名就叫mazeWidget,然后基类选择QWidget,剩下默认即可。
然后修改mazewidget.ui。
ui
先把主窗口字体改成14号。
拖入7个PushButton,5个Label,三个Spacer。
然后分别把对象名(objectName)、显示文本(text)改成:
QPushButton:
start_btn 开始游戏
stop_ptn 暂停
end_btn 终止游戏
rule_btn 规则
setting_btn 设置
battle_btn 双人对战
about_btn 关于
QLabel:
plaque 迷宫游戏
plaque_time 时间:
time_value 0
plaque_grade 分数:
grade_value 0
然后修改对象属性,其中暂停、终止游戏、双人对战按钮默认禁用(enabled = false)。
各个标签文字水平、垂直分别居中显示(修改alignment)。
然后选中这些按钮,应用垂直的布局器。
排列成这个样子:
现在再拖入一个Frame、一个ProgressBar,也应用垂直布局器。
ProgressBar隐藏文字(textVisible = false)。
Frame设置边框形状(frameShape)为Box,边框阴影(frameShadow)为Raised。
最后给整个窗口应用水平布局器,效果应该是这样的:
这里先不用管界面风格
ok,那么到此ui就完成了,接下来我们来写逻辑。
step 3:窗口逻辑
第一步:在项目中加入我们的迷宫类文件。
加入好后如图所示:
第二步:我们的窗口类头文件的编写
首先是头文件的加入:
#include <QMessageBox> //Qt信息窗口头文件
#include <QPainter> //Qt绘图头文件
#include <QDebug> //QtDebug头文件
#include <QKeyEvent> //Qt按键事件头文件
#include <QTimer> //Qt计时器头文件
#include <QInputDialog> //Qt输入对话框头文件
#include "maze.h" //迷宫类头文件
其次是函数的声明:
protected:
void paintEvent(QPaintEvent*); //绘图事件
void keyPressEvent(QKeyEvent*); //按键按下事件
private slots:
void on_start_btn_clicked(); //|
void on_stop_ptn_clicked(); //|
void on_end_btn_clicked(); //|
void on_rule_btn_clicked(); //|各按钮点击槽函数
void on_setting_btn_clicked(); //|
void on_battle_btn_clicked(); //|
void on_about_btn_clicked(); //|
void time_update(); //时间更新槽函数
其中绘图时间与按键按下事件是继承自QWidget类的虚函数,实现了绘制迷宫地图和响应键盘按下的功能,绘图事件函数在窗口移动、改变大小、调用repaint()函数等情况是自动调用,键盘按下事件函数在键盘按下时调用,这两个函数名称不可以修改。
之后的7个函数是我们ui按钮点击时调用的槽函数,可以通过设计界面右击对应的按钮,点击转到槽来自动生成函数的声明和实现(定义)的框架,在这里是自动生成的函数名,不可以更改。
最后是我们自己定义的时间更新的槽函数,在计时器(QTimer)发出timeout()信号时调用,由于是自己定义的函数,名称可以修改,只要在代码中做对应修改就可以,需要手动对信号和槽进行链接。
最后是窗口类成员变量的声明:
private:
Ui::mazeWidget* ui; //ui对象指针
maze* map; //迷宫对象指针
bool painting_switch; //绘图开关
bool timing_switch; //计时开关
bool keybord_switch; //键盘响应开关
bool stop_switch; //暂停按钮状态
int grade; //分数
int time; //时间
QTimer* timer; //计时器对象指针
其中ui对象指针不需要我们写,Qt自动生成。
迷宫对象与计时器对象指针用来实现迷宫与计时对应功能。
剩余变量用来存储游戏运行中状态与数据。
那么好,窗口类头文件的编写就完成了。
接下来是各函数的实现
第三步:窗口类源文件的编写
首先需要添加头文件"mazewidget.h"
#include "mazewidget.h"
#include "ui_mazewidget.h"
接下来开始挨个编写函数:
1.构造函数
#include "mazewidget.h"
#include "ui_mazewidget.h"
mazeWidget::mazeWidget(QWidget* parent)
: QWidget(parent)
, ui(new Ui::mazeWidget), map(new maze(20)), painting_switch(false), timing_switch(false)
, keybord_switch(false), grade(0), time(0) {
//TODO:状态栏
ui->setupUi(this);
ui->progressBar->setVisible(false); //初始隐藏进度条
ui->end_btn->setEnabled(false); //设置终止按钮禁用
ui->stop_ptn->setEnabled(false); //设置暂停按钮禁用
ui->grade_value->setText(" "); //设置分数值显示为空
ui->time_value->setText(" "); //设置时间值显示为空
map->makemap(); //生成地图
timer = new QTimer(this); //初始化计时器
connect(timer, &QTimer::timeout, this, &mazeWidget::time_update); //链接时间更新信号与槽
}
2.析构函数
加入两行:
mazeWidget::~mazeWidget() {
delete ui;
delete map; //1.
delete timer; //2.
}
3.绘图事件虚函数
void mazeWidget::paintEvent(QPaintEvent*) {
if(!painting_switch) return;
QPainter painter(this); //画笔对象
//绘图逻辑:
int perblock = (std::min(ui->frame->width(), ui->frame->height()) - 20) / (map->getside());
int start_x = ui->frame->x() + (ui->frame->width() - (ui->frame->x() + (map->getside()) * perblock)) / 2;
int strat_y = ui->frame->y() + (ui->frame->height() - (ui->frame->y() + (map->getside()) * perblock)) / 2;
for(int i = 0; i < map->getlevel() * 2 + 1; i++) {
for(int j = 0; j < map->getlevel() * 2 + 1; j++) {
if(i == map->p_x && j == map->p_y) {
painter.fillRect(start_x + i * perblock, strat_y + j * perblock, perblock, perblock, QBrush(Qt::red));
} else if(map->getmap()[i][j] == 7) {
painter.fillRect(start_x + i * perblock, strat_y + j * perblock, perblock, perblock, QBrush(Qt::yellow));
} else if(map->getmap()[i][j] == 3 || map->getmap()[i][j] == 4) {
painter.fillRect(start_x + i * perblock, strat_y + j * perblock, perblock, perblock, QBrush(Qt::gray));
} else if(map->getmap()[i][j] == 5) {
painter.fillRect(start_x + i * perblock, strat_y + j * perblock, perblock, perblock, QBrush(Qt::blue));
} else if(map->getmap()[i][j] == 6) {
painter.fillRect(start_x + i * perblock, strat_y + j * perblock, perblock, perblock, QBrush(Qt::green));
} else {
painter.fillRect(start_x + i * perblock, strat_y + j * perblock, perblock, perblock, QBrush(Qt::black));
}
}
}
}
首先判断绘图开关是否打开。
这里创建一个画笔对象用于绘制,我想使地图始终保持在frame的中央,所以计算了每个块的像素perblock,绘图开始的位置start_x与start_y,然后用painter的成员函数fillRect()对每个块分别涂色。
黑色为墙壁、边界,灰色为可通行路径,蓝色与绿色分别为起点和终点,红色是所在的位置,黄色是经过的路径。
然后使用两层循环涂色。
4.键盘按下事件虚函数
键盘按下时间将处理角色移动。
void mazeWidget::keyPressEvent(QKeyEvent* event) {
if(!keybord_switch) return;
int x = map->p_x;
int y = map->p_y;
//键盘移动逻辑:
if(event->key() == Qt::Key_I || event->key() == Qt::Key_W) {
if((*map)[x][y - 1] == 3 || (*map)[x][y - 1] == 4 || (*map)[x][y - 1] == 5 || (*map)[x][y - 1] == 6 || (*map)[x][y - 1] == 7) {
map->p_y--;
}
} else if(event->key() == Qt::Key_K || event->key() == Qt::Key_S) {
if((*map)[x][y + 1] == 3 || (*map)[x][y + 1] == 4 || (*map)[x][y + 1] == 5 || (*map)[x][y + 1] == 6 || (*map)[x][y + 1] == 7) {
map->p_y++;
}
} else if(event->key() == Qt::Key_J || event->key() == Qt::Key_A) {
if((*map)[x - 1][y] == 3 || (*map)[x - 1][y] == 4 || (*map)[x - 1][y] == 5 || (*map)[x - 1][y] == 6 || (*map)[x - 1][y] == 7) {
map->p_x--;
}
} else if(event->key() == Qt::Key_L || event->key() == Qt::Key_D) {
if((*map)[x + 1][y] == 3 || (*map)[x + 1][y] == 4 || (*map)[x + 1][y] == 5 || (*map)[x + 1][y] == 6 || (*map)[x + 1][y] == 7) {
map->p_x++;
}
}
//经过路径
if((*map)[map->p_x][map->p_y] != 5 && (*map)[map->p_x][map->p_y] != 6)(*map)[map->p_x][map->p_y] = 7;
repaint();
//到达终点
if((*map)[map->p_x][map->p_y] == 6) {
map->makemap();
repaint();
grade += pow(map->getlevel(), 2);
ui->grade_value->setText(QString::number(grade));
}
}
这里使用传入参数event来判断按下的是什么按键,然后判断对应方向是否能进行移动,如果能,改变map的p_x与p_y。
同时经过的路径数值改为7,视作已经经过。
如果当前位置与终点重合,即生成下一轮地图,计分数为当前迷宫阶数的平方,并更新记分标签控件。
5.时间更新槽函数
void mazeWidget::time_update() {
if(time != 0) {
//计时中
time--;
ui->time_value->setText(QString::number(time));
ui->progressBar->setValue(time / 2);
} else {
timer->stop(); //停止计时器
ui->progressBar->setVisible(false); //隐藏进度条
keybord_switch = false; //设置键盘响应、
painting_switch = false; //绘图响应、
timing_switch = false; //计时响应为关闭状态
repaint(); //清除画布
ui->start_btn->setEnabled(true); //|
ui->time_value->setText(" "); //|
ui->grade_value->setText(" "); //|
ui->stop_ptn->setEnabled(false); //|设置各按钮与标签状态
ui->end_btn->setEnabled(false); //|
ui->setting_btn->setEnabled(true); //|
//提示
QMessageBox outgrade(QMessageBox::NoIcon, "恭喜", "您得分:" + QString::number(grade), QMessageBox::Ok);
outgrade.exec();
//分数重置
grade = 0;
}
}
计时器将在开始按钮按下时被设置为1000个周期,即一秒发出一次timeout()信号。
游戏共计时200秒,故进度条值为time / 2。同时计时停止时进行游戏停止的对应ui设置,并弹出消息窗口显示分数。
6.各按钮点击槽函数
void mazeWidget::on_start_btn_clicked() {
painting_switch = true;
timing_switch = true;
keybord_switch = true;
time = 200;
timer->start(1000);
ui->progressBar->setVisible(true);
ui->progressBar->setValue(100);
repaint();
ui->time_value->setText(QString::number(time));
ui->grade_value->setText(QString::number(grade));
ui->start_btn->setEnabled(false);
ui->stop_ptn->setEnabled(true);
ui->end_btn->setEnabled(true);
ui->setting_btn->setEnabled(false);
}
void mazeWidget::on_stop_ptn_clicked() {
if(stop_switch) {
timing_switch = false;
keybord_switch = false;
timer->stop();
ui->stop_ptn->setText("继续");
stop_switch = false;
} else {
timing_switch = true;
keybord_switch = true;
timer->start();
ui->stop_ptn->setText("暂停");
stop_switch = true;
}
}
void mazeWidget::on_end_btn_clicked() {
timing_switch = false;
painting_switch = false;
keybord_switch = false;
stop_switch = false;
timer->stop();
time = 0;
grade = 0;
ui->progressBar->setVisible(false);
ui->grade_value->setText(" ");
ui->time_value->setText(" ");
ui->stop_ptn->setText("暂停");
ui->stop_ptn->setEnabled(false);
ui->end_btn->setEnabled(false);
ui->start_btn->setEnabled(true);
ui->setting_btn->setEnabled(true);
map->rebuildmap();
repaint();
}
void mazeWidget::on_rule_btn_clicked() {
QMessageBox rule(QMessageBox::NoIcon, "规则", "计时200秒,根据迷宫等级与经过关卡记分。\n操作方式:WASD或者IJKL控制方向。", QMessageBox::Ok);
rule.exec();
}
void mazeWidget::on_setting_btn_clicked() {
QStringList difficultys;
difficultys << tr("小朋友启蒙难度(5阶迷宫)") << tr("简单难度(10阶迷宫)") << tr("普通难度(20阶迷宫)") << tr("困难难度(40阶迷宫)");
QString difficulty = QInputDialog::getItem(this, tr("选择难度"),
tr("请选择一个条目"), difficultys, 0, false);
if(difficulty == tr("小朋友启蒙难度(5阶迷宫)")) {
delete map;
map = new maze(5);
map->makemap();
} else if(difficulty == tr("简单难度(10阶迷宫)")) {
delete map;
map = new maze(10);
map->makemap();
} else if(difficulty == tr("普通难度(20阶迷宫)")) {
delete map;
map = new maze(20);
map->makemap();
} else if(difficulty == tr("困难难度(40阶迷宫)")) {
delete map;
map = new maze(40);
map->makemap();
}
}
void mazeWidget::on_battle_btn_clicked() {
}
void mazeWidget::on_about_btn_clicked() {
QMessageBox about(QMessageBox::NoIcon, "关于", "由amazcuter制作,代码请访问我的CSDN博客。\n玩得开心。\n\n双人对战模式未开发完成,将会在未来版本开放。", QMessageBox::Ok);
about.exec();
}
分别进行对应的ui设置,功能调用。
step 4.主函数
#include "mazewidget.h" //窗口类头文件
#include "maze.h" //迷宫类头文件
#include <QApplication>
#include <QStyleFactory> //界面风格头文件
int main(int argc, char* argv[]) {
srand(time(NULL)); //设置随机数种子
QApplication a(argc, argv);
a.setStyle(QStyleFactory::create("Fusion")); //更改界面风格
mazeWidget w;
w.show();
return a.exec();
}
这里加入头文件,设置一下界面风格和随机数种子。
双人模式放在之后更新吧,现在先放着。
好了现在项目就算编写好了。
成果:
转载请表明出处哈,我还是很虚荣的233。
持续更新,我打算把这搞成我的实训作业,努力添加功能,优化外观。
当前版本v2.0。
觉得还行的看官点个攒再走吧…