使用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 = ; //敌人当前所在的路标
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)—— 项目总结的更多相关文章
- 使用Unity创建塔防游戏(Part2)
How to Create a Tower Defense Game in Unity – Part 2 原文地址:https://www.raywenderlich.com/107529/unity ...
- 使用Unity创建塔防游戏(Part1)
How to Create a Tower Defense Game in Unity - Part1 原文作者:Barbara Reichart 文章原译:http://www.cnblogs.co ...
- 使用unity创建塔防游戏(原译)(part1)
塔防游戏非常地受欢迎,木有什么能比看着自己的防御毁灭邪恶的入侵者更爽的事了. 在这个包含两部分的教程中,你将使用Unity创建一个塔防游戏. 你将会学到如何: 创建一波一波的敌人 使敌人随着路标移动 ...
- Unity塔防游戏的创建
看了下塔防游戏的教程,比我想像的还简单一些,有些收获: (1)敌人的移动路径,其时比较简单,用了N个Empty GameObject作为路径点,然后做一个总的Empty GameObject 作为父级 ...
- Unity简单塔防游戏的开发——敌人移动路径的创建及移动
软件工程综合实践专题第一次作业 Unity呢是目前一款比较火热的三维.二维动画以及游戏的开发引擎,我也由于一些原因开始接触并喜爱上了这款开发引擎,下面呢是我在学习该引擎开发小项目时编写的一些代码的脚本 ...
- [译]终极塔防——运用HTML5从头创建一个塔防游戏
翻译共享一篇CodeProject的高星力作,原文地址:http://www.codeproject.com/Articles/737238/Ultimate-Tower-Defense 下载演示项目 ...
- Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(一)
一.前提: 完成Hello Game项目的创建编译. 具体参考:Cocos2dx.3x_Hello Game项目创建篇 二.本篇目标: l 说说关于塔防游戏的想法和思路 l 实现一个简单的塔防游戏 ...
- Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(三)
一.前提: 完成前一篇的内容. 具体参考:Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(二)篇 二.本篇目标: l 说说游戏中各种角色的动作.属性以及重构思路 l 进行代码重构让色狼大叔和 ...
- Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(二)
一.前提: 完成前一篇的内容. 具体参考:Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(一)篇 二.本篇目标: l 说说关于cocos2dx手机分辨率适配 l 对前一篇完成的塔防游戏原型进 ...
随机推荐
- webpack打包静态资源和动态资源
1.对于静态引用的资源: <img src = "static/modelname/imgname.png"> // 修改为下面的写法 <img src = &q ...
- nmon监控及简要分析
性能测试中,各个服务器资源占用统计分析是一个很重要的组成部分,通常我们使用nmon这个工具来进行监控以及监控结果输出. 一. 在监控阶段使用类似下面的命令 ./nmon -f write_3s_20v ...
- 带你走进ajax(4)
处理ajax返回数据类型 ajax返回数据类型:纯文本格式.xml.json 如果只获取简单的字符串可以采用纯文本格式. 如果返回的数据类型比较复杂,则采用xml或者json. 采用XML来处理数据 ...
- USB Transfer and Packet Sizes
https://msdn.microsoft.com/en-us/library/ff538112.aspx http://blog.csdn.net/chenyujing1234/article/d ...
- mysql 手动安装和管理
版本:5.7.10 my.ini简单配置 [client] default-character-set=utf8 [mysqld] port = 3306 basedir =D:/programs/M ...
- Centos75 解决Nginx出现403 forbidden(13: Permission denied)
Centos75 新安装的vm,nginx出现403 forbidden 一般为SELinux设置为开启状态(enabled)的原因 切为root ,执行: sed -i 's/SELINUX=enf ...
- 20145310《Java程序设计》第2次实验报告
20145310<Java程序设计>第2次实验报告 实验内容 初步掌握单元测试和TDD 理解并掌握面向对象三要素:封装.继承.多态 初步掌握UML建模 熟悉S.O.L.I.D原则 了解设计 ...
- linux 用户态和内核态以及进程上下文、中断上下文 内核空间用户空间理解
1.特权级 Intel x86架构的cpu一共有0-4四个特权级,0级最高,3级最低,ARM架构也有不同的特权级,硬件上在执行每条指令时都会对指令所具有的特权级做相应的检查.硬件已经提 ...
- Linux入门之--Linux文件系统结构
对于每一个Linux学习者来说,了解Linux文件系统的目录结构,是学好Linux的至关重要的一步.,深入了解linux文件目录结构的标准和每个目录的详细功能,对于我们用好linux系统只管重要,下面 ...
- Hive压缩格式
TextFile Hive数据表的默认格式,存储方式:行存储. 可使用Gzip,Bzip2等压缩算法压缩,压缩后的文件不支持split 但在反序列化过程中,必须逐个字符判断是不是分隔符和行结束符,因此 ...