上篇已经为敌人的出现做好准备了,现在是时候让敌人登场了:

4、敌人初步实现

这里出去3件套(尺寸可以直接用图片大小,我用的是静态常量,习惯而已)

其中m_active表示是否可以移动,只有当其为true时,敌人才可以移动

m_destinationWayPoint用来存储当前航点,在判断中,一般如下使用

if (collisionWithCircle(m_pos, 1, m_destinationWayPoint->pos(), 1))
{// 敌人抵达了一个航点if (m_destinationWayPoint->nextWayPoint()){// 还有下一个航点m_pos = m_destinationWayPoint->pos();m_destinationWayPoint = m_destinationWayPoint->nextWayPoint();}else{// 表示进入基地m_game->getHpDamage();m_game->removedEnemy(this);return;}
}

每次判断敌人的中心(m_pos也表示中心,绘制的时候也就需要偏移)与航点中心是否碰撞了,碰撞了,则继续向下一航点出发,若没有航点,表示到基地了,由MainWindow调用getHpDamage(先给一个空实现)和removeEnemy(Enemy *enemy),说的没错,又是在MainWindow中用容器管理:

QList<Enemy *> m_enemyList;    // 记得需要在paintEvent中进行绘制

m_game就是MainWindow,用于最后敌人进入基地或被打死的时候调用移除函数

同时,这里新添了一个碰撞函数,新建一个utility.h就可以了,基本上公共函数就这么一个

inline bool collisionWithCircle(QPoint point1, int radius1, QPoint point2, int radius2)
{const int xdif = point1.x() - point2.x();const int ydif = point1.y() - point2.y();const int distance = qSqrt(xdif * xdif + ydif * ydif);if (distance <= radius1 + radius2)return true;return false;
}

这里设置inline,纯粹是放在.h中,被多个包含会创建多个实例,应该在cpp中放实现,不过这个不是重点啦~

m_rotationSprite,用来存储敌人到下一个航点时的图片旋转角度,其实炮台也有这个属性,不过现在不打炮,也就不添加了。

来看下Enemy有哪些具体实现吧:

Enemy::Enemy(WayPoint *startWayPoint, MainWindow *game, const QPixmap &sprite/* = QPixmap(":/image/enemy.png")*/): QObject(0), m_pos(startWayPoint->pos()), m_sprite(sprite)
{m_maxHp = 40;m_currentHp = 40;m_active = false;m_walkingSpeed = 1.0;m_destinationWayPoint = startWayPoint->nextWayPoint();m_rotationSprite = 0.0;m_game = game;
}

构造中,很简单的进行了些默认赋值,40点血,够炮台打4炮啦,嘿嘿

不过默认图片是向左的,而实际开始,图片应该要向右,不过有修正啦

看下绘制函数吧

void Enemy::draw(QPainter *painter)
{if (!m_active)return;// 血条的长度// 其实就是2个方框,红色方框表示总生命,固定大小不变// 绿色方框表示当前生命,受m_currentHp / m_maxHp的变化影响static const int Health_Bar_Width = 20;painter->save();QPoint healthBarPoint = m_pos + QPoint(-Health_Bar_Width / 2 - 5, -ms_fixedSize.height() / 3);// 绘制血条painter->setPen(Qt::NoPen);painter->setBrush(Qt::red);QRect healthBarBackRect(healthBarPoint, QSize(Health_Bar_Width, 2));painter->drawRect(healthBarBackRect);painter->setBrush(Qt::green);QRect healthBarRect(healthBarPoint, QSize((double)m_currentHp / m_maxHp * Health_Bar_Width, 2));painter->drawRect(healthBarRect);// 绘制偏转坐标,由中心+偏移=左上static const QPoint offsetPoint(-ms_fixedSize.width() / 2, -ms_fixedSize.height() / 2);painter->translate(m_pos);painter->rotate(m_rotationSprite);// 绘制敌人painter->drawPixmap(offsetPoint, m_sprite);painter->restore();
}

这个基本上前面和炮台绘制类似,只是多了步painter->rotate(m_rotationSprite);不过这个旋转比较简单,就是直来直往的

再来看下,敌人实际每次移动调用的函数

