之前我们完成了使用Unity创建塔防游戏这个小项目,在这篇文章里,我们对项目中学习到的知识进行一次总结。

  Part1的地址:http://www.cnblogs.com/lcxBlog/p/6075984.html

  Part2的地址:http://www.cnblogs.com/lcxBlog/p/6185330.html

  首先,在我们开展这个项目之前,必须具备Unity的基础知识,例如如何添加游戏资源和组件,理解预设体(prefabs)以及一些C#的编程基础。可以点击Chris LaPollo的Unity教程来学习这些基础知识。

  不论是做2D游戏还是3D游戏,搭建好游戏场景是第一步,由于在starter工程中已经包含了背景和UI设置好的场景,所以我们只需要在这基础之上进行即可。

  为Game视图设置合适的显示比例,可以保证场景中的Lable(标签)能够正确对齐。

prefab

  快速创建prefab的方法:将游戏对象从Hierarchy视图拖拽到Project视图。

  将Project视图中的prefab拖拽到场景视图中,就能以此prefab创建出一个游戏对象来,重复多次就能创建多个这样的对象了。

  为脚本中的prefab对象赋值,将prefab从Project视图拖拽到Inspector视图。

  假如我们为prefab添加了一个游戏组件(例如,脚本、刚体、碰撞体等),那么场景中所有以此prefab创建的对象都会拥有这个游戏组件。

  快速复制prefab:传统的Ctrl + C,Ctrl + V不可行,Unity提供了快捷键 Ctrl + D,即Duplicate命令。选中prefab后,按下Ctrl + D即可。同理,也可以用于其他类型资源的复制。

  项目中遇到的BUG:小怪兽的所有形态都叠在一起,原因:当一个prefab下有多个子sprite时,若未指定显示哪个子sprite,则当游戏对象被创建出来后,所有的sprite都会被显示出来。解决办法:在创建游戏对象的时候,指定要显示的sprite。

脚本中数据初始化

  通常我们在Start() 中进行数据初始化,但考虑脚本中方法执行顺序的问题,有些操作必须放在Start()之前的方法(例如,Awake()、OnEnable() )中做。注意:这些方法名称的大小写必须正确,否则不会被调用。执行顺序:Awake() ——》OnEnable()  ——》Start() 。

  项目中运用的地方:脚本MonsterData属于Monster对象,在OnEnable()中初始化小怪兽的数据,因为OnEnable()会在Unity创建小怪兽的prefab时,立即被调用;Start()需要等到小怪兽对象作为场景的一部分时才会被调用;所以在小怪兽作为场景的一部分之前,我们需要设置好有关的数据;最终得到结论,在OnEnable()中初始化小怪兽的数据。

项目中游戏信息的共享

  使用一个其他对象都能访问的共享对象来存储数据:GameManager,选择Create Empty来创建这样的一个游戏对象。对应的类:GameManagerBehavior,这个类里面管理的信息包括:金币、波数(第X波敌人)、游戏是否结束、玩家的生命值。

  以一个public的bool 变量 gameOver来表示游戏是否结束,其他信息则都有各自对应的属性,这些属性的getter方法都很简单,只是返回字段的值而已,Setter方法除了设置字段的值,还做了不少其他的操作,例如设置Label的显示,播放相关的动画等。

  C#中的属性

   对应一个私有字段,它是对外使用的,在项目中用于信息的共享。

  在类的内部进行取值操作的时候,如果没有特殊要求,尽量使用字段,直接取值一步到位。

  赋值的选择:对属性赋值,还是对字段赋值? 取决于我们的目的,是一次单纯的赋值,还是要调用Setter方法做更多的操作。 项目中出现的BUG:对字段进行赋值,召唤小怪兽后,小怪兽所有的形态都叠在一起了;因为Setter方法中指定了小怪兽的当前形态。

  这个项目中,我们用到的属性的getter方法都很简单,只是返回字段的值而已;setter方法中做的操作可以看作一个小函数。同样是扣除玩家100金币,gameManager.Gold -= 100; 和  gameManager.DeductPlayerGold(100);  都能做到,但很明显前者显得更简洁,我们不必为函数起名而烦恼了。

