这将是咱们这个稍大一些的示例程序的最后一部分。在本章中,咱们将完成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,程序不会崩溃。
还望答疑。