Unity中是如何触发UGUI中各组件对应的接口方法
文章目录
- 一、引入
- 二、具体实现
- 1.在EventSystem类中
- 2.在StandaloneInputModule类中
一、引入
笔者在做数字孪生项目时,遇到过这样一个要求:当鼠标点击覆盖在物体上的一个空白标签时,能
够调用HTTP接口拿到数据并且展示出来,对于这个要求,很自然的想到用去了图片的Button或者自己
写一个类实现IPointerClickHandler接口,做完之后我就在想,这个接口中的方法到底是怎样实现出来
的呢,然后我就翻阅UGUI中的源码与别人的博客,之后以这篇文章来记录我对触发该接口的理解.
以下内容可能需要一些对UGUI源码的阅读及理解.不会介绍每个类的具体方法.
在创建任意一个Image等UI物体时,系统会自动生成一个EventSystem物体,里面包含了这两个
类,其中EventSystem类是一个控制UI事件及其所有控制模块的类,另外一个则是一个标准输入模块
二、具体实现
1.在EventSystem中
找到其中的Update方法,注意不是UpdateModules方法(UpdateModules方法是来检查每个控制
模块是否还在激活可用状态,不可用则移除在EventSystem类中存储模块的List)
public void UpdateModules()
{
GetComponents(m_SystemInputModules);
var systemInputModulesCount = m_SystemInputModules.Count;
for (int i = systemInputModulesCount - 1; i >= 0; i--)
{
if (m_SystemInputModules[i] && m_SystemInputModules[i].IsActive())
continue;
m_SystemInputModules.RemoveAt(i);
}
}
控制模块的检查方法
protected virtual void Update()
{
if (current != this)
return;
TickModules();
bool changedModule = false;
var systemInputModulesCount = m_SystemInputModules.Count;
for (var i = 0; i < systemInputModulesCount; i++)
{
var module = m_SystemInputModules[i];
if (module.IsModuleSupported() && module.ShouldActivateModule())
{
if (m_CurrentInputModule != module)
{
ChangeEventModule(module);
changedModule = true;
}
break;
}
}
// no event module set... set the first valid one...
if (m_CurrentInputModule == null)
{
for (var i = 0; i < systemInputModulesCount; i++)
{
var module = m_SystemInputModules[i];
if (module.IsModuleSupported())
{
ChangeEventModule(module);
changedModule = true;
break;
}
}
}
if (!changedModule && m_CurrentInputModule != null)
m_CurrentInputModule.Process();
#if UNITY_EDITOR
if (Application.isPlaying)
{
int eventSystemCount = 0;
for (int i = 0; i < m_EventSystems.Count; i++)
{
if (m_EventSystems[i].GetType() == typeof(EventSystem))
eventSystemCount++;
}
if (eventSystemCount > 1)
Debug.LogWarning("There are " + eventSystemCount + " event systems in the scene. Please ensure there is always exactly one event system in the scene");
}
#endif
}
在Update方法中第一部分只有三行代码,前两行是为了保证此时运行的EventSystem实例是该实例,
之后则跳转到TickModules方法中.
private void TickModules()
{
var systemInputModulesCount = m_SystemInputModules.Count;
for (var i = 0; i < systemInputModulesCount; i++)
{
if (m_SystemInputModules[i] != null)
m_SystemInputModules[i].UpdateModule();
}
}
该方法主要来Tick每个控制模块
在Update方法中第二三部分,先是定义了一个局部变量changeModule为false,此处变量是为了后续
如果出现需要更改此时的输入模块时,以保证这一帧来更改模块下一帧来进一步验证输入模块是否
支持并可用之后再执行对应模块的Process方法.
因此第二三部分是为了更改不适应的模块及在模块为空时取模块List中第一个模块.
Update方法第二部分
Update方法第三部分
来到第四部分,第四部分是在前两验证保证不可切换模块以及此时模块不为空的情况下才去此时模
块的Process方法
Update方法第四部分
2.在StandaloneInputModule中
在EventSystem的Update方法中首先执行的是TickModules方法,在该方法中,是轮转每个输入模块
然后执行对应的UpdateModule方法,对于StandaloneInputModule中的UpdateModule方法,如下图
所示
public override void UpdateModule()
{
if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
{
if (m_InputPointerEvent != null && m_InputPointerEvent.pointerDrag != null && m_InputPointerEvent.dragging)
{
ReleaseMouse(m_InputPointerEvent, m_InputPointerEvent.pointerCurrentRaycast.gameObject);
}
m_InputPointerEvent = null;
return;
}
m_LastMousePosition = m_MousePosition;
m_MousePosition = input.mousePosition;
}
由于在该类初始化时m_InputPointerEvent为空,因此该方法第一次执行最后只会记录鼠标的位置
接着在EventSystem的Update方法中执行的是StandaloneInputModule的Process方法,该方法如下
public override void Process()
{
if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
return;
bool usedEvent = SendUpdateEventToSelectedObject();
// case 1004066 - touch / mouse events should be processed before navigation events in case
// they change the current selected gameobject and the submit button is a touch / mouse button.
// touch needs to take precedence because of the mouse emulation layer
if (!ProcessTouchEvents() && input.mousePresent)
ProcessMouseEvent();
if (eventSystem.sendNavigationEvents)
{
if (!usedEvent)
usedEvent |= SendMoveEventToSelectedObject();
if (!usedEvent)
SendSubmitEventToSelectedObject();
}
}
该方法在我们使用的是键鼠时,会执行ProcessMouseEvent方法
protected void ProcessMouseEvent(int id)
{
var mouseData = GetMousePointerEventData(id);
var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;
m_CurrentFocusedGameObject = leftButtonData.buttonData.pointerCurrentRaycast.gameObject;
// Process the first mouse button fully
ProcessMousePress(leftButtonData);
ProcessMove(leftButtonData.buttonData);
ProcessDrag(leftButtonData.buttonData);
// Now process right / middle clicks
ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData);
ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData.buttonData);
ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData);
ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData.buttonData);
if (!Mathf.Approximately(leftButtonData.buttonData.scrollDelta.sqrMagnitude, 0.0f))
{
var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(leftButtonData.buttonData.pointerCurrentRaycast.gameObject);
ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler);
}
}
在有参的ProcessMouseEvent方法中第一行代码执行了StandaloneInputModule父类
PointerInputModule中的GetMousePointerEventData()方法,该方法主要用来获取鼠标的各种信息以
及以射线检测的方式来得到鼠标指针下的物体的信息.
在ProcessMouseEvent方法中最重要的部分是方法中Process方法
(ProcessMousePress,ProcessMove,ProcessDrag等),这些方法是检测鼠标指针的按键状态以及触
发指针下的物体中对应的事件接口方法,代码以ProcessMousePress为例,如下
protected void ProcessMousePress(MouseButtonEventData data)
{
var pointerEvent = data.buttonData;
var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;
// PointerDown notification
if (data.PressedThisFrame())
{
pointerEvent.eligibleForClick = true;
pointerEvent.delta = Vector2.zero;
pointerEvent.dragging = false;
pointerEvent.useDragThreshold = true;
pointerEvent.pressPosition = pointerEvent.position;
pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;
DeselectIfSelectionChanged(currentOverGo, pointerEvent);
// search for the control that will receive the press
// if we can't find a press handler set the press
// handler to be what would receive a click.
var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);
var newClick = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
// didnt find a press handler... search for a click handler
if (newPressed == null)
newPressed = newClick;
// Debug.Log("Pressed: " + newPressed);
float time = Time.unscaledTime;
if (newPressed == pointerEvent.lastPress)
{
var diffTime = time - pointerEvent.clickTime;
if (diffTime < 0.3f)
++pointerEvent.clickCount;
else
pointerEvent.clickCount = 1;
pointerEvent.clickTime = time;
}
else
{
pointerEvent.clickCount = 1;
}
pointerEvent.pointerPress = newPressed;
pointerEvent.rawPointerPress = currentOverGo;
pointerEvent.pointerClick = newClick;
pointerEvent.clickTime = time;
// Save the drag handler as well
pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);
if (pointerEvent.pointerDrag != null)
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);
m_InputPointerEvent = pointerEvent;
}
// PointerUp notification
if (data.ReleasedThisFrame())
{
ReleaseMouse(pointerEvent, currentOverGo);
}
}
可以看到,这个方法首先得到指针信息以及指针下的物体信息,之后在该帧下对得到的指针信息类中
的属性进行覆盖修改,之后以ExecuteEvents(事件执行方法的管理类)类,参数为指针以及指针下的物
体信息,取得该物体的对应事件接口方法,在该方法不为空的情况下执行.
此时接口中的方法就如愿以偿的被执行了