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、旋转角度的问题:与旋转敌人角度的处理方式是相同的。

        

  • 相关阅读:
    启动php-fpm时报错
    安装php时,make test报错
    安装php时,make步骤报错make: *** [sapi/fpm/php-fpm] Error 1
    安装php时,make步骤报错make: *** [ext/gd/gd.lo] Error 1
    运行phpize时出现:Cannot find autoconf. Please check your autoconf installation
    Linux下安装php加速组件XCache
    加速器eaccelerator不兼容高版本php
    Apache下Worker模式MPM参数分析
    Linux SVN直接删除版本库文件
    Linux SVN一次增加多个文件并批量上传
  • 原文地址:https://www.cnblogs.com/lcxBlog/p/6364258.html
Copyright © 2011-2022 走看看