首页 Qt 学习之路 2 Qt 学习之路 2(33):贪吃蛇游戏(3)

Qt 学习之路 2(33):贪吃蛇游戏(3)

20 2.5K

继续前面一章的内容。上次我们讲完了有关蛇的静态部分,也就是绘制部分。现在,我们开始添加游戏控制的代码。首先我们从最简单的四个方向键开始:

void Snake::moveLeft()
{
    head.rx() -= SNAKE_SIZE;
    if (head.rx() < -100) {
        head.rx() = 100;
    }
}

void Snake::moveRight()
{
    head.rx() += SNAKE_SIZE;
    if (head.rx() > 100) {
        head.rx() = -100;
    }
}

void Snake::moveUp()
{
    head.ry() -= SNAKE_SIZE;
    if (head.ry() < -100) {
        head.ry() = 100;
    }
}

void Snake::moveDown()
{
    head.ry() += SNAKE_SIZE;
    if (head.ry() > 100) {
        head.ry() = -100;
    }
}

我们有四个以 move 开头的函数,内容都很类似:分别以 SNAKE_SIZE 为基准改变头部坐标,然后与场景边界比较,大于边界值时,设置为边界值。这么做的结果是,当蛇运动到场景最右侧时,会从最左侧出来;当运行到场景最上侧时,会从最下侧出来。

然后我们添加一个比较复杂的函数,借此,我们可以看出 Graphics View Framework 的强大之处:

void Snake::handleCollisions()
{
    QList collisions = collidingItems();

    // Check collisions with other objects on screen
    foreach (QGraphicsItem *collidingItem, collisions) {
        if (collidingItem->data(GD_Type) == GO_Food) {
            // Let GameController handle the event by putting another apple
            controller.snakeAteFood(this, (Food *)collidingItem);
            growing += 1;
        }
    }

    // Check snake eating itself
    if (tail.contains(head)) {
        controller.snakeAteItself(this);
    }
}

顾名思义,handleCollisions()的意思是处理碰撞,也就是所谓的“碰撞检测”。首先,我们使用collidingItems()取得所有碰撞的元素。这个函数的签名是:

QList<QGraphicsItem *> QGraphicsItem::collidingItems(
        Qt::ItemSelectionMode mode = Qt::IntersectsItemShape) const

该函数返回与这个元素碰撞的所有元素。Graphcis View Framework 提供了四种碰撞检测的方式:

  • Qt::ContainsItemShape:如果被检测物的形状(shape())完全包含在检测物内,算做碰撞;
  • Qt::IntersectsItemShape:如果被检测物的形状(shape())与检测物有交集,算做碰撞;
  • Qt::ContainsItemBoundingRect:如果被检测物的包含矩形(boundingRect())完全包含在检测物内,算做碰撞;
  • Qt::IntersectsItemBoundingRect:如果被检测物的包含矩形(boundingRect())与检测物有交集,算做碰撞。

注意,该函数默认是Qt::IntersectsItemShape。回忆一下,我们之前编写的代码,FoodboundingRect()要大于其实际值,却不影响我们的游戏逻辑判断,这就是原因:因为我们使用的是Qt::IntersectsItemShape判断检测,这与boundingRect()无关。

后面的代码就很简单了。我们遍历所有被碰撞的元素,如果是食物,则进行吃食物的算法,同时将蛇的长度加 1。最后,如果身体包含了头,那就是蛇吃了自己的身体。

还记得我们在 Food 类中有这么一句:

setData(GD_Type, GO_Food);

QGraphicsItem::setData()以键值对的形式设置元素的自定义数据。所谓自定义数据,就是对应用程序有所帮助的用户数据。Qt 不会使用这种机制来存储数据,因此你可以放心地将所需要的数据存储到元素对象。例如,我们在Food的构造函数中,将GD_Type的值设置为GO_Food。那么,这里我们取出GD_Type,如果其值是GO_Food,意味着这个QGraphicsItem就是一个Food,因此我们可以将其安全地进行后面的类型转换,从而完成下面的代码。

下面是advance()函数的代码:

