超抽象飞机大战——天翌版
一,场景设置
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());
- 创建两个背景图片,并将它们添加到场景中。
- 再将originalPixmap缩放到GameSetting::SceneWidthGameSetting::SceneHeight的大小。Qt::IgnoreAspectRatio参数表示在缩放时忽略原始图片的宽高比,Qt::SmoothTransformation参数表示使用平滑的缩放算法。
- 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 帧每秒
- 创建一个QTimer的对象
- 连接timer的timeout信号到一个匿名函数。这个匿名函数会在每次timer的timeout信号被触发时执行。
- 将background1和background2的y坐标向下移动1个单位。这会使得背景图片向下滚动。
- 检查background1和background2的y坐标是否已经到达或超过0。如果是,那么就将另一个背景图片的y坐标设置为当前背景图片的y坐标减去另一个背景图片的高度。这会使得另一个背景图片被放置在当前背景图片的上方,从而在当前背景图片滚动到底部时,另一个背景图片就会滚动到顶部,创建出无限滚动的背景的效果。
- 启动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;
}
}
}
- foreach循环,它会遍历keys容器中的每一个元素。在每次循环中,key变量都会被设置为keys容器中的一个元素。
- 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;
}
}
}
- 调用了collidingItems函数来获取所有与当前对象发生碰撞的对象的列表,然后将这个列表赋值给itemList变量。
- for(auto item:itemList):这是一个范围for循环,它会遍历itemList列表中的每一个元素。在每次循环中,item变量都会被设置为itemList列表中的一个元素。
- 如果碰到玩家,则玩家生命值减少,碰到子弹则敌机生命值减少
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的移动不是简单的从上往下,而是随机在场中缓慢移动
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是我自己添加的