使用Unity创建塔防游戏(Part1)

 How to Create a Tower Defense Game in Unity - Part1

原文作者:Barbara Reichart   

文章原译:http://www.cnblogs.com/le0zh/p/create-tower-defense-game-unity-part-1.html

参考了这篇文章,我打算做一些改进,以及翻译这篇文章的第2部分。如有不恰当的地方,欢迎各位指正。

 塔防游戏极为流行,没有什么能比看着自己的防御塔消灭邪恶的入侵者更爽的事了。

你将会学习到

  • 创建一波一波的敌人
  • 令敌人沿着路标移动
  • 创建和升级防御塔,消灭你的敌人们。

最后,你会有一个此类型的游戏框架,你可以在此基础上自行扩展!

小贴士:你必须具备Unity的基础知识,例如如何添加游戏资源和组件,理解预设体(prefabs)以及一些C#的编程基础。想要学习这些东西的朋友可以参考Chris LaPolloUnity教程。本文作者所使用的UnityOS X版本的,但本教程也适用于Windows版本的Unity

最终效果

在本教程中,你将创建一个塔防游戏,敌人们(小虫子)会朝你的饼干移动,它们还会遇到你的帮手们——一些小怪兽。为了消灭这些入侵者,你可以在一些战略点上,消耗一些金币来放置和升级你的小怪兽。

消灭那些小虫子,别让它们碰你的饼干!敌人会随着波数的增加而变得更强大。游戏将在玩家活到最后(胜利),或者有5个敌人抵达饼干后结束(失败)。

下面是一张完成的游戏截图:

快召唤你的小怪兽们!保护你的饼干啊!

准备开始

如果你还没有安装Unity5,请前往Unity的商店下载。

同时,请下载starter项目,解压并使用Unity打开 TowerDefense-Part1-Starter 这个工程。

starter项目中包括了美术和声音资源,同时还有预设的动画以及一些帮助用的脚本,这些脚本跟塔防游戏没有直接的关系,所以不会再本教程中详细介绍。但是如果你想要更多的学习关于unity 2d动画的创建,请参考Unity 2D 教程

项目中同时包含了一些 prefab供你稍后扩展用来创建游戏角色。最后,工程中包含背景和UI设置好的场景。

Scenes文件夹中找到并打开GameScene, 设置Game视图的显示比例为4:3来保证labels能够正确的在背景中对齐,你在Game视图中看到的应该如下所示:

鸣谢:

  • 工程中用到的美术资源是由Vicki Wenderlich提供的免费资源包。在这里——gameartguppy,你可以找到更多她的作品,都是棒棒哒!
  • 工程中的音乐资源是由 BenSound 提供的,他拥有不少美妙的配乐!
  • 感谢给力的 Michael Jasper 提供了关于 camera shake 的资料。

Starter工程、资源都已经到位了!

征服世界,额,这个应该是指你的塔防游戏的第一步,已经准备好了。

放置X标记点

只能在标记有 X 的地方召唤小怪兽。

先从Project Brower中依次选择Image、Objects,将Openspot.png拖动到场景中。目前,随便放哪都行。

在Hierarchy视图中选中Openspot,在Inspector面板中点击 Add Componet 并且依次选择 Physics 2D\Box Collider 2D。Unity将会在场景中以绿色的线来显示Box Collider 2D。这个碰撞器用来检测在某个点的鼠标点击。

如上图所示,Unity能够为碰撞器量体裁衣。这是不是很酷?

跟刚才一样,把一个Audio\Audio Source组件添加到 Openspot上。在Inspector面板中设置Audio Source的属性,将AudioClip设置为 Audio 文件夹中的 tower_place ,不要勾选 Play On Awake 。

你还需要创建11个这样的召唤点,每个都得重复上面的步骤,不过不用担心,Unity有个妙招:Prefab !

将 Openspot 从 Hierarchy视图 拖动到位于 Project视图的 Prefabs 文件夹中。在 Hierarchy视图 中它的名字变成了蓝色,用来表示它是和一个prefab相关联的。如下图所示:

