Unity官方教程:Space Shooter 基础篇(PM版)


1. 准备场景

下载并导入官方Space Shooter教程的范例素材包,我们仅使用其中的模型、材质、贴图、Prefab(里面有粒子特效)。

001.png

首先设置Player。
Models文件夹中的vehicle_playerShip放入场景,断开Prefab联接,改名为Player。然后为Player添加一个Mesh Collider,并指定Modes文件夹中的vehicle_playerShip_collider为其Mesh。勾选上Convex选项。

这是因为飞船模型比较复杂,不适合直接拿来当做Collider,所以专门制作一个简化版的Collider模型。而使用Mesh Collider必须要勾选上Convex选项,否则就不会计算碰撞效果。

Prefabs > VFX > Engines文件夹中的engines_player拖动到场景中的Player内部,这是预制的引擎火焰粒子特效。

002.png

然后设置摄影机。
这个范例中使用的是正交摄影机(设置ProjectionOrthographic),不使用天空球,背景设置为纯黑(设置Clear FlagsSolid Color,然后指定为黑色)。将摄影机调整为垂直向下,根据Player在摄像机中的大小调整合适的Size值。

正交摄影机的视角范围是固定的,不根据其高度变化而变化,只能通过Size来调节。

003.png

接下来设置背景。
新建一个Quad物体,重命名为BG,将星空贴图赋给BG,Unity会自动创建一个材质球。因为星空背景不需要受到光照作用,所以我们修改这个材质球的Shader为Unlit/Texture。放大BG到合适大小,降低其Y轴位移避免挡住Player。

004.png

最后设置灯光。
原教程中使用了三点光照法来设置光照,我个人觉得没什么必要,效果有限。

大家可以自行设置场景光照效果,需要注意的几点是:

  1. 这个场景不需要全局光照,也不需要环境光,这两块都要去Lighting面板中去取消(菜单Window > Lighting > Settings);
  2. Player上的两个材质的高光范围都很大,且Player物体在摄影机中又挺小的,所以在平行光照射下很容易被 “洗白”,大家可以适当增大Smoothness值减少高光范围;
  3. 灯光可以无需投射阴影;
  4. 建议使用Forward渲染路径来渲染这个场景,系统资源占用会比较少。

原版教程中是设置成了600×960分辨率的纵版游戏画面,我懒得新建项目,所以就直接做成4:3的横版画面了。


2. 添加Player运动控制

Player添加上Rigidbody组件,我们还是使用Set Velocity的方式来控制其运动。因为是在太空中运动,所以无需考虑重力,不勾选Use Gravity

005_player.png

Player添加Fsm,在State 1中依次添加Get Axis VectorVector3 NormalizeVector3 MultiplySet Velocity四个行为。并按下图设置好相关的变量。

最基本的运动控制,只需要Get Axis VectorSet Velocity两个行为就可以完成了,但其实是不准确的,因为角色沿着斜角运动速度会比较快(比如向左上方运动时,左和上的向量长度都是1,但综合起来的左上方向量长度大概是1.2)。所以这次我添加Vector3 Normalize来将input axis标准化到向量长度始终为1,然后再用speed变量将其放大为速度向量值。

006_player_fsm_movement1.png

飞船的倾斜控制

飞机在左右摆动时机身会有一定的倾斜效果,下面我们来制作这个效果。

在如下图所示的位置依次添加Get Vector3 XYZFloat MultiplySet Rotation三个行为。(被折叠起来的Actions代表之前就已经有了的行为)

006_player_fsm_movement2.png
  • Get Vector3 XYZ将最原始的input axis的X值单独储存为input x变量,我们需要这个数值来控制Player在Z轴上的自身旋转;
  • Float Multiplyinput x值放大(原始的input x最大才为1,作为旋转量肯定是不够的),并指定一个max tilt angle变量作为Multiply By参数的值(初始设置为30);
  • Set Rotation设置Player沿自身Z轴旋转的程度(Space参数设置为Self),这时候的input x值已经被放大了30倍了。

要注意,Set Rotation行为和Rotate行为是不一样的,Set Rotation是设置目标的旋转属性,而Rotate主要用来让目标持续旋转。

在Variables面板中,记得将speed变量和max tilt angle变量都公开到Inspector中,并设置好其数值。

