Unity3D-UGUI原理篇使用 UnityEngine.Events 让程序更灵活稳定
Posted 恬静的小魔龙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity3D-UGUI原理篇使用 UnityEngine.Events 让程序更灵活稳定相关的知识,希望对你有一定的参考价值。
推荐阅读
大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。
一、前言
这个系列文章,是为了记录分析UGUI的各种原理:
- UGUI的渲染模式。
- UGUI的缩放计算。
- UGUI的锚点定位。
- UGUI的层级渲染流程。
- UGUI的事件触发原理。
- UGUI的布局自动布局方式。
这是本系列文章的第六篇:
【Unity3D-UGUI原理篇】(一)Canvas 渲染模式
【Unity3D-UGUI原理篇】(二)Canvas Scaler 缩放原理
【Unity3D-UGUI原理篇】(三)RectTransform 组件详解
【Unity3D-UGUI原理篇】(四)Event System Manager 事件与触发
【Unity3D-UGUI原理篇】(五)Auto Layout 自动布局
【Unity3D-UGUI原理篇】(六)使用 UnityEngine.Events 让程序更灵活、稳定
二、如何使用UnityEngine.Events
Unity 4.6发布的新的GUI系统后,我们可以从建立的GUI Control上发现新的事件栏位。
比如,建立一个Button,可以从Inspector视图下面的OnClick栏位指定当前按钮被点击时执行哪一个GameObject上的那个组件的函数,使得按钮事件更加的直观、更加弹性的编辑。
当然,其他的GUI Control也有类似的栏位可以做这样的设置,而这个栏位则是由UnityEngine.Events底下的UnityEvent类所产生的,我们自己的脚本组件同样也可以设置这样的栏位,使程序可以视觉化编辑,让程序更加有弹性。
下面,就通过两个例子来演示如果使用UnityEngine.Events添加栏位。
在开始之前,我们需要了解一下事件的栏位。
在Inspector视图中添加脚本组件后,脚本组件的属性设置为Public后出现栏位,这个因为将属性设置public后,Unity本身会对public栏位进行自动序列化的关系,代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyScript : MonoBehaviour
{
public string Str;
}
效果如下图所示:
如果说,这个参数不可以使用public访问权限,只能使用private 或 protected 来修饰的话,但是还希望这个参数出现在Inspector视图的可编辑的栏位。
就可以在这个参数上一行添加SerializeField 的 Attribute(特性),那么 Unity 就知道这个栏位是要序列化使可以在 Inspector 视窗编辑:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyScript : MonoBehaviour
{
public string Str;
[SerializeField]
private int Integer;
}
效果如下图所示:
如果,要制作UGUI的Button那样的事件栏位的话,就需要在代码前加上using UnityEngine.Events命名空间,然后使用UnityEvent类型的参数可以出现这个栏位,代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class MyScript : MonoBehaviour
{
public string Str;
[SerializeField]
private int Integer;
public UnityEvent UnityEvent;
}
效果如下图所示:
这个UnityEvent栏位非常的灵活,当组件里面含有public访问权限修饰的属性和方法,并且传入值为bool、int、float、string中的一种的话,就可以直接在这里选择并提供传入的值。
还可以设置多个不同类别的参数,通过这个事件栏进行调用,另外,函数没有传入参数的话,也可以在这里进行设置,如果是设置两个以上的参数,这里就不能显示了。
虽然UnityEvent很方便,但是它传入的值是在编辑器里面设置的,而且只能设置一个值,如果我们的组件的程序要传递多个参数的话,光靠UnityEvent就没有办法了。
幸好,UnityEngine.Event除了UnityEvent之外,还提供了四个泛型类供我们扩充使用。
四个泛型类:UnityEvent、UnityEvent<T0,T1>、UnityEvent<T0,T1,T2>、UnityEvent<T0,T1,T2,T3>
这四个可供扩充,其实,他们所具备的功能都是一样的,只是,能接受的参数数量不同而已。
我们可以透过继承这几个泛型类来设置我们自己需求的参数类别,而且可容纳的参数数量可以有一到四个。
比如:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class MyScript : MonoBehaviour
{
[System.Serializable]
public class PassString : UnityEvent<string> { }
[System.Serializable]
public class PassColor : UnityEvent<Color> { }
}
如果让类可以显示于 Inspector 视窗,除了使用 SerializeField 标示在栏位上面之外。
该型别也必须是可序列化的才行,所以,还要在 class 上方也加上 System.Serializable。
到这里,我们就能够使用自己定义的 UnityEvent 来宣告事件栏位,并使它可以显示于 Inspector 视窗。
三、简单计算器实例
第一个实例,主要是制作一个简单的计算器来示范 UnityEngine.Events 的用法以及所带来的好处。
制作任何东西之前,一定要先了解到这个东西的使用目的以及功能有哪些。
所以,我们先用简单的 UI 排出一个运算式的样子,这个运算式提供了两个输入栏位、一个运算符号的文字、一个显示计算结果的文字,以及四个运算功能按钮:
在此,我们先定义计算器的基本功能:
- 点击运算功能按钮,会依照计算种类去改变运算符号的文字。
- 点击运算功能按钮之后,在 UI 显示计算结果文字。
新建一个脚本命名为 MyComputer.cs。
由于,最后会将计算结果转换为字串传给 UI 去显示,在这里会使用到 UnityEvent 去传递数据,只是一般的 UnityEvent 并无法直接在代码发送时就带入参数,所以我们还需要使用前面提到的做法来传递带参数的 UnityEvent:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class MyComputer : MonoBehaviour
{
[System.Serializable]
public class PassString : UnityEvent<string> { }
}
我们接收到的值是string类型,需要转成数值类型来计算,这两个值是通过UGUI 的 InputField的 Edit End 事件传过来的。
提供两个 Property 让外部可以把字串传入,并且把传入的字串转为数值暂存下来,这样其他计算功能就能计算了。
目前只做加、减、乘、除四个计算功能,直接将获得的数值计算出结果,并将结果转为字串后,通过 Invoke 来执行个别对应的事件,那么计算器的基本功能就算完成了。代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class MyComputer : MonoBehaviour
{
[System.Serializable]
public class PassString : UnityEvent<string> { }
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() 的部分要特别注意,因为,除法的除数不得为零,不然,会发生错误,所以计算之前要先判断一下,如果除数为零,就不要执行后面的计算。
编译通过后,可以从 Inspector 视窗看到分别为加、减、乘、除的事件栏位:
接下来,就可以在Inspector视图中,设置彼此的关系,首先,让UI输入的值传递给MyComputer,所以,要从两个 InputField 中的 End Edit 事件分别设置将输入的字串传给 MyComputer 的 value1 以及 value2 两个 属性:
第一个InputField:
第二个InputField:
这里的On End Edit设置好后,不需要我们在Inspector视图传入任何值,因为这个On End Edit在文字输入完毕后,会自动将输入的文字通过这个事件传递出去。
接下来,需要设置4个按钮的加、减、乘、除功能,在它们的 On Click 事件栏位设置执行 MyComputer 里面相对应的计算功能:
加:
减:
乘:
除:
到这里,UI就可以将输入的值提供给MyComputer,并将需要执行的功能要求 MyComputer 去进行。
然后,我们就要设置计算符号,然后显示运算结果了,这里就可以在MyComputer脚本组件的事件中将计算结果的字符串传递给对应的事件即可:
第一个设置:
第二个设置:
总体设置:
每个计算功能事件让 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事件栏,添加4个按钮的Intractable:
状态重置时再次启动按钮,然后在两个InputField 的 End Edit 事件指定执行 MyComputer 的状态重置功能:
虽然,已经在End Edit事件中恢复状态,但是这边也可以任意变更状态重置事件所要执行的动作。
例如,让计算结果文字变成问号,那么,当每次输入栏位重新被输入完毕之后,不但被停用的按钮会重新启用,也会使结果文字变成问号,等按下功能按钮之后才显示正确的结果。
既然有了状态重置的功能,那么,我们是不是可以只让当前计算出结果的按钮被停用,其它按钮是启用的状态呢?
这样就不用一定要重新在输入栏位输入数值才能启用按钮。
在做这个功能之前,要先解释一下,每个事件栏位可以指定多个执行目标,而这些执行目标其实是有执行顺序的,它会在执行完第一个之后才会执行第二个,一个一个往下执行。
要做这个功能不需要去修改代码,直接变更各计算功能的事件内容以及顺序,让每次执行计算之后先执行状态重置,然后才显示计算结果、停用当前的计算按钮、变更运算符号:
到这里,简单的计算器功能将告一段落,我们可以发现 MyComputer Script 的代码相当的独立,只是提供传入数值的入口让传入的数值可以暂存下来,以及提供计算功能给外部执行,并将执行结果丢到事件中。
至于,谁会传入数值、谁会执行计算功能、谁会回应事件的执行,通通不需要去管。
因此,如果全部的事件栏位都未设置目标,当被要求执行计算时,它也能照常执行计算,而不会因为未设置目标而发生错误,而究竟事件目标应该是谁,基本上,MyComputer 也不需要知道,一切就在 Inspector 视窗上,依照实际需求去设置或变更即可。
状态重置的部分也是一样,MyComputer Script 只负责提供状态重置功能并执行状态重置事件,至于,谁要求状态重置、状态重置到底要做些什么,都不用去管。
这样,就可以让程式编写变得相当简洁,而实际使用上的非常灵活,另外,因为一些需求功能的变更不需要依赖修改代码完成,除了节省代码编译的时间之外,也可避免因为修改程式码的人为失误而发生错误。
最重要的是,因为 MyComputer Script 根本就不需要知道谁要执行它的功能,以及它的事件最后会去执行什么目标功能,所以,也使代码间的藕合度降到最低,如果将这种做法的 Script 移到其它项目中使用,也不用特别顾虑会与其它什么程式有关联而发生许多缺漏的情形。
四、游戏对象管理器
在第二个实例中,设计了五个球体分别使用相同的脚本组件,但在实际使用中的设置不同,反应不同的行为,并且演示使用UnityEvent传递参数,以及返回参数。
首先,在场景中新建5个球体:
定义几个球体的功能需求:
- 球体可以被点击触发。
- 球体可以弹跳起来。
- 球体可以变色。
依据这几个需求,先分别建3个 C# Script,并个别命名为 SphereTouch、SphereJump、SphereDiscolor。
球体点击触发 SphereTouch.cs
先来写 SphereTouch 的代码,SphereTouch 只提供让使用者透过滑鼠点击球体时的事件反应而已,所以,SphereTouch 会有一个 UnityEvent 事件栏位。
以及 Unity 内部的函数 OnMouseDown,在当鼠标在 GameObject 上按下按键时,就可以触发 OnMouseDown 执行其内容:
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.cs
再来是 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.cs
首先,设置一个用来传递颜色参数色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 事件栏位,当颜色被改变的时候,事件就会被执行,并且将所改变的颜色传递出去。
所以,当球体颜色被改变时,可以让它引发其它行为,甚至是提供一个颜色去影响被引发的行为。
SphereDiscolor.cs 整体代码如下:
usin以上是关于Unity3D-UGUI原理篇使用 UnityEngine.Events 让程序更灵活稳定的主要内容,如果未能解决你的问题,请参考以下文章
Unity3D-UGUI原理篇RectTransform 组件详解
Unity3D-UGUI原理篇Auto Layout 自动布局