【Unity小游戏】游戏开发案例,轻松打造一款塔防游戏!(下)

欢迎来到如何在 Unity 中创建塔防游戏的第二部分。你正在Unity中制作一个塔防游戏,在第一部分结束时,你可以放置和升级怪物。你还有一个敌人攻击饼干。

然而,敌人不知道该面对哪条路!此外,这是攻击的一个严重的失误。在这一部分中,你将添加敌人的生成波次,武装你的怪物,这样他们就可以保护你珍贵的饼干。

开始

在 Unity 中,打开本教程系列第一部分中已完成的项目,或者如果您现在刚刚加入,请下载入门项目并打开 TowerDefense-Part2-Starter。

使敌人转起来

在上一个教程结束时,敌人沿着道路前进,但似乎不知道该面对哪条路。

在 IDE 中打开 MoveEnemy.cs,然后添加以下方法来解决此问题。

private void RotateIntoMoveDirection()
{
  
  Vector3 newStartPosition = waypoints [currentWaypoint].transform.position;
  Vector3 newEndPosition = waypoints [currentWaypoint + 1].transform.position;
  Vector3 newDirection = (newEndPosition - newStartPosition);
  
  float x = newDirection.x;
  float y = newDirection.y;
  float rotationAngle = Mathf.Atan2 (y, x) * 180 / Mathf.PI;
  
  GameObject sprite = gameObject.transform.Find("Sprite").gameObject;
  sprite.transform.rotation = Quaternion.AngleAxis(rotationAngle, Vector3.forward);
}

RotateIntoMoveDirection rotates the enemy so that it always looks forward, like so:
RotateIntoMoveDirection 旋转敌人,使其始终向前看,如下所示:

它通过从下一个航点的位置中减去当前航点的位置来计算 虫子 的当前移动方向。
它使用 `Mathf.Atan2` 来确定 `newDirection` 指向的角度(以弧度为单位),假设零点向右。将结果乘以 `180 / Mathf.PI` 会将角度转换为度数。
最后,它检索名为 Sprite 的子项,并沿 z 轴将其旋转 `rotationAngle` 度。请注意,您旋转的是子项而不是父项,因此生命条(您很快就会添加它)保持水平状态。

Update() 中,将注释 // TODO: Rotate into move direction 替换为对 RotateIntoMoveDirection 的以下调用:

RotateIntoMoveDirection();

保存文件并切换到 Unity。运行场景;现在你的怪物知道他要去哪里了。

BugFollowsRoad.gif

现在错误表现在哪里?

一个敌人?几乎不令人印象深刻。让成群结队的人来。就像每个塔防游戏一样,成群结队的人会一波又一波地来!

通知玩家

在你让部落开始行动之前,你需要让玩家知道即将到来的猛攻。另外,为什么不在屏幕顶部显示当前波次的数字?

多个游戏对象需要生成波次信息,因此您需要将其添加到 GameManager 上的 GameManagerBehavior 组件中。

在 IDE 中.cs打开 GameManagerBehavior,然后添加以下两个变量:

public Text waveLabel;
public GameObject[] nextWaveLabels;

waveLabel 在屏幕右上角存储对波读数的引用。 nextWaveLabels 存储两个游戏对象,当它们组合在一起时,将创建一个动画,您将在新波开始时显示,如下所示:

nextWaveAnimation

保存文件并切换到 Unity。在层次结构中选择游戏管理器。单击“生成波次标签”右侧的小圆圈,然后在“选择文本”对话框中,选择“场景”选项卡中的“生成波次标签”。

现在将下一波标签的大小设置为 2。然后将元素 0 分配给 NextWaveBottomLabel,将元素 1 分配给 NextWaveTopLabel,方法与设置 Wave Label 的方式相同。

这就是您的游戏管理器行为应该的样子

如果玩家输掉了游戏,他应该不会看到下一波消息。要解决此问题,请在 IDE 中切换回 GameManagerBehavior.cs并添加另一个变量:

public bool gameOver = false;

gameOver 中,您将存储玩家是否输掉了游戏。

同样,您将使用属性来保持游戏元素与当前波同步。将以下代码添加到 GameManagerBehavior

private int wave;
public int Wave
{
  get
  {
    return wave;
  }
  set
  {
    wave = value;
    if (!gameOver)
    {
      for (int i = 0; i < nextWaveLabels.Length; i++)
      {
        nextWaveLabels[i].GetComponent<Animator>().SetTrigger("nextWave");
      }
    }
    waveLabel.text = "WAVE: " + (wave + 1);
  }
}