void Snake::advance(int step)
{
    if (!step) {
        return;
    }
    if (tickCounter++ % speed != 0) {
        return;
    }
    if (moveDirection == NoMove) {
        return;
    }

    if (growing > 0) {
        QPointF tailPoint = head;
        tail << tailPoint;
        growing -= 1;
    } else {
        tail.takeFirst();
        tail << head;
    }

    switch (moveDirection) {
        case MoveLeft:
            moveLeft();
            break;
        case MoveRight:
            moveRight();
            break;
        case MoveUp:
            moveUp();
            break;
        case MoveDown:
            moveDown();
            break;
    }

    setPos(head);
    handleCollisions();
}

QGraphicsItem::advance()函数接受一个 int 作为参数。这个 int 代表该函数被调用的时间。QGraphicsItem::advance()函数会被QGraphicsScene::advance()函数调用两次:第一次时这个 int 为 0,代表即将开始调用;第二次这个 int 为 1,代表已经开始调用。在我们的代码中,我们只使用不为 0 的阶段,因此当 !step 时,函数直接返回。

tickCounter实际是我们内部的一个计时器。我们使用 speed 作为蛇的两次动作的间隔时间,直接影响到游戏的难度。speed 值越大,两次运动的间隔时间越大,游戏越简单。这是因为随着 speed 的增大,tickCounter % speed != 0 的次数响应越多,刷新的次数就会越少,蛇运动得越慢。

moveDirection显然就是运动方向,当是 NoMove 时,函数直接返回。

growing是正在增长的方格数。当其大于 0 时,我们将头部追加到尾部的位置,同时减少一个方格;当其小于 0 时,我们删除第一个,然后把头部添加进去。我们可以把 growing 看做即将发生的变化。比如,我们将 growing 初始化为 7。第一次运行advance()时,由于 7 > 1,因此将头部追加,然后 growing 减少 1。直到 growing 为 0,此时,蛇的长度不再发生变化,直到我们吃了一个食物。

下面是相应的方向时需要调用对应的函数。最后,我们设置元素的坐标,同时检测碰撞。

20 评论

仗贱天涯 2013年1月8日 - 14:04

受益颇多,把前面的内容很好的串起来了,但有几点疑问:1.文中写道我们只关心0这一阶段,但为0时直接返回了,还怎么利用?2.文中说speed越大速度越快,但tickCounter % speed != 0 的次数越多就越容易返回,后面的path路径都没机会更新,刷新速度怎么会越多呢?,我尝试改大speed值,似乎游戏速度变慢了3.当新的蛇形路径形成时,什么时候重新执行paint函数? 😆

回复
豆子 2013年1月8日 - 14:49

1. 笔误,应该是不为 0 的阶段(不好意思~);
2. 这个是我理解有误,speed 是间隔时间,也就是蛇的运行间隔时间,不是游戏速度,已经改过了(感谢指出!);
3. paint() 函数一般由 QGraphicsView 调用,此时是在 advance() 函数修改了坐标之后,会自动调用 paint() 函数。

回复
仗贱天涯 2013年1月9日 - 14:24

又来问问题啦, 😛 ,path.addRect(QRectF(0, 0, SNAKE_SIZE, SNAKE_SIZE));,理应每次刷新在(0,0)的位置都会有一个方格啊,还是场景坐标与元素坐标有什么不同之处?我重新编写的代码中food的起位置(0,-50)与snake的起始位置(0, 0)不在一条直线上,snake起始位置(-10, 0)才到一条直线上去,我把(-10, 0)映射为元素坐标后,每次移动,起始位置都会有一个方格?我不明白我的food位置和snake位置会相差10,还有游戏运行后(0,0)位置的方格为什么不会出现。有点罗嗦,不知道能否看的懂

回复
豆子 2013年1月10日 - 10:08

的确没有看懂…元素坐标系和场景坐标系是不一样的,这个文档上有说明。(0, 0)位置的方格,如果你指的是 snake 的,这是因为这个 (0, 0) 坐标是 snake 坐标系下的,不是场景中的,因此它实际就是第一个方格。

回复
仗贱天涯 2013年1月10日 - 11:10

还是谢谢了,自己慢慢琢磨吧,是你的脑残粉哦, 😳