新手常常会犯的错误是新建了变量却忘记赋值。比如这里的如果我们在Float Multiply行为中新建speed变量时忘记将其初始值设置成5的话,speed变量的初始值就默认为0,然后被其“放大”后的速度向量也就变成了0,调试的时候飞船根本就不会动。

006_player_fsm_movement3.png

经过调试发现,max tilt angle数值为正的时候,Player倾斜的角度和我们预想的正好是反的,方便起见,我就直接将max tilt angle值设置成负数了,否则就需要添加一个Action专门把这个值变成负数。

006_player_fsm_movement4.png

限制Player移动范围

我们不希望Player移动到星空背景以外的区域去,所以需要限制Player的移动范围。解决办法是直接限制其X方向和Z方向的位置,也就是使用Clamp。

在如下图所示的位置依次添加Get PositionFloat ClampFloat ClampSet Position四个Action:

006_player_fsm_movement5.png
  • Get Position行为获得Player的当前位置,并不储存为Vector3类型的变量,而是分别将X轴位移和Z轴位移储存为player position xplayer position z两个Float类型的变量;
  • 两个Float Clamp行为分别限制player position xplayer position z的数值,使其不超过设定好的最大最小值,这个最大最小值可以手动从场景中获得;
  • Set Position行为将被限制(Clamp)以后的player position xplayer position z设置给Player的位移参数,Y方向参数始终保持为0。

每次用PlayMaker做数学计算我都非常非常的蛋疼,明明一句话就能说明白的事情,却非要折腾出一堆Action来,也许这就是“可视化”的代价吧。

这样设置的原理是,虽然Velocity会驱使物体改变其自身位置,但我们强行用直接设置物体位置参数的方式把错误的位置给纠正了过来,于是物体不可以跑出限制区域。

如果不愿意这么麻烦,最简单的办法是在场景中设置4面空气墙(有碰撞,无渲染)。


3. 让Player发射子弹

创建子弹prefab

在场景中新建一个Quad,命名为VFX,另外新建一个空物体,命名为Bolt,两个物体都重置位置属性,然后将VFX放在Bolt里面。

007_bolt1.png

删掉VFX的Collider,将Textures文件夹中的fx_lazer_orange_dff赋给VFX,Unity会自动新建材质球,修改材质球的Shader为Mobile/Particles/Additive

Mobile类的Shader系统资源占用最少,而Additive类型的Shader,其颜色会与下层颜色相加,适合于用来制作亮光物体,比如这里的激光子弹。

007_bolt2.png

Bolt上添加Rigidbody,取消Use Gravity选项。
添加Capsule Collider,勾选Is Trigger选项,并修改其形状以适合VFX的子弹形状(设置Radius为0.03,Height为0.5,DirectionZ-Axis

007_bolt3.png

结果如图:

007_bolt5.png

Bolt添加Fsm,使用Set Velocity设置其出生就沿着自身(Self)坐标系的Z轴方向以speed = 10的速度飞行。

Bolt的Fsm和Player的Fsm中我都设置了speed变量,但这两个speed变量之间是没有任何关系的,因为它们属于不同的Fsm。

007_bolt4.png

最后,将Bolt做成prefab。

按下“开火”键连续发射子弹

首先在Player内部创建一个空物体,命名为Shot Spawn,放置到合适位置,用来在此位置发射子弹。

008_shot01.png

空物体在场景中是看不到的,可以改变其默认显示图标,使其在场景视图中可见。

008_shot02.png
008_shot03.png

虽然目前用来控制Player运动的Fsm非常简单,但我依然不希望在这个Fsm中进行关于射击的交互制作,所以我添加一个新的Fsm给Player。

希望我懒得给Fsm以及State命名的坏习惯不会影响到大家。:)

008_shot04.png

发射子弹的制作原理并不困难,因为之前在Bolt上已经制作了“出生即飞行”的行为,所以这里只需要在“枪口位置”生成子弹就可以了。但因为是纵版射击游戏,如果开火一次只能发射一颗子弹的话就太累了,而且还需要控制两颗子弹之间的发射间隔,不能让手速高的玩家人为制造弹幕。所以这个“开火”的基本逻辑我设计为:按下“开火”键即开始发射子弹,每隔0.5秒发射一颗,松开“开火”键停止发射子弹,但再次“开火”依然需要比之前最后一枚子弹滞后同样的时间间隔。

