使塔防成为统一游戏-第2部分

图片

这是教程“在Unity中创建塔防游戏”的第二部分。 我们正在Unity中创建塔防游戏,到第一部分结束时,我们学习了如何放置和升级怪物。 我们也有一个敌人在攻击Cookie。

但是,敌人还不知道在哪里看! 此外,仅凭一次攻击就显得很奇怪。 在本部分的教程中,我们将添加一波敌人和手臂怪物,以便它们可以防御珍贵的cookie。

开始工作


在Unity中打开项目,我们在最后一部分中停止了该项目。 如果您刚刚加入我们,请下载项目草案并打开TowerDefense-Part2-Starter

从“ 场景”文件夹中打开GameScene

转敌人


在上一教程的结尾,敌人学会了沿路行驶,但似乎他不知道在哪里看。

在IDE中打开MoveEnemy.cs脚本,并向其中添加以下方法以解决这种情况。

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

RotateIntoMoveDirection旋转敌人,使其始终向前看。 他这样做如下:

  1. 计算错误的当前方向,从下一个点的位置减去当前航路点的位置。
  2. 使用Mathf.Atan2确定将newDirection定向到的弧度角(零点在右侧)。 将结果乘以180 / Mathf.PI ,将角度转换为度。
  3. 最后,它获取Sprite子级并旋转轴rotationAngle度。 请注意,我们旋转子级而不是父级,以便稍后添加的能量条保持水平。

Update() ,将注释// TODO: 替换为// TODO: 在下一个对RotateIntoMoveDirection调用下// TODO: RotateIntoMoveDirection

 RotateIntoMoveDirection(); 

保存文件并返回到Unity。 运行场景; 现在敌人知道他要移到哪里。


现在,该错误知道了它的去向。

唯一的敌人看起来并不令人印象深刻。 我们需要成群结队! 就像任何塔防游戏一样,成群结队!

通知玩家


在开始移动部落之前,我们需要警告玩家即将发生的战斗。 此外,值得在屏幕顶部显示当前波数。

Wave信息是几个GameObjects所必需的,因此我们将其添加到GameManager的GameManagerBehavior组件中。

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

 public Text waveLabel; public GameObject[] nextWaveLabels; 

waveLabel将指向波形编号输出标签的链接存储在屏幕的右上角。 nextWaveLabels存储两个nextWaveLabels创建动画的组合,我们将在新一轮浪潮开始时显示它们:


保存文件并返回到Unity。 在“ 层次结构”中选择GameManager 。 单击“ 波形标签 ”右侧的圆圈,然后在“ 选择文本”对话框中,从“ 场景”选项卡中选择“ WaveLabel

现在将“下一波标签大小 ”设置为2 。 现在,将元素0设置为NextWaveBottomLabel ,对于元素1,将 NextWaveTopLabel设置为与Wave Label相同。


这就是现在的游戏管理员行为

如果玩家输了,那么他应该不会看到有关下一波的消息。 要处理这种情况,请返回GameManagerBehavior.cs并添加另一个变量:

 public bool gameOver = false; 

gameOver我们将存储玩家是否输了的价值。

在这里,我们再次使用属性将游戏元素与当前wave同步。 将以下代码添加到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对您来说应该已经很熟悉了。 但是,有了二传手,一切都会变得更加有趣。

我们给wave指定wavevalue

然后我们检查游戏是否完成。 如果不是,则遍历所有nextWaveLabels标签-这些标签具有Animator组件。 为了启用Animator动画,我们定义nextWave触发器。

最后,我们将waveLabeltext设置为wave + 1 。 为什么+1 ? 普通人不会从头开始计算(是的,这很奇怪)。

Start()设置此属性的值:

 Wave = 0; 

我们从数字0 Wave开始计数。

保存文件并在Unity中运行场景。 Wave标签将正确显示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此波浪中创建的敌人的数量。

该类是Serializable ,也就是说,我们可以在Inspector中更改其值。

将以下变量添加到SpawnEnemy类:

 public Wave[] waves; public int timeBetweenWaves = 5; private GameManagerBehavior gameManager; private float lastSpawnTime; private int enemiesSpawned = 0; 

