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

图片

塔防游戏越来越受欢迎,这并不奇怪-几乎没有什么比观察自己的防御线能够消灭邪恶敌人的乐趣可比了! 在这个分为两部分的教程中,我们将在Unity引擎上创建一个塔防游戏!

您将学习如何执行以下操作:

  • 制造敌人之波
  • 让他们跟随路线点
  • 建造和升级塔,并教他们如何将敌人分解成小像素

最后,我们得到了游戏的框架,可以进一步开发它!

注意 :您需要基本的Unity知识(例如,您需要知道如何添加资产和组件,什么是预制件)以及C#的基础知识。 要学习所有这些,我建议您阅读Sean Duffy的 Unity教程或Brian Mockley的Unity入门C#系列教程。

我将在Unity for OS X中工作,但本教程也适用于Windows。

透过象牙塔的窗户


在本教程中,我们将创建一个塔防游戏,其中敌人(小虫子)爬到属于您和您的奴才(当然,这些都是怪物!)的cookie。 玩家可以将怪物放置在战略要点上并升级以获得金牌。

玩家必须杀死所有错误,直到它们到达cookie。 每一波新的敌人都越来越难以战胜。 当您在所有海浪中幸免(胜利!)或五个敌人爬到cookie(丢失!)时,游戏结束。

这是完成游戏的屏幕截图:


怪物,团结起来! 保护cookie!

开始工作


项目下载为空白 ,解压缩,然后在Unity中打开TowerDefense-Part1-Starter项目。

项目草案包含图形和声音,现成的动画以及一些有用的脚本的资产。 这些脚本与塔防游戏没有直接关系,因此在此我将不再赘述。 但是,如果您想了解有关在Unity中创建2D动画的更多信息,请查看此Unity 2D教程

该项目还包含预制件,我们将在以后添加这些预制件来创建角色。 最后,项目中有一个场景,具有背景和自定义的用户界面。

打开位于“ 场景”文件夹中的GameScene ,并将“游戏”模式设置为4:3的纵横比,以便所有标签正确匹配背景。 在游戏模式下,您将看到以下内容:


著作权:

  • 该项目的图形取自免费的Wiki Wenderlich包! 其他图形作品可以在她的gameartguppy网站上找到。
  • BenSound带来的美妙音乐,还有其他很棒的配乐!
  • 我也感谢Michael Jesper的相机抖动功能

这个地方标有一个十字架:怪物的位置


怪物只能放置在标有x的点上

要将它们添加到场景中, 从“ 项目浏览器” 中将“图像\对象\ Openspot”拖动到“ 场景”窗口中。 虽然这个职位对我们并不重要。

在层次结构中选择Openspot后,在检查器中单击“ 添加组件” ,然后选择Box Collider 2D 。 在“场景”窗口中,Unity将显示带有绿线的矩形对撞机。 我们将使用此对撞机识别此位置上的鼠标单击。


以相同的方式将Audio \ Audio Source组件添加到Openspot 。 对于“音频源”组件的AudioClip参数,选择位于“ 音频”文件夹中的tower_place文件,并禁用“唤醒时播放”

我们需要再创建11个点。 尽管有重复这些步骤的诱惑,但Unity还是有一个更好的解决方案: Prefab

Openspot层次结构拖动到项目浏览器内的Prefabs文件夹中。 其名称在层次结构中将变为蓝色,这表示它已连接到预制件。 像这样:


现在有了预制坯,我们可以创建任意数量的副本。 只需将OpenspotProject Browser内的Prefabs文件夹拖放到Scene窗口中。 重复此11次,场景中将出现12个Openspot对象。

现在,使用检查器使用以下坐标设置这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)

执行此操作时,场景将如下所示:


我们放置怪物


为了简化放置,项目的Prefab文件夹中有一个Monster预制件。


Monster Prefab即用型

目前,它由一个带有三个不同精灵的空游戏对象组成,并以子级形式拍摄动画。

每个精灵都是具有不同等级力量的怪物。 预制件还包含音频源组件,当怪物发射激光时,它将启动以播放声音。

现在,我们将创建一个脚本,该脚本将在Openspot托管 Monster

