原文地址:https://www.raywenderlich.com/107529/unity-tower-defense-tutorial-part-2算法
欢迎你们来查看,使用Unity建立塔防游戏(第二篇)。在第一篇的结尾,咱们已经能够召唤和升级小怪兽,召唤一个敌人朝着饼干前进的敌人。数组
可是这个敌人没有方向感,让人感受怪怪的。接下来,咱们要作的是召唤一波一波的敌人,而后令小怪兽可以消灭它们,都是为了保护你那块美味的饼干。ide
用Unity打开你以前完成的工程,但若是你没看过Part1,先下载starter project ,而后打开TowerDefense-Part2-Starter这个工程。打开Scenes文件夹下的GameScene。函数
在Part1的结尾,咱们能够令敌人沿着路线前进,但它们毫无方向感。学习
用VS打开脚本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)gameObject.transform.FindChild("Sprite").gameObject; sprite.transform.rotation = Quaternion.AngleAxis(rotationAngle, Vector3.forward); }
RotateIntoMoveDirection 这个方法是将场景中敌人对象的角度进行旋转,让敌人看起来有方向感。咱们一步一步地来看:spa
将Update() 中的注释 // TODO: Rotate into move direction
替换成调用咱们刚写好的函数—— RotateIntoMoveDirection3d
RotateIntoMoveDirection();
保存好脚本,返回Unity,运行游戏,看敌人如今有方向感了。这样才算是朝着饼干前进。code
才一个小兵?这怎行,要来就来一大群。在通常的塔防游戏中,都是每一波敌人都是一大群。orm
在一大群敌人出现以前,咱们应该先告知玩家——敌人来了。同时,咱们须要显示这是第几波敌人,在界面的右上角显示。
在脚本中,有很多须要用到波数的地方,咱们先在GameManager的脚本组件GameManagerBehavior中添加有关波数的代码。
用VS打开GameManagerBehavior.cs,而后添加下面两个变量:
public Text waveLable; public GameObject[] nextWaveLabels;
显示在屏幕右上角的波数会存储在waveLabel 这个变量中。 nextWaveLabels 这个数组保存了两个游戏对象。在一波新的敌人到来以前,它们会构成一个文字合并的动画,以下图所示:
保存好脚本,返回Unity。选中Hierarchy视图中的GameManager,在Inspector面板中,点击Wave Label右侧的小圆圈,而后从弹出的Text对话框中的Scene标签页下选择 WaveLabel 。
将NextWave Labels 的Size 设置为2。就像刚才设置WaveLabel那样,将Element0设置为NextWaveBottomLabel ,将Element1设置为NextWaveTopLabel。
这是设置好数据的结果。
当玩家输掉游戏的时候,它没法看到有关下一波敌人的信息。回到GameManagerBehavior.cs中,添加一个变量:
public bool gameOver = false;
gameOver这个变量表示玩家是否输掉了游戏。
一样的,咱们也要为wave这个私有变量添加一个属性,让wave中的值与游戏当前波数保持一致,再向GameManagerBehavior.cs添加如下代码:
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"); } } waveLable.text = "WAVE: " + (wave + 1); } }
在上面的代码中,咱们建立了一个私有变量,一个属性。这个属性的getter方法,咱们已经习觉得常了,但它的setter方法看起来有些棘手。
先是更新了wave的值。接下来,判断游戏是否未结束,若是是的话,遍历nextWaveLabels中元素,这些元素都带有一个Animator组件。调用SetTrigger来触发动画。
最后,咱们设置waveLabel上的数值为 wave + 1。为何呢?由于在程序中,变量的初始值能够是0,可是人们都是从1开始数数的。
在Start()方法中设置这个属性的值:
Wave = 0;
将Wave的初始值设置为1。
保存好脚本,返回Unity中,运行游戏。波数的确是从1开始的。
对于玩家而言,首先要解决的是第一波敌人。
显然,咱们如今要作的是建立一支敌军(由想吃掉你饼干的小虫子组成),但咱们暂时没法作到。
此外,当玩家刚消灭一波敌人的时候,先不要建立下一波敌人,至少如今是这样。
因而,咱们必需要知道游戏场景中是否还有敌人存在,咱们为敌人对象添加Tags(标签)来区别于其余游戏对象。此外,在脚本中,能够经过标签名快速查找物体。
在Project视图中,选中名为Enemy的prefab。在Inspector面板的顶部,点击Tag右边的下拉框,从弹出的对话框中选择Add Tag。
新建一个标签,命名为Enemy。
选中名为Enemy的prefab,在Inspector中将它的标签设置为咱们刚才建立的标签——Enemy。
如今,咱们须要定义有关敌军的类和变量。用VS打开SpawnEnemy.cs,在SpawnEnemy的上方添加一个新的类,以下面代码所示:
[System.Serializable] public class Wave { public GameObject enemyPrefab; public float spawnInterval = 2; public int maxEnemies = 20; }
Wave这个类表示一支敌军,它有3个字段,enemyPrefab用于实例化敌人对象;每隔spawnInterval秒产生一个敌人,每波建立单个敌人的时间间隔多是不一样的;一波敌人的最大数量为maxEnemies。
这个类是序列化的,因此咱们能够在Inspector面板中更改它的数据。
接下来为SpawnEnemy这个类添加下列变量:
public Wave[] waves; public int timeBetweenWaves = 5; private GameManagerBehavior gameManager; private float lastSpawnTime; private int enemiesSpawned = 0;
这几个变量都是与建立敌人有关的。咱们将各个级别的敌军存储在waves这个数组里;enemiesSpawned记录了已产生的敌人的数量;lastSpawnTime记录了仍是上一个敌人产生的时间;
玩家须要一些时间来消灭这些敌人,因而咱们将timeBetweenWaves设置为5秒,即每隔5秒产生一波敌人。
将Start()方法中的代码替换为如下代码:
lastSpawnTime = Time.time; gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
咱们将lastSpawnTime设置为当前时间,当场景加载完成后,Start()方法就会被执行。而后,咱们获取了游戏对象GameManager的引用。
向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); 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); }
让咱们一步一步来理解这段代码:
保存好脚本,返回Unity,选中Hierarchy视图中的Road对象,在Inspector面板中,将数组Waves的Size设置为4。
接下来,依次为数组的4个元素赋值。将名为Enemy的prefab赋值给Enemy Prefab,分别设置Spawn Interval和Max Enemies的值以下:
最终设置好的结果以下如图所示:
咱们能够经过上面的设置达到平衡游戏的目的。运行游戏,哈哈!那些小虫子正朝着你的饼干前进!
塔防游戏里的敌人通常都不止一种。在咱们工程的Prefab文件夹中还包含着另外一种敌人的prefab,Enemy2。
选中Prefab文件夹中的Enemy2,在Inspector面板中,为它添加一个脚本组件,咱们选择已有的MoveEnemy这个脚本。将Speed的值设置为3,将它的标签设置为Enemy。咱们用这种快速前进的小虫子,让玩家保持警觉。
如今,即便一大群小虫子抵达了你那美味的饼干,你的血量都丝毫未损。因而,当有小虫子碰了你那块饼干的时候,你就要受伤了。
打开GameManagerBehavior.cs,添加下面两个变量。
public Text healthLabel; public GameObject[] healthIndicator;
咱们用healthLabel来显示玩家当前的血量,healthIndicator用于表示5只正在啃你饼干的小虫子,比起一个简单的数字或血条,用它们来表示玩家的血量会更有趣一些。
接下来,为 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); } } } }
以上代码块用于管理玩家的血量,一样的,setter方法是这段代码的主体。
gameOver
的值为true,再触发游戏失败的动画。在Start()中初始化Health:
Health = 5;
在游戏开始的时候,玩家的血量为5。
有了这个属性,当小虫子抵达饼干的时候,咱们就能够更新玩家的血量了。保存好脚本,在VS中打开MoveEnemy.cs这个脚本。
将MoveEnemy.cs中Update()方法内部的注释:// TODO: deduct health ,替换成如下代码:
GameManagerBehavior gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>(); gameManager.Health -= 1;
这段代码是为了获取GameManagerBehavior对象,而后将Health的值减1。
保存好脚本,返回Unity。
选中Hierarchy视图中的GameManager对象,为Health Label 赋值,选择HealthLabel。
在Hierarchy视图中展开Cookie对象,注意不要选中它,咱们只要让它下面的5个子对象显示出来便可。将这5个子对象拖拽赋值给GameManager的Health Indicator数组。咱们用5只正在开心地啃着饼干的青色小虫子来表示玩家的血量。玩家受到一次伤害,就减小一只青色的小虫子。
运行游戏,让那些小虫子冲向饼干,什么都别作,直到游戏结束。
该召唤小怪兽?仍是让小虫子前进?如今咱们的小怪兽仍是纸老虎,咱们要作的是让小怪兽们可以消灭那些小虫子。
咱们先要把如下几件事情作好:
咱们用两张图片来显示血条,一张是暗的,用于显示血条的背景,另外一张是绿色较小的细长图片,咱们经过缩放它的长度来与敌人当前血量匹配。
将Project视图中的Prefabs\Enemy拖到场景中。
将Images\Objects\HealthBarBackground拖拽到Hierarchy视图中的Emeny对象上,令HealthBarBackground做为Enemy的子对象。
在Inspector面板中,将HealthBarBackground的Position设置为 (0, 1, -4) 。
接下来选中Project视图中的Images\Objects\HealthBar,确保它的Pivot被设置为Left。一样的,也将它做为Hierarchy视图中的Emeny对象的子对象,将它的Position设置为 (-0.63, 1, -5),将它的X Scale设置为125 。
为游戏对象HealthBar添加一个C#脚本,命名为HealthBar,后面咱们须要在脚本中调整血条长度。
如今咱们将Hierarchy视图中的Emeny对象的坐标调整为(20, 0, 0) 。
点击Inspector面板顶部的Apply按钮,保存刚才对prefab的更改。回到Project视图,刚才咱们所做的更改已经成为了Prefab的一部分。最后,删除Hierarchy视图中的Emeny对象。
同上,咱们也为Prefab\Enemy2添加一个血条。
在VS中打开HealthBar.cs,添加下列变量:
public float maxHealth = 100; public float currentHealth = 100; private float originalScale;
maxHealth表示敌人的最大生命值,currentHealth则表示敌人的当前的生命值,originalScale记录的是血条图片的初始长度。
在Start()方法中,为originalScale赋值:
originalScale = gameObject.transform.localScale.x;
这里,咱们获取了HealthBar这个游戏对象的X Scale。
在Update()方法中,咱们经过缩放HealthBar的图片长度,令它与敌人的当前生命值匹配:
Vector3 tmpScale = gameObject.transform.localScale; tmpScale.x = currentHealth / maxHealth * originalScale; gameObject.transform.localScale = tmpScale;
以上代码可以简写为下面的代码么?
gameObject.transform.localScale.x = currentHealth / maxHealth * originalScale;
不行的,单独为localScale.x赋值的时候,编译器报错了。
因而,咱们只可以先用一个临时变量tmpScale获取localScale的值,而后为tmpScale.X赋值,最后将tmpScale赋值localScale。
保存好脚本,启动游戏。如今咱们能够看到每一个敌人都有了本身的血条。
选中一个敌人对象Enemy(Clone),在Hierarchy视图将它展开,选中它的子对象HealthBar。在Inspector面板中调整Current Health这个变量的值,咱们能够看到敌人的血条的长度随着Current Health的值变化。
如今,小怪兽们须要知道它们的攻击目标在哪里。在咱们作这件事以前,咱们要先为小怪兽和敌人作一点准备工做。
选中Project面板中的Prefab\Monster,在Inspector面板中为它添加一个Circle Collider 2D组件,这是一个2D圆形碰撞体组件。
将该圆形碰撞体的半径设置为2.5——这是小怪兽的射程。
启用Is Trigger这个属性,目的是令此碰撞体用于触发事件,而且不会发生任何物理交互。若是不启用这个属性的话,就是会发生碰撞。
最后,在Inspector面板的顶部,将Monster的Layer属性设置为Ignore Raycast。在弹出的对话框中选择Yes,change children。若是你不这样设置的话,碰撞体会响应鼠标点击事件,这是咱们不须要的。小怪兽位于召唤点Openspot的上方,这个碰撞体又是小怪兽的组件,因而鼠标点击事件就会被碰撞体优先响应,而不是被Openspot响应。这样的结果是什么?上一篇文章中,Openspot经过响应鼠标点击事件,能够放置或升级小怪兽;想一想看,放置小怪兽后不能对它升级,这是否是违背了以前的设定?
为了令小怪兽的碰撞体可以检测到在它范围内的敌人,咱们须要为敌人对象添加一个碰撞体和刚体。在两个碰撞体发生碰撞的时候,假如其中一个有附加刚体组件,那么就会触发碰撞事件。
在Project面板中,选中Prefab\Enemy,为它添加Rigid Body 2D组件,勾选Is Kinematic属性。这是为了令敌人对象不受Unity中的物理引擎影响。
再添加一个Circle Collider 2D,半径设置为1。对Prefab\Enemy2重复以上步骤。
如今全部的设置都已完成,你的小怪兽们能够侦测到射程内的敌人。
还有一件事情要作:在脚本中告知小怪兽敌人是否被消灭,当它们的射程内没有敌人的时候,不必一直开火。
为Enemy和Enemy2这两个prefab添加一个新的脚本组件,命名为EnemyDestructionDelegate。
在VS中打开这个脚本,为它添加一个委托的声明:
public delegate void EnemyDelegate(GameObject enemy); public EnemyDelegate enemyDelegate;
这里咱们建立了一个委托,它包含了一个方法的声明,能够像变量同样传递。
提示: 当咱们须要让一个游戏对象灵活地通知另外一个游戏对象作出改变,请使用委托吧。关于委托的更多知识点,你能够从这里学习到—— the Unity documentation。
再添加下面的方法:
void OnDestroy() { if (enemyDelegate != null) { enemyDelegate(gameObject); } }
以上代码的目的是为了销毁一个游戏对象,如同Start()和Update()方法同样,Unity会自动调用OnDestroy()这个方法。在这个方法中,咱们先判断委托变量的值是否不为null。若是是这样的话,咱们调用这个委托,将gameObject做为它的参数。全部注册过这个委托的游戏对象都会得知敌人对象被销毁了。
保存好脚本,返回Unity。
如今,小怪兽们能侦测到攻击范围内的敌人。为Monster prefab添加一个C#脚本组件,命名为ShootEnemies。
在VS中打开它,添加下面的代码,目的是引用命名空间Generics。
using System.Collections.Generic;
添加一个集合变量,用于追中全部攻击范围内的敌人:
public List<GameObject> enemiesInRanges;
这个集合里面存储了攻击范围内全部的敌人对象。
在Start()方法里对这个集合进行初始化。
enemiesInRanges = new List<GameObject>();
起先,小怪兽的射程内木有敌人,因而咱们就建立了一个空的List。
接下来是向这个List中添加元素,在脚本中添加下面的代码段:
// 1 void OnEnemyDestroy(GameObject enemy) { enemiesInRanges.Remove(enemy); } void OnTriggerEnter2D(Collider2D other) { // 2 if (other.gameObject.tag.Equals("Enemy")){ enemiesInRanges.Add(other.gameObject); EnemyDestructionDelegate del = other.gameObject.GetComponent<EnemyDestructionDelegate>(); del.enemyDelegate += OnEnemyDestroy; } } // 3 void OnTriggerExit2D(Collider2D other) { if (other.gameObject.tag.Equals("Enemy")){ enemiesInRanges.Remove(other.gameObject); EnemyDestructionDelegate del = other.gameObject.GetComponent<EnemyDestructionDelegate>(); del.enemyDelegate -= OnEnemyDestroy; } }
这段代码分为3个小方法:
1. 在OnEnemyDestroy()方法中,咱们移除了enemiesInRange中的enemy对象。当有敌人通过小怪兽的射程时,方法OnTriggerEnter2D()就会被调用。
2. 将敌人对象添加到enemiesInRange当中,而且将方法OnEnemyDestroy()添加到委托EnemyDestructionDelegate上。这是为了确保当敌人对象被销毁的时候,方法OnEnemyDestroy()会被调用。你的小怪兽们不须要为已死的敌人浪费火力。
3. 在OnTriggerExit2D()方法中,咱们将敌人对象enemy从当中enemiesInRange移除,而且移除以前添加到委托上方法。如今小怪兽们能够知道它射程内的敌人是哪些了。
保存好脚本,启动游戏,看看咱们以前作的行不行。召唤一只小怪兽,选中它,而后在Inspector面板中查看enemiesInRange这个变量的变化。
就像数绵羊那样。围栏(Fence )和绵羊(sheep)都由OpenClipArt提供。
如今小怪兽们能够侦测到它射程以内的敌人,但问题是当有多个敌人存在它射程以内的时候,该怎么办?
固然是对离饼干最近的敌人开火啦!
在VS中打开MoveEnemy.cs,添加一个新的方法来完成这个任务:
public float distanceToGoal() { float distance = 0; distance += Vector3.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 += Vector3.Distance(startPosition, endPosition); } return distance; }
这个方法计算出了敌人还没有走完的路有多长。咱们使用了Distatnce这个方法来计算两个Vector3之间的距离。
·经过这个方法来决定小怪兽的攻击目标。可是,如今你的小怪兽们没法攻击敌人,什么事都作不了,这个问题在下一步中解决。
保存好脚本,返回Unity中,咱们须要为小怪兽们配备射击敌人的子弹。
将 Images/Objects/Bullet1 拖拽到场景视图中。将它的Z坐标设置为-2,在游戏过程当中,咱们须要不断地产生新的子弹,X和Y坐标是在子弹产生时候设置的。
为Bullet1添加一个名为 BulletBehavior 的C#脚本组件,将下面的变量添加到脚本中:
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 = Vector3.Distance(startPosition, targetPosition); GameObject gm = GameObject.Find("GameManager"); gameManager = gm.GetComponent<GameManagerBehavior>();
咱们将 startTime 设置为当前时间;distance变量的值为 startPosition 和 targetPosition 之间的距离;最后,咱们获取了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.FindChild("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); }
Vector3.Lerp
这个方法。HealthBar
组件,按子弹形成的伤害来削减目标的生命值。保存好脚本,返回Unity中。
假如等级高的小怪兽能发射较大的子弹,这是否是很酷呢?是的,咱们能作到,由于这很简单。
将 Hierarchy 视图中的 Bullet1 拖拽到Project 视图中的Prefab文件夹下,创造出一个子弹的prefab。删除场景中的子弹对象,咱们已经再也不须要它。
利用 Bullet1 prefab再建立两个prefab,分别命名为 Bullet2 和 Bullet3 。传统的CTRL + C,CTRL + V命令在这里行不通。选中Bullet1后,按下快捷键CTRL + D,(duplicate 复制的意思),按下CTRL + D 两次后,建立 Bullet2 和 Bullet3。由于Bullet2 和 Bullet3都是比较大的子弹,接下来,咱们要为这两个prefab设置新的子弹图片。
选中Bullet2 ,在Inspector面板中,设置 Sprite Renderer 组件的Sprite为 Images/Objects/Bullet2。这样,Bullet2的样子会比Bullet1更大一些。
同上,将Bullet3 prefab的sprite设置为 Images/Objects/Bullet3。
以前在编写Bullet Behavior脚本的时候,没有进行设置 Damage 这个变量的值,接下来,分别设置这三种子弹形成的伤害值。
在Inspector面板中,对Bullet1 、Bullet2 、Bullet3 的Damage进行赋值,分别为十、1五、20,或者随你的便。
注意:级别越高的子弹形成的伤害越大。玩家须要将金币花在刀刃上,优先升级那些位置好的小怪兽们。
子弹的大小与小怪兽的等级成正比。
为不一样等级的小怪兽分配威力不一样的子弹,这样小怪兽越强,就能越快地消灭敌人。
打开脚本 MonsterData.cs ,为 MonsterLevel 添加下面的变量:
public GameObject bullet; public float fireRate;
前者是指子弹的 prefab,后者是指小怪兽发射子弹的速率。保存好脚本,返回Unity,让咱们完成对小怪兽的配置。
在Project视图中选中Monster prefab。在Inspector面板中,展开Monster Data脚本组件中的Levels数组,将全部元素的Fire Rate都设置为1,分别设置Elements0、Elements一、Elements2的Bullet为Bullet1、Bullet2、Bullet3。
配置好后的结果以下图所示:
打开脚本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); }
bulletPrefab的Z坐标
。以前咱们设置bullet prefab的Z坐标的缘由是为了表现一种层次感,子弹所处的位置要比小怪兽和敌人更低。方法开头从MonsterData中获取了bulletPrefab,再
以bulletPrefab建立出一个子弹对象。
将startPosition
和 targetPosition
赋值给咱们建立出来的子弹对象。如今是时候该整合一切了,让你的小怪兽可以准确地朝着目标开火。
往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)); }
让咱们一步一步地来看这些代码:
minimalEnemyDistance设置为float.MaxValue,这样就不会有比它更大的数出现了。遍历集合中的全部敌人,当循环结束的时候,咱们就能够找出距离饼干最近的敌人。
Shoot方法,
再将lastShotTime
设置为当前时间。保存好脚本,启动游戏。看你的小怪兽们正在奋力地保护你的饼干。好样的,如今咱们完成了整个工程。
从这里能够下载完整的项目。
如今咱们这个教程就要结束了,咱们完成了一个很棒的塔防游戏。
这个游戏咱们还能够作出如下扩展:
1. 添加更多种类的敌人和小怪兽
2. 为敌人创建更多的通往饼干的道路
3. 为小怪兽们设置更多的级别
这些小小的扩展能够令咱们的游戏更好玩。假如你以此教程为基础创造出了属于本身的新游戏,请在评论中分享你的连接,让你们都可以好好地体验一回。
在这里你能够发现更多有趣的关于塔防游戏的想法。
感谢你们抽出时间来完成这篇教程。但愿你们可以提出更多好的想法,祝你们都可以愉快地杀敌。