在这里,我们设置了生成敌人的变量,这与在路线上的点之间移动敌人的方式非常相似。

我们将单个敌人的waves设置为wave,并在敌人enemiesSpawnedlastSpawnTime跟踪创建的敌人的数量以及创建它们的时间。

在所有这些杀死之后,玩家需要时间呼吸,因此将timeBetweenWaves设置为5秒。

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

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

在这里,我们为lastSpawnTime分配当前时间,即场景加载后脚本开始的时间。 然后我们得到了已经熟悉的GameManagerBehavior

将以下代码添加到Update()

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

让我们逐步分析它:

  1. 我们获取当前波的索引,并检查它是否为最后一个。
  2. 如果是这样,那么我们将计算在前一个敌人产生之后经过的时间,并检查是否该创建一个敌人了。 这里我们考虑两种情况。 如果这是波浪中的第一个敌人,则我们检查timeInterval是否timeInterval timeBetweenWaves 。 否则,我们检查timeInterval是否timeInterval spawnInterval wave。 无论如何,我们都要检查是否没有在这一波中制造出所有敌人。
  3. 如有必要,生成敌人,并创建敌人的实例。 还可以增加enemiesSpawned的价值。
  4. 检查屏幕上的敌人数量。 如果他们不在那,而这是浪潮中的最后一个敌人,那么我们创建下一个浪潮。 同样在浪潮结束时,我们给玩家所有剩余金币的10%。
  5. 击败最后一波后,在这里播放游戏胜利的动画。

设置生成间隔


保存文件并返回到Unity。 在“ 层次结构”中选择Road对象。 在检查器中,Waves对象的Size设置为4

现在,为所有四个元素选择一个敌人对象作为Enemy Prefab 。 如下配置Spawn IntervalMax Enemies字段:

  • 元素0 :生成间隔: 2.5 ,最大敌人: 5
  • 元素1 :产生间隔: 2 ,最大敌人: 10
  • 元素2 :生成间隔: 2 ,最大敌人: 15
  • 元素3 :生成时间间隔: 1 ,最大敌人: 5

完成的方案应如下所示:


当然,您可以尝试使用这些值来增加或减少复杂性。

启动游戏。 是的 甲虫已经开始了您的cookie之旅!

虫子

附加任务:添加不同类型的敌人


没有一种塔防游戏可以被认为只有一种类型的敌人就可以完成。 幸运的是, Prefabs文件夹中还包含 Enemy2

检查器中,选择“ Prefabs \ Enemy2”,然后向其添加MoveEnemy脚本。 将Speed设置为3并设置Enemy 标签 。 现在您可以使用这个快速的敌人,让玩家不会放松!

玩家生活更新


即使成群的敌人攻击cookie,玩家也不会受到伤害。 但是很快我们将修复它。 如果玩家允许敌人偷袭,则玩家必须遭受痛苦。

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

 public Text healthLabel; public GameObject[] healthIndicator; 

我们使用healthLabel来获取玩家的生命价值,并使用healthIndicator来获取五个咀嚼饼干的绿色小怪物-它们只是象征玩家的健康; 它比标准的健康指标更有趣。

健康管理


现在,在GameManagerBehavior添加一个用于存储玩家健康状况的GameManagerBehavior

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

这就是我们管理玩家健康的方式。 同样,代码的主要部分位于设置器中:

  1. 如果我们降低了玩家的健康状况,我们会使用CameraShake组件来创造漂亮的震动效果。 该脚本包含在可下载的项目中,在此不再赘述。
  2. 我们更新屏幕左上角的私有变量和运行状况标签。
  3. 如果运行状况下降到0并且游戏尚未结束,则将gameOvertrue并启动gameOver动画。
  4. 我们从Cookie中删除其中一个怪物。 如果仅将其关闭,则可以更轻松地编写此部分,但是在此处,如果添加了运行状况,我们将支持重新包含。

我们在Start()初始化Health

 Health = 5; 

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

完成所有这些操作后,我们现在可以在错误进入Cookie时更新玩家的健康状况。 保存文件,然后转到IDE的MoveEnemy.cs脚本。

