cocos2d 多边形触摸检测

业务上会遇到这么一种情况,不规则图片需要添加触摸事件,这个时候点击图片的空白区域也会有触摸事件进来,这个时候如果我想只在有像素的地方点击有事件进来,那么要怎么处理呢?比如下图:

找了一下资料有2种方法:

方案一:射线法(Ray Casting Algorithm)- 通用性最强

这是最经典且适用于任意多边形(凸多边形和凹多边形)的算法。其原理是从触摸点向任意方向(通常向右)发射一条射线,统计射线与多边形边相交的次数。如果相交次数为‌奇数 ‌,则点在多边形内;如果是‌偶数‌,则在多边形外。

方案二:像素级检测(Alpha 检测)- 适合复杂异形图片

如果形状极其不规则且难以用多边形描述,可以通过读取图片像素的 Alpha 值来判断。如果触摸点对应的像素 Alpha > 0,则视为击中。

注意 ‌:此方法性能开销较大,不建议在每帧或大量对象中使用,通常用于静态按钮。需要修改 C++ 底层或使用 RenderTexture 获取像素数据,纯 Lua 实现较困难且效率低。一般建议优先使用多边形近似。

那么这里只讲一下方案一的实现逻辑

1.先用DrawNode画多边形,前提需要把顶点坐标找到。

2.用射线法检测触摸点是否在多边形内。

可以看一下我画的图:

顶点数越多一般就和多边形越贴合,我本来3个顶点就够了,只是为了试试而已,不止如此我还试了凹多边形,虽然drawNode不支持渲染凹多边形,只支持凸多边形,但不影响射线法的判断。

至于这个射线法我也去研究了一下

一、核心原理

射线法的核心逻辑是‌交点数奇偶性判定 ‌:从待判断点向任意方向(通常选水平向右)引一条无限延伸的射线,统计该射线与多边形所有边的交点总数。若交点数为奇数,说明点在多边形内部;若为偶数,则点在多边形外部。

背后的拓扑逻辑是:射线每穿越一次多边形边界,点的内外状态就会翻转一次,从初始的外部(0次,偶数)经过奇数次翻转后,最终会停留在内部区域。

二、关键前提

多边形的顶点必须按顺时针或逆时针的顺序环绕排列,且首尾顶点闭合,否则算法会直接失效。该方法天然支持凹多边形,也可适配带孔多边形(外环用射线法判定为内部后,再对每个内环单独判定,点落在任意内环内则最终结果为外部)。

三、特殊情况处理

  1. 射线刚好穿过多边形顶点:通过规则过滤,仅统计边的纵坐标较大的上端点作为有效交点,避免重复计数导致结果错误。
  2. 射线与多边形某条边完全重合:直接跳过该水平边,避免无效计算和除零错误。
  3. 点落在多边形的边上:可在主逻辑外单独增加点在线段上的判断,根据业务需求将其判定为内部或外部。

下面贴一下测试代码:

Lua 复制代码
local test = class("test", function()
	return display.newNode()
end)

function test:ctor(param)
    self:setTouchEnabled(true)
	self:setNodeEventEnabled(true)
	self:setTouchSwallowEnabled(true)

	self:initUI()
end

function test:initUI()
    self:setContentSize(cc.size(display.width,display.height))
    local colorLayer = cc.LayerColor:create(cc.c4b(255,255,255,255))
        :addTo(self)
        :align(display.CENTER,0,0)
        :size(display.width*1.5,display.height*1.5)


    self.sprite = display.newSprite("#xh_role_02_lock.png")
        :addTo(self)
        :align(display.CENTER,display.cx,display.cy)



    self.verts = {
                    cc.p(-230, -170), -- 顶点 A
                    cc.p(350, 180),  -- 顶点 B
                    cc.p(-385, 180),  -- 顶点 C
                    cc.p(-250, 50),  -- 顶点 D
                  }

    local drawNode = cc.DrawNode:create()
    self:addChild(drawNode) -- 假设 self 是当前 Layer 或 Scene
    drawNode:setPosition(self.sprite:getPositionX(),self.sprite:getPositionY())
    self.drawNode = drawNode
    --self.drawNode:hide()

    -- 2. 定义顶点 (顺时针或逆时针均可,建议保持一致)
    local verts = self.verts

    -- 3. 定义颜色
    local fillColor = cc.c4f(1, 0, 0, 1)   -- 半透明红色填充 cc.c4f(1, 0, 0, 0.5)
    local borderColor = cc.c4f(0, 0, 0, 1)   -- 黑色边框 cc.c4f(0, 0, 0, 1)
    local borderWidth = 2                    -- 边框宽度

    -- 4. 绘制多边形
    drawNode:drawPolygon(self.verts,{fillColor = fillColor, borderWidth = borderWidth, borderColor = borderColor})
   
    
    self:setTouchEnabled(true)
    self:setTouchSwallowEnabled(true)
    self:addNodeEventListener(cc.NODE_TOUCH_EVENT, handler(self, self.onTouch))
end

function test:onTouch(event)
    local name,x,y = event.name, event.x, event.y

    if name == "began" then
        return true
    elseif name == "moved" then

    elseif name == "ended" then
        local location = cc.p(x,y)

        local localPos = self.drawNode:convertToNodeSpace(location)
        if self:isPointInPolygon(localPos, self.verts) then
            print(">>> 触摸点在多边形内部!")
        else
            print(">>> 触摸点在多边形外部。")
        end
        
    end
end

function test:isPointInPolygon(pos, points)
    local nCross = 0 -- 交点计数器
    local count = #points
    
    for i = 1, count do
        local p1 = points[i]
        local p2 = points[(i % count) + 1] -- 下一个点,最后一个点连回第一个点
        
        -- 求解 y=p.y 与线段 p1p2 的交点
        -- 1. 判断线段是否跨越水平线 p.y
        -- 如果两点都在水平线同侧,则不相交
        if (p1.y > pos.y and p2.y <= pos.y) or (p1.y <= pos.y and p2.y > pos.y) then
            
            -- 2. 计算交点的 x 坐标
            -- 直线方程两点式: (x - x1)/(x2 - x1) = (y - y1)/(y2 - y1)
            -- 推导 x: x = x1 + (y - y1) * (x2 - x1) / (y2 - y1)
            local xIntersect = p1.x + (pos.y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y)
            
            -- 3. 判断交点是否在测试点的右侧
            -- 如果交点在右侧,说明射线穿过了一条边
            if xIntersect > pos.x then
                nCross = nCross + 1
            end
        end
    end
    
    -- 如果交点数为奇数,则在多边形内;偶数则在外部
    return (nCross % 2 == 1)
end

return test

判断交点的时候判断的是右侧,其实左侧也是一样的,拿上纸笔试试看,还真是那么回事,但这个方法也是有弊端的:

  1. 特殊边界判定复杂
    当射线恰好经过多边形顶点、与多边形边重合时,常规奇偶计数逻辑会失效,必须额外编写代码处理这类临界情况,否则会直接出现检测错误。
  2. 性能开销随顶点数上升
    算法需要遍历多边形的所有边做相交判断,若多边形顶点数量多,单次检测的计算量会明显增加,大量对象同时检测时容易出现性能卡顿。
  3. 无法直接处理非简单多边形
    对于存在自相交的复杂多边形,射线法的奇偶计数规则会完全失效,不能直接得到正确的点内外判定结果。
  4. 点在边上的判定冗余
    射线法本身无法直接识别点落在多边形边上的情况,必须额外单独编写逻辑做前置判断,增加了代码的复杂度。