Qt 学习之路 2(31):贪吃蛇游戏(1)

经过前面一段时间的学习,我们已经了解到有关 Qt 相当多的知识。现在,我们将把前面所讲过的知识综合起来,开发一个贪吃蛇游戏。游戏很简单,相信大家都有见过,多多少少也都玩过。我们在实现这个贪吃蛇游戏时,会利用到事件系统、Graphics View Framework、QPainter 等相关内容,也会了解到一个游戏所具有的一些特性,比如游戏循环等,在 Qt 中如何体现出来。当然,最重要的是,通过一个相对较大的程序,学习到如何将之前的点点滴滴结合在一起。

本部分的代码出自:http://qtcollege.co.il/developing-a-qt-snake-game/,但是有一些基于软件工程方面考虑的修改,例如常量放置的位置等。

前面说过,Qt 提供了自己的绘制系统,还提供了 Graphics View Framework。很明显,绘制图形和移动图形,是一个游戏的核心。对于游戏而言,将其中的每一个部分看做对象是非常合理的,也是相当有成效的。因此,我们选择 Graphics View Framework 作为核心框架。回忆一下,这个框架具有一系列面向对象的特性,能够让我们将一个个图形作为对象进行处理。同时,Graphics View Framework 的性能很好,即便是数千上万的图形也没有压力。这一点非常适合于游戏。

正如我们前面所说,Graphics View Framework 有三个主要部分:

  • QGraphicsScene:能够管理元素的非 GUI 容器;
  • QGraphicsItem:能够被添加到场景的元素;
  • QGraphicsView:能够观察场景的可视化组件视图。

对于游戏而言,我们需要一个QGraphicsScene,作为游戏发生的舞台;一个QGraphicsView,作为观察游戏舞台的组件;以及若干元素,用于表示游戏对象,比如蛇、食物以及障碍物等。

大致分析过游戏组成以及各部分的实现方式后,我们可以开始编码了。这当然是一个 GUI 工程,主窗口应该是一个QGraphicsView。为了以后的实现方便(比如,我们希望向工具栏添加按钮等),我们不会直接以QGraphicsView作为顶层窗口,而是将其添加到一个主窗口上。这里,我们不会使用 QtDesigner 进行界面设计,而是直接编码完成(注意,我们这里的代码并不一定能够通过编译,因为会牵扯到其后几章的内容,因此,如果需要编译代码,请在全部代码讲解完毕之后进行):

 
 
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

class QGraphicsScene;
class QGraphicsView;

class GameController;

class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();

private slots:
void adjustViewSize();

private:
void initScene();
void initSceneBackground();

QGraphicsScene *scene;
QGraphicsView *view;

GameController *game;
};

#endif // MAINWINDOW_H

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
 
#include <QMainWindow>
 
class QGraphicsScene;
class QGraphicsView;
 
class GameController;
 
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = 0);
    ~MainWindow();
 
private slots:
    void adjustViewSize();
 
private:
    void initScene();
    void initSceneBackground();
 
    QGraphicsScene *scene;
    QGraphicsView *view;
 
    GameController *game;
};
 
#endif // MAINWINDOW_H