健康改变


要更改您的健康状况,请在Update()找到带有// TODO: 字样的// TODO: 并将其替换为以下代码:

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

因此,我们得到GameManagerBehavior并从其Health减去单位。

保存文件并返回到Unity。

层次结构中选择一个GameManager ,然后选择HealthLabel作为其Health Label

层次结构中展开Cookie对象,并将其五个子HealthIndicators拖动到GameManager的Health Indicator数组中-健康指标将是吃饼干的绿色小怪物。

运行场景,直到错误到达cookie。 什么都不做,直到输了。

Cookie攻击

怪物复仇


怪物到位了吗? 是的 敌人会进攻吗? 是的,它们看起来很危险! 现在该回答这些动物了!

为此,我们需要以下内容:

  • 健康通道,让玩家知道哪些敌人强而哪些弱
  • 检测怪物范围内的敌人
  • 做出决定-向哪个敌人射击
  • 一堆贝壳

敌人的健康吧


为了实现健康等级,我们使用两个图像-一个用于深色背景,第二个(绿色条稍小),我们将根据敌人的健康进行缩放。

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

然后在层次结构中,Images \ Objects \ HealthBarBackground拖放Enemy上,以将其作为子级添加。

检查器中,HealthBarBackground位置设置为(0,1,-4)

然后在“ 项目浏览器”中,选择“ 图像\对象\健康栏” ,并确保其“ 透视”为“ 左” 。 然后将其添加为层次结构中敌人的子级,并设置其位置(-0.63,1,-5) 。 对于X 比例,将值设置为125

将一个名为HealthBar的新C#脚本添加到HealthBar游戏对象。 稍后我们将对其进行更改,以使其更改运行状况栏的长度。

在“ 层次结构”中选择一个敌人对象后,请确保其位置为(20,0,0)

单击检查器顶部的“ 应用” ,将所有更改保存为预制件的一部分。 最后,删除Hierarchy中Enemy对象。


现在,重复所有这些步骤,为Prefabs \ Enemy2添加一个健康栏。

更改健康栏的长度


打开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 。 更改其当前运行状况值,并查看其运行状况栏如何变化。


检测范围内的敌人


现在,我们的怪物需要找出要瞄准的敌人。 但是在意识到这个机会之前,您需要准备《怪物与敌人》。

选择“项目浏览器Prefabs \ Monster” ,然后在“ 检查器” 中将Circle Collider 2D”组件添加到其中。

将对撞机的“ 半径”参数设置为2.5-这将指示怪物的攻击半径。

选择“ 是触发器”复选框,以使对象穿过该区域而不是与其碰撞。

最后,在检查器的顶部,将“怪物的图层 ”设置为“ 忽略射线广播” 。 在对话框中,单击“ 是,更改子项” 。 如果未选择“忽略射线广播”,则对撞机将响应鼠标单击事件。 这将是一个问题,因为怪物会阻止发往其下的Openspot对象的事件。


为了确保在触发区域中检测到敌人,我们需要向其添加一个对撞机和刚体,因为Unity仅在将刚体附着到其中一个对撞机时才发送触发事件。

项目浏览器中,选择Prefabs \ Enemy 。 添加Rigidbody 2D组件,然后选择Kinematic作为Body Type 。 这意味着身体将不受物理影响。

添加半径1的 Circle Collider 2D 。 对Prefabs \ Enemy 2重复这些步骤。

触发器已配置,因此怪物将了解敌人在其作用范围内。

我们还需要准备一件事:一个脚本,告诉怪物何时消灭了敌人,以使它们在继续射击时不会引发异常。

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

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

我们给怪物杀人的许可证


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

在IDE中打开ShootEnemies.cs ,并using以下结构将其添加到其中以访问Generics

 using System.Collections.Generic; 

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

 public List<GameObject> enemiesInRange; 

在敌人范围内,我们将存储范围内的所有敌人。

初始化Start()的字段。

 enemiesInRange = new List<GameObject>(); 

