#(31):貪吃蛇游戲(1)
經過前面一段時間的學習,我們已經了解到有關 Qt 相當多的知識。現在,我們將把前面所講過的知識綜合起來,開發一個貪吃蛇游戲。游戲很簡單,相信大家都有見過,多多少少也都玩過。我們在實現這個貪吃蛇游戲時,會利用到事件系統、Graphics View Framework、QPainter 等相關內容,也會了解到一個游戲所具有的一些特性,比如游戲循環等,在 Qt 中如何體現出來。當然,最重要的是,通過一個相對較大的程序,學習到如何將之前的點點滴滴結合在一起。
本部分的代碼出自:[http://qtcollege.co.il/developing-a-qt-snake-game/](http://qtcollege.co.il/developing-a-qt-snake-game/),但是有一些基于軟件工程方面考慮的修改,例如常量放置的位置等。
前面說過,Qt 提供了自己的繪制系統,還提供了 Graphics View Framework。很明顯,繪制圖形和移動圖形,是一個游戲的核心。對于游戲而言,將其中的每一個部分看做對象是非常合理的,也是相當有成效的。因此,我們選擇 Graphics View Framework 作為核心框架。回憶一下,這個框架具有一系列面向對象的特性,能夠讓我們將一個個圖形作為對象進行處理。同時,Graphics View Framework 的性能很好,即便是數千上萬的圖形也沒有壓力。這一點非常適合于游戲。
正如我們前面所說,Graphics View Framework 有三個主要部分:
* `QGraphicsScene`:能夠管理元素的非 GUI 容器;
* `QGraphicsItem`:能夠被添加到場景的元素;
* `QGraphicsView`:能夠觀察場景的可視化組件視圖。
對于游戲而言,我們需要一個`QGraphicsScene`,作為游戲發生的舞臺;一個`QGraphicsView`,作為觀察游戲舞臺的組件;以及若干元素,用于表示游戲對象,比如蛇、食物以及障礙物等。
大致分析過游戲組成以及各部分的實現方式后,我們可以開始編碼了。這當然是一個 GUI 工程,主窗口應該是一個`QGraphicsView`。為了以后的實現方便(比如,我們希望向工具欄添加按鈕等),我們不會直接以`QGraphicsView`作為頂層窗口,而是將其添加到一個主窗口上。這里,我們不會使用 QtDesigner 進行界面設計,而是直接編碼完成(**注意,我們這里的代碼并不一定能夠通過編譯,因為會牽扯到其后幾章的內容,因此,如果需要編譯代碼,請在全部代碼講解完畢之后進行**):
~~~
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
class QGraphicsScene;
class QGraphicsView;
class GameController;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void adjustViewSize();
private:
void initScene();
void initSceneBackground();
QGraphicsScene *scene;
QGraphicsView *view;
GameController *game;
};
#endif // MAINWINDOW_H
~~~
在頭文件中聲明了`MainWindow`。構造函數除了初始化成員變量,還設置了窗口的大小,并且需要對場景進行初始化:
~~~
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
scene(new QGraphicsScene(this)),
view(new QGraphicsView(scene, this)),
game(new GameController(*scene, this))
{
setCentralWidget(view);
resize(600, 600);
initScene();
initSceneBackground();
QTimer::singleShot(0, this, SLOT(adjustViewSize()));
}
~~~
值得說明的是最后一行代碼。`singleShot()`函數原型如下:
~~~
static void QTimer::singleShot(int msec, QObject * receiver, const char * member);
~~~
該函數接受三個參數,簡單來說,它的作用是,在 msec 毫秒之后,調用 receiver 的 member?**槽函數**。在我們的代碼中,第一個參數傳遞的是 0,也就是 0ms 之后,調用`this->adjustViewSize()`。這與直接調用`this->adjustViewSize();`有什么區別呢?如果你看文檔,這一段的解釋很隱晦。文檔中寫到:“It is very convenient to use this function because you do not need to bother with a timerEvent or create a local QTimer object”,也就是說,它的作用是方便使用,無需重寫`timerEvent()`函數或者是創建一個局部的`QTimer`對象。當我們使用`QTimer::signleShot(0, ...)`的時候,實際上也是對`QTimer`的簡化,而不是簡單地函數調用。`QTimer`的處理是將其放到事件列表中,等到下一次事件循環開始時去調用這個函數。那么,`QTimer::signleShot(0, ...)`意思是,在下一次事件循環開始時,立刻調用指定的槽函數。在我們的例子中,我們需要在視圖繪制完畢后才去改變大小(視圖繪制當然是在`paintEvent()`事件中),因此我們需要在下一次事件循環中調用`adjustViewSize()`函數。這就是為什么我們需要用`QTimer`而不是直接調用`adjustViewSize()`。如果熟悉 flash,這相當于 flash 里面的`callLater()`函數。接下來看看`initScene()`和`initSceneBackground()`的代碼:
~~~
void MainWindow::initScene()
{
scene->setSceneRect(-100, -100, 200, 200);
}
void MainWindow::initSceneBackground()
{
QPixmap bg(TILE_SIZE, TILE_SIZE);
QPainter p(&bg);
p.setBrush(QBrush(Qt::gray));
p.drawRect(0, 0, TILE_SIZE, TILE_SIZE);
view->setBackgroundBrush(QBrush(bg));
}
~~~
`initScene()`函數設置場景的范圍,是左上角在 (-100, -100),長和寬都是 200px 的矩形。默認情況下,場景是無限大的,我們代碼的作用是設置了一個有限的范圍。Graphics View Framework 為每一個元素維護三個不同的坐標系:場景坐標,元素自己的坐標以及其相對于父組件的坐標。除了元素在場景中的位置,其它幾乎所有位置都是相對于元素坐標系的。所以,我們選擇的矩形 (-100, -100, 200, 200),實際是設置了場景的坐標系。此時,如果一個元素坐標是 (-100, -100),那么它將出現在場景左上角,(100, 100) 的坐標則是在右下角。
`initSceneBackground()`函數看似很長,實際卻很簡單。首先我們創建一個邊長`TILE_SIZE`的`QPixmap`,將其使用灰色填充矩形。我們沒有設置邊框顏色,默認就是黑色。然后將這個`QPixmap`作為背景畫刷,鋪滿整個視圖。
現在我們的程序看起來是這樣的:
[](http://files.devbean.net/images/2012/12/snake-scene.png)
在后面的章節中,我們將繼續我們的游戲之旅。下一章,我們開始創建游戲對象。
- (1)序
- (2)Qt 簡介
- (3)Hello, world!
- (4)信號槽
- (5)自定義信號槽
- (6)Qt 模塊簡介
- (7)MainWindow 簡介
- (8)添加動作
- (9)資源文件
- (10)對象模型
- (11)布局管理器
- (12)菜單欄、工具欄和狀態欄
- (13)對話框簡介
- (14)對話框數據傳遞
- (15)標準對話框 QMessageBox
- (16)深入 Qt5 信號槽新語法
- (17)文件對話框
- (18)事件
- (19)事件的接受與忽略
- (21)事件過濾器
- (22)事件總結
- (23)自定義事件
- (24)Qt 繪制系統簡介
- (25)畫刷和畫筆
- (26)反走樣
- (27)漸變
- (28)坐標系統
- (29)繪制設備
- (30)Graphics View Framework
- (31)貪吃蛇游戲(1)
- (32)貪吃蛇游戲(2)
- (33)貪吃蛇游戲(3)
- (34)貪吃蛇游戲(4)
- (35)文件
- (36)二進制文件讀寫
- (37)文本文件讀寫
- (38)存儲容器
- (39)遍歷容器
- (40)隱式數據共享
- (41)model/view 架構
- (42)QListWidget、QTreeWidget 和 QTableWidget
- (43)QStringListModel
- (44)QFileSystemModel
- (45)模型
- (46)視圖和委托
- (47)視圖選擇
- (48)QSortFilterProxyModel
- (49)自定義只讀模型
- (50)自定義可編輯模型
- (51)布爾表達式樹模型
- (52)使用拖放
- (53)自定義拖放數據
- (54)剪貼板
- (55)數據庫操作
- (56)使用模型操作數據庫
- (57)可視化顯示數據庫數據
- (58)編輯數據庫外鍵
- (59)使用流處理 XML
- (60)使用 DOM 處理 XML
- (61)使用 SAX 處理 XML
- (62)保存 XML
- (63)使用 QJson 處理 JSON
- (64)使用 QJsonDocument 處理 JSON
- (65)訪問網絡(1)
- (66)訪問網絡(2)
- (67)訪問網絡(3)
- (68)訪問網絡(4)
- (69)進程
- (70)進程間通信
- (71)線程簡介
- (72)線程和事件循環
- (73)Qt 線程相關類
- (74)線程和 QObject
- (75)線程總結
- (76)QML 和 QtQuick 2
- (77)QML 語法
- (78)QML 基本元素
- (79)QML 組件
- (80)定位器
- (81)元素布局
- (82)輸入元素
- (83)Qt Quick Controls
- (84)Repeater
- (85)動態視圖
- (86)視圖代理
- (87)模型-視圖高級技術
- (88)Canvas
- (89)Canvas(續)