塔防游戏越来越受欢迎,这并不奇怪-几乎没有什么比观察自己的防御线能够消灭邪恶敌人的乐趣可比了! 在这个分为两部分的教程中,我们将在
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的纵横比,以便所有标签正确匹配背景。 在游戏模式下,您将看到以下内容:
著作权:。
这个地方标有一个十字架:怪物的位置
怪物只能放置在标有
x的点上 。
要将它们添加到场景中,
请从“
项目浏览器” 中将“图像\对象\ Openspot”拖动到“
场景”窗口中。 虽然这个职位对我们并不重要。
在层次结构中选择
Openspot后,在
检查器中单击“
添加组件” ,然后选择
Box Collider 2D 。 在“场景”窗口中,Unity将显示带有绿线的矩形对撞机。 我们将使用此对撞机识别此位置上的鼠标单击。
以相同的方式将
Audio \ Audio Source组件添加到
Openspot 。 对于“音频源”组件的
AudioClip参数,选择位于“
音频”文件夹中的
tower_place文件,并禁用
“唤醒时播放” 。
我们需要再创建11个点。 尽管有重复这些步骤的诱惑,但Unity还是有一个更好的解决方案:
Prefab !
将
Openspot从
层次结构拖动到
项目浏览器内的
Prefabs文件夹中。 其名称在层次结构中将变为蓝色,这表示它已连接到预制件。 像这样:
现在有了预制坯,我们可以创建任意数量的副本。 只需将
Openspot从
Project 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时放置怪物:
当您单击鼠标或触摸屏幕时,此代码将定位怪物。 他如何工作?
- 当玩家触摸物理对撞机GameObject时,Unity会自动调用OnMouseUp。
- 调用时,如果
CanPlaceMonster()
返回true
,则此方法放置怪物。 - 我们使用
Instantiate
方法创建一个怪物,该方法创建具有指定位置和旋转的给定预制实例。 在这种情况下,我们复制monsterPrefab
,为它提供当前monsterPrefab
的位置并且不旋转,将结果传输到GameObject
并将其保存到GameObject
- 最后,我们调用
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 。
接下来,为每个级别设置
成本 :
- 元素0 : 200
- 元素1 : 110
- 元素2 : 120
现在,我们分配视觉显示字段的值。
在项目浏览器中展开
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
:
相当大的C#代码块,对不对? 让我们按顺序进行:
- 设置私有变量
currentLevel
的属性 。 通过设置属性,我们可以像调用其他任何变量一样调用它:作为CurrentLevel
(在类内部)或monster.CurrentLevel
(在外部)。 我们可以在属性的getter或setter方法中定义任何行为,并且通过仅创建getter,setter或这两者,我们可以控制属性的属性:只读,只写和写/读。 - 在getter中,我们返回
currentLevel
的值。 - 在设置器中,我们为
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()) {
我们使用
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
的价值。 塞特犬更有趣。 除了设置变量的值外,它还设置
goldLabel
的
text
字段以显示新的黄金值。
我们会多么慷慨? 将以下行添加到
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。
您可以像这样使敌人移动:
- 设定敌人要走的路
- 沿着道路移动敌人
- 转向敌人,以便他向前看
从航点创建道路
右键单击“
层次结构”,然后选择“
创建空白”以创建一个新的空白游戏对象。 将其命名为
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预制件相同,并具有
AudioSource
和
Sprite
子代,将来我们可以旋转此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()
:
让我们逐步分析代码:
- 从路线点数组中,我们可以获得当前路线段的起点和终点。
- 我们使用公式time = distance / speed来计算覆盖整个距离所需的时间 ,然后确定路线上的当前时间。 使用
Vector2.Lerp
,我们在开始和结束精确段之间插入敌人的当前位置。 - 检查敌人是否到达
endPosition
。 如果是,那么我们将处理两种可能的情况:
- 敌人尚未到达路线的最后一点,因此增加
currentWaypoint
的值并更新lastWaypointSwitchTime
。 稍后,我们将添加代码以使敌人转身,以便他朝自己的运动方向看。 - 敌人已到达路线的最后一点,然后我们将其摧毁并开始音效。 稍后,我们将添加一条代码,以减少玩家的
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
存储对
敌人 testEnemyPrefab
的
testEnemyPrefab
。
要在运行脚本时创建敌人,请将以下代码添加到
Start()
:
Instantiate(testEnemyPrefab).GetComponent<MoveEnemy>().waypoints = waypoints;
因此,我们将创建一个存储在
testEnemy
中的预制件的新副本,并为其分配路由。
保存文件并返回到Unity。 在“
层次结构”中选择
Road对象,然后为“
测试敌人”参数选择“
敌人”预制件。
启动该项目,并观察敌人如何沿道路行驶(在GIF中,为清楚起见,速度提高了20倍)。注意到他并不总是看他要去哪里?这很有趣,但是我们正在尝试制作专业游戏。因此,在本教程的第二部分中,我们将教导敌人前瞻。接下来要去哪里?
我们已经做了很多工作,并且正在迅速创建自己的塔防游戏。玩家可以创建数量有限的怪物,敌人沿着马路奔向我们的cookie。玩家拥有金币,他们可以升级怪物。从此处下载完成的结果。在第二部分中,我们将考虑制造巨大的敌人浪潮并将其消灭。待会见!