豆子 2013年1月10日 - 14:14

共同提高啦

Hans 2014年5月16日 - 00:36

初学者感觉看着比较吃力……
1.setPos()的作用是什么呢?看文档也不是很懂……

2.如果我用QList保存一系矩形的位置,我想让这些矩形以每秒30帧,每3帧同时向同一个方向移动10个单位,用类似本例中蛇移动的方法该怎么实现呢?【比如位置在(30,10),(30,20),(30, 30)的三个矩形移动到(40,10),(40,20),(40, 30)】

回复
豆子 2014年5月16日 - 09:03

setPos() 函数就是设置这个 item 的位置,其坐标是相对父对象的。

如果要实现你说的这种效果,应该在 advance() 函数中对 QList 中所有矩形的坐标进行修改。这个函数相当于动画的每一帧的改变。

回复
Hans 2014年5月16日 - 11:21

我是这样理解的啊,但是在advance()里的setPos()该设置哪个点呢?对这个还是不理解

回复
wenmd0703 2014年7月15日 - 11:29

growing是正在增长的方格数。当其大于 0 时,我们将头部追加到尾部的位置,同时减少一个方格;当其小于 0 时,我们删除第一个,然后把头部添加进去。我们可以把 growing 看做即将发生的变化。比如,我们将 growing 初始化为 7。第一次运行advance()时,由于 7 > 1,因此将头部追加,然后 growing 减少 1。直到 growing 为 0,此时,蛇的长度不再发生变化,直到我们吃了一个食物。
博主,这个growing到底是干嘛的?不是代表蛇的长度么?没有理解,谢谢解答!

回复
wenmd0703 2014年7月15日 - 11:57

growing最小是0吧,初始化7是为了让它刚开始长度为7么

回复
豆子 2014年7月16日 - 10:44

是的,初始化为 7 就是让它开始的长度是 7,相当于生命值这种设定。

回复
hkax 2016年5月30日 - 09:01

蛇的移动这块,如何检测尾部的正确位置呢?就是确定尾部在什么地方。

回复
问题一号 2016年12月26日 - 11:42

QList collisions = collidingItems();
这句代码不理解,他是怎么取得所有碰撞的元素的?

回复
豆子 2016年12月26日 - 20:59

这个是 Qt 内置的函数,如果你对算法感兴趣,可以阅读其实现代码

回复
柒月 2017年3月23日 - 17:16

if (growing > 0) {
QPointF tailPoint = head;
tail << tailPoint;
growing -= 1;
} else {
tail.takeFirst();
tail << head;
}
你好,上面这段代码是什么意思?谢谢了

回复
打不死的黄妖精 2019年5月17日 - 15:31

“随着 speed 的增大,tickCounter % speed != 0 的次数响应越多,刷新的次数就会越少,蛇运动得越慢。”豆哥,这句我觉得有问题啊,这个speed的增大与这个等式等不等于零的次数好像关系不大吧。另外我编译了一下,一运行游戏就退出,不知道哪儿出了问题,,,

回复
衡琪 2020年4月1日 - 15:54

GameController中游戏循环器每秒30帧,每帧都会调用scene的advance()。
scene的advance()调用一次,会以0和1的step调用item的advance两次
_head值就是item的step为1的advance中每次移动更新的

回复
tron 2020年10月10日 - 15:00

Qt 不会使用这种机制来存储数据,因此你可以放心地将所需要的数据存储到元素对象。
“不会”是什么意思?

setData(GD_Type, GO_Food);

if (collidingItem->data(GD_Type) == GO_Food) {
// Let GameController handle the event by putting another apple
controller.snakeAteFood((Food *)collidingItem);
growing += 5;
}

在Food的构造函数中,将GD_Type的值设置为GO_Food。那么,这里我们取出GD_Type,如果其值是GO_Food,意味着这个QGraphicsItem就是一个Food,因此我们可以将其安全地进行后面的类型转换.

这里这些都是什么啊...

回复
豆子 2020年10月13日 - 17:20

我们将额外的数据使用 setData() 函数存在 QGraphicsItem 中,用于标识 QGraphicsItem 的类型。

回复

回复 打不死的黄妖精 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2