创建私有变量、属性和 getter 现在应该是第二天性。但同样,setter有点棘手。

使用新的 value 更新 wave

然后你检查游戏是否没有结束。如果是这样,则遍历 nextWaveLabels 中的所有标签 — 这些标签具有 Animator 组件。要在动画器上触发动画,请设置触发器 nextWave。

最后,您将 waveLabeltext 设置为 wave + 1 的值。为什么是 +1 ?– 正常人不会从零开始计数。奇怪,我知道:]

Start() 中,设置此属性的值:

Wave = 0;

您从 Wave 数字 0 开始计数。

保存文件,然后在 Unity 中运行场景。波读数从 1 正确开始。

Internally you start counting with 0, but for the player everything starts with wave 1.

对于玩家来说,一切都从第 1 波开始。

生成敌人波次

这听起来很明显,但你需要能够创造更多的敌人来释放成群结队的敌人——现在你不能这样做。此外,一旦当前生成波次被抹去,你就不应该产生下一波——至少目前是这样。

因此,游戏必须能够识别场景中是否有敌人,而标签是识别游戏对象的好方法。

设置敌人标签

在项目浏览器中选择敌人预制件。在检查器的顶部,单击“标记”下拉列表,然后选择“添加标记”。

Create Tag

创建一个命名为enemy_的标签。
create a new tag

选择敌人预制件。在检查器中,将其标签设置为敌人。

定义敌人波次信息

现在你需要定义一波敌人。在 IDE 中打开 SpawnEnemy.cs,并在 SpawnEnemy 之前添加以下类实现:

[System.Serializable]
public class Wave
{
  public GameObject enemyPrefab;
  public float spawnInterval = 2;
  public int maxEnemies = 20;
}

Wave 包含一个 enemyPrefab ,这是实例化该波中所有敌人的基础, spawnInterval 是波中敌人之间的时间(以秒为单位)和 maxEnemies ,即在该波中生成的敌人数量。

此类是可序列化的,这意味着您可以在检查器中更改值。

将以下变量添加到 SpawnEnemy 类:

public Wave[] waves;
public int timeBetweenWaves = 5;

private GameManagerBehavior gameManager;

private float lastSpawnTime;
private int enemiesSpawned = 0;

这将设置一些用于生成的变量,这些变量与您沿航点移动敌人的方式非常相似。
您将在 waves 中定义游戏的各种波次,并分别在 enemiesSpawnedlastSpawnTime 中跟踪生成的敌人数量和生成时间。

玩家在杀戮后需要休息,因此将 timeBetweenWaves 设置为5秒

Start() 的内容替换为以下代码。

lastSpawnTime = Time.time;
gameManager =
    GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();

在这里,您将 lastSpawnTime 设置为当前时间,这将是脚本在场景加载后立即启动的时间。然后以熟悉的方式检索 GameManagerBehavior

将此添加到 Update()

int currentWave = gameManager.Wave;
if (currentWave < waves.Length)
{
  
  float timeInterval = Time.time - lastSpawnTime;
  float spawnInterval = waves[currentWave].spawnInterval;
  if (((enemiesSpawned == 0 && timeInterval > timeBetweenWaves) ||
       timeInterval > spawnInterval) && 
      enemiesSpawned < waves[currentWave].maxEnemies)
  {
    
    lastSpawnTime = Time.time;
    GameObject newEnemy = (GameObject)
        Instantiate(waves[currentWave].enemyPrefab);
    newEnemy.GetComponent<MoveEnemy>().waypoints = waypoints;
    enemiesSpawned++;
  }
  
  if (enemiesSpawned == waves[currentWave].maxEnemies &&
      GameObject.FindGameObjectWithTag("Enemy") == null)
  {
    gameManager.Wave++;
    gameManager.Gold = Mathf.RoundToInt(gameManager.Gold * 1.1f);
    enemiesSpawned = 0;
    lastSpawnTime = Time.time;
  }
  
}
else
{
  gameManager.gameOver = true;
  GameObject gameOverText = GameObject.FindGameObjectWithTag ("GameWon");
  gameOverText.GetComponent<Animator>().SetBool("gameOver", true);
}

