当前位置: 首页 > news >正文

Unity UGUI Button事件流程

场景结构
在这里插入图片描述

测试代码

public class TestBtn : MonoBehaviour
{void Start(){var btn = GetComponent<Button>();btn.onClick.AddListener(OnClick);}private void OnClick(){Debug.Log("666");}}

当添加事件时

// 实例化一个ButtonClickedEvent的事件
[FormerlySerializedAs("onClick")]
[SerializeField]
private ButtonClickedEvent m_OnClick = new ButtonClickedEvent();//常用的onClick.AddListener()就是监听这个事件
public ButtonClickedEvent onClick
{get { return m_OnClick; }set { m_OnClick = value; }
}//Button.cs部分源码

当按钮点击时

 //如果按钮处于活跃状态并且可交互(Interactable设置为true),则触发事件
private void Press(){if (!IsActive() || !IsInteractable())return;UISystemProfilerApi.AddMarker("Button.onClick", this);m_OnClick.Invoke();}//鼠标点击时调用该函数,继承自 IPointerClickHandler 接口public virtual void OnPointerClick(PointerEventData eventData){if (eventData.button != PointerEventData.InputButton.Left)return;Press();}
//Button.cs部分源码

是如何执行的呢

private static readonly EventFunction<IPointerClickHandler> s_PointerClickHandler = Execute;public static EventFunction<IPointerClickHandler> pointerClickHandler
{get { return s_PointerClickHandler; }
}//调用关键代码
private static void Execute(IPointerClickHandler handler, BaseEventData eventData)
{handler.OnPointerClick(ValidateEventData<PointerEventData>(eventData));
}//ExecuteEvents.cs部分源码
  • 实际调用了目标对象(如 Button)的 OnPointerClick() 方法。

继续跟踪源码在StandaloneInputModule中

private void ReleaseMouse(PointerEventData pointerEvent, GameObject currentOverGo)
{// 执行 PointerUp 事件处理ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);// 获取可以处理点击事件的对象var pointerClickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);// 处理 PointerClick 和 Drop 事件if (pointerEvent.pointerClick == pointerClickHandler && pointerEvent.eligibleForClick){// 如果当前对象可以接收点击事件并且符合点击条件,则执行 PointerClick 事件ExecuteEvents.Execute(pointerEvent.pointerClick, pointerEvent, ExecuteEvents.pointerClickHandler);}//....省略其他代码...
}//StandaloneInputModule.cs部分源码
关键点:
  • pointerEvent.eligibleForClick 表示这次点击是否满足触发条件(比如没有被拖动打断)。
  • 获取当前鼠标释放时的 GameObject (currentOverGo)。
  • 获取其 IPointerClickHandler 接口实现者(即 Button 组件)。
  • 最后通过 ExecuteEvents.Execute(...) 调用 OnPointerClick 方法。

pointerEvent.eligibleForClick在下面会设置为true

而ReleaseMouse在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;}//StandaloneInputModule.cs部分源码
  • 主要用于记录鼠标位置。
  • 如果窗口失焦并且正在拖拽,则调用 ReleaseMouse() 来释放鼠标状态。

而UpdateModule是BaseInputModule 类的方法

 public abstract class BaseInputModule : UIBehaviour{public virtual void UpdateModule(){}}

UpdateModule被在EventSystem中的TickModules方法调用

private List<BaseInputModule> m_SystemInputModules = new List<BaseInputModule>();private void TickModules(){var systemInputModulesCount = m_SystemInputModules.Count;for (var i = 0; i < systemInputModulesCount; i++){if (m_SystemInputModules[i] != null)m_SystemInputModules[i].UpdateModule();}}
//EventSystem.cs部分源码

TickModules方法被在EventSystem里的Update调用

 protected virtual void Update(){if (current != this)return;TickModules();bool changedModule = false;//m_CurrentInputModule 就是场景里面的StandaloneInputModule组件//判断当前m_CurrentInputModule 有没有被更改或者为空的情况if (!changedModule && m_CurrentInputModule != null)//处理鼠标事件m_CurrentInputModule.Process();//...省略其他代码...
}

EventSystem的Update继承UIBehaviour

public class EventSystem : UIBehaviour
  • EventSystem 是一个继承自 UIBehaviour 的组件,必须挂载在场景中的某个 GameObject 上。
  • 它每帧调用自身的 Update() 方法(由 Unity 引擎自动调用):
    在这里插入图片描述