这里需要3个不同的状态来达到此目的:State 1检测“开火”键是否被按下;State 2中发射1枚子弹,发射完毕等待0.5秒;State 3中暂停0.5秒,暂停完毕后根据“开火”键是否还被按下来选择是继续发射子弹(返回State 2)还是等待下一次开火(返回State 1)。

008_shot05.png

State 1:使用Get Button Down行为来检测Fire1这个Button是否被按下了,如果是,则触发Firing事件。

默认的Input设置中,Fire1等同于鼠标左键,以及左Ctrl键。

008_shot06.png

State 2:使用Create Object行为在Shot Spawn所在位置创建一个Bolt预设物体的实例,使用Wait行为等待fire duration变量中所设置的时间(公开到Inspector,预设值0.5),等待结束触发FINISHED事件。

008_shot07.png

State 3:使用Get Button行为检查Fire1按钮是否处于被按下状态,将结果储存在Is Firing变量(Bool类型)中,然后使用Bool Test行为检查Is Firing变量的值,如果为True则触发Keep Firing事件,如果为False则触发Stop Firing事件。

008_shot08.png

一些必要的解释:

  1. Wait行为放在State 2中而不是放在State 3中是迫使每次发射完一颗子弹都必须休息0.5s才能发射下一颗。如果放在State 3中与“开火”按键判定在一起,则会由于Bool Test达成条件而导致未能休息够0.5秒就跳回State 1,这样玩家可以通过快速点击“开火”键而达到无视发射间隔的目的,所以必须等到发射间隔之后再行判断;
  2. State 3中使用Get Button而不使用Get Button Down,是因为我们需要在“按下”和“没按下”两种状态中分别触发两个事件,而Get Button Down仅能在“按下”时触发一个事件。当然我们也可以把Get Button Down当做Get Button来使用,但Get Button Down是一个默认每帧执行的行为,而Get Button可以只执行一次,这里仅需要判定一次,所以使用Get Button更合适;
  3. 在设计PlayMaker行为逻辑的时候,“每帧执行(Every Frame)”的行为和“单次执行”的行为是混在一起使用的,这是与编写脚本非常非常不同的一个特征。所以我们要时刻注意哪些行为是必须要每帧执行的,哪些行为是可以单次执行的,同时尽量设计逻辑减少“每帧执行”行为的使用以提高执行效率。

让子弹自动销毁

每次发射的子弹都会保留在场景中不断前进,这对于系统资源来说是不必要的浪费。所以我们希望子弹能够自动销毁。我们可以选择多种方案自动销毁子弹,比如让其在一定时间之后销毁,或者如同本范例中所选择的让其超出某个边界之后消失。

首先创建一个边界物体,比如一个大Cube。

009_boundary01.png

这个Cube表示游戏场景的边界,但我们不需要它实际可见。所以取消掉Mesh Renderer组件(或者干脆删除这个组件),并勾选上其Box Collider组件中的Is Trigger选项(不勾的话,这个Cube内部的非Trigger碰撞体就可能会与其发生非常诡异的碰撞)。

009_boundary02.png

最终这个Cube在场景中显示如下图:

009_boundary03.png

Cube添加Fsm。State 1中检测是否有Trigger离开,如果有,则跳转到State 2中删除该Trigger。

009_boundary04.png

State 1:添加Trigger Event行为,设置只要有Trigger物体离开该Cube的碰撞体,就触发Trigger Leaving事件,并将离开的Trigger物体储存在leaving trigger变量中。

009_boundary05.png

State 2:添加Destroy Object行为,删除leaving trigger变量中所储存的游戏物体。

009_boundary06.png

注意,是使用Trigger Event还是使用Collision Event取决于需要监测的对象的碰撞体是否被设置成了Trigger,而不取决于本体的碰撞体是否被设置成了Trigger。也就是说,不论这里的Cube是不是一个Trigger,它都可以使用Trigger Event来监测是否有其他Trigger进入或者离开,也都可以用Collision Event来监测是否有其他Collider进入或者离开。按我们现在的设置,Cube是可以监测Bolt的,因为Bolt是一个Trigger,但Cube并不能监测Player,因为我们的Player并没有勾上Is Trigger选项,依然是普通Collider,如果需要Cube同样监测Player,则必须使用Collision Event行为。


4. 陨石

制作陨石物体