项目中用到的特性

  1、System.Serializable

    在C#中主要用于将一个对象序列化,在Unity中主要作用是使一个数据类型出现在Inspector中。这个数据类型必须是C#基本的数据类型(这里不只是C#,其他Unity能够识别的编程语言也可以,如JS),或者是Unity3D对象,另外再加上以这些可识别的对象构建的自定义数据类型(如类、结构体等)。注意:我们必须将访问权限设置为public。

    这样做的好处——用于调节游戏的平衡性:我们可以在游戏运行时随时更改数据,并且在游戏中立即生效,停止运行后各属性又能恢复到最初的状态。这是Unity3D提供的一种运行时调试方式。

        [System.Serializable]
public class MonsterLevel
{
public int cost; //召唤小怪兽所消耗的金币
public GameObject visualization; //小怪兽在某个特定等级的外观
public GameObject bullet;
public float fireRate;
}

    Inspector中,我们可以查看MonsterLevel这个类的所有public成员,修改它们的数值。

  2、HideInspector

    与上面的System.Serializable作用相反,可以确保某个数据类型不会出现在Inspector中,这些数据类型往往不希望在Inspector中被修改,但仍然可以在其他脚本中访问它们。

    在下面的代码中,HideInspector只对waypoints起作用,但被private修饰的currentWaypoint和lastWaypointSwitchTime也不会出现在Inspector中。

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

出场率较高的方法

   1、实例化游戏对象的方法 Static Instantiate()

    它的返回值是Object类型,所以它可以克隆任何物体,包括脚本。

    Instantiate(original : Object) : Object,等同于复制命令(duplicate,即Ctrl + D),只是对原物体进行复制,不指定position和rotation。

    Instantiate(original : Object, position : Vector3, rotation : Quaternion) : Object,等同于复制命令(duplicate),对原物体进行复制,还指定了position和rotation。

    这个方法有多个重载,在项目中,我们要选择合适的重载来完成功能。

   2、获取游戏对象组件的方法 GetComponet(type: Type) : Componet

    如果这个游戏对象包含一个类型为type的组件,则返回该组件;如果没有则为空。我们通过这个方法访问内建的组件或者脚本组件。调用方式举例:

    //保持金币数和显示的同步
goldLable.GetComponet<Text>().text = "GOLD" + gold; //播放游戏结束的动画
gameOverText.GetComponent<Animator>().SetBool("gameOver", true);

    获取子物体组件的方法 GetComponetInChildren(type: Type) : Componet

      返回这个游戏物体或者它的所有子物体上(深度优先)的类型为type的组件,只返回活动组件(Only active components are returned)。调用方式举例:

    monsterData = gameObject.GetComponentInChildren<MonsterData>();

  3、查找游戏对象组件的方法 static Function Find(name: string) : GameObject

    Find()方法执行过程是较耗时,所以尽量不要在每一帧中使用它,例如不要在Update()中调用它。

    为游戏对象添加标签:为敌人对象添加标签Enemy。在Project视图中,选中名为Enemy的prefab。在Inspector面板的顶部,点击Tag右边的下拉框,从弹出的对话框中选择Add Tag

    

    点击下图中的 + ,新建一个标签,命名为Enemy。选中Enemy prefab,将它的标签属性设置为Enemy。

      

   通过对游戏对象添加Tags(标签)来区别于其他游戏对象,在脚本中可以通过标签名快速查找游戏对象。调用的方法:static Function FindGameObjectWithTag(name: string) : GameObject

   在项目中是如何运用的:为了便于判断场景中是否还有敌人存在  GameObject.FindGameObjectWithTag("Enemy") == null

项目中的难点1:

创建塔防游戏里的敌人

  1、单波敌人的信息

    在大部分塔防游戏中,每一波敌人的数量、外观、能力都不完全相同,在一波敌人都是一个一个出现的(植物大战僵尸,一大群僵尸一起出现)。于是我们需要配置每一波敌人的信息有:敌人的外观、数量、每隔多少秒出现下一个敌人。这些数据可以写在一个序列化的类Wave里面,这样我们可以在Inspector面板中更改它的数据。然后,再议Wave[] waves 这个数组来存储每一波敌人的信息。我们在Inspector面板中设置好waves的长度,为数组的每一个元素都赋值。

  2、把一波敌人创建出来

    对应的脚本为 SpawnEnemy.cs。

    要点:1、游戏未结束,且满足创建敌人的条件,就要不停地创建敌人,敌人是一个一个被创建出来的,所以在创建一个敌人后,必须隔spawnInterval秒才能创建下一个敌人。

         2、这波敌人中,已被创建出来的敌人有多少个enemiesSpawned ;创建上一个敌人的时间 lastSpawnTime,在Start() 中将它设置为 Time.time。

         3、同一时刻,场景中只能有一波敌人  4、给玩家留一些时间来准备(放置新的防御塔,升级防御塔),于是在第一波敌人出现之前 或者 第N波敌人全部被消灭时,不要马上创建第N+1波敌人。于是我们设置 timeBetweenWaves = 5;  5秒钟后,才会开始出现下一波敌人。

         5、当某一波敌人被全部消灭时,为创建下一波敌人做准备,再给予玩家一些金币奖励   6、若所有敌人都被消灭,就要播放游戏胜利的动画

    实现:1、判断是否还有下一波敌人,若没有的话,游戏结束,玩家胜利;  int currentWave = gameManager.Wave;  if (currentWave < waves.Length)

       2、创建单个敌人。  计算出距离创建上一个敌人过去了多少时间,timeInterval = Time.time - lastSpawnTime

           前提:enemiesSpawned < 这波敌人的总数量 。只要满足以下两个条件之一,就可以创建。

          条件1:已创建的敌人数量 为0,因为要留给玩家一些准备时间,所以还须满足 timeInterval > timeBetweenWaves ,创建第1个敌人的时候不必考虑spawnInterval的问题 。

          条件2:timeInterval > spawnInterval,这个条件表示已经在场景中创建了X个敌人,且到了可以创建下一个敌人的时间。

         创建出某个敌人后,enemiesSpawned++

         3、表示玩家消灭了一波敌人: enemiesSpawned 等于 这波敌人的总数量  并且  场景中没有一个敌人对象。

         为创建下一波敌人做准备:gameManager.Wave++     enemiesSpawned = 0     lastSpawnTime = Time.time

项目中的难点2:

让敌人沿着你设定的路线移动

   1、为敌人定义移动的路线

    按照背景图中的路径,建立6个Waypoint路标,游戏中敌人是沿着直线移动的,我们将路标设置在起点、终点、4个拐点上。

    如下图所示,起点路标是在游戏场景之外,敌人的初始位置是在起点路标上,终点路标在我们的饼干上。

    

   2、让敌人沿着路线移动

    这里我们要先设置好敌人的移动速度。

    要点:1、敌人是沿着直线移动的,是一种缓动效果。   2、只要敌人没有被消灭,它们就会一直朝着饼干移动

        3、敌人的初始位置在路标0,游戏开始不久后,敌人处在路标0和路标1之间;当敌人经过了路标1后,它的处于路标1和路标2之间。于是,我们得到结论:敌人所处的位置必然在 [路标X , 路标X+ 1] 这个区间里,我们需要记录敌人已经通过的路标——路标X,以及敌人经过此路标的时间(游戏开始时敌人在路标0,所以敌人经过路标0的时间为当前时间)。

        4、当敌人移动后,需判断它是否抵达了终点路标。A、未抵达,则敌人已通过的路标变为路标X+1,敌人经过进过路标X+1的时间为当前时间,旋转敌人让敌人朝着饼干前进;B、抵达了终点路标,销毁敌人对象,减少玩家的血量。 

    实现:1、实现缓动效果的方法:Vector3.Lerp(startPosition, endPosition, currentTimeOnPath / totalTimeForPath),计算出某个时刻敌人所处的位置。 startPosition 路标X所在的位置,endPostion 路标X+1所在的位置;totalTimeForPath表示敌人从路标X走到路标X+1所需的时间;由于敌人在路标X的时间lastTime是已知的,所以我们可以计算出currentTimeOnPath = 当前时间 - lastTime ; currentTimeOnPath / totalTimeForPath 就可以表示敌人走完路程的百分比。 最后,Lerp返回值类型为Vector3,即为敌人当前所处的位置。

         2、敌人移动的代码放在Update()中。

               3、若敌人当前位置与终点路标的位置相同,则敌人抵达了最终路标。此时需要扣减玩家的血量,我们只需要gameManager.Health -= 1;  即可

       4、当敌人抵达一个新的路标(非终点路标)时,旋转敌人,让敌人看起来有方向感。将敌人对象围绕Z旋转,让敌人沿着路线前进。此处是本项目中一个不易理解的地方。

        A、敌人前进的方向发生了改变,所以我们要先计算出敌人新的前进方向。Vector3 newDirection = (newEndposition - newStartPosition); 我们要让敌人沿着newDirection所指的方向前进。

        B、敌人要旋转的角度就是新的前进方向和旧的前进方向之间的夹角,我们要计算出这个角度。float rotationAngle = Mathf.Atan2(newDirection.y ,newDirection. x ) * 180 / Mathf.PI;   Mathf.Atan2的返回结果是弧度,需要将它 *180 / Math.PI 转化为弧度。

        C、在2D的塔防游戏中,敌人头顶上的血条都始终保持水平,所以敌人头顶上的血条没有必要旋转,我们只旋转敌人的子对象——Sprite。    GameObject sprite = (GameObject)gameObject.transform.FindChild("Sprite").gameObject;  sprite.transform.rotation = Quaternion.AngleAxis(rotationAngle , Vector3.forward);

   