现在,你拥有了一个prefab,你就可以创建任意多的拷贝。简单的将OpenspotPrefabs文件夹中拖拽到场景中,再重复11次,一共在场景中创建12个召唤点。

现在,我们需要在Inspector面板中设置这12个召唤点的坐标,数据如下:

  • (-5.2, 3.5, 0)
  • (-2.2, 3.5, 0)
  • (0.8, 3.5, 0)
  • (3.8, 3.5, 0)
  • (-3.8, 0.4, 0)
  • (-0.8, 0.4, 0)
  • (2.2, 0.4, 0)
  • (5.2, 0.4, 0)
  • (-5.2, -3.0, 0)
  • (-2.2, -3.0, 0)
  • (0.8, -3.0, 0)
  • (3.8, -3.0, 0)

做好后,你的场景是这个样子的:

召唤小怪兽(放置防御塔)

为了简化放置怪兽的工作,工程的Prefab文件夹下包含了一个名为 Monster 的 prefab。

这就是工程中用到的名为Monster的 prefab 。

现在,它包含了一个空的游戏对象,由三种不同的精灵组成,和它们各自的射击动画。

每一个小精灵代表了小怪兽不同的能力级别。这个prefab中也包含了一个音频组件(Audio Source component),当小怪兽发射激光的时候,就会播放此音效。

现在该创建一个脚本来控制在召唤点(Openspot)上召唤小怪兽。

在Project 视图中选中 Prefab文件夹下的 Openspot。在Inspector 面板中,单击 Add Component ,选择 New Script ,将它命名为 PlaceMonster。脚本语言选择 C Sharp ,在点击 Create and Add。刚才我们为 Openspot prefab 添加了脚本组件后,场景中所有的 Openspot 都会拥有各自的脚本组件。真棒!

Inspector面板中双击刚才创建的脚本,用VS打开。注意,请双击下图红圈部分打开脚本。

往里面添加这两个变量:

    public GameObject monsterPrefab;
    private GameObject monster;

你将使用monsterPrefab中的对象实例化一个拷贝来创建的一个小怪兽,然后保存在monster变量中,方便之后的操作。

一个萝卜一个坑

现在让我们添加一个方法,令一个召唤点只能召唤一只小怪兽。

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

这个方法先判断 monster这个变量的值是否为null。如果是的,这个召唤点是没有小怪兽的,我们可以在此召唤一只。

再添加如下代码,来执行当玩家点击召唤点后,召唤一只小怪兽:

// 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
        }
}    

上面的代码在玩家点击召唤点的时候,就在上面召唤一只小怪兽,那么这是怎么执行的呢?

  1. 当玩家在点击了一个游戏对象的碰撞器时,Unity就会自动调用 OnMouseUp 方法。
  2. 当 OnMouseUp 方法被调用的时候,先判断这个点能否召唤小怪兽,在 canPlaceMonster() 返回true的情况下就可以。
  3. 调用 Instantiate方法来创建一只小怪兽对象,这个方法会根据指定的prefab、指定的位置(position)和旋转角度(rotation)创建一个对象。在本例中,我们拷贝了 monsterPrefab ,并指定位置为当前游戏对象(召唤点)的位置,不指定旋转角度,然后将方法的返回值强转为GameObject类型,并且存储在monster这个变量中。
  4. 最后,调用 PlayOneShot 方法播放附加在召唤点上声音特效。

现在, PlaceMonster脚本可以处理召唤小怪兽了,但我们还需要为此脚本中指定prefab。

 

使用正确的 Prefab

先保存脚本,在回到Unity中。

下面给变量monsterPrefab赋值,首先在Project面板,Prefabs文件夹中选中OpenSpot。

然后,在Inspector面板中,点击PlaceMonster (Script)组件的Monster Prefab属性右边的小圆圈按钮,然后在弹出来的对话框中选择Monster

现在开始游戏,在X标记上点击来召唤一些小怪兽。

成功啦!现在我们可以召唤小怪兽了。但它们看起来像一团浆糊,因为小怪兽所有的形态都叠在一起了。这个问题在下一步中解决。

升级你的小怪兽