新建一个空物体,重命名为“Asteroid”,将Models文件夹中的prop_asteroid_01拖到Asteroid中,重置它们的位移属性。

010_rock_01.png

Asteroid添加一个Capsule Collider,设置其大小,使其正好包裹住陨石物体。然后再为Asteroid添加一个Rigidbody,取消Use Gravity选项。

通常,我们都会将交互设置做在一个空物体节点上,然后其内部再放置视觉物体,这是一个基本的操作规范。

010_rock_02.png

预设的Asteroid在材质上也有同样的问题,让我们修改Smoothness为0.8。

010_rock_03.png

制作陨石自旋

原教程中的陨石自旋使用的是随机指定一个旋转向量,放大后设置其为陨石的Angular Velocity的方式,在PlayMaker中完全重现这个方法是可行的,就是稍显麻烦了一点。

Asteroid添加一个Fsm。

010_rock_04.png

State 1中依次添加如下行为,并设置相应的变量:

010_rock_05.png
  • 我们先使用一个Set Random Rotation行为给陨石添加一个随机的旋转;
  • 然后使用Transform Direction行为获得这个陨石正方向(Z轴正方向)所指方向在世界坐标中的向量值,并储存为rotate vector变量;
  • 然后使用Set Set Random Rotation行为再次随机旋转陨石,让其初始旋转方向与自旋方向并不完全一致;
  • 接下来使用Vector3 Multiply行为放大rotate vector变量,将其倍乘上rotate speed变量中所设定的数值(公开到Inspector中,预设为5);
  • 再然后添加一个Add Torque行为给陨石施加一个旋转动力,设置Vector参数为rotate vector变量(旋转动力向量),设置Space参数为World,设置Force Mode参数为Velocity Change
  • 最后使用一个Add Force行为,让陨石出生即受到一个沿着Z轴方向,大小为speed变量(Float类型,公开到Inspector中),且设置Force Mode参数为Velocity Change,使其成为一个改变目标速度的推力。

一些解释:

  1. 也许会有更好的办法获得一个随机的扭力矢量,我这里用的办法并不是特别简便;
  2. Add TorqueAdd Force很类似,但一个是给物体添加“扭力”,一个是给物体添加“推力”,将其模式设置成Velocity Change就基本上等同于Set Angular Velocity(PlayMaker中并没有这个Action)和Set Velocity了;
  3. 最后一个Add Force完全可以用Set Velocity来替代,更为直观。我这里使用Add Force完全是为了跟Add Torque作对比。
010_rock_06.png

在Inspector中设置两个公开了的变量初始值,rotate speed = 5;speed = -1(因为陨石应该往Z轴负方向运动)。

子弹与陨石的交互

简化起见,我们设置陨石会被子弹一击击毁。

为陨石添加一个新Fsm来设置其与子弹的交互。我选择在陨石而不在子弹上添加这个Fsm是因为一般来讲,场景中的子弹当然多过场景中的陨石,交互放在陨石上运行效率会高一点点。

010_rock_07.png

基本的交互逻辑是:陨石检测到有子弹物体进入其碰撞体,就自动销毁。

010_rock_08.png

State 1:添加Trigger Event行为(因为子弹是Trigger嘛),设置为On Trigger Enter(当有Trigger进入陨石碰撞体时),触发Hit a Trigger事件,并储存Trigger物体到trigger变量。

010_rock_09.png

State 2:添加Destroy Self行为,删除自身。为了检查交互逻辑是否正确,我还在Destroy Self之前添加了一个Get Name行为,以获得trigger变量中所储存物体的名称,并紧接一个Debug Log行为,让这个名称在系统控制台(Console面板)中被打印出来。

010_rock_10.png

运行场景,一开始陨石就消失了,明显有问题。而且Console面板中并没有任何信息被输出。

010_rock_11.png

PlayMaker有时候使用Debug Log行为输出信息给Unity控制台会出现一些奇怪的问题,尤其是Info级别的信息,不知道什么时候就不输出了。所以我将State 2中的Actions修改一下以便来找出问题所在(Debug):

  • 修改Debug LogLog LevelWarning,这样就保证一定会输出给Unity控制台;
  • 取消Destroy Self行为(别删除,以后还要用的),让陨石不会消失,以便在运行状态中选择陨石查看其状态。
010_rock_12.png

