【遊戲製作】 從零開始的Qt5貪吃蛇代碼分析

悲傷的現實

離期末第一場考試之剩下32天了,而oop的期中project還有24天截止。此刻,我們剛剛寫完了人生中第一個c++程序,分數類的運算符重載。繼承和多態還沒學。儘管如此,也是時候作爲全裸勇者勇敢地去挑戰大魔王了。

目標

通過研究貪吃蛇小遊戲的源碼,學會用Qt進行遊戲開發,並且完成期中Pro報告中挖下的2D Rougelike類小遊戲第一關的大坑,並且寫下博客給新鮮的學妹學弟留下可以借鑑的學習經驗。

資料

Qt5貪吃蛇小遊戲源代碼下載處
來自Rimond_Jing的Qt5基本安裝教程
來自 齊亮,非常棒的一個參考資料

高能預警

因爲目前c++也沒學好,Qt也剛開始學,所以本篇博客只適合和我一樣什麼都不會的人。而且很有可能錯誤連篇,期待有人能指正。
目前在一遍分析一遍寫博客,所以會對內容進行不斷地修正

以及非常囉嗦(和廢話)

總體分析

注:純新手一上來就看總體分析極有可能不懂,看看就好,不懂就跳過去看完全文回來再看。

界面轉換

在本例子中,採取建立不同的界面類(mainWidget,開始界面,和GameWidget,遊戲界面),通過接收事件(event,本例是按下按鈕),利用信號和槽,來進行界面切換。

main函數分析

感覺並不需要分析:)

#include "mainWidget.h"
//自己寫的主窗口的頭文件
#include <QApplication>
//一個基礎的類,所有工程(pro文件)都要include。
//有時候include這個文件會報錯,可能是因爲是Qt4的代碼的原因

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    mainWidget w;
    w.show();
    return a.exec();
}

主窗口分析:mainWidget

頭文件分析

#ifndef MAINWIDGET_H
#define MAINWIDGET_H
#include "GameWidget.h"
#include <QWidget>
#include <QIcon>
#include <QPalette>
#include <QBrush>
#include <QPixmap>
#include <QPushButton>
#include <QMessageBox>
#include <QLabel>
#include <QFont>
class mainWidget : public QWidget//繼承了QWidget類
{
    Q_OBJECT
//只要有槽和信號機制,就要寫Q_OBJECT
public:
    mainWidget(QWidget *parent = 0);
    ~mainWidget();
    //void resizeEvent(QResizeEvent *);
private:
    QPushButton *startbtn;//一個按鈕,用鼠標點擊後會開始遊戲
    QPushButton *exitbtn;//一個按鈕,用鼠標點擊後會退出
    GameWidget *g;
    QLabel *label;
signals://信號,mainWidget不會發出信號
public slots://可以接收所有信號的公共槽
    void exitSlot();//用來接收退出信號的槽
    void startSlot();//用來接收開始信號的槽
};

#endif // MAINWIDGET_H

.cpp文件分析
註釋的都是可以在自己的程序中使用的函數

#include "mainWidget.h"

mainWidget::mainWidget(QWidget *parent): QWidget(parent)
{
    this->resize(480,270);//resize函數,用來設置mainWidget這個窗口的大小
    this->setMaximumSize(480,270);
    this->setWindowIcon(QIcon(":/new/prefix1/img/icon.png"));//設置ICON
    this->setWindowTitle("貪吃蛇");
    QPalette palette;
    palette.setBrush(QPalette::Background,QBrush(QPixmap(":/new/prefix1/img/back.jpg").scaled(this->size())));
    this->setPalette(palette);

    startbtn=new QPushButton(this);
    startbtn->setIcon(QIcon(":/new/prefix1/img/start.png"));
    startbtn->setIconSize(QSize(75,75));
    startbtn->setGeometry(QRect(250,170,75,75));
    startbtn->setFlat(true);

    exitbtn=new QPushButton(this);
    exitbtn->setIcon(QIcon(":/new/prefix1/img/quit.png"));
    exitbtn->setIconSize(QSize(70,70));
    exitbtn->setGeometry(QRect(350,170,70,70));
    exitbtn->setFlat(true);
    //設置說明標籤
    QFont font;
    font.setFamily("Consolas");
    font.setBold(true);
    font.setPixelSize(13);
    label=new QLabel(this);
    label->setText("遊戲說明:貪吃蛇遊戲可使用按鈕或者w a s d控制蛇的走動");
    label->setFont(font);
    label->setGeometry(QRect(10,10,400,50));

    connect(exitbtn,SIGNAL(clicked()),this,SLOT(exitSlot()));
    connect(startbtn,SIGNAL(clicked()),this,SLOT(startSlot()));
}

