游戏开发小结——Boss AI
目标:为我们的游戏创建一个 Boss。
让我们的游戏有一个 Boss 系统,这样当我们的玩家到达第 5 波时,它将产生我们的新 Boss。
Boss sprite.
https://free-game-assets.itch.io/free-enemy-spaceship-2d-sprites-pixel-art
因此,我们将首先创建一个新类来处理我们的 BossBehavior,而不是使用我们的敌人类。
与我们的敌人不同,我们的Boss会在屏幕外生成,并慢慢进入屏幕。在此期间,他将刀枪不入。
private bool _isInvulnerable = true;
private bool _readyForCombat = false;
private void Update()
{
if(_readyForCombat)
CalculateMovement();
else
{
transform.Translate(_speed / 2f * Time.deltaTime * Vector3.down);
if (transform.position.y <= _maxYPosition)
{
_readyForCombat = true;
_isInvulnerable = false;
}
}
}
我们在这里所做的就是当我们的 Boss 创建时,它有一个 bool readyForCombat ,允许我们慢慢地将他带入屏幕。他在这段时间内是无敌的,这是由我们的 isInvulnerable 变量处理的。
现在让我们检查一下我们将如何让我们的Boss采取行动。与我们的敌人不同,我想要的是 Boss 向侧面移动,并试图找到玩家。当玩家位于 Boss 面前时,它会向下移动直到屏幕末端。一旦到达那里,它就会向随机方向发射激光,使玩家难以逃脱。
然后他就回到自己的位置并再次开始侧向移动。每次他这样做,他就会变得更快。
private enum BossState
{
Sideways,
Down
}
private BossState _state;
private float _speed = 5f;
private int _direction = 1;
private float _maxYPosition = 5f;
private float _minYPosition = -3f;
private float _maxXPosition = 9f;
private float _minXPosition = -9f;
private void CalculateMovement()
{
switch (_state)
{
case BossState.Sideways:
transform.Translate(_speed * _speedMultiplier * _direction * Time.deltaTime * Vector3.right);
if(transform.position.x >= _maxXPosition || transform.position.x <= _minXPosition)
{
ChangeDirection();
}
CheckPlayerPosition();
break;
case BossState.Down:
transform.Translate(_speed * 1.5f * _speedMultiplier * _direction * Time.deltaTime * Vector3.down);
if (transform.position.y <= _minYPosition)
{
ShootLasers();
ChangeDirection();
}
if(transform.position.y >= _maxYPosition)
{
IncreaseSpeed();
ChangeState(BossState.Sideways);
}
break;
}
}
那么让我们检查一下我们在做什么。我们有一个枚举,表示 Boss 当前所处的状态。它可以横向移动,也可以向下移动。
如果我们要横向移动,我们会根据一个整数方向移动,这将使我们横向移动。当我们达到基于 maxXPosition 或 minXPosition 的屏幕限制时,我们只需改变方向即可。移动后,我们检查玩家是否在我们前面,我们稍后将创建该方法。
如果我们要向下,我们只需向下移动并根据 minYPosition 检查 Boss 是否已达到屏幕的极限。
如果这是真的,我们会发射激光(我们稍后将创建激光),并改变方向,这样我们就可以向后移动到原来的位置。
如果这是错误的,我们只需检查我们是否回到了 maxYPosition,然后我们只需改变我们的状态并增加我们的移动速度。
private void IncreaseSpeed()
{
_speedMultiplier += 0.05f;
}
private void ChangeDirection()
{
_direction *= -1;
}
private void ChangeState(BossState newState)
{
_state = newState;
}
我们只是创建了基本方法来帮助我们处理 Boss 速度的增加、改变方向和状态。
private void CheckPlayerPosition()
{
if(_player != null)
{
Vector2 toPlayer = _player.transform.position - transform.position;
float angle = Vector2.SignedAngle(-transform.up, toPlayer.normalized);
float alignmentThreshold = 5f;
float verticalAlignment = Mathf.Abs(toPlayer.x) / Mathf.Abs(toPlayer.y);
if (Mathf.Abs(angle) < alignmentThreshold && verticalAlignment < 1f)
AttackPlayer();
}
}
这是我们用来检查玩家位置的方法。我们检查玩家是否不为空,如果玩家不为空,我们只需执行以下操作:
toPlayer:计算从boss到玩家的向量。
angle角度:计算 Boss 向上方向与玩家的归一化向量之间的带符号角度。负的transform.up用于检查敌人的前方(downwards下方)。
VerticalAlignment:计算垂直对齐方式,表示玩家相对于 Boss 的垂直对齐方式。
if 语句:检查玩家向下移动的条件,考虑角度对齐和垂直对齐。当条件满足时,我们只需调用 AttackPlayer 方法即可。
private void AttackPlayer()
{
ChangeState(BossState.Down);
_direction = 1;
}
我们只需将状态更改为 Down 并确保方向为 1,这样我们就向下走。
private float _laserYPosition = 7.5f;
private void ShootLasers()
{
Vector2[] positions = new Vector2[]
{ new(-9.0f, -5f), new(-4.5f, -0.5f), new(0f, 4.5f), new(5.0f, 9.0f) };
foreach (Vector2 laserPosition in positions)
{
float randomX = Random.Range(laserPosition.x, laserPosition.y);
Vector3 laserSpawnPosition = new(randomX, _laserYPosition, 0.0f);
Instantiate(_laserPrefab, laserSpawnPosition, Quaternion.identity);
}
}
这是我们发射激光的方法。因此,当我们到达屏幕底部时,我们将发射随机落在玩家身上的激光。
我们的向量数组包含一对向量2,其中包含每个激光的位置。您可以看到每个位置都有屏幕的特定部分,这意味着激光将始终落在屏幕的大部分区域,而不是随机落下。这样我们就可以确保覆盖很大一部分,让玩家很难逃脱。
在 foreach 循环中,我们简单地为激光器的 X 分配一个随机位置,该位置受到向量值的限制。然后我们在生成的位置上实例化激光。
最后,我们想要确保当我们的Boss 受到伤害时,我们不是能够持续受到攻击,而是简单地轻弹我们的Boss ,表明他是无敌的。
private IEnumerator BossInvulnerabilityRoutine()
{
_isInvulnerable = true;
for (int i = 3; i >= 0; i--)
{
_spriteTransform.gameObject.SetActive(false);
yield return new WaitForSeconds(0.17f);
_spriteTransform.gameObject.SetActive(true);
yield return new WaitForSeconds(0.17f);
}
_isInvulnerable = false;
}
这样,当我们的 Boss 受到伤害时,我们只需调用此例程即可确保玩家可以看到 Boss 在短时间内不会再次受到伤害。
private int _health = 10;
public void TakeDamage()
{
if (_isInvulnerable)
return;
_health -= 1;
StartCoroutine(BossInvulnerabilityRoutine());
if (_health <= 0)
{
Instantiate(_explosionPrefab, transform.position, Quaternion.identity);
Destroy(this);
}
}
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("PlayerLaser"))
{
Destroy(other.gameObject);
TakeDamage();
}
else if (other.CompareTag("Player"))
{
if (_player != null)
_player.TakeDamage();
TakeDamage();
}
}
这是我们的最后两种方法。其中一种允许 Boss 受到伤害,降低他的生命值并启动协程以使其刀枪不入。
我们的 TakeDamage 方法检查 Boss 是否无敌,如果是,该方法将返回而不采取进一步操作。否则,它会将健康值降低 1 并启动我们的协程。当他没有生命值时,我们会引发爆炸并摧毁我们的老板。
我们的 OnTriggerEnter2D 只是检查我们是否与玩家激光或玩家发生碰撞,以便在需要时摧毁碰撞的激光并损坏玩家。
我们boss的日常。

现在我们的游戏中有一个boss了!到达第 5 波时会产生这个敌人,这会让我们的玩家变得更加困难!