再次运行,发现陨石是检测到了Cube这个Trigger,于是跳转到State 2了,Console里也正确显示了警告信息。

010_rock_13.png
010_rock_14.png

Cube是我们的边界物体,不可能被删除掉,所以需要在Trigger Event中排除掉对Cube的检测。这里可以利用Trigger EventCollider Tag参数。

Collider Tag参数可以指定该行为仅针对具有特定标签(Tag)的碰撞体执行,默认设置为Untagged的意思是对所有没有打标签的碰撞体都执行操作,Untagged代表“没有任何标签”**。

我们可以给Cube物体添加一个叫做“Boundary”的标签,系统默认并没有一个叫做“Boundary”的标签,于是我们可以新建一个:

010_rock_15.png
010_rock_16.png

顺便我们还可以把Player场景物体和Bolt预设物体都分别打上Player标签和Bolt标签。

010_rock_17.png
010_rock_18.png

但现在还是有点小麻烦,我们希望Trigger Event排除掉有Boundary标签的物体,但Trigger Event只能设置为“仅针对某一个标签有效”,所以我们不得不使用多个Trigger Event来分别处理不同的标签。

010_rock_19.png
010_rock_20.png

这里有一个小错误,其实第一个Trigger Event并不会起作用,因为Player物体本身并不是一个Trigger,虽然给了标签,但并不会被检测到,也不会被陨石所“消灭”。这个错误不是很致命,就暂时先留在这里好了。

运行场景,问题解决!但新问题出现了,我们的子弹穿过了陨石继续前进。这在一定程度上是合理的,激光子弹能量足够强的话是可以打穿物体的,但我们并不希望这样,我们还是希望“一发子弹只消灭一个敌人”,所以在State 2中再添加一个Destroy Object行为,让它也销毁掉子弹物体(变量trigger)就好了。

如果勾选Detach Children选项,Destroy Object行为将仅销毁顶端节点(父节点),而保留所有子物体,大家别把Detach看成Destroy了。

科普一下Unity中对于“销毁物体(Destroy Object)”的设定:当Unity执行Destroy命令销毁某个游戏物体时,它并非立即从场景中删除该物体,而是将该物体标记为“需要被删除”,直到系统刷新到下一帧画面的时候,才将所有标记为“需要被删除”的游戏物体一次性删掉。

010_rock_21.png

下面我们将Asteroid做成Prefab,再复制多个,放在场景顶端。运行场景,所有的陨石都会一边自旋一边匀速下落,且能够被子弹摧毁。

010_rock_22.png

别忘了可以在Inspector中设置陨石的自旋速度以及下落速度。

010_rock_23.png

添加爆炸效果

在Unity中可以制作物体爆裂开的效果,但在我们这个例子中就没有这个必要了,我们直接使用一个爆炸粒子特效来代替就好了。

我们下载的教程工程文件包中就提供了陨石的爆炸粒子特效,我们只需要在陨石爆炸时,在陨石所在位置创建这个粒子特效就可以了。

由于我们已经将Asteroid做成了prefab,所以在选择场景中的Asteroid实例时,PlayMaker会询问我们是在实例上做修改(Edit Instance按钮)还是在预设物体(Edit Prefab按钮)上做修改,我们选择Edit Prefab

010_rock_24.png

FSM 2State 2中插入一个Create Object行为,选择explosion_asteroid预设物体作为其Game Object参数的取值。

注意,导入教程工程文件包以后,会有很多预设,大家不要选错了,尤其是不要去选那个done_explosion_asteroid预设物体。

010_rock_25.png

运行场景,发现爆炸效果虽然出现了,但位置好像有所偏差,不是在所击中的陨石中心,而是在坐标原点位置,这是因为我们忘记指定新建物体的位置了。

010_rock_26.png

Create Object行为之前添加一个Get Position行为,获取自身位置为变量position,然后将position赋给Get Position行为的Position参数。

010_rock_27.png

问题解决!

仔细观察运行场景,爆炸结束后,爆炸粒子物体的实例依然保留在场景中(虽然已经不可见了),这是对于系统资源的浪费,要坚决予以制止!

010_rock_28.png

为了不破坏教程工程文件包中资源的纯洁性(完全没必要),我把explosion_asteroid预设物体复制了一份,重新命名为my_explosion_asteroid,重新将其指定给Asteroid预设物体的FSM 2State 2Create Object行为的Game Object参数。

