這將是咱們這個稍大一些的示例程序的最後一部分。在本章中,咱們將完成GameController
中有關用戶控制的相關代碼。git
首先,咱們來給GameController
添加一個事件過濾器:github
1
2
3
4
5
6
7
8
9
|
bool GameController::eventFilter(QObject *object, QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
handleKeyPressed((QKeyEvent *)event);
return true;
} else {
return QObject::eventFilter(object, event);
}
}
|
回憶一下,咱們使用QGraphicsScene
做爲遊戲場景。爲何不直接繼承QGprahicsScene
,重寫其keyPressEvent()
函數呢?這裏的考慮是:第一,咱們不想只爲重寫一個鍵盤事件而繼承QGraphicScene
。這不符合面向對象設計的要求。繼承首先應該有「是一個(is-a)」的關係。咱們將遊戲場景繼承QGraphcisScene
固然知足這個關係,無可厚非。可是,繼承還有一個「特化」的含義,咱們只想控制鍵盤事件,並無添加其它額外的代碼,所以感受並不該該做此繼承。第二,咱們但願將表示層與控制層分離:明明已經有了GameController
,顯然,這是一個用於控制遊戲的類,那麼,爲何鍵盤控制還要放在場景中呢?這豈不將控制與表現層耦合起來了嗎?基於以上兩點考慮,咱們選擇不繼承QGraphicsScene
,而是在GameController
中爲場景添加事件過濾器,從而完成鍵盤事件的處理。下面咱們看看這個handleKeyPressed()
函數是怎樣的:ubuntu
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
void GameController::handleKeyPressed(QKeyEvent *event)
{
switch (event->key()) {
case Qt::Key_Left:
snake->setMoveDirection(Snake::MoveLeft);
break;
case Qt::Key_Right:
snake->setMoveDirection(Snake::MoveRight);
break;
case Qt::Key_Up:
snake->setMoveDirection(Snake::MoveUp);
break;
case Qt::Key_Down:
snake->setMoveDirection(Snake::MoveDown);
break;
}
}
|
這段代碼並不複雜:只是設置蛇的運動方向。記得咱們在前面的代碼中,已經爲蛇添加了運動方向的控制,所以,咱們只須要修改這個狀態,便可完成對蛇的控制。因爲前面咱們已經在蛇的對象中完成了相應控制的代碼,所以這裏的遊戲控制就是這麼簡單。接下來,咱們要完成遊戲邏輯:吃食物、生成新的食物以及咬到本身這三個邏輯:函數
1
2
3
4
5
6
7
|
void GameController::snakeAteFood(Snake *snake, Food *food)
{
scene.removeItem(food);
delete food;
addNewFood();
}
|
首先是蛇吃到食物。若是蛇吃到了食物,那麼,咱們將食物從場景中移除,而後添加新的食物。爲了不內存泄露,咱們須要在這裏 delete 食物,以釋放佔用的空間。固然,你應該想到,咱們確定會在addNewFood()
函數中使用 new 運算符從新生成新的食物。post
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
void GameController::addNewFood()
{
int x, y;
do {
x = (int) (qrand() % 100) / 10;
y = (int) (qrand() % 100) / 10;
x *= 10;
y *= 10;
} while (snake->shape().contains(snake->mapFromScene(QPointF(x + 5, y + 5))));
Food *food = new Food(x , y);
scene.addItem(food);
}
|
在addNewFood()
代碼中,咱們首先計算新的食物的座標:使用一個循環,直到找到一個不在蛇身體中的座標。爲了判斷一個座標是否是位於蛇的身體上,咱們利用蛇的shape()
函數。須要注意的是,shape()
返回元素座標系中的座標,而咱們計算而得的 x,y 座標位於場景座標系,所以咱們必須利用QGraphicsItem::mapFromScene()
將場景座標系映射爲元素座標系。當咱們計算出食物座標後,咱們在堆上從新建立這個食物,並將其添加到遊戲場景。學習
1
2
3
4
5
6
7
8
9
10
11
12
13
|
void GameController::snakeAteItself(Snake *snake)
{
QTimer::singleShot(0, this, SLOT(gameOver()));
}
void GameController::gameOver()
{
scene.clear();
snake = new Snake(*this);
scene.addItem(snake);
addNewFood();
}
|
若是蛇咬到了它本身,遊戲即宣告結束。所以,咱們直接調用gameOver()
函數。這個函數將場景清空,而後從新建立蛇並增長第一個食物。爲何咱們不直接調用gameOver()
函數,而是利用QTimer
調用呢(但願你沒有忘記QTimer::singleShot(0, ...)
的用法)?這是由於,咱們不該該在一個 update 操做中去清空整個場景。所以咱們使用QTimer
,在 update 事件以後完成這個操做。測試
至此,咱們已經把這個簡單的貪吃蛇遊戲所有完成。最後咱們來看一下運行結果:this
文末的附件中是咱們當前的所有代碼。若是你檢查下這部分代碼,會發現咱們其實尚未完成整個遊戲:Wall
對象徹底沒有實現,難度控制也沒有完成。固然,經過咱們的講解,但願你已經理解了咱們設計的原則以及各部分代碼之間的關係。若是感興趣,能夠繼續完成這部分代碼。豆子在 github 上面建立了一個代碼庫,若是你感受本身的改進比較成功,或者但願與你們分享,歡迎 clone 倉庫提交代碼!spa
附件:snake
git:git@github.com:devbean/snake-game.git.net
WIN1064BIT QTCREATOR QT5.5.1 須要delete
個人環境是Ubuntu 12.10 64bit/gcc 2.7.2
在
void GameController::snakeAteFood(Snake *snake, Food *food)
{
scene.removeItem(food);
delete food;
addNewFood();
}中,
delete food;這句代碼會致使程序報錯退出。註釋掉程序能夠正常使用,可是內存會泄露。若是在一個函數體內new/delete Food,程序不會崩潰。
還望答疑。