在下面的图片中,我们看到小怪兽在不同的等级有不同的外观。

 

这些小家伙是不是萌萌哒!但如果有谁想偷吃饼干的话,它们就会痛下杀手。

我们需要编写一个脚本来管理小怪兽升级的功能。我们需要跟踪管理怪物在各个级别的能力大小,当然还有怪物所处的当前等级。

现在让我们来添加这个脚本。

Project面板中选中Prefabs下的Monster这个Prefab,为它添加一个C#脚本组件,命名为MonsterData。在VS中打开它,然后将下面的代码添加到MonsterData类的上方。

[System.Serializable]  
public class MonsterLevel
{
    public int cost; //召唤怪物所消耗的金币
    public GameObject visualization;    //怪物在某个特定等级的视觉效果
}

这里定义了一个MonsterLevel类型,包含了费用(金币,这个后面再说)以及对于某个特定等级的视觉效果。

我们添加了[System.Serializable]这个特性来使这个类的对象可以在Inspector面板中编辑。这可以使我们方便快速的改变MonsterLevel中的值,甚至在游戏运行过程中。这在调节游戏平衡性的时候特别的有用。

 

设定怪物的等级

我们将预先定义的MonsterLevel存储在List<T>中。

为什么不简单的使用数组MonsterLevel[]呢?首先我们会经常用到某个特定MonsterLevel对象的下标,当然如果使用数组编写一点代码来做这件事也不是特别困难。我们可以直接使用List对象的IndexOf()方法,没有必要重新发明轮子了这次 :

重新发明轮子可不是一个好主意。

MonsterData.cs 文件的顶部添加下面的引用:

using System.Collections.Generic;

这将允许我们使用泛型的数据结构,所以可以在代码中使用List<T>。

小贴士:泛型是C#中的一个很有用的功能,这允许我们定义类型安全的数据结构,并且不闲鱼实际数据类型。涉及到泛型的常用的容器类有ListSet。如果你学习更多关于泛型的知识,请参阅Introduction to C# Generics

接下来添加下面的变量到MonsterData类,用来存储MonsterLevel的列表。

public List<MonsterLevel> levels;

使用泛型,可以保证levels List 只能存放MonsterLevel 类型的对象。

保存好脚本文件,返回Unity中设置小怪兽的等级数据。

Proejct面板中,选中Prefabs文件夹下的Monster,然后在Inspector面板中,我们可以在MonsterData(脚本组件),可以看到Levels属性,设置size为3。

接下来,设置每个等级的花费如下:

  • Element 0: 200
  • Element 1: 110
  • Element 2: 120

接下来给visualization这个变量赋值。

 在Project视图中展开Prefabs/Monster来查看其子节点。拖拽子节点Monster0visualization属性的Element 0

重复上面的动作Monster1Element 1Monster2Element 2,参考下面动图中的演示:

当我们选中Prefabs/MonsterInspector面板中的显示如下:(现在,我们配置好了小怪兽的等级数据)

定义当前的等级

切换回MonsterData.cs中,向MonsterData类中添加另外一个变量:

private MonsterLevel currentLevel;

在私有变量currentLevel中,我们存放怪物当前的等级信息。

现在我们需要让其他脚本能够调用这个变量,写一个属性对外使用。

     // 1
     public MonsterLevel CurrentLevel 
    {
        //  2
        get 
        {
            return currentLevel;
        }
        //  3
        set 
        {
            currentLevel = value;
            int currentLevelIndex = levels.IndexOf(currentLevel);

            //根据currentLevelIndex的值,来决定小怪兽的形态
            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. 在setter方法中,给currentLevel赋值。先拿到当前等级的下标,然后遍历levels数组,依据currentLevelIndex的值来决定小怪兽的形态,好处就在于不管什么时候谁设置了currentLevel,精灵会自动更新。属性确实很好用!

添加下面的OnEnable的一个实现:

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

这里设置了CurrentLevel的默认值,确保它只显示正确的那个精灵图片。

注意:  

OnEnable中而不是OnStart中初始化属性的值,原因是:当prefabs被实例化时,脚本中几个自带方法调用次序的问题,OnEnable会在Start之前被调用。

OnEnable会在Unity创建小怪兽的prefab时立即被调用,而Start会等到小怪兽对象作为场景的一部分的时候才会被调用。我们能够确定的是,先有小怪兽的prefab,才会有小怪兽的对象。
小怪兽被召唤出来之前,我们需要确定它的有关数据:如等级、召唤它耗费的金币等等,于是在召唤一只小怪兽之前就要先把这些数值设置好,所以选择在OnEnable() 方法中初始化。

  注意OnEnable中的大小写,如果大小写不对,方法不会被调用!

     返回到Unity中,运行项目并且召唤一些小怪兽,现在它们外观显示正确的,也就是最低等级的形态,如下图:

升级小怪兽

返回到代码编辑器,增加下面的方法到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];
        }
    }