然后在my_explosion_asteroid预设物体上添加Fsm,使其出生后等待2秒(使用Wait行为),然后跳转到State 2中销毁自身(使用Destroy Self行为)。

010_rock_29.png
010_rock_31.png

选择2秒是因为这个粒子特效的Duration值就是2秒钟,方便起见,我就直接输入这个数值了,当然最标准的做法还是新建一个变量(lifespan)来储存这个数值并公开到Inspector比较好。

010_rock_30.png

陨石与飞船的碰撞

方便起见,还是把Player的碰撞体也设置成Trigger吧。

陨石与飞船的碰撞和陨石与子弹的碰撞不同之处在于,撞上Player之后需要生成两个爆炸效果,一个是陨石的,一个是飞船的。

修改Asteroid预设物体上的FSM 2,让两个Trigger Event分别触发Hit Bolt事件和Hit Player事件,并分别设置事件跳转到State 2State 3中。

可以直接在Events面板中将原来的Hit a Trigger事件改名为Hit Bolt,然后再新建一个Hit Player事件。

010_rock_34.png
010_rock_33.png

State 3是陨石碰撞到Player之后所处于的状态,其中大部分Actions都和State 2中的一样,可以直接从State 2中复制过来,需要做的修改是添加一个Get Position行为以获得Player物体(保存在trigger变量中)的位置并储存于target position变量中,以及添加一个Create Object行为在Player所处位置(target position变量值)创建一个飞船爆炸粒子效果预设(explosion_player预设)。

这里我暂时取消了删除Player的行为,用来查看新爆炸位置是否正确。

010_rock_35.png

运行场景,没有什么问题。现在可以激活删除Player的行为,并删除场景中的陨石物体了,真正的游戏进程中的陨石物体会使用PlayMaker来自动生成。


5. 游戏控制(Game Controller)

下面我们来制作控制游戏的交互逻辑。

基本版游戏流程是:游戏开始后等待1秒钟,然后每隔0.5秒随机在屏幕上方生成一个陨石,10个陨石一波,每波陨石之间休息3秒。

在场景中新建一个空物体,命名为Game Controller,重置位置。

012_game_controller_01.png

Game Controller上建立Fsm,我们先制作“不断在屏幕上方生成陨石”的交互。

012_game_controller_02.png

State 1

  • 使用Random Float获得一个随机的spawn position x变量,其最大最小值分别指定使用变量min spawn xmax spawn x。这两个变量都公开到Inspection,设置其初始值为-5.3和5.3(这个数值根据手动测量得到);
  • 使用Set Vector3 XYZ获得一个位置变量spawn position(选择Set Vector3 XYZ是因为其允许用户分别设置X、Y、Z值),然后设置X取值为变量spawn position x,设置Y取值为0,设置Z取值为变量spawn position z(公开到Inspector,设置初始值为9);
  • 在最后添加一个Create Object行为,指定Game Object参数为变量rock object(变量rock object公开到Inspector,设置初始值为我们之前制作的Asteroid预设物体),创建位置(Position)为变量spawn position
012_game_controller_03.png

State 2

  • 添加一个Wait行为,设置其等待时间为新建变量spawn duration(公开到Inspection,初始值为0.5),等待结束触发FINISHED事件,并勾选Real Time选项。

看起来我们使用了很多很多变量,这是为了能够在Inspector中对这些数值进行调整,而无需进到Fsm中去寻找特定状态的特定行为。在我们这个非常简单的例子里,这么做似乎挺繁琐的,但如果日后需要对这个项目进行修改或改进,就会感受到尽可能变量化的优势所在了。

接下来制作“游戏开始时,等待1秒钟”的交互行为。

在Game Controller的Fsm中添加一个State 3并设置为Start State,在State 3中使用Wait行为让其等待1秒钟(新建变量start wait time,公开到Inspector,初始值1),等待结束触发FINISHED事件跳转到State 1。

012_game_controller_04.png

到目前为止,公开到Inspector的变量及其取值如下图所示:

012_game_controller_05.png

运行场景,我们可以得到如下图所示密集的陨石下落效果。

012_game_controller_12.png

继续对Fsm进行改造,让每生成10个陨石,就休息3秒钟。

