0 前言
UICamera是NGUI中专门用于捕获和分发交互事件的脚本,和UI渲染无关,需要挂在UI摄像机上。
其核心思想是在Update中检测Input的各种输入情况,并对屏幕做raycast投射,以决定是哪个go的collider触发事件,最终将事件分发出去。
Q: 检测各种输入在哪里做?
- Update()中.
Q: UICamera如何作事件分发
- Unity3D中的SendMessage使用(消息传递的三种方法)
- NGUI中的消息传递方式为: SendMessage
1 核心数据结构
有许多公共静态属性. 如 currentXXX
.
用于存放触发事件的摄像机 / ray 等等.
EventTpye
分为:
- World_3D
- UI_3D
- World_2D
- UI_2D
其中3D用Physics.Raycast实现,2D用Physics.OverlapPoint实现
- World会根据触发点的 世界距离 排序(常用于游戏摄像机)
- UI会根据 widget depth 排序(常用于UI界面)
ControlScheme
有三种类型: 鼠标 / 触摸 / 手柄
触犯事件会根据这个类型做出相应的调整. 不同设备不同的处理方式?
MouseOrTouch
MouseOrTouch是一个UI事件的抽象,用于保存输入数据中的位置、偏移量等重要信息。
public class MouseOrTouch
{
public Vector2 pos; // 当前鼠标或触摸的位置
public Vector2 lastPos; // 上一次鼠标或触摸的位置
public Vector2 delta; // 当前帧与上一帧的偏移
public Vector2 totalDelta; // delta的累积,通常用于drag事件
public Camera pressedCam; // OnPress(true)触发时对应的捕获事件的摄像机
public GameObject last; // 上一个触发触摸或鼠标事件的go
public GameObject current; // 当前触发触摸或鼠标事件的go
public GameObject pressed; // 上一个接收OnPress的go
public GameObject dragged; // 正在被拖拽的go
public float clickTime = 0f; // 上一次click事件的时间(通常用于判断doubleClick)
public ClickNotification clickNotification = ClickNotification.Always; // OnClick的触发条件,None为不触发,Always为总是触发,BasedOnDelta为根据位置移动的偏移量来决定是否发生(偏移量和Thresholds参数有关)
public bool touchBegan = true; // Touch模式下标识一个触摸是否为开始
public bool pressStarted = false; // 标识是否开始按住
public bool dragStarted = false; // 标识是否开始拖拽
}
有了MouseOrTouch,UICamera就通过它来保存不同输入设备对应的输入数据。
mMouse、mTouches、controller就分别对应了鼠标、触摸屏和手柄等的输入数据。
- 鼠标分左键、右键和滚轮,包含3个MouseOrTouch对象的数组。
- 触摸屏会有多点触控发生。是一个列表。
2 核心方法
2.1 Notify(GameObject go, string funcName, object obj)
这个方法本质上调用的是 go.SendMessage(funcName, obj)
.
这样就能触发到 具体的UI控件中与funcName同名的方法
正因为是通过这种特殊方式来调用,所以控件源码中一部分方法会查找不到引用,和反射的道理类似.
同时也会发一份消息到genericEventHandler这个用户自己设置的全局go
相当于一个全局的事件接收器。
static public void Notify(GameObject go, string funcName, object obj)
{
if (mNotifying > 10) return;
// Automatically forward events to the currently open popup list
if (currentScheme == ControlScheme.Controller && UIPopupList.isOpen &&
UIPopupList.current.source == go && UIPopupList.isOpen)
go = UIPopupList.current.gameObject;
// 被触发的不为空,且在场景中?
if (go && go.activeInHierarchy)
{
++mNotifying;
//if (currentScheme == ControlScheme.Controller)
// Debug.Log((go != null ? "[" + go.name + "]." : "[global].") + funcName + "(" + obj + ");", go);
go.SendMessage(funcName, obj, SendMessageOptions.DontRequireReceiver);
if (mGenericHandler != null && mGenericHandler != go)
mGenericHandler.SendMessage(funcName, obj, SendMessageOptions.DontRequireReceiver);
--mNotifying;
}
}
2.2 Update
本质上是对UnityEngine.Input的封装和处理。
- 根据useTouch或useMouse标记(在Awake中根据当前平台设置,如手机只有touch),选择执行ProcessTouches或ProcessMouse
- 调用用户自定义的委托onCustomInput
- ProcessOthers处理键盘和手柄
- 处理tooltip相关逻辑
// Update核心函数
void ProcessEvents()
{
current = this;
NGUIDebug.debugRaycast = debug;
// Process touch events first
if (useTouch) ProcessTouches();
else if (useMouse) ProcessMouse();
// Custom input processing
if (onCustomInput != null) onCustomInput();
// Update the keyboard and joystick events
if ((useKeyboard || useController) && !disableController && !ignoreControllerInput) ProcessOthers();
// If it's time to show a tooltip, inform the object we're hovering over
if (useMouse && mHover != null)
{
float scroll = !string.IsNullOrEmpty(scrollAxisName) ? GetAxis(scrollAxisName) : 0f;
if (scroll != 0f)
{
if (onScroll != null) onScroll(mHover, scroll);
Notify(mHover, "OnScroll", scroll);
}
if (currentScheme == ControlScheme.Mouse && showTooltips && mTooltipTime != 0f && !UIPopupList.isOpen && mMouse[0].dragged == null &&
(mTooltipTime < RealTime.time || GetKey(KeyCode.LeftShift) || GetKey(KeyCode.RightShift)))
{
currentTouch = mMouse[0];
currentTouchID = -1;
ShowTooltip(mHover);
}
}
if (mTooltip != null && !NGUITools.GetActive(mTooltip))
ShowTooltip(null);
current = null;
currentTouchID = -100;
}
以 ProcessTouches
为例,该方法中用 Input.GetTouch
获取每个touch分别做如下处理:
- 创建MouseOrTouch结构并设置数据
currentTouch
; - 调用
ProcessTouch
分发事件 ; - 若touch数目为0,则转为ProcessMouse或ProcessFakeTouches(用于编辑器用鼠标模拟触摸);
接下来. 我们详细来看一下 ProcessTouch
- 根据传入的pressed.处理的函数为: ProcessPress.
- 向currentTouch.pressed分发OnPress事件;
void ProcessPress(bool pressed, float click, float drag);
- 根据传入的released. 处理的函数为: ProcessRelease
- 向currentTouch.last分发OnDragOut事件;
- 向currentTouch.dragged分发OnDragEnd事件;
- 向currentTouch.pressed分发OnPress事件;
2.3 RayCast(Vector3 inPos)
给定位置判断有没有与控件产生交互
最终要得到mRayHitObject这个结果。
-
currentCamera.ScreenToViewportPoint(inPos)
: 算出viewport的坐标,并排除一些异常情况
此方法的功能是实现坐标点position 从屏幕坐标系向摄像机视口的单位化坐标系转换 。
参考点position的x和y分量为屏幕的实际坐标值,单位为像素,z值无效。 -
【注:屏幕坐标左下角是(0,0),右上角是(pixelWidth,pixelHeight)】,viewport坐标右上角是(1,1)】
-
currentCamera.ScreenPointToRay(inPos)
将屏幕坐标转换为ray。 -
UICamera有个表示射线长度的参数rangeDistance,默认为摄像机远近裁剪面的距离;
-
eventReceiverMask表示摄像机投射ray时哪些层可以响应
-
接下来根据EventType采用不同的算法来算ray射到的物体,以两种3D模式为例:
-
World_3D
:
if (Physics.Raycast(ray, out lastHit, dist, mask)) hoveredObject = lastHit.collider.gameObject
UI_3D
:
Physics.RaycastAll(ray, dist, mask)
获取射线穿到的所有hit,取每个hit对应的collider的go,计算其raycastDepth,并按从大到小排序
mRayHitObject= 上述最大的,且对应panel可见的hit对应的go
[定义:UIWidget.raycastDepth = 自身depth + 所属panel.depth * 1000]
[定义:NGUITools.CalculateRaycastDepth(go)计算go下所有可用widget的raycastDepth的最小值]