这里我们获取当前等级的下标,然后确保它不会大于最高等级的下标,如果不大于,则将CurrentLevel设置为下一个等级。

测试是否可以升级

保存刚才的脚本文件,返回到PlaceMonster.cs文件,增加下面的方法:

    private bool canUpdateMonster() 
    {
        if (monster != null)
        {
            MonsterData monsterData = monster.GetComponent<MonsterData>();
            MonsterLevel nextLevel = monsterData.getNextLevel();
            if (null != nextLevel) //更高的等级存在
            {
                return true;
            }
        }
        return false;
    }

首先,检查monster变量是否为null,如果为null则肯定不能升级了,如果不为null则获取其MonsterData组件,并检查更高的等级是否存在,如果getNextLevel方法返回的值不是null则说明更高的等级存在(返回true),否则不存在(返回false)。

为了能够升级,在PlaceMonster中的OnMouseUp中添加else if分支:

    void OnMouseUp() 
    {
        if (canPlaceMonster()) 
        {
            // ... 这里根原来的代码一样,先省略
        }
        else if (canUpdateMonster()) 
        {
            monster.GetComponent<MonsterData>().increaseLevel();
            AudioSource audioSource = gameObject.GetComponent<AudioSource>();
            audioSource.PlayOneShot(audioSource.clip);
        }
    }

首先我们通过canUpdateMonster检查能否升级,如果可以升级,则通过调用MonsterData组件的increaseLevel方法升级怪物,最后播放一次声音特效。

保存好脚本,回到Unity中。运行游戏,现在我们可以召唤小怪兽并升级它们了。

支付金币 – Game Manager 

现在,我们可以召唤和升级任意数目的小怪兽,但这样用来,就木有挑战了。

我们接下来就处理金币的问题,为例维护金币的信息,你不得不要在不同的游戏对象之间共享数据信息。
下面的图片显示了所有需要金币信息的游戏对象:

上图中被标记的游戏对象都需要知道玩家拥有的金币数量。

我们将使用一个其他对象都能访问的共享对象来存储这类数据。

在Hierarchy面板中,右键选择Create Empty,并命名为GameManager

添加一个C#脚本组件GameManagerBehavior到刚刚创建的GameManager上,然后打开并编辑这个脚本。

因为我们需要使用一个label显示玩家所拥有的金币数,所以在文件的顶部增加下面的引用:

using UnityEngine.UI;

这将允许我们使用UI相关的类型,比如Text用做显示用的label,接下来在类中添加下面的变量:

public Text goldLabel;

这个变量存储了对Text组件的引用,将用于显示玩家所拥有的所有金币数。

现在GameManager已经可以操作label了,但是我们如何保证变量中存储的金币数和label显示的数量同步呢,我们创建一个属性:

private int gold;

public int Gold
{
    get { return gold; }
    set
    {
        gold = value;
        goldLabel.GetComponent<Text>().text = "GOLD: " + gold;
    }
}

是不是看起来很熟悉,这个跟我们在Monster中定义的CurrentLevel比较相像,首先我们创建一个私有的变量gold用来存储当前所有的金币数,然后定义一个名为Gold的属性,并提供getter和setter方法。