逐步完成此代码:

  • 获取当前波次的索引,并检查它是否是最后一个波次。
  • 如果是这样,请计算自上次敌人生成以来经过了多少时间,以及是否是时候生成敌人了。在这里,您考虑两种情况。如果是波中的第一个敌人,请检查 timeInterval 是否大于 timeBetweenWaves 。否则,请检查 timeInterval 是否大于此波次的 spawnInterval 。无论哪种情况,你都要确保你没有为这一波生成所有的敌人。
  • 如有必要,通过实例化 enemyPrefab 的副本来生成敌人。您还可以增加 enemiesSpawned 计数。
  • 你检查屏幕上的敌人数量。如果没有,它是波次中的最后一个敌人,你就会生成下一波。你还会给玩家在波次结束时剩余的所有金币的10%。
  • 在击败最后一波运行后,游戏赢得了动画。

设置生成间隔

保存文件并切换到 Unity。在层次结构中选择道路。在检查器中,将波次的大小设置为 4。

现在,将所有四个元素的“敌人预制件”设置为“敌人”。设置生成间隔和最大敌人数字段,如下所示:

  • Element 0: Spawn Interval: 2.5, Max Enemies: 5
  • Element 1: Spawn Interval: 2, Max Enemies: 10
  • Element 2: Spawn Interval: 2, Max Enemies: 15
  • Element 3: Spawn Interval: 1, Max Enemies: 5

最终设置应如下面的屏幕截图所示。

Waves
当然,你可以使用这些设置来增加或减少攻击伤害。
运行游戏。啊哈!虫子正在向你的饼干进军!

bugs.gif

可选:添加不同类型的敌人

没有一个塔防游戏只有一种类型的敌人是完整的。幸运的是,预制件文件夹包含另一个选项,Enemy2。

在检查器中选择预制件\Enemy2,然后将MoveEnemy脚本添加到其中。将其速度设置为 3,将其标签设置为 敌人。您现在可以使用这个快速错误来让玩家保持警惕!

更新玩家生命值

即使成群结队的虫子冲向饼干,玩家也不会受到任何伤害。但仅此而已。当玩家让敌人入侵时,他应该受到打击。

在 IDE 中.cs打开“游戏管理器行为”,然后添加以下两个变量:

public Text healthLabel;
public GameObject[] healthIndicator;

您将使用 healthLabel 访问玩家的生命值读数,并使用 healthIndicator 访问五个绿色饼干嘎吱嘎吱的小怪物——它们只是以比标准健康标签更有趣的方式代表玩家健康。

管理生命值

接下来,添加一个属性以在 GameManagerBehavior 中维护玩家的生命值:

private int health;
public int Health
{
  get
  {
    return health;
  }
  set
  {
    
    if (value < health)
    {
      Camera.main.GetComponent<CameraShake>().Shake();
    }
    
    health = value;
    healthLabel.text = "HEALTH: " + health;
    
    if (health <= 0 && !gameOver)
    {
      gameOver = true;
      GameObject gameOverText = GameObject.FindGameObjectWithTag("GameOver");
      gameOverText.GetComponent<Animator>().SetBool("gameOver", true);
    }
    
    for (int i = 0; i < healthIndicator.Length; i++)
    {
      if (i < Health)
      {
        healthIndicator[i].SetActive(true);
      }
      else
      {
        healthIndicator[i].SetActive(false);
      }
    }
  }
}

这将管理玩家的生命值。再一次,大部分代码都在 setter 中:

  • 如果要降低玩家的生命值,请使用 CameraShake 组件创建漂亮的摇晃效果。此脚本包含在项目中,此处未介绍。
  • 更新屏幕左上角的私有变量和健康标签。
  • 如果生命值降至 0 且游戏尚未结束,请将 gameOver 设置为 true 并触发 GameOver 动画。
  • 从饼干中移除其中一个怪物。如果它只是禁用了它们,则可以更简单地编写此位,但它也支持在添加运行状况时重新启用它们。

Initialize Health in Start():

Health = 5;

当场景开始播放时,将 Health 设置为 5

设置此属性后,您现在可以在 虫子 到达 Cookie 时更新玩家的运行状况。保存此文件,然后切换到仍在 IDE 中的 MoveEnemy.cs。

更新显示生命值

若要更新玩家的生命值,请在 Update() 中找到 // TODO: deduct health 的注释,并将其替换为以下代码:

GameManagerBehavior gameManager =
    GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
gameManager.Health -= 1;

这将获取 GameManagerBehavior 并从其 Health 中减去一个。