项目浏览器中,Prefabs文件夹中选择Openspot对象。 在检查器中,单击“ 添加组件” ,然后选择“ 新建脚本” ,并将脚本命名为PlaceMonster 。 选择C Sharp作为语言,然后点击创建和添加 。 由于我们已将脚本添加到Openspot预制中,因此场景中的所有Openspot对象现在都将具有此脚本。 太好了!

双击脚本以在IDE中将其打开。 然后添加两个变量:

public GameObject monsterPrefab; private GameObject monster; 

我们将创建一个存储在monsterPrefab中的对象的实例,以创建怪物,并将其存储在monsterPrefab中,以便可以在游戏中对其进行操作。

每点一个怪物


为了使一个怪物只能放在一个点上,请添加以下方法:

 private bool CanPlaceMonster() { return monster == null; } 

CanPlaceMonster()我们可以检查CanPlaceMonster()变量是否仍然为null 。 如果是这样,那么此时就没有怪物了,我们可以将其放置。

现在添加以下代码,以在玩家单击此GameObject时放置怪物:

 //1 void OnMouseUp() { //2 if (CanPlaceMonster()) { //3 monster = (GameObject) Instantiate(monsterPrefab, transform.position, Quaternion.identity); //4 AudioSource audioSource = gameObject.GetComponent<AudioSource>(); audioSource.PlayOneShot(audioSource.clip); // TODO:   } } 

当您单击鼠标或触摸屏幕时,此代码将定位怪物。 他如何工作?

  1. 当玩家触摸物理对撞机GameObject时,Unity会自动调用OnMouseUp。
  2. 调用时,如果CanPlaceMonster()返回true ,则此方法放置怪物。
  3. 我们使用Instantiate方法创建一个怪物,该方法创建具有指定位置和旋转的给定预制实例。 在这种情况下,我们复制monsterPrefab ,为它提供当前monsterPrefab的位置并且不旋转,将结果传输到GameObject并将其保存到GameObject
  4. 最后,我们调用PlayOneShot播放附加到对象AudioSource组件上的声音效果。

现在,我们的PlaceMonster脚本可以包含一个新的怪物,但是我们仍然需要指定一个预制件。

使用正确的预制件


保存文件并返回到Unity。

要设置monsterPrefab变量,请首先从项目浏览器的Prefabs文件夹中选择Openspot对象。

检查器中,单击PlaceMonster(脚本)组件的Monster Prefab字段右侧的圆圈然后在出现的对话框中选择Monster


仅此而已。 通过单击鼠标或触摸屏幕来启动场景并在不同位置创建怪物。


太好了! 现在我们可以创建怪物了。 但是,它们看起来像一个奇怪的烂摊子,因为绘制了怪物的所有子精灵。 现在我们将修复它。

提高怪物的等级


下图显示,随着等级的提高,怪物看起来越来越可怕。


真可爱! 但是,如果您尝试偷走他的饼干,那么这个怪物就会变成杀手。

该脚本用作实现怪物关卡系统的基础。 它跟踪怪物在每个级别上的力量,当然还跟踪怪物的当前级别。

添加此脚本。

项目浏览器中选择Prefabs / Monster预制件。 添加一个名为MonsterData的新C#脚本。 在IDE中打开脚本,然后在MonsterData上方添加以下代码。

 [System.Serializable] public class MonsterLevel { public int cost; public GameObject visualization; } 

因此,我们创建了MonsterLevel 。 它对价格(以金币计,我们将在下面提供支持)进行分组,并以可视化方式表示怪物的水平。

我们在[System.Serializable]上添加,以便可以在检查器中修改类实例。 这样,即使在游戏运行时,我们也可以快速更改Level类的所有值。 这对于平衡游戏非常有用。

设定怪物等级


在我们的例子中,我们将指定的MonsterLevel存储在List<T>

为什么不只使用MonsterLevel[] ? 我们将多次需要特定MonsterLevel对象的索引。 尽管为此编写代码很容易,但是我们仍然必须使用实现Lists功能的IndexOf() 。 重新发明轮子没有任何意义。


重新设计自行车通常不是一个好主意。

