首页 > 编程语言 > 详细

使用Unity创建塔防游戏(Part3)—— 项目总结

时间:2017-02-07 01:01:44      阅读:293      评论:0      收藏:0      [点我收藏+]

  之前我们完成了使用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()中初始化小怪兽的数据。

 

 

碰撞体组件——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被调用      当敌人离开小怪兽的射程内时会被调用

 

项目中游戏信息的共享

  使用一个其他对象都能访问的共享对象来存储数据:GameManager,对应的类: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、为敌人定义移动的路线

    按照背景图中的路径,建立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、若敌人当前位置与终点路标的位置相同,则敌人抵达了最终路标。

       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、敌人头顶上的血条

    2、玩家的生命值

 

 

 

 

    

 

使用Unity创建塔防游戏(Part3)—— 项目总结

原文:http://www.cnblogs.com/lcxBlog/p/6364258.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!