保存文件并切换到 Unity。

在层次结构中选择“游戏管理器”,并将其“健康标签”设置为“健康标签”。

展开层次结构中的 Cookie,并将其五个健康指示器子项拖放到 GameManager 的健康指示器数组中 - 健康指示器是快乐地吃饼干的绿色小怪物。

播放场景并等待虫子到达饼干。什么都不做,直到你输了。

cookie-attack

怪物战争:怪物的复仇

怪物就位?完毕。敌人在前进?完毕。 - 他们看起来很卑鄙!是时候把那些傻瓜割下来了!

一个生命条,所以玩家知道哪些敌人是强的,哪些是弱的
探测怪物范围内的敌人
决策点——向哪个敌人开火

Enemy Health Bar 敌人生命条

你将使用两张图片来实现生命条,一张用于深色背景,另一张用于稍微小一点的绿色条,你将缩放以匹配敌人的生命值。

Prefabs\Enemy 从项目浏览器拖到场景中。

然后将“ Images\Objects\HealthBarBackground ”拖到层次结构中的“敌人”上,将其添加为子项。

在检查器中,将 _HealthBarBackground_的位置设置为 (0, 1, -4)。

接下来,在项目浏览器中选择“ Images\Objects\HealthBar”,并确保其“透视”设置为“左”。然后,将其添加为层次结构中敌人的子项,并将其位置设置为 (-0.63, 1, -5)。将其 X 比例设置为 125。

将一个名为 HealthBar 的新 C# 脚本添加到 HealthBar 游戏对象。稍后,您将对其进行编辑以调整生命条的长度。

在层次结构中选择敌人后,确保它的位置为 (20, 0, 0)。

单击检查器顶部的“应用”,将所有更改保存为预制件的一部分。最后,从层次结构中删除敌人。

虫子 with healthbar

现在,重复这些步骤,将生命值栏添加到预制件_Prefabs\Enemy2_。

Adjust Health Bar Length 调整生命条长度

在 IDE 中打开 HealthBar.cs,然后添加以下变量:

public float maxHealth = 100;
public float currentHealth = 100;
private float originalScale;

maxHealth 存储敌人的最大生命值, currentHealth 跟踪剩余的生命值。最后, originalScale 会记住健康条的原始大小。

将对象的 originalScale 存储在 Start() 中:

originalScale = gameObject.transform.localScale.x;

保存 localScalex 值。

通过将以下内容添加到 Update() 来设置健康条的比例:

Vector3 tmpScale = gameObject.transform.localScale;
tmpScale.x = currentHealth / maxHealth * originalScale;
gameObject.transform.localScale = tmpScale;

localScale 复制到临时变量,因为不能仅调整其 x 值。然后,根据 虫子 的当前运行状况计算新的 x 刻度,并将临时变量设置回 localScale

保存文件并在 Unity 中运行游戏。你会在敌人上方看到生命条。

Resistance is futile! - Wait, what resistance?

在游戏运行时,展开层次结构中的一个敌人(克隆)对象,然后选择其 HealthBar 子对象。更改其“当前运行状况”值,并检查该运行状况栏是否要更改。

AdjustHealthbar

Track Enemies in Range 跟踪范围内的敌人

现在怪物需要知道要瞄准哪些敌人。在实施之前,你对怪物和敌人有一些准备工作要做。

在项目浏览器中选择预制件\怪物,然后在检查器中向其添加圆形碰撞体 2D 组件。

将碰撞体的半径设置为 2.5 - 这将设置怪物的射程。

选中“ Is Trigger”,以便对象穿过该区域而不是撞到该区域。

最后,在检查器的顶部,将怪物图层设置为忽略光线投射。在对话框中单击“是,更改子项”。如果不忽略光线投射,碰撞体会对单击事件做出反应。这是一个问题,因为怪物会阻止针对他们下方的开放点的事件。

Bildschirmfoto 2015-06-05 um 14.47.15

为了允许在触发区域中检测敌人,您需要向其添加碰撞体和刚体,因为 Unity 仅在其中一个碰撞体附加了刚体时才发送触发事件。

在“项目浏览器”中,选择“预制件\敌人”。添加主体类型设置为运动学的刚体 2D 零部件。这意味着身体不应该受到物理学的影响。

添加半径为 1 的 2D 圆形碰撞体。对预制件\敌人 2 重复这些步骤