在头文件中声明了MainWindow。构造函数除了初始化成员变量,还设置了窗口的大小,并且需要对场景进行初始化:

 
 
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
scene(new QGraphicsScene(this)),
view(new QGraphicsView(scene, this)),
game(new GameController(*scene, this))
{
setCentralWidget(view);
resize(600, 600);

initScene();
initSceneBackground();

QTimer::singleShot(0, this, SLOT(adjustViewSize()));
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    scene(new QGraphicsScene(this)),
    view(new QGraphicsView(scene, this)),
    game(new GameController(*scene, this))
{
    setCentralWidget(view);
    resize(600, 600);
 
    initScene();
    initSceneBackground();
 
    QTimer::singleShot(0, this, SLOT(adjustViewSize()));
}

值得说明的是最后一行代码。singleShot()函数原型如下:

 
 
static void QTimer::singleShot(int msec, QObject * receiver, const char * member);
1
static void QTimer::singleShot(int msec, QObject * receiver, const char * member);

该函数接受三个参数,简单来说,它的作用是,在 msec 毫秒之后,调用 receiver 的 member 槽函数。在我们的代码中,第一个参数传递的是 0,也就是 0ms 之后,调用this->adjustViewSize()。这与直接调用this->adjustViewSize();有什么区别呢?如果你看文档,这一段的解释很隐晦。文档中写到:“It is very convenient to use this function because you do not need to bother with a timerEvent or create a local QTimer object”,也就是说,它的作用是方便使用,无需重写timerEvent()函数或者是创建一个局部的QTimer对象。当我们使用QTimer::signleShot(0, ...)的时候,实际上也是对QTimer的简化,而不是简单地函数调用。QTimer的处理是将其放到事件列表中,等到下一次事件循环开始时去调用这个函数。那么,QTimer::signleShot(0, ...)意思是,在下一次事件循环开始时,立刻调用指定的槽函数。在我们的例子中,我们需要在视图绘制完毕后才去改变大小(视图绘制当然是在paintEvent()事件中),因此我们需要在下一次事件循环中调用adjustViewSize()函数。这就是为什么我们需要用QTimer而不是直接调用adjustViewSize()。如果熟悉 flash,这相当于 flash 里面的callLater()函数。接下来看看initScene()initSceneBackground()的代码:

 
 
void MainWindow::initScene()
{
scene->setSceneRect(-100, -100, 200, 200);
}

void MainWindow::initSceneBackground()
{
QPixmap bg(TILE_SIZE, TILE_SIZE);
QPainter p(&bg);
p.setBrush(QBrush(Qt::gray));
p.drawRect(0, 0, TILE_SIZE, TILE_SIZE);

view->setBackgroundBrush(QBrush(bg));
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void MainWindow::initScene()
{
    scene->setSceneRect(-100, -100, 200, 200);
}
 
void MainWindow::initSceneBackground()
{
    QPixmap bg(TILE_SIZE, TILE_SIZE);
    QPainter p(&bg);
    p.setBrush(QBrush(Qt::gray));
    p.drawRect(0, 0, TILE_SIZE, TILE_SIZE);
 
    view->setBackgroundBrush(QBrush(bg));
}

initScene()函数设置场景的范围,是左上角在 (-100, -100),长和宽都是 200px 的矩形。默认情况下,场景是无限大的,我们代码的作用是设置了一个有限的范围。Graphics View Framework 为每一个元素维护三个不同的坐标系:场景坐标,元素自己的坐标以及其相对于父组件的坐标。除了元素在场景中的位置,其它几乎所有位置都是相对于元素坐标系的。所以,我们选择的矩形 (-100, -100, 200, 200),实际是设置了场景的坐标系。此时,如果一个元素坐标是 (-100, -100),那么它将出现在场景左上角,(100, 100) 的坐标则是在右下角。

initSceneBackground()函数看似很长,实际却很简单。首先我们创建一个边长TILE_SIZEQPixmap,将其使用灰色填充矩形。我们没有设置边框颜色,默认就是黑色。然后将这个QPixmap作为背景画刷,铺满整个视图。

现在我们的程序看起来是这样的:

在后面的章节中,我们将继续我们的游戏之旅。下一章,我们开始创建游戏对象。

Qt 学习之路 2(31):贪吃蛇游戏(1)的更多相关文章

  1. Qt 学习之路 2(34):贪吃蛇游戏(4)

    Qt 学习之路 2(34):贪吃蛇游戏(4) 豆子 2012年12月30日 Qt 学习之路 2 73条评论 这将是我们这个稍大一些的示例程序的最后一部分.在本章中,我们将完成GameControlle ...

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

    Qt 学习之路 2(33):贪吃蛇游戏(3) 豆子 2012年12月29日 Qt 学习之路 2 16条评论 继续前面一章的内容.上次我们讲完了有关蛇的静态部分,也就是绘制部分.现在,我们开始添加游戏控 ...

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

    Qt 学习之路 2(32):贪吃蛇游戏(2) 豆子 2012年12月27日 Qt 学习之路 2 55条评论 下面我们继续上一章的内容.在上一章中,我们已经完成了地图的设计,当然是相当简单的.在我们的游 ...

  4. 《Qt 学习之路 2》目录

    <Qt 学习之路 2>目录 <Qt 学习之路 2>目录  豆子  2012年8月23日  Qt 学习之路 2  177条评论 <Qt 学习之路 2>目录 序 Qt ...

  5. Qt 学习之路 2(71):线程简介

    Qt 学习之路 2(71):线程简介 豆子 2013年11月18日 Qt 学习之路 2 30条评论 前面我们讨论了有关进程以及进程间通讯的相关问题,现在我们开始讨论线程.事实上,现代的程序中,使用线程 ...

  6. Qt 学习之路 2(69):进程

    Qt 学习之路 2(69):进程 豆子 2013年11月9日 Qt 学习之路 2 15条评论 进程是操作系统的基础之一.一个进程可以认为是一个正在执行的程序.我们可以把进程当做计算机运行时的一个基础单 ...

  7. Qt 学习之路 2(67):访问网络(3)

    Qt 学习之路 2(67):访问网络(3) 豆子 2013年11月5日 Qt 学习之路 2 16条评论 上一章我们了解了如何使用我们设计的NetWorker类实现我们所需要的网络操作.本章我们将继续完 ...

  8. Qt 学习之路 2(66):访问网络(2)

    Home / Qt 学习之路 2 / Qt 学习之路 2(66):访问网络(2) Qt 学习之路 2(66):访问网络(2)  豆子  2013年10月31日  Qt 学习之路 2  27条评论 上一 ...

  9. Qt 学习之路 2(63):使用 QJson 处理 JSON

    Home / Qt 学习之路 2 / Qt 学习之路 2(63):使用 QJson 处理 JSON Qt 学习之路 2(63):使用 QJson 处理 JSON  豆子  2013年9月9日  Qt ...

随机推荐

  1. ParallaxEffect

    [ParallaxEffect] ParallaxEffect是一种用简单的2D贴图来模拟3D效果的简易方法.譬如一棵树,摄像机俯视时,当树远离摄像机时,树顶偏远,当树靠近,树顶偏近.苹果官方Adve ...

  2. JSSDK用法

    参照微信官方文档,调试成功之后总结如下: 步骤一:绑定域名 先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”. 备注:登录后可在“开发者中心”查看对应的接口权限. 步骤二: ...

  3. Java EE的十三个规范

    J2EE想必大家都不陌生吧,貌似现在更流行将其称作JavaEE,不管名字怎么变,核心和思想是没有变的.学习J2EE首先要了解它的规范,下面我们一起看看它的十三个规范. 1,JDBC(Java Data ...

  4. Linux查看内存,负载状态

    Linux查看内存,负载状态 查看内存使用情况  www.ahlinux.com cat /proc/meminfo MemTotal:       16332644 kB MemFree:     ...

  5. 如何在VS和CB中配置MySQL环境

    这里,由于我的MySQL安装在D盘 MY SQL\MySQL Server 5.6该路径下,所以后面的路径均以D:\MY SQL\MySQL Server 5.6开头 在VS中配置MySQL环境 包含 ...

  6. C# 随机数 Radom 循环生成同一的数字

    错误:在一个循环结构中,利用下列代码生成随机数,发生生成的随机数是一样的! for (int i = 0; i < myArray.Length; i++) //给数组赋值 { Random m ...

  7. LightOJ 1248 Dice (III) (水题,期望DP)

    题意:给出一个n面的色子,问看到每个面的投掷次数期望是多少. 析:这个题很水啊,就是他解释样例解释的太...我鄙视他,,,,, dp[i] 表示 已经看到 i 面的期望是多少,然后两种选择一种是看到新 ...

  8. 编写高质量代码改善C#程序的157个建议——建议31:在LINQ查询中避免不必要的迭代

    建议31:在LINQ查询中避免不必要的迭代 无论是SQL查询还是LINQ查询,搜索到结果立刻返回总比搜索完所有的结果再将结果返回的效率要高. 示例代码: class MyList : IEnumera ...

  9. eclipse中的项目无法在build/classes目录下生成.class字节码

    转载 原文链接:https://www.cnblogs.com/iceblow/p/6648715.html 1.首先确定project->Build Automatically是否勾选上:  ...

  10. Visual Studio 2013 新增功能:“Browser Link”

    今天新装了 Visual Studio 2013, 使用 VS2013 打开一个现有的 WEB 网站, 在调试网站的时候出现在了脚本错误,一个文件名叫 "browserLink", ...