[转]Unity实战之塔防游戏(一)
塔防游戏非常受欢迎,也就不足为奇了。没有什么比观看防御消灭邪恶入侵者更令人满足的了!在这个分为两部分的教程中,您将使用Unity构建塔防游戏!
您将学习如何...
- 制造敌人之波
- 让他们跟随航点
- 建造和升级塔,并让它们将敌人减少到像素
最后,您将拥有一个可以扩展的流派框架!
注意:您需要了解Unity基础知识,例如如何添加游戏资产和组件,了解预制件以及一些基本C#。要学习这些东西,我建议您完成Sean Duffy的Unity教程或Brian Moakley的Unity入门C系列。
我使用的是OS X版本的Unity,但本教程也适用于Windows。
象牙塔的景色
在本教程中,您将构建一个塔防游戏,敌人(小虫子)爬向属于您和您的奴才(当然是怪物)的cookie!您可以在战略要点放置和升级怪物,以获得一点金。
玩家必须先杀死bug,然后才能享用您的cookie。一波又一波的敌人越来越难击败。当您在所有海浪中幸存下来(胜利!)或五个敌人到达cookie时,游戏结束。(打败!)。
这是完成游戏的屏幕截图:
入门
如果尚未安装Unity,请从Unity的网站下载。
另外,下载此启动程序项目,解压缩并在Unity中打开TowerDefense-Part1-Starter项目。
入门项目包括艺术和声音资产,以及预建的动画和一些有用的脚本。这些脚本与塔防游戏并不直接相关,因此在此不再赘述。但是,如果您想了解有关创建Unity 2D动画的更多信息,请查看此Unity 2D教程。
该项目还包含预制件,您稍后将对其进行扩展以创建角色。最后,该项目包括一个场景及其背景和用户界面。
打开“场景”文件夹中的GameScene,然后将“游戏”视图的纵横比设置为4:3,以确保标签与背景正确对齐。您应该在游戏视图中看到以下内容:**
学分:
- 该项目的艺术品来自Vicki Wenderlich提供的免费艺术品!您可以在gameartguppy上从她那里找到更多很棒的图形。
- 很酷的音乐来自BenSound,他的音乐很棒!
- 谢谢Michael Jasper带来的震撼的相机震动。
入门项目–检查!
资产–检查!
走向世界统治的第一步……嗯,我的意思是你的塔防游戏……完成了!
X标记位置:展示位置
怪物只能在带有x的位置张贴。
要将这些添加到场景中,请将Image \ Objects \ Openspot从项目浏览器拖放到“*场景”*视图中。目前,位置无关紧要。
随着Openspot在选定的层次,点击添加组件的检查和选择框撞机2D。Unity在“场景”视图中用绿线显示框对撞机。您将使用该对撞机检测该位置上的鼠标单击。
按照相同的步骤,将音频\音频源组件添加到Openspot。设定声音来源的音频剪辑到tower_place,您可以在找到的音频文件,并停用玩醒。
您需要再创建11个地点。尽管很想重复所有这些步骤,但Unity为此提供了一个很好的解决方案:预制件!
将Openspot从层次结构拖放到项目浏览器的Prefabs文件夹中。然后,其名称在层次结构中变为蓝色,以表明它已连接到预制件。像这样:**
现在有了预制件,您可以根据需要创建任意数量的副本。只需将Openspot从项目浏览器的Prefabs文件夹拖放到场景视图中。这样做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放置在Openspot上。
在项目浏览器中,在Prefabs文件夹中选择Openspot。在检查器中,单击“添加组件”,然后选择“新建脚本”并将其命名为PlaceMonster。选择C Sharp作为语言,然后点击创建和添加。由于您已将脚本添加到Openspot预制中,因此场景中的所有Openspot现在都已附加了脚本。整齐!******************
双击脚本以在IDE中将其打开。然后添加以下两个变量:
public GameObject monsterPrefab;
private GameObject monster;
您将实例化一个存储在其中的对象的副本monsterPrefab
以创建一个怪物,并将其存储在其中,monster
以便在游戏过程中对其进行操作。
每个位置一个怪物
添加以下方法,每个位置只允许一个怪物:
private bool CanPlaceMonster()
{
return monster == null;
}
在CanPlaceMonster()
您检查monster
变量是否仍然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: Deduct gold
}
}
此代码将怪物放置在鼠标单击或点击上。那么这是如何工作的呢?
OnMouseUp
当玩家点击GameObject的物理对撞机时,Unity会自动调用。- 当被调用时,这个方法的地方,如果一个新的怪物
CanPlaceMonster()
回报true
。 - 使用创建怪物
Instantiate
,该方法创建具有指定位置和旋转的给定预制实例。在这种情况下,您复制并monsterPrefab
赋予其当前GameObject的位置且不旋转,将结果转换为aGameObject
并将其存储在中monster
。 - 最后,您调用
PlayOneShot
以播放附加到对象AudioSource
组件的声音效果。
现在,您的PlaceMonster
脚本可以放置一个新的怪物,但是您仍然必须指定预制件。
使用正确的预制件
保存文件,然后切换回Unity。
要分配monsterPrefab变量,请首先在项目浏览器的Prefabs文件夹中选择Openspot。**
在“检查器”中,单击PlaceMonster(脚本)组件的Monster Prefab字段右侧的圆圈,然后从出现的对话框中选择Monster。
而已。单击或点击运行场景并在各个x点上构建怪物。
成功!您可以建造怪物。但是,它们看起来像个怪异的糊状,因为绘制了怪物的所有子精灵。接下来,您将解决此问题。
升级那些怪物
在下图中,您可以看到怪物在更高等级下看起来越来越恐怖。
它是如此蓬松!但是,如果您尝试窃取其Cookie,则该怪物可能会成为杀手。
脚本充当为怪物实施升级系统的基础。它跟踪怪物在每个级别上应该有多强大,当然还要跟踪怪物的当前级别。
立即添加此脚本。
在项目浏览器中选择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
多次特定对象的索引。尽管为此编写代码并不难,但是您将使用IndexOf()
,它实现的功能Lists
。这次无需重新发明轮子。:]
重新发明轮子通常是个坏主意(来自Michael Vroegop)
在MonsterData.cs的顶部,添加以下using
语句:
using System.Collections.Generic;
这使您可以访问通用数据结构,因此可以List<T>
在脚本中使用该类。
注意:泛型是C#的强大组成部分。它们允许您定义类型安全的数据结构而无需提交类型。这对于列表和集合之类的容器类很实用。要了解有关泛型的更多信息,请参阅《C#泛型介绍》。
现在添加以下变量MonsterData
来存储的列表MonsterLevel
:
public List<MonsterLevel> levels;;
使用泛型,可以确保levels
List
只能包含MonsterLevel
对象。
保存文件并切换到Unity以配置每个阶段。
在项目浏览器中选择Prefabs / Monster。在“检查器”中,您现在可以在MonsterData(脚本)组件中看到“色阶”字段。将其大小设置为3。**************
接下来,将每个级别的成本设置为以下值:
- 元素0:200
- 元素1:110
- 元素2:120
现在分配可视化字段值。
在项目浏览器中展开Prefabs / Monster,以便您可以查看其子级。将子Monster0拖放到Element 0的可视化字段。
重复分配Monster1到元1和Monster2到元2。请参阅以下GIF演示此过程:
当您选择Prefabs / Monster时,预制件应如下所示:
定义当前水平
在您的IDE中切换回MonsterData.cs,然后向中添加另一个变量MonsterData
。
private MonsterLevel currentLevel;
在私有变量中,currentLevel
您将存储……等待中……怪物的当前水平。我敢打赌,您没有看到一个来临:]
现在设置currentLevel
并使其可被其他脚本访问。将以下内容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#,是吗?顺其自然:
- 为私有变量定义一个属性
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
获得的索引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
。如果是这种情况,您可以从怪物的当前等级获得它MonsterData
。
然后,您测试是否有更高级别的可用,即何时GetNextLevel()
不返回null
。如果可以进行升级,则返回true
,否则返回false
。
启用黄金升级
要启用升级选项,请将else if
分支添加到OnMouseUp
:
if (CanPlaceMonster())
{
// Your code here stays the same as before
}
else if (CanUpgradeMonster())
{
monster.GetComponent<MonsterData>().IncreaseLevel();
AudioSource audioSource = gameObject.GetComponent<AudioSource>();
audioSource.PlayOneShot(audioSource.clip);
// TODO: Deduct gold
}
用检查是否可以升级CanUpgradeMonster()
。如果是,则MonsterData
使用GetComponent()
和调用访问组件IncreaseLevel()
,这会增加怪物的等级。最后,您触发怪物的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;
}
}
看起来很熟悉?类似于CurrentLevel
您在中定义的Monster
。首先,您创建一个私有变量gold
来存储当前的黄金总量。然后定义一个名为-creative的属性Gold
,对吗?-并实现一个getter和setter。
该获取器仅返回的值gold
。塞特犬更有趣。除了设置变量的值,它还将text
字段设置goldLabel
为显示新的黄金量。
您感觉如何?添加以下行以Start()
给玩家1000黄金,如果您感到不舒服,则少给黄金:
Gold = 1000;
将标签对象分配给脚本
保存文件并切换到Unity。
在层次结构中,选择GameManager。在检查器中,单击“金标”右边的圆圈。在“*选择文本”*对话框中,选择“*场景”*选项卡,然后选择“ GoldLabel”。
运行场景,标签显示Gold:1000。
检查玩家的“钱包”
在IDE中打开PlaceMonster.cs,并添加以下实例变量:
private GameManagerBehavior gameManager;
您将用于gameManager
访问GameManagerBehavior
场景的GameManager的组件。要分配它,请将以下内容添加到Start()
:
gameManager = GameObject.Find(“ GameManager”).GetComponent <GameManagerBehavior>();
您可以使用来获得名为GameManager的GameObject GameObject.Find()
,它会返回找到的具有给定名称的第一个游戏对象。然后,检索其GameManagerBehavior
组件并将其存储以备后用。
注意:您可以通过在Unity编辑器中设置字段,或向其中添加一个静态方法(GameManager
返回一个单例实例)来实现此目的,以实现此目的GameManagerBehavior
。
但是,上面的代码块中有一个黑马方法:Find
,它在运行时速度较慢,但方便且可以少量使用。
得到的钱!
您还没有扣除金,所以加入这一行两次内OnMouseUp()
,更换每一个阅读的评论// TODO: Deduct gold
:
gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
保存文件并切换到Unity,升级一些怪物并观看Gold读数更新。现在您可以扣除金币,但是只要有空间,玩家就可以建造怪物。他们只是负债累累。
无限信用?太棒了!但是你不能允许这个。只能在玩家拥有足够的金币时放置怪物。
需要金币来怪物
在您的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()
自己是否有足够的金币。
揭示
在Unity中保存并运行场景。继续吧,只需尝试放置无限的怪物!
塔政治:敌人,波浪和航路点
是时候为敌人“铺路”了。敌人出现在第一个航路点,移向下一个并重复直到到达您的Cookie。
您将通过以下方式使敌人前进:
- 为敌人定义一条道路
- 沿着道路移动敌人
- 旋转敌人,使其向前看
使用路标创建道路
右键单击“层次结构”,然后选择“创建空白”以创建一个新的空白游戏对象。将其命名为Road,并确保其位于位置(0,0,0)。
现在,右键单击道的层次结构并创建另一个空的游戏对象的道儿了。将其命名为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
和一个孩子Sprite
,它是一个精灵,因此您以后可以旋转它而不必旋转即将出现的运行状况栏。
沿着道路移动怪物
将名为MoveEnemy的新C#脚本添加到Prefabs \ Enemy预制中。在您的IDE中打开脚本,并添加以下变量:****
[HideInInspector]
public GameObject[] waypoints;
private int currentWaypoint = 0;
private float lastWaypointSwitchTime;
public float speed = 1.0f;
waypoints
将航路点的副本存储在数组中,而上述确保您不会意外更改检查器中的字段,但仍可以从其他脚本访问它。[HideIn<em>inspector</em>]``waypoints
****
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: Rotate into move direction
}
else
{
// 3.b
Destroy(gameObject);
AudioSource audioSource = gameObject.GetComponent<AudioSource>();
AudioSource.PlayClipAtPoint(audioSource.clip, transform.position);
// TODO: deduct health
}
}
一步步:
- 从Waypoints数组中,检索当前路径段的开始和结束位置。
- 使用公式time = distance / speed来计算整个距离所需的时间,然后确定路径上的当前时间。使用
Vector2.Lerp
,您可以在分段的开始位置和结束位置之间插入敌人的当前位置。 - 检查敌人是否到达了
endPosition
。如果是,请处理以下两种可能的情况:- 敌人尚未到达最后一个航路点,因此请增加
currentWaypoint
和更新lastWaypointSwitchTime
。稍后,您将添加代码以旋转敌人,使其也指向其移动的方向。 - 敌人到达了最后一个航路点,因此将其摧毁并触发了音效。稍后,您还将添加代码以减少播放器的
health
。
- 敌人尚未到达最后一个航路点,因此请增加
保存文件并切换到Unity。
给敌人一种方向感
在当前状态下,敌人不知道航路点的顺序。
在层次结构中选择Road,然后添加一个名为SpawnEnemy的新*C#脚本。然后在您的IDE中打开它,并添加以下变量:*****
public GameObject[] waypoints;
您将用来waypoints
以正确的顺序存储对场景中航路点的引用。
保存文件并切换到Unity。在层次结构中选择Road并将Waypoints数组的Size设置为6。********
将每个Road的子级拖到字段中,将Waypoint0放入Element 0,将Waypoint1放入Element 1,依此类推。
现在,您有了一个包含整齐排列的航路点的数组,因此有了一条路径–请注意,它们永远不会后退;他们会死于试图解决糖问题。
检查一切正常
在您的IDE中转到SpawnEnemy,并添加以下变量:
public GameObject testEnemyPrefab;
这使该参考敌人的预制testEnemyPrefab
。
要在脚本启动时创建敌人,请将以下代码添加到Start()
:
Instantiate(testEnemyPrefab).GetComponent<MoveEnemy>().waypoints = waypoints;
这将实例化存储在其中的预制件的新副本,testEnemy
并为其指定要遵循的航路点。
保存文件并切换到Unity。在层次结构中选择“道路” ,并将其“测试敌人”设置为“敌人”预制件。**
运行项目,看看敌人在路上。
您是否注意到他们并不总是在寻找他们要去的地方?滑稽!但是,您正在尝试成为一名专业人士,对吗?继续第二部分,学习如何使他们表现出最好的面孔。
然后去哪儿?
您已经完成了很多工作,并且在拥有自己的塔防游戏的途中也步入正轨。
玩家可以建造怪物,但数量不限,而且有一个敌人冲向您的cookie。玩家有金币,也可以升级怪物。
在此处下载结果。
在第二部分中,您将介绍产生大量敌人并将其吹走的浪潮。在第二部分见!
标题:[转]Unity实战之塔防游戏(一)
作者:shirln
地址:https://www.mmzsblog.cn/articles/2020/10/09/1602235884982.html
如未加特殊说明,文章均为原创,转载必须注明出处。均采用CC BY-SA 4.0 协议!
本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。若本站转载文章遗漏了原文链接,请及时告知,我们将做删除处理!文章观点不代表本网站立场,如需处理请联系首页客服。• 网站转载须在文章起始位置标注作者及原文连接,否则保留追究法律责任的权利。
• 公众号转载请联系网站首页的微信号申请白名单!