使用Qt图形框架开发俄罗斯方块
Qt作为一个图形库拥有着大量的成功案例,其中也不乏一些非常出色的游戏。
使用QT制作俄罗斯方块便是非常不错的选择。
1.总体概述
俄罗斯方块是我们最熟悉不过的一款休闲类小游戏,自从它被发明以来,在各个终端也是有诸多的实例。作为一款经久不衰的一款小游戏,也是受到了软件开发人员的青睐,将这款小游戏作为了一个软件编程入门的小实例。可以说,作为一个软件开发者,这款小游戏是不得不会的一个小例子。
俄罗斯方块在游戏规则上非常简单,在10×20的舞台上,自动添加一个图形并自上向下移动,直至下方存在图形无法下落,之后再次添加一个新的图形。当无法添加图形的时候,认为游戏结束。

本文将会从核心游戏逻辑、显示舞台、分数计算、多线程图形绘制、图形碰撞检测、图形变换六个方面来进行详解。
2.图形界面
首先是一个舞台(QWidget)用于显示和游戏内容相关的图形,另外是一个用于显示下一个图形的窗口(QWidget),下方是一个开始游戏的入口按钮。
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>275</width>
<height>350</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>275</width>
<height>350</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>275</width>
<height>350</height>
</size>
</property>
<property name="windowTitle">
<string>俄罗斯方块</string>
</property>
<property name="windowIcon">
<iconset resource="Icons.qrc">
<normaloff>:/main.png</normaloff>:/main.png</iconset>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QWidget" name="stage" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>161</width>
<height>321</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>161</width>
<height>321</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QWidget{
border:1px solid #000;
background-color:#000;
}</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QWidget" name="Tip" native="true">
<property name="minimumSize">
<size>
<width>65</width>
<height>65</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>65</width>
<height>65</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">
border:1px solid #000;
background-color:#000;
</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="Score">
<property name="text">
<string>点击开始游戏</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="StartGame">
<property name="text">
<string>开始游戏</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources>
<include location="Icons.qrc"/>
</resources>
<connections/>
</ui>

