这是教程
“在Unity中创建塔防游戏”的第二部分。 我们正在Unity中创建塔防游戏,到
第一部分结束时,我们学习了如何放置和升级怪物。 我们也有一个敌人在攻击Cookie。
但是,敌人还不知道在哪里看! 此外,仅凭一次攻击就显得很奇怪。 在本部分的教程中,我们将添加一波敌人和手臂怪物,以便它们可以防御珍贵的cookie。
开始工作
在Unity中打开项目,我们在最后一部分中停止了该项目。 如果您刚刚加入我们,请下载
项目草案并打开
TowerDefense-Part2-Starter 。
从“
场景”文件夹中打开
GameScene 。
转敌人
在上一教程的结尾,敌人学会了沿路行驶,但似乎他不知道在哪里看。
在IDE中打开
MoveEnemy.cs脚本,并向其中添加以下方法以解决这种情况。
private void RotateIntoMoveDirection() {
RotateIntoMoveDirection
旋转敌人,使其始终向前看。 他这样做如下:
- 计算错误的当前方向,从下一个点的位置减去当前航路点的位置。
- 使用
Mathf.Atan2
确定将newDirection
定向到的弧度角(零点在右侧)。 将结果乘以180 / Mathf.PI
,将角度转换为度。 - 最后,它获取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
指定
wave
新
value
。
然后我们检查游戏是否完成。 如果不是,则遍历所有
nextWaveLabels标签-这些标签具有
Animator组件。 为了启用
Animator动画,我们
定义了
nextWave触发器。
最后,我们将
waveLabel
的
text
设置为
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,并在敌人
enemiesSpawned
和
lastSpawnTime
跟踪创建的敌人的数量以及创建它们的时间。
在所有这些杀死之后,玩家需要时间呼吸,因此将
timeBetweenWaves
设置为5秒。
用以下代码替换
Start()
内容。
lastSpawnTime = Time.time; gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
在这里,我们为
lastSpawnTime
分配当前时间,即场景加载后脚本开始的时间。 然后我们得到了已经熟悉的
GameManagerBehavior
。
将以下代码添加到
Update()
:
让我们逐步分析它:
- 我们获取当前波的索引,并检查它是否为最后一个。
- 如果是这样,那么我们将计算在前一个敌人产生之后经过的时间,并检查是否该创建一个敌人了。 这里我们考虑两种情况。 如果这是波浪中的第一个敌人,则我们检查
timeInterval
是否timeInterval
timeBetweenWaves
。 否则,我们检查timeInterval
是否timeInterval
spawnInterval
wave。 无论如何,我们都要检查是否没有在这一波中制造出所有敌人。 - 如有必要,生成敌人,并创建敌人的实例。 还可以增加
enemiesSpawned
的价值。 - 检查屏幕上的敌人数量。 如果他们不在那,而这是浪潮中的最后一个敌人,那么我们创建下一个浪潮。 同样在浪潮结束时,我们给玩家所有剩余金币的10%。
- 击败最后一波后,在这里播放游戏胜利的动画。
设置生成间隔
保存文件并返回到Unity。 在“
层次结构”中选择
Road对象。 在
检查器中,将
Waves对象的
Size设置为
4 。
现在,为所有四个元素选择一个
敌人对象作为
Enemy Prefab 。 如下配置
Spawn Interval和
Max 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 {
这就是我们管理玩家健康的方式。 同样,代码的主要部分位于设置器中:
- 如果我们降低了玩家的健康状况,我们会使用
CameraShake
组件来创造漂亮的震动效果。 该脚本包含在可下载的项目中,在此不再赘述。 - 我们更新屏幕左上角的私有变量和运行状况标签。
- 如果运行状况下降到0并且游戏尚未结束,则将
gameOver
为true
并启动gameOver
动画。 - 我们从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。 什么都不做,直到输了。
怪物复仇
怪物到位了吗? 是的 敌人会进攻吗? 是的,它们看起来很危险! 现在该回答这些动物了!
为此,我们需要以下内容:
- 健康通道,让玩家知道哪些敌人强而哪些弱
- 检测怪物范围内的敌人
- 做出决定-向哪个敌人射击
- 一堆贝壳
敌人的健康吧
为了实现健康等级,我们使用两个图像-一个用于深色背景,第二个(绿色条稍小),我们将根据敌人的健康进行缩放。
从
项目浏览器 拖到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#脚本,并将其添加到
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。
我们给怪物杀人的许可证
现在,怪物可以在其行动范围内检测敌人。 将新的
C#脚本添加到
Monster预制中,并将其命名为
ShootEnemies 。
在IDE中打开
ShootEnemies.cs ,并
using
以下结构将其添加到其中以访问
Generics
。
using System.Collections.Generic;
添加一个变量以跟踪范围内的所有敌人:
public List<GameObject> enemiesInRange;
在敌人范围内,我们将存储范围内的所有敌人。
初始化
Start()
的字段。
enemiesInRange = new List<GameObject>();
一开始,行动范围内没有敌人,因此我们创建了一个空列表。
填写
enemiesInRange
列表! 将以下代码添加到脚本中:
- 在
OnEnemyDestroy
我们将敌人从敌人enemiesInRange
。 当敌人OnTriggerEnter2D
在怪物周围的扳机上时,将OnTriggerEnter2D
。 - 然后,将敌人添加到敌人列表中,并添加
EnemyDestructionDelegate
事件OnEnemyDestroy
。 因此,我们保证在敌人被摧毁时将OnEnemyDestroy
。 我们不希望怪物为死去的敌人花费弹药,对吗? - 在
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
清楚看出
damage
的
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()
以下代码:
- 我们使用
Vector3.Lerp
起点和终点之间的插值来计算弹丸的新位置。 - 如果弹丸到达
targetPosition
,则我们检查弹丸是否仍然存在target
。 - 我们获得了
HealthBar
目标的组成部分,并通过damage
射弹的大小降低了它的生命值。 - 如果敌人的生命值降低到零,我们将摧毁它,重现声音效果,并奖励玩家准确性。
保存文件并返回到Unity。我们做大贝壳
如果怪物开始以高水平射击更多的炮弹,那不是很好吗?幸运的是,这很容易实现。将Bullet1游戏对象从“ 层次结构 ” 拖到 “ 项目”选项卡上,以创建射弹预制件。从场景中删除原始对象-我们将不再需要它。将Bullet1预制件复制两次。命名Bullet2和Bullet3的副本。选择Bullet2。在检查器中,将“ Sprite Renderer”组件的“ Sprite”字段设置为“ Images / Objects / Bullet2”。因此,我们将使Bullet2比Bullet1多一点。重复此过程,将Bullet3 预制的精灵更改为Images / Objects / Bullet3。在“ 子弹行为”中,我们将进一步调整由炮弹造成的伤害。在“ 项目”选项卡中选择预制的Bullet1。在检查你看到子弹行为(脚本),它可以将伤害值10为Bullet1,15对文档bullet2和20的Bullet3 -或您喜欢的任何其他值。注意:我更改了值,以便在较高级别下损坏的价格会更高。这样可以防止升级程序允许玩家在最佳点升级怪物。
预制壳-尺寸随级别增加改变炮弹水平
将不同的外壳分配给不同级别的怪物,以便更强的怪物更快地消灭敌人。在IDE中打开MonsterData.cs并添加到MonsterLevel
以下变量: public GameObject bullet; public float fireRate;
因此,我们为每个级别的怪物设置了射弹的预制件和射击频率。保存文件并返回Unity以完成怪物设置。在项目浏览器中选择Monster预制件。在检查器中,展开“ 怪物数据(脚本)”组件中的“ 关卡 ” 。将每个项目的射击率设置为1。然后将元素0、1和2 的Bullet参数设置为Bullet1,Bullet2和Bullet3。怪物等级应设置如下:炮弹杀死敌人?是的 让我们开火吧!开火
在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;
- 我们得到了子弹的起始位置和目标位置。将位置z设置为z
bulletPrefab
。以前,我们将弹丸的预制位置设置为z,以使弹丸显示在射击怪物下方,但在敌人上方。 - 我们使用
bulletPrefab
适当的shell创建一个新shell的实例MonsterLevel
。分配startPosition
并targetPosition
投射。 - 我们使游戏更有趣:当怪物射击时,开始射击动画并播放激光的声音。
全部放在一起
现在是时候将所有内容放在一起了。定义目标并使怪物看着它。在ShootEnemies.cs脚本中,添加Update()
以下代码: GameObject target = null;
逐步考虑此代码。- 确定怪物的目的。我们从中的最大可能距离开始
minimalEnemyDistance
。如果敌人到Cookie的距离小于当前的最小距离,我们将在一个范围内的所有敌人周围循环,并使其成为新的目标。 - 我们调用
Shoot
经过时间是否大于怪物的射击频率,并设置lastShotTime
当前时间的值。 - 我们计算怪物与其目标之间的旋转角度。我们将怪物旋转到这个角度。现在,他将始终盯着目标。
保存文件并在Unity中启动游戏。怪物将开始拼命保护cookie。我们终于完成了!接下来要去哪里
可以从此处下载完成的项目。我们在本教程中做得很好,现在我们有了出色的游戏。以下是一些对该项目进行进一步开发的想法:这些方面中的每一个都将需要最小的更改,并且可以使游戏更加有趣。如果您根据本教程创建了一个新游戏,我将很乐于玩该游戏,因此请共享一个链接。在这次采访中可以找到有关创建塔防热门游戏的有趣想法。