触发器现在已设置,因此怪物会检测敌人何时在射程内。

你需要准备一件事:一个脚本,当敌人被摧毁时通知怪物,这样他们就不会因为继续射击而引起异常。

创建一个名为 EnemyDestructionDelegate 的新 C# 脚本,并将其添加到 Enemy 和 Enemy2 预制件中。

在 IDE 中打开 EnemyDestructionDelegate.cs,并添加以下委托声明:

public delegate void EnemyDelegate (GameObject enemy);
public EnemyDelegate enemyDelegate;

在这里,您创建一个 delegate ,它是一个函数的容器,可以像变量一样传递。

注: 当您希望一个游戏对象主动通知其他游戏对象更改时,请使用委托。有关委托的更多信息,请参阅 Unity 文档 。

添加以下方法:

void OnDestroy()
{
  if (enemyDelegate != null)
  {
    enemyDelegate(gameObject);
  }
}

销毁游戏对象后,Unity 会自动调用此方法,并检查委托是否不是 null 。在这种情况下,您可以使用 gameObject 作为参数调用它。这让所有注册为代表的侦听器都知道敌人已被消灭。

保存文件并返回到 Unity。

Give Monsters a License to Kill

给怪物一个击杀的脚本

现在怪物可以探测范围内的敌人。将新的 C# 脚本添加到 Monster 预制件中,并将其命名为 ShootEnemies。

在 IDE 中打开 ShootEnemies.cs,然后添加以下 using 语句以访问 Generics

using System.Collections.Generic;

添加一个变量来跟踪范围内的所有敌人:

public List<GameObject> enemiesInRange;

enemiesInRange 中,您将存储范围内的所有敌人。

初始化 Start() 中的字段。

enemiesInRange = new List<GameObject>();

一开始,范围内没有敌人,所以你创建一个空列表。

填写 enemiesInRange 列表!将此代码添加到脚本中:

void OnEnemyDestroy(GameObject enemy)
{
  enemiesInRange.Remove (enemy);
}

void OnTriggerEnter2D (Collider2D other)
{

  if (other.gameObject.tag.Equals("Enemy"))
  {
    enemiesInRange.Add(other.gameObject);
    EnemyDestructionDelegate del =
        other.gameObject.GetComponent<EnemyDestructionDelegate>();
    del.enemyDelegate += OnEnemyDestroy;
  }
}

void OnTriggerExit2D (Collider2D other)
{
  if (other.gameObject.tag.Equals("Enemy"))
  {
    enemiesInRange.Remove(other.gameObject);
    EnemyDestructionDelegate del =
        other.gameObject.GetComponent<EnemyDestructionDelegate>();
    del.enemyDelegate -= OnEnemyDestroy;
  }
}

  • OnEnemyDestroy 中,您将敌人从 enemiesInRange 中移除。当敌人在你的怪物周围扣动扳机时, OnTriggerEnter2D 被召唤。

  • 然后将敌人添加到 enemiesInRange 列表中,并将 OnEnemyDestroy 添加到 EnemyDestructionDelegate 。这样可以确保在敌人被摧毁时调用 OnEnemyDestroy 。你现在不想让怪物把弹药浪费在死去的敌人身上——是吗?

  • OnTriggerExit2D 中,您将敌人从列表中删除并取消注册您的代表。现在你知道哪些敌人在射程内了。

  • 保存文件,然后在 Unity 中运行游戏。要测试它是否有效,请放置一个怪物,选择它并在检查器中观察对 enemiesInRange 列表的更改。

Select a Target 选择目标

现在怪物知道哪个敌人在范围内。但是当有多个射程内的敌人时,他们会怎么做?

当然,他们攻击最接近饼干的人!

在 IDE 中打开 MoveEnemy.cs,然后添加以下新方法来计算:

public float DistanceToGoal()
{
  float distance = 0;
  distance += Vector2.Distance(
      gameObject.transform.position, 
      waypoints [currentWaypoint + 1].transform.position);
  for (int i = currentWaypoint + 1; i < waypoints.Length - 1; i++)
  {
    Vector3 startPosition = waypoints [i].transform.position;
    Vector3 endPosition = waypoints [i + 1].transform.position;
    distance += Vector2.Distance(startPosition, endPosition);
  }
  return distance;
}

此代码计算敌人尚未前进的道路长度。它使用 Distance 来计算两个 Vector3 实例之间的距离。