mainWidget::~mainWidget()
{
    delete startbtn;
    delete exitbtn;
}
void mainWidget::exitSlot()
{
    if(QMessageBox::question(this,"退出遊戲","是否退出當前遊戲",QMessageBox::Yes|QMessageBox::No)==QMessageBox::Yes)
    {
        delete this;
        exit(0);
    }
}
void mainWidget::startSlot()
{

    g=new GameWidget(this);
    g->show();
}
//重寫resizeEvent
/*void mainWidget::resizeEvent(QResizeEvent *)
{
    QPalette palette;
    palette.setBrush(QPalette::Background,QBrush(QPixmap("img/back.jpg").scaled(this->size())));
    this->setPalette(palette);
    startbtn->setGeometry(QRect(this->size().width()-230,this->size().height()-100,75,75));
    exitbtn->setGeometry(QRect(this->size().width()-130,this->size().height()-100,70,70));
}*/

QMessageBox的初始化及效果

void mainWidget::exitSlot()
{
    if(QMessageBox::question(this,"退出遊戲","是否退出當前遊戲",QMessageBox::Yes|QMessageBox::No)==QMessageBox::Yes)
    {
        delete this;
        exit(0);//直接結束進程
    }
    //生成一個彈窗。question的初始化爲(鏈接的窗口,"標題","示意文字",QMessageBox::Yes|QMessageBox::No)
}

QMessageBox

遊戲窗口分析:GameWidgit

遊戲窗口是這個程序的主體部分,各種複雜的事件(槽和信號機制),狀態的更新,動畫、碰撞檢測都是在這個類(遊戲窗口就是一個繼承了普通窗口< Widget >的派生類)中完成的。

頭文件分析

#ifndef GAMEWIDGET_H
#define GAMEWIDGET_H

#include <QWidget>
//既然繼承自QWidget,肯定需要include相關的頭文件
#include <QPalette>
#include <QIcon>
//這些是爲了讓你在顯示背景圖片和窗口圖標,一般做遊戲(自己設計UI)都要用
#include <QPushButton>
//這個庫在你有鼠標事件的事後非常好用
#include <QPainter>
//如果有動畫,需要包含QPainter庫(如果只是顯示圖片可以用其他方法,但遊戲一般都有動畫)
#include <QDebug>
//主要用於調試代碼,類似於std::cout的替代品,支持QT的數據類型。
#include <QTime>
#include <QTimer>
//和計時有關的庫,做遊戲也一般都要用到
#include <QMessageBox>
//如果你的遊戲有彈出框,就要包含(非必須)
#include <QKeyEvent>
//如果你的遊戲可以用鍵盤操縱,需要包含
#include <QLabel>
//可以用來顯示字(也可以顯示圖片)
#include <QFont>
//如果你要用Qt繪製字(而不是用自己設計的UI)時,需要包含
#include <QSound>
//音頻相關,遊戲一般必須

class GameWidget : public QWidget
{
    Q_OBJECT
public:
    //構造函數
    explicit GameWidget(QWidget *parent = 0);
    ~GameWidget();
    //繪製事件,遊戲中非常重要的函數
    void paintEvent(QPaintEvent *);
    //鍵盤事件
    void keyPressEvent(QKeyEvent *);
private:
    //按鈕相當於鼠標事件
    QPushButton *upbtn;
    QPushButton *leftbtn;
    QPushButton *downbtn;
    QPushButton *rightbtn;
    QPushButton *startbtn;
    QPushButton *returnbtn;

    //記錄蛇目前運動方向
    int direction;
    //用來記錄蛇xy座標,可以看出最大能得100分
    int snake[100][2];
    int snake1[100][2];
    //計算吃過了幾個食物
    int foodcount;
    //計時器!和動畫的幀數也有關
    QTimer *timer;

    //食物的屬性
    int foodx,foody;
    int score;
    int level;

    QLabel *scorelabel;
    QLabel *levellabel;
    QLabel *scoreshow;
    QLabel *levelshow;
    QString str1,str2;
    QSound *sound;
    QSound *sound1;
signals:
//因爲有鍵盤事件(KeyPressEvent),鍵盤事件要發射不同的信號
    void UpSignal();
    void DownSignal();
    void LeftSignal();
    void RightSignal();
public slots:
    //一般來說有幾個按鈕就要有幾個槽函數
    void upbtnSlot();
    void leftbtnSlot();
    void rightbtnSlot();
    void downbtnSlot();
    void startbtnSlot();
    void returnbtnSlot();

    //本例的主函數,和timer結合使用(比較複雜,之後會詳細講)
    void timeoutSlot();
};

#endif // GAMEWIDGET_H

QDebug的說明

