PlayMaker是什么?
PlayMaker是Unity3D的一款 可视化 的 有限元状态机(Finite-state machine,简称Fsm) 插件,用来进行交互设计。
有限状态机(英语:Finite-state machine,缩写:Fsm)又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
我个人对Fsm的理解是这样的:
Fsm将对象的复杂行为特征归纳为有限个不同的“状态”,然后在每个状态中分别指定一系列“行为”让处于该该状态的对象来执行,同时设置一些“条件”(在FSM中称做“事件”),当这些条件被满足时(按FSM的说法就是事件被触发),对象的从当前状态变换为另一个状态,由此带来其所执行“行为”的的变化。
很多人把PlayMaker等同于“可视化编程”,官方在宣传时也自我标榜为“PlayMaker - Visual Scripting for Unity3D”。在我看来,这么说其实是不太恰当的。
Unity3D中真正可以称作“可视化编程插件”应该是 uScript Professional,而PlayMaker只能算是一个“可视化交互设计插件”,因为PlayMaker只允许使用FSM这一种“编程策略”,而且Fsm这一策略的设计思路其实是有别于正常撰写程序脚本时所采用的设计思路的。
准确的说,PlayMaker提供了一套可视化的解决方案,让用户可以无需撰写脚本代码,就能运用有限元状态机的设计思路在Unity3D中设计并实现交互逻辑。
对于完全没有编程基础的用户来说,了解PlayMaker和编程的区别没什么太大的意义。但对于有一点点编程基础(却还没有深厚到自己写状态机的程度),但又想要用PlayMaker来做交互的人来说,直接把以前写简单程序的思维习惯代入PlayMaker,是会吃苦头的。
所以,我们学习PlayMaker,最主要的是去学习一种交互设计的思维方法。掌握PlayMaker本身的特性和功能当然很有必要,但最终的目的是建立一种思维习惯,这种思维方法和思维习惯,是可以带到其他软件工具中去的,比如UE4的 Blueprints(蓝图系统),甚至真正的Programming。
PlayMaker究竟能做什么?
PlayMaker曾被认为是能够将游戏制作者从“代码”的汪洋大海中拯救出来的“救世主”,这当然是一个笑话。
虽然PlayMaker官方网站上列举了很多使用PlayMaker制作的游戏产品。但实际上,如果你希望通过几周的学习,掌握了解PlayMaker本身用法之后就能够制作一个功能完成的游戏产品,那是极为不现实的。
PlayMaker可以让用户摆脱“写代码”这项工作,但不代表可以让用户摆脱“程序设计”这项工作,而且由于PlayMaker完全基于状态机这种“策略”,实际上增加了“程序设计”的工作难度。掌握PlayMaker这件工具本身的使用方法只是第一步,更重要的是利用这件工具去锻炼我们思维,去解决实际问题。
PlayMaker真正的用途并不是去替代那些程序员(游戏开发)的编程工作,而是给非程序员提供一个快速制作玩法原型(prototype)的工具,让他们能够独立把脑海中“想象”的玩法设计实现出来,无需花费好几年的时间去专门学习编程,也无需去“跪求”程序员大大们的帮助。
当我们经过努力的学习,逐渐能够使用PlayMaker实现一个相对完整的游戏交互逻辑的时候,其实我们的“编程思维”就不知不觉中建立起来了。那些使用PlayMaker制作出完整游戏的开发者,我相信大多数都是很优秀的程序员,因为不论是C# Scripting还是PlayMaker FSM,其背后的本质都是所谓的“计算机程序设计思维”。
所以,PlayMaker究竟能做什么?这个问题其实挺不好回答的。PlayMaker只是一个工具,有优势,也有不足。我们能用这个工具做什么取决于我们自身,而不是这个工具。
PlayMaker的基本概念:Fsm、States、Events、Transition、Actions、Variables
Fsm(状态机)
举个小例子
前面已经讲过了Fsm的基本定义,现在举一个日常生活的例子。
我们一天中会做很多事情,但我们可以把这些事情归纳为几个状态:睡觉;吃饭;上课;打游戏等。如果把早上作为一天的开始,我们的生活逻辑是这样的:
- 一开始我们在睡觉(初始状态),但设了个闹钟(触发条件);到7点了,闹钟响起来(满足触发条件),我们去吃饭(状态转换);吃饭完成(满足触发条件),我们去上课(状态转换);下课铃响(满足触发条件),我们又去吃饭(状态转换);吃完(满足触发条件)看看时间(新状态哦),如果还早(满足触发条件1),就打打游戏(状态转换);如果时间不早了(满足触发条件2),就赶紧去上课(状态转换);……
可以看到,如果我没有添加“看看时间”这样一个新状态,只有吃饭睡觉上课打游戏的状态机是多么无聊啊!当然,其实现在也挺无聊的,不过我们可以尝试把“看看时间”这个状态添加到整个生活逻辑的各个部分,就会有意思很多。
在不同的状态下我们会做不同的事情,睡觉状态下会循环进行“呼吸”操作。吃饭状态下会依次进行“夹菜”、“送入口中”、“咀嚼”、“吞下”等操作。重要的是,不同状态下的行为设计是彼此独立的,而且与交互逻辑的设计本身也是彼此独立的。我们可以在进行行为设计之前就完成完整的交互逻辑设计,然后再慢慢添加从简单到复杂的各状态行为。
创建Fsm
在PlayMaker中,Fsm是被作为component(组件)添加给GameObject的。因此,一个Fsm可以被看做是一个独立的脚本程序,用以实现一个独立的功能。
从菜单PlayMaker
> PlayMaker Editor
中打开PlayMaker编辑器。
选择需要添加FSM的GameObject(这里我新创建了一个Cube),打开PlayMaker编辑器,在编辑器中按照提示点击鼠标右键,选择Add FSM
,就为该Cube附加了全新的FSM类型的组件(Component):
这时界面上已经提示我们的Fsm是被添加在Cube物体上了:“Cube:FSM”(注意,这里的“FSM”代表的是这个Fsm的名字,新建的Fsm的名字都叫做“FSM”、“FSM 1”、“FSM 2”,最好能够修改一下,以免混乱)。
Inspector面板上我们可以看到这个Component:
没有很多参数,可以修改名称,可以点击Edit
打开编辑器来进行详细设置,可以添加一些简单描述Description...
,还可以选择一个Fsm模板以重复利用以前的工作成果。
PlayMaker的编辑器分两栏,左边叫Graph(图表面板),右边是参数面板。参数面板有4部分组成:FSM、State、Events、Variables,分别用来对状态机、状态、事件、变量进行设置。
在FSM栏中其实没有什么太多需要修改的,名称要改一下,我通常会使用FSM_*
的方式来命名,然后单词首字母大写,单词间不留空格,比如FSM_Movement
、FSM_HealthControl
这样。
当我们创建了一个新的Fsm时,会自动得到一个初始状态State 1
,这个State 1
上方还有一个START
的图标并有箭头指向State 1
。
START
这样位于状态上方的黑底方块叫做“Global Transition”(全局转换),而且START
这个事件本身还是个“系统事件”。关于事件和转换的问题等下再讲,现在我们只需要知道这个图示的意思是:当一个叫做“START”的事情发生时,这个Cube物体进入“State 1”状态,由于“START”在Fsm组件被创建的那一刻就会发生,也就是这个Cube物体被创建的时候就会发生,所以当场景被载入时,游戏物体Cube就已经处于“State 1”状态了。
State(状态)
在空白处点击鼠标右键,选择Add State
,可以添加一个新的状态State 2
。可以看到新的State 2
上是没有触发条件的,这说明State 2
完全没可能被激活,也就是完全不起作用。
我们可以在State 2
上单击右键并选择Set as Start State
,那么State 2
就会变成我们的初始状态,State 1
不起任何作用。
点击Unity3D工具栏中间的Play按钮运行场景,我们会发现State 2
上有一圈绿光,PM编辑器左下角也有提示当前状态处于“State 2”。
这时我们是没有办法让Cube返回“State 1”的,因为我们还没有设置任何触发条件。
Event(事件)和Transition(转换)
下面我们来添加一些触发条件,也就是event事件。
在State 2
上点击右键,选择Add Transition
> FINISHED
。(“FINISHED”也是一个系统事件,代表“本状态已经执行完所有操作的意思)
这时候State 2
下面多出来一行FINISHED
,说明我们已经添加了一个Transition(转换)给State 2
,同时这个Transition的发生条件是FINISHED
事件被触发。
但这时出现了一个错误标志(红色圆圈中间有白色感叹号),这是因为我们给一个State添加了Transition,却还没有指定这个Transition的目标State,就好像我们上了一辆的士,却还没告诉司机往哪里开一样。这个错误标志就是在提示我:你小子还有事情没干完呢!
用鼠标把FINISHED
拖到State 1
上松开,我们就指定了这个Transition的目的地是State 1
,也就是说我们告诉电脑,当FINISHED
这个事件被触发时,请将Cube的当前状态转换到State 1
,这时候错误提示消失了。
当我们选择一个state的时候,编辑器右边会自动切换到State栏,这一栏中应该是储存该状态下需要执行的所有Action(动作)的,但在这个例子中我们还没有添加任何Action,所以这里是空的。
点击State栏位右边的Events
进入Events栏,我们会发现这里已经有了一个叫“FINISHED”的Event了,而且显示被使用过1次。Fsm中所有被使用到的Events都会被显示在这一栏内,我们可以通过查看Used
数挑出那些无用的Event并删除掉。
如果需要添加新的Event,只需要在下方Add Event
中输入名称,点击Enter
(回车)键就可以了。
Event最前面的小方框如果被勾选的话,这个Event就会变成一个“Global Event”(全局事件)。普通的Event只能在Fsm内部被触发,而Global Event可以从Fsm外部被触发。也就是说Fsm A中的global event可以在Fsm B中通过特殊的Action(
Send Event
)来触发,但Fsm A中的普通event只可以在Fsm A内部被触发。
前面在添加Transition的时候会看到除了可以添加普通Transition以外还可以添加Global Transition(全局转换)。全局转换的形态就是好像最前面的START
一样,位于State的上方且黑底。所谓全局转换的意思是不论物体当前处于哪个State,只要该Event被触发,都会转换到目标State。而一般的Transition,只有在其所在State是当前状态的情况下才能够进行转换。
区分清楚事件/全局事件、转换/全局转换、变量/全局变量等概念对于掌握PlayMaker非常重要。
下面我们可以把之前的生活逻辑做成一个Fsm了:
在场景中新建一个空物体(Create
> Create Empty
),Reset位移,改名为MyLife
。
打开PM编辑器,建立如下Graph:
看起来,我们的生活还是蛮复杂的嘛!
点击Play:当前状态停留在Sleeping
。因为我们还没有设置任何Action,所以也不会自动触发任何非系统Event,但我们可以手动激活Transition。
按住Alt
键不放,点击我们希望激活的Transition,会发现当前状态发生了改变。当状态改变到Getting up
的时候,会很快跳到Breakfast
然后又跳到Check time
,这是因为FINISHED
是一个系统事件,状态中的Action都执行完毕就会自动跳转。
“无限循环”错误:
下面我们建立一个简单的Graph来演示一个PlayMaker中很常见的错误:
我们希望状态1完成以后去完成状态2,状态2完成以后又回到状态1,貌似是个很正常的逻辑,在两个状态间不断循环。(底下的State 3
是这个错误的简化版,也是不断循环执行自身的意思)
但如果点击Play
按钮,会出现[DISABLED]
提示,Unity的Console栏也会出现错误提示:
Error : FSM : Loop cont exceeded maximum: 1000 Default is 1000. Override in Fsm inspector.
估计大家在自己实践的过程中会经常看到这个错误提示。它表面上的意思是告诉我们,这个FSM循环了超过1000次,预设的最大循环次数是1000,请在Inspector面板中修改设置。如果大家真的跑去修改这个预设的最大循环数,可能会出现两个可能,一是会超过你新设置的最大循环数继续报错,二是会导致死机。
为什么呢?我们在两个State中都没有任何Action,所以会立即转换到目标State,于是在游戏时间1帧内,State 1
和State 2
会不断转换,直到达到1000次被强行停止,在这个过程中,游戏一直停顿在那一帧,如果你的电脑很快,也许这个错误信息不到1秒钟就报出来了,但如果你的电脑很慢,可能会停顿四五秒才会达到1000次循环,在这四五秒中整个游戏是卡住的。
这个错误类似于编程中出现的“无限循环”错误。如果出现这个错误,首先要检查一下我们的程序逻辑有没有问题,如果逻辑上确实没有问题,可以给
State 2
添加一个叫Next Frame Event
的行为,然后用这个行为触发FINISHED
事件,这样从State 2
往State 1
的转换就会被强制在下一个游戏帧中发生。
System Events(系统事件)
-
APPLICATION FOCUS
:游戏运行时 -
APPLICATION PAUSE
:游戏暂停时 -
APPLICATION QUIT
:游戏退出时 -
BECAME INVISIBLE
:物体不可见时 -
BECAME VISIBLE
:物体可见时 -
COLLISION ENTER
:碰撞体进入时 -
COLLICION ENTER 2D
:2D碰撞体进入时 -
COLLISION EXIT
:碰撞体离开时 -
COLLISION EXIT 2D
:2D碰撞体离开时 -
COLLISION STAY
:碰撞体停留期间 -
COLLISION STAY 2D
:2D碰撞体停留期间 -
CONTROLLER COLLIDER HIT
:Controller类碰撞体被触碰时 -
JOINT BREAK
:骨骼断开时 -
JOINT BREAK 2D
:2D骨骼断开时 -
LEVEL LOADED
;关卡载入时 -
MOUSE DOWN
:鼠标在物体上被按下时 -
MOUSE DRAG
:鼠标在物体上被按下然后拖动时 -
MOUSE ENTER
:鼠标滑入物体时 -
MOUSE EXIT
:鼠标滑出物体时 -
MOUSE OVER
:鼠标悬停物体之上时 -
MOUSE UP
:鼠标在物体上按下并松开时(单击) -
MOUSE UP AS BUTTON
:鼠标单击(作为按钮) -
PARTICLE COLLISION
:粒子碰到碰撞体时 -
TRIGGER ENTER
:触发器被进入时 -
TRIGGER ENTER 2D
:2D触发器被进入时 -
TRIGGER EXIT
:触发器被离开时 -
TRIGGER EXIT 2D
:2D触发器被离开时 -
TRIGGER STAY
:触发器被停留期间 -
TRIGGER STAY 2D
:2D触发器被停留期间
普通的event必须由Action来触发,而系统事件则无需通过Action来触发。我们可以做一个简单的例子,通过鼠标左键的按下和松开来控制两个状态之间的转换:
新建场景,创建一个Cube,Reset位移。然后打开PM编辑器,创建FSM,并另外创建一个State 2
。在State 1
上点击右键,选择Add Transition
> System Events
> MOUSE DOWN
,同理在State 2
上添加MOUSE UP
,然后把它们连起来。
在Scene View中按然后点击Play运行场景。f
键使Cube居中,
保持PM的编辑器可见,然后在Game View中按下鼠标左键。
如果是在Cube上按下左键的话,Cube会进入State 2,松开鼠标,Cube返回State 1。
我划掉上面前半句的意思是这步操作没什么用。测试场景必须在Game View中进行,在Scene View中改变视角并不影响Game View。
要注意的是,这个范例能够做成功,是因为我们的Fsm是添加在一个Cube物体上的,
MOUSE UP
和MOUSE DOWN
这两个系统事件是探测鼠标是否在当前物体范围内被按下或者松开,因而都需要这个“当前物体”是具有Collider碰撞体设置的,而Cube创建出来就自带Collider,这样才没有出错。
Network Events(网络事件)
-
CONNECTED TO SERVER
:连接上服务器时 -
DISCONNECTED FROM SERVER
:从服务器断开时 -
FAILED TO CONNECT
:连接服务器失败时 -
FAILED TO CONNECT TO MASTER SERVER
:链接主服务器失败时 -
MASTER SERVER EVENT
:主服务器事件 -
NETWORK INSTANTIATE
:网络重名时 -
PLAYER CONNECTED
:玩家连接成功时 -
PLAYER DISCONNECTED
:玩家连接中断时 -
SERVER INITIALIZED
:服务器重置时
Action(动作)
如果说使用Fsm、States、Events和Transitions可以搭出一个合理的交互逻辑的框架的话,这个交互逻辑在添加Action之前就完全是一个空架子,一个设计而已。只有添加了Action,State才变得有意义,GameObject才会随着PlayMaker设计的这个逻辑来行动。
PlayMaker有非常多的Action,而且还有很多开发者在为PlayMaker编写各式各样的第三方Action(可以理解成有人为PlayMaker这个插件开发插件),一个Action通常执行一项或几项Unity3D的“操作”,比如获取某个GameObject的位置,在场景中新建一个Cube,改变一个材质球的颜色,为一个变量赋值等等。
选择一个State,点击编辑器右下角的Action Browser
可以打开动作浏览器。第一眼看到这个浏览器我整个人是崩溃的,那么多Action找都找不过来,更别说使用了。好在这个浏览器提供了搜索过滤功能,我们可以输入一些关键字来快速定位我们想要使用的Action。
大家可以到 这里 查看完整的行为列表手册,特别特别多。
Ceeger上有 较早版本的PlayMaker汉化手册 可供下载,不过和现在的1.8.4
版已经有些不同了。虽然PlayMaker可以设置语言为中文(
Preferences
>General
>Language
>Chinese Simplified
),但暂时并没有汉化Action列表。
怎么学习PlayMaker的Action
这么多Action应该怎么去学?我们要不要把每个Action的用法都背下来?我个人的经验是从实践中去学习。
首先当然需要把整个Action列表浏览一遍,大致知道PlayMaker提供了哪些方便有用的Action给我们,又有哪些是很基础性的功能,简单却常常会用到。
Action列表自身的组织结构是有分类的,但这个分类主要是依照Action所处理的对象来分的,而且主类就有几十个,并不是特别适合我们学习。我这里提供一个我自己理解的依照Action所执行功能而做的大致分类给大家参考:
- 用来获取参数/变量数值的
- 用来改变参数/变量数值
- 用来创建或删除游戏物体
- 用来给游戏物体添加或删除组件
- 用来执行某个组件(或脚本)中的特定功能函数(Function)
- 用来触发Fsm事件
- 对整个游戏系统进行控制,比如暂停、退出、载入场景等等
其实,Action列表中有很大一部分是我们平时很少会用到的,甚至有一些是针对特定插件的支持,如果不用这些插件,就根本不会需要用到这些Action。
按照功能标准在脑海中对所有Action有了个初步印象以后,就要记忆一些关键字了,因为我们99%的情况下,都是通过关键字在Action Browser中去搜索需要的Action,而不是顺着列表慢慢找。记住这些关键字可以极大的方便我们定位Action,节省时间。这些关键字包括:get
、set
、gameobject
、position
、fsm
、float
、bool
、vector3
、collision
、trigger
、ray
、compare
等等。
这个记忆工作可以随着我们学习和练习的过程来做,看得多了,做得多了,慢慢也就知道要实现哪些功能需要用到哪些Action了,那么这些Action的用法就是需要熟练掌握的。总而言之,学习Action要按照“功能类”来一类一类地学,而不是一个一个Action地学。
对于我们用到的Action,花一点点时间去看看这个Action的具体用法。在Action Browser中定位到Action之后是可以看到一个简单的描述的,英文好的同学可以直接阅读一下,看不懂的就去找中文翻译。我有时间的时候也会慢慢完成 PlayMaker Actions (未完成) 这一篇文章,以供大家参考。
对PlayMaker有个基本的掌握之后,网上的PlayMaker教程对你基本上就没有什么难度(总体来说PlayMaker的教学资源还是太少,也太浅),这个时候可以开始看那些入门级别的Unity3D小游戏教程,看人家怎么写脚本来实现交互设计的,然后把别人的思路转化到PlayMaker里面,用PlayMaker去重现这些教程的内容。
关于 Every Frame
选项
有些Actions会具有一个Every Frame
的选项,通常都在最下方。勾选这个选项,会迫使这个Action每帧都执行一遍,直到游戏物体离开当前状态。不勾选的话这个选项的话,每次进入这个状态以后,这个Action的操作都会进行一次,不论游戏物体在这个状态停留多久。
但这并不是说没有这个选项的Action就都是只执行一次的,没有这个选项代表这个Action要么只能执行一次,要么是必须每帧执行的。
怎么来判断是否勾选这个选项呢?比如我们有个Action是监控是否有子弹击中玩家的,那么这个Action当然要每帧都执行一遍咯,但如果是发射子弹的Action,通常就不能每帧执行了,仅在“发射”状态被激活时执行一次就可以了。
如果一个State的列表中有至少1个
Every Frame
类型的Action,那么这个State就不可能自然终止,也就是说系统事件FINISHED
永远不会被触发,因为这个Every Frame
类型的Action永远不会执行完毕。
这里有一个思维误区,就是理所当然的认为从一个状态转换到另一个状态的时候,游戏进入了新的一帧,这是不对的。如果没有
Next Frame Event
的话,从状态A转换到状态B然后再到状态C是有可能发生在同一帧游戏时间以内的。因此在PlayMaker中利用状态转换做循环要非常小心,否则很容易在一帧里面无限循环。
变量(variables)
变量是用来储存数据/数值的。
Unity3D自身有变量,不同的Component都有很多或私有(private)或公开(public)的变量,PlayMaker可以通过Action去调用它们(Get Property
)或者直接对其赋值(Set Property
)。
PlayMaker自身也有变量,我们叫做Fsm变量,以区别于Unity3D的变量。调用其他Fsm的变量需要用到Get FSM Variable
这个Action,为其他Fsm变量赋值要用到Set FSM Variable
这个Action。
对于本Fsm内部的变量进行操作是最简单的,很多Action都可以读取某个内部变量值或者将某个值储存在内部变量中。
变量需要事先申明其“类型”,PlayMaker对于变量类型的要求非常严格,大家可以在 Unity3D的数据类型以及PlayMaker的变量 一文中详细了解。