State 3中添加一个Set Int Value行为,将新建变量rock number的取值设置为0,这个rock number变量就是我们的“计数器”。

012_game_controller_06.png

State 1的最后,添加一个Int Add行为,让rock number变量的数值+1。这个“计数器+1”的操作最好放在生成陨石的Action后面,避免逻辑混乱。

012_game_controller_07.png

State 2中,插入一个Int Compare行为比较当前的rock number变量的取值有没有达到变量max wave spawn number中的数值(公开到Inspector,初始值10),也就是有没有生成完所有这一波的陨石,如果达到了(Equal),就触发Wave Finished事件跳转到State 4,如果还没达到,继续执行后面的Wait行为,等待0.5秒之后返回State 1生成下一颗陨石。

012_game_controller_08.png

State 4中,我们重置计数器,用Set Int Value行为将变量rock number的取值归零,然后添加一个Wait行为让其等待3秒钟(变量wave duration,公开到Inspector,初始值3),等待结束触发FINISHED事件跳转回State 1

012_game_controller_09.png

这个Fsm中用到的所有变量如下图:

012_game_controller_11.png

这些变量是公开到Inspector以方便我们修改的:

012_game_controller_10.png

运行游戏,可以清楚的分出不同的波次效果了。不过,这个游戏既没有计分,也没有难度递增,很快就会兴趣缺缺了。


音效、 计分系统、游戏进程控制

添加音效

选择Asteroid预设物体,在FSM 2中为“被子弹击中”状态和“和玩家相撞”状态添加音效。

State 2代表被子弹击中,我们添加一个Play Sound行为,并在Audio Clip中指定explosion_asteroid素材。

013_sound_01.png

State 3代表与玩家相撞,我们添加两个Play Sound行为,分别在Audio Clip中指定explosion_asteroid素材和explosion_player素材。

013_sound_02.png

对于飞船武器发射的声音,我们采取另外一种方法。

选择Player,添加一个Audio Source组件。

013_sound_03.png

Audio Source组件的AudioClip参数中指定weapon_player素材为其音频来源,取消Play On Awake选项的勾选避免游戏开始即播放该声音。

013_sound_04.png

PlayerFSM 2中的State 2中,插入Audio Play行为,无需做任何设置。

013_sound_05.png

或者也可以不设置Audio Source组件的AudioClip参数,改为在Audio Play行为中,将On Shot Clip指定为weapon_player素材。效果一样,但系统资源会占用得多一点。

013_sound_06.png

Play Sound行为和Audio Play行为不一样,前者无需对象上指定音源,即时调用游戏素材来播放,而后者则主要是用来控制游戏物体上所指定的音源的播放,如果我取消Audio Source组件,即使Audio Play行为中另外指定了音源素材,也不会发出声音。

计分系统

原教程是Unity3D4.x时代的,UI部分基本已被新UI系统所取代,所以我们采用新UI系统来进行这一部分的重制。

在场景中添加一个UI Text物体(菜单GameObject > UI > Text),修改名称为Score Text

014_UI_01.png

按下图修改Score Text的参数,让其显示在画面左上方。

014_UI_02.png
014_UI_03.png

然后在Game Controller中新建Fsm(FSM 2),State 1中用Set Int Value行为初始化两个变量:current scoreadded score

注意,关于游戏的控制都会尽量放在Game Controller中,这也是方便日后的管理。在本范例中,Game ControllerFSM专职控制生成陨石,FSM 2专职控制得分系统以及游戏进程。

014_UI_04.png

State 2中,使用Convert Int To String行为将current score转换成格式为“Score: 0”这样的字符串,储存于score text变量中。然后使用U Gui Text Set Text行为设置Score Text物体的Text为变量score text

注意,这里的Actions我都没有设置成Every Frame的,而是仅执行一次。虽然在我们的概念中“显示得分”不是应该实时跟踪分数值么?但实际上只有“得分”的那一瞬间才需要更新我们的UI显示,并不是必须每帧执行。

在Events面板中新建一个Score Add事件,并设置State 2通过Score Add事件的触发跳转到State 3

注意,因为State 2中并没有任何Action会触发Score Add事件,所以这个Fsm单靠自己是永远不可能跳转到State 3的。

014_UI_05.png

State 3中,我们添加一个Int Add行为,为current score变量的值增加add score变量中储存的数值。