.c文件分析
因爲GameWidget略長,且有一些部分和MainWidget用法一致,就不放上來全部代碼(想要自己運行的可以從之前附上的鏈接下載,記得只適用於QT5
注: 需要include相應的庫文件

1.如何讓你的遊戲發出聲音
首先要載入音頻

    //聲音區:載入音頻,注意音頻文件要加入.qrc文件中
    sound=new QSound(":/listen/img/5611.wav");
    sound1=new QSound(":/listen/img/die.wav");

在什麼時候播放聲音,使用if語句判斷,一般是一個可以update()的槽函數中(本例爲timeoutSlot(),見下文)

sound->play();

2.如何即時顯示得分

    str1=QString::number(score);//轉化爲字符串

    scoreshow=new QLabel(this);//用Label輸出
    scoreshow->setFont(font);
    scoreshow->setGeometry(QRect(385,1,60,30));
    scoreshow->setText(str1);

3.如何設置交互(信號和槽機制)

    //設置按鈕操作
    connect(leftbtn,SIGNAL(clicked()),this,SLOT(leftbtnSlot()));
    connect(rightbtn,SIGNAL(clicked()),this,SLOT(rightbtnSlot()));
    connect(upbtn,SIGNAL(clicked()),this,SLOT(upbtnSlot()));
    connect(downbtn,SIGNAL(clicked()),this,SLOT(downbtnSlot()));
    connect(startbtn,SIGNAL(clicked()),this,SLOT(startbtnSlot()));
    connect(returnbtn,SIGNAL(clicked()),this,SLOT(returnbtnSlot()));
    //設置鍵盤操作
    connect(this,SIGNAL(UpSignal()),upbtn,SLOT(click()));
    connect(this,SIGNAL(DownSignal()),downbtn,SLOT(click()));
    connect(this,SIGNAL(LeftSignal()),leftbtn,SLOT(click()));
    connect(this,SIGNAL(RightSignal()),rightbtn,SLOT(click()));

connect的使用

connect(發射信號的類,SIGNAL(信號函數),接收信號的類,SLOT(槽函數));

按鈕操作的實現
QPushButton這個類中有click()的信號函數和槽函數,如果發生了鼠標點擊事件,被點擊的Button就發射click()信號,因爲我們用connect將這個信號和GameWidget中的槽函數(比如leftbtnSlot())連接起來,GameWidget就能接收到這個信號並且調用相應的槽函數。
鍵盤操作同理,因爲KeyPressEvent是我們自己寫在GameWidget類中,所以爲 ‘this’ 指針。這次connect是把鍵盤事件和按鈕聯繫了起來。

4.如何設置隨機數

    QTime t;
    t= QTime::currentTime();
    qsrand(t.msec()+t.second()*1000);

5.如何update程序

    timer=new QTimer(this);
    timer->setInterval(500);//設置時間間隔
    connect(timer,SIGNAL(timeout()),this,SLOT(timeoutSlot()));

該程序設置了一個timer(一個定時觸發器,參考這裏),並且設置了時間間隔,每過一次時間間隔發送一個timeout()信號。
而GameWidget類中定義了timeoutSlot()槽函數,每過一定時間間隔被調用,執行update()操作,來更新界面狀態,下文會講

6.(重點)timeoutSlot分析
因爲這一段很長,也只寫出代碼結構(需要完整代碼見開頭鏈接)

void GameWidget::timeoutSlot()
{
    //判斷是否吃到食物的代碼塊(判斷蛇的座標和食物的座標是否重合)
    //其中包含,發出聲音、更新食物座標(隨機更新,食物不能出現在蛇身上)、如果分數到達一定的檔次,就提升遊戲等級(改變時間間隔)

    memcpy(snake1,snake,sizeof(snake));//不能直接對目前顯示的數組進行操作,因此一開始就定義了兩個數組存放當前狀態和改變後的狀態,用memcpy拷貝

    //實現蛇的遊動
    for(int i=foodcount;i>=1;i--)
    {
        snake[i][0]=snake[i-1][0];
        snake[i][1]=snake[i-1][1];
    }

    switch(direction)
    {
    case UP:snake[0][1]--;break;
    case DOWN:snake[0][1]++;break;
    case LEFT:snake[0][0]--;break;
    case RIGHT:snake[0][0]++;break;
    }

    //判斷蛇是否撞到自身的代碼塊,如果撞到,顯示遊戲結束的彈窗,並重置遊戲

    //判斷蛇是否撞到牆體的代碼塊

    //最關鍵,調用update()函數,更新窗口狀態
    this->update();
}

memcpy輔助理解
update輔助理解
其中的碰撞檢測機制是通過判斷xy座標是否相同來進行的。
Qt工具書上似乎有其他碰撞檢測的辦法,挖個坑

7.(重點)paintEvent分析
函數聲明

void GameWidget::paintEvent(QPaintEvent *);

要定義一個繪製器painter

QPainter painter(this);

畫一個小方格(長方形)的代碼(該遊戲畫了很多很多小方格),只摘取一個:j,i是座標,20, 20是方格的大小

painter.drawRect(j,i,20,20);

讀取素材繪製
foodx要乘20的原因是這是相對座標,而不是絕對座標

painter.drawImage(foodx*20,foody*20,QImage(":/new/prefix1/img/apple.png").scaled(QSize(20,20)));

該遊戲主要用到的就是這兩種繪製方式、當然還有更多的繪製方式:參考這裏

今日學習獲得的一些經驗

  • Qt4和Qt5雖然用法差別不大,但是Qt4的代碼在Qt5上很難跑通。Qt4的遊戲源代碼值得參考,但要自己嘗試運行,還是去找Qt5的源代碼吧。
  • Qt的各種參考書和博客中給的代碼多是模板,不要直接放在Qt中運行
  • Qt中讀取資源(圖片、音頻etc)都要把文件加入qrc文件中以獲取相對路徑

吐槽:根本不是今日獲得的經驗,從上週開始寫一直拖到現在orz

end

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章