zoukankan      html  css  js  c++  java
  • 使用Unity创建塔防游戏(Part3)—— 项目总结

      之前我们完成了使用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 = 0; //敌人当前所在的路标
            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 -= 1;

      

    碰撞体组件——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) {
        // 2
            if (other.gameObject.tag.Equals("Enemy")){
                enemiesInRange.Add(other.gameObject);
                EnemyDestructionDelegate del =
                    other.gameObject.GetComponent<EnemyDestructionDelegate>();
                del.enemyDelegate += OnEnemyDestroy;
            }
        }
        // 3
        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、旋转角度的问题:与旋转敌人角度的处理方式是相同的。

        

  • 相关阅读:
    Azure PowerShell (2) 修改Azure订阅名称
    Windows Azure Platform Introduction (11) 了解Org ID、Windows Azure订阅、账户
    Azure PowerShell (3) 上传证书
    Azure PowerShell (1) PowerShell入门
    Windows Azure Service Bus (2) 队列(Queue)入门
    Windows Azure Service Bus (1) 基础
    Windows Azure Cloud Service (10) Role的生命周期
    Windows Azure Cloud Service (36) 在Azure Cloud Service配置SSL证书
    Android studio 使用心得(一)—android studio快速掌握快捷键
    android 签名、混淆打包
  • 原文地址:https://www.cnblogs.com/lcxBlog/p/6364258.html
Copyright © 2011-2022 走看看