[TOC]node
最近有初学CocosCreator的小伙伴问到一个点击穿透的问题,正好整理些方案一块儿来看下。一般在制做课件的过程当中,会遇到点击或拖拽多边图形的需求,不少时候就会不可避免的遇到一个问题:两个图形的叠加在了一块儿。好比A和B两个Sprite,咱们会发现,层级高的会被点击,但想点击下方的B,却始终或者没法精准的点到B,这跟cocos自己的点击机制有关。web
有什么办法能够解决这个问题吗? 答案确定是有的,接下来就一块儿看下几种解决方案。canvas
经过修改节点的_hitTest函数便可快速的达到咱们想要的效果,对于刚接触cocos的开发者来讲,可能对这个不是很熟悉,由于官方本来也没有直接暴露这个方法给外部使用,算是私有方法。为了让你们更清晰的了解咱们接下来要说的内容,仍是先把_hitTest函数作一下讲解,在CCNode.js文件中咱们能够找到相关代码段(官方源码是没有注释的):编辑器
// 使用_hitTest的地方(省略了部分代码段) var _touchStartHandler = function (touch, event) { ... if (node._hitTest(pos, this)) { ... return true; } return false; }; ... /** * @param point 触发的坐标点位置 * @param listener 节点自己 */ _hitTest (point, listener) { let w = this._contentSize.width, h = this._contentSize.height, cameraPt = _vec2a, testPt = _vec2b; // 获取节点所在的第一个摄像机 let camera = cc.Camera.findCamera(this); if (camera) { // 将一个摄像机坐标系下的点转换到世界坐标系下 camera.getCameraToWorldPoint(point, cameraPt); } else { cameraPt.set(point); } // 更新世界坐标矩阵 this._updateWorldMatrix(); // 逆矩阵赋值计算, 返回的是下面要用到的_mat4_temp math.mat4.invert(_mat4_temp, this._worldMatrix); // 变换矩阵赋值计算,返回的是计算后的testPt math.vec2.transformMat4(testPt, cameraPt, _mat4_temp); // 根据锚点和宽高计算出须要检测的点的xy值 testPt.x += this._anchorPoint.x * w; testPt.y += this._anchorPoint.y * h; // 检测点是否在node节点的区域内 if (testPt.x >= 0 && testPt.y >= 0 && testPt.x <= w && testPt.y <= h) { if (listener && listener.mask) { // 若是用到mask,会在其父节点进行推算 var mask = listener.mask; var parent = this; for (var i = 0; parent && i < mask.index; ++i, parent = parent.parent) { } // find mask parent, should hit test it 如备注所言 if (parent === mask.node) { var comp = parent.getComponent(cc.Mask); return (comp && comp.enabledInHierarchy) ? comp._hitTest(cameraPt) : true; } // mask parent no longer exists else { listener.mask = null; return true; } } else { // 很显然,多数状况下咱们是不会使用mask的,一般会走到这里 return true; } } else { // 不在区域内,则返回false return false; } }
查看源码会发现,_hitTes函数在触摸和鼠标事件回调函数中基本都有用到。关于_hitTest具体实现,大部分我已经在代码段中作了注释来加以解释。代码段中矩阵变换相关的知识之后我会在WebGL相关知识讲解里面会提到,到时再一块儿探讨,这里你们只须要知道矩阵计算在此处有用到便可,喜欢深究的同窗能够自行查看相关代码段。ide
经过修改_hitTest函数的断定,来达到咱们想要的"像素级"检测。函数
// 启用透明区检测 useTransparencyCheck() { this.node._hitTest = this.hitTest.bind(this); } /** * point : 鼠标点击的坐标 */ hitTest(point) { // 坐标转换 let hitPos = this.node.convertToNodeSpace(point); // 获取节点尺寸 let nodeSize = this.node.getContentSize(); // 矩形区域判断 var rect = cc.rect(0, 0, nodeSize.width, nodeSize.height); if(!rect.contains(hitPos)) return false; // 获取Sprite节点 let sprite = this.node.getComponent(cc.Sprite); if(sprite) { var image = sprite.spriteFrame.getTexture().getHtmlElementObj(); if(this.isTransparency(image, hitPos.x, nodeSize.height - hitPos.y)) { return true }else { return false; } } return false; } // 判断 isTransparency(img, x, y) { var cvs = document.createElement("canvas"); var ctx = cvs.getContext('2d'); cvs.width = 1; cvs.height = 1; ctx.drawImage(img,x,y,1,1,0,0,1,1); var imgdata = ctx.getImageData(0,0,1,1); return imgdata.data[3]; // 第三个份量来判断,webgl经常使用来判断点击的手法 } start() { // 测试:方案一没有改动其余地方,正常的使用以下监听方式便可 this.node.on(cc.Node.EventType.TOUCH_END,()=>{ cc.log("node name:",this.node.name); }); this.useTransparencyCheck(); }
在获取节点尺寸那里,要注意一点,假设有一个正方形,实际渲染尺寸为5050,但图片尺寸为10050,左右分别多出了25像素的透明区,那么会对接下来的操做产生影响,点击区域发生偏移。解决方法也是有的,咱们能够来编写this.node._getLocalBounds函数,经过实现该方法以提供自定义的轴向对齐的包围盒(AABB),以便编辑器的场景视图能够正确地执行点选测试。一般的办法是本身或找美术老师用PS把图片多余的透明区剪裁掉便可。学习
判断部分同方案一,区别在于使用了cc.EventListener.TOUCH_ONE_BY_ONE事件和吞没事件的断定变量swallowTouches,实现点击事件的向下传递。当swallowTouches = true时,事件不会向下传递,反之,事件会依次向下传递。测试
useOneByOneCheck() { let self = this; this.hitListenerCallBack = cc.eventManager.addListener({ event: cc.EventListener.TOUCH_ONE_BY_ONE, onTouchBegan: function (touch, event) { if(self.hitTest(touch.getLocation())) { this.swallowTouches = true; return true; }else { this.swallowTouches = false; } return false; }, onTouchMoved: function (touch, event) { // cc.log('onTouchMoved: ' + self.node.name); }, onTouchEnded: function (touch, event) { cc.log('onTouchEnded: ' + self.node.name); }, onTouchCancelled: function (touch, event) { // cc.log('onTouchCancelled: ' + self.node.name); } }, this.node); } // 注意:若是只是拷贝粘贴代码进行测试,记得把前面start()函数中的this.node.on注释掉,否则不会走到你重写的onTouchXXX里面。
这里须要注意的有两点:webgl
好啦,结合上面的两组方案基本已经能够实现"像素级"检测了,可是你会发现一个问题,若是监听touch事件的节点过多,就会出现较为明显的效率问题,由于每个监听touch时间的节点,都会走一遍hitTest和isTransparency两个函数,那么还有没有更优一些的方案呢?必然是有的,接下来咱们一块儿看下。this
使用碰撞系统中的Collider组件来绘制咱们想要的区域,这里用能够处理多边形的PolygonCollider组件来作方案演示,代码段部分很简单,只须要在咱们前面提到的hitTest方法中添加以下代码便可(代码中已添加备注,直接上完整代码段):
hitTest(point) { // 坐标转换 let hitPos = this.node.convertToNodeSpace(point); // 获取节点尺寸 let nodeSize = this.node.getContentSize(); // 扩展:对碰撞系统的支持 let polygonCollider = this.getComponent(cc.PolygonCollider); if (polygonCollider) { hitPos.x -= nodeSize.width / 2; hitPos.y -= nodeSize.height / 2; // console.log("碰撞组件的点击测试") return cc.Intersection.pointInPolygon(hitPos, polygonCollider.points); } // 矩形区域判断 let rect = cc.rect(0, 0, nodeSize.width, nodeSize.height); if(!rect.contains(hitPos)) return false; // 获取Sprite节点 let sprite = this.node.getComponent(cc.Sprite); if(sprite) { var image = sprite.spriteFrame.getTexture().getHtmlElementObj(); if(this.isTransparency(image, hitPos.x, nodeSize.height - hitPos.y)) { return true }else { return false; } } return false; }
这里须要注意的有两点:
节点Sprite的SpriteFrame若是源于图集,会存在触摸或点击不精准和可触发区域异常的问题,由于若是启用了动态合图功能,动态合图会自动将合适的贴图在开始场景时动态合并到一张大图上来减小 drawcall,同时会将贴图合并到大图中会修改原始贴图的 uv 坐标。在isTransparency()中的返回份量w来做为判断依据的方法就会存在误差。
到此,Cocos Creator点击透明区穿透的解决方案也都一一讲解完了,根据本身的理解和需求来有选择地使用吧,若是有其余的解决方案也欢迎提出来一块儿学习。