稍后您将使用此方法来确定要攻击的目标。但是,你的怪物手无寸铁,所以先解决这个问题。

保存文件并返回 Unity 以开始设置项目符号。

给怪物子弹 - 很多子弹!

将图像/对象/项目符号 1 从项目浏览器拖放到场景中。将 z 位置设置为 -2 - x 和 y 位置无关紧要,因为每次在运行时实例化新项目符号时都会设置它们。

添加一个名为 BulletBehavior 的新 C# 脚本,并在 IDE 中向其添加以下变量:

public float speed = 10;
public int damage;
public GameObject target;
public Vector3 startPosition;
public Vector3 targetPosition;

private float distance;
private float startTime;

private GameManagerBehavior gameManager;

speed 确定子弹的飞行速度; damage 是不言自明的。

targetstartPositiontargetPosition 确定项目符号的方向。

distancestartTime 跟踪项目符号的当前位置。 gameManager 在玩家粉碎敌人时奖励玩家。

Start() 中为这些变量赋值:

startTime = Time.time;
distance = Vector2.Distance (startPosition, targetPosition);
GameObject gm = GameObject.Find("GameManager");
gameManager = gm.GetComponent<GameManagerBehavior>();

startTime 设置为当前时间并计算起始位置和目标位置之间的距离。您也可以像往常一样获得 GameManagerBehavior

将以下代码添加到 Update() 以控制项目符号移动:

float timeInterval = Time.time - startTime;
gameObject.transform.position = Vector3.Lerp(startPosition, targetPosition, timeInterval * speed / distance);


if (gameObject.transform.position.Equals(targetPosition))
{
  if (target != null)
  {
    
    Transform healthBarTransform = target.transform.Find("HealthBar");
    HealthBar healthBar = 
        healthBarTransform.gameObject.GetComponent<HealthBar>();
    healthBar.currentHealth -= Mathf.Max(damage, 0);
    
    if (healthBar.currentHealth <= 0)
    {
      Destroy(target);
      AudioSource audioSource = target.GetComponent<AudioSource>();
      AudioSource.PlayClipAtPoint(audioSource.clip, transform.position);

      gameManager.Gold += 50;
    }
  }
  Destroy(gameObject);
}

您可以使用 `Vector3.Lerp` 计算新的项目符号位置,以在开始位置和结束位置之间进行插值。
如果项目符号到达 `targetPosition` ,则验证 `target` 是否仍然存在。
检索目标的 `HealthBar` 组件,并通过项目符号的 `damage` 降低其生命值。
如果敌人的生命值降至零,您可以摧毁它,播放声音效果并奖励玩家的枪法。

保存文件并返回到 Unity。

Get Bigger Bullets 获得更大的子弹

如果你的怪物在更高的水平上射出更大的子弹,那不是很酷吗?- 是的,是的,会的!幸运的是,这很容易实现。

将 Bullet1 游戏对象从“层次结构”拖放到“项目”选项卡,以创建项目符号的预制件。从场景中删除原始对象 - 您不再需要它。

复制 Bullet1 预制件两次。将副本命名为项目符号 2 和项目符号 3。

选择项目符号 2。在检查器中,将精灵渲染器组件的精灵字段设置为图像/对象/子弹 2。这使得 Bullet2 看起来比 Bullet1 大一点。

重复该过程,将 Bullet3 预制件的子画面设置为图像/对象/项目符号 3。

接下来,在子弹行为中设置子弹造成的伤害。

在“项目”选项卡中选择 Bullet1 预制件。在检查器中,您可以看到子弹行为(脚本),在那里您将子弹 1 的伤害设置为 10,子弹 2 的伤害设置为 15,子弹 3 设置为 20 - 或者任何让你开心的东西。

注意:我设置的值是为了在更高的级别,每次伤害的成本更高。这抵消了升级允许玩家在最佳位置改进怪物的事实。

Bullet prefabs - size increases with level

项目符号预制件 - 大小随级别增加

Leveling the Bullets 调平子弹

将不同的子弹分配给不同的怪物级别,以便更强壮的怪物更快地撕碎敌人。

在 IDE 中.cs打开 MonsterData,并将这些变量添加到 MonsterLevel

public GameObject bullet;
public float fireRate;

这些将为每个怪物关卡设置子弹预制件和射速。保存文件并返回 Unity 以完成怪物的设置。

