- 优化列表
- 第一课:制作预制体、材质
- 第二课:制作地图
- 第三课:制作路径
- 第四课:摄像机
- 第五课:配置路点
- 第六课:创建敌人,并且移动。
- 第七课:创建敌人孵化器
- 第八课:改进敌人的生成策略
- 第九课:创建三种炮台的Prefab
- 第十课:创建炮台选择的UI
- 第十一课:创建炮台的数据类(保存炮台数据)
- 第十二课:监听炮台选择的事件(保存选择的炮台)
- 第十三课:检测点击哪个MapCube
- 第十四课:检查是否足够可以创建炮塔
- 第十五课:资源管理(金钱消费和更新UI)
- 第十六课:钱不够,制作动画提示效果
- 第十七课:炮台的实例化
- 第十八课:做特效
- 第十九课:炮台搜敌
- 第二十课:制作子弹
- 第二十一课:子弹飞行
- 第二十二课:子弹碰撞盒
- 第二十三课:创建爆炸特效
- 第二十四课:fixbug修复敌人销毁时,子弹销毁
- 第二十五课:fixbug修复敌人销毁时,炮台的引用
- 第二十六课:敌人死亡时,销毁特效
- 第二十七课:手动配置一下所有敌人的销毁特效
- 第二十八课:控制炮管指向敌人射击子弹
- 第二十九课:
- 第三十一课:敌人添加血条
- 第三十二课:制作一个ui
- 第三十三课:给按钮添加动画
- 第三十四课:给按钮添加动画
- 第三十五课:给按钮添加动画
- 第三十六课:给升级面板添加动画
- 第三十七课:处理升级和销毁的点击
- 第三十八课:处理升级和销毁
- 第三十九课:升级特效、销毁特效
- 第四十课:2级标准炮塔
- 第四十一课:火箭弹炮塔
- 第四十二课:激光炮塔
- 第四十三课:激光炮塔的发射(LineRenderer)
- 第四十四课:激光伤害和伤害特效
- 第四十五课:2级激光炮塔
- 第四十六课:炮塔消耗
- 第四十七课:游戏结束时候的UI
- 炮塔攻击的时候, 先隐藏一下炮弹这个节点。更像是炮弹发射出去的感觉。
- Unity的Layout采用Tall布局。
- Project面板采用One Column Layout。
- 在Project中创建Scenes文件夹,存放主场景MainScene以及后续的副本场景。
- 在Project中创建Material文件夹,存放各种材质。
- 在Hierarchy中创建空物体,重命名为Plane作为整个游戏的地表。
- 在Material文件夹中,创建一个Plane的材质,赋予给主场景中的Plane,调节Plane材质颜色即可修改地表的颜色,我这里调整设置为金黄色的地表。
- 地面有点儿反光,调节Plane材质的属性smoothness,设置为0或者1,我选择设置为1,地面就不会出现反光现象。
- 在Inspector窗口去掉主场景中Plane的Mesh Collider组件,因为该塔防游戏,我们不处理碰撞检测。
- 新建Prefab文件夹,存放后续的各种预制体。新建一个cube,调整合适的大小、高度,从Hierarchy窗口拖到Project窗口下,就可以把cube设置为一个prefab预制体。
预制体简介:可以复用,在游戏运行时,可以重复创建。
这里把这个cube作为一格地板,后续会点击该cube,在地板上面制造炮台。整个地图大小是
30*15,总计450个地板(这个可以自己调整修改大小),地板铺在地表上面。
| 图1. Uity设置 创建地表 地表材质 |
|---|
- 将上述的cube命名为MapCube,在场景中会创建很多的MapCube。新建一个空物体命名为Map,用来挂载管理所有的MapCube。将所有的地块MapCube拖放到Map层级下。这样通过在世界中查找Map节点,就可以获取到所有的MapCube地块。
- ctrl+d复制1个MapCube,因为MapCube大小是4*4,两个cube之间计划设置为间隔1m距离。
- 按住ctrl键点击cube拖动,每次拖动距离是1m。如果不按住ctrl键,拖动距离的精度是任意的。
- 花费一些时间,拖动创建出15*15的Map,然后下节课设计地图元素:起点、路径、终点。
| 图2. 创建地图Map层级、创建地图块MapCube |
|---|
- 在Map上面,挖出一条路径(删除路径所经过的所有MapCube即可)
- 制作一个RoadCube,将RoadCube制作为Prefab。
- 制作RoadCube的材质,将该材质设置给RoadCube。
- 创建一个空物体,重命名为Road,将RoadCube归类到Road下。(所有的MapCube归类到Map下管理;所有的RoadCube归类到Road管理),这样归类木的地方便之后查找。
- 把挖出来的路径,从起点到终点,用RoadCube填充一遍。
- 制作起点(绿色的正方体)和终点(红色的正方体)。(有个小知识点:因为要从Map中扣掉一个MapCube,即从Map层级中,使一个MapCube脱离该层级。可以通过GameObject=>Break Prefab Instance来实现该操作)
| 图3. 移除MapCube,从起点(绿色正方体)到终点(红色正方体)创建一条路径Road |
|---|
- 左上角,点击对应按钮,可以切换摄像机的移动和旋转模式。
(上下左右四个箭头的图标,功能是控制预制体位置的。两个循环的箭头图标,功能是控制预制体旋转的) - 给摄像机添加移动功能、给地图添加缩小放大功能:
- 因为已经对摄像机做了旋转,所以移动时用世界坐标(摄像机移动其实是前、后、左、右修改摄像机坐标)
- 摄像头在垂直方向上的上下移动,就是地图的放大和缩小功能。
- 开始制作功能:选中Main Camera,点击Add Component输入Script,然后点击New script给摄像机添加一个c#脚本文件,编码控制摄像机坐标。
- 键盘的上下左右或wasd按键。接口:Input.GetAxis(“Horizontal”)获取水平轴滑动;Input.GetAxis(“Vertical”)获得垂直轴滑动。
- 在c#脚本Update()函数中获取轴的滑动值,Update()函数1min跑60帧,Time.deltaTime可以获取当前Update()处于第几帧。
- 因为摄像机经过了旋转,所以transform.Translate()移动相机时,要用世界坐标系Space.World,而不能使用自身坐标系Space.Self。
- 获取的轴滑动值范围是[-1,1]。
- 鼠标滚轮控制地图的放大缩小。获取鼠标滚轮滑动值的函数:Input.GetAxis(“Mouse ScrollWheel”)。
| 图4. 创建控制摄像机脚本:获取水平轴、垂直轴的滑动值,在Update中实现摄像机前、后、左、右 |
|---|
配置路点来实现怪物的寻路功能。
- 添加路径点:首先创建一个空物体Way,在Way层级下面挂载路径点WayPoint(因为没有其它功能,所以每一个WayPoint就是一个空物体)。点击Inspector下的椭圆体,可以给WayPoint修改颜色。
- 给Way这个节点,新增一个c#脚本组件:WayPointController.cs,用来管理所有挂载在Way上面的路径点。
- 脚本在Awake()函数中,获取节点下挂载的所有WayPoint。代码如下:
wayPointsPosition = new Transform[transform.childCount]; for (int idx=0; idxwayPointsPosition[idx] = transform.GetChild(idx); }
获取子节点:GetChild()。
| 图5. 地图寻路路径:WayPoint1 ~ WayPoint11 |
|---|
- 首先,我们制作一个预制体,命名为Enemy(敌人)。
- 在Enemy预制体上创建c#脚本:Enemy.cs,来控制敌人的行为:移动、攻击等。
- 移动的思路:获取Way节点下所有子物体,放到一个数组中,这个数组保存了整张地图的所有路点。然后让敌人顺序移动到数组中路点的位置。就能从起点移动到终点。
- 控制物体移动的函数:transform.Translate(移动的向量),举例:
transform.Translate((positions[index].position - transform.position).normalized * Time.deltaTime * speed);
- 判断物体是否达到某个路点:Vector3.Distance(位置1,位置2)<0.2f
Vector3.Distance(positions[index].position,transform.position)<0.2f
| 图6. 制作的预制体Enemy1、游戏游戏时在场景中创建的敌人Enemy1(Clone) |
|---|
- 在空物体GameManager上面,挂载一个Enemy Spawner脚本,来管理敌人的孵化、生成。
- 在Start()函数中启动协程来创建怪物:StartCoroutine(SpawnEnemy());
- 怪物被记录到class Wave中,这个类不能继承MonoBehaviour,是个独立的类。
- Wave记录了一波怪物的属性:类型、数量、出生间隔。目前设定一波怪物中,只能出现一种怪物。
- 孵化器脚本,根据波数控制脚本,来创建怪物。创建的接口:GameObject.Instantiate(对象,位置,旋转)
- 暂停n秒:yield return new WaitForSeconds(间隔)
| 图7. 敌人孵化器 |
|---|
- 改进后:前一波的敌人到达终点,消失以后,才会创建下一波敌人。
- bug:3个单选按钮,会全部被选中,显示黑色遮挡。
- 解决方案:把3个Toggle设置为一个group,这样每次选中显示1个黑色遮挡。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum TurrentType
{
LasserTurret, // 激光炮台;
MissleTurret, // 炮弹炮台;
StandTurret, // 标准炮台;
}
public class TurrentData{
public TurrentType type; // 炮台类型;
public GameObject turretPrefab; // 武器对应的prefab;
public int createCost; // 武器创建消耗;
public GameObject turretUpLevPrefab; // 武器升级后的prefab;
public int UpLevCost; // 升级消耗;
}
第十二课:监听炮台选择的事件(保存选择的炮台)
- 在UI的togger上,可以区别图标是否被点击。会抛出OnValueChange事件。
- 在BuildManager上实现监听函数,并绑定到UI上。
- 阿萨德
- 射线检测,点击了哪个方块。先检查下方块上是否有炮台,有炮台弹出销毁界面,没有则创建。
- 给方块Cube添加一个Layer,这样射线检测的时候,只检测和MapCube的碰撞。
void Update()
{
do
{
if(!Input.GetMouseButtonDown(0)){
break; // 鼠标没有按下;
}
if(EventSystem.current.IspointerOverGameObject()){
break; // 属性按在了UI上;(检测鼠标调用该函数不需要传入参数,但是手机上的话,需要传入参数 Help=>Manual查看Api接口说明)
}
// 射线判断点击在哪个方块上;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // 将鼠标点击的位置转换为一条射线;
RaycastHit hit;
// p1:射线 p2:碰撞结果 p3:最大距离 p4:需要检测的layer层,如果不传参数,就和所有的层做检测;
bool isCollider = Physice.Raycast(ray,out hit,1000,LayerMask.GetMack("MapCube"));
if(!isCollider){
break;
}
GameObject mapCube = hit.collider.GameObject; // 得到碰撞到的MapCube;
// &&& 开始炮台的建设;
}
while(false);
}
第十四课:检查是否足够可以创建炮塔
第十五课:资源管理(金钱消费和更新UI)
-
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
报错NullReferenceException,原因是之前在场景中加了两个摄像机Camera,后来我删除了一个, 应该是把MainCamera删除了。解决办法是:把剩下的那个摄像机的Tag修改为MainCamera即可。
第十六课:钱不够,制作动画提示效果- 制作Animations动画:
-
选中需要制作动画的文本,然后点击Window>Animation,打开Animation执制作器。
-
拖动红色的竖线,右键Add Key就可以添加动画的关键帧。
-
制作好以后,Food>Text下面就会挂上一个Animator组件,其中的controller指向了刚刚制作的动画。双击打开Text动画的状态机。
-
动画有一个Entry状态,这是动画的入口。原先Entry默认指向food_flicker。因为需求是钱不够才会播放food_flicker闪烁动画。所以右键新建一个Empty状态,Entry默认指向Empty状态。然后新增两条线路,分别指向Empty和food_flicker。
-
-
在场景中新建空物体,重命名为BuildEffect。在BuildEffect下新建一个Particle System粒子系统。
-
旋转特效,设置x为-90,使其特效朝上运动 。
-
点击BuildEffect>Renderer(Renderer就是每一个粒子的效果),将RenderMode修改为Mesh。Mesh就是一个网格。这里的游戏例子使用Cube网格。
-
然后为Mesh创建一个Material(材质),修改一下材质的颜色属性Albedo。保存命名为BuildEffectMaterial。Smoothness控制材质是否反光。
-
将新创建的材质,拖动到特效BuildEffect的Renderer中的Material。至此,我们完成了一个从下往上发散的特效。但是我们的建造特效,希望是从建筑中心,向四周发散的。所以我们需要调整一下参数。
-
首先,调节Particle System>Start Size,设置为0.6。再把Particle System>Shape的Shape属性设置为Circle,就可以看到特效向水平方向四周发散。
-
特效默认是循环的,这里我们只在创建的时候,发散一次就行。
7.1 修改Particle System>Emmission,将Rate over Time设置为0。
7.2 Bursts设置为一次发散30个cube小方块。
7.3 特效的持续时间,修改Start LifeTime这里设置为1秒。
7.4 修改Duration为1,表示每次循环时长为1秒。
7.5 修改Size over Lifetime,特效从开始到结束,是从大逐渐变小的。
-
这里做完特效,是水平扩散的。如果想要特效还是稍微朝上发散的话。需要修改Particle System>Shape>Shape的模式改为Cone(圆锥体),整个特效就是下面这种效果。
-
特效有个属性Looping,控制其是否循环播放。
- 修改Missile炮弹的属性:Smoothness控制透明度,Metalic控制金属性。
- 通过触发器来检测,哪些敌人进入到了攻击范围之内。
- 点击预制体>Add Component>Sphere Collider添加一个球形触发器。
- 勾上属性Is Trigger,表示该物体是个触发器。
- 修改Radius,调整大小,就可以看到如下触发器。
- 触发器要能正常检测敌人进入范围,需要添加刚体。
- 给预制体加上Rigidbody属性,去掉Use Gravity(使用重力)属性。
- 在代码中定义敌人列表,如果敌人进入触发器范围,会回调函数void OnTriggerEnter(Collider col);同理,离开触发器范围,会回调函数void OnTriggerEnter(Collider col)。
- 如何识别是敌人进入了触发器范围?
- 给所有的敌人添加一个Enemy的标签。
- 如果遇到炮台没有检测到敌人,可能是敌人没有添加碰撞器组件,检查一下对应的prefab,给敌人的prefab也添加上Sphere Collider即可。
- 给所有的敌人添加一个Enemy的标签。
- 碰撞检测的效率优化:在Edit>Project Settings>Physics中,可以配置指定的层1和层2做物理的碰撞检测。给所有的敌人添加EnemyLayer,给所有的武器添加WeaponLayer。然后在Physics中如下配置:
- 在炮台的枪管前面,创建一个空物体,作为射击开火点,随后的子弹初始化位置会被设置在这里。
- 建造一个子弹的预制体,然后在Attack()函数被调用的时候,将子弹创建在fire_pos位置。
- 创建一个球体作为标准炮台的子弹,调整子弹的缩放。
- 为标准炮台子弹创建材质,添加刚体和碰撞盒。
- 给子弹添加Bullet.cs脚本,通过触发函数OnTriggerEnter来检测和敌人的碰撞。
void OnTriggerEnter(Collider col)
{
if(col.tag == "Enemy")
{
col.GetComponent().Damage(damage); // 1.让敌人掉血;
GameObject.Instantiate(explosionEffect,transform.position,transform.rotation); // 2.播放受击特效;
Destroy(this.gameObject); // 3.销毁子弹
}
}
第二十三课:创建爆炸特效
- 创建Particle System,调整Renderer>RenderMode>Mesh>Cube。
- 调整Shape(发散属性),勾选Sphere表示球体,向四周发散。
- 只要播放一次,设置Emission>Rate over Time为0。Rate over Distance添加一个范围数量60~80的方块。
- 爆发的速度:Start Speed 调大一点,则特效播放的速度也会加快。
- Start Lifetime控制存在的时长。
- Size over Lifetime拖动曲线,可以控制特效的方块由大到小,或者由小到大。
- 不需要循环播放的特效取消Looping。
- 子弹的速度如果太快了,子弹的刚体可能和敌人的刚体,检测不到。(可以通过设置刚体的属性Collision Detection>Continuous来提高检测的精度)
- 子弹之前到达敌人身上,不会爆炸,原因是没有勾上Weapon层,因为在工程设置中,敌人的碰撞体只会和Weapon层做碰撞检测。
- 特效实例化以后,一定记得要销毁。
GameObject effect = GameObject.Instantiate(explosionEffect,transform.position,transform.rotation); Destroy(effect,1); // 1s后销毁;第二十四课:fixbug修复敌人销毁时,子弹销毁 第二十五课:fixbug修复敌人销毁时,炮台的引用 第二十六课:敌人死亡时,销毁特效 第二十七课:手动配置一下所有敌人的销毁特效 第二十八课:控制炮管指向敌人射击子弹
- 在turret.cs中持有一个炮管的变量(Transform turretHead;),在Update()函数中让该变量LookAt()敌人的位置即可。
- 代码完成以后,炮管会进行旋转。但是发现炮管旋转的中心是炮管的1/2处,并不是以炮台的基座为中心旋转点。
- 在StandTurret中新建一个空物体,保证空物体的中心和炮台基座的中心点是一致的,然后将炮管拖动到空物体节点下。这样炮管旋转就以基座为中心了。
- 修改以后,炮管确实围绕基座为中心进行旋转。但是炮管的初始朝向还是不正确。5.
- 我们是以y轴为中心轴,旋转炮管。所以保证蓝色的z轴在初始状态是和炮管的朝向是一致的。所以修改head节点的旋转(旋转90度) ,使蓝色z轴和炮管朝向一致。
- 新建一个Canvas(画布),里面新建一个Slider(滑动条)来实现血条的功能。
- 这里Slider不需要交互,所以去掉选项Interactable。
- Slider所处的Canvas和原先的Canvas重合了,准备调整一下Canvas的大小,发现无法调节。需要修改Render Mode为World Space。点击这个小正方形,可以调整血条Canvas的大小。
- 因为不需要血条的那个小圆圈,所以移除Handle Slide Area。血条的前背景图片,不能完整的覆盖后背景,需要调节一下节点Fill Area,使得前后背景的大小一致。调节一下Slider的前背景为绿色。
- 将血条Canvas移动到坐标(1,1,1)处,并且绑定到Enemy的节点下面。
- 在敌人脚本中,用Slider类声明变量hpSlider。然后修改控制hpSlider.value的值,来动态显示血条的百分比。
- 制作多个敌人的血条的时候,可以将已经制作好的预制体1的参数,copy复制到预制体2的控件上。
- button的Transition设置为Animation,可以为按钮添加动画
- button的Transition可以点击Auto Gen Animation,会自动会按钮生成4个动画,分别对应:
- normal:常规时按钮的动画
- 鼠标移动到按钮上的动画
- 鼠标点击按钮的动画
- 按钮失效后的动画
- 实现的效果是从小到大,慢慢从0到1的比例缩放。(可以用这种方式给UI做动效的效果,比如UI从左到右慢慢滑动消失)
- Animation是动画,一个按钮可以设计多个动画。然后用Animator状态机来通过trigger条件来控制不同动画之间的切换。
- 先失效再生效,可以修正:点击不同炮塔的时候,没有播放show动画。



