Qt版本-塔防遊戲實現二

上篇已經爲敵人的出現做好準備了,現在是時候讓敵人登場了:

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的攻擊決定


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 something

	drawWave(&cachePainter);
	drawHP(&cachePainter);
	drawPlayerGold(&cachePainter);
	
	QPainter painter(this);
	painter.drawPixmap(0, 0, cachePix);
}
這下再看下效果圖!


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



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

下篇文章繼續放出處理聲音相關內容和XML讀取相關內容!









發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章