因为前面的舞台中将会有200个方块,后面的窗口中将会有16个方块,数量巨大,使用QtCreator中的图形界面绘制工具直接绘制工作量过于巨大,所以在这里使用了空置这两个QWidget,之后再在结构函数中生成应当存在的QLabel。同时,如果之前使用Qt Creator绘制完成舞台和窗口,将会在调用其中内容时候因为命名的问题产生相当大的工作量。
3.显示舞台
首先是整体数据结构:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QLabel>
#include <QTimer>
#include <QMainWindow>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
void keyPressEvent(QKeyEvent *event); //按键事件处理
private slots:
void gameEvent(); //timer槽函数
void on_StartGame_clicked(); //开始游戏槽函数
private:
Ui::MainWindow *ui;
QTimer *timer; //计时器
QLabel *stage[20][10]; //高20宽10:舞台
QLabel *tipBox[4][4]; //高4宽4提示窗口
int _stageContent[20][10]; //纯数字舞台
//7种图形4个方向4个高4个宽
int _shapes[7][4][4][4]={{{{1,1,0,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}},{{0,1,0,0},{1,1,0,0},{1,0,0,0},{0,0,0,0}},{{1,1,0,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}},{{0,1,0,0},{1,1,0,0},{1,0,0,0},{0,0,0,0}}},{{{0,1,1,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}},{{1,0,0,0},{1,1,0,0},{0,1,0,0},{0,0,0,0}},{{0,1,1,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}},{{1,0,0,0},{1,1,0,0},{0,1,0,0},{0,0,0,0}}},{{{1,1,0,0},{0,1,0,0},{0,1,0,0},{0,0,0,0}},{{1,1,1,0},{1,0,0,0},{0,0,0,0},{0,0,0,0}},{{1,0,0,0},{1,0,0,0},{1,1,0,0},{0,0,0,0}},{{0,0,1,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}}},{{{1,1,0,0},{1,0,0,0},{1,0,0,0},{0,0,0,0}},{{1,0,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},{{0,1,0,0},{0,1,0,0},{1,1,0,0},{0,0,0,0}},{{1,1,1,0},{0,0,1,0},{0,0,0,0},{0,0,0,0}}},{{{0,1,0,0},{0,1,0,0},{0,1,0,0},{0,1,0,0}},{{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}},{{0,1,0,0},{0,1,0,0},{0,1,0,0},{0,1,0,0}},{{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}}},{{{1,1,0,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}},{{1,1,0,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}},{{1,1,0,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}},{{1,1,0,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}}},{{{0,0,0,0},{1,1,1,0},{0,1,0,0},{0,0,0,0}},{{1,0,0,0},{1,1,0,0},{1,0,0,0},{0,0,0,0}},{{0,1,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},{{0,1,0,0},{1,1,0,0},{0,1,0,0},{0,0,0,0}}}};
int _x=3,_y=0;//当前图形位置00位置
int nextShape,currentShape; //下一个图形,当前图形
int nextPos,currentPos; //下一个图形姿态,当前图形姿态
bool _sl=false; //图形锁定变量
int Score=0; //当前分数
void lockShape(); //锁定图形
bool shapeLocked(); //判断是否锁定
void unlockShape(); //解锁图形
bool hitTest(int x, int y, int xshift, int yshift, int shape, int pos);//碰撞检测
bool downOK(); //图形是否可以下落
bool leftOK(); //是否可以左移
bool rightOK(); //是否可以右移
bool switchOK(); //是否可以变换
bool addOK(); //是否可以添加
void clearOldBlock(); //清除旧图形
void putNewBlock(); //添加新图形
void randomShape(); //随机下一个
void addShape(); //添加图形
void drawStage(); //绘制舞台
void dropShape(); //图形下落
void leftShape(); //图形左移
void rightShape(); //图形右移动
void switchShape(); //图形变换
void calculateScore(); //消除并计算分数
void shiftRow(int row); //消除某行
};
#endif // MAINWINDOW_H
在这里,需要将7种图形
这里为了方便操作,舞台使用QLabel作为每一个方块,每个方块的大小为15px×15px,并使用一个二维数组对应每个方块的有无颜色(0为无,1为有),同时,使用drawStage()函数来同步数组和舞台的显示。
#include "MainWindow.h"
//绘制舞台
void MainWindow::drawStage(){
for(int i=0;i<20;i++){
for(int j=0;j<10;j++){
//当舞台映射中为1时,对应的QLabel设定为有色,否则为无色
if(_stageContent[i][j]){
stage[i][j]->setStyleSheet("border:0px;background:#5709A8");
}else{
stage[i][j]->setStyleSheet("border:0px;background:#FFF");
}
}
}
}
这里用到了CSS来规定方块的颜色。
4.图形碰撞检测
图形碰撞检测是俄罗斯方块中最复杂的一部分。当用户输入一个移动指令,游戏需要检验此操作是否可行;当游戏自动执行下降时,需要判断是否可以下降;当添加一个图形时,需要判断是否存在空间添加新的图形;当图形变换时,判断是否可以进行变换。
#include "MainWindow.h"
#include <QDebug>
//可以下落
bool MainWindow::downOK(){
//判断y向下偏移一个是否可以添加图形
return !hitTest(_x,_y,0,1,currentShape,currentPos);
}
//是否可以左移
bool MainWindow::leftOK(){
//判断x减少一个是否可以添加图形
return !hitTest(_x,_y,-1,0,currentShape,currentPos);
}
//是否可以右移
bool MainWindow::rightOK(){
//判断x增加一个是否可以添加图形
return !hitTest(_x,_y,1,0,currentShape,currentPos);
}
//是否可以变换
bool MainWindow::switchOK(){
int _pos=currentPos+1;
if(_pos>3) _pos=0;
return !hitTest(_x,_y,0,0,currentShape,_pos);
}
//是否可以添加
bool MainWindow::addOK(){
//添加直接添加即可,不存在旧图形,所以无法使用hitTest函数
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
if(_stageContent[i][3+j]&&_shapes[nextShape][nextPos][i][j]) return false;
return true;
}
//碰撞检测
bool MainWindow::hitTest(int x, int y,int xshift,int yshift, int shape, int pos){
int _s[20][10]; //用户试验的舞台映射
int test; //用于存储重叠方块数量的变量
memcpy(_s,_stageContent,sizeof(_stageContent)); //拷贝舞台
//首先将已有的旧图形去除
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
if(_shapes[shape][pos][i][j]) {
_s[y+i][x+j]=0;
}
//尝试进行图形的更改,并统计会之后是否有重叠(即有重叠)
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
if(_shapes[shape][pos][i][j]){
//此处判断一下边界
if(x+j+xshift<0||x+j+xshift>9||y+i+yshift<0||y+i+yshift>19) {
return true;
}
//重叠计数
test=_s[y+i+yshift][x+j+xshift]+_shapes[shape][pos][i][j];
//一旦发现重叠即可返回
if(test>1) return true;
}
return false;
}
5.图形变换
当经过了图形碰撞检测之后,需要将图形进行对应的变换,从而实现图形的变化。
经过分析,我发现不管对于哪一种变换,总体的算法思路都是先去除旧的图形,对图形进行变换,绘制新的图形。所以四种变换(左移,右移,下移,旋转)整体结构都是相同的。
#include "MainWindow.h"
//下落
void MainWindow::dropShape(){
while(shapeLocked());
lockShape();
clearOldBlock();
_y++;
putNewBlock();
unlockShape();
}
//左移
void MainWindow::leftShape(){
while(shapeLocked());
lockShape();
clearOldBlock();
_x--;
putNewBlock();
unlockShape();
}
//右移
void MainWindow::rightShape(){
while(shapeLocked());
lockShape();
clearOldBlock();
_x++;
putNewBlock();
unlockShape();
}
//变换
void MainWindow::switchShape(){
while(shapeLocked());
lockShape();
clearOldBlock();
currentPos++;
currentPos=currentPos>3?0:currentPos;
putNewBlock();
unlockShape();
}
//清除旧图形
void MainWindow::clearOldBlock(){
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
if(_shapes[currentShape][currentPos][i][j]) _stageContent[_y+i][_x+j]=0;
}
//添加新图形
void MainWindow::putNewBlock(){
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
if(_shapes[currentShape][currentPos][i][j]) _stageContent[_y+i][_x+j]=1;
}
6.多线程图形绘制
QT本身是一个调用系统API实现多线程的图形化框架。多线程有一个非常重要的特点就是CPU资源随机分配。对于多个线程,下一个CPU指令遵循哪一个线程的指令将会是不确定的。然而在俄罗斯方块中,不管是哪一个方块的图形变换,都是需要多个CPU指令才能够完成,所以上面的几个函数必须要等到当前已经开始的变换函数完全执行结束后才能够开始对舞台映射数组进行操作,否则将会出现图形残留或者无法正确变换。
在Java中,Java本身支持多线程编程,所以保留了synchronize关键字用于多线程中的同步问题。在网上经过了一些查阅,我并没有找到关于Qt中多线程的线程同步的一些方法,所以就采用了一个标记变量用于标记当前是否存在正在执行的图形,如果存在,既不能提前结束函数(假设是玩家提出的图形变换命令,那么表现出来就是没有反应),也不能继续执行,所以采用了一个while循环将函数暂停。
#include "MainWindow.h"
bool MainWindow::shapeLocked(){
return _sl;
}
void MainWindow::lockShape(){
_sl=true;
}
void MainWindow::unlockShape(){
_sl=false;
}
7.核心游戏逻辑
根据游戏的逻辑,可以得出下面的代码:
#include "MainWindow.h"
#include "ui_MainWindow.h"
#include <QDateTime>
#include <QDebug>
#include <QKeyEvent>
#include <QMessageBox>
#include <QTimer>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
memset(_stageContent,0,sizeof(_stageContent));
//绘制舞台格子
QLabel *temp;
for(int i=0;i<20;i++){
for(int j=0;j<10;j++){
temp=new QLabel(ui->stage);
temp->setText("");
temp->setMinimumSize(QSize(15, 15));
temp->setMaximumSize(QSize(15, 15));
temp->setGeometry(j*16+1,i*16+1,15,15);
temp->setStyleSheet("border:0px;background-color:#FFF");
stage[i][j]=temp;
}
}
//绘制提示格子
for(int i=0;i<4;i++){
for(int j=0;j<4;j++){
temp=new QLabel(ui->Tip);
temp->setText("");
temp->setMinimumSize(QSize(15, 15));
temp->setMaximumSize(QSize(15, 15));
temp->setGeometry(j*16+1,i*16+1,15,15);
temp->setStyleSheet("border:0px;background-color:#FFF");
tipBox[i][j]=temp;
}
}
//随机数种子
qsrand(QDateTime::currentDateTime().toTime_t());
//计时器
timer=new QTimer();
connect(timer,SIGNAL(timeout()),this,SLOT(gameEvent()));
}
MainWindow::~MainWindow()
{
delete ui;
}
//随机图形
void MainWindow::randomShape(){
nextShape=qrand()%7;
nextPos=qrand()%4;
for(int i=0;i<4;i++){
for(int j=0;j<4;j++){
if(_shapes[nextShape][nextPos][i][j]){
tipBox[i][j]->setStyleSheet("border:0px;background-color:#5709A7");
}else{
tipBox[i][j]->setStyleSheet("border:0px;background-color:#FFF");
}
}
}
}
void MainWindow::addShape(){
currentShape=nextShape;
currentPos=nextPos;
for(int i=0;i<4;i++){
for(int j=0;j<4;j++){
_stageContent[i][3+j]+=_shapes[currentShape][currentPos][i][j];
}
}
_x=3;
_y=0;
}
void MainWindow::gameEvent(){
//如果可以下降,就下降
if(downOK()){
dropShape();
}else{
//否则计算分数消除可以消除的
calculateScore();
//如果可以添加,说明游戏未结束
if(addOK()){
addShape();
randomShape();
}else{
//否则游戏结束
timer->stop();
QMessageBox::information(this,"Game Over","房顶都被你丫的顶破了~~");
ui->StartGame->setEnabled(true);
ui->StartGame->setText("重新开始");
}
}
drawStage();
}
void MainWindow::keyPressEvent(QKeyEvent *event){
//处理键盘
switch (event->key()){
case Qt::Key_W :
if(switchOK()) switchShape();
break;
case Qt::Key_S :
while(downOK()) dropShape();
break;
case Qt::Key_A :
if(leftOK()) leftShape();
break;
case Qt::Key_D :
if(rightOK()) rightShape();
break;
}
drawStage();
}
void MainWindow::on_StartGame_clicked(){
ui->StartGame->setEnabled(false);
ui->StartGame->setText("正在游戏...");
//清除舞台
memset(_stageContent,0,sizeof(_stageContent));
//置零分数
Score=0;
calculateScore();
//随机产生图形
randomShape();
//添加图形
addShape();
//准备下一个
randomShape();
//刷新舞台
drawStage();
//启动计时器
timer->start(700);
}
在这个俄罗斯方块的例子中,我并没有对暂停做一个处理,纯粹是因为懒_(:зゝ∠)_.
发现了一个bug,L型在落到下面的时候狂按变形,他就会吃掉几个格子
您好,请问Scores.cpp中第二十六行:for(int i=0;i<10;i++) _stageContent[0][i]=0; 为什么i突然又变成列了,不是j是列吗?
实际上这个i只是一个用来枚举的变量。shiftRow里边是两个互不干扰的循环,第一个循环是在把指定row上当的所有方块下移一行,然后整个舞台最上一行就应该是空的,所以第二个循环把第0行中的每个格子赋值为没有。具体谁是列谁是行关键看变量放在了_stageContent的哪个下标了,_stageContent的第一个下标表示行号,第二个表示某行中的格号。所以遍历用的变量是谁都可以。
为什么代码下载不了,什么原因呢?
抱歉,CDN把地址换了,导致无法正常访问下载,已经修复,请重试
为什么我给整个MainWindow插入一张背景照片后,提示区域里的俄罗斯方块就显示不出来了,只有游戏区域的俄罗斯方块能显示出来,这是为什么?
额这个俄罗斯方块的方块其实是用样式表实现的,如果你给他用样式表加背景可能会覆盖掉原来用于显示色块的样式表.推荐做法应该还是用QWidget自己去实现一套绘制方法,这样会减少父QObject样式表对子QObject的影响.