[教程]超级简单的“对决”小游戏基础版(Duel Game Ba

前言
一个月的课程上完了,总体感觉比去年要好一些,当然还是有很多需要改进的地方。学生的普遍的反映是看图文版的教程会比较方便一些,所以还是坚持以这种方式来做吧。但从这一篇开始,会尝试用一种新的方式。之前都是一边做范例一边截屏并写文字,其实还是蛮手忙脚乱的,而且有时候出错了也不好修改。这一篇开始先录视频,然后对着视频来写图文版教程,希望能够更高效一些。
这个范例特别特别简单,属于入门级别中的入门级别,是在上课的时候脑筋急转弯想到的(当然原身还是以前在手机上玩过的一个游戏)。
游戏的原身是“Ready Steady Bang”,有兴趣的同学可以下载来试试,反正我是手残,过不了几关就Game Over了:

本教程的在线视频发在YouTube:[教程]使用Unity和PlayMaker制作一个超级简单的“对决”小游戏,主要原因是录制的码率达不到优酷所要求的5M,所以在优酷上反而只能达到“高清”分辨率,效果不是特别好。
上不了YouTube的同学可以在百度网盘下载视频文件观看学习。
另外,由于学生普遍反馈对于众多Actions的英文名称不熟悉,从而导致跟不上课堂节奏,因此从这篇教程开始,会将所涉及到的Action在文末进行总结注释。
制作流程
准备场景
首先新建一个项目。

导入PlayMaker插件,会在项目Assets文件夹中生成如下文件结构。

由于PM的安装导入过程还是需要一定的时间的,而且未来可能会有很多附加的Actions包需要一并使用,所以个人建议把常用的Actions包导入以后,或者至少在安装完PM主程序之后就关闭Unity3D,将这个项目文件夹打包保存起来,以后再要新建使用PM的项目时,直接将这个压缩包解压改名使用即可,可以避免重新安装的麻烦。
创建场景元素:
- 一个
Ground
当地面,一个Cube
当敌人,摄影机放正; - 添加必要的UI,一个
Hint Text
作为游戏进程提示(告诉玩家什么时候可以开枪了),一个Score Text
显示玩家分数;-
Hint Text
上还添加了一个Shadow组件(直接用Add Component功能,输入“Shadow”即可找到)以增加一点阴影效果;
-

- 最后还需要添加一个空物体
GameController
作为添加FSM的对象。

设计交互逻辑
为GameController
添加一个FSM。
这个游戏的进程非常简单,所以我们只在
GameController
上添加一个FSM作为全部的交互逻辑设计即可,但未来比较复杂的项目则需要针对不同的游戏物体添加不同的FSM。
我在GameController
的FSM中建立如下Graph(流程图),这是没有添加任何Action的Graph,只是用来表示交互逻辑的设计思路:

这里所使用的
FINISHED
事件是一个系统事件,代表“该状态中的所有工作都已经结束”,当条件满足时会自动触发状态跳转,但其他的事件(比如Start、Player Fire等)都是自定义事件,需要用Action来进行触发。
- 当场景被载入时,进入
Start
状态,在该状态中会进行游戏场景相关参数的预设工作; - 预设工作完成,进入
Wait
状态,这个状态中会等待一个随机的时间,然后开始“对决”,Wait状态中会提示让玩家等待; - 开始“对决”即进入
Duel
状态,这时会提示玩家可以开枪,并通过鼠标左键的按下触发Player Fire
事件,或者通过一个随机等待时间来触发Enemy Fire
事件,两个事件分别导致不同的结果状态; - 如果玩家先开枪,则玩家胜利,进入
Win
状态,玩家得分,随即继续游戏(通过进入Continue
状态,然后返回Wait
状态); - 如果敌人先开枪,则敌人胜利,进入
Fail
状态,重新开始游戏(通过进入Restart
状态,然后返回Start
状态)。
这时我们可以运行场景来测试一下整个逻辑。绿框状态代表“当前所处的状态",可以用Alt
+鼠标左键单击的方式来手动触发自定义事件来查看状态的跳转情况。

- 考虑到“玩家提前开枪”的可能性,为
Wait
状态添加一个Too Quick
事件,触发该事件则会进入Warning
状态,警告玩家,然后直接判定玩家该局游戏失败(进入Fail
状态)

需要注意的是,这里所设计(或规划)好的逻辑流程不一定就是我们最后所使用的流程,有时候因为流程过于简单(比如我们这个范例),会将多个逻辑状态合并成为一个;也有时候会因为PM功能的限制或者因为流程过于复杂,需要将一个逻辑状态拆分成多个,甚至分成不同的FSM来实现。
即便是这个简单的范例,在后面我们也会对目前这个Graph进行调整。
添加Actions制作交互功能
下面开始为各个State(状态)添加Actions(行为),使其能够执行一些实际的交互功能。
首先给Start状态添加一个Wait行为,设置让其等待2秒之后再触发FINISHED事件。


然后给Wait状态添加“随机等待”的功能。这里先使用Random Float行为获得一个随机等待时间,储存为变量wait time,然后用Wait行为读取这个时间作为等待时间,设置Finish Event为Start。
这个操作与使用Random Wait行为所获得的结果是一样的。

在Duel状态中添加一个Get Mouse Button Down行为,以监测鼠标左键(Left)是否被按下,如果是,则触发Player Fire事件。
同时添加一个Random Wait行为,设置一个Enemy反应时间(0.4~1s),等待结束则触发Enemy Fire事件。
按这样的方式来设置,不论Player Fire事件和Enemy Fire事件哪个先被触发,都会跳转到别的状态,从而导致另一个事件永远不会被触发。同时,由于Get Mouse Button Down行为在前而Random Wait行为在后,所以即便两个事件的触发条件被同时满足(基本可能发生这种情况),也是Player Fire事件先被触发。

给Win状态添加Int Add行为,新建一个Int变量score int,并指定该行为为score int增加1。然后添加一个Wait行为,使其可以在Win状态停留2秒钟,再跳转到Wait状态,进入下一轮游戏。


在Variables面板中将score int变量设置为在Inspector中可见,并确保其初始值为0。

给Fail状态添加Set Int Value行为,指定让变量score int值归0,然后也添加一个Wait行为,使其停留2秒钟后再跳转到Wait状态,重新开始游戏。
严格按照逻辑来讲,“分数归零”的操作应该放在Start状态中,因为这个状态是用来进行初始化设置的,失败之后重新开始,自然要重新初始化,但像现在这样在Fail状态中就归零分数,然后跳转Wait状态,结果上不能说错,但逻辑上并不是很正确。
当然,在使用PM的时候,这样的“调整”、“简化”会时常发生,说不上来就是是好是坏,毕竟“灵活性”与“不规范”有时候是一体两面的存在。

比起“重置游戏参数”这样的精细操作来,更为简单粗暴地制作“游戏重置”的方法是使用“重新载入本场景”的Unity内置功能,在PlayMaker中就是是使用Restart Level行为。我们可以将Fail状态的FINISHED事件直接连给Restart状态,然后给Restart状态添加一个Restart Level行为。

将Duel状态中的Get Mouse Button Down行为复制(Ctrl+C)并粘贴(Ctrl+V)给Wait状态,将Send Event设置为Too Quick(或者直接添加该行为并照此设置也可以)。这是给Wait状态添加“监测玩家是否提前开枪”的功能,如果是,则触发Too Quick事件跳转到Warning状态。


在Warning状态中添加Wait行为,设置为1秒后完成。(然后就会跳转到Fail状态,玩家失败。)

运行场景,当玩家失败并触发场景重置时,场景会变得很暗,原因是重置后的场景失去了光照贴图。这是由于Unity3D5.6版本会自动为3D项目场景生成临时的光照贴图,这样我们默认就能看到比较“亮”的三维场景,但由于这个光照贴图是临时的,所以重置场景之后就丢失了。

我们可以通过菜单Window -> Lighting -> Settings打开Lighting面板,在Scene一栏中取消最下面Auto Generate的选项让Unity不再自动创建光照贴图。

然后分别将Cube和Ground物体设置成“光照贴图静态”(勾选上Mesh Renderer组件中的Lightmap Static选项)。

由于Ground地面物体特别大,所以最好将Scale in Lightmap参数设置小一些(我设置成0.1),以免浪费。
这里已经有提示说“Object's size in lightmap has reached the max atlas size.”,也就是说系统已经在提示你这个物体的光照贴图已经大得超标了。

然后点击Lighting面板中的Generate Lighting按钮为场景创建一个光照贴图。

经过一定时间的等待。

在场景文件的同级目录下会出现一个与场景文件同名的文件夹,其中的文件,就是我们烘焙出来的光照贴图数据和反射探针数据。

选择场景物体,在Lighting面板的Object maps一栏中可以看到当前物体的光照贴图。

关于光照贴图烘焙以及场景灯光的相关知识,可以参考我的其他文章。
对UI元素进行控制
因为我使用的是较新的Unity GUI(俗称uGUI)功能来制作的UI元素,所以需要用到PlayMaker的uGUI相关Actions来进行控制,这些Actions并没有被整合进PM安装包中,需要单独下载安装。
在 https://hutonggames.fogbugz.com/default.asp?W1192 可以找到这个行为包,只需要下载 Package for uGUI Proxy With Full set of Actions 就好了。

安装完成后在Action Browser中就可以看到uGui一栏,其中有很多很多的Actions。
如果不想安装第三方行为包(其实这个行为包也是官方做的,并不是什么第三方),也可以使用PM的Set Property行为来控制Text组件。
由于我们这个范例中的UI都是Text,所以只需要使用U Gui Text Set Text行为来设置其所显示的文本就行了。在Action Browser中输入关键字就可以找到相关行为,不需要在列表中慢慢寻找。

在Start状态中添加U Gui Text Set Text行为,由于Owner(本物体,也就是GameController物体)上并没有Text组件,所以行为会提示是否需要添加该组件,但我们并不是要来控制Owner的UI文字,而是要控制Hint Text物体的UI文字,所以需要设置Game Object参数为Specific Game Object,新建一个Game Object变量hint text object并指定其为这里的指定游戏物体。因为在Start状态时我不需要任何文字显示,所以在Text参数中只输入一个空格键。

这时其实我们还没有指定控制Hint Text游戏物体。我们需要在Variables面板中找到hint text object变量,使其显示在Inspector中,然后将场景中的Hint Text物体拖到hint text object变量中才真正完成参数的指定工作。

将这个U Gui Text Set Text行为复制粘贴到其他几个状态中,并修改Text参数使Hint Text在不同的状态能够显示不同的提示文字。





同理可以一样设置在不同状态更新Score Text中所显示的分数UI文字。
不过,为了简单(毕竟是初级入门教程嘛),我采用另一种“简单粗暴”的方法,让Score Text物体实时监控score int变量的值并将其显示出来。
为Score Text添加一个FSM。

在State 1中添加一个Get Fsm Int行为,指定Game Object参数为一个新的Game Object变量game controller object,指定Fsm Name为“FSM”,Variable Name为“score int”(这是我们之前就在GameController游戏物体的FSM中都已经设置好了的),并设置Store Value参数为新建的int变量score int。
这个行为的作用是,从目标游戏物体的目标FSM中获取一个变量的数值,并保存为本FSM的一个变量。虽然这里我们获取的目标变量和本地变量都取了一样的名字,但由于其隶属于不同的FSM,所以重名不会有影响。

手动将GameController物体指定给game controller object变量。

由于要实时监控score int变量值,所以Get Fsm Int行为需要勾选Every Frame选项。
然后再添加一个Convert Int To String行为,将(本FSM的)score int变量转换为一个字符串变量score string,同样勾选Every Frame选项以保持同步。

最后添加一个U Gui Text Set Text行为,让Owner(本物体)的Text组件中的文字显示score string变量中的字符串,同样勾选Every Frame选项以保持实时更新。

在这种处理方式下,Score Text每帧都会获取分数值、转换数据类型、更新UI显示,更优化的处理方法是让其只在需要的时候才会进行此操作,比如只在分数发生变化的时候才会进行一次“获取分数值、转换数据类型、更新UI显示”的操作序列。
添加音效
从 http://soundbible.com/ 找到几个声音:380 Gunshot、9mm Gunshot、Computer Error Alert,并将其导入到Assets文件夹中。



给Win状态添加一个Play Sound行为,指定其播放player gun shot变量中的声音。

给Fail状态添加一个Play Sound行为,指定其播放enemy gun shot变量中的声音。

给Warning状态添加一个Play Sound行为,指定其播放warning sound变量中的声音。

将这3个变量(player gun shot、enemy gun shot、warning sound)都设置为在Inspector中可见,并将对应的声音素材指定给这3个变量。


现在我们不知不觉中已经设置了很多变量了,它们默认是按照名称字母顺序排列,因此不同类型不同用途的变量混合在一起,看上去非常混乱。我们可以通过对不同变量指定Category的方式将其分组,这个分组也会反映在Inspector面板上。

增加游戏难度
这个游戏的机制非常简单,就是比谁快,所以增加游戏难度的最简单的方法就是提高Enemy的反应时间。我们之前是通过硬编码的方式在Random Wait行为中设置Enemy的反应时间为0.4s~1s,可以修改成将这个“最快/最慢反应时间”的属性设置给Enemy物体,然后用GameController去获取,然后再设置给Random Wait行为。这样一来,就可以通过“更换不同的敌人”来达到修改游戏难度的目的。
也可以在每次胜利之后,也就是在Win状态和Wait状态之间,对Enemy的反应时间进行修正,比如“每次胜利之后敌人反应速度提高一成(即原反应时间的90%)”,这样敌人的反应则会越来越快,游戏也越来越难。
本地双人对战模式
如果想要将游戏改成“Player1 vs. Player2”的本地对战模式,则可以将Duel状态中的Random Wait换成另外一种监测玩家输入的行为(比如Get Key Down),然后两个玩家一个用鼠标左键,一个用键盘空格键,看谁反应快。当然如果这样的话,则需要设置两个不同的Score UI,以显示不同玩家的分数。
总而言之,在这个超级简单的游戏原型的基础上,可以进行很多新的拓展。
我计划在Duel Game Advanced版本中加入角色动作,并以双人对战为主要模式,敬请期待!
本教程涉及到的Actions:
Wait
等待指定时间之后触发指定事件。勾选Real Time选项将使其忽视游戏时间缩放(Time Scale)的设置,比如利用Time Scale = 0来暂停游戏时,如果没有勾选Real Time选项,该Wait行为也会暂停,但如果勾选了Real Time选项,则不会受到游戏时间暂停的影响。
Random Float
获得一个在所设置的最大/最小值中间的随机float值,并储存为指定变量。
Get Mouse Button Down
当鼠标的指定按键(Left、Middle、Right)被按下时,触发指定事件。如果In Update Only选项被勾选,则在同一游戏帧内,该行为仅执行一次(即按住鼠标不放不会导致无限循环错误)。
Random Wait
在所设置的最大/最小时间范围内随机等待一个时间段后触发指定事件。勾选Real Time选项将使其忽视游戏时间缩放(Time Scale)的设置,比如利用Time Scale = 0来暂停游戏时,如果没有勾选Real Time选项,该Wait行为也会暂停,但如果勾选了Real Time选项,则不会受到游戏时间暂停的影响。
Int Add
将所指定的int类型变量的数值增加一定值(可以设置增加量为负值)。
Set Int Value
将所指定的int类型变量的数值设置为所输入的数值(或所指定的int类型变量的值)。
Restart Level
重新载入当前关卡(所有本地变量数值都会重置,但全局变量数值不会)。
U Gui Text Set Text
将指定物体的Text组件的Text参数(即其显示的UI文字内容)设置为所输入的字符串(或所指定的string类型变量的值)。
Get Fsm Int
获得所指定的游戏物体上指定名称的FSM中指定名称的int变量的数值,并储存为自身FSM的int变量。这个行为也可以用来获取同一个游戏物体上的不同FSM中的int变量的数值。
Convert Int To String
将一个int(整数)类型的变量转换成string(字符串)类型的变量,并储存起来。Format参数可以对这个转换进行一定的格式化,比如对于3这个int,如果直接转换会得到“3”这个字符串,如果设置Format为“0000.00”,则会得到“0003.00”,如果设置Format为“Score:00”,则会得到“Score:03”。