欢迎来到如何在 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。
最后,您将 waveLabel
的 text
设置为 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
中定义游戏的各种波次,并分别在 enemiesSpawned
和 lastSpawnTime
中跟踪生成的敌人数量和生成时间。
玩家在杀戮后需要休息,因此将 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;
保存 localScale
的 x
值。
通过将以下内容添加到 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
是不言自明的。
target
、 startPosition
和 targetPosition
确定项目符号的方向。
distance
和 startTime
跟踪项目符号的当前位置。 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
实例化新项目符号。分配项目符号的startPosition
和targetPosition
。 - 让游戏更具趣味性:运行射击动画,并在怪物射击时播放激光声音。
把所有东西放在一起
是时候将所有内容连接在一起了。确定目标并让你的怪物看着它。
仍然在射击敌人.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