#(32):貪吃蛇游戲(2)
下面我們繼續上一章的內容。在上一章中,我們已經完成了地圖的設計,當然是相當簡單的。在我們的游戲中,另外的主角便是蛇和食物。下面我們便開始這部分的開發。
我們的地圖是建立在`QGraphicsScene`的基礎之上的,所以,里面的對象應該是`QGraphicsItem`實例。通常,我們會把所有的圖形元素(這里便是游戲中需要的對象,例如蛇、食物等)設計為`QGraphicsItem`的子類,在這個類中添加繪制自身的代碼以及動畫邏輯。這也是面向對象的開發方式:封裝自己的屬性和操作。在我們的游戲中,應該有三個對象:蛇 Snake、食物 Food 以及墻 Wall。
我們從食物開始。因為它是最簡單的。我們將其作為一個紅色的小圓餅,大小要比地圖中的一個方格要小,因此我們可以將其放置在一個方格中。正如上面分析的那樣,我們的`Food`類需要繼承`QGraphicsItem`。按照接口約束,`QGraphicsItem`的子類需要重寫至少兩個函數:`boundingRect()`和`paint()`。
`boundingRect()`返回一個用于包裹住圖形元素的矩形,也就是這個圖形元素的范圍。需要注意的是,這個矩形必須能夠**完全包含**圖形元素。所謂“完全包含”,意思是,在圖形元素有動畫的時候,這個矩形也必須將整個圖形元素包含進去。如果范圍矩形過小。圖形會被剪切;如果范圍矩形過大,就會影響性能。
`paint()`的作用是使用`QPainter`將圖形元素繪制出來。
下面是 food.h 和 food.cpp 的內容:
~~~
////////// food.h //////////
#ifndef FOOD_H
#define FOOD_H
#include <QGraphicsItem>
class Food : public QGraphicsItem
{
public:
Food(qreal x, qreal y);
QRectF boundingRect() const;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *);
QPainterPath shape() const;
};
#endif // FOOD_H
////////// food.cpp //////////
#include <QPainter>
#include "constants.h"
#include "food.h"
static const qreal FOOD_RADIUS = 3;
Food::Food(qreal x, qreal y)
{
setPos(x, y);
setData(GD_Type, GO_Food);
}
QRectF Food::boundingRect() const
{
return QRectF(-TILE_SIZE, -TILE_SIZE,
TILE_SIZE * 2, TILE_SIZE * 2 );
}
void Food::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
painter->fillPath(shape(), Qt::red);
painter->restore();
}
QPainterPath Food::shape() const
{
QPainterPath p;
p.addEllipse(QPointF(TILE_SIZE / 2, TILE_SIZE / 2), FOOD_RADIUS, FOOD_RADIUS);
return p;
}
~~~
雖然這段代碼很簡單,我們還是有必要解釋一下。構造函數接受兩個參數:x 和 y,用于指定該元素的坐標。`setData()`函數是我們之后要用到的,這里簡單提一句,它的作用為該圖形元素添加額外的數據信息,類似于散列一樣的鍵值對的形式。`boundingRect()`簡單地返回一個`QRect`對象。由于我們的元素就是一個圓形,所以我們返回的是一個簡單的矩形。注意,這個矩形的范圍實際是四倍于實際區域的:以元素坐標 (x, y) 為中心,邊長為`TILE_SIZE * 2`的正方形。我們還重寫了`shape()`函數。這也是一個虛函數,但是并不是必須覆蓋的。這個函數返回的是元素實際的路徑。所謂路徑,可以理解成元素的矢量輪廓線,就是`QPainterPath`所表示的。我們使用`addEllipse()`函數,添加了一個圓心為 (TILE_SIZE / 2, TILE_SIZE / 2),半徑 FOOD_RADIUS 的圓,其范圍是左上角為 (x, y) 的矩形。由于設置了`shape()`函數,`paint()`反而更簡單。我們所要做的,就是把`shape()`函數定義的路徑繪制出來。注意,我們使用了`QPainter::save()`和`QPainter::restore()`兩個函數,用于保存畫筆狀態。
現在我們有了第一個圖形元素,那么,就讓我們把它添加到場景中吧!對于一個游戲,通常需要有一個中心控制的類,用于控制所有游戲相關的行為。我們將其取名為`GameController`。
`GameController`的工作是,初始化場景中的游戲對象,開始游戲循環。每一個游戲都需要有一個游戲循環,類型于事件循環。想象一個每秒滴答 30 次的表。每次響起滴答聲,游戲對象才有機會執行相應的動作:移動、檢查碰撞、攻擊或者其它一些游戲相關的活動。為方便起見,我們將這一次滴答成為一幀,那么,每秒 30 次滴答,就是每秒 30 幀。游戲循環通常使用定時器實現,因為應用程序不僅僅是一個游戲循環,還需要響應其它事件,比如游戲者的鼠標鍵盤操作。正因為如此,我們不能簡單地使用無限的 for 循環作為游戲循環。
在 Graphics View Framework 中,每一幀都應該調用一個稱為`advance()`的函數。`QGraphicsScene::advance()`會調用場景中每一個元素自己的`advance()`函數。所以,如果圖形元素需要做什么事,必須重寫`QGraphicsItem`的`advance()`,然后在游戲循環中調用這個函數。
`GameController`創建并開始游戲循環。當然,我們也可以加入`pause()`和`resume()`函數。現在,我們來看看它的實現:
~~~
GameController::GameController(QGraphicsScene *scene, QObject *parent) :
QObject(parent),
scene(scene),
snake(new Snake(this))
{
timer.start(1000/33);
Food *a1 = new Food(0, -50);
scene->addItem(a1);
scene->addItem(snake);
scene->installEventFilter(this);
resume();
}
~~~
`GameController`的構造函數。首先開啟充當游戲循環的定時器,定時間隔是 1000 / 33 毫秒,也就是每秒 30(1000 / 33 = 30)幀。`GameController`有兩個成員變量:scene 和 snake,我們將第一個食物和蛇都加入到場景中。同時,我們為`GameController`添加了事件過濾器,以便監聽鍵盤事件。這里我們先不管這個事件過濾器,直接看看后面的代碼:
~~~
void GameController::pause()
{
disconnect(&timer, SIGNAL(timeout()),
scene, SLOT(advance()));
}
void GameController::resume()
{
connect(&timer, SIGNAL(timeout()),
scene, SLOT(advance()));
}
~~~
`pause()`和`resume()`函數很簡答:我們只是連接或者斷開定時器的信號。當我們把這一切都準備好之后,我們把`GameController`添加到`MainWindow`中:
~~~
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent),
game(new GameController(scene, this))
{
...
}
~~~
由于`GameController`在構造時已經開始游戲循環,因此我們不需要另外調用一個所謂的“start”函數。這樣,我們就把第一個食物添加到了游戲場景:
[](http://files.devbean.net/images/2012/12/snake_1.png)
接下來是有關蛇的處理。
蛇要更復雜一些。在我們的游戲中,蛇是由黃色的小方塊組成,這是最簡單的實現方式了。第一個是蛇的頭部,緊接著是它的身體。對此,我們有兩個必須面對的困難:
1. 蛇具有復雜得多的形狀。因為蛇的形狀隨著游戲者的控制而不同,因此,我們必須找出一個能夠恰好包含蛇頭和所有身體塊的矩形。這也是 boundingRect() 函數所要解決的問題。
2. 蛇會長大(比如吃了食物之后)。因此,我們需要在蛇對象中增加一個用于代表蛇身體長度的`growing`變量:當`growing`為正數時,蛇的身體增加一格;當`growing`為負數時,蛇的身體減少一格。
3. `advance()`函數用于編碼移動部分,這個函數會在一秒內調用 30 次(這是我們在`GameController`的定時器中決定的)。
我們首先從`boundingRect()`開始看起:
~~~
QRectF Snake::boundingRect() const
{
qreal minX = head.x();
qreal minY = head.y();
qreal maxX = head.x();
qreal maxY = head.y();
foreach (QPointF p, tail) {
maxX = p.x() > maxX ? p.x() : maxX;
maxY = p.y() > maxY ? p.y() : maxY;
minX = p.x() < minX ? p.x() : minX;
minY = p.y() < minY ? p.y() : minY;
}
QPointF tl = mapFromScene(QPointF(minX, minY));
QPointF br = mapFromScene(QPointF(maxX, maxY));
QRectF bound = QRectF(tl.x(), // x
tl.y(), // y
br.x() - tl.x() + SNAKE_SIZE, // width
br.y() - tl.y() + SNAKE_SIZE //height
);
return bound;
}
~~~
這個函數的算法是:遍歷蛇身體的每一個方塊,找出所有部分的最大的 x 坐標和 y 坐標,以及最小的 x 坐標和 y 坐標。這樣,夾在其中的便是蛇身體的外圍區域。
`shape()`函數決定了蛇身體的形狀,我們遍歷蛇身體的每一個方塊向路徑中添加:
~~~
QPainterPath Snake::shape() const
{
QPainterPath path;
path.setFillRule(Qt::WindingFill);
path.addRect(QRectF(0, 0, SNAKE_SIZE, SNAKE_SIZE));
foreach (QPointF p, tail) {
QPointF itemp = mapFromScene(p);
path.addRect(QRectF(itemp.x(), itemp.y(), SNAKE_SIZE, SNAKE_SIZE));
}
return path;
}
~~~
在我們實現了`shape()`函數的基礎之上,`paint()`函數就很簡單了:
~~~
void Snake::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
painter->save();
painter->fillPath(shape(), Qt::yellow);
painter->restore();
}
~~~
現在我們已經把蛇“畫”出來。下一章中,我們將讓它“動”起來,從而完成我們的貪吃蛇游戲。
- (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(續)