void Enemy::move()
{if (!m_active)return;if (collisionWithCircle(m_pos, 1, m_destinationWayPoint->pos(), 1)){// 敌人抵达了一个航点if (m_destinationWayPoint->nextWayPoint()){// 还有下一个航点m_pos = m_destinationWayPoint->pos();m_destinationWayPoint = m_destinationWayPoint->nextWayPoint();}else{// 表示进入基地m_game->getHpDamage();m_game->removedEnemy(this);return;}}// 还在前往航点的路上// 目标航点的坐标QPoint targetPoint = m_destinationWayPoint->pos();// 未来修改这个可以添加移动状态,加快,减慢,m_walkingSpeed是基准值// 向量标准化double movementSpeed = m_walkingSpeed;QVector2D normalized(targetPoint - m_pos);normalized.normalize();m_pos = m_pos + normalized.toPoint() * movementSpeed;// 确定敌人选择方向// 默认图片向左,需要修正180度转右m_rotationSprite = qRadiansToDegrees(qAtan2(normalized.y(), normalized.x())) + 180;
}

这里唯一和数学搭点界的就是对向量进行标准化,移动速度,其实每次都是1,normalized取值只有(1,0),(-1,0),(0,-1),(0,1)四种,主要用来得到角度计算敌人旋转角度,这里的角度不够细腻,90,180,270的,在炮塔旋转中,角度会细腻很多

再来看下MainWindow中添加的方法

void MainWindow::getHpDamage(int damage/* = 1*/)
{// 暂时空实现,以后这里进行基地费血行为
}
void MainWindow::removedEnemy(Enemy *enemy)
{Q_ASSERT(enemy);m_enemyList.removeOne(enemy);delete enemy;if (m_enemyList.empty()){++m_waves; // 当前波数加1// 继续读取下一波if (!loadWave()){// 当没有下一波时,这里表示游戏胜利// 设置游戏胜利标志为truem_gameWin = true;// 游戏胜利转到游戏胜利场景// 这里暂时以打印处理}}
}

同时MainWindow中需要添加方法loadWave来加载下一波敌人的数目和出现时间,见下:

bool MainWindow::loadWave()
{if (m_waves >= 6)return false;WayPoint *startWayPoint = m_wayPointsList.back(); // 这里是个逆序的,尾部才是其实节点int enemyStartInterval[] = { 100, 500, 600, 1000, 3000, 6000 };for (int i = 0; i < 6; ++i){Enemy *enemy = new Enemy(startWayPoint, this);m_enemyList.push_back(enemy);QTimer::singleShot(enemyStartInterval[i], enemy, SLOT(doActivate()));}return true;
}

这里初步设计6波结束,每波出现6个敌人,时间按ms记,以后这里会改用xml文件来读取控制,在 构造函数中先初始化航点,再调用此函数

用一个QTimer::singleShot来定时发送信息,是的enemy可以移动,因此Enemy需要继承于QObject,才可以使用信号和槽

直接看下doActiate

void Enemy::doActivate()
{m_active = true;
}

默认m_active = false;是不行动的,只有在调用这个槽函数之后,才可以行动

在MainWindow中继续关联一个QTimer,每30ms发送一个信号,更新一次map,主要是为了移动敌人,模拟帧数

QTimer *timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(updateMap()));
timer->start(30);

在构造函数中完成此事

同时添加updateMap槽函数

void MainWindow::updateMap()
{foreach (Enemy *enemy, m_enemyList)enemy->move();update();
}

这样,大概1秒会执行33次此函数,来对敌人进行移动

同时要在构造函数中填对对m_waves = 0的赋值,paintEvent中补充对敌人的绘制,看下效果图吧!

5、为界面绘制添加缓存

一直都是直接在界面上绘制,这样难免效率会底很多了,因此采用先绘制到一张QPixmap上

最后再绘制此QPixmap即可

见MainWindow中的修改

void MainWindow::paintEvent(QPaintEvent *)
{QPixmap cachePix(":/image/Bg.png");QPainter cachePainter(&cachePix);foreach (const TowerPosition &towerPos, m_towerPositionsList)towerPos.draw(&cachePainter);foreach (Tower *tower, m_towersList)tower->draw(&cachePainter);foreach (const WayPoint *wayPoint, m_wayPointsList)wayPoint->draw(&cachePainter);foreach (Enemy *enemy, m_enemyList)enemy->draw(&cachePainter);QPainter painter(this);painter.drawPixmap(0, 0, cachePix);
}

这里就这样做一个缓存即可

6、炮塔完善实现

炮塔不是花架子,不能让敌人就这么赤果果冲进老家,来,先打两炮,这里需要为炮塔提供可以攻击敌人的方法

这里红色部分,除了draw以外都是新加的,全是针对Enemy的,为了打炮,新建一个类Bullet(子弹),比较简单,一会介绍

其中,shootWeapon和m_fireRateTimer还有m_fireRate关联,设置打炮频率,因此Tower类也需要继承于QObject

m_fireRateTimer = new QTimer(this);
connect(m_fireRateTimer, SIGNAL(timeout()), this, SLOT(shootWeapon()));

查看新添方法:

void Tower::attackEnemy()
{// 启动打炮模式m_fireRateTimer->start(m_fireRate);
}
void Tower::chooseEnemyForAttack(Enemy *enemy)
{// 选择敌人,同时设置对敌人开火m_chooseEnemy = enemy;// 这里启动timer,开始打炮attackEnemy();// 敌人自己要关联一个攻击者,这个用QList管理攻击者,因为可能有多个m_chooseEnemy->getAttacked(this);
}
void Tower::shootWeapon()
{// 每次攻击,产生一个子弹// 子弹一旦产生,交由m_game管理,进行绘制Bullet *bullet = new Bullet(m_pos, m_chooseEnemy->pos(), m_damage, m_chooseEnemy, m_game);bullet->move();m_game->addBullet(bullet);
}
void Tower::targetKilled()
{// 目标死亡时,也需要取消关联// 取消攻击if (m_chooseEnemy)m_chooseEnemy = NULL;m_fireRateTimer->stop();m_rotationSprite = 0.0;
}
void Tower::lostSightOfEnemy()
{// 当敌人脱离炮塔攻击范围,要将炮塔攻击的敌人关联取消// 同时取消攻击m_chooseEnemy->gotLostSight(this);if (m_chooseEnemy)m_chooseEnemy = NULL;m_fireRateTimer->stop();m_rotationSprite = 0.0;
}

这里炮塔打炮的原则是,锁定第一个关联的目标,一直攻击,直到敌人离开或死亡

看下子弹类的声明

m_startPos记录炮塔的位置,也就是子弹起始的位置

m_targetPos记录敌人的位置,也就是终点位置

m_currentPos,这里用来记录子弹当前位置,这里利用Qt的动画机制,将m_currentPos注册为属性,来使用

m_target就是要击中的敌人

m_damage就是由Tower的攻击决定

Qt动画效果使用见下

Q_PROPERTY(QPoint m_currentPos READ currentPos WRITE setCurrentPos)

这里注册为Qt属性,在生成子弹之后,调用move方法,使子弹进行自动动画效果

void Tower::shootWeapon()
{Bullet *bullet = new Bullet(m_pos, m_chooseEnemy->pos(), m_damage, m_chooseEnemy, m_game);bullet->move();m_game->addBullet(bullet);
}

这里调用move执行动画

void Bullet::move()
{// 100毫秒内击中敌人static const int duration = 100;QPropertyAnimation *animation = new QPropertyAnimation(this, "m_currentPos");animation->setDuration(duration);animation->setStartValue(m_startPos);animation->setEndValue(m_targetPos);connect(animation, SIGNAL(finished()), this, SLOT(hitTarget()));animation->start();
}

设定的是100ms内集中敌人,简单易懂

动画结束,关联hitTarget

void Bullet::hitTarget()
{// 这样处理的原因是:// 可能多个炮弹击中敌人,而其中一个将其消灭,导致敌人delete// 后续炮弹再攻击到的敌人就是无效内存区域// 因此先判断下敌人是否还有效if (m_game->enemyList().indexOf(m_target) != -1)m_target->getDamage(m_damage);m_game->removedBullet(this);
}

这里就需要MainWindow返回一个敌人链表,从中查看,该敌人是否还存在

敌人阵亡直接受伤,这里没有所谓防御力一说,见下

void Enemy::getRemoved()
{if (m_attackedTowersList.empty())return;foreach (Tower *attacker, m_attackedTowersList)attacker->targetKilled();// 通知game,此敌人已经阵亡m_game->removedEnemy(this);
}void Enemy::getDamage(int damage)
{m_currentHp -= damage;// 阵亡,需要移除if (m_currentHp <= 0)getRemoved();
}

Enemy现在需要维护一个QList<Tower*>,因为同一时间可能有多个炮塔对其进行攻击

最后看下敌人死亡时,从MainWindow中移除的处理:

void MainWindow::removedEnemy(Enemy *enemy)
{Q_ASSERT(enemy);m_enemyList.removeOne(enemy);delete enemy;if (m_enemyList.empty()){++m_waves;if (!loadWave()){m_gameWin = true;// 游戏胜利转到游戏胜利场景// 这里暂时以打印处理}}
}

直接remove,然后delete,所以刚刚在Bullet的hitTarget判断中需要 先判断该敌人是否还存在

这里通过设置一个bool来判断游戏是否胜利

游戏还有一个bool来判断是否结束(也就是基地沦陷)

bool m_gameEnded;
bool m_gameWin;

这两个一个只用来表示胜利否,另一个只用来表示输了否,他俩的false值我不关心,只在乎是否为true

在paintEvent中开始部分添加以下内容

if (m_gameEnded || m_gameWin)
{QString text = m_gameEnded ? "YOU LOST!!!" : "YOU WIN!!!";QPainter painter(this);painter.setPen(QPen(Qt::red));painter.drawText(rect(), Qt::AlignCenter, text);return;
}

直接在屏幕中央打印信息输出就好了

m_gameEnded属性只有在基地被爆了以后才能赋值,这里需要为基地设置血量

添加属性m_playerHp,默认为5

在以前实现的MainWindow::getHpDamage中添加以下内容

void MainWindow::getHpDamage(int damage/* = 1*/)
{m_audioPlayer->playSound(LifeLoseSound);m_playerHp -= damage;if (m_playerHp <= 0)doGameOver();
}void MainWindow::doGameOver()
{if (!m_gameEnded){m_gameEnded = true;// 此处应该切换场景到结束场景// 暂时以打印替代,见paintEvent处理}
}

这样子,基本上就算完成了一大部分了,看下效果图


胜利失败界面比较丑陋,嘿嘿,没有图片啦~,哎

7、添加打印信息同时限制玩家经济

限制经济很简单,在MainWindow中添加属性

int m_playerGold;

默认值为1000,每次买炮塔需要300,每击毁一个坦克就奖励200

以前空实现的canBuyTower,现在可以大展身手了

static const int TowerCost = 300;bool MainWindow::canBuyTower() const
{if (m_playrGold >= TowerCost)return true;return false;
}

这里判断是否可以买

在MousePressEvent中进行真正减钱的操作

void MainWindow::mousePressEvent(QMouseEvent *event)
{QPoint pressPos = event->pos();auto it = m_towerPositionsList.begin();while (it != m_towerPositionsList.end()){if (canBuyTower() && it->containPoint(pressPos) && !it->hasTower()){m_playerGold -= TowerCost;it->setHasTower();Tower *tower = new Tower(it->centerPos(), this);m_towersList.push_back(tower);update();break;}++it;}
}

这部分处理其实和以前是一样的,只是多了 m_playerGold -= TowerCost;

在Enemy阵亡的时候,进行奖励操作

void Enemy::getDamage(int damage)
{m_game->audioPlayer()->playSound(LaserShootSound);m_currentHp -= damage;// 阵亡,需要移除if (m_currentHp <= 0){m_game->audioPlayer()->playSound(EnemyDestorySound);m_game->awardGold(200);getRemoved();}
}
void MainWindow::awardGold(int gold)
{m_playrGold += gold;update();
}

这下子,就可以对玩家进行经济限制了

然后就是打印一下信息输出,在paintEvent中添加以下代码

void MainWindow::drawWave(QPainter *painter)
{painter->setPen(QPen(Qt::red));painter->drawText(QRect(400, 5, 100, 25), QString("WAVE : %1").arg(m_waves + 1));
}void MainWindow::drawHP(QPainter *painter)
{painter->setPen(QPen(Qt::red));painter->drawText(QRect(30, 5, 100, 25), QString("HP : %1").arg(m_playerHp));
}void MainWindow::drawPlayerGold(QPainter *painter)
{painter->setPen(QPen(Qt::red));painter->drawText(QRect(200, 5, 200, 25), QString("GOLD : %1").arg(m_playrGold));
}
void MainWindow::paintEvent(QPaintEvent *)
{// ... do somethingdrawWave(&cachePainter);drawHP(&cachePainter);drawPlayerGold(&cachePainter);
    QPainter painter(this);painter.drawPixmap(0, 0, cachePix);
}

这下再看下效果图!

Oh Yeah,不错哦,是那么回事,O(∩_∩)O哈哈~

嘿嘿,目前基本工作完成!

下篇文章继续放出处理声音相关内容和XML读取相关内容!

Qt版本-塔防游戏实现二相关推荐

  1. Qt版本-塔防游戏实现一

    这个游戏来源于一篇较早的国外作品,不过原作是以Cocos2D为基础实现的,链接见下: http://www.raywenderlich.com/37701/how-to-make-a-tower-de ...

  2. Qt之塔防游戏 c++(一)

    话不多说,我们直接进入正题吧. 这个阶段我们要完成如下功能: 1:图片的绘制 2:敌人运动轨迹的绘制 3:防御塔坑(可放置防御塔点)的绘制 4:鼠标点击事件,实现防御塔的出现 图片的绘制 首先我们在Q ...

  3. 塔防游戏(二) 埋雷(防止游戏数据被更改)

    本文给两个问题提供解决方案 1,防御塔的属性相当多,什么攻击力,攻击速度,攻击范围,建造花费,等等等等...... 把他们的数据放到程序里面显然是不合适的,如果把这些数据放到程序中,十分不便于修改, ...

  4. QT实现简单的塔防游戏

    QT实现简单的塔防游戏 该程序中实现了购买炮塔.炮塔升级.怪物按照设定路径移动.炮塔自动寻找范围内目标.朝目标怪物发射炮弹.爆炸效果.怪物走到家时我方生命值减少.方便添加关卡等功能. 另附重构版本代码 ...

  5. QT 框架搭建,用最原始的方法实现简单的塔防游戏 | 原力计划

    作者 | 白家名 责编 | 王晓曼 出品 | CSDN博客 本文作者使用 QT 框架写了一个塔防游戏程序,该程序中实现了购买炮塔.炮塔升级.怪物按照设定路径移动.炮塔自动寻找范围内目标.朝目标怪物发射 ...

  6. 基于QT多关卡的塔防游戏

    基于QT多关卡的塔防游戏 基于QT多关卡的塔防游戏 参考模板 核心实现思想 运行图片 代码 基于QT多关卡的塔防游戏 多关卡多怪兽,多防御塔与多子弹类型,对于界面无美化,主要实现其功能,Boss尺寸是 ...

  7. 一起来设计“塔防游戏”吧

    2020年就要结束了! 我们的公众号也已经走过了一年.感谢大家的陪伴! 新的2021年 我们准备玩点不一样的东西--我们一起来设计一个游戏. 我们将在新的一年里, 不断地改进这个游戏. 不断地添加新的 ...

  8. unity塔防游戏怪物转向_Unity官方新手游戏项目推荐合集

    Unity官方新手游戏项目推荐合集 今天给同学们介绍一些Unity官方发布过的一些游戏项目,这些项目都简化了游戏开发的入门学习过程,可以快速地制作出游戏,适合新手入门体验,下面就带同学们看一看: Un ...

  9. IOS塔防游戏《坦克对大炮》的开发设计记录

    IOS塔防游戏<坦克对大炮>的开发设计记录 引子 游戏已经在App Store上线几个月了,一直很想写点什么记录一下.真要写的时候,却又发现无从下笔没啥好写的.在2012年进入IOS,对于 ...

最新文章

  1. 要如何努力,才干成为非常厉害的人?!
  2. matlab脉宽调制pwm,PWM脉宽调制直流调速系统设计及MATLAB仿真验证
  3. 6.Python补充_Python之道
  4. 等待链表_调度(准备运行)链表
  5. php调用matlab
  6. pytorch基础知识整理(三)模型保存与加载
  7. 深入理解脚本化CSS系列第二篇——查询计算样式
  8. SharePoint 跨域还原网站一则
  9. 第三次大转型:中国居住新十年报告
  10. springboot thymeleaf模板使用
  11. MySQL双主(master-master)补充
  12. 部署heroku代码时,权限被拒绝(公钥)。 致命:远端意外挂断
  13. PAM for Kmedoids algorithm, PAM算法的实现, kmeans 算法实现. 利用scikit-learn toolbox.
  14. js a/a中this的使用
  15. 命令以及查找帮助方法
  16. Db4o数据库:快速入门
  17. 内存泄露检测工具VLD(Visual Leak Detector)使用说明
  18. 调试铁通与联通专线遇到的问题
  19. 所以,网络工程师能从事什么工作?
  20. Kafka 的消息异常情况~追日

热门文章

  1. 微软答应2021年再给Flash续命几个月
  2. 停简单电子优惠系统_停简单app下载
  3. 【动画】css实现旋转和平移效果
  4. 逐行分析鸿蒙系统的 JavaScript 开发框架
  5. android 应用联想,联想推出Android应用商店 面向大型企业
  6. Struts2之配置文件中Action的详细配置
  7. win10环境安装tensorflow-gpu,软件版本、硬件支持、安装过程
  8. (已部分解决)MySQL:IntegrityError(1062, Duplicate entry 'NULL' for key 'id')
  9. 看电影学英语--欧美经典电影
  10. 华为鸿蒙到底出来了没,你知道华为鸿蒙到底是什么吗?