欢迎来到如何在 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。运行场景;现在你的怪物知道他要去哪里了。

现在错误表现在哪里?

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

通知玩家

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

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

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

public Text waveLabel;
public GameObject[] nextWaveLabels;

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

保存文件并切换到 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 正确开始。

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

生成敌人波次

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

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

设置敌人标签

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

创建一个命名为enemy_的标签。

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

定义敌人波次信息

现在你需要定义一波敌人。在 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

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


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

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

没有一个塔防游戏只有一种类型的敌人是完整的。幸运的是,预制件文件夹包含另一个选项,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 的健康指示器数组中 - 健康指示器是快乐地吃饼干的绿色小怪物。

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

怪物战争:怪物的复仇

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

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

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)。

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

现在,重复这些步骤,将生命值栏添加到预制件_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 中运行游戏。你会在敌人上方看到生命条。

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

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

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

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

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

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

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

为了允许在触发区域中检测敌人,您需要向其添加碰撞体和刚体,因为 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 - 或者任何让你开心的东西。

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

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

Leveling the Bullets 调平子弹

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

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

public GameObject bullet;
public float fireRate;

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

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

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

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

开火

在 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

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

  1. 零基础学CocosCreator·第七季-制作一款塔防游戏

    第七季-制作一款塔防游戏 01.塔防前言 为什么是塔防? 准备 02.使用TileMap创建地图 新建地图 获取地图 编辑地图 代码操控 运行 03-16.实战中 04.状态机 代码 08.事件分发器 ...

  2. 你的时间是如何被谋杀的?---由一款塔防游戏引发的思考

    你的时间是如何被谋杀的? ---由一款塔防游戏引发的思考 文/LL 之前一段时间花了很长时间来玩了一个 Mac OS平台下的塔防游戏<iBomber Defense Pacific>,为此 ...

  3. 用Unity开发一款塔防游戏(一):攻击方设计

    大家好.偶尔想起了这个手把手教学的.但现已长满杂草的坑,还是来挖几铲子. 这一期的游戏是最常见的类型之一--塔防. 塔防游戏相信大家并不陌生,几个主要元素如下: 1.敌方士兵 2.我方防御塔 3.我方 ...

  4. unity塔防游戏怪物转向_玩一玩这款塔防游戏?

    关注上方蓝字获得更多内容 今日分享塔防游戏 塔防游戏,曾经也是风靡一时,非常受人喜欢的. 今天小皮分享的这款和保卫萝卜可以说是异曲同工,还是不错的. 该游戏拥有精美的画面,并且在形象设计上也制作的相当 ...

  5. unity塔防游戏怪物转向_红包版塔防游戏合集-可以赚钱领红包的塔防游戏-无广告塔防游戏红包版大全...

    家园保卫战红包版等级:9.22020-11-1016.7MB简体中文下载推荐理由:家园保卫战红包版是一款全新塔防赚钱小游戏,和植物大战僵尸画风有些相似,只不过这款游戏增加了红包版机制,每次闯关成功都有 ...

  6. 塔防游戏c语言源代码,用Unity开发一款塔防游戏(一):攻击方设计

    private void Update() { hpObj.rotation = mainCamera.rotation; //血条始终面向镜头 if (GameMain.instance.gameO ...

  7. unity制作一款塔防游戏

    文章目录 介绍 寻路系统 怪物生成器 制作3种初级炮台.3种升级炮台 设置炮台属性 选择炮台,添加监听事件 炮弹追踪攻击敌人 拖动鼠标实现相机视角转换 鼠标光标放在cube上变色 文字动画 介绍 关键 ...

  8. 如何轻松打造一款智能防丢神器?让生活更省心省力

    在生活中,你一定遇到过丢东西的情况.比如马上要出门了,却发现钥匙不见了,你翻箱倒柜找半天,看着上班时间临近,急得团团转-- 这时候,如果拥有一款蓝牙防丢器,就能在找不到钥匙的时候,打开App点击&qu ...

  9. 【HMS Core案例分享】华为分析 X 江湖游戏 | 揭秘塔防游戏的增长秘籍

    江湖游戏是一家集网络游戏发行.运营和服务于一体的公司,于2018年成立,是国内新兴的游戏发行商之一.江湖游戏凭借着3D欧美卡通塔防巨作<塔塔帝国>,以及诙谐幽默的<泡面三国>, ...

最新文章

  1. 计算机书籍-机器学习预测分析java神经网络算法与实现
  2. Xcode 9 快速跳转到定义新姿势(Jump to Definition)
  3. js字符串的操作方法
  4. 【机器学习】梯度下降的Python实现
  5. sign函数的功能 oracle,Oracle中sign函数和decode函数的使用
  6. MyBatis 源码解读-mapperElement()
  7. 科普扫盲,HTTP Status Code详解,从此排错无忧!
  8. python linux编程与window编程_Python3如何在Windows和Linux上打包
  9. 第八课-第二讲 08_02_bash脚本编程之七 case语句及脚本选项进阶
  10. Exchange 2013SP1和O365混合部署系列一
  11. 人工智能TensorFlow工作笔记009---认识TensorFlow中的会话
  12. 内联函数inline,无比节省开销的
  13. html在线快递单号打印,HTML 快递打印模板
  14. 抖音短视频教程VIP培训课程(2019实时更新中)
  15. oracle block corrupted,ORA-01578: ORACLE data block corrupted
  16. 卸载python2.7_98%的人这样卸载软件,真的卸载干净了吗?这才是正确的卸载方式...
  17. Flutter 项目实战 网络请求MD5+时间戳+验证签名 十一
  18. Python 爬虫爬取奥运奖牌榜数据
  19. Linux-脚本安装、快照、重置虚拟机
  20. 利用python获取身份证号中年龄和性别信息

热门文章

  1. 单片机LED点阵控制。
  2. matlab画列车运行图,列车运行图常用画法的具体方法是什么?
  3. 学霸占据互联网过半江山
  4. css2d炫酷效果,利用CSS3实现炫酷的飞机起飞动画
  5. npoi 执行公式_NPOI 关于excel计算公式,且公式有外部引用
  6. 北大物理学院欧阳颀院士:成为科学家的五大要素
  7. ABAP RSA方式调用工行银企直联API
  8. 使用百度UNIT配置智能对话机器人的注意事项,开发者必看!
  9. 【总结】Markdown使用笔记
  10. 这里聚焦了全球嵌入式技术风景~