找回密码
 立即注册
收起左侧

Qt学习之路第34篇 贪吃蛇游戏(4)

11
回复
23822
查看
[复制链接]
累计签到:3 天
连续签到:1 天
来源: 2013-9-9 15:32:02 显示全部楼层 |阅读模式
版权声明

该文章原创于Qter开源社区(www.qter.org),作者devbean,博客www.devbean.net,转载请注明出处!



这将是我们这个稍大一些的示例程序的最后一部分。在本章中,我们将完成 GameController 中有关用户控制的相关代码。


首先,我们来给 GameControll 添加一个事件过滤器:

  1. bool GameController::eventFilter(QObject *object, QEvent *event)
  2. {
  3.     if (event->type() == QEvent::KeyPress) {
  4.         handleKeyPressed((QKeyEvent *)event);
  5.         return true;
  6.     } else {
  7.         return QObject::eventFilter(object, event);
  8.     }
  9. }
复制代码


回忆一下,我们使用 QGraphicsScene 作为游戏场景。为什么不直接继承 QGprahicsScene,重写其 keyPressEvent() 函数呢?这里的考虑是:第一,我们不想只为重写一个键盘事件而继承 QGraphicScene。这不符合面向对象设计的要求。继承首先应该有“是一个(is-a)”的关系。我们将游戏场景继承 QGraphcisScene 当然满足这个关系,无可厚非。但是,继承还有一个“特化”的含义,我们只想控制键盘事件,并没有添加其它额外的代码,因此感觉并不应该作此继承。第二,我们希望将表示层与控制层分离:明明已经有了 GameController,显然,这是一个用于控制游戏的类,那么,为什么键盘控制还要放在场景中呢?这岂不将控制与表现层耦合起来了吗?基于以上两点考虑,我们选择不继承 QGraphicsScene,而是在 GameController 中为场景添加事件过滤器,从而完成键盘事件的处理。下面我们看看这个 handleKeyPressed() 函数是怎样的:

  1. void GameController::handleKeyPressed(QKeyEvent *event)
  2. {
  3.     switch (event->key()) {
  4.         case Qt::Key_Left:
  5.             snake->setMoveDirection(Snake::MoveLeft);
  6.             break;
  7.         case Qt::Key_Right:
  8.             snake->setMoveDirection(Snake::MoveRight);
  9.             break;
  10.         case Qt::Key_Up:
  11.             snake->setMoveDirection(Snake::MoveUp);
  12.             break;
  13.         case Qt::Key_Down:
  14.             snake->setMoveDirection(Snake::MoveDown);
  15.             break;
  16.     }
  17. }
复制代码

这段代码并不复杂:只是设置蛇的运动方向。记得我们在前面的代码中,已经为蛇添加了运动方向的控制,因此,我们只需要修改这个状态,即可完成对蛇的控制。由于前面我们已经在蛇的对象中完成了相应控制的代码,因此这里的游戏控制就是这么简单。接下来,我们要完成游戏逻辑:吃食物、生成新的食物以及咬到自己这三个逻辑:

  1. void GameController::snakeAteFood(Snake *snake, Food *food)
  2. {
  3.     scene.removeItem(food);
  4.     delete food;

  5.     addNewFood();
  6. }
复制代码

首先是蛇吃到食物。如果蛇吃到了食物,那么,我们将食物从场景中移除,然后添加新的食物。为了避免内存泄露,我们需要在这里 delete 食物,以释放占用的空间。当然,你应该想到,我们肯定会在 addNewFood() 函数中使用 new 运算符重新生成新的食物。

  1. void GameController::addNewFood()
  2. {
  3.     int x, y;

  4.     do {
  5.         x = (int) (qrand() % 100) / 10;
  6.         y = (int) (qrand() % 100) / 10;

  7.         x *= 10;
  8.         y *= 10;
  9.     } while (snake->shape().contains(snake->mapFromScene(QPointF(x + 5, y + 5))));

  10.     Food *food = new Food(x , y);
  11.     scene.addItem(food);
  12. }
复制代码

在 addNewFood() 代码中,我们首先计算新的食物的坐标:使用一个循环,直到找到一个不在蛇身体中的坐标。为了判断一个坐标是不是位于蛇的身体上,我们利用蛇的 shape() 函数。需要注意的是,shape() 返回元素坐标系中的坐标,而我们计算而得的 x,y 坐标位于场景坐标系,因此我们必须利用 QGraphicsItem::mapFromScene() 将场景坐标系映射为元素坐标系。当我们计算出食物坐标后,我们在堆上重新创建这个食物,并将其添加到游戏场景。

  1. void GameController::snakeAteItself(Snake *snake)
  2. {
  3.     QTimer::singleShot(0, this, SLOT(gameOver()));
  4. }

  5. void GameController::gameOver()
  6. {
  7.     scene.clear();

  8.     snake = new Snake(*this);
  9.     scene.addItem(snake);
  10.     addNewFood();
  11. }
