Cocos Creator 實現點擊透明區穿透的解決方案

[TOC]node

前言

        最近有初學CocosCreator的小夥伴問到一個點擊穿透的問題,正好整理些方案一塊兒來看下。一般在製做課件的過程當中,會遇到點擊或拖拽多邊圖形的需求,不少時候就會不可避免的遇到一個問題:兩個圖形的疊加在了一塊兒。好比A和B兩個Sprite,咱們會發現,層級高的會被點擊,但想點擊下方的B,卻始終或者沒法精準的點到B,這跟cocos自己的點擊機制有關。web

有什麼辦法能夠解決這個問題嗎? 答案確定是有的,接下來就一塊兒看下幾種解決方案。canvas


1、知識準備

       經過修改節點的_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


2、解決方案

方案一:重寫_hitTest

經過修改_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

  • 一、this的做用域,這裏只是爲了讓代碼看上去直觀才這麼幹的。
  • 二、須要重寫onTouchBegan、onTouchMoved、onTouchEnded、onTouchCancelled方法來達到你想要的效果。

       好啦,結合上面的兩組方案基本已經能夠實現"像素級"檢測了,可是你會發現一個問題,若是監聽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;
}

       這裏須要注意的有兩點:

  • 一、適當調整Threshold屬性值,能夠在必定程度上減小計算量。
  • 二、對於過於複雜的圖形,並不使用這套方案,由於。咱們會發現很難去完美的勾勒出全部的點。

3、小結

       節點Sprite的SpriteFrame若是源於圖集,會存在觸摸或點擊不精準和可觸發區域異常的問題,由於若是啓用了動態合圖功能,動態合圖會自動將合適的貼圖在開始場景時動態合併到一張大圖上來減小 drawcall,同時會將貼圖合併到大圖中會修改原始貼圖的 uv 座標。在isTransparency()中的返回份量w來做爲判斷依據的方法就會存在誤差。

       到此,Cocos Creator點擊透明區穿透的解決方案也都一一講解完了,根據本身的理解和需求來有選擇地使用吧,若是有其餘的解決方案也歡迎提出來一塊兒學習。

相關文章
相關標籤/搜索