上篇已经为敌人的出现做好准备了,现在是时候让敌人登场了:
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()) { // 当没有下一波时,这里表示游戏胜利 // 设置游戏胜利标志为true m_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的攻击决定
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 something
drawWave(&cachePainter);
drawHP(&cachePainter);
drawPlayerGold(&cachePainter);
QPainter painter(this);
painter.drawPixmap(0, 0, cachePix);
}
这下再看下效果图!
Oh Yeah,不错哦,是那么回事,O(∩_∩)O哈哈~
嘿嘿,目前基本工作完成!
下篇文章继续放出处理声音相关内容和XML读取相关内容!