MonsterData.cs的顶部, using以下结构添加以下内容:

 using System.Collections.Generic; 

它使我们能够访问通用数据结构,以便我们可以在脚本中使用List<T>类。

注意 :泛化是强大的C#概念。 它们使您可以指定类型安全的数据结构,而不必遵守类型。 这对于诸如列表和集合之类的容器类很有用。 要了解有关泛型结构的更多信息,请阅读《 C#泛型介绍》一书。

现在将以下变量添加到MonsterData来保存MonsterLevel列表:

 public List<MonsterLevel> levels; 

多亏了归纳法,我们可以保证level中的List仅包含MonsterLevel对象。

保存文件并切换到Unity以配置每个级别。

项目浏览器中选择“ 预制件/怪物 。 现在, 检查器将显示MonsterData(脚本)组件的“ 级别”字段。 将大小设置为3


接下来,为每个级别设置成本

  • 元素0200
  • 元素1110
  • 元素2120

现在,我们分配视觉显示字段的值。

在项目浏览器中展开Prefabs / Monster ,以查看其子级。 将子Monster0拖到可视化的 Element 0字段中。

接下来,将Element 1设置为Monster1 ,将Element 2设置Monster2 。 GIF显示此过程:


当您选择Prefabs / Monster时 ,预制件应如下所示:


设置当前水平


返回IDE中的MonsterData.cs并将另一个变量添加到MonsterData

 private MonsterLevel currentLevel; 

在私有变量currentLevel我们将存储怪物的当前等级。

现在设置currentLevel并使它对其他脚本可见。 MonsterData与实例变量的声明一起添加到MonsterData

 //1 public MonsterLevel CurrentLevel { //2 get { return currentLevel; } //3 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#代码块,对不对? 让我们按顺序进行:

  1. 设置私有变量currentLevel属性 。 通过设置属性,我们可以像调用其他任何变量一样调用它:作为CurrentLevel (在类内部)或monster.CurrentLevel (在外部)。 我们可以在属性的getter或setter方法中定义任何行为,并且通过仅创建getter,setter或这两者,我们可以控制属性的属性:只读,只写和写/读。
  2. 在getter中,我们返回currentLevel的值。
  3. 在设置器中,我们为currentLevel分配currentLevel新值。 然后我们得到当前水平的指数。 最后,我们遍历所有级别并根据currentLevelIndex启用/禁用可视显示。 这很棒,因为当currentLevel更改时,子画面会自动更新。 属性是非常方便的事情!

添加以下OnEnable实现:

 void OnEnable() { CurrentLevel = levels[0]; } 

在这里,我们在放置时设置CurrentLevel 。 这样可以确保仅显示所需的精灵。

注意 :在OnEnable中而不是OnStart初始化属性很重要,因为在创建预制实例时我们会调用序数方法。

创建预制件时(如果预制件保存在启用状态下)将立即调用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我们获得currentLevel索引和最高级别的索引; 如果怪物尚未达到最大等级,则返回下一个等级。 否则,返回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; } 

首先,我们检查是否有可以通过将monster变量与null进行比较来改善的monster 。 如果这是真的,那么我们从其MonsterData获取当前的怪物等级。

然后,我们检查下一个级别是否可用,即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); // TODO:   } 

我们使用CanUpgradeMonster()检查升级的可能性。 如果可能,我们使用GetComponent()访问MonsterData组件,然后调用GetComponent() IncreaseLevel() ,这会增加怪物的等级。 最后,我们启动Monster AudioSource

保存文件并返回到Unity。 运行游戏,放置并升级任意数量的怪物(但目前为止)。


支付金-游戏管理员


尽管我们可以立即构建和改进任何怪物,但在游戏中会有趣吗?

让我们看看黄金的问题。 跟踪问题在于我们必须在不同游戏对象之间传递信息。

下图显示了应该参与的所有对象。


所有选定的游戏对象必须知道玩家拥有多少黄金。

为了存储此数据,我们将使用其他对象可以访问的公共对象。

右键单击层次结构,然后选择创建空 。 命名新的GameManager对象。

将名为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; } } 

