之前我们完成了使用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、玩家的生命值
原文:http://www.cnblogs.com/lcxBlog/p/6364258.html