所以,如果在按下鼠标后,EventSystem每帧都会检测何时释放鼠标,然后触发点击事件。如果有的话触发点击事件,那么是如何知道要触发的哪个Button的点击事件的呢


回到EventSystem的Update方法中

 protected virtual void Update(){if (current != this)return;TickModules();bool changedModule = false;//m_CurrentInputModule 就是场景里面的StandaloneInputModule组件//判断当前m_CurrentInputModule 有没有被更改或者为空的情况if (!changedModule && m_CurrentInputModule != null)//处理鼠标事件m_CurrentInputModule.Process();//...省略其他代码...
}
  • 这里会调用所有注册的 BaseInputModule 子类的 UpdateModule() 方法。
  • 其中就包括 StandaloneInputModule

可以看到,如果没有变更输入模块并且当前输入模块不为空,会在每帧执行Process方法

 public abstract class BaseInputModule : UIBehaviour{public abstract void Process();}

而场景里面的StandaloneInputModule 继承了PointerInputModule

public class StandaloneInputModule : PointerInputModule

PointerInputModule实现了BaseInputModule接口

public abstract class PointerInputModule : BaseInputModule

StandaloneInputModule 重写了Process方法

 public override void Process(){if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())return;bool usedEvent = SendUpdateEventToSelectedObject();// 案例 1004066 - 在处理导航事件之前应先处理触摸/鼠标事件,// 因为它们可能会改变当前选中的游戏对象,并且提交按钮可能是触摸/鼠标按钮。// 由于存在鼠标模拟层,触摸需要优先处理。if (!ProcessTouchEvents() && input.mousePresent)//处理鼠标事件ProcessMouseEvent();if (eventSystem.sendNavigationEvents){if (!usedEvent)usedEvent |= SendMoveEventToSelectedObject();if (!usedEvent)SendSubmitEventToSelectedObject();}}protected void ProcessMouseEvent(){//鼠标左键事件Id为0ProcessMouseEvent(0);}protected void ProcessMouseEvent(int id)
{//关键函数var mouseData = GetMousePointerEventData(id);var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;m_CurrentFocusedGameObject = leftButtonData.buttonData.pointerCurrentRaycast.gameObject;// 处理鼠标左键点击ProcessMousePress(leftButtonData);ProcessMove(leftButtonData.buttonData);ProcessDrag(leftButtonData.buttonData);// 处理鼠标右键键点击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);}
}

GetMousePointerEventData方法是StandaloneInputModule 继承PointerInputModule的

protected virtual MouseState GetMousePointerEventData(int id){// Populate the left button...PointerEventData leftData;var created = GetPointerData(kMouseLeftId, out leftData, true);leftData.Reset();if (created)leftData.position = input.mousePosition;Vector2 pos = input.mousePosition;if (Cursor.lockState == CursorLockMode.Locked){// We don't want to do ANY cursor-based interaction when the mouse is lockedleftData.position = new Vector2(-1.0f, -1.0f);leftData.delta = Vector2.zero;}else{leftData.delta = pos - leftData.position;leftData.position = pos;}leftData.scrollDelta = input.mouseScrollDelta;leftData.button = PointerEventData.InputButton.Left;//发射射线eventSystem.RaycastAll(leftData, m_RaycastResultCache);//省略后面的代码----下面会分析
}

上面代码主要作用是发射射线,填充MouseButtonEventData 事件

EventSystem的RaycastAll方法

 public class EventSystem : UIBehaviour
{public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults){raycastResults.Clear();var modules = RaycasterManager.GetRaycasters();var modulesCount = modules.Count;for (int i = 0; i < modulesCount; ++i){var module = modules[i];if (module == null || !module.IsActive())continue;//发射射线module.Raycast(eventData, raycastResults);}raycastResults.Sort(s_RaycastComparer);}
}

这个RaycasterManager会收集场景里面所有实现BaseRaycaster接口的类
在这里插入图片描述

由于我们场景里面只有Canvas上面挂载了GraphicRaycaster 组件
在这里插入图片描述

//这部分详细解析可以查看这篇文章public class GraphicRaycaster : BaseRaycaster{private List<Graphic> m_RaycastResults = new List<Graphic>();public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)   {//。。。省略部分代码。。。//拿到所有继承了Graphic的类,Button继承了该类Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);//。。。省略部分代码。。。int totalCount = m_RaycastResults.Count;for (var index = 0; index < totalCount; index++){//。。。省略部分代码。。。//m_RaycastResults进行结果排序//构建结果返回var castResult = new RaycastResult{gameObject = go,//这个就是我们的Button对象或者Text对象module = this,distance = distance,screenPosition = eventPosition,displayIndex = displayIndex,index = resultAppendList.Count,depth = m_RaycastResults[index].depth,sortingLayer = canvas.sortingLayerID,sortingOrder = canvas.sortingOrder,worldPosition = ray.origin + ray.direction * distance,worldNormal = -transForward};resultAppendList.Add(castResult);}
}   

