塔防游戏非常受欢迎,没有什么比看着你的防御消灭邪恶的入侵者更令人满意的了!在这个由两部分组成的教程中,您将使用 Unity 构建一个塔防游戏!
您将学习如何…
- 制造一波又一波的敌人
- 让他们遵循路点移动
- 建造和升级塔,让它们将你的敌人减少直至消失
最后,您将拥有该类型的框架,可以对其进行扩展!
注意:您需要了解 Unity 基础知识,例如如何添加游戏资源和组件、了解预制件并了解一些基本的 C#。
塔防的全貌
在本教程中,您将构建一个塔防游戏,其中敌人(小虫子)爬向属于您和您的爪牙的饼干,这些爪牙当然是怪物!您可以在战略要点放置和升级怪物,以获得一点金币。
玩家必须在虫子吃你的饼干之前杀死它们。每一波敌人都更难被击败。当你在所有浪潮中幸存下来(胜利!)或五个敌人到达饼干时,游戏结束。(失败!)
以下是已完成游戏的屏幕截图:
怪物联合起来!保护饼干!
开始
在 Unity 中打开 TowerDefense-Part1-Starter 项目。
入门项目包括美术和声音资源,以及预构建的动画和一些有用的脚本。这些脚本与塔防游戏没有直接关系,因此此处不再解释。但是,如果您想了解有关创建 Unity 2D 动画的更多信息,请查看此 Unity 2D 教程。
该项目还包含预制件,稍后您将对其进行扩展以创建角色。最后,该项目包括一个场景及其背景和用户界面设置。
打开_“Scenes”_文件夹中的 GameScene,并将游戏视图的纵横比设置为 4:3,以确保标签与背景正确对齐。您应该在“游戏”视图中看到以下内容:
学分:
- 该项目的艺术来自Vicki Wenderlich的免费艺术包!你可以在gameartguppy上找到更多很棒的图形。
- 很酷的音乐来自BenSound,他有一些很棒的配乐!
- 感谢Michael Jasper的震撼。
入门项目 – 检查!
资源 – 检查!
迈向统治世界的第一步…嗯,我是说你的塔防游戏…大功告成!
X 标记点:放置
怪物只能在标有_x_的地方张贴。
要将它们添加到场景中,请将“Images\Objects\Openspot”从“项目浏览器”拖放到“场景_”视图中。目前,职位并不重要。
在“层次结构”中选择_“Openspot_”后,单击__Inspector_中的“Add Component”,然后选择“Box Collider 2D”。Unity 在场景视图中显示带有绿线的框碰撞体。您将使用此碰撞体来检测该位置上的鼠标单击。
Unity 会自动检测碰撞体的合适尺寸。这有多酷?
按照相同的步骤,将Audio\Audio Source组件添加到Openspot。将音频源的_音频剪辑_设置为 tower_place,您可以在“Audio”文件夹中找到该文件夹,然后停用_“Play On Awake_”。
您需要再创建 11 个点。虽然重复所有这些步骤很诱人,但 Unity 有一个很好的解决方案:预制件!
将 Openspot 从“层次结构”拖放到“项目浏览器”的_“预制件_”文件夹中。然后,它的名称在层次结构中变为蓝色,以显示它已连接到预制件。像这样:
现在您已经有了预制件,您可以根据需要创建任意数量的副本。只需将 Openspot 从_项目浏览器中_的_预制件_文件夹拖放到_场景_视图中即可。执行此操作 11 次,即可在场景中总共创建 12 个 Openspot 对象。
现在使用_Inspector_将这 12 个 Openspot 对象的位置设置为以下坐标:
- (X:-5.2, Y:3.5, Z:0)
- (X:-2.2, Y:3.5, Z:0)
- (X:0.8, Y:3.5, Z:0)
- (X:3.8, Y:3.5, Z:0)
- (X:-3.8, Y:0.4, Z:0)
- (X:-0.8, Y:0.4, Z:0)
- (X:2.2, Y:0.4, Z:0)
- (X:5.2, Y:0.4, Z:0)
- (X:-5.2, Y:-3.0, Z:0)
- (X:-2.2, Y:-3.0, Z:0)
- (X:0.8, Y:-3.0, Z:0)
- (X:3.8, Y:-3.0, Z:0)
完成后,场景应如下所示。
放置怪物
为了便于放置,项目的预制件文件夹包含一个_怪物__预制_件。
怪物预制件 – 随时可用
此时,它由一个空的游戏对象组成,其中包含三个不同的精灵及其子元素的射击动画。
每个精灵代表不同力量水平的怪物。预制件还包含一个_音频源_组件,每当怪物发射激光时,您都会触发该组件来播放声音。
您现在将创建一个可以将Monster放置在Openspot上的脚本
在“项目浏览器”中,选择“预制件”文件夹中的_“Openspot_”。在_检查器_中,点按“添加组件”,然后选取_“新建脚本_”并将其命名_为 PlaceMonster_。选择C Sharp 作为_语言_,然后单击_创建和添加_。由于您已将脚本添加到 Openspot _预制件_中,因此场景中的所有 Openspot 现在也附加了该脚本。整洁!
双击脚本以在 IDE 中将其打开。然后添加这两个变量:
public GameObject monsterPrefab;
private GameObject monster;
您将实例化存储在其中的对象的副本monsterPrefab
以创建怪物,并将其存储在其中monster
以便您可以在游戏过程中对其进行操作。
每个位置一个怪物
添加以下方法,每个位置只允许一个怪物:
private bool CanPlaceMonster()
{
return monster == null;
}
在CanPlaceMonster()
你检查monster
变量是否仍然null
. 如果是的话,说明这里目前没有怪物,放一个也可以。
现在添加以下代码,以便在玩家单击此游戏对象时实际放置怪物:
void OnMouseUp()
{
if (CanPlaceMonster())
{
monster = (GameObject)
Instantiate(monsterPrefab, transform.position, Quaternion.identity);
AudioSource audioSource = gameObject.GetComponent<AudioSource>();
audioSource.PlayOneShot(audioSource.clip);
}
}
此代码在鼠标单击或点击时放置一个怪物。那么这是如何实现的呢?
OnMouseUp
当玩家点击游戏对象的物理碰撞器时,Unity 会自动调用。CanPlaceMonster()
调用时,如果返回,此方法会放置一个新怪物true
。- 您使用 来创建怪物
Instantiate
,该方法创建具有指定位置和旋转的给定预制件的实例。在这种情况下,您复制monsterPrefab
,为其提供当前游戏对象的位置且不进行旋转,将结果转换为 aGameObject
并将其存储在monster
. - 最后,您调用
PlayOneShot
播放附加到对象AudioSource
组件的声音效果。
现在你的PlaceMonster
脚本可以放置一个新的怪物,但你仍然必须指定预制件。
使用正确的预制件
保存文件并切换回 Unity。
要分配 monsterPrefab 变量,请先在项目浏览器的_预制件_文件夹中选择 Openspot。
在__检查器__中,单击 PlaceMonster(脚本)组件的“怪物_预制件”字段右侧的圆圈,然后从出现的对话框中选择“Monster”。
就是这样。运行场景并通过单击或点击在各种x点上构建怪物。
成功!你可以建造怪物。然而,它们看起来像一个奇怪的糊状物,因为你的怪物的所有子精灵都被绘制出来了。接下来,您将解决此问题。
升级那些怪物
在下图中,你会看到你的怪物在更高层次上看起来越来越可怕。
太蓬松了!但是如果你试图偷它的饼干,这个怪物可能会变成杀手。
脚本充当为怪物实现升级系统的基础。它跟踪怪物在每个级别上应该有多强大,当然还有怪物的当前级别。
现在添加此脚本。
在_项目浏览器中_选择_预制件/怪物_。添加一个名为 MonsterData 的新 C# 脚本。在 IDE 中打开脚本,并在类_上方_添加以下代码。MonsterData
[System.Serializable]
public class MonsterLevel
{
public int cost;
public GameObject visualization;
}
这将创建 MonsterLevel
.它对成本(以金币为单位,稍后会支持)和特定怪物关卡的视觉表示形式进行分组。
[System.Serializable]
您可以在顶部添加以使类的实例可从检查器进行编辑。这使您可以快速更改 Level 类中的所有值,即使在游戏运行时也是如此。它对于平衡你的游戏非常有用。
定义怪物等级
在本例中,您将预定义的怪物等级
存储在列表<T>
中。
为什么不简单地使用MonsterLevel[]
?MonsterLevel
那么,您将多次需要特定对象的索引。虽然为此编写代码并不困难,但您将使用IndexOf()
,它实现了 的功能Lists
。这次无需重复写一个方法。:]
在 MonsterData.cs 的顶部,添加以下语句:using
using System.Collections.Generic;
这使您可以访问通用数据结构,因此可以在脚本中使用该类。List<T>
注意:泛型是 C# 的重要组成部分。它们允许您定义类型安全的数据结构,而无需提交类型。这对于列表和集合等容器类非常实用。若要了解有关泛型的详细信息,请查看 C# 泛型简介。
现在将以下变量添加到 MonsterData
中以存储 MonsterLevel
列表:
public List<MonsterLevel> levels;
使用泛型,您可以确保只能包含对象。levels``List``MonsterLevel
保存文件并切换到 Unity 以配置每个阶段。
在_项目浏览器中_选择_预制件/怪物_。在__检查器__中,您现在可以在 MonsterData(脚本)组件中看到“级别”字段。将其_大小_设置为 3。
接下来,将每个级别的_成本_设置为以下值:
- 元素 0:200
- 元素 1:110
- 元素 2:120
现在分配可视化字段值。
在项目浏览器中展开预_制件/怪物_,以便可以看到其子项。将子 Monster0 拖放到_元素 0_ 的_可视化_字段中。
重复此操作,将怪物 1 分配给_元素 1_,将_怪物_ 2 分配给_元素 2_。请参阅以下演示此过程的 GIF:
选择预制件_/怪物时,预制件_应如下所示:
在检查器中定义怪物的等级。
定义当前等级
在 IDE 中_切换_回 MonsterData.cs并将另一个变量添加到 MonsterData
。
private MonsterLevel currentLevel;
在私有变量中,currentLevel
您将存储……等等……怪物的当前等级。]
现在设置currentLevel
并使其可供其他脚本访问。将以下内容添加到MonsterData
, 以及实例变量声明:
public MonsterLevel CurrentLevel
{
get
{
return currentLevel;
}
set
{
currentLevel = value;
int currentLevelIndex = levels.IndexOf(currentLevel);
GameObject levelVisualization = levels[currentLevelIndex].visualization;
for (int i = 0; i < levels.Count; i++)
{
if (levelVisualization != null)
{
if (i == currentLevelIndex)
{
levels[i].visualization.SetActive(true);
}
else
{
levels[i].visualization.SetActive(false);
}
}
}
}
}
那里有相当多的 C#脚本,嗯?把所有都看一遍:
- 定义私有_变量的属性
currentLevel
。_定义属性后,您可以像调用任何其他变量一样调用:asCurrentLevel
(从类内部)或 asmonster.CurrentLevel
(从类外部)。您可以在属性的 getter 或 setter 方法中定义自定义行为,并且通过仅提供 getter、setter 或两者,您可以控制属性是只读、只写还是读/写。 - 在 getter 中,返回
currentLevel
的值。 - 在资源库中,将新值分配给currentLevel 。接下来,您将获得当前级别的索引。最后,循环访问所有_级别_,并将可视化设置为活动或非活动,具体取决于currentLevelIndex .这很棒,因为这意味着每当有人设置currentLevel 时,子画面都会自动更新。属性肯定会派上用场!
添加以下 实现OnEnable
:
void OnEnable()
{
CurrentLevel = levels[0];
}
这CurrentLevel将在放置时设置,确保它只显示正确的精灵。
注意:在 OnEnable
中初始化属性而不是OnStart
非常重要,因为您可以在实例化预制件时调用 order 方法。
创建预制件时,将立即调用 OnEnable
(如果预制件以启用状态保存),但直到对象开始作为场景的一部分运行后才会调用 OnStart
。
在放置怪物之前,您需要检查此数据,因此您可以在 OnEnable
中对其进行初始化。
保存文件并切换到 Unity。运行项目并放置怪物;现在,它们显示正确和最低级别的精灵。
升级那些怪物
切换回您的 IDE,并将以下方法添加到 MonsterData
:
public MonsterLevel GetNextLevel()
{
int currentLevelIndex = levels.IndexOf (currentLevel);
int maxLevelIndex = levels.Count - 1;
if (currentLevelIndex < maxLevelIndex)
{
return levels[currentLevelIndex+1];
}
else
{
return null;
}
}
在GetNextLevel
中,您可以获得当前级别的索引和最高级别的
索引,前提是怪物没有达到最大级别以返回下一个级别。否则,返回 null
。
您可以使用此方法来确定是否可以升级怪物。
添加以下方法以提高怪物的等级:
public void IncreaseLevel()
{
int currentLevelIndex = levels.IndexOf(currentLevel);
if (currentLevelIndex < levels.Count - 1)
{
CurrentLevel = levels[currentLevelIndex + 1];
}
}
在这里,您可以获得当前级别的索引,然后通过检查它是否小于 来确保它不是最大级别。如果是这样,请设置为下一个级别。levels.Count - 1``CurrentLevel
测试升级能力
保存该文件,然后在 IDE 中切换到 PlaceMonster.cs 并添加以下新方法:
private bool CanUpgradeMonster()
{
if (monster != null)
{
MonsterData monsterData = monster.GetComponent<MonsterData>();
MonsterLevel nextLevel = monsterData.GetNextLevel();
if (nextLevel != null)
{
return true;
}
}
return false;
}
首先检查是否有可以升级的怪物,方法是检查怪物
变量是否为 null
。如果是这种情况,您可以从怪物数据
中获取怪物的当前等级。
然后测试是否有更高级别可用,即 GetNextLevel()
不返回 null
时。如果可以升级,则返回 true
,否则返回 false
。
启用金牌升级
要启用升级选项,请将 else if
分支添加到 OnMouseUp
:
if (CanPlaceMonster())
{
}
else if (CanUpgradeMonster())
{
monster.GetComponent<MonsterData>().IncreaseLevel();
AudioSource audioSource = gameObject.GetComponent<AudioSource>();
audioSource.PlayOneShot(audioSource.clip);
}
检查是否可以使用 CanUpgradeMonster()
进行升级。如果是,您可以使用 GetComponent()
访问 MonsterData
组件并调用 IncreaseLevel(),
这将增加怪物的级别。最后,你触发怪物的_音频源_。
保存文件并切换回 Unity。运行游戏,放置和升级_任意数量的_怪物…目前。
所有怪物升级
支付金币 - 游戏管理器
现在,可以立即建造和升级所有怪物,但这其中的挑战在哪里?
让我们深入了解黄金问题。跟踪它的问题在于您需要在不同的游戏对象之间共享信息。
下图显示了所有要参与其中的对象。
突出显示的游戏对象都需要知道,玩家拥有多少金币。
您将使用其他对象可访问的共享对象来存储此数据。
在_层次结构_中单击鼠标右键,然后选择“创建空”。将新游戏对象命名_为游戏管理器_。
将一个名为 GameManagerBehavior 的 C# 脚本添加到 GameManager,然后在 IDE 中打开新脚本。您将在标签中显示玩家的总金币,因此请在文件顶部添加以下行:
using UnityEngine.UI;
这使您可以访问特定于 UI 的类,例如 ,项目将其用于标签。现在将以下变量添加到类中:Text
public Text goldLabel;
Text
这将存储对用于显示玩家拥有多少金币的组件的引用。
既然GameManager
知道了标签,您如何确保变量中存储的黄金数量与标签上显示的数量同步?您将创建一个属性。
将以下代码添加到GameManagerBehavior
:
private int gold;
public int Gold {
get
{
return gold;
}
set
{
gold = value;
goldLabel.GetComponent<Text>().text = "GOLD: " + gold;
}
}
似乎很熟悉?它类似于您在CurrentLevel 中定义的Monster
。首先,创建一个私有变量gold
,来存储当前的黄金总量。然后你定义一个名为Gold
– creative的属性,对吧?-- – 并实现一个 getter 和 setter。
getter 只需返回 gold
的值。setter更有趣。除了设置变量的值外,它还将字段text
设置为“打开”以goldLabel
显示新的黄金数量。
你觉得有多慷慨?添加以下行以Start()
给玩家 1000 金币,如果您感到吝啬,则分配更少:
Gold = 1000;
将标签对象分配给脚本
保存文件并切换到 Unity。
在Hierarchy中,选择GameManager。在Inspector 中,单击Gold Label右侧的圆圈。在Select Text对话框中,选择Scene选项卡并选择GoldLabel。
运行场景,标签显示_Gold:1000_。
检查玩家的“钱包”
在 IDE 中_.cs打开 PlaceMonster_,然后添加以下实例变量:
private GameManagerBehavior gameManager;
您将用于gameManager
访问GameManagerBehavior
场景的GameManager的组件。要分配它,请将以下内容添加到Start()
:
gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
您可以使用 获取名为 GameManager 的游戏对象,该游戏对象返回它找到的第一个具有给定名称的游戏对象。然后,检索其组件并将其存储以供以后使用。GameObject.Find()``GameManagerBehavior
注意:您可以通过在 Unity 编辑器中设置字段或向
GameManager
添加静态方法来实现此目的,该方法返回一个单一实例,您可以从中获取游戏管理器行为
。但是,上面的块中有一个黑马方法:,它在运行时速度较慢,但方便且可以谨慎使用。
Find
拿钱!
你还没有扣除金币,所以在 OnMouseUp()
中添加_了两次_这一行,替换每个注释 // TODO: 扣除金币:
gameManager.Gold -= monster.GetComponent<MonsterData>().CurrentLevel.cost;
保存文件并切换到 Unity,升级一些怪物并观看黄金读数更新。现在你扣除金币,但玩家只要有空间就可以建造怪物。
无限能力?棒!但你不能允许这种情况发生。只有当玩家有足够的金币时,才应该放置怪物。
怪物需要金币
在 IDE 中切换到 PlaceMonster.cs,并将 CanPlaceMonster()
的内容替换为以下内容:
int cost = monsterPrefab.GetComponent<MonsterData>().levels[0].cost;
return monster == null && gameManager.Gold >= cost;
levels
从其 中检索放置怪物的成本MonsterData
。然后你检查它monster
不是null
并且它gameManager.Gold
大于成本。
CanUpgradeMonster()
挑战:添加自己检查玩家是否有足够的金币。替换此行:
return true;
有了这个:
return gameManager.Gold >= nextLevel.cost;
这将检查玩家拥有的_金币_是否超过升级成本。
在 Unity 中保存并运行场景。来吧,试着放置无限的怪物!
现在你只能建造有限数量的怪物。
塔政治:敌人、波浪和航点
是时候为你的敌人“铺路”了。敌人出现在第一个航点,向下一个航点移动并重复,直到他们到达你的饼干。
你会让敌人行进:
- 为敌人确定一条道路
- 沿路移动敌人
- 旋转敌人使其向前看
创建带有航点的道路
在_层次结构_中单击鼠标右键,然后选择创建空以创建新的_空_游戏对象。将其命名为 Road,并确保它位于位置 (0, 0, 0)。
现在,右键单击_层次结构_中的 Road,并创建另一个空游戏对象作为 Road 的子对象。将其命名为_Waypoint0_并将其位置设置为_(-12,2,0)_ - 这是敌人开始攻击的地方。
使用以下名称和位置以相同的方式再创建五个航点:
- 航点 1: (X:7, Y:2, Z:0)
- 航点 2: (X:7, Y:-1, Z:0)
- 航点3: (X:-7.3, Y:-1, Z:0)
- 航点4: (X:-7.3, Y:-4.5, Z:0)
- 航点5: (X:7, Y:-4.5, Z:0)
以下屏幕截图突出显示了航点位置和生成的路径。
生成敌人
现在要制造一些敌人来跟路。_预制件_文件夹包含_敌人_预制件。它的位置是 _(-20, 0, 0),_因此新实例将在屏幕外生成。
否则,它的设置很像Monster预制件,有一个AudioSource
和一个 child Sprite
,而且它是一个精灵,所以你可以稍后旋转它,而无需旋转即将需要的血量条。
### 把怪物移到路上
将一个名为 MoveEnemy 的新 C# 脚本添加到预制件_\Enemy 预制件_。在 IDE 中打开脚本,然后添加以下变量:
[HideInInspector]
public GameObject[] waypoints;
private int currentWaypoint = 0;
private float lastWaypointSwitchTime;
public float speed = 1.0f;
waypoints
将航点的副本存储在一个数组中,而上述内容可确保您不会意外更改__检查器__中的字段,但您仍然可以从其他脚本访问它。[HideIn_inspector_]``waypoints
currentWaypoint
跟踪敌人当前正在离开的路点,lastWaypointSwitchTime
存储敌人经过它的时间。最后,存储敌人的speed
。
在 Start()
中添加此行:
lastWaypointSwitchTime = Time.time;
这将初始化lastWaypointSwitchTime
为当前时间。
要使敌人沿着路径移动,请将以下代码添加到 Update()
中:
Vector3 startPosition = waypoints [currentWaypoint].transform.position;
Vector3 endPosition = waypoints [currentWaypoint + 1].transform.position;
float pathLength = Vector3.Distance (startPosition, endPosition);
float totalTimeForPath = pathLength / speed;
float currentTimeOnPath = Time.time - lastWaypointSwitchTime;
gameObject.transform.position = Vector2.Lerp (startPosition, endPosition, currentTimeOnPath / totalTimeForPath);
if (gameObject.transform.position.Equals(endPosition))
{
if (currentWaypoint < waypoints.Length - 2)
{
currentWaypoint++;
lastWaypointSwitchTime = Time.time;
}
else
{
Destroy(gameObject);
AudioSource audioSource = gameObject.GetComponent<AudioSource>();
AudioSource.PlayClipAtPoint(audioSource.clip, transform.position);
}
}
循序渐进:
- 从路点数组中,您可以检索当前路径段的开始和结束位置。
- 用公式时间=距离/速度计算全程所需时间,然后确定路径上的当前时间。使用
Vector2.Lerp
,您可以在段的开始位置和结束位置之间插入敌人的当前位置。 - 检查敌人是否已经到达
endPosition
。如果是,请处理这两种可能的情况:- 敌人还没有在最后一个航路点,所以增加
currentWaypoint
和更新lastWaypointSwitchTime
。稍后,您将添加代码来旋转敌人,使其也指向它移动的方向。 - 敌人到达了最后一个路点,所以这会摧毁它并触发声音效果。稍后您还将添加代码来减少玩家的
health
。
- 敌人还没有在最后一个航路点,所以增加
保存文件并切换到 Unity。
给敌人一个方向感
在目前的状态下,敌人不知道路标的顺序。
在Hierarchy中选择Road ,并添加一个名为 SpawnEnemy的新C# 脚本。然后在您的 IDE 中打开它,并添加以下变量:
public GameObject[] waypoints;
您将使用它waypoints
以正确的顺序存储场景中的路点。
保存文件并切换到 Unity。在_层次结构_中选择_道路_,并将_航点_数组_的大小_设置为 6。
将 Road 的每个子项拖到字段中,将 Waypoint0 放入元素 0,将 Waypoint1 放入_元素 1_,依此类推。
现在你有一个数组,其中包含整齐有序的航点,所以有一条路径——请注意,它们永远不会撤退;他们会死在试图得到糖的路上。
检查一切是否正常
前往 IDE 中的 SpawnEnemy,并添加以下变量:
public GameObject testEnemyPrefab;
这会在 testEnemyPrefab
中保留对 Enemy 预制件的引用。
要在脚本启动时创建敌人,请将以下代码添加到 Start():
Instantiate(testEnemyPrefab).GetComponent<MoveEnemy>().waypoints = waypoints;
这将实例化存储在 testEnemy
中的预制件的新副本,并为其分配要遵循的航点。
保存文件并切换到 Unity。在Hierarchy中选择Road并将其Test Enemy设置为Enemy预制件。
运行项目,看到敌人沿着这条路走。
运行项目以查看敌人沿着道路行驶。
你有没有注意到他们并不总是在寻找他们要去的地方?有趣!如果你想在这个项目上更进一步,继续第二部分并完善这个项目,学习如何令他们表现得更出色。
后续
你已经做了很多工作,并且正在拥有自己的塔防游戏。
玩家可以建造怪物,但不是无限数量,并且有一个敌人跑向你的饼干。玩家拥有金币,还可以升级怪物。
在第二部分中,你将介绍生成大量敌人并将它们炸飞。第二部分见
博主属于自学型选手,如果你也是Unity初学者,欢迎加入我的群聊进行互助交流:618012892