一开始,行动范围内没有敌人,因此我们创建了一个空列表。

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

 // 1 void OnEnemyDestroy(GameObject enemy) { enemiesInRange.Remove (enemy); } void OnTriggerEnter2D (Collider2D other) { // 2 if (other.gameObject.tag.Equals("Enemy")) { enemiesInRange.Add(other.gameObject); EnemyDestructionDelegate del = other.gameObject.GetComponent<EnemyDestructionDelegate>(); del.enemyDelegate += OnEnemyDestroy; } } // 3 void OnTriggerExit2D (Collider2D other) { if (other.gameObject.tag.Equals("Enemy")) { enemiesInRange.Remove(other.gameObject); EnemyDestructionDelegate del = other.gameObject.GetComponent<EnemyDestructionDelegate>(); del.enemyDelegate -= OnEnemyDestroy; } } 

  1. OnEnemyDestroy我们将敌人从敌人enemiesInRange 。 当敌人OnTriggerEnter2D在怪物周围的扳机上时,将OnTriggerEnter2D
  2. 然后,将敌人添加到敌人列表中,并添加EnemyDestructionDelegate事件OnEnemyDestroy 。 因此,我们保证在敌人被摧毁时将OnEnemyDestroy 。 我们不希望怪物为死去的敌人花费弹药,对吗?
  3. OnTriggerExit2D我们从列表中删除敌人,并取消注册OnTriggerExit2D 。 现在我们知道哪些敌人在射程之内。

保存文件并在Unity中启动游戏。 为了确保一切正常,请enemiesInRange怪物,将其选中,然后按照“ enemiesInRange ”中enemiesInRange列表中的更改进行enemiesInRange

目标选择


怪物现在知道哪个敌人在射程内。 但是,如果方圆内有几个敌人,他们会怎么做?

当然,它们会攻击最靠近肝脏的那一个!

打开MoveEnemy.cs IDE脚本,并添加一个计算该怪物的新方法:

 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清楚看出damagedamage

targetstartPositiontargetPosition确定弹丸的方向。

distancestartTime跟踪弹丸的当前位置。 当玩家杀死敌人时, gameManager奖励玩家。

Start()分配这些变量的值:

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

startTime我们设置当前时间的值并计算起始位置和目标位置之间的距离。而且,照常获得GameManagerBehavior

要控制弹丸的​​运动,请添加Update()以下代码:

 // 1 float timeInterval = Time.time - startTime; gameObject.transform.position = Vector3.Lerp(startPosition, targetPosition, timeInterval * speed / distance); // 2 if (gameObject.transform.position.Equals(targetPosition)) { if (target != null) { // 3 Transform healthBarTransform = target.transform.Find("HealthBar"); HealthBar healthBar = healthBarTransform.gameObject.GetComponent<HealthBar>(); healthBar.currentHealth -= Mathf.Max(damage, 0); // 4 if (healthBar.currentHealth <= 0) { Destroy(target); AudioSource audioSource = target.GetComponent<AudioSource>(); AudioSource.PlayClipAtPoint(audioSource.clip, transform.position); gameManager.Gold += 50; } } Destroy(gameObject); } 

  1. 我们使用Vector3.Lerp起点和终点之间的插值来计算弹丸的新位置。
  2. 如果弹丸到达targetPosition,则我们检查弹丸是否仍然存在target
  3. 我们获得了HealthBar目标的组成部分,并通过damage射弹的大小降低了它的生命值
  4. 如果敌人的生命值降低到零,我们将摧毁它,重现声音效果,并奖励玩家准确性。

保存文件并返回到Unity。

我们做大贝壳


如果怪物开始以高水平射击更多的炮弹,那不是很好吗?幸运的是,这很容易实现。

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

Bullet1预制件复制两次。命名Bullet2Bullet3的副本

选择Bullet2。在检查器中,将“ Sprite Renderer”组件的“ Sprite”字段设置为“ Images / Objects / Bullet2”因此,我们将使Bullet2比Bullet1多一点。

重复此过程,将Bullet3 预制的精灵更改Images / Objects / Bullet3

在“ 子弹行为”中,我们将进一步调整由炮弹造成的伤害。

在“ 项目”选项卡中选择预制的Bullet1检查你看到子弹行为(脚本),它可以将伤害10Bullet115文档bullet220Bullet3 -或您喜欢的任何其他值。