复制代码

如果蛇咬到了它自己,游戏即宣告结束。因此,我们直接调用 gameOver() 函数。这个函数将场景清空,然后重新创建蛇并增加第一个食物。为什么我们不直接调用 gameOver() 函数,而是利用 QTimer 调用呢(希望你没有忘记 QTimer::singleShot(0, …) 的用法)?这是因为,我们不应该在一个 update 操作中去清空整个场景。因此我们使用 QTimer,在 update 事件之后完成这个操作。



至此,我们已经把这个简单的贪吃蛇游戏全部完成。最后我们来看一下运行结果:




文末的附件中是我们当前的全部代码。如果你检查下这部分代码,会发现我们其实还没有完成整个游戏:Wall 对象完全没有实现,难度控制也没有完成。当然,通过我们的讲解,希望你已经理解了我们设计的原则以及各部分代码之间的关系。如果感兴趣,可以继续完成这部分代码。豆子在 github 上面创建了一个代码库,如果你感觉自己的改进比较成功,或者希望与大家分享,欢迎 clone 仓库提交代码!

git:git@github.com:devbean/snake-game.git

代码:





本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
参与人数 1人气 +2 收起 理由
wjq57 + 2 对我帮助很大!

查看全部评分总评分 : 人气 +2

回复

使用道具 举报

累计签到:5 天
连续签到:1 天
2014-12-12 11:22:13 显示全部楼层
很不错的代码,小游戏也很好玩
回复 支持 反对

使用道具 举报

累计签到:19 天
连续签到:1 天
2015-7-15 21:12:59 显示全部楼层
刚接触qt,这几个cpp文件执行有先后顺序么,= =可能还是c的思维。其中的boundingrect和paint我都没看到调用啊,是怎么执行的?
回复 支持 反对

使用道具 举报

累计签到:18 天
连续签到:1 天
2015-10-10 18:16:41 显示全部楼层
seasidemym 发表于 2015-7-15 21:12
刚接触qt,这几个cpp文件执行有先后顺序么,= =可能还是c的思维。其中的boundingrect和paint我都没看到调用 ...

可以先去QT的事件机制,我也是从C转过来的,搞了好久才明白面向对象的编程方法
回复 支持 反对

使用道具 举报

累计签到:109 天
连续签到:1 天
2016-1-18 15:29:21 显示全部楼层
从作者博客copy过来的可用的代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复 支持 反对

使用道具 举报

累计签到:69 天
连续签到:1 天
2016-7-14 19:53:08 显示全部楼层
楼上的这一份代码有一点问题,在Qt5.6上运行,每当Snake与小红点相撞的时候就会中止程序
回复 支持 反对

使用道具 举报

尚未签到

2016-8-15 15:53:57 显示全部楼层
一枝梅不落 发表于 2016-7-14 19:53
楼上的这一份代码有一点问题,在Qt5.6上运行,每当Snake与小红点相撞的时候就会中止程序 ...

我也是的!!!!!!!!
回复 支持 反对

使用道具 举报

尚未签到

2016-8-15 16:07:59 显示全部楼层
一枝梅不落 发表于 2016-7-14 19:53
楼上的这一份代码有一点问题,在Qt5.6上运行,每当Snake与小红点相撞的时候就会中止程序 ...

git上的是可以正常运行的
回复 支持 反对

使用道具 举报

尚未签到

2017-7-24 10:51:24 显示全部楼层
#include<QGraphicsItem>
#include<QRectF>
为什么我把这include的顺序调换之后,mapFromScene这个函数就会报错
也就是
#include<QRectF>
#include<QGraphicsItem>
这样写会错,这是为啥呢
回复 支持 反对

使用道具 举报

累计签到:1 天
连续签到:1 天
2017-11-24 16:14:53 显示全部楼层
一枝梅不落 发表于 2016-7-14 19:53
楼上的这一份代码有一点问题,在Qt5.6上运行,每当Snake与小红点相撞的时候就会中止程序 ...

你好,请问解决了吗?
回复 支持 反对

使用道具 举报

累计签到:2 天
连续签到:1 天
2017-12-29 11:22:43 显示全部楼层
很不错,有趣,感谢分享。
回复 支持 反对

使用道具 举报

尚未签到

2019-12-23 17:05:08 显示全部楼层
学习QT非常有用
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了