在这里插入图片描述
可以看到拿到的结果有两个
在这里插入图片描述

在这里插入图片描述

继续查看PointerInputModule的GetMousePointerEventData方法

protected virtual MouseState GetMousePointerEventData(int id)
{//省略前面的代码,之前分析过了//发射射线eventSystem.RaycastAll(leftData, m_RaycastResultCache);//拿到第一个结果,即Text所在对象var raycast = FindFirstRaycast(m_RaycastResultCache);//赋值给leftData.pointerCurrentRaycast,后面需要用到leftData.pointerCurrentRaycast = raycast;m_RaycastResultCache.Clear();// copy the apropriate data into right and middle slotsPointerEventData rightData;GetPointerData(kMouseRightId, out rightData, true);rightData.Reset();CopyFromTo(leftData, rightData);rightData.button = PointerEventData.InputButton.Right;PointerEventData middleData;GetPointerData(kMouseMiddleId, out middleData, true);middleData.Reset();CopyFromTo(leftData, middleData);middleData.button = PointerEventData.InputButton.Middle;m_MouseState.SetButtonState(PointerEventData.InputButton.Left, StateForMouseButton(0), leftData);m_MouseState.SetButtonState(PointerEventData.InputButton.Right, StateForMouseButton(1), rightData);m_MouseState.SetButtonState(PointerEventData.InputButton.Middle, StateForMouseButton(2), middleData);return m_MouseState;}

处理点击事件函数

/// <summary>
/// 计算并处理任何鼠标按钮状态的变化。
/// </summary>
protected void ProcessMousePress(MouseButtonEventData data)
{// 获取当前的指针事件数据var pointerEvent = data.buttonData;// 这个对象就是前面拿到的Text所在的对象var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;// 处理按下事件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);// 查找继承了IPointerDownHandler的控件,因为Text没有实现改接口,所以向上查找可以处理点击事件的对象,找到了Button对象var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);var newClick = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);// 如果没有找到按下处理器,则查找点击处理器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;elsepointerEvent.clickCount = 1;pointerEvent.clickTime = time;}else{pointerEvent.clickCount = 1;}// 设置按下对象、原始按下对象和点击对象pointerEvent.pointerPress = newPressed;pointerEvent.rawPointerPress = currentOverGo;pointerEvent.pointerClick = newClick;pointerEvent.clickTime = time;// 保存拖拽处理器pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);// 如果有拖拽处理器,执行初始化潜在拖拽操作if (pointerEvent.pointerDrag != null)ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);// 保存输入指针事件m_InputPointerEvent = pointerEvent;}// 处理释放事件if (data.ReleasedThisFrame()){ReleaseMouse(pointerEvent, currentOverGo);}
}

这里面 pointerEvent.eligibleForClick = true; 前面释放鼠标需要用到。

ExecuteEvents最终执行按下事件

 public static class ExecuteEvents{private static void GetEventChain(GameObject root, IList<Transform> eventChain){eventChain.Clear();if (root == null)return;var t = root.transform;while (t != null){eventChain.Add(t);t = t.parent;}}public static GameObject ExecuteHierarchy<T>(GameObject root, BaseEventData eventData, EventFunction<T> callbackFunction) where T : IEventSystemHandler{GetEventChain(root, s_InternalTransformList);var internalTransformListCount = s_InternalTransformList.Count;for (var i = 0; i < internalTransformListCount; i++){var transform = s_InternalTransformList[i];//关键函数,执行点击事件if (Execute(transform.gameObject, eventData, callbackFunction))return transform.gameObject;}return null;}public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler{var internalHandlers = ListPool<IEventSystemHandler>.Get();GetEventList<T>(target, internalHandlers);//  if (s_InternalHandlers.Count > 0)//      Debug.Log("Executinng " + typeof (T) + " on " + target);var internalHandlersCount = internalHandlers.Count;for (var i = 0; i < internalHandlersCount; i++){T arg;try{arg = (T)internalHandlers[i];}catch (Exception e){var temp = internalHandlers[i];Debug.LogException(new Exception(string.Format("Type {0} expected {1} received.", typeof(T).Name, temp.GetType().Name), e));continue;}try{//最终执行事件functor(arg, eventData);}catch (Exception e){Debug.LogException(e);}}var handlerCount = internalHandlers.Count;ListPool<IEventSystemHandler>.Release(internalHandlers);return handlerCount > 0;}//获取实现了IPointerDownHandler接口的组件private static void GetEventList<T>(GameObject go, IList<IEventSystemHandler> results) where T : IEventSystemHandler{// Debug.LogWarning("GetEventList<" + typeof(T).Name + ">");if (results == null)throw new ArgumentException("Results array is null", "results");if (go == null || !go.activeInHierarchy)return;var components = ListPool<Component>.Get();go.GetComponents(components);var componentsCount = components.Count;for (var i = 0; i < componentsCount; i++){if (!ShouldSendToComponent<T>(components[i]))continue;// Debug.Log(string.Format("{2} found! On {0}.{1}", go, s_GetComponentsScratch[i].GetType(), typeof(T)));results.Add(components[i] as IEventSystemHandler);}ListPool<Component>.Release(components);// Debug.LogWarning("end GetEventList<" + typeof(T).Name + ">");}
}

GetEventChain拿到4个结果
在这里插入图片描述
分别是
在这里插入图片描述

最终触发IPointerDownHandler事件
在这里插入图片描述
在这里插入图片描述

总结如下

EventSystem会在每帧检测鼠标左键是否按下,如果按下,发射射线,拿到第一个检查到的物体,执行IPointerDownHandler事件,同时构造好PointerEventData给后面释放鼠标时使用,在鼠标释放时,查找点击的GameObject上是否有组件实现了IPointerClickHandler接口,如果有就触发事件,如果没有,就向父节点GameObject查找,直到找到发出射线的Canvas为止。

✅ 总结:整个流程图解

阶段内容
📌 Unity 引擎调用EventSystem.Update()
🔁 每帧更新TickModules()StandaloneInputModule.UpdateModule()
🖱️ 鼠标释放ReleaseMouse() 被调用
🎯 查找点击对象使用 GetEventHandler<IPointerClickHandler>()
⚡ 执行点击ExecuteEvents.Execute()handler.OnPointerClick()
🧱 Button 响应OnPointerClick()Press()m_OnClick.Invoke()
📈 用户监听onClick.AddListener(() => { ... }) 中的方法被调用

相关文章:

  • 2025.6.9总结(利与弊)
  • ADS-B态势显示 ASD-View
  • GPIO(通用输入输出)与LPUART(低功耗通用异步收发传输器)简述
  • 【案例篇】为什么设置了 ulimit 但 nofile 限制仍不生效?
  • SpringCloudGateway 自定义局部过滤器
  • Android屏幕刷新率与FPS(Frames Per Second) 120hz
  • 博科Brocade FC交换机常用操作命令
  • 具身智能之人形机器人核心零部件介绍
  • 本地部署drawDB结合内网穿透技术实现数据库远程管控方案
  • 判断是否是润年
  • 从0开始学习R语言--Day20--Wilcoxon秩和检验
  • 企业如何一键复制 DolphinScheduler 项目到新项目服务器?全套自动化方案来了!(企业不外传的实用工具)
  • Python网页自动化Selenium中文文档
  • 走进离线语音:安信可 VC‑01 智能模块全面拆解与 MCU 实战
  • 爬虫基础学习day2
  • Electron简介(附电子书学习资料)
  • day030-Shell自动化编程-函数
  • Electron 防脱壳转二进制 JSC 打包过程以及踩坑记录
  • 【向量库】Weaviate 搜索与索引技术:从基础概念到性能优化
  • 二维数组 行列混淆区分 js
  • 网站建设创业计划书模板范文/何鹏seo
  • 网站排名突然掉没了/注册网址
  • 网站排名如何稳定/如何宣传自己的网站
  • 做网站服务器是必须购买的吗/宁波seo外包推广平台
  • 手机网站制作教程下载/网络营销促销方案
  • 新网域名官网/河南自助建站seo公司