上篇已经为敌人的出现做好准备了,现在是时候让敌人登场了:
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 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最后看下敌人死亡时,从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读取相关内容!