他看起来很熟吗? 该代码类似于我们在Monster设置的CurrentLevel 。 首先,我们创建一个私有变量gold来保存当前数量的黄金。 然后,我们设置Gold属性(出乎意料,对吧?)并实现getter和setter。

吸气剂只是返回gold的价值。 塞特犬更有趣。 除了设置变量的值外,它还设置goldLabeltext字段以显示新的黄金值。

我们会多么慷慨? 将以下行添加到Start() ,为播放器提供1000金币;如果您为这笔钱感到遗憾,则少给他:

 Gold = 1000; 

将标签对象分配给脚本


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


运行场景,标签将显示Gold:1000


查看玩家的“钱包”


在IDE中打开PlaceMonster.cs脚本,并添加以下实例变量:

 private GameManagerBehavior gameManager; 

我们将使用gameManager访问场景中GameManagerBehavior对象的GameManagerBehavior组件。 要指定它,请将以下内容添加到Start()

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

我们使用GameObject.Find GameObject.Find()函数获得了一个名为GameManager的GameObject,它返回使用该名称找到的第一个游戏对象。 然后我们获取其组件GameManagerBehavior并将其保存以备将来使用。

注意 :您可以通过在Unity编辑器中设置一个字段或向GameManager添加静态方法来返回单例实例,从中可以获取GameManagerBehavior

但是,在上面显示的代码块中有一个黑马: Find方法,它在应用程序执行期间工作得更慢; 但它很方便,可以适量使用。

拿走我的钱!


我们还没有减去金,所以我们将这行添加两次OnMouseUp() ,替换每个注释// TODO:

 gameManager.Gold -= monster.GetComponent<MonsterData>().CurrentLevel.cost; 

保存文件并返回到Unity,升级一些怪物并查看Gold值的更新。 现在我们扣除金币,但是玩家只要有足够的空间就可以建造怪物。 他们只是借钱。


无限信用? 太好了! 但是我们不能允许它。 玩家必须在有足够的金币时下注怪物。

怪物金牌


在IDE中切换到PlaceMonster.cs并将CanPlaceMonster()的内容替换CanPlaceMonster()以下内容:

 int cost = monsterPrefab.GetComponent<MonsterData>().levels[0].cost; return monster == null && gameManager.Gold >= cost; 

我们从MonsterData levels MonsterData怪物放置价格。 然后我们检查gameManager.Gold不为null ,以及gameManager.Gold超过此价格。

您的任务:独立地向CanUpgradeMonster()添加CanUpgradeMonster()检查玩家是否有足够的金币。

内部解决方案
替换行:

 return true; 

在此:

 return gameManager.Gold >= nextLevel.cost; 

它将检查玩家的金币数量是否超过升级价格。

在Unity中保存并运行场景。 现在尝试那些如何无限添加怪物!


现在我们只能建造数量有限的怪物。

塔式策略:敌人,海浪和航路点


现在是时候为我们的敌人“铺路”了。 敌人出现在路线的第一点,移至下一个并重复该过程,直到到达Cookie。

您可以像这样使敌人移动:

  1. 设定敌人要走的路
  2. 沿着道路移动敌人
  3. 转向敌人,以便他向前看

从航点创建道路


右键单击“ 层次结构”,然后选择“ 创建空白”以创建一个新的空白游戏对象。 将其命名为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)

下面的屏幕截图显示了路线点和生成的路径。


结交敌人


现在创建一些敌人,以便他们可以沿着道路移动。 Prefabs文件夹中有一个敌人预制件。 它的位置是(-20,0,0) ,因此将在屏幕外创建新​​实例。

在所有其他方面,它的配置几乎与Monster预制件相同,并具有AudioSourceSprite子代,将来我们可以旋转此Sprite,而无需打开运行状况栏。


我们沿着道路移动敌人


Prefabs \ Enemy预制件中添加一个名为MoveEnemy的新C#脚本。 在IDE中打开脚本并添加以下变量:

 [HideInInspector] public GameObject[] waypoints; private int currentWaypoint = 0; private float lastWaypointSwitchTime; public float speed = 1.0f; 

waypoints ,路径点的副本存储在数组中,并且waypoints上方的[HideIn inspector ]确保我们不会意外更改Inspector中的此字段,但仍可以从其他脚本访问它。