注意:我更改了值,以便在较高级别下损坏的价格会更高。这样可以防止升级程序允许玩家在最佳点升级怪物。


预制壳-尺寸随级别增加

改变炮弹水平


将不同的外壳分配给不同级别的怪物,以便更强的怪物更快地消灭敌人。

在IDE中打开MonsterData.cs并添加到MonsterLevel以下变量:

 public GameObject bullet; public float fireRate; 

因此,我们为每个级别的怪物设置了射弹的预制件和射击频率。保存文件并返回Unity以完成怪物设置。

在项目浏览器中选择Monster预制件检查器中,展开怪物数据(脚本)”组件中的关卡每个项目射击率设置1然后将元素0、1和2 Bullet参数设置Bullet1Bullet2Bullet3怪物等级应设置如下:




炮弹杀死敌人?是的 让我们开火吧!

开火


在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; // 1 Vector3 startPosition = gameObject.transform.position; Vector3 targetPosition = target.transform.position; startPosition.z = bulletPrefab.transform.position.z; targetPosition.z = bulletPrefab.transform.position.z; // 2 GameObject newBullet = (GameObject)Instantiate (bulletPrefab); newBullet.transform.position = startPosition; BulletBehavior bulletComp = newBullet.GetComponent<BulletBehavior>(); bulletComp.target = target.gameObject; bulletComp.startPosition = startPosition; bulletComp.targetPosition = targetPosition; // 3 Animator animator = monsterData.CurrentLevel.visualization.GetComponent<Animator>(); animator.SetTrigger("fireShot"); AudioSource audioSource = gameObject.GetComponent<AudioSource>(); audioSource.PlayOneShot(audioSource.clip); } 

  1. 我们得到了子弹的起始位置和目标位置。将位置z设置为z bulletPrefab以前,我们将弹丸的预制位置设置为z,以使弹丸显示在射击怪物下方,但在敌人上方。
  2. 我们使用bulletPrefab适当的shell创建一个新shell的实例MonsterLevel分配startPositiontargetPosition投射。
  3. 我们使游戏更有趣:当怪物射击时,开始射击动画并播放激光的声音。

全部放在一起


现在是时候将所有内容放在一起了。定义目标并使怪物看着它。


ShootEnemies.cs脚本中添加Update()以下代码:

 GameObject target = null; // 1 float minimalEnemyDistance = float.MaxValue; foreach (GameObject enemy in enemiesInRange) { float distanceToGoal = enemy.GetComponent<MoveEnemy>().DistanceToGoal(); if (distanceToGoal < minimalEnemyDistance) { target = enemy; minimalEnemyDistance = distanceToGoal; } } // 2 if (target != null) { if (Time.time - lastShotTime > monsterData.CurrentLevel.fireRate) { Shoot(target.GetComponent<Collider2D>()); lastShotTime = Time.time; } // 3 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)); } 

逐步考虑此代码。

  1. 确定怪物的目的。我们从中的最大可能距离开始minimalEnemyDistance如果敌人到Cookie的距离小于当前的最小距离,我们将在一个范围内的所有敌人周围循环,并使其成为新的目标。
  2. 我们调用Shoot经过时间是否大于怪物的射击频率,并设置lastShotTime当前时间值。
  3. 我们计算怪物与其目标之间的旋转角度。我们将怪物旋转到这个角度。现在,他将始终盯着目标。

保存文件并在Unity中启动游戏。怪物将开​​始拼命保护cookie。我们终于完成了!

接下来要去哪里


可以从此处下载完成的项目

我们在本教程中做得很好,现在我们有了出色的游戏。

以下是一些对该项目进行进一步开发的想法:

  • 更多类型的敌人和怪物
  • 敌人的不同路线
  • 不同的游戏等级

这些方面中的每一个都将需要最小的更改,并且可以使游戏更加有趣。如果您根据本教程创建了一个新游戏,我将很乐于玩该游戏,因此请共享一个链接。

在这次采访中可以找到有关创建塔防热门游戏的有趣想法

Source: https://habr.com/ru/post/zh-CN413915/


All Articles