在getter方法中简单的直接返回gold,setter方法则比较有趣了,除了设置gold的值,还设置了goldLabel的显示。
这样就保持了金币数和显示的同步。

Start()中增加下面的初始化语句,默认给玩家1000金币(当然你可以给更少的)

Gold = 1000;

给脚本中的Label对象赋值

保存脚本文件并返回到Unity中。

在 Hierarchy面板中,选中GameManager 对象。在Inspector面板,点击GoldLabel右边的圆圈按钮,在Select Text对话框中,选中Scene标签页,并选中GoldLabel。

运行游戏,可以看到金币的显示如下:

检查玩家的钱包

打开PlaceMonster.cs文件,增加下面这行代码:

private GameManagerBehavior gameManager;

我们将通过变量gameManager来访问场景中GameManager的GameManagerBehavior组件,并在Start方法中初始化:

void Start ()
{
    gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
}

我们使用GameObject.Find方法,找到名为GameManager的游戏对象,然后定位到其GameManagerBehavior组件并存到一个私有变量中,供稍后使用。

注意:你可以在Unity中为gameManager这个变量赋值,或者是添加一个静态方法,它返回一个GameMmanagerBehavior 类型的单例实例。在上面的代码块中,我们使用了Find 这个黑马方法,虽然它的执行过程耗时,但它却很方便,不需要频繁地调用。

收钱

我们还没有减少金币数,所以在OnMouseUp() 方法里,将原来的TODO注释改为下面的代码:

//todo: Deduct gold
gameManager.Gold -= monster.GetComponent<MonsterData>().CurrentLevel.cost;

注意是两个地方,在放置和升级的逻辑里各有一处,在if 和else if分支里面都各添加一处。

保存文件并返回unity,升级一些小怪兽并注意观察金币数目的变化。现在我们能够减少金币数了,但是。。。玩家可以一直召唤小怪兽(只要有空位),金币数甚至可以变成负数。

这显然是不允许的!只有当玩家有足够金币时,才能召唤和升级小怪兽。

花在小怪兽上的金币

切换到PlaceMonster.cs脚本,更新canPlaceMonstercanUpdateMonster方法如下,就是加上检查剩余金币是否足够的条件。

private bool canPlaceMonster()
{
    int cost = monsterPrefab.GetComponent<MonsterData>().levels[0].cost;
    return monster == null && gameManager.Gold >= cost; //确保金币足够
}

private bool canUpdateMonster()
{
    if (monster != null)
    {
        MonsterData monsterData = monster.GetComponent<MonsterData>();
        MonsterLevel nextLevel = monsterData.getNextLevel();
        if (nextLevel != null)
        {
            return gameManager.Gold >= nextLevel.cost; //确保金币足够
        }
    }
    return false;
}

保存,并运行游戏,试试还能不能无限添加怪物。

现在我们召唤和升级小怪兽的数目取决于金币的数量。

塔防游戏的要素:敌人、波数和路标

是时候给敌人“铺路”了。敌人首先在第一个路标的地方出现,然后向下一个路标移动并重复这个动作,直到他们抵达你的饼干。
我们将通过下面的手段使敌人行军起来:

    1. 定义敌人移动的路线
    2. 使敌人沿着路线移动
    3. 旋转敌人,使他们看起来是向前方行进

通过路标建立路线

  在Hierarcy视图中右击空白处,选择Create Empty创建一个新的空游戏对象,命名为Road,并确保其坐标为 (0, 0 , 0) 。接下来,右击Road并创建一个空游戏对象,命名为Waypoint0,设置其坐标为 (-12, 2 ,0) ,这是敌人开始进攻的起点。

    

按照相同的方法再创建5个路标:

  • Waypoint1: (7, 2, 0)
  • Waypoint2: (7, -1, 0)
  • Waypoint3: (-7, 3, 0)
  • Waypoint4: (-7.3, -4.5, 0)
  • Waypoint5: (7, -4.5, 0)

下面的截图标示出了路标的位置以及最终的路线:

