Unity:使用 UnityEngine.Events 让程式更灵活、稳定
自从 Unity 4.6 发佈新的 GUI 系统之后,我们可以从所建立的 GUI Control 发现新的事件栏位,例如,建立一个 Button,可以从 Inspector 视窗裡面的 On Click 栏位指定当按钮被点击时要去执行那一个 GameObject 上的哪个 Component 的功能,使得按钮事件可以更视觉化、更弹性的编辑,当然,其他的 GUI Control 也都有类似的栏位可以做这样的设置,而这个栏位则是由 UnityEngine.Events 底下的 UnityEvent 型别所产生的,我们自己所製作的 Component 同样也可以提供这样的栏位,使可以视觉化编辑,以及让程式更加弹性。
在此影片使用两个范例来示范作法,透过画面操作以及语音的说明,相信可以更加了解到
首先,简单来说明一下 Unity 写作程式的基础,就是当我们在 Project 视窗建立我们自己的 Script 之后,将这个 Script 附加到 GameObject 之上,就会成为该 GameObject 的 Component,通常,变数栏位(Field)如果是使用 public 修饰字来宣告,而其型别是可序列化的话,在 Inspector 视窗就会产生可以设定其值的栏位,这是因为 Unity 本身预设会对 public 栏位自动序列化的关系。
public 的变数栏位会出现在 Inspector 视窗,可供编辑。
可是,如果 Class 裡所宣告的栏位并没有要提供给外部存取的话,使用 public 修饰字就有些不恰当,所以,当使用 private 或 protected 等其他修饰字来宣告时,如果,希望该栏位变数也可以在 Inspector 视窗出现可编辑的栏位,可以在它的上一行插入 SerializeField 的 Attribute(特性),那麽 Unity 就知道这个栏位是要序列化使可以在 Inspector 视窗编辑。
有 SerializeField 的 private 变数栏位也会出现在 Inspector 视窗。
如果,要製作像是 UGUI 的 Button 那样的事件栏位,最基本的就是要在程式档最上方加个 using UnityEngine.Events,然后只要是使用 UnityEvent 为型别宣告的栏位,都会有个像是 Button 那样的事件栏位。
UnityEvent 型别的变数栏位,在 Inspector 视窗会出现事件栏位。
这个 UnityEvent 栏位在编辑器上的编辑相当弹性,当目标 Component 裡面含有使用 public 宣告的属性(Property)或方法(Method),并且传入值的型别为 bool、int、float、string 其中一种的话,就能直接在这裡选择并提供传入的值,而且可以设置多个不同型别的目标,并在之后程式执行时直接呼叫执行,另外,如果 Method 并没有宣告传入参数的话,在此也可以选择设置,如果是宣告两个以上的参数,在这边的选单就不会出现了。
虽然 UnityEvent 栏位相当方便和弹性,不过,它所传入的值是在编辑器上面设置的,而且只能设置一个值,如果,我们的 Component 中的程式想要直接提供参数值给目标的话,光靠 UnityEvent 就没办法做到,幸好,UnityEngine.Events 除了 UnityEvent 之外,还另外提供了四个泛型类给我们扩充使用。
我们在影片中的两个范例可以看到,其中的 PassEvents Script 专门用来撰写扩充 UnityEvent 泛型类的新事件型别,相关资料可以参考官方文件的 UnityEngine.Events 相关资料,例如 UnityEvent。总共有 UnityEvent、UnityEvent、UnityEvent、UnityEvent 这四个可供扩充,其实,他们所具备的功能都是一样的,只是,能接受的参数数量不同而已,也就是说,我们可以透过继承这几个泛型类来宣告我们自己需求的参数型别的 UnityEvent,而且可容纳的参数数量可以有一到四个。例如:
[System.Serializable] public class PassString : UnityEvent<string>{} [System.Serializable] public class PassColor : UnityEvent<Color>{} 复制代码
因为,所宣告的 class 希望用于宣告栏位变数时,可以显示于 Inspector 视窗,除了使用 SerializeField 标示在栏位上面之外,该型别也必须是可序列化的才行,所以,还要在 class 上方也加上 System.Serializable。
到这裡,我们就能够使用自己定义的 UnityEvent 来宣告事件栏位,并使它可以显示于 Inspector 视窗。
###1. Simple Computer 第一个范例裡,主要是製作一个简单的计算器来示范 UnityEngine.Events 的用法以及所带来的好处;製作任何东西之前,一定要先了解到这个东西的使用目的以及功能有哪些,所以,我们先用简单的 UI 排出一个运算式的样子,这个运算式提供了两个输入栏位、一个运算符号的文字、一个显示计算结果的文字,以及四个运算功能按钮。
在此,我们先定义计算器的基本功能:
- 点击运算功能按钮,会依照计算种类去改变运算符号的文字。
- 点击运算功能按钮之后,在 UI 显示计算结果文字。
功能相当简单,所以,我们直接在 Project 视窗建立一个 C# Script 就差不多能处理全部的功能,先将此 Script 命名为 MyComputer。
由于,最后会将计算结果转换为字串传给 UI 去显示,在这裡会使用到 UnityEvent 去传递资料,只是一般的 UnityEvent 并无法直接在程式呼叫时就带入参数,所以我们还需要使用前面提到的做法来宣告可以带字串参数的 UnityEvent,因为,宣告这个衍生类只需要写一行,而且,将来还可能会宣告好几个这种带不同参数的 UnityEvent,所以,我们还要在 Project 视窗建立一个名为 PassEvents 的 C# Script,专门用来写这些 Class 用,如果分类好,这支 Script 还可以直接拿到其他专案中使用。
在 PassEvents Script 裡面,首先,由于是要製作 UnityEngine.Events 的那些泛型类的衍生类,所以,最开头一定要先写一行 using UnityEngine.Events,接下来,在这个示范裡只会用到传递字串,所以,先只宣告一个能传递字串的 Class 就好。
using UnityEngine.Events; [System.Serializable] public class PassString : UnityEvent<string>{} 复制代码
别忘了 System.Serializable,否则,在 Inspector 视窗会看不到栏位。
而在 MyComputer Script 之中,我们这个简单计算器主要就是对两个计算值计算出结果而已,配合 UI 的使用,所接收到的值原本会是字串,但我们在程式中却必须做为数值来计算,所以,先宣告两个仅限内部存取的栏位来暂存传入的数值,这个数值的来源,则是要由 UGUI 的 InputField 的 Edit End 事件传过来的,当然,我们在 MyComputer Script 裡面,并不用管那麽多,只要提供两个 Property 让外部可以把字串传入就行了,管他是谁要传进来的,只要提供入口,并且把传入的字串转为数值暂存下来,这样其他计算功能就能计算了。
因为 MyComputer Script 的工作主要工作就是接收计算资料并计算出结果传递出去,所以,我们需要宣告每个计算结果的事件,来表达事情的发生,至于,最后会把结果传给谁,其实,并不需要 MyComputer 去管。
目前,我们定义 MyComputer Script 只做加、减、乘、除四个计算功能,于是,就直接将获得的数值计算出结果,并将结果转为字串后,透过 Invoke 来呼叫执行个别对应的事件,那麽计算器的基本功能就算完成了。
private float _value1; private float _value2; [SerializeField] private PassString onAdd; [SerializeField] private PassString onSubtract; [SerializeField] private PassString onMultiply; [SerializeField] private PassString onDivide; public string value1{ set{ float.TryParse(value , out this._value1); } } public string value2{ set{ float.TryParse(value , out this._value2); } } public void Add(){ this.onAdd.Invoke((this._value1 + this._value2).ToString()); } public void Subtract(){ this.onSubtract.Invoke((this._value1 - this._value2).ToString()); } public void Multiply(){ this.onMultiply.Invoke((this._value1 * this._value2).ToString()); } public void Divide(){ if(this._value2 == 0) return; this.onDivide.Invoke((this._value1 / this._value2).ToString()); } 复制代码
因此,获得以上的程式码,其中,除法 Divide() 的部分要特别注意,因为,除法裡的除数不得为零,不然,会发生错误,所以计算之前要先判断一下,如果除数为零,就不要执行后面的计算。
写好 MyComputer Script 的内容之后,在 Unity 中,先建立一个空物件并且赋予它 MyComputer 成为它的 Component,这时候,可以很明显地从 Inspector 视窗看到分别为加、减、乘、除的事件栏位。
MyConputer 出现加、减、乘、除的事件栏位。
接下来就可以在 UI 以及 MyComputer 间设置彼此之间的关系,首先,需要让 UI 输入的值可以传递给 MyComputer,所以,要从两个 InputField 中的 End Edit 事件分别设置将输入的字串传给 MyComputer 的 value1 以及 value2 两个 Property。
Value1 Field 输入的字串传给 MyComputer 的 value1
Value2 Field 输入的字串传给 MyComputer 的 value2
在这裡可以发现这个 End Edit 其实也是跟我们所宣告的 PassString 一样的东西,设置好之后,不需要在 Inspector 视窗上设置传入什麽字串,因为,这个 End Edit 事件是当 InputField 中的文字输入完毕后,按下 Enter 或者点击 UI 文字栏位外的地方,使它不能被输入文字时,而视为文字输入完毕时被呼叫执行的,当他被呼叫执行时,就会将其输入的文字透过这个事件而传递出去,所以,这裡设置完毕之后,只要 UI 上的文字栏位输入完毕后,都会将所输入的内容传递给 MyComputer,而 MyComputer 则如我们程式中所写的一样,将收到的字串转为数值暂存下来。
接下来,就是个别选择 UI 上分别代表加、减、乘、除功能的按钮,并在它们的 On Click 事件栏位设置执行 MyComputer 裡面相对应的计算功能。
Button + 的 On Click 事件执行 MyComputer 的 Add()
Button - 的 On Click 事件执行 MyComputer 的 Subtract()
Button x 的 On Click 事件执行 MyComputer 的 Multiply()
Button / 的 On Click 事件执行 MyComputer 的 Divide()
而这个 On Click 事件,相信大家都非常熟悉,就是按钮被点击时触发执行。正确来说,应该是点击按钮之后,并在同一个按钮之内放开按钮才会执行。
到这裡,UI 已可将输入的资料提供给 MyComputer,并将需要执行的功能要求 MyComputer 去进行,然后,我们就要来为 MyComputer 设置它的加、减、乘、除事件,使得 UI 可以显示出结果。
回想一下我们前面写的计算器主要功能,第一步,要让计算符号改变,所以,每个计算事件,都要设置事件发生时去改变 UI 上的计算符号文字,在这裡,虽然 MyComputer 的计算事件预设是会由程式裡面传递出结果,但 UnityEvent 栏位本身就能直接传入静态参数,所以,我们的程式码裡面不用为那些计算功能撰写有关改变运算符号的部分,只要直接在 Inspector 视窗上面设置就行了。
第二步,则是要让 UI 上显示出计算结果,由于 MyComputer 程式码裡面直接将计算结果的字串传给了个别对应的事件,所以,直接为每个事件设置他们的目标是 UI 上显示结果的 Text 即可。
每个计算功能事件让 UI 变更运算符号以及显示计算结果
这样子,每当 MyComputer 执行计算时,就会把字串传递给 UI 显示出来。
计算结果的画面
现在,计算器需求的功能齐全了,我们可能还想要做些更细微的调整,例如,在输入的值未改变的情况下,已经执行过的计算的,就不想要再次计算,所以,要将该功能按钮关闭。在 UGUI 的 Button 中,有一个 Interactable 栏位,是用来管理使用者与按钮之间的互动开关,当它被关闭时,Button 就会失去作用而呈现较暗的颜色,所以,我们在这边可以制订一个功能是当计算结果出来之后,把所对应的功能按钮上的 Interactable 关闭,而这个功能完全不用去修改程式码,只要再次去为每个计算功能的事件栏位加入执行目标即可。
计算结果之后关闭按钮。
计算过的按钮都关闭了。
虽然,按钮在计算之后可以被关闭了,可是,如果输入数值的栏位内容被改变了,应该还要有可以重新计算的机会,所以,需要有个能让按钮被重新打开的功能,这时候,其实我们可以直接在两个输入资料的 InputField 的 End Edit 事件对每个按钮执行开启 Interactable 的动作,只是,如果计算器有很多个功能按钮、以及有很多个输入栏位的话,要一个一个慢慢设置,执行流程会太过分散了。
所以,我们可以想像成 MyComputer 除了负责计算之外,还提供一个状态重置的功能,这个状态重置的功能本身并不执行任何事情,只是呼叫执行状态重置事件,那麽,设置在这个事件上的目标功能,只要状态重置的功能被呼叫执行,它们都会被执行,所以,我们在 MyComputer Script 要再加上以下的程式码:
[SerializeField] private UnityEvent onResetStatus; public void ResetStatus(){ this.onResetStatus.Invoke(); } 复制代码
而在 Unity 编辑器中,我们就可以让 MyComputer 的状态重置事件(On Reset Status)栏位,设置开启四个功能按钮的 Intractable。
状态重置时,再次启用按钮。
如此,两个 InputField 的 End Edit 事件则是指定执行 MyComputer 的状态重置功能即可。
End Edit 事件指定执行 MyComputer 的 ResetStatus()。
虽然,影片中状态重置事件是让按钮重新启用,但到这边也可以任意变更状态重置事件所要执行的动作,例如,让计算结果文字变成问号,那麽,当每次输入栏位重新被输入完毕之后,不但被停用的按钮会重新启用,也会使结果文字变成问号,等按下功能按钮之后才显示正确的结果。
既然有了状态重置的功能,那麽,我们是不是可以只让当前计算出结果的按钮被停用,其它按钮是启用的状态呢?这样就不用一定要重新在输入栏位输入资料完毕才能启用按钮。在做这个功能之前,要先解释一下,每个事件栏位可以指定多个执行目标,而这些执行目标其实是有执行顺序的,它会在执行完第一个之后才会执行第二个,一个一个往下执行。
因此,我们要做这个功能就不需要去修改程式码,而能直接变更各计算功能的事件内容以及顺序,让每次执行计算之后先执行状态重置,然后才显示计算结果、停用当前的计算按钮、变更运算符号。
更改为先执行状态重置之后,才去执行其他行为。
到这裡,简单的计算器功能将告一段落,我们可以发现 MyComputer Script 的程式码相当的独立,只是提供传入资料的入口让传入的资料可以暂存下来,以及提供计算功能给外部执行,并将执行结果丢到事件中,至于,谁会传入资料、谁会执行计算功能、谁会回应事件的执行,通通不需要去管,因此,如果全部的事件栏位都未设置目标,当被要求执行计算时,它也能照常执行计算,而不会因为未设置目标而发生错误,而究竟事件目标应该是谁,基本上,MyComputer 也不需要知道,一切就在 Inspector 视窗上,依照实际需求去设置或变更即可。
状态重置的部分也是一样,MyComputer Script 只负责提供状态重置功能并执行状态重置事件,至于,谁要求状态重置、状态重置到底要做些什麽,都不用去管。
这样,就可以让程式撰写变得相当简洁,而实际使用上的弹性变化却非常的大,另外,因为一些需求功能的变更不需要依赖修改程式码达成,除了节省程式编译的时间之外,也可避免因为修改程式码的人为失误而发生错误,最重要的是,因为 MyComputer Script 根本就不需要知道谁要执行它的功能,以及它的事件最后会去执行什麽目标功能,所以,也使程式间的藕合度降到最低,如果将这种做法的 Script 移到其它专案中使用,也不用特别顾虑会与其它什麽程式有关联而发生许多缺漏的情形。
以下是 MyComputer.cs 的完整内容:
using UnityEngine; using UnityEngine.Events; public class MyComputer : MonoBehaviour { private float _value1; private float _value2; [SerializeField] private PassString onAdd; [SerializeField] private PassString onSubtract; [SerializeField] private PassString onMultiply; [SerializeField] private PassString onDivide; [SerializeField] private UnityEvent onResetStatus; public string value1{ set{ float.TryParse(value , out this._value1); } } public string value2{ set{ float.TryParse(value , out this._value2); } } public void Add(){ this.onAdd.Invoke((this._value1 + this._value2).ToString()); } public void Subtract(){ this.onSubtract.Invoke((this._value1 - this._value2).ToString()); } public void Multiply(){ this.onMultiply.Invoke((this._value1 * this._value2).ToString()); } public void Divide(){ if(this._value2 == 0) return; this.onDivide.Invoke((this._value1 / this._value2).ToString()); } public void ResetStatus(){ this.onResetStatus.Invoke(); } } 复制代码
###2. Sphere Control 第二个范例裡,设计了五个球体个别使用相同的 Componet,但却因为实际使用时设置的不同,直接反应出不同的行为,并且示范如何让 UnityEvent 除了传递参数之外,也能带回资料。
首先,在场景中建立好五个球体,这只是 Unity 预设的原生物件,预设上,Unity 会个别为它们赋予 Sphere Collider,以及预设的 Material,Material 的 Shader 则是 Unity 5 的 Standand Shader,这部分,我们都不需要做任何更改。
原生物件球体的 Componet。
在此,定义了几个球体的功能需求:
- 球体可以被点击触发。
- 球体可以弹跳起来。
- 球体可以变色。
依据这几个需求,先在 Project 视窗分别建立它们个别的 C# Script,并个别命名为 SphereTouch、SphereJump、SphereDiscolor。
先来写 SphereTouch 的程式码,SphereTouch 只提供让使用者透过滑鼠点击球体时的事件反应而已,所以,SphereTouch 会有一个 UnityEvent 事件栏位,以及实作 Unity 内建的 OnMouseDown,只要 GameObject 本身有 Collider 的 Componet,当滑鼠在 GameObject 上按下按键时,就可以触发 OnMouseDown 执行其内容,而其内容裡,只是直接呼叫 UnityEvent 事件栏位执行而已,至于点击之后要反应什麽行为,那是别人的事。
using UnityEngine; using UnityEngine.Events; public class SphereTouch : MonoBehaviour { [SerializeField] private UnityEvent onTouch; public void DoTouch(){ this.onTouch.Invoke(); } void OnMouseDown(){ this.DoTouch(); } } 复制代码
看到这个程式码可能觉得奇怪,为什麽不在 OnMouseDown 裡面直接 Invoke 呢?其实,这只是要让 SphereTouch 多提供一个外部功能,让其他物件也可以传达点击讯息进来,例如,A 物件被滑鼠点击,然后它的 On Touch 事件栏位设置去执行 B、C、D 物件的 DoTouch 功能,那麽就可以一个物件被点击,四个物件同时做出反应。
再来是 SphereJump 的程式码,让球体跳动的行为可以有很多做法,例如使用 Unity 的动画系统去做,或者给予物体一个往上的推力,再让它因为重力而落下,不过,这边为了简化操作步骤,直接用程式码让它靠移动位置来达到跳动的效果,而这个跳动的动作,说穿了就是从原位置移动到一个指定高度的位置,再移动回来原来的位置,至于,要跳多高、移动速度多快,预先并不确定,所以,首先需要宣告两个可以在 Inspector 视窗设置的数值栏位,让我们可以在编辑器调整目标高度及跳动速度。
[SerializeField] private float hight = 1; [SerializeField] private float speed = 5; 复制代码
另外,在跳动的行为进行中,如果马上又被要求跳动,就会跳到一半又往上跳,这样的行为是有问题的,所以必须要设置一个状态纪录,当动作进行中,不接受再次的跳动要求,等到动作完毕之后才能再次接受并执行要求的行为。
private enum Status{ None, Moving } private Status _status = Status.None; 复制代码
因为我们这裡要做的跳动行为其实是两个移动行为的组合,所以要先写个可以提供物体本身从起点移动到终点的功能,两点间的移动,直接透过 Unity 内建的 Vector3.Lerp 就可以达成。
private IEnumerator Move(Vector3 source , Vector3 target){ float t = 0; while(t < 1){ transform.position = Vector3.Lerp(source , target , t); t += Time.deltaTime * this.speed; yield return null; } transform.position = target; } 复制代码
Vector3.Lerp 的 t 是介于 0 到 1 的值,我们可以把它当作是起点到终点的进度位置来看待,0 是起点,1 是终点。因为实际执行时每次刷新画面的时间都不一样,所以,我们不能让 t 随著时间的推移增加固定的值,而应该要为我们预计增加的值(即是速度)乘上 Time.deltaTime,于是,我们在这裡透过 yield return null 让 while 迴圈每经过一个 frame 才执行一次,当 t 超过 1 的时候,表示已经到达终点,即可结束迴圈。
要完成移动还有最后的步骤,就是 Vector3.Lerp 最后一次执行时,不可能刚刚好 t = 1,所以,最后还要校正一下位置到正确的终点位置,如此,移动行为才算是真正完成。
在这裡要特别注意的是,这个 Method 所回传的是 IEnumerator,代表它是做为 Coroutine 来使用,所以才可以在其内部使用 yield 来控制一些流程和时间,而要呼叫这个 Method 执行则需要使用 StartCoroutine 来执行。
接下来要做跳的动作,就是跳动开始时,变更状态为移动中,然后,取得起点和终点的位置,先执行起点移动到终点,执行完之后,再执行终点移动到起点的行为,等待动作完成之后,跳动就结束了,所以,就可以再将状态改回 None 的状态。
private IEnumerator DoJump(){ this._status = Status.Moving; Vector3 source = transform.position; Vector3 target = source; target.y += this.hight; yield return StartCoroutine(this.Move(source , target)); yield return StartCoroutine(this.Move(target , source)); this._status = Status.None; } 复制代码
既然跳动行为已经写好,那麽就要提供一个让外部呼叫的功能,当被外部呼叫执行时,先判断有没有在跳动中,没有的话就执行跳动。
public void Jump(){ if(this._status == Status.None) StartCoroutine(this.DoJump()); } 复制代码
如此,SphereJump 就算完成了,它并没有使用到 UnityEvent 事件,主要负责被呼叫执行动作而已,当然,视需求还是可以另外帮它补上基本事件,例如,开始跳动、跳动中、跳动结束。不过,这个示范中并没有用到,所以在此省略。
以下为 SphereJump.cs 的内容:
using UnityEngine; using System.Collections; public class SphereJump : MonoBehaviour { private enum Status{ None, Moving } [SerializeField] private float hight = 1; [SerializeField] private float speed = 5; private Status _status = Status.None; public void Jump(){ if(this._status == Status.None) StartCoroutine(this.DoJump()); } private IEnumerator Move(Vector3 source , Vector3 target){ float t = 0; while(t < 1){ transform.position = Vector3.Lerp(source , target , t); t += Time.deltaTime * this.speed; yield return null; } transform.position = target; } private IEnumerator DoJump(){ this._status = Status.Moving; Vector3 source = transform.position; Vector3 target = source; target.y += this.hight; yield return StartCoroutine(this.Move(source , target)); yield return StartCoroutine(this.Move(target , source)); this._status = Status.None; } } 复制代码
在撰写 SphereDiscolor 之前,我们要先回到 PassEvents Script 档裡面,宣告一个能够用来传递颜色参数的 UnityEvent。
[System.Serializable] public class PassColor : UnityEvent<Color>{} 复制代码
在 SphereDiscolor 中,先宣告用来暂存 Material 以及 Material 的颜色的两个变数栏位,然后,也宣告一个用来设置球体预设颜色的栏位,在 Awake 中取得球体本身的 Material 暂存下来,并使用所设置的预设颜色改变球体的颜色。
private Material _material; private Color _color; [SerializeField] private Color color = Color.white; void Awake(){ this._material = GetComponent<Renderer>().material; this.DefaultColor(); } public void DefaultColor(){ this._material.color = this.color; this._color = this.color; } 复制代码
同样的,改变为预设颜色的功能,也是独立做为可提供外部呼叫执行的功能。
然后,改变球体颜色的功能,我们分别宣告了直接改变为指定颜色的功能以及改变为随机颜色的功能,都是可提供给外部呼叫执行。
在此,也宣告了一个可以传递颜色值的 UnityEvent 事件栏位,当颜色被改变的时候,事件就会被执行,并且将所改变的颜色传递出去,所以,当球体颜色被改变时,可以让它引发其它行为,甚至是提供一个颜色去影响被引发的行为。
[SerializeField] private PassColor onChangeColor; void Awake(){ this._material = GetComponent<Renderer>().material; this.DefaultColor(); } public void DefaultColor(){ this._material.color = this.color; this._color = this.color; } public void Discolor(Color color){ this._material.color = color; this._color = color; this.onChangeColor.Invoke(color); } public void RandomColor(){ this.Discolor(new Color(Random.value , Random.value , Random.value)); } 复制代码
到这裡,这个范例的程式撰写部分暂告一段落,回到 Unity 画面,为每个球体加入这三个 Script 做为 Component。
在这裡可能会有疑问,为什麽要分成三个 Script,而不写在同一个 Script 中呢?因为,Unity 的游戏物件所具备的功能是 Component 导向的,具备哪些 Component 就能拥有什麽样的功能,拔除哪个 Component,该游戏物件就不具备哪个功能,所以,我们将功能分开来独立不同的 Script 来撰写,每个 Script 只要提供本身的功能就好了,不要去牵扯到其他的功能,那麽,当球体拥有 SphereTouch Component 时,他就可被点击,没有的话就无法被点击,当球体拥有 SphereJump 时,它就具备跳动的功能,这样,我们就能明确的为游戏物件抽换并改变其能力。
所以,当每个球体都具备变色功能时,变色功能有个预设颜色栏位会将球体于 Play Mode 开始时变更为其所指定的初始颜色。
接著,当每个球体都具有被触发功能时,我们就能为其指定当球体被触发时要引发什麽行为,如影片所示,我们可以为每个球体指定被点击触发时,去要求它的下一颗球跳起来,要求它的前一颗球随机改变颜色。
第 2 颗球设置被点击时,第三颗球跳起来,第一颗球随机改变颜色。
而最后一颗球被点击触发时,则是让前四颗球回到初始颜色。
第五颗球被点击时,前四颗球改变成初始颜色。
然后,我们也可以为第二颗球设置当它颜色被改变时,影响其他颗球的颜色。
第二颗球变色的时候,让第一颗球和第五颗球改变为随机的颜色。
在此,我们再次体验到,单纯写好 Script 提供的功能,不用在程式码中指定它人的行为,而能在编辑器中自由变更的好处以及功能弹性。
虽然,UnityEvent 可以直接在 Inspector 视窗指定一个固定值传递给某个 Component 的功能并执行它,也可以透过程式码使用 Invoke 呼叫执行并传递参数进去,但是,它却无法像我们平常呼叫执行一般的 Method 那样回传资料,似乎有点美中不足之感。
接下来,我们就来讨论如何也让 UnityEvent 带回资料,其实,这主要就是利用参考型别物件在参数间传送并不是传送实值的原理,来达到带回资料的目的。也就是说,我们可以透过实例化一个参考型别物件,传入 UnityEvent 的参数中,当这个物件内含的资料在 UnityEvent 所执行的功能中有任何变更后,在呼叫执行 UnityEvent 事件的这一方也可以从原本的物件获得改变后的资料。
只是,为了带回不同的型别的资料而宣告许多不同的 Class,似乎太麻烦了,因此,我们最好是可以製作一个通用的 Class,专门用来做为传递资料的持有者物件。所以,可以建立了一个名为 PassHolder 的 C# Script,专门用来做这件事。
public class PassHolder { public object value{set; private get;} public T GetValue<T>(){ if(this.value == null) return default(T); return (T)this.value; } } 复制代码
因为,所有的型别都是直接或间接继承自 Object(这裡所指的并不是 Unity 的 Object),所以,宣告一个 Property 统一接收任何型别的物件,然后,利用泛型方法的宣告方式,来取出所储存的资料,这裡只是很简单的判断有没有资料,如果没有资料则传回预计要取得的型别预设值。如此,这个 class 的物件就能通用存取任何型别的资料。
有了这样的 class,那麽就可以拿 SphereDiscolor 来实验看看;先来为 SphereDiscolor 加入交换颜色的功能。当然,要宣告能够传递 PassHolder 的 UnityEvent 事件栏位之前,还是要先回到 PassEvents 的 Script 档中,加入宣告可传递颜色以及 PassHolder 两个参数的型别。
[System.Serializable] public class PassColorReturn : UnityEvent<Color , PassHolder>{} 复制代码
这个功能要做的是,当它被呼叫执行时会透过交换颜色事件把自己的颜色传送出去,并透过 PassHolder 带回对方的颜色来改变自己的颜色。
而我们目前的范例裡,能被改变颜色的就属 SphereDiscolor 了,所以,再帮它加入另一个改变颜色的功能是除了接受目标颜色之外,还要能接收 PassHolder 物件,因此,除了拿接收到的颜色为自己变色之外,也要把原本的颜色写入到 PassHolder 中,那么呼叫执行这个变色功能的彼方,也就能收到所要带回去的颜色值。
[SerializeField] private PassColorReturn onSwapColor; public void SwapColor(){ PassHolder holder = new PassHolder(); this.onSwapColor.Invoke(this._color , holder); this.Discolor(holder.GetValue<Color>()); } public void Discolor(Color color , PassHolder holder){ holder.value = this._color; this.Discolor(color); } 复制代码
完成之后,我们在 Unity 编辑器的 Inspector 视窗中,就可以直接在 On Swap Color 的事件栏位指定要跟哪颗球交换颜色。
当第四颗球被点击时,使第五颗球跳起来、第三颗球改变颜色、要求自己执行交换颜色并指明与第一颗球交换颜色。
如此,几支程式、简短的程式码,只是定义它们本身的功能,以及什麽时候呼叫该执行的事件,而不需要特别指定所影响的型别及功能是什麽,而能在 Inspector 视窗中,很弹性的为具备同样 Component 的 GameObject 配置出完全不同的行为。也不会让程式中因为型别不对或参数数量不符合而出现错误,使得程式执行及设计上变得更弹性、更稳定,也让程式码内容变得更简洁,逻辑更清晰。在维护上以及流程调整上也会变得更视觉化、更清楚一些。
过去曾经发佈「Unity:使用 UGUI 的 ScrollRect 製作虚拟摇杆」的文章与影片,其中为虚拟摇杆传递操作行为的事件就是 UnityEngine.Events 的应用,善用这些做法,所写的程式复用性以及扩充性将会大大提高。
好了,有关 UnityEngine.Events 的说明及示范解说,到此结束,如果,你喜欢这个文章或所展示的影片,请帮忙介绍给你的朋友,也别忘了订阅影片频道以及来粉丝专页按个讚,谢谢!
UnityEvent官方文档:
目前版本 Unity 5.2.1f1