上篇已經爲敵人的出現做好準備了,現在是時候讓敵人登場了:
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讀取相關內容!