召唤敌人

  现在我们该创建一些敌人沿着刚才设定的路线前进。在Prefabs的文件夹中包含了一个Enemy的prefab。 它的位置坐标是(-20,0,0) ,所以新创建的敌人对象一开始在游戏界面的外面。

  跟Monster的prefab一样,Enemy的prefab同样包含了一个AudioSource,一个精灵图片(一会儿可以旋转其方向,并且不必旋转敌人头上的血条)。

    

令敌人沿着路线移动

  为Prefabs文件夹下的 Enemy prefab 添加一个名为MoveEnemy的C#脚本组件,使用VS打开,并添加下面的变量定义:

    [HideInInspector]
    public GameObject[] waypoints; //所有的路标
    private int currentWaypoint = 0; //敌人当前所在的路标
    private float lastWaypointSwitchTime; //敌人经过上一个路标的时刻
    public float speed = 1.0f;  //敌人的移动速度

  waypoints以数组的形式存储了所有的路标,它上面的HideInInspector特性确保了我们不会在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 = Vector3.Lerp(startPosition, endPosition, currentTimeOnPath / totalTimeForPath);
    
        // 3 检查敌人是否已经抵达结束路标
        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);

                //TODO: deduct health
            }
        }

让我们一步一步来看:

  1. 从路标数组中,取出当前路段的开始路标和结束路标。
  2. 计算出通过整个路段所需要的时间totalTimeForPath(使用 距离除以速度 的公式),计算出敌人在当前路段行进的时间currentTimeOnPath,使用Vector3.Lerp插值,通过currentTimeOnPath / totalTimeForPath 计算出当前时刻应该在的位置。
  3. 检查敌人是否已经抵达结束路标,如果是,则有两种可能的场景:
    A. 敌人尚未抵达最终的路标,所以增加currentWayPoint并更新lastWaypointSwitchTime,稍后我们要增加旋转敌人的代码使他们朝向前进的方向。
    B. 敌人抵达了最终的路标,就销毁敌人对象,并触发声音特效,稍后我们要增加减少玩家生命值的代码。

保存文件,并返回到Unity。

给敌人指明方向

  现在,敌人还不知道路标的次序。

  为Hierarchy视图中的Road对象添加一个命名为SpawnEnemy的C#脚本组件,用VS打开,添加下面的变量:

    public GameObject[] waypoints;  //存储路标

  游戏中路标的引用按照合理的顺序存储在waypoints这个数组中。

  保存好脚本,返回Unity中。选中Hierarchy视图中的Road对象,在Inspector面板中,将Waypoints数组的Size设置为6。拖拽Road的孩子节点到想用的Element位置,Waypoint0对应Element0以此类推。如下图所示:

  

  现在我们已经有了路线的路标数组,注意到敌人不会退缩,为了你那块甜饼,它们不畏死亡。

  

  你们这些不怕死的家伙,尽管来吧,看我的小怪兽们如何消灭你们。

检查一切顺利

  打开SpawnEnemy脚本,添加下面的变量:

    public GameObject testEnemyPrefab; //保存对Enemyprefab的引用

  ·使用下面的代码,当脚本开始时添加一个敌人:

    // Use this for initialization
    void Start () {
        //实例化一个敌人对象,并将路标告诉敌人
        Instantiate(testEnemyPrefab).GetComponent<MoveEnemy>().waypoints = waypoints;
    }

  上面的代码使用testEnemyPrefab实例化一个敌人对象,并将路标赋值给它。

  保存文件返回Unity,在Hierarchy视图中选中Road对象并将Enemy prefab赋值给testEnemyPrefab。
  运行游戏,可以看到敌人已经能够沿着路线移动了:

      

  nice,但是有没有注意到敌人移动的时候没有朝向移动的方向。。没有关系,这个问题将在part2部分修复。

小结:

  为了完成自制的塔防游戏,我们已经很好的完成了不少的工作。

  玩家可以召唤小怪兽,但数量有限;敌人会朝着你的饼干前进;玩家拥有金币,可以用来升级小怪兽。

  在第二部分,我们的主要工作:创建数波大量的敌人,并且消灭它们。

 

posted @ 2017-01-13 23:49  苏打兴  阅读(4652)  评论(2编辑  收藏  举报