ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

《Game Programming using Qt 5 Beginner’s Guide》 - jgame学习

2021-08-04 09:30:42  阅读:236  来源: 互联网

标签:direction Qt Beginner int void Programming player MyScene include


在这里插入图片描述

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_horizo​​ntalInput 类字段来累加输入值。
    //无论何时更改,我们都会在将其传递给 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_horizo​​ntalInput 类字段来累加输入值
    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();
    }
}

视差滚动是一种为游戏背景添加深度错觉的技巧。 当背景具有以不同速度移动的不同层时,就会出现这种错觉。 最近的背景必须比远处的背景移动得更快。 在我们的例子中,我们有从最远到最近的四个背景:

天空

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

地面

在这里插入图片描述

标签:direction,Qt,Beginner,int,void,Programming,player,MyScene,include
来源: https://blog.csdn.net/TM1695648164/article/details/119361261

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有