游戏中的生命值

   1、敌人头顶上的血条

    思路:A、用两张图片来显示,一张是暗的,表示背景图;另一张是绿色较小的细长图片,表示前景图。通过缩放前景图的长度,来匹配敌人当前血量。

       B、设置好两张图片的属性

         C、为前景图添加一个脚本,用来调整它的缩放长度

    如何为敌人添加血条:

      A、将Enemy prefab 拖拽到场景中,现在Hierarchy视图中出现了一个名为Enemy的对象。

      B、将Image HealthBarBackground 拖拽到Enemy对象上,作为Enemy的子对象。

      C、将Image HealthBar 的Pivot设置为Left,因为血条的缩减是从右到左的;将HealthBar的X scale设置为125,把它拉长,令它的长度不小于HealthBarBackground

      D、为HealthBar添加一个C#脚本,命名为HealthBar.cs

      E、Enemy对象的初始位置是在场景之外的,于是需要将它的坐标设置为(20, 0, 0)

      F、点击Inspector面板顶部的Apply按钮,保存对prefab的更改。删除Hierarchy视图中的Enemy对象。

      

    反向思考:删掉敌人头顶上的血条?例如:将Enemy2的血条删掉。(实质问题:删掉prefab下的某个、某些Sprite

      选中与为敌人添加血条的过程相似:将Enemy prefab拖拽到场景中,然后依次删除Enemy对象下的两个Sprite,最后Inspector面板顶部的Apply按钮,保存对prefab的更改。删除Hierarchy视图中的Enemy对象。

      不启用敌人头顶上的血条?

      

      取消上图的勾勾,只是不启用HealthBarBackground 这个Sprite而已,当我们想要用到它的时候,勾上这个勾勾即可。不启用的效果如下图所示:

      

    在脚本中缩放血条的长度

     要点: A、敌人刚出现的时候,都是满血的,我们需要记录敌人的最大生命值、当前生命值、血条图片缩放的长度——X Scale。

          B、在Start()方法中,设置血条图片缩放的长度

             C、敌人在移动过程中遭到攻击,血量会减少,我们需要在Update()方法中缩放血条的长度

      实现: A、用2个public类型的变量来记录敌人的最生命值 maxHealth 和 敌人当前的生命值 currentHealth。用一个private类型的变量 originalScale 来记录血条图片缩放的长度——X Scale。

          B、在Start() 中写: originalScale = gameObject.transform.localScale.x;

          C、用一个临时变量tmpScale获取localScale的值,然后为tmpScale.X赋值,最后将tmpScale赋给localScale 。

                   void Update () 
           {
        Vector3 tmpScale = gameObject.transform.localScale;
        tmpScale.x = currentHealth / maxHealth * originalScale;
        gameObject.transform.localScale = tmpScale;
         }

      以上代码不能简写成:  gameObject.transform.localScale.x = currentHealth / maxHealth * originalScale;

     因为编译器会报错,提示:” 不能修改 UnityEngine.Transform.localScale 的返回值,因为它不是变量“。

  2、玩家的生命值

     在GameManagerBehavior.cs 中管理玩家的血量。

    要点:A、以一个Text healthLabel来显示玩家的血量;为了让游戏更有趣些,GameObject[] healthIndicator 数组用来表示5只正在啃饼干的小虫子,当玩家血量减1的时候,就隐藏一只小虫子。

      B、玩家血量减到0的时候,需要结束游戏,播放游戏失败的动画。

      C、以一个属性Helath来管理玩家的血量,处理血量变化的代码都放在Setter方法中。

      D、需要削减玩家血量的时候,只要写出如下简洁的代码即可:

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

  

碰撞体组件——Collider 2D

   我们需要根据,物体的形状和游戏需求来选择合适形状的碰撞体,这个组件在项目中发挥了两个作用:

  1、检测在某个点的鼠标点击

  在鼠标点击召唤点的时候,就可以在上面放置防御塔(就是我们的小怪兽啦)或者对防御塔进行升级。 为召唤点Openspot添加一个Box  Collider 2D,看矩形的碰撞体最适合。

  

  响应鼠标点击的方法:OnMouseUp(),在鼠标点击了一个游戏对象的碰撞体时,Unity会自动调用这个方法。这个方法的大小写不可写错,否则不会被调用。

  2、用于触发事件

  令小怪兽能够检测到在它射程内的敌人,在添加碰撞体的时候,我们需要做一些适当的设置。

  A、为Monster prefab添加一个Circle Collider 2D组件,一个2D圆形碰撞体组件。

  为什用Circle,而不是上面的Box?使用Circle可以很好地展示小怪兽的攻击范围(以它为圆心的一个圆形区域),它的半径就是小怪兽的射程。

  启用Is Trigger这个属性,目的是令此碰撞体用于触发事件,并且不会发生任何物理交互。如果不启用这个属性的话,就是会发生碰撞。我们希望触发的事件——当敌人进入小怪兽的射程中时,小怪兽立即对它开火。

  因为小怪兽被放置在召唤点的上方,所以必须防止小怪兽的Circle Collider 2D响应鼠标点击——应该由召唤点来响应;否则,会造成召唤小怪兽后,无法对其进行升级。在Inspector面板中,将Layer属性设置为Ignore Raycast,然后在弹出的对话框中选择Yes,change children。这样,小怪兽的Circle Collider 2D就不会响应鼠标点击了。

  

  B、为Enemy prefab添加一个Rigid Body 2D组件(刚体)和一个Circle Collider 2D组件。

  当两个碰撞体发生碰撞的时候,至少要有一个附带刚体组件,才会触发碰撞事件。而我们希望触发的碰撞事件为:Enemy的碰撞体和Monster的碰撞体互相碰撞时所触发的碰撞事件。

  勾选刚体的Is Kinematic属性,这是为了令敌人对象不受Unity中的物理引擎影响。

  将的Circle Collider 2D组件半径设置为1。

  C、响应碰撞事件的方法

  void OnTriggerEnter2D(Collider2D other)   当碰撞体other进入触发器时OnTriggerEnter2D被调用    当敌人进入小怪兽的射程内时会被调用

  void OnTriggerExit2D(Collider2D other)      当碰撞体other离开触发器时OnTriggerExit2D被调用      当敌人移动到小怪兽的射程外时会被调用

项目中的难点3:

让小怪兽们追踪射程内的敌人  

  为Enemy prefab添加一个脚本组件——EnemyDestructionDelegate.cs,这个脚本包含了一个委托 void EnemyDelegate(GameObject enemy);

  为Monster prefab添加一个脚本组件,命名为ShootEnemies.cs。

  思路:1、以一个List集合——enemiesInRange 来存储某个小怪兽攻击范围内所有的敌人。这个List初始是空的。每个小怪兽对象都有一个这样的List,一个敌人可能会在多个小怪兽的射程内。

       2、当敌人进入射程内时,将此敌人添加到这个List中;当敌人移动到射程外 或者 敌人被消灭 时,将此敌人从这个List中移除。   

     3、由于我们无法得知Unity什么时候会调用OnTriggerEnter2D和OnTriggerExit2D这两个方法,于是我们需要灵活地添加、移除敌人对象。而委托可以让一个游戏对象灵活地通知另一个游戏对象做出改变。

  实现:1、这个List里面储存的类型是GameObject类型,为什么不是Enemy类型?因为游戏中的敌人不止Enemy这一种,还有Enemy2等等。

       2、写一个方法:当敌人被消灭的时候移除enemiesInRange 中的某个对象   void OnEnemyDestroy(GameObject enemy);

      3、当敌人进入射程时,我们需要将OnEnemyDestroy添加到委托EnemyDestructionDelegate的方法列表中;当敌人移动到射程外时,我们需要将OnEnemyDestroy从委托EnemyDestructionDelegate的方法列表中移除。

     以下是三个方法的实现:

    void OnEnemyDestroy(GameObject enemy) {
enemiesInRange.Remove(enemy);
} void OnTriggerEnter2D(Collider2D other) {
//
if (other.gameObject.tag.Equals("Enemy")){
enemiesInRange.Add(other.gameObject);
EnemyDestructionDelegate del =
other.gameObject.GetComponent<EnemyDestructionDelegate>();
del.enemyDelegate += OnEnemyDestroy;
}
}
//
void OnTriggerExit2D(Collider2D other) {
if (other.gameObject.tag.Equals("Enemy")){
enemiesInRange.Remove(other.gameObject);
EnemyDestructionDelegate del =
other.gameObject.GetComponent<EnemyDestructionDelegate>();
del.enemyDelegate -= OnEnemyDestroy;
}
}

项目中的难点4:

小怪兽的子弹

  要点:A、子弹也是由prefab初始化出来的游戏对象,带有一个脚本BulletBehavior.cs来处理子弹的行为。

       B、子弹的坐标设置:由于本项目是个2D游戏,所以我们可以事先设置好子弹的Z坐标。而子弹产生的位置是不确定的,于是我们只能在子弹产生的时候设置它的X、Y坐标。

     C、子弹的飞行速度、子弹的攻击力 这两个数据可以配置的。子弹的初始位置、子弹的目标(小怪兽要攻击的敌人)、目标所处的位置,这3个数据在子弹对象产生的时候才能确定下来。

       D、与敌人移动的方式一样,子弹的飞行也是一种缓动效果,只不过比敌人移动得更快而已。

     E、子弹击中敌人后,如果敌人被消灭,需要给予玩家金币奖励。

       F、每一种子弹对应以一个等级的小怪兽,小怪兽的等级越高,子弹的攻击力越强。

  实现:1、子弹产生的时间startTime = Time.time ,用于实现子弹的缓动效果;在Start()中计算出子弹与目标间的距离;获取GameManagerBehavior的实例,用于给予玩家金币奖励。

     2、子弹产生后就会朝着目标飞过去,与处理敌人移动的逻辑相同,都是放在Update()中。

     3、计算子弹的当前位置: A、子弹飞行了多长时间 timeInterval = Time.time - startTime;

      B、还是使用Lerp来计算,gameObject.transform.position = Vector3.Lerp(startPosition, targetPosition, timeInterval * speed / distance);

     4、当子弹的位置与目标的位置相同时,子弹击中了敌人。若敌人已不存在(它已被其他小怪兽消灭了);若敌人存在,则按子弹的攻击力削减敌人的生命值。最后子弹消失(销毁子弹这个游戏对象)

     5、子弹击中敌人后,若敌人的生命值被削减至0或0以下,则该敌人被消灭,玩家获得一些金币奖励。

     6、子弹和小怪兽的对应关系需要在MonsterData.cs里进行配置。在Inspector面板中,展开Monster Data脚本组件中的Levels数组,设置好每一项数据。

项目中的难点5:

小怪兽的攻击对象 

  每个小怪兽都有一个射程内的敌人List,但我们的小怪兽每次只能攻击一个敌人(你可以在此之上拓展,做出有AOE能力的小怪兽),所以必须确定对哪个敌人开火。其实答案很简单,对距离饼干最近的敌人开火。这部分的逻辑写在ShootEnemies.cs这个脚本里。

  1、找出距离饼干最近的敌人    如何找出这样的敌人是关键点!

    思路:A、MoveEnemy.cs这个脚本要提供一个方法:计算敌人与饼干之间的距离

        B、计算List中每一个敌人与饼干的距离,游戏的每一帧中都需要找出距离饼干最近的敌人。

          C、通过寻找最小数的算法找到距离饼干最近的敌人,

    实现:A、计算出敌人尚未走完的路程distance有多长。任何时候敌人都处于[ 路标X,路标X+1 ] 这个区间内。我们先计算出敌人当前位置与路标X+1之间的距离;然后通过循环累加路标X+1与路标X+2的距离,一直累加到路标X+N与终点路标的距离。将这些距离都累加起来,就可以得出敌人与饼干之间的距离了。

       B、在Update()中遍历enemiesInRange,计算出每一个敌人与饼干之间的距离distanceToGoal。

       C、临时变量 minimalEnemyDistance = float.MaxValue; 确保不会有比它更大的距离。 若 distanceToGoal < minimalEnemyDistance ,  目标被暂定为这个敌人,minimalEnemyDistance = distanceToGoaL 。当循环结束的时候,我们就找出了距离饼干最近的敌人。

         D、假如List是空的,那么这个循环不会执行,小怪兽就没有开火的目标了。

  2、攻击这个敌人

     只要这个敌人仍然存在场景中,我们就要攻击它。

    思路:A、因为每个等级的小怪兽都有自己的发射率(如3秒发射一次,2秒发射一次),所以小怪兽必须是间歇性地发射子弹。这一点与创建敌人的方式是相同的,需要记录上一次发射子弹的时间。

       B、写一个void Shoot(target)方法,处理射击的逻辑

       C、旋转小怪兽的角度,让它能够对着敌人开火(如果你做出了能够AOE的防御塔,可以不必旋转它)。

     实现:A、计算 当前时间 与 上一次射击时间 的差值,若大于 小怪兽的当前等级的发射率,则小怪兽可以继续发射子弹,上一次射击时间更新为当前的时间。

        B、 分为以下3个步骤: 1、获取小怪兽当前等级的子弹的prefab,子弹的初始坐标startPosition与小怪兽的坐标相同,目标的坐标targetPostion就是target的坐标了。但startPosition.z和targetPostion.z必须设置为bulletPrefab的Z坐标。

         2、实例化一个子弹对象,设置好它的位置、初始位置、目标所在位置。

         3、播放一个射击的动画和一个射击的音效。

        C、旋转角度的问题:与旋转敌人角度的处理方式是相同的。

    

使用Unity创建塔防游戏(Part3)—— 项目总结的更多相关文章

  1. 使用Unity创建塔防游戏(Part2)

    How to Create a Tower Defense Game in Unity – Part 2 原文地址:https://www.raywenderlich.com/107529/unity ...

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

    How to Create a Tower Defense Game in Unity - Part1 原文作者:Barbara Reichart 文章原译:http://www.cnblogs.co ...

  3. 使用unity创建塔防游戏(原译)(part1)

    塔防游戏非常地受欢迎,木有什么能比看着自己的防御毁灭邪恶的入侵者更爽的事了. 在这个包含两部分的教程中,你将使用Unity创建一个塔防游戏. 你将会学到如何: 创建一波一波的敌人 使敌人随着路标移动 ...

  4. Unity塔防游戏的创建

    看了下塔防游戏的教程,比我想像的还简单一些,有些收获: (1)敌人的移动路径,其时比较简单,用了N个Empty GameObject作为路径点,然后做一个总的Empty GameObject 作为父级 ...

  5. Unity简单塔防游戏的开发——敌人移动路径的创建及移动

    软件工程综合实践专题第一次作业 Unity呢是目前一款比较火热的三维.二维动画以及游戏的开发引擎,我也由于一些原因开始接触并喜爱上了这款开发引擎,下面呢是我在学习该引擎开发小项目时编写的一些代码的脚本 ...

  6. [译]终极塔防——运用HTML5从头创建一个塔防游戏

    翻译共享一篇CodeProject的高星力作,原文地址:http://www.codeproject.com/Articles/737238/Ultimate-Tower-Defense 下载演示项目 ...

  7. Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(一)

    一.前提: 完成Hello Game项目的创建编译. 具体参考:Cocos2dx.3x_Hello Game项目创建篇 二.本篇目标: l  说说关于塔防游戏的想法和思路 l  实现一个简单的塔防游戏 ...

  8. Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(三)

    一.前提: 完成前一篇的内容. 具体参考:Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(二)篇 二.本篇目标: l  说说游戏中各种角色的动作.属性以及重构思路 l  进行代码重构让色狼大叔和 ...

  9. Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(二)

    一.前提: 完成前一篇的内容. 具体参考:Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(一)篇 二.本篇目标: l  说说关于cocos2dx手机分辨率适配 l  对前一篇完成的塔防游戏原型进 ...

随机推荐

  1. webpack打包静态资源和动态资源

    1.对于静态引用的资源: <img src = "static/modelname/imgname.png"> // 修改为下面的写法 <img src = &q ...

  2. nmon监控及简要分析

    性能测试中,各个服务器资源占用统计分析是一个很重要的组成部分,通常我们使用nmon这个工具来进行监控以及监控结果输出. 一. 在监控阶段使用类似下面的命令 ./nmon -f write_3s_20v ...

  3. 带你走进ajax(4)

    处理ajax返回数据类型 ajax返回数据类型:纯文本格式.xml.json 如果只获取简单的字符串可以采用纯文本格式. 如果返回的数据类型比较复杂,则采用xml或者json. 采用XML来处理数据 ...

  4. USB Transfer and Packet Sizes

    https://msdn.microsoft.com/en-us/library/ff538112.aspx http://blog.csdn.net/chenyujing1234/article/d ...

  5. mysql 手动安装和管理

    版本:5.7.10 my.ini简单配置 [client] default-character-set=utf8 [mysqld] port = 3306 basedir =D:/programs/M ...

  6. Centos75 解决Nginx出现403 forbidden(13: Permission denied)

    Centos75 新安装的vm,nginx出现403 forbidden 一般为SELinux设置为开启状态(enabled)的原因 切为root ,执行: sed -i 's/SELINUX=enf ...

  7. 20145310《Java程序设计》第2次实验报告

    20145310<Java程序设计>第2次实验报告 实验内容 初步掌握单元测试和TDD 理解并掌握面向对象三要素:封装.继承.多态 初步掌握UML建模 熟悉S.O.L.I.D原则 了解设计 ...

  8. linux 用户态和内核态以及进程上下文、中断上下文 内核空间用户空间理解

    1.特权级         Intel x86架构的cpu一共有0-4四个特权级,0级最高,3级最低,ARM架构也有不同的特权级,硬件上在执行每条指令时都会对指令所具有的特权级做相应的检查.硬件已经提 ...

  9. Linux入门之--Linux文件系统结构

    对于每一个Linux学习者来说,了解Linux文件系统的目录结构,是学好Linux的至关重要的一步.,深入了解linux文件目录结构的标准和每个目录的详细功能,对于我们用好linux系统只管重要,下面 ...

  10. Hive压缩格式

    TextFile Hive数据表的默认格式,存储方式:行存储. 可使用Gzip,Bzip2等压缩算法压缩,压缩后的文件不支持split 但在反序列化过程中,必须逐个字符判断是不是分隔符和行结束符,因此 ...