项目背景
俄罗斯方块作为最为经典80、90年代的小游戏,是无数人的童年,从最开始的栅格游戏机,到后面的红白机,再到“小霸王”游戏机,俄罗斯方块游戏的内容和玩法再被不断的变革与创新。时至今日,俄罗斯方块已经成为一代人的回忆。最近学习了QT应用开发,搞一波经典复刻。
游戏玩法
1、按 方向键 左右控制方块的左右移动
2、按 方向键 下 加速方块下落
3、按 空格键 快速下落
4、可以设置难度
5、可以显示分数和难度等级
设计思路
界面布局:
左侧是游戏方块区域,采用自绘widget
右侧使用UI设计器布局,其中显示下一个方块的widget也是自绘
这2个 widget 都在UI设计器里通过普通 widget 做了提升
游戏区域:
游戏区域使用15*20矩形格子,以横纵坐标表示格子位置
每个下落的图形元素使用4个格子表示
依次绘制背景、已固定的元素、当下移动的元素
方块形状:
每个元素都是4个格子,存4个坐标点即可
元素一共有6种类型,长条、山字形、枪形1、枪形2、田字形、Z字形,每种可以变换4个方向,就又有1到4种形状
每种形状都可以在一个4*4的矩阵中以4个坐标表示,改变形状时,就是换一种坐标集
移动碰撞检测:
先假设移动一步,计算移动后元素和已存在的坐标集是否存在重合,存在则不能移动,退回
游戏分数、等级、下落速度控制:
一次消除1行=100分;2行=300分;3行=500分;4行=700分
即同时消除行数越多,奖励分数越多
每1000分1级,粗略设计共8级
下落是定时器控制,每升1级定时器加快一点,满级不再加速
源码:
头文件:
1、mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QKeyEvent>
#include <QMainWindow>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
protected:
void keyPressEvent(QKeyEvent *e);
private slots:
void slotUpdateScore(int nScore);
void slotUpdateLevel(int nSpeed);
private:
Ui::MainWindow *ui;
};
#endif
2、GameArea.h
#ifndef GAMEAREA_H
#define GAMEAREA_H
#include "Item.h"
#include <QWidget>
class GameArea : public QWidget
{
Q_OBJECT
public:
explicit GameArea(QWidget *parent = nullptr);
void DrawBKRects(); //用作背景的方块
void DrawFixedRects(); //下落后已固定不动的方块
void DrawCurItem(); //当前下落中的方块
void NewGame();
void KeyPressed(int key);
bool HitSide(); //判断当前下落方块是否超左右边界
bool HitBottom(); //判断当前下落方块是否达到底部
bool HitTop(); //判断当前下落方块是否达到顶部
void AddToFixedRects(); //把当前方块加入到 固定方块
void DeleteFullRows(); //删除完整的行
int GetLevelTime(int level); //获取不同等级关卡对应的定时器时间,关卡越高,时间越快(短)。比如1关=1s,2关=900ms,3关=800ms
signals:
void sigUpdateNextItem(ITEM_TYPE t,int nShape);
void sigUpdateScore(int nScore);
void sigUpdateLevel(int nSpeed);
protected:
void paintEvent(QPaintEvent *);
void timerEvent(QTimerEvent*);
private:
Item mFixItems; //已落下、固定住了的方块们
Item mCurItem; //当前移动中的方块
Item mNextItem; //下一个方块
int mTimerID; //定时器ID
int mScore; //得分
int mLevel; //关卡
};
#endif
3、Item.h
#pragma once
#include "qvector.h"
#include "qpoint.h"
#include "qmap.h"
#include "qpainter.h"
enum ITEM_TYPE{
ITEM_1 = 0, //长条
ITEM_2, //山字形
ITEM_3, //手枪形1
ITEM_4, //手枪形2
ITEM_5, //田
ITEM_6, //Z字形1
ITEM_MAX,
};
class Item
{
public:
Item(){}
Item(ITEM_TYPE t,int nShape = 0);
~Item(void);
void InitNew(int nSeed = 0);
void InitItem(ITEM_TYPE t,int nShape = 0);
void ChangeShape(int nAdd = 1);
void AddPoints(QVector<QPoint>& points);
void Move(int x,int y); //横向移动x格,竖向移动y格
void MoveTo(int x,int y); //移动到位置(x,y)格
void MoveDown(int nRow,int y); //第nRow行以上的部分下移y个单位,用在消除之后
void Draw(QPainter& painter,int nStartX,int nStartY,int nW,int nH);
void DeleteRow(int y);
public:
QVector<QPoint> mPoints; //方块元素内含有的点数,每个点代表一个格子坐标,每个方块Item含有4个坐标点,就是4个格子
QPoint mPos;
ITEM_TYPE mType; //方块有6种类型
int mShape; //但是每个类型又可以变形(改变方向),每个大类下面又可能有1~4个方向的不同形状
};
4、NextArea.h
#ifndef NEXTAREA_H
#define NEXTAREA_H
#include "item.h"
#include <QWidget>
//右侧显示下一个元素的自绘widget
class NextArea : public QWidget
{
Q_OBJECT
public:
explicit NextArea(QWidget *parent = nullptr);
protected:
void paintEvent(QPaintEvent *);
public slots:
void slotUpdateNextItem(ITEM_TYPE t,int nShape);
private:
Item mItem;
};
#endif // NEXTAREA_H
界面文件:
1、mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
setFixedSize(1000,800);
//widgetGameArea 和 widgetNextArea 已在界面设计器内由普通QWidget分别提升成GameArea和NextArea
//GameArea: 左侧游戏区域,自绘widget
//NextArea: 右侧显示下个item的widget,也是自绘widget
//游戏主运行逻辑在GameArea内,不过按键消息因为是MainWindow接受,通过ui->widgetGameArea->KeyPressed()函数调用传递下去
//GameArea通过信号sigUpdateScore、sigUpdateLevel 通知MainWindow更新界面的得分和关卡
//GameArea通过信号sigUpdateNextItem 通知 NextArea 刷新下一个元素
connect(ui->widgetGameArea,&GameArea::sigUpdateScore,this,&MainWindow::slotUpdateScore);
connect(ui->widgetGameArea,&GameArea::sigUpdateLevel,this,&MainWindow::slotUpdateLevel);
connect(ui->widgetGameArea,&GameArea::sigUpdateNextItem,ui->widgetNextArea,&NextArea::slotUpdateNextItem);
ui->widgetGameArea->NewGame();
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::keyPressEvent(QKeyEvent *e)
{
ui->widgetGameArea->KeyPressed(e->key());
QMainWindow::keyPressEvent(e);
}
void MainWindow::slotUpdateScore(int nScore)
{
ui->labelScore->setText(QString::number(nScore));
}
void MainWindow::slotUpdateLevel(int nSpeed)
{
ui->labelSpeed->setText(QString::number(nSpeed));
}
2、GameArea.cpp
#include "GameArea.h"
#include <QTimerEvent>
#include <QMessageBox>
#include <QKeyEvent>
#include <QTime>
#define RECT_COLUMES 15
#define RECT_ROWS 20
#define RECT_WIDTH 40
#define RECT_HEIGHT 40
//默认出生点 x方向
#define DEFAULT_BORN_POS_X (RECT_COLUMES / 2 - 1)
GameArea::GameArea(QWidget *parent) : QWidget(parent)
{
mScore = 0;
mLevel = 1;
setMinimumSize(RECT_COLUMES*RECT_WIDTH, RECT_ROWS*RECT_HEIGHT);
}
void GameArea::NewGame()
{
mFixItems.mPoints.clear();
//mCurItem 和 mNextItem 使用不同随机因子,避免初始一样
mCurItem.InitNew(QTime::currentTime().msec());
mCurItem.MoveTo(DEFAULT_BORN_POS_X, 1);
mNextItem.InitNew(QTime::currentTime().second());
emit sigUpdateNextItem(mNextItem.mType,mNextItem.mShape);
mScore = 0;
mLevel = 1;
mTimerID = startTimer( GetLevelTime(mLevel) );
}
void GameArea::KeyPressed(int key)
{
int x = 0;
int y = 0;
switch(key)
{
case Qt::Key_Left:
{
x = -1;
break;
}
case Qt::Key_Up:
{
mCurItem.ChangeShape();
if(HitSide() || HitBottom())
{
mCurItem.ChangeShape(-1);
}
return;
}
case Qt::Key_Right:
{
x = 1;
break;
}
case Qt::Key_Down:
{
y = 1;
break;
}
case Qt::Key_Space:
{
//空格键直接移到底部
while(1)
{
mCurItem.Move(0,1);
if(HitSide() || HitBottom())
{
mCurItem.Move(0,-1);
break;
}
}
return;
}
}
mCurItem.Move(x,y);
if (HitSide() || HitBottom())
{
mCurItem.Move(-x,-y);
}
}
void GameArea::paintEvent(QPaintEvent *)
{
//绘制左侧游戏区域
DrawBKRects();
DrawFixedRects();
DrawCurItem();
update();
}
void GameArea::DrawBKRects()
{
QPainter painter(this);
painter.setBrush(QColor("#696969"));
painter.setPen(Qt::NoPen);
for(int i = 0;i<RECT_COLUMES; i++)
{
for (int j = 0; j<RECT_ROWS; j++)
{
if (i == 0 || i == RECT_COLUMES - 1 || j==0 || j==RECT_ROWS-1)
{
painter.drawRect( i*RECT_WIDTH,j*RECT_HEIGHT,RECT_WIDTH,RECT_HEIGHT);
}
}
}
}
void GameArea::DrawFixedRects()
{
QPainter painter(this);
painter.setBrush(QColor("#D3D3D3"));
painter.setPen(QPen(QColor(Qt::black),1));
mFixItems.Draw(painter,0,0,RECT_WIDTH,RECT_HEIGHT);
}
void GameArea::DrawCurItem()
{
QPainter painter(this);
painter.setBrush(QColor("#FFDEAD"));
painter.setPen(QPen(QColor(Qt::black),1));
mCurItem.Draw(painter,0,0,RECT_WIDTH,RECT_HEIGHT);
}
void GameArea::timerEvent(QTimerEvent* e)
{
if (e->timerId() == mTimerID)
{
mCurItem.Move(0,1);
if (HitBottom())
{
mCurItem.Move(0,-1);
AddToFixedRects();
DeleteFullRows();
if (HitTop())
{
killTimer(mTimerID);
QMessageBox::information(NULL, "GAME OVER", "GAME OVER", QMessageBox::Yes , QMessageBox::Yes);
NewGame();
return;
}
mCurItem = mNextItem;
mCurItem.MoveTo(DEFAULT_BORN_POS_X, 1);
mNextItem.InitNew();
emit sigUpdateNextItem(mNextItem.mType,mNextItem.mShape);
}
}
}
bool GameArea::HitSide()
{
for (int i = 0; i<mCurItem.mPoints.size(); i++)
{
QPoint pt = mCurItem.mPoints[i];
if (pt.x() <= 0 || pt.x() >= RECT_COLUMES - 1)
{
return true;
}
}
return false;
}
bool GameArea::HitBottom()
{
for (int i = 0; i<mCurItem.mPoints.size(); i++)
{
QPoint pt = mCurItem.mPoints[i];
if (pt.y() >= RECT_ROWS - 1)
{
return true;
}
if (mFixItems.mPoints.contains(pt))
{
return true;
}
}
return false;
}
bool GameArea::HitTop()
{
for (int i = 0; i<mFixItems.mPoints.size(); i++)
{
QPoint pt = mFixItems.mPoints[i];
if (pt.y() <= 1)
{
return true;
}
}
return false;
}
void GameArea::AddToFixedRects()
{
mFixItems.AddPoints(mCurItem.mPoints);
}
void GameArea::DeleteFullRows()
{
int nRowsDeleted = 0;
for (int i = 1; i<RECT_ROWS-1; i++)
{
int nCount = 0;
for (int j = 1; j<RECT_COLUMES-1; j++)
{
if (mFixItems.mPoints.contains(QPoint(j,i)))
{
nCount++;
}
}
if (nCount >= RECT_COLUMES-2)
{
mFixItems.DeleteRow(i);
mFixItems.MoveDown(i,1); //消除行之上的内容下移一个单位
nRowsDeleted++;
}
}
//一次元素落下,最多可能消4行
//一次消除的越多,得分越多
if (nRowsDeleted == 1)
{
mScore += 100;
}
else if (nRowsDeleted == 2)
{
mScore += 300;
}
else if (nRowsDeleted == 3)
{
mScore += 500;
}
else if (nRowsDeleted == 4)
{
mScore += 700;
}
emit sigUpdateScore(mScore); //更新MainWindow界面得分
//粗略使用每1000分一关
if(mScore >= 1000 * mLevel)
{
mLevel++;
//随关卡增加下落速度,即把定时器加快
killTimer(mTimerID);
mTimerID = startTimer( GetLevelTime(mLevel) );
emit sigUpdateLevel(mLevel); //更新MainWindow界面关卡
}
}
int GameArea::GetLevelTime(int level)
{
//第1关=1000ms,第2关=900ms 越来越快 第8关=300ms
//关卡>8后,速度不再增加,保持200ms
if(level > 8)
{
return 200;
}
if (level > 0)
{
return (11 - level) * 100;
}
}
3、NextArea.cpp
#include "NextArea.h"
NextArea::NextArea(QWidget *parent) : QWidget(parent)
{
}
void NextArea::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setBrush(QColor("#FFDEAD"));
painter.setPen(QPen(QColor(Qt::black),1));
int xStart = 80; //为了绘制在显示下一个方块区域的中部
int yStart = 10;
int w = 20;
int h = 20;
foreach (QPoint pt, mItem.mPoints)
{
int x = xStart + pt.x() * w;
int y = yStart + pt.y() * h;
painter.drawRect(x, y, w, h);
}
update();
}
void NextArea::slotUpdateNextItem(ITEM_TYPE t, int nShape)
{
mItem.InitItem(t,nShape);
}
4、Item.cpp
#include "Item.h"
#include "qpainter.h"
#include <QTime>
Item::Item(ITEM_TYPE t,int nShape)
{
mPos = QPoint(0,0);
InitItem(t,nShape);
}
Item::~Item(void)
{
}
void Item::InitNew(int nSeed)
{
if(nSeed == 0)
{
//默认不传参,就使用当前时间作随机因子
qsrand(QTime::currentTime().msec());
}
else
{
//传入随机因子
qsrand(nSeed);
}
ITEM_TYPE t = (ITEM_TYPE)(qrand()%ITEM_MAX);
int s = qrand()%4;
InitItem(t,s);
}
void Item::InitItem(ITEM_TYPE t,int nShape)
{
mPoints.clear();
mType = t;
mShape = nShape;
switch (t)
{
case ITEM_1:
{
if (nShape%2 == 0)
{
for (int i = 0; i < 4; i++)
{
mPoints.append(mPos + QPoint( i,2));
}
}
else if (nShape%2 == 1)
{
for (int i = 0; i < 4; i++)
{
mPoints.append(mPos + QPoint( 2,i));
}
}
break;
}
case ITEM_2:
{
if (nShape == 0)
{
mPoints.append(mPos + QPoint( 1,0));
mPoints.append(mPos + QPoint( 0,1));
mPoints.append(mPos + QPoint( 1,1));
mPoints.append(mPos + QPoint( 2,1));
}
else if (nShape == 1)
{
mPoints.append(mPos + QPoint( 1,0));
mPoints.append(mPos + QPoint( 1,2));
mPoints.append(mPos + QPoint( 1,1));
mPoints.append(mPos + QPoint( 2,1));
}
else if (nShape == 2)
{
mPoints.append(mPos + QPoint( 0,1));
mPoints.append(mPos + QPoint( 1,2));
mPoints.append(mPos + QPoint( 1,1));
mPoints.append(mPos + QPoint( 2,1));
}
else if (nShape == 3)
{
mPoints.append(mPos + QPoint( 1,0));
mPoints.append(mPos + QPoint( 0,1));
mPoints.append(mPos + QPoint( 1,1));
mPoints.append(mPos + QPoint( 1,2));
}
break;
}
case ITEM_3:
{
if (nShape == 0)
{
mPoints.append(mPos + QPoint( 1,0));
mPoints.append(mPos + QPoint( 1,1));
mPoints.append(mPos + QPoint( 1,2));
mPoints.append(mPos + QPoint( 2,2));
}
else if (nShape == 1)
{
mPoints.append(mPos + QPoint( 1,1));
mPoints.append(mPos + QPoint( 2,1));
mPoints.append(mPos + QPoint( 3,1));
mPoints.append(mPos + QPoint( 1,2));
}
else if (nShape == 2)
{
mPoints.append(mPos + QPoint( 1,1));
mPoints.append(mPos + QPoint( 2,1));
mPoints.append(mPos + QPoint( 2,2));
mPoints.append(mPos + QPoint( 2,3));
}
else if (nShape == 3)
{
mPoints.append(mPos + QPoint( 2,1));
mPoints.append(mPos + QPoint( 2,2));
mPoints.append(mPos + QPoint( 1,2));
mPoints.append(mPos + QPoint( 0,2));
}
break;
}
case ITEM_4:
{
if (nShape == 0)
{
mPoints.append(mPos + QPoint( 2,0));
mPoints.append(mPos + QPoint( 2,1));
mPoints.append(mPos + QPoint( 2,2));
mPoints.append(mPos + QPoint( 1,2));
}
else if (nShape == 1)
{
mPoints.append(mPos + QPoint( 1,1));
mPoints.append(mPos + QPoint( 1,2));
mPoints.append(mPos + QPoint( 2,2));
mPoints.append(mPos + QPoint( 3,2));
}
else if (nShape == 2)
{
mPoints.append(mPos + QPoint( 1,1));
mPoints.append(mPos + QPoint( 2,1));
mPoints.append(mPos + QPoint( 1,2));
mPoints.append(mPos + QPoint( 1,3));
}
else if (nShape == 3)
{
mPoints.append(mPos + QPoint( 0,1));
mPoints.append(mPos + QPoint( 1,1));
mPoints.append(mPos + QPoint( 2,1));
mPoints.append(mPos + QPoint( 2,2));
}
break;
}
case ITEM_5:
{
mPoints.append(mPos + QPoint( 0,0));
mPoints.append(mPos + QPoint( 0,1));
mPoints.append(mPos + QPoint( 1,0));
mPoints.append(mPos + QPoint( 1,1));
break;
}
case ITEM_6:
{
if (nShape == 0)
{
mPoints.append(mPos + QPoint( 1,0));
mPoints.append(mPos + QPoint( 1,1));
mPoints.append(mPos + QPoint( 2,1));
mPoints.append(mPos + QPoint( 2,2));
}
else if (nShape == 1)
{
mPoints.append(mPos + QPoint( 1,1));
mPoints.append(mPos + QPoint( 2,1));
mPoints.append(mPos + QPoint( 0,2));
mPoints.append(mPos + QPoint( 1,2));
}
else if (nShape == 2)
{
mPoints.append(mPos + QPoint( 2,0));
mPoints.append(mPos + QPoint( 2,1));
mPoints.append(mPos + QPoint( 1,1));
mPoints.append(mPos + QPoint( 1,2));
}
else if (nShape == 3)
{
mPoints.append(mPos + QPoint( 0,1));
mPoints.append(mPos + QPoint( 1,1));
mPoints.append(mPos + QPoint( 1,2));
mPoints.append(mPos + QPoint( 2,2));
}
break;
}
default:
break;
}
}
void Item::ChangeShape(int nAdd)
{
mShape = (mShape + nAdd)%4;
InitItem(mType,mShape);
}
void Item::Draw(QPainter& painter,int nStartX,int nStartY,int nW,int nH)
{
for (int i = 0; i< mPoints.size(); i++)
{
QPoint pt = mPoints[i];
painter.drawRect(QRect(nStartX + pt.x() * nW,nStartY + pt.y() * nH,nW,nH));
}
}
void Item::AddPoints(QVector<QPoint>& points)
{
for (int i = 0; i<points.size(); i++)
{
if (!mPoints.contains(points[i]))
{
mPoints.append(points[i]);
}
}
}
void Item::Move(int x,int y)
{
for (int i = 0; i<mPoints.size(); i++)
{
int x1 = mPoints[i].x() + x;
int y1 = mPoints[i].y() + y;
mPoints[i].setX(x1);
mPoints[i].setY(y1);
}
mPos += QPoint(x,y);
}
void Item::MoveTo(int x,int y)
{
for (int i = 0; i<mPoints.size(); i++)
{
int x1 = mPoints[i].x() - mPos.x() + x;
int y1 = mPoints[i].y() - mPos.y() + y;
mPoints[i].setX(x1);
mPoints[i].setY(y1);
}
mPos = QPoint(x,y);
}
void Item::DeleteRow( int y )
{
QVector<QPoint> newPoints;
for (int i=0; i<mPoints.size(); i++)
{
if (mPoints[i].y() != y)
{
newPoints.append(mPoints[i]);
}
}
mPoints = newPoints;
}
void Item::MoveDown( int nRow,int y )
{
for (int i = 0; i<mPoints.size(); i++)
{
if(mPoints[i].y() < nRow)
{
mPoints[i].setY(mPoints[i].y()+ y);
}
}
}
ui设计
主函数
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
效果演示
初始阶段:
1、能够实现基本 定时下落 和 到底判断
2、能够左右移动
3、能够实现按上进行变形
4、能够实现按下加速下落
5、能够实现空格直接到底
后续阶段:
1、能够实现方块的碰撞检测
2、能够消除
3、达到1000分难度等级变化、速度加快
4、如果没办法继续下落方块,则游戏结束