在项目浏览器中选择怪物预制件。在检查器中,展开怪物数据(脚本)组件中的关卡。将每个元素的“射速”设置为 1。然后将元素 0、1 和 2 的项目符号分别设置为项目符号 1、项目符号 2 和项目符号 3。

您的怪物等级应按如下所示进行配置:

MonsterData with bullets

子弹杀死你的敌人?-检查!开火!

开火

在 IDE 中打开 ShootEnemies.cs,并添加一些变量:

private float lastShotTime;
private MonsterData monsterData;

顾名思义,这些变量跟踪这个怪物上次发射的时间,以及 MonsterData 结构,其中包括有关这个怪物的子弹类型、射速等的信息。

Start() 中的这些字段赋值:

lastShotTime = Time.time;
monsterData = gameObject.GetComponentInChildren<MonsterData>();

在这里,您将 lastShotTime 设置为当前时间并访问此对象的 MonsterData 组件。

添加以下方法实现拍摄:

void Shoot(Collider2D target)
{
  GameObject bulletPrefab = monsterData.CurrentLevel.bullet;
  
  Vector3 startPosition = gameObject.transform.position;
  Vector3 targetPosition = target.transform.position;
  startPosition.z = bulletPrefab.transform.position.z;
  targetPosition.z = bulletPrefab.transform.position.z;

  
  GameObject newBullet = (GameObject)Instantiate (bulletPrefab);
  newBullet.transform.position = startPosition;
  BulletBehavior bulletComp = newBullet.GetComponent<BulletBehavior>();
  bulletComp.target = target.gameObject;
  bulletComp.startPosition = startPosition;
  bulletComp.targetPosition = targetPosition;

  
  Animator animator = 
      monsterData.CurrentLevel.visualization.GetComponent<Animator>();
  animator.SetTrigger("fireShot");
  AudioSource audioSource = gameObject.GetComponent<AudioSource>();
  audioSource.PlayOneShot(audioSource.clip);
}

  • 获取子弹的起始位置和目标位置。将 z 位置设置为 bulletPrefab 的位置。之前,您设置了子弹预制件的 z 位置值,以确保子弹出现在发射它的怪物后面,但在敌人的前面。
  • 使用 bulletPrefab 表示 MonsterLevel 实例化新项目符号。分配项目符号的 startPositiontargetPosition
  • 让游戏更具趣味性:运行射击动画,并在怪物射击时播放激光声音。

把所有东西放在一起

是时候将所有内容连接在一起了。确定目标并让你的怪物看着它。

仍然在射击敌人.cs中,将此代码添加到 Update()

GameObject target = null;

float minimalEnemyDistance = float.MaxValue;
foreach (GameObject enemy in enemiesInRange)
{
  float distanceToGoal = enemy.GetComponent<MoveEnemy>().DistanceToGoal();
  if (distanceToGoal < minimalEnemyDistance)
  {
    target = enemy;
    minimalEnemyDistance = distanceToGoal;
  }
}

if (target != null)
{
  if (Time.time - lastShotTime > monsterData.CurrentLevel.fireRate)
  {
    Shoot(target.GetComponent<Collider2D>());
    lastShotTime = Time.time;
  }
  
  Vector3 direction = gameObject.transform.position - target.transform.position;
  gameObject.transform.rotation = Quaternion.AngleAxis(
      Mathf.Atan2 (direction.y, direction.x) * 180 / Mathf.PI,
      new Vector3 (0, 0, 1));
}

逐步完成此代码。

  • 确定怪物的目标。从 minimalEnemyDistance 中的最大可能距离开始。遍历范围内的所有敌人,如果敌人与 cookie 的距离小于当前最小值,则将其设为新目标。
  • 如果经过的时间大于怪物的射速,请调用 Shoot ,并将 lastShotTime 设置为当前时间。
  • 计算怪物与其目标之间的旋转角度。您将怪物的旋转设置为此角度。现在它总是面对目标。

保存文件并在 Unity 中玩游戏。你的怪物大力保护你的饼干。你完全,完全完成了!

尾声

哇,所以你在两个教程之间真的做了很多,你有一个很酷的游戏来展示它。
以下是一些可以在您所做的工作基础上构建的想法:

  • 更多敌人类型和怪物
  • 多条敌人路径
  • 不同的敌人级别

博主属于自学型选手,如果你也是Unity初学者,欢迎加入我的群聊进行互助交流:618012892

猜你喜欢

转载自blog.csdn.net/weixin_72715182/article/details/130629834