player.h
#ifndef PLAYER_H
#define PLAYER_H
#include <QGraphicsPixmapItem>
//选择QGraphicsPixmapItem作为基类
class Player : public QGraphicsPixmapItem
{
public:
explicit Player(QGraphicsItem *parent = 0);
int direction() const;
void setDirection(int direction);
private:
//哪个方向走——向左或向右
//如果 m_direction 为 1,则 大象 向右移动,如果值为 -1,则向左移动
int m_direction;
};
#endif // PLAYER_H
player.cpp
#include "player.h"
#include <QPen>
#include <QDebug>
Player::Player(QGraphicsItem *parent)
: QGraphicsPixmapItem(parent)
, m_direction(0) //我们将 m_direction 设置为 0,这意味着 大象 根本没有移动
{
QPixmap pixmap(":/elephant");
//在构造函数的主体中,我们通过调用 setPixmap() 设置项目的图像
//Benjamin的图像存储在 Qt Resource 系统中;
//因此,我们通过 QPixmap(":/elephant") 访问它
setPixmap(pixmap);
//最后,我们使用 setOffset() 函数更改像素图在项目坐标系中的定位方式
//默认情况下,原点对应于像素图的左上角,但我们更喜欢将其置于像素图的中心,以便更容易地应用转换。
setOffset(-pixmap.width() / 2, -pixmap.height() / 2);
}
int Player::direction() const
{
//direction() 函数是 m_direction 返回其值的标准 getter 函数
return m_direction;
}
void Player::setDirection(int direction)
{
m_direction = direction;
//setDirection() setter 函数另外检查 Benjamin 移动的方向
if (m_direction != 0) {
QTransform transform;
if (m_direction < 0) {
//如果他向左移动,我们需要翻转他的图像
//以便 Benjamin 向左看,即他移动的方向
transform.scale(-1, 1);
}
//如果他向右移动,我们通过分配一个空的 QTransform 对象来恢复正常状态,该对象是一个单位矩阵。
setTransform(transform);
}
}
backgrounditem.h
背景项
#ifndef BACKGROUNDITEM_H
#define BACKGROUNDITEM_H
#include <QGraphicsPixmapItem>
class BackgroundItem : public QGraphicsPixmapItem
{
public:
explicit BackgroundItem(const QPixmap &pixmap, QGraphicsItem *parent = 0);
public:
virtual QPainterPath shape() const;
};
#endif // BACKGROUNDITEM_H
backgrounditem.cpp
#include "backgrounditem.h"
BackgroundItem::BackgroundItem(const QPixmap &pixmap, QGraphicsItem * parent)
: QGraphicsPixmapItem(pixmap, parent)
{
}
QPainterPath BackgroundItem::shape() const
{
return QPainterPath();
}
coin.h
#ifndef COIN_H
#define COIN_H
#include <QObject>
#include <QGraphicsEllipseItem>
class Coin : public QObject, public QGraphicsEllipseItem
{
Q_OBJECT
Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity)
Q_PROPERTY(QRectF rect READ rect WRITE setRect)
public:
explicit Coin(QGraphicsItem *parent = 0);
enum { Type = UserType + 1 };
int type() const;
void explode();
private:
bool m_explosion;
};
#endif // COIN_H
coin.cpp
#include "coin.h"
#include <QParallelAnimationGroup>
#include <QPen>
#include <QPropertyAnimation>
#include <QSequentialAnimationGroup>
Coin::Coin(QGraphicsItem *parent) :
QGraphicsEllipseItem(parent)
, m_explosion(false)
{
setPen(QPen(QColor(241, 190, 56), 2));
setBrush(QColor(252, 253, 151));
setRect(-12, -12, 24, 24);
}
int Coin::type() const
{
return Type;
}
void Coin::explode()
{
if (m_explosion) {
return;
}
m_explosion = true;
QParallelAnimationGroup *group = new QParallelAnimationGroup(this);
//QSequentialAnimationGroup *group = new QSequentialAnimationGroup(this);
QPropertyAnimation *scaleAnimation = new QPropertyAnimation(this, "rect");
scaleAnimation->setDuration(700);
QRectF r = rect();
scaleAnimation->setStartValue(r);
scaleAnimation->setEndValue(QRectF(r.topLeft() - r.bottomRight(), r.size() * 2));
scaleAnimation->setEasingCurve(QEasingCurve::OutQuart);
group->addAnimation(scaleAnimation);
QPropertyAnimation *fadeAnimation = new QPropertyAnimation(this, "opacity");
fadeAnimation->setDuration(700);
fadeAnimation->setStartValue(1);
fadeAnimation->setEndValue(0);
fadeAnimation->setEasingCurve(QEasingCurve::OutQuart);
group->addAnimation(fadeAnimation);
connect(group, &QParallelAnimationGroup::finished,
this, &Coin::deleteLater);
group->start();
}
myscene.h
#ifndef MYSCENE_H
#define MYSCENE_H
#include <QGraphicsScene>
#include <QTimer>
class QGraphicsPixmapItem;
class QPropertyAnimation;
class BackgroundItem;
class Player;
//由于我们将不得不在场景上做一些工作,我们将 QGraphicsScene 子类化,并将新类命名为 MyScene在那里,我们实现了游戏逻辑的一部分。
//这很方便,因为 QGraphicsScene 继承了 QObject,因此我们可以使用 Qt 的信号和槽机制
//该场景创建了我们的大象将在其中行走和跳跃的环境。
//总的来说,我们有一个固定大小的视图,包含一个场景,它和视图一样大。我们不考虑视图的大小变化,因为它们会使示例过于复杂。
class MyScene : public QGraphicsScene
{
Q_OBJECT
Q_PROPERTY(qreal jumpFactor
READ jumpFactor
WRITE setJumpFactor
NOTIFY jumpFactorChanged)
public:
explicit MyScene(QObject *parent = 0);
qreal jumpFactor() const;
void setJumpFactor(const qreal &jumpFactor);
protected:
void keyPressEvent(QKeyEvent *event);
void keyReleaseEvent(QKeyEvent *event);
private slots:
void movePlayer();
void checkTimer();
void checkColliding();
void axisLeftXChanged(double value);
void axisLeftYChanged(double value);
signals:
void jumpFactorChanged(qreal);
//比赛场地内的所有动画都是通过移动项目而不是场景来完成的。所以我们必须区分视图的宽度,或者说场景的宽度,以及大象可以在其中移动的虚拟“世界”的宽度
//为了正确处理移动,我们需要在 MyScene 类中创建一些私有字段
private:
void initPlayField();
void jump();
int m_velocity;
int m_worldShift;
qreal m_groundLevel;
qreal m_minX;
qreal m_maxX;
//让我们的大象可以移动。 为了实现这一点,我们向 MyScene 添加了一个 QTimer m_timer 私有成员
//QTimer 是一个可以以给定的时间间隔周期性地发出 timeout() 信号的类
QTimer m_timer;
QPropertyAnimation *m_jumpAnimation;
qreal m_jumpFactor;
int m_jumpHeight;
int m_fieldWidth;
qreal m_currentX;
//我们引入了 Player * m_player 字段,它将包含一个指向玩家对象的指针和 int m_horizontalInput 字段
Player* m_player;
//场景将为背景的每个部分创建一个图形项,并将指向它们的指针存储在 m_sky、m_grass 和 m_trees 私有字段中
//现在的问题是如何以不同的速度移动它们
//解决方案非常简单——最慢的天空是最小的图像。 最快的背景,地面和草地
BackgroundItem *m_sky;
BackgroundItem *m_trees;
BackgroundItem *m_grass;
QGraphicsRectItem *m_coins;
int m_horizontalInput;
void addHorizontalInput(int input);
void applyParallax(qreal ratio, QGraphicsItem *item);
};
#endif // MYSCENE_H
myscene.cpp
#include "myscene.h"
#include <QKeyEvent>
#include <QPropertyAnimation>
#include <QDebug>
#include <QGraphicsView>
#include <QPen>
#include <QGraphicsPixmapItem>
#include "player.h"
#include "coin.h"
#include "backgrounditem.h"
#include <QGamepadManager>
#include <QGamepad>
#include <math.h>
MyScene::MyScene(QObject *parent) :
QGraphicsScene(parent)
, m_velocity(4)
, m_worldShift(0)
, m_groundLevel(300)
, m_minX(0)
, m_maxX(0)
, m_jumpAnimation(new QPropertyAnimation(this))
, m_jumpHeight(180)
, m_fieldWidth(1500)
, m_player(0)
, m_sky(0)
, m_trees(0)
, m_grass(0)
, m_coins(0)
, m_horizontalInput(0)
{
initPlayField();
//在 MyScene 构造函数中,我们使用以下代码设置计时器
//首先,我们定义定时器每 30 毫秒发出一次超时信号
//然后,我们将该信号连接到名为 movePlayer() 的场景槽
//但我们还没有启动计时器。 当玩家按下一个键移动时,计时器将启动。
m_timer.setInterval(30);
connect(&m_timer, &QTimer::timeout, this, &MyScene::movePlayer);
//对于这里创建的 QPropertyAnimation 的实例,我们将 item 定义为 parent
//因此,当场景删除项目时动画会被删除,我们不必担心释放已用的内存
//然后,我们定义动画的目标——我们的 MyScene 类
m_jumpAnimation->setTargetObject(this);
//以及应该被动画化的属性jumpFactor
m_jumpAnimation->setPropertyName("jumpFactor");
//然后,我们定义该属性的开始和结束值
m_jumpAnimation->setStartValue(0);
//除此之外,我们还通过设置 setKeyValueAt() 定义了一个介于两者之间的值
m_jumpAnimation->setKeyValueAt(0.5, 1);
m_jumpAnimation->setEndValue(0);
//您的 jumpFactor 元素将在 800 毫秒内从 0 变为 1 并返回到 0
//这是由 setDuration() 定义的
m_jumpAnimation->setDuration(800);
//我们定义开始值和结束值之间的插值应该如何完成并调用 setEasingCurve(),使用 QEasingCurve::OutInQuad 作为参数。
//Qt 为线性、二次、三次、四次、五次、正弦、指数、圆形、弹性、后向缓动和反弹函数定义了多达 41 种不同的缓动曲线。
m_jumpAnimation->setEasingCurve(QEasingCurve::OutInQuad);
//在我们的例子中,QEasingCurve::OutInQuad 确保 Benjamin 的跳跃速度看起来像真正的跳跃:开始时快,顶部慢,最后又快。 我们用跳转函数开始这个动画:
QList<int> gamepadIds = QGamepadManager::instance()->connectedGamepads();
if (!gamepadIds.isEmpty()) {
QGamepad *gamepad = new QGamepad(gamepadIds[0], this);
connect(gamepad, &QGamepad::axisLeftXChanged,
this, &MyScene::axisLeftXChanged);
connect(gamepad, &QGamepad::axisLeftYChanged,
this, &MyScene::axisLeftYChanged);
}
}
void MyScene::keyPressEvent(QKeyEvent *event)
{
//在按键事件处理程序中,我们首先检查按键事件是否由于自动重复而被触发
//如果是这种情况,我们退出函数,因为我们只想对第一个真正的按键事件做出反应
//此外,我们不调用该事件处理程序的基类实现,因为场景中的任何项目都不需要获取按键事件
//如果您确实有可以并且应该接收事件的项目,请不要忘记在现场重新实现事件处理程序时转发它们
if (event->isAutoRepeat()) {
return;
}
//一旦我们知道事件不是通过自动重复传递的,我们就会对不同的按键做出反应。
//我们没有直接调用 Player *m_player 字段的 setDirection() 方法,而是使用 m_horizontalInput 类字段来累加输入值。
//无论何时更改,我们都会在将其传递给 setDirection() 之前确保该值的正确性。
switch (event->key()) {
case Qt::Key_Right:
addHorizontalInput(1);
break;
case Qt::Key_Left:
addHorizontalInput(-1);
break;
//玩家按下键盘上的 Space 键时激活这个跳跃动作
case Qt::Key_Space:
jump();
break;
default:
break;
}
}
void MyScene::keyReleaseEvent(QKeyEvent *event)
{
if (event->isAutoRepeat()) {
return;
}
switch (event->key()) {
case Qt::Key_Right:
addHorizontalInput(-1);
break;
case Qt::Key_Left:
addHorizontalInput(1);
break;
// case Qt::Key_Space:
// return;
// break;
default:
break;
}
}
void MyScene::movePlayer()
{
//首先,我们将玩家当前的方向缓存在一个局部变量中
//以避免多次调用 direction()。
const int direction = m_player->direction();
//然后,我们检查玩家是否在移动
//如果不是,我们退出函数,因为没有任何动画
if (0 == direction) {
return;
}
//我们计算玩家项目应该获得的偏移并将其存储在 dx 中
//玩家每 30 毫秒应移动的距离由 int m_velocity 成员变量定义,以像素表示
//默认值 4 像素
//我们得到玩家向右或向左移动 4 个像素
const int dx = direction * m_velocity;
//基于这个偏移,我们计算玩家新的 x 位置
//我们检查新位置是否在 m_minX 和 m_maxX 的范围内
qreal newX = qBound(m_minX, m_currentX + dx, m_maxX);
if (newX == m_currentX) {
return;
}
//如果新位置不等于存储在 m_currentX 中的实际位置,我们继续将新位置分配为当前位置。
m_currentX = newX;
//假设当大象中心到窗口边框的距离小于 150 像素时,我们将尝试移动视图
const int shiftBorder = 150;
int rightShiftBorder = width() - shiftBorder;
//int m_worldShift 类字段显示我们已经将世界向右移动了多少
//我们计算大象在视图中的实际坐标并将其保存到visiblePlayerPos 变量中
const int visiblePlayerPos = m_currentX - m_worldShift;
//如果visiblePlayerPos 超出了允许区域的右边界
//则newWorldShiftRight 将为正,我们需要将世界向右移动newWorldShiftRight
const int newWorldShiftRight = visiblePlayerPos - rightShiftBorder;
if (newWorldShiftRight > 0) {
m_worldShift += newWorldShiftRight;
}
//类似地,当我们需要将其向左移动时,newWorldShiftLeft 将为正数
//并且它将包含所需的移动量
const int newWorldShiftLeft = shiftBorder - visiblePlayerPos;
if (newWorldShiftLeft > 0) {
m_worldShift -= newWorldShiftLeft;
}
const int maxWorldShift = m_fieldWidth - qRound(width());
m_worldShift = qBound(0, m_worldShift, maxWorldShift);
//最后,我们使用类似于 setPos() 的 setX() 辅助方法更新 m_player 的位置,但保持 y 坐标不变。
m_player->setX(m_currentX - m_worldShift);
//我们在这里做什么?一开始,天空的左边界与视图的左边界相同,都在 (0, 0) 点
const qreal ratio = qreal(m_worldShift) / maxWorldShift;
applyParallax(ratio, m_sky);
applyParallax(ratio, m_grass);
applyParallax(ratio, m_trees);
applyParallax(ratio, m_coins);
//们调用场景的 QGraphicsScene::collidingItems() 函数
checkColliding();
}
void MyScene::applyParallax(qreal ratio, QGraphicsItem* item) {
item->setX(-ratio * (item->boundingRect().width() - width()));
}
void MyScene::initPlayField()
{
setSceneRect(0, 0, 500, 340);
m_sky = new BackgroundItem(QPixmap(":/sky"));
addItem(m_sky);
BackgroundItem *ground = new BackgroundItem(QPixmap(":/ground"));
addItem(ground);
ground->setPos(0, m_groundLevel);
m_trees = new BackgroundItem(QPixmap(":/trees"));
m_trees->setPos(0, m_groundLevel - m_trees->boundingRect().height());
addItem(m_trees);
m_grass = new BackgroundItem(QPixmap(":/grass"));
m_grass->setPos(0,m_groundLevel - m_grass->boundingRect().height());
addItem(m_grass);
m_player = new Player();
m_minX = m_player->boundingRect().width() * 0.5;
m_maxX = m_fieldWidth - m_player->boundingRect().width() * 0.5;
m_player->setPos(m_minX, m_groundLevel - m_player->boundingRect().height() / 2);
m_currentX = m_minX;
addItem(m_player);
// Add some coins
m_coins = new QGraphicsRectItem(0, 0, m_fieldWidth, m_jumpHeight);
m_coins->setPen(Qt::NoPen);
m_coins->setPos(0, m_groundLevel - m_jumpHeight);
const int xRange = (m_maxX - m_minX) * 0.94;
for (int i = 0; i < 25; ++i) {
Coin *c = new Coin(m_coins);
c->setPos(m_minX + qrand() % xRange, qrand() % m_jumpHeight);
}
addItem(m_coins);
}
void MyScene::jump()
{
//我们只在动画没有运行时调用 start() 来启动动画
//因此,我们检查动画的状态以查看它是否已停止
if (QAbstractAnimation::Stopped == m_jumpAnimation->state()) {
m_jumpAnimation->start();
}
}
void MyScene::addHorizontalInput(int input)
{
//而是使用 m_horizontalInput 类字段来累加输入值
m_horizontalInput += input;
//论何时更改,我们都会在将其传递给 setDirection() 之前确保该值的正确性
//为此,我们使用 qBound(),它返回一个由第一个和最后一个参数绑定的值
//中间的参数是我们想要绑定的实际值,所以在我们的例子中可能的值被限制为 -1、0 和 1。
m_player->setDirection(qBound(-1, m_horizontalInput, 1));
checkTimer();
}
qreal MyScene::jumpFactor() const
{
return m_jumpFactor;
}
//当我们的 QPropertyAnimation 运行时,它会调用我们的 setJumpFactor() 函数来更新属性的值
void MyScene::setJumpFactor(const qreal &jumpFactor)
{
if (m_jumpFactor == jumpFactor) {
return;
}
m_jumpFactor = jumpFactor;
emit jumpFactorChanged(m_jumpFactor);
//在该函数中,我们计算玩家项目的 y 坐标以 m_groundLevel 定义的地面水平
//这是通过从地平面的值中减去Item高度的一半来完成的
//因为项目的原点在其中心
qreal groundY = (m_groundLevel - m_player->boundingRect().height() / 2);
qreal y = groundY - m_jumpAnimation->currentValue().toReal() * m_jumpHeight;
m_player->setY(y);
checkColliding();
}
void MyScene::checkTimer()
{
//该函数首先检查玩家是否移动。
//如果没有,计时器就会停止,因为当我们的大象静止不动时,不需要更新任何东西。
if (m_player->direction() == 0) {
m_timer.stop();
//否则,计时器将启动,但前提是它尚未运行。
//我们通过在计时器上调用 isActive() 来检查这一点。
} else if (!m_timer.isActive()) {
m_timer.start();
}
}
void MyScene::checkColliding()
{
//调用场景的 QGraphicsScene::collidingItems() 函数
//该函数将应检测到碰撞项目的项目作为第一个参数
//使用第二个可选参数,您可以定义应如何检测碰撞
//该参数的类型是 Qt::ItemSelectionMode
//默认情况下,如果两个项目的形状相交,则项目将被视为与 m_player 碰撞
for(QGraphicsItem* item: collidingItems(m_player)) {
//我们遍历找到的项目列表并检查当前项目是否是 Coin 对象
//这是通过尝试将指针强制转换为 Coin 来完成的
//如果成功,我们通过调用explode() 来爆炸硬币
//所以玩家第一次击中硬币时,硬币会爆炸,但这需要时间。 在这次爆炸中,玩家很可能会再次被移动,从而再次与硬币碰撞。 在这种情况下,explode() 可能会被多次调用
if (Coin *c = qgraphicsitem_cast<Coin*>(item)) {
c->explode();
}
}
}
void MyScene::axisLeftXChanged(double value)
{
int direction;
if (value > 0) {
direction = 1;
} else if (value < 0) {
direction = -1;
} else {
direction = 0;
}
m_player->setDirection(direction);
checkTimer();
}
void MyScene::axisLeftYChanged(double value)
{
if (value < -0.25) {
jump();
}
}
视差滚动是一种为游戏背景添加深度错觉的技巧。 当背景具有以不同速度移动的不同层时,就会出现这种错觉。 最近的背景必须比远处的背景移动得更快。 在我们的例子中,我们有从最远到最近的四个背景:
天空
树
草
地面