超抽象飞机大战——天翌版,Qt,Cpp实现

超抽象飞机大战——天翌版

一,场景设置

A. 在main函数中创建场景

//创建场景
    QGraphicsScene * scene =new QGraphicsScene;

创建一个QGraphicsScene类的对象

B.场景滚动效果的实现

// 创建两个背景图片
    QPixmap originalPixmap(":/images/rubbish/stone.png");
    QPixmap scaledPixmap = originalPixmap.scaled(GameSetting::SceneWidth, GameSetting::SceneHeight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
    QGraphicsPixmapItem *background1 = scene->addPixmap(scaledPixmap);
    QGraphicsPixmapItem *background2 = scene->addPixmap(scaledPixmap);
    background2->setY(-background2->pixmap().height());
  1. 创建两个背景图片,并将它们添加到场景中。
  2. 再将originalPixmap缩放到GameSetting::SceneWidthGameSetting::SceneHeight的大小。Qt::IgnoreAspectRatio参数表示在缩放时忽略原始图片的宽高比,Qt::SmoothTransformation参数表示使用平滑的缩放算法。
  3. background2->setY(-background2->pixmap().height());:将background2的y坐标设置为-background2->pixmap().height()。这意味着background2的顶部会被放置在场景的顶部之上,这样在开始时background2就不会被看到。这通常用于创建滚动背景的效果,当background1滚动到场景的底部时,background2就会滚动到场景的顶部,从而创建出无限滚动的背景的效果。
 // 创建定时器,每隔一段时间就移动背景图片
    QTimer *timer = new QTimer;
    QObject::connect(timer, &QTimer::timeout, [=]() {
        background1->setY(background1->y() + 1);
        background2->setY(background2->y() + 1);
        if (background1->y() >= 0) {
            background2->setY(background1->y() - background2->pixmap().height());
        }
        if (background2->y() >= 0) {
            background1->setY(background2->y() - background1->pixmap().height());
        }
    });
    timer->start(1000 / 60);  // 60 帧每秒
  1. 创建一个QTimer的对象
  2. 连接timer的timeout信号到一个匿名函数。这个匿名函数会在每次timer的timeout信号被触发时执行。
  3. 将background1和background2的y坐标向下移动1个单位。这会使得背景图片向下滚动。
  4. 检查background1和background2的y坐标是否已经到达或超过0。如果是,那么就将另一个背景图片的y坐标设置为当前背景图片的y坐标减去另一个背景图片的高度。这会使得另一个背景图片被放置在当前背景图片的上方,从而在当前背景图片滚动到底部时,另一个背景图片就会滚动到顶部,创建出无限滚动的背景的效果。
  5. 启动timer,并设置其超时时间为1000 / 60毫秒,也就是大约16.67毫秒。这意味着timer的timeout信号会每秒触发大约60次,也就是每秒更新60帧,从而创建出流畅的滚动效果。

C场景的大小设置

 scene->setSceneRect(0,0,GameSetting::SceneWidth,GameSetting::SceneHeight);//设置场景高度和宽度
 scene->setBackgroundBrush(QImage(":/images/rubbish/stone.png"));//背景源

定义场景的宽度和高度,这个游戏只有x轴和y轴,所以前两个参数就设置成0了

二,Player类的思路与实现

A.类的创建与继承

class Player :public QObject, public QGraphicsPixmapItem
{
    Q_OBJECT
public:
    Player(QGraphicsItem *parent = nullptr);

    // QGraphicsItem interface
protected:
    virtual void keyPressEvent(QKeyEvent *event) override;//press
    void keyReleaseEvent(QKeyEvent *event) override;//release

这里的Player之间继承QObject和QGraphicsPixmapItem。

B.按键的基本设置和按键连贯性的实现

如果只是单纯写按键后面加个功能的话,这样在游戏按键的时候就会有移动的适合卡帧,或者移动时按发子弹键会卡住的情况,为了解决这个问题,翌就使用了Qt的事件循环和信号槽机制。
在这里插入图片描述

1.按键连贯实现

创建QTimer对象
连接信号槽

keyRespondTimer = new QTimer(this);	//构造函数中创建定时器对象,并连接信号槽
    connect(keyRespondTimer, &QTimer::timeout, this, &Player::slotTimeOut);

设置按下按键的函数

void Player::keyPressEvent(QKeyEvent *event)
{
    if(!event->isAutoRepeat())  //判断如果不是长按时自动触发的按下,就将key值加入容器
        keys.append(event->key());
    if(!keyRespondTimer->isActive()) //如果定时器不在运行,就启动一下
        keyRespondTimer->start(4);
}

设置释放按键的函数

void Player::keyReleaseEvent(QKeyEvent *event){
    if(!event->isAutoRepeat())  //判断如果不是长按时自动触发的释放,就将key值从容器中删除
        keys.removeAll(event->key());
    if(keys.isEmpty()) //容器空了,关闭定时器
        keyRespondTimer->stop();
}
2.按键基本功能添加
void Player::slotTimeOut(){
    foreach (int key, keys) {
        switch(key){
        case Qt::Key_A://左移
            if(pos().x()>0)//在边框右边才能继续左移
                setPos(x()-PlayerMoveSpeed,y());
            break;
        case Qt::Key_D://右移
            if(pos().x()<SceneWidth-boundingRect().width()*PlayerScale)
                setPos(x()+PlayerMoveSpeed,y());
            break;
        case Qt::Key_W://前移
            if(pos().y()>0)//在边框右边才能继续左移
                setPos(x(),y()-PlayerMoveSpeed);
            break;
        case Qt::Key_S://后移
            if(pos().y()<SceneHeight-boundingRect().height()*PlayerScale)//在边框右边才能继续左移
                setPos(x(),y()+PlayerMoveSpeed);
            break;
        case Qt::Key_R://重启游戏
            if(playing) return;//不希望在游戏过程中不小心按到了
            playing=true;//更新游戏状态
            Health::getInstance().reset();//健康值重设置
            Score::getInstance().reset();//分数重设置
            messageItem->hide();//隐藏message
            //pictureItem->hide();
            //qDebug() << "Hiding pictureItem";
            break;
        case Qt::Key_Space://发射子弹
        {
            bulletSound.play();//播放子弹发射的音效
            Bullet*bullet=new Bullet;//生成子弹
            int temp=x()+boundingRect().width()*PlayerScale/2;//只考虑player的宽度
            temp-=bullet->boundingRect().width()*BulletScale/2;//减去子弹宽度,向左移动半个子弹的宽度
            bullet->setPos(temp,y());//设置位置
            scene()->addItem(bullet);//在场景中添加bullet
        }

        break;
        }
    }
}
  1. foreach循环,它会遍历keys容器中的每一个元素。在每次循环中,key变量都会被设置为keys容器中的一个元素。
  2. switch语句,它会根据key变量的值来执行相应的case语句。

C.生成器系统

这个生成器系统就是,在游戏中会有一些敌机呀,子弹呀这样的元素,这些元素都是会在游戏中持续生成的,而在整个游戏过程中player是一直存在的,所以我就把这些生成器放在player类中,然后通过timerEvent来实现周期性的调用

//timerEvent调用敌机孵化器,rty,bullet
void Player::timerEvent(QTimerEvent *event)
{
    if(playing){
        if (event->timerId() == timerEnemy) {
            enemySpawn();//如果处于游戏状态,则调用敌机孵化器
        }else if(event->timerId() == timerRty){rtySpawn();
        }else if(event->timerId() == timerBullet){bulletSpawn();
        }else if(event->timerId() == timerBoss){bossSpawn();
        }
    }

    if(Health::getInstance().getHealth()<=0){
        gameOver();//如果生命值小于0则调用gameOver
    }
}

我在这里有一个使用startTimer函数启动一个定时器,然后每当定时器超时,就会自动调用timerEvent函数的设计。

void Player::bulletSpawn()
{
    if(playing){
        bulletSound.play();//播放子弹发射的音效
        Bullet*bullet=new Bullet;//生成子弹
        int temp=x()+boundingRect().width()*PlayerScale/2;//只考虑player的宽度
        temp-=bullet->boundingRect().width()*BulletScale/2;//减去子弹宽度,向左移动半个子弹的宽度
        bullet->setPos(temp,y());//设置位置
        scene()->addItem(bullet);//在场景中添加bullet
    }
}

这里以子弹生成器为例,如果游戏处于游玩阶段就会调用这个子弹生成器,子弹生成在飞机的中间。

D.游戏状态和游戏结束设计

 bool playing =true;//游戏状态

在player类中添加playing的成员,默认值为真

void Player::gameOver()
{
    playing=false;
    for(auto item:scene()->items()){//游戏结束时删除所有敌机
        if(typeid(*item)==typeid(Enemy)){
            scene()->removeItem(item);//删除敌机
            delete item;
        }else if(typeid(*item)==typeid(Boss)){
            scene()->removeItem(item);//删除敌机
            delete item;
        }
    }

在player中添加gameover函数,gameover会让playing状态变为否,同时遍历所有item,删除画面中的enemy和boss。

三,Enemy类的思路与实现

Enemy类用于小兵的生成

A.类的创建与继承

#include <QGraphicsPixmapItem>
#include <QObject>

class Enemy :public QObject, public QGraphicsPixmapItem
{
    Q_OBJECT
public:
    Enemy(int type,QGraphicsItem *parent = nullptr);
    ~Enemy(){
        //qDebug()<<"析构Enemy";
    };//析构Enemy
    // QObject interface
protected:
    virtual void timerEvent(QTimerEvent *event) override;
private:
    int Ehealth;  // 表示敌机的血量
    int type;
    qreal scale;  // 敌人的缩放
};

Enemy继承QObject,QGraphicsPixmapItem
成员有敌机血量,敌机类型

B敌机随机种类生成的实现

Enemy::Enemy(int type, QGraphicsItem *parent) :QGraphicsPixmapItem(parent),type(type)
{
    switch (type) {
    case 1:
        setPixmap(QPixmap(":/images/rubbish/R__1_-removebg-preview.png"));//插入敌人的图片
        setScale(PangolinScale);//设置比例
        // ...设置其他的设置...
        break;
    case 2:
        setPixmap(QPixmap(":/images/rubbish/OIP-removebg-preview.png"));//插入敌人的图片
        setScale(iKunScale);//设置比例
        // ...设置其他的设置...
        break;
    case 3:
        setPixmap(QPixmap(":/images/rubbish/maomao-removebg-preview.png"));//插入敌人的图片
        setScale(MaomaoScale);//设置比例
        // ...设置其他的设置...
        break;

    default:
        setPixmap(QPixmap(":/images/enemy.png"));//插入敌人的图片
        setScale(EnemyScale);//设置比例
        // ...设置其他的设置...
        break;
    }


    int max=SceneWidth-boundingRect().width()*EnemyScale;//随机位置的最大值
    int randomNumber=QRandomGenerator::global()->bounded(1,max);//使用随机数
    setPos(randomNumber,0);//敌人出现的位置
    startTimer(EnemyTimer);//50毫秒
    Ehealth=EnemyHealth;
}

在构造函数中添加一个switch每个type对应一种敌机

void Player::enemySpawn()
{
    if(playing){  // 如果程序处于运行状态
        int type = QRandomGenerator::global()->bounded(1, 5);  // 生成一个在 范围内的随机数
        Enemy *enemy = new Enemy(type);  // 生成新的 enemy,传入敌人的类型
        scene()->addItem(enemy);  // 将 enemy 插入场景
    }
}

在敌机生成器中type的值是随机的

C.碰撞系统

   QList<QGraphicsItem*>  itemList=collidingItems();//collidingItems返回列表,收集所有的碰撞物体
    for(auto item:itemList){//遍历
        if(typeid(*item)==typeid(Player)){//如果类型为Player
            Health::getInstance().decrease();//健康值需要decrease
            scene()->removeItem(this);//删除场景
            delete this;//如果敌机撞到了Player也会被析构
            return;
        }
        else if (typeid(*item) == typeid(Bullet)) {
            Ehealth--;  // 当敌机被子弹击中时,减少血量
            scene()->removeItem(item);
            delete item;
            if (Ehealth <= 0) {  // 当血量降至0时,敌机被销毁
                Score::getInstance().increase();
                scene()->removeItem(this);
                delete this;
                return;
            }
        }
    }
  1. 调用了collidingItems函数来获取所有与当前对象发生碰撞的对象的列表,然后将这个列表赋值给itemList变量。
  2. for(auto item:itemList):这是一个范围for循环,它会遍历itemList列表中的每一个元素。在每次循环中,item变量都会被设置为itemList列表中的一个元素。
  3. 如果碰到玩家,则玩家生命值减少,碰到子弹则敌机生命值减少

D.随机位置生成和向下匀速运动

int max=SceneWidth-boundingRect().width()*EnemyScale;//随机位置的最大值
    int randomNumber=QRandomGenerator::global()->bounded(1,max);//使用随机数
    setPos(randomNumber,0);//敌人出现的位置
    startTimer(EnemyTimer);//50毫秒
    Ehealth=EnemyHealth;

在构造函数中添加随机生成的横坐标位置`

    setPos(x(),y()+EnemySpeed);//Enemy下落
    if(y()>SceneHeight){//如果Enemy掉到场景下面就析构Enemy
        scene()->removeItem(this);//删场景
        delete this;
        return;

向下运动的部分,如果敌机落到了视野范围外就会被析构,并移除场景

四,健康值和分数值的实现

在第三方入图片描述
这里的分数和健康值是展现在左上角的
实现的话是建立两个类,Health和Score
接下来我以health为例来讲

A.全局静态接口

static Health&getInstance(){//提供接口 全局静态

它只会被创建一次,然后在后续的调用中都会返回同一个实例。这样就保证了Health类只有一个实例。

    //健康值文字item
    scene->addItem(&Health::getInstance());

在main函数中添加Health,这里使用getInstance来保证值由一个实例

B.内置功能

 int getHealth(){return health;}//获取健康值 内联函数
    void decrease();//碰到敌机健康值下降
    void reset();

我这里是内置了获取生命值,生命值下降,重置生命值三个部分

void Health::reset()
{
    health=GameSetting::HealthStart;//健康值赋值
    setPlainText("健康值: "+QString::number(health));//设置文字
    setDefaultTextColor(Qt::red);//设置文字颜色
    setFont(QFont("Courier New",GameSetting::FontSize,QFont::Bold));//设置字体 Blod加粗
    setPos(x(),GameSetting::FontSize*2);//设置文字位置
}

生命值每次重置都会在左上角设置一次位置

五,Boss类的思路设计与实现

Boss在场景中最多只能存在一个,Boss区别与其他小怪除了外形的差异外害多了一个血条,Boss的移动不是简单的从上往下,而是随机在场中缓慢移动
   fds

A.Boss血条的实现

    // 初始化血条
    healthBar = new QGraphicsRectItem(0, 0, SceneWidth, 10, this);
    healthBar->setBrush(Qt::red);
    healthBar->setPos(0, 0);

在构造函数中初始化血条的宽度,颜色,和初始位置

 // 更新血条的位置和长度
    healthBar->setPos(0, 0);
    healthBar->setRect(0, 0, HealthWidth * Bhealth / BossHealth, 10);

在碰撞中添加更新位置和长度的操作

B.随机移动的设置

// 初始化 Boss 的移动方向
       double randomX = QRandomGenerator::global()->generateDouble() * 2 - 1;  // 生成一个 [-1, 1) 范围内的随机浮点数
       double randomY = QRandomGenerator::global()->generateDouble() - 1;  // 生成一个 [-1, 0) 范围内的随机浮点数
       direction = QPointF(randomX, randomY);

在构造函数中初始化Boss的随机运动方向`

// 更新 Boss 的位置
    setPos(x() + direction.x() * BossSpeed, y() + direction.y() * BossSpeed);

在timerevent中添加移动操作

// 如果 Boss 移动到了地图边界,则改变移动方向
    if (x() < 0 || x() > SceneWidth - boundingRect().width() * EnemyScale) {
        direction.setX(-direction.x());
    }
    if (y() < 0 || y() > SceneHeight / 2 - boundingRect().height() * EnemyScale) {
        direction.setY(-direction.y());
    }

为了保证Boss在场景中,这里Boss移动到边界时会改变方向

C.最多存在1个Boss

static bool isExist();  // 检查是否存在Boss的静态函数

添加静态成员确定Boss的存在状态,初始值设置为false,在构造函数中再赋值为true

if (exist) {
        // 如果场上已经存在一个 Boss,则不创建新的 Boss
        return;
    }

在构造函数中添加这段,如果已经存在,就直接return,不进行后面的操作了

void Player::bossSpawn()
{
    if (playing && !Boss::isExist()) {  // 如果游戏正在进行,并且场上不存在 Boss
        Boss *boss = new Boss;  // 生成新的 Boss
        scene()->addItem(boss);  // 将 Boss 添加到场景中
    }
}

Boss生成器中叶添加Boss存在性的判断,双重保险(其实是写多余了哈哈)

六,其他小设置,和背景音乐

    scene->setStickyFocus(true);//不会在被点击时,取消player的focus状态
    QGraphicsView view(scene);
    view.setFixedSize(GameSetting::SceneWidth,GameSetting::SceneHeight);
    view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

这串让鼠标不点击玩家飞机也能实现目标锁定

  //添加背景音乐
    QMediaPlayer bgMusic;//创建播放器对象
    QAudioOutput audioOutput;//创建音频输出设备
    bgMusic.setAudioOutput(&audioOutput);//为bgMusic设置音频输出设备
    bgMusic.setSource(QUrl("qrc:/sounds/bigBanana.m4a"));//背景音乐源
    QObject::connect(&bgMusic, &QMediaPlayer::mediaStatusChanged, [&](QMediaPlayer::MediaStatus status) {
        if (status == QMediaPlayer::EndOfMedia) {
            bgMusic.play();
        }
    }); // 设置循环播放
    bgMusic.play();//播放

这串是添加bgm并一直循环

七,引用文献

【Qt6.3.1 C++飞机大战(完整版)】 https://www.bilibili.com/video/BV1JM411R7pW/?share_source=copy_web&vd_source=cd3f6fe4c6e6eefd42df5642f07b538c

整体的架构是参考这个视频,中间的按键设置和多种敌人的添加和Boss是我自己添加的

  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值