使用QT:复刻俄罗斯方块游戏

项目背景      

        俄罗斯方块作为最为经典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、如果没办法继续下落方块,则游戏结束

 

 

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值