在这个例子中我们开始研究一个带有记分的真正可玩的游戏。我们给MyWidget
一个新的名字GameBoard
并添加一些槽。
我们把定义放在gamebrd.h
并把实现放在gamebrd.cpp
。
CannonField
现在有了一个游戏结束状态。
在LCDRange
中的布局问题已经修好了。
- lcdrange.h包含LCDRange类定义
- lcdrange.cpp包含LCDRange类实现
- cannon.h包含CannonField类定义
- cannon.cpp包含CannonField类实现
- gamebrd.h包含GameBoard类定义
- gamebrd.cpp包含GameBoard类实现
- main.cpp包含MyWidget和main
一行一行地解说
lcdrange.h
#include <QWidget>
class QSlider;
class QLabel;
class LCDRange : public QWidget
我们继承了QWidget
,并且使用QVBoxLayout
。QVBoxLayout不是一个窗口部件,它管理窗口部件。
lcdrange.cpp
LCDRange::LCDRange(QWidget *parent, Qt::WindowFlags name)
: QWidget(parent, name)
我们使用一种平常的方式继承QWidget
。
另外一个构造函数作了同样的改动。init()没有变化
QVBoxLayout *layout = new QVBoxLayout;
我们使用所有默认值创建一个QVBoxLayout
,管理这个窗口部件的子窗口部件。
cannon.h:
CannonField
现在有一个游戏结束状态和一些新的函数。
bool gameOver() const {return gameEnded;}
如果游戏结束了,这个函数返回TRUE,或者如果游戏还在继续,返回FALSE。
void setGameOver();
void restartGame();
这里是两个新槽:setGameOver()
和restartGame()
。
void canShoot(bool);
这个新的信号表明CannonField
使shoot()
槽生效的状态。我们将在下面使用它用来使`Shoot按钮生效或失效。
bool gameEnded;
这个私有变量包含游戏的状态。true说明游戏结束,false说明游戏还将继续。
cannon.cpp:
gameEnded = false;
这一行已经被加入到构造函数中。最开始的时候,游戏没有结束。
void CannonField::shoot()
{
if (isShooting())
return;
timerCount = 0;
shoot_ang = ang;
shoot_f = f;
autoShootTimer->start(50);
emit canShoot(false);
}
我们添加一个新的isShooting()
函数,所以shoot()
使用它替代直接的测试。同样,shoot
告诉CannonField
现在不可以射击。
void CannonField::setGameOver()
{
if (gameEnded)
return;
if (isShooting())
autoShootTimer->stop();
gameEnded = true;
repaint();
}
这个槽终止游戏。它必须被CannonField
外面的调用,因为这个窗口部件不知道什么时候终止游戏。这是组件编程中一条重要设计原则。我们选择使组件可以尽可能灵活以适应不同的规则(比如,在一个首先射中十次的人胜利的多人游戏版本可能使用不变的CannonField
)。
如果游戏已经被终止,我们立即返回。如果游戏会继续到我们的射击完成,设置游戏结束标志,并且重新绘制整个窗口部件。
void CannonField::restartGame()
{
if (isShooting())
autoShootTimer->stop();
gameEnded = false;
repaint();
emit canShoot(true);
}
这个槽开始一个新游戏。如果炮弹还在空中,我们停止射击。然后我们重置gameEnded
变量并重新绘制窗口部件。
就像hit()
或miss()
一样,moveShot()
同时也发射新的canShoot(true)
信号。
CannonField::paintEvent()
的修改:
void CannonField::paintEvent(QPaintEvent *e)
{
QRect updateR = e->rect();
QPainter p(this);
if (gameEnded)
{
p.setPen(Qt::black);
p.setFont(QFont("Courier", 48, QFont::Bold ));
p.drawText(rect(),Qt::AlignHCenter, "Game Over");
}
绘画事件已经通过如果游戏结束,比如gameEnded
是true
,就显示文本Game Over
而被增强了。我们在这里不怕麻烦来检查更新矩形,是因为在游戏结束的时候速度不是关键性的。
为了画文本,我们先设置了黑色的画笔,当画文本的时候,画笔颜色会被用到。接下来我们选择Courier字体中的48号加粗字体。最后我们在窗口部件的矩形中央绘制文本。不幸的是,在一些系统中(特别是使用Unicode的X服务器)它会用一小段时间来载入如此大的字体。因为Qt缓存字体,我们只有第一次使用这个字体的时候才会注意到这一点。
if (updateR.intersects(cannonRect()))
paintCannon(&p);
if (autoShootTimer->isActive() && updateR.intersects(shotRect()))
paintShot(&p);
if (updateR.intersects(targetRect()))
paintTarget(&p);
}
我们只有在射击的时候画炮弹,在玩游戏的时候画目标。
gamebrd.h
这个文件是新的。它包含最后被用来作为MyWidget
的GameBoard
类的定义。
#ifndef GAMEBOARD_H
#define GAMEBOARD_H
#include <QWidget>
#include "cannon.h"
class LCDRange;
class QLCDNumber;
class CannonField;
class QPushButton;
class GameBoard : public QWidget
{
Q_OBJECT
public:
explicit GameBoard(QWidget *parent = 0, Qt::WindowFlags name = Qt::WindowFlags());
protected slots:
void fire();
void hit();
void missed();
void newGame();
private:
QLCDNumber *hits;
QLCDNumber *shotsLeft;
CannonField *cannonField;
};
#endif // GAMEBOARD_H
我们现在已经添加了四个槽。这些槽都是被保护的,只在内部使用。我们也已经加入了两个QLCDNumbers
(hits和shotsLeft)用来显示游戏的状态。
gamebrd.cpp:
这个文件是新的。它包含最后被用来作为MyWidget
的GameBoard
类的实现,
我们已经在GameBoard
的构造函数中做了一些修改。
cannonField = new CannonField;
cannonField
现在是一个成员变量,所以我们在使用它的时候要小心地改变它的构造函数。
connect(cannonField, SIGNAL(hit()), this, SLOT(hit()));
connect(cannonField, SIGNAL(missed()), this, SLOT(missed()));
这次当炮弹射中或者射失目标的时候,我们想做些事情。所以我们把CannonField
的hit()
和missed()
信号连接到这个类的两个被保护的同名槽。
connect(shoot, SIGNAL(clicked()), SLOT(fire()));
以前我们直接把Shoot按钮的clicked()信号连接到CannonField的shoot()槽。这次我们想跟踪射击的次数,所以我们把它改为连接到这个类里面一个被保护的槽。
注意当你用独立的组件工作的时候,改变程序的行为是多么的容易。
connect(cannonField, SIGNAL(canShoot(bool)), shoot, SLOT(setEnabled(bool)));
我们也使用cannonField
的canShoot()
信号来适当地使Shoot
按钮生效和失效。
QPushButton *restart = new QPushButton("&New Game");
restart->setFont(QFont("Times", 18, QFont::Bold));
connect(restart, SIGNAL(clicked()), this, SLOT(newGame()));
我们创建、设置并且连接这个New Game
按钮就像我们对其它按钮所做的一样。点击这个按钮就会激活这个窗口部件的newGame()
槽。
hits = new QLCDNumber(2);
shotsLeft = new QLCDNumber(2);
QLabel *hitsL = new QLabel("HITS");
QLabel *shotsLeftL = new QLabel("SHOTS LEFT");
我们创建了四个新的窗口部件。注意我们不怕麻烦的把QLabel
窗口部件的指针保留到GameBoard
类中是因为我们不想再对它们做什么了。当GameBoard
窗口部件被销毁的时候,Qt将会删除它们,并且布局类会适当地重新定义它们的大小。
QHBoxLayout *topBox = new QHBoxLayout;
grid->addLayout(topBox, 0, 1);
topBox->addWidget(shoot);
topBox->addWidget(hits);
topBox->addWidget(hitsL);
topBox->addWidget(shotsLeft);
topBox->addWidget(shotsLeftL);
topBox->addStretch(1);
topBox->addWidget(restart);
右上单元格的窗口部件的数量正在变大。从前它是空的,现在它是完全充足的,我们把它们放到布局中来更好的看到它们。
注意我们让所有的窗口部件获得它们更喜欢的大小,改为在New Game
按钮的左边加入了一个可以自由伸展的东西。
newGame();
}
我们已经做完了所有关于GameBoard
的构造,所以我们使用newGame()
来开始。(newGame()
是一个槽,但是就像我们所说的,槽也可以像普通的函数一样使用。)
void GameBoard::fire()
{
if (cannonField->gameOver() || cannonField->isShooting())
return;
shotsLeft->display(shotsLeft->intValue() - 1);
cannonField->shoot();
}
这个函数进行射击。如果游戏结束了或者还有炮弹在空中,立即返回。我们减少炮弹的数量并告诉加农炮进行射击。
void GameBoard::hit()
{
hits->display( hits->intValue() + 1 );
if ( shotsLeft->intValue() == 0 )
cannonField->setGameOver();
else
cannonField->newTarget();
}
当炮弹击中目标的时候这个槽被激活。我们增加射中的数量。如果没有炮弹了,游戏就结束了。否则,我们会让CannonField
生成新的目标。
void GameBoard::missed()
{
if (shotsLeft->intValue() == 0)
cannonField->setGameOver();
}
当炮弹射失目标的时候这个槽被激活,如果没有炮弹了,游戏就结束了。
void GameBoard::newGame()
{
shotsLeft->display( 15 );
hits->display( 0 );
cannonField->restartGame();
cannonField->newTarget();
}
当用户点击Restart
按钮的时候这个槽被激活。它也会被构造函数调用。首先它把炮弹的数量设置为15。注意这里是我们在程序中唯一设置炮弹数量的地方。把它改变为你所想要的游戏规则。接下来我们重置射中的数量,重新开始游戏,并且生成一个新的目标。
main.cpp:
这个文件仅仅被删掉了一部分。MyWidget
没了,并且唯一剩下的是main()
函数,除了名称的改变其它都没有改变。
行为
射中的和剩余炮弹的数量被显示并且程序继续跟踪它们。游戏可以结束了,并且还有一个按钮可以开始一个新游戏。
练习
添加一个随机的风的因素并把它显示给用户看。
当炮弹击中目标的时候做一些飞溅的效果。
实现多个目标。
现在你可以进行第十四章了。
lcdrange.h
#ifndef LCDRANGE_H
#define LCDRANGE_H
#include <QWidget>
class QSlider;
class QLabel;
class LCDRange : public QWidget
{
Q_OBJECT
public:
LCDRange(QWidget *parent = 0, Qt::WindowFlags name = Qt::WindowFlags());
LCDRange(const QString &s, QWidget *parent = 0, Qt::WindowFlags name = Qt::WindowFlags());
int value() const;
const QString text() const;
public slots:
void setValue(int);
void setRange(int minVal, int maxVal);
void setText(const QString &s);
signals:
void valueChanged(int);
private:
void init();
QSlider *slider;
QLabel *label;
};
#endif // LCDRANGE_H
lcdrange.cpp
#include "lcdrange.h"
#include <QVBoxLayout>
#include <QLCDNumber>
#include <QSlider>
#include <QLabel>
LCDRange::LCDRange(QWidget *parent, Qt::WindowFlags name)
: QWidget(parent, name)
{
init();
}
LCDRange::LCDRange(const QString &s, QWidget *parent, Qt::WindowFlags name)
: QWidget(parent, name)
{
init();
setText(s);
}
void LCDRange::init()
{
QLCDNumber *lcd = new QLCDNumber(2);
slider = new QSlider(Qt::Horizontal);
slider->setRange(0, 99);
slider->setValue(0);
label = new QLabel;
label->setAlignment(Qt::AlignHCenter);
connect(slider, SIGNAL(valueChanged(int)), lcd, SLOT(display(int)));
connect(slider, SIGNAL(valueChanged(int)), SIGNAL(valueChanged(int)));
setFocusProxy(slider); //设置这个窗口部件的焦点为slider
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(lcd);
layout->addWidget(slider);
layout->addWidget(label);
setLayout(layout);
}
int LCDRange::value() const
{
return slider->value();
}
const QString LCDRange::text() const
{
return label->text();
}
void LCDRange::setValue(int value)
{
slider->setValue(value);
}
void LCDRange::setRange(int minVal, int maxVal)
{
if (minVal < 0 || maxVal > 99 || minVal > maxVal)
{
qWarning( "LCDRange::setRange(%d,%d)\n"
"\tRange must be 0~99\n"
"\tand minVal must not be greater than maxVal",
minVal, maxVal);
return;
}
slider->setRange(minVal, maxVal);
}
void LCDRange::setText(const QString &s)
{
label->setText(s);
}
cannon.h
#ifndef CANNON_H
#define CANNON_H
class QTimer;
#include <QWidget>
class CannonField : public QWidget
{
Q_OBJECT
public:
CannonField(QWidget *parent = 0, Qt::WindowFlags name = Qt::WindowFlags());
int angle() const {return ang;}
int force() const {return f;}
bool gameOver() const {return gameEnded;}
bool isShooting() const;
public slots:
void setAngle(int degrees);
void setForce(int newton);
void shoot();
void newTarget();
void setGameOver();
void restartGame();
private slots:
void moveShot(); // 更新炮弹的位置
signals:
void hit();
void missed();
void angleChanged(int);
void forceChanged(int);
void canShoot(bool);
protected:
void paintEvent(QPaintEvent *); // 窗口刷新/重绘
private:
void paintShot(QPainter *);
void paintTarget(QPainter *);
void paintCannon(QPainter *);
QRect cannonRect() const;
QRect shotRect() const;
QRect targetRect() const;
int ang;
int f;
int timerCount;
QTimer *autoShootTimer;
float shoot_ang;
float shoot_f;
QPoint target;
bool gameEnded;
};
#endif // CANNON_H
cannon.cpp
#include "cannon.h"
#include <QPaintEvent>
#include <QDateTime>
#include <QPainter>
#include <QPixmap>
#include <QTimer>
#include <stdlib.h>
#include <math.h>
CannonField::CannonField(QWidget *parent, Qt::WindowFlags name)
: QWidget(parent, name)
{
ang = 45;
f = 0;
timerCount = 0;
autoShootTimer = new QTimer();
connect(autoShootTimer, SIGNAL(timeout()), this, SLOT(moveShot()));
shoot_ang = 0;
shoot_f = 0;
target = QPoint(0, 0);
gameEnded = false;
setAutoFillBackground(true);
setPalette(QPalette(QColor(250, 250, 200)));
newTarget();
}
void CannonField::setAngle(int degrees)
{
if (degrees < 5)
degrees = 5;
if (degrees > 70)
degrees = 70;
if (ang == degrees)
return;
ang = degrees;
repaint(cannonRect());
emit angleChanged(ang);
}
void CannonField::setForce(int newton)
{
if (newton < 0)
newton = 0;
if (f == newton)
return;
f = newton;
emit forceChanged(f);
}
void CannonField::shoot()
{
if (isShooting())
return;
timerCount = 0;
shoot_ang = ang;
shoot_f = f;
autoShootTimer->start(50);
emit canShoot(false);
}
void CannonField::newTarget()
{
static bool first_time = true;
if (first_time)
{
first_time = false;
QTime midnight(0, 0, 0);
srand(midnight.secsTo(QTime::currentTime()));
}
QRegion r(targetRect());
target = QPoint(200 + rand() % 190, 10 + rand() % 255);
repaint(r.united(targetRect()));
}
void CannonField::setGameOver()
{
if (gameEnded)
return;
if (isShooting())
autoShootTimer->stop();
gameEnded = true;
repaint();
}
void CannonField::restartGame()
{
if (isShooting())
autoShootTimer->stop();
gameEnded = false;
repaint();
emit canShoot(true);
}
void CannonField::moveShot()
{
QRegion r(shotRect());
timerCount++;
QRect shotR = shotRect();
if (shotR.intersects(targetRect()))
{
autoShootTimer->stop();
emit hit();
}
else if (shotR.x() > width() || shotR.y() > height())
{
autoShootTimer->stop();
emit missed();
emit canShoot(true);
}
else
{
r = r.united(QRegion(shotR));
}
repaint(r);
}
void CannonField::paintEvent(QPaintEvent *e)
{
QRect updateR = e->rect();
QPainter p(this);
if (gameEnded)
{
p.setPen(Qt::black);
p.setFont(QFont("Courier", 48, QFont::Bold ));
p.drawText(rect(),Qt::AlignHCenter, "Game Over");
}
if (updateR.intersects(cannonRect()))
paintCannon(&p);
if (autoShootTimer->isActive() && updateR.intersects(shotRect()))
paintShot(&p);
if (updateR.intersects(targetRect()))
paintTarget(&p);
}
void CannonField::paintShot(QPainter *p)
{
p->setBrush(Qt::black);
p->setPen(Qt::NoPen);
p->drawEllipse(shotRect()); // 绘制填充圆
}
void CannonField::paintTarget(QPainter *p)
{
p->setBrush(Qt::red);
p->setPen(Qt::NoPen);
p->drawRect(targetRect());
}
const QRect barrelRect(33, -4, 15, 8);
void CannonField::paintCannon(QPainter *p)
{
QRect cr = cannonRect();
QPixmap pix(cr.size());
pix.fill(Qt::transparent); //使用透明像素图
QPainter tmp(&pix);
tmp.setBrush(Qt::blue);
tmp.setPen(Qt::NoPen);
tmp.translate(0, pix.height() - 1);
tmp.drawPie(QRect(-35,-35, 70, 70), 0, 90*16);
tmp.rotate(-ang);
tmp.drawRect(barrelRect);
tmp.end();
p->drawPixmap(cr.topLeft(), pix);
}
QRect CannonField::cannonRect() const
{
QRect r(0, 0, 50, 50);
r.moveBottomLeft(rect().bottomLeft());
return r;
}
QRect CannonField::shotRect() const
{
const double gravity = 4;
double time = timerCount / 4.0;
double velocity = shoot_f;
double radians = shoot_ang * 3.14159265 / 180;
double velx = velocity * cos(radians);
double vely = velocity * sin(radians);
double x0 = (barrelRect.right() + 5) * cos(radians);
double y0 = (barrelRect.right() + 5) * sin(radians);
double x = x0 + velx * time;
double y = y0 + vely * time - 0.5 * gravity * time * time;
QRect r = QRect(0, 0, 6, 6);
r.moveCenter(QPoint(qRound(x), height() - 1 - qRound(y)));
return r;
}
QRect CannonField::targetRect() const
{
QRect r(0, 0, 20, 10);
r.moveCenter(QPoint(target.x(),height() - 1 - target.y()));
return r;
}
bool CannonField::isShooting() const
{
return autoShootTimer->isActive();
}
gamebrd.h
#ifndef GAMEBOARD_H
#define GAMEBOARD_H
#include <QWidget>
#include "cannon.h"
class LCDRange;
class QLCDNumber;
class CannonField;
class QPushButton;
class GameBoard : public QWidget
{
Q_OBJECT
public:
explicit GameBoard(QWidget *parent = 0, Qt::WindowFlags name = Qt::WindowFlags());
protected slots:
void fire();
void hit();
void missed();
void newGame();
private:
QLCDNumber *hits;
QLCDNumber *shotsLeft;
CannonField *cannonField;
};
#endif // GAMEBOARD_H
gamebrd.cpp
#include "gameboard.h"
#include <QApplication>
#include <Qpushbutton>
#include <QVBoxLayout>
#include <QGridLayout>
#include <QLCDNumber>
#include <QLabel>
#include <Qfont>
#include "lcdrange.h"
#include "cannon.h"
GameBoard::GameBoard(QWidget *parent, Qt::WindowFlags name)
: QWidget(parent, name)
{
QPushButton *quit = new QPushButton("&Quit");
quit->setFont(QFont("Times", 18, QFont::Bold));
connect(quit, SIGNAL(clicked()), qApp, SLOT(quit()));
LCDRange *angle = new LCDRange("ANGLE");
angle->setRange(5, 70);
LCDRange *force = new LCDRange("FORCE");
force->setRange(10, 50);
cannonField = new CannonField;
connect(angle, SIGNAL(valueChanged(int)), cannonField, SLOT(setAngle(int)));
connect(cannonField, SIGNAL(angleChanged(int)), angle, SLOT(setValue(int)));
connect(force, SIGNAL(valueChanged(int)), cannonField, SLOT(setForce(int)));
connect(cannonField, SIGNAL(forceChanged(int)), force, SLOT(setValue(int)));
connect(cannonField, SIGNAL(hit()), this, SLOT(hit()));
connect(cannonField, SIGNAL(missed()), this, SLOT(missed()));
QPushButton *shoot = new QPushButton("&Shoot");
shoot->setFont(QFont("Times", 18, QFont::Bold));
connect(shoot, SIGNAL(clicked()), SLOT(fire()));
connect(cannonField, SIGNAL(canShoot(bool)), shoot, SLOT(setEnabled(bool)));
QPushButton *restart = new QPushButton("&New Game");
restart->setFont(QFont("Times", 18, QFont::Bold));
connect(restart, SIGNAL(clicked()), this, SLOT(newGame()));
hits = new QLCDNumber(2);
shotsLeft = new QLCDNumber(2);
QLabel *hitsL = new QLabel("HITS");
QLabel *shotsLeftL = new QLabel("SHOTS LEFT");
QGridLayout *grid = new QGridLayout;
grid->addWidget(quit, 0, 0);
grid->addWidget(cannonField, 1, 1);
grid->setColumnStretch(1, 10);
QVBoxLayout *leftBox = new QVBoxLayout;
grid->addLayout(leftBox, 1, 0);
leftBox->addWidget(angle);
leftBox->addWidget(force);
QHBoxLayout *topBox = new QHBoxLayout;
grid->addLayout(topBox, 0, 1);
topBox->addWidget(shoot);
topBox->addWidget(hits);
topBox->addWidget(hitsL);
topBox->addWidget(shotsLeft);
topBox->addWidget(shotsLeftL);
topBox->addStretch(1);
topBox->addWidget(restart);
QVBoxLayout *layout = new QVBoxLayout;
layout->addLayout(grid);
setLayout(layout);
angle->setValue(60);
force->setValue(25);
angle->setFocus();
newGame();
}
void GameBoard::fire()
{
if (cannonField->gameOver() || cannonField->isShooting())
return;
shotsLeft->display(shotsLeft->intValue() - 1);
cannonField->shoot();
}
void GameBoard::hit()
{
hits->display(hits->intValue() + 1);
if (shotsLeft->intValue() == 0)
cannonField->setGameOver();
else
cannonField->newTarget();
}
void GameBoard::missed()
{
if (shotsLeft->intValue() == 0)
cannonField->setGameOver();
}
void GameBoard::newGame()
{
shotsLeft->display(10);
hits->display(0);
cannonField->restartGame();
cannonField->newTarget();
}
main.cpp
#include <QApplication>
#include "gameboard.h"
int main(int argc, char **argv)
{
QApplication a(argc, argv);
GameBoard gb;
gb.setGeometry(100, 100, 500, 355);
gb.show();
return a.exec();
}