注意,现在这个add score我一开始初始化为0了,也没有准备将它公开到Inspection。这是因为在我的设计中,这个“增加的分数”是由Player所击中的物体来决定的,物体有多少分,这里就增加多少分。

014_UI_06.png

在这个关于“得分”的Fsm中,有两件事情是不由Fsm自身来控制的,一是Score Add事件的触发,二是实际增加的分数值。这两件事情都需要由被击毁的陨石来告知Game Controller:“你得分啦!”、“你得到了10分!”

要做到这一点,我们还需要将Score Add事件设置成Global全局模式,也就是在Events面板中,勾上Score Add前面的选框。

014_UI_07.png

要从外部触发一个Event,我们需要将这个Event设置成Global全局模式。而要从外部修改一个变量,我们只需要使用专门的Action来操作就好了,并不需要将这个变量设置成全局模式。

选择Asteroid预设物体,修改其FSM 2,让它在被击毁时能够发送消息给Game Controller

014_UI_08.png

首先要让这个Asteroid找到Game Controller。由于这是prefab,不能用直接指定场景中游戏物体的方式来指定Game Controller物体,只能用Find Game Object行为让其寻找游戏场景中的Game Controller

FSM 2中新建一个状态State 4,添加Find Game Object行为。设置Object Name参数为Game Object,然后设置Store参数为变量game controller(也就是把找到的游戏物体储存在这个变量中)。

014_UI_09.png

我们设定陨石被子弹击中才会得分,陨石与飞船撞击不会得分。所以在State 2(也就是被击中事件发生以后的状态)中,添加Set Fsm Int行为和Send Event行为。

  • Set Fsm Int行为能够设置任意Fsm中的Int类型变量。设置Game Object参数为Specify Game Object,然后指定变量game controller,再手动填写Fsm Name为“FSM 2”,手动填写Variable Name为“added score”,并设置Set Value参数为新建变量score(公开到Inspector),也就是将目标Fsm中的added score变量值设置为本Fsm中变量score的值;
  • Send Event行为能够发送一个全局事件到任意Fsm中,使目标Fsm触发该事件从而改变状态。由于Game Object上不止一个Fsm,所以需要设置Event TargetGame Object FSM(这样才会提供参数给我们指定Fsm名称),然后指定GameObject参数为变量game controller,手动填写Fsm Name为“FSM 2”,选择Score Add事件为Sent Event
014_UI_10.png

最后设置Asteroidscore变量值为5。

014_UI_11.png

运行游戏,果然有趣多了。

014_UI_12.png

重置游戏

我们设计当Player被击毁时,Game Controller停止生成陨石,然后显示提示字幕,告诉玩家按下R键重新开始游戏。

在场景中选择Score TextCtrl + D复制一个新的出来,修改名称为Restart Text

015_game_restart_01.png

按下图修改Restart Text物体,主要是显示文字、位置。设置好以后设置其“休眠”(取消Activate勾选)。

015_game_restart_02.png

Asteroid预制物体的FSM 2中的State 3(也就是玩家被击中之后的状态)中,添加Sent Event,按下图设置(或者从State 2中直接复制粘贴过来以后修改也可以):修改FSM Name为“FSM”,设置Send Event为一个新的全局事件(Global Event):Player Destroyed

意思是:告诉Game Controller上的FSM:“玩家死翘翘了!”

015_game_restart_03b.png

然后选择Game Controller,进入FSM,新建一个State 5,为其添加一个Global Transition,并设置Player Destroyed事件为其触发条件。

015_game_restart_04.png

State 5中,添加Activate Game Object行为让其激活Restart Text游戏物体,然后在添加一个Get Key Down行为让其检测R键是否被按下,如果按下则触发Game Restart事件,跳转到State 6

015_game_restart_05.png

在State 6中,添加一个Restart Level行为。

015_game_restart_06.png

大功告成!!!

从逻辑结构上来说,“玩家被击毁”这件事情并不应该由击毁玩家的那颗陨石来告诉系统,而是应该由玩家本身来告诉系统。在这里例子中能够让陨石来告诉系统,纯粹是因为我们设置了“一击即毁”的规则。

正确的逻辑应该是:陨石和玩家碰撞,陨石自身击毁,给予玩家一定伤害;玩家自身判断伤害累积超过生命值,被击毁,告知系统。

有兴趣的同学可以自行改造一下。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容