currentWaypoint会跟踪敌人当前路线的位置, lastWaypointSwitchTime存储敌人经过它的时间。 另外,我们存储敌人的speed

将此行添加到Start()

 lastWaypointSwitchTime = Time.time; 

因此,我们使用当前时间的值初始化lastWaypointSwitchTime

为了使敌人沿着路线前进,请将以下代码添加到Update()

 // 1 Vector3 startPosition = waypoints [currentWaypoint].transform.position; Vector3 endPosition = waypoints [currentWaypoint + 1].transform.position; // 2 float pathLength = Vector3.Distance (startPosition, endPosition); float totalTimeForPath = pathLength / speed; float currentTimeOnPath = Time.time - lastWaypointSwitchTime; gameObject.transform.position = Vector2.Lerp (startPosition, endPosition, currentTimeOnPath / totalTimeForPath); // 3 if (gameObject.transform.position.Equals(endPosition)) { if (currentWaypoint < waypoints.Length - 2) { // 3.a currentWaypoint++; lastWaypointSwitchTime = Time.time; // TODO:     } else { // 3.b Destroy(gameObject); AudioSource audioSource = gameObject.GetComponent<AudioSource>(); AudioSource.PlayClipAtPoint(audioSource.clip, transform.position); // TODO:   } } 

让我们逐步分析代码:

  1. 从路线点数组中,我们可以获得当前路线段的起点和终点。
  2. 我们使用公式time = distance / speed来计算覆盖整个距离所需的时间 ,然后确定路线上的当前时间。 使用Vector2.Lerp ,我们在开始和结束精确段之间插入敌人的当前位置。
  3. 检查敌人是否到达endPosition 。 如果是,那么我们将处理两种可能的情况:
    1. 敌人尚未到达路线的最后一点,因此增加currentWaypoint的值并更新lastWaypointSwitchTime 。 稍后,我们将添加代码以使敌人转身,以便他朝自己的运动方向看。
    2. 敌人已到达路线的最后一点,然后我们将其摧毁并开始音效。 稍后,我们将添加一条代码,以减少玩家的health

保存文件并返回到Unity。

我们告知敌人运动的方向


在当前状态下,敌人不知道路线点的顺序。

层次结构中选择Road ,然后添加一个名为SpawnEnemy的新C#脚本。 在IDE中打开它并添加以下变量:

 public GameObject[] waypoints; 

我们将使用waypoints以所需顺序在场景中存储对waypoints引用。

保存文件并返回到Unity。 在层次结构中选择Road并将Waypoints数组的Size设置为6

通过将元素0中的Waypoint0粘贴到元素1中的 Waypoint1 等等,将每个Road子级拖到字段中。


现在,我们有了一个包含正确顺序的路线点的数组-提醒您,敌人永不退缩,他们不断努力争取甜蜜的回报。

检查一切如何


在IDE中打开SpawnEnemy并添加以下变量:

 public GameObject testEnemyPrefab; 

它将在testEnemyPrefab存储对敌人 testEnemyPrefabtestEnemyPrefab

要在运行脚本时创建敌人,请将以下代码添加到Start()

 Instantiate(testEnemyPrefab).GetComponent<MoveEnemy>().waypoints = waypoints; 

因此,我们将创建一个存储在testEnemy中的预制件的新副本,并为其分配路由。

保存文件并返回到Unity。 在“ 层次结构”中选择Road对象,然后为“ 测试敌人”参数选择“ 敌人”预制件。

启动该项目,并观察敌人如何沿道路行驶(在GIF中,为清楚起见,速度提高了20倍)。


注意到他并不总是看他要去哪里?这很有趣,但是我们正在尝试制作专业游戏。因此,在本教程的第二部分中,我们将教导敌人前瞻。

接下来要去哪里?


我们已经做了很多工作,并且正在迅速创建自己的塔防游戏。

玩家可以创建数量有限的怪物,敌人沿着马路奔向我们的cookie。玩家拥有金币,他们可以升级怪物。从此处

下载完成的结果第二部分中,我们将考虑制造巨大的敌人浪潮并将其消灭。待会见!

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


All Articles