最新要闻
- 7999元起 国人买iPhone 14 Pro需9.5%年收入引热议:你多久才能买?
- 外星人在美一户人家后院活动?别信:3D建模软件做的_当前资讯
- 北京通州一业主私家车长期占用消防车通道且拒不整改 遭行政拘留7日
- 科学家付巧妹获得联合国首位阿勒福赞奖 从进化角度为人类健康问题带来新见解
- 郑州将于6月份发放5000万元汽车消费券 不限购买者户籍
- 著名球星贝克汉姆使用中文为中国粉丝献上端午祝福 网友热情回应
- 微软宣布Xbox Series X和XGP订阅服务将涨价 称其反应市场的竞争情况
- 美国亿万富翁家庭以91万元年薪招聘住家狗保姆 将狗的幸福放在首位
- 顺丰旗下大型无人机FH-98顺利降落 最大起飞重量5.25吨
- 余承东曾预告!中国自动驾驶迎来大进展:官方首次表态L3商用
- 天天微资讯!《英雄联盟》新英雄纳亚菲利玩法公布 这是真的狗!
- 为减少内卷 四家头部猪企发起“互不挖人公约”
- 贵州贵阳周边现奇幻红色湖泊 吸引众多游客前往打卡
- 第28届白玉兰奖评委见面会举行 《庆余年》导演称应杜绝文盲演员
- 吉林一市民驾车偶遇“拦路虎” 强调路边遇到老虎不要开车窗或下车
- 《超级马里奥兄弟 惊奇》公布 预计于本年10月20日发售
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
介绍CocosCreator系统事件是怎么产生及触发的
这篇文章主要介绍了CocosCreator系统事件是怎么产生及触发的,虽然内容不少,但是只要一点点抽丝剥茧,具体分析其内容,就会豁然开朗
目录
- 环境
- 概要
- 模块作用
- 涉及文件
- 源码解析
- CCGame.js
- CCInputManager.js
- 事件是怎么从引擎到节点的?
- CCEventManager.js
- 事件是注册到了哪里?
- event-target.js(EventTarget)
- callbacks-invoker.js(CallbacksInvoker)
- 事件是怎么触发的?
- callbacks-invoker.js
- 结尾
- 加点有意思的监听器排序算法
- 总结
环境
CocosCreator 2.4Chrome 88
(资料图片仅供参考)
概要
模块作用
事件监听机制应该是所有游戏都必不可少的内容。不管是按钮的点击还是物体的拖动,都少不了事件的监听与分发。主要的功能还是通过节点的on/once函数,对系统事件(如触摸、点击)进行监听,随后触发对应的游戏逻辑。同时,也支持用户发射/监听自定义的事件,这方面可以看一下官方文档监听和发射事件。
涉及文件
其中,CCGame和CCInputManager都有涉及注册事件,但他们负责的是不同的部分。
源码解析
事件是怎么(从浏览器)到达引擎的?
想知道这个问题,必须要了解引擎和浏览器的交互是从何而起。上代码。
CCGame.js
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 | // 初始化事件系统 _initEvents: function () { var win = window, hiddenPropName; //_ register system events // 注册系统事件,这里调用了CCInputManager的方法 if ( this .config.registerSystemEvent) _cc.inputManager.registerSystemEvent( this .canvas); // document.hidden表示页面隐藏,后面的if用于处理浏览器兼容 if ( typeof document.hidden !== "undefined" ) { hiddenPropName = "hidden" ; } else if ( typeof document.mozHidden !== "undefined" ) { hiddenPropName = "mozHidden" ; } else if ( typeof document.msHidden !== "undefined" ) { hiddenPropName = "msHidden" ; } else if ( typeof document.webkitHidden !== "undefined" ) { hiddenPropName = "webkitHidden" ; } // 当前页面是否隐藏 var hidden = false ; // 页面隐藏时的回调,并发射game.EVENT_HIDE事件 function onHidden () { if (!hidden) { hidden = true ; game.emit(game.EVENT_HIDE); } } //_ In order to adapt the most of platforms the onshow API. // 为了适配大部分平台的onshow API。应该是指传参的部分... // 页面可视时的回调,并发射game.EVENT_SHOW事件 function onShown (arg0, arg1, arg2, arg3, arg4) { if (hidden) { hidden = false ; game.emit(game.EVENT_SHOW, arg0, arg1, arg2, arg3, arg4); } } // 如果浏览器支持隐藏属性,则注册页面可视状态变更事件 if (hiddenPropName) { var changeList = [ "visibilitychange" , "mozvisibilitychange" , "msvisibilitychange" , "webkitvisibilitychange" , "qbrowserVisibilityChange" ]; // 循环注册上面的列表里的事件,同样是是为了兼容 // 隐藏状态变更后,根据可视状态调用onHidden/onShown回调函数 for ( var i = 0; i < changeList.length; i++) { document.addEventListener(changeList[i], function (event) { var visible = document[hiddenPropName]; //_ QQ App visible = visible || event[ "hidden" ]; if (visible) onHidden(); else onShown(); }); } } // 此处省略部分关于 页面可视状态改变 的兼容性代码 // 注册隐藏和显示事件,暂停或重新开始游戏主逻辑。 this .on(game.EVENT_HIDE, function () { game.pause(); }); this .on(game.EVENT_SHOW, function () { game.resume(); }); } |
其实核心代码只有一点点…为了保持对各个平台的兼容性,重要的地方有两个:
- 调用CCInputManager的方法
- 注册页面可视状态改变事件,并派发game.EVENT_HIDE和game.EVENT_SHOW事件。
来看看CCInputManager。
CCInputManager.js
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465 | // 注册系统事件 element是canvas registerSystemEvent (element) { if ( this ._isRegisterEvent) return ; // 注册过了,直接return this ._glView = cc.view; let selfPointer = this ; let canvasBoundingRect = this ._canvasBoundingRect; // 监听resize事件,修改this._canvasBoundingRect window.addEventListener( "resize" , this ._updateCanvasBoundingRect.bind( this )); let prohibition = sys.isMobile; let supportMouse = ( "mouse" in sys.capabilities); // 是否支持触摸 let supportTouches = ( "touches" in sys.capabilities); // 省略了鼠标事件的注册代码 //_register touch event // 注册触摸事件 if (supportTouches) { // 事件map let _touchEventsMap = { "touchstart" : function (touchesToHandle) { selfPointer.handleTouchesBegin(touchesToHandle); element.focus(); }, "touchmove" : function (touchesToHandle) { selfPointer.handleTouchesMove(touchesToHandle); }, "touchend" : function (touchesToHandle) { selfPointer.handleTouchesEnd(touchesToHandle); }, "touchcancel" : function (touchesToHandle) { selfPointer.handleTouchesCancel(touchesToHandle); } }; // 遍历map注册事件 let registerTouchEvent = function (eventName) { let handler = _touchEventsMap[eventName]; // 注册事件到canvas上 element.addEventListener(eventName, ( function (event) { if (!event.changedTouches) return ; let body = document.body; // 计算偏移量 canvasBoundingRect.adjustedLeft = canvasBoundingRect.left - (body.scrollLeft || window.scrollX || 0); canvasBoundingRect.adjustedTop = canvasBoundingRect.top - (body.scrollTop || window.scrollY || 0); // 从事件中获得触摸点,并调用回调函数 handler(selfPointer.getTouchesByEvent(event, canvasBoundingRect)); // 停止事件冒泡 event.stopPropagation(); event.preventDefault(); }), false ); }; for (let eventName in _touchEventsMap) { registerTouchEvent(eventName); } } // 修改属性表示已完成事件注册 this ._isRegisterEvent = true ; } |
在代码中,主要完成的事情就是注册了touchstart等一系列的原生事件,在事件回调中,则分别调用了selfPointer(=this)中的函数进行处理。这里我们用touchstart事件作为例子,即handleTouchesBegin函数。
123456789101112131415161718192021222324252627282930313233343536373839404142 | // 处理touchstart事件 handleTouchesBegin (touches) { let selTouch, index, curTouch, touchID, handleTouches = [], locTouchIntDict = this ._touchesIntegerDict, now = sys.now(); // 遍历触摸点 for (let i = 0, len = touches.length; i < len; i ++) { // 当前触摸点 selTouch = touches[i]; // 触摸点id touchID = selTouch.getID(); // 触摸点在触摸点列表(this._touches)中的位置 index = locTouchIntDict[touchID]; // 如果没有获得index,说明是个新的触摸点(刚按下去) if (index == null ) { // 获得一个没有被使用的index let unusedIndex = this ._getUnUsedIndex(); // 取不到,抛出错误。可能是超出了支持的最大触摸点数量。 if (unusedIndex === -1) { cc.logID(2300, unusedIndex); continue ; } //_curTouch = this._touches[unusedIndex] = selTouch; // 存储触摸点 curTouch = this ._touches[unusedIndex] = new cc.Touch(selTouch._point.x, selTouch._point.y, selTouch.getID()); curTouch._lastModified = now; curTouch._setPrevPoint(selTouch._prevPoint); locTouchIntDict[touchID] = unusedIndex; // 加到需要处理的触摸点列表中 handleTouches.push(curTouch); } } // 如果有新触点,生成一个触摸事件,分发到eventManager if (handleTouches.length > 0) { // 这个方法会把触摸点的位置根据scale做处理 this ._glView._convertTouchesWithScale(handleTouches); let touchEvent = new cc.Event.EventTouch(handleTouches); touchEvent._eventCode = cc.Event.EventTouch.BEGAN; eventManager.dispatchEvent(touchEvent); } }, |
函数中,一部分代码用于过滤是否有新的触摸点产生,另一部分用于处理并分发事件(如果需要的话)。到这里,事件就完成了从浏览器到引擎的转化,事件已经到达eventManager里。那么引擎到节点之间又经历了什么?
事件是怎么从引擎到节点的?
传递事件到节点的工作主要都发生在CCEventManager类中。包括了存储事件监听器,分发事件等。先从_dispatchTouchEvent作为入口来看看。
CCEventManager.js
1234567891011121314151617181920212223242526272829303132333435363738394041424344 | // 分发事件 _dispatchTouchEvent: function (event) { // 为触摸监听器排序 // TOUCH_ONE_BY_ONE:触摸事件监听器类型,触点会一个一个地分开被派发 // TOUCH_ALL_AT_ONCE:触点会被一次性全部派发 this ._sortEventListeners(ListenerID.TOUCH_ONE_BY_ONE); this ._sortEventListeners(ListenerID.TOUCH_ALL_AT_ONCE); // 获得监听器列表 var oneByOneListeners = this ._getListeners(ListenerID.TOUCH_ONE_BY_ONE); var allAtOnceListeners = this ._getListeners(ListenerID.TOUCH_ALL_AT_ONCE); //_ If there aren"t any touch listeners, returndirectly. // 如果没有任何监听器,直接return。 if ( null === oneByOneListeners && null === allAtOnceListeners) return ; // 存储一下变量 var originalTouches = event.getTouches(), mutableTouches = cc.js.array.copy(originalTouches); var oneByOneArgsObj = {event: event, needsMutableSet: (oneByOneListeners && allAtOnceListeners), touches: mutableTouches, selTouch: null }; // //_ process the target handlers 1st // 不会翻。感觉是首先处理单个触点的事件。 if (oneByOneListeners) { // 遍历触点,依次分发 for ( var i = 0; i < originalTouches.length; i++) { event.currentTouch = originalTouches[i]; event._propagationStopped = event._propagationImmediateStopped = false ; this ._dispatchEventToListeners(oneByOneListeners, this ._onTouchEventCallback, oneByOneArgsObj); } } // //_ process standard handlers 2nd // 不会翻。感觉是其次处理多触点事件(一次性全部派发) if (allAtOnceListeners && mutableTouches.length > 0) { this ._dispatchEventToListeners(allAtOnceListeners, this ._onTouchesEventCallback, {event: event, touches: mutableTouches}); if (event.isStopped()) return ; } // 更新触摸监听器列表,主要是移除和新增监听器 this ._updateTouchListeners(event); }, |
函数中,主要做的事情就是,排序、分发到注册的监听器列表、更新监听器列表。平平无奇。你可能会奇怪,怎么有一个突兀的排序?哎,这正是重中之重!关于排序的作用,可以看官方文档触摸事件的传递。正是这个排序,实现了不同层级/不同zIndex的节点之间的触点归属问题。排序会在后面提到,妙不可言。分发事件是通过调用_dispatchEventToListeners函数实现的,接着就来看一下它的内部实现。
1234567891011121314151617181920212223242526272829303132333435363738 | /** * 分发事件到监听器列表 * @param {*} listeners 监听器列表 * @param {*} onEvent 事件回调 * @param {*} eventOrArgs 事件/参数 */ _dispatchEventToListeners: function (listeners, onEvent, eventOrArgs) { // 是否需要停止继续分发 var shouldStopPropagation = false ; // 获得固定优先级的监听器(系统事件) var fixedPriorityListeners = listeners.getFixedPriorityListeners(); // 获得场景图优先级别的监听器(我们添加的监听器正常都是在这里) var sceneGraphPriorityListeners = listeners.getSceneGraphPriorityListeners(); /** * 监听器触发顺序: * 固定优先级中优先级 < 0 * 场景图优先级别 * 固定优先级中优先级 > 0 */ var i = 0, j, selListener; if (fixedPriorityListeners) { //_ priority < 0 if (fixedPriorityListeners.length !== 0) { // 遍历监听器分发事件 for (; i < listeners.gt0Index; ++i) { selListener = fixedPriorityListeners[i]; // 若 监听器激活状态 且 没有被暂停 且 已被注册到事件管理器 // 最后一个onEvent是使用_onTouchEventCallback函数分发事件到监听器 // onEvent会返回一个boolean,表示是否需要继续向后续的监听器分发事件,若true,停止继续分发 if (selListener.isEnabled() && !selListener._isPaused() && selListener._isRegistered() && onEvent(selListener, eventOrArgs)) { shouldStopPropagation = true ; break ; } } } } // 省略另外两个优先级的触发代码 }, |
在函数中,通过遍历监听器列表,将事件依次分发出去,并根据onEvent的返回值判定是否需要继续派发。一般情况下,一个触摸事件被节点接收到后,就会停止派发。随后会从该节点进行冒泡派发等逻辑。这也是一个重点,即触摸事件仅有一个节点会进行响应,至于节点的优先级,就是上面提到的排序算法啦。这里的onEvent其实是_onTouchEventCallback函数,来看看。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081 | // 触摸事件回调。分发事件到监听器 _onTouchEventCallback: function (listener, argsObj) { //_ Skip if the listener was removed. // 若 监听器已被移除,跳过。 if (!listener._isRegistered()) return false ; var event = argsObj.event, selTouch = event.currentTouch; event.currentTarget = listener._node; // isClaimed:监听器是否认领事件 var isClaimed = false , removedIdx; var getCode = event.getEventCode(), EventTouch = cc.Event.EventTouch; // 若 事件为触摸开始事件 if (getCode === EventTouch.BEGAN) { // 若 不支持多点触摸 且 当前已经有一个触点了 if (!cc.macro.ENABLE_MULTI_TOUCH && eventManager._currentTouch) { // 若 该触点已被节点认领 且 该节点在节点树中是激活的,则不处理事件 let node = eventManager._currentTouchListener._node; if (node && node.activeInHierarchy) { return false ; } } // 若 监听器有对应事件 if (listener.onTouchBegan) { // 尝试分发给监听器,会返回一个boolean,表示监听器是否认领该事件 isClaimed = listener.onTouchBegan(selTouch, event); // 若 事件被认领 且 监听器是已被注册的,保存一些数据 if (isClaimed && listener._registered) { listener._claimedTouches.push(selTouch); eventManager._currentTouchListener = listener; eventManager._currentTouch = selTouch; } } } // 若 监听器已有认领的触点 且 当前触点正是被当前监听器认领 else if (listener._claimedTouches.length > 0 && ((removedIdx = listener._claimedTouches.indexOf(selTouch)) !== -1)) { // 直接领回家 isClaimed = true ; // 若 不支持多点触摸 且 已有触点 且 已有触点还不是当前触点,不处理事件 if (!cc.macro.ENABLE_MULTI_TOUCH && eventManager._currentTouch && eventManager._currentTouch !== selTouch) { return false ; } // 分发事件给监听器 // ENDED或CANCELED的时候,需要清理监听器和事件管理器中的触点 if (getCode === EventTouch.MOVED && listener.onTouchMoved) { listener.onTouchMoved(selTouch, event); } else if (getCode === EventTouch.ENDED) { if (listener.onTouchEnded) listener.onTouchEnded(selTouch, event); if (listener._registered) listener._claimedTouches.splice(removedIdx, 1); eventManager._clearCurTouch(); } else if (getCode === EventTouch.CANCELED) { if (listener.onTouchCancelled) listener.onTouchCancelled(selTouch, event); if (listener._registered) listener._claimedTouches.splice(removedIdx, 1); eventManager._clearCurTouch(); } } //_ If the event was stopped, return directly. // 若事件已经被停止传递,直接return(对事件调用stopPropagationImmediate()等情况) if (event.isStopped()) { eventManager._updateTouchListeners(event); return true ; } // 若 事件被认领 且 监听器把事件吃掉了(x)(指不需要再继续传递,默认为false,但在Node的touch系列事件中为true) if (isClaimed && listener.swallowTouches) { if (argsObj.needsMutableSet) argsObj.touches.splice(selTouch, 1); return true ; } return false ; }, |
函数主要功能是分发事件,并对多触点进行兼容处理。重要的是返回值,当事件被监听器认领时,就会返回true,阻止事件的继续传递。分发事件时,以触摸开始事件为例,会调用监听器的onTouchBegan方法。奇了怪了,不是分发给节点嘛?为什么是调用监听器?监听器是个什么东西?这就要研究一下,当我们对节点调用on函数注册事件的时候,事件注册到了哪里?
事件是注册到了哪里?
对节点调的on函数,那相关代码自然在CCNode里。直接来看看on函数都干了些啥。
12345678910111213141516 | /** * 在节点上注册指定类型的回调函数 * @param {*} type 事件类型 * @param {*} callback 回调函数 * @param {*} target 目标(用于绑定this) * @param {*} useCapture 注册在捕获阶段 */ on (type, callback, target, useCapture) { // 是否是系统事件(鼠标、触摸) let forDispatch = this ._checknSetupSysEvent(type); if (forDispatch) { // 注册事件 return this ._onDispatch(type, callback, target, useCapture); } // 省略掉非系统事件的部分,其中包括了位置改变、尺寸改变等。 }, |
官方注释老长一串,我给写个简化版。总之就是用来注册针对某事件的回调函数。你可能想说,内容这么少???然而这里分了两个分支,一个是调用_checknSetupSysEvent函数,一个是_onDispatch函数,代码都在里面555。注册相关的是_onDispatch函数,另一个一会讲。
1234567891011121314151617181920212223242526272829303132333435363738 | // 注册分发事件 _onDispatch (type, callback, target, useCapture) { //_ Accept also patameters like: (type, callback, useCapture) // 也可以接收这样的参数:(type, callback, useCapture) // 参数兼容性处理 if ( typeof target === "boolean" ) { useCapture = target; target = undefined; } else useCapture = !!useCapture; // 若 没有回调函数,报错,return。 if (!callback) { cc.errorID(6800); return ; } // 根据useCapture获得不同的监听器。 var listeners = null ; if (useCapture) { listeners = this ._capturingListeners = this ._capturingListeners || new EventTarget(); } else { listeners = this ._bubblingListeners = this ._bubblingListeners || new EventTarget(); } // 若 已注册了相同的回调事件,则不做处理 if ( !listeners.hasEventListener(type, callback, target) ) { // 注册事件到监听器 listeners.on(type, callback, target); // 保存this到target的__eventTargets数组里,用于从target中调用targetOff函数来清除监听器。 if (target && target.__eventTargets) { target.__eventTargets.push( this ); } } return callback; }, |
节点会持有两个监听器,一个是_capturingListeners,一个是_bubblingListeners,区别是什么呢?前者是注册在捕获阶段的,后者是冒泡阶段,更具体的区别后面会讲。从listeners.on(type, callback, target);
可以看出其实事件是注册在这两个监听器中的,而不在节点里。那就看看里面是个啥玩意。
event-target.js(EventTarget)
12345678910111213141516171819 | //_注册事件目标的特定事件类型回调。这种类型的事件应该被 `emit` 触发。 proto.on = function (type, callback, target, once) { // 若 没有传递回调函数,报错,return if (!callback) { cc.errorID(6800); return ; } // 若 已存在该回调,不处理 if ( ! this .hasEventListener(type, callback, target) ) { // 注册事件 this .__on(type, callback, target, once); if (target && target.__eventTargets) { target.__eventTargets.push( this ); } } return callback; }; |
追到最后,又是一个on…由js.extend(EventTarget,CallbacksInvoker);
可以看出,EventTarget继承了CallbacksInvoker,再扒一层!
callbacks-invoker.js(CallbacksInvoker)
12345678910111213 | //_ 事件添加管理 proto.on = function (key, callback, target, once) { // 获得事件对应的回调列表 let list = this ._callbackTable[key]; // 若 不存在,到池子里取一个 if (!list) { list = this ._callbackTable[key] = callbackListPool.get(); } // 把回调相关信息存起来 let info = callbackInfoPool.get(); info.set(callback, target, once); list.callbackInfos.push(info); }; |
终于到头啦!其中,callbackListPool和callbackInfoPool都是js.Pool对象,这是一个对象池。回调函数最终会存储在_callbackTable中。了解完存储的位置,那事件又是怎么被触发的?
事件是怎么触发的?
了解触发之前,先来看看触发顺序。先看一段官方注释。
鼠标或触摸事件会被系统调用dispatchEvent方法触发,触发的过程包含三个阶段:*1.捕获阶段:派发事件给捕获目标(通过
_getCapturingTargets
获取),比如,节点树中注册了捕获阶段的父节点,从根节点开始派发直到目标节点。*2.目标阶段:派发给目标节点的监听器。*3.冒泡阶段:派发事件给冒泡目标(通过_getBubblingTargets
获取),比如,节点树中注册了冒泡阶段的父节点,从目标节点开始派发直到根节点。
啥意思呢?on函数的第四个参数useCapture,若为true,则事件会被注册在捕获阶段,即可以最早被调用。需要注意的是,捕获阶段的触发顺序是从父节点到子节点(从根节点开始)。随后会触发节点本身注册的事件。最后,进入冒泡阶段,将事件从父节点传递到根节点。简单理解:捕获阶段从上到下,然后本身,最后冒泡阶段从下到上。理论可能有点生硬,一会看代码就懂了!还记得_checknSetupSysEvent函数嘛,前面的注释只是写了检查是否为系统事件,其实它做的事情可不止这么一点点。
123456789101112131415161718192021222324252627282930313233343536373839 | // 检查是否是系统事件 _checknSetupSysEvent (type) { // 是否需要新增监听器 let newAdded = false ; // 是否需要分发(系统事件需要) let forDispatch = false ; // 若 事件是触摸事件 if (_touchEvents.indexOf(type) !== -1) { // 若 当前没有触摸事件监听器 新建一个 if (! this ._touchListener) { this ._touchListener = cc.EventListener.create({ event: cc.EventListener.TOUCH_ONE_BY_ONE, swallowTouches: true , owner: this , mask: _searchComponentsInParent( this , cc.Mask), onTouchBegan: _touchStartHandler, onTouchMoved: _touchMoveHandler, onTouchEnded: _touchEndHandler, onTouchCancelled: _touchCancelHandler }); // 将监听器添加到eventManager eventManager.addListener( this ._touchListener, this ); newAdded = true ; } forDispatch = true ; } // 省略事件是鼠标事件的代码,和触摸事件差不多 // 若 新增了监听器 且 当前节点不是活跃状态 if (newAdded && ! this ._activeInHierarchy) { // 稍后一小会,若节点仍不是活跃状态,暂停节点的事件传递, cc.director.getScheduler().schedule( function () { if (! this ._activeInHierarchy) { eventManager.pauseTarget( this ); } }, this , 0, 0, 0, false ); } return forDispatch; }, |
重点在哪呢?在eventManager.addListener(this._touchListener, this);
这行。可以看到,每个节点都会持有一个_touchListener,并将其添加到eventManager中。是不是有点眼熟?哎,这不就是刚刚eventManager分发事件时的玩意嘛!这不就连起来了嘛,虽然eventManager不持有节点,但是持有这些监听器啊!新建监听器的时候,传了一大堆参数,还是拿熟悉的触摸开始事件,onTouchBegan: _touchStartHandler
,这又是个啥玩意呢?
12345678910111213141516 | // 触摸开始事件处理器 var _touchStartHandler = function (touch, event) { var pos = touch.getLocation(); var node = this .owner; // 若 触点在节点范围内,则触发事件,并返回true,表示这事件我领走啦! if (node._hitTest(pos, this )) { event.type = EventType.TOUCH_START; event.touch = touch; event.bubbles = true ; // 分发到本节点内 node.dispatchEvent(event); return true ; } return false ; }; |
简简单单,获得触点,判断触点是否落在节点内,是则分发!
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677 | //_ 分发事件到事件流中。 dispatchEvent (event) { _doDispatchEvent( this , event); _cachedArray.length = 0; }, // 分发事件 function _doDispatchEvent (owner, event) { var target, i; event.target = owner; //_ Event.CAPTURING_PHASE // 捕获阶段 _cachedArray.length = 0; // 获得捕获阶段的节点,储存在_cachedArray owner._getCapturingTargets(event.type, _cachedArray); //_ capturing event.eventPhase = 1; // 从尾到头遍历(即从根节点到目标节点的父节点) for (i = _cachedArray.length - 1; i >= 0; --i) { target = _cachedArray[i]; // 若 目标节点注册了捕获阶段的监听器 if (target._capturingListeners) { event.currentTarget = target; //_ fire event // 在目标节点上处理事件 target._capturingListeners.emit(event.type, event, _cachedArray); //_ check if propagation stopped // 若 事件已经停止传递了,return if (event._propagationStopped) { _cachedArray.length = 0; return ; } } } // 清空_cachedArray _cachedArray.length = 0; //_ Event.AT_TARGET //_ checks if destroyed in capturing callbacks // 目标节点本身阶段 event.eventPhase = 2; event.currentTarget = owner; // 若 自身注册了捕获阶段的监听器,则处理事件 if (owner._capturingListeners) { owner._capturingListeners.emit(event.type, event); } // 若 事件没有被停止 且 自身注册了冒泡阶段的监听器,则处理事件 if (!event._propagationImmediateStopped && owner._bubblingListeners) { owner._bubblingListeners.emit(event.type, event); } // 若 事件没有被停止 且 事件需要冒泡处理(默认true) if (!event._propagationStopped && event.bubbles) { //_ Event.BUBBLING_PHASE // 冒泡阶段 // 获得冒泡阶段的节点 owner._getBubblingTargets(event.type, _cachedArray); //_ propagate event.eventPhase = 3; // 从头到尾遍历(实现从父节点到根节点),触发逻辑和捕获阶段一致 for (i = 0; i < _cachedArray.length; ++i) { target = _cachedArray[i]; if (target._bubblingListeners) { event.currentTarget = target; //_ fire event target._bubblingListeners.emit(event.type, event); //_ check if propagation stopped if (event._propagationStopped) { _cachedArray.length = 0; return ; } } } } // 清空_cachedArray _cachedArray.length = 0; } |
不知道看完有没有对事件的触发顺序有更进一步的了解呢?其中对于捕获阶段的节点和冒泡阶段的节点,是通过别的函数来获得的,用捕获阶段的代码来做示例,两者是类似的。
12345678910111213 | _getCapturingTargets (type, array) { // 从父节点开始 var parent = this .parent; // 若 父节点不为空(根节点的父节点为空) while (parent) { // 若 节点有捕获阶段的监听器 且 有对应类型的监听事件,则把节点加到array数组中 if (parent._capturingListeners && parent._capturingListeners.hasEventListener(type)) { array.push(parent); } // 设置节点为其父节点 parent = parent.parent; } }, |
一个自底向上的遍历,将沿途符合条件的节点加到数组中,就得到了所有需要处理的节点!好像有点偏题… 回到刚刚的事件分发,同样,因为不管是捕获阶段的监听器,还是冒泡阶段的监听器,都是一个EventTarget,这边拿自身的触发来做示例。owner._bubblingListeners.emit(event.type, event);
上面这行代码将事件分发到自身节点的冒泡监听器里,所以直接看看emit里是什么。emit其实是CallbacksInvoker里的方法。
callbacks-invoker.js
12345678910111213141516171819202122232425262728293031323334353637383940 | proto.emit = function (key, arg1, arg2, arg3, arg4, arg5) { // 获得事件列表 const list = this ._callbackTable[key]; // 若 事件列表存在 if (list) { // list.isInvoking 事件是否正在触发 const rootInvoker = !list.isInvoking; list.isInvoking = true ; // 获得回调列表,遍历 const infos = list.callbackInfos; for (let i = 0, len = infos.length; i < len; ++i) { const info = infos[i]; if (info) { let target = info.target; let callback = info.callback; // 若 回调函数是用once注册的,那先把这个函数取消掉 if (info.once) { this .off(key, callback, target); } // 若 传递了target,则使用call保证this的指向是正确的 if (target) { callback.call(target, arg1, arg2, arg3, arg4, arg5); } else { callback(arg1, arg2, arg3, arg4, arg5); } } } // 若 当前事件没有在被触发 if (rootInvoker) { list.isInvoking = false ; // 若 含有被取消的回调,则调用purgeCanceled函数,过滤已被移除的回调并压缩数组 if (list.containCanceled) { list.purgeCanceled(); } } } }; |
核心是,根据事件获得回调函数列表,遍历调用,最后根据需要做一个回收。到此为止啦!
结尾
加点有意思的监听器排序算法
前面的内容中,有提到_sortEventListeners函数,用于将监听器按照触发优先级排序,这个算法我觉得蛮有趣的,与君共赏。先理论。节点树顾名思义肯定是个树结构。那如果树中随机取两个节点A、B,有以下几种种特殊情况:
- A和B属于同一个父节点
- A和B不属于同一个父节点
- A是B的某个父节点(反过来也一样)
如果要排优先级的话,应该怎么排呢?令p1 p2分别等于A B。往上走:A = A.parent
- 最简单的,直接比较_localZOrder
- A和B往上朔源,早晚会有一个共同的父节点,这时如果比较_localZOrder,可能有点不公平,因为可能有一个节点走了很远的路(层级更高),应该优先触发。此时又分情况:A和B层级一样。那p1 p2往上走,走到相同父节点,比较_localZOrder即可,A层级大于B。当p走到根节点时,将p交换到另一个起点。举例:p2会先到达根节点,此时,把p2放到A位置,继续。早晚他们会走过相同的距离,此时父节点相同。根据p1 p2的_localZOrder排序并取反即可。因为层级大的已经被交换到另一边了。这段要捋捋,妙不可言。
- 同样往上朔源,但不一样的是,因为有父子关系,在交换走过相同距离后,p1 p2最终会在A或B节点相遇!所以此时只要判断,是在A还是在B,若A,则A层级比较低,反之一样。所以相遇的节点优先级更低。
洋洋洒洒一大堆,上代码,简洁有力!
1234567891011121314151617181920212223242526272829303132333435363738 | // 场景图级优先级监听器的排序算法 // 返回-1(负数)表示l1优先于l2,返回正数则相反,0表示相等 _sortEventListenersOfSceneGraphPriorityDes: function (l1, l2) { // 获得监听器所在的节点 let node1 = l1._getSceneGraphPriority(), node2 = l2._getSceneGraphPriority(); // 若 监听器2为空 或 节点2为空 或 节点2不是活跃状态 或 节点2是根节点 则l1优先 if (!l2 || !node2 || !node2._activeInHierarchy || node2._parent === null ) return -1; // 和上面的一样 else if (!l1 || !node1 || !node1._activeInHierarchy || node1._parent === null ) return 1; // 使用p1 p2暂存节点1 节点2 // ex:我推测是 是否发生交换的意思(exchange) let p1 = node1, p2 = node2, ex = false ; // 若 p1 p2的父节不相等 则向上朔源 while (p1._parent._id !== p2._parent._id) { // 若 p1的爷爷节点是空(p1的父节点是根节点) 则ex置为true,p1指向节点2。否则p1指向其父节点 p1 = p1._parent._parent === null ? (ex = true ) && node2 : p1._parent; p2 = p2._parent._parent === null ? (ex = true ) && node1 : p2._parent; } // 若 p1和p2指向同一个节点,即节点1、2存在某种父子关系,即情况3 if (p1._id === p2._id) { // 若 p1指向节点2 则l1优先。反之l2优先 if (p1._id === node2._id) return -1; if (p1._id === node1._id) return 1; } // 注:此时p1 p2的父节点相同 // 若ex为true 则节点1、2没有父子关系,即情况2 // 若ex为false 则节点1、2父节点相同,即情况1 return ex ? p1._localZOrder - p2._localZOrder : p2._localZOrder - p1._localZOrder; }, |
总结
游戏由CCGame而起,调用CCInputManager、CCEventManager注册事件。随后的交互里,由引擎的回调调用CCEventManager中的监听器们,再到CCNode中对于事件的处理。若命中,进而传递到EventTarget中存储的事件列表,便走完了这一路。模块其实没有到很复杂的地步,但是涉及若干文件,加上各种兼容性、安全性处理,显得多了起来。
以上就是详解CocosCreator系统事件是怎么产生及触发的的详细内容,更多关于CocosCreator系统事件产生及触发的资料请关注米米素材网其它相关文章!
原文链接:https://www.mimisucai.com/teach/javascript/29767.html
关键词:
介绍CocosCreator系统事件是怎么产生及触发的
ChatGPT 会取代程序员吗?揭穿神话 世界观天下
7999元起 国人买iPhone 14 Pro需9.5%年收入引热议:你多久才能买?
外星人在美一户人家后院活动?别信:3D建模软件做的_当前资讯
寻找新的 AI 应用程序和 ChatGPT 工具的前 5 个网站
部署zabbix5_每日资讯
北京通州一业主私家车长期占用消防车通道且拒不整改 遭行政拘留7日
科学家付巧妹获得联合国首位阿勒福赞奖 从进化角度为人类健康问题带来新见解
郑州将于6月份发放5000万元汽车消费券 不限购买者户籍
著名球星贝克汉姆使用中文为中国粉丝献上端午祝福 网友热情回应
微软宣布Xbox Series X和XGP订阅服务将涨价 称其反应市场的竞争情况
美国亿万富翁家庭以91万元年薪招聘住家狗保姆 将狗的幸福放在首位
顺丰旗下大型无人机FH-98顺利降落 最大起飞重量5.25吨
余承东曾预告!中国自动驾驶迎来大进展:官方首次表态L3商用
天天微资讯!《英雄联盟》新英雄纳亚菲利玩法公布 这是真的狗!
为减少内卷 四家头部猪企发起“互不挖人公约”
贵州贵阳周边现奇幻红色湖泊 吸引众多游客前往打卡
第28届白玉兰奖评委见面会举行 《庆余年》导演称应杜绝文盲演员
吉林一市民驾车偶遇“拦路虎” 强调路边遇到老虎不要开车窗或下车
《超级马里奥兄弟 惊奇》公布 预计于本年10月20日发售
我国最长的深水油气管道完工 区域最大作业水深近1000米
辽宁铁岭一位母亲带着高考结束的女儿去整容 当事人提醒勿盲目去跟风
北京宣布对电动自行车进行管理 因含有尖锐的金属杆导致安全系数高
视讯!中国乡村“现象级”体育赛事频现
曾排名国内服装品牌第一:拉夏贝尔正式破产清算 世界快讯
世界今日讯!Java基础复习笔记详细版
xxx.opt-1.pyc、xxx.opt-2.pyc和xxx.pyc的区别-热点在线
每日热议!redis简单介绍和使用
尤文官方:35岁迪马利亚自由身离队,据悉将加盟本菲卡
高考生注意!明天起多地高考出分_世界新要闻
全球微动态丨盘点粽子界“显眼包”:各种奇葩口味粽子层出不穷
订单爆了?日产就奇骏e-POWER发布情况说明:满30天未提车获补偿|每日快报
Go-指针篇 世界滚动
首屈一指的首屈是什么意思(首屈一指的指是哪个指)_最资讯
长白山人参鲜参大促!10支独立包装券后仅39.9:煲汤超营养|报道
【天天新视野】99%人没用过的电视隐藏功能 宅家也能爽翻天
今日热搜:西湖边上卖花郎和卖花姑娘走红 本人:希望学习分享中国传统文化
京东四面面经整理|环球百事通
全球动态:OPEN AI 全新版本在线免费体验2.0.0支持最新对话模型,和16K上下文
学霸王小九防骗视频_学霸征集令 天天滚动
世界热资讯!古人过端午仪式感有多足?非常讲究
全球观天下!大雨中校长毕业典礼致辞:只念了标题 为了让学生少淋雨
宿命还是巧合?泰坦尼克号遇难者后裔丈夫在失踪观光潜艇上
世界视讯!专题速览:成都哪家医院治疗外阴白斑强
正统新作《超级马里奥兄弟惊奇》公布!10月20日发售
植物人父亲帮儿子挠痒引关注 背后故事暖心:为救人遭重创
焦点速递!在AI大爆发的背景下,企业管理软件有什么冲击
世界观焦点:国足未来再添新对手:排名157的也门晋级U17亚洲杯8强!
中国天眼取得重要成果!发现迄今轨道周期最短脉冲双星 最新资讯
天天短讯!会有奇迹吗?参观泰坦尼克号失联潜艇氧气即将耗尽 专家分析两种故障
深圳拟立法规范预付式消费
不限速超良心!阿里云盘iOS 4.8.1版发布:会员能看2K臻彩视频了
世界热消息:腾讯《王者荣耀》衍生游戏《星之破晓》开启预约:已拿到版号
折叠屏比直板手机都便宜!moto razr 40即将开卖:3999元
网友济南街头偶遇韦东奕:90后北大“扫地僧” 数学天才
东京股市小幅上扬_焦点滚动
陈思诚监制 悬疑片《消失的她》上映:打破过去5年端午档累计票房纪录
全球视点!数毛社评《最终幻想16》:体验近乎完美无缺
司机分神开车冲入羊群:撞死20只羊 全球热点评
【宝机产品分享】BL20-HSY高速高精车铣中心
世界资讯:Redis6 的安装
天天快看:关于使用rsync命令小技巧-交互式输入密码后-再置于后台运行的方法
环球快看:端午节将至 请收藏吃粽子的正确打开方式
碾压谷歌、斯坦福大学!CVPR最佳论文颁给中国自动驾驶大模型:近10年首例
环球快讯:女子喊3遍拔针:护士打游戏不理会
斯佩伯爵六级船令人犯难-当前热门
基金价格查询官网(基金价格查询)
超燃震撼!深圳1500架无人机编队:上演“飞龙在天”空中大片-天天速讯
又是电热水器惹祸!深圳一女子洗澡时因触电身亡 世界新视野
不止封杀老头乐!北京拟禁售加装车篷改装座位电动车:很危险
参观泰坦尼克号失联!工程师:失踪潜艇可能会发生坍塌|全球报道
全球热头条丨福特CEO:特斯拉Cybertruck好看不中用、对F-150构不成威胁
国家图书馆借书在哪个馆_国家图书馆借阅规则 热点在线
2-Redis概述_天天新视野
读发布!设计与部署稳定的分布式系统(第2版)笔记08_自黑与放大
天天播报:周鸿祎:好专业比好大学更重要 不会用大语言模型可能会淘汰
今日视点:丰田最贵“大面包”上新!全新一代埃尔法、威尔法上市:89.9万起
华晨破产重整方案敲定:沈阳汽车拟获三家上市公司控制权 环球看热讯
【天天播资讯】Redmi K60 Ultra关键参数敲定:1.5K直屏+天玑9200+芯片
excel怎么筛选重复项并标记颜色_excel怎么筛选重复项 全球看点
每日快报!免费开源压缩神器!7-Zip 23.01正式版发布:仅1.5MB小而美
法国巴黎市区建筑爆炸起火事故已造成29人受伤_微头条
热点聚焦:Go-变量篇
全球消息!乘客坐顺风车猝死 家属索赔127万:判了
胖东来董事长于东来宣布退休:雷军称其为中国零售业神一般存在!
新能源免购置税又延了4年:但这次就不爽了-全球今日讯
1999元!锐龙7000! 这款迷你主机到底值不值
股票行情快报:迈克生物(300463)6月21日主力资金净买入125.34万元
最资讯丨福特小跨界改款测试车曝光,纯电版Puma EV预定明年登场!
中国籍夫妇在日本抓了683只寄居蟹 称不知道其为一般保护动物
成都猎人队现已退出《守望先锋》联赛 其战队皮肤将于7月6日下架
《暗黑破坏神4》官方晒英伟达 为拥有莉莉丝雕塑装饰自定义RTX4080显卡
日本两名男子盗窃约3万3000张游戏王卡牌而被捕 总价值高达3800万日元
《王者荣耀》IP下首款英雄剧情格斗手游揭晓 由腾讯天美工作室群制作
女子吃转转火锅取一半再放回餐盘 商家吐槽不道德:网友看醉_每日观点
《庆余年》导演说演员应该杜绝文盲:演员是学者
1nm以下关键技术 英特尔研发2D芯片工艺_热点在线
视频|他们赶着回家收麦子,图方便竟选择了超员
TiDB简介与应用场景 环球聚看点
树莓酮胶囊减肥有用吗_树莓酮