Qt版本-塔防游戏实现二

5 篇文章 0 订阅
3 篇文章 2